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

Seq2seq আর encoder–decoder — অনুবাদ আর teacher forcing

Seq2seq and Encoder–Decoder — Context Vector, Teacher Forcing, Greedy Decoding, Masked Loss, CER and the Fixed-Context Bottleneck

Taught in Bangla, this lesson builds the encoder-decoder: an encoder that compresses the input into a context vector and a decoder that writes the output step by step with start and end tokens and greedy decoding. It explains teacher forcing and exposure to its own mistakes, shifted targets with ignore_index, and exact-match versus character error rate. Real desktop runs convert dates under four teacher-forcing settings and measure how a single fixed context breaks on longer inputs.

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

  • Encoder, context আর decoder-এর সমীকরণ আর প্রতিটি ধাপের array shape
  • Start আর end token, আর greedy decoding হাতে ধরে অনুসরণ করা
  • Teacher forcing, free-running আর scheduled sampling-এর পার্থক্য
  • Masked cross-entropy, আর exact-match ও CER শূন্য থেকে লেখা
  • PyTorch-এ একটি GRU seq2seq চালিয়ে input-এর দৈর্ঘ্যের সঙ্গে ভাঙন মাপা

আগের পর্বগুলোতে (পর্ব ২৭ Sequence ডেটা, পর্ব ২৮ Recurrent neural network, পর্ব ২৯ LSTM আর GRU) একটি sequence পড়ে একটি লেবেল, বা প্রতিটি token-এর পাশে একটি করে লেবেল দেওয়ার কথা এসেছে। এবার কাজ আলাদা: পুরো একটি sequence পড়ে অন্য দৈর্ঘ্যের আরেকটি sequence লেখা। Padding আর attention mask এসেছে পর্ব ১১-এ, ignore_index পর্ব ১৩-এ, gradient clipping পর্ব ১৭-এ; এখানে শুধু সেগুলো ব্যবহার করব।

১ · এক sequence থেকে আরেক sequence — সমস্যাটি কেন আলাদা

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

বাজারঘরের একটি ছোট সমস্যা নিই। পুরোনো অর্ডারের খাতায় তারিখ লেখা আছে নানা ভাবে: “১২ মার্চ ২০২৫”, “মার্চ ১২, ২০২৫” বা “১২/০৩/২০২৫”. আমাদের চাই একটিই রূপ: 2025-03-12. প্রতিটি input অক্ষরের পাশে একটি output অক্ষর বসানো এখানে সম্ভব নয়। প্রথম কারণ, দৈর্ঘ্য আলাদা। দ্বিতীয় কারণ, ক্রম উল্টে যায়: input-এর শুরুর দিন output-এর শেষে যায়। তাই আগে সবটা পড়া দরকার, তারপর লেখা।

এমন কাজকে বলে seq2seq বা sequence-to-sequence. অনুবাদ, প্রশ্নের উত্তর, সারাংশ, বাংলিশ থেকে বাংলা লিপ্যন্তর, এসবই এই ধাঁচের। সমাধানের নকশাটির নাম encoder–decoder.

ইতিহাস: ২০১৩–২০১৪ সালে কয়েকটি দল প্রায় একই সময়ে এই পথে এগোয়। Kalchbrenner ও Blunsom (২০১৩) একটি continuous translation model দেখান। Cho ও সহকর্মীরা (২০১৪) RNN Encoder–Decoder প্রস্তাব করেন, সেখানে এটি মূলত প্রচলিত statistical অনুবাদ ব্যবস্থায় phrase-এর স্কোর দিতে ব্যবহৃত হয়; GRU-এর মতো unit-টিও সেই প্রবন্ধে আসে। Sutskever, Vinyals ও Le (২০১৪) একা multilayer LSTM দিয়ে ইংরেজি থেকে ফরাসি অনুবাদ করে দেখান। “কে প্রথম”, এই প্রশ্নের একক উত্তর দেওয়া কঠিন।

২ · Encoder, context আর decoder — সমীকরণ আর shape

Encoder একটি RNN (এখানে পরে GRU), যা input অক্ষরগুলো একে একে পড়ে তার hidden state হালনাগাদ করে। শেষ state-টিই context vector. Decoder আরেকটি RNN, যা ওই context দিয়ে শুরু করে একটি করে token লেখে।

ht = f(ht−1, xt)  ·  c = hT  ·  s0 = c
st = g(st−1, yt−1)  ·  p(yt) = softmax(W st)

এখানে xt হলো t-তম input অক্ষরের embedding, ht encoder-এর state, T input-এর আসল দৈর্ঘ্য, c context, st decoder-এর state, yt−1 আগের output token-এর embedding, আর W একটি linear layer, যা state-কে vocabulary-র প্রতিটি token-এর logits-এ নেয়। Encoder আর decoder-এর parameter আলাদা, vocabulary-ও আলাদা হতে পারে। এই নকশায় c শুধু decoder-এর শুরুর state; Cho-র নকশায় c প্রতিটি ধাপেও দেওয়া হতো।

Encoder–decoder — সব তথ্য একটি context vector দিয়ে পার হয়

Training-এ decoder-এর ইনপুট খাতা থেকে আসে (teacher forcing); ব্যবহারের সময় আসে নিজের আগের output থেকে। ছবিতে অংশগুলো টুকরো করে দেখানো, আসল model-এ token হলো একটি একটি অক্ষর।

Encoder–decoder with a single fixed-size context vector Encoder Decoder Context ১২মার্চ২০২৫ h₁h₂h₃ c s₁s₂s₃s₄ 20250312</s> <s>20250312 input: দৈর্ঘ্য T যত খুশি আকার স্থির (B, H) ইনপুট: training-এ খাতা, ব্যবহারে নিজের output

এবার shape দেখুন। নিচের কোডে দুটি উদাহরণের একটি batch, ছোট একটি vocabulary, এলোমেলো weight. Model এখনো কিছু শেখেনি; আমরা শুধু দেখছি কোন array কোন আকারে কোথায় যায়। Cell হিসেবে সরল RNN (পর্ব ২৮), NumPy-তে।

import numpy as np
rng = np.random.default_rng(42)
PAD, BOS, EOS = 0, 1, 2
Vs, Vt, E, H = 6, 8, 4, 6                 # দুই vocab, embedding 4, hidden 6
src = np.array([[1, 2, 3], [4, 5, 0]])    # "১২৩" আর "৪৫" + pad
lens = np.array([3, 2])
tgt = np.array([[1, 3, 4, 5, 2],          # <s> 1 2 3 </s>
                [1, 6, 7, 2, 0]])         # <s> 4 5 </s> <pad>
Es, Et = rng.normal(size=(Vs, E)), rng.normal(size=(Vt, E))
Wh, Wx = rng.normal(size=(H, H)) * 0.3, rng.normal(size=(E, H)) * 0.3
Dh, Dx = rng.normal(size=(H, H)) * 0.3, rng.normal(size=(E, H)) * 0.3
Out = rng.normal(size=(H, Vt)) * 0.1
X = Es[src]; h = np.zeros((2, H))
print("src", src.shape, "→ embedded", X.shape, "(B, T_src, E)")
for t in range(src.shape[1]):             # encoder: বাঁ থেকে ডানে
    new = np.tanh(h @ Wh + X[:, t] @ Wx)
    h = np.where((t < lens)[:, None], new, h)   # pad-এ state থামে
print("context h", h.shape, "(B, H) — input যত লম্বাই হোক")
dec_in, gold = tgt[:, :-1], tgt[:, 1:]    # এক ঘর সরানো
print("dec_in", dec_in.shape, "gold", gold.shape)
logits = []
for t in range(dec_in.shape[1]):          # teacher forcing: ইনপুট খাতা থেকে
    h = np.tanh(h @ Dh + Et[dec_in[:, t]] @ Dx)
    logits.append(h @ Out)
logits = np.stack(logits, 1)
print("logits", logits.shape, "(B, T_tgt-1, V_tgt)")
z = logits - logits.max(-1, keepdims=True)
logp = z - np.log(np.exp(z).sum(-1, keepdims=True))
ce = -np.take_along_axis(logp, gold[..., None], -1)[..., 0]
mask = gold != PAD
print("mask:\n", mask.astype(int))
print(f"masked loss {ce[mask].mean():.4f} | সব ঘর {ce.mean():.4f} | ln 8 = {np.log(8):.4f}")

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

src (2, 3) → embedded (2, 3, 4) (B, T_src, E)
context h (2, 6) (B, H) — input যত লম্বাই হোক
dec_in (2, 4) gold (2, 4)
logits (2, 4, 8) (B, T_tgt-1, V_tgt)
mask:
 [[1 1 1 1]
 [1 1 1 0]]
masked loss 2.1202 | সব ঘর 2.1077 | ln 8 = 2.0794

তিনটি জিনিস দেখুন। এক, context-এর আকার (2, 6): input-এর দৈর্ঘ্য T এই আকারে কোথাও নেই। দুই, ছোট উদাহরণ “৪৫”-এর state তৃতীয় ধাপে থেমে থাকে, np.where-এর জন্য; PyTorch-এ একই কাজ করে PackedSequence. তিন, না-শেখা model-এর loss ২.১২, যা ৮টি token-এর সমান সম্ভাবনার ln ৮ ≈ ২.০৮-এর কাছে। প্রথম batch-এর এই পরীক্ষাটি পর্ব ১৮-এর পরিচিত অভ্যাস।

৩ · Start, end আর greedy decoding — model লেখে কীভাবে

Decoder প্রথম ধাপে কী পড়বে? তার জন্য একটি বিশেষ token: <s>, মানে “লেখা শুরু করুন”। আর কখন থামবে? আরেকটি token: </s>. Model এটিকেও অন্য অক্ষরের মতো শেখে, তাই output-এর দৈর্ঘ্য model নিজেই ঠিক করে। সাবধানতা হিসেবে একটি max_len-ও রাখা হয়, যাতে না-শেখা model চিরকাল না চলে।

সবচেয়ে সরল লেখার নিয়ম হলো greedy decoding: প্রতিটি ধাপে সবচেয়ে বড় logit-এর token নিন, সেটিই পরের ধাপের ইনপুট করুন, </s> এলে থামুন। AI-এর ভিত্তি কোর্সের পর্ব ৭০-এ LLM-এর পরের token বাছাইয়ের যে ধারণা দেখেছেন, এটি তারই সরল রূপ।

নিচের decoder-এর সব weight আমরা হাতে বসিয়েছি, তাই প্রতিটি সংখ্যার কারণ বলা যায়। State h-এ প্রতিটি token-এর একটি ঘর আছে; প্রতি ধাপে পুরোনো মান অর্ধেক হয় আর আগের token-এর ঘরে ১ যোগ হয়। শেষের একটি ঘর context, যা বদলায় না। Context +১ হলে “1” আগে, −১ হলে “2” আগে।

import numpy as np
np.set_printoptions(precision=2, suppress=True, linewidth=64)
V = ["<s>", "</s>", "0", "3", "-", "1", "2"]          # ৭টি token
ID = {t: i for i, t in enumerate(V)}
N = len(V)
O = np.zeros((N, N + 1))                              # logits = O @ h; শেষ ঘর context
for cur, nxt in [("<s>", "0"), ("0", "3"), ("3", "-"), ("-", "1"), ("-", "2"),
                 ("1", "2"), ("2", "1")]:
    O[ID[nxt], ID[cur]] = 4.0                         # cur-এর পরে nxt-এর পছন্দ
O[ID["1"], ID["1"]] = O[ID["2"], ID["2"]] = -4.0      # একই অঙ্ক আবার নয়
O[ID["</s>"], ID["1"]] = O[ID["</s>"], ID["2"]] = 3.5 # দুই অঙ্ক হলে থামার টান
O[ID["1"], N] = 1.0; O[ID["2"], N] = -1.0             # context +1: 1 আগে; −1: 2 আগে

def step(h, tok):
    new = 0.5 * h                                     # পুরোনো স্মৃতি অর্ধেক
    new[N] = h[N]                                     # context ঘর অটুট
    new[ID[tok]] += 1.0                               # আগের token যোগ
    return new, O @ new

def greedy(ctx, max_len=8, show=False):
    h = np.zeros(N + 1); h[N] = ctx                   # encoder-এর context
    tok, out = "<s>", []
    for t in range(1, max_len + 1):
        h, logits = step(h, tok)
        tok = V[int(logits.argmax())]
        if show: print(f"t={t} logits {logits} → {tok}")
        if tok == "</s>": break
        out.append(tok)
    return "".join(out)

print("token:", V)
print("context = +1"); print("output:", greedy(+1.0, show=True))
print("context = −1 → output:", greedy(-1.0))

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

token: ['<s>', '</s>', '0', '3', '-', '1', '2']
context = +1
t=1 logits [ 0.  0.  4.  0.  0.  1. -1.] → 0
t=2 logits [ 0.  0.  2.  4.  0.  1. -1.] → 3
t=3 logits [ 0.  0.  1.  2.  4.  1. -1.] → -
t=4 logits [0.  0.  0.5 1.  2.  5.  3. ] → 1
t=5 logits [ 0.    3.5   0.25  0.5   1.   -1.    5.  ] → 2
t=6 logits [ 0.    5.25  0.12  0.25  0.5   4.   -2.  ] → </s>
output: 03-12
context = −1 → output: 03-21

t = ৪-এ “1”-এর logit ৫ আর “2”-এর ৩: দুজনেই “-”-এর পরে আসতে পারে, ফয়সালা করে শুধু context-এর ±১. একই decoder, একই weight, কিন্তু context বদলালে output “03-12” থেকে “03-21” হয়ে যায়। Input সম্পর্কে decoder-এর জানার রাস্তা এই একটিই। t = ৬-এ দুই অঙ্কের স্মৃতি মিলে </s>-এর logit ৩.৫ × ১.৫ = ৫.২৫ হয়, তাই থামে।

Greedy সবসময় সেরা sequence দেয় না। ধরুন প্রথম ধাপে “ক”-এর probability ০.৬, “খ”-এর ০.৪. “ক”-এর পরে সেরা token-এর ০.৫, “খ”-এর পরে ০.৯. Greedy নেয় “ক”, যার মোট ০.৬ × ০.৫ = ০.৩০; অথচ “খ”-এর পথে ০.৪ × ০.৯ = ০.৩৬. কয়েকটি আংশিক উত্তর একসঙ্গে ধরে রাখলে এই সমস্যা কমে, তারই নাম beam search; তার বিস্তার পর্ব ৫০ (Decoding — greedy, beam search, temperature, top-k আর top-p)-এ।

৪ · Teacher forcing — খাতা দেখে শেখা, নিজের ওপর চলা

শিশুকে নামতা শেখাচ্ছেন। সে বলল “তিন দুগুণে সাত”. আপনি যদি থামিয়ে ঠিক করে দেন, “না, ছয়; এবার তিন তিনে?”, পরের ধাপটি সে ঠিক জায়গা থেকেই শুরু করে। আর যদি না থামান, তার ভুল উত্তরের ওপরই পরের হিসাব চলে। প্রথমটি teacher forcing, দ্বিতীয়টি free-running.

Teacher forcing-এ (Williams ও Zipser-এর ১৯৮৯ সালের কাজে এই নামে আলোচিত) training-এর প্রতিটি ধাপে decoder-এর ইনপুট হয় আগের ধাপের সঠিক token, model যা-ই অনুমান করুক। Loss হিসাব হয় সেই ধাপের অনুমানের ওপর, যথারীতি। একটি ধাপ হাতে দেখুন।

s = tanh(0.5·sআগের + e), z = O·s (৪ ঘর দশমিক)
ধাপ
হিসাব
  1. ইনপুট. আগের state আর আগের ধাপের সঠিক token “2”-এর embedding. Model আগের ধাপে কী বলেছিল, এখানে তা আসে না।
  2. নতুন state. দুটি মিলিয়ে tanh: s = [০.৮৪৮৩, −০.২৪৪৯].
  3. Logits. তিন token-এর স্কোর: </s> ০.২৪৪৯, “2” ১.৬৯৬৬, “5” ০.৬০৩৪.
  4. Softmax. প্রতিটির ez নিয়ে ৮.৫৬১০ দিয়ে ভাগ। Model “2”-কে ০.৬৩৭২ দিচ্ছে, মানে সে ভুল করছে।
  5. Loss. সঠিক token “5”-এর probability ০.২১৩৬, তাই cross-entropy ১.৫৪৩৮. এই ভুলের শাস্তি এখানেই।
  6. পরের ইনপুট. ভুল ধরা পড়লেও পরের ধাপে যায় সঠিক “5”. তাই পরের ধাপের loss এই ভুলের দ্বারা দূষিত হয় না। Free-running হলে যেত “2”.

সুবিধা স্পষ্ট: প্রথম দিকে model প্রায় সবই ভুল করে; নিজের ভুলের ওপর চললে পরের ধাপগুলোর লক্ষ্য অর্থহীন হয়ে পড়ে। অসুবিধাও স্পষ্ট: training-এ decoder সবসময় সঠিক ইতিহাস দেখে, অথচ ব্যবহারের সময় দেখে নিজের ইতিহাস। একে কখনো exposure bias বলা হয়। পরের কোডে আগের decoder-এর একটি weight ইচ্ছা করে ভুল করে দেখি, দুই পদ্ধতিতে কী হয়। Free-running মানে পরের ইনপুট নিজের অনুমান।

O[ID["1"], ID["0"]] = 5.0                  # একটি ভুল ওজন: 0-এর পরে 1 বেশি পছন্দ
gold = ["0", "3", "-", "1", "2", "</s>"]

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

def run(forced):
    h = np.zeros(N + 1); h[N] = 1.0
    tok, picks, ces = "<s>", [], []
    for t, g in enumerate(gold):
        h, logits = step(h, tok)
        pick = V[int(logits.argmax())]
        picks.append(pick)
        ces.append(-np.log(softmax(logits)[ID[g]]))
        tok = g if forced else pick            # পরের ইনপুট: খাতা, না নিজের
    return picks, ces

tf, tf_ce = run(True)
fr, fr_ce = run(False)
print("t  gold  | TF: pick  CE    | free: pick  CE")
for t, g in enumerate(gold):
    print(f"{t+1}  {g:5} | {tf[t]:8} {tf_ce[t]:4.2f}  | {fr[t]:10} {fr_ce[t]:5.2f}")
wrong = lambda p: sum(a != b for a, b in zip(p, gold))
print(f"ভুল ঘর: TF {wrong(tf)}/6, free {wrong(fr)}/6")
print(f"গড় CE: TF {np.mean(tf_ce):.3f}, free {np.mean(fr_ce):.3f}")

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

t  gold  | TF: pick  CE    | free: pick  CE
1  0     | 0        0.12  | 0           0.12
2  3     | 1        2.15  | 1           2.15
3  -     | -        0.61  | </s>        4.19
4  1     | 1        0.06  | </s>        2.53
5  2     | 2        0.24  | </s>        2.32
6  </s>  | </s>     0.35  | 1           1.74
ভুল ঘর: TF 1/6, free 5/6
গড় CE: TF 0.589, free 2.176

একটিমাত্র ভুল weight, অথচ দুই পদ্ধতির ফল কত আলাদা। Teacher forcing-এ ভুল ধরা পড়ে শুধু t = ২-এ। তাই শুধু teacher-forced loss দেখে model-কে “প্রায় ঠিক” মনে হতে পারে। আসল ব্যবহারে এই model প্রায় কিছুই ঠিক লেখে না। নিচের ধাপগুলোতে দেখুন ভুলটি কীভাবে গড়ায়।

প্রতিটি ধাপে দুই পদ্ধতির ইনপুট আর পছন্দ
t
সঠিক
TF ইনপুট → পছন্দ
TF-এর CE
Free ইনপুট → পছন্দ
Free-এর CE
  1. t = ১. দুই পদ্ধতিই <s> পড়ে “0” বলে, ঠিক। পার্থক্য এখনো শুরু হয়নি।
  2. t = ২, ভুলের জন্ম. ভুল weight-এর জন্য দুজনেই “3”-এর বদলে “1” বলে। CE ২.১৫, দুজনের সমান।
  3. t = ৩, পথ আলাদা. TF পড়ে সঠিক “3”, আর ঠিক “-” বলে। Free পড়ে নিজের “1”, যার পরে model-এর থামার টান আছে, তাই </s> বলে। CE ৪.১৯.
  4. t = ৪. TF আবার ঠিক পথে। Free-এর ইতিহাস এখন training-এ কখনো না-দেখা: </s>-এর পরে আবার লেখা।
  5. t = ৫. Free একই জায়গায় আটকে। TF-এর CE মাত্র ০.২৪, free-এর ২.৩২.
  6. t = ৬. মোট ভুল ঘর: TF ১টি, free ৫টি। গড় CE ০.৫৮৯ বনাম ২.১৭৬. আসল ব্যবহারে অবশ্য free-running প্রথম </s>-এই থামত, output হতো শুধু “01”.

মাঝের পথ হলো teacher forcing ratio: প্রতিটি ধাপে সম্ভাবনা r-এ সঠিক token, নইলে নিজের অনুমান। r-কে training-জুড়ে ধীরে ধীরে কমালে তাকে বলে scheduled sampling (Bengio ও সহকর্মীরা, ২০১৫)। নিজের অনুমান নেওয়ার সময় সাধারণত argmax-এর ভেতর দিয়ে gradient যায় না; তাই পরের ধাপের ভুলের দায় আগের ধাপের পছন্দ পর্যন্ত পৌঁছায় না, শুধু hidden state ধরে যায়।

৫ · এক ঘর সরানো লক্ষ্য আর masked loss

একটি লক্ষ্য sequence থেকে decoder-এর ইনপুট আর লক্ষ্য দুটোই বের হয়, এক ঘর সরিয়ে:

tgt     : <s>  2  0  2  5  </s>  <pad>
dec_in  : <s>  2  0  2  5  </s>           tgt[:, :-1]
gold    :  2   0  2  5  </s>  <pad>        tgt[:, 1:]

লক্ষ্য করুন: <s> কখনো gold-এ নেই, </s> আছে। Gold-এ </s> না থাকলে model থামতে শেখে না। আর <pad> ঘরগুলো loss-এ ঢোকা চলবে না, কারণ সেগুলো ডেটা নয়, batch মেলানোর ভরাট। পর্ব ১৩-এর ignore_index এই কাজই করে। Logits-কে (B·T, V) আর লক্ষ্যকে (B·T) আকারে আনতে হয়:

def loss_fn(logits, tgt):          # logits: (B, T-1, V), tgt: (B, T)
    return F.cross_entropy(logits.reshape(-1, logits.size(-1)),
                           tgt[:, 1:].reshape(-1), ignore_index=PAD)

না দিলে কী হয়, মেপে দেখেছি। অর্ডার নম্বরের একটি কাজে (§৮-এ বিস্তারিত) দৈর্ঘ্য ১ থেকে ৪০, তাই একটি batch-এর লক্ষ্যের প্রায় অর্ধেক ঘরই pad. একই model দুইভাবে ১০ epoch চালালাম (সত্যিকারের run, CPU-তে, torch 2.15.0):

~/bazarghar-dl$ uv run python run_mask.py
একটি batch: T (128, 42) | লক্ষ্যের ঘরের মধ্যে pad 46.5%
ignore_index=PAD   | train loss-এর নিজের মান 1.4292 | test: আসল token-এর loss 1.4526 | CER 0.6095 | 37s
ignore_index নেই   | train loss-এর নিজের মান 0.7251 | test: আসল token-এর loss 1.3654 | CER 0.5670 | 32s

পড়ে দেখুন। Mask ছাড়া train loss-এর নিজের মান ০.৭২৫, প্রায় অর্ধেক; কিন্তু আসল token ধরে হিসাব করলে দুটোই প্রায় ১.৪. কারণ pad ঘর আন্দাজ করা সহজ, সেগুলো গড়কে নিচে টেনে রাখে। এই এক run-এ mask ছাড়া model খারাপও হয়নি, বরং সামান্য ভালো; সেটিকে সাধারণ নিয়ম ভাববেন না। আসল শিক্ষা অন্য: দৈর্ঘ্যের মিশ্রণ বদলালেই এই সংখ্যা বদলাবে, তাই দুটি পরীক্ষার train loss আর তুলনা করা যায় না।

তিনটি pad-সংক্রান্ত জিনিস গুলিয়ে ফেলবেন না। padding_idx (nn.Embedding-এ) pad-এর vector শূন্য রাখে; pack_padded_sequence RNN-কে pad পড়তে দেয় না, ফলে context হয় শেষ আসল অক্ষরের state; ignore_index loss থেকে pad বাদ দেয়। একটি অন্য দুটির কাজ করে না। Packing বাদ দিলে ছোট input-এর context-ও বহু pad পড়ে নষ্ট হতে পারে।

৬ · মাপা — exact-match আর character error rate

Loss দিয়ে শেখানো হয়, কিন্তু ব্যবহারকারী loss দেখেন না, দেখেন output. দুটি মাপ শূন্য থেকে লিখি। Exact-match accuracy: কত ভাগ output হুবহু মিলেছে। CER নরম: ভুলের পরিমাণ মাপে। তার ভিত্তি Levenshtein distance, মানে সবচেয়ে কম কতটি অক্ষর যোগ, বাদ বা বদল করলে একটি লেখা আরেকটি হয়। এর dynamic programming DSA কোর্সের পর্ব ৫৪-এ আছে; এখানে একটি সারি রেখে সংক্ষেপে লিখছি।

CER = Σ d(ref, hyp) / Σ |ref|

এখানে ref সঠিক উত্তর, hyp model-এর output, d তাদের Levenshtein distance, |ref| সঠিক উত্তরের অক্ষর-সংখ্যা।

def levenshtein(ref, hyp):
    prev = list(range(len(hyp) + 1))           # প্রথম সারি: 0, 1, 2, ...
    for i, r in enumerate(ref, 1):
        cur = [i]
        for j, h in enumerate(hyp, 1):
            cur.append(min(prev[j] + 1,        # ref-এর অক্ষর বাদ
                           cur[j - 1] + 1,     # hyp-এ বাড়তি অক্ষর
                           prev[j - 1] + (r != h)))  # মিল বা বদল
        prev = cur
    return prev[-1]

pairs = [("2025-03-12", "2025-03-12"),   # হুবহু
         ("2025-03-12", "2025-03-21"),   # দুই অঙ্ক উল্টো
         ("1999-11-05", "1999-11-5"),    # একটি অঙ্ক বাদ
         ("2031-07-30", "2013-07-30"),   # সাল উল্টো: অর্থ পুরো বদল
         ("03-12", "3-21"),              # ছোট জোড়া, হাতের হিসাব
         ("1987-12-01", "")]             # কিছুই নেই

dist_sum = ref_sum = ok = per_item = 0
print("ref         hyp          d  CER")
for ref, hyp in pairs:
    d = levenshtein(ref, hyp)
    print(f"{ref:11} {hyp or '(ফাঁকা)':12} {d:<2} {d / len(ref):.2f}")
    dist_sum += d; ref_sum += len(ref); ok += ref == hyp; per_item += d / len(ref)
print(f"exact-match = {ok}/{len(pairs)} = {ok / len(pairs):.3f}")
print(f"CER (মোট) = {dist_sum}/{ref_sum} = {dist_sum / ref_sum:.3f}")
print(f"CER (প্রতি জোড়ার গড়) = {per_item / len(pairs):.3f}")
print("hyp বেশি লম্বা হলে:", levenshtein("12", "1212345") / 2)

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

ref         hyp          d  CER
2025-03-12  2025-03-12   0  0.00
2025-03-12  2025-03-21   2  0.20
1999-11-05  1999-11-5    1  0.10
2031-07-30  2013-07-30   2  0.20
03-12       3-21         3  0.60
1987-12-01  (ফাঁকা)      10 1.00
exact-match = 1/6 = 0.167
CER (মোট) = 18/55 = 0.327
CER (প্রতি জোড়ার গড়) = 0.350
hyp বেশি লম্বা হলে: 2.5

“2013” বনাম “2031”-এ CER মাত্র ০.২০, কিন্তু অর্ডারটি ভুল বছরে চলে যায়; এমন কাজে exact-match আসল মাপ। আবার “1999-11-5” বাদ পড়া একটি শূন্য, যা CER ০.১০ দেখায়; মানুষ সহজে ঠিক করতে পারে, exact-match সেটি পুরো ভুল বলে। দুটিই দেখুন। মোট আর গড় CER আলাদা হয় যখন লেখার দৈর্ঘ্য আলাদা, তাই রিপোর্টে কোনটি, তা লিখবেন। আর শেষ সারি: CER ১-এর বেশিও হতে পারে, এটি কোনো শতাংশ নয়।

Levenshtein টেবিল: d[সারি][কলাম], কলাম = hyp-এর অক্ষর (ফাঁকা 3 - 2 1)
কলাম
ফাঁকা
0
3
-
1
2
ফল
  1. প্রথম সারি. ফাঁকা লেখা থেকে hyp-এর j অক্ষর বানাতে j-টি যোগ: ০, ১, ২, ৩, ৪. প্রথম কলামও একইভাবে সারির নম্বর।
  2. সারি “0”. hyp-এর “3”-এর সঙ্গে “0” মেলে না। তির্যকে ০ + ১ = ১, ওপরে ১ + ১ = ২, বাঁয়ে ১ + ১ = ২; সবচেয়ে কম ১.
  3. সারি “3”. hyp-এর “3”-এর সঙ্গে মিল, তির্যকে ঘরের মান ১ অপরিবর্তিত। অর্থ: “03” থেকে “3” বানাতে একটি বাদ লাগে।
  4. সারি “-”. আবার মিল: কলাম “-”-এ তির্যকের ১ থেকে ১. “03-” আর “3-” এর দূরত্ব এখনো ১.
  5. সারি “1”. কলাম “2”-এ “1” আর “2” মেলে না: তির্যকের ১ + ১ = ২. কলাম “1”-এ মিল: তির্যকের ২ অপরিবর্তিত থাকে।
  6. সারি “2”. কলাম “2”-এ মিল, ঘর ২. শেষ ঘরে তিন দিক থেকেই ৩: “12” আর “21”-এর পার্থক্য দুটি বদল, আগের একটি বাদের সঙ্গে মোট ৩.
  7. ফল. d = ৩, ref-এর দৈর্ঘ্য ৫, CER = ০.৬০. উপরের কোডের সারির সঙ্গে মিলিয়ে নিন।

৭ · সত্যিকারের run — বাজারঘরের তারিখ, চার রকম teacher forcing

এবার PyTorch-এ। ডেটা course-এর বানানো, seed দিয়ে: ১৯৫০ থেকে ২০৪৯-এর এলোমেলো তারিখ, তিনটি লেখার ধরনে; train ৮,০০০, test ২,০০০, কোনো input দুই ভাগে নেই। Token হলো NFC করা Unicode অক্ষর (বাংলা লেখার এই খুঁটিনাটি AI-এর ভিত্তি কোর্সের পর্ব ৬৭-এ)। Model-এর মূল অংশ:

class Seq2Seq(nn.Module):
    def __init__(self, n_src, n_tgt, emb=32, hid=128):
        super().__init__()
        self.src_emb = nn.Embedding(n_src, emb, padding_idx=PAD)
        self.tgt_emb = nn.Embedding(n_tgt, emb, padding_idx=PAD)
        self.enc = nn.GRU(emb, hid, batch_first=True)
        self.dec = nn.GRUCell(emb, hid)
        self.out = nn.Linear(hid, n_tgt)

    def encode(self, src, lens):
        packed = nn.utils.rnn.pack_padded_sequence(
            self.src_emb(src), lens.cpu(), batch_first=True, enforce_sorted=False)
        _, h_n = self.enc(packed)      # h_n: (1, B, hid), শেষ আসল অক্ষরের state
        return h_n[0]                  # context: (B, hid)

    def forward(self, src, lens, tgt, tf_ratio):
        h, inp, logits = self.encode(src, lens), tgt[:, 0], []
        for t in range(1, tgt.size(1)):
            h = self.dec(self.tgt_emb(inp), h)
            step = self.out(h); logits.append(step)
            use_gold = torch.rand(len(inp), device=inp.device) < tf_ratio
            inp = torch.where(use_gold, tgt[:, t], step.argmax(-1))
        return torch.stack(logits, 1)  # (B, T-1, n_tgt)

Adam, learning rate ০.০০২, batch ৬৪, ১২ epoch, gradient clipping ১.০, মোট ১,২৮,২৩৯টি parameter. Ratio চার রকম: সবসময় ১.০, ০.৫, ০.০ (পুরো free-running), আর scheduled sampling (১.০ থেকে সরলরেখায় ০.০). প্রতিটি ৩টি seed-এ। সত্যিকারের run, CPU-তে; torch 2.15.0, Python 3.14.4, প্রতি seed প্রায় আধা মিনিট:

~/bazarghar-dl$ uv run python run_tf.py 0.5 0,1,2
0.5    seed 0 | train 0.1825 | test TF 0.1678 | test free 0.1804 | exact 0.256 | CER 0.0803 | exact@3,6,9,12 0.012 0.072 0.163 0.256 | 36s
0.5    seed 1 | train 0.0016 | test TF 0.0016 | test free 0.0016 | exact 1.000 | CER 0.0000 | exact@3,6,9,12 0.028 0.964 1.000 1.000 | 28s
0.5    seed 2 | train 0.0319 | test TF 0.0295 | test free 0.0511 | exact 0.905 | CER 0.0117 | exact@3,6,9,12 0.019 0.514 0.980 0.905 | 29s
0.5    গড় | test TF 0.0663 | test free 0.0777 | exact 0.720 ± 0.405 | CER 0.0307 ± 0.0434 | exact@3,6,9,12 0.020 0.517 0.714 0.720

চারটির গড় (৩ seed) এক জায়গায়। Greedy free-running দিয়ে মাপা; “৩, ৬, ৯, ১২” মানে সেই epoch-এর পরে test exact-match:

RatioExact (১২ epoch)CERTest loss: TF / freeExact: ৩, ৬, ৯ epoch
১.০০.৯৮০ ± ০.০২২০.০০২৪০.০১৬২ / ০.০১৯৯০.০০৯, ০.২৮০, ০.৬৬১
০.৫০.৭২০ ± ০.৪০৫০.০৩০৭০.০৬৬৩ / ০.০৭৭৭০.০২০, ০.৫১৭, ০.৭১৪
০.০১.০০০ ± ০.০০০০.০০০০০.০০২১ / ০.০০২১০.০২২, ০.৭৬৮, ০.৯৫০
Scheduled০.৯৯৯ ± ০.০০১০.০০০১০.০০৪৪ / ০.০০৪৪০.০০৯, ০.১৫৭, ০.৮৮৭

ফলটি পাঠ্যবইয়ের সরল গল্পের সঙ্গে হুবহু মেলে না, আর সেটিই শেখার বিষয়। “Teacher forcing সবসময় দ্রুত শেখায়” এখানে দেখা গেল না: এই ছোট, নিয়মে বাঁধা, ১০ অক্ষরের output-এ পুরো free-running সবচেয়ে এগিয়ে থাকল। ০.৫-এর একটি seed আটকে গেল (০.২৫৬), আর sd এত বড় যে ০.৫ বনাম ১.০ নিয়ে এই ৩ seed থেকে কিছু দাবি করা যায় না। ৩টি seed-এর একটি স্থানীয় পরীক্ষা, সাধারণ সিদ্ধান্ত নয়। লম্বা output-এ ছবিটি বদলাতে পারে: §৮-এর অর্ডার নম্বরের কাজে (h = ১২৮, একটি seed) ৬–১০ অঙ্কে exact-match teacher forcing-এ ০.৭৯২, free-running-এ ০.৪৫৩; যদিও ৩৬–৪০ অঙ্কে CER-এ free-running সামান্য এগিয়ে (০.৫২০ বনাম ০.৫৭৯)। তাই ratio একটি hyperparameter, আগে থেকে ঠিক করা সত্য নয়।

আর দেখুন test loss-এর দুই কলাম: ratio ১.০-তে TF আর free-এর মধ্যে ফাঁক, শেখা শেষ হলে সেই ফাঁক প্রায় নেই। অর্ধেক শেখা অবস্থায় ফাঁকটি কেমন? ratio ১.০-র ৬ epoch-এর model-এ, একটি test উদাহরণ:

~/bazarghar-dl$ uv run python run_trace.py 6
6 epoch পরে: exact 0.236 | CER 0.0956
input      : ডিসেম্বর ২, ২০৩১
gold       : 2031-12-02
TF pick    : 2031-10-22$ (ঠিক 9/11)
free-running: 2031-10-20 (edit distance 3)
…
token accuracy TF 0.923 | free 0.913
অবস্থান অনুযায়ী token accuracy (1 থেকে 11):
 TF   1.00 1.00 1.00 1.00 1.00 0.99 0.89 1.00 0.90 0.36 1.00
 free 1.00 1.00 1.00 1.00 1.00 0.99 0.89 1.00 0.90 0.26 1.00

এখানে $ মানে </s>. Token accuracy ০.৯২ শুনতে ভালো, অথচ exact-match মাত্র ০.২৩৬: প্রতি তারিখে একটি অঙ্ক ভুল হলেই পুরোটা ভুল। সাল প্রায় শতভাগ ঠিক; দিনের শেষ অঙ্কে model সবচেয়ে দুর্বল (TF ০.৩৬, free ০.২৬)। ভুলের গড়ানো এখানে ছোট, কারণ দিনের অঙ্কই প্রায় শেষ ঘর, পরে গড়ানোর জায়গা কম। Output যত লম্বা, এই ফাঁক তত বাড়ার কথা।

সততার কথা: টেবিলের ১.০০০ দেখে ভাববেন না model বাস্তবের তারিখ পড়তে পারে। Test ডেটা train-এর একই ছাঁচে বানানো, তিনটি ধরনই train-এ ছিল। “১২ই মার্চ”, “১২/৩/২৫” বা হাতের লেখার ভুল এলে ফল কী হবে, তা এই পরীক্ষা বলে না। আর এই কাজের জন্য কয়েক লাইনের regular expression-ই হয়তো বেশি নির্ভরযোগ্য; এখানে তারিখ শুধু শেখার ছোট উদাহরণ।

৮ · লম্বা হলে কী ভাঙে — fixed context মাপা

তারিখের কাজে input ছোট, তাই context-এর সীমা ধরা পড়েনি। Fixed-context bottleneck মাপতে দৈর্ঘ্য ইচ্ছামতো বদলানো যায় এমন একটি কাজ দরকার। বাজারঘরের অর্ডার নম্বর নিন: এলোমেলো বাংলা অঙ্ক, দৈর্ঘ্য ১ থেকে ৪০, output একই সংখ্যা ইংরেজি অঙ্কে (“৬৮০৬” → “6806”). কাজটি ইচ্ছা করেই হাস্যকর রকম সহজ: দশ ঘরের একটি lookup টেবিলই ১০০% পায়। তাই ভুল হলে দোষ কাজের নয়, model-এর নকশার।

Train ২০,০০০ জোড়া, batch ১২৮, ২০ epoch, ratio ১.০; বাকি সব তারিখের model-এর মতো। Test প্রতিটি দৈর্ঘ্যের ভাগে ৪০০টি। সত্যিকারের run, CPU-তে, torch 2.15.0:

~/bazarghar-dl$ uv run python run_len.py 128 0
hid 128 seed 0 rev False epochs 20 train loss 0.8449 time 195 s
L= 1- 5 exact 1.000 CER 0.0000
L= 6-10 exact 0.792 CER 0.0679
L=11-15 exact 0.223 CER 0.2437
L=16-20 exact 0.007 CER 0.3658
L=21-25 exact 0.000 CER 0.4593
…
L=36-40 exact 0.000 CER 0.5789
L=36-40 পজিশন অনুযায়ী ঠিক: 0.579 0.198 0.135 0.128
নমুনা: 6806736227020903394956701480025843446149 → 680673267020932056937401490842940356491

~/bazarghar-dl$ uv run python run_ctrl.py     # একই GRU, প্রতি input অক্ষরের পাশেই output
tagger, hid 128, 2 epoch, 12 s
L= 1- 5 exact 1.000
…
L=36-40 exact 1.000

এই লাইনের “নমুনা” model-এর আসল output-এর একটি উদাহরণ, বানানো নয়। সব মিলিয়ে নিচের টেবিল: প্রতিটি ঘরে exact-match / CER, h = hidden size. h = ৬৪, ১২৮ আর ২৫৬ দুটি seed-এর গড়, h = ৫১২ একটি seed; শেষ কলাম সেই নিয়ন্ত্রণ পরীক্ষা, যেখানে কোনো context নেই।

দৈর্ঘ্যh = ৬৪h = ১২৮h = ২৫৬h = ৫১২পাশে পাশে (h = ১২৮), exact
১–৫০.৯৫৬ / ০.০৩৪১.০০০ / ০.০০০০.৯৯৯ / ০.০০১১.০০০ / ০.০০০১.০০০
৬–১০০.৩৮১ / ০.২৩৮০.৭৮৬ / ০.০৭০০.৮৭৬ / ০.০৩৩০.৯৪৫ / ০.০১৩১.০০০
১১–১৫০.০৩২ / ০.৩৯৬০.২১২ / ০.২৩৭০.৫২১ / ০.০৯৮০.৮০৭ / ০.০৩৩১.০০০
১৬–২০০.০০০ / ০.৫০৩০.০০৭ / ০.৩৫৭০.১৮৮ / ০.১৮০০.৫৭০ / ০.০৬৫১.০০০
২১–২৫০.০০০ / ০.৫৬১০.০০০ / ০.৪৪৭০.০১৯ / ০.২৭৮০.২৯০ / ০.১০৮১.০০০
২৬–৩০০.০০০ / ০.৫৯৬০.০০০ / ০.৫০৫০.০০২ / ০.৩৫২০.০৯০ / ০.১৫২১.০০০
৩১–৩৫০.০০০ / ০.৬২৬০.০০০ / ০.৫৪৭০.০০০ / ০.৪১১০.০১০ / ০.২২৭১.০০০
৩৬–৪০০.০০০ / ০.৬৩৯০.০০০ / ০.৫৭২০.০০০ / ০.৪৫৩০.০০০ / ০.২৯৪১.০০০

তিনটি জিনিস পড়ুন। এক, দৈর্ঘ্যের সঙ্গে পতন। h = ১২৮-এ ৫ অঙ্ক পর্যন্ত প্রায় নিখুঁত, ৬–১০-এ ০.৭৮৬, ১১–১৫-এ ০.২১২, ১৬–২০-এ ০.০০৭. দুই, শেষ কলাম। একই GRU, একই hidden size, মাত্র ২ epoch: সব দৈর্ঘ্যে ১.০০০. কাজ বা unit সমস্যা নয়, সমস্যা সব তথ্য এক vector দিয়ে পার করা। তিন, h বাড়ালে সীমা সরে, যায় না। ১৬–২০ অঙ্কে exact-match h = ৬৪-এ ০.০০০, ১২৮-এ ০.০০৭, ২৫৬-এ ০.১৮৮, ৫১২-এ ০.৫৭০. কিন্তু ৩৬–৪০ অঙ্কে h = ৫১২-তেও ০.০০০, CER ০.২৯৪. h আটগুণ করতে parameter বেড়েছে প্রায় ৪২ গুণ (৪০,৪৯৫ থেকে ১৬,৮৬,৮৯৫), আর এই CPU-তে সময় ১৩৭ s থেকে ৮৪৩ s. আরও training দিলে সংখ্যা কিছু বাড়ত, কিন্তু দৈর্ঘ্যের সঙ্গে দৌড়ে এটি মোটেই সস্তা পথ নয়।

Output-এর কোন অংশে ভুল? L = ৩৬–৪০-এ output-কে চার সিকিতে ভাগ করলে h = ১২৮-এর (seed ০) ঠিক অক্ষরের ভাগ ০.৫৭৯, ০.১৯৮, ০.১৩৫, ০.১২৮. এলোমেলো অঙ্কে আন্দাজের মান ০.১০, তাই শেষের দিকে model প্রায় আন্দাজ করছে। এই সংখ্যায় একটু অতিরিক্ত ভুল আছে, কারণ একটি অঙ্ক বাদ পড়লে পরের সবই এক ঘর সরে যায়; CER সেটি শাস্তি দেয় না, তাই CER এই হিসাবের চেয়ে কম।

Sutskever-দের একটি পুরোনো কৌশলও চেষ্টা করলাম: input উল্টো ক্রমে পড়ানো। h = ২৫৬, একটি seed, সব ঠিক আগের মতো:

~/bazarghar-dl$ uv run python run_len.py 256 0 rev
hid 256 seed 0 rev True epochs 20 train loss 0.2409 time 470 s
L= 1- 5 exact 1.000 CER 0.0000
L= 6-10 exact 0.965 CER 0.0069
L=11-15 exact 0.855 CER 0.0213
L=16-20 exact 0.708 CER 0.0401
L=21-25 exact 0.383 CER 0.0948
L=26-30 exact 0.083 CER 0.1817
L=31-35 exact 0.003 CER 0.2750
L=36-40 exact 0.000 CER 0.3595

একই h-এ, একই seed-এ সাধারণ ক্রমে ১৬–২০-এর exact ০.১৪২ ছিল, এখানে ০.৭০৮. বিশাল লাভ, কিন্তু ৩১ অঙ্কের পরে আবার প্রায় শূন্য। সীমা সরেছে, যায়নি। প্রথম প্রশ্নের ভাবনায় এর কারণ নিয়ে কথা আছে।

পরের ধাপ

Decoder যদি শুধু শেষ state না দেখে, প্রতি ধাপে encoder-এর সব state থেকে দরকারি অংশটি বেছে পড়তে পারত? পাশে-পাশে কলামের ১.০০০ দেখায়, পড়ার সময় ঠিক অক্ষরটি হাতের কাছে থাকলে কাজটি কত সহজ। এটিই attention-এর ধারণা, পর্ব ৩২ (Attention — query, key, value আর alignment)-এ।

আরও গভীরে: দৈর্ঘ্য বাড়লে ফলের পতন Cho ও সহকর্মীদের ২০১৪ সালের “On the Properties of Neural Machine Translation” প্রবন্ধে রিপোর্ট করা হয়; Bahdanau, Cho ও Bengio-র attention (arXiv ২০১৪, ICLR ২০১৫) সেখান থেকেই আসে। Sutskever-রা ২০১৪ সালে উল্টো input-এ ভালো ফলের কথা জানান। Scheduled sampling-এর তত্ত্বগত দুর্বলতা নিয়ে Huszár (২০১৫) আর ভিন্ন সমাধান হিসেবে Professor Forcing (Lamb ও সহকর্মীরা, ২০১৬) পড়তে পারেন। আমাদের এলোমেলো অঙ্কে কোনো গঠন নেই, তাই এটি context-এর জন্য সবচেয়ে কঠিন পরীক্ষা; ভাষায় পতন এত তীব্র না-ও হতে পারে।

পরীক্ষা ও ইন্টারভিউতে: Teacher forcing কী, আর ঝুঁকি? ইনপুট হিসেবে আগের সঠিক token; ব্যবহারের পরিস্থিতির সঙ্গে অমিল। Decoder-এর ইনপুট আর লক্ষ্য? tgt[:, :-1] আর tgt[:, 1:]. Fixed context-এর সমস্যা? স্থির (B, H) vector-এ লম্বা input-এর সব তথ্য; attention এর উত্তর। CER কত হতে পারে? ০ থেকে ১-এর বেশি; হর সঠিক উত্তরের দৈর্ঘ্য। Greedy কি সেরা sequence দেয়? না, প্রতি ধাপের সেরা নিলে পুরো sequence-এর সেরা না-ও হতে পারে।

৯ · AI-এর যুগে — seq2seq-এর কোড যাচাই

AI assistant seq2seq-এর কোড সহজেই লিখে দেয়, আর চলেও। সমস্যা হলো ভুলগুলো error দেয় না, শুধু সংখ্যা বদলে দেয়।

যে ভুল প্রায়ই দেখা যায়ফলকীভাবে ধরবেন
লক্ষ্য এক ঘর না সরিয়ে lossModel শুধু ইনপুট নকল করে, loss প্রায় শূন্য, greedy output অর্থহীনএক উদাহরণে dec_in আর gold ছাপিয়ে পড়া
ignore_index নেই, বা pad-এর id আলাদাLoss কম দেখায়, তুলনা অর্থহীনPad-এর ভাগ ছাপানো; mask-সহ আর ছাড়া loss মেলানো
Packing ছাড়া শেষ ঘরের state কে context ধরাContext-এ pad পড়া stateএকা চালানো আর batch-এ চালানো উদাহরণের context মেলানো
মূল্যায়নে teacher forcing চালুদারুণ “accuracy”, আসলে অর্থহীনমূল্যায়নে শুধু greedy; লক্ষ্য decoder-এর হাতেই না দেওয়া
Greedy-তে model.eval() বা torch.no_grad() নেই, বা </s>-এর পরের অক্ষর গোনাভুল মাপ, মেমরি বাড়াপরিচিত জোড়ায় হাতে হিসাব করা CER-এর সঙ্গে মেলানো
একটি seed-এ “scheduled sampling ভালো” দাবিকাকতালীয় ফলকয়েকটি seed আর sd
বানানো বা ভুল-বছরের citationভুল ইতিহাসমূল প্রবন্ধ খুঁজে মিলিয়ে দেখা

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

আমি PyTorch 2.x-এ বাংলা তারিখ থেকে ISO তারিখের একটি GRU seq2seq বানাচ্ছি (character-level, দৈর্ঘ্য আলাদা, padding আছে)। ১. Encoder, decoder আর training step দাও; pack_padded_sequence ব্যবহার করো আর teacher forcing ratio একটি argument রাখো। ২. একটি উদাহরণে dec_in আর gold ছাপানোর কোড দাও, যাতে এক ঘর সরানো চোখে দেখা যায়। ৩. Loss-এ pad বাদ যাচ্ছে, তার একটি ছোট assert লেখো; আর একই input একা ও batch-এ দিলে context মেলে কি না, তার পরীক্ষা লেখো। ৪. মূল্যায়ন শুধু greedy free-running দিয়ে করো; exact-match আর CER দাও, CER-এর একটি হাতে-হিসাব করা উদাহরণ দিয়ে পরীক্ষা করো। ৫. আগে এক batch overfit করার কোড দাও, তারপর ৩টি seed-এ গড় ± sd. ৬. যেসব API তুমি PyTorch docs-এ যাচাই করোনি, তা স্পষ্ট করে বলো।

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

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

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

প্র ০১Input উল্টো করে পড়ালে ভালো ফল আসে, এই কৌশলটি কেন কাজ করার কথা? আর এটি কি fixed context-এর সমস্যার সমাধান?

Sutskever, Vinyals আর Le ২০১৪ সালের প্রবন্ধে জানান, source বাক্যের শব্দ উল্টো ক্রমে পড়ালে তাঁদের LSTM অনুবাদের ফল উল্লেখযোগ্যভাবে ভালো হয়। যুক্তিটি সরল। “a b c” থেকে “x y z” লিখতে হলে সাধারণ ক্রমে “a” আর “x”-এর মধ্যে দূরত্ব অনেক: encoder-এর পুরো পথ পেরিয়ে তারপর decoder-এর প্রথম ধাপ। “c b a” ক্রমে পড়ালে “a” ঠিক context-এর পাশে, প্রথম কয়েকটি output-এর উৎস খুব কাছে। প্রবন্ধের লেখকদের অনুমান ছিল, এতে SGD-র পক্ষে শুরুর সংযোগ খুঁজে পাওয়া সহজ হয়। প্রথম কয়েকটি শব্দ ঠিক হলে পরের ধাপগুলোও ভালো ইতিহাস পায়।

আমাদের অর্ডার নম্বরের কাজে একই পরীক্ষা করেছি, h = ২৫৬, একটি seed: ১৬–২০ অঙ্কে exact-match ০.১৪২ থেকে ০.৭০৮, আর ৩৬–৪০ অঙ্কে CER ০.৪৬৪ থেকে ০.৩৬০. ৩৬–৪০ অঙ্কের output-এর চার সিকিতে ঠিক অক্ষরের ভাগ সাধারণ ক্রমে ০.৭৫৯, ০.৩৫৫, ০.২২৯, ০.১৬৭; উল্টো ক্রমে ০.৭১৫, ০.৩৯৪, ০.৪২৯, ০.৫২৯. এটি একটি কাজে একটি run; ভাষার অনুবাদে বাক্যের গঠন আলাদা, ফল এক হবে এমন দাবি চলে না।

লক্ষ্য করুন, কৌশলটি মোট তথ্যের পরিমাণ বাড়ায় না। Context-এর আকার একই থাকে। প্রথম দিকের output-এর দূরত্ব কমলে শেষ দিকের output-এর দূরত্ব বাড়ে: উল্টো পড়ালে input-এর শেষ অক্ষরটি পড়া হয় সবার আগে, লেখা হয় সবার পরে। অথচ আমাদের run-এ শেষ সিকিও ভালো হয়েছে, তাই দূরত্বের সরল যুক্তি পুরো ঘটনা ব্যাখ্যা করে না; সম্ভবত প্রথম অক্ষরগুলো ঠিক হলে পরের ধাপগুলোও ভালো ইতিহাস পায়। তবু ৩৬–৪০ অঙ্কে exact-match ০.০০০: সমস্যার মূল উৎস রয়েই যায়। আবার এটি একটি ভাষা-নির্ভর অনুমানও: যেখানে input আর output-এর ক্রম মোটামুটি একই, সেখানে সুবিধা বেশি হওয়ার কথা; যেখানে ক্রম খুব আলাদা (ইংরেজি থেকে বাংলার মতো subject–object–verb ক্রমের ভাষায়), সেখানে সুবিধা ভিন্ন হতে পারে। পরের প্রজন্মের bidirectional encoder আর attention এই কৌশলের প্রয়োজন অনেকটাই কমিয়ে দেয়।

মূল উপলব্ধি: ছোট কৌশল optimization সহজ করতে পারে, কিন্তু তথ্যের পথ সরু থাকলে সেটি পথই থেকে যায়; কৌশল আর স্থাপত্যের সমাধান আলাদা জিনিস।

প্র ০২Teacher forcing-এ training আর ব্যবহারের পরিস্থিতি আলাদা। তবু আজকের বড় language model-ও কেন প্রায় সবাই এভাবেই শেখে?

Teacher forcing-এর ধারণা পুরোনো; Williams আর Zipser ১৯৮৯ সালের কাজে recurrent network-এর শেখার প্রসঙ্গে এটি আলোচনা করেন। এর একটি পরিষ্কার সম্ভাবনা-তত্ত্বের যুক্তি আছে। একটি sequence-এর joint probability chain rule দিয়ে ভাঙা যায়: p(y1, y2, …) = p(y1) · p(y2 | y1) ⋯. Maximum likelihood চাইলে প্রতিটি ধাপে দেখতে হয় সঠিক ইতিহাসের শর্তে সঠিক token-এর probability. Teacher forcing ঠিক এই হিসাবটিই করে। তাই এটি কোনো সস্তা কৌশল নয়, likelihood-এর সরাসরি রূপ।

ব্যবহারিক সুবিধাও বড়। সব ধাপের ইনপুট আগে থেকে জানা, তাই Transformer-এ পুরো sequence একসঙ্গে, সমান্তরালে হিসাব করা যায়; ধাপে ধাপে নিজের অনুমান নিতে হলে সেই সুবিধা থাকে না। এটিই বড় language model-এর প্রশিক্ষণে এর প্রধান কারণগুলোর একটি।

তবু exposure bias সত্যি। Bengio ও সহকর্মীরা ২০১৫ সালে scheduled sampling প্রস্তাব করেন। Huszár ২০১৫ সালেই দেখান, এই পদ্ধতির objective সাধারণভাবে সঠিক maximum likelihood-এর দিকে যায় না, মানে তত্ত্বে এর একটি দুর্বলতা আছে। Lamb ও সহকর্মীদের Professor Forcing (২০১৬) GAN-এর ধারণা দিয়ে দুই অবস্থার hidden state-এর আচরণ কাছাকাছি আনার চেষ্টা করে। কোনোটিই সর্বজনীন সমাধান হয়নি।

আমাদের ছোট পরীক্ষায় পুরো free-running ছোট output-এ ভালো করেছে, আর লম্বা output-এর মাঝারি দৈর্ঘ্যে teacher forcing এগিয়ে ছিল; মানে ফল কাজ-নির্ভর। তবে বাস্তবে অনেক দল সরল teacher forcing দিয়ে শেখায়, তারপর decoding (beam search, sampling) আর পরের ধাপের সংশোধনে মনোযোগ দেয়। Output-এর ভুল বেশি হলে আসল ব্যবহারের পরিবেশে যাচাই করাই একমাত্র নিরাপদ পথ।

মূল উপলব্ধি: Teacher forcing গণিতে likelihood আর হিসাবে সমান্তরাল; তার দাম হলো নিজের ভুলের অভিজ্ঞতার অভাব, যা মূল্যায়নে আলাদা করে মাপতে হয়।

প্র ০৩Fixed context কি সত্যিই “জায়গার অভাব”? ১২৮টি দশমিক সংখ্যায় তো তাত্ত্বিকভাবে অনেক তথ্য রাখা যায়।

প্রশ্নটি ভালো, আর এর উত্তর “শুধু জায়গা নয়”। ৪০ অঙ্কের একটি এলোমেলো সংখ্যায় তথ্য প্রায় ৪০ × log2 ১০ ≈ ১৩৩ bit. ১২৮টি ৩২-bit সংখ্যায় কাগজে-কলমে ৪,০৯৬ bit জায়গা। তাহলে ধারণক্ষমতার সরল হিসাবে কোনো সমস্যা নেই। কিন্তু তথ্যটি এমনভাবে রাখতে হবে, যেন একটি মসৃণ, tanh-এ বাঁধা, gradient-এ শেখা function সেটি একটি একটি করে পড়তে পারে। প্রতিটি নতুন অক্ষর পড়ার সময় একই state-এ আগের সব তথ্য নষ্ট না করে নতুনটি যোগ করতে হয়। সংখ্যার খুব সূক্ষ্ম পার্থক্যে তথ্য রাখলে আবার noise-এর সামনে তা টেকে না, gradient-ও সহায়তা করে না। তাই ব্যবহারযোগ্য ধারণক্ষমতা কাগজের হিসাবের অনেক নিচে।

আমাদের তিনটি মাপ এই কথাকে সমর্থন করে। প্রথম, h বাড়ালে ফল ধাপে ধাপে ভালো হয়, কিন্তু parameter আর সময় অনেক বেশি বাড়ে। দ্বিতীয়, একই GRU প্রতিটি ঘরে input-এর পাশেই output দিলে ২ epoch-এ সব দৈর্ঘ্যে নিখুঁত: কাজটি কঠিন নয়, পথটিই কঠিন। তৃতীয়, লম্বা input-এ শুরুর অঙ্ক তুলনায় ভালো মনে থাকে, মাঝের আর শেষের অঙ্ক chance-এর কাছে। আরও বেশি epoch দিলে সংখ্যা কিছু বাড়তে পারত; এটি ছোট বাজেটের পরীক্ষা।

গবেষণাতেও একই লক্ষণ এসেছে। Cho ও সহকর্মীরা ২০১৪ সালের “On the Properties of Neural Machine Translation” প্রবন্ধে দেখান, বাক্য লম্বা হলে encoder–decoder-এর ফল দ্রুত নামে। Bahdanau, Cho আর Bengio (arXiv ২০১৪, ICLR ২০১৫) এই ধারণা থেকেই attention আনেন, যেখানে decoder প্রতি ধাপে encoder-এর সব state দেখতে পারে। তবে মনে রাখবেন, অনুবাদের বাক্যে ভাষার গঠন আছে, এলোমেলো অঙ্কে নেই; তাই আমাদের পরীক্ষা সবচেয়ে কঠিন পরিস্থিতি, প্রকৃত ভাষার পতন এত তীব্র না-ও হতে পারে।

মূল উপলব্ধি: Bottleneck-এর সীমা শুধু bit-এর হিসাব নয়; শেখা যায় এমন উপায়ে কতটা লেখা ও পড়া যায়, সেটিই আসল সীমা।

প্র ০৪একটি seq2seq model-কে কোন সংখ্যা দিয়ে বিচার করবেন? Exact-match, CER, না BLEU?

Metric বাছাই মানে আসলে জিজ্ঞেস করা: কোন ভুলের দাম কত? Levenshtein ১৯৬৫ সালে প্রকাশিত কাজে এই দূরত্বের ধারণা দেন, মূলত সংকেত পাঠানোর ভুল মাপার প্রসঙ্গে। এটি সব অক্ষরের ভুলকে সমান ধরে। অথচ তারিখে সালের প্রথম অঙ্ক আর দিনের একটি অঙ্কের ভুলের দাম এক নয়। CER তাই ভালো হলেও ভরসা করা যায় না; তারিখ, অর্ডার নম্বর, ফোন নম্বরের মতো কাঠামোবদ্ধ উত্তরে exact-match বা field-ভিত্তিক accuracy (সাল, মাস, দিন আলাদা) বেশি সৎ।

উল্টো দিকে OCR, বক্তৃতা-থেকে-লেখা বা বাংলিশ লিপ্যন্তরে ছোট ভুল মানুষ সহজে সামলায়। সেখানে CER বা শব্দ-স্তরের WER প্রচলিত, আর exact-match খুব কড়া।

অনুবাদে সমস্যা আরও বড়: একটি বাক্যের অনেক সঠিক অনুবাদ থাকে, অক্ষরের দূরত্ব তাই অর্থ মাপে না। Papineni ও সহকর্মীদের BLEU (২০০২) মানুষের অনুবাদের সঙ্গে n-gram-এর মিল মাপে, আর দীর্ঘদিন অনুবাদের প্রধান সংখ্যা ছিল। তবে এটিরও সমালোচনা আছে: অর্থ ঠিক রেখে ভিন্ন শব্দে লিখলে কম নম্বর পায়, আর একটি বাক্যের বিচারে এটি দুর্বল। পরে নানা বিকল্প এসেছে, আর গুরুত্বপূর্ণ কাজে মানুষের বিচার এখনো ব্যবহৃত হয়। BLEU-র বিস্তার পর্ব ৪৫ (NLP মূল্যায়ন)-এ।

Goodhart-এর সতর্কতাও এখানে খাটে: একটি সংখ্যাকেই লক্ষ্য বানালে model বা দল সেটি বাড়াতে শেখে, আসল কাজ না-ও ভালো হতে পারে। আরেকটি বিষয় হলো গড়ের ফাঁদ। মোট CER ভালো থাকতে পারে, যখন ৪০ অঙ্কের উদাহরণ সব ভুল: আমাদের টেবিলে তাই দৈর্ঘ্য অনুযায়ী ভাগ করে দেখানো হয়েছে। ব্যবসার ক্ষেত্রে “ভুল তারিখে কত টাকার ক্ষতি” ধরনের একটি মাপ প্রায়ই সব metric-এর চেয়ে কাজের।

মূল উপলব্ধি: ভালো metric ভুলের আসল দামকে অনুসরণ করে; একটির বদলে দুটি মাপ আর দৈর্ঘ্য অনুযায়ী ভাগ অনেক ফাঁদ ধরে ফেলে।

প্র ০৫আজ Transformer-এর যুগে একটি GRU seq2seq শেখার কী দরকার? আর তারিখ ঠিক করার মতো কাজে neural model আদৌ দরকার কি?

দুটি প্রশ্নই সৎ। প্রথমটির উত্তর: কাঠামোটি মরেনি, শুধু ভেতরের অংশ বদলেছে। ২০১৭ সালের Transformer-ও একটি encoder–decoder; T5, BART-এর মতো model সরাসরি এই নকশার বংশধর। Start আর end token, এক ঘর সরানো লক্ষ্য, masked loss, teacher forcing, greedy আর beam decoding, exact-match ধরনের মূল্যায়ন, এসবই সেখানে একই। আজকের LLM মূলত শুধু-decoder, কিন্তু training-এর সময় সেটিও সরানো লক্ষ্যে teacher forcing-এর মতোই চলে। AI-এর ভিত্তি কোর্সের পর্ব ৭০-এর পরের-token-এর ছবিটি এখন আপনি ভেতর থেকে দেখতে পাচ্ছেন। GRU-তে এসব ছোট আকারে, ল্যাপটপে, কয়েক মিনিটে দেখা যায়; এটি শেখার সস্তা গবেষণাগার।

RNN-ও পুরোপুরি বিদায় নেয়নি। খুব কম মেমরির যন্ত্রে, ধারাবাহিক ডেটায় প্রতি ধাপে সামান্য হিসাবে, এগুলো এখনো বিবেচনায় আসে। তবে অনুবাদের মতো বড় ভাষার কাজে এদের জায়গা মূলত attention-ভিত্তিক model নিয়ে নিয়েছে।

দ্বিতীয় প্রশ্নের উত্তরে সংযম দরকার। বাংলা তারিখের কয়েকটি জানা ধরন থাকলে একটি parser বা regular expression ব্যাখ্যাযোগ্য, পরীক্ষাযোগ্য, প্রায় বিনামূল্যের। আমাদের GRU-র ১.২৮ লাখ parameter-এর তুলনায় ত্রিশ লাইনের নিয়ম ভালো সমাধান হতে পারে, আর ভুল হলে কোথায় ভুল তা বোঝা যায়। Neural model-এর যুক্তি আসে তখন, যখন ধরন অগণিত আর নিয়ম লেখা অসম্ভব: হাতের লেখা, স্ক্যানের গোলমাল, বাক্যের ভেতরে লুকানো তারিখ। সেখানেও নিয়ম আর model একসঙ্গে চলতে পারে, যেমন model-এর output যাচাই করা হলো একটি পাকা তারিখ-parser দিয়ে: “2025-13-45” এলে বাতিল।

এই সংযম বড় কাজেও প্রযোজ্য: চমৎকার demo-র আগে একটি সরল baseline লিখুন। Model সেটিকে হারাতে না পারলে তার জটিলতা ও পরিচালনার খরচের যুক্তি নেই।

মূল উপলব্ধি: Seq2seq শেখা মানে আধুনিক model-এর কঙ্কাল শেখা; কিন্তু একটি নির্দিষ্ট কাজে neural model লাগবে কি না, সে সিদ্ধান্ত baseline-এর সঙ্গে মেপেই নিতে হয়।

অনুশীলন

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

  1. পূর্ণ numerical: (ক) ref = “2025”, hyp = “205”. Levenshtein distance আর CER বের করুন। (খ) একটি জোড়ায় ref “12” আর hyp “123456”: CER কত? (গ) একটি seq2seq-এ src vocabulary ৪৪, tgt vocabulary ১৫, embedding ৩২, hidden ১২৮; encoder nn.GRU, decoder nn.GRUCell, output nn.Linear. PyTorch-এর GRU-তে প্রতি gate-এ দুটি bias vector ধরে মোট parameter কত?

    (ক) একটি “2” বাদ দিলেই হয়, d = ১; CER = ১/৪ = ০.২৫. (খ) ৪টি অক্ষর বাড়তি, d = ৪; CER = ৪/২ = ২.০, মানে ১-এর বেশিও হতে পারে। (গ) Embedding: ৪৪ × ৩২ = ১,৪০৮ আর ১৫ × ৩২ = ৪৮০. প্রতিটি GRU: ৩ × (৩২ × ১২৮ + ১২৮ × ১২৮ + ২ × ১২৮) = ৩ × ২০,৭৩৬ = ৬২,২০৮; দুটি মিলে ১,২৪,৪১৬. Output: ১২৮ × ১৫ + ১৫ = ১,৯৩৫. মোট ১,২৮,২৩৯, যা PyTorch-এর গোনার সঙ্গে মেলে।

  2. সমালোচনা ও নকশা: একটি AI assistant এই training ধাপ দিয়েছে: out = model(src, tgt) (teacher forcing, আকার (B, T, V)), তারপর loss = F.cross_entropy(out.reshape(-1, V), tgt.reshape(-1)). মূল্যায়নে একই model(src, tgt) চালিয়ে argmax নিয়ে accuracy ৯৯% পেয়েছে। কী কী ভুল?

    তিনটি বড় ভুল। এক, লক্ষ্য সরানো হয়নি: ধাপ t-তে ইনপুট tgt[t], লক্ষ্যও tgt[t], তাই model শুধু ইনপুট নকল করে শেখে। সমাধান: ইনপুট tgt[:, :-1], লক্ষ্য tgt[:, 1:]. দুই, ignore_index নেই, তাই pad-ও loss-এ ঢোকে। তিন, মূল্যায়নে সঠিক উত্তর decoder-কে দেওয়া হচ্ছে, যা আসলে উত্তরপত্র দেখে পরীক্ষা। Greedy free-running চালিয়ে exact-match আর CER মাপতে হবে। ৯৯%-এর মানে নকল করার দক্ষতা; greedy-তে সম্ভবত অর্থহীন লেখা আসবে। একটি উদাহরণে dec_in আর gold ছাপালেই প্রথম ভুলটি ধরা পড়ে।

  3. আউটপুট অনুমান করুন: কী ছাপা হবে? PAD=0; tgt = np.array([[1, 5, 6, 2], [1, 7, 2, 0]]); print(tgt[:, :-1].tolist(), tgt[:, 1:].tolist(), int((tgt[:, 1:] != 0).sum()))

    [[1, 5, 6], [1, 7, 2]] [[5, 6, 2], [7, 2, 0]] 5. ইনপুটের প্রথম কলামে সবার জন্য <s>, মানে 1. লক্ষ্যে ছয়টি ঘরের একটিই pad, তাই loss-এ ঢুকবে ৫টি ঘর। দ্বিতীয় সারির ইনপুটে </s> (2) আছে: সেই ধাপের লক্ষ্য pad, তাই সেই ঘর loss-এ গোনা হয় না।

  4. নকশা: বাজারঘর ক্রেতাদের বাংলিশ নাম (“rahim”) বাংলা লিপিতে (“রহিম”) আনতে একটি seq2seq চায়। ডেটা ভাগ, মাপ, baseline আর দৈর্ঘ্যের ঝুঁকি নিয়ে একটি ছোট পরিকল্পনা লিখুন।

    ডেটা ভাগ: একই নাম train আর test-এ যেন না থাকে, নইলে model মুখস্থ করেও ভালো নম্বর পাবে। আর আসল ক্রেতার নাম ব্যক্তিগত তথ্য, তাই বাইরের সেবায় না পাঠানো। Token: character-level; বাংলা দিকে যুক্তাক্ষরের জন্য Unicode অক্ষর স্তর, NFC করে। Baseline: অক্ষর-ম্যাপের একটি নিয়ম-ভিত্তিক টেবিল, আর সবচেয়ে ঘন ঘন বানানের lookup. Model: নাম ছোট, তাই fixed context সম্ভবত যথেষ্ট; তবু দৈর্ঘ্য অনুযায়ী ফল ভাগ করে দেখা। মাপ: exact-match আর CER দুটিই; একই নামের একাধিক সঠিক বানান থাকলে (“রহীম”, “রহিম”) top-k বা একাধিক ref বিবেচনা। যাচাই: ৩টি seed, আর অপরিচিত ধরনের নাম (বিদেশি, লম্বা) দিয়ে আলাদা পরীক্ষা। নিরাপত্তা: কম নিশ্চিত output মানুষের কাছে পাঠানো।

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

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

ক্রমTeacher forcing-সহ seq2seq-এর একটি training ধাপ সঠিক ক্রমে সাজান।

  1. Padding-সহ src আর tgt-এর batch বানানো
  2. Encoder src পড়ে context vector দেয়
  3. Decoder context আর সঠিক আগের token নিয়ে প্রতিটি ধাপের logits দেয়
  4. Logits আর এক ঘর সরানো লক্ষ্য থেকে masked cross-entropy হিসাব
  5. Backward, gradient clipping আর optimizer step

একটি GRU seq2seq-এ batch ৩২, সবচেয়ে লম্বা input ২০ অক্ষর, hidden size ৬৪. নতুন একটি batch-এ সবচেয়ে লম্বা input ৫০ অক্ষর হলে context vector-এর আকার কী হবে (একস্তরের encoder)?

  • (৩২, ৫০, ৬৪)
  • (৩২, ৬৪)
  • (৩২, ১৬০)
  • (৫০)

লক্ষ্য sequence “<s> a b </s>” দিয়ে seq2seq শেখানো হচ্ছে। Decoder-এর ইনপুট আর লক্ষ্য কী হওয়া উচিত?

  • ইনপুট “<s> a b </s>”, লক্ষ্য “<s> a b </s>”
  • ইনপুট “a b”, লক্ষ্য “a b”
  • ইনপুট “<s> a b”, লক্ষ্য “a b </s>”
  • ইনপুট “a b </s>”, লক্ষ্য “<s> a b”

একটি decoder ধাপে model-এর probability: “x” ০.৭, “y” ০.২, “z” ০.১. সঠিক token “y”. Teacher forcing ratio ১.০ হলে এই ধাপের cross-entropy আর পরের ধাপের ইনপুট কী?

  • প্রায় ১.৬১; পরের ইনপুট “y”
  • প্রায় ১.৬১; পরের ইনপুট “x”
  • প্রায় ০.৩৬; পরের ইনপুট “y”
  • প্রায় ০.৩৬; পরের ইনপুট “x”

ref = “abcd” (৪ অক্ষর), hyp = “axcdef”. Character error rate কত?

  • ০.২৫
  • ০.৫০
  • ১.০০
  • ০.৭৫

একটি seq2seq-এর teacher-forced test loss খুব কম, কিন্তু greedy free-running-এ exact-match খুব খারাপ। সবচেয়ে সম্ভাব্য ব্যাখ্যা কোনটি?

  • Loss-এ ignore_index ব্যবহার করা হয়েছে
  • ভুল পরের ধাপে গড়ায়, আর exact-match একটি ভুলকেই পুরো ভুল ধরে
  • Encoder একদমই শেখেনি, তাই teacher forcing কাজ করেনি
  • Beam search ছাড়া কোনো model কখনো exact-match পায় না

একটি একই GRU দিয়ে দুটি model: (ক) প্রতিটি input অক্ষরের পাশেই output দেয়, (খ) পুরো input পড়ে একটি context থেকে output লেখে। এলোমেলো ৪০ অঙ্কের সংখ্যার অক্ষর-রূপান্তরে (ক) প্রায় নিখুঁত, (খ) প্রায় সবসময় ভুল। এতে কী বোঝা যায়?

  • GRU এই কাজের জন্য কোনোভাবেই উপযুক্ত নয়
  • কাজটি নিজেই খুব কঠিন, কোনো model পারবে না
  • সমস্যার বড় অংশ fixed context-এর পথে
  • Teacher forcing-ই ফলের একমাত্র কারণ

শব্দকোষ

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

মনে রাখুন

  1. কাঠামো: encoder পড়ে, context (B, H) পার করে, decoder লেখে; দৈর্ঘ্য স্বাধীন। Start আর end token দিয়ে শুরু ও থামা।
  2. Teacher forcing: ইনপুট খাতা থেকে, যা likelihood-এর সরাসরি রূপ; ব্যবহারে ইনপুট নিজের। মূল্যায়ন সবসময় greedy free-running.
  3. লক্ষ্য আর mask: tgt[:, :-1] ইনপুট, tgt[:, 1:] লক্ষ্য, ignore_index pad বাদ দেয়। Padding, packing আর mask তিনটি আলাদা কাজ।
  4. মাপ: exact-match কড়া, CER = Σd/Σ|ref| নরম, ১-এর বেশিও হতে পারে। দুটি দেখুন, দৈর্ঘ্য অনুযায়ী ভাগ করুন।
  5. Bottleneck: একই GRU পাশে-পাশে নিখুঁত, কিন্তু context দিয়ে পার করলে লম্বা input-এ ভেঙে পড়ে। h বাড়ালে সীমা সরে, যায় না।

পরের পর্বে: পর্ব ৩১-এ sequence পড়ার একটি ভিন্ন পথ দেখবেন: RNN ছাড়াই 1D convolution দিয়ে, time series-এর সৎ তুলনাসহ। আর পরের মডিউলের পর্ব ৩২-এ decoder পাবে এই পর্বের সমস্যার উত্তর: attention.

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

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