35.1 Saliency és Grad-CAM

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

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?

35.1.2 Definíciók: input gradiens, feature map és a magyarázat tárgya

Rögzítsünk egy f:ℝ1×H×W→ℝKf: \mathbb{R}^{1\times H\times W} \to \mathbb{R}^K osztályozót, amely egy xx képet KK logitba képez le. Legyen c∈{0,…,K−1}c \in \{0,\ldots,K-1\} az a célosztály, amelynek a döntését magyarázni akarjuk. A magyarázat mindig egy konkrét (x,c)(x, c) 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 cc célosztály logitjának az input szerinti gradiense

S(x,c)=∇xzc=(∂zc∂x0,i,j)i,j∈ℝ1×H×W. S(x,c) \;=\; \nabla_x\, z_c \;=\; \left(\frac{\partial z_c}{\partial x_{0,i,j}}\right)_{i,j} \;\in\; \mathbb{R}^{1\times H\times W}.

Az SS 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 Δx\Delta x perturbációra zc(x+Δx)≈zc(x)+⟨S,Δx⟩z_c(x+\Delta x) \approx z_c(x) + \langle S, \Delta x\rangle. 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 SS gyakran „szétszórt”, és nem feltétlenül esik egybe az ember által értelmesnek tartott objektummal.

2. definíció (feature map). Legyen Al∈ℝCl×Hl×WlA^l \in \mathbb{R}^{C_l\times H_l\times W_l} egy köztes ll réteg aktivációtensora, ClC_l csatornával és Hl×WlH_l\times W_l térbeli felbontással. A kk-adik csatornát Akl∈ℝHl×WlA_k^l \in \mathbb{R}^{H_l\times W_l} jelöli, a (k,i,j)(k, i, j) eleme pedig Ak,i,jlA_{k,i,j}^l. A cél az, hogy a zcz_c 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.

35.1.3 A Grad-CAM súlyozási képletének levezetése

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 cc célosztály logitjának egy olyan közelítő felbontását a ClC_l 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):

A‾kl=1HlWl∑i=1Hl∑j=1WlAk,i,jl. \bar A_k^l \;=\; \frac{1}{H_l W_l}\sum_{i=1}^{H_l}\sum_{j=1}^{W_l} A_{k,i,j}^l .

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 kk-adik csatorna egésze milyen mértékben járul hozzá a célosztályhoz.

Motiváció a gradiensből. Tekintsük a zcz_c logitot az AlA^l aktivációk függvényének. A lokális elsőrendű közelítés szerint

Δzc≈∑k,i,j∂zc∂Ak,i,jlΔAk,i,jl. \Delta z_c \approx \sum_{k,i,j} \frac{\partial z_c}{\partial A_{k,i,j}^l}\,\Delta A_{k,i,j}^l.

Egy rögzített csatorna minden elemét azonos δ\delta értékkel eltolva és a többi csatornát változatlanul hagyva a logit iránymenti deriváltja a csatorna gradienseinek összege:

ddδzc(Al+δEk)|δ=0=∑i,j∂zc∂Ak,i,jl, \left.\frac{d}{d\delta}z_c(A^l+\delta E_k)\right|_{\delta=0} =\sum_{i,j}\frac{\partial z_c}{\partial A_{k,i,j}^l},

ahol EkE_k a kk-adik csatornában minden térbeli pozíción 11, másutt 00. A Grad-CAM a csatorna gradienseinek átlagát választja súlynak:

wkc=1HlWl∑i=1Hl∑j=1Wl∂zc∂Ak,i,jl. w_k^c=\frac1{H_lW_l}\sum_{i=1}^{H_l}\sum_{j=1}^{W_l} \frac{\partial z_c}{\partial A_{k,i,j}^l}.

Ez az előbbi iránymenti derivált 1/(HlWl)1/(H_lW_l)-szerese. Fontos, hogy az Ak,i,jlA_{k,i,j}^l értékek és a csatornaátlag nem független változók: nem állíthatjuk korlátozás nélkül, hogy ∂Ak,i,jl/∂A‾kl=1\partial A_{k,i,j}^l/\partial\bar A_k^l=1. 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:

LGrad−CAMc(x)=ReLU(∑k=1ClwkcAkl)∈ℝHl×Wl. L_{Grad-CAM}^c(x) \;=\; \mathrm{ReLU}\!\left(\sum_{k=1}^{C_l} w_k^c\, A_k^l\right) \;\in\;\mathbb{R}^{H_l\times W_l}.

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 LGrad−CAMcL_{Grad-CAM}^c értéktartománya rétegenként és bemenetenként eltérő. Az összehasonlíthatósághoz min–max normalizálást végzünk:

L̃i,jc=Li,jc−mini′,j′Li′,j′cmaxi′,j′Li′,j′c−mini′,j′Li′,j′c. \tilde L_{i,j}^c \;=\; \frac{L_{i,j}^c - \min_{i',j'} L_{i',j'}^c} {\max_{i',j'} L_{i',j'}^c - \min_{i',j'} L_{i',j'}^c}.

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 L̃c\tilde L^c térképet bilineáris interpolációval felskálázzuk H×WH\times W méretre.

35.1.4 Kézi számpélda

Legyen Cl=2C_l=2, Hl=Wl=2H_l=W_l=2. A ll réteg aktivációi:

A1l=[1234],A2l=[0120]. A_1^l=\begin{bmatrix}1&2\\3&4\end{bmatrix}, \qquad A_2^l=\begin{bmatrix}0&1\\2&0\end{bmatrix}.

Tegyük fel, hogy a cc célosztályra a visszafelé terjedő gradiensek:

∂zc∂A1l=[1110],∂zc∂A2l=[0202]. \frac{\partial z_c}{\partial A_1^l} =\begin{bmatrix}1&1\\1&0\end{bmatrix}, \qquad \frac{\partial z_c}{\partial A_2^l} =\begin{bmatrix}0&2\\0&2\end{bmatrix}.

A GAP-konvencióval

w1c=14(1+1+1+0)=34,w2c=14(0+2+0+2)=1. w_1^c=\frac{1}{4}(1+1+1+0)=\frac{3}{4}, \qquad w_2^c=\frac{1}{4}(0+2+0+2)=1.

A súlyozott összeg csatornánként:

w1cA1l=[0,751,52,253],w2cA2l=[0120]. w_1^c A_1^l = \begin{bmatrix} 0{,}75 & 1{,}5 \\ 2{,}25 & 3 \end{bmatrix}, \qquad w_2^c A_2^l = \begin{bmatrix} 0 & 1 \\ 2 & 0 \end{bmatrix}.

Összegük és a ReLU (itt minden elem pozitív, tehát hatástalan):

LGrad−CAMc=[0,752,54,253]. L_{Grad-CAM}^c = \begin{bmatrix} 0{,}75 & 2{,}5 \\ 4{,}25 & 3 \end{bmatrix}.

Min–max normalizálás: maximum 4,254{,}25, minimum 0,750{,}75, terjedelem 3,53{,}5, így

L̃c=13,5[01,753,52,25]=[00,510,6429]. \tilde L^c = \frac{1}{3{,}5} \begin{bmatrix} 0 & 1{,}75 \\ 3{,}5 & 2{,}25 \end{bmatrix} = \begin{bmatrix} 0 & 0{,}5 \\ 1 & 0{,}6429 \end{bmatrix}.

A bal alsó elem a legfontosabb: ott a 22-es csatorna erős aktivációja és a 00-s gradiensű, de nagy A1A_1-elem együtt magas pontszámot ad. A példa jól mutatja, hogy a hőtérkép nem azonos a gradienssel: a 44-es aktivációjú jobb alsó elem gradienséhez 00 tartozik, mégis a normalizált térkép második legfontosabb eleme, mert A1A_1 erős.

Ellenőrzés szélső esetre. Ha minden gradiens zérus lenne, akkor w1c=w2c=0w_1^c=w_2^c=0, a hőtérkép azonosan nulla, és a min–max normalizálást a nullás ág kezeli.

35.1.5 Labor: hőtérkép a saját modellen és a célosztály szerepe

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.

35.1.6 Feladatok

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 Hl=1H_l=1, Wl=2W_l=2, Cl=2C_l=2, A1l=(2,4)A_1^l=(2,4), A2l=(1,0)A_2^l=(1,0), és a célosztály gradiense ∂zc∂A1l=(3,1)\frac{\partial z_c}{\partial A_1^l}=(3,1), ∂zc∂A2l=(−2,2)\frac{\partial z_c}{\partial A_2^l}=(-2,2). Számítsa ki a két wkcw_k^c súlyt, a LGrad−CAMcL_{Grad-CAM}^c 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 (Cl,Hl,Wl)(C_l,H_l,W_l) és dA a ∂zc/∂A\partial z_c/\partial 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.

35.1.7 Megoldások és indoklás

F1. Az input gradiens S=∇xzc∈ℝ1×H×WS=\nabla_x z_c\in\mathbb{R}^{1\times H\times W}, 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 Cl×Hl×WlC_l\times H_l\times W_l feature mapból számol: a wkcw_k^c súlyok a ∂zc/∂Ak,i,jl\partial z_c/\partial A_{k,i,j}^l gradiensek térbeli átlagai, és a hőtérkép a wkcAklw_k^c A_k^l 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ó: w1c=12(3+1)=2w_1^c=\frac{1}{2}(3+1)=2, w2c=12(−2+2)=0w_2^c=\frac{1}{2}(-2+2)=0. A nyers térkép L=ReLU(2⋅(2,4)+0⋅(1,0))=(4,8)L=ReLU(2\cdot(2,4)+0\cdot(1,0))=(4,8); a ReLU nem változtat, mert minden elem pozitív. Min–max: minimum 44, maximum 88, így L̃=(0,1)\tilde L=(0,1).

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 10−610^{-6} 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 (1,Cl,Hl,Wl)(1, C_l, H_l, W_l).

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 H×WH\times W méretre interpoláljuk. A kísérlet azonban a maszkkal való átfedést méri: binarizáljuk a hőtérképet egy τ\tau küszöbbel, és kiszámítjuk az IoU(küszöbölt hőtérkép,maszk)\mathrm{IoU}(\text{küszöbölt hőtérkép}, \text{maszk}) értéket. A τ\tau-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.

35.1.8 Tipikus tévedés és ellenpélda

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”.

35.1.9 Rövid összefoglaló

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 wkcw_k^c súly a logitnak a kk-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.

35.1.10 Felhasznált források