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

Transformer fine-tuning — classification, NER আর বাংলা ডেটা

Fine-Tuning Transformers — Classification, NER and Bangla Data

Taught in Bangla, this lesson fine-tunes pretrained encoders with Trainer, TrainingArguments, data collators and compute_metrics, with warmup and linear decay, per-epoch evaluation, early stopping and the best checkpoint. Real desktop runs on the GPU classify Bangla comments and tag NER spans with Bangla BERT against the earlier BiLSTM-CRF, align labels to subwords with word_ids() and -100, and measure catastrophic forgetting and learning rate by epoch sensitivity over seeds.

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

  • Trainer, TrainingArguments, data collator আর compute_metrics দিয়ে fine-tuning-এর সাধারণ রেসিপি, আর নিজের loop-এর সঙ্গে তার তফাত
  • Warmup + linear decay-এর হিসাব, eval strategy, early stopping আর সেরা checkpoint
  • word_ids() দিয়ে label-কে subword-এ বসানো, −১০০, আর span-এ ফেরা
  • Token classification-এ seqeval strict span F1, পর্ব ৪৩-এর CRF-এর পাশে
  • Catastrophic forgetting মাপা, আর lr × epoch-এর সংবেদনশীলতা কয়েকটি seed-এ

১ · একই রেসিপি, দুই ধরনের কাজ

বিয়েবাড়ির বাবুর্চির কথা ভাবুন। বিরিয়ানি হোক বা রেজালা, ধাপগুলো একই: উপকরণ কাটা, মশলা মাপা, আঁচ ঠিক রাখা, মাঝে মাঝে চেখে দেখা, ঠিক সময়ে চুলা থেকে নামানো। বদলায় শুধু উপকরণ আর শেষের পরিবেশন। Fine-tuning-ও তাই। পর্ব ৪৮-এ নিজের লেখা PyTorch loop দিয়ে একটি pretrained Bangla BERT-কে রিভিউয়ের sentiment শিখিয়েছিলেন, আর দেখেছিলেন TF-IDF-এর সঙ্গে পার্থক্য ধরা পড়ে না। এই পর্বে সেই রান্নাকে একটি সাধারণ রেসিপিতে বাঁধব, Hugging Face-এর Trainer দিয়ে, আর একই রেসিপি দুই ধরনের কাজে চালাব:

  • Sequence classification: পুরো লেখায় একটি লেবেল। পর্ব ৪২-এর বাংলা মন্তব্যের ডেটা, একই ভাগ।
  • Token classification: প্রতিটি শব্দে একটি লেবেল। পর্ব ৪৩-এর বাজারঘরের অর্ডার-বার্তার NER, যেখানে BiLSTM-CRF ০.৯৬৩ span F1 পেয়েছিল।

Fine-tuning-এর রেসিপি, এক নজরে

Same pipeline for both tasks; only the head and the labels change

Fine-tuning pipeline with Trainer Data: text + labeltrain / validation / test, split once Tokenizer + mapdo_lower_case=False, word_ids() Data collatorpad per batch, labels −100 Pretrained encoder + new headone label per text, or per token Trainer steploss, backward, AdamW, lr schedule Each epoch: evaluate, savecompute_metrics on validation Early stopping?no better for "patience" evals: stop Load best checkpointthen test once, never tune on it
ছবি ৫৪.১ — দুটি কাজেই একই ধাপ। Classification-এ head পুরো লেখার একটি vector নেয়; NER-এ প্রতিটি token-এর vector. ড্যাশ দেওয়া তীর: মূল্যায়নের পর আবার পরের epoch.

এই পর্বের সীমানা. Hub, datasets আর pipeline()-এর ভেতরের কথা পর্ব ৫৩ (Hugging Face ecosystem — transformers, datasets, tokenizers আর pipeline)-এ। কম parameter শিখিয়ে fine-tuning, যেমন LoRA, পর্ব ৫৫ (Parameter-efficient fine-tuning — LoRA, QLoRA আর adapter)-এ। এখানে পুরো model শেখে। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy-তে চলে; PyTorch আর transformers-এর সব আউটপুট ডেস্কটপের সত্যিকারের run, GPU-তে: Python 3.12.13, torch 2.15.0, transformers 5.22.2, datasets 5.1.0, accelerate 1.15.0, seqeval 1.2.2, লেখার সময়ে।

২ · Trainer-এর যন্ত্রাংশ — TrainingArguments, collator আর compute_metrics

Trainer চারটি জিনিস চায়: model, ডেটা, নিয়মের তালিকা (TrainingArguments), আর মূল্যায়নের একটি ফাংশন। নিচে এই পর্বের classification run-এর মূল অংশ। পুরো ফাইলে GPU lock, সময় মাপা আর log ছাপানোর লাইনও আছে; সেগুলো বাদ দিয়ে সংক্ষেপে:

tok = AutoTokenizer.from_pretrained(MODEL, do_lower_case=False)
def ds(idx):                                   # পর্ব ৪২-এর একই ভাগ
    d = Dataset.from_dict({"text": T[idx], "label": y[idx]})
    return d.map(lambda b: tok(b["text"], truncation=True, max_length=100),
                 batched=True, remove_columns=["text"])
def compute_metrics(p):                        # logits আর label আসে numpy-তে
    return {"macro_f1": f1_score(p.label_ids, p.predictions.argmax(-1), average="macro")}
args = TrainingArguments(
    output_dir="ckpt_cls", eval_strategy="epoch", save_strategy="epoch",
    save_total_limit=2, load_best_model_at_end=True,
    metric_for_best_model="macro_f1", greater_is_better=True,
    learning_rate=2e-5, weight_decay=0.01, warmup_steps=0.1,
    lr_scheduler_type="linear", num_train_epochs=6,
    per_device_train_batch_size=32, bf16=True, seed=42, report_to="none")
model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=2)
trainer = Trainer(model=model, args=args, train_dataset=ds(tr), eval_dataset=ds(va),
                  processing_class=tok, data_collator=DataCollatorWithPadding(tok),
                  compute_metrics=compute_metrics,
                  callbacks=[EarlyStoppingCallback(early_stopping_patience=2)])
trainer.train()
print(trainer.predict(ds(te)).metrics)         # test একবারই
Argumentকী করেভুল হলে যা হয়
eval_strategy="epoch"প্রতি epoch শেষে validation-এ মূল্যায়ন"no" থাকলে early stopping-এর কিছু দেখার থাকে না
save_strategyকখন checkpoint লেখা হবে; load_best_model_at_end-এর জন্য eval-এর সঙ্গে মিলতে হয়না মিললে Trainer শুরুতেই error দেয়
metric_for_best_modelকোন সংখ্যা দিয়ে সেরা checkpoint; না দিলে eval lossLoss আর F1 ভিন্ন epoch বাছতে পারে (অংশ ৫)
warmup_steps=0.1১-এর কম হলে মোট step-এর ভগ্নাংশ, ওপরের দিকে roundপুরোনো কোডের warmup_ratio এই version-এ নেই
weight_decayAdamW-এর decay; Trainer bias আর LayerNorm-এ দেয় নাঅনেক বড় দিলে pretrained weight শূন্যের দিকে সরে
bf16=Truebf16 autocast (পর্ব ১২)পুরোনো যে GPU-তে bf16 নেই, সেখানে চলবে না; তখন fp16 বা float32
seedHead-এর শুরুর weight, dropout আর shuffle-এর seedএকটি seed-এর ফলকে সাধারণ ফল ভাবা

Data collator batch বানায়। DataCollatorWithPadding প্রতিটি batch-কে শুধু তার সবচেয়ে লম্বা সারি পর্যন্ত pad করে (dynamic padding), পুরো ডেটাকে ১০০-তে নয়। Token classification-এর DataCollatorForTokenClassification একই কাজ করে, আর label-এর ফাঁকা ঘরে −১০০ বসায়। compute_metrics পায় পুরো validation-এর logits আর label, numpy array হিসেবে; ফেরত দেয় একটি dictionary. Trainer নামের আগে eval_ জুড়ে দেয়, তাই metric_for_best_model="macro_f1" মানে log-এর eval_macro_f1.

Version বদলায়, নথি পড়ুন. ইন্টারনেটের বহু উদাহরণে এখনো evaluation_strategy, warmup_ratio, overwrite_output_dir আর tokenizer=tok দেখবেন। এই lab-এর transformers 5.22.2-এর TrainingArguments-এ প্রথম তিনটি নেই; তা নিজে source খুলে যাচাই করা হয়েছে। Tokenizer এখন যায় processing_class-এ। AI assistant-ও প্রায়ই পুরোনো নাম লেখে।

Trainer বনাম নিজের loop. পর্ব ১০-এ যে loop লিখেছিলেন, Trainer তার ভেতরে সেটিই চালায়: model.train(), forward, loss, backward(), clipping (ডিফল্ট max_grad_norm=1.0), optimizer.step(), scheduler.step(), zero_grad(); তারপর model.eval() আর no_grad-এ মূল্যায়ন। পর্ব ৪৮-এর ft.py ঠিক এই ধাপগুলো হাতে লিখেছিল, সঙ্গে class weight আর layer-wise lr, যা Trainer-এ সরাসরি নেই। নিয়ম মোটামুটি এমন: সাধারণ রেসিপিতে Trainer কম ভুলের সুযোগ দেয়; loss, sampling বা optimizer-এর গঠন বদলাতে চাইলে নিজের loop, অথবা Trainer-এর compute_loss-এর মতো অংশ বদলানো। কোনোটিই “ঠিক পথ” নয়; Trainer যে কাজগুলো লুকিয়ে করে, সেগুলো জানা থাকলে দুটিতেই ভুল ধরা সহজ।

৩ · Learning rate-এর পথ — warmup আর linear decay

নতুন রিকশাচালক প্রথম দিনেই ঢাকার মোড়ে জোরে টান দেন না। প্রথমে আস্তে, হাত বসে গেলে পুরো গতি, গন্তব্য কাছে এলে ধীরে। Fine-tuning-এর শুরুতে head এলোমেলো, তাই প্রথম কয়েকটি gradient বড় আর এলোমেলো; সঙ্গে সঙ্গে পূর্ণ lr দিলে pretrained weight অকারণে নড়ে যেতে পারে। তাই warmup, তারপর linear decay. নীতিটি পর্ব ১৫-এ পড়েছেন। এখানে transformers-এর আসল সূত্র ধরে সংখ্যাগুলো দেখি, অংশ ৫-এর run-এর মাপে।

η(s) = ηmax · s / W, যখন s < W;   η(s) = ηmax · (N − s) / (N − W), নইলে

এখানে s হলো কততম optimizer step (০ থেকে শুরু), W warmup-এর step, N পরিকল্পিত মোট step, ηmax শীর্ষ learning rate. N আসে num_train_epochs থেকে, early stopping থেকে নয়।

import math
import matplotlib.pyplot as plt
def factor(step, warm, total):      # transformers-এর linear schedule-এর সূত্র
    if step < warm:
        return step / max(1, warm)
    return max(0.0, (total - step) / max(1, total - warm))

base, rows, bs, epochs = 2e-5, 7510, 32, 6
per_epoch = math.ceil(rows / bs)    # এক epoch-এ কত step
total = per_epoch * epochs
warm = math.ceil(0.1 * total)       # warmup = মোট step-এর ১০%
print(f"step/epoch {per_epoch} | total {total} | warmup {warm}")
for s in (0, 1, warm // 2, warm - 1, warm, per_epoch * 2, per_epoch * 4, total - 1):
    print(f"step {s:5d}  lr {base * factor(s, warm, total):.3e}")
for stop in (2, 3, 4):              # early stopping থামালে শেষ lr
    s = per_epoch * stop
    print(f"epoch {stop}-এ থামলে শেষ lr {base * factor(s, warm, total):.2e}"
          f" (শীর্ষের {factor(s, warm, total):.0%})")
lrs = [base * factor(s, warm, total) for s in range(total)]
print("গড় lr:", f"{sum(lrs) / total:.2e}", "| শুরুর step-এ lr:", lrs[0])
fig, ax = plt.subplots(figsize=(6, 2.6))
ax.plot(lrs, color="#2563eb")
for e in range(1, epochs):
    ax.axvline(e * per_epoch, color="#94a3b8", lw=0.6)
ax.set_xlabel("optimizer step"); ax.set_ylabel("learning rate")
ax.set_title("Linear warmup then linear decay (grey: epoch ends)")

আউটপুট পড়ুন: এক epoch-এ 235 step (৭৫১০/৩২, ওপরের দিকে), মোট 1410, warmup 141; অংশ ৫-এর run ঠিক এই সংখ্যা ছাপে। Step ০-এ lr 0: PyTorch-এর scheduler শুরুতেই সূত্রের s = ০ বসায়, তাই প্রথম update কার্যত শূন্য lr-এ। অংশ ৫-এর log মিলিয়ে দেখুন: “step 50 … lr 6.95e-06” = ২ × ১০−৫ × ৪৯/১৪১, অর্থাৎ ৫০তম update-এ s = ৪৯. শীর্ষ 2.000e-05 step ১৪১-এ। Early stopping epoch ৪-এ থামালে শেষ lr 7.41e-06, শীর্ষের 37%: শিডিউলের শেষ অংশ চলেই না। পুরো পথের গড় lr শীর্ষের ঠিক অর্ধেক, 1.00e-05, কারণ দুই অংশই সরলরেখা। ছবির ধূসর রেখাগুলো epoch-এর সীমা; প্রথম epoch-এর ২৩৫ step-এর ১৪১টিই warmup.

৪ · প্রতি epoch-এ পরীক্ষা, early stopping আর সেরা checkpoint

পরীক্ষার আগে বাসায় মডেল টেস্ট দেওয়ার কথা ভাবুন। প্রতি সপ্তাহে একটি মডেল টেস্ট, নম্বর খাতায় লেখা, সবচেয়ে ভালো সপ্তাহের নোটখাতাটি আলাদা করে রাখা। দুই সপ্তাহ টানা নম্বর না বাড়লে বুঝবেন, এখন শুধু মুখস্থ হচ্ছে; থামুন, আর সেই সেরা খাতা নিয়ে আসল পরীক্ষায় যান। Trainer-এর তিনটি সেটিং ঠিক এই তিনটি কাজ: eval_strategy হলো সাপ্তাহিক টেস্ট, load_best_model_at_end সেরা খাতা, আর EarlyStoppingCallback থামার নিয়ম। মূল্যায়ন হয় validation ভাগে; test থাকে একেবারে শেষের জন্য।

নিচের ব্লকে Trainer-এর কঙ্কাল, NumPy-তে, একটি logistic regression-এর ওপর। ৬০টি feature, কাজের মাত্র ৫টি, আর train মাত্র ৮০টি সারি: overfitting-এর জন্য আদর্শ জায়গা। মন্তব্যে প্রতিটি লাইনের Trainer-নাম দেওয়া।

import numpy as np
rng = np.random.default_rng(42)
n, d = 600, 60; w_true = np.zeros(d); w_true[:5] = 1.5   # ৬০টি feature, কাজের মাত্র ৫টি
X = rng.normal(size=(n, d))
y = (X @ w_true + rng.normal(0, 2, n) > 0).astype(float)
tr, va, te = np.arange(80), np.arange(200, 400), np.arange(400, 600)
def evaluate(w, s):                 # Trainer-এর evaluate(): loss আর accuracy
    p = 1 / (1 + np.exp(-(X[s] @ w)))
    loss = -np.mean(y[s] * np.log(p + 1e-9) + (1 - y[s]) * np.log(1 - p + 1e-9))
    return loss, np.mean((p > 0.5) == y[s])

w, lr, B, patience = np.zeros(d), 0.2, 16, 2
best, best_w, best_ep, bad, stop = np.inf, None, 0, 0, None
print("epoch train_loss  val_loss  val_acc")
for ep in range(1, 41):             # num_train_epochs = 40
    order = rng.permutation(tr)
    for i in range(0, len(order), B):          # এক epoch = ৫টি step
        j = order[i:i + B]
        p = 1 / (1 + np.exp(-(X[j] @ w)))
        w = w - lr * X[j].T @ (p - y[j]) / len(j)
    trl, _ = evaluate(w, tr)        # eval_strategy = "epoch"
    vl, vacc = evaluate(w, va)
    if stop is None:
        note = ""
        if vl < best:               # metric_for_best_model = "eval_loss"
            best, best_w, best_ep, bad, note = vl, w.copy(), ep, 0, "  * save best"
        else: bad += 1
        print(f"{ep:5d} {trl:10.3f} {vl:9.3f} {vacc:8.3f}{note}")
        if bad >= patience:         # EarlyStoppingCallback(patience=2)
            stop = ep; print(f"early stop, epoch {ep}")
print(f"না থামালে, epoch 40: train {trl:.3f} | val {vl:.3f}")
for name, ww in ((f"সেরা epoch {best_ep}", best_w), ("epoch 40", w)):
    l, acc = evaluate(ww, te)
    print(f"test, {name}: loss {l:.3f} acc {acc:.3f}")

আউটপুট পড়ুন: Validation loss সবচেয়ে কম epoch ৪-এ (0.550), তারপর দুবার উন্নতি নেই, তাই epoch ৬-এ থামা; train loss তখনও কমছে (0.211). দুই loss-এর ফাঁক বাড়াই overfitting-এর চিহ্ন। থামানো না হলে epoch ৪০-এ train 0.059, কিন্তু val 0.741. আর test-এ মজার জিনিস: দুই অবস্থাতেই accuracy 0.730, অথচ loss 0.534 বনাম 0.759. অর্থাৎ বেশি শেখানো model ভুলের সংখ্যা বাড়ায়নি, কিন্তু অনেক বেশি আত্মবিশ্বাসী হয়েছে। Accuracy বা F1 দেখে বাছলে এই ক্ষতি চোখেই পড়ত না; তাই metric_for_best_model আগে ভেবে ঠিক করুন। Patience ছোট হলে আগে থামে, কিন্তু validation শব্দময় হলে ভুল করে থামার ঝুঁকিও বাড়ে।

৫ · Classification — পর্ব ৪২-এর ভাগে Trainer, আর log পড়া

এবার আসল run. পর্ব ৪২-এর ডেটা (Salim Sazzed-এর YouTube নাটকের বাংলা মন্তব্য, Mendeley DOI 10.17632/p6zc7krs37.4, CC BY 4.0) আর সেই একই StratifiedGroupKFold ভাগ, repeat ০-এর fold ০ আর ১. Checkpoint sagorsarker/bangla-bert-base (card-এ MIT, ২০২৬ সালের নভেম্বরে দেখা), do_lower_case=False, কারণ পর্ব ৪৮-এ দেখেছেন ডিফল্ট tokenizer হসন্ত আর উ-কার মুছে দেয়। ১৫টি ভাগের পুরো তুলনা পর্ব ৪৮-এ হয়ে গেছে; এখানে লক্ষ্য রেসিপি আর log পড়া, নতুন রায় নয়। সত্যিকারের run, GPU-তে; log-এর লাইন ছাঁটা, শেষের সারাংশ নিজের ছাপানো:

~/bazarghar-dl$ uv run python trainer_cls.py --fold 0
train/val/test 7510 1056 2143 | columns ['label', 'input_ids', 'token_type_ids', 'attention_mask']
step   50 epoch 0.21 loss 0.6520 lr 6.95e-06
step  150 epoch 0.64 loss 0.1739 lr 1.99e-05
step  200 epoch 0.85 loss 0.1989 lr 1.91e-05
EVAL epoch 1 eval_loss 0.1591 eval_macro_f1 0.9226
step  450 epoch 1.91 loss 0.1338 lr 1.51e-05
EVAL epoch 2 eval_loss 0.1354 eval_macro_f1 0.9416
step  700 epoch 2.98 loss 0.0632 lr 1.12e-05
EVAL epoch 3 eval_loss 0.1975 eval_macro_f1 0.9372
step  900 epoch 3.83 loss 0.0265 lr 8.05e-06
EVAL epoch 4 eval_loss 0.2575 eval_macro_f1 0.9388
best checkpoint: checkpoint-470 best metric 0.9416
global steps 940 of planned 1410
test macro-F1 0.9409 | train 123 s | peak alloc 3309 MiB
~/bazarghar-dl$ uv run python trainer_cls.py --fold 1   # শেষ তিন লাইন
best checkpoint: checkpoint-705 best metric 0.9392
global steps 1175 of planned 1410
test macro-F1 0.9332 | train 154 s | peak alloc 3309 MiB
fold ০ · ৬ epoch পরিকল্পিত · patience ২
সময়
train loss
eval_loss
eval F1
patience
test
  1. Epoch 1. প্রথম ১৪১ step warmup, lr শূন্য থেকে ২ × ১০−৫-এ ওঠে। Train loss ০.৬৫ থেকে ০.২-এ নামে: head এলোমেলো থেকে কাজ শিখছে। Validation F1 ০.৯২২৬.
  2. Epoch 2. দুটি সংখ্যাই সেরা: eval_loss সবচেয়ে কম, F1 সবচেয়ে বেশি। Trainer এই অবস্থাকে checkpoint-470 নামে রাখে; ২৩৫ × ২ = ৪৭০ step.
  3. Epoch 3. Train loss অর্ধেক হয়ে ০.০৬৩, কিন্তু eval_loss বেড়ে ০.১৯৭৫. F1 সামান্য কমে। উন্নতি নেই, তাই patience ১/২.
  4. Epoch 4. Train loss আরও কমে ০.০২৬, eval_loss আরও বাড়ে ০.২৫৭৫. এটি overfitting-এর ক্লাসিক ছবি: ফাঁক বাড়ছে। F1 ০.৯৩৮৮, সেরার নিচে; patience ২/২.
  5. থামা. পরিকল্পিত ১,৪১০-এর মধ্যে ৯৪০ step-এ training থামে। মানে lr শিডিউলের শেষ অংশ কখনো চলেনি; শেষ lr প্রায় ৭.৪ × ১০−৬, শূন্য নয় (অংশ ৩).
  6. শেষে. load_best_model_at_end epoch ৪-এর নয়, epoch ২-এর weight ফেরত আনে। এরপর test, একবার: ০.৯৪০৯. পর্ব ৪৮-এর নিজের loop-এ এই একই fold-এ ছিল ০.৯৩৮৫, TF-IDF ০.৯৪১২.
  7. রায়. F1 প্রায় সমতল থাকলেও loss বাড়ছে মানে model ভুলগুলোতে আরও জোর দিয়ে ভুল বলছে, অর্থাৎ আত্মবিশ্বাস বাড়ছে, নির্ভুলতা নয়। Probability দরকার হলে loss দিয়ে বাছা নিরাপদ (মেশিন লার্নিং কোর্সের পর্ব ২৬)।
ভাগTrainer (এই পর্ব)নিজের loop (পর্ব ৪৮)TF-IDF + LRসেরা epoch, থামা
fold ০০.৯৪০৯০.৯৩৮৫০.৯৪১২২, epoch ৪-এ
fold ১০.৯৩৩২০.৯৩৬০০.৯৩৩৭৩, epoch ৫-এ

তিনটি পদ্ধতি দুই ভাগেই ±০.০০৩-এর মধ্যে। পর্ব ৪৮ ১৫টি ভাগ দিয়ে দেখিয়েছিল, এই মাপের পার্থক্য এই ডেটায় আলাদা করা যায় না; দুটি ভাগ তাই কোনো নতুন রায় দেয় না, শুধু দেখায় যে রেসিপি ঠিকঠাক চলছে। দুই loop হুবহু এক নয়: এখানে class weight নেই, epoch ৬ আর early stopping, পর্ব ৪৮-এ ছিল class weight আর ৩ epoch. Peak allocation এখানে প্রায় ৩.২ GiB, পর্ব ৪৮-এ প্রায় ৩.৯; কোন পার্থক্যের জন্য, তা আলাদা করে মাপা হয়নি। দুই ভাগেই একই ছবি: সেরা epoch ২ বা ৩, তারপর eval loss বাড়ে।

৬ · Subword আর label — word_ids, প্রথম টুকরো বনাম সব টুকরো, −১০০

NER-এ লেবেল দেওয়া থাকে শব্দে শব্দে, অথচ model দেখে subword. ধরুন দর্জির দোকানে মাপ নেওয়া হয়েছে পুরো জামার, কিন্তু কাটা হবে কয়েক টুকরো কাপড়ে; প্রতিটি টুকরোতে কোন মাপ লিখবেন? পর্ব ৪৩-এর শেষে প্রশ্নটি খোলা রাখা হয়েছিল। উত্তরের চাবি fast tokenizer-এর word_ids(): প্রতিটি token কোন শব্দ থেকে এসেছে তার সূচক, আর [CLS]/[SEP]-এ None. শব্দগুলো আগে থেকেই ভাঙা থাকলে tok(words, is_split_into_words=True) দিতে হয়। সত্যিকারের run, বাজারঘরের একটি কাল্পনিক অর্ডার, Bangla BERT-এর tokenizer:

~/bazarghar-dl$ uv run python tok_demo.py
fast tokenizer: True
নারায়ণগঞ্জে হাতে-বোনা লাল শাড়ি পাঠান , দাম ৳২৫০০
 tokens   ['[CLS]', 'নারায়ণগঞ্জে', 'হাতে', '-', 'বোনা', 'লাল', 'শাড়ি', 'পাঠান', ',', 'দাম', '[UNK]', '[SEP]']
 word_ids [None, 0, 1, 1, 1, 2, 3, 4, 5, 6, 7, None]
পঞ্চগড়ে খদ্দরের ফতুয়া পাঠান , দাম ১,৫৫০ টাকা
 tokens   ['[CLS]', 'পঞ্চগড়ে', 'খদ্দর', '##ের', 'ফতুয়া', 'পাঠান', ',', 'দাম', '১', ',', '[UNK]', 'টাকা', '[SEP]']
 word_ids [None, 0, 1, 1, 2, 3, 4, 5, 6, 6, 6, 7, None]
train: words 3405 | subwords 3605 | ratio 1.06 | words split into >1 piece 136 (4.0%) | [UNK] 241
test: words 5540 | subwords 6004 | ratio 1.08 | words split into >1 piece 329 (5.9%) | [UNK] 386

দুটি জিনিস লক্ষ করুন। “হাতে-বোনা” তিন টুকরো, কারণ হাইফেন আলাদা হয়; “খদ্দরের” দুই টুকরো, ## মানে আগের টুকরোর সঙ্গে জোড়া (পর্ব ৪০)। আর দামের ঘরে [UNK]. এরপর দুটি নিয়মে label বসানো, ধাপে ধাপে:

পঞ্চগড়ে খদ্দরের ফতুয়া পাঠান , দাম ১,৫৫০ টাকা
token
word_id
first
all
  1. [CLS]. word_id None, কোনো শব্দ নয়। Label −১০০, অর্থাৎ loss-এ বাদ: PyTorch-এর CrossEntropyLoss-এর ডিফল্ট ignore_index −১০০.
  2. পঞ্চগড়ে. এক টুকরোর শব্দ, word_id ০. দুই নিয়মেই শব্দের নিজের label, B-DIST.
  3. খদ্দর. শব্দ ১-এর প্রথম টুকরো। আগের word_id ছিল ০, এখন ১: নতুন শব্দ শুরু। দুই নিয়মেই B-PROD.
  4. ##ের. word_id আবার ১, একই শব্দের বাকি অংশ। “first” নিয়মে −১০০, loss-এ নেই। “all” নিয়মে label বসে, কিন্তু B-PROD নয়, I-PROD: একটি span-এ দুটি B থাকলে দুটি আলাদা পণ্য বোঝাত।
  5. ফতুয়া. শব্দ ২, নিজের label I-PROD. “খদ্দরের ফতুয়া” একটি পণ্যের span.
  6. পাঠান , দাম. তিনটি এক টুকরোর শব্দ, সবই O. Label-এর বেশির ভাগই এমন O.
  7. ১. “১,৫৫০” শব্দের প্রথম টুকরো, B-PRICE. শব্দটি তিন টুকরো হয়েছে: “১”, “,” আর একটি [UNK].
  8. , [UNK]. একই শব্দের বাকি দুই টুকরো। “first”-এ −১০০, “all”-এ I-PRICE. লক্ষ করুন, “৫৫০” অংশটি [UNK] হয়ে হারিয়েছে; model শুধু জানে এখানে কোনো অজানা জিনিস।
  9. টাকা. শব্দ ৭, I-PRICE. “১,৫৫০ টাকা” একটি দামের span.
  10. [SEP], আর হিসাব. আবার −১০০. “first”-এ loss-এ ৮টি অবস্থান, ঠিক শব্দের সংখ্যা; “all”-এ ১১টি. অর্থাৎ “all” নিয়মে বহু টুকরোর শব্দ loss-এ বেশি ওজন পায়। মূল্যায়ন দুই ক্ষেত্রেই শব্দে, প্রথম টুকরোর prediction ধরে।

এবার একই কাজ কোডে, সঙ্গে উল্টো দিকটিও: subword-এর prediction থেকে শব্দ আর span-এ ফেরা। Prediction-টি কাল্পনিক, একটি সাধারণ ভুল দেখানোর জন্য বানানো: ##ের-এ model O বলেছে।

words = ["পঞ্চগড়ে", "খদ্দরের", "ফতুয়া", "পাঠান", ",", "দাম", "১,৫৫০", "টাকা"]
tags = ["B-DIST", "B-PROD", "I-PROD", "O", "O", "O", "B-PRICE", "I-PRICE"]
# Bangla BERT-এর আসল tokenizer-এর আউটপুট (ডেস্কটপে ছাপানো, হুবহু)
toks = ["[CLS]", "পঞ্চগড়ে", "খদ্দর", "##ের", "ফতুয়া", "পাঠান", ",", "দাম",
        "১", ",", "[UNK]", "টাকা", "[SEP]"]
word_ids = [None, 0, 1, 1, 2, 3, 4, 5, 6, 6, 6, 7, None]

def align(word_ids, tags, mode):
    labels, prev = [], None
    for w in word_ids:
        if w is None: labels.append(-100)            # [CLS], [SEP]
        elif w != prev: labels.append(tags[w])       # শব্দের প্রথম টুকরো
        elif mode == "first": labels.append(-100)    # বাকি টুকরো loss-এ নেই
        else: labels.append("O" if tags[w] == "O" else "I-" + tags[w][2:])
        prev = w
    return labels

def spans(words, tags):                              # strict BIO: I- জোড়ে শুধু একই ধরনের পরে
    out, prev = [], "O"
    for w, t in zip(words, tags):
        if t[:2] == "B-": out.append([t[2:], w])
        elif t[:2] == "I-" and prev[2:] == t[2:]: out[-1][1] += " " + w
        prev = t if (t[:2] == "B-" or prev[2:] == t[2:]) else "O"
    return [tuple(s) for s in out]

for mode in ("first", "all"):
    lab = align(word_ids, tags, mode)
    print(mode, "| loss-এ ঢোকে", sum(l != -100 for l in lab), "টি অবস্থান")
    print("  ", [f"{t}:{l}" for t, l in zip(toks, lab)][7:12])
# কাল্পনিক prediction, প্রতিটি subword-এ একটি tag
pred = ["O", "B-DIST", "B-PROD", "O", "I-PROD", "O", "O", "O",
        "B-PRICE", "O", "I-PRICE", "I-PRICE", "O"]
word_pred = [pred[word_ids.index(i)] for i in range(len(words))]
print("শব্দে ফেরত:", word_pred == tags, "| span:", spans(words, word_pred))
print("subword ধরে পড়লে:", spans(toks, pred))

আউটপুট পড়ুন: “first”-এ loss-এ যায় 8টি অবস্থান, ঠিক শব্দের সংখ্যা; “all”-এ 11টি. “১,৫৫০”-এর তিন টুকরোয় প্রথম নিয়মে B-PRICE, -100, -100, দ্বিতীয়তে B-PRICE, I-PRICE, I-PRICE. এরপর উল্টো পথ: কাল্পনিক prediction-এ ##ের-এ O, তবু শব্দে ফেরার পর tag পুরো মিলেছে (True), কারণ শব্দের tag নেওয়া হয় শুধু প্রথম টুকরো থেকে। Span তিনটি ঠিক: খদ্দরের ফতুয়া আর ১,৫৫০ টাকা. শেষ লাইনে একই prediction subword ধরে পড়লে পণ্য হয় শুধু খদ্দর, দাম শুধু ১: strict নিয়মে দুটিই ভুল span. তাই মূল্যায়ন আর আউটপুট দুটিই শব্দে, পর্ব ৪৩-এর মতো।

এই checkpoint-এ সংখ্যা মানেই [UNK]. ওপরের run-এ train-এর ২৪১টি [UNK]-এর উৎস খুঁজতে আলাদা পরীক্ষা চালানো হলো। সত্যিকারের run:

২৫০০ ['[UNK]'] | ৳২৫০০ ['[UNK]'] | ৳ ['৳'] | ১৫০০ ['[UNK]'] | 1500 ['[UNK]'] | ১ ['১'] | ৫ ['৫']
unk words, tags: {'B-PRICE': 536, 'O': 91}
vocab 101975 | '##0' False | '##০' False | '15' False | '১৫' False | '2020' False
0 ## pieces with digits

(কয়েকটি লাইন এক সারিতে সাজানো.) Vocabulary-তে এক অঙ্কের token আছে, কিন্তু অঙ্কের কোনো ## টুকরো বা দুই অঙ্কের token নেই, বাংলা বা ইংরেজি কোনো হরফেই। তাই দুই বা বেশি অঙ্কের যেকোনো সংখ্যা পুরোটাই [UNK]. দাম চেনার কাজটি তাই চলে প্রসঙ্গ দেখে: “দাম [UNK] টাকা” বনাম “চার্জ [UNK] টাকা”. ক্ষতিটি চুপচাপ: কোনো error নেই, শুধু টাকার অঙ্কটি model-এর কাছে অদৃশ্য। অঙ্ক কত, সেটি দরকার হলে span খুঁজে মূল লেখা থেকে নিয়মে পড়ুন, অথবা অন্য tokenizer-এর model মাপুন। এটি এই একটি checkpoint-এর vocabulary-র মাপ; অন্য বাংলা model-এ ছবি আলাদা হতে পারে।

৭ · বাজারঘরের NER — Bangla BERT বনাম পর্ব ৪৩-এর CRF

ডেটা হুবহু পর্ব ৪৩-এর: একই seed-এর generator, পুরোপুরি কাল্পনিক অর্ডার-বার্তা, train ৩০০, dev ২০০, test ৫০০ বাক্য। Test-এ কিছু জেলা, পণ্য আর বিশেষণ train-এ একবারও নেই। মনে রাখুন, template train আর test-এ একই, তাই কোনো সংখ্যাই আসল গ্রাহকের লেখায় খাটবে না। বদল শুধু model-এ: AutoModelForTokenClassification, ৭টি label, lr ৫ × ১০−৫, batch ১৬, সর্বোচ্চ ১০ epoch, warmup ১০%, early stopping patience ৩, সেরা checkpoint বাছাই dev-এর strict span F1-এ। compute_metrics প্রতিটি শব্দের প্রথম টুকরোর prediction নিয়ে শব্দে ফেরে, তারপর seqeval-এর mode="strict", scheme=IOB2, পর্ব ৪৩-এর নিয়মেই। পাঁচটি seed, পর্ব ৪৩-এর মতোই ১ থেকে ৫. সত্যিকারের run, GPU-তে; BiLSTM-এর সারিগুলো পর্ব ৪৩-এর রেকর্ড করা ফল থেকে পড়া:

~/bazarghar-dl$ uv run python ner_summary.py
model            seeds  spanF1     sd    min   DIST   PROD  PRICE   sec
BiLSTM+softmax       5   0.952  0.007  0.940  0.968  0.898  1.000   3.6
BiLSTM+CRF           5   0.963  0.007  0.951  0.978  0.920  0.999   5.8
BERT first           5   0.984  0.015  0.961  0.993  0.962  1.000  20.9
BERT all             5   0.989  0.010  0.976  0.997  0.975  0.998  26.7
per seed test, first: [0.961, 0.993, 0.985, 0.979, 1.0]
per seed test, all  : [0.993, 0.996, 0.976, 0.981, 0.999]
all - first per seed: [0.032, 0.003, -0.008, 0.002, -0.001]
best epoch (first): ['checkpoint-114', 'checkpoint-57', 'checkpoint-76', 'checkpoint-57', 'checkpoint-95'] | 19 steps per epoch
peak alloc MiB: 2511
  • BERT এগিয়ে, তবে ছড়ানো বড়. গড় span F1 ০.৯৮৪, CRF-এর ০.৯৬৩. কিন্তু seed-ভেদে ০.৯৬১ থেকে ১.০০০: সবচেয়ে খারাপ seed-টি CRF-এর গড়ের প্রায় সমান। BiLSTM-এর sd ০.০০৭, BERT-এর ০.০১৫. ৩০০ বাক্যে এক epoch মাত্র ১৯ step; সেরা checkpoint epoch ৩ থেকে ৬-এ ছড়ানো।
  • লাভ প্রায় পুরোটাই পণ্যে: PROD ০.৯২০ থেকে ০.৯৬২. অচেনা শব্দ বেশি থাকে এখানেই; subword আর pretrained প্রসঙ্গ পর্ব ৪৩-এর UNK-সমস্যা অনেকটা মেটায়। পর্ব ৪৩-এ দুই BiLSTM-ই যে “জামালপুরে শাড়ি…” আর “টিফিন-বক্স ১,১৫০…” বাক্যে ভুল করেছিল, BERT-এর সবচেয়ে খারাপ seed-ও সেগুলো ঠিক ধরেছে।
  • প্রথম টুকরো বনাম সব টুকরো: গড়ে ০.৯৮৪ বনাম ০.৯৮৯, কিন্তু প্রতি seed-এর পার্থক্য −০.০০৮ থেকে +০.০৩২; লাভের প্রায় সবটা একটি seed-এর। এই ডেটায় শব্দের মাত্র ৪–৬% একাধিক টুকরো, তাই নিয়মের প্রভাব ছোট হওয়ারই কথা। পাঁচ seed-এ এটিকে কোনো পক্ষের জয় বলা যায় না।
  • খরচ: BERT GPU-তে প্রতি run প্রায় ২১–২৭ সেকেন্ড, peak allocation প্রায় ২.৫ GiB; BiLSTM CPU-তে ৪–৬ সেকেন্ড, ভিন্ন যন্ত্রে। তুলনাটি তাই মোটা দাগের। আর BERT-এর checkpoint একাই float32-এ প্রায় ৬৫৮ MB (পর্ব ৪৮-এর মাপ)।

বাকি ভুল কোথায়? সবচেয়ে খারাপ seed (১)-এর ৩৬টি ভুল token-এর হিসাব। সত্যিকারের run; আউটপুট ছাঁটা, কয়েকটি ঘর এক লাইনে সাজানো:

~/bazarghar-dl$ uv run python errs2.py   # (আগের শব্দ, শব্দ, gold, pred), প্রথম কয়েকটি
(('যশোরের', 'খেজুর', 'I-PROD', 'B-PROD'), 13), (('খদ্দরের', 'টিফিন-বক্স', 'I-PROD', 'B-PROD'), 8),
(('', 'নাটোরের', 'B-PROD', 'B-DIST'), 7), (('খদ্দরের', 'গামছা', 'I-PROD', 'B-PROD'), 2), …
খদ্দরের টিফিন-বক্স ৪২৫০ টাকা , ঠিকানা কক্সবাজার সদর
 gold B-PROD I-PROD B-PRICE I-PRICE O O B-DIST O
 bert B-PROD B-PROD B-PRICE I-PRICE O O B-DIST O

প্রায় সব ভুল একটি জায়গায়: “-এর” বিভক্তির পরে. “যশোরের খেজুর গুড়” আর “নাটোরের কাঁচাগোল্লা”-র মতো পণ্য test-এ নতুন; model জেলার নাম দেখে DIST বলেছে বা span ভেঙেছে। “খদ্দরের” (test-এর নতুন বিশেষণ)-এর পরেও একই ভাঙন। অর্থাৎ model শিখেছে “ষষ্ঠী বিভক্তির পরে নতুন টুকরো শুরু হয়”, যা train-এর বেশির ভাগ বাক্যে সত্য ছিল। এই ভুল আরও epoch-এ সারবে না; লাগবে এমন উদাহরণ, যেখানে “-এর”-যুক্ত শব্দ পণ্যের ভেতরে। আলাদা করে গুনলে অবৈধ ক্রম মাত্র ৭টি, সবই “নাটোরের কাঁচাগোল্লা”-য়: B-DIST-এর ঠিক পরে I-PROD. বাকি ২৯টি ভুল বৈধ ক্রমে, তাই CRF বা নিয়ম-বাঁধা decoding বড়জোর ওই ৭টিতে সাহায্য করতে পারত।

def encode(words, tags, mode="first"):
    enc = tok(words, is_split_into_words=True, truncation=True, max_length=128)
    labels, prev = [], None
    for w in enc.word_ids():
        if w is None: labels.append(-100)
        elif w != prev: labels.append(T2I[tags[w]])
        elif mode == "first": labels.append(-100)
        else: labels.append(T2I["O" if tags[w] == "O" else "I-" + tags[w][2:]])
        prev = w
    return {**enc, "labels": labels}
def metrics(p):                         # dev-এর প্রতিটি শব্দের প্রথম টুকরো, শব্দে ফেরা
    pred = p.predictions.argmax(-1)
    words = [[TAGS[pred[i, k]] for k in first] for i, first in enumerate(first_pos_dev)]
    return {"span_f1": f1_score(gold_dev, words, mode="strict", scheme=IOB2)}
model = AutoModelForTokenClassification.from_pretrained(MODEL, num_labels=7)
collator = DataCollatorForTokenClassification(tok)   # label-এর padding −100

ওপরের কোড পুরো ফাইল থেকে ছোট করা; আসল ফাইলে encode একটি তালিকার ওপর চলে আর প্রথম টুকরোর অবস্থান (first_pos_dev) আলাদা রাখে, কারণ Trainer-এর compute_metrics শুধু logits আর label পায়, word_ids পায় না। Eval-এ Trainer ক্রম বদলায় না, তাই অবস্থানের তালিকা মিলে যায়।

৮ · Catastrophic forgetting — নতুন কাজ শিখতে গিয়ে কী হারায়

পাড়ার এক দক্ষ দর্জি, যিনি সব রকম জামা বানাতেন, এক কারখানায় ঢুকে ছয় মাস শুধু একই মাপের শার্টের কলার সেলাই করলেন। কলারে তিনি এখন দ্রুত আর নিখুঁত, কিন্তু পাঞ্জাবির মাপ নিতে বললে হাত আগের মতো চলে না। Neural network-এর এই ঝোঁকের নাম catastrophic forgetting: একই weight নতুন কাজের জন্য সরলে আগের কাজের দক্ষতা কমে। প্রথমে একটি খেলনা model-এ দেখি। একটি সাধারণ কাজ A-তে logistic regression শেখানো, তারপর ছোট একটি নতুন কাজ B-তে fine-tune.

import numpy as np
rng = np.random.default_rng(0)
def task(angle, n=400):             # দুই শ্রেণি; সীমারেখার দিক angle ডিগ্রি
    a = np.deg2rad(angle); u = np.array([np.cos(a), np.sin(a)])
    X = rng.normal(size=(n, 2)) * 2
    return np.c_[X, np.ones(n)], (X @ u > 0).astype(float)
XA, yA = task(0)                    # "pretraining": বড়, সাধারণ কাজ
XB, yB = task(70, n=40)             # "fine-tuning": ছোট, নতুন কাজ
XA2, yA2 = task(0); XB2, yB2 = task(70)   # দুই কাজের আলাদা test

def acc(w, X, y): return np.mean(((X @ w) > 0) == y)
def grad(w, X, y): return X.T @ (1 / (1 + np.exp(-(X @ w))) - y) / len(y)

w0 = np.zeros(3)
for _ in range(500):
    w0 -= 0.5 * grad(w0, XA, yA)
print(f"pretrained: A {acc(w0, XA2, yA2):.3f} | B {acc(w0, XB2, yB2):.3f}")

def finetune(lr, steps, lam=0.0):   # lam: শুরুর weight-এর দিকে টান
    w = w0.copy()
    for _ in range(steps):
        w -= lr * (grad(w, XB, yB) + lam * (w - w0))
    return w
print("   lr  steps  lam   A_acc  B_acc  |w-w0|")
for lr, steps, lam in [(0.05, 20, 0), (0.05, 200, 0), (0.5, 200, 0),
                       (0.5, 2000, 0), (0.5, 2000, 0.05)]:
    w = finetune(lr, steps, lam)
    print(f"{lr:5} {steps:6} {lam:4}  {acc(w, XA2, yA2):.3f}  "
          f"{acc(w, XB2, yB2):.3f}  {np.linalg.norm(w - w0):6.2f}")

আউটপুট পড়ুন: শুরুর model কাজ A-তে 0.998, B-তে 0.603. ছোট lr-এ ২০ step: B প্রায় শেখেনি, A প্রায় অক্ষত (0.983). Step বা lr বাড়ালে B ওঠে 0.970-এ, কিন্তু A নামে 0.570-এ, প্রায় আন্দাজের সমান। শেষ কলাম দেখায় weight শুরু থেকে কত দূরে সরল; forgetting সেই দূরত্বের সঙ্গে বাড়ে। শেষ সারিতে শুরুর weight-এর দিকে একটি টান (λ = ০.০৫) দূরত্ব প্রায় অর্ধেক রাখে: A 0.705, B 0.905, দুটির মাঝামাঝি একটি আপস। খেলনাটি ইচ্ছা করে কঠিন: দুই কাজের সীমারেখা ৭০ ডিগ্রি আলাদা, আর তিনটি weight-এর একটি সরলরেখা দুটিই পারে না। বড় network-এ ধারণক্ষমতা অনেক বেশি, তাই এত তীব্র হয় না; কিন্তু “নতুন কাজের দিকে যত সরবেন, আগের কাজ তত হারাবেন” প্রবণতাটি একই।

আসল model-এ মাপ. BERT-এর আগের কাজ ছিল masked language modeling (পর্ব ৪৭)। তাই প্রশ্ন: fine-tuning-এর পর encoder কি আগের মতো ঢাকা শব্দ অনুমান করতে পারে? পদ্ধতি: checkpoint-এর আসল, অপরিবর্তিত MLM head নিয়ে তার নিচে fine-tuned encoder-এর weight বসানো, তারপর একই লেখায় একই mask-এ loss. লেখা দুটি: পর্ব ৪৬-এর উপেন্দ্রকিশোর রায়চৌধুরীর গল্পের val আর test ভাগ (বাংলা উইকিসংকলন, পাতায় PD-India ট্যাগ), আর পর্ব ৪২-এর test ভাগের ৬০০টি মন্তব্য। ১২৮ token-এর টুকরো, seed ০-এ বাছা ১৫% token, সবগুলো [MASK] দিয়ে ঢাকা (BERT-এর ৮০/১০/১০ নিয়ম নয়, যাতে তুলনা স্থির থাকে)। সংখ্যাটি তাই আপেক্ষিক মাপ, pretraining-এর loss-এর সঙ্গে সরাসরি তুলনীয় নয়। সত্যিকারের run, GPU-তে:

~/bazarghar-dl$ uv run python forget.py ft_cls_f0 ft_ner
masked tokens: stories 1674 | comments 1494
pretrained             stories 4.763 | comments 6.145
ft_cls_f0              stories 5.143 | comments 6.806
ft_ner                 stories 5.106 | comments 6.390

দুটি fine-tuning-এর পরই গল্পের MLM loss প্রায় ০.৩৪–০.৩৮ বেড়েছে। চমকের জায়গা মন্তব্যের সারি: sentiment model ঠিক এই ধরনের মন্তব্যে শিখেছে, তবু সেখানেই loss সবচেয়ে বেশি বেড়েছে, ৬.১৪৫ থেকে ৬.৮০৬. কারণ শেখার লক্ষ্য বদলেছে: encoder এখন মন্তব্যকে “ধনাত্মক না ঋণাত্মক” প্রশ্নের দিকে সাজায়, ঢাকা শব্দ খোঁজার দিকে নয়। একই ডেটা, ভিন্ন কাজ, তাই forgetting “ডেটা নতুন কি না”-র প্রশ্ন নয়, “কাজ বদলেছে কি না”-র প্রশ্ন। মাপগুলো একটি করে run, একটি mask-এর; ছোট পার্থক্যে ভরসা করবেন না। পরের অংশের grid দেখায়, lr বাড়ালে এই বৃদ্ধি কত দ্রুত বাড়ে।

প্রতিকারের পথ চারটি, সবগুলোই “কম সরা”-র রূপ: ছোট lr আর কম step; শুরুর weight-এর দিকে টান, যেমন L2-SP (খেলনার শেষ সারি); পুরোনো কাজের ডেটা বা loss মিশিয়ে রাখা, rehearsal; আর নিচের layer জমিয়ে রাখা বা মূল weight না ছুঁয়ে ছোট adapter শেখানো, যা পর্ব ৫৫-এর বিষয়।

৯ · lr × epoch, seed, আর কী freeze করবেন

Hyperparameter-এর সংবেদনশীলতা মাপার সৎ উপায় একটি ছোট grid, কয়েকটি seed-এ। NER-এর একই ডেটায় চারটি lr আর দুটি epoch-সংখ্যা, দুটি seed, মোট ১৬টি run. এখানে early stopping নেই: ঠিক ৩ বা ১০ epoch, শেষ checkpoint, যাতে epoch-এর প্রভাব আলাদা দেখা যায়। প্রতিটির পর আগের অংশের MLM মাপও। সত্যিকারের run, GPU-তে, প্রতিটি run ৪–১৩ সেকেন্ড:

~/bazarghar-dl$ uv run python grid_summary.py
pretrained MLM loss (stories) 4.763
     lr  ep  dev s1  dev s2 test s1 test s2  MLM s1  MLM s2
  1e-05   3   0.892   0.893   0.878   0.893    4.83    4.86
  1e-05  10   0.966   0.973   0.969   0.977    4.94    4.98
  3e-05   3   0.943   0.981   0.944   0.986    4.97    5.00
  3e-05  10   0.976   0.985   0.984   0.991    5.02    5.11
  1e-04   3   0.967   0.988   0.948   0.983    5.34    5.24
  1e-04  10   0.984   0.976   0.978   0.977    5.28    5.36
  3e-04   3   0.940   0.973   0.935   0.966    6.29    6.71
  3e-04  10   0.921   0.951   0.924   0.947    7.41    8.60
best mean dev: 3e-05 10
  • খুব কম lr, কম epoch (১০−৫, ৩): dev ০.৮৯, underfitting; মাত্র ৫৭ step-এ model কাজটি শেখেনি। একই lr-এ ১০ epoch দিলে ০.৯৭.
  • খুব বেশি lr (৩ × ১০−৪): ফল নামে, আর ১০ epoch দিলে আরও খারাপ। MLM loss ৪.৭৬ থেকে ৮.৬০ পর্যন্ত: encoder-এর আগের জ্ঞান প্রায় ভেঙে পড়েছে। ছোট model-এর অভ্যাসে অনেকে ১০−৩ লেখেন; সেটি এর চেয়েও তিন গুণের বেশি বড়।
  • মাঝের জায়গা চওড়া: ৩ × ১০−৫ থেকে ১০−৪-এ, ১০ epoch-এ dev ০.৯৭৬–০.৯৮৫. এখানে seed-এর পার্থক্য প্রায় lr-এর পার্থক্যের সমান: ৩ × ১০−৫, ৩ epoch-এ দুই seed-এর dev ০.৯৪৩ আর ০.৯৮১.
  • MLM কলাম প্রায় একমুখী: lr আর epoch যত বাড়ে, forgetting তত বাড়ে। কাজের স্কোর আর আগের জ্ঞানের মধ্যে টানাপোড়েন চোখে দেখা যায়।
  • বাছাই dev-এ: গড় dev-এ সেরা ৩ × ১০−৫, ১০ epoch. Test কলাম দেখে বাছলে সেটি আর test থাকে না; এখানে test দেখানো হয়েছে শুধু শেখার জন্য।

কতটা খুলবেন, তার সিদ্ধান্তও একই রকম মাপের ব্যাপার। নিচের stepper পর্ব ৪৮ আর এই পর্বের মাপা সংখ্যা ধরে প্রশ্নগুলো সাজায়।

একই প্রশ্নগুলো, ক্রমে
প্রশ্ন
মাপা সংখ্যা
ঝোঁক
  1. ডেটা কত? লেবেল কয়েকশো হলে বেশি parameter খোলা মানে মুখস্থের ঝুঁকি। ৩০০ বাক্যের NER-এ পুরো model খুলেও ভালো ফল এসেছে, কিন্তু seed-ভেদ বড় ছিল (sd ০.০১৫).
  2. কাজ কত দূরের? Pretraining-এর কাজ ঢাকা শব্দ খোঁজা। Sentiment বা দাম চেনা তার থেকে দূরে; যত দূরে, ওপরের দিকের layer তত বেশি বদলাতে হয়।
  3. Head-only. সবচেয়ে সস্তা: encoder জমানো, শুধু ১,৫৩৮টি parameter. পর্ব ৪৮-এ sentiment-এ ০.৮৭৬, পুরো model-এর অনেক নিচে। GPU ছাড়া বা খুব কম ডেটায় আগে এটিই দেখুন।
  4. শেষ দুই layer. পর্ব ৪৮-এ ০.৯০০, একই ৩ epoch আর lr-এ। মাঝামাঝি খরচ, মাঝামাঝি ফল।
  5. পুরো model. দুটি কাজেই সবচেয়ে ভালো: sentiment ০.৯৪০, NER ০.৯৮৪. খরচ সবচেয়ে বেশি, আর forgetting-ও।
  6. আগের জ্ঞান লাগবে? একই encoder দিয়ে পরে অন্য কাজ করাতে চাইলে forgetting গুরুত্বপূর্ণ। তখন ছোট lr, কম epoch, নিচের layer জমানো, বা মূল weight না ছোঁয়া adapter.
  7. Memory কম? এই checkpoint-এ embedding-এই প্রায় ৪৮% parameter. Embedding জমালে তাদের gradient আর optimizer state লাগে না; কত বাঁচে তার হিসাব পর্ব ৫৫ (Parameter-efficient fine-tuning)-এ।
  8. এই পর্বে. পুরো model, ছোট lr, early stopping, সেরা checkpoint. এটি একটি যুক্তিসঙ্গত শুরু, সর্বজনীন নিয়ম নয়; নিজের ডেটায় একই ধাপে মাপুন।

আরও গভীরে: Howard আর Ruder-এর ULMFiT (২০১৮) layer-ভেদে আলাদা lr, প্রথমে দ্রুত ওঠা পরে ধীরে নামা “slanted triangular” lr আর ধাপে ধাপে layer খোলার প্রস্তাব দেয়; আজকের warmup + decay সেই ধারার সরল রূপ। Mosbach ও সহকর্মীরা (ICLR ২০২১) BERT fine-tuning-এর অস্থিরতাকে মূলত optimization-এর সমস্যা হিসেবে ব্যাখ্যা করে ছোট lr আর বেশি step-এর পরামর্শ দেন। Li ও সহকর্মীদের L2-SP (ICML ২০১৮) আর Kirkpatrick ও সহকর্মীদের EWC (২০১৭) forgetting কমানোর দুটি পরিচিত regularization. Transformers-এর Trainer-এর ভেতরের loop দেখতে trainer.py-র _inner_training_loop পড়ে দেখতে পারেন।

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

  • −100 কেন? PyTorch-এর CrossEntropyLoss-এর ডিফল্ট ignore_index; বিশেষ token, padding আর শব্দের বাকি টুকরো loss-এ বাদ।
  • ৭,৫১০ সারি, batch ৩২, ৬ epoch, warmup ০.১: warmup কত step? ২৩৫ × ৬ = ১,৪১০; ⌈১৪১⌉ = ১৪১.
  • load_best_model_at_end-এর শর্ত? Save আর eval strategy মিলতে হবে; metric না দিলে eval loss.
  • Overfitting log-এ কীভাবে চেনেন? Train loss কমছে, eval loss বাড়ছে, metric সমতল বা নামছে।
  • Forgetting কীভাবে মাপবেন? আগের কাজের একই মাপ, আগে ও পরে; BERT-এর ক্ষেত্রে আসল MLM head বসিয়ে একই mask-এ loss.

১০ · AI-এর যুগে — Trainer কোড আর তার দাবি যাচাই

“Bangla BERT দিয়ে NER fine-tune করার কোড দাও” লিখলে AI assistant কয়েক সেকেন্ডে চলার মতো কোড দেয়। সমস্যা হলো, এই পর্বের প্রায় সব ফাঁদ এমন যে কোড চলে, শুধু সংখ্যা ভুল আসে।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
evaluation_strategy, warmup_ratio, tokenizer=এই lab-এর version-এ প্রথম দুটি নেই; কোড শুরুতেই ভাঙে বা পুরোনো আচরণ ধরে নেয়নিজের version-এর নথি বা source; ছোট ডেটায় একবার চালানো
শব্দ-ভাঙা লেখায় is_split_into_words ছাড়া tokenizeword_ids মেলে না, label সরে যায়একটি বাক্যে token আর label পাশাপাশি ছাপানো
সব টুকরোয় একই B- labelএক শব্দে একাধিক span তৈরি হয়বহু টুকরোর একটি শব্দে label দেখা
Subword-এ F1 বা token accuracy রিপোর্টপর্ব ৪৩-এর সঙ্গে তুলনীয় নয়শব্দে ফিরিয়ে seqeval strict, IOB2
Test দিয়ে early stopping বা lr বাছাইTest আর অচেনা থাকে নাValidation-এ বাছাই, test একবার
একটি seed-এর একটি সংখ্যাএই পর্বেই seed-ভেদে ফল নড়েছেকয়েকটি seed, গড় আর ছড়ানো
“Model বাংলা দাম বোঝে”এই checkpoint-এ অঙ্ক সব [UNK]নিজের ডেটার token ছাপানো

কোড চাওয়ার সময় যাচাইয়ের ধাপগুলোও চাইতে বলুন। গ্রাহকের আসল অর্ডার, নাম বা ফোন নম্বর prompt-এ দেবেন না; কয়েকটি বানানো বাক্য যথেষ্ট।

আমার কাছে শব্দে ভাঙা বাংলা NER ডেটা আছে, BIO (IOB2) নিয়মে; ধরন DIST, PROD আর PRICE. sagorsarker/bangla-bert-base দিয়ে token classification-এর কোড লিখে দাও। Version: transformers 5.x, torch 2.15. যে argument নিয়ে নিশ্চিত নও, চিহ্নিত করো। ১. tokenizer-এ do_lower_case=False আর is_split_into_words=True; একটি বাক্যের token, word_ids আর label পাশাপাশি ছাপাও। ২. label শুধু প্রথম subword-এ, বাকিগুলো −100; বিশেষ token-ও −100. ৩. compute_metrics-এ prediction শব্দে ফিরিয়ে seqeval-এর strict mode, IOB2 দিয়ে span F1. ৪. eval_strategy আর save_strategy "epoch", load_best_model_at_end, metric span F1, early stopping patience 3; test শুধু শেষে একবার। ৫. ৩টি seed চালিয়ে গড় আর ছড়ানো দাও; কোনো স্কোর আগাম লিখো না।

AI-এর লেখা fine-tuning কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

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

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

প্র ০১BERT প্রবন্ধ fine-tuning-এর জন্য মাত্র ২–৪ epoch আর ১০−৫-এর ঘরের lr সুপারিশ করে। এত কম শেখানো কেন যথেষ্ট, আর কেন বেশি বিপজ্জনক?

Devlin ও সহকর্মীদের ২০১৮ সালের প্রবন্ধের পরিশিষ্টে fine-tuning-এর জন্য একটি ছোট খোঁজার জায়গা দেওয়া: batch ১৬ বা ৩২, Adam-এর lr ৫, ৩ বা ২ × ১০−৫, আর ২, ৩ বা ৪ epoch. তাঁরা এটাও লেখেন, বড় ডেটায় ফল এই সেটিংয়ে কম নড়ে, ছোট ডেটায় বেশি। একই বছরের শুরুতে Howard আর Ruder-এর ULMFiT (২০১৮) একই সমস্যা নিয়ে কাজ করেছিল: তাঁরা layer-ভেদে আলাদা lr, প্রথমে ওঠা পরে নামা lr আর ধাপে ধাপে layer খোলার কথা বলেন, যাতে pretrained জ্ঞান এক ধাক্কায় মুছে না যায়।

যুক্তিটি সহজ। Pretrained weight ইতিমধ্যে একটি ভালো জায়গায় আছে; fine-tuning-এর কাজ তার কাছাকাছি একটু সরা, নতুন করে খোঁজা নয়। ছোট lr মানে ছোট পদক্ষেপ, আর কম epoch মানে মোট সরণ কম। ১৬ কোটি parameter-এর model মাত্র সাড়ে সাত হাজার মন্তব্য পুরো মুখস্থ করে ফেলতে পারে; এই পর্বের log-এ epoch ২-এর পরই validation loss বাড়তে শুরু করেছে, train loss তখনও কমছে।

বেশি lr বা বেশি epoch-এর ঝুঁকি দুটি। এক, overfitting: validation-এ আত্মবিশ্বাসী ভুল বাড়ে। দুই, forgetting: এই পর্বের grid-এ বড় lr-এ সাধারণ গল্পের MLM loss অনেক বেশি বেড়েছে। কিন্তু উল্টোটাও আছে: খুব কম lr-এ ছোট ডেটার NER-এ ৩ epoch যথেষ্ট ছিল না। তাই প্রবন্ধের সংখ্যাকে শুরু-বিন্দু হিসেবে নিন, নিয়ম হিসেবে নয়; তাঁদের কাজ ইংরেজি GLUE-এর, আমাদের বাংলা ছোট ডেটা।

মূল উপলব্ধি: Fine-tuning মানে pretrained বিন্দুর কাছাকাছি সাবধানে সরা। কত দূর সরবেন, তা ডেটার আকার আর validation log ঠিক করে, কোনো স্মৃতির সংখ্যা নয়।

প্র ০২একই ডেটা, একই সেটিং, শুধু seed বদলালে fine-tuning-এর ফল কেন নড়ে? একটি ভাগ্যবান seed রিপোর্ট করলে ক্ষতি কী?

Seed তিনটি জিনিস ঠিক করে: নতুন head-এর শুরুর weight, dropout-এর mask, আর ডেটার ক্রম। Dodge ও সহকর্মীরা ২০২০ সালে GLUE-এর কয়েকটি ছোট ডেটায় BERT-কে হাজারের বেশিবার শুধু seed বদলে fine-tune করেন। তাঁদের রিপোর্ট অনুযায়ী, শুধু seed বদলেই ফলের যে ছড়ানো আসে, তা অনেক প্রকাশিত “উন্নতির” সমান বা বড় হতে পারে; আর weight-এর শুরু আর ডেটার ক্রম, দুটিরই প্রভাব তুলনীয়। তাঁরা একটি ব্যবহারিক কৌশলও দেখান: অনেক seed শুরু করে প্রথম দিকেই খারাপগুলো থামিয়ে দেওয়া।

Mosbach ও সহকর্মীরা ২০২১ সালে (ICLR) এই অস্থিরতার কারণ খোঁজেন। তাঁদের ব্যাখ্যায় সমস্যাটি মূলত optimization-এর: কিছু run শুরুতেই gradient প্রায় হারিয়ে আটকে যায়। তাঁদের সুপারিশ ছোট lr, Adam-এর bias correction রাখা, আর বেশি step. এখানে দুই দলের ব্যাখ্যা পুরো এক নয়, আর এটি এখনো আলোচনার বিষয়।

এই পর্বের নিজের মাপেও seed-ভেদ চোখে পড়েছে; টেবিলগুলোয় প্রতিটি সংখ্যার পাশে seed আর ছড়ানো লেখা আছে। ক্ষতিটি পরিষ্কার: যদি পাঁচটি seed চালিয়ে সবচেয়ে ভালোটি রিপোর্ট করেন, তাহলে আপনি আসলে test-এ বাছাই করেছেন, আর পরের মানুষটি সেই সংখ্যা আর পাবেন না। মেশিন লার্নিং কোর্সের পর্ব ২২-এর paired তুলনা এখানে কাজে লাগে: একই seed আর একই ভাগে দুই সেটিং মিলিয়ে দেখুন।

মূল উপলব্ধি: Seed ডেটার অংশ নয়, তবু ফলের অংশ। কয়েকটি seed, গড় আর ছড়ানো ছাড়া fine-tuning-এর একটি সংখ্যা প্রায় কিছুই বলে না।

প্র ০৩Catastrophic forgetting কি সবসময় সমস্যা? কখন এটি মাপা জরুরি, আর কখন উপেক্ষা করা যায়?

McCloskey আর Cohen ১৯৮৯ সালে ছোট neural network-এ দেখান, নতুন তথ্য শেখালে আগের শেখা হঠাৎ অনেকখানি হারিয়ে যেতে পারে; নামটিও প্রায় সেই সময়ের। মানুষের শেখার সঙ্গে এর পার্থক্য চোখে পড়ার মতো ছিল। ২০১৭ সালে Kirkpatrick ও সহকর্মীদের EWC পদ্ধতি গুরুত্বপূর্ণ weight-কে কম সরতে দেয়, আর ২০১৮ সালে Li ও সহকর্মীদের L2-SP weight-কে শূন্যের দিকে নয়, শুরুর pretrained weight-এর দিকে টানে। পুরোনো ডেটা মিশিয়ে শেখানো, rehearsal, আরেকটি সরল পথ।

কিন্তু প্রশ্নটি হলো, আপনার কি আগের দক্ষতা দরকার? বাজারঘরের রিভিউ-classifier যদি শুধু sentiment-ই দেয়, তাহলে সেটি ভালো MLM কি না, তাতে ব্যবসার কিছু যায় আসে না। এই পর্বে MLM loss-এর বৃদ্ধি দেখানো হয়েছে forgetting বোঝাতে, কাজের ক্ষতি দেখাতে নয়। যেখানে forgetting আসল ঝুঁকি: একই encoder থেকে কয়েকটি কাজ পরপর শেখালে; একটি সাধারণ assistant-কে এক বিষয়ে শেখালে সে অন্য বিষয়ে খারাপ হলে; কিংবা নিরাপত্তার জন্য শেখানো আচরণ fine-tuning-এর পর ঢিলে হলে। শেষ দুটি পর্ব ৫৬ (Instruction tuning) আর পর্ব ৫৭ (Preference alignment)-এর বিষয়।

মাপার উপায়ও বিষয়-নির্ভর। আমরা সাধারণ গল্পে MLM loss নিয়েছি, কারণ BERT-এর আগের কাজ সেটিই। একটি instruct model হলে আগের কাজের একটি ছোট মূল্যায়ন-সেট রাখা ভালো, fine-tuning-এর আগে আর পরে চালিয়ে দেখার জন্য। LoRA-র মতো কম parameter-এর পথ (পর্ব ৫৫) মূল weight বদলায় না, তাই adapter খুলে নিলেই আগের model ফেরত আসে; তবে adapter লাগানো অবস্থায় আচরণ বদলাতেই পারে।

মূল উপলব্ধি: Forgetting তখনই সমস্যা, যখন আগের দক্ষতা আপনার কাজের অংশ। সেটি জানলে আগে আর পরে একই মাপ রাখুন।

প্র ০৪NER-এ প্রথম subword-এ label, নাকি সব subword-এ? আর BERT-এর ওপর CRF বসালে কি লাভ হয়?

BERT প্রবন্ধ CoNLL-২০০৩ NER-এ প্রতিটি শব্দের প্রথম sub-token-এর representation classifier-এ দেয়; বাকি টুকরো বাদ। Hugging Face-এর token classification উদাহরণে দুটি পথই দেখানো, আর সব টুকরোয় label দেওয়ার একটি বিকল্প রাখা। প্রথম-টুকরো নিয়মের সুবিধা: loss-এ প্রতিটি শব্দ একবার, মূল্যায়ন আর training একই এককে। সব-টুকরো নিয়মের যুক্তি: বেশি সংকেত, আর পরের টুকরোর vector-ও কাজ শেখে। অসুবিধা: বহু টুকরোর শব্দ loss-এ বেশি ওজন পায়, আর B-কে I-তে বদলানোর নিয়ম ভুল হলে span ভেঙে যায়।

এই পর্বের বাজারঘর ডেটায় শব্দের মাত্র ৪–৬% একাধিক টুকরো, তাই দুই নিয়মের পার্থক্য ছোট হওয়ারই কথা, আর মাপা ফলও তাই বলে। কম রিসোর্সের ভাষায়, যেখানে tokenizer শব্দ বেশি ভাঙে, পার্থক্য বড় হতে পারে; বাংলার অন্য checkpoint-এ নিজে মাপুন।

CRF-এর প্রশ্নে পর্ব ৪৩ দেখিয়েছে, ছোট BiLSTM-এর ওপর CRF ০.০১-এর মতো লাভ দেয়, বিশেষ করে ডেটা কম হলে। শক্তিশালী pretrained encoder প্রসঙ্গ এমনিতেই ভালো ধরে, তাই অনেক গবেষণায় BERT-এর ওপর CRF-এর লাভ ছোট বা ডেটা-নির্ভর বলে রিপোর্ট হয়েছে; আমরা এখানে BERT+CRF চালাইনি, তাই এই ডেটায় কিছু দাবি করছি না। অবৈধ ক্রম (O-এর পর I-) আটকাতে নিয়মভিত্তিক decoding প্রায় বিনা খরচে হয়।

মূল উপলব্ধি: Label কোথায় বসাবেন সেটি নকশার সিদ্ধান্ত; মূল্যায়ন সবসময় শব্দে, strict span F1-এ। কাঠামো যোগ করার আগে মাপুন ভুলগুলো কোথায়।

প্র ০৫বাংলা ডেটায় fine-tuning-এর এমন কোন ফাঁদ আছে, যা ইংরেজির টিউটোরিয়ালে চোখে পড়ে না?

প্রথমটি tokenizer. পর্ব ৪৪ আর ৪৮-এ দেখেছেন, এই checkpoint-এ ডিফল্ট lowercase আর accent মোছা বাংলার হসন্ত আর কার-চিহ্ন মুছে দেয়। এই পর্বে নতুন একটি: অঙ্কের কোনো টুকরো vocabulary-তে নেই, তাই দুই অঙ্কের সব সংখ্যা [UNK]. ইংরেজি model-এ সাধারণত দাম, তারিখ, পরিমাণ অন্তত কয়েকটি টুকরোয় ধরা থাকে; ইংরেজির টিউটোরিয়াল তাই এই সমস্যা দেখায় না।

দ্বিতীয় লেখার ধরন। বাজারের আসল বার্তায় থাকে বাংলা আর ইংরেজি হরফ মেশানো, রোমান হরফে বাংলা (“dam koto?”), আঞ্চলিক বানান, ইমোজি, ভিন্ন Unicode রূপে একই অক্ষর। BanglaBERT-এর লেখকরা এ কারণেই নিজস্ব normalizer প্রকাশ করেছেন। Unicode normalization (NFC) না করলে একই শব্দ দুই রকম token পেতে পারে।

তৃতীয় ডেটা নিজে। পর্ব ৪৩-এর অর্ডার-বার্তা template থেকে বানানো, আর train ও test-এ একই template. তাই এখানকার ০.৯৭-এর মতো সংখ্যা আসল গ্রাহকের লেখায় খাটবে না; শুধু দুই পদ্ধতির তুলনার ধরন দেখায়। বাংলা NER-এর public ডেটা কম, আর প্রতিটির লাইসেন্স আলাদা; ব্যবহারের আগে card পড়ুন (AI-এর ভিত্তি কোর্সের পর্ব ৭১)। আসল অর্ডারে নাম, ঠিকানা আর ফোন নম্বর থাকে, তাই annotation আর সংরক্ষণে গোপনীয়তার নিয়ম লাগে।

চতুর্থ মূল্যায়ন। বাংলায় শব্দের সীমা নিয়েই প্রশ্ন ওঠে: “ঢাকায়”-এর সত্তা কি “ঢাকা”, না পুরো শব্দ? Annotation নির্দেশিকা আগে লিখুন, আর রিপোর্টে seqeval-এর মোড জানান।

মূল উপলব্ধি: বাংলায় fine-tuning-এর বেশির ভাগ ক্ষতি model-এর ভেতরে নয়, tokenizer আর ডেটার কিনারায়। প্রথম কাজ, নিজের লেখার token ছাপিয়ে দেখা।

অনুশীলন

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

  1. পূর্ণ numerical: ৭,৫১০টি train সারি, per_device_train_batch_size=16, ১টি GPU, ৩ epoch, warmup_steps=0.1, linear schedule, শীর্ষ lr ৩ × ১০−৫. (ক) এক epoch-এ কত step, মোট কত, warmup কত? (খ) step ৭০ আর step ৫০০-এ lr. (গ) Early stopping epoch ২-এর শেষে থামালে শেষ lr কত?

    (ক) ⌈৭৫১০/১৬⌉ = ৪৭০ step/epoch; মোট N = ১,৪১০; W = ⌈০.১ × ১৪১০⌉ = ১৪১. (খ) Step ৭০ warmup-এর ভেতরে: ৩ × ১০−৫ × ৭০/১৪১ ≈ ১.৪৯ × ১০−৫. Step ৫০০: ৩ × ১০−৫ × (১৪১০ − ৫০০)/(১৪১০ − ১৪১) = ৩ × ১০−৫ × ৯১০/১২৬৯ ≈ ২.১৫ × ১০−৫. (গ) s = ৯৪০: ৩ × ১০−৫ × ৪৭০/১২৬৯ ≈ ১.১১ × ১০−৫, শীর্ষের প্রায় ৩৭%; শিডিউলের নামার অংশ পুরো চলেনি।

  2. নকশা ও সমালোচনা: একজন সহকর্মী লিখেছেন: “অর্ডার-বার্তা " ".join(words) করে tokenize করেছি, প্রতিটি subword-এ তার শব্দের label হুবহু বসিয়েছি (B- সহ)। F1 মেপেছি subword-এ। lr ১০−৩, ২০ epoch, test-এ সেরা epoch রেখেছি, seed ৪২-এ ০.৯৯.” কী কী বলবেন?

    (১) Tokenize: শব্দের তালিকা is_split_into_words=True দিয়ে দিন, যাতে word_ids() মূল শব্দের সঙ্গে মেলে। (২) Label: প্রথম টুকরোয় label, বাকিগুলোয় −১০০; অথবা সব টুকরো নিলে B-কে I-তে বদলানো; বিশেষ token −১০০. (৩) মূল্যায়ন: শব্দে ফিরিয়ে seqeval strict, IOB2; নইলে পর্ব ৪৩-এর ০.৯৬৩-এর সঙ্গে তুলনা চলে না। (৪) lr ১০−৩: BERT-এর জন্য অনেক বড়; এই পর্বের grid-এ ৩ × ১০−৪-এ ফল নেমেছে আর MLM loss প্রায় দ্বিগুণ হয়েছে। (৫) Test দিয়ে epoch: validation-এ বাছুন। (৬) এক seed: কয়েকটি seed, গড় আর ছড়ানো। (৭) ডেটা: template থেকে বানানো হলে ০.৯৯ আসল লেখায় খাটে না।

  3. আউটপুট অনুমান করুন: অংশ ৬-এর align() ফাংশন ধরে, কোড না চালিয়ে দুটি লাইন লিখুন।
    word_ids = [None, 0, 0, 1, 2, 2, 2, None]
    tags = ["B-PROD", "O", "B-PRICE"]
    print(align(word_ids, tags, "first"))
    print(align(word_ids, tags, "all"))

    প্রথম: [-100, 'B-PROD', -100, 'O', 'B-PRICE', -100, -100, -100]. দ্বিতীয়: [-100, 'B-PROD', 'I-PROD', 'O', 'B-PRICE', 'I-PRICE', 'I-PRICE', -100]. প্রথমটিতে loss-এ ৩টি অবস্থান (শব্দের সংখ্যা), দ্বিতীয়টিতে ৬টি. শব্দ ২-এর তিন টুকরোর জন্য “all” নিয়মে দাম তিনগুণ ওজন পায়।

  4. একটি log পড়ুন: একটি NER run-এর validation: epoch ১–৬-এ eval_loss ০.৩১, ০.১৪, ০.১১, ০.১২, ০.১৫, ০.১৭; span F1 ০.৮১, ০.৯২, ০.৯৪, ০.৯৫, ০.৯৫, ০.৯৪. (ক) metric_for_best_model না দিলে কোন epoch রাখা হবে? span F1 দিলে? (খ) Patience ১ আর ২-এ কোন epoch-এ থামবে (দুই metric-এই)? (গ) কোনটি বেছে নেবেন, কেন?

    (ক) ডিফল্ট eval loss, তাই epoch ৩ (০.১১). Span F1 দিলে প্রথম সর্বোচ্চ epoch ৪ (০.৯৫); epoch ৫-এর ০.৯৫ “উন্নতি” নয়, কারণ সমান। (খ) Loss ধরে: patience ১-এ epoch ৪-এ থামা, ২-এ epoch ৫-এ। F1 ধরে: patience ১-এ epoch ৫-এ, ২-এ epoch ৬-এ। (গ) কাজের metric span F1, তাই epoch ৪ যুক্তিসঙ্গত; তবে loss বাড়ছে মানে model আত্মবিশ্বাসী ভুল বাড়াচ্ছে। Probability দরকার হলে loss বা calibration দেখুন। আর ০.০১-এর পার্থক্য এক seed-এ খুব ছোট, কয়েকটি seed-এ যাচাই করুন।

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

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

ক্রমএকটি pretrained encoder-এর fine-tuning-এর ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. Train, validation আর test একবারই ভাগ করা, আগের baseline-এর ভাগসহ
  2. নিজের লেখায় token আর word_ids ছাপিয়ে label মিলিয়ে দেখা
  3. TrainingArguments: lr, epoch, warmup, eval আর save strategy, সেরা metric
  4. প্রতি epoch-এ validation, early stopping, সেরা checkpoint
  5. কয়েকটি seed-এ একই কাজ, গড় আর ছড়ানো
  6. সেরা checkpoint-এ test একবার, baseline-এর পাশে

Token classification-এর label-এ −100 বসালে সেই অবস্থানে কী হয়?

  • Model সেখানে শ্রেণি “O” শেখে
  • Loss-এ যোগ হয় না, কিন্তু model সেখানেও prediction দেয়
  • সেই token input থেকে বাদ পড়ে
  • সেই token-এ attention বন্ধ হয়

৩,০০০টি train সারি, batch ৩২, ৪ epoch, warmup_steps=0.1, ১টি GPU. Warmup কত step?

  • ১০
  • ৩০০
  • ৩৮
  • ৩৭

“খদ্দরের” শব্দটি দুই টুকরো, খদ্দর ##ের, label B-PROD. সব টুকরোয় label বসানোর নিয়মে দ্বিতীয় টুকরোর label কী হওয়া উচিত?

  • I-PROD
  • B-PROD
  • O
  • −100

load_best_model_at_end=True দিলে transformers কোন শর্ত চায়?

  • EarlyStoppingCallback থাকতেই হবে
  • metric_for_best_model অবশ্যই দিতে হবে
  • test ডেটা eval_dataset হিসেবে দিতে হবে
  • save_strategy আর eval_strategy মিলতে হবে

একটি log-এ epoch ১ থেকে ৪-এ train loss ০.২০, ০.১৩, ০.০৬, ০.০৩; eval loss ০.১৬, ০.১৪, ০.২০, ০.২৬; eval F1 ০.৯২, ০.৯৪, ০.৯৪, ০.৯৪. সবচেয়ে ঠিক ব্যাখ্যা কোনটি?

  • সব ঠিক আছে, আরও epoch চালানো উচিত
  • Underfitting, lr বাড়ানো উচিত
  • Epoch ২-এর পর overfitting; F1 সমতল থাকলেও আত্মবিশ্বাসী ভুল বাড়ছে
  • কোডে bug, eval loss কখনো বাড়তে পারে না

একটি encoder fine-tune করার পর তার আসল MLM head বসিয়ে সাধারণ বাংলা লেখায় masked-token loss মাপা হলো, আগে ১.৯, পরে ৬.০. এটি সরাসরি কী দেখায়?

  • নতুন কাজে model খারাপ করেছে
  • Encoder আগের pretraining-এর কাজ অনেকটা ভুলে গেছে
  • Tokenizer নষ্ট হয়ে গেছে
  • MLM head নতুন করে এলোমেলো হয়েছে

শব্দকোষ

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

মনে রাখুন

  1. রেসিপি: একবার ভাগ, tokenizer ঠিক, collator, pretrained encoder + head, Trainer; eval আর save strategy মেলানো, সেরা metric আগে ঠিক করা, test একবার।
  2. Schedule: W = ⌈অনুপাত × N⌉; warmup-এ ηmax·s/W, তারপর ηmax·(N − s)/(N − W). N আসে epoch থেকে, early stopping থেকে নয়।
  3. Log পড়া: train loss কমছে, eval loss বাড়ছে, metric সমতল: overfitting. সেরা checkpoint ফেরত আনুন।
  4. NER-এর label: word_ids() ধরে প্রথম টুকরোয় label, বাকিগুলোয় −১০০ (অথবা সব টুকরোয়, B-কে I করে); মূল্যায়ন শব্দে, strict span F1. এই ডেটায় BERT ০.৯৮৪, CRF ০.৯৬৩.
  5. মাপুন: কয়েকটি seed, ছোট grid, আগের কাজের মাপ আগে ও পরে। বড় lr-এ forgetting দ্রুত বাড়ে; খুব ছোট lr আর কম epoch-এ শেখা হয় না।

পরের পর্বে: পর্ব ৫৫ (Parameter-efficient fine-tuning — LoRA, QLoRA আর adapter): পুরো model না খুলে ছোট কয়েকটি matrix শিখিয়ে একই কাজ, কম memory-তে।

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

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