Basculer vers la classification : sigmoïde et log-loss
Le scénario : BoundaryScope
Section titled “Le scénario : BoundaryScope”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.
Pourquoi pas le MSE ?
Section titled “Pourquoi pas le MSE ?”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 fonction sigmoïde
Section titled “La fonction sigmoïde”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.
Le log-loss remplace le MSE
Section titled “Le log-loss remplace le MSE”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.
Entraîner le classifieur
Section titled “Entraîner le classifieur”np.random.seed(0)n_per_class = 100debit_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/20indices = 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.9924329127821289perte finale: 0.003568180795850563Prochaine étape
Section titled “Prochaine étape”👉 Frontière de décision et comparaison à une baseline
Junior TSAFACK – 12/09/2026