A trained model in a notebook isn't a product. This project takes the LSTM from the Fake News Detector and puts it behind a small, production-shaped API: authenticated, containerized, observable, and honest about when it doesn't know.
The problem
The original project ended where most ML coursework ends: a model with good test accuracy and no way for anything else to use it. Serving it means solving a different set of problems. The exact preprocessing used in training has to run again at request time. The model file is too large to live comfortably in git. Someone has to be allowed to call it, and nobody else should be. And a classifier forced to pick “real” or “fake” for every input will confidently mislabel the articles it's least sure about.
Architecture
How it works
Same preprocessing as training
Train/serve skew is the most common way a served model quietly gets worse. The API rebuilds the training pipeline exactly: lowercase, tokenize with NLTK, lemmatize with WordNet, and drop stop words and tokens of two characters or fewer. It then uses the tokenizer fitted during training (shipped as a pickle) rather than fitting a new one, so every word maps to the same index the model learned.
def preprocess(text):
tokens = nltk.word_tokenize(text.lower())
tokens = [lemmatizer.lemmatize(token) for token in tokens if token not in stop_words and len(token) > 2]
return ' '.join(tokens)An explicit “Uncertain” band
Instead of thresholding at 0.5, the API only commits to a label when the score is past 0.7 on either side. Anything in between comes back as “Uncertain”, with a confidence that reflects how far the score sits from the midpoint. For a misinformation tool, a visible “I don't know” is more useful than a coin flip presented as an answer.
if prediction > PREDICTION_THRESHOLD:
result = "Real News"
confidence = round(prediction * 100, 2)
elif prediction < (1 - PREDICTION_THRESHOLD):
result = "Fake News"
confidence = round((1 - prediction) * 100, 2)
else:
result = "Uncertain"
confidence = round(abs(0.5 - prediction) * 200, 2)Access control
Predictions require an X-API-Key header, checked by a FastAPI dependency against a key read from the environment, so the secret never lives in the code. A separate /health endpoint stays open for uptime checks.
Model artifacts as releases
The .keras file isn't committed to the repo. Each trained model is published as a GitHub Release asset (an initial release, then retrains v1.0.1 and v1.0.2 over the following days), and the Docker build downloads a specific version. That keeps the repository small and makes every image traceable to the exact model it serves.
RUN mkdir -p /app/fake_news_detector && \
wget -O /app/fake_news_detector/fake_news_detection_model.keras \
https://github.com/anirudhrao20/Portfolio-ML-Models/releases/download/Model-Retrain-v1.0.1/fake_news_detection_model_1.0.1.kerasWhat I'd change next
- Make the model version a build argument. The Dockerfile pins v1.0.1 even though v1.0.2 shipped a day later; a build arg would make upgrades a one-line change and rollbacks just as easy.
- Slim the image. The requirements still include pandas, matplotlib, and seaborn from the training notebook, which serving never uses.
- Lock CORS down to known origins instead of allowing every origin.
- Cap input length and add tests for the preprocessing path, since that's where train/serve skew would creep in.