import numpy as np
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-z))
def sigmoid_prime(z):
a = sigmoid(z)
return a * (1.0 - a)
def bce_loss(y_true, y_prob, eps=1e-9):
"""Entropie croisée binaire moyennée sur les exemples."""
p = np.clip(y_prob, eps, 1 - eps)
return -np.mean(y_true * np.log(p) + (1 - y_true) * np.log(1 - p))
def xavier_init(rng, fan_in, fan_out):
std = np.sqrt(2.0 / (fan_in + fan_out))
return rng.normal(0.0, std, size=(fan_in, fan_out))
class SimplerMLP:
"""MLP sigmoïde pour la classification binaire; taux constant."""
def __init__(self, layer_sizes, lr=0.1, seed=None):
self.sizes = list(layer_sizes)
self.lr = float(lr)
self.rng = np.random.default_rng(seed)
self.W = []
for din, dout in zip(self.sizes[:-1], self.sizes[1:]):
Wk = xavier_init(self.rng, din, dout)
self.W.append(Wk)
self.b = [np.zeros(dout) for dout in self.sizes[1:]]
def forward(self, X):
a = X
for W, b in zip(self.W, self.b):
a = sigmoid(a @ W + b)
return a.ravel()
def predict_proba(self, X):
return self.forward(X)
def predict(self, X, threshold=0.5):
return (self.predict_proba(X) >= threshold).astype(int)
def loss(self, X, y):
p = self.predict_proba(X)
return bce_loss(y, p)
def _forward_full(self, X):
a = X
activations = [a]
zs = []
for W, b in zip(self.W, self.b):
z = a @ W + b
a = sigmoid(z)
zs.append(z)
activations.append(a)
return activations, zs
def train(self, X, y, epochs=30, batch_size=None, verbose=True):
"""X : (N, d), y : (N,); batch_size=None utilise tout le jeu de données."""
N = X.shape[0]
idx = np.arange(N)
B = N if batch_size is None else int(batch_size)
for ep in range(1, epochs + 1):
self.rng.shuffle(idx)
base_loss = self.loss(X, y)
for start in range(0, N, B):
sl = idx[start:start + B]
Xb = X[sl]
yb = y[sl].reshape(-1, 1)
Bsz = Xb.shape[0]
activations, zs = self._forward_full(Xb)
A_L = activations[-1]
# Chaque ligne est la sensibilité aux préactivations d'un exemple.
delta = A_L - yb
grads_W = [None] * len(self.W)
grads_b = [None] * len(self.b)
# Calculer une seule fois la moyenne des gradients sur les exemples.
grads_W[-1] = activations[-2].T @ delta / Bsz
grads_b[-1] = delta.mean(axis=0)
for k in range(len(self.W) - 2, -1, -1):
z = zs[k]
d_act = sigmoid_prime(z)
delta = (delta @ self.W[k + 1].T) * d_act
grads_W[k] = activations[k].T @ delta / Bsz
grads_b[k] = delta.mean(axis=0)
# Tous les gradients utilisent les poids du même passage avant.
for k in range(len(self.W)):
self.W[k] -= self.lr * grads_W[k]
self.b[k] -= self.lr * grads_b[k]
new_loss = self.loss(X, y)
if verbose:
print(f"Époque {ep:3d} | perte {base_loss:.5f} -> {new_loss:.5f} | lr={self.lr:.4f}")