15.2 Inputgradiens és átfedő recepciós mezők

15.2.1 Cél, előismeret és induló kérdés

Ebben az egységben a konvolúciós réteg bemenet szerinti gradiensét, a dXdX tenzort vezetjük le, és megvalósítását a scatter-add művelettel ellenőrizzük. A 15.1. Súly- és biasgradiens egységben a kernel- és biasgradiens képletét már levezettük; most az ellenkező irányú kérdést tesszük fel. Induló becslésként gondoljuk végig egy 3×33\times3-as kernel és stride 11 esetét: egy belső input pixel hány kimeneti elem kiszámításában vesz részt? A válasz tipikusan khkwk_h k_w darab, tehát a visszafelé terjedő gradiensből ugyanennyi hozzájárulást kell összegeznünk. Ha ezeket felülírjuk egymást, az inputgradiens hibás lesz.

Előismeret: a 14.2. Több bemeneti és kimeneti csatorna alfejezetben bevezetett többcsatornás forward definíció, a 15.1. Súly- és biasgradiens alfejezetben használt keresztkorrelációs indexelés, valamint a 3.2. Mátrixszorzás és affín réteg alfejezetből a mátrixszorzás inputgradiensének tranzponált szerkezete.

15.2.2 Definíció és intuíció

Legyen a bemenet X∈ℝN×Cin×H×WX\in\mathbb R^{N\times C_{in}\times H\times W} NCHW alakban, a kernel K∈ℝCout×Cin×kh×kwK\in\mathbb R^{C_{out}\times C_{in}\times k_h\times k_w}, a bias b∈ℝCoutb\in\mathbb R^{C_{out}}. A konvolúciós réteg kimenete Y=Conv2d(X;K,b)Y=\operatorname{Conv2d}(X;K,b), ahol a PyTorch Conv2d keresztkorrelációt számol [D10]. A veszteség YY szerinti gradiense legyen

D:=∇YL∈ℝN×Cout×Hout×Wout.D:=\nabla_Y L\in\mathbb R^{N\times C_{out}\times H_{out}\times W_{out}}.

A cél a

dX:=∇XL∈ℝN×Cin×H×WdX:=\nabla_X L\in\mathbb R^{N\times C_{in}\times H\times W}

tensor kiszámítása. A forwardban minden kimeneti elem a bemenet egy kh×kwk_h\times k_w-s patch-éből készül; a backwardban ezért minden D[n,o,i,j]D[n,o,i,j] elem a megfelelő patch minden pozíciójához hozzájárul a kernel súllyal szorozva. Mivel a szomszédos kimeneti elemek patch-ei átfednek, ugyanazon input pixel több hozzájárulást kap. A helyes művelet tehát scatter-add: a célkoordinátához tartozó gradienshez hozzáadunk, nem felülírunk.

A stride, padding és dilation szerepe a célkoordináta meghatározásában:

u=is+khd−p,v=js+kwd−p,u=i s+k_h d-p,\qquad v=j s+k_w d-p,

ahol i,ji,j a kimeneti térbeli index, u,vu,v a bemeneti térbeli index, ss a stride, pp a padding, dd a dilation. Csak a 0≤u<H0\le u<H és 0≤v<W0\le v<W tartományba eső pozíciók kapnak gradienst; a paddingelt virtuális pixelekhez tartozó hozzájárulások kimaradnak.

15.2.3 A nemnulla hozzájárulások indexfeltétele

A következő lemmában a teljes inputgradiens képletet mondjuk ki. A bizonyítás a forward definícióból és a láncszabályból áll, ezért a feltételeket expliciten megadjuk.

Lemma (a konvolúciós réteg inputgradiense). Legyen YY a 14.2. Több bemeneti és kimeneti csatorna szerinti keresztkorreláció N,Cin,Cout,H,W,kh,kw>0N,C_{in},C_{out},H,W,k_h,k_w>0 pozitív egészekkel, p≥0p\ge0 egész paddinggel, s≥1s\ge1 egész stride-dal és d≥1d\ge1 egész dilationnel úgy, hogy a kimeneti méretek

Hout=⌊H+2p−d(kh−1)−1s⌋+1,Wout=⌊W+2p−d(kw−1)−1s⌋+1H_{out}=\left\lfloor\frac{H+2p-d(k_h-1)-1}{s}\right\rfloor+1,\qquad W_{out}=\left\lfloor\frac{W+2p-d(k_w-1)-1}{s}\right\rfloor+1

pozitívak. Ekkor minden n∈{0,…,N−1}n\in\{0,\ldots,N-1\}, c∈{0,…,Cin−1}c\in\{0,\ldots,C_{in}-1\} és 0≤u<H,0≤v<W0\le u<H,\;0\le v<W esetén

dX[n,c,u,v]=∑o=0Cout−1∑kh=0kh−1∑kw=0kw−1∑i,j∈ℤu=is+khd−pv=js+kwd−p0≤i<Hout0≤j<WoutD[n,o,i,j]K[o,c,kh,kw]. dX[n,c,u,v]= \sum_{o=0}^{C_{out}-1} \sum_{k_h=0}^{k_h-1} \sum_{k_w=0}^{k_w-1} \sum_{\substack{i,j\in\mathbb Z\\ u=i s+k_h d-p\\ v=j s+k_w d-p\\ 0\le i<H_{out}\\ 0\le j<W_{out}}} D[n,o,i,j]\,K[o,c,k_h,k_w].

A belső összegzés pontosan azokra az (i,j)(i,j) párokra terjed ki, amelyekre az

i=u+p−khds,j=v+p−kwdsi=\frac{u+p-k_h d}{s},\qquad j=\frac{v+p-k_w d}{s}

értékek egészek és a kimeneti tartományba esnek. A dXdX többi eleme definíció szerint nulla; a paddingelt virtuális pixelekhez nincs dXdX-elem.

Bizonyítás. A forward definícióból, a paddingelt XpX_p tenzorral:

Y[n,o,i,j]=b[o]+∑c′=0Cin−1∑kh′=0kh−1∑kw′=0kw−1Xp[n,c′,is+kh′d−p,js+kw′d−p]K[o,c′,kh′,kw′]. Y[n,o,i,j]=b[o]+\sum_{c'=0}^{C_{in}-1}\sum_{k_h'=0}^{k_h-1}\sum_{k_w'=0}^{k_w-1} X_p[n,c',\,i s+k_h'd-p,\,j s+k_w'd-p]\,K[o,c',k_h',k_w'].

A láncszabály szerint

∂L∂X[n,c,u,v]=∑o=0Cout−1∑i=0Hout−1∑j=0Wout−1D[n,o,i,j]∂Y[n,o,i,j]∂X[n,c,u,v]. \frac{\partial L}{\partial X[n,c,u,v]} =\sum_{o=0}^{C_{out}-1}\sum_{i=0}^{H_{out}-1}\sum_{j=0}^{W_{out}-1} D[n,o,i,j]\frac{\partial Y[n,o,i,j]}{\partial X[n,c,u,v]}.

Mivel a bias XX-től független, csak a keresztkorrelációs tag deriváltja marad. A parciális derivált:

∂Xp[n,c′,is+kh′d−p,js+kw′d−p]∂X[n,c,u,v]={1,ha c′=c,u=is+kh′d−p,v=js+kw′d−p,0,különben, \frac{\partial X_p[n,c',\,i s+k_h'd-p,\,j s+k_w'd-p]}{\partial X[n,c,u,v]} = \begin{cases} 1, & \text{ha } c'=c,\ u=i s+k_h'd-p,\ v=j s+k_w'd-p,\\ 0, & \text{különben,} \end{cases}

feltéve, hogy 0≤u<H0\le u<H és 0≤v<W0\le v<W; ha a célkoordináta a paddingre mutat, a derivált nulla. Ezt behelyettesítve és a véges összegeket átrendezve a lemma képlete adódik. A véges összegek felcserélhetősége miatt az átrendezés indokolt. ▫\square

Következmény (scatter-add megvalósítás). A lemma nem az (u,v)(u,v) bemeneti indexekre szervezett összegzést adja, hanem megfordítva: minden D[n,o,i,j]D[n,o,i,j] elemhez és minden (kh,kw)(k_h,k_w) kernelpozícióhoz kiszámítjuk a célkoordinátát (u,v)(u,v); ha az érvényes bemeneti tartományba esik, akkor

dX[n,c,u,v]+=D[n,o,i,j]K[o,c,kh,kw]dX[n,c,u,v]\mathrel{+}=D[n,o,i,j]\,K[o,c,k_h,k_w]

minden cc-re. A += művelet az összeadás, nem a felülírás. Az átfedő patch-ek miatt ugyanazon (n,c,u,v)(n,c,u,v) cél több forrásból is kap hozzájárulást, ezért az accumulation a helyes szemantika.

A stride hatása az indexfeltételben látszik: ha u+p−khdu+p-k_h d nem osztható ss-sel, akkor az adott (u,kh)(u,k_h) párhoz nincs ii. Például p=0p=0, s=3s=3, d=1d=1, kh=2k_h=2 esetén a u≡2(mod3)u\equiv2\pmod3 pozíciók semmilyen kernelpozícióval sem érhetők el, így azok dXdX-e nulla marad, feltéve, hogy a kernel súlya nem nulla.

15.2.4 Kézi kis példa

Legyen N=1N=1, Cin=Cout=1C_{in}=C_{out}=1, H=W=4H=W=4, kh=kw=3k_h=k_w=3, p=1p=1, s=1s=1, d=1d=1. A kernel legyen diagonális:

K=(100020003). K=\begin{pmatrix} 1&0&0\\ 0&2&0\\ 0&0&3 \end{pmatrix}.

Az upstream gradients tenzor is diagonális:

D=(0000010000200003). D=\begin{pmatrix} 0&0&0&0\\ 0&1&0&0\\ 0&0&2&0\\ 0&0&0&3 \end{pmatrix}.

A lemma képletéből p=1p=1, s=1s=1, d=1d=1 miatt i=u+1−khi=u+1-k_h, j=v+1−kwj=v+1-k_w. A diagonális kernel kh=kwk_h=k_w esetén ad nemnulla hozzájárulást, így

dX[u,v]=1⋅D1[u+1,v+1]+2⋅D1[u,v]+3⋅D1[u−1,v−1],dX[u,v]=1\cdot D_1[u+1,v+1]+2\cdot D_1[u,v]+3\cdot D_1[u-1,v-1],

ahol D1D_1 a DD tenzor kiterjesztése nullákkal a tartományon kívül. A diagonális elemekre:

A többi elem nulla, mert a DD off-diagonális elemei nullák. Tehát

dX=(100004000010000012). dX=\begin{pmatrix} 1&0&0&0\\ 0&4&0&0\\ 0&0&10&0\\ 0&0&0&12 \end{pmatrix}.

Megfigyelhető, hogy a középső pixel több kimeneti gradiensből is részesül: például a (2,2)(2,2) pixel a D[3,3]D[3,3], D[2,2]D[2,2] és D[1,1]D[1,1] elemekből kap hozzájárulást a kernel megfelelő súlyával.

15.2.5 NumPy scatter-add és PyTorch autograd összehasonlítás

Az alábbi labor a lemmát több stride/padding/dilation konfiguráció mellett ellenőrzi. A referenciakód a scatter-add-öt a fenti következmény szerint valósítja meg; a PyTorch oldalon az autograd számítja a dXdX-et. A kimenet az eltérések maximuma.

# lab/n058_dx_check.py
import numpy as np
import torch
import torch.nn.functional as F

rng = np.random.default_rng(20261020)

def conv2d_output_shape(H, W, kh, kw, stride, padding, dilation):
    Hout = (H + 2 * padding - dilation * (kh - 1) - 1) // stride + 1
    Wout = (W + 2 * padding - dilation * (kw - 1) - 1) // stride + 1
    return Hout, Wout

def conv2d_input_grad_numpy(D, K, H, W, stride, padding, dilation):
    # D: (N, Cout, Hout, Wout), K: (Cout, Cin, kh, kw)
    N, Cout, Hout, Wout = D.shape
    Cout2, Cin, kh, kw = K.shape
    assert Cout2 == Cout
    dX = np.zeros((N, Cin, H, W), dtype=D.dtype)
    for n in range(N):
        for o in range(Cout):
            for i in range(Hout):
                for j in range(Wout):
                    du = D[n, o, i, j]
                    for c in range(Cin):
                        for a in range(kh):
                            for b in range(kw):
                                u = i * stride + a * dilation - padding
                                v = j * stride + b * dilation - padding
                                if 0 <= u < H and 0 <= v < W:
                                    dX[n, c, u, v] += du * K[o, c, a, b]
    return dX

N, Cin, Cout, H, W = 2, 3, 4, 6, 7
kh, kw = 3, 3

for stride in (1, 2):
    for padding in (0, 1):
        for dilation in (1, 2):
            Hout, Wout = conv2d_output_shape(H, W, kh, kw, stride, padding, dilation)
            if Hout <= 0 or Wout <= 0:
                continue
            X_np = rng.standard_normal((N, Cin, H, W)).astype(np.float64)
            K_np = rng.standard_normal((Cout, Cin, kh, kw)).astype(np.float64)
            b_np = rng.standard_normal(Cout).astype(np.float64)
            D_np = rng.standard_normal((N, Cout, Hout, Wout)).astype(np.float64)

            dX_np = conv2d_input_grad_numpy(D_np, K_np, H, W, stride, padding, dilation)

            X_t = torch.tensor(X_np, dtype=torch.float64, requires_grad=True)
            K_t = torch.tensor(K_np, dtype=torch.float64, requires_grad=True)
            b_t = torch.tensor(b_np, dtype=torch.float64, requires_grad=True)
            Y_t = F.conv2d(X_t, K_t, b_t, stride=stride, padding=padding, dilation=dilation)
            D_t = torch.tensor(D_np, dtype=torch.float64)
            loss = (Y_t * D_t).sum()
            loss.backward()
            dX_t = X_t.grad

            diff = np.max(np.abs(dX_np - dX_t.detach().numpy()))
            print(f"s={stride} p={padding} d={dilation} "
                  f"Hout,Wout={Hout},{Wout} max_abs_diff={diff:.3e}")

A futtatás parancsa a lab munkakönyvtárból:

python n058_dx_check.py

A kimenet számított elvárás, nem a kéziratban elvégzett futás: minden konfigurációra a max_abs_diff értékének 10−1210^{-12} alatt kell lennie f64 CPU aritmetikával. A loss = (Y_t * D_t).sum() használata a 15.1. Súly- és biasgradiens alfejezetben bevezetett ellenőrző konvenció: a skalár loss gradiense a megadott upstream tenzor, ezért nincs szükség tényleges veszteségfüggvényre.

15.2.6 Feladatok

F1 (fogalmi, nehézség 1). Magyarázza el, miért nem elegendő a dXdX kiszámításakor az értékadás (=), és miért kell scatter-addöt (+=) használni. Adjon egy konkrét 1D ellenpéldát: H=4H=4, k=3k=3, p=1p=1, s=d=1s=d=1, K=[1,1,1]K=[1,1,1], D=[1,2,3,4]D=[1,2,3,4]. Számítsa ki a helyes dXdX második elemét, és a hibás felülíró implementáció által adott értéket, ha az utolsó kernelpozíció ír utoljára. Átadandó: szöveges indoklás és a két számérték.

F2 (levezetési, nehézség 2). Tekintse a H=5H=5, W=1W=1, kh=3k_h=3, p=1p=1, s=2s=2, d=2d=2 esetet. Határozza meg a lemmából, mely uu bemeneti magasságindexekre lehet dX[n,c,u,0]dX[n,c,u,0] nemnulla, és minden ilyen uu-ra adja meg az összes hozzájáruló (i,kh)(i,k_h) párt. Átadandó: a HoutH_{out} értéke, a lehetséges uu-k halmaza, és a hozzájárulás-táblázat.

F3 (implementációs, nehézség 1). Írjon 1D NumPy referenciát a dXdX kiszámítására a lemmából, majd ellenőrizze PyTorch F.conv1d autograddal a H=5H=5, Cin=2C_{in}=2, Cout=2C_{out}=2, k=3k=3, s=2s=2, p=1p=1, d=1d=1, N=1N=1 konfigurációra. Átadandó: teljes kód, seed, dtype, parancs, és a kapott max_abs_diff.

F4 (hibakeresési, nehézség 2). Az alábbi kódrészlet a scatter-add után a padding levágását próbálja elvégezni:

# HIBA: p=0 esetén a 0:-0 szelet üres.
dX = full[:, :, padding:-padding, padding:-padding]

Lokalizálja a hibát p=0p=0 esetén: miért üres a szelet, mi a következménye a dXdX alakjára és a numerikus összehasonlításra? Javítsa a kódot, és adja meg az ellenőrző teszt várt kimenetét. Átadandó: hiba magyarázata, javított kód, és a javítás utáni shape-ellenőrzés.

F5 (kísérleti/transzfer, nehézség 3). Tervezzen kísérletet annak mérésére, hogy a Python ciklusos scatter-add implementáció futásideje és memóriaigénye hogyan skálázódik a batchmérettel (N∈{1,4,16,64}N\in\{1,4,16,64\}) kis CPU-n, rögzített H=W=8H=W=8, Cin=Cout=2C_{in}=C_{out}=2, k=3k=3, s=1s=1, p=1p=1 mellett. Nem kell ténylegesen futtatnia. Átadandó: hipotézis, mért mennyiségek, mérési protokoll, baseline, és az érvényességi korlát.

15.2.7 Megoldások és indoklás

F1 megoldás. A forwardban az u=1u=1 bemeneti pixel három kimeneti elemhez járul hozzá: Y[0]Y[0]-hoz a k=2k=2 kernelpozícióval, Y[1]Y[1]-hez a k=1k=1 pozícióval, és Y[2]Y[2]-höz a k=0k=0 pozícióval. A megfelelő gradiens-hozzájárulások:

D[0]⋅K[2]=1⋅1=1,D[1]⋅K[1]=2⋅1=2,D[2]⋅K[0]=3⋅1=3.D[0]\cdot K[2]=1\cdot1=1,\quad D[1]\cdot K[1]=2\cdot1=2,\quad D[2]\cdot K[0]=3\cdot1=3.

A helyes dX[1]=1+2+3=6dX[1]=1+2+3=6. A felülíró implementáció, ha a kernelpozíciók ciklusában az utolsó, k=0k=0 pár ír a célba, dX[1]=3dX[1]=3-at eredményez, tehát a három hozzájárulásból kettőt elveszít. Az átfedő patch-ek miatt a gradiens-hozzájárulásokat mindig összegezni kell.

F2 megoldás. A kimeneti méret:

Hout=⌊5+2−2⋅2−12⌋+1=⌊22⌋+1=2.H_{out}=\left\lfloor\frac{5+2-2\cdot2-1}{2}\right\rfloor+1=\left\lfloor\frac{2}{2}\right\rfloor+1=2.

A lehetséges u=i⋅2+kh⋅2−1u=i\cdot2+k_h\cdot2-1 értékek i∈{0,1}i\in\{0,1\}, kh∈{0,1,2}k_h\in\{0,1,2\} mellett:

ii khk_h uu érvényes?
0 0 −1-1 nem
0 1 11 igen
0 2 33 igen
1 0 11 igen
1 1 33 igen
1 2 55 nem

Tehát a lehetséges uu-k halmaza {1,3}\{1,3\}. Az u=1u=1 ponthoz az (i,kh)=(0,1)(i,k_h)=(0,1) és (1,0)(1,0) párok, az u=3u=3 ponthoz az (i,kh)=(0,2)(i,k_h)=(0,2) és (1,1)(1,1) párok járulnak hozzá. A 0,2,40,2,4 magasságindexekre a feltétel nem teljesíthető, ezért azok dXdX-e nulla.

F3 megoldás. Az 1D referencia és az ellenőrző kód:

# lab/n058_f3_1d.py
import numpy as np
import torch
import torch.nn.functional as F

rng = np.random.default_rng(20261020)

def conv1d_input_grad_numpy(D, K, H, stride, padding, dilation):
    N, Cout, Hout = D.shape
    Cout2, Cin, kh = K.shape
    assert Cout2 == Cout
    dX = np.zeros((N, Cin, H), dtype=D.dtype)
    for n in range(N):
        for o in range(Cout):
            for i in range(Hout):
                for c in range(Cin):
                    for a in range(kh):
                        u = i * stride + a * dilation - padding
                        if 0 <= u < H:
                            dX[n, c, u] += D[n, o, i] * K[o, c, a]
    return dX

N, Cin, Cout, H = 1, 2, 2, 5
kh, stride, padding, dilation = 3, 2, 1, 1
Hout = (H + 2 * padding - dilation * (kh - 1) - 1) // stride + 1

X_np = rng.standard_normal((N, Cin, H)).astype(np.float64)
K_np = rng.standard_normal((Cout, Cin, kh)).astype(np.float64)
b_np = rng.standard_normal(Cout).astype(np.float64)
D_np = rng.standard_normal((N, Cout, Hout)).astype(np.float64)

dX_np = conv1d_input_grad_numpy(D_np, K_np, H, stride, padding, dilation)

X_t = torch.tensor(X_np, dtype=torch.float64, requires_grad=True)
K_t = torch.tensor(K_np, dtype=torch.float64, requires_grad=True)
b_t = torch.tensor(b_np, dtype=torch.float64, requires_grad=True)
Y_t = F.conv1d(X_t, K_t, b_t, stride=stride, padding=padding, dilation=dilation)
D_t = torch.tensor(D_np, dtype=torch.float64)
loss = (Y_t * D_t).sum()
loss.backward()

diff = np.max(np.abs(dX_np - X_t.grad.detach().numpy()))
print(f"Hout={Hout} max_abs_diff={diff:.3e}")

Futtatás: python n058_f3_1d.py. Várható elvárás f64 CPU-n: max_abs_diff 10−1210^{-12} alatt; a tényleges érték platformfüggő.

F4 megoldás. A hiba oka: Pythonban a : -0 szelet alsó határa -0, ami 0-val egyenlő. A padding:-padding ezért 0:0-t jelent, ami üres. A következmény: p=0p=0 esetén a dXdX térbeli dimenziói 00 méretűek lesznek, miközben a várt alak (N,Cin,H,W)(N,C_{in},H,W), tehát a PyTorch autograddal való összehasonlítás shape-hibát vagy hamis egyezést adhat. Javítás: padding nélkül nincs mit levágni, mert a scatter-add eleve csak a valós tartományba ír:

if padding > 0:
    dX = full[:, :, padding:-padding, padding:-padding]
else:
    dX = full

Ellenőrzés: assert dX.shape == (N, Cin, H, W), majd a max_abs_diff kiszámítása. A javítás után a p=0p=0 esetben a teljes dXdX shape megmarad, és a numerikus eltérés a f64 tűréshatár alá kerül.

F5 megoldás. Hipotézis: a ciklusos referencia futásideje közelítőleg lineárisan nő NN-nel, mert a batch iterációja független ismétlés; a csúcsmemória szintén lineárisan nő a dXdX és DD tenzorok miatt. Mért mennyiségek: medián futásidő három ismétlésből time.perf_counter-rel, csúcsmemória tracemalloc vagy resource modullal. Protokoll: rögzített seed, f64 dtype, minden NN-re azonos X,K,b,DX,K,b,D generálás; minden mérés előtt egy warm-up hívás; az iterációk sorrendjét véletlenszerűen keverve, hogy az operációs rendszer gyorsítótárazása ne torzítson. Baseline: az üres ciklushurok futásideje azonos NN-re, vagy a PyTorch backward ideje ugyanazon konfigurációra. Érvényességi korlát: az eredmény csak a jelen NumPy referencia implementációra és a helyi CPU-ra vonatkozik; nem vihető át GPU-ra vagy a PyTorch kernelre, és a kis batchméret tartományán kívül a Python interpreter és memória-allokátor viselkedése megváltozhat.

15.2.8 Tipikus tévedés ellenpéldával

A leggyakoribb hiba a scatter-add összeadásának felülírásra cserélése. Az F1 példában láttuk: a helyes dX[1]=6dX[1]=6 helyett a felülíró kód 33-at ad, ha az utolsó kernelpozíció ír utoljára. A hiba oka az, hogy a forwardban egy input pixel több kimeneti elemhez tartozik; a láncszabály ezeknek a részhozzájárulásoknak az összegét írja elő. A felülírás tönkreteszi a weight sharing dualitását: ahogy a 15.1. Súly- és biasgradiens alfejezetben a kernelgradiensnél a felhasználási helyek összegzése volt a kulcs, itt ugyanaz az összegzés a bemeneti irányban jelenik meg.

15.2.9 Önellenőrzés és visszahívás

Ellenőrizze: a dXdX alakja mindig megegyezik az XX alakjával, függetlenül a stride-tól és paddingtől. A p=0p=0, s=3s=3, d=1d=1, kh=2k_h=2 példában a u≡2(mod3)u\equiv2\pmod3 bemeneti pozíciók dXdX-e minden kernel és DD mellett nulla, mert az indexfeltétel nem oldható meg. A bias gradiensre a dXdX-ből nincs szükség: dbdb mindenképpen a DD batch- és térbeli összege a 15.1. Súly- és biasgradiens szerint.

Visszahívás: a 3.2. Mátrixszorzás és affín réteg alfejezetben az affin réteg inputgradiense W⊤dZW^\top dZ volt; a mostani levezetés ugyanezt a tranzponált operátort valósítja meg keresztkorrelációs formában. A 14.2. Több bemeneti és kimeneti csatorna alfejezetben a forward csatornaösszeg volt a kimeneti csatornák felépítésének kulcsa; a dXdX képletében a oo szerinti összegzés ennek a csatornastruktúrának a megfordítása. A 11.2. nn.Module és paraméterregisztráció alfejezetben bevezetett modul-nyilvántartás itt abban érhető tetten, hogy a PyTorch conv2d autograd számítása a regisztrált Parameter és a bemeneti Tensor gráfján automatikusan elvégzi a fenti accumulationt.

15.2.10 Rövid összefoglaló

A konvolúciós réteg inputgradiense az upstream gradiens és a kernel súlyának szorzatából álló scatter-add. A lemmában levezettük a teljes NCHW képletet, amely minden lehetséges kimeneti pozícióból összegzi a hozzájárulásokat a u=is+khd−pu=is+k_h d-p, v=js+kwd−pv=js+k_w d-p célkoordinátákra. A stride oszthatósági feltételt szab, a padding a virtuális pixelek hozzájárulásait zárja ki, a dilation a kernel effektív kiterjedését növeli. Az átfedő patch-ek miatt a hozzájárulásokat összeadni kell; a felülírás hibás gradienst ad. A NumPy-referenciát PyTorch autograddal több stride/padding/dilation konfiguráció mellett hasonlítottuk össze f64 tűréssel.

15.2.11 Következő alkalmazás

A 15.3. Transposed convolution mint adjungált egységben a scatter-add operátort formális lineáris leképezésként vizsgáljuk: a forward konvolúció adjungáltja a transzponált konvolúció. A mostani indexfeltételből közvetlenül leolvasható lesz a ConvTranspose2d bemeneti–kimeneti kapcsolata, és az, hogy miért nem általános inverz.

15.2.12 Források