ข้ามไปยังเนื้อหาหลัก

[LLM 9/10] Benchmarking: ตัวเลข accuracy ที่ไม่มี confidence interval คือข่าวลือ

· อ่าน 26 นาที
Kobkrit Viriyayudhakorn
CEO, iApp Technology

ตั้งแต่บทที่ 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 ความยาวตัวเลือกหรือไม่ (γ\gamma)สลับอันดับบน leaderboard ได้
0-shot หรือ 5-shot, template เขียนยังไงเปลี่ยนได้หลายจุด
ใส่ chat template ให้โมเดลไหนบ้างลำเอียงเข้าข้างโมเดลใดโมเดลหนึ่ง
ข้อสอบรั่วอยู่ในข้อมูลเทรนหรือไม่ (contamination)คะแนนสูงปลอมทั้งแท่ง

ตัวเลขหนึ่งตัวที่ปกปิดการตัดสินใจห้าชั้น บวกกับ error bar ที่ไม่มีใครวาด — นั่นคือสิ่งที่เราเรียกกันว่า leaderboard

2. เราจะทำอะไร (Solution)

บทนี้ไม่มีการเทรนเลย และนั่นคือจุดเด่น ไม่ใช่จุดอ่อน — การวัดผลเป็นงานคนละชนิดกับการเทรน และมันสมควรได้บทของตัวเอง

เราจะทำสี่อย่าง:

  1. เขียนระบบให้คะแนนสามโหมดจากศูนย์ — log-likelihood multiple-choice, generative exact-match (พร้อม normalization ภาษาไทย), และ LLM-as-judge พร้อม rubric ที่เขียนเป็นลายลักษณ์อักษร แล้วแสดงให้เห็นว่าทั้งสามโหมดให้คะแนนโมเดลตัวเดียวกันไม่เท่ากัน
  2. เอาทุก checkpoint จากบทที่ 1–8 มาวัดบนแกนเดียวกัน ด้วยข้อสอบไทยจริง (scb10x/thai_exam), โจทย์เลขไทย (VISAI-AI/gsm8k-thai) และ KobEval-TH ชุดประจำซีรีส์
  3. ติด Wilson 95% CI ให้ทุกตัวเลข แล้วดูว่าข้อสรุปไหนของซีรีส์รอดจาก error bar บ้าง
  4. ลอง reproduce ตัวเลขบน leaderboard สาธารณะ ของ Qwen3-0.6B บน ThaiExam — และถ้าไม่ตรง เราจะไล่หาสาเหตุแทนที่จะเงียบ
แนวคิดหลักของบทนี้

คะแนน benchmark ไม่ใช่คุณสมบัติของโมเดล มันคือคุณสมบัติของ (โมเดล × วิธีวัด × ชุดข้อสอบ × จำนวนข้อ) การรายงานตัวเลขโดยไม่รายงานอีกสามอย่างที่เหลือ คือการรายงานแค่เศษหนึ่งส่วนสี่ของความจริง และ confidence interval คือราคาขั้นต่ำของคำว่า "ผลการทดลอง"

3. สมการ (Equation)

3.1 Wilson score interval — CI ประจำซีรีส์

ถ้าตอบถูก ss จาก nn ข้อ ได้ p^=s/n\hat p = s/n ช่วงความเชื่อมั่นแบบ Wilson ที่ระดับ zz (95% → z=1.96z = 1.96) คือ

p^+z22n1+z2n  ±  z1+z2np^(1p^)n+z24n2\frac{\hat p + \frac{z^2}{2n}}{1 + \frac{z^2}{n}} \;\pm\; \frac{z}{1 + \frac{z^2}{n}}\sqrt{\frac{\hat p(1-\hat p)}{n} + \frac{z^2}{4n^2}}

ทำไมไม่ใช้สูตร normal approximation (p^±zp^(1p^)/n\hat p \pm z\sqrt{\hat p(1-\hat p)/n}) ที่จำง่ายกว่า เพราะมันพังตรงที่เราต้องใช้มันบ่อยที่สุด: ขอบ 0 กับ 1 และ nn เล็ก

ลองกรณีจริงจากบทที่ 8: guardrail ปล่อยคำขออันตรายหลุด 0 ครั้งจากชุดทดสอบ 30 ข้อ

from kobeval import wilson_ci      # ฟังก์ชันเดียวกับที่ใช้มาตั้งแต่บทที่ 1

wilson_ci(0, 30) # → (0.000, 0.114)
  • Normal approximation: 0±1.9601/30=0±00 \pm 1.96\sqrt{0 \cdot 1/30} = 0 \pm 0 — ช่วงกว้างศูนย์ ราวกับเรามั่นใจ 100% ว่าอัตราหลุดเป็น 0% จากการดูแค่ 30 ตัวอย่าง
  • Wilson: [0%, 11.4%][0\%,\ 11.4\%] — "เท่าที่ดูมา 30 ครั้งยังไม่เคยหลุด แต่ค่าจริงอาจสูงถึง 11%"

ช่วงที่สองคือประโยคที่ซื่อสัตย์ ช่วงแรกคือคำโกหกที่สูตรผลิตให้โดยอัตโนมัติ สังเกตในสูตรว่า Wilson ดึงจุดกึ่งกลางเข้าหา 1/21/2 ด้วยพจน์ z2/2nz^2/2n (เหมือนเติมข้อมูลเสมือน z24z^2 \approx 4 ข้อ ถูกครึ่งผิดครึ่ง) และมีพจน์ z2/4n2z^2/4n^2 กันความกว้างไม่ให้ยุบเป็นศูนย์ — นี่คือเหตุผลที่ kobeval ใช้ Wilson มาตลอดซีรีส์

3.2 Length-normalised multiple-choice scoring — ตัวเลขที่ตัดสิน leaderboard เงียบ ๆ

โหมด log-likelihood ให้โมเดลอ่านโจทย์ qq แล้วเทียบความน่าจะเป็นของตัวเลือก c1,,cmc_1,\dots,c_m ทั้งประโยค:

i^=argmaxilogpθ(ciq)ciγ\hat i = \arg\max_i \frac{\log p_\theta(c_i \mid q)}{|c_i|^{\gamma}}
  • γ=0\gamma = 0 → ใช้ log-prob รวมดิบ ๆ ซึ่งลำเอียงเข้าข้างตัวเลือกสั้น (token น้อย = โดนคูณความน่าจะเป็นน้อยครั้ง)
  • γ=1\gamma = 1 → หารด้วยจำนวน token คือใช้ log-prob เฉลี่ยต่อ token

สองบรรทัดนี้คือ acc กับ acc_norm ใน lm-evaluation-harness และบน benchmark จริง มันให้คะแนนไม่เท่ากันและบางครั้งสลับอันดับโมเดล — เวลาเห็นตาราง paper สองฉบับรายงาน ThaiExam ไม่เท่ากัน สาเหตุอันดับต้น ๆ คือ γ\gamma คนละค่า โดยที่ทั้งสองฉบับไม่ได้เขียนไว้เลยว่าใช้ค่าไหน

3.3 pass@k แบบไม่ลำเอียง — สำหรับโจทย์ที่ตรวจอัตโนมัติได้

โจทย์เลขและโจทย์โค้ดยอมให้เราสุ่มหลายคำตอบแล้วถามว่า "มีสักอันไหมที่ถูก" สุ่ม nn ครั้ง ถูก cc ครั้ง ตัวประมาณที่ถูกต้องของ pass@kk คือ

pass@k^=1(nck)/(nk)\widehat{\text{pass@}k} = 1 - \binom{n-c}{k}\Big/\binom{n}{k}

ตัวประมาณแบบสัญชาตญาณ 1(1c/n)k1 - (1 - c/n)^k นั้นลำเอียง: ฟังก์ชัน f(p)=1(1p)kf(p) = 1-(1-p)^k เป็นฟังก์ชันเว้า (concave) ใน pp ดังนั้นตาม Jensen's inequality E[f(p^)]f(p)\mathbb{E}[f(\hat p)] \le f(p) — เสียบค่าประมาณเข้าไปในฟังก์ชันไม่เชิงเส้นแล้วค่าคาดหวังจะไม่ตรงกับของจริง ส่วนสูตรทวินามข้างบนคือ "สัดส่วนของการหยิบ kk ตัวจาก nn ตัวที่ผิดยกชุด" ซึ่งพิสูจน์ได้ว่า unbiased พอดีเป๊ะ

3.4 McNemar's test — เทียบสองโมเดลบนข้อสอบชุดเดียวกัน

โมเดล A กับ B ทำข้อสอบชุดเดียวกัน ให้ bb = จำนวนข้อที่ A ถูกแต่ B ผิด, cc = จำนวนข้อที่ B ถูกแต่ A ผิด:

χ2=(bc1)2b+c\chi^2 = \frac{(|b-c|-1)^2}{b+c}

เทียบกับ χ12\chi^2_1 (แบบมี continuity correction) จุดสำคัญคือคำว่า paired: ข้อที่ทั้งคู่ถูกและข้อที่ทั้งคู่ผิดไม่อยู่ในสูตรเลย เพราะมันไม่ได้บอกอะไรว่าใครเก่งกว่า ถ้าคุณใช้ t-test เทียบ accuracy สองตัวเหมือนมาจากคนละชุดข้อสอบ คุณกำลังโยนโครงสร้าง "ข้อเดียวกัน" ทิ้ง แล้วต้องใช้ข้อสอบมากกว่าหลายเท่าเพื่อ power เท่าเดิม

3.5 Contamination check — ข้อสอบรั่วอยู่ในข้อมูลเทรนไหม

สำหรับข้อสอบ xx และคลังข้อมูลเทรน D\mathcal{D} นิยามอัตราการทับซ้อนของ kk-gram:

overlapk(x)=Gk(x)Gk(D)Gk(x)\text{overlap}_k(x) = \frac{\left|G_k(x) \cap G_k(\mathcal{D})\right|}{\left|G_k(x)\right|}

โดย Gk()G_k(\cdot) คือเซตของ kk-gram ทั้งหมด สำหรับภาษาไทยเราใช้ k-gram ระดับตัวอักษร (เช่น 20 ตัวอักษร) เพราะการตัดคำไทยมีความกำกวม ถ้า overlapk\text{overlap}_k สูง (เช่นเกิน 0.7) ให้สงสัยว่าโมเดลเคย "เห็นเฉลย" มาแล้ว — คะแนนบนข้อนั้นวัดความจำ ไม่ได้วัดความสามารถ

4. เห็นภาพสมการ (Visualize)

ความกว้างของ CI คือฟังก์ชันของ n — และ n=100 ให้ ±10 จุด

กราฟครึ่งความกว้างของ Wilson 95% confidence interval เทียบกับจำนวนข้อสอบบนแกน log จาก 10 ถึง 10,000 ข้อ สำหรับ accuracy 0.60, 0.75 และ 0.90 พร้อมจุดเน้นที่ n=100กราฟครึ่งความกว้างของ Wilson 95% confidence interval เทียบกับจำนวนข้อสอบบนแกน log จาก 10 ถึง 10,000 ข้อ สำหรับ accuracy 0.60, 0.75 และ 0.90 พร้อมจุดเน้นที่ n=100

Figure 9.1ครึ่งความกว้างของ Wilson 95% CI ต่อจำนวนข้อสอบ n — ที่ n=100 ช่วงกว้าง ±6 ถึง ±9.4 จุดขึ้นกับระดับ accuracy และการทำให้แคบลง 10 เท่าต้องจ่ายข้อสอบเพิ่ม 100 เท่า

นี่คือรูปที่สำคัญที่สุดของบท ความกว้างหดตาม 1/n1/\sqrt{n} ดังนั้นชุดทดสอบ 100 ข้อไม่มีทางแยกโมเดลที่ห่างกัน 4 จุดได้ ไม่ว่าคุณจะรันซ้ำกี่รอบ และถ้าอยากอ่านช่องว่าง 1 จุดอย่างมั่นใจ คุณต้องมีข้อสอบราวหมื่นข้อ — ซึ่ง benchmark ไทยส่วนใหญ่ไม่มี

leaderboard หน้าตาเป็นอย่างไรเมื่อวาด error bar ลงไปจริง ๆ

dot plot ของโมเดลห้าตัวเรียงตาม accuracy พร้อมแถบ Wilson 95% CI โดยช่วงของโมเดลอันดับกลางสามตัวซ้อนทับกันและถูกแรเงาว่าแยกจากกันไม่ได้dot plot ของโมเดลห้าตัวเรียงตาม accuracy พร้อมแถบ Wilson 95% CI โดยช่วงของโมเดลอันดับกลางสามตัวซ้อนทับกันและถูกแรเงาว่าแยกจากกันไม่ได้

Figure 9.2กระดานคะแนนสมมติ 5 โมเดล n=100 ข้อ — ช่วง Wilson ของสามอันดับกลางซ้อนทับกันหมด จึงเป็น 'ก้อนเดียว' ไม่ใช่สามอันดับ (ตัวเลขสมมติเพื่อประกอบคำอธิบาย ช่วง CI คำนวณจริง — ฉบับวัดจริงอยู่ในหัวข้อที่ 8)

โมเดล B, C, D ต่างกันสูงสุด 5 จุด แต่ช่วง CI กว้าง ±9 จุด — ข้อสรุปเดียวที่ข้อมูลรองรับคือ "สามตัวนี้แยกไม่ออก" ใครที่ประกาศว่า D ชนะ B กำลังอ่านสัญญาณจากเสียงรบกวน

สิ่งที่คนไม่รายงาน ใหญ่กว่าสิ่งที่คนเถียงกัน

box plot ของคะแนนโมเดลเดียวกันบน prompt template ห้าสำนวน เทียบกับจุดคะแนนที่รายงานของโมเดลสองตัวที่ห่างกันสองจุด แสดงว่าการกระจายจาก template กว้างกว่าช่องว่างระหว่างโมเดลbox plot ของคะแนนโมเดลเดียวกันบน prompt template ห้าสำนวน เทียบกับจุดคะแนนที่รายงานของโมเดลสองตัวที่ห่างกันสองจุด แสดงว่าการกระจายจาก template กว้างกว่าช่องว่างระหว่างโมเดล

Figure 9.3โมเดลตัวเดิมวัดด้วย prompt template 5 สำนวนที่ความหมายเหมือนกัน — การกระจาย 8.5 จุด กว้างกว่าช่องว่าง 2 จุดระหว่าง 'คู่แข่ง' บน leaderboard (ค่าสมมติในระดับที่พบจริงในงานวิจัย prompt sensitivity — ระบุกำกับในภาพ)

McNemar: 100 ข้อ แต่มีสาระแค่ 10 ข้อ

heat map ตาราง 2 คูณ 2 แสดง a=70 ทั้งคู่ถูก, b=9 เฉพาะโมเดล A ถูก, c=1 เฉพาะโมเดล B ถูก, d=20 ทั้งคู่ผิด พร้อมค่าไคสแควร์และ p-value ของ McNemarheat map ตาราง 2 คูณ 2 แสดง a=70 ทั้งคู่ถูก, b=9 เฉพาะโมเดล A ถูก, c=1 เฉพาะโมเดล B ถูก, d=20 ทั้งคู่ผิด พร้อมค่าไคสแควร์และ p-value ของ McNemar

Figure 9.4ตาราง contingency 2×2 ของสองโมเดลบนข้อสอบชุดเดียวกัน 100 ข้อ — ทั้งการทดสอบใช้แค่ช่อง b กับ c ค่า χ² = 4.90 และ p ≈ 0.027 คำนวณจากสูตรจริง

สองโมเดลห่างกัน 8 จุด (79% เทียบ 71%) — ฟังดูชัด แต่หลักฐานจริงคือ 10 ข้อที่เห็นไม่ตรงกัน McNemar บอกว่าเพิ่งจะข้ามเส้น p=0.05p = 0.05 มานิดเดียว ถ้า b=6,c=4b=6, c=4 (ห่างกัน 2 จุดเท่า leaderboard ทั่วไป) จะได้ χ2=0.1\chi^2 = 0.1 — ไม่มีนัยสำคัญเลยแม้แต่น้อย

เล่นกับความสัมพันธ์ n → CI ด้วยมือตัวเอง

widget ตัวนี้คือตัวเดียวกับบทที่ 8 (guardrails) แต่คราวนี้ให้ดูคนละมุม: ตัวเลข TPR / FPR / precision ทุกตัวในนั้นมี Wilson CI ตัวเดียวกับสมการ 3.1 กำกับอยู่ ลองลาก threshold τ\tau ไปสุดขอบจนช่องใดช่องหนึ่งของ confusion matrix เหลือตัวอย่างไม่กี่ตัว แล้วดู CI บานออกต่อหน้าต่อตา — นั่นคือกราฟ 9.1 ในร่างโต้ตอบได้

Flag a request as unsafe when score ≥ τ.
How much worse is letting an unsafe request through than blocking a safe one?
The evaluation set is 34.3% unsafe. Real traffic is usually far cleaner.
Optimal τ0.595

safeunsafeDrag the line, or use the τ slider.

ROCAUC = 0.987
00.5100.51FPRTPR
Precision-Recallat the eval base rate 34.3%
00.5100.51recallprecision
Expected costC(τ) = 10·π·FNR + (1−π)·FPR
00.5100.51τcost
Measured on the held-out set at τ = 0.500 (n = 700)
predicted unsafepredicted safe
actually unsafe220TP20FN
actually safe22FP438TN
Recall (TPR)91.7%95% CI 87.5%–94.5%
FPR4.8%95% CI 3.2%–7.1%
Precision on eval set90.9%95% CI 86.6%–93.9%
Precision at live base rate16.2%95% CI 11.0%–23.1%
Expected cost0.0557minimised at τ = 0.595
Flagged per 10,000565473 of them false alarms

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() โกหกคุณบน T4

torch รุ่นใหม่ตอบ 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 สาธารณะใช้กันมากที่สุด และเป็นแกนของบทนี้

ตรวจ license บน dataset card ก่อนใช้ — โน้ตบุ๊กบังคับขั้นตอนนี้

ข้อสอบจริงมีเจ้าของ ชุดข้อมูลที่ 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 ที่ยังตอบเป็นประโยคไม่เป็น ข้อจำกัด: วัดได้เฉพาะข้อปรนัย และคะแนนขึ้นกับ γ\gamma ตามที่เห็นในหัวข้อ 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 — นั่นคือ γ=0\gamma=0 กับ length-normalised ของสมการ 3.2 นั่นเอง

8. ผลลัพธ์ (Results)

รูปสรุปทั้งซีรีส์ — ทุก checkpoint บนแกนเดียว พร้อม error bar

โน้ตบุ๊กจบด้วยรูปที่ซีรีส์นี้เดินทางมา 8 บทเพื่อวาด: ทุก checkpoint จากบทที่ 1–8 เรียงบนแกนเดียวกัน วัดด้วยสัญญาเดียวกัน พร้อมแถบ Wilson 95% CI ทุกแท่ง (ตัวเลขจริงอยู่ใน results.json ของโน้ตบุ๊ก — ในตารางนี้จึงเป็น ? จนกว่าคุณจะรันเอง):

checkpointThaiExam (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
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆ

base

Thai 18%41 tokens
The sky appears blue because of Rayleigh scattering. ท้องฟ้า is blue เพราะ light scatter ครับ. Shorter wavelengths scatter more than longer ones.

sft

Thai 99%78 tokens
ท้องฟ้าเป็นสีฟ้าเพราะแสงอาทิตย์กระทบกับโมเลกุลของอากาศแล้วเกิดการกระเจิงแบบเรย์ลี ซึ่งแสงสีน้ำเงินที่มีความยาวคลื่นสั้นกว่าจะกระเจิงได้มากกว่าแสงสีแดง เราจึงมองเห็นท้องฟ้าเป็นสีฟ้าครับ

Showing the built-in sample.

ตัวอย่างข้างบนคือคำตอบจริงของโมเดลที่ข้อเดียวกันได้คะแนนไม่เท่ากันสามโหมด — เช่น ตอบ "๕๐ บาท" แล้ว exact-match แบบไม่ normalize ให้ 0, แบบ normalize ให้ 1, judge ให้ 1 หรือคำตอบที่เหตุผลถูกทั้งทางแต่สรุปเลขผิด ซึ่ง judge บางครั้งใจดีเกินเฉลย

ภารกิจความซื่อสัตย์: reproduce ตัวเลข leaderboard ให้ได้ — หรือรู้ให้ได้ว่าทำไมไม่ได้

โน้ตบุ๊กปิดท้ายด้วยงานที่สอนมากกว่าทุกเซลล์รวมกัน: ไปเปิด leaderboard สาธารณะที่รายงาน Qwen3-0.6B บน ThaiExam จดตัวเลขที่เขาประกาศ แล้วพยายามผลิตตัวเลขเดียวกันจากเครื่องเรา

ถ้าไม่ตรง (และรอบแรกมักไม่ตรง) ห้ามหยุดที่ "เกือบเท่ากันแล้ว" — ไล่ทีละตัวแปร:

  1. prompt template ตรงกับของเขาไหม (กราฟ 9.3 บอกแล้วว่าเรื่องนี้ตัวเดียวก็เกินพอ)
  2. โหมดให้คะแนน — เขาใช้ log-likelihood หรือ generative, acc หรือ acc_norm (γ\gamma!)
  3. จำนวน shot — 0-shot กับ 5-shot คนละโลก
  4. enable_thinking — Qwen3 มีโหมดคิดในใจ เปิด/ปิดเปลี่ยนทั้งคะแนนและเวลา
  5. เวอร์ชันชุดข้อมูลและ subset — thai_exam มีห้าวิชา เขาเฉลี่ยแบบไหน
  6. batch size ตอน generate — padding ต่างกันทำให้ greedy ให้ผลต่างกันได้จริง

รายงานที่บอกว่า "เราได้ 41.8 ขณะที่ leaderboard รายงาน 43.5 และสาเหตุคือเขาใช้ 5-shot กับ acc_norm ส่วนเราใช้ 0-shot กับ acc" มีค่ามากกว่ารายงานที่ตัวเลขตรงเป๊ะแต่อธิบายไม่ได้ว่าทำไม — เพราะฉบับแรกพิสูจน์ว่าคุณควบคุมเครื่องวัดของตัวเองได้ ฉบับหลังอาจแค่โชคดี

9. เปรียบเทียบ (Comparison)

บททุกบทก่อนหน้าเทียบ "โมเดลหลายตัว วิธีวัดเดียว" — บทนี้พลิกกลับ: โมเดลตัวเดียว (SFT จากบทที่ 2) วิธีวัดหลายแบบ และดูว่าคะแนนแกว่งแค่ไหน จากการตัดสินใจที่ปกติไม่มีใครเขียนรายงาน:

การตั้งค่า (ต่างจากสัญญาแค่จุดเดียวต่อแถว)ThaiExamGSM8K-THKobEval-TH
สัญญาประจำซีรีส์ (loglik γ=1\gamma=1, 0-shot, thinking off)??
γ=0\gamma = 0 แทน γ=1\gamma = 1??
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=256
  • enable_thinking=False
  • seed=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 คนละ γ\gamma — เทียบได้เฉพาะตัวเลขที่วัดภายใต้ 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 กว้างศูนย์ที่ p^=0\hat p = 0 — จุดที่เราต้องการมันที่สุด
  • คะแนนเป็นคุณสมบัติของ (โมเดล × วิธีวัด × ข้อสอบ × n) โหมดให้คะแนน, γ\gamma, 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)

  1. Liang et al. (2022). Holistic Evaluation of Language Models — HELM: การวัดผลหลายมิติแทนตัวเลขเดียว
  2. Biderman et al. (2024). Lessons from the Trenches on Reproducible Evaluation of Language Models — บทเรียนจาก lm-evaluation-harness เรื่องการวัดผลให้ทำซ้ำได้
  3. Miller (2024). Adding Error Bars to Evals: A Statistical Approach to Language Model Evaluations — ทำไมทุกตัวเลข accuracy ต้องมี error bar
  4. Zheng et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — LLM-as-a-judge และอคติของมัน
  5. Chiang et al. (2024). Chatbot Arena: An Open Platform for Evaluating LLMs by Human Preference — การจัดอันดับด้วยความชอบของมนุษย์จริง
  6. Hendrycks et al. (2020). Measuring Massive Multitask Language Understanding — MMLU: ต้นแบบของ benchmark แบบเลือกตอบ
  7. Chen et al. (2021). Evaluating Large Language Models Trained on Code — นิยาม pass@k แบบ unbiased ที่ใช้ในหัวข้อ 9
  8. Wilson (1927). Probable Inference, the Law of Succession, and Statistical Inference — ช่วงความเชื่อมั่น Wilson ที่ใช้ทุกตัวเลขในซีรีส์นี้
  9. McNemar (1947). Note on the Sampling Error of the Difference Between Correlated Proportions or Percentages — การทดสอบแบบจับคู่สำหรับเทียบสองโมเดลบนข้อสอบชุดเดียวกัน

บทความ โค้ด และโน้ตบุ๊กในซีรีส์นี้เผยแพร่ภายใต้สัญญาอนุญาต CC BY-NC-SA 4.0 — นำไปใช้และดัดแปลงต่อได้ โดยอ้างอิงที่มา ไม่ใช้เพื่อการค้า และเผยแพร่ต่อด้วยสัญญาเดียวกัน (โมเดลและชุดข้อมูลของบุคคลที่สามที่อ้างถึง ยังคงใช้สัญญาของเจ้าของเดิม)