DL · Chapitre 2
Deep learning 2 — Classification avec un neurone logistique et PyTorch
Le chapitre précédent a posé les bases de l'apprentissage profond sur le problème le plus simple : la régression. Un neurone affine, , entraîné par descente de gradient sur l'erreur quadratique moyenne, suffit pour ajuster une droite à un nuage de points. Mais que se passe-t-il quand la cible n'est plus une grandeur continue ? Quand on cherche à savoir si une tumeur est maligne ou bénigne, si un manchot appartient à l'espèce Adelie, Gentoo ou Chinstrap, ou si un courriel est un spam ? Ce sont des problèmes de classification, et ils demandent un changement à la fois conceptuel — passer d'une grandeur réelle à une distribution de probabilité — et technique — remplacer la sortie linéaire par une fonction écrasante, et la MSE par une fonction de coût adaptée à la nature discrète de la cible.
Ce chapitre poursuit donc le fil tendu au cours précédent. Nous reprenons le neurone élémentaire, et nous le transformons en neurone logistique. Nous construisons sa fonction de coût à partir d'un raisonnement probabiliste — le maximum de vraisemblance d'une loi de Bernoulli — qui nous donne la binary cross-entropy. Nous re-dérivons le gradient à la main pour observer un fait remarquable : il prend exactement la même forme algébrique que pour la régression linéaire. Nous traduisons ensuite ce neurone en PyTorch, en exposant les pièges récurrents autour des logits et des pertes (BCELoss contre BCEWithLogitsLoss), avant d'empiler des couches et de généraliser au cas multiclasse, qui mobilise la fonction softmax et la perte CrossEntropyLoss.
Du neurone linéaire au neurone logistique
En classification binaire, la variable cible prend deux valeurs : . La convention usuelle est d'appeler "classe positive" celle codée 1 (par exemple tumeur maligne, spam) et "classe négative" celle codée 0. L'objectif n'est pas de prédire directement une classe, mais d'estimer une probabilité conditionnelle :
C'est cette probabilité qui sera apprise par le modèle. La décision finale (classe 0 ou 1) sera prise ensuite, en seuillant la probabilité estimée — typiquement à , mais pas nécessairement, comme nous le verrons quand le coût d'un faux négatif diffère de celui d'un faux positif.
Modélisation probabiliste : loi de Bernoulli
Une variable binaire est naturellement modélisée par une loi de Bernoulli. Pour un paramètre :
Une écriture compacte, qui nous servira à construire la vraisemblance, regroupe ces deux cas :
Quand on retrouve , quand on retrouve . Ici, est un paramètre inconnu, qui dépendra dans la suite des entrées .
De la probabilité à la vraisemblance
Dans un cadre supervisé, les données sont observées et fixées. La quantité , vue comme fonction de , n'est plus une probabilité au sens strict : on l'appelle alors une vraisemblance. Pour une observation :
Sous l'hypothèse que les observations sont indépendantes, la vraisemblance globale est le produit des vraisemblances individuelles :
où est la probabilité attribuée par le modèle à l'exemple .
Faire dépendre de : le neurone logistique
Pour que le modèle s'adapte aux entrées, doit dépendre de . Comme en régression, on commence par une combinaison affine :
Cette quantité, appelée logit, n'est pas bornée : elle peut prendre n'importe quelle valeur réelle. Pour la transformer en probabilité, on lui applique la fonction logistique (ou sigmoïde) :
Cette fonction écrase vers , est dérivable partout, et possède la propriété élégante que l'on retrouvera dans le calcul du gradient. On pose alors :
Le neurone logistique est donc un estimateur de probabilité conditionnelle. À comparer avec le perceptron historique, qui décidait directement si et sinon : décision dure, sans probabilité, non différentiable, donc incompatible avec la descente de gradient. La sigmoïde est la version lisse et probabiliste de ce seuil.
Cross-entropy et maximum de vraisemblance
En remplaçant chaque par la sortie du neurone logistique, la vraisemblance devient :
L'apprentissage consiste à choisir et qui rendent les données observées les plus vraisemblables possibles : c'est le principe du maximum de vraisemblance.
Pourquoi passer au logarithme
Maximiser un produit de nombres compris entre 0 et 1 conduit rapidement à des valeurs numériquement minuscules : vaut déjà . On maximise donc plutôt la log-vraisemblance :
Le logarithme est strictement croissant : il ne change pas la position du maximum. Il transforme en revanche un produit en somme, ce qui simplifie à la fois le calcul du gradient et la stabilité numérique.
La perte de cross-entropy binaire
Par convention, on minimise plutôt qu'on ne maximise. On définit donc la fonction de coût comme l'opposée de la log-vraisemblance, normalisée par :
Cette fonction porte plusieurs noms selon les communautés : log-loss en science des données, binary cross-entropy en deep learning, negative log-likelihood de la Bernoulli en statistique. Ce sont trois manières de désigner la même quantité.
Pourquoi pas la MSE ? On pourrait être tenté d'utiliser l'erreur quadratique moyenne entre et . Cela fonctionne encore, mais le gradient devient pathologiquement plat quand sature à 0 ou 1 — l'apprentissage s'arrête prématurément. La cross-entropy, dérivée du maximum de vraisemblance, donne au contraire un gradient proportionnel à l'erreur de prédiction, comme nous allons le voir.
Calcul du gradient : un détour qui paye
Le neurone logistique se décompose en trois blocs :
avec , et . La règle de dérivation en chaîne donne :
Calculons chaque facteur séparément.
Loss par rapport à la sortie. En dérivant terme à terme :
Activation par rapport au pré-activé. La dérivée classique de la sigmoïde :
Pré-activé par rapport au poids. Comme :
Recomposition. En multipliant les trois et en simplifiant le produit du premier facteur par :
D'où, après simplification :
En version vectorielle puis batch, on obtient les expressions remarquablement compactes :
où .
Le miracle algébrique
Comparons avec le gradient du neurone linéaire (régression) :
- régression linéaire : , gradient
- classification logistique : , gradient
Les expressions du gradient sont formellement identiques. Seule la définition de change. Conséquence pratique : pour transformer un neurone linéaire from scratch en neurone logistique, il suffit de modifier deux lignes — la sortie de forward et la fonction de coût — et de garder rigoureusement la même boucle d'entraînement, le même calcul de gradient, les mêmes mises à jour. C'est ce que nous allons faire.
Implémentation from scratch en NumPy
Le code suivant condense tout ce que nous venons de voir : initialisation paresseuse des poids, méthode forward qui applique la sigmoïde, méthode predict qui seuille à , et boucle d'entraînement qui calcule la BCE et applique la formule de gradient. Le mode batch, sgd ou minibatch est un paramètre, comme dans le chapitre précédent.
class LogisticNeuron: def __init__(self): self.w = None self.b = 0.0 self.history = [] def forward(self, X): z = X @ self.w + self.b return 1.0 / (1.0 + np.exp(-z)) def predict(self, X, threshold=0.5): u = self.forward(X) return (u >= threshold).astype(int) def fit(self, X, y, lr=0.1, epochs=100, mode="batch", batch_size=32): n, m = X.shape if self.w is None: self.w = np.random.uniform(size=m) self.history = [] for _ in range(epochs): if mode == "batch": idx = np.arange(n) elif mode == "sgd": idx = np.array([np.random.randint(0, n)]) elif mode == "minibatch": B = min(batch_size, n) idx = np.random.choice(n, size=B) Xb, yb = X[idx], y[idx] u = self.forward(Xb) eps = 1e-12 loss = -np.mean(yb * np.log(u + eps) + (1 - yb) * np.log(1 - u + eps)) self.history.append(loss) grad_w = (Xb.T @ (u - yb)) / len(idx) grad_b = (u - yb).mean() self.w -= lr * grad_w self.b -= lr * grad_b return self
Stabilité numérique. Notez le
eps = 1e-12ajouté dans leslog: si un vaut exactement 0 ou 1 (par saturation de la sigmoïde), vaut et la perte devientnan. Ce petit décalage évite l'écueil. PyTorch règle ce problème plus proprement avecBCEWithLogitsLoss, comme nous le verrons.
Sur le dataset cancer_mini, après normalisation par StandardScaler, ce neurone atteint typiquement 95 à 98 % d'accuracy en 100 époques avec un taux d'apprentissage de 0,5 — un score honorable pour un modèle qui tient en vingt lignes.
Le neurone logistique en PyTorch
PyTorch nous épargne le calcul manuel du gradient et la gestion des modes batch/SGD : le moteur autograd se charge de différencier toute fonction composée, et les classes du module torch.optim mettent à jour les paramètres. Reste à construire le modèle et à choisir la perte — et c'est précisément là que se cachent les pièges les plus fréquents.
Deux options, et une recommandation
Option 1 : sigmoïde explicite + BCELoss. Le modèle calcule explicitement la probabilité , et la perte applique la binary cross-entropy.
model = nn.Sequential( nn.Linear(m, 1), nn.Sigmoid() ) criterion = nn.BCELoss()
Option 2 (recommandée) : pas de sigmoïde + BCEWithLogitsLoss. Le modèle renvoie les logits bruts , et la perte applique la sigmoïde et la cross-entropy en interne, de manière numériquement stable.
model = nn.Linear(m, 1) criterion = nn.BCEWithLogitsLoss()
Piège classique. Combiner
nn.Sigmoiddans le modèle etBCEWithLogitsLossrevient à appliquer la sigmoïde deux fois : une fois explicitement, une fois à l'intérieur de la perte. La loss reste finie mais le modèle apprend péniblement. Inversement, oublier la sigmoïde et utiliserBCELossproduit l'erreurRuntimeError: all elements of input should be between 0 and 1dès la première itération. La règle est simple : si la perte contientWithLogits, le modèle ne contient pas l'activation finale.
Préparation des tenseurs
Comme en régression, on convertit les arrays NumPy en tenseurs float32. La cible doit avoir la forme (n, 1), sinon le broadcasting implicite de PyTorch produit une perte sur une matrice — bug silencieux, modèle inutilisable.
X_train = torch.tensor(X_train, dtype=torch.float32) y_train = torch.tensor(y_train, dtype=torch.float32).view(-1, 1) X_test = torch.tensor(X_test, dtype=torch.float32) y_test = torch.tensor(y_test, dtype=torch.float32).view(-1, 1)
Forme de .
BCEWithLogitsLossattend des ciblesfloat32de même forme que les logits, soit(n, 1). La dernière dimension n'est pas accessoire :(n,)et(n, 1)ne sont pas équivalents pour le broadcaster.
Boucle d'entraînement
model = nn.Linear(m, 1) criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.1) epochs = 200 loss_history = [] for _ in range(epochs): optimizer.zero_grad() logits = model(X_train) loss = criterion(logits, y_train) loss.backward() optimizer.step() loss_history.append(loss.item())
Le squelette est strictement identique à celui de la régression : zero_grad, forward, calcul de la perte, backward, step. Seuls la dernière couche (sortie scalaire au lieu de vecteur) et le critère changent.
De logits à classes
À l'évaluation, on bascule en mode eval pour désactiver d'éventuelles couches sensibles (Dropout, BatchNorm — pas encore introduites, mais la discipline se prend tôt), on désactive le calcul du gradient avec torch.no_grad(), on applique manuellement la sigmoïde aux logits, et on seuille :
model.eval() with torch.no_grad(): logits = model(X_test) proba = torch.sigmoid(logits) y_hat = (proba >= 0.5).float()
L'évaluation finale repasse classiquement par scikit-learn :
from sklearn.metrics import accuracy_score, confusion_matrix y_hat_np = y_hat.cpu().numpy().reshape(-1) y_test_np = y_test.cpu().numpy().reshape(-1) print("Accuracy =", accuracy_score(y_test_np, y_hat_np)) print(confusion_matrix(y_test_np, y_hat_np))
Choix du seuil. Le seuil 0,5 est un défaut, pas une vérité. Sur
cancer_mini, manquer une tumeur maligne (faux négatif) est bien plus grave qu'une fausse alerte (faux positif). On peut alors abaisser le seuil — par exemple à 0,3 — pour augmenter le rappel au prix d'un peu de précision. La courbe ROC et l'analyse précision/rappel, vues en ML, restent pertinentes ici.
Réseaux multicouches : profondeur et non-linéarité
Un seul neurone logistique réalise une frontière de décision linéaire : il sépare l'espace par un hyperplan. Pour traiter des frontières plus complexes, on empile des couches, chacune suivie d'une activation non linéaire (typiquement ReLU) :
La sortie reste un logit, et la dernière couche n'a pas d'activation. La probabilité est obtenue après, soit explicitement par torch.sigmoid, soit implicitement par BCEWithLogitsLoss.
model = nn.Sequential( nn.Linear(m, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) # logits, pas d'activation finale ) criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
Pas de ReLU sur la dernière couche. Une
ReLUfinale renverrait des valeurs dans , donc des logits exclusivement positifs — le modèle ne pourrait plus exprimer . La règle générale : la dernière couche est presque toujours unLinearnu, et l'activation propre à la tâche (sigmoïde pour binaire, softmax pour multiclasse) est soit explicite, soit incorporée dans la perte.
La boucle d'entraînement, elle, ne change pas. C'est la grande économie cognitive de PyTorch : modifier l'architecture du modèle ne demande aucune réécriture du training loop, parce que loss.backward() propage automatiquement les gradients à travers toutes les couches enregistrées dans le graphe d'autograd.
Définir un module avec nn.Module
Pour des architectures plus expressives — branches multiples, sauts de connexion, paramètres partagés — nn.Sequential ne suffit plus. On hérite alors de nn.Module :
class LogisticMLP(nn.Module): def __init__(self, m, hidden=(64, 32)): super().__init__() self.fc1 = nn.Linear(m, hidden[0]) self.fc2 = nn.Linear(hidden[0], hidden[1]) self.out = nn.Linear(hidden[1], 1) self.act = nn.ReLU() def forward(self, x): x = self.act(self.fc1(x)) x = self.act(self.fc2(x)) return self.out(x) # logits
Toute couche affectée à self.something est automatiquement enregistrée comme paramètre du module : model.parameters() les expose à l'optimiseur, model.to(device) les déplace vers GPU, model.state_dict() les sérialise.
Classification multiclasse : softmax et CrossEntropyLoss
Quand la cible prend valeurs — espèce de manchot, chiffre manuscrit, type de cellule — le neurone logistique se généralise au cas multiclasse. La cible est :
et chaque observation appartient à exactement une classe. L'objectif est d'estimer la distribution complète :
Logits multiclasse et softmax
Le réseau produit cette fois un vecteur de scores réels, un par classe :
ce qui se traduit en PyTorch par une dernière couche nn.Linear(h, C). Pour transformer ce vecteur de logits en distribution de probabilité, on applique la fonction softmax :
La softmax généralise la sigmoïde : elle écrase chaque composante dans tout en garantissant que la somme vaut 1, ce qui en fait une distribution de probabilité valide.
La perte de cross-entropy multiclasse
Pour un exemple dont la vraie classe est , la perte est :
C'est l'extension naturelle de la log-vraisemblance Bernoulli au cas multinomial. En PyTorch, on n'a pas besoin d'écrire la softmax soi-même :
criterion = nn.CrossEntropyLoss()
Cette perte applique en interne LogSoftmax puis la cross-entropy, encore une fois pour des raisons de stabilité numérique.
Trois pièges connexes. Premièrement : ne pas appliquer de softmax dans le modèle —
CrossEntropyLossattend des logits, pas des probabilités. Deuxièmement : la cible est un vecteur d'indices entiers de forme(n,)et de typetorch.long, pas un encodage one-hot . Troisièmement : ne pas confondreCrossEntropyLoss(logits multiclasse, cible entière) avecNLLLoss(sortie déjà passée parLogSoftmax, cible entière) ni avecBCEWithLogitsLoss(logits binaires ou multilabel, cible flottante).
Anatomie d'un modèle multiclasse
C = 3 # nombre de classes model = nn.Sequential( nn.Linear(m, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, C) # logits multiclasse ) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
À l'évaluation, on prédit la classe par argmax :
with torch.no_grad(): logits = model(X_test) # (n_test, C) y_hat = torch.argmax(logits, dim=1) # (n_test,)
soit, formellement :
Notez qu'on n'a pas besoin de calculer la softmax pour prédire : appliquer une fonction strictement croissante (l'exponentielle) à chaque composante, puis normaliser, ne change pas l'argmax. C'est une économie qu'on apprend à apprécier sur de gros volumes.
Étude de cas : penguins_mini
Le dataset penguins_mini propose trois espèces de manchots — Adelie, Gentoo, Chinstrap — à reconnaître à partir de variables morphologiques (longueur du bec, profondeur du bec, longueur des nageoires, masse corporelle) et catégorielles (île, sexe). Le pipeline de préparation est typique :
- imputation des valeurs manquantes (par exemple, remplir
sexpar"male"puis supprimer les lignes restantes incomplètes) ; - encodage des variables catégorielles (
mappoursex,pd.get_dummiespourisland) ; - encodage de la cible en indices : Adelie → 0, Gentoo → 1, Chinstrap → 2 — qu'on peut faire à la main ou via
LabelEncoder; - split stratifié train/test avec
stratify=ypour préserver la distribution des classes ; - normalisation des entrées numériques avec
StandardScaler.
Côté tenseurs :
X_train_t = torch.tensor(X_train, dtype=torch.float32) y_train_t = torch.tensor(y_train, dtype=torch.long) # long, pas float
Le passage par un DataLoader permet de gérer proprement les minibatches :
from torch.utils.data import TensorDataset, DataLoader train_loader = DataLoader( TensorDataset(X_train_t, y_train_t), batch_size=32, shuffle=True, )
et la boucle d'entraînement itère sur les batches :
for _ in range(epochs): epoch_loss, nb_batches = 0.0, 0 for Xb, yb in train_loader: optimizer.zero_grad() logits = model(Xb) # (B, C) loss = criterion(logits, yb) # yb shape (B,) loss.backward() optimizer.step() epoch_loss += loss.item() nb_batches += 1 loss_history.append(epoch_loss / nb_batches)
Sur ce jeu de données très propre, un MLP à deux couches cachées atteint sans peine une accuracy supérieure à 98 % en 300 époques avec Adam et un taux d'apprentissage de . La matrice de confusion permet d'identifier où le modèle se trompe — souvent entre Adelie et Chinstrap, qui partagent la même île dans une partie du dataset.
Récapitulatif : quelle perte pour quelle tâche ?
Avant les exercices, fixons la grille de décision PyTorch — c'est le tableau qu'il faut avoir en tête à chaque nouveau modèle de classification.
| Tâche | Sortie du modèle | Perte | Cible attendue |
|---|---|---|---|
| Binaire | 1 logit (n, 1) | BCEWithLogitsLoss | float32, (n, 1) |
| Binaire (avec sigmoïde) | 1 proba (n, 1) | BCELoss | float32, (n, 1) |
| Multiclasse | logits (n, C) | CrossEntropyLoss | long, (n,) |
| Multilabel | logits (n, C) | BCEWithLogitsLoss | float32, (n, C) |
Multilabel vs multiclasse. Le cas multilabel (un texte peut être étiqueté à la fois "sport" et "économie") n'est pas couvert par
CrossEntropyLoss, qui suppose une classe et une seule. On revient alors à classifications binaires indépendantes viaBCEWithLogitsLosssur une sortie(n, C).
Exercices
Exercice 1 — Dérivation du gradient logistique
On considère , et la perte .
- Calculer .
- Calculer en utilisant la définition de la sigmoïde.
- Calculer et .
- Appliquer la règle de dérivation en chaîne pour obtenir , et simplifier pour obtenir l'expression compacte .
- Écrire les gradients en version batch et reconnaître la forme matricielle .
Rappels utiles : , , , .
Exercice 2 — Du neurone linéaire au neurone logistique from scratch
À partir du code du LinearNeuron du chapitre précédent, construire un LogisticNeuron :
- modifier
forward(X)pour renvoyer ; - remplacer la MSE par la BCE dans
fit, en ajoutant une constante pour éviter ; - conserver telles quelles les formules du gradient — elles sont identiques au cas linéaire ;
- ajouter une méthode
predict(X, threshold=0.5)qui renvoie une classe binaire ; - tester sur
cancer_mini: split avecrandom_state=42,StandardScaler(fit sur train, transform sur test), entraînement, évaluation par accuracy.
Exercice 3 — Neurone logistique PyTorch sur cancer_mini
- charger
cancer_mini, construire et (binaire 0/1) ; - split stratifié avec
test_size=0.2,random_state=42,stratify=y; - normaliser via
StandardScaler; - convertir en tenseurs
float32, en mettant au format(n, 1); - définir un modèle PyTorch d'un seul
nn.Linear(m, 1)avecBCEWithLogitsLoss; - entraîner en full batch sur 200 époques, stocker la perte, tracer la courbe ;
- à l'évaluation, calculer les logits, appliquer
torch.sigmoid, seuiller à 0,5 ; - calculer accuracy, matrice de confusion, précision et rappel ; comparer plusieurs seuils (0,3, 0,5, 0,7).
Exercice 4 — MLP binaire profond
Reprendre l'exercice 3 en remplaçant le nn.Linear unique par un nn.Sequential à deux couches cachées (par exemple 64 puis 32 unités, ReLU). Comparer la courbe de perte et les métriques de test avec celles du modèle linéaire. À quel moment l'augmentation de la profondeur devient-elle contre-productive sur ce jeu de données ?
Exercice 5 — Classification multiclasse sur penguins_mini
- charger
penguins, traiter les valeurs manquantes, encodersexen 0/1, encoderislandpar one-hot, encoderspeciesen indices entiers ; - split stratifié (
stratify=y,random_state=42) puis normalisation ; - tenseurs :
Xenfloat32,yenlong(sansview(-1, 1)) ; - construire un MLP avec une couche de sortie
nn.Linear(h, C), où ; - utiliser
nn.CrossEntropyLoss— sans softmax dans le modèle ; - entraîner via un
DataLoader(minibatches de taille 32) sur 300 époques avec Adam etlr=1e-3; - évaluer :
argmaxsur les logits de test, accuracy, matrice de confusion ; - bonus : comparer un modèle linéaire (
nn.Linear(m, C)seul) à votre MLP. Quelle hauteur de précision peut-on déjà atteindre sans non-linéarité sur ce dataset ?
Exercice 6 — Pièges PyTorch
Pour chacune des situations suivantes, indiquer si le code lèvera une erreur, donnera un résultat correct mais pédagogiquement louche, ou produira un bug silencieux. Justifier brièvement.
model = nn.Sequential(nn.Linear(m, 1), nn.Sigmoid())aveccriterion = nn.BCEWithLogitsLoss().model = nn.Linear(m, 1)aveccriterion = nn.BCELoss().model = nn.Sequential(nn.Linear(m, C), nn.Softmax(dim=1))aveccriterion = nn.CrossEntropyLoss().- Cible encodée en one-hot
(n, C), typefloat32, avecCrossEntropyLoss. - de forme
(n,)avec un modèle qui sort(n, 1)etBCEWithLogitsLoss.
Pour aller plus loin
- Documentation officielle PyTorch. Les pages
nn.Linear,nn.BCEWithLogitsLoss,nn.CrossEntropyLossettorch.nn.functionalsont tenues à jour à chaque release et précisent exactement les formes attendues, les types et les options (poids des classes, ignorance d'index, label smoothing). - Tutoriel "60-minute blitz" sur le site de PyTorch, et la série Learn the Basics — couvrent en chemin
Dataset,DataLoader,nn.Module, et le déplacement vers GPU. - Goodfellow, Bengio, Courville, Deep Learning, MIT Press 2016 (chap. 3 et 6, libre en ligne sur deeplearningbook.org) — la référence pour la dérivation probabiliste de la cross-entropy.
- Bishop, Pattern Recognition and Machine Learning — chapitre 4 pour la régression logistique vue par un statisticien, et chapitre 5 pour les réseaux de neurones.
- Aurélien Géron, Hands-On Machine Learning with Scikit-Learn, Keras and TensorFlow — bonne mise en perspective pratique, même si le code y est en Keras plutôt qu'en PyTorch ; les concepts (logits, cross-entropy, softmax) sont identiques.
- Andrej Karpathy, série YouTube Neural Networks: Zero to Hero — la première vidéo, micrograd, refait le calcul de gradient à la main que nous avons mené ici, mais étendu à un graphe d'autograd minimaliste : extrêmement formateur.
Au prochain chapitre, nous garderons ce neurone logistique comme brique de base, mais nous nous attaquerons à des entrées dont la structure ne se résume pas à un vecteur de variables tabulaires : les images, qui demandent une nouvelle classe de couches — les convolutions.