2.1 Python, NumPy és tensoralak

2.1.1 Cél és indító becslés

Ez az egység a NumPy ndarray objektumán keresztül rögzíti a skalár, vektor, mátrix és négydimenziós képtensor közötti mozgás szabályait. A cél nem a NumPy API katalógusa, hanem az, hogy minden művelet előtt és után egyértelmű legyen a shape, a dtype és a redukált tengely. Az objektumok alakja a modell bemeneti és kimeneti szerződésének része 1.1. Modell, tanulás és felhasználási szerződés; a tévesen értelmezett alak csendben hibás számolást adhat.

Indító becslés: legyen xx alakja (4,), yy alakja (4,1). Az x + y kifejezés a NumPy-értelmezés szerint milyen alakot ad? Becsülje meg, mielőtt tovább olvas, és jegyezze fel a tippjét. Ha (4,4)-et tippelt, a broadcasting szabályát már érzi; ha (4,)-et, akkor éppen ez az egység fogja tisztázni, miért nem az.

2.1.2 ndarray, dtype, shape és axis

Definíció (ndarray). A NumPy ndarray homogén, rögzített alakú, többdimenziós tömb: minden elemének azonos a típusa, az alakot a létrehozás után megváltoztatni csak új tömb létrehozásával lehet. A memóriában az elemek lineáris sorrendben tárolódnak; a többdimenziós indexelést a shape és a hozzá tartozó strides írja le. Az ndarray.ndim a dimenziók száma, ndarray.size az elemek darabszáma, ndarray.shape pedig egy tuple, amelynek ii-edik eleme az ii-edik tengely hossza.

Definíció (dtype). A dtype az elemi tárolási típus: np.float32, np.float64, np.int64, np.uint8, np.bool_ stb. A dtype nem tartalmaz mértékegységet, és a shape sem; mindkettő dimenzió nélküli tárolási és szerkezeti információ 1.4. Tanulási diagnózis és kísérleti napló. A jelentést (például „intenzitás a [0,1][0,1] intervallumban”) a programozó dokumentálja, nem a tömb hordozza. A könyv főútja float32 a tanításhoz, mert ez a tipikus CPU/GPU numerikus típus; az ellenőrzésekhez helyenként float64-et használunk.

Definíció (axis). Az axis a redukciós műveletek tengelyparamétere. Az axis=0 az első, axis=1 a második tengely mentén összegez; negatív index a végétől számol. axis=None az összes elem feletti redukciót jelenti. Például X∈ℝN×C×H×WX \in \mathbb{R}^{N\times C\times H\times W} esetén axis=0 a batch, axis=1 a csatorna tengely.

A Python-szintaxis egyetlen pontja, amely itt félreérthető: a (n,) egyelemű tuple, tehát nn hosszú egydimenziós vektor; a (n,1) kétdimenziós oszlopvektor. A kettő nem ugyanaz az objektum, és a broadcasting eltérően kezeli őket.

2.1.3 Broadcasting alakszabálya és a külső összeg lemmája

Definíció (broadcasting alakszabálya, [D04] szerinti megfogalmazás). Legyen a két operandus alakja sA=(a1,…,ar)s_A=(a_1,\dots,a_r) és sB=(b1,…,bq)s_B=(b_1,\dots,b_q). A rövidebb alak elé írjunk 1-eseket, amíg a két tuple hossza egyenlő nem lesz, majd jobbról balra igazítsuk a méreteket. Az elemenkénti bináris művelet akkor és csak akkor értelmezett, ha minden igazított kk pozícióban ak=bka_k=b_k, vagy ak=1a_k=1, vagy bk=1b_k=1. Az eredmény alakja pozíciónként a nagyobbik méret. A végrehajtáskor az 1 méretű dimenzió virtuálisan ismétlődik az eredmény méretére.

Ez a szabály a NumPy API definíciója, nem tétel; a stride-0 implementáció a [D04] hivatalos leírásában ellenőrizendő. Az alábbi lemma a szabály első, gyakran félreértett következménye.

Lemma (külső összeg). Legyen a∈ℝna \in \mathbb{R}^n alakja (n,) és b∈ℝn×1b \in \mathbb{R}^{n\times 1} alakja (n,1). Ekkor az a + b művelet eredménye (n,n) alakú, és az eredmény Cij=aj+biC_{ij}=a_j+b_i.

Bizonyítás. A jobbról igazított alakok: aa esetén (1,n)(1,n) — a hiányzó első dimenziót 1-nek tekintjük —, bb esetén (n,1)(n,1). Az első pozícióban 11 és nn szerepel, tehát broadcastelhető; a másodikban nn és 11, szintén broadcastelhető. Az eredmény alakja (max(1,n),max(n,1))=(n,n)(\max(1,n),\max(n,1))=(n,n). A virtuális ismétlés következménye: aa a második tengelyen nn hosszú, ezért a kimenet minden sorában megismétlődik, vagyis az (i,j)(i,j) elemhez aja_j járul; bb az első tengelyen nn hosszú, a második tengelyen 1, tehát minden oszlopban ismétlődik, vagyis az (i,j)(i,j) elemhez bib_i járul. Az összeadás elemenkénti, így Cij=aj+biC_{ij}=a_j+b_i. ∎

A lemma mutatja, hogy a (n,) vektor és a (n,1) oszlopvektor összeadása nem a kívánt komponensenkénti összeadás, hanem az összes lehetséges pár összegét tartalmazó mátrix. Ez a későbbi hibajavító labor központi esete.

2.1.4 Kézi példa NCHW alakellenőrzéssel

Legyen a vizsgált képtensor

X∈ℝ2×1×2×3,X=[[012345][67891011]], X \in \mathbb{R}^{2\times 1\times 2\times 3},\qquad X = \begin{bmatrix} \begin{bmatrix} 0 & 1 & 2\\ 3 & 4 & 5 \end{bmatrix}\\ \begin{bmatrix} 6 & 7 & 8\\ 9 & 10 & 11 \end{bmatrix} \end{bmatrix},

ahol az egydimenziós csatornát az áttekinthetőség kedvéért elhagytuk a leírásból, de a shape (2,1,2,3) és a dtype float32. A matematikai indexelés 1-től, a Python 0-tól számol; az alábbiakban a Python-tengelyeket használjuk.

A teljes átlag:

X.𝚖𝚎𝚊𝚗()=12⋅1⋅2⋅3∑n=01∑c=00∑h=01∑w=02Xn,c,h,w=6612=5,5, X.\texttt{mean()} = \frac{1}{2\cdot1\cdot2\cdot3}\sum_{n=0}^{1}\sum_{c=0}^{0}\sum_{h=0}^{1}\sum_{w=0}^{2} X_{n,c,h,w} = \frac{66}{12} = 5{,}5,

amely 0-dimenziós tömb, alakja (). A batch-átlag axis=0 mentén redukál, tehát a két kép pixelenkénti átlaga:

X‾c,h,w=12(X0,c,h,w+X1,c,h,w),X‾=[345678], \bar{X}_{c,h,w} = \frac{1}{2}\bigl(X_{0,c,h,w}+X_{1,c,h,w}\bigr),\qquad \bar{X} = \begin{bmatrix} 3 & 4 & 5\\ 6 & 7 & 8 \end{bmatrix},

alakja (1,2,3). A csatornaátlag a batch, magasság és szélesség felett átlagol, axis=(0,2,3), eredménye (1,) és értéke [5,5][5{,}5]. A képenkénti átlag axis=(1,2,3), eredménye (2,), értékei [2,5,8,5][2{,}5,\ 8{,}5], mert a két kép elemeinek összege 15, illetve 51, egyenként 6 elemmel osztva.

Vegyük észre a tengelyek szerepét: ugyanaz a XX más-más kérdésre más alakú eredményt ad. A modellkódban a mean tengelyparamétere ezért része a szerződésnek.

2.1.5 Labor: batch-átlag és hibajavítás

A labor egy kis, helyi CPU-s NumPy-szkript. A device paraméter itt nem értelmes; az izolált uv-projektet a lockfájl szerint szinkronizáljuk [D01]. A munkakönyvtár lab/n005, a futtatási parancs:

uv sync
python lab/n005/lab_batch_mean.py

A szkript:

# lab/n005/lab_batch_mean.py
"""Kézi és NumPy batch-átlag, valamint a (n,)+(n,1) hiba javítása."""
import numpy as np

X = np.array([[1.0, 0.0, 2.0],
              [0.0, 2.0, 1.0],
              [2.0, 1.0, 0.0],
              [1.0, 1.0, 2.0]], dtype=np.float32)

# Kézi átlag oszloponként: shape (3,)
manual_mean = np.array([X[:, j].mean() for j in range(X.shape[1])],
                       dtype=np.float32)
np_mean = X.mean(axis=0)
print("X shape:", X.shape)
print("manual_mean:", manual_mean)
print("np_mean:", np_mean)

# Hiba: (3,) és (3,1) összeadása (3,3) mátrixot ad
x = np.array([1.0, 2.0, 3.0], dtype=np.float32)      # (3,)
y = np.array([[10.0], [20.0], [30.0]], dtype=np.float32)  # (3,1)
bad = x + y          # (3,3) -- csendes, de rossz művelet
good = x + y[:, 0]   # (3,)  -- explicit 1D vektor
print("bad shape:", bad.shape)
print("good shape:", good.shape, good)

A kimenet kézi számolásból származó várt érték, nem futtatott mérés:

X shape: (4, 3)
manual_mean: [1.   1.   1.25]
np_mean: [1.   1.   1.25]
bad shape: (3, 3)
good shape: (3,) [11. 22. 33.]

Az átadandó eredmény: a kézi és a NumPy-átlag egyezése, a bad és good alakok megkülönböztetése, valamint a futtatott szkript kimenete. A seed itt nem szükséges, mert az adatok determinisztikusan vannak megadva; a véletlen adatok generálásakor viszont a [D02] szerint az azonos seed nem garantál platformok közötti bitazonosságot, ezért a protokoll a seed mellett a NumPy-verziót is rögzíti.

2.1.6 Feladatok

F1 (fogalmi, nehézség 1). Magyarázza el, miért nem felcserélhető a (n,), (n,1) és (1,n) alak. Konkrétan: legyen XX alakja (5,8), b1b_1 alakja (8,), b2b_2 alakja (1,8), b3b_3 alakja (8,1). Mely X+b_i kifejezések értelmesek, és mi az eredmény alakja?

F2 (levezetési, nehézség 2). Legyen AA alakja (3,4), bb alakja (4,), cc alakja (3,1). A broadcasting szabályból vezesse le, hogy A+b alakja (3,4) és eleme Aij+bjA_{ij}+b_j; mutassa meg azt is, hogy b[:,None] + c.T alakja (4,3) és eleme bi+cjb_i+c_j. Mindkét állítást igazolja az alakszabály pozíciónkénti alkalmazásával.

F3 (implementációs, nehézség 2). Írjon batch_and_channel_mean(x) függvényt, amely x alak (N,C,H,W) bemenetre visszaadja a batch-átlagot ((C,H,W)) és a csatornaátlagot ((C,)) float32 típusban. Ellenőrizze a fenti 2×1×2×3 példán az np.testing.assert_allclose eszközzel; a tűrés rtol=1e-7, atol=1e-7.

F4 (hibakeresési, nehézség 2). Az alábbi függvény hibát okoz, amikor a hívó (4,3) kimenetet vár:

def add_bias(X, b):
    return X + b   # X.shape == (4,3), b.shape == (3,1)

Azonosítsa a hiba okát a broadcasting szabály alapján, adja meg a következményt, javítsa a függvényt úgy, hogy a kimenet (4,3) legyen, és ellenőrizze egy konkrét tömbön.

F5 (kísérlettervezési, nehézség 3). Tervezzen kis kísérletet annak eldöntésére, hogy egy adott preprocess(images) függvény a csatornaátlag kivonását batch-szintű statisztikával végzi-e, vagy rögzített, előre kiszámolt átlaggal. Adja meg a kérdést, a két összehasonlítandó útvonalat, a használt GlyphLab-állományt, a metrikát és a következtetés érvényességi korlátját.

2.1.7 Megoldások és indoklás

F1 megoldás. A (n,) egydimenziós, a (n,1) kétdimenziós oszlop-, a (1,n) kétdimenziós sorvektor. X+b1X+b_1: jobbról igazítva b1b_1 alakja (1,8), az első pozíció 55 és 11 broadcastelhető, a második 88 és 88 egyenlő, eredmény (5,8). X+b2X+b_2: alakok (5,8) és (1,8), ugyancsak (5,8). X+b3X+b_3: alakok (5,8) és (8,1); jobbról a második pozíció 88 és 11 broadcastelhető, de az első pozíció 55 és 88 sem nem egyenlő, sem nem 1, tehát ValueError. Az oszlopvektor-bias nem adható hozzá soronként; csak akkor lenne jó, ha XX első dimenziója is 8 lenne.

F2 megoldás. A+b: a b alakja (4,), helyettesítve (1,4). Az első pozíció 33 és 11 → broadcast, a második 44 és 44 → egyenlő. Az eredmény (3,4). Az (i,j)(i,j) kimeneti elemhez AijA_{ij} járul; bb stride-0 első dimenziója miatt minden sorban ugyanaz a bb ismétlődik, tehát a hozzájárulás bjb_j. b[:,None] + c.T: a bal oldal alakja (4,1), a jobb oldalé (1,3). Az első pozíció 44 és 11, a második 11 és 33, mindkettő broadcastelhető; eredmény (4,3). A bal oldal a második tengelyen ismétlődik, ezért eleme az (i,j)(i,j) pontban bib_i; a jobb oldal az első tengelyen ismétlődik, ezért cjc_j; az összeg bi+cjb_i+c_j.

F3 megoldás.

import numpy as np

def batch_and_channel_mean(x: np.ndarray):
    assert x.ndim == 4, f"Várt shape (N,C,H,W), kapott: {x.shape}"
    batch_mean = x.mean(axis=0)            # (C,H,W)
    channel_mean = x.mean(axis=(0, 2, 3))  # (C,)
    return batch_mean.astype(np.float32), channel_mean.astype(np.float32)

X = np.arange(12, dtype=np.float32).reshape(2, 1, 2, 3)
batch, channel = batch_and_channel_mean(X)
expected_batch = np.array([[[3, 4, 5], [6, 7, 8]]], dtype=np.float32)
expected_channel = np.array([5.5], dtype=np.float32)
np.testing.assert_allclose(batch, expected_batch, rtol=1e-7, atol=1e-7)
np.testing.assert_allclose(channel, expected_channel, rtol=1e-7, atol=1e-7)
print("OK", batch.shape, channel.shape)  # OK (1, 2, 3) (1,)

A szélsőeset: N=1N=1 esetén a batch-átlag maga a kép, a csatornaátlag a képenkénti átlaggal egyezik.

F4 megoldás. A hiba oka: (4,3) és (3,1) jobbról igazítva az első pozícióban 44 és 33 áll, amelyek sem egyenlők, sem 1, tehát a broadcasting szabály szerint a művelet nem értelmezett; a NumPy ValueError-t dob. A következmény az, hogy a hívó egyáltalán nem kap kimenetet. A javítás: alakítsuk a bias-t egydimenzióssá, például b.reshape(-1) vagy b[:, 0]; ekkor az alak (3,), a broadcasting (1,3)-ként értelmezi, és az eredmény (4,3). Ellenőrzés:

def add_bias(X, b):
    return X + b.reshape(-1)

X = np.ones((4, 3), dtype=np.float32)
b = np.array([[1.0], [2.0], [3.0]], dtype=np.float32)
out = add_bias(X, b)
assert out.shape == (4, 3)
np.testing.assert_allclose(out[0], [2.0, 3.0, 4.0], rtol=1e-7, atol=1e-7)

F5 megoldásminta. Kérdés: a preprocess a csatornaátlag kivonásakor a batch statisztikáját használja-e, ami eltérő tanítási és inference-viselkedést okozhat. Protokoll: a GlyphLab data/images.npy állomány (360,1,24,24) alakú float32 tömbjéből vegyünk egy rögzített train batch-et, számítsuk ki a csatornaátlagot, és futtassuk a preprocess első útvonalát ezzel a rögzített átlaggal. A második útvonal ugyanarra a bemenetre batch-szintű átlagot számol, majd kivon. Metrika: a két kimenet maximális abszolút eltérése. Ha az eltérés 0, a függvény nem batch-függő; ha pozitív, a batch-szintű normalizálás jelen van. Érvényességi korlát: a mérés egy implementációs tulajdonságot mutat, nem a modellminőséget; a tanítás és kiértékelés közötti eltérés hatását külön, rögzített protokollú kísérletben kell mérni.

2.1.8 Gyakori tévedés: a lefutó broadcasting lehet rossz művelet

A legveszélyesebb eset nem az, amikor a broadcasting hibát dob, hanem amikor lefut, de más eredményt ad. A fenti lemmában x (3,) és y (3,1) összeadása (3,3) mátrixot hoz létre; ha a kód egy (3,) vektort vár, a hiba csak akkor derül ki, amikor egy későbbi függvény rossz alakot kap, vagy ami rosszabb, a modell csendben rossz értékekkel tanul.

Ellenpélda a „mindig működik, tehát jó” érvelésre: az X∈ℝ4×3X \in \mathbb{R}^{4\times 3} batch és a b∈ℝ3b \in \mathbb{R}^{3} bias összeadása soronkénti torzítást ad, ahogyan az F4 javítása mutatja; ha viszont valaki tévedésből a bias-t (4,1) alakban adja meg, a broadcasting csak akkor fut le, ha a batch mérete éppen 3, és az eredmény (4,3) helyett (4,3)? Nem: (4,3) és (4,1) kompatibilis, mert a második pozíció 33 és 11; az eredmény (4,3), és a bias oszloponként ismétlődik, ami a szándékkal ellentétes lehet, ha a bias soronkénti volt. A shape-assert és a kimenet explicit ellenőrzése ezért nem adminisztratív teher, hanem a modell bemeneti szerződésének része 1.1. Modell, tanulás és felhasználási szerződés.

A nézet/másolat különbsége is hasonló csendes hibát okozhat: az alapszeletelés nézetet (view) ad, amely osztozik a memórián, ezért

x = np.arange(6, dtype=np.float32).reshape(2, 3)
row = x[0]      # view
row[0] = -1.0   # x[0,0] is megváltozik

Ha független másolatra van szükség, explicit x.copy() kell. A Python-listák szeletelése ezzel szemben másolatot ad; a két viselkedés összekeverése nehezen lokalizálható hibákhoz vezet.

2.1.9 Önellenőrzés

  1. Mi az alakja a np.ones((3,1)) + np.arange(4) kifejezésnek? Válasz: ValueError, mert a jobbról igazított első méret 33 és 44 nem kompatibilis.
  2. Adott XX alak (2,1,2,3). Mi az alakja a X.mean(axis=0) és a X.mean(axis=1) kifejezésnek? Válasz: (1,2,3), illetve (2,2,3).
  3. A x[0] NumPy esetén nézet vagy másolat? Válasz: nézet, hacsak a kód nem készít explicit másolatot.

Visszahívás: az 1.1. Modell, tanulás és felhasználási szerződés alfejezetben a modell bemenete XX batch, a hipotézisosztály pedig a paraméterezett leképezések halmaza; a shape mindkettő szerződésének része. Az 1.4. Tanulási diagnózis és kísérleti napló diagnózisban a tensor alakját és dtype-ját különválasztottuk: az alak a tengelyek hossza, a dtype a tárolás típusa. A seed a generátor kezdőállapota, a futásazonosító a kísérleti napló kulcsa; ebben a laborban determinisztikus adat miatt seed nem kellett, de a jövőbeli véletlen adatgeneráláshoz a protokoll rögzíti [D02].

2.1.10 Rövid összefoglaló

Az ndarray homogén, rögzített alakú többdimenziós tömb; a dtype a tárolási típust, a shape a tengelyhosszakat, az axis a redukciós irányt adja meg. A broadcasting alakszabálya jobbról balra igazítva engedi meg az 1 méretű dimenziók kiterjesztését; a (n,) és (n,1) alakok összeadása ezért (n,n) mátrixot ad, nem vektort. A labor kézi és NumPy batch-átlagot számolt, majd a téves broadcastingból származó hibát explicit átalakítással javította. A nézet osztozik a memórián, a másolat független; alapszeletelésnél ez eltér a Python-lista viselkedésétől. A shape-ellenőrzés a modell bemeneti szerződésének része 1.1. Modell, tanulás és felhasználási szerződés.

2.1.11 Következő alkalmazás

A 2.2. Lebegőpont és stabil elemi számítás egység a lebegőpontos ábrázolás és a stabil elemi számítások felé lép: a float32 kerekítés, a katasztrofális törlés és a log-sum-exp alapjai ott épülnek erre a tensoralapra.

2.1.12 Források