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

Multimodal model — CLIP, image captioning আর vision-language model

Multimodal Models — CLIP, Image Captioning and Vision-Language Models

Taught in Bangla, this lesson turns images into patch tokens, explains CLIP's two encoders and symmetric contrastive loss, and runs zero-shot classification with prompts. A real CPU run of CLIP ViT-B/32 on Fashion-MNIST compares English and Bangla prompts, a template ensemble and a linear probe. It then covers captioners and vision-language models built from a vision encoder, projector and LLM, measuring counting, text reading and object hallucination in SmolVLM-256M, with in-page Python.

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

  • ছবি কীভাবে patch token হয়, আর API-র নথিতে ছবির token কীভাবে গোনা হয়
  • CLIP-এর দুটি encoder আর contrastive loss, খেলনা ডেটায় নিজে শেখানোসহ
  • Prompt দিয়ে zero-shot classification হাতে, ইংরেজি বনাম বাংলা prompt আর linear probe-এর সত্যিকারের তুলনা
  • Image captioning আর vision-language model-এর গঠন: vision encoder, projector আর LLM
  • গণনা, ছোট লেখা, বাংলা লেখা আর object hallucination: একটি ছোট VLM-এ মাপা

এ পর্যন্ত model হয় ছবি পড়েছে (পর্ব ২০, পর্ব ৩৮), নয়তো লেখা (মডিউল ৫ থেকে ৯)। বাস্তবের প্রশ্ন প্রায়ই দুটি মিলিয়ে: “এই জুতার ছবির সঙ্গে কোন বিবরণ মেলে?”, “রসিদের ছবিতে মোট কত টাকা?” এই পর্বে তিন ধরনের multimodal model দেখবেন: ছবি আর লেখাকে একই জায়গায় আনা CLIP, ছবি দেখে বাক্য লেখা captioner, আর ছবি দেখে প্রশ্নের উত্তর দেওয়া vision-language model. চারটি ব্লক ব্রাউজারের Python-এ চলে (প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে)। CLIP আর SmolVLM-এর সংখ্যা ডেস্কটপের সত্যিকারের run থেকে, CPU-তে, Python 3.14-এ।

১ · ছবি কীভাবে token হয় — patch, projection আর CLS

দর্জির দোকানে একটি বড় কাপড়ের নকশা মাপতে গেলে দর্জি পুরোটা একবারে মাপেন না; খড়ি দিয়ে সমান বর্গে দাগ কেটে প্রতিটি ঘরের রং আর নকশা খাতায় একটি লাইনে লেখেন। খাতার প্রতিটি লাইন যেন একটি “শব্দ”। Transformer-এর কাছে ছবিও ঠিক এমন: সমান মাপের ঘরে কাটা, প্রতিটি ঘর একটি token.

LLM লেখা পড়ে token-এর sequence হিসেবে (পর্ব ৪০)। ছবিকে একই যন্ত্রে ঢোকাতে হলে ছবিকেও token-এর sequence বানাতে হয়। পর্ব ৩৮-এ ViT-এর পুরো হিসাব দেখেছেন; এখানে শুধু সারাংশ, কারণ বাকি পর্বের প্রতিটি model এই ধাপ দিয়ে শুরু। H × W pixel-এর ছবিকে P × P মাপের patch-এ কাটলে patch-এর সংখ্যা

N = (H ÷ P) × (W ÷ P),   প্রতিটি patch-এ P × P × 3টি সংখ্যা

প্রতিটি patch-কে একটি সারিতে সাজিয়ে একই matrix W দিয়ে গুণ করলে d-মাত্রার vector আসে; এটিই patch embedding. সামনে একটি শেখা CLS token বসে, আর প্রতিটি অবস্থানে একটি শেখা position embedding যোগ হয়, যাতে model জানে কোন patch কোথায় ছিল।

ছবি 224 × 224 × 3 · patch P = 32 · মডেলের প্রস্থ d = 768
ধাপ
shape
মানে
  1. Grid. ২২৪ ÷ ৩২ = ৭, তাই ৭ × ৭ = ৪৯টি patch. Patch ১৬ হলে ১৯৬টি, অর্থাৎ ৪ গুণ token আর attention-এর খরচ প্রায় ১৬ গুণ।
  2. একটি patch. ৩২ × ৩২ pixel, প্রতিটিতে ৩টি রং: ৩০৭২টি সংখ্যা। ৪৯টি patch একটি (৪৯, ৩০৭২) matrix.
  3. Projection. সব patch-এ একই W (৩০৭২ × ৭৬৮)। আমাদের run-এ model-এর patch_embedding weight-এর shape (768, 3, 32, 32), bias নেই: ঠিক ২৩,৫৯,২৯৬টি সংখ্যা। Stride ৩২-এর convolution আর এই গুণ একই কাজ।
  4. CLS. সামনে একটি শেখা token বসে, sequence হয় ৫০টি। Transformer-এর শেষে এই token-এর vector পুরো ছবির সারাংশ।
  5. Position. (৫০, ৭৬৮) মাপের শেখা position embedding যোগ হয়; নইলে patch-এর ক্রম এলোমেলো করলেও model একই ফল দিত।
  6. শেষ vector. ১২টি Transformer block-এর পর CLS-এর ৭৬৮-মাত্রার output একটি projection (৭৬৮ → ৫১২) দিয়ে ৫১২ মাত্রায় যায়। লেখার encoder-ও ৫১২ মাত্রায় শেষ হয়, তাই দুটি তুলনা করা যায়।

নিচের ব্লক এই হিসাব NumPy-তে করে: patch কাটা ঠিক জায়গা থেকে হচ্ছে কি না যাচাই, projection আর CLS-এর shape, একটি vision-language model যে pixel shuffle দিয়ে token কমায় তার shape, আর API-র নথিতে দেওয়া token-গণনার সূত্র।

import math
import numpy as np

for h, p, name in [(224, 32, "CLIP ViT-B/32"), (224, 16, "ViT-B/16"), (336, 14, "ViT-L/14, 336px"), (512, 16, "SigLIP-B/16, 512px")]:
    g = h // p
    print(f"{name:18s} {h}x{h}, patch {p:2d}: {g}x{g} = {g * g:4d} patches, {p * p * 3:4d} numbers each")

rng = np.random.default_rng(0); P = 32
img = rng.random((224, 224, 3), dtype=np.float32)
x = img.reshape(7, P, 7, P, 3).transpose(0, 2, 1, 3, 4).reshape(49, P * P * 3)   # cut into patches
print("patch 0 = top-left block:", np.array_equal(x[0], img[:32, :32].reshape(-1)),
      "| patch 8 = row 1, col 1:", np.array_equal(x[8], img[32:64, 32:64].reshape(-1)))
W = rng.normal(0, 0.02, (P * P * 3, 768)).astype(np.float32)   # one shared projection
tok = x @ W
cls = rng.normal(0, 0.02, (1, 768)).astype(np.float32)         # learned CLS token
pos = rng.normal(0, 0.02, (50, 768)).astype(np.float32)        # learned position embedding
seq = np.vstack([cls, tok]) + pos
print("patches", x.shape, "@ W", W.shape, "->", tok.shape, "-> + CLS + pos ->", seq.shape)
print(f"projection weights {W.size:,} (no bias, as in CLIP)")

grid = rng.random((32, 32, 768), dtype=np.float32)              # SigLIP output for one 512x512 tile
r = 4                                                            # pixel shuffle: merge 4x4 neighbours
s = grid.reshape(8, r, 8, r, 768).transpose(0, 2, 1, 3, 4).reshape(64, r * r * 768)
print("pixel shuffle r=4:", grid.shape, "->", s.shape, "-> 64 visual tokens for the LLM")

for w, h in [(1000, 1000), (1920, 1080)]:
    print(f"{w}x{h}: ceil(w/28)*ceil(h/28) = {math.ceil(w / 28) * math.ceil(h / 28)}, "
          f"ceil(w/32)*ceil(h/32) = {math.ceil(w / 32) * math.ceil(h / 32)}  (before any resize cap)")

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

CLIP ViT-B/32      224x224, patch 32: 7x7 =   49 patches, 3072 numbers each
ViT-B/16           224x224, patch 16: 14x14 =  196 patches,  768 numbers each
ViT-L/14, 336px    336x336, patch 14: 24x24 =  576 patches,  588 numbers each
SigLIP-B/16, 512px 512x512, patch 16: 32x32 = 1024 patches,  768 numbers each
patch 0 = top-left block: True | patch 8 = row 1, col 1: True
patches (49, 3072) @ W (3072, 768) -> (49, 768) -> + CLS + pos -> (50, 768)
projection weights 2,359,296 (no bias, as in CLIP)
pixel shuffle r=4: (32, 32, 768) -> (64, 12288) -> 64 visual tokens for the LLM
1000x1000: ceil(w/28)*ceil(h/28) = 1296, ceil(w/32)*ceil(h/32) = 1024  (before any resize cap)
1920x1080: ceil(w/28)*ceil(h/28) = 2691, ceil(w/32)*ceil(h/32) = 2040  (before any resize cap)

প্রথম চার লাইন দেখায়, patch ছোট বা ছবি বড় হলে token দ্রুত বাড়ে: ৪৯ থেকে ১০২৪. পঞ্চম লাইন যাচাই করে reshape আর transpose-এর পর patch ০ সত্যিই বাঁ-ওপরের ৩২ × ৩২ ঘর, আর patch ৮ দ্বিতীয় সারির দ্বিতীয় ঘর (৭ × ১ + ১); transpose ভুলে গেলে ঘরগুলো ফালি ফালি কাটা পড়ত। তারপর stepper-এর shape (৫০, ৭৬৮) আর ২৩,৫৯,২৯৬টি weight. Pixel shuffle ৪ × ৪ প্রতিবেশী patch-কে একটি token-এ জোড়ে: ১০২৪টি থেকে ৬৪টি, প্রতিটি ১৬ গুণ চওড়া, পরে linear layer LLM-এর মাপে আনে; SmolVLM-এর card-এর “৫১২ × ৫১২-এ ৬৪টি visual token” ঠিক এই হিসাব। শেষ দুই লাইন নথির সূত্র: ১০০০ × ১০০০ ছবিতে ২৮-pixel patch-এ ১২৯৬টি, যা Claude-এর নথির সারণির সংখ্যার সঙ্গে মেলে; বড় ছবিকে প্রতিটি API আগে resize করে সীমার মধ্যে আনে, তাই আসল সংখ্যা কম হতে পারে।

২ · CLIP — দুটি encoder, একটি জায়গা, contrastive শেখা

CLIP (Contrastive Language–Image Pre-training; Radford ও সহকর্মীরা, OpenAI, ২০২১) দুটি আলাদা encoder রাখে: একটি ছবির জন্য (ViT বা ResNet), একটি লেখার জন্য (Transformer). দুটির শেষে একটি করে linear projection, যাতে দুই দিকের vector একই মাপের (আমাদের model-এ ৫১২) আর দৈর্ঘ্য ১-এ normalise করা। প্রবন্ধ অনুযায়ী শেখানো হয় ইন্টারনেট থেকে সংগ্রহ করা প্রায় ৪০ কোটি ছবি-লেখা জোড়ায়; আমরা যে OpenCLIP সংস্করণ চালাব, তার card বলে LAION-2B-এর ইংরেজি অংশে।

শেখার নিয়মটি contrastive: একটি batch-এ N জোড়া থাকলে N × N-টি cosine হয়। কর্ণের N-টি মিলে যাওয়া জোড়া, বাকি N2 − N-টি “negative”. লক্ষ্য কর্ণের মান বাড়ানো আর বাকিগুলো কমানো। ঠিক এই ধারণা বাক্যের embedding-এ পর্ব ৬৭-এ InfoNCE আর in-batch negative নামে দেখেছেন; CLIP একই loss দুই দিকে চালায়।

CLIP-এর গঠন: দুটি encoder, একটি মিলের ছক

Image encoder + text encoder → same 512-d space → N × N cosine matrix, diagonal = matched pairs

CLIP dual encoder and the N by N similarity matrix N ছবি 224 × 224 N caption ≤ 77 token Image ViT-B/32 Text Transformer I₁ … I_N T₁ … T_N 512-d, ‖·‖ = 1 cos(Iᵢ, Tⱼ) × s কর্ণ = মিল বাকি = negative সারি ও কলামে cross-entropy
ছবি ৮২.১ — একটি batch-এ N জোড়া ছবি আর caption. দুই encoder আলাদা, কিন্তু শেষে একই মাপের vector দেয়। ছকের প্রতিটি সারিতে “এই ছবির caption কোনটি”, প্রতিটি কলামে “এই caption-এর ছবি কোনটি”, দুটিই একটি N-বিকল্পের classification. s হলো শেখা logit scale (১/temperature).

সূত্রে, normalise করা ছবির vector Ii আর লেখার vector Tj-এর জন্য logit ℓij = s · (Ii · Tj), যেখানে s হলো logit scale, অর্থাৎ ১/temperature. তারপর

L = ½ [ −(1/N) ∑i log softmaxj(ℓij)j=i  −  (1/N) ∑j log softmaxi(ℓij)i=j ]

প্রথম অংশ প্রতিটি সারিতে (ছবি → লেখা), দ্বিতীয়টি প্রতিটি কলামে (লেখা → ছবি) সাধারণ cross-entropy, যার সঠিক “class” কর্ণের ঘরটি (পর্ব ১৩)। এলোমেলো শুরুতে loss প্রায় log N, কারণ N-টির মধ্যে আন্দাজ। প্রবন্ধে s নিজেই শেখা হয়: শুরু ১/০.০৭ ≈ ১৪.৩ থেকে, আর ১০০-এর বেশি হতে দেওয়া হয় না। আমাদের model-এর config-এ শুরুর মান ln s = ২.৬৫৯২ লেখা, যা ln(১/০.০৭); শেখার পর logit_scale.exp() ছাপে 100.0.

কেন বড় batch? প্রতিটি সারিতে negative যত বেশি, কাজ তত কঠিন আর শেখা তত সূক্ষ্ম; প্রবন্ধে batch ছিল ৩২,৭৬৮. পরে SigLIP (Zhai ও সহকর্মীরা, ২০২৩) softmax-এর বদলে প্রতিটি ঘরে আলাদা sigmoid loss ব্যবহার করে, যাতে পুরো সারির যোগফল লাগে না; নিচে যে SmolVLM চালাব, তার vision encoder এই পরিবারের। নিচের ব্লক ছয় জোড়া খেলনা ডেটায় দুটি ছোট linear encoder-কে এই loss দিয়ে শেখায়, হাতে লেখা gradient দিয়ে।

import numpy as np

def softmax(L, ax):
    e = np.exp(L - L.max(ax, keepdims=True)); return e / e.sum(ax, keepdims=True)

def clip_loss(A, B, s):                      # symmetric InfoNCE, diagonal = matched pair
    L = s * A @ B.T; N = len(A); I = np.eye(N)
    Pi, Pt = softmax(L, 1), softmax(L, 0)    # image->text (rows), text->image (columns)
    loss = -(np.log(np.diag(Pi)).mean() + np.log(np.diag(Pt)).mean()) / 2
    G = ((Pi - I) + (Pt - I)) / (2 * N)      # dloss/dL
    return loss, L, G

rng = np.random.default_rng(42); N = 6
z = rng.normal(size=(N, 3))                  # hidden "meaning" shared by a pair
X = z @ rng.normal(size=(3, 5)) + 0.1 * rng.normal(size=(N, 5))   # image features
Y = z @ rng.normal(size=(3, 4)) + 0.1 * rng.normal(size=(N, 4))   # text features
Wi = 0.1 * rng.normal(size=(5, 3)); Wt = 0.1 * rng.normal(size=(4, 3))
s = 2.0
print(f"chance level log(N) = {np.log(N):.3f}")
for step in range(301):
    A, B = X @ Wi, Y @ Wt
    loss, L, G = clip_loss(A, B, s)
    if step in (0, 10, 50, 300):
        acc = (L.argmax(1) == np.arange(N)).mean()
        print(f"step {step:3d}  loss {loss:.3f}  image->text top-1 {acc:.2f}")
    dA, dB = s * G @ B, s * G.T @ A
    Wi -= 0.1 * X.T @ dA; Wt -= 0.1 * Y.T @ dB
np.set_printoptions(precision=1, suppress=True)
print("similarity matrix after training (rows = images):")
print(L)

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

chance level log(N) = 1.792
step   0  loss 1.802  image->text top-1 0.00
step  10  loss 0.780  image->text top-1 0.67
step  50  loss 0.513  image->text top-1 0.67
step 300  loss 0.323  image->text top-1 0.83
similarity matrix after training (rows = images):
[[ 6.8 -2.2  1.1  1.9 -0.4 -6.3]
 [-0.3 13.1  0.8 -8.2 -4.4  0. ]
 [ 0.6  0.6  0.1 -0.4 -0.1 -0.8]
 [ 2.9 -8.6 -0.2  6.9  0.6  0.6]
 [-3.4 -4.3 -0.8  0.9  4.3 -0.6]
 [-2.9  3.5 -0.5 -0.1 -4.2  8.3]]

শুরুতে loss ১.৮০২, প্রায় log ৬ = ১.৭৯২: weight ছোট বলে সব logit প্রায় সমান, model আন্দাজ করছে। ৩০০ ধাপে loss ০.৩২৩, আর ছয়টি ছবির পাঁচটিতে নিজের জোড়ার লেখাই সবচেয়ে কাছে। ছকের কর্ণে বড় মান (৬.৮, ১৩.১, ৬.৯, ৪.৩, ৮.৩), বাইরে ছোট বা ঋণাত্মক: ঠিক ছবি ৮২.১-এর ছক। তৃতীয় সারিটি ব্যতিক্রম, সব মান শূন্যের কাছে, ছবিটি এখনো কারও সঙ্গে স্পষ্টভাবে মেলেনি। Gradient-এর সূত্র সরল: ছকের ওপর ∂L/∂ℓ = (সারির softmax − I + কলামের softmax − I) ÷ ২N, অর্থাৎ প্রতিটি ঘরে “যা ভাবা হলো বিয়োগ যা হওয়া উচিত”। আসল CLIP vector-কে normalise করে আর s নিজে শেখে; খেলনায় দুটিই বাদ, যাতে gradient ছোট থাকে।

৩ · Zero-shot classification — label নয়, prompt

CLIP-এর সবচেয়ে চমকপ্রদ ব্যবহার: কোনো label-যুক্ত ছবি ছাড়াই classifier. বাজারঘরের নতুন একটি পণ্য-বিভাগ “স্যান্ডেল, কেডস না বুট” আলাদা করতে চাইলে প্রতিটি class-এর নাম একটি বাক্যে বসান (“a photo of a sneaker.”), লেখার encoder দিয়ে তিনটি vector নিন। এই vector-গুলোই classifier-এর weight. নতুন ছবির vector-এর সঙ্গে cosine নিয়ে s দিয়ে গুণ, তারপর softmax. একে zero-shot বলে, কারণ এই class-গুলোর একটি উদাহরণও দেখানো হয়নি।

Fashion-MNIST test ছবি #8108 (sneaker) · prompt “a photo of a {sneaker / sandal / ankle boot}.” · s = 100
ধাপ
হিসাব
মানে
  1. Embedding. Fashion-MNIST test সেটের একটি sneaker ছবির ৫১২-মাত্রার vector আর তিনটি prompt-এর vector, সবগুলোর দৈর্ঘ্য ১. Model দুটি encoder-এর শেষে normalise করে।
  2. Cosine. দৈর্ঘ্য ১ বলে dot product-ই cosine: sneaker ০.১৯৮৪, sandal ০.১৬১৯, ankle boot ০.১২৬২. ঠিক উত্তর সবচেয়ে বেশি, কিন্তু ব্যবধান মাত্র ০.০৩৬৫.
  3. × s. শেখা logit scale s = ১০০ দিয়ে গুণ: ১৯.৮৪, ১৬.১৯, ১২.৬২. এখন পার্থক্য ৩.৬৫ আর ৭.২২, যা softmax-এর কাছে বড়।
  4. exp. সবচেয়ে বড় logit (১৯.৮৪) বাদ দিয়ে exp নেওয়া হয়, ফল একই কিন্তু সংখ্যা বিশাল হয় না: ১, e−3.65 ≈ ০.০২৬, e−7.22 ≈ ০.০০০৭৩; যোগফল ১.০২৬৭২.
  5. Softmax. প্রতিটিকে যোগফল দিয়ে ভাগ: ০.৯৭৪০, ০.০২৫৩, ০.০০০৭. Run-এর আউটপুটে ০.৯৭৩৯ আর ০.০২৫৪, কারণ সেখানে চার দশমিকে কাটার আগের cosine. s = ১০ হলে একই ছবিতে ০.৪৫৯, ০.৩১৮, ০.২২৩: জয়ী একই, আত্মবিশ্বাস অনেক কম।

দুটি জিনিস এখানেই চোখে পড়ে। এক, cosine-গুলো খুব কাছাকাছি (০.২-এর আশপাশে); s = ১০০ না থাকলে softmax প্রায় সমান ভাগ দিত। দুই, ফলাফল prompt-এর শব্দের ওপর নির্ভর করে, তাই CLIP প্রবন্ধ একাধিক template-এর embedding-এর গড় নেয়, যাকে prompt ensemble বলে (প্রবন্ধে ImageNet-এ ৮০টি template). নিচের ব্লক stepper-এর একই সত্যিকারের embedding (৫১২ মাত্রা, পাতায় ডেটা হিসেবে রাখা) থেকে হিসাব করে, আর s বদলালে softmax কীভাবে বদলায় তা দেখায়।

import numpy as np
import matplotlib.pyplot as plt

E = {}
for line in open("clip_emb.txt"):                  # real CLIP embeddings x 10000, 512 numbers each
    name, *vals = line.split()
    E[name] = np.array(vals, dtype=float) / 10000
img = E["image"]; names = ["sneaker", "sandal", "ankle_boot"]
T = np.stack([E[n] for n in names])
print("vector length:", img.shape, "norms", np.round(np.linalg.norm(T, axis=1), 4))
cos = T @ img / (np.linalg.norm(T, axis=1) * np.linalg.norm(img))
print("cosine:", ", ".join(f"{n} {c:.4f}" for n, c in zip(names, cos)))
np.set_printoptions(suppress=True)

def softmax(z):
    e = np.exp(z - z.max())
    return e / e.sum()

for s in [1, 10, 100]:                            # s = logit scale = 1 / temperature
    print(f"s = {s:3d}  logits {np.round(s * cos, 2)}  softmax {np.round(softmax(s * cos), 4)}")

ss = np.linspace(1, 120, 60)
P = np.array([softmax(s * cos) for s in ss])
plt.figure(figsize=(5, 3))
for k, n in enumerate(names):
    plt.plot(ss, P[:, k], label=n)
plt.axvline(100, ls="--", c="grey")
plt.xlabel("logit scale s"); plt.ylabel("probability"); plt.legend()
plt.title("Same cosines, sharper softmax as s grows")
plt.show()

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

vector length: (512,) norms [1. 1. 1.]
cosine: sneaker 0.1984, sandal 0.1619, ankle_boot 0.1262
s =   1  logits [0.2  0.16 0.13]  softmax [0.3455 0.3331 0.3214]
s =  10  logits [1.98 1.62 1.26]  softmax [0.4587 0.3184 0.2229]
s = 100  logits [19.84 16.19 12.62]  softmax [0.974  0.0253 0.0007]

সত্যিকারের ৫১২-মাত্রার embedding, দৈর্ঘ্য ১, আর cosine ঠিক stepper-এর তিনটি সংখ্যা। s = ১-এ softmax প্রায় সমান ভাগ (০.৩৪৫৫, ০.৩৩৩১, ০.৩২১৪): cosine-এর পার্থক্য এত ছোট যে সম্ভাবনায় প্রায় ধরা পড়ে না। s = ১০-এ জয়ী ০.৪৫৯, আর শেখা মান s = ১০০-এ ০.৯৭৪. তিন ক্ষেত্রেই জয়ী একই; s শুধু আত্মবিশ্বাস ঠিক করে। ছবিতে s বাড়ার সঙ্গে তিনটি রেখা আলাদা হতে দেখবেন; ধূসর দাগ s = ১০০. মনে রাখুন, CLIP-এর এই “সম্ভাবনা” calibrate করা নয় (মেশিন লার্নিং কোর্সের probability calibration দেখুন)।

৪ · সত্যিকারের run — ইংরেজি বনাম বাংলা prompt, ensemble আর linear probe

Model: laion/CLIP-ViT-B-32-laion2B-s34B-b79K (OpenCLIP দলের ViT-B/32, card-এ MIT লাইসেন্স, token ছাড়া নামে, প্রায় ৬০৫ MB; পর্ব ৮১-এ CLIP score-এ এটিই ছিল)। Card স্পষ্ট বলে, যাচাই ছাড়া চালু পণ্যে ব্যবহার “out of scope”; আমরা শুধু শেখার জন্য মাপছি। ডেটা Fashion-MNIST (Zalando Research, MIT লাইসেন্স), বাজারঘরের পণ্যের ছবির বদলি: test সেট থেকে প্রতি class-এ ১০০টি (মোট ১০০০, seed ৪২), সাদা পটভূমিতে গাঢ় পণ্য করে উল্টে নেওয়া। Linear probe-এর জন্য train সেট থেকে আলাদা ১০০০টি। তুলনা করা হয়েছে:

  • ইংরেজি prompt “a photo of a {}.”, আর ৬টি template-এর ensemble (“a black and white photo of a {}.” ইত্যাদি)।
  • বাংলা prompt “একটি {}-এর ছবি”, বাংলা class-নামে (টি-শার্ট, প্যান্ট, … কেডস, বুট জুতা), আর ৬টি বাংলা template-এর ensemble; সঙ্গে বাংলা বাক্যে ইংরেজি class-নাম।
  • Linear probe: ছবির encoder স্থির রেখে তার embedding-এর ওপর logistic regression (scikit-learn, C = ১), প্রতি class-এ ১, ১০ আর ১০০টি label দিয়ে।

প্রতিটি accuracy-র পাশে ৯৫% Wilson interval (confidence interval-এর একটি রূপ, অনুপাতের জন্য)। CPU-তে, Python 3.14, torch 2.15.0, transformers 5.22.2, scikit-learn 1.9.1 (লেখার সময়ে)।

~/bazarghar-dl$ uv run python clip_zs.py
model laion/CLIP-ViT-B-32-laion2B-s34B-b79K | logit_scale.exp() = 100.0
patch_embedding weight (768, 3, 32, 32) bias False | class_embedding (768,) | position_embedding (50, 768) | visual_projection (512, 768) | text_projection (512, 512)
pixel_values (1, 3, 224, 224) -> vision last_hidden_state (1, 50, 768)
image embeddings: test (1000, 512), train (1000, 512), 55.0s on CPU
tokens for 'a photo of a sneaker.': 6 (+2 special), context limit 77
tokens for 'একটি কেডস-এর ছবি': 27 (+2 special), context limit 77
zero-shot EN, 1 template           acc 0.845  95% CI [0.821, 0.866]
zero-shot EN, 6-template ensemble  acc 0.858  95% CI [0.835, 0.878]
zero-shot BN, 1 template           acc 0.113  95% CI [0.095, 0.134]
zero-shot BN, 6-template ensemble  acc 0.135  95% CI [0.115, 0.158]
zero-shot class name only (EN)     acc 0.789  95% CI [0.763, 0.813]
zero-shot BN sentence, EN names    acc 0.835  95% CI [0.811, 0.857]
per class (EN ensemble): T-shirt 0.64, pair of trousers 0.98, pullover sweater 0.93, dress 0.84, coat 0.91, sandal 0.95, shirt 0.43, sneaker 0.94, handbag 0.98, ankle boot 0.98
linear probe,   1 labelled/class        acc 0.703  95% CI [0.674, 0.731]
linear probe,  10 labelled/class        acc 0.848  95% CI [0.824, 0.869]
linear probe, 100 labelled/class        acc 0.899  95% CI [0.879, 0.916]
page example: test index 8108 cos sneaker/sandal/ankle boot [0.1984 0.1619 0.1262]
x100 logits [19.84 16.19 12.62] softmax [9.739e-01 2.540e-02 7.000e-04]
symmetric InfoNCE on 8 real image-caption pairs: image->text 1.113, text->image 1.063, mean 1.088; chance log(8)=2.079

পাঁচটি শিক্ষা। এক, প্রথম তিন লাইন অংশ ১-এর হিসাব নিশ্চিত করে: patch-এর weight (৭৬৮, ৩, ৩২, ৩২), bias নেই, position embedding (৫০, ৭৬৮), আর ছবি ঢুকে (১, ৫০, ৭৬৮) বের হয়। দুই, ইংরেজি prompt-এ zero-shot ০.৮৪৫, ensemble-এ ০.৮৫৮; ব্যবধান দুটি অনেকটা মিলে যায়, তাই এই উন্নতি এখানে নিশ্চিত নয় (একই ছবিতে জোড়া তুলনা লাগত)। শুধু class-নাম (template ছাড়া) ০.৭৮৯, স্পষ্টভাবে কম: prompt-এর গঠন সত্যিই প্রভাব ফেলে। তিন, বাংলা prompt-এ ০.১১৩ আর ০.১৩৫, দশ class-এ আন্দাজের (০.১০) প্রায় সমান। অথচ বাংলা বাক্যে ইংরেজি class-নাম বসালে ০.৮৩৫: model আসলে ইংরেজি শব্দটি চিনছে, বাংলা বাক্য নয়। কারণ দুটি: card অনুযায়ী training ছিল ইংরেজি অংশে, আর বাংলা বাক্যটি ২৭টি byte-level token-এ ভেঙেছে (ইংরেজিটি ৬টিতে)। চার, “shirt” ০.৪৩ আর “T-shirt” ০.৬৪: ২৮ × ২৮ ধূসর ছবিতে শার্ট, টি-শার্ট আর কোট আলাদা করা মানুষের পক্ষেও কঠিন। পাঁচ, প্রতি class-এ মাত্র ১০টি label-এর linear probe (০.৮৪৮) zero-shot-এর সমান, ১০০টিতে ০.৮৯৯, স্পষ্টভাবে বেশি; আর মাত্র ১টি label-এ (০.৭০৩) zero-shot-ই ভালো। অর্থাৎ label একেবারে না থাকলে prompt, কয়েক ডজন থাকলে probe. শেষ লাইনে ৮টি সত্যিকারের জোড়ায় দুই দিকের InfoNCE ১.০৮৮, আন্দাজের log ৮ = ২.০৭৯-এর অনেক নিচে। এটি একটি ছোট স্থানীয় run, একটি ডেটাসেট আর একটি model; সাধারণ ফল নয়।

নিচের ব্লক এই সংখ্যাগুলো থেকে Wilson ব্যবধান নিজে হিসাব করে ছবি আঁকে, যাতে কোন পার্থক্য ব্যবধানের বাইরে, তা চোখে দেখা যায়।

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

n = 1000                                       # correct answers out of 1000 test images (desktop run)
runs = {"EN prompt, 1 template": 845, "EN prompt, 6-template ensemble": 858,
        "BN prompt, 1 template": 113, "BN prompt, 6-template ensemble": 135,
        "class name only (EN)": 789, "BN sentence + EN class name": 835,
        "linear probe, 1 label/class": 703, "linear probe, 10 labels/class": 848,
        "linear probe, 100 labels/class": 899}

def wilson(k, n, z=1.96):                       # 95% Wilson score interval
    p = k / n; d = 1 + z**2 / n
    c = (p + z**2 / (2 * n)) / d
    h = z * np.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / d
    return c - h, c + h

rows = [(name, k / n, *wilson(k, n)) for name, k in runs.items()]
df = pd.DataFrame(rows, columns=["setting", "acc", "lo", "hi"]).round(3)
pd.set_option("display.width", 100)
print(df.to_string(index=False))
print("chance with 10 classes:", 1 / 10)

fig, ax = plt.subplots(figsize=(6, 3.6))
y = np.arange(len(df))
ax.errorbar(df.acc, y, xerr=[df.acc - df.lo, df.hi - df.acc], fmt="o", capsize=3)
ax.axvline(0.1, ls=":", c="grey")
ax.set_yticks(y); ax.set_yticklabels(df.setting, fontsize=8); ax.invert_yaxis()
ax.set_xlabel("accuracy (95% Wilson interval), n = 1000")
ax.set_title("CLIP ViT-B/32 on Fashion-MNIST: prompts vs probe")
fig.subplots_adjust(left=0.42)
plt.show()

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

                       setting   acc    lo    hi
         EN prompt, 1 template 0.845 0.821 0.866
EN prompt, 6-template ensemble 0.858 0.835 0.878
         BN prompt, 1 template 0.113 0.095 0.134
BN prompt, 6-template ensemble 0.135 0.115 0.158
          class name only (EN) 0.789 0.763 0.813
   BN sentence + EN class name 0.835 0.811 0.857
   linear probe, 1 label/class 0.703 0.674 0.731
 linear probe, 10 labels/class 0.848 0.824 0.869
linear probe, 100 labels/class 0.899 0.879 0.916
chance with 10 classes: 0.1

সংখ্যাগুলো ডেস্কটপ run-এর Wilson ব্যবধানের সঙ্গে হুবহু মেলে। ১০০০টি ছবিতে ব্যবধানের অর্ধেক প্রস্থ প্রায় ০.০২, তাই ০.৮৪৫ আর ০.৮৫৮ (ensemble) একে অপরের ব্যবধানে ঢুকে যায়; এখানে ensemble-এর লাভ প্রমাণিত নয়। ০.৭৮৯ (শুধু নাম) আর ০.৮৯৯ (১০০ label-এর probe) স্পষ্টভাবে আলাদা। বাংলা prompt-এর ব্যবধান [০.০৯৫, ০.১৩৪] আন্দাজের ০.১০ ছুঁয়ে আছে। ব্যবধান একটি একটি করে দেখা রক্ষণশীল পরীক্ষা; একই ছবিতে দুটি পদ্ধতির তুলনায় জোড়া পরীক্ষা (যেমন McNemar, মেশিন লার্নিং কোর্সের model comparison test) বেশি সংবেদনশীল, তার জন্য প্রতিটি ছবির ফল লাগে।

৫ · Image captioning — ছবি দেখে বাক্য লেখা

CLIP মিল মাপে, কিন্তু নিজে একটি শব্দও লেখে না। ছবি থেকে বাক্য লেখা image captioning, যা গঠনে পর্ব ৩০-এর encoder-decoder: encoder ছবিকে vector-এর sequence বানায়, decoder একটি একটি করে শব্দ লেখে, আর প্রতিটি ধাপে cross-attention দিয়ে ছবির কোন অংশে তাকাবে ঠিক করে (পর্ব ৩২)। Training-এ loss সাধারণ next-token cross-entropy: ছবি আর আগের শব্দগুলো দেখে পরের শব্দ।

ইতিহাসের ধাপ: Show and Tell (Vinyals ও সহকর্মীরা, ২০১৫) একটি CNN-এর শেষ vector দিয়ে LSTM decoder চালায়; Show, Attend and Tell (Xu ও সহকর্মীরা, ২০১৫) ছবির grid-এর ওপর attention যোগ করে। পরে encoder আর decoder দুটিই Transformer হয়। BLIP (Li ও সহকর্মীরা, Salesforce, ২০২২) তিনটি লক্ষ্য একসঙ্গে শেখায়: CLIP-এর মতো contrastive মিল, ছবি-লেখা জোড়া মেলে কি না তার হ্যাঁ/না (matching), আর caption লেখা। ইন্টারনেটের caption প্রায়ই অগোছালো বলে BLIP নিজের captioner দিয়ে নতুন caption বানিয়ে আরেকটি filter দিয়ে খারাপগুলো বাদ দেয় (প্রবন্ধে CapFilt). BLIP-2 (২০২৩) একটি স্থির (frozen) image encoder আর স্থির LLM-এর মাঝে ছোট একটি Q-Former বসায়, যা কয়েকটি শেখা query দিয়ে ছবির তথ্য LLM-এর জন্য সংক্ষেপ করে।

মূল্যায়নে মডেলের caption-কে মানুষের লেখা কয়েকটি caption-এর সঙ্গে n-gram মিলিয়ে BLEU বা CIDEr মাপা হয় (ধারণা পর্ব ৪৫-এ)। সমস্যাও একই: “একটি লাল মগ টেবিলে” আর “টেবিলের ওপর একটি লাল কাপ” দুটিই ঠিক, কিন্তু শব্দ কম মেলে; আবার ছবিতে নেই এমন বস্তুর নাম লিখলেও সাবলীল বাক্য ভালো নম্বর পেতে পারে। তাই caption-এ ভুল বস্তু ধরার আলাদা মাপ লাগে (অংশ ৭)।

৬ · Vision-language model — vision encoder, projector আর LLM

আজকের “ছবি আপলোড করে প্রশ্ন” ধরনের সহায়ক সাধারণত একটি vision-language model (VLM). সবচেয়ে প্রচলিত প্রকাশিত নকশাটি LLaVA (Liu ও সহকর্মীরা, ২০২৩) থেকে পরিচিত, তিনটি অংশে:

  1. Vision encoder: CLIP বা SigLIP-এর ছবির অংশ; patch-গুলোর vector দেয় (CLS-এর একটি নয়, সবগুলো)।
  2. Projector: একটি linear layer বা ছোট MLP, যা ছবির vector-কে LLM-এর token embedding-এর মাপে আনে। LLaVA-র প্রথম সংস্করণে একটি linear layer, LLaVA-1.5-এ দুই স্তরের MLP.
  3. LLM: সাধারণ decoder-only model (পর্ব ৪৯)। Projector-এর output “visual token” হিসেবে prompt-এর ভেতরে লেখার token-এর পাশে বসে, আর LLM সেগুলোর ওপর সাধারণ self-attention চালায়।

LLaVA প্রবন্ধে শেখানো হয় দুই ধাপে: প্রথমে শুধু projector, ছবি-caption জোড়ায় (encoder আর LLM স্থির); তারপর projector আর LLM একসঙ্গে, ছবি নিয়ে প্রশ্নোত্তরের নির্দেশনা-ডেটায় (পর্ব ৫৬-এর ধারণা), যার বড় অংশ লেখকেরা একটি লেখা-ভিত্তিক GPT-4 দিয়ে বানান। ভিন্ন পথ নেয় Flamingo (Alayrac ও সহকর্মীরা, DeepMind, ২০২২): visual token prompt-এ না বসিয়ে LLM-এর স্তরের মাঝে নতুন gated cross-attention স্তর ঢোকায়। খোলা model-এর মধ্যে Qwen-VL পরিবার ছবির আকার অনুযায়ী token-এর সংখ্যা বদলায়; Qwen2.5-VL-এর card বলে প্রতিটি visual token ২৮ × ২৮ pixel-এর সমান, আর ডিফল্টে একটি ছবিতে ৪ থেকে ১৬,৩৮৪টি visual token (লেখার সময়ে, নভেম্বর ২০২৬; পরিবর্তন হতে পারে)।

আমরা চালাব HuggingFaceTB/SmolVLM-256M-Instruct (card-এ Apache 2.0 লাইসেন্স, প্রায় ২৫.৬ কোটি parameter). Card অনুযায়ী এটি Idefics3-ভিত্তিক: ৯.৩ কোটি parameter-এর SigLIP vision encoder (siglip-base-patch16-512), ভাষার অংশে SmolLM2, আর প্রতিটি ৫১২ × ৫১২ টুকরোকে ৬৪টি visual token-এ আনে; বড় ছবি কয়েকটি টুকরোয় ভাগ হয়। নিচের stepper সত্যিকারের run-এর shape ধরে একটি প্রশ্নের পুরো পথ দেখায়।

  1. ছবি(1, 1, 3, 512, 512)resize, normalise
  2. Vision encoder(1, 1024, 768)SigLIP, patch 16
  3. Projector(1, 64, 576)pixel shuffle + linear
  4. Prompt82 token, 64টি ছবিরvisual + লেখার token
  5. LLM18 token, 1.2 sgreedy decode
  1. ছবি. Processor ছবিকে ৫১২ × ৫১২-এ এনে normalise করে; shape (১, ১, ৩, ৫১২, ৫১২) মানে ১টি ছবি, ১টি টুকরো, ৩টি রং। ডিফল্ট টুকরো-ভাগ চালু থাকলে একই ছবি ১৭টি টুকরো হয় (৪ × ৪ = ১৬ অংশ আর পুরো ছবির ছোট রূপ)।
  2. Vision encoder. SigLIP ১৬ × ১৬ patch-এ কাটে: ৫১২ ÷ ১৬ = ৩২, তাই ৩২ × ৩২ = ১০২৪টি vector, প্রতিটি ৭৬৮ মাত্রার। এখানে CLS নেই; সব patch-এর vector পরের ধাপে যায়।
  3. Projector. Pixel shuffle ৪ × ৪ প্রতিবেশীকে জুড়ে ১০২৪ থেকে ৬৪টি vector বানায় (প্রতিটি ৭৬৮ × ১৬ = ১২,২৮৮ চওড়া), তারপর একটি linear layer LLM-এর মাপ ৫৭৬-এ আনে: (১, ৬৪, ৫৭৬)।
  4. Prompt. Chat template-এ ছবির জায়গায় ৬৪টি বিশেষ image token বসে, পাশে প্রশ্ন “Describe this image in one sentence.” আর ভূমিকার token; মোট ৮২টি। LLM-এর embedding স্তরে ওই ৬৪টি token-এর জায়গায় projector-এর ৬৪টি vector বসানো হয়।
  5. LLM. SmolLM2-ভিত্তিক ৩০ স্তরের decoder সাধারণ next-token নিয়মে greedy উত্তর লেখে: ১৮টি নতুন token, CPU-তে ১.২ সেকেন্ড: “In this image, there is a table with a red square on the left side.” টুকরো-ভাগ চালু থাকলে prompt ১১৪২ token (১০৮৮টি ছবির), সময় ১৩.৮ সেকেন্ড।

এই হিসাবের ব্যবহারিক মানে: প্রতিটি visual token LLM-এর context-এ জায়গা নেয়, KV cache-এ মেমরি নেয় (পর্ব ৩৭), আর hosted API-তে input token হিসেবে গোনা হয় (পর্ব ৬৫)। বড় ছবিকে টুকরো করলে ছোট লেখা পড়া সহজ হয়, কিন্তু token কয়েক গুণ বাড়ে; সত্যিকারের run-এ ঠিক এই trade-off মাপা হয়েছে।

৭ · সত্যিকারের run — SmolVLM-256M: বর্ণনা, গণনা, লেখা পড়া আর hallucination

ছবি: আটটি, কোনোটিতে মানুষ নেই। পাঁচটি নিজে আঁকা (PIL দিয়ে, ৫১২ × ৫১২): একটি প্লেটে ৭টি কমলা, দামের ট্যাগে “Price: 450 Tk” বড় (৬৪ px) আর ছোট (১৪ px) font-এ, নীল বোর্ডে বাংলায় “দাম ১২০ টাকা”, আর টেবিলে একটি লাল মগ, নীল বই ও টবের গাছ। বাকি তিনটি Fashion-MNIST-এর sneaker, ব্যাগ আর প্যান্ট, ২৮ থেকে ৫১২ pixel-এ বড় করা। Model fp32-এ CPU-তে, greedy decoding (do_sample=False), একটি টুকরোয় (do_image_splitting=False) যদি না অন্যভাবে বলা থাকে। প্রশ্ন ইংরেজি আর বাংলায়, উত্তর হুবহু।

~/bazarghar-dl$ uv run python vlm_run.py
transformers 5.22.2, torch 2.15.0+cpu, params 256,484,928
== trace: desk.png, one 512x512 tile vs default splitting
  pixel_values (1, 1, 3, 512, 512)  prompt tokens 82 (image tokens 64)  {'vision_out': (1, 1024, 768), 'connector_out': (1, 64, 576)}
  split=False: 1.2s, 18 new tokens -> 'In this image, there is a table with a red square on the left side.'
  pixel_values (1, 17, 3, 512, 512)  prompt tokens 1142 (image tokens 1088)  {'vision_out': (17, 1024, 768), 'connector_out': (17, 64, 576)}
  split=True: 13.8s, 15 new tokens -> 'In this image we can see a table with some objects on it.'
== descriptions (no splitting, greedy, max 60 new tokens)
[desk] Describe this image in one sentence.
   -> 'In this image, there is a table with a red square on the left side.'  (1.2s)
[desk] এই ছবিতে কী আছে? বাংলায় এক বাক্যে বলুন।
   -> '1.'  (0.9s)
[oranges7] Describe this image in one sentence.
   -> 'In this image there is a white plate and there are orange fruits on it.'  (1.2s)
[oranges7] এই ছবিতে কী আছে? বাংলায় এক বাক্যে বলুন।
   -> '9'  (0.9s)
[fm_sneaker] Describe this image in one sentence.
   -> 'In this image there is a shoe.'  (1.0s)
[fm_sneaker] এই ছবিতে কী আছে? বাংলায় এক বাক্যে বলুন।
   -> 'Yes, the shoe is a standard size for children.'  (1.1s)
[bangla_sign] Describe this image in one sentence.
   -> 'A blue rectangle with the text "বাংলাকাতাক"'  (1.4s)
[bangla_sign] এই ছবিতে কী আছে? বাংলায় এক বাক্যে বলুন।
   -> 'নিতে কাক্কার কাক্কার কাক্কার কা�'  (2.1s)
== reading text and counting
[tag_big] What price is written on the tag? -> '450 TK'
[tag_small] What price is written on the tag? -> 'The price written on the tag is 450 Tk.'
[bangla_sign] What does the text in the image say? -> '11000000'
[oranges7] How many oranges are on the plate? -> '9'
[oranges7] How many oranges are on the plate? Answer with a number. -> '9'
== hand-labelled yes/no questions: 'Is there a X in the image? Answer yes or no.'
  desk        red mug           truth yes model 'No.'
  desk        blue book         truth yes model 'No.'
  desk        plant             truth yes model 'Yes.'
  desk        cat               truth no  model 'No.'
  desk        laptop            truth no  model 'No.'
  desk        person            truth no  model 'No.'
  desk        banana            truth no  model 'No.'
  oranges7    plate             truth yes model 'Yes.'
  oranges7    orange            truth yes model 'Yes.'
  oranges7    banana            truth no  model 'No.'
  oranges7    knife             truth no  model 'No.'
  oranges7    dog               truth no  model 'No.'
  fm_sneaker  shoe              truth yes model 'Yes.'
  fm_sneaker  dog               truth no  model 'No.'
  fm_sneaker  car               truth no  model 'No.'
  fm_bag      bag               truth yes model 'Yes.'
  fm_bag      shoe              truth no  model 'No.'
  fm_trouser  pair of trousers  truth yes model 'No.'
  fm_trouser  shoe              truth no  model 'No.'
  fm_trouser  cat               truth no  model 'No.'
present objects: said yes 5/8 | absent objects: said yes 0/12 (hallucinated) | accuracy 17/20
peak RSS of this process: 2296 MiB

ফলগুলো পড়ুন সাবধানে; এটি একটি ছোট model, ২০টি প্রশ্ন আর আটটি ছবি, তাই এক run-এর পর্যবেক্ষণ, সাধারণ সিদ্ধান্ত নয়।

  • বর্ণনা: কমলার প্লেট আর জুতো ঠিকমতো বর্ণনা করেছে। টেবিলের ছবিতে মগকে “a red square” বলেছে: সরল আঁকা মগটি সে মগ হিসেবে চেনেনি, কিন্তু যা দেখেছে তার বাইরে কিছু বানায়নি। টুকরো-ভাগে ১৭ গুণ token দিয়েও বর্ণনা আরও অস্পষ্ট হয়েছে (“some objects”), ১১ গুণ বেশি সময়ে।
  • গণনা: প্লেটে ৭টি কমলা, দুবারই উত্তর ৯. সংখ্যা আত্মবিশ্বাসী, কিন্তু ভুল।
  • লেখা পড়া: ইংরেজি দাম “450 Tk” বড় আর ছোট (১৪ px) দুই font-এই ঠিক পড়েছে; এই পরিষ্কার, কৃত্রিম ছবিতে ছোট লেখা সমস্যা হয়নি। কিন্তু বাংলা “দাম ১২০ টাকা” পড়তে পারেনি: বর্ণনায় অর্থহীন “বাংলাকাতাক”, আর লেখা জিজ্ঞেস করলে “11000000”. এটি hallucination: সাবলীল, নির্দিষ্ট, আর সম্পূর্ণ বানানো।
  • বাংলায় প্রশ্ন: চারটি বাংলা প্রশ্নের একটিরও বোধগম্য উত্তর আসেনি: “1.”, “9”, জুতা নিয়ে বানানো তথ্য (“standard size for children”), আর বাংলা চিহ্নে একই শব্দের পুনরাবৃত্তি, যা ৬০ token-এর সীমায় একটি বাংলা অক্ষরের মাঝখানে কেটে গেছে (শেষের � চিহ্ন)। Card-এ training ডেটার ভাষা আলাদা করে বলা নেই; ফল দেখে বলা যায়, এই model বাংলায় ব্যবহারযোগ্য নয়।
  • হ্যাঁ/না প্রশ্ন: ছবিতে নেই এমন ১২টি বস্তুর কোনোটিতেই “হ্যাঁ” বলেনি (object hallucination ০/১২), কিন্তু উপস্থিত ৮টির ৩টিতে “না” বলেছে (লাল মগ, নীল বই, প্যান্ট)। মোট ১৭/২০. এই model “না”-র দিকে ঝুঁকে আছে; শুধু hallucination-এর হার দেখলে ভালো মনে হতো, তাই দুটি হার একসঙ্গে দিতে হয়। ১২টি প্রশ্নে ০ পেলেও আসল হার শূন্য বলা যায় না: Wilson-এর ৯৫% ব্যবধান প্রায় [০, ০.২৪]।

পুরো run একটি Python process-এ, সর্বোচ্চ মেমরি প্রায় ২,২৯৬ MiB (fp32-এ; card bf16-এ ১ GB-এর কম GPU মেমরির কথা বলে)। এক টুকরোয় একটি বর্ণনা প্রায় ১ সেকেন্ড, CPU-তে ৩টি thread-এ।

৮ · অডিও, আর পণ্যের নথিতে image input

একই কৌশল শব্দেও খাটে: শব্দ-তরঙ্গকে আগে ছোট ছোট সময়-খণ্ডের কম্পাঙ্ক-ছবিতে (log-Mel spectrogram) আনা হয়, তারপর সেই “ছবি”-কে token বানানো হয়। Whisper (Radford ও সহকর্মীরা, OpenAI, ২০২২) একটি encoder-decoder Transformer: প্রবন্ধ অনুযায়ী ৩০ সেকেন্ডের খণ্ডের spectrogram encoder-এ যায়, আর decoder লেখা দেয়; শেখানো হয় ইন্টারনেট থেকে নেওয়া প্রায় ৬.৮ লাখ ঘণ্টার অডিও আর তার transcript-এ। এটি বহু ভাষায় কাজ করে বলে প্রবন্ধে দাবি, কিন্তু প্রবন্ধের ফলাফলেই training ডেটা কম এমন ভাষায় ভুলের হার বেশি; বাংলায় ব্যবহার করলে নিজের অডিওর নমুনায় word error rate মেপে নিন। আজকের কিছু model শব্দ সরাসরি LLM-এর token হিসেবেও নেয়; সেই খুঁটিনাটি এই পর্বের বাইরে।

Hosted assistant-গুলো image input কীভাবে নেয়, তাদের নিজের নথিতে যা লেখা পেয়েছি, তার সারাংশ নিচে। সংখ্যাগুলো লেখার সময়ের (নভেম্বর ২০২৬), পরিবর্তন হতে পারে; দাম বাদ রাখা হয়েছে, খরচ বোঝাতে শুধু token. কোনো তুলনা বা মান-বিচার নয়, শুধু নথির বক্তব্য।

API (নথি)ছবি পাঠানোর উপায় ও ফরম্যাটছবি থেকে tokenনথিতে লেখা সীমা
OpenAI APIURL, base64 data URL বা Files API-এর file ID; PNG, JPEG, WEBP, স্থির GIFনতুন model-এ ৩২ × ৩২ patch: ⌈w/৩২⌉ × ⌈h/৩২⌉, তারপর model-ভেদে একটি গুণক আর সর্বোচ্চ সীমা; detail দিয়ে low/high/original/autoগণনা আনুমানিক; সূক্ষ্ম অবস্থান নির্ণয়ে দুর্বল; জাপানি বা কোরিয়ানের মতো অ-লাতিন লিপিতে মান কমতে পারে; CT scan-এর মতো চিকিৎসা-ছবির জন্য নয়
Claude API (Anthropic)base64, URL বা Files API; JPEG, PNG, GIF, WebP২৮ × ২৮ patch: ⌈w/২৮⌉ × ⌈h/২৮⌉; বড় ছবি ছোট করা হয় (সাধারণ স্তরে সর্বোচ্চ ১,৫৬৮ token, নতুন model-এ উঁচু স্তরে ৪,৭৮৪)ছবির মানুষের নাম বলে না; ঝাপসা, ঘোরানো বা ২০০ pixel-এর ছোট ছবিতে ভুল বা hallucination হতে পারে; গণনা আনুমানিক; AI-তৈরি ছবি চিনতে পারে না
Gemini API (Google)inline base64 (পুরো request ২০ MB পর্যন্ত) বা Files API; PNG, JPEG, WEBP, HEIC, HEIFদুই দিকই ৩৮৪ pixel বা কম হলে ২৫৮ token; বড় ছবি ৭৬৮ × ৭৬৮ টুকরোয়, প্রতিটি ২৫৮; media_resolution দিয়ে সীমাছবি ঠিকমতো ঘোরানো আর স্পষ্ট রাখার পরামর্শ; বেশি resolution-এ ছোট লেখা ভালো পড়ে, কিন্তু token আর সময় বাড়ে
Grok API (xAI)public URL বা base64 data URL; JPG/JPEG, PNG; প্রতি ছবি ২০ MiB পর্যন্তনথিতে পাইনিনথিতে পাইনি
DeepSeek APIuser message-এ image_url অংশ: http(s) URL বা base64; JPEG, PNG, GIF, WebPনথিতে পাইনিনথিতে পাইনি
Kimi API (Moonshot AI)vision নির্দেশিকার উদাহরণে base64 data URL, বড় ফাইলে আগে upload; URL সমর্থন নিয়ে নথির পাতাগুলোতে ভিন্ন কথা পেয়েছিনথিতে পাইনিSVG গ্রহণ করে না; প্রস্তাবিত আকারের চেয়ে বড় ছবিতে সময় বাড়ে কিন্তু বোঝা ভালো হয় না

লক্ষ করুন, তিনটি নথিই patch বা টুকরো-ভিত্তিক token গণনা বলে, ঠিক অংশ ১-এর হিসাব। Claude-এর নথির উদাহরণ ১০০০ × ১০০০ ছবিতে ১,২৯৬ token, যা ব্লক ১-এর ⌈১০০০/২৮⌉2 = ৩৬2-এর সমান। নিচের request-এর আকার একটি নমুনা, চালানো হয়নি (key ছাড়া hosted API ডাকা যায় না):

// নমুনা (OpenAI-ধাঁচের chat request; DeepSeek ও Kimi-ও এই আকার নথিতে দেখায়)
{"model": "<model-name>",
 "messages": [{"role": "user", "content": [
   {"type": "text", "text": "এই রসিদে মোট কত টাকা?"},
   {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}}]}]}

৯ · ব্যর্থতার চার ধরন আর কীভাবে মাপবেন

গণনা

আমাদের run-এ প্লেটের ৭টি কমলা দুবারই ৯টি বলা হয়েছে, পুরো আত্মবিশ্বাসে। Hosted API-র নথিও গণনাকে “আনুমানিক” বলে। গণনা জরুরি হলে আলাদা detector দিয়ে বস্তু গুনে মিলিয়ে নিন।

ছোট লেখা

পরিষ্কার কৃত্রিম ট্যাগে ১৪ px-এর ইংরেজি দামও ঠিক পড়েছে; বাস্তব ছবিতে ঝাপসা, বাঁকা বা ছোট লেখায় ভুল বাড়ে বলে নথিগুলো সতর্ক করে। বেশি resolution বা টুকরো (বেশি token) সাহায্য করে; লেখা পড়াই মূল কাজ হলে আগে OCR.

অনুপস্থিত বস্তু

আমাদের model অনুপস্থিত বস্তুতে “হ্যাঁ” বলেনি, কিন্তু উপস্থিত তিনটিকে “না” বলেছে, আর বাংলা লেখা বানিয়ে লিখেছে। মূল্যায়নে অর্ধেক প্রশ্ন এমন বস্তু নিয়ে রাখুন যা ছবিতে নেই, আর “হ্যাঁ”-র হার আলাদা করে দিন।

বাংলা লেখা ও প্রশ্ন

CLIP-এ বাংলা prompt আন্দাজের সমান, SmolVLM-এ বাংলা প্রশ্নের উত্তর অর্থহীন, বাংলা লেখা পড়তে পারেনি। বাংলায় ব্যবহারের আগে বাংলা ছবি আর বাংলা প্রশ্নের নিজস্ব মূল্যায়ন সেট বানান।

মূল্যায়নের সাধারণ নিয়ম আগের পর্বগুলোর মতোই (পর্ব ৫৮): নির্দিষ্ট test সেট, prompt বাছাইয়ের জন্য আলাদা validation সেট, ব্যবধানসহ সংখ্যা, আর একই ছবিতে দুটি model-এর জোড়া তুলনা। ছবির ক্ষেত্রে বাড়তি সতর্কতা: test ছবি যেন model-এর training ডেটায় না থাকে (ইন্টারনেটের জনপ্রিয় ছবি প্রায়ই থাকে), আর মানুষের ছবি হলে সম্মতি ও গোপনীয়তার প্রশ্ন। এই পর্বের সব ছবি নিজে আঁকা বা MIT লাইসেন্সের Fashion-MNIST; কোনো মানুষের ছবি নেই।

আরও গভীরে: Contrastive loss আসলে দুই দিকের ছবি আর লেখার পারস্পরিক তথ্যের (mutual information) একটি নিম্নসীমা বাড়ায়, যা batch-এর আকার N দিয়ে log N-এ সীমিত; বড় batch কেন সাহায্য করে, এটি তার একটি তাত্ত্বিক ব্যাখ্যা (van den Oord ও সহকর্মীদের CPC প্রবন্ধ, ২০১৮)। SigLIP প্রতিটি ঘরকে আলাদা হ্যাঁ/না সমস্যা ধরে, তাই পুরো সারির softmax লাগে না আর বহু device-এ ভাগ করা সহজ। VLM-এ visual token কমানোর কৌশল (pixel shuffle, Q-Former, token pruning) মূলত তথ্য আর context-এর খরচের trade-off.

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

  • ২২৪ × ২২৪ ছবি, patch ১৬: কতটি token? ১৪ × ১৪ = ১৯৬, CLS নিয়ে ১৯৭.
  • CLIP-এর loss কী? N × N cosine ছকের সারি আর কলামে cross-entropy-র গড় (symmetric InfoNCE), কর্ণ সঠিক উত্তর, logit scale শেখা।
  • Zero-shot বনাম linear probe? Zero-shot-এ class-prompt-এর text embedding-ই weight; probe-এ স্থির ছবির embedding-এর ওপর label দিয়ে শেখা linear classifier.
  • LLaVA-ধাঁচের VLM-এর তিন অংশ? Vision encoder, projector (linear বা MLP), আর decoder-only LLM, যার prompt-এ visual token বসে।
  • Object hallucination কীভাবে মাপবেন? উপস্থিত ও অনুপস্থিত বস্তু নিয়ে ভারসাম্যপূর্ণ হ্যাঁ/না প্রশ্ন (POPE-ধাঁচ); অনুপস্থিত বস্তুতে “হ্যাঁ”-র হার।

১০ · AI-এর যুগে — multimodal কোড আর দাবিতে AI-এর ভুল

AI assistant CLIP বা VLM-এর কোড কয়েক লাইনে লিখে দেয়, আর তা প্রায়ই চলে; কিন্তু সাধারণ ভুলগুলো ফলকে নীরবে বদলে দেয়। আর “model ছবিটা বুঝেছে” ধরনের দাবি প্রায়ই একটি দুটি সুন্দর উত্তর দেখে করা হয়।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
Embedding normalise না করে dot product, বা logit scale বাদ দিয়ে softmaxক্রম বদলাতে পারে, সম্ভাবনা প্রায় সমান দেখায়Norm ছাপুন (১ হওয়া উচিত); logit_scale.exp() দেখুন
পুরোনো বা বানানো argument (যেমন processor-এর বাদ দেওয়া নাম, ভুল model class)Error, বা নীরবে অন্য preprocessingনিজের transformers version-এর docs আর model card পড়ুন; একটি ছবিতে shape ছাপুন
ধূসর বা ছোট ছবি সরাসরি পাঠায়, রং-চ্যানেল বা resize-এর নিয়ম না দেখেModel-এর training ছবির মতো নয়, accuracy কমেProcessor-এর আসল output (pixel_values-এর shape, পরিসর) দেখুন
Zero-shot-এর prompt test সেট দেখে বারবার বদলায়, তারপর সেই test-এ accuracy দেয়Test ডেটা ফাঁস, ফল আশাবাদীPrompt বাছাই আলাদা validation সেটে; test একবার
দুই-তিনটি সুন্দর caption দেখে “model নির্ভুল” বলেHallucination আর গণনার ভুল চোখ এড়ায়হাতে label করা প্রশ্নের সেট, অনুপস্থিত বস্তুর প্রশ্নসহ; ব্যবধানসহ হার
Hosted API-র সীমা বা token-এর সূত্র স্মৃতি থেকে বলেনথি বদলায়; পুরোনো সংখ্যা ভুল খরচের হিসাব দেয়সেই দিনের অফিসিয়াল নথি খুলে তারিখসহ লিখুন

AI-কে কোডের সঙ্গে যাচাইয়ের উপায়ও চাইতে বলুন:

আমি একটি খোলা CLIP model দিয়ে আমার দোকানের ১০টি পণ্য-বিভাগে zero-shot classification করতে চাই। আমার torch আর transformers-এর version নিচে দিলাম। ১. কোন model class আর processor ব্যবহার করছ, আর আমার version-এর docs-এর কোন অংশ থেকে বলছ, লেখো। ২. ছবি আর লেখার embedding normalise করা আর logit scale গুণ করার লাইনগুলো মন্তব্যসহ দেখাও। ৩. Prompt template বাছাইয়ের জন্য আলাদা validation সেট রাখার কোড দাও, test-এ একবারই মাপো। ৪. Accuracy-র পাশে 95% Wilson ব্যবধান আর প্রতিটি class-এর accuracy ছাপো। ৫. কোনো API বা argument সম্পর্কে নিশ্চিত না হলে স্পষ্ট বলো; বানিয়ে লিখো না।

Multimodal কাজ — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১হাতে লাগানো label থেকে ইন্টারনেটের caption — CLIP কী বদলেছিল, আর কোন দাম দিতে হয়েছে?

২০১২-এর AlexNet থেকে কম্পিউটার ভিশনের মূল পথ ছিল ImageNet: প্রায় ১০ লাখ ছবি, ১০০০টি স্থির class, মানুষের লাগানো label. নতুন কাজের জন্য নতুন label, নতুন fine-tune. প্রাকৃতিক ভাষার caption থেকে ছবি শেখার চেষ্টা আগেও ছিল (যেমন Li ও সহকর্মীদের Visual N-Grams, ২০১৭; চিকিৎসা-ছবিতে ConVIRT, ২০২০), কিন্তু ফল দুর্বল ছিল।

CLIP (২০২১) দেখায়, ডেটা আর batch যথেষ্ট বড় হলে সরল contrastive লক্ষ্যই কাজ করে। প্রবন্ধ অনুযায়ী প্রায় ৪০ কোটি জোড়ায় শেখা model ImageNet-এ একটি label না দেখেও zero-shot-এ মূল ResNet-50-এর কাছাকাছি accuracy পায়, আর distribution বদলালে (স্কেচ, কার্টুন) সাধারণ ImageNet model-এর চেয়ে কম ভাঙে। একই বছরে Google-এর ALIGN আরও বড়, আরও অগোছালো জোড়ায় একই ধারণা দেখায়।

দাম তিন দিক থেকে। এক, ডেটার উৎস: OpenAI তাদের ডেটাসেট প্রকাশ করেনি, তাই LAION-এর মতো খোলা উদ্যোগ ইন্টারনেট থেকে কোটি কোটি জোড়া সংগ্রহ করে। ২০২৩-এর শেষে Stanford Internet Observatory-র একটি প্রতিবেদনে LAION-5B-এ শিশু নির্যাতনের উপাদানের লিংক পাওয়ার কথা আসে, আর ডেটাসেটটি সাময়িকভাবে সরিয়ে পরে পরিষ্কার করা সংস্করণ দেওয়া হয় বলে জানা যায়। দুই, পক্ষপাত: caption যা বলে, model তাই শেখে, সমাজের বাঁধা ধারণাসহ; CLIP প্রবন্ধের লেখকেরা নিজেরাই মানুষের ছবিতে বিপজ্জনক ভুল-শ্রেণিবিন্যাস দেখান। তিন, ভাষা: ডেটার বেশির ভাগ ইংরেজি, তাই বাংলা prompt-এ মান কম (অংশ ৪-এর run).

মূল উপলব্ধি: CLIP label-এর বদলে ভাষাকে তত্ত্বাবধান বানিয়ে নমনীয় classifier দিয়েছে; কিন্তু ইন্টারনেটের ডেটা মানে ইন্টারনেটের পক্ষপাত, ভাষার ভারসাম্যহীনতা আর ডেটা-নিরাপত্তার দায়, যা প্রতিটি ব্যবহারে আলাদা করে যাচাই করতে হয়।

প্র ০২আপেলের গায়ে “iPod” লেখা কাগজ — CLIP কেন লেখা দেখে ঠকে, আর এটি কী শেখায়?

২০২১ সালে OpenAI-এর গবেষকেরা (Goh ও সহকর্মীরা, Distill পত্রিকায় “Multimodal Neurons”) CLIP-এর ভেতরের neuron বিশ্লেষণ করে দেখান, কিছু neuron একটি ধারণায় সাড়া দেয়, তা ছবিতে আসুক, আঁকায় আসুক, বা লেখা শব্দে। এর মজার কিন্তু উদ্বেগজনক ফল তাঁরা “typographic attack” নামে দেখান: একটি আপেলের গায়ে হাতে লেখা “iPod” কাগজ লাগালে model ছবিটিকে iPod বলে।

কারণটি training-এর লক্ষ্যেই। ইন্টারনেটের ছবিতে প্রায়ই লেখা থাকে (বিজ্ঞাপন, পণ্যের মোড়ক, স্ক্রিনশট), আর caption প্রায়ই সেই লেখাই বলে। ছবি আর caption মেলানোর সবচেয়ে সহজ উপায় অনেক সময় ছবির ভেতরের লেখা পড়া। তাই CLIP একটি দুর্বল OCR-ও শিখে ফেলে, আর সেই সংকেত কখনো বস্তুর আকারের চেয়ে জোরালো হয়।

বাজারঘরে এর মানে: পণ্যের ছবিতে বড় করে “ORIGINAL LEATHER” লেখা থাকলে zero-shot classifier উপাদান হিসেবে চামড়াই বলবে, ব্যাগটা যা দিয়েই বানানো হোক। একই দুর্বলতা আক্রমণের পথ: কেউ ছবিতে উদ্দেশ্যমূলক লেখা বসিয়ে ফিল্টার এড়াতে পারে। Vision-language model-এ এটি আরও বড় প্রশ্ন, কারণ ছবির লেখা তখন prompt-এর অংশ হয়ে যায়; ছবিতে লুকানো নির্দেশ দিয়ে prompt injection-এর ঝুঁকি পর্ব ৭৩-এর একই নীতিতে দেখতে হয়।

প্রতিরক্ষা নিখুঁত নয়: ছবির লেখা আলাদা OCR দিয়ে পড়ে নীতি অনুযায়ী আলাদা করা, ছবির ভেতরের নির্দেশকে কখনো system নির্দেশ না ধরা, আর লেখা-যুক্ত ও লেখা-ছাড়া ছবিতে আলাদা করে মূল্যায়ন।

মূল উপলব্ধি: Model সেটাই শেখে যা তার লক্ষ্যকে সবচেয়ে সস্তায় পূরণ করে; ছবি-লেখার মিল শেখাতে গিয়ে CLIP লেখা পড়াও শেখে, যা কখনো সুবিধা, কখনো আক্রমণের দরজা।

প্র ০৩ছবিতে নেই এমন চেয়ার — vision-language model-এর object hallucination কেন হয়, আর কীভাবে মাপা হয়?

Image captioning-এর যুগেই সমস্যাটি চোখে পড়েছিল। Rohrbach ও সহকর্মীরা (২০১৮) CHAIR নামের একটি মাপ প্রস্তাব করেন: caption-এ উল্লেখ করা বস্তুর কত ভাগ আসলে ছবির label-তালিকায় নেই। তাঁরা দেখান, প্রচলিত মাপ (BLEU, CIDEr) ভালো হলেও hallucination বেশি হতে পারে। LLM-ভিত্তিক VLM আসার পর Li ও সহকর্মীরা (২০২৩) POPE প্রস্তাব করেন: “ছবিতে কি একটি X আছে?” ধরনের হ্যাঁ/না প্রশ্ন, যার অর্ধেকে X সত্যিই আছে, অর্ধেকে নেই; অনুপস্থিত বস্তুকে বেছে নেওয়া হয় এলোমেলো, জনপ্রিয়, বা সাধারণত সঙ্গে দেখা যায় এমন বস্তু থেকে।

কারণ কয়েকটি। LLM-এর ভাষার অভ্যাস প্রবল: “টেবিল” থাকলে “চেয়ার” আসার সম্ভাবনা বেশি, ছবি যাই বলুক। ছবির তথ্য অল্প কয়েকটি visual token-এ সংক্ষেপ হয় (আমাদের run-এ ৬৪টি), তাই ছোট জিনিস হারায়। আর নির্দেশনা-ডেটায় “হ্যাঁ” উত্তর বেশি থাকলে model হ্যাঁ বলার দিকে ঝোঁকে।

অংশ ৭-এর ছোট পরীক্ষা এই পদ্ধতির হাতে-বানানো সংস্করণ; সেখানে ২০টি প্রশ্নই, তাই ব্যবধান বড়, আর ফল শুধু এই model আর এই ছবির। প্রশমনের প্রচলিত উপায়: বেশি resolution বা টুকরো (বেশি token), “নিশ্চিত না হলে বলো জানি না” নির্দেশ ও তার মূল্যায়ন, উত্তরের বস্তু আলাদা detector দিয়ে যাচাই, আর গুরুত্বপূর্ণ সিদ্ধান্তে মানুষের পর্যালোচনা।

মূল উপলব্ধি: VLM-এর সাবলীল উত্তর ছবির প্রমাণ নয়; অনুপস্থিত বস্তুর প্রশ্নসহ ভারসাম্যপূর্ণ হ্যাঁ/না সেটে “হ্যাঁ”-র হার মাপা ছাড়া নির্ভরযোগ্যতার দাবি করা যায় না।

প্র ০৪বাংলা prompt আর ছবির বাংলা লেখা — multimodal model-এ ভাষার ভারসাম্যহীনতা কোথা থেকে আসে, আর কী করা যায়?

সমস্যার তিনটি স্তর। প্রথম, ডেটা: আমরা যে CLIP চালিয়েছি তার card বলে training হয়েছে LAION-2B-এর ইংরেজি অংশে। বাংলা caption প্রায় দেখেইনি, তাই বাংলা prompt-এর vector ছবির জায়গার সঙ্গে ভালোভাবে মেলে না।

দ্বিতীয়, tokenizer: CLIP-এর tokenizer byte-level BPE (পর্ব ৪০)। ইংরেজি শব্দ প্রায় এক token, কিন্তু বাংলা অক্ষর UTF-8-এ তিন byte বলে একটি ছোট বাংলা বাক্য অনেক token হয় (আমাদের run-এ “একটি কেডস-এর ছবি” ২৭টি, ইংরেজি বাক্যটি ৬টি)। CLIP-এর লেখার সীমা ৭৭ token, তাই লম্বা বাংলা বিবরণ কেটে যায়।

তৃতীয়, ছবির ভেতরের লেখা: যুক্তাক্ষর, কার-চিহ্নের অবস্থান আর নানা font-এর বাংলা লেখা পড়তে model-কে সেই লিপির অনেক ছবি দেখতে হয়। OpenAI-এর নথিই অ-লাতিন লিপিতে মান কমার কথা বলে; আমাদের ছোট model বাংলা চিহ্নটি পড়তেই পারেনি (অংশ ৭)।

করণীয়: বহুভাষিক model বাছাই (যেমন Carlsson ও সহকর্মীদের Multilingual-CLIP, যা ইংরেজি CLIP-এর text encoder-কে বহুভাষিক encoder দিয়ে distill করে প্রতিস্থাপন করে; বা বহুভাষিক ডেটায় শেখা SigLIP), নিজের ভাষার ছোট মূল্যায়ন সেট বানানো (আমাদের ১০০০ ছবির সেটের মতো), দরকারে ইংরেজিতে অনুবাদ করে প্রশ্ন আর বাংলায় উত্তর, আর ছবির বাংলা লেখার জন্য বাংলায় পরীক্ষিত আলাদা OCR. সব ক্ষেত্রে নিজের ডেটায় মাপুন, কারণ সাধারণ benchmark বাংলা ব্যবহারকারীর পরিস্থিতি বলে না।

মূল উপলব্ধি: বাংলায় দুর্বলতা model-এর “বুদ্ধি”-র নয়, ডেটা, tokenizer আর লিপি-প্রশিক্ষণের ফল; তাই সমাধানও ডেটা আর স্থানীয় মূল্যায়ন দিয়ে শুরু হয়।

প্র ০৫Prompt-এ visual token, নাকি LLM-এর ভেতরে cross-attention — দুটি নকশার trade-off কী?

Flamingo (DeepMind, ২০২২) LLM-কে স্থির রেখে তার স্তরগুলোর মাঝে নতুন gated cross-attention স্তর বসায়; শুরুতে gate প্রায় বন্ধ থাকে বলে training-এর শুরুতে LLM-এর আচরণ বদলায় না। ছবির তথ্য prompt-এর দৈর্ঘ্য বাড়ায় না, আর একটি লেখার মাঝে অনেক ছবি সহজে রাখা যায়। দাম: নতুন স্তরের অনেক parameter, আর LLM-এর গঠন বদলাতে হয়।

LLaVA (২০২৩) উল্টো পথে সরল: শুধু একটি projector, visual token prompt-এর ভেতরে। যেকোনো decoder-only LLM প্রায় না বদলে নেওয়া যায়, কম খরচে শেখানো যায়, আর serving-এর সব কৌশল (KV cache, batching) অপরিবর্তিত থাকে। দাম: প্রতিটি ছবি context-এ শত শত বা হাজার token খায়, তাই বড় ছবি, অনেক ছবি বা video-তে খরচ দ্রুত বাড়ে। এ জন্যই token কমানোর কৌশল এসেছে: BLIP-2-এর Q-Former অল্প কয়েকটি query-তে সংক্ষেপ করে, SmolVLM pixel shuffle দিয়ে ১৬ গুণ কমায়, Qwen-VL ছবির আকার অনুযায়ী token বদলায়।

তৃতীয় একটি ধারা “early fusion”: ছবিকে শুরু থেকেই আলাদা token-এর অভিধানে এনে একটি model-কে ছবি আর লেখা একসঙ্গে শেখানো (যেমন Meta-র Chameleon, ২০২৪, প্রবন্ধ অনুযায়ী)। এতে model ছবি তৈরিও করতে পারে, কিন্তু training কঠিন আর ব্যয়বহুল বলে প্রবন্ধেই অস্থিরতার কথা আছে।

কোনটি “সবচেয়ে ভালো” তার সাধারণ উত্তর নেই; প্রশ্ন হলো ছবি কত বড় ও কতগুলো, লেখা কত পড়তে হবে, LLM বদলানো যাবে কি না, আর serving-এ token-এর খরচ কত সহ্য করা যায়।

মূল উপলব্ধি: নকশার মূল টানাপোড়েন ছবির তথ্য কত token-এ, কোন পথে LLM-এ ঢুকবে; বিস্তারিত বেশি রাখলে ছোট লেখা পড়া যায়, কিন্তু context, মেমরি আর সময়ের দাম দিতে হয়।

অনুশীলন

প্রথমে খাতায় করুন, তারপর উত্তর মেলান। পাতার ব্লক বদলে নিজের সংখ্যাও যাচাই করতে পারেন।

  1. পূর্ণ numerical: (ক) একটি VLM-এর vision encoder ৪৪৮ × ৪৪৮ ছবি নেয়, patch ১৪. কতটি patch? Projector আগে ২ × ২ প্রতিবেশী patch জুড়ে দিলে (pixel shuffle, r = ২) LLM কতটি visual token পায়? (খ) একটি ছবির তিনটি class-prompt-এর সঙ্গে cosine ০.৩০, ০.২৫ আর ০.২০. s = ১০০ আর s = ১০-এ softmax সম্ভাবনা কত?

    (ক) ৪৪৮ ÷ ১৪ = ৩২, তাই ৩২ × ৩২ = ১০২৪টি patch. ২ × ২ জুড়লে grid ১৬ × ১৬, অর্থাৎ ২৫৬টি visual token; প্রতিটির মাপ ৪ গুণ চওড়া (৪d), যা পরে linear layer LLM-এর মাপে আনে।

    (খ) s = ১০০: logit ৩০, ২৫, ২০; সবচেয়ে বড়টি বাদ দিলে e0 = ১, e−5 ≈ ০.০০৬৭৪, e−10 ≈ ০.০০০০৪৫; যোগফল ≈ ১.০০৬৮, তাই ০.৯৯৩৩, ০.০০৬৭, ≈ ০. s = ১০: logit ৩, ২.৫, ২; e0 = ১, e−0.5 ≈ ০.৬০৬৫, e−1 ≈ ০.৩৬৭৯, যোগফল ১.৯৭৪৪, তাই ০.৫০৬৫, ০.৩০৭২, ০.১৮৬৩. উত্তর (argmax) একই, শুধু আত্মবিশ্বাস বদলায়।

  2. সমালোচনা ও নকশা: বাজারঘরের একজন সহকর্মীর প্রস্তাব: “বিক্রেতারা যে পণ্যের ছবি তোলেন, তার দামের ট্যাগ (বাংলায় লেখা) একটি ছোট খোলা VLM পড়ে সরাসরি ডেটাবেসে দাম বসাবে; আর CLIP zero-shot দিয়ে বিভাগ ঠিক হবে। ২০টি ছবিতে দেখেছি, ভালোই কাজ করে।” এই পরিকল্পনার ঝুঁকি কী, আর কীভাবে নিরাপদ করবেন?

    ঝুঁকি: (১) ছোট VLM বাংলা লেখা পড়তে পারে না বা বানিয়ে লেখে (অংশ ৭-এ “দাম ১২০ টাকা” পড়তেই পারেনি), আর ভুল দাম সরাসরি ক্রেতার কাছে যায়। (২) ২০টি ছবি খুব ছোট নমুনা; ব্যবধান বিশাল। (৩) ইংরেজিতে শেখা CLIP বাংলা বিভাগ-নামে দুর্বল, আর ছবির লেখা দেখে ঠকতে পারে (typographic attack). (৪) বিক্রেতার ছবিতে মানুষের মুখ বা ব্যক্তিগত তথ্য থাকতে পারে।

    নিরাপদ নকশা: দাম পড়ার জন্য বাংলায় পরীক্ষিত OCR, তারপর বিক্রেতার কাছে নিশ্চিত করানো (মানুষ শেষ সিদ্ধান্তে); কয়েকশো লেবেল-করা ছবির validation সেট আর আলাদা test সেট, ব্যবধানসহ accuracy; বিভাগের জন্য ইংরেজি prompt ensemble বা অল্প label দিয়ে linear probe (অংশ ৪-এ অনেক ভালো); কম আত্মবিশ্বাসে “মানুষ দেখবেন” সারিতে পাঠানো; আর ছবি সংরক্ষণ ও ব্যক্তিগত তথ্যে দেশের উপাত্ত সুরক্ষা আইন মেনে চলা (প্রয়োজনে আইনজ্ঞের পরামর্শ নিন)।

  3. আউটপুট অনুমান করুন: নিচের কোড কী ছাপবে?
    import numpy as np
    H, P = 384, 16
    print((H // P) ** 2 + 1)
    a = np.array([0.6, 0.8])
    T = np.array([[0.8, 0.6], [0.6, 0.8], [-0.6, 0.8]])
    z = 10 * (T @ a)
    p = np.exp(z - z.max()); p /= p.sum()
    print(np.round(T @ a, 2), np.round(p, 3), p.argmax())
    577
    [0.96 1.   0.28] [0.401 0.598 0.   ] 1

    ৩৮৪ ÷ ১৬ = ২৪, ২৪2 = ৫৭৬, CLS নিয়ে ৫৭৭. সব vector-এর দৈর্ঘ্য ১, তাই dot product-ই cosine: ০.৪৮ + ০.৪৮ = ০.৯৬, ০.৩৬ + ০.৬৪ = ১.০, −০.৩৬ + ০.৬৪ = ০.২৮. s = ১০-এ logit ৯.৬, ১০, ২.৮; প্রথম দুটির পার্থক্য ০.৪, তাই e−0.4 ≈ ০.৬৭ অনুপাতে ০.৪০১ আর ০.৫৯৮; তৃতীয়টি প্রায় শূন্য। জয়ী index ১.

  4. কোড লিখুন: NumPy দিয়ে clip_loss(A, B, s) লিখুন, যা দুটি (N, d) embedding matrix (প্রতিটি সারির i-তম জোড়া মিলে যায়) থেকে দুই দিকের InfoNCE-এর গড় দেয়। দুটি sanity check: সব embedding শূন্য হলে loss ঠিক log N কি না, আর A = B (normalise করা) ও বড় s-এ loss শূন্যের কাছে যায় কি না।
    import numpy as np
    
    def log_softmax(L, axis):
        m = L.max(axis, keepdims=True)
        return L - m - np.log(np.exp(L - m).sum(axis, keepdims=True))
    
    def clip_loss(A, B, s):
        L = s * A @ B.T                       # (N, N), কর্ণ = মিলে যাওয়া জোড়া
        i2t = -np.diag(log_softmax(L, 1)).mean()
        t2i = -np.diag(log_softmax(L, 0)).mean()
        return (i2t + t2i) / 2
    
    N, d = 8, 16
    print(clip_loss(np.zeros((N, d)), np.zeros((N, d)), 100), np.log(N))
    A = np.random.default_rng(0).normal(size=(N, d))
    A /= np.linalg.norm(A, axis=1, keepdims=True)
    print(clip_loss(A, A, 100))

    শূন্য embedding-এ সব logit সমান, তাই প্রতিটি সারিতে সম্ভাবনা ১/N আর loss log ৮ ≈ ২.০৭৯৪, দুটি print একই। A = B হলে কর্ণে cosine ১, বাইরে কম, আর s = ১০০ সেই পার্থক্যকে বিশাল করে, তাই loss প্রায় শূন্য। log_softmax-এ সবচেয়ে বড় মান বাদ দেওয়া জরুরি, নইলে e100 overflow করে।

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

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

ক্রমএকটি vision-language model ছবি দেখে প্রশ্নের উত্তর দেওয়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ছবি resize আর normalise করে pixel-এর tensor বানানো
  2. Vision encoder ছবিকে patch-এ কেটে প্রতিটির vector দেয়
  3. Projector সেই vector-গুলোকে LLM-এর token-এর মাপে আনে
  4. Visual token আর প্রশ্নের লেখার token একটি prompt-এ বসানো
  5. LLM একটি একটি করে উত্তরের token তৈরি করে

একটি ViT ৩৩৬ × ৩৩৬ ছবি নেয়, patch ১৪, আর সামনে একটি CLS token যোগ করে। Transformer-এ মোট কতটি token যায়?

  • ২৪
  • ৫৭৬
  • ৫৭৭
  • ৭৮৪

CLIP-এর training-এ N জোড়া ছবি-caption-এর একটি batch থেকে loss কীভাবে হিসাব হয়?

  • প্রতিটি মিলে যাওয়া জোড়ার vector-এর দূরত্বের বর্গের গড়
  • N × N similarity ছকের সারি আর কলাম দুই দিকে cross-entropy, যেখানে কর্ণ সঠিক উত্তর
  • ছবি দেখে caption-এর প্রতিটি শব্দ অনুমানের next-token loss
  • ১০০০টি স্থির class-এর ওপর সাধারণ classification loss

CLIP দিয়ে zero-shot classification-এ classifier-এর “weight” কোথা থেকে আসে?

  • Label-যুক্ত ছবিতে শেখানো একটি logistic regression থেকে
  • ছবির encoder-এর শেষ layer নতুন করে শেখানো থেকে
  • এলোমেলো শুরু করা vector থেকে
  • প্রতিটি class-এর prompt-এর text embedding থেকে

LLaVA-ধাঁচের vision-language model-এ projector-এর কাজ কী?

  • ছবির vector-কে LLM-এর token embedding-এর মাপে আনা, যাতে visual token prompt-এ বসতে পারে
  • ছবিকে patch-এ কাটা
  • উত্তরের লেখা তৈরি করা
  • প্রশ্নের লেখাকে ছবির জায়গায় অনুবাদ করা

একটি VLM-কে ২০টি ছবিতে “ছবিতে কি একটি X আছে?” প্রশ্ন করা হলো, যার ১০টিতে X নেই। Model ১০টির ৬টিতেই “হ্যাঁ” বলল। এটি কোন সমস্যার লক্ষণ?

  • উপস্থিত বস্তু চিনতে না পারা (কম recall)
  • Tokenizer-এর ভুল
  • Object hallucination: ছবিতে নেই এমন বস্তু “আছে” বলা
  • শুধু গণনার ভুল

CLIP zero-shot-এ logit scale s ১০ থেকে ১০০ করলে কী হয়? (Cosine-গুলো একই থাকে।)

  • সবচেয়ে বেশি সম্ভাবনার class বদলে যায়
  • জয়ী class একই থাকে, কিন্তু সম্ভাবনা আরও এক দিকে জমে (তীক্ষ্ণ হয়)
  • সম্ভাবনাগুলো আরও সমান হয়ে যায়
  • Cosine-গুলো ১০ গুণ বড় হয়

শব্দকোষ

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

মনে রাখুন

  1. ছবি থেকে token: (H ÷ P) × (W ÷ P)টি patch, একই projection, সামনে CLS আর position embedding; ২২৪ পিক্সেলে patch ৩২ মানে ৪৯ + ১ = ৫০টি token.
  2. CLIP: দুটি encoder, একই মাপের normalise করা vector, N × N ছকে দুই দিকের InfoNCE; s = ১/temperature শেখা হয়।
  3. Zero-shot: class-এর prompt-এর text embedding-ই classifier; ensemble সাহায্য করে, কিন্তু অল্প label-এর linear probe প্রায়ই অনেক ভালো। ইংরেজিতে শেখা model বাংলা prompt-এ দুর্বল।
  4. VLM: vision encoder → projector → LLM-এর prompt-এ visual token; প্রতিটি visual token context, মেমরি আর খরচ নেয়।
  5. ব্যর্থতা মাপুন: গণনা, ছোট লেখা, বাংলা লেখা আর অনুপস্থিত বস্তু নিয়ে হাতে label করা প্রশ্নে ব্যবধানসহ হার; পণ্যের সীমা সেই দিনের নথি থেকে।

পরের পর্বে: পর্ব ৮৩ (LLM serving — batching, KV cache, throughput আর খরচ): এখানে দেখা visual token-সহ prompt কীভাবে GPU-তে দ্রুত আর অনেক ব্যবহারকারীর জন্য চালানো হয়, prefill আর decode-এর হিসাবসহ।

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

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