এই পর্বে যা শিখবেন
- Confusion matrix থেকে macro আর micro F1 হাতে বের করা, আর কোনটি কখন সৎ
- Character n-gram আর Jaccard দিয়ে near-duplicate খোঁজা, আর group split
- একই ভাগে পাঁচটি model-এর সৎ তুলনা: F1, প্রতি শ্রেণির F1, সময়, আর কয়েকটি seed
- সেরা দুটির paired comparison, আর error analysis দিয়ে পরের পদক্ষেপ ঠিক করা
- Threshold খরচ দিয়ে বাছা, আর deep model কখন লাভজনক তার সিদ্ধান্ত
এই মডিউলের আগের তিনটি পর্বে (পর্ব ৩৯ NLP-এর পথ, পর্ব ৪০ Tokenization, পর্ব ৪১ Word embedding) টুকরোগুলো এসেছে। এবার একটি পুরো কাজ শুরু থেকে শেষ পর্যন্ত। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; প্রথমবার প্যাকেজ নামতে কয়েক সেকেন্ড লাগে। PyTorch আর scikit-learn-এর সব আউটপুট ডেস্কটপের সত্যিকারের run থেকে নেওয়া।
১ · কাজ, ডেটা আর পুরো পথ
Sentiment analysis মানে একটি লেখা ধনাত্মক না ঋণাত্মক, তা বলা। পরীক্ষার খাতা দেখার মতো ভাবুন: খাতায় দুই লাইন পড়েই অভিজ্ঞ শিক্ষক আন্দাজ করতে পারেন, কিন্তু ন্যায্য নম্বর দিতে একটি নিয়ম, আলাদা প্রশ্নপত্র আর আলাদা পরীক্ষক লাগে। Model-এর বেলায় নিয়ম হলো metric, আলাদা প্রশ্নপত্র হলো সৎ test ভাগ।
ডেটা: Mendeley Data-তে প্রকাশিত “Bangla (Bengali) sentiment analysis classification benchmark dataset corpus”, version 4 (DOI 10.17632/p6zc7krs37.4), সংকলক Salim Sazzed, লাইসেন্স CC BY 4.0. ডেটার সঙ্গের প্রবন্ধ: Sazzed আর Jayarathna (২০১৯), Sazzed (২০২০). ফাইলে YouTube-এর বাংলা নাটকের ৮,৫০০টি ধনাত্মক আর ৩,৩০৭টি ঋণাত্মক মন্তব্য, হাতে লেবেল করা। লাইসেন্স মেনে জানাই, আমরা যা বদলেছি: NFC, ফাঁকা জায়গা গোছানো, Latin অক্ষর ছোট হাতের করা, হুবহু একই সারি বাদ।
এই ডেটার তিনটি সীমা. এক, এগুলো নাটকের মন্তব্য, পণ্যের রিভিউ নয়; বাজারঘরের রিভিউতে একই ফল আশা করবেন না, নিজের ডেটায় আবার মাপতে হবে। দুই, শ্রেণি দুটি; “মিশ্র” বা “নিরপেক্ষ” বলে কিছু নেই। তিন, ডেটায় অভিনেতার নাম আর প্রচুর গালি আছে। তাই এই পাতায় শুধু এমন সারি দেখানো হয়েছে যাতে কারও নাম বা গালি নেই।
পথটি নিচের ছবিতে। মূল কথা: ভাগ করার আগে ডেটা পরিষ্কার আর দলবদ্ধ করা হয়, আর সব model একই ভাগে চলে।
কাঁচা ডেটা থেকে সিদ্ধান্ত
Same splits for every model; test rows never touch training choices
সত্যিকারের run (Python 3.14.4, লেখার সময়ের version):
~/bazarghar-dl$ uv run python prep.py
lines 11807 | negative 3307 | positive 8500
unique after NFC + whitespace 10711 | same text, both labels 2 (dropped) | kept 10709
exact-duplicate lines removed 1096 (9.3%)
negative share 0.285
near-duplicate pairs (char 3-gram Jaccard >= 0.8) 486 | with opposite labels 3
clusters 10331 | rows in clusters of 2+ 569 | largest 14
mean tokens 13.0 | latin share 0.103
…
পড়ুন: ৯.৩% সারি হুবহু আগের কোনো সারির কপি, যেমন “অসাধারণ” বা “ফালতু নাটক”. দুটি লেখায় একই শব্দ, অথচ দুই লেবেল: label noise-এর ছোট চিহ্ন (মেশিন লার্নিং কোর্সের পর্ব ৫). ১০.৩% সারিতে Latin অক্ষর আছে, অর্থাৎ Banglish বা ইংরেজি মেশানো। Unicode আর NFC-র ভিত্তি AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ আছে।
২ · কী মাপবেন — macro আর micro F1
এই ডেটায় ৭১.৫% সারি ধনাত্মক। সব সারিকে “ধনাত্মক” বললেই accuracy ০.৭১৫, অথচ ঋণাত্মক একটিও ধরা পড়ে না। ক্রিকেটের দলের গড় রানের মতো: এক ব্যাটারের সেঞ্চুরি বাকি দশজনের ব্যর্থতা ঢেকে দেয়। তাই প্রতি শ্রেণির স্কোরকার্ড আলাদা দেখতে হয়। মূল সংজ্ঞা AI-এর ভিত্তি কোর্সের পর্ব ৬০-এ; এখানে শুধু সূত্র: F1 = 2·TP / (2·TP + FP + FN). TP মানে ঠিক ধরা, FP মানে ভুল করে এই শ্রেণি বলা, FN মানে এই শ্রেণি বলতে না পারা।
নিচে একটি কাল্পনিক তিন শ্রেণির উদাহরণ, ১০০টি রিভিউ। একই ম্যাট্রিক্স থেকে দুটি গড় বের করুন।
| আসল ↓ উত্তর → | ভালো | খারাপ | মিশ্র |
|---|---|---|---|
| ভালো | 72 | 4 | 4 |
| খারাপ | 5 | 8 | 2 |
| মিশ্র | 3 | 1 | 1 |
| কোথায় | |
| TP, FP, FN | |
| হিসাব | |
| ফল |
- ম্যাট্রিক্স পড়া. সারি হলো আসল শ্রেণি, কলাম model-এর উত্তর। কর্ণের 72, 8 আর 1 ঠিক উত্তর। “ভালো” ৮০টি, “খারাপ” ১৫টি, “মিশ্র” মাত্র ৫টি।
- ভালো. TP = 72. FP = কলামের বাকি 5 + 3 = 8. FN = সারির বাকি 4 + 4 = 8. F1 = 144 / 160 = ০.৯০০.
- খারাপ. TP = 8, FP = 4 + 1 = 5, FN = 5 + 2 = 7. F1 = 16 / 28 = ০.৫৭১. পনেরোটির মধ্যে আটটিই ধরা পড়েছে।
- মিশ্র. TP = 1, FP = 4 + 2 = 6, FN = 3 + 1 = 4. F1 = 2 / 12 = ০.১৬৭. এই শ্রেণিতে model প্রায় ব্যর্থ।
- Macro. তিনটি F1-এর সরল গড়: ০.৫৪৬. প্রতিটি শ্রেণির ওজন সমান, পাঁচটি রিভিউয়ের শ্রেণিরও। তাই ব্যর্থতা সংখ্যায় দেখা যায়।
- Micro. আগে যোগ: TP 81, FP 19, FN 19. F1 = 162 / 200 = ০.৮১০. ভুল উত্তর মানে একই সঙ্গে একটি শ্রেণির FP আর আরেকটির FN, তাই ∑FP = ∑FN, আর micro-F1 হুবহু accuracy-র সমান।
- দুটি পাশাপাশি. একই model, ০.৮১০ আর ০.৫৪৬. কোনোটি ভুল নয়; দুটি ভিন্ন প্রশ্নের উত্তর। “একটি এলোমেলো রিভিউতে কতটা ঠিক?” micro বলে। “প্রতিটি শ্রেণিতে কেমন?” macro বলে।
এবার একই হিসাব কোডে, সঙ্গে দুটি পরীক্ষা: সবাইকে “ভালো” বলা model, আর এমন model যে ছোট শ্রেণি ঠিক করে বড় শ্রেণি থেকে কিছু হারায়।
import numpy as np
def report(C, names):
tp = np.diag(C); fp = C.sum(0) - tp; fn = C.sum(1) - tp
f1 = 2 * tp / np.maximum(2 * tp + fp + fn, 1)
for n, a, b, c, f in zip(names, tp, fp, fn, f1):
print(f"{n:6} TP={a:3d} FP={b:3d} FN={c:3d} F1={f:.3f}")
micro = 2 * tp.sum() / (2 * tp.sum() + fp.sum() + fn.sum())
weighted = (f1 * C.sum(1)).sum() / C.sum()
print(f"macro={f1.mean():.3f} micro={micro:.3f} weighted={weighted:.3f} accuracy={tp.sum() / C.sum():.3f}")
names = ["ভালো", "খারাপ", "মিশ্র"]
C = np.array([[72, 4, 4], # সারি = আসল, কলাম = model-এর উত্তর
[5, 8, 2],
[3, 1, 1]])
report(C, names)
print("--- সবাইকে 'ভালো' বলা model")
report(np.array([[80, 0, 0], [15, 0, 0], [5, 0, 0]]), names)
print("--- 'মিশ্র' পাঁচটি ঠিক করল, 'ভালো'-র ৪টি হারাল")
report(np.array([[68, 4, 8], [5, 8, 2], [0, 0, 5]]), names)আউটপুট পড়ুন: সবাইকে “ভালো” বলা model-এ accuracy=0.800, প্রথম model-এর প্রায় সমান, কিন্তু macro=0.296. তৃতীয় ক্ষেত্রে micro একচুলও নড়েনি (0.810), অথচ macro 0.546 থেকে 0.660. Weighted F1 (0.825) শ্রেণির আকার দিয়ে ওজন দেয়, তাই সেও ছোট শ্রেণির ব্যর্থতা ঢাকতে পারে: সবাইকে “ভালো” বলা model-ও ০.৭১১ পায়। বহু শ্রেণির পুরো আলোচনা মেশিন লার্নিং কোর্সের পর্ব ২৫-এ।
কোনটি রিপোর্ট করবেন? প্রধান সংখ্যা macro-F1, কারণ এই কাজে ঋণাত্মক মন্তব্য ধরাটাই মূল্যবান, যদিও সেগুলো কম। সঙ্গে প্রতি শ্রেণির F1 আর confusion matrix. শুধু একটি সংখ্যা কখনো নয়। ব্যবসায় প্রতিটি ভুলের দাম জানা থাকলে আসল মাপ হওয়া উচিত খরচ, যা অংশ ৯-এ আসছে।
৩ · ভাগের আগে যমজ খোঁজা — near-duplicate
প্রশ্নপত্র ফাঁস মানে শুধু হুবহু প্রশ্ন নয়; একটি শব্দ বদলানো প্রশ্নও ফাঁসই। “পণ্যটা খুব ভালো” train-এ আর “পণ্যটা খুব ভাল” test-এ থাকলে test আর অচেনা থাকে না। এটি data leakage-এর একটি রূপ (AI-এর ভিত্তি কোর্সের পর্ব ৫৬). খোঁজার সরল উপায়: প্রতিটি লেখাকে তিন অক্ষরের টুকরোর set বানিয়ে Jaccard মাপা।
J(A, B) = |A ∩ B| / |A ∪ B|
A আর B হলো দুটি লেখার character 3-gram-এর set; ∩ মানে দুটিতেই আছে, ∪ মানে অন্তত একটিতে আছে। Broder ১৯৯৭ সালে ওয়েবের প্রায় একই পাতা খুঁজতে এই ধারণা (“resemblance”) ব্যবহার করেন।
| ধাপ | |
| সংখ্যা | |
| Train | |
| Test |
- টুকরো গোনা. Code point ধরে তিন-তিন অক্ষরের জানালা সরান। A-তে ১৩টি আলাদা 3-gram, B-তে ১২টি। Code point মানে যুক্তাক্ষর ভেঙে যায়, তবে মিল মাপতে তাতে সমস্যা নেই।
- Jaccard. ১২টি মেলে, দুটিতে মিলিয়ে মোট ১৩টি। J = ০.৯২৩. শেষের একটি কার ছাড়া দুটি লেখা একই।
- অসতর্ক ভাগ. এলোমেলো ভাগে A পড়ল train-এ, B পড়ল test-এ। Model A শিখলেই B-র উত্তর জানে। Test-এর এই সারি আর “না দেখা” নয়।
- Group split. Jaccard ≥ ০.৮ হলে দুটিকে এক দলে জুড়ুন; দল কখনো ভাঙবে না। এটি AI-এর ভিত্তি কোর্সে দেখা একই গ্রাহককে এক ভাগে রাখার নিয়ম, শুধু দল এখানে লেখার মিল থেকে আসে।
- আসল ডেটায় যমজ কত? কাঁচা এলোমেলো ভাগে test-এর ১৪.০% সারির যমজ train-এ ছিল। হুবহু একই সারি বাদ দিলে ৪.২%. Group split-এ শূন্য।
- Score কত বদলাল? TF-IDF: ০.৯৪৫, ০.৯৩৮, ০.৯৪১ (৫ seed-এর গড়). পার্থক্য seed-এর ছড়ানোর কাছাকাছি। এই ডেটায় ফাঁস ছোট, যা না মাপলে জানা যেত না।
- কেন ছোট? একই test সারি, একই model, শুধু train থেকে যমজ সরিয়ে দেখা হলো। ঐ সারিগুলোতে macro-F1 ০.৯৯৫ থেকে ০.৯৯০. অর্থাৎ এই সারিগুলো মূলত “অসাধারণ নাটক” ধরনের সহজ ছোট লেখা। মুখস্থের চেয়ে সহজ হওয়াই এখানে বড় কারণ।
এবার নিজে চালান। তালিকার লেখাগুলো কাল্পনিক। খেয়াল করুন কোথায় পদ্ধতিটি কাজ করে আর কোথায় করে না।
def grams(s, n=3):
s = " ".join(s.split())
return {s[i:i + n] for i in range(max(1, len(s) - n + 1))}
def jaccard(a, b):
A, B = grams(a), grams(b)
return len(A & B) / len(A | B)
rows = ["পণ্যটা খুব ভালো", "পণ্যটা খুব ভাল", "পণ্যটা খুব ভালো!!", "জিনিসটা দারুণ",
"অসাধারণ শাড়ি", "অসাধারন শাড়ি", "শাড়ির রং উঠে গেছে", "শাড়ির রং উঠে গেছে, খুব খারাপ",
"ডেলিভারি দ্রুত ছিল", "ডেলিভারি দ্রুত ছিল না"]
THR = 0.8
parent = list(range(len(rows)))
def find(i):
while parent[i] != i:
i = parent[i]
return i
pairs = []
for i in range(len(rows)):
for j in range(i + 1, len(rows)):
J = jaccard(rows[i], rows[j])
if J >= 0.4:
pairs.append((J, i, j))
if J >= THR:
parent[find(i)] = find(j)
for J, i, j in sorted(pairs, reverse=True):
print(f"{J:.2f} {'যমজ' if J >= THR else ' '} {rows[i]} | {rows[j]}")
print("জিনিসটা দারুণ বনাম পণ্যটা খুব ভালো:", round(jaccard(rows[0], rows[3]), 2))
groups = {}
for i, r in enumerate(rows):
groups.setdefault(find(i), []).append(r)
print("দল:", len(groups), "| সারি:", len(rows))
for g in groups.values():
if len(g) > 1:
print(" একই ভাগে রাখতে হবে:", g)
n = 10709
print(f"{n} সারিতে জোড়া: {n * (n - 1) // 2:,} (তাই বড় ডেটায় sparse matrix বা MinHash)")আউটপুট পড়ুন: তিনটি জিনিস। এক, 0.84 যমজ “ডেলিভারি দ্রুত ছিল” আর “… ছিল না”: লেখা প্রায় এক, অর্থ উল্টো। আসল ডেটাতেও এমন ৩টি জোড়ার লেবেল উল্টো ছিল। তাই যমজ মুছে ফেলবেন না, এক ভাগে রাখুন। দুই, 0.57 “অসাধারণ” আর “অসাধারন”: ছোট লেখায় একটি বানানের বদলও Jaccard অনেক নামায়, তাই ০.৮-এ ধরা পড়ে না। তিন, 0.04 “জিনিসটা দারুণ”: একই কথা অন্য শব্দে; হরফের মিল এটি কখনো ধরবে না, সেজন্য sentence embedding লাগে (পর্ব ৬৭)। শেষ লাইন: সব জোড়া মেলানো O(n²); বড় ডেটায় sparse matrix বা MinHash.
Threshold বাছাও সিদ্ধান্ত। সত্যিকারের run, একই ১০,৭০৯ সারি:
~/bazarghar-dl$ uv run python ablation.py # দ্বিতীয় অংশ
thr 0.9: pairs 157 | rows in groups of 2+ 251 | largest group 5
thr 0.8: pairs 486 | rows in groups of 2+ 569 | largest group 14
thr 0.7: pairs 1308 | rows in groups of 2+ 1019 | largest group 106
thr 0.6: pairs 3665 | rows in groups of 2+ 1573 | largest group 817
thr 0.5: pairs 11177 | rows in groups of 2+ 2422 | largest group 1694
০.৬-এ একটি দলে ৮১৭টি সারি: A মেলে B-র সঙ্গে, B মেলে C-র সঙ্গে, এভাবে শিকলের মতো জুড়ে যায়, যদিও A আর শেষের সারির মিল সামান্য। আমরা ০.৮ নিয়েছি, কারণ দলগুলো চোখে দেখে যমজই মনে হয়; এটি কোনো প্রমাণিত সীমা নয়।
৪ · আগে baseline — TF-IDF + logistic regression
চায়ের দোকানের হিসাব খাতা দিয়ে যদি হিসাব মেলে, সফটওয়্যার কেনার আগে সেটিই দেখা উচিত। Baseline সেই খাতা। TF-IDF আর logistic regression আগের দুই কোর্সে পড়া (AI-এর ভিত্তি কোর্সের পর্ব ৬৭, মেশিন লার্নিং কোর্সের পর্ব ২৩), তাই পুনরায় শেখাচ্ছি না; শুধু খালি হাতে, NumPy-তে একবার লিখে দেখি। ডেটা ৩২টি কাল্পনিক বাজারঘর রিভিউ, পাতার ভেতরেই রাখা।
import numpy as np, re
rows = [l.split("\t") for l in open("reviews.tsv", encoding="utf-8").read().strip().split("\n")]
def toks(t):
w = re.findall(r"[\u0980-\u09FF]+", t)
return w + [a + "_" + b for a, b in zip(w, w[1:])] # শব্দ + bigram
tr = [(toks(t), int(y)) for s, y, t in rows if s == "train"]
te = [(toks(t), int(y), t) for s, y, t in rows if s == "test"]
vocab = {w: i for i, w in enumerate(sorted({w for d, _ in tr for w in d}))}
df = np.zeros(len(vocab))
for d, _ in tr:
df[[vocab[w] for w in set(d)]] += 1
idf = np.log((1 + len(tr)) / (1 + df)) + 1 # idf শুধু train থেকে
def tfidf(d):
x = np.zeros(len(vocab))
for w in d:
if w in vocab:
x[vocab[w]] += 1
x *= idf
return x / max(np.linalg.norm(x), 1e-9)
X = np.array([tfidf(d) for d, _ in tr]); y = np.array([v for _, v in tr])
w, b = np.zeros(X.shape[1]), 0.0
for _ in range(500): # L2-সহ gradient descent
p = 1 / (1 + np.exp(-(X @ w + b)))
w -= 1.0 * (X.T @ (p - y) / len(y) + 0.01 * w)
b -= 1.0 * (p - y).mean()
names = sorted(vocab, key=vocab.get); o = np.argsort(w)
print("vocab", len(vocab), "| train", len(y), "| test", len(te))
print("সবচেয়ে ঋণাত্মক:", [names[i] for i in o[:4]])
print("সবচেয়ে ধনাত্মক:", [names[i] for i in o[-4:]])
P = np.array([1 / (1 + np.exp(-(tfidf(d) @ w + b))) for d, _, _ in te])
Y = np.array([v for _, v, _ in te]); H = (P >= 0.5).astype(int)
for (d, v, t), q, h in zip(te, P, H):
print(f"{'ঠিক' if h == v else 'ভুল'} P(ভালো)={q:.2f} লেবেল={v} {t}")
f = [2*((H==c)&(Y==c)).sum() / max((H==c).sum()+(Y==c).sum(), 1) for c in (0, 1)]
print(f"F1 খারাপ={f[0]:.2f} ভালো={f[1]:.2f} macro={np.mean(f):.2f} (কাল্পনিক ৮টি test)")আউটপুট পড়ুন: সবচেয়ে ঋণাত্মক weight না, টাকা, গেছে, নেই; এগুলো মানুষের চোখেও যুক্তিসঙ্গত। “হাঁড়ির মান খারাপ না” ঠিক হয়েছে, কিন্তু কোনোমতে: P(ভালো)=0.52. কারণ train-এ খারাপ_না bigram একবার দেখা গেছে। ব্যঙ্গের “বাহ, দশ দিনে ডেলিভারি, দারুণ সেবা” ভুল। আটটি কাল্পনিক লেখায় macro=0.87 কোনো সাধারণ ফল নয়, এই কোড বোঝার একটি উদাহরণ মাত্র।
আসল ডেটায় scikit-learn দিয়ে: শব্দ আর bigram-এর TF-IDF, সঙ্গে char_wb অক্ষর 2–4 gram; class weight balanced; C বাছা validation-এ। কোন অংশ কত কাজ করে, repeat ০-এর ৫ fold-এ:
~/bazarghar-dl$ uv run python ablation.py # প্রথম অংশ; scikit-learn 1.9.1
word 1-gram macro-F1 0.924
word 1-2 macro-F1 0.929
char_wb 2-4 macro-F1 0.933
word 1-2 + char macro-F1 0.939
অক্ষর n-gram একাই শব্দের চেয়ে ভালো। বাংলার জন্য এটি আশ্চর্য নয়: “অসাধারণ”, “অসাধারন”, “অসাধারান” তিনটি আলাদা শব্দ, কিন্তু অনেক অক্ষর-টুকরো একই; “নাটকটি”, “নাটকটা”, “নাটকের”-ও তাই। পর্ব ৪১-এর fastText-এর পেছনেও এই যুক্তি।
৫ · চারটি neural model — কে কী দেখে
একই লেখা চারজন চারভাবে পড়ে। সবার শুরু একই: শব্দ tokenizer, শুধু train থেকে vocabulary (অন্তত দুবার দেখা শব্দ, প্রতি fold-এ প্রায় ৪,৯০০), ৮০ শব্দে কাটা, embedding ৬৪ মাত্রা। কোনো pretrained embedding নেই, সব শূন্য থেকে। Padding আর mask-এর পুরো নিয়ম পর্ব ১১ আর পর্ব ২৭-এ।
| Model | কীভাবে পড়ে | কী ধরে, কী হারায় | আগে পড়েছেন |
|---|---|---|---|
| Bag of embeddings | সব শব্দের গড়, তারপর MLP | শব্দের ক্রম হারায় | পর্ব ২৭ |
| 1D CNN (k = 2, 3, 4) | ছোট জানালা, সময় ধরে max | স্থানীয় নকশা ধরে; দূরের সম্পর্ক কম | পর্ব ৩১ |
| BiLSTM (৬৪ × ২) | দুই দিক থেকে পড়া, শেষ state জোড়া | ক্রম ধরে; ধীর | পর্ব ২৯ |
| Transformer (২ স্তর, ৪ head) | সব শব্দ সবার দিকে, তারপর mean | দূরের সম্পর্ক ধরে; ডেটা বেশি চায় | পর্ব ৩৫ |
কোডের যে অংশে ভুল সবচেয়ে সহজ, সেটুকু (পুরো কোড থেকে সংক্ষেপে):
class BiLSTM(nn.Module): # __init__ বাদ
def forward(s, x): # x (N, T); 0 = PAD
lens = (x != 0).sum(1).cpu()
pk = nn.utils.rnn.pack_padded_sequence(
s.drop(s.emb(x)), lens, batch_first=True, enforce_sorted=False)
_, (h, _) = s.lstm(pk) # h (2, N, H): দুই দিকের শেষ state
return s.fc(s.drop(torch.cat([h[0], h[1]], 1))) # (N, 2H) → (N, 2)
class TinyTransformer(nn.Module): # __init__ বাদ
def forward(s, x):
pad = x == 0
pos = torch.arange(x.size(1)).unsqueeze(0)
h = s.enc(s.drop(s.emb(x) + s.pos(pos)), src_key_padding_mask=pad)
m = (~pad).unsqueeze(-1)
return s.fc((h * m).sum(1) / m.sum(1)) # PAD বাদ দিয়ে গড়লক্ষ্য করুন: BiLSTM-এ PackedSequence না দিলে শেষ state হতো padding-এর; Transformer-এ গড় নিতে হয় শুধু আসল শব্দে (masked pooling). Training সবার এক: AdamW, weight decay ০.০১, batch ৬৪, gradient clipping ১.০, class-weight দেওয়া cross-entropy (পর্ব ১৩), validation macro-F1 তিন epoch না বাড়লে early stopping. Learning rate bag-এ ০.০০২, বাকিদের ০.০০১. কোনো model-এ বড় hyperparameter খোঁজা হয়নি; এটি এই তুলনার একটি সীমা।
৬ · মুখোমুখি — ফল, প্রতি শ্রেণি আর সময়
ভাগ: StratifiedGroupKFold, ৫ fold, ৩ বার, মোট ১৫টি ভাগ; প্রতি ভাগে train ৭,৫১০, validation ১,০৫৬, test ২,১৪৩-এর কাছাকাছি। প্রতি ভাগে পাঁচটি model-ই একই সারি পায়, আর প্রতি ভাগের seed আলাদা। সত্যিকারের run, CPU-তে, ৪ thread; PyTorch 2.15.0, NumPy 2.5.4. একই মেশিনে অন্য কাজও চলছিল, তাই সময় মোটামুটি।
~/bazarghar-dl$ uv run python compare.py main 0 # 1, 2 একইভাবে
~/bazarghar-dl$ uv run python analyze.py
rows 75 | splits 15 | n_test 2143
model macro sd F1neg F1pos acc secs ms/1k params ep
tfidf 0.941 0.007 0.916 0.966 0.951 5.5 120.1 41529 0
bag 0.917 0.007 0.883 0.951 0.931 4.9 4.0 318210 11
cnn 0.913 0.009 0.878 0.949 0.928 36.6 58.8 351362 13
bilstm 0.910 0.010 0.873 0.947 0.926 79.6 62.9 380738 11
transformer 0.904 0.011 0.865 0.944 0.920 170.3 194.2 386114 13
majority (all positive): acc 0.715 | F1pos 0.834 | F1neg 0 | macro 0.417
…
কলামগুলো: macro আর sd ১৫ ভাগের গড় আর ছড়ানো; F1neg, F1pos প্রতি শ্রেণি; secs এক fit-এর মধ্যমা, TF-IDF-এ চারটি C মিলিয়ে; ms/1k হাজার সারিতে অনুমানের সময়; ep সেরা epoch.
এই ডেটায়, এই run-এ, baseline সবার ওপরে. সবচেয়ে সরল neural model (শব্দের গড়) দ্বিতীয়, আর সবচেয়ে জটিল model সবার নিচে, সময়েও সবচেয়ে ধীর (baseline-এর প্রায় ৩০ গুণ)। এটি “deep learning টেক্সটে কাজ করে না” প্রমাণ করে না। এটি বলে: সাড়ে সাত হাজার ছোট মন্তব্যে, pretrained কিছু ছাড়া, শব্দ-token দিয়ে শূন্য থেকে শিখলে বেশি ক্ষমতার ফল পাওয়া যায়নি। Sequence model-এর সুবিধা (ক্রম) এখানে কম কাজে লেগেছে, কারণ এই ডেটায় মত প্রায়ই কয়েকটি শব্দেই স্পষ্ট, আর bigram ও অক্ষর n-gram সেটি ধরে ফেলে।
Imbalance. ঋণাত্মক ২৮.৫%, তেমন বেশি নয়। Class weight বন্ধ করে একই ৫ fold-এ মাপলে TF-IDF-এর macro ০.৯৩৯ থেকে ০.৯৩৬, ঋণাত্মক F1 ০.৯১৩ থেকে ০.৯০৮; CNN-এর macro ০.৯১০ থেকে ০.৯০৮. পার্থক্য ছোট, তবে একই দিকে। আরও বেশি অসম ডেটায় এর ওজন বাড়ে; তখন focal loss (পর্ব ১৩) আর threshold (মেশিন লার্নিং কোর্সের পর্ব ৫০) দেখবেন।
একই ভাগে শুধু seed বদলালে. repeat ০-এর fold ০ স্থির রেখে ৫টি seed (TF-IDF এখানে নির্ধারিত, seed-এ বদলায় না):
model count mean std min max
bag 5 0.9185 0.0044 0.9130 0.9249
bilstm 5 0.9138 0.0088 0.9012 0.9257
cnn 5 0.9073 0.0164 0.8901 0.9285
transformer 5 0.9043 0.0141 0.8826 0.9199
একটি seed-এ CNN পেয়েছে ০.৯২৮, আরেকটিতে ০.৮৯০. শুধু একটি seed চালিয়ে “CNN, bag-কে হারিয়েছে” বা “CNN বাজে” দুটোই বলা যেত।
৭ · সেরা দুটির paired তুলনা
“০.৯৪১ বনাম ০.৯১৭” দেখে কি সিদ্ধান্ত নেওয়া যায়? প্রশ্নটি মেশিন লার্নিং কোর্সের পর্ব ২২-এ ধাপে ধাপে আছে; এখানে শুধু প্রয়োগ। একই ভাগে দুই model, তাই প্রতি ভাগের পার্থক্য d নিন। সমস্যা: fold-গুলো একই ডেটা ভাগ করে, তাই স্বাধীন নয়। Corrected resampled t-test-এ variance-এর গুণক বাড়ে:
t = d̄ / √( s² · (1/J + ntest/ntrain) )
d̄ হলো J-টি পার্থক্যের গড়, s² তাদের variance. এখানে J = ১৫, ntest/ntrain = ২১৪৩/৭৫১০ ≈ ০.২৮৫. সত্যিকারের run:
~/bazarghar-dl$ uv run python analyze.py # মাঝের অংশ
tfidf - bag: mean diff 0.0234 | sd 0.0048 | tfidf ahead in 15/15 splits
naive t 18.88 p=2.3e-11 | corrected t 8.22 p=1e-06 | corrected 95% CI [0.0173, 0.0295]
bag - cnn: mean diff 0.0038 | sd 0.0085 | bag ahead in 12/15 splits
naive t 1.74 p=0.1 | corrected t 0.76 p=0.46 | corrected 95% CI [-0.0070, 0.0147]
bag - bilstm: mean diff 0.0068 | sd 0.0077 | bag ahead in 13/15 splits
naive t 3.45 p=0.0039 | corrected t 1.50 p=0.16 | corrected 95% CI [-0.0029, 0.0166]
McNemar tfidf vs bag, repeat 0, all 10709 rows: only tfidf wrong 137 | only bag wrong 359 | both 400 | chi2 98.5 p=3.3e-23
confusion (rows = actual neg,pos) {'tfidf': [[2818, 235], [302, 7354]], 'bag': [[2758, 295], [464, 7192]]}
- TF-IDF বনাম bag: ১৫-এর ১৫টি ভাগেই TF-IDF এগিয়ে। পার্থক্য ০.০২৩, ব্যবধান [০.০১৭, ০.০৩০]; শূন্য অনেক দূরে। সংশোধনের পরেও স্পষ্ট।
- Bag বনাম BiLSTM: এটিই শিক্ষা। Naive t বলে p = ০.০০৪, “significant”. সংশোধনের পরে p = ০.১৬, ব্যবধানে শূন্য আছে। চারটি neural model-এর ক্রমকে তাই শক্ত প্রমাণ ভাববেন না। আর অনেক জোড়া মেলালে একাধিক তুলনার সংশোধনও লাগে (একই পর্ব).
- McNemar: একবারের ৫-fold-এ প্রতিটি সারি একবার test হয়। শুধু TF-IDF ভুল ১৩৭, শুধু bag ভুল ৩৫৯, দুজনই ভুল ৪০০. এই অসমতা বড়। তবে McNemar শেখা model-এর ভিন্নতা আর ভাগের অনিশ্চয়তা পুরো ধরে না, তাই t-এর পাশে সহায়ক হিসেবে পড়ুন। ৪০০টিতে দুজনই ভুল: এগুলোই পরের অংশের বিষয়।
- কাজের দিক থেকে: ০.০২৩ মানে কী? এই confusion matrix-এ ১০,৭০৯ সারিতে TF-IDF-এর ভুল ৫৩৭, bag-এর ৭৫৯. প্রতি হাজারে প্রায় ২১টি ভুল কম। এর সঙ্গে সময় আর সরলতাও TF-IDF-এর পক্ষে, তাই এখানে সিদ্ধান্ত সহজ।
৮ · ভুল পড়া — error analysis
ডাক্তার শুধু জ্বরের মাপ দেখে ওষুধ দেন না, রোগীকে দেখেন। F1 হলো জ্বরের মাপ; ভুলগুলো পড়া হলো রোগী দেখা। আগে একটি সহজ কাটাকাটি, repeat ০-এর পুরো ডেটায়। সত্যিকারের run:
subset n neg% tfidf bag cnn bilstm transformer
all 10709 0.29 0.939 0.914 0.910 0.913 0.903
negation word 2334 0.43 0.915 0.882 0.887 0.882 0.869
has latin 1074 0.17 0.917 0.885 0.878 0.866 0.869
<=3 words 1442 0.30 0.952 0.912 0.896 0.907 0.903
>=26 words 1177 0.24 0.914 0.890 0.892 0.866 0.872
(“negation word” মানে আলাদা না, নাই, নেই, নয় ইত্যাদি বা শব্দের শেষে নি; একটি মোটা regex. কলামের ফাঁক এখানে গুছিয়ে দেখানো।) না-যুক্ত সারি, Latin-যুক্ত সারি আর লম্বা সারি সবার জন্য কঠিন। না-যুক্ত সারিতে “sequence model negation-এ জিতবে” এমন আশা এই run-এ মেলেনি: TF-IDF তবুও ০.৯১৫. তবে এই উপদলে শ্রেণির অনুপাতও আলাদা (ঋণাত্মক ৪৩%), তাই সরাসরি তুলনায় সাবধান।
এবার TF-IDF-এর ৫৩৭টি ভুল থেকে seed দিয়ে এলোমেলো ৬০টি বেছে হাতে পড়ে ভাগ করা হলো। একজনের বিচার, দ্বিতীয় পাঠক নেই, kappa নেই, প্রতিটিতে একটি মূল ধরন। ৬০টি ছোট নমুনা: “২০%”-এর ৯৫% ব্যবধান প্রায় ১২% থেকে ৩২%.
| মূল ধরন | কয়টি | কী করা যায় |
|---|---|---|
| পরোক্ষ মত বা বিরল শব্দ | ১৩ | আরও ডেটা, pretrained embedding |
| মিশ্র মত (“ভালো, কিন্তু …”) | ১২ | লেবেল-নীতি; তৃতীয় “মিশ্র” শ্রেণি |
| নাটকের বাইরের কথা | ১২ | “প্রাসঙ্গিক নয়” শ্রেণি, ডেটা পরিষ্কার |
| ব্যঙ্গ, রূপক, বাগধারা | ৭ | কঠিন; প্রসঙ্গ-সচেতন model দেখা |
| Negation | ৬ | “পারলাম না” ধরনের উদাহরণ যোগ |
| বানান, আঞ্চলিক ভাষা | ৫ | অক্ষর n-gram, subword (পর্ব ৪০) |
| লেবেল সন্দেহজনক | ৫ | দ্বিতীয় পাঠক, লেবেল পরিষ্কার |
কয়েকটি আসল সারি, হুবহু, উৎস Sazzed-এর corpus (CC BY 4.0); কারও নাম বা গালি নেই এমন সারিই বাছা। P = TF-IDF-এর দেওয়া ধনাত্মক সম্ভাবনা।
| লেখা | লেবেল | P | ধরন |
|---|---|---|---|
| ধেত,শেষটা মানতে পারলাম না... | ঋণাত্মক | 0.82 | Negation |
| সবকিছু অতিরিক্ত ভালো না | ধনাত্মক | 0.08 | Negation, লেবেলও ভাবার মতো |
| সময় টাই মাটি করলাম | ঋণাত্মক | 0.90 | বাগধারা |
| জাতি ধ্বংসের জন্য এমন নাটক খুব প্রয়োজন. | ঋণাত্মক | 0.80 | ব্যঙ্গ |
| লো বাজেট নাটক বাট ভালো হইছে | ধনাত্মক | 0.37 | মিশ্র, ইংরেজি শব্দ বাংলা হরফে |
| বেতালা ভালা লাগের | ধনাত্মক | 0.33 | আঞ্চলিক |
| ভাললাগচ্ছে | ধনাত্মক | 0.35 | বানান, জোড়া শব্দ |
| আধিক্যেতায় ভরা একটি নাটক। | ঋণাত্মক | 0.92 | বিরল শব্দ |
| ঠান্ডা নরম এবং গরম | ধনাত্মক | 0.15 | বাইরের কথা |
| মজা পেলাম | ঋণাত্মক | 0.90 | লেবেল সন্দেহজনক |
| হাসতে হাসতে আমি শেষ | ঋণাত্মক | 0.79 | লেবেল সন্দেহজনক |
পড়ুন: “মজা পেলাম”-এ লেবেল ঋণাত্মক হওয়া ব্যঙ্গ হতে পারে, ভিডিওটি না দেখে বলা যায় না, লেবেল ভুলও হতে পারে। মানে, model-এর কিছু “ভুল” হয়তো ভুল নয়, আর test-এর একটি অংশে কোনো model-ই পুরো নম্বর পাবে না। ৬০-এর মধ্যে ২৪টি (“মিশ্র” + “বাইরের কথা”) দুই-শ্রেণির লেবেল-কাঠামোর সমস্যার দিকে ইঙ্গিত করে, model-এর দিকে নয়। পাঁচটি model-ই একসঙ্গে ভুল করেছে ২৪৮টি সারিতে; সেখানে আরেকটি model বদলে হয়তো কম লাভ হবে। এই ধরনের কাজের নিয়ম মেশিন লার্নিং কোর্সের পর্ব ৫-এ।
৯ · খরচ, threshold আর deep model কখন
ধরুন বাজারঘরে model খারাপ রিভিউ চিহ্নিত করে, আর একজন কর্মী সেগুলো পড়েন। খারাপ রিভিউ চোখ এড়ালে ক্ষতি ধরা যাক ৫ একক; ভালো রিভিউ অকারণে পড়লে ১ একক (খরচের অনুপাত ৫ : ১)। সব সংখ্যা কাল্পনিক। সম্ভাবনা যদি ঠিকঠাক হয়, হিসাব সরল: পড়া তখনই লাভজনক যখন p·৫ ≥ (১ − p)·১, অর্থাৎ t* = Cপড়া / (Cপড়া + Cএড়ানো) = ১/৬ ≈ ০.১৬৭. এটিই cost matrix থেকে threshold পাওয়া (পূর্ণ আলোচনা মেশিন লার্নিং কোর্সের পর্ব ৫০).
import numpy as np
rng = np.random.default_rng(42)
n = 2000
p = rng.beta(0.7, 1.6, n) # P(খারাপ রিভিউ): কাল্পনিক, calibrated
bad = rng.random(n) < p
C_MISS, C_READ = 5.0, 1.0 # খরচ একক: চোখ এড়ালে ৫, মানুষ পড়লে ১
def cost(score, t):
flag = score >= t
miss = (bad & ~flag).sum()
read = (~bad & flag).sum() # শুধু ভালো রিভিউ অকারণে পড়া
return C_MISS * miss + C_READ * read, flag.sum(), miss
print(f"খারাপ রিভিউ {bad.mean():.2f} | হিসাবের t* = {C_READ / (C_READ + C_MISS):.3f}")
print(" t মোট খরচ মানুষ পড়ল চোখ এড়াল")
for t in (0.1, 0.167, 0.3, 0.5, 0.7, 0.9):
c, f, m = cost(p, t)
print(f"{t:5.3f} {c:7.0f} {f:9d} {m:9d}")
grid = np.linspace(0.01, 0.99, 99)
best = grid[np.argmin([cost(p, t)[0] for t in grid])]
print(f"grid-এ সেরা t = {best:.2f}")
q = p**3 / (p**3 + (1 - p)**3) # একই ক্রম, কিন্তু অতি-আত্মবিশ্বাসী স্কোর
best2 = grid[np.argmin([cost(q, t)[0] for t in grid])]
print(f"অতি-আত্মবিশ্বাসী স্কোরে: t=0.167 খরচ {cost(q, 0.167)[0]:.0f}"
f" | সেরা t={best2:.2f} খরচ {cost(q, best2)[0]:.0f}")আউটপুট পড়ুন: ডিফল্ট t = ০.৫-এ খরচ 1569, হিসাবের t = ০.১৬৭-এ 974; একই model, শুধু সিদ্ধান্তের নিয়ম বদলে প্রায় ৩৮% কম। Grid-এর সেরা 0.21 হিসাবের কাছেই, পার্থক্য নমুনার ছড়ানো। শেষ লাইনে একই ক্রমের অতি-আত্মবিশ্বাসী স্কোর: t = ০.১৬৭ আর খাটে না (1249), সেরা t হয় 0.02 (971). Neural model প্রায়ই অতি-আত্মবিশ্বাসী হয়। তাই হয় calibration (মেশিন লার্নিং কোর্সের পর্ব ২৬), নয় validation ভাগে সরাসরি খরচ মেপে threshold বাছুন। Test ভাগে কখনো নয়।
এবার মূল সিদ্ধান্ত। নিচের ধাপগুলো এই পর্বের মাপা সংখ্যা ধরে; শেষের ক্ষেত্রগুলো কাল্পনিক।
| প্রশ্ন | |
| মাপা সংখ্যা | |
| মানে |
- Baseline. আগে দুটি নিচের সীমা: সব-ধনাত্মক (০.৪১৭) আর TF-IDF + LR (০.৯৪১). এদের ছাড়া “০.৯০৪ ভালো” কথাটির মানে নেই।
- লাভ. সবচেয়ে ভালো neural model bag, ০.৯১৭. অর্থাৎ deep দিকে গিয়ে লাভ নয়, ০.০২৩ ক্ষতি।
- আসল কি না. Paired, সংশোধিত ব্যবধান শূন্য থেকে দূরে। আর bag-এর seed-এর ছড়ানো ০.০০৪, পার্থক্য তার পাঁচ গুণের বেশি।
- শেখানোর দাম. এই run-এ TF-IDF ৫.৫ সেকেন্ডে চারটি C খোঁজে। Transformer নেয় ১৭০ সেকেন্ড, CPU-তে। সেকেন্ডের পার্থক্য নিজে তুচ্ছ, কিন্তু বারবার পরীক্ষায় এটি জমে।
- চালানোর দাম. Bag অনুমানে সবচেয়ে দ্রুত (হাজারে ৪ ms), TF-IDF-এর অক্ষর n-gram অনেক ধীর (১২০ ms). গতি মূল চাহিদা হলে bag একটি যুক্তিসঙ্গত বিকল্প। সরল মানেই সব দিকে দ্রুত নয়।
- ভুলের ধরন. ৬০-এর ২৪টি লেবেল-কাঠামোর দিকে ইঙ্গিত করে। Model বদলের আগে “মিশ্র” আর “বাইরের কথা” শ্রেণি যোগ করা হয়তো বেশি কাজের।
- রায়, এই ডেটায়. TF-IDF + logistic regression চালান: সবচেয়ে ভালো, ব্যাখ্যা করা সহজ, ঠিক করা সহজ। মনে রাখুন, এটি নাটকের মন্তব্যে একটি মাপ, সাধারণ নিয়ম নয়।
- অন্য ক্ষেত্রে. কাল্পনিক দুটি ক্ষেত্র ভাবুন। বাজারঘরে বছরে লাখ লাখ রিভিউ আর শ্রেণি ২০টি: ডেটা বাড়লে neural-এর সুযোগ বাড়তে পারে। ৮০০টি লেবেলের ছোট ডেটা: শূন্য থেকে শেখা নয়, TF-IDF, পরে pretrained encoder. দুটিতেই একই ভাগে baseline রেখে মাপুন। Pretrained encoder-এর পরীক্ষা এই পর্বে নেই; সেটি পর্ব ৪৮ আর পর্ব ৫৪-এর বিষয়।
আরও গভীরে: Wang আর Manning (২০১২, “Baselines and Bigrams”) দেখান, sentiment-এ bigram-সহ সরল linear model অনেক ক্ষেত্রে তখনকার জটিল পদ্ধতির সমান বা ভালো। Joulin ও সহকর্মীরা (২০১৭, fastText-এর “Bag of Tricks”) গড় করা embedding আর n-gram দিয়ে কয়েকটি benchmark-এ তখনকার deep model-এর কাছাকাছি ফল পান, অনেক কম সময়ে। Near-duplicate-এর প্রভাব নিয়ে Lewis ও সহকর্মীরা (২০২১) প্রশ্নোত্তরের benchmark-এ train আর test-এর মিল মাপেন। সবার ফল নিজের ডেটায় আবার মাপার বিষয়।
পরীক্ষা ও ইন্টারভিউতে:
- একক-লেবেল multiclass-এ micro-F1 কীসের সমান? Accuracy-র; কারণ প্রতিটি ভুল একটি FP আর একটি FN, তাই ∑FP = ∑FN.
- F1 ০.৯, ০.৬, ০.০ হলে macro? ০.৫. Micro জানতে confusion matrix লাগবে।
- Near-duplicate ধরা পড়লে কী করবেন? এক দলে রেখে group split; অর্থ উল্টো হতে পারে বলে সবসময় মুছে ফেলা ঠিক নয়।
- ৫-fold CV-র পার্থক্যে সাধারণ paired t কেন আশাবাদী? Fold-গুলো train ভাগ করে, তাই স্বাধীন নয়; Nadeau–Bengio গুণক 1/J + ntest/ntrain.
- CFN = 9, CFP = 1 হলে calibrated সম্ভাবনায় threshold? ১/(১ + ৯) = ০.১.
১০ · AI-এর যুগে — classifier-এর দাবি যাচাই
AI assistant কয়েক মিনিটে পুরো classifier লিখে দেয়, আর একটি সুন্দর সংখ্যাও দেখায়। সমস্যা ঐ সংখ্যাটিতে।
| AI প্রায়ই যা করে | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| Baseline ছাড়াই BiLSTM বা Transformer | এই run-এ baseline-ই সেরা ছিল | একই ভাগে TF-IDF + LR |
| শুধু accuracy | সব-ধনাত্মকেই ০.৭১৫ | Macro-F1, প্রতি শ্রেণি, ম্যাট্রিক্স |
ভাগের আগে পুরো ডেটায় fit বা vocabulary | Test-এর তথ্য train-এ ঢোকে | Pipeline; vocabulary-র উৎস দেখা |
কোনো যমজ পরীক্ষা ছাড়া train_test_split | হুবহু কপি দুই পাশে | Set মিলিয়ে দেখা; Jaccard দল |
| Pad করা শেষ state বা mask ছাড়া গড় | ক্র্যাশ করে না, চুপচাপ খারাপ করে | একই লেখা ভিন্ন padding-এ চালানো |
model.eval() আর no_grad বাদ | Dropout অনুমানেও চলে | দুবার চালিয়ে একই ফল কি না |
| Test দেখে threshold বা C বাছা | Test আর অচেনা থাকে না | Validation-এ বাছা, test একবার |
| একটি seed; “৯৬%, state of the art” ধরনের দাবি | আমাদের CNN-এর seed ভেদে ০.৮৯০ থেকে ০.৯২৮ | কয়েক seed, সংশোধিত t; সূত্র খুঁজে পড়া |
বাংলা বা অন্য কোনো ভাষার classifier চাইলে পরীক্ষাগুলোও চাইতে বলুন। ব্যক্তিগত তথ্য বা গ্রাহকের আসল রিভিউ prompt-এ দেবেন না; কয়েকটি বানানো সারি যথেষ্ট।
AI-এর লেখা classifier হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১TF-IDF-এর মতো পুরোনো পদ্ধতি একটি Transformer-কে হারাল। এতে কি প্রমাণ হয় যে টেক্সটে deep learning অকেজো?
না। আমাদের পরীক্ষায় Transformer-কে কঠিন শর্তে নামানো হয়েছে: সাড়ে সাত হাজার ছোট মন্তব্য, শব্দ-token, শূন্য থেকে শেখা embedding, ২ স্তর। ইতিহাসের বড় বদলটি ঠিক এই শর্তগুলো ভাঙে। ২০১৮ সালে Howard আর Ruder-এর ULMFiT, আর একই বছরে Devlin ও সহকর্মীদের BERT দেখায়, বিশাল unlabelled ভাষা থেকে আগে শিখে নিয়ে অল্প লেবেলের ডেটায় fine-tune করলে অনেক benchmark-এ শূন্য থেকে শেখা model-কে পেছনে ফেলা যায়। বাংলায় Bhattacharjee ও সহকর্মীরা (২০২২) BanglaBERT প্রকাশ করেন, এবং তাঁদের প্রতিবেদনে SentNoB নামের বাংলা sentiment ডেটায় তাঁদের model আগের কয়েকটি পদ্ধতির চেয়ে ভালো করে। এগুলো অন্য ডেটা আর অন্য শর্তের ফল, আমাদের মাপ নয়।
আবার উল্টো ভুলও আছে। Wang আর Manning ২০১২ সালে দেখিয়েছিলেন, সরল bigram-ভিত্তিক model প্রায়ই খুব শক্ত প্রতিপক্ষ। Joulin ও সহকর্মীরা ২০১৭ সালে একই বার্তা আবার দেন। কাজ যদি মূলত “কয়েকটি শব্দ দেখে ঠিক করা” হয়, তবে বাড়তি ক্ষমতা কাজে লাগানোর মতো সংকেত কমই থাকে, আর তা শেখার মতো ডেটাও থাকে না।
তাই প্রশ্নটি “deep না shallow” নয়। প্রশ্ন হলো: এই ডেটায়, এই পরিমাণ লেবেলে, এই খরচে কোনটি ভালো, আর আমি কীভাবে জানব? আমরা pretrained encoder এই তুলনায় আনিনি, তাই এই পর্ব তার পক্ষে বা বিপক্ষে কিছু বলে না। পর্ব ৪৮ আর পর্ব ৫৪-এর পরে একই ভাগে সেটি যোগ করাই পরের সৎ পরীক্ষা।
মূল উপলব্ধি: একটি তুলনার ফল তার শর্তের বাইরে যায় না। শর্ত লিখে রাখুন, আর যে দাবি মাপেননি সেটি করবেন না।
প্র ০২ডেটার লাইসেন্স CC BY 4.0, তাহলে কি এর যেকোনো সারি পাতায় দেখানো আর যেকোনো কাজে লাগানো সম্পূর্ণ ঠিক?
লাইসেন্স একটি প্রশ্নের উত্তর দেয়: এই সংকলন ব্যবহার, বদল ও ভাগ করার অনুমতি কী শর্তে। CC BY-এর শর্ত উৎসের নাম জানানো আর বদলের কথা বলা। কিন্তু লাইসেন্স অন্য প্রশ্নের উত্তর দেয় না। যাঁরা YouTube-এ মন্তব্য লিখেছেন, তাঁরা গবেষণার ডেটায় থাকার কথা ভেবে লেখেননি। মন্তব্যে নাম থাকতে পারে, কোনো মানুষকে আক্রমণ থাকতে পারে, ধর্ম বা লিঙ্গ নিয়ে কটূক্তি থাকতে পারে। প্ল্যাটফর্মের নিজের শর্তও আলাদা বিষয়। লাইসেন্স কোনো আইনি পরামর্শের বিকল্প নয়।
এজন্যই এই পাতায় আমরা যেসব সারি দেখাই, সেগুলো বাছাই করা: কোনো নাম নেই, গালি নেই। মডেলের ভুল পড়ার সময় আমাদের সব সারি দেখতে হয়েছে, কিন্তু প্রকাশের মান আলাদা। ডেটায় যে অভিনেতাদের নাম আছে, model হয়তো তাঁদের সঙ্গে sentiment জুড়ে ফেলে; কোনো শিল্পীর নাম একাই “ধনাত্মক” সংকেত হতে পারে। এটি নতুন নাটকে ভুল করবে আর মানুষকে নিয়ে পক্ষপাতও শিখবে। নাম সরিয়ে আবার মেপে দেখা একটি সরল পরীক্ষা, যা এই পর্বে করা হয়নি।
Gebru ও সহকর্মীদের “Datasheets for Datasets” প্রস্তাব (২০১৮ সালে প্রথম প্রকাশ) এই কারণেই বলে, প্রতিটি ডেটার সঙ্গে লিখে রাখা উচিত কে, কেন, কীভাবে সংগ্রহ করেছে, আর কাদের ওপর কী প্রভাব পড়তে পারে। Hugging Face-এর dataset card সেই চিন্তারই একটি ব্যবহারিক রূপ। এখানে আরেকটি শিক্ষা: একই ডেটার ভিন্ন কপিতে সারির সংখ্যা ও লাইসেন্স-লেখা মিলছে কি না মূল উৎসে দেখা উচিত।
মূল উপলব্ধি: লাইসেন্স অনুমতির প্রশ্ন, দায়িত্বের প্রশ্ন নয়। দুটিই আলাদা করে যাচাই করতে হয়।
প্র ০৩এই পর্বে near-duplicate ফাঁসে score প্রায় বাড়েনি। তাহলে এত ঝামেলার দরকার কী?
দরকার, কারণ ফলটি জানা গেছে মাপার পরে। আমরা দেখেছি যমজ সরালেও ঐ সারিগুলোর macro-F1 প্রায় একই থাকে, অর্থাৎ এখানে সারিগুলো মূলত ছোট আর সহজ। কিন্তু একই মাপ অন্য ডেটায় বড় হতে পারে: templated লেখা, একই পণ্যের ক্যাটালগ বিবরণ, একই খবরের ভিন্ন সংস্করণ, বা একই রোগীর একাধিক নোট।
Lewis, Stenetorp আর Riedel প্রশ্নোত্তরের কয়েকটি বহুল ব্যবহৃত benchmark-এ দেখান, test প্রশ্নের একটি বড় অংশের উত্তর train-এও আছে, আর সেই অংশে model-এর ফল বাকি অংশের চেয়ে অনেক আলাদা। Elangovan ও সহকর্মীরা (২০২১) NLP-এর কয়েকটি ডেটায় train-test মিল মাপেন, আর দেখান যে এটি মূল্যায়নকে উল্লেখযোগ্যভাবে প্রভাবিত করতে পারে। আরও বড় পরিসরে Kapoor আর Narayanan (২০২৩) নানা বিজ্ঞান ক্ষেত্রে ML গবেষণার পুনরুৎপাদনের সমস্যায় leakage-কে একটি বড় কারণ হিসেবে দেখান। প্রতিটি ফলের সংখ্যা নিজের নিজের প্রসঙ্গে।
খরচের দিক থেকেও হিসাবটি একপেশে। আমাদের দল বানাতে কয়েক মিনিটের কোড লেগেছে, আর group split-এ test সারির ৪.২% যমজ শূন্যে নেমেছে। আর সৎ হওয়ার একটি মূল্য: হুবহু কপি রেখে দিলে “ফালতু নাটক” ১০০ বার গোনা হয়, অর্থাৎ test-এর গঠনই সহজ দিকে হেলে যায়, যা আপনার পণ্যের আসল ব্যবহারে ঘটবে এমন নিশ্চয়তা নেই। তবে এটিও ভাবার বিষয়: বাস্তবেও যদি মানুষ একই কথা বারবার লেখে, তাহলে সেই পুনরাবৃত্তি প্রত্যাশিত। তাই “আসলে কী মাপতে চাই” ঠিক করুন, আর চাইলে দুটি সংখ্যাই রিপোর্ট করুন।
মূল উপলব্ধি: ফাঁস ছোট হওয়া একটি মাপা ফল। মাপা ছাড়া “ছোট” বলা অনুমান।
প্র ০৪মানুষের মত কি আদৌ “ধনাত্মক/ঋণাত্মক”-এ বাঁধা যায়? Sentiment-এর লেবেল নিজেই কতটা ঠিক?
Pang, Lee আর Vaithyanathan-এর ২০০২ সালের “Thumbs up?” প্রবন্ধ সিনেমার রিভিউকে দুই শ্রেণিতে ফেলে মেশিন লার্নিং প্রয়োগের একটি পরিচিত সূচনা। সেখানে লেবেল এসেছিল রিভিউয়ারের নিজের দেওয়া তারা থেকে, মানে মত মাপার একটি হাতে-গড়া প্রতিনিধি। পরে Socher ও সহকর্মীরা (২০১৩) Stanford Sentiment Treebank-এ বাক্যের প্রতিটি অংশে পাঁচ স্তরের লেবেল দেন, যাতে negation-এর মতো গঠন মাপা যায়। অর্থাৎ শ্রেণির কাঠামো নিজেই একটি নকশার সিদ্ধান্ত।
আমাদের ডেটায় লেবেল হাতে দেওয়া, আর শ্রেণি দুটি। ৬০টি ভুলের মধ্যে ১২টিতে মত মিশ্র, ১২টিতে নাটকের কথাই নেই। “সবকিছু ঠিকঠাক ছিল, কিন্তু শেষটা মেনে নিতে পারলাম না” ধরনের লেখার কোনো একটি সঠিক দুই-শ্রেণির উত্তর আছে কি? ভিন্ন পাঠক ভিন্ন লেবেল দিতে পারেন। তাই একই লেখায় মানুষের মধ্যে মিল (kappa; মেশিন লার্নিং কোর্সের পর্ব ৫) কত, সেটিই model-এর স্কোরের একটি বাস্তব সিলিং দেয়: মানুষ যেখানে একমত নয়, সেখানে ৯৯% F1 মানে হয়তো একজন লেবেলকারীর অভ্যাস মুখস্থ করা।
বাংলার নিজস্ব সমস্যাও আছে। ব্যঙ্গে শব্দের অর্থ উল্টে যায়; আঞ্চলিক রূপ আর Banglish-এ বানান ছড়িয়ে যায়; একই মন্তব্য নাটক, অভিনেতা আর পরিচালক তিনজনকে নিয়ে হতে পারে। SentNoB-এর মতো ডেটা তিন শ্রেণি ব্যবহার করে। Aspect-ভিত্তিক sentiment আবার প্রতিটি দিকের আলাদা মত খোঁজে। বাজারঘরের জন্য হয়তো “পণ্য”, “ডেলিভারি” আর “দাম” আলাদা করে জানাই বেশি কাজের।
মূল উপলব্ধি: Model শেখে লেবেল, সত্য নয়। আগে ঠিক করুন আসল প্রশ্নটি কী, তারপর সেই প্রশ্নের লেবেল বানান।
প্র ০৫একটি model-এর একটি স্কোর রিপোর্ট করা কেন বিপজ্জনক? সৎ রিপোর্টে কী কী থাকা উচিত?
আমাদের run-এ শুধু seed বদলে একই ভাগে CNN ০.৮৯০ থেকে ০.৯২৮-এ গেছে। Bag আর CNN-এর পার্থক্য ০.০০৪, যা এই ছড়ানোর অনেক ভেতরে। একটি seed দেখে তাই যেকোনো ক্রম সাজানো সম্ভব। আর লেখক সবচেয়ে ভালো seed-টি বাছলে, অনিচ্ছাকৃত হলেও, ফল ফুলে ওঠে।
এই সমস্যা ব্যাপকভাবে আলোচিত। Dodge ও সহকর্মীরা (২০১৯, “Show Your Work”) NLP-র প্রবন্ধে একটি best score-এর বদলে কত কনফিগারেশন চেষ্টা করা হয়েছে তা ধরে ফল দেখানোর প্রস্তাব দেন। Bouthillier ও সহকর্মীরা (২০২১) benchmark-এর variance-এর উৎস ভাগ করে দেখান, ডেটার ভাগ, seed আর hyperparameter থেকে পার্থক্য আসে, আর সেটি অনেক “উন্নতি”-র মাপের কাছাকাছি হতে পারে। Reimers আর Gurevych (২০১৭) sequence labeling-এ দেখান, শুধু seed বদলে একই model-এর স্কোর বেশ ছড়িয়ে যেতে পারে।
সৎ রিপোর্টের একটি সরল তালিকা: ডেটার উৎস, version, লাইসেন্স আর পরিষ্কারের ধাপ; ভাগের নিয়ম, যমজের সংখ্যা; সব model একই ভাগে; ভাগ ও seed-এর সংখ্যা; গড়, ছড়ানো, প্রতি শ্রেণির F1; সংশোধিত ব্যবধান, naive নয়; hyperparameter কী চেষ্টা হয়েছে; সময় আর সেটি কোন শর্তে; baseline; কী মাপা হয়নি। এই পর্বের ক্ষেত্রে শেষেরটি হলো pretrained encoder, ডেটার বাইরে সাধারণীকরণ, আর সময়ের ধারায় পরিবর্তন।
দাবি করার ভাষাও গুরুত্বপূর্ণ। “Model sentiment বোঝে” নয়; “এই ডেটায়, এই ভাগে, macro-F1 এত, এই ব্যবধানে”. একই কারণে একটি উচ্চ স্কোর দেখে আগে leakage খুঁজুন, তারপর আনন্দ করুন।
মূল উপলব্ধি: স্কোর একটি মাপ, আর প্রতিটি মাপের ত্রুটি আছে। ত্রুটির সীমা ছাড়া সংখ্যা অসম্পূর্ণ।
অনুশীলন
প্রথমে খাতায় করুন, তারপর উত্তর মেলান।
-
পূর্ণ numerical: একটি দুই-শ্রেণির ম্যাট্রিক্স: আসল ঋণাত্মক থেকে 2818 ঠিক, 235 ভুল; আসল ধনাত্মক থেকে 302 ভুল, 7354 ঠিক। (ক) ঋণাত্মক আর ধনাত্মকের F1. (খ) Macro আর micro. (গ) সবাইকে ধনাত্মক বললে macro কত?
(ক) ঋণাত্মক: TP = ২৮১৮, FP = ৩০২, FN = ২৩৫; F1 = ৫৬৩৬ / (৫৬৩৬ + ৫৩৭) = ০.৯১৩. ধনাত্মক: TP = ৭৩৫৪, FP = ২৩৫, FN = ৩০২; F1 = ১৪৭০৮ / (১৪৭০৮ + ৫৩৭) = ০.৯৬৫. (খ) Macro = (০.৯১৩ + ০.৯৬৫)/২ = ০.৯৩৯. Micro = accuracy = ১০১৭২ / ১০৭০৯ = ০.৯৫০. (গ) ধনাত্মকের অনুপাত π = ৭৬৫৬/১০৭০৯ = ০.৭১৫. F1 = ২π/(১ + π) = ০.৮৩৪; ঋণাত্মকের F1 = ০; macro = ০.৪১৭.
-
নকশা ও সমালোচনা: একজন সহকর্মী লিখেছেন: “পুরো ডেটায় vocabulary বানিয়ে
train_test_split, একটি seed, শুধু accuracy. BiLSTM ০.৯৬, তাই TF-IDF দেখার দরকার নেই। Test দেখে threshold ০.৪২ ঠিক করেছি। ডেটা আমাদের গ্রাহকদের কিছু আসল রিভিউ, যেগুলো নাম-ঠিকানাসহ ChatGPT-তে পাঠিয়ে লেবেল করিয়েছি।” কী কী বলবেন?(১) Vocabulary পুরো ডেটায়: test-এর শব্দ train-এর সিদ্ধান্তে; শুধু train থেকে বানান। (২) যমজ: হুবহু আর প্রায় একই সারি গুনুন; group split. (৩) একটি seed: আমাদের CNN-এ seed ভেদে ০.০৩৮ পার্থক্য; কয়েকটি seed ও ভাগ। (৪) শুধু accuracy: macro-F1, প্রতি শ্রেণি, ম্যাট্রিক্স। (৫) Baseline নেই: এই পর্বের run-এ baseline-ই সেরা। (৬) Test-এ threshold: validation-এ, খরচ ধরে; test একবার। (৭) ব্যক্তিগত তথ্য: নাম-ঠিকানা বাইরের সেবায় পাঠানো গুরুতর ঝুঁকি। আগে তথ্য সরান, প্রতিষ্ঠানের নীতি ও আইন দেখুন। (৮) লেবেল: LLM-এর দেওয়া লেবেল যাচাই ছাড়া সত্য নয়; একটি অংশ মানুষকে দিয়ে দেখিয়ে মিল মাপুন।
-
আউটপুট অনুমান করুন: কোড না চালিয়ে দুটি লাইনের আউটপুট লিখুন।
gramsঅংশ ৩-এর মতোই (n = 3).print(grams("abc abc")) print(round(jaccard("abcd", "abcde"), 2))প্রথম লাইন: টুকরোগুলো
abc,bc,c a,ab,abc. Set-এ পুনরাবৃত্তি থাকে না, তাই ৪টি উপাদান (ছাপার ক্রম ভিন্ন হতে পারে)। দ্বিতীয়:{abc, bcd}আর{abc, bcd, cde}; মিল ২, মোট ৩; ফল 0.67. শিক্ষা: ছোট লেখায় একটি অক্ষর যোগ করলেই Jaccard অনেক কমে, যা “অসাধারণ নাটক” ধরনের লেখায় আমরা দেখেছি। -
দুটি সিদ্ধান্তের হিসাব: (ক) ১৫টি ভাগে পার্থক্যের গড় d̄ = ০.০০৬৮, s = ০.০০৭৭, ntest/ntrain = ০.২৮৫. Naive আর সংশোধিত t বের করুন। ১৪ df-এ দুই-দিকের ৫% সীমা প্রায় ২.১৪৫. (খ) খারাপ রিভিউ চোখ এড়ালে ক্ষতি ৯ একক, ভালো রিভিউ পড়লে ১ একক। Calibrated সম্ভাবনায় t*? (গ) একই সম্ভাবনা p = ০.০৮-এ রিভিউটি পড়বেন কি?
(ক) Naive SE = ০.০০৭৭/√১৫ = ০.০০১৯৯; t ≈ ৩.৪, সীমা পার। সংশোধিত SE = ০.০০৭৭·√(১/১৫ + ০.২৮৫) = ০.০০৭৭ × ০.৫৯৩ = ০.০০৪৫৭; t ≈ ১.৫, সীমার ভেতরে। সিদ্ধান্ত উল্টে গেল। (খ) t* = ১/(১ + ৯) = ০.১. (গ) ০.০৮ < ০.১, তাই না. যাচাই: প্রত্যাশিত ক্ষতি না পড়লে ০.০৮ × ৯ = ০.৭২ একক; পড়লে ০.৯২ × ১ = ০.৯২ একক। না পড়াই সস্তা।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি text classifier-এর সৎ তুলনার ধাপগুলো সাজান (▲▼ চেপে সরান)
- লাইসেন্স দেখা, লেখা পরিষ্কার করা, হুবহু কপি বাদ
- প্রায় একই সারিগুলো এক দলে জোড়া
- দল না ভেঙে train, validation আর test ভাগ
- একই ভাগে baseline আর অন্য model, validation-এ বাছাই
- Test-এ macro-F1, প্রতি শ্রেণি, paired তুলনা
- ভুল পড়া, খরচ ধরে সিদ্ধান্ত
একটি একক-লেবেল তিন শ্রেণির কাজে ম্যাট্রিক্সের কর্ণে 72, 8, 1, মোট সারি 100. Micro-F1 কত?
- 0.546
- 0.900
- 0.810
- 0.081
৭২% ধনাত্মক দুই-শ্রেণির ডেটায় একটি model-এর accuracy ০.৭২, ঋণাত্মক শ্রেণির F1 শূন্য। কী বলা যায়?
- Model বেশ ভালো, ৭২% ঠিক
- Model সম্ভবত সবাইকে ধনাত্মক বলছে; macro-F1 এটি ধরবে
- Micro-F1 দেখলে সমস্যা ধরা পড়বে
- শুধু epoch বাড়ালেই ঠিক হবে
দুটি রিভিউয়ের character 3-gram Jaccard ০.৮৪, কিন্তু একটি “ডেলিভারি দ্রুত ছিল”, অন্যটি “ডেলিভারি দ্রুত ছিল না”. ভালো কাজ কোনটি?
- একটি মুছে ফেলা, কারণ দুটি প্রায় একই
- একটি train-এ আর একটি test-এ রাখা, যাতে negation পরীক্ষা হয়
- দুটির লেবেল এক করে দেওয়া
- এক দলে রেখে group split করা, লেবেল যেমন আছে তেমনই রাখা
১৫টি CV ভাগে দুই model-এর পার্থক্যে naive paired t বলছে p = ০.০০৪, আর Nadeau–Bengio সংশোধনে p = ০.১৬. সবচেয়ে ঠিক ব্যাখ্যা কোনটি?
- Fold স্বাধীন নয়; naive t অতি-আশাবাদী, তাই পার্থক্য নিশ্চিত নয়
- ছোট p-টিই নেওয়া উচিত, কারণ পার্থক্য আছে
- প্রমাণ হলো দুটি model হুবহু সমান
- ১০০ বার পুনরাবৃত্তি করে naive t-ই ব্যবহার করলে সমস্যা মিটবে
খারাপ রিভিউ চোখ এড়ালে ক্ষতি ৫ একক, ভালো রিভিউ পড়লে ক্ষতি ১ একক, আর সম্ভাবনা calibrated. খারাপ-সম্ভাবনার কোন threshold-এ মানুষকে পড়তে দেওয়া উচিত?
- 0.5
- 0.833
- প্রায় 0.167
- 0, অর্থাৎ সব পড়া
প্রায় ৭,৫০০ ছোট বাংলা মন্তব্যে, শূন্য থেকে শেখা embedding-এ একটি ছোট Transformer TF-IDF + logistic regression-এর চেয়ে কম macro-F1 পেল, সব ভাগে। কোন সিদ্ধান্ত সবচেয়ে যুক্তিসঙ্গত?
- টেক্সটে Transformer কখনো কাজ করে না
- এই ডেটা ও শর্তে baseline নিন; pretrained encoder আলাদা করে একই ভাগে মাপুন
- Transformer জেতা পর্যন্ত test দেখে hyperparameter বদলানো
- বাংলায় শব্দের ক্রমের কোনো গুরুত্ব নেই
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Metric: F1 = 2·TP / (2·TP + FP + FN). Macro সব শ্রেণিকে সমান ওজন দেয়; একক-লেবেলে micro = accuracy. সঙ্গে প্রতি শ্রেণি আর ম্যাট্রিক্স।
- ভাগ: হুবহু কপি বাদ, Jaccard দিয়ে যমজ দল, দল না ভেঙে ভাগ। ফাঁস ছোট না বড়, তা মেপে বলুন; এই ডেটায় প্রায় ০.০০৪.
- তুলনা: একই ১৫ ভাগে TF-IDF + LR ০.৯৪১, bag ০.৯১৭, CNN ০.৯১৩, BiLSTM ০.৯১০, Transformer ০.৯০৪. এই ডেটার একটি মাপ, সাধারণ নিয়ম নয়।
- প্রমাণ: কয়েকটি seed আর সংশোধিত t. Bag বনাম BiLSTM-এ naive p = ০.০০৪, সংশোধিত p = ০.১৬.
- সিদ্ধান্ত: ভুল পড়ুন, লেবেল সন্দেহ করুন, threshold খরচ দিয়ে বাছুন, আর deep model আনুন তখনই যখন একই ভাগে সে baseline-কে হারায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪৩ · Sequence labeling — NER, POS tagging, BIO আর CRF-এর ধারণাপরবর্তী পর্বপ্রতিটি token-এ লেবেল, span F1 আর CRF.
- ডিপ লার্নিং, NLP আর Generative AI · কোর্স হোম৯১টি পর্ব১৪টি মডিউলের পুরো রোডম্যাপ।
- পর্ব ৩১ · Sequence-এর জন্য 1D convolutionআগের পর্বকাল্পনিক ডেটায় text CNN আর negation.
- পর্ব ১৩ · Deep learning-এর lossআগের পর্বClass weight, focal loss আর imbalance.
- মেশিন লার্নিং · পর্ব ২২ · Model তুলনার পরিসংখ্যানআগের ভিত্তিMcNemar, Nadeau–Bengio আর একাধিক তুলনা।
- মেশিন লার্নিং · পর্ব ৫০ · Imbalanced ডেটাআগের ভিত্তিClass weight আর খরচ থেকে threshold.
- AI-এর ভিত্তি · পর্ব ৬৭ · বাংলা লেখার ডেটাআগের ভিত্তিUnicode, NFC, tokenization আর TF-IDF.
- AI-এর ভিত্তি · পর্ব ৫৬ · Train, validation আর testআগের ভিত্তিGroup split আর data leakage.