NLP · EDA · PyTorch BiLSTM · FastAPI · Jinja · SQLite · Docker

Disaster Twitts: From Kaggle Notebook to First Professional ML Deployment

A complete disaster tweet classification project: broad notebook EDA, text signal analysis, GloVe-backed PyTorch sequence modeling, threshold tuning, artifact export, FastAPI JSON serving, Jinja frontend, SQLite prediction logging, and Docker Compose deployment.
Research Lab Kaggle Competition
Labeled Tweets
0
Disaster Class Share
0
Test Tweets
0
Token Vocabulary
0
Tuned Validation F1
0
Decision Threshold
0
Four-Part Project Strategy

Research to Deployment Map

The single page is organized around the real lifecycle of the project: understand text, train the model, serve predictions, and package the app.

Notebook 01 · EDA
Text Signals

Reads the Kaggle data and studies class balance, missingness, keywords, tweet length, words, n-grams, hashtags, sentiment, danger lexicons, and POS ratios.

Notebook 02 · PyTorch Model
BiLSTM

Builds a reusable text pipeline, trains a GloVe-backed BiLSTM classifier, tracks validation metrics, tunes the threshold, and exports artifacts.

Application Layer
FastAPI + Jinja

Loads artifacts during app startup and exposes the same prediction function through JSON API and browser form workflows.

Runtime Layer
SQLite + Docker

Persists prediction context in SQLite and packages the app with pinned dependencies, healthcheck, environment settings, and a Compose volume.

Notebook 01 · Exploratory Analysis

EDA Scope

The EDA notebook is a full language-and-metadata investigation, not a quick class-count check.

The notebook reads the Kaggle disaster tweet training data and studies the dataset from several angles: target balance, missing metadata, keyword reliability, text length, common words, wordclouds, bigrams/trigrams, class-specific n-grams, structural tweet features, hashtags, raw unique tokens, VADER sentiment, danger lexicons, and part-of-speech ratios. The purpose is to understand what the model should preserve, normalize, or ignore before training.

Dataset fieldsid, keyword, location, text, and binary target.
EDA conclusionkeyword is useful, location is too sparse/noisy, and raw Twitter artifacts need careful normalization.
Model handoffThe final model keeps word order and context while using EDA findings to define cleaning, keyword merging, validation, and serving warnings.
Pandas + NumPyLoad train/test CSVs, compute missingness, class counts, text statistics, grouped summaries, and notebook tables.
Matplotlib + SeabornRender target distribution, keyword probability, length distributions, structural feature means, sentiment, lexicon, and POS plots.
NLTK + VADERProvide stopwords, tokenization/POS tagging, and sentiment scores for checking whether emotional tone separates disaster from normal tweets.
WordCloud + CountVectorizerCreate qualitative wordclouds and quantitative bigram/trigram/class-specific n-gram signals.
EDA Reasoning

Text Signal Theory

The EDA notebook treats text as overlapping signals: topic, structure, sentiment, and ambiguity.

Class-Conditional Keyword Signal
EDA
$$P(y=1\mid keyword)=\frac{\sum_i \mathbb{1}[keyword_i=k]y_i}{\sum_i \mathbb{1}[keyword_i=k]}$$
The keyword field is not merely metadata. For terms such as derailment, debris, wreckage, outbreak, typhoon, and oil spill, the class-conditional disaster probability is high enough to justify merging keyword into the final model text.
Log-Ratio N-Grams
Phrase signal
$$score(g)=\log\frac{count(g,y=1)+1}{count(g,y=0)+1}$$
The notebook compares phrases by class instead of only counting popular terms. This separates true disaster phrases such as suicide bomber and california wildfire from misleading social phrases such as body bag or cross body.
Structural Tweet Features
Metadata
has_url, has_hashtag, has_mention, upper_ratio, punct_ratio
The EDA measures how tweets are written, not only what they say. Disaster tweets contain URLs more often, while normal tweets contain more mentions and questions. These checks explain why URL, mention, punctuation, and number normalization matter before modeling.
VADER Sentiment Probe
Sentiment
neg, neu, pos, compound = sia.polarity_scores(text)
VADER is used as an interpretable sentiment probe. Disaster tweets are more negative on average, but the overlap is large, so sentiment is treated as insight about the dataset rather than a standalone classifier.
Danger Lexicon Check
Semantic cues
death, injury, emergency, violence, natural_disaster
Hand-built lexicon groups test whether explicit emergency vocabulary separates the classes. It works strongly for natural disaster and violence terms, but also exposes false positives where dramatic language is not a real disaster report.
POS Ratio Analysis
Syntax
$$noun\_ratio=\frac{\#NN}{\#tokens},\qquad verb\_ratio=\frac{\#VB}{\#tokens}$$
Part-of-speech ratios help describe reporting style. Disaster tweets show a higher noun ratio, which matches event reports that name locations, incidents, objects, and organizations.
Dataset Diagnostics

Target, Missingness & Metadata Quality

Class balance is acceptable, but metadata quality is uneven: keyword is mostly present, location is not.

4,342
Normal Tweets
3,271
Disaster Tweets
221
Unique Keywords
3,341
Unique Locations
Target Distribution
Target distribution pie chart
CheckNotebook OutputDecision
Missing text0 missing values.Text is the stable primary input.
Missing keyword61 rows, about 0.80%.Keep keyword when present and merge with text.
Missing location2,533 rows, about 33.27%.Drop location for the deployed model.
Class split57.03% normal, 42.97% disaster.Track precision, recall, and F1, not accuracy alone.
Keyword Intelligence

Keyword Risk Signal

The keyword field is sparse only in a tiny fraction of rows and often has strong target meaning.

Top Keywords by Disaster Probability
Top keywords by disaster probability
KeywordCountDisaster Ratio
derailment391.000
debris371.000
wreckage391.000
outbreak400.975
typhoon, oil spill380.974
keyword_stats = (
    df.dropna(subset=["keyword"])
      .groupby("keyword")["target"]
      .agg(["count", "mean"])
      .sort_values("mean", ascending=False)
)

top_keywords = keyword_stats.head(10)
Text Geometry

Length, Edge Cases & Structural Shape

Tweet length and structure are useful diagnostics, but the model still needs contextual sequence learning.

101.0
Mean Characters
15
Median Words
108.1
Disaster Mean Chars
95.7
Normal Mean Chars
Message Length
Message length histogram
Length by Label
Message length by label
Char and Word Counts
Character and word count histograms by label
Char vs Word Scatter
Character versus word count scatter
Linguistic Feature Means
Linguistic feature means by target
Hashtag Count Boxplot
Hashtag count boxplot
URLsDisaster tweets contain URLs more often: 0.664 vs 0.414.
MentionsNormal tweets contain mentions more often: 0.309 vs 0.204.
Questions/exclamationsNormal tweets ask more questions and use more exclamation marks; disaster text is often more report-like.
Vocabulary Analysis

Words, WordClouds & N-Grams

The notebook checks both raw frequency and class-specific phrases to separate real disaster language from noisy social language.

Disaster Common WordsCountNormal Common WordsCount
fire151like250
suicide103new163
disaster97get161
police94body106
killed92love85
Class-Specific N-GramSignalInterpretation
suicide bomber+4.09Strong disaster phrase.
northern california+3.74Wildfire/reporting context.
california wildfire+3.56Event-specific phrase.
cross body-3.00Fashion/product false alarm.
body bag-2.64Ambiguous phrase, often non-disaster.
Top Disaster Words
Top disaster words
Top Normal Words
Top normal words
Disaster WordCloud
Disaster wordcloud
Normal WordCloud
Normal wordcloud
Top Disaster N-Grams
Top disaster ngrams
Top Non-Disaster N-Grams
Top non-disaster ngrams
Semantics Beyond Counts

Hashtags, Sentiment, Danger Lexicons & POS

The notebook tests whether semantic cues are clean enough to trust, then identifies where they overlap.

VADER Sentiment
VADER sentiment distribution
Danger Keyword Presence
Danger keyword presence by label
POS Ratios
POS ratios by target
AnalysisNotebook ResultInterpretation
SentimentDisaster compound mean -0.265 vs normal -0.061.Disaster text is more negative on average, but overlap is too large for sentiment alone.
Danger lexiconsNatural-disaster terms appear in 23.8% of disaster tweets vs 7.8% of normal tweets.Explicit danger words are useful but not complete; some real disasters lack them.
POS ratiosDisaster tweets have higher noun ratio, 0.449 vs 0.384, and lower verb ratio.Real reports often name places, objects, events, and entities.
HashtagsDisaster hashtags include news, hiroshima, earthquake, breaking.Hashtags can carry topic signal but also marketing/noise.
All Notebook Visuals

Complete EDA Figure Gallery

All exported EDA plots are included here so the page reflects the real notebook work.

Notebook 02 · PyTorch Modeling

Training Pipeline

The training notebook moves from cleaned text to deployable BiLSTM artifacts.

The modeling notebook is not just a single model call. It defines configuration, seed control, preprocessing, keyword fusion, vocabulary creation, JSON artifact export, dataset encoding, DataLoader batching, GloVe embedding loading, BiLSTM construction, weighted BCE loss, torchmetrics metrics, early stopping, freeze/unfreeze experiments, training history plots, threshold tuning, and final config export.

01Config + seed

Seed 28, CUDA when available, reproducible paths, model constants, and training flags.

02Text pipeline

Clean tweet and keyword, drop location, build final_text.

03Sequence data

Vocabulary, pad/unknown tokens, max length 200, DataLoader batches.

04Artifacts

Model checkpoint, threshold, vocab JSON, label mapping, history, config.

PyTorchDefines the dataset, embedding layer, BiLSTM classifier, weighted BCE loss, optimizer, checkpointing, and inference-compatible tensors.
torchmetricsTracks train accuracy plus validation accuracy, precision, recall, and F1 during each epoch.
scikit-learnCreates the train/validation split and supports post-training threshold selection on validation probabilities.
GloVe + JSON/YAML artifactsLoads pretrained 100d word vectors and exports vocabulary, label mapping, threshold, training history, and runtime config.
Text to Tensor

Cleaning, Vocabulary & Dataset Encoding

The model receives fixed-length integer sequences produced from the same cleaning assumptions later used by the API.

def clean_text(text, config):
    if config.LOWERCASE:
        text = text.lower()
    text = re.sub(r"https?://\S+|www\.\S+", " <URL> ", text)
    text = re.sub(r"&", " and ", text)
    text = re.sub(r"@\w+", " <USER> ", text)
    text = re.sub(r"%20", " ", text)
    text = re.sub(r"[^\x00-\x7F]+", " ", text)
    text = re.sub(r"!{2,}", " !! ", text)
    text = re.sub(r"\?{2,}", " ?? ", text)
    text = re.sub(r"\b\d+\b", " <NUM> ", text)
    return re.sub(r"\s+", " ", text).strip()
def final_text_with_keyword(row, config):
    keyword = clean_text(row["keyword"], config) if pd.notna(row["keyword"]) else ""
    cleaned_text = clean_text(row["text"], config)

    if config.USE_KEYWORD and keyword:
        return f"{keyword} : {cleaned_text}"
    return cleaned_text

df = preprocess_df(df, cfg)
df_train, df_val = train_test_split(df, train_size=0.8, random_state=28)
ComponentValueRole
Vocabulary10,000 tokensBuilt from training text and exported as vocabs.json.
Special tokens<PAD>=0, <UNK>=1Stable padding and fallback handling for unseen words.
Max length200 tokensFixed tensor shape for DataLoader and deployment.
Batch shape[128, 200]Notebook verified batched input IDs and label shape.
Unknown ratio0.0057Vocabulary covers the training corpus well.
Model Architecture

Frozen GloVe BiLSTM

The architecture balances contextual sequence modeling with a deployment-friendly parameter footprint.

DisasterTwittsClassifier(
  (embedding): Embedding(10000, 100)
  (lstm): LSTM(
      input_size=100,
      hidden_size=64,
      num_layers=2,
      batch_first=True,
      dropout=0.28,
      bidirectional=True
  )
  (dropout): Dropout(p=0.28)
  (fc): Linear(in_features=128, out_features=1)
)
Design ChoiceNotebook SettingPurpose
Embedding sourceGloVe 6B, 100dPretrained word semantics for short text.
Embedding modeFrozen first experimentStable baseline and lower overfitting risk.
Unfreeze testEmbedding unfrozen after epoch 5Second experiment to test fine-tuning embeddings.
Classifier head128 to 1 linear layerConcatenates forward/backward hidden states.
LossWeighted BCEWithLogitsLossAccounts for class imbalance during optimization.
Binary Logit Objective
BCE
$$\mathcal{L}=-w_y\left[y\log(\sigma(z))+(1-y)\log(1-\sigma(z))\right]$$
The classifier produces one logit for each tweet. Weighted BCEWithLogitsLoss lets the notebook compensate for the normal/disaster class imbalance while keeping the numerically stable logit formulation.
Tuned Decision Rule
F1
$$p=\sigma(f_\theta(x)), \qquad \hat{y}=\mathbb{1}[p \geq 0.52]$$
After training, the notebook sweeps thresholds on validation predictions. The saved threshold near 0.52 improves F1 compared with blindly using the default 0.50 cutoff.
Training Loop

Metrics, Early Stopping & Results

The notebook monitors loss, accuracy, precision, recall, and F1, then tunes the classification threshold after training.

Training Dynamics
Training dynamics chart
0.5490
Best Val Loss
0.7971
Best Val Accuracy
0.7588
Best Epoch F1
0.7609
Best Threshold F1
train_metrics = {"accuracy": BinaryAccuracy(threshold=0.5)}
val_metrics = {
    "accuracy": BinaryAccuracy(threshold=0.5),
    "precision": BinaryPrecision(threshold=0.5),
    "recall": BinaryRecall(threshold=0.5),
    "f1": BinaryF1Score(threshold=0.5),
}

criterion = nn.BCEWithLogitsLoss(pos_weight=pos_class_weight)
optimizer = optim.Adam(model.parameters(), lr=3e-4)

best_f1, best_t = find_best_threshold(model, val_dl)
torch.save(best_t, "best_threshold.pt")
Best checkpoint logicThe notebook saves best checkpoints during training and exports history for later review.
Threshold tuningThe best validation F1 is reached near threshold 0.52, so deployment uses the tuned boundary instead of blindly using 0.50.
Portable outputsThe handoff includes best_model.pt, best_threshold.pt, vocabs.json, label_mapping.json, training_history.json, and config.yaml.
Scripted Research

Notebook Logic Formalized as Modules

The project also has a script/module layout, which makes the training work reusable outside notebook cells.

def main():
    CFG = TrainConfig()
    seed_everything(CFG.SEED)

    df = pd.read_csv(CFG.TRAIN_CSV)
    df = preprocess_df(df, CFG)

    train_df, val_df = train_test_split(
        df,
        train_size=CFG.TRAIN_TEST_SPLIT,
        stratify=df["target"],
        random_state=CFG.SEED,
        shuffle=True,
    )

    vocab = build_vocab(train_df["final_text"], CFG)
    save_vocabs(vocab, CFG.VOCAB_PATH)

    train_loader = DataLoader(DisasterDataset(train_df, vocab, CFG), batch_size=128, shuffle=True)
    val_loader = DataLoader(DisasterDataset(val_df, vocab, CFG), batch_size=128, shuffle=False)

    embedding_matrix = load_embedding(CFG.GLOVE_PATH, vocab, CFG.EMB_DIM)
    model = DisasterTwittsClassifier(..., embedding=embedding_matrix).to(CFG.DEVICE)
Deployment 01 · Application Serving

FastAPI Runtime

The deployment is a real app layer: lifecycle loading, prediction API, Jinja UI, validation, model inference, and error handling.

The FastAPI app loads artifacts once during lifespan startup, stores model/predictor/vocabulary/database objects in app.state, then serves both machine-readable and human-facing prediction flows. The same run_prediction function powers the JSON API and the Jinja form, so UI and API behavior do not drift apart.

FastAPI + UvicornServe the JSON prediction endpoint, health route, logs route, Swagger docs, and browser form route at port 8000.
Pydantic + custom AppError classesDefine request/response contracts and return structured validation, artifact, model, and inference errors.
Jinja2 + python-multipart + StaticFilesRender the form UI, process browser form posts, and serve the deployment app CSS.
PyTorch CPU + langdetectRun the exported BiLSTM checkpoint in production mode and warn or reject inputs whose language does not match the English training data.
01Lifespan

Validate artifacts, load vocab/model/threshold, initialize database.

02Validate

Clean input, check empty text, short text, long text, language issues.

03Infer

Tokenize, pad, run BiLSTM with lengths, apply sigmoid and threshold.

04Respond

Return JSON or render Jinja, then log request and result.

Routes

API and UI Contract

Five routes cover health, UI rendering, JSON inference, form inference, and prediction history.

RouteInterfacePurpose
GET /Jinja pageRender tweet form, result area, warnings, and error display.
GET /healthJSONSimple service readiness check.
POST /predictJSON APIAccept tweet and optional keyword; return prediction payload.
POST /predict-uiHTML formRun prediction from browser UI and re-render the result.
GET /logsJSON listRead recent SQLite prediction history with a bounded limit.
class PredictRequest(BaseModel):
    tweet: str = Field(..., min_length=1)
    keyword: str | None = None

class PredictResponse(BaseModel):
    probability: float
    label: int
    label_name: str
    threshold: float
    warnings: list[dict]
@app.post("/predict", response_model=PredictResponse)
def predict(req: PredictRequest):
    return run_prediction(tweet=req.tweet, keyword=req.keyword)

@app.post("/predict-ui", response_class=HTMLResponse)
def predict_ui(request: Request, tweet: str = Form(...), keyword: str = Form(default="")):
    result = run_prediction(tweet=tweet, keyword=keyword.strip() or None)
    return templates.TemplateResponse("index.html", {...})
Lifespan & Artifacts

Startup Loads the Model Once

The app checks critical files, builds runtime state, and keeps request-time inference lightweight.

@asynccontextmanager
async def lifespan(app: FastAPI):
    config = AppConfig()
    seed_everything(config.SEED)

    if not config.VOCAB_PATH.exists():
        raise ArtifactError("VOCAB_NOT_FOUND", "Vocabulary file not found", {"path": str(config.VOCAB_PATH)})
    if not config.MODEL_PATH.exists():
        raise ArtifactError("MODEL_NOT_FOUND", "Model file not found", {"path": str(config.MODEL_PATH)})

    word2idx, idx2word, vocab_size = load_vocabs(config.VOCAB_PATH)
    predictor = DisasterTwittsPredictor.from_config(config, vocab_size)
    predictor.load_label_mapping(config.LABEL_MAPPING_PATH)
    db_conn = connect_db(config.DB_PATH)
    init_db(db_conn)

    app.state.config = config
    app.state.word2idx = word2idx
    app.state.predictor = predictor
    app.state.db = db_conn
    yield
    db_conn.close()
Artifact safetyMissing model/vocab files become structured app errors instead of silent failure.
Runtime configAppConfig mirrors training dimensions: embedding 100, hidden 64, two layers, dropout 0.28, max length 200.
Request speedModel, vocabulary, threshold, label mapping, and database connection are prepared before requests arrive.
Prediction Core

Validation, Tokenization & Inference

The API does the production work around the model: safe validation, consistent preprocessing, tensor conversion, prediction, and warnings.

def validate(self, tweet, keyword=None):
    warnings = []
    final_text = build_final_text(tweet, keyword=keyword, config=self.config)

    if is_empty_text(final_text):
        raise InputError("EMPTY_TWEET", "Tweet became empty after cleaning.")

    tokens = final_text.split()
    if len(tokens) < 3:
        warnings.append(self._warning("SHORT_INPUT", "Input is very short."))
    if len(tokens) > self.config.MAX_LENGTH:
        warnings.append(self._warning("TRIMMED_LENGTH", "Input was trimmed."))

    return final_text, warnings
@torch.no_grad()
def predict(self, input_ids, input_length, return_label_name=True):
    logits = self.model(input_ids, lengths=input_length)
    prob = torch.sigmoid(logits).item()
    label = 1 if prob >= self.threshold else 0
    label_name = self.label_mapping.get(label, str(label))
    return prob, label, label_name
def run_prediction(tweet: str, keyword: str | None = None) -> PredictResponse:
    final_text, warnings = validator.validate(tweet, keyword=keyword)
    input_ids, input_length = tokenize_and_pad(final_text, word2idx, config)
    prob, label, label_name = predictor.predict(input_ids, input_length, return_label_name=True)

    response = PredictResponse(
        probability=prob,
        label=label,
        label_name=label_name,
        threshold=predictor.threshold,
        warnings=list(warnings),
    )
    log_prediction(db_conn, tweet=tweet, keyword=keyword, final_text=final_text, ...)
    return response
Jinja Frontend

Browser UI for the Model

The Jinja page provides a real user-facing path with form state, prediction bars, result modes, warnings, and error panels.

Form stateTweet and optional keyword remain filled after prediction, so the UI feels usable during testing.
Result modesThe template switches between disaster and normal visual states based on result.label.
WarningsShort input, trimmed length, uncertain language, and validation errors are surfaced to the user.
{% set has_result = (result is not none) %}
{% set is_disaster = has_result and (result.label == 1) %}
{% set disaster_prob = ((result.probability * 100)|round(1)) if has_result else 0 %}

<form method="post" action="/predict-ui" class="card">
  <textarea id="tweet" name="tweet" rows="6" required>{{ form_data.tweet }}</textarea>
  <input id="keyword" name="keyword" type="text" value="{{ form_data.keyword }}" />
  <button type="submit">Predict</button>
</form>

{% if result.warnings %}
  <ul>{% for item in result.warnings %}<li>{{ item.warning_code }}: {{ item.message }}</li>{% endfor %}</ul>
{% endif %}
Deployment 02 · Persistence and Containerization

SQLite, Docker Compose & Reproducible Runtime

This is where the project becomes a professional first deployment attempt: serving, persistence, environment configuration, and container orchestration work together.

The deployment folder contains the pieces expected in a small real service: a FastAPI app, Jinja frontend, static assets, Pydantic schemas, model loader, preprocessing module, input validator, SQLite storage, Dockerfile, Compose file, devcontainer, environment files, and dependency pinning. This is not just "run notebook model"; it is a deployable application layout.

SQLite3Persist raw tweet, keyword, cleaned final text, probability, label, threshold, and warnings for each inference call.
Docker + Docker ComposePackage the FastAPI app, model artifacts, data directory, pinned dependencies, port mapping, volume, healthcheck, and restart policy.
Python 3.11 slim + CPU wheelsKeep the deployment image focused and compatible with CPU inference through the PyTorch CPU index.
Devcontainer + DependabotDocument a reproducible container development environment and dependency update workflow.
Database Layer

Prediction Logging Schema

SQLite stores the request, cleaned model text, decision, threshold, and validation warnings for later inspection.

CREATE TABLE IF NOT EXISTS prediction_logs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    created_at TEXT NOT NULL,
    tweet TEXT NOT NULL,
    keyword TEXT,
    final_text TEXT NOT NULL,
    probability REAL NOT NULL,
    label INTEGER NOT NULL,
    label_name TEXT NOT NULL,
    threshold REAL NOT NULL,
    warnings_json TEXT
);
Stored FieldWhy It Exists
tweet and keywordPreserve raw user/API input.
final_textDebug the exact text sent to the model.
probability and label_nameMake predictions auditable and readable.
thresholdRecord the decision boundary used for that prediction.
warnings_jsonKeep validation context such as short input or language suspicion.
def log_prediction(conn, *, tweet, keyword, final_text, probability, label, label_name, threshold, warnings):
    warnings_json = json.dumps(warnings or [], ensure_ascii=True)
    created_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
    conn.execute(
        "INSERT INTO prediction_logs (...) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
        (created_at, tweet, keyword, final_text, probability, label, label_name, threshold, warnings_json),
    )
    conn.commit()
Container Build

Dockerfile: Minimal Runtime With Non-Root User

The Dockerfile packages dependencies, application code, artifacts, and data into a Uvicorn service.

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1

WORKDIR /app

RUN addgroup --system appgroup \
    && adduser --system --ingroup appgroup appuser

COPY requirements.txt .
RUN python -m pip install --no-cache-dir --retries 5 --timeout 120 -r requirements.txt

COPY app ./app
COPY artifacts ./artifacts
COPY data ./data
RUN mkdir -p /app/data && chown -R appuser:appgroup /app

USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.api:app", "--host", "0.0.0.0", "--port", "8000"]
DependencyPinned VersionRole
FastAPI0.115.0Web framework and route layer.
Uvicorn0.30.6ASGI runtime server.
Pydantic2.9.2Request/response validation.
PyTorch CPU2.4.1Model inference inside container.
Jinja2 + python-multipart3.1.4 / 0.0.12HTML templates and form posts.
langdetect1.0.9Input language warning/rejection logic.
Compose Runtime

Service Configuration, Volume & Healthcheck

Docker Compose gives the deployment a repeatable command, configurable port, persistent database volume, and restart policy.

services:
  app:
    build: .
    image: disaster_tweets:latest
    ports:
      - "${APP_PORT:-8000}:8000"
    environment:
      APP_ENV: ${APP_ENV:-production}
      DB_PATH: ${DB_PATH:-/app/data/predictions.db}
    volumes:
      - disaster_data:/app/data
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/docs', timeout=5)"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 20s
    restart: unless-stopped

volumes:
  disaster_data:
Configurable portAPP_PORT lets the service bind to another host port without editing Compose.
Persistent databasedisaster_data keeps SQLite state separate from disposable containers.
HealthcheckCompose checks the FastAPI docs endpoint and restarts the service unless stopped.
Professional Deployment Readout

Why This Project Matters

The strength of the project is the full bridge from exploratory research to a working ML application.

This project is important because it shows the first professional deployment pattern: notebooks explain the data, PyTorch training produces reproducible artifacts, FastAPI exposes the model as both JSON and UI, Jinja makes the prediction flow testable in a browser, SQLite records inference context, and Docker Compose gives the app a portable runtime. The final shape is not just a classifier; it is a compact ML product skeleton.

Amir Mohamad Askari · NLP Train and Deployment · 2026