A 34.2. Kvantálás és hibatovábbterjedés
egységben a kvantálás a tárolandó súly bitszélességét
csökkentette, miközben az egyes rétegek alakja és műveletszáma
változatlan maradt. A pruning ezzel szemben a paraméterek
számát akarja csökkenteni: a kérdés az, hogy egy súly elhagyása
ténylegesen kisebb futási költséget jelent-e. Az induló kérdés: van egy
lineáris rétegünk, amelynek súlymátrixában a belépések 70 százaléka
nulla; a torch.nn.Linear ugyanannyi szorzást végez-e ekkor,
mint a nulla nélküli változaton? A válasz a szokásos sűrű futási úton
igen: a sűrű Linear kernel a nulla
súlypozíciókat is bejárja, ugyanannyi szorzást végez. Kevesebb számítás
csak akkor lesz, ha a nullák mintázatát ténylegesen kihasználó ritka
kernel fut, vagy a pruning a tensor dimenzióját is csökkenti (például
egész csatornák eltávolításával). Ezért kell szétválasztanunk a
nullázott súlyt és a ténylegesen kihagyott számítást.
A témához tartozik az unstructured/structured pruning, a ritkasági
arány, a csatorna-eltávolítás, az újratanítás és a kernel-támogatás
kérdése is.
Előismeret: a tenzoralakok és paraméterszámlálás a 14.3. Paraméterszám, MAC és aktivációmemória
egységből, a PyTorch nn.Module regisztrációja a 11.2. nn.Module és paraméterregisztráció egységből,
valamint a kvantálásból származó numerikus hibakezelés a 34.2. Kvantálás és hibatovábbterjedés egységből. A
33.3. torch.export és ONNX export-ellenőrzés
gondolata abban tér vissza, hogy egy pruned modellt is ugyanúgy
exportálni és ellenőrizni kell; a 30.3. Negatív
nélküli módszerek és collapse egységből a „kontroll nélkül nem
állítunk javulást” elve érvényes; a 22.3. Zajos és
hiányos címke egység pedig arra emlékeztet, hogy a tanítási és
értékelési kiírásokat a pruning után sem szabad összekeverni.
Legyen egy teljesen összekötött réteg súlymátrixa. A strukturálatlan pruning (unstructured pruning) elemenkénti maszkot rendel a súlyhoz: , és a használt súly
ahol elemenkénti szorzás. A ritkaság (sparsity) a nullázott elemek aránya:
Ha , a súlytároló héttizede elvileg elhagyható, de a tényleges megtakarítás attól függ, mit tud a futtató kernel. A kernel-támogatás fogalma: egy ritkítási mintázatot akkor támogat a futási út, ha a minta alapján a végrehajtott szorzások és memóriamozgatások száma is csökken. A sűrű (dense) mátrixszorzó implementáció ezt nem teszi meg automatikusan: bejárja mind a súlypozíciót, akár nulla az ott tárolt érték, akár nem.
A strukturált pruning (structured pruning) nem elemeket, hanem teljes tensorszeleteket távolít el: egy lineáris rétegnél kimeneti neuront, konvolúciós rétegnél pedig egy teljes bemeneti vagy kimeneti csatornát. A channel removal a konvolúciós súly valamelyik csatornaindexét törli a tensorból, nem nullává teszi. Ekkor az új súly alakja kisebb, tehát az általános dense konvolúciós kernel is kevesebb tényleges szorzást végez. A strukturált pruning azonban a hálózat függvényét általában megváltoztatja, ezért fine-tune, azaz a megmaradt súlyokból kiinduló további tanítás követi.
A PyTorch torch.nn.utils.prune modulja strukturálatlan
maszkolást valósít meg: a maszk külön bufferként jelenik meg a paraméter
mellett, és a forward kiértékelés a maszkolt súlyt használja. Ez
önmagában nem csökkenti a dense kernel műveletszámát.
Legyen
egy Conv2d réteg, amely
bemeneti csatornából
kimeneti csatornába képez
kernelmérettel, és legyen
egy második Conv2d, amely
bemeneti csatornából
kimeneti csatornába képez
kernelmérettel. Mindkét réteg legyen biasszal ellátott. A
művelet a JELOLESEK szerinti térbeli cross-correlation, nem matematikai
konvolúció.
Lemma (csatornatörlés ekvivalenciája nulla fogyasztósúly esetén). Ha valamely indexre minden kimeneti csatornára, akkor a -edik kimeneti csatorna törlése -ből és a -edik bemeneti csatorna törlése -ből a kompozíció függvényét pontosan változatlanul hagyja.
Bizonyítás. Jelölje és a törlés előtti és utáni közbülső aktivációt. csatornái éppen csatornái a index kihagyásával, mert a megmaradó csatornák súlyai és biasai változatlanok. A -adik kimenetre
A feltétel miatt a harmadik tag azonosan nulla, így a indexek összege pontosan a törlés utáni értékét adja. Ezzel az állítást beláttuk. A bizonyítás kulcsa, hogy a törölt csatornát a fogyasztó oldalán is töröltük; ha a fogyasztósúly nem nulla, az ekvivalencia nem áll fenn.
Állítás (paraméterszám-csökkenés egy csatorna eltávolításakor). A fenti két réteg paraméterszáma a törlés előtt
a törlés után pedig
feltéve, hogy . A különbség
Bizonyítás. Az súlyának alakja , minden kimeneti csatornához súly és bias tartozik, ezért . A -edik kimeneti szelet törlése után a kimeneti méret , a biasvektor egy elemmel rövidebb. Az súlyának alakja ; a -edik bemeneti szelet törlése a középső tengely hosszát csökkenti -re, a biasvektor hossza pedig változatlan marad, mert a kimeneti csatornák száma ugyanaz. A kivonást elvégezve
A törlés során a közbülső aktiváció alakja ugyanígy alakról alakra változik; a térbeli dimenziók nem változnak, ha a padding, stride és dilation beállítások azonosak maradnak.
Használjunk a 19.2. MobileNet és inverted residual egységben megismert pontonkénti rétegeket, hogy minden szám kézzel követhető legyen. Legyen , , , . A bemenet legyen az egyelemű alakú tensor, és . Az súlya és biása:
Ekkor , , . Az úgy legyen megválasztva, hogy az -es bemeneti csatorna fogyasztósúlya nulla legyen:
A kimenet
Töröljük most az -es közbülső csatornát. A megtartott indexek és , az új súlya alakú:
ezért . Az új súlyában az eredeti súly középső bemeneti oszlopát töröljük:
A kimenet
tehát a törlés előtti kimenettel pontosan egyezik, ahogy a lemma állítja. A paraméterszám a törlés előtt , utána , a különbség . A harmadik szakasz képlete szerint , ami egyezik.
A következő példa a fenti számítást a PyTorch Conv2d
rétegeivel ellenőrzi. A példa az olvasó által futtatható; a kódban
kommentként jelölt kimenetek a kézi példából számított értékek, nem
előre elvégzett futási eredmények.
# munkakönyvtár: lab/
# python n135_prune_example.py
import torch
import torch.nn as nn
torch.manual_seed(0)
x = torch.tensor([[[[1.0]], [[2.0]]]], dtype=torch.float32)
def make_pair():
l1 = nn.Conv2d(2, 3, 1)
l2 = nn.Conv2d(3, 2, 1)
with torch.no_grad():
l1.weight.copy_(torch.tensor([
[[[1.0]], [[0.0]]],
[[[0.0]], [[2.0]]],
[[[-1.0]], [[1.0]]]
], dtype=torch.float32))
l1.bias.copy_(torch.tensor([0.0, 1.0, 0.0]))
l2.weight.copy_(torch.tensor([
[[[2.0]], [[0.0]], [[-1.0]]],
[[[1.0]], [[0.0]], [[3.0]]]
], dtype=torch.float32))
l2.bias.copy_(torch.tensor([0.0, -2.0]))
return l1, l2
def remove_channel(l1, l2, j):
keep = [c for c in range(l1.out_channels) if c != j]
l1p = nn.Conv2d(l1.in_channels, l1.out_channels - 1, l1.kernel_size,
stride=l1.stride, padding=l1.padding, bias=True)
l2p = nn.Conv2d(l2.in_channels - 1, l2.out_channels, l2.kernel_size,
stride=l2.stride, padding=l2.padding, bias=True)
with torch.no_grad():
l1p.weight.copy_(l1.weight[keep])
l1p.bias.copy_(l1.bias[keep])
l2p.weight.copy_(l2.weight[:, keep])
l2p.bias.copy_(l2.bias)
return l1p, l2p
l1, l2 = make_pair()
l1p, l2p = remove_channel(l1, l2, 1)
z = l2(l1(x)) # számított: [[[1.0]], [[2.0]]]
zp = l2p(l1p(x)) # számított: ugyanaz
torch.testing.assert_close(z, zp, rtol=1e-7, atol=1e-7)
print(z.flatten().tolist()) # [1.0, 2.0]A remove_channel függvény ténylegesen új
Conv2d objektumokat hoz létre, és a megtartott indexekkel
másolja át a súlyokat; nem maszkol. A törlés után a paraméterszám a
sum(p.numel() for p in l1.parameters()) hívásokkal is
ellenőrizhető: 9-ből 6 és 8-ból 6 lesz.
A labor egy kis GlyphLab-CNN-en két modellt hasonlít össze: egy tanított baseline-t és egy strukturáltan pruned változatot. Az alábbi eredmények közül a paraméterszámok a pythonbeli alakokból számított értékek; a pontosság és futásidő mérését a protokoll szerint kell elvégezni, így azokat itt üres mezőként vagy jelölt szemléltetésként adjuk meg.
# lab/n135_prune_lab.py (vázlat)
import torch
import torch.nn as nn
class TinyGlyphCNN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
self.pool = nn.MaxPool2d(2)
self.head = nn.Linear(16 * 12 * 12, 3)
def forward(self, x):
x = torch.relu(self.conv1(x))
x = torch.relu(self.conv2(x))
x = self.pool(x)
return self.head(torch.flatten(x, 1))
def channel_l1(weight):
# weight: (C_out, C_in, kh, kw)
return weight.abs().sum(dim=(1, 2, 3))
def remove_pair(conv1, conv2, drop1, drop2):
keep1 = [c for c in range(conv1.out_channels) if c not in drop1]
keep2 = [c for c in range(conv2.out_channels) if c not in drop2]
c1p = nn.Conv2d(conv1.in_channels, conv1.out_channels - len(drop1),
conv1.kernel_size, stride=conv1.stride,
padding=conv1.padding, bias=True)
c2p = nn.Conv2d(conv2.in_channels - len(drop1),
conv2.out_channels - len(drop2),
conv2.kernel_size, stride=conv2.stride,
padding=conv2.padding, bias=True)
with torch.no_grad():
c1p.weight.copy_(conv1.weight[keep1])
c1p.bias.copy_(conv1.bias[keep1])
c2p.weight.copy_(conv2.weight[keep2][:, keep1])
c2p.bias.copy_(conv2.bias[keep2])
return c1p, c2pA ritkítási specifikáció: a baseline tanítása után a
conv1 nyolc kimeneti csatornájából kettőt, a
conv2 tizenhat kimeneti csatornájából négyet törlünk;
mindkét törlést a train-állapotból számított legkisebb
csatorna--normák
alapján választjuk, a val- vagy test-eloszlás megtekintése nélkül. Az új
fej bemeneti mérete
lesz; a régi head súlyát nem lehet átmásolni, mert a
bemeneti reprezentáció csatornaszáma megváltozott, ezért a fejet újra
kell inicializálni.
A paraméterszám:
| Modell | Súlyok és biask | Összesített |
|---|---|---|
| baseline | conv1 80, conv2 1168, fej 6915 | 8163 |
| pruned | conv1 60, conv2 660, fej 5187 | 5907 |
A pruned változat paraméterszáma
-kal
kisebb. A memóriaellenőrzéshez a paraméter-bájtok összege
sum(p.numel() * p.element_size() for p in model.parameters());
a baseline esetén
bájt, a pruned esetén
bájt. Az aktivációs memóriát a torch.profiler
profile_memory=True kapcsolójával lehet mérni; a pontos
mezőneveket a helyi dokumentációból kell ellenőrizni ([D17] státuszát
lásd a forrásoknál).
A futtatási parancs:
python n135_prune_lab.py --images data/images.npy --labels data/labels.npy \
--metadata data/metadata.csv --epochs 10 --drop1 2 --drop2 4 --seed 20261020A protokoll: Tanuljuk a baseline-t train-en, maximálisan 20 epochig,
Adam tanulóval, 0.001 tanulási rátával, 32-es batchcsel; a legjobb
állapotot a val veszteség választja ki. A pruned modellt a kiválasztott
baseline állapotból másoljuk, a fenti csatornák törlésével, majd 10
epochig finomhangoljuk. A zárótesztet egyszer, a protokoll rögzítése
után értékeljük. A nem futtatott pontosságértékeket a laborjelentésben
MÉRENDŐ mező jelöli; a paraméter- és alakkontroll
tételszerű, a pontossághatás viszont empirikus és seedfüggő.
A strukturálatlan ritkítás gyakori félreértése, hogy a nullázás már
gyorsítja a hálózatot. Tekintsük a torch.nn.Linear réteget,
amely az
bemenetből
kimenetet számít. Ha a
mátrixban nullákat helyezünk el, a dense mátrixszorzó kernel továbbra is
végigjárja a teljes
súlyterületet; nem létezik a futásidőben automatikusan kiértékelt
maszkfeltétel a kernel belsejében. A nullázott dense súly tehát nem
csökkenti a futtatott dense műveletek számát, csak a tárolt értéket
változtatja.
Ellenpélda: legyen , azaz a súly 25 százaléka nulla. Egy -beli bemenetre a kimenet , . A futtatott GEMM akkor is négy szorzást és a hozzájuk tartozó összeadásokat végzi, ha a második elem nulla; a kernel nem tudja kihagyni a pozíciót a dense reprezentációból. Ezzel szemben a strukturált törlés, például a fenti csatorna-eltávolítás, megváltoztatja a súlytenzor alakját, ezért az általános dense Conv2d kernel is kisebb dimenzióval és kevesebb tényleges szorzással fut. A maszkolt Python-reteg ráadásul a forwardban még egy elemenkénti szorzást is hozzáadhat, tehát rosszul megválasztott ritkítási módszerrel a pruned modell akár lassabb is lehet.
F1 (fogalmi, nehézség 1). Definiálja a strukturálatlan és a strukturált pruning különbségét. Adjon egy méretű súlymátrixon példát mindkettőre, és mondja meg, miért nem ad az unstructured nullázás automatikus gyorsulást egy sűrű GEMM-ben. Átadandó: két mátrix, maszkok, shape-változás, egy rövid indoklás.
F2 (levezetési, nehézség 2). Igazolja a paraméterszám-lemmát bias nélküli rétegekre, majd alkalmazza , , , , értékekkel: adja meg a két réteg súlyalakját, paraméterszámát törlés előtt és után. Átadandó: teljes levezetés és , , .
F3 (implementációs, nehézség 2). Írjon
PyTorch-függvényt, amely két Conv2d rétegből ténylegesen
eltávolítja a
-edik
közbülső csatornát, majd a 4. szakasz numerikus példáján ellenőrizze
torch.testing.assert_close segítségével. Átadandó:
futtatható kód, shape-kontroll, egyezési feltétel.
F4 (hibakeresési, nehézség 2). Adott a következő kód:
l1.weight.data[1] = 0
l1.bias.data[1] = 0
# a l2-t nem módosítjukMagyarázza el, miért hamis channel pruningnak nevezni ezt, és mi a kimenetre és a számítási költségre gyakorolt hatása. Adja meg a javítást. Átadandó: hiba ok, következmény, javított kódrészlet, ellenőrzési sor.
F5 (kísérlettervezési/transzfer, nehézség 3). Tervezzen GlyphLab-kísérletet a baseline, a 70 százalékos unstructured pruning és a 25 százalékos structured channel pruning összehasonlítására CPU-n. A cél annak eldöntése, hogy az azonos val-pontosságú tűréshatár mellett melyik változat ad rövidebb CPU forward időt. Írja le a splitet, a baseline-t, a metrikákat, a seed- és fine-tune-protokollt, valamint a következtetés korlátait. Átadandó: protokollterv és alternatív értelmezés.
F1 megoldás. Az unstructured pruning az elemenkénti maszkkal nulláz, a mátrix alakja marad. Például
A structured pruning teljes oszlopot töröl: ha a második oszlopot elhagyjuk, az eredmény alakú, és a sűrű mátrixszorzás tényleges mérete is csökken. A sűrű GEMM az unstructured nullás mátrixot teljes -ként beolvassa, nullákkal együtt, ezért azok a pozíciók továbbra is részt vesznek a ciklusban.
F2 megoldás. Bias nélkül . A törlés után , tehát
A konkrét értékekkel , , . A súlyalakok: : , törlés után ; : , törlés után . Ez a feladat megerősíti, hogy a -es tagok csak biasos esetben lépnek be.
F3 megoldás. Az 5. szakasz
remove_channel függvénye teljes: a keep
indexekkel átmásolja
kimeneti és
bemeneti szeleteit. A szükséges kontrollok:
assert l1p.weight.shape == (2, 2, 1, 1)
assert l2p.weight.shape == (2, 2, 1, 1)
assert sum(p.numel() for p in l1p.parameters()) == 6
assert sum(p.numel() for p in l2p.parameters()) == 6
torch.testing.assert_close(zp, z, rtol=1e-7, atol=1e-7)Az első feltétel az alak-ellenőrzés: a törlés után mindkét 1×1 réteg súlyú. A numerikus egyezés itt a lemma feltételét ellenőrzi, nem pedig általánosan bizonyítja bármely réteg törlését.
F4 megoldás. A hiba az, hogy a kód a csatornát
nullázza, nem törli. A
csatorna kimenete azonosan nulla lesz, ezért az
kimenete numerikusan megegyezhet a törölt változattal, de a
súlyainak megfelelő bemeneti oszlopa továbbra is a memóriában és a
kernelben él; a paraméterszám nem csökken, és a futott szorzások száma
sem. A javítás az 5. szakasz függvénye: új Conv2d példányok
létrehozása és a megtartott indexek átmásolása. Az ellenőrzés a
paraméterszám és a assert_close együttes futtatása.
F5 megoldásminta. A független mintavételi egység a
scene_id; a train/val/test split rögzített. A baseline a
TinyGlyphCNN, a változók: no prune, unstructured 70%,
structured 25%. A structured modellt a baseline legjobb
val-checkpointjából vágjuk; az unstructuredt ugyanabból maszkoljuk. A
tanulási lépések három seeddel ismétlődnek
,
,
,
mert egyetlen seed nem hordozhatósági garancia. A metrika a val accuracy
a fine-tune után, a végső teszten egyszer, a legjobb val-állapottal
mérve; a futásidőt torch.utils.benchmark.Timer méri
melegítéssel és ismétlésekkel, a memóriát a profiler. A következtetés
csak a helyi CPU-környezetre és a GlyphLab-adatra érvényes; GPU-ra vagy
természetes képekre nem általánosítható. A vártnál rosszabb pruned
pontosság sem a structured pruning érvénytelenítése, hanem a fine-tune
és a törlési kritérium hibájának jele lehet.
Ellenőrző lista: a maszkolt súly alakja mindig azonos az eredetiével, a törölt súly alakja kisebb; a bias hossza csak a kimeneti tengely törlésekor csökken; a esetben a törlés üres csatornateret hagyna, ezért a lemma feltétele ; a maradékcsatlakozásos blokkokban a csatorna törlése az összeadás mindkét ágát módosítja, ezért a 18.1. Reziduális blokk és identitásút blokkja külön feltételt igényel. A 34.2. Kvantálás és hibatovábbterjedés kvantálás visszahívása: ott az alak nem változik, de a számábrázolási hiba megjelenik; itt az alak változik, de a numerikus ekvivalencia csak nulla fogyasztósúly esetén garantált. A 33.3. torch.export és ONNX export-ellenőrzésből örökölt elv: az új modell viselkedését véges kontrollhalmazon is újra meg kell mérni.
A pruning akkor csökkenti a számítást, ha ténylegesen törli a
tensordimenziót vagy ha a kernel kifejezetten támogatja a ritka
mintázatot. A strukturálatlan nullázás ritkaságot adhat a tárolásban, de
nem csökkenti automatikusan a futott dense műveleteket. A strukturált
csatorna-eltávolítás egy Conv2d kimeneti csatornáját és a
következő réteg bemeneti csatornáját együtt törli; két rétegnél a
paraméterszám csökkenése
.
Nulla fogyasztósúly esetén a törlés a függvényt pontosan megőrzi,
általában azonban fine-tune-ra van szükség. A laborban a kiválasztást
train-statisztika, a végső döntést val-mérés, a zárt tesztet egyszeri
mérés igazolja.
A 34.4. Teacher–student desztilláció egység a tudásdesztillációt tárgyalja: ott a már pruned tanulóháló egy nagyobb teacher-lágy céljait közelíti, ami a pruninghoz képest független sűrítési út ([P26] az absztrakt és a rekord szintjén jelöli a keretet). A laborban használt profiler és benchmark vázlat a hivatalos PyTorch-oldalakra mutat, amelyek tartalma a jelen kézirat írásakor még nem volt megnyitva, ezért a konkrét API-mezőket a telepített 2.14-es verzióban ellenőrizni kell.
Felhasznált források: