ปรับปรุงโมเดล 350M ให้สร้างผลลัพธ์แบบมีโครงสร้างได้ดีขึ้น ด้วย GRPO เพียง 100 ขั้นตอน
การสร้างผลลัพธ์ที่มีโครงสร้าง (Structured Output) เป็นหนึ่งในงานที่ใช้กันทั่วไปสำหรับโมเดลภาษาขนาดใหญ่ (LLMs) ในโลกจริง แต่ส่วนใหญ่แล้ว การทดสอบมาตรฐาน (Benchmarks) มักจะรวมเอาการสร้างผลลัพธ์นี้เข้ากับการวัดผลด้านการให้เหตุผล (Reasoning) หรือการดึงข้อมูล (Extraction) มากกว่าที่จะวัดผลโดยตรง
หัวใจสำคัญคือการที่โมเดลสามารถสร้างผลลัพธ์ที่ถูกต้องและสามารถนำไปประมวลผลต่อได้ตามรูปแบบที่กำหนด (Schema Compliance) ซึ่งมักจะเป็นปัจจัยตัดสินว่าโมเดลนั้นจะสามารถนำไปเชื่อมต่อกับระบบอื่น ๆ ได้หรือไม่
บทความนี้จะแสดงให้เห็นว่า การปรับแต่งโมเดลขนาดเล็กให้เหมาะกับงานเฉพาะทาง (Task-specific Fine-tuning) สามารถเพิ่มประสิทธิภาพ และเทียบเท่ากับโมเดลที่มีขนาดใหญ่กว่ามากได้อย่างไร
การเตรียมความพร้อม: การติดตั้งและตั้งค่า
ก่อนเริ่มต้น เราจำเป็นต้องเตรียมเครื่องมือต่าง ๆ เพื่อให้การทำงานราบรื่น:
- GPU: ส่วนของการ Fine-tuning จะต้องใช้ GPU
- Colab/Kaggle: โน้ตบุ๊กที่ใช้ในการปรับแต่งโมเดลนี้ ถูกออกแบบมาให้สามารถทำงานบน GPU ฟรีของ Colab หรือ Kaggle ได้
- MacBook (สำหรับ Evaluation): ส่วนของการประเมินผล (Evaluation) สามารถรันบน MacBook ได้ โดยใช้
llama.cppซึ่งจะเปิดใช้งานเซิร์ฟเวอร์ที่รองรับ OpenAI API เพื่อให้เครื่องมือประเมินผล IFStruct สามารถสื่อสารได้
เครื่องมือที่เราต้องใช้คือ uv สำหรับจัดการเครื่องมือ Python และ llama.cpp สำหรับการให้บริการโมเดล
การติดตั้ง llama.cpp
ทำตามเอกสารของ Liquid AI เกี่ยวกับการติดตั้ง llama.cpp โดยใช้ Homebrew และตรวจสอบว่า llama-server พร้อมใช้งาน
การประเมิน IFStruct บน LFM2.5-350M (โมเดลพื้นฐาน)
ก่อนที่เราจะเริ่มปรับแต่งโมเดล ลองมาประเมิน LFM2.5-350M บน IFStruct Benchmark กันก่อน เพื่อดูว่าเราสามารถทำคะแนนได้ใกล้เคียงกับ 21.1% ที่รายงานไว้หรือไม่
IFStruct คือ Benchmark ที่ใช้ทดสอบความถูกต้องของผลลัพธ์ LLM และการปฏิบัติตาม Schema โดย Benchmark นี้เป็น Open-source อยู่ที่ Liquid4All/ifstruct และชุดข้อมูลสาธารณะสามารถเข้าถึงได้ที่ Hugging Face ในชื่อ LiquidAI/ifstruct-v1.0
สำหรับการเปรียบเทียบผลการประเมิน เราจะให้บริการโมเดลผ่าน llama.cpp บน MacBook โดยใช้โมเดล BF16 GGUF (LiquidAI/LFM2.5-350M-GGUF)
การเริ่มเซิร์ฟเวอร์โมเดลพื้นฐาน
เมื่อติดตั้ง llama.cpp เรียบร้อยแล้ว ให้เริ่มเซิร์ฟเวอร์โมเดลพื้นฐานด้วยคำสั่งต่อไปนี้:
./server -m --alias IFStruct --ngl 99 -np 4 -c 32768--alias: ชื่อโมเดลที่ IFStruct จะส่งไปยัง Endpoint ที่รองรับ OpenAI API-ngl 99: สั่งให้llama.cppโหลดทุก Layer ไปยัง GPU (หากมี)-np 4: ให้บริการคำขอ 4 รายการพร้อมกัน-c 32768: ขนาดของ Context Prompt
การรัน Benchmark
เมื่อเซิร์ฟเวอร์พร้อมทำงานแล้ว เราสามารถรัน Benchmark แบบเต็มรูปแบบด้วยตัวอย่าง 2000 รายการ:
python3 -m ifstruct.eval --model-name IFStruct --max-samples 2000ผลลัพธ์จาก IFStruct Release Blog รายงานคะแนน 21.1% สำหรับ LFM2.5-350M การตั้งค่า llama.cpp/BF16 ของเราวัดผลได้ 22.6% ซึ่งใกล้เคียงกับ 21.1% ที่รายงานใน IFStruct Blog เราจะใช้ผลลัพธ์จากการทดสอบภายในนี้เป็น Baseline สำหรับการเปรียบเทียบภายใต้ Stack การให้บริการเดียวกัน
GRPO Fine-tuning ด้วย TRL บน Structured Outputs
กระบวนการปรับแต่งโมเดลแบบเต็มรูปแบบสามารถดูได้ในโน้ตบุ๊กที่แนบมา ส่วนนี้จะกล่าวถึงเฉพาะส่วนที่สำคัญ
เราใช้ nvidia/Nemotron-RL-instructionfollowing-structuredoutputs ซึ่งจับคู่ Prompt แต่ละอันกับ Target JSON Schema และจำนวน Field ที่คาดหวัง เราใช้ตัวอย่างประมาณ 500 รายการสำหรับการฝึก
เนื่องจากลักษณะการกระจายข้อมูลของ Nemotron แตกต่างจาก IFStruct Evaluation เราจึงทำการเพิ่ม Prompt เพื่อปิดช่องว่างระหว่างทั้งสองส่วน:
- 40% ของ Prompt จะมีการเพิ่มคำสั่ง "return the output inside a fenced code block" เพื่อให้โมเดลเรียนรู้ที่จะทำตามคำสั่งรูปแบบ แทนที่จะปล่อย JSON ดิบเสมอ
- อีก 20% ที่แยกออกมา จะถูกแปลงเป็น Task แบบ Top-level Array (Schema จะถูกห่อหุ้มใน Array ที่มีจำนวน Item ที่ต้องการ) ซึ่งเป็นการฝึกให้โมเดลสามารถสร้าง Output แบบ List และปฏิบัติตามจำนวน Item ที่กำหนด
การสร้าง LoRA Adapter
เราโหลด LiquidAI/LFM2.5-350M และแนบ LoRA Adapter เข้าไป เนื่องจาก LFM2.5 ใช้สถาปัตยกรรมแบบ Hybrid Attention/Convolution เราจึงกำหนดเป้าหมายไปที่ Module Name เฉพาะของ LFM:
# ตัวอย่างโค้ด (ย่อ)
from trl import create_reference_model, AutoModelForCausalLMWithValueHead
from peft import LoraConfig
model = AutoModelForCausalLMWithValueHead.from_pretrained(
"LiquidAI/LFM2.5-350M",
load_in_8bit=True,
device_map="auto",
)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "wi", "wo", "fc1", "fc2"],
# ตัวอย่าง target modules
)
model.add_adapter(lora_config)การฝึกนี้จะปรับปรุงพารามิเตอร์ประมาณ 6 ล้านตัว ซึ่งคิดเป็นประมาณ 1.66% ของโมเดลทั้งหมด
การกำหนด Reward Functions
จากนั้น เรากำหนด Reward Functions 3 ตัว โดยแต่ละตัวจะให้คะแนนบนสเกล [0, 1] เพื่อประเมินว่าโครงสร้างที่ดึงออกมานั้นถูกต้องหรือไม่:
jsonformatreward: ผลลัพธ์สามารถ Parse ได้หรือไม่ และอยู่ในรูปแบบที่ต้องการหรือไม่? ได้คะแนนเต็ม (1.0) สำหรับรูปแบบที่ต้องการ (Fenced Code Block vs Raw JSON), 0.2 สำหรับรูปแบบที่ parse ได้แต่ไม่ถูกต้อง, และ 0.0 สำหรับผลลัพธ์ที่ Parse ไม่ได้fieldcountreward: Object มีจำนวน Top-level Fields ตรงตามที่คาดหวังหรือไม่? การตรงกันพอดีจะได้ 1.0 และคะแนนจะลดลงตามความคลาดเคลื่อนschemavalidationreward: ผลลัพธ์สามารถ Validate กับ JSON Schema ของ Row ได้หรือไม่? นับจำนวนข้อจำกัดที่ละเมิด และให้คะแนนบางส่วนโดยพิจารณาจาก Key ที่จำเป็น
เรานำทั้งสามมารวมกันเป็น Weighted Sum ด้วย reward_weights=[1.0, 0.5, 2.0]
การฝึกโมเดล
เราทำการฝึกเป็นเวลา 100 ขั้นตอน โดยมีการสร้างผลลัพธ์ 8 ครั้งต่อกลุ่ม Prompt ซึ่งเหมาะสำหรับ GPU ขนาด 16 GB ฟรี:
# ตัวอย่างโค้ด (ย่อ)
from trl import PPOConfig, PPOTrainer
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("LiquidAI/LFM2.5-350M")
# ... กำหนด Reward Functions และ Dataset ...
config = PPOConfig(
steps=100,
mini_batch_size=1,
# ปรับตามขนาด GPU
batch_size=8,
# จำนวน generations per prompt group
gradient_accumulation_steps=1,
# ... อื่นๆ ...
)
trainer = PPOTrainer(config, model, ref_model=None, tokenizer=tokenizer, dataset=dataset)
# ... ลูปการฝึก ...ดังที่เห็นในโน้ตบุ๊ก ระหว่างการฝึก ส่วนประกอบของ Reward ทั้งสามส่วนจะเพิ่มขึ้น KL จากโมเดลอ้างอิงจะยกตัวขึ้นจากศูนย์หลังช่วง Warm-up และสัดส่วนของ Truncated Completion จะคงที่ใกล้ศูนย์
การรวมและบันทึกโมเดล
สุดท้าย เราทำการรวม LoRA Adapter เข้ากับ Base Weights และบันทึกเป็น Checkpoint ที่สมบูรณ์พร้อมสำหรับการแปลงเป็น GGUF เพื่อให้บริการ
IFStruct Evaluation บน GRPO Tuned LFM2.5-350M
หลังจากการ Fine-tuning ด้วย GRPO เราทำการประเมิน IFStruct อีกครั้ง สำหรับขั้นตอนนี้ เราจำเป็นต้องแปลง Checkpoint ของโมเดลที่รวมแล้วให้เป็น BF16 GGUF ตัวแปลง Script นี้มาพร้อมกับ Source Code ของ llama.cpp ดังนั้นเราจะ Clone Repository และติดตั้ง Package gguf ของตัวแปลง
การเริ่มเซิร์ฟเวอร์โมเดลที่ Fine-tuned
จากนั้น เราให้บริการโมเดลที่รวมแล้วด้วยคำสั่งต่อไปนี้:
./server -m --alias IFStruct-GRPO --ngl 99 -np 4 -c 32768การรัน Benchmark อีกครั้ง
จากนั้น เราจะรัน IFStruct Evaluation แบบเต็มอีกครั้งด้วยโมเดลที่ Fine-tuned แล้ว:
python3 -m ifstruct.eval --model-name IFStruct-GRPO --max-samples 2000การเปรียบเทียบผลลัพธ์
เมื่อเปรียบเทียบผลการรันทั้งสองครั้งบน Stack การให้บริการที่เหมือนกัน:
| Metric | Base Model | GRPO Tuned Model | Gain |
| :------------------ | :--------- | :--------------- | :------- |
| IFStruct JSON Pass Rate | 22.6% | 29.7% | +7.1 pts |
| IFStruct YAML Pass Rate | 19.5% | 21.8% | +2.3 pts |
จะเห็นว่า การปรับปรุงนั้นตรงตามเป้าหมายของการฝึก: อัตราการผ่าน JSON เพิ่มขึ้นเกือบ 7 จุด (22.6% → 29.7%) ในขณะที่ YAML ยังคงใกล้เคียงเดิม แม้ว่าคะแนนนี้จะยังต่ำกว่าคะแนนของ Qwen3.5-2B ที่ 33.15% แต่ก็แสดงให้เห็นว่า การ Fine-tuning ที่เน้นงานเฉพาะทาง แม้จะเล็กน้อย ก็สามารถทำให้โมเดลขนาดเล็กเข้าใกล้โมเดลขนาดใหญ่ได้
การรัน GRPO สั้นๆ ด้วยตัวอย่างประมาณ 500 รายการ และ 100 ขั้นตอน สามารถยกระดับโมเดลขนาดเล็ก 350M พารามิเตอร์ จาก 22.6% เป็น 29.7% บน IFStruct ได้ ข้อคิดที่ได้คือ สัญญาณ Reward ที่ราคาไม่แพงและเฉพาะเจาะจงกับงาน สามารถทำให้โมเดลขนาดเล็กมีความน่าเชื่อถือเกี่ยวกับรูปแบบผลลัพธ์ได้อย่างมาก และช่วยลดช่องว่างกับโมเดลที่มีขนาดใหญ่กว่าหลายเท่าตัว
หากต้องการทำซ้ำหรือต่อยอดงานนี้ โปรดดูที่ IFStruct v1.0 Blog Post, Liquid4All/ifstruct benchmark repo, และ LiquidAI/ifstruct-v1.0 dataset
#AI #LLM #FineTuning #GRPO #StructuredOutput
ขอบคุณ แหล่งข้อมูล
https://huggingface.co/blog/grpo-with-trl-ifstruct