Aller au contenu principal

Deep Learning 1 — Neurones linéaires

:::tip Notebook Kaggle Le code complet et exécutable de ce chapitre est sur Kaggle : Ouvrir →

Versions anglaise et chinoise disponibles depuis la page d'accueil. :::

Premier contact avec les réseaux de neurones, à partir du cas le plus simple : un neurone linéaire. Ne vous attendez pas à du deep learning ici — nous bâtissons les fondations qui vont nous servir pour tous les chapitres suivants.

Pourquoi ce chapitre ?

Avant les CNN et les Transformers, il faut comprendre ce qu'est un neurone et comment il apprend. Vous y voyez :

  • ce qu'est un neurone linéaire et le lien avec la régression linéaire ;
  • la descente de gradient codée à la main ;
  • pourquoi il faut normaliser les entrées ;
  • l'introduction à PyTorch : tenseurs, nn.Linear, optimiseurs, DataLoader ;
  • l'importance des fonctions d'activation non linéaires.

Le neurone linéaire

C'est exactement la régression linéaire du chapitre 2 ML, sous une autre forme :

u=Xw+bu = X w + b

avec XRn×mX \in \mathbb{R}^{n \times m} les entrées, wRmw \in \mathbb{R}^m les poids, bb le biais, uu la sortie.

L'apprentissage consiste à trouver les bons ww et bb qui minimisent une fonction de coût. Pour la régression :

E(w,b)=1ni=1n(yiui)2E(w, b) = \frac{1}{n}\sum_{i=1}^n (y_i - u_i)^2

C'est la MSE, déjà rencontrée.

La descente de gradient

Quand la solution analytique n'existe pas ou est trop coûteuse, nous ajustons les paramètres pas à pas, dans la direction qui fait baisser EE :

wwηEw,bbηEbw \leftarrow w - \eta \, \frac{\partial E}{\partial w}, \quad b \leftarrow b - \eta \, \frac{\partial E}{\partial b}

η\eta est le learning rate.

Pour la régression linéaire, le calcul donne :

Ew=1nXT(uy),Eb=1ni=1n(uiyi)\frac{\partial E}{\partial w} = \frac{1}{n} X^T (u - y), \quad \frac{\partial E}{\partial b} = \frac{1}{n} \sum_{i=1}^n (u_i - y_i)

Le terme (uy)(u - y), c'est l'erreur de prédiction. Le gradient propage cette erreur vers ww via XTX^T.

Trois variantes

  • Batch : gradient calculé sur tout le dataset à chaque étape. Lent sur gros datasets.
  • SGD (Stochastic) : un seul exemple. Rapide mais bruité.
  • Minibatch : un sous-ensemble (32, 64, ...). Le compromis utilisé en pratique.

Pourquoi normaliser ?

Sans normalisation, les variables à grande échelle dominent la dynamique de la descente de gradient et imposent un learning rate minuscule. La convergence devient très lente.

Pour une régression linéaire, la courbure de la fonction de coût en ww est proportionnelle à xi2\sum x_i^2. Si xx a des valeurs grandes, la courbure est grande et il faut η\eta petit.

Solution : StandardScaler ramène toutes les variables à moyenne 0 et écart-type 1.

from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

:::warning fit_transform vs transform On ajuste uniquement sur le train (fit_transform), puis on applique au test (transform). Sinon : data leakage. :::

PyTorch : autograd et optimiseurs

PyTorch automatise deux choses qu'on faisait à la main :

  • le calcul des gradients (autograd) ;
  • la mise à jour des paramètres (optimiseurs : SGD, Adam, etc.).

Les tenseurs PyTorch sont l'équivalent des tableaux NumPy, mais avec en plus la possibilité de stocker un gradient et de tourner sur GPU.

import torch
import torch.nn as nn

# Conversion NumPy → tenseur
X_train_t = torch.tensor(X_train, dtype=torch.float32)
y_train_t = torch.tensor(y_train, dtype=torch.float32).view(-1, 1)

Le .view(-1, 1) met y au format (n, 1) pour qu'il matche la sortie d'une couche nn.Linear(m, 1).

Le neurone linéaire en PyTorch

model = nn.Linear(m, 1)
criterion = nn.MSELoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.1)

La boucle d'entraînement standard

for _ in range(epochs):
optimizer.zero_grad() # 1. remettre les gradients à zéro
y_hat = model(X_train_t) # 2. forward
loss = criterion(y_hat, y_train_t)
loss.backward() # 3. autograd calcule tous les gradients
optimizer.step() # 4. mise à jour des paramètres

Mémorisez ces 4 étapes — elles reviendront dans tous les chapitres suivants.

DataLoader : minibatch automatique

Pour ne pas écrire à la main la sélection de minibatchs :

from torch.utils.data import TensorDataset, DataLoader

train_dataset = TensorDataset(X_train_t, y_train_t)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

for _ in range(epochs):
for Xb, yb in train_loader:
optimizer.zero_grad()
y_hat = model(Xb)
loss = criterion(y_hat, yb)
loss.backward()
optimizer.step()

shuffle=True remélange les données à chaque epoch — presque toujours souhaité en train.

Empilement et non-linéarité

Empiler deux couches nn.Linear ne crée pas un modèle plus puissant — algébriquement, deux transformations linéaires se composent en une seule transformation linéaire :

u=(XW1+b1)W2+b2=X(W1W2)+(b1W2+b2)u = (X W_1 + b_1) W_2 + b_2 = X (W_1 W_2) + (b_1 W_2 + b_2)

Pour aller plus loin, il faut insérer une non-linéarité entre les couches.

Trois activations classiques

  • Sigmoid : σ(z)=11+ez\sigma(z) = \dfrac{1}{1 + e^{-z}}, sortie dans (0,1)(0, 1). Saturation pour z|z| grand.
  • Tanh : tanh(z)\tanh(z), sortie dans (1,1)(-1, 1), centrée en 0.
  • ReLU : max(0,z)\max(0, z). Pas de saturation pour z>0z > 0. Activation par défaut en deep learning moderne.
model = nn.Sequential(
nn.Linear(m, 16),
nn.ReLU(), # non-linéarité
nn.Linear(16, 1),
)

La profondeur n'a de sens qu'avec de la non-linéarité.


Notebook complet sur Kaggle (forkable) →