這份指南提供了一個完全公開且經濟實惠的方法,能大幅提升小型模型在結構化輸出合規性方面的表現。我們使用 TRL 函式庫,透過群體相對策略優化 (GRPO) 技術微調 LFM2.5-350M 模型,並在 IFStruct 基準測試上進行評估。
整個訓練過程僅需約 500 個樣本和 100 個訓練步驟,規模之小足以在免費層級的 Colab 或 Kaggle GPU 上運行,相關程式碼已在 GitHub 上提供。結果顯示,即使是輕量級的微調程序,也能將模型在 IFStruct 基準測試上的效能從 22.6% 提升至 29.7%。
結構化輸出是大型語言模型 (LLM) 最常見的實際應用任務之一,然而大多數基準測試卻將其納入更廣泛的推理或資訊擷取分數中,而非獨立衡量。模型是否能可靠地以所要求的格式和形狀返回有效、可解析的輸出(即結構描述合規性),往往決定了它是否能被整合到下游系統中。
請注意,此處描述的訓練流程並非用於訓練 IFStruct 部落格中提及的強化學習模型。本筆記本的目標不是重現 IFStruct 基準測試分數,而是展示小型模型如何透過任務特定微調來提升效能,甚至媲美大型模型。
前置條件
本指南分為兩個部分,在不同的環境中運行:
微調部分在 GPU 上運行。隨附的筆記本已針對免費層級的 Colab 或 Kaggle GPU 進行調整。評估部分則可在本地的 MacBook 上運行(例如配備 Apple M5 Max 和 36 GB 統一記憶體的 MacBook Pro),透過 llama.cpp 暴露一個與 OpenAI 相容的伺服器,供 IFStruct 評估器使用。
我們需要 uv 來管理 Python 工具,並使用 llama.cpp 進行模型服務。按照 Liquid AI 的 llama.cpp 部署文件,請使用 Homebrew 安裝 llama.cpp 並驗證 llama-server 是否可用:
brew install llama.cpp
llama-server --version
在 LFM2.5-350M(基礎模型)上進行 IFStruct 評估
在開始之前,讓我們先在 IFStruct 基準測試上評估 LFM2.5-350M,看看是否能重現其報告的 21.1% 分數。IFStruct 是一個用於測試 LLM 輸出有效性和結構描述合規性的基準測試。
該基準測試在 Liquid4All/ifstruct 上開源,公開的基準測試資料集可在 Hugging Face 的 LiquidAI/ifstruct-v1.0 找到。首先,我們需要複製儲存庫:
git clone https://github.com/Liquid4All/ifstruct.git
為了進行評估比較,我們使用 llama.cpp 在 MacBook 上本地服務模型。我們將使用 BF16 GGUF 格式的模型 (LiquidAI/LFM2.5-350M-GGUF)。然後,我們使用以下命令啟動基礎模型伺服器:
llama-server \
-hf LiquidAI/LFM2.5-350M-GGUF:BF16 \
-c 32768 \
-np 4 \
-ngl 99 \
--alias LiquidAI/LFM2.5-350M \
--host 127.0.0.1 \
--port 8080
其中,`--alias` 是 IFStruct 發送到 OpenAI 相容端點的模型名稱;`-ngl 99` 要求 llama.cpp 在可用時將所有層卸載到 GPU;`-np 4` 同時服務四個請求;`-c 32768` 則設定提示詞上下文的大小。
一旦伺服器運行,我們就可以使用 2000 個樣本運行完整的基準測試:
uv run ifstruct-eval \
--model LiquidAI/LFM2.5-350M \
--base-url http://localhost:8080/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm2.5-350m-llamacpp-base.json \
--n-threads 4 \
--max-tokens 2048 \
-v
評估結果顯示,模型總體通過率為 452/2000 (22.6%),平均延遲為 1453 毫秒。在格式方面,JSON 通過率為 18.0%,YAML 為 27.2%。在頂層結構方面,Wrapper key 通過率為 28.5%,Bare list 為 16.6%。
IFStruct 發布部落格報告 LFM2.5-350M 的分數為 21.1%。我們本地的 llama.cpp/BF16 設定測得 22.6%,與報告的 21.1% 相近。我們將此本地結果作為相同服務堆疊比較的基準線。
使用 TRL 進行結構化輸出的 GRPO 微調
完整的可運行流程位於隨附的筆記本中。本節僅涵蓋相關部分。
訓練資料
我們使用 `nvidia/Nemotron-RL-instruction_following-structured_outputs` 資料集,其中每個提示詞都與一個目標 JSON Schema 和預期的欄位計數配對。我們使用大約 500 個樣本進行訓練。
由於 Nemotron 資料分佈與 IFStruct 評估存在差異,我們對提示詞進行了增強,以彌補兩者之間的兩個差距。其中 40% 的提示詞會被附加「將輸出放在圍欄程式碼區塊內」的指令,讓模型學習遵循格式指令而非總是發出原始 JSON。另外 20% 的提示詞則被轉換為頂層陣列任務(結構描述被包裹在一個帶有必需項目計數的陣列中),這訓練了純粹列表輸出和項目計數的合規性。
模型與 LoRA
我們載入 LiquidAI/LFM2.5-350M 並附加一個 LoRA 轉接器。由於 LFM2.5 使用混合注意力/卷積架構,我們針對 LFM 特定的模組名稱進行設定:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "out_proj", "in_proj",
"w1", "w2", "w3",
],
)
這訓練了大約 600 萬個參數,約佔模型總參數的 1.66%。
獎勵函數
接著,我們定義了三個獎勵函數,每個函數的評分範圍為 [0, 1],用於評估每個完成的輸出結構是否正確。`json_format_reward` 評估輸出是否可解析且符合要求的形式,正確形式得 1.0 分,錯誤但可解析得 0.2 分,不可解析得 0.0 分。
`field_count_reward` 評估物件是否具有預期的頂層欄位數量,精確匹配得 1.0 分,分數隨差異線性遞減。`schema_validation_reward` 評估輸出是否符合該行的 JSON Schema,它會計算每個約束違規,並根據必要鍵的覆蓋率給予部分分數。我們將這三個函數以 `reward_weights=[1.0, 0.5, 2.0]` 的權重加權求和。
訓練
我們針對 100 個步驟進行訓練,每個提示詞組生成 8 個完成,此設定適用於免費層級的 16 GB GPU:
from trl import GRPOConfig
training_args = GRPOConfig(
output_dir="./outputs/lfm25-350m-nemotron-schema-grpo",
learning_rate=5e-5,
max_steps=100,
warmup_steps=10,
num_generations=8, # completions sampled per prompt group
per_device_train_batch_size=4,
gradient_accumulation_steps=8, # 4 prompt groups per optimizer step
steps_per_generation=2,
max_completion_length=1024, # room for nested JSON
mask_truncated_completions=False,
temperature=1.1, # hotter sampling keeps groups varied
beta=0.01, # KL penalty toward the reference model
reward_weights=[1.0, 0.5, 2.0], # json_format, field_count, schema_validation
logging_steps=1,
save_steps=100,
)
正如您在筆記本中看到的,在整個運行過程中,所有三個獎勵組件都逐漸上升,與參考模型的 KL 懲罰在暖身期後從零開始增加,而截斷完成的比例則保持在接近零。
合併並儲存模型
最後,我們將 LoRA 轉接器合併回基礎權重中,並將其儲存為一個獨立的檢查點,準備轉換為 GGUF 格式以供服務:
MERGED_DIR = f"{training_args.output_dir}-merged"
merged_model = trainer.model.merge_and_unload()
merged_model.save_pretrained(MERGED_DIR)
tokenizer.save_pretrained(MERGED_DIR)
在 GRPO 微調後的 LFM2.5-350M 上進行 IFStruct 評估
在 GRPO 微調之後,我們重新運行 IFStruct 評估。為此,我們需要將合併後的模型檢查點轉換為 BF16 GGUF 格式。轉換腳本隨 llama.cpp 原始碼提供,因此我們只需複製儲存庫一次並安裝轉換器的 gguf 套件。
git clone --depth 1 https://github.com/ggml-org/llama.cpp
pip install ./llama.cpp/gguf-py
mkdir -p models
python llama.cpp/convert_hf_to_gguf.py \
PATH_TO_YOUR_MERGED_MODEL \
--outfile ./models/lfm25-350m-grpo-bf16.gguf \
--outtype bf16
然後,我們使用以下命令服務合併後的模型:
llama-server \
-m ./models/lfm25-350m-grpo-bf16.gguf \
--alias lfm25-350m-grpo-structured-output \
-c 32768 \
-np 4 \
-ngl 99 \
--host 127.0.0.1 \
--port 8081
接著,我們將再次使用微調後的模型運行完整的 IFStruct 評估:
uv run ifstruct-eval \
--model lfm25-350m-grpo-structured-output \
--base-url http://localhost:8081/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm25-350m-grpo.json \
--n-threads 4 \
--max-tokens 2048 \
-v
評估結果顯示,微調後的模型總體通過率為 594/2000 (29.7%),平均延遲為 1518 毫秒。在格式方面,JSON 通過率為 31.9%,YAML 為 27.5%。在頂層結構方面,Wrapper key 和 Bare list 的通過率均為 29.7%。
