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

[LLM 3/10] RLHF และ PPO: เทรนโมเดลด้วยรางวัลที่หาอนุพันธ์ไม่ได้

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

บทที่ 2 เราสอนโมเดลด้วยการ "เลียนแบบเฉลย" ทีละ token แต่คุณสมบัติที่ทำให้ผู้ช่วย AI ใช้งานได้จริง — ตอบถูก สุภาพ ไม่มั่ว ไม่หลุดเป็นภาษาอังกฤษ — ไม่มีเฉลยให้เลียนแบบ และเขียนเป็น loss function ตรง ๆ ไม่ได้ บทนี้คือคำตอบแบบดั้งเดิมที่สุดของปัญหานั้น: RLHF (Reinforcement Learning from Human Feedback) ด้วย PPO เราจะเทรน reward model จริงจากคู่ preference ภาษาไทย แล้วเขียนลูป PPO เองจากศูนย์ราว 120 บรรทัด และปิดท้ายด้วยการทดลองที่ผมชอบที่สุดในซีรีส์: ถอดสายจูง KL ออก แล้วดูโมเดลโกงรางวัลกันสด ๆ นี่คือบทที่หนักที่สุดของซีรีส์โดยตั้งใจ เพราะบทที่ 4 (DPO) และบทที่ 5 (GRPO) ต่างก็เริ่มจากสมการของบทนี้ แล้วเลือก "ลบ" ชิ้นส่วนออกคนละชิ้น

Open in Colab03_rlhf_ppo.ipynb

1. ปัญหา (Problem statement)

SFT ในบทที่ 2 มีสมมติฐานซ่อนอยู่หนึ่งข้อ: ต้องมีเฉลยให้เลียนแบบ แต่ลองนึกถึงสิ่งที่เราอยากได้จริง ๆ เช่น "ตอบโจทย์เลขให้ถูก และอธิบายเป็นภาษาไทยที่อ่านรู้เรื่อง" ประโยคนี้ไม่มีเฉลยเดียว คำตอบที่ดีมีได้ร้อยแบบ และคำว่า "อ่านรู้เรื่อง" ก็เขียนเป็นสมการไม่ออก

พอพยายามจะ optimize สิ่งเหล่านี้ตรง ๆ เราจะชนกำแพงสองชั้นเสมอ:

กำแพงที่หนึ่ง — คุณภาพเขียนเป็น loss ไม่ได้ "ดีกว่า" นิยามเป็นฟังก์ชันไม่ได้ แต่มนุษย์เปรียบเทียบได้เก่งมาก ให้ดูคำตอบสองอันแล้วชี้ว่าชอบอันไหน ทำได้ทันทีและตรงกันพอสมควร ข้อมูลที่เก็บได้จริงจึงเป็นสามสิ่ง: prompt xx, คำตอบที่ถูกเลือก ywy_w, คำตอบที่ถูกปัด yly_l

กำแพงที่สอง — ต่อให้มีคะแนน ก็ backprop ไม่ได้ สมมติมีฟังก์ชันวิเศษ r(x,y)r(x,y) ให้คะแนนทุกคำตอบ คุณก็ยังเทรนแบบ supervised ไม่ได้อยู่ดี เพราะคำตอบ yy เกิดจากการสุ่ม token ทีละตัว คะแนนมาถึงหลังการสุ่มจบแล้ว และอนุพันธ์เดินทางย้อนผ่านการสุ่มไม่ได้ — เส้นทางจาก rr กลับไปหา weights θ\theta ขาดตอนตรงนั้นพอดี

ทางที่อยากเดินติดกำแพงอะไร
เขียน loss ของ "คำตอบที่ดี" ตรง ๆนิยาม "ดี" เป็นสมการไม่ได้ มีแต่การเปรียบเทียบ
ให้คนให้คะแนน แล้ว backpropคะแนนอยู่หลังการสุ่ม token — gradient เดินผ่านการสุ่มไม่ได้
ให้คนนั่งให้คะแนนสดระหว่างเทรนมนุษย์ให้คะแนนไม่ทันแม้แต่เสี้ยวเดียวของ rollout

นี่คือที่มาของชื่อบท: เรากำลังจะ optimize รางวัลที่หาอนุพันธ์ไม่ได้ เครื่องมือที่ทำแบบนั้นได้ชื่อว่า reinforcement learning

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

RLHF แก้กำแพงทั้งสองชั้นด้วยการเดินสองขั้น:

  • Stage A — Reward Model: เทรนโมเดล rϕ(x,y)r_\phi(x,y) ให้เลียนแบบการเปรียบเทียบของมนุษย์จากคู่ preference (แก้กำแพงที่หนึ่ง และแทนมนุษย์ที่ให้คะแนนไม่ทัน)
  • Stage B — PPO: ใช้ policy-gradient RL ดัน policy ไปหาคะแนนของ rϕr_\phi โดยไม่ต้องหาอนุพันธ์ผ่านการสุ่ม (แก้กำแพงที่สอง) พร้อมสายจูง KL รั้งไม่ให้วิ่งหนีโมเดลตั้งต้น

ราคาที่จ่ายคือความซับซ้อน: ระหว่างเทรนจะมีโมเดลสี่ตัวอยู่ใน VRAM พร้อมกัน — policy πθ\pi_\theta (ตัวที่เทรน), reference πref\pi_{\text{ref}} (ตัวตั้งต้นที่แช่แข็ง), reward model rϕr_\phi และ value network VψV_\psi ที่ยังไม่ได้แนะนำตัว (รอหัวข้อ 3.4)

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

RLHF คือการ optimize รางวัลที่คุณหาอนุพันธ์ไม่ได้ ผ่านตัวแทนที่ไม่สมบูรณ์ (reward model) และการ optimize ตัวแทนแรง ๆ โดยไม่มีอะไรรั้ง จะพังตาม Goodhart's law เสมอ: เมื่อตัวชี้วัดกลายเป็นเป้าหมาย มันจะเลิกเป็นตัวชี้วัดที่ดี

พจน์ KL ในสมการ 3.2 จึงไม่ใช่ regularizer ที่ใส่ไว้กันเหนียว — มันคือสิ่งเดียวที่ขวางอยู่ระหว่างคุณกับ reward hacking หัวข้อ 8 จะพิสูจน์ประโยคนี้ด้วยการถอดมันออกให้ดูกับตา

และอีกประโยคที่อยากให้ถือไว้ทั้งซีรีส์: สมการ objective ของบทนี้คือสมการแม่ของครึ่งหลังของซีรีส์ บทที่ 4 (DPO) แก้สมการนี้ในรูปปิดจน reward model และ RL loop ตัดกันหายไป บทที่ 5 (GRPO) เปลี่ยนวิธีประมาณ advantage จน value network หายไป เข้าใจบทนี้บทเดียว อีกสองบทจะกลายเป็น "การลบชิ้นส่วน" ที่อ่านออกทันที

3. สมการ (Equation)

3.1 Reward model: Bradley–Terry

Stage A เทรน rϕr_\phi ด้วย loss เดียวสั้น ๆ:

LRM(ϕ)=E(x,yw,yl)D[logσ(rϕ(x,yw)rϕ(x,yl))]\mathcal{L}_{\text{RM}}(\phi) = -\mathbb{E}_{(x,y_w,y_l)\sim\mathcal{D}}\Big[\log\sigma\big(r_\phi(x,y_w) - r_\phi(x,y_l)\big)\Big]
  • rϕ(x,y)r_\phi(x,y) = คะแนนสเกลาร์หนึ่งตัวต่อหนึ่งข้อความ — ในทางปฏิบัติคือโมเดลภาษาที่เปลี่ยนหัวเป็น linear ชั้นเดียว (num_labels=1)
  • σ\sigma = sigmoid แปลงผลต่างคะแนนเป็นความน่าจะเป็นที่มนุษย์จะเลือก ywy_w (โมเดล Bradley–Terry)
  • ยิ่งผลต่าง rϕ(x,yw)rϕ(x,yl)r_\phi(x,y_w) - r_\phi(x,y_l) กว้าง loss ยิ่งต่ำ

จุดที่คนมองข้ามแล้วไปเจ็บตัวทีหลัง: loss นี้เห็นแค่ผลต่างของคะแนน ลองแทน rϕr_\phi ด้วย rϕ+cr_\phi + c ด้วยค่าคงที่ cc ใดก็ได้ — loss ไม่เปลี่ยนเลย แปลว่าสเกลสัมบูรณ์ของ reward model ไม่มีความหมายและไม่ถูกกำหนดโดยการเทรน รันสองครั้งอาจได้คะแนนเฉลี่ย 3.7 กับ −12.4 ที่จัดอันดับเหมือนกันเป๊ะ นี่คือเหตุผลที่ต้อง standardize reward ก่อนป้อนเข้า PPO เสมอ (ลบ mean หารด้วย std) — จำจุดนี้ไว้ มันจะกลับมาในหัวข้อ 7 และ 9

3.2 สมการแม่: RLHF objective

ถ้าทั้งซีรีส์นี้จะท่องจำได้สมการเดียว จงจำสมการนี้:

maxθ ExD,yπθ(x)[rϕ(x,y)]    βDKL(πθ(x)πref(x))\max_\theta\ \mathbb{E}_{x\sim\mathcal{D},\,y\sim\pi_\theta(\cdot|x)}\big[r_\phi(x,y)\big] \;-\; \beta\,\mathbb{D}_{\text{KL}}\big(\pi_\theta(\cdot|x)\,\|\,\pi_{\text{ref}}(\cdot|x)\big)

อ่านเป็นภาษาคน: "เก็บคะแนน reward ให้มากที่สุด แต่ทุกก้าวที่เดินห่างจากโมเดลตั้งต้น ต้องจ่ายค่าปรับ"

  • πθ\pi_\theta = policy คือโมเดลที่กำลังเทรน — สังเกตว่า yy ถูกสุ่มจาก πθ\pi_\theta เอง นี่คือความต่างเชิงโครงสร้างจาก SFT ที่เรียนจากข้อมูลนิ่ง ๆ ในไฟล์
  • πref\pi_{\text{ref}} = reference คือโมเดลตั้งต้น (โมเดลหลัง SFT จากบทที่ 2) แช่แข็งตลอดการเทรน
  • β\beta = ราคาต่อหนึ่ง nat ของการเดินห่าง — ความตึงของสายจูง
  • DKL\mathbb{D}_{\text{KL}} = ระยะห่างเชิงการกระจายระหว่าง policy กับ reference
ทำไมสมการนี้คือสมการแม่ของครึ่งหลังของซีรีส์

บทที่ 4 (DPO) จะพิสูจน์ว่าสมการนี้มีคำตอบในรูปปิด แล้วพลิกกลับด้านจน rϕr_\phi และ RL loop หายไปทั้งคู่ บทที่ 5 (GRPO) จะเก็บโครง RL ไว้ แต่เปลี่ยนวิธีคำนวณ advantage จน VψV_\psi หายไป ทั้งสองบทไม่ได้เสนอ objective ใหม่ — พวกมันแก้สมการเดียวกันนี้ด้วยเครื่องมือที่ต่างกัน

3.3 PPO clipped surrogate: เครื่องยนต์ของ Stage B

policy gradient แบบดิบ (REINFORCE) ใช้ rollout หนึ่งชุดอัปเดตได้ครั้งเดียวแล้วต้องทิ้ง ซึ่งแพงมากเพราะการ generate คือคอขวด PPO อยากรีดค่า rollout ชุดเดิมหลาย epoch จึงต้องมีตัวคูณแก้ทาง (importance sampling ratio):

ρt=πθ(atst)πθold(atst)\rho_t = \frac{\pi_\theta(a_t \mid s_t)}{\pi_{\theta_{\text{old}}}(a_t \mid s_t)}
  • sts_t = สถานะ ณ ตำแหน่ง tt คือ prompt บวก token ที่สุ่มมาแล้วทั้งหมด
  • ata_t = "การกระทำ" คือ token ตัวถัดไปที่ถูกสุ่มไปแล้วตอน rollout
  • πθold\pi_{\theta_{\text{old}}} = snapshot ของ policy ณ ตอน rollout — ค่านี้ถูกคำนวณครั้งเดียวแล้วแช่แข็ง

แล้วหนีบ ρt\rho_t ไว้ด้วย clip:

LCLIP(θ)=Et[min(ρtA^t, clip(ρt,1ϵ,1+ϵ)A^t)]\mathcal{L}^{\text{CLIP}}(\theta) = \mathbb{E}_t\Big[\min\big(\rho_t\,\hat A_t,\ \text{clip}(\rho_t,\,1-\epsilon,\,1+\epsilon)\,\hat A_t\big)\Big]
  • A^t\hat A_t = advantage คือ "token นี้ดีกว่าที่คาดไว้เท่าไหร่" (นิยามในข้อถัดไป)
  • ϵ\epsilon = ความกว้างของ trust region (ค่ามาตรฐาน 0.2)

หัวใจอยู่ที่ min + clip ทำงานร่วมกันแบบมองโลกแง่ร้ายอย่างจงใจ: ถ้า A^t\hat A_t เป็นบวก (token ดี) ผลตอบแทนจากการดัน ρt\rho_t ถูกตัดเพดานที่ 1+ϵ1+\epsilon — ดันเกินนั้นไม่ได้อะไรเพิ่ม gradient เป็นศูนย์ แต่ถ้า A^t\hat A_t เป็นลบ (token แย่) ตัว min จะเลือกฝั่งที่แย่กว่าเสมอ — ค่าปรับไม่มีเพดาน สรุปหนึ่งประโยค: ได้จำกัด เสียไม่จำกัด นโยบายจึงขยับทีละก้าวเล็ก ๆ ใกล้ ๆ ตัวเดิม

อย่าสับสน: มี "โมเดลเก่า" สองตัว และมันคนละตัวกัน

πref\pi_{\text{ref}} ในสมการ 3.2 แช่แข็งตลอดการเทรน ทำหน้าที่สายจูง KL πθold\pi_{\theta_{\text{old}}} ในสมการ 3.3 คือ snapshot ณ ตอน rollout ล่าสุด เปลี่ยนทุกรอบ ทำหน้าที่ trust region บั๊กยอดฮิตอันดับหนึ่งของคนเขียน PPO เองคือจับสองตัวนี้ใส่ตัวแปรเดียวกัน

3.4 GAE: คำนวณ advantage อย่างไรไม่ให้จมน้ำเสียง noise

advantage สร้างจาก TD error ของ value network VψV_\psi:

δt=rt+γVψ(st+1)Vψ(st)\delta_t = r_t + \gamma V_\psi(s_{t+1}) - V_\psi(s_t) A^t=l=0(γλ)lδt+l\hat A_t = \sum_{l=0}^{\infty} (\gamma\lambda)^l\,\delta_{t+l}
  • Vψ(st)V_\psi(s_t) = value network ทำนายว่า "จากจุดนี้ไปจนจบ จะเก็บ reward ได้อีกเท่าไหร่" — นี่คือโมเดลตัวที่สี่
  • rtr_t = reward ต่อ token (ในงานของเรา: ค่าปรับ KL ทุกตำแหน่ง บวกคะแนนงานที่ token สุดท้าย)
  • γ\gamma = discount factor (งาน LLM มักใช้ 1.0)
  • λ\lambda = ปุ่มหมุน bias–variance: λ=0\lambda = 0 เชื่อ VψV_\psi สุดใจ (bias สูงถ้า VψV_\psi ทำนายพลาด), λ=1\lambda = 1 ไม่เชื่อเลยและรอดูผลจริงจนจบ (variance สูงเพราะแบก noise ทั้งสาย), ค่าที่นิยมใช้คือ 0.95
จำ V_ψ ตัวนี้ไว้ให้ดี — มันคือตัวที่ GRPO จะฆ่าทิ้ง

VψV_\psi เป็นโมเดลขนาดเท่า ๆ policy ที่ต้องเทรนไปพร้อมกันด้วย loss ของมันเอง ถ้า VψV_\psi ทำนายมั่ว advantage ก็มั่ว แล้ว policy ก็เรียนจากสัญญาณมั่ว — จุดพังคลาสสิกของ PPO บทที่ 5 จะตอบคำถามว่า "ถ้าแทน VψV_\psi ด้วยค่าเฉลี่ยของกลุ่มคำตอบที่สุ่มจาก prompt เดียวกันล่ะ?" นั่นแหละคือ GRPO ทั้งอัลกอริทึม — ลบโมเดลตัวที่สี่ทิ้งด้วยค่าเฉลี่ยตัวเดียว

3.5 Loss เต็มของ PPO: สามพจน์ สองโมเดล

รวมทุกชิ้นเป็น loss เดียวที่ optimizer เห็นจริง (เขียนในรูป minimize):

LPPO=LCLIP  +  c1Et[(Vψ(st)R^t)2]    c2Et[H[πθ(st)]]\mathcal{L}_{\text{PPO}} = -\mathcal{L}^{\text{CLIP}} \;+\; c_1\,\mathbb{E}_t\Big[\big(V_\psi(s_t) - \hat R_t\big)^2\Big] \;-\; c_2\,\mathbb{E}_t\Big[\mathcal{H}\big[\pi_\theta(\cdot \mid s_t)\big]\Big]
  • พจน์แรก = clipped surrogate จากข้อ 3.3 (ติดลบเพราะเราต้องการ maximize)
  • พจน์ที่สอง = value loss สอน VψV_\psi ให้ทายเข้าใกล้ return จริง R^t\hat R_t, c1c_1 มักเป็น 0.5
  • พจน์ที่สาม = entropy bonus H\mathcal{H} กันการกระจายยุบเร็วเกินไป, c2c_2 มักเป็น 0.01
  • ส่วนสายจูง KL ของสมการ 3.2 ในทางปฏิบัตินิยมยัดเข้าไปใน reward ต่อ token: rtrtβ(logπθlogπref)r_t \leftarrow r_t - \beta\,(\log\pi_\theta - \log\pi_{\text{ref}}) ซึ่งเป็นวิธีที่เราใช้ในหัวข้อ 7

นับของเล่นทั้งหมดที่ต้องจูน: โมเดล 4 ตัว บวก ϵ,β,γ,λ,c1,c2\epsilon, \beta, \gamma, \lambda, c_1, c_2 และ learning rate อีกสองชุด นี่คือเหตุผลที่ PPO ขึ้นชื่อว่า "รันสองรอบด้วย seed ต่างกัน ได้ผลคนละเรื่อง" และเป็นเหตุผลการมีอยู่ของบทที่ 4 ทั้งบท

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

Bradley–Terry: gradient ไปกองอยู่ที่คู่ที่ยังเรียงผิด

เลือกโหมด Bradley-Terry ในเครื่องมือด้านล่าง แล้วลากค่า margin ดู: คู่ที่ reward model จัดอันดับถูกแล้วอย่างมั่นใจ (margin บวกมาก) แทบไม่เหลือ gradient เลย — การเทรน Stage A ใช้งบไปกับคู่ที่มันยังเรียงผิดโดยอัตโนมัติ

Loss family
Positive means the model already prefers the chosen response.
Bradley-Terry has no temperature term.
-6-4-20246reward margin Δlossgradient weight

Drag anywhere on the plot, or use the Δ slider with the arrow keys.

Δ2.00
Loss0.1269
Gradient weight0.119211.9% of maximum
σ(−βΔ)0.119211.92%

This pair teaches almost nothing.The model already ranks this pair correctly, so σ(−βΔ) is only 11.92% and the gradient is 11.9% of what a hard pair would give. This is why preference datasets full of obvious wins barely move the model: the easy pairs are silently ignored, and the few genuinely confusing pairs do all the work.

ถ้ากราฟนี้ทำให้นึกถึง gradient ของ DPO ในบทที่ 4 — ไม่ใช่เรื่องบังเอิญ DPO ยกโมเดล Bradley–Terry ตัวนี้ไปใช้ทั้งก้อน แค่เปลี่ยนว่าอะไรทำหน้าที่เป็น reward

Trust region ที่มองเห็นได้: min + clip

กราฟสองแผงของ clipped surrogate objective เทียบกับ probability ratio สำหรับ advantage บวกและลบ พร้อมแรเงาบริเวณที่ถูก clipกราฟสองแผงของ clipped surrogate objective เทียบกับ probability ratio สำหรับ advantage บวกและลบ พร้อมแรเงาบริเวณที่ถูก clip

Figure 3.1สมการ 3.3 วาดออกมาตรง ๆ ที่ ε = 0.2 — ฝั่ง Â บวก ผลตอบแทนถูกตัดเพดานที่ 1+ε (บริเวณแบน = gradient ศูนย์) แต่ฝั่ง Â ลบ ค่าปรับไม่มีเพดาน เพราะ min เลือกกิ่งที่แย่กว่าเสมอ

สังเกตความไม่สมมาตรให้ดี มันคือบุคลิกทั้งหมดของ PPO: ขยับเข้าหาสิ่งที่ดี ได้ทีละไม่เกิน 20% แต่ถ้าเผลอให้ความน่าจะเป็นกับ token แย่ ๆ มากไป โดนลากกลับเต็มแรงเสมอ "บริเวณแบน" ในภาพคือ trust region ที่ทำให้ PPO ใช้ rollout เก่าซ้ำหลาย epoch ได้โดยไม่ระเบิด

ปุ่มหมุน bias–variance ของ GAE

กราฟสองแผง: rollout สังเคราะห์ที่มี reward ปลายทาง กับเส้น advantage ของ GAE ที่ lambda 0, 0.5, 0.95 และ 1.0กราฟสองแผง: rollout สังเคราะห์ที่มี reward ปลายทาง กับเส้น advantage ของ GAE ที่ lambda 0, 0.5, 0.95 และ 1.0

Figure 3.2GAE บน rollout สังเคราะห์ 20 ขั้น (γ = 1): reward ต่อขั้นเป็น noise เล็ก ๆ คะแนนจริงมาตอนจบ และ V_ψ ตั้งใจให้ทายต่ำกว่าจริงราว 0.4 — λ = 0 สัญญาณไปไม่ถึง token ต้น ๆ, λ = 1 ทุก token ได้เครดิตเต็มพร้อม noise เต็ม (ภาพประกอบกลไก ไม่ใช่ข้อมูลเทรนจริง)

อ่านจากเส้นล่างขึ้นบน: ที่ λ=0\lambda = 0 (เขียว) advantage เกาะศูนย์เกือบตลอด — คะแนนตอนจบไปไม่ถึง token ต้น ๆ เพราะทุกอย่างถูกกรองผ่าน VψV_\psi ที่ทายพลาด ที่ λ=1\lambda = 1 (แดง) ทุก token ได้เครดิตเต็มจากตอนจบ แต่แบก noise สะสมของทั้งสายมาด้วย λ=0.95\lambda = 0.95 (น้ำเงิน) คือจุดกลางที่คนทั้งวงการเลือกใช้ — สัญญาณเดินทางไกล แต่ noise ถูกหน่วง

ก่อนไปต่อ ลองสร้างสัญชาตญาณของคำว่า advantage — "ดีกว่าที่คาดเท่าไหร่" — ด้วยมือ: เครื่องมือนี้ใช้ ค่าเฉลี่ยของกลุ่ม เป็น baseline แทน VψV_\psi (สปอยล์บทที่ 5 แบบเต็ม ๆ) กดชุด All correct แล้วดูว่าเกิดอะไรขึ้นเมื่อทุกคำตอบได้ reward เท่ากัน:

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.

advantage เป็นศูนย์ทั้งกลุ่ม = ไม่มีสัญญาณให้เรียน — จำความรู้สึกนี้ไว้ตอนอ่านหัวข้อ 9

สายจูง KL: สมการ 3.2 วาดเป็นภาพ

กราฟ proxy reward เทียบกับ KL แสดงเส้นทาง beta 0.05 ที่หยุดที่จุดสมดุล และ beta 0 ที่วิ่งเข้าเขต reward hackingกราฟ proxy reward เทียบกับ KL แสดงเส้นทาง beta 0.05 ที่หยุดที่จุดสมดุล และ beta 0 ที่วิ่งเข้าเขต reward hacking

Figure 3.3เส้นทางการเทรนสองแบบในระนาบ (KL, reward) — β = 0.05 ไต่ขึ้นแล้วหยุดตรงจุดที่กำไรส่วนเพิ่มเท่ากับค่าปรับ ส่วน β = 0 ไม่มีจุดหยุด วิ่งขวาเข้าเขต reward hacking (ภาพประกอบกลไกของ failure mode — เส้นจริงที่วัดได้อยู่ในหัวข้อ 8)

จุดหยุดของเส้นเขียวไม่ใช่การเดา — มันคือคณิตศาสตร์ของสมการ 3.2: optimization หยุดตรงที่กำไร reward ต่อ nat เท่ากับ β พอดี เดินต่อจากนั้นขาดทุน เมื่อ β=0\beta = 0 เงื่อนไขหยุดนี้ไม่มีอยู่ — ทุก nat ของการเดินห่างที่แลก reward ได้แม้นิดเดียวคือ "กำไร" โมเดลจึงวิ่งออกจากภาษาธรรมชาติไปเรื่อย ๆ ตราบใดที่ตัวเลข reward ยังกระดิกขึ้น

5. เตรียมสภาพแวดล้อม (Environment)

สเกลจริงของสิ่งที่เรากำลังย่อส่วน — อ่านก่อนรัน

RLHF ระดับใช้งานจริงถือโมเดล 4 ตัวที่ตัวใหญ่สุดมัก 7B ขึ้นไป ใช้คู่ preference จากมนุษย์จริง หลักหมื่นถึงหลักล้านคู่ และแยกเครื่อง generate (rollout fleet) ออกจากเครื่องเทรน โน้ตบุ๊กนี้ใช้ Qwen3-0.6B ทุกตำแหน่ง คู่ preference 100 คู่ และโจทย์เลข 64 ข้อ สิ่งที่มันสาธิตคืออัลกอริทึมครบทุกชิ้นส่วน — ไม่ใช่การทำ RLHF จริง ผลที่ได้จะพิสูจน์กลไก ไม่ได้พิสูจน์ว่าโมเดลเก่งขึ้นสำหรับใช้งาน

เปิด 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 มีระเบิดเพิ่มอีกหนึ่งลูกชื่อ ratio overflow — รอดูในหัวข้อ 7

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 จริง ๆ

ทำไม RLHF ถึงแพง: ภาพเดียวจบ

แผนภูมิแท่งแนวนอนเปรียบเทียบ VRAM ของ PPO แบบโหลดสี่โมเดลเต็ม กับแบบ LoRA ที่ policy และ reference ใช้น้ำหนักฐานร่วมกันแผนภูมิแท่งแนวนอนเปรียบเทียบ VRAM ของ PPO แบบโหลดสี่โมเดลเต็ม กับแบบ LoRA ที่ policy และ reference ใช้น้ำหนักฐานร่วมกัน

Figure 3.4โมเดลสี่ตัวที่ต้องอยู่ใน VRAM พร้อมกันระหว่างหนึ่ง PPO step คำนวณจากพารามิเตอร์จริงของ Qwen3-0.6B (fp16 เฉพาะน้ำหนัก) — LoRA ทำให้ policy กับ reference ใช้ base ก้อนเดียวกัน ประหยัดไปหนึ่งโมเดลเต็ม ๆ

ตัวเลขในภาพคือแค่น้ำหนัก — ยังไม่รวม activation, KV cache ตอน generate, gradient และ optimizer state และที่ 0.6B ทุกอย่างยังดูจิ๋ว แต่ตัวคูณ ×4 ไม่หายไปไหนเมื่อสเกลขึ้น: ที่ 7B คือ 56 GB ก่อนเริ่มทำอะไรเลย

นี่คือผลตอบแทนจากบทที่ 2 (รอบที่สอง)

policy ของเราคือ base + LoRA adapter จากบทที่ 2 ส่วน πref\pi_{\text{ref}} คือ base ตัวเดิมที่ปิด adapter เรียกผ่าน policy.disable_adapter() ได้เลย — reference model มีต้นทุน VRAM เพิ่มศูนย์ไบต์ บทที่ 4 จะใช้ท่าเดียวกันนี้กับ DPO อีกครั้ง มันคือเหตุผลเชิงสถาปัตยกรรมของการเลือก LoRA ตั้งแต่ต้นซีรีส์

6. เตรียมข้อมูล (Data)

Stage A — คู่ preference สำหรับ reward model

ใช้ iapp/dpo_thai_tutorial (100 คู่, Apache-2.0) — ชุดข้อมูล preference ภาษาไทยที่ผมทำขึ้นเองสำหรับซีรีส์นี้และปล่อยให้ใช้ต่อได้อิสระ แต่ละแถวมี prompt, chosen, rejected ที่คัดด้วยมือ เน้นความสุภาพและความเป็นธรรมชาติของภาษา

แบ่ง 80/20: เทรน 80 คู่ กัน 20 คู่ไว้เป็น held-out ห้ามแตะระหว่างเทรน เกณฑ์ผ่านของ Stage A คือ pairwise ranking accuracy บน 20 คู่ที่กันไว้ พร้อม Wilson 95% CI ที่ไม่คร่อม 0.5 — แค่ "ดีกว่าโยนเหรียญอย่างมีนัยสำคัญ" ก็ถือว่าพิสูจน์กลไกได้แล้ว เพราะ 20 คู่ทำ CI แคบกว่านั้นไม่ได้จริง ๆ

Stage B — โจทย์ที่ตรวจได้ด้วยกติกา

สำหรับลูป PPO เราใช้โจทย์เลข 64 ข้อจาก VISAI-AI/gsm8k-thai (GSM8K ฉบับแปลไทย) และให้คะแนนด้วยกติกาที่ตรวจสอบได้ แทนที่จะใช้ reward model จาก Stage A:

import re

def rule_reward(response: str, gold: int) -> float:
"""+1.0 ถ้าเลขจำนวนเต็มตัวสุดท้ายในคำตอบถูก, +0.2 ถ้าตอบเป็นภาษาไทยจริง"""
nums = re.findall(r"-?\d+", response.replace(",", ""))
correct = 1.0 if nums and int(nums[-1]) == gold else 0.0
thai = sum("ก" <= ch <= "๛" for ch in response)
thai_bonus = 0.2 if thai / max(len(response), 1) >= 0.5 else 0.0
return correct + thai_bonus
ทำไม Stage B ไม่ใช้ reward model จาก Stage A

ในระบบจริง Stage B กินผลผลิตของ Stage A ตรง ๆ — นั่นคือนิยามของ RLHF แต่ RM ที่เทรนจาก 100 คู่อ่อนเกินกว่าจะรับแรงกดดันของ PPO มันจะโดน hack ภายในไม่กี่ update แล้วเราจะแยกไม่ออกว่าลูป PPO ของเราผิด หรือ RM แค่อ่อน — การทดลองจะพิสูจน์อะไรไม่ได้เลย

rule reward จึงทำหน้าที่เป็น stand-in ของ RM ที่ตรวจสอบได้: เมื่อ reward ขึ้น เรารู้แน่ว่าลูปทำงานจริง แต่มันยังคง "ไม่สมบูรณ์" เหมือน RM ทุกตัว — มันวัดแค่เลขท้ายกับสัดส่วนอักขระไทย ไม่วัดความอ่านรู้เรื่องของทุกอย่างรอบ ๆ ซึ่งเป็นช่องโหว่ที่การทดลอง β=0\beta = 0 ในหัวข้อ 8 จะทิ่มให้ดู (แนวคิด reward ที่ตรวจได้ด้วยกติกาแบบนี้จะกลับมาเป็นพระเอกเต็มตัวในบทที่ 5)

7. โค้ดหลัก (Main code)

7.1 Stage A — เทรน reward model จริง (~6 นาที)

เปลี่ยนโมเดลภาษาให้เป็นเครื่องให้คะแนน: หัว LM ถูกแทนด้วย linear ชั้นเดียวที่คืนสเกลาร์หนึ่งตัว

import torch, torch.nn.functional as F
from transformers import AutoModelForSequenceClassification, AutoTokenizer
from peft import LoraConfig, get_peft_model

tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
rm = AutoModelForSequenceClassification.from_pretrained(
"Qwen/Qwen3-0.6B",
num_labels=1, # หัวสเกลาร์: หนึ่งคะแนนต่อหนึ่งข้อความ
torch_dtype=torch.float16, # T4 ไม่มี bf16
attn_implementation="sdpa",
).cuda()
rm.config.pad_token_id = tok.pad_token_id

rm = get_peft_model(rm, LoraConfig(
task_type="SEQ_CLS", r=8, lora_alpha=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
modules_to_save=["score"], # หัวคะแนนตั้งต้นแบบสุ่ม ต้องเทรนเต็ม
))
for p in rm.parameters():
if p.requires_grad:
p.data = p.data.float() # บทเรียน fp16 จากบทที่ 1: เทรนใน fp32

def rm_loss(chosen, rejected):
s_w = rm(**chosen).logits.squeeze(-1) # r_φ(x, y_w)
s_l = rm(**rejected).logits.squeeze(-1) # r_φ(x, y_l)
return -F.logsigmoid(s_w - s_l).mean() # สมการ 3.1 ตรงตัว

เทรน 3 epoch บน 80 คู่ แล้ววัด pairwise accuracy บน 20 คู่ที่กันไว้ พร้อม Wilson CI ถ้า CI ไม่คร่อม 0.5 — คุณเพิ่งเทรน reward model ตัวแรกในชีวิตสำเร็จ ด้วยข้อมูล 80 แถว

7.2 Stage B — PPO เขียนเองจากศูนย์ ~120 บรรทัด (~10 นาที)

เราจะไม่ใช้ PPOTrainer ของ TRL และนี่คือการตัดสินใจที่ตั้งใจ ไม่ใช่ความดื้อ: TRL ย้าย PPOTrainer ไปอยู่ trl.experimental แล้ว และประกาศแผนถอดออกใน 0.29.0 — โค้ดที่สอนด้วยไลบรารีตัวนี้จะรันไม่ได้ในอีกไม่กี่เดือน ส่วนลูป PPO ที่เขียนเองราว 120 บรรทัดจะรันได้ตราบเท่าที่ PyTorch ยังอยู่ และสำคัญกว่านั้น: เขียนเองแล้วคุณจะรู้ว่าทุกบรรทัดทำอะไร แบบเดียวกับ DPO loss 25 บรรทัดในบทที่ 4

ชิ้นส่วนบนกระดาน: policy = base + LoRA adapter (จากบทที่ 2, is_trainable=True), reference = base ตัวเดิมปิด adapter, และ value head เป็น MLP สองชั้นเสียบบน hidden state สุดท้าย:

value_head = torch.nn.Sequential(
torch.nn.Linear(1024, 1024), torch.nn.Tanh(),
torch.nn.Linear(1024, 1),
).float().cuda() # ~1M พารามิเตอร์ — จิ๋วเดียวเทียบกับอีกสามตัว

rollout: สุ่มคำตอบครั้งละ 8 prompt (max_new_tokens=200, do_sample=True) ให้คะแนนด้วย rule_reward, standardize คะแนนภายใน batch, และเก็บ log-prob ณ ตอน rollout แบบ detach ไว้ทันที แล้วเข้าลูป update นี้ — 40 บรรทัดที่เป็นหัวใจของทั้งบท (ลูปเต็มอยู่ในโน้ตบุ๊ก):

def compute_gae(rewards, values, gamma=1.0, lam=0.95):
"""rewards: [T] ของหนึ่ง response, values: [T+1] (ช่องสุดท้าย = 0 หลังจบ)"""
adv, acc = torch.zeros_like(rewards), 0.0
for t in reversed(range(rewards.shape[0])):
delta = rewards[t] + gamma * values[t + 1] - values[t] # สมการ 3.4
acc = delta + gamma * lam * acc # Â_t = δ_t + γλ Â_{t+1}
adv[t] = acc
return adv

def ppo_update(rollout, eps=0.2, beta=0.05):
out = policy(rollout.ids, output_hidden_states=True) # forward เดียวได้สองอย่าง
logp = gather_logprobs(out.logits, rollout.ids) # [B, T] มี gradient
with torch.no_grad(), policy.disable_adapter():
logp_ref = gather_logprobs(policy(rollout.ids).logits, rollout.ids)

# reward ต่อ token = ค่าปรับ KL ทุกตำแหน่ง + คะแนนงานที่ token สุดท้าย (สมการ 3.2)
rew = -beta * (logp.detach() - logp_ref)
rew[:, -1] += rollout.scores_std # คะแนน rule ที่ standardize แล้ว

hidden = out.hidden_states[-1].detach().float() # detach = ตัด gradient เข้าลำต้น
values = value_head(hidden).squeeze(-1)
adv = torch.stack([compute_gae(r, F.pad(v, (0, 1))) for r, v in zip(rew, values)])
adv = (adv - adv.mean()) / (adv.std() + 1e-8) # standardize advantage อีกชั้น

log_ratio = (logp - rollout.logp_old).clamp(-10, 10) # กัน fp16 overflow!
ratio = log_ratio.exp() # ρ_t (สมการ 3.3)
surr = torch.min(ratio * adv.detach(),
ratio.clamp(1 - eps, 1 + eps) * adv.detach())

returns = (adv + values).detach() # เป้าหมายของ value head
m = rollout.resp_mask # นับเฉพาะ token ของ response
loss = (-(surr * m).sum() + 0.5 * ((values - returns).pow(2) * m).sum()) / m.sum()
return loss # (พจน์ entropy อยู่ในโน้ตบุ๊ก)

ตั้งค่า: ϵ=0.2\epsilon = 0.2, β=0.05\beta = 0.05, γ=1.0\gamma = 1.0, λ=0.95\lambda = 0.95, 4 epoch ต่อ rollout เดียวกัน บน 64 prompt, LR ของ adapter 1e-5 ของ value head 1e-4 รวมประมาณ 10 นาทีต่อหนึ่งรันบน T4 (โน้ตบุ๊กรันสองครั้ง: β=0.05\beta = 0.05 และ β=0\beta = 0)

สามบรรทัดที่ถ้าพลาด การทดลองทั้งหมดเป็นโมฆะ

1. .clamp(-10, 10) ก่อน .exp() — ใน fp16 ค่า exp(12) = 162,754 เกินเพดาน fp16 (65,504) ผลคือ inf แล้วกลายเป็น NaN ระบาดทั้ง batch ภายใน step เดียว

2. logp_old ต้องคำนวณครั้งเดียวตอน rollout แล้ว detach เก็บไว้ — ห้ามคำนวณใหม่ในลูป epoch ถ้าคำนวณใหม่ ρt=1\rho_t = 1 เสมอ clip ไม่เคยทำงาน และ PPO ของคุณเสื่อมเป็น REINFORCE เงียบ ๆ โดยไม่มี error ใด ๆ

3. standardize คะแนนก่อนใช้ — สมการ 3.1 บอกแล้วว่าสเกลของ reward ไม่ถูกนิยาม rule reward ของเราอยู่ในช่วง 0 ถึง 1.2 ก็จริง แต่นิสัยนี้ต้องติดตัวไปตอนใช้ RM จริงที่สเกลมั่วได้ตามใจ

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

โน้ตบุ๊กวัดสามเส้นโค้งพร้อมกันทุก update แล้วเขียนลง results.json:

  1. Reward เฉลี่ยต่อ rollout — ควรไต่ขึ้น (นี่คือสิ่งที่เราซื้อ)
  2. KL ต่อ token เทียบ πref\pi_{\text{ref}} — ควรโตแล้วอิ่มตัวใต้เพดานที่ β\beta กำหนด (นี่คือราคาที่จ่าย)
  3. ความยาวคำตอบเฉลี่ย — ตัวจับโรค: ความยาวที่พุ่งหรือดิ่งผิดปกติคือสัญญาณแรกของ policy ที่กำลังเพี้ยน
ห้ามอ่านเส้น reward โดยไม่มีเส้น KL ประกบ — เด็ดขาด

reward ที่ไต่ขึ้นแปลว่าอะไรไม่ได้เลยถ้าไม่รู้ว่าโมเดลจ่ายอะไรไปแลกมา reward ขึ้น + KL อิ่มตัว = กำลังเรียนรู้ภายใต้สายจูง reward ขึ้น + KL พุ่งไม่หยุด = กำลังหนีออกจากภาษา ไปหาช่องโหว่ของกรรมการ เส้นเดียวกันคนละบริบท ความหมายตรงข้ามกันเป๊ะ — นี่คือเวอร์ชันวัดจริงของรูปที่ 3.3

จับ reward hacking คาหนังคาเขา

รันที่สองของโน้ตบุ๊กตั้ง β=0\beta = 0 โดยแตะอย่างอื่นเลยแม้แต่ตัวเดียว — ไม่มีสายจูง รูปแบบที่คาดว่าจะเห็น: reward ไต่เท่าเดิมหรือเร็วกว่า แต่ KL ทะยานไม่มีเพดาน และคำตอบเริ่มเสื่อมสภาพ — วนซ้ำ สั้นผิดปกติ หรือกลายเป็นสูตรสำเร็จที่ยัดตัวเลขไว้ท้ายประโยค เพราะ rule_reward มองเห็นแค่เลขท้ายกับสัดส่วนอักขระไทย ทุกอย่างที่มันมองไม่เห็นคือของฟรีที่โมเดลทิ้งได้

ตัวอย่างคำตอบเสื่อมสภาพจากรัน β=0\beta = 0 จะถูกพิมพ์โดยเซลล์สุดท้ายของโน้ตบุ๊ก:

[ช่องนี้เติมจากผลรันจริงของโน้ตบุ๊กเท่านั้น — ผมจะไม่แต่งตัวอย่าง degenerate ขึ้นเอง
เพราะทั้งซีรีส์นี้ยืนอยู่บนกติกาว่าไม่มีตัวเลขหรือ output ที่ invent ขึ้นมา
รันโน้ตบุ๊กแล้วเซลล์ "hacking exhibits" จะโชว์คำตอบจริง 2-3 อันพร้อม KL ของมัน]
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 (TH-MATH):

โมเดลTH-MATH acc (95% CI)KL เฉลี่ยตอนจบความยาวตอบเฉลี่ยเวลาเทรน
SFT จากบทที่ 2 (ตั้งต้น)baseline0baseline
PPO, β = 0.05?? (ควรอิ่มตัว)?~10 นาที
PPO, β = 0 (ablation)?? (ควรทะยาน)?~10 นาที

รูปแบบที่ควรจะเห็น: แถว β=0.05\beta = 0.05 ขยับ accuracy ขึ้นเล็กน้อยหรือเสมอตัวโดย KL นิ่ง ส่วนแถว β=0\beta = 0 ได้ rule reward ตอนเทรนสูงที่สุด แต่ accuracy บน held-out ไม่ควรดีกว่า และภาษาพัง — คะแนนที่โกงมาไม่โอนย้ายไปยังโจทย์ใหม่

ถ้าเห็นอย่างอื่น ให้ตีความแบบนี้:

  • ทั้งสามแถวแทบไม่ต่างกัน → advantage เป็นศูนย์เกือบตลอด เช็คว่า reward ในแต่ละ batch มีความหลากหลายไหม (ถ้าโมเดลตอบผิดหมดทุกข้อ advantage หลัง standardize คือ noise ล้วน — ความรู้สึกเดียวกับชุด All wrong ในเครื่องมือหัวข้อ 4)
  • β = 0 แล้ว KL ไม่ทะยาน → 4 epoch คูณ 64 prompt สั้นเกินกว่าที่การ hack จะสุกงอม — เพิ่มรอบแล้วดูใหม่ อย่าเพิ่งสรุปว่า "ไม่มี hacking"
  • β = 0.05 แต่ KL ก็ยังทะยาน → เกือบแน่นอนว่าลืม standardize คะแนน ทำให้สเกล reward ท่วม β หรือไม่ก็ LR สูงเกิน

กับดักที่ต้องระวัง

1. ไม่ standardize reward สเกลของ reward ไม่ถูกนิยามตามสมการ 3.1 — RM สองตัวที่จัดอันดับเหมือนกันเป๊ะอาจให้สเกลต่างกันสิบเท่า สเกลนั้นคูณตรงเข้าไปใน advantage และ gradient: รันเดิมที่เคยนิ่งจะระเบิดทันทีเมื่อเปลี่ยน RM

2. คำนวณ logp_old ใหม่ในลูป update บั๊กที่เงียบที่สุดของบทนี้: ไม่มี error, loss ดูปกติ แต่ ρt=1\rho_t = 1 ตลอดกาล clip ไม่เคยทำงาน trust region ไม่มีจริง — คุณกำลังรัน REINFORCE โดยเข้าใจว่าตัวเองรัน PPO

3. ใช้ลำต้นร่วมระหว่าง policy กับ value โดยไม่ stop-gradient value loss มีสเกลใหญ่และไหลย้อนเข้า backbone ไปเขียนทับความสามารถทางภาษา โน้ตบุ๊กนี้กันด้วย .detach() ตรง hidden state (เห็นในโค้ดหัวข้อ 7.2) — ราคาหนึ่งบรรทัด

4. fp16 ratio overflow exp(log_ratio) ระเบิดเกินเพดาน fp16 ได้ตั้งแต่ log-ratio ประมาณ 11.1 ต้อง .clamp(-10, 10) ก่อน .exp() เสมอ — และถ้าเห็น log-ratio โตถึงระดับนั้นบ่อย ๆ นั่นเป็นสัญญาณว่า policy กำลังวิ่งหนี rollout เก่าเร็วเกินไป (ลด LR หรือลดจำนวน epoch ต่อ rollout)

10. สรุป (Summary)

  • RLHF = เดินอ้อมสองขั้น เพื่อ optimize สิ่งที่หาอนุพันธ์ไม่ได้: เทรนกรรมการ (rϕr_\phi) แล้วใช้ RL วิ่งเข้าหาคะแนนของกรรมการ
  • Bradley–Terry เห็นแค่ผลต่าง — สเกลสัมบูรณ์ของ reward ไม่ถูกนิยาม จึงต้อง standardize ก่อนใช้เสมอ
  • สมการแม่ max E[r]βDKL\max\ \mathbb{E}[r] - \beta\,\mathbb{D}_{\text{KL}} คือสมการเดียวที่ต้องท่องจำ — บทที่ 4 และ 5 คือการแก้สมการนี้ด้วยวิธีอื่น
  • KL ไม่ใช่ regularizer — มันคือเงื่อนไขหยุดเพียงอย่างเดียวของระบบ ถอดออกเมื่อไหร่ Goodhart ทำงานเมื่อนั้น
  • min + clip = มองโลกแง่ร้ายโดยดีไซน์: ได้จำกัด เสียไม่จำกัด — trust region ที่ทำให้ rollout เก่าใช้ซ้ำได้
  • GAE คือปุ่ม bias–variance และ VψV_\psi ที่มันพึ่งพาคือโมเดลตัวที่สี่ ที่ GRPO จะลบทิ้งในบทที่ 5
  • PPO แพงเพราะโครงสร้าง ไม่ใช่เพราะเขียนโค้ดห่วย: โมเดล 4 ตัว + hyperparameter ร่วมสิบตัวคือราคาหน้าตั๋ว
ข้อจำกัดของการทดลองนี้

64 prompt กับ rule-based reward คือการสาธิตอัลกอริทึม ไม่ใช่การทำ RLHF rule reward ของเราเป็นแค่ stand-in ของ reward model จาก Stage A — RLHF จริงใช้ RM ระดับ 7B ขึ้นไปที่เทรนจาก preference ของมนุษย์หลักหมื่นถึงหลักล้านคู่ และต้องมี rollout fleet แยกต่างหาก เพราะการ generate กิน compute มากกว่าการ update หลายเท่า

สิ่งที่การทดลองนี้พิสูจน์ได้จริงมีสองอย่าง: ลูป PPO ที่เขียนเองทำงานถูกต้อง (reward ขึ้นใต้สายจูง KL) และ กลไกของ reward hacking มีจริง (ถอด β แล้ววัดได้ ไม่ใช่แค่เล่าให้ฟัง) อย่าเอาผลนี้ไปอ้างว่าได้โมเดลไทยที่ align แล้ว — สิ่งที่ได้คือความเข้าใจว่าเครื่องจักรทั้งเครื่องหมุนอย่างไร ซึ่งจำเป็นเป๊ะ ๆ ต่อการอ่านสองบทถัดไป

บทต่อไป: DPO — DPO ลบทั้ง reward model และ RL loop ทิ้งด้วยพีชคณิตล้วน ๆ จากสมการแม่ 3.2 ที่คุณเพิ่งท่องจำไป

อ้างอิง (References)

  1. Schulman et al. (2017). Proximal Policy Optimization Algorithms — PPO ต้นฉบับ: สมการ clipped surrogate ในหัวข้อ 3
  2. Schulman et al. (2015). High-Dimensional Continuous Control Using Generalized Advantage Estimation — GAE: สมการ advantage ที่ใช้ใน PPO
  3. Christiano et al. (2017). Deep reinforcement learning from human preferences — งานที่เริ่มต้นแนวคิด RL จาก preference ของมนุษย์
  4. Stiennon et al. (2020). Learning to summarize from human feedback — RLHF ที่ใช้งานได้จริงเป็นครั้งแรกในงาน summarization
  5. Ouyang et al. (2022). Training language models to follow instructions with human feedback — InstructGPT: ต้นแบบของ pipeline SFT -> RM -> PPO ทั้งหมด
  6. Bai et al. (2022). Training a Helpful and Harmless Assistant with Reinforcement Learning from Human Feedback — ชุดข้อมูล helpful/harmless และบทเรียนเรื่อง KL
  7. Zheng et al. (2023). Secrets of RLHF in Large Language Models Part I: PPO — รายละเอียดภาคปฏิบัติของ PPO ที่เปเปอร์อื่นไม่เขียน
  8. Bradley & Terry (1952). Rank Analysis of Incomplete Block Designs: I. The Method of Paired Comparisons — โมเดล Bradley-Terry ที่ reward model ทั้งหมดตั้งอยู่บนนั้น

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