13.1 Kép mint diszkrét többcsatornás jel

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

Az affin rétegek eddig X∈ℝN×dX\in\mathbb R^{N\times d} alakú bemenetet kaptak, ahol egy megfigyelés egyetlen vektorsor. A kép ezzel szemben két térbeli koordinátával és csatornákkal rendelkező diszkrét jel. Ennek az egységnek az a célja, hogy a képértéket, a színcsatornát, a koordinátát és a batchelemet pontosan elválassza, mert a következő konvolúciós egységekben minden levezetés ezen a tengelyszerződésen áll.

Előismeret: NumPy tömb és tengelyfogalom a 2.1. Python, NumPy és tenzoralak alfejezetből, mátrixindexelés a 3.2. Mátrixszorzás és affín réteg alfejezetből, valamint a 11.4. Egy teljes PyTorch tanítási iteráció alfejezetben rögzített input-szerződés: a modell első rétege a dokumentált alakkal és skálával dolgozik.

Induló becslés: egy GlyphLab-kép alakja (1,24,24)(1,24,24). Egy kép hány skalárt tárol, és egy 360 képből álló batch mennyit? Hány koordináta, hány csatorna és hány megfigyelés van ebben a batchben? A választ a definíciók után pontos alakkal indokoljuk.

13.1.2 Definíciók: rács, pixel, csatorna, batch

Definíció (mintavételi rács). Rögzített H,W∈ℤ>0H,W\in\mathbb Z_{>0} magasság és szélesség mellett a mintavételi rács (sampling grid) az indexpárok

𝒢(H,W)={(h,w)∈ℤ2:0≤h<H,0≤w<W}\mathcal G(H,W)=\{(h,w)\in\mathbb Z^2 : 0\le h<H,\ 0\le w<W\}

halmaza. A (h,w)(h,w) pár a sor- és oszlopkoordináta; a könyv tömbkiíratási konvenciója szerint hh lefelé, ww jobbra növekszik. A rács H⋅WH\cdot W pontot tartalmaz. A koordináta tehát index, nem pixelérték.

Definíció (digitális kép). Egy CC csatornás digitális kép egy

X∈ℝC×H×WX\in\mathbb R^{C\times H\times W}

tensor, ahol Xc,h,w∈ℝX_{c,h,w}\in\mathbb R a cc csatorna intenzitása a (h,w)(h,w) rácspontban. A c=0,…,C−1c=0,\ldots,C-1 index a csatornakoordináta, (h,w)(h,w) a térbeli koordináta, az Xc,h,wX_{c,h,w} skalár pedig a képérték.

Definíció (pixel). Ebben a könyvben a pixel a rács egy pontjához tartozó csatornaérték-vektor: a (h,w)(h,w) pontban

X:,h,w=(X0,h,w,…,XC−1,h,w)⊤∈ℝC.X_{:,h,w}=(X_{0,h,w},\ldots,X_{C-1,h,w})^\top\in\mathbb R^C .

Grayscale képnél ez egyetlen skalár; RGB esetén három számból álló vektor.

Definíció (batch). Ha NN képet egyszerre tárolunk, a batch tenzor

X∈ℝN×C×H×W,X\in\mathbb R^{N\times C\times H\times W},

és n=0,…,N−1n=0,\ldots,N-1 a batchelem indexe. Az nn tehát megfigyelést azonosít, a cc csatornát, a (h,w)(h,w) pár térbeli pozíciót. A célcímke, a maszk vagy a doboz nem a képtensor része; a GlyphLab-szerződésben ezek külön tömbök, amelyek a térbeli rácsot megosztják a képpel.

Definíció (NCHW és NHWC). A tengelyek szemantikáját két elterjedt sorrend rögzíti:

Elrendezés Tengelysorrend Gyorsan változó index A könyv főútja
NCHW batch, channel, height, width ww PyTorch Conv2d, [D10]
NHWC batch, height, width, channel cc gyakori NumPy/képi formátum

A két elrendezés ugyanazt a képet tárolhatja, de a memóriában másképp rendezi. A PyTorch hivatalos Conv2d-dokumentációja NCHW bemenetet ír elő; a 13.2. Konvolúció és cross-correlation alfejezetben bevezetett konvolúciós réteg is ezt használja [D10].

Definíció (grayscale és RGB). A C=1C=1 eset grayscale: minden pixel egy intenzitásskalár. A C=3C=3 eset színkép; a jelen könyvben a (c=0,1,2)=(R,G,B)(c=0,1,2)=(R,G,B) sorrend a konvenció. Ez nem a könyvtár automatikus ténye: az OpenCV például BGR sorrendet használ, ezért külső modelleknél a csatornasorrendet mindig a forrás dokumentációjából kell rögzíteni.

13.1.3 Intenzitás és skálaválasztás: kimondott állítások

Állítás 1 (NCHW offset-képlet és index-tartomány). Legyen N,C,H,W∈ℤ>0N,C,H,W\in\mathbb Z_{>0} és X∈ℝN×C×H×WX\in\mathbb R^{N\times C\times H\times W} C-sorrendű, összefüggő tömbben tárolva. A lineáris elemindexre

off(n,c,h,w)=nCHW+cHW+hW+w.\mathrm{off}(n,c,h,w)=n\,C H W+c\,H W+h\,W+w .

Ez a leképezés bijekció az IN×IC×IH×IWI_N\times I_C\times I_H\times I_W indexhalmaz és {0,1,…,NCHW−1}\{0,1,\ldots,NCHW-1\} között, ahol IK={0,…,K−1}I_K=\{0,\ldots,K-1\}.

Bizonyítás. (i) A C-sorrend definíciója szerint az utolsó tengely stride-ja 11, tehát ww lépésenként 11-et; hh stride-ja WW; cc stride-ja HWHW; nn stride-ja CHWCHW. A kezdőcímtől mért elemeltolás ezért a fenti lineáris kombináció.

  1. Injektivitás. Ha off(n,c,h,w)=off(n′,c′,h′,w′)\mathrm{off}(n,c,h,w)=\mathrm{off}(n',c',h',w'), akkor mindkét oldal WW-vel vett maradéka ww, illetve w′w' (a többi tag WW többszöröse), így w=w′w=w'. Kivonás és WW-vel osztás után nCH+cH+h=n′CH+c′H+h′nCH+cH+h=n'CH+c'H+h'; a HH-val vett maradékból h=h′h=h', majd CC-vel való osztás után a maradékból c=c′c=c', végül n=n′n=n'.

  2. Szürjektivitás. Tetszőleges m∈{0,…,NCHW−1}m\in\{0,\ldots,NCHW-1\} esetén az euklideszi osztások egymás utáni alkalmazása

w=mmodW,h=(m÷W)modH,c=(m÷(WH))modC,n=m÷(CHW)w=m\bmod W,\quad h=(m\div W)\bmod H,\quad c=(m\div(WH))\bmod C,\quad n=m\div(CHW)

érvényes indexeket ad, és a képletbe visszahelyettesítve mm-et kapunk. Így minden lineáris index pontosan egyszer keletkezik. ▫\square

Megjegyzés (ellenőrzési típus). Az Állítás 1 tisztán egész-aritmetikai azonosság, nem empirikus állítás; ellenőrzése kódban egész indexszámítással történik. A képlet csak C-sorrendű, összefüggő tárolásra érvényes. Egy tükrözött vagy transzponált nézet stride-jai eltérhetnek (akár negatívak is lehetnek); az általános alak off=b+nsn+csc+hsh+wsw\mathrm{off}=b+n s_n+c s_c+h s_h+w s_w a tényleges sks_k stride-okkal.

Állítás 2 (NCHW–NHWC transzpozíció és inverze). Legyen π=(0,2,3,1)\pi=(0,2,3,1) a tengelypermutáció. A Y=permπ(X)Y=\operatorname{perm}_\pi(X) tömb elemeire

Yn,h,w,c=Xn,c,h,w∀n,c,h,w.Y_{n,h,w,c}=X_{n,c,h,w}\qquad\forall n,c,h,w .

A π−1=(0,3,1,2)\pi^{-1}=(0,3,1,2) permutáció inverz, tehát permπ−1(Y)=X\operatorname{perm}_{\pi^{-1}}(Y)=X; a kettős transzponálás az identitás. Következésképp Yn,h,w,:=Xn,:,h,wY_{n,h,w,:}=X_{n,:,h,w}, vagyis a pixel csatornavektora megmarad.

Bizonyítás. Ez a tengelypermutáció definíciójának közvetlen elemenkénti ellenőrzése: a YY tömb (n,h,w,c)(n,h,w,c) pozíciójába a permutáció pontosan az XX tömb (n,c,h,w)(n,c,h,w) elemét írja; π−1\pi^{-1}-et alkalmazva az elem az eredeti pozícióba kerül vissza. A kétforma tehát ugyanazt a képfüggvényt reprezentálja, csak a csatornatengely helye változik. ▫\square

Állítás 3 (uint8 tárolás és a „fehér = 1” skálaválasztás). Legyen u∈[0,1]u\in[0,1] normalizált intenzitás és q=clip(⌊255u+12⌋,0,255)∈{0,…,255}q=\operatorname{clip}(\lfloor 255u+\tfrac12\rfloor,0,255)\in\{0,\ldots,255\} a 88-bites tárolási érték. A visszaolvasás û=q/255\hat u=q/255 szigorúan monoton bijekció a {0,…,255}→{0,1255,…,1}\{0,\ldots,255\}\to\{0,\tfrac1{255},\ldots,1\} halmazra; speciálisan a maximális tárolási érték visszaolvasva 11. A 256256-tal való osztás ezzel szemben 255/256≈0,996255/256\approx 0{,}996-ot adna, ezért a „0 fekete, 1 maximális intenzitás” konvenció a 255255 osztót rögzíti.

Bizonyítás. q=255q=255 esetén 255/255=1255/255=1. Ha q<q′q<q', akkor q′/255−q/255=(q′−q)/255>0q'/255-q/255=(q'-q)/255>0. A k↦k/255k\mapsto k/255 leképezés tehát megőrzi a rendezést és injektív, két 255255 elemű halmaz között pedig bijektív. A második állítás a 255256<1\tfrac{255}{256}<1 egyenlőtlenség közvetlen következménye. ▫\square

A GyphLab-képek közvetlenül float32 [0,1] tartományban tároltak, ezért ott nincs kvantálási lépés. Dataset-szintű standardizáláskor a x̂c=(xc−μc)/σc\hat x_c=(x_c-\mu_c)/\sigma_c csatornánkénti affin transzformáció csak σc>0\sigma_c>0 mellett értelmes; a per-image min-max normalizálás x̃=(x−xmin)/(xmax−xmin)\tilde x=(x-x_{\min})/(x_{\max}-x_{\min}) konstans képen (xmax=xminx_{\max}=x_{\min}) nullával osztana, tehát ilyen bemenetre explicit leágazást kell definiálni.

13.1.4 Kézi kis példa és minimális kód

Tekintsünk egy 1×1×3×41\times1\times3\times4 alakú, azaz H=3,W=4H=3, W=4 grayscale képet átlóval:

X=(0,200000,500000,90).X = \begin{pmatrix} 0{,}2 & 0 & 0 & 0\\ 0 & 0{,}5 & 0 & 0\\ 0 & 0 & 0{,}9 & 0 \end{pmatrix}.

Az NCHW offset-képlet N=1,C=1N=1, C=1 miatt off(c=0,h,w)=hW+w\mathrm{off}(c=0,h,w)=hW+w. A nemnulla elemek lineáris indexei: (0,0)→0(0,0)\to 0, (1,1)→1⋅4+1=5(1,1)\to 1\cdot4+1=5, (2,2)→2⋅4+2=10(2,2)\to 2\cdot4+2=10. A C-sorrendű, lapított tömbben tehát a 0,20{,}2 a 0.0., a 0,50{,}5 az 5.5., a 0,90{,}9 a 10.10. pozícióban áll, a többi elem nulla.

RGB példa (N,C,H,W)=(1,3,2,2)(N,C,H,W)=(1,3,2,2), csatornasorrend (R,G,B)(R,G,B):

X0,R=(0,10,20,30,4),X0,G=(0,50,60,70,8),X0,B=(0,91,01,01,0).X_{0,R}=\begin{pmatrix}0{,}1&0{,}2\\0{,}3&0{,}4\end{pmatrix},\quad X_{0,G}=\begin{pmatrix}0{,}5&0{,}6\\0{,}7&0{,}8\end{pmatrix},\quad X_{0,B}=\begin{pmatrix}0{,}9&1{,}0\\1{,}0&1{,}0\end{pmatrix}.

A (h,w)=(0,1)(h,w)=(0,1) pixel vektora (0,2,0,6,1,0)(0{,}2,0{,}6,1{,}0). NCHW lapításban a 0,60{,}6 elem címe off(0,1,0,1)=1⋅4+0⋅2+1=5\mathrm{off}(0,1,0,1)=1\cdot4+0\cdot2+1=5; az NHWC transzponáltban off(0,0,1,1)=((0⋅2+0)⋅2+1)⋅3+1=4\mathrm{off}(0,0,1,1)=((0\cdot2+0)\cdot2+1)\cdot3+1=4, tehát ugyanannak a pixelnek a két elrendezés más lineáris címet ad.

# munkakönyvtár: lab/n049_layout
import numpy as np

X = np.zeros((1, 3, 2, 2), dtype=np.float32)      # NCHW, [0,1]
X[0, 0, 0, 0], X[0, 0, 0, 1] = 0.1, 0.2           # R sor
X[0, 0, 1, 0], X[0, 0, 1, 1] = 0.3, 0.4
X[0, 1, 0, 0], X[0, 1, 0, 1] = 0.5, 0.6           # G sor
X[0, 1, 1, 0], X[0, 1, 1, 1] = 0.7, 0.8
X[0, 2] = [[0.9, 1.0], [1.0, 1.0]]                # B csatorna, explicit mátrix

Y = np.transpose(X, (0, 2, 3, 1))                 # NCHW -> NHWC
print("NCHW:", X.shape, "NHWC:", Y.shape)
print("pixel (0,1):", Y[0, 0, 1, :])              # várt: [0.2 0.6 1. ]
# ugyanazon pixel három csatornája a transzponálás előtt és után
assert np.array_equal(X[0, :, 0, 1], Y[0, 0, 1, :])
print("G csatorna (0,1) NCHW lapított címe:", (1*2 + 0)*2 + 1)   # 5
print("G csatorna (0,1) NHWC lapított címe:", ((0*2 + 0)*2 + 1)*3 + 1)  # 4

A lab-profilban (NumPy 2.3.5) mért kimenet: NCHW: (1, 3, 2, 2) NHWC: (1, 2, 2, 3), pixel (0,1): [0.2 0.6 1. ], a két lapított cím 55 és 44; az assert igazolja, hogy a pixel csatornavektora a transzponálás előtt és után azonos.

13.1.5 Labor: mesterséges kép és memóriaelrendezés-ellenőrzés

A labor egy kis, osztálycélú mintára hasonlító mesterséges képpárt rajzol, majd NCHW és NHWC között transzponál és visszafelé is ellenőriz. Ez a GlyphLab-labor előtti gyakorlólépés; nem modellkísérlet.

# lab/n049_layout/lab_n049.py
import numpy as np
from pathlib import Path

out = Path("lab/n049_layout")
out.mkdir(parents=True, exist_ok=True)

RNG_SEED = 20261020
H, W = 6, 7
rng = np.random.default_rng(RNG_SEED)   # csak a vágási tervet rögzíti; jelen példa determinisztikus

X = np.zeros((2, 1, H, W), dtype=np.float32)       # NCHW, [0,1], CPU
X[0, 0, :, 2] = 1.0                                # 0. kép: függőleges vonal
X[1, 0, 3, :] = np.array([0.2, 0.4, 0.6, 0.8, 1.0, 1.0, 0.8],
                         dtype=np.float32)         # 1. kép: vízszintes sáv

assert X.shape == (2, 1, H, W)
assert X.dtype == np.float32
assert X.min() >= 0.0 and X.max() <= 1.0

Y = np.transpose(X, (0, 2, 3, 1))                 # NHWC nézet
assert Y.shape == (2, H, W, 1)
assert np.shares_memory(X, Y)                     # transzponálás: nézet, nem másolat

# NCHW offset kézzel: off(n,0,h,w)=((n*1+0)*H+h)*W+w
def off(n, h, w):
    return ((n * 1 + 0) * H + h) * W + w

flat = X.reshape(-1)
assert flat[off(1, 3, 4)] == X[1, 0, 3, 4]        # lapított cím ellenőrzése
assert Y[1, 3, 4, 0] == X[1, 0, 3, 4]             # ugyanaz a pixel NHWC-ben

X2 = np.transpose(Y, (0, 3, 1, 2))
assert np.array_equal(X, X2)                      # kettős transzponálás = identitás

np.savez(out / "mini_images.npz", x_nchw=X, x_nhwc=Y, seed=np.int64(RNG_SEED))
print("NCHW:", X.shape, X.dtype, "min/max:", X.min(), X.max())
print("NHWC:", Y.shape)
print("ellenőrzött pixel X[1,0,3,4] =", X[1, 0, 3, 4])
print("mentve:", out / "mini_images.npz")

Futtatás a könyv gyökérkönyvtárából:

python lab/n049_layout/lab_n049.py

A kódból számított, jelölt elvárt kimenet — nem egy dokumentált mérés jegyzőkönyve:

NCHW: (2, 1, 6, 7) float32 min/max: 0.0 1.0
NHWC: (2, 6, 7, 1)
ellenőrzött pixel X[1,0,3,4] = 1.0
mentve: lab/n049_layout/mini_images.npz

Értelmezés: a transzponálás nem változtatta a pixelértékeket, csak a csatornatengely helyét. A np.shares_memory igaz volta azt mutatja, hogy a két elrendezés ugyanazon az adaton osztozik; másolni csak akkor kell, ha összefüggő NHWC-tömbre van szükség. A mini_images.npz visszatöltése után a np.array_equal összevetés a lemezre írás-újraolvasás kontrollja is.

13.1.6 Feladatok

F1 (fogalmi; nehézség 1). A fenti RGB kézi példában X∈ℝ1×3×2×2X\in\mathbb R^{1\times3\times2\times2}. Írja fel a teljes NHWC YY tömböt elemenként, és adja meg, hogy a YY indexeiben hol található a (h,w)=(0,1)(h,w)=(0,1) koordinátájú, GG csatornájú 0,60{,}6 érték. Indokolja, miért nem veszít információt a transzpozíció, és adjon rövid magyarázatot, miért nem azonos mégis a két lapított memóriakép.

F2 (levezetési; nehézség 2). Legyen N=1,C=3,H=2,W=2N=1, C=3, H=2, W=2. Az (n,c,h,w)=(0,1,1,1)(n,c,h,w)=(0,1,1,1) logikai pixelre vezesse le az NCHW és az NHWC C-sorrendű offset-képletet a stride-okból, és mutassa meg, hogy a két lineáris cím 77, illetve 1010. Bizonyítsa be a két képlet különbségéből, hogy a két lapított sorrend akkor és csak akkor azonos minden indexre, ha C=1C=1 vagy H=W=1H=W=1.

F3 (implementációs; nehézség 2). Írjon futtatható függvényt nchw_to_nhwc_ellenorzott(x) néven, amely elfogad egy 4 dimenziós float32/view float64 NumPy-tömböt, ValueError-t dob rossz dimenziószámra, TypeError-t egész dtype-ra, majd np.transpose(x,(0,2,3,1)) módon NHWC-t ad. Adjon seedelt véletlentesz tet a (2,3,4,5)(2,3,4,5) alakkal: shape, egy ismert pixel, osztott memória, visszatranszponálás (00 abszolút hibával) és negatív uint8 teszt.

F4 (hibakeresési; nehézség 2). Az alábbi kód egy grayscale glyph.png-t ad egy NCHW modellnek. Hol a hiba, mi a következménye, és hogyan javítható?

from PIL import Image
import numpy as np, torch

x = np.asarray(Image.open("glyph.png")).astype(np.float32)  # (H, W) szürke tömb
logits = model(torch.as_tensor(x))                          # modell 4D (N,C,H,W)-t vár

F5 (kísérleti/transzfer; nehézség 3). Egy GlyphLab-ra tanított osztályozót külső, saját rajzú vonalképekre akarunk átvinni. Tervezzen protokollt annak eldöntésére, hogy a rögzített /255 skálázás elegendő-e, vagy a külső képek változó hátterén per-image min-max normalizálás kell. Adja meg: kérdés, hipotézis, adat és forrás, megfigyelési egység, split, baseline, változó tényező, metrika, erőforrás és a következtetés érvényességi határa. Ne igényeljen meglévő lefutást; tervet és buktatókat várunk.

13.1.7 Megoldások és indoklás

F1. A transzponált Y=perm(0,2,3,1)(X)Y=\operatorname{perm}_{(0,2,3,1)}(X) elemei Y0,h,w,c=X0,c,h,wY_{0,h,w,c}=X_{0,c,h,w}. Tehát

Y0,0,0,:=(0,1,0,5,0,9),Y0,0,1,:=(0,2,0,6,1,0),Y_{0,0,0,:}=(0{,}1,0{,}5,0{,}9),\quad Y_{0,0,1,:}=(0{,}2,0{,}6,1{,}0), Y0,1,0,:=(0,3,0,7,1,0),Y0,1,1,:=(0,4,0,8,1,0).Y_{0,1,0,:}=(0{,}3,0{,}7,1{,}0),\quad Y_{0,1,1,:}=(0{,}4,0{,}8,1{,}0).

A GG csatorna 0,60{,}6 értéke Y0,0,1,1Y_{0,0,1,1}-ben van: az első két térbeli index a koordináta, az utolsó index a csatorna. Az Állítás 2 szerint a két forma között invertálható index-permutáció áll, ezért adat nem vész el. A lapított memóriakép mégis eltér: az NCHW-ben a csatornastride HWHW, az NHWC-ben 11, ezért az elem lineáris címe 55, illetve 44.

F2. NCHW: off=nCHW+cHW+hW+w=0+1⋅4+1⋅2+1=7\mathrm{off}=nCHW+cHW+hW+w=0+1\cdot4+1\cdot2+1=7. NHWC: off=nHWC+hWC+wC+c=0+1⋅2⋅3+1⋅3+1=10\mathrm{off}=nHWC+hWC+wC+c=0+1\cdot2\cdot3+1\cdot3+1=10. A különbség általánosan

Δ=offNCHW−offNHWC=c(HW−1)+(1−C)(hW+w)\Delta=\mathrm{off}_\mathrm{NCHW}-\mathrm{off}_\mathrm{NHWC}=c(HW-1)+(1-C)(hW+w)

(mert nCHW=nHWCnCHW=nHWC). Ez pontosan akkor nulla minden indexre, ha c(HW−1)+(1−C)(hW+w)=0c(HW-1)+(1-C)(hW+w)=0 minden c,h,wc,h,w-re. C=1C=1 esetén c=0c=0 és 1−C=01-C=0, így Δ=0\Delta=0. Ha C>1C>1, válasszuk c=1,h=w=0c=1, h=w=0: ekkor HW−1=0HW-1=0, tehát H=W=1H=W=1 szükséges; ez az eset valóban nullát ad. Ezzel az „akkor és csak akkor” állítás kész. A megadott pontban Δ=1⋅3+(1−3)⋅3=3−6=−3\Delta=1\cdot3+(1-3)\cdot3=3-6=-3, azaz 7−10=−37-10=-3.

F3. Futtatható minta; a kimenet jelölt, a futás a tanuló gépén végzendő.

import numpy as np

def nchw_to_nhwc_ellenorzott(x):
    if x.ndim != 4:
        raise ValueError(f"négy dimenzió kell, kapott {x.ndim}")
    if x.dtype not in (np.float32, np.float64):
        raise TypeError(f"float32/float64 kell, kapott {x.dtype}")
    return np.transpose(x, (0, 2, 3, 1))

rng = np.random.default_rng(0)
x = rng.random((2, 3, 4, 5)).astype(np.float32)
y = nchw_to_nhwc_ellenorzott(x)
assert y.shape == (2, 4, 5, 3)
assert np.array_equal(y[1, 0, 2, :], x[1, :, 0, 2])
assert np.shares_memory(x, y)
vissza = np.transpose(y, (0, 3, 1, 2))
max_abs = float(np.max(np.abs(x - vissza)))
assert max_abs == 0.0
try:
    nchw_to_nhwc_ellenorzott(np.zeros((1, 3, 2, 2), dtype=np.uint8))
except TypeError:
    print("uint8 elutasítva: explicit float dtype-kérés")
print(f"Rendben: shape {y.shape}, dtype {y.dtype}, max abs hiba {max_abs:.1e}")

Elvárt kimenet: uint8 elutasítva: explicit float dtype-kérés, majd Rendben: shape (2, 4, 5, 3), dtype float32, max abs hiba 0.0e+00. A nulla hiba nem tolerancia-kérdés: a transzponálás bitazonos másolatot ad, mert csak indexeket cserél, számot nem.

F4. Az Image.open(...) grayscale PNG-re (H,W)(H,W) alakú tömböt ad. A Conv2d csak 3D, csatornás batch nélküli (C,H,W)(C,H,W) vagy 4D (N,C,H,W)(N,C,H,W) bemenetet fogad el; a kétdimenziós bemenetet elutasítja (RuntimeError: Expected 3D (unbatched) or 4D (batched) input to conv2d, but got input of size: [24, 24]), tehát ez a hiba nem csendes. A javítás: adjunk batch- és csatornadimenziót, és alkalmazzuk a [0,1] skálakonvenciót.

x = np.asarray(Image.open("glyph.png")).astype(np.float32)   # (H, W)
t = torch.from_numpy(x / 255.0).unsqueeze(0).unsqueeze(0)    # (1, 1, H, W)
assert t.shape == (1, 1, x.shape[0], x.shape[1])
assert torch.equal(torch.as_tensor(np.float32(x[0, 0] / 255.0)), t[0, 0, 0, 0])
logits = model(t)

A valóban csendes hiba az azonos dimenziószámú tengelycsere. Ha a (1,1,H,W)(1,1,H,W) tensort valahol x.transpose(-1, -2) alakban, a HH és WW felcserélésével adjuk a modellnek, az alak továbbra is négydimenziós, a Conv2d hiba nélkül lefut, a kép viszont a főátlójára tükröződik: egy függőleges vonalból vízszintes lesz, a GlyphLabben tehát a 0 és 1 címke jelentése felcserélődik. Ezt csak tartalmi kontroll szűri ki, például egy ismert orientációjú tesztkép predikciója, vagy egy pixel helyének összevetése a transzformáció előtt és után.

Színes fájl esetén előbb rögzíteni kell a csatornasorrendet, például Image.open(p).convert("RGB") után permute(2,0,1) az (R,G,B)→NCHW átálláshoz; az OpenCV BGR-t ad, ott a permutáció nem felcserélhető.

F5. Egy lehetséges terv. Kérdés: változó hátterű külső vonalképeken a rögzített /255 baseline-hoz képest a per-image min-max javítja-e a validációs pontosságot, és nem okoz-e degenerált esetet? Hipotézis: a per-image transzformáció erősítheti a háttérzajt, konstans foltokon pedig osztás előtt leágazás kell. Adat: 60 saját, annotált külső kép (a GlyphLab-laborból származó split nem módosítható); megfigyelési egység scene_id, class-stratified split. Baseline: modell rögzített, input /255, CPU. Változó: csak a preprocessing lépés (a modell súlya és seed rögzített). Metrika: val accuracy; a bizonytalanság per-scene bootstrap CI, nem képenkénti. Erőforrás: CPU, kis batch. Következtetési határ: az eredmény erre a saját külső rajztartományra érvényes; egyetlen seed nem általánosítás, és a külső val spliten tilos a modellt újrahangolni; konstans képek arányát a jelentésben fel kell tüntetni.

13.1.8 Tipikus tévedés, önellenőrzés és visszahívás

Tévhit: a konstanssal való átskálázás csak „mértékegységet” cserél, ezért nem változtatja a döntést. Ez akkor igaz, ha a modell paramétereit a skálának megfelelően értelmezzük; egy fix, [0,1] bemenetre tanított modellnél nem.

Ellenpélda (számított, nem mért). Legyen w=(1,−2,0,5)⊤w=(1,-2,0{,}5)^\top, b=−0,6b=-0{,}6, bemenet x=(0,4,0,1,0,7)⊤x=(0{,}4,0{,}1,0{,}7)^\top. Az affin pontszám z=w⊤x+b=0,4−0,2+0,35−0,6=−0,05z=w^\top x+b=0{,}4-0{,}2+0{,}35-0{,}6=-0{,}05. Ugyanezt a képet nyers [0,255][0,255] skálán tárolva x′=(102,25,5,178,5)x'=(102,25{,}5,178{,}5), és z′=102−51+89,25−0,6=139,65z'=102-51+89{,}25-0{,}6=139{,}65. Egy sign\operatorname{sign}-döntő osztályozó előjele negatívból pozitívba vált, tehát a skálaváltoztatás pusztán az adatbeadásnál megváltoztatja a modell inputjelentését. Ugyanígy az NCHW–NHWC csere nem no-op: az Állítás 2 megőrzi a pixelvektort, de a lapított memóriaképet átrendezi.

Önellenőrző lista. Minden tömbhöz felírtam-e az (N,C,H,W)(N,C,H,W) alakot, a dtype-ot és a skálakorlátot? A kettős transzponálást np.array_equal-rel ellenőriztem? A lapított offset-képletet egész indexszámítással teszteltem, nem pixelérték-heurisztikával? A normalizálásnál kezeltem a konstans kép nullosztóját?

Visszahívás. A 4.4. I. ellenőrző pont: matematika és kód megfelelése alfejezetből: numerikus egyezést dtype-, shape- és hibahatár-megadással ellenőrzünk; jelen egységben a transzpozíció bitazonos, ezért a hiba pontosan 00. A 11.4. Egy teljes PyTorch tanítási iteráció alfejezetből: a modell első rétegének in_channels várakozása és a betöltött kép csatornatengelye ugyanaz a szerződés; a dimenziórendezési hiba gyakran nem dob kivételt, hanem csendesen félreértelmezi a koordinátákat.

13.1.9 Rövid összefoglaló

A mintavételi rács a 𝒢(H,W)={0,…,H−1}×{0,…,W−1}\mathcal G(H,W)=\{0,\ldots,H-1\}\times\{0,\ldots,W-1\} indexhalmaz; a pixel az egy rácsponthoz tartozó ℝC\mathbb R^C csatornavektor, az intenzitás a konvenció szerinti skalár. Az NCHW tensor indexeit (n,c,h,w)(n,c,h,w), az NHWC-ét (n,h,w,c)(n,h,w,c) azonosítja; a Yn,h,w,c=Xn,c,h,wY_{n,h,w,c}=X_{n,c,h,w} permutáció invertálható, ezért adat nem vész, de a lineáris memóriacím megváltozik. Bizonyítottuk az NCHW C-sorrendű offset-képletét és bijektivitását, a kettős transzponálás identitását, valamint a q/255q/255 skála monoton voltát. A labor kis mesterséges képpáron a két elrendezést alakkal, osztott memóriával és pixel-egyezéssel ellenőrzi.

13.1.10 Következő alkalmazás

A 13.2. Konvolúció és cross-correlation a kernelmegfordítás nélküli cross-correlationt és a matematikai konvolúciót vezeti be ugyanezen a rácson; az itt rögzített NCHW-indexelés a súly- és kimeneti indexek levezetésének alapja.

13.1.11 Források