Aivaro
  • Оглавление
  • Вопросы
  • Практика
  • Вики
  • Материалы сообщества
  • Тесты
  • Поиск
✈Telegram @ai_varo
RUEN中文
…
Оглавление/Вопросы/#975

Какую функцию потерь использовать для 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 (для простоты):

Позиция0123456789
input_ids42981575163387000
tokensКакойсегоднядень?Сегодняпонедельник.[PAD][PAD][PAD]

Здесь первые 4 токена — промпт, следующие 3 — ответ, последние 3 — паддинг.

Тогда labels должны быть:

Позиция0123456789
labels-100-100-100-100163387-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
  • Несколько сотен диалогов (генерация через шаблон)

Шаги:

  1. Создать датасет в формате {"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}.
  2. Препроцессинг: токенизировать, сформировать input_ids = concat(prompt_tokens, answer_tokens) + padding до max_length.
  3. Построить labels: copy input_ids, заменить все токены промпта и паддинга на -100 (промпт: позиции до длины промпта; паддинг: позиции после attention_mask).
  4. Задать DataCollatorForLanguageModeling с mlm=False (или написать свой коллатор, возвращающий labels с -100).
  5. Обучить модель с помощью Trainer, используя стандартный CrossEntropyLoss (он встроен в CausalLM).
  6. Оценить: до обучения модель генерирует мусор, после — отвечает осмысленно.

Ожидаемый результат:

  • Потери (eval loss) снижаются на валидационной выборке.
  • При генерации с промптом модель выдает связный ответ, при этом в логе обучения не видно штрафа за неточное воспроизведение промпта.

Связь с другими вопросами

ВопросТема
656Как работает fine-tuning LLM: общий процесс, заморозка слоёв, выбор гиперпараметров

Навигация

  • Предыдущий: 974
  • Следующий: 976
  • Индекс: 00. Индекс разборов