A 11.2. nn.Module és paraméterregisztráció
alfejezetben a modell paramétereit már regisztráltuk, és a
state_dict menthetővé vált. Most azt az adatutat építjük
meg, amely a rögzített train/val/test splitből tanulási batch-eket állít
elő anélkül, hogy a GlyphLab scene-szintű függetlenségi szerződését
megsértené. Az előismeret a tenzoralakok és a nn.Module
használata; a veszteség- és célalakok szerződését a 7.3. Bináris, multilabel és regressziós cél, a
numerikus ellenőrzés módszerét a 10.3.
Gradiensellenőrzés és numerikus hibamérce adta.
Induló kérdés: egy Dataset osztály
__getitem__-je visszaad egy képet, egy címkét és egy
maszkot. Ki garantálja, hogy a DataLoader által
összeállított batch-ben nem keveredik a train és a val képe, és hogy egy
jelenet két nézete mindig ugyanabba a splitbe kerül?
A PyTorch torch.utils.data moduljában a
map-style dataset egy indexelt hozzáférést adó
objektum: adott
indexre visszaadja az
-edik
mintát a __getitem__(i) metódus, a __len__
pedig a minták számát. A GlyphLab esetében egy minta nem csak egy kép,
hanem egy négyes:
ahol a kép alakja
és float32, a label skalár int64, a mask
és uint8, a scene_id skalár int64
kód (a metadata.csv szöveges jelenetazonosítójának, például
G0-0003-nak a rendezett sorszáma; az eredeti szöveget a
dataset scene_names tömbje őrzi). A batch-elés után az
alakok
,
,
és
lesznek.
A sampler az indexek sorozatát adja meg. A
SequentialSampler a
sorrendet, a RandomSampler egy véletlen permutációt állít
elő; a BatchSampler ezt
hosszú csoportokra bontja. A DataLoader a dataset, a
sampler és a collate_fn összekapcsolása: a sampler indexeit
bekéri a datasetből, a visszaadott elemek listáját a
collate_fn tenzorrészletekből batchelt tensorokká alakítja.
Az alapértelmezett default_collate a tuple minden
pozíciójára külön-külön stackel, ezért a skalár labelből
alakú int64 tensor lesz.
A shuffle=True a DataLoader konstruktorban
csak egy rövidítés: a RandomSampler-t kapcsolja be. A
shuffle és a sampler argumentum együtt nem
adható meg, mert mindkettő ugyanazt az indexelési feladatot oldaná meg.
A drop_last=True az utolsó,
-nél
kisebb batch-et eldobja; a fixed batch méretet igénylő batch-normalizáló
rétegeknél lehet hasznos, de a GlyphLab esetében
,
és
mind osztható
-gyel,
így nincs ilyen maradék.
A worker seed kérdése a párhuzamos betöltéshez
tartozik. Ha a num_workers nagyobb nullánál, minden worker
külön folyamatban fut, és a véletlen mintavétel állapota nem
automatikusan azonos a fő folyamatéval. A reprodukálhatósághoz a
DataLoader generator paraméterének vagy a
worker_init_fn-nek a használata szükséges; ennek pontos,
verzióspecifikus viselkedését a [D08] dokumentációjának olvasott
szakasza alapján kell rögzíteni, a kiinduló URL státuszát megtartva. A
jelen egység laborja num_workers=0 értékkel fut, így az
indexelés a fő folyamatban marad, és a seed kérdése nem befolyásolja az
eredményt.
A transform boundary azt a határt jelöli, ahol a
kép- és címketranszformáció megtörténik. A véletlen train-augmentáció a
train Dataset __getitem__-jében alkalmazandó,
mert ott a transzformáció mintánként, a betöltés pillanatában fut. A val
és test dataset csak determinisztikus transzformációt kaphat; a
geometriai augmentáció a maszkot is együtt kell hogy transzformálja,
különben a pixelcímke és a bemenet elcsúszik. A GlyphLab diagonal
osztálya csak egy ferde irányt tartalmaz, ezért a tükrözés és a 90 fokos
forgatás nem címkemegőrző; ebben az egységben a labor semmilyen
augmentációt nem alkalmaz, csak a betöltési szerződést ellenőrzi.
A GlyphLab adatszerződésének központi állítása, hogy a megfigyelési
egység a scene_id, nem az egyes kép. Ezt formálisan a
következő invariáns írja le.
Állítás (scene-split invariáns). Legyen
a képek indexhalmaza,
a scene_id hozzárendelés, és
a split hozzárendelés. A GlyphLab generátor olyan
függvényt valósít meg, amelyre minden
esetén
Ebből következik, hogy minden
-re
Levezetés. Ha
,
akkor
tehát az azonos scene-hez tartozó képek
splitje megegyezik. A
függvény létezése a generátor scene-szintű felosztásának formális
alakja: a splitet először a 180 jelenetre sorsoljuk, majd minden jelenet
minden nézetét ehhez a splithez rendeljük. A DataLoader ezt
az invariánst nem ronthatja el, mert a sampler csak indexeket permutál,
és a három loader külön indexhalmazt kap a split szűrés
után. Az invariáns ellenőrzése tehát két részből áll: a
metadata.csv scene-szintű konzisztenciájából és abból, hogy
a dataset konstruktora a split oszlop alapján szűr, nem
keveri a sorokat.
Négy jelenet, jelenetenként két nézet, összesen nyolc kép. A helyes split-hozzárendelés:
| scene_id | view | split | |
|---|---|---|---|
| 0 | 0 | 0 | train |
| 1 | 0 | 1 | train |
| 2 | 1 | 0 | val |
| 3 | 1 | 1 | val |
| 4 | 2 | 0 | test |
| 5 | 2 | 1 | test |
| 6 | 3 | 0 | train |
| 7 | 3 | 1 | train |
Itt , , . Ha a 0. képet train-be, az 1. képet val-ba tennénk, akkor teljesülne, de , tehát az invariáns sérülne; ekkor a val loss függő nézeteket tartalmazna a train képektől, és a val metricát optimistán torzítaná.
Munkakönyvtár: lab/. Az adatfájlok a data/
alkönyvtárban vannak: images.npy, labels.npy,
masks.npy, metadata.csv. A labor csak CPU-t és
NumPy/PyTorch alapműveleteket használ; seedre a betöltés miatt nincs
szükség, mert a betöltés determinisztikus.
# lab/n043_dataset.py
import csv
from pathlib import Path
import numpy as np
import torch
from torch.utils.data import Dataset
class GlyphLabDataset(Dataset):
"""GlyphLab: kép, label, mask és scene_id együtt, split szerint szűrve.
transform: opcionális függvény, amely a (2,24,24) alakú, egymásra rakott
[kép, maszk] tensort kapja és ugyanilyet ad vissza; így egy geometriai
transzformáció a képet és a maszkot együtt, ugyanúgy alakítja át."""
def __init__(self, root: str = "data", split: str | None = None, transform=None):
self.root = Path(root)
self.transform = transform
images = np.load(self.root / "images.npy") # (360,1,24,24) float32
labels = np.load(self.root / "labels.npy") # (360,) int64
masks = np.load(self.root / "masks.npy") # (360,1,24,24) uint8
self.images = images
self.labels = labels
self.masks = masks
rows = list(csv.reader(open(self.root / "metadata.csv", newline="")))
header = rows[0]
expected = ["index", "scene_id", "view_id", "label", "split",
"xmin", "ymin", "xmax", "ymax"]
if header != expected:
raise ValueError("metadata fejléc nem egyezik")
recs = rows[1:]
idx = np.array([int(r[0]) for r in recs], dtype=np.int64)
if not np.array_equal(idx, np.arange(len(images))):
raise ValueError("metadata index nem egyezik a tomb indexxel")
# a scene_id szöveg (pl. "G0-0003"); int64 kódot a rendezett egyedi értékek adnak
self.scene_names = np.array([r[1] for r in recs], dtype=object)
_, codes = np.unique(self.scene_names.astype(str), return_inverse=True)
self.scene_ids = codes.astype(np.int64)
self.splits = np.array([r[4] for r in recs], dtype=object)
if split is not None:
keep = self.splits == split
self.images = self.images[keep]
self.labels = self.labels[keep]
self.masks = self.masks[keep]
self.scene_ids = self.scene_ids[keep]
self.scene_names = self.scene_names[keep]
self.splits = self.splits[keep]
def __len__(self):
return len(self.images)
def __getitem__(self, i):
img = torch.from_numpy(self.images[i])
lab = torch.tensor(self.labels[i], dtype=torch.int64)
msk = torch.from_numpy(self.masks[i])
sid = torch.tensor(self.scene_ids[i], dtype=torch.int64)
if self.transform is not None:
joint = self.transform(torch.cat([img, msk.to(img.dtype)], dim=0)) # (2,24,24)
img, msk = joint[:1].contiguous(), joint[1:].round().to(torch.uint8)
return img, lab, msk, sidAz ellenőrző szkript a split-invariánst is teszteli:
# lab/n043_check.py
import csv
from collections import defaultdict
from torch.utils.data import DataLoader
from n043_dataset import GlyphLabDataset
if __name__ == "__main__":
ds_train = GlyphLabDataset("data", split="train")
ds_val = GlyphLabDataset("data", split="val")
ds_test = GlyphLabDataset("data", split="test")
print("hossz:", len(ds_train), len(ds_val), len(ds_test))
img, lab, msk, sid = ds_train[0]
print("elem:", img.shape, img.dtype, lab.shape, lab.dtype,
msk.shape, msk.dtype, sid.shape, sid.dtype)
print("tartomany:", float(img.min()), float(img.max()),
int(msk.min()), int(msk.max()))
loader = DataLoader(ds_train, batch_size=4, shuffle=False)
batch = next(iter(loader))
for name, x in zip(["image", "label", "mask", "scene_id"], batch):
print(name, tuple(x.shape), x.dtype)
scene_split = {}
with open("data/metadata.csv", newline="") as f:
for r in list(csv.reader(f))[1:]:
s, sp = r[1], r[4] # scene_id szövegként
if s in scene_split and scene_split[s] != sp:
raise SystemExit("INVARIANTS SERUL")
scene_split[s] = sp
print("egyedi scene:", len(scene_split))
splits = defaultdict(int)
for s in scene_split.values():
splits[s] += 1
print("scene per split:", dict(splits))A futtatási parancs a lab/ munkakönyvtárból:
python n043_check.py
A lab-profilban (PyTorch 2.14.1+cpu) mért kimenet:
hossz: 240 60 60
elem: torch.Size([1, 24, 24]) torch.float32 torch.Size([]) torch.int64 torch.Size([1, 24, 24]) torch.uint8 torch.Size([]) torch.int64
tartomany: 0.0 0.9264628887176514 0 1
image (4, 1, 24, 24) torch.float32
label (4,) torch.int64
mask (4, 1, 24, 24) torch.uint8
scene_id (4,) torch.int64
egyedi scene: 180
scene per split: {'train': 120, 'val': 30, 'test': 30}
Ez egyezik a szerződéssel: a hosszak ; az elem alakjai float32, int64, uint8 és int64; a batch alakjai , , , ; a képpixelek a intervallumban, a maszkértékek -ben vannak; az egyedi scene-k száma , és a scene-k splitenkénti száma .
F1 (fogalmi, nehézség 1). Magyarázza el a
Dataset és a DataLoader különbségét. Miért nem
elegendő önmagában egy Dataset a minibatch-tanításhoz?
Adjon egy kis ellenpéldát, ahol a Dataset indexelése
működik, de a batchelt iteráció hibát adna.
F2 (levezetési, nehézség 2). Hat képhez adott a következő scene-hozzárendelés: , , , és a view indexek rendre . Adjon egy olyan split-hozzárendelést, amely teljesíti a scene-split invariánst, és egy olyat, amely sérti. Indokolja mindkettőt a függvény segítségével.
F3 (implementációs, nehézség 2). Írja meg a
GlyphLabDataset osztályt a fenti szerződés szerint, majd
ellenőrizze: a train dataset hossza
;
a ds_train[0] elemeinek alakja és dtype-ja
pontosan
torch.float32,
torch.int64,
torch.uint8,
torch.int64; a
DataLoader(ds_train, batch_size=4, shuffle=False) első
batch-ének alakjai
,
,
,
.
A dtype-okra pontos egyezés, a shape-ekre pontos egyezés a
tolerancia.
F4 (hibakeresési, nehézség 2). A következő kódrészlet a val loaderhez véletlen tükrözést ad:
def random_hflip(x, p=0.5):
# véletlen vízszintes tükrözés a (2,24,24) [kép, maszk] tensoron
return torch.flip(x, dims=(-1,)) if torch.rand(()) < p else x
val_ds = GlyphLabDataset("data", split="val", transform=random_hflip)
val_loader = DataLoader(val_ds, batch_size=4, shuffle=False)Azonosítsa a hibát, írja le a következményét a validation loss-ra és a korrupt teszt páros összevetésére, és adja meg a javítást. Hogyan ellenőrizné vissza, hogy a javítás után a val loader determinisztikus?
F5 (kísérleti/transzfer, nehézség 3). Tervezzen egy olyan kísérletet, amely a meglévő GlyphLab splittől független, új scene-szintű train/val/test felosztást készít 70/15/15 arányban, majd a tanítás után a test accuracy-t méri. Adja meg a protokollt: megfigyelési egység, baseline, változó tényező, metrika, erőforrás és a következtetés érvényességi korlátja. Nem szükséges futtatni; a terv legyen végrehajtható.
F1. A Dataset egy indexelt
adathozzáférés: az
indexre mintát ad, de nem tud batch méretet, sorrendet vagy párhuzamos
betöltést kezelni. A DataLoader a sampler által adott
indexsorozatot bekéri, a mintákat a collate_fn segítségével
batchelt tensorokká alakítja, és opcionálisan több workerrel dolgozik.
Ellenpélda: ha a Dataset.__getitem__ különböző hosszúságú
szekvenciákat ad vissza, az indexelés működik, de az alapértelmezett
default_collate nem tudja stackelni a különböző hosszúságú
tenzorokat, ezért a DataLoader iterációja hibát ad;
ilyenkor egyedi collate_fn vagy padding szükséges.
F2. A helyes függvény például , , . Ekkor a split-hozzárendelés: train; val; test. Az invariáns teljesül, mert azonos scene-hez tartozó indexek azonos splitet kapnak. Sértő példa: és , miközben . Itt nem létezhet olyan , amelyre minden -re, mert egyszerre train és val lenne; ezért a levezetett invariáns sérül.
F3. A megoldás a fenti n043_dataset.py
és n043_check.py. Az ellenőrzés kulcsa, hogy a
torch.from_numpy megőrzi a NumPy dtype-ot:
float32
torch.float32, uint8
torch.uint8. A label és a scene_id explicit
torch.tensor(..., dtype=torch.int64) módon int64 lesz, így
a default_collate stackelés után is int64 marad. A shape-ek
pontos egyezése azt jelenti, hogy nincs engedmény:
nem lehet
.
A futtatási parancs python n043_check.py a
lab/ könyvtárból.
F4. A hiba: a val dataset transzformációja véletlen
augmentáció, ami a transform boundary megsértése. A
random_hflip
valószínűséggel tükrözi a képet (a __getitem__ a
transform-ot ténylegesen meghívja, a maszkkal együtt); a
GlyphLab diagonal osztálya csak egy ferde irányt tartalmaz, ezért a
tükrözés a címke geometriai jelentését megváltoztatja, a vertical és
horizontal pedig felcserélődik. Következmény: a val loss nem
determinisztikus, a best checkpoint kiválasztása zajos; a clean és
corrupt teszt páros összevetése is torzul, mert a két nézet nem ugyanazt
a véletlen tükrözést kapja. Javítás: a val dataset ne kapjon véletlen
transzformációt (transform=None); csak determinisztikus
normalizálást, vagy semmit. Visszaellenőrzés ismételt
__getitem__-hívásokkal és két teljes iterációval:
torch.manual_seed(0)
bad = GlyphLabDataset("data", split="val", transform=random_hflip)
print(any(not torch.equal(bad[0][0], bad[0][0]) for _ in range(20))) # True: véletlen
good = GlyphLabDataset("data", split="val") # javítva
print(all(torch.equal(good[0][0], good[0][0]) for _ in range(20))) # True: determinisztikus
a = torch.cat([b[0] for b in DataLoader(good, batch_size=4)])
c = torch.cat([b[0] for b in DataLoader(good, batch_size=4)])
print(torch.equal(a, c)) # TrueF5. A kísérleti terv: a 180 egyedi
scene_id-ból rétegzett véletlen mintavétellel készítünk 126
train, 27 val és 27 test jelenetet, majd minden jelenet mindkét nézetét
a jelenet splitjéhez rendeljük; ez a scene-split invariánst megtartja. A
baseline a meglévő, gyárilag rögzített 120/30/30 scene-felosztás; a
változó tényező csak a split sorsolása, a modell és a tanítási recept
azonos. A metrika a test accuracy; az erőforrás kis CPU, a modell a
korábbi kis CNN vagy MLP. Az érvényességi korlát: a GlyphLab 180
jelenete kicsi, ezért a test accuracy szórása nagy; egyetlen új split
nem általánosít, a konfidenciaintervallumot több seedelt split
ismétlésével kell becsülni, és az eredmény nem természetes képi
benchmark.
A leggyakoribb hiba a validation/test loaderen alkalmazott
véletlen train-augmentáció. Ha a val vagy test dataset
__getitem__-je véletlen tükrözést, forgatást vagy kivágást
kap, akkor az értékelés nem a modell képességét, hanem egy véletlen
transzformációval zajosított bemenetet mér. A páros korrupt teszt
összevetése különösen érzékeny: a clean és corrupt kép ugyanazon jelenet
és nézet párja, ezért a geometriai transzformációnak is azonosnak kell
lennie; a véletlen augmentáció ezt a párosítást felbontja, és a
clean–corrupt különbséghez mesterséges varianciát ad. A transform
boundary tehát split-függő szerződés: véletlen transzformáció csak a
train datasetben, a val/test datasetben kizárólag determinisztikus
előfeldolgozás.
Ellenőrizze, hogy a dataset konstruktora a split oszlop
alapján szűr, és a metadata.csv fejlécét, valamint az
indexsorrendet is validálja. A DataLoader batch alakjait a
default_collate szabálya szerint vezesse le: a tuple első
eleme
,
a stackelés után
;
a skalár labelből
lesz. A 11.2. nn.Module és paraméterregisztráció
alfejezetben bevezetett state_dict most az adatbetöltővel
együtt lesz használható a tanítási lépésben; a 7.3.
Bináris, multilabel és regressziós cél alfejezetben rögzített int64
célalak és logits-bemenet szerződése a 11.4. Egy
teljes PyTorch tanítási iteráció alfejezetben találkozik a betöltött
batch-ekkel.
A Dataset indexelt mintahozzáférést ad, a
DataLoader a sampler indexsorozatát batchelt tensorokká
alakítja a collate_fn segítségével. A GlyphLab scene-split
invariánsa formálisan azt mondja ki, hogy azonos
scene_id-hez tartozó képek splitje megegyezik; a
függvény létezése biztosítja, hogy a split scene-szinten, nem képenként
dől el. A labor betölti a train/val/test dataseteket, ellenőrzi a
hosszakat, a dtype-okat és az alakokat, valamint a metadata
scene-konzisztenciáját. A transform boundary szerint véletlen
augmentáció csak a train datasetben megengedett; a val/test loader
determinisztikus transzformációt kaphat. A következő 11.4. Egy teljes PyTorch tanítási iteráció
egységben ezt a betöltőt kapcsoljuk össze a tanítási iterációval és a
loss-számítással.