Ez az alfejezet a 7.3. Bináris, multilabel és
regressziós cél alfejezetben bevezetett loss-szerződéseket vizsgálja
a lebegőpontos implementáció oldaláról. Cél, hogy a hallgató meg tudja
mondani, miért ad egy naiv képlet véges helyett NaN vagy
inf értéket nagy logitokra, és hogyan őrizhető meg a
matematikai jelentés a cross-entropy, a BCE és a kapcsolódó számítások
során. Előismeret: alapvető Python/NumPy, logits és target alakok,
softmax és BCEWithLogits definíciója, valamint a float32 alapvető
korlátai.
Induló kérdés: ha
,
akkor a
képlet matematikailag jól definiált. Miért számíthat mégis
NaN-t egy float32 NumPy implementáció, és melyik egyetlen
kivonás teszi stabillá?
Legyen logit-mátrix, egy sor . A softmax definíciója:
A cross-entropy logitsból közvetlenül a log-softmax negáltját használja:
Programozói értelemben a két alak nem ugyanazt a műveleti sorrendet
jelenti. A naiv út először exponenciálist számol: float32-ben
,
ami meghaladja a legnagyobb normális float32 értéket
(),
ezért inf lesz. A számláló és a nevező is inf,
a hányados NaN; a logaritmus sem menti meg. Az underflow
ellentétes irányú:
float32-ben 0, így a
-inf. Mindkettő az exponenciális függvény dinamikus
tartományából fakad, nem a loss matematikai hibájából.
A stabilitás alapja egy azonosság, amelyet most tételként mondunk ki és bizonyítunk. Jelölje
a log-sum-exp függvényt.
Tétel (logsumexp eltolási azonosság). Legyen és . Ekkor
Bizonyítás. A jobb oldalból indulunk ki. A kitevő azonossága miatt , tehát
A logaritmus szorzat-azonossága szerint a log a szorzatból összeg lesz:
Mivel a természetes logaritmus és az exponenciális egymás inverze, . Így
A feltételek pontosan a következők: véges valós számok, véges valós, a logaritmus argumentuma pozitív — ez teljesül, mert minden -re. A bizonyítás befejezett.
Stabil választás. Válasszuk értéket, és jelölje ezt a maximumot. Ekkor , ezért , és az összeg a intervallumba esik. A véges, nem overflowol. A gyakorlatban tehát
a log-softmax pedig
A softmax maga, ha a predikciót is ki akarjuk írni,
ugyancsak a max-eltolással stabil.
A PyTorch nn.CrossEntropyLoss dokumentációja [D07]
szerint a számítás log-softmax és negatív log-likelihood kombinációja; a
függvény logitsot vár, tehát a hívó nem alkalmaz előtte
softmaxot, és osztályindex-céloknál a cél int64 tömbje
alakú; alternatívaként
alakú, osztályonkénti valószínűségi cél is megadható megfelelő
lebegőpontos típussal. Az egyszerű főútban az int64,
alakot használjuk. A dokumentáció által garantált reduction
érték mean, sum és none; az
itteni példák mean-t használnak.
A laborban a könyvtári kimenetet float64-es, külön megírt számításhoz hasonlítjuk: ez a dokumentált bemeneti és redukciós viselkedés numerikus ellenőrzése, nem a könyvtár belső implementációjának bizonyítása. A matematikai azonosság, amelyet fent bizonyítottunk, a stabil implementáció alapja.
log1p és a
BCEWithLogits stabil alakjaA 7.3. Bináris, multilabel és regressziós cél alfejezetben bevezetett BCEWithLogits veszteség naiv alakja
Ha
nagyon negatív,
float32-ben nullává válik, és a
-inf-et ad; ha
nagy pozitív, a
tag nullázódik. A stabil formula az
-tól
függő algebrai átalakítást használ:
ahol
A számítására a
alak praktikus, mert
miatt a logaritmus argumentuma
-ben
van. Az
azonban kis pozitív
-re
majdnem nulla, és a
hozzáadása után a log elveszítheti a kis tagot. Erre való a
log1p függvény:
amely kis
esetén lényegesen pontosabb, mint a naiv np.log(1+x), mert
a könyvtár közvetlenül a
tag alapján, nem az
kerekített értékéből számol. Például
esetén log1p(x) körülbelül
,
míg 1+x float64-ben kerekítve
,
a log pedig
.
A BCEWithLogits stabil implementációjának levezetése a 7.3. Bináris, multilabel és regressziós cél
feladatanyagában szerepelt; itt a log1p a megvalósítási
eszköz.
A clipping vágás azt jelenti, hogy egy értéket korlátos
intervallumba szorítunk. Ha a felhasználó a predikált valószínűséget
-ként
naplózza, és abból később
-t
vagy entrópiát számol, akkor a
érték -inf-et adna. Egy lehetséges gyakorlat
ahol kicsi, tipikusan körüli. Ez a jelentéskészítés vágása: biztosítja, hogy a lementett predikció logaritmusa véges legyen.
Fontos határ: a tanítási loss logitjait nem szabad így vágni. Ha a cross-entropy bemenetét clipeljük, megváltozik a gradiens jelentése: a háló többé nem a valódi logits alapján tanul, és a kalibráció is torzul. A vágás tehát output-utófeldolgozás, nem a numerikus stabilitás tanítási trükkje. Ez heurisztika, mert az optimális feladatonként eltérhet; ellenőrzési tervet igényel.
A float16 számformátum tartománya jóval szűkebb: a legnagyobb normális érték , a legkisebb pozitív normális , a subnormalis tartomány még kisebb. Ezért float16-ban már esetén , ami túlcsordul. A logsumexp max-eltolása itt még kritikusabb, mint float32-ben.
Az automatikus kevert pontosság (AMP) egy gyakori lépése a loss scaling: mivel a hátraterjesztés során a veszteség gradiense gyakran olyan kicsi, hogy float16-ban nullává válna, a veszteséget egy nagy skalárral megszorozzák a skálázott hátraterjesztéshez, majd a frissítés előtt visszaosztanak. Ez a jelen egységben csak előkép; a teljes GPU/AMP-labort a 24.3. Mixed precision és nagyobb batch tárgyalja. A cél itt az, hogy a hallgató tudja: a stabil matematikai formula a float16 környezetben is ugyanaz, de a tartományhatárok miatt az ellenőrzés szigorúbb.
Legyen , a helyes osztály indexe (azaz a második komponens, mert a Python-indexelés 1-től számított második eleme az 501). A maximum . A max-eltolás után
Az exponenciális összeg:
A log-sum-exp:
A log-softmax:
A második komponens negáltja, a veszteség . Ellenőrzésképp a softmax valószínűségek:
amelyek összege
kerekítési hibán belül. A naiv float32 softmax ezzel szemben
inf/inf = NaN-t adna.
A labor célja a három implementáció összevetése egy f64 referencia ellen. A következő kimenet számított szemléltetés, nem mérési jegyzőkönyv; a kódot a laborban kell végrehajtani.
Munkakönyvtár: lab/. Futtatási parancs:
python n028_stability.pyA szkript:
import numpy as np
import torch
import torch.nn as nn
# Seed, dtype, device
rng = np.random.default_rng(20261028)
device = "cpu"
# Input: N=4, K=3 logits, float32; target int64
logits32 = np.array([
[ 500.0, 501.0, -501.0],
[ 2.0, 1.0, 0.0],
[-500.0, 500.0, 500.0],
[ 10.0, -10.0, 20.0],
], dtype=np.float32)
target_np = np.array([1, 0, 2, 2], dtype=np.int64)
# f64 kontroll: a float64 tartományban 500 még véges
z64 = logits32.astype(np.float64)
lse64 = np.log(np.exp(z64).sum(axis=1, keepdims=True))
logp64 = z64 - lse64
ref = -logp64[np.arange(logits32.shape[0]), target_np].mean()
print("f64 control:", ref)
def naive_nll(logits, target):
# Szándékosan naiv: először softmax, majd log
expz = np.exp(logits) # float32: overflow
probs = expz / expz.sum(axis=1, keepdims=True)
logp = np.log(probs) # 0-ból -inf
return -logp[np.arange(logits.shape[0]), target].mean()
def stable_nll(logits, target):
# Logsumexp max-eltolással, közvetlen log-softmax
m = logits.max(axis=1, keepdims=True)
shifted = logits - m
s = np.exp(shifted).sum(axis=1, keepdims=True)
lse = m + np.log(s)
logp = logits - lse
return -logp[np.arange(logits.shape[0]), target].mean()
loss_fn = nn.CrossEntropyLoss()
t_logits = torch.from_numpy(logits32).to(device)
t_target = torch.from_numpy(target_np).to(device)
with torch.no_grad():
torch_mean = loss_fn(t_logits, t_target).item()
naive_val = naive_nll(logits32, target_np)
stable_val = stable_nll(logits32, target_np)
print("naive float32:", naive_val)
print("stable float32:", stable_val)
print("torch float32:", torch_mean)
print("abs stable-ref:", abs(stable_val - ref))
print("abs torch-ref :", abs(torch_mean - ref))A naiv ág RuntimeWarning: overflow üzenetet ír ki,
eredménye nan. A stabil és a torch eredménye várhatóan:
| Implementáció | dtype | Érték (jelölt) |
|---|---|---|
| f64 kontroll | float64 | 0.31326169 |
| naiv NumPy | float32 | nan |
| stabil NumPy | float32 | 0.31326166 |
| PyTorch CrossEntropyLoss | float32 | 0.31326169 |
Ellenőrzés és értelmezés. A szkriptben
abs(stable_val - ref) < 1e-5 és
abs(torch_mean - ref) < 1e-5 az elvárt tolerancia; ezt a
hallgató assert-tel is ellenőrizheti. A naiv érték nem
véges, tehát a stabilizálás nem kozmetikai javítás, hanem az eredmény
létezését adja vissza. A torch és a stabil NumPy egyezése a float32
kerekítési hibahatáron belül van.
Tévedés: „Ha a veszteség NaN, elég
numerikus stabilizálást alkalmazni.” Ellenpélda: a hívó
fél a logitsra már softmaxot alkalmaz, vagy a célvektort float32-ként
adja át, és a veszteség még a stabil formulával is hibát jelez.
Tekintsük a következő hibás hívást:
probs = torch.softmax(logits, dim=1) # ez már valószínűség, nem logit
loss_fn(probs, target) # szerződéssértésA CrossEntropyLoss logitsot vár [D07]; a softmax dupla alkalmazása
nemcsak rossz értéket ad, hanem a gradiens is elromlik. Ugyanígy, ha a
target alakja
helyett float32
egyes könyvtári verziókban, a hibát a szerződés ellenőrzése oldja meg,
nem a stabilitási trükk. A stabil logsumexp tehát szükséges, de nem
elégséges: előbb a matematikai szerződést kell helyesen
teljesíteni.
F1 (fogalmi, nehézség 1). Adja meg az overflow és az underflow definícióját, és mutasson egy-egy konkrét float32 példát a softmax környékéről. Átadandó: saját megfogalmazású definíciók és két konkrét számérték.
F2 (levezetési, nehézség 2). Bizonyítsa be, hogy a softmax invariáns a konstans eltolásra: minden -re, ahol a -dimenziós csupa egyes vektor. Ezután vezesse le a stabil képletet a választással. Átadandó: teljes levezetés és a végképlet.
F3 (implementációs, nehézség 2). Írjon
NumPy-függvényt, amely a BCEWithLogits veszteséget a stabil
softplus-képlettel számolja, és hasonlítsa össze egy f64
referenciaimplementációval. Bemenet:
z = np.array([[-1000.0, 0.0, 1000.0]], dtype=np.float32),
y = np.array([[0.0, 0.5, 1.0]], dtype=np.float32).
Átadandó: futtatható kód, a várt hiba
abszolút toleranciával.
F4 (hibakeresési, nehézség 2). Az alábbi kód hibás:
p = torch.softmax(logits, dim=1)
loss = nn.CrossEntropyLoss()(p, target_float)Azonosítsa mindkét hibát, javítsa a kódot, és ellenőrizze a javítást a , kézi eseten. Átadandó: a hibák megnevezése, javított kód és a kézi ellenőrzés értéke.
F5 (kísérleti/transzfer, nehézség 3). Tervezzen
kísérletet annak vizsgálatára, hogy a logits hőmérséklet-skálázása
hogyan hat a cross-entropy értékére és a predikciók szétterülésére egy
rögzített CNN-modellen. Használja a GlyphLab val és
test splitjét a scene_id megfigyelési
egységgel, egy előre rögzített modell-állapottal. Adja meg a protokollt,
a változó tényezőt, a baseline-t, a metrikát és a következtetési határt.
Átadandó: egy lehetséges terv, nem feltétlenül futtatott eredmény.
F1 megoldás. Overflow: olyan művelet, amelynek
eredménye nagyobb a reprezentálható legnagyobb értéknél; float32
np.exp(500.0) inf-et ad. Underflow: az
eredmény abszolút értéke kisebb a legkisebb reprezentálható normális
értéknél, ezért nullává kerekítik; float32 np.exp(-500.0)
0.0 lesz. A softmax naiv nevezője overflowol, a számláló
underflowolhat; az eredmény inf/inf=NaN vagy
0/0=NaN.
F2 megoldás. A bizonyítás:
A második lépésben az exponenciális szorzat-azonosságát, a harmadikban a közös kiemelését használtuk; , ezért a törlés megengedett. Ebből a invarianciája is következik:
A stabil választás: , ekkor minden komponense nempozitív. Legyen . A fenti invariancia miatt
így
A korlát miatt a logaritmus argumentuma véges és pozitív;
F3 megoldás.
import numpy as np
z = np.array([[-1000.0, 0.0, 1000.0]], dtype=np.float32)
y = np.array([[0.0, 0.5, 1.0]], dtype=np.float32)
def stable_bce_with_logits(z, y):
# stabil softplus: max(t,0)+log1p(exp(-abs(t)))
t = -z
softplus = np.maximum(t, 0.0) + np.log1p(np.exp(-np.abs(t)))
return (1.0 - y) * z + softplus
def ref_bce_with_logits(z, y):
# f64 naiv matematikai definíció: z itt 1000-ig biztonságos
z64 = z.astype(np.float64)
y64 = y.astype(np.float64)
sig = 1.0 / (1.0 + np.exp(-z64))
return -y64 * np.log(sig) - (1.0 - y64) * np.log(1.0 - sig)
print(stable_bce_with_logits(z, y))
print(ref_bce_with_logits(z, y))
print(np.abs(stable_bce_with_logits(z, y) - ref_bce_with_logits(z, y)).max())A várt eltérés
.
A naiv f32 ágon a
-hez
tartozó
,
,
-inf; a stabil formula ezt elkerüli.
F4 megoldás. A két hiba: (1) a
CrossEntropyLoss logitsot vár, nem softmax-kimenetet; (2) a
cél target_float float típusú, holott a függvény int64
osztályindexet vár. Javítás:
loss = nn.CrossEntropyLoss()(logits, target.long())Ellenőrzés a
,
esetre: log-softmax
,
a negált log-valószínűség
.
A torch mean reduction egy minta esetén ugyanezt adja.
F5 megoldásminta. Protokoll: rögzítsünk egy, a
GlyphLab train-en tanított modell state_dict-jét; a
val spliten
képekre és int64 célokra gyűjtsük a logitsokat. Változó
tényező:
;
a modell maga változatlan. Metrika: átlagos cross-entropy a skálázott
logitsokból és a predikált valószínűség maximumának átlaga, kontrollként
a val accuracy rögzítve. Baseline:
az eredeti logits. Kiértékelés: minden
-re
a val spliten számolt metrika; a kísérleti következtetés
csak a hőmérséklet hatását állapíthatja meg a rögzített modellen, nem
általános optimalitást. A test splitet legfeljebb egyszer,
a protokoll végén szabad használni. Megfigyelési egység a
scene_id, mert a két view függő.
Ellenőrzési lista az alfejezethez: a logsumexp-tételben lépésenként indokoltuk a kitevő- és logaritmus-azonosságokat; a max-eltolás után a összeg -ban van; a kézi példában a softmax összege kerekítési hibán belül; a labor toleranciája explicit.
Visszahívás 7.3. Bináris, multilabel és regressziós cél alfejezetből: a BCEWithLogits gradiense , így a stabil formula nem változtatja meg a tanítási jelet. Visszahívás 6.4. Momentum és adaptív optimizálók alfejezetből: az Adam-epsilon a nevező stabilitását szolgálja, hasonló céllal, mint itt a max-eltolás; de a két számítási hely különböző. Visszahívás 3.4. Sajátérték, SVD és spektrális határ alfejezetből: ha egy lineáris réteg súlyának spektrális normája nagy, a logitok szórása megnőhet, és ezzel a softmax stabilitási tartaléka csökken; a spektrális norma hasznos diagnosztikai eszköz, de nem helyettesíti a stabil formulát.
A naiv softmax és BCE implementáció nagy vagy kis logitokra
inf, -inf vagy NaN értéket adhat,
miközben a matematikai eredmény véges. A logsumexp konstanseltolási
azonossága szerint
,
és a
választás biztosítja, hogy az exponenciális összeg
-ban
maradjon. A log-softmax ebből közvetlenül számolható, a BCEWithLogits
pedig a
softplus-képlettel és log1p-pel stabilizálható. A labor f64
kontrollon mutatja ki a naiv, a stabil és a könyvtári implementáció
viszonyát.
A 8.1. Kockázat, mintavétel és baseline egységben a kockázat, a mintavétel és a baseline fogalmát építjük fel; ott azt vizsgáljuk, hogy egy stabilan kiszámolt veszteség milyen tartalmi állításokat tesz lehetővé a modell várható teljesítményéről.