20.1 Feature extractor és új fej

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

Ebben az alfejezetben azt az üzemmódot vizsgáljuk, amelyben egy már betanított háló nagy részét változatlanul hagyjuk, és csak egy kicsi, feladatspecifikus utolsó réteget tanítunk. Előfeltétel a 12.4. III. ellenőrző pont: önállóan tanuló kis háló tanítási ciklusa, a 17.4. Architektúra összehasonlítása kontrollált keretben kontrollált összehasonlítás szempontjai, a 18.4. Reziduális blokk ablatív vizsgálata ablatív gondolkodásmód és a 19.4. ConvNeXt és a recept/architektúra elválasztása recept/architektúra szétválasztás. Induló becslés: ha egy 100000100\,000 paraméteres modellből 9850098\,500 tartozik a backbone-hoz, és azt befagyasztjuk, hány paramétert frissít egy optimizer-lépés? A válasz várhatóan 15001\,500, de a bufferek kérdése ennél finomabb: a fagyasztás nem állít meg minden állapotváltozást.

20.1.2 Backbone, fej és pretrained súly: definíciók

Definíció (backbone, feature extractor). Legyen a modell

F(x)=hH(fB(x;ΘB,ℬB);ΘH,ℬH),F(x)=h_H\!\big(f_B(x;\Theta_B,\mathcal B_B);\,\Theta_H,\mathcal B_H\big),

ahol fB:ℝC×H×W→ℝdf_B:\mathbb R^{C\times H\times W}\to\mathbb R^{d} a backbone vagy feature extractor, hH:ℝd→ℝKh_H:\mathbb R^{d}\to\mathbb R^{K} pedig a head (fej). ΘB\Theta_B és ΘH\Theta_H a két rész súlyparaméterei, ℬB\mathcal B_B és ℬH\mathcal B_H a hozzájuk tartozó, nem súly jellegű állapotok (bufferek, például BatchNorm futó statisztikák). Batchelt bemenetre X∈ℝN×C×H×WX\in\mathbb R^{N\times C\times H\times W}, a köztes reprezentáció H=fB(X)∈ℝN×dH=f_B(X)\in\mathbb R^{N\times d}, a logit kimenet Z=hH(H)∈ℝN×KZ=h_H(H)\in\mathbb R^{N\times K}.

Definíció (pretrained weights). Egy Θ\Theta súlyvektor akkor előtanított, ha egy dokumentált korábbi tanítási futtatás végállapotából származik, és most új inicializálásként töltjük be. A minősítés a tanítási előzményhez és a betöltési eljáráshoz kötött, nem a fájlformátumhoz: a pretrained=False jellel létrehozott háló architektúrája lehet azonos, de súlya véletlen, így nem előtanított.

Definíció (frozen parameter). Egy pp paraméter befagyasztott, ha a keretrendszerben p.requires_grad == False. Ilyenkor a backward a paraméterhez nem számol új gradienst. Két dolgot ez önmagában nem biztosít: egy korábban már kiszámolt p.grad nem törlődik, és a paraméter továbbra is szerepelhet az optimizer paraméterlistájában. A szokásos PyTorch-optimalizálók a grad is None paramétereket hagyják ki, nem a requires_grad zászlót nézik, ezért egy megmaradt régi gradienssel (vagy momentum- és weight decay-állapottal) a „fagyott” súly mégis változhat. A befagyasztás ajánlott receptje ezért:

for p in model.backbone.parameters():
    p.requires_grad_(False)
    p.grad = None                                  # régi gradiens törlése
optimizer = torch.optim.SGD(
    [p for p in model.parameters() if p.requires_grad], lr=1e-2)  # csak a tanítandók

A listából kizárás ajánlott, egyértelművé tevő lépés, nem API-tilalom. A befagyasztás a frissítés tiltása; a forward számításban a fagyott súly továbbra is aktív.

Definíció (optimizer groups). Az optimalizáló paramétercsoportjai a paraméterek diszjunkt halmazaihoz külön beállítást rendelnek. A fejcserés tanítás tipikus felosztása: az egyik csoport a fej paramétereit tartalmazza a teljes tanulási rátával, a másik csoport üres vagy nulla rátájú, mert a backbone-ot nem adjuk át az optimizernek. A csoportosítás tetszőleges, de a tanulási rátán kívül weight decay vagy momentum is eltérhet.

20.1.3 A befagyasztott backbone paraméter- és bufferleltára

A befagyasztás nem azonos az eval() móddal. Ezt a következő lemmában rögzítjük.

Lemma (befagyasztott backbone állapotváltozásai). Tekintsük a fenti FF modellt. Tegyük fel, hogy minden p∈ΘBp\in\Theta_B-re requires_grad=False, minden q∈ΘHq\in\Theta_H-ra requires_grad=True, és az optimizer csak ΘH\Theta_H elemeit kapja meg. Tegyük fel továbbá, hogy a modulokat a hívó vagy train(), vagy eval() módba állította. Ekkor egyetlen minibatch-tanítási lépésben:

  1. A backbone súlyparaméterei nem változnak: ΘB\Theta_B elemeire nem számolódik gradiens, és az optimizer nem frissíti őket.
  2. A fej súlyparaméterei frissülhetnek: ΘH\Theta_H a szokásos gradienslépés szabálya szerint változik.
  3. A backbone BatchNorm bufferei train() módban változnak. Minden BatchNorm réteg a momentum-szabályt hajtja végre a forward alatt: μ̂←(1−m)μ̂+mμbatch,σ̂2←(1−m)σ̂2+mσbatch2,\hat\mu\leftarrow(1-m)\,\hat\mu+m\,\mu_{\text{batch}},\qquad \hat\sigma^2\leftarrow(1-m)\,\hat\sigma^2+m\,\sigma^2_{\text{batch}}, ahol mm a modul momentum paramétere, μbatch\mu_{\text{batch}} és σbatch2\sigma^2_{\text{batch}} az aktuális batch statisztikái, a varianciakonvenció a modul beállítása szerinti. A num_batches_tracked buffer is növekszik. Ez akkor is megtörténik, ha a backbone súlyai fagyottak, mert a bufferfrissítés a forward része, nem autograd-művelet.
  4. A backbone BatchNorm bufferei eval() módban nem változnak: a réteg a tárolt μ̂,σ̂2\hat\mu,\hat\sigma^2 értékekkel normalizál.
  5. A fej bufferei a fej saját training attribútuma szerint viselkednek, függetlenül a backbone fagyasztásától.

Indoklás. Az (1)–(2) állítás az autograd gráfépítés és az optimizer definíciójából következik: a requires_grad=False levélre nincs visszafelé út, az optimizer pedig kizárólag a paraméterlistájában szereplő, requires_grad=True tenzorokat módosítja. A (3)–(4) állítás a BatchNorm implementált viselkedése: a forwardban a self.training érték dönti el, hogy a batch statisztikát vagy a futó statisztikát használja-e, és a futó statisztika frissítése közvetlen, grafikon kívüli értékadás. A (5) pont ugyanezen szabály alkalmazása a fejre. Ez implementációs lemma a PyTorch 2.x viselkedéséről, nem analitikus tétel; a momentum-szabály a könyvtár definíciója. A BatchNorm train/eval megkülönböztetését a 18.2. Batch normalization train és eval módban vezette be.

A lemma gyakorlati konzekvenciája: ha a befagyasztott backbone-ot train() módban hagyjuk, a súlyok állandóak, de a normalizálási állapot tovább mozog. Ha a cél a feature extractor teljes kimerevítése, a backbone moduljait eval() módba kell tenni, vagy a BatchNorm frissítését külön le kell tiltani.

20.1.4 Kézi kis példa

Legyen a backbone egyetlen affin réteg WB∈ℝ2×4W_B\in\mathbb R^{2\times4}, a fej WH∈ℝ3×2W_H\in\mathbb R^{3\times2}, bias nélkül. Legyen

WB=(102−101−11),WH=(1−12001),x=(2,1,−1,1).W_B=\begin{pmatrix}1&0&2&-1\\0&1&-1&1\end{pmatrix},\qquad W_H=\begin{pmatrix}1&-1\\2&0\\0&1\end{pmatrix},\qquad x=(2,1,-1,1).

A köztes reprezentáció

h=WBx=(1⋅2+0−2−10+1+1+1)=(−13).h=W_B x=\begin{pmatrix}1\cdot2+0-2-1\\0+1+1+1\end{pmatrix}=\begin{pmatrix}-1\\3\end{pmatrix}.

A logitok z=WHh=(−4,−2,3)z=W_Hh=(-4,-2,3). Legyen a cél y=0y=0; a softmax valószínűségei

p=softmax(z)≈(0,00091,0,00669,0,99240).p=\operatorname{softmax}(z)\approx(0{,}00091,\;0{,}00669,\;0{,}99240).

A cross-entropy gradiens a logitson p−e1≈(−0,99909,0,00669,0,99240)p-e_1\approx(-0{,}99909,\;0{,}00669,\;0{,}99240). A fej súlygradiense

∂L∂WH=(p−e1)h⊤≈(0,99909−2,99727−0,006690,02007−0,992402,97720),\frac{\partial L}{\partial W_H}=(p-e_1)\,h^\top\approx\begin{pmatrix}0{,}99909&-2{,}99727\\-0{,}00669&0{,}02007\\-0{,}99240&2{,}97720\end{pmatrix},

a fej biasgradiense ugyanaz a vektor. A köztes reprezentáció gradiense g=WH⊤(p−e1)≈(1⋅(−0,99909)+2⋅0,00669,−1⋅(−0,99909)+1⋅0,99240)=(−0,98572,1,99150)g=W_H^\top(p-e_1)\approx(1\cdot(-0{,}99909)+2\cdot0{,}00669,\;-1\cdot(-0{,}99909)+1\cdot0{,}99240)=(-0{,}98572,\;1{,}99150). Ha a backbone nincs befagyasztva, WBW_B gradiense gx⊤g\,x^\top, egy 2×42\times4 mátrix. Ha a backbone fagyott, három esetet kell elválasztani. (a) Paramétergradiens tiltása: WBW_B nem kap gradienst, WB.gradW_B.\mathrm{grad} None marad; a fej viszont frissül. (b) Bemeneti gradiens: ha xx requires_grad=True, a gradiens a fagyott lineáris backbone-on át is eljut a bemenetig, ∇xL=WB⊤g\nabla_xL=W_B^\top g; a fagyasztás tehát nem vágja el a gráfot. (c) Gráfvágás: ha sem xx, sem a backbone súlya nem igényel gradienst (vagy a backbone torch.no_grad() alatt, illetve detach() után fut), a hh már a gráf levele, a backward a fejnél megáll, és gg nem is számolódik ki automatikusan.

20.1.5 Kódos ellenőrzés a kézi példára

# lab/n077/manual_check.py
import torch

torch.manual_seed(0)
dtype = torch.float64

x = torch.tensor([2.0, 1.0, -1.0, 1.0], dtype=dtype)
WB = torch.tensor([[1.0, 0.0, 2.0, -1.0],
                   [0.0, 1.0, -1.0, 1.0]], dtype=dtype)
WH = torch.tensor([[1.0, -1.0],
                   [2.0, 0.0],
                   [0.0, 1.0]], dtype=dtype, requires_grad=True)
y = torch.tensor(0)

def forward(wb, wh, freeze_backbone):
    wb = wb.detach()
    wb.requires_grad_(not freeze_backbone)
    h = wb @ x
    z = wh @ h
    loss = torch.nn.functional.cross_entropy(z.unsqueeze(0), y.unsqueeze(0))
    loss.backward()
    return h.detach(), loss.detach(), wb.grad, wh.grad

# Nem fagyasztott eset: mindkét gradiens létezik
h0, loss0, gWB0, gWH0 = forward(WB, WH.clone().detach().requires_grad_(True), False)
# Fagyasztott eset: csak a fej gradiense létezik
h1, loss1, gWB1, gWH1 = forward(WB, WH.clone().detach().requires_grad_(True), True)

assert torch.allclose(h0, torch.tensor([-1.0, 3.0], dtype=dtype), atol=1e-12)
assert torch.allclose(loss0, loss1, atol=1e-12)
p = torch.softmax(WH.detach() @ h0, dim=0)
g_h = WH.detach().T @ (p - torch.nn.functional.one_hot(y, 3).to(dtype))
expected_gWB = torch.outer(g_h, x)
assert gWB0 is not None
torch.testing.assert_close(gWB0, expected_gWB, rtol=1e-10, atol=1e-12)
# A pontos analitikus és autograd-gradiens alakja:
assert gWB0.shape == (2, 4) and gWB1 is None
assert gWH1 is not None and torch.allclose(gWH1, gWH0, atol=1e-12)
print("manual_check ok: frozen backbone -> WB.grad is None, WH.grad kept")

# (b) Fagyott backbone, de a bemenet gradienst igényel: a gradiens eljut x-ig.
xb = x.clone().requires_grad_(True)
zb = WH.detach() @ (WB @ xb)
torch.nn.functional.cross_entropy(zb.unsqueeze(0), y.unsqueeze(0)).backward()
torch.testing.assert_close(xb.grad, WB.T @ g_h, rtol=1e-10, atol=1e-12)
print("g_h =", [round(v, 5) for v in g_h.tolist()], " x.grad =", [round(v, 5) for v in xb.grad.tolist()])

# Ellenpélda: requires_grad_(False) után a régi .grad megmarad, és step() frissít.
w = torch.nn.Parameter(torch.tensor([1.0], dtype=dtype))
(w * 3.0).sum().backward()                      # w.grad = 3
w.requires_grad_(False)                         # "fagyasztás", de a grad nem törlődik
opt = torch.optim.SGD([w], lr=0.1)
opt.step()
print("fagyott w a step után:", w.item())       # 0.7: mégis változott
w.grad = None
opt.step()
assert w.item() == 0.7                           # grad=None mellett már nem változik

A kód futtatható a lab/n077 munkakönyvtárból: python manual_check.py. A várható kimenet az utolsó sor üzenete, feltéve hogy a losszámítás numerikusan megegyezik a kézi levezetéssel.

20.1.6 Labor: fejcsere a helyi tanított CNN-en, külső súly nélkül

A labor két fázisból áll. Először egy kis GlyphLab-CNN-t tanítunk 3 osztályra, és a teljes state_dict-et elmentjük. Másodszor a mentett modellt betöltjük, a fejet újra inicializáljuk, a backbone-ot befagyasztjuk, és ellenőrizzük, hogy a fenti lemma minden pontja teljesül. Ez nem valódi transzfer: az adat és a feladat ugyanaz marad, csak a frissítési mechanizmust gyakoroljuk. Valódi transzferhez dokumentált külső súlyazonosító és az adott súly preprocessing-szerződése szükséges; a [D13] katalógus konkrét elemeit e kézirat írásakor még nem ellenőriztük.

# lab/n077/head_swap_lab.py
import copy, json, math, random
from pathlib import Path
import numpy as np
import torch
import torch.nn as nn

SEED = 20261020
LR = 1e-3
EPOCHS = 3
DATA = Path("data")

random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
torch.set_default_dtype(torch.float32)

class Block(nn.Module):
    def __init__(self, cin, cout):
        super().__init__()
        self.conv = nn.Conv2d(cin, cout, 3, padding=1)
        self.bn = nn.BatchNorm2d(cout)
        self.act = nn.ReLU()
        self.pool = nn.MaxPool2d(2)
    def forward(self, x):
        return self.pool(self.act(self.bn(self.conv(x))))

class MiniCNN(nn.Module):
    def __init__(self, num_classes=3):
        super().__init__()
        self.backbone = nn.Sequential(Block(1, 16), Block(16, 32))
        self.head = nn.Linear(32 * 6 * 6, num_classes)
    def forward(self, x):
        h = self.backbone(x).flatten(1)
        return self.head(h)

def load_glyphlab():
    X = np.load(DATA / "images.npy").astype(np.float32)
    y = np.load(DATA / "labels.npy").astype(np.int64)
    split = np.loadtxt(DATA / "metadata.csv", delimiter=",",
                       dtype=str, skiprows=1)[:, 4]
    train_idx = np.where(split == "train")[0]
    val_idx = np.where(split == "val")[0]
    return X, y, train_idx, val_idx

def train_mini_cnn():
    X, y, tr, va = load_glyphlab()
    model = MiniCNN()
    opt = torch.optim.Adam(model.parameters(), lr=LR)
    lossf = nn.CrossEntropyLoss()
    Xtr = torch.from_numpy(X[tr]); ytr = torch.from_numpy(y[tr])
    for ep in range(EPOCHS):
        model.train()
        for i in range(0, len(tr), 32):
            xb, yb = Xtr[i:i+32], ytr[i:i+32]
            opt.zero_grad()
            loss = lossf(model(xb), yb)
            loss.backward()
            opt.step()
    torch.save(model.state_dict(), DATA / "mini_cnn_3class.pt")
    return model

def head_swap_probe():
    sd = torch.load(DATA / "mini_cnn_3class.pt", weights_only=True)
    model = MiniCNN()
    model.load_state_dict(sd)               # teljes modell, 3 osztályos fej is
    # Fejcsere: a fejet újra inicializáljuk, a backbone súlyát megtartjuk.
    model.head = nn.Linear(32 * 6 * 6, 3)
    model.head.weight.data.normal_(0, 0.02)
    model.head.bias.data.zero_()
    # Backbone befagyasztás, optimizer csak a fejet látja.
    model.backbone.requires_grad_(False)
    opt = torch.optim.Adam(model.head.parameters(), lr=LR)
    print("optimizer param_groups:", len(opt.param_groups))
    print("trainable params:", sum(p.numel() for p in model.parameters() if p.requires_grad))
    # Buffer-kontroll: BatchNorm train módban frissül, eval módban nem.
    bn = model.backbone[0].bn
    bn.eval(); before = bn.running_mean.clone()
    _ = model.backbone(torch.zeros(1, 1, 24, 24))
    assert torch.equal(bn.running_mean, before), "eval mode: running buffer must not change"
    bn.train(); before = bn.running_mean.clone()
    _ = model.backbone(torch.zeros(1, 1, 24, 24))
    assert not torch.equal(bn.running_mean, before), "train mode: running buffer must change"
    # Gradiens-kontroll egyetlen lépésen
    model.train()
    xb = torch.zeros(2, 1, 24, 24); yb = torch.tensor([0, 1])
    opt.zero_grad(); loss = nn.functional.cross_entropy(model(xb), yb); loss.backward()
    assert model.backbone[0].conv.weight.grad is None
    assert model.head.weight.grad is not None
    print("head_swap_probe ok: backbone frozen, head gradient exists, buffer rule verified")

if __name__ == "__main__":
    Path(DATA).mkdir(exist_ok=True)
    if not (DATA / "mini_cnn_3class.pt").exists():
        train_mini_cnn()
    head_swap_probe()

Futtatási parancs a lab/n077 könyvtárból: python head_swap_lab.py. A kód a jelen kéziratban nem futtatott; elvárt kimenete a három ellenőrző sor: a paramétercsoportok száma, a tanítható paraméterszám és a záró head_swap_probe ok üzenet. A pontos veszteségértékek a gép és a PyTorch-verzió szerint változhatnak, ezért a laborban nem veszteségszám, hanem a fenti három invariáns a kontroll.

20.1.7 Feladatok

F1 (fogalmi, nehézség 1). Mi a különbség a pretrained=True és pretrained=False konstruktor-argumentum közt egy olyan keretben, ahol a True súlybetöltést indít el? Melyik esetben beszélhetünk transzfertanulásról, és miért?

F2 (levezetési, nehézség 2). A fenti lemmában a backbone BatchNorm bufferei train() módban frissülnek, hiába fagyottak a súlyok. Bizonyítsd be ezt az állítást a momentum-szabályból: add meg μ̂t+1\hat\mu_{t+1} képletét, és mutasd meg, hogy μ̂\hat\mu akkor is változhat, ha ΘB\Theta_B egyetlen eleme sem változik.

F3 (implementációs, nehézség 2). Írj freeze_by_prefix(model, prefix) függvényt, amely minden olyan paramétert befagyaszt, amelynek neve prefix-szel kezdődik, és adja vissza a megmaradt tanítható paraméterek listáját. Teszteld egy nn.Sequential-be ágyazott Linear(4,3) + Linear(3,2) modellen; az elvárt kimenet: csak a második réteg paraméterei taníthatók.

F4 (hibakeresési, nehézség 2). Egy fejlesztő az alábbi pszeudokódot használja befagyasztott backbone-hoz, de az eval metrika a fej tanítása közben romlik, noha a training loss csökken. Mi a hibás feltevés, mi a következménye, és mi a két lehetséges javítás?

model.backbone.requires_grad_(False)
optimizer = Adam(model.head.parameters(), lr=1e-3)
for xb, yb in train_loader:      # a DataLoader nem állít módot
    model.train()                # az egész modellt train módba teszi
    loss = CE(model(xb), yb)
    loss.backward(); optimizer.step()
# később: model.eval(); eval_metrics(model, val_loader)

F5 (kísérlettervezési, nehézség 3). Tervezz fejcsere-kísérletet a GlyphLab adaton: a 3-osztályos CNN backbone-ját fagyaszd be, és cseréld a fejet bináris feladatra, ahol az egyik osztály vertical, a másik a horizontal és diagonal egyesítése. Add meg a megfigyelési egységet, a split szabályt, a baseline-t, a metrikát és a következtetési korlátot. Miért nem tekinthető ez külső domainre történő transzfernek?

20.1.8 Megoldások és indoklás

F1. A pretrained=True egy dokumentált korábbi tanításból származó súlyt tölt be; a False véletlen inicializálást ad. Transzfertanulásról csak az előbbi esetben beszélhetünk, mert a tanult reprezentáció átvitele a korábbi súlyokból indul. A False esetben a háló architektúrája lehet azonos, de a súlyokban nincs korábbi tudás. A gyakorlatban a logikai kapcsoló mellett a súlyfájl forrását és a preprocessinget is dokumentálni kell.

F2. A momentum-szabály definíció szerint

μ̂t+1=(1−m)μ̂t+mμbatch.\hat\mu_{t+1}=(1-m)\hat\mu_t+m\mu_{\text{batch}}.

Ha m>0m>0 és μbatch≠μ̂t\mu_{\text{batch}}\neq\hat\mu_t, akkor a különbség μ̂t+1−μ̂t=m(μbatch−μ̂t)≠0\hat\mu_{t+1}-\hat\mu_t=m(\mu_{\text{batch}}-\hat\mu_t)\neq0. A jobb oldal egyetlen tényezője sem függ ΘB\Theta_B elemeinek gradiensétől: a forward kimenet és a batch statisztika a fagyott súlyokkal számolódik, de a bufferfrissítés maga közvetlen értékadás. Tehát μ̂\hat\mu változhat, miközben ΘB\Theta_B változatlan. Ugyanez a levezetés érvényes σ̂2\hat\sigma^2-re.

F3. Egy teljes megoldás:

def freeze_by_prefix(model, prefix):
    trainable = []
    for name, p in model.named_parameters():
        if name.startswith(prefix):
            p.requires_grad_(False)
        else:
            if p.requires_grad:
                trainable.append((name, p))
    return trainable

m = nn.Sequential(nn.Linear(4, 3), nn.Linear(3, 2))
for n, p in m.named_parameters():
    assert p.requires_grad
tr = freeze_by_prefix(m, "0")
assert len(tr) == 2          # a második réteg weight és bias
assert not m[0].weight.requires_grad
assert m[1].weight.requires_grad

Alakellenőrzés: a lista elemei (str, Parameter) párok; a befagyasztott réteg paraméterei requires_grad=False értékűek. Szélsőeset: ha prefix üres, minden befagy; ha egyetlen név sem illeszkedik, a lista az összes tanítható paramétert visszaadja.

F4. A hibás feltevés az, hogy a requires_grad_(False) a BatchNorm futó statisztikáit is leállítja. A model.train() az egész modult train módba teszi, így a backbone BatchNorm rétegei a kis minibatch statisztikákkal frissítik a running_mean és running_var puffereket, noha a súlyaikat nem tanítjuk. Következmény: az eval során a modell olyan normalizálási állapotot használ, amely a legutóbbi mini-batchekhez idomult, ezért a valós kiértékelés romolhat. Két javítás: (1) a backbone moduljait külön eval() módba tesszük, a fejet train() módban hagyjuk; vagy (2) ha a backbone train módban marad, a BatchNorm momentum=None vagy track_running_stats=False beállítással letiltjuk a futó statisztika frissítését, és ezt a protokollban dokumentáljuk.

F5. Egy lehetséges terv. Protokoll: a bináris címke legyen 00 a vertical, 11 a horizontal+diagonal egyesítésre. A megfigyelési egység scene_id; a split a GlyphLab meglévő felosztását tartja, és mindkét view ugyanabba a részbe kerül. A 3-osztályos CNN mini_cnn_3class.pt súlyát betöltjük, a fejet Linear(1152,1) logisztikus rétegre cseréljük, a backbone-ot befagyasztjuk. Baseline: a tanítóbeli többségi osztály aránya, valamint a befagyasztott backbone feature-ein tanított logisztikus regresszió. Metrika: balanced accuracy, mert az egyesítés osztályaránya eltolódhat. A fejet a val spliten választjuk, a testet egyszer értékeljük. Korlát: az adat ugyanabból a generátorból származik, tehát ez feladatváltás, nem domain-transzfer; a mérés nem állít semmit külső eloszláson való használhatóságról.

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

Gyakori tévedés, hogy a pretrained=False háló „mégis transzfer, mert az architektúra ismert”. Ez nem következik. Példa: hozzunk létre két MiniCNN példányt azonos magból, az egyiket tanítsuk 3 osztályra, a másikat hagyjuk véletlen súlyokkal. A második betöltése nélkül a fejcserés tanítás nem indul tanult reprezentációból, így az eredmény nem transzferbizonyíték. A transzfer állításához legalább a betöltött súlyfájl forrását, a tanítási előzményt és a betöltés utáni súly-egyezés ellenőrzését kell felmutatni.

20.1.10 Önellenőrzés és korábbi fogalom visszahívása

Ellenőrizd: (1) a fagyott paraméter requires_grad értéke mindenütt False-e; (2) a backbone BatchNorm train/eval módja tudatosan választott-e; (3) az optimizer paramétercsoportja csak a tanítandó paramétereket tartalmazza-e; (4) a betöltött state_dict kulcsai illeszkednek-e az architektúrához. Korábbi fogalom: a 16.1. Eltolási equivariance és invariance alfejezetben bizonyított eltolási equivariancia a backbone belső tartományán megmarad a fejcserénél, mert fBf_B változatlan; a 19.1. Depthwise separable és pointwise réteg alfejezetből ismert paraméter/MAC arány sem változik a backbone-ban. A 8.1. Kockázat, mintavétel és baseline alfejezetből pedig azt használjuk, hogy a tanító adaton mért fejeredmény nem általánosít automatikusan más eloszlásra.

20.1.11 Rövid összefoglaló

A feature extractor vagy backbone a bemenetet fix dimenziós reprezentációba képezi, a head pedig erre a reprezentációra épít feladatspecifikus kimenetet. Előtanított súlynak csak dokumentált korábbi tanításból betöltött súly nevezhető. A befagyasztás a gradiens és az optimizer-frissítés tiltása; ettől függetlenül a BatchNorm futó statisztikák train módban tovább mozognak, ezért a befagyasztott backbone-ot általában eval módba tesszük. A labor helyi GlyphLab-CNN-en cserél fejet, és három invariánssal ellenőrzi a mechanizmust. Valódi transzferhez ellenőrzött külső súlyazonosító és preprocessing-szerződés szükséges.

20.1.12 Következő alkalmazás

A 20.2. Finomhangolás és tanulási ráta a finomhangolást tárgyalja: ott a backbone egy részét felengedjük, és a fejnél kisebb tanulási rátával tanítjuk együtt. Az itteni optimizer groups és a bufferleltár lesz a finomhangolás ütemezésének alapja.

20.1.13 Felhasznált források