A 27.1. Pixelszintű klasszifikáció és FCN egységben a szegmentálást pixelszintű osztályozásként definiáltuk: a modell minden pixelre logitot ad, a tanítás per-pixel cross-entropy vagy bináris logitveszteséggel történik. Ott a kérdés az volt, hogyan kell a logitok és a maszk tengelyeit párosítani. Most azt vizsgáljuk, milyen hálózati szerkezet állítja elő a térbeli kimenetet anélkül, hogy a felbontást azonnal eldobnánk. A U-Net az egyik leggyakrabban használt encoder–decoder architektúra; benne a downsampling kontextust gyűjt, az upsampling a felbontást állítja helyre, a skip-kapcsolat pedig a korai, nagy felbontású aktivációkat közvetlenül a dekódolóhoz vezeti.
Előismeret: a 15.2. Inputgradiens és átfedő recepciós mezők alfejezetből a konvolúciós inputgradiens scatter-add indexfeltétele, a 15.3. Transposed convolution mint adjungált alfejezetből a transzponált konvolúció mint adjungált operátor értelmezése, a 23.2. Kalibráció és döntési küszöb alfejezetből a logit és döntési küszöb különválasztása, a 26.2. Egy- és kétlépcsős detektor alfejezetből pedig a detektálási fejek felelősségének szétválasztása. A U-Net itt nem javaslattevő rendszer: maszkot jósol.
Induló kérdés. Egy bemeneti kép -es, egy csatornás. A kódoló két -es max-pool-lépéssel -ra csökkenti a felbontást. A dekódoló két tanulható upsampling-lépést használ. Mekkora lesz a kimenet, és hány csatornája van a második konkatenációnál, ha a skip-ek rendre és csatornásak? Becslés után a 3. szakasz levezetése ad pontos választ.
Legyen a bemeneti képbatch , ahol a főútban , . A bináris szegmentálási maszk : minden pixelhez egyetlen logit tartozik. A hálózat kimenete , ahol a pixel előtér-logitja, szigmoid előtt.
Encoder alatt azt a részhálózatot értjük, amely a térbeli felbontást csökkenti, miközben a csatornaszámot növeli. Decoder az a részhálózat, amely a felbontást ismét növeli és a csatornaszámot csökkenti. Skip concat a kódoló egy közbülső aktivációjának és a dekódoló azonos térbeli felbontású aktivációjának csatorna-irányú összefűzése:
A torch.cat hívásban ez dim=1, mert a
tenzoralak NCHW.
Upsampling minden olyan leképezés, amely
térbeli méretet növel. Két fő változatot különböztetünk meg. A nem
tanulható interpoláció, például
F.interpolate(..., mode="bilinear"), rögzített szabállyal
becsli a közbülső értékeket; ennek nincs paramétere. A tanulható
ConvTranspose2d a konvolúciós operátor adjungáltjaként szór
szét aktivációkat; ez nem általános inverz, hanem lineáris leképezés,
amely a 15.3. Transposed convolution mint
adjungált alfejezetben tárgyalt alakkal rendelkezik. A jelen
fejezetben
,
,
transzponált konvolúciót használunk, amely minden
-t
-ra
növel.
Spatial alignment az a feltétel, hogy a skip tenzor
kiterjedése egyenlő legyen az upsampling kimenet
kiterjedésével a cat előtt. Ha nem egyenlő, akkor crop vagy
pad szükséges; a mi főpéldánkban a
,
-gyel
osztható bemenet miatt a feltétel teljesül.
A levezetési kötelezettség egy konkrét, teljesen megadott kis U-Net
minden térbeli alakjának és a két concat csatornaszámának kiszámítása. A
modellt TinyUNet-nek nevezzük; ez a könyv saját
tanulópéldája, nem az eredeti publikáció másolata. Térbeli konvolúciók:
,
padding
,
stride
,
dilation
.
Pool:
max, stride
.
Upsampling:
,
,
transposed conv.
Lemma (alak- és csatornaegyezés a TinyUNetben). Legyen . A fenti paraméterezésű TinyUNet esetén
Bizonyítás. A konvolúciós kimeneti méret képlete 14.1. Stride, padding, dilation és outputméret jelölésével, mellett:
A , , esetben
tehát az ilyen konvolúció a térbeli méretet megtartja. A -es, stride-2 max-pool képlete , esetén
Innen esetén , majd esetén . A transzponált konvolúció , , , paraméterekkel a 15.3. Transposed convolution mint adjungált alfejezetben levezetett képlet szerint
Ezért a
bottleneck-ből
,
majd a
-ből
lesz. A cat a csatornadimenzióban ad össze: az első
upsampling
,
a hozzá tartozó skip
csatornás, így
;
a második upsampling
,
a skip
csatornás, így
.
A térbeli méretek a fenti értékek szerint rendre megegyeznek, tehát az
alignment feltétel teljesül. ∎
A teljes rétegenkénti alak a következő táblázatban:
| Blokk | Réteg | Kimeneti alak |
|---|---|---|
| encoder szint 1 | enc1a,
enc1b |
|
| pool | pool1 |
|
| encoder szint 2 | enc2a,
enc2b |
|
| pool | pool2 |
|
| bottleneck | bot_a,
bot_b |
|
| decoder szint 1 | up1 |
|
| skip concat 1 | cat(up1, enc2b) |
|
| decoder conv | dec1a,
dec1b |
|
| decoder szint 2 | up2 |
|
| skip concat 2 | cat(up2, enc1b) |
|
| decoder conv | dec2a,
dec2b |
|
| maszkfej | head |
A táblázat egyben kézi példa is: a -es skip és a -es up-aktiváció csatorna szerint -re bővül, majd a -es szinten -ra. A bottlenecknél a térbeli felbontás , a csatorna pedig : ez a modell legsűrűbb csatornareprezentációja.
A levezetést a következő, teljesen futtatható PyTorch-modullal
ellenőrizhetjük. A modul minden Conv2d rétegnél
padding=1-et használ, a két ConvTranspose2d
pedig kernel_size=2, stride=2 paraméterű, ezért a fenti
lemmát valósítja meg.
# tiny_unet_shapecheck.py
import torch
import torch.nn as nn
class TinyUNet(nn.Module):
def __init__(self, in_channels=1, out_channels=1, base=8):
super().__init__()
# encoder, 1. szint: 1 -> 8 csatorna, 24x24 megmarad
self.enc1a = nn.Conv2d(in_channels, base, 3, padding=1)
self.enc1b = nn.Conv2d(base, base, 3, padding=1)
self.pool1 = nn.MaxPool2d(2)
# encoder, 2. szint: 8 -> 16, 12x12 megmarad
self.enc2a = nn.Conv2d(base, 2 * base, 3, padding=1)
self.enc2b = nn.Conv2d(2 * base, 2 * base, 3, padding=1)
self.pool2 = nn.MaxPool2d(2)
# bottleneck: 16 -> 32, 6x6
self.bot_a = nn.Conv2d(2 * base, 4 * base, 3, padding=1)
self.bot_b = nn.Conv2d(4 * base, 4 * base, 3, padding=1)
# decoder, 1. szint
self.up1 = nn.ConvTranspose2d(4 * base, 2 * base, 2, stride=2)
self.dec1a = nn.Conv2d(4 * base, 2 * base, 3, padding=1) # 16 skip + 16 up
self.dec1b = nn.Conv2d(2 * base, 2 * base, 3, padding=1)
# decoder, 2. szint
self.up2 = nn.ConvTranspose2d(2 * base, base, 2, stride=2)
self.dec2a = nn.Conv2d(2 * base, base, 3, padding=1) # 8 skip + 8 up
self.dec2b = nn.Conv2d(base, base, 3, padding=1)
# maszkfej: logit, nem softmax előtti valószínűség
self.head = nn.Conv2d(base, out_channels, 1)
def forward(self, x):
e1 = torch.relu(self.enc1b(torch.relu(self.enc1a(x)))) # 8x24x24
e2 = torch.relu(self.enc2b(torch.relu(self.enc2a(self.pool1(e1))))) # 16x12x12
b = torch.relu(self.bot_b(torch.relu(self.bot_a(self.pool2(e2))))) # 32x6x6
u1 = self.up1(b) # 16x12x12
d1 = torch.relu(self.dec1b(torch.relu(self.dec1a(torch.cat([u1, e2], dim=1)))))
u2 = self.up2(d1) # 8x24x24
d2 = torch.relu(self.dec2b(torch.relu(self.dec2a(torch.cat([u2, e1], dim=1)))))
return self.head(d2) # 1x24x24 logitA modell célja a rétegspecifikáció ellenőrzése, nem a tanítás. A
paraméterszám is kiszámítható a rétegenkénti
összegből; a fenti modulra ez
.
Ez magában foglalja a bias-eket is. A dec1a bemeneti
csatornája azért
,
mert a concat után
csatorna érkezik; a dec2a bemenete pedig
.
A labor célja annak igazolása, hogy a lemmában szereplő minden térbeli alak és concat-csatornaszám ténylegesen megvalósul a kódban. A következő ellenőrzőkód közvetlenül a fenti modul után illeszthető:
if __name__ == "__main__":
torch.manual_seed(20261020)
device = torch.device("cpu")
model = TinyUNet(in_channels=1, out_channels=1, base=8).to(device)
model = model.to(torch.float32)
x = torch.randn(2, 1, 24, 24, dtype=torch.float32, device=device)
with torch.no_grad():
model.eval()
y = model(x)
print("input", tuple(x.shape))
print("output", tuple(y.shape))
print("parameters", sum(p.numel() for p in model.parameters()))
assert tuple(y.shape) == (2, 1, 24, 24)
assert sum(p.numel() for p in model.parameters()) == 29321Futtatás a projektkönyvtárból:
python tiny_unet_shapecheck.pyVárt, számított kimenet — ez nem mért futás, hanem a fenti képletekből és a paraméterszám-összegzésből származó ellenőrzött érték; a kódot a hallgató a saját környezetében futtatja:
input (2, 1, 24, 24)
output (2, 1, 24, 24)
parameters 29321
Az eredmény értelmezése: a kimenet
,
tehát a hálózat valóban dense predictiont ad: minden bemeneti pixelhez
egy logit tartozik. A model.eval() hívás itt azért
szerepel, hogy a kiértékelési mód szokását rögzítsük; a modulban nincs
BatchNorm vagy dropout, így a train/eval viselkedés most numerikusan nem
tér el. A tanításhoz a maszkot float32 típusra konvertálva
torch.nn.functional.binary_cross_entropy_with_logits(logits, mask_float)
hívható; a logitokból a döntési maszk
szabállyal áll elő, a 23.2. Kalibráció és döntési
küszöb alfejezetben tárgyalt küszöb–kalibráció szétválasztás
jegyében.
F1 (fogalmi, nehézség 1). Adja meg két konkrét
különbséget a bilineáris F.interpolate-alapú upsampling és
a ConvTranspose2d-alapú upsampling között a tanulhatóság és
az adjungált jelleg szempontjából. Előfeltétel: 15.3. Transposed convolution mint adjungált.
Átadandó: legfeljebb fél oldalas saját válasz, amely mindkét állítást a
jelen modul példájára vonatkoztatja.
F2 (levezetési, nehézség 2). Vezesse le a fenti TinyUNet rétegalakjait bemenetre. Hol sérül a spatial alignment, és mekkora a második upsampling kimenete? Előfeltétel: a 3. szakasz formulái. Átadandó: a pool- és upsampling-méretek lépésenként, a szükséges crop mérete, valamint annak kimondása, hogy a kimenet most nem .
F3 (implementációs, nehézség 1). Írjon
center_crop(x, target_h, target_w) függvényt, amely az
utolsó két dimenziót a megadott méretre vágja középre, és tesztelje
,
target_h=target_w=22 bemenetre. Átadandó: kód, futtatási
parancs és az ellenőrzött kimeneti alak.
F4 (hibakeresési, nehézség 2). A következő modulrészlet az első decoder szint definíciója:
self.up1 = nn.ConvTranspose2d(32, 16, 2, stride=2)
self.dec1a = nn.Conv2d(16, 16, 3, padding=1)
# ...
d1 = torch.cat([self.up1(b), e2], dim=1)
d1 = torch.relu(self.dec1a(d1))Mi a hiba, mi a közvetlen következménye, és hogyan javítható? Átadandó: ok, következmény, a javított sor és a visszaellenőrzés módja.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen
GlyphLab-kísérletet, amely a skip-kapcsolatok hatását vizsgálja a
szegmentálásra. A cél: ugyanaz a TinyUNet skip-pel és skip nélkül. A
maszk a data/masks.npy előtér-maszkja. Adjon meg:
megfigyelési egység, split, baseline, változó tényező, loss, metrika, a
választás szabálya és a következtetés határa. Előfeltétel: 27.1. Pixelszintű klasszifikáció és FCN, 1.3. Adatfelosztás és a megfigyelési egység.
Átadandó: protokoll, nem várt győztes.
F1 megoldás. A bilineáris interpoláció paraméter
nélküli, determinisztikus leképezés: a kimeneti érték a bemeneti értékek
rögzített súlyozott átlaga, ezért a hálózat a tanítás során nem tudja a
feladathoz igazítani. A ConvTranspose2d ezzel szemben
tanulható kernelű lineáris leképezés. Második különbség: a
ConvTranspose2d a megfelelő konvolúciós lineáris operátor
adjungáltja, tehát a forward képlete a 15.3.
Transposed convolution mint adjungált alfejezetben levezetett
scatter-add szerkezetű; a bilineáris interpoláció nem ilyen adjungált
szerkezetű. A jelen példában a up1
csatornás tanulható leképezés, miközben a bilineáris változat legfeljebb
a meglévő csatornákat interpolálná, csatornát nem keverne.
F2 megoldás. esetén a kódolási lépések:
Az első upsampling -ből
méretet állít elő, a skip viszont , tehát pixel eltérés van: a skip-ből -re kell vágni. A concat csatornaszáma változatlanul . A második upsampling -ből -at állít elő, a skip itt , tehát pixelnyi eltérés van. A kimenet ezért , nem . A pontos crop-policy implementációfüggő; a levezetés állítása csak a méreteltérés és a kimeneti felbontás. Ez mutatja a lemma feltételének szerepét: a -es bemenet azért igazodik, mert egész.
F3 megoldás.
def center_crop(x, target_h, target_w):
h, w = x.shape[-2:]
top = (h - target_h) // 2
left = (w - target_w) // 2
return x[..., top:top + target_h, left:left + target_w]
# teszt
import torch
x = torch.zeros(2, 8, 23, 23)
y = center_crop(x, 22, 22)
assert tuple(y.shape) == (2, 8, 22, 22)A függvény a top, left levágással a felesleget a
bal/felső és jobb/alsó oldal között osztja el;
-nél
(23-22)//2=0, tehát a jobb/alsó egy pixelt elveszít. A
függvény a feladat specifikációjának megfelel; ha szigorúan szimmetrikus
kerekítés kell, azt a feladatban külön definiálni kell.
F4 megoldás. A hiba oka: a
cat([up1(b), e2], dim=1) kimenete
csatornás, mert az upsampling
,
a skip
csatornát ad össze. A dec1a azonban
bemeneti csatornát deklarál. Következmény: a PyTorch a
Conv2d bemeneti csatorna-ellenőrzésénél
RuntimeError-t ad, mert a várt
helyett
csatorna érkezik. Javítás:
self.dec1a = nn.Conv2d(32, 16, 3, padding=1), vagy
4 * base jelöléssel
self.dec1a = nn.Conv2d(4 * base, 2 * base, 3, padding=1).
Visszaellenőrzés: a 4. szakasz kódjának assert-jein a
kimenet
és a paraméterszám
marad, mert a javított rétegnek
paramétere van.
F5 megoldásminta. Kérdés: a skip-kapcsolatok
javítják-e a GlyphLab előtér-szegmentálást? Megfigyelési egység:
scene_id, mert egy jelenet két nézete függő; a
train/val/test split a GlyphLab szerződés szerint jelenetszintű.
Bemenet: data/images.npy float32
;
cél: data/masks.npy uint8
.
Baseline: ugyanaz a TinyUNet, amelyben a két
cat-et eltávolítjuk — a skip nélküli decoder csak a
bottleneckből építkezik. Változó tényező: skip megléte. Loss:
binary_cross_entropy_with_logits, a maszk
float32-vé konvertálva. Metrika: threshold
melletti pixel-IoU képenként, majd a jelenetszintű aggregálás a 27.3. Dice, IoU és aggregálás alfejezetben
tárgyalandó módon; a képenkénti IoU-kat itt még nem szabad független
mintaként kezelni. Választási szabály: a legjobb kísérletenkénti
checkpointot a validációs loss választja, a test-halmazt csak a végleges
modellpár kiválasztása után értékeljük. Erőforrás: CPU, float32,
rögzített seed. Következtetési határ: a GlyphLab szintetikus, három
orientációhoz tartozó alakzatmaszk; az eredmény nem általános
természetes képi szegmentálási validáció, és a skip hatása csak ezen az
adaton, ezzel a protokollal állítható.
A gyakori tévedés az, hogy a fenti TinyUNet-et az
eredeti U-Net reprodukciójának nevezzük. A jelen modul U-Net-szerű
tanulópélda: van encoder, decoder és skip concat, de az eredeti, [P14]
által hivatkozott U-Net orvosbiológiai képszegmentálásra készült, eltérő
bemeneti felbontással, mélységgel, csatornaszámmal és tanítási
eljárással. Az a kijelentés, hogy „implementáltuk a U-Net cikket”, csak
akkor lenne igazolható, ha a modell, az adat, a loss, az augmentáció és
a kiértékelési metrika a hivatkozott közleményben leírt protokollnak
felelne meg; ezek jelen kéziratban nincsenek reprodukálva. A mi
ellenpéldánk a konkrét adatszerződés: a GlyphLab
darab
-es
szintetikus kép, míg a [P14] absztraktja orvosbiológiai szegmentálási
felhasználásról szól. A két állítás közötti különbség nem pusztán
méretbeli, hanem a reprodukciós állítás típusában van.
Önellenőrzés: minden
-as,
konvolúció megtartja a térbeli méretet? A kódban minden
decoder-Conv2d is padding=1? A két
cat valóban dim=1-en történik? A
dec1a bemeneti csatornája
,
a dec2a bemenete
?
Ha a válasz bármelyikre nem, a paraméterszám-ellenőrzés vagy a
shape-assert el fog csúszni.
Visszahívás a 15.2. Inputgradiens és átfedő recepciós mezők alfejezetből: a transzponált konvolúció upsamplingjének magja ugyanaz a scatter-add gondolat, mint az inputgradiens számításakor. A , esetben egy bemeneti pixel a kimeneti , pozíciókhoz járul hozzá; ezért a kimenet lesz. A skip concat ezt a magasabb felbontású, kódoló oldali aktivációval egészíti ki, de nem változtat a scatter-add indexfeltételén.
A U-Net encoder–decoder szerkezet: a kódoló konvolúciókkal és max-poollal növeli a csatornaszámot, miközben csökkenti a térbeli felbontást; a dekódoló tanulható transzponált konvolúciókkal növeli a felbontást. A skip concat a kódoló azonos felbontású aktivációját csatorna szerint fűzi a dekódolóhoz, így a korai térbeli részletek nem csak a bottlenecken át jutnak a kimenetre. A lemmában levezettük a -es bemenet minden rétegalakját: , a concat-csatornaszámok és . A -es ellenpélda megmutatta, hogy a pool és upsampling méretezése miatt crop válhat szükségessé. A paraméteres modul a szerződésnek megfelelő rétegspecifikáció.
A 27.3. Dice, IoU és aggregálás egységben a szegmentálás értékelését építjük fel: a Dice-együttható és az IoU definícióját, a per-image, per-scene és micro aggregálás különbségét, valamint az üres maszkok és a küszöbválasztás hatását. A mostani Tensor-logit maszk az ottani metrikák közvetlen bemenete.