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

Language modeling — n-gram থেকে neural LM আর perplexity

Language Modeling — From N-gram Counts to Neural Language Models, Measured by Held-out Perplexity

Taught in Bangla, this lesson builds n-gram language models from the chain rule and the Markov assumption, computes MLE, Laplace and add-k estimates by hand, and measures held-out perplexity in nats and bits. In-page Python shows why larger n overfits on Bangla stories and how backoff and interpolation help, a fixed-window neural LM is written in NumPy with a gradient check, and real desktop runs compare n-gram, MLP, LSTM and small Transformer models fairly.

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

  • Chain rule আর Markov assumption দিয়ে n-gram model দাঁড় করানো
  • MLE, Laplace আর add-k হাতে হিসাব করা, আর শূন্য probability-র সমস্যা ধরা
  • Held-out perplexity হাতে করা; nats আর bits-এর মধ্যে বদলানো
  • Backoff আর interpolation কেন add-k-কে হারায়, সেটি পাতায় চালিয়ে দেখা
  • NumPy-তে fixed-window neural LM লিখে gradient check করা, আর চার ধরনের model-এর ন্যায্য তুলনা পড়া

পর্ব ৩৬-এ MiniGPT-এর পাশে bigram আর 5-gram ছিল শুধু baseline. মডিউল ৮ এখন LLM-এর দিকে যাবে, তাই শুরুতে প্রশ্নটি গোড়া থেকে: language model আসলে কী হিসাব করে, আর সেটি ভালো কি না কীভাবে মাপব? AI-এর ভিত্তি কোর্সের পর্ব ৭০-এ পরের শব্দের অনুমান বাইরে থেকে দেখেছেন। আজ গণিত আর সংখ্যা।

১ · Language model কী — পুরো লেখার probability

চায়ের দোকানের পুরোনো কর্মচারীর কথা ভাবুন। ক্রেতা বললেন “এক কাপ চা আর…”। তিনি বাক্য শেষ হওয়ার আগেই বিস্কুটের বয়ামে হাত দেন। কারণ হাজারবার শুনেছেন। “এক কাপ চা আর ল্যাপটপ” শুনলে তিনি থমকে যাবেন। Language model ঠিক এই দুটি কাজ করে: কোন লেখা কতটা স্বাভাবিক তার নম্বর দেয়, আর পরের শব্দ আন্দাজ করে।

আনুষ্ঠানিক সংজ্ঞায় language model হলো token-এর ক্রমের ওপর একটি probability বণ্টন: P(x১, x২, …, xT). এখানে xt হলো t-তম token, যা অক্ষর, শব্দ বা BPE টুকরো হতে পারে (পর্ব ৪০)। পুরো ক্রমের probability সরাসরি জানা অসম্ভব। কিন্তু probability-র chain rule সেটি ধাপে ভাঙে:

P(x১, …, xT) = P(x১) · P(x২ | x১) · P(x৩ | x১, x২) ⋯ P(xT | x১, …, xT−১)

এখানে কোনো অনুমান নেই; এটি probability-র নিয়ম। তাই “পরের token অনুমান করা” আর “পুরো লেখার probability দেওয়া” একই কাজ। সমস্যা হলো শেষের পদগুলোতে ইতিহাস অনেক লম্বা, আর ঠিক সেই ইতিহাস আগে কখনো দেখা যায়নি। প্রতিটি পদ্ধতির পার্থক্য এখানেই: এই লম্বা ইতিহাস কীভাবে সামলাবে।

n-gram model সবচেয়ে সরল পথ নেয়। সে ধরে নেয়, পরের token নির্ভর করে শুধু শেষ n − ১টি token-এর ওপর। একে Markov assumption বলে (AI-এর ভিত্তি কোর্সের পর্ব ৫৩):

P(xt | x১, …, xt−১) ≈ P(xt | xt−n+১, …, xt−১)

nনামকী দেখে“শাড়ি ভালো” শব্দ-স্তরে
১unigramকিছুই নাP(শাড়ি) · P(ভালো)
২bigramআগের ১টিP(শাড়ি | ⟨s⟩) · P(ভালো | শাড়ি)
৩trigramআগের ২টিP(ভালো | ⟨s⟩, শাড়ি)

⟨s⟩ হলো বাক্য শুরুর চিহ্ন, ⟨/s⟩ বাক্য শেষের। Trigram-এর সারিতে শেষ পদটিই দেখানো হয়েছে।

Jurafsky ও Martin-এর পাঠ্যবইয়ের n-gram অধ্যায়ের ইতিহাস অংশ অনুযায়ী, Markov ১৯১৩ সালে Pushkin-এর Eugene Onegin-এর ২০,০০০ অক্ষরকে স্বরবর্ণ আর ব্যঞ্জনবর্ণে ভাগ করে bigram আর trigram probability হিসাব করেন। Shannon ১৯৪৮ সালে n-gram দিয়ে ইংরেজির approximation বানান। “Language model” আর “perplexity” শব্দ দুটি এই প্রযুক্তিতে প্রথম ব্যবহার করে IBM-এর Fred Jelinek-এর দল, speech recognition-এর কাজে।

২ · গোনা থেকে probability — MLE আর Laplace

Probability কোথা থেকে আসবে? খাতায় গুনে। দেখা ডেটার likelihood সবচেয়ে বেশি করে যে মান, তাকে বলে maximum likelihood estimate (মেশিন লার্নিং কোর্সের পর্ব ১৮)। Bigram-এ সেটি একটি সরল ভাগ:

PMLE(y | x) = c(x y) / c(x)

c(x y) হলো x-এর ঠিক পরে y কতবার এসেছে, আর c(x) হলো x-এর পরে মোট কতবার কিছু এসেছে। সমস্যা শুরু হয় যখন c(x y) = ০। তখন একটি জোড়ার জন্য পুরো বাক্যের probability শূন্য, আর log-এ −∞। Smoothing দেখা ঘটনা থেকে সামান্য কেটে না-দেখা ঘটনাকে ভাগ দেয়। সবচেয়ে সরলটি Laplace smoothing, যা AI-এর ভিত্তি কোর্সের পর্ব ৫১-এ Naive Bayes-এ দেখেছেন। সব ঘরে ১ যোগ:

PLaplace(y | x) = (c(x y) + ১) / (c(x) + V)

V হলো পরের token হিসেবে সম্ভাব্য token-এর সংখ্যা। হরে V না যোগ করলে probability-র যোগফল ১ থাকে না। নিচের ছোট খাতায় তিনটি বাজারঘরের রিভিউ দিয়ে হিসাবটি ধাপে ধাপে দেখুন।

রিভিউ: শাড়ি ভালো · শাড়ি দামি · কাপ ভালো
ধাপ
হিসাব
  1. খাতা. প্রতিটি রিভিউতে ⟨s⟩ আর ⟨/s⟩ জুড়ে পাশাপাশি জোড়া গোনা হলো। পরের token হতে পারে পাঁচটি: শাড়ি, কাপ, ভালো, দামি, ⟨/s⟩. তাই V = ৫. ⟨s⟩ কখনো “পরের” হয় না।
  2. MLE. ⟨s⟩-এর পরে তিনবার কিছু এসেছে, দুবার শাড়ি। তাই ২/৩. বাকিগুলো একইভাবে।
  3. চেনা বাক্য. Chain rule-এর গুণ। ⟨/s⟩-ও একটি অনুমান, তাই তিনটি পদ।
  4. শূন্যের ফাঁদ. “কাপ দামি” খুব স্বাভাবিক বাক্য, কিন্তু কাপ→দামি জোড়া খাতায় নেই। MLE বলছে এই বাক্য অসম্ভব। Test-এ এমন একটি বাক্য থাকলেই perplexity অসীম।
  5. Laplace. সব ঘরে ১ যোগ। কাপ-এর পরে এখন পাঁচটি ঘরে ভাগ: ভালো ২/৬, বাকি চারটি ১/৬ করে। যোগফল ১.
  6. ফাঁদ ভাঙা. ⟨s⟩→কাপ = (১ + ১)/(৩ + ৫) = ২/৮, দামি→⟨/s⟩ = (১ + ১)/(১ + ৫) = ২/৬. গুণফল এখন শূন্য নয়।
  7. মূল্য. চেনা বাক্যের probability ০.৩৩৩ থেকে ০.০৪৫৯, সাত গুণের বেশি কমল। Laplace দেখা ঘটনা থেকে অনেক বেশি কেটে নেয়, বিশেষ করে V বড় হলে। এজন্য k-কে ১-এর ছোট করা হয়।

সেই সাধারণ রূপের নাম add-k smoothing: P = (c(x y) + k) / (c(x) + kV). k-এর মান train-এর লেখা দিয়ে নয়, আলাদা val অংশে বাছতে হয়। Train-এ বাছলে k = ০-ই সেরা দেখাবে।

৩ · Perplexity — না-দেখা লেখায় মাপা

Model কতটা ভালো, তা train-এ মাপলে মুখস্থকেই পুরস্কার দেওয়া হয়। তাই মাপতে হবে held-out data-য়, যে লেখা গোনার সময় সরিয়ে রাখা ছিল (AI-এর ভিত্তি কোর্সের পর্ব ৫৬)। N টি অনুমানের জন্য:

NLL = −(১/N) ∑ ln P(xt | ইতিহাস)  ·  perplexity = eNLL

এই NLL-ই test লেখার ওপর গড় cross-entropy (পর্ব ১৩, AI-এর ভিত্তি কোর্সের পর্ব ৩৮)। ln নিলে একক nats; log২ নিলে bits per token. দুটি থেকে একই perplexity আসে। ব্যাখ্যাটি হলো: perplexity p মানে model গড়ে যেন p টি সমান সম্ভাব্য বিকল্পের মধ্যে দোটানায়। V টি token-এ এলোমেলো অনুমানের perplexity ঠিক V; নিখুঁত অনুমানের ১.

N = ৩ অনুমান: শাড়ি, ভালো, ⟨/s⟩
ধাপ
হিসাব
  1. Probability. আগের খাতার Laplace মান। ⟨s⟩-কে অনুমান করা হয় না, তাই সেটি গোনায় নেই; ⟨/s⟩ গোনায় আছে।
  2. Surprise. প্রতিটি অনুমানের −ln p. যে অনুমানে probability কম, সেখানে দণ্ড বেশি।
  3. গড়. তিনটি যোগ করে তিন দিয়ে ভাগ। এটিই NLL.
  4. Exp. e১.০২৭ ≈ ২.৭৯. V = ৫-এ এলোমেলো অনুমানের perplexity হতো ৫. Model সেটি ২.৭৯-এ নামিয়েছে, যদিও এই বাক্যটি train-এই ছিল।
  5. Bits. ln ২ ≈ ০.৬৯৩ দিয়ে ভাগ করলে bits. ২ ঘাত bits একই ২.৭৯ দেয়। Log-এর base বদলালে perplexity বদলায় না।
  6. জ্যামিতিক গড়. একই সংখ্যা অন্য পথে: ১/p-গুলোর জ্যামিতিক গড়। Log ছাড়া হাতে যাচাইয়ের সবচেয়ে সহজ উপায়।

Log-এ যোগ করুন, probability গুণ করবেন না। ১০০০টি ০.১ গুণ করলে কম্পিউটারের float শূন্য হয়ে যায় (underflow), অথচ −ln-এর যোগফল ঠিকই ২৩০২.৬ থাকে। আর একটি শূন্য probability থাকলে NLL অসীম: এজন্যই প্রতিটি n-gram model-এ smoothing বাধ্যতামূলক।

৪ · n বাড়ালে কী হয় — আসল বাংলা গল্পে

এবার পর্ব ৩৬-এর উপেন্দ্রকিশোর রায়চৌধুরীর ৩৯টি গল্প, একই ভাগে, তবে শুধু গল্প: train ৩১টি, val ৪টি, test ৪টি। ভাগ পুরো গল্প ধরে, যাতে একই গল্পের বাক্য দুই পাশে না পড়ে। উৎস বাংলা উইকিসংকলন, পাতায় PD-India ট্যাগ আছে; নিজের কাজে ব্যবহারের আগে ট্যাগ আর আপনার দেশের নিয়ম নিজে দেখে নিন। Token হলো code point.

নিচের কোড পাতাতেই চলে। ব্যবহার করে সেই ভাগেরই ছোট টুকরো, পাতার ভেতরে রাখা: train ১৩,৮৫৪ অক্ষর, val আর test প্রায় ২,৪০০ করে। প্রথম চালাতে কয়েক সেকেন্ড লাগতে পারে। ব্লকগুলো একই namespace ভাগ করে, তাই পরেরগুলো এই খাতা ব্যবহার করবে। প্রতিটি n একই অবস্থানে মাপা হয়।

import numpy as np
from collections import Counter, defaultdict
read = lambda f: open(f, encoding="utf-8").read()
train = read("golpo_train.txt")
chars = sorted(set(train)); V = len(chars)
keep = lambda s: "".join(c for c in s if c in chars)
val, test = keep(read("golpo_val.txt")), keep(read("golpo_test.txt"))
C, Ctx = {}, {}                  # n-gram খাতা আর context খাতা
for n in range(1, 5):
    C[n] = Counter(train[i:i+n] for i in range(len(train)-n+1))
    Ctx[n] = defaultdict(int)
    for g, c in C[n].items():
        Ctx[n][g[:-1]] += c
START = 3                        # সব n একই অবস্থানে মাপা হবে
def grams(s, n):
    return [s[i-n+1:i+1] for i in range(START, len(s))]
def addk(g, n, k):
    return (C[n].get(g, 0) + k) / (Ctx[n].get(g[:-1], 0) + k*V)
def perp(s, n, k):
    p = np.array([addk(g, n, k) for g in grams(s, n)])
    return float(np.exp(-np.log(p).mean()))
print(f"অক্ষর train {len(train)}, val {len(val)}, test {len(test)}, V = {V}")
print(" n  খাতার ঘর  k(val)  train    val   test  নতুন%")
for n in range(1, 5):
    ks = [1, .3, .1, .03, .01, .003]
    k = min(ks, key=lambda k: perp(val, n, k))
    new = np.mean([g not in C[n] for g in grams(test, n)])
    print(f" {n} {len(C[n]):8d} {k:7.3f} {perp(train, n, k):6.2f} "
          f"{perp(val, n, k):6.2f} {perp(test, n, k):6.2f} {new:6.1%}")
print("Laplace k = 1, test:",
      [round(perp(test, n, 1), 2) for n in range(1, 5)])

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

অক্ষর train 13854, val 2468, test 2370, V = 68
 n  খাতার ঘর  k(val)  train    val   test  নতুন%
 1       68   1.000  28.50  29.71  29.10   0.0%
 2      795   0.300   9.71  15.20  12.53   3.0%
 3     2533   0.100   5.10  17.30  10.07  15.9%
 4     4877   0.100   4.30  27.00  13.56  34.8%
Laplace k = 1, test: [29.1, 13.29, 14.88, 23.87]

“নতুন%” হলো test-এর যে n-gram train-এ কখনো আসেনি। Train-এর perplexity n বাড়লে টানা কমে: ২৮.৫০ থেকে ৪.৩০. Test কমে n = ৩ পর্যন্ত, ১০.০৭, তারপর n = ৪-এ বেড়ে ১৩.৫৬. একই সময়ে নতুন n-gram ০% থেকে ৩৪.৮%. Laplace-এর সারি দেখুন: n = ৪-এ ২৩.৮৭, মানে k = ১ এখানে অনেক বেশি কেটে নিচ্ছে। Val আর test-এর ফারাকও লক্ষ্য করুন। ছোট টুকরোয় গল্পভেদে সংখ্যা অনেক নড়ে।

পুরো গল্পে একই পরীক্ষা, desktop-এ (সত্যিকারের run; Python 3.14, শুধু standard library; সব n মিলিয়ে গোনায় CPU-তে ০.৪৩ সেকেন্ড)। Test-এর ২৫,৬৬২টি একই অবস্থান, মানে প্রথম ১২৮ অক্ষরের পর থেকে:

nখাতার ঘরk (val)trainvaltesttest-এ নতুন n-gramtest-এ নতুন context
১৮৩০.০০১২৮.২৮২৮.২৪২৮.১২০.০%০.০%
২১,৭৩০০.১০০১০.৫০১০.৯৫১০.৮২০.২%০.০%
৩৮,৯৯৭০.১০০৫.৯৮৭.৩৯৬.৯১২.১%০.২%
৪২৫,৩৬৭০.০৩০৩.৯৯৬.৫৯৬.২০৮.০%২.১%
৫৫০,৭৬৩০.০৩০৩.২৩৮.০৬৭.৫৮১৯.৬%৮.০%
৬৭৮,৪৮১০.০৩০২.৯৪১১.৫৫১১.০২৩৪.১%১৯.৬%
৭১,০৩,৯৮৯০.০১০২.১৩১৭.৪৮১৬.৮৯৪৮.৭%৩৪.১%

train, val, test কলামে add-k-এর perplexity; train মাপা হয়েছে প্রথম ৬০,০০০ অক্ষরে। একটি ভাগ, গোনা তাই seed নেই।

এই হলো data sparsity. ৮৩টি অক্ষরে সম্ভাব্য bigram ৬,৮৮৯টি, দেখা গেছে ২৫%. সম্ভাব্য 4-gram প্রায় ৪.৭৫ কোটি, দেখা গেছে ০.০৫%. অনেক ঘর বানানের নিয়মেই অসম্ভব, কিন্তু সম্ভব ঘরের বড় অংশও এত ছোট খাতায় আসে না। একটি আসল অনুমান দিয়ে দেখা যাক: test-এর “ধানের খোসা” শব্দে “খোস”-এর পরে “া”।

train-এর ১,৯২,০১৪ অক্ষরে গোনা, V = ৮৩
ধাপ
context
গোনা
probability
  1. ছোট context. “স”-এর পরে “া” ২৮১ বার। গোনা ভরসাযোগ্য, কিন্তু context খুব ঝাপসা।
  2. Context লম্বা. “োস” train-এ ২১ বার, “া” একবারও নয়। Model এখন আত্মবিশ্বাসের সঙ্গে ভুল: probability ০.০০৩৪, n = ২-এর চেয়ে প্রায় ২৬ গুণ কম।
  3. Context অচেনা. “খোস” train-এ নেই। Add-k-তে সব ঘর সমান হয়ে যায়, ১/৮৩. n যত বাড়ুক, ফল একই; ইতিহাসের কোনো তথ্য আর কাজে লাগে না।
  4. পুরো test. একটি উদাহরণ নয়, এটিই নিয়ম। n = ৭-এ প্রায় অর্ধেক অনুমানেই n-gram-টি অচেনা।
  5. দুই মুখ. Train-এর সংখ্যা কমতেই থাকে, test উল্টো বাড়ে। এটিই overfitting-এর n-gram রূপ।
  6. ওষুধ. সব order মিশিয়ে নিলে অচেনা context-এর ক্ষতি কমে, ছোট context-এর তথ্য ফেরে। ০.০১২০ থেকে ০.০১৭২. পরের অংশে এটিই।

৫ · Add-k-এর পরে — backoff, interpolation আর Kneser-Ney

Add-k-এর দোষ হলো, সে না-দেখা ঘরগুলোকে সমান ভাগ দেয়। অথচ ছোট context জানে কোন অক্ষর বেশি সম্ভাব্য। দুটি পরিচিত সমাধান:

Backoff

লম্বা context-এ ভরসা, না পেলে ছোট context-এ নামা। এখানে absolute discounting: প্রতিটি দেখা গোনা থেকে d কেটে নিচের ধাপকে দেওয়া।

Pn = max(c − d, ০)/c(x) + d · T(x)/c(x) · Pn−১

Interpolation

সব order-এর ওজন করা যোগ। ওজন λ-গুলো শেখা হয় val-এ, train-এ নয়। Train-এ শিখলে সব ওজন লম্বা order-এ চলে যেত।

P = λ০/V + λ১P১ + ⋯ + λnPn

T(x) হলো x-এর পরে কত রকম আলাদা token দেখা গেছে। ফলে কেটে নেওয়া ভাগের যোগফল মিলে যায়, আর probability-র যোগ ১ থাকে।

fan = {n: Counter(g[:-1] for g in C[n]) for n in C}   # কত রকম পরের অক্ষর
def backoff(g, d):
    p = 1 / V                    # শুরু: সবার সমান ভাগ
    for n in range(1, len(g) + 1):
        h = g[-n:]; c0 = Ctx[n].get(h[:-1], 0)
        if c0:                   # d করে কেটে নিচের n-এর ভাগে দেওয়া
            p = max(C[n].get(h, 0) - d, 0) / c0 + d * fan[n][h[:-1]] / c0 * p
    return p
def table(s, order):             # প্রতি অবস্থানে সব order-এর MLE
    rows = []
    for g in grams(s, order):
        r = [1 / V]
        for n in range(1, order + 1):
            h = g[-n:]; c0 = Ctx[n].get(h[:-1], 0)
            r.append(C[n].get(h, 0) / c0 if c0 else 1 / V)
        rows.append(r)
    return np.array(rows)
def em(order, steps=30):         # λ শেখা হয় শুধু val-এ
    T = table(val, order); lam = np.ones(order + 1) / (order + 1)
    for _ in range(steps):
        w = lam * T
        lam = (w / w.sum(1, keepdims=True)).mean(0)
    return lam
ex = lambda p: float(np.exp(-np.log(p).mean()))
print("order  add-k  backoff  interp   λ(ক্রম ০..n)")
for o in [2, 3, 4]:
    k = min([1, .3, .1, .03, .01, .003], key=lambda k: perp(val, o, k))
    d = min([.3, .5, .7, .9], key=lambda d: ex(np.array(
        [backoff(g, d) for g in grams(val, o)])))
    b = ex(np.array([backoff(g, d) for g in grams(test, o)]))
    lam = em(o)
    print(f"  {o}  {perp(test, o, k):6.2f} {b:7.2f} {ex(table(test, o) @ lam):7.2f}",
          " ", np.round(lam, 2))

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

order  add-k  backoff  interp   λ(ক্রম ০..n)
  2   12.53   12.57   12.51   [0.03 0.1  0.87]
  3   10.07    9.27    9.15   [0.03 0.08 0.47 0.42]
  4   13.56    9.20    8.61   [0.02 0.07 0.46 0.29 0.15]

λ-গুলো EM দিয়ে শেখা: প্রতি অবস্থানে দেখা হয় কোন order সঠিক অক্ষরকে কত probability দিল, সেই অনুপাতে ওজন বদলায়। Order ৪-এ add-k ১৩.৫৬, কিন্তু interpolation ৮.৬১: একই খাতা, শুধু ব্যবহার ভিন্ন। Order বাড়াতে এখন ক্ষতি নেই, কারণ λ৪ মাত্র ০.১৫; val নিজেই বলে দিচ্ছে এই খাতায় লম্বা order-এ কম ভরসা।

পুরো গল্পে একই তুলনা, desktop-এ (সত্যিকারের run, test perplexity; d আর λ শুধু val-এ বাছা):

orderadd-kbackoff (d)interpolation
৩৬.৯১৬.৮০ (০.৮)৬.৮০
৪৬.২০৫.৬৮ (০.৮)৫.৫৮
৫৭.৫৮৫.৬১ (০.৯)৫.৩২
৬১১.০২৫.৮৩ (০.৯)৫.২৬
৭১৬.৮৯৬.১৪ (০.৯)৫.২৫

Interpolation-এ order ৭-এ এসেও ক্ষতি হয় না, কিন্তু লাভও প্রায় নেই। Sparsity-র ওষুধ আছে; দেয়ালটি অন্য জায়গায়: খাতায় “চা” আর “কফি”-র মধ্যে কোনো সম্পর্ক নেই।

Kneser-Ney smoothing এই ধারায় আরেক ধাপ। সে নিচের ধাপে token-এর মোট ঘনত্বের বদলে দেখে, token-টি কত রকম আলাদা context-এর পরে এসেছে। কোনো শব্দ ঘন ঘন এলেও যদি প্রায় সবসময় একটি নির্দিষ্ট শব্দের পরেই আসে, নতুন context-এ সে কম ভাগ পায়। Chen ও Goodman (১৯৯৬, ১৯৯৯)-এর তুলনামূলক পরীক্ষার পর এর একটি রূপ দীর্ঘদিন n-gram-এর পরিচিত baseline ছিল। এই পর্বে Kneser-Ney চালানো হয়নি; শুধু নাম আর ধারণা। Brants ও সহকর্মীদের ২০০৭ সালের stupid backoff আবার বিশাল ডেটার জন্য, সেটি সত্যিকারের probability-ই দেয় না, তাই perplexity মাপার কাজে আসে না।

৬ · Neural LM-এর ধারণা — খাতার বদলে শেখা weight

“শাড়ি ভালো” অনেকবার দেখলেও “লুঙ্গি ভালো” না দেখলে খাতা কিছুই জানে না; দুটি আলাদা ঘর। Bengio ও সহকর্মীরা ২০০৩ সালে “A Neural Probabilistic Language Model” প্রবন্ধে অন্য পথ দেখান। প্রতিটি token পায় একটি ছোট embedding (পর্ব ৪১), আর শেষ w টি token-এর embedding জুড়ে একটি MLP পরের token-এর probability দেয়। একই ধরনের token-এর embedding কাছাকাছি হলে তথ্য তাদের মধ্যে ভাগ হতে পারে। একে বলে neural language model, আর এই রূপটি fixed-window LM.

x = [E(xt−w), …, E(xt−১)]  ·  logits = b + U tanh(d + Hx)

E হলো embedding টেবিল, H আর U weight matrix, d আর b bias. প্রবন্ধে input থেকে output-এ ঐচ্ছিক সরাসরি সংযোগও ছিল (Wx), যা আমরা বাদ দিয়েছি। Parameter বাড়ে w-এর সঙ্গে সরলরেখায়, n-gram-এর মতো গুণিতকে নয়; এটিই আসল লাভ।

Fixed-window neural LM, w = ৪

একই E চারটি অক্ষরে ব্যবহার হয়। shape: V = ৬৮, e = ১৬, h = ৯৬।

Fixed-window neural language model with shapes বললে E: একই টেবিল, জুড়ে দেওয়া tanh(d + Hx) logits = b + U · t softmax P(পরের অক্ষর) (B, 4) (B, 64) (B, 96) (B, 68) (B, 68)
পরের অক্ষর অনুমানে খাতার ঘর নেই; আছে ১৩,৯২৪টি শেখা সংখ্যা।

Parameter হাতে: V·e + (w·e)·h + h + h·V + V = ১,০৮৮ + ৬,১৪৪ + ৯৬ + ৬,৫২৮ + ৬৮ = ১৩,৯২৪. Backprop নিজে লেখা (পর্ব ৪), তাই আগে gradient check: প্রতিটি parameter দলের একটি করে ঘরে হাতে লেখা gradient আর সংখ্যায় পাওয়া (θ + ε থেকে θ − ε) gradient মেলানো (পর্ব ১৮)।

w, e, h = 4, 16, 96              # জানালা, embedding, hidden
ix = {c: i for i, c in enumerate(chars)}
def windows(s):
    a = np.array([ix[c] for c in s], dtype=np.intp)
    return np.stack([a[i:i+w] for i in range(len(a)-w)]), a[w:]
rng = np.random.default_rng(42)
P = dict(E=rng.normal(0, .3, (V, e)), H=rng.normal(0, .15, (w*e, h)),
         d=np.zeros(h), U=rng.normal(0, .1, (h, V)), b=np.zeros(V))
def forward(P, X):
    x = P["E"][X].reshape(len(X), -1)
    t = np.tanh(x @ P["H"] + P["d"])
    L = t @ P["U"] + P["b"]
    q = np.exp(L - L.max(1, keepdims=True))
    return x, t, q / q.sum(1, keepdims=True)
def loss(P, X, y):
    return -np.log(forward(P, X)[2][np.arange(len(y)), y]).mean()
def grad(P, X, y):               # হাতে লেখা backprop
    n = len(y); x, t, q = forward(P, X)
    q[np.arange(n), y] -= 1; q /= n      # ∂loss/∂logit
    dt = q @ P["U"].T * (1 - t**2)
    dE = np.zeros_like(P["E"])
    np.add.at(dE, X, (dt @ P["H"].T).reshape(n, w, e))
    return dict(E=dE, H=x.T @ dt, d=dt.sum(0), U=t.T @ q, b=q.sum(0))
Xc, yc = windows(train[:300])    # gradient check: ছোট batch
G, r = grad(P, Xc, yc), np.random.default_rng(0)
for k in P:
    j = tuple(int(r.integers(s)) for s in P[k].shape)
    old = P[k][j]; eps = 1e-5
    P[k][j] = old + eps; up = loss(P, Xc, yc)
    P[k][j] = old - eps; dn = loss(P, Xc, yc); P[k][j] = old
    num = (up - dn) / (2*eps)
    print(f"{k}{j}: হাতে {G[k][j]:+.6f}  সংখ্যায় {num:+.6f}")
print("parameter:", sum(p.size for p in P.values()), " শুরুর loss:",
      round(float(loss(P, Xc, yc)), 3), " ln V:", round(float(np.log(V)), 3))

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

E(57, 10): হাতে +0.005150  সংখ্যায় +0.005150
H(32, 25): হাতে -0.001048  সংখ্যায় -0.001048
d(29,): হাতে +0.009766  সংখ্যায় +0.009766
U(3, 5): হাতে +0.000601  সংখ্যায় +0.000601
b(1,): হাতে -0.145224  সংখ্যায় -0.145224
parameter: 13924  শুরুর loss: 4.268  ln V: 4.22

পাঁচটি ঘরেই ছয় দশমিক পর্যন্ত মিল, মানে backprop-এর পাঁচটি সূত্র ঠিক আছে। np.add.at লক্ষ্য করুন: একই অক্ষর জানালায় দুবার থাকলে তার gradient যোগ হতে হবে, নইলে check ধরা পড়ত। শুরুর loss ৪.২৬৮, ln V = ৪.২২০-এর কাছে; এলোমেলো weight এলোমেলো অনুমান দিচ্ছে, যেমন হওয়া উচিত।

এবার training, Adam দিয়ে (মেশিন লার্নিং কোর্সের পর্ব ১৪), আর দুই model থেকে লেখা তোলা। প্রতিবার probability থেকে একটি অক্ষর তুলে জানালা সরানো হয়। বিস্তারিত sampling কৌশল আসবে পর্ব ৫০ (Decoding)-এ।

Xtr, ytr = windows(train); Xva, yva = windows(val); Xte, yte = windows(test)
M = {k: np.zeros_like(p) for k, p in P.items()}   # Adam-এর দুই স্মৃতি
S = {k: np.zeros_like(p) for k, p in P.items()}
best, bestP, every = 1e9, None, 250
rng = np.random.default_rng(42)
for step in range(1, 1501):
    i = rng.integers(0, len(ytr), 128)
    g = grad(P, Xtr[i], ytr[i])
    lr = .004 * (1 - step / 1700)
    for k in P:
        M[k] = .9*M[k] + .1*g[k]; S[k] = .999*S[k] + .001*g[k]**2
        mh = M[k] / (1 - .9**step); sh = S[k] / (1 - .999**step)
        P[k] -= lr * mh / (np.sqrt(sh) + 1e-8)
    if step % every == 0:
        v = float(loss(P, Xva, yva))
        print(f"step {step}  train {float(loss(P, Xtr[:4000], ytr[:4000])):.3f}"
              f"  val {v:.3f}  perplexity {np.exp(v):.2f}")
        if v < best:
            best, bestP = v, {k: p.copy() for k, p in P.items()}
print(f"সেরা val-এর weight, test perplexity {np.exp(loss(bestP, Xte, yte)):.2f}")
def write(model_p, prompt, n=110, seed=42):
    r = np.random.default_rng(seed); s = prompt
    for _ in range(n):
        s += chars[int(r.choice(V, p=model_p(s)))]
    return s.replace("\n", " / ")
def neural(s):
    X = np.array([[ix[c] for c in s[-w:]]], dtype=np.intp)
    p = forward(bestP, X)[2][0]
    return p / p.sum()
def bo4(s):
    p = np.array([backoff(s[-3:] + c, .7) for c in chars])
    return p / p.sum()
print("4-gram backoff:", write(bo4, "এক যে ছিল"))
print("window MLP    :", write(neural, "এক যে ছিল"))

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

step 250  train 2.074  val 2.690  perplexity 14.73
step 500  train 1.884  val 2.687  perplexity 14.69
step 750  train 1.781  val 2.667  perplexity 14.40
step 1000  train 1.704  val 2.682  perplexity 14.62
step 1250  train 1.637  val 2.713  perplexity 15.07
step 1500  train 1.603  val 2.689  perplexity 14.72
সেরা val-এর weight, test perplexity 9.82
4-gram backoff: এক যে ছিল। সে সেই থলের পায়ে বলে, তাই ভালো সাজার নীচ দিয়ে গড়ি-মিড়ি-বাঘ বললে, রোজ তারপর পিঁটাকে দাও, তাদে হাটিয়ে ছিল
window MLP    : এক যে ছিলে।-সত্যে নাল।য়। আর বালেন, আতিনে ফের পরলেন। / রেখলে গকেটে পাগদ্ধুর বাড়ি দেখ। / তাবুড়ী আগুদিধ ভাতে তা হুতুরার চাল

একটি সৎ ফল: এই ছোট টুকরোয় MLP-এর test perplexity ৯.৮২, যা add-k-এর সেরা ১০.০৭-কে হারায়, কিন্তু backoff-এর ৯.২০ আর interpolation-এর ৮.৬১-এর চেয়ে খারাপ (MLP এখানে এক অবস্থান কম মাপে, ২,৩৬৬টি, যা নগণ্য)। Train loss নামছে, val থেমে গেছে step ৭৫০-এ: ১৪ হাজার অক্ষর এই model-এর জন্য কম। “Neural মানেই ভালো” কথাটি এখানে খাটে না। ডেটা বাড়লে কী হয়, পরের অংশে।

৭ · একই ডেটায় তিন neural model — সত্যিকারের run

ন্যায্য তুলনার শর্ত আগে লিখে নিই: একই ৮৩ অক্ষরের tokenizer; একই train, val, test; একই ২৫,৬৬২টি test অবস্থান, প্রতিটিতে আগের অন্তত ১২৮ অক্ষর উপলব্ধ; hyperparameter আর সেরা চেকপয়েন্ট শুধু val দিয়ে বাছা; test মাপা হয়েছে সবার শেষে। Parameter সংখ্যা কাছাকাছি রাখা হয়েছে। প্রতিটি neural model তিনটি seed-এ, GPU-তে, torch 2.15.0-তে।

Modelনকশাকত ইতিহাস দেখে
Window MLPembedding ৩২, দুই hidden layer ৬৪০, GELU, dropout ০.৪w = ৬ অক্ষর (val-এ বাছা)
LSTM (পর্ব ২৮–২৯)embedding ৬৪, দুই layer ২৫৬, dropout ০.২পুরো আগের লেখা, hidden state-এ; train-এ ১২৮ অক্ষর
Transformer (পর্ব ৩৬)d = ১২৮, ৪ head, ৪ layer, pre-norm, causal mask, dropout ০.২১২৮ অক্ষর

Bengio-ধাঁচের model-টির কোড (models.py থেকে ছাঁটা; LSTM আর Transformer আগের পর্বগুলোর ধরনেই, তাই বাদ):

class WindowMLP(nn.Module):
    def __init__(self, vocab, w=16, emb=32, hidden=640, p=0.2):
        super().__init__()
        self.w = w
        self.emb = nn.Embedding(vocab, emb)
        self.net = nn.Sequential(
            nn.Linear(w * emb, hidden), nn.GELU(), nn.Dropout(p),
            nn.Linear(hidden, hidden), nn.GELU(), nn.Dropout(p),
            nn.Linear(hidden, vocab))

    def forward(self, ctx):                        # ctx: (B, w)
        return self.net(self.emb(ctx).flatten(1))  # (B, V)

Training-এর কাঠামো তিনটিতেই একই: AdamW, OneCycle schedule, gradient clip ১.০, batch ৬৪ জানালা, প্রতিটি ১২৮ অক্ষরের। MLP-তে ১,৫০০ step, বাকি দুটিতে ৪,০০০। MLP-এর window আর dropout seed ৪২-এ val দিয়ে বাছা হলো (সত্যিকারের run, torch 2.15.0, GPU-তে; সংখ্যা val NLL, nats-এ):

$ uv run python train_all.py mlp 42 <w> <dropout>
w   dropout  parameters  best step  val NLL
4   0.2      548659      1200       1.661
4   0.4      548659      1500       1.662
6   0.2      589619      1200       1.658
6   0.4      589619      1500       1.639   ← বাছা
8   0.2      630579       900       1.710
8   0.4      630579      1500       1.669
16  0.2      794419       400       1.844
16  0.4      794419       800       1.773
32  0.2     1122099       300       2.040
32  0.4     1122099       400       1.965

জানালা লম্বা করলে parameter বাড়ে, আর val খারাপ হয়, যদিও বেশি ইতিহাস দেখছে। MLP প্রতিটি অবস্থানের জন্য আলাদা weight শেখে: একই অক্ষর এক ঘর সরে গেলে সে নতুন করে শিখতে হয়। এই অপচয়ই ওজন ভাগ করে নেওয়া RNN আর Transformer-কে যুক্তি দেয়।

৮ · ফল — perplexity, parameter, সময় আর নমুনা

Modelparameter / খাতার ঘরসেরা steptrain perplexitytest NLL (nats)test perplexity (seed ৪২, ৪৩, ৪৪)bits প্রতি অক্ষরtraining সময় (s)
4-gram, add-k২৫,৩৬৭ ঘর—৩.৯৯১.৮২৫৬.২০২.৬৩৭ order মিলিয়ে গোনায় ০.৪৩, CPU-তে
5-gram, backoff৮৬,৯৪০ ঘর——১.৭২৫৫.৬১২.৪৯
7-gram, interpolation২,৬৯,৪১০ ঘর + ৮টি λ——১.৬৫৮৫.২৫২.৩৯
Window MLP, w = ৬৫,৮৯,৬১৯১৫০০, ১৫০০, ১৫০০৩.৪০–৩.৪৩১.৫৮৭৪.৮৭, ৪.৯০, ৪.৯০২.২৯৫২–৫৭
ছোট Transformer৮,১৮,৩০৪২০০০, ১৫০০, ২০০০২.৯৩–৩.৩৯১.৫৭০৪.৮৭, ৪.৮৪, ৪.৭০২.২৬৩৩১–২,০৬১
LSTM৮,৮২,৭০৭২২৫০, ১৫০০, ২২৫০২.৭৪–২.৯০১.৫১৪৪.৫৫, ৪.৫৭, ৪.৫২২.১৯৪৫২–৯০৭

সব সংখ্যা এই যন্ত্রের সত্যিকারের run, একটি ভাগে; test NLL আর bits তিন seed-এর গড়। সবার test অবস্থান একই ২৫,৬৬২টি; train মাপা হয়েছে প্রথম ৬০,০০০ অক্ষরে। এলোমেলো অনুমানের perplexity হতো ৮৩।

LSTM-এর একটি run-এর লগ (সত্যিকারের run, ছাঁটা):

$ uv run python train_all.py lstm 42
lstm_s42: parameters 882707, vocab 83
    1 train(batch) 4.430 val NLL 4.428
 1000 train(batch) 1.550 val NLL 1.606
 2250 train(batch) 1.257 val NLL 1.570
 4000 train(batch) 1.173 val NLL 1.583
best step 2250;
train 1.009 val 1.570 test 1.516 nats;
test perplexity 4.55;
2.187 bits;
positions 25662;
train time 452 s

টেবিল থেকে পাঁচটি কথা পড়া যায়, আর প্রতিটির পাশে সীমাও আছে।

  • Neural জিতেছে, তবে ডেটা থাকলে। ১,৯২,০১৪ অক্ষরে interpolation-এর ৫.২৫-এর তুলনায় MLP প্রায় ৪.৮৯, LSTM প্রায় ৪.৫৫: প্রায় ৭% থেকে ১৩% কম। পাতার ১৪ হাজার অক্ষরে উল্টো ফল দেখেছেন। “Neural না n-gram” প্রশ্নের উত্তর ডেটার পরিমাণ ছাড়া দেওয়া যায় না।
  • Bengio-র মূল দাবি এখানে মেলে। মাত্র ৬ অক্ষরের জানালায় MLP 7-gram interpolation-কে হারিয়েছে। তবে প্রবন্ধের ফল ছিল ইংরেজি শব্দ-স্তরে, আমাদের ফল বাংলা code point-এ; দুটিকে সংখ্যায় মেলানো যাবে না।
  • এই পরীক্ষায় LSTM ছোট Transformer-কে হারিয়েছে। তিন seed-এর কোনোটিতেই পার্থক্য উল্টোয়নি। কিন্তু এটি ছোট ডেটা, একটি ভাগ, প্রায় সমান parameter-এর ফল, আর দুটি model-এর hyperparameter খোঁজা হয়নি; শুধু MLP-এরটি খোঁজা হয়েছে। মাপার সময় LSTM-এর hidden state পুরো আগের লেখা বয়ে আনে, Transformer দেখে ১২৮ অক্ষর। “LSTM ভালো” বলে সাধারণ সিদ্ধান্ত নেওয়া চলে না; আকার বাড়ালে কী হয়, সেটি পর্ব ৫১ (Scaling laws)-এর প্রশ্ন।
  • MLP হয়তো থেমে গেছে আগেই। তিনটি seed-এই সেরা step শেষ step, ১,৫০০; তার আগে learning rate শূন্যের দিকে নেমে আসে। বেশি step দিলে সামান্য বদলাতে পারে, যা আমরা পরীক্ষা করিনি।
  • সময় সবচেয়ে নড়বড়ে সংখ্যা। GPU অন্য কাজের সঙ্গে ভাগ করা ছিল। একই Transformer-এ ৩৩১ থেকে ২,০৬১ সেকেন্ড লেগেছে। আগের কয়েকটি চেষ্টা driver error-এ থেমে যায়, সেগুলো শুরু থেকে আবার চালানো হয়েছে; টেবিলের run-গুলো থামেনি। সময়কে শুধু মাত্রা হিসেবে পড়ুন: গোনা সেকেন্ডের ভগ্নাংশ, neural model মিনিট।

এবার লেখা। সবগুলো একই শুরু “শিয়াল ” থেকে, seed ৪২, τ = ১, আগের অক্ষরগুলোর পরে ২৪০টি অক্ষর তোলা; প্রথম অংশ দেখানো হলো, কোনো সম্পাদনা ছাড়া (সত্যিকারের run):

[3-gram, backoff]
শিয়াল বড়ারপর তখন আনলে নি মি কোথা শুনে, তারতে বড়া খানী তাকে পরনা-াঘারাতজ; গারিণ তো খানবার ক’রে এতগুজন্ত্র ভিতরে তাহে? এসে পাখানে, / ঘ সেখল লো দায় না শিয়ে চ’লে, করে খা …

[5-gram, backoff]
শিয়াল বলল, ‘যদি কাঁদতে বিক্ষতবে আবার বাসা করে বললে, মামা, আর নাম ছিল, ‘ভাই, এমন সময় কাজেই তো আগুনভাই, তোর ভিতরে ছিল। তখনই বাগানের তখনই তাহা যতক্ষণ তিন-চার হাতি চমৎকার ক …

[window MLP, w = 6]
শিয়াল না?’ ঘোঁতো দেখি, আর বাঘা করেন আজ জল খেতে হয়।’ / বুড়ী চলিয়াছে, আমি জহ্মণিতে চলিল, ‘সাবার চারটিকে ডেকে মুখতে পেল যেন চলে একটু বসিল, ‘হিং, ভয়ানকটা কেউ কাল যে তবে …

[LSTM]
শিয়াল নৌকা আসছি; দোঁচাই তো কী, করে নিজেই তো রাজার মিওয়াদারা শেষ করলে?’ / রাজা মিলে সেই চিল আবার চিষট কুঁড়ো আনতে-পার চলে নাচল, একটু বস ফুটিয়ে বললে, ‘টাকা গিয়ে যাচ্ছিল …

[Transformer]
শিয়াল নৌকা আসিলেন, তাহাইতে সেই কাঁধনে কুলেই রাজার মেয়ের পর ক্ষেতে লাগেন। তখন শিয়ালনী বললে, ‘হ্যাঁ হুঁ, কে মারতে; এর দুঃখীরাম করছে আর বলিল, ‘মামা, বটে! এটা আল!’ গুপি বা …

ধাপে ধাপে উদ্ধৃতি খোলা-বন্ধ, চেনা শব্দ (“বললে”, “মামা”), কথোপকথনের ছাঁচ স্পষ্ট হয়। তবে কোনোটিতেই গল্প নেই, আর অর্থহীন শব্দ সব model-এই আছে। একটি করে নমুনা দিয়ে model বিচার করা যায় না, তাই আসল বিচার ওপরের টেবিলে। Sampling-এর কৌশল বদলালে এই লেখা অনেক বদলাবে, যা পর্ব ৫০-এর বিষয়।

৯ · Perplexity-র সীমা — কম সংখ্যা মানেই সত্য নয়

সীমাকেনকী করবেন
Tokenizer বদলালে তুলনা নেইPerplexity “প্রতি token”. BPE-র একটি token-এ কয়েকটি অক্ষর, তাই তার perplexity অনেক বড় দেখাবে, অথচ model খারাপ নাও হতে পারে।একই tokenizer রাখুন; না পারলে পুরো test লেখার মোট NLL-কে মোট অক্ষর দিয়ে ভাগ করুন (bits per character).
Test বদলালে তুলনা নেইপর্ব ৩৬-এ একই MiniGPT গল্পে ৪.৮২, ছাঁচে বানানো রিভিউতে ১.৩৫.একই লেখা, একই অবস্থান; সংখ্যার পাশে ডেটার নাম লিখুন।
Vocabulary ছেঁটে “উন্নতি”রেয়ার শব্দকে এক UNK-এ মেশালে perplexity কমে, কিন্তু কাজ সহজ হয়েছে, model ভালো হয়নি।Vocabulary হুবহু একই রাখুন।
Test ফুটোVal বা test দেখে গোনা বা tokenizer বানালে সংখ্যা কৃত্রিমভাবে কমে।Vocabulary আর খাতা শুধু train থেকে; পুরো নথি ধরে ভাগ।
সত্য মাপে নাObjective শুধু “পরের token কতটা সম্ভাব্য”। মিথ্যা কিন্তু প্রচলিত বাক্যের perplexity কম হতে পারে।কাজের মাপে আলাদা যাচাই (পর্ব ৪৫); Hallucination আসবে পর্ব ৫২-এ।
এক সংখ্যায় অনিশ্চয়তা নেইSeed আর গল্প ভেদে নড়ে। পাতার ছোট টুকরোয় val আর test-এর ফারাকই বড়।কয়েকটি seed, সম্ভব হলে অবস্থানভিত্তিক paired তুলনা (মেশিন লার্নিং কোর্সের পর্ব ২২)।

আরও গভীরে: Perplexity-র একটি নিচের সীমা আছে: লেখার আসল entropy rate-এর চেয়ে কম cross-entropy কোনো model গড়ে পায় না। ইংরেজিতে Shannon (১৯৫১) মানুষের অনুমান পরীক্ষা থেকে অক্ষরপ্রতি মোটামুটি ০.৬–১.৩ bit-এর পরিসর দেন, আর Brown ও সহকর্মীরা (১৯৯২) word trigram দিয়ে ১.৭৫ bit-এর ঊর্ধ্বসীমা হিসাব করেন। পদ্ধতি আর লেখা আলাদা, তাই সংখ্যাগুলো সরাসরি তুলনীয় নয়; আমাদের ২.১৯ bit বাংলার code point-এ, সেটিও এদের পাশে বসানো চলে না। Mikolov ও সহকর্মীরা (২০১০) RNN LM-এর মিশ্রণে একটি backoff model-এর তুলনায় প্রায় ৫০% কম perplexity জানান, আর খরচ হিসেবে training-এর সময়ের কথা বলেন।

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

  • Chain rule আর Markov assumption-এর পার্থক্য? প্রথমটি সবসময় সত্য; দ্বিতীয়টি ইতিহাস ছাঁটার অনুমান।
  • Laplace-এ হরে V কেন? V টি ঘরে ১ করে যোগ হয়েছে; না দিলে যোগফল ১ থাকে না।
  • ৩টি probability ০.৫, ০.২৫, ০.১২৫-এর perplexity? ১/p-এর জ্যামিতিক গড়: ∛(২ × ৪ × ৮) = ৪, মানে ২ bit.
  • n বাড়ালে train ভালো, test খারাপ কেন? Sparsity: লম্বা context-এর ঘর ফাঁকা বা অল্প গোনায় ভরা।
  • Fixed-window LM-এর parameter? V·e + (w·e)·h + h + h·V + V; w বাড়লে সরলরেখায় বাড়ে।

১০ · AI-এর যুগে — language model মাপায় AI-এর সাহায্য

AI assistant n-gram বা char-LSTM-এর কোড সহজেই দেবে, আর সংখ্যাও বেরোবে। ভুলগুলো থাকে হিসাবের নিচে: val বা test থেকে vocabulary বানানো, এলোমেলো টুকরোয় ভাগ, model.eval() বা torch.no_grad() ভুলে dropout-সহ মাপা, loss-এর গড় আর perplexity গুলিয়ে ফেলা, log-এর base না বলা, BPE আর অক্ষরের perplexity পাশাপাশি বসানো, পুরোনো API, বানানো উদ্ধৃতি। একটি সুন্দর sample দেখিয়ে “model বুঝে লিখছে” বলাও সাধারণ ভুল।

AI-কে সংখ্যার সঙ্গে প্রমাণও চাইতে বলুন:

আমি বাংলা লেখায় char-level n-gram আর একটি LSTM-এর perplexity তুলনা করছি। ১. Train, val, test পুরো গল্প ধরে ভাগ করো। Vocabulary আর গোনা শুধু train থেকে। ২. k, d আর λ শুধু val-এ বাছো; test একবারই ছোঁবে। ৩. সব model একই test অবস্থানে মাপো, আর সেই অবস্থানের সংখ্যা ছাপাও। ৪. প্রতিটি ফল nats, bits আর perplexity তিনভাবে দাও; ছোট বাক্যে হাতে মিলিয়ে দেখাও। ৫. প্রতিটি probability table-এ একটি context-এর যোগফল ১ কি না যাচাই করো। ৬. Neural model-এ eval আর no_grad দেখাও, শুরুর loss আর ln V মেলাও, কমপক্ষে ৩টি seed দাও। ৭. যে API তোমার ব্যবহৃত সংস্করণের docs-এ যাচাই করোনি, তা স্পষ্ট বলো। কোনো paper-এর সংখ্যা উৎস ছাড়া দিও না।

AI-এর দেওয়া perplexity টেবিল হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১Chomsky বলেছিলেন Markov-ধরনের model ভাষা ধরতে পারে না। তাহলে n-gram এত দিন টিকে গেল কীভাবে?

১৯৫০-এর দশকে Markov model প্রকৌশল আর ভাষাবিজ্ঞানের কাজে বেশ ব্যবহৃত হতো। Noam Chomsky ১৯৫৬–১৯৫৭ সালের কাজে যুক্তি দেন, সীমিত অবস্থার Markov process মানুষের ব্যাকরণ-জ্ঞানের পূর্ণ model হতে পারে না। তাঁর বিখ্যাত উদাহরণ “Colorless green ideas sleep furiously”: বাক্যটি অর্থহীন, তবু ব্যাকরণে ঠিক; উল্টো ক্রমটি ব্যাকরণেই ভুল। অথচ কোনোটিই আগে দেখা নয়, তাই তাঁর মতে গোনা দিয়ে পার্থক্য ধরা যাবে না। Jurafsky ও Martin লিখেছেন, এই যুক্তিগুলোর পর বহু ভাষাবিজ্ঞানী দশকের পর দশক পরিসংখ্যানভিত্তিক কাজ উপেক্ষা করেন।

যুক্তিটি ভাষাবোধের model হিসেবে এখনো গুরুত্বপূর্ণ। কিন্তু প্রকৌশলের প্রশ্নটি আলাদা ছিল: speech recognition-এ দুটি সম্ভাব্য বাক্যের মধ্যে কোনটি বেশি স্বাভাবিক? ১৯৭০-এর দশকে IBM-এ Jelinek-এর দল আর CMU-তে Baker n-gram দিয়ে এই কাজ করেন, আর ফল কাজে লাগে। পরে Fernando Pereira (২০০০) দেখান, smoothing আর শব্দের শ্রেণি ব্যবহার করলে “গোনা শূন্য” যুক্তিটি আর খাটে না: সংবাদের লেখায় শেখা একটি মডেলে, রিপোর্ট অনুযায়ী, Chomsky-র বাক্যটি উল্টো ক্রমের চেয়ে প্রায় দুই লাখ গুণ বেশি সম্ভাব্য।

আমাদের “ধানের খোসা” উদাহরণে একই শিক্ষা। না-দেখা মানেই অসম্ভব নয়। Add-k সেটি ধরতে পারে না, interpolation আংশিক পারে, embedding আরেকটু পারে। তবে দুই পক্ষের মূল বিতর্ক শেষ হয়নি: কম perplexity কি ভাষার গঠন বোঝার প্রমাণ, নাকি শুধু ভালো অনুমান? এই পর্বের সংখ্যা সে প্রশ্নের উত্তর দেয় না; সংখ্যা শুধু অনুমানের মান মাপে।

মূল উপলব্ধি: একটি model মানুষের ভাষাবোধ ব্যাখ্যা না করেও কোনো কাজে উপকারী হতে পারে; কোন প্রশ্নের উত্তর খুঁজছেন, সেটি আগে ঠিক করতে হয়।

প্র ০২Laplace এত খারাপ হলে পাঠ্যবই এখনো শেখায় কেন? আর Naive Bayes-এ তো ঠিকই চলে।

Add-one-এর ধারণা পুরোনো, Laplace-এর ১৮১২ সালের law of succession পর্যন্ত যায়; Jurafsky ও Martin-এর অধ্যায় অনুযায়ী শূন্য ঘনত্বের সমস্যায় এর প্রকৌশলী প্রয়োগ Jeffreys (১৯৪৮)-এর, আর সমস্যাগুলোর সারসংক্ষেপ আছে Gale ও Church (১৯৯৪)-এ। আমাদের desktop run-এ n = ৪-এ test perplexity Laplace-এ ৯.৬০, k = ০.০৩-এ ৬.২০; n = ৫-এ ১৫.০২ আর ৭.৫৮. কারণটি হিসাবে দেখা যায়: ৮৩টি অক্ষরে context-প্রতি ৮৩টি ঘরে ১ করে যোগ। যে context মাত্র ৩ বার দেখা গেছে, সেখানে আসল গোনা ৩, আর বানানো গোনা ৮৩. Model প্রায় uniform হয়ে যায়।

V যত বড়, ক্ষতি তত বেশি। শব্দ-স্তরের bigram-এ V হতে পারে লক্ষাধিক; তখন অধিকাংশ probability ভরটি না-দেখা ঘরে চলে যায়, দেখা ঘরগুলো ন্যায্য ভাগ পায় না।

তাহলে Naive Bayes-এ কেন চলে? সেখানে কাজ শ্রেণি বাছা, probability-র মান নয়। দুই শ্রেণির স্কোরের তুলনায় দোষগুলো অনেকটা কাটাকাটি হয়। শ্রেণিপ্রতি একটি শব্দ-ঘনত্বের টেবিল, যেখানে প্রতিটি শ্রেণিতে গোনার পরিমাণ বেশি; n-gram-এর মতো লাখ লাখ পাতলা context নেই। Laplace সেখানে ভাঙে না, কারণ মোট গোনার তুলনায় V ছোট।

আর শেখানো হয় কারণ এক লাইনে ধারণাটি ধরে: হরে যোগ, যোগফল ১, শূন্য নেই। সবচেয়ে কাজের অভ্যাসটিও এখান থেকে আসে: k একটি hyperparameter, আর তা val-এ বাছতে হয়। এরপরের backoff আর interpolation একই চিন্তার পরের ধাপ: ভাগ দিতে হবে, কিন্তু যুক্তি দিয়ে।

মূল উপলব্ধি: কোনো সরল কৌশলের মূল্য তার কাজের ওপর নির্ভর করে; probability-র সঠিক মান চাইলে Laplace দুর্বল, শ্রেণি বাছতে অনেক সময় যথেষ্ট।

প্র ০৩Bengio-র ২০০৩ সালের প্রবন্ধে neural LM জিতেছিল। তাহলে সেটি ব্যাপক হতে আরও প্রায় এক দশক লাগল কেন?

প্রবন্ধটি পড়লে কারণ নিজেই চোখে পড়ে। Brown corpus-এ ১১.৮ লাখের কিছু বেশি শব্দ, vocabulary ১৬,৩৮৩; সেখানে test perplexity-র পার্থক্য প্রায় ২৪%, n-gram-গুলোর মধ্যে val-এ সেরাটির সঙ্গে। আরও বড় AP News-এ পার্থক্য প্রায় ৮%. কিন্তু সেই AP News-এর পরীক্ষায় মাত্র ৫ epoch চলেছে, প্রায় তিন সপ্তাহ ধরে, ৪০টি CPU-তে। তাঁরা আলাদা করে parallel training-এর পদ্ধতিও লিখেছেন।

একই সময়ে একটি সুরক্ষিত n-gram খাতা গোনা যেত অনেক দ্রুত, আর বড় ডেটায় সরল গোনার সুবিধা বাড়ে। আমাদের ছোট পরীক্ষাতেও দেখুন: সব order-এর গোনা ০.৪৩ সেকেন্ডে, অথচ neural model-এ GPU-তেও মিনিট লাগে। Mikolov ও সহকর্মীরা ২০১০ সালে RNN LM-এ বড় উন্নতি জানালেও, নিজেরাই training-এর উচ্চ খরচের কথা লেখেন।

পরে কয়েকটি জিনিস একসঙ্গে বদলায়। GPU-তে সস্তা গুণ, ডেটার পরিমাণ, আর ২০১৩ সালের দিকে word2vec-এর মতো (পর্ব ৪১) দ্রুত embedding পদ্ধতি, যার মূলে Bengio-র প্রবন্ধের শেখা feature vector-এর ধারণা অনেকটা আছে। তারপর Transformer (২০১৭), যা লম্বা context-এ সমান্তরাল training সহজ করে।

এর অর্থ এই নয় যে n-gram মরে গেছে। Jurafsky ও Martin-এর অধ্যায়ে SRILM আর KenLM-এর মতো toolkit-এর কথা আছে; হিসাবের খরচ যেখানে সবচেয়ে বড়, যেমন ছোট যন্ত্রে দ্রুত স্কোর, সেখানে গোনা-ভিত্তিক model এখনো বিবেচ্য হতে পারে। আর baseline হিসেবে তো অবশ্যই। “প্রথম ধারণা” আর “প্রথম ব্যবহারযোগ্য” এক জিনিস নয়; ইতিহাসে এই ফাঁক প্রায়ই বছরের পর বছর লম্বা হয়।

মূল উপলব্ধি: একটি পদ্ধতির ব্যাপক হওয়া শুধু সঠিকতার ওপর নয়, কম্পিউট, ডেটা আর টুলের দামের ওপরও নির্ভর করে।

প্র ০৪দুই model-এর perplexity সমান প্রায়। তাহলে আপনি কোনটি বাজারঘরের সার্ভারে চালাবেন?

Perplexity একটি মাত্র সংখ্যা, আর Jurafsky ও Martin সতর্ক করেন: perplexity-র উন্নতি কোনো আসল কাজের উন্নতির নিশ্চয়তা দেয় না; সম্ভব হলে সেই কাজেই যাচাই করতে হয়। তাই প্রশ্নটি আগে কাজ দিয়ে শুরু হবে। ধরুন বাজারঘরের অ্যাপে ক্রেতার রিভিউ লেখার সময় পরের শব্দ দেখানো হবে।

প্রথম মাপ কাজের: দেখানো পরের শব্দ কতবার ক্রেতা নেন, কত কীস্ট্রোক বাঁচে। দুই model-এর গড় probability সমান হলেও একটি হয়তো ঘন ঘন শব্দে ভালো, আরেকটি বিরল পণ্যের নামে। গড় সেই পার্থক্য লুকিয়ে রাখে।

দ্বিতীয় মাপ খরচের: ফোনে কত মেমরি, একটি অনুমানে কত মিলিসেকেন্ড। ৫-gram খাতা লক্ষাধিক স্ট্রিং রাখে, আর MLP-তে কয়েক লাখ float. LSTM-এ hidden state বয়ে চলে, প্রতিটি অক্ষরে একবার হিসাব। Transformer-এ জানালা সরালে KV cache দরকার (পর্ব ৩৭)। পরের ডেটা যোগ করা কতটা সহজ, সেটিও দেখার বিষয়: খাতায় নতুন গোনা যোগ হয় সেকেন্ডে, neural model-এ নতুন training.

তৃতীয় মাপ ক্ষতির: কোন model আপত্তিকর লেখা, ক্রেতার ব্যক্তিগত তথ্য বা মিথ্যা দাবি সামনে আনে? পর্ব ৩৬-এ দেখেছেন, val-এ থামানো না হলে একই নকশা train লেখা হুবহু বলে দেয়। ব্যক্তিগত তথ্যে শেখালে সেটি ঝুঁকি। শেষে, দুই model-এর পার্থক্য seed-এর নড়াচড়ার চেয়ে বড় কি না, সেটিও মাপতে হয়। না হলে “সমান” ধরে সস্তাটি বাছাই যুক্তিযুক্ত, আর সেটি দলের সবার সামনে লিখে রাখাও জরুরি।

মূল উপলব্ধি: Perplexity model বাছাইয়ের একটি ছাঁকনি; চূড়ান্ত সিদ্ধান্ত আসে কাজের মাপ, খরচ আর ঝুঁকি থেকে।

প্র ০৫আমাদের পরীক্ষায় LSTM ছোট Transformer-কে হারাল। তাহলে কি LLM-এর Transformer-এ চলে যাওয়াটা ভুল ছিল?

না, আর এই ভুল সিদ্ধান্তটি থেকে বাঁচাই প্রশ্নটির উদ্দেশ্য। আগে দেখুন আমরা কী মেপেছি: প্রায় দুই লাখ অক্ষর, ৮–৯ লাখ parameter, একটি ভাগ, তিনটি seed, আর দুই model-এ কোনো hyperparameter খোঁজা ছাড়া। এটি একটি ছোট অঞ্চলের একটি বিন্দু। সেখানে LSTM-এর পক্ষে কিছু সুবিধাও আছে: অক্ষর-স্তরের লেখায় শেষের কয়েকটি অক্ষরই বেশিরভাগ তথ্য দেয়, recurrent ধাঁচ সেই স্থানীয় নকশা সস্তায় ধরে, আর মাপার সময় hidden state ১২৮ অক্ষরের সীমায় আটকে নেই।

Transformer-এর আসল যুক্তি অন্য জায়গায়। Vaswani ও সহকর্মীদের ২০১৭ সালের প্রবন্ধে গুরুত্ব পায় সমান্তরাল হিসাব: RNN-কে ধাপে ধাপে চলতে হয়, attention পুরো ক্রম একসঙ্গে দেখে (পর্ব ৩৩)। Layer আর ডেটা বহু গুণ বাড়ালে এই সমান্তরালতা training-কে বাস্তবসম্মত রাখে। দূরের token-এর সঙ্গে সরাসরি সংযোগও সেখানে বড় সুবিধা, যা আমাদের ছোট, মূলত স্থানীয় অক্ষরের অনুমানে খুব কম কাজে লাগে। আমাদের টেবিলে মাপা হয়নি এমন প্রশ্নই আসলে সিদ্ধান্ত নিয়েছিল: বড় আকারে কোনটি শেখানো যায়।

তবে উল্টো অহংকারও ভুল। RNN বা অন্য নকশা নিয়ে গবেষণা বন্ধ হয়নি, আর ছোট যন্ত্রে বা স্ট্রিমিং কাজে হালকা recurrent model এখনো যুক্তিসংগত বিকল্প হতে পারে। আমাদের পরীক্ষা থেকে নেওয়ার মতো কথা হলো পদ্ধতিটি: একই ভাগ, একই test অবস্থান, কয়েকটি seed, আর সীমা লিখে রাখা। বড় model-এর দাবি যাচাই করতে হলে সেই আকারে মাপতে হয়। একই model বড় করলে loss কীভাবে বদলায়, তা পর্ব ৫১ (Scaling laws)-এ আসবে।

মূল উপলব্ধি: একটি ছোট পরীক্ষার ফল শুধু সেই আকার আর সেই ডেটার কথা বলে; নকশা বাছাইয়ে সিদ্ধান্ত নিতে হয় যে আকারে কাজ হবে, সেই আকারের প্রমাণ দেখে।

অনুশীলন

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

  1. পূর্ণ numerical: চারটি রিভিউ: “চা গরম”, “চা ঠান্ডা”, “চা গরম”, “কফি গরম”. ⟨s⟩ আর ⟨/s⟩ জুড়ে V = ৫ ধরে “কফি ঠান্ডা”-র (ক) MLE probability, (খ) Laplace probability, NLL (nats), bits আর perplexity, (গ) k = ০.৫-এ perplexity বের করুন।

    গোনা: ⟨s⟩→চা ৩, ⟨s⟩→কফি ১, চা→গরম ২, চা→ঠান্ডা ১, কফি→গরম ১, গরম→⟨/s⟩ ৩, ঠান্ডা→⟨/s⟩ ১. (ক) ১/৪ × ০/১ × ১/১ = ০. (খ) (১+১)/(৪+৫) = ২/৯; (০+১)/(১+৫) = ১/৬; (১+১)/(১+৫) = ১/৩. গুণফল ১/৮১ ≈ ০.০১২৩. NLL = ln ৮১ / ৩ ≈ ১.৪৬৫ nats ≈ ২.১১৩ bits; perplexity = ∛৮১ ≈ ৪.৩৩. (গ) ১.৫/৬.৫, ০.৫/৩.৫, ১.৫/৩.৫; ১/p-এর গুণফল ≈ ৭০.৭৮, perplexity ≈ ৪.১৪. এই বাক্যে k কমালে সুবিধা হলো, তবে k-এর আসল বাছাই val-এর ওপর।

  2. সমালোচনা ও নকশা: একজন লিখেছেন: “আমাদের BPE-Transformer-এর perplexity ৪০, char-LSTM-এর ৫, তাই LSTM আট গুণ ভালো।” Vocabulary পুরো ডেটা থেকে, ভাগ এলোমেলো ১০% জানালায়, মাপ dropout চালু রেখে, এক seed. কী কী ভুল? ঠিক পরীক্ষাটি নকশা করুন।

    (১) Token আলাদা, তাই সরাসরি তুলনা অর্থহীন। মোট NLL-কে মোট অক্ষর দিয়ে ভাগ করে bits per character-এ আনতে হবে। (২) Vocabulary-তে test ঢুকে গেছে। (৩) এলোমেলো জানালায় একই গল্প দুই পাশে, মুখস্থ পুরস্কার পায়। (৪) Dropout চালু থাকলে মাপ ভুল আর এলোমেলো। (৫) এক seed. ঠিক নকশা: পুরো নথি ধরে ভাগ; vocabulary আর tokenizer শুধু train থেকে; model.eval(); একই লেখায় bits per character; কয়েকটি seed; একটি interpolated n-gram baseline; শেষে কাজের মাপ।

  3. আউটপুট অনুমান: চালানোর আগে দুটি সারি লিখুন।
    import math
    c = {"ক": 3, "খ": 1}   # একটি context-এর পরে যা দেখা গেছে
    V, k = 4, 1
    t = sum(c.values())
    p = lambda x: (c.get(x, 0) + k) / (t + k * V)
    print(p("ক"), p("গ"))
    print(round(math.exp(-(math.log(p("ক")) + math.log(p("খ"))) / 2), 3))

    প্রথম সারি 0.5 0.125: হর ৪ + ৪ = ৮, “ক” পায় ৪/৮, না-দেখা “গ” ১/৮. দ্বিতীয় সারি 2.828: p(খ) = ২/৮, তাই ১/p হলো ২ আর ৪, জ্যামিতিক গড় √৮ ≈ ২.৮২৮.

  4. কোড: পাতার window_lm.py-তে w = ৪-এর বদলে w = ৮ করে একই কোড আবার চালান। কী কী সংখ্যা আগেই হাতে বলা যায়? Test perplexity বাড়বে না কমবে, অনুমান করে তারপর মিলিয়ে দেখুন।

    হাতে বলা যায় parameter: ১,০৮৮ + (৮ × ১৬ × ৯৬ = ১২,২৮৮) + ৯৬ + ৬,৫২৮ + ৬৮ = ২০,০৬৮. Gradient check-এর মিল থাকা উচিত, কারণ কোড w-এর ওপর সাধারণভাবে লেখা; শুরুর loss ln V-এর কাছে থাকবে। Perplexity অনুমান করা কঠিন: ইতিহাস বেশি, কিন্তু ডেটা অল্প, তাই desktop-এর লম্বা জানালার মতো খারাপ হওয়ার সম্ভাবনা বেশি। এটি অনুমান; এক seed দেখে সিদ্ধান্ত নেবেন না, আর START-এর কারণে test অবস্থান বদলেছে কি না খেয়াল করুন, কারণ windows() প্রথম w অক্ষর বাদ দেয়।

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

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

ক্রমকয়েকটি language model ন্যায্যভাবে তুলনা করার ধাপগুলো সঠিক ক্রমে সাজান।

  1. পুরো নথি ধরে train, val আর test ভাগ করা
  2. শুধু train থেকে vocabulary আর গোনার খাতা বানানো
  3. Val-এ k, λ, dropout আর সেরা চেকপয়েন্ট বাছা
  4. একই test অবস্থানে একবার NLL মাপা
  5. Perplexity আর bits, কয়েকটি seed-সহ, parameter আর সময়ের পাশে লেখা

P(a, b, c) = P(a) P(b | a) P(c | a, b) আর P(c | a, b) ≈ P(c | b): এই দুটি লেখার মধ্যে কোনটি অনুমান?

  • শুধু প্রথমটি
  • শুধু দ্বিতীয়টি
  • দুটিই
  • কোনোটিই নয়

একটি character bigram খাতায় “ক”-এর পরে মোট ৪০ বার কিছু এসেছে, তার মধ্যে ১০ বার “া”. P(া | ক)-এর MLE কত?

  • ২.৫
  • ০.৪
  • ০.২৫
  • ০.১০

V = ১০, c(x) = ৪, c(x y) = ০. Laplace smoothing-এ P(y | x) কত?

  • ০
  • ১/৪
  • ১/১০
  • ১/১৪

একটি model তিনটি সঠিক token-কে probability দিয়েছে ০.৫, ০.২৫ আর ০.১২৫. Perplexity কত?

  • ৪
  • ২
  • ৪.৬৭
  • ৬৪

Model A character token-এ perplexity ৫, Model B BPE token-এ perplexity ৩০; একই test লেখা। কোন সিদ্ধান্ত ঠিক?

  • A নিশ্চিতভাবে ভালো
  • এখনো বলা যায় না; bits per character-এ আনতে হবে
  • B নিশ্চিতভাবে ভালো
  • দুটি সমান ভালো

add-k n-gram-এ n বাড়ালে train-এর perplexity কমতে থাকে, কিন্তু একসময় test-এর perplexity বাড়ে। মূল কারণ কী?

  • Vocabulary ছোট হয়ে যায়
  • Log-এর base বদলে যায়
  • Data sparsity: লম্বা n-gram-এর বড় অংশ train-এ আসেনি
  • Learning rate বেশি হয়ে যায়

শব্দকোষ

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

মনে রাখুন

  1. ভিত্তি: chain rule সবসময় সত্য; Markov assumption ইতিহাস ছাঁটে। n-gram MLE = c(x y) / c(x).
  2. Smoothing: শূন্য ঠেকায়। Laplace হরে V, add-k হরে kV; k বাছা হয় val-এ। Backoff আর interpolation সাধারণত ভালো ফল দেয়।
  3. মাপ: perplexity = eNLL = ২bits = ১/p-এর জ্যামিতিক গড়। সবসময় held-out লেখায়।
  4. Sparsity: n বাড়লে train ভালো হয়, test একসময় খারাপ। Embedding আর ওজন ভাগ করে নেওয়া এর এক উত্তর, তবে ডেটা লাগে।
  5. সীমা: একই tokenizer আর test ছাড়া তুলনা চলে না। কম perplexity সত্য বা বোঝার প্রমাণ নয়।

পরের পর্বে: পর্ব ৪৭ (Pretraining objectives)-এ দেখবেন, পরের token অনুমান ছাড়া আর কোন কোন লক্ষ্য দিয়ে label ছাড়া লেখা থেকে model শেখানো যায়: masked language modeling, causal next-token আর span corruption.

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

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