Введение

В современной разработке AI-агентов возникает необходимость адаптации больших языковых моделей (LLM) для решения специфических задач, требующих не просто генерации текста, а выполнения последовательных действий с рассуждениями. В этой статье мы рассмотрим и сравним два основных подхода к настройке моделей: Supervised Fine-Tuning (SFT) и Reinforcement Learning (RL), используя библиотеку TRL (Transformer Reinforcement Learning) от Hugging Face. Мы рассмотрим применение этих методов для двух категорий задач:

Задачи программирования:

  • Нахождение подходящего инпута для программы

  • Логические задачи на дискретную математику (SAT, задачи на графы)

  • Написание кода по описанию задачи (должны проходить тесты)

Игровые задачи с рассуждениями:

  • Игры с навигацией по сетке, такие как, например, эта (ссылка) — платформа для изучения эффективности обучения с подкреплением в контексте grounded language learning. Данная среда предоставляет 19 уровней возрастающей сложности, где агенту нужно выполнять инструкции на естественном языке (например, "put the red ball next to the box on your left"). В оригинальной статье авторы использовали PPO для обучения агентов, показывая, что текущие методы глубокого обучения недостаточно sample-efficient для обучения языку с композиционными свойствами.

  • Игры на получение сложных вложенных рецептов — среды, где требуется выполнить последовательность действий для достижения цели: собрать ресурсы, использовать инструменты, комбинировать предметы в правильном порядке. Эти задачи требуют многошагового планирования и понимания иерархии зависимостей между действиями. Агенту нужн�� не просто выполнить действия, а оптимизировать путь к цели, избегая лишних шагов.

В качестве базовой модели будем использовать Qwen-4B (instruct или thinking версии), так как она достаточно быстро тюнится и демонстрирует хорошие результаты на подобных задачах.

Выбор типа модели: Reasoning vs Instruct

Для задач, требующих многошагового рассуждения, наилучшим образом подходят reasoning модели (thinking models), которые явно генерируют процесс рассуждения перед финальным ответом. Однако их тюнинг сложнее, так как требует генерации reasoning-части для обучающих данных. Instruct модели тоже могут показывать результаты, но это скорее будет оверфит: модель не обучается "думать", а обучается генерировать токены на основе тренировочного датасета. Если задача — выбить максимальный скор и все данные известны, то можно, конечно, по-жесткому оверфитнуться и сидеть радоваться. Но для обобщения на новые задачи и среды reasoning модели предпочтительнее. Важно отметить, что для различных моделей может отличаться формат Chat-ML темплейта. Например, для моделей семейства GPT-OSS он отличается от того, что используется в большинстве моделей.

SFT vs RL: Общее сравнение

Библиотеки и фреймворки

Для упрощения в основном будем рассматривать библиотеку TRL (Transformer Reinforcement Learning), но существуют и альтернативы:

  • Stable-Baselines3 — для классического RL с последовательными действиями

  • Ray RLlib — распределенное обучение RL

  • Custom frameworks — специализированные фреймворки

В кастомных фреймворках для RL обычно используются несколько моделей:

Цель оптимизации

Мы хотим решить задачу оптимальным образом, то есть за минимально возможное количество шагов. Например, в навигационных задачах не блуждать по всем комнатам карты, поднимая все возможные артефакты, а пойти сразу в нужную, взяв нужный ключ, если комната закрыта, и взять после этого желтый мяч. Мы не хотим городить "ансамбль пней", а хотим одну единственную модель без костылей, чтобы хорошо решала всё (обобщалась, скажем, на 10 различных сред) и была совместима с библиотеками для инференса (vLLM, SGLang). Поэтому добавить какой-нибудь дополнительный слой в существующую модель будет проблематично, а скорее невозможно.

Исходные данные

Мы полагаем, что у нас есть среды, с которыми мы работаем, либо в случае задач на программирование — хороший качественный датасет с ground truth ответами.

Подход к реализации SFT тюнинга

С SFT я знаком достаточно хорошо: совсем недавно с командой сумел взять третье золото в соревновании на Kaggle. Различия тюнинга с SFT можно охарактеризовать следующими основными составляющими:

Thinking модели и reasoning-часть

Для thinking моделей помимо ground truth нам потребуется сгенерировать reasoning часть, что существенно увеличивает время подготовки данных. Сначала его нужно сгенерировать какой-нибудь хорошей большой моделью, добавить в данные для обучения. В темплейте обучение помимо user response (ответ от среды) и assistant response (действие, совершаемое моделью) будет еще и часть thought, аналогично как, например, в Cursor, когда "думающая" модель сначала пишет свои "мысли" — как она поняла задачу и какой план по ее решению составила, а уже затем пишет решение. Поскольку здесь мы берем thinking часть от какой-то большой хорошей модели, например, какой-то большой открытой модели из семейства GPT, то получается, что мы частично ее дистиллируем. Модель само собой должна изначально показывать адекватные, а лучше хорошие, результаты на этой задаче. Иначе придется извращаться дальше и препроцессить полученный сырой reasoning, повышая его качество и пригодность для использования.

Использование различных вариантов адаптеров

LoRA для Qwen-4B

LoRA (Low-Rank Adaptation) — один из самых популярных методов параметрически-эффективного тюнинга. Рассмотрим конфигурацию для Qwen-4B-Instruct и Qwen-4B-Thinking:

python
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, SFTTrainer
import torch

# Конфигурация LoRA для Qwen-4B-Instruct (~1% параметров)
lora_config_instruct = LoraConfig(
    r=8,                    # Rank (низкий для ~1% параметров)
    lora_alpha=16,          # Alpha = 2 * r для баланса
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",  # Attention layers
        "gate_proj", "up_proj", "down_proj"      # MLP layers
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# Конфигурация LoRA для Qwen-4B-Thinking
lora_config_thinking = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM"
)

# Загрузка модели
model_name = "Qwen/Qwen2.5-4B-Instruct"  # или Qwen/Qwen2.5-4B-Thinking
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Применение LoRA
model = get_peft_model(model, lora_config_instruct)
model.print_trainable_parameters()

# Конфигурация обучения
training_args = TrainingArguments(
    output_dir="./qwen4b_lora_output",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    num_train_epochs=3,
    logging_steps=10,
    save_steps=500,
    fp16=False,
    bf16=True,
    gradient_checkpointing=True,
    optim="adamw_torch",
    lr_scheduler_type="cosine",
    warmup_steps=100,
)

# Trainer
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    tokenizer=tokenizer,
    max_seq_length=2048,
    dataset_text_field="text",
    packing=True,
)

Расчет процента тюнимых параметров:

Для Qwen-4B (примерно 4 миллиарда параметров):

  • Каждый LoRA адаптер добавляет: r (input_dim + output_dim) параметров

  • Для attention слоев (q_proj, k_proj, v_proj, o_proj): 8 (4096 + 4096) = 65,536 параметров на слой

  • Для MLP слоев (gate_proj, up_proj, down_proj): 8 (4096 + 11008) = 120,832 параметров на слой

  • Всего слоев в модели: ~32 слоя - Общее количество LoRA параметров: ~(65,536 4 32 + 120,832 3 32) ≈ 40 миллионов параметров

  • Процент: 40M / 4B ≈ *1%**

IA³ для Qwen-4B

IA³ (Infused Adapter by Inhibiting and Amplifying Inner Activations) — более эффективный метод, который работает быстрее LoRA и лучше обобщается:

python
from peft import IA3Config, get_peft_model

# Конфигурация IA³ для Qwen-4B-Instruct
ia3_config = IA3Config(
    task_type="CAUSAL_LM",
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ],
    feedforward_modules=["gate_proj", "up_proj", "down_proj"],
    fan_in_fan_out=False
)

# Применение IA³
model = get_peft_model(model, ia3_config)
model.print_trainable_parameters()

# Остальная конфигурация аналогична LoRA
training_args = TrainingArguments(
    output_dir="./qwen4b_ia3_output",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=1e-5,  # IA³ обычно требует меньший LR
    num_train_epochs=3,
    # ... остальные параметры
)

Поддержка PEFT адаптеров:

Библиотека PEFT поддерживает следующие адаптеры:

  • LoRA — Low-Rank Adaptation

  • IA³ — Infused Adapter by Inhibiting and Amplifying Inner Activations

  • AdaLoRA — Adaptive LoRA с динамическим рангом

  • QLoRA — Quantized LoRA (4-bit)

  • Prefix Tuning — настройка префиксов

  • Prompt Tuning — настройка промптов

  • P-Tuning — параметрический п-tuning

Проблема LoRA с обобщением

Не LoRA-ой единой. Существуют различные варианты адаптеров, и некоторые (или даже многие) из них не только работают быстрее, чем LoRA, но и позволяют модели гораздо лучше обобщаться на различные задачи. С LoRA основная проблема заключается в том, что если до-тюнивать модель для какой-то конкретной задачи из нескольких, на которые эта модель должна обобщаться, то происходит перекос и скоры этой модели на других задачах деградируют сильно даже при малом количестве эпох и параметрах для тюнинга (rank и alpha), даже если процент параметров для тюнинга << 1%. IA³ и другие адаптеры показывают лучшую устойчивость к такому перекосу, что делает их предпочтительными для multi-task обучения.

SFT с Unsloth

В недавнем соревновании Jigsaw - Agile Community Rules Classification мы заняли 5 место. Победитель использовал Unsloth — библиотеку для ускорения обучения LLM.

Unsloth — это оптимизированная библиотека, которая значительно ускоряет процесс SFT обучения за счет:

  • Оптимизированных операций матричного умножения

  • Эффективного управления памятью

  • Автоматической оптимизации графов вычислений

  • Поддержки различных адаптеров (LoRA, QLoRA)

Хотя настройка может быть непростой, Unsloth может ускорить обучение в 2-5 раз, что критично при работе с большими датасетами.

python
from unsloth import FastLanguageModel

# Загрузка модели через Unsloth
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="Qwen/Qwen2.5-4B-Instruct",
    max_seq_length=2048,
    dtype=None,  # Auto detection
    load_in_4bit=True,  # QLoRA
)

# Быстрое применение LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_alpha=16,
    lora_dropout=0.05,
    bias="none",
    use_gradient_checkpointing=True,
    random_state=3407,
)

Подход к реализации RL тюнинга

С RL сложнее, и в нем я ориентируюсь хуже, чем в SFT. По мере усложнения существуют следующие подходы:

PPO, DPO, GRPO

  • PPO (Proximal Policy Optimization) — классический алгоритм для RL, использует clipping для стабильности обучения

  • DPO (Direct Preference Optimization) — обучение на предпочтениях без reward модели

  • GRPO (Group Relative Policy Optimization) — улучшенная версия PPO, использующая относительное ранжирование внутри групп генераций

На описанных выше задачах, да и не только на них, GRPO вроде бы как наилучший. Именно его использовали, чтобы обучить DeepSeek, и разработчики многих сред для тренировки RL моделей ссылаются на него, как на подход, показавший наилучшие результаты в их собственных экспериментах.

И здесь для RL нам, конечно, нужно как можно больше хороших траекторий для обучения.

Основные шаги RL тюнинга

В целом тюнинг RL модели у меня имел следующие основные шаги:

  1. Начал с простого PPO пайплайна на TRL и постепенно двигался в сторону GRPO: получаем запрос от среды, делаем шаг, получаем ответ от среды (плюс возможно промежуточный reward), делаем следующий шаг

  2. Писал свой reward function — критически важно правильно определить функцию награды

  3. Экспериментировал с промежуточными rewards — для многошаговых задач важно давать обратную связь на каждом шаге

Пример пайплайна RL с GRPO

python
from trl import GRPOConfig, GRPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import IA3Config, get_peft_model
from datasets import Dataset
import torch

# Загрузка модели
model_name = "Qwen/Qwen2.5-4B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    torch_dtype=torch.bfloat16,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# Применение адаптера (опционально, для экономии памяти)
peft_config = IA3Config(
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    feedforward_modules=["gate_proj", "up_proj", "down_proj"]
)
model = get_peft_model(model, peft_config)

# Функция reward
def reward_function(prompts, completions, **kwargs):
    """
    Вычисляет reward для каждой генерации.
    Для задач программирования: проверка прохождения тестов
    Для игровых задач: оценка прогресса к цели
    """
    rewards = []
    for prompt, completion in zip(prompts, completions):
        # Пример для задачи программирования
        # Извлекаем код из completion
        code = extract_code(completion)
        # Запускаем тесты
        test_results = run_tests(code, test_cases)
        reward = 1.0 if all(test_results) else 0.0
        rewards.append(reward)
    return rewards

# Конфигурация GRPO
training_args = GRPOConfig(
    output_dir="./grpo_output",
    learning_rate=1e-6,
    adam_epsilon=1e-8,
    max_grad_norm=0.1,
    lr_scheduler_type="cosine",
    gradient_accumulation_steps=8,
    loss_type="dr_grpo",
    optim="adamw_torch",
    gradient_checkpointing=True,
    per_device_train_batch_size=1,
    num_generations=4,  # Размер группы для GRPO
    max_steps=10000,
    save_steps=100,
    logging_steps=10,
    report_to="wandb",
    max_prompt_length=2048,
    max_completion_length=1024,
    temperature=0.7,
    fp16=False,
    bf16=True,
)

# Создание trainer
trainer = GRPOTrainer(
    model=model,
    processing_class=tokenizer,
    args=training_args,
    train_dataset=train_dataset,
    reward_funcs=reward_function,
)

# Обучение
trainer.train()
trainer.save_model()

Альтернатива: PPO (если GRPO недоступен)

python
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead

# Добавление value head для PPO
model = AutoModelForCausalLMWithValueHead.from_pretrained(model)

# Конфигурация PPO
ppo_config = PPOConfig(
    output_dir="./ppo_output",
    learning_rate=1e-6,
    batch_size=4,
    mini_batch_size=1,
    gradient_accumulation_steps=8,
    ppo_epochs=4,
    cliprange=0.2,
    cliprange_value=0.2,
    value_loss_coef=1.0,
    max_grad_norm=0.5,
)

ppo_trainer = PPOTrainer(
    model=model,
    config=ppo_config,
    tokenizer=tokenizer,
    dataset=train_dataset,
)

# Обучение
for epoch in range(num_epochs):
    for batch in dataloader:
        # Генерация
        outputs = model.generate(...)
        # Вычисление rewards
        rewards = reward_function(outputs)
        # Обновление
        ppo_trainer.step(rewards)

Сравнение SFT и RL

Аспект

SFT

RL

Сложность реализации

Проще

Сложнее

Требования к данным

Ground truth ответы

Траектории с rewards

Скорость обучения

Быстрее

Медленнее

Обобщение

Зависит от данных

Лучше через exploration

Оптимизация

Минимизация loss

Максимизация reward

Для thinking моделей

Требует reasoning-часть

Генерирует reasoning сам

Sample efficiency

Высокая (если есть данные)

Ниже (нужно много траекторий)

Оптимизация стратегии

Нет

Да (минимизация шагов)

Когда использовать SFT

  • Есть качественный датасет с ground truth

  • Нужно быстро получить рабочую модель

  • Задача относительно простая и не требует оптимизации стратегии - Достаточно данных для покрытия всех случаев

Когда использовать RL

  • Нужно оптимизировать стратегию (минимизировать количество шагов)

  • Есть возможность собирать траектории через взаимодействие со средой

  • Задача требует exploration и поиска оптимальных решений

  • SFT модель уже есть, но нужно улучшить стратегию

Практические рекомендации

Выбор адаптера

Для multi-task обучения, где модель должна обобщаться на множество сред, рекомендуется:

  1. IA³ — лучший баланс скорости и обобщения

  2. AdaLoRA — если нужна адаптивность ранга

  3. QLoRA — если критична память

  4. LoRA — только если задача одна и обобщение не требуется

Стратегия обучения

Рекомендуемый пайплайн:

  1. SFT с IA³ — быстрая настройка на базовых данных

  2. Сбор траекторий — использование SFT модели для генерации траекторий

  3. RL с GRPO — оптимизация стратегии для минимизации шагов

  4. Итеративное улучшение — повторение шагов 2-3 при необходимости

Мониторинг обучения

Важно отслеживать:

  • Для SFT: loss на валидации, метрики качества (accuracy, pass rate)

  • Для RL: средний reward, длина траекторий, KL divergence от reference модели

Заключение

В своих экспериментах я двигался по стратегии достижения хорошего результата с SFT вначале, после чего доучивал SFT модель с помощью RL.

Рекомендуемый пайплайн:

  1. Начальный SFT — быстрая настройка на базовых данных с ground truth

  2. Сбор траекторий — использование SFT модели для генерации траекторий

  3. RL дообучение — оптимизация с помощью GRPO для улучшения стратегии

  4. Итеративное улучшение — повторение шагов 2-3 при необходимости

Такой подход позволяет:

  • Быстро получить рабочую модель (SFT)

  • Улучшить стратегию решения задач (RL)

  • Достичь оптимального баланса между качеством и количеством шагов

Для задач, требующих обобщения на множество сред, предпочтительнее использовать IA³ или другие адаптеры вместо LoRA, так как они показывают лучшую устойчивость к перекосу на конкретные задачи.

Ключевые выводы:

  1. Reasoning модели предпочтительнее для задач, требующих многошагового рассуждения

  2. IA³ показывает лучшее обобщение, чем LoRA для multi-task обучения

  3. Комбинация SFT → RL дает лучшие результаты, чем каждый метод отдельно

  4. GRPO показывает лучшие результаты, чем классический PPO для LLM

  5. Unsloth может значительно ускорить процесс SFT обучения

Для production-систем важно обеспечить совместимость с библиотеками инференса (vLLM, SGLang), что исключает использование дополнительных слоев и требует работы только с параметрами базовой модели через адаптеры.

Комментарии (0)