A projekt eddigi egységei külön-külön igazolták a saját konvolúciós réteget: a 37.1. Operátorspecifikáció és kézi referencia a forward és a backward referenciakódját, a 37.2. Backward és ellenőrzési lefedettség a független teszteket, a 37.3. Linearitás, adjungált és határfeltétel pedig az operátor lineáris, adjungált és határfeltételi tulajdonságait. Ez az egység a záró kérdést vizsgálja: használható-e a réteg nemcsak tenzorműveletként, hanem egy tényleges, gradiensalapú tanulómodell építőköveként?
Előismeret: a 37.3. Linearitás, adjungált és határfeltétel operátorállításai; a 10.4. Tanítási lépés és tanuló MLP tanítási lépése; a 11.4. Egy teljes PyTorch tanítási iteráció PyTorch-modul és a 12.3. Sanity check és kis mintára túlillesztés kis mintára túlillesztés mint sanity check; a 36.4. IX. ellenőrző pont: lektorálható modellállítás lektorálható csomag hat komponense. A 33.4. Üzemeltetés, drift és késő címke alfejezetből annyit használunk, hogy a teljesítményt csak a rendelkezésre álló mintához és feladathoz kötjük, a 25.4. Nyílt halmaz és ismeretlen input alfejezetből pedig azt, hogy a betanított modell csak a zárt feladat tartományán értelmezhető.
Induló kérdés. Ha egy réteg forward és backward függvénye külön ellenőrzött, akkor automatikusan helyesen tanul egy modell belőle? Becslés: nem. A rétegszintű ellenőrzés ugyan szükséges, de a tanítás közben új hibák léphetnek fel: a paraméterregisztráció, az optimalizálóba kerülés, a loss csatlakoztatása és a több lépéses gradiensáramlás együttműködése külön is elromolhat. Ezt az egységet arra szánjuk, hogy ezt a rétegszintű ellenőrzés és a modellszintű tanulás közötti hidat ellenőrzötten megépítsük.
A saját rétegünk a projekt korábbi specifikációját követi:
és ugyanígy, szélességgel, szimmetrikus zero padding, stride, dilation. A réteg cross-correlationt számol, kernelmegfordítás nélkül.
A projektben eddig a réteget tiszta függvényként kezeltük:
def conv2d_forward(x, weight, bias, stride=1, padding=0, dilation=1):
...
def conv2d_backward_input(dy, x, weight, stride=1, padding=0, dilation=1):
...
def conv2d_backward_weight(dx, dy, x, stride=1, padding=0, dilation=1):
...Ahhoz, hogy a PyTorch optimalizálója ezt a réteget tanítsa, a három
függvényt egy torch.nn.Module-ba kell integrálni, és a
weight-et, bias-t
nn.Parameter-ként kell regisztrálni. Ebből következik az
integrációs szerződés:
Definíció (integrációs szerződés). A saját réteg akkor integrált egy PyTorch-modellben, ha
weight és bias tenzor
nn.Parameter, tehát az
optimizer.param_groups() listában szerepel;forward csak differenciálható tenzorműveleteket
használ az inputon és a paramétereken, és a backward által szolgáltatott
gradiensek a PyTorch autograd-gráfján keresztül áramlanak;loss.backward() hívás
minden paraméterre requires_grad=True mellett nem üres
grad-et állít elő.Itt tudatosan a PyTorch nn.Module mechanizmusát
használjuk, mert a projekt numerikus főútja a 11.1.
Tensor és autograd-életciklus alfejezetben lefektetett
autograd-életciklusra épül.
A tanulási tesztet nem a GlyphLab 24×24-es képein kezdjük, mert egy ilyen kis kontroll két okból is félrevezető lehet: a háromosztályos orientációs feladatban a konvolúciós modell architektúrája és az augmentáció együtt hat, és a réteg hibája nem választható el a modellválasztástól. Ehelyett egy szintetikus, determinisztikus feladatot építünk, amelyben a tanulandó szűrőnek explicit célja van.
A tanulási feladat. A modell egyetlen konvolúciós rétegből áll, , stride 1, padding 1, dilation 1. A bemenet float32 tensor. A célkép az input mozgóátlaga egy 3×3-as átlagoló szűrővel, azaz
zero padding mellett. Ez azért jó teszt, mert:
A veszteség az MSE átlagos formája:
ahol
a modell kimenete. A redukció fontos: a PyTorch MSELoss
alapértelmezésben a teljes batch elemszámával oszt, ami itt
.
Ezt expliciten kiírjuk, mert a [D07]-szerződés szerint a loss skáláját
minden levezetésnek külön meg kell adnia.
A tanítás előtt a levezetési kötelezettséget teljesítjük: a saját
réteget és a PyTorch nn.Conv2d-t ugyanazzal a súllyal és
biasszal, ugyanazon a bemeneten futtatjuk le, és a kimeneteket
összehasonlítjuk.
Egyeztetési állítás. Legyen
,
,
rögzített,
,
,
.
A projekt saját conv2d_forward függvénye és a PyTorch
F.conv2d hívás ugyanazt a
kimenetet adja, ha a PyTorch-hívás stride=s,
padding=p, dilation=d, groups=1,
és mindkét oldal float32 számítást végez. Az állítás típusa: numerikus
egyenlőségre vonatkozó ellenőrzött teszt, nem analitikus tétel, mert a
lebegőpontos műveletek sorrendje implementációfüggő lehet.
A teszt pontos numerikus feltételeit a 37.2.
Backward és ellenőrzési lefedettség alfejezetben rögzítettük; itt a
modellen belüli elrendezésre koncentrálunk. A PyTorch hívást nem a
megszokott nn.Conv2d modulon, hanem közvetlenül az
F.conv2d funkcionális API-n keresztül végezzük, hogy
elkerüljük a modulparaméter-regisztráció esetleges különbségét. A saját
réteget a Conv2dReference osztályba csomagoljuk:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Conv2dReference(nn.Module):
"""A projekt saját, NCHW/OIHW, groups=1 konvolúciós referenciarétege."""
def __init__(self, in_channels, out_channels, kernel_size,
stride=1, padding=0, dilation=1, bias=True):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = (kernel_size, kernel_size) \
if isinstance(kernel_size, int) else tuple(kernel_size)
self.stride = stride
self.padding = padding
self.dilation = dilation
k_h, k_w = self.kernel_size
self.weight = nn.Parameter(
torch.empty(out_channels, in_channels, k_h, k_w,
dtype=torch.float32))
if bias:
self.bias = nn.Parameter(torch.empty(out_channels,
dtype=torch.float32))
else:
self.register_parameter("bias", None)
self.reset_parameters()
def reset_parameters(self):
# A kontrollhoz mindkét oldalon ugyanazt a seedet használjuk,
# ezért itt csak egy kis determinisztikus skálát adunk.
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
if self.bias is not None:
bound = 1.0 / (self.in_channels *
self.kernel_size[0] * self.kernel_size[1])**0.5
nn.init.uniform_(self.bias, -bound, bound)
def forward(self, x):
return conv2d_forward(
x, self.weight, self.bias,
stride=self.stride, padding=self.padding,
dilation=self.dilation)A conv2d_forward a projekt korábbi egységében megírt
függvény. Itt nem másoljuk be a teljes implementációt, mert a 37.1. Operátorspecifikáció és kézi referencia
alfejezetből származik; a modell definíciójában a függvényhívás a
lényeg.
Az egyeztetési teszt:
torch.manual_seed(20261020)
N, C_in, H, W = 2, 1, 8, 8
C_out, k = 1, 3
x = torch.rand(N, C_in, H, W, dtype=torch.float32)
ref = Conv2dReference(C_in, C_out, k, stride=1, padding=1)
torch.nn.init.constant_(ref.weight, 1.0 / 9.0)
torch.nn.init.constant_(ref.bias, 0.0)
weight_pt = ref.weight.detach().clone()
bias_pt = ref.bias.detach().clone()
y_ref = ref(x)
y_pt = F.conv2d(x, weight_pt, bias_pt, stride=1, padding=1)
max_abs = (y_ref - y_pt).abs().max().item()
print(f"shape: ref={tuple(y_ref.shape)}, pt={tuple(y_pt.shape)}")
print(f"max_abs_diff: {max_abs:.3e}")Számított szemléltetés, nem futtatott kimenet. A
képletből és a műveletek azonosságából következő várt eredmény:
shape: ref=(2, 1, 8, 8), pt=(2, 1, 8, 8),
max_abs_diff: 0.000e+00 vagy a float32 műveleti sorrendtől
függően
alatti érték. Ha az eltérés ezt jelentősen meghaladja, a hiba nem a
lebegőpontos kerekítésből, hanem a saját forward indexeléséből vagy
padding-szeletéből származik.
A rétegszintű gradiensellenőrzésen túl egy modellszintű, véges differenciás ellenőrzést is végzünk. Ennek célja nem a backward implementációjának újraellenőrzése — azt a 37.2. Backward és ellenőrzési lefedettség és 37.3. Linearitás, adjungált és határfeltétel megtette —, hanem annak igazolása, hogy a modulba integrált paraméterekre a PyTorch autograd által kiszámított gradiens megegyezik a független numerikus becsléssel.
A numerikus gradiens definíciója egy skalár kimenetű függvényre és a paraméterre:
ahol az -edik egységvektor a paramétertérben. A kiértékelés float64-ben történik a 10.3. Gradiensellenőrzés és numerikus hibamérce előírása szerint, noha a modell float32-t használ. A kód:
def finite_grad(model, x, target, loss_fn, eps=1e-4):
model.eval()
grads = []
for p in model.parameters():
g = torch.zeros_like(p, dtype=torch.float64)
flat = g.view(-1)
for i in range(flat.numel()):
with torch.no_grad():
orig = p.view(-1)[i].item()
p.view(-1)[i] = orig + eps
loss_plus = loss_fn(model(x), target).item()
with torch.no_grad():
p.view(-1)[i] = orig - eps
loss_minus = loss_fn(model(x), target).item()
with torch.no_grad():
p.view(-1)[i] = orig
flat[i] = (loss_plus - loss_minus) / (2 * eps)
grads.append(g.reshape(p.shape))
return gradsItt x és target float64-re konvertált,
rögzített tenzor, a target a függvény explicit argumentuma,
a model paramétereit pedig a teszt idejére float64-be
helyezzük át. Ez nem a tanítási beállítás, hanem a numerikus ellenőrzés
külön konfigurációja. A teszthez
,
,
,
,
beállítást használunk, mert a teljes paraméterezés 9 súly + 1 bias = 10
változó, ami egyenként kiértékelhető.
Miért nem futtatunk ReLU-t ebben a modellben? A véges differencia sima pontban pontos; a ReLU töréspontjában a 9.4. Aktivációk és gradiensáramlás szerint a könyvtár által választott egyoldali érték konvenció, és a középső differencia ott nem feltétlenül egyezik a backwarddal. A jelen modell lineáris, tehát ez a csapda nem áll fenn.
Várt eredmény a képlet alapján. Az átlagoló célfeladatban a veszteség kvadratikus, ezért a középső differencia analitikusan a pontos gradienshez tart, és -es mellett a relatív hiba alatt marad a minden komponensére. Ha a gradiens abszolút értéke nullához közeli, a relatív hibát stabil nevezővel, -gyel számoljuk.
A következő kód a tényleges kis tanítást futtatja. Az állítás típusa: empirikus állítás, ezért pontosan megadjuk az adatot, a splitet, a receptet és a mintavételi egységet.
20261020.Conv2dReference,
,
,
,
,
.MSELoss redukció
mean.import torch
import torch.nn as nn
import torch.optim as optim
torch.manual_seed(20261020)
device = torch.device("cpu")
dtype = torch.float32
N = 64
x = torch.rand(N, 1, 8, 8, dtype=dtype, device=device)
target = torch.zeros_like(x)
for u in (-1, 0, 1):
for v in (-1, 0, 1):
target += 1.0 / 9.0 * torch.nn.functional.pad(
x, (1, 1, 1, 1), mode="constant", value=0.0
)[:, :, 1+u:1+u+8, 1+v:1+v+8]
model = Conv2dReference(1, 1, 3, stride=1, padding=1).to(device)
optimizer = optim.SGD(model.parameters(), lr=0.5)
loss_fn = nn.MSELoss()
for epoch in range(20):
model.train()
perm = torch.randperm(N, device=device)
total_loss = 0.0
for start in range(0, N, 16):
idx = perm[start:start+16]
xb, yb = x[idx], target[idx]
optimizer.zero_grad()
out = model(xb)
loss = loss_fn(out, yb)
loss.backward()
optimizer.step()
total_loss += loss.item() * len(idx)
total_loss /= N
if epoch in (0, 1, 4, 9, 19):
with torch.no_grad():
w = model.weight.detach()
ew = (w - 1.0 / 9.0).abs().max().item()
print(f"epoch={epoch:2d} loss={total_loss:.6f} E_W={ew:.6f}")A megadott kimenet számított szemléltetés; érvényességét a kód futtatásával kell ellenőrizni. A konvex kvadratikus feladatban a veszteség várhatóan alá csökken, és teljesül. Ha a tényleges futtatás ettől eltér, az a réteg hibájára utal, nem a feladat nehézségére.
A tanulási referencia mivolta. A réteg nem termelési
sebességű: minden kimeneti pixel kiszámításához Python-ciklust használ.
A 34.1. Mért késleltetés, throughput és memória
alfejezetben bevezetett mérési követelmények szerint a sebességet csak
megjelölt hardveren, ismételt futással szabad közölni. Itt az állítás
tehát nem az, hogy a réteg gyors, hanem az, hogy a tanulási protokollban
a gradiens helyesen áramlik. A sebességre vonatkozó egyetlen állításunk
egy korlát: a Python-hivatkozás várhatóan nagyságrendekkel lassabb, mint
a natív F.conv2d, és ez a 37.1.
Operátorspecifikáció és kézi referencia alfejezetben elfogadott
tervezési döntés. Egy lassú, világos referenciakód nem hibás
csak attól, hogy nem termelési kernel. A hibásság kritériuma a
numerikus egyezés megsértése, nem a sebesség.
A 36.4. IX. ellenőrző pont: lektorálható modellállítás hat komponenséből itt a kódot, a levezetést és a futási jegyzéket adjuk át; a páros eredmény nem része ennek az egységnek, mert nincs két összehasonlítandó modell. A benchmark hatóköre három pontban rögzíthető:
Az átadandó minimális környezet- és futásjegyzék:
| Tétel | Érték |
|---|---|
| Könyvtár | projekt helyi lab/ könyvtára |
| Python | a projekt lock szerinti verziója |
| PyTorch | a 2.3. Izolált környezet, notebook és verzió alfejezetben rögzített verzió |
| Futtatás | python n148_integration_check.py |
| Seed | 20261020 |
| dtype/device | float32, CPU |
| Output | integration_check.json |
Az integration_check.json a lektor számára a nyers
számokat tartalmazza: a max abszolút rétegdiffet, a modellszintű
gradienshibát, a végső losszt és
-t.
Így a futtatás eredménye szétválasztható a szöveges értelmezéstől.
A tipikus tévedés az, hogy aki a rétegszintű teszteket lefuttatta, az
a modell tanulási sikerét automatikusan a réteg helyességének
tulajdonítja. Ellenpélda. Tegyük fel, hogy a réteghez
hozzáadunk egy helytelen bias-regisztrációt: a bias-t nem
nn.Parameter-ként, hanem sima attribútumként tároljuk. A
forward kimenete a rögzített bias miatt a rétegszintű teszten
tökéletesen egyezhet a PyTorch-csal. A tanítás során azonban a biasra
soha nem érkezik gradiens, a tanulási görbe a konvex feladat ellenére
magas szinten áll meg, és a hiba a réteg backwardjának tűnik, pedig az
integráció hibás. A helyes diagnózis: ellenőrizni kell, hogy minden
paraméter szerepel-e az optimizer.param_groups() listában,
és minden grad nem üres-e az első backward
után. A kód itt nem a sebesség miatt hibás, hanem a
paraméterregisztráció megsértése miatt.
Két gyors önellenőrzést végzünk el:
Visszahívás a 37.3. Linearitás, adjungált és határfeltétel alfejezetből: a réteg linearitása bias nélkül ; a modellszintű gradiensellenőrzés ezzel összhangban a kvadratikus veszteségen konzisztens eredményt ad. A 36.4. IX. ellenőrző pont: lektorálható modellállítás alfejezetből pedig azt használjuk, hogy a lektorálható csomagnak a feltételeket és a futáskörnyezetet is tartalmaznia kell, nem csak a végső számot.
F1 (fogalmi, nehézség 1). Magyarázza el, mi a különbség a rétegszintű backward-ellenőrzés és a modellszintű tanulási kontroll között. Adjon egy olyan hibát, amelyet az első nem feltétlenül fed fel, a második viszont igen.
F2 (levezetési, nehézség 2). Vezesse le a 3×3-as átlagoló szűrő célfüggvényére, hogy a modell által elérhető minimális MSE pontosan 0, és adja meg az optimális súlyt és biast. Igazolja, hogy egyetlen minimum létezik a térben.
F3 (implementációs, nehézség 2). Írjon teljes
Python-függvényt, amely a saját Conv2dReference réteggel 10
epochig tanítja az átlagoló feladatot, és a végén ellenőrzi, hogy
.
A függvény adja vissza a (loss, E_W) párt. Input: seed, N;
output: a két lebegőpontos érték.
F4 (hibakeresési, nehézség 2). A tanítási ciklusban a loss minden epochban nő a második epoch után, noha a feladat konvex. Soroljon fel két lehetséges okot, és mindkettőre adjon konkrét javítást a kód szintjén.
F5 (kísérlettervezési, nehézség 3). Tervezze meg azt a kísérletet, amellyel a saját réteg integrációját a GlyphLab 24×24-es képein, egy két konvolúciós rétegből és egy lineáris fejből álló modellen ellenőrizné. Adja meg: a réteg konfigurációját, a tanítási receptet, a baseline-t, a metrikát és az érvényességi korlátot. Nem kell futtatni.
F1. A rétegszintű backward-ellenőrzés a backward tenzorműveletet önmagában vizsgálja: rögzített inputra/súlyra a saját gradiens egyezik-e a numerikus vagy PyTorch-referenciával. A modellszintű tanulási kontroll ezen felül a paraméterregisztrációt, az optimalizálóba kerülést, a loss csatlakoztatását és a több lépéses gradiensáramlást is teszteli. A bias sima attribútumként tárolása a rétegszintű teszten átmegy, de a tanulásban a bias nem frissül, és a loss magasan ragad.
F2. Legyen a modell kimenete , a cél , ahol az átlagoló szűrő. A veszteség
Mivel , és , esetén minden -re nulla a veszteség, a minimum elérhető. Az egyértelműséghez vegyük észre, hogy kvadratikus a kilenc komponensében és -ben; a Hesse-mátrix alakú, ahol a -as patch-ek eloszlása teljes rangú, mert a bemenet -ből független komponensekből áll. Ezért a Hesse-mátrix pozitív definit, tehát a minimum egyértelmű.
F3. A kód:
def train_mean_filter(seed=20261020, N=64):
torch.manual_seed(seed)
x = torch.rand(N, 1, 8, 8, dtype=torch.float32)
target = torch.zeros_like(x)
for u in (-1, 0, 1):
for v in (-1, 0, 1):
target += 1.0/9.0 * torch.nn.functional.pad(
x, (1,1,1,1), value=0.0
)[:, :, 1+u:1+u+8, 1+v:1+v+8]
model = Conv2dReference(1, 1, 3, stride=1, padding=1)
opt = optim.SGD(model.parameters(), lr=0.5)
loss_fn = nn.MSELoss()
for _ in range(10):
opt.zero_grad()
out = model(x)
loss = loss_fn(out, target)
loss.backward()
opt.step()
with torch.no_grad():
E_W = (model.weight - 1.0/9.0).abs().max().item()
return loss.item(), E_W
print(train_mean_filter())Alakellenőrzés: a cél , a modell kimenete ugyanaz. Szélsőeset: esetén is működik, mert a batchméret nem osztja el a súly alakját. Tolerancia: a float32 konvergenciától függően alatt várható.
F4. Első ok: a tanítási ráta túl nagy, például
;
a gradienslövés miatt a veszteség oszcillál. Javítás: csökkentsük
-t
-re,
és rajzoljuk ki a lossgörbét. Második ok: az optimalizáló nem a modell
paramétereit frissíti, mert a Conv2dReference-ben a
paramétereket nem regisztráltuk, és az optimizer üres
listát kapott. Javítás: győződjünk meg arról, hogy a konstruktor
nn.Parameter-t hoz létre, és teszteljük az
optimizer.param_groups() hosszát.
F5. A kísérleti terv: a GlyphLab train/val/test
splitjét a [ADATOK.md] szerint használjuk, a modell két
Conv2dReference rétegből áll: az első
csatorna,
,
,
;
a második
,
,
,
;
ezután BatchNorm nélkül, ReLU-val és egy
lineáris fejjel. A baseline ugyanez a modell PyTorch
nn.Conv2d rétegekkel, azonos seeddel és
súlyinicializálással. A metrika a validációs accuracy; a kísérlet akkor
tekinthető sikeres integrációnak, ha a saját réteges modell és a
baseline validációs pontossága legfeljebb a seed-szórásból adódó
tolerancián belül eltér. A korlát: a GlyphLab nem valós benchmark, és a
90°-os augmentáció a [ADATOK.md] szerint labelváltó, tehát nem
használható ebben a kísérletben.
A saját konvolúciós réteget nn.Module-ba integráltuk,
paramétereit nn.Parameter-ként regisztráltuk, és azonos
súlyokon egyeztettük a PyTorch F.conv2d-vel. Modellszintű
véges differenciás ellenőrzést adtunk float64-ben. A kis tanítási
feladat egy ismert 3×3-as átlagoló szűrőre konvergál SGD-vel; az
állítást szintetikus sanity taskként jelöltük, nem benchmarkként. A
lassú referenciakód nem hibás, ha numerikusan egyezik; a gyakori tévedés
a paraméterregisztráció hibája, amelyet azonos kimeneti tesztek
önmagukban nem fednek fel. Az átadási csomag a kódot, a környezet- és
futásjegyzéket, valamint a nyers integration_check.json
számokat tartalmazza.
A 38.1. Adat, split és előzetes protokoll egységben ezt a réteget a teljes GlyphLab osztályozási kísérlet adatfolyamába illesztjük: adat, split, előzetes protokoll és a modell kiválasztásának rögzített rendje.