- Je bewaart bij maxpooling de gekozen positie en gebruikt die in de terugweg.
- Je berekent hoe het bereik in oorspronkelijke pixels groeit door meerdere lagen.
Een featurekaart vertelt waar een patroon sterk aanwezig is. Pooling vat buurten samen zonder trainbare gewichten: maxpooling bewaart het maximum, gemiddelde pooling gebruikt alle waarden. Met grotere stappen wordt de kaart kleiner, maar daarbij gaat informatie verloren.
Je gaat niet alleen de maxima vinden. Je bewaart ook waar ze stonden, zodat de terugweg het foutsignaal naar de juiste oorspronkelijke pixel kan sturen. Bij overlappende vensters kunnen meerdere routes bij dezelfde pixel uitkomen.
1. Bewaar de waarde én haar oorspronkelijke positie
Bekijk eerst de visualisatie met middenpixel 9 en stride 1. Alle vier vensters kiezen het midden; de totale middengradiënt is daarom 4. Zet de middenwaarde op 2 en bekijk hoe de keuze bij gelijke maxima verandert.
2×2-maxpooling. Iedere output ontvangt dy=1. Bij een gelijk maximum wint de eerste positie in rijvolgorde; de gekozen inputpixel is gemarkeerd.
| 1 | 2 | 1 |
| 2 | 9 | 2 |
| 1 | 2 | 1 |
| 9 | 9 |
| 9 | 9 |
| 0 | 0 | 0 |
| 0 | 4 | 0 |
| 0 | 0 | 0 |
Gekozen venster: start (0,0). Maximum op (1,1). Vier overlappende vensters kunnen dezelfde pixel kiezen.
Neem als eerste los venster [[1,3],[4,2]]. Het maximum is 4 op lokale positie (1,0), met nulgebaseerde indices. Staat het venster twee kolommen verder in het beeld, dan is dezelfde lokale positie absoluut (1,2); die absolute rij en kolom bewaren we.
np.argmax geeft de positie in een afgevlakte rijvolgorde. Voor dit venster is dat index 2: de derde waarde. np.unravel_index zet die index terug naar (1,0), waarna je de vensterstart optelt.
import numpy as np
def maxpool(x,size=2,stride=2):
x = np.asarray(x,float)
oh = (x.shape[0]-size)//stride+1
ow = (x.shape[1]-size)//stride+1
out = np.empty((oh,ow))
indices = np.empty((oh,ow,2),int)
for i,j in np.ndindex(out.shape):
r,c = i*stride,j*stride
patch = x[r:r+size,c:c+size]
pick = int(np.argmax(patch))
dr,dc = np.unravel_index(
pick,patch.shape)
indices[i,j] = [r+dr,c+dc]
out[i,j] = patch[dr,dc]
return out,indices
x = np.array([[1,3,2,0],[4,2,5,1],
[0,6,1,2],[3,1,2,7]])
out,indices = maxpool(x)
print(out.tolist()) # [[4.,5.],[6.,7.]]
print(indices[0,1].tolist()) # [1,2]
Maak maxpool voor één 2D-kaart zonder padding, met positieve gehele size en stride waarbij het venster past. Retourneer een tuple met uitvoer en een indexarray met vorm (Hout,Wout,2), zonder x te veranderen. Een laatste onvolledig venster telt niet mee.
Bij gelijke maxima kiezen we expliciet de eerste positie in rijvolgorde. Max is op zo'n tie niet differentieerbaar; de opgeslagen keuze legt onze terugconventie vast. Ook negatieve vensters blijven geldig: het maximum van vier negatieve waarden is geen nul.
out,idx = maxpool([[2,2],[1,0]])
print(out.item(),idx[0,0].tolist())
# 2.0 [0,0]
out,idx = maxpool([[-3,-2],[-5,-4]])
print(out.item(),idx[0,0].tolist())
# -2.0 [0,1]
012. Tel alle routes naar een winnende pixel op
Voor de gekozen positie is de lokale afgeleide één; de overige posities ontvangen nul. De binnenkomende gradiënt dy kan positief, negatief of fractioneel zijn. Er hoort geen extra deling door het aantal vensters bij: eventuele verliesmiddeling zit al in dy.
Neem een 3×3-beeld met middenwaarde 9 en verder de kleine waarden uit de visualisatie. Bij stride 1 wint het midden in alle vier vensters. Met dy=[[1,−1],[2,0.5]] ontvangt die ene pixel 1−1+2+0.5=2.5.
def pool_backward(dy,indices,input_shape):
dx = np.zeros(input_shape,dtype=float)
for i,j in np.ndindex(dy.shape):
r,c = indices[i,j]
dx[r,c] += dy[i,j]
return dx
x = np.array([[1,2,1],
[2,9,2],
[1,2,1]])
out,indices = maxpool(x,stride=1)
dy = np.array([[1.,-1.],[2.,.5]])
dx = pool_backward(dy,indices,x.shape)
print(dx.tolist())
# [[0.,0.,0.],[0.,2.5,0.],[0.,0.,0.]]
Maak deze terugfunctie met de bewaarde absolute indices en de oorspronkelijke inputvorm. Laat dy en indices intact. Met = in plaats van += zou het laatste venster de eerdere bijdragen overschrijven; je houdt dan hier 0.5 over in plaats van 2.5.
3. Volg het bereik in het oorspronkelijke beeld
Een latere output vat steeds meer oorspronkelijke pixels samen. Het receptive field r is de mogelijke geometrische spanwijdte; j is de afstand in oorspronkelijke pixels tussen buuroutputs. Begin bij r=1,j=1.
Voor een laag met kernelmaat k en stride s groeit het bereik met (k−1)×j. Gebruik daarvoor de oude j; vergroot daarna pas de buurafstand met stride. We nemen dilatie één en binnenposities zonder randeffecten.
Een convolutie (k=3,s=1), pooling (2,2) en convolutie (3,1) geven onderstaande stappen. De laatste groei is 2×2=4, omdat buuroutputs inmiddels twee oorspronkelijke pixels uit elkaar liggen.
| Na laag | Bereik r | Afstand j |
|---|---|---|
| Begin | 1 | 1 |
| Conv 3, s1 | 3 | 1 |
| Pool 2, s2 | 4 | 2 |
| Conv 3, s1 | 8 | 2 |
def receptive_field(layers):
r,j = 1,1
for k,s in layers:
r += (k-1)*j
j *= s
return r,j
print(receptive_field(
[(3,1),(2,2),(3,1)])) # (8,2)
print(receptive_field([])) # (1,1)
Maak deze functie voor een lijst (kernel,stride) met positieve gehele waarden en geef een tuple met bereik en afstand terug. De spanwijdte zegt welke pixels mogelijk meetellen. Bij één concrete maxpoolpass draagt alleen de gekozen route bij; een groot receptive field betekent niet dat alle pixels in die pass evenveel invloed hebben.
03Wat neem je mee?
- Bewaar bij ieder maximum een absolute index en een vaste keuze bij ties.
- Overlappende vensters tellen hun gradiënten bij dezelfde pixel op.
- Bereken nieuw bereik met de oude buurafstand, en vergroot daarna pas die afstand.
Hierna leer je niet alleen de winnende routes, maar ook de gedeelde filtergewichten via backprop.