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

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

Корпус

Модель — это то, что она прочитала. Откуда взялись 1,4 миллиарда токенов Ростка, как мы их чистили и сколько текста на самом деле нужно модели такого размера.

В этой главе

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

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

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

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

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

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

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

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

SODA — зверь другой породы: бытовые диалоги двух людей, и перед каждым — короткое вступление, которое описывает ситуацию. Каждый диалог мы превратили в строки вида 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% файла. К концу обучения большинство клеток едва тронуто цветом: около четырёх пятых корпуса Росток не увидит вообще, а дважды и больше увидит лишь около 2%. Росток не читает свою библиотеку от корки до корки — и ему это не нужно: в разделе про бюджет токенов мы увидим, что 330 миллионов — как раз правильная доза для его размера.

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

Чистка

В сыром тексте всегда есть мусор, даже если его написала модель. Типографские кавычки “вот такие” и прямые "вот такие" значат одно и то же, но для токенизатора это разные байты, а значит, разные токены. Модели пришлось бы дважды учить, что кавычка открывает реплику. То же с тремя видами тире, символом многоточия, неразрывным пробелом и переводами строк в стиле 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

Последнее правило стоит разобрать подробнее. Оно считает символы за пределами печатного ASCII и выбрасывает текст, если их больше 0,2%. Для истории в 500 символов это значит: один чужой символ можно, два уже нельзя. Правило грубое: оно убирает русский, китайский, эмодзи и «кракозябры» (текст, прочитанный не в той кодировке и превратившийся в «café»), но заодно и невинную историю со словом «café». Для маленькой англоязычной модели это разумный компромисс: каждый такой символ стоит двух-трёх токенов и не учит Ростка ничему полезному.

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

  • 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». Два оказались слишком короткими.

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

Дубликаты

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

Точные дубликаты искать дёшево. Для каждого документа считаем хеш — короткий отпечаток в 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|>. Модель быстро выучивает его смысл: всё, что было до него, никак не связано с тем, что идёт после, — начинается новый текст. Этот же токен позволяет генерировать «с чистого листа»: даём модели один <|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 токенов) приходится тратить четыре байта.

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

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

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

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

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

Посмотрите на Llama 3 8B: 15 триллионов токенов на 8 миллиардов параметров, почти две тысячи токенов на параметр — в сто раз больше, чем советует Chinchilla. Это не ошибка. Chinchilla оптимизирует стоимость обучения, а популярную модель потом запускают миллиарды раз, и маленькую запускать дешевле. Поэтому сегодня выгодно учить маленькую модель гораздо дольше «оптимума»: ошибка продолжает снижаться, просто медленнее. Для Ростка мы выбрали классический оптимум: нашим настоящим ограничением было само обучение — несколько часов на одном Mac.

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

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

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

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

А теперь посмотрите, кто живёт в этом мире. « 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. Документы и&nbsp;окна
    6. Из текста в&nbsp;train.bin
    7. Сколько текста нужно?
    8. Что Росток читает чаще всего
    9. Росток сейчас
  11. 10 Обучение
  12. 11 Выборка
  13. 12 Разговор
  14. 13 LoRA
  15. 14 Что дальше