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
- Map of the ecosystem by task
- Classical ML: scikit-learn and the boosting libraries
- Deep learning: PyTorch, TensorFlow/Keras and JAX
- The same MLP in PyTorch and in Keras, side by side
- NLP and pretrained models: Hugging Face, spaCy, NLTK
- Vision: OpenCV, torchvision, Pillow
- Generative AI and LLMs: SDKs, Ollama, LangChain and LlamaIndex
- Logic, probability and optimisation
- Experimentation, MLOps and data at scale
- Deployment: FastAPI and ONNX Runtime
- Criteria for choosing a library
- Code: saving and reloading the pipeline and the MLP
- NovaMarket's toolbox
- Common Mistakes and Tips
- Exercises
- Conclusion
- 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) |
- 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 addpredictandpredict_proba; transformers addtransform(andfit_transform). It does not matter whether it isLogisticRegression,RandomForestClassifier,StandardScalerorOneHotEncoder: same shape. - The learned state is stored in attributes with a trailing underscore:
sc = StandardScaler().fit([[1.0], [2.0], [3.0]])leavessc.mean_ = [2.]andsc.scale_ = [0.816], andsc.transform([[4.0]])returns[[2.449]]. That is why a transformer fitted on training data applies the same means in production (04-03). Pipelinechains transformers and a final predictor;ColumnTransformerapplies different transformers to different columns (our 21 columns of 04-03);GridSearchCV/RandomizedSearchCVandcross_val_scoreselect models (04-06);sklearn.metricsmeasures (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]
- 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 andbackward()computes the gradients (05-03). Minimal runnable example:x = torch.tensor(2.0, requires_grad=True); y = x**2 + 3*x; y.backward(); x.gradgivestensor(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 fromnn.Moduleand defineforward.DatasetandDataLoader: 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.
- 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.Linear ↔ layers.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.
- NLP and pretrained models: Hugging Face, spaCy, NLTK
- Hugging Face
transformersis the door to the pretrained models of 05-05: thousands of models (BERT, RoBERTa, T5, Llama, Whisper...) downloaded by name and used withpipelinein three lines, or fine-tuned withTrainer.datasetsloads public datasets in the same style;tokenizersdoes 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 hereBehind pipeline is the same chain as in 05-05: tokeniser, transformer model in PyTorch, classification layer. And behind everything, tensors.
- 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,
timmfor more architectures,albumentationsfor augmentations.
- 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.
- Logic, probability and optimisation
- Rules: in 06-02 we wrote the
Rule/InferenceEngineengine 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:
pgmpyimplements the Bayesian networks of 06-03 with efficient inference (variable elimination, not enumeration) and parameter learning from data;pomegranateand PyMC (general Bayesian inference) are alternatives. For incident diagnosis (case 9),pgmpyis 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
optimizecover 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.
- 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.csvis 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.
- 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, adjustableA 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).
- 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.
- 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.dumpserialises 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, andnp.allcloseconfirms 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 theSequential. To load it you have to rebuild the same architecture with the same code and thenload_state_dict; that is whybuild_mlpgoes 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
prepseparately withjobliband 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.loadon files from elsewhere carries the pickle risk of 07-01; to share weights with third parties there is the safetensors format.
- 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 thestate_dictand the transformer, and the architecture code in a versioned module. - Loading a
state_dictinto 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__andtorch.__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
groupbywas 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
- Fundamental Concepts: Agents, Environments and Rationality
- Types of Artificial Intelligence
- Data as the Raw Material of AI
- Ethics and Considerations in AI
Module 3: Algorithms in AI
- Introduction to Algorithms
- Search Algorithms
- Adversarial Search: Games and Minimax
- Optimization Algorithms
Module 4: Machine Learning
- Basic Concepts of Machine Learning
- Types of Machine Learning
- Data Preparation and Feature Engineering
- Machine Learning Algorithms
- Model Evaluation and Validation
- Overfitting, Regularization and Hyperparameter Tuning
Module 5: Neural Networks and Deep Learning
- Introduction to Neural Networks
- Neural Network Architecture
- How a Network Learns: Gradient Descent and Backpropagation
- Deep Learning and Its Applications
- Transformers, Large Language Models and Generative AI
Module 6: Logic and Expert Systems
- Logic in AI
- Expert Systems
- Reasoning under Uncertainty: Probability and Bayesian Networks
- Applications of Expert Systems
Module 7: Tools and Programming Languages in AI
- Programming Languages for AI
- Scientific Python: NumPy, pandas and Matplotlib
- Popular Tools and Libraries
- Development Environments
Module 8: Projects and Case Studies
Module 9: Exercises and Practice
- Algorithm Exercises
- Machine Learning Practice
- Neural Network Projects
- Capstone Project: from Idea to Prototype
