Reliez l'optimisation à l'apprentissage
Quand on dit qu’un modèle « apprend », que se passe-t-il concrètement dans la machine ? Presque toujours la même chose : un algorithme d’optimisation ajuste des paramètres pour rendre une erreur la plus petite possible. La régression linéaire, la régression logistique, les SVM et les réseaux de neurones ne diffèrent que par la fonction à minimiser et par la méthode employée pour le faire.
Ce dernier chapitre relie tout ce que vous avez vu à l’apprentissage automatique : la convexité, la descente de gradient et son pas, Newton et BFGS, les multiplicateurs de Lagrange et KKT. Vous y découvrirez aussi les outils propres au domaine : le gradient stochastique, le momentum et Adam. Pour les modèles eux-mêmes, reportez-vous au cours d’Apprentissage automatique.
Entraîner un modèle, c’est minimiser
Section intitulée « Entraîner un modèle, c’est minimiser »Un modèle dépend de paramètres (les poids d’une régression, ceux d’un réseau de neurones). On dispose de exemples et d’une fonction de perte qui mesure l’écart entre une prédiction et la vraie valeur. Entraîner le modèle, c’est résoudre
- est le risque empirique, la perte moyenne sur les données d’apprentissage ;
- peut être le carré de l’erreur (régression), l’entropie croisée (classification), la perte charnière (SVM)…
C’est un problème sans contrainte (partie 2), souvent en très grande dimension, et dont la fonction objectif est une somme sur les exemples. Cette structure de somme va tout changer.
Le cas convexe par excellence : les moindres carrés
Section intitulée « Le cas convexe par excellence : les moindres carrés »Pour une régression linéaire, on range les exemples dans une matrice (une ligne par exemple, une colonne par variable, plus une colonne de 1 pour la constante) et l’on minimise la somme des carrés des erreurs :
Son gradient vaut et sa Hessienne , semi-définie positive : le problème est convexe, c’est une quadratique du chapitre 5. Annuler le gradient donne les équations normales :
Si les colonnes de sont linéairement indépendantes, est définie positive et la solution est unique.
Le gradient stochastique
Section intitulée « Le gradient stochastique »Le gradient de est une moyenne de gradients individuels : . Avec un million d’exemples, chaque pas de descente de gradient demande de parcourir tout le jeu de données. C’est beaucoup de calcul pour un seul petit pas.
L’idée du gradient stochastique (stochastic gradient descent, SGD) : estimer ce gradient sur un petit échantillon tiré au hasard, un mini-lot (mini-batch) de quelques dizaines d’exemples :
Cette estimation est sans biais (en moyenne, elle vaut le vrai gradient) mais bruitée. On fait donc beaucoup plus de pas, chacun beaucoup moins cher, et l’on parcourt le jeu de données par époques (une époque = tous les exemples vus une fois).
| Gradient complet | SGD (un exemple) | Mini-lots | |
|---|---|---|---|
| Coût d’un pas | gradients | 1 gradient | gradients (32 à 512) |
| Bruit | aucun | très fort | modéré |
| Convergence avec pas fixe | vers le minimum | fluctue autour du minimum | fluctue, moins fort |
| Usage | petits jeux de données | historique | standard en apprentissage profond |
Le bruit a deux conséquences. D’une part, avec un pas fixe, les itérées ne convergent pas exactement : elles tournent autour du minimum dans une zone d’autant plus large que le pas est grand. Pour converger, il faut un pas décroissant (par exemple ). D’autre part, ce bruit aide à s’échapper des points selles, très nombreux dans les réseaux de neurones, où un gradient exact resterait coincé.
Accélérer : momentum et Adam
Section intitulée « Accélérer : momentum et Adam »Le momentum
Section intitulée « Le momentum »Au chapitre 6, la descente de gradient zigzaguait dans les vallées étroites. Le momentum (méthode de la boule lourde) lisse la trajectoire en accumulant une « vitesse » :
avec proche de 1 (typiquement 0,9). Dans les directions où le gradient change de signe à chaque pas (le zigzag), les contributions successives se compensent ; dans la direction de la vallée, elles s’additionnent. La boule prend de l’élan dans la bonne direction.
Adam (adaptive moment estimation) combine le momentum et un pas adapté à chaque coordonnée. Avec le gradient (stochastique) à l’itération :
- est une moyenne mobile des gradients (le momentum) ;
- est une moyenne mobile de leurs carrés, coordonnée par coordonnée ;
- et corrigent le biais dû à l’initialisation à zéro ;
- (de l’ordre de ) évite la division par zéro.
Diviser par donne de grands pas aux paramètres dont le gradient est faible et de petits pas à ceux dont le gradient est fort. C’est une forme bon marché de préconditionnement : Adam compense en partie le mauvais conditionnement, un peu comme Newton, mais sans Hessienne. Les valeurs par défaut (, , ) fonctionnent souvent sans réglage, ce qui en a fait l’optimiseur par défaut de l’apprentissage profond.
Voici un SGD par mini-lots avec momentum sur une régression linéaire, comparé à la solution exacte des moindres carrés :
import numpy as np
rng = np.random.default_rng(0)n = 1000X = np.column_stack([np.ones(n), rng.uniform(0, 10, n)]) # constante + une variabley = 2.0 + 0.5 * X[:, 1] + rng.normal(0, 1, n) # vraie droite : 2 + 0,5 x
w_exact, *_ = np.linalg.lstsq(X, y, rcond=None) # moindres carrés exacts
w, v = np.zeros(2), np.zeros(2)pas, beta, taille_lot = 0.001, 0.9, 32for epoque in range(50): ordre = rng.permutation(n) # on mélange à chaque époque for debut in range(0, n, taille_lot): lot = ordre[debut:debut + taille_lot] grad = 2 * X[lot].T @ (X[lot] @ w - y[lot]) / len(lot) # gradient sur le mini-lot v = beta * v + grad # momentum w = w - pas * v
print(w_exact.round(3), w.round(3)) # [1.999 0.498] [1.985 0.474]# les deux sont proches de (2, 0.5) ; avec un pas fixe, w fluctue de quelques centièmes d'une époque à l'autreAvec PyTorch, la même boucle se réduit à quelques lignes, l’optimiseur se chargeant des mises à jour :
import torch
modele = torch.nn.Linear(1, 1)optimiseur = torch.optim.Adam(modele.parameters(), lr=1e-2)perte = torch.nn.MSELoss()
x = torch.rand(1000, 1) * 10y = 2 + 0.5 * x + torch.randn(1000, 1)for epoque in range(200): optimiseur.zero_grad() # remet les gradients à zéro erreur = perte(modele(x), y) # risque empirique erreur.backward() # calcule le gradient (rétropropagation) optimiseur.step() # un pas d'AdamLa régularisation, une contrainte déguisée
Section intitulée « La régularisation, une contrainte déguisée »Pour éviter le surapprentissage, on régularise : on empêche les poids de devenir trop grands. Deux formulations semblent différentes.
- Contrainte : sous (un budget sur la taille des poids).
- Pénalité : (la régression Ridge quand est l’erreur quadratique).
Les conditions KKT du chapitre 9 montrent qu’elles sont équivalentes. Pour la version contrainte, la stationnarité s’écrit , qui est exactement la condition d’optimalité de la version pénalisée avec . Le coefficient de régularisation est un multiplicateur de Lagrange. À chaque budget correspond un , et inversement : un grand correspond à un petit budget.
Pour les moindres carrés, la solution Ridge vérifie
Ajouter augmente toutes les valeurs propres de : la matrice devient définie positive même si les variables sont corrélées, et son conditionnement s’améliore. La régularisation stabilise donc à la fois le modèle et l’optimisation.
Avec la norme (), l’ensemble réalisable est un losange (en dimension 2) dont les coins sont sur les axes. Les lignes de niveau de le touchent souvent en un coin, où certains poids sont exactement nuls : c’est pourquoi le Lasso sélectionne des variables.
Convexité, SVM et conditions KKT
Section intitulée « Convexité, SVM et conditions KKT »La convexité (chapitre 5) sépare les modèles en deux familles.
| Modèle | Problème | Conséquence | Optimiseur typique |
|---|---|---|---|
| Régression linéaire, Ridge | quadratique convexe | minimum global, formule fermée possible | équations normales, QR, gradient |
| Régression logistique | convexe | minimum global, indépendant de l’initialisation | L-BFGS, Newton |
| SVM | quadratique convexe sous contraintes | minimum global, dual via KKT | solveurs dédiés (SMO) |
| Réseau de neurones | non convexe | minimums locaux, points selles | SGD, momentum, Adam |
Pour un réseau de neurones, on ne vise pas le minimum global : on cherche un bon minimum local, qui généralise bien. Le résultat dépend alors de l’initialisation, du taux d’apprentissage et même du bruit du SGD.
Les SVM sont un bel exemple de KKT à l’œuvre. Pour des données séparables, la SVM à marge maximale résout
où est la classe de l’exemple . Avec un multiplicateur par contrainte, les conditions KKT donnent :
- stationnarité : et ;
- complémentarité : .
La complémentarité a une conséquence remarquable : seuls les exemples situés exactement sur la marge peuvent avoir . Ce sont les vecteurs de support, et ne dépend que d’eux ; tous les autres exemples pourraient disparaître sans changer le modèle. Enfin, le problème dual (chapitre 9) ne fait intervenir les données que par leurs produits scalaires , qu’on peut remplacer par un noyau : c’est l’astuce du noyau.
En résumé
Section intitulée « En résumé »- Entraîner un modèle revient à minimiser un risque empirique, une moyenne de pertes sur les exemples.
- Les moindres carrés sont convexes ; leur solution vérifie les équations normales , qu’on résout en pratique par QR ou
lstsq. - Le SGD estime le gradient sur un mini-lot : des pas bien moins chers mais bruités, qui demandent un pas décroissant pour converger exactement.
- Le momentum lisse le zigzag ; Adam ajoute un pas adapté à chaque paramètre, une forme de préconditionnement.
- La régularisation Ridge équivaut à une contrainte sur la norme des poids ; son coefficient est un multiplicateur de Lagrange, et elle améliore le conditionnement.
- Dans une SVM, la complémentarité KKT fait apparaître les vecteurs de support, et le dual permet l’astuce du noyau.
Exercices
Section intitulée « Exercices »Exercice · Résoudre les équations normales
facileAjustez une droite aux points , et par les moindres carrés.
Voir la solution
Les sommes valent , , et , pour . Les équations normales s’écrivent et . En soustrayant, , donc , puis .
La droite est . Résidus : , , , de somme nulle.
En code
import numpy as np
x = np.array([0, 1, 2]); y = np.array([0, 1, 3])X = np.column_stack([x, np.ones(3)]) # colonnes : a (pente), b (constante)print(X.T @ X, X.T @ y) # [[5 3] [3 3]] [7 4] : équations normalesw, *_ = np.linalg.lstsq(X, y, rcond=None)print(w) # [ 1.5 -0.1667] : y = 1,5 x - 1/6residus = y - X @ wprint(residus, residus.sum()) # [ 0.1667 -0.3333 0.1667] 0 (à 1e-15 près)x <- c(0, 1, 2); y <- c(0, 1, 3)X <- cbind(a = x, b = 1) # colonnes : pente, constantecrossprod(X); crossprod(X, y) # [5 3; 3 3] et (7, 4) : équations normalessolve(crossprod(X), crossprod(X, y)) # a = 1.5, b = -0.1667modele <- lm(y ~ x) # même résultat avec lmcoef(modele); residuals(modele) # -0.1667 1.5 ; 0.1667 -0.3333 0.1667Exercice · Ridge et conditionnement
moyenDans une régression, la matrice a pour valeurs propres 1 et 100. On la résout par descente de gradient avec le meilleur pas fixe.
- Quel est le facteur de réduction de l’erreur par itération ?
- Même question pour Ridge avec .
- Combien d’itérations faut-il, environ, pour diviser l’erreur par 1 000 dans chaque cas ?
Voir la solution
- Le conditionnement vaut , d’où un facteur .
- Avec Ridge, la matrice devient , de valeurs propres 11 et 110 : , et le facteur vaut .
- Il faut tel que , soit itérations sans régularisation, contre avec Ridge. La régularisation rend l’optimisation environ dix fois plus rapide (au prix d’un léger biais sur les coefficients).
En code
import numpy as np
for nom, vp in [("sans régularisation", [1, 100]), ("Ridge λ = 10", [11, 110])]: lmin, lmax = min(vp), max(vp) facteur = (lmax / lmin - 1) / (lmax / lmin + 1) # Vérification : gradient au meilleur pas fixe sur la quadratique ½ w'Aw A, pas = np.diag(vp).astype(float), 2 / (lmin + lmax) w, k = np.ones(2), 0 while np.linalg.norm(w) > 1e-3 * np.sqrt(2): # erreur divisée par 1000 w, k = w - pas * A @ w, k + 1 print(nom, round(facteur, 3), round(np.log(1e-3) / np.log(facteur), 1), k)# sans régularisation 0.98 345.4 346# Ridge λ = 10 0.818 34.4 35for (vp in list(c(1, 100), c(11, 110))) { kappa <- max(vp) / min(vp) facteur <- (kappa - 1) / (kappa + 1) # Vérification : gradient au meilleur pas fixe sur la quadratique ½ w'Aw A <- diag(vp); pas <- 2 / sum(vp) w <- c(1, 1); k <- 0 while (sqrt(sum(w^2)) > 1e-3 * sqrt(2)) { w <- w - pas * A %*% w; k <- k + 1 } cat("kappa", kappa, ": facteur", round(facteur, 3), ", théorie", round(log(1e-3) / log(facteur), 1), ", simulation", k, "\n")}# kappa 100 : facteur 0.98 , théorie 345.4 , simulation 346# kappa 10 : facteur 0.818 , théorie 34.4 , simulation 35Exercice · Trouver les vecteurs de support
difficileOn dispose de deux exemples, de classe et de classe .
- Résolvez la SVM à marge maximale (on admettra que par symétrie). Quelle est la largeur de la marge ?
- Calculez les multiplicateurs et avec les conditions KKT.
- On ajoute un troisième exemple de classe . Que deviennent la solution et ?
Voir la solution
- Par symétrie, avec . Les contraintes s’écrivent et . On minimise , donc on prend le plus petit possible : , et . La marge vaut , exactement la distance entre les deux points () : la frontière passe au milieu.
- La stationnarité donne (car ) et . Comme , . Les deux multiplicateurs sont strictement positifs : les deux points sont des vecteurs de support, et les deux contraintes sont bien actives.
- Pour , : la contrainte est inactive. Par complémentarité, . Les conditions KKT restent vérifiées avec les mêmes , , et , et le problème est convexe : la solution ne change pas. Le point , loin de la frontière, n’est pas un vecteur de support.
En code
import numpy as npfrom sklearn.svm import SVC
X = np.array([[1, 1], [-1, -1]]); y = np.array([1, -1])svm = SVC(kernel="linear", C=1e6).fit(X, y) # C très grand : marge durew = svm.coef_[0]print(w, svm.intercept_, 2 / np.linalg.norm(w)) # [0.5 0.5] [0.] 2.83print(svm.dual_coef_) # [[-0.25 0.25]] : α_i y_i, donc α = 1/4
# On ajoute x3 = (2, 3) de classe +1X3 = np.vstack([X, [2, 3]]); y3 = np.append(y, 1)svm3 = SVC(kernel="linear", C=1e6).fit(X3, y3)print(svm3.coef_[0], svm3.support_) # [0.5 0.5] [1 0] : x3 n'est pas supportprint(w @ [2, 3]) # 2.5 > 1 : contrainte inactive, α3 = 0X <- rbind(c(1, 1), c(-1, -1), c(2, 3)); y <- c(1, -1, 1)svm <- function(n) { # marge dure sur les n premiers points ui <- y[1:n] * cbind(X[1:n, , drop = FALSE], 1) # y_i (w'x_i + b) - 1 >= 0 res <- constrOptim(c(2, 2, 0), function(t) sum(t[1:2]^2) / 2, grad = function(t) c(t[1:2], 0), ui = ui, ci = rep(1, n), outer.eps = 1e-10) round(res$par, 3) # (w1, w2, b)}t2 <- svm(2); t2 # 0.5 0.5 0 : w = (0.5, 0.5), b = 02 / sqrt(sum(t2[1:2]^2)) # 2.83 : largeur de la marge# Stationnarité w = α (x1 - x2) = 2α (1, 1), donc α1 = α2 = 1/4t2[1] / 2svm(3) # 0.5 0.5 0 : la solution ne change passum(t2[1:2] * X[3, ]) # 2.5 > 1 : x3 hors de la marge, α3 = 0