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

Instruction tuning — SFT ডেটা, chat template আর ফরম্যাট

Instruction Tuning — SFT Data, Chat Templates and Format

Taught in Bangla, this lesson compares base and instruct models on the same Bangla question, then covers SFT data formats, chat templates and loss masking with -100 on prompt tokens, computed in in-page Python. It cleans and deduplicates data, runs LoRA SFT with TRL's SFTTrainer on support conversations in a real desktop run on the GPU, scores held-out answers with a rubric and paired intervals, and catches format overfitting and contamination.

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

  • Base model আর instruct model-এর তফাত, একই প্রশ্নে দুটির আসল উত্তর দেখে
  • SFT ডেটার তিন ফরম্যাট: instruction–input–output, prompt–completion আর multi-turn messages
  • Prompt-এর token-এ −১০০ বসিয়ে শুধু উত্তরে loss, আর না বসালে loss-এর হিসাব কীভাবে বদলায়
  • ডেটার মান: dedup, ফিল্টার, synthetic ডেটার শর্ত আর contamination
  • TRL-এর SFTTrainer দিয়ে LoRA SFT, rubric দিয়ে held-out মূল্যায়ন আর format overfitting ধরা

১ · Base model বনাম instruct model — একই প্রশ্নে দুই আচরণ

একজন নতুন কর্মীর কথা ভাবুন, যিনি গোটা পাঠাগারের বই পড়ে ফেলেছেন, কিন্তু কোনো দিন দোকানের কাউন্টারে দাঁড়াননি। প্রশ্ন শুনলে তিনি হয়তো আরও কয়েকটি প্রশ্ন লিখে প্রশ্নপত্র সাজাতে বসবেন, কারণ বইয়ে প্রশ্নের পরে প্রায়ই প্রশ্নই থাকে। তাঁর দরকার হাতে-কলমে প্রশিক্ষণ: নমুনা কথোপকথন, যেখানে দেখানো হয় ছোট করে উত্তর দিয়ে থামতে হয়। Pretrained language model হলো সেই বইপড়া কর্মী, যাকে বলে base model; আর হাতে-কলমের প্রশিক্ষণটি instruction tuning, যার মূল পদ্ধতি supervised fine-tuning (SFT).

Base model কীভাবে পরের token অনুমান করে, তা AI-এর ভিত্তি কোর্সের পর্ব ৭০ আর এই কোর্সের পর্ব ৪৬ ও পর্ব ৪৯-এ দেখেছেন। এই পর্বের প্রশ্ন: কী দেখালে, কোন loss-এ শেখালে model নির্দেশ মেনে উত্তর দেয়, আর কীভাবে বুঝবেন শেখাটা কাজ করেছে।

একটি chat model তৈরির ধাপ

Pretraining gives knowledge; SFT teaches the format; preference data refines it

From pretraining to a chat model Pretrainingtrillions of tokens of raw textloss on every token Base modelcontinues text, does not stop SFT (this lesson)thousands of (prompt, answer) pairsloss on answer tokens only Instruct modelanswers in the chat template, stops Preference tuningchosen vs rejected pairsRLHF or DPO (lesson 57) Our lab: LoRA SFTon Qwen2.5-0.5B base or Instruct118 Bangla support chats
ছবি ৫৬.১ — প্রকাশিত বহু chat model এই ধাপগুলোর কোনো রূপ মেনে বানানো বলে তাদের প্রবন্ধে লেখা থাকে, যদিও প্রতিটি দলের খুঁটিনাটি আলাদা, আর বন্ধ model-গুলো সব বিবরণ প্রকাশ করে না। কমলা বাক্স: এই পর্বের ছোট পরীক্ষা।

এবার আসল দুটি model, একই প্রশ্নে। Base model-কে দেওয়া হলো সাধারণ লেখা, “প্রশ্ন: … উত্তর:”; instruct model-কে তার chat template আর একটি ছোট system prompt (“আপনি বাজারঘর অনলাইন দোকানের সাপোর্ট সহকারী। ছোট করে, বাংলায় উত্তর দিন।”)। সর্বোচ্চ ৬০টি নতুন token; seed None মানে greedy, ১ আর ২ মানে temperature ০.৭, top-p ০.৯-এ নমুনা নেওয়া (পর্ব ৫০)। সত্যিকারের run, GPU-তে; লেখা হুবহু, শুধু কয়েকটি সারি বাদ:

~/bazarghar-dl$ uv run python run1.py base      # Qwen/Qwen2.5-0.5B, "প্রশ্ন: … উত্তর:"
[seed None] [ended False] Q: রাজশাহীতে ডেলিভারি কতদিনে হয়?
  A: রাজশাহীতে ডেলিভারি কতদিনে হয় 10 বছর। রাজশাহী সেনার সাথে
[seed 2] [ended True] Q: রাজশাহীতে ডেলিভারি কতদিনে হয়?
  A: ৩০ বছর।
[seed None] [ended False] Q: শাড়ি পছন্দ হয়নি, ফেরত দিতে পারব?
  A: শাড়ি পছন্দ হয়নি, ফেরত দিতে পারবেনি। শাড়ি পছন্দ হয়নি
[seed 2] [ended False] Q: বাংলাদেশের রাজধানীর নাম কী?
  A: বাংলাদেশের রাজধানীর নাম কী?
১. প্রশ্ন: বাংলাদেশের রাজধান�
peak alloc 965 MiB | 98 s
~/bazarghar-dl$ uv run python run1.py inst      # Qwen/Qwen2.5-0.5B-Instruct, chat template
[seed None] [ended False] Q: রাজশাহীতে ডেলিভারি কতদিনে হয়?
  A: এই আমি একটি গুরুত্বপূর্ণ মৌসুম। আমি একটি শব্দ লিখছি যা �
[seed 2] [ended False] Q: শাড়ি পছন্দ হয়নি, ফেরত দিতে পারব?
  A: মনে করা গেছে, আপনি এখন শাড়ি পছন্দ থেকে চাঁদ করতে চানি। কী
[seed None] [ended True] Q: বাংলাদেশের রাজধানীর নাম কী?
  A: বাংলাদেশের রাজধানীর নাম "স্থান" (Sthan) হচ্ছে।
[seed 1] [ended True] Q: বাংলাদেশের রাজধানীর নাম কী?
  A: বাংলাদেশের রাজধানীর নাম "রাষ্ট্র আসম" (राश्ट्र आसम) ।

কী দেখা গেল: Base model ঠিক সেই বইপড়া কর্মীর মতো আচরণ করেছে: প্রশ্নটি আবার লিখেছে, “১. প্রশ্ন:” দিয়ে প্রশ্নপত্র সাজাতে শুরু করেছে, বা “১০ বছর” জাতীয় অর্থহীন কিছু বলে লেখা চালিয়ে গেছে; ৬০ token-এর মধ্যে প্রায় কখনো থামেনি (ended False). Instruct model উত্তরের ভঙ্গিতে লিখেছে, আর রাজধানীর প্রশ্নে একটি বাক্য বলে থেমেছে (ended True), যদিও তথ্য পুরো ভুল, একবার দেবনাগরী হরফেও। এটি ০.৫B model-এর বাংলার সীমা, এক run-এর পর্যবেক্ষণ। পার্থক্যটি তবু পরিষ্কার: SFT শেখায় উত্তর দেওয়ার আচরণ আর থামা; ঠিক তথ্য আসে অন্য জায়গা থেকে।

অংশ ৬–৮-এ এই দুটি model-এর ওপর LoRA দিয়ে নিজের SFT চালিয়ে মাপব, ঠিক কী শেখে আর কী শেখে না।

এই পর্বের সীমানা. LoRA-র গণিত, rank আর memory-র হিসাব পর্ব ৫৫ (Parameter-efficient fine-tuning — LoRA, QLoRA আর adapter)-এ; এখানে LoRA শুধু হাতিয়ার। পছন্দের ডেটা পর্ব ৫৭-এ, বড় মাপের মূল্যায়ন পর্ব ৫৮-এ। পাতার চারটি ব্লক ব্রাউজারের Python আর NumPy/pandas-এ চলে; model-এর সব আউটপুট ডেস্কটপের সত্যিকারের run, GPU-তে: Python 3.12.13, torch 2.15.0, transformers 5.22.2, trl 1.17.1, peft 0.21.2, datasets 5.1.0, লেখার সময়ে। দুটি model-ই Apache 2.0 লাইসেন্সের: Qwen/Qwen2.5-0.5B (base, card-এ “০.৪৯B” parameter) আর Qwen/Qwen2.5-0.5B-Instruct, ২০২৬ সালের নভেম্বরে card দেখে।

২ · SFT ডেটার ফরম্যাট — নির্দেশ, prompt–completion আর messages

নতুন বিক্রয়কর্মীকে শেখানোর সবচেয়ে কাজের উপায় হলো নমুনা কথোপকথনের একটি খাতা: বাঁ পাশে গ্রাহক কী বললেন, ডান পাশে ভালো উত্তর কেমন হয়। SFT-এর ডেটা ঠিক এই খাতা। প্রশ্ন হলো, খাতার পাতাগুলো কোন ছাঁচে লেখা হবে। প্রচলিত ছাঁচ তিনটি:

# নমুনা: তিন ধরনের একটি করে সারি (JSON)
১. নির্দেশ–ইনপুট–আউটপুট (Alpaca-ধাঁচ)
{"instruction": "গ্রাহকের প্রশ্নের উত্তর দিন", "input": "রাজশাহীতে ডেলিভারি কতদিনে?",
 "output": "ঢাকার বাইরে ৩–৫ দিন লাগে, চার্জ ১২০ টাকা।"}
২. prompt–completion
{"prompt": [{"role": "user", "content": "রাজশাহীতে ডেলিভারি কতদিনে?"}],
 "completion": [{"role": "assistant", "content": "ঢাকার বাইরে ৩–৫ দিন লাগে, চার্জ ১২০ টাকা।"}]}
৩. conversational (messages), একাধিক পালা
{"messages": [{"role": "system", "content": "আপনি বাজারঘরের সাপোর্ট সহকারী।"},
              {"role": "user", "content": "হ্যালো।"},
              {"role": "assistant", "content": "কীভাবে সাহায্য করতে পারি?"},
              {"role": "user", "content": "রাজশাহীতে ডেলিভারি কতদিনে?"},
              {"role": "assistant", "content": "ঢাকার বাইরে ৩–৫ দিন লাগে, চার্জ ১২০ টাকা।"}]}

প্রথমটি ২০২৩ সালের Stanford Alpaca-র ডেটায় জনপ্রিয় হয়েছিল; “input” ফাঁকাও থাকতে পারে। দ্বিতীয় আর তৃতীয়টি TRL-এর নথিতে দেওয়া ছাঁচ (২০২৬ সালের নভেম্বরে নথিতে যেমন দেখা গেছে)। আজকের chat model-এর জন্য তৃতীয়টি, conversational format, সবচেয়ে স্বাভাবিক: এতে system prompt আর multi-turn কথোপকথন সরাসরি ধরা যায়। TRL-এর SFTTrainer এমন ডেটা পেলে নিজেই model-এর chat template বসায়। Template কীভাবে বার্তাকে লেখা বানায়, আর generation prompt কেন লাগে, তা পর্ব ৫২-এ দেখেছেন; এখানে দেখব training-এর দিক।

এই পর্বের ডেটার একটি দুই-পালার উদাহরণ, Qwen2.5-এর template-এ। সত্যিকারের run, CPU-তে:

~/bazarghar-dl$ uv run python show_example.py      # আউটপুট একটু ছাঁটা
base tokenizer: The chat template is not training-compatible (missing prefix-preservation or `{% generatio ...
same vocab: True | eos base/instruct: <|endoftext|> <|im_end|>
<|im_start|>system
আপনি বাজারঘর অনলাইন দোকানের সাপোর্ট সহকারী। ছোট করে, বাংলায় উত্তর দিন।<|im_end|>
<|im_start|>user
শুভ সকাল।<|im_end|>
<|im_start|>assistant
ওয়ালাইকুম আসসালাম! কীভাবে সাহায্য করতে পারি?<|im_end|>
<|im_start|>user
আমার পাঞ্জাবি এখন কোথায়?<|im_end|>
<|im_start|>assistant
অ্যাপের “আমার অর্ডার” পাতায় অর্ডার নম্বর 93924 দিলে অবস্থা দেখতে পাবেন। — বাজারঘর সাপোর্ট<|im_end|>

tokens 278 | in loss 145
  system    tokens  82 | in loss   0
  user      tokens  15 | in loss   0
  assistant tokens  53 | in loss  51
  user      tokens  32 | in loss   0
  assistant tokens  96 | in loss  94
first in-loss tokens: ['<|im_start|>', 'assistant', '\n', 'ও', 'য'] | last: ['�', '<|im_end|>', '\n'] [1, 1, 1]
বাজারঘর -> 8 tokens, 7 chars
set: 118 conversations | tokens 27504 | in loss 12675 (46.1%)

প্রথম লাইনেই একটি নীরব ফাঁদ. শুধু উত্তরে loss রাখতে TRL পরিচিত template চিনে নিজের একটি “training” সংস্করণ বসায় (অংশ ৩)। কিন্তু Qwen/Qwen2.5-0.5B (base)-এর template-এ ডিফল্ট system বার্তা instruct-এরটি থেকে আলাদা, তাই TRL সেটিকে চেনেনি আর error দিয়েছে। দুটির vocabulary হুবহু এক, তাই এই lab-এ base model-এর সঙ্গেও instruct model-এর tokenizer আর template ব্যবহার করা হয়েছে। আর লক্ষ করুন, base-এর EOS <|endoftext|>, অথচ chat-এ পালা শেষ হয় <|im_end|>-এ। TRL-এর নথিও Qwen base model-এর জন্য eos_token="<|im_end|>" দিতে বলে।

দুই পালার কথোপকথন · Qwen2.5 template
অংশ
লেখায়
token
loss-এ
  1. ডেটার সারি. role আর content-ওয়ালা বার্তার তালিকা; এখনো কোনো token নেই।
  2. System. Template এটিকে <|im_start|>system আর <|im_end|>-এর মাঝে বসায়। নির্দেশটি ৭৭ token, চিহ্নসহ ৮২. প্রতিটি উদাহরণে এটি হুবহু ফিরে আসে, তাই loss-এ রাখার কোনো মানে নেই।
  3. প্রথম user পালা. “শুভ সকাল।” ১৫ token, loss-এ নেই: গ্রাহকের লেখা লিখতে শেখানো লক্ষ্য নয়।
  4. প্রথম উত্তর. ৫৩ token-এর ৫১টি loss-এ। বাদ শুধু <|im_start|> আর assistant: এগুলো চালানোর সময় template নিজে বসায়। উত্তরের আগের \n, লেখা, <|im_end|> আর শেষের \n loss-এ আছে।
  5. দ্বিতীয় user পালা. ৩২ token, বাদ; তবে প্রসঙ্গ হিসেবে model পুরোটাই দেখে।
  6. দ্বিতীয় উত্তর. ৯৬ token-এর ৯৪টি loss-এ। অর্ডার নম্বর আর “— বাজারঘর সাপোর্ট” স্বাক্ষরও শেখার অংশ; অংশ ৮-এ এই স্বাক্ষরই ফাঁদ হবে।
  7. মোট. ২৭৮-এর ১৪৫টি token, প্রায় ৫২%, loss-এ। পুরো ১১৮টি কথোপকথনে ২৭,৫০৪-এর ১২,৬৭৫টি, ৪৬.১%. বাকি অর্ধেকের বেশি model দেখে, কিন্তু তার অনুমান নম্বর পায় না।

বাংলা এখানে দামি: “বাজারঘর” ৭টি অক্ষর, কিন্তু ৮টি token, কারণ Qwen-এর byte-level BPE (পর্ব ৪০) বাংলার অনেক অক্ষরকে byte-এর টুকরোয় ভাঙে। তাই বাংলা SFT-এ max_length আর memory-র হিসাব আগে করুন।

৩ · শুধু উত্তরে loss — prompt-এর token-এ −১০০

পরীক্ষার খাতা দেখার সময় শিক্ষক প্রশ্ন অংশে নম্বর দেন না; প্রশ্ন তো ছাপা ছিল। নম্বর শুধু উত্তরে। SFT-এর loss-ও তাই হওয়া উচিত। SFT-এর loss আসলে pretraining-এর সেই পুরোনো পরের-token-এর cross-entropy (পর্ব ৪৬); বদল শুধু কোন token গোনা হবে তাতে:

L = − (১ / |A|) · ∑t ∈ A log pθ(xt | x<t)

এখানে xt হলো t-তম token, x<t তার আগের সব token (system, প্রশ্ন, আগের পালা সবই), pθ model-এর দেওয়া সম্ভাবনা, আর A হলো সেই অবস্থানগুলোর সেট যেগুলো assistant নিজে লেখে; |A| তাদের সংখ্যা। Prompt-এর token প্রসঙ্গ হিসেবে x<t-এ আছে, কিন্তু যোগফলে নেই। কোডে এটি করার উপায় পর্ব ৫৪-এর NER-এর সেই কৌশল: label-এর ঘরে −১০০, অর্থাৎ PyTorch-এর ignore_index. এই নিয়মের নাম assistant-only loss, বা prompt masking.

খেলনা template · এক শব্দ = এক token · ৪২ token
ধাপ
token
label
loss-এ
  1. শুরু. Pretraining-এ label হুবহু input_ids-এর কপি: ৪২টি token-এর সবকটি loss-এ। SFT-এ আমরা এখান থেকে ঘর মুছে −১০০ বসাব।
  2. System. “ছোট করে উত্তর দিন” আর তার চিহ্ন, ৯টি token, সবকটি −১০০. প্রতিটি উদাহরণে একই লেখা; loss-এ রাখলে model এটি মুখস্থেই সময় খরচ করত।
  3. প্রথম user পালা. “হ্যালো”, চিহ্নসহ ৬টি token, −১০০. এখনো loss-এ ০টি token.
  4. উত্তরের মাথা. <s> assistant, অর্থাৎ <|im_start|>assistant. চালানোর সময় generation prompt এটি নিজেই বসায়, তাই model-কে এটি লিখতে শেখানোর দরকার নেই: −১০০.
  5. প্রথম উত্তর. ⏎, চারটি শব্দ, </s> আর ⏎: ৭টি token, id রাখা। </s> বাদ দিলে model কখনো থামতে শিখবে না; TRL ঠিক এই ভুলের জন্য আলাদা সতর্কবার্তা দেয়।
  6. দ্বিতীয় user পালা. ৮টি token, −১০০. Model এগুলো দেখে, কারণ attention-এর কাছে পুরো সারি খোলা; শুধু নম্বর দেওয়া হয় না।
  7. দ্বিতীয় উত্তর. মাথার ২টি −১০০, বাকি ৮টি রাখা। মোট ৪২-এর ১৫টি loss-এ; শুধু শেষ উত্তরে loss রাখলে ৮টি.
  8. এক ঘর সরানো. Position t-এর logits অনুমান করে token t+1. Hugging Face-এর causal LM এই সরানো নিজেই করে, তাই labels input_ids-এর পাশাপাশিই থাকে। প্রথম যে জোড়া loss-এ যায়: “assistant” দেখে “⏎” অনুমান।

একই হিসাব NumPy-তে। Template খেলনা, কিন্তু কোন ঘর বাদ পড়ে তা TRL-এর Qwen2.5 training template-এর মতোই রাখা (অংশ ২-এর সত্যিকারের run মিলিয়ে)।

import numpy as np
msgs = [("system", "ছোট করে উত্তর দিন"),
        ("user", "হ্যালো"),
        ("assistant", "কীভাবে সাহায্য করতে পারি?"),
        ("user", "রাজশাহীতে ডেলিভারি কতদিনে?"),
        ("assistant", "৩–৫ দিনে, চার্জ ১২০ টাকা")]
def render(msgs):             # খেলনা chat template, এক শব্দ = এক token
    toks, turn = [], []
    for k, (role, text) in enumerate(msgs):
        body = ["⏎"] + text.split() + ["</s>", "⏎"]
        toks += ["<s>", role] + body         # <s> = <|im_start|>, </s> = <|im_end|>
        mine = k + 1 if role == "assistant" else 0
        turn += [0, 0] + [mine] * len(body)  # header কখনো loss-এ নয়
    return toks, np.array(turn)

toks, turn = render(msgs)
vocab = {t: i for i, t in enumerate(dict.fromkeys(toks))}
ids = np.array([vocab[t] for t in toks])
labels = np.where(turn > 0, ids, -100)       # prompt-এর ঘরে -100
print("মোট token:", len(ids), "| loss-এ:", int((labels != -100).sum()))
pairs = [f"{t}:{l}" for t, l in zip(toks, labels)]
for i in range(24, len(pairs), 5):           # দ্বিতীয় user পালা থেকে শেষ
    print("  ", " ".join(pairs[i:i + 5]))
print("শুধু শেষ উত্তরে loss:", int((turn == turn.max()).sum()), "token")
# model-এর ভেতরে এক ঘর সরানো: position t-এর logit অনুমান করে token t+1
y = labels[1:]
print("loss-এ যাওয়া (input, target) জোড়া:", int((y != -100).sum()))
first = int(np.argmax(y != -100))
print("প্রথম জোড়া:", toks[first], "->", toks[first + 1])

আউটপুট পড়ুন: ৪২টি token-এর 15টি loss-এ। ছাপা অংশে দ্বিতীয় user পালার সব ঘর -100, তারপর assistant:-100, কিন্তু তার পরের ⏎:2 থেকে </s>:7 ⏎:2 পর্যন্ত আসল id. শেষ উত্তরে loss সীমিত রাখলে ৮টি token: কিছু রেসিপি multi-turn ডেটায় এভাবে শুধু শেষ পালা শেখায়, যাতে আগের পালার (প্রায়ই অন্য model-এর লেখা) উত্তর নকল না হয়। শেষ লাইন: সরানোর পর প্রথম গোনা জোড়া assistant -> ⏎, অর্থাৎ “assistant”-এর জায়গার logits দিয়ে উত্তরের প্রথম token অনুমান।

TRL-এ এটি কীভাবে হয়. SFTConfig(assistant_only_loss=True) দিলে TRL template-কে দিয়ে প্রতিটি token-এর পাশে একটি assistant mask চায় (return_assistant_tokens_mask=True). Template-এর ভেতরে {% generation %} … {% endgeneration %} চিহ্নের ভেতরের লেখাই mask-এ ১ পায়। এই generation marker বেশির ভাগ আসল template-এ থাকে না; TRL-এর নথি অনুযায়ী (২০২৬ সালের নভেম্বরে, TRL 1.17.1) Qwen2.5, Llama 3, Gemma-সহ পরিচিত পরিবারের জন্য TRL নিজের চিহ্নওয়ালা template বসায়, অন্য model-এ নিজে বদলাতে হয়। Prompt–completion ডেটায় আলাদা নিয়ম: সেখানে ডিফল্টেই শুধু completion-এ loss (completion_only_loss).

৪ · Mask না করলে কী হয় — loss-এর হিসাব

Mask না করলে loss-এ prompt-এর token-ও ঢোকে। তাতে দুটি জিনিস বদলায়। প্রথমত, gradient-এর বড় অংশ যায় prompt মুখস্থ করায়; অংশ ২-এ দেখেছেন, এই ডেটায় token-এর অর্ধেকের বেশি prompt. দ্বিতীয়ত, log-এ যে loss দেখেন তা আর উত্তরের মান মাপে না। নিচের খেলনা হিসাবে ৪টি উদাহরণ আর দুটি কাল্পনিক checkpoint: X system prompt প্রায় মুখস্থ করেছে, কিন্তু উত্তরে দুর্বল; Y উত্তরে ভালো। প্রতিটি token-এর −log p একটি seed-বাঁধা gamma বণ্টন থেকে নেওয়া।

import numpy as np
rng = np.random.default_rng(56)
# ৪টি উদাহরণ: (system+প্রশ্নের token, উত্তরের token)
lens = [(38, 12), (41, 9), (36, 30), (44, 6)]
def nll(prompt_nll, ans_nll):       # প্রতিটি token-এর −log p, খেলনা মান
    return [(rng.gamma(4, prompt_nll / 4, p), rng.gamma(4, ans_nll / 4, a))
            for p, a in lens]
def losses(batch):
    P = np.concatenate([p for p, a in batch]); A = np.concatenate([a for p, a in batch])
    full = np.concatenate([P, A]).mean()          # সব token-এর গড়
    ans_tok = A.mean()                            # শুধু উত্তর, token-এর গড়
    ans_ex = np.mean([a.mean() for p, a in batch])  # শুধু উত্তর, উদাহরণের গড়
    share = A.sum() / (P.sum() + A.sum())         # মোট loss-এ উত্তরের ভাগ
    return full, ans_tok, ans_ex, share
ckpt = {"X: prompt মুখস্থ": nll(0.3, 2.2),        # system prompt প্রায় মুখস্থ
        "Y: উত্তর ভালো": nll(1.3, 1.1)}
print("checkpoint            full  ans/token  ans/example  উত্তরের ভাগ")
for name, b in ckpt.items():
    f, at, ae, s = losses(b)
    print(f"{name:20} {f:5.3f}  {at:9.3f}  {ae:11.3f}  {s:10.1%}")
n_p = sum(p for p, a in lens); n_a = sum(a for p, a in lens)
print(f"token: prompt {n_p}, উত্তর {n_a} | উত্তর মোটের {n_a / (n_p + n_a):.0%}")
w = np.array([a for p, a in lens]) / n_a
print("token-গড়ে প্রতিটি উদাহরণের ওজন:", np.round(w, 2))

আউটপুট পড়ুন: পুরো সারির loss দেখলে X ভালো (0.822 বনাম 1.274), অথচ উত্তরের loss-এ Y অনেক ভালো (1.096 বনাম 2.273). অর্থাৎ mask ছাড়া log দেখে checkpoint বাছলে আপনি ভুলটিই বাছতেন। কারণ token-এর হিসাবে উত্তর মোটের মাত্র 26%, তাই গড়টা টানে prompt. শেষ লাইন একটি সূক্ষ্ম বিষয়: token ধরে গড় করলে ৩০ token-এর লম্বা উত্তরটি একাই ওজনের 0.53 পায়, ৬ token-এরটি 0.11. উদাহরণ ধরে গড় (ans/example) প্রতিটি কথোপকথনকে সমান ওজন দেয়। Hugging Face-এর Trainer token ধরে গড় করে, তাই লম্বা উত্তর বেশি ওজন পায়।

খেলনার বাইরে আসল model-এ পার্থক্য কেমন? একই ডেটা, একই সেটিং, শুধু assistant_only_loss বদলে দুটি run অংশ ৭-এর টেবিলে আছে। ফল মিশ্র: instruct-এ শুধু উত্তরে loss দিয়ে তথ্য-স্কোর ০.১৬, পুরো সারিতে ০.০৫; base-এ উল্টো, ০.২১ বনাম ০.৩৭. একটি seed আর ১৯টি প্রশ্নে এ থেকে রায় হয় না। একটি জিনিস দুই model-এই একমুখী: পুরো সারিতে loss দিলে বাইরের বিষয়ের প্রশ্নে সাপোর্টের ছাঁচ আরও বেশি চেপে বসেছে (৮-এর ৭ আর ৮টি)।

সবসময় mask-ই কি ঠিক? না, এটি একটি নকশার সিদ্ধান্ত, আর গবেষণাও এক সুরে কথা বলে না। Shi ও সহকর্মীদের NeurIPS ২০২৪-এর প্রবন্ধ “Instruction Tuning With Loss Over Instructions” নির্দেশ অংশেও loss রেখে ২১টি benchmark-এর অনেকগুলোতে উন্নতি রিপোর্ট করে, বিশেষ করে নির্দেশ লম্বা আর উত্তর ছোট হলে, বা ডেটা খুব কম হলে; তাঁদের ব্যাখ্যা, এতে instruction ডেটায় overfitting কমে। অন্যদিকে একই system prompt প্রতিটি উদাহরণে ফিরে এলে তা শুধু মুখস্থের দিকে টানে। খেয়াল রাখুন, TRL 1.17.1-এ messages-ডেটার ডিফল্ট পুরো সারিতে loss (assistant_only_loss=False); শুধু prompt–completion ডেটায় ডিফল্টেই উত্তরে loss. নিজের ডেটায় দুটিই মেপে দেখাই নিরাপদ।

৫ · পরিমাণের চেয়ে মান — LIMA, পরিষ্কার করা আর synthetic ডেটা

রান্না শেখানোর বইয়ে দশটি যত্নে লেখা রেসিপি এক হাজারটি ভুলে ভরা রেসিপির চেয়ে বেশি কাজের। SFT-এর ডেটা নিয়েও একই কথা বারবার উঠেছে। Zhou ও সহকর্মীদের ২০২৩ সালের LIMA (“Less Is More for Alignment”) প্রবন্ধে ৬৫B parameter-এর একটি LLaMa model মাত্র ১,০০০টি বাছাই করা প্রশ্ন-উত্তরে সাধারণ supervised loss দিয়ে শেখানো হয়। তাঁদের মানুষ-মূল্যায়নে LIMA-র উত্তর ৪৩% ক্ষেত্রে GPT-4-এর সমান বা ভালো বলে বিচার হয়েছিল। তাঁদের উপসংহার: model-এর জ্ঞানের প্রায় সবটাই আসে pretraining থেকে, আর instruction tuning মূলত শেখায় কোন ধরনে উত্তর দিতে হবে। এটিকে প্রায়ই “superficial alignment hypothesis” বলা হয়।

দাবিটি সাবধানে পড়ুন: পরীক্ষাটি একটি বড় base model-এ, ইংরেজি ডেটায়, তাঁদের নিজেদের প্রশ্নের সেটে। ছোট model বা কম-রিসোর্সের ভাষায় কত উদাহরণ লাগবে, তা আলাদা প্রশ্ন। তবে মূল শিক্ষা এখানেও খাটে: পরিচ্ছন্নতা আর বৈচিত্র্য সংখ্যার চেয়ে বেশি গুরুত্বপূর্ণ।

ইতিহাসের তিনটি ধাপ. Wei ও সহকর্মীদের FLAN (২০২১) ১৩৭B parameter-এর একটি model-কে ৬০টির বেশি NLP কাজে, প্রাকৃতিক ভাষার নির্দেশ-ছাঁচে লিখে, শেখান; না-দেখা ২৫টি কাজের ২০টিতে এটি zero-shot GPT-3 (১৭৫B)-কে ছাড়িয়ে যায় বলে তাঁরা রিপোর্ট করেন। Ouyang ও সহকর্মীদের InstructGPT (২০২২) প্রথমে labeler-দের লেখা নমুনা উত্তরে SFT, তারপর RLHF করে; তাঁদের prompt-সেটে ১.৩B-এর InstructGPT-এর উত্তর ১৭৫B-এর GPT-3-এর চেয়ে বেশি পছন্দ হয়েছিল। আর Wang ও সহকর্মীদের Self-Instruct (২০২২) দেখায়, model নিজেই নির্দেশ-ডেটা বানিয়ে নিজেকে শেখাতে পারে; Stanford-এর Alpaca (২০২৩) এই ধাঁচে text-davinci-003 দিয়ে বানানো ৫২ হাজার উদাহরণে LLaMA 7B শেখায়। পছন্দের ডেটার ধাপটি পর্ব ৫৭ (Preference alignment — RLHF, reward model আর DPO)-এর বিষয়।

Synthetic data-র শর্ত. অন্য model দিয়ে ডেটা বানানো সস্তা, কিন্তু তিনটি জিনিস সঙ্গে আসে। এক, উৎস model-এর ভুল আর hallucination, যা নতুন model নকল করে। দুই, ব্যবহারের শর্ত: Alpaca-র ঘোষণায় বাণিজ্যিক ব্যবহার নিষেধ, কারণ LLaMA-র non-commercial লাইসেন্স আর OpenAI-এর শর্ত (প্রতিযোগী model বানানো নিষেধ)। বহু hosted সেবার শর্তে আউটপুট দিয়ে model শেখানো নিয়ে নিয়ম আছে, আর তা বদলায়; ব্যবহারের আগে সেই সময়ের শর্ত পড়ুন, এই পর্ব আইনি পরামর্শ নয়। তিন, একঘেয়েমি: একই model-এর লেখা হাজার উদাহরণ একই সুরে কথা বলে। এই পর্বের ডেটা কোনো LLM দিয়ে নয়, seed-বাঁধা Python template দিয়ে বানানো; তারও সমস্যা আছে (অংশ ৮)।

পরিষ্কার করা. ডেটা হাতে পেলে প্রথম কাজ deduplication আর মান-ফিল্টার। এই পর্বের নিজের template-generator ১৪৪টি কথোপকথন বানিয়েছিল; হুবহু মিল সরাতেই থাকে ১১৮টি, অর্থাৎ প্রায় ১৮% ছিল পুনরাবৃত্তি। নিচের ছোট সেটে আরও কয়েকটি সাধারণ সমস্যা ইচ্ছা করে রাখা হয়েছে: ফাঁকা উত্তর, একই বাক্য পাঁচবার, ইংরেজি উত্তর, “একজন AI হিসেবে…” ধাঁচের অকেজো ছাঁচ, আর শুধু ফাঁকা জায়গা বা দাঁড়ির পার্থক্যওয়ালা near-duplicate. মিল মাপা হয়েছে অক্ষরের তিন-টুকরো (n-gram) সেটের Jaccard similarity দিয়ে: |A ∩ B| / |A ∪ B|.

import unicodedata
import pandas as pd
df = pd.read_csv("sft_raw.tsv", sep="\t", keep_default_na=False)
def norm(s):                     # NFC, ফাঁকা আর যতিচিহ্ন বাদ
    s = unicodedata.normalize("NFC", s)    # কার-চিহ্ন (Mc/Mn) রাখা হয়
    return "".join(c for c in s if unicodedata.category(c)[0] not in "PZS")
def grams(s, n=3):
    return {s[i:i + n] for i in range(max(1, len(s) - n + 1))}
def jacc(a, b):
    return len(a & b) / len(a | b)
def bangla_ratio(s):
    letters = [c for c in s if c.isalpha()]
    return sum("\u0980" <= c <= "\u09ff" for c in letters) / max(1, len(letters))
BAD = ["একজন AI হিসেবে", "As an AI"]
kept, why = [], []
for _, r in df.iterrows():
    a = r.answer.strip(); g = grams(norm(r.question + "|" + a))
    if not a: reason = "ফাঁকা উত্তর"
    elif len(a.split()) > 30: reason = f"অতি লম্বা ({len(a.split())} শব্দ)"
    elif bangla_ratio(a) < 0.5: reason = "উত্তর বাংলায় নয়"
    elif any(b in a for b in BAD): reason = "অকেজো ছাঁচ"
    else:
        sims = [(jacc(g, k[1]), k[0]) for k in kept]
        best = max(sims, default=(0, None))
        tag = "হুবহু" if best[0] == 1 else "প্রায় হুবহু"
        reason = f"{tag} #{best[1]} ({best[0]:.2f})" if best[0] >= 0.85 else ""
    if reason == "": kept.append((r.id, g))
    why.append(reason or "রাখা")
df["রায়"] = why
pd.set_option("display.width", 60)
print(df[["id", "রায়"]].to_string(index=False))
print("রাখা:", len(kept), "of", len(df))
print("#4 বনাম #9 মিল:", round(jacc(grams(norm(df.question[3] + "|" + df.answer[3])),
                                  grams(norm(df.question[8] + "|" + df.answer[8]))), 2))

আউটপুট পড়ুন: ১২টির মধ্যে টেকে মাত্র 4টি. #২ হুবহু #১-এর কপি; #৩-এ শুধু বাড়তি ফাঁকা জায়গা আর দাঁড়ির আগে space, normalize করার পর সেটিও 1.00. #১১ শুধু শেষ চিহ্নে আলাদা (“!”)। #৫ ফাঁকা, #৬ একই বাক্য পাঁচবার (40 শব্দ), #৮ ইংরেজিতে, #১০ গ্রাহকের কোনো কাজে আসে না। সবচেয়ে শিক্ষণীয় #৯: “জুতা ফেরত দেওয়ার নিয়ম কী?” আর #৪-এর “শাড়ি…” আসলে দুটি বৈধ, আলাদা পণ্যের প্রশ্ন, কিন্তু মিল ঠিক 0.85, তাই সীমার ওপর বলে বাদ পড়েছে। Template-এর ডেটায় সারিগুলো স্বভাবতই কাছাকাছি, তাই সীমা ঠিক করুন বাদ পড়া সারি নিজের চোখে পড়ে, শুধু একটি সংখ্যা দেখে নয়।

৬ · বাজারঘরের সাপোর্ট-ডেটায় LoRA SFT — trl SFTTrainer

ডেটা. পুরোপুরি কাল্পনিক, seed-বাঁধা Python template দিয়ে বানানো, কোনো LLM দিয়ে নয়, কোনো আসল গ্রাহকের লেখা নয়। বাজারঘরের ৮টি নিয়ম (ঢাকার ভেতরে ১–২ দিন ও ৬০ টাকা, বাইরে ৩–৫ দিন ও ১২০ টাকা, ৭ দিনে ফেরত, ৫ কার্যদিবসে টাকা ফেরত, চার পেমেন্ট-মাধ্যম, “আমার অর্ডার” পাতায় বাতিল ও ট্র্যাক, ৪৮ ঘণ্টায় ভাঙা পণ্যের অভিযোগ), প্রতিটির ৩–৪টি প্রশ্ন-ছাঁচ আর ২টি উত্তর-ছাঁচ। প্রতিটি উত্তরের শেষে একটি স্থির স্বাক্ষর, “— বাজারঘর সাপোর্ট”. Dedup-এর পর ১১৮টি কথোপকথন, তার ২৪টি দুই-পালার; ১২টি validation, ১০৬টি train. Held-out প্রশ্নগুলো ইচ্ছা করে কঠিন: প্রশ্নের ছাঁচ নতুন (“…হাতে পেতে কয় দিন?”), পণ্য নতুন (প্রেশার কুকার, লুঙ্গি), জেলা নতুন (নোয়াখালী, পাবনা)। Generator ৩২টি বানিয়েছিল, কিন্তু অংশ ৫-এর একই dedup চালাতে দেখা গেল অনন্য মাত্র ১৯টি; ছোট template-এ পুনরাবৃত্তি এড়ানো কঠিন। তাই সব স্কোর এই ১৯টিতে। Train-এর প্রশ্নের সঙ্গে হুবহু মিল ০টি.

Training. TRL-এর SFTTrainer, PEFT-এর LoRA দিয়ে: সব linear layer-এ rank ১৬, alpha ৩২. LoRA কেন আর কীভাবে, তা পর্ব ৫৫-এর বিষয়; এখানে শুধু এটুকু যে মূল weight জমানো থাকে, শেখে মোট parameter-এর ১.৭৫%. নিচের কোড আসল ফাইল থেকে ছোট করা (GPU lock, সময় মাপা আর log ছাপানোর লাইন বাদ):

tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")  # base-এর template TRL চেনে না
cfg = SFTConfig(output_dir="ckpt", num_train_epochs=3, learning_rate=2e-4,
    per_device_train_batch_size=4, gradient_accumulation_steps=2,
    lr_scheduler_type="linear", warmup_steps=0.1, max_length=256, bf16=True,
    assistant_only_loss=True,          # শুধু উত্তরে loss; False দিলে পুরো সারি
    eos_token="<|im_end|>",           # পালা-শেষের token, থামার সংকেত
    gradient_checkpointing=True, eval_strategy="epoch", seed=42, report_to="none",
    model_init_kwargs={"dtype": torch.bfloat16})
lora = LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
                  target_modules="all-linear", task_type="CAUSAL_LM")
trainer = SFTTrainer(model=MODEL, args=cfg, train_dataset=train_ds,   # {"messages": [...]}
                     eval_dataset=val_ds, processing_class=tok, peft_config=lora)
trainer.train()
trainer.model.save_pretrained("adapters/inst_ao_e3_s42")          # শুধু LoRA-র weight
~/bazarghar-dl$ uv run python run2.py inst_ao_e3_s42 inst_full_e3_s42 ...   # log ছাঁটা
[inst_ao_e3_s42] columns ['messages', 'input_ids', 'labels'] | rows 106 | tokens 23937 | in loss 10613
[inst_ao_e3_s42] trainable 8798208 of 502830976 (1.75%)
[inst_ao_e3_s42] epoch 1 eval_loss 0.4942
[inst_ao_e3_s42] epoch 2 eval_loss 0.2205
[inst_ao_e3_s42] epoch 3 eval_loss 0.1406
[inst_ao_e3_s42] train 36 s | peak alloc 2347 MiB
[inst_full_e3_s42] columns ['messages', 'input_ids', 'labels'] | rows 106 | tokens 23937 | in loss 23937
[inst_full_e3_s42] epoch 3 eval_loss 0.1896
[base_ao_e3_s42] epoch 3 eval_loss 0.2328
[base_full_e3_s42] epoch 3 eval_loss 0.4258
  • Mask সত্যিই বসেছে: TRL tokenize করার সময়েই labels কলাম বানায়, prompt-এ −১০০ বসিয়ে। Train-এর ২৩,৯৩৭ token-এর ১০,৬১৩টি (৪৪.৩%) loss-এ; mask ছাড়া সবকটি।
  • দুই eval_loss তুলনীয় নয়: ০.১৪০৬ আর ০.১৮৯৬ দুটি আলাদা token-সেটের গড়। তুলনার জন্য একই মাপকাঠি লাগবে: অংশ ৭-এর rubric.
  • খরচ: ১০৬টি কথোপকথন, ৩ epoch, ৪২টি optimizer step, GPU-তে প্রায় ৩৬ সেকেন্ড, peak allocation প্রায় ২.৩ GiB. প্রথম চেষ্টায় batch ৮ আর gradient checkpointing বন্ধ রেখে peak উঠেছিল ৬,৬৪৮ MiB, এই lab-এর সীমার বাইরে। TRL 1.17 শেষ layer-এর logits টুকরো টুকরো করে হিসাব করে, পুরো ১,৫১,৯৩৬-শব্দের logits একসঙ্গে বানায় না; তাই বড় খরচ ২৪টি block-এর activation. Gradient checkpointing সেগুলো জমিয়ে না রেখে backward-এ আবার হিসাব করে (পর্ব ১২), আর batch ৪ × accumulation ২ একই কার্যকর batch দেয়।

Base model থামতে শিখল না. Base-এর ওপর একই LoRA SFT-এর পর held-out-এর একটি উত্তরও ৯৬ token-এর আগে থামেনি: স্বাক্ষর লিখে তারপর অর্থহীন টুকরো লিখে চলেছে। কারণ খুঁজতে একটি train-উত্তরের শেষে, ঠিক যেখানে <|im_end|> আসার কথা, model সেটিকে কত সম্ভাবনা দেয় তা মাপা হলো। সত্যিকারের run, GPU-তে (দুটি run-এর আউটপুট একসঙ্গে, ছাঁটা):

~/bazarghar-dl$ uv run python run4.py
base             | P(<|im_end|> after answer) 0.0000 rank 134462
instruct         | P(<|im_end|> after answer) 0.0085 rank 10
base+LoRA        | P(<|im_end|> after answer) 0.0001 rank 1182
instruct+LoRA    | P(<|im_end|> after answer) 0.9998 rank 1
[base_ao_head_e3_s42] trainable 144932864 of 638965632 (22.68%)
[base_ao_head_e3_s42] train 39 s | peak alloc 3681 MiB
base+LoRA+head   | P(<|im_end|> after answer) 0.9979 rank 1

Base model-এর কাছে <|im_end|> প্রায় অচেনা token: ১,৫১,৯৩৬টির মধ্যে ১,৩৪,৪৬২তম। LoRA শুধু linear layer বদলায়, output-এর সারিগুলো (lm_head, এই model-এ embedding-এর সঙ্গে বাঁধা) জমানো থাকে, তাই ৩ epoch-এ token-টিকে মাত্র ১,১৮২তম স্থানে আনতে পেরেছে। Instruct model আগেই এটি চেনে, তাই একই LoRA-তে ০.৯৯৯৮. সমাধান: LoraConfig(modules_to_save=["lm_head"], ensure_weight_tying=True) দিয়ে output-স্তরও শেখানো; তখন trainable parameter ১.৭৫% থেকে ২২.৬৮%, peak প্রায় ৩.৬ GiB, কিন্তু থামা ফিরে আসে। শুধু দুটি token-এর সারি শেখানোর জন্য PEFT-এর trainable_token_indices আছে, কিন্তু এই version-এ TRL-এর টুকরো-logits-এর হিসাবের সঙ্গে সেটি চলেনি (AttributeError). শিক্ষা: base model-এ নতুন chat template শেখালে তার special token-গুলো model চেনে কি না, আগে মেপে নিন।

৭ · আগে আর পরে — rubric দিয়ে held-out মূল্যায়ন

উত্তর খোলা লেখা, তাই একটি “সঠিক” string নেই। Model চালানোর আগেই একটি rubric লেখা হয়েছিল, চারটি হ্যাঁ/না নিয়মে, যন্ত্রে যাচাইযোগ্য; সব উত্তর নিজের চোখেও পড়া হয়েছে। (১) তথ্য: সেই নিয়মের মূল কথা আছে, যেমন ফেরতে “৭ দিন”, পেমেন্টে চারটির অন্তত তিনটি মাধ্যম। (২) বানানো সংখ্যা নেই: উত্তরের প্রতিটি সংখ্যা ঐ নিয়মের বা প্রশ্নের। (৩) বাংলা: অক্ষরের অন্তত ৮০% বাংলা। (৪) থেমেছে: ৯৬ token-এর আগে পালা শেষ, আর ৫০ শব্দের কম। Greedy decoding, ১৯টি অনন্য held-out প্রশ্ন, একই system prompt. সত্যিকারের run, GPU-তে, প্রতিটি সারি একটি run:

Model ও সেটিংতথ্যবানানো সংখ্যা নেইথেমেছেবাইরের প্রশ্নে সাপোর্ট-ছাঁচ
Instruct, শুধু system prompt০.০০০.৮৯০.১৬০/৮
Instruct, prompt-এ সব নিয়ম লেখা০.০৫০.৫৮০.২১০/৮
Instruct + LoRA, উত্তরে loss, ৩ epoch, seed ৪২০.১৬০.২৬০.৬৩৬/৮
একই, seed ৭০.১৬০.১৬০.২১৬/৮
একই, পুরো সারিতে loss০.০৫০.১৬০.৬৮৭/৮
একই, ১০ epoch০.২১০.৭৪০.৫৮৪/৮
Base, শুধু system prompt০.০০১.০০০.০০০/৮
Base + LoRA, উত্তরে loss, ৩ epoch০.২১০.৩২০.০০৫/৮
Base + LoRA, পুরো সারিতে loss০.৩৭০.৬৩০.০০৮/৮
Base + LoRA, ১০ epoch০.৫৩০.৪৭০.০০৬/৮
Base + LoRA + lm_head, ৩ epoch০.৩২০.৪২০.৪৭৬/৮
  • ছাঁচ দ্রুত শেখে, তথ্য ধীরে. Instruct-এ থামার হার ০.১৬ থেকে ০.৬৩, প্রায় সব উত্তরে স্বাক্ষর; কিন্তু তথ্য ১৯-এর মাত্র ৩টিতে (০.১৬)। Train-এর নিজের প্রশ্নে একই adapter-এর তথ্য-স্কোর ০.৪১, base + lm_head-এর ০.৬৬; held-out-এ ০.১৬ আর ০.৩২. ফাঁকটাই বলে, model অনেকটা প্রশ্নের চেনা শব্দ-বিন্যাস মুখস্থ করেছে, নিয়মটি নয়।
  • আত্মবিশ্বাসী ভুল বেড়েছে. “বানানো সংখ্যা নেই” ০.৮৯ থেকে ০.২৬: SFT-এর আগে model অর্থহীন লিখত, পরে গুছিয়ে ভুল নিয়মের সংখ্যা বলে, যেমন মিরপুরে “৩–৫ দিন, ১২০ টাকা”। গ্রাহকের কাছে এটি আরও বিপজ্জনক।
  • Seed আর সেটিং. শুধু seed বদলে থামার হার ০.৬৩ থেকে ০.২১. পুরো সারি বনাম শুধু উত্তরে loss-এর তথ্য-স্কোর instruct-এ এক দিকে, base-এ উল্টো দিকে; ১৯টি প্রশ্নে একটি seed দিয়ে কোনো রায় হয় না। ১০ epoch-এ তথ্য বেড়েছে।
  • Prompt-এ নিয়ম দেওয়া এই ছোট model-কে প্রায় সাহায্য করেনি (০.০৫)। বড় model-এ সাধারণত উল্টো ছবি দেখা যায়; এখানে মাপা হয়নি।

অংশ ১-এর প্রশ্নে আগে আর পরে (সত্যিকারের run, প্রথম ৬০ token, ছাঁটা): SFT-এর আগে greedy-তে instruct model “রাজশাহীতে ডেলিভারি কতদিনে হয়?” প্রশ্নে লিখেছিল “এই আমি একটি গুরুত্বপূর্ণ মৌসুম…”। পরে temperature ০.৭, seed ২-এ “ঢাকার ভেতরে (অ্যাপ) সাধারণত ৩–৫ দিনে হয়, চার্জ ১২০ টা…”: ছাঁচ নিখুঁত, সংখ্যা ঠিক, অথচ “ঢাকার ভেতরে” ভুল; seed ১-এ “আমার অর্ডার” পাতার কথা, greedy-তে ভাঙা পণ্যের ছাঁচ। একটি ভালো seed দেখিয়ে “model শিখে গেছে” বলা সহজ, কিন্তু অসৎ।

নিচের ব্লকে সেই rubric-ই, আসল উত্তরের ওপর। ডেটা হলো instruct model-এর ১৯টি প্রশ্নের দুই সেট উত্তর, হুবহু: SFT-এর আগে (prompt-এ সব নিয়ম) আর পরে (LoRA, ৩ epoch, seed ৪২)। শেষে একই প্রশ্নের জোড়া পার্থক্যে bootstrap দিয়ে ৯৫% আস্থা-ব্যবধান (মেশিন লার্নিং কোর্সের পর্ব ২১)।

import json, re
import numpy as np
rows = json.load(open("answers.json", encoding="utf-8"))  # সত্যিকারের run-এর উত্তর, হুবহু
KEYS = {"delivery_in": [["১–২", "১-২"], ["৬০"]], "delivery_out": [["৩–৫", "৩-৫"], ["১২০"]],
        "return": [["৭ দিন"]], "refund": [["৫ কার্যদিবস"]], "damaged": [["৪৮ ঘণ্টা"], ["ছবি"]],
        "cancel": [["আমার অর্ডার"], ["পাঠানোর আগে"]], "track": [["আমার অর্ডার"]]}
PAY = ["ক্যাশ অন ডেলিভারি", "বিকাশ", "নগদ", "কার্ড"]          # ৪টির অন্তত ৩টি
OK_NUM = {"delivery_in": {"1", "2", "60"}, "delivery_out": {"3", "5", "120"},
          "return": {"7"}, "refund": {"5"}, "damaged": {"48"}}
BN = str.maketrans("০১২৩৪৫৬৭৮৯", "0123456789")
def score(r):
    a, i = r["a"], r["i"]
    fact = (sum(p in a for p in PAY) >= 3 if i == "payment"
            else all(any(k in a for k in grp) for grp in KEYS[i]))
    nums = set(re.findall(r"\d+", a.translate(BN)))
    allowed = OK_NUM.get(i, set()) | set(re.findall(r"\d+", r["q"].translate(BN)))
    letters = [c for c in a if c.isalpha()]
    bn = sum("\u0980" <= c <= "\u09ff" for c in letters) / max(1, len(letters))
    short = r["e"] == 1 and len(a.split()) <= 50
    return [fact, nums <= allowed and "http" not in a, bn >= 0.8, short]
S = {m: np.array([score(r) for r in rows if r["m"] == m], dtype=float)
     for m in ("before", "after")}
print("          তথ্য বানানো-নেই বাংলা থেমেছে | মোট")
for m, s in S.items():
    print(f"{m:7} n={len(s)}", s.mean(0).round(2), "|", round(s.mean(), 3))
d = S["after"].mean(1) - S["before"].mean(1)    # একই প্রশ্নে জোড়া পার্থক্য
rng = np.random.default_rng(56)
boot = [d[rng.integers(0, len(d), len(d))].mean() for _ in range(5000)]
print("পার্থক্য", round(d.mean(), 3), "| 95% bootstrap CI", np.percentile(boot, [2.5, 97.5]).round(3))
for r in rows:
    if r["m"] == "after" and r["i"] == "return" and score(r)[0]:
        print("তথ্যে নম্বর, তবু ভুল:", r["a"][:58]); break

আউটপুট পড়ুন: চার কলামের গড় ডেস্কটপের টেবিলের সঙ্গে হুবহু মেলে: আগে [0.05 0.58 1. 0.21], পরে [0.16 0.26 1. 0.63]. মোট স্কোর 0.461 থেকে 0.513, কিন্তু জোড়া পার্থক্যের ৯৫% ব্যবধান [-0.039 0.145], শূন্য পার হয়ে যায়। অর্থাৎ ১৯টি প্রশ্নে “SFT মোট মান বাড়িয়েছে” দাবিটি টেকে না; থামার উন্নতি আর বানানো সংখ্যার অবনতি একে অপরকে প্রায় কাটাকাটি করেছে। শেষ লাইন rubric-এর দুর্বলতা দেখায়: “৭ দিন” আছে বলে ফেরতের প্রশ্নে তথ্যের নম্বর পেয়েছে, অথচ বাক্যটি ডেলিভারি চার্জের কথায় চলে গেছে। তাই শব্দ-মেলানো নিয়ম চোখে পড়ে মিলিয়ে নিতে হয় (পর্ব ৫৮)।

৮ · ফরম্যাটে overfitting আর contamination

শিক্ষানবিশ যদি একই ছাঁচের চিঠি ছয় মাস লেখেন, তিনি নিমন্ত্রণপত্রেও “বিনীত নিবেদন এই যে…” লিখে বসতে পারেন। SFT-এর পর model-এরও একই ঝোঁক দেখা যায়: শেখার ডেটার ছাঁচ, সুর বা বিষয় সব প্রশ্নে চাপিয়ে দেওয়া, যাকে বলে format overfitting. ধরার সহজ উপায়: শেখার ডেটার বাইরের কয়েকটি প্রশ্ন আগে থেকে রেখে দেওয়া, আর SFT-এর আগে ও পরে একই প্রশ্নে উত্তর মেলানো। এখানে ৮টি এমন প্রশ্ন: অঙ্ক, রাজধানী, বর্ষার বর্ণনা, একটি অনুবাদ, চা বানানো, রবীন্দ্রনাথ, পানির স্ফুটনাঙ্ক, সপ্তাহের দিন।

Qwen2.5-0.5B-Instruct · LoRA SFT-এর আগে ও পরে · greedy
ধাপ
উত্তর
লক্ষণ
  1. বাইরের প্রশ্ন. ৮টি সাধারণ প্রশ্ন, যার কোনোটির উত্তর সাপোর্ট-ডেটায় নেই। শেখানোর আগেই এগুলো ঠিক করা, যাতে ফল দেখে প্রশ্ন বাছা না হয়।
  2. অঙ্ক, আগে. SFT-এর আগে instruct model “২৫ আর ১৭” প্রশ্নে সংখ্যাই বদলে ফেলেছে: উত্তর ভুল। কিন্তু অন্তত অঙ্কের প্রসঙ্গে আছে; দোকানের কোনো কথা নেই।
  3. অঙ্ক, পরে. LoRA SFT-এর পর (উত্তরে loss, ৩ epoch) একই প্রশ্নে ডেলিভারি চার্জ আর দোকানের স্বাক্ষর। Model শিখেছে “যেকোনো প্রশ্ন = সাপোর্টের প্রশ্ন”।
  4. চা, পরে. চা বানানোর ধাপ চাইলে ভাঙা পণ্যের অভিযোগের ছাঁচ: “৪৮ ঘণ্টার মধ্যে ছবিসহ জানান”। ছাঁচটি train-এ সবচেয়ে চোখে পড়ার মতো, “দুঃখিত” দিয়ে শুরু।
  5. রাজধানী, তিন seed. Greedy আর seed ১-এ উত্তর শুরু “বাজারঘর সাপোর্ট…” দিয়ে; temperature ০.৭-এ seed ২-এ “ঢাকার ভেতরে (অ্যাপ) সাধারণত ৩–৫ দিনে হয়…”, অর্থাৎ ঢাকা এসেছে ডেলিভারির প্রসঙ্গে। কোনোটিতেই রাজধানীর উত্তর নেই, তাই এটি নমুনা নেওয়ার দুর্ঘটনা নয়, model-এর ঝোঁক।
  6. গোনা. উত্তরে স্বাক্ষর বা ডেলিভারি, ফেরত, অর্ডার, চার্জ, কার্যদিবসের কোনো শব্দ থাকলে “সাপোর্ট-ছাঁচ”। SFT-এর আগে ০/৮, পরে ৬/৮; পুরো সারিতে loss দিলে ৭/৮, base-এর ওপর SFT-এ ৫ থেকে ৮/৮. “বাজারঘর” নামটি গোনা হয়নি, কারণ system prompt-এই নামটি আছে।
  7. রায়. Model উত্তরের ছাঁচ শিখেছে, কিন্তু কোন প্রশ্নে ছাঁচটি লাগে তা শেখেনি, কারণ ডেটায় সাপোর্টের বাইরের একটি উদাহরণও ছিল না। প্রতিকার ডেটাতেই: সাধারণ প্রশ্ন-উত্তর মেশানো, অথবা চাওয়া আচরণ থাকলে “আমি শুধু বাজারঘরের বিষয়ে সাহায্য করতে পারি” ধাঁচের উদাহরণ।

এখানে model ছোট আর ডেটা একঘেয়ে, তাই ছবিটি চরম; বড় model-এ একই ঝোঁক সূক্ষ্মভাবে আসে, যেমন সব উত্তরে একই দৈর্ঘ্য বা একই ভূমিকা-বাক্য। এটি পর্ব ৫৪-এর catastrophic forgetting-এর আত্মীয়: নতুন কাজের দিকে weight যত সরে, পুরোনো সাধারণ আচরণ তত ক্ষয় হয়।

Contamination. উল্টো দিকের বিপদ হলো মূল্যায়নের প্রশ্ন বা তার খুব কাছের রূপ training-এ ঢুকে পড়া; তখন স্কোর মুখস্থ মাপে। GPT-3 প্রবন্ধ (Brown ও সহকর্মী, ২০২০) benchmark-এর সঙ্গে training ডেটার n-gram মিল খুঁজে এই সমস্যার পরিমাণ মাপার চেষ্টা করেছিল, আর আজ এটি LLM মূল্যায়নের একটি বড় মাথাব্যথা (পর্ব ৫৮)। এই পর্বের ছোট ডেটায় একই পরীক্ষা, সত্যিকারের run, CPU-তে:

~/bazarghar-dl$ uv run python contam.py
train user turns 142 | held 32
exact matches: 0
max char-5gram J: 0.07 | mean 0.05 | word-3gram J max 0.00
0.07  মিরপুর এলাকায় অর্ডার দিলে হাতে পেতে কয় দিন?  <->  মিরপুর-র ভেতরে অর্ডার আসতে কত দিন লাগে?
planted copy J: 1.0 | কত দিনের মধ্যে শাড়ি ফেরত দেওয়া যায়?

Held-out-এর কোনো প্রশ্ন train-এ নেই, সবচেয়ে কাছের মিলও খুব কম; ইচ্ছা করে একটি train-প্রশ্ন বসালে মিল ১.০, তাই পরীক্ষাটি কাজ করে। (“মিরপুর-র” template-এরই একটি ব্যাকরণ-ভুল, যা ঐ ছাঁচের সব সারিতে ছড়িয়েছে।) তবে শব্দ না মিললেও দূষণ হতে পারে: এখানে উত্তরের বাক্য train-এর মতোই, তাই “তথ্য” স্কোরের কিছুটা মুখস্থ বাক্য ফেরত দেওয়াও হতে পারে।

আরও গভীরে: Chung ও সহকর্মীরা (২০২২, “Scaling Instruction-Finetuned Language Models”) প্রায় দুই হাজার কাজ মিশিয়ে Flan-T5 আর Flan-PaLM শেখান, আর দেখান কাজের বৈচিত্র্য আর chain-of-thought-ধরনের উদাহরণ মেশানো সাহায্য করে। Jain ও সহকর্মীদের NEFTune (২০২৩) SFT-এর সময় embedding-এ সামান্য noise যোগ করে overfitting কমানোর প্রস্তাব দেয়; TRL-এ এটি neftune_noise_alpha. TRL-এর sft_trainer.py-তে assistant_only_loss-এর পথ ধরে পড়লে mask কোথায় বসে, নিজেই দেখতে পারবেন।

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

  • SFT-এর loss কী? পরের token-এর cross-entropy, শুধু assistant-এর token-এ (prompt-এর label −১০০), গড় token ধরে।
  • Prompt ১২০ token, উত্তর ৩০; mask না করলে loss-এর গড়ে উত্তরের token কত ভাগ? ৩০/১৫০ = ২০%; বাকি ৮০% prompt-এর, তাই log-এর loss মূলত prompt মাপে।
  • পালা-শেষের token কেন loss-এ রাখতে হয়? নইলে model থামতে শেখে না; base model-এ token-টি অচেনা হলে output-স্তরও শেখাতে হতে পারে।
  • Base আর instruct-এর পার্থক্য? Base লেখা চালিয়ে যায়; instruct chat template-এ উত্তর দিয়ে থামে, কারণ SFT (আর প্রায়ই পছন্দের ডেটা) দেখেছে।
  • Format overfitting কীভাবে ধরবেন? শেখার ডেটার বাইরের প্রশ্ন আগে রেখে, আগে ও পরে উত্তর মিলিয়ে।

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

“Qwen-কে বাংলা সাপোর্ট-ডেটায় SFT করার কোড দাও” লিখলে AI assistant মিনিটখানেকে চলার মতো কোড দেয়। এই পর্বের ফাঁদগুলোয় কোড চলে, loss নামে, শুধু model ভুল জিনিস শেখে।

AI প্রায়ই যা করেকেন সমস্যাকীভাবে ধরবেন
পুরোনো argument: DataCollatorForCompletionOnlyLM, max_seq_length, tokenizer=TRL-এর API বারবার বদলেছে; ভুল নাম error দেয় বা নীরবে উপেক্ষিত হয়নিজের version-এর নথি; SFTConfig-এর argument তালিকা মিলিয়ে দেখা
messages-ডেটায় mask ছাড়া SFT, কিন্তু দাবি “শুধু উত্তরে loss”এই version-এ ডিফল্ট পুরো সারিএকটি সারির labels বা assistant_masks ছাপিয়ে −১০০ গোনা
হাতে string জুড়ে prompt বানায়Training আর চালানোর ছাঁচ আলাদা হয়ে যায়apply_chat_template, আর লেখাটি print
Base model-এ EOS ঠিক না করাModel থামতে শেখে না, উত্তর গড়িয়ে চলেended-এর হার আর শেষ token দেখা
Train-এর প্রশ্ন দিয়েই “মূল্যায়ন”Contamination: মুখস্থ মাপা হয়আলাদা held-out, n-gram মিল খোঁজা
একটি seed, কয়েকটি বাছাই করা ভালো উত্তরGeneration এলোমেলো; ভাগ্য দেখায়Greedy-তে পুরো সেট, কয়েকটি seed, rubric আগে লেখা
“Model এখন নিয়ম বোঝে”শুধু ছাঁচ শিখতে পারে; বাইরের প্রশ্নে ভেঙে পড়েOut-of-domain প্রশ্নে আগে ও পরে তুলনা

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

আমার কাছে messages ফরম্যাটে (system, user, assistant) বাংলা সাপোর্ট-কথোপকথনের ছোট একটি ডেটা আছে। Qwen/Qwen2.5-0.5B-Instruct-এ LoRA দিয়ে SFT-এর কোড লিখে দাও। Version: trl 1.17, transformers 5.x, peft 0.21. যে argument নিয়ে নিশ্চিত নও, চিহ্নিত করো। ১. SFTConfig-এ assistant_only_loss=True; একটি সারির token আর mask পাশাপাশি ছাপাও, আর loss-এ কত token তা গুনে দেখাও। ২. eos_token আর chat template কোথা থেকে আসছে বলো; training আর generation-এ একই template. ৩. ডেটা থেকে হুবহু ও প্রায় হুবহু সারি বাদ দাও; held-out প্রশ্নের সঙ্গে train-এর মিল খোঁজো। ৪. Held-out-এ greedy উত্তর আর আগে থেকে লেখা rubric; বাইরের বিষয়ের কয়েকটি প্রশ্নে আগে ও পরের উত্তর। ৫. কোনো স্কোর আগাম লিখো না; ২টি seed চালানোর ব্যবস্থা রাখো।

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

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

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

প্র ০১LIMA-র ১,০০০টি উদাহরণের ফল কি বলে যে SFT-এ ডেটা কম হলেই চলে? দাবিটির সীমা কোথায়?

Zhou ও সহকর্মীরা ২০২৩ সালে ৬৫B parameter-এর LLaMa-কে মাত্র ১,০০০টি যত্নে বাছাই করা প্রশ্ন-উত্তরে শেখান, কোনো RLHF ছাড়া। উদাহরণগুলো এসেছিল কয়েকটি প্রশ্নোত্তর-ফোরাম আর লেখকদের নিজেদের লেখা থেকে, উত্তরের ধরন ইচ্ছা করে একরকম। তাঁদের মানুষ-মূল্যায়নে উত্তরগুলো অনেক ক্ষেত্রে তখনকার বাণিজ্যিক model-এর সঙ্গে তুলনীয় বলে বিচার হয়। এ থেকে তাঁরা “superficial alignment” ধারণা দেন: জ্ঞান আসে pretraining থেকে, SFT শুধু কোন ছাঁচে কথা বলতে হবে তা শেখায়।

এর তিনটি সীমা আছে। এক, base model বড় আর শক্তিশালী ছিল; ছোট model-এর ভেতরে যা নেই, ১,০০০টি উদাহরণ তা যোগ করতে পারবে না। দুই, মূল্যায়ন ছিল সাধারণ সহায়তার প্রশ্নে, পছন্দের তুলনায়; গণিত, কোড বা কোনো বিশেষ ক্ষেত্রের নির্ভুলতা আলাদা ব্যাপার। তিন, “কম ডেটা” মানে সস্তা ডেটা নয়: প্রতিটি উদাহরণ মানুষ বেছেছেন আর অনেকগুলো নিজে লিখেছেন। পরে Gekhman ও সহকর্মীরা (EMNLP ২০২৪) একটি নিয়ন্ত্রিত পরীক্ষায় দেখান, fine-tuning-এ নতুন তথ্যওয়ালা উদাহরণ ধীরে শেখা হয়, আর শেখা হয়ে গেলে model-এর hallucination বাড়ার প্রবণতা দেখা যায়; এটিও একই দিকে ইঙ্গিত করে যে SFT জ্ঞান ঢোকানোর ভালো পথ নয়।

এই পর্বের ছোট পরীক্ষাও একই শিক্ষা দেয়, তবে উল্টো দিক থেকে: বাজারঘরের নিয়ম model-এর ভেতরে কোথাও ছিল না, তাই SFT সেগুলো মুখস্থ করিয়েছে, আর যেখানে প্রশ্নের ধরন নতুন, সেখানে ভুল হয়েছে। বাংলার মতো ভাষায় ছোট model-এর ক্ষেত্রে “কত উদাহরণ যথেষ্ট” প্রশ্নের উত্তর নিজের held-out মূল্যায়ন দিয়েই দিতে হবে।

মূল উপলব্ধি: SFT মূলত আচরণ আর ছাঁচ শেখায়; ভালো base model আর পরিষ্কার, বৈচিত্র্যময় ডেটা থাকলে অল্প উদাহরণে অনেক দূর যাওয়া যায়, কিন্তু নতুন জ্ঞান ঢোকাতে এটি দুর্বল আর ঝুঁকিপূর্ণ।

প্র ০২বড় একটি hosted model দিয়ে SFT ডেটা বানিয়ে ছোট model শেখানো (Self-Instruct, Alpaca-ধাঁচ) কতটা নিরাপদ? প্রযুক্তি আর শর্ত দুই দিক থেকে ভাবুন।

Wang ও সহকর্মীদের Self-Instruct (২০২২) ১৭৫টি মানুষের লেখা কাজ থেকে শুরু করে GPT-3 দিয়ে নতুন নির্দেশ, ইনপুট আর উত্তর বানায়, খারাপ বা প্রায়-কপি সারি ছেঁকে ফেলে, আর সেই ডেটায় GPT-3-কেই শেখায়; তাঁদের রিপোর্টে Super-NaturalInstructions-এ মূল model-এর চেয়ে প্রায় ৩৩ পয়েন্ট উন্নতি। ২০২৩ সালে Stanford-এর Alpaca একই ধাঁচে text-davinci-003 দিয়ে ৫২ হাজার উদাহরণ বানিয়ে LLaMA 7B শেখায়।

প্রযুক্তির দিকে তিনটি ঝুঁকি। এক, শিক্ষক model-এর ভুল আর বানানো তথ্য ছাত্র নিখুঁতভাবে শেখে, আর আত্মবিশ্বাসী সুরে বলে। দুই, বৈচিত্র্য কমে: একই model-এর লেখা হাজার উত্তর একই দৈর্ঘ্য, একই শুরু, একই ভঙ্গি। Shumailov ও সহকর্মীরা (Nature, ২০২৪) দেখান, model-এর লেখায় বারবার model শেখালে বণ্টনের কিনারার জিনিস হারিয়ে যেতে পারে; বাস্তবে মানুষের ডেটা মিশিয়ে রাখলে ঝুঁকি কতটা কমে, তা নিয়ে আলোচনা চলছে। তিন, মূল্যায়নের দূষণ: শিক্ষক model যদি benchmark-এর প্রশ্ন দেখে থাকে, তার উত্তরে সেগুলোর ছায়া ঢুকতে পারে।

শর্তের দিকে: Alpaca-র ঘোষণায় বাণিজ্যিক ব্যবহার নিষেধ ছিল, LLaMA-র লাইসেন্স আর OpenAI-এর শর্ত দুটোরই কারণে। আজ প্রতিটি hosted সেবার নিজের শর্ত আছে, আর তা সময়ের সঙ্গে বদলায়; কোনো কোনোটি আউটপুট দিয়ে প্রতিযোগী model বানানো সীমিত করে। তাই ডেটা বানানোর আগে উৎসের শর্ত পড়া, কোথা থেকে কী এসেছে তা লিখে রাখা (dataset card), আর দরকার হলে আইনি পরামর্শ নেওয়া জরুরি।

মূল উপলব্ধি: Synthetic ডেটা দ্রুত শুরু করার ভালো উপায়, কিন্তু উৎসের ভুল, একঘেয়েমি আর ব্যবহারের শর্ত সবই সঙ্গে আসে; ছেঁকে নেওয়া, মানুষের ডেটা মেশানো আর উৎস নথিভুক্ত করা ছাড়া একে বিশ্বাস করবেন না।

প্র ০৩বাজারঘরের ডেলিভারি চার্জ আগামী মাসে বদলাবে। নিয়মগুলো SFT দিয়ে model-এর weight-এ ঢোকানো ভালো, নাকি prompt বা retrieval দিয়ে দেওয়া?

এই পর্বের পরীক্ষায় দুটি পথই আছে: system prompt-এ নিয়ম লিখে দেওয়া instruct model, আর নিয়মগুলো উদাহরণে দেখিয়ে SFT. SFT-এর সুবিধা স্পষ্ট: প্রতিটি অনুরোধে লম্বা নিয়মের তালিকা পাঠাতে হয় না, তাই token আর latency কম; আর ছাঁচ, সুর, স্বাক্ষর একরকম থাকে। কিন্তু অসুবিধাগুলো বড়। চার্জ ৬০ থেকে ৭০ টাকা হলে পুরো ডেটা বদলে আবার শেখাতে হবে, আর পুরোনো সংখ্যা weight-এ থেকে যেতে পারে; কোন সংখ্যা model কোথা থেকে বলছে, তার কোনো উৎস দেখানো যায় না।

গবেষণাও সাবধান করে। LIMA-র লেখকেরা বলেন জ্ঞান আসে pretraining থেকে; Gekhman ও সহকর্মীরা দেখান নতুন তথ্য fine-tuning-এ ধীরে শেখা হয় আর hallucination-এর প্রবণতা বাড়াতে পারে। এই পর্বেই দেখবেন, ছোট model held-out-এ নতুন ধরনের প্রশ্নে বেশ কিছু তথ্য ভুল বলেছে; ১১৮টি উদাহরণ নিয়মগুলো নিখুঁত মুখস্থ করায়নি।

প্রচলিত শিল্প-চর্চা তাই ভাগ করে নেয়: যা ঘন ঘন বদলায় আর যার উৎস দেখানো দরকার (দাম, নীতি, স্টক), তা retrieval বা system prompt দিয়ে দেওয়া, যা পর্ব ৬৯ (RAG — chunking, retrieval, reranking আর উত্তর তৈরি)-এর বিষয়; আর যা স্থির (সুর, ছাঁচ, কোন প্রশ্নে কী জিজ্ঞেস করতে হবে, কখন মানুষের কাছে পাঠাতে হবে) তা SFT দিয়ে শেখানো। দুটি মেশানোও যায়: SFT ডেটায় prompt-এর ভেতরে নিয়মের টুকরো রেখে model-কে শেখানো, “দেওয়া নিয়ম থেকে উত্তর দাও”। তখন model তথ্য মুখস্থ না করে প্রসঙ্গ থেকে পড়তে শেখে।

মূল উপলব্ধি: বদলায় এমন তথ্য weight-এ নয়, প্রসঙ্গে দিন; SFT দিয়ে শেখান আচরণ আর প্রসঙ্গ ব্যবহারের অভ্যাস।

প্র ০৪Chat template এত গুরুত্বপূর্ণ কেন? Training আর ব্যবহারের সময় ছাঁচ না মিললে কী কী ভাঙতে পারে?

Model “বার্তা” চেনে না, চেনে token-এর একটি সারি। SFT-এর সময় সে শেখে: <|im_start|>assistant আর একটি newline-এর পরে উত্তর শুরু হয়, আর উত্তর শেষ হয় <|im_end|>-এ। চালানোর সময় এই সংকেতগুলো না থাকলে, বা অন্য ছাঁচে থাকলে, model এমন জায়গায় দাঁড়ায় যা সে প্রায় দেখেনি। ফল নীরব: কোনো error নেই, শুধু উত্তর দুর্বল, ভুল ভাষায়, বা না থেমে পরের “user” পালাও নিজে লিখে ফেলে।

ভাঙার জায়গা কয়েকটি। এক, EOS: এই পর্বে দেখেছেন, Qwen base-এর EOS <|endoftext|>, অথচ পালা শেষ হয় <|im_end|>-এ; training-এ পালা-শেষের token mask-এর বাইরে থাকলে model থামতে শেখে না। TRL এ জন্য আলাদা সতর্কবার্তা রাখে। দুই, ডিফল্ট system বার্তা: Qwen2.5-এর base আর instruct template-এ ডিফল্ট লেখা আলাদা; system না দিলে দুটি ভিন্ন prompt তৈরি হয়। তিন, whitespace আর newline: একটি বাড়তি space tokenization বদলে দেয়, আর model-এর দেখা সারি সরে যায়। চার, special token-এর ভূমিকা নিয়ে নিরাপত্তা: ব্যবহারকারীর লেখায় <|im_end|> থাকলে তা আসল সীমানা হয়ে যেতে পারে (পর্ব ৫২-এ দেখেছেন), যা পর্ব ৭৩ (LLM নিরাপত্তা)-এর বিষয়।

ঐতিহাসিকভাবে প্রতিটি পরিবার নিজের ছাঁচ বানিয়েছে; ChatML-ধাঁচের <|im_start|> তার একটি। Hugging Face ২০২৩ সালের দিকে tokenizer-এর সঙ্গে Jinja template রাখার ব্যবস্থা করে, যাতে apply_chat_template সব জায়গায় একই লেখা দেয়। TRL-এর {% generation %} চিহ্ন তার ওপরে আরেকটি স্তর: কোন অংশ assistant লেখে, তা template নিজেই জানায়।

মূল উপলব্ধি: Template হলো training আর চালানোর মধ্যে চুক্তি। একই tokenizer-এর apply_chat_template দুই জায়গায় ব্যবহার করুন, আর একটি সারি ছাপিয়ে নিজের চোখে মিলিয়ে নিন।

প্র ০৫SFT-এর পর model “ভালো হয়েছে” কীভাবে বলবেন? Exact match, rubric আর LLM-as-judge-এর তুলনা করুন।

খোলা লেখার উত্তরে একটি “সঠিক” string থাকে না। Exact match শুধু এমন কাজে চলে যেখানে উত্তর ছোট আর নির্দিষ্ট (একটি সংখ্যা, একটি শ্রেণি); “৩–৫ দিনে, চার্জ ১২০ টাকা” আর “ঢাকার বাইরে ৩ থেকে ৫ দিন লাগে” দুটিই ঠিক, কিন্তু string আলাদা। তাই এই পর্বে একটি rubric ব্যবহার করা হয়েছে, model চালানোর আগে লেখা: মূল তথ্য আছে কি না, বানানো সংখ্যা নেই তো, বাংলায় কি না, সংক্ষিপ্ত আর থেমেছে কি না। প্রতিটি নিয়ম যন্ত্রে যাচাইযোগ্য, আর সব উত্তর নিজের চোখেও পড়া হয়েছে।

Rubric-এর দুর্বলতাও আছে: শব্দ-মেলানো নিয়ম প্রতিশব্দ ধরে না (“সাত দিন” বনাম “৭ দিন”), আর ভুল প্রসঙ্গে ঠিক শব্দ থাকলেও নম্বর দেয়। বড় পরিসরে অনেকে আরেকটি LLM-কে বিচারক বানান; Zheng ও সহকর্মীরা ২০২৩ সালে MT-Bench আর Chatbot Arena-র কাজে দেখান, শক্তিশালী বিচারক model মানুষের সঙ্গে অনেকটা মেলে, কিন্তু অবস্থান, দৈর্ঘ্য আর নিজের লেখার প্রতি পক্ষপাত আছে। এই বিষয়টি পর্ব ৫৮ (LLM মূল্যায়ন — benchmark, LLM-as-judge আর তার ফাঁদ)-এ মেপে দেখানো হবে।

যে পদ্ধতিই নিন, তিনটি জিনিস লাগবে। এক, held-out set, যেখানে প্রশ্নের ধরন train থেকে আলাদা, শুধু শব্দ নয়। দুই, baseline: SFT-এর আগের model আর শুধু prompt-এ নিয়ম দেওয়া model. তিন, বাইরের বিষয়ের কিছু প্রশ্ন, যাতে format overfitting আর forgetting ধরা পড়ে। আর ১৯টি প্রশ্নে ৫ শতাংশের পার্থক্যও এক-দুটি উত্তরের ব্যাপার; ছোট সেটের সংখ্যাকে সাধারণ ফল ভাববেন না।

মূল উপলব্ধি: আগে মানদণ্ড, তারপর model চালানো; আলাদা ধরনের held-out, baseline আর বাইরের প্রশ্ন ছাড়া SFT-এর কোনো উন্নতির দাবি টেকে না।

অনুশীলন

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

  1. Numerical: একটি এক-পালার SFT উদাহরণে system অংশ ৮০ token, user অংশ ৪০ token, আর assistant অংশ ৩২ token, যার প্রথম ২টি (<|im_start|>, assistant) মাথা। Prompt-এর প্রতিটি token-এর −log p গড়ে ০.৪, উত্তরের প্রতিটির গড়ে ১.৮. (ক) Assistant-only loss-এ কত token গোনা হবে? (খ) Mask-সহ আর mask ছাড়া গড় loss কত? (মাথার ২টি token-কে prompt ধরুন।) (গ) Mask ছাড়া মোট loss-এর কত ভাগ উত্তর থেকে আসে?

    (ক) ৩২ − ২ = ৩০ token. (খ) Mask-সহ শুধু উত্তরের গড়: ১.৮. Mask ছাড়া: prompt token = ৮০ + ৪০ + ২ = ১২২; মোট = (১২২ × ০.৪ + ৩০ × ১.৮) / ১৫২ = (৪৮.৮ + ৫৪) / ১৫২ = ১০২.৮ / ১৫২ ≈ ০.৬৭৬. (গ) ৫৪ / ১০২.৮ ≈ ৫২.৫%, যদিও token-এর হিসাবে উত্তর মাত্র ৩০/১৫২ ≈ ২০%. লক্ষ করুন, mask ছাড়া log-এর ০.৬৭৬ দেখে মনে হবে model অনেক ভালো, অথচ উত্তরের আসল loss ১.৮.

  2. নকশা ও সমালোচনা: একজন সহকর্মী লিখেছেন: “বড় একটি hosted chatbot দিয়ে ৫,০০০টি বাংলা সাপোর্ট-উত্তর বানিয়েছি, dedup করিনি। প্রতিটির শুরুতে ৩০০ token-এর একই system prompt. messages ফরম্যাটে TRL-এ ডিফল্ট সেটিংয়ে SFT করেছি, ১০ epoch. মূল্যায়ন করেছি train থেকে এলোমেলো ১০০টি প্রশ্ন নিয়ে, ৯৮% ঠিক।” কী কী বলবেন?

    (১) শর্ত: ঐ সেবার আউটপুট দিয়ে model শেখানোর অনুমতি আছে কি না আগে পড়ুন, আর উৎস নথিভুক্ত করুন। (২) Dedup: একই model-এর লেখায় হুবহু আর প্রায়-কপি অনেক থাকে; সরান, আর বাদ পড়া সারি চোখে দেখুন। (৩) Loss: TRL 1.17-এ messages-ডেটার ডিফল্ট পুরো সারিতে loss; ৩০০ token-এর system prompt প্রতিবার loss-এ ঢুকছে। assistant_only_loss=True দিন, mask ছাপিয়ে যাচাই করুন। (৪) ১০ epoch: ছোট ছাঁচ-ভরা ডেটায় format overfitting আর মুখস্থের ঝুঁকি; validation loss আর বাইরের প্রশ্ন দেখুন। (৫) মূল্যায়ন: train থেকে নেওয়া প্রশ্ন মানে contamination; ৯৮% মুখস্থ মাপে। আলাদা ধরনের held-out, আগে লেখা rubric, baseline (SFT-এর আগে, আর prompt-এ নিয়ম দিয়ে) আর কয়েকটি seed লাগবে। (৬) তথ্য: উৎস model-এর ভুল সংখ্যা যাচাই করুন; বাজারঘরের আসল নীতির সঙ্গে মিলিয়ে নিন।

  3. আউটপুট অনুমান করুন: অংশ ৩-এর render() ধরে, কোড না চালিয়ে বলুন নিচের তিন লাইন কী ছাপবে।
    toks, turn = render([("user", "দাম কত?"), ("assistant", "৫০০ টাকা")])
    print(len(toks), int((turn > 0).sum()))
    print(toks[4:])

    প্রতিটি পালা = ২টি মাথা + ⏎ + শব্দ + </s> + ⏎. User: ২ + ১ + ২ + ১ + ১ = ৭; assistant: ২ + ১ + ২ + ১ + ১ = ৭. প্রথম লাইন: 14 5, কারণ assistant-এর মাথার ২টি ছাড়া বাকি ৫টি loss-এ। দ্বিতীয় লাইন: ['কত?', '</s>', '⏎', '<s>', 'assistant', '⏎', '৫০০', 'টাকা', '</s>', '⏎']. এখানে কোনো system নেই; খেলনা template-এ ডিফল্ট system বসে না, কিন্তু Qwen-এর আসল template বসায়, তাই আসল token-সংখ্যা বেশি হতো।

  4. একটি ফল পড়ুন: একটি কাল্পনিক SFT-এর হিসাব: held-out rubric গড় SFT-এর আগে ০.৪০, ৩ epoch-এ ০.৮৫, ১০ epoch-এ ০.৮৮. বাইরের বিষয়ের ১০টি প্রশ্নে (অঙ্ক, ভূগোল) সাপোর্ট-স্বাক্ষর এসেছে আগে ০টি, ৩ epoch-এ ৩টি, ১০ epoch-এ ৯টি. (ক) কোন checkpoint বাছবেন? (খ) আর কী মাপবেন? (গ) সমস্যা কমানোর দুটি উপায়।

    (ক) ৩ epoch: held-out-এ ০.০৩-এর লাভ ১০ epoch-এর পক্ষে যুক্তি নয়, অথচ বাইরের প্রশ্নে ছাঁচ চাপানো ৩ থেকে ৯-এ উঠেছে। এটি format overfitting-এর ক্লাসিক ছবি। (খ) বাইরের প্রশ্নের উত্তর আসলে ঠিক কি না (শুধু স্বাক্ষর নয়), কয়েকটি seed-এ একই মাপ, validation loss-এর পথ, আর held-out-এর কোন প্রশ্নগুলো বদলেছে। (গ) ডেটায় বৈচিত্র্য আনা: সাপোর্টের বাইরের সাধারণ প্রশ্ন-উত্তর মিশিয়ে রাখা, যাতে model শেখে কখন ছাঁচ লাগে না; স্বাক্ষরের মতো স্থির লেখা সব উদাহরণে না রাখা; কম epoch বা ছোট lr; আর চাওয়া আচরণ হলে বাইরের প্রশ্নে “আমি শুধু বাজারঘরের বিষয়ে সাহায্য করতে পারি” ধাঁচের উদাহরণ।

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

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

ক্রমএকটি ছোট SFT প্রকল্পের ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. লক্ষ্য-আচরণ লেখা, আর held-out প্রশ্ন ও rubric আগে থেকে ঠিক করা
  2. ডেটা জোগাড় বা লেখা, উৎস আর লাইসেন্স নথিভুক্ত করা
  3. Dedup, মান-ফিল্টার, আর held-out-এর সঙ্গে মিল খোঁজা
  4. Chat template-এ একটি সারি ছাপিয়ে mask আর EOS মিলিয়ে দেখা
  5. LoRA SFT, কয়েকটি seed, validation loss দেখা
  6. Held-out-এ rubric, baseline আর বাইরের প্রশ্নে আগে ও পরে তুলনা

Assistant-only loss-এ system আর user অংশের label −100 করা হয়। এতে training-এর সময় কী ঘটে?

  • Model আর system আর user-এর লেখা দেখতে পায় না
  • Model পুরো লেখা প্রসঙ্গ হিসেবে দেখে, কিন্তু শুধু উত্তরের token অনুমানে loss হয়
  • ঐ token-গুলোতে attention বন্ধ হয়ে যায়
  • Model ঐ ঘরে একটি বিশেষ “ফাঁকা” token শেখে

একটি উদাহরণে prompt ১২০ token, যার প্রতিটির −log p গড়ে ০.৫; উত্তর ৩০ token, প্রতিটির গড়ে ২.০. Prompt mask না করলে log-এ গড় loss কত দেখাবে?

  • ২.০
  • ১.২৫
  • ০.৮
  • ০.৫

TRL 1.17-এ messages ফরম্যাটের ডেটায় SFTConfig(assistant_only_loss=True) কাজ করতে model-এর chat template-এ কী লাগে?

  • শুধু add_generation_prompt সমর্থন
  • ডেটায় আলাদা prompt আর completion কলাম
  • একটি slow tokenizer
  • Assistant অংশ ঘিরে {% generation %} … {% endgeneration %} চিহ্ন

SFT-এ assistant উত্তরের শেষের <|im_end|> token-কে ভুল করে mask-এর বাইরে (−100) রাখলে সবচেয়ে সম্ভাব্য সমস্যা কোনটি?

  • Model উত্তর শেষে থামতে শেখে না, লেখা গড়িয়ে চলে
  • Model উত্তর শুরুই করতে পারে না
  • Training loss NaN হয়ে যায়
  • Tokenizer থেকে token-টি মুছে যায়

SFT ডেটা পরিষ্কারের সময় দুটি সারির অক্ষর-ত্রয়ী সেটে ছেদ ৪৫ আর সংযোগ ৫০. Jaccard similarity কত, আর ০.৮৫ সীমায় কী হবে?

  • ০.৫৬, দুটিই থাকবে
  • ০.৯০, দ্বিতীয়টি প্রায়-কপি হিসেবে বাদ
  • ১.১১, দুটিই বাদ
  • ০.৪৭, দুটিই থাকবে

সাপোর্ট-ডেটায় SFT-এর পর model “পানি কত ডিগ্রিতে ফোটে?” প্রশ্নেও ডেলিভারি চার্জের কথা আর দোকানের স্বাক্ষর জুড়ে উত্তর দিচ্ছে। এটিকে সবচেয়ে ভালো কী বলে?

  • Underfitting
  • Data contamination
  • Format overfitting
  • Tokenizer-এর ভুল

শব্দকোষ

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

মনে রাখুন

  1. Base বনাম instruct: base model লেখা চালিয়ে যায়; SFT শেখায় chat template-এ প্রশ্ন পেলে উত্তর দিয়ে <|im_end|>-এ থামতে।
  2. ফরম্যাট: instruction–input–output, prompt–completion বা messages; chat model-এর জন্য messages, আর training ও চালানোয় একই template আর একই EOS.
  3. Loss: L = −(১/|A|) ∑t∈A log p(xt | x<t); prompt-এর label −১০০. TRL 1.17-এ messages-ডেটায় এটি assistant_only_loss=True দিয়ে চালু করতে হয়, আর template-এ {% generation %} লাগে।
  4. ডেটা: পরিমাণের চেয়ে মান; dedup, ফিল্টার, বাদ পড়া সারি চোখে দেখা; synthetic ডেটার ভুল আর শর্ত; held-out-এর সঙ্গে মিল খোঁজা।
  5. মূল্যায়ন: আগে লেখা rubric, আলাদা ধরনের held-out, baseline, কয়েকটি seed, আর বাইরের প্রশ্নে format overfitting দেখা।

পরের পর্বে: পর্ব ৫৭ (Preference alignment — RLHF, reward model আর DPO): SFT শেখায় “এভাবে উত্তর দাও”; পছন্দের ডেটা শেখায় দুটি উত্তরের কোনটি ভালো।

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

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