অধ্যায় 4 · এক্সপেরিমেন্ট ও মূল্যায়ন
মেট্রিক ও এক্সপেরিমেন্ট রিপোর্ট: accuracy, precision, recall, ল্যাটেন্সি ও CSV
- পৃষ্ঠা 16 / 24
- 18 মিনিট পড়া
গত পাতায় আপনি একই কেসে দুটি প্রম্পট ভার্সন চালিয়ে সারিগুলো পড়েছিলেন। ছয়টি সারি পড়া চলে, ষাটটি চলে না; আর "ভার্সন ২ বেশি পরিচ্ছন্ন দেখাচ্ছে" এমন কথা রিপোর্টে ফলাফল হিসেবে দেওয়া যায় না। এই পাতা সংখ্যা যোগ করে: কত শতাংশ উত্তর সঠিক, প্রম্পট কী ধরনের ভুল করে, একটি রান কত সময় ও খরচ নেয়, বারবার টেস্ট করলে ফলাফল কতটা বদলায়, এবং একটি CSV যা সহকর্মী স্প্রেডশিটে খুলতে পারেন।
আমরা ছোট টুলকিট গড়ে চলেছি। নতুন ফাইল metrics.py; Python প্রম্পট এক্সপেরিমেন্ট: টেস্ট কেস, রানার, JSONL লগ ও মক মোড-এর ফাইলগুলো আবার দরকার, কারণ প্রতিটি পাতা নতুন ফোল্ডারে চলে। সবকিছু টয় প্রোভাইডার দিয়ে চলে, তাই নিচের প্রতিটি সংখ্যা পুনরাবৃত্তিযোগ্য স্ট্যান্ড-ইন, যা আপনার স্কোরিং কোড পরীক্ষা করে। আসল মডেল কেমন স্কোর করবে সে সম্পর্কে এটি কিছুই বলে না।
যা শিখবেন
- টেস্ট কেসে মানুষের লেখা প্রত্যাশিত উত্তর বা রুব্রিক যোগ করা, এবং কখন exact match সঠিক যাচাই।
- accuracy, ফিল্ড-লেভেল এক্সট্র্যাকশন accuracy, এবং ছোট কনফিউশন ম্যাট্রিক্সসহ precision ও recall শূন্য থেকে হিসাব করা।
- কোড যা বিচার করতে পারে না সেই মানের জন্য CSV-তে ব্লাইন্ড রিভিউ শিট দিয়ে মানুষের রিভিউ ব্যবহার করা।
- ল্যাটেন্সি, ব্যবহার ও আনুমানিক খরচ সততার সঙ্গে রেকর্ড করা, পরিবর্তনশীলতা দেখতে টেস্ট পুনরাবৃত্তি করা এবং এক্সপেরিমেন্ট রিপোর্ট CSV-তে এক্সপোর্ট করা।
আগের পাতার ফাইল, আবার
দুটি প্রম্পট টেমপ্লেট, টয় প্রোভাইডার এবং সংক্ষিপ্ত runner.py (আগের মতোই যুক্তি, তবে CSV লোডার ও পাশাপাশি-প্রিন্টার ছাড়া) এখানে আবার দেওয়া হলো, যাতে এই পাতা নিজে নিজে চলে।
# file: prompts/v1.txt
Classify the support message as billing, delivery or other.
### MESSAGE
{text}# file: prompts/v2.txt
Classify the support message as billing, delivery or other.
A refund request is billing. Reply with the label only.
### MESSAGE
{text}# file: toys.py
"""toys.py: stand-in providers (NOT real models) so experiments can be repeated exactly."""
import random
BILLING = ["invoice", "charged", "payment"]
DELIVERY = ["parcel", "package", "delivery", "arrived", "tracking", "driver"]
def make_triage(seed=7, noise=0.3):
"""Toy classifier. It reads the prompt: a 'refund' hint adds a billing word, 'label only' stops wordy
replies on long messages. With temperature above 0 it sometimes answers 'other' (seeded)."""
rng = random.Random(seed)
def provider(messages, temperature=0, **settings):
head, text = messages[-1]["content"].lower().split("### message", 1)
billing = BILLING + (["refund"] if "refund" in head else [])
label = ("billing" if any(w in text for w in billing)
else "delivery" if any(w in text for w in DELIVERY) else "other")
if temperature > 0 and rng.random() < noise:
label = "other"
if "label only" not in head and len(text.split()) > 9:
return f"I think it is {label}."
return label
return provider# file: runner.py
"""runner.py (shortened from the previous page): load cases, run one prompt version, log JSONL."""
import hashlib, json
from pathlib import Path
from promptlab import ProviderError, render
def load_cases(path):
return json.loads(Path(path).read_text(encoding="utf-8"))
def run_experiment(client, version, cases, log, **settings):
template = Path("prompts", f"{version}.txt").read_text(encoding="utf-8")
sha = hashlib.sha256(template.encode()).hexdigest()[:8]
records = []
Path(log).parent.mkdir(parents=True, exist_ok=True)
with open(log, "a", encoding="utf-8") as out:
for case in cases:
rec = {"case": case["id"], "version": version, "prompt_sha": sha,
"model": client.model, "settings": settings}
try:
r = client.complete([{"role": "user", "content": render(template, text=case["text"])}], **settings)
rec.update(status="ok", text=r.text.strip(), input_tokens=r.usage.input_tokens,
output_tokens=r.usage.output_tokens, latency_ms=r.latency_ms)
except ProviderError as exc:
rec.update(status="error", error=str(exc))
out.write(json.dumps(rec, sort_keys=True) + "\n")
out.flush()
records.append(rec)
return recordsপ্রত্যাশিত উত্তর ও রুব্রিক
স্কোরের জন্য তুলনা করার মতো কিছু লাগে। প্রত্যাশিত উত্তর নিজে লিখুন, কোনো আউটপুট দেখার আগে, এবং কেসের সঙ্গে রাখুন। মডেলের উত্তর পড়ার পরে লিখলে আপনি তার সঙ্গে একমত হয়ে যাবেন। কেস এখন বারোটি কাল্পনিক সাপোর্ট মেসেজ, প্রতিটির expected-এ মানুষের লেখা লেবেল। দুটি ইচ্ছাকৃতভাবে কঠিন: c6-এ দুটি সমস্যা, আর c9 এমন একটি শব্দ ("ship") ব্যবহার করে যা টয় চেনে না।
# file: cases.json
[
{"id": "c1", "text": "I was charged twice for invoice 3318.", "expected": "billing"},
{"id": "c2", "text": "My parcel has not arrived after two weeks.", "expected": "delivery"},
{"id": "c3", "text": "Please send me a refund for the broken lamp that I bought last month.", "expected": "billing"},
{"id": "c4", "text": "How do I change my profile photo?", "expected": "other"},
{"id": "c5", "text": "Tracking shows nothing for my package.", "expected": "delivery"},
{"id": "c6", "text": "The driver left the box outside in the rain, and the invoice total looks wrong too.", "expected": "delivery"},
{"id": "c7", "text": "Why was my card debited two times?", "expected": "billing"},
{"id": "c8", "text": "The payment page crashed and I think I was charged.", "expected": "billing"},
{"id": "c9", "text": "Can you ship to a post office box?", "expected": "delivery"},
{"id": "c10", "text": "Where can I download the mobile app?", "expected": "other"},
{"id": "c11", "text": "The package arrived damaged and I want a refund.", "expected": "billing"},
{"id": "c12", "text": "Thanks, the invoice is correct now and everything is fine.", "expected": "other"}
]সব কাজের একটিই সঠিক স্ট্রিং থাকে না। রিপ্লাই ড্রাফটের মতো মুক্ত লেখার জন্য রুব্রিক লিখুন: রানের আগে লেখা হ্যাঁ/না যাচাইয়ের একটি ছোট তালিকা, যা দুজন মানুষ প্রয়োগ করলে বেশিরভাগ ক্ষেত্রে একমত হবেন।
| কাজের ধরন | প্রত্যাশিত উত্তর | যাচাই |
|---|---|---|
| লেবেল, আইডি, হ্যাঁ/না | হুবহু মান | Exact match (পরের অংশ) |
| ফিল্ডে এক্সট্র্যাকশন | সত্য মানসহ রেকর্ড | ফিল্ড ধরে ধরে তুলনা |
| রিপ্লাই ড্রাফট | রুব্রিক: অর্ডার আইডি উল্লেখ করে; একবার দুঃখ প্রকাশ করে; পরের ধাপ বলে; সর্বোচ্চ ৬০ শব্দ; যা দেওয়া হয়নি এমন কিছু প্রতিশ্রুতি দেয় না | গোনা যায় এমন বিষয়ে কোড, বাকিতে মানুষ |
Exact-match accuracy
Accuracy হলো যত শতাংশ কেসে আউটপুট প্রত্যাশিত উত্তরের সমান। উত্তরের সেট ছোট ও বদ্ধ হলে (লেবেল, হ্যাঁ/না, আইডি) exact match সঠিক যাচাই, কারণ এটি সস্তা, পুনরাবৃত্তিযোগ্য এবং বিচারবুদ্ধি লাগে না। সারাংশ বা পরামর্শের জন্য এটি ভুল যাচাই। নিচে স্কোরিং ফাইল; এটি শুধু স্ট্যান্ডার্ড লাইব্রেরি ব্যবহার করে।
# file: metrics.py
"""metrics.py: small scoring functions (standard library only)."""
import csv
from collections import Counter
def norm(value):
"""Lowercase, trim and collapse spaces, so 'Billing.' and 'billing' match."""
return " ".join(str(value).lower().split()).strip(".!")
def accuracy(pairs):
"""Share of (predicted, expected) pairs that match exactly after norm()."""
return sum(norm(p) == norm(g) for p, g in pairs) / len(pairs) if pairs else 0.0
def first_label(text, labels):
"""Lenient reading of a wordy answer: the label that appears earliest in text, else 'none'."""
found = [(text.lower().find(l), l) for l in labels if l in text.lower()]
return min(found)[1] if found else "none"
def field_accuracy(preds, golds, fields):
"""Accuracy per field, plus 'all' = share of records where every field is right."""
n = len(golds)
result = {f: sum(norm(p.get(f)) == norm(g[f]) for p, g in zip(preds, golds)) / n for f in fields}
result["all"] = sum(all(norm(p.get(f)) == norm(g[f]) for f in fields) for p, g in zip(preds, golds)) / n
return result
def confusion(pairs):
"""Counter of (expected, predicted) -> count."""
return Counter((g, p) for p, g in pairs)
def precision_recall(pairs, labels):
"""Per label: precision = tp/(tp+fp), recall = tp/(tp+fn), f1 = their harmonic mean (0.0 if undefined)."""
cm, out = confusion(pairs), {}
for label in labels:
tp = cm[(label, label)]
fp = sum(c for (g, p), c in cm.items() if p == label and g != label)
fn = sum(c for (g, p), c in cm.items() if g == label and p != label)
pr = tp / (tp + fp) if tp + fp else 0.0
rc = tp / (tp + fn) if tp + fn else 0.0
out[label] = {"precision": round(pr, 2), "recall": round(rc, 2),
"f1": round(2 * pr * rc / (pr + rc), 2) if pr + rc else 0.0}
return out
def show_confusion(pairs, labels):
cm, cols = confusion(pairs), labels + ["none"]
print(f"{'expected/predicted':<20}" + "".join(f"{c:>10}" for c in cols))
for g in labels:
print(f"{g:<20}" + "".join(f"{cm[(g, c)]:>10}" for c in cols))
def write_csv(path, rows):
with open(path, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)এবার দুটি ভার্সনই চালিয়ে দুভাবে স্কোর করুন। Strict accuracy পুরো আউটপুটকে প্রত্যাশিত লেবেলের সঙ্গে মেলায়। Lenient accuracy আগে আউটপুটের ভেতরে লেবেল শব্দ খোঁজে। দুটির ব্যবধান হলো ব্যর্থতার সেই অংশ যা সিদ্ধান্ত নয়, ফরম্যাট নিয়ে।
from promptlab import Client
from runner import load_cases, run_experiment
from metrics import *
from toys import make_triage
LABELS = ["billing", "delivery", "other"]
cases = load_cases("cases.json")
gold = {c["id"]: c["expected"] for c in cases}
def pairs(records, lenient=False):
"""(predicted, expected) for the cases that returned an answer; error records are left out."""
return [(first_label(r["text"], LABELS) if lenient else r["text"], gold[r["case"]])
for r in records if r["status"] == "ok"]
client = Client(make_triage(), model="toy-triage")
runs = {v: run_experiment(client, v, cases, f"logs/{v}.jsonl") for v in ("v1", "v2")}
for v, records in runs.items():
print(v, "strict", round(accuracy(pairs(records)), 3), "lenient", round(accuracy(pairs(records, True)), 3))v1 strict 0.417 lenient 0.5
v2 strict 0.667 lenient 0.667ভার্সন ১ ফরম্যাটে হারে (strict কম lenient-এর চেয়ে) এবং সিদ্ধান্তেও হারে (lenient তবু অর্ধেক মাত্র)। ভার্সন ২ ফরম্যাটের ফাঁক বন্ধ করেছে, তাই তার দুটি সংখ্যা সমান। উত্তর বাচাল হতে পারলে দুটিই রিপোর্ট করুন, আর মনে রাখুন বারোটি কেস খুব মোটা সংখ্যা দেয়: একটি কেস accuracy প্রায় আট পয়েন্ট নাড়ায়।
ফিল্ড-লেভেল এক্সট্র্যাকশন accuracy
কাজ এক্সট্র্যাকশন হলে "রেকর্ড"-এর একটি সংখ্যা লুকিয়ে ফেলে কোন ফিল্ড ব্যর্থ হচ্ছে। ফিল্ড ধরে ধরে তুলনা করুন এবং পুরোপুরি সঠিক রেকর্ডও গুনুন। নিচের পাঁচটি ইনভয়েস কাল্পনিক; "predicted" দিকটি একটি এক্সট্র্যাকশন প্রম্পটের ফেরত দেওয়া ফল।
golds = [{"vendor": "Nile Tools", "total": "120.50", "currency": "USD"},
{"vendor": "Aiko Print", "total": "89.00", "currency": "JPY"},
{"vendor": "Lena GmbH", "total": "1205.00", "currency": "EUR"},
{"vendor": "Mateo SAS", "total": "40.00", "currency": "COP"},
{"vendor": "Priya Foods", "total": "15.75", "currency": "INR"}]
preds = [{"vendor": "Nile Tools", "total": "120.50", "currency": "USD"},
{"vendor": "Aiko Print", "total": "89.00", "currency": None},
{"vendor": "Lena GMBH", "total": "1,205.00", "currency": "EUR"},
{"vendor": "Mateo", "total": "40.00", "currency": "COP"},
{"vendor": "Priya Foods", "total": "15.75", "currency": "INR"}]
print({k: round(v, 2) for k, v in field_accuracy(preds, golds, ["vendor", "total", "currency"]).items()}){'vendor': 0.8, 'total': 0.8, 'currency': 0.8, 'all': 0.4}প্রতিটি ফিল্ড ০.৮, তবু পাঁচটির মধ্যে মাত্র ২টি রেকর্ড পুরোপুরি সঠিক (all ০.৪), কারণ ভুলগুলো আলাদা আলাদা রেকর্ডে পড়েছে। কোন ফিল্ড ঠিক করবেন জানতে ফিল্ডভিত্তিক সংখ্যা দেখুন, আর ব্যবহারকারী আসলে কী পান তা জানতে all দেখুন। দুটি ভুল ভাবার মতো: Lena GMBH শুধু অক্ষরের ছাঁদে আলাদা, যা norm ক্ষমা করে; আর 1,205.00 বনাম 1205.00 একটি ফরম্যাটের পার্থক্য, যা exact তুলনা ভুল গোনে। এমন নিয়ম ইচ্ছা করে ঠিক করুন (সংখ্যাকে সংখ্যা হিসেবে তুলনা করুন, হাজারের বিভাজক উপেক্ষা করুন) এবং লিখে রাখুন, কারণ এগুলো স্কোর বদলে দেয়।
শ্রেণিবিন্যাসের precision ও recall
Accuracy সব ভুলকে সমান ধরে। কোনো লেবেলের precision প্রশ্নের উত্তর দেয়: "প্রম্পট billing বললে কত বার সঠিক ছিল?" (ট্রু পজিটিভ ÷ ট্রু পজিটিভ ও ফলস পজিটিভের যোগফল)। Recall বলে: "আসল billing মেসেজের কতগুলো সে খুঁজে পেল?" (ট্রু পজিটিভ ÷ ট্রু পজিটিভ ও ফলস নেগেটিভের যোগফল)। কনফিউশন ম্যাট্রিক্স প্রতিটি (প্রত্যাশিত, পূর্বানুমিত) জোড়া গোনে, ফলে কোন লেবেল গুলিয়ে যাচ্ছে তা দেখা যায়। দুটি ফাংশনই metrics.py-তে আছে; নিচের লাইনগুলো ভার্সন ২-কে স্কোর করে।
show_confusion(pairs(runs["v2"], True), LABELS)
for label, scores in precision_recall(pairs(runs["v2"], True), LABELS).items():
print(label, scores)expected/predicted billing delivery other none
billing 4 0 1 0
delivery 1 2 1 0
other 1 0 2 0
billing {'precision': 0.67, 'recall': 0.8, 'f1': 0.73}
delivery {'precision': 1.0, 'recall': 0.5, 'f1': 0.67}
other {'precision': 0.5, 'recall': 0.67, 'f1': 0.57}ম্যাট্রিক্স সারি ধরে পড়ুন: প্রত্যাশিত পাঁচটি billing মেসেজের চারটি পাওয়া গেছে আর একটি other-এ গেছে (recall ০.৮)। প্রত্যাশিত চারটি delivery মেসেজের দুটিতে delivery বলা হয়েছে (recall ০.৫), আর delivery precision ১.০, কারণ আর কিছুকে ভুলে delivery বলা হয়নি। কোন ভুল ব্যয়বহুল তা পণ্যের ওপর নির্ভর করে: billing মেসেজ ছুটে যাওয়া গুরুতর হলে billing recall দেখুন; ভুল ফ্ল্যাগ এজেন্টের সময় নষ্ট করলে precision দেখুন। একটি accuracy সংখ্যার আগে দুটিই দেখা দরকার।
মানুষের রিভিউ দিয়ে সেমান্টিক মান
কিছু মানের কোনো একটিমাত্র সঠিক উত্তর নেই: উত্তরটি ভদ্র, স্পষ্ট ও উপযুক্ত কি না? মানুষ ব্যবহার করুন, এবং রিভিউ ন্যায্য করুন। নিচের শিট প্রতিটি রিপ্লাই কোন প্রম্পট ভার্সন লিখেছে তা লুকায় (ব্লাইন্ড শিট) এবং স্থির সিড দিয়ে ক্রম এলোমেলো করে, ফলে রিভিউয়ার ভার্সনের নাম বা অবস্থান দ্বারা প্রভাবিত হন না। ছয়টি রিপ্লাই কাল্পনিক, লগ করা আউটপুটের বদলি।
import csv, json, random
replies = {"t1": {"v1": "Sorry. Refund started.",
"v2": "Hello Kwame, I am sorry your order 48213 arrived damaged. We will start your refund today."},
"t2": {"v1": "Your parcel is late, we know.",
"v2": "Hello Lena, I am sorry order 51877 is late. A colleague will check its location and write to you."},
"t3": {"v1": "Hello Mateo, thank you for writing. We will send the right item.",
"v2": "Hello Mateo, sorry about the wrong item in order 60412. We will send the correct one."}}
items = [(case, v, text) for case, d in replies.items() for v, text in d.items()]
random.Random(3).shuffle(items)
key = {f"r{i}": (case, v) for i, (case, v, _) in enumerate(items, 1)}
with open("review_sheet.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["item", "reply", "score_1_to_5", "comment"])
writer.writerows([f"r{i}", text, "", ""] for i, (_, _, text) in enumerate(items, 1))
json.dump(key, open("review_key.json", "w"))
print(open("review_sheet.csv", encoding="utf-8").read().splitlines()[0])
print(len(key), "items; first two:", list(key.items())[:2])item,reply,score_1_to_5,comment
6 items; first two: [('r1', ('t1', 'v1')), ('r2', ('t2', 'v1'))]আসল কাজে আপনি রুব্রিকসহ review_sheet.csv (কী ফাইল নয়) এক বা দুজন রিভিউয়ারকে পাঠান। নিচে কাল্পনিক স্কোরের একটি ছোট টেবিল রিভিউয়ারের বদলি: শিট ফিরে এলে স্কোরগুলো কী-এর সঙ্গে জুড়ে ভার্সন ধরে গড় করুন।
invented = {("t1", "v1"): 2, ("t1", "v2"): 5, ("t2", "v1"): 1, ("t2", "v2"): 4, ("t3", "v1"): 3, ("t3", "v2"): 5}
by_version = {"v1": [], "v2": []}
for item, (case, v) in key.items():
by_version[v].append(invented[(case, v)])
print({v: round(sum(s) / len(s), 2) for v, s in by_version.items()}, "n per version:", len(by_version["v1"])){'v1': 2.0, 'v2': 4.67} n per version: 3প্রতি ভার্সনে তিনটি রিপ্লাই পার্থক্য দাবি করার জন্য অনেক কম; এটি শুধু পদ্ধতি দেখায়। আসল রিভিউতে অন্তত দুজন রিভিউয়ার রাখুন, রুব্রিক লিখে রাখুন, এবং তাঁরা কত বার একমত হন তা মাপুন (পরের পাতা মূল্যায়ন ডেটাসেট ডিজাইন: মেট্রিক, এজ কেস, হেল্ড-আউট সেট ও বেসলাইন তা দেখায়)।
ল্যাটেন্সি ও পাওয়া যায় এমন ব্যবহারের মেট্রিক
প্রতিটি রেকর্ডে ইতিমধ্যে latency_ms, input_tokens ও output_tokens আছে। এই টিউটোরিয়ালে ল্যাটেন্সি নির্ধারিত নকল সংখ্যা, যা promptlab আউটপুটের দৈর্ঘ্য ও কল কাউন্টার থেকে হিসাব করে, যাতে টেস্ট পুনরাবৃত্তিযোগ্য হয়। এটি পরিমাপ নয়। আসল ল্যাটেন্সি ক্লায়েন্টের দিকে মাপতে হয়: time.perf_counter() দিয়ে কলের সময় নিজে মাপুন; Client(real_timing=True) ঠিক এটাই করে। টোকেন সংখ্যাও টোকেনাইজার নয়, চার-অক্ষরের মোটামুটি আন্দাজ।
প্রোভাইডার নোট (২০২৬-১০-১২ অনুযায়ী): প্রোভাইডারদের রেসপন্স অবজেক্টে টোকেন ব্যবহারের তথ্য থাকে (যেমন রিজনিং-টোকেন সংখ্যা, ক্যাশ পড়া ও লেখার সংখ্যা, বা থট-টোকেন সংখ্যা), কিন্তু যাচাই করা ডকুমেন্টেশনে ল্যাটেন্সি রেসপন্সের কোনো ফিল্ড নয়, তাই ক্লায়েন্টের দিকে মাপুন। ফিল্ডের নাম প্রোভাইডারভেদে আলাদা এবং বদলায়; নির্ভর করার আগে বর্তমান ডকুমেন্টেশন দেখুন।
fake = [r["latency_ms"] for r in runs["v2"]]
print("fake latency, v2: mean", round(sum(fake) / len(fake)), "ms, max", max(fake), "ms")
real = Client(make_triage(), real_timing=True).complete([{"role": "user", "content": "### MESSAGE\nhello"}])
print("real timing is an int >= 0:", isinstance(real.latency_ms, int) and real.latency_ms >= 0)fake latency, v2: mean 58 ms, max 70 ms
real timing is an int >= 0: Trueআসল রান থেকে ল্যাটেন্সি রিপোর্ট করার সময় শুধু গড় নয়, মিডিয়ান (median) ও সবচেয়ে ধীর কয়েকটি দিন, এবং কতগুলো কল কোথা থেকে মেপেছেন তা বলুন। একটি ধীর কল খুবই সাধারণ।
প্রোভাইডারের মূল্য তালিকা থেকে খরচের আনুমানিক হিসাব
খরচ হলো টোকেন গুণ দাম। দাম রাখুন কোডে বা ফাইলে একটি ছোট টেবিলে, সূত্রের ভেতরে কখনো নয়, যাতে একটি সম্পাদনায় সব হিসাব বদলায়। নিচের সংখ্যাগুলো উদাহরণ দাম, আসল নয়। আসল দাম প্রোভাইডারের বর্তমান মূল্য পাতা থেকে আনুন; তা মডেল ও টোকেনের ধরনভেদে আলাদা এবং বদলায়; তারিখসহ কপি করুন। ফলকে আনুমানিক ধরুন: এখানে টোকেন সংখ্যা মোটামুটি, আর ক্যাশ করা ইনপুট, রিজনিং টোকেন ও ছাড়ের দাম আলাদা (দেখুন প্রম্পট ক্যাশিং ও খরচ: স্থির প্রিফিক্স, ব্যবহারের ডেটা ও আনুমানিক হিসাব)।
PRICES = {"input_per_million": 1.00, "output_per_million": 4.00} # EXAMPLE numbers, not real prices
def cost(records, prices=PRICES):
ok = [r for r in records if r["status"] == "ok"]
tokens_in, tokens_out = sum(r["input_tokens"] for r in ok), sum(r["output_tokens"] for r in ok)
return (tokens_in * prices["input_per_million"] + tokens_out * prices["output_per_million"]) / 1_000_000
for v, records in runs.items():
per_call = cost(records) / len(records)
print(v, "per call", f"{per_call:.7f}", "| per 10,000 calls", round(per_call * 10_000, 3), "(example currency units)")v1 per call 0.0000418 | per 10,000 calls 0.418 (example currency units)
v2 per call 0.0000507 | per 10,000 calls 0.507 (example currency units)ভার্সন ২-এর প্রম্পটে এক লাইন বেশি নির্দেশ আছে বলে এখানে প্রতি কলে খরচ প্রায় এক-পঞ্চমাংশ বেশি; ৪০০ টোকেন বাড়ালে তা এই টেবিলে তখনই ধরা পড়ত। খরচ দেখতে হয় মানের পাশাপাশি: accuracy হারানো সস্তা ভার্সন মোট হিসাবে সস্তা হয় কদাচিৎ।
পরিবর্তনশীলতা দেখতে টেস্ট পুনরাবৃত্তি
একটি রান মানে একটি নমুনা। আসল মডেল প্রতিবার আলাদা উত্তর দিতে পারে, বিশেষ করে temperature-এর মতো স্যাম্পলিং সেটিং শূন্যের বেশি হলে, আর temperature শূন্য হলেও হুবহু একই উত্তরের নিশ্চয়তা নেই। টয় প্রোভাইডার এটি নকল করে: temperature ০-এর বেশি হলে সিড-করা এলোমেলো ভুল উত্তর দেয়। একই টেস্ট কয়েকবার চালান এবং সেরা রান নয়, ব্যাপ্তি দেখুন।
noisy = Client(make_triage(seed=11), model="toy-triage")
hot = [round(accuracy(pairs(run_experiment(noisy, "v2", cases, f"logs/hot{i}.jsonl", temperature=0.7), True)), 2) for i in range(5)]
cold = [round(accuracy(pairs(run_experiment(noisy, "v2", cases, f"logs/cold{i}.jsonl", temperature=0), True)), 2) for i in range(3)]
print("temperature 0.7, five runs:", hot, "min", min(hot), "max", max(hot))
print("temperature 0, three runs :", cold)temperature 0.7, five runs: [0.58, 0.67, 0.5, 0.58, 0.67] min 0.5 max 0.67
temperature 0, three runs : [0.67, 0.67, 0.67]দুটি প্রম্পট ভার্সনের পার্থক্য যদি একই ভার্সনের পুনরাবৃত্তির ব্যাপ্তির চেয়ে কম হয়, আপনি দেখাননি যে একটি ভালো। টয়ের temperature-০ রান হুবহু একই, কারণ এটি টয়; পরীক্ষা না করে আসল মডেলের ক্ষেত্রে তা ধরে নেবেন না।
এক্সপেরিমেন্ট রিপোর্ট CSV-তে এক্সপোর্ট
রিপোর্ট হলো প্রতি প্রম্পট ভার্সনে একটি সারি, প্রতিবার একই কলাম, CSV হিসেবে লেখা, যাতে যেকোনো স্প্রেডশিটে খোলা যায় এবং প্রম্পটের পাশে কমিট করা যায়।
def summarise(records):
ok = [r for r in records if r["status"] == "ok"]
return {"version": records[0]["version"], "prompt_sha": records[0]["prompt_sha"], "cases": len(records), "ok": len(ok),
"strict_accuracy": round(accuracy(pairs(records)), 3), "lenient_accuracy": round(accuracy(pairs(records, True)), 3),
"mean_latency_ms_fake": round(sum(r["latency_ms"] for r in ok) / len(ok)),
"cost_per_10k_example": round(cost(records) / len(records) * 10_000, 3)}
write_csv("report.csv", [summarise(r) for r in runs.values()])
print(open("report.csv", encoding="utf-8").read().strip())version,prompt_sha,cases,ok,strict_accuracy,lenient_accuracy,mean_latency_ms_fake,cost_per_10k_example
v1,55f857e1,12,12,0.417,0.5,65,0.418
v2,fbece8b2,12,12,0.667,0.667,58,0.507অনুশীলন: ছোট প্রম্পট-তুলনা স্ক্রিপ্ট
এই পাতার শেষ বিষয়টি একটি ছোট নোটবুক-ধাঁচের স্ক্রিপ্ট: # %% চিহ্নসহ সাধারণ Python, যাতে এডিটরে সেল ধরে ধরে এবং টার্মিনালে ওপর থেকে নিচ পর্যন্ত চালানো যায়। নিচের কঠিন কাজে আপনাকে এটি লিখতে বলা হয়েছে; একটি আদর্শ উত্তর উত্তর অংশে আছে।
কাজের জায়গায় এটি কোথায় দেখা যায়
প্রম্পট বদল চালু হওয়ার আগে একটি টিম পুরনো ও নতুন ভার্সন একটি স্থির টেস্ট সেটে তুলনা করে এবং পরিবর্তনের অনুরোধের সঙ্গে CSV জুড়ে দেয়। রিভিউয়ার accuracy দেখেন, পণ্যের জন্য জরুরি লেবেলভিত্তিক recall দেখেন, প্রতি হাজার কলের খরচ দেখেন, আর যে সারিগুলো খারাপ হয়েছে সেগুলো দেখেন।
সাধারণ ভুল
- আউটপুট দেখার পরে প্রত্যাশিত উত্তর লেখা। আপনি মডেলের সঙ্গে একমত হবেন। প্রত্যাশা আগে লিখুন।
- শুধু accuracy ব্যবহার। একটি প্রম্পট সামগ্রিকভাবে ভালো স্কোর করেও জরুরি লেবেলটি মিস করতে পারে। লেবেল ধরে recall ও precision দেখুন।
- এররকে ভুল উত্তর গোনা, বা চুপচাপ বাদ দেওয়া। স্কোরের পাশে কতগুলো কেস উত্তর দিয়েছে তা রিপোর্ট করুন।
- নকল ল্যাটেন্সি বা মোটামুটি দামকে পরিমাপ বলা। উৎস ও তারিখ লিখুন, আর আসল ল্যাটেন্সি ক্লায়েন্টে মাপুন।
- একটি রানে বিশ্বাস করা। পুনরাবৃত্তি করুন, এবং পার্থক্যকে ব্যাপ্তির সঙ্গে তুলনা করুন।
নিজে চেষ্টা করুন
এই পাতার ফাইল ও নেমস্পেস ব্যবহার করুন, promptlab.py-সহ একই ফোল্ডারে।
- সহজ। lenient পাঠ ধরে ভার্সন ১-এর লেবেলভিত্তিক recall হিসাব করুন, এবং কোন লেবেল সবচেয়ে বেশি মিস হয় বলুন।
- মাঝারি। ইনভয়েস
{"vendor": "Omar Traders", "total": "75.00", "currency": "EGP"}সত্য মান হিসেবে এবং ভুল total-সহ একটি পূর্বানুমান যোগ করুন, আরfield_accuracy-র চারটি সংখ্যা রিপোর্ট করুন। - কঠিন। নোটবুক-ধাঁচের তুলনা স্ক্রিপ্ট লিখুন: সেটআপ, রান, স্কোর-এর সেল, আর একটি শেষ সেল যা এক লাইনের রায় ছাপে ("v2 is ahead by N points" বা "no clear difference") এবং CSV রিপোর্ট লেখে।
উত্তর
সহজ।
for label, s in precision_recall(pairs(runs["v1"], True), LABELS).items():
print(label, "recall", s["recall"])billing recall 0.4
delivery recall 0.5
other recall 0.67Billing recall সবচেয়ে কম (০.৪): ভার্সন ১-এ রিফান্ডের নিয়ম নেই, তাই রিফান্ডের মেসেজ c3 ও c11 অন্য লেবেলে গেছে, আর c7 এমন একটি শব্দ ব্যবহার করে যা টয় চেনে না। পাসের চেকলিস্ট: lenient জোড়া ব্যবহার করেছে, লেবেল ধরে recall ছেপেছে, এবং সবচেয়ে কম মানের লেবেলের নাম বলেছে।
মাঝারি।
golds2 = golds + [{"vendor": "Omar Traders", "total": "75.00", "currency": "EGP"}]
preds2 = preds + [{"vendor": "Omar Traders", "total": "57.00", "currency": "EGP"}]
print({k: round(v, 2) for k, v in field_accuracy(preds2, golds2, ["vendor", "total", "currency"]).items()}){'vendor': 0.83, 'total': 0.67, 'currency': 0.83, 'all': 0.33}পাসের চেকলিস্ট: চারটি কী (তিনটি ফিল্ড ও all); এখন প্রতিটি মান ছয়টি রেকর্ডের মধ্যে; নতুন total ভুল বলে total নেমে ০.৬৭ হয়েছে এবং all নেমে ০.৩৩ হয়েছে।
কঠিন। তিনটি সেল ক্রমানুসারে চলে; শেষ সেলটি শুধু দেওয়া দুটি ভার্সনের তুলনা করে এবং একটি কেসের চেয়ে ছোট পার্থক্যের দাবি করে না।
# %% setup
versions = ["v1", "v2"]
client = Client(make_triage(), model="toy-triage")
# %% run
results = {v: run_experiment(client, v, cases, f"logs/nb_{v}.jsonl") for v in versions}
# %% score
rows = [summarise(results[v]) for v in versions]
for row in rows:
print(row["version"], row["strict_accuracy"], row["lenient_accuracy"])
# %% verdict and report
gap = round((rows[1]["strict_accuracy"] - rows[0]["strict_accuracy"]) * 100)
one_case = round(100 / len(cases))
print(f"v2 is ahead by {gap} points" if gap >= one_case else "no clear difference")
write_csv("nb_report.csv", rows)v1 0.417 0.5
v2 0.667 0.667
v2 is ahead by 25 pointsপাসের চেকলিস্ট: প্রতি ধাপে একটি সেল, ফলে ব্যর্থতা কোথায় ঘটল দেখা যায়; দুটি ভার্সনের জন্য একই কেস ও ক্রম; রায়ের থ্রেশহোল্ড কোডে লেখা, চোখে ঠিক করা নয় (একটি কেস খুবই নিচু মানদণ্ড; বারোটি কেসে যেকোনো রায়কে ইঙ্গিত ধরুন, প্রমাণ নয়); CSV শেষে লেখা। এটি আগের পাতার error রেকর্ডের পুনরাবৃত্তি: summarise accuracy-র জন্য শুধু উত্তর পাওয়া কেস গোনে এবং cases-এর পাশে ok-ও রিপোর্ট করে।
সারসংক্ষেপ
- আউটপুট দেখার আগে প্রত্যাশিত উত্তর বা রুব্রিক লিখুন; বদ্ধ উত্তরের সেটে exact match ব্যবহার করুন, আর উত্তর বাচাল হতে পারলে strict ও lenient দুটি স্কোরই রিপোর্ট করুন।
- এক্সট্র্যাকশনে ফিল্ড ধরে তুলনা করুন; শ্রেণিবিন্যাসে কেবল accuracy নয়, কনফিউশন ম্যাট্রিক্স এবং লেবেলভিত্তিক precision ও recall পড়ুন।
- কোড যা বিচার করতে পারে না সেই মানের জন্য রুব্রিক ও একাধিক রিভিউয়ারসহ ব্লাইন্ড, এলোমেলো ক্রমের মানুষের রিভিউ ব্যবহার করুন।
- নকল ল্যাটেন্সি ও মোটামুটি টোকেন সংখ্যা শুধু আপনার কোড পরীক্ষা করে; আসল ল্যাটেন্সি ক্লায়েন্টে মাপুন এবং দাম প্রোভাইডারের বর্তমান মূল্য পাতা থেকে নিন।
- পরিবর্তনশীলতা দেখতে টেস্ট পুনরাবৃত্তি করুন, আর প্রতি প্রম্পট ভার্সনে একটি সারি দিয়ে একটি CSV এক্সপোর্ট করুন।
এরপর: মূল্যায়ন ডেটাসেট ডিজাইন: মেট্রিক, এজ কেস, হেল্ড-আউট সেট ও বেসলাইন, যেখানে আপনি কী মাপবেন তা ঠিক করবেন, বড় লেবেলযুক্ত ডেটাসেট বানাবেন এবং তার একটি অংশ লুকিয়ে রাখবেন।