記事のサマリー(TL;DR)
- LFM2.5-350Mに対しTRLライブラリとGroup Relative Policy Optimization(GRPO)を用いたファインチューニングを行い、IFStructベンチマークのスコアを22.6%から29.7%に改善したと報告されています。
- 学習は約500サンプル・100ステップで完了し、無料枠のColabまたはKaggle GPUで実行可能な規模とされ、コードはGitHubで公開されています。
- 評価にはllama.cppが提供するOpenAI互換サーバーを利用し、記事内ではMacBook Pro(Apple M5 Max、36GBユニファイドメモリ)上でローカル実行した例が示されています。
前提条件
この手順は2つの作業に分かれており、それぞれ異なる環境で実行する構成になっています。
- ファインチューニングはGPU上で実行します。付属のノートブックは無料枠のColabまたはKaggle GPUに収まるサイズに調整されています。
- 評価はローカルのMacBook(本記事ではApple M5 Max搭載、36GBユニファイドメモリのMacBook Pro)上で、llama.cppを通じて実行できます。llama.cppはOpenAI互換サーバーを公開し、IFStructの評価スクリプトがこれと通信します。
Pythonツールにはuvを、サービングにはllama.cppを使用します。Liquid AIのllama.cppデプロイ手順に従い、Homebrewでllama.cppをインストールし、llama-serverが利用可能であることを確認します。
brew install llama.cpp
llama-server --version
詳細
ベースモデル(LFM2.5-350M)でのIFStruct評価
まず、LFM2.5-350MをIFStructベンチマークで評価し、報告されているスコア21.1%を再現できるか確認します。IFStructは、LLM出力の妥当性とスキーマ準拠を検証するためのベンチマークです。ベンチマークはLiquid4All/ifstructとしてオープンソース公開されており、公開ベンチマークデータセットはHugging FaceのLiquidAI/ifstruct-v1.0から入手できます。
git clone https://github.com/Liquid4All/ifstruct.git
評価比較のため、MacBook上でllama.cppを用いてモデルをローカルにサービングします。使用するのは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:利用可能な場合、全レイヤーをGPUにオフロードするようllama.cppに指示-np 4: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
結果は以下の通りです。
Model: LiquidAI/LFM2.5-350M
Overall: 452/2000 passed (22.6%)
Average latency: 1453ms
By format:
JSON: 180/1000 passed (18.0%)
YAML: 272/1000 passed (27.2%)
By top-level structure:
Wrapper key: 288/1011 passed (28.5%)
Bare list: 164/989 passed (16.6%)
エンティティ種別ごとの結果(一部抜粋):test__camera_review 6/83(7.2%)、test__event_ticket_booking 49/107(45.8%)、test__recipe 3/70(4.3%)、test__real_estate_listing 31/82(37.8%)など、種類によって通過率に大きな差がありました。
主なエラー内容としては、必須フィールドの欠落(7228件)、項目数の不一致(738件)、型の不一致(540件)、コードブロックの閉じ忘れ(317件)などが挙げられています。
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]の範囲で評価される3つの報酬関数を定義し、各補完結果について抽出された構造が正しいかを採点します。
json_format_reward:出力がパース可能か、要求された形式かを評価します。要求された形式(フェンス付きか生のJSONか)であれば満点(1.0)、形式は誤っているがパース可能な場合は0.2、パース不可能な出力は0.0とします。field_count_reward:トップレベルのフィールド数が期待通りかを評価します。完全一致で1.0、差があればその分だけ線形に減点します。schema_validation_reward:出力が各行のJSON Schemaに対して検証可能かを評価します。制約違反のすべてをカウントし、必須キーのカバー率に応じて部分点を与えます。
これら3つはreward_weights=[1.0, 0.5, 2.0]として重み付き和で結合しています。
学習
100ステップ、プロンプトグループあたり8件の生成で学習を行い、無料枠の16GB 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, # プロンプトグループごとにサンプリングする補完数
per_device_train_batch_size=4,
gradient_accumulation_steps=8, # 1最適化ステップあたり4プロンプトグループ
steps_per_generation=2,
max_completion_length=1024, # ネストしたJSON用の余地
mask_truncated_completions=False,
temperature=1.1, # 高めの温度でグループの多様性を維持
beta=0.01, # 参照モデルに対するKLペナルティ
reward_weights=[1.0, 0.5, 2.0], # json_format, field_count, schema_validation
logging_steps=1,
save_steps=100,
)
ノートブックで確認できる通り、学習の過程で3つの報酬成分すべてが上昇し、参照モデルからの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
結果は以下の通りです。
Model: lfm25-350m-grpo-structured-output
Overall: 594/2000 passed (29.7%)
Average latency: 1518ms
By format:
JSON: 319/1000 passed (31.9%)
YAML: 275/1000 passed (27.5%)
By top-level structure:
Wrapper key: 300/1011 passed (29.7%)
Bare list: 294/989 passed (29.7%)
エンティティ種別ごとの結果(一部抜粋):test__event_ticket_booking 62/107(57.9%)、test__escaping__support_ticket_batch 36/73(49.3%)、test__escaping__log_parser_examples 33/72(45.8%)、test__recipe 7/70(10.0%)など、種類によるばらつきは引き続き見られます。
主なエラー内容は、必須フィールドの欠落(7331件)、項目数の不一致(890件)、型の不一致(555件)、裸のリストが期待されるところでラッパーが返された事例(102件)などです。
2つの実行結果の比較(同一サービング構成)
| IFStructグループ | ベース | GRPOチューニング後 | 差分 |
|---|---|---|---|
| Overall | 22.6% | 29.7% | +7.1 |
| JSON | 18.0% | 31.9% | +13.9 |
| YAML | 27.2% | 27.5% | +0.3 |
| Wrapper key | 28.5% | 29.7% | +1.2 |
| Bare list | 16.6% | 29.7% | +13.1 |
改善は学習の狙い通りの箇所に表れており、JSONの通過率は18.0%から31.9%へと14ポイント近く上昇した一方、YAMLはほぼ変化がありませんでした。この結果はQwen3.5-2Bのスコア33.15%には及ばないものの、軽量なタスク特化型のファインチューニングでも小規模モデルを大規模モデルに近づけられることを示していると説明されています。
まとめ
約500サンプル・100ステップという短時間のGRPO学習により、350Mパラメータの小規模モデルをIFStructベンチマークで22.6%から29.7%に引き上げられたと報告されています。提供元は、安価でタスク特化型の報酬信号によって、小規模モデルが形式面でより信頼性を高められ、数倍の規模を持つモデルとの差の多くを縮められると説明しています。再現や拡張を行う場合は、元のIFStruct v1.0ブログ記事、Liquid4All/ifstructのベンチマークリポジトリ、およびLiquidAI/ifstruct-v1.0データセットを参照するよう案内されています。