- Je bewaart een snelheidsvector tussen updates en legt de gebruikte conventie vast.
- Je onderzoekt wanneer momentum een dal versnelt of juist overschrijdt.
Een minibatch kan je model de ene keer naar links duwen en de volgende keer naar rechts. Een optimizer met momentum kijkt daarom ook naar eerdere zoekrichtingen. Consequente beweging kan optellen, terwijl afwisselende componenten elkaar deels opheffen.
Denk aan een duw waarvan een deel nog doorwerkt bij de volgende stap. De extra toestand heet hier de snelheid v. Dit geheugen kan helpen, maar kan je ook voorbij het laagste punt voeren; meer momentum is geen algemene verbetering.
1. Tel de huidige duw bij het geheugen
Bekijk eerst het smalle dal. Groen gebruikt alleen de huidige gradiënt; oranje bewaart momentum. Zet het aantal updates op één en daarna op twee: waarom kan een tweede stap weer hoger uitkomen, terwijl de eerste stap de fout verkleinde?
L=½(w₀²+20w₁²), start (3,1.5). Groen: SGD. Oranje: momentum. Beide krijgen dezelfde leersnelheid en hetzelfde aantal updates.
We gebruiken een vaste conventie: vermenigvuldig de oude snelheid met mu, tel de huidige gradiënt g erbij op en trek eta maal de nieuwe snelheid van w af. Mu ligt hier tussen nul en één; eta is de leersnelheid. De snelheid bevat zelf nog geen leersnelheid.
De index t telt updates. Begin bij w=1, v=0, g=2, mu=0.5 en eta=0.1. De nieuwe snelheid is 2 en het gewicht wordt 0.8. Bij mu=0 verdwijnt het geheugen en krijg je gewone gradient descent.
import numpy as np
def momentumstap(w,v,g,eta,mu):
w = np.asarray(w,dtype=float)
v = np.asarray(v,dtype=float)
g = np.asarray(g,dtype=float)
new_v = mu*v + g
return w-eta*new_v, new_v
w, v = momentumstap([1.],[0.],[2.],.1,.5)
print(w.tolist(), v.tolist()) # [0.8] [2.0]
Maak momentumstap als (new_w,new_v), met twee NumPy-arrays van dezelfde vorm als de invoer. Verander w, v en g niet in-place. Andere bronnen kunnen eta al in de snelheid verwerken of de nieuwe gradiënt met 1−mu wegen; meng zulke conventies niet met deze opdracht.
2. Een tekenwissel wordt anders ontvangen
Laat de gradiënt eerst nogmaals 2 zijn en daarna −2. Vanuit nul snelheid geeft dat [2,3,−0.5]. Bij de derde stap weegt de vorige snelheid nog voor 1.5 mee; de negatieve nieuwe duw maakt de som −0.5, dus het gewicht beweegt weer omhoog.
w, v = np.array([1.]), np.array([0.])
for g in [2., 2., -2.]:
w, v = momentumstap(w,v,[g],.1,.5)
print(round(v[0],3), round(w[0],3))
# 2.0 0.8
# 3.0 0.5
# -0.5 0.55
Maak snelheden(gradients,mu) voor een lijst scalar-gradiënten, met beginwaarde v=0. Geef de nieuwe snelheid na iedere update als lijst terug. De hele opgegeven reeks telt mee, ook als de richting verandert.
Voor een constante gradiënt g komt iedere vorige duw met een extra factor mu terug: g, mu×g, mu²×g, enzovoort. De som van die steeds kleiner wordende bijdragen is een meetkundige reeks. Bij nul startsnelheid kun je de recurrentie onafhankelijk controleren met deze formule.
Bij g=2 en mu=0.5 krijg je achtereenvolgens 2,3,3.5 en 3.75; de limiet is 4. Dezelfde eta kan dus later een grotere verplaatsing geven dan zonder geheugen. Vergelijk optimizers daarom niet alleen op de naam van hun leersnelheid.
def snelheden(gradients,mu):
v, result = 0., []
for g in gradients:
v = mu*v + g
result.append(v)
return result
print(snelheden([2.]*4,.5))
# [2.0, 3.0, 3.5, 3.75]
023. Bereken het hele traject
Het getekende dal heeft verlies één helft van w₀²+20w₁² en gradiënt [w₀,20w₁]. De tweede richting is twintig keer zo steil. Vanuit [3,1.5] geeft eta=0.05 eerst [2.85,0]; bij mu=0.8 wordt de tweede positie [2.5875,−1.2].
De beginloss is 27 en na de eerste stap ongeveer 4.06125. De tweede stap met momentum geeft ongeveer 17.74758: geheugen heeft de steile richting voorbij nul geduwd. Het traject hoeft dus niet bij iedere stap te dalen.
Maak momentumtraject(start,eta,mu,steps) als NumPy-array van vorm (steps+1,2). Begin met nul snelheid en bewaar zowel de start als iedere nieuwe positie. Gebruik de eigen momentumstap met de actuele dalgradiënt.
def momentumtraject(start,eta,mu,steps):
w = np.array(start,dtype=float)
v = np.zeros_like(w)
history = [w.copy()]
for _ in range(steps):
g = np.array([w[0],20*w[1]])
w, v = momentumstap(w,v,g,eta,mu)
history.append(w.copy())
return np.array(history)
pad = momentumtraject([3,1.5],.05,.8,2)
print(np.round(pad,4))
# [[ 3. 1.5 ]
# [ 2.85 0. ]
# [ 2.5875 -1.2 ]]
loss = .5*pad[:,0]**2 + 10*pad[:,1]**2
print(np.round(loss,5))
# [27. 4.06125 17.74758]
Bewaar kopieën van posities en verander de startarray niet. Als alle geschiedenismomenten naar dezelfde gewijzigde array wijzen, zie je achteraf alleen de eindpositie herhaald. We gebruiken hier niet-negatieve gehele aantallen stappen; nul stappen geeft alleen de start terug.
03Wat neem je mee?
- Momentum bewaart een gewogen spoor van eerdere gradiënten, met een expliciete updateconventie.
- Constante duwen kunnen optellen; een tekenwissel en overshoot vragen aandacht.
- Meet het echte traject en bewaar eerdere posities zonder ze mee te veranderen.
Hierna gebruikt Adam ook de grootte van eerdere gradiënten om iedere parameter anders te schalen.