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

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

Корпус

Ростку нужна библиотека. Посмотрим, из каких текстов она собрана, как подготовить их к обучению и почему размер корпуса ещё не говорит, сколько текста модель прочитала.

В этой главе

  • полистать три источника корпуса Ростка и прочитать ровно то, что читал он
  • почистить и дедуплицировать текст так же, как это делает prepare.py, правило за правилом
  • прикинуть, сколько токенов нужно модели заданного размера и почему Ростку подходит 330 миллионов

В прошлой главе мы собрали трансформер: восемь блоков и 17,31 миллиона параметров. Пока его веса случайны, он выдаёт бессмыслицу — даже самая удачная архитектура сама по себе не знает английского. Чтобы в этих числах появились закономерности языка, нужны примеры. От выбора текстов зависит, какие слова модель встретит часто, какие сюжеты научится продолжать и о чём почти ничего не узнает.

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

Библиотека Ростка

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

Настоящие документы из корпуса, взятые наугад из каждого источника. «Из смеси» выбирает источник пропорционально его доле в токенах — так же, как это происходит при обучении.

TinyStories — сердце коллекции. Это короткие сказки: GPT-4 попросили написать их словами, понятными трёх-четырёхлетнему ребёнку. У них узнаваемый ритм: «Once upon a time… One day… The end». Словарь крошечный, сюжеты простые, и каждая история доведена до конца. Мы берём вторую версию набора — в ней только истории, написанные GPT-4.

Истории SimpleStories тоже синтетические, но их генерировали по сетке параметров: тема, мотив, стиль, повествовательный приём и так далее. Английский по-прежнему простой, зато истории разнообразнее и длиннее: путешествия во времени, бабушкины коробки с фотографиями, рассказ от лица маяка. Мир Ростка с ними становится чуть шире.

SODA добавляет в библиотеку разговоры: это синтетические бытовые диалоги, сгенерированные с помощью InstructGPT по описаниям социальных ситуаций. Перед диалогом есть короткое вступление, а реплики мы записываем строками вроде Anna: I'm fine, thanks. Такие примеры знакомят Росток с чередованием говорящих и ответами на чужую реплику. В главе 12 этот опыт пригодится, когда мы будем дообучать его для чата.

Почему детские сказки?

Почему именно сказки, если в интернете столько текстов? У модели на 17 миллионов параметров ограниченная вместимость. В разнородном корпусе ей пришлось бы одновременно разбираться с кодом, спортивными репортажами, рецептами и рекламой. Сказки дают более посильную задачу: небольшой словарь, повторяющиеся конструкции и короткие сюжеты, начало и конец которых помещаются в контекст.

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

У такого выбора есть цена. Синтетический текст несёт на себе отпечаток модели, которая его написала: любимые обороты GPT-4 («filled with joy», «learned a valuable lesson») повторяются в сотнях тысяч историй, и Росток переймёт их так же прилежно, как грамматику. В конце главы мы найдём следы этих привычек среди самых частых токенов корпуса.

Корпус задаёт модели круг примеров, из которых она учится. Она может соединять знакомые элементы по-новому и переносить закономерности на новые случаи, но вместе с полезными навыками усваивает повторы, ошибки и перекосы своих источников.

Смесь

После чистки и токенизации в корпусе 1,43 миллиарда токенов: 541,5 миллиона из TinyStories, 607,7 миллиона из SimpleStories и 285,2 миллиона из SODA. Пропорции проще увидеть, чем прочитать, поэтому ниже каждая клетка — миллион токенов, примерно один толстый роман.

Каждая цветная клетка — около миллиона обучающих токенов, серые внизу — проверочная часть. «Обучить» проигрывает все шаги обучения Ростка со случайными окнами настоящей длины (128, потом 256, потом 512 токенов), и оценивает покрытие каждой клетки. Это симуляция случайной выборки, а не запись того, какие позиции прочитал реальный запуск.

Переключитесь на «Как в train.bin». Перед записью файла prepare.py режет каждый источник на куски примерно по миллиону токенов, сохраняя границы документов, и перемешивает куски. В результате источники чередуются по всему файлу. Это не гарантирует нужные пропорции в каждом коротком отрезке: внутри одного куска по-прежнему лежит один источник. При обучении окна выбираются случайно из всего файла, поэтому доли источников в среднем определяются их объёмом в токенах.

Теперь нажмите «Обучить». Наш запуск — это 10 070 шагов по 32 768 токенов, около 330 миллионов токенов суммарно. Это 23% длины обучающего файла, но не 23% уникально прочитанного текста: случайные окна иногда перекрываются. По приближённой оценке, около 79% позиций останутся непрочитанными, а примерно 2% встретятся дважды или чаще. Ниже разберём, почему для первого запуска мы остановились на таком бюджете.

Когда корпус гораздо больше прочитанной за запуск выборки, модель реже встречает один и тот же фрагмент. Это снижает повторяемость примеров, но не исключает запоминание: некоторые последовательности модель способна усвоить и с одного предъявления, а похожие фразы встречаются в разных документах. Поэтому качество проверяют отдельно. В следующей главе мы сравним ошибку на обучающих батчах с ошибкой на отложенном тексте.

Чистка

В сыром тексте всегда есть мусор, даже если его написала модель. Типографские кавычки “вот такие” и прямые "вот такие" значат одно и то же, но для токенизатора это разные байты, а значит, разные токены. Модели пришлось бы дважды учить, что кавычка открывает реплику. То же с тремя видами тире, символом многоточия, неразрывным пробелом и переводами строк в стиле Windows. Добавьте к этому лишние пробелы, груды пустых строк, обрывки, слишком короткие для истории, и тексты на других языках.

Наш чистильщик — одна маленькая функция. Попробуйте её на примерах или вставьте свой текст:

Те же правила, что в clean() из prepare.py, только в браузере. Память дедупликации заранее знает документы из библиотеки выше: пример «Дубликат» — один из них, набранный заглавными буквами.

Вот эта функция — в точности такая, какой она записана в prepare.py. Это чистый Python, поэтому её можно запустить прямо здесь:

REPLACE = str.maketrans({'‘': "'", '’': "'", '“': '"', '”': '"', '–': '-', '—': ' - ', '…': '...', ' ': ' ', '\r': ''}) def clean(text): """Normalise quotes and dashes, drop stray spaces; None if the text should go.""" text = text.translate(REPLACE).strip() lines = [' '.join(line.split()) for line in text.split('\n')] text = '\n'.join(lines).strip() while '\n\n\n' in text: text = text.replace('\n\n\n', '\n\n') if len(text) < 80: return None if sum(ord(c) > 126 for c in text) > 0.002 * len(text): # other scripts, mojibake return None return text

Последнее правило считает символы с кодом больше 126 и отбрасывает документ, если их доля превышает 0,2%. В тексте длиной 500 символов один такой символ допустим, два — уже нет. Это грубый фильтр, а не распознавание языка: он отсеивает многие тексты на русском или китайском, но может задеть и обычный английский текст с именем Chloé или словом café. Для этого эксперимента мы выбрали почти полностью ASCII-корпус; полезность других языков или символов из этого, конечно, не следует.

На наших данных чистильщику почти нечего было делать — это типично для синтетических текстов. Вот что именно он сделал (причины мы посчитали отдельно):

  • TinyStories: 2 717 700 → 2 717 373 документа. 232 оказались слишком короткими, а в 95 было слишком много символов вне ASCII: в основном из-за слов café, piñata и Chloé; ещё попались несколько эмодзи, невидимые пробелы нулевой ширины и одна английская сказка с парой предложений по-китайски. Кавычки или пробелы пришлось поправить примерно в 338 тысячах историй, в каждой восьмой.
  • SimpleStories: 2 115 696 → 2 115 670. Выброшены всего 26 точных дубликатов, зато пробелы или переводы строк пришлось поправить почти в 1,28 миллиона историй — в 60% всех.
  • SODA: 1 186 423 → 1 184 876 (ещё 5 159 диалогов, у которых списки говорящих и реплик не совпадали по длине, пропущены при чтении). 1 545 диалогов отсеялись из-за чужих символов — в основном разговоры, где герои переходят на испанский или французский либо вспоминают «déjà vu». Два оказались слишком короткими.

С текстом из интернета всё наоборот: на этом этапе выбрасывается большая часть сырого веб-обхода, и чистка становится самой трудоёмкой частью всего проекта.

Дубликаты

Последний шаг чистки — убрать повторы. Если одна и та же история встречается в корпусе тысячу раз, модель видит её в тысячу раз чаще любой другой: вызубрит слово в слово, а её представление об «обычной» истории перекосится в сторону этой одной. К тому же дубликаты попросту съедают бюджет обучения.

Для поиска повторов prepare.py переводит очищенный текст в нижний регистр и вычисляет MD5 — короткий, 16-байтовый отпечаток. Уже встреченные отпечатки хранятся в множестве. Проверка принадлежности такому множеству в среднем занимает почти постоянное время, поэтому не нужно заново сравнивать документ со всей библиотекой. Здесь повторы удаляются внутри каждого источника отдельно; совпадения между источниками этот шаг не ищет. Как и у любого короткого хеша, у MD5 возможны коллизии, поэтому математической гарантии тождества текстов он не даёт.

import hashlib messy = "“Can we go to the park?” asked Lily.\u00a0\u00a0Mom smiled — “Yes… but first, lunch!”\r\n\r\n\r\nThey ate quickly and ran outside." print(repr(clean(messy))) print(clean("The end.")) print(clean("Жили-были дед да баба, и была у них курочка Ряба. Снесла курочка яичко, да не простое.")) seen = set() for doc in ["Tom had a red ball. He liked to play with it in the park every day with his dog.", "TOM HAD A RED BALL. HE LIKED TO PLAY WITH IT IN THE PARK EVERY DAY WITH HIS DOG.", "Tom had a blue ball. He liked to play with it in the park every day with his dog."]: h = hashlib.md5(clean(doc).lower().encode()).digest() print('duplicate' if h in seen else 'new ', h.hex()[:12], doc[:28]) seen.add(h)

Третий документ отличается от первого всего одним словом, но его хеш уже другой. Значит, эта проверка пропускает почти-дубликаты. Они бывают и в синтетических историях, и в веб-корпусах: например, одна статья может оказаться на сотне сайтов с разными меню. Их поиск требует отдельного метода; в подготовке корпуса Ростка такого шага нет.

Документы и окна

Какой длины документы? Росток читает текст окнами по 512 токенов — это его контекст. Если бы документы были длиннее, модель никогда не видела бы их начала и конца вместе; если бы были сильно короче, в одно окно попадало бы несколько не связанных между собой историй.

Распределение длин документов в токенах. Пунктир — три длины окна, с которыми учится Росток: 128, 256 и 512 токенов. Ползунок показывает, какая доля документов помещается в окно целиком.

На всей проверочной выборке (30 088 документов) средняя история из TinyStories — 199 токенов, из SimpleStories — 286, диалог из SODA — 240. Почти все документы — 97% — целиком помещаются в 512 токенов. Значит, случайное окно в 512 токенов обычно захватывает конец одного документа, целиком второй и начало третьего.

Каждый документ в файле начинается с <|endoftext|>. Этот специальный токен служит разделителем: во время обучения модель получает много примеров того, что после него начинается новый текст. Сам по себе токен не стирает контекст и не запрещает вниманию смотреть на предыдущий документ. Он даёт выученный сигнал границы. Для генерации с нуля мы подаём только этот токен и просим модель продолжить.

График заодно объясняет приём из следующей главы. Первые 10% обучения Росток читает окна всего по 128 токенов, потом по 256 и только потом по 512. Короткие окна дешевле, потому что стоимость внимания растёт как квадрат длины окна, а в начале обучения модель всё равно учит локальные вещи: слова, пунктуацию, грамматику. Нажмите «128»: почти ни один документ в такое окно целиком не влезает, но для отдельных предложений его вполне хватает.

Из текста в train.bin

Обучение не должно тратить время на обработку текста, иначе видеокарта будет простаивать в ожидании токенизатора. Поэтому весь корпус токенизируется один раз, заранее, и результат хранится как плоский массив чисел. Вот часть prepare.py, которая это делает:

# 3. encode in parallel; a small slice of every source goes to validation tok = Tokenizer.load(tok_path) eot = tok['<|endoftext|>'] train, val = [], [] with Pool(max(1, os.cpu_count() - 2), initializer=_init, initargs=(tok_path,)) as pool: for name, texts in docs.items(): n_val = int(len(texts) * args.val_frac) for split, part in ((val, texts[:n_val]), (train, texts[n_val:])): chunks = [part[i:i + 2000] for i in range(0, len(part), 2000)] split.append(np.concatenate(pool.map(_encode, chunks))) stats[name]['tokens'] = int(len(train[-1]) + len(val[-1])) print(f'{name}: {stats[name]["tokens"] / 1e6:.1f}M tokens', flush=True) # shuffle whole runs of documents between sources, so any stretch of the file is a fair mix blocks = [] for arr in train: starts = np.flatnonzero(arr == eot) cuts = starts[np.searchsorted(starts, np.arange(1_000_000, len(arr), 1_000_000))] blocks.extend(np.split(arr, np.unique(cuts))) rng.shuffle(blocks) np.concatenate(blocks).tofile(f'{args.out}/train.bin') np.concatenate(val).tofile(f'{args.out}/val.bin')

Несколько деталей заслуживают комментария:

  • Параллельно. Документы режутся на пачки по 2 000 и раздаются пулу процессов — по одному на логическое ядро, кроме одного, оставленного системе. Каждый процесс загружает свою копию токенизатора и возвращает массив чисел. На нашем Mac все 1,43 миллиарда токенов закодировались примерно за минуту, а весь prepare.py, от сырых файлов до train.bin, отработал минут за пять.
  • Проверка. Первые 0,5% документов каждого источника (список перед этим перемешан) уходят в val.bin: 7,17 миллиона токенов, которые не используются для обновления весов языковой модели. Токенизатор при этом строится раньше разделения и может видеть часть этих документов. Поэтому это отложенная проверка языковой модели, а не полностью изолированный набор для всей цепочки обработки.
  • uint16. Каждый токен хранится в двух байтах. В два байта помещаются числа от 0 до $2^{16} - 1 = 65\,535$, а наш самый большой номер — 8 191, так что места с запасом. 1,43 миллиарда токенов занимают 2,85 ГБ — вдвое меньше, чем с обычными четырёхбайтовыми целыми.

Посмотрим, как это выглядит на уровне байтов и как обучение читает такой файл. Вот начало истории в виде номеров и случайные окна из него — так же, как их берёт класс Batches в train.py:

import numpy as np # "Once upon a time, there was a little girl." after <|endoftext|> (8188) ids = [8188, 679, 690, 258, 466, 44, 490, 307, 258, 531, 548, 46] arr = np.array(ids, dtype=np.uint16) print(arr.nbytes, 'bytes for', len(arr), 'tokens') print(arr.tobytes()[:8].hex(' ')) # little-endian: the low byte comes first # train.bin is just this, 1.4 billion times; np.memmap reads it without loading it into memory data = np.frombuffer(arr.tobytes(), dtype=np.uint16) rng = np.random.default_rng(0) T = 4 # Sprout's windows are 128-512 tokens for s in rng.integers(0, len(data) - T - 1, 3): window = data[s:s + T + 1] print('x =', window[:-1].tolist(), ' y =', window[1:].tolist())

Пара x, y — весь секрет обучающих данных языковой модели. y — это x, сдвинутый на один токен: в каждой позиции модель видит всё до этой позиции включительно и должна угадать следующий токен. Одно окно в 512 токенов даёт сразу 512 таких вопросов, а один шаг обучения (32 768 токенов) — 32 768. Никаких меток и разметчиков: текст сам себе ответ.

Почему токены хранятся в uint16, а не в обычных 32-битных целых?

В два байта помещаются числа до 65 535. При словаре в 8 192 токена этого с запасом хватает, и train.bin занимает 2,85 ГБ вместо 5,7. При словаре больше 65 536 токенов uint16 уже недостаточно: можно использовать, например, 32-битные целые.

Сколько текста нужно?

У нас есть 1,43 миллиарда токенов, а учимся мы на 330 миллионах. Почему не на всех? И почему не на меньшем? Чтобы ответить, нужна мера стоимости.

Теперь можно поставить вопрос о бюджете: как распределить вычисления $C$ между числом параметров $N$ и количеством обучающих токенов $D$? Если взять слишком большую модель, на её обучение не хватит текста и шагов. Если долго обучать маленькую, улучшения могут стать настолько медленными, что выгоднее было бы увеличить её размер. В экспериментах DeepMind 2022 года оптимальный баланс менялся примерно пропорционально: больше параметров — больше токенов. Отсюда известный ориентир — около 20 токенов на параметр. Это эмпирическое правило для определённых условий, а не универсальная норма.

Для Ростка этот ориентир даёт $17{,}31 \text{ млн} \times 20 \approx 346$ миллионов токенов. Наши 330 миллионов — примерно 19 токенов на параметр. Числа близки, но это ещё не доказывает оптимальность: Росток меньше моделей в исходных экспериментах, и корпус у него другой. Мы использовали правило как начальную оценку, согласованную с бюджетом в несколько часов на одном Mac. Перетащите точку на графике и сравните варианты:

Обе оси логарифмические. Пунктирные диагонали — линии равных вычислений $6ND$, золотая линия — ориентир 20 токенов на параметр. «На нашем Mac» — время при скорости, реально измеренной на Ростке: 29,7 тысячи токенов в секунду на Mac с чипом M4 Pro, это около $3 \cdot 10^{12}$ полезных операций в секунду. Настоящий запуск шёл несколько медленнее этого замера; его реальное время показывает съёмка в следующей главе.

Для сравнения, Llama 3 8B обучали более чем на 15 триллионах токенов — это свыше тысячи токенов на параметр. Почему так далеко от двадцати? Правило Chinchilla ищет выгодное распределение обучающих вычислений. Если модель потом много раз используют, имеет значение и стоимость каждого ответа. Тогда может оказаться выгоднее дольше обучать небольшую модель: потратить больше один раз, чтобы затем дешевле её запускать. У Ростка другая цель — показать полный цикл обучения в пределах одного короткого эксперимента.

Что Росток читает чаще всего

Напоследок посмотрим на корпус глазами модели. Вот самые частые токены проверочного текста и то, какую долю всего текста они покрывают.

Подсчёт по 7,17 миллиона токенов проверочного текста. В списке 400 самых частых токенов; остальные 7 792 делят между собой оставшуюся четверть текста.

Десять самых частых токенов — точка, запятая, перевод строки и короткие слова вроде « the», « and», « a» и « to» — составляют больше четверти текста, а первая сотня — больше половины. Это похоже на неравномерное распределение частот, которое мы обсуждали вместе с законом Ципфа в главе 6. Частые токены дают много обучающих примеров, но угадать их не всегда просто: выбор между « a» и « the» тоже зависит от контекста. По мере обучения улучшаются и редкие продолжения, и использование знакомых слов в сложных случаях.

А теперь посмотрите, кто живёт в этом мире. « Lily» и « Tim» встречаются чаще большинства глаголов, « happy» — чаще, чем « sad», а « joy» и « together» входят в первые две сотни. Это отпечатки моделей, которые писали истории. Росток их унаследует: он будет добрым, немного сентиментальным и неравнодушным к имени Lily. Теперь вы знаете, откуда это.

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

Это настоящий Росток в полный рост: восемь блоков, шесть голов, 17,31 миллиона параметров. Только веса у него — случайные числа, и он не прочитал ни одного токена корпуса. Попросите его написать что-нибудь — и увидите случайную последовательность токенов. Спросите, насколько его удивляет настоящая история, — и ответ будет близок к $\ln 8192 \approx 9{,}01$ ната на токен: ровно столько сказала бы модель, которая размазывает вероятность поровну по всем 8 192 токенам. В следующей главе Росток начнёт читать свою библиотеку, и примерно за 1 500 шагов это число опустится ниже двух (к последнему шагу — до 1,55).

Главы

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