Какую функцию потерь использовать для fine-tuning на диалогах (CrossEntropyLoss с masking падинга)?
Краткий тезис
При fine-tuning языковой модели на диалоговых данных стандартной функцией потерь является CrossEntropyLoss, но с обязательным маскированием двух типов токенов: 1) токенов самого запроса (prompt), чтобы модель училась только генерировать ответ, а не повторять вопрос; 2) токенов padding, чтобы пустые позиции не вносили шум в градиент. Маскирование реализуется через параметр ignore_index, которому присваивается значение -100 (по умолчанию в PyTorch и Hugging Face Transformers). Потери усредняются или суммируются только по значимым токенам ответа.
2. Маскируем потери на prompt (ignore_index)
Маскирование реализуется в большинстве фреймворков (Hugging Face Transformers, PyTorch Lightning, DeepSpeed) через аргумент ignore_index в CrossEntropyLoss. В PyTorch его значение по умолчанию — -100. Если в тензоре меток (labels) стоит -100, потери для этой позиции не вычисляются и не учитываются в сумме/среднем.
Таким образом, для каждого примера в батче мы формируем:
input_ids— полная последовательность[prompt_tokens, answer_tokens, pad_tokens]labels— тензор той же длины, где:- для позиций, соответствующих промпту:
-100 - для позиций ответа: сдвинутые на один токен
answer_tokens(стандартный сдвиг для causal LM) - для позиций padding:
-100
- для позиций, соответствующих промпту:
Этот подход эквивалентен концепции “teacher forcing только на ответе”, где модель видит промпт целиком и каждый следующий токен ответа предсказывается с учётом всех предыдущих токенов ответа (но не видит будущих).
3. Пример: labels = [-100, -100, answer_tokens]
Допустим, мы имеем диалог:
- Пользователь: "Какой сегодня день?"
- Ассистент: "Сегодня понедельник."
После токенизации с максимальной длиной 10 (для простоты):
| Позиция | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| input_ids | 42 | 98 | 15 | 75 | 16 | 33 | 87 | 0 | 0 | 0 |
| tokens | Какой | сегодня | день | ? | Сегодня | понедельник | . | [PAD] | [PAD] | [PAD] |
Здесь первые 4 токена — промпт, следующие 3 — ответ, последние 3 — паддинг.
Тогда labels должны быть:
| Позиция | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
| labels | -100 | -100 | -100 | -100 | 16 | 33 | 87 | -100 | -100 | -100 |
Пояснение:
- Для промпта (поз. 0–3) ставим -100, чтобы потери на предсказание токенов запроса игнорировались.
- Для ответа (поз. 4–6) ставим истинные токены, сдвинутые на 1: в позиции 4 мы ожидаем токен "Сегодня" (id 16), в позиции 5 – "понедельник" (33), в позиции 6 – "." (87). Обратите внимание: для causal LM обычно на вход подают
input_ids, аlabelsсовпадают сinput_ids, но для первых токенов ответа нужно именно предсказание следующего. В нашем примере мы не сдвигаем явно, так как модель предсказывает следующий токен: при предсказании для позиции k используется контекст до k-1. Еслиlabels[k] = answer_token_id, то модель на позиции k должна предсказать токен, который идёт после позиции k. Здесь на позиции 4 ожидается токен "Сегодня", который является первым токеном ответа, и это корректно – модель должна его предсказать, имея контекст из промпта. - Для паддинга (поз. 7–9) снова -100.
Важно также добавить attention_mask для исключения влияния паддинга на self-attention, но это отдельная тема.
4. Суммирование по последовательности
После того как мы вычислили CrossEntropyLoss на каждом токене (с игнорированием -100), возникает вопрос агрегации. По умолчанию в PyTorch CrossEntropyLoss использует reduction='mean' — усреднение по всем non-ignored токенам. Это стандартный выбор, так как среднее значение нормирует потери на количество значимых токенов и делает размер лосса независимым от длины ответа.
Альтернативы:
reduction='sum'— суммирование потерь по всем токенам ответа. Используется реже, но может быть полезно, когда важно штрафовать длинные ответы сильнее коротких. Однако при смешанной длине в батчеsumделает градиенты несбалансированными.reduction='none'— возвращает вектор потерь, который затем можно обработать вручную (например, для анализа вклада отдельных токенов).
В большинстве фреймворков (Hugging Face Trainer, SFTTrainer из TRL) reduction по умолчанию mean. Рекомендуется оставить его, так как он стабилен и хорошо работает с маскированием.
Совмещение с маскировкой: если мы задаём labels с -100 для промпта и паддинга, то CrossEntropyLoss с reduction='mean' автоматически усреднит потери только по токенам ответа. При этом знаменатель — количество токенов ответа (не учитывая -100). Это даёт корректную оценку качества генерации ответов.
Пет-проект для закрепления
Задача: Дообучить маленькую модель (например, DistilGPT2) на синтетических диалогах, реализовав маскирование loss на промпте и паддинге. Убедиться, что модель начинает генерировать осмысленные ответы, а не повторять промпт.
Инструменты:
- Python, PyTorch, Transformers, Datasets
- Базовый
Trainerиз Hugging Face - Несколько сотен диалогов (генерация через шаблон)
Шаги:
- Создать датасет в формате
{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}. - Препроцессинг: токенизировать, сформировать
input_ids= concat(prompt_tokens, answer_tokens) + padding доmax_length. - Построить
labels: copyinput_ids, заменить все токены промпта и паддинга на -100 (промпт: позиции до длины промпта; паддинг: позиции послеattention_mask). - Задать
DataCollatorForLanguageModelingсmlm=False(или написать свой коллатор, возвращающийlabelsс -100). - Обучить модель с помощью
Trainer, используя стандартныйCrossEntropyLoss(он встроен вCausalLM). - Оценить: до обучения модель генерирует мусор, после — отвечает осмысленно.
Ожидаемый результат:
- Потери (eval loss) снижаются на валидационной выборке.
- При генерации с промптом модель выдает связный ответ, при этом в логе обучения не видно штрафа за неточное воспроизведение промпта.
Связь с другими вопросами
| Вопрос | Тема |
|---|---|
| 656 | Как работает fine-tuning LLM: общий процесс, заморозка слоёв, выбор гиперпараметров |
Навигация
- Предыдущий: 974
- Следующий: 976
- Индекс: 00. Индекс разборов