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.
La régression linéaire et les moindres carrés
Section intitulée « La régression linéaire et les moindres carrés »Le modèle prédit le prix comme une combinaison linéaire des caractéristiques :
- décrit l’appartement (surface, étage…) ;
- sont les poids : dit de combien le prix augmente quand augmente d’une unité, les autres caractéristiques restant fixes ;
- est le biais (ou ordonnée à l’origine), une constante ;
- désigne le produit scalaire .
Pour choisir et , on minimise l’erreur quadratique moyenne (MSE) sur les exemples d’apprentissage :
C’est la méthode des moindres carrés. Elle a une solution exacte : en ajoutant à (la matrice des exemples, une ligne par appartement) une colonne de 1 pour absorber , on obtient , à condition que 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.
Régulariser : Ridge et Lasso
Section intitulée « Régulariser : Ridge et Lasso »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 , où . Tous les poids sont réduits vers 0, sans l’atteindre. La solution reste explicite, (avec la matrice identité), et elle est stable même quand les variables sont corrélées.
- Lasso (pénalité L1) : on minimise , où . 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.
Le coefficient règle le compromis biais-variance : redonne les moindres carrés, un 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, RidgeCVfrom sklearn.pipeline import make_pipelinefrom 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éeridge.fit(X, y)lasso.fit(X, y)print(lasso[-1].coef_) # certains coefficients valent exactement 0Dans scikit-learn, le coefficient s’appelle alpha.
Classer avec une frontière linéaire
Section intitulée « Classer avec une frontière linéaire »Passons à la seconde question : l’appartement partira-t-il en moins d’un mois ? On code les deux classes (oui) et (non), et on calcule un score . La prédiction est le signe du score, et la frontière de décision est l’ensemble des points où : une droite en dimension 2, un plan en dimension 3, un hyperplan en général.
La quantité 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
Section intitulée « Le perceptron »Le perceptron est l’algorithme historique pour trouver et . On parcourt les exemples ; à chaque exemple mal classé (marge ), on corrige :
où 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.
La régression logistique
Section intitulée « La régression logistique »Le perceptron donne une réponse sèche. La régression logistique transforme le score en probabilité grâce à la fonction sigmoïde :
La sigmoïde écrase n’importe quel réel entre 0 et 1 : , tend vers 1 quand est grand et vers 0 quand est très négatif. On apprend et 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 : est le facteur par lequel la cote est multipliée quand augmente d’une unité.
Pour plus de deux classes, on calcule un score par classe et on les transforme en probabilités avec la fonction softmax, . Un classifieur strictement binaire peut aussi être étendu en entraînant un modèle par classe contre toutes les autres (un-contre-tous).
Les fonctions de perte
Section intitulée « Les fonctions de perte »Ce qui distingue vraiment ces modèles, c’est la perte qu’ils minimisent, en fonction de la marge :
| Perte | Formule | Utilisée par |
|---|---|---|
| 0-1 | 1 si , 0 sinon | l’évaluation (non dérivable) |
| Perceptron | le perceptron | |
| Charnière (hinge) | les SVM | |
| Logistique | la régression logistique | |
| Exponentielle | 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.
Les SVM : chercher la plus grande marge
Section intitulée « Les SVM : chercher la plus grande marge »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 pour que les exemples les plus proches vérifient . La largeur de la bande vide autour de la frontière vaut alors , et la maximiser revient à résoudre :
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.
La marge souple et le paramètre C
Section intitulée « La marge souple et le paramètre C »Les vraies données sont rarement séparables. La marge souple autorise des exemples à franchir la marge, moyennant une pénalité :
Le second terme est la somme des pertes charnière, et règle le compromis. Avec petit, on tolère beaucoup de violations pour garder une marge large (modèle plus simple, risque de sous-apprentissage). Avec grand, on veut classer chaque exemple correctement, quitte à réduire la marge (risque de surapprentissage).
L’astuce du noyau
Section intitulée « L’astuce du noyau »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 , où elles deviennent séparables. Or l’optimisation des SVM ne fait intervenir les exemples qu’à travers des produits scalaires . L’astuce du noyau (kernel trick) remplace ce produit par une fonction calculée directement, sans jamais construire . Les noyaux usuels :
- linéaire : ;
- polynomial : , où est une constante et le degré ;
- gaussien (RBF) : , où règle la portée de l’influence de chaque exemple. Un grand donne une frontière très sinueuse (surapprentissage), un petit une frontière presque linéaire.
from sklearn.model_selection import GridSearchCVfrom sklearn.pipeline import make_pipelinefrom sklearn.preprocessing import StandardScalerfrom 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 et 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.
En résumé
Section intitulée « En résumé »- La régression linéaire prédit 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 ; se règle par validation croisée, après normalisation.
- Un classifieur linéaire prédit le signe de ; la marge 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 ; règle la tolérance aux erreurs, et les noyaux (polynomial, RBF) donnent des frontières non linéaires.
Exercices
Section intitulée « Exercices »Exercice · Lire une régression logistique
facileL’agence a appris le modèle , 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 , et . 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 : , 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.269print(proba_vente(200)) # 0.5 : score nul pour 4 / 0,02 = 200 k€proba_vente <- function(prix) plogis(4 - 0.02 * prix) # plogis est la sigmoïde
round(proba_vente(250), 3) # 0.269proba_vente(200) # 0.5 : score nul pour 4 / 0,02 = 200 k€Exercice · Deux pas de perceptron
moyenOn part de , et d’un pas . Appliquez la règle du perceptron à l’exemple de classe , puis à l’exemple de classe . L’exemple est-il encore bien classé à la fin ?
Voir la solution
Exemple 1 : , donc : mal classé. Correction : et . Vérification : , bien classé.
Exemple 2 : , donc : mal classé. Correction : et .
À la fin, : 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 , ), donc l’algorithme finira par converger.
En code
import numpy as np
w, b, eta = np.array([1.0, -1.0]), 0.0, 0.5exemples = [(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.0w <- c(1, -1); b <- 0; eta <- 0.5X <- rbind(c(1, 2), c(3, 1)); y <- c(+1, -1)
for (i in 1:2) { marge <- y[i] * (sum(w * X[i, ]) + b) if (marge <= 0) { # mal classé : on corrige w <- w + eta * y[i] * X[i, ] b <- b + eta * y[i] } cat("marge", marge, ", puis w =", w, ", b =", b, "\n")}sum(w * X[1, ]) + b # -1 : x1 est de nouveau mal classé# marge -1 , puis w = 1.5 0 , b = 0.5# marge -5 , puis w = 0 -0.5 , b = 0Exercice · Marge et perte charnière
difficileUn SVM a appris et .
- Quelle est la largeur de la marge ?
- Pour l’exemple de classe , calculez sa distance à la frontière et sa perte charnière.
- Même question pour de classe .
Voir la solution
- , donc la largeur de la marge vaut .
- . La distance à la frontière vaut . La marge , donc la perte charnière est nulle : l’exemple est bien classé et hors de la marge, il n’influence pas la solution.
- : l’exemple est sur la frontière, à distance 0. Sa perte charnière vaut . Il viole la marge et contribue à la fonction objectif avec le poids : c’est un vecteur de support.
En code
import numpy as np
w, b = np.array([3, 4]), -10norme = 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 1w <- c(3, 4); b <- -10norme <- sqrt(sum(w^2))c(norme = norme, largeur_marge = 2 / norme) # 5 0.4
X <- rbind(c(2, 3), c(2, 1)); y <- c(+1, +1)s <- as.vector(X %*% w + b) # scores : 8 et 0data.frame(score = s, distance = abs(s) / norme, # 1.6 et 0 charniere = pmax(0, 1 - y * s)) # 0 et 1