39.2 Encoder–decoder vagy lokalizációs fej

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

A 39.1. Feladat- és annotációs döntés egységben rögzítettük a projekt annotációs döntését. Ebben az alfejezetben a szegmentáló főutat építjük meg: az encoder–decoder törzs után egy spatial head állítja elő a pixelszintű logit-maszkot, és a célmaszkkal konzisztens loss redukcióját vezetjük le. Előismeret a 27.2. U-Net, encoder–decoder és skip U-Net-alakok és a 38.2. Modell, training és best checkpoint tanítási szerződés; a 35.2. Magyarázat sanity check magyarázat-ellenőrzésből csak annyit használunk, hogy minden kimeneti állítást a modell tényleges számítási útjához kötünk.

Induló kérdés: miért nem elég egy Linear(576, 576) réteg, ha a kép és a maszk egyaránt 576=24⋅24576=24\cdot 24 pixel? A válasz a térbeli indexelésben és a paramétermegosztásban van: a kép pontjainak sorrendje hordozza a geometriát, egy teljesen összekötött réteg ezt a sorrendet el tudja rontani, a konvolúciós fej viszont megtartja.

39.2.2 A spatial head és a választott modell

Legyen a dekódoló utolsó jellemzőtérképe

F∈ℝN×Cf×Hf×Wf.F\in\mathbb R^{N\times C_f\times H_f\times W_f}.

A bináris szegmentáló fej egy 1×11\times1-es konvolúció, amely minden térbeli pozícióban azonos affin leképezést alkalmaz a csatornavektorra:

Zn,1,i,j=b+∑c=1CfKcFn,c,i,j,Z_{n,1,i,j}=b+\sum_{c=1}^{C_f}K_{c}\,F_{n,c,i,j},

ahol K∈ℝ1×Cf×1×1K\in\mathbb R^{1\times C_f\times1\times1} és b∈ℝb\in\mathbb R. A kimenet egyetlen logitcsatorna, mert egy előtérosztályunk van:

Z∈ℝN×1×H×W.Z\in\mathbb R^{N\times1\times H\times W}.

A cél ugyanilyen térbeli alakú bináris maszk:

Y∈{0,1}N×1×H×W.Y\in\{0,1\}^{N\times1\times H\times W}.

A főútban H=W=24H=W=24, mert a GlyphLab maszkjai natív 24×2424\times24 felbontásúak. A választott kis modell:

Lépés Réteg Kimenet alakja
bemenet XX (N,1,24,24)(N,1,24,24)
enc1 Conv2d 1→81\to8, k=3k=3, p=1p=1 + ReLU (N,8,24,24)(N,8,24,24)
pool1 MaxPool2d(2) (N,8,12,12)(N,8,12,12)
enc2 Conv2d 8→168\to16, k=3k=3, p=1p=1 + ReLU (N,16,12,12)(N,16,12,12)
pool2 MaxPool2d(2) (N,16,6,6)(N,16,6,6)
bottleneck Conv2d 16→3216\to32, k=3k=3, p=1p=1 + ReLU (N,32,6,6)(N,32,6,6)
up Upsample(size=(24,24), bilinear) (N,32,24,24)(N,32,24,24)
concat az up és enc1 csatorna szerint (N,40,24,24)(N,40,24,24)
dec Conv2d 40→1640\to16, k=3k=3, p=1p=1 + ReLU (N,16,24,24)(N,16,24,24)
head Conv2d 16→116\to1, k=1k=1 (N,1,24,24)(N,1,24,24)

A paraméterszám csak konvolúciós súly és bias: 80+1168+4640+5776+17=1168180+1168+4640+5776+17=11681; az upsamplingnek és a poolingnak nincs paramétere. A lokalizációs alternatíva ebben az egységben nem a főút: ahhoz a head például dobozparaméteres térképet vagy egy utólagos regresszorfejet adhatna. Az itteni szegmentáló fej önálló oktatási példa, nem a [D18] dokumentáció detektálási receptjének reprodukciója.

39.2.3 A kimeneti alak levezetése

A GlyphLab-kép 24×2424\times24-es. Az általános szimmetrikus paddinges Conv2d-alakképlet:

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

A k=3k=3, p=1p=1, s=1s=1, d=1d=1 esetben Henc1=24H_{enc1}=24. A k=2k=2, s=2s=2 MaxPool2d esetében p=0p=0 és d=1d=1 mellett Hpool1=12H_{pool1}=12, majd Hpool2=6H_{pool2}=6. A bottleneck tehát 6×66\times6-os, 3232 csatornával.

A dekódoló upsampling lépése explicit méretet kap: Upsample(size=(24,24), mode="bilinear", align_corners=False). Bilineáris átmintavételezésnél az align_corners=False konvenció félegységnyi eltolással helyezi a kimeneti rácspontokat a forrásrácsra; a size=(24,24) argumentum azonban nem hagy kerekítési szabadságot, a kimenet térbeli alakja pontosan a kért 24×2424\times24. A kódoló első aktivációja is 24×2424\times24-es, ezért a csatorna szerinti összefűzés jól definiált, és a csatornaszám 32+8=4032+8=40. A dekódoló k=3k=3, p=1p=1 konvolúciója ismét megőrzi a 24×2424\times24-et, a fej 1×11\times1-es konvolúciója pedig nem változtat a térbeli alakon. A végső logit-maszk így ugyanazon a rácson értelmezett, mint a célmaszk.

39.2.4 A loss nevezőjének levezetése

Minden (n,p)(n,p) pixelre, ahol p=(i,j)p=(i,j) fut a H×WH\times W rácson, a logit zn,p∈ℝz_{n,p}\in\mathbb R, a címke yn,p∈{0,1}y_{n,p}\in\{0,1\}. A pixel feltételes modellje Bernoulli:

p(yn,p∣zn,p)=σ(zn,p)yn,p(1−σ(zn,p))1−yn,p,p(y_{n,p}\mid z_{n,p})=\sigma(z_{n,p})^{y_{n,p}}\bigl(1-\sigma(z_{n,p})\bigr)^{1-y_{n,p}},

ahol σ(z)=1/(1+e−z)\sigma(z)=1/(1+e^{-z}). A negatív log-likelihood az azonos alakú, nem redukált BCE:

$$L_{n,p}=-\Bigl[y_{n,p}\log\sigma(z_{n,p})+(1-y_{n,p})\log\bigl(1-\sigma(z_{n,p})\bigr)\Bigr].$$

A kiválasztott empirikus loss rögzített térbeli nevezővel átlagol:

ℒ=1P∑n=1N∑p=1HWLn,p,P=N⋅1⋅H⋅W=NHW.\mathcal L=\frac{1}{P}\sum_{n=1}^{N}\sum_{p=1}^{HW}L_{n,p},\qquad P=N\cdot 1\cdot H\cdot W=NHW.

A PP nevező akkor is pozitív egész, ha egy képen egyáltalán nincs előtérpixel. Ez a kulcs az üres cél kezeléséhez: ha a normalizálást a foreground-pixelek számához kötnénk, üres maszknál nullával osztanánk. Itt ehelyett az üres cél is értelmes, véges tanulási jelet ad: minden pixelre y=0y=0, így a loss a háttérlogitek lenyomására tanít.

A gradiens levezetése egy pixelre a logisztikus függvény deriváltját használja. Mivel σ′(z)=σ(z)(1−σ(z))\sigma'(z)=\sigma(z)(1-\sigma(z)),

∂Ln,p∂zn,p=−yn,p(1−σ(zn,p))+(1−yn,p)σ(zn,p)=σ(zn,p)−yn,p.\frac{\partial L_{n,p}}{\partial z_{n,p}}=-y_{n,p}\bigl(1-\sigma(z_{n,p})\bigr)+(1-y_{n,p})\sigma(z_{n,p})=\sigma(z_{n,p})-y_{n,p}.

Az átlagredukció után tehát

∂ℒ∂zn,p=1NHW(σ(zn,p)−yn,p).\frac{\partial\mathcal L}{\partial z_{n,p}}=\frac{1}{NHW}\bigl(\sigma(z_{n,p})-y_{n,p}\bigr).

A BCEWithLogitsLoss(reduction="none") ezt a nem redukált LL-t adja vissza (N,1,H,W)(N,1,H,W) alakban, majd a .mean() implicit módon P=NHWP=NHW-vel oszt. Numerikusan a szigmoid és a logaritmus külön hívása instabil; a könyvtári függvény a logitot közvetlenül fogadja, ezért a saját kódban külön sigmoid-hívást nem végzünk.

39.2.5 Maszk-interpoláció: mit szabad és mit nem

A logit- vagy jellemzőtérkép bilineáris felnagyítása a tanuló modell része, ezért folytonos értékei a gradiensszámításban jól definiáltak. A bináris célmaszk bilineáris interpolálása viszont nem konzisztens címkeképzés: az 11-es előtér és a 00-ás háttér közötti élen olyan értékek keletkeznek, amelyek sem nem megfigyelt maszk, sem nem valószínűségi súly. Ha a cél felbontását mégis változtatni kell, a maszkra nearest-neighbor mintavételt használunk, például a páros indexű pixelek kiválasztását. A főútban erre nincs szükség: a modell kimenete a natív 24×2424\times24-es célrácsra illeszkedik.

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

A kimenet és a target azonos pixelszáma nem elég, ha a térbeli illesztés hibás. Legyen 2×22\times2-es logit-maszk

Z=[5−5−5−5],Z=\begin{bmatrix}5&-5\\-5&-5\end{bmatrix},

és a helyes cél

Y=[1000].Y=\begin{bmatrix}1&0\\0&0\end{bmatrix}.

Mindkét célváltozatban egyetlen előtérpixel van, tehát a pixelszám azonos. A helyes illesztésnél a négy pixel vesztesége:

log(1+e−5)+3log(1+e5)≈0,0067+3⋅5,0067=15,0268.\log(1+e^{-5})+3\log(1+e^{5})\approx 0{,}0067+3\cdot 5{,}0067=15{,}0268.

Ha a cél elcsúszik,

Y′=[0010],Y'=\begin{bmatrix}0&0\\1&0\end{bmatrix},

akkor mind a négy pixel rosszul illeszkedik, és a veszteség

4log(1+e5)≈20,0268.4\log(1+e^{5})\approx20{,}0268.

Az eltérés abból fakad, hogy a nagy pozitív logit a háttércímkével, a nagy negatív logit pedig az előtércímkével találkozik. Az AssertionError-t nem vált ki egy shape-ellenőrzés, mert mindkét eset (1,1,2,2); csak a veszteség vagy a Dice-kontroll mutatja ki a hibát.

39.2.7 Minimális PyTorch-megvalósítás és sanity check

A Projekt III összes szkriptje a lab/project3 munkakönyvtárban van (ott készült a 39.1. Feladat- és annotációs döntés annotation_spec.py-ja is); az adatok a GlyphLab lab/data/ könyvtárából töltődnek. Az első szkript a modellt és a loss-szerződést ellenőrzi egy rögzített kis batch-en.

# lab/project3/segmentation_head.py
from __future__ import annotations
import csv
from pathlib import Path

import numpy as np
import torch
from torch import nn
import torch.nn.functional as F

DATA = Path(__file__).resolve().parents[1] / "data"
torch.set_num_threads(1)   # egyszálas CPU: a futás a magszámtól függetlenül ismételhető


class SmallSegUNet(nn.Module):
    """GlyphLab 24x24-es maszkfej; a kimenet (N,1,24,24) logit."""

    def __init__(self, in_ch: int = 1, out_ch: int = 1, base: int = 8) -> None:
        super().__init__()
        self.enc1 = nn.Sequential(
            nn.Conv2d(in_ch, base, 3, padding=1), nn.ReLU()
        )
        self.pool = nn.MaxPool2d(2)
        self.enc2 = nn.Sequential(
            nn.Conv2d(base, 2 * base, 3, padding=1), nn.ReLU()
        )
        self.bottleneck = nn.Sequential(
            nn.Conv2d(2 * base, 4 * base, 3, padding=1), nn.ReLU()
        )
        self.up = nn.Upsample(size=(24, 24), mode="bilinear", align_corners=False)
        self.dec = nn.Sequential(
            nn.Conv2d(4 * base + base, 2 * base, 3, padding=1), nn.ReLU()
        )
        self.head = nn.Conv2d(2 * base, out_ch, 1)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        e1 = self.enc1(x)          # (N,8,24,24)
        p1 = self.pool(e1)         # (N,8,12,12)
        e2 = self.enc2(p1)         # (N,16,12,12)
        p2 = self.pool(e2)         # (N,16,6,6)
        b = self.bottleneck(p2)    # (N,32,6,6)
        u = self.up(b)             # (N,32,24,24)
        c = torch.cat([u, e1], dim=1)  # (N,40,24,24)
        d = self.dec(c)            # (N,16,24,24)
        return self.head(d)        # (N,1,24,24)


def bce_fixed_denominator(logits: torch.Tensor, targets: torch.Tensor) -> torch.Tensor:
    """Pixel-BCE, a nevező mindig N*1*H*W; üres target esetén is stabil."""
    per_pixel = F.binary_cross_entropy_with_logits(
        logits, targets, reduction="none"
    )
    return per_pixel.mean()


def load_train_batch(n: int = 8) -> tuple[torch.Tensor, torch.Tensor]:
    images = np.load(DATA / "images.npy")           # (360,1,24,24) float32
    masks = np.load(DATA / "masks.npy").astype(np.float32)
    with open(DATA / "metadata.csv", newline="") as f:
        rows = list(csv.DictReader(f))
    idx = [int(r["index"]) for r in rows if r["split"] == "train"][:n]
    return torch.from_numpy(images[idx]), torch.from_numpy(masks[idx])


def main() -> None:
    torch.manual_seed(20261020)
    device = torch.device("cpu")
    model = SmallSegUNet().to(device)
    x, y = load_train_batch(8)
    x, y = x.to(device), y.to(device)

    # 1. Alakellenőrzés.
    with torch.no_grad():
        z0 = model(x)
    assert z0.shape == (8, 1, 24, 24), z0.shape

    # 2. BCE numerikus kontroll egy pixelre: z=0.5, y=1 esetén
    #    log(1+exp(-0.5)) ~ 0.474077.
    one = torch.tensor([[[[0.5]]]], dtype=torch.float32, device=device)
    target_one = torch.ones_like(one)
    loss_px = float(F.binary_cross_entropy_with_logits(one, target_one))
    assert abs(loss_px - 0.474077) < 1e-5, loss_px

    # 3. Üres cél: véges, gradiense nem NaN. A z0 no_grad alatt készült,
    #    ezért a backwardhoz új, gráffal rendelkező forward kell.
    empty = torch.zeros_like(x)
    loss_empty = bce_fixed_denominator(model(x), empty)
    assert torch.isfinite(loss_empty), loss_empty
    loss_empty.backward()
    assert all(torch.isfinite(p.grad).all() for p in model.parameters() if p.grad is not None)

    # 4. Kis túlillesztési sanity check: a loss csökken a rögzített batch-en.
    model.train()
    opt = torch.optim.Adam(model.parameters(), lr=1e-3)
    losses: list[float] = []
    for _ in range(60):
        opt.zero_grad()
        loss = bce_fixed_denominator(model(x), y)
        loss.backward()
        opt.step()
        losses.append(loss.item())
    assert torch.isfinite(torch.tensor(losses)).all()
    assert losses[-1] < losses[0], f"{losses[0]:.4f} -> {losses[-1]:.4f}"
    print(f"loss {losses[0]:.4f} -> {losses[-1]:.4f} (CPU, 8 train kép)")


if __name__ == "__main__":
    main()

Futtatási parancs:

cd lab/project3
python segmentation_head.py

Az ellenőrzés értelmezése: ha a 2. numerikus egyezés megvan, a BCE-szerződés helyes; ha a 4. lépésben a loss csökken, a térbeli illesztés és a gradiensút összeállt a batch-en. A loss kezdeti értéke a maszk előfordulási arányától függ, ezért önmagában nem viszonyítási alap. A valós kiértékelés a 39.3. Geometriai kontroll és metrika metrikájával, a legjobb checkpointot pedig a 38.2. Modell, training és best checkpoint szabálya szerint a validációs loss alapján végezzük; a corrupt tesztet csak a protokoll végén értékeljük.

A lab-profilban (CPU, PyTorch 2.14.1+cpu) a szkript minden assert-en átment, és loss 0.5897 -> 0.1252 (CPU, 8 train kép) sort írt.

39.2.8 A főág tanítása és a best checkpoint

A sanity check után a főág a teljes train spliten tanít, és a 38.2. Modell, training és best checkpoint szabálya szerint a legkisebb validációs BCE-jű epoch állapotát menti. A teszt itt nem nyílik meg; a tesztkiértékelés és az export a 39.4. Alkalmazási riport és átadás alfejezetben történik, ebből a checkpointból.

# lab/project3/train_seg.py — n154 főág: SmallSegUNet tanítása, best checkpoint val BCE alapján.
import csv, json, time
from pathlib import Path

import numpy as np
import torch

from segmentation_head import DATA, SmallSegUNet, bce_fixed_denominator

HERE = Path(__file__).resolve().parent
SEED, EPOCHS, BATCH, LR = 20261020, 30, 16, 1e-3


def split_ids(split):
    with open(DATA / "metadata.csv", newline="") as f:
        return np.array([int(r["index"]) for r in csv.DictReader(f) if r["split"] == split])


def main():
    torch.manual_seed(SEED)
    images = torch.from_numpy(np.load(DATA / "images.npy"))                    # float32
    masks = torch.from_numpy(np.load(DATA / "masks.npy").astype(np.float32))   # {0,1}
    tr, va = split_ids("train"), split_ids("val")       # a teszt itt nem nyílik meg
    model = SmallSegUNet()
    opt = torch.optim.Adam(model.parameters(), lr=LR)
    g = torch.Generator().manual_seed(SEED)
    (HERE / "checkpoints").mkdir(exist_ok=True)
    best, log = {"val_loss": float("inf")}, []
    for epoch in range(1, EPOCHS + 1):
        model.train()
        perm = tr[torch.randperm(len(tr), generator=g).numpy()]
        for i in range(0, len(perm), BATCH):
            b = perm[i:i + BATCH]
            opt.zero_grad(set_to_none=True)
            loss = bce_fixed_denominator(model(images[b]), masks[b])
            loss.backward()
            opt.step()
        model.eval()
        with torch.no_grad():
            val_loss = float(bce_fixed_denominator(model(images[va]), masks[va]))
        log.append({"epoch": epoch, "train_last_batch_loss": loss.item(), "val_loss": val_loss})
        if val_loss < best["val_loss"]:
            best = {"epoch": epoch, "val_loss": val_loss}
            torch.save({"model_state_dict": model.state_dict(), "arch": "SmallSegUNet",
                        "epoch": epoch, "val_loss": val_loss, "seed": SEED,
                        "params": sum(p.numel() for p in model.parameters()),
                        "threshold": 0.5}, HERE / "checkpoints" / "seg_best.pt")
    (HERE / "checkpoints" / "seg_train_log.json").write_text(json.dumps(log, indent=1))
    print(f"best epoch {best['epoch']}, val BCE {best['val_loss']:.4f} -> checkpoints/seg_best.pt")


if __name__ == "__main__":
    main()
cd lab/project3
python train_seg.py

A kimenet a checkpoints/seg_best.pt (kulcsai: model_state_dict, arch, epoch, val_loss, seed, params, threshold) és a checkpoints/seg_train_log.json epochnapló. A küszöb (0,50{,}5 a szigmoid kimenetén) a checkpoint része, így a későbbi export nem választhat más küszöböt a teszt ismeretében. Futási jegyzőkönyv: a lab-profilban a tanítás körülbelül 4 másodpercig tartott, a legjobb epoch a 30., a validációs BCE 0,00290{,}0029; két futás azonos checkpointot adott.

39.2.9 Feladatok

F1 (fogalmi, nehézség 1). Indokolja meg, miért egycsatornás a választott spatial head kimenete, és adjon ellenpéldát arra, hogy egy nn.Flatten() + nn.Linear(576, 576) miért nem helyettesíti a fejet akkor sem, ha a pixelszám egyezik.

F2 (levezetési, nehézség 2). Legyen N=2N=2, H=W=3H=W=3, C=1C=1. Az első kép célja teljesen üres, a második kép (0,0)(0,0) pixelén y=1y=1. Írja fel a loss nevezőjét, a teljes ℒ\mathcal L összegzési határait, és a gradiens értékét a két kijelölt pixelre. Adja meg az autograddal konzisztens skálatényezőt is.

F3 (implementációs, nehézség 2). Készítsen nearest_target_half(y) függvényt, amely egy (N,1,H,W) alakú uint8 maszkból a páros indexű pixelek kiválasztásával (N,1,H//2,W//2) alakot állít elő. Páratlan HH vagy WW esetén emeljen ValueError-t. Tesztelje az

[1000000000110011]\begin{bmatrix}1&0&0&0\\0&0&0&0\\0&0&1&1\\0&0&1&1\end{bmatrix}

bemenetre az elvárt

[1001]\begin{bmatrix}1&0\\0&1\end{bmatrix}

kimenetet.

F4 (hibakeresési, nehézség 1). Egy hallgató a target maszkot F.interpolate(target_float, size=(24,24), mode="bilinear") módon skálázza, majd ezt adja a BCE-nek. A training hiba nélkül lefut, a validációs Dice mégis rossz. Nevezze meg a hiba következményét a célértékekre, adjon kimutatási módszert és javítást.

F5 (kísérleti/transzfer, nehézség 3). Tegyük fel, hogy egy új adatkészlet 128×128128\times128-as maszkokkal érkezik, de a modell dekódolója változatlanul 24×2424\times24 logitot ad. Tervezzen kis összehasonlító kísérletet két ágra: (A) a logitot bilineárisan 128×128128\times128-ra nagyítjuk, és ott számítjuk a BCE-t; (B) a célmaszkot nearest módon 24×2424\times24-re csökkentjük. Adja meg a megfigyelési egységet, a splitet, a baseline-t, a metrikát, a két loss nevezőjét, valamint a toy kísérlet érvényességi korlátját.

39.2.10 Megoldások és indoklás

F1. A fej kimenete egy logitcsatorna, mert a bináris szegmentálás minden pixelre egyetlen előtér-valószínűségi logitot igényel: Z∈ℝN×1×H×WZ\in\mathbb R^{N\times1\times H\times W}. A lineáris 576→576 réteg egy oszlopfolytonossági konvenciót égetne a súlyokba, megszüntetné a pixelpozíciók közötti paramétermegosztást, és a célmaszk eltérő sorrendbe lapítása azonos pixelszámmal is más veszteséget adna, ahogy a fenti YY és Y′Y' példa mutatja. A 1×11\times1-es konvolúció ezzel szemben pozíciófüggetlen affin fej, és a térbeli indexelést a bemeneti rácstól örökli.

F2. A logit (2,1,3,3), a cél ugyanilyen. A pixelek száma képenként HW=9HW=9, így a nevező P=NHW=18P=NHW=18. A loss

ℒ=118∑n=01∑i=02∑j=02Ln,1,i,j.\mathcal L=\frac{1}{18}\sum_{n=0}^{1}\sum_{i=0}^{2}\sum_{j=0}^{2}L_{n,1,i,j}.

Az első kép üres, de a nevező változatlan. A (0,0,0)(0,0,0) pixelre y=0y=0, ezért a gradiens σ(z0,0,0)/18\sigma(z_{0,0,0})/18. A második kép (0,0)(0,0) pixelére y=1y=1, ezért a gradiens (σ(z1,0,0)−1)/18(\sigma(z_{1,0,0})-1)/18. Az autograd ugyanezt a skálát adja, mert a mean() redukció a teljes (N,1,H,W) tenzorelemszámot használja.

F3. Egy lehetséges megoldás:

def nearest_target_half(y: np.ndarray) -> np.ndarray:
    if y.ndim != 4 or y.shape[1] != 1:
        raise ValueError("várt alak: (N,1,H,W)")
    _, _, h, w = y.shape
    if h % 2 or w % 2:
        raise ValueError(f"páratlan méret: ({h},{w})")
    return y[:, :, 0:h:2, 0:w:2]

A megadott bemenetre a kiválasztott indexek (0,0)(0,0), (0,2)(0,2), (2,0)(2,0), (2,2)(2,2), azaz 1,0,0,11,0,0,1; az eredmény 1001\begin{smallmatrix}1&0\\0&1\end{smallmatrix}. Ellenőrzés: np.testing.assert_array_equal a várt (1,1,2,2) tömbbel, és pytest.raises(ValueError) a 5×45\times4-es bemenetre.

F4. A bilineáris interpoláció a bináris célon 00 és 11 közötti értékeket hoz létre az előtér–háttér átmeneteknél, tehát a BCE soft labelként értelmezi azokat. A hiba nem vált ki RuntimeError-t, mert a BCE a [0,1][0,1] tartományt elfogadja, viszont a tanulási cél már nem a megfigyelt bináris maszk. Kimutatás: egy éles maszkon az interpolált tenzor unique() értékeinek megjelenítése; a bináris kontroll csak {0,1} elemeket enged meg. Javítás: a targetet ne interpoláljuk, hanem a modell kimenetét igazítsuk a cél natív felbontásához, vagy ha elkerülhetetlen, nearest módon mintavételezzük a címkét. Visszaellenőrzés a validációs Dice újraszámításával és a target min/max értékével.

F5. Kiinduló hipotézis: a target eredeti 128×128128\times128-as rácsán mért veszteség pontosabban bünteti a határhibákat, de az (A) ág nagyobb számítási igényű. Megfigyelési egység a scene_id, a splitet scene-szinten kell tartani. Fix modell, seed és training; baseline a teljes háttér predikciója. A metrika a legjobb validációs checkpoint után a 128×128128\times128-as rácson számított thresholdolt Dice/IoU. Az (A) veszteség nevezője N⋅1⋅128⋅128N\cdot1\cdot128\cdot128, a (B) nevezője N⋅1⋅24⋅24N\cdot1\cdot24\cdot24. A két ágat azonos seeddel tanítjuk, a végső tesztet páros scene-párokon vetjük össze. A toy kísérlet nem bizonyít általános interpolációs tételt, és egyetlen seed nem hordozhatósági garancia.

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

A 27.2. U-Net, encoder–decoder és skip levezetéséhez hasonlóan minden egyes rétegre felírtuk a térbeli alakot; a concat előtt ellenőriztük, hogy a két ág HH és WW mérete azonos. A 38.2. Modell, training és best checkpoint tanítási szerződéséből megtartottuk, hogy a validáció választ checkpointot, a tesztet csak a rögzített protokoll végén értékeljük. A 35.2. Magyarázat sanity check tanulsága itt annyi, hogy a kimeneti maszk a modell számítási útjának eredménye, nem egy külső magyarázat; a térbeli illesztést éppen ezért a loss és a metrika szintjén kell ellenőrizni, nem pusztán a shape-ek összehasonlításával.

39.2.12 Rövid összefoglaló

A szegmentáló fej 1×11\times1-es konvolúcióból áll, és pixelre lebontott logit-maszkot ad; a GlyphLab főútban a bemenettel azonos 24×2424\times24-es célrácsra illeszkedik. A skip concat és a bilineáris upsampling alakhelyességét rétegenként vezettük le. A loss nem redukált BCEWithLogits, majd a teljes N⋅1⋅H⋅WN\cdot1\cdot H\cdot W pixelszámmal átlagolunk; így az üres cél sem okoz nullával osztást. A célmaszkot nem interpoláljuk bilineárisan, mert a bináris címkéből hamis soft label lesz. A sanity check a shape-t, a BCE kis numerikus értékét és a loss csökkenését ellenőrzi kis CPU-futásban.

39.2.13 Következő alkalmazás és felhasznált források

A 39.3. Geometriai kontroll és metrika egységben a betanított fej kimenetére geometriai kontrollt és szegmentálási metrikát vezetünk be: a thresholdolt maszk Dice/IoU kiértékelését, a per-image, per-scene és micro aggregálást, valamint a páros corrupt tesztre vonatkozó összevetést.