Skip to content

Sortir de la séparation linéaire : la couche cachée

Un neurone (régression logistique) ne peut tracer qu’une frontière de décision linéaire — une droite en 2D. Certains problèmes ne sont tout simplement pas séparables par une droite, quel que soit l’entraînement.

Deux anneaux concentriques : une classe au centre, une classe en périphérie. Aucune droite ne peut les séparer.

def make_concentric_rings(n_per_class=150, noise=0.08):
theta1 = np.random.uniform(0, 2 * np.pi, n_per_class)
r1 = 1.0 + np.random.normal(0, noise, n_per_class)
X1 = np.column_stack([r1 * np.cos(theta1), r1 * np.sin(theta1)])
theta2 = np.random.uniform(0, 2 * np.pi, n_per_class)
r2 = 2.5 + np.random.normal(0, noise, n_per_class)
X2 = np.column_stack([r2 * np.cos(theta2), r2 * np.sin(theta2)])
X = np.vstack([X1, X2])
y = np.concatenate([np.zeros(n_per_class), np.ones(n_per_class)])
return X, y.reshape(-1, 1)
print("Accuracy test (neurone seul):", single_acc)
Accuracy test (neurone seul): 0.48333333333333334

Une accuracy proche de 50 % sur un problème à deux classes équilibrées : le neurone unique ne fait pas mieux qu’un tirage au hasard — la confirmation expérimentale de sa limite théorique.

class ShallowNeuralNetwork:
def __init__(self, n_features, n_hidden):
self.W1 = np.random.randn(n_hidden, n_features) * 0.1
self.b1 = np.zeros((n_hidden, 1))
self.W2 = np.random.randn(1, n_hidden) * 0.1
self.b2 = np.zeros((1, 1))
def _forward(self, X):
Z1 = self.W1 @ X + self.b1
A1 = self._sigmoid(Z1)
Z2 = self.W2 @ A1 + self.b2
A2 = self._sigmoid(Z2)
return A1, A2

Chaque neurone de la couche cachée (A1) apprend sa propre frontière linéaire ; la couche de sortie (A2) combine ces frontières linéaires en une frontière globale bien plus riche, capable d’entourer une région circulaire.

def fit(self, X, y, learning_rate=0.5, n_iterations=5000):
X, y = X.T, y.T
m = X.shape[1]
for i in range(n_iterations):
A1, A2 = self._forward(X)
dZ2 = A2 - y
dW2 = (1 / m) * dZ2 @ A1.T
db2 = (1 / m) * np.sum(dZ2, axis=1, keepdims=True)
dZ1 = (self.W2.T @ dZ2) * A1 * (1 - A1)
dW1 = (1 / m) * dZ1 @ X.T
db1 = (1 / m) * np.sum(dZ1, axis=1, keepdims=True)
self.W1 -= learning_rate * dW1
self.b1 -= learning_rate * db1
self.W2 -= learning_rate * dW2
self.b2 -= learning_rate * db2

Le gradient se propage de la sortie vers l’entrée : dZ2 (l’erreur en sortie) sert à calculer dZ1 (l’erreur de la couche cachée), via la dérivée de la sigmoïde (A1 * (1 - A1)) — c’est la rétropropagation, appliquée ici dans son cas le plus simple (2 couches).

print("Accuracy test (reseau a 2 couches):", nn_acc)
Accuracy test (reseau a 2 couches): 1.0

Avec seulement 6 neurones cachés, le réseau sépare parfaitement les deux anneaux — précisément là où le neurone unique échouait totalement.

👉 Généraliser en profondeur : un réseau à N couches


Junior TSAFACK – 12/09/2026