এই পর্বে যা শিখবেন
- স্থির vector কেন বহু-অর্থের শব্দে ব্যর্থ হয়, তা হিসাব করে দেখানো
- ELMo-র মূল ধারণা আর scalar mix-এর সূত্র, হাতে আর NumPy-তে
- BERT-এর input: CLS, SEP, segment, position আর masked language modeling-এর প্রাথমিক ধারণা
- সত্যিকারের বাংলা ও বহুভাষিক model থেকে subword মিলিয়ে শব্দের vector নেওয়া, layer ও pooling বাছা
- ছোট হাতে-লেখা ডেটায় word-sense probe চালানো, আর baseline, ব্যবধান ও control task দিয়ে ফল যাচাই করা
AI-এর ভিত্তি কোর্সের পর্ব ৬৯-এ pretrained embedding আর cosine ব্যবহার করেছেন, আর পর্ব ২৭-এ nn.Embedding দেখেছেন। স্থির vector কীভাবে শেখা হয়, তা আছে পর্ব ৪১-এ (Word embedding — word2vec, GloVe আর fastText). এই পর্বের প্রশ্ন: একই বানানের শব্দের অর্থ যদি বাক্য অনুযায়ী বদলায়, vector কেন বদলাবে না?
১ · এক শব্দ, এক vector — সমস্যাটা কোথায়
চায়ের দোকানে “কাল আসব” শুনলে আপনি বোঝেন আগামীকাল। “কাল এসেছিলাম” শুনলে গতকাল। “সে কালে” শুনলে পুরোনো সময়। বানান একই, কিন্তু অর্থ ঠিক করে দেয় আশেপাশের শব্দ। এখন ভাবুন, একটি অভিধানে “কাল”-এর পাশে একটিই মাত্র লাইন লেখার অনুমতি আছে। তিন অর্থের একটি জগাখিচুড়ি ছাড়া কী লিখবেন?
একই শব্দের একাধিক সম্পর্কিত বা আলাদা অর্থ থাকাকে এখানে সহজে polysemy বলছি। প্রতিটি অর্থকে বলে word sense. Static embedding-এ প্রতিটি বানানের জন্য table-এ একটি সারি থাকে। বাক্য যা-ই হোক, ফল সেই সারিই। প্রশিক্ষণের সময় ধারণা করা যায়, শব্দটির সব ব্যবহার মিলে vector-টিকে টানে। Arora ও সহকর্মীরা (২০১৮) দেখান, word embedding-এ অর্থগুলো একধরনের linear superposition হিসেবে থাকে। আমরা নিচে একটি সরল গড় দিয়ে এর স্বাদ নিচ্ছি; এটি word2vec-এর আসল training নয়।
| ধাপ | |
| মান |
- দুই অর্থ. ধরুন অর্থ দুটির নিজস্ব জায়গা আছে। প্রায় লম্ব, cosine প্রায় ০.
- একটি সারি. Static table বাক্য দেখে না। বানান মিললেই একই সারি আসে।
- গড়. ১০০ ব্যবহারের ৮০টি গাছের, ২০টি বইয়ের হলে vector অনেকটা গড়ের মতো হয়।
- টান. v প্রায় পুরোটা “গাছের” দিকে। “বইয়ের” অর্থের সঙ্গে মিল মাত্র ০.২৫২.
- ব্যর্থতা. “বইয়ের পাতা উল্টে দেখুন” বাক্যেও v-ই আসে। Model নিজের input থেকে পৃষ্ঠার অর্থ পায় না।
- মূল সমস্যা. দুই ব্যবহারের cosine ঠিক ১. স্থির vector দিয়ে দুই অর্থ আলাদা করার কোনো উপায়ই নেই; আশেপাশের শব্দ পরে আলাদা করে দেখতে হয়।
কম্পাঙ্ক বদলালে কী হয়, পাতার Python-এ দেখুন। প্রতিটি ব্যবহারে একটু noise আছে, আর ৫,০০০ ব্যবহারের গড়ই “স্থির vector”.
import numpy as np
rng = np.random.default_rng(42)
# ৩টি অক্ষ: [গাছপালা, বইখাতা, অন্য]; দুই অর্থের বানানো কেন্দ্র
leaf = np.array([1.0, 0.0, 0.1])
page = np.array([0.0, 1.0, 0.1])
def cos(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
print("গাছের ভাগ cos(v, গাছের) cos(v, বইয়ের) v")
for f in (0.5, 0.8, 0.95):
n = 5000 # ৫,০০০ বার শব্দটি এলো
is_leaf = rng.random(n) < f
occ = np.where(is_leaf[:, None], leaf, page)
occ = occ + rng.normal(0, 0.15, size=occ.shape) # প্রতিটি ব্যবহার একটু আলাদা
v = occ.mean(axis=0) # এক শব্দ, একটিই vector
print(f"{f:8.2f} {cos(v, leaf):13.3f} {cos(v, page):13.3f} {np.round(v, 2)}")আউটপুট (পাতার Python-এ চালানো):
গাছের ভাগ cos(v, গাছের) cos(v, বইয়ের) v
0.50 0.727 0.694 [0.51 0.49 0.1 ]
0.80 0.970 0.254 [0.8 0.2 0.1]
0.95 0.999 0.064 [0.95 0.05 0.1 ]
ভাগ ০.৫০ হলে v দুই অর্থের মাঝখানে, কোনোটিরই ঠিক কাছে নয়। ভাগ ০.৯৫ হলে কম ব্যবহৃত অর্থ প্রায় মুছে যায় (০.০৬৪)। ডেটা বানানো, তাই সংখ্যাগুলো কোনো বাংলা corpus-এর মাপ নয়; দেখানোর বিষয় শুধু গড়ের আচরণ।
২ · আশেপাশের শব্দ vector-কে বদলায় — ছোট একটি self-attention
Contextual embedding মানে শব্দের vector বাক্যের একটি function. একই “পাতা”, বাক্য ভিন্ন, vector ভিন্ন। এর সবচেয়ে সরল যন্ত্র আপনি পর্ব ৩৩-এ দেখেছেন: self-attention-এ প্রতিটি token বাকিদের একটি মিশ্রণ পায়। নিচে হাতে বানানো embedding, Q = K = V = X, কোনো training নেই।
import numpy as np
np.set_printoptions(precision=3, suppress=True)
# হাতে বানানো embedding: [গাছপালা, বইখাতা, ক্রিয়া]
E = {"গাছের": [2.0, 0.0, 0.0], "বইয়ের": [0.0, 2.0, 0.0],
"পাতা": [1.0, 1.0, 0.0], "ঝরে": [0.5, 0.0, 1.5], "উল্টে": [0.0, 0.5, 1.5]}
def softmax(z):
e = np.exp(z - z.max(-1, keepdims=True))
return e / e.sum(-1, keepdims=True)
def attend(words):
X = np.array([E[w] for w in words])
S = X @ X.T / np.sqrt(X.shape[1]) # পর্ব ৩৩-এর সূত্র
A = softmax(S)
return A @ X, A
def cos(a, b):
return a @ b / (np.linalg.norm(a) * np.linalg.norm(b))
s1, s2 = ["গাছের", "পাতা", "ঝরে"], ["বইয়ের", "পাতা", "উল্টে"]
Z1, A1 = attend(s1)
Z2, A2 = attend(s2)
print("পাতা-র weight, বাক্য ১:", A1[1])
print("পাতা-র weight, বাক্য ২:", A2[1])
print("পাতা-র নতুন vector ১:", Z1[1])
print("পাতা-র নতুন vector ২:", Z2[1])
print("attention-এর আগে cos: 1.0 পরে cos:", round(float(cos(Z1[1], Z2[1])), 3))
g, b = np.array(E["গাছের"]), np.array(E["বইয়ের"])
print("বাক্য ১: cos(পাতা, গাছের) =", round(float(cos(Z1[1], g)), 3), " cos(পাতা, বইয়ের) =", round(float(cos(Z1[1], b)), 3))
print("বাক্য ২: cos(পাতা, গাছের) =", round(float(cos(Z2[1], g)), 3), " cos(পাতা, বইয়ের) =", round(float(cos(Z2[1], b)), 3))আউটপুট (পাতার Python-এ চালানো):
পাতা-র weight, বাক্য ১: [0.413 0.413 0.174]
পাতা-র weight, বাক্য ২: [0.413 0.413 0.174]
পাতা-র নতুন vector ১: [1.326 0.413 0.261]
পাতা-র নতুন vector ২: [0.413 1.326 0.261]
attention-এর আগে cos: 1.0 পরে cos: 0.583
বাক্য ১: cos(পাতা, গাছের) = 0.938 cos(পাতা, বইয়ের) = 0.292
বাক্য ২: cos(পাতা, গাছের) = 0.292 cos(পাতা, বইয়ের) = 0.938
Input-এ “পাতা” একই [1, 1, 0]. একটি attention স্তর পার হয়েই দুই বাক্যে vector আলাদা, cosine ০.৫৮৩. প্রথম বাক্যে vector “গাছের” দিকে, দ্বিতীয়টিতে “বইয়ের” দিকে সরেছে। Embedding আমরা সাজিয়ে দিয়েছি, তাই সংখ্যাগুলো বাস্তব model-এর নয়; এটি শুধু যন্ত্রটির ছবি। Model এখানে কিছু “বোঝেনি”, শুধু গুণ আর যোগ করেছে।
৩ · ELMo — দুই দিকের language model আর layer-এর মিশ্রণ
Self-attention-নির্ভর model জনপ্রিয় হওয়ার আগে একই লক্ষ্যে কাজ হয়েছিল LSTM দিয়ে। ELMo (Peters ও সহকর্মীরা, “Deep contextualized word representations”, NAACL ২০১৮) ধারণাটিকে ব্যাপক পরিচিতি দেয়; তার আগে CoVe (McCann ও সহকর্মীরা, ২০১৭) অনুবাদের encoder থেকে একই রকম চেষ্টা করেছিল।
ধারণা তিন ধাপে। এক, বিপুল লেখায় একটি দুই দিকের language model শেখানো: সামনের LSTM পরের শব্দ অনুমান করে, পেছনের LSTM আগের শব্দ (পর্ব ২৯)। মূল প্রবন্ধে character থেকে তৈরি token vector-এর ওপর দুই স্তরের BiLSTM ছিল। দুই, শেখা শেষে model স্থির রাখা। তিন, নতুন বাক্যের প্রতিটি শব্দের জন্য সব layer-এর output নেওয়া, আর আপনার কাজের জন্য তাদের একটি শেখা মিশ্রণ করা। একে বলে scalar mix:
ELMok = γ · ∑j sj · hk,j, s = softmax(a)
এখানে k হলো শব্দের অবস্থান, j হলো layer, hk,j সেই layer-এ শব্দটির vector, a প্রতিটি layer-এর জন্য একটি শেখা সংখ্যা, s তার softmax (যোগফল ১), আর γ একটি শেখা স্কেল। পুরো vector-এ একটিই ওজন; তাই নাম scalar mix.
হাতে একটি হিসাব। ২-মাত্রার তিনটি layer: h₀ = [1, 0], h₁ = [1, 2], h₂ = [0, 2]; a = [0, 1, 1]. তাহলে ea = [১, ২.৭১৮, ২.৭১৮], যোগফল ৬.৪৩৭, আর s = [০.১৫৫৪, ০.৪২২৩, ০.৪২২৩]. মিশ্রণ = [০.৫৭৭৭, ১.৬৮৯৩]. γ = ২ হলে [১.১৫৫৪, ৩.৩৭৮৬]. এবার দেখি, কাজ বদলালে ওজন সত্যিই বদলায় কি না। নিচের বানানো ডেটায় layer ০-তে শুধু POS-এর সংকেত, layer ২-তে শুধু অর্থের সংকেত রাখা আছে।
import numpy as np
rng = np.random.default_rng(42)
n, d = 200, 4
y_sense = rng.integers(0, 2, n)
y_pos = rng.integers(0, 2, n)
H = rng.normal(0, 1.0, size=(3, n, d)) # (layer, n, d)
H[0, :, 0] += 1.5 * (2 * y_pos - 1) # layer 0-তে শুধু POS-এর সংকেত
H[2, :, 1] += 1.5 * (2 * y_sense - 1) # layer 2-তে শুধু অর্থের সংকেত
def softmax(a):
e = np.exp(a - a.max())
return e / e.sum()
def fit(y, steps=400, lr=0.3):
a, w, b = np.zeros(3), np.zeros(d), 0.0
for _ in range(steps):
s = softmax(a)
m = np.einsum("j,jnd->nd", s, H) # scalar mix
p = 1 / (1 + np.exp(-(m @ w + b))) # logistic classifier
r = p - y
w -= lr * (r @ m) / n
b -= lr * r.mean()
ga = [(r * ((H[j] - m) @ w)).mean() * s[j] for j in range(3)]
a -= lr * np.array(ga) # ∂s/∂a থেকে আসা gradient
return softmax(a), ((p > 0.5) == y).mean()
for name, y in [("অর্থ (sense)", y_sense), ("POS", y_pos)]:
s, acc = fit(y)
print(f"{name:14s} s = {np.round(s, 3)} train acc = {acc:.3f}")আউটপুট (পাতার Python-এ চালানো):
অর্থ (sense) s = [0.022 0.044 0.933] train acc = 0.950
POS s = [0.877 0.069 0.055] train acc = 0.930
একই H, কাজ ভিন্ন। অর্থের কাজে প্রায় সব ওজন layer ২-এ, POS-এর কাজে layer ০-এ। সংকেত কোথায় আছে, তা আমরাই সাজিয়েছি, তাই এটি শুধু দেখায় যে মিশ্রণ সংকেতের দিকে যেতে পারে; কোনো বাস্তব model-এ অর্থ শেষ layer-এই থাকবে, এই কোড তা প্রমাণ করে না। Gradient-এ sj(hj − m) পদটি এসেছে softmax-এর গুণফলের নিয়ম থেকে: ∂si/∂aj = si(δij − sj), যেখানে δij হলো i = j হলে ১, নইলে ০.
৪ · Transformer যখন contextual encoder — BERT-এর input এক নজরে
Transformer encoder-এও (পর্ব ৩৫) প্রতিটি token-এর output আসলে contextual vector. BERT (Devlin ও সহকর্মীরা, arXiv ২০১৮, NAACL ২০১৯) এই ব্যবহারকে ধারার কেন্দ্রে নিয়ে আসে। পুরো পরিবার আর fine-tuning আসবে পর্ব ৪৮-এ (BERT পরিবার — encoder model আর fine-tuning), আর শেখার লক্ষ্যের তুলনা পর্ব ৪৭-এ। এখানে শুধু এটুকু, যা vector তোলার জন্য লাগে।
- [CLS]: বাক্যের শুরুতে একটি বিশেষ token. মূল ধারণায় এর output পুরো input-এর সারাংশ হিসেবে classification-এ যায়।
- [SEP]: বাক্যের শেষ বা দুই বাক্যের সীমা চিহ্নিত করে।
- Segment embedding: Token কোন বাক্যে, A না B, তার জন্য একটি শেখা vector.
- Position embedding: Token কত নম্বর অবস্থানে, তার জন্য শেখা vector (পর্ব ৩৪)। BERT-এ সর্বোচ্চ ৫১২.
- Masked language modeling (MLM): প্রবন্ধ অনুযায়ী প্রায় ১৫% token বাছাই হয়; তাদের ৮০% [MASK] হয়, ১০% এলোমেলো token, ১০% অপরিবর্তিত। Model দুই পাশ দেখে আসল token অনুমান করে। ফলে একটি token-এর vector-কে আশেপাশের সংকেত ধরে রাখতে হয়।
| দিক | স্থির (word2vec ধরনের) | ELMo | BERT |
|---|---|---|---|
| একই বানানে vector | সবসময় এক | বাক্য অনুযায়ী | বাক্য অনুযায়ী |
| প্রসঙ্গ আসে | শেখার সময়ের গড়ে | দুই দিকের LSTM থেকে | Self-attention থেকে |
| শেখার লক্ষ্য | আশেপাশের শব্দ অনুমান | পরের ও আগের শব্দ অনুমান | লুকানো token অনুমান |
| পড়ার ক্রম | Table-এর একটি সারি | Token একে একে | পুরো বাক্য একসঙ্গে |
| নতুন বানান | সাধারণত নেই (fastText ব্যতিক্রম) | Character থেকে vector | Subword-এ ভেঙে |
| খরচ | প্রায় শূন্য | একটি network চালানো | একটি network চালানো |
৫ · সত্যিকারের model — আর একটি নীরব tokenizer-ফাঁদ
এবার সত্যিকারের run. দুটি ছোট model, দুটিই account ছাড়া নামে। Model card দেখে নেওয়ার নিয়ম আছে AI-এর ভিত্তি কোর্সের পর্ব ৭১-এ। লেখার সময়ের version: Python 3.12.13, torch 2.15.0, transformers 5.22.2, scikit-learn 1.9.1. সব কাজ CPU-তে। লম্বা আউটপুট ছেঁটে আর সাজিয়ে দেখানো হয়েছে; সংখ্যাগুলো run থেকে হুবহু নেওয়া।
| Checkpoint | লাইসেন্স (card) | Layer, ফাইল | মাপা parameter |
|---|---|---|---|
sagorsarker/ (OSCAR আর বাংলা Wikipedia-তে শেখা, card অনুযায়ী) | MIT | ১২, ৬৬০ MB | ১৬,৪৩,৯৬,৫৪৪ |
distilbert/ (১০৪ ভাষার Wikipedia, card অনুযায়ী) | Apache 2.0 | ৬, ৫৪২ MB | ১৩,৪৭,৩৪,০৮০ |
একটি খুঁটিনাটি: Bangla BERT-এর card-এ “110M parameters” লেখা, আমাদের গণনায় প্রায় ১৬.৪ কোটি। বড় vocabulary-র embedding table-ই প্রায় ৭.৮ কোটি। এজন্যই নিজে মাপা ভালো।
Token দেখতে গিয়েই একটি অবাক করা জিনিস পেলাম। এই repository-তে tokenizer_config.json নেই, তাই tokenizer-এর default চলে: lowercase চালু। BERT-এর normalizer-এ lowercase মানে accent মোছাও। Unicode-এর হিসাবে হসন্ত, উ-কার, নুকতা এই “accent”-এর দলে পড়ে (NFC আর Unicode-এর ভিত্তি আছে AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ)।
from transformers import AutoTokenizer
name = "sagorsarker/bangla-bert-base"
for kw in [{}, {"do_lower_case": False}]:
t = AutoTokenizer.from_pretrained(name, **kw)
print(kw, t.convert_ids_to_tokens(t("পুকুরে কুকুর।").input_ids))সত্যিকারের run (একই ধরনের কয়েকটি বাক্য, ছাঁটা):
{} ['[CLS]', 'পক', '##রে', 'কক', '##র', '।', '[SEP]']
{} ['[CLS]', 'কাল', 'এবং', 'কল', '[SEP]'] # ইনপুট: কাল এবং কুল
{} ['[CLS]', 'পাতা', '##গলে', '##ার', 'নিচে', 'ব', '##যা', '##ঙ', 'ছিল', '।', '[SEP]']
{'do_lower_case': False} ['[CLS]', 'পুকুরে', 'কুকুর', '।', '[SEP]']
{'do_lower_case': False} ['[CLS]', 'কাল', 'এবং', 'কুল', '[SEP]']
{'do_lower_case': False} ['[CLS]', 'পাতাগুলো', '##র', 'নিচে', 'ব্যাঙ', 'ছিল', '।', '[SEP]']
“কুল” হয়ে গেল “কল”. “পুকুরে” ভেঙে “পক ##রে”. অথচ vocabulary-তে “পুকুরে”, “কুকুর”, “ব্যাঙ” আছে। ১,০২,০২৫ সারির vocab-এ হসন্তওয়ালা token ৪৪,০২৫টি, যেগুলোতে default-এ পৌঁছানোই যায় না। Model কোনটিতে শেখা হয়েছিল? Card-এর নিজের উদাহরণেও '##য' ছাপা আছে, অর্থাৎ card default-ই দেখিয়েছে। প্রমাণ খুঁজতে দুটি পরীক্ষা করলাম।
# embedding table-এর সারির norm; শুরুর এলোমেলো মান (sd 0.02) হলে প্রায় 0.554
survives stripping tokens 31895 norm mean 0.766 median 0.776
has marks that stripping removes tokens 69080 norm mean 0.784 median 0.798
# fill-mask, শীর্ষ ৩
default | আমি বাংলায় [MASK] গাই। | গান 0.134, ##ও 0.061, সাহিত্য 0.047
do_lower_case=False | আমি বাংলায় [MASK] গাই। | গান 0.705, নাটক 0.036, কবিতা 0.032
default | গাছের [MASK] ঝরে পড়ে। | পাতা 0.582, চারা 0.058, রস 0.027
do_lower_case=False | গাছের [MASK] ঝরে পড়ে। | পাতা 0.316, ডাল 0.099, চারা 0.074
default | বইয়ের [MASK] উল্টে দেখুন। | [UNK] 0.054, জন্য 0.052, মধ্যে 0.035
do_lower_case=False | বইয়ের [MASK] উল্টে দেখুন। | অঙ্ক 0.045, পাতা 0.042, পৃষ্ঠা 0.034
চিহ্নওয়ালা token-এর norm শুরুর মানের চেয়ে অনেক বেশি, অর্থাৎ সেগুলোও শেখার সময় বদলেছে। চিহ্ন-রাখা tokenizer-এ “গান” পায় ০.৭০৫, আর “বইয়ের [MASK]” বাক্যে “পাতা” ও “পৃষ্ঠা” শীর্ষ ৩-এ আসে। “গাছের [MASK]” বাক্যে অবশ্য default-এই “পাতা” বেশি, তাই ছবিটি একমুখী নয়। MLM-এর স্বাদও এখানে: দুই বাক্যে একই ফাঁকা, প্রসঙ্গ ভিন্ন। এটি ইঙ্গিত দেয় যে চিহ্ন-রাখা লেখাই model-এর শেখার সঙ্গে বেশি মেলে; কিন্তু training-এর সেটিং আমরা দেখিনি, তাই এটি অনুমান, প্রমাণ নয়। এই পর্বের বাকি run-এ do_lower_case=False. Card-এ এ নিয়ে কোনো সতর্কতা আমরা পাইনি, তাই নিজের হাতে token ছাপানোই ভরসা। পর্ব ৪০ (Tokenization — BPE, WordPiece, SentencePiece আর বাংলা) normalization নিয়ে বিস্তারিত বলে।
৬ · Token থেকে শব্দ — word_ids() আর pooling
Tokenizer শব্দ ভেঙে subword বানায়। কিন্তু আমাদের দরকার শব্দের vector. Fast tokenizer-এর word_ids() বলে, প্রতিটি token কোন শব্দের; বিশেষ token-এর জন্য None. শব্দ আগে থেকে ভাগ করে is_split_into_words=True দিলে এই নম্বর আপনার তালিকার সূচকই হয়। দুই model-এ একই বাক্য:
['পাতাগুলোর', 'নিচে', 'একটা', 'ছোট', 'ব্যাঙ', 'লুকিয়ে', 'ছিল', '।']
Bangla BERT
['[CLS]', 'পাতাগুলো', '##র', 'নিচে', 'একটা', 'ছোট', 'ব্যাঙ', 'লুকিয়ে', 'ছিল', '।', '[SEP]']
[None, 0, 0, 1, 2, 3, 4, 5, 6, 7, None]
DistilBERT multilingual
['[CLS]', 'প', '##াত', '##াগ', '##ুল', '##োর', 'ন', '##ি', '##চে', 'এক', '##টা', 'ছোট', 'ব', '##্যা', '##ঙ', 'ল', '##ুক', '##িয়ে', 'ছিল', '।', '[SEP]']
[None, 0, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 4, 4, 5, 5, 5, 6, 7, None]
sentences 192 targets with >1 token: Bangla BERT 4, DistilBERT 155
বাংলার জন্য শেখা vocabulary-তে আমাদের ১৯২টি লক্ষ্য-শব্দের মাত্র ৪টি ভাঙে; বহুভাষিক model-এ ১৫৫টি। তাই pooling-এর প্রশ্ন বহুভাষিক model-এ অনেক বড়। সবচেয়ে সরল তিনটি pooling ধাপে ধাপে দেখুন। মানগুলো Bangla BERT-এর শেষ layer-এর ৭৬৮টির প্রথম ৩টি।
| ধাপ | |
| মান |
- মিল.
word_ids-এ শব্দ ০ মানে “পাতাগুলোর”, দুটি token জুড়ে। শব্দের নম্বর আর token-এর নম্বর এক নয়। - সূচক. যে যে token-এ নম্বর ০, তাদের vector তুলে নেওয়া হলো। Shape (2, 768).
- First. প্রথম subword-এর vector. BERT প্রবন্ধে NER-এ এই পথই নেওয়া হয়েছিল। মূল শব্দটি প্রায়ই এতে থাকে, প্রত্যয় বাদ পড়ে।
- Mean. সব subword-এর গড়; হাতে মেলান: (−১.৯৮৭ − ০.৯৬৮)/২ = −১.৪৭৭ (৩ ঘরে ছাঁটা). Shape (768,).
- CLS. শব্দ নয়, পুরো বাক্যের একটি vector. একই বাক্যের সব শব্দের জন্য একই।
- তুলনা. “পাতাগুলো” আর “##র”-এর মধ্যে মিল মাত্র ০.২২৮: একই শব্দের টুকরোও অনেক আলাদা হতে পারে। তাই কোন pooling, সেটি একটি সিদ্ধান্ত।
এই পর্বের মূল function, যেমন চলেছে:
@torch.no_grad()
def word_vector(words, i, layer=-1):
enc = tok(words, is_split_into_words=True, return_tensors="pt")
hs = model(**enc).hidden_states[layer][0] # (T, 768)
idx = [t for t, w in enumerate(enc.word_ids(0)) if w == i]
return hs[idx].mean(0).numpy()output_hidden_states=True দিয়ে model লোড করলে Bangla BERT-এ hidden_states ১৩টি: layer ০ (embedding) আর ১২টি Transformer layer. শেষেরটি last_hidden_state-এর হুবহু সমান; run-এ যাচাই করেছি। Batch-এ padding থাকলে বাক্যের গড় নিতে attention_mask লাগে। একটি ছোট run-এ mask ছাড়া আর mask-সহ গড়ের cosine ০.৯৫: কোনো error নেই, শুধু vector ভুল (পর্ব ২৭)।
৭ · Cosine দিয়ে মাপা — একই অর্থ বনাম ভিন্ন অর্থ
হাতে লেখা বাক্য, Bangla BERT, mean pooling. তিন কলাম: শেষ layer, layer ০, আর input table. সত্যিকারের run:
পাতা শেষ | layer0 | table
গাছের পাতা বাতাসে / শীতের… আমগাছের পাতা 0.813 | 0.931 | 1.000 একই অর্থ
বইয়ের পাতা উল্টে / খাতার পাতা ছিঁড়ে 0.786 | 1.000 | 1.000 একই অর্থ
গাছের পাতা বাতাসে / বইয়ের পাতা উল্টে 0.687 | 1.000 | 1.000 ভিন্ন
শীতের… আমগাছের পাতা / বইয়ের পাতা 0.670 | 0.931 | 1.000 ভিন্ন
কাল
আমি বাজারে গিয়েছিলাম / রাতে বৃষ্টি হয়েছিল 0.762 | 1.000 | 1.000 একই (গতকাল)
আমি বাজারে যাব / রাতে বৃষ্টি হতে পারে 0.730 | 1.000 | 1.000 একই (আগামীকাল)
আমি বাজারে গিয়েছিলাম / আমি বাজারে যাব 0.874 | 1.000 | 1.000 ভিন্ন!
রাতে বৃষ্টি হয়েছিল / রাতে বৃষ্টি হতে পারে 0.899 | 1.000 | 1.000 ভিন্ন!
আমি বাজারে গিয়েছিলাম / সেই কালে গ্রামে… 0.235 | 0.218 | 0.158 ভিন্ন (কাল / কালে)
এখান থেকে চারটি কথা। এক, input table-এ একই বানানে সব ১.০০০: এটিই static embedding. দুই, layer ০-তেই ০.৯৩১ কেন? কারণ এখানে position embedding যোগ হয়ে গেছে। শব্দটি একই অবস্থানে থাকলে ১.০০০, ভিন্ন অবস্থানে ০.৯৩১. তাই “layer ০ = স্থির” পুরোপুরি ঠিক নয়। তিন, “পাতা”-য় একই অর্থের জোড়া সত্যিই বেশি কাছে। চার, “কাল”-এ ফাঁদ: ভিন্ন অর্থের “আমি বাজারে গিয়েছিলাম / যাব” (০.৮৭৪) একই অর্থের দুই ভিন্ন বাক্যের (০.৭৬২) চেয়ে কাছে। Cosine অর্থের পাশাপাশি বাক্যের বাকি অংশের মিলও মাপে।
কয়েকটি বাছাই জোড়া দিয়ে সিদ্ধান্ত নেওয়া ঠিক নয়। তাই সব জোড়ায় হিসাব করি। AUC মানে এলোমেলো একটি একই-অর্থের জোড়ার cosine একটি ভিন্ন-অর্থের জোড়ার চেয়ে বেশি হওয়ার সম্ভাবনা; ০.৫ মানে আলাদা করতে পারছে না।
Bangla BERT, mean pool
পাতা layer 0: same 0.593 diff 0.525 AUC 0.557
পাতা layer 12: same 0.524 diff 0.433 AUC 0.675
কাল layer 0: same 0.892 diff 0.562 AUC 0.738
কাল layer 12: same 0.627 diff 0.412 AUC 0.760
হার layer 0: same 0.715 diff 0.712 AUC 0.520
হার layer 12: same 0.486 diff 0.364 AUC 0.742
DistilBERT multilingual, mean pool
পাতা layer 6: same 0.914 diff 0.899 AUC 0.622
কাল layer 6: same 0.929 diff 0.900 AUC 0.717
হার layer 6: same 0.849 diff 0.841 AUC 0.546
“হার”-এ layer ০-র AUC ০.৫২০ থেকে শেষ layer-এ ০.৭৪২: প্রসঙ্গের প্রভাব স্পষ্ট। “কাল”-এ layer ০-তেই ০.৭৩৮, কারণ “কালে”, “কালের” মতো বানান যুগের অর্থ আলাদা করে দেয়; এটি প্রসঙ্গের কৃতিত্ব নয়। DistilBERT-এ সব মান ০.৮৪-এর ওপরে: এক model-এর cosine অন্য model-এর সঙ্গে সরাসরি তুলনা করা যায় না। আর AUC কোনোটিই ০.৮-এ পৌঁছায়নি; সরল cosine একা অর্থ-বিচারক হিসেবে ঝাপসা।
৮ · Sense probe — layer আর pooling তুলনা
Probe মানে model স্থির রেখে তার vector-এর ওপর একটি ছোট classifier বসানো: এই vector থেকে অর্থ বলা যায় কি না। ডাক্তার যেমন পরীক্ষায় ধরেন শরীরে কোন উপাদান আছে; রোগ কেন হলো, তা এতে বলা হয় না।
ডেটা: এই পর্বের জন্য হাতে লেখা ১৯২টি বাক্য, কোনো public dataset নয়। “পাতা” (গাছের ৩০, বইয়ের ৩০), “কাল” (গতকাল ৩০, আগামীকাল ৩০, যুগ/সময় ২৪), “হার” (গলার ২৪, পরাজয় ২৪). একজনের লেখা, একজনের দেওয়া label; বাস্তব ডেটা হলে দ্বিতীয় annotator আর agreement মাপা লাগত (মেশিন লার্নিং কোর্সের পর্ব ৫)। পদ্ধতি: প্রতি শব্দে ৭০/৩০ stratified ভাগ (seed ৪২), পরীক্ষায় মোট ৫৯টি বাক্য। Standard scale করে logistic regression. Layer আর pooling বাছাই শুধু train অংশের ৫-fold cross-validation-এ; পরীক্ষার ডেটা ছোঁয়া হয়নি শেষ ধাপের আগে।
সত্যিকারের run (Bangla BERT, CPU-তে পুরো script প্রায় ৩০ সেকেন্ড, train CV accuracy, ছাঁটা):
layer first mean last cls
0 0.548 0.555 0.548 0.415
1 0.744 0.750 0.750 0.781
2 0.840 0.848 0.848 0.817
4 0.848 0.840 0.840 0.826
6 0.909 0.901 0.901 0.864
7 0.948 0.940 0.940 0.895
8 0.962 0.954 0.947 0.917
10 0.962 0.947 0.947 0.902
11 0.918 0.918 0.910 0.894
12 0.955 0.955 0.947 0.902
last4 0.940 0.932 0.932 0.909
all 0.925 0.910 0.910 0.916
selected: (8, 'first') 0.962
ধারা স্পষ্ট: layer ০ থেকে ৭ পর্যন্ত মোটের ওপর বাড়ে, তারপর প্রায় সমতল, সামান্য ওঠানামা। Layer ৮ “জিতেছে” ঠিকই, কিন্তু layer ১০-এর সঙ্গে সমান, আর পরের layer ১১ একটু নিচে: বাছাইটির অনেকখানিই noise. Bangla BERT-এ first, mean আর last প্রায় এক, কারণ মাত্র ৪টি লক্ষ্য-শব্দ ভাঙে। এবার একবারই পরীক্ষার ডেটা:
HELD-OUT TEST (70/30 split, seed 42), Bangla BERT
majority class 25/59 = 0.424 [0.306, 0.551]
static: target vector only 36/59 = 0.610 [0.483, 0.724]
static: mean of neighbours 48/59 = 0.814 [0.696, 0.893]
first layer 0 30/59 = 0.508 [0.384, 0.632]
cls last layer 51/59 = 0.864 [0.755, 0.930]
mean last layer 54/59 = 0.915 [0.816, 0.963]
SELECTED first layer 8 54/59 = 0.915 [0.816, 0.963]
nearest centroid, selected 52/59 = 0.881
DistilBERT multilingual (selected: first, layer 5)
static: mean of neighbours 47/59 = 0.797 [0.677, 0.880]
cls last layer 43/59 = 0.729 [0.604, 0.826]
mean last layer 46/59 = 0.780 [0.659, 0.866]
SELECTED first layer 5 47/59 = 0.797 [0.677, 0.880]
দুটি baseline বুঝে নিন। “target vector only”: শুধু লক্ষ্য-শব্দের table-এর vector. এটিও ০.৬১০, কারণ “কালে”, “পাতায়” ইত্যাদি বানানই কিছু খবর দেয়। “mean of neighbours”: বাকি শব্দের table-vector-এর গড়, মানে স্থির embedding-ই, কিন্তু প্রসঙ্গসহ। এটি ০.৮১৪, সহজে হার মানে না। দ্বিতীয় নজর: বহুভাষিক DistilBERT-এর শেষ layer আমাদের ডেটায় নিজের স্থির baseline-কে ছাড়াতে পারেনি। কারণ এই একটি পরীক্ষা থেকে বলা যায় না; ছোট model আর শব্দের টুকরো হওয়া একটি সম্ভাব্য ব্যাখ্যা মাত্র।
ভুলগুলো পড়ুন, শুধু সংখ্যা নয়। বাছাই করা probe-এর ৫৯-এর ৫টি ভুল, পুরোটাই:
পাতা true leaf pred page | টবের গাছটার পাতাগুলো হলুদ হয়ে গেছে।
পাতা true leaf pred page | পাতাগুলোর নিচে একটা ছোট ব্যাঙ লুকিয়ে ছিল।
পাতা true leaf pred page | বটগাছের একটা পাতা ছিঁড়ে সে বাঁশির মতো বাজাল।
কাল true yesterday pred tomorrow | কাল সকালে পার্সেলটা ফেরত পাঠিয়েছি।
কাল true yesterday pred tomorrow | কাল বিলটা পরিশোধ করে দিয়েছি।
দুটি “কাল”-এর ভুলেই ক্রিয়াটি “-য়েছি” রূপে, “গিয়েছিলাম”-এর মতো সুস্পষ্ট অতীত নয়। “পাতা”-র প্রথম আর তৃতীয় বাক্যে গাছের কথা আছে, তবু probe “পৃষ্ঠা” বলেছে; দ্বিতীয়টিতে প্রসঙ্গই দুর্বল। এত কম উদাহরণে নিয়ম বের করা যায় না, কিন্তু কোন ধরনের বাক্য বেশি জোগাড় করবেন, তার ইঙ্গিত এখান থেকেই আসে।
সবচেয়ে পরিষ্কার পরীক্ষা: শুধু ঠিক “কাল” বানান, গতকাল বনাম আগামীকাল, ৬০টি বাক্য, পরীক্ষায় ১৮টি:
distinct static vectors: 1
static target 9/18 = 0.500 [0.290, 0.710]
static neighbours 16/18 = 0.889 [0.672, 0.969]
mean last 17/18 = 0.944 [0.742, 0.990]
cls last 16/18 = 0.889 [0.672, 0.969]
Table-এ একটিই vector, তাই ফল ঠিক ০.৫০০: এর চেয়ে ভালো হওয়ার উপায় নেই। প্রসঙ্গ আনলেই লাফ, কারণ ক্রিয়ার রূপ (“যাব”, “গিয়েছিলাম”) বড় সংকেত। আর শেষ বিচার: একই শব্দ এক বাক্যে দুই অর্থে। প্রতি শব্দের সব বাক্যে শেখানো probe:
[word] বইয়ের পাতার ফাঁকে গাছের পাতা রেখে দিয়েছি। → পাতার: leaf 0.635 (ভুল) পাতা: leaf 0.963
[cls ] একই বাক্য → দুটিই page 0.552
[word] কাল বাজারে গিয়েছিলাম, কাল আবার যাব। → yesterday 0.561, tomorrow 0.976
[cls ] একই বাক্য → দুটিই tomorrow 0.976
[word] সোনার হার বাজি রেখে খেলায় হার হলো। → necklace 0.611, defeat 0.997
[cls ] একই বাক্য → দুটিই defeat 0.998
শব্দের vector (first, layer ৮) ৬টির ৫টিতে ঠিক। CLS (শেষ layer) গঠনগতভাবেই দুটিকে এক label দেয়, তাই সর্বোচ্চ ৩টি, এখানেও ৩টিই। মাত্র ৩টি বাক্য, তাই এটি উদাহরণ, পরিসংখ্যান নয়। তবে নীতিটি হিসাব ছাড়াই সত্য: শব্দের অর্থ চাইলে শব্দের vector নিন। পরের অংশে দেখব, এই সব সংখ্যা কতটা বিশ্বাস করা যায়।
৯ · Probe-এর ফল সৎভাবে পড়া
আবহাওয়ার পূর্বাভাসের কথা ভাবুন। “কাল বৃষ্টি হবে” বলে একবার মিলে গেলেই কেউ ভালো পূর্বাভাসদাতা হন না। দেখতে হয় কতবার মিলল, সবসময় “বৃষ্টি হবে না” বলা লোকের চেয়ে কতটা ভালো, আর এলোমেলো বললেও কতটা মিলত।
| ধাপ | |
| মান |
- কাঁচা ফল. ৫৪/৫৯. একটি বাক্য এদিক-ওদিক হলেই প্রায় ১.৭ শতাংশ বিন্দু বদলে যায়।
- Baseline. সবচেয়ে সাধারণ label বললেই ০.৪২৪. আসল প্রতিদ্বন্দ্বী প্রসঙ্গসহ স্থির গড়, ০.৮১৪.
- ব্যবধান. Wilson ব্যবধান দুটি অনেকটা মিলে আছে। ৫৯টি বাক্যে সূক্ষ্ম পার্থক্য ধরা কঠিন।
- জোড়া তুলনা. একই বাক্যে দুই model. মোট ১০টি মতভেদ, ৮টিতে BERT ঠিক। McNemar p ≈ ০.১০৯, প্রচলিত ০.০৫-এর নিচে নয়। দিকটি ইঙ্গিতপূর্ণ, প্রমাণ নয়।
- Control. এলোমেলো label-এও train accuracy ১.০০০: ৭৬৮ মাত্রায় ৪২টি বিন্দু মুখস্থ করা সহজ। তাই train-এর সংখ্যা অর্থহীন। Test প্রায় আন্দাজের স্তরে (০.৫, ০.৩৩, ০.৫-এর কাছে), যা স্বস্তির।
- সীমা. অস্পষ্ট বাক্যেও probe ০.৯৩৭ নিশ্চিত। Probability-কে আত্মবিশ্বাসের মাপ ধরা যায় না। “Vector থেকে অর্থ পড়া যায়” মানে এই নয় যে model “অর্থ বোঝে”।
মোট ৩০টি ভিন্ন ৭০/৩০ ভাগে পুনরাবৃত্তি করলে:
30 random 70/30 splits; pooled accuracy mean, min, max
static neighbours mean 0.808 sd 0.051 min 0.712 max 0.898
cls last mean 0.916 sd 0.034 min 0.831 max 1.000
mean last mean 0.934 sd 0.033 min 0.847 max 0.983
Bangla BERT এখানে গড়ে স্পষ্টতই এগিয়ে, কিন্তু একই ১৯২টি বাক্য বারবার ভাগ হচ্ছে, তাই ভাগগুলো স্বাধীন নয়, আর sd-কে নতুন ডেটার অনিশ্চয়তা ধরা যায় না (মেশিন লার্নিং কোর্সের পর্ব ২১, পর্ব ২২)। সবচেয়ে বড় সীমা অন্য জায়গায়: বাক্য একজনের হাতে লেখা, শব্দ আমরাই বেছেছি, অনেক বাক্যে সংকেত-শব্দ চোখে পড়ার মতো। বাস্তব লেখায় ফল ভিন্ন হতেই পারে। আর tokenizer-এর default-এ একই পরীক্ষার ফল ছিল: mean last ৫৫/৫৯, static neighbours ৪১/৫৯, ৩০ ভাগে গড় ০.৯২৭ ও ০.৬৬৫. অর্থাৎ probe-এর accuracy-তে default-এর ক্ষতি এখানে প্রায় চোখেই পড়ে না, স্থির গড়ে পড়ে। একটি ভালো সংখ্যা tokenizer-কে নির্দোষ প্রমাণ করে না।
নিজে দেখুন: খাঁটি noise-এ probe কী করে, আর আমাদের সংখ্যার ব্যবধান ও p-value কত। প্রথমবার scikit-learn লোড হতে কয়েক সেকেন্ড লাগে।
import numpy as np
from math import comb, sqrt
from sklearn.linear_model import LogisticRegression
def wilson(k, n, z=1.96):
p = k / n
d = 1 + z*z/n
c = (p + z*z/(2*n)) / d
h = z * sqrt(p*(1-p)/n + z*z/(4*n*n)) / d
return round(c - h, 3), round(c + h, 3)
def mcnemar_exact(b, c): # শুধু মতভেদগুলো দেখে
n = b + c
tail = sum(comb(n, i) for i in range(min(b, c) + 1))
return min(1.0, 2 * tail / 2**n)
n_tr, n_te, dim = 42, 18, 768 # “পাতা”-র মাপ, কিন্তু vector আর label খাঁটি noise
for seed in range(3):
r = np.random.default_rng(seed)
Xtr, Xte = r.normal(size=(n_tr, dim)), r.normal(size=(n_te, dim))
ytr, yte = r.integers(0, 2, n_tr), r.integers(0, 2, n_te)
c = LogisticRegression(C=1.0, max_iter=2000).fit(Xtr, ytr)
print(f"noise, seed {seed}: train {c.score(Xtr, ytr):.3f} test {c.score(Xte, yte):.3f}")
for k, n in [(54, 59), (48, 59), (17, 18)]:
print(f"{k}/{n} = {k/n:.3f}, Wilson 95% = {wilson(k, n)}")
print("McNemar exact, 8 বনাম 2: p =", round(mcnemar_exact(8, 2), 4))
print("McNemar exact, 3 বনাম 0: p =", round(mcnemar_exact(3, 0), 4))আউটপুট (পাতার Python-এ চালানো):
noise, seed 0: train 1.000 test 0.444
noise, seed 1: train 1.000 test 0.611
noise, seed 2: train 1.000 test 0.500
54/59 = 0.915, Wilson 95% = (0.816, 0.963)
48/59 = 0.814, Wilson 95% = (0.696, 0.893)
17/18 = 0.944, Wilson 95% = (0.742, 0.99)
McNemar exact, 8 বনাম 2: p = 0.1094
McNemar exact, 3 বনাম 0: p = 0.25
Noise-এ train ১.০০০, test আন্দাজের কাছাকাছি, আর ১৮টি বাক্যে সেই আন্দাজও ০.৪৪৪ থেকে ০.৬১১ পর্যন্ত নাচে। হাতে Wilson: p = ৫৪/৫৯ = ০.৯১৫৩, z = ১.৯৬ হলে d = ১ + z²/n = ১.০৬৫১; কেন্দ্র (p + z²/2n)/d = ০.৮৮৯৯; অর্ধ-প্রস্থ z√(p(1−p)/n + z²/4n²)/d = ০.০৭৩৪. ব্যবধান প্রায় [০.৮১৬, ০.৯৬৩]। পূর্ণ F1 আর bootstrap-এর আলোচনা পর্ব ৪৫-এ (NLP মূল্যায়ন — F1, BLEU, ROUGE, chrF আর মানুষের বিচার)।
আরও গভীরে: ELMo-র সামনের আর পেছনের LSTM আলাদা শেখে, আর দুই দিকের vector শুধু শেষে জোড়া লাগে। BERT-এ self-attention-এর প্রতিটি layer-এই দুই পাশ মেশে। Hewitt ও Liang (EMNLP ২০১৯) probe-এর জন্য “selectivity” প্রস্তাব করেন: আসল কাজের accuracy থেকে একটি random-label control task-এর accuracy বাদ। আর Reif ও সহকর্মীরা (২০১৯) BERT-এর vector দিয়ে nearest-neighbour পদ্ধতিতে word-sense-এর হিসাব দেখান।
পরীক্ষা ও ইন্টারভিউতে: Static আর contextual-এর পার্থক্য? Static-এ বানান-প্রতি এক সারি; contextual-এ বাক্যের ওপর নির্ভরশীল output. ELMo-র সূত্র? γ∑sjhj, s = softmax(a). BERT-এর input-এ কী কী যোগ হয়? Token + segment + position embedding. hidden_states-এ কয়টি? L + ১, প্রথমটি embedding-এর output. Word-এর vector কীভাবে? word_ids() দিয়ে subword বের করে first বা mean; CLS দিয়ে নয়। Train accuracy ১.০০ মানে কী? n < d হলে প্রায় কিছুই না; control task চালান।
১০ · AI-এর যুগে — contextual vector-এর কোড যাচাই
AI assistant এই কোড অল্প সময়ে লিখে দেয়, আর বেশির ভাগ ভুলে কোনো error আসে না। নিচের সব সংখ্যা আমাদের নিজের ছোট run-এর।
| যে ভুল প্রায়ই দেখা যায় | ফল | কীভাবে ধরবেন |
|---|---|---|
model.eval() নেই | Dropout চলে; একই input দুইবারে cosine ০.৮৪৯৬ (eval-এ ১.০০০০) | একই input দুইবার চালিয়ে মেলানো |
| শব্দের সূচককে token-এর সূচক ধরা | “পাতা”-র বদলে “গাছের”-এর vector; cosine ০.৫১৪ | word_ids() ছাপানো, ছোট বাক্যে হাতে মেলানো |
| Tokenizer-এর default না দেখা | “কুল” হয়ে যায় “কল” | নিজের লেখায় token ছাপিয়ে পড়া |
attention_mask ছাড়া গড় | Pad-ও গড়ে মেশে; cosine ০.৯৫ | একা আর batch-এর ফল মেলানো |
hidden_states[12] আর “layer ১২”-এর গোলমাল | ৬ layer-এর model-এ IndexError, বা অন্য layer | দৈর্ঘ্য ছাপানো; নিজের model-এর L দেখা |
| Test-এ layer বাছা, train accuracy-কে ফল বলা | অতি-আশাবাদী সংখ্যা | Train-এর CV-তে বাছাই; একবার test |
| “Model অর্থ বোঝে” বা ভুয়া বাংলা model-এর নাম | অতিরঞ্জন, বা নেই এমন checkpoint | Model card খোলা; probe-এর সীমা লেখা |
AI-কে কোডের সঙ্গে প্রমাণও চাইতে বলুন:
AI-এর দেওয়া contextual vector-এর কোড হাতে পেলে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১এক শব্দের কয়েকটি vector রাখলেই তো হতো: “পাতা-১”, “পাতা-২”। তাহলে contextual embedding লাগল কেন?
এই চেষ্টা সত্যিই হয়েছিল। Reisinger ও Mooney (২০১০) একটি শব্দের প্রসঙ্গগুলোকে cluster করে প্রতিটি cluster-এর জন্য আলাদা vector বানান; এই ধারাকে বলে multi-prototype. পরে Neelakantan ও সহকর্মীরা (২০১৪) skip-gram-এর মধ্যেই শব্দ-প্রতি একাধিক vector শেখার পথ দেখান। Arora ও সহকর্মীরা (২০১৮) ভিন্ন দিক থেকে দেখান, একটি স্থির vector-এর ভেতর থেকেই sparse coding দিয়ে অর্থের কাছাকাছি vector উদ্ধার করা যায়।
তাহলে সমস্যা কোথায়? প্রথমত, অর্থ কয়টি, তা আগে থেকে ঠিক করা কঠিন; “কাল”-এর ক্ষেত্রে গতকাল, আগামীকাল, সময়, এমনকি ব্যাকরণের “কাল” কি আলাদা? দ্বিতীয়ত, নতুন বাক্য এলে কোন vector নেব, সেই সিদ্ধান্তের জন্য আবার প্রসঙ্গ পড়তে হয়। অর্থাৎ একটি প্রসঙ্গ-পাঠক এমনিতেই লাগে। তৃতীয়ত, বাক্যে শব্দের ভূমিকা অসংখ্য সূক্ষ্মভাবে বদলায়: “পাতা” কর্তা না কর্ম, ইতিবাচক না নেতিবাচক প্রসঙ্গে; এত কিছু কয়েকটি স্থির vector-এ ধরা যায় না।
Contextual model এই ধাপটি সরিয়ে দেয়। অর্থের তালিকা না বানিয়েই প্রতিটি ব্যবহারে একটি vector দেয়। তবে এর দাম আছে। স্থির table-এ এক সারি পড়া প্রায় বিনামূল্যে; contextual-এ প্রতিবার একটি পুরো network চালাতে হয়, আর আমাদের Bangla BERT-এর জন্য ৬৬০ MB ফাইল। বাছাই করে দেখুন: পর্ব ৪১-এর স্থির vector অনেক ছোট কাজে এখনো যথেষ্ট হতে পারে। ওপরের run-এও স্থির গড় ০.৮১৪ পেয়েছে, যা ফেলনা নয়।
মূল উপলব্ধি: অর্থের সংখ্যা ঠিক করার বদলে প্রসঙ্গ থেকে vector হিসাব করা একটি নকশার বদল, যার দাম গণনার খরচে দিতে হয়।
প্র ০২ELMo তো ভালোই কাজ করছিল। মাত্র কয়েক মাসের মধ্যে BERT কেন আলোচনার কেন্দ্রে চলে এল? আর ELMo কি এখন অকেজো?
সময়রেখাটি দেখুন। ELMo-র প্রবন্ধ arXiv-এ আসে ২০১৮-র ফেব্রুয়ারিতে, ULMFiT-ও সেই বছরের শুরুতে, প্রথম GPT জুনের দিকে, আর BERT অক্টোবরে। অল্প সময়ে একই ধারণার কয়েকটি রূপ এল: আগে বিশাল লেখায় শিখুন, পরে কাজে লাগান। এদের একটির জয়ের কারণ একটিই, এমন বলা কঠিন; কয়েকটি কারণ একসঙ্গে কাজ করেছে বলে ধারণা করা যায়।
গণনায় সুবিধা একটি কারণ। LSTM-এ token একে একে পড়তে হয়। Self-attention-এ পুরো বাক্য একসঙ্গে matrix গুণে চলে, GPU-তে যা অনেক সহজ। দ্বিতীয় কারণ শেখার লক্ষ্য। ELMo-র দুটি LSTM আলাদা, একটি শুধু আগের দিক, অন্যটি শুধু পরের দিক দেখে শেখে। BERT-এর MLM-এ প্রতিটি layer-এই দুই পাশ একসঙ্গে দেখা যায়। তৃতীয় কারণ ব্যবহার। ELMo সাধারণত স্থির থাকে, ওপরে আলাদা একটি model বসে। BERT-এ পুরো model fine-tune করা ছিল প্রধান পথ, আর প্রবন্ধটি নিজের হিসাবে কয়েকটি পরিচিত benchmark-এ আগের ফল ছাড়িয়ে যাওয়ার কথা জানায়। মুক্ত কোড আর pretrained checkpoint-ও প্রসারে ভূমিকা রাখে।
তবে পুরো গল্প জয়-পরাজয়ের নয়। ELMo-র scalar mix ধারণাটি থেকে গেছে: BERT-এর প্রবন্ধেও layer-এর weighted sum পরীক্ষা হয়েছে, আর পরের অনেক কাজ layer মেশানো ব্যবহার করে। RNN-ভিত্তিক model-ও ছোট ডেটা, কম memory বা ধারাবাহিক input-এ যুক্তিসংগত হতে পারে; “অকেজো” বলার মতো সাধারণ প্রমাণ নেই। বাংলার মতো ভাষায় কোন পথ ভালো, তা ডেটা আর হাতের checkpoint-এর ওপর নির্ভর করে; নিজে baseline-এর সঙ্গে মিলিয়ে দেখুন।
মূল উপলব্ধি: একটি model-এর জনপ্রিয়তা শুধু accuracy-র ফল নয়; গণনার সুবিধা, শেখার লক্ষ্য আর ব্যবহারের সহজতাও বড় ভূমিকা রাখে।
প্র ০৩কোন layer আর কোন pooling সবচেয়ে ভালো? এর কি কোনো সর্বজনীন উত্তর আছে?
না। BERT প্রবন্ধের একটি পরিচিত টেবিলে CoNLL-2003 NER-এর dev F1 দেওয়া আছে, যেখানে BERT-এর vector-এর ওপর একটি BiLSTM বসানো: শুধু embedding ৯১.০, শেষ hidden layer ৯৪.৯, শেষের আগেরটি ৯৫.৬, শেষ চারটির weighted sum ৯৫.৯, সব ১২টির weighted sum ৯৫.৫, শেষ চারটি জোড়া ৯৬.১. পুরো fine-tuning-এ ৯৬.৪. অনেকে এখান থেকে “শেষ চারটি জুড়ে দিন” নিয়মটি নেন। কিন্তু এটি একটি ইংরেজি dataset-এর একটি কাজের ফল, সর্বজনীন আইন নয়।
Tenney, Das ও Pavlick (২০১৯) probe দিয়ে দেখান, কিছু কাজের সংকেত BERT-এর নিচের layer-এ, কিছু ওপরের দিকে বেশি মেলে; তাঁরা একে “classical NLP pipeline”-এর সঙ্গে তুলনা করেন। পরের আলোচনায় এই ব্যাখ্যা নিয়ে প্রশ্নও উঠেছে, আর Rogers ও সহকর্মীদের BERT-সমীক্ষা (২০২০) জানায়, এই ক্ষেত্রের অনেক ফল পদ্ধতির ওপর নির্ভরশীল।
আমাদের ছোট run-ও একই কথা বলে। Bangla BERT-এ layer ৭-এর পর সংখ্যাগুলো প্রায় একই সীমায় ওঠানামা করে। First, mean, last প্রায় এক, কারণ মাত্র ৪টি শব্দ ভাঙে। বহুভাষিক model-এ ১৫৫টি ভাঙে, আর সেখানে last-subword স্পষ্টতই পিছিয়ে (যেমন শেষ layer-এ mean ০.৮৫০, last ০.৮১১)। কারণ হিসেবে অনুমান করা যায়, বাংলায় শেষের টুকরোটি প্রায়ই প্রত্যয় বা কার-চিহ্ন, তবে আমরা এটি আলাদা করে মাপিনি।
ব্যবহারিক পথ: এক বা দুটি যুক্তিসংগত বিকল্প বাছুন (শেষ layer বা শেষ চারটির গড়; first বা mean), train-এর cross-validation-এ তুলনা করুন, পার্থক্য ছোট হলে সরল ও সস্তাটি রাখুন। সূক্ষ্ম বাছাইয়ের চেয়ে সাধারণত fine-tuning (পর্ব ৫৪) আর ডেটার মান বড় ভূমিকা রাখে; তবে তা-ও মেপে দেখার বিষয়।
মূল উপলব্ধি: Layer আর pooling হলো hyperparameter: ভাষা, tokenizer আর কাজ বদলালে নতুন করে মাপতে হয়।
প্র ০৪Probe যদি ঠিকঠাক অর্থ বের করতে পারে, তাহলে কি বলা যায় model নিজের কাজে সেই অর্থ ব্যবহার করে?
না, এটিই probing-এর মূল সতর্কতা। Probe দেখায়, তথ্যটি vector থেকে কোনো একটি function দিয়ে পড়া যায়। Model সেই তথ্য নিজের পরের ধাপে ব্যবহার করে কি না, সেটি আলাদা প্রশ্ন। থার্মোমিটারে জ্বর মাপা যায় বলেই শরীর থার্মোমিটার দেখে কাজ করে না।
দ্বিতীয় সমস্যা, probe-এর নিজের শক্তি। Probe যথেষ্ট শক্তিশালী হলে দুর্বল সংকেতকেও, এমনকি noise-কেও, মুখস্থ করে ফেলতে পারে। আমাদের run-এ এলোমেলো label-এও train accuracy ১.০০০. Hewitt ও Liang (EMNLP ২০১৯) এ জন্য control task-এর প্রস্তাব করেন: প্রতিটি শব্দের জন্য এলোমেলো label বানিয়ে একই probe চালান। আসল কাজের accuracy আর control-এর accuracy-র পার্থক্যকে তাঁরা selectivity বলেন। প্রবন্ধটির একটি ELMo উদাহরণে, POS কাজে MLP probe-এর accuracy ৯৭.৩, control ৯২.৮; linear probe-এর ৯৭.২, control ৭১.২. প্রায় একই accuracy, কিন্তু linear probe-এর selectivity অনেক বেশি। একা accuracy কত ভুল বোঝাতে পারে, তার ভালো উদাহরণ।
আমাদের control এর চেয়ে সরল (shuffled label, আলাদা test), আর এটি তাঁদের পদ্ধতির হুবহু রূপ নয়। ব্যবহার সত্যিই হচ্ছে কি না জানতে অন্য পদ্ধতি লাগে: সেই তথ্য vector থেকে সরিয়ে দেখা model-এর আচরণ বদলায় কি না (যেমন Elazar ও সহকর্মীদের amnesic probing, ২০২১)। Belinkov-এর (২০২২) সমীক্ষা probing-এর এই প্রতিশ্রুতি ও সীমা একত্রে আলোচনা করে।
তাই “BERT অর্থ বোঝে” কোনো probe-ভিত্তিক সিদ্ধান্ত হতে পারে না। সৎ বাক্যটি হবে: “এই ছোট ডেটায়, এই layer-এর vector থেকে একটি linear classifier অর্থের label-এর সঙ্গে এতটা মিল পেয়েছে।”
মূল উপলব্ধি: Probe বলে তথ্য আছে; ব্যবহার হচ্ছে কি না, তা প্রমাণ করতে হস্তক্ষেপ-ভিত্তিক পরীক্ষা লাগে।
প্র ০৫“অর্থ” কি সত্যিই আলাদা আলাদা বাক্সে থাকে? “পাতা” বা “কাল”-এর label দেওয়ার সমস্যা কী?
আমরা probe-কে দুই বা তিনটি পরিষ্কার label দিয়েছি। ভাষা অত পরিষ্কার নয়। Adam Kilgarriff-এর ১৯৯৭ সালের “I don't believe in word senses” প্রবন্ধটি এই সন্দেহের একটি পরিচিত সূত্র: অর্থগুলো ব্যবহারের গুচ্ছ থেকে তৈরি, উদ্দেশ্যভেদে বদলায়, প্রকৃতিতে আগে থেকে ভাগ করা নয়। অভিধান ও WordNet-এর মতো সম্পদ অর্থের ভাগে একে অন্যের সঙ্গে মেলে না, আর খুব সূক্ষ্ম ভাগে মানুষের মধ্যেও মতভেদ বাড়ে বলে এই গবেষণাধারায় বারবার জানা গেছে।
বাংলায় উদাহরণ সহজেই পাওয়া যায়। “পাতা” গাছের, বইয়ের, আবার “ফাঁদ পাতা”, “বিছানা পাতা”-য় ক্রিয়া; “চোখের পাতা”-ও আছে। আমাদের ডেটায় শুধু দুটি অর্থ রাখা হয়েছে, কারণ কাজ ছোট রাখতে চেয়েছি। “কাল”-এ “গতকাল” আর “আগামীকাল” কি সত্যিই ভিন্ন অর্থ, নাকি একই শব্দ, যার সময়টি ক্রিয়া ঠিক করে? ভাষাবিদেরা যা-ই বলুন, আমাদের probe আসলে ক্রিয়ার রূপও ধরতে পারে; “static neighbours”-এর ০.৮৮৯ সেই ইঙ্গিতই দেয়। আবার “পাতাটা ছিঁড়ে গেছে” বাক্যে label দেওয়াই যায় না, তবু probe ০.৯৩৭ নিশ্চিত। এই আত্মবিশ্বাস calibration-এর প্রশ্ন (মেশিন লার্নিং কোর্সের পর্ব ২৬)।
বাস্তব প্রকল্পে তাই আগে ঠিক করুন, অর্থের ভাগ কী কাজের জন্য। বাজারঘরের খোঁজে “হার” গয়না না খেলার খবর, এই ভাগই যথেষ্ট হতে পারে। Label-এর নির্দেশিকা লিখুন, অন্তত দুজনকে দিয়ে দাগান, kappa মাপুন (মেশিন লার্নিং কোর্সের পর্ব ৫), অস্পষ্ট শ্রেণি রাখুন। আমরা এর কিছুই করিনি, তাই ফলের ওপর ভরসাও সেই মাপেরই হওয়া উচিত। উপভাষা আর লেখার রীতি (সাধু, চলিত, Banglish) ভিন্ন হলে ফল বদলাতে পারে।
মূল উপলব্ধি: Label হলো কাজের জন্য তৈরি একটি মডেল, ভাষার চূড়ান্ত সত্য নয়; তাই সংখ্যার মান label-এর মানের চেয়ে বেশি হতে পারে না।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান।
-
পূর্ণ numerical: (ক) একটি শব্দের তিনটি layer: h₀ = [1, 0], h₁ = [1, 2], h₂ = [0, 2]; a = [0, 1, 1], γ = ২. ELMo-র vector কত? (খ) সেই মিশ্রণের সঙ্গে [0, 1]-এর cosine কত, আর শুধু h₀-এর সঙ্গে [0, 1]-এর? (গ) একটি probe ৩০টি পরীক্ষার বাক্যে ২৮টি ঠিক করল। Wilson ৯৫% ব্যবধান কত? (ঘ) দুই model-এর মতভেদ ১০টি: ৮টিতে A ঠিক, ২টিতে B ঠিক। McNemar exact p কত?
(ক) s = softmax(a) = [০.১৫৫৪, ০.৪২২৩, ০.৪২২৩]. মিশ্রণ = ০.১৫৫৪·[1,0] + ০.৪২২৩·[1,2] + ০.৪২২৩·[0,2] = [০.৫৭৭৭, ১.৬৮৯৩]. × ২ = [১.১৫৫৪, ৩.৩৭৮৬]. (খ) Cosine = ১.৬৮৯৩/√(০.৫৭৭৭² + ১.৬৮৯৩²) = ০.৯৪৬২; γ cosine বদলায় না। h₀-এর সঙ্গে ০. (গ) p = ০.৯৩৩৩; ব্যবধান প্রায় [০.৭৮৭, ০.৯৮২]. প্রায় ০.২ চওড়া! (ঘ) ২ × (C(১০,০) + C(১০,১) + C(১০,২))/২১০ = ২ × ৫৬/১০২৪ = ০.১০৯৪.
-
সমালোচনা ও নকশা: একটি রিপোর্টে লেখা: “আমাদের BERT মডেল বাংলা শব্দের অর্থ ৯৭% ঠিক বোঝে। ৪০টি হাতে লেখা বাক্যে ৮০/২০ ভাগ করে ১৩টি layer-এর মধ্যে পরীক্ষার সেটে সেরাটি নেওয়া হয়েছে। সব বাক্য একই ছাঁচের। Tokenizer default.” কী কী সমস্যা? কীভাবে নতুন করে নকশা করবেন?
(১) ৪০-এর ২০% মানে ৮টি পরীক্ষার বাক্য; ৯৭% সেখানে অসম্ভবই (৭/৮ = ৮৭.৫, ৮/৮ = ১০০), তাই সংখ্যাটির উৎস নিজেই প্রশ্নের। (২) Test-এ ১৩ layer-এর সেরা নেওয়া মানে test-কে বাছাইয়ে ব্যবহার, ফল অতি-আশাবাদী। (৩) একই ছাঁচ থাকলে ভাগ করেও leakage-এর মতো সমস্যা হয়। (৪) Baseline, ব্যবধান, control কিছুই নেই। (৫) Tokenizer default-এ বাংলার চিহ্ন টিকে আছে কি না দেখা হয়নি। (৬) “বোঝে” দাবি অতিরঞ্জিত।
নকশা: বেশি ও নানা উৎসের বাক্য; ছাঁচ বা উৎস ধরে ভাগ; দুজন annotator ও kappa; train-এর CV-তে বাছাই, test একবার; majority আর স্থির baseline; shuffled control; n আর ব্যবধান লেখা; একই test-এ McNemar; সিদ্ধান্তে লেখা “এই ডেটায় vector থেকে label পড়া যায়”.
-
আউটপুট অনুমান করুন:
ids = [None, 0, 0, 1, 2, 2, 2, None]; H = np.arange(16).reshape(8, 2); idx = [t for t, w in enumerate(ids) if w == 2]; print(idx, H[idx[0]], H[idx].mean(0), H[0], len(set(ids) - {None}))[4, 5, 6] [8 9] [10. 11.] [0 1] 3. শব্দ ২ আছে token ৪, ৫, ৬-এ। First হলো H[4] = [8, 9]. গড় [(8+10+12)/3, (9+11+13)/3] = [10, 11]. H[0] হলো CLS-এর ঘর। শব্দ আছে ৩টি (০, ১, ২); টোকেন ৮টি। -
কোড লিখুন (ডেস্কটপে): ওপরের
word_vectorব্যবহার করে একটি শব্দের দুই অর্থে ২৪টি করে বাক্য লিখুন। Scale + logistic regression দিয়ে ৫-fold accuracy, আর এলোমেলো label-এর একই হিসাব বের করুন। আগে অনুমান করুন, control কত হবে।X = np.stack([word_vector(ws, i) for ws, i in rows]) clf = make_pipeline(StandardScaler(), LogisticRegression(max_iter=3000)) cv = StratifiedKFold(5, shuffle=True, random_state=0) print(cross_val_score(clf, X, y, cv=cv).mean()) rng = np.random.default_rng(42) print(cross_val_score(clf, X, rng.permutation(y), cv=cv).mean())সত্যিকারের run, “হার”-এর ৪৮টি বাক্যে, Bangla BERT, শেষ layer: accuracy
0.978, এলোমেলো label-এ0.609. Control-এর ০.৬০৯ শুনে ঘাবড়াবেন না: একটিমাত্র shuffle, ৪৮টি বাক্য, আন্দাজ ০.৫-এর আশেপাশে এতটা নড়াচড়া স্বাভাবিক। অনেকগুলো shuffle-এর গড় নিন। এই বাক্যগুলোর ওপর ০.৯৭৮ শুধু এই সংকীর্ণ ডেটার ফল।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি সৎ word-sense probe-এর ধাপগুলো সঠিক ক্রমে সাজান।
- নিজের ভাষার বাক্যে token ছাপিয়ে normalization যাচাই
- Train আর test-এ ভাগ করা
- Model eval অবস্থায় রেখে
word_ids()দিয়ে শব্দের vector তোলা - Train-এর cross-validation-এ layer আর pooling বাছা
- বাছাই করা বিকল্প আর baseline একবার test-এ চালানো
- Control task, ব্যবধান আর সীমা লেখা
একটি শব্দের ৮০% ব্যবহার এক অর্থে, ২০% অন্য অর্থে। ব্যবহারগুলোর গড়ের মতো একটি স্থির vector-এর ক্ষেত্রে কোনটি সবচেয়ে ঠিক?
- Vector নিজে থেকে দুই ভাগ হয়ে দুই অর্থ আলাদা রাখে
- Vector বেশি ব্যবহৃত অর্থের দিকে ঝোঁকে, আর সব বাক্যে একই থাকে
- বাক্যের প্রসঙ্গ দেখে vector বদলায়
- Vector দুই অর্থের সঙ্গেই সবসময় লম্ব হয়
ELMo-র scalar mix-এ a = [0, 0] হলে দুই layer h₀ = [2, 0] আর h₁ = [0, 4] এবং γ = ১-এর জন্য ফল কত?
- [2, 0]
- [2, 4]
- [1, 2]
- [0, 4]
একটি বাক্যে একই শব্দ দুই অর্থে আছে। প্রতিটির অর্থ বলতে একটি probe-এ শেষ layer-এর CLS vector দেওয়া হলো। কী ঘটবে?
- দুটিতে একই label আসবে, কারণ input হুবহু একই
- দুটিতে ঠিক দুই আলাদা label আসবে
- CLS না থাকায় error আসবে
- Probe সবসময় NaN দেবে
word_ids() = [None, 0, 0, 1, None] আর “কাল” শব্দ ০ হলে, শব্দটির mean-pooled vector কীভাবে পাবেন? H-এর shape (5, 768).
- H[0]
- H.mean(0)
- H[0:1].mean(0)
- H[[1, 2]].mean(0)
৭৬৮ মাত্রার vector আর ৪২টি train বাক্যে এলোমেলো label দিয়েও একটি logistic probe-এর train accuracy ১.০০ এলো। এর সবচেয়ে যুক্তিসংগত ব্যাখ্যা কোনটি?
- Vector-এ এলোমেলো label-এর অর্থও রয়েছে
- Probe মুখস্থ করতে পারে, তাই train accuracy একা কিছু প্রমাণ করে না
- Test accuracy-ও ১.০০ হবেই
- Library-তে নিশ্চয় bug আছে
একটি বাংলা BERT tokenizer-এ lowercase default চালু, আর “কুল” থেকে token এলো “কল”। এখানে কী ঘটেছে, আর কী করা উচিত?
- বড় হাতের অক্ষর ছোট হয়েছে; কিছু করার দরকার নেই
- NFC উ-কার মুছেছে; NFC বন্ধ করতে হবে
- Accent মোছার ধাপে উ-কার গেছে; token দেখে সেটিং বদলে যাচাই করা উচিত
- Model-এর weight নষ্ট; নতুন করে train করতে হবে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব শব্দ এখানে।
মনে রাখুন
- সমস্যা: Static embedding-এ বানান-প্রতি এক সারি, দুই ব্যবহারের cosine ১. গড়ে বেশি ব্যবহৃত অর্থ জেতে।
- সমাধান: Vector বাক্যের function. ELMo: BiLSTM আর γ∑sjhj. BERT: CLS, SEP, token + segment + position, self-attention, MLM.
- শব্দের vector: Token ≠ শব্দ।
word_ids()→ first বা mean.hidden_states-এ L + ১টি। CLS শব্দের জন্য নয়। - ফাঁদ: Tokenizer-এর default বাংলার চিহ্ন মুছতে পারে; নিজে token দেখুন। Eval, mask আর সূচক নীরবে ভুল হয়।
- সততা: Cosine বাক্যও মাপে। Probe-এ n, baseline, ব্যবধান, control লিখুন। “তথ্য পড়া যায়” মানে “বোঝে” নয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৫ · NLP মূল্যায়ন — F1, BLEU, ROUGE, chrF আর মানুষের বিচারপরবর্তী পর্বBLEU, ROUGE আর chrF হাতে হিসাব করে মানুষের বিচারের সঙ্গে মেলানো।
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ।
- পর্ব ৩৩ · Self-attention আর multi-head attentionআগের পর্বVector কীভাবে প্রসঙ্গ পায়, তার হিসাব।
- পর্ব ২৯ · LSTM আর GRUআগের পর্বELMo-র ভেতরের যন্ত্র।
- পর্ব ২৩ · Transfer learningআগের পর্বPretrained feature বনাম fine-tuning-এর ধারণা।
- AI-এর ভিত্তি · পর্ব ৬৯ · Embedding আর vector searchআগের ভিত্তিPretrained embedding আর cosine.
- মেশিন লার্নিং · পর্ব ২২ · Model তুলনার পরিসংখ্যানআগের ভিত্তিMcNemar আর paired test.