এই পর্বে যা শিখবেন
- Token-এর vector থেকে mask-সহ mean pooling করে বাক্যের vector বানানো, আর CLS-এর সঙ্গে পার্থক্য
- Bi-encoder আর cross-encoder-এর নকশা ও খরচের হিসাব
- In-batch negative আর temperature-সহ InfoNCE loss হাতে আর NumPy-তে
- Normalise, cosine বনাম dot, আর e5-এর “query: ”/“passage: ” উপসর্গ
- BM25 নিজে লিখে, দুটি বহুভাষিক model আর RRF-এর সঙ্গে recall@k, MRR আর bootstrap ব্যবধানে তুলনা করা
১ · Token-এর vector থেকে বাক্যের একটি vector — pooling আর mask
বাজারঘরের হেল্প সেন্টারে প্রতিদিন শত শত প্রশ্ন আসে: “রিফান্ড কত দিনে পাব?”, “refund kotodin e pabo”, “How long does a refund take?” তিনটি একই প্রশ্ন, অথচ একটি শব্দও মিলছে না। একজন অভিজ্ঞ কর্মী তিনটিকেই একই তাকের নথির দিকে পাঠান। আমরা চাই প্রতিটি লেখার জন্য একটি “ঠিকানা”, যাতে একই উদ্দেশ্যের লেখা কাছাকাছি ঠিকানায় থাকে।
AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ এমন একটি model (multilingual-e5-small) সরাসরি ব্যবহার করেছেন, আর recall@k ও MRR হাতে গুনেছেন। এখানে আমরা ভেতরে ঢুকব: vector কীভাবে তৈরি হয়, কীভাবে শেখানো হয়, আর কোথায় শব্দ মেলানো খোঁজ এখনো জেতে।
পর্ব ৪৪-এ দেখেছেন, transformer প্রতিটি token-এর জন্য একটি contextual vector দেয়। ১৩টি token-এর বাক্যে output-এর shape (13, 384). Semantic search-এ দরকার পুরো বাক্যের একটি vector, যাকে বলে sentence embedding. সেখানে পৌঁছানোর ধাপটির নাম pooling. দুটি পরিচিত পথ:
- CLS pooling: প্রথম বিশেষ token-এর vector নেওয়া। Model যদি এভাবে শেখানো হয়, তবেই এটি অর্থবহ।
- Mean pooling: সব আসল token-এর vector-এর গড়। Sentence-BERT ধারায় এটিই সবচেয়ে প্রচলিত (পর্ব ৪৮-এ এর উল্লেখ আছে)।
Batch-এ বাক্যগুলোর দৈর্ঘ্য সমান করতে ছোট বাক্যে pad বসে। Pad-এর ঘরেও model কিছু না কিছু vector দেয়। তাই গড় নেওয়ার সময় attention mask দিয়ে pad বাদ দিতে হয়: mean = ∑t mt·ht ÷ ∑t mt, যেখানে ht হলো t নম্বর token-এর vector আর mt আসল token-এ ১, pad-এ ০.
| ধাপ | |
| মান |
- Input. Transformer-এর output-এর তিনটি আসল token আর একটি pad. Mask বলে কোনটি আসল।
- গুণ. প্রতিটি সারিকে তার mask দিয়ে গুণ। Pad-এর সারি পুরোটা ০ হয়ে গেল।
- যোগ. সারিগুলো যোগ, আর আলাদা করে mask-এর যোগফল: আসল token ৩টি।
- ভাগ. যোগফলকে ৩ দিয়ে ভাগ। এটাই বাক্যের vector, shape (3,).
- ভুল পথ. Mask ভুলে গেলে ৪ দিয়ে ভাগ হয়, আর pad-এর [9, 9, 9] গড়কে টেনে নেয়। কোনো error আসে না, শুধু vector ভুল।
- Normalise. দৈর্ঘ্য দিয়ে ভাগ করলে দৈর্ঘ্য ১. এরপর dot product আর cosine একই সংখ্যা দেয় (অংশ ৪).
একই হিসাব দুটি বাক্যের batch-এ, NumPy-তে। শেষে দেখুন, mask ভুললে দুই বাক্য কতটা “কাছে” দেখায়।
import numpy as np
np.set_printoptions(precision=3, suppress=True)
# batch-এ ২টি বাক্য, প্রতিটি ৪ ঘরে pad করা: shape (2, 4, 3)
H = np.array([[[1.0, 0.0, 2.0], [3.0, 1.0, 0.0], [2.0, 2.0, 1.0], [9.0, 9.0, 9.0]],
[[0.0, 1.0, 1.0], [2.0, 0.0, 3.0], [8.0, 8.0, 8.0], [8.0, 8.0, 8.0]]])
mask = np.array([[1, 1, 1, 0],
[1, 1, 0, 0]]) # ১ = আসল token, ০ = pad
cls = H[:, 0] # প্রথম ঘরের vector
naive = H.mean(axis=1) # pad-ও গড়ে ঢুকে যায়
m = mask[:, :, None].astype(float) # (2, 4, 1): broadcasting-এর জন্য
mean = (H * m).sum(axis=1) / m.sum(axis=1) # শুধু আসল token-এর গড়
print("CLS:\n", cls)
print("mask ছাড়া গড়:\n", naive)
print("mask-সহ গড়:\n", mean)
def unit(x):
return x / np.linalg.norm(x, axis=-1, keepdims=True)
print("দৈর্ঘ্য আগে:", np.linalg.norm(mean, axis=1), " পরে:", np.linalg.norm(unit(mean), axis=1))
print("cos, mask-সহ :", round(float(unit(mean)[0] @ unit(mean)[1]), 3))
print("cos, mask ছাড়া:", round(float(unit(naive)[0] @ unit(naive)[1]), 3))
a, b = mean[0], 10 * mean[1] # দ্বিতীয়টিকে ১০ গুণ লম্বা করলাম
print("dot:", round(float(a @ b), 3), " cos:", round(float(unit(a) @ unit(b)), 3))আউটপুট (পাতার Python-এ চালানো):
CLS:
[[1. 0. 2.]
[0. 1. 1.]]
mask ছাড়া গড়:
[[3.75 3. 3. ]
[4.5 4.25 5. ]]
mask-সহ গড়:
[[2. 1. 1. ]
[1. 0.5 2. ]]
দৈর্ঘ্য আগে: [2.449 2.291] পরে: [1. 1.]
cos, mask-সহ : 0.802
cos, mask ছাড়া: 0.99
dot: 45.0 cos: 0.802
Mask-সহ cosine ০.৮০২, mask ছাড়া ০.৯৯. দুই বাক্যের pad-এর বড় সংখ্যাগুলো দুটি গড়কেই একই দিকে টেনেছে, তাই সব বাক্য একে অন্যের কাছে দেখায়। শেষ লাইনে দ্বিতীয় vector ১০ গুণ লম্বা করলে dot ৪৫ হয়ে যায়, কিন্তু cosine একই থাকে ০.৮০২: cosine শুধু দিক দেখে, দৈর্ঘ্য দেখে না।
সত্যিকারের model-এ কি একই হিসাব চলে? multilingual-e5-small-এর last_hidden_state থেকে নিজে mask-সহ গড় নিয়ে sentence-transformers-এর ফলের সঙ্গে মেলালাম। সত্যিকারের run (CPU-তে; version অংশ ৬-এ):
~/bazarghar-dl$ uv run python pool.py
input_ids (3, 13) | mask row sums [13, 11, 12]
['<s>', '▁que', 'ry', ':', '▁রি', 'ফা', 'ন্ড', '▁কত', '▁দিনে', '▁পা', 'ব', '?', '</s>']
['<s>', '▁que', 'ry', ':', '▁refund', '▁koto', 'din', '▁e', '▁pa', 'bo', '</s>', '<pad>', '<pad>']
H (3, 13, 384) | max |own mean - sentence-transformers| = 5.22e-08
cos(own mask mean, no-mask mean) per sentence: [1.0, 0.998, 0.9986]
cos matrix, mask mean:
[[1. 0.846 0.886]
[0.846 1. 0.859]
[0.886 0.859 1. ]]
cos matrix, CLS:
[[1. 0.918 0.948]
[0.918 1. 0.93 ]
[0.948 0.93 1. ]]
(1): Pooling({... 'pooling_mode': 'mean' ...})
(2): Normalize() # … SentenceTransformer-এর গঠন, ছাঁটা
চারটি কথা। এক, নিজের গড় আর library-র ফলের পার্থক্য ৫.২২ × ১০−৮, অর্থাৎ library ঠিক এই হিসাবই করে; model-এর গঠনে লেখা আছে pooling_mode: mean আর তারপর Normalize. দুই, tokenizer “রিফান্ড”-কে তিন টুকরো করেছে, আর Banglish বাক্যে ২টি pad বসেছে। তিন, এখানে pad কম, তাই mask ছাড়া গড়ও প্রায় একই (০.৯৯৮); লম্বা আর ছোট বাক্য একসঙ্গে থাকলে পার্থক্য বাড়ে। চার, এই model mean pooling দিয়ে শেখানো; CLS নিলে তিন ভাষার cosine আরও বেশি (০.৯১৮–০.৯৪৮) দেখায়, কিন্তু উঁচু মান মানেই ভালো নয়, কারণ অন্য সব বাক্যও তখন কাছে আসতে পারে। Model যে pooling-এ শেখানো, সেটিই ব্যবহার করুন; model card আর modules.json তা বলে দেয় (পর্ব ৫৩)।
২ · Bi-encoder বনাম cross-encoder — কে কখন পড়ে
প্রশ্ন আর নথির মিল মাপার দুটি নকশা আছে।
- Bi-encoder: প্রশ্ন আর নথি আলাদা আলাদা একই encoder দিয়ে যায়; দুটি vector, তারপর একটি cosine. নথির vector আগেই একবার বানিয়ে রাখা যায়।
- Cross-encoder: প্রশ্ন আর নথি জুড়ে একটি input, “[CLS] প্রশ্ন [SEP] নথি [SEP]”. Self-attention-এ প্রশ্নের প্রতিটি token নথির প্রতিটি token দেখে, আর শেষে একটি score আসে। নির্ভুলতা প্রায়ই বেশি, কিন্তু আগে থেকে কিছু হিসাব করে রাখা যায় না।
খরচের হিসাবটাই আসল পার্থক্য। আমাদের run-এ (অংশ ৬) e5-small CPU-তে একটি নথি encode করতে গড়ে প্রায় ২৯ ms নিয়েছে। ধরুন ১,০০,০০০টি নথি।
- Bi-encoder: নথিগুলো একবারই encode: ১,০০,০০০ × ২৯ ms ≈ ৪৮ মিনিট, আগেভাগে। প্রতিটি প্রশ্নে একবার encode (প্রায় ২৯ ms) আর ১,০০,০০০ × ৩৮৪টি গুণ-যোগ, অর্থাৎ প্রায় ৩.৮ কোটি; একই যন্ত্রে NumPy-তে float32-এ এই গুণ মেপে পেয়েছি প্রায় ৪ ms.
- Cross-encoder: প্রতিটি প্রশ্নে ১,০০,০০০টি জোড়া, প্রতিটিতে একটি পুরো forward pass. একই গতি ধরলে প্রতি প্রশ্নে প্রায় ৪৮ মিনিট। এটি অচল।
তাই বাস্তবে দুটি একসঙ্গে: bi-encoder (বা BM25) দিয়ে হাজারের মধ্যে প্রথম ৫০–১০০টি, তারপর শুধু সেগুলোতে cross-encoder দিয়ে নতুন ক্রম। একে বলে reranking; Nogueira আর Cho-র ২০১৯ সালের কাজ BERT দিয়ে এই ধাপকে পরিচিত করে। বিস্তারিত আর সত্যিকারের reranker আসবে পর্ব ৬৯-এ (RAG — chunking, retrieval, reranking আর উত্তর তৈরি)। লাখ লাখ vector-এ দ্রুত খোঁজার কৌশল পর্ব ৬৮-এ (Vector index — FAISS, approximate nearest neighbor আর vector database). মাঝামাঝি একটি পথও আছে: ColBERT (Khattab ও Zaharia, ২০২০) প্রতিটি token-এর vector রাখে আর খোঁজার সময় token-স্তরে মেলায়; জায়গা বেশি লাগে।
৩ · Contrastive শেখা — Sentence-BERT, in-batch negative আর InfoNCE
সাধারণ BERT-এর vector গড় করলেই কি ভালো sentence embedding পাওয়া যায়? Reimers আর Gurevych-এর Sentence-BERT প্রবন্ধ (EMNLP ২০১৯) জানায়, তাঁদের পরীক্ষায় BERT-এর গড় vector বা CLS vector বাক্যের মিল মাপায় বেশ দুর্বল ছিল, কিছু ক্ষেত্রে GloVe-এর গড়ের চেয়েও খারাপ। সমাধান: একই encoder দুই বাক্যে চালিয়ে (siamese গঠন), জোড়ার label দিয়ে fine-tune করা, যাতে মিলের বাক্য কাছে আসে। এভাবে শেখার সাধারণ নাম contrastive learning; মূল ধারণা আর triplet loss আছে পর্ব ১৩-এ।
Retrieval-এর জন্য এখন বহুল প্রচলিত loss হলো InfoNCE (নামটি van den Oord ও সহকর্মীদের ২০১৮ সালের প্রবন্ধ থেকে)। একটি batch-এ B জোড়া (প্রশ্ন qi, সঠিক নথি di). প্রতিটি প্রশ্নের জন্য batch-এর বাকি B − ১টি নথিকে ভুল উত্তর ধরা হয়: এগুলো in-batch negative, আলাদা করে খুঁজতে হয় না। Henderson ও সহকর্মীরা (২০১৭) উত্তর-সুপারিশে আর DPR (Karpukhin ও সহকর্মী, ২০২০) প্রশ্নোত্তরের retrieval-এ এই কৌশল ব্যবহার করেন।
Li = −log [ exp(sii/τ) ÷ ∑j exp(sij/τ) ]
এখানে sij = cos(qi, dj), আর τ হলো temperature, একটি ছোট ধনাত্মক সংখ্যা। লক্ষ্য করুন, এটি B শ্রেণির cross-entropy, যেখানে “সঠিক শ্রেণি” হলো নিজের জোড়া। e5-এর model card লিখেছে, তাদের InfoNCE-তে τ = ০.০১; সেই কারণে e5-এর cosine প্রায় সবসময় ০.৭–১-এর ঘরে থাকে, আর মানের চেয়ে ক্রম বেশি অর্থবহ।
| ধাপ | |
| মান |
- Similarity. ৩টি প্রশ্ন × ৩টি নথি। q1-এর সঠিক নথি d1 (০.৮), কিন্তু d2-ও কাছে (০.৭): একটি কঠিন negative.
- Temperature. τ = ০.১ দিয়ে ভাগে পার্থক্য ১০ গুণ বড়: ০.১-এর ফারাক এখন ১.
- Exp. Softmax-এর লব আর হর। d3 প্রায় কিছুই যোগ করে না।
- Loss. সঠিক নথির সম্ভাবনা ০.৭৩০৬, loss ০.৩১৪. Gradient d1-কে q1-এর দিকে টানবে আর d2-কে ঠেলবে।
- Batch. q2 সহজ (০.০০৩), q3 মাঝারি। Batch-এর loss তিনটির গড় ০.১৫০.
- τ = ১. একই S-এ সম্ভাবনা মাত্র ০.৪১৬, গড় loss ০.৮২৬. বড় τ-তে সব নথি প্রায় সমান দেখায়; ছোট τ পার্থক্য ধারালো করে।
এবার একটু বড় batch, এলোমেলো vector-এ, তিনটি τ দিয়ে।
import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(42)
def unit(x):
return x / np.linalg.norm(x, axis=1, keepdims=True)
B, d = 4, 8
Qv = unit(rng.normal(size=(B, d))) # ৪টি প্রশ্নের vector
Dv = unit(Qv + 0.6 * rng.normal(size=(B, d))) # প্রতিটির সঠিক নথি: একটু সরানো
def infonce(Q, D, tau):
S = Q @ D.T / tau # (B, B): সারি প্রশ্ন, কলাম নথি
S = S - S.max(axis=1, keepdims=True) # exp যেন overflow না করে
logp = S - np.log(np.exp(S).sum(axis=1, keepdims=True))
return -np.diag(logp).mean(), np.exp(np.diag(logp))
print("cos matrix (কর্ণে সঠিক জোড়া):\n", Qv @ Dv.T)
for tau in (1.0, 0.1, 0.05):
L, p = infonce(Qv, Dv, tau)
print(f"tau={tau:<4} loss={L:.3f} P(সঠিক)={p}")
R = unit(rng.normal(size=(B, d))) # এলোমেলো নথি, কোনো মিল নেই
print("এলোমেলো নথি, tau=1:", round(float(infonce(Qv, R, 1.0)[0]), 3), " log B =", round(float(np.log(B)), 3))
# একটি ধাপ: সঠিক নথিকে প্রশ্নের দিকে একটু টানলে loss কমে কি?
D2 = unit(Dv + 0.3 * (Qv - Dv))
print("টানার পরে, tau=0.1:", round(float(infonce(Qv, D2, 0.1)[0]), 3))আউটপুট (পাতার Python-এ চালানো):
cos matrix (কর্ণে সঠিক জোড়া):
[[ 0.844 -0.048 0.909 0.263]
[ 0.118 0.706 0.32 -0.029]
[ 0.371 0.292 0.696 0.059]
[-0.148 0.692 0.166 0.398]]
tau=1.0 loss=1.123 P(সঠিক)=[0.329 0.368 0.343 0.269]
tau=0.1 loss=1.037 P(সঠিক)=[0.342 0.976 0.945 0.05 ]
tau=0.05 loss=1.856 P(সঠিক)=[0.214 1. 0.998 0.003]
এলোমেলো নথি, tau=1: 1.53 log B = 1.386
টানার পরে, tau=0.1: 0.342
τ = ১-এ সব সম্ভাবনা ০.২৫-এর (চার ভাগের এক) কাছে: model প্রায় কিছুই আলাদা করছে না। τ = ০.১-এ সারি ২ আর ৩ প্রায় নিশ্চিত, কিন্তু সারি ১-এ নথি ৩ (০.৯০৯) সঠিক নথির (০.৮৪৪) চেয়ে কাছে, আর সারি ৪-এ নথি ২ (০.৬৯২) সঠিকটির (০.৩৯৮) চেয়ে অনেক কাছে। τ = ০.০৫-এ এই ভুল সারিগুলোর শাস্তি আরও বাড়ে, সারি ৪-এর সম্ভাবনা ০.০০৩, তাই গড় loss বেড়ে ১.৮৫৬. ছোট τ মানে ভুল ক্রমের ওপর কড়া চাপ, যা gradient-কে ঠিক সেই জোড়াগুলোর দিকে মনোযোগী করে। এলোমেলো নথিতে loss ১.৫৩, log B = ১.৩৮৬-এর কাছে: আন্দাজের মাত্রা। সঠিক নথিগুলোকে প্রশ্নের দিকে একটু টানতেই τ = ০.১-এ loss ১.০৩৭ থেকে ০.৩৪২. প্রশিক্ষণ মানে লক্ষ লক্ষ জোড়ায় এই টানাটানি, gradient দিয়ে encoder-এর weight বদলে।
Batch বড় হলে কী হয়: negative বাড়ে, loss-এর কাজ কঠিন হয়, আর শেখা সাধারণত ভালো হয় বলে অনেক প্রবন্ধে জানানো হয়েছে; দামটি GPU memory-তে। ঝুঁকিও আছে: batch-এর অন্য একটি নথি আসলে সঠিক উত্তর হতে পারে (false negative), যেমন একই নীতির দুটি নথি। আলাদা করে খুঁজে আনা কাছাকাছি-কিন্তু-ভুল নথিকে বলে hard negative; এগুলো যোগ করলে model সূক্ষ্ম পার্থক্য শেখে। SimCSE (Gao ও সহকর্মী, ২০২১) দেখায়, শুধু dropout-এর দুটি ভিন্ন রূপকে জোড়া ধরেও কাজ চলে।
৪ · Normalise, cosine না dot — আর “query:” / “passage:”
Cosine = a·b ÷ (‖a‖‖b‖). দুই vector-এর দৈর্ঘ্য ১ হলে হর ১, তাই cosine আর dot product হুবহু সমান। এজন্য index বানানোর আগে সব vector normalise করে রাখা হয়; তারপর খোঁজ মানে শুধু একটি matrix গুণ (AI-এর ভিত্তি কোর্সের পর্ব ৩৩-এ এর ভিত্তি)। কিন্তু model যদি normalise না করা vector দেয়, আর আপনি dot দিয়ে খোঁজেন, তবে লম্বা vector-এর নথি সব প্রশ্নে এগিয়ে যায়। কিছু model ইচ্ছে করে dot-এর জন্য শেখানো হয় (dot-এ শেখানো DPR-এর মতো); কোন মাপে শেখানো, model card দেখে সেটিই ব্যবহার করুন।
দ্বিতীয় বিষয় asymmetric search: প্রশ্ন ছোট (“রিফান্ড কবে?”), নথি লম্বা অনুচ্ছেদ। দুটি একই রকম লেখা নয়। e5 পরিবার এটি সামলায় উপসর্গ দিয়ে: card অনুযায়ী প্রশ্নের আগে "query: ", নথির আগে "passage: "; card স্পষ্ট বলে, না দিলে মান কমবে। দুটি বাক্যের মিল মাপার মতো symmetric কাজে দুই দিকেই "query: ". অন্য model-এর নিয়ম ভিন্ন হতে পারে, যেমন কিছু model নির্দেশনার বাক্য চায়, কিছু model কিছুই চায় না।
Card-এর কথা আমাদের বাংলা প্রশ্নে কতটা খাটে, তা মেপে দেখেছি। সত্যিকারের run, অংশ ৬-এর একই ৪০টি নথি আর ৬০টি প্রশ্নে:
~/bazarghar-dl$ uv run python run.py # অংশ ৬-এর একই run, ছাঁটা
MiniLM-L12: dim 384, params 117.7M, max_seq 128, pooling mean, docs 20.8 ms/doc CPU
MiniLM unnormalised doc norms: min 1.63 max 4.17
system recall@1 recall@5 MRR@10
e5-small 0.517 0.817 0.651
e5-small no-prefix 0.550 0.850 0.677
MiniLM-L12 0.067 0.308 0.172
MiniLM-L12 dot-unnorm 0.050 0.233 0.125
e5-small - e5-small no-prefix: -0.026 [-0.080, +0.027] wins 7 losses 12 ties 41
দুটি অবাক করা ফল। এক, e5-small-এ উপসর্গ বাদ দিলে এই ৬০টি প্রশ্নে MRR@১০ সামান্য বেশি (০.৬৭৭ বনাম ০.৬৫১), কিন্তু paired ব্যবধান [−০.০৮০, +০.০২৭] শূন্য পেরিয়ে যায়: ১২টি প্রশ্নে উপসর্গ ছাড়া ভালো, ৭টিতে খারাপ, ৪১টিতে সমান। তাই card-এর কথা এখানে ভুল প্রমাণিত হয়নি, আবার সমর্থনও পায়নি; ছোট, বানানো সংগ্রহে এটাই স্বাভাবিক। আমরা card-এর নিয়মই রাখছি, কারণ model ওভাবেই শেখানো। দুই, MiniLM-এর গঠনে কোনো Normalize ধাপ নেই; নথির vector-এর দৈর্ঘ্য ১.৬৩ থেকে ৪.১৭. Normalise না করে সরাসরি dot নিলে MRR@১০ ০.১৭২ থেকে ০.১২৫-এ নামে, কারণ লম্বা vector-এর নথি সব প্রশ্নে এগিয়ে আসে। MiniLM-এর নিজের মান কেন এত কম, তা অংশ ৬-এ।
৫ · BM25 — শব্দ মেলানোর খোঁজ, নিজে লেখা
Embedding-এর সঙ্গে তুলনার জন্য একটি শক্ত baseline দরকার। শব্দভিত্তিক খোঁজে বহু বছর ধরে মানদণ্ড BM25; এটি Stephen Robertson ও সহকর্মীদের Okapi ব্যবস্থা থেকে, ১৯৯০-এর দশকের TREC মূল্যায়নে পরিচিতি পায়। Inverted index আর ranking-এর ব্যবস্থাগত দিক আছে সিস্টেম ডিজাইন কোর্সের পর্ব ২৭-এ। সূত্রটি:
score(q, d) = ∑w ∈ q IDF(w) · tf·(k₁ + 1) ÷ (tf + k₁·(1 − b + b·|d| ÷ avgdl))
এখানে tf হলো নথি d-তে শব্দ w কতবার আছে, |d| নথির দৈর্ঘ্য (শব্দ-সংখ্যা), avgdl সব নথির গড় দৈর্ঘ্য, আর IDF(w) = ln(১ + (N − df + ০.৫) ÷ (df + ০.৫)), যেখানে N মোট নথি আর df হলো কয়টি নথিতে শব্দটি আছে। k₁ (সাধারণত ১.২–২) ঠিক করে, একই শব্দ বারবার থাকলে score কত দ্রুত থেমে যায়; b (সাধারণত ০.৭৫) লম্বা নথিকে কতটা শাস্তি দেবে। TF-IDF-এর সঙ্গে পার্থক্য এই দুটি: tf-এর প্রভাব একটা সীমায় থামে, আর দৈর্ঘ্যের সমন্বয় আছে। IDF-এর এই রূপটি Lucene-এ ব্যবহৃত; মূল সূত্রে “১ +” নেই, তাই খুব সাধারণ শব্দে IDF ঋণাত্মক হতে পারত।
import math
docs = {"bh04": "ঢাকা সিটির ভেতরে ডেলিভারি চার্জ ৬০ টাকা, ১৫০০ টাকার বেশি অর্ডারে ফ্রি",
"bh05": "গাজীপুর নারায়ণগঞ্জ সাভার আর কেরানীগঞ্জে ডেলিভারি চার্জ ৯০ টাকা",
"bh06": "ঢাকা আর আশপাশের বাইরে সব জেলায় ডেলিভারি চার্জ ১২০ টাকা",
"bh07": "ঢাকা সিটিতে ১ থেকে ২ দিনে আর অন্যান্য জেলায় ৩ থেকে ৫ দিনে ডেলিভারি হয়",
"bh09": "ক্যাশ অন ডেলিভারি সর্বোচ্চ ২০০০০ টাকার অর্ডার পর্যন্ত চলে"}
tok = lambda s: s.replace(",", " ").replace("?", " ").split()
D = {k: tok(v) for k, v in docs.items()}
N, avgdl = len(D), sum(len(d) for d in D.values()) / len(D)
df = {}
for d in D.values():
for w in set(d):
df[w] = df.get(w, 0) + 1
def idf(w):
return math.log((N - df[w] + 0.5) / (df[w] + 0.5) + 1)
def bm25(query, k1=1.5, b=0.75):
scores = {}
for k, d in D.items():
s = 0.0
for w in tok(query):
tf = d.count(w)
if tf:
s += idf(w) * tf * (k1 + 1) / (tf + k1 * (1 - b + b * len(d) / avgdl))
scores[k] = round(s, 3)
return sorted(scores.items(), key=lambda x: -x[1])
print("avgdl =", avgdl, " idf(ডেলিভারি) =", round(idf("ডেলিভারি"), 3), " idf(সিটির) =", round(idf("সিটির"), 3))
for q in ["ঢাকা সিটির চার্জ কত?", "ঢাকায় ডেলিভারি চার্জ কত?", "রংপুরে ডেলিভারি চার্জ কত?"]:
print(q, bm25(q)[:3])আউটপুট (পাতার Python-এ চালানো):
avgdl = 11.0 idf(ডেলিভারি) = 0.087 idf(সিটির) = 1.386
ঢাকা সিটির চার্জ কত? [('bh04', 2.367), ('bh06', 1.124), ('bh05', 0.587)]
ঢাকায় ডেলিভারি চার্জ কত? [('bh05', 0.682), ('bh06', 0.653), ('bh04', 0.601)]
রংপুরে ডেলিভারি চার্জ কত? [('bh05', 0.682), ('bh06', 0.653), ('bh04', 0.601)]
“ডেলিভারি” ৫টি নথির ৫টিতেই আছে, তাই IDF মাত্র ০.০৮৭; “সিটির” আছে একটিতে, IDF ১.৩৮৬. প্রথম প্রশ্নে “ঢাকা” আর “সিটির” মিলে bh04 পরিষ্কারভাবে প্রথম। দ্বিতীয় প্রশ্নে “ঢাকায়” লিখতেই কোনো নথির সঙ্গে মিলল না, কারণ নথিতে আছে “ঢাকা”, “সিটিতে”. শুধু সাধারণ শব্দ মিলেছে, আর সবচেয়ে ছোট নথি bh05 জিতেছে। তৃতীয় প্রশ্নের ফল হুবহু দ্বিতীয়টির মতো: “রংপুরে” কোথাও নেই, তাই BM25-এর চোখে দুই প্রশ্ন একই। বাংলার বিভক্তি (-য়, -এ, -র, -তে) শব্দভিত্তিক খোঁজের বড় শত্রু; stemming বা character n-gram কিছুটা সাহায্য করে (AI-এর ভিত্তি কোর্সের পর্ব ৬৭)।
৬ · সত্যিকারের তুলনা — BM25 বনাম দুটি model, ব্যবধানসহ
ডেটা. এই মডিউলের সব পর্বে একই সংগ্রহ: বাজারঘরের ৪০টি হেল্প-নথি (৩৫টি বাংলা, ৩টি ইংরেজি, ২টি Banglish), ফেরত, রিফান্ড, জেলাভিত্তিক ডেলিভারি চার্জ, পেমেন্ট, COD, ওয়ারেন্টি ইত্যাদি নিয়ে। নথি, নীতি আর সংখ্যা সবই কোর্সের জন্য হাতে বানানো; কোনো আসল প্রতিষ্ঠানের নয়। একটি নথি (bh40) পুরোনো, “archived” চিহ্নিত, ইচ্ছে করে রাখা। প্রশ্ন ৬০টি, হাতে লেখা, প্রতিটির সঠিক (gold) নথিসহ; দুটি প্রশ্নের gold নথি দুটি। প্রশ্নের ধরন: সাধারণ বাংলা ২৪, জায়গার নাম ৬, সংখ্যা ৫, “না”-বাচক ৫, বাংলা প্রশ্নে ইংরেজি বা Banglish নথি ৭, ইংরেজি প্রশ্ন ৪, Banglish প্রশ্ন ৯.
| Checkpoint | লাইসেন্স (card) | মাত্রা, সর্বোচ্চ token | উপসর্গ, pooling |
|---|---|---|---|
intfloat/ | MIT | ৩৮৪, ৫১২ | “query: ”/“passage: ”, mean |
sentence-transformers/ | Apache 2.0 | ৩৮৪, ১২৮ | নেই, mean; card-এর ভাষার তালিকায় bn নেই |
দুটি model একই ভিত্তি model (Multilingual-MiniLM-L12-H384) থেকে শুরু, তাই parameter সংখ্যাও সমান; পার্থক্য শেখার ডেটা আর পদ্ধতিতে। তৃতীয় একটি বড় model (multilingual-e5-base, ফাইল প্রায় ১.১ GB) চালানোর পরিকল্পনা ছিল; নামানোর গতি খুব কম থাকায় বাদ দিয়েছি, তার কোনো সংখ্যা এখানে নেই। লেখার সময়ের version: Python 3.14.4, torch 2.15.0 (CPU), transformers 5.22.2, sentence-transformers 6.2.0; model-এর revision পিন করা, সব কাজ CPU-তে। Recall@k-এর সংজ্ঞা recall@k = প্রথম k-এ gold নথির ভাগ; MRR@১০-এ প্রথম ১০-এর বাইরে হলে ০. ব্যবধান আসে paired bootstrap থেকে: একই প্রশ্নগুলো সব system-এর জন্য একসঙ্গে টানা। মূল অংশ, সংক্ষিপ্ত:
m = SentenceTransformer(name, revision=rev, device="cpu")
P = m.encode(["passage: " + t for t in texts], normalize_embeddings=True) # (40, 384)
Qe = m.encode(["query: " + q for q in qs], normalize_embeddings=True) # (60, 384)
ranks = np.argsort(-(Qe @ P.T), axis=1, kind="stable") # cosine = dot
idx = rng.integers(0, 60, size=(10000, 60)) # একই প্রশ্ন-সূচক সব system-এ: paired
lo, hi = np.percentile(d[idx].mean(1), [2.5, 97.5]) # d = দুই system-এর প্রশ্নভিত্তিক পার্থক্য~/bazarghar-dl$ uv run python run.py
python 3.14.4, torch 2.15.0+cpu, transformers 5.22.2, sentence-transformers 6.2.0
docs 40 {'bn': 35, 'en': 3, 'banglish': 2} | questions 60 {'para': 24, 'dist': 6, 'xl': 7, 'num': 5, 'neg': 5, 'en': 4, 'bl': 9}
BM25 k1=1.5 b=0.75, avg doc len 38.1 tokens, queries with zero score everywhere: 1
e5-small: dim 384, params 117.7M, max_seq 512, pooling mean, docs 28.7 ms/doc CPU
MiniLM-L12: dim 384, params 117.7M, max_seq 128, pooling mean, docs 20.8 ms/doc CPU
MiniLM unnormalised doc norms: min 1.63 max 4.17
system recall@1 recall@5 MRR@10
BM25 0.392 0.667 0.502
e5-small 0.517 0.817 0.651
MiniLM-L12 0.067 0.308 0.172
RRF BM25+e5-small 0.483 0.717 0.598
RRF BM25>0+e5-small 0.483 0.750 0.612
95% bootstrap CI (10,000 resamples of the 60 questions, seed 42)
BM25 MRR 0.502 [0.394, 0.612] recall@5 0.667 [0.550, 0.783]
e5-small MRR 0.651 [0.552, 0.746] recall@5 0.817 [0.717, 0.917]
MiniLM-L12 MRR 0.172 [0.106, 0.247] recall@5 0.308 [0.200, 0.425]
paired differences in MRR
e5-small - BM25: +0.149 [+0.047, +0.253] wins 26 losses 12 ties 22
MiniLM-L12 - BM25: -0.330 [-0.442, -0.217] wins 5 losses 40 ties 15
e5-small - MiniLM-L12: +0.479 [+0.375, +0.580] wins 48 losses 4 ties 8
recall@5 by question type
system para dist num neg xl en bl
n 24 6 5 5 7 4 9
BM25 0.83 0.50 1.00 1.00 0.00 0.75 0.44
e5-small 0.96 0.50 0.60 1.00 0.43 1.00 0.89
MiniLM-L12 0.21 0.50 0.40 0.60 0.00 0.75 0.28
e5-small স্পষ্টভাবে BM25-এর চেয়ে ভালো: MRR@১০-এ +০.১৪৯, ব্যবধান [+০.০৪৭, +০.২৫৩] শূন্য ছোঁয় না; ২৬টি প্রশ্নে জয়, ১২টিতে হার। MiniLM BM25-এরও অনেক নিচে (০.১৭২)। লেখার সময়ে এর card-এর ভাষার তালিকায় bn নেই। Tokenizer একই, তাই বাংলা লেখা ঠিকই টুকরো হয়; কিন্তু শেখার সময় বাংলার জোড়া না থাকলে vector অর্থের কাছাকাছি সাজে না। ইংরেজি প্রশ্নে এর recall@৫ ০.৭৫, বাংলা প্রশ্নে ০.২১. “Multilingual” নাম দেখে নয়, card-এর তালিকায় নিজের ভাষা দেখে model বাছুন। ধরনভিত্তিক ছবি: BM25 সংখ্যা আর “না”-বাচক প্রশ্নে ১.০০, কারণ নির্দিষ্ট শব্দ মিলেছে, কিন্তু ভাষা-পেরোনো প্রশ্নে ০. e5 ভাষা-পেরোনোয় ০.৪৩, Banglish-এ ০.৮৯, অথচ সংখ্যায় ০.৬০. প্রতিটি ধরনে মাত্র ৪–৯টি প্রশ্ন, তাই এই ভাগগুলো দিক দেখায়, প্রমাণ নয়। পুরো ফলই একটি run, একটি বানানো সংগ্রহ, আর প্রশ্নগুলো লেখকের নিজের লেখা; এটি সাধারণ benchmark নয়।
৭ · Hybrid search — reciprocal rank fusion
BM25-এর score ০ থেকে ১৪, e5-এর ০.৭৮ থেকে ০.৯০: দুটি যোগ করা অর্থহীন। Reciprocal rank fusion (RRF; Cormack, Clarke আর Büttcher, SIGIR ২০০৯) শুধু অবস্থান দেখে। প্রতিটি নথির score = ∑ ১/(k + rank), যেখানে rank প্রতিটি তালিকায় তার অবস্থান (১ থেকে শুরু), আর k একটি ধ্রুবক; প্রবন্ধে k = ৬০. Rank ১ আর rank ২-এর পার্থক্য ছোট রাখে বলে একটি তালিকার একটি ভুল পুরো ফল উল্টে দিতে পারে না। এভাবে দুই খোঁজ মেলানোকে বলে hybrid search.
| ধাপ | |
| মান |
- দুই তালিকা. BM25 “ডেলিভারি চার্জ” শব্দে ঢাকার নথি (bh04) আর পুরোনো নথি (bh40) আগে এনেছে; e5 সঠিক নথি bh05 প্রথমে এনেছে।
- bh05. দুই তালিকার অবদান যোগ: ০.০৩২২৬৬.
- bh04. একটিতে প্রথম, অন্যটিতে চতুর্থ: ০.০৩২০১৮. সামান্য পিছিয়ে।
- সমান. bh28 আর bh40 উল্টো অবস্থানে, তাই score হুবহু সমান; সমান হলে ক্রম ঠিক করে বাস্তবায়নের নিয়ম।
- ফল. সঠিক নথি প্রথমে। দুই তালিকা মোটামুটি একমত হলে RRF দুটির ভালো দিক নেয়।
- ফাঁদ. আরেকটি প্রশ্নে BM25 মাত্র একটি নথিতে কিছু মিলিয়েছে; বাকিরা score ০ নিয়েও তালিকায় থাকে আর RRF-এর পয়েন্ট পায়। তাই score ০-এর নথি বাদ দিয়ে তালিকা বানানো উচিত।
এবার সব ৬০টি প্রশ্নে, পাতাতেই। নিচের ফাইলে প্রতিটি প্রশ্নের জন্য সত্যিকারের run-এর BM25 আর e5-small-এর প্রথম ১০টি নথির নম্বর (bh বাদ দিয়ে) আর BM25-এ কয়টি নথির score ০-এর বেশি, তা দেওয়া আছে। Model এখানে চলছে না; শুধু ক্রম জোড়া লাগানো আর মাপা।
import numpy as np
import pandas as pd
cols = ["type", "gold", "bm25", "npos", "e5"]
df = pd.read_csv("ranks.tsv", sep="\t", header=None, names=cols, dtype=str)
for c in ["gold", "bm25", "e5"]:
df[c] = df[c].str.split(r"[ ,]") # "04 06" → ["04", "06"]
df["bm25+"] = [l[:int(n)] for l, n in zip(df.bm25, df.npos)] # শুধু score > 0
def rrf(a, b, k=60):
s = {}
for lst in (a, b):
for pos, d in enumerate(lst): # pos 0 মানে rank 1
s[d] = s.get(d, 0) + 1 / (k + pos + 1)
return sorted(s, key=lambda d: -s[d])
df["rrf"] = [rrf(a, b) for a, b in zip(df.bm25, df.e5)]
df["rrf>0"] = [rrf(a, b) for a, b in zip(df["bm25+"], df.e5)]
def recall(g, lst, k):
return len(set(g) & set(lst[:k])) / len(g)
def rr(g, lst): # MRR@10: প্রথম ১০-এ না থাকলে ০
hit = [i for i, d in enumerate(lst[:10]) if d in g]
return 1 / (hit[0] + 1) if hit else 0.0
names = ["bm25", "e5", "rrf", "rrf>0"]
rows = {}
for s in names:
df[s + "@5"] = [recall(g, l, 5) for g, l in zip(df.gold, df[s])]
rows[s] = {"recall@1": np.mean([recall(g, l, 1) for g, l in zip(df.gold, df[s])]),
"recall@5": df[s + "@5"].mean(),
"MRR@10": np.mean([rr(g, l) for g, l in zip(df.gold, df[s])])}
print(pd.DataFrame(rows).T.round(3))
print(df.groupby("type")[[s + "@5" for s in names]].mean().round(2))আউটপুট (পাতার Python-এ চালানো):
recall@1 recall@5 MRR@10
bm25 0.392 0.667 0.502
e5 0.517 0.817 0.651
rrf 0.475 0.717 0.603
rrf>0 0.475 0.783 0.614
bm25@5 e5@5 rrf@5 rrf>0@5
type
bl 0.44 0.89 0.56 0.89
dist 0.50 0.50 0.50 0.50
en 0.75 1.00 1.00 0.75
neg 1.00 1.00 1.00 1.00
num 1.00 0.60 0.60 0.80
para 0.83 0.96 0.92 0.92
xl 0.00 0.43 0.14 0.29
ফলটি আশা জাগানোর মতো নয়, আর সেটাই শেখার বিষয়। Hybrid (rrf) e5-এর চেয়ে খারাপ: MRR@১০ ০.৬০৩ বনাম ০.৬৫১. ধরন ভাগ করলে কারণ দেখা যায়। ভাষা-পেরোনো (xl) প্রশ্নে BM25 শূন্য, তাই মিশ্রণ e5-এর ঠিক উত্তরকে নিচে নামায় (০.৪৩ থেকে ০.১৪)। Banglish (bl)-এ score ০-এর নথিগুলো তালিকা ভরিয়ে ফেলে; সেগুলো বাদ দিলে (rrf>0) ০.৫৬ থেকে ০.৮৯-এ ফেরে, আর সংখ্যার প্রশ্নে (num) BM25-এর সাহায্যে ০.৬০ থেকে ০.৮০. তবু মোটের ওপর rrf>0 (০.৬১৪) e5-কে ছাড়ায়নি। এখানে শুধু প্রথম ১০টি নথি থেকে মেশানো হয়েছে; ডেস্কটপে পুরো তালিকা দিয়ে মান সামান্য আলাদা (০.৫৯৮ আর ০.৬১২, অংশ ৬)। শিক্ষা: দুটি খোঁজ মেশালে দুর্বলটির ভুলও ঢোকে; ধরনভিত্তিক মাপ ছাড়া hybrid চালু করবেন না, আর weighted RRF বা শুধু কিছু ধরনের প্রশ্নে BM25 ব্যবহারের মতো বিকল্প আলাদা যাচাই-ডেটায় পরীক্ষা করুন।
৮ · ভাষা, লিপি আর ব্যর্থতার জায়গা — সংখ্যা, “না” আর জেলার নাম
গড় সংখ্যা লুকিয়ে রাখে কোথায় ভুল হচ্ছে। সত্যিকারের run থেকে বাছাই করা কয়েকটি প্রশ্ন; প্রতিটিতে প্রথম ৩টি নথি আর score, * মানে gold, শেষে gold-এর অবস্থান:
[04 dist] রংপুরে পার্সেল পাঠাতে কত টাকা লাগে? gold bh06
BM25 bh02 5.998 bh07 4.648 bh29 2.738 gold rank 13
e5-small bh33 0.808 bh09 0.807 bh38 0.804 gold rank 13
MiniLM-L12 bh33 0.795 bh16 0.786 bh06* 0.757 gold rank 3
[05 dist] গুলশানে জিনিস পাঠাতে কত টাকা লাগে? gold bh04
BM25 bh02 5.998 bh07 4.648 bh29 2.738 gold rank 8
e5-small bh26 0.832 bh03 0.830 bh28 0.828 gold rank 11
MiniLM-L12 bh33 0.764 bh16 0.750 bh14 0.739 gold rank 12
[08 num] দশ হাজার টাকার ফোন কি হাতে পেয়ে টাকা দিয়ে নেওয়া যাবে? gold bh09
BM25 bh19 6.716 bh16 6.250 bh40 3.953 gold rank 4
e5-small bh34 0.826 bh12 0.816 bh20 0.815 gold rank 13
MiniLM-L12 bh16 0.865 bh06 0.852 bh33 0.844 gold rank 35
[42 num] ৫ কেজির পার্সেল সিলেটে পাঠাতে কত লাগবে? gold bh06
BM25 bh07 5.787 bh06* 3.200 bh02 2.616 gold rank 2
e5-small bh12 0.838 bh15 0.822 bh38 0.821 gold rank 18
MiniLM-L12 bh16 0.821 bh33 0.804 bh14 0.801 gold rank 4
[43 num] কত টাকার অর্ডারে ঢাকায় ফ্রি ডেলিভারি? gold bh04
BM25 bh40 9.607 bh33 7.003 bh04* 5.872 gold rank 3
e5-small bh40 0.900 bh04* 0.894 bh06 0.886 gold rank 2
MiniLM-L12 bh14 0.812 bh16 0.809 bh33 0.803 gold rank 8
[15 neg] ফোন পানিতে পড়ে নষ্ট, ওয়ারেন্টিতে ঠিক করে দেবে? gold bh13
BM25 bh13* 3.015 bh17 3.015 bh36 2.801 gold rank 1
e5-small bh15 0.813 bh13* 0.811 bh30 0.805 gold rank 2
MiniLM-L12 bh16 0.828 bh14 0.796 bh06 0.770 gold rank 16
[38 neg] উপন্যাস পড়ে ভালো লাগেনি, ফেরত দেওয়া যাবে? gold bh36
BM25 bh36* 9.750 bh18 3.848 bh37 3.640 gold rank 1
e5-small bh37 0.808 bh36* 0.806 bh01 0.797 gold rank 2
MiniLM-L12 bh36* 0.653 bh16 0.628 bh07 0.623 gold rank 1
[36 xl] ছাত্রদের জন্য কোনো ছাড় আছে? gold bh34
BM25 bh23 2.138 bh21 2.112 bh19 1.921 gold rank 34
e5-small bh34* 0.834 bh36 0.813 bh37 0.807 gold rank 1
MiniLM-L12 bh36 0.719 bh16 0.710 bh06 0.707 gold rank 26
[58 xl] এক্সপ্রেস ডেলিভারির খরচ কত? gold bh08
BM25 bh29 4.530 bh02 4.494 bh03 3.461 gold rank 12
e5-small bh28 0.866 bh05 0.839 bh06 0.836 gold rank 19
MiniLM-L12 bh40 0.655 bh04 0.655 bh26 0.597 gold rank 19
[53 bl] jama size e hoy nai, change korte chai gold bh25
BM25 bh34 6.031 bh10 5.290 bh01 0.000 gold rank 26
e5-small bh34 0.822 bh10 0.808 bh25* 0.805 gold rank 3
MiniLM-L12 bh16 0.441 bh19 0.420 bh36 0.400 gold rank 11
[00 para] জিনিস পছন্দ না হলে কত দিনের মধ্যে ফেরত দিতে পারব? gold bh01
BM25 bh01* 8.689 bh25 6.419 bh02 5.393 gold rank 1
e5-small bh01* 0.841 bh02 0.841 bh37 0.839 gold rank 1
MiniLM-L12 bh16 0.845 bh14 0.819 bh06 0.798 gold rank 16
প্রতিটি থেকে একটি করে শিক্ষা।
- জেলা আর এলাকার নাম: “রংপুরে” প্রশ্নের উত্তর আছে bh06-এ, যেখানে লেখা “সব জেলায়”; রংপুর যে সেই দলে, তা জানতে বাইরের জ্ঞান লাগে। BM25 আর e5 দুটিতেই rank ১৩. “গুলশান” যে ঢাকা সিটির ভেতরে, তা কোনো নথিতে নেই; rank ৮ আর ১১. Embedding-এর ভূগোল-জ্ঞানের ওপর ভরসা না করে নথিতে জেলা আর এলাকার তালিকা লিখুন, অথবা প্রশ্ন থেকে জায়গা চিনে metadata দিয়ে ছাঁকুন (পর্ব ৬৮)।
- সংখ্যা: “দশ হাজার টাকার ফোন” প্রশ্নে e5-এ সঠিক COD নথি ১৩ নম্বরে; “৫ কেজির পার্সেল সিলেটে” প্রশ্নে ১৮ নম্বরে, অথচ BM25-এ ২. Embedding “২০,০০০-এর নিচে কি না” ধরনের তুলনা করে না। আর উত্তরটি নিজেই হিসাব: ১২০ + ৪ × ২০ = ২০০ টাকা; এটি খোঁজার নয়, উত্তর তৈরির বা একটি হিসাবের tool-এর কাজ (পর্ব ৬৯ আর পর্ব ৭১, Tool use আর function calling — LLM থেকে কাজ)।
- পুরোনো নথি: “কত টাকার অর্ডারে ঢাকায় ফ্রি ডেলিভারি?” প্রশ্নে BM25 আর e5 দুটিই archived bh40-কে (১,০০০ টাকা, পুরোনো নিয়ম) প্রথমে আনে। Embedding তারিখ বা status দেখে না; status দিয়ে আগেই ছাঁকতে হয়, নইলে ভুল তথ্য সোজা উত্তরে ঢোকে।
- “না” আর শর্ত: “ফোন পানিতে পড়ে নষ্ট, ওয়ারেন্টিতে ঠিক করে দেবে?” প্রশ্নে e5 প্রথমে আনে দাবি করার নিয়ম (bh15); আসল উত্তর “পানি ঢোকা ওয়ারেন্টির বাইরে” আছে দুই নম্বরের bh13-এ। বিষয় মেলে, শর্ত মেলে না।
- ভাষা আর লিপি: ভাষা-পেরোনো খোঁজে “ছাত্রদের জন্য কোনো ছাড়” প্রশ্নে e5 Banglish নথি bh34 প্রথমে আনে, ভালো। কিন্তু “এক্সপ্রেস ডেলিভারির খরচ” প্রশ্নে ইংরেজি নথি “Express delivery” ১৯ নম্বরে। আবার Banglish প্রশ্নে e5 প্রায়ই দুটি Banglish নথি (bh10, bh34) আগে আনে, বিষয় না মিললেও: লিপির মিল একটি টান তৈরি করে। MiniLM-এ একই নথি bh16 অনেক প্রশ্নে ওপরে ওঠে, যা model ভাষাটি না জানার আরেকটি লক্ষণ।
প্রতিকারের সারকথা: নথির লেখা উন্নত করা (জেলার তালিকা, প্রতিটি ভাষায় সারাংশ), metadata আর status দিয়ে ছাঁকা, সংখ্যার কাজ হিসাবের ধাপে দেওয়া, cross-encoder দিয়ে reranking, আর প্রতিটি ধরনের প্রশ্ন eval set-এ রেখে নিয়মিত মাপা।
আরও গভীরে: Sentence-BERT-এর মূল training ছিল NLI জোড়ায় classification আর STS-এ regression; পরে retrieval-এ in-batch negative-এর InfoNCE (sentence-transformers-এ MultipleNegativesRankingLoss নামে) প্রধান হয়ে ওঠে। DPR প্রশ্ন আর নথির জন্য দুটি আলাদা encoder ব্যবহার করে, e5 একটি encoder আর উপসর্গ; দুটিই asymmetric খোঁজের দুই সমাধান। ANCE (Xiong ও সহকর্মী, ২০২০) শেখার সময় চলমান index থেকে hard negative খুঁজে আনে। বহুভাষিক মূল্যায়নে Mr. TyDi আর MIRACL-এর মতো ডেটাসেটে বাংলা আছে বলে তাদের প্রবন্ধে উল্লেখ আছে; e5-এর card Mr. TyDi-র ফল দেখায়।
পরীক্ষা ও ইন্টারভিউতে: Bi- আর cross-encoder-এর পার্থক্য? আলাদা vector আর cosine বনাম জোড়া একসঙ্গে পড়ে score; প্রথমটি খোঁজে, দ্বিতীয়টি reranking-এ। Mask-সহ mean pooling? ∑m·h ÷ ∑m. InfoNCE? −log softmax(s/τ)-এর সঠিক ঘর; batch-এর বাকিরা negative. Normalise কেন? তখন cosine = dot, খোঁজ একটি matrix গুণ। BM25-এর k₁ আর b? tf-এর সম্পৃক্তি আর দৈর্ঘ্যের শাস্তি। RRF? ∑ ১/(k + rank), score-এর মাপ লাগে না।
৯ · AI-এর যুগে — AI-এর লেখা semantic search যাচাই
AI assistant কয়েক সেকেন্ডে একটি semantic search লিখে দেয়। ভুলগুলো প্রায় সবই নীরব: কোড চলে, ফলও আসে, শুধু ফল খারাপ।
| যে ভুল প্রায়ই দেখা যায় | ফল | কীভাবে ধরবেন |
|---|---|---|
Mask ছাড়া last_hidden_state.mean(1) | Pad গড়ে ঢোকে; বাক্য দৈর্ঘ্যভেদে ভুল | নিজের গড় library-র ফলের সঙ্গে মেলানো (আমাদের পার্থক্য ৫ × ১০−৮) |
| e5-এ উপসর্গ বাদ, বা নথিতেও “query: ” | Card-এর নিয়ম ভাঙা; মান বদলাতে পারে | Card পড়া, দুইভাবে মেপে দেখা |
| Normalise না করে dot | লম্বা vector-এর নথি এগিয়ে যায় | ‖v‖ ছাপানো; cosine আর dot-এর ক্রম মেলানো |
| পুরোনো বা বানানো API, যেমন নেই এমন argument | Error, বা নীরবে default | নিজের version-এর নথি; ছোট input-এ shape ছাপানো |
| “০.৮-এর ওপরে হলে মিল” ধরনের স্থির সীমা | Model-ভেদে অর্থহীন | দাগানো ডেটায় সীমা ঠিক করা |
| তিনটি প্রশ্নে “দারুণ কাজ করছে” | ভাগ্যের ফল | Baseline, প্রশ্ন-তালিকা, ব্যবধান |
| বানানো benchmark score বা ভুল citation | ভুল সিদ্ধান্ত | প্রবন্ধ আর model card খুলে মেলানো |
AI-কে কোডের সঙ্গে প্রমাণও চাইতে বলুন:
AI-এর দেওয়া semantic search হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১BERT তো প্রতিটি token-এর চমৎকার contextual vector দেয়। তাহলে সেগুলোর গড়ই কেন ভালো sentence embedding হলো না?
কারণটি শেখার লক্ষ্যে। BERT শেখে লুকানো token অনুমান করতে (পর্ব ৪৭)। এই লক্ষ্য কোথাও বলে না যে “একই অর্থের দুটি বাক্যের গড় vector-এর cosine বেশি হতে হবে”। তাই বাক্যের জায়গাটি cosine-এর জন্য সাজানো থাকে না। Reimers আর Gurevych (২০১৯) ঠিক এটাই মাপেন: তাঁদের STS পরীক্ষায় BERT-এর গড় আর CLS vector GloVe-এর গড়ের চেয়েও পিছিয়ে ছিল, আর জোড়া বাক্য দিয়ে siamese fine-tuning করার পর ফল অনেক ভালো হয়।
আরেকটি পর্যবেক্ষণ Ethayarajh-এর (EMNLP ২০১৯): contextual vector-গুলো একটি সরু শঙ্কুর মতো এলাকায় জড়ো থাকে, যাকে বলে anisotropy. তখন প্রায় যে-কোনো দুটি বাক্যের cosine বেশি আসে, আর পার্থক্যগুলো ছোট হয়ে যায়। এর প্রতিকারে BERT-flow (Li ও সহকর্মী, ২০২০) আর whitening (Su ও সহকর্মী, ২০২১) vector-এর বণ্টন বদলানোর চেষ্টা করে। তবে পরে contrastive training, বিশেষ করে বিপুল প্রশ্ন-উত্তর জোড়ায়, মূল পথ হয়ে দাঁড়ায়, যেমন e5-এর card দুই ধাপের শেখার কথা বলে: প্রথমে দুর্বল তত্ত্বাবধানের জোড়ায় contrastive pre-training, পরে label দেওয়া ডেটায় fine-tuning.
এখান থেকে ব্যবহারিক শিক্ষা: “এই model-এর output-এর গড় নিলেই embedding” কথাটি সাধারণভাবে ভুল। কোনো BERT checkpoint নামিয়ে গড় নিয়ে খোঁজ বানালে কাজ চলতে পারে, কিন্তু retrieval-এর জন্য শেখানো model-এর সঙ্গে নিজের প্রশ্নে মেপে দেখবেন। আমাদের CLS-এর ছোট পরীক্ষাতেও দেখা গেল, ভুল pooling-এ cosine বেড়ে যায়, অর্থাৎ সব বাক্য বেশি কাছে দেখায়।
মূল উপলব্ধি: vector-এর জ্যামিতি শেখার লক্ষ্যের প্রতিফলন; cosine-এ খোঁজার জন্য এমন লক্ষ্যেই শেখাতে হয় যা cosine-কে অর্থবহ করে।
প্র ০২Embedding-এর যুগে BM25 কি পুরোনো? কেন বড় ব্যবস্থাগুলো এখনো শব্দ মেলানো খোঁজ রাখে?
পুরোনো, কিন্তু অকেজো নয়। BEIR benchmark (Thakur ও সহকর্মী, NeurIPS ২০২১ datasets track) নানা ক্ষেত্রের ১৮টি ডেটাসেটে zero-shot তুলনা করে জানায়, অনেক dense retriever প্রশিক্ষণের বাইরের ক্ষেত্রে BM25-এর চেয়ে খারাপ করেছিল; BM25 সেখানে একটি কঠিন baseline. পরের model-গুলো অনেক ক্ষেত্রে এগিয়েছে, তবে ডেটা বদলালে ফল বদলায়, এটিই স্থায়ী শিক্ষা।
BM25-এর শক্তি কয়েকটি জায়গায় স্পষ্ট। পণ্যের কোড, অর্ডার নম্বর, বিরল নাম, আইনের ধারা: এসব হুবহু মেলাতে হয়, আর embedding-এ এগুলো ঝাপসা হয়ে যেতে পারে। BM25 ব্যাখ্যা করা যায় (কোন শব্দ মিলেছে), নতুন নথি যোগ করলে কোনো model চালাতে হয় না, আর inverted index দশকের পর দশক ধরে পরীক্ষিত (সিস্টেম ডিজাইন কোর্সের পর্ব ২৭)। দুর্বলতাও স্পষ্ট: বাংলার বিভক্তি, প্রতিশব্দ, অন্য ভাষা বা লিপি। আমাদের run-এ ইংরেজি বা Banglish নথিতে বাংলা প্রশ্নে BM25 প্রায় শূন্য।
তাই hybrid search সাধারণ চর্চা হয়ে উঠেছে। লেখার সময়ে (নভেম্বর ২০২৬) Elasticsearch-এর নথিতে একটি RRF retriever আছে, যা BM25 আর kNN-এর ফল মেলাতে পারে, default rank_constant ৬০; Qdrant-এর নথিতে hybrid query-তে RRF আর DBSF নামের দুটি মিশ্রণের পথ আছে, আর সেখানে RRF-এর k-এর default লেখা ২. এসব সংস্করণভেদে পরিবর্তন হতে পারে; নিজে নথি পড়ে নেবেন, আর k-এর মান যে এক ব্যবস্থা থেকে আরেকটিতে ভিন্ন, সেটিও খেয়াল রাখবেন। কিন্তু আমাদের নিজের ছোট run দেখায়, RRF সবসময় জেতে না: একটি পক্ষ অনেক দুর্বল হলে মিশ্রণ ভালো পক্ষটিকে টেনে নামাতে পারে।
মূল উপলব্ধি: শব্দ মেলানো আর অর্থ মেলানো ভিন্ন ভুল করে; কোনটি বা কোন মিশ্রণ আপনার ডেটায় ভালো, তা নিজের প্রশ্নে মেপেই ঠিক হয়।
প্র ০৩পুরো একটি নথি, হোক তা ৫ লাইন বা ৫০ পাতা, একটিমাত্র vector-এ ধরা যায় কি? সীমাটা কোথায়?
প্রথম সীমা দৈর্ঘ্য। Model একটি নির্দিষ্ট সংখ্যক token-এর বেশি পড়ে না: e5-small-এ ৫১২, আমাদের ব্যবহৃত paraphrase-multilingual-MiniLM-এ ১২৮. এর বেশি হলে বাকিটা নীরবে কেটে যায়; কোনো error আসে না। বাংলায় token বেশি লাগে (পর্ব ৬৫), তাই একই অনুচ্ছেদ ইংরেজির চেয়ে আগে সীমা ছোঁয়।
দ্বিতীয় সীমা অর্থের মিশ্রণ। Mean pooling সব token-এর গড়; একটি নথিতে পাঁচটি বিষয় থাকলে vector পাঁচটির মাঝামাঝি কোথাও বসে, কোনোটির কাছেই নয়। আমাদের ফেরত নীতির নথিতে (bh01) চারটি উপশিরোনাম আছে: সময়সীমা, যা ফেরত হয় না, ইলেকট্রনিক্স, যাচাই। প্রশ্ন একটির সঙ্গে মিললেও পুরো নথির vector অন্যগুলোর দিকে টানা থাকে।
সমাধানের পথ কয়েকটি। নথিকে ছোট অংশে ভাগ করা (chunking), যা পর্ব ৬৯-এর বিষয়। প্রতিটি token-এর vector রেখে token-স্তরে মেলানো, যেমন ColBERT; জায়গা অনেক বেশি লাগে। আরেক দিকে Matryoshka representation learning (Kusupati ও সহকর্মী, ২০২২) এমনভাবে শেখায় যাতে vector-এর প্রথম কয়েকশো ঘর একাই কাজ চালায়; তখন জায়গা আর গতির বিনিময়ে নির্ভুলতা কিছুটা ছাড়া যায়। কোনটি দরকার, তা নথির দৈর্ঘ্য, প্রশ্নের ধরন আর খরচের সীমা দেখে মাপতে হয়।
মূল উপলব্ধি: একটি vector একটি সারাংশ, আর সারাংশে খুঁটিনাটি হারায়; দৈর্ঘ্যের সীমা দেখে নথি ভাগ করা retrieval-এর অর্ধেক কাজ।
প্র ০৪বাংলা প্রশ্নে ইংরেজি নথি খুঁজে পাওয়া কীভাবে সম্ভব হয়? আর Banglish কেন আলাদা সমস্যা?
বহুভাষিক model-কে এমনভাবে শেখানো হয় যাতে অনুবাদের জোড়া কাছাকাছি vector পায়। Reimers আর Gurevych-এর ২০২০ সালের পদ্ধতিতে (Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation) একটি ইংরেজি “শিক্ষক” model-এর vector-কে লক্ষ্য ধরে একটি বহুভাষিক “ছাত্র” model শেখে: ইংরেজি বাক্য আর তার অনুবাদ, দুটির vector-ই শিক্ষকের ইংরেজি vector-এর কাছে যেতে হবে। sentence-transformers-এর নথিতে paraphrase-multilingual-MiniLM-L12-v2-কে ইংরেজি paraphrase model-এর বহুভাষিক রূপ বলা হয়েছে, ৫০টির বেশি ভাষার সমান্তরাল (অনুবাদ) ডেটায় শেখানো, আর এই পরিবারের জন্য ওই প্রবন্ধটিকেই সূত্র ধরা হয়েছে। খেয়াল করুন: লেখার সময়ে এর card-এর ভাষার তালিকায় bn নেই। LaBSE (Feng ও সহকর্মী) অনুবাদ-জোড়ায় dual-encoder শেখায়; e5-এর বহুভাষিক রূপ বহু ভাষার জোড়ায় contrastive শেখে।
e5-এর তালিকায় বাংলা আছে, কিন্তু বাংলা ডেটা ইংরেজির তুলনায় অনেক কম বলে ধারণা করা হয়, তাই মান সাধারণত ইংরেজির চেয়ে কম হতে পারে; এটি আমরা মাপিনি, শুধু আমাদের ছোট প্রশ্ন-তালিকায় ফল দেখেছি। Banglish আরও কঠিন। এটি লাতিন লিপিতে বাংলা, যার কোনো মানক বানান নেই: “kotodin”, “koto din”, “kotodine”. Tokenizer এগুলোকে ইংরেজির মতো টুকরো করে, আর শেখার ডেটায় এমন লেখা কতটা ছিল, card-এ তার উল্লেখ নেই। আমাদের run-এ Banglish প্রশ্নে model-ভেদে ফল বেশ ভিন্ন।
ব্যবহারিক পথ: Banglish প্রশ্নের একটি আলাদা ভাগ রেখে মাপা; প্রয়োজনে বাংলা লিপিতে transliteration করে দুইভাবে খোঁজা; নথির পাশে Banglish-এ লেখা সাধারণ প্রশ্নও রাখা। এ ধরনের গ্রাহকেরা যেন কম ভালো উত্তর না পান, সেটি একধরনের ন্যায্যতার প্রশ্নও।
মূল উপলব্ধি: ভাষা-পেরোনো খোঁজ শেখার ডেটার জোড়া থেকে আসে; যে লেখার ধরন শেখায় কম ছিল, সেখানে মাপ না নিয়ে ভরসা করা যায় না।
প্র ০৫অনলাইনে embedding model-এর leaderboard আছে। সেখানে যে শীর্ষে, সেটিই কি বাজারঘরের জন্য নেওয়া উচিত?
সবচেয়ে পরিচিত তুলনা MTEB (Massive Text Embedding Benchmark; Muennighoff ও সহকর্মী, প্রবন্ধ ২০২২, EACL ২০২৩)। এতে retrieval, classification, clustering-সহ নানা কাজের অনেক ডেটাসেট একসঙ্গে মাপা হয়। এটি উপকারী শুরু, কিন্তু কয়েকটি সতর্কতা আছে। এক, গড় score অনেক কাজ মিশিয়ে আসে; আপনার কাজ হয়তো তার একটি ছোট অংশ। দুই, বাংলার ডেটাসেট সেখানে তুলনামূলক কম, আর Banglish প্রায় নেই বলে ধারণা করা যায়। তিন, জনপ্রিয় benchmark-এ বারবার পরীক্ষা করে model বাছাই হলে, বা শেখার ডেটায় benchmark-এর লেখা ঢুকে গেলে, score আশাবাদী হতে পারে (পর্ব ৫৮-এ একই ফাঁদ)।
চার, খরচ। বড় model একটু ভালো করলেও প্রতিটি নথি আর প্রশ্নে বেশি সময় আর memory লাগে, আর vector-এর মাত্রা বাড়লে index-এর জায়গাও বাড়ে। পাঁচ, লাইসেন্স আর ডেটা কোথায় যায়: hosted embedding সেবায় গ্রাহকের লেখা বাইরে যায়।
সঠিক পথ আমাদের ৬০টি প্রশ্নের মতো নিজের একটি প্রশ্ন-তালিকা: বাস্তব প্রশ্নের ধরন (বাংলা, Banglish, ইংরেজি, সংখ্যা, জায়গার নাম) মিলিয়ে, gold নথি হাতে দাগিয়ে। তারপর BM25 baseline, ২–৩টি model, recall@k আর MRR, bootstrap ব্যবধান আর paired পার্থক্য (মেশিন লার্নিং কোর্সের পর্ব ২১ আর পর্ব ২২)। আমাদের ৬০টি প্রশ্নে ব্যবধান এখনো চওড়া; সিদ্ধান্ত নেওয়ার আগে আরও প্রশ্ন যোগ করা ভালো। আর model-এর revision পিন করে রাখুন, কারণ model বদলালে পুরো index নতুন করে বানাতে হয়।
মূল উপলব্ধি: Leaderboard তালিকা ছোট করে; চূড়ান্ত বাছাই হয় নিজের প্রশ্নে, নিজের খরচের সীমায়, অনিশ্চয়তাসহ।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান।
-
পূর্ণ numerical: (ক) ৫টি নথি, একটি শব্দ শুধু একটি নথিতে একবার আছে; সেই নথির দৈর্ঘ্য ৯, গড় দৈর্ঘ্য ১১, k₁ = ১.৫, b = ০.৭৫. Lucene ধাঁচের IDF ধরে এই শব্দের BM25 অবদান কত? (খ) একটি প্রশ্নের তিন নথির cosine [০.৫, ০.২, ০.২], প্রথমটি সঠিক। τ = ০.১ আর τ = ১-এ InfoNCE loss কত? (গ) RRF-এ (k = ৬০) নথি X দুই তালিকায় rank ১ আর ৩, নথি Y দুটিতেই rank ২. কে আগে? (ঘ) তিনটি প্রশ্নে প্রথম সঠিক নথির rank ১, ৪ আর ১২. MRR@১০ আর recall@৫ কত (প্রতিটির একটিই সঠিক নথি)?
(ক) IDF = ln(১ + (৫ − ১ + ০.৫)/(১ + ০.৫)) = ln ৪ = ১.৩৮৬. হর = ১ + ১.৫ × (০.২৫ + ০.৭৫ × ৯/১১) = ২.২৯৫. tf-অংশ = ২.৫/২.২৯৫ = ১.০৮৯. অবদান = ১.৫১০. (খ) τ = ০.১: logits [৫, ২, ২], p = e⁵/(e⁵ + ২e²) = ০.৯০৯৪, loss = ০.০৯৫. τ = ১: p = ০.৪০৩, loss = ০.৯০৯. (গ) X = ১/৬১ + ১/৬৩ = ০.০৩২২৬৬; Y = ২/৬২ = ০.০৩২২৫৮. X সামান্য আগে; পার্থক্য খুব ছোট, k বড় হলে rank-এর পার্থক্যের প্রভাব কমে। (ঘ) MRR@১০ = (১ + ০.২৫ + ০)/৩ = ০.৪১৭ (rank ১২ দশের বাইরে, তাই ০). recall@৫ = ২/৩ = ০.৬৬৭.
-
সমালোচনা ও নকশা: একটি দলের পরিকল্পনা: “একটি ইংরেজি BERT checkpoint-এর CLS vector দিয়ে বাংলা হেল্প সেন্টারের খোঁজ বানাব। Vector normalise করব না, dot দিয়ে খুঁজব। cosine ০.৮-এর ওপরে হলে উত্তর দেখাব। পাঁচটি প্রশ্ন চালিয়ে দেখেছি, ভালো লাগছে।” কী কী সমস্যা? কীভাবে নতুন করে নকশা করবেন?
(১) ইংরেজি model-এর card-এ বাংলা না থাকলে বাংলার vector প্রায় অর্থহীন হতে পারে। (২) Retrieval-এর জন্য শেখানো নয় এমন model-এর CLS; ভুল pooling. (৩) Normalise ছাড়া dot-এ লম্বা vector-এর নথি এগিয়ে যায়। (৪) স্থির ০.৮ সীমা: model-ভেদে score-এর ঘর আলাদা (e5-এ প্রায় সব ০.৭-এর ওপরে)। (৫) পাঁচটি প্রশ্ন কোনো প্রমাণ নয়; baseline নেই।
নকশা: বহুভাষিক retrieval model, card অনুযায়ী pooling আর উপসর্গ, normalise করে cosine; BM25 baseline আর hybrid বিকল্প; বাংলা, Banglish, ইংরেজি, সংখ্যা ও জায়গার নামের অন্তত ৫০–১০০টি দাগানো প্রশ্ন; recall@k, MRR, bootstrap ব্যবধান; সীমা ঠিক করতে আলাদা যাচাই-ডেটা; model revision পিন করা।
-
আউটপুট অনুমান করুন:
H = np.arange(12).reshape(1, 4, 3).astype(float); m = np.array([[1, 1, 0, 0]]); print((H * m[:, :, None]).sum(1) / m.sum(1, keepdims=True)); print(H.mean(1)); print(H[:, 0])[[1.5 2.5 3.5]],[[4.5 5.5 6.5]],[[0. 1. 2.]]. সারিগুলো [0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]. Mask-সহ গড় শুধু প্রথম দুটির: [(0+3)/2, (1+4)/2, (2+5)/2]. Mask ছাড়া চারটিরই গড়। শেষটি প্রথম token, অর্থাৎ CLS-এর জায়গা। Shape সবগুলোর (1, 3). -
কোড লিখুন: অংশ ৭-এর পাতার কোডে RRF-কে weighted করুন: score = w/(k + rankBM25) + (১ − w)/(k + ranke5). w = ০.২, ০.৩৫, ০.৫-এ MRR@১০ ছাপান। তারপর ভাবুন, কেন এই ৬০টি প্রশ্নে সেরা w বেছে সেটিকেই চূড়ান্ত ফল বলা ঠিক নয়।
def wrrf(a, b, w, k=60): s = {} for lst, wt in ((a, w), (b, 1 - w)): for pos, d in enumerate(lst): s[d] = s.get(d, 0) + wt / (k + pos + 1) return sorted(s, key=lambda d: -s[d]) for w in (0.2, 0.35, 0.5): r = [wrrf(a, b, w) for a, b in zip(df.bm25, df.e5)] print(w, round(np.mean([rr(g, l) for g, l in zip(df.gold, r)]), 3))পাতার কোডে চালিয়ে (রক্ষিত ৬০টি প্রশ্নের ক্রম, প্রথম ১০টি): w = ০.২ →
0.626, ০.৩৫ →0.617, ০.৫ →0.603(সাধারণ RRF-এর সমান, কারণ দুই পাশে একই ওজন)। BM25-এর ওজন কমালে ফল e5-একার (০.৬৫১) দিকে এগোয়, কিন্তু ছাড়ায় না। আর এই ৬০টি প্রশ্ন দেখেই w বাছলে সেটি আর নিরপেক্ষ মাপ থাকে না: বাছাইয়ের ডেটায় জয় সবসময় একটু আশাবাদী। w আলাদা যাচাই-প্রশ্নে বাছুন, তারপর নতুন প্রশ্নে একবার মাপুন, ঠিক যেমন পর্ব ৬৬-এ eval set ভাগ করেছিলেন।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমহেল্প সেন্টারের জন্য একটি semantic search বানানো ও যাচাইয়ের ধাপগুলো সঠিক ক্রমে সাজান।
- Gold নথিসহ প্রশ্ন-তালিকা হাতে লেখা
- Model card পড়ে pooling, উপসর্গ আর লাইসেন্স দেখা
- নথিগুলো “passage: ” দিয়ে encode করে normalise করে রাখা
- প্রশ্ন “query: ” দিয়ে encode করে cosine-এ সাজানো
- BM25 baseline আর hybrid-এর সঙ্গে recall@k ও MRR মাপা
- Bootstrap ব্যবধান আর ব্যর্থ প্রশ্ন পড়ে সিদ্ধান্ত নেওয়া
একটি বাক্যের token vector [2, 0], [4, 2] আর একটি pad [10, 10]; mask [1, 1, 0]. Mask-সহ mean pooling-এর ফল কত?
- [5.33, 4]
- [2, 0]
- [3, 1]
- [6, 2]
১০ লাখ নথি থেকে প্রতিটি প্রশ্নে দ্রুত উত্তর খুঁজতে হবে। কোন নকশা সবচেয়ে যুক্তিসংগত?
- সব নথিতে cross-encoder চালানো
- Bi-encoder বা BM25-এ প্রথম কয়েকশো, তারপর সেগুলোতে cross-encoder
- Cross-encoder-এর score আগেই হিসাব করে রাখা
- প্রতিটি প্রশ্নে সব নথি আবার bi-encoder দিয়ে encode করা
InfoNCE-এ একটি প্রশ্নের তিন নথির cosine [0.6, 0.6, 0.0], প্রথমটি সঠিক, τ = ০.১. সঠিক নথির softmax সম্ভাবনা প্রায় কত?
- প্রায় ১.০
- প্রায় ০.৩৩
- ০.৬
- প্রায় ০.৫
দুটি vector-এর দৈর্ঘ্য ১ (unit vector). তাদের dot product আর cosine-এর সম্পর্ক কী?
- হুবহু সমান
- Dot হলো cosine-এর বর্গ
- Dot = ১ − cosine
- কোনো নির্দিষ্ট সম্পর্ক নেই
“ঢাকায় ডেলিভারি চার্জ কত?” প্রশ্নে BM25 সঠিক নথিটি পেল না, যেখানে লেখা “ঢাকা সিটির ভেতরে ডেলিভারি চার্জ ৬০ টাকা”. সবচেয়ে সম্ভাব্য কারণ কী?
- Vector normalise করা হয়নি
- IDF ঋণাত্মক হয়ে গেছে
- বিভক্তির কারণে “ঢাকায়” আর “ঢাকা” মেলেনি
- BM25 বাংলা লিপিতে কাজ করে না
৬০টি প্রশ্নে system A-এর MRR ০.৬৬, B-এর ০.৬৮; paired bootstrap-এ পার্থক্যের ৯৫% ব্যবধান [−০.০৫, +০.০৯]. সবচেয়ে ঠিক সিদ্ধান্ত কোনটি?
- B নিশ্চিতভাবে ভালো
- এই ডেটায় পার্থক্য নিশ্চিত নয়; আরও প্রশ্ন বা অন্য বিবেচনা লাগবে
- A নিশ্চিতভাবে ভালো
- ৬০টি প্রশ্নে bootstrap চালানোই যায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে।
মনে রাখুন
- Pooling: mean = ∑mtht ÷ ∑mt; mask ভুললে pad গড়ে ঢোকে, কোনো error ছাড়াই। Model যে pooling-এ শেখানো, সেটিই নিন।
- দুই নকশা: bi-encoder-এ নথির vector আগেই রাখা যায়; cross-encoder প্রতিটি জোড়া একসঙ্গে পড়ে, তাই শুধু অল্প প্রার্থীর reranking-এ।
- শেখা: InfoNCE = নিজের জোড়াকে “সঠিক শ্রেণি” ধরে cross-entropy, batch-এর বাকিরা negative; ছোট τ পার্থক্য ধারালো করে।
- খোঁজ: normalise করলে cosine = dot; e5-এ “query: ” / “passage: ”. BM25 সংখ্যা, কোড আর হুবহু শব্দে শক্ত, বিভক্তি আর ভাষা-পেরোনোয় দুর্বল।
- মাপ: নিজের দাগানো প্রশ্নে recall@k, MRR, bootstrap ব্যবধান; RRF = ∑ ১/(k + rank), কিন্তু জয় নিশ্চিত নয়, মেপে দেখুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৬৮ · Vector index — FAISS, approximate nearest neighbor আর vector databaseপরবর্তী পর্বলাখো vector-এ দ্রুত খোঁজার index, গতি আর নির্ভুলতার লেনদেন মেপে।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ।
- পর্ব ৪৪ · Contextual embedding — ELMo থেকে BERTআগের পর্বToken-এর vector,
word_ids()আর pooling. - পর্ব ১৩ · Deep learning-এর lossআগের পর্বCross-entropy, triplet আর contrastive loss.
- পর্ব ৬৬ · Prompt পরীক্ষা — eval set, regression আর failure modeআগের পর্বপ্রশ্ন-তালিকা বানানো আর paired bootstrap.
- AI-এর ভিত্তি · পর্ব ৬৯ · Embedding আর vector searchআগের ভিত্তিপ্রথম vector search, recall@k আর MRR হাতে।
- মেশিন লার্নিং · পর্ব ২১ · Bootstrap আর resamplingআগের ভিত্তিব্যবধান কীভাবে আসে।
- সিস্টেম ডিজাইন · পর্ব ২৭ · Searchসম্পর্কিতInverted index, ranking আর search cluster.