With Python chosen (07-01) and NumPy, pandas and Matplotlib in hand (07-02), Marta's workshop needs the rest of the shelf: the libraries with which each of the tasks we have gone through in the course is done. Some we have already used in depth (scikit-learn in module 4, PyTorch in module 5), others we have cited without installing (Hugging Face, experta, pgmpy, OR-Tools) and others appear for the first time because they belong to what comes after training: saving the model, serving it, tracking its versions. This lesson draws the map of the ecosystem by task (classical ML, deep learning, NLP, vision, generative AI, logic and probability, optimisation, experimentation, deployment, data at scale), shows the same 21→16→8→1 MLP of 05-02 written in PyTorch and in Keras side by side so that you see the ideas are the same with different syntax, gives criteria for choosing a library, and runs what links training to deployment: saving and reloading the scikit-learn pipeline with joblib and the MLP with state_dict, checking that they predict exactly the same. It closes with NovaMarket's toolbox: which library for each of the nine use cases. It matters because the ecosystem is huge and changes fast; what does not change is the map of tasks and the criteria. How these libraries are installed, in which environment and with which versions is the business of 07-04.

Contents

  1. Map of the ecosystem by task
  2. Classical ML: scikit-learn and the boosting libraries
  3. Deep learning: PyTorch, TensorFlow/Keras and JAX
  4. The same MLP in PyTorch and in Keras, side by side
  5. NLP and pretrained models: Hugging Face, spaCy, NLTK
  6. Vision: OpenCV, torchvision, Pillow
  7. Generative AI and LLMs: SDKs, Ollama, LangChain and LlamaIndex
  8. Logic, probability and optimisation
  9. Experimentation, MLOps and data at scale
  10. Deployment: FastAPI and ONNX Runtime
  11. Criteria for choosing a library
  12. Code: saving and reloading the pipeline and the MLP
  13. NovaMarket's toolbox
  14. Common Mistakes and Tips
  15. Exercises
  16. Conclusion

  1. Map of the ecosystem by task

Everything rests on the 07-02 layer and is organised by what you want to do, not by fashion:

flowchart TB
    subgraph BASE["Foundation: 07-02"]
        NP[NumPy] --- PD[pandas] --- MPL[Matplotlib / seaborn]
    end
    subgraph LEARN["Learning from data"]
        SK["scikit-learn<br/>classical ML"]
        XGB[XGBoost / LightGBM / CatBoost<br/>boosting]
        PT[PyTorch]
        TF[TensorFlow / Keras]
        JAX[JAX]
    end
    subgraph DOMAIN["By domain"]
        HF["Hugging Face transformers / datasets<br/>spaCy, NLTK: text"]
        CV["OpenCV, torchvision, Pillow: images"]
        LLM[LLM SDKs, Ollama<br/>LangChain, LlamaIndex]
        LOG["experta, pgmpy<br/>Drools, CLIPS: rules and probability"]
        OPT["OR-Tools, PuLP: optimisation"]
    end
    subgraph OPERATE["Operating"]
        MLF["MLflow, W&B, DVC: experiments"]
        API["FastAPI, ONNX Runtime: serving"]
        BIG["Spark, Dask, Polars: scale"]
    end
    BASE --> LEARN --> DOMAIN --> OPERATE
    PT --> HF
    PT --> CV
Task Main libraries Where we saw it
Classical ML (tables) scikit-learn; XGBoost, LightGBM, CatBoost Module 4
Deep learning PyTorch; TensorFlow/Keras; JAX Module 5
Text and pretrained models Hugging Face transformers/datasets, spaCy, NLTK 05-05 (reviews, case 4)
Images OpenCV, torchvision, Pillow 05-04 (incident photos)
Generative AI and LLMs Provider SDKs, Ollama, LangChain, LlamaIndex 05-05 (assistant, case 7)
Rules and probability experta, pgmpy; Drools, CLIPS Module 6 (cases 8 and 9)
Optimisation OR-Tools, PuLP, SciPy 03-04 (routes and assignment, cases 5 and 6)
Experiments and MLOps MLflow, Weights & Biases, DVC New (mention)
Deployment FastAPI, ONNX Runtime 07-01 (train in Python, serve in X)
Data at scale Spark, Dask, Polars New (mention)

  1. Classical ML: scikit-learn and the boosting libraries

scikit-learn is the library of module 4 and the most important one to learn well, because its uniform API has been copied by almost all the others:

  • Every object is an estimator with fit(X, y); predictors add predict and predict_proba; transformers add transform (and fit_transform). It does not matter whether it is LogisticRegression, RandomForestClassifier, StandardScaler or OneHotEncoder: same shape.
  • The learned state is stored in attributes with a trailing underscore: sc = StandardScaler().fit([[1.0], [2.0], [3.0]]) leaves sc.mean_ = [2.] and sc.scale_ = [0.816], and sc.transform([[4.0]]) returns [[2.449]]. That is why a transformer fitted on training data applies the same means in production (04-03).
  • Pipeline chains transformers and a final predictor; ColumnTransformer applies different transformers to different columns (our 21 columns of 04-03); GridSearchCV/RandomizedSearchCV and cross_val_score select models (04-06); sklearn.metrics measures (04-05). The whole pipeline is saved and served as one piece (section 12).

What scikit-learn does not cover well is top-performance gradient boosting on large tables, and there are three libraries with a compatible API for that (fit/predict, and they can go inside a Pipeline): XGBoost (the classic of competitions), LightGBM (faster and lighter in memory, from Microsoft) and CatBoost (handles categoricals without one-hot, from Yandex). For NovaMarket's returns predictor, after the logistic regression (AUC 0.844) and the forest of 04-04, a boosting model would be the natural candidate to scrape a few hundredths; scikit-learn includes HistGradientBoostingClassifier, its own implementation of the same kind, which is enough to start without installing anything. None of the three is in the course environment; their code is identical in form:

# Illustrative (XGBoost is not installed in the course environment): the same API as scikit-learn
from xgboost import XGBClassifier
model = XGBClassifier(n_estimators=300, max_depth=4, learning_rate=0.05)
model.fit(X_train, y_train)
prob = model.predict_proba(X_test)[:, 1]

  1. Deep learning: PyTorch, TensorFlow/Keras and JAX

PyTorch is the library of module 5 and today the dominant one in research and in most new projects. Its four pieces, which you have already handled:

  • Tensors: arrays with dtype, shape and broadcasting like NumPy (07-02), which can live on the GPU (.to("cuda")).
  • Autograd: if a tensor has requires_grad=True, PyTorch records every operation and backward() computes the gradients (05-03). Minimal runnable example: x = torch.tensor(2.0, requires_grad=True); y = x**2 + 3*x; y.backward(); x.grad gives tensor(7.), the derivative 2x + 3 at x = 2.
  • nn.Module: the base class of layers and networks. nn.Sequential (05-02) is the quick form; for architectures with branches you inherit from nn.Module and define forward.
  • Dataset and DataLoader: serve the data in shuffled batches (DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True) produces batches of shape (32, 21); with 100 rows you get 4 batches).

TensorFlow (Google) was the standard of 2016-2019 and is still very present in production and on mobile (TensorFlow Lite); its high-level API is Keras, which since version 3 can run on TensorFlow, PyTorch or JAX. Keras is the friendliest to start with (Sequential, compile, fit) and hides the training loop that in PyTorch you write by hand. JAX (Google) is NumPy with automatic differentiation and XLA compilation, a favourite in large-scale research; Flax and Optax live on top of it. The practical choice today: PyTorch for community, examples and pretrained models; Keras if you value conciseness or come from TensorFlow; JAX for advanced numerical research.

  1. The same MLP in PyTorch and in Keras, side by side

The 21→16→8→1 network of 05-02 (497 parameters) written in the two libraries. The PyTorch version runs in the course environment; the Keras one is illustrative (same architecture, no invented outputs):

# PyTorch (runnable): the architecture, and the explicit training loop
import torch, torch.nn as nn

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(nn.Linear(21, 16), nn.ReLU(),
                                    nn.Linear(16, 8), nn.ReLU(),
                                    nn.Linear(8, 1))          # logit; the sigmoid goes in the loss
    def forward(self, x):
        return self.layers(x)

net = MLP()
print(sum(p.numel() for p in net.parameters()))            # 497
optimizer = torch.optim.Adam(net.parameters(), lr=0.001)
loss_fn = nn.BCEWithLogitsLoss()
# for xb, yb in loader:                                     # the loop of 05-03
#     optimizer.zero_grad(); L = loss_fn(net(xb), yb); L.backward(); optimizer.step()
# Keras (illustrative, not installed in the course environment): same architecture, loop hidden in fit
from tensorflow import keras
from keras import layers

net = keras.Sequential([
    layers.Input(shape=(21,)),
    layers.Dense(16, activation="relu"),
    layers.Dense(8, activation="relu"),
    layers.Dense(1, activation="sigmoid"),                   # here yes: the sigmoid in the layer
])
net.summary()                                                # table of layers and parameters (497)
net.compile(optimizer=keras.optimizers.Adam(learning_rate=0.001),
            loss="binary_crossentropy", metrics=["AUC"])
# net.fit(X_train, y_train, epochs=100, batch_size=64, validation_split=0.2)
# net.predict(X_test)

Correspondences: nn.Linearlayers.Dense; the activation in PyTorch is a separate module and in Keras an argument; in PyTorch the sigmoid is applied by BCEWithLogitsLoss for numerical stability and in Keras it usually goes in the layer with binary_crossentropy; the zero_grad / backward / step loop of 05-03 is what compile + fit do for you. If you understand one, you read the other in ten minutes: the difficulty of deep learning is in 05-03, not in the syntax.

  1. NLP and pretrained models: Hugging Face, spaCy, NLTK

  • Hugging Face transformers is the door to the pretrained models of 05-05: thousands of models (BERT, RoBERTa, T5, Llama, Whisper...) downloaded by name and used with pipeline in three lines, or fine-tuned with Trainer. datasets loads public datasets in the same style; tokenizers does fast tokenisation (written in Rust, 07-01). The Hugging Face Hub is the repository where models and data are shared, with licences and model cards (remember 02-04).
  • spaCy: "industrial" NLP: tokenisation, lemmatisation, named entities, syntactic parsing, with models per language (including Spanish) and production speed. Ideal for extracting from NovaMarket's reviews which product and which component is mentioned.
  • NLTK: the classic academic library (corpora, stemming, n-grams); useful for learning, less so for production.
# Illustrative (transformers is not installed in the course environment): classifying reviews (case 4)
from transformers import pipeline
classifier = pipeline("sentiment-analysis",
                      model="nlptown/bert-base-multilingual-uncased-sentiment")   # multilingual 1-5 star model
classifier(["The NovaSound headphones sound wonderful, very comfortable.",
            "The NovaBrew coffee maker arrived with the jug broken and nobody answers."])
# returns a list of dictionaries with 'label' (stars) and 'score' (confidence); we do not reproduce figures here

Behind pipeline is the same chain as in 05-05: tokeniser, transformer model in PyTorch, classification layer. And behind everything, tensors.

  1. Vision: OpenCV, torchvision, Pillow

  • Pillow (PIL): opening, resizing, cropping, converting images. The basics.
  • OpenCV (cv2): the classic computer-vision library, written in C++: filters, edge detection, contours, camera calibration, real-time video. It preprocesses the incident photos before a CNN sees them, or solves simple tasks on its own (reading a barcode, measuring a box).
  • torchvision: pretrained vision models (ResNet, EfficientNet, ViT...), data-augmentation transforms and standard datasets, integrated with PyTorch. The CNN of 05-04 on synthetic 16×16 photos (98.7 %) would become in production a torchvision model fine-tuned on real photos from the Zaragoza and Getafe warehouses.
  • On top: Ultralytics (YOLO) for object detection, timm for more architectures, albumentations for augmentations.

  1. Generative AI and LLMs: SDKs, Ollama, LangChain and LlamaIndex

For NovaMarket's assistant (case 7) and the LLM calls with RAG we sketched in 05-05, the box has three shelves, and it pays to look at them in a neutral tone because it is the corner that changes fastest:

  • Provider SDKs: each LLM provider offers a Python library that sends messages to its API and returns text (or tool calls, or embeddings). The pattern is always the same: client, list of messages with roles, response. Its cost is per token and your data leaves the company: review 02-04 and the GDPR before sending reviews or customer data.
  • Local models: Ollama (and llama.cpp, vLLM for serving at scale) run open models on your machine or server, with an API compatible with the providers'. Less capability, more privacy, fixed cost: the argument Diego will want to hear for the internal assistant.
  • Orchestration: LangChain and LlamaIndex chain steps (retrieve documents, build the prompt, call the model, parse the response, call tools): they are the standard way to build the RAG of 05-05 over NovaMarket's knowledge base (return policies, product sheets, resolved incidents) and agents that execute actions. They rely on vector databases (FAISS, Chroma, pgvector...) to search by embedding similarity. They are useful, they change their API often and they add a layer that sometimes is worth replacing with a hundred lines of your own when the flow is simple.

  1. Logic, probability and optimisation

  • Rules: in 06-02 we wrote the Rule/InferenceEngine engine in pure Python. Its equivalents with more muscle: experta (a rule engine in Python descended from CLIPS, with facts and rules as decorated classes), CLIPS (the classic engine in C, with Python bindings) and Drools (Java, the enterprise standard, with DMN decision tables like those of 06-04). For returns (case 8) our own engine is enough while the rules number in the dozens; from hundreds upwards, and with business users who want to edit them, a BRMS.
  • Probability: pgmpy implements the Bayesian networks of 06-03 with efficient inference (variable elimination, not enumeration) and parameter learning from data; pomegranate and PyMC (general Bayesian inference) are alternatives. For incident diagnosis (case 9), pgmpy is the natural replacement for our network by enumeration.
  • Optimisation: OR-Tools (Google) solves vehicle routing, assignment and constraint scheduling (cases 5 and 6 of 03-04, where we used our own local search); PuLP and SciPy optimize cover linear programming and numerical optimisation. For NovaMarket's van, OR-Tools would give in seconds routes that our local search of 03-04 only approximated.

  1. Experimentation, MLOps and data at scale

When you move from a notebook to a project (07-04 and module 8), new questions appear: which hyperparameters gave AUC 0.844? With which data? Which model is in production? Tools, just to place them:

  • MLflow: logs parameters, metrics and artefacts of each run (mlflow.log_param, log_metric, log_model) and has a model registry with versions and stages; it installs locally. Weights & Biases (W&B) does the same as a service with rich dashboards. DVC versions data and large models alongside Git (07-04).
  • Data at scale: when orders.csv is 3,000 orders/day × years and does not fit in memory, Polars (DataFrame in Rust, API similar to pandas, much faster on one machine), Dask (distributed pandas) and Spark/PySpark (the standard on clusters, 07-01). The rule: pandas up to a few gigabytes; then Polars on one machine; Spark when there is a cluster and a data team.

  1. Deployment: FastAPI and ONNX Runtime

Serving the returns predictor means that NovaMarket's website sends the data of an order and receives a probability. FastAPI is the most used Python library for that: you define the shape of the request with Pydantic, write a function, and get an HTTP API with automatic documentation. Illustrative example (FastAPI is not installed in the course environment; we load the model with joblib as in section 12):

# Illustrative: returns prediction server (file serve.py; launched with `uvicorn serve:app`)
from fastapi import FastAPI
from pydantic import BaseModel
import joblib, pandas as pd

app = FastAPI(title="NovaMarket - return risk")
model = joblib.load("models/returns_model.joblib")          # loaded ONCE at start-up

class Order(BaseModel):                                      # input schema, validated automatically
    amount: float
    num_items: int
    delivery_days: float
    new_customer: int
    category: str
    postcode_zone: str
    payment_method: str
    shipping_type: str
    weekday: int
    month: int
    weekend: int
    days_since_start: int
    amount_per_item: float
    previous_orders: int
    previous_return_rate: float

@app.post("/return-risk")
def risk(order: Order):
    X = pd.DataFrame([order.model_dump()])                   # one row with the same 15 training columns
    prob = float(model.predict_proba(X)[0, 1])
    return {"return_probability": round(prob, 3),
            "review": prob > 0.5}                            # the 04-05 threshold, adjustable

A POST /return-risk request with an order JSON returns something like {"return_probability": 0.048, "review": false}. ONNX Runtime is the alternative when the consumer is not Python (07-01): the model is exported to .onnx (with torch.onnx.export or skl2onnx) and run from Java, C++ or the browser. Other deployment pieces, for the map: BentoML and TorchServe (model servers), Streamlit and Gradio (quick interfaces for demos), Docker (07-04).

  1. Criteria for choosing a library

Criterion Question to ask Example
Maturity and maintenance How many years has it been around? Is it still getting releases? Is there a team or a foundation behind it? scikit-learn, NumPy and PyTorch are safe bets; a six-month-old RAG library, not so much
Community and documentation Do you find answers to the errors? Are there tutorials, books, courses? The PyTorch and Hugging Face community is a reason to choose them
Licence Does it allow commercial use? Does it force you to release your code? (MIT, BSD, Apache: permissive; GPL: copyleft; model licences: check) Almost the whole Python ecosystem is BSD/MIT/Apache; some pretrained models restrict uses
Performance Does it handle your volume? Does it use the GPU? Is the core in C/C++/Rust? LightGBM versus a scikit-learn forest on 10 million rows; Polars versus pandas
Integration Does it fit what you already have (pandas, scikit-learn, your server, your model format)? XGBoost inside a Pipeline; ONNX for Java
Cost and lock-in Is it free? Does it tie you to a vendor (cloud, LLM API)? Can you leave? Local model with Ollama versus paid API
Fit to the problem Does it solve your task or are you forcing it? Rules for the returns policy (06-04), not a neural network

And a common-sense criterion Diego would applaud: the best library is the simplest one that solves the problem and that the team already knows how to work with.

  1. Code: saving and reloading the pipeline and the MLP

What joins training and deployment is a file. We train the returns predictor of 04-03/04-04 (logistic regression on the 21-column ColumnTransformer) and the MLP of 05-02, save both, reload them as the server would and check that the predictions are identical:

import numpy as np, joblib, torch, torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from novamarket_ml import generate_orders_ml, dirty_orders, prepare_orders, build_preprocessing

# Data from 04-03: dirtied and prepared orders (15 columns -> 21 after the ColumnTransformer)
X, y = prepare_orders(dirty_orders(generate_orders_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)

# --- 1) scikit-learn: complete pipeline and joblib
pipe = Pipeline([("prep", build_preprocessing()),
                 ("model", LogisticRegression(max_iter=1000))]).fit(Xtr, ytr)
prob_before = pipe.predict_proba(Xte)[:, 1]
print(f"Test AUC logistic regression: {roc_auc_score(yte, prob_before):.3f}")

joblib.dump(pipe, "returns_model.joblib")                 # saves the WHOLE pipeline (preprocessing + model)
pipe_loaded = joblib.load("returns_model.joblib")         # in another process, another day, the server
prob_after = pipe_loaded.predict_proba(Xte)[:, 1]
print("equal after reload (sklearn):", np.allclose(prob_before, prob_after))

# --- 2) PyTorch: MLP 21->16->8->1 and state_dict
prep = build_preprocessing().fit(Xtr)                      # the preprocessing is fitted on train...
Xtr_t = torch.tensor(prep.transform(Xtr), dtype=torch.float32)
Xte_t = torch.tensor(prep.transform(Xte), dtype=torch.float32)
ytr_t = torch.tensor(ytr.values, dtype=torch.float32).unsqueeze(1)

def build_mlp():
    return nn.Sequential(nn.Linear(21, 16), nn.ReLU(),
                         nn.Linear(16, 8), nn.ReLU(),
                         nn.Linear(8, 1))

torch.manual_seed(42)
net = build_mlp()
opt = torch.optim.Adam(net.parameters(), lr=0.003)
loss_fn = nn.BCEWithLogitsLoss()
for epoch in range(150):                                   # short training, full batch
    opt.zero_grad(); L = loss_fn(net(Xtr_t), ytr_t); L.backward(); opt.step()
net.eval()
with torch.no_grad():
    prob_net = torch.sigmoid(net(Xte_t)).numpy().ravel()
print(f"Test AUC MLP: {roc_auc_score(yte, prob_net):.3f}   parameters: {sum(p.numel() for p in net.parameters())}")

torch.save(net.state_dict(), "returns_mlp.pt")             # only the weights
joblib.dump(prep, "preprocessing.joblib")                  # ...and it is saved separately: the MLP does not include it
print(list(net.state_dict().keys()))

net2 = build_mlp()                                         # same architecture, fresh random weights
net2.load_state_dict(torch.load("returns_mlp.pt"))         # overwritten with the saved ones
net2.eval()
prep2 = joblib.load("preprocessing.joblib")
with torch.no_grad():
    prob_net2 = torch.sigmoid(net2(torch.tensor(prep2.transform(Xte), dtype=torch.float32))).numpy().ravel()
print("equal after reload (PyTorch):", np.allclose(prob_net, prob_net2))

# --- 3) a new order, as it would arrive at the API of section 10
new_order = Xte.iloc[[0]]                                  # one-row DataFrame (double bracket)
print(pipe_loaded.predict_proba(new_order)[0, 1].round(3),
      torch.sigmoid(net2(torch.tensor(prep2.transform(new_order), dtype=torch.float32))).item())

Output:

Test AUC logistic regression: 0.844
equal after reload (sklearn): True
Test AUC MLP: 0.841   parameters: 497
['0.weight', '0.bias', '2.weight', '2.bias', '4.weight', '4.bias']
equal after reload (PyTorch): True
0.048 0.0756823718547821

Explanation:

  • The AUC 0.844 is exactly that of 04-04: same data, same seed, same pipeline. joblib.dump serialises the whole object (imputers with their medians, scaler with its means, one-hot with its categories, regression with its coefficients) into a file of about 7 KB, and np.allclose confirms that the 750 probabilities match after reloading. That is what the FastAPI server of section 10 loads.
  • In PyTorch we save only the state_dict: an ordered dictionary of tensors whose keys (0.weight, 0.bias...) are the positions of the layers in the Sequential. To load it you have to rebuild the same architecture with the same code and then load_state_dict; that is why build_mlp goes in a shared module (07-04). The file weighs about 5 KB (497 floats plus metadata).
  • The detail that breaks the most deployments: the network does not include the preprocessing. We save prep separately with joblib and apply it before the network; if in production it were scaled with other means, the predictions would be different without anything raising an error. The scikit-learn pipeline avoids that risk by carrying everything inside; with PyTorch you have to be disciplined.
  • For the same new order (a 73 € laptop from a returning customer with three previous orders and no returns), the regression gives 4.8 % and the MLP 7.6 %: different models, same conclusion, low risk. torch.load on files from elsewhere carries the pickle risk of 07-01; to share weights with third parties there is the safetensors format.

  1. NovaMarket's toolbox

Use case Task Chosen library Alternatives / evolution
1 Recommendation Collaborative filtering, similarity scikit-learn (neighbours, factorisation) + pandas implicit, two-tower models in PyTorch
2 Demand forecasting Time series pandas (resample, rolling) + scikit-learn (regression with lags) statsmodels, Prophet, sktime, recurrent networks
3 Fraud / returns Tabular classification scikit-learn Pipeline + logistic regression / forest; HistGradientBoosting LightGBM/XGBoost, MLP in PyTorch
4 Reviews Text classification, extraction Hugging Face transformers (pipeline, fine-tuning), spaCy scikit-learn with TF-IDF as a quick baseline
5 Routes Combinatorial optimisation OR-Tools Own local search (03-04), PuLP
6 Assignment Constraint programming OR-Tools (CP-SAT) PuLP
7 Assistant LLM + RAG Provider SDK or Ollama; LangChain or LlamaIndex; vector store Own code if the flow is simple
8 Return rules Rule engine Own engine from 06-02 experta, Drools with DMN
9 Incident diagnosis Bayesian network pgmpy Rule engine + own probabilities (06-03)
Cross-cutting Saving and serving joblib, state_dict, FastAPI ONNX Runtime; MLflow for the registry
Cross-cutting Data pandas + SQL Polars if it grows; Spark if there is a cluster

Common Mistakes and Tips

  • Learning five libraries at once. Master scikit-learn (API, Pipeline, validation) and one deep-learning library; the rest is learned in days because they copy the same API.
  • Saving the model without the preprocessing (or the other way round): different predictions in production with no error at all. With scikit-learn, save the complete Pipeline; with PyTorch, save the state_dict and the transformer, and the architecture code in a versioned module.
  • Loading a state_dict into a different architecture: PyTorch fails with a key or shape error; if you change the network, retrain or convert explicitly.
  • Trusting that the file will always load: pickle/joblib depend on versions (07-01, 07-04). Write down sklearn.__version__ and torch.__version__ next to the model and test the reload in the target environment.
  • Installing everything "just in case": every library brings dependencies and conflicts (07-04). Add when you need it.
  • Choosing by Internet benchmark: rankings change from month to month; the criteria of section 11 and a test with your own data do not.
  • Putting an LLM where a rule or a groupby was enough: cost, latency and opacity (06-04). Start simple.

Exercises

Exercise 1. Replace the logistic regression of section 12 with scikit-learn's HistGradientBoostingClassifier (import from sklearn.ensemble; leave the rest of the Pipeline the same). Train, measure the test AUC, save with joblib, reload and check with np.allclose that the predictions match. Does it improve on the logistic regression? Also compare the size in bytes of the two .joblib files (os.path.getsize) and explain the difference.

Exercise 2. Write (illustrative, without running it) the MLP of section 4 with one more hidden layer of 32 neurons and dropout 0.2, both in PyTorch (nn.Module) and in Keras. Say in each version where the dropout goes and compute the number of parameters of the new 21→32→16→8→1 network.

Exercise 3. Fill in the table of section 13 for a tenth use case Marta proposes: detecting damaged products in the photos customers upload when opening an incident (case 9 extended with images). Give the task, the chosen library, alternatives and which format you would use to serve the model if the Java developer wants to integrate it into the incidents app without depending on a Python service. Justify with the criteria of section 11.

Solutions

Solution 1.

from sklearn.ensemble import HistGradientBoostingClassifier
import os
pipe_hgb = Pipeline([("prep", build_preprocessing()),
                     ("model", HistGradientBoostingClassifier(max_depth=4, learning_rate=0.05,
                                                             max_iter=200, random_state=42))]).fit(Xtr, ytr)
p1 = pipe_hgb.predict_proba(Xte)[:, 1]
print(f"Test AUC HGB: {roc_auc_score(yte, p1):.3f}")
joblib.dump(pipe_hgb, "hgb_model.joblib")
p2 = joblib.load("hgb_model.joblib").predict_proba(Xte)[:, 1]
print(np.allclose(p1, p2), os.path.getsize("returns_model.joblib"), os.path.getsize("hgb_model.joblib"))

Output: Test AUC HGB: 0.805, True 6650 337074. The boosting model does not improve on the logistic regression here (0.805 versus 0.844): with 2,249 orders and a "hidden truth" that is almost linear in the logit (04-01), the linear model is the right one, and the boosting with 200 trees of depth 4 overfits (remember 04-06: complexity has to be earned with data; with fewer iterations or early_stopping=True it would get closer). It does serve to check that the API is identical and that joblib saves any pipeline. The file weighs about 50 times more (about 330 KB versus 7 KB) because it stores 200 trees with their thresholds, versus 21 coefficients and a bias: model size is another deployment criterion (server memory, load time, devices).

Solution 2.

# PyTorch (illustrative)
class MLP2(nn.Module):
    def __init__(self):
        super().__init__()
        self.layers = nn.Sequential(nn.Linear(21, 32), nn.ReLU(), nn.Dropout(0.2),
                                    nn.Linear(32, 16), nn.ReLU(), nn.Dropout(0.2),
                                    nn.Linear(16, 8), nn.ReLU(),
                                    nn.Linear(8, 1))
    def forward(self, x):
        return self.layers(x)
# Keras (illustrative)
net = keras.Sequential([layers.Input(shape=(21,)),
                        layers.Dense(32, activation="relu"), layers.Dropout(0.2),
                        layers.Dense(16, activation="relu"), layers.Dropout(0.2),
                        layers.Dense(8, activation="relu"),
                        layers.Dense(1, activation="sigmoid")])

The dropout goes after the activation of the layer you want to regularise, in both libraries as a layer/module with no parameters; in PyTorch it is active only in train() mode and disabled with eval(), in Keras fit and predict handle it on their own. Parameters: (21 × 32 + 32) + (32 × 16 + 16) + (16 × 8 + 8) + (8 × 1 + 1) = 704 + 528 + 136 + 9 = 1,377, almost three times the 497 of the original network, with the same 2,249 orders: the reason for adding dropout.

Solution 3. Case 10, "damaged products in photos": an image classification task (or detection, if you want to locate the damage). Chosen library: PyTorch + torchvision with a pretrained model (a small ResNet) fine-tuned on the labelled incident photos, and Pillow/OpenCV for preprocessing (resize, normalise); it is the mature choice, with community and available models, permissive licence and acceptable CPU performance for a few dozen photos a day. Alternatives: Keras with the same idea; Ultralytics/YOLO if the damage has to be located; a cloud provider's vision service if there are not enough labelled photos (with the 02-04 caution about sending data outside). To integrate it into the Java app without a Python service: export to ONNX with torch.onnx.export and run it with ONNX Runtime for Java (07-01); the image preprocessing is replicated in Java (resizing and normalising is simple, unlike a ColumnTransformer). Criteria applied: maturity and community (PyTorch/torchvision), integration (ONNX for Java), performance (CPU is enough at NovaMarket's volume), cost (local model, no paid API), fit (real images call for a pretrained network, not the synthetic CNN of 05-04).

Conclusion

We have drawn the tool map of Marta's workshop by task: scikit-learn and its fit/predict/transform API with Pipeline for classical ML, and the boosting libraries as reinforcement; PyTorch (tensors, autograd, nn.Module, DataLoader) versus TensorFlow/Keras and JAX, with the 21→16→8→1 MLP written in both syntaxes to check that the idea is the same; Hugging Face, spaCy and NLTK for text; OpenCV, torchvision and Pillow for images; the LLM SDKs, Ollama and the LangChain/LlamaIndex orchestrators for the assistant with RAG; experta, pgmpy and the rule engines for the symbolic half; OR-Tools and PuLP for optimisation; MLflow, W&B and DVC so as not to lose track of experiments; FastAPI and ONNX Runtime for serving; Spark, Dask and Polars when the data grows. We have set criteria for choosing (maturity, community, licence, performance, integration, cost, fit) and we have run the piece that joins training and deployment: the returns pipeline saved with joblib (AUC 0.844) and the MLP saved as a state_dict with its preprocessing kept separately, both reloaded with identical predictions. NovaMarket's toolbox assigns a library to each of the nine use cases.

The last piece of the workshop remains, and it is the one that stops all of this collapsing the day another colleague tries to run the code: the environment. Which version of Python and of each library, in which virtual environment, with which dependency file; how to use Jupyter without falling into its traps; which IDE; what to version with Git and what not; how to go from the loose scripts of modules 3-6 to a project structure with modules, a train.py and tests; and when a GPU, the cloud or Docker is needed. That is 07-04, Development Environments, with which we close the module.

Fundamentals of Artificial Intelligence (AI)

Module 1: Introduction to Artificial Intelligence

Module 2: Basic Principles of AI

Module 3: Algorithms in AI

Module 4: Machine Learning

Module 5: Neural Networks and Deep Learning

Module 6: Logic and Expert Systems

Module 7: Tools and Programming Languages in AI

Module 8: Projects and Case Studies

Module 9: Exercises and Practice

Module 10: Additional Resources

© Copyright 2026. All rights reserved