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

[LLM 5/10] GRPO: ลบ value network ทิ้ง แล้วให้กลุ่มคำตอบเป็น baseline ของกันเอง

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

บทที่แล้วเราปิดท้ายด้วยช่องว่างของ 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 model4
DPO (บทที่ 4)ไม่ได้ — offline ล้วนคู่ preference2 (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 ตอบด้วยการสุ่มให้เห็นกับตา:

แนวคิดหลักของบทนี้

สุ่มคำตอบ GG อันจาก 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 — หัวใจทั้งหมดอยู่บรรทัดเดียว

A^i=rimean(r1,,rG)std(r1,,rG)\hat A_i = \frac{r_i - \operatorname{mean}(r_1,\dots,r_G)}{\operatorname{std}(r_1,\dots,r_G)}
  • GG = จำนวนคำตอบที่สุ่มจาก prompt เดียวกัน (ในบทนี้คือ 8)
  • rir_i = reward ของคำตอบที่ ii
  • ทุก token ของคำตอบที่ ii ใช้ A^i\hat A_i ตัวเดียวกันทั้งประโยค — ต่างจาก PPO ที่พยายามให้ advantage ละเอียดราย token ผ่าน value network และ GAE

อ่านเป็นภาษาคน: "คำตอบนี้ดีกว่าหรือแย่กว่าความพยายามครั้งอื่น ๆ ของฉันเอง ต่อโจทย์ข้อเดียวกัน" ไม่มีการเปรียบเทียบข้ามโจทย์ ไม่มีการทำนายอนาคต มีแค่การแข่งกันเองในกลุ่ม

สมการสั้น ๆ นี้มีผลตามมาที่สำคัญมาก: ถ้าทั้งกลุ่มได้ reward เท่ากันหมด (ถูกหมดหรือผิดหมด) ทุก A^i\hat A_i เป็นศูนย์ และ batch นั้นไม่สอนอะไรเลย จำประโยคนี้ไว้ มันจะกลายเป็นทั้งกับดักอันดับหนึ่งและตัวชี้วัดที่สำคัญที่สุดของบท

3.2 GRPO objective ฉบับเต็ม

JGRPO(θ)=E[1Gi=1G1oit=1oi{min ⁣(ρi,tA^i, clip(ρi,t,1ϵ,1+ϵ)A^i)βDKL[πθπref]}]\mathcal{J}_{\text{GRPO}}(\theta) = \mathbb{E}\left[\frac{1}{G}\sum_{i=1}^{G}\frac{1}{|o_i|}\sum_{t=1}^{|o_i|}\Big\{\min\!\big(\rho_{i,t}\,\hat A_i,\ \operatorname{clip}(\rho_{i,t},\,1-\epsilon,\,1+\epsilon)\,\hat A_i\big) - \beta\,\mathbb{D}_{\text{KL}}\big[\pi_\theta \,\|\, \pi_{\text{ref}}\big]\Big\}\right]

โดย ρi,t=πθ(oi,tq,oi,<t)πθold(oi,tq,oi,<t)\rho_{i,t} = \dfrac{\pi_\theta(o_{i,t} \mid q, o_{i,<t})}{\pi_{\theta_{\text{old}}}(o_{i,t} \mid q, o_{i,<t})} คืออัตราส่วนความน่าจะเป็นของ token เทียบกับ policy ตอนสุ่ม

อ่านทีละชิ้น เพราะทุกชิ้นเคยผ่านตามาแล้วในซีรีส์นี้:

  • min(, clip())\min(\cdot,\ \operatorname{clip}(\cdot)) = PPO clip เดิมจากบทที่ 3 ไม่มีอะไรใหม่ — กันไม่ให้ก้าวไกลเกินไปจากจุดที่สุ่ม rollout มา
  • 1oi\frac{1}{|o_i|} = เฉลี่ยต่อ token กันคำตอบยาวได้อิทธิพลเกินตัว (นึกถึง length bias จากบทที่ 4)
  • βDKL\beta\,\mathbb{D}_{\text{KL}} = สายจูงเส้นเดิมที่ผูกกับ πref\pi_{\text{ref}} ตัวเดียวกับบทที่ 3 และ 4

สิ่งที่ควรอ่านคือสิ่งที่ไม่อยู่ในสมการ: ไม่มี V(s)V(s) ไม่มี GAE ไม่มี critic loss ทั้งบรรทัดใช้แค่โมเดลสองตัว (πθ\pi_\theta กับ πref\pi_{\text{ref}}) และเลข reward จากตัวตรวจ

3.3 พจน์ KL ไม่ได้คำนวณตรง ๆ — รู้จัก k3 estimator

KL divergence จริง ๆ ต้อง sum ทั้ง vocabulary ของทุกตำแหน่ง ซึ่งแพงและไม่จำเป็น GRPO ประมาณมันจาก token ที่สุ่มออกมาแล้ว ด้วย estimator ชื่อเล่นว่า k3:

D^k3=πref(oi,t)πθ(oi,t)logπref(oi,t)πθ(oi,t)1\hat{\mathbb{D}}_{k3} = \frac{\pi_{\text{ref}}(o_{i,t})}{\pi_\theta(o_{i,t})} - \log\frac{\pi_{\text{ref}}(o_{i,t})}{\pi_\theta(o_{i,t})} - 1

คำถามที่นักเรียนถามเสมอ (และควรถาม): ทำไมไม่ใช้ log(πθ/πref)\log(\pi_\theta/\pi_{\text{ref}}) ตรง ๆ ในเมื่อค่าคาดหวังของมันก็คือ KL อยู่แล้ว?

คำตอบ: ตัว naive (เรียกว่า k1) unbiased ก็จริง แต่ราย sample มันติดลบได้ — ประมาณ 40% ของ sample ให้ค่าติดลบ ทั้งที่ KL เป็นลบไม่ได้โดยนิยาม — และ variance สูงมาก ที่ batch ขนาดจริง ค่าประมาณจะแกว่งจน penalty เดี๋ยวผลักเดี๋ยวดึง

k3 แก้ทั้งสองข้อพร้อมกัน ให้ x=πref/πθx = \pi_{\text{ref}}/\pi_\theta แล้วสังเกตสองข้อเท็จจริง:

  1. อสมการ x1logxx - 1 \geq \log x เป็นจริงเสมอ ดังนั้น k3 =(x1)logx0= (x-1) - \log x \geq 0 ทุก sample
  2. Eπθ[x]=πθπrefπθ=1\mathbb{E}_{\pi_\theta}[x] = \sum \pi_\theta \cdot \frac{\pi_{\text{ref}}}{\pi_\theta} = 1 ดังนั้นพจน์ (x1)(x-1) มีค่าคาดหวังเป็นศูนย์ — มันคือ control variate ที่หักล้าง noise ของ logx-\log x โดยไม่แตะค่าคาดหวัง

ผลคือ estimator ที่ unbiased เท่าเดิม แต่ variance ต่ำกว่ากันเป็นระดับ และไม่มีวันติดลบ รูปที่ 5.3 จะให้เห็นความต่างนี้กับตา

3.4 หมายเหตุขั้นสูง: การหารด้วย std ไม่ได้บริสุทธิ์อย่างที่เห็น (Dr.GRPO)

การหารด้วย std(r1..rG)\operatorname{std}(r_1..r_G) ในสมการ 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 ต้องใช้

สุ่มคำตอบ nn ครั้งต่อโจทย์ ถูก cc ครั้ง แล้วอยากรู้ว่า "ถ้าให้โควตา kk ครั้ง จะมีสักครั้งที่ถูกไหม":

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

เศษส่วนข้างหลังคือความน่าจะเป็นที่หยิบ kk อันจาก nn แล้วเจอแต่คำตอบผิดล้วน ๆ สูตรที่คนมักใช้ผิดคือ 1(1c/n)k1-(1-c/n)^k ซึ่ง bias เข้าข้างตัวเองอย่างเป็นระบบเมื่อ nn เล็ก (นี่คือเหตุผลที่กระดาษ HumanEval ของ Chen และคณะ 2021 ต้องมี appendix แยกเรื่องนี้) จำสูตรนี้ไว้ — มันคือมาตรวัดที่ใช้ตัดสินว่า GRPO "สร้าง" ความสามารถใหม่ หรือแค่ "เหลา" ของเดิม

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

กลุ่มเดียวสอนอะไร — และกลุ่มแบบไหนไม่สอนอะไรเลย

แผนภูมิแท่งสองแผง แผงซ้ายแสดง advantage บวกและลบของคำตอบ 8 อันรอบค่าเฉลี่ยของกลุ่ม แผงขวาแสดงกลุ่มเสื่อมที่ทุกคำตอบได้ reward เท่ากันทำให้ advantage เป็นศูนย์ทั้งหมดแผนภูมิแท่งสองแผง แผงซ้ายแสดง advantage บวกและลบของคำตอบ 8 อันรอบค่าเฉลี่ยของกลุ่ม แผงขวาแสดงกลุ่มเสื่อมที่ทุกคำตอบได้ reward เท่ากันทำให้ advantage เป็นศูนย์ทั้งหมด

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 ด้วยตาตัวเอง:

Try a group:

Rewards r_i (G = 8)

Unchecked is the Dr.GRPO variant: it keeps the centring but drops the std, removing the bias toward low-variance groups.
r1 = 1.001.00r2 = 0.00-1.00r3 = 1.001.00r4 = 1.001.00r5 = 0.00-1.00r6 = 0.00-1.00r7 = 1.001.00r8 = 0.00-1.00Â = 0 (no update)
mean(r)0.5000
std(r)0.5000
max |Â|1.000
Formula(r − μ) / σGRPO

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

แผนภูมิแท่งแนวนอนเปรียบเทียบหน่วยความจำของ PPO ที่มีโมเดล 4 ตัวกับ GRPO ที่เหลือ 2 ตัว โดย value network ถูกขีดฆ่าและ reward model กลายเป็นฟังก์ชันไพทอนที่ใช้หน่วยความจำเป็นศูนย์แผนภูมิแท่งแนวนอนเปรียบเทียบหน่วยความจำของ PPO ที่มีโมเดล 4 ตัวกับ GRPO ที่เหลือ 2 ตัว โดย value network ถูกขีดฆ่าและ reward model กลายเป็นฟังก์ชันไพทอนที่ใช้หน่วยความจำเป็นศูนย์

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 เหมือนกัน ใช้งานได้ไม่เหมือนกัน

ฮิสโตแกรมเปรียบเทียบการกระจายของตัวประมาณ KL แบบ k1 กับ k3 และกราฟ running mean ที่แสดงว่าทั้งคู่ลู่เข้าค่า KL จริงแต่ k3 นิ่งกว่ามากฮิสโตแกรมเปรียบเทียบการกระจายของตัวประมาณ KL แบบ k1 กับ k3 และกราฟ running mean ที่แสดงว่าทั้งคู่ลู่เข้าค่า KL จริงแต่ k3 นิ่งกว่ามาก

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

torch รุ่นใหม่ตอบ True บน T4 เพราะนับ การจำลอง (emulation) ว่ารองรับด้วย ซึ่งช้ากว่า fp16 มาก ให้เช็ค compute capability ≥ 8.0 (Ampere ขึ้นไป) แทน — นี่คือบั๊กจริงที่เจอตอนรันโน้ตบุ๊กบน Colab จริง ๆ

โมเดลสองตัวในราคาหนึ่งตัว — สูตรเดิมจากบทที่ 4

policy คือ LoRA adapter จากบทที่ 2 ส่วน πref\pi_{\text{ref}} คือ 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)
งบจริงของบทนี้คือ token ที่ generate ไม่ใช่ step ที่เทรน

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

ทำไมต้องมี reward ย่อย ไม่ใช่แค่ "ถูก/ผิด"

กลับไปดูรูปที่ 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 — ซึ่งดีแล้ว พังดังดีกว่าพังเงียบ

learning rate ของ RL online ต้องต่ำที่สุดในซีรีส์

ลำดับของทั้งซีรีส์คือ 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:

  1. Mean reward ต่อ step — ควรไต่ขึ้น (นี่คือสิ่งที่ optimizer เห็น)
  2. สัดส่วนของกลุ่มที่ std ไม่เป็นศูนย์ ต่อ step — ตัวชี้วัดที่แทบไม่มีใคร plot
  3. pass@1 และ pass@8 บน held-out ด้วยสูตร unbiased จากหัวข้อ 3.5 พร้อม Wilson 95% CI
  4. ความยาว completion เฉลี่ยต่อ step — เทียบคู่กับ accuracy
ตัวชี้วัด (config เต็ม)ก่อนเทรนหลังเทรน
pass@1, held-out (95% CI)??
pass@8, held-out (unbiased)??
mean reward ต่อกลุ่ม??
ความยาวคำตอบเฉลี่ย (token)??
สัดส่วนกลุ่มที่ std > 0 (step แรก → step สุดท้าย)??
ตัวชี้วัดข้อ 2 คือสัญญาณชีพของ GRPO

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
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.

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 สร้างความสามารถใหม่ หรือเหลาของเดิม

กราฟสองแผง แผงซ้ายแสดงฟังก์ชันการเหลาความน่าจะเป็นที่จุดศูนย์ไม่ขยับ แผงขวาแสดงแท่ง pass@1 ที่เพิ่มขึ้นมากเข้าใกล้เส้นเพดาน pass@8 เดิม ขณะที่ pass@8 เพิ่มขึ้นน้อยกว่ากราฟสองแผง แผงซ้ายแสดงฟังก์ชันการเหลาความน่าจะเป็นที่จุดศูนย์ไม่ขยับ แผงขวาแสดงแท่ง pass@1 ที่เพิ่มขึ้นมากเข้าใกล้เส้นเพดาน pass@8 เดิม ขณะที่ pass@8 เพิ่มขึ้นน้อยกว่า

Figure 5.4โมเดลของเล่นที่ระบุกลไกชัดเจน (RL คูณ odds ของโจทย์ที่เคยทำถูกได้ ×8 แต่แตะโจทย์ที่ p = 0 ไม่ได้เลย): pass@1 พุ่งเข้าหาเพดาน pass@8 เดิม — ภาพประกอบกลไกเท่านั้น ตัวเลขวัดจริงอยู่ในโน้ตบุ๊ก

ตรรกะเบื้องหลังภาพนี้แข็งแรงกว่าที่เห็น: GRPO เรียนจาก advantage ซึ่งไม่เป็นศูนย์ได้ ก็ต่อเมื่อมีอย่างน้อยหนึ่งคำตอบในกลุ่มที่ทำได้ดีกว่าเพื่อน — แปลว่าโจทย์ที่โมเดลฐาน สุ่มยังไงก็ไม่เคยถูกเลย (p = 0) จะไม่มีวันส่งสัญญาณเรียนรู้เข้ามาในระบบ สิ่งที่ RLVR ทำได้ดีคือย้ายความสามารถที่กระจัดกระจายอยู่ใน pass@8 มากระจุกที่ pass@1 งานวิจัยปี 2025 (Yue และคณะ) วัดพบด้วยซ้ำว่าที่ kk ใหญ่มาก ๆ โมเดลฐานอาจชนะ โมเดลหลัง 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 เท่ากันยิ่งสูง — ที่ G=2G = 2 เหรียญสองเหรียญออกหน้าเดียวกันบ่อยมาก กลุ่มเล็กกว่า 4 จึงเผา compute ทิ้งเป็นสัดส่วนใหญ่ และ mean ที่ประมาณจากสองตัวอย่างก็ noise สูงด้วย G=8G = 8 คือจุดสมดุลที่ใช้กันแพร่หลาย (โหมด 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 ที่ไม่ต้องเทรน — สุ่ม GG คำตอบจาก 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)

  1. Shao et al. (2024). DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models — DeepSeekMath: ต้นกำเนิดของ GRPO
  2. DeepSeek-AI et al. (2025). DeepSeek-R1: Incentivizing Reasoning Capability in LLMs via Reinforcement Learning — R1: RL บน verifiable reward ในระดับสเกลจริง
  3. Liu et al. (2025). Understanding R1-Zero-Like Training: A Critical Perspective — Dr.GRPO: อคติจากการหารด้วย std ที่หัวข้อ 3 พูดถึง
  4. Ahmadian et al. (2024). Back to Basics: Revisiting REINFORCE Style Optimization for Learning from Human Feedback in LLMs — REINFORCE ธรรมดาก็อาจพอ -- อ่านคู่กับการตัด value network
  5. Schulman et al. (2017). Proximal Policy Optimization Algorithms — PPO ต้นฉบับ: สมการ clipped surrogate ในหัวข้อ 3
  6. Chen et al. (2021). Evaluating Large Language Models Trained on Code — นิยาม pass@k แบบ unbiased ที่ใช้ในหัวข้อ 9

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