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

[LLM 2/10] SFT + LoRA: สอนโมเดลให้เป็นผู้ช่วย ด้วยการเทรน 1.69% ของพารามิเตอร์

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

บทที่แล้วเราใช้ Continue Pretraining ใส่ความรู้เข้าไปในน้ำหนักโมเดล แต่โมเดลที่ "รู้" ไม่ได้แปลว่าโมเดลที่ "ตอบ" — โมเดล base มีอาชีพเดียวคือเขียนข้อความต่อ บทนี้จะสอน SFT (Supervised Fine-Tuning) ด้วย LoRA: เทคนิคที่เทรนแค่ราว 1.7% ของพารามิเตอร์ แต่เปลี่ยนพฤติกรรมของโมเดลได้ทั้งตัว — จบใน ~15 นาทีบน Colab ฟรี และสิ่งที่ได้กลับมาคือไฟล์ adapter ขนาดราว 40 MB ที่จะกลายเป็นกระดูกสันหลังของบทที่เหลือทั้งซีรีส์

Open in Colab02_sft_lora.ipynb

1. ปัญหา (Problem statement)

เอาโมเดล base แท้ ๆ อย่าง Qwen3-0.6B-Base จากบทที่แล้วมาพิมพ์ใส่ว่า "ช่วยแนะนำอาหารไทยให้หน่อยครับ" สิ่งที่ได้กลับมามักไม่ใช่คำตอบ แต่เป็นการเขียนต่อ — มันอาจแต่งคำถามเพิ่มอีกสามข้อ เขียนต่อเป็นบทความท่องเที่ยว หรือเปลี่ยนไปเป็นภาษาอังกฤษกลางทาง เพราะสิ่งเดียวที่มันเคยถูกเทรนคือ "ข้อความแบบนี้บนอินเทอร์เน็ต มักตามด้วยอะไร"

ความสามารถในการตอบ — รับคำสั่ง ตอบตรงประเด็น แล้วหยุด — ไม่ได้มากับ pretraining มันมาจาก SFT: การเทรนต่อด้วยคู่ (คำสั่ง, คำตอบที่ดี) หลายพันถึงหลายล้านคู่ โมเดล instruct ทุกตัวที่คุณเคยใช้ ล้วนผ่านขั้นนี้มาแล้วทั้งนั้น

แต่พอจะลงมือทำเอง จะเจอปัญหาซ้อนอยู่สองชั้น:

ชั้นแรก — ต้นทุนของ full fine-tuning ถ้าเทรนทุกพารามิเตอร์ คุณจะได้โมเดลใหม่ทั้งก้อน (~1.2 GB ต่อหนึ่งงานสำหรับโมเดล 0.6B) องค์กรที่มีสิบงาน — สรุปเอกสาร, ร่างจดหมาย, ตอบลูกค้า, จัดหมวดเรื่องร้องเรียน — ต้องเก็บสิบสำเนา และการขยับน้ำหนักทุกตัวด้วย learning rate สูง ๆ คือสูตรลบความรู้ที่เพิ่งใส่ไปในบทที่ 1 (จำกล่อง learning rate ได้ไหมครับ)

ชั้นที่สอง — ภาษาไทย แม้แต่ตัวที่ผ่านการ post-train มาแล้วอย่าง Qwen3-0.6B ก็ยังมีอาการที่เราเห็นซ้ำ ๆ ทั้งซีรีส์: ถามเป็นไทย แล้วคำตอบไหลไปเป็นภาษาอังกฤษกลางประโยค (นี่คือที่มาของ metric th_ratio ประจำซีรีส์) เพราะข้อมูล SFT ที่มันเคยเห็นเป็นภาษาอังกฤษเป็นหลัก

บทนี้แก้ทั้งสองชั้นพร้อมกัน: SFT ด้วยข้อมูลคำสั่งภาษาไทย และทำผ่าน LoRA แทน full fine-tuning

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

เราจะเอา Qwen3-0.6B มาเทรนด้วยคู่คำสั่ง-คำตอบภาษาไทย 4,000 ตัวอย่าง โดยใช้ loss ตัวเดียวกับบทที่ 1 เป๊ะ ๆ บวกของใหม่สองชิ้น:

  1. Completion mask — คิด loss เฉพาะ token ฝั่งคำตอบ ไม่คิดฝั่งคำถาม (หัวข้อ 3.1 จะอธิบายว่าทำไมข้ามข้อนี้แล้วพังแบบตลก ๆ)
  2. LoRA (Low-Rank Adaptation) — แช่แข็งน้ำหนักเดิมทั้งหมด แล้วเทรนเมทริกซ์เล็ก ๆ สองตัวที่วางทับแต่ละเลเยอร์แทน
แนวคิดหลักของบทนี้

คุณไม่ได้กำลังเทรนน้ำหนักโมเดล — คุณกำลังเทรน "ส่วนแก้" (correction) อันดับต่ำที่วางทับน้ำหนักเดิม

นี่คือเหตุผลที่ adapter มีขนาดแค่ ~40 MB ไม่ใช่ 1.2 GB, เหตุผลที่คุณเก็บ adapter ยี่สิบตัวไว้สลับใช้บน base ตัวเดียวได้ (ยี่สิบงาน = 0.8 GB ไม่ใช่ 24 GB), และเหตุผลที่ reference model ในบทที่ 4 (DPO) มีต้นทุน VRAM เพิ่มศูนย์ไบต์ — แค่ปิด adapter ก็ได้โมเดลตั้งต้นกลับคืนมาเป๊ะ ๆ

3. สมการ (Equation)

3.1 SFT loss กับ completion mask

LSFT(θ)=E(x,y)[t=1ymtlogpθ(ytx,y<t)]\mathcal{L}_{\text{SFT}}(\theta) = -\mathbb{E}_{(x,y)}\left[\sum_{t=1}^{|y|} m_t \log p_\theta(y_t \mid x, y_{<t})\right]
  • (x,y)(x, y) = ตัวอย่างหนึ่งคู่ — xx คือส่วนคำสั่ง (รวม chat template) และ yy คือลำดับ token ของตัวอย่างที่โมเดลเห็นจริงตอนเทรน
  • yty_t = token ตำแหน่งที่ tt และ y<ty_{<t} = token ทั้งหมดก่อนหน้า
  • pθp_\theta = ความน่าจะเป็นที่โมเดลพารามิเตอร์ θ\theta ทำนาย
  • mt{0,1}m_t \in \{0,1\} = completion mask — เป็น 1 เฉพาะ token ฝั่งคำตอบ และเป็น 0 บน token ของ prompt

ลองถอด mtm_t ออก (คือตั้ง mt1m_t \equiv 1 ทุกตำแหน่ง) สมการนี้จะกลายเป็น objective ของ CPT ในบทที่ 1 ทันที SFT คือ CPT บนข้อความที่ถูกจัดฉากเป็นบทสนทนา บวกกับ mask หนึ่งตัว — ไม่มีอะไรมากกว่านั้น

แต่ mask หนึ่งตัวนี้คือครึ่งหนึ่งของความสำเร็จ เพราะ mt1m_t \equiv 1 คือ default ที่พลาดกันง่ายที่สุด (หลาย pipeline รวมถึง SFTTrainer ถ้าไม่ใส่ collator ให้ถูก จะเทรนแบบนี้เงียบ ๆ) และในข้อมูลคำสั่งภาษาไทยทั่วไป token ฝั่ง prompt กินราว 60% ของตัวอย่าง แปลว่า gradient ส่วนใหญ่ของคุณกำลังสอนโมเดลให้หัดเขียนคำถามของผู้ใช้ ไม่ใช่หัดตอบ ผลข้างเคียงที่ตามมาจะได้เห็นในหัวข้อ 9

3.2 LoRA: เทรนส่วนแก้ ไม่ใช่น้ำหนัก

แทนที่จะอัปเดตเมทริกซ์น้ำหนัก W0W_0 ตรง ๆ LoRA ตรึง W0W_0 ไว้ แล้วเรียนรู้ส่วนต่างที่เป็นผลคูณของเมทริกซ์เล็กสองตัว:

W=W0+ΔW=W0+αrBAW' = W_0 + \Delta W = W_0 + \frac{\alpha}{r}\,B A
  • W0Rd×kW_0 \in \mathbb{R}^{d\times k} = น้ำหนักเดิมของเลเยอร์ ถูกแช่แข็ง ไม่รับ gradient เลย
  • BRd×rB \in \mathbb{R}^{d\times r} และ ARr×kA \in \mathbb{R}^{r\times k} = เมทริกซ์ adapter สองตัวที่เราเทรนจริง
  • rmin(d,k)r \ll \min(d, k) = rank ของส่วนแก้ — ปุ่มหลักของ LoRA (บทนี้ใช้ r=16r = 16)
  • α\alpha = ตัวคูณ scale — ผลคูณ BABA ถูกคูณด้วย α/r\alpha/r เสมอ (บทนี้ α=32\alpha = 32 ดังนั้น α/r=2\alpha/r = 2)

รายละเอียดสองข้อในนิยามนี้สำคัญกว่าที่หน้าตามันบอก:

BB ถูก initialize เป็นศูนย์ทั้งเมทริกซ์ ดังนั้น ΔW=0\Delta W = 0 ณ step แรก — การเทรนเริ่มจากโมเดล base เป๊ะ ๆ ไม่มีช่วงที่โมเดลถูกรบกวนด้วยน้ำหนักสุ่มเลย (ส่วน AA เป็น Gaussian สุ่ม — ถ้าตั้งศูนย์ทั้งคู่ gradient ของทั้งคู่จะเป็นศูนย์ตลอดกาล เพราะต่างฝ่ายต่างคูณกับศูนย์)

ตัวหาร rr ใน α/r\alpha/r ทำให้ขนาดของ update ไม่ขึ้นกับ rank — เพิ่ม rr เป็นสองเท่า ผลรวม BABA มีพจน์มากขึ้นสองเท่า แต่ถูกหารกลับพอดี คุณจึงกวาดหา rr ได้โดยไม่ต้องจูน learning rate ใหม่ทุกครั้ง

และเมื่อเทรนเสร็จ คุณเลือกได้สองทาง: merge (W=W0+αrBAW' = W_0 + \frac{\alpha}{r}BA แล้วได้โมเดลเดียวที่ไม่มี latency เพิ่ม) หรือเก็บแยก — ทางที่สองคือทางที่ซีรีส์นี้เลือก เพราะ adapter ที่ถอด-ใส่ได้คือสิ่งที่บทที่ 4 ใช้สร้าง reference model ฟรี ๆ

3.3 สัดส่วนพารามิเตอร์ที่เทรน — ตัวเลขที่ต้องเช็ค ไม่ใช่ท่อง

ต่อเมทริกซ์ขนาด d×kd \times k หนึ่งตัว adapter มีพารามิเตอร์ rd+rkrd + rk ตัว คิดเป็นสัดส่วน

r(d+k)dk\frac{r(d+k)}{dk}
  • d,kd, k = มิติของเมทริกซ์น้ำหนักเดิม
  • rr = rank ของ adapter

ลองแทนค่าจริงของ Qwen3-0.6B (hidden 1024, intermediate 3072, 28 เลเยอร์ ติด adapter ทั้ง 7 เมทริกซ์: q, k, v, o, gate, up, down) จะได้พารามิเตอร์ที่เทรนได้ 10,092,544 ตัว จากฐาน 596,049,920 ตัว = 1.69%

1.69% ไม่ใช่ "ต่ำกว่า 1%" — บทเรียนเรื่องการเช็คตัวเลข

บทความ LoRA แทบทุกชิ้นพูดว่า "เทรนต่ำกว่า 1% ของพารามิเตอร์" ตัวเลขนั้นจริงที่สเกล 7B ขึ้นไป แต่ไม่จริงกับโมเดลเล็ก เพราะพารามิเตอร์ adapter โตแบบ r(d+k)r(d+k)เชิงเส้นกับ hidden size ขณะที่พารามิเตอร์ฐานโตแบบ dkdkกำลังสอง ยิ่งโมเดลเล็ก adapter จึงยิ่งเป็นสัดส่วนที่ใหญ่

สังเกตด้วยว่า 1.69% ต่ำกว่าสัดส่วนต่อเมทริกซ์ (~2.1–2.3%) — เพราะตัวหารรวม embedding ราว 156 ล้านพารามิเตอร์ที่เราไม่ได้ติด adapter เข้าไปด้วย เลขพวกนี้เช็คได้ด้วยเลขคณิตล้วน ๆ และโน้ตบุ๊กจะให้ peft พิมพ์ค่าจริงให้ดูกับตาในหัวข้อ 7 — เชื่อ print ไม่ใช่บล็อก (รวมถึงบล็อกนี้)

เนื้อหาเต็มอยู่ในคอร์ส

บทความนี้คือประมาณ 30% แรกของบทเรียน — ส่วนที่เหลือ (การเตรียมสภาพแวดล้อม, การเตรียมข้อมูล, โค้ดหลัก, ผลลัพธ์จริง และบทสรุป) อยู่ในคอร์ส LLM Finetuning ซึ่งเรียนฟรี เพียงเข้าสู่ระบบด้วย Google

อ่านเนื้อหาเต็มในคอร์ส →