MathQuest

Régression logistique

⏱ ~12 min·+30 XP
💡

Pourquoi apprendre ça ?

Un modèle qui prédit "positif" pour tout le monde aura une accuracy de 99% sur un dataset de fraude où 1% des cas sont frauduleux — et sera inutile. Les métriques comme la précision, le rappel et l'AUC-ROC existent précisément pour éviter ces pièges. Savoir choisir la bonne métrique est aussi important que savoir entraîner un modèle.

🎯

Analogie

Imagine un détecteur d'incendie dans une usine. Un détecteur hypersensible déclenche l'alarme à la moindre fumée de cuisine (beaucoup de faux positifs — faible précision). Un détecteur trop laxiste rate des vrais incendies (beaucoup de faux négatifs — faible rappel). Le bon équilibre dépend du coût de chaque type d'erreur — c'est exactement ce que les métriques ML permettent de calibrer.

1. Matrice de confusion

📐

Théorie

Pour un classifieur binaire, chaque prédiction tombe dans l'une de ces 4 cases :

Preˊdit PositifPreˊdit NeˊgatifReˊel PositifTPFNReˊel NeˊgatifFPTN\begin{array}{c|cc} & \text{Prédit Positif} & \text{Prédit Négatif} \\ \hline \text{Réel Positif} & TP & FN \\ \text{Réel Négatif} & FP & TN \end{array}

  • TP (True Positive) : positif prédit correctement
  • TN (True Negative) : négatif prédit correctement
  • FP (False Positive) : négatif prédit positif — "fausse alarme"
  • FN (False Negative) : positif prédit négatif — "raté"

Toutes les métriques de classification découlent de ces 4 nombres.

📝

Matrice de confusion — détection de fraude

import numpy as np

y_true = np.array([0, 1, 1, 0, 1, 0, 0, 1, 0, 1])
y_pred = np.array([0, 1, 0, 0, 1, 1, 0, 1, 0, 0])

# Calcul manuel
TP = int(((y_true == 1) & (y_pred == 1)).sum())  # 4
TN = int(((y_true == 0) & (y_pred == 0)).sum())  # 3
FP = int(((y_true == 0) & (y_pred == 1)).sum())  # 1
FN = int(((y_true == 1) & (y_pred == 0)).sum())  # 2

print(f"TP={TP}, TN={TN}, FP={FP}, FN={FN}")
# TP=4, TN=3, FP=1, FN=2

# Via scikit-learn
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_true, y_pred)
print(cm)
# [[TN  FP]    [[3  1]
#  [FN  TP]]    [2  4]]

2. Accuracy, Précision, Rappel, F1

📐

Théorie

Accuracy : proportion de prédictions correctes (trompeuse sur les datasets déséquilibrés).

Accuracy=TP+TNTP+TN+FP+FN\text{Accuracy} = \frac{TP + TN}{TP + TN + FP + FN}

Précision : parmi les prédictions positives, combien sont vraiment positives ?

Preˊcision=TPTP+FP\text{Précision} = \frac{TP}{TP + FP}

Rappel (Recall / Sensibilité) : parmi les vrais positifs, combien sont détectés ?

Rappel=TPTP+FN\text{Rappel} = \frac{TP}{TP + FN}

F1-Score : moyenne harmonique de la précision et du rappel.

F1=2Preˊcision×RappelPreˊcision+Rappel=2TP2TP+FP+FNF_1 = 2 \cdot \frac{\text{Précision} \times \text{Rappel}}{\text{Précision} + \text{Rappel}} = \frac{2 \cdot TP}{2 \cdot TP + FP + FN}

Quand optimiser quoi :

  • Priorité au rappel : détection de maladies, fraude — rater un cas coûte cher
  • Priorité à la précision : spam filtering — les faux positifs irritent l'utilisateur
  • F1 : équilibre des deux, standard pour les datasets déséquilibrés
📝

Calcul des métriques en Python

from sklearn.metrics import (
    accuracy_score, precision_score,
    recall_score, f1_score, classification_report
)
import numpy as np

y_true = np.array([0, 1, 1, 0, 1, 0, 0, 1, 0, 1])
y_pred = np.array([0, 1, 0, 0, 1, 1, 0, 1, 0, 0])

print(f"Accuracy  : {accuracy_score(y_true, y_pred):.2f}")   # 0.70
print(f"Précision : {precision_score(y_true, y_pred):.2f}")  # 0.80
print(f"Rappel    : {recall_score(y_true, y_pred):.2f}")     # 0.60
print(f"F1        : {f1_score(y_true, y_pred):.2f}")         # 0.69

print(classification_report(y_true, y_pred,
      target_names=["Négatif", "Positif"]))
🧩

Checkpoint

Un modèle détecte 90 vrais positifs sur 100 cas positifs réels, et génère 45 faux positifs. Quel est son rappel ?

3. Régression logistique

📐

Théorie

La régression logistique est le classifieur binaire de référence. Malgré son nom, c'est un modèle de classification.

Le modèle prédit la probabilité que y=1y = 1 sachant x\mathbf{x} :

P(y=1x)=σ(wTx+b)=11+e(wTx+b)P(y=1|\mathbf{x}) = \sigma(\mathbf{w}^T \mathbf{x} + b) = \frac{1}{1 + e^{-(\mathbf{w}^T \mathbf{x} + b)}}

Entraînement : minimisation de la Binary Cross-Entropy (log-vraisemblance négative).

L(w,b)=1ni=1n[yilogp^i+(1yi)log(1p^i)]\mathcal{L}(\mathbf{w}, b) = -\frac{1}{n}\sum_{i=1}^n \left[ y_i \log \hat{p}_i + (1-y_i)\log(1-\hat{p}_i) \right]

Décision : on prédit y^=1\hat{y} = 1 si p^0.5\hat{p} \geq 0.5 (seuil ajustable selon le compromis précision/rappel).

Frontière de décision : hyperplan wTx+b=0\mathbf{w}^T \mathbf{x} + b = 0 — linéaire dans l'espace des features.

📝

Régression logistique avec PyTorch

import torch
import torch.nn as nn

class LogisticRegression(nn.Module):
    def __init__(self, n_features):
        super().__init__()
        self.linear = nn.Linear(n_features, 1)

    def forward(self, x):
        return torch.sigmoid(self.linear(x))

# Dataset synthétique binaire
torch.manual_seed(42)
X = torch.randn(200, 5)
y = (X[:, 0] + X[:, 1] > 0).float().unsqueeze(1)

model     = LogisticRegression(5)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
criterion = nn.BCELoss()

for epoch in range(100):
    pred = model(X)
    loss = criterion(pred, y)
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

with torch.no_grad():
    probs = model(X)
    preds = (probs >= 0.5).float()
    acc   = (preds == y).float().mean()
    print(f"Accuracy finale : {acc.item():.2f}")

4. Courbe ROC et AUC

📐

Théorie

La courbe ROC (Receiver Operating Characteristic) trace le Taux de vrais positifs (rappel / TPR) en fonction du Taux de faux positifs (FPR) pour tous les seuils de décision τ[0,1]\tau \in [0, 1] :

TPR(τ)=TP(τ)TP(τ)+FN(τ),FPR(τ)=FP(τ)FP(τ)+TN(τ)\text{TPR}(\tau) = \frac{TP(\tau)}{TP(\tau) + FN(\tau)}, \quad \text{FPR}(\tau) = \frac{FP(\tau)}{FP(\tau) + TN(\tau)}

AUC (Area Under the Curve) : aire sous la courbe ROC.

  • AUC = 1.0 : classifieur parfait
  • AUC = 0.5 : classifieur aléatoire (diagonale)

Interprétation probabiliste : l'AUC est la probabilité que le modèle assigne un score plus élevé à un exemple positif aléatoire qu'à un exemple négatif aléatoire.

Avantage : l'AUC est invariante au seuil de décision et au déséquilibre de classes.

📝

Courbe ROC et choix du seuil optimal

from sklearn.metrics import roc_auc_score, roc_curve
import numpy as np

y_true  = np.array([0, 0, 1, 1, 0, 1, 0, 1, 1, 0])
y_score = np.array([0.1, 0.3, 0.8, 0.7, 0.2, 0.9, 0.4, 0.6, 0.85, 0.15])

auc = roc_auc_score(y_true, y_score)
print(f"AUC-ROC : {auc:.3f}")   # 0.96

# Courbe ROC
fpr, tpr, thresholds = roc_curve(y_true, y_score)

# Seuil optimal : Youden Index = max(TPR - FPR)
j_scores      = tpr - fpr
best_idx      = np.argmax(j_scores)
best_threshold = thresholds[best_idx]
print(f"Seuil optimal  : {best_threshold:.2f}")
print(f"TPR : {tpr[best_idx]:.2f}, FPR : {fpr[best_idx]:.2f}")
🧩

Checkpoint

Quel AUC-ROC indique un classifieur aléatoire (pas mieux qu'un tirage au sort) ?

5. Overfitting et Underfitting

📐

Théorie

Underfitting : le modèle est trop simple — loss d'entraînement élevée et loss de validation élevée.

  • Remède : modèle plus complexe, plus de features, moins de régularisation

Overfitting : le modèle a mémorisé les données — loss d'entraînement faible mais loss de validation élevée.

  • Remède : plus de données, régularisation, Early stopping, modèle plus simple

Compromis biais-variance :

Erreur=Biais2+Variance+Bruit irreˊductible\text{Erreur} = \text{Biais}^2 + \text{Variance} + \text{Bruit irréductible}

  • Biais élevé → underfitting
  • Variance élevée → overfitting

Techniques de régularisation :

  • L2 (Weight Decay) : Lreg=L+λw22\mathcal{L}_{\text{reg}} = \mathcal{L} + \lambda \|\mathbf{w}\|_2^2 — pénalise les poids grands
  • L1 (Lasso) : Lreg=L+λw1\mathcal{L}_{\text{reg}} = \mathcal{L} + \lambda \|\mathbf{w}\|_1 — favorise la parcimonie
  • Dropout : désactive aléatoirement des neurones à l'entraînement
  • Early Stopping : arrêt si la validation loss ne s'améliore plus pendant kk epochs
📝

Dropout, Weight Decay et Early Stopping

import torch
import torch.nn as nn

class MLPRegularized(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Dropout(p=0.3),      # 30% des neurones désactivés a l'entraînement
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Dropout(p=0.3),
            nn.Linear(128, 10)
        )

    def forward(self, x):
        return self.net(x)

model = MLPRegularized()
# weight_decay = régularisation L2
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)

# Early stopping
best_val_loss    = float('inf')
patience         = 10
patience_counter = 0

for epoch in range(200):
    model.train()   # Dropout actif
    # ... (boucle d'entraînement) ...

    model.eval()    # Dropout désactivé — IMPORTANT pour l'évaluation
    val_loss = 0.05 + 0.001 * epoch  # exemple

    if val_loss < best_val_loss:
        best_val_loss    = val_loss
        patience_counter = 0
        torch.save(model.state_dict(), "best_model.pt")
    else:
        patience_counter += 1
        if patience_counter >= patience:
            print(f"Early stopping a l'epoch {epoch}")
            break
⚠️

Évaluer sans appeler model.eval()

model.train() et model.eval() changent le comportement de Dropout et BatchNorm. En mode train(), Dropout désactive aléatoirement des neurones — les prédictions varient à chaque forward pass. Évaluer les métriques sans passer en model.eval() produit des résultats bruités et non reproductibles. Toujours appeler model.eval() (et torch.no_grad()) avant l'inférence.

À retenir

  • Matrice de confusion : TP, TN, FP, FN — toutes les métriques en découlent
  • Accuracy trompeuse sur les datasets déséquilibrés — préférer F1, précision ou rappel
  • Rappel = TP/(TP+FN) : proportion des vrais positifs détectés
  • Précision = TP/(TP+FP) : proportion des positifs prédits qui sont vrais
  • AUC-ROC : 0.5 = aléatoire, 1.0 = parfait — invariant au seuil et au déséquilibre de classes
  • Overfitting : train loss basse, val loss haute → Dropout, L2, Early Stopping, plus de données
Passer aux exercices →