Avant-propos
L'intelligence artificielle est l'une des inventions technologiques majeures de notre époque. Sa variante la plus impressionnante, notamment celle des grands modèles de langage (LLM) comme ChatGPT ou Claude, est le « Deep Learning » ou apprentissage profond. Cet article a pour but d'en faire une introduction accessible au plus grand nombre.
Un modèle de Deep Learning est un Réseau de Neurones Artificiel (RNA). C'est l'interconnexion de fonctions mathématiques appelées neurones qui permet à notre modèle de fonctionner. Pas de panique, je détaillerai ces concepts par la suite et je suis persuadé que cela vous paraîtra limpide.
Le saviez-vous ? Selon un rapport de Microsoft, en 2025, 35 % des entreprises utilisent déjà l'IA quotidiennement dans leurs opérations. Environ 1,5 milliard de personnes utilisent des outils d'IA générative au quotidien. (Global AI Adoption, 2025)
Les ingénieurs qui développent de nouveaux outils d'IA utilisent bien souvent des bibliothèques spécialisées (PyTorch, TensorFlow…). Cela signifie qu'ils utilisent des outils d'IA qui existent déjà, sans avoir besoin de les recréer de zéro, et ne ressentent donc pas toujours le besoin de comprendre leur fonctionnement réel.
Par conséquent, peu de gens en dehors des chercheurs en intelligence artificielle s'intéressent vraiment au cœur du fonctionnement mathématique et algorithmique des réseaux de neurones artificiels.
Dans cet article, je vais expliquer le fonctionnement détaillé du premier réseau de neurones : le Perceptron Multicouche (MLP). Les MLP sont encore des rouages essentiels des Transformers, les architectures de Deep Learning les plus puissantes, théorisées en 2017 par Google (Attention Is All You Need). Pour l'anecdote, l'acronyme « GPT » signifie littéralement Generative Pre-Trained Transformers.
Pour vraiment comprendre les MLP en profondeur, vous allez m'accompagner dans la création d'un réseau de neurones sans bibliothèques spécialisées. Nous allons construire un réseau de neurones de zéro qui aura une tâche spécifique : résoudre le problème historique du XOR.
Prérequis : Aucun. Pour comprendre le code en détail, il est judicieux d'avoir des bases en programmation Python orientée objet (OOP).
Contexte historique
L'histoire des réseaux de neurones artificiels commence dans les années 1940, lorsque les neurophysiologistes Warren McCulloch et Walter Pitts proposent le premier modèle mathématique formel d'un neurone artificiel. L'idée est révolutionnaire : simuler le fonctionnement du cerveau humain à l'aide de mathématiques et d'électronique.
En 1958, Frank Rosenblatt invente le Perceptron multicouche : le premier ancêtre direct de nos réseaux modernes. Ce modèle apprend à classer des données en ajustant des poids numériques, un peu comme un enfant qui apprend par essais et erreurs. L'enthousiasme est immense : on imagine déjà des machines capables de reconnaître des objets, de comprendre le langage, de prendre des décisions.
Mais en 1969, les mathématiciens Marvin Minsky et Seymour Papert publient un livre dévastateur : ils démontrent que le perceptron simple est fondamentalement incapable de résoudre certains problèmes de base, dont la fonction XOR.
Le problème XOR : La fonction XOR est non linéaire. Les neurones artificiels sont des fonctions linéaires. Il est impossible pour une fonction linéaire, aussi complexe soit-elle, d'approximer une fonction non linéaire. Cela signifiait que l'IA n'était capable de résoudre qu'un nombre très limité de tâches. C'est le premier grand hiver de l'IA : une période de désillusion générale et de coupes budgétaires massives.
Table de vérité de la fonction XOR
| A | B | A XOR B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Il faudra attendre les années 1980 pour que la solution soit découverte : en empilant plusieurs couches de neurones et en introduisant des fonctions d'activation non linéaires, on obtient un réseau capable de résoudre XOR, et bien plus encore. L'algorithme de rétropropagation du gradient (backpropagation), popularisé par Rumelhart, Hinton et Williams en 1986, donne enfin un moyen efficace d'entraîner ces réseaux multicouches.
Aujourd'hui, les réseaux qui font tourner ChatGPT ou Gemini s'appuient toujours sur ces mêmes principes fondamentaux. C'est pourquoi comprendre le MLP, c'est comprendre le cœur de l'IA moderne.
Déplace les curseurs pour modifier les entrées A et B. Le réseau (déjà entraîné, 6 000 itérations) calcule la prédiction en temps réel.
Fonctionnement général
Un réseau de neurones artificiel est une fonction mathématique prédictive. Elle prend une information en entrée et produit une prédiction en sortie. Par exemple, un modèle de langage génératif prend en entrée notre phrase (prompt) et calcule la probabilité de chaque mot (token) que produirait un être humain.
Ces architectures mathématiques ont la particularité de pouvoir s'auto-évaluer et de s'améliorer grâce à des données. C'est ce qu'on appelle communément l'apprentissage automatique (Machine Learning).
Les paramètres : Un paramètre est un nombre. Ce qui rend un réseau différent d'un autre, ce sont les valeurs et la quantité de ses paramètres. Plus un RNA a de neurones, plus il a de paramètres, car il y aura davantage de liens entre les neurones.
Le but est donc de trouver les valeurs des paramètres qui permettent au réseau de faire les meilleures prédictions possibles.
Les trois étapes de l'apprentissage
Propagation avant (Forward Pass)
Le réseau fait une prédiction. Les données d'entrée traversent le réseau de couche en couche. En sortie, on obtient une ou plusieurs probabilités — par exemple, 89 % de chance que l'image représente un être humain, 11 % de chance que non.
Calcul de l'erreur (Loss)
Le modèle s'auto-évalue. Il mesure à quel point sa prédiction est éloignée de la réponse attendue grâce à une fonction de perte.
Rétropropagation (Backward Pass)
Le modèle calcule comment ajuster chacun de ses paramètres pour s'améliorer, puis met à jour leurs valeurs pour réduire l'erreur de prédiction.
Le neurone artificiel
Un neurone est une fonction mathématique à n variables. Il reçoit un nombre n d'entrées et produit une valeur en sortie. Par exemple, pour n = 3, les entrées peuvent être 2, 3 et 0,00001, et la sortie 42.
Un neurone possède les caractéristiques suivantes : pour chaque entrée, il possède un poids qui lui est associé, ainsi qu'un biais (un nombre stocké dans la mémoire du neurone).
Lorsqu'un neurone s'active (feedforward), il multiplie la valeur de chacune de ses entrées par le poids associé, puis additionne le biais.
Enfin, la sortie du neurone passe dans une fonction appelée fonction d'activation (sigmoïde) : une fonction non linéaire qui prend un nombre en entrée et retourne une valeur comprise entre 0 et 1.
Formule du neurone
\[ \text{Sortie} = \sigma\!\left(\sum_{i=1}^{n} w_i \cdot x_i + b\right) = \sigma(\mathbf{w} \cdot \mathbf{x} + b) \]Pour les plus attentifs, cela ressemble à la formule du produit scalaire ! En effet, la sortie d'un neurone (avant activation) est le produit scalaire de ses poids et de ses entrées, auquel on additionne un biais.
Analogie visuelle : Imaginez que les poids représentent la pente d'une droite, et le biais son point de départ. En faisant varier ces deux valeurs, on peut tracer n'importe quelle droite, mais seulement des droites. C'est précisément pourquoi le premier hiver de l'IA a eu lieu : la fonction XOR n'est pas une droite.
Forme vectorielle compacte
\[ z = \mathbf{X} \cdot \mathbf{W} + b \]Couches de neurones
Généralement, toutes les sorties des neurones d'une couche entrent dans chacun des neurones de la couche suivante. Tous les neurones d'une même couche ont les mêmes entrées, mais pas les mêmes poids ni le même biais.
C'est bien pour cela qu'au début de l'entraînement, les poids et les biais sont générés aléatoirement. Car sinon, tous les neurones auraient la même sortie et le réseau les corrigerait tous de la même manière, comme si l'on n'en avait qu'un seul par couche. Ce serait évidemment bien moins efficace.
Chaque neurone possède un biais et des poids qui lui sont propres. Il est important que le nombre de poids soit égal au nombre d'entrées : la dimension du vecteur des entrées doit être égale à la dimension du vecteur des poids.
Implémentation en Python
class Dense(Layer): def __init__(self, in_dim, out_dim, seed=0): rng = np.random.default_rng(seed) # Poids : valeurs aléatoires autour d'une gaussienne self.W = rng.normal(0.0, 0.5, size=(in_dim, out_dim)) # Biais : initialisé à zéro (convention) self.b = np.zeros((1, out_dim)) # Vecteurs de gradients pré-alloués self.dW = np.zeros_like(self.W) self.db = np.zeros_like(self.b) def forward(self, x): self.x = x # mémorisé pour backward return x @ self.W + self.b
Pourquoi initialiser les biais à zéro ? Contrairement aux poids, les biais n'ont pas besoin de symétrie brisée pour fonctionner. Un biais à zéro est un point de départ neutre que le réseau ajustera de lui-même lors de l'entraînement.
Fonction d'activation
Le problème majeur des débuts de l'IA était que les RNA n'étaient pas capables d'implémenter des fonctions non linéaires. Pour une raison très simple : un neurone artificiel est une fonction linéaire.
Par définition, la combinaison linéaire d'une fonction linéaire est toujours linéaire
\[ T(c\mathbf{A} + d\mathbf{B}) = c \cdot T(\mathbf{A}) + d \cdot T(\mathbf{B}) \]Pour prendre une analogie visuelle : on ne pourra jamais dessiner un cercle ou une courbe en traçant uniquement des droites. Cela donnera toujours une sorte de polygone. Or la plupart des fonctions utiles sont non linéaires.
Pour garder la logique des neurones tout en brisant la linéarité, dans les années 1980, les scientifiques ont trouvé un stratagème ingénieux : la fonction d'activation.
Une fonction d'activation transforme la sortie d'un neurone via une fonction non linéaire. Par exemple, si notre neurone produit en sortie 40 003, la fonction d'activation peut la transformer en 0,065. L'intérêt ? Une fonction d'activation est non linéaire, ce qui permet à l'ensemble du réseau de modéliser des relations non linéaires.
La fonction Sigmoïde
Nous allons utiliser la fonction Sigmoïde car elle se dérive facilement (vous verrez plus tard pourquoi c'est utile) et donne en sortie une valeur entre 0 et 1, le format idéal pour un calcul de probabilité, qui est toujours comprise entre ces deux valeurs.
Fonction Sigmoïde et sa dérivée
\[ \sigma(z) = \frac{1}{1 + e^{-z}} \qquad \sigma'(z) = \sigma(z) \cdot (1 - \sigma(z)) \]class Sigmoid(Layer): def forward(self, z): z = np.clip(z, -50, 50) # éviter les overflows self.s = 1.0 / (1.0 + np.exp(-z)) return self.s def backward(self, grad_out): # σ'(z) = σ(z) * (1 - σ(z)) return grad_out * (self.s * (1.0 - self.s))
Auto-évaluation : la Loss Function
Lorsqu'on entraîne un réseau de neurones pour une tâche spécifique, nous savons quel résultat nous voulons obtenir. Pour cela, nous avons besoin de données d'entraînement : des données qui correspondent à ce que l'on veut que notre IA prédise.
Par exemple, si on entraîne une IA censée prédire le nombre de boîtes de LEGO vendues à Chicago l'année prochaine, nous avons besoin des données de ventes des dernières années. Plus nous disposerons de données pertinentes, plus notre modèle pourra apprendre et ajuster ses paramètres pour devenir performant.
Dans le monde de l'intelligence artificielle, l'une des ressources les plus cruciales sont les données. Sans données, pas d'entraînement. Sans entraînement, pas d'apprentissage. Sans apprentissage, pas de résultats.
Mean Squared Error (MSE).">Pour évaluer notre réseau, on calcule l'écart entre la réponse attendue et la prédiction. Plus l'écart est faible, meilleure est la prédiction. C'est le rôle de la Loss Function (fonction de perte). Nous utilisons l'Erreur Quadratique Moyenne (MSE).
Mean Squared Error (MSE)
\[ \text{MSE} = \frac{1}{N} \sum_{i=1}^{N} (\hat{y}_i - y_i)^2 \]class MSELoss: def forward(self, y_pred, y_true): self.y_pred = y_pred self.y_true = y_true return float(np.mean((y_pred - y_true) ** 2)) def backward(self): n = self.y_pred.shape[0] return 2 * (self.y_pred - self.y_true) / n
Auto-correction : la Backpropagation
On sait désormais à quel point notre réseau est imprécis. Mais comment l'améliorer ? C'est exactement le rôle de la rétropropagation du gradient : l'actualisation des paramètres pour améliorer le RNA.
Ce qui fait la spécificité et la performance d'un réseau de neurones, ce sont ses paramètres. C'est en modifiant soigneusement leurs valeurs qu'on améliore un RNA. Certains RNA contiennent des millions, voire des milliards de paramètres. Pour savoir lesquels ajuster et de combien, nous utilisons un outil issu du calcul différentiel d'Isaac Newton.
Rappel : La dérivée partielle d'une fonction par rapport à une de ses variables nous apprend à quel point la fonction va varier si on fait varier cette variable. Pour schématiser : en étudiant une heure de plus pour mon contrôle, ma moyenne va-t-elle augmenter de 1 %, de 5 %, ou va-t-elle diminuer à cause de la fatigue engendrée ?
La dérivée partielle de la Loss par rapport à un paramètre nous dit : à quel point la Loss va varier si je fais varier ce paramètre ? Nous saurons alors dans quel sens modifier nos paramètres pour réduire notre MSE.
En appliquant la règle de la dérivée en chaîne, on calcule les gradients de chaque couche en multipliant le gradient entrant par le vecteur X des entrées de la couche. Le learning rate (lr), ou taux d'apprentissage, est une constante fixée à l'avance qui définit la vitesse à laquelle le réseau s'actualise.
La règle de la chaîne (Chain Rule)
\[ \frac{\partial L}{\partial W} = \frac{\partial L}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial W} \]Gradients de la couche Dense
\[ \frac{\partial L}{\partial W} = X^T \cdot \nabla_{out} \quad \frac{\partial L}{\partial b} = \sum \nabla_{out} \quad \frac{\partial L}{\partial X} = \nabla_{out} \cdot W^T \]Mise à jour des poids (descente de gradient)
\[ W \leftarrow W - \eta \cdot \frac{\partial L}{\partial W} \qquad b \leftarrow b - \eta \cdot \frac{\partial L}{\partial b} \]où η est le learning rate (taux d'apprentissage)
def backward(self, grad_out): # grad_out = dL/d(out) de shape (N, out_dim) self.dW = self.x.T @ grad_out # dL/dW self.db = np.sum(grad_out, axis=0, keepdims=True) # dL/db grad_x = grad_out @ self.W.T # dL/dx return grad_x def step(self, lr): # Descente de gradient self.W -= lr * self.dW self.b -= lr * self.db
Implémentation complète
Maintenant que nous avons compris chaque composant, assemblons-les. Notre MLP aura l'architecture suivante : Dense(2→2) → Sigmoid → Dense(2→1) → Sigmoid. Deux neurones cachés suffisent pour résoudre XOR.
Le réseau (MLP)
class MLP: """ Dense(2→2) → Sigmoid → Dense(2→1) → Sigmoid XOR n'est pas linéairement séparable : une couche cachée non-linéaire rend le problème résoluble. """ def __init__(self, seed=0): self.layers = [ Dense(2, 2, seed=seed), Sigmoid(), Dense(2, 1, seed=seed + 1), Sigmoid() ] def forward(self, x): for layer in self.layers: x = layer.forward(x) return x def backward(self, grad_out): # On remonte les couches en sens inverse for layer in reversed(self.layers): grad_out = layer.backward(grad_out) def step(self, lr): for layer in self.layers: layer.step(lr)
L'entraîneur (Trainer)
class Trainer: def __init__(self, model, lr=0.5): self.model = model self.lr = lr self.loss_fn = MSELoss() def fit(self, X, Y, epochs=5000): for epoch in range(1, epochs + 1): # 1) Forward : prédiction Y_pred = self.model.forward(X) # 2) Loss : évaluation loss = self.loss_fn.forward(Y_pred, Y) # 3) Backward : gradients grad = self.loss_fn.backward() self.model.backward(grad) # 4) Update : correction des poids self.model.step(self.lr)
Résolution du XOR
# Dataset XOR (4 points) X = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]]) Y = np.array([[0.0], [1.0], [1.0], [0.0]]) model = MLP(seed=42) trainer = Trainer(model, lr=0.8) trainer.fit(X, Y, epochs=6000) proba, binary = trainer.predict(X) print("Probas :", np.round(proba, 4)) print("Sortie :", binary)
Résultats après 6 000 itérations
| Entrée (A, B) | XOR attendu | Prédiction du réseau | Sortie binaire |
|---|---|---|---|
| (0, 0) | 0 | ≈ 0.02 | 0 ✓ |
| (0, 1) | 1 | ≈ 0.97 | 1 ✓ |
| (1, 0) | 1 | ≈ 0.97 | 1 ✓ |
| (1, 1) | 0 | ≈ 0.03 | 0 ✓ |
Le réseau a appris la fonction XOR ! En partant de poids aléatoires, après 6 000 itérations de forward → loss → backward → update, notre MLP produit des prédictions proches de 0 et 1 avec une marge d'erreur inférieure à 3 %. Notre réseau est donc bien capable d'approximer une fonction non linéaire, dans ce cas précis la fonction XOR !
Appuie sur Play pour voir le réseau apprendre XOR depuis zéro. La loss descend, les poids changent, les prédictions s'améliorent.
Références
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep learning. MIT Press. https://www.deeplearningbook.org/
- McCulloch, W. S., & Pitts, W. (1943). A logical calculus of the ideas immanent in nervous activity. Bulletin of Mathematical Biophysics, 5(4), 115–133. https://doi.org/10.1007/BF02478259
- Microsoft. (2025). Global AI adoption report. https://www.microsoft.com
- Minsky, M., & Papert, S. (1969). Perceptrons : An introduction to computational geometry. MIT Press.
- Nielsen, M. (2015). Neural networks and deep learning. Determination Press. http://neuralnetworksanddeeplearning.com/
- Rosenblatt, F. (1958). The perceptron : A probabilistic model for information storage and organization in the brain. Psychological Review, 65(6), 386–408. https://doi.org/10.1037/h0042519
- Rumelhart, D. E., Hinton, G. E., & Williams, R. J. (1986). Learning representations by back-propagating errors. Nature, 323(6088), 533–536. https://doi.org/10.1038/323533a0
- Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, Ł., & Polosukhin, I. (2017). Attention is all you need. Dans Advances in Neural Information Processing Systems (Vol. 30). Curran Associates. https://arxiv.org/abs/1706.03762