- Je telt gradiënten van alle posities op voor gedeelde filtergewichten.
- Je leidt de terugweg naar pixels af en controleert die onafhankelijk met numerieke verschillen.
Een filter is pas trainbaar wanneer je weet hoe zijn gewichten het verlies beïnvloeden. Hetzelfde gewicht zit in veel vensters en ontvangt dus meerdere bijdragen. De terugweg verzamelt die bijdragen, net zoals bij gedeelde knopen in autograd.
We gebruiken één beeld, één filter, stride één en geen padding. De voorwaartse corr2d staat al in de starter. Dy is een inkomend foutsignaal met dezelfde vorm als de featurekaart; het hoeft niet uit enen te bestaan.
1. Verzamel iedere bijdrage aan hetzelfde gewicht
Neem x met rijen [1,2,3],[4,5,6],[7,8,9], filter [[1,2],[3,4]] en dy=[[1,−1],[2,0]]. De uitvoerscores zijn [[37,47],[67,77]]. Met dit dy krijgt bijvoorbeeld de derde score dubbel gewicht en de vierde geen invloed op het scalaire verlies.
Voor het filtergewicht linksboven zijn de gebruikte pixels 1,2,4,5. Hun gewogen bijdragen zijn 1×1−1×2+2×4+0×5=7. Het gewicht is gedeeld: we tellen alle vier routes op in plaats van het laatste venster te bewaren.
X bevat pixels, K filtergewichten, r/c een relatieve filterpositie en i/j een uitvoerpositie. dK heet in de code dw; dy bevat de afgeleide van het uiteindelijke verlies naar iedere score. Voor de bias is de lokale afgeleide altijd één, dus db=1−1+2+0=2.
import numpy as np
def filter_backward(x,k_shape,dy):
x = np.asarray(x,float)
dy = np.asarray(dy,float)
dw = np.zeros(k_shape,dtype=float)
kh,kw = k_shape
for i,j in np.ndindex(dy.shape):
dw += dy[i,j]*x[i:i+kh,j:j+kw]
return dw,float(dy.sum())
x = np.arange(1,10).reshape(3,3)
k = np.array([[1,2],[3,4]])
dy = np.array([[1.,-1.],[2.,0.]])
dw,db = filter_backward(x,k.shape,dy)
print(dw.tolist(),db)
# [[7.,9.],[13.,15.]] 2.0
Maak deze functie voor NumPy-arrays x en dy, met k_shape als vormtuple. Zet vóór vermenigvuldigen en optellen om naar float, zodat integerpixels of -gradiënten niet tussentijds overlopen. Laat de invoer intact en geef een tuple (dw,db) terug; deel niet nogmaals door het aantal vensters, want eventuele middeling zit al in dy.
012. Een pixel ontvangt foutsignalen uit meerdere vensters
De centrale pixel 5 staat in alle vier vensters. Zij ontvangt 1×4+(−1)×3+2×2+0×1=5: telkens het filtergewicht op haar lokale positie maal het bijbehorende dy. Andere pixels raken minder vensters.
Start dx als nulmatrix met de oorspronkelijke inputvorm. Leg voor iedere uitvoerpositie het filter terug op haar venster en voeg de gewogen bijdrage toe. Wij gebruiken cross-correlatie; bij deze expliciete vensterlus draaien we de kernel niet om.
De pijl betekent hier een optellende update van de accumulator dx, geen wijziging van de oorspronkelijke pixels. Het hele voorbeeld geeft onderstaande inputgradiënt; de som op het midden is 5.
def input_backward(x_shape,k,dy):
k = np.asarray(k,float)
dy = np.asarray(dy,float)
dx = np.zeros(x_shape,dtype=float)
kh,kw = k.shape
for i,j in np.ndindex(dy.shape):
dx[i:i+kh,j:j+kw] += dy[i,j]*k
return dx
dx = input_backward(x.shape,k,dy)
print(dx.tolist())
# [[1.,1.,-2.],[5.,5.,-4.],[6.,8.,0.]]
Maak input_backward zonder k of dy te veranderen. Overschrijven met = verliest overlappende routes, terwijl een extra middeling alle gradiënten te klein maakt. Nieuwe dx/dw-arrays mogen ook fractionele waarden bevatten wanneer de oorspronkelijke pixels en kernel integers waren.
3. Bewijs je terugweg met een onafhankelijke meetlat
Combineer beide functies in conv_backward(x,k,dy), met een tuple (dx,dw,db) als resultaat. Er hoort nog geen optimizerupdate in deze functie. Alle gradiënten gaan over dezelfde oude invoer en gewichten.
def conv_backward(x,k,dy):
dw,db = filter_backward(x,k.shape,dy)
dx = input_backward(x.shape,k,dy)
return dx,dw,db
dx,dw,db = conv_backward(x,k,dy)
print(dx[1,1],dw[0,0],db) # 5.0 7.0 2.0
Neem als controleverlies de som van iedere voorwaartse score maal zijn vastgelegde dy. Verhoog één gewicht een beetje, verlaag het evenveel en meet beide verliezen opnieuw. Deel het verschil door tweemaal de verstoring: dit centrale verschil moet de analytische gradiënt benaderen.
L is het scalaire controleverlies en epsilon een kleine positieve stap, hier 0.00001. Voor het eerste filtergewicht verandert L met ongeveer zevenmaal die stap.
def loss(kernel):
weighted = corr2d(x,kernel)*dy
return float(weighted.sum())
plus,minus = k.astype(float),k.astype(float)
plus[0,0] += 1e-5
minus[0,0] -= 1e-5
numeric = (loss(plus)-loss(minus))/2e-5
print(round(numeric,6)) # 7.0
De nakijker bevriest eerst x,k,dy en rekent met een eigen voorwaartse referentie. Zo kan een foutieve leerling-corr2d de meetlat niet mee veranderen. Controleer zelf elke pixel, ieder filtergewicht en de bias; één juist gewicht bewijst de rest nog niet.
03Wat neem je mee?
- Ieder gedeeld gewicht verzamelt bijdragen van alle outputposities.
- Iedere pixel telt de routes uit alle overlappende vensters op.
- Bewaar invoer en foutsignaal voor een onafhankelijke centrale-verschilcontrole.
Hierna train je met deze bouwstenen een echt convolutienetwerk op cijferbeelden.