Глава 12 из 14 35 мин
Учим разговаривать
Базовая модель продолжает текст, а не отвечает. Четыре служебных токена, двадцать тысяч разговоров и функция потерь, которая пропускает мимо ушей всё, что пишет человек, превращают рассказчика в собеседника.
В этой главе
- понять, почему базовая модель не отвечает на вопросы, — и хитростью заставить её ответить
- собрать шаблон чата токен за токеном и увидеть, за какие именно токены модель получает оценку
- разобраться, что меняет дообучение с учителем, как устроены наши данные и чего дообучение добавить не может
Спросите Ростка из главы 11 «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|> почти невозможным
Вспомним из главы 3, как softmax и перекрёстная энтропия толкают логиты: производная функции потерь по логиту $z_j$ равна $p_j - y_j$, где $y_j$ — единица для правильного токена и ноль для всех остальных. У токена, который ни разу не встретился в обучающих данных, $y_j$ всегда ноль, значит, производная $p_j$ всегда положительна, и каждый шаг градиентного спуска немного опускает его логит. За десять тысяч шагов предобучения <|user|>, <|assistant|> и <|end|> толкали вниз в каждой позиции каждого батча — и ни разу не потянули вверх. А поскольку входные и выходные эмбеддинги у Ростка общие (глава 8), те же строки описывают эти токены и тогда, когда они стоят на входе, — вот почему базовая модель так теряется, читая их. Дообучение — первый момент, когда их что-то тянет вверх.
Сами веса сдвинулись совсем чуть-чуть: несколько сотен шагов с маленьким шагом обучения. А поведение изменилось полностью. Так дообучение обычно и выглядит: небольшой толчок в пространстве весов — и большая перемена в поведении.
Почему <|end|>, закрывающий реплику Ростка, оценивается, а <|assistant|> — нет?
В разговоре наш код сам дописывает <|assistant|>, передавая слово Ростку, так что угадывать его бессмысленно. А конец ответа — собственное решение Ростка, и если его этому не научить, он никогда не остановится.
Чего дообучение не умеет
Хочется думать, что дообучение учит модель тому, что говорить. По большей части нет. Оно учит форме: отвечать, а не продолжать; быть кратким и добрым; вовремя останавливаться. Содержание ответов почти целиком приходит из предобучения. Работа LIMA (Чжоу и соавторы, 2023) показала это очень наглядно: большую базовую модель дообучили всего на 1 000 тщательно отобранных разговоров, и люди на удивление часто оценивали её ответы не ниже, чем ответы моделей, настроенных куда основательнее. Авторы назвали это гипотезой поверхностного выравнивания (superficial alignment hypothesis): почти все знания модель получает при предобучении, а дообучение в основном выбирает стиль, в котором ими пользоваться.
Обратная сторона: дообучение не добавит знаний, которых у базовой модели нет. Спросите Ростка в первом виджете «What is 17 times 23?». Чат-версия ответит сразу, без тени сомнения и в форме ответа — «это есть то», — ведь отвечать её и учили. Но числа внутри — каша: за дюжину попыток мы получили «7 times 3 is 45», «19 = 56» и «980 is 7 times 36», а 391 — ни разу. Детские истории умножению не учат; даже на «2 plus 2» его четвёрка обычно приходит в обёртке из чепухи вроде «30 is 4». В этом суть того, что называют галлюцинациями: модель прекрасно усвоила форму полезного ответа и заполняет её тем, что подсказывают знания, — какими бы они ни были. Наши данные учат Ростка честно говорить о пределах, которые мы смогли предвидеть: нет даты, нет новостей, только английский, со сложной арифметикой туго. Но никакие данные не научат его замечать каждый пробел в собственных знаниях. Гудибанде и соавторы (2023) увидели то же самое в большем масштабе: маленькие модели, дообученные на ответах большой, убедительно копировали её стиль и гораздо хуже — её точность.
Дообучение не учит модель новому о мире. Оно учит её новому жанру — разговору, в котором она играет саму себя.
Росток сейчас
Вот тот Росток, которого мы растили всё это время: те же 17,31 миллиона параметров, теперь дообученные вести разговор. Поздоровайтесь, спросите про животных или погоду, попросите историю, расскажите, как прошёл ваш день. Он маленький, поэтому иногда путает факты или теряет нить в длинном разговоре, и понимает только английский. В следующей главе вы научитесь дообучать его сами и дадите ему собственный стиль, не переучивая все 17 миллионов чисел.