← 文章 / AI技术
HuggingFace博客 3小时前 · 2026-09-03 20:07:15 · 5 阅读

用 GRPO 在 100 步内微调 350M 模型,提升结构化输出能力

本指南是一份完全公开、低成本的方法,能让小模型在结构化输出合规性上实现显著提升。我们使用 LFM2.5-350M 模型,结合 TRL 库 中的 Group Relative Policy Optimization (GRPO) 方法进行微调,并在 IFStruct 基准 上进行评估。整个训练过程仅需约 500 个样本和 100 步训练,规模足够小,可在免费版的 Colab 或 Kaggle GPU 上运行,相关代码已发布于 GitHub。实验结果表明,即使是轻量级的微调流程,也能让模型在 IFStruct 基准上的表现从 22.6% 提升至 29.7%。 结构化输出是 LLM 最常见的实际应用场景之一,但大多数评测 benchmark 将其并入更广泛的推理或抽取指标中,而非单独衡量。一个模型能否稳定返回符合指定格式和结构的合法可解析输出——即 schema 合规性——往往是决定其能否接入下游系统的关键。

请注意,本指南所述的训练流程并非 IFStruct 博客 中介绍的 RL 模型所使用的训练 pipeline。本 notebook 的目标并非复现 IFStruct 基准的分数,而是展示针对小模型进行任务特定微调如何提升性能,并使其表现接近参数量大得多的模型。

前置条件

本指南分为两部分,分别在以下环境运行:
  • 微调(Fine-tuning) 需在 GPU 上执行。配套的 notebook 已针对免费版的 Colab 或 Kaggle GPU 优化配置。
  • 评估(Evaluation) 可在 MacBook 本地运行(本文使用配备 Apple M5 Max 芯片和 36GB 统一内存的 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 ``` ## IFStruct 评测 LFM2.5-350M(基础模型) 开始前,先在 IFStruct 基准上评测 LFM2.5-350M,看看能否复现其报告的 21.1% 得分。 **IFStruct** 是一个用于测试大语言模型输出有效性和模式遵循能力的基准。该基准开源在 [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct),公共数据集可在 Hugging Face 的 [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) 获取。 ``` git clone https://github.com/Liquid4All/ifstruct.git ``` 为便于对比评测,我们使用 `llama.cpp` 在 MacBook 上本地部署模型,选用的是 `BF16` 格式的 GGUF 文件([LiquidAI/LFM2.5-350M-GGUF](https://huggingface.co/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,将全部层卸载到 GPU - `-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 ```

============================================================
模型:LiquidAI/LFM2.5-350M
============================================================
总计:452/2000 通过 (22.6%)
平均延迟:1453ms

按格式:
  JSON:180/1000 通过 (18.0%)
  YAML:272/1000 通过 (27.2%)

按顶层结构:
  Wrapper key 288/1011 通过 (28.5%)
  Bare list   164/989 通过 (16.6%)

按实体类型:
  test__camera_review                 6/83 通过 (7.2%)
  test__clinical_trial                20/104 通过 (19.2%)
  test__conference_schedule           7/87 通过 (8.0%)
  test__escaping__bug_report_batch    24/89 通过 (27.0%)
  test__escaping__config_snippet_audit 15/85 通过 (17.6%)
  test__escaping__customer_email_thread 5/73 通过 (6.8%)
  test__escaping__dialogue_sample     14/95 通过 (14.7%)
  test__escaping__interview_transcript_segment 21/80 通过 (26.2%)
  test__escaping__log_parser_examples 21/72 通过 (29.2%)
  test__escaping__pr_discussion       22/87 通过 (25.3%)
  test__escaping__repro_steps_batch   16/73 通过 (21.9%)
  test__escaping__screenplay_scene    16/92 通过 (17.4%)
  test__escaping__short_story_chapter 15/84 通过 (17.9%)
  test__escaping__support_ticket_batch 27/73 通过 (37.0%)
  test__escaping__terminal_session_notes 20/70 通过 (28.6%)
  test__event_ticket_booking          49/107 通过 (45.8%)
  test__gpu_review                    6/94 通过 (6.4%)
  test__invoice                       28/86 通过 (32.6%)
  test__job_posting                   25/85 通过 (29.4%)
  test__real_estate_listing           31/82 通过 (37.8%)
  test__recipe                        3/70 通过 (4.3%)
  test__rental_car_booking            27/79 通过 (34.2%)
  test__scientific_experiment         13/69 通过 (18.8%)
  test__travel_itinerary              21/81 通过 (25.9%)

常见错误:
  7228次 缺失必填字段
  738次 项目数量错误
  540次 类型不匹配
  317次 未闭合的代码块
  190次 多余字段 'notes'
  181次 多余字段 'path'
  175次 多余字段 'constraints'
  170次 多余字段 'type'
  170次 缺少代码块
  100次 期望 Bare list,实际返回 Wrapper

IFStruct 官方博文指出 LFM2.5-350M 的通过率为 21.1%。我们在本地 llama.cpp/BF16 环境下的测试结果为 22.6%,与博文数据基本吻合。后续将以该本地结果作为同推理服务栈对比的基准线。

使用 TRL 对结构化输出进行 GRPO 微调

完整的可运行流水线见配套笔记本,本节仅介绍相关部分。

训练数据

我们使用 nvidia/Nemotron-RL-instruction_following-structured_outputs,该数据集将每条提示词与目标 JSON Schema 和预期字段数配对。我们选取约 500 条样本进行训练。

由于 Nemotron 的数据分布与 IFStruct 评估存在差异,我们对提示词做了增强以弥合两者间的两处 gap:

  • 40% 的样本追加了"将输出放在围栏代码块中"的指令,让模型学会遵循格式指令,而不是总是直接输出裸 JSON。
  • 另外 20%(与前一组不重叠)被转换为顶层数组任务(schema 被包裹在带有必填 item count 的 array 中),从而训练裸列表输出和 item 数量合规性。

模型与 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],用于衡量每个 completion 中 结构 是否正确:

  • 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]

    训练

    针对免费级16GB GPU的显存配置,我们以100步完成训练,每组提示词采样8个生成结果:

    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,  # 每步优化器处理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,
    )
    

    从 Notebook 运行结果可见,整个训练过程中三项奖励指标均稳步上升,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)
    

    IFStruct 评估(GRPO 微调后的 LFM2.5-350M)

    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
    
    ============================================================
    模型:lfm25-350m-grpo-structured-output
    ============================================================
    总体:2000 个通过 594 个(29.7%)
    平均延迟:1518ms
    
    按格式分类:
      JSON:1000 个通过 319 个(31.9%)
      YAML:1000 个通过 275 个(27.5%)
    
    按顶层结构分类:
      Wrapper key 1011 个通过 300 个(29.7%)
      裸列表   989 个通过 294 个(29.7%)
    
    按实体类型分类:
      test__camera_review                 83 个通过 5 个(6.0%)
      test__clinical_trial                104 个通过 31 个(29.8%)
      test__conference_schedule           87 个通过 11 个(12.6%)
      test__escaping__bug_report_batch    89 个通过 32 个(36.0%)
      test__escaping__config_snippet_audit 85 个通过 24 个(28.2%)
      test__escaping__customer_email_thread 73 个通过 9 个(12.3%)
      test__escaping__dialogue_sample     95 个通过 17 个(17.9%)
      test__escaping__interview_transcript_segment 80 个通过 13 个(16.2%)
      test__escaping__log_parser_examples 72 个通过 33 个(45.8%)
      test__escaping__pr_discussion       87 个通过 26 个(29.9%)
      test__escaping__repro_steps_batch   73 个通过 23 个(31.5%)
      test__escaping__screenplay_scene    92 个通过 34 个(37.0%)
      test__escaping__short_story_chapter 84 个通过 24 个(28.6%)
      test__escaping__support_ticket_batch 73 个通过 36 个(49.3%)
      test__escaping__terminal_session_notes 70 个通过 23 个(32.9%)
      test__event_ticket_booking          107 个通过 62 个(57.9%)
      test__gpu_review                    94 个通过 7 个(7.4%)
      test__invoice                       86 个通过 36 个(41.9%)
      test__job_posting                   85 个通过 33 个(38.8%)
      test__real_estate_listing           82 个通过 32 个(39.0%)
      test__recipe                        70 个通过 7 个(10.0%)
      test__rental_car_booking            79 个通过 37 个(46.8%)
      test__scientific_experiment         69 个通过 14 个(20.3%)
      test__travel_itinerary              81 个通过 25 个(30.9%)
    
    常见错误:
      7331次 缺少必填字段
      890次  条目数量错误
      555次  类型不匹配
      102次  期望裸列表,实际为 wrapper
       62次  多余字段 'metadata.tone'
       55次  6 大于最大值 5
       49次  多余字段 'speaker_labels'
       47次  多余字段 'tone'
       44次  'cups' 不在允许值中 ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'
       44次  多余字段 'notes'
    

    对比两组在相同服务堆栈上的运行结果:

    IFStruct 分组 base GRPO 微调 Δ
    总体 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通过率提升近14个百分点(18.0% → 31.9%),YAML基本持平。尽管仍低于 Qwen3.5-2B 的 33.15%,但这表明即便是轻量级的任务专用微调,也能让小模型逼近大模型的表现。

    结论

    仅用约500条样本、100步的短轮GRPO训练,就能将350M参数模型在IFStruct上的得分从22.6%提升至29.7%。核心启示在于:廉价的、面向任务的奖励信号可以让小模型在格式规范性上获得显著提升,大幅缩小其与体积数倍于己的大模型之间的差距。

    如需复现或扩展本工作,请参考原始 IFStruct v1.0 博客文章Liquid4All/ifstruct 基准仓库,以及 LiquidAI/ifstruct-v1.0 数据集。

    原始来源: HuggingFace博客

    评论 (0)