Skip to content

Basculer vers la classification : sigmoïde et log-loss

Un opérateur réseau veut classer un flux comme normal ou à risque, à partir de deux métriques : le débit et la latence. Contrairement à la régression, la sortie attendue n’est plus une quantité continue mais une catégorie parmi deux — exactement la distinction posée au module 1.

Rien n’empêche techniquement de réutiliser model(X, theta) = X @ theta et le MSE pour prédire 0 ou 1. Mais rien ne borne alors la sortie entre 0 et 1 : le modèle pourrait tout aussi bien prédire -3 ou 47, des valeurs qui n’ont aucun sens comme probabilité d’appartenance à une classe.

La sigmoïde écrase n’importe quel score réel dans l’intervalle ]0, 1[, ce qui en fait une probabilité valide :

def sigmoid(z):
return 1 / (1 + np.exp(-z))
def model(X, theta):
return sigmoid(X @ theta)

Le score linéaire X @ theta (identique à la régression) est maintenant interprété comme la probabilité que l’exemple appartienne à la classe 1.

def log_loss(X, y, theta, eps=1e-15):
m = len(y)
A = model(X, theta)
A = np.clip(A, eps, 1 - eps)
return -(1 / m) * np.sum(y * np.log(A) + (1 - y) * np.log(1 - A))

Le np.clip évite un log(0) (indéfini) si le modèle prédit une probabilité exactement 0 ou 1. Le gradient de ce coût, remarquablement, a exactement la même forme que celui du MSE en régression :

def grad(X, y, theta):
m = len(y)
return (1 / m) * X.T @ (model(X, theta) - y)

Seule la définition de model a changé (sigmoïde au lieu d’identité) — la mécanique de la descente de gradient, elle, reste identique à celle du module de régression.

np.random.seed(0)
n_per_class = 100
debit_normal = np.random.normal(loc=50, scale=8, size=n_per_class)
latence_normal = np.random.normal(loc=20, scale=5, size=n_per_class)
debit_risque = np.random.normal(loc=15, scale=8, size=n_per_class)
latence_risque = np.random.normal(loc=80, scale=10, size=n_per_class)
debit = np.concatenate([debit_normal, debit_risque])
latence = np.concatenate([latence_normal, latence_risque])
label = np.concatenate([np.zeros(n_per_class), np.ones(n_per_class)]).reshape(-1, 1)
X = make_design_matrix(standardize(debit), standardize(latence))
# split 80/20
indices = np.random.permutation(len(label))
split = int(0.8 * len(label))
X_train, X_test = X[indices[:split]], X[indices[split:]]
y_train, y_test = label[indices[:split]], label[indices[split:]]
theta_final, loss_history = gradient_descent(X_train, y_train, np.random.randn(3, 1), learning_rate=0.1, n_iterations=3000)
print("perte initiale:", loss_history[0])
print("perte finale:", loss_history[-1])
perte initiale: 0.9924329127821289
perte finale: 0.003568180795850563

👉 Frontière de décision et comparaison à une baseline


Junior TSAFACK – 12/09/2026