← LLM fine-tuning: LoRA, QLoRA, and when not to do it
02 / 07 · 50 min

Dataset curation: 80% of the work

Where examples come from, why a thousand curated ones beat a hundred thousand random ones, chat templates and loss on responses only — and a filtering pipeline with a held-out split before training starts.

Lesson content is currently available in Russian.

What you will learn

  • →Собирать примеры из продакшен-логов, экспертной разметки и фильтрованной синтетики.
  • →Форматировать данные chat-шаблоном целевой модели и считать loss только по токенам ответа.
  • →Строить конвейер фильтрации: дедупликация, деконтаминация, проверка длин, held-out сплит.

Качество бьёт количество

Интуиция «чем больше данных, тем лучше» в SFT работает против вас. Работа LIMA показала: 65B-модель, дообученная всего на 1000 тщательно отобранных примеров, конкурирует с моделями, выровненными на порядки большими объёмами. Причина в том, что SFT не добавляет знаний — он показывает модели формат и стандарт ответа. Тысячи посредственных примеров учат посредственности с той же прилежностью, с какой сотни отличных учат отличному: модель усвоит и ваши канцеляризмы, и ошибки разметчиков, и однообразие формулировок.

Для узкого навыка рабочий диапазон — 500–5000 примеров. Важнее объёма два свойства. Первое — качество каждого примера: ответ в датасете должен быть ровно таким, какой вы хотите видеть в проде, без «сойдёт». Правка одного плохого примера ценнее добавления сотни средних. Второе — разнообразие: примеры должны покрывать реальные кластеры сценариев, включая граничные случаи и корректные отказы. Датасет из одних счастливых путей даст модель, которая уверенно галлюцинирует на всём остальном.

Практическое следствие: бюджет времени распределяется как 80/20 в пользу данных. Команды, которые неделями крутят гиперпараметры на сыром датасете, получают стабильно худший результат, чем те, кто один раз прогнал обучение с дефолтами, но трижды переработал данные по итогам чтения генераций.

Три источника примеров и фильтрация

Примеры берутся из трёх источников, у каждого свой профиль риска:

  • Продакшен-логи — лучший источник: реальное распределение входов. Отберите диалоги с положительным сигналом (оценка пользователя, доведённая до конца задача) и перепишите слабые ответы до эталона.
  • Экспертная разметка — дорого, но это золотой стандарт для ядра датасета и всех граничных случаев: отказов, эскалаций, редких сценариев.
  • Синтетика от сильной модели — дёшево и быстро масштабируется, но даёт однообразие формулировок и наследует чужие паттерны. Синтетику всегда пропускайте через фильтр: LLM-судья по рубрике плюс ручная выборочная проверка 10%.

Дальше конвейер фильтрации. Дедупликация: точные и почти-дубликаты (MinHash или косинус эмбеддингов выше 0,95) выбрасываются — сто копий одного сценария смещают модель к нему. Деконтаминация: ни один пример не должен пересекаться с будущим eval-набором, иначе оценка превратится в проверку памяти. Проверка длины: распределение длин ответов в датасете — это распределение длин, которые модель будет выдавать; аномально длинные и короткие хвосты режутся осознанно.

И главное правило: held-out набор откладывается до обучения, из того же распределения, и не трогается ни при какой доработке датасета. Это ваша единственная честная линейка на весь проект.

Chat-шаблон: невидимая причина сломанных моделей

Инструктная модель обучена не на сыром тексте, а на диалогах, размеченных служебными токенами. У Qwen и многих других это ChatML: сообщение оборачивается в <|im_start|>role … <|im_end|>. У Llama 3 — свой формат с <|start_header_id|>. Токенизатор знает правильную разметку: tokenizer.apply_chat_template(messages) превращает список сообщений с ролями system/user/assistant в точную строку обучения.

Здесь закопаны три классических провала. Первый — самодельный шаблон: если при обучении склеивать диалоги «своим» форматом, а на инференсе использовать стандартный (или наоборот), модель тихо деградирует — без ошибок в логах, просто хуже отвечает. Шаблон обязан побайтово совпадать между обучением и инференсом. Второй — обучение на токенах промпта: если loss считается по всей последовательности, половина ёмкости обучения уходит на имитацию вопросов пользователя. Нужен режим completion-only: в TRL это assistant_only_loss или collator, маскирующий всё до ответа ассистента. Третий — потерянный EOS: если примеры не заканчиваются токеном конца последовательности, модель не научится останавливаться и будет генерировать до лимита.

Перед запуском обучения распечатайте три-пять примеров после apply_chat_template и прочитайте их глазами: роли на месте, спецтокены правильные, EOS в конце. Эти пять минут экономят GPU-часы на переобучение.

JSONL и проверка перед стартом

Канонический формат хранения — JSONL, по одному диалогу на строку:

{"messages": [{"role": "system", "content": "Ты — ревьюер..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}

Минимальный скрипт проверки перед обучением:

from datasets import load_dataset
from transformers import AutoTokenizer

ds = load_dataset("json", data_files="train.jsonl", split="train")
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

for i in range(3):
    print(tok.apply_chat_template(ds[i]["messages"], tokenize=False))

lengths = [len(tok.apply_chat_template(m, tokenize=True)) for m in ds["messages"]]
print(max(lengths))  # влезает ли в max_seq_length?

Сплит: отложите 10% (или минимум 50–100 примеров) в holdout.jsonl до первого запуска обучения, стратифицированно по типам сценариев. Проверьте пересечения: ни один пример held-out не должен встречаться в train даже в перефразированном виде — прогоните поиск ближайших соседей по эмбеддингам и просмотрите топ совпадений руками.

Итоговый чек-лист перед стартом: дубликаты удалены, шаблон совпадает с целевой моделью, loss только по ответам, EOS на месте, held-out отложен, максимальная длина не превышает контекст обучения.

Practice

Датасет на 300 примеров

Соберите датасет для узкого навыка из вашего домена: минимум два источника (логи или ручные примеры плюс отфильтрованная синтетика), конвейер дедупликации и деконтаминации, форматирование chat-шаблоном целевой модели.

Deliverable

Файлы train.jsonl и holdout.jsonl, скрипт фильтрации и отчёт: сколько примеров отсеяно на каждом шаге конвейера и почему.

Checkpoint

Покажите свой конвейер подготовки данных и обоснуйте каждый шаг фильтрации.

  • ✓ Датасет прошёл дедупликацию и деконтаминацию против held-out набора.
  • ✓ Примеры отформатированы chat-шаблоном целевой модели, loss считается только по токенам ответа.
  • ✓ Held-out отложен до обучения и не пересекается с train ни по одному примеру.

Check yourself

Главный вывод работы LIMA для практика SFT?

01Главный вывод работы LIMA для практика SFT?

Зачем считать loss только по токенам ответа ассистента?

02Зачем считать loss только по токенам ответа ассистента?

Чем опасно несовпадение chat-шаблона между обучением и инференсом?

03Чем опасно несовпадение chat-шаблона между обучением и инференсом?

Что произойдёт, если в примерах обучения нет токена EOS?

04Что произойдёт, если в примерах обучения нет токена EOS?

Когда правильно откладывать held-out набор?

05Когда правильно откладывать held-out набор?

Sources

Dataset curation: 80% of the work · LLM fine-tuning: LoRA, QLoRA, and when not to do it · VORCL