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

Prompt পরীক্ষা — eval set, regression আর failure mode

Prompt Evaluation: Eval Sets, Regression Tests and Failure Modes

Taught in Bangla, this lesson tests prompts like code. It designs an eval set from a coverage grid of task, language (Bangla, Banglish, English) and edge cases, picks a metric per task from exact match and field-level F1 to rubrics, tallies failure modes, keeps versioned answers for regression diffs, and compares prompt versions and models with paired bootstrap and McNemar on 60 questions with two small open Qwen2.5 models in a real desktop run, with in-page Python checks.

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

  • একটি eval set নকশা করা: coverage ছক, edge case, বাংলা আর বাংলিশ
  • প্রতিটি কাজের জন্য ঠিক মাপকাঠি: exact match, field-level F1, rubric আর বিচারক model
  • Failure mode-এর তালিকা বানিয়ে ভুলগুলো ভাগ করে গোনা
  • Regression test: version ধরে উত্তর রেখে diff, আর নতুন করে ফেল করা প্রশ্ন খোঁজা
  • দুটি prompt বা দুটি model-এর paired bootstrap তুলনা, আর নতুন version চালু করার সিদ্ধান্তের নিয়ম

এই পর্বের desktop run-এ দুটি model: Qwen/Qwen2.5-0.5B-Instruct আর Qwen/Qwen2.5-1.5B-Instruct (দুটিই Apache 2.0, token ছাড়া নামানো যায়), torch 2.15.0, transformers 5.22.2, Python 3.12, লেখার সময়ে। পাতায় script ছোট করে দেখানো, আউটপুট হুবহু। Eval set-টি এই পর্বের জন্য হাতে লেখা বাজারঘর-এর নমুনা, আসল গ্রাহকের নয়। সব সংখ্যা একটি স্থানীয় run-এর, ছোট model আর ছোট set-এ; বড় কোনো পণ্য একই পরীক্ষায় অন্যরকম আচরণ করতে পারে। CI, জোড়া তুলনা আর বিচারক model-এর পক্ষপাতের মূল কথা পর্ব ৫৮-এ; এখানে সেগুলো ব্যবহার করব, আবার শেখাব না।

১ · Prompt-এর test কেন লাগে

রান্নাঘরে নতুন বাবুর্চি এলে রেসিপিতে একটু লবণ কম, একটু মশলা বেশি করেন। মালিক কী করেন? পাঁচটি চেনা পদ রাঁধিয়ে চেখে দেখেন, আর আগের বাবুর্চির খাতার সঙ্গে মেলান। শুধু একটি পদ চাখলে বিরিয়ানি ভালো হলেও ডাল নষ্ট হয়েছে কি না জানা যায় না।

Prompt বদলানো ঠিক এমন। পর্ব ৬০-এ দেখেছেন, ছোট শব্দ-বদলেও ফল নড়ে (prompt sensitivity). কিন্তু সাধারণ কোডের মতো এখানে compiler নেই, type check নেই। একটি নির্দেশ যোগ করলে হিসাবের প্রশ্ন ঠিক হতে পারে, আর একই সঙ্গে ইংরেজি প্রশ্নে উত্তর ইংরেজিতে চলে যেতে পারে। চোখে দু-তিনটি উদাহরণ দেখে এটি ধরা যায় না।

সমাধান সফটওয়্যারের পুরনো অভ্যাস: test. Software Engineering কোর্সের পর্ব ৪৭-এ equivalence partitioning আর boundary value দিয়ে test case বানানো দেখেছেন; এখানে একই চিন্তা, শুধু “প্রত্যাশিত আউটপুট” হুবহু লেখা যায় না বলে মাপকাঠি একটু নরম। লেখার সময়ে (নভেম্বর ২০২৬) OpenAI-এর evaluation best practices নথি একে বলে “eval-driven development” আর পরামর্শ দেয় প্রতিটি বদলে eval চালাতে (“run evals on every change”); পরিবর্তন হতে পারে। পুরো চক্রটি নিচের ছবিতে।

Prompt evaluation loop eval setধরন × ভাষা × edge prompt vNhash, তারিখ, কারণ model-এ চালানোএকই decoding, seed লেখা উত্তর জমাouts/model/vN.json checkerপ্রতিটি উত্তরে failure mode diff আর জোড়া CIনতুন ফেল, নতুন পাশ চালু আবার বদলানো new failure → new item
Prompt পরীক্ষার চক্র। প্রতিটি version-এর উত্তর জমা থাকে, তাই পরের version-এর সঙ্গে প্রশ্ন ধরে মেলানো যায়। নতুন ধরনের ভুল পেলে সেটি eval set-এ নতুন প্রশ্ন হয়, যাতে আর ফিরে না আসে।

২ · Eval set নকশা — ছক আগে, প্রশ্ন পরে

পরীক্ষার প্রশ্নপত্র যিনি বানান, তিনি আগে একটি ছক করেন: কোন অধ্যায় থেকে কত নম্বর, কতটি সহজ, কতটি কঠিন। তারপর প্রশ্ন লেখেন। Eval set-ও তেমন। প্রথমে coverage ছক: এক দিকে কাজের ধরন, আরেক দিকে ভাষা, আর প্রতিটি ঘরে কোন edge case থাকবে। Edge case মানে কিনারার প্রশ্ন, যেখানে ভুলের সম্ভাবনা বেশি: ঠিক ২০০০ টাকার অর্ডার, জেলার নাম দিয়ে প্রশ্ন (“রাজশাহী” মানে ঢাকার বাইরে), রাগী গ্রাহক।

বাজারঘরের support bot-এর জন্য পাঁচটি ধরন: তথ্য (চার্জ, সময়, ফেরত, পেমেন্ট), হিসাব (দাম + চার্জ), অজানা (দোকানের প্রশ্ন, কিন্তু নিয়মে নেই), মানুষ (ভাঙা পণ্য, টাকা ফেরত, মানুষ চাওয়া) আর বাইরের বিষয়। প্রতিটি ধরন তিন ভাষায়, প্রতি ঘরে ৪টি: মোট ৬০টি প্রশ্ন। নিচে একটি একটি সারি যোগ করার সিদ্ধান্ত।

নতুন সারি আর তার কারণ
প্রশ্ন
ধরন · ভাষা
প্রত্যাশা
কেন
  1. ভিত্তি. প্রথমে সবচেয়ে সরল প্রশ্ন। এটি ফেল করলে বাকিগুলো দেখার আগেই বোঝা যায় prompt-এর মৌলিক কিছু ভাঙা।
  2. জেলার নাম. নিয়মে লেখা “ঢাকার বাইরে”, প্রশ্নে “রাজশাহী”। Model-কে এক ধাপ অনুমান করতে হয়; বাস্তবের গ্রাহক এভাবেই লেখেন।
  3. সীমার ওপরে. ২০০০ বা বেশি হলে চার্জ ০, তাই উত্তর ২০০০। Boundary value test-এর মতো: “বা তার বেশি” কথাটি ঠিক পড়া হলো কি না।
  4. সীমার নিচে. ১৯০০-তে চার্জ লাগে: ১৯০০ + ৬০ = ১৯৬০. একই নিয়মের দুই পাশ, আর বাংলিশে Latin অঙ্ক।
  5. অজানা. নিয়মে শুক্রবারের কথা নেই। ঠিক আচরণ “তথ্য নেই”; “হ্যাঁ, হয়” বললে সেটি বানানো নীতি।
  6. রাগ আর বাংলিশ. বিস্ময়চিহ্ন, ছোট বাক্য, বাংলিশ: তবু টাকা ফেরতের অনুরোধ, তাই মানুষের কাছে পাঠাতে হবে।
  7. ফাঁদ. প্রশ্নে সংখ্যা আছে, শুনতে হিসাবের মতো, কিন্তু দোকানের বিষয় নয়। ১৫৬ বলে দিলে নির্দেশ উপেক্ষা।

ছক বানানোর পর যন্ত্র দিয়ে ফাঁক খোঁজা সহজ। নিচের coverage checker পুরো ৬০টি প্রশ্ন পড়ে (ফাইলটি এই পাতাতেই রাখা), ধরন × ভাষার গণনা দেখায়, আর আগে লেখা edge case-এর তালিকার সঙ্গে মেলায়। তালিকাটি ইচ্ছা করে ফল দেখার আগে লেখা, আর তাতে এমন কয়েকটি edge আছে যা set-এ রাখা হয়নি।

import pandas as pd
ev = pd.read_csv("bazar_eval.csv")          # এই পর্বের হাতে লেখা বাজারঘর set
print(len(ev), "টি প্রশ্ন")
print(pd.crosstab(ev.cat, ev.lang, margins=True, margins_name="মোট"))

# প্রতিটি ধরনে কোন edge case থাকা উচিত, তা আগে লেখা (ফল দেখার আগে)
NEED = {"info": ["district", "boundary", "synonym", "typo"],
        "calc": ["district", "boundary", "multi", "typo"],
        "unk":  ["abroad", "typo"], "esc": ["human", "angry", "long"],
        "oos":  ["number", "injection"]}
for cat, edges in NEED.items():
    have = ev[ev.cat == cat]
    for e in edges:
        langs = sorted(have[have.edge == e].lang)
        miss = sorted({"bn", "bl", "en"} - set(langs))
        if miss:
            print(f"ফাঁক: {cat:4} {e:9} নেই এই ভাষায়: {', '.join(miss)}")

ev["bn_digit"] = ev.question.str.contains("[০-৯]")
ev["latin_digit"] = ev.question.str.contains("[0-9]")
print(ev.groupby("lang")[["bn_digit", "latin_digit"]].sum())
dup = ev.question.str.lower().str.replace(r"\W", "", regex=True).duplicated()
print("একই প্রশ্ন দুবার:", int(dup.sum()))

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

60 টি প্রশ্ন
lang  bl  bn  en  মোট
cat                  
calc   4   4   4   12
esc    4   4   4   12
info   4   4   4   12
oos    4   4   4   12
unk    4   4   4   12
মোট   20  20  20   60
ফাঁক: info typo      নেই এই ভাষায়: bl, bn, en
ফাঁক: calc typo      নেই এই ভাষায়: bl, bn, en
ফাঁক: unk  abroad    নেই এই ভাষায়: bl
ফাঁক: unk  typo      নেই এই ভাষায়: bl, bn, en
ফাঁক: esc  long      নেই এই ভাষায়: bl, bn, en
ফাঁক: oos  injection নেই এই ভাষায়: bl, bn, en
      bn_digit  latin_digit
lang                       
bl           0            7
bn           6            0
en           0            6
একই প্রশ্ন দুবার: 0

গণনার ছক সুষম: প্রতিটি ঘরে ৪টি। কিন্তু checker ছয়টি ফাঁক ধরেছে। বানান ভুলের (typo) প্রশ্ন একটিও নেই, অথচ আসল গ্রাহক “ডেলিভারী”, “charj” লেখেন। লম্বা, গল্পের মতো অভিযোগ নেই। অজানা ধরনে বিদেশে পাঠানোর প্রশ্ন বাংলা আর ইংরেজিতে আছে, বাংলিশে নেই। শেষ সারণিও শিক্ষণীয়: বাংলা প্রশ্নে সব সংখ্যা বাংলা অঙ্কে, বাংলিশ আর ইংরেজিতে সব Latin. “বাংলা লেখা, Latin অঙ্ক” (যেমন “৮৫০” না লিখে “850 টাকা”) একটিও নেই, তাই সেই মিশ্রণে checker ঠিক কাজ করে কি না, এই set বলতে পারবে না। এগুলো পরের version-এর কাজের তালিকা। Prompt injection-এর প্রশ্ন ইচ্ছা করে এখানে রাখা হয়নি; তার আলাদা set পর্ব ৬৪ · System prompt, role আর conversation design-এ, আর পূর্ণ আলোচনা পর্ব ৭৩ · LLM নিরাপত্তায়।

Eval set-এর আরও চারটি নিয়ম। এক, আসল ব্যবহার থেকে প্রশ্ন নিন (নাম, ফোন নম্বর মুছে), আর প্রতিটি নতুন ভুলের পর একটি প্রশ্ন যোগ করুন। দুই, set দুই ভাগে রাখুন: একটি dev ভাগ, যা দেখে prompt বদলাবেন, আর একটি লুকানো test ভাগ, যা শুধু শেষ সিদ্ধান্তে চলে। একই প্রশ্ন দেখে দেখে prompt ঘষলে prompt সেই প্রশ্নগুলোর জন্য overfit হয়, model training-এর মতোই। তিন, প্রত্যাশিত উত্তর প্রশ্নের পাশেই লিখুন, আর দুজন মিলিয়ে নিন। চার, আকার: পর্ব ৫৮-এর হিসাবে ৬০টি প্রশ্নে পাশের হার ০.৫-এর কাছে থাকলে CI প্রায় ±০.১৩; ধরন ধরে ১২টিতে ভাগ করলে প্রায় ±০.২৮. ধরন-স্তরের সিদ্ধান্তে তাই সাবধান।

৩ · প্রতিটি কাজের মাপকাঠি — exact match থেকে rubric

সব প্রশ্নে এক মাপকাঠি চলে না। ডাক্তার জ্বর মাপেন থার্মোমিটারে, কিন্তু রোগীর ব্যথা মাপেন প্রশ্ন করে। যেখানে উত্তর নির্দিষ্ট, সেখানে কোড দিয়ে কড়া মাপ; যেখানে উত্তর খোলা, সেখানে লিখিত নিয়ম আর বিচারক।

কাজমাপকাঠিবাজারঘরের উদাহরণঅন্ধ জায়গা
শ্রেণি বা ট্যাগExact matchউত্তরের শুরুতে [ESCALATE]ট্যাগ ঠিক, বাকি লেখা ভুল হতে পারে
সংখ্যা বা হিসাবNormalisation-এর পর সংখ্যা মেলানো“১,৩১০ টাকা” → 1310ঠিক সংখ্যা ভুল প্রসঙ্গে
তথ্য বের করাField-level F1অর্ডারের জেলা, পণ্য, দাম (পর্ব ৬২)কোন field বেশি জরুরি, তা F1 জানে না
খোলা উত্তরRubric, মানুষ বা বিচারক modelভদ্রতা, পরের পদক্ষেপ স্পষ্ট কি নাবিচারকের ক্রম আর দৈর্ঘ্যের পক্ষপাত
নিষেধনিয়ম-ভিত্তিক checkerতালিকার বাইরের সংখ্যা নেইশুধু যা লিখে দিয়েছেন, তা-ই ধরে

Field-level F1, একটি হিসাব। অর্ডার থেকে চারটি field বের করার কথা: জেলা, পণ্য, দাম, পরিমাণ। Model দিল জেলা ঠিক, পণ্য ঠিক, দাম ভুল, আর একটি বাড়তি field “রং” যা চাওয়া হয়নি; পরিমাণ দেয়নি। ঠিক = ২, দেওয়া = ৪ (জেলা, পণ্য, দাম, রং), চাওয়া = ৪. Precision = ২/৪ = ০.৫০, recall = ২/৪ = ০.৫০, F1 = ২ × ০.৫ × ০.৫ ÷ (০.৫ + ০.৫) = ০.৫০. পুরো উত্তরের exact match এখানে ০, কিন্তু F1 বলে অর্ধেক কাজ হয়েছে, আর কোন field-এ ভুল, তা-ও। F1-এর মূল সংজ্ঞা পর্ব ৪৫-এ।

বিচারক model. খোলা উত্তরে একটি LLM-কে rubric দিয়ে নম্বর দেওয়ানো যায়। পর্ব ৫৮-এ মেপেছেন, ছোট বিচারক ক্রম বদলালেই রায় বদলায় (position bias), আর লম্বা উত্তরের দিকে হেলে (verbosity bias). লেখার সময়ে OpenAI-এর নথিও দৈর্ঘ্য নিয়ন্ত্রণ, pairwise বা pass/fail রায় আর মানুষের লেবেলের সঙ্গে মিলিয়ে তবেই বড় পরিসরে চালানোর কথা বলে; Anthropic-এর নথি বলে যে model উত্তর বানিয়েছে, বিচারে অন্য model নেওয়া সাধারণত ভালো। এই পর্বে বিচারক model নেই: সব মাপ কোডে, যাতে প্রতিবার একই রায় আসে আর ভুলগুলো হাতে মেলানো যায়।

মাপকাঠি prompt-এর ওপর নির্ভর করবে না। Prompt v1 আর v2 যা-ই বলুক, eval-এর চাহিদা একটি: উত্তর ট্যাগ দিয়ে শুরু, বাংলায়, ৪৫ শব্দের মধ্যে, নিয়মের বাইরের সংখ্যা নয়। Prompt বদলের সঙ্গে মাপকাঠিও বদলালে তুলনার অর্থ থাকে না।

৪ · সত্যিকারের run — তিনটি prompt, দুটি model

System prompt-এর তিনটি version, তিনটিতেই একই নিয়মের তালিকা (চার্জ ৬০/১২০, ২০০০ বা বেশিতে চার্জ ০, সময়, ফেরত ৭ দিন, পেমেন্ট):

  • v1 (hash 684847f7): নিয়ম, আর এক লাইনে “শুরুতে একটি ট্যাগ দিন, বাংলায় উত্তর দিন”। প্রথম খসড়া যেমন হয়।
  • v2 (fafdd1e2): শিরোনামে ভাগ, প্রতিটি ট্যাগের মানে আর “লিখুন: …” দিয়ে নির্দিষ্ট বাক্য, ভাষা আর দৈর্ঘ্যের নিয়ম, হিসাবের সূত্র, দুটি উদাহরণ।
  • v3 (1e44fbed): v2-এর ভুলগুলো পড়ে লেখা। “লিখুন:”-এর বদলে “উত্তর হুবহু:”, চারটি উদাহরণ (বাংলিশ, ইংরেজি, অজানা, বাইরের), আর শেষে ভাষার নিয়ম আবার।

প্রতিটি version ৬০টি প্রশ্নে, তিনভাবে: greedy, আর temperature=0.7, top_p=0.9-এ দুটি sampling run (s1, s2; প্রশ্ন ধরে seed, যেমন 101000 + id), max_new_tokens=120. Generation GPU-তে, float16-এ; একটি run ১.৫B-তে ৭৩ থেকে ১২৩ সেকেন্ড, মাপা সর্বোচ্চ GPU memory বরাদ্দ ৩.২২ GB (০.৫B-তে ১.০৬ GB). মূল লুপ (ছোট করে দেখানো):

# মূল লুপ, একটু ছোট করে (সত্যিকারের run.py থেকে)
sysmsg = PROMPTS[pid]                                  # v1 বা v2
out = {"model": model_id, "prompt": pid, "mode": mode,
       "sha": hashlib.sha256(sysmsg.encode()).hexdigest()[:8], "rows": []}
for (i, cat, lang, edge, q, exp) in ITEMS:              # ৬০টি প্রশ্ন
    msgs = [{"role": "system", "content": sysmsg}, {"role": "user", "content": q}]
    ids = tok.apply_chat_template(msgs, add_generation_prompt=True,
                                  return_tensors="pt", return_dict=True)
    kw = dict(max_new_tokens=120, do_sample=False)      # greedy
    if mode != "greedy":
        torch.manual_seed(SEEDS[mode] * 1000 + i)       # প্রতি প্রশ্নে আলাদা seed
        kw.update(do_sample=True, temperature=0.7, top_p=0.9)
    g = model.generate(**ids, **kw)
    a = tok.decode(g[0, ids["input_ids"].shape[1]:], skip_special_tokens=True).strip()
    out["rows"].append({"id": i, "answer": a, "new_tokens": ...})
json.dump(out, open(f"outs/{short}__{pid}__{mode}.json", "w"), ensure_ascii=False)
~/bazarghar-dl$ uv run python summary.py      # সত্যিকারের run-এর ফল, প্রতিটি সারি ৬০টি প্রশ্ন
model prompt mode   pass format ignore invent  wrong langua verbos
0.5B  v1/6848 greedy    0     60      0      5     22     37     20
0.5B  v1/6848 s1        0     59      1      3     24     39     24
0.5B  v1/6848 s2        0     59      1      6     20     36     29
0.5B  v2/fafd greedy    0     60      0      7     20     36     27
0.5B  v2/fafd s1        0     60      0     12     24     36     30
0.5B  v2/fafd s2        0     60      0      6     20     37     31
0.5B  v3/1e44 greedy    2     56      1      4     21     37     14
0.5B  v3/1e44 s1        1     56      1      7     23     34     23
0.5B  v3/1e44 s2        1     56      2      7     21     35     25
1.5B  v1/6848 greedy    0     57      2      6     16     35     25
1.5B  v1/6848 s1        0     57      2      6     18     31     32
1.5B  v1/6848 s2        0     58      1      9     18     30     37
1.5B  v2/fafd greedy    3     30      8      7     15     28     10
1.5B  v2/fafd s1        1     33      6     11     17     29     15
1.5B  v2/fafd s2        3     33      5      8     18     29     14
1.5B  v3/1e44 greedy    7     42      1      3     16     32     21
1.5B  v3/1e44 s1        4     45      1     11     18     31     25
1.5B  v3/1e44 s2        4     46      0      5     18     35     24

কীভাবে পড়বেন: pass = যে উত্তরে কোনো failure mode নেই (কড়া মাপ: ট্যাগ, বিষয়, ভাষা আর দৈর্ঘ্য সব ঠিক)। পরের ছয়টি ঘর প্রতিটি mode কতবার, একটি উত্তরে একাধিক থাকতে পারে। সংজ্ঞা §৫-এ।

প্রথম শিক্ষা: কড়া মাপে দুটি model-ই খুব দুর্বল। সর্বোচ্চ ৬০-এ ৭. v1-এ দুটি model প্রায় কখনো ট্যাগ লেখেনি। ইংরেজি প্রশ্নে উত্তর প্রায় সবসময় ইংরেজিতে (language ২৮ থেকে ৩৯)। ছোট model-এর এই দুর্বলতা পর্ব ৫৮-এ মাপা বাংলার দুর্বলতার সঙ্গে মেলে। বড় কোনো পণ্যে একই set-এ ফল অনেক আলাদা হতে পারে; এখানকার সংখ্যা দিয়ে কোনো পণ্যের মান বিচার করা যায় না।

দ্বিতীয় শিক্ষা: v2 থেকে v3-এ গড় বাড়ল, কিন্তু কিছু mode খারাপ হলো। ১.৫B greedy-তে পাশ ৩ থেকে ৭, বানানো নীতি ৭ থেকে ৩; কিন্তু ট্যাগ না থাকা (format) ৩০ থেকে ৪২, অতিরিক্ত লম্বা ১০ থেকে ২১. Seed বদলালে ছবি নড়ে: s1-এ বানানো নীতি দুই version-এই ১১. কোন পরিবর্তনটি আসল, তা পরের তিনটি অংশে খুঁজব।

৫ · Failure mode — ভুলের নাম দেওয়া আর গোনা

ডাক্তার শুধু “রোগী অসুস্থ” লেখেন না; রোগের নাম লেখেন, কারণ চিকিৎসা নামের ওপর নির্ভর করে। Failure mode-এর তালিকা একই কাজ করে। এই পর্বের checker ছয়টি নাম দেয়:

ModeমানেChecker কীভাবে ধরেআসল উদাহরণ (১.৫B)
format · ফরম্যাট ভাঙাশুরুতে ঠিক ট্যাগ নেই, বা prompt-এর লেখা নকলregex, আর “লিখুন:”[damage] দুঃখিত, একজন কর্মী…
invented · বানানো নীতিনিয়মে নেই এমন কিছু সত্য বলে দেওয়াঅজানায় [INFO] ট্যাগ, বা তালিকার বাইরের সংখ্যা[INFO] ৬০ টাকা. (কলকাতায় পাঠানো যায়?)
language · ভুল ভাষাউত্তর বাংলায় নয়বাংলা অক্ষর ৬০%-এর কমThe delivery charge inside Dhaka is 60 Taka.
ignored · নির্দেশ উপেক্ষাট্যাগ আছে, কিন্তু ভুল কাজেরপ্রত্যাশিত ট্যাগের সঙ্গে মিল নেই[INFO] 12 * 13 = 156.
verbose · অতিরিক্ত লম্বা৪৫ শব্দের বেশি বা ১২০ token-এ কাটাশব্দ আর token গোনাক্রেডিট কার্ডের EMI-র ধাপে ধাপে বর্ণনা
wrong · ভুল উত্তরতথ্য বা হিসাব ভুলপ্রত্যাশিত সংখ্যা; হিসাবে শেষ সংখ্যা[INFO] ৮৫০ + ৬০ = ৯২০ টাকা. (সিলেট)

এখানে checker-টি পাতাতেই রাখা (checker.py, নিচে দেখানো)। নিচের block এটি দিয়ে ১.৫B-এর তিনটি greedy run-এর ১৮০টি উত্তর গোনে, তারপর ট্যাগের ভুল ধরন ধরে আর ভাষার ভুল প্রশ্নের ভাষা ধরে ভাগ করে।

import re
BN = str.maketrans("০১২৩৪৫৬৭৮৯", "0123456789")
ALLOWED = {"0", "60", "120", "2000", "2", "3", "5", "7"}      # নিয়মের সংখ্যা
WANT = {"info": "INFO", "calc": "INFO", "unk": "UNKNOWN", "esc": "ESCALATE", "oos": "OOS"}
TAG = re.compile(r"^\s*\[(INFO|ESCALATE|UNKNOWN|OOS)\]")

def nums(s):                     # সংখ্যাগুলো ক্রমে, বাংলা অঙ্ক আর কমা normalise করে
    return re.findall(r"\d+", s.translate(BN).replace(",", ""))

def check(cat, q, expected, a, new_tokens):
    """একটি উত্তরের failure mode-এর তালিকা; খালি তালিকা = পাশ"""
    f, m = [], TAG.match(a)
    body = TAG.sub("", a)
    if not m or "লিখুন:" in a:                       # ট্যাগ নেই, বা prompt-এর লেখা নকল
        f.append("format")
    elif m.group(1) != WANT[cat]:
        guess = cat == "unk" and m.group(1) == "INFO" and "তথ্য নেই" not in a
        f.append("invented" if guess else "ignored")
    if cat in ("info", "unk", "esc") and set(nums(body)) - ALLOWED - set(nums(q)):
        if "invented" not in f: f.append("invented")
    if cat in ("info", "calc"):
        exp = [e for e in str(expected).split(";") if e]
        if not exp[0].isdigit():
            ok = any(w in a for w in exp)               # শব্দ: যেকোনো একটি
        elif cat == "calc":
            ok = nums(body)[-1:] == exp                 # হিসাব: শেষ সংখ্যাই উত্তর
        else:
            ok = set(exp) <= set(nums(body))            # তথ্য: সব সংখ্যা আছে
        if not ok:
            f.append("wrong")
    bn = len(re.findall("[\u0980-\u09FF]", body))
    en = len(re.findall("[A-Za-z]", body))
    if bn / max(1, bn + en) < 0.6:
        f.append("language")
    if new_tokens >= 120 or len(body.split()) > 45:
        f.append("verbose")
    return f
import json
import pandas as pd
from checker import check
ev = pd.read_csv("bazar_eval.csv").set_index("id")
RUNS = json.load(open("bazar_runs.json", encoding="utf-8"))   # সত্যিকারের run-এর উত্তর
MODES = ["format", "ignored", "invented", "wrong", "language", "verbose"]

rows = []
for v in ["v1", "v2", "v3"]:                          # Qwen2.5-1.5B, greedy
    for i, n, a in RUNS[f"{v}|greedy"]["rows"]:
        f = check(ev.cat[i], ev.question[i], ev.expected[i], a, n)
        rows.append({"v": v, "lang": ev.lang[i], "cat": ev.cat[i],
                     "pass": not f, **{m: m in f for m in MODES}})
df = pd.DataFrame(rows)
print(df.groupby("v")[["pass"] + MODES].sum())

print("\nট্যাগ ঠিক নেই (format বা ignored), ধরন ধরে, প্রতি ঘরে ১২টি:")
df["tag_bad"] = df.format | df.ignored
print(df.pivot_table(index="cat", columns="v", values="tag_bad", aggfunc="sum")
        .loc[["info", "calc", "unk", "esc", "oos"]])
print("\nভুল ভাষা, ভাষা ধরে, প্রতি ঘরে ২০টি:")
print(df.pivot_table(index="lang", columns="v", values="language", aggfunc="sum")
        .loc[["bn", "bl", "en"]])

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

    pass  format  ignored  invented  wrong  language  verbose
v                                                            
v1     0      57        2         6     16        35       25
v2     3      30        8         7     15        28       10
v3     7      42        1         3     16        32       21

ট্যাগ ঠিক নেই (format বা ignored), ধরন ধরে, প্রতি ঘরে ১২টি:
v     v1  v2  v3
cat             
info  11   6  11
calc  12   0   1
unk   12   9  12
esc   12  11   7
oos   12  12  12

ভুল ভাষা, ভাষা ধরে, প্রতি ঘরে ২০টি:
v     v1  v2  v3
lang            
bn     0   1   1
bl    15   9  15
en    20  18  16

দ্বিতীয় সারণিই আসল খবর। v3-এ মানুষের কাছে পাঠানোর ধরনে ট্যাগের ভুল ১১ থেকে ৭, কিন্তু তথ্যের ধরনে ৬ থেকে ১১ আর অজানায় ৯ থেকে ১২. উদাহরণ বাড়ানোয় model escalation শিখেছে, অথচ সাধারণ তথ্যের প্রশ্নে ট্যাগ লেখা ছেড়েছে; ১ নম্বর প্রশ্নের উত্তর নির্ভুল (“ডেলিভারি চার্জ ঢাকার বাইরে ১২০ টাকা।”), শুধু ট্যাগ নেই। তৃতীয় সারণি বলে ভাষার ভুল প্রায় পুরোটাই ইংরেজি আর বাংলিশ প্রশ্নে: বাংলায় প্রশ্ন করলে উত্তর বাংলায় আসে। Prompt বদলে এই ছোট model-এ এটি সারেনি; সম্ভাব্য পথ হলো অ্যাপের কোডে ভাষা যাচাই করে আবার জিজ্ঞাসা, বা বড় model.

Checker-এরও ভুল হয়, আর তাতে সিদ্ধান্ত উল্টে যায়। এই checker-এর প্রথম version হিসাবে শুধু দেখত প্রত্যাশিত সংখ্যাটি উত্তরে আছে কি না। তাতে [INFO] 2000 + 120 = 2120 টাকা. “পাশ” করছিল (২০০০ আছে বলে), আর লিখুন: এ বিষয়ে… দিয়ে prompt-এর লাইন নকল করা উত্তরও পাশ। প্রথম version-এ ১.৫B-এর v2 আর v3: ৭ বনাম ৮, অর্থাৎ “কোনো পার্থক্য নেই”। উত্তর হাতে পড়ে নিয়ম শক্ত করার পর (হিসাবে শেষ সংখ্যাই উত্তর; “লিখুন:” মানে ফরম্যাট ভাঙা): ৩ বনাম ৭. এখনো অন্ধ জায়গা আছে: [ESCALATE] পণ্য ভাঙা বা ভুল, টাকা ফেরত, অথবা মানুষ চাইলে. নিয়মের লাইন নকল, তবু পাশ। তাই checker বদলালে তার নিজের version আর তারিখও লিখে রাখুন, আর পুরনো সব run নতুন checker-এ আবার মাপুন।

৬ · Regression harness — version ধরে উত্তর জমা আর diff

পরীক্ষার খাতা দেখার পর শিক্ষক শুধু মোট নম্বর লেখেন না; প্রতিটি প্রশ্নের নম্বর রাখেন, যাতে পরের পরীক্ষায় দেখা যায় কোন অধ্যায়ে উন্নতি, কোথায় পিছিয়ে। Regression harness তা-ই করে: প্রতিটি version-এর প্রতিটি উত্তর hash-সহ জমা রাখে, আর দুটি version প্রশ্ন ধরে মেলায়। মূল প্রশ্ন একটি: কোন প্রশ্ন আগে পাশ করত, এখন ফেল?

import json
import pandas as pd
from checker import check
ev = pd.read_csv("bazar_eval.csv").set_index("id")
RUNS = json.load(open("bazar_runs.json", encoding="utf-8"))   # সত্যিকারের run-এর উত্তর

def grade(key):                          # একটি version-এর প্রতিটি উত্তরে failure mode
    return {i: check(ev.cat[i], ev.question[i], ev.expected[i], a, n)
            for i, n, a in RUNS[key]["rows"]}

store = {}                               # version ধরে ফল জমা
for key in ["v2|greedy", "v3|greedy"]:
    store[key] = grade(key)
    ok = sum(not f for f in store[key].values())
    print(f"{key}  sha {RUNS[key]['sha']}  পাশ {ok}/60")

old, new = store["v2|greedy"], store["v3|greedy"]
ans = {i: a for i, n, a in RUNS["v3|greedy"]["rows"]}
print("\nনতুন পাশ:", [i for i in old if old[i] and not new[i]])
print("নতুন ফেল:")
for i in [i for i in old if not old[i] and new[i]]:
    print(f"  #{i} {ev.cat[i]}/{ev.lang[i]} {new[i]}: {ans[i][:38]}")
print("দুবারই ফেল, কিন্তু কারণ বদলেছে:",
      sum(1 for i in old if old[i] and new[i] and old[i] != new[i]))

t = pd.DataFrame({"v2": {i: not f for i, f in old.items()},
                  "v3": {i: not f for i, f in new.items()}}).join(ev.cat)
print(t.groupby("cat")[["v2", "v3"]].sum().T)

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

v2|greedy  sha fafdd1e2  পাশ 3/60
v3|greedy  sha 1e44fbed  পাশ 7/60

নতুন পাশ: [24, 37, 40, 44, 45, 46]
নতুন ফেল:
  #1 info/bn ['format']: ডেলিভারি চার্জ ঢাকার বাইরে ১২০ টাকা।
  #2 info/bn ['format', 'wrong', 'verbose']: রাজশাহীতে পণ্য পৌঁছাতে ঢাকার ভেতরে ৬০ 
দুবারই ফেল, কিন্তু কারণ বদলেছে: 25
cat  calc  esc  info  oos  unk
v2      1    0     2    0    0
v3      2    5     0    0    0

ছয়টি নতুন পাশের পাঁচটি মানুষের কাছে পাঠানোর ধরনে (৩৭, ৪০, ৪৪, ৪৫, ৪৬), আর ২টি নতুন ফেল দুটিই তথ্যের। কিন্তু সবচেয়ে বড় সংখ্যাটি শেষের দিকে: ২৫টি প্রশ্ন দুবারই ফেল, কিন্তু কারণ বদলেছে। মোট পাশের সংখ্যা এদের কিছুই দেখায় না। নিচের stepper-এ diff পড়ার ক্রম।

কী দেখছি আর কী বুঝছি
দেখা
বোঝা
  1. শিরোনাম. ৩ থেকে ৭ শুনতে দ্বিগুণের বেশি, কিন্তু ৬০টির মধ্যে মাত্র ৪টির পার্থক্য। দৈব কি না, তা §৭ বলবে।
  2. নতুন পাশ. পাঁচটিই মানুষের কাছে পাঠানোর প্রশ্ন। v3-এ “My parcel arrived broken.” উদাহরণ যোগ হয়েছিল; model ঠিক সেই বাক্যই লিখছে ([ESCALATE] দুঃখিত, একজন কর্মী শিগগির যোগাযোগ করবেন.)।
  3. নতুন ফেল. দুটি সরল তথ্যের প্রশ্ন, বাংলায়। ১ নম্বরের তথ্য নির্ভুল, শুধু ট্যাগ নেই। ক্ষতি ছোট মনে হয়, কিন্তু অ্যাপ ট্যাগ দেখে উত্তর পাঠায়, তাই এটি গ্রাহকের কাছে পৌঁছাবে না।
  4. কারণ বদল. ২৫টি প্রশ্ন দুবারই ফেল, কিন্তু অন্য কারণে, যেমন আগে ভুল ভাষা, এখন ভুল ভাষা আর লম্বা। এগুলো পাশের হারে অদৃশ্য।
  5. ধরন ধরে. §৫-এর সারণি: ট্যাগের ভুল মানুষের ধরনে কমেছে, তথ্য আর অজানায় বেড়েছে। গড়ের উন্নতি একটি ধরনের লাভ, যা দুটি ধরনের ক্ষতিকে ঢেকে রেখেছে।
  6. কারণ খোঁজা. Prompt-এর diff (নিচে) দেখায়, v3 সাজানোর সময় “## হিসাব” অংশটি বাদ পড়েছে, আর ট্যাগের তালিকার শিরোনাম বদলেছে। এমন অনিচ্ছাকৃত বদল diff ছাড়া চোখে পড়ে না।
  7. পরের ধাপ. একটি run-এর diff সিদ্ধান্তের জন্য যথেষ্ট নয়। জোড়া CI, অন্য seed আর সিদ্ধান্তের নিয়ম মিলিয়ে তবে রায়।
~/bazarghar-dl$ uv run python promptdiff.py v2 v3   # সত্যিকারের run, difflib, প্রথম অংশ
--- prompts/v2.txt
+++ prompts/v3.txt
@@ -10,11 +10,5 @@
-## ট্যাগ: উত্তরের প্রথম শব্দ, ঠিক এই চারটির একটি
-[INFO] নিয়ম থেকে উত্তর, বা দামের হিসাব।
-[ESCALATE] পণ্য ভাঙা বা ভুল, টাকা ফেরত, বা মানুষ চাইলে। লিখুন: একজন কর্মী শিগগির যোগাযোগ করবেন।
-[UNKNOWN] দোকানের প্রশ্ন, কিন্তু নিয়মে নেই। লিখুন: এ বিষয়ে আমার কাছে তথ্য নেই, একজন কর্মী জানাবেন।
-[OOS] দোকানের বাইরের বিষয়। লিখুন: দুঃখিত, আমি শুধু বাজারঘরের বিষয়ে সাহায্য করতে পারি।
-
-## ভাষা আর দৈর্ঘ্য
-গ্রাহক ইংরেজি বা বাংলিশে লিখলেও উত্তর সবসময় বাংলায়, দুই বাক্যের মধ্যে।
-
-## হিসাব
-মোট = পণ্যের দাম + ডেলিভারি চার্জ; দাম ২০০০ টাকা বা বেশি হলে চার্জ ০।
+## উত্তরের ধরন: প্রথমে একটি ট্যাগ, তারপর বাংলায় এক বা দুই বাক্য
+[INFO] নিয়ম থেকে উত্তর বা দামের হিসাব। হিসাবের শেষে মোট টাকা লিখুন।
+[ESCALATE] পণ্য ভাঙা বা ভুল, টাকা ফেরত, অথবা মানুষ চাইলে। উত্তর হুবহু: দুঃখিত, একজন কর্মী শিগগির যোগাযোগ করবেন।
+[UNKNOWN] দোকানের প্রশ্ন, কিন্তু নিয়মে নেই। উত্তর হুবহু: এ বিষয়ে আমার কাছে তথ্য নেই, একজন কর্মী জানাবেন।
+[OOS] দোকানের বাইরের যেকোনো বিষয়, অঙ্ক বা অনুবাদও। উত্তর হুবহু: দুঃখিত, আমি শুধু বাজারঘরের বিষয়ে সাহায্য করতে পারি।
…

৭ · দুটি prompt-এর জোড়া তুলনা — paired bootstrap

দুটি version একই ৬০টি প্রশ্নে চলেছে, তাই তুলনা হবে জোড়ায়: প্রতিটি bootstrap নমুনায় একই প্রশ্নগুলো দুই পাশে, তারপর পার্থক্য (paired bootstrap; তত্ত্ব মেশিন লার্নিং কোর্সের পর্ব ২১-এ, McNemar পর্ব ২২-এ, আর LLM-এ প্রয়োগ পর্ব ৫৮-এ)। নতুন যা যোগ হচ্ছে: একই তুলনা তিনটি decoding-এ, কারণ sampling-এ ফল seed-ভেদে নড়ে। শেষ লাইনে ইচ্ছা করে ভুল পথটিও দেখানো হলো: দুই পাশে আলাদা সূচক।

import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import binomtest
from checker import check
ev = pd.read_csv("bazar_eval.csv").set_index("id")
RUNS = json.load(open("bazar_runs.json", encoding="utf-8"))   # সত্যিকারের run-এর উত্তর

def passed(key):                          # প্রতিটি প্রশ্নে পাশ = 1, ফেল = 0
    return np.array([not check(ev.cat[i], ev.question[i], ev.expected[i], a, n)
                     for i, n, a in RUNS[key]["rows"]], dtype=int)

rng = np.random.default_rng(66)
idx = rng.integers(0, 60, size=(10000, 60))           # একই সূচক দুই পাশে: জোড়া
ci = lambda d: np.percentile(d, [2.5, 97.5]).round(3)
fig, ax = plt.subplots(figsize=(6, 3))
for mode in ["greedy", "s1", "s2"]:
    a, b = passed(f"v2|{mode}"), passed(f"v3|{mode}")
    d = (b[idx] - a[idx]).mean(1)
    only_b, only_a = int(((b == 1) & (a == 0)).sum()), int(((a == 1) & (b == 0)).sum())
    p = binomtest(only_b, only_b + only_a).pvalue
    print(f"{mode:6} v2 {a.sum()} v3 {b.sum()} | শুধু v3 {only_b}, শুধু v2 {only_a}"
          f" | জোড়া CI {ci(d)} | McNemar p={p:.3f}")
    ax.hist(d, bins=np.arange(-0.15, 0.25, 1 / 60), alpha=0.5, label=mode)
a, b = passed("v2|greedy"), passed("v3|greedy")
idx2 = rng.integers(0, 60, size=(10000, 60))          # ভুল পথ: দুই পাশে আলাদা সূচক
print("greedy, জোড়া ছাড়া CI", ci(b[idx2].mean(1) - a[idx].mean(1)))
ax.axvline(0, color="black", lw=1)
ax.set_xlabel("pass rate difference v3 - v2 (paired bootstrap)")
ax.set_ylabel("count"); ax.legend(title="decoding")

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

greedy v2 3 v3 7 | শুধু v3 6, শুধু v2 2 | জোড়া CI [-0.017  0.167] | McNemar p=0.289
s1     v2 1 v3 4 | শুধু v3 3, শুধু v2 0 | জোড়া CI [0.    0.117] | McNemar p=0.250
s2     v2 3 v3 4 | শুধু v3 4, শুধু v2 3 | জোড়া CI [-0.067  0.1  ] | McNemar p=1.000
greedy, জোড়া ছাড়া CI [-0.033  0.167]

তিনটি decoding-এই v3 গড়ে একটু এগিয়ে, কিন্তু কোনোটিতেই অর্থবহ নয়: greedy-তে জোড়া CI শূন্য ছোঁয় (−০.০১৭ থেকে +০.১৬৭), McNemar p ≈ ০.২৯; s1-এ CI-এর নিচের সীমা ঠিক শূন্য, p ≈ ০.২৫; s2-তে ৪ বনাম ৩, p = ১. দুই version-এর ফল আলাদা মাত্র ৩ থেকে ৮টি প্রশ্নে, এত কম অমিলে কোনো test জোর দিয়ে কিছু বলতে পারে না। জোড়া ছাড়া CI (−০.০৩৩ থেকে +০.১৬৭) একটু চওড়া; এখানে ফারাক ছোট, কারণ দুই version-এর পাশ করা প্রশ্নগুলো প্রায় আলাদা, তাদের ফল একসঙ্গে ওঠানামা করে না। ছবিতে তিনটি decoding-এর bootstrap বণ্টন; তিনটিই শূন্যের দুই পাশে ছড়ানো।

সিদ্ধান্তের ভাষায়: “v3 ভালো” বলার প্রমাণ এই set-এ নেই; “v3 খারাপ নয়” বলারও না, কারণ তথ্যের ধরনে ক্ষতি দেখা গেছে। আর মনে রাখুন, v3 লেখা হয়েছে এই set-এর v2-এর ভুল পড়ে; একই set-এ মাপা তাই আশাবাদী (ভাবনার প্রশ্ন ১)।

৮ · Model বদল — গড় বাড়ে, নতুন ভুল লুকায়

এবার prompt একই (v3), বদলাচ্ছে model: Qwen/Qwen2.5-0.5B-Instruct থেকে Qwen/Qwen2.5-1.5B-Instruct (দুটিই Apache 2.0). বাস্তবে এটি সবচেয়ে সাধারণ বদলগুলোর একটি: “বড় model নিলে ভালো হবে”। Harness-এ একই set, একই checker; শুধু model-এর নাম বদল। নিচে প্রতিটি ঘরে ঠিক উত্তরের সংখ্যা (প্রতি ধরনে ১২টির মধ্যে), চারটি মাপকাঠিতে আলাদা: ট্যাগ, বিষয় (ভুল উত্তর বা বানানো নীতি নেই), ভাষা, দৈর্ঘ্য।

~/bazarghar-dl$ uv run python analyze.py 0.5B v3 1.5B v3 greedy   # সত্যিকারের run
0.5B/v3 -> 1.5B/v3: pass 2 -> 7 / 60 | only 1.5B/v3 6, only 0.5B/v3 1, McNemar p=0.125 | diff CI [+0.000, +0.167]
  category       info       calc        unk        esc        oos
  tag         0->1       1->11      0->0       2->5       0->0    
  content     1->5       2->3      11->10     12->12     12->12   
  lang        2->5       6->8       4->3       6->8       5->4    
  len         8->6       8->11      9->5      11->9      10->8    
  newly failing on tag: 1 ['41:esc/bl']
  newly failing on content: 3 ['22:calc/en', '29:unk/bl', '30:unk/bl']
  newly failing on lang: 2 ['28:unk/bn', '56:oos/bl']
  newly failing on len: 16 ['2:info/bn', '4:info/bn', '10:info/en', '12:info/en', …
s1: 0.5B/v3 -> 1.5B/v3: pass 1 -> 4 / 60 | only 1.5B/v3 4, only 0.5B/v3 1, McNemar p=0.375 | diff CI [-0.017, +0.117]
s2: 0.5B/v3 -> 1.5B/v3: pass 1 -> 4 / 60 | only 1.5B/v3 3, only 0.5B/v3 0, McNemar p=0.250 | diff CI [+0.000, +0.117]

গড়ের গল্প: কড়া পাশ ২ থেকে ৭, হিসাবে ট্যাগ ১ থেকে ১১, তথ্যের বিষয় ১ থেকে ৫. তিনটি decoding-এই বড় model এগিয়ে, যদিও প্রতিটি আলাদাভাবে অর্থবহ নয় (p ০.১২৫ থেকে ০.৩৭৫)।

লুকানো গল্প: বিষয়ের মাপকাঠিতে তিনটি নতুন ফেল, আর তার দুটি সবচেয়ে দামি ধরনের: অজানা প্রশ্নে বানানো নীতি, দুটিই বাংলিশে। ছোট model-এর উত্তর ছিল অপ্রাসঙ্গিক বা প্রত্যাখ্যান; বড় model আত্মবিশ্বাসের সঙ্গে নীতি বানিয়েছে (greedy, হুবহু, প্রথম ১১০টি অক্ষর; লাইন ভাঙা ⏎ চিহ্নে):

[#29 unk/bl] credit card e EMI hobe?
  0.5B: No, credit cards and EMIs are not related to the concept of a "bajrang har ghar." A bajrang har ghar is a type …
  1.5B: Yes, you can get an EMI on a credit card. Here's how it works: ⏎  ⏎ 1. You apply for the credit card and agree to  …
[#30 unk/bl] order er sathe gift card dite parben?
  0.5B: I'm sorry, but I can't assist with that.
  1.5B: Yes, you can order a Gift Card after placing an order. Here's how: ⏎  ⏎ 1. Go to the checkout page of your online  …
[#22 calc/en] An order of 3000 taka inside Dhaka. What is the total?
  0.5B: The total amount for an order of 3000 taka in Dhaka would be: ⏎  ⏎ 3000 taka ⏎  ⏎ This is a straightforward calculatio …
  1.5B: The total amount for an order of 3000 taka in Dhaka would be: ⏎  ⏎ \[ \text{Total} = 3000 + 1200 = 4200 \] ⏎  ⏎ So, th …

দৈর্ঘ্যেও বড় model খারাপ: ১৬টি প্রশ্ন নতুন করে অতিরিক্ত লম্বা, অজানার ধরনে ঠিক দৈর্ঘ্য ৯ থেকে ৫. আর ৪১ নম্বরে বড় model নিজের একটি ট্যাগ বানিয়েছে: [damage]. গড় পাশের হার এগুলোর একটিও দেখায় না। ১২টি প্রশ্নের ঘরে ২টির বদল দৈবও হতে পারে; কিন্তু বানানো নীতির মতো ভুলে “দৈব হতে পারে” যথেষ্ট সান্ত্বনা নয়। ঠিক পদক্ষেপ হলো এই ধরনে প্রশ্ন বাড়িয়ে আবার মাপা।

এখন সিদ্ধান্ত। নিয়মটি ফল দেখার আগে লেখা থাকা চাই, নইলে পছন্দের ফলের পক্ষে যুক্তি খোঁজা সহজ। নিচের stepper-এ বাজারঘরের release gate, v3 + ১.৫B-কে v2 + ১.৫B-এর জায়গায় চালু করা হবে কি না।

শর্ত, মাপা মান আর রায়
শর্ত
মাপা
রায়
  1. মোট পাশ. ৩ থেকে ৭, কমেনি। সবচেয়ে সহজ শর্ত, আর একা সবচেয়ে কম তথ্য দেয়।
  2. অনিশ্চয়তা. Greedy-তে জোড়া CI-এর নিচের সীমা −০.০১৭, শূন্যের নিচে। উন্নতি দৈবও হতে পারে।
  3. ঝুঁকির ধরন. মানুষের কাছে পাঠানোর প্রশ্নে কোনো নতুন ফেল নেই; বরং ৫টি নতুন পাশ।
  4. বানানো নীতি. Greedy আর s2-তে কমেছে, s1-এ সমান। বাড়েনি, তাই পাশ।
  5. ধরন ধরে ট্যাগ. তথ্যে ৫টি আর অজানায় ৩টি বেশি ট্যাগ-ভুল। সীমা +২, তাই ফেল। এই শর্তটি না থাকলে ক্ষতিটি গড়ে ঢাকা পড়ত।
  6. Seed. তিনটি decoding-এই পার্থক্য ধনাত্মক: দিক একই।
  7. রায়. দুটি শর্ত ফেল, তাই চালু নয়। পরের কাজ: তথ্য আর অজানার ট্যাগ ঠিক করা (যেমন ওই দুই ধরনের উদাহরণ), তারপর লুকানো test ভাগে আবার পুরো পরীক্ষা।

আরও গভীরে: Ribeiro ও সহকর্মীদের CheckList (২০২০) NLP model-এর জন্য software testing-এর ধাঁচে ক্ষমতা × test-এর ধরনের ছক প্রস্তাব করে, যা এই পর্বের coverage ছকের পূর্বসূরি। Liang ও সহকর্মীদের HELM (২০২২) একটি গড় নম্বরের বদলে অনেক পরিস্থিতি আর মাপকাঠির সারণির পক্ষে যুক্তি দেয়। Miller-এর “Adding Error Bars to Evals” (২০২৪) জোড়া পার্থক্য আর একই উৎসের প্রশ্নে clustered standard error-এর কথা বলে; একই prompt-এর কয়েকটি paraphrase থাকলে প্রশ্নগুলো স্বাধীন নয়, তাই bootstrap প্রশ্ন-দল ধরে করা ভালো। Prompt-এর ছোট শব্দ-বদলে ফলের বড় দোল নিয়ে Sclar ও সহকর্মীরা (২০২৩) রিপোর্ট করেছেন, যা পর্ব ৬০-এ আলোচিত।

পরীক্ষা ও ইন্টারভিউতে: Eval set-এ কী কী রাখবেন? ধরন × ভাষা × edge case-এর ছক, প্রত্যাশিত উত্তরসহ, dev আর লুকানো test ভাগ। Regression test-এ গড় ছাড়া কী দেখবেন? নতুন ফেলের তালিকা আর ধরন ধরে হার। দুটি prompt তুলনায় কোন test? একই প্রশ্নে paired bootstrap বা McNemar. LLM বিচারকের দুটি পরিচিত পক্ষপাত? ক্রম আর দৈর্ঘ্য। Model বদলালে কী করবেন? পুরো eval আবার, কারণ একই prompt নতুন model-এ নতুন আচরণ দেয়।

৯ · Prompt versioning আর ছয়টি প্রতিষ্ঠানের নথি

দোকানের হিসাবের খাতায় প্রতিটি পাতায় তারিখ থাকে, কাটাকাটি করলে পাশে সই। Prompt-এরও তেমন খাতা লাগে, কারণ তিন মাস পরে কেউ জিজ্ঞেস করবেন, “গত সপ্তাহে bot হঠাৎ ইংরেজিতে উত্তর দিচ্ছে কেন?” Prompt versioning-এর ন্যূনতম অভ্যাস:

  • Prompt কোডের ভেতরে string হিসেবে নয়, আলাদা ফাইলে, Git-এ (Software Engineering কোর্সের Git মডিউল)। প্রতিটি বদল একটি commit, কারণসহ।
  • প্রতিটি উত্তরের সঙ্গে prompt-এর hash (এই পর্বে sha256-এর প্রথম ৮ অক্ষর), model-এর নাম, decoding আর তারিখ লগে রাখা। তাহলে কোন উত্তর কোন version থেকে, তা পরে খুঁজে পাওয়া যায়।
  • Version বদলের সঙ্গে eval-এর ফল জুড়ে রাখা: পাশের হার, ধরন ধরে ভাগ, নতুন ফেলের তালিকা আর সিদ্ধান্ত।
  • Model, chat template আর library-ও version. একই prompt নতুন model-এ নতুন prompt-এর মতোই; §৮-এ দেখলেন।

বাংলাদেশে অনেকে ChatGPT (OpenAI), Gemini (Google), Claude (Anthropic), Grok (xAI), DeepSeek আর Kimi (Moonshot AI) ব্যবহার করেন। Prompt পরীক্ষার কোনো সুবিধা তাদের developer নথিতে আছে কি না, নিচে তার সারাংশ। সব তথ্য প্রতিটির নিজের নথি থেকে, লেখার সময়ে (নভেম্বর ২০২৬) পড়া; পরিবর্তন হতে পারে। “নথিতে পাইনি” মানে যে পাতাগুলো পড়েছি সেখানে পাইনি। কোনো পণ্যের মান তুলনা এখানে নেই।

নথি (নভেম্বর ২০২৬)Prompt পরীক্ষা নিয়ে যা পেয়েছি
OpenAI: Evals guide, Evaluation best practicesএকটি eval-এ test ডেটার schema (data_source_config) আর grader-এর তালিকা (testing_criteria, যেমন string_check); ডেটা JSONL ফাইলে। পরামর্শ: typical, edge আর adversarial case, প্রতিটি বদলে চালানো, LLM বিচারকে দৈর্ঘ্যের পক্ষপাত নিয়ন্ত্রণ। একই নথিতে ঘোষণা: Evals platform বন্ধ হচ্ছে, ৩১ অক্টোবর ২০২৬ থেকে শুধু পড়া যায়, নভেম্বর ২০২৬-এর শেষে বন্ধ; পরীক্ষার জন্য Datasets ব্যবহারের পরামর্শ।
Anthropic (Claude): Define success criteria and build evaluationsসাফল্যের মাপ নির্দিষ্ট আর মাপা যায় এমন; eval কাজের মতো, edge case-সহ; যতটা সম্ভব স্বয়ংক্রিয় grading; নথির ভাষায় “Prioritize volume over quality”. Console-এর পুরনো Evaluate পাতার ঠিকানা এখন এই পাতায় নিয়ে যায়, তাই Console-এর সেই সুবিধার বর্তমান অবস্থা নিশ্চিত হতে পারিনি।
Google (Gemini): Prompt design strategiesPrompt বানানো “iterative”, ফল না এলে অন্যভাবে লেখা, উপাদানের ক্রম বদলে দেখা, উদাহরণের সংখ্যা নিয়ে পরীক্ষা। Eval set বা version তুলনার সুবিধা এই পাতায় পাইনি।
xAI (Grok): docs overviewনথিতে পাইনি
DeepSeek: API docsনথিতে পাইনি
Moonshot AI (Kimi): docs overviewPlayground-এ কোড ছাড়া prompt আর উদাহরণ পরীক্ষা; eval framework-এর পাতা পাইনি

সবার মিল: যেখানে লেখা আছে, সেখানে একই কথা: নির্দিষ্ট মাপকাঠি, প্রতিনিধিত্বমূলক আর কিনারার প্রশ্ন, বারবার চালানো। শিক্ষা: একটি সেবাদাতার eval সুবিধা বন্ধ বা বদলাতে পারে, যেমন OpenAI-এর নথিতেই ঘোষণা আছে। তাই eval set, checker আর জমা রাখা উত্তর নিজের repository-তে রাখাই নিরাপদ; সেবাদাতার সুবিধা থাকলে সেটি চালানোর একটি উপায় মাত্র। নিচে OpenAI-এর নথির ধাঁচে একটি eval-এর আকার; এটি নমুনা, চালানো হয়নি (এখানে কোনো API key নেই)।

# নমুনা (চালানো হয়নি): OpenAI Evals নথির ধাঁচে
{"name": "bazarghar-support-tags",
 "data_source_config": {"type": "custom",
   "item_schema": {"type": "object", "properties":
     {"question": {"type": "string"}, "tag": {"type": "string"}}},
   "include_sample_schema": true},
 "testing_criteria": [{"type": "string_check", "name": "tag match",
   "input": "{{ sample.output_text }}", "operation": "ilike",
   "reference": "[{{ item.tag }}]"}]}

নথি অনুযায়ী ilike মানে input-এ reference আছে কি না (ছোট-বড় অক্ষর না দেখে)। অর্থাৎ ট্যাগ উত্তরের শুরুতে না হয়ে মাঝে থাকলেও পাশ; এই পর্বের checker শুরুতেই চায়, তাই কড়া। একই নামের grader দুই জায়গায় দুই অর্থে চলতে পারে; সংজ্ঞা পড়ে নিন।

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

Eval harness, checker আর bootstrap-এর কোড AI assistant দ্রুত লিখে দেয়। ঝুঁকি হলো ভুল checker: সেটি চলে, নম্বর দেয়, আর নম্বরটি ভুল জিনিস মাপে।

AI প্রায়ই যা করেকেন ভুলকীভাবে ধরবেন
দুই version আলাদা bootstrap করে CI মেলায়জোড়া ভেঙে যায়, CI চওড়া আর বিভ্রান্তিকরএকই সূচক দুই পাশে; কোড পড়ে দেখা
বাংলা অঙ্ক normalise করে না“৯৭০” আর 970 আলাদা, ঠিক উত্তর ফেলহাতে লেখা ৫টি উত্তরে checker চালানো
শুধু গড় পাশের হার রিপোর্টনতুন ফেল আর ধরনের ভাগ লুকায়diff আর ধরন ধরে সারণি চাওয়া
Eval set দেখে prompt ঘষে, একই set-এ রিপোর্টPrompt set-এর জন্য overfitলুকানো test ভাগ
পুরনো বা বানানো API parameter (eval সেবার)সেবা আর নাম বদলায়নিজের তারিখের official নথি

কোড চাওয়ার সময় যাচাইও চেয়ে নিন। গ্রাহকের আসল কথোপকথন, ফোন নম্বর বা কোনো key hosted assistant-এ দেবেন না।

আমি একটি বাংলা support bot-এর prompt পরীক্ষার harness বানাচ্ছি (Python version লিখে দিচ্ছি)। ১. প্রতিটি run-এর উত্তর model, prompt-এর sha256 আর decoding-সহ JSON-এ রাখার কোড দাও। ২. একটি checker দাও যা বাংলা অঙ্ক আর কমা normalise করে, আর প্রতিটি উত্তরে failure mode-এর তালিকা ফেরত দেয়; প্রতিটি mode-এর জন্য দুটি test. ৩. দুই version-এর diff দাও: নতুন ফেল, নতুন পাশ, আর ধরন ধরে পাশের হার। ৪. Paired bootstrap দাও, একই সূচক দুই পাশে; আলাদা bootstrap কেন ভুল, এক লাইনে বলো। ৫. যে API বা library-র নাম নিশ্চিত নও, তা বলো; বানিয়ে লিখবে না। Model “বোঝে” এমন দাবি করবে না।

AI বা LLM-এর দেওয়া eval কোডের চেকলিস্ট

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

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

প্র ০১একই ৬০টি প্রশ্ন দেখে দেখে prompt দশবার বদলালে শেষ version-এর নম্বর কেন বিশ্বাস করা কঠিন?

কারণ prompt-ও শেখে, শুধু হাতে। প্রতিবার আপনি ফেল করা প্রশ্ন দেখেন আর সেই প্রশ্নের জন্য একটি লাইন যোগ করেন: “জেলার নাম এলে ঢাকার বাইরে ধরুন”, “বিকাশ মানে মোবাইল ব্যাংকিং”। দশবার পর prompt এই ৬০টি প্রশ্নের জন্য সাজানো, ঠিক যেমন বেশি epoch-এ model training set মুখস্থ করে। নতুন গ্রাহকের প্রশ্নে সেই উন্নতির কতটা টিকবে, এই ৬০টি আর বলতে পারে না।

অর্থনীতিতে এর একটি নাম আছে: Goodhart-এর সূত্র, মোটামুটি “কোনো মাপকে লক্ষ্য বানালে সেটি আর ভালো মাপ থাকে না”। Machine learning-এ এর পরিচিত রূপ benchmark-এ অতিরিক্ত মানিয়ে নেওয়া। Recht ও সহকর্মীরা (২০১৯) ImageNet-এর ধাঁচে নতুন test set বানিয়ে রিপোর্ট করেন, পুরনো set-এ ভালো model-গুলো নতুন set-এ লক্ষণীয়ভাবে কম নির্ভুলতা পায়, যদিও তাদের ক্রম মোটামুটি একই থাকে। প্রতিযোগিতার public leaderboard-এ বারবার জমা দিয়ে নম্বর বাড়ানো আর লুকানো private set-এ নেমে যাওয়ার অভিজ্ঞতাও বহু প্রতিযোগীর।

প্রতিকার তিনটি। এক, set দুই ভাগ: dev ভাগ দেখে prompt বদলান, test ভাগ লুকানো থাকে আর শুধু চূড়ান্ত সিদ্ধান্তে চলে। দুই, নিয়মিত নতুন প্রশ্ন যোগ করুন, বিশেষ করে আসল ব্যবহার থেকে; পুরনো test ভাগ ধীরে ধীরে dev-এ নামিয়ে দিন। তিন, prompt-এ প্রশ্ন-নির্দিষ্ট “জোড়াতালি” দেখলে সন্দেহ করুন: সাধারণ নিয়ম লিখুন (“নিয়মে নেই এমন কিছু বানাবেন না”), নির্দিষ্ট প্রশ্নের উত্তর নয়।

মূল উপলব্ধি: যে set দেখে prompt বানিয়েছেন, সেটি দিয়ে prompt-এর মান প্রমাণ হয় না। সিদ্ধান্তের জন্য একটি না-দেখা ভাগ রাখুন।

প্র ০২গড় পাশের হার বাড়লেও নতুন version কেন খারাপ হতে পারে? এর পরিসংখ্যানের দিকটি কী?

গড় একটি সংখ্যায় অনেক তথ্য চেপে ধরে। দুই version-এর গড় ৩৫ আর ৩৮ হলে মনে হয় তিনটি প্রশ্ন বেশি পাশ। কিন্তু হতে পারে নতুন version-এ দশটি নতুন পাশ আর সাতটি নতুন ফেল। সেই সাতটি যদি টাকা ফেরতের অনুরোধ হয়, যেখানে bot মানুষের কাছে পাঠায়নি, তাহলে দোকানের ক্ষতি তিনটি সহজ তথ্যের প্রশ্নের লাভের চেয়ে অনেক বেশি।

পরিসংখ্যানে এর একটি বিখ্যাত চরম রূপ Simpson-এর paradox: প্রতিটি দলে এক দিক, সব মিলিয়ে উল্টো দিক। প্রায়ই উদ্ধৃত উদাহরণ ১৯৭৩ সালের University of California, Berkeley-র ভর্তির তথ্য, যা Bickel ও সহকর্মীরা (১৯৭৫) বিশ্লেষণ করেন: মোট হিসাবে নারী আবেদনকারীর ভর্তির হার কম দেখাচ্ছিল, কিন্তু বিভাগ ধরে দেখলে সেই পক্ষপাতের প্রমাণ মেলেনি, কারণ নারীরা বেশি আবেদন করেছিলেন কঠিন বিভাগে। Eval-এ “বিভাগ” হলো প্রশ্নের ধরন বা ভাষা। ধরন ধরে মিশ্রণ বদলালে গড়ের দিক বদলাতে পারে।

দ্বিতীয় দিক ক্ষতির অসমতা। সব ভুল সমান নয়। বানানো ছাড়ের প্রতিশ্রুতি বা OTP চাওয়ার মতো ভুলের দাম একটি দেরিতে পৌঁছানোর তারিখ ভুল বলার চেয়ে অনেক বেশি। তাই সিদ্ধান্তের নিয়মে গড়ের পাশাপাশি থাকে ধরন ধরে “নামতে পারবে না” সীমা, ঝুঁকির ধরনে শূন্য সহনশীলতা, আর নতুন ফেলের তালিকা মানুষ দিয়ে পড়া। জোড়া তুলনায় McNemar-এর ছকের “শুধু পুরনোটি ঠিক” ঘরটিই এই নতুন ফেল (মেশিন লার্নিং কোর্সের পর্ব ২২)।

মূল উপলব্ধি: গড় সিদ্ধান্তের শুরু, শেষ নয়। ধরন ধরে ভাগ, নতুন ফেল আর ভুলের দাম একসঙ্গে দেখুন।

প্র ০৩Prompt-এর উত্তর মাপতে কোড checker, মানুষ আর বিচারক LLM — কোনটি কখন?

কোড checker সস্তা, দ্রুত আর প্রতিবার একই রায় দেয়। ট্যাগ, সংখ্যা, ভাষা, দৈর্ঘ্য, নিষিদ্ধ শব্দ: যা নিয়মে লেখা যায়, তা কোডে মাপুন। দুর্বলতা হলো অন্ধ জায়গা। এই পর্বের checker “৯ দিন পরে ফেরত দেওয়া যাবে?” প্রশ্নে উত্তরে ৭ আছে কি না দেখে, কিন্তু উত্তর “হ্যাঁ, ৭ দিনের মধ্যে যাবে” হলে সেটি ভুল সিদ্ধান্ত হলেও পাশ। Checker শুধু তা-ই ধরে, যা আপনি ভেবে রেখেছেন।

মানুষ সবচেয়ে ভালো বিচারক, কিন্তু ধীর আর দামি, আর দুজন মানুষও সবসময় একমত হন না। তাই rubric লাগে, আর দুজনের মিল Cohen's kappa দিয়ে মাপতে হয় (পর্ব ৫৮)। মানুষের আসল কাজ তিনটি: শুরুতে কিছু উত্তর লেবেল করে checker-এর ভুল খোঁজা, নতুন ফেলের তালিকা পড়া, আর নতুন failure mode চিনে নাম দেওয়া।

বিচারক LLM মাঝামাঝি: খোলা উত্তরে (ভদ্রতা, স্পষ্টতা) নম্বর দিতে পারে, মানুষের চেয়ে সস্তায়। Zheng ও সহকর্মীরা (২০২৩, MT-Bench) রিপোর্ট করেন, শক্তিশালী বিচারকের সঙ্গে মানুষের মিল মানুষে-মানুষে মিলের কাছাকাছি হতে পারে; একই প্রবন্ধে ক্রম, দৈর্ঘ্য আর নিজের উত্তরের প্রতি পক্ষপাতও দেখানো হয়। পর্ব ৫৮-এর ছোট বিচারক ক্রম বদলালেই রায় বদলেছিল। তাই বিচারককে আগে মানুষের লেবেলে মেপে নিতে হয়, দুই ক্রমে চালাতে হয়, আর বিচারকের prompt-ও version করে রাখতে হয়, কারণ সেটি বদলালে সব নম্বর বদলায়।

ব্যবহারিক মিশ্রণ: নিয়মে যা যায় তা কোডে, খোলা অংশে মানুষের লেবেলে যাচাই করা বিচারক, আর প্রতিটি release-এর আগে নতুন ফেলের তালিকায় মানুষের চোখ।

মূল উপলব্ধি: প্রতিটি মাপার যন্ত্রেরও মাপ লাগে। Checker আর বিচারক দুটোকেই কিছু মানুষের লেবেলের সঙ্গে মিলিয়ে নিন।

প্র ০৪Sampling চালু থাকলে একই prompt-এ উত্তর বদলায়। তাহলে regression test কীভাবে নির্ভরযোগ্য করবেন?

Software testing-এ যে test কখনো পাশ, কখনো ফেল করে, তাকে বলে flaky test (Software Engineering কোর্সের পর্ব ৫১)। Sampling-সহ LLM-এর প্রতিটি প্রশ্নই কিছুটা flaky: temperature=0.7-এ একই প্রশ্নে একবার ঠিক, একবার ভুল। একটি run-এর diff-এ “নতুন ফেল” তাই কখনো কখনো শুধু ভাগ্য।

প্রথম উপায়: regression run-এ greedy decoding, যাতে একই model, একই prompt আর একই software-এ একই উত্তর আসে। এই পর্বের desktop run তা-ই করেছে। তবে hosted API-তে temperature=0 দিলেও উত্তর পুরোপুরি একই থাকবে, এমন নিশ্চয়তা কিছু নথি দেয় না; batching আর floating-point হিসাবের ক্রম ছোট পার্থক্য আনতে পারে বলে আলোচিত, তাই নিজের মেপে দেখাই নিরাপদ। দ্বিতীয় উপায়: আসল ব্যবহারের decoding-এ কয়েকটি seed চালিয়ে প্রতিটি প্রশ্নের পাশের হার মাপা (যেমন ৫ বারের ৪ বার), আর diff-এ শুধু সেই প্রশ্নগুলো ধরা যেখানে হার বড়ভাবে নেমেছে।

তৃতীয়: seed-এর ছড়ানো রিপোর্ট। এই পর্বের দুটি sampling seed-এ দুই prompt-এর পার্থক্য কতটা নড়ল, তা §৭-এ দেখেছেন। seed-ভেদে যদি পার্থক্যের চিহ্নই বদলায়, তাহলে সিদ্ধান্ত নেওয়ার মতো তথ্য নেই। চতুর্থ: model-এর নাম স্থির রাখা। অনেক API-তে চলমান নাম (alias) আর তারিখ-দেওয়া নির্দিষ্ট নাম দুটোই থাকে; alias-এর পেছনের model বদলালে আপনার কোনো বদল ছাড়াই regression আসতে পারে। নির্দিষ্ট নাম ব্যবহার আর লগে রাখা ভালো অভ্যাস।

মূল উপলব্ধি: দৈবতা নিয়ন্ত্রণ করুন (greedy, স্থির model) বা মাপুন (কয়েকটি seed, প্রতি প্রশ্নে হার)। একটি sampled run-এর diff একা সিদ্ধান্ত নয়।

প্র ০৫Software-এর regression test-এর ধারণা prompt-এ আনতে গিয়ে কোথায় মেলে, কোথায় মেলে না?

মেলে মূল অভ্যাসে। পুরনো software প্রকৌশলে নিয়মটি পরিচিত: প্রতিটি ঠিক করা bug-এর জন্য একটি test, যাতে সেটি ফিরে এলে ধরা পড়ে। Kent Beck-এর “Test-Driven Development: By Example” (২০০২ সালের দিকে প্রকাশিত) test আগে লেখার অভ্যাস জনপ্রিয় করে। OpenAI-এর নথির “eval-driven development” একই চিন্তা: কী চাই, তা আগে মাপার মতো করে লিখুন। Snapshot test, যেখানে আগের আউটপুট জমা রেখে নতুনটির সঙ্গে মেলানো হয়, এই পর্বের উত্তর জমা রাখা আর diff-এর খুব কাছের আত্মীয়।

অমিল তিন জায়গায়। এক, প্রত্যাশিত আউটপুট হুবহু লেখা যায় না: “একজন কর্মী যোগাযোগ করবেন” আর “আমাদের কর্মী শিগগির আপনাকে ফোন করবেন” দুটোই ঠিক। তাই হুবহু মেলানোর বদলে নিয়ম, rubric বা বিচারক। দুই, পাশ-ফেল সম্ভাবনার: সাধারণ কোডে একটি ফেল মানে bug; prompt-এ ৬০টির ৩টি নতুন ফেল দৈবও হতে পারে, তাই CI আর জোড়া পরীক্ষা লাগে। তিন, সব test পাশ করানো লক্ষ্য নয়। ছোট model-এ ১০০% পাশ সম্ভব না-ও হতে পারে; লক্ষ্য হলো জানা অবস্থার চেয়ে খারাপ না হওয়া, আর ঝুঁকির ধরনে নির্দিষ্ট সীমা।

আরেকটি সূক্ষ্ম পার্থক্য: software-এ কোড আর test আলাদা মানুষ লিখলে ভালো। Prompt-এর ক্ষেত্রে যে model উত্তর দেয়, সেটিই বিচারক হলে নিজের উত্তরের প্রতি পক্ষপাত আসতে পারে; Anthropic-এর নথিও বিচারে অন্য model নেওয়ার কথা বলে। আর eval set নিজেই কোডের মতো version করা দরকার: প্রশ্ন যোগ বা বদলালে আগের নম্বরের সঙ্গে সরাসরি তুলনা আর চলে না।

মূল উপলব্ধি: Regression test-এর আত্মা একই: পুরনো ভুল যেন ফিরে না আসে। শুধু “পাশ” এখানে একটি হার, আর মাপকাঠি একটি নিয়ম, হুবহু string নয়।

অনুশীলন

খাতা-কলমে করুন, তারপর উত্তর মেলান। বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের হিসাব কাজে লাগে।

  1. জোড়া তুলনার হিসাব: ৬০টি প্রশ্নের একটি eval set-এ prompt v1 পাশ করেছে ৩০টিতে। v2 চালানোর পর দেখা গেল ৯টি প্রশ্ন শুধু v2-তে পাশ, ৩টি শুধু v1-এ পাশ, বাকিগুলোতে দুটির ফল এক। (ক) v2 কয়টিতে পাশ, আর পাশের হারের পার্থক্য কত? (খ) exact McNemar test-এর দুই-দিকের p-মান কত? (গ) সিদ্ধান্তের নিয়ম: “মানুষের কাছে পাঠানোর ধরনে কোনো নতুন ফেল চলবে না।” শুধু v1-এ পাশ ৩টির একটি টাকা ফেরতের অনুরোধ। v2 চালু হবে?

    (ক) v2 = ৩০ + ৯ − ৩ = ৩৬টি. হার ০.৫০ থেকে ০.৬০, পার্থক্য +০.১০.

    (খ) শুধু অমিলের ১২টি প্রশ্ন গোনে। শূন্য অনুমানে প্রতিটি অমিল দুই দিকে সমান সম্ভাব্য, তাই ছোট দিকের সংখ্যা ৩ বা কম আসার সম্ভাবনা: (C(12,0) + C(12,1) + C(12,2) + C(12,3)) ÷ 212 = (১ + ১২ + ৬৬ + ২২০) ÷ ৪০৯৬ = ২৯৯ ÷ ৪০৯৬ ≈ ০.০৭৩. দুই দিক মিলে p ≈ ০.১৪৬. ৯ বনাম ৩ শুনতে বড়, কিন্তু ১২টি অমিলে এটি দৈবেও আসতে পারে।

    (গ) না। নিয়মটি ফল দেখার আগে লেখা, আর একটি নতুন ফেল ঠিক সেই ধরনে। গড়ের উন্নতি অর্থবহও নয়। পরের ধাপ: ফেলটি পড়া, prompt ঠিক করা, তারপর আবার পুরো set.

  2. আউটপুট অনুমান করুন: প্রতিটি প্রশ্নের id-এর পাশে failure mode-এর তালিকা (খালি = পাশ)। কোড না চালিয়ে দুটি লাইনের আউটপুট লিখুন।
    old = {1: [], 2: ["wrong"], 3: [], 4: ["language"], 5: []}
    new = {1: [], 2: [], 3: ["format"], 4: ["language", "verbose"], 5: ["invented"]}
    newly_fail = [i for i in old if not old[i] and new[i]]
    newly_pass = [i for i in old if old[i] and not new[i]]
    changed = [i for i in old if old[i] and new[i] and old[i] != new[i]]
    print(newly_fail, newly_pass, changed)
    print(sum(not v for v in old.values()), "->", sum(not v for v in new.values()))
    [3, 5] [2] [4]
    3 -> 2

    খালি তালিকা Python-এ মিথ্যা, তাই not old[i] মানে পুরনোতে পাশ। ৩ আর ৫ আগে পাশ, এখন ফেল; ২ আগে ফেল, এখন পাশ; ৪ দুবারই ফেল, কিন্তু কারণ বেড়েছে, তাই “changed”. পাশের সংখ্যা ৩ থেকে ২. লক্ষ করুন, ৫ নম্বরের নতুন ফেল “বানানো নীতি”: শুধু সংখ্যা গুনলে এক প্রশ্ন কম, diff পড়লে দেখা যায় সবচেয়ে দামি ধরনের ভুল এসেছে।

  3. নকশা সমালোচনা করুন: একজন সহকর্মীর পরিকল্পনা: “আমি ChatGPT-তে আমাদের prompt দিয়ে ১০টি প্রশ্ন করে দেখেছি, ৯টি ঠিক। নতুন prompt-এ আবার একই ১০টি প্রশ্ন করব; ৯-এর বেশি এলে চালু করব। Model পরে সস্তাটায় বদলাব, prompt তো একই।” অন্তত পাঁচটি সমস্যা আর সমাধান বলুন।
    • খুব ছোট set: ১০টিতে ৯ মানে CI প্রায় [০.৬০, ০.৯৮]. সমাধান: ধরন × ভাষার ছক ধরে কয়েক ডজন থেকে কয়েকশো প্রশ্ন।
    • Coverage অজানা: ১০টি কোন ধরনের? বাংলিশ, সীমার প্রশ্ন, অজানা প্রশ্ন আছে? সমাধান: coverage checker.
    • চোখে দেখা বিচার: “ঠিক” কে ঠিক করল? সমাধান: লিখিত মাপকাঠি, কোডে checker, কিছু উত্তরে দুজন মানুষ।
    • একই ১০টি প্রশ্নে prompt ঘষা: overfit. সমাধান: dev আর লুকানো test ভাগ।
    • শুধু মোট সংখ্যা: নতুন ফেল লুকায়। সমাধান: উত্তর জমা রেখে diff, ধরন ধরে সীমা।
    • Model বদল = নতুন পরীক্ষা: একই prompt নতুন model-এ অন্যরকম চলে; পুরো set আবার চালাতে হবে। আর chat interface-এ চালানো ফল API-র সঙ্গে নাও মিলতে পারে (আলাদা system prompt, সেটিং); version আর তারিখ লিখুন।
  4. কোড লিখুন: একটি ফাংশন gate(old, new, cats, risky) লিখুন। old আর new হলো id থেকে পাশ (True/False), cats id থেকে ধরন, risky ঝুঁকির ধরনের set. ফাংশনটি "ship" ফেরত দেবে শুধু যদি নতুনটির মোট পাশ পুরনোটির চেয়ে কম না হয় আর ঝুঁকির ধরনে একটিও নতুন ফেল না থাকে; নইলে "block" আর কারণ। দুটি test দিন।
    def gate(old, new, cats, risky):
        reasons = []
        if sum(new.values()) < sum(old.values()):
            reasons.append("মোট পাশ কমেছে")
        bad = [i for i in old if old[i] and not new[i] and cats[i] in risky]
        if bad:
            reasons.append(f"ঝুঁকির ধরনে নতুন ফেল: {bad}")
        return ("block", reasons) if reasons else ("ship", [])
    
    cats = {1: "info", 2: "esc", 3: "info"}
    assert gate({1: 0, 2: 1, 3: 1}, {1: 1, 2: 1, 3: 1}, cats, {"esc"})[0] == "ship"
    assert gate({1: 0, 2: 1, 3: 1}, {1: 1, 2: 0, 3: 1}, cats, {"esc"})[0] == "block"
    print("সব test পাশ")

    দ্বিতীয় test-এ মোট পাশ সমান (২ আর ২), তবু block, কারণ ২ নম্বর প্রশ্নটি মানুষের কাছে পাঠানোর ধরনে। আসল নিয়মে এর সঙ্গে জোড়া CI-এর নিচের সীমাও দেখা যায়; কিন্তু ঝুঁকির শর্তটি CI-এর ওপর নির্ভর করে না, এটি একটি কড়া সীমা।

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

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

ক্রমএকটি support bot-এর prompt বদলে চালু করার ধাপগুলো সাজান (▲▼ চেপে সরান)

  1. সিদ্ধান্তের নিয়ম লেখা: কোন সীমা, কোন ধরনে নতুন ফেল চলবে না
  2. নতুন prompt version আলাদা ফাইলে, hash আর কারণসহ
  3. পুরো eval set একই decoding-এ চালিয়ে উত্তর জমা রাখা
  4. পুরনো version-এর সঙ্গে diff আর জোড়া CI দেখা
  5. নতুন ফেল পড়ে নিয়ম মিলিয়ে চালু বা বাতিল

একটি bot-এর eval set-এ ৬০টি প্রশ্ন: ৩০টি তথ্যের, ৩০টি হিসাবের, সবই বাংলায়। কোন দুর্বলতা সবচেয়ে গুরুতর?

  • প্রশ্ন ১০০-র কম
  • তথ্য আর হিসাবের সংখ্যা সমান
  • অজানা, মানুষ আর বাইরের ধরন এবং বাংলিশ বা ইংরেজি একেবারে নেই
  • প্রশ্নে বাংলা অঙ্ক ব্যবহার

দুটি prompt একই ৮০টি প্রশ্নে চলল। শুধু A পাশ ৪টিতে, শুধু B পাশ ১৪টিতে। Paired bootstrap-এ পার্থক্যের ৯৫% CI কীভাবে নেওয়া উচিত?

  • A আর B-র জন্য আলাদা আলাদা resample, তারপর CI দুটি মেলানো
  • একই প্রশ্ন-সূচক দুই পাশে নিয়ে প্রতিবার B − A, তারপর শতাংশিক বিন্দু
  • শুধু অমিলের ১৮টি প্রশ্ন resample করা
  • ফেরত না রেখে resample করা

“৯ দিন আগে পণ্য পেয়েছি, ফেরত দেওয়া যাবে?” প্রশ্নে checker শুধু দেখে উত্তরে ৭ সংখ্যাটি আছে কি না। Model লিখল “হ্যাঁ, ৭ দিনের মধ্যে ফেরত দেওয়া যাবে।” কী ঘটে?

  • Checker ফেল দেয়, কারণ উত্তর ভুল
  • Checker বাংলা অঙ্ক পড়তে পারে না
  • উত্তর ঠিক, কারণ ৭ আছে
  • ভুল উত্তর পাশ করে যায়: checker-এর অন্ধ জায়গা

একটি bot-এর গড় পাশের হার নতুন model-এ ০.৫২ থেকে ০.৬৮ হলো, কিন্তু “মানুষের কাছে পাঠানো” ধরনে ১২-এর ১০ থেকে ১২-এর ৬. সবচেয়ে ভালো পদক্ষেপ কোনটি?

  • চালু আটকে নতুন ফেলগুলো পড়া, ঠিক করে আবার মাপা
  • গড় বেড়েছে, তাই চালু করা
  • ওই ধরনের প্রশ্ন set থেকে বাদ দেওয়া
  • Checker বদলে বিচারক model দিয়ে আবার মাপা, যাতে ফল ভালো আসে

অর্ডার থেকে ৪টি field (জেলা, পণ্য, দাম, পরিমাণ) চাওয়া। Model দিল জেলা আর দাম ঠিক, পণ্য ভুল, পরিমাণ দেয়নি, কোনো বাড়তি field নেই। Field-level F1 কত?

  • ০
  • ০.৬৭
  • প্রায় ০.৫৭
  • ০.৫০

একটি eval set-এ ৪৮টি প্রশ্ন, পাশের হার প্রায় ০.৫. ধরন ধরে ৬টি দলে (প্রতিটিতে ৮টি) ভাগ করলে একটি দলের হারের ৯৫% CI-এর অর্ধ-প্রস্থ মোটামুটি কত (১.৯৬ × √(p(1 − p)/n))?

  • প্রায় ০.১৪
  • প্রায় ০.০৫
  • প্রায় ০.১৮
  • প্রায় ০.৩৫

শব্দকোষ

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

মনে রাখুন

  1. Prompt-এর বদল মানে কোডের বদল: প্রতিবার পুরো eval set আবার চলে, উত্তর জমা থাকে prompt hash, model আর decoding-সহ।
  2. Eval set একটি ছক: ধরন × ভাষা × edge case. Coverage checker ফাঁক দেখায়; dev আর লুকানো test ভাগ আলাদা।
  3. কাজ ধরে মাপকাঠি: ট্যাগে exact match, সংখ্যায় normalisation, field-এ F1, খোলা উত্তরে rubric; মাপকাঠি prompt-এর সঙ্গে বদলায় না।
  4. ফেলের কারণ গুনুন: ফরম্যাট, বানানো নীতি, ভাষা, নির্দেশ উপেক্ষা, দৈর্ঘ্য, ভুল উত্তর। Checker-কেও হাতে মিলিয়ে নিন।
  5. জোড়ায় তুলনা, ধরন ধরে সিদ্ধান্ত: paired bootstrap আর McNemar; গড়ের পাশে নতুন ফেলের তালিকা আর ঝুঁকির ধরনে কড়া সীমা।

পরের পর্বে: পর্ব ৬৭ · Sentence embedding আর semantic search. মডিউল ১১-এ model-কে বাজারঘরের নিজের নথি থেকে উত্তর দিতে শেখানো শুরু; সেই উত্তরও এই পর্বের মতো মাপতে হবে, যার বিস্তার পর্ব ৭০ · RAG মূল্যায়নে।

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

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