Egy betanított CNN a GlyphLab adaton 97% pontosságot ér el a tiszta tesztképeken. Az üzemeltető azonban nemcsak arra kíváncsi, hogy a modell mit válaszol, hanem arra is, hogy a döntés mely képrészleteken alapul. A 33.4. Üzemeltetés, drift és késő címke egységben láttuk, hogy a konfidencia önmagában nem magyarázat; itt a modell belső aktivációinak és gradienseinek lokális elemzését építjük fel. Az egység célja, hogy pontosan definiálja, mit jelent egy osztályozási döntés térbeli magyarázata, és hogyan állítható elő ilyen hőtérkép a betanított modell megváltoztatása nélkül.
Előismeret: az autograd és a VJP iránya a 11.1. Tensor és autograd-életciklus alfejezetből; a feature map mint köztes reprezentáció a 13.3. Lokális szűrők és tanult reprezentáció alfejezetből; a modellkártya és a szeletek szerinti értékelés a 23.4. Hibaszeletek és alkalmazási határ alfejezetből; a gradiens mint érzékenység a 4.2. Parciális derivált és gradiens alfejezetből. A valószínűségi alapon tárgyalt konfidencia itt nem elegendő: a softmax egyetlen skalárt ad, a magyarázathoz térbeli felbontás kell.
Induló kérdés. Adott egy 24×24-es tesztkép, amelyre
a modell a 1 (horizontal) osztályt jósolja. A kép tartalmaz
egy vízszintes vonalat és egy világos hátteret. Honnan tudjuk, hogy a
modell a vonalra figyelt, és nem a kép bal felső sarkában lévő zajos
foltra?
Rögzítsünk egy osztályozót, amely egy képet logitba képez le. Legyen az a célosztály, amelynek a döntését magyarázni akarjuk. A magyarázat mindig egy konkrét párra vonatkozik; más bemenetre vagy más célosztályra a hőtérkép általában eltérő.
1. definíció (input gradiens, más néven saliency). A célosztály logitjának az input szerinti gradiense
Az megmondja, hogy az egyes inputpixelek első rendben mennyire érzékenyek a cél logitra. Ez a logit linearizációjának együtthatója: kis perturbációra . Az input gradiens pixel felbontású, de zajos: a magasabb rétegekben a gradiensjel sok nemlineáris és pooling-műveleten haladt át, ezért gyakran „szétszórt”, és nem feltétlenül esik egybe az ember által értelmesnek tartott objektummal.
2. definíció (feature map). Legyen egy köztes réteg aktivációtensora, csatornával és térbeli felbontással. A -adik csatornát jelöli, a eleme pedig . A cél az, hogy a logit magyarázatát ne pixel-, hanem feature map szinten adjuk meg, majd a kapott durvább hőtérképet interpoláljuk vissza az input felbontására.
A két fogalom nem felcserélhető: az input gradiens az inputnak a logitra gyakorolt lokális érzékenysége; a Grad-CAM a köztes csatornák térben aggregált fontosságát kombinálja a csatornák aktivációival, ezért simább és szemantikusabb térképet adhat.
A Grad-CAM a [P23] cikk által bevezetett módszer; az alábbi levezetés a tankönyv saját kidolgozása, a cikkben szereplő aggregációs konvenciót követve. A levezetés célja: adjunk a célosztály logitjának egy olyan közelítő felbontását a csatorna felett, amely minden csatornához egyetlen skalár súlyt rendel, és ez a súly kifejezhető a visszafelé terjedő gradiensből.
3. definíció (a választott aggregációs konvenció). A csatornán belüli térbeli aggregáció a globális átlag pooling (GAP):
Ez egy lineáris, átlagoló redukció, amely a térbeli dimenziókat egyetlen skalárrá húzza össze. Azért ezt választjuk, mert a logit szempontjából nem azt akarjuk mérni, hogy egyetlen feature-pixel mennyire fontos, hanem hogy a -adik csatorna egésze milyen mértékben járul hozzá a célosztályhoz.
Motiváció a gradiensből. Tekintsük a logitot az aktivációk függvényének. A lokális elsőrendű közelítés szerint
Egy rögzített csatorna minden elemét azonos értékkel eltolva és a többi csatornát változatlanul hagyva a logit iránymenti deriváltja a csatorna gradienseinek összege:
ahol a -adik csatornában minden térbeli pozíción , másutt . A Grad-CAM a csatorna gradienseinek átlagát választja súlynak:
Ez az előbbi iránymenti derivált -szerese. Fontos, hogy az értékek és a csatornaátlag nem független változók: nem állíthatjuk korlátozás nélkül, hogy . Az azonosság csak az explicit, azonos eltolást alkalmazó útra értelmezhető. A csatornánkénti gradiensek átlagolása a Grad-CAM választott aggregációs szabálya; általános hálózatra nem vezethető le egy, a GAP szerinti logitdekompozíció egzakt azonosságaként.
A hőtérkép a csatornák súlyozott kombinációja, amelyből a negatív hozzájárulásokat a ReLU eltávolítja:
A ReLU indoklása heurisztikus: a cél az, hogy a célosztályt támogató régiókat mutassuk; azok a csatornák, amelyek összességében negatívan járulnak hozzá, más osztályok ellen szólhatnak, és a vizualizációban félrevezetőek lennének. Ez egy heurisztika, nem bizonyított tény a modell döntési mechanizmusáról.
Normalizáció. A nyers értéktartománya rétegenként és bemenetenként eltérő. Az összehasonlíthatósághoz min–max normalizálást végzünk:
Ha a nevező nulla, az egész térkép nulla; ezt a szélső esetet explicit módon kezelni kell. Végül az térképet bilineáris interpolációval felskálázzuk méretre.
Legyen , . A réteg aktivációi:
Tegyük fel, hogy a célosztályra a visszafelé terjedő gradiensek:
A GAP-konvencióval
A súlyozott összeg csatornánként:
Összegük és a ReLU (itt minden elem pozitív, tehát hatástalan):
Min–max normalizálás: maximum , minimum , terjedelem , így
A bal alsó elem a legfontosabb: ott a -es csatorna erős aktivációja és a -s gradiensű, de nagy -elem együtt magas pontszámot ad. A példa jól mutatja, hogy a hőtérkép nem azonos a gradienssel: a -es aktivációjú jobb alsó elem gradienséhez tartozik, mégis a normalizált térkép második legfontosabb eleme, mert erős.
Ellenőrzés szélső esetre. Ha minden gradiens zérus lenne, akkor , a hőtérkép azonosan nulla, és a min–max normalizálást a nullás ág kezeli.
A labor a 24.4. VI. ellenőrző pont: megbízható
CNN-training alfejezetben tanított TinyCNN modellt és
az ott létrehozott lab/best.pt checkpointot használja. Ha
ez még nem készült el, előbb a 24.4. VI. ellenőrző
pont: megbízható CNN-training tanítását kell futtatni. A bemenet
kizárólag a 24×24-es grayscale kép; maszkot vagy dobozt a modell nem
lát, és a hőtérkép sem használhatja őket. A hőtérkép a
model.features[3] (második konvolúció) kimenetére
vonatkozik.
# lab/n137_gradcam.py
# Futtatás a projekt gyökérkönyvtárából; a checkpoint architektúráját
# az alábbi osztály a tanításnál használt architektúrával egyezik.
from pathlib import Path
import numpy as np
import torch
root = Path("lab")
device = torch.device("cpu")
images = np.load(root / "data" / "images.npy") # (360,1,24,24)
class TinyCNN(torch.nn.Module):
# Egyezik az n096 tanítóprogramjának architektúrájával.
def __init__(self, in_ch=1, num_classes=3):
super().__init__()
self.features = torch.nn.Sequential(
torch.nn.Conv2d(in_ch, 8, 3, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(2),
torch.nn.Conv2d(8, 16, 3, padding=1),
torch.nn.ReLU(),
torch.nn.MaxPool2d(2),
torch.nn.Flatten(),
)
self.fc = torch.nn.Linear(16 * 6 * 6, num_classes)
def forward(self, x):
return self.fc(self.features(x))
model = TinyCNN().to(device)
state = torch.load(root / "best.pt", map_location=device, weights_only=True)
model.load_state_dict(state["model"])
model.eval()
target_layer = model.features[3] # második konvolúció kimenete, (N,16,12,12)
# Egyetlen forwardhoz tartozó aktivációt és logitokat használunk.
def heatmap_for_class(x, class_idx):
captured = {}
hook = target_layer.register_forward_hook(
lambda module, inputs, output: captured.update(A=output))
try:
logits = model(x)
A = captured["A"]
grad_A, = torch.autograd.grad(logits[0, class_idx], A)
weights = grad_A.mean(dim=(2, 3), keepdim=True)
raw = torch.relu((weights * A).sum(dim=1, keepdim=True))
span = raw.max() - raw.min()
heat = (raw - raw.min()) / span.clamp_min(1e-8)
# A magyarázó térképet a bemeneti kép 24×24-es rácsára helyezzük,
# így a képi maszkokkal is értelmesen összevethető.
heat = torch.nn.functional.interpolate(
heat, size=x.shape[-2:], mode="bilinear", align_corners=False)
return heat.detach(), logits.detach()
finally:
hook.remove()
# A metaadatban szereplő split alapján kell valódi tesztképet választani.
# Itt az index demonstrációs példa, nem állítjuk, hogy a testhez tartozik.
x = torch.from_numpy(images[10:11]).to(device=device, dtype=torch.float32)
with torch.enable_grad():
logits = model(x)
c = int(logits.argmax(dim=1).item())
heat, _ = heatmap_for_class(x, c)
other, _ = heatmap_for_class(x, (c + 1) % 3)
assert heat.shape == (1, 1, 24, 24)
assert torch.isfinite(heat).all() and torch.isfinite(other).all()
diff = (heat - other).abs().max().item()
print("Prediktált osztály:", c, "térképek eltérése:", diff)
# Más osztályhoz más térkép várható, de a különbség szigorú
# pozitivitása degenerált modellnél nem matematikai garancia.
torch.save({"image_index": 10, "target_class": c,
"heatmap": heat.cpu()}, root / "gradcam_demo.pt")A kód a prediktált osztályt, a normalizált hőtérképet és egy másik
célosztállyal vett eltérést rögzít. Mindkét Grad-CAM-számításban a logit
és a köztes aktiváció ugyanabból a forwardból
származik; ettől lesz helyes az autograd.grad
gráfkapcsolata. A clamp_min(1e-8) kezeli a zérus
terjedelmet, de nagyon kis pozitív terjedelemnél módosíthatja a skálát,
ezért az ilyen térképek értelmezése külön óvatosságot igényel.
F1 (fogalmi, nehézség 1). Definiálja külön az input gradiens saliencyt és a Grad-CAM hőtérképet. Melyik objektum melyik tenzorból készül, és mi a szerepe a ReLU-nak a Grad-CAM-ben?
F2 (levezetési, nehézség 2). Legyen , , , , , és a célosztály gradiense , . Számítsa ki a két súlyt, a nyers térképet és annak min–max normalizáltját.
F3 (implementációs, nehézség 2). Írjon
NumPy-függvényt gradcam_numpy(A, dA) néven, ahol
A alakja
és dA a
gradiens, ugyanakkora alakban. A függvény adja vissza a normalizált
Grad-CAM-térképet. Ellenőrizze a fenti 2×2-es kézi példán, és adja meg
az elvárt toleranciát.
F4 (hibakeresési, nehézség 2). Egy fejlesztő a
laborkódban a model(x) hívást
with torch.no_grad(): blokkba teszi, hogy gyorsítsa a
hőtérkép-számítást. Miért nem kap gradienst a
torch.autograd.grad(score, feature) hívásnál? Pontosan
lokalizálja a hibát, és adja meg a javítás utáni ellenőrzést.
F5 (kísérlettervezési, nehézség 3). Tervezzen GlyphLab-kísérletet annak eldöntésére, hogy a Grad-CAM hőtérkép a corrupted tesztképeken az eredeti alakzat maszkjára vagy a fényességgradiensre fókuszál. Adja meg a megfigyelési egységet, a hőtérkép és a maszk átfedésének mérőszámát, a szükséges küszöbprotokollt és a következtetés korlátait. A tesztet a modell kiválasztása után, befagyasztott súllyal kell lefuttatni.
F1. Az input gradiens , vagyis a logitnak a bemeneti pixelek szerinti parciális deriváltja. Ez pixel felbontású, de a mélyebb rétegeken keresztül visszavezetett jel miatt gyakran zajos. A Grad-CAM egy feature mapból számol: a súlyok a gradiensek térbeli átlagai, és a hőtérkép a súlyozott csatornaösszeg ReLU-ja. A ReLU eltávolítja a negatív összegű régiókat; ez a célosztályt támogató régiók megjelenítését szolgálja, de heurisztika.
F2. A GAP-konvenció: , . A nyers térkép ; a ReLU nem változtat, mert minden elem pozitív. Min–max: minimum , maximum , így .
F3. Egy lehetséges megoldás:
import numpy as np
def gradcam_numpy(A, dA, eps=1e-12):
# A, dA: (C, H, W)
assert A.shape == dA.shape and A.ndim == 3
w = dA.mean(axis=(1, 2), keepdims=True) # (C,1,1)
heat = (w * A).sum(axis=0, keepdims=True) # (1,H,W)
heat = np.maximum(heat, 0.0)
lo, hi = heat.min(), heat.max()
if hi - lo < eps:
return np.zeros_like(heat)
return (heat - lo) / (hi - lo)Az elvárt eredmény a kézi példán
[[0, 0.5], [1, 0.64285714]], float32 toleranciával
abszolút eltérés.
F4. A torch.no_grad() kontextus
letiltja az autograd gráf építését, ezért a
feature["value"] tenzornak nincs grad_fn
lánca, és a score sem vezethető vissza a bemenetre. A
torch.autograd.grad(score, feature) ekkor
RuntimeError üzenetet ad, mert a kimenet és a bemenet
között nincs gráfkapcsolat. A javítás: a model(x) hívást a
no_grad() blokkon kívül kell futtatni; a betanított modell
eval() állapota önmagában nem tiltja a gradiensszámítást,
csak a dropout és a BatchNorm viselkedését változtatja meg. Ellenőrzés:
a javítás után a grads tenzor nem None, alakja
pedig
.
F5. Protokollvázlat: a rögzített, befagyasztott modellen mind a 60 corrupted tesztképre kiszámítjuk a Grad-CAM hőtérképet a predikált osztályra. A hőtérképet a metrikához méretre interpoláljuk. A kísérlet azonban a maszkkal való átfedést méri: binarizáljuk a hőtérképet egy küszöbbel, és kiszámítjuk az értéket. A -t a train képek hőtérkép-eloszlásából választjuk, nem a corrupted tesztből. Emellett kiszámítjuk ugyanezt a fényességgradiens maszkjára is. Ha az IoU az alakzatmaszkkal szignifikánsan nagyobb, mint a gradiensmaszkkal, akkor a hőtérkép a modell bevallott fókusza szerint az alakzatra koncentrál; az állítás azonban csak az adott korrupciós eloszlásra és a választott küszöbre érvényes, és nem bizonyít oksági mechanizmust. A scene_id a bootstrap egység, nem a kép.
A leggyakoribb tévedés azt hinni, hogy a vizuálisan tetszetős hőtérkép oksági bizonyíték. A Grad-CAM a logit elsőrendű érzékenységéből és a forward aktivációkból származtatott korrelációs jellegű térkép; nem mutatja meg, hogy az adott régió eltávolítása ténylegesen megváltoztatná-e a döntést.
Ellenpélda: legyen egy olyan modell, amely az input bal felső
sarkában lévő állandó világos foltra támaszkodik, mert a tanító adatban
az mindig együtt járt a 2 osztállyal. A Grad-CAM magas
értéket adhat erre a foltra, mert a logit érzékeny rá, és a hőtérkép
szép, kontúros képet mutat. A folt eltávolítása azonban lehet, hogy nem
változtatja meg a döntést, mert a modell más, a hőtérképen nem kiemelt
csatornákon keresztül is a 2 osztályra szavaz. A P25
sanity-check vizsgálatai azt is megmutatták, hogy egyes
saliency-módszerek a háló súlyainak részleges véletlenítése után is
hasonló képet adhatnak; ezért a „szép hőtérkép” nem azonos a „verifikált
modellmagyarázattal”.
Az input gradiens a célosztály logitjának pixelenkénti érzékenysége, a Grad-CAM ezzel szemben egy köztes feature map csatornáit súlyozza a visszafelé terjedő gradiensek térbeli átlagával. A levezetésben megmutattuk, hogy a súly a logitnak a -adik csatorna globális átlagára vett érzékenységével arányos; a hőtérkép a súlyozott csatornaösszeg ReLU-ja, amelyet min–max normalizálással és interpolációval teszünk összehasonlíthatóvá. A labor egy befagyasztott GlyphLab-modellre készít hőtérképet, és numerikusan ellenőrzi, hogy más célosztály más térképet ad. A hőtérkép azonban korrelációs jellegű vizualizáció, nem oksági bizonyíték; a 35.2. Magyarázat sanity check alfejezetben ezt sanity check protokollokkal vizsgáljuk tovább.