37.4 Rétegből kis tanulómodell

37.4.1 1. Cél, előismeret és induló kérdés

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.

37.4.2 2. A réteg integrációs szerződése

A saját rétegünk a projekt korábbi specifikációját követi:

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

és WoutW_{out} ugyanígy, WW szélességgel, pp szimmetrikus zero padding, ss stride, dd 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

  1. a weight és bias tenzor nn.Parameter, tehát az optimizer.param_groups() listában szerepel;
  2. a 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;
  3. a modell kiértékelése után a 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.

37.4.3 3. Veszteség és a tanítási feladat

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 1→11\to1 konvolúciós rétegből áll, kh=kw=3k_h=k_w=3, stride 1, padding 1, dilation 1. A bemenet N×1×8×8N\times1\times8\times8 float32 tensor. A célkép az input mozgóátlaga egy 3×3-as átlagoló szűrővel, azaz

Yn,0,i,j=19∑u=−11∑v=−11Xn,0,i+u,j+v Y_{n,0,i,j}=\frac1{9}\sum_{u=-1}^{1}\sum_{v=-1}^{1} X_{n,0,i+u,j+v}

zero padding mellett. Ez azért jó teszt, mert:

A veszteség az MSE átlagos formája:

L=1NHoutWout∑n,i,j(Yn,0,i,j−Ŷn,0,i,j)2, L=\frac{1}{N H_{out} W_{out}}\sum_{n,i,j}\left(Y_{n,0,i,j}-\hat Y_{n,0,i,j}\right)^2,

ahol Ŷ\hat Y a modell kimenete. A redukció fontos: a PyTorch MSELoss alapértelmezésben a teljes batch elemszámával oszt, ami itt N⋅Cout⋅Hout⋅WoutN\cdot C_{out}\cdot H_{out}\cdot W_{out}. 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.

37.4.4 4. A modell és a keretrendszer egyeztetése azonos súlyokon

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 X∈ℝN×Cin×H×WX\in\mathbb R^{N\times C_{in}\times H\times W}, K∈ℝCout×Cin×kh×kwK\in\mathbb R^{C_{out}\times C_{in}\times k_h\times k_w}, b∈ℝCoutb\in\mathbb R^{C_{out}} rögzített, s≥1s\ge1, p≥0p\ge0, d≥1d\ge1. A projekt saját conv2d_forward függvénye és a PyTorch F.conv2d hívás ugyanazt a YY 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 10−710^{-7} 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.

37.4.5 5. Véges gradiens a modellen belül

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 ff skalár kimenetű függvényre és a θi\theta_i paraméterre:

∂f∂θi≈f(θ+εei)−f(θ−εei)2ε, \frac{\partial f}{\partial\theta_i}\approx \frac{f(\theta+\varepsilon e_i)-f(\theta-\varepsilon e_i)}{2\varepsilon},

ahol eie_i az ii-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 grads

Itt 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 N=1N=1, H=W=6H=W=6, k=3k=3, p=1p=1, s=1s=1 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 10−410^{-4}-es ε\varepsilon mellett a relatív hiba 10−310^{-3} alatt marad a ∂L/∂θ\partial L/\partial\theta minden komponensére. Ha a gradiens abszolút értéke nullához közeli, a relatív hibát stabil nevezővel, max(|ref|,1)\max(|\text{ref}|,1)-gyel számoljuk.

37.4.6 6. Kis tanítás és az állítás típusa

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.

EW=maxi,j|W0,0,i,j−19|. E_W = \max_{i,j}\left|W_{0,0,i,j}-\frac19\right|.

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 10−510^{-5} alá csökken, és EW<10−3E_W<10^{-3} 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.

37.4.7 7. A benchmark hatóköre és az átadási csomag

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ő:

  1. A teszt célja: integrációs kontroll, nem architektúra-összehasonlítás.
  2. Az adat tartománya: szintetikus, 8×8-as, ismert lineáris cél; a 25.4. Nyílt halmaz és ismeretlen input terminológiája szerint a modell csak ezen a zárt tartományon értelmezhető.
  3. A nem vizsgált kérdések: általánosítás zajra, augmentációra, nagy csatornaszámra, GPU-ra, mixed precisionre. Ezeket a 24.4. VI. ellenőrző pont: megbízható CNN-training kerete szerint későbbi érvényességi lépésben kell külön igazolni.

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 EWE_W-t. Így a futtatás eredménye szétválasztható a szöveges értelmezéstől.

37.4.8 8. Gyakori tévedés és ellenpélda

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.

37.4.9 9. Önellenőrzés és visszahívás

Két gyors önellenőrzést végzünk el:

  1. Alakellenőrzés. A modell bemenete (N,1,8,8)(N,1,8,8), súlya (1,1,3,3)(1,1,3,3), padding p=1p=1, stride s=1s=1, dilation d=1d=1; a kimeneti alak Hout=Wout=8H_{out}=W_{out}=8, tehát a kimeneti alak (N,1,8,8)(N,1,8,8), az MSE ugyanolyan alakú cél ellen számolható.
  2. Szélsőeset. Ha a bemenet konstans cc, a 3×3-as átlagoló szűrő kimenete a belső pixeleken szintén cc, a széleken kisebb a zero padding miatt; a veszteség ekkor is véges. Ha a súly minden eleme 1/91/9 és a bias 00, a veszteség pontosan 0 az egész tartományon, mert a modell definíció szerint a célt számolja.

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 T(aX+bZ)=aT(X)+bT(Z)T(aX+bZ)=aT(X)+bT(Z); 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.

37.4.10 10. Feladatok

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 (W,b)(W,b) 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 EW<10−3E_W<10^{-3}. 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.

37.4.11 11. Megoldások és indoklás

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 Ŷ=W*X+b\hat Y=W*X+b, a cél Y=A*XY=A*X, ahol AA az átlagoló szűrő. A veszteség

L=𝔼X[164∑i,j((W−A)*X+b)2]. L=\mathbb E_X\left[\frac1{64}\sum_{i,j}\left((W-A)*X+b\right)^2\right].

Mivel L≥0L\ge0, és W=AW=A, b=0b=0 esetén minden XX-re nulla a veszteség, a minimum elérhető. Az egyértelműséghez vegyük észre, hogy LL kvadratikus a WW kilenc komponensében és bb-ben; a Hesse-mátrix 𝔼X[2vec(Xpatch)vec(Xpatch)⊤]\mathbb E_X[2\, \mathrm{vec}(X_{\mathrm{patch}})\mathrm{vec}(X_{\mathrm{patch}})^\top] alakú, ahol a 3×33\times3-as patch-ek eloszlása teljes rangú, mert a bemenet Unif[0,1]\mathrm{Unif}[0,1]-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 (64,1,8,8)(64,1,8,8), a modell kimenete ugyanaz. Szélsőeset: N=1N=1 esetén is működik, mert a batchméret nem osztja el a súly alakját. Tolerancia: EWE_W a float32 konvergenciától függően 10−310^{-3} alatt várható.

F4. Első ok: a tanítási ráta túl nagy, például η=10\eta=10; a gradienslövés miatt a veszteség oszcillál. Javítás: csökkentsük η\eta-t 0,10{,}1-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ő 1→81\to8 csatorna, k=3k=3, p=1p=1, s=2s=2; a második 8→168\to16, k=3k=3, p=1p=1, s=2s=2; ezután BatchNorm nélkül, ReLU-val és egy 16⋅6⋅6→316\cdot6\cdot6\to3 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.

37.4.12 Rövid összefoglaló

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.

37.4.13 Következő alkalmazás

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.

37.4.14 Források