- Je onderscheidt het eerste moment en het ongecentreerde tweede moment van gradiënten.
- Je bouwt Adam met een correcte stappenteller, biascorrectie en epsilon buiten de wortel.
Een parameter kan steeds grote gradiënten krijgen, terwijl een andere vooral kleine waarden ziet. Adam bewaart daarom zowel een gemiddelde richting als een gemiddelde kwadratische grootte. Daarmee schaalt hij de update voor iedere parameter afzonderlijk.
Je bouwt drie onderdelen: twee momentenschattingen, hun correctie voor de nulstart en de uiteindelijke stap. Bewaar die toestand tussen updates. Adam kan het afstellen van training helpen, maar garandeert geen beter model of globale convergentie voor ieder probleem.
1. Bewaar richting en grootte apart
De visualisatie laat zien hoe een gemiddelde vanuit nul op gang komt. Verhoog de stappenteller en keer daarna de gradiënt om. De gecorrigeerde oranje lijn verhelpt de nulstart, maar volgt een nieuwe richting nog steeds met vertraging.
Gradiënt g=2, eventueel −2 vanaf stap 11. Groen is m, oranje is het gecorrigeerde m̂. De grijze lijn is g.
β₂=0.999. v̂ blijft hier vier: g² verandert niet bij een tekenwissel. Biascorrectie verwijdert de nulstartbias, niet de vertraging bij een veranderend signaal.
Het eerste moment m middelt de gradiënt g; het tweede moment v middelt g², elementgewijs. De letter v benoemt hier het tweede moment, niet de snelheid uit de momentumles. Beta1 en beta2 bepalen hoeveel van de vorige schatting blijft staan; beide zijn hier minstens nul en strikt kleiner dan één.
Begin met m=v=0 en kies voor het handvoorbeeld beide beta's 0.5. Bij g=2 krijg je m=1 en v=2; bij een tweede component g=−1 worden die −0.5 en 0.5. Het kwadraat is altijd niet-negatief en wordt genomen vóór het middelen.
import numpy as np
def adam_momenten(m,v,g,beta1=.9,
beta2=.999):
m = np.asarray(m,dtype=float)
v = np.asarray(v,dtype=float)
g = np.asarray(g,dtype=float)
return (beta1*m+(1-beta1)*g,
beta2*v+(1-beta2)*g*g)
m, v = adam_momenten([0,0],[0,0],
[2,-1],.5,.5)
print(m.tolist(), v.tolist())
# [1.0, -0.5] [2.0, 0.5]
Maak adam_momenten als tuple met twee nieuwe NumPy-arrays van dezelfde vorm als de invoer, zonder m, v of g te veranderen. Anders dan bij onze momentumconventie krijgt de nieuwe gradiënt hier wel factor 1−beta1. V is geen gecentreerde variantie: er wordt geen kwadraat van het gemiddelde afgetrokken.
2. Compenseer het begin bij nul
Bij de eerste stap heeft m maar een deel van g ontvangen. De nulstart trekt de schatting dus naar nul. Deel daarom door één min beta tot de macht t, met t als het totale aantal uitgevoerde updates, vanaf één.
Met beta 0.5 en t=1 deel je onze schattingen door 0.5. Je krijgt m-hat=[2,−1] en v-hat=[4,1], precies de oorspronkelijke gradiënten en hun kwadraten. De standaardbeta's in code zijn 0.9 en 0.999; de visualisatie houdt beta2 op 0.999.
def corrigeer_momenten(m,v,t,beta1=.9,
beta2=.999):
if t < 1:
raise ValueError('t begint bij 1')
mh = np.asarray(m)/(1-beta1**t)
vh = np.asarray(v)/(1-beta2**t)
return mh, vh
mh, vh = corrigeer_momenten(m,v,1,.5,.5)
print(mh.tolist(), vh.tolist())
# [2.0, -1.0] [4.0, 1.0]
Maak deze functie met twee NumPy-arrays als resultaat; weiger een t kleiner dan 1 met ValueError en bewaar de invoer. Bij een constante gradiënt herstelt de correctie de constante waarden. Zij verwijdert de nulstartbias, niet alle vertraging wanneer het signaal verandert.
3. Bewaar ruwe momenten voor de volgende stap
De state is een dictionary met m, v en t. Verhoog t, werk beide ruwe momenten bij, corrigeer ze en bereken dan de nieuwe gewichten. Gebruik de gecorrigeerde arrays alleen voor de stap; bewaar de ruwe momenten in de nieuwe state.
Eta is de leersnelheid en epsilon een kleine positieve stabilisator, hier standaard 10⁻⁸. Zij staat buiten de wortel. De bewerkingen zijn elementgewijs, zodat iedere parameter zijn eigen noemer krijgt.
def adamstap(w,g,state,eta=.001,
beta1=.9,beta2=.999,
epsilon=1e-8):
t = state['t'] + 1
m,v = adam_momenten(state['m'],
state['v'],g,
beta1,beta2)
mh,vh = corrigeer_momenten(
m,v,t,beta1,beta2)
stap = eta*mh/(np.sqrt(vh)+epsilon)
new_w = np.asarray(w)-stap
return new_w,dict(m=m,v=v,t=t)
w = np.array([1.])
state = dict(m=np.zeros(1),
v=np.zeros(1),t=0)
for g in [[2.],[-1.],[-1.]]:
w,state = adamstap(w,g,state,.1,.5,.5)
print(round(w[0],6), state['t'])
# 0.9 1
# 0.9 2
# 0.947809 3
Bij de tweede gradiënt −1 worden de ruwe m=0 en v=1.5. Na delen door 0.75 blijft m-hat nul, dus de tweede gewichtsupdate is nul. Een reset van t of het bewaren van gecorrigeerde momenten zou de volgende stap veranderen.
Maak adamstap als (new_w,new_state), met een nieuwe NumPy-gewichtsarray en nieuwe dictionary. Bewaar w, g en alle oude statevelden intact. Eén state hoort bij één doorlopende optimizer; begin een nieuwe state wanneer je een nieuw trainingsexperiment start.
4. Wanneer is de eerste stap ongeveer eta?
Bij nulstate vereenvoudigt de af te trekken stap s tot eta×g/(|g|+epsilon). Alleen als |g| veel groter is dan epsilon, is de grootte ongeveer eta. Voor een heel kleine gradiënt kan epsilon juist overheersen.
g = np.array([2., 1e-12])
stap = .1*g/(np.abs(g)+1e-8)
print(np.round(stap,8).tolist())
# [0.1, 1e-05]
De tweede component beweegt hier ongeveer tienduizendmaal minder dan eta. Ook latere stappen hangen van eerdere gradiënten af. “Adam doet altijd dezelfde stap” is dus een verkeerde conclusie uit het eenvoudige eerste voorbeeld.
03Wat neem je mee?
- Adam bewaart richting en ongecentreerde kwadratische grootte per parameter.
- Correctie gebruikt de totale stappenteller; bewaar ruwe momenten voor de volgende update.
- Epsilon staat buiten de wortel en kan de stap bij kleine gradiënten sterk beïnvloeden.
Hierna onderzoek je hoe normalisatie, dropout en leerschema's het trainingsgedrag verder veranderen.