← Росток Своя LLM с нуля Словарик Код EN

Глава 14 из 17 30 мин

Больше данных

Корпус Ростка-2 — десять с половиной миллиардов токенов интернета, кода, вопросов с ответами и математики. Откуда берётся такая груда текста, что разрешают её лицензии и что с ней нужно сделать до первого шага обучения.

В этой главе

  • сравнить, как три токенизатора режут код, и объяснить, зачем Ростку-2 понадобился свой
  • разобраться в смеси из 31 источника, в их лицензиях и в том, почему к концу обучения смесь меняется
  • проследить, как корпус из веба и кода проходит чистку: фильтры, заглушки вместо секретов, сверку с тестами по 13 словам, дубликаты и заполнение середины

Росток вырос на детских сказках. 330 миллионов токенов сказок и бытовых диалогов научили 17 миллионов его чисел рассказывать истории и болтать о погоде. Теперь мы сажаем новое семечко. У Ростка-2 125,1 миллиона параметров, всемеро больше, и работа для него уже есть: дальше в курсе он станет крошечным агентом, который читает код и запускает команды.

Модель — это то, что она прочитала (глава 9), поэтому Ростку-2 нужна совсем другая библиотека: около 10,6 миллиарда токенов современного англоязычного интернета, учебников, кода, вопросов с ответами, команд и математики. Но сначала — самое заметное изменение: покажем Ростку-1 немного кода.

Росток-1 встречает код

Ниже три токенизатора режут один и тот же текст. В словаре GPT-2 50 257 токенов, выученных в 2019 году на веб-страницах. У Ростка-1 — те самые 8 192 токена из главы 6, выученные на сказках. У Ростка-2 — 32 768 токенов, выученных на выборке, смешанной так же, как корпус этой главы. Начните с примера на Python, потом попробуйте остальные или напишите что-нибудь своё.

Каждая плашка — токен; нажмите на любую, и в других строках подсветятся те же символы. «Куски» показывают, как правило каждого токенизатора режет текст ещё до склеек. Токены GPT-2 посчитаны заранее для примеров, а ваш собственный текст оба Ростка режут прямо в браузере.

Пример на Python говорит сам за себя.

  • Росток-1 никогда не видел кода. «def» у него — два токена, «de» и «f», а «values» — «val» и «ues». Ни одна из его 7 932 склеек не состоит из пробелов и не содержит перевода строки, так что каждый пробел отступа — отдельный токен: 67 токенов на функцию в пять строк.
  • GPT-2 знает слова — он читал интернет, — но и он тратит по токену на каждый пробел отступа: 16 из его 53 токенов — одинокие пробелы.
  • Ростку-2 хватает 34 токенов. «):↵» закрывает строку одним токеном, пробелы отступа — тоже один токен, как и «(values».

На обычном английском ничья: у GPT-2 и Ростка-2 по 15 токенов, у Ростка-1 — 19: в его сказках было мало океанов, и он пишет «·o|ce|ans» по слогам. А в одном примере Росток-2 проигрывает всем, причём нарочно: это числа. Мы к ним ещё вернёмся.

Глава 6 закончилась мыслью, что токенизатор читает крупными дешёвыми кусками то, чего в его корпусе было много. Ростку-2 код даётся легко, потому что в его корпусе был код. Так что начнём с корпуса.

Зачем больше и чего именно

В главе 9 мы нарочно выбрали детские сказки: модель на 17 миллионов параметров не проглотит весь мир, а на веб-страницах размажет свои немногочисленные числа слишком тонко. Росток-2 в семь раз больше, и задача у него шире, поэтому вместо одного вида текста в его рационе шесть:

  • Веб и учебники, 57,5% смеси: объяснения всего на свете и обычный живой английский.
  • Код, 17%, и ещё 2% документации в Markdown: больше всего Python и shell — два языка будущего агента.
  • Вопросы и ответы, 8%: код и команды, объяснённые словами, со Stack Overflow и других сайтов Stack Exchange.
  • Команды и разговоры разработчиков, 3,5%: примеры для утилит командной строки, каналы помощи Ubuntu в IRC, обсуждения на GitHub.
  • Математика, 9%: решения шаг за шагом и текстовые задачи, решённые короткими программами на Python.
  • Сказки и диалоги, 3%: собственный корпус Ростка-1, чтобы и новое растение умело рассказывать истории.

Сколько нужно текста? Правило из главы 9 — около 20 токенов на параметр — дало бы Ростку-2 примерно 2,5 миллиарда. Он учится на 9 миллиардах: около 72 токенов на параметр и в 27 раз больше, чем прочитал Росток-1. Это современная привычка, знакомая нам по Llama 3: маленькую модель, которую будут запускать много раз, выгодно учить далеко за оптимумом Chinchilla. А чтобы модель не перечитывала раз за разом одни и те же тексты, корпус должен быть не меньше этих 9 миллиардов.

Смесь

Вот все 31 источник по видам; длина полосы — доля токенов обучения, которая достаётся источнику. Нажмите на любой, чтобы узнать, что это за источник и под какой он лицензией.

Настоящие цифры по каждому источнику. «Прочитан за обучение» — сколько раз обучение в среднем проходит источник целиком, с учётом спуска (последняя пятая часть шагов, как в train.py).

После чистки в корпусе 10,57 миллиарда токенов в 13,07 миллиона документов; один только FineWeb-Edu даёт 3,65 миллиарда. Обучение не высыпает источники один за другим: каждая строка каждого батча берётся из источника, выбранного случайно по его весу. Веса повторяют, сколько токенов мы хотели взять из каждого источника, поэтому любой из них прочитывается примерно по разу: от 0,7 до 1,1 раза. Исключение — страницы tldr: в них всего 1,16 миллиона токенов, но это одни из лучших коротких примеров для утилит командной строки, и смесь показывает их вчетверо чаще, чем полагалось бы по размеру.

Смесь для спуска

Под конец обучения его скорость падает до нуля — это спуск из главы 10. Шаги мельчают, и модель окончательно подстраивается под то, что читает напоследок, — удачный момент налечь на самое важное. В train.py спуск занимает последнюю пятую часть шагов, и веса меняются: у веб-страниц остаётся 0,6 обычного веса, у YouTube и IRC — 0,5; у Python, shell, сайтов Stack Exchange, FineMath и TinyGSM вес вырастает в полтора раза, у tldr — вдвое. Сверху добавляются разговоры в формате чата Ростка-2: 3% спуска и ни одного раньше. Здесь модель впервые встречает служебные токены, которые понадобятся ей в главе 16. Переключите виджет на «Спуск»: код вырастает с 19% до 26%, вопросы с ответами — с 8% до 12%, математика — с 9% до 14%, а веб сжимается с 57% до 39%.

Лицензии

Веса Ростка-2, его код и список данных будут открыты — значит, всё, на чём он учится, должно это позволять. Каждый источник смеси распространяется под открытой лицензией:

ЛицензияИсточникиДоля токеновЧто она просит
ODC-BY 1.0FineWeb-Edu, Cosmopedia, FineMath48%указать источник
разрешительные, пофайлововесь код18%у каждого файла своя лицензия; оставлены только MIT, Apache, BSD и подобные
CC-BYDCLM, tldr, SODA, YouTube16%указать автора
CC-BY-SAStack Exchange9%указать автора и передавать тексты дальше под той же лицензией
MITTinyGSM, SimpleStories5%сохранить текст лицензии
open (Common Pile)Ubuntu IRC, обсуждения на GitHub4%открытые лицензии или общественное достояние
CDLA-Sharing 1.0TinyStories1%передавать данные дальше на тех же условиях

Почти все они просят одного: указывать, откуда взяты данные. Поэтому prepare.py кладёт рядом с готовыми данными файл SOURCES.md — список всех источников, их происхождения и лицензий. Честная оговорка: у двух веб-коллекций, FineWeb-Edu и DCLM, лицензия относится к подборке целиком. Сами страницы собраны в открытом интернете, и их авторы никакой лицензии не выбирали. Это обычный компромисс любой открытой модели, обученной на вебе. Источники из Common Pile строже: в них только тексты под открытыми лицензиями или в общественном достоянии.

Смесь — такая же часть устройства модели, как её слои: какие источники, сколько каждого, под какой лицензией и на что налечь в конце.

Токенизатор для кода

Вернёмся к виджету в начале главы. Почему GPT-2, прочитавший 40 гигабайт веб-текста, тратит по токену на каждый пробел отступа? Переключите виджет на «Куски» и посмотрите на пример с Python. Прежде чем склеивать пары, токенизатор режет текст на куски регулярным выражением, и склейка никогда не выходит за границу куска (глава 6). Правило GPT-2 приклеивает перевод строки к пробелам, которые идут за ним, поэтому каждая глубина отступа даёт свой кусок: «↵···», «↵·······». На веб-страницах, где GPT-2 учил склейки, такие куски редки, склеек для них нет, и они рассыпаются на отдельные символы.

Правило Ростка-2 писали с оглядкой на код:

# Split text into chunks first, so a merge never crosses a chunk boundary: # contractions, words (with one leading space or punctuation mark), single # digits, runs of punctuation (with the newlines after them), newlines with the # spaces before them, spaces. Written without possessive quantifiers so that # JavaScript can use the same pattern. SPLIT = re.compile( r"""'(?:[sdmtSDMT]|ll|ve|re|LL|VE|RE)""" r"""|[^\r\n\p{L}\p{N}]?\p{L}+""" r"""|\p{N}""" r"""| ?[^\s\p{L}\p{N}]+[\r\n]*""" r"""|\s*[\r\n]+""" r"""|\s+(?!\S)""" r"""|\s+""")
  • [^\r\n\p{L}\p{N}]?\p{L}+: слово может прихватить один символ, который не буква, не цифра и не перевод строки. Обычно это пробел, как и раньше, но может быть и скобка или точка — так «(values» и «.append» могут стать одним токеном.
  • \p{N}: ровно одна цифра, и без пробела перед ней.
  • ?[^\s\p{L}\p{N}]+[\r\n]*: знаки препинания забирают идущие за ними переводы строк, поэтому «):↵» — конец строки кода — один кусок.
  • \s*[\r\n]+: перевод строки забирает пробелы перед собой и никогда — после.
  • \s+(?!\S)|\s+: цепочки пробелов, которые теперь стоят сами по себе, — это и есть отступ. Последний пробел уходит к следующему слову, так что «····if» становится «···» и «·if».

Разница видна с самой первой склейки Ростка-2: это два пробела. Четыре пробела — девятая склейка, раньше, чем «·the» (одиннадцатая), восемь — 57-я. Из одних пробельных символов состоят 126 его токенов (цепочки до 64 пробелов, табуляции, пустые строки), а перевод строки есть в 498 склейках, например «):↵» и «:↵». В словаре Ростка-1 нет ни тех, ни других. А раз в правиле нет сверхжадных квантификаторов, JavaScript выполняет его без изменений: виджет выше берёт его прямо из tokenizer.json Ростка-2.

Шестнадцать служебных токенов

У Ростка-1 служебных токенов было четыре, у Ростка-2 — шестнадцать, отложенных заранее на всё, что будет дальше: <|endoftext|> между документами; <|user|>, <|assistant|>, <|end|> и новый <|system|> для разговоров; <|think|> и <|/think|> вокруг собственных рассуждений модели; <|tool_call|> и <|tool_result|> для вызова инструмента, например Python (всё это — в главе 16); <|fim_prefix|>, <|fim_suffix|> и <|fim_middle|> для заполнения середины кода, о нём ниже; и четыре запасных, от <|reserved_0|> до <|reserved_3|>, для будущего агента.

Попробуйте пример «чат и мысли» в начале главы: у Ростка-2 каждая метка — один токен, а Росток-1 знает <|user|>, но <|think|> читает как горстку знаков и букв. Запасной токен стоит одну строку таблицы эмбеддингов, 576 чисел, зато потом не придётся перекраивать ни таблицу, ни токенизатор. Всего получается 256 байтов, 32 496 склеек и 16 служебных токенов — ровно $2^{15} = 32\,768$; номера по-прежнему с запасом помещаются в uint16 (помните вопрос из главы 9?).

Какого размера словарь?

train_tokenizer.py выучил склейки Ростка-2 на 603 миллионах символов, смешанных в тех же пропорциях, что и корпус, всего за 75 секунд на Mac: помогла куча, которая находит самую частую пару, не пересчитывая всё заново. Чтобы выбрать размер, мы обучили три словаря и измерили, сколько символов вмещает токен на незнакомом тексте; чем больше, тем меньше токенов.

Текст16k32k48kGPT-2 (50k)
FineWeb-Edu3,984,284,434,46
Python3,333,623,772,17
shell2,692,953,072,34
JavaScript3,573,864,022,08
Stack Overflow3,844,084,183,41
страницы tldr3,613,964,103,22
всё вместе2,953,133,222,86

Обычный английский из интернета по-прежнему лучше всех читает GPT-2: 4,46 символа на токен против 4,28 — свои 50 тысяч токенов он потратил в основном на английские слова. На коде картина обратная: тот же Python у Ростка-2 занимает на 40% меньше токенов, чем у GPT-2, а JavaScript — на 46% меньше. Что до размера, переход с 16k на 32k даёт 6% на всём вместе, а с 32k на 48k — лишь ещё 3%, хотя таблица эмбеддингов выросла бы на $16\,384 \times 576 \approx 9{,}4$ миллиона чисел. При 32k в ней уже 18,9 миллиона, 15% модели. Значит, 32k.

Цена: числа

Теперь пример, который Росток-2 проигрывает. «In 2024 the price rose from 1,250 to 13,999 dollars»: GPT-2 нужно 15 токенов, Ростку-1 — 23, Ростку-2 — 26. Росток-2 пишет каждую цифру отдельным токеном и даже пробел перед числом держит отдельно, так что «·2024» — пять токенов: «·», «2», «0», «2», «4». Ни одна из его 32 496 склеек не содержит цифр.

Эту цену мы платим сознательно. В словаре GPT-2 «250», «999» и «·13» — отдельные токены: чтобы складывать числа, такой модели приходится заучивать факты о тысячах произвольных токенов-чисел, которые к тому же режутся по-разному в разном окружении. Когда на каждую цифру по токену, любое число записывается по одному правилу — цифра за цифрой, и сложение в столбик становится закономерностью, которую достаточно выучить один раз. Она пригодится Ростку-2 в главе 16, где он будет решать текстовые задачи.

На «·2024» Росток-2 тратит пять токенов, а GPT-2 — один. Почему это осознанный выбор, а не недостаток?

Правило выделяет каждую цифру в отдельный кусок (\p{N}), так что BPE не может склеить цифры, как бы часто они ни встречались. Лишние токены на каждом числе — плата за то, что единицы стоят под единицами, десятки под десятками, и не нужно заучивать тысячи токенов-чисел.

Чистка оптом

В главе 9 чистильщик почти ничего не выбрасывал: синтетические сказки аккуратны. Теперь источников 31, а документов — 13,19 миллиона, от веб-страниц до скриптов на shell. prepare.py обрабатывает сырые файлы параллельно, по многу сразу, и проводит каждый через одни и те же шаги: почистить, отфильтровать, сверить с тестами, спрятать секреты, разрезать для заполнения середины, перевести в токены. Потом выбрасывает точные дубликаты по всем источникам и откладывает от каждого источника кусочек для проверки.

Уже первый шаг показывает, почему с кодом нужна осторожность. Чистильщик из главы 9 сжимал любую цепочку пробелов в один: сказке это не вредит, а для Python смертельно — там отступ и есть программа. Поэтому prepare.py различает три вида текста. В прозе типографские кавычки и тире заменяются простыми, а лишние пробелы сжимаются; в коде остаётся каждый пробел; в смешанных источниках — вопросах с ответами, Markdown, IRC — кавычки заменяются, а пробелы остаются.

Цифры полного прогона. Выброшенное — меньше 1% прочитанного, поэтому смотрим на него под лупой. Выберите источник, чтобы увидеть его собственную воронку.

Из 13 191 986 прочитанных документов осталось 13 074 403: ушло всего 0,89%. Большие веб-коллекции их составители уже отфильтровали, а наши фильтры нарочно простые. Две трети выброшенного — точные дубликаты. Ещё 23 706 документов написаны в основном не латиницей (латинских меньше 90% букв, у кода — меньше 85%); чаще всего это файлы на Python с комментариями на другом языке. 8 347 — код, написанный машиной: сжатые скрипты, сгенерированные таблицы, файлы с «do not edit» в первых строках. Писать код они не учат. В 3 472 документах нашлись вопросы из тестов (о них ниже), и примерно по две тысячи оказались слишком короткими или слишком длинными.

Дубликаты

Напоследок prepare.py снимает с каждого документа отпечаток (MD5 от текста в нижнем регистре со сжатыми пробелами), проходит все 31 источник в заданном порядке и оставляет только первый документ с каждым отпечатком. Ушли 77 713 копий: 52 тысячи из FineWeb-Edu, 19 тысяч из TinyGSM, 5,5 тысячи из Cosmopedia. Это та же точная дедупликация, что в главе 9, и почти-дубликаты она по-прежнему пропускает; как их ловят в больших корпусах, рассказывает заметка про MinHash там же.

Ключи и адреса

Код в интернете полон того, что никогда не следовало публиковать: ключей API, токенов доступа, паролей — и повсюду адресов электронной почты. Модель может запомнить такие строки и потом повторить их кому угодно. prepare.py эти документы не выбрасывает (файл настроек остаётся хорошим примером файла настроек), а заменяет опасные строки заглушками — «placeholders the model may learn to write instead of real secrets», как сказано в комментарии к коду: модель научится писать заглушку там, где стоял бы настоящий секрет.

Правила из prepare.py, перенесённые в JavaScript и сверенные с Python. Все ключи в примерах выдуманы. Вставьте что угодно: из браузера ничего не уходит.

Адреса почты превращаются в name@example.com, кроме тех, что и так примеры, служебных адресов GitHub и git@… (в git@github.com это часть команды, а не человек): заменено 219 810 адресов. Ключи узнаются по их настоящим форматам: ключи AWS начинаются с AKIA и становятся AKIAIOSFODNN7EXAMPLE — примером из документации самой AWS; токены GitHub начинаются с ghp_ и родственных приставок, токены Slack — с xox, многие API-ключи — с sk-; от закрытого ключа остаются только строки BEGIN и END. Уходит и длинное случайное значение сразу после имени вроде api_key. Пароли остаются: в коде это почти всегда примеры. Всего заменено 4 172 секрета, больше всего в Python (1 700), на Stack Overflow (568) и в JavaScript (562).

Первая версия чистильщика была жаднее и хватала лишнее. Ссылки вроде …/task-to-replace-… подходили под правило для API-ключей: загляните внутрь «task-to-replace» — там «sk-», а за ним длинная цепочка букв и дефисов. Переменные shell вроде ${PASSWORD} принимались за пароли. Попробуйте оба примера в виджете. Теперь правило требует, чтобы прямо перед «sk-» не было куска другого слова, а в самом ключе нашлись цифра, заглавная буква и не меньше 32 символов; значение после имени ключа должно выглядеть случайным — от 16 символов, с цифрой. Ложные срабатывания бывают у любого фильтра, и найти их можно, только читая то, что он поймал, а не просто считая.

Вот код из prepare.py; в следующем разделе он пойдёт в дело.

from collections import Counter import regex as re # placeholders the model may learn to write instead of real secrets EMAIL = re.compile(r'\b([A-Za-z0-9._%+-]+)@([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})\b') EMAIL_KEEP = {'example.com', 'example.org', 'example.net', 'users.noreply.github.com'} SECRETS = [ (re.compile(r'AKIA[0-9A-Z]{16}'), 'AKIAIOSFODNN7EXAMPLE'), (re.compile(r'gh[pousr]_[A-Za-z0-9]{36,}'), 'YOUR_GITHUB_TOKEN'), (re.compile(r'xox[baprs]-[A-Za-z0-9-]{10,}'), 'YOUR_SLACK_TOKEN'), (re.compile(r'(?<![A-Za-z0-9_-])sk-(?:proj-)?(?=[\w-]*\d)(?=[\w-]*[A-Z])[A-Za-z0-9_-]{32,}'), 'YOUR_API_KEY'), (re.compile(r'(-----BEGIN ([A-Z ]*)PRIVATE KEY-----).*?(-----END \2PRIVATE KEY-----)', re.S), r'\1\nYOUR_PRIVATE_KEY\n\3'), ] # a long random-looking value right after a key-like name (passwords in code are nearly always examples: kept) ASSIGNED = re.compile(r'''((?:api[_-]?key|apikey|secret[_-]?key|client[_-]?secret|access[_-]?token|auth[_-]?token)''' r'''["']?\s*[:=]\s*["'])((?=[A-Za-z0-9+/=_-]*\d)[A-Za-z0-9+/=_-]{16,})(["'])''', re.I) def scrub(text, counts): def email(m): if m.group(2).lower() in EMAIL_KEEP or m.group(1).lower() == 'git': return m.group(0) counts['emails'] += 1 return 'name@example.com' text = EMAIL.sub(email, text) for pattern, placeholder in SECRETS: text, n = pattern.subn(placeholder, text) counts['secrets'] += n def assigned(m): counts['secrets'] += 1 return m.group(1) + 'YOUR_API_KEY' + m.group(3) return ASSIGNED.sub(assigned, text)

Экзамен без шпаргалок

Ростку-2 предстоят экзамены: на здравый смысл (HellaSwag, PIQA, WinoGrande), школьные естественные науки (ARC, OpenBookQA, SciQ), понимание прочитанного (BoolQ) и умение писать работающий Python (MBPP, HumanEval и текстовые задачи GSM8K). Тест измеряет умение, только если модель не видела его вопросов. Но вопросы утекают в интернет — на страницы с викторинами и в шпаргалки, — и модель, прочитавшая их при обучении, получает оценку за память.

Поэтому, прежде чем оставить документ, prepare.py сверяет его со всеми тестами. Каждый текст из тестов режется на все цепочки из 13 слов подряд (в нижнем регистре, без знаков препинания), и их хеши складываются во множество. Потом то же самое делается с каждым документом, и одной общей цепочки достаточно, чтобы документ ушёл. Проверка, есть ли хеш во множестве, занимает одно и то же время, каким бы большим ни было множество (глава 9), так что сверка остаётся дешёвой даже для 13 миллионов документов.

Четыре настоящих совпадения из выборок источников Ростка-2; подсвечена самая длинная цепочка слов, общая для двух текстов. «Свои тексты» ищут такую цепочку в любых двух текстах.

Ушли 3 472 документа: 1 386 из FineWeb-Edu, 646 из FineMath, 574 файла на Python, 207 из DCLM, 183 из Cosmopedia. Примеры показывают, что это за документы. На странице с математической викториной лежит задача из тестовой части GSM8K — совпадают первые 28 слов. Страница косметической клиники из FineWeb-Edu слово в слово повторяет определение келоидного рубца из статьи WikiHow, по которой составлен вопрос HellaSwag. А фраза про эффект Кориолиса встречается и на странице из DCLM, и в вопросе ARC.

Четвёртый пример — ложная тревога. Файл на Python, который ищет ближайшую пару точек, содержит тот же двойной цикл, что и решение задачи MBPP о подсчёте инверсий: for i in range(n): for j in range(i + 1, n): if. Знаки препинания не считаются, так что этот самый обычный цикл — ровно 13 слов: for, i, in, range, n, for, j, in, range, i, 1, n, if. В прозе 13 слов подряд почти никогда не совпадают случайно, а в коде это одна строчка ходовой конструкции. Для кода проверка грубая, и среди 574 выброшенных файлов на Python наверняка были невинные. Но это меньше 0,1% всего Python — цена, которую мы готовы заплатить за честный экзамен.

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

# prepare.py's ngrams(), unchanged: every run of 13 words, as a hash WORD = re.compile(r'\w+') def ngrams(text, n=13): words = WORD.findall(text.lower()) return (hash(tuple(words[i:i + n])) for i in range(len(words) - n + 1)) mbpp = ("def get_Inv_Count(arr,n): \n inv_count = 0\n for i in range(n): \n" " for j in range(i + 1,n): \n if (arr[i] > arr[j]): \n" " inv_count += 1\n return inv_count") bad = set(ngrams(mbpp)) # prepare.py fills this set from every benchmark text fake_key = 'sk-proj-' + 'NotARealKey0' * 3 doc = f'''import math API_KEY = "{fake_key}" OWNER = "jane.doe@mycompany.io" def closest(points, n): best = float('inf') for i in range(n): for j in range(i + 1, n): if math.dist(points[i], points[j]) < best: best = math.dist(points[i], points[j]) return best ''' counts = Counter() print(scrub(doc, counts)[:80]) print(dict(counts)) shared = [h for h in ngrams(doc) if h in bad] print(len(shared), 'run(s) of 13 words shared with MBPP:', 'the document goes' if shared else 'it stays')

В prepare.py сверка с тестами стоит раньше чистильщика секретов, так что до него этот файл даже не дошёл бы.

Почему проверка на 13 слов выбрасывает невинный код чаще, чем невинную прозу?

В прозе 13 одинаковых слов подряд — почти всегда настоящая копия. Код собран из немногих ключевых слов и коротких имён, а скобки между ними не считаются, поэтому обычные конструкции быстро набирают 13 слов. Честной проверке для кода нужны цепочки подлиннее или сравнение целых функций.

Заполнить середину

Агент редко пишет файл сверху вниз. Он открывает готовый и меняет несколько строк посередине. Модель, которая читала тексты только слева направо, умеет лишь продолжать: когда она пишет, ей известно начало файла, но никогда — его конец. А конец важен: недостающая строка должна дать коду ниже то, чего он ждёт.

Решение на удивление простое. Разрежем файл в двух случайных местах на три части: начало, середину и конец. Переставим их, пометив тремя служебными токенами: сначала начало, потом конец, потом середина. Модель, читающая слева направо, теперь добирается до середины, уже увидев и начало, и конец. Она учится заполнять пропуск — а в самой модели ничего менять не нужно.

Двигайте разрезы или режьте наугад, как prepare.py. Нижняя строка — то, что на самом деле читает Росток-2.

В prepare.py на это ушло шесть строк:

if source in CODE and len(text) > 200 and random.Random(h).random() < FIM_RATE: rng = random.Random(h + 1) a, b = sorted(rng.randrange(len(text)) for _ in range(2)) pre, mid, suf = (fast.encode_ordinary(t) for t in (text[:a], text[a:b], text[b:])) doc = [eot, _w['fim'][0], *pre, _w['fim'][1], *suf, _w['fim'][2], *mid] counts['fim'] += 1

Участвуют только файлы с кодом длиннее 200 символов, и только 30% из них (FIM_RATE); выбор и место разрезов зависят от хеша документа h, так что при повторном запуске разрезы будут те же. Разрезано 511 834 файла — 29% оставшихся файлов с кодом (Markdown не в счёт). Остальные идут в обычном порядке, и модель по-прежнему учится писать код сверху вниз. Разрез может попасть внутрь слова, а раз каждая часть переводится в токены отдельно, токены на стыке меняются: попробуйте разрезать «json» пополам.

Порядок, в котором модель читает, — тоже часть обучения: переставьте части файла, и модель, которая умеет только предсказывать следующий токен, научится писать в середине.

Сколько кода?

Остался открытым один вопрос: почему кода именно 17%? Больше кода — лучше программист, но каждый токен кода — это токен, не потраченный на английский, а агенту нужно понимать и человека, и программу. Гадать мы не стали: обучили маленькие копии Ростка-2, «прокси», — по 14,3 миллиона параметров, 100 миллионов токенов и около часа на Mac на каждую. Три из них отличались только долей кода: вдвое меньше плана, по плану и в полтора раза больше. Потом каждую проверили на незнакомом тексте из каждой группы источников.

Изменение ошибки на проверке, в натах, относительно прокси на плановой смеси; в таблице ниже — сами значения.

Результат несимметричный. От половины кода до полуторной доли ошибка на коде падает на 0,24 ната, а на веб-тексте растёт всего на 0,06: код намного лучше, английский чуть хуже. Но первый шаг даёт больше второго. От половины до плана код улучшается на 0,16, дальше — лишь на 0,08, а цена в веб-английском растёт с 0,02 до 0,04. Понемногу проигрывают и сказки с математикой: по 0,02–0,04 ната на каждом шаге. Отдача с одной стороны падает, цена с другой растёт; плановые 17% — компромисс между ними, а спуск и так налегает на код.

Прокси почти в девять раз меньше Ростка-2 и читают в 90 раз меньше, поэтому их цифры указывают направление, но не дают прогноза. Скорости обучения и маску документов из главы 15 тоже выбирали на прокси.

Росток сейчас

В браузере Ростка-2 пока нет: он ещё растёт. Зато можно заглянуть в его дневник: во время обучения он время от времени продолжает одни и те же пять затравок. Один шаг обучения — 524 288 токенов. К 200-му шагу Росток-2 прочитал 105 миллионов токенов — треть всего, что прочитал Росток-1, — и уже пишет английский с интонацией сказки, а Python — с отступами на своих местах, хотя его код пока ничего не вычисляет. К шагу 1 000 у сказки появляются героиня и сюжет, а у функции — строка документации. К шагу 6 000 позади треть из 9 миллиардов токенов. Это первые часы его обучения; в следующей главе — модель, которая всё это читает, и то, как она дорастает до конца.

Главы

  1. 0 Знакомство
  2. 1 Считаем буквы
  3. 2 Мера удивления
  4. 3 Градиентный спуск
  5. 4 Обратное распространение
  6. 5 Эмбеддинги
  7. 6 Токены
  8. 7 Внимание
  9. 8 Трансформер
  10. 9 Корпус
  11. 10 Обучение
  12. 11 Выборка
  13. 12 Разговор
  14. 13 LoRA
  15. 14 Больше данных
    1. Росток-1 встречает код
    2. Зачем больше и чего именно
    3. Смесь
    4. Токенизатор для кода
    5. Чистка оптом
    6. Ключи и адреса
    7. Экзамен без шпаргалок
    8. Заполнить середину
    9. Сколько кода?
    10. Росток сейчас
  16. 15 Больше модели
  17. 16 Думать и Python
  18. 17 Что дальше