A model with 97% accuracy that only exists in your Python session is worth exactly zero to the business. The TecnoMarket team is clear about this: the review classifier from 04-03 only delivers value when the website can query it live, and the photo classifier from 05-03 only saves work when the warehouse system invokes it for every new product. This lesson closes the cycle that began with that timid model.save() in 02-05: you will learn the saving formats of Keras and PyTorch and when to use each, why the preprocessing must be saved together with the model, the options for serving predictions (from a batch script to a REST API with FastAPI, with a complete example), ONNX as a bridge between frameworks, and how to validate and watch over a model before and after deploying it. It is the last mile: where deep learning becomes a product.
Contents
- Saving and loading in Keras: .keras, SavedModel and weights-only
- Saving and loading in PyTorch: state_dict and checkpoints
- The model does not travel alone: saving the preprocessing
- Serving a model: options at scale
- A REST API with FastAPI for TecnoMarket's reviews
- ONNX: the bridge format
- Validating before deploying and monitoring afterwards
- TecnoMarket's deployment checklist
Saving and loading in Keras: .keras, SavedModel and weights-only
In 02-05 you ran model.save("tecnomarket-dl/models/mnist_dense_v1.keras") and moved on. Let's make it professional: Keras has three ways to save, each with its use case.
from tensorflow import keras
# 1. .keras format (the recommended one): EVERYTHING in one file
model.save("models/reviews_v3.keras")
loaded_model = keras.models.load_model("models/reviews_v3.keras")
# Saves: architecture + weights + optimizer state + compile()
# -> you can keep training exactly where you left off
# 2. SavedModel (TensorFlow's format, a complete directory)
model.export("models/reviews_v3_savedmodel")
# Saves the self-contained computation graph, WITHOUT needing your Python code
# -> it is what TF Serving and the converters (TFLite) consume
# 3. Weights only
model.save_weights("models/reviews_v3.weights.h5")
new_model = create_model() # you need to REBUILD the architecture
new_model.load_weights("models/reviews_v3.weights.h5")| Format | What it saves | When to use it |
|---|---|---|
.keras |
Architecture + weights + optimizer | General use: pause/resume, archive, share with anyone who has Keras |
SavedModel (export) |
Self-contained graph for inference | Handing off to TF production systems (Serving, TFLite) |
| Weights only | Just the learned numbers | The code defines the architecture (transfer learning as in 05-03, lightweight checkpoints) |
Practical note: the ModelCheckpoint from 06-01 writes in .keras format; you already had half this lesson learned.
Saving and loading in PyTorch: state_dict and checkpoints
PyTorch revolves around the state_dict: a {parameter_name: tensor} dictionary with all the model's weights. The canonical way to save is that dictionary, not the model object:
import torch
# SAVE: the recommended way (only the state_dict)
torch.save(model.state_dict(), "models/reviews_v3.pt")
# LOAD: rebuild the architecture and pour the weights into it
model = ReviewClassifier() # the class from 06-02
model.load_state_dict(torch.load("models/reviews_v3.pt"))
model.eval() # into inference mode! (see 06-02)Why not save the whole model with torch.save(model, ...)? It can be done, but it serializes references to your code (class, module, paths): loading it months later with the project reorganized, it breaks. The state_dict is just named tensors: robust and portable. It is the analog of Keras's "weights only" — in PyTorch, the default option.
Full checkpoint: pausing and resuming training
To resume a training run, the weights are not enough: Adam (02-04) keeps per-parameter internal statistics that must be restored too. The professional pattern:
# Save a full checkpoint at the end of every epoch
checkpoint = {
"epoch": epoch,
"model_state": model.state_dict(),
"optimizer_state": optimizer.state_dict(), # Adam's state too!
"val_loss": val_loss,
}
torch.save(checkpoint, "models/checkpoint_latest.pt")
# Resume days later, exactly where it stopped
ckpt = torch.load("models/checkpoint_latest.pt")
model.load_state_dict(ckpt["model_state"])
optimizer.load_state_dict(ckpt["optimizer_state"])
start_epoch = ckpt["epoch"] + 1| Need | Keras | PyTorch |
|---|---|---|
| Archive/share a ready model | .keras |
state_dict + the class's code |
| Resume training | .keras (carries the optimizer) |
checkpoint dict (model + optimizer + epoch) |
| Hand off to production | SavedModel | state_dict (or export to ONNX/TorchScript) |
The model does not travel alone: saving the preprocessing
Here is the mistake that breaks the most deployments, and it is conceptual, not technical. Your model learned from transformed data: if in production it receives raw data, or data transformed differently, its predictions will be garbage without raising any error. Remember two cases from the course:
- In 04-04, the demand predictor trained on sales scaled by a
scalerfitted on the training set (carefully avoiding data leakage). If in production you feed it raw euros where it expected values in [0, 1], it will predict nonsense with complete self-confidence. - In 04-03, the review classifier uses a
TextVectorizationlayer whose vocabulary was learned from the training corpus. A different vocabulary = different indices = a different "language": the network would receive gibberish.
The golden rule: the (preprocessing, model) pair IS the deployable unit. They never travel separately.
# Option A (Keras): put the preprocessing INSIDE the model before saving
# (TextVectorization is a layer: it can be part of the graph)
full_model = keras.Sequential([
keras.Input(shape=(1,), dtype="string"), # RAW text goes in!
vectorizer, # the already-adapted TextVectorization (04-03)
trained_model, # the network that expects indices
])
full_model.save("models/reviews_full.keras")
# In production: model.predict(["The charger arrived broken"]) -> straight from text
# Option B (general, also valid for PyTorch): save the transformer separately
import joblib
joblib.dump(scaler, "models/demand_scaler.joblib") # the scaler from 04-04
# ...and in production, ALWAYS as a pair:
scaler = joblib.load("models/demand_scaler.joblib")
x = scaler.transform(raw_data) # same scaler, same parameters
prediction = model.predict(x)Option A is the safest (impossible to desynchronize); B is the most flexible. Whichever you choose: same files versioned together, under the same version name.
Serving a model: options at scale
"Deploying" does not always mean "standing up a server". There is a scale of options, from least to most complex, and the professional move is choosing the smallest one that covers the need:
| Option | How it works | TecnoMarket case |
|---|---|---|
| Batch script | A script runs periodically, predicts over what has accumulated and stores the results | Weekly demand forecasting (04-04): no real time needed |
| REST API | A server exposes the model over HTTP; other applications request predictions on the spot | Classifying each review as it is posted (04-03) |
| Model server (TF Serving / TorchServe) | Specialized software: coexisting versions, automatic batching, high throughput | When requests per second overwhelm the handcrafted API |
| Edge (TFLite) | The model, compressed, runs ON the device, no network | The photo classifier in the warehouse app (the plan from 06-03) |
A minimal batch script (sometimes the best architecture is the most boring one):
# predict_weekly_demand.py -- scheduled to run every Monday
import joblib
from tensorflow import keras
model = keras.models.load_model("models/demand_v2.keras")
scaler = joblib.load("models/demand_scaler_v2.joblib") # ALWAYS as a pair
sales = load_recent_weekly_sales() # from the database
pred = model.predict(scaler.transform(sales))
save_predictions(inverse_scale(pred)) # to the table purchasing readsTF Serving and TorchServe stay noted on your radar: they are the natural evolution when scale starts to squeeze, and they consume precisely the "production" formats from the first part (SavedModel and packaged state_dict).
A REST API with FastAPI for TecnoMarket's reviews
Let's serve the review classifier live. FastAPI is a modern, minimalist Python web framework, ideal for wrapping a model. Installation: pip install fastapi uvicorn.
# review_api.py
from fastapi import FastAPI
from pydantic import BaseModel
from tensorflow import keras
app = FastAPI(title="TecnoMarket - Review analysis")
# 1. Load the model ONCE, when the server starts (not on every request)
# It is the "full" model with TextVectorization inside (option A above)
model = keras.models.load_model("models/reviews_full.keras")
REVIEW_THRESHOLD = 0.65 # the confidence threshold from the 03-04 pipeline
class Review(BaseModel): # 2. input contract: JSON with a "text" field
text: str
@app.post("/classify") # 3. endpoint: POST /classify
def classify(review: Review):
prob = float(model.predict([review.text], verbose=0)[0][0]) # 4. inference
sentiment = "positive" if prob >= 0.5 else "negative"
confidence = max(prob, 1 - prob)
return { # 5. JSON response for the website/CRM
"sentiment": sentiment,
"confidence": round(confidence, 3),
"human_review": confidence < REVIEW_THRESHOLD, # human review queue (03-04)
"model_version": "reviews_v3",
}Explanation of the decisions:
- Load at startup: loading the model takes seconds; a prediction, milliseconds. Loading it on every request would kill the API. The load goes at module level.
- Pydantic (
BaseModel) defines and validates the input: if a JSON withouttextarrives, FastAPI returns a clear error automatically, without the model seeing anything. - The endpoint is
POSTbecause it sends data in the request body. - Thanks to option A (vectorizer inside), the API receives raw text: zero risk of desynchronized preprocessing.
- The response includes the model version (traceability) and the
human_reviewfield: the same confidence threshold + human review queue pattern we designed in 03-04, now in production.
Start it and try it:
uvicorn review_api:app --port 8000
# Automatic interactive documentation: http://localhost:8000/docs
curl -X POST http://localhost:8000/classify \
-H "Content-Type: application/json" \
-d '{"text": "The charger arrived broken and nobody responds"}'
# {"sentiment":"negative","confidence":0.94,"human_review":false,"model_version":"reviews_v3"}With this, any TecnoMarket system (the website, the customer service CRM) consumes the model with a simple HTTP request, knowing nothing about deep learning.
ONNX: the bridge format
In 06-03 we noted down ONNX (Open Neural Network Exchange): a standard model interchange format. The idea: you train wherever you like, export to .onnx, and run with ONNX Runtime almost anywhere (Python, C#, Java, mobile...), without installing the source framework.
# Export the PyTorch review model (06-02) to ONNX
import torch
model.eval()
example = torch.randn(1, 200) # example input (defines shapes)
torch.onnx.export(model, example, "models/reviews.onnx",
input_names=["review"], output_names=["logit"])
# Run it WITHOUT PyTorch, only with onnxruntime (pip install onnxruntime)
import onnxruntime as ort
import numpy as np
session = ort.InferenceSession("models/reviews.onnx")
output = session.run(None, {"review": np.random.rand(1, 200).astype("float32")})
print(output[0]) # the logit, identical to what the original model would giveWhen it pays off: handing a model to a team working in another language or framework, unifying inference across models of different origins (exactly TecnoMarket's case after the 06-03 decision: Keras in production, PyTorch in exploration), or exploiting ONNX Runtime's inference optimizations. Minimal good practices: after exporting, compare the outputs of the original model and the ONNX one on a test batch (they must match up to decimals) — and remember that external preprocessing still travels separately, here too.
Validating before deploying and monitoring afterwards
Deploying is not an act of faith; it is a process with two guards:
Before: validation on a frozen set. The team maintains a fixed and versioned test set (real reviews labeled by hand, never used for training — the anti-leakage discipline from 04-04 elevated to team policy). Every production candidate, whether Keras or PyTorch (the rule agreed in 06-03), must:
- Match or beat the current production model's metrics on that set.
- Also be checked on the critical segments (short reviews, new products): a model that is better "on average" can be worse exactly where it hurts most.
- Pass a smoke test of the complete deployable unit: spin up the API with the final artifact and verify known predictions end to end.
Afterwards: monitoring data drift. The world changes and production data slowly moves away from the training data: TecnoMarket launches new categories, review vocabulary evolves ("BLE pairing doesn't work" did not exist in the original corpus). Performance decays in silence: no exception, no error log, just increasingly worse predictions. Minimal conceptual watch:
- Log every prediction with its confidence and the model version (that is why the API returns it).
- Watch indirect signals: the percentage of cases sent to human review rising, the confidence distribution shifting, and the human reviewers' corrections as a continuous sampling of the truth.
- Define the action threshold in advance: "if X worsens by more than Y for Z weeks, retrain with recent data" — and the retrained model goes through the frozen set again. The cycle closes.
TecnoMarket's deployment checklist
The list the team goes through before every production release:
- Model saved in the right format (
.keras/ state_dict) with a version name (reviews_v3), notfinal_model_GOOD. - Preprocessing saved and versioned together with the model (inside the graph or as a paired artifact).
- Metrics on the frozen set ≥ the current model, critical segments included.
- Random seeds, config and library versions recorded (the reproducibility from 06-04): this model can be rebuilt from scratch.
- Smoke test of the deployable unit: API spun up locally, known predictions verified end to end.
- The API response includes the model version and a confidence signal; doubtful cases go to the human review queue (03-04).
- Active monitoring: prediction logging and drift alerts defined before launch, not after the first scare.
- Rollback plan: the previous version (
reviews_v2) stays archived and ready to restore in minutes.
Common Mistakes and Tips
- Deploying the model without its preprocessing (or with a different version of it): the number one silent failure. No error, just absurd predictions with high confidence. The deployable unit is the pair, always.
torch.save(model, ...)of the whole object: works today, breaks when you refactor the project. State_dict + the class's code, as a rule.- Forgetting
model.eval()when loading in PyTorch for inference: with dropout active, every request to your API would give a different prediction for the same review (you saw it in 06-02; in production it is a baffling bug). - Loading the model inside the endpoint: every request would take seconds and the API would crawl. Load once at startup; predict many times.
- Validating on a test set that changes: if every candidate is measured against different data, the comparisons mean nothing. Frozen and versioned set.
- Trusting that "if there are no errors, it works": degradation from data drift throws no exceptions. Without monitoring, your model may have been wrong for months by the time someone notices.
- Tip: after every
save, do the full cycle in a clean interpreter: load, predict on 3 known examples, compare with the expected values. Thirty seconds that catch 90% of serialization problems.
Exercises
Exercise 1: choosing a format
For each situation, state the appropriate saving mechanism and why: (a) module 7's PyTorch training will take three nights on Colab and must be resumable every morning; (b) the Keras photo classifier must be handed to the platform team to serve it with TF Serving; (c) you want to archive the Keras review classifier to pick it up again (even retrain it) in six months; (d) the backend team, which programs in C# and does not want to install PyTorch, needs to run the fraud model.
Exercise 2: the deployment that predicts nonsense
The demand predictor from 04-04 is deployed as a batch script. In testing it had an 8% mean error; in production it predicts absurd quantities (thousands of units of a product that sells dozens), with no error in the logs. The script loads demand_v2.keras correctly and the database delivers the sales properly. What is the most likely cause, why does no error appear, and which two changes would you make to fix it and to keep it from happening again?
Exercise 3: extending the API
Extend review_api.py with a POST /classify_batch endpoint that receives a list of reviews (up to 100) and returns a list of results, taking advantage of the fact that the model predicts more efficiently in batches than one at a time. Also add a GET /health endpoint that returns {"status": "ok", "model_version": "reviews_v3"} and explain what it is for in production.
Solutions
Solution 1:
- (a) Full PyTorch checkpoint (dict with
model_state,optimizer_stateand epoch) saved to Drive every epoch: resuming requires Adam's state, not just weights. - (b) SavedModel (
model.export(...)): it is the self-contained format TF Serving consumes directly. - (c)
.keras: a single file with architecture, weights and optimizer; in six months,load_modeland carry on (retraining included). - (d) ONNX: export with
torch.onnx.exportand deliver the.onnx; the backend runs it with ONNX Runtime for C#, no PyTorch. (After verifying the outputs match the original.)
Solution 2: Most likely cause: the script applies the scaler wrongly (or not at all) — it uses one refitted on other data, a desynchronized version, or feeds raw sales to a model that expected scaled values; forgetting the inverse transform of the output also fits. There is no error because the shapes and dtypes are valid: the model operates on "legal" numbers but at the wrong scale — the classic silent failure. Fixes: (1) deploy the pair as a unit — load demand_scaler_v2.joblib versioned alongside the model, same version suffix, and apply transform to the input and the inverse to the output; (2) add a smoke test to the checklist: after each deployment, predict on 3 known historical cases and compare with the expected values before writing anything to the purchasing table (plus a range validation: if the prediction falls outside the reasonable historical range, alert and do not publish).
Solution 3:
from typing import List
from pydantic import BaseModel, Field
class ReviewBatch(BaseModel):
texts: List[str] = Field(..., max_length=100) # validates the limit of 100
@app.post("/classify_batch")
def classify_batch(batch: ReviewBatch):
probs = model.predict(batch.texts, verbose=0)[:, 0] # ONE batched pass
results = []
for text, prob in zip(batch.texts, probs):
prob = float(prob)
confidence = max(prob, 1 - prob)
results.append({
"sentiment": "positive" if prob >= 0.5 else "negative",
"confidence": round(confidence, 3),
"human_review": confidence < REVIEW_THRESHOLD,
})
return {"model_version": "reviews_v3", "results": results}
@app.get("/health")
def health():
return {"status": "ok", "model_version": "reviews_v3"}The efficiency key: a single call to model.predict with the 100 reviews (the batch parallelism running through the whole course), not 100 calls. The /health endpoint (health check) lets the infrastructure verify every few seconds that the API is alive and which version it serves: if it stops responding, it gets restarted or an alert fires automatically — and during a deployment it confirms the new version is actually live.
Conclusion
The circle opened by the model.save() of 02-05 closes. You now know how to save with judgment (.keras/SavedModel/weights in Keras; state_dict and checkpoints in PyTorch), you know the deployable unit is the preprocessing + model pair (the scaler from 04-04 and the vocabulary from 04-03 always travel with their network), you know how to choose the serving approach for the need (batch → FastAPI → model servers → edge), you have ONNX as a bridge between worlds, and you understand that deploying requires validating against a frozen set beforehand and monitoring data drift afterwards — all condensed into TecnoMarket's checklist.
And with this, module 6 is complete: you know TensorFlow from the inside, you speak PyTorch, you know how to choose between them, you work with a professional environment and discipline, and you take models all the way to production. Techniques (modules 2-5) and tools (module 6): the toolbox is full. In module 7 we will empty it entirely: we will build TecnoMarket's complete projects end to end — image classification, text generation, anomaly detection, a GAN and the fine-tuning of a pretrained model. It stops being a course; it starts being a portfolio.
Deep Learning Course
Module 1: Introduction to Deep Learning
- What is Deep Learning?
- History and evolution of Deep Learning
- Applications of Deep Learning
- Basic concepts of neural networks
- Setting up the work environment
Module 2: Neural Network Fundamentals
- Perceptron and Multilayer Perceptron
- Activation functions
- Forward and backward propagation
- Optimization and loss functions
- Your first complete neural network
Module 3: Convolutional Neural Networks (CNN)
- Introduction to CNNs
- Convolutional and pooling layers
- Popular CNN architectures
- CNN applications in image recognition
Module 4: Recurrent Neural Networks (RNN)
- Introduction to RNNs
- LSTM and GRU
- RNN applications in natural language processing
- Sequences and time series
Module 5: Advanced Deep Learning Techniques
- Generative Adversarial Networks (GAN)
- Autoencoders
- Transfer Learning
- Regularization and improvement techniques
- Attention mechanisms and Transformers
Module 6: Tools and Frameworks
- Introduction to TensorFlow
- Introduction to PyTorch
- Framework comparison
- Development environments and additional resources
- Saving, loading and deploying models
Module 7: Hands-On Projects
- Image classification with CNNs
- Text generation with RNNs
- Anomaly detection with Autoencoders
- Building a GAN for image generation
- Fine-tuning a pretrained model
