এই পর্বে যা শিখবেন
- প্রথম স্তরের filter আর feature map পড়া, ছোট CNN আর pretrained ResNet-18-এ
- Gradient saliency আর occlusion sensitivity নিজে NumPy-তে লেখা, আর একটি shortcut ধরা
- Grad-CAM-এর সূত্র, হাতে হিসাব, আর PyTorch hook দিয়ে বাস্তব model-এ প্রয়োগ
- Saliency-র sanity check: weight বা label এলোমেলো করলে ব্যাখ্যা বদলায় কি না
- FGSM হাতে এক ধাপ, accuracy বনাম ε, PGD আর adversarial training, সবকিছু শুধু শেখা আর সুরক্ষার জন্য
পর্ব ২০ থেকে ২৫-এ convolution, স্থাপত্য, transfer learning, augmentation আর detection দেখেছেন। প্রতিটিতে প্রশ্ন ছিল “কত ভালো অনুমান করে”। এই পর্বের প্রশ্ন ভিন্ন: কীসের ভিত্তিতে করে, আর কতটা সহজে ঠকানো যায়। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে, আর চারটি ব্লক এক namespace ভাগ করে। PyTorch-এর সব সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে।
১ · কেন জানতে চাই CNN কী দেখে — বাজারঘরের লোগোর গল্প
পরীক্ষার খাতা দেখার সময় একজন পরীক্ষক লক্ষ করলেন, একটি ছাত্রের সব উত্তর ঠিক, কিন্তু খাতায় কোনো হিসাব নেই। নম্বর দেওয়ার আগে তিনি জানতে চান, উত্তরগুলো বুঝে লেখা, নাকি পাশের জনেরটা দেখে। Model-এর বেলাতেও তাই: test accuracy ভালো হলেও জানা দরকার, model আসল লক্ষণ দেখছে, নাকি এমন কোনো সূত্র ধরেছে যা শুধু training ডেটায় কাকতালীয়ভাবে উত্তরের সঙ্গে মিলে যায়।
ধরুন বাজারঘর পণ্যের ছোট আইকন দেখে তিন ধরনের পণ্য আলাদা করে: আনুভূমিক দাগ (h-bar), খাড়া দাগ (v-bar) আর চৌকো বাক্স (box). পুরোনো এক সরবরাহকারীর প্রায় সব v-bar ছবির ডান-ওপর কোণে ছোট একটি লোগো থাকত। Model দ্রুত শিখে নিল, আর তার কারণ খুঁজতে গিয়ে দেখা গেল সে আসলে লোগো খোঁজে। এই ঘটনার নাম shortcut learning; ছবিগুলো কাল্পনিক, পাতার কোডে seed দিয়ে বানানো, কিন্তু একই ধরনের ঘটনা প্রকাশিত গবেষণায় বহুবার দেখা গেছে (প্র ০৩).
মেশিন লার্নিং কোর্সে interpretability-র দুই রূপ দেখেছেন: পুরো model-এর আচরণ (পর্ব ৫৬, permutation importance আর PDP) আর একটি অনুমানের ব্যাখ্যা (পর্ব ৫৭, SHAP আর LIME). ছবিতে feature মানে লাখো pixel, যাদের একটি একা অর্থহীন। তাই ছবির জন্য আলাদা কৌশল লাগে, যা মূলত post-hoc: শেখানো model-কে বাইরে থেকে প্রশ্ন করা।
| কৌশল | যে প্রশ্নের উত্তর দেয় | খরচ | মূল দুর্বলতা |
|---|---|---|---|
| Filter আর feature map দেখা | স্তরগুলো কী ধরনের নকশায় সাড়া দেয় | প্রায় শূন্য | গভীর স্তরে মানুষের চোখে অর্থহীন |
| Gradient saliency | কোন pixel একটু বদলালে score সবচেয়ে বদলায় | এক backward | গোলমেলে, saturation-এ ভুল |
| Occlusion sensitivity | কোন অংশ ঢাকলে আস্থা কমে | প্রতি অবস্থানে এক forward | ধীর; “ঢাকা”র রং নিজেই নতুন সংকেত |
| Grad-CAM | শেষ conv স্তরের কোন জায়গা class-এর পক্ষে | এক forward + এক backward | মোটা রেজোলিউশন (যেমন ৭×৭) |
| Adversarial পরীক্ষা | সামান্য বদলে অনুমান কতটা টলে | এক বা কয়েক backward | দুর্বল আক্রমণে মিথ্যা নিরাপত্তাবোধ |
২ · প্রথম স্তরের filter আর feature map
প্রথম conv স্তরের weight সরাসরি pixel-এর সঙ্গে গুণ হয়, তাই এর kernel নিজেই একটি ছোট ছবি (পর্ব ২০, Convolution). প্রতিটি kernel ছবির ওপর সরে যে output বানায়, তার নাম feature map বা activation map: যেখানে নকশাটি মেলে, সেখানে বড় মান। শুরুতে দুটি model দেখি: আমাদের ছোট CNN, যা Fashion-MNIST-এ (Zalando-র প্রকাশিত ২৮×২৮ পোশাকের ধূসর ছবি, MIT লাইসেন্স) বাজারঘরের পোশাক-ক্যাটালগের প্রতিনিধি হিসেবে শেখানো, আর torchvision-এর ImageNet-এ শেখানো ResNet-18.
ছোট CNN-এর গঠন: conv 1→8 (5×5) → ReLU → max-pool → conv 8→16 (3×3) → ReLU → max-pool → conv 16→32 (3×3) → ReLU → Linear 1568→64 → ReLU → Linear 64→10. Adam, lr 10−3, batch 128, ৮ epoch. সত্যিকারের run (ডেস্কটপ, CPU-তে; লেখার সময়ের সংস্করণ):
~/bazarghar-dl$ uv run python train.py
Python 3.14.4 | torch 2.15.0+cpu | torchvision 0.29.2+cpu
parameters: 107082
epoch 1: train loss 0.6766 test acc 0.8287 (11 s)
epoch 2: train loss 0.4166 test acc 0.8233 (22 s)
…
epoch 7: train loss 0.2721 test acc 0.8916 (83 s)
epoch 8: train loss 0.2574 test acc 0.8903 (95 s)
~/bazarghar-dl$ uv run python filters.py
conv1 weight shape: (8, 1, 5, 5)
.+--- ..... +++.+ ++-.- ++-.- ....- -...+ ...+.
..+-. ..--- -.--. -++-. .--++ ..... -.--. .....
.+--- ...-- ++.++ .+..- -.-++ ..... ..--- ..---
.++-. ...-- ..+++ .++.- -..++ +++.- ...-. ..---
++.+. ..... -.+++ -++-- ++-+- ...-- +.+.. .....
filter mean |dx| |dy|
f0 +0.003 3.72 2.57
f1 -0.166 5.44 6.18
…
f7 -0.126 4.84 6.04
প্রতিটি ৫×৫ filter চিহ্নে আঁকা: + মানে বড় ধনাত্মক weight, − বড় ঋণাত্মক, . প্রায় শূন্য। |dx| হলো পাশাপাশি ঘরের পার্থক্যের যোগ (খাড়া কিনারায় বড়), |dy| ওপর-নিচের। f0-এ বাঁয়ে +, ডানে −, অর্থাৎ মোটামুটি একটি খাড়া কিনারা-খোঁজক; f1-এর গড় −০.১৬৬, উজ্জ্বল জায়গায় নেতিবাচক সাড়া দেয়। তবে সৎভাবে বললে, ৮ epoch-এর এই ছোট model-এর filter বেশ গোলমেলে। বড় ডেটায় শেখা model-এ ছবি পরিষ্কার:
~/bazarghar-dl$ uv run python resnet_filters.py
weights: ResNet18_Weights.IMAGENET1K_V1 | conv1: (64, 3, 7, 7)
filters with colour share > 0.6: 22 of 64
vertical-ish (|dx| > 1.5|dy|): 10 | horizontal-ish (|dy| > 1.5|dx|): 14
8 strongest filters: [24, 0, 39, 53, 55, 47, 8, 23]
f24 norm 2.67 colour 0.25 |dx| 7.59 |dy| 12.08
f0 norm 2.63 colour 0.28 |dx| 4.51 |dy| 12.08
…
f8 norm 2.44 colour 0.23 |dx| 11.63 |dy| 3.87
f23 norm 2.36 colour 1.00 |dx| 0.50 |dy| 0.50
“colour” হলো filter-এর তিন channel তাদের গড় থেকে কতটা আলাদা, তার অনুপাত: ০-এর কাছে মানে ধূসর কিনারা-খোঁজক, ১-এর কাছে মানে রঙের পার্থক্য-খোঁজক (যেমন লাল বনাম সবুজ)। ৬৪টির মধ্যে ২২টি রঙ-প্রধান; f0 আনুভূমিক আর f8 খাড়া কিনারা ধরে। ওজন torchvision-এর ResNet18_Weights.IMAGENET1K_V1; কোড BSD-3-Clause লাইসেন্সে, কিন্তু ImageNet ডেটার নিজস্ব ব্যবহারের শর্ত আছে, তাই বাণিজ্যিক কাজে model card আর শর্ত পড়ে নিন (AI-এর ভিত্তি কোর্সের পর্ব ৭১). নিচের ছবি run-এর আসল weight থেকে আঁকা।
প্রথম স্তরের filter — বড় model বনাম ছোট model
Top: ResNet-18 conv1, min–max scaled to RGB. Bottom: small CNN conv1, orange = positive, blue = negative.
গভীর স্তরে কী হয়? সেখানে weight নয়, feature map দেখতে হয়: কোন ছবিতে একটি channel সবচেয়ে বেশি জ্বলে, সেগুলো পাশাপাশি রাখলে বোঝা যায় channel-টি কী খোঁজে। Zeiler আর Fergus (২০১৪) এভাবে দেখান, প্রথম স্তরে কিনারা আর রং, মাঝের স্তরে texture আর অংশ (চাকা, চোখ), শেষের দিকে প্রায় পুরো বস্তু। Feature visualization (Olah ও সহকর্মী, Distill, ২০১৭) আরেক পথ নেয়: এলোমেলো ছবি থেকে শুরু করে gradient ascent দিয়ে এমন ছবি বানায় যা একটি channel-কে সর্বোচ্চ জ্বালায়। দুটিই সুন্দর ছবি দেয়, কিন্তু একটি channel প্রায়ই কয়েকটি ভিন্ন জিনিসে সাড়া দেয়, তাই “এই channel মানে কুকুরের কান” ধরনের দাবি সাবধানে করতে হয়।
৩ · Saliency map — input-এর gradient
চায়ের দোকানের হিসাব খাতায় দিনের মোট লাভ একটি সংখ্যা। কোন জিনিসের দাম এক টাকা বাড়লে লাভ সবচেয়ে বেশি বদলাবে, তা জানলে বোঝা যায় কোন জিনিস লাভের জন্য সবচেয়ে সংবেদনশীল। Saliency ঠিক এই প্রশ্নটি করে, শুধু জিনিসের জায়গায় pixel আর লাভের জায়গায় class-এর score.
২০১৩ সালে Simonyan, Vedaldi আর Zisserman একটি সরল প্রশ্ন করেন: class c-এর score yc কোন pixel-এর প্রতি সবচেয়ে সংবেদনশীল? উত্তর হলো input-এর সাপেক্ষে gradient, যা backpropagation এমনিতেই দেয় (পর্ব ০৪):
S(x) = | ∂yc / ∂x |
S-এর প্রতিটি ঘর বলে, ওই pixel সামান্য বদলালে score কত দ্রুত বদলায়। একটি linear softmax model z = xW + b-এ, যেখানে W-এর কলাম wk প্রতিটি class-এর ওজন, ∂zc/∂x = wc: সব ছবির জন্য একই template. তাই এখানে log-সম্ভাবনার gradient নিই, যা ছবি অনুযায়ী বদলায়:
∂ log pc / ∂x = wc − ∑k pk wk
অর্থাৎ class c-এর template থেকে সব class-এর template-এর সম্ভাবনা-ভারিত গড় বাদ: “অন্যদের চেয়ে c-কে আলাদা করতে কোন pixel কাজে লাগে”। নিচের ব্লক বাজারঘরের ৮×৮ কাল্পনিক আইকন বানায়, দুটি softmax regression শেখায় (একটি লোগোওয়ালা ডেটায়, একটি লোগো ছাড়া), তারপর লোগো ছাড়া একটি v-bar-এর saliency আঁকে।
import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(42)
def make(n, logo_rate): # ৮×৮ কাল্পনিক আইকন: 0 h-bar, 1 v-bar, 2 box
X, y = np.zeros((n, 8, 8)), rng.integers(0, 3, n)
for img, c in zip(X, y):
r, s = rng.integers(2, 5, 2)
if c == 0: img[r:r + 2, 1:7] = 1
if c == 1: img[1:7, s:s + 2] = 1
if c == 2: img[r - 1:r + 3, s - 1:s + 3] = 1; img[r:r + 2, s:s + 2] = 0
if c == 1 and rng.random() < logo_rate: img[:2, 6:] = 1 # কোণের "লোগো"
return (X + rng.normal(0, 0.4, X.shape)).reshape(n, 64), y
def softmax(Z):
E = np.exp(Z - Z.max(1, keepdims=True)); return E / E.sum(1, keepdims=True)
def fit(X, y, steps=400, lr=0.1): # softmax regression, gradient descent
W, b = np.zeros((64, 3)), np.zeros(3)
for _ in range(steps):
P = softmax(X @ W + b); P[np.arange(len(y)), y] -= 1 # ∂L/∂z = p − onehot
W -= lr * (X.T @ P / len(y) + 1e-3 * W); b -= lr * P.mean(0)
return W, b
Xlogo, ylogo = make(600, 0.95) # পুরোনো সরবরাহকারী: v-bar-এ প্রায় সবসময় লোগো
Xclean, yclean = make(600, 0.0)
Xte, yte = make(300, 0.0) # নতুন ছবি: লোগো নেই
acc = lambda m, X, y: (np.argmax(X @ m[0] + m[1], 1) == y).mean()
(W, b), m_clean = fit(Xlogo, ylogo), fit(Xclean, yclean)
print(f"logo-trained: train {acc((W, b), Xlogo, ylogo):.3f} | test {acc((W, b), Xte, yte):.3f}")
print(f"clean-trained: train {acc(m_clean, Xclean, yclean):.3f} | test {acc(m_clean, Xte, yte):.3f}")
x = Xte[np.flatnonzero(yte == 1)[0]] # লোগো ছাড়া একটি v-bar
p = softmax((x @ W + b)[None])[0]
sal = W[:, 1] - W @ p # ∂ log p₁/∂x = w₁ − Σₖ pₖ wₖ
print("p =", p.round(3), "| top-4 |saliency| pixels:", [divmod(int(i), 8) for i in np.argsort(-np.abs(sal))[:4]])
fig, ax = plt.subplots(1, 2, figsize=(6, 3))
ax[0].imshow(x.reshape(8, 8), cmap="gray"); ax[0].set_title("test v-bar (no logo)")
ax[1].imshow(np.abs(sal).reshape(8, 8), cmap="hot"); ax[1].set_title("|saliency|, logo model")
plt.show()আউটপুট (পাতার Python-এ চালানো; সঙ্গে দুটি ছবি):
logo-trained: train 0.917 | test 0.680
clean-trained: train 0.878 | test 0.827
p = [0.004 0.686 0.31 ] | top-4 |saliency| pixels: [(1, 7), (0, 7), (0, 6), (1, 6)]
লোগোওয়ালা ডেটায় শেখা model training-এ ভালো (০.৯১৭), কিন্তু লোগো ছাড়া নতুন ছবিতে ০.৬৮০; লোগো ছাড়া শেখা model নতুন ছবিতে ০.৮২৭. পার্থক্য পুরোটাই shortcut-এর দাম, আর এটি distribution shift-এর একটি রূপ (মেশিন লার্নিং কোর্সের পর্ব ৫৯). Saliency-র সবচেয়ে উজ্জ্বল চারটি pixel ঠিক কোণের ২×২ লোগোর জায়গা: (0, 6), (0, 7), (1, 6), (1, 7), যদিও এই ছবিতে লোগো নেই। Saliency তাই এই ছবি নয়, model-এর অভ্যাস দেখাচ্ছে: “ওখানে উজ্জ্বল কিছু থাকলে v-bar-এর দিকে ঝুঁকব”।
Saliency-র তিনটি পরিচিত সমস্যা। গোলমেলে: গভীর network-এ gradient pixel থেকে pixel-এ লাফায়; SmoothGrad (Smilkov ও সহকর্মী, ২০১৭) অনেক noisy কপির gradient গড় করে এটি কমায়। Saturation: score যখন ইতিমধ্যে সর্বোচ্চে, তখন গুরুত্বপূর্ণ pixel-এরও gradient প্রায় শূন্য; Integrated Gradients (Sundararajan ও সহকর্মী, ২০১৭) একটি কালো ছবি থেকে আসল ছবি পর্যন্ত পথ ধরে gradient যোগ করে এটি সামলায়। Gradient × input: অনেকে S-কে x দিয়ে গুণ করেন, যাতে ছবি দেখতে সুন্দর হয়; অংশ ৬-এ দেখবেন এর ফাঁদ।
৪ · Occlusion sensitivity — ঢেকে দেখা
Gradient হলো খুব ছোট বদলের প্রতিক্রিয়া। আরও সরাসরি প্রশ্ন: ছবির একটি অংশ পুরো সরিয়ে দিলে আস্থা কতটা কমে? Zeiler আর Fergus (২০১৪) এই কৌশল জনপ্রিয় করেন: একটি ধূসর বা কালো patch ছবির ওপর সরিয়ে প্রতিটি অবস্থানে pc আবার মাপা হয়। ডাক্তার যেমন শরীরের এক এক জায়গায় চাপ দিয়ে দেখেন কোথায় ব্যথা, এটিও তেমন।
def occlusion(x, cls, size=2): # ২×২ জায়গা ঢেকে p কতটা কমে
base = softmax((x @ W + b)[None])[0, cls]
img, heat = x.reshape(8, 8), np.zeros((8 - size + 1, 8 - size + 1))
for r in range(heat.shape[0]):
for c in range(heat.shape[1]):
z = img.copy(); z[r:r + size, c:c + size] = 0 # ঢাকা = পটভূমির মান
heat[r, c] = base - softmax((z.reshape(1, 64) @ W + b))[0, cls]
return base, heat
x_logo = Xlogo[np.flatnonzero((ylogo == 1) & (Xlogo[:, 7] > 0.5))[0]] # লোগোসহ v-bar
p0, heat = occlusion(x_logo, 1)
r, c = np.unravel_index(heat.argmax(), heat.shape)
print(f"p(v-bar) = {p0:.3f}; biggest drop {heat.max():.3f} when covering rows {r}-{r+1}, cols {c}-{c+1}")
away = heat.copy(); away[:2, 5:] = 0 # লোগোর কোণ ছোঁয় এমন patch বাদ
print(f"largest drop away from the logo corner: {away.max():.3f}")
fig, ax = plt.subplots(1, 2, figsize=(6, 3))
ax[0].imshow(x_logo.reshape(8, 8), cmap="gray"); ax[0].set_title("train v-bar with logo")
im = ax[1].imshow(heat, cmap="viridis"); ax[1].set_title("drop in p(v-bar), 2x2 patch")
fig.colorbar(im, ax=ax[1], shrink=0.8)
plt.show()আউটপুট (পাতার Python-এ চালানো; সঙ্গে ছবি আর heatmap):
p(v-bar) = 0.999; biggest drop 0.352 when covering rows 0-1, cols 6-7
largest drop away from the logo corner: 0.002
লোগোসহ একটি training v-bar-এ model-এর আস্থা ০.৯৯৯. লোগোর কোণ ঢাকলে আস্থা ০.৩৫২ কমে, অথচ খাড়া দাগের যেকোনো অংশ ঢাকলে সর্বোচ্চ ০.০০২. Occlusion তাই saliency-র সন্দেহ নিশ্চিত করল, ভিন্ন পদ্ধতিতে। দ্বিতীয় শিক্ষাও আছে: আস্থা ০.৯৯৯ হলে দাগের একটি অংশ সরালে বাকি প্রমাণেই p প্রায় ১ থাকে, তাই occlusion-এও saturation আছে। একসঙ্গে কয়েকটি অংশ ঢাকলে তবে বোঝা যায়।
খরচের হিসাব পরীক্ষায় আসে। H × W ছবিতে k × k patch, stride s হলে অবস্থান ((H − k)/s + 1) × ((W − k)/s + 1); প্রতিটিতে একটি forward pass. ২২৪×২২৪ ছবিতে ১৬×১৬ patch আর stride ৮ হলে ২৭ × ২৭ = ৭২৯টি forward, যেখানে saliency বা Grad-CAM-এ একটি backward যথেষ্ট। Batch করে চালালে GPU-তে এটি সহনীয়। আরেকটি সূক্ষ্মতা: “ঢাকা”র রং নিজেই একটি নতুন সংকেত। কালো patch-কে model একটি কালো বস্তু ভাবতে পারে; তাই গড় রং, ঝাপসা করা অংশ বা অন্য ছবির অংশ দিয়ে ঢেকে ফল মিলিয়ে দেখা ভালো।
৫ · Grad-CAM — gradient-এর গড় দিয়ে ওজন করা feature map
শেষ conv স্তরের feature map-এ স্থানের তথ্য তখনো থাকে (কোন জায়গায় কী), আর অর্থের তথ্যও অনেকটা জমে গেছে (কী ধরনের অংশ)। Zhou ও সহকর্মীরা (২০১৬) CAM-এ দেখান, শেষ conv স্তরের পরে global average pooling আর একটি linear স্তর থাকলে, class c-এর weight wck দিয়ে feature map-গুলোর যোগফলই একটি localisation map. সমস্যা: এর জন্য বিশেষ গঠন লাগে। Selvaraju ও সহকর্মীদের Grad-CAM (প্রথম রূপ ২০১৬, ICCV ২০১৭) weight-এর জায়গায় gradient বসায়, তাই যেকোনো CNN-এ চলে:
αk = (1/Z) ∑i,j ∂yc/∂Akij · L = ReLU( ∑k αk Ak )
এখানে Ak হলো শেষ conv স্তরের k-তম channel-এর feature map, Z তার ঘরের সংখ্যা (৭×৭ হলে ৪৯), yc class c-এর softmax-এর আগের score, আর αk বলে channel k class c-এর পক্ষে কতটা গুরুত্বপূর্ণ। ReLU রাখা হয় শুধু সেই জায়গাগুলো দেখাতে যা c-এর পক্ষে প্রমাণ দেয়। GAP + linear head-এ ∂yc/∂Akij = wck/Z সব ঘরে সমান, তাই সেখানে Grad-CAM আর CAM একই map দেয় (মাপ ছাড়া)।
| ধাপ | |
| channel ১ | |
| channel ২ | |
| map |
- Forward. Forward hook শেষ conv স্তরের দুটি feature map ধরে রাখে। A₁ ডান দিকে জ্বলে, A₂ বাঁ-ওপরে।
- Backward. yc থেকে backward চালালে backward hook প্রতিটি ঘরের ∂yc/∂A পায়। G₁-এর সব ঘর ধনাত্মক বা শূন্য: channel ১ বাড়লে score বাড়ে। G₂ মূলত ঋণাত্মক: channel ২ class c-এর বিপক্ষে প্রমাণ।
- গড়. G₁-এর যোগ ০.৯, নয়টি ঘরে গড় α₁ = ০.১. G₂-এর যোগ −০.৪৫, α₂ = −০.০৫. প্রতিটি channel এখন একটি সংখ্যায় এসেছে।
- ওজন করা যোগ. 0.1·A₁ − 0.05·A₂. যেমন প্রথম সারির প্রথম ঘর: 0.1 × 0 − 0.05 × 2 = −০.১; দ্বিতীয় সারির শেষ ঘর: 0.1 × 3 − 0 = ০.৩.
- ReLU. ঋণাত্মক দুটি ঘর (−০.১ আর −০.০৫) ০ হয়ে যায়; বাঁ-নিচের ঘর আগে থেকেই ০। বাঁ-ওপরের জায়গা, যেখানে channel ২ জ্বলেছিল, এখন অন্ধকার।
- Normalise. সর্বোচ্চ ০.৩ দিয়ে ভাগ: চূড়া দ্বিতীয় সারির শেষ ঘরে (১.০)। বাস্তবে এই ছোট grid ছবির মাপে বড় করে (upsample) ছবির ওপর রং হিসেবে বসানো হয়।
PyTorch-এ Grad-CAM — দুটি hook আর একটি গড়
Forward (blue) stores A; backward (orange) stores G; then mean, weighted sum, ReLU.
পর্ব ০৮-এ tensor hook আর পর্ব ১৭-এ module-এর backward hook দেখেছেন। Grad-CAM-এ দুটিই লাগে: register_forward_hook স্তরের output A ধরে, register_full_backward_hook সেই output-এর gradient G ধরে। স্তর হিসেবে নেওয়া হলো ছোট CNN-এর শেষ conv-এর পরের ReLU, তাই A-এর আকার ৩২×৭×৭.
model = load_model() # state_dict লোড, তারপর .eval()
store = {}
layer = model.features[-1] # শেষ conv স্তরের ReLU, output 32×7×7
layer.register_forward_hook(lambda m, i, o: store.update(A=o.detach()))
layer.register_full_backward_hook(lambda m, gi, go: store.update(G=go[0].detach()))
def grad_cam(x, cls=None):
logits = model(x) # forward hook A জমা রাখে
cls = logits.argmax(1).item() if cls is None else cls
model.zero_grad()
logits[0, cls].backward() # backward hook G = ∂y_c/∂A জমা রাখে
alpha = store["G"].mean((2, 3)) # (1, 32): প্রতিটি channel-এর গড় gradient
cam = F.relu((alpha[:, :, None, None] * store["A"]).sum(1))[0] # (7, 7)
return cam, cls, logits.softmax(1)[0, cls].item()সত্যিকারের run (CPU-তে). প্রতিটি ছবির জন্য: আসল আর অনুমিত class, আস্থা, ৭×৭ map-এর চূড়া (সারি, কলাম), আর মোট heat-এর কত ভাগ জিনিসের ওপর পড়েছে, দুইভাবে মাপা: ৭×৭ ঘরে, আর ২৮×২৮-এ bilinear upsample করার পরে pixel-এ। দুটি ছবির map ০–৯ মাপে ছাপা।
~/bazarghar-dl$ uv run python gradcam.py
#1 true Pullover pred Pullover p=0.98 peak (6,1) on-object 7x7 97% | 28x28 66%
1 0 0 6 0 0 0
0 0 0 3 0 0 0
0 0 6 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 5 0 0 0 1
0 9 0 0 0 8 7
#2 true Trouser pred Trouser p=1.00 peak (1,4) on-object 7x7 100% | 28x28 88%
#0 true Ankle boot pred Ankle boot p=0.98 peak (1,5) on-object 7x7 0% | 28x28 6%
0 0 0 0 0 2 1
0 0 0 0 7 9 0
0 0 0 0 0 0 0
2 8 5 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
#4 true Shirt pred Shirt p=0.97 peak (1,5) on-object 7x7 92% | 28x28 82%
কোন স্তর বেছে নেবেন? শেষ conv স্তর সাধারণত সেরা আপস: আগের স্তরে রেজোলিউশন বেশি কিন্তু অর্থ কম, তাই map কিনারার মতো দেখায়; শেষ স্তরে অর্থ বেশি কিন্তু grid মোটা। ResNet-এ প্রচলিত পছন্দ শেষ residual stage-এর output. সূক্ষ্ম রেজোলিউশন চাইলে Grad-CAM-কে guided backpropagation-এর সঙ্গে গুণ করা হতো (Guided Grad-CAM), কিন্তু অংশ ৬-এ দেখবেন কেন তা সতর্কতার সঙ্গে নিতে হয়। Grad-CAM++ বা Score-CAM-এর মতো পরের রূপও আছে; কোনোটিই sanity check-এর প্রয়োজন সরায় না।
Pullover-এ heat ছড়ানো বিন্দুতে: নিচের সারির দুই প্রান্ত (হাতার কবজি) আর ওপরের মাঝখান (গলা)। Pullover, coat আর shirt-এর পার্থক্য সত্যিই এই জায়গাগুলোতে, তাই ব্যাখ্যাটি বিশ্বাসযোগ্য মনে হয়। Ankle boot অদ্ভুত: চূড়া (১, ৫)-এ, যেখানে ছবিতে কিছুই নেই, আর জিনিসের ওপর heat ০%. এর মানে কি model ফাঁকা জায়গা দেখে? পরের অংশে এই প্রশ্নই আসল বিষয়।
৬ · Heatmap কতটা বিশ্বাস করবেন — sanity check
২০১৮ সালে Adebayo ও সহকর্মীরা “Sanity Checks for Saliency Maps” প্রবন্ধে দুটি সরল পরীক্ষা প্রস্তাব করেন। Model randomisation: শেখা weight এলোমেলো করে দিলে ব্যাখ্যা বদলানো উচিত; না বদলালে পদ্ধতিটি model নয়, অন্য কিছু দেখাচ্ছে। Data randomisation: label এলোমেলো করে শেখানো model-এর ব্যাখ্যাও আলাদা হওয়া উচিত। তাঁদের পরীক্ষায় কয়েকটি জনপ্রিয় পদ্ধতি, যেমন Guided Backpropagation, weight এলোমেলো করার পরেও প্রায় একই ছবি দিয়েছিল, অনেকটা একটি কিনারা-খোঁজকের মতো, আর gradient ও Grad-CAM পরীক্ষায় পাস করেছিল বলে প্রবন্ধে রিপোর্ট করা হয়। মিল মাপা হয় Spearman rank correlation দিয়ে: ১-এর কাছে মানে দুটি map প্রায় একই ক্রমে উজ্জ্বল।
from scipy.stats import spearmanr
def saliency(Wm, bm, x, cls): # ∂ log p_cls / ∂x
p = softmax((x @ Wm + bm)[None])[0]
return Wm[:, cls] - Wm @ p
W_c, b_c = m_clean # লোগো ছাড়া শেখা model
W_rand = rng.normal(0, W_c.std(), W_c.shape) # একই মাপের এলোমেলো weight
y_shuf = rng.permutation(yclean) # label এলোমেলো করে আবার শেখানো
W_shuf, b_shuf = fit(Xclean, y_shuf)
rows = {"random weights": [], "shuffled labels": [], "grad x input, random": []}
for i in range(60):
x, c = Xte[i], yte[i]
s = np.abs(saliency(W_c, b_c, x, c))
rows["random weights"].append(spearmanr(s, np.abs(saliency(W_rand, b_c, x, c)))[0])
rows["shuffled labels"].append(spearmanr(s, np.abs(saliency(W_shuf, b_shuf, x, c)))[0])
gxi = np.abs(saliency(W_rand, b_c, x, c) * x) # random model, gradient × input
rows["grad x input, random"].append(spearmanr(gxi, np.abs(x))[0])
print("mean Spearman over 60 test images")
print(f" trained saliency vs random-weight saliency : {np.mean(rows['random weights']):+.2f}")
print(f" trained saliency vs shuffled-label saliency: {np.mean(rows['shuffled labels']):+.2f}")
print(f" random model's |grad x input| vs |input| : {np.mean(rows['grad x input, random']):+.2f}")
x, c = Xte[0], yte[0]
maps = {"input": x, "trained saliency": saliency(W_c, b_c, x, c),
"random saliency": saliency(W_rand, b_c, x, c),
"random grad x input": saliency(W_rand, b_c, x, c) * x}
fig, ax = plt.subplots(1, 4, figsize=(8, 2.4))
for a, (t, m) in zip(ax, maps.items()):
a.imshow(np.abs(m).reshape(8, 8), cmap="gray" if t == "input" else "hot")
a.set_title(t, fontsize=8); a.set_xticks([]); a.set_yticks([])
plt.show()আউটপুট (পাতার Python-এ চালানো; সঙ্গে চারটি ছোট ছবি):
mean Spearman over 60 test images
trained saliency vs random-weight saliency : -0.12
trained saliency vs shuffled-label saliency: +0.03
random model's |grad x input| vs |input| : +0.69
শেখা model-এর saliency এলোমেলো weight-এর saliency-র সঙ্গে প্রায় সম্পর্কহীন (−০.১২), আর এলোমেলো label-এ শেখা model-এর সঙ্গেও (+০.০৩): gradient saliency দুটি পরীক্ষাতেই পাস। কিন্তু তৃতীয় সারি সতর্ক করে: এলোমেলো model-এর gradient × input ছবির নিজের উজ্জ্বলতার সঙ্গে +০.৬৯ মেলে। ছবির চতুর্থ প্যানেলে আকারটি স্পষ্ট দেখা যায়, অথচ model কিছুই শেখেনি। গুণ করা x-ই ছবির আকার এনে দিয়েছে। সুন্দর দেখায় বলে ব্যাখ্যা সঠিক নয়।
একই পরীক্ষা ডেস্কটপের CNN-এ। top layers random মানে শেষ conv আর head এলোমেলো, প্রথম দুটি conv শেখা অবস্থায়; all random পুরো model এলোমেলো। শেষ সারি দেখে, একই ছবিতে প্রথম আর দ্বিতীয় সম্ভাব্য class-এর Grad-CAM কতটা আলাদা। সত্যিকারের run (CPU-তে):
~/bazarghar-dl$ uv run python sanity.py
mean Spearman over 200 test images
saliency: top layers random +0.20 (n=200)
saliency: all random +0.04 (n=200)
Grad-CAM: top layers random -0.04 (n=190)
Grad-CAM: all random -0.05 (n=143)
Grad-CAM: 1st vs 2nd class -0.06 (n=189)
দুটি পদ্ধতিই এই একটি run-এ পাস করেছে: ওপরের স্তর এলোমেলো করলেই map প্রায় সম্পর্কহীন। Saliency-তে +০.২০ বাকি থাকে, কারণ প্রথম দুটি conv স্তর তখনো শেখা অবস্থায়। n ২০০-র কম কারণ কিছু ছবিতে ReLU-র পরে পুরো Grad-CAM map শূন্য, সেখানে correlation সংজ্ঞায়িত নয়; এলোমেলো model-এ ৫৭টি ছবিতে এমন হয়েছে, নিজেই একটি সতর্কসংকেত। শেষ সারি ভালো খবর: ভিন্ন class-এর জন্য map সত্যিই আলাদা।
| ফাঁদ | |
| আসল সংখ্যা | |
| শিক্ষা |
- মোটা grid. Ankle boot-এর Grad-CAM-এর চূড়া (১, ৫) ঘরে, যেখানে ছবি ফাঁকা। ৭×৭ grid-এর একটি ঘর ছবির ৪×৪ pixel-এর ওপর বসে, তাই এই সংখ্যা মোটা দাগের।
- Receptive field. ছোট CNN-এ 5×5 conv, pool, 3×3, pool, 3×3 পার হয়ে শেষ স্তরের একটি ঘর input-এর প্রায় ২০×২০ pixel দেখে (5 → 6 → 10 → 12 → 20). ঘর (১, ৫) তাই জুতার ওপরের কিনারাও দেখে; “ফাঁকা জায়গায় heat” সম্ভবত সেই কিনারার সাড়া, নিশ্চিত নয়।
- Upsampling. একই map ২৮×২৮-এ মসৃণ করে বড় করলে pullover-এ জিনিসের ওপর heat ৯৭% থেকে ৬৬%. ছবিতে রং যত মসৃণ, সংখ্যা তত বদলায়; সুন্দর ওভারলে নিজেই একটি সিদ্ধান্ত।
- Gradient × input. পাতার ব্লক ৩-এ এলোমেলো model-এর gradient × input ছবির সঙ্গে +০.৬৯ মেলে। ছবির আকার দেখা মানেই model আকার দেখেছে, তা নয়।
- Saturation. ব্লক ২-এ আস্থা ০.৯৯৯; দাগের অংশ ঢাকলে কমে মাত্র ০.০০২, কারণ লোগো একাই যথেষ্ট। একক অংশের ব্যাখ্যা শূন্য হলেও অংশটি অপ্রয়োজনীয় নয়।
- প্রতিকার. Randomisation পরীক্ষা (Grad-CAM −০.০৪, saliency +০.২০), class বদলে map বদলায় কি না, অনেক ছবিতে সংখ্যা দিয়ে মাপা, আর ভিন্ন পদ্ধতি (occlusion) দিয়ে মিলিয়ে দেখা।
৭ · Adversarial example আর FGSM — হাতে এক ধাপ
২০১৩ সালে Szegedy ও সহকর্মীরা “Intriguing properties of neural networks” প্রবন্ধে দেখান, ভালো accuracy-র ছবি-classifier-কে প্রায় অদৃশ্য বদলে ভুল উত্তর দেওয়ানো যায়। এমন input-এর নাম adversarial example. পরের বছর Goodfellow, Shlens আর Szegedy একটি দ্রুত পদ্ধতি দেন, Fast Gradient Sign Method:
x′ = clip[0,1]( x + ε · sign( ∇x L(x, y) ) )
L হলো আসল label y-এর cross-entropy loss, sign প্রতিটি ঘরকে +১ বা −১ বানায়, আর ε প্রতি pixel-এ সর্বোচ্চ বদল। এটি gradient descent-এর উল্টো: weight নয়, input বদলায়, আর loss কমানোর বদলে বাড়ায়। বদলের মাপ L∞ norm-এ বাঁধা: কোনো pixel ε-এর বেশি নড়ে না।
| ধাপ | |
| x | |
| হিসাব | |
| অনুমান |
- শুরু. z = 2 × 0.5 − 1 × 0.2 + 0.5 × 0.4 = ১.০; σ(১) = ০.৭৩১১, তাই model ঠিক বলছে, class ১.
- Loss. L = −ln ০.৭৩১১ = ০.৩১৩৩. Logistic loss-এ ∂L/∂z = p − y = −০.২৬৮৯.
- Gradient. z = w·x, তাই ∂L/∂x = (p − y)·w. প্রথম pixel বাড়লে loss কমে (−০.৫৩৭৯), দ্বিতীয়টি বাড়লে loss বাড়ে (+০.২৬৮৯).
- চিহ্ন. FGSM মাপ ফেলে শুধু চিহ্ন রাখে: [−১, +১, −১]. প্রতিটি pixel loss বাড়ানোর দিকে পুরো ε নড়বে।
- বদল. x′ = [0.5 − 0.3, 0.2 + 0.3, 0.4 − 0.3] = [০.২, ০.৫, ০.১]; সবই ০ আর ১-এর মধ্যে, তাই clip কিছু বদলায় না।
- ফল. z′ = −০.০৫, p′ = ০.৪৮৭৫: অনুমান class ০-তে উল্টে গেল। z কমেছে ঠিক ε‖w‖₁ = 0.3 × 3.5 = ১.০৫, কারণ প্রতিটি pixel w-এর চিহ্নের উল্টো দিকে ε নড়েছে।
শেষ ধাপের হিসাবই Goodfellow ও সহকর্মীদের “linearity” ব্যাখ্যা। Linear অংশে ε-এর বদল z-কে ε‖w‖1 = ε ∑ |wi| সরায়। প্রতিটি |wi| ছোট হলেও n মাত্রায় যোগফল n-এর সঙ্গে বাড়ে: ৭৮৪ pixel-এ গড় |w| ০.১ হলে ε = ০.০৫-এ সরণ ০.০৫ × ৭৮.৪ = ৩.৯২. এলোমেলো চিহ্নের noise-এ বদলগুলো একে অপরকে কাটে, তাই সরণ মোটামুটি √n-এর হারে বাড়ে। নিচের ব্লক এটি মাপে, লোগো ছাড়া শেখা model-এ।
def loss_grad(Wm, bm, X, y): # cross-entropy-র ∂L/∂x, প্রতিটি সারির জন্য
P = softmax(X @ Wm + bm); P[np.arange(len(y)), y] -= 1
return P @ Wm.T # (n, 64)
def fgsm(X, y, eps): # x' = x + ε · sign(∂L/∂x)
return X + eps * np.sign(loss_grad(W_c, b_c, X, y))
accm = lambda X: (np.argmax(X @ W_c + b_c, 1) == yte).mean()
epss = [0, 0.05, 0.1, 0.2, 0.3, 0.5]
adv = [accm(fgsm(Xte, yte, e)) for e in epss]
noise = [accm(Xte + e * rng.choice([-1, 1], Xte.shape)) for e in epss]
for e, a, n in zip(epss, adv, noise):
print(f"eps {e:4.2f}: FGSM acc {a:.3f} | random ±eps acc {n:.3f}")
# এক ছবিতে: logit-এর ব্যবধান কতটা বদলায়, আর linear হিসাবে কতটা হওয়ার কথা
x, c = Xte[:1], yte[:1]
z = (x @ W_c + b_c)[0]; k = np.argsort(z)[-2] if z.argmax() == c[0] else z.argmax()
d = W_c[:, c[0]] - W_c[:, k] # margin = z_c − z_k = d·x + …
x2 = fgsm(x, c, 0.1)
print(f"margin before {d @ x[0] + b_c[c[0]] - b_c[k]:.3f}, after {d @ x2[0] + b_c[c[0]] - b_c[k]:.3f}, "
f"0.1·||d||₁ = {0.1 * np.abs(d).sum():.3f}")
fig, ax = plt.subplots(figsize=(5, 3))
ax.plot(epss, adv, "o-", label="FGSM"); ax.plot(epss, noise, "s--", label="random sign noise")
ax.set_xlabel("epsilon (max change per pixel)"); ax.set_ylabel("test accuracy")
ax.set_title("Clean linear model under attack"); ax.legend()
plt.show()আউটপুট (পাতার Python-এ চালানো; সঙ্গে accuracy বনাম ε-এর রেখা):
eps 0.00: FGSM acc 0.827 | random ±eps acc 0.827
eps 0.05: FGSM acc 0.647 | random ±eps acc 0.830
eps 0.10: FGSM acc 0.417 | random ±eps acc 0.837
eps 0.20: FGSM acc 0.067 | random ±eps acc 0.797
eps 0.30: FGSM acc 0.007 | random ±eps acc 0.817
eps 0.50: FGSM acc 0.000 | random ±eps acc 0.713
margin before 3.096, after 1.084, 0.1·||d||₁ = 2.012
ε = ০.১-এ FGSM accuracy ০.৮২৭ থেকে ০.৪১৭; একই মাপের এলোমেলো ±ε noise-এ ০.৮৩৭, কার্যত অপরিবর্তিত। ε = ০.৩-এ FGSM প্রায় সব ছবি ভুল করায়। শেষ লাইনে প্রথম test ছবির (একটি v-bar) প্রথম আর দ্বিতীয় class-এর score-এর ব্যবধান ৩.০৯৬ থেকে ১.০৮৪, অর্থাৎ ঠিক ২.০১২ কমেছে, যা 0.1·‖d‖₁-এর সমান; এখানে d = wv-bar − wbox. এই ছবিতে ৬৪টি pixel-এর সবগুলোতেই gradient-এর চিহ্ন −d-এর চিহ্নের সঙ্গে মিলেছে, তাই হিসাব হুবহু মেলে।
৮ · বাস্তব CNN-এ FGSM, PGD আর adversarial training
এবার Fashion-MNIST-এর ছোট CNN-এ। Pixel ০ থেকে ১-এ, normalization ছাড়া, তাই ε সরাসরি pixel-এর মাপে: ε = ০.০১ মানে ২৫৫ ধূসর স্তরের মধ্যে প্রায় ২.৫ স্তর। তুলনার জন্য একই ε-এর এলোমেলো ±ε noise. শক্তিশালী আক্রমণ হিসেবে PGD (Madry ও সহকর্মী, ২০১৮): ছোট ধাপে (ε/4) দশবার FGSM, প্রতিবার ε-এর সীমায় ফেরত। শুধু শেখা ও প্রতিরক্ষা মাপার জন্য, নিজের model আর প্রকাশ্য ডেটায়।
def fgsm(model, x, y, eps):
x = x.clone().requires_grad_(True)
loss = F.cross_entropy(model(x), y)
g, = torch.autograd.grad(loss, x) # শুধু input-এর gradient
return (x + eps * g.sign()).clamp(0, 1).detach() # এক ধাপ, তারপর বৈধ pixel সীমা
def pgd(model, x, y, eps, steps=10): # বারবার ছোট FGSM, ε-সীমার ভেতরে
a, xa = eps / 4, x.clone()
for _ in range(steps):
xa = fgsm(model, xa, y, a)
xa = torch.min(torch.max(xa, x - eps), x + eps)
return xa
# adversarial training: প্রতিটি batch-এ অর্ধেক loss পরিষ্কার ছবিতে, অর্ধেক FGSM(ε = 0.1) ছবিতে
# loss = 0.5 * CE(model(x), y) + 0.5 * CE(model(fgsm(model, x, y, 0.1)), y)সত্যিকারের run (CPU-তে; ১০,০০০টি test ছবি):
~/bazarghar-dl$ uv run python fgsm.py
eps FGSM acc random ±eps acc
0.00 0.8903 0.8903
0.01 0.7604 0.8891
0.02 0.5801 0.8883
0.05 0.1910 0.8762
0.10 0.0614 0.8192
0.20 0.0036 0.6078
adversarial fine-tune: 2 epochs, 50 s
model clean FGSM 0.1 PGD-10 0.1
standard 0.8903 0.0614 0.0117
adv-trained 0.8583 0.7117 0.2453
চোখে প্রায় অদৃশ্য বদলেই পতন: ε = ০.০১-এ ৮৯.০% থেকে ৭৬.০%, ε = ০.০৫-এ ১৯.১%. এলোমেলো noise-এ ε = ০.০৫-এও ৮৭.৬%. পার্থক্যটি বদলের দিক-এর, মাপের নয়। Adversarial training: শেখা model-কে দুই epoch FGSM ছবিসহ আবার শেখানো হলো (৫০ সেকেন্ড)। পরিষ্কার accuracy ৮৯.০% থেকে ৮৫.৮%, অর্থাৎ কিছু দাম দিতে হলো। FGSM ০.১-এ ৬.১% থেকে ৭১.২%, দেখতে দারুণ। কিন্তু PGD-তে মাত্র ২৪.৫%. যে আক্রমণে শেখানো হয়েছে শুধু সেটিতে মাপলে robustness অনেক বেশি দেখায়; FGSM-এ শেখা model অনেক সময় gradient-কে এমন অগোছালো করে যে এক ধাপের আক্রমণ দিক হারায়। এর নাম gradient masking. এগুলো এক run-এর সংখ্যা, সাধারণ ফল নয়।
নিরাপত্তার কথা: এই পর্বের কৌশল নিজের model-এর দুর্বলতা মাপা আর প্রতিরক্ষা শেখার জন্য। অন্যের চালু সেবা বা model-এ অনুমতি ছাড়া আক্রমণ পরীক্ষা অনৈতিক, আর অনেক দেশে আইনত অপরাধ হতে পারে। বাস্তব প্রতিরক্ষায় একক কৌশল যথেষ্ট নয়: input যাচাই, আস্থা কম হলে মানুষের কাছে পাঠানো, আর পর্যবেক্ষণ একসঙ্গে লাগে (সাইবার নিরাপত্তা কোর্সের পর্ব ৫১).
প্রতিরক্ষার বাস্তব মানচিত্র. Adversarial training এখনো সবচেয়ে পরীক্ষিত পদ্ধতি, কিন্তু একা যথেষ্ট নয়। Input-কে JPEG করা, ঝাপসা করা বা গোপন preprocessing-এর মতো সহজ “প্রতিরক্ষা” অনেক সময় শুধু gradient লুকায়, আর অভিযোজিত আক্রমণে ভাঙে। ব্যবস্থার স্তরে কাজের অভ্যাস: model-কে কম ক্ষমতা দেওয়া (তার একটি ভুলে বড় কিছু না ঘটে), আস্থা কম বা input অস্বাভাবিক হলে মানুষের কাছে পাঠানো, query-র হার সীমিত রাখা, আর ব্যবহারের ডেটায় ভুলের ধরন নজরে রাখা। বাজারঘরের পণ্য-ছবির classifier-এ যেমন, ভুল category-তে গেলে বিক্রেতা আপত্তি জানাতে পারেন, আর সেই আপত্তিগুলোই নতুন test ডেটা হয়।
Robustness আর distribution shift. Adversarial example হলো সবচেয়ে খারাপ দিকের ছোট বদল; বাস্তবে বেশি দেখা যায় স্বাভাবিক বদল: ঝাপসা ক্যামেরা, কম আলো, নতুন সরবরাহকারীর পটভূমি। পাতার লোগো-পরীক্ষা ঠিক এমন একটি shift. দুটিই একই প্রশ্ন করে: training ডেটার বাইরে model কতটা টেকে? মাপার উপায় মেশিন লার্নিং কোর্সের পর্ব ৫৯-এ, আর augmentation দিয়ে কিছু অপরিবর্তনশীলতা শেখানো পর্ব ২৪ (Data augmentation)-এ। AI নিরাপত্তা ও নৈতিকতার বড় প্রশ্নগুলো ভবিষ্যতের একটি আলাদা কোর্সে আরও গভীরে আসবে।
আরও গভীরে: Athalye, Carlini আর Wagner (২০১৮) দেখান, প্রকাশিত অনেক প্রতিরক্ষা আসলে gradient masking ছিল, আর উপযুক্ত আক্রমণে ভেঙে পড়ে; তাই robustness মাপতে শক্তিশালী ও অভিযোজিত আক্রমণ লাগে। Wong, Rice আর Kolter (২০২০) দেখান, এলোমেলো শুরু-বিন্দুসহ FGSM training অনেকটা কাজ করে, তবে “catastrophic overfitting”-এর ঝুঁকি আছে। গাণিতিক নিশ্চয়তা চাইলে randomized smoothing (Cohen ও সহকর্মী, ২০১৯) একটি পরিচিত পথ। ব্যাখ্যার দিকে Captum হলো PyTorch-এর ব্যাখ্যা-library, যাতে Saliency, Occlusion আর LayerGradCam-এর মতো class আছে; ব্যবহারের আগে নিজের version-এর docs দেখুন।
পরীক্ষা ও ইন্টারভিউতে:
- Grad-CAM-এ αk কীভাবে আসে? শেষ conv স্তরের feature map Ak-এর সাপেক্ষে ∂yc-এর সব ঘরের গড়; তারপর ReLU(∑ αkAk).
- FGSM-এর সূত্র? x′ = clip(x + ε·sign(∇xL)); এক backward, L∞ সীমা ε.
- w = [2, −1, 0.5], ε = 0.3-এ linear score কতটা সরে? ε‖w‖₁ = 0.3 × 3.5 = ১.০৫.
- Saliency-র sanity check? Weight বা label এলোমেলো করলে map বদলানো উচিত; না বদলালে পদ্ধতি model-কে প্রতিফলিত করে না।
- FGSM-এ শেখানো model FGSM-এ ৭১% কিন্তু PGD-তে ২৫% পেলে? Robustness অতিরঞ্জিত, সম্ভবত gradient masking; শক্তিশালী আক্রমণে মাপুন।
৯ · AI-এর যুগে — ব্যাখ্যা আর robustness-এর কোড যাচাই
“আমার CNN-এর জন্য Grad-CAM লিখে দাও” বললে AI assistant কয়েক সেকেন্ডে কোড আর একটি সুন্দর ছবি দেয়। ছবি সুন্দর হলেই কোড ঠিক, এমন নয়; এই পর্বে দেখেছেন, এলোমেলো model-ও সুন্দর ছবি দিতে পারে।
| AI প্রায়ই যা করে | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
পুরোনো register_backward_hook দেয় | deprecated; কিছু module-এ ভুল gradient দিতে পারে | register_full_backward_hook, নিজের version-এর docs |
model.eval() ছাড়া Grad-CAM বা আক্রমণ | dropout আর BatchNorm-এ প্রতিবার ভিন্ন ফল | একই ছবিতে দুবার চালিয়ে মিলিয়ে দেখুন |
পুরো আক্রমণ torch.no_grad()-এর ভেতরে | input-এর gradient-ই তৈরি হয় না | শুধু মূল্যায়নের forward no_grad-এ রাখুন |
| Normalize করা input-এ ε, কিন্তু clip [0, 1]-এ | ε-এর মাপ আর বৈধ সীমা ভিন্ন জগতে | normalization model-এর ভেতরে রাখুন, ε pixel-এর মাপে |
| শুধু FGSM-এ robustness রিপোর্ট | gradient masking লুকিয়ে থাকে | PGD, এলোমেলো শুরু, কয়েকটি seed |
| “Heatmap দেখাচ্ছে model কী ভাবে” বলে | map একটি আনুমানিক বর্ণনা, model-এর চিন্তা নয় | randomisation পরীক্ষা, অনেক ছবিতে সংখ্যা |
AI-কে কোডের সঙ্গে যাচাইয়ের পরিকল্পনাও চাইতে বলুন:
AI-এর দেওয়া কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Adversarial example কীভাবে আবিষ্কৃত হলো, আর কেন এটি শুধু গবেষণাগারের কৌতূহল নয়?
২০১৩ সালের শেষে Christian Szegedy ও সহকর্মীরা (Google আর কয়েকটি বিশ্ববিদ্যালয়ের গবেষক) “Intriguing properties of neural networks” প্রবন্ধে (ICLR ২০১৪) দেখান, ImageNet-এ ভালো ফল দেওয়া network-কে প্রায় অদৃশ্য বদলে ভুল উত্তর দেওয়ানো যায়, আর একটি model-এর জন্য বানানো বদল অনেক সময় অন্য model-কেও ঠকায় (transferability). তাঁরা বদল খুঁজেছিলেন একটি ধীর optimization দিয়ে। ২০১৪ সালে Goodfellow, Shlens আর Szegedy “Explaining and Harnessing Adversarial Examples” প্রবন্ধে (ICLR ২০১৫) যুক্তি দেন, কারণটি অতিরিক্ত nonlinearity নয়, বরং উচ্চ মাত্রায় প্রায়-linear আচরণ: হাজারো pixel-এর প্রতিটিতে ছোট বদল একই দিকে যোগ হয়ে বড় সরণ ঘটায়। এই ধারণা থেকেই এক ধাপের FGSM, আর adversarial training-এর প্রথম রূপ।
গবেষণাগারের বাইরেও প্রশ্নটি এসেছে। Kurakin, Goodfellow আর Bengio (২০১৬) দেখান, adversarial ছবি কাগজে ছেপে ফোনের ক্যামেরায় তুললেও কিছু ক্ষেত্রে model ঠকে। Eykholt ও সহকর্মীরা (২০১৮) রাস্তার চিহ্নে স্টিকার লাগিয়ে একটি classifier-কে ভুল পড়ানোর পরীক্ষা রিপোর্ট করেন। এসব পরীক্ষা নিয়ন্ত্রিত অবস্থায়, নির্দিষ্ট model-এ; বাস্তব ব্যবস্থায় ঝুঁকি কতটা তা নির্ভর করে আক্রমণকারী কী জানে আর কী বদলাতে পারে তার ওপর, যাকে threat model বলে।
এই পর্বের run দেখায় সমস্যাটি কত তীব্র: ২৫৫ ধূসর স্তরের মধ্যে মাত্র আড়াই স্তরের বদলে (ε = ০.০১) accuracy ৮৯% থেকে ৭৬%. অথচ একই মাপের এলোমেলো noise প্রায় কিছুই করে না। মানুষ আর model তাই একই জিনিস দেখে না; model এমন দিকেও সংবেদনশীল যা আমাদের চোখে অর্থহীন। চিকিৎসা, পরিচয় যাচাই বা স্বয়ংক্রিয় গাড়ির মতো জায়গায় তাই শুধু accuracy নয়, সবচেয়ে খারাপ অবস্থার আচরণও মাপা দরকার।
মূল উপলব্ধি: Adversarial example উচ্চ মাত্রার linear জ্যামিতির স্বাভাবিক ফল; তাই প্রতিরক্ষা একটি কৌশল নয়, বরং মাপা, শক্ত করা আর ব্যবস্থার স্তরে সুরক্ষার সমন্বয়।
প্র ০২“Sanity Checks for Saliency Maps” প্রবন্ধ কী দেখিয়েছিল, আর ব্যাখ্যার পদ্ধতি বাছাইয়ে এর শিক্ষা কী?
২০১৮ সালে Julius Adebayo, Justin Gilmer, Michael Muelly, Ian Goodfellow, Moritz Hardt আর Been Kim NeurIPS-এ এই প্রবন্ধ প্রকাশ করেন। তখন অনেক saliency পদ্ধতি ছিল, আর সেগুলোর তুলনা হতো মূলত চোখে দেখে: কোন ছবি বেশি পরিষ্কার, বস্তুর আকার বেশি ধরা পড়ে। তাঁদের প্রশ্ন ছিল উল্টো: ব্যাখ্যা যদি model-এর হয়, তবে model নষ্ট করে দিলে ব্যাখ্যাও বদলানো উচিত। তাঁরা ওপর থেকে নিচে স্তর ধরে weight এলোমেলো করেন (cascading randomisation), আর আলাদাভাবে এলোমেলো label-এ model শেখান।
ফল চমকপ্রদ ছিল। Guided Backpropagation আর Guided Grad-CAM-এর মতো কিছু পদ্ধতি পুরো model এলোমেলো হওয়ার পরেও প্রায় একই ছবি দিল। প্রবন্ধে তাঁরা দেখান, এসব পদ্ধতির ফল অনেকটা ছবির কিনারা-খোঁজকের মতো, যা model ছাড়াই পাওয়া যায়। তুলনায় সাধারণ gradient আর Grad-CAM পরীক্ষায় অনেক ভালো করে। এই পর্বের পাতার ব্লকে একই ঘটনা ছোট আকারে দেখেছেন: এলোমেলো model-এর gradient × input ছবির সঙ্গে +০.৬৯ মেলে, কারণ x দিয়ে গুণ করলেই ছবির আকার ফিরে আসে।
শিক্ষাটি পদ্ধতিগত। প্রথমত, ব্যাখ্যার “সৌন্দর্য” আর “বিশ্বস্ততা” আলাদা জিনিস; মানুষ যে ছবিকে বিশ্বাসযোগ্য মনে করে, তা model-এর আসল হিসাব প্রতিফলিত নাও করতে পারে। দ্বিতীয়ত, প্রতিটি নতুন model আর ডেটায় এমন পরীক্ষা সস্তা, তাই নিয়মিত চালানো উচিত। তৃতীয়ত, পরীক্ষায় পাস করা মানে ব্যাখ্যা সঠিক, এমন নয়; শুধু একটি স্পষ্ট ব্যর্থতা বাদ পড়ল। পরের গবেষণায় এই পরীক্ষাগুলোর সীমাবদ্ধতা নিয়েও বিতর্ক হয়েছে, যেমন randomisation-এর পরে কোন মাপে মিল মাপা উচিত।
মূল উপলব্ধি: একটি ব্যাখ্যা-পদ্ধতিকে বিশ্বাস করার আগে দেখুন, model বদলালে ব্যাখ্যা বদলায় কি না; চোখের সৌন্দর্য কোনো প্রমাণ নয়।
প্র ০৩“Clever Hans” ধরনের model কী, আর ছবির model-এ shortcut কোথা থেকে আসে?
বিংশ শতাব্দীর শুরুতে জার্মানির একটি ঘোড়া, Clever Hans, গণিতের প্রশ্নের উত্তরে খুর ঠুকে সবাইকে অবাক করত। মনোবিজ্ঞানী Oskar Pfungst পরীক্ষা করে দেখান, ঘোড়াটি প্রশ্নকর্তার শরীরের সূক্ষ্ম ইঙ্গিত পড়ছিল, অঙ্ক নয়। ২০১৯ সালে Lapuschkin ও সহকর্মীরা Nature Communications-এ “Unmasking Clever Hans predictors” প্রবন্ধে এই নাম ধার করেন। তাঁদের রিপোর্ট অনুযায়ী, PASCAL VOC ডেটাসেটের ঘোড়ার ছবি চেনার একটি model অনেক সময় ছবির কোণের কপিরাইট-লেখা দেখে সিদ্ধান্ত নিচ্ছিল, কারণ ঘোড়ার অনেক ছবি একই উৎস থেকে এসেছিল। ব্যাখ্যার heatmap সেটি ধরিয়ে দেয়। এই পর্বের লোগো-পরীক্ষা সেই ঘটনার একটি ছোট, কাল্পনিক অনুকরণ।
চিকিৎসা-ছবিতেও এমন সতর্কতা আছে। Zech ও সহকর্মীরা (২০১৮) বুকের X-ray-এর একটি model নিয়ে রিপোর্ট করেন, model হাসপাতাল চিনে ফেলতে পারছিল, আর হাসপাতাল ভেদে রোগের হার ভিন্ন হওয়ায় সেই তথ্য অনুমানে ঢুকে যাচ্ছিল। Geirhos ও সহকর্মীরা (২০১৯) দেখান, ImageNet-এ শেখা CNN অনেক সময় আকারের চেয়ে texture-এর ওপর বেশি নির্ভর করে, যেখানে মানুষ আকারকে বেশি গুরুত্ব দেয়।
Shortcut-এর উৎস প্রায় সবসময় ডেটা সংগ্রহের প্রক্রিয়া: একই class-এর ছবি একই ক্যামেরা, পটভূমি, সময় বা উৎস থেকে আসা। Model-এর দোষ নেই; training loss কমানোর সবচেয়ে সহজ পথ সে নেয়। প্রতিকার ডেটার স্তরে: উৎস ভেদে আলাদা test set, metadata দেখে বিভাজন, পটভূমি বদলানো পরীক্ষা, আর ব্যাখ্যা দিয়ে সন্দেহ খোঁজা। বাজারঘরের ক্ষেত্রে যেমন, একেক সরবরাহকারীর ছবি আলাদা রেখে মূল্যায়ন করলে লোগোর ফাঁদ শুরুতেই ধরা পড়ত।
মূল উপলব্ধি: উঁচু test accuracy shortcut লুকিয়ে রাখতে পারে, যদি test ডেটাও একই পক্ষপাতে বানানো হয়; ব্যাখ্যা সন্দেহ জাগায়, আর ভিন্ন উৎসের ডেটায় পরীক্ষা তা প্রমাণ করে।
প্র ০৪Grad-CAM-এর মতো পরে-জোড়া ব্যাখ্যা কি উচ্চ-ঝুঁকির সিদ্ধান্তের জন্য যথেষ্ট? কোন যুক্তি দুই পক্ষে আছে?
পক্ষের যুক্তি: CNN-এর মতো জটিল model ছবিতে অনেক ভালো ফল দেয়, আর Grad-CAM, occlusion বা SHAP-এর মতো কৌশল সেই model না বদলেই কিছুটা ভেতরে দেখার সুযোগ দেয়। Selvaraju ও সহকর্মীরা Grad-CAM-এর প্রবন্ধে দেখান, এই ব্যাখ্যা দিয়ে ডেটাসেটের পক্ষপাত খুঁজে পাওয়া যায় আর মানুষকে দুটি model-এর মধ্যে বেশি নির্ভরযোগ্যটি বাছতে সাহায্য করা যায়। এই পর্বেও লোগোর shortcut saliency আর occlusion দিয়েই ধরা পড়েছে।
বিপক্ষের যুক্তি সবচেয়ে জোরালোভাবে দেন Cynthia Rudin, ২০১৯ সালে Nature Machine Intelligence-এ “Stop explaining black box machine learning models for high stakes decisions” প্রবন্ধে। তাঁর মতে, পরে-জোড়া ব্যাখ্যা model-এর আনুমানিক বর্ণনা, আর বর্ণনা ভুল হলে সিদ্ধান্তও ভুল পথে যায়; উচ্চ-ঝুঁকির কাজে তাই শুরু থেকেই ব্যাখ্যাযোগ্য model বানানো উচিত, যেখানে তা সম্ভব। Grad-CAM-এর নিজস্ব সীমাও আছে: ৭×৭-এর মতো মোটা grid, ReLU-তে বিপক্ষের প্রমাণ বাদ পড়া, আর upsampling-এর ওপর নির্ভরতা। “কোথায়” দেখায়, “কী” দেখে তা বলে না: জুতার কিনারা না texture, map থেকে বোঝা যায় না।
বাস্তব অবস্থান সাধারণত মাঝখানে। ব্যাখ্যা দিয়ে ডিবাগ আর সন্দেহ খোঁজা খুব কাজের; কিন্তু একজন গ্রাহক বা রোগীকে “model এখানে দেখেছে” বলে সিদ্ধান্তের যৌক্তিকতা দেখানোর জন্য তা দুর্বল। যেখানে আইন বা নিয়ম ব্যাখ্যা চায়, সেখানে কোন ধরনের ব্যাখ্যা গ্রহণযোগ্য, তা দেশ আর ক্ষেত্র ভেদে আলাদা; নিজের প্রসঙ্গে নিয়ম পড়ে নিতে হয়। পরিমাপযোগ্য পরীক্ষা, যেমন সবচেয়ে উজ্জ্বল অংশ মুছলে accuracy কতটা কমে, ব্যাখ্যাকে বিশ্বাসের কাছাকাছি আনে।
মূল উপলব্ধি: পরে-জোড়া ব্যাখ্যা ডিবাগের হাতিয়ার হিসেবে শক্তিশালী, সিদ্ধান্তের প্রমাণ হিসেবে দুর্বল; উচ্চ-ঝুঁকির কাজে সহজ model-এর বিকল্প আগে বিবেচনা করুন।
প্র ০৫Adversarial training কেন পরিষ্কার accuracy কমায়, আর robustness কীভাবে সৎভাবে মাপতে হয়?
Madry ও সহকর্মীরা (ICLR ২০১৮) adversarial training-কে একটি min-max সমস্যা হিসেবে লেখেন: weight এমনভাবে শেখানো, যাতে ε-সীমার ভেতরের সবচেয়ে খারাপ বদলেও loss কম থাকে। ভেতরের সর্বোচ্চ খোঁজা হয় PGD দিয়ে, বাইরের সর্বনিম্ন সাধারণ gradient descent দিয়ে। এভাবে শেখানো model প্রায়ই অনেক বেশি robust, কিন্তু খরচ কয়েক গুণ, কারণ প্রতিটি batch-এ কয়েকবার বাড়তি backward লাগে।
পরিষ্কার accuracy কেন কমে, তা নিয়ে Tsipras ও সহকর্মীরা (২০১৯) একটি তাত্ত্বিক উদাহরণে দেখান, robust আর সাধারণভাবে নির্ভুল হওয়ার মধ্যে আসল টানাপোড়েন থাকতে পারে: যে দুর্বল কিন্তু কাজের সংকেত ছোট বদলে উল্টে যায়, robust model সেগুলো ব্যবহার করতে পারে না। এই পর্বের run-এও পরিষ্কার accuracy ৮৯.০% থেকে ৮৫.৮%-এ নেমেছে। তবে টানাপোড়েন কতটা অনিবার্য, তা নিয়ে গবেষকদের মধ্যে আলোচনা চলছে; বেশি ডেটা আর বড় model-এ ব্যবধান কমে বলেও রিপোর্ট আছে।
সৎ মাপের নিয়ম কয়েকটি। এক, যে আক্রমণে শেখানো হয়েছে শুধু সেটিতে মাপা নয়; এই run-এ FGSM-এ ৭১% কিন্তু PGD-তে ২৪.৫%. দুই, আক্রমণকারী প্রতিরক্ষা জানে ধরে নিয়ে অভিযোজিত আক্রমণ চালানো, কারণ Athalye ও সহকর্মীরা (২০১৮) দেখিয়েছেন অনেক প্রতিরক্ষা আসলে gradient লুকিয়ে টিকে ছিল। তিন, ε আর norm স্পষ্ট বলা: L∞ ε = ০.১ আর L2-এর একটি সীমা ভিন্ন প্রতিশ্রুতি। চার, পরিষ্কার accuracy-র সঙ্গে robust accuracy একসঙ্গে রিপোর্ট, কয়েকটি seed-এ। RobustBench-এর মতো উদ্যোগ এই কারণেই মানসম্মত মূল্যায়ন চালায়।
মূল উপলব্ধি: Robustness একটি দাবি, যার প্রমাণ সবচেয়ে শক্তিশালী আক্রমণে মাপা; দুর্বল আক্রমণে উঁচু সংখ্যা মিথ্যা নিরাপত্তাবোধ দেয়।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান। পাতার ব্লক বদলে চালিয়েও যাচাই করতে পারেন।
-
পূর্ণ numerical: (ক) শেষ conv স্তরে দুটি ২×২ feature map: A₁ = [[1, 0], [2, 1]], A₂ = [[0, 3], [1, 0]]; class c-এর gradient G₁ = [[0.2, 0.2], [0.4, 0.0]], G₂ = [[−0.1, 0.1], [−0.3, −0.1]]. Grad-CAM map আর তার চূড়া বের করুন। (খ) একটি linear model-এর weight w = [3, −1, 2, 0.5]; ε = 0.05-এর FGSM score সর্বোচ্চ কতটা সরাতে পারে? (গ) ২২৪×২২৪ ছবিতে ৩২×৩২ patch, stride ১৬-এ occlusion-এ কয়টি forward লাগে?
(ক) α₁ = (0.2 + 0.2 + 0.4 + 0) ÷ 4 = ০.২; α₂ = (−0.1 + 0.1 − 0.3 − 0.1) ÷ 4 = −০.১. 0.2·A₁ − 0.1·A₂ = [[0.2, −0.3], [0.3, 0.2]]; ReLU-র পরে [[0.2, 0], [0.3, 0.2]], চূড়া নিচের বাঁ ঘরে (০.৩). ডান-ওপরের ঘরে channel ২ জ্বলেছিল, যা class-এর বিপক্ষে, তাই সেটি শূন্য। (খ) ε‖w‖₁ = 0.05 × (3 + 1 + 2 + 0.5) = ০.৩২৫. (গ) (224 − 32) ÷ 16 + 1 = ১৩; ১৩ × ১৩ = ১৬৯টি forward.
-
সমালোচনা ও নকশা: বাজারঘরের একজন সহকর্মী পণ্য-ছবির classifier-এর পাঁচটি বাছাই করা Grad-CAM ছবি দেখিয়ে বললেন, “model পণ্যটাই দেখে, পটভূমি নয়, তাই এটি নতুন সরবরাহকারীর ছবিতেও চলবে।” এই যুক্তির দুর্বলতা কী, আর আপনি কী পরীক্ষা করতেন?
দুর্বলতা: (১) পাঁচটি বাছাই করা ছবি নমুনা নয়, প্রমাণও নয়; (২) সুন্দর heatmap বিশ্বস্ততার প্রমাণ নয়, sanity check হয়নি; (৩) “কোথায় দেখে” জানা গেলেও “কী দেখে” (আকার, texture, লোগো) জানা যায় না; (৪) নতুন সরবরাহকারীর ছবিতে চলবে কি না, তা ব্যাখ্যা নয়, সেই ডেটায় মাপই বলবে। পরিকল্পনা: weight এলোমেলো করে map বদলায় কি না; শত শত test ছবিতে জিনিসের ওপর heat-এর ভাগ সংখ্যায়; সবচেয়ে উজ্জ্বল অংশ মুছলে accuracy কতটা কমে আর পটভূমি মুছলে কতটা; সরবরাহকারী ধরে আলাদা test set; পটভূমি বদলে বা লোগো ঢেকে accuracy; আর কয়েকটি seed-এ ফলাফল।
-
আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
import numpy as np w = np.array([2.0, -1.0, 0.5]); x = np.array([0.5, 0.2, 0.4]); y = 1 p = 1 / (1 + np.exp(-(w @ x))) g = (p - y) * w x_adv = np.clip(x + 0.2 * np.sign(g), 0, 1) print(x_adv, round(float(w @ x_adv), 2), int(w @ x_adv > 0))[0.3 0.4 0.2] 0.3 1p − y ঋণাত্মক, তাই sign(g) = −sign(w) = [−1, +1, −1]. ε = ০.২-এ x′ = [0.3, 0.4, 0.2], আর w·x′ = 0.6 − 0.4 + 0.1 = ০.৩. Score ১.০ থেকে ঠিক ε‖w‖₁ = 0.2 × 3.5 = ০.৭ কমেছে, কিন্তু এখনো ধনাত্মক, তাই অনুমান class ১-ই থাকে। ε = ০.৩-এ (১.০৫ সরণ) অনুমান উল্টে যায়।
-
কোড লিখুন: PyTorch-এ একটি function
occlusion_map(model, x, cls, patch=4, stride=4)লিখুন, যা (1, C, H, W) আকারের ছবির জন্য প্রতিটি অবস্থানে patch শূন্য করে classcls-এর সম্ভাবনা কতটা কমে তার map ফেরত দেয়। সব ঢাকা ছবি একটি batch-এ চালাবেন।import torch @torch.no_grad() # শুধু forward, gradient লাগে না def occlusion_map(model, x, cls, patch=4, stride=4): model.eval() _, _, H, W = x.shape rows = range(0, H - patch + 1, stride) cols = range(0, W - patch + 1, stride) batch = [] for r in rows: for c in cols: z = x.clone() z[:, :, r:r + patch, c:c + patch] = 0 # ঢাকা = শূন্য (চাইলে গড় রং) batch.append(z) base = model(x).softmax(1)[0, cls] p = model(torch.cat(batch)).softmax(1)[:, cls] return (base - p).reshape(len(rows), len(cols)) # বড় মান = গুরুত্বপূর্ণ জায়গা২৮×২৮ ছবিতে patch ৪, stride ৪-এ ৭ × ৭ = ৪৯টি ঢাকা ছবি, এক batch-এ। বড় ছবিতে সব কপি একসঙ্গে memory-তে না ধরলে কয়েকটি ছোট batch-এ ভাগ করুন। শূন্যের বদলে ছবির গড় রং দিয়ে ঢেকে ফল মিলিয়ে দেখুন, কারণ “ঢাকা”র রং নিজেই একটি সংকেত।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমPyTorch-এ hook দিয়ে Grad-CAM-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- শেষ conv স্তরে forward আর backward hook বসানো, model.eval()
- Forward চালানো; hook feature map A জমা রাখে
- Class c-এর score yc থেকে backward; hook gradient G জমা রাখে
- G-এর প্রতিটি channel-এর স্থান জুড়ে গড় নিয়ে αk
- ∑ αkAk-এর ওপর ReLU
- Map ছবির মাপে upsample করে ওভারলে, তারপর hook সরানো
Grad-CAM-এ একটি channel-এর ৩×৩ gradient map-এর নয়টি ঘরের যোগফল ০.৯. ওই channel-এর ওজন α কত?
- ০.৯
- ০.১
- gradient map-এর সবচেয়ে বড় ঘর
- feature map-এর গড়
Logistic model-এ w = [1, −2], b = 0, x = [0.5, 0.5], আসল label y = 1. ε = 0.1-এর FGSM-এর পরে x′ কত (clip-এ কিছু বদলায় না)?
- [0.6, 0.4]
- [0.44, 0.62]
- [0.4, 0.6]
- [0.4, 0.4]
একটি saliency পদ্ধতি শেখা model আর সব weight এলোমেলো করা model-এ প্রায় একই map দেয় (Spearman প্রায় ০.৯). এর সবচেয়ে যুক্তিসংগত ব্যাখ্যা কী?
- পদ্ধতিটি model-কে নয়, মূলত ছবির গঠন দেখাচ্ছে; sanity check-এ ব্যর্থ
- পদ্ধতিটি খুব নির্ভরযোগ্য, কারণ ফল স্থির
- দুটি model একই কারণে সিদ্ধান্ত নেয়
- Spearman correlation এখানে অর্থহীন
একটি CNN-এর accuracy ε = 0.05-এর FGSM-এ ৮৯% থেকে ১৯%-এ নামে, কিন্তু একই মাপের এলোমেলো ±ε noise-এ ৮৮%-এ থাকে। মূল কারণ কী?
- FGSM-এ pixel বেশি বদলায়
- Noise কম pixel বদলায়
- FGSM-এর পরীক্ষায় কঠিন ছবি বেছে নেওয়া হয়েছে
- FGSM-এর বদলের দিক loss বাড়ানোর দিকে সাজানো, তাই ছোট বদল যোগ হয়ে বড় সরণ ঘটায়
FGSM দিয়ে adversarial training করা একটি model FGSM (ε = 0.1)-এ ৭১% কিন্তু ১০ ধাপের PGD (একই ε)-এ ২৫% accuracy পায়। কোন সিদ্ধান্ত সবচেয়ে ঠিক?
- Model-এর robustness ৭১%
- FGSM-এর সংখ্যা অতিরঞ্জিত, সম্ভবত gradient masking; robustness শক্তিশালী আক্রমণে মাপতে হয়
- PGD-র কোডে নিশ্চয়ই ভুল আছে
- Adversarial training কোনো কাজে আসেনি
২২৪×২২৪ ছবিতে ১৬×১৬ patch আর stride ৮ দিয়ে occlusion sensitivity-তে কয়টি forward pass লাগে?
- ১৯৬
- ৭৮৪
- ৭২৯
- একটি backward যথেষ্ট
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রশ্ন বুঝে কৌশল: Filter দেখায় স্তর কী নকশা খোঁজে; saliency দেখায় কোন pixel-এ score সংবেদনশীল; occlusion দেখায় কী সরালে আস্থা কমে; Grad-CAM দেখায় শেষ conv স্তরের কোন জায়গা class-এর পক্ষে।
- Grad-CAM: αk = gradient-এর স্থান-গড়, map = ReLU(∑ αkAk); forward hook A, backward hook G, model.eval().
- যাচাই: Weight আর label এলোমেলো করে map বদলায় কি না দেখুন; gradient × input এলোমেলো model-এও সুন্দর দেখায়; অনেক ছবিতে সংখ্যা দিয়ে মাপুন।
- FGSM: x′ = clip(x + ε·sign(∇xL)); linear অংশে score সরে ε‖w‖₁, তাই উচ্চ মাত্রায় ছোট ε-ই যথেষ্ট, আর এলোমেলো noise অনেক কম ক্ষতি করে।
- Robustness: Adversarial training কিছু পরিষ্কার accuracy-র দামে robustness বাড়ায়; সবসময় PGD-র মতো শক্তিশালী আক্রমণে মাপুন, আর কৌশলগুলো শুধু নিজের model-এর সুরক্ষায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৭ · Sequence ডেটা — সময়, টেক্সট আর padding ও maskingপরবর্তী পর্বমডিউল ৫-এর শুরু: window, padding, mask, nn.Embedding আর PackedSequence শেখা।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ; পর্ব ২৭ আর মডিউল ৫-এর sequence model এখান থেকে খুলবেন।
- পর্ব ০৮ · PyTorch autogradআগের পর্বTensor hook আর autograd.grad, যা Grad-CAM আর FGSM-এর ভিত্তি।
- পর্ব ১৭ · Vanishing আর exploding gradientআগের পর্বModule-এর backward hook দিয়ে স্তর ধরে gradient মাপা।
- মেশিন লার্নিং · পর্ব ৫৭ · SHAP আর LIMEআগের ভিত্তিএকটি অনুমানের ব্যাখ্যা, Shapley value আর ব্যাখ্যার ফাঁদ।
- মেশিন লার্নিং · পর্ব ৫৯ · Distribution shiftআগের ভিত্তিTraining-এর বাইরের ডেটায় model কতটা টেকে, তা ধরা আর সামলানো।
- AI-এর ভিত্তি · পর্ব ১৮ · ছবি মানেই arrayআগের ভিত্তিPixel, channel আর ছবির ছোট হিসাব।
- সাইবার নিরাপত্তা · পর্ব ৫১ · AI নিরাপত্তাসম্পর্কিতAI ব্যবস্থার ঝুঁকি আর সুরক্ষার প্রতিরক্ষামূলক দৃষ্টি।