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 paraméteres modellből tartozik a backbone-hoz, és azt befagyasztjuk, hány paramétert frissít egy optimizer-lépés? A válasz várhatóan , de a bufferek kérdése ennél finomabb: a fagyasztás nem állít meg minden állapotváltozást.
Definíció (backbone, feature extractor). Legyen a modell
ahol a backbone vagy feature extractor, pedig a head (fej). és a két rész súlyparaméterei, és a hozzájuk tartozó, nem súly jellegű állapotok (bufferek, például BatchNorm futó statisztikák). Batchelt bemenetre , a köztes reprezentáció , a logit kimenet .
Definíció (pretrained weights). Egy
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
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ókA 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.
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
modellt. Tegyük fel, hogy minden
-re
requires_grad=False, minden
-ra
requires_grad=True, és az optimizer csak
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:
train() módban
változnak. Minden BatchNorm réteg a momentum-szabályt hajtja
végre a forward alatt:
ahol
a modul momentum paramétere,
és
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.eval() módban nem
változnak: a réteg a tárolt
értékekkel normalizál.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.
Legyen a backbone egyetlen affin réteg , a fej , bias nélkül. Legyen
A köztes reprezentáció
A logitok . Legyen a cél ; a softmax valószínűségei
A cross-entropy gradiens a logitson . A fej súlygradiense
a fej biasgradiense ugyanaz a vektor. A köztes reprezentáció
gradiense
.
Ha a backbone nincs befagyasztva,
gradiense
,
egy
mátrix. Ha a backbone fagyott, három esetet kell
elválasztani. (a) Paramétergradiens tiltása:
nem kap gradienst,
None marad; a fej viszont frissül. (b) Bemeneti
gradiens: ha
requires_grad=True, a gradiens a fagyott lineáris
backbone-on át is eljut a bemenetig,
;
a fagyasztás tehát nem vágja el a gráfot. (c) Gráfvágás: ha sem
,
sem a backbone súlya nem igényel gradienst (vagy a backbone
torch.no_grad() alatt, illetve detach() után
fut), a
már a gráf levele, a backward a fejnél megáll, és
nem is számolódik ki automatikusan.
# 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áltozikA 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.
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.
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
képletét, és mutasd meg, hogy
akkor is változhat, ha
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?
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
Ha és , akkor a különbség . A jobb oldal egyetlen tényezője sem függ 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 változhat, miközben változatlan. Ugyanez a levezetés érvényes -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_gradAlakellenő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
a vertical,
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.
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.
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
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.
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.
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.