[LLM 1/10] Continue Pretraining: สอนความรู้ใหม่ให้ LLM ภาษาไทย
โมเดลภาษาขนาดใหญ่ที่เก่งภาษาไทยระดับหนึ่ง มักจะ "ไม่รู้จัก" ความรู้เฉพาะทางขององค์กรคุณเลย — ไม่รู้ระเบียบราชการไทย ไม่รู้ศัพท์เฉพาะในวงการของคุณ ไม่รู้เอกสารภายในบริษัท บทความนี้จะสอนวิธีแก้ที่ตรงที่สุด คือ Continue Pretraining (CPT) ตั้งแต่สมการ ไปจนถึงโค้ดที่รันจบได้จริงบน Colab ฟรีภายในราว 15 นาที
Open in Colab01_continue_pretraining.ipynb
1. ปัญหา (Problem statement)
ลองนึกภาพว่าคุณเอา Qwen3-0.6B มาถามว่า "ตามระเบียบสำนักนายกรัฐมนตรี การจัดซื้อจัดจ้างโดยวิธีเฉพาะเจาะจงทำได้เมื่อใด" โมเดลจะตอบได้อย่างมั่นใจ และตอบผิด เพราะมันไม่เคยเห็นเอกสารราชการไทยมากพอ
หลายคนพยายามแก้ด้วยการทำ fine-tuning ด้วยคู่ถาม-ตอบไม่กี่พันตัวอย่าง แล้วพบว่าไม่ได้ผล เหตุผลคือ SFT สอน "รูปแบบการตอบ" ไม่ได้สอน "ความรู้" ถ้าโมเดลไม่เคยมีความรู้นั้นอยู่ในน้ำหนัก (weights) การสอนให้มันตอบด้วยน้ำเสียงที่ถูกต้องก็แค่ทำให้มันมั่นใจเวลาโกหกเท่านั้น
ความรู้ใหม่เข้าสู่โมเดลได้ 3 ทาง และเลือกผิดคือสาเหตุที่โปรเจกต์ LLM ส่วนใหญ่ล้มเหลว:
| วิธี | เหมาะกับ | ต้นทุนตอนใช้งาน |
|---|---|---|
| RAG | ความรู้ที่เปลี่ยนบ่อย ต้องอ้างอิงแหล่งที่มา | ค้นทุกครั้ง + prompt ยาว |
| Continue Pretraining | ความรู้เฉพาะทางจำนวนมาก ที่ค่อนข้างนิ่ง | ไม่มี (อยู่ในน้ำหนักแล้ว) |
| SFT | รูปแบบ น้ำเสียง โครงสร้างคำตอบ | ไม่มี |
บทความนี้คือทางที่สอง
2. เราจะทำอะไร (Solution)
เราจะเอาโมเดล base (ยังไม่ผ่าน instruction tuning) มาเทรนต่อด้วย objective เดียวกับตอน pretrain เป๊ะ ๆ คือทายคำถัดไป บนข้อความดิบภาษาไทยในโดเมนที่เราสนใจ ไม่มี label ไม่มีคู่ถาม-ตอบ มีแค่ข้อความล้วน ๆ
แต่หัวใจของบทความนี้ไม่ใช่ "เทรนแล้วเก่งขึ้น" — มันคือสิ่งที่แลกมา:
CPT ซื้อความแม่นในโดเมน ด้วยการจ่ายความสามารถทั่วไปที่หายไป มันคือการแลกเปลี่ยน ไม่ใช่ของฟรี และ "อัตราแลกเปลี่ยน" ถูกควบคุมด้วยตัวเลขตัวเดียวชื่อ replay ratio
ปรากฏการณ์ที่โมเดลลืมสิ่งที่เคยทำได้ เรียกว่า catastrophic forgetting เราจะไม่พูดลอย ๆ แต่จะวัดมันออกมาเป็นตัวเลข แล้วหาจุดที่ยอมรับได้
3. สมการ (Equation)
3.1 Objective ของ CPT
- = token ตำแหน่งที่
- = token ทั้งหมดก่อนหน้า
- = ความน่าจะเป็นที่โมเดลทำนาย
สมการนี้เหมือนกับตอน pretrain ทุกประการ สิ่งเดียวที่เปลี่ยนคือข้อมูล นี่คือเหตุผลที่ CPT ไม่ต้องการ label — ข้อความเองคือเฉลย
3.2 Perplexity: หน่วยวัดของเรา
แปลเป็นภาษาคน: "โดยเฉลี่ยแล้ว โมเดลกำลังลังเลอยู่ระหว่างกี่ตัวเลือก" PPL = 20 คือลังเลประมาณ 20 คำ, PPL = 5 คือมั่นใจกว่ามาก ยิ่งต่ำยิ่งดี
3.3 สมการที่สำคัญที่สุดในบทนี้ — Replay Mixing
คือสัดส่วนข้อมูลโดเมนในแต่ละ batch
- → ข้อมูลโดเมนล้วน → เก่งโดเมนเร็วที่สุด และลืมเร็วที่สุด
- → ผสมครึ่งต่อครึ่ง → ช้ากว่าแต่ลืมน้อยกว่ามาก
อย่า hardcode ค่านี้ จงกวาดหาค่ามัน แล้วเลือกจุดที่คุณยอมรับได้
4. เห็นภาพสมการ (Visualize)
Perplexity บอกอะไรเรากันแน่
Figure 1.1PPL คือ exp ของ loss — และการลดลง 5 หน่วยมีความหมายต่างกันมาก ขึ้นกับว่าคุณเริ่มจากตรงไหน
กราฟขวาคือสิ่งที่คนมักพลาด: ถ้ามีคนบอกว่า "ลด perplexity ได้ 5 หน่วย" แล้วไม่บอกว่าเริ่มจากเท่าไหร่ ประโยคนั้นแทบไม่มีความหมาย เพราะ 80 → 75 คือดีขึ้น 6% แต่ 10 → 5 คือดีขึ้น 50%
การแลกเปลี่ยนที่ replay ratio ควบคุม
Figure 1.2รูปทรงของการแลกเปลี่ยนที่เกิดจากสมการ replay mixing (ภาพประกอบกลไก ไม่ใช่ผลการวัดจริง — ผลจริงอยู่ในหัวข้อที่ 8)
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 # ใน TrainingArguments (ไม่ใช่ 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 จริง ๆ
งบ VRAM มีเท่าไหร่ และหมดไปกับอะไร
Figure 1.3ที่มาของ VRAM ตอนเทรนทั้งโมเดล คำนวณจากค่าจริงใน config ของ Qwen3-0.6B (596M พารามิเตอร์)
สังเกตว่า optimizer state กินที่มากกว่าตัวโมเดลเอง — Adam เก็บ และ อย่างละหนึ่งชุดเท่าจำนวนพารามิเตอร์
การเปลี่ยนไปใช้ adamw_bnb_8bit ประหยัดได้ 3.3 GB ซึ่งแปลว่าคุณเพิ่ม batch size หรือความยาว sequence ได้อีกมาก
ลองเล่นกับงบ VRAM ด้วยตัวเอง — ปรับค่าแล้วดูว่าเมื่อไหร่จะ OOM:
- Weights1.13 GiB
- Gradients19.25 MiB
- Optimizer state115.50 MiB
- Activations170.00 MiB
- KV cache—
It fits.This run needs 1.43 GiB and leaves 14.57 GiB of headroom on a free Colab T4.
6. เตรียมข้อมูล (Data)
เราใช้ pythainlp/thaigov-v2-corpus-22032023 — คลังข่าวและเอกสารราชการไทย (สาธารณสมบัติ)
เป็นตัวแทนของ "ความรู้เฉพาะทางที่โมเดลไม่เคยเห็นมากพอ"
และใช้ข้อความภาษาไทยทั่วไปอีกชุดเป็น replay data เพื่อกันการลืม
from datasets import load_dataset
domain = load_dataset("pythainlp/thaigov-v2-corpus-22032023", split="train")
domain = domain.shuffle(seed=42).select(range(8000))
Packing: อย่าปล่อยให้ padding กินงบ
ถ้าเอาแต่ละเอกสารมา pad ให้ยาวเท่ากัน คุณจะเสีย compute ไปกับ <pad> มหาศาล
วิธีที่ถูกคือ ต่อทุกเอกสารเข้าด้วยกันแล้วหั่นเป็นบล็อกยาว 512 token เท่า ๆ กัน
def pack(examples, block_size=512):
ids = []
for text in examples["context"]:
ids.extend(tokenizer(text + tokenizer.eos_token).input_ids)
n = (len(ids) // block_size) * block_size
return {"input_ids": [ids[i:i+block_size] for i in range(0, n, block_size)]}
tokenizer ของโมเดลส่วนใหญ่ถูกฝึกด้วยข้อมูลภาษาอังกฤษเป็นหลัก ข้อความไทยจึงถูกหั่นเป็น token ถี่กว่า — ประโยคเดียวกันอาจใช้ token มากกว่า 2–3 เท่า แปลว่าค่า API แพงกว่า, context เต็มเร็วกว่า, และเทรนช้ากว่า โน้ตบุ๊กจะวัดตัวเลขนี้ให้ดู
7. โค้ดหลัก (Main code)
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-0.6B-Base", # base ไม่ใช่ instruct — CPT ต้องเริ่มจาก base
torch_dtype=torch.float16, # T4 ไม่มี bf16
attn_implementation="sdpa", # T4 ไม่มี FlashAttention-2
).cuda()
model = model.float() # ต้อง cast เป็น fp32 ก่อนเทรน — ดูกล่องด้านล่าง
args = TrainingArguments(
output_dir="cpt-out",
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # effective batch = 16
num_train_epochs=1,
learning_rate=2e-5, # ต่ำกว่า SFT 10 เท่า — ดูคำเตือนด้านล่าง
lr_scheduler_type="cosine",
warmup_steps=50,
optim="adamw_bnb_8bit", # ประหยัด 3.3 GB
gradient_checkpointing=True,
max_grad_norm=1.0, # กัน fp16 ระเบิด
fp16=True, # ไม่ใช่ bf16
logging_steps=10,
)
fp16=True ไม่ได้แปลว่าน้ำหนักเป็น fp16 — มันคือ mixed precision:
การคูณเมทริกซ์ทำใน fp16 แต่ น้ำหนักหลัก (master weights) ต้องเป็น fp32
เพราะ optimizer ต้องบวกค่าที่เล็กมาก (lr = 2e-5) เข้าไป ซึ่ง fp16 ละเอียดไม่พอจะเก็บ
ถ้าโหลดโมเดลเป็น fp16 แล้วเทรนทั้งโมเดลด้วย fp16=True ตรง ๆ คุณจะเจอ:
ValueError: Attempting to unscale FP16 gradients.
เพราะ max_grad_norm=1.0 บังคับให้ต้อง unscale gradient ก่อน clip แต่ gradient นั้นเป็น fp16
วิธีแก้คือ model.float() ก่อนเทรน แล้ว cast กลับเป็น fp16 ตอนวัดผล
(สำหรับ LoRA ในบทที่ 2 และ 4 จะ cast เฉพาะพารามิเตอร์ของ adapter เท่านั้น)
ถ้าคุณใช้ learning_rate=2e-4 (ค่าที่คนมักใช้กับ LoRA) มาทำ CPT แบบเต็มโมเดล
คุณจะลบความสามารถของโมเดลทิ้งภายในไม่กี่ร้อย step
CPT ต้องการ LR ต่ำกว่า SFT ประมาณ 10–50 เท่า เพราะเรากำลังขยับน้ำหนักทุกตัว
8. ผลลัพธ์ (Results)
โน้ตบุ๊กจะวัด 3 อย่างก่อนและหลังเทรน แล้วเขียนลง results.json:
- Domain held-out PPL — ควรลดลงชัดเจน (นี่คือสิ่งที่เราจ่ายเงินซื้อ)
- General held-out PPL — ควรเพิ่มขึ้นบ้าง (นี่คือราคาที่จ่าย)
- TH-KNOW accuracy จากชุดวัด KobEval-TH พร้อม Wilson 95% CI
ถ้าชุดทดสอบมี 100 ข้อ ช่วงความเชื่อมั่น 95% จะกว้างประมาณ ±10 จุด แปลว่า "78% เทียบกับ 74%" มักจะแยกไม่ออกจากความบังเอิญ ตัวเลข accuracy ที่ไม่มี CI ไม่ใช่ผลการทดลอง มันคือข่าวลือ — เราจะลงลึกเรื่องนี้ในบทที่ 9
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆbase
sft
Showing the built-in sample.
9. เปรียบเทียบ (Comparison)
โน้ตบุ๊กเทรน 3 แบบบนข้อมูลชุดเดียวกัน เพื่อให้เห็นการแลกเปลี่ยนเป็นตัวเลข:
| โมเดล | Domain PPL ↓ | General PPL ↓ | TH-DOMAIN | เวลาเทรน |
|---|---|---|---|---|
| Base (ยังไม่เทรน) | 4.83 | 5.88 | 27.3% | — |
| CPT, λ = 1.0 (โดเมนล้วน) | 4.07 (−0.76) | 6.72 (+0.85) | — | 8.0 นาที |
| CPT, λ = 0.5 (ผสม replay) | 4.29 (−0.53) | 4.98 (−0.90) | 36.4% | 8.0 นาที |
results.json ที่โน้ตบุ๊กเขียนเอง
อ่านตารางนี้ให้ออกคือหัวใจของบทนี้:
- λ = 1.0 ชนะ domain PPL (4.07 ต่ำสุด) แต่ general PPL แย่ลง จาก 5.88 เป็น 6.72 — นี่คือ catastrophic forgetting ที่วัดออกมาเป็นตัวเลขได้จริง ไม่ใช่คำเล่าลือ
- λ = 0.5 ยอมเสีย domain นิดหน่อย (4.29) แต่ general กลับดีขึ้น เป็น 4.98 — replay ไม่ได้แค่กันลืม มันช่วยให้โมเดลอ่านภาษาไทยทั่วไปดีขึ้นด้วย
27.3% → 36.4% ดูน่าดีใจ แต่ Wilson 95% CI คือ 13.2–48.2 กับ 19.7–57.0 ซึ่งซ้อนทับกันเกือบทั้งช่วง ที่ n=22 ข้อ ผลนี้จึงเป็นได้แค่ สัญญาณ ไม่ใช่ข้อสรุป
หลักฐานที่หนักแน่นจริงคือ PPL เพราะวัดจาก token หลายหมื่นตัว ไม่ใช่ 22 ข้อ ถ้าอยากให้ TH-DOMAIN สรุปได้ ต้องเพิ่มจำนวนข้อเป็นหลักร้อย — เราจะลงลึกเรื่องนี้ในบทที่ 9
10. สรุป (Summary)
- CPT ใส่ความรู้เข้าไปในน้ำหนัก ด้วย objective เดียวกับ pretraining ไม่ต้องมี label
- มันคือการแลกเปลี่ยนเสมอ ความแม่นในโดเมนแลกมาด้วยความสามารถทั่วไปที่หายไป
- replay ratio คือปุ่มควบคุมอัตราแลกเปลี่ยน — กวาดหาค่า อย่าเดา
- learning rate ต่ำ ๆ คือเส้นแบ่งระหว่าง CPT กับการทำลายโมเดล
- ทุกตัวเลขต้องมาพร้อม confidence interval
เราเทรนด้วยเอกสารราว 8,000 ชิ้น ขณะที่ CPT จริงระดับ OpenThaiGPT ใช้ข้อมูลระดับหมื่นล้าน token ต่างกันประมาณ 6 ระดับขนาด (order of magnitude)
การทดลองนี้พิสูจน์ "กลไก" และ "การแลกเปลี่ยน" ได้จริง แต่ไม่ได้ทำให้ได้โมเดลที่ดีขึ้นสำหรับใช้งานจริง อย่าเอาผลนี้ไปอ้างว่าสร้างโมเดลไทยที่ดีกว่าเดิม สิ่งที่คุณได้คือความเข้าใจว่าปุ่มแต่ละปุ่มทำอะไร ซึ่งจะโอนไปใช้กับงานสเกลจริงได้
บทต่อไป: SFT และ LoRA — เมื่อโมเดลมีความรู้แล้ว เราจะสอนให้มันตอบอย่างไร และทำไมการเทรนแค่ 1.7% ของพารามิเตอร์ถึงเกือบดีเท่าเทรนทั้งหมด
อ้างอิง (References)
- Gururangan et al. (2020). Don't Stop Pretraining: Adapt Language Models to Domains and Tasks — ต้นตำรับของ domain-adaptive pretraining ที่บทนี้ทำตาม
- Ibrahim et al. (2024). Simple and Scalable Strategies to Continually Pre-train Large Language Models — กลยุทธ์ replay และ LR ที่ทำให้ CPT ไม่ทำลายโมเดล
- Gupta et al. (2023). Continual Pre-Training of Large Language Models: How to (re)warm your model? — ทำไม learning rate warmup ถึงสำคัญมากตอนเทรนต่อ
- Luo et al. (2023). An Empirical Study of Catastrophic Forgetting in Large Language Models During Continual Fine-tuning — การวัด catastrophic forgetting อย่างเป็นระบบ
- Kaplan et al. (2020). Scaling Laws for Neural Language Models — scaling laws -- ที่มาของคำว่า "ข้อมูล 8,000 ชิ้นน้อยเกินไป"
- Hoffmann et al. (2022). Training Compute-Optimal Large Language Models — Chinchilla: สัดส่วนข้อมูลต่อพารามิเตอร์ที่เหมาะสม
- Yuenyong et al. (2025). OpenThaiGPT 1.6 and R1: Thai-Centric Open Source and Reasoning Large Language Models — CPT ภาษาไทยระดับจริง เทียบกับสเกลของบทนี้
- Lowphansirikul et al. (2021). WangchanBERTa: Pretraining transformer-based Thai Language Models — โมเดลภาษาไทยรุ่นบุกเบิกและวิธีเตรียมคอร์ปัสไทย
บทความ โค้ด และโน้ตบุ๊กในซีรีส์นี้เผยแพร่ภายใต้สัญญาอนุญาต CC BY-NC-SA 4.0 — นำไปใช้และดัดแปลงต่อได้ โดยอ้างอิงที่มา ไม่ใช้เพื่อการค้า และเผยแพร่ต่อด้วยสัญญาเดียวกัน (โมเดลและชุดข้อมูลของบุคคลที่สามที่อ้างถึง ยังคงใช้สัญญาของเจ้าของเดิม)
