[LLM 9/10] Benchmarking: ตัวเลข accuracy ที่ไม่มี confidence interval คือข่าวลือ
ตั้งแต่บทที่ 1 ซีรีส์นี้พูดประโยคเดิมซ้ำทุกครั้งที่รายงานตัวเลข: "accuracy ที่ไม่มี confidence interval ไม่ใช่ผลการทดลอง มันคือข่าวลือ" และสัญญาไว้ว่าจะอธิบายเต็ม ๆ ในบทที่ 9 — บทนี้แหละครับ เราจะไม่เทรนอะไรเลยแม้แต่ step เดียว แต่จะสร้างระบบวัดผลสามโหมดขึ้นจากศูนย์ เอาทุก checkpoint ที่เทรนมาตลอดซีรีส์ขึ้นมาวัดบนข้อสอบไทยจริง แล้วพิสูจน์ว่า การตั้งค่าที่ไม่มีใครเขียนไว้ใน paper เปลี่ยนคะแนนของโมเดลตัวเดิมได้มากกว่าช่องว่างบน leaderboard ที่คนเถียงกัน
Open in Colab09_benchmarking.ipynb
1. ปัญหา (Problem statement)
ลองอ่านประโยคแบบนี้ที่เจอได้ทุกสัปดาห์: "โมเดล X ได้ 71.2% บน ThaiExam แซงโมเดล Y ที่ได้ 69.8%"
คำถามเดียวที่ควรถามคือ วัดจากกี่ข้อ ถ้าชุดทดสอบมี 100 ข้อ ช่วงความเชื่อมั่น 95% ของแต่ละตัวเลขกว้างราว ±8–10 จุด แปลว่า 71.2% กับ 69.8% คือตัวเลขเดียวกันที่บังเอิญสุ่มออกมาไม่เท่ากัน การประกาศผู้ชนะจากช่องว่าง 1.4 จุดบนชุดทดสอบ 100 ข้อ ไม่ต่างจากโยนเหรียญสิบครั้งแล้วสรุปว่าเหรียญเอียง
ปัญหาไม่ได้หยุดที่ขนาดชุดทดสอบ เพราะ "คะแนน benchmark" หนึ่งตัวเลข เกิดจากการตัดสินใจหลายชั้นที่แทบไม่มีใครรายงาน:
| การตัดสินใจที่ซ่อนอยู่ | ผลต่อคะแนน |
|---|---|
| ให้คะแนนแบบ log-likelihood หรือ generative | เปลี่ยนได้หลายจุด |
| normalize ความยาวตัวเลือกหรือไม่ () | สลับอันดับบน leaderboard ได้ |
| 0-shot หรือ 5-shot, template เขียนยังไง | เปลี่ยนได้หลายจุด |
| ใส่ chat template ให้โมเดลไหนบ้าง | ลำเอียงเข้าข้างโมเดลใดโมเดลหนึ่ง |
| ข้อสอบรั่วอยู่ในข้อมูลเทรนหรือไม่ (contamination) | คะแนนสูงปลอมทั้งแท่ง |
ตัวเลขหนึ่งตัวที่ปกปิดการตัดสินใจห้าชั้น บวกกับ error bar ที่ไม่มีใครวาด — นั่นคือสิ่งที่เราเรียกกันว่า leaderboard
2. เราจะทำอะไร (Solution)
บทนี้ไม่มีการเทรนเลย และนั่นคือจุดเด่น ไม่ใช่จุดอ่อน — การวัดผลเป็นงานคนละชนิดกับการเทรน และมันสมควรได้บทของตัวเอง
เราจะทำสี่อย่าง:
- เขียนระบบให้คะแนนสามโหมดจากศูนย์ — log-likelihood multiple-choice, generative exact-match (พร้อม normalization ภาษาไทย), และ LLM-as-judge พร้อม rubric ที่เขียนเป็นลายลักษณ์อักษร แล้วแสดงให้เห็นว่าทั้งสามโหมดให้คะแนนโมเดลตัวเดียวกันไม่เท่ากัน
- เอาทุก checkpoint จากบทที่ 1–8 มาวัดบนแกนเดียวกัน ด้วยข้อสอบไทยจริง
(
scb10x/thai_exam), โจทย์เลขไทย (VISAI-AI/gsm8k-thai) และ KobEval-TH ชุดประจำซีรีส์ - ติด Wilson 95% CI ให้ทุกตัวเลข แล้วดูว่าข้อสรุปไหนของซีรีส์รอดจาก error bar บ้าง
- ลอง reproduce ตัวเลขบน leaderboard สาธารณะ ของ Qwen3-0.6B บน ThaiExam — และถ้าไม่ตรง เราจะไล่หาสาเหตุแทนที่จะเงียบ
คะแนน benchmark ไม่ใช่คุณสมบัติของโมเดล มันคือคุณสมบัติของ (โมเดล × วิธีวัด × ชุดข้อสอบ × จำนวนข้อ) การรายงานตัวเลขโดยไม่รายงานอีกสามอย่างที่เหลือ คือการรายงานแค่เศษหนึ่งส่วนสี่ของความจริง และ confidence interval คือราคาขั้นต่ำของคำว่า "ผลการทดลอง"
3. สมการ (Equation)
3.1 Wilson score interval — CI ประจำซีรีส์
ถ้าตอบถูก จาก ข้อ ได้ ช่วงความเชื่อมั่นแบบ Wilson ที่ระดับ (95% → ) คือ
ทำไมไม่ใช้สูตร normal approximation () ที่จำง่ายกว่า เพราะมันพังตรงที่เราต้องใช้มันบ่อยที่สุด: ขอบ 0 กับ 1 และ เล็ก
ลองกรณีจริงจากบทที่ 8: guardrail ปล่อยคำขออันตรายหลุด 0 ครั้งจากชุดทดสอบ 30 ข้อ
from kobeval import wilson_ci # ฟังก์ชันเดียวกับที่ใช้มาตั้งแต่บทที่ 1
wilson_ci(0, 30) # → (0.000, 0.114)
- Normal approximation: — ช่วงกว้างศูนย์ ราวกับเรามั่นใจ 100% ว่าอัตราหลุดเป็น 0% จากการดูแค่ 30 ตัวอย่าง
- Wilson: — "เท่าที่ดูมา 30 ครั้งยังไม่เคยหลุด แต่ค่าจริงอาจสูงถึง 11%"
ช่วงที่สองคือประโยคที่ซื่อสัตย์ ช่วงแรกคือคำโกหกที่สูตรผลิตให้โดยอัตโนมัติ
สังเกตในสูตรว่า Wilson ดึงจุดกึ่งกลางเข้าหา ด้วยพจน์ (เหมือนเติมข้อมูลเสมือน ข้อ ถูกครึ่งผิดครึ่ง)
และมีพจน์ กันความกว้างไม่ให้ยุบเป็นศูนย์ — นี่คือเหตุผลที่ kobeval ใช้ Wilson มาตลอดซีรีส์
3.2 Length-normalised multiple-choice scoring — ตัวเลขที่ตัดสิน leaderboard เงียบ ๆ
โหมด log-likelihood ให้โมเดลอ่านโจทย์ แล้วเทียบความน่าจะเป็นของตัวเลือก ทั้งประโยค:
- → ใช้ log-prob รวมดิบ ๆ ซึ่งลำเอียงเข้าข้างตัวเลือกสั้น (token น้อย = โดนคูณความน่าจะเป็นน้อยครั้ง)
- → หารด้วยจำนวน token คือใช้ log-prob เฉลี่ยต่อ token
สองบรรทัดนี้คือ acc กับ acc_norm ใน lm-evaluation-harness และบน benchmark จริง
มันให้คะแนนไม่เท่ากันและบางครั้งสลับอันดับโมเดล —
เวลาเห็นตาราง paper สองฉบับรายงาน ThaiExam ไม่เท่ากัน สาเหตุอันดับต้น ๆ คือ คนละค่า
โดยที่ทั้งสองฉบับไม่ได้เขียนไว้เลยว่าใช้ค่าไหน
3.3 pass@k แบบไม่ลำเอียง — สำหรับโจทย์ที่ตรวจอัตโนมัติได้
โจทย์เลขและโจทย์โค้ดยอมให้เราสุ่มหลายคำตอบแล้วถามว่า "มีสักอันไหมที่ถูก" สุ่ม ครั้ง ถูก ครั้ง ตัวประมาณที่ถูกต้องของ pass@ คือ
ตัวประมาณแบบสัญชาตญาณ นั้นลำเอียง: ฟังก์ชัน เป็นฟังก์ชันเว้า (concave) ใน ดังนั้นตาม Jensen's inequality — เสียบค่าประมาณเข้าไปในฟังก์ชันไม่เชิงเส้นแล้วค่าคาดหวังจะไม่ตรงกับของจริง ส่วนสูตรทวินามข้างบนคือ "สัดส่วนของการหยิบ ตัวจาก ตัวที่ผิดยกชุด" ซึ่งพิสูจน์ได้ว่า unbiased พอดีเป๊ะ
3.4 McNemar's test — เทียบสองโมเดลบนข้อสอบชุดเดียวกัน
โมเดล A กับ B ทำข้อสอบชุดเดียวกัน ให้ = จำนวนข้อที่ A ถูกแต่ B ผิด, = จำนวนข้อที่ B ถูกแต่ A ผิด:
เทียบกับ (แบบมี continuity correction) จุดสำคัญคือคำว่า paired: ข้อที่ทั้งคู่ถูกและข้อที่ทั้งคู่ผิดไม่อยู่ในสูตรเลย เพราะมันไม่ได้บอกอะไรว่าใครเก่งกว่า ถ้าคุณใช้ t-test เทียบ accuracy สองตัวเหมือนมาจากคนละชุดข้อสอบ คุณกำลังโยนโครงสร้าง "ข้อเดียวกัน" ทิ้ง แล้วต้องใช้ข้อสอบมากกว่าหลายเท่าเพื่อ power เท่าเดิม
3.5 Contamination check — ข้อสอบรั่วอยู่ในข้อมูลเทรนไหม
สำหรับข้อสอบ และคลังข้อมูลเทรน นิยามอัตราการทับซ้อนของ -gram:
โดย คือเซตของ -gram ทั้งหมด สำหรับภาษาไทยเราใช้ k-gram ระดับตัวอักษร (เช่น 20 ตัวอักษร) เพราะการตัดคำไทยมีความกำกวม ถ้า สูง (เช่นเกิน 0.7) ให้สงสัยว่าโมเดลเคย "เห็นเฉลย" มาแล้ว — คะแนนบนข้อนั้นวัดความจำ ไม่ได้วัดความสามารถ
4. เห็นภาพสมการ (Visualize)
ความกว้างของ CI คือฟังก์ชันของ n — และ n=100 ให้ ±10 จุด
Figure 9.1ครึ่งความกว้างของ Wilson 95% CI ต่อจำนวนข้อสอบ n — ที่ n=100 ช่วงกว้าง ±6 ถึง ±9.4 จุดขึ้นกับระดับ accuracy และการทำให้แคบลง 10 เท่าต้องจ่ายข้อสอบเพิ่ม 100 เท่า
นี่คือรูปที่สำคัญที่สุดของบท ความกว้างหดตาม ดังนั้นชุดทดสอบ 100 ข้อไม่มีทางแยกโมเดลที่ห่างกัน 4 จุดได้ ไม่ว่าคุณจะรันซ้ำกี่รอบ และถ้าอยากอ่านช่องว่าง 1 จุดอย่างมั่นใจ คุณต้องมีข้อสอบราวหมื่นข้อ — ซึ่ง benchmark ไทยส่วนใหญ่ไม่มี
leaderboard หน้าตาเป็นอย่างไรเมื่อวาด error bar ลงไปจริง ๆ
Figure 9.2กระดานคะแนนสมมติ 5 โมเดล n=100 ข้อ — ช่วง Wilson ของสามอันดับกลางซ้อนทับกันหมด จึงเป็น 'ก้อนเดียว' ไม่ใช่สามอันดับ (ตัวเลขสมมติเพื่อประกอบคำอธิบาย ช่วง CI คำนวณจริง — ฉบับวัดจริงอยู่ในหัวข้อที่ 8)
โมเดล B, C, D ต่างกันสูงสุด 5 จุด แต่ช่วง CI กว้าง ±9 จุด — ข้อสรุปเดียวที่ข้อมูลรองรับคือ "สามตัวนี้แยกไม่ออก" ใครที่ประกาศว่า D ชนะ B กำลังอ่านสัญญาณจากเสียงรบกวน
สิ่งที่คนไม่รายงาน ใหญ่กว่าสิ่งที่คนเถียงกัน
Figure 9.3โมเดลตัวเดิมวัดด้วย prompt template 5 สำนวนที่ความหมายเหมือนกัน — การกระจาย 8.5 จุด กว้างกว่าช่องว่าง 2 จุดระหว่าง 'คู่แข่ง' บน leaderboard (ค่าสมมติในระดับที่พบจริงในงานวิจัย prompt sensitivity — ระบุกำกับในภาพ)
McNemar: 100 ข้อ แต่มีสาระแค่ 10 ข้อ
Figure 9.4ตาราง contingency 2×2 ของสองโมเดลบนข้อสอบชุดเดียวกัน 100 ข้อ — ทั้งการทดสอบใช้แค่ช่อง b กับ c ค่า χ² = 4.90 และ p ≈ 0.027 คำนวณจากสูตรจริง
สองโมเดลห่างกัน 8 จุด (79% เทียบ 71%) — ฟังดูชัด แต่หลักฐานจริงคือ 10 ข้อที่เห็นไม่ตรงกัน McNemar บอกว่าเพิ่งจะข้ามเส้น มานิดเดียว ถ้า (ห่างกัน 2 จุดเท่า leaderboard ทั่วไป) จะได้ — ไม่มีนัยสำคัญเลยแม้แต่น้อย
เล่นกับความสัมพันธ์ n → CI ด้วยมือตัวเอง
widget ตัวนี้คือตัวเดียวกับบทที่ 8 (guardrails) แต่คราวนี้ให้ดูคนละมุม: ตัวเลข TPR / FPR / precision ทุกตัวในนั้นมี Wilson CI ตัวเดียวกับสมการ 3.1 กำกับอยู่ ลองลาก threshold ไปสุดขอบจนช่องใดช่องหนึ่งของ confusion matrix เหลือตัวอย่างไม่กี่ตัว แล้วดู CI บานออกต่อหน้าต่อตา — นั่นคือกราฟ 9.1 ในร่างโต้ตอบได้
safeunsafeDrag the line, or use the τ slider.
| predicted unsafe | predicted safe | |
|---|---|---|
| actually unsafe | 220TP | 20FN |
| actually safe | 22FP | 438TN |
Precision has collapsed.The classifier looks excellent on the evaluation set — 90.9% precision — but at a live base rate of 1.0% it drops to 16.2%. Nothing about the model changed; TPR and FPR are identical. There are simply so many more safe requests than unsafe ones that an FPR of 4.8% produces more false alarms than the classifier finds true positives. This is why a guardrail benchmarked on a balanced set falls apart in production, and why FPR, not accuracy, is the number to negotiate over.
Showing a synthetic held-out set.
5. เตรียมสภาพแวดล้อม (Environment)
เปิด Colab เลือก Runtime → Change runtime type → T4 GPU (แผนฟรีพอ) บทนี้เป็น inference ล้วน — ไม่มี optimizer ไม่มี gradient — กวาดทุก checkpoint ที่ขนาด 0.6B ใช้เวลารวมราว 15 นาที สบาย ๆ กว่าทุกบทที่ผ่านมา
Colab T4 คือสถาปัตยกรรม Turing (SM 7.5) ซึ่ง ไม่รองรับ bfloat16 และ ไม่รองรับ FlashAttention-2
แต่ config.json ของ Qwen3-0.6B ระบุ torch_dtype: bfloat16 เอาไว้
ดังนั้น torch_dtype="auto" คือกับดัก โค้ดจะพังหรือช้าผิดปกติโดยไม่บอกสาเหตุ
torch_dtype=torch.float16 # ไม่ใช่ bfloat16
attn_implementation="sdpa" # ไม่ใช่ flash_attention_2
บทนี้ไม่เทรน จึงไม่ต้องกังวลเรื่อง fp16=True ใน TrainingArguments — โหลดโมเดลเป็น fp16 แล้ววัดได้เลย
cap = torch.cuda.get_device_capability(0)
print("compute capability:", cap) # T4 = (7, 5)
print("native bf16:", cap[0] >= 8) # T4 -> False
print("torch says :", torch.cuda.is_bf16_supported()) # T4 -> True (นับ emulation!)
is_bf16_supported() โกหกคุณบน T4torch รุ่นใหม่ตอบ True บน T4 เพราะนับ การจำลอง (emulation) ว่ารองรับด้วย ซึ่งช้ากว่า fp16 มาก
ให้เช็ค compute capability ≥ 8.0 (Ampere ขึ้นไป) แทน — นี่คือบั๊กจริงที่เจอตอนรันโน้ตบุ๊กบน Colab จริง ๆ
checkpoint ทั้งซีรีส์ต่อยอดจากฐาน Qwen3-0.6B ตัวเดียวกัน ส่วนใหญ่เป็น LoRA adapter เราจึงโหลดน้ำหนักฐานครั้งเดียว แล้วสลับ adapter เข้า-ออกทีละตัว — VRAM ไม่บานตามจำนวน checkpoint:
CHECKPOINTS = {
"base": None, # Qwen3-0.6B-Base เปล่า ๆ
"01-cpt": "kobkrit/qwen3-0.6b-th-cpt", # full weights (บทที่ 1)
"02-sft": "kobkrit/qwen3-0.6b-th-sft-lora", # LoRA (บทที่ 2)
"03-ppo": "kobkrit/qwen3-0.6b-th-ppo-lora", # LoRA (บทที่ 3)
"04-dpo": "kobkrit/qwen3-0.6b-th-dpo-lora", # LoRA (บทที่ 4)
"05-grpo": "kobkrit/qwen3-0.6b-th-grpo-lora", # LoRA (บทที่ 5)
"06-ctx-dist": "kobkrit/qwen3-0.6b-th-ctxdist-lora", # LoRA (บทที่ 6)
"07-distill": "kobkrit/qwen3-0.6b-th-distill", # นักเรียนจากบทที่ 7
"08-guarded": "kobkrit/qwen3-0.6b-th-sft-lora+guard", # โมเดล + ตัวกรองจากบทที่ 8
}
6. เตรียมข้อมูล (Data)
6.1 scb10x/thai_exam — ข้อสอบมาตรฐานไทยของจริง
ชุดข้อสอบปรนัยจากสนามสอบจริงของไทย (O-NET, IC, TGAT, TPAT-1, A-Level) นี่คือ benchmark ภาษาไทยที่ leaderboard สาธารณะใช้กันมากที่สุด และเป็นแกนของบทนี้
ข้อสอบจริงมีเจ้าของ ชุดข้อมูลที่ derive จากข้อสอบจริงจึงมีเงื่อนไขการใช้ที่ต้องอ่านเองจาก dataset card บน Hugging Face ก่อนโหลด — อย่าเดา อย่า copy โค้ดข้ามขั้นตอนนี้ โน้ตบุ๊กจะดึง metadata ของ card ขึ้นมาพิมพ์และหยุดรอให้คุณยืนยันก่อนไปต่อ:
from huggingface_hub import DatasetCard
card = DatasetCard.load("scb10x/thai_exam")
print("license:", card.data.get("license"))
print(card.text[:1500]) # อ่านเงื่อนไขในหน้า card ด้วยตาตัวเอง
ถ้า license ไม่อนุญาตการใช้งานของคุณ (เช่นเชิงพาณิชย์) ให้หยุดตรงนั้น การวัดผลที่เริ่มจากการละเมิดเงื่อนไขข้อมูล ไม่มีทางเรียกว่า rigorous
6.2 VISAI-AI/gsm8k-thai — โจทย์เลขแบบ generative
GSM8K ฉบับแปลไทย: โจทย์ปัญหาคณิตศาสตร์ที่ต้องสร้างคำตอบเอง ไม่มีตัวเลือกให้เทียบ log-likelihood จึงเป็นสนามทดสอบของโหมด generative exact-match โดยเฉพาะ (ตรวจ license บน card เช่นเดียวกัน)
6.3 KobEval-TH — ชุดประจำซีรีส์ 100 ข้อ
ชุดวัดที่ใช้มาตั้งแต่บทที่ 1 (TH-KNOW ความรู้ไทย, การทำตามคำสั่ง, th_ratio)
ข้อดีของมันไม่ใช่ความใหญ่ — 100 ข้อให้ CI ±10 จุดตามกราฟ 9.1 —
แต่คือความคงที่: ทุกบทวัดด้วยชุดเดิม วิธีเดิม จึงเทียบข้ามบทได้จริง
6.4 Normalization ภาษาไทย — ด่านที่ exact-match ไทยตายบ่อยที่สุด
"๕๐" กับ "50" กับ " 50 " คือคำตอบเดียวกัน แต่ == ของ Python ไม่คิดแบบนั้น:
import re
THAI_DIGITS = str.maketrans("๐๑๒๓๔๕๖๗๘๙", "0123456789")
def normalize_thai(s: str) -> str:
s = s.strip().translate(THAI_DIGITS) # เลขไทย ๐-๙ → อารบิก
s = s.replace(",", "") # 1,000 → 1000
s = re.sub(r"\s+", "", s) # ไทยไม่ใช้ช่องว่างแบ่งคำ — ตัดทิ้งทั้งหมด
return s
โน้ตบุ๊กมี unit test เล็ก ๆ ของฟังก์ชันนี้ เพราะบั๊กในตัวตรวจคือ contamination กลับด้าน: มันทำให้โมเดลดูแย่กว่าจริงอย่างเป็นระบบ และไม่มี error ใด ๆ ฟ้องคุณ
7. โค้ดหลัก (Main code)
7.1 โหมดที่ 1 — log-likelihood multiple-choice (สมการ 3.2 ตรงตัว)
import torch
@torch.no_grad()
def score_mc(model, tok, question, choices, gamma=1.0):
scores = []
for c in choices:
q_ids = tok(question, return_tensors="pt").input_ids.cuda()
full_ids = tok(question + c, return_tensors="pt").input_ids.cuda()
logits = model(full_ids).logits[:, :-1]
targets = full_ids[:, 1:]
logp = torch.log_softmax(logits.float(), -1).gather(
-1, targets.unsqueeze(-1)).squeeze(-1)
ans = logp[0, q_ids.shape[1] - 1:] # เฉพาะ token ของตัวเลือก
scores.append(ans.sum().item() / len(ans) ** gamma)
return int(torch.tensor(scores).argmax()) # ไม่มีการ generate เลยสักตัว
ข้อดี: deterministic 100%, เร็ว, ใช้ได้กับ base model ที่ยังตอบเป็นประโยคไม่เป็น ข้อจำกัด: วัดได้เฉพาะข้อปรนัย และคะแนนขึ้นกับ ตามที่เห็นในหัวข้อ 9
7.2 โหมดที่ 2 — generative exact-match
@torch.no_grad()
def score_generative(model, tok, question, gold):
msgs = [{"role": "user", "content": question}]
ids = tok.apply_chat_template(msgs, add_generation_prompt=True,
enable_thinking=False, # สัญญาประจำซีรีส์
return_tensors="pt").cuda()
out = model.generate(ids, max_new_tokens=256, do_sample=False) # greedy
pred = tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True)
return int(normalize_thai(extract_answer(pred)) == normalize_thai(gold))
extract_answer ดึงคำตอบสุดท้ายจากข้อความ (ตัวเลขตัวท้ายสำหรับ GSM8K-TH,
ตัวอักษรข้อสำหรับปรนัย) — ฟังก์ชันนี้เองก็เป็นการตัดสินใจที่กระทบคะแนน และต้องรายงาน
7.3 โหมดที่ 3 — LLM-as-judge พร้อม rubric เป็นลายลักษณ์อักษร
JUDGE_RUBRIC = """คุณคือกรรมการตรวจข้อสอบ ตัดสินตามเกณฑ์นี้เท่านั้น:
1 = ใจความถูกต้องตรงกับเฉลย (ยอมรับการสะกดต่างกัน เลขไทย/อารบิก
และการเรียบเรียงคนละแบบที่ความหมายเดียวกัน)
0 = ผิด ตอบไม่ตรงคำถาม หรือไม่ตอบ
ห้ามให้คะแนนความสวยงามของภาษา ห้ามให้คะแนนความยาว
ตอบเป็น JSON เท่านั้น: {"score": 0 หรือ 1, "reason": "สั้น ๆ"}
โจทย์: {q}
เฉลย: {gold}
คำตอบของโมเดล: {pred}"""
ผู้ตัดสินต้องเป็นโมเดลที่แข็งแรงกว่าผู้ถูกตัดสินมาก โน้ตบุ๊กออกแบบให้เสียบ endpoint
แบบ OpenAI-compatible ตัวไหนก็ได้ และบันทึก rubric + ชื่อรุ่นผู้ตัดสินลง results.json เสมอ
— ผลจาก judge ที่ไม่บอกว่า judge คือใครและใช้เกณฑ์อะไร ก็เป็นข่าวลืออีกชนิดหนึ่ง
7.4 สิ่งที่มืออาชีพใช้ — lm-evaluation-harness
สามโหมดข้างบนเราเขียนเองเพื่อให้เห็นไส้ใน แต่งานจริงควรยืนบนเครื่องมือที่ community ตรวจสอบแล้ว:
pip install lm-eval
lm_eval --tasks list | grep -i thai # ดูชื่อ task ที่มีจริงในเวอร์ชันของคุณก่อน
lm_eval --model hf \
--model_args pretrained=Qwen/Qwen3-0.6B,dtype=float16 \
--tasks thai_exam \
--num_fewshot 5 \
--batch_size 8 \
--seed 42 \
--log_samples --output_path results/harness
--log_samples สำคัญที่สุดในบรรทัดทั้งหมด: มันบันทึกคำตอบรายข้อ
ทำให้เรา (1) คำนวณ Wilson CI เองได้ (2) จับคู่รายข้อทำ McNemar ได้ และ (3) ไล่ดูว่าข้อไหนผิดเพราะอะไร
สังเกตว่า harness รายงานทั้ง acc และ acc_norm — นั่นคือ กับ length-normalised ของสมการ 3.2 นั่นเอง
8. ผลลัพธ์ (Results)
รูปสรุปทั้งซีรีส์ — ทุก checkpoint บนแกนเดียว พร้อม error bar
โน้ตบุ๊กจบด้วยรูปที่ซีรีส์นี้เดินทางมา 8 บทเพื่อวาด:
ทุก checkpoint จากบทที่ 1–8 เรียงบนแกนเดียวกัน วัดด้วยสัญญาเดียวกัน พร้อมแถบ Wilson 95% CI ทุกแท่ง
(ตัวเลขจริงอยู่ใน results.json ของโน้ตบุ๊ก — ในตารางนี้จึงเป็น ? จนกว่าคุณจะรันเอง):
| checkpoint | ThaiExam (95% CI) | GSM8K-TH (95% CI) | KobEval-TH (95% CI) | th_ratio |
|---|---|---|---|---|
| Qwen3-0.6B-Base | ? | ? | ? | ? |
| บทที่ 1 — CPT | ? | ? | ? | ? |
| บทที่ 2 — SFT-LoRA | ? | ? | ? | ? |
| บทที่ 3 — PPO | ? | ? | ? | ? |
| บทที่ 4 — DPO | ? | ? | ? | ? |
| บทที่ 5 — GRPO | ? | ? | ? | ? |
| บทที่ 6 — Context distillation | ? | ? | ? | ? |
| บทที่ 7 — Model distillation | ? | ? | ? | ? |
| บทที่ 8 — SFT + guardrail | ? | ? | ? | ? |
สิ่งที่ควรคาดหวังก่อนรัน (เขียน hypothesis ก่อนดูผลเสมอ — นิสัยที่ดีที่สุดที่บทนี้สอนได้):
CI ของ checkpoint ส่วนใหญ่จะซ้อนทับกันบน ThaiExam เพราะการเทรนของเราจิ๋วมากเทียบกับ pretraining
ความแตกต่างที่ควรรอดจาก error bar คือ th_ratio (บทที่ 4 ตั้งใจแก้เรื่องนี้ตรง ๆ)
และการทำตามรูปแบบคำสั่งใน KobEval-TH (ฝีมือของ SFT บทที่ 2)
ถ้ารูปจริงออกมาต่างจากนี้ นั่นคือสิ่งที่น่าตื่นเต้น ไม่ใช่สิ่งที่ต้องซ่อน
คำตอบเดียวกัน สามกรรมการ สามคะแนน
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆbase
sft
Showing the built-in sample.
ตัวอย่างข้างบนคือคำตอบจริงของโมเดลที่ข้อเดียวกันได้คะแนนไม่เท่ากันสามโหมด — เช่น ตอบ "๕๐ บาท" แล้ว exact-match แบบไม่ normalize ให้ 0, แบบ normalize ให้ 1, judge ให้ 1 หรือคำตอบที่เหตุผลถูกทั้งทางแต่สรุปเลขผิด ซึ่ง judge บางครั้งใจดีเกินเฉลย
ภารกิจความซื่อสัตย์: reproduce ตัวเลข leaderboard ให้ได้ — หรือรู้ให้ได้ว่าทำไมไม่ได้
โน้ตบุ๊กปิดท้ายด้วยงานที่สอนมากกว่าทุกเซลล์รวมกัน: ไปเปิด leaderboard สาธารณะที่รายงาน Qwen3-0.6B บน ThaiExam จดตัวเลขที่เขาประกาศ แล้วพยายามผลิตตัวเลขเดียวกันจากเครื่องเรา
ถ้าไม่ตรง (และรอบแรกมักไม่ตรง) ห้ามหยุดที่ "เกือบเท่ากันแล้ว" — ไล่ทีละตัวแปร:
- prompt template ตรงกับของเขาไหม (กราฟ 9.3 บอกแล้วว่าเรื่องนี้ตัวเดียวก็เกินพอ)
- โหมดให้คะแนน — เขาใช้ log-likelihood หรือ generative,
accหรือacc_norm(!) - จำนวน shot — 0-shot กับ 5-shot คนละโลก
enable_thinking— Qwen3 มีโหมดคิดในใจ เปิด/ปิดเปลี่ยนทั้งคะแนนและเวลา- เวอร์ชันชุดข้อมูลและ subset — thai_exam มีห้าวิชา เขาเฉลี่ยแบบไหน
- batch size ตอน generate — padding ต่างกันทำให้ greedy ให้ผลต่างกันได้จริง
รายงานที่บอกว่า "เราได้ 41.8 ขณะที่ leaderboard รายงาน 43.5 และสาเหตุคือเขาใช้ 5-shot กับ acc_norm ส่วนเราใช้ 0-shot กับ acc" มีค่ามากกว่ารายงานที่ตัวเลขตรงเป๊ะแต่อธิบายไม่ได้ว่าทำไม — เพราะฉบับแรกพิสูจน์ว่าคุณควบคุมเครื่องวัดของตัวเองได้ ฉบับหลังอาจแค่โชคดี
9. เปรียบเทียบ (Comparison)
บททุกบทก่อนหน้าเทียบ "โมเดลหลายตัว วิธีวัดเดียว" — บทนี้พลิกกลับ: โมเดลตัวเดียว (SFT จากบทที่ 2) วิธีวัดหลายแบบ และดูว่าคะแนนแกว่งแค่ไหน จากการตัดสินใจที่ปกติไม่มีใครเขียนรายงาน:
| การตั้งค่า (ต่างจากสัญญาแค่จุดเดียวต่อแถว) | ThaiExam | GSM8K-TH | KobEval-TH |
|---|---|---|---|
| สัญญาประจำซีรีส์ (loglik , 0-shot, thinking off) | ? | — | ? |
| แทน | ? | — | ? |
| generative exact-match แทน loglik | ? | ? | ? |
| 5-shot แทน 0-shot | ? | ? | ? |
| LLM-as-judge แทน exact-match | ? | ? | ? |
enable_thinking=True | ? | ? | ? |
(GSM8K-TH ไม่มีตัวเลือกให้เทียบ log-likelihood จึงวัดได้เฉพาะโหมด generative — ช่องนั้นเว้นว่างโดยตั้งใจ)
ทุกแถวคือโมเดลตัวเดิม น้ำหนักเดิมทุกไบต์ สิ่งที่ควรเห็นคือคะแนนแกว่งหลายจุด — มากกว่าช่องว่างระหว่างโมเดลบน leaderboard ทั่วไป และนี่คือคำตอบสุดท้ายของคำถามที่บทนี้ตั้งไว้: เวลาต่างสำนักรายงานตัวเลขไม่ตรงกัน ส่วนมากไม่มีใครโกหก — แค่ไม่มีใครวัดด้วยเครื่องเดียวกัน
ทุกตัวเลขในบทที่ 1–8 วัดภายใต้เงื่อนไขเดียวกันเป๊ะ:
- greedy decoding (
do_sample=False) max_new_tokens=256enable_thinking=Falseseed=42- KobEval-TH ชุดเดิม 100 ข้อ ไม่เคยแก้ระหว่างทาง + Wilson 95% CI ทุกตัวเลข
ตอนบทที่ 1 ประกาศเงื่อนไขพวกนี้ มันดูเหมือนความจู้จี้ มาถึงบรรทัดนี้คุณรู้แล้วว่าทำไม: ถ้าไม่มีสัญญานี้ ตารางในหัวข้อ 8 จะเทียบกันข้ามบทไม่ได้เลย — มันจะเป็นตาราง 9 แถวที่วัดด้วยไม้บรรทัด 9 อัน
กับดักที่ต้องระวัง
1. Contamination — ข้อสอบรั่วอยู่ในข้อมูลเทรนของเราเอง
โน้ตบุ๊กรัน check ตามสมการ 3.5 (20-char-gram) ระหว่างข้อสอบ ThaiExam / KobEval-TH
กับคลังที่เราใช้เทรนในบทที่ 1–2 (thaigov-v2 และชุด instruct สังเคราะห์)
จุดที่ต้องจับตา: thaigov คือเอกสารราชการไทย ส่วน ThaiExam มีข้อสอบเกี่ยวกับกฎ ระเบียบ และความรู้ราชการ —
โอกาสทับซ้อนจริงมีอยู่ ถ้าเจอ hit ให้รายงานรายข้อและตัดออกจากการสรุปผล ไม่ใช่ทำเป็นมองไม่เห็น
(ผลการสแกนจริงพิมพ์อยู่ในโน้ตบุ๊ก — จำนวน hit คือ ? จนกว่าจะรัน)
2. เทียบตัวเลขข้าม paper ที่ template ต่างกัน "โมเดลเราได้ 45 บน ThaiExam ส่วน paper นั้นรายงาน 43" ไม่มีความหมายใด ๆ ถ้าคนละ template คนละ shot คนละ — เทียบได้เฉพาะตัวเลขที่วัดภายใต้ harness เดียวกัน config เดียวกันเท่านั้น
3. ใส่ chat template ให้โมเดลหนึ่งแต่ไม่ใส่ให้อีกโมเดล วัด base model ด้วย chat template = กดคะแนนมันฟรี ๆ / วัด instruct model แบบไม่ใส่ template = กดคะแนนเช่นกัน ในตารางหัวข้อ 8 checkpoint ของเรามีทั้งสองชนิด (CPT คือ base, ที่เหลือคือ chat) โน้ตบุ๊กจึงพิมพ์ prompt จริงข้อแรกของทุกโมเดลให้ตรวจด้วยตา — บรรทัดเดียวที่กันความอยุติธรรมทั้งตาราง
4. Judge ลำเอียงเข้าข้างครอบครัวตัวเอง (self-enhancement bias) งานวิจัยหลายชิ้นพบว่า LLM judge ให้คะแนนคำตอบสไตล์โมเดลตระกูลเดียวกับตัวเองสูงกว่าที่ควร ถ้า judge กับผู้ถูกวัดคือญาติกัน ตัวเลขจะหอมหวานผิดปกติ — ทางแก้ที่ถูกไม่ใช่หา judge ที่ "เป็นกลาง" (ไม่มีอยู่จริง) แต่คือรายงานชื่อ judge เสมอ และเช็คด้วย judge ต่างตระกูลเมื่อผลใกล้เคียงกัน
5. Exact-match ภาษาไทยพังเพราะเลขไทย
โมเดลตอบ "๕๐" เฉลยเขียน "50" — ผิดทันทีถ้าลืม normalize_thai
บั๊กชนิดนี้กระจายไม่สม่ำเสมอด้วย: โมเดลที่ CPT ด้วยเอกสารราชการ (ซึ่งใช้เลขไทยเยอะ) จะโดนหักคะแนนมากกว่าเพื่อน
กลายเป็น bias เชิงระบบที่แกล้งโมเดลบางตัวโดยเฉพาะ — ตัวตรวจต้องยุติธรรมก่อน แล้วค่อยพูดเรื่องอันดับ
10. สรุป (Summary)
- accuracy ที่ไม่มี CI คือข่าวลือ — n=100 ให้ ±10 จุด และช่องว่างบน leaderboard ส่วนใหญ่เล็กกว่านั้น
- Wilson ไม่ใช่ความหรูหรา normal approximation ให้ CI กว้างศูนย์ที่ — จุดที่เราต้องการมันที่สุด
- คะแนนเป็นคุณสมบัติของ (โมเดล × วิธีวัด × ข้อสอบ × n) โหมดให้คะแนน, , shot, template เปลี่ยนคะแนนได้มากกว่าที่โมเดลต่างกันจริง
- เทียบสองโมเดลบนข้อสอบชุดเดียวกัน ให้ใช้ McNemar — ข้อที่เห็นตรงกันไม่ใช่หลักฐาน
- pass@k ต้องใช้ตัวประมาณทวินาม ตัวประมาณ plug-in ลำเอียงเพราะ Jensen
- สแกน contamination ก่อนเชื่อคะแนน โดยเฉพาะเมื่อข้อมูลเทรนกับข้อสอบมาจากโดเมนใกล้กัน
- ประกาศสัญญาการวัดผลตั้งแต่วันแรกแล้วไม่แตะมันอีก — ของซีรีส์นี้: greedy, 256 token, thinking off, seed 42
- reproduce ตัวเลขคนอื่นแล้วอธิบายส่วนต่างให้ได้ มีค่ากว่าได้ตัวเลขตรงโดยไม่รู้สาเหตุ
Benchmark วัดสิ่งที่วัดง่าย ไม่ใช่สิ่งที่สำคัญ ข้อปรนัยตรวจอัตโนมัติได้จึงกลายเป็นมาตรฐาน แต่ผู้ใช้จริงไม่ได้มาพร้อมตัวเลือก ก ข ค ง — เขามาพร้อมคำถามยาว ๆ บริบทเฉพาะตัว และความคาดหวังที่วัดเป็น accuracy ไม่ได้
ThaiExam สูงไม่ได้แปลว่ามีประโยชน์กับคนไทย โมเดลที่ทำข้อสอบ A-Level เก่ง อาจร่างหนังสือราชการไม่ได้เรื่อง ตอบลูกค้าไม่เป็นธรรมชาติ หรือแข็งทื่อจนไม่มีใครอยากใช้ ความสัมพันธ์ระหว่างคะแนนสอบกับคุณค่าใช้งานจริงนั้นหลวมกว่าที่ leaderboard ทำให้เรารู้สึกมาก
และข้อที่ควรติดผนังไว้: human eval 30 ข้อบน traffic จริงของ product คุณ มักบอกอะไรได้มากกว่าข้อสอบปรนัย 10,000 ข้อ — CI ของมันกว้างกว่าก็จริง (สมการ 3.1 บอกเราแล้วว่ากว้างเท่าไหร่) แต่มันวัดสิ่งที่ถูกต้องอย่างหยาบ ๆ ซึ่งชนะการวัดสิ่งที่ผิดอย่างละเอียดเสมอ บทนี้ให้เครื่องมือสถิติไว้ใช้กับการวัดทั้งสองชนิด — อย่าใช้มันกับชนิดที่สะดวกเพียงชนิดเดียว
บทต่อไป: Deployment — โมเดลที่วัดผลแล้วต้องออกไปเจอผู้ใช้จริง quantization ต้องแลกอะไร, เสิร์ฟบนเครื่องอะไรได้บ้าง และตัวเลขจากบทนี้จะกลายเป็น regression test ของ production ได้อย่างไร
อ้างอิง (References)
- Liang et al. (2022). Holistic Evaluation of Language Models — HELM: การวัดผลหลายมิติแทนตัวเลขเดียว
- Biderman et al. (2024). Lessons from the Trenches on Reproducible Evaluation of Language Models — บทเรียนจาก lm-evaluation-harness เรื่องการวัดผลให้ทำซ้ำได้
- Miller (2024). Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations — ทำไมทุกตัวเลข accuracy ต้องมี error bar
- Zheng et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — LLM-as-a-judge และอคติของมัน
- Chiang et al. (2024). Chatbot Arena: An Open Platform for Evaluating LLMs by Human Preference — การจัดอันดับด้วยความชอบของมนุษย์จริง
- Hendrycks et al. (2020). Measuring Massive Multitask Language Understanding — MMLU: ต้นแบบของ benchmark แบบเลือกตอบ
- Chen et al. (2021). Evaluating Large Language Models Trained on Code — นิยาม pass@k แบบ unbiased ที่ใช้ในหัวข้อ 9
- Wilson (1927). Probable Inference, the Law of Succession, and Statistical Inference — ช่วงความเชื่อมั่น Wilson ที่ใช้ทุกตัวเลขในซีรีส์นี้
- McNemar (1947). Note on the Sampling Error of the Difference Between Correlated Proportions or Percentages — การทดสอบแบบจับคู่สำหรับเทียบสองโมเดลบนข้อสอบชุดเดียวกัน
บทความ โค้ด และโน้ตบุ๊กในซีรีส์นี้เผยแพร่ภายใต้สัญญาอนุญาต CC BY-NC-SA 4.0 — นำไปใช้และดัดแปลงต่อได้ โดยอ้างอิงที่มา ไม่ใช้เพื่อการค้า และเผยแพร่ต่อด้วยสัญญาเดียวกัน (โมเดลและชุดข้อมูลของบุคคลที่สามที่อ้างถึง ยังคงใช้สัญญาของเจ้าของเดิม)
