Aller au contenu

Séparez les classes avec les modèles linéaires

Une agence immobilière rennaise se pose deux questions sur chaque appartement qu’elle met en vente : à quel prix le proposer, et partira-t-il en moins d’un mois ? La première est une régression, la seconde une classification. Pour les deux, la réponse la plus simple consiste à faire une somme pondérée des caractéristiques : un peu de surface, un peu d’étage, moins pour l’ancienneté, etc.

C’est l’idée des modèles linéaires. Ils sont anciens, rapides, interprétables, et ils restent la première chose à essayer sur beaucoup de problèmes. Ils sont aussi la porte d’entrée vers les réseaux de neurones (chapitre 9), qui empilent des modèles linéaires.

Le modèle prédit le prix comme une combinaison linéaire des caractéristiques :

y^=w⊤x+b=w1x1+⋯+wdxd+b\hat{y} = w^\top x + b = w_1 x_1 + \dots + w_d x_d + b
  • x=(x1,…,xd)x = (x_1, \dots, x_d) décrit l’appartement (surface, étage…) ;
  • w=(w1,…,wd)w = (w_1, \dots, w_d) sont les poids : wjw_j dit de combien le prix augmente quand xjx_j augmente d’une unité, les autres caractéristiques restant fixes ;
  • bb est le biais (ou ordonnée à l’origine), une constante ;
  • w⊤xw^\top x désigne le produit scalaire ∑jwjxj\sum_j w_j x_j.

Pour choisir ww et bb, on minimise l’erreur quadratique moyenne (MSE) sur les mm exemples d’apprentissage :

MSE(w,b)=1m∑i=1m(yi−w⊤xi−b)2\text{MSE}(w, b) = \frac{1}{m} \sum_{i=1}^{m} \big(y_i - w^\top x_i - b\big)^2

C’est la méthode des moindres carrés. Elle a une solution exacte : en ajoutant à XX (la matrice des exemples, une ligne par appartement) une colonne de 1 pour absorber bb, on obtient w∗=(X⊤X)−1X⊤yw^* = (X^\top X)^{-1} X^\top y, à condition que X⊤XX^\top X soit inversible.

La régression linéaire est interprétable et rapide. Mais elle suppose une relation linéaire, et le carré dans la MSE la rend sensible aux valeurs aberrantes : une seule vente à un prix délirant tire toute la droite vers elle.

20406080surface (m²)100150200250300350400prix (k€)−5+10−560 m² : 282,5 k€ŷ = 4,25 x + 27,5Trois ventes : résidus minimisés20406080surface (m²)une vente aberrante(40 m² à 380 k€)pente 2,69 au lieu de 4,25Une seule valeur aberrante tire la droite
À gauche, l'exemple : la droite ŷ = 4,25 x + 27,5 minimise la somme des carrés des résidus (segments roses). À droite, une seule vente aberrante (inventée) suffit à diviser la pente par plus de 1,5, parce que les grands écarts sont élevés au carré.

Avec beaucoup de caractéristiques, surtout si certaines sont corrélées (la surface et le nombre de pièces, par exemple), les poids appris peuvent devenir énormes et instables : c’est du surapprentissage. La régularisation ajoute à la perte une pénalité sur la taille des poids.

  • Ridge (pénalité L2) : on minimise MSE+λ∥w∥22\text{MSE} + \lambda \lVert w \rVert_2^2, où ∥w∥22=∑jwj2\lVert w \rVert_2^2 = \sum_j w_j^2. Tous les poids sont réduits vers 0, sans l’atteindre. La solution reste explicite, w∗=(X⊤X+λI)−1X⊤yw^* = (X^\top X + \lambda I)^{-1} X^\top y (avec II la matrice identité), et elle est stable même quand les variables sont corrélées.
  • Lasso (pénalité L1) : on minimise MSE+λ∥w∥1\text{MSE} + \lambda \lVert w \rVert_1, où ∥w∥1=∑j∣wj∣\lVert w \rVert_1 = \sum_j \lvert w_j \rvert. Cette pénalité met certains poids exactement à 0 : le Lasso fait de la sélection de variables.
  • Elastic Net combine les deux pénalités.
moindrescarréssolution (0,80 ; 0,60)w₁w₂Ridge (L2)moindrescarréssolution (1,00 ; 0,00)w₁w₂le coin du losange est sur l’axe :w₂ = 0 exactementLasso (L1)
Pénaliser revient à chercher le point de plus faible MSE dans une région autour de l'origine. Le disque de Ridge est rond : les poids rétrécissent mais restent non nuls. Le losange du Lasso a des coins sur les axes : l'ellipse les touche souvent là, et un poids tombe exactement à zéro.

Le coefficient λ≥0\lambda \geq 0 règle le compromis biais-variance : λ=0\lambda = 0 redonne les moindres carrés, un λ\lambda très grand écrase tous les poids (sous-apprentissage). On le choisit par validation croisée. Comme la pénalité traite tous les poids de la même façon, il faut normaliser les variables avant : sinon, une variable en euros et une autre en milliers d’euros seraient pénalisées très différemment.

from sklearn.linear_model import LassoCV, RidgeCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
# X : caractéristiques des appartements, y : prix (en k€)
ridge = make_pipeline(StandardScaler(), RidgeCV(alphas=[0.01, 0.1, 1, 10, 100]))
lasso = make_pipeline(StandardScaler(), LassoCV(cv=5)) # choisit lambda par validation croisée
ridge.fit(X, y)
lasso.fit(X, y)
print(lasso[-1].coef_) # certains coefficients valent exactement 0

Dans scikit-learn, le coefficient λ\lambda s’appelle alpha.

Passons à la seconde question : l’appartement partira-t-il en moins d’un mois ? On code les deux classes y=+1y = +1 (oui) et y=−1y = -1 (non), et on calcule un score s(x)=w⊤x+bs(x) = w^\top x + b. La prédiction est le signe du score, et la frontière de décision est l’ensemble des points où s(x)=0s(x) = 0 : une droite en dimension 2, un plan en dimension 3, un hyperplan en général.

La quantité y s(x)y \, s(x) s’appelle la marge de l’exemple : elle est positive si l’exemple est bien classé, et d’autant plus grande qu’il est loin de la frontière, du bon côté.

Le perceptron est l’algorithme historique pour trouver ww et bb. On parcourt les exemples ; à chaque exemple mal classé (marge y s(x)≤0y \, s(x) \le 0), on corrige :

w←w+η y xb←b+η yw \leftarrow w + \eta \, y \, x \qquad b \leftarrow b + \eta \, y

où η>0\eta > 0 est le pas d’apprentissage. Intuitivement, on fait pivoter la frontière pour rapprocher le score de l’exemple du bon signe. Si les données sont linéairement séparables, le perceptron converge vers une frontière sans erreur ; sinon, il ne s’arrête jamais de corriger.

Le perceptron donne une réponse sèche. La régression logistique transforme le score en probabilité grâce à la fonction sigmoïde :

P(y=1∣x)=σ(w⊤x+b),σ(z)=11+e−zP(y = 1 \mid x) = \sigma(w^\top x + b), \qquad \sigma(z) = \frac{1}{1 + e^{-z}}

La sigmoïde écrase n’importe quel réel entre 0 et 1 : σ(0)=0,5\sigma(0) = 0{,}5, σ(z)\sigma(z) tend vers 1 quand zz est grand et vers 0 quand zz est très négatif. On apprend ww et bb en minimisant la perte logistique (log-loss, ou entropie croisée binaire), par descente de gradient, puisqu’il n’existe pas de formule exacte. Malgré son nom, c’est bien un modèle de classification, discriminant, dont les poids s’interprètent : ewje^{w_j} est le facteur par lequel la cote P/(1−P)P / (1 - P) est multipliée quand xjx_j augmente d’une unité.

−6−4−20246score z = wᵀx + b0,00,20,40,60,81,0σ(z) = P(y = 1 | x)σ(0) = 0,5 : la frontièreσ(−2) ≈ 0,12σ(2) ≈ 0,88
La sigmoïde transforme le score en probabilité. Le seuil de 0,5 correspond au score nul, c'est-à-dire à la frontière w⊤x + b = 0 ; plus l'exemple est loin de la frontière, plus la probabilité est tranchée.

Pour plus de deux classes, on calcule un score par classe et on les transforme en probabilités avec la fonction softmax, P(y=j∣x)=esj/∑keskP(y = j \mid x) = e^{s_j} / \sum_k e^{s_k}. Un classifieur strictement binaire peut aussi être étendu en entraînant un modèle par classe contre toutes les autres (un-contre-tous).

Ce qui distingue vraiment ces modèles, c’est la perte qu’ils minimisent, en fonction de la marge z=y s(x)z = y \, s(x) :

PerteFormuleUtilisée par
0-11 si z≤0z \le 0, 0 sinonl’évaluation (non dérivable)
Perceptronmax⁡(0,−z)\max(0, -z)le perceptron
Charnière (hinge)max⁡(0,1−z)\max(0, 1 - z)les SVM
Logistiquelog⁡(1+e−z)\log(1 + e^{-z})la régression logistique
Exponentiellee−ze^{-z}AdaBoost (chapitre 8)

La perte 0-1 est celle qu’on voudrait minimiser (le nombre d’erreurs), mais elle est plate presque partout, donc inutilisable pour une descente de gradient. Les autres sont des substituts (surrogates) continus qui la majorent ou l’approchent.

−2−10123marge z = y · s(x)0,00,51,01,52,02,53,03,5perte← mal classébien classé →0-1 (évaluation)perceptron max(0, −z)charnière max(0, 1 − z) (SVM)logistique log(1 + e⁻ᶻ)exponentielle e⁻ᶻ (AdaBoost)
Toutes les pertes pénalisent les marges négatives (erreurs), mais pas de la même façon. La charnière exige en plus une marge d'au moins 1, et l'exponentielle punit très fort les grosses erreurs, ce qui rend AdaBoost sensible aux étiquettes fausses.

Quand les données sont séparables, une infinité d’hyperplans séparent les classes. Lequel choisir ? Les machines à vecteurs de support (SVM) choisissent celui qui passe le plus loin possible des exemples des deux classes : l’hyperplan de marge maximale.

On fixe l’échelle de ww pour que les exemples les plus proches vérifient yi(w⊤xi+b)=1y_i (w^\top x_i + b) = 1. La largeur de la bande vide autour de la frontière vaut alors 2/∥w∥2 / \lVert w \rVert, et la maximiser revient à résoudre :

min⁡w,b  12∥w∥2sous les contraintesyi(w⊤xi+b)≥1 pour tout i\min_{w, b} \; \frac{1}{2} \lVert w \rVert^2 \quad \text{sous les contraintes} \quad y_i (w^\top x_i + b) \geq 1 \text{ pour tout } i

Les exemples situés sur le bord de la marge sont les vecteurs de support : ce sont les seuls qui déterminent la solution. On pourrait supprimer tous les autres sans rien changer.

Les vraies données sont rarement séparables. La marge souple autorise des exemples à franchir la marge, moyennant une pénalité :

min⁡w,b  12∥w∥2+C∑i=1mmax⁡(0, 1−yi(w⊤xi+b))\min_{w, b} \; \frac{1}{2} \lVert w \rVert^2 + C \sum_{i=1}^{m} \max\big(0, \, 1 - y_i (w^\top x_i + b)\big)

Le second terme est la somme des pertes charnière, et C>0C > 0 règle le compromis. Avec CC petit, on tolère beaucoup de violations pour garder une marge large (modèle plus simple, risque de sous-apprentissage). Avec CC grand, on veut classer chaque exemple correctement, quitte à réduire la marge (risque de surapprentissage).

cercles : vecteurs de supportC = 100 : marge 0,44, 2 vecteurs de supporttirets : bords de la marge,s(x) = ±1C = 0,05 : marge 2,85, 19 vecteurs de support
Seuls les points cerclés (sur le bord de la marge ou à l'intérieur) déterminent la frontière. Un C grand refuse les violations et resserre la marge ; un C petit les tolère et élargit la marge (données simulées).

Et si la frontière n’est pas du tout linéaire ? On peut envoyer les données dans un espace de plus grande dimension, par une transformation Φ\Phi, où elles deviennent séparables. Or l’optimisation des SVM ne fait intervenir les exemples qu’à travers des produits scalaires ⟨x,x′⟩\langle x, x' \rangle. L’astuce du noyau (kernel trick) remplace ce produit par une fonction K(x,x′)=⟨Φ(x),Φ(x′)⟩K(x, x') = \langle \Phi(x), \Phi(x') \rangle calculée directement, sans jamais construire Φ\Phi. Les noyaux usuels :

  • linéaire : K(x,x′)=x⊤x′K(x, x') = x^\top x' ;
  • polynomial : K(x,x′)=(x⊤x′+c)pK(x, x') = (x^\top x' + c)^p, où cc est une constante et pp le degré ;
  • gaussien (RBF) : K(x,x′)=exp⁡(−γ∥x−x′∥2)K(x, x') = \exp(-\gamma \lVert x - x' \rVert^2), où γ>0\gamma > 0 règle la portée de l’influence de chaque exemple. Un γ\gamma grand donne une frontière très sinueuse (surapprentissage), un γ\gamma petit une frontière presque linéaire.
app. 0,60 ; test 0,57noyau linéaireapp. 1,00 ; test 0,99RBF, γ = 1app. 1,00 ; test 0,96RBF, γ = 100
Aucune droite ne sépare deux cercles concentriques ; le noyau gaussien, si. Un γ modéré donne une frontière régulière ; un γ très grand fait de chaque point un îlot : parfait en apprentissage, moins bon en test (données simulées).
from sklearn.model_selection import GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
# X, y : caractéristiques et classe (vendu en moins d'un mois ou non)
svm = make_pipeline(StandardScaler(), SVC(kernel="rbf"))
grille = {"svc__C": [0.1, 1, 10, 100], "svc__gamma": [0.001, 0.01, 0.1, 1]}
recherche = GridSearchCV(svm, grille, cv=5, scoring="f1").fit(X, y)
print(recherche.best_params_)

On règle CC et γ\gamma ensemble, sur une grille en échelle logarithmique. Les SVM exigent des variables normalisées, et leur entraînement devient lent au-delà de quelques dizaines de milliers d’exemples.

  • La régression linéaire prédit w⊤x+bw^\top x + b et s’ajuste par moindres carrés ; elle est interprétable mais sensible aux valeurs aberrantes.
  • Ridge (L2) réduit tous les poids, Lasso (L1) en annule certains ; λ\lambda se règle par validation croisée, après normalisation.
  • Un classifieur linéaire prédit le signe de w⊤x+bw^\top x + b ; la marge y s(x)y \, s(x) est positive pour un exemple bien classé.
  • Le perceptron corrige les poids à chaque erreur et converge si les données sont séparables ; la régression logistique produit des probabilités grâce à la sigmoïde.
  • Les SVM cherchent l’hyperplan de marge maximale ; CC règle la tolérance aux erreurs, et les noyaux (polynomial, RBF) donnent des frontières non linéaires.

Exercice · Lire une régression logistique

facile

L’agence a appris le modèle P(vendu en moins d’un mois∣x)=σ(4−0,02×prix)P(\text{vendu en moins d'un mois} \mid x) = \sigma(4 - 0{,}02 \times \text{prix}), avec le prix en milliers d’euros. Quelle est la probabilité pour un appartement à 250 k€ ? À quel prix la probabilité vaut-elle exactement 0,5 ?

Voir la solution

Pour 250 k€, le score vaut 4−0,02×250=−14 - 0{,}02 \times 250 = -1, et σ(−1)=1/(1+e1)≈1/3,718≈0,27\sigma(-1) = 1 / (1 + e^{1}) \approx 1 / 3{,}718 \approx 0{,}27. Il y a environ 27 % de chances qu’il parte en moins d’un mois.

La probabilité vaut 0,5 quand le score est nul, puisque σ(0)=0,5\sigma(0) = 0{,}5 : 4−0,02×prix=04 - 0{,}02 \times \text{prix} = 0, soit un prix de 200 k€. C’est la frontière de décision du modèle avec un seuil de 0,5.

En code

import numpy as np
def sigmoide(z):
return 1 / (1 + np.exp(-z))
def proba_vente(prix): # prix en k€
return sigmoide(4 - 0.02 * prix)
print(round(proba_vente(250), 3)) # 0.269
print(proba_vente(200)) # 0.5 : score nul pour 4 / 0,02 = 200 k€

Exercice · Deux pas de perceptron

moyen

On part de w=(1,−1)w = (1, -1), b=0b = 0 et d’un pas η=0,5\eta = 0{,}5. Appliquez la règle du perceptron à l’exemple x1=(1,2)x_1 = (1, 2) de classe y1=+1y_1 = +1, puis à l’exemple x2=(3,1)x_2 = (3, 1) de classe y2=−1y_2 = -1. L’exemple x1x_1 est-il encore bien classé à la fin ?

Voir la solution

Exemple 1 : s(x1)=1×1+(−1)×2+0=−1s(x_1) = 1 \times 1 + (-1) \times 2 + 0 = -1, donc y1s(x1)=−1≤0y_1 s(x_1) = -1 \le 0 : mal classé. Correction : w←(1,−1)+0,5×(1,2)=(1,5;0)w \leftarrow (1, -1) + 0{,}5 \times (1, 2) = (1{,}5 ; 0) et b←0+0,5=0,5b \leftarrow 0 + 0{,}5 = 0{,}5. Vérification : s(x1)=1,5+0,5=2>0s(x_1) = 1{,}5 + 0{,}5 = 2 > 0, bien classé.

Exemple 2 : s(x2)=1,5×3+0×1+0,5=5s(x_2) = 1{,}5 \times 3 + 0 \times 1 + 0{,}5 = 5, donc y2s(x2)=−5y_2 s(x_2) = -5 : mal classé. Correction : w←(1,5;0)−0,5×(3,1)=(0;−0,5)w \leftarrow (1{,}5 ; 0) - 0{,}5 \times (3, 1) = (0 ; -0{,}5) et b←0,5−0,5=0b \leftarrow 0{,}5 - 0{,}5 = 0.

À la fin, s(x1)=0×1−0,5×2+0=−1s(x_1) = 0 \times 1 - 0{,}5 \times 2 + 0 = -1 : x1x_1 est de nouveau mal classé. Corriger un exemple peut en dérégler un autre ; c’est pourquoi le perceptron parcourt les données plusieurs fois. Ici, les deux points sont séparables (par exemple par w=(−1,1)w = (-1, 1), b=0b = 0), donc l’algorithme finira par converger.

En code

import numpy as np
w, b, eta = np.array([1.0, -1.0]), 0.0, 0.5
exemples = [(np.array([1, 2]), +1), (np.array([3, 1]), -1)]
for x, y in exemples:
marge = y * (w @ x + b)
if marge <= 0: # mal classé : on corrige
w, b = w + eta * y * x, b + eta * y
print(f"marge {marge}, puis w = {w}, b = {b}")
x1 = exemples[0][0]
print(w @ x1 + b) # -1.0 : x1 est de nouveau mal classé
# marge -1.0, puis w = [1.5 0. ], b = 0.5
# marge -5.0, puis w = [ 0. -0.5], b = 0.0

Exercice · Marge et perte charnière

difficile

Un SVM a appris w=(3,4)w = (3, 4) et b=−10b = -10.

  1. Quelle est la largeur de la marge ?
  2. Pour l’exemple x=(2,3)x = (2, 3) de classe +1+1, calculez sa distance à la frontière et sa perte charnière.
  3. Même question pour x′=(2,1)x' = (2, 1) de classe +1+1.
Voir la solution
  1. ∥w∥=32+42=5\lVert w \rVert = \sqrt{3^2 + 4^2} = 5, donc la largeur de la marge vaut 2/∥w∥=0,42 / \lVert w \rVert = 0{,}4.
  2. s(x)=3×2+4×3−10=8s(x) = 3 \times 2 + 4 \times 3 - 10 = 8. La distance à la frontière vaut ∣s(x)∣/∥w∥=8/5=1,6\lvert s(x) \rvert / \lVert w \rVert = 8 / 5 = 1{,}6. La marge y s(x)=8≥1y \, s(x) = 8 \geq 1, donc la perte charnière max⁡(0,1−8)\max(0, 1 - 8) est nulle : l’exemple est bien classé et hors de la marge, il n’influence pas la solution.
  3. s(x′)=6+4−10=0s(x') = 6 + 4 - 10 = 0 : l’exemple est sur la frontière, à distance 0. Sa perte charnière vaut max⁡(0,1−0)=1\max(0, 1 - 0) = 1. Il viole la marge et contribue à la fonction objectif avec le poids CC : c’est un vecteur de support.

En code

import numpy as np
w, b = np.array([3, 4]), -10
norme = np.linalg.norm(w)
print(norme, 2 / norme) # 5.0 0.4 (largeur de la marge)
for x, y in [(np.array([2, 3]), +1), (np.array([2, 1]), +1)]:
s = w @ x + b # score
distance = abs(s) / norme
charniere = max(0, 1 - y * s)
print(f"x = {x} : score {s}, distance {distance}, perte charnière {charniere}")
# x = [2 3] : score 8, distance 1.6, perte 0 ; x = [2 1] : score 0, distance 0.0, perte 1