LYCÉE → PRÉPA · L14

Module L14 · Partie H · Intelligence artificielle

Réseaux de neurones : les construire en NumPy.

Un réseau de neurones est une composition de fonctions simples — produit matriciel, non-linéarité — dont on ajuste les paramètres par descente de gradient. Rien de plus. Ce module le construit couche par couche, dérive la rétropropagation à la main, vérifie chaque gradient numériquement, et entraîne un classifieur de chiffres manuscrits dans la page. Quand vous utiliserez PyTorch, vous saurez exactement ce qu’il fait.

Durée : 4 séances · Prérequis : L12 (gradient, autodiff), L13. Objectifs : neurone, couche, activation, perceptron multicouche ; théorème d’approximation universelle (intuition) ; rétropropagation matricielle ; initialisation, SGD, momentum, Adam ; régularisation (dropout, arrêt anticipé) ; vérification numérique des gradients ; mini-MNIST ; convolution.

Ce que vous saurez faire à la fin
  • Écrire un perceptron multicouche complet en NumPy avec passe avant, arrière et optimiseur.
  • Dériver et vérifier les gradients de chaque couche.
  • Entraîner un réseau à >95 % sur des chiffres 8×8 et lire ses courbes d’apprentissage.
  • Expliquer ce que fait une convolution et pourquoi elle est adaptée aux images.

Références : Neural Networks and Deep Learning (Nielsen, gratuit), cours CS231n (Stanford), série « Neural networks » de 3Blue1Brown, Deep Learning (Goodfellow) chapitres 6-8, « micrograd » et « makemore » (Karpathy).

Fiche de cours · Définitions

Réseaux de neurones : définitions

Définition (neurone, couche, réseau). Un neurone calcule a = φ(wᵀx + b) : combinaison linéaire puis activation non linéaire φ. Une couche dense applique a = φ(Wx + b) à un vecteur. Un perceptron multicouche (MLP) enchaîne L couches : h = φ(Whℓ−1 + b), h₀ = x. Les paramètres θ = (W, b) sont appris ; l’architecture (tailles, activations) est fixée.
Définition (activations). Sigmoïde σ(z) = 1/(1 + e−z) ∈ ]0, 1[ ; tanh ∈ ]−1, 1[ ; ReLU(z) = max(0, z) ; softmax(z)k = ezkj ezj (sortie multi-classes, somme 1).
Définition (perte, gradient, rétropropagation). On minimise L(θ) = (1/n)Σ ℓ(fθ(xi), yi). Le gradient ∇θL est calculé par la rétropropagation : application de la règle de dérivation en chaîne de la sortie vers l’entrée, en réutilisant les valeurs du passage avant. Coût : ≈ 2× le passage avant.
Définition (descente de gradient stochastique, époque, mini-lot). À chaque pas, le gradient est estimé sur un mini-lot de m exemples : θ ← θ − α·ĝ. Une époque = un passage sur toutes les données. Variantes : momentum (v ← βv + g, θ ← θ − αv), Adam (moments d’ordre 1 et 2 normalisés).
Définition (régularisation des réseaux). Weight decay (L2), dropout (mettre à zéro aléatoirement une fraction p des activations à l’entraînement, tout garder à l’inférence en multipliant par 1 − p), arrêt précoce (early stopping sur la validation), augmentation de données, normalisation par lots (batch norm).
Définition (convolution). Une couche de convolution applique le même petit noyau K (3×3, 5×5) à toutes les positions de l’image : (I ∗ K)(i, j) = Σu,v I(i+u, j+v)K(u, v). Partage des poids ⇒ peu de paramètres, invariance par translation. Pooling : réduction de résolution (max sur 2×2).

Fiche de cours · Formules

Formules à connaître

Passage avant : z = Whℓ−1 + b, h = φ(z)
Rétropropagation : δL = ∂ℓ/∂zL ; δ = (Wℓ+1ᵀ δℓ+1) ⊙ φ′(z) ; ∂L/∂W = δ hℓ−1ᵀ ; ∂L/∂b = δ
Softmax + entropie croisée : ∂ℓ/∂z = p − y (y one-hot) le gradient de sortie est « prédiction moins cible », comme en régression logistique
Activationφ(z)φ′(z)Remarque
Sigmoïde1/(1 + e−z)σ(1 − σ) ≤ 1/4Gradient qui s’évanouit en profondeur
tanh(ez − e−z)/(ez + e−z)1 − tanh² ≤ 1Centrée en 0
ReLUmax(0, z)1z>0Pas de saturation ; « neurones morts » si z < 0 toujours
GELU / SiLUz·Φ(z) / z·σ(z)lisseTransformers
Nombre de paramètres d’une couche dense nin → nout : nin·nout + nout ; d’une convolution Cin → Cout, noyau k×k : k²·Cin·Cout + Cout
Taille de sortie d’une convolution : ⌊(n + 2p − k)/s⌋ + 1 (n entrée, p padding, k noyau, s stride)
Initialisation : Var(W) = 2/nin (He, ReLU) ou 1/nin (Xavier, tanh) pour conserver la variance des activations

Fiche de cours · Théorèmes et démonstrations

Démonstrations à savoir refaire (1/2)

Théorème 1 (rétropropagation). Pour un MLP, les gradients de la perte par rapport aux paramètres de chaque couche se calculent par les formules ci-dessus, en un passage arrière de coût comparable au passage avant.
Notons δ = ∂ℓ/∂z (vecteur de la taille de la couche ℓ). Comme zℓ+1 = Wℓ+1φ(z) + bℓ+1, la règle de la chaîne donne ∂ℓ/∂zℓ,i = Σj (∂ℓ/∂zℓ+1,j)(∂zℓ+1,j/∂zℓ,i) = Σj δℓ+1,j Wℓ+1,ji φ′(zℓ,i), soit δ = (Wℓ+1ᵀδℓ+1) ⊙ φ′(z). Puis, z = Whℓ−1 + b étant linéaire en W : ∂ℓ/∂Wℓ,ij = δℓ,ihℓ−1,j, c’est-à-dire δhℓ−1ᵀ, et ∂ℓ/∂b = δ. Chaque couche demande un produit matrice-vecteur (comme à l’aller) et un produit extérieur : même ordre de coût. L’autodiff (PyTorch) généralise cela à tout graphe de calcul : chaque opération connaît sa dérivée locale, et le graphe est parcouru en sens inverse.
Théorème 2 (gradient de softmax + entropie croisée). Pour p = softmax(z) et ℓ = −log pc (c la classe vraie), ∂ℓ/∂zk = pk − 1k=c.
ℓ = −zc + log Σj ezj. ∂ℓ/∂zk = −1k=c + ezkj ezj = pk − 1k=c. Remarque numérique : calculer log Σ ezj comme m + log Σ ezj − m avec m = max z (log-sum-exp) évite le débordement.

Fiche de cours · Théorèmes et démonstrations

Démonstrations à savoir refaire (2/2)

Théorème 3 (évanouissement du gradient). Dans un réseau de L couches à activation sigmoïde et poids de norme ≤ 1, ‖δ₁‖ ≤ (1/4)L−1‖δL‖ : le gradient des premières couches décroît exponentiellement avec la profondeur.
δ = (Wℓ+1ᵀδℓ+1) ⊙ σ′(z) avec σ′ ≤ 1/4 partout ; donc ‖δ‖ ≤ ‖Wℓ+1‖·‖δℓ+1‖/4 ≤ ‖δℓ+1‖/4. Par récurrence sur L − ℓ. Avec des poids plus grands, c’est l’explosion (facteur ‖W‖/4 > 1). Remèdes, tous vérifiables par cette inégalité : ReLU (φ′ = 1 sur la partie active), initialisation qui garde ‖W‖ ≈ 1 (He/Xavier), normalisation par lots, et surtout les connexions résiduelles hℓ+1 = h + F(h) dont le jacobien est I + ∂F : le gradient a un chemin direct de norme 1.
Théorème 4 (initialisation de He). Si les entrées d’une couche ReLU ont une variance v et les poids sont i.i.d. centrés de variance 2/nin, les sorties ont (approximativement) la variance v.
z = Σj wjxj : Var(z) = nin·Var(w)·E[x²] (termes indépendants centrés). Pour x = ReLU(zprécédent) avec zprécédent symétrique de variance v : E[x²] = ½E[zprécédent²] = v/2 (la moitié négative est mise à 0). Donc Var(z) = nin·(2/nin)·(v/2) = v. Sans ce facteur 2 (variance 1/nin), la variance est divisée par 2 à chaque couche : après 20 couches, 10⁻⁶ — les activations et gradients s’effondrent.
Théorème 5 (approximation universelle — énoncé). Toute fonction continue sur un compact de ℝⁿ peut être approchée uniformément, à ε près, par un MLP à une couche cachée (assez large) d’activation non polynomiale (Cybenko 1989, Hornik 1991).
(Admis ; idée en dimension 1 avec ReLU : une combinaison de ReLU est une fonction affine par morceaux, et toute fonction continue sur [a, b] est limite uniforme de fonctions affines par morceaux — il suffit d’un « coude » ReLU par nœud d’interpolation.) Le théorème ne dit rien de la taille nécessaire (exponentielle en général pour une couche) ni de la possibilité de trouver les poids par descente de gradient : la profondeur rend certaines fonctions exponentiellement plus économes à représenter, et l’optimisation est un sujet à part.

Fiche de cours · Méthodes

Méthodes et pièges

Méthode — entraîner un réseau, dans l’ordre. (1) Vérifier le gradient par différences finies sur un petit réseau (erreur relative < 10⁻⁵). (2) Sur-apprendre volontairement un mini-lot de 8 exemples : la perte doit tomber à ~0 — sinon bug. (3) Baseline simple (régression logistique). (4) Choisir le pas par balayage log (10⁻⁴ … 10⁻¹) sur quelques centaines de pas : le plus grand qui ne diverge pas. (5) Suivre perte train et val par époque ; arrêt précoce. (6) Ajouter régularisation si écart train/val ; capacité si les deux stagnent. (7) Fixer la graine, journaliser tout.
Méthode — lire une courbe de perte. Explose → pas trop grand ou gradients non normalisés. Plateau dès le début → pas trop petit, mauvaise initialisation, ReLU mortes, données non standardisées. Oscille sans descendre → pas trop grand ou mini-lot trop petit. Train ↓ val ↑ → sur-apprentissage.
Méthode — dimensionner. Paramètres ≈ données/10 comme point de départ pour un MLP ; images → convolutions ; séquences → transformer (L15). Compter les FLOPs (≈ 2 × paramètres par exemple par passage avant) pour prévoir le temps.

Pièges : oublier model.eval()/dropout à l’inférence ; standardiser le test avec ses propres statistiques ; softmax suivi d’une entropie croisée qui refait un log (instable, et PyTorch attend des logits) ; mélanger les données temporelles ; comparer deux architectures avec des budgets de pas différents ; croire qu’une perte train de 0 est une réussite.

Fiche de cours · Exercices corrigés

Exercices corrigés

Exercice 1. Réseau à une couche cachée de 2 neurones ReLU, entrée x ∈ ℝ², sortie scalaire linéaire, perte quadratique. W₁ = [[1, −1], [0, 1]], b₁ = (0, 0), W₂ = [1, 1], b₂ = 0. Pour x = (1, 2), y = 1 : calculer le passage avant, la perte, et le gradient par rapport à W₂ et W₁.
Correction. z₁ = W₁x + b₁ = (1 − 2, 0 + 2) = (−1, 2) ; h₁ = ReLU(z₁) = (0, 2) ; ŷ = W₂h₁ = 2 ; ℓ = (ŷ − y)² = 1. Arrière : δ₂ = ∂ℓ/∂ŷ = 2(ŷ − y) = 2 ; ∂ℓ/∂W₂ = δ₂h₁ᵀ = (0, 4) ; ∂ℓ/∂b₂ = 2. δ₁ = (W₂ᵀδ₂) ⊙ ReLU′(z₁) = (2, 2) ⊙ (0, 1) = (0, 2) ; ∂ℓ/∂W₁ = δ₁xᵀ = [[0, 0], [2, 4]] ; ∂ℓ/∂b₁ = (0, 2). Le premier neurone (z = −1 < 0) ne reçoit aucun gradient : c’est le phénomène du neurone inactif.
Exercice 2. Un CNN prend des images 32×32×3. Couches : conv 3×3, 16 filtres, padding 1 ; maxpool 2 ; conv 3×3, 32 filtres, padding 1 ; maxpool 2 ; dense → 10. Donner les tailles intermédiaires et le nombre total de paramètres.
Correction. Conv1 : sortie 32×32×16 (padding 1 conserve la taille), paramètres 3·3·3·16 + 16 = 448. Pool → 16×16×16. Conv2 : 16×16×32, paramètres 3·3·16·32 + 32 = 4 640. Pool → 8×8×32 = 2 048 valeurs. Dense 2 048 → 10 : 20 490. Total 25 578. Un MLP dense équivalent 3 072 → 512 → 10 aurait 1,58 million de paramètres : la convolution en économise 60× grâce au partage des poids.
Exercice 3. Expliquer pourquoi la descente de gradient stochastique avec mini-lots de taille m a un gradient dont la variance est divisée par m, et pourquoi on n’utilise pas m = n.
Correction. Le gradient d’un exemple gi est une estimation sans biais du gradient complet g (E[gi] = g si i est tiré uniformément). La moyenne de m tirages i.i.d. a la même espérance et une variance divisée par m (Théorème 2 de L11). Avec m = n on retrouve le gradient exact, mais chaque pas coûte n passages avant/arrière : pour un budget de calcul fixé, on fait n/m fois plus de pas avec des mini-lots, et un gradient un peu bruité suffit à progresser (et aide même à sortir des cols). Compromis : m de 32 à 512, limité par la mémoire du GPU ; augmenter m exige d’augmenter le pas (règle linéaire) pour garder la même dynamique.

01 / Le neurone

Un neurone = régression logistique ; une couche = plusieurs neurones ; un réseau = des couches

Le théorème d’approximation universelle

Un réseau à une couche cachée assez large peut approcher n’importe quelle fonction continue sur un compact, à la précision voulue (Cybenko 1989, Hornik 1991). Intuition avec ReLU : chaque neurone caché est une « rampe » ; en additionnant des rampes de pentes et positions différentes, on construit n’importe quelle fonction affine par morceaux, qui approche n’importe quelle fonction continue. Le théorème dit que c’est possible, pas que la descente de gradient trouvera les poids, ni combien de neurones il faut — la profondeur permet d’en utiliser exponentiellement moins pour certaines fonctions. C’est un sujet de recherche actif.

02 / Rétropropagation

Dériver la passe arrière d’une couche, et la vérifier

D’où viennent ces formules

Z = XW + b ⇒ Z[i,k] = Σ_j X[i,j] W[j,k] + b[k]. Par la règle de la chaîne, ∂L/∂W[j,k] = Σ_i ∂L/∂Z[i,k] · X[i,j] = (Xᵀ dZ)[j,k]. De même pour b (somme sur les exemples i) et pour X (dZ Wᵀ, qu’on transmet à la couche précédente). Le gradient passe de la sortie vers l’entrée, couche par couche, chaque couche multipliant par sa jacobienne locale : c’est la rétropropagation, la règle de la chaîne organisée en matrices. Le module L12 le faisait scalaire par scalaire ; ici, un produit matriciel traite tous les exemples et tous les neurones d’un coup — et c’est ce qui tourne sur GPU.

02 / Rétropropagation

Le réseau complet : perte, optimiseur, boucle d’entraînement

Toute la « magie » tient en quatre lignes : avant, perte, arriere, pas. PyTorch : out = model(X); loss = F.cross_entropy(out, y); loss.backward(); optimizer.step(). Même structure, mêmes noms.

03 / Entraîner

Mini-MNIST : reconnaître des chiffres 8×8 dans la page

03 / Entraîner

Mini-lots, courbes d’apprentissage, arrêt anticipé

Lire les courbes

Train et test montent ensemble puis le train continue seul : c’est le début du sur-apprentissage, et le moment de s’arrêter (arrêt anticipé). Mini-lots : un gradient calculé sur 32 exemples est bruité mais 40 fois moins cher que sur 1200 ; le bruit aide même à généraliser. Un epoch = un passage sur les données = 1200/32 ≈ 38 pas. Ordres de grandeur réels : MNIST (60 000 images 28×28) → 98 % avec ce même code ; ImageNet (1,2 million d’images) → réseaux convolutifs sur GPU pendant des jours.

03 / Entraîner

Ce qui fait ou défait un entraînement : initialisation, Adam, dropout

Ordre de priorité quand ça n’apprend pas : 1) vérifier les gradients numériquement ; 2) sur-apprendre volontairement un tout petit lot (si le réseau n’arrive pas à mémoriser 10 exemples, il y a un bug) ; 3) baisser le pas d’apprentissage ; 4) vérifier l’initialisation et la normalisation des entrées ; 5) seulement ensuite, changer l’architecture.

04 / Convolutions

La convolution : un détecteur local partagé

Pourquoi ça marche sur les images

Deux biais inductifs : la localité (un pixel est lié à ses voisins) et l’invariance par translation (un bord est un bord où qu’il soit). Le noyau partagé encode les deux, avec 400 fois moins de paramètres qu’une couche dense — donc moins de données nécessaires et moins de sur-apprentissage. Un CNN empile convolution → ReLU → sous-échantillonnage (pooling) plusieurs fois, puis une couche dense. LeNet (1998) lisait les chèques ; AlexNet (2012) a lancé l’ère du deep learning en gagnant ImageNet ; ResNet (2015) a ajouté les connexions résiduelles pour aller à 150 couches. Module L15 pour la suite (attention, transformeurs).

Cours

Cours 1 — Le réseau comme composition de fonctions : notation et dimensions

Définition. Un perceptron multicouche à L couches calcule h0 = x, puis pour l = 1..L : zl = Wl hl−1 + bl, hl = φl(zl), et la sortie ŷ = hL. En traitant un lot de n exemples, on empile les exemples en lignes : Hl est (n × dl), Wl est (dl−1 × dl), et Zl = Hl−1 Wl + bl (b diffusé sur les lignes).

ObjetFormeNombre de paramètres
Entrée X(n, d0)
Couche lW : (dl−1, dl), b : (dl,)dl−1·dl + dl
Sortie (K classes)(n, K) logits → softmax → probabilités
Total [64, 128, 10]64·128 + 128 + 128·10 + 10 = 9 610

Règle des dimensions (à vérifier avant de lancer quoi que ce soit) : dans un produit A @ B, la dernière dimension de A égale la première de B ; le gradient de W a la même forme que W ; le gradient qui remonte a la forme de l’entrée de la couche. 90 % des bugs de réseaux sont des erreurs de forme ou de transposée ; assert X.shape == (n, d) partout, et la vérification numérique du gradient (§02) pour le reste.

Cours

Cours 2 — Rétropropagation : la règle de la chaîne écrite proprement

Notation. Pour une perte scalaire J, notons δl = ∂J/∂Zl (même forme que Zl). La rétropropagation calcule les δ de la sortie vers l’entrée, puis les gradients des paramètres.

  1. Sortie : pour softmax + entropie croisée, δL = (P − Y)/n (P : probabilités, Y : one-hot). Pour MSE, δL = 2(Ŷ − Y)/n.
  2. Traverser une activation : δl = (∂J/∂Hl) ⊙ φ′(Zl) (produit terme à terme ; ReLU′ = 1[Z > 0]).
  3. Traverser une couche affine : ∂J/∂Wl = Hl−1ᵀ δl ; ∂J/∂bl = Σlignes δl ; ∂J/∂Hl−1 = δl Wlᵀ.
  4. Répéter jusqu’à la première couche.

Preuve de la ligne 3 (une entrée, pour alléger) : zk = Σj hj Wjk + bk. Donc ∂J/∂Wjk = Σk′ (∂J/∂zk′)(∂zk′/∂Wjk) = δk hj, soit la matrice hᵀδ ; et ∂J/∂hj = Σk δk Wjk = (δ Wᵀ)j. Sur un lot, les contributions des exemples s’additionnent (d’où Hl−1ᵀ δl qui somme sur n). ∎

Coût. La passe arrière coûte environ deux fois la passe avant (deux produits matriciels par couche au lieu d’un) — quel que soit le nombre de paramètres. C’est ce qui rend l’entraînement de 10⁹ paramètres possible, et c’est la même idée que l’autodiff scalaire du module L12.

Cours

Cours 3 — Recette d’entraînement : les hyperparamètres et leur diagnostic

SymptômeDiagnostic probableRemède
La perte ne bouge pas (ou NaN)Pas d’apprentissage trop grand, gradient faux, données non normaliséesVérifier le gradient ; lr ÷ 10 ; standardiser ; écrêter le gradient
La perte descend très lentementlr trop petit, initialisation trop petite, sigmoïdes saturéeslr × 3 ; init He ; ReLU ; Adam
Train ↓, validation ↑Sur-apprentissagePlus de données, augmentation, dropout, L2, arrêt anticipé, modèle plus petit
Train et validation hautsSous-apprentissageModèle plus grand, plus d’epochs, moins de régularisation, meilleures caractéristiques
Impossible de mémoriser 10 exemplesBugLe premier test à faire : un réseau doit toujours pouvoir sur-apprendre un lot minuscule
Résultats différents à chaque exécutionVariance des grainesFixer les graines ; rapporter moyenne ± écart-type sur 5 graines (L24)

Ordre de réglage recommandé (Karpathy, « A Recipe for Training Neural Networks ») : 1) regarder les données ; 2) une baseline minuscule qui marche de bout en bout ; 3) sur-apprendre un petit lot ; 4) régulariser ; 5) chercher les hyperparamètres (lr en premier, sur une échelle log : 10⁻⁴ … 10⁻¹) ; 6) ensembles et moyennes de modèles pour les derniers pourcents.

TP guidé

TP — Le même réseau en NumPy et en PyTorch, jusqu’au vrai MNIST (sur PC, 3 h)

Exercices

Exercices auto-corrigés — couches et gradients

Exercice 1 — Une couche Tanh et une couche Sigmoïde avec passe arrière vérifiée

Sur le modèle de ReLU du cours, écrivez Tanh et Sigmoide (avant/arrière). La cellule vérifie numériquement les deux.

Correction
class Tanh:
    def avant(self, Z): self.A = np.tanh(Z); return self.A
    def arriere(self, dA): return dA * (1 - self.A**2)
class Sigmoide:
    def avant(self, Z): self.A = 1 / (1 + np.exp(-Z)); return self.A
    def arriere(self, dA): return dA * self.A * (1 - self.A)

Exercice 2 — Softmax numériquement stable et perte pondérée

a) log_softmax(Z) stable (soustraire le max, utiliser logsumexp) : doit fonctionner avec des logits de 1000 sans NaN. b) entropie_croisee_ponderee(Z, y, poids)poids[k] pondère la classe k (pour les classes rares), et son gradient par rapport à Z ; vérifiez le gradient numériquement.

Correction
def log_softmax(Z):
    m = Z.max(1, keepdims=True); return Z - m - np.log(np.exp(Z - m).sum(1, keepdims=True))
def entropie_croisee_ponderee(Z, y, poids):
    ls = log_softmax(Z); n = len(y); wi = poids[y]
    L = -(wi * ls[np.arange(n), y]).sum() / wi.sum()
    P = np.exp(ls); dZ = P * wi[:, None]; dZ[np.arange(n), y] -= wi
    return L, dZ / wi.sum()

Exercices

Exercices auto-corrigés — entraîner et diagnostiquer

Exercice 3 — Sur-apprendre un lot minuscule (le test de santé)

Écrivez peut_memoriser(X, y, epochs) qui entraîne un MLP [d, 32, K] (Adam, lr 1e-2) sur les données fournies et renvoie l’exactitude d’entraînement finale. Sur 8 exemples aux étiquettes aléatoires, un réseau correct doit atteindre 100 % ; s’il n’y arrive pas, il y a un bug. Utilisez les classes du module (Affine, ReLU, SoftmaxEntropie, Adam) — elles sont dans l’espace de noms si vous avez exécuté les cellules du cours.

Correction
def peut_memoriser(X, y, epochs=300):
    net = Reseau([X.shape[1], 32, int(y.max()) + 1]); opt = Adam(net.parametres(), lr=1e-2)
    for _ in range(epochs):
        net.perte.avant(net.avant(X), y); net.arriere(); opt.pas()
    return float(np.mean(net.predire(X) == y))

Exercice 4 — Courbe d’apprentissage et décision

courbe_apprentissage(tailles) entraîne le MLP sur des sous-ensembles croissants du mini-MNIST (X_tr, y_tr du cours) et renvoie, pour chaque taille, (exactitude train, exactitude test). Puis complétez la variable diagnostic : « plus de données aideraient » si l’écart train−test reste grand à la plus grande taille, sinon « le modèle plafonne ».

Correction
def courbe_apprentissage(tailles, epochs=15):
    out = []
    for n in tailles:
        idx = rng.permutation(len(y_tr))[:n]
        net = Reseau([64, 64, 10]); opt = Adam(net.parametres(), lr=2e-3)
        for _ in range(epochs):
            for i in range(0, n, 32):
                b = idx[i:i + 32]; net.perte.avant(net.avant(X_tr[b]), y_tr[b]); net.arriere(); opt.pas()
        out.append((np.mean(net.predire(X_tr[idx]) == y_tr[idx]), np.mean(net.predire(X_te) == y_te)))
    return out
# puis, après avoir lu l'écart obtenu :
# diagnostic = "plus de données aideraient"   (écart train−test > 5 points à 1200 exemples) ou "le modèle plafonne"

05 / Défis

Défi ★ — Régression par réseau et vérification de gradient complète

Consigne

1) Ajoutez une perte ErreurQuadratique (avant : mean((Z − y)²), arrière : 2(Z − y)/n) et entraînez un réseau [1, 32, 32, 1] à approcher y = sin(3x) + 0,3x² sur [−2, 2]. Tracez la courbe apprise vs la vraie. 2) Vérifiez numériquement le gradient de tous les paramètres du réseau complet (pas seulement une couche), avec une erreur relative < 1e-5. 3) Combien de neurones cachés faut-il au minimum pour une erreur RMS < 0,05 ?

Correction (extrait)
class ErreurQuadratique:
    def avant(self, Z, y): self.d = Z - y; return (self.d**2).mean()
    def arriere(self): return 2 * self.d / self.d.size
net = Reseau([1, 32, 32, 1]); net.perte = ErreurQuadratique(); opt = Adam(net.parametres(), lr=5e-3)
for e in range(2000):
    net.perte.avant(net.avant(x), y); net.arriere(); opt.pas()
print("RMS :", np.sqrt(((net.avant(x) - y)**2).mean()).round(4))

def verifier_reseau(net, X, y, h=1e-5):
    net.perte.avant(net.avant(X), y); net.arriere(); pire = 0
    for c, n in net.parametres():
        P = getattr(c, n); G = getattr(c, "d" + n)
        for idx in rng.choice(P.size, min(20, P.size), replace=False):
            i = np.unravel_index(idx, P.shape)
            P[i] += h; Lp = net.perte.avant(net.avant(X), y); P[i] -= 2 * h; Lm = net.perte.avant(net.avant(X), y); P[i] += h
            pire = max(pire, abs((Lp - Lm) / (2 * h) - G[i]) / (abs(G[i]) + 1e-8))
    return pire
print("erreur relative max :", f"{verifier_reseau(net, x[:16], y[:16]):.1e}")

05 / Défis

Défi ★★ — Un CNN minimal et un autoencodeur

Consigne

1) Implémentez une couche Conv2D (K noyaux 3×3, passe avant et arrière — l’arrière du noyau est la corrélation entrée ⋆ dOut, celle de l’entrée est la convolution « pleine » de dOut par le noyau retourné), une couche Aplatir, et entraînez [Conv(8) → ReLU → Aplatir → Affine(10)] sur mini-MNIST. Comparez au MLP : exactitude, nombre de paramètres, robustesse à un décalage de 2 pixels des images de test. 2) Autoencodeur [64 → 16 → 2 → 16 → 64] avec erreur quadratique : tracez les 1200 images dans le plan des 2 neurones centraux, colorées par chiffre. Les classes se séparent-elles sans étiquettes ?

Correction (extrait : Conv2D)
class Conv2D:
    def __init__(self, K, k=3): self.W = rng.normal(0, np.sqrt(2 / (k * k)), (K, k, k)); self.b = np.zeros(K); self.k = k
    def avant(self, X):                                   # X : (n, 8, 8) → (n, K, 6, 6)
        self.X = X; n, H, W = X.shape; k = self.k; K = len(self.W)
        self.fen = np.lib.stride_tricks.sliding_window_view(X, (k, k), axis=(1, 2))   # (n, 6, 6, k, k)
        return np.einsum("nijab,kab->nkij", self.fen, self.W) + self.b[None, :, None, None]
    def arriere(self, dZ):                                # dZ : (n, K, 6, 6)
        self.dW = np.einsum("nijab,nkij->kab", self.fen, dZ); self.db = dZ.sum((0, 2, 3))
        dX = np.zeros_like(self.X); k = self.k
        for a in range(k):
            for b in range(k):
                dX[:, a:a + dZ.shape[2], b:b + dZ.shape[3]] += np.einsum("nkij,k->nij", dZ, self.W[:, a, b])
        return dX
class Aplatir:
    def avant(self, X): self.forme = X.shape; return X.reshape(len(X), -1)
    def arriere(self, d): return d.reshape(self.forme)

einsum et sliding_window_view évitent les boucles Python : c’est ainsi (avec en plus le « im2col ») que les bibliothèques transforment une convolution en produit matriciel. Pour l’autoencodeur : le plan des 2 neurones centraux est une réduction de dimension non linéaire apprise sans étiquettes — les chiffres forment des amas, préfiguration de l’apprentissage de représentations et des modèles génératifs (VAE).

05 / Défis

Défi ★★★ — Esprit prépa : normalisation par lots et le problème du gradient qui disparaît

Consigne

1) Entraînez des réseaux de profondeur 2, 6, 12, 20 (32 neurones par couche) avec des sigmoïdes au lieu de ReLU et une initialisation N(0, 0,1) : mesurez la norme du gradient de la première couche. Expliquez (dérivée de la sigmoïde ≤ 0,25, produit de 20 facteurs). 2) Refaites avec ReLU + He : que se passe-t-il ? 3) Implémentez la normalisation par lots (BatchNorm : centrer-réduire chaque activation sur le lot, puis γ·x̂ + β appris ; avant et arrière — la passe arrière est un exercice de dérivation à faire soigneusement, puis à vérifier numériquement). Montrez qu’elle permet d’entraîner le réseau sigmoïde de profondeur 20. 4) Lisez le résumé de l’article « Deep Residual Learning » (He et al., 2015) et expliquez en quoi les connexions résiduelles attaquent le même problème.

Correction (extrait : BatchNorm)
class BatchNorm:
    def __init__(self, m, eps=1e-5): self.W = np.ones(m); self.b = np.zeros(m); self.eps = eps   # W = γ, b = β
    def avant(self, X):
        self.mu = X.mean(0); self.var = X.var(0); self.std = np.sqrt(self.var + self.eps)
        self.xh = (X - self.mu) / self.std; return self.W * self.xh + self.b
    def arriere(self, dY):
        n = len(dY); self.dW = (dY * self.xh).sum(0); self.db = dY.sum(0)
        dxh = dY * self.W
        return (dxh - dxh.mean(0) - self.xh * (dxh * self.xh).mean(0)) / self.std

La formule de la passe arrière vient de ce que μ et σ dépendent de tout le lot : on dérive x̂ = (x − μ)/σ en tenant compte de ∂μ/∂x et ∂σ/∂x. La BatchNorm maintient les activations à une échelle constante à chaque couche, ce qui rend le conditionnement du problème indépendant de la profondeur ; les connexions résiduelles (y = x + f(x)) donnent au gradient un chemin direct (dérivée 1 + ∂f/∂x) qui ne s’atténue pas. Ensemble, elles ont permis de passer de 8 à 1000 couches. Le « pourquoi exactement ça marche » reste débattu (l’explication originale, la « covariate shift », est aujourd’hui contestée) : un bon exemple de question de recherche née de l’ingénierie.

06 / Vérification

On retire toutes les fonctions d’activation d’un réseau à 10 couches. Il devient :

Deux questions supplémentaires

1. Pourquoi vérifier les gradients numériquement ? Parce qu’une erreur de signe ou de transposée dans la passe arrière peut laisser le réseau « apprendre un peu » et passer inaperçue pendant des jours.

2. Que fait le dropout à l’inférence ? Rien : on l’éteint (et on a compensé l’échelle à l’entraînement en divisant par 1 − p).

Référence

Les mots à retenir

MotDéfinition
Neurone / coucheactivation(w·x + b) / activation(Wx + b).
ReLU, sigmoïde, softmaxNon-linéarités ; softmax pour K classes.
RétropropagationRègle de la chaîne appliquée en arrière, en matrices.
Vérification de gradientComparer à des différences finies.
Mini-lot / epochSous-échantillon par pas / passage complet sur les données.
Initialisation He / XavierÉchelle des poids qui garde les activations stables.
Momentum / AdamOptimiseurs avec inertie / pas adaptatif.
Dropout / arrêt anticipéRégularisations.
ConvolutionNoyau local partagé ; localité et invariance par translation.
BatchNorm / résiduelTechniques qui permettent la grande profondeur.
Approximation universelleUne couche cachée suffit en théorie ; la profondeur aide en pratique.

Pour continuer

Vous avez construit un réseau de neurones

Module suivant : le deep learning moderne — séquences, attention, transformeurs et grands modèles de langage : comment ils fonctionnent, implémentés en miniature.

À faire chez soi

← L13SommaireL15 : Deep learning et transformeurs →