Глава 12 из 17 35 мин
Учим разговаривать
Как превратить продолжение текста в ответ собеседнику? Подготовим разговоры, разберём служебные метки и маску ошибки, а затем посмотрим, что изменилось после дообучения.
В этой главе
- понять, почему базовая модель не отвечает на вопросы, — и хитростью заставить её ответить
- собрать шаблон чата токен за токеном и увидеть, за какие именно токены модель получает оценку
- разобраться, что меняет дообучение с учителем, как устроены наши данные и чего дообучение добавить не может
Попробуйте спросить базового Ростка: «Why is the sky blue?» Вместо ответа он может придумать ещё один вопрос, начать историю или продолжить сценку с двумя персонажами. Иногда он всё же ответит — в корпусе были и диалоги. Но его учили предсказывать продолжение документа, а не последовательно выполнять роль вашего собеседника. Теперь у нас другая задача: сделать так, чтобы после вопроса пользователя чаще появлялся уместный ответ ассистента.
Для этого не потребуется менять архитектуру модели. Возьмём уже обученные веса и продолжим обучение на специально подготовленных разговорах. Кроме данных изменится способ подсчёта ошибки: оценивать будем ответы ассистента. Такой этап называется дообучением с учителем (supervised fine-tuning, SFT). Это распространённый способ подготовить языковую модель к чату; у Ростка для него есть около двадцати тысяч записей с учётом повторов.
Рассказчик не отвечает
Ниже один и тот же вопрос задают двум версиям Ростка. Слева — базовая модель из главы 10, справа — та же модель после дообучения на разговорах. Базовую модель можно спросить тремя способами.
Попробуйте все три:
- Просто вопрос. Базовая модель считает его началом текста и продолжает: ещё одна реплика, слова рассказчика, иногда целая история. Ничто в её обучении не говорило, что за вопросом в начале документа должен идти ответ.
- Как сценка. Здесь вопрос обёрнут в маленькую сцену: «Anna talks to her friend Sprout. Anna: … Sprout:». Таких сценок Росток немало прочитал среди диалогов SODA (глава 9), а в сценарии строчка после «Sprout:» — естественно, ответ. Часто это работает на удивление хорошо. Этот приём — переодеть задачу в документ, естественным продолжением которого будет ответ, — был главным способом пользоваться языковыми моделями до появления чат-моделей: именно так почти всегда и работали с GPT-3 (Браун и соавторы, 2020).
- В шаблоне чата. Вопрос упакован в те же служебные метки, что получает чат-модель. Базовая модель их никогда не видела и теряется: часто начинает с середины слова («es can be bright…», «holders, I'm Sophia»), а потом уходит в историю или в разговор незнакомцев, мало связанный с вопросом.
Главное происходит справа: дообученная модель отвечает. А ведь у неё та же архитектура, те же 17,31 миллиона параметров, и дополнительного текста она прочитала совсем немного. Базовая модель уже знала грамматику, устройство диалогов и простые факты детского мира. Ей не хватало формы и роли: понимания, что она — одна из сторон разговора и должна отвечать другой.
Сценарий со служебными метками
Приём со сценкой подсказывает решение: превратить разговор в документ фиксированной формы и учить модель на множестве таких документов. Нужна только форма, которую не спутаешь с обычным текстом. «Anna:» и «Sprout:» могут встретиться в любой истории; нужны метки, которых в обычном тексте не бывает.
Для этого и существуют служебные токены. Ещё в главе 6, когда строили токенизатор, мы отложили четыре номера в самом конце словаря: с 8188 по 8191. До сих пор работал только один из них, <|endoftext|>, — он разделял документы при предобучении. Теперь в дело вступают остальные три:
<|user|>— здесь начинается реплика человека;<|assistant|>— здесь начинается реплика Ростка;<|end|>— текущая реплика закончилась.
Весь разговор превращается в одну строку токенов:
<|endoftext|><|user|>hi!<|end|><|assistant|>Hello! How is your day going?<|end|><|user|>…
Чтобы поговорить, мы собираем такую строку из всего разговора, дописываем в конец <|assistant|> и даём модели писать, пока она не выдаст <|end|> — стоп-токен из прошлой главы. Ровно так работает каждое окно чата на этом сайте. Соберите свой разговор и посмотрите, как он превращается в токены:
А что происходит в длинном разговоре?
В нашем чате при отправке сообщения доступная история снова собирается в последовательность токенов. Именно она хранит содержание разговора: веса модели от переписки не меняются. У Ростка контекст вмещает 512 токенов, поэтому для начала очередного ответа чат оставляет последние реплики в пределах бюджета 440 токенов. Более ранний текст отбрасывается — отсюда забытые имена и договорённости в длинной беседе. У других систем окна и способы работы с историей бывают иными: например, старую переписку могут сжимать в сводку или искать в отдельном хранилище.
Вот функция из sft.py, которая превращает разговор в материал для обучения. Кроме номеров токенов она строит второй список той же длины — маску: 1 там, где модель должна научиться угадывать токен, и 0 там, где она только читает.
Параметр allow_special=False не даёт обычному тексту сообщения превратиться в служебную разметку. Набранная пользователем строка «<|end|>» кодируется как обычный текст; настоящий номер метки вставляет программа. Так мы отделяем содержание реплик от границ и ролей. Это защищает от подделки меток через токенизатор, но не решает проблему prompt injection целиком: нежелательные инструкции можно написать и обычными словами.
Оцениваем только ответы
При предобучении целью был каждый токен: функция потерь — средняя неожиданность по всем позициям. При дообучении мы умножаем неожиданность в каждой позиции на маску и усредняем только по тем позициям, что идут в зачёт:
$$\mathcal{L} \;=\; \frac{\sum_t m_t \cdot \big(-\ln p_\theta(x_t \mid x_{\lt t})\big)}{\sum_t m_t}, \qquad m_t \in \{0, 1\}.$$Модель по-прежнему читает каждый токен: слова пользователя проходят через сеть и влияют на предсказание ответа. Маска убирает прямую оценку предсказания пользовательских токенов, но градиенты от ответа могут проходить через вычисления, связанные с вопросом. Поэтому фраза «учим только на ответах» описывает функцию потерь, а не запрет учиться обрабатывать вход. Почему мы выбрали такую маску?
- Цель — ответы ассистента. Маска направляет прямой сигнал ошибки на ту часть разговора, которую модель будет генерировать при использовании.
- Вопрос уже дан. При генерации модель получает его как условие, а не пишет вместо пользователя. Поэтому отдельно предсказывать его токены для нашей задачи необязательно.
- Управление долями. Длинные вопросы не увеличивают свой вклад в ошибку только за счёт длины. Обучение без такой маски тоже возможно; лучший вариант зависит от данных и задачи.
Роли служебных токенов здесь различаются. <|assistant|> вставляет программа, поэтому в нашей функции потерь его предсказание не оценивается. А <|end|> в конце ответа оценивается вместе с ответом: по этим примерам модель учится выдавать сигнал завершения. Если его исключить, мы уберём прямой обучающий сигнал для своевременной остановки; внешний лимит длины всё равно сможет прервать генерацию.
Вот вся идея на игрушечном примере. Вероятности выдуманы; посмотрите, как слово пользователя «sprout», которого модель совсем не ждала, тянет обычное среднее вверх — и как маска его убирает.
В model.py Ростка то же самое занимает две строчки в конце forward: кросс-энтропия считается для каждой позиции с reduction='none', умножается на маску и делится на число единиц.
При таком дообучении входом служит весь разговор, а ошибку считают по ответам ассистента и их завершающим меткам. Вопросы остаются важными: от них зависит, какой ответ модель должна предсказать.
Двадцать тысяч разговоров
Шаблон и маска — это механизм. Какой станет модель, решают разговоры, которыми мы её кормим. В коллекции Ростка их 19 734 из четырёх источников:
- 7 217 разговоров, написанных под уровень Ростка, на 18 тем: болтовня, природа, наука, быт, истории, учёба, друзья, сам Росток, мир вокруг, разговоры с опорой на предыдущие реплики, техника, прошлое, «как сделать», развлечения, здоровье, планета, простые объяснения и всякая всячина. Все они написаны по одним правилам: короткие предложения, слова, понятные десятилетнему, простой текст без списков и эмодзи — и честность насчёт своих пределов. Росток говорит, что он маленький, что понимает только английский, не знает сегодняшней даты и новостей и может ошибаться. Каждый разговор входит в смесь дважды, а 400 разговоров о самом Ростке — четырежды: модель должна твёрдо усвоить, кто она.
- 1 200 бытовых разговоров из открытого набора everyday-conversations — более длинные диалоги с ассистентом.
- 2 500 просьб рассказать историю. Просьба называет героя истории из TinyStories («Tell me a story about a little fox»), а ответ — сама эта история, не длиннее 170 слов.
- 800 просьб «продолжи историю»: в просьбе — одно-два первых предложения истории из TinyStories, в ответе — всё остальное.
Переключите полоску на «по токенам Ростка» — и картина меняется. Разговоры, написанные для Ростка, — это 77% всех разговоров, но лишь половина оцениваемых токенов: они короткие, в среднем 64 токена, а просьба об истории в среднем занимает 178. Функция потерь усредняется по оцениваемым токенам, поэтому каждый источник тянет модель к себе пропорционально своей доле оцениваемых токенов, а вовсе не разговоров.
На этой арифметике мы и споткнулись. В первой попытке историй было 5 000, длиной до 230 слов. Дообученная модель говорила гладко и вежливо, но на «I feel a bit sad today» отвечала сказкой. Неудивительно: истории составляли самую большую долю всего, за что её оценивали, и «ответить историей» стало её поведением по умолчанию. Историй пришлось оставить вдвое меньше, и только короткие. Потом нашлась вторая, более коварная ловушка. Первые просьбы об историях мы собирали из меток SimpleStories вроде «bygone eras» или «an anti-hero», а сами истории совпадали с ними лишь отдалённо. На «a story about a brave little fox» модель бодро рассказывала про девочку, которая печёт торт: она выучила, что история не обязана подходить к просьбе. Мы пересобрали просьбы из героя самой истории («Once upon a time, there was a little fox…» превращается в «Tell me a story about a little fox»), и лисёнок вернулся. (Спросите «I feel a bit sad today» в первом виджете и посмотрите, как с этим справляется итоговая модель.) Смешивание данных — одно из самых незаметных и самых важных решений при создании языковой модели.
Вот как собирается коллекция. conversation() проверяет, что разговор начинается с человека, заканчивается ассистентом и реплики чередуются; story_requests() находит героя в первой фразе истории и просит рассказать именно её:
Всего в разговорах 1,8 миллиона токенов, и 75% из них — реплики Ростка. Сравните с 330 миллионами токенов предобучения: данные для дообучения — около половины процента того, что модель уже прочитала.
Само дообучение
sft.py — близкий родственник train.py из главы 10. Он загружает веса базовой модели вместо случайных, а дальше та же модель, те же оптимизаторы (Muon для матриц, AdamW для остального) и тот же прямой проход, только теперь с маской. Отличаются масштаб и осторожность:
- шаг обучения впятеро меньше, чем при предобучении (это значения по умолчанию в
sft.py): 0,004 вместо 0,02 для матриц и 0,0008 вместо 0,004 для эмбеддингов. Модель уже многое знает, и большими шагами её легко растоптать; - по умолчанию данные показываются трижды (три эпохи) батчами по 32 строки, с 20 шагами разогрева и линейным затуханием шага обучения во второй половине;
- 3% разговоров откладываются: на них не учатся, а только измеряют функцию потерь, чтобы видеть, учится ли модель разговаривать или просто зубрит примеры.
Разговоры бывают разной длины, а учится модель на строках фиксированной длины. pack() склеивает разговоры один за другим в строки ровно по 513 токенов: 512 входов плюс один лишний, потому что цели — это та же строка, сдвинутая на одну позицию. Разговор, который не влез, начинает новую строку, а свободное место в конце заполняется пустышками, которых маска не замечает.
А вот та же упаковка прямо в вашем браузере — на разговорах из виджета с данными выше:
Разговоры у Ростка короткие: медиана — 66 токенов, самый длинный — 359, так что выбрасывать не приходится ни одного. Обучающая часть укладывается примерно в 3 900 строк, заполненных почти на 90%; с настройками по умолчанию это около 370 шагов. Рядом с 10 070 шагами предобучения — лёгкое касание.
Сколько эпох? Слишком мало — новая форма не закрепится; слишком много — модель начнёт заучивать конкретные разговоры вместо того, чтобы учиться разговаривать. Судьёй здесь служат отложенные разговоры: пока функция потерь на них падает, модель обобщает; если она пошла вверх, а на обучающих данных продолжает падать, — модель заучивает. Вот настоящий запуск дообучения Ростка:
Что изменилось внутри
Кривая обучения — это среднее. Чтобы рассмотреть перемену вблизи, дадим обеим моделям один и тот же разговор, которого ни одна из них не видела, и измерим, насколько каждая удивляется каждому токену в ответе Ростка. Именно эту величину дообучение и уменьшает.
Бросаются в глаза две вещи. Обычные слова ответа становятся менее неожиданными, особенно первые: базовая модель не ждёт, что сразу после <|assistant|> начнётся ответ, — она этот токен никогда не видела. А сильнее всего меняется последний <|end|>. Для базовой модели это токен, который ни разу не встречался в обучающих данных, и его вероятность исчезающе мала. После дообучения Росток ждёт конца реплики в нужный момент: в первых двух разговорах он даёт <|end|> больше 99%. Любопытное исключение — лягушка. После истории всего из двух предложений чат-модель ждёт продолжения (истории, на которых её учили, тянулись на абзац-другой), и <|end|> получает лишь несколько шансов из ста тысяч. Но и это в тысячу раз больше, чем даёт ему базовая модель.
Почему базовая модель считает <|end|> почти невозможным
Для softmax и кросс-энтропии производная по логиту $z_j$ равна $p_j-y_j$. Если токен никогда не бывает правильной целью, $y_j=0$ и прямой сигнал от функции потерь всегда направлен на уменьшение его оценки. Так при предобучении обстоит дело с чат-метками. Но логиты не являются независимыми обучаемыми параметрами: они получаются из общих весов, поэтому каждый реальный шаг оптимизатора не обязан уменьшать каждый такой логит. Кроме того, базовая модель не тренировалась читать эти метки на входе. Дообучение впервые даёт ей примеры их использования в разговоре, а для завершающего <|end|> — ещё и положительные целевые примеры.
Сами веса сдвинулись совсем чуть-чуть: несколько сотен шагов с маленьким шагом обучения. А поведение изменилось полностью. Так дообучение обычно и выглядит: небольшой толчок в пространстве весов — и большая перемена в поведении.
Почему <|end|>, закрывающий реплику Ростка, оценивается, а <|assistant|> — нет?
В разговоре наш код сам дописывает <|assistant|>, передавая слово Ростку, так что угадывать его бессмысленно. А конец ответа — собственное решение Ростка, поэтому мы включаем эту метку в целевые токены. Внешний лимит длины может остановить генерацию независимо от неё.
Чего дообучение не умеет
Что именно модель усваивает при дообучении? В нашем случае особенно заметны формат ответа, тон и момент остановки. В работе LIMA (Чжоу и соавторы, 2023) модель LLaMA на 65 миллиардов параметров дообучили на тысяче тщательно подобранных примеров и получили хорошие оценки ответов. Авторы предложили гипотезу: при сильной базовой модели значительная часть работы такого дообучения — научить её пользоваться уже приобретёнными возможностями в нужном формате. Это гипотеза об их условиях, а не утверждение, будто дообучение вообще не способно добавлять знания или навыки.
Новые знания при дообучении усвоить можно, но небольшой набор бесед не превращает Росток в надёжный калькулятор. Попробуйте «What is 17 times 23?»: правильный результат — 391. В записанных пробах модель давала вместо него уверенно оформленные, но неверные числа. Правильная форма ответа и правильное содержание — разные достижения. Под галлюцинацией обычно понимают правдоподобно звучащий, но неверный или не подтверждённый контекстом вывод. Обучение на примерах признания своих ограничений помогает, однако не гарантирует распознавания каждой ошибки. Гудибанде и соавторы (2023) тоже обнаружили, что в их опытах имитация ответов большой модели лучше переносила стиль, чем фактическую точность. Поэтому разговорные навыки Ростка и правильность его ответов нужно оценивать отдельно.
Дообучение меняет модель на новых примерах: может настроить формат и стиль, а может добавить знания и навыки. Но уверенный тон после дообучения ещё не означает, что модель умеет надёжно решать задачу.
Росток сейчас
Вот тот Росток, которого мы растили всё это время: те же 17,31 миллиона параметров, теперь дообученные вести разговор. Поздоровайтесь, спросите про животных или погоду, попросите историю, расскажите, как прошёл ваш день. Он маленький, поэтому иногда путает факты или теряет нить в длинном разговоре, и понимает только английский. В следующей главе вы научитесь дообучать его сами и дадите ему собственный стиль, не переучивая все 17 миллионов чисел.