Aller au contenu

Descendez le gradient

Au chapitre précédent, nous avons trouvé les minimums en résolvant ∇f=0\nabla f = 0 à la main. C’est possible pour x2+xy+y2x^2 + xy + y^2, mais pas pour la fonction d’erreur d’un réseau de neurones à dix millions de paramètres. Il faut une méthode qui s’approche du minimum pas à pas, sans jamais résoudre d’équation.

Revenez à notre randonneur dans le brouillard. Sa stratégie la plus naturelle : regarder dans quel sens le terrain descend le plus, faire quelques pas dans cette direction, et recommencer. C’est exactement la descente de gradient, la méthode d’optimisation la plus utilisée au monde. Dans ce chapitre, vous allez la programmer et la régler, en suivant les fonctions de notre TP.

On part d’un point x0x_0 et l’on construit une suite de points par la règle

xk+1=xk−αk ∇f(xk)x_{k+1} = x_k - \alpha_k \, \nabla f(x_k)
  • xkx_k est le point courant à l’itération kk ;
  • ∇f(xk)\nabla f(x_k) est le gradient en ce point, et −∇f(xk)-\nabla f(x_k) la direction de plus forte descente ;
  • αk>0\alpha_k \gt 0 est le pas (step size), la longueur du déplacement. En apprentissage automatique, on l’appelle le taux d’apprentissage (learning rate).

Pourquoi est-ce que ça descend ? D’après Taylor à l’ordre 1, f(xk−α∇f(xk))≈f(xk)−α∥∇f(xk)∥2f(x_k - \alpha \nabla f(x_k)) \approx f(x_k) - \alpha \lVert \nabla f(x_k) \rVert^2, qui est plus petit que f(xk)f(x_k) dès que le gradient n’est pas nul et que α\alpha est assez petit. Plus généralement, toute direction dd telle que ∇f(xk)Td<0\nabla f(x_k)^T d \lt 0 est une direction de descente.

On s’arrête quand le gradient devient presque nul (on est près d’un point critique), ou après un nombre maximal d’itérations :

∥∇f(xk)∥<δouk=K\lVert \nabla f(x_k) \rVert \lt \delta \quad \text{ou} \quad k = K

Dans notre TP, la tolérance valait δ=0,025\delta = 0{,}025 et le nombre maximal d’itérations K=20K = 20.

Tout le réglage tient dans le choix du pas. Trop petit, le randonneur avance à pas de fourmi. Trop grand, il enjambe la vallée et se retrouve sur le versant d’en face. Voyons cela sur des chiffres.

La version la plus simple garde le même pas α\alpha à chaque itération.

En TP, nous avons minimisé f(x)=x4−7x+8f(x) = x^4 - 7x + 8 en partant de x0=0,8x_0 = 0{,}8, dont le minimum exact est x∗=(7/4)1/3≈1,2051x^* = (7/4)^{1/3} \approx 1{,}2051. Nous avons testé trois pas : 0,125, 0,1 et 0,01. Le pas 0,125 ne converge pas, le pas 0,01 converge plus lentement, et le pas 0,1 donne le meilleur résultat des trois : en sortie, x≈1,204474x \approx 1{,}204474 et f(x)≈1,673380f(x) \approx 1{,}673380, pour un minimum exact f(x∗)≈1,673377f(x^*) \approx 1{,}673377.

Pourquoi un écart aussi net entre 0,1 et 0,125 ? Près du minimum, f′(x)≈f′′(x∗)(x−x∗)f'(x) \approx f''(x^*)(x - x^*), donc une itération donne

xk+1−x∗≈(1−αf′′(x∗))(xk−x∗)x_{k+1} - x^* \approx \big(1 - \alpha f''(x^*)\big)(x_k - x^*)

L’erreur est multipliée à chaque pas par le facteur 1−αf′′(x∗)1 - \alpha f''(x^*). Elle diminue si ce facteur est compris entre −1-1 et 11, c’est-à-dire si α<2/f′′(x∗)\alpha \lt 2 / f''(x^*). Ici f′′(x∗)=12(x∗)2≈17,43f''(x^*) = 12 (x^*)^2 \approx 17{,}43, d’où la limite α<0,115\alpha \lt 0{,}115.

0,000,020,040,060,080,100,120,14pas α0,000,250,500,751,001,251,501,75|1 − α f″(x*)|α = 2 / f″(x*) ≈ 0,1150,01 : ×0,830,1 : ×−0,740,125 : ×−1,18l’erreur diminuel’erreur grandit
Tant que le facteur reste sous 1 en valeur absolue, l'erreur diminue. Le pas 0,125 franchit la limite 2 / f″(x*) ≈ 0,115 : l'erreur grandit à chaque itération.
Pas α\alphaFacteur 1−αf′′(x∗)1 - \alpha f''(x^*)Comportement
0,125≈−1,18\approx -1{,}18l’erreur grandit en changeant de signe : pas de convergence
0,1≈−0,74\approx -0{,}74oscillations de part et d’autre du minimum, qui s’amortissent
0,01≈0,83\approx 0{,}83approche monotone mais lente

Si l’on rejoue le code du TP, on le voit très bien : avec 0,125, les itérées finissent par sauter indéfiniment entre environ 0,87 et 1,42 sans jamais s’approcher de 1,205 ; avec 0,01, elles ne sont encore qu’à 1,19 après les 20 itérations autorisées.

05101520itération k0,81,01,21,41,6xₖ1,420,871,19α = 0,125 : oscille sans convergerα = 0,1 : oscillations amortiesα = 0,01 : monotone mais lentminimum x* ≈ 1,2051
Les trois pas du TP : trop grand, les itérées sautent d'un versant à l'autre sans fin ; trop petit, elles avancent à pas de fourmi ; bien choisi, les oscillations s'amortissent vers x* ≈ 1,2051.

Passons à la quadratique du TP, f1(x,y)=2x2+3x+y2−2f_1(x, y) = 2x^2 + 3x + y^2 - 2, de minimum (−0,75;0)(-0{,}75 ; 0) et de valeur −3,125-3{,}125. Son gradient vaut (4x+3,2y)(4x + 3, 2y). Avec un pas fixe α=0,1\alpha = 0{,}1, nous avons obtenu :

DépartPoint finalf1f_1 final
(3,3)(3, 3)(−0,749771;0,043235)(-0{,}749771 ; 0{,}043235)−3,123131-3{,}123131
(10,10)(10, 10)(−0,749607;0,115292)(-0{,}749607 ; 0{,}115292)−3,111707-3{,}111707

La coordonnée xx est presque exacte, mais yy traîne. L’explication tient en une ligne. Une itération s’écrit, coordonnée par coordonnée :

xk+1+0,75=(1−4α)(xk+0,75),yk+1=(1−2α) ykx_{k+1} + 0{,}75 = (1 - 4\alpha)(x_k + 0{,}75), \qquad y_{k+1} = (1 - 2\alpha)\, y_k

Avec α=0,1\alpha = 0{,}1, l’erreur sur xx est multipliée par 0,6 à chaque pas, celle sur yy par 0,8 seulement. Depuis (10,10)(10, 10), après 20 itérations, y=10×0,820≈0,115y = 10 \times 0{,}8^{20} \approx 0{,}115 : c’est exactement ce qu’affiche le TP. Les deux directions ont des courbures différentes (4 et 2, les valeurs propres de la Hessienne), et un pas unique ne peut pas convenir parfaitement aux deux.

−20246810x−20246810ydépart (10, 10)minimum (−0,75 ; 0)après 20 pas : (−0,7496 ; 0,1153)x (courbure 4) : erreur × 0,6 par pasy (courbure 2) : erreur × 0,8 par pas
La coordonnée x (courbure 4) converge vite, la coordonnée y (courbure 2) traîne : après 20 pas, y vaut encore 0,115, exactement 10 × 0,8²⁰.

Plutôt que de fixer α\alpha, on peut choisir à chaque itération le meilleur pas possible dans la direction de descente :

αk=arg⁡min⁡α>0  f(xk−α∇f(xk))\alpha_k = \arg\min_{\alpha \gt 0} \; f\big(x_k - \alpha \nabla f(x_k)\big)

C’est une minimisation en une seule variable, qu’on appelle recherche linéaire (line search). Pour une fonction quadratique f(x)=12xTAx−bTxf(x) = \tfrac12 x^T A x - b^T x, elle se résout exactement. Notons dk=−∇f(xk)d_k = -\nabla f(x_k). La fonction φ(α)=f(xk+αdk)\varphi(\alpha) = f(x_k + \alpha d_k) est un polynôme de degré 2 en α\alpha, de dérivée φ′(α)=−dkTdk+α dkTAdk\varphi'(\alpha) = -d_k^T d_k + \alpha \, d_k^T A d_k. Elle s’annule pour

αk=dkTdkdkTAdk\alpha_k = \frac{d_k^T d_k}{d_k^T A d_k}

où AA est la matrice (définie positive) de la quadratique. C’est la formule que nous avons programmée en TP.

Ce n’est pas un hasard : au pas optimal, φ′(αk)=0\varphi'(\alpha_k) = 0, ce qui s’écrit ∇f(xk+1)Tdk=0\nabla f(x_{k+1})^T d_k = 0. Deux directions successives sont toujours orthogonales, et la trajectoire dessine un zigzag caractéristique. Le TP insistait d’ailleurs sur un détail : tracer les itérées dans un repère orthonormé, sans quoi ces angles droits ne se voient pas.

départ (10, 10)(−1,8 ; 4,5)minimum (−0,75 ; 0)

En TP, depuis (10,10)(10, 10), le pas optimal a atteint (−0,750332;0,001427)(-0{,}750332 ; 0{,}001427), où f1≈−3,124998f_1 \approx -3{,}124998 : bien plus près du minimum qu’avec le pas fixe (où yy valait encore 0,115). Sur f2(x,y)=x2+y2−2y+1f_2(x, y) = x^2 + y^2 - 2y + 1, le résultat est encore plus frappant : le point final est exactement (0,1)(0, 1), avec f2=0f_2 = 0. Pourquoi ? Sa matrice est A=2IA = 2I, ses lignes de niveau sont des cercles centrés sur le minimum, et le gradient pointe droit vers le centre : un seul pas optimal suffit.

Le zigzag est d’autant plus lent que les lignes de niveau sont allongées. On mesure cet allongement par le conditionnement de la Hessienne :

κ=λmax⁡λmin⁡\kappa = \frac{\lambda_{\max}}{\lambda_{\min}}

où λmax⁡\lambda_{\max} et λmin⁡\lambda_{\min} sont la plus grande et la plus petite valeur propre. Des cercles donnent κ=1\kappa = 1 ; des ellipses très aplaties, un κ\kappa énorme. Sur une quadratique, on montre que chaque itération à pas optimal multiplie l’erreur (mesurée dans la norme associée à AA) par au plus

κ−1κ+1\frac{\kappa - 1}{\kappa + 1}

C’est aussi le meilleur facteur accessible avec un pas fixe, obtenu pour α=2/(λmin⁡+λmax⁡)\alpha = 2 / (\lambda_{\min} + \lambda_{\max}).

Fonctionκ\kappaFacteur κ−1κ+1\frac{\kappa - 1}{\kappa + 1}Conséquence
f2f_2 (cercles)10un pas suffit
f1f_120,33convergence rapide
ellipses aplaties1000,98des centaines d’itérations
κ = 10 : zigzag à angles droits0102030itération10−1510−1310−1110−910−710−510−310−1erreur relativefacteur 0,98facteur 1/3un seul pasf₂, κ = 1f₁, κ = 2κ = 100
Plus les ellipses sont allongées, plus le zigzag est serré. À droite, l'erreur est multipliée à chaque pas par (κ − 1)/(κ + 1) : 0 pour les cercles de f₂, 1/3 pour f₁, 0,98 pour κ = 100.

Pour lutter contre un mauvais conditionnement, on peut changer d’échelle (préconditionnement), utiliser le gradient conjugué (qui résout une quadratique de dimension nn en au plus nn itérations) ou exploiter la courbure avec la méthode de Newton, au chapitre suivant.

Sur une fonction non quadratique, le pas optimal n’a pas de formule. Le chercher exactement coûterait trop cher, et ce serait inutile : il suffit d’un pas raisonnable. La règle d’Armijo (backtracking line search) accepte un pas dès qu’il fait baisser ff suffisamment :

f(xk−α∇f(xk))≤f(xk)−c α ∥∇f(xk)∥2f\big(x_k - \alpha \nabla f(x_k)\big) \leq f(x_k) - c \, \alpha \, \lVert \nabla f(x_k) \rVert^2

avec une petite constante cc (typiquement 10−410^{-4}). On part de α=1\alpha = 1 et l’on divise le pas par 2 tant que la condition n’est pas remplie. On évite ainsi les pas trop grands sans connaître la constante LL.

0,000,050,100,150,200,250,30pas α024681012valeur de fα = 1/4 : refuséα = 1/8 : acceptéα = 1 et 1/2 refusés (φ ≈ 1 062 et 100)pas acceptablesφ(α) = f(x₀ − α f′(x₀))seuil d’Armijo, c = 10⁻⁴tangente (pente −f′(x₀)²)
Le retour arrière sur f(x) = x⁴ − 7x + 8 depuis x₀ = 0,8 : on part de α = 1 et l'on divise par 2 jusqu'à passer sous le seuil. Ici, le premier pas accepté est α = 1/8.

Voici une descente de gradient propre, avec un critère d’arrêt sur la norme du gradient :

import numpy as np
def descente_gradient(grad, x0, pas, tol=1e-6, max_iter=10_000):
"""Descente de gradient à pas fixe ; s'arrête quand le gradient est presque nul."""
x = np.asarray(x0, dtype=float)
for k in range(max_iter):
g = grad(x)
if np.linalg.norm(g) < tol:
return x, k
x = x - pas * g
return x, max_iter
grad_f1 = lambda v: np.array([4 * v[0] + 3, 2 * v[1]])
for pas in [0.1, 1 / 3]:
x, k = descente_gradient(grad_f1, [10, 10], pas)
print(f"pas {pas:.3f} : {k} itérations, point final {x.round(4)}")
# pas 0.100 : 76 itérations ; pas 0.333 : 17 itérations (les deux autour de (-0.75, 0))

Le pas 1/3=2/(2+4)1/3 = 2/(2 + 4) est le meilleur pas fixe pour f1f_1 : il divise le nombre d’itérations par plus de quatre.

  • La descente de gradient itère xk+1=xk−αk∇f(xk)x_{k+1} = x_k - \alpha_k \nabla f(x_k) et s’arrête quand ∥∇f∥\lVert \nabla f \rVert est petit.
  • Avec un pas fixe, il faut α<2/L\alpha \lt 2/L, où LL majore la courbure ; au-delà, oscillations ou divergence, comme le pas 0,125 du TP.
  • Le pas optimal minimise ff le long de la direction ; sur une quadratique, αk=dkTdkdkTAdk\alpha_k = \frac{d_k^T d_k}{d_k^T A d_k}.
  • Au pas optimal, deux directions successives sont orthogonales : la trajectoire zigzague.
  • La vitesse dépend du conditionnement κ\kappa : facteur κ−1κ+1\frac{\kappa - 1}{\kappa + 1} par itération. Des lignes de niveau circulaires (κ=1\kappa = 1) se résolvent en un pas.
  • La règle d’Armijo choisit un pas qui fait suffisamment baisser ff, sans connaître LL.

Exercice · Un pas à la main

facile

Pour f2(x,y)=x2+y2−2y+1f_2(x, y) = x^2 + y^2 - 2y + 1, calculez une itération de gradient à pas fixe depuis (10,10)(10, 10) avec α=0,1\alpha = 0{,}1, puis avec α=0,5\alpha = 0{,}5. Que remarquez-vous ?

Voir la solution

Le gradient vaut (2x,2y−2)(2x, 2y - 2), soit (20,18)(20, 18) en (10,10)(10, 10).

  • Avec α=0,1\alpha = 0{,}1 : (10−2,  10−1,8)=(8;8,2)(10 - 2, \; 10 - 1{,}8) = (8 ; 8{,}2).
  • Avec α=0,5\alpha = 0{,}5 : (10−10,  10−9)=(0,1)(10 - 10, \; 10 - 9) = (0, 1), le minimum exact.

Pour f2f_2, la Hessienne vaut 2I2I : chaque coordonnée de l’erreur est multipliée par 1−2α1 - 2\alpha à chaque pas. Avec α=0,5=1/L\alpha = 0{,}5 = 1/L, ce facteur est nul, et l’on arrive au minimum en une itération. C’est d’ailleurs exactement le pas optimal : dTd/dTAd=1/2d^T d / d^T A d = 1/2 quand A=2IA = 2I.

En code

import numpy as np
grad_f2 = lambda v: np.array([2 * v[0], 2 * v[1] - 2])
x0 = np.array([10.0, 10.0])
for pas in [0.1, 0.5]:
print(pas, x0 - pas * grad_f2(x0)) # 0.1 [8. 8.2] ; 0.5 [0. 1.] = le minimum

Exercice · Calculer un pas optimal

moyen

Soit f(x,y)=x2+5y2f(x, y) = x^2 + 5y^2. Depuis (5,1)(5, 1), calculez le pas optimal, le nouveau point, et vérifiez l’orthogonalité des deux premières directions. Quel est le conditionnement, et quel facteur de réduction peut-on en attendre ?

Voir la solution

On a f=12xTAxf = \tfrac12 x^T A x avec A=diag(2,10)A = \mathrm{diag}(2, 10). En (5,1)(5, 1), le gradient vaut (10,10)(10, 10), donc d0=(−10,−10)d_0 = (-10, -10). Alors d0Td0=200d_0^T d_0 = 200 et d0TAd0=2×100+10×100=1200d_0^T A d_0 = 2 \times 100 + 10 \times 100 = 1200, d’où α0=1/6\alpha_0 = 1/6.

Nouveau point : (5−106,  1−106)=(103,−23)(5 - \tfrac{10}{6}, \; 1 - \tfrac{10}{6}) = (\tfrac{10}{3}, -\tfrac23). Le gradient y vaut (203,−203)(\tfrac{20}{3}, -\tfrac{20}{3}) et son produit scalaire avec d0d_0 est −2003+2003=0-\tfrac{200}{3} + \tfrac{200}{3} = 0 : les directions sont bien orthogonales.

Le conditionnement vaut κ=10/2=5\kappa = 10/2 = 5, d’où un facteur 5−15+1=23\frac{5 - 1}{5 + 1} = \tfrac23 par itération au pire : il faut environ 6 itérations pour diviser l’erreur par 10.

En code

import numpy as np
A = np.diag([2.0, 10.0]) # f = x² + 5y² = ½ x'Ax
x = np.array([5.0, 1.0])
d0 = -A @ x # direction : -gradient = (-10, -10)
alpha = d0 @ d0 / (d0 @ A @ d0)
x1 = x + alpha * d0
print(alpha, x1, (-A @ x1) @ d0) # 0.1667 [ 3.333 -0.667] 2e-14 : orthogonales
kappa = 10 / 2
facteur = (kappa - 1) / (kappa + 1)
print(kappa, facteur, np.log(0.1) / np.log(facteur)) # 5.0 0.667 5.68 : environ 6 itérations
# Vérification : on itère jusqu'à diviser l'erreur (norme A) par 10
k, e0 = 0, np.sqrt(x @ A @ x)
while np.sqrt(x @ A @ x) > e0 / 10:
d = -A @ x
x, k = x + (d @ d / (d @ A @ d)) * d, k + 1
print(k) # 6

Exercice · Le plus grand pas admissible

difficile

Pour f1(x,y)=2x2+3x+y2−2f_1(x, y) = 2x^2 + 3x + y^2 - 2 et la descente à pas fixe :

  1. Pour quelles valeurs de α\alpha la méthode converge-t-elle, quel que soit le point de départ ?
  2. Que se passe-t-il exactement pour α=0,5\alpha = 0{,}5 ?
  3. Quel pas fixe minimise le plus lent des deux facteurs de réduction ?
Voir la solution
  1. Les erreurs sur xx et yy sont multipliées par 1−4α1 - 4\alpha et 1−2α1 - 2\alpha. Il faut ∣1−4α∣<1\lvert 1 - 4\alpha \rvert \lt 1 et ∣1−2α∣<1\lvert 1 - 2\alpha \rvert \lt 1, soit 0<α<0,50 \lt \alpha \lt 0{,}5 (la première condition est la plus sévère). On retrouve α<2/L\alpha \lt 2/L avec L=λmax⁡=4L = \lambda_{\max} = 4.
  2. Pour α=0,5\alpha = 0{,}5, le facteur sur xx vaut −1-1 : l’erreur sur xx change de signe à chaque pas sans diminuer. Depuis (10,10)(10, 10), xx alterne indéfiniment entre 1010 et −11,5-11{,}5 (symétriques par rapport à −0,75-0{,}75), tandis que yy converge (facteur 0). La méthode ne converge pas.
  3. On veut minimiser max⁡(∣1−4α∣,∣1−2α∣)\max(\lvert 1 - 4\alpha \rvert, \lvert 1 - 2\alpha \rvert). Le maximum est le plus petit quand les deux facteurs sont opposés : 4α−1=1−2α4\alpha - 1 = 1 - 2\alpha, soit α=13\alpha = \tfrac13. Les deux facteurs valent alors 13\tfrac13 en valeur absolue, ce qui est bien κ−1κ+1\frac{\kappa - 1}{\kappa + 1} avec κ=2\kappa = 2.

En code

import numpy as np
# Facteurs de réduction sur x et sur y : 1 - 4α et 1 - 2α
pas = np.arange(10, 6000) / 10_000 # de 0,001 à 0,6
pire = np.maximum(abs(1 - 4 * pas), abs(1 - 2 * pas))
print(pas[pire < 1].max()) # 0.4999 : convergence si 0 < α < 0,5
print(pas[pire.argmin()], pire.min()) # 0.3333 0.3334 : meilleur pas 1/3, facteur 1/3
# α = 0,5 depuis (10, 10) : x oscille sans converger
grad_f1 = lambda v: np.array([4 * v[0] + 3, 2 * v[1]])
v = np.array([10.0, 10.0])
for k in range(4):
v = v - 0.5 * grad_f1(v)
print(v) # [-11.5 0] [10 0] [-11.5 0] [10 0]