সরাসরি পর্বে যান
পর্ব ৩৮ · ৯১-এর মধ্যে · মডিউল ৬

Vision Transformer — ছবিকে patch-এর sequence হিসেবে দেখা

Vision Transformer: Seeing an Image as a Sequence of Patches

Taught in Bangla, this lesson splits images into patches, counts tokens for a 224 by 224 input, and shows in in-page NumPy that patch embedding equals a stride-equal-kernel convolution. It covers class token versus mean pooling and 2D positions, trains a small ViT against a CNN of equal size on Fashion-MNIST at several data sizes in a real desktop run on the GPU, measures shift robustness, extracts pretrained vit_b_16 features beside resnet18, and reads attention maps carefully.

এই পর্বে যা শিখবেন

  • ছবিকে patch-এ কেটে token বানানো, আর ২২৪ × ২২৪ থেকে ১৯৭ token-এর পুরো হিসাব
  • Patch embedding কেন একই সঙ্গে linear map আর stride = kernel convolution
  • Class token বনাম mean pooling, আর ছবির grid-এ positional embedding
  • নিজে লেখা ছোট ViT বনাম সমান parameter-এর CNN: ডেটার আকার বদলে সৎ তুলনা, আর inductive bias মাপা
  • Pretrained vit_b_16 দিয়ে feature extraction, আর class token-এর attention map পড়া ও তার সীমা

এই মডিউলের আগের পর্বগুলোতে Transformer পড়েছে শব্দ: পর্ব ৩৩ (Self-attention আর multi-head attention), পর্ব ৩৪ (Positional encoding), পর্ব ৩৫ (Transformer block). Block-এর ভেতরে কোথাও লেখা নেই যে token মানে শব্দই হতে হবে। শুধু একটি (B, T, d) আকারের tensor চাই। তাহলে ছবিকে এই আকারে আনতে পারলেই কি চলবে? আর পর্ব ২০ ও পর্ব ২২-এর CNN যে অনুমানগুলো নিয়ে কাজ করত, সেগুলো ছেড়ে দিলে দাম কত? এই পর্বে দুটি প্রশ্নই মেপে দেখব। ছবি যে আসলে সংখ্যার array, তা মনে না থাকলে AI-এর ভিত্তি কোর্সের পর্ব ১৮ দেখে নিন।

১ · মূল ধারণা — ছবি কেটে শব্দ বানানো

বাজারঘরের গুদামে একটি পণ্যের ছবি বর্গ বর্গ করে কেটে কার্ডে লাগানো হলো। প্রতিটি কার্ডের কোণে ক্রমিক নম্বর, যাতে বোঝা যায় টুকরোটি ছবির কোথায় ছিল। টেবিলে একটি বাড়তি ফাঁকা কার্ডও আছে: ম্যানেজারের। এরপর কয়েক দফা বৈঠক। প্রতি দফায় প্রতিটি কার্ড বাকি সবার কথা শোনে, দূরের কার্ডেরও। শেষে ম্যানেজারের কার্ড দেখেই বলা হয়, এটি শাড়ি না জুতা। টুকরো হলো patch, ক্রমিক নম্বর positional embedding, ম্যানেজার class token, বৈঠক self-attention.

Vision Transformer বা ViT ঠিক এটুকুই। ২০২০ সালে Dosovitskiy ও সহকর্মীদের “An Image is Worth 16x16 Words” প্রবন্ধে ধারণাটি পরিচিতি পায়। প্রবন্ধটির নিজের ভাষ্যে, বিশাল ডেটায় আগে শেখানোর পরে ViT ছোট benchmark-এ সেরা CNN-গুলোর কাছাকাছি বা সমান ফল দেয়। “বড় ডেটায় আগে শেখানোর পরে” কথাটি লক্ষ করুন; পুরো পর্বটি এই শর্তের চারপাশে ঘুরবে।

ViT-এর পথ, ২২৪ × ২২৪ ছবি আর patch ১৬ দিয়ে

Shapes for ViT-B/16: image, patches, tokens with class token, encoder blocks, class token output, head

Vision Transformer pipeline with tensor shapes ছবি(3, 224, 224) ১৯৬টি patch, flatten(196, 768) Linear projection E(196, 768) + class token, + position(197, 768) ১২টি Transformer block(197, 768), T একই থাকে শুধু class token, LayerNorm(768) Linear head(1000): শ্রেণির score

CNN-এর সঙ্গে আসল পার্থক্য একটি বাক্যে: CNN-এ প্রথম স্তরের একটি neuron দেখে ৩ × ৩ pixel, দূরের pixel-এর খবর পৌঁছায় অনেক স্তর পরে। ViT-এ প্রথম block থেকেই প্রতিটি patch বাকি সব patch-এর দিকে তাকাতে পারে। এই স্বাধীনতার দাম আছে, যা অংশ ৬-এ মাপব।

২ · Patch-এ কাটা — shape-এর পুরো হিসাব

ছবির উচ্চতা H, প্রস্থ W, channel C, patch-এর বাহু P হলে patch-এর সংখ্যা N আর প্রতিটির দৈর্ঘ্য:

N = (H / P) × (W / P)  ·  একটি patch = P × P × C সংখ্যা  ·  T = N + 1

T হলো class token-সহ মোট token. H আর W-কে P দিয়ে নিঃশেষে ভাগ যেতে হয়, নইলে ধারে কিছু pixel বাড়তি থাকে।

Cutting a 224 by 224 image into 196 patches and adding a class token token-এর সারি …196 cls
  1. Input. রঙিন ছবি, shape (3, 224, 224). মোট ৩ × ২২৪ × ২২৪ = ১,৫০,৫২৮টি সংখ্যা। প্রতিটি pixel-কে token বানালে T হতো ৫০,১৭৬, আর attention-এর জোড়া প্রায় ২৫০ কোটি। তাই সরাসরি pixel চলে না।
  2. Grid. ২২৪ / ১৬ = ১৪. প্রতি বাহুতে ১৪টি patch, ভাগশেষ ০. এটাই ১৪ × ১৪ grid.
  3. N. ১৪ × ১৪ = ১৯৬টি patch. বাঁ থেকে ডানে, ওপর থেকে নিচে পড়ে এক সারিতে সাজানো হয়, যেমন বইয়ের লেখা। সারি r, কলাম c-এর patch-এর ক্রম ১৪r + c.
  4. Flatten. প্রতিটি patch-এ ১৬ × ১৬ × ৩ = ৭৬৮টি সংখ্যা। Shape এখন (196, 768). একটি ছবি হলো ১৯৬ “শব্দের” বাক্য।
  5. Linear projection. E নামের শেখা map প্রতিটি ৭৬৮-কে D = ৭৬৮ মাত্রায় নেয়। Parameter ৭৬৮ × ৭৬৮ + ৭৬৮ = ৫,৯০,৫৯২. দুই ৭৬৮ এখানে কাকতালীয়ভাবে সমান; patch ৮ হলে input ১৯২, D তবু ৭৬৮.
  6. Class token. একটি শেখা vector সামনে জোড়া হয়: T = ১৯৬ + ১ = ১৯৭। সব token-এর জন্য একটি শেখা position: (1, 197, 768), মানে ১,৫১,২৯৬টি সংখ্যা। Attention-এর জোড়া ১৯৭২ = ৩৮,৮০৯, pixel-এর হিসাবের প্রায় ৬৫,০০০ ভাগের এক ভাগ।

কাটা আর flatten করার আসল কাজ reshape আর transpose দিয়ে, আর ভুলটিও ঠিক সেখানে হয়। নিচের ব্লকে সামান্য একটি কৃত্রিম ছবি কেটে দেখা যাক। (পাতার প্রথম run-এ package নামতে কয়েক সেকেন্ড লাগতে পারে।)

import numpy as np
import matplotlib.pyplot as plt

H = W = 24; P = 6; N = H // P            # ২৪ × ২৪ ছবি, ৬ × ৬ patch
yy, xx = np.mgrid[:H, :W]
img = 0.15 + 0.03 * np.random.default_rng(38).normal(size=(H, W))
img[(yy - 14) ** 2 + (xx - 9) ** 2 < 36] = 0.9     # একটি গোল
img[3:9, 15:21] = 0.55                             # একটি চতুর্ভুজ

grid = img.reshape(N, P, N, P).transpose(0, 2, 1, 3)   # (সারি, কলাম, P, P)
tokens = grid.reshape(N * N, P * P)                    # (N*N, P*P)
print("image  ", img.shape)
print("grid   ", grid.shape)
print("tokens ", tokens.shape, "| 16 patches, 36 numbers each")
print("patch (1, 2) == img[6:12, 12:18]:", np.array_equal(grid[1, 2], img[6:12, 12:18]))
print("token 6 == patch (1, 2):", np.array_equal(tokens[1 * N + 2], grid[1, 2].ravel()))
back = tokens.reshape(N, N, P, P).transpose(0, 2, 1, 3).reshape(H, W)
print("rebuilt image equal:", np.array_equal(back, img))
bad = img.reshape(N * N, P * P)                        # transpose ছাড়া: ভুল
print("no transpose, token 6 correct:", np.array_equal(bad[1 * N + 2], grid[1, 2].ravel()))

fig, ax = plt.subplots(1, 3, figsize=(8, 3))
ax[0].imshow(img, cmap="gray", vmin=0, vmax=1); ax[0].set_title("image 24 x 24")
ax[0].set_xticks(range(-1, 24, 6)); ax[0].set_yticks(range(-1, 24, 6))
ax[0].set_xticklabels([]); ax[0].set_yticklabels([]); ax[0].grid(color="w")
gap = np.ones((N * (P + 1) - 1, N * (P + 1) - 1))
for r in range(N):
    for c in range(N):
        gap[r * (P + 1):r * (P + 1) + P, c * (P + 1):c * (P + 1) + P] = grid[r, c]
ax[1].imshow(gap, cmap="gray", vmin=0, vmax=1); ax[1].set_title("16 patches"); ax[1].axis("off")
ax[2].imshow(tokens, cmap="gray", vmin=0, vmax=1, aspect="auto")
ax[2].set(title="tokens (16, 36)", xlabel="pixel in patch", ylabel="token")
plt.show()

আউটপুট (পাতার Python-এ চালানো; সঙ্গে একটি ছবি):

image   (24, 24)
grid    (4, 4, 6, 6)
tokens  (16, 36) | 16 patches, 36 numbers each
patch (1, 2) == img[6:12, 12:18]: True
token 6 == patch (1, 2): True
rebuilt image equal: True
no transpose, token 6 correct: False

১৬টি patch, প্রতিটিতে ৩৬টি সংখ্যা। (১, ২) নম্বর patch ঠিক img[6:12, 12:18], আর সে এক সারিতে ৬ নম্বর token, কারণ ৪ × ১ + ২ = ৬. উল্টো পথে ছবি হুবহু ফেরে। শেষ সারিটি ফাঁদ: transpose ছাড়াও reshape কোনো error দেয় না, shape-ও ঠিক থাকে, কিন্তু token-গুলোতে এক patch-এর সংখ্যা থাকে না, থাকে একই সারির টুকরো মেশানো। Shape মিলে যাওয়া তাই প্রমাণ নয়; নিজের চোখে একটি patch মিলিয়ে দেখতে হয়। ডানের ছবিতে প্রতিটি আড়াআড়ি সারি একটি token.

এবার বিভিন্ন আকারের হিসাব। Token বাড়লে attention-এর খরচ বাড়ে T2-এর হারে (পর্ব ৩৩ Self-attention), তাই patch-এর আকার সরাসরি খরচ ঠিক করে।

import numpy as np
from scipy.ndimage import zoom

print(" image patch grid  tokens    T^2   proj params (d=768, RGB)")
for S, P in [(28, 4), (28, 7), (32, 4), (224, 32), (224, 16), (224, 14), (384, 16)]:
    n = S // P
    T = n * n + 1                                  # + class token
    proj = P * P * 3 * 768 + 768
    print(f"{S:6d} {P:5d} {n:2d}x{n:<2d} {T:6d} {T * T:7,d} {proj:10,d}")

print("224 / 16 remainder:", 224 % 16, "| 225 / 16 remainder:", 225 % 16)

base = 197 ** 2
for S in (224, 384, 512):
    T = (S // 16) ** 2 + 1
    print(f"{S}: attention pairs vs 224 = {T * T / base:.1f}x, tokens {T / 197:.1f}x")

rng = np.random.default_rng(38)                    # 14×14 grid-এ শেখা pos, নকল
d = 8
freq = rng.normal(scale=0.3, size=(d, 2))
yy, xx = np.mgrid[:14, :14]
pos = np.stack([np.sin(f[0] * yy + f[1] * xx) for f in freq], axis=-1)
print("pos grid at 224:", pos.shape)
pos24 = zoom(pos, (24 / 14, 24 / 14, 1), order=1)  # 384-এর জন্য bilinear
print("pos grid at 384:", pos24.shape, "| tokens", 24 * 24 + 1)
print("corners kept:", np.allclose(pos[-1, -1], pos24[-1, -1]))

আউটপুট (পাতার Python-এ চালানো):

image patch grid  tokens    T^2   proj params (d=768, RGB)
    28     4  7x7      50   2,500     37,632
    28     7  4x4      17     289    113,664
    32     4  8x8      65   4,225     37,632
   224    32  7x7      50   2,500  2,360,064
   224    16 14x14    197  38,809    590,592
   224    14 16x16    257  66,049    452,352
   384    16 24x24    577 332,929    590,592
224 / 16 remainder: 0 | 225 / 16 remainder: 1
224: attention pairs vs 224 = 1.0x, tokens 1.0x
384: attention pairs vs 224 = 8.6x, tokens 2.9x
512: attention pairs vs 224 = 27.1x, tokens 5.2x
pos grid at 224: (14, 14, 8)
pos grid at 384: (24, 24, 8) | tokens 577
corners kept: True

দুটি জিনিস পড়ুন। প্রথম, patch ৩২ থেকে ১৬-তে নামলে token চার গুণ, attention-এর জোড়া প্রায় ১৬ গুণ। ছবি ২২৪ থেকে ৩৮৪ হলে জোড়া ৮.৬ গুণ, যদিও E-এর parameter একই থাকে। দ্বিতীয়, ৩৮৪-এ grid ১৪ × ১৪ থেকে ২৪ × ২৪ হয়, তাই শেখা position আর মেলে না। সমাধান: position-গুলোকে একটি ছোট ছবির মতো ধরে bilinear resize. কোণার মান ঠিক থাকে, মাঝের মান দুই পাশ থেকে আন্দাজ। এখানে position গুলো নকল, sin দিয়ে বানানো; শুধু আকার বদলানো দেখাই উদ্দেশ্য। মূল প্রবন্ধও এই 2-D interpolation-এর কথা বলে, আর torchvision-এ এর ফাংশন interpolate_embeddings.

হাতে একটি পূর্ণ হিসাব: ৯৬ × ৯৬ RGB ছবি, P = ৮, D = ১৯২, batch ৩২. Grid ৯৬ / ৮ = ১২, N = ১৪৪, T = ১৪৫. একটি patch ৮ × ৮ × ৩ = ১৯২টি সংখ্যা। E-এর parameter ১৯২ × ১৯২ + ১৯২ = ৩৭,০৫৬; position ১৪৫ × ১৯২ = ২৭,৮৪০; একটি head-এ attention-এর জোড়া ১৪৫২ = ২১,০২৫. Token tensor (32, 145, 192) float32-এ ৩২ × ১৪৫ × ১৯২ × ৪ = ৩৫,৬৩,৫২০ byte, প্রায় ৩.৬ MB (পর্ব ১২-এর মেমরির হিসাব)। পরীক্ষায় এই ধরনের প্রশ্নে এই ক্রমে হিসাব করলে ভুলের সুযোগ কম।

৩ · Patch embedding — linear map আর convolution একই জিনিস

কাগজে লেখা আছে “কাটুন, flatten করুন, linear map”. কিন্তু বাস্তব কোডে, torchvision-সহ, প্রায়ই পাবেন একটি মাত্র লাইন: nn.Conv2d(C, D, kernel_size=P, stride=P). পর্ব ২০-এর convolution কীভাবে “কাটা + linear” হলো? ছোট সংখ্যায় হাতে দেখুন।

A convolution with stride equal to kernel size computes a patch embedding ছবি, ৪ × ৪ 3 1 0 2 4 2 5 1 1 0 2 6 7 3 0 4 output grid output 3 × 3
  1. Linear map-এর চোখে. ছবির ওপর-বাঁয়ের patch: [3, 1, 4, 2]. E-এর প্রথম সারি [1, 0, 0, −1] হলে output ১·৩ + ০·১ + ০·৪ − ১·২ = ১.
  2. Convolution-এর চোখে. ওই চারটি সংখ্যা ২ × ২ আকারে সাজালেই kernel [[1, 0], [0, −1]]. একই ঘরে একই গুণ, কারণ ফল একই: ১. Kernel আসলে E-এর একটি সারি, শুধু ভাঁজ করা।
  3. Stride = ২. জানালা ঠিক P ঘর সরে। ওপর-ডানের patch [0, 2, 5, 1] → ০ − ১ = −১. কোনো pixel দুই জানালায় পড়ে না, কোনোটি বাদও যায় না।
  4. পরের সারি. নিচের দুটি patch: [1, 0, 7, 3] → ১ − ৩ = −২, আর [2, 6, 0, 4] → ২ − ৪ = −২. প্রথম filter শেষ: output grid [[1, −1], [−2, −2]].
  5. দ্বিতীয় filter. E-এর দ্বিতীয় সারি [0, 1, 1, 0], মানে kernel [[0, 1], [1, 0]]. চার জায়গায় ৫, ৭, ৭, ৬. Output (2, 2, 2), মানে (D, N-এর বাহু, N-এর বাহু)। Flatten আর transpose করলে চারটি token: [1, 5], [−1, 7], [−2, 7], [−2, 6]. প্রতিটি filter একটি মাত্রা, প্রতিটি অবস্থান একটি token.
  6. Stride = ১ হলে? জানালা বসে (৪ − ২ + ১)২ = ৯ জায়গায়, পাশাপাশি জানালায় pixel ভাগাভাগি হয়। এটি আর patch কাটা নয়, সাধারণ convolution. দুটি একই শুধু stride = kernel হলে।

এবার বড় আকারে: একই সংখ্যা দিয়ে দুই পথে হিসাব, আর একটি ক্রমের ফাঁদ।

import numpy as np

rng = np.random.default_rng(38)
H, P, C, d = 8, 4, 3, 5                  # ৮ × ৮, ৩ channel, patch ৪, d = ৫
img = rng.random((C, H, H))              # (C, H, W): PyTorch-এর ক্রম
E = rng.normal(size=(d, C * P * P))      # linear map-এর weight (d, 48)
b = rng.normal(size=d)                   # bias

def conv2d(x, kern, bias, stride):       # kern: (d, C, P, P)
    k = kern.shape[-1]; pos = range(0, x.shape[-1] - k + 1, stride)
    out = np.zeros((len(kern), len(pos), len(pos)))
    for f in range(len(kern)):
        for i, r in enumerate(pos):
            for j, c in enumerate(pos):
                out[f, i, j] = np.sum(kern[f] * x[:, r:r+k, c:c+k]) + bias[f]
    return out

kern = E.reshape(d, C, P, P)             # একই সংখ্যা, শুধু আকার বদল
conv = conv2d(img, kern, b, stride=P)    # (d, 2, 2)
print("conv output          ", conv.shape)
conv_tok = conv.reshape(d, -1).T         # (4, d): flatten + transpose

N = H // P                               # এবার patch কেটে linear map
pat = img.reshape(C, N, P, N, P).transpose(1, 3, 0, 2, 4).reshape(N * N, -1)
lin_tok = pat @ E.T + b
print("linear output        ", lin_tok.shape)
print("max |conv - linear|  ", np.abs(conv_tok - lin_tok).max())
print("params linear        ", E.size + b.size, "= 48*5 + 5")
print("params conv          ", kern.size + b.size, "= 5*3*4*4 + 5")

hwc = img.transpose(1, 2, 0)             # (H, W, C) ক্রমে কাটলে কী হয়?
pat2 = hwc.reshape(N, P, N, P, C).transpose(0, 2, 1, 3, 4).reshape(N * N, -1)
print("same weights, HWC flatten: max diff",
      round(np.abs(pat2 @ E.T + b - conv_tok).max(), 3))
print("stride 1 output      ", conv2d(img, kern, b, stride=1).shape, "(overlap)")

আউটপুট (পাতার Python-এ চালানো):

conv output           (5, 2, 2)
linear output         (4, 5)
max |conv - linear|   5.329070518200751e-15
params linear         245 = 48*5 + 5
params conv           245 = 5*3*4*4 + 5
same weights, HWC flatten: max diff 4.258
stride 1 output       (5, 5, 5) (overlap)

দুই পথের পার্থক্য ১০−১৫-এর ঘরে, যা শুধু float-এর গোলের ফল; parameter-ও দুই পথে ২৪৫. ক্রমের ফাঁদটি লক্ষ করুন: একই weight, কিন্তু patch-কে (P, P, C) ক্রমে flatten করলে output অনেক আলাদা (পার্থক্য ৪.২৫৮)। E-এর প্রতিটি সংখ্যা একটি নির্দিষ্ট channel আর ঘরের জন্য, তাই ক্রম বদলালে সে অন্য সংখ্যার সঙ্গে গুণ হয়। অন্য framework থেকে weight আনার সময় এই ভুল হয়, আর কোনো error দেখায় না।

একই পরীক্ষা PyTorch-এ, nn.Conv2d আর nn.Linear-এ weight কপি করে (সত্যিকারের run, torch 2.15.0, CPU-তে):

~/bazarghar-dl$ uv run python shapes.py    # অংশবিশেষ
conv tokens (2, 16, 8) | linear tokens (2, 16, 8) | max diff 2.4e-07
nn.Linear params 392 | nn.Conv2d params 392
interpolate_embeddings 224 -> 384: (1, 197, 768) -> (1, 577, 768)

Conv পথের সুবিধা হাতে কলমে: unfold আর permute-এর ঝামেলা নেই, আর GPU-তে দক্ষ convolution কোড আগে থেকেই আছে। মডেলে এটিই একমাত্র convolution; এরপর সব Transformer block.

৪ · Class token, mean pooling আর ছবির position

Block-এর পরেও T-টি token থাকে, অথচ শ্রেণিবিভাগের head চায় একটি vector. দুটি প্রচলিত পথ:

  • Class token: সামনে জোড়া একটি শেখা vector; শেষ block-এর পরে শুধু তাকেই head-এ পাঠানো। ধারণাটি BERT-এর [CLS] token থেকে নেওয়া (BERT আসবে পর্ব ৪৮-এ)।
  • Mean pooling: Class token নেই; সব patch token-এর গড়। CNN-এর global average pooling-এর সমতুল্য।

Position নিয়ে: Attention নিজে patch-এর ক্রম জানে না (পর্ব ৩৩-এ শব্দের ক্রমে দেখা বৈশিষ্ট্য)। Patch এলোমেলো করলে ফলও শুধু একইভাবে এলোমেলো হয়। তাই position যোগ করতে হয়। ViT-এর মূল প্রবন্ধে একটি সাধারণ শেখা vector প্রতিটি ক্রমের জন্য, “1-D” position, যদিও ছবি 2-D; প্রবন্ধের রিপোর্ট অনুযায়ী 2-D-সচেতন position-এ বড় লাভ পাওয়া যায়নি। Beyer ও সহকর্মীদের পরের একটি কাজ (২০২২) শেখা position-এর বদলে স্থির 2-D sin-cos আর mean pooling ব্যবহার করে ভালো ফল জানায়। বিস্তারিত সূত্র পর্ব ৩৪-এ (Positional encoding).

নিচের ব্লকে একটি block-এর পুরো forward pass, NumPy-তে, প্রতিটি shape ছাপানো। পরীক্ষাটি সরল: patch এলোমেলো করলে শেষ vector বদলায় কি?

import numpy as np
rng = np.random.default_rng(38)
B, N, PP, d, h = 2, 16, 36, 16, 2         # ২ ছবি, ১৬ patch, প্রতিটি ৩৬ সংখ্যা, d, head
x = rng.random((B, N, PP))
E = rng.normal(scale=0.2, size=(PP, d))
cls = rng.normal(scale=0.2, size=(1, 1, d))
pos = rng.normal(scale=0.2, size=(1, N + 1, d))
Wq, Wk, Wv, M1, M2 = rng.normal(scale=0.3, size=(5, d, d))
ln = lambda z: (z - z.mean(-1, keepdims=True)) / (z.std(-1, keepdims=True) + 1e-6)
split = lambda a: a.reshape(B, -1, h, d // h).transpose(0, 2, 1, 3)

def attend(z):                            # (B, T, d) -> ২ head-এর attention
    q, k, v = split(z @ Wq), split(z @ Wk), split(z @ Wv)
    s = q @ k.transpose(0, 1, 3, 2) / np.sqrt(d // h)
    w = np.exp(s - s.max(-1, keepdims=True)); w /= w.sum(-1, keepdims=True)
    return (w @ v).transpose(0, 2, 1, 3).reshape(B, -1, d), w

def vit(x, use_pos=True):                 # pre-norm block একটি
    t = x @ E
    z = np.concatenate([np.repeat(cls, B, axis=0), t], axis=1)
    z = z + pos if use_pos else z
    y, w = attend(ln(z)); z = z + y
    z = z + np.tanh(ln(z) @ M1) @ M2
    return z, w, t

z, w, t = vit(x)
print("patches", x.shape, "-> tokens", t.shape, "-> + cls", z.shape)
print("attention", w.shape, "| row sum", w[0, 0, 0].sum().round(6))
c, m = z[:, 0], z[:, 1:].mean(1)
print("class token", c.shape, "| mean pool", m.shape, "| max diff", np.abs(c - m).max().round(3))
perm = rng.permutation(N)
for p in (False, True):
    a, b = vit(x, p)[0], vit(x[:, perm], p)[0]
    dc, dm = np.abs(a[:, 0] - b[:, 0]).max(), np.abs(a[:, 1:].mean(1) - b[:, 1:].mean(1)).max()
    print(f"pos={p!s:5} patches shuffled: cls change {dc:.2e}, mean change {dm:.2e}")

আউটপুট (পাতার Python-এ চালানো):

patches (2, 16, 36) -> tokens (2, 16, 16) -> + cls (2, 17, 16)
attention (2, 2, 17, 17) | row sum 1.0
class token (2, 16) | mean pool (2, 16) | max diff 1.286
pos=False patches shuffled: cls change 6.66e-16, mean change 8.88e-16
pos=True  patches shuffled: cls change 4.16e-01, mean change 3.24e-01

Attention-এর shape (2, 2, 17, 17): ২ ছবি, ২ head, ১৭ × ১৭ জোড়া, প্রতিটি সারির যোগফল ১. Weight এলোমেলো, শেখানো হয়নি; এখানে শুধু গঠন দেখা। আসল ফল শেষ দুই সারিতে। Position ছাড়া patch এলোমেলো করলেও class token আর mean দুটিই কার্যত একই থাকে (পার্থক্য ১০−১৫-এর নিচে)। অর্থাৎ position ছাড়া ViT-এর কাছে ছবি একটি patch-এর থলে; জুতার ফিতা ওপরে না নিচে, তা জানার পথ নেই। Position যোগ করতেই ফল বদলে যায়। ছবির সব জ্যামিতির খবর ঢোকে এই একটি যোগ দিয়ে।

৫ · নিজে লেখা ছোট ViT — PyTorch-এ

NumPy-র গঠন এবার শেখানোর মতো করে PyTorch-এ। Attention আর block পর্ব ৩৩ আর পর্ব ৩৫-এর মতোই, pre-norm (পর্ব ১৪); নতুন শুধু TinyViT. কোডটি আমাদের চালানো ফাইল থেকে নেওয়া, শুধু pooling আর position বন্ধ করার শাখাগুলো সংক্ষেপ করা।

class Attention(nn.Module):
    def __init__(self, d, heads):
        super().__init__()
        self.h, self.dk = heads, d // heads
        self.qkv, self.out = nn.Linear(d, 3 * d), nn.Linear(d, d)
    def forward(self, x):
        B, T, d = x.shape
        q, k, v = self.qkv(x).reshape(B, T, 3, self.h, self.dk).permute(2, 0, 3, 1, 4)
        w = (q @ k.transpose(-2, -1) / math.sqrt(self.dk)).softmax(-1)  # (B, h, T, T)
        return self.out((w @ v).transpose(1, 2).reshape(B, T, d))

class Block(nn.Module):                       # pre-norm
    def __init__(self, d, heads, ratio):
        super().__init__()
        self.n1, self.att = nn.LayerNorm(d), Attention(d, heads)
        self.n2 = nn.LayerNorm(d)
        self.mlp = nn.Sequential(nn.Linear(d, ratio * d), nn.GELU(), nn.Linear(ratio * d, d))
    def forward(self, x):
        x = x + self.att(self.n1(x))
        return x + self.mlp(self.n2(x))

class TinyViT(nn.Module):
    def __init__(self, img=28, patch=4, chans=1, d=64, depth=4, heads=4, ratio=2, classes=10):
        super().__init__()
        self.proj = nn.Conv2d(chans, d, patch, stride=patch)     # patch embedding
        n = (img // patch) ** 2
        self.cls = nn.Parameter(torch.zeros(1, 1, d))
        self.pos = nn.Parameter(torch.zeros(1, n + 1, d))
        self.blocks = nn.ModuleList(Block(d, heads, ratio) for _ in range(depth))
        self.norm, self.head = nn.LayerNorm(d), nn.Linear(d, classes)
    def forward(self, x):
        x = self.proj(x).flatten(2).transpose(1, 2)              # (B, N, d)
        x = torch.cat([self.cls.expand(len(x), -1, -1), x], 1) + self.pos
        for b in self.blocks: x = b(x)
        return self.head(self.norm(x)[:, 0])

সত্যিকারের run (Fashion-MNIST-এর ২৮ × ২৮ ধূসর ছবির মাপে, torch ২.১৫.০, Python ৩.১৪.৪):

~/bazarghar-dl$ uv run python shapes.py    # অংশবিশেষ, CPU-তে
proj (patch embedding)      1,088
4 blocks                  133,888
final LayerNorm               128
head                          650
class token                    64
pos embedding               3,200
total                     139,018
one block: attention 16,640 | mlp 16,576 | 2 LayerNorm 256
input       (2, 1, 28, 28)
after conv  (2, 64, 7, 7)
flatten     (2, 49, 64)
cls + pos   (2, 50, 64)
logits      (2, 10)

হাতে মেলান। Patch ৪-এ ২৮ / ৪ = ৭, ৪৯টি patch, class token-সহ ৫০. proj: ৬৪ × ১ × ৪ × ৪ + ৬৪ = ১,০৮৮. Position: ৫০ × ৬৪ = ৩,২০০. Attention: qkv ৩ × (৬৪ × ৬৪ + ৬৪) + out (৬৪ × ৬৪ + ৬৪) = ১৬,৬৪০. মোট parameter-এর ৯৬% Transformer block-এ; patch embedding প্রায় বিনা খরচের।

৬ · সমান parameter-এর CNN-এর সঙ্গে সৎ তুলনা

পর্ব ২০-এ দেখেছেন, CNN কাঠামোতেই দুটি অনুমান বসায়: কাছের pixel একসঙ্গে অর্থ তৈরি করে (locality), আর একই নকশা যেকোনো জায়গায় একই (translation equivariance). এই অনুমানই CNN-এর inductive bias (পর্ব ৬)। ViT-এর প্রবন্ধও লেখে, তার ছবি-নির্দিষ্ট inductive bias CNN-এর চেয়ে অনেক কম। তাহলে যা কাঠামো দেয় না, তা শিখতে হবে ডেটা থেকে। এই দাবিটিকেই বলে ViT-এর data hunger. মেপে দেখা যাক।

পরীক্ষা: Fashion-MNIST (Zalando Research-এর GitHub থেকে, MIT licence; ১০ শ্রেণির ২৮ × ২৮ পোশাকের ছবি, ৬০,০০০ train, ১০,০০০ test). ১,৩৯,০১৮ parameter-এর TinyViT বনাম ১,৪০,৩১৪ parameter-এর SmallCNN (চারটি convolution, BatchNorm, দুবার max pool, global average pooling). দুটিতেই একই recipe: AdamW (lr 1e-3, weight decay 0.05), warmup আর cosine (পর্ব ১৫), batch ১২৮, ৩টি seed. ছোট ডেটায় ৩,০০০ ধাপ, পুরো ডেটায় ৬,০০০ ধাপ। aug মানে ±২ pixel সরানো আর উল্টানো (পর্ব ২৪)। সত্যিকারের run, GPU-তে:

~/bazarghar-dl$ uv run python vit_vs_cnn.py    # অংশবিশেষ
torch 2.15.0+cu132 | train (60000, 1, 28, 28) | test (10000, 1, 28, 28)
TinyViT params 139,018 | SmallCNN params 140,314
model     train-n  aug   test acc %          seeds               time s
TinyViT     1000  False  74.5 +- 0.3   74.9 74.4 74.2    123.0
SmallCNN    1000  False  83.2 +- 0.2   83.2 83.5 83.1    117.1
TinyViT     1000  True   78.9 +- 0.2   79.2 78.7 78.8    111.3
SmallCNN    1000  True   83.9 +- 0.3   83.7 83.6 84.3    307.2
TinyViT     5000  False  81.3 +- 0.3   81.0 81.7 81.3     67.8
SmallCNN    5000  False  88.0 +- 0.2   87.9 87.9 88.3     55.7
TinyViT     5000  True   83.7 +- 0.5   84.3 83.2 83.7     72.9
SmallCNN    5000  True   88.4 +- 0.2   88.1 88.7 88.4    107.4
TinyViT    60000  False  89.4 +- 0.3   89.7 89.0 89.6     50.0
SmallCNN   60000  False  92.2 +- 0.0   92.2 92.2 92.3     42.6
shifted test (full data, no aug), mean of 3 seeds
TinyViT shift 0px: 89.4 shift 1px: 83.4 shift 2px: 63.9 shift 4px: 43.1 shift 6px: 26.5
SmallCNN shift 0px: 92.2 shift 1px: 90.1 shift 2px: 85.5 shift 4px: 86.9 shift 6px: 70.5
Fashion-MNIST · test accuracy, ৩ seed-এর গড়
পরিস্থিতি
TinyViT
SmallCNN
CNN-এর এগিয়ে থাকা
  1. খুব অল্প ডেটা. প্রতি শ্রেণিতে ১০০টি ছবি। CNN ৮৩.২%, ViT ৭৪.৫%; ফারাক ৮.৭ পয়েন্ট, যেখানে seed-এর ওঠানামা মাত্র ০.৩-এর ঘরে। Parameter প্রায় সমান, তাই ফারাকটি আকারের নয়, কাঠামোর।
  2. পাঁচ গুণ ডেটা. দুটিই বাড়ে, কিন্তু ViT বেশি (৬.৮ বনাম ৪.৮ পয়েন্ট)। ফারাক কমে ৬.৭.
  3. পুরো ডেটা. ফারাক ২.৮ পয়েন্ট। ডেটা বাড়লে ফারাক কমে, দাবিটির দিক ঠিক। কিন্তু ViT এখানে CNN-কে ধরতে পারেনি, পার হওয়া তো দূরের কথা। ৬০,০০০ ছবি ViT-এর প্রবন্ধের কোটির হিসাবের ধারেকাছেও নয়।
  4. Augmentation, ১,০০০ ছবি. ViT ৪.৪ পয়েন্ট বাড়ে, CNN মাত্র ০.৭. সরানো আর উল্টানো ডেটাই ViT-কে সেই জ্ঞান দেয়, যা CNN-এর কাঠামোয় আগে থেকে আছে। ফারাক ৮.৭ থেকে ৫.০.
  5. Augmentation, ৫,০০০ ছবি. একই দিক: ViT +২.৪, CNN +০.৪, ফারাক ৪.৭. DeiT-এর মতো কাজের মূল কৌশল এটিই, অনেক বেশি শক্ত augmentation-এ।
  6. সরানো ছবি. Augmentation ছাড়া শেখা model, test ছবি ৪ pixel সরানো (ঘুরিয়ে, torch.roll). CNN ৯২.২ থেকে ৮৬.৯; ViT ৮৯.৪ থেকে ৪৩.১. Equivariance এখানে কাঠামো থেকে এসেছে, আর ViT-এর position সেটি শেখেনি। এই একটি সংখ্যাতেই inductive bias দেখা যায়।

সরানোর সারিটি আরও পড়ুন। ViT মাত্র ১ pixel-এই ৬ পয়েন্ট, ২ pixel-এ ২৫.৫ পয়েন্ট হারায়। কারণ patch-এর সীমানা স্থির: ছবি সরলে প্রতিটি patch-এর সংখ্যা বদলে যায়। আর CNN-এর একটি মজার জিনিস: ২ pixel (৮৫.৫%)-এর চেয়ে ৪ pixel-এ (৮৬.৯%) ফল ভালো। দুটি max pool মিলে stride ৪, তাই ৪-এর গুণিতকে সরানো grid-এর সঙ্গে মেলে। অর্থাৎ CNN-এর equivariance-ও নিখুঁত নয় (পর্ব ২২)।

ডেটা কেন বেশি লাগে, তত্ত্বে: CNN-এ একই kernel সব জায়গায় বসে, তাই ছবির এক কোণে দেখা একটি কিনারা সব জায়গার জন্য শেখা হয়ে যায়। ViT-তেও E আর MLP সব patch-এ একই, কিন্তু কোন অবস্থানের patch কোন অবস্থানের সঙ্গে কতটা সম্পর্ক রাখে, তা position আর attention-কে উদাহরণ দেখে শিখতে হয়। অর্থাৎ যে function-গুলো model প্রকাশ করতে পারে, তাদের জগৎ ViT-তে বড়, CNN-এ ছোট আর ছবির মতো ডেটার দিকে ঝোঁকা। জগৎ বড় হলে একই ডেটায় generalisation gap বড় হওয়ার ঝুঁকি থাকে (মেশিন লার্নিং কোর্সের পর্ব ৩)। ওপরের সংখ্যা এই যুক্তির সঙ্গে মেলে, তবে যুক্তিটি প্রমাণ করে না।

উল্টো পরীক্ষাও করা যাক: ৪ × ৪ patch-এর ৭ × ৭ grid একটি নির্দিষ্ট ক্রমে এলোমেলো করে সব train আর test ছবিতে বসানো। ৩,০০০ ধাপ, পুরো ডেটা:

~/bazarghar-dl$ uv run python variants.py    # অংশবিশেষ
-- fixed shuffle of the 7x7 grid of 4x4 patches, train and test
TinyViT shuffled           params  139,018  acc  87.8 +- 0.4  (88.1 87.3 88.1)  time  64.5 s
SmallCNN shuffled          params  140,314  acc  90.2 +- 0.0  (90.2 90.3 90.1)  time  51.6 s
SmallCNN normal            params  140,314  acc  91.8 +- 0.1  (91.8 91.9 91.6)  time  52.5 s
patch  4 tokens  50        params  139,018  acc  87.9 +- 0.3  (88.3 87.5 87.8)  time  94.4 s

শেষ সারিটি একই run-এর আগের অংশ থেকে আনা, এলোমেলো না করা ViT, একই ৩,০০০ ধাপে। ViT প্রায় বদলায়নি (৮৭.৯ থেকে ৮৭.৮)। এটিই প্রত্যাশিত: এলোমেলো করা মানে position-গুলোর আরেক ক্রম, যা শেখা position পুষিয়ে নিতে পারে। CNN হারায় ১.৬ পয়েন্ট, কিন্তু তবু এগিয়ে। আরও বড় পতন হওয়ার কথা মনে হতে পারে; ৪ × ৪-এর ভেতরের নকশা অক্ষত থাকায় আর পোশাকের ছবি সহজ হওয়ায় সম্ভবত CNN টিকে গেছে। কাঠামোর অনুমান ভাঙলে দাম আছে, কিন্তু এই কাজে সেটি ছোট।

এই ফল থেকে কী বলা যায় না. এটি একটি সহজ ডেটায় একটি ছোট model, একই recipe, কোনো tuning ছাড়া, একটি local run. “CNN, ViT-এর চেয়ে ভালো” এখান থেকে প্রমাণ হয় না; ViT-এর জন্য আলাদা learning rate বা বেশি ধাপে ফারাক হয়তো আরও কমত, যা এখানে পরীক্ষা করা হয়নি। সময়ের সংখ্যাও মোটা দাগের: GPU অন্য কাজেও ভাগ হচ্ছিল, তাই একই SmallCNN-এ একই ৩,০০০ ধাপে কখনো ৫৬, কখনো ১১৭ সেকেন্ড লেগেছে। বলা যায় শুধু এটুকু: এই পরিসরে ডেটা আর augmentation বাড়লে ফারাক কমেছে, মূল দাবির সঙ্গে যা মেলে।

৭ · Patch-এর আকার, pooling আর position — কোনটি কত দামি

Parameter প্রায় একই রেখে শুধু একটি জিনিস বদলালে কী হয়? সব run-এ পুরো ডেটা, ৩,০০০ ধাপ, ৩ seed (এই কারণে ৬,০০০ ধাপের ৮৯.৪%-এর চেয়ে সংখ্যা কম):

~/bazarghar-dl$ uv run python variants.py    # অংশবিশেষ
-- patch size (d = 64, depth 4)
patch  4 tokens  50        params  139,018  acc  87.9 +- 0.3  (88.3 87.5 87.8)  time  94.4 s
patch  7 tokens  17        params  139,018  acc  88.1 +- 0.2  (88.3 88.1 87.8)  time  58.9 s
patch 14 tokens   5        params  147,658  acc  88.7 +- 0.1  (88.8 88.8 88.5)  time  51.6 s
-- pooling and position
mean + learned pos         params  138,890  acc  87.4 +- 0.3  (87.8 87.3 87.1)  time  82.3 s
cls, no pos                params  135,818  acc  84.7 +- 0.5  (85.3 84.2 84.5)  time  81.1 s
mean, no pos               params  135,754  acc  84.4 +- 0.4  (85.0 84.0 84.4)  time  66.8 s
  • Patch-এর আকার: ফলটি প্রত্যাশার উল্টো। ১৪ × ১৪ patch-এ মাত্র ৫টি token, অথচ accuracy সবচেয়ে বেশি (৮৮.৭%), ৪-এর ৮৭.৯%-এর চেয়ে ০.৮ পয়েন্ট বেশি। ব্যাখ্যা নিশ্চিত নয়; হতে পারে কম ধাপে কম token সহজে শেখে, আর পোশাকের সহজ ছবিতে সূক্ষ্ম নকশা কম লাগে। সাধারণ নিয়ম এটি নয়: ViT-এর প্রবন্ধের টেবিলে ImageNet-এ ViT-B/16 সাধারণত B/32-এর চেয়ে ভালো। শিক্ষা হলো, patch-এর আকার একটি hyperparameter; নিজের ডেটায় মাপতে হয়।
  • কাকতালীয় মিল: Patch ৪ আর ৭-এ parameter হুবহু সমান। ৪-এ proj ১,০৮৮ + position ৩,২০০; ৭-এ proj ৩,২০০ + position ১,০৮৮. Patch বড় হলে E বড় হয়, position ছোট।
  • Class token বনাম mean: ০.৫ পয়েন্ট, ওঠানামার ০.৩-এর কাছাকাছি। কার্যত সমান। মূল প্রবন্ধও জানায়, ঠিক learning rate-এ দুটির পার্থক্য সামান্য।
  • Position বাদ: ৩ পয়েন্টের মতো কমে, অথচ অংশ ৪ দেখিয়েছিল ছবি তখন একটি থলে। থলে হয়েও ৮৪.৭%! জুতার patch আর জামার patch চেনাই এখানে অনেকটা কাজ করে দেয়। তাই “৮০%-এর বেশি হয়েছে, কোড ঠিক” বলা বিপজ্জনক; position বাদ পড়া একটি নীরব ভুল হতে পারে।

৮ · Pretrained vit_b_16 — feature extraction, resnet18-এর পাশে

নিজে ViT শেখানোর বদলে বড় ডেটায় শেখানো model ধার নেওয়া যায়, ঠিক পর্ব ২৩-এর মতো। সেখানকার ধরনেই: CIFAR-10 (uoft-cs/cifar10, Hugging Face) থেকে ২,০০০টি train আর ১,০০০টি test ছবি, backbone পুরো freeze, feature একবার বের করে standardize, তারপর একটি linear head ৩০ epoch, ৩ seed. Weight torchvision-এর ViT_B_16_Weights.IMAGENET1K_V1 (প্রায় ৩৩০ MB, লেখার সময়ে নভেম্বর ২০২৬-এ, torchvision ০.২৯.২)।

~/bazarghar-dl$ uv run python pretrained.py        # সত্যিকারের run, GPU-তে, অংশবিশেষ
torch 2.15.0+cu132
train 2000 test 1000
vit_b_16 meta: 86,567,656 (224, 224) {'ImageNet-1K': {'acc@1': 81.072, 'acc@5': 95.318}}
conv_proj: Conv2d(3, 768, kernel_size=(16, 16), stride=(16, 16))
class_token (1, 1, 768) pos_embedding (1, 197, 768)
encoder blocks 12 | heads 12
{'conv_proj': 590592, 'class_token': 768, 'pos_embedding': 151296, '12 blocks': 85054464, 'final LayerNorm': 1536, 'head': 769000} total 86567656
vit features (2000, 768) time 29.8 s
vit_b_16 cls head acc [91.5, 91.6, 91.3] mean 91.5 std 0.1 head params 7,690
vit_b_16 mean-of-patches head acc [90.2, 90.4, 90.0] mean 90.2 std 0.2
resnet18 params 11,689,512 {'ImageNet-1K': {'acc@1': 69.758, 'acc@5': 89.078}}
resnet18 features (2000, 512) time 12.5 s
resnet18 head acc [80.0, 79.6, 79.6] mean 79.7 std 0.2 head params 5,130

প্রথমে হাতের হিসাব মেলান। conv_proj ৫,৯০,৫৯২, position ১,৫১,২৯৬, ঠিক অংশ ২-এর stepper-এর সংখ্যা। আর দেখুন, conv_proj সত্যিই একটি stride = kernel convolution, অংশ ৩-এর কথাই। মোট ৮.৬৬ কোটি parameter-এর ৯৮%-এর বেশি Transformer block-এ।

ফল: ViT ৯১.৫%, resnet18 ৭৯.৭%, ব্যবধান প্রায় ১২ পয়েন্ট। “ViT, CNN-এর চেয়ে ভালো” বলে ফেলা ভুল হবে। এখানে তুলনাটি সমান নয়: vit_b_16-এ parameter প্রায় ৭.৪ গুণ, আর ImageNet-এ নিজের accuracy-ও ৮১.১% বনাম ৬৯.৮% (weight-এর নিজস্ব তথ্য অনুযায়ী)। ফলে তুলনাটি মূলত বড় ও দামি model বনাম ছোট model. সময়ও উল্টো দিকে: feature বের করতে ২৯.৮ বনাম ১২.৫ সেকেন্ড, যদিও GPU তখন অন্য কাজেও ভাগ হচ্ছিল, তাই সংখ্যাটি মোটা দাগের। সঠিক প্রশ্ন “কোনটি ভালো” নয়, বরং “আমার latency আর মেমরির সীমায় কোনটি”। এখানে class token-এর head ৯১.৫%, patch-গুলোর গড়ের head ৯০.২%: এই weight যেহেতু class token দিয়ে শেখানো, ফলটি অস্বাভাবিক নয়। পর্ব ২৩-এর ৭৮.৩% আর এখানকার resnet18-এর ৭৯.৭%-এর ফারাক আসে subset, epoch আর feature standardize করা থেকে।

বাজারঘরের মতো ছোট দোকানের জন্য একটি মোটা দাগের পথ (পরীক্ষিত নিয়ম নয়, শুরুর অনুমান): কয়েক হাজার ছবি হলে একটি ছোট pretrained CNN আর একটি pretrained ViT-এর feature একই split-এ মেপে দেখুন, সবচেয়ে সস্তা কাজ। Scratch থেকে ViT শেখানো হবে শেষ বিকল্প, যদি ডেটা সত্যিই বড় হয় বা ছবি সাধারণ ছবির মতো না হয়, যেখানে ImageNet-এর feature কম খাটে। মাঝের একটি পথ হলো hybrid: patch-এর বদলে CNN-এর feature map থেকে token বানানো, যা মূল প্রবন্ধেও পরীক্ষিত।

ViT-এর পরিবার: কে কোন সমস্যা ধরল

কাজসমস্যামূল কৌশল (প্রবন্ধের দাবি)
DeiT (Touvron ও সহকর্মী, ২০২০)বিশাল ডেটা লাগেশুধু ImageNet-এ শক্ত augmentation আর একটি distillation token, যা একটি শিক্ষক network থেকে শেখে
Swin (Liu ও সহকর্মী, ২০২১)T2 খরচ, এক মাপের featureছোট জানালার ভেতরে attention, জানালা সরানো, স্তরে স্তরে ছোট হওয়া grid; খরচ ছবির আকারের সঙ্গে linear
MAE (He ও সহকর্মী, ২০২১)লেবেল দামিপ্রায় ৭৫% patch লুকিয়ে pixel পুনর্গঠন; encoder শুধু দৃশ্যমান patch দেখে
DINO (Caron ও সহকর্মী, ২০২১)লেবেল ছাড়া শেখাTeacher-student self-distillation; প্রবন্ধটি বলে, feature-এ বস্তুর সীমানার তথ্য স্পষ্ট হয়

এগুলোর ফল প্রবন্ধের নিজের রিপোর্ট, এই পর্বে চালানো হয়নি। উল্টো স্রোতও আছে: ConvNeXt (পর্ব ২২) দেখিয়েছে, আধুনিক training recipe পেলে CNN-ও অনেক এগোয়। “Transformer CNN-কে হারিয়েছে” তাই সরলীকরণ।

৯ · Class token কোথায় তাকায় — সংখ্যায় আর ছবিতে

ViT-এর একটি আকর্ষণ হলো attention সরাসরি পড়া যায়। torchvision-এর block সাধারণত need_weights=False দিয়ে চলে, তাই শেষ block-টি হাতে ডাকতে হয়:

t = vit._process_input(x)                         # (B, 196, 768)
c = vit.class_token.expand(len(t), -1, -1)
z = torch.cat([c, t], 1) + vit.encoder.pos_embedding
L = list(vit.encoder.layers)
for b in L[:-1]:
    z = b(z)
n = L[-1].ln_1(z)
_, w = L[-1].self_attention(n, n, n, need_weights=True,
                            average_attn_weights=True)   # (B, 197, 197)
cls_to_patch = w[:, 0, 1:]                        # class token-এর সারি, (B, 196)
~/bazarghar-dl$ uv run python pretrained.py    # শেষের অংশ, ১,০০০ test ছবি
attention shape (1000, 197, 197) | row sum 1.0
class token to itself, mean 0.34
share of top 10% patches (20 of 196): mean 0.616; uniform would be 0.102
entropy mean 4.05; uniform log(196) = 5.28
share in central 8x8 of 14x14: mean 0.335; uniform 0.327
horse top-5 patches (row, col, share): [(2, 12, 0.049), (2, 11, 0.047), (1, 12, 0.044), (1, 11, 0.044), (7, 13, 0.037)]
ship top-5 patches (row, col, share): [(13, 10, 0.09), (12, 1, 0.087), (1, 1, 0.071), (1, 2, 0.069), (2, 12, 0.051)]

Class token-এর attention, শেষ block, ১২ head-এর গড়

vit_b_16, CIFAR-10 test images (32 × 32 upscaled); left: image shrunk to 14 × 14; right: share per patch, darker means more

Class token attention over 14 by 14 patches in the last block of vit_b_16 horse: ছবি attention, সর্বোচ্চ 0.049 ship: ছবি attention, সর্বোচ্চ 0.090

সংখ্যাগুলো একটি অস্বস্তিকর গল্প বলে। Attention ঘন: ১০% patch পায় ৬১.৬% ভাগ। কিন্তু মাঝের ৮ × ৮ অংশে, CIFAR-এর বস্তু সাধারণত যেখানে থাকে, ভাগ মাত্র ৩৩.৫%, সমান ভাগের ৩২.৭%-এর প্রায় সমান। জাহাজের ছবিতে সবচেয়ে ভারী patch-গুলো কোনায় আর কিনারায়, ঘোড়ার ছবিতে ওপর-ডানের পটভূমিতে। পোস্টারের মতো “model বস্তুর দিকে তাকিয়ে আছে” ছবি এখানে পাওয়া যায়নি। সম্ভাব্য কারণগুলো (নিশ্চিত নয়): ৩২ × ৩২ ছবি টেনে বড় করা, শুধু শেষ block দেখা, ১২ head-এর গড় নেওয়া। Darcet ও সহকর্মীরা (২০২৩) ViT-এর feature map-এ পটভূমির কিছু token-এ অদ্ভুত উঁচু মানের কথাও জানিয়েছেন, যা এই ধরনের ছবি ব্যাখ্যা করতে পারে; এখানে তা যাচাই করা হয়নি।

তাহলে attention কি কিছুই বলে না? সরাসরি পরীক্ষা: ১৯৬-এর ৪৯টি patch মুছে একই head-এর accuracy মাপা (সত্যিকারের run, এক seed-এর head):

~/bazarghar-dl$ uv run python occlude.py
blank 49 of 196 patches, test images 1000, one trained head
no blanking                 acc 91.5
highest-attention 49 patches acc 83.9
lowest-attention  49 patches acc 87.1
random 49 patches            acc 87.2
central 7x7 block            acc 82.3
share of attention in the highest 49 0.839

সবচেয়ে বেশি attention-এর ৪৯টি patch মুছলে accuracy ৮৩.৯%, এলোমেলো ৪৯টিতে ৮৭.২%: attention-এ কিছু তথ্য আছে। কিন্তু ওই ৪৯ patch attention-এর ৮৩.৯% ভাগ ধরে রাখলেও ক্ষতি মাত্র আরও ৩.৩ পয়েন্ট; আর attention যাই বলুক, মাঝের ৭ × ৭ মুছলে ক্ষতি সবচেয়ে বেশি (৮২.৩%). এটি পর্ব ৩২-এর (Attention) সতর্কতারই মাপা রূপ: attention-এর ভাগ আর সিদ্ধান্তে গুরুত্ব এক জিনিস নয়। Abnar আর Zuidema (২০২০) তাই attention rollout প্রস্তাব করেন, যা সব স্তরের attention মিলিয়ে হিসাব করে; তবু সেটিও আনুমানিক। গুরুত্ব জানতে মুছে মাপা (occlusion, পর্ব ২৬-এ CNN-এর জন্য দেখা ধারণা) বেশি নির্ভরযোগ্য।

আরও গভীরে: ViT-এর মূল প্রবন্ধে “mean attention distance” মাপা হয়: নিচের স্তরে কিছু head কাছের, কিছু দূরের patch-এ তাকায়, আর গভীরে গেলে দূরত্ব বাড়ে। মানে “ViT প্রথম স্তর থেকেই বিশ্বজুড়ে দেখে” পুরো সত্য নয়; দেখতে পারে, কিন্তু ডেটা থেকে কিছুটা স্থানীয়তাও শেখে। Xiao ও সহকর্মীদের ২০২১ সালের একটি কাজ জানায়, patch-এর এক বড় convolution-এর বদলে কয়েকটি ছোট convolution বসালে training অনেক স্থির হয়। আর Darcet-দের কাজে কয়েকটি বাড়তি “register” token জুড়ে দিলে map মসৃণ হওয়ার কথা বলা হয়েছে।

পরীক্ষা ও ইন্টারভিউতে:

  • ২২৪ × ২২৪, patch ১৬-এ token কত? ১৪২ = ১৯৬, class token-সহ ১৯৭.
  • RGB, P = ১৬, D = ৭৬৮-এর patch embedding parameter? ৭৬৮ × ৭৬৮ + ৭৬৮ = ৫,৯০,৫৯২.
  • Patch embedding কেন conv? Stride = kernel = P হলে জানালা না মিশে প্রতি patch-এ একই linear map বসায়।
  • ViT-এ data বেশি লাগে কেন? Locality আর translation equivariance কাঠামোয় কম বাঁধা, তাই ডেটা থেকে শিখতে হয়।
  • ছবির আকার বদলালে কী বদলাতে হয়? শুধু position: grid-এ 2-D interpolation; E আর block-এর weight একই থাকে।

১০ · AI-এর যুগে ViT — কী যাচাই করবেন

“PyTorch-এ একটি ViT লিখে দাও” বললে AI assistant কয়েক সেকেন্ডে চলমান কোড দেয়, আর সেটি চলেও। সমস্যা হলো, ভুলগুলো error দেয় না: ভুল flatten ক্রম, position-এর আকারে class token বাদ, ভুল normalization, একটিমাত্র seed-এ “ViT জিতেছে”। সাধারণ ভুলের তালিকা:

AI যা করেযাচাই
reshape-এ patch কাটে, transpose ভুলেএকটি patch আর সরাসরি কাটা অংশ equal কি না দেখা
পুরোনো argument বা বানানো ফাংশন (pretrained=True ইত্যাদি)নিজের version-এর docs; torchvision-এ এখন weights=
vit._process_input-এর মতো ব্যক্তিগত ফাংশনকে স্থায়ী API ধরে নেয়আন্ডারস্কোরের ফাংশন version-এ বদলাতে পারে; version লিখে রাখা
ছোট ডেটায় ViT-এর বড় দাবি, একটিমাত্র runএকই budget-এর CNN, কয়েকটি seed, একই split
Attention map দেখিয়ে বলে “model এখানে দেখে বুঝেছে”Occlusion দিয়ে মাপা; “বুঝেছে” কথাটি বাদ
model.eval() আর torch.no_grad() ভুলে যায়, বা ভুল transforms দেয়weights.transforms() ব্যবহার, ছাপিয়ে দেখা
প্রবন্ধের সংখ্যা বা উদ্ধৃতি বানিয়ে লেখেমূল প্রবন্ধ খুলে মেলানো

কোড চাওয়ার সময় পরীক্ষাগুলোও চাইতে বলুন:

আমি PyTorch-এ (version লিখে দিচ্ছি) ২৮ × ২৮ ধূসর ছবির জন্য একটি ছোট Vision Transformer বানাব, প্রায় ১.৫ লাখ parameter-এ। ১. প্রতিটি ধাপের পরে tensor-এর shape একটি টেবিলে দাও, আর token সংখ্যার হিসাব দেখাও। ২. Component অনুযায়ী parameter গুনে দাও, আর আমার হাতের হিসাবের সঙ্গে মেলানোর একটি test লেখো। ৩. Patch embedding-এর conv আর হাতে কাটা linear map একই ফল দেয় কি না, তার test দাও। ৪. প্রায় সমান parameter-এর একটি CNN baseline দাও, একই optimizer আর একই split-এ, অন্তত ৩টি seed. ৫. Attention map-কে ব্যাখ্যা বলে দাবি কোরো না; occlusion দিয়ে যাচাইয়ের কোডও দাও। ৬. যে API বা প্রবন্ধ নিয়ে নিশ্চিত নও, তা অনুমান বলে চিহ্নিত করো; কোনো সংখ্যা বানিয়ে লিখো না।

AI-এর লেখা ViT কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১Self-attention ছবিতে ২০২০-এর আগেও চেষ্টা হয়েছে। তাহলে ViT-এর প্রবন্ধে নতুনটা কী, আর কেন পুরো জায়গা জুড়ে এত আলোচনা?

ViT-এর আগেও ছবিতে attention ছিল। Non-local network (Wang ও সহকর্মী, ২০১৮) আর DETR (Carion ও সহকর্মী, ২০২০) CNN-এর সঙ্গে attention জুড়েছিল। Cordonnier ও সহকর্মীরা ২০২০ সালে ২ × ২ patch-এ self-attention দেখান, আর গাণিতিকভাবে দেখান self-attention যথেষ্ট head পেলে convolution-এর মতো আচরণ করতে পারে। তবে ২ × ২ patch-এ বড় ছবিতে T বিশাল, তাই তা ছোট ছবিতেই সীমিত ছিল।

ViT-এর আসল অবদান তাই কোনো নতুন গাণিতিক উপাদান নয়। এর সাহস ছিল প্রায় সব CNN-সুলভ কাঠামো ফেলে দেওয়া আর দেখানো যে বড় patch (১৬ × ১৬) যথেষ্ট, এবং যথেষ্ট ডেটা পেলে ফল প্রতিযোগিতামূলক। প্রবন্ধ ImageNet-21k (প্রায় ১.৪ কোটি ছবি) আর JFT-300M (প্রায় ৩০ কোটি ছবি) ব্যবহার করে। শেষেরটি গুগলের অভ্যন্তরীণ ডেটা, সবার জন্য খোলা নয়।

এর সঙ্গে ছিল একটি সরলতার আকর্ষণ। NLP-এর যন্ত্রপাতি, যেমন BERT-ধাঁচের class token, প্রায় বদল ছাড়াই ছবিতে চলে। ফলে একই কাঠামোয় পরে ছবি আর টেক্সট মিলিয়ে কাজ করা সহজ হয়। পরবর্তী গবেষণার বড় অংশ, যেমন MAE আর DINO, এই সরল কাঠামোকে ভিত্তি করে এগিয়েছে।

তবে একটি সতর্কতা: কোনো প্রবন্ধের প্রভাবকে শুধু তার ধারণা দিয়ে মাপলে ভুল হয়। তার ফল নির্ভর করেছিল বড় TPU হিসাব, বিশাল ডেটা আর লম্বা training-এর ওপর। Beyer-দের ২০২২ সালের কাজ দেখায়, recipe ঠিক করলে সরল ViT মাঝারি ডেটাতেও অনেক ভালো চলে। মানে “ViT-এর জন্য বিশাল ডেটা লাগেই” কথাটিও পুরোপুরি স্থায়ী নয়; সেটি কিছুটা সেই সময়ের recipe-এরও ফল ছিল।

মূল উপলব্ধি: বড় অগ্রগতি প্রায়ই নতুন উপাদান নয়, বরং পুরোনো উপাদানকে নতুন মাপের ডেটা আর হিসাব-ক্ষমতায় পরীক্ষা করার সাহস; আর তার সীমা বুঝতে পরের recipe-গুলোও পড়তে হয়।

প্র ০২Inductive bias কি ভালো না খারাপ? যে bias ছোট ডেটায় বাঁচায়, সে কি বড় ডেটায় আটকে দেয়?

Inductive bias হলো ডেটা দেখার আগেই ধরে নেওয়া অনুমান (পর্ব ৬)। “No free lunch” ফলাফলের সহজ ব্যাখ্যা হলো, সব সমস্যায় গড়ে কোনো learner অন্যটির চেয়ে ভালো নয়; লাভ আসে অনুমান সমস্যার সঙ্গে মিললে। তাই প্রশ্নটি “ভালো না খারাপ” নয়, প্রশ্ন “কতটা মিলছে”।

CNN-এর দুটি মূল অনুমান: কাছের pixel একসঙ্গে অর্থ তৈরি করে (locality), আর একই নকশা ছবির যেকোনো জায়গায় একই মানে রাখে (translation equivariance). সাধারণ ছবিতে এগুলো ভালোই মেলে, তাই অল্প ডেটায় CNN দ্রুত শেখে। কিন্তু অনুমান ভাঙলে দাম দিতে হয়। অংশ ৬-এর পরীক্ষায় patch-এর grid এলোমেলো করলে CNN ৯১.৮ থেকে ৯০.২%-এ নামে, ViT প্রায় একই থাকে। দামটি এখানে ছোট, কারণ patch-এর ভেতরের নকশা অক্ষত ছিল, আর CNN তবু এগিয়েই ছিল; কিন্তু দিকটি পরিষ্কার।

আরেকটি দিক হলো bias-এর স্পষ্টতা। ViT-এর কিছু bias তো আছেই: patch-এর grid একটি স্থির কাঠামো, E সব patch-এ একই (এটিও একধরনের weight sharing), আর MLP প্রতিটি token-এ একই। Dosovitskiy-রা নিজেই লিখেছেন “অনেক কম”, “নেই” নয়। আবার bias শুধু architecture-এ থাকে না; augmentation-ও bias, যেমন translation বা flip augmentation ডেটার মাধ্যমে equivariance শেখায়। DeiT-এর শক্ত augmentation কিছুটা এই পথেই কাঠামোর ঘাটতি পূরণ করে।

আর বড় ডেটায় কী হয়? ডেটা যথেষ্ট হলে অনুমান ডেটা থেকেই পাওয়া যায়, তখন কঠিন bias কম গুরুত্বপূর্ণ হতে পারে, আর নমনীয়তা কাজে লাগতে পারে। তবে এটিও নিশ্চিত নিয়ম নয়: ConvNeXt দেখায়, ভালো recipe-তে CNN বড় ডেটাতেও প্রতিযোগিতামূলক। শেষ পর্যন্ত এই বিতর্ক এখনো খোলা, আর বাস্তবে hybrid বা স্তরভিত্তিক model প্রায়ই দুই দিকের সুবিধা নেয়।

মূল উপলব্ধি: Bias হলো ডেটার বিকল্প: সঠিক অনুমান ডেটা বাঁচায়, ভুল অনুমান ক্ষতি করে; কতটা কাঠামো বাঁধবেন, তা নির্ভর করে আপনার কাছে কতটা ডেটা আর হিসাব-ক্ষমতা আছে।

প্র ০৩Attention map এত সুন্দর দেখায়। তাহলে চিকিৎসা বা ঋণের মতো গুরুতর কাজে একে ব্যাখ্যা হিসেবে দেখানো যায় না কেন?

একটি heatmap দেখলে মানুষের মন সহজেই কারণ খুঁজে নেয়। এক্স-রেতে গাঢ় দাগ রোগের জায়গায় পড়লে মনে হয় model সেখান দেখেই সিদ্ধান্ত নিয়েছে। কিন্তু attention weight বলে কোন token-এর value কত ভাগে মিশেছে, এটি বলে না সেই মিশ্রণ পরের স্তরে কীভাবে ব্যবহার হলো।

টেক্সটে Jain আর Wallace (২০১৯) দেখান, attention-এর সঙ্গে অন্য গুরুত্বের মাপের মিল অনেক সময় দুর্বল, আর ভিন্ন attention বিন্যাসে একই প্রায় ফল পাওয়া যেতে পারে। Wiegreffe আর Pinter (২০১৯) পাল্টা যুক্তি দেন: এটি পরীক্ষার ধরনের ওপর নির্ভর করে, attention-কে পুরো খারিজ করা যায় না। তাই বিতর্কটি এখনো একপক্ষে মীমাংসিত নয়।

ViT-এ সমস্যা আরও স্তরে। প্রতিটি block আগের স্তরের token-কে মিশিয়ে ফেলে, তাই শেষ block-এর “patch ৫” আর আসলে শুধু patch ৫ নয়, আগের সব মিশ্রণ। এই পর্বের run-এই দেখলেন: জাহাজের ছবিতে সবচেয়ে বেশি attention কোনায়, অথচ ১,০০০ ছবির মাপে মাঝখান মুছলেই ক্ষতি সবচেয়ে বেশি। উঁচু-attention-এর ৪৯ patch-এ attention-এর ৮৩.৯% থাকলেও ফল শুধু কয়েক পয়েন্ট বেশি পড়ে। এটি একটি ডেটায় একটি run, কিন্তু সতর্ক হওয়ার মতো যথেষ্ট।

গুরুতর কাজে তাই দরকার: occlusion বা feature attribution দিয়ে মাপা, sanity check (Adebayo ও সহকর্মী, ২০১৮: model-এর weight এলোমেলো করলেও saliency map প্রায় একই থাকলে সেটি বিশ্বাসযোগ্য নয়), আর ডাক্তারের মতো বিশেষজ্ঞের সঙ্গে যাচাই। ডেটার ভেতরের পক্ষপাতও দেখা দরকার; এক্স-রের কোনার হাসপাতালের চিহ্ন দেখে শেখা model-এর map সুন্দর হলেও সিদ্ধান্ত ভুল কারণে হতে পারে।

মূল উপলব্ধি: Attention map একটি পর্যবেক্ষণ, প্রমাণ নয়; একে ব্যাখ্যা বলতে হলে হস্তক্ষেপ করে মাপতে হবে, আর ঝুঁকি বেশি হলে বাইরের যাচাই লাগবে।

প্র ০৪বাজারঘরে ৫,০০০টি পণ্যের ছবি আছে, ২০টি শ্রেণি। আপনি ViT নেবেন, CNN, নাকি অন্য কিছু? সিদ্ধান্তটি কীভাবে নেবেন?

প্রথম নিয়ম: scratch থেকে ViT প্রায় বাদ। এই পর্বের ছোট run-এ অল্প ডেটায় CNN-এর এগিয়ে থাকা, আর ViT-এর মূল প্রবন্ধের বড় ডেটার শর্ত, দুটিই একই দিকে ইঙ্গিত করে। ৫,০০০টি ছবিতে প্রথম প্রশ্ন “কোন architecture” নয়, প্রশ্ন হলো “কোন pretrained backbone”, ঠিক পর্ব ২৩-এর মতো।

এরপর সীমাগুলো লিখুন। মোবাইলে বা সস্তা CPU সার্ভারে চলবে? তাহলে resnet18 বা MobileNet-ধাঁচের ছোট model আগে; ৮.৬ কোটি parameter-এর model-এর ফাইল প্রায় ৩৩০ MB, আর এই পর্বের run-এ feature বের করতে সময়ও প্রায় আড়াই গুণ। GPU সার্ভার থাকলে আর accuracy বেশি জরুরি হলে vit_b_16 বা তার মতো বড় backbone-কে পাশে রাখা যায়। Licence-ও দেখতে হবে; pretrained weight-এর শর্ত সবসময় ব্যবসায়িক ব্যবহারে এক নয়।

তারপর পরীক্ষা, সবচেয়ে সস্তাটি দিয়ে। Feature একবার বের করে linear head: কয়েক মিনিটের কাজ, ফলে দুটি backbone একই split-এ তুলনা হয়ে যায়। দরকার হলে শেষের block fine-tune. সঙ্গে দরকার শ্রেণিভিত্তিক error দেখা: শাড়ি আর ওড়না গুলিয়ে যাচ্ছে কি না, ছবির পটভূমি স্টুডিও আর মোবাইলে আলাদা কি না। ডেটার মান প্রায়ই architecture-এর চেয়ে বেশি প্রভাব ফেলে; ভুল লেবেল ঠিক করা, নতুন ছবি জোগাড় করা সস্তা উন্নতি।

সবশেষে শুধু accuracy নয়, মোট খরচ। ১% উন্নতির জন্য প্রতি ছবিতে সময় তিন গুণ, সার্ভার খরচ বেশি, এই বিনিময় ব্যবসার জন্য লাভজনক কি না, সেটি একটি পরিমাপযোগ্য প্রশ্ন। মনে রাখা দরকার, এখানকার সংখ্যাগুলো এক কম্পিউটারে একবারের মাপ; নিজের ডেটায় আবার মাপতে হবে।

মূল উপলব্ধি: ছোট ডেটায় সিদ্ধান্তের একক “ViT বনাম CNN” নয়, “কোন pretrained feature, কত খরচে”; সবচেয়ে সস্তা পরীক্ষা আগে, আর শেষ কথা বলবে নিজের ডেটায় মাপা ফল।

প্র ০৫Patch কি শব্দের মতোই? ছবির “বাক্য” আর ভাষার বাক্যের মধ্যে কোন পার্থক্য ViT-এর নকশাকে বদলে দিয়েছে?

“১৬ × ১৬ শব্দ” শিরোনামটি মনে থাকার মতো, কিন্তু উপমার সীমা আছে। প্রথম পার্থক্য: শব্দের একটি নির্দিষ্ট অভিধান আছে, আর প্রতিটি token একটি পূর্ণসংখ্যা। Patch-এর কোনো অভিধান নেই, এটি ৭৬৮টি অবিচ্ছিন্ন সংখ্যা। তাই embedding table নয়, একটি linear map লাগে। আর “পরের token অনুমান” ছবিতে স্বাভাবিক কাজ নয়, তাই MAE-এর মতো পদ্ধতি pixel পুনর্গঠন করে।

দ্বিতীয়: শব্দের সীমানা ভাষার অংশ; patch-এর সীমানা কৃত্রিম, যেখানে কাঁচি পড়েছে সেখানেই। একটি বিড়ালের চোখ দুই patch-এ ভাগ হয়ে যেতে পারে। ছবি কয়েক pixel সরলে patch-গুলোর সংখ্যা সম্পূর্ণ বদলে যায়। এখান থেকেই ViT-এর translation-এর দুর্বলতা, যা অংশ ৬-এ মাপলেন, আর overlap বা ছোট convolution-ভিত্তিক শুরুর পক্ষে যুক্তি।

তৃতীয়: ভাষায় ক্রম মূলত এক অক্ষে; ছবিতে দুই অক্ষে। অথচ মূল ViT-এ 1-D ক্রমের শেখা position-ই যথেষ্ট ছিল, কারণ শেখা vector নিজেই ২-D সম্পর্ক শিখে নিতে পারে; প্রবন্ধে শেখা position-এর মিল দেখে সারি-কলামের গঠন বোঝা যায়। কিন্তু আকার বদলালে শেখা position-কে interpolate করতে হয়, যা শব্দের ক্ষেত্রে স্বাভাবিক অর্থে হয় না। আর পরের কাজগুলো 2-D sin-cos-এ ফিরে গেছে।

চতুর্থ: মাপ। ভাষায় একটি শব্দের অর্থ প্রায় এক মাপের; ছবিতে একই বস্তু ২০ pixel বা ২০০ pixel হতে পারে। ViT-এর এক মাপের patch এতে দুর্বল; তাই Swin-এর মতো স্তরভিত্তিক নকশা CNN-এর pyramid ফিরিয়ে এনেছে, বিশেষ করে detection আর segmentation-এর জন্য (পর্ব ২৫)। সবশেষে দুই কাজের ভিন্ন খরচও মনে রাখুন: টেক্সটে T ঠিক করে ব্যবহারকারী, ছবিতে তা ঠিক করে patch আর ছবির আকার।

মূল উপলব্ধি: ভালো উপমা শুরুর পথ দেখায়, কিন্তু উন্নতি আসে তার ভাঙা জায়গাগুলো থেকে: patch-এর কৃত্রিম সীমানা, দুই অক্ষ আর ভিন্ন মাপই পরের ViT-গুলোর নকশা ঠিক করেছে।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান।

  1. পূর্ণ numerical: ৩৮৪ × ৩৮৪ RGB ছবি, P = ১৬, D = ১০২৪, class token-সহ। (ক) Token T কত? (খ) Patch embedding-এর parameter (bias-সহ)? (গ) Position-এর সংখ্যা কত? (ঘ) ২২৪-এর তুলনায় attention-এর জোড়া কত গুণ? (ঙ) ২২৪ × ২২৪-এ ৭৬৮ মাত্রার ViT-তে P = ৩২ হলে T আর patch embedding-এর parameter?

    (ক) ৩৮৪ / ১৬ = ২৪; ২৪২ = ৫৭৬; T = ৫৭৭. (খ) Input ১৬ × ১৬ × ৩ = ৭৬৮; ৭৬৮ × ১০২৪ + ১০২৪ = ৭,৮৭,৪৫৬. (গ) ৫৭৭ × ১০২৪ = ৫,৯০,৮৪৮. (ঘ) ৫৭৭২ / ১৯৭২ = ৩,৩২,৯২৯ / ৩৮,৮০৯ ≈ ৮.৬ গুণ। (ঙ) ২২৪ / ৩২ = ৭; T = ৭২ + ১ = ৫০; input ৩২ × ৩২ × ৩ = ৩,০৭২; ৩,০৭২ × ৭৬৮ + ৭৬৮ = ২৩,৬০,০৬৪. লক্ষ করুন, patch বড় হলে token কমে কিন্তু E-এর parameter বাড়ে।

  2. Output অনুমান: নিচের কোড কী ছাপাবে? import numpy as np; x = np.arange(16).reshape(4, 4); g = x.reshape(2, 2, 2, 2).transpose(0, 2, 1, 3); print(g[0, 1].ravel()); print(x.reshape(4, 4)[:2, 2:].ravel()). আর transpose ছাড়া x.reshape(2, 2, 2, 2)[0, 1].ravel() কী দিত?

    দুটি print-ই [2 3 6 7]: ওপর-ডানের patch, সারি ০–১, কলাম ২–৩. transpose ছাড়া x.reshape(2, 2, 2, 2)[0, 1] হলো মূল array-এর দ্বিতীয় সারি: [4 5 6 7], যা কোনো patch নয়। Shape দুই ক্ষেত্রেই (4,), তাই error আসে না। এজন্যই সংখ্যা মিলিয়ে দেখতে হয়।

  3. নকশা ও সমালোচনা: একজন সহকর্মী লিখেছেন: “৩,০০০ পণ্যের ছবিতে scratch থেকে ১২ block-এর ViT, ২০ epoch, একটি seed; test accuracy ৬৮%; CNN baseline করিনি; attention map দেখে বোঝা গেল model পণ্যের দিকে তাকায়, তাই ViT-ই যথেষ্ট।” কমপক্ষে চারটি সমস্যা লিখে ঠিক করুন।

    (১) Scratch থেকে বড় ViT, অল্প ডেটায়: এই আকারে pretrained feature (অংশ ৮) আগে; শেখাতেই হলে ছোট model আর শক্ত augmentation. (২) Baseline নেই: ৬৮% ভালো না খারাপ বলা যায় না; একই split-এ CNN আর pretrained resnet18 দরকার। (৩) এক seed: ফলটি ভাগ্যের হতে পারে; অন্তত ৩টি seed, গড় আর ওঠানামা। (৪) Attention-কে ব্যাখ্যা ধরা: “তাকায়” আর “সেখান দেখে সিদ্ধান্ত” এক নয় (অংশ ৯); occlusion দিয়ে মাপুন। আরও: test দেখে সিদ্ধান্ত নিলে validation আলাদা নেই; learning-rate warmup ও schedule (পর্ব ১৫) আছে কি না দেখা দরকার।

  4. কোডিং: অংশ ৪-এর NumPy ব্লক বদলে class token ফেলে শুধু mean pooling করুন, আর position শুধু patch-এ যোগ করুন। (ক) কোন কোন shape বদলাবে? (খ) Parameter কত কমবে, D = ১৬-এ? (গ) Position ছাড়া patch এলোমেলো করলে mean কি বদলাবে?

    (ক) z (2, 17, 16) থেকে (2, 16, 16); attention (2, 2, 17, 17) থেকে (2, 2, 16, 16); pos (1, 17, 16) থেকে (1, 16, 16). (খ) Class token-এর ১৬টি আর position-এর ১৬টি, মোট ৩২টি কম। (গ) না: attention permutation-equivariant, প্রতিটি token-এর output শুধু একইভাবে সরে যায়, আর গড়ে ক্রমের কোনো প্রভাব নেই; পার্থক্য শুধু float-এর গোলের ঘরে। কোড: z = t + pos[:, 1:] আর m = z.mean(1).

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমশ্রেণিবিভাগের একটি ViT-এর forward pass-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ছবিকে P × P patch-এ কেটে flatten করা (বা stride = P convolution)
  2. Linear projection দিয়ে প্রতিটি patch-কে D মাত্রায় নেওয়া
  3. সামনে class token জোড়া
  4. সব token-এ positional embedding যোগ করা
  5. Transformer block-গুলো চালানো
  6. Class token-কে নিয়ে head দিয়ে শ্রেণির score বের করা

২২৪ × ২২৪ RGB ছবি আর ১৬ × ১৬ patch-এ class token-সহ একটি ViT-এ মোট token কত?

  • ১৪
  • ১৯৬
  • ১৯৭
  • ৭৬৮

nn.Conv2d(3, 768, kernel_size=16, stride=16)-এর patch embedding-এ bias-সহ মোট কতটি parameter?

  • ৫,৮৯,৮২৪
  • ৫,৯০,৫৯২
  • ১,৫০,৫২৮
  • ১২,২৮৮

একটি convolution কখন “কেটে flatten করে linear map” পদ্ধতির সমান হয়?

  • Stride = ১ আর padding থাকলে
  • Kernel ১ × ১ হলে
  • প্রতি স্তরের পরে pooling থাকলে
  • Stride = kernel = P আর padding নেই

Positional embedding ছাড়া একটি ViT-এ mean pooling করলে, ছবির patch-গুলো এলোমেলো করলে শেষের vector-এ কী হবে?

  • বদলাবে না
  • অনেক বদলাবে
  • Attention-এর shape ছোট হয়ে যাবে
  • Runtime error হবে

২২৪-এ শেখানো একটি ViT-B/16-কে ৩৮৪ × ৩৮৪ ছবিতে চালাতে কোন weight-এ মূলত বদল দরকার?

  • Patch embedding E নতুন করে শেখা
  • প্রতিটি block-এর আকার বাড়ানো
  • শুধু positional embedding-কে 2-D interpolate করা
  • Class token-এর সংখ্যা বাড়ানো

কাঠামোগত দিক থেকে CNN-এর তুলনায় একটি সাধারণ ViT-এর বৈশিষ্ট্য কোনটি?

  • অল্প ডেটায় সবসময় বেশি accuracy দেয়
  • ছবি-নির্দিষ্ট inductive bias অনেক কম
  • এর attention-এর খরচ T-এর সঙ্গে linear
  • এর attention map সিদ্ধান্তের নিশ্চিত ব্যাখ্যা দেয়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Shape: N = (H/P)(W/P), একটি patch P·P·C সংখ্যা, T = N + 1. ২২৪, ১৬ মানে ১৯৭. Attention-এর খরচ T2.
  2. Patch embedding: stride = kernel = P convolution আর কেটে linear map একই; flatten-এর ক্রম ভুল হলে error ছাড়াই ফল ভুল।
  3. Class token, mean, position: Class token আর mean কাছাকাছি। Position ছাড়া ছবি patch-এর থলে; আকার বদলালে 2-D interpolation.
  4. ডেটা: কাঠামোয় bias কম, তাই ডেটা আর augmentation বেশি লাগে। অল্প ডেটায় CNN বা pretrained আগে; একই budget, একই split, কয়েক seed-এ তুলনা।
  5. Attention: পড়া যায়, কিন্তু ব্যাখ্যা নয়। গুরুত্ব জানতে occlusion-এর মতো হস্তক্ষেপ করে মাপুন।

পরের পর্বে: পর্ব ৩৯ (NLP-এর পথ — টেক্সট থেকে task): মডিউল ৬-এর Transformer এবার টেক্সটের দিকে ফিরবে; কাঁচা টেক্সট কীভাবে token আর task হয়, তা দিয়ে শুরু।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান