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
Fiche de cours · Formules
Formules à connaître
| Activation | φ(z) | φ′(z) | Remarque |
|---|---|---|---|
| Sigmoïde | 1/(1 + e−z) | σ(1 − σ) ≤ 1/4 | Gradient qui s’évanouit en profondeur |
| tanh | (ez − e−z)/(ez + e−z) | 1 − tanh² ≤ 1 | Centrée en 0 |
| ReLU | max(0, z) | 1z>0 | Pas de saturation ; « neurones morts » si z < 0 toujours |
| GELU / SiLU | z·Φ(z) / z·σ(z) | lisse | Transformers |
Fiche de cours · Théorèmes et démonstrations
Démonstrations à savoir refaire (1/2)
Fiche de cours · Théorèmes et démonstrations
Démonstrations à savoir refaire (2/2)
Fiche de cours · Méthodes
Méthodes et pièges
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
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).
| Objet | Forme | Nombre de paramètres |
|---|---|---|
| Entrée X | (n, d0) | — |
| Couche l | W : (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.
- Sortie : pour softmax + entropie croisée, δL = (P − Y)/n (P : probabilités, Y : one-hot). Pour MSE, δL = 2(Ŷ − Y)/n.
- Traverser une activation : δl = (∂J/∂Hl) ⊙ φ′(Zl) (produit terme à terme ; ReLU′ = 1[Z > 0]).
- Traverser une couche affine : ∂J/∂Wl = Hl−1ᵀ δl ; ∂J/∂bl = Σlignes δl ; ∂J/∂Hl−1 = δl Wlᵀ.
- 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ôme | Diagnostic probable | Remède |
|---|---|---|
| La perte ne bouge pas (ou NaN) | Pas d’apprentissage trop grand, gradient faux, données non normalisées | Vérifier le gradient ; lr ÷ 10 ; standardiser ; écrêter le gradient |
| La perte descend très lentement | lr trop petit, initialisation trop petite, sigmoïdes saturées | lr × 3 ; init He ; ReLU ; Adam |
| Train ↓, validation ↑ | Sur-apprentissage | Plus de données, augmentation, dropout, L2, arrêt anticipé, modèle plus petit |
| Train et validation hauts | Sous-apprentissage | Modèle plus grand, plus d’epochs, moins de régularisation, meilleures caractéristiques |
| Impossible de mémoriser 10 exemples | Bug | Le premier test à faire : un réseau doit toujours pouvoir sur-apprendre un lot minuscule |
| Résultats différents à chaque exécution | Variance des graines | Fixer 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)
- Installer.
pip install torch torchvision matplotlib(CPU suffit ; GPU NVIDIA : suivre pytorch.org). Vérifier :python -c "import torch; print(torch.__version__, torch.cuda.is_available())". - Porter le module. Reprenez les classes
Affine,ReLU,Reseau,SGDdu cours dansnumpy_net.pyavec un test de gradient numérique sur le réseau complet (défi ★). Puistorch_net.py: le même réseau avecnn.Sequential(nn.Linear(64,128), nn.ReLU(), nn.Linear(128,10)),F.cross_entropy,torch.optim.SGD. Sur le mini-MNIST 8×8 (sklearn.datasets.load_digits), les deux doivent donner la même courbe de perte à graine égale (copiez les mêmes poids initiaux dans les deux :with torch.no_grad(): lin.weight.copy_(torch.tensor(W.T))). - Autograd. Comparez
loss.backward()à vos gradients NumPy :lin.weight.gradvscouche.dW.T, écart < 1e-6. Vous venez de vérifier PyTorch avec votre code. - Vrai MNIST.
torchvision.datasets.MNIST(download=True),DataLoader(batch_size=64, shuffle=True). MLP [784, 256, 10], Adam lr 1e-3, 5 epochs : ≥ 97,5 % test. Puis un CNN :Conv2d(1,16,3) → ReLU → MaxPool → Conv2d(16,32,3) → ReLU → MaxPool → Flatten → Linear: ≥ 99 %. Temps par epoch CPU vs GPU si disponible. - Diagnostic. Courbes train/val ; matrice de confusion ; les 20 images les plus mal classées (avec leur probabilité) ; balayage de lr sur une échelle log ; effet du dropout et de l’augmentation (rotations ±10°).
- Livrable. Dépôt avec les deux implémentations, le test d’égalité NumPy/PyTorch, les courbes, un tableau (modèle, paramètres, temps, exactitude test ± écart-type sur 3 graines).
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) où 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.stdLa 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
| Mot | Définition |
|---|---|
| Neurone / couche | activation(w·x + b) / activation(Wx + b). |
| ReLU, sigmoïde, softmax | Non-linéarités ; softmax pour K classes. |
| Rétropropagation | Règle de la chaîne appliquée en arrière, en matrices. |
| Vérification de gradient | Comparer à des différences finies. |
| Mini-lot / epoch | Sous-échantillon par pas / passage complet sur les données. |
| Initialisation He / Xavier | Échelle des poids qui garde les activations stables. |
| Momentum / Adam | Optimiseurs avec inertie / pas adaptatif. |
| Dropout / arrêt anticipé | Régularisations. |
| Convolution | Noyau local partagé ; localité et invariance par translation. |
| BatchNorm / résiduel | Techniques qui permettent la grande profondeur. |
| Approximation universelle | Une 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
- Sur PC :
pip install torch, réécrire ce module en PyTorch (nn.Linear,nn.ReLU,optim.Adam) et entraîner sur le vrai MNIST (torchvision.datasets.MNIST) : viser 98 %. - Suivre « micrograd » puis « makemore » (vidéos de Karpathy) en codant chaque ligne.
- Lire le chapitre 6 de Nielsen sur les convolutions et l’article original de LeNet-5.