- Je verzamelt foutsignalen over alle verbindingen in een gedeelde rekengraaf.
- Je voorkomt onbedoelde ophoping tussen backward-aanroepen en spoort een foute lokale afgeleide op.
Eén gewicht kan op meerdere plekken in een berekening terugkomen. Als je alleen de laatste teruggaande bijdrage bewaart, vergeet je een deel van zijn invloed. We bouwen daarom een terugweg die alle routes verzamelt en hun bijdragen optelt.
Je gebruikt de knopen uit de vorige les. De editor bevat hun functies en de topologische volgorde al; jij maakt de gradiënten schoon, loopt de graaf achteruit en controleert de uitkomst met een onafhankelijke meting.
1. Begin iedere terugweg schoon
Neem x = 3, s = x×x en f = s+x. Vooruit krijg je s = 9 en f = 12. Een eerdere terugberekening kan nog grad-waarden in de knopen hebben achtergelaten; zet alle bereikbare gradiënten eerst op nul.
def wis_gradients(root):
for node in volgorde(root):
node['grad'] = 0.
x = knoop(3)
s = keer(x, x)
f = optel(s, x)
ander = knoop(8)
x['grad'], s['grad'], f['grad'] = 7, 8, 9
ander['grad'] = 5
wis_gradients(f)
print(x['grad'], s['grad'], f['grad'])
# 0.0 0.0 0.0
print(ander['grad']) # 5
Het losse blad ander hoort niet bij deze graaf en blijft ongemoeid. Maak wis_gradients(root) zonder waarden of ouderverbindingen te veranderen. Alleen bladeren wissen is onvoldoende: een oud signaal in een tussenknoop kan anders opnieuw naar beneden stromen.
Sommige engines verzamelen bewust gradiënten van meerdere berekeningen. Ons contract is eenvoudiger: iedere aanroep begint opnieuw. Een tweede aanroep met dezelfde graaf moet daardoor dezelfde uitkomst geven.
012. Vermenigvuldig onderweg, tel samenkomende routes op
Start bij f met de seed, standaard 1. Dat is de afgeleide van de uitkomst naar zichzelf. Loop daarna de topologische volgorde achteruit, zodat een knoop alle signalen van haar kinderen heeft ontvangen voordat zij haar ouders bijwerkt.
Voor s=x×x en f=s+x heeft x drie terugwegen. Volg ze apart; de twee ingangen van de vermenigvuldiging mogen niet samenvallen.
- Rechtstreeks via de optelling—
- Via de eerste ingang van x×x—
- Via de tweede ingang van x×x—
De gradiënten staan op nul.
Bij x = 3 gaat één bijdrage rechtstreeks van f naar x: 1. Via s ontvangt x tweemaal factor 3, omdat x beide ingangen van de vermenigvuldiging vult. Samen geven die routes 1+3+3 = 7, precies de afgeleide van het kwadraat plus de lineaire term.
| Terugstap | Bijdrage bij x | Totaal bij x |
|---|---|---|
| Optelling → x | 1 | 1 |
| Product, ingang 1 | 1 × 3 | 4 |
| Product, ingang 2 | 1 × 3 | 7 |
De algemene bijdrage langs één verbinding is de binnengekomen totale gradiënt maal de opgeslagen lokale factor. Noem de totale gradiënt g en de lokale factor ℓ; bij een ouder a van knoop c tel je dus g(c)×ℓ erbij op.
def terug(root, seed=1.):
wis_gradients(root)
root['grad'] = float(seed)
for node in reversed(volgorde(root)):
for ouder, local in node['parents']:
bijdrage = node['grad'] * local
ouder['grad'] += bijdrage
terug(f)
print(f['value'], x['grad']) # 12.0 7.0
terug(f)
print(x['grad']) # 7.0, geen 14
terug(f, seed=2)
print(x['grad']) # 14.0
terug(f, seed=0)
print(x['grad']) # 0.0
reversed leest de lijst van achteren naar voren. De dubbele lus verwerkt iedere verbinding, ook als dezelfde ouder tweemaal voorkomt. Maak terug(root,seed=1.0) met +=; met = zou in dit voorbeeld alleen de laatste bijdrage 3 overblijven.
Een andere seed schaalt de hele terugweg. Seed 2 betekent de afgeleide van tweemaal de einduitkomst; een negatieve seed draait de tekens om. Dit is nog steeds één gezamenlijke terugweg door dezelfde graaf.
023. Gebruik ook een meetlat die fouten vindt
Voor een onafhankelijke controle verander je een bladwaarde een klein beetje omhoog en omlaag. Bouw in beide gevallen een nieuwe graaf: alleen een bestaand value-veld veranderen zou de opgeslagen tussenresultaten niet opnieuw berekenen. Het centrale verschil benadert daarna de afgeleide.
Neem nu f(x,y) = x²+xy bij x = 3 en y = −2. De waarde is 3, de afgeleide naar x is 2×3−2 = 4 en die naar y is 3. De volgende code vergelijkt de eerste afgeleide met twee nieuwe voorwaartse berekeningen.
def bouw(q):
a = keer(q['x'], q['x'])
b = keer(q['x'], q['y'])
return optel(a, b)
waarden = dict(x=3., y=-2.)
bladen = {k:knoop(v)
for k,v in waarden.items()}
terug(bouw(bladen))
epsilon = 1e-5
plus, minus = dict(waarden), dict(waarden)
plus['x'] += epsilon
minus['x'] -= epsilon
hoog = bouw({k:knoop(v)
for k,v in plus.items()})
laag = bouw({k:knoop(v)
for k,v in minus.items()})
numeriek = (hoog['value']-laag['value'])
numeriek /= 2*epsilon
print(bladen['x']['grad']) # 4.0
print(round(numeriek, 6)) # 4.0
Maak controleer_gradient(build,values,epsilon=1e-5). build ontvangt benoemde bladknopen en geeft een eindknoop; values bevat hun oorspronkelijke getallen. Herhaal de numerieke meting voor iedere naam en geef per blad een dictionary met analytisch, numeriek en fout, de absolute afstand tussen beide.
Een controle hoort ook een echte fout te melden. Bewaart een kwadraatknoop bij x = 3 ten onrechte lokale factor nul, dan vindt autograd nul terwijl het centrale verschil ongeveer 6 blijft. Een rapport dat altijd fout=0 invult, zou juist die fout verbergen.
q = knoop(3)
fout = knoop(q['value']**2, ((q,0.),))
terug(fout)
print(q['grad']) # 0.0, hoort 6 te zijn
03Wat neem je mee?
- Begin schoon en behandel ook alle tussenknopen.
- Loop achteruit, vermenigvuldig lokale factoren en tel iedere route op.
- Vergelijk elk benoemd blad met nieuwe plus/minus-grafen en rapporteer echte afwijkingen.
Hierna train je met deze eigen engine een netwerk voor XOR, zonder de gradiënten per laag zelf uit te schrijven.