[LLM 5/10] GRPO: ลบ value network ทิ้ง แล้วให้กลุ่มคำตอบเป็น baseline ของกันเอง
บทที่แล้วเราปิดท้ายด้วยช่องว่างของ DPO: มันจัดอันดับได้เฉพาะคำตอบที่มีคนเตรียมไว้ในไฟล์ ส่วนบทที่ 3 เราจ่ายราคาเต็มของ PPO: โมเดล 4 ตัวใน VRAM และ value network ทั้งตัวที่ต้องเทรนเพิ่ม บทนี้เราจะเอาข้อดีของทั้งสองมารวมกัน — ให้โมเดลสุ่มคำตอบของตัวเองมาเรียนแบบ RL จริง ๆ แต่ลบ value network ทิ้งทั้งก้อน ด้วยข้อสังเกตทางสถิติที่เรียบง่ายจนน่าหงุดหงิดว่าทำไมไม่มีใครคิดก่อน: ถ้าสุ่มคำตอบหลายอันต่อโจทย์เดียวกัน ค่าเฉลี่ย reward ของกลุ่มก็คือ baseline ที่ value network พยายามประมาณอยู่แล้ว และถ้าโจทย์ตรวจคำตอบได้ด้วยโค้ด เราไม่ต้องใช้ข้อมูล preference จากมนุษย์เลย — ศูนย์คู่ ศูนย์บาท
Open in Colab05_grpo.ipynb
1. ปัญหา (Problem statement)
ลองตั้งโจทย์แบบนี้: สอน Qwen3-0.6B ให้แก้โจทย์คณิตศาสตร์ภาษาไทย
คำตอบสุดท้ายเป็นตัวเลขหนึ่งตัว ตรวจถูกผิดได้ด้วยเครื่องหมาย == บรรทัดเดียว
เอาเครื่องมือจากสามบทที่ผ่านมามาไล่ดูทีละตัว จะพบว่าไม่มีตัวไหนพอดีกับงานนี้เลย:
| วิธี | โมเดลสุ่มคำตอบเองแล้วเรียนจากมันได้ไหม | ต้องมี label จากมนุษย์ | โมเดลใน VRAM |
|---|---|---|---|
| SFT (บทที่ 2) | ไม่ได้ — เลียนแบบเฉลยอย่างเดียว | เฉลยที่คนเขียน | 1 |
| PPO (บทที่ 3) | ได้ | คู่ preference สำหรับเทรน reward model | 4 |
| DPO (บทที่ 4) | ไม่ได้ — offline ล้วน | คู่ preference | 2 (LoRA เหลือ 1) |
- SFT สอนให้เลียนแบบวิธีทำของเฉลย แต่ไม่เคยให้โมเดลลองผิดลองถูกเอง โมเดลไม่เคยเห็นว่า "วิธีคิดของตัวเอง" แบบไหนพาไปคำตอบถูก
- PPO ให้โมเดลลองเองได้ แต่แลกด้วยการเทรน reward model จากคู่ preference บวก value network อีกทั้งตัว — ทั้งที่งานนี้ reward เขียนเป็นฟังก์ชัน Python ได้ตรง ๆ
- DPO ตัด RL ทิ้งได้สวยงาม แต่มันจัดอันดับได้แค่คำตอบที่มีอยู่แล้วในชุดข้อมูล โจทย์เลขต้องการให้โมเดลลองหลาย ๆ ทางแล้วเสริมทางที่ไปถึงคำตอบถูก
คำถามของบทนี้จึงแคบและคม: ใน PPO มีชิ้นส่วนไหนที่จำเป็นจริง และชิ้นไหนลบทิ้งได้ เมื่อ reward ของเราตรวจได้ด้วยโค้ด
2. เราจะทำอะไร (Solution)
ย้อนกลับไปที่หน้าที่ของ value network ในบทที่ 3: มันมีไว้ตอบคำถามเดียวคือ "โดยเฉลี่ยแล้ว prompt นี้ควรได้ reward ประมาณเท่าไหร่" เพื่อใช้เป็น baseline เอาไปหักออกจาก reward จริง — คำตอบที่ "ดีกว่าค่าเฉลี่ย" ได้ gradient บวก ที่ "แย่กว่าค่าเฉลี่ย" ได้ลบ ถ้าไม่มี baseline ตัวนี้ policy gradient จะ noise สูงจนเทรนแทบไม่ได้
PPO ตอบคำถามนั้นด้วยการเทรนโมเดลอีกตัวทั้งตัวขึ้นมาทำนายค่าเฉลี่ยนี้ GRPO ตอบด้วยการสุ่มให้เห็นกับตา:
สุ่มคำตอบ อันจาก prompt เดียวกัน แล้วเฉลี่ย reward ของกลุ่ม — ค่าเฉลี่ยนั้นคือ unbiased estimate ของ "reward ที่คาดหวังจาก prompt นี้" อยู่แล้วโดยนิยาม มันคือสิ่งเดียวกับที่ value network พยายามประมาณ แต่ไม่ต้องเทรน ไม่ต้องโหลด ไม่มีวันประมาณเพี้ยน value network ทั้งตัวจึงลบทิ้งได้ และเมื่อ reward ตรวจด้วยโค้ด (คำตอบเลขถูกหรือผิด) reward model กับข้อมูล preference จากมนุษย์ก็หายตามไปด้วย — เหลือศูนย์ label
นี่คือ GRPO (Group Relative Policy Optimization) เสนอโดย Shao และคณะ (2024) ใน DeepSeekMath และเป็นเครื่องยนต์ตัวเดียวกับที่เทรน DeepSeek-R1 แนวทางนี้มีชื่อเรียกรวม ๆ ว่า RLVR (RL with Verifiable Rewards) — RL ที่ reward มาจากตัวตรวจ ไม่ใช่จากรสนิยมมนุษย์
และขอวางความคาดหวังให้ตรงตั้งแต่ต้นบท: หลักฐานปัจจุบันชี้ว่า RL แบบนี้ส่วนใหญ่ทำหน้าที่ "เหลา" ความสามารถที่โมเดลฐานมีอยู่แล้วที่ pass@8 ให้ย้ายมาโผล่ที่ pass@1 มากกว่าจะสร้างความสามารถใหม่จากศูนย์ เราจะกลับมาเรื่องนี้พร้อมเครื่องมือวัดในหัวข้อ 9
3. สมการ (Equation)
3.1 Group-relative advantage — หัวใจทั้งหมดอยู่บรรทัดเดียว
- = จำนวนคำตอบที่สุ่มจาก prompt เดียวกัน (ในบทนี้คือ 8)
- = reward ของคำตอบที่
- ทุก token ของคำตอบที่ ใช้ ตัวเดียวกันทั้งประโยค — ต่างจาก PPO ที่พยายามให้ advantage ละเอียดราย token ผ่าน value network และ GAE
อ่านเป็นภาษาคน: "คำตอบนี้ดีกว่าหรือแย่กว่าความพยายามครั้งอื่น ๆ ของฉันเอง ต่อโจทย์ข้อเดียวกัน" ไม่มีการเปรียบเทียบข้ามโจทย์ ไม่มีการทำนายอนาคต มีแค่การแข่งกันเองในกลุ่ม
สมการสั้น ๆ นี้มีผลตามมาที่สำคัญมาก: ถ้าทั้งกลุ่มได้ reward เท่ากันหมด (ถูกหมดหรือผิดหมด) ทุก เป็นศูนย์ และ batch นั้นไม่สอนอะไรเลย จำประโยคนี้ไว้ มันจะกลายเป็นทั้งกับดักอันดับหนึ่งและตัวชี้วัดที่สำคัญที่สุดของบท
3.2 GRPO objective ฉบับเต็ม
โดย คืออัตราส่วนความน่าจะเป็นของ token เทียบกับ policy ตอนสุ่ม
อ่านทีละชิ้น เพราะทุกชิ้นเคยผ่านตามาแล้วในซีรีส์นี้:
- = PPO clip เดิมจากบทที่ 3 ไม่มีอะไรใหม่ — กันไม่ให้ก้าวไกลเกินไปจากจุดที่สุ่ม rollout มา
- = เฉลี่ยต่อ token กันคำตอบยาวได้อิทธิพลเกินตัว (นึกถึง length bias จากบทที่ 4)
- = สายจูงเส้นเดิมที่ผูกกับ ตัวเดียวกับบทที่ 3 และ 4
สิ่งที่ควรอ่านคือสิ่งที่ไม่อยู่ในสมการ: ไม่มี ไม่มี GAE ไม่มี critic loss ทั้งบรรทัดใช้แค่โมเดลสองตัว ( กับ ) และเลข reward จากตัวตรวจ
3.3 พจน์ KL ไม่ได้คำนวณตรง ๆ — รู้จัก k3 estimator
KL divergence จริง ๆ ต้อง sum ทั้ง vocabulary ของทุกตำแหน่ง ซึ่งแพงและไม่จำเป็น GRPO ประมาณมันจาก token ที่สุ่มออกมาแล้ว ด้วย estimator ชื่อเล่นว่า k3:
คำถามที่นักเรียนถามเสมอ (และควรถาม): ทำไมไม่ใช้ ตรง ๆ ในเมื่อค่าคาดหวังของมันก็คือ KL อยู่แล้ว?
คำตอบ: ตัว naive (เรียกว่า k1) unbiased ก็จริง แต่ราย sample มันติดลบได้ — ประมาณ 40% ของ sample ให้ค่าติดลบ ทั้งที่ KL เป็นลบไม่ได้โดยนิยาม — และ variance สูงมาก ที่ batch ขนาดจริง ค่าประมาณจะแกว่งจน penalty เดี๋ยวผลักเดี๋ยวดึง
k3 แก้ทั้งสองข้อพร้อมกัน ให้ แล้วสังเกตสองข้อเท็จจริง:
- อสมการ เป็นจริงเสมอ ดังนั้น k3 ทุก sample
- ดังนั้นพจน์ มีค่าคาดหวังเป็นศูนย์ — มันคือ control variate ที่หักล้าง noise ของ โดยไม่แตะค่าคาดหวัง
ผลคือ estimator ที่ unbiased เท่าเดิม แต่ variance ต่ำกว่ากันเป็นระดับ และไม่มีวันติดลบ รูปที่ 5.3 จะให้เห็นความต่างนี้กับตา
3.4 หมายเหตุขั้นสูง: การหารด้วย std ไม่ได้บริสุทธิ์อย่างที่เห็น (Dr.GRPO)
การหารด้วย ในสมการ 3.1 แอบใส่ bias เข้ามาหนึ่งอย่าง: กลุ่มที่ reward เกือบเท่ากันหมด (std เล็ก เช่น ถูก 7 ใน 8) จะถูกขยาย advantage ด้วยตัวคูณมหาศาล ขณะที่กลุ่มที่เสียงแตกจริง ๆ (std ใหญ่ — ซึ่งมี information มากที่สุด) กลับถูกกดให้เบาลงโดยเปรียบเทียบ ผลรวมคือ gradient เอนเอียงไปหาโจทย์ที่โมเดลเกือบจะเห็นพ้องกับตัวเองอยู่แล้ว งาน Dr.GRPO (Liu และคณะ, 2025) เสนอให้ตัดการหาร std ทิ้ง เหลือแค่การลบ mean ซึ่งยังเป็น baseline ที่ถูกต้องทุกประการ วิดเจ็ตในหัวข้อ 4 มีปุ่มสลับให้ลองทั้งสองแบบ
3.5 pass@k แบบ unbiased — เครื่องมือที่หัวข้อ 9 ต้องใช้
สุ่มคำตอบ ครั้งต่อโจทย์ ถูก ครั้ง แล้วอยากรู้ว่า "ถ้าให้โควตา ครั้ง จะมีสักครั้งที่ถูกไหม":
เศษส่วนข้างหลังคือความน่าจะเป็นที่หยิบ อันจาก แล้วเจอแต่คำตอบผิดล้วน ๆ สูตรที่คนมักใช้ผิดคือ ซึ่ง bias เข้าข้างตัวเองอย่างเป็นระบบเมื่อ เล็ก (นี่คือเหตุผลที่กระดาษ HumanEval ของ Chen และคณะ 2021 ต้องมี appendix แยกเรื่องนี้) จำสูตรนี้ไว้ — มันคือมาตรวัดที่ใช้ตัดสินว่า GRPO "สร้าง" ความสามารถใหม่ หรือแค่ "เหลา" ของเดิม
4. เห็นภาพสมการ (Visualize)
กลุ่มเดียวสอนอะไร — และกลุ่มแบบไหนไม่สอนอะไรเลย
Figure 5.1ซ้าย: advantage ของกลุ่มจริง 8 คำตอบภายใต้ reward shaping ของบทนี้ (ถูก +1.0, format +0.3, ภาษาไทย +0.2) — เส้นศูนย์คือ mean ของกลุ่มพอดี ขวา: กลุ่มที่ reward เท่ากันหมด ทุก advantage เป็นศูนย์ gradient เป็นศูนย์
แผงซ้ายคือสมการ 3.1 ทำงานจริง: สองคำตอบที่ทำครบทุกอย่าง (r = 1.5) ได้แรงผลักบวกแรง ๆ คำตอบที่ได้แค่ format (r = 0.3) โดนผลักลง ทั้งที่ reward เป็นบวก — เพราะเกณฑ์ไม่ใช่ "ดีไหม" แต่คือ "ดีกว่าเพื่อนร่วมกลุ่มไหม" แผงขวาคือโหมดตายเงียบของ GRPO ทั้งบท: reward เท่ากันหมด = std เป็นศูนย์ = ไม่มีการเรียนรู้
ลองป้อน reward เองแล้วดู advantage เปลี่ยนสด ๆ — และอย่าพลาดติ๊กกล่อง Divide by std ออก เพื่อดูข้อแตกต่างของ Dr.GRPO จากหัวข้อ 3.4 ด้วยตาตัวเอง:
Rewards r_i (G = 8)
Watch the std term.Dividing by std(r) = 0.500 rescales this whole group. A group that happened to be near-unanimous gets a large multiplier and dominates the update, even though it carries less information than a group that genuinely disagreed. Untick the box to see the same rewards without the rescaling.
สิ่งที่ GRPO ลบออกจาก PPO
Figure 5.2จำนวนโมเดลใน VRAM คิดจากน้ำหนัก fp16 ของ Qwen3-0.6B (1.11 GB ต่อชุด): PPO ต้องโหลด 4 ชุด GRPO เหลือ 2 ชุด — value network ถูกแทนด้วยค่าเฉลี่ยของกลุ่ม และ reward model ถูกแทนด้วยฟังก์ชัน Python
สังเกตว่าสองก้อนที่หายไปคือสองก้อนที่ต้องเทรนทั้งคู่ (value net) หรือต้องเทรนล่วงหน้า (reward model) สิ่งที่เหลือคือ policy กับ reference ซึ่งบทที่ 4 สอนเราแล้วว่า LoRA ทำให้สองตัวนี้ใช้น้ำหนักฐานร่วมกันได้ ต้นทุนโมเดลสุทธิของ GRPO ในโน้ตบุ๊กบทนี้จึงเท่ากับ SFT ธรรมดา
k3 กับ k1: unbiased เหมือนกัน ใช้งานได้ไม่เหมือนกัน
Figure 5.3ตัวอย่างสังเคราะห์ที่รู้เฉลย: π_θ = N(0,1), π_ref = N(0.5,1) ทำให้ KL จริง = 0.125 พอดี — k1 กระจายกว้างและติดลบราว 40% ของ sample ขณะที่ k3 ไม่ติดลบเลยและ std ต่ำกว่ากันเกือบสามเท่า (0.18 เทียบ 0.50)
แผงขวาคือประเด็นเชิงปฏิบัติ: ทั้งสองเส้นลู่เข้าเฉลยเดียวกัน (ทั้งคู่ unbiased) แต่ที่จำนวน sample เท่า batch จริง (หลักสิบถึงหลักร้อย) เส้น k1 ยังแกว่งแรง ส่วน k3 นิ่งพอจะใช้เป็น penalty ที่เชื่อถือได้ตั้งแต่ step แรก ๆ
5. เตรียมสภาพแวดล้อม (Environment)
เปิด Colab เลือก Runtime → Change runtime type → T4 GPU (แผนฟรีพอ)
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 # ใน GRPOConfig (ไม่ใช่ bf16=True)
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 จริง ๆ
โมเดลสองตัวในราคาหนึ่งตัว — สูตรเดิมจากบทที่ 4
policy คือ LoRA adapter จากบทที่ 2 ส่วน คือ base ตัวเดิมที่ปิด adapter
TRL รู้จักกลไกนี้เอง: ถ้า model เป็น PeftModel มันจะไม่โหลด reference แยก
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-0.6B",
torch_dtype=torch.float16,
attn_implementation="sdpa",
).cuda()
policy = PeftModel.from_pretrained(base, "kobkrit/qwen3-0.6b-th-sft-lora", is_trainable=True)
GRPO เป็น online RL: ก่อนอัปเดตน้ำหนักทุกครั้ง ต้องสุ่มคำตอบสดจากโมเดลก่อน config เต็มของบทนี้ generate สูงสุด 128 โจทย์ × 8 คำตอบ × 256 token = 262,144 token เทียบกับบทที่ 4 ที่ forward ข้อความซึ่งมีอยู่แล้วในไฟล์เฉย ๆ — คนละโลกกันเลย
เวลาบน T4 จึงหมดไปกับการ generate ไม่ใช่ backprop และนี่คือเหตุผลที่โน้ตบุ๊กตั้ง
FAST_MODE = True เป็นค่าเริ่มต้น (64 โจทย์ × 4 คำตอบ × 192 token ≈ 49k token, ~10 นาที)
ส่วน config เต็มที่ใช้รายงานผล (~18 นาที) เปิดได้ด้วยการสลับ flag เดียว —
เราบอกตรง ๆ แบบนี้เพราะบทความที่ไม่บอกว่า "ตัวเลขสวย ๆ มาจาก config ไหน" กำลังโกหกคุณครึ่งประโยค
6. เตรียมข้อมูล (Data)
เราใช้ VISAI-AI/gsm8k-thai — ชุดโจทย์คณิตศาสตร์ GSM8K ฉบับแปลไทย
สุ่มมา 128 ข้อจาก train split และแบ่ง held-out ไว้วัดผลต่างหาก ไม่แตะระหว่างเทรน
from datasets import load_dataset
ds = load_dataset("VISAI-AI/gsm8k-thai", split="train")
ds = ds.shuffle(seed=42).select(range(128))
SYSTEM = "จงคิดทีละขั้นใน <think>...</think> แล้วจบด้วยคำตอบเป็นตัวเลขบรรทัดสุดท้าย"
def to_prompt(ex):
return {
"prompt": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": ex["translated_question"]}],
"answer": extract_final_int(ex["translated_answer"]), # เฉลย GSM8K อยู่หลัง "####"
}
train_ds = ds.map(to_prompt, remove_columns=ds.column_names)
สังเกตว่าไม่มีคอลัมน์ chosen/rejected และไม่มี label มนุษย์ใด ๆ — มีแค่โจทย์กับเฉลยตัวเลข สิ่งที่แทน label คือ reward สามฟังก์ชันที่ตรวจด้วยโค้ดล้วน ๆ:
import re
THAI_DIGITS = str.maketrans("๐๑๒๓๔๕๖๗๘๙", "0123456789")
def extract_final_int(text):
text = text.translate(THAI_DIGITS) # เผื่อโมเดลตอบเป็นเลขไทย ๔๒
tail = text.split("</think>")[-1] # ตรวจเฉพาะส่วนหลังการคิด
nums = re.findall(r"-?\d[\d,]*", tail)
return int(nums[-1].replace(",", "")) if nums else None
def reward_correct(completions, answer, **kwargs): # +1.0 คำตอบสุดท้ายถูก
return [1.0 if extract_final_int(c) == a else 0.0
for c, a in zip(completions, answer)]
def reward_format(completions, **kwargs): # +0.3 มี <think> ที่ไม่ว่างเปล่า
pat = re.compile(r"<think>.+?</think>", re.DOTALL)
return [0.3 if pat.search(c) else 0.0 for c in completions]
def reward_thai(completions, **kwargs): # +0.2 คิดเป็นภาษาไทยจริง
def th_ratio(s):
letters = [ch for ch in s if ch.isalpha()]
return sum("ก" <= ch <= "๛" for ch in letters) / max(len(letters), 1)
return [0.2 if th_ratio(c) > 0.5 else 0.0 for c in completions]
reward รวมของคำตอบหนึ่งอันคือผลบวกของทั้งสาม: สูงสุด 1.5 ต่ำสุด 0.0
กลับไปดูรูปที่ 5.1 แผงขวา: ต้นทางของการเรียนรู้คือความแตกต่างภายในกลุ่ม ช่วงต้นของการเทรน โมเดล 0.6B ตอบโจทย์เลขถูกน้อยมาก — ถ้า reward มีแค่ถูก/ผิด กลุ่มส่วนใหญ่จะเป็น [0,0,0,0,0,0,0,0] คือ std ศูนย์ gradient ศูนย์ เทรนฟรีไม่ได้อะไร reward ย่อยเรื่อง format กับภาษาไทยทำให้กลุ่มยังมีความต่างให้เรียนตั้งแต่ก่อนโมเดลจะเริ่มตอบถูก นี่คือ reward shaping ในความหมายที่ตรงตัวที่สุด และหมายเหตุตัวโต ๆ: มันเปิดช่องโกงด้วย — ดูกับดักข้อ 1 ในหัวข้อ 9 ว่าโมเดลหาช่องจากข้อไหนเจอ (เจอจริง มีหลักฐานในโน้ตบุ๊ก)
7. โค้ดหลัก (Main code)
7.1 GRPOTrainer — คราวนี้ trainer เป็นพลเมืองชั้นหนึ่ง
บทที่ 3 เราต้องหรี่ตาใช้ PPOTrainer ของ TRL ที่ยังอยู่ในสถานะกึ่งทดลอง API เปลี่ยนแทบทุก minor version
GRPOTrainer คนละเรื่องเลย: มันคือ trainer ที่ TRL ดูแลเป็นตัวชูโรงหลังกระแส R1
รับ reward เป็นฟังก์ชัน Python ธรรมดาตรง ๆ ไม่ต้องห่อโมเดลอะไรทั้งนั้น
from trl import GRPOConfig, GRPOTrainer
FAST_MODE = True # ค่าเริ่มต้น: จบใน ~10 นาทีบน T4 ฟรี
# False = config เต็มที่ใช้รายงานผล (~18 นาที)
cfg = GRPOConfig(
output_dir="grpo-out",
num_generations=4 if FAST_MODE else 8, # G — ขนาดกลุ่ม
max_completion_length=192 if FAST_MODE else 256,
max_prompt_length=256,
temperature=1.0, # ห้ามลด — ความหลากหลายในกลุ่มคือเชื้อเพลิง
beta=0.04, # น้ำหนัก KL (คำนวณด้วย k3 จากหัวข้อ 3.3)
epsilon=0.2, # ช่วง clip เดียวกับ PPO
learning_rate=1e-6, # ต่ำกว่า DPO อีก — ดูคำเตือนด้านล่าง
per_device_train_batch_size=16, # ต้องหารด้วย num_generations ลงตัว
gradient_accumulation_steps=2 if FAST_MODE else 4,
num_train_epochs=1,
fp16=True, # T4 ไม่มี bf16
logging_steps=1,
)
trainer = GRPOTrainer(
model=policy, # PeftModel → ref ฟรี
reward_funcs=[reward_correct, reward_format, reward_thai],
args=cfg,
train_dataset=train_ds,
)
trainer.train()
คิดเลขงบให้ดูตรง ๆ: config เต็มคือ 128 โจทย์ × 8 คำตอบ = 1,024 completion หารด้วย effective batch 64 completion ต่อ step = 16 optimizer step — แค่นั้นจริง ๆ เวลาที่เหลือเกือบทั้งหมดคือการ generate ราว 262k token (สูงสุด) ก่อนแต่ละ step
per_device_train_batch_size นับเป็น completion ไม่ใช่ prompt และต้องหารด้วย
num_generations ลงตัว เพราะสมาชิกกลุ่มเดียวกันต้องอยู่ใน batch เดียวกันถึงจะคำนวณ
mean/std ของกลุ่มได้ ถ้าตั้งไม่ลงตัว TRL จะ error ตั้งแต่สร้าง trainer — ซึ่งดีแล้ว
พังดังดีกว่าพังเงียบ
ลำดับของทั้งซีรีส์คือ SFT 2e-4 → DPO 5e-6 → GRPO 1e-6
เหตุผล: ข้อมูลเทรนของ GRPO คือคำตอบที่โมเดลตัวปัจจุบันสุ่มออกมา
ถ้าน้ำหนักขยับแรงจนภาษาเริ่มเพี้ยน คำตอบรุ่นถัดไปจะเพี้ยนตาม แล้ว reward จะพังทั้งกระดาน —
ความผิดพลาดของ online RL ทบต้น ไม่เหมือน supervised ที่ข้อมูลเทรนไม่หนีไปไหน
7.2 คิด advantage เองด้วยมือ — เลขทั้งหมดอยู่ตรงนี้
เพื่อไม่ให้ GRPOTrainer เป็นกล่องดำ โน้ตบุ๊กมีเซลล์ที่ทำเลขของสมการ 3.1 ให้ดูตรง ๆ:
import torch
def group_advantages(rewards, G, eps=1e-4):
"""rewards: [B] เรียงเป็นกลุ่มละ G ตัวจาก prompt เดียวกัน"""
r = rewards.view(-1, G) # [B/G, G]
mean = r.mean(dim=1, keepdim=True)
std = r.std(dim=1, keepdim=True)
return ((r - mean) / (std + eps)).view(-1) # Dr.GRPO: ลบ "/ (std + eps)" ทิ้ง
r = torch.tensor([1.5, 0.3, 0.5, 1.5, 0.3, 0.0, 0.3, 0.5]) # กลุ่มจากรูปที่ 5.1
print(group_advantages(r, G=8))
# → [+1.56, -0.55, -0.20, +1.56, -0.55, -1.08, -0.55, -0.20]
แปดบรรทัดนี้คือทั้งหมดที่ GRPO เพิ่มเข้ามาจาก PPO clip เดิม ถ้าเทียบกับ value network + GAE ของบทที่ 3 ที่ต้องเทรนคู่กันไปตลอด — นี่คือการแลกที่คุ้มที่สุดในซีรีส์
8. ผลลัพธ์ (Results)
โน้ตบุ๊กวัด 4 อย่างแล้วเขียนลง results.json:
- Mean reward ต่อ step — ควรไต่ขึ้น (นี่คือสิ่งที่ optimizer เห็น)
- สัดส่วนของกลุ่มที่ std ไม่เป็นศูนย์ ต่อ step — ตัวชี้วัดที่แทบไม่มีใคร plot
- pass@1 และ pass@8 บน held-out ด้วยสูตร unbiased จากหัวข้อ 3.5 พร้อม Wilson 95% CI
- ความยาว completion เฉลี่ยต่อ step — เทียบคู่กับ accuracy
| ตัวชี้วัด (config เต็ม) | ก่อนเทรน | หลังเทรน |
|---|---|---|
| pass@1, held-out (95% CI) | ? | ? |
| pass@8, held-out (unbiased) | ? | ? |
| mean reward ต่อกลุ่ม | ? | ? |
| ความยาวคำตอบเฉลี่ย (token) | ? | ? |
| สัดส่วนกลุ่มที่ std > 0 (step แรก → step สุดท้าย) | ? | ? |
mean reward ที่นิ่ง ๆ อ่านได้สองแบบ: "โมเดลอิ่มตัวแล้ว" หรือ "การเรียนรู้หยุดไปนานแล้ว" ตัวแยกสองกรณีนี้คือสัดส่วนกลุ่มที่ std ไม่เป็นศูนย์ — ถ้ามันแตะศูนย์เมื่อไหร่ ทุก batch หลังจากนั้นคือ no-op เงียบ ๆ: loss ยังพิมพ์ออกมา step ยังเดิน GPU ยังร้อน แต่ gradient เป็นศูนย์เป๊ะทุก step (รูปที่ 5.1 แผงขวาคูณทั้ง batch) เทรนต่ออีกชั่วโมงก็ได้ผลเท่าเดิม โน้ตบุ๊ก plot เส้นนี้คู่กับ mean reward เสมอ และนี่ควรเป็นนิสัยของคุณในทุกโปรเจกต์ RLVR
โมเมนต์ mini-R1 ที่สัญญาไว้
กระดาษ DeepSeek-R1 มีกราฟที่โด่งดังมาก: ความยาวคำตอบโตขึ้นเองพร้อมกับความแม่น โดยไม่มีใครสั่งให้คิดยาว — โมเดลค้นพบเองว่าการเขียนขั้นตอนคิดละเอียดขึ้นพา reward มาให้ โน้ตบุ๊กของเรา plot คู่เดียวกันนี้ (ความยาว completion กับ accuracy ต่อ step) ที่สเกลจิ๋ว ถ้าเห็นทั้งสองเส้นขยับขึ้นด้วยกันแม้เพียงเล็กน้อย นั่นคือกลไกเดียวกับ R1 ในหลอดทดลองของคุณเอง และถ้าความยาวโตแต่ accuracy นิ่ง ให้สงสัย reward hacking ก่อนเสมอ (กับดักข้อ 1 หัวข้อ 9)
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆbase
sft
Showing the built-in sample.
9. เปรียบเทียบ (Comparison)
สี่บท สี่วิธี วัดบนงานเดียวกัน (โจทย์เลขไทย held-out ชุดเดียวกัน) — ตารางนี้คือจุดที่ซีรีส์ทั้งชุดจ่ายผลตอบแทน เพราะคอลัมน์ขวาสุดไม่เคยปรากฏในบทไหนมาก่อน:
| วิธี | pass@1 (95% CI) | ความยาวคำตอบ | เวลาเทรน | โมเดลใน VRAM | ต้นทุน label มนุษย์ |
|---|---|---|---|---|---|
| SFT (บทที่ 2) | ? | ? | ? | 1 | เฉลยที่คนเขียนต่อทุกตัวอย่าง |
| PPO (บทที่ 3) | ? | ? | ? | 4 | คู่ preference เพื่อเทรน reward model |
| DPO (บทที่ 4) | ? | ? | ~9 นาที | 2 (LoRA เหลือ 1) | ~500 คู่ preference |
| GRPO (บทนี้) | ? | ? | ~18 นาที | 2 (LoRA เหลือ 1) | ศูนย์ |
อ่านตารางนี้จากขวาไปซ้าย: เส้นทางของซีรีส์คือการทยอยลดการพึ่งพา label มนุษย์ จากเฉลยทุกตัวอย่าง → คู่ preference → ศูนย์ โดยที่เครื่องจักรข้างใต้เรียบง่ายลงเรื่อย ๆ ด้วย เงื่อนไขเดียวที่ทำให้คอลัมน์สุดท้ายเป็นศูนย์ได้คืองานต้องตรวจได้ด้วยโค้ด — จำข้อนี้ไว้ให้ขึ้นใจ
RL สร้างความสามารถใหม่ หรือเหลาของเดิม
Figure 5.4โมเดลของเล่นที่ระบุกลไกชัดเจน (RL คูณ odds ของโจทย์ที่เคยทำถูกได้ ×8 แต่แตะโจทย์ที่ p = 0 ไม่ได้เลย): pass@1 พุ่งเข้าหาเพดาน pass@8 เดิม — ภาพประกอบกลไกเท่านั้น ตัวเลขวัดจริงอยู่ในโน้ตบุ๊ก
ตรรกะเบื้องหลังภาพนี้แข็งแรงกว่าที่เห็น: GRPO เรียนจาก advantage ซึ่งไม่เป็นศูนย์ได้ ก็ต่อเมื่อมีอย่างน้อยหนึ่งคำตอบในกลุ่มที่ทำได้ดีกว่าเพื่อน — แปลว่าโจทย์ที่โมเดลฐาน สุ่มยังไงก็ไม่เคยถูกเลย (p = 0) จะไม่มีวันส่งสัญญาณเรียนรู้เข้ามาในระบบ สิ่งที่ RLVR ทำได้ดีคือย้ายความสามารถที่กระจัดกระจายอยู่ใน pass@8 มากระจุกที่ pass@1 งานวิจัยปี 2025 (Yue และคณะ) วัดพบด้วยซ้ำว่าที่ ใหญ่มาก ๆ โมเดลฐานอาจชนะ โมเดลหลัง RL นี่ไม่ได้แปลว่า GRPO ไร้ค่า — ผู้ใช้จริงได้คำตอบเดียว pass@1 คือของจริง — แต่แปลว่าอย่าอ่านกราฟ reward ที่ไต่ขึ้นแล้วสรุปว่าโมเดล "ฉลาดขึ้น" มันแค่ "นิ่งขึ้น" เป็นหลัก
กับดักที่ต้องระวัง
1. Reward hacking: ฟาร์ม +0.3 ด้วย <think> ว่างเปล่า
เวอร์ชันแรกของ reward format ในโน้ตบุ๊กใช้ regex <think>.*?</think> (จุดสำคัญ: .*? ยอมรับสตริงว่าง)
ผลคือโมเดลค้นพบภายในไม่กี่ step ว่าพิมพ์ <think></think> เปล่า ๆ แล้วเดาเลขมั่ว
เก็บ +0.3 ได้ฟรีทุกครั้ง ถูกกว่าการคิดจริงมาก — mean reward ไต่สวยแต่ accuracy ไม่ขยับ
โน้ตบุ๊กเก็บตัวอย่างที่จับได้จริงไว้ให้ดู แล้วแก้เป็น .+? บังคับให้ต้องมีเนื้อหา
บทเรียน: โมเดลไม่ได้ optimize สิ่งที่คุณตั้งใจ มัน optimize สิ่งที่คุณเขียน
2. Reward เท่ากันทั้งกลุ่ม → บทเรียนของรูปที่ 5.1 แผงขวา ยิ่งกลุ่มเล็ก โอกาสที่ทุกคำตอบได้ reward เท่ากันยิ่งสูง — ที่ เหรียญสองเหรียญออกหน้าเดียวกันบ่อยมาก กลุ่มเล็กกว่า 4 จึงเผา compute ทิ้งเป็นสัดส่วนใหญ่ และ mean ที่ประมาณจากสองตัวอย่างก็ noise สูงด้วย คือจุดสมดุลที่ใช้กันแพร่หลาย (โหมด FAST ของเรายอมลดเหลือ 4 แลกกับเวลา — และบอกไว้ตรง ๆ)
3. Temperature ต่ำเกิน = ฆ่าความหลากหลายตั้งแต่ต้นทาง
ลด temperature แล้วคำตอบทั้ง 8 อันแทบเหมือนกัน → reward เท่ากัน → กลับไปกับดักข้อ 2
ห้ามเอานิสัยตอน inference (temperature ต่ำ ๆ เอาความนิ่ง) มาใช้ตอนเก็บ rollout
เราตั้ง temperature=1.0 เพราะความหลากหลายในกลุ่มคือเชื้อเพลิงของการเรียนรู้ทั้งระบบ
4. คอขวดคือ generation ไม่ใช่ backprop — จัดงบให้ถูกก้อน
ถ้ารันแล้วช้า อย่าเพิ่งไปลด batch size หรือหรี่ optimizer — ดูงบ 262k token ในหัวข้อ 5 ก่อน
ตัวเลือกที่ได้ผลเรียงตามแรง: ลด max_completion_length, ลด num_generations, ลดจำนวนโจทย์
(ระบบ production แก้เรื่องนี้ด้วย inference engine อย่าง vLLM ซึ่ง TRL ต่อได้ แต่เกินขอบเขต Colab ฟรี)
10. สรุป (Summary)
- ค่าเฉลี่ยของกลุ่มคือ baseline ที่ไม่ต้องเทรน — สุ่ม คำตอบจาก prompt เดียวกัน แล้ว value network ของ PPO ทั้งตัวก็ไม่จำเป็นอีกต่อไป
- Reward ที่ตรวจได้ด้วยโค้ด = ศูนย์ label มนุษย์ — จุดที่ซีรีส์ไต่มาตั้งแต่บทที่ 2 มาบรรจบ
- Advantage เป็นเรื่องสัมพัทธ์ภายในกลุ่ม คำตอบ reward บวกยังโดนผลักลงได้ ถ้าเพื่อนร่วมกลุ่มทำได้ดีกว่า
- กลุ่มที่ reward เท่ากันหมดสอนอะไรไม่ได้เลย — plot สัดส่วนกลุ่มที่ std > 0 เสมอ มันคือเส้นแบ่งระหว่าง "อิ่มตัว" กับ "หยุดเรียนไปนานแล้วโดยไม่มีใครรู้"
- k3 ทำให้ KL penalty ใช้งานได้จริงที่ batch เล็ก — unbiased เท่า log-ratio แต่ไม่ติดลบและ variance ต่ำ
- หารด้วย std มี bias ซ่อนอยู่ — Dr.GRPO ตัดทิ้งเหลือแค่ลบ mean ลองเองได้ในวิดเจ็ตหัวข้อ 4
- Reward shaping จำเป็นแต่อันตราย — reward ย่อยกันกลุ่ม all-zero ช่วงแรก แต่เปิดช่องให้ฟาร์มคะแนน
- RLVR ส่วนใหญ่ "เหลา" ไม่ใช่ "สร้าง" — pass@1 ไต่เข้าหาเพดาน pass@8 เดิม วัดทั้งคู่เสมอ
GRPO ต้องการ reward ที่ตรวจได้ด้วยโค้ด โจทย์เลขตรวจได้ โค้ดตรวจได้ (รัน test) แต่ "เขียนอีเมลภาษาไทยให้สุภาพเป็นธรรมชาติ" ไม่มีฟังก์ชันตรวจ — งานปลายเปิดแบบนั้น คือดินแดนของ preference data และ DPO จากบทที่ 4 สองบทนี้จึงเสริมกัน ไม่ได้แทนกัน เลือกเครื่องมือจากรูปร่างของ reward ไม่ใช่จากความใหม่ของอัลกอริทึม
อย่าตีความผลว่าโมเดล "ฉลาดขึ้น" — หลักฐานทั้งของเราและของงานวิจัยที่สเกลจริงชี้ว่า RLVR ส่วนใหญ่จัดระเบียบความน่าจะเป็นของความสามารถที่มีอยู่แล้วที่ pass@8 ให้มาโผล่ที่ pass@1 อย่างเสถียร ถ้าอยากได้ความรู้ใหม่จริง ๆ ต้องย้อนกลับไปบทที่ 1 (CPT)
และเช่นเดิม: 128 โจทย์ 16 optimizer step คือการสาธิตกลไก ไม่ใช่การเทรนจริง DeepSeek-R1 ใช้โจทย์ระดับแสนข้อและ compute ต่างจากเราหลาย order of magnitude สิ่งที่โอนไปสเกลจริงได้คือความเข้าใจ: กลุ่มคือ baseline, ความหลากหลายคือเชื้อเพลิง, และตัวตรวจ reward คือสิ่งที่โมเดลจะหาช่องโหว่ให้คุณเจอเสมอ
บทต่อไป: Context Distillation — ระบบ prompt ยาวเหยียดที่ต้องจ่ายทุกครั้งที่เรียกโมเดล เอามากลั่นใส่น้ำหนักให้โมเดลประพฤติตัวตามนั้นโดยไม่ต้องเห็น prompt อีกเลยได้อย่างไร
อ้างอิง (References)
- Shao et al. (2024). DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models — DeepSeekMath: ต้นกำเนิดของ GRPO
- DeepSeek-AI et al. (2025). DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning — R1: RL บน verifiable reward ในระดับสเกลจริง
- Liu et al. (2025). Understanding R1-Zero-Like Training: A Critical Perspective — Dr.GRPO: อคติจากการหารด้วย std ที่หัวข้อ 3 พูดถึง
- Ahmadian et al. (2024). Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs — REINFORCE ธรรมดาก็อาจพอ -- อ่านคู่กับการตัด value network
- Schulman et al. (2017). Proximal Policy Optimization Algorithms — PPO ต้นฉบับ: สมการ clipped surrogate ในหัวข้อ 3
- Chen et al. (2021). Evaluating Large Language Models Trained on Code — นิยาม pass@k แบบ unbiased ที่ใช้ในหัวข้อ 9
บทความ โค้ด และโน้ตบุ๊กในซีรีส์นี้เผยแพร่ภายใต้สัญญาอนุญาต CC BY-NC-SA 4.0 — นำไปใช้และดัดแปลงต่อได้ โดยอ้างอิงที่มา ไม่ใช้เพื่อการค้า และเผยแพร่ต่อด้วยสัญญาเดียวกัน (โมเดลและชุดข้อมูลของบุคคลที่สามที่อ้างถึง ยังคงใช้สัญญาของเจ้าของเดิม)
