Дообучение модели на 350 млн. параметров для улучшения структурированных выходных данных за 100 шагов GRPO
Структурированный вывод — одна из самых распространённых практических задач для LLM, однако большинство бенчмарков включают её в более общие оценки рассуждений или извлечения данных, вместо того чтобы измерять отдельно. То, насколько надёжно модель возвращает корректный, пригодный для разбора ответ в требуемом формате и структуре — то есть соблюдает схему, — часто определяет, можно ли вообще подключить её к последующей системе.
Обратите внимание, что описанный здесь конвейер обучения отличается от использованного для обучения RL-модели, описанной в статье об IFStruct. Цель этого ноутбука — не воспроизвести результат бенчмарка IFStruct, а показать, как дообучение небольших моделей под конкретную задачу может улучшить их производительность и приблизить её к показателям гораздо более крупных моделей.
Предварительные требования
Это руководство состоит из двух частей, которые выполняются в разных местах:
- Дообучение выполняется на GPU. Сопроводительный ноутбук рассчитан на бесплатный GPU в Colab или Kaggle.
- Оценивание можно выполнять локально на MacBook (в данном случае — на MacBook Pro с Apple M5 Max и 36 ГБ объединённой памяти) через
llama.cpp, который предоставляет совместимый с OpenAI сервер, используемый оценщиком IFStruct.
Для инструментов Python нам понадобится uv, а для запуска сервера — llama.cpp. Следуя документации Liquid AI по развёртыванию llama.cpp, установите llama.cpp с помощью Homebrew и убедитесь, что llama-server доступен:
brew install llama.cpp
llama-server --version
Оценивание IFStruct на LFM2.5-350M (базовая модель)
Перед началом оценим 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. Мы будем использовать GGUF в формате BF16 (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
============================================================
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%)
By entity type:
test__camera_review 6/83 passed (7.2%)
test__clinical_trial 20/104 passed (19.2%)
test__conference_schedule 7/87 passed (8.0%)
test__escaping__bug_report_batch 24/89 passed (27.0%)
test__escaping__config_snippet_audit 15/85 passed (17.6%)
test__escaping__customer_email_thread 5/73 passed (6.8%)
test__escaping__dialogue_sample 14/95 passed (14.7%)
test__escaping__interview_transcript_segment 21/80 passed (26.2%)
test__escaping__log_parser_examples 21/72 passed (29.2%)
test__escaping__pr_discussion 22/87 passed (25.3%)
test__escaping__repro_steps_batch 16/73 passed (21.9%)
test__escaping__screenplay_scene 16/92 passed (17.4%)
test__escaping__short_story_chapter 15/84 passed (17.9%)
test__escaping__support_ticket_batch 27/73 passed (37.0%)
test__escaping__terminal_session_notes 20/70 passed (28.6%)
test__event_ticket_booking 49/107 passed (45.8%)
test__gpu_review 6/94 passed (6.4%)
test__invoice 28/86 passed (32.6%)
test__job_posting 25/85 passed (29.4%)
test__real_estate_listing 31/82 passed (37.8%)
test__recipe 3/70 passed (4.3%)
test__rental_car_booking 27/79 passed (34.2%)
test__scientific_experiment 13/69 passed (18.8%)
test__travel_itinerary 21/81 passed (25.9%)
Common errors:
7228x required field missing
738x wrong item count
540x type mismatch
317x Unclosed code block
190x extraneous field 'notes'
181x extraneous field 'path'
175x extraneous field 'constraints'
170x extraneous field 'type'
170x missing code block
100x expected bare list, got wrapper
В статье о выпуске IFStruct сообщается результат 21,1% для LFM2.5-350M. В нашей локальной конфигурации llama.cpp/BF16 получено 22,6%, что близко к указанным в статье об IFStruct 21,1%. Этот локальный результат мы используем как базовый для сравнения в том же стеке запуска.
Дообучение GRPO на структурированных ответах с помощью TRL
Полный готовый к запуску конвейер находится в сопроводительном ноутбуке. В этом разделе мы рассмотрим только важные фрагменты.
Данные для обучения
Мы используем nvidia/Nemotron-RL-instruction_following-structured_outputs, где каждому запросу сопоставлены целевая JSON Schema и ожидаемое количество полей. Для обучения мы используем около 500 примеров.
Поскольку распределение данных Nemotron отличается от данных оценки IFStruct, мы дополняем запросы, чтобы устранить два различия между ними:
- Для 40% добавляется инструкция «вернуть вывод внутри блока кода с ограждением», чтобы модель училась соблюдать указание о формате, а не всегда выдавала необработанный JSON.
- Отдельные 20% преобразуются в задачи с массивом верхнего уровня (схема оборачивается в
arrayс обязательным количеством элементов), что обучает выводу простых списков и соблюдению количества элементов.
Модель и 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",
],
)
Обучаются около 6 млн параметров — примерно 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 генераций на группу запросов; конфигурация рассчитана на бесплатный GPU с 16 ГБ памяти:
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,
steps_per_generation=2,
max_completion_length=1024,
mask_truncated_completions=False,
temperature=1.1,
beta=0.01,
reward_weights=[1.0, 0.5, 2.0],
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)
Оценивание 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
============================================================
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%)
By entity type:
test__camera_review 5/83 passed (6.0%)
test__clinical_trial 31/104 passed (29.8%)
test__conference_schedule 11/87 passed (12.6%)
test__escaping__bug_report_batch 32/89 passed (36.0%)
test__escaping__config_snippet_audit 24/85 passed (28.2%)
test__escaping__customer_email_thread 9/73 passed (12.3%)
test__escaping__dialogue_sample 17/95 passed (17.9%)
test__escaping__interview_transcript_segment 13/80 passed (16.2%)
test__escaping__log_parser_examples 33/72 passed (45.8%)
test__escaping__pr_discussion 26/87 passed (29.9%)
test__escaping__repro_steps_batch 23/73 passed (31.5%)
test__escaping__screenplay_scene 34/92 passed (37.0%)
test__escaping__short_story_chapter 24/84 passed (28.6%)
test__escaping__support_ticket_batch 36/73 passed (49.3%)
test__escaping__terminal_session_notes 23/70 passed (32.9%)
test__event_ticket_booking 62/107 passed (57.9%)
test__gpu_review 7/94 passed (7.4%)
test__invoice 36/86 passed (41.9%)
test__job_posting 33/85 passed (38.8%)
test__real_estate_listing 32/82 passed (39.0%)
test__recipe 7/70 passed (10.0%)
test__rental_car_booking 37/79 passed (46.8%)
test__scientific_experiment 14/69 passed (20.3%)
test__travel_itinerary 25/81 passed (30.9%)
Common errors:
7331x required field missing
890x wrong item count
555x type mismatch
102x expected bare list, got wrapper
62x extraneous field 'metadata.tone'
55x 6 is greater than maximum 5
49x extraneous field 'speaker_labels'
47x extraneous field 'tone'
44x 'cups' not in allowed values ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'
44x extraneous field 'notes'
Сравнение двух запусков на идентичном стеке:
| Группа IFStruct | базовая | дообученная GRPO | Δ |
|---|---|---|---|
| Всего | 22.6% | 29.7% | +7.1 |
| JSON | 18.0% | 31.9% | +13.9 |
| YAML | 27.2% | 27.5% | +0.3 |
| Ключ-обёртка | 28.5% | 29.7% | +1.2 |
| Простой список | 16.6% | 29.7% | +13.1 |
Прирост наблюдается именно там, где было направлено обучение: доля успешных ответов в формате JSON увеличивается почти на 14 пунктов (с 18,0% до 31,9%), тогда как результат для YAML в основном не меняется. Хотя это всё ещё ниже, чем результат Qwen3.5-2B — 33,15%, это показывает, что даже лёгкое дообучение под конкретную задачу может приблизить небольшую модель к более крупной.
Заключение
Короткий запуск GRPO примерно на 500 примерах и 100 шагах может повысить результат небольшой модели на 350 млн параметров с 22,6% до 29,7% на IFStruct. Главный вывод заключается в том, что недорогой сигнал вознаграждения, ориентированный на конкретную задачу, способен значительно повысить надёжность небольшой модели в отношении формы ответа, существенно сократив отставание от моделей, в несколько раз превосходящих её по размеру.
Чтобы воспроизвести или расширить эту работу, ознакомьтесь с оригинальной статьёй о IFStruct v1.0, репозиторием бенчмарка Liquid4All/ifstruct и набором данных LiquidAI/ifstruct-v1.0.
Модели, упомянутые в этой статье 2
Наборы данных, упомянутые в этой статье 2
Другие статьи в нашем блоге
Сообщество
· Зарегистрируйтесь или войдите, чтобы оставить комментарий
Модели, упомянутые в этой статье 2
Наборы данных, упомянутые в этой статье 2
Переведено автоматически с английского. Оригинал статьи — по ссылке ниже.