Lecture 19 · room 204
HEMA · Università Cattolica del Sacro Cuore, Rome
Friday 4 December 2026
Lecture 18 · Neural networks · Mon 30 Nov
| Time | Min | |
|---|---|---|
| 10:00 | Last time · Set 16, the In class questions | 15’ |
| 10:15 | We switch to Python: setup | 15’ |
| 10:30 | Fit one properly: three splits, the validation curve | 60’ |
| 11:30 | Break | 15’ |
| 11:45 | Refit, touch the test set once · the four rivals | 30’ |
| 12:15 | What you will see · deep learning in one slide | 25’ |
| 12:40 | The rule · deliverable | 20’ |
The one lab of this course that is not in R — on purpose
nnet is a 1990s single-hidden-layer fitter kept alive for teaching; nobody deploys itscikit-learn for a multilayer perceptron, PyTorch or Keras past that. The day a colleague hands you a model, it will be a .py fileSetup: the Python section of the setup page. uv run jupyter lab, and you are in.
Training fits the weights. Validation chooses size and decay. Test is touched once, at the end, to report a number you can defend.
Attention
Choose hyperparameters on the test set and your reported accuracy is fiction — you have fitted the test set through the back door, with your own hands instead of gradient descent. This is the single most common mistake in applied machine learning, and it is invisible in the output.
import pandas as pd, numpy as np
from sklearn.model_selection import train_test_split
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
churn = pd.read_csv("materials/churn.csv").drop(columns="id")
y = (churn.pop("churn") == "yes").to_numpy()
num = churn.select_dtypes("number").columns
cat = churn.columns.difference(num)
# 70 / 15 / 15, stratified so every split has the same churn rate
X_tr, X_tmp, y_tr, y_tmp = train_test_split(churn, y, test_size=0.30,
stratify=y, random_state=1)
X_val, X_te, y_val, y_te = train_test_split(X_tmp, y_tmp, test_size=0.50,
stratify=y_tmp, random_state=1)
prep = ColumnTransformer([("num", StandardScaler(), num),
("cat", OneHotEncoder(drop="first"), cat)])Networks need both steps: dummies because they only eat numbers, scaling because a column measured in thousands would dominate the gradient.
The mistake that inflates every result, and never announces itself
Pipeline fits the scaler on training rows only, then applies it to the others. Same for the encoderscale() before sample() — you have probably done itfrom sklearn.pipeline import Pipeline
from sklearn.neural_network import MLPClassifier
net = Pipeline([("prep", prep),
("mlp", MLPClassifier(hidden_layer_sizes=(5,), alpha=0.01,
max_iter=600, random_state=7))])
net.fit(X_tr, y_tr) # the scaler learns from X_tr, and only from X_trhidden_layer_sizes=(5,) is size = 5 in nnet. alpha is decay — the same \lambda from ridge, third time this course.
from sklearn.metrics import log_loss
sizes = [1, 3, 5, 10, 20, 50]
alphas = [0.0, 0.001, 0.01, 0.1, 1.0]
rows = []
for s in sizes:
for a in alphas:
m = Pipeline([("prep", prep),
("mlp", MLPClassifier((s,), alpha=a, max_iter=600, random_state=7))])
m.fit(X_tr, y_tr)
rows.append((s, a, log_loss(y_val, m.predict_proba(X_val)[:, 1])))
grid = pd.DataFrame(rows, columns=["size", "alpha", "val_logloss"])Thirty fits, about a minute. Plot it before the next slide: one line per alpha, size on the x-axis.
Three questions to answer from your own figure
size — probably 3 to 10 — or at 50?alpha = 0 do as size grows? That rising line is slide 17 of Monday, drawn by your own laptopalpha = 1.0 do everywhere? Why is it flat and mediocre?import matplotlib.pyplot as plt
for a, g in grid.groupby("alpha"):
plt.plot(g["size"], g["val_logloss"], marker="o", label=f"alpha={a}")
plt.xscale("log"); plt.xlabel("hidden neurons"); plt.ylabel("validation log-loss"); plt.legend()Note
A flat penalised curve is not a failure. It says the signal in this table is nearly linear, so extra capacity has nothing to find. That is the lab’s conclusion arriving early.
best = grid.loc[grid["val_logloss"].idxmin()]
X_full = pd.concat([X_tr, X_val]); y_full = np.concatenate([y_tr, y_val])
final = Pipeline([("prep", prep),
("mlp", MLPClassifier((int(best["size"]),), alpha=best["alpha"],
max_iter=800, random_state=7))])
final.fit(X_full, y_full)
p_nn = final.predict_proba(X_te)[:, 1]Hyperparameters chosen on validation, weights refitted on training plus validation, score reported on test. Every number below comes from the same X_te rows.
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, HistGradientBoostingClassifier
from sklearn.metrics import roc_auc_score, accuracy_score, log_loss
from sklearn.tree import DecisionTreeClassifier
models = {
"Logistic": LogisticRegression(max_iter=1000),
"Pruned tree": DecisionTreeClassifier(max_depth=4, random_state=7),
"Random forest": RandomForestClassifier(n_estimators=500, random_state=7),
"Gradient boosting": HistGradientBoostingClassifier(random_state=7),
}
for name, m in models.items():
pipe = Pipeline([("prep", prep), ("model", m)]).fit(X_full, y_full)
p = pipe.predict_proba(X_te)[:, 1]
print(f"{name:18} acc {accuracy_score(y_te, p > .5):.3f} "
f"AUC {roc_auc_score(y_te, p):.3f} logloss {log_loss(y_te, p):.3f}")| Model | Accuracy | AUC | Log-loss |
|---|---|---|---|
| Logistic | |||
| Pruned tree | |||
| Random forest | |||
| Gradient boosting | |||
| Neural network |
On a table of a few thousand customers with twenty columns, the order is almost always: boosting ≥ forest > network ≈ logistic > tree.
tenure < 6 AND contract = month-to-month in two splits. A network has to build it out of smooth functions of scaled dummiesNote
This is not a defect of networks. It is a mismatch between tool and data. The same network on 50 000 images would win by a mile.
From Monday: the network with identity activation is OLS. So when does the nonlinearity earn its place?
Try option 1 first. It costs a minute and, more often than not, the curve was a log.
MNIST: 28 × 28 pixel images of handwritten digits, 10 classes. A network 784 → 1024 → 1024 → 2048 → 10 carries about four million weights for sixty thousand images.
Attention
None of this applies to a customer table. They win on spatial or sequential data — images, text, audio, sensors. On rows and columns, use last week’s tools.
An autoencoder is a network trained to reproduce its own input through a narrow hidden layer: p inputs → q < p neurons → p outputs, loss = reconstruction error.
Lecture 14’s last slide, closed. Unsupervised learning and neural networks are the same subject seen from two sides.
| Your data are… | Start with | Consider a network when… |
|---|---|---|
| A table, < 100k rows | logistic → forest → boosting | never, unless residuals curve and transformations fail |
| A table, millions of rows, hundreds of columns | boosting | a well-tuned MLP is competitive; test it |
| Images, audio, free text, long sequences | a pretrained deep network | always — nothing else works |
| Anything where you must explain the decision | tree, logistic, GAM | only with lecture 20’s tools, and a lawyer |
The honest version of every AI vendor pitch you will ever hear fits in this table.
Ungraded, but bring it Wednesday:
size and alpha markedStatistics & Big Data · HEMA, Università Cattolica del Sacro Cuore