Ennek az egységnek a célja, hogy a programozó a ndarray,
dtype, shape és axis fogalmai
után 2.1. Python, NumPy és tenzoralak megértse:
a kódban tárolt szám nem ugyanaz az objektum, mint a matematikai valós
szám, és a kettő közötti eltérést explicit hibamértékkel kell
ellenőrizni. A tanulás végére meg tudja különböztetni a matematikai
egyenlőséget a numerikus közelítő egyenlőségtől, indokolni tudja az
allclose tűréseit, és stabilan tudja kiszámítani a
mennyiséget nagy, illetve negatív bemenetekre. Előismeret: a Python
skalár, az np.ndarray, a dtype, a broadcasting
és a redukció alapjai 2.1. Python, NumPy és
tenzoralak; az állítástípusok megkülönböztetése 1.2. Szakmailag megalapozott modellezési
állítások.
Induló kérdés: miért lehet, hogy a 0.1 + 0.2 == 0.3
kifejezés False értéket ad Pythonban, miközben a három szám
tizedestört-alakban pontosan ennyi? És mi lehet a helyes ellenőrzés, ha
két, elvileg azonos eredményű számítást akarunk összevetni?
Definíció 1: float32 és float64 normális számok. A
kódban használt float32 és float64 az IEEE 754
bináris lebegőpontos formátumra épülő, véges pontosságú ábrázolás. Egy
normális float32 szám
ahol
az előjelbit,
a 23 bites tört rész,
pedig 8 bites,
kitevőmező. A kettes számrendszerbeli vezető
implicit, ezért összesen 24 bites pontosságról szokás beszélni. A
float64 analóg: 1 előjelbit, 11 bites kitevő, 52 bites
explicit tört, 53 bites teljes pontosság, kitevőeltolás 1023. A nullához
közeli, nem normális számok a szubnormális tartományt alkotják, amelyet
itt nem használunk részletesen.
Jelölések és alapértékek. Legyen a pontos valós szám, a reprezentált érték. A gépi epszilon a legkisebb olyan , amelyre a reprezentációban már különbözik -től:
Az egységnyi kerekítési lépés, az ulp (unit in the last place) normális tartományban
ahol
float32 és
float64 esetén. A legnagyobb normális float32
érték
,
míg a legnagyobb float64 érték
.
A legkisebb pozitív normális float32
.
Definíció 2: overflow, underflow, cancellation, NaN.
inf vagy -inf lesz. Például
np.exp(np.float32(1000)) túlcsordul, mert
,
tehát
már nem fér el float32-ben.0.0 / 0.0 vagy np.inf - np.inf eredménye. A
NaN nem egyenlő önmagával: np.nan == np.nan
False; az ellenőrzésre az np.isnan való.Példa a kerekítésre. A 0.1 tizedestört
nem véges kettes tizedestört, ezért sem float32, sem
float64 nem tárolja pontosan. Emiatt a
0.1 + 0.2 == 0.3 kifejezés False lehet: a
három tizedestört külön-külön kerekített közelítése, és a kerekítések
után a kettes számrendszerbeli összeg nem pontosan a 0.3
kerekített értéke. A jelenség nem hiba a hardverben, hanem a véges
ábrázolás következménye.
Definíció 3: hibamértékek. Legyen a pontos matematikai érték, a kódban kapott érték. Az abszolút hiba
a relatív hiba pedig esetén
Ha , a relatív hiba nem értelmes; ilyenkor abszolút hibát vagy nullához közeli nevezővel stabilizált mennyiséget kell használni.
Vizsgáljuk meg a float32 összeadás
és
példáján. A pontos matematikai összeg
.
A float32 ábrázolásban
pontosan megvan, mert a 24 bites szignifikandban az egyetlen vezető
egyes és utána 23 nulla éppen elfér. A következő ábrázolható érték ezen
a tartományon
,
tehát
A pontosan félúton van a két reprezentálható szám között. A leggyakrabban használt kerekítési szabály, a legközelebbi értékre kerekítés döntetlennél a páros alsó bittel rendelkező értéket választja; itt ez a érték. Ezért a tárolt összeg
Az abszolút hiba tehát
a relatív hiba pedig
Ezt kétféleképpen is értelmezhetjük. Egyrészt a hiba
ulp, mert az ulp most
,
és
.
Másrészt a relatív hiba
,
ami a float32 gépi epszilon felével, körülbelül
-nel
egyenlő. A levezetésből látszik a két hibamérték eltérő szerepe: az
abszolút hiba értékfüggő, nagyobb számnál nagyobb lehet, a normális
tartományban a szabályos kerekítés relatív hibája viszont korlátos; a
példában a korlát
.
Korlát mint alkalmazási szabály, nem általános
tétel. A fenti megfigyelés a normális tartományra és a
legközelebbi értékre kerekítésre vonatkozik. Szubnormális számokra és
túlcsorduló eredményekre nem ad ilyen relatív garanciát. A gyakorlatban
ezért nem a gépi epszilon önmagában, hanem az adott számítás skálájához
igazított atol és rtol a helyes ellenőrzési
eszköz.
A többosztályos modellek későbbi tárgyalásához szükségünk lesz a log-sum-exp műveletre. Legyen egy logitvektor. Ekkor
Ha a
értékek nagyok, például
körüliek, a naiv np.exp hívás float32-ben
túlcsordul. A cél egy olyan átalakítás, amely ugyanazt a matematikai
értéket adja, de véges közbülső értékekkel.
Állítás 1: a max-levonásos stabil alak. Legyen , minden véges, és legyen . Ekkor
Levezetés. Az tagok pozitívak, ezért az összeg pozitív. Az exponenciális szorzási azonossága alapján
A természetes logaritmus pozitív számra szigorúan monoton, és a szorzat logaritmusa a tagok logaritmusának összege:
Mivel , a keresett azonosságot kapjuk. A levonás után minden , tehát minden érték a tartományba esik, az összeg -ba, a logaritmus pedig -ba. Ezért az exponenciális túlcsordulás elkerülhető, feltéve, hogy maga véges.
Megjegyzés a tartományokról. Ha a legnagyobb logit
is inf, a stabil formula nem segít; az inf
bemenet vagy korábbi numerikus hiba, vagy olyan logit, amelyet előbb
kezelni kell. Ha a logitok között hatalmas a különbség, a kis tagoknál
az
alulcsordulhat nullává; ez a végösszeget általában nem rontja a
gyakorlatban, mert a legnagyobb tagnak dominálnia kell, de az
alulcsordulás tudatos korlát.
Legyen
és
.
A pontos matematikai eredmény float64-ben:
összegük , ennek természetes logaritmusa , így
A naiv út float32-ben:
np.exp(np.float32(1000)) inf, az összeg
inf, a logaritmus inf. A stabil út
float32-ben
,
a levonás [-2, -1, 0] tartományú, az eredmény körülbelül
lesz. A float32 kerekítés okozta abszolút hiba körülbelül
az ulp feléig terjedhet;
körül az ulp
,
tehát az eltérés jellemzően
nagyságrendű. A float64 referencia és a
float32 stabil eredmény összevetéséhez ezért az abszolút
tűrést az ulp nagyságrendjéhez kell igazítani.
A labor kódja kis CPU-s NumPy-környezetben fut. Nincs véletlen seed,
a műveletek determinisztikusak; a dtype explicit
float32, az eszköz a CPU. A futtatási parancs a
projektkörnyezetben:
python n006_lab.pyHa a projekt uv-kezelt környezet, akkor a parancs
uv run python n006_lab.py, a projektkonfigurációról lásd
[D01].
# n006_lab.py — naiv és stabil logsumexp nagy logitokon
import numpy as np
# Bemenet: 1-D float32 logitvektor
z = np.array([1000.0, 1001.0, 1002.0], dtype=np.float32)
def naive_logsumexp(v):
# v alakja (K,), float32; itt overflow várható
return np.log(np.sum(np.exp(v)))
def stable_logsumexp(v):
# v alakja (K,), float32
m = np.max(v) # max-logit, skalár float32
shifted = v - m # (K,) float32, minden elem <= 0
return m + np.log(np.sum(np.exp(shifted)))
# float32 eredmények
naive = naive_logsumexp(z)
stable = stable_logsumexp(z)
# float64 referencia, a NumPy beépített stabilitását használva
ref64 = np.logaddexp.reduce(z.astype(np.float64))
print("naive:", naive)
print(" finite:", bool(np.isfinite(naive)))
print("stable:", stable)
print(" finite:", bool(np.isfinite(stable)))
print("ref64: ", ref64)
# Ellenőrzés: a stabil float32 érték véges legyen, és
# a float64 referenciához képest a skálához illő tűréssel egyezzen.
assert np.isfinite(stable), "A stabil eredmény nem véges."
assert np.allclose(stable, ref64, rtol=1e-6, atol=2e-4), (
"A stabil float32 eredmény nem esik a megadott tűrésen belülre."
)
# Negatív nagy logitokon is véges kontroll
z2 = np.array([-1000.0, -1001.0, -1002.0], dtype=np.float32)
print("stable negatív:", stable_logsumexp(z2))
print("finite negatív:", bool(np.isfinite(stable_logsumexp(z2))))A kimenet az alábbi számított szemléltetés; a konkrét utolsó jegyek a NumPy-verziótól és a lebegőpontos kerekítéstől függhetnek, ezért a kézirat ezt nem állítja mért futásnak:
naive: inf
finite: False
stable: 1002.4076
finite: True
ref64: 1002.4076059645616
stable negatív: -999.5924
finite negatív: True
Az np.allclose ellenőrzés az alábbi egyenlőtlenséget
vizsgálja elemenként [D04]:
Ebben a mérésben
a float64 referencia. Mivel
körülbelül
,
a relatív tag
lenne önmagában; az atol=2e-4 szigorúbb, és a
float32 várt ulp-fél-skáláját,
-öt,
kényelmesen lefedi. A negatív bemenetnél a max-levonás miatt ugyancsak
nincs túlcsordulás, és az eredmény véges marad.
A labor értelmezése: a naiv alak inf-et ad, tehát a
matematikai érték elveszett; a stabil alak véges, és a magasabb
pontosságú referenciával való egyezése a skálához illő abszolút és
relatív hibahatáron belül van. Ez a numerikus egyezés implementációs
ellenőrzés, nem a matematikai azonosság bizonyítéka; az azonosságot az
Állítás 1 levezetése bizonyítja.
F1 (fogalmi, nehézség 1). Magyarázza el, miért lehet
egy matematikailag igaz egyenlőség a kódban hamis. Térjen ki a tárolt
érték, a kerekítés és az allclose szerepére. Adjon egy
saját kis példát is.
F2 (levezetési, nehézség 2). A float32
ábrázolásban
,
az ulp ezen a tartományon
.
Mi lesz a tárolt érték, ha a float32 reprezentációban
kiszámítjuk
-et?
Vezesse le az abszolút és relatív hibát.
F3 (implementációs, nehézség 2). Írjon
stable_softmax függvényt, amely egy tetszőleges
(N, K) alakú float32 logitmátrix minden sorára
kiszámítja a stabil softmaxot. A függvény először vonja le a sor
maximumát, majd normalizáljon. Ellenőrizze, hogy minden sor összege
-hez
közeli, és a [[1000, 1000], [0, 1]] bemenetre a kimenet
[[0.5, 0.5], [1/(1+e), e/(1+e)]] közelítő értéke.
F4 (hibakeresési, nehézség 2). Az alábbi kód
float32-ben egy varianciaszámítást végez:
x = np.array([1_000_000.0, 1_000_000.1, 1_000_000.2], dtype=np.float32)
mean_x = np.mean(x)
var_naive = np.mean(x * x) - mean_x * mean_x
print(var_naive)Mi a matematikai variancia az adott három értékre, és miért kaphat a
kód hibás, akár negatív eredményt? Javítsa a számítást központosítással,
és adja meg a helyes ellenőrzést float64 referenciával.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen egy
kis protokollt annak eldöntésére, hogy egy saját float32
függvény és a float64 referencia implementációja egy adott
bemeneti tartományon elfogadhatóan egyezik-e. A protokollban adja meg: a
bemeneti vektorok számát és tartományát, a megfigyelési egységet, a
kiszámított hibamérőket, a hibahatár indoklását és a döntési szabályt. A
feladat nem egy előre megjósolt győztest, hanem egy lefuttatható tervet
kér.
F1 megoldása. A matematikai egyenlőség a valós
számokon értett azonosság. A kódban a számok véges
dtype-beli közelítések, a műveletek pedig ezeken a
közelítéseken hajtódnak végre, ezért a kerekítési hibák műveletenként
halmozódnak. Az 0.1 + 0.2 == 0.3 példában a
0.1, 0.2 és 0.3 egyike sem pontos
kettes számrendszerbeli tört, és a közelítő összeg nem pontosan a
0.3 kerekített közelítése. A helyes ellenőrzés nem a pontos
egyenlőség, hanem az alkalmazás skálájához illő abszolút és relatív
tűréssel végzett összevetés, például
np.allclose(a, b, rtol=..., atol=...). Ugyanakkor az
allclose használata sem bizonyít matematikai azonosságot;
csak azt jelenti, hogy az adott bemeneteken a két implementáció
közelítőleg egyezik.
F2 megoldása. A pontos matematikai összeg . A tartományban az ulp , tehát a reprezentálható számok A távolsága -től , a következő reprezentálható értéktől, -től pedig . A legközelebbi értékre kerekítés ezért a értéket választja, mert nincs döntetlen:
Az abszolút hiba:
a relatív hiba pedig
Alakellenőrzés: a hiba
kisebb, mint az ulp
,
és a relatív hiba kisebb, mint a float32 gépi epszilon,
,
összhangban a normális tartomány kerekítési korlátjával. Szélsőeset: ha
a bemenet pontosan
,
a hiba nulla; ha döntetlen lenne két szomszédos reprezentálható szám
között, a páros alsó bit szabálya dönt.
F3 megoldása. A softmax stabil alakja minden logitra, mellett
A függvény:
import numpy as np
def stable_softmax(Z):
# Z alakja (N, K), float32
m = np.max(Z, axis=1, keepdims=True) # (N, 1)
E = np.exp(Z - m) # (N, K)
return E / np.sum(E, axis=1, keepdims=True) # (N, K)
Z = np.array([[1000.0, 1000.0],
[0.0, 1.0]], dtype=np.float32)
P = stable_softmax(Z)
print(P)
print("Sorösszegek:", np.sum(P, axis=1))A kimenet számított ellenőrzés: az első sor [0.5, 0.5],
a második sor [0.26894143, 0.7310586] közelítőleg. A
sorösszegek [1., 1.]; float32 kerekítéssel a
sorösszeg legfeljebb néhány ulp-nyit térhet el
-től,
ezért az ellenőrzés például
assert np.allclose(np.sum(P, axis=1), 1.0, atol=1e-6)
lehet. A stabilizálás nélkül az első sor inf/inf NaN-t
adna, mert
float32-ben túlcsordul.
F4 megoldása. A matematikai variancia a három
értékre: az átlag pontosan
,
az eltérések
,
így a variancia
.
A float32 x * x értékek
nagyságrendűek; a tárolási hiba ezen a skálán abszolút értelemben már
lényegesen nagyobb lehet, mint a keresett
variancia. A
képlet ekkor katasztrofális kivonást hajt végre, és a különbség
negatívvá vagy nullává válhat. A javítás a központosított alak:
x = np.array([1_000_000.0, 1_000_000.1, 1_000_000.2], dtype=np.float32)
d = x - np.mean(x)
var_center = np.mean(d * d)
x64 = x.astype(np.float64)
ref = np.mean((x64 - np.mean(x64)) ** 2)
print("javított:", var_center)
print("referencia:", ref)A javított érték nem negatív, és a float64 referencia
körülbelül
.
Az ellenőrzésnek itt is a várható abszolút hibát kell figyelembe vennie;
a központosítás megszünteti a
nagyságrendű köztes értékeket, ezért a hiba a float32
epszilon skálájához közeli lehet.
F5 megoldásmintája. Egy lehetséges protokoll:
float32 függvény és a
float64 referencia adott tartományon egyezik-e.float32 relatív kerekítési hibája
a normális tartományban
körüli, ezért
relatív határ kényelmes; abszolút határnak az ulp
becsléséből
adódik a használt logit-tartományban.Az algebrailag ekvivalens képletek nem feltétlenül numerikusan egyenértékűek. Tekintsük a
azonosságot. Matematikailag
minden
-ra,
mert a második az első gyöktelenített alakja. Nagy
-re
float32-ben a
és
értékek kerekítve egyenlővé válhatnak, így
nullát adhat, miközben a pontos érték pozitív. A második alak nem végez
közeli értékű tagok kivonását, ezért a pontos értékhez közeli eredményt
ad. Az ellenpélda tanulsága: az ekvivalens átalakítás a matematikai
objektumon nem jelenti azt, hogy a lebegőpontos implementációk is
egyformán stabilak.
Az abszolút hiba mérési skálától függ, a relatív hiba a mérési skálát
kiküszöböli, de nullánál nem értelmes. A np.allclose két
tűrést kombinál, és a b értékkel arányos relatív tűrést
használ. Ha egy ellenőrzés inf vagy NaN értéket tartalmaz,
az np.allclose alapértelmezésben False-t ad; a
np.isfinite és np.isnan először a
végesérték-kontroll. A korábbi fogalomhoz visszatérve: a
dtype és a shape előzetes rögzítése nélkül a
numerikus hiba sem lokalizálható 2.1. Python, NumPy
és tenzoralak. A numerikus teszt adott bemenetre vonatkozó empirikus
állítás; a matematikai azonosság bizonyítása a levezetés típusú állítás
1.2. Szakmailag megalapozott modellezési
állítások.
A float32 és float64 véges pontosságú
ábrázolás, a hibát abszolút és relatív értékkel mérjük. A
0.1 + 0.2 == 0.3 hamis volta a tizedestört kerekítésének
következménye. Az overflow, a katasztrofális kivonás, a NaN és az ulp
fogalma megmagyarázza, miért nem helyettesíthető a pontos egyenlőség
könyvtári allclose nélkül. A stabil logsumexp a maximum
levonásával elkerüli az exponenciális túlcsordulást, és a kis példában
véges
értéket ad az inf helyett. A numerikus egyezés
implementációs ellenőrzés; a matematikai eredményt levezetés igazolja, a
toleranciát pedig az adott skála és dtype kerekítési tulajdonságaihoz
kell indokolni.
A 2.3. Izolált környezet, notebook és verzió
egység izolált Python-környezetet, notebook-használatot és verziózást
tárgyal. Az ottani verziórögzítés és újrafuttathatóság teszi
ellenőrizhetővé az itt bemutatott tolerancia- és végesérték-teszteket; a
környezetváltozás ugyanis ugyanolyan hibridet okoz, mint egy
megváltozott dtype.