Перейти к содержимому

Токенизатор и данные

llm.tokenizers.BPETokenizer — символьный BPE: словарь начинается с символов корпуса, слияния учатся внутри слов (текст заранее разбивается на слова, как в GPT-2: пробел прикрепляется к началу следующего слова, пунктуация — отдельно). Новый текст кодируется применением выученных слияний по порядку ранга, как в GPT-2 и HuggingFace. Как это работает — в главе Токенизация.

from llm.tokenizers import BPETokenizer
texts = ["Нейронные сети учатся на данных.", "Трансформеры обрабатывают последовательности."]
tokenizer = BPETokenizer()
tokenizer.train(texts=texts, vocab_size=300, special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"])
ids = tokenizer.encode("Нейронные сети") # список int
ids_with_bos_eos = tokenizer.encode("Нейронные сети", add_special_tokens=True)
text = tokenizer.decode(ids) # "Нейронные сети"
tokenizer.save("bpe_tokenizer.json")
tokenizer = BPETokenizer.load("bpe_tokenizer.json")
print(tokenizer.get_vocab_size(), tokenizer.pad_token_id, tokenizer.eos_token_id)

Что важно знать:

  • vocab_size в train — число обученных токенов без специальных; итоговый словарь — tokenizer.get_vocab_size(). Его и передавайте в конфиг модели, иначе id специальных токенов выйдут за пределы матрицы эмбеддингов.
  • На маленьком корпусе обучение останавливается раньше vocab_size, когда каждое слово уже стало одним токеном.
  • Специальные токены добавляются в конец словаря, поэтому pad_token_id — не 0. Внутри текста они не распознаются: строка "<eos>" кодируется как обычные символы; вставить специальный токен можно только по id.
  • Неизвестный символ (которого не было в корпусе) кодируется как <unk>. Без <unk> в special_tokens — ValueError с перечнем таких символов. decode по умолчанию выбрасывает специальные токены, включая <unk>, как skip_special_tokens в HF; увидеть их — decode(ids, skip_special_tokens=False).
  • Модель и токенизатор — пара. Модель, обученная с одним словарём, бессмысленна с другим. Для чужих весов нужен их токенизатор (например, из transformers).

Три датасета из llm.datasets принимают список строк, токенизатор и block_size и возвращают словари с тремя тензорами формы [block_size]:

КлючЧто внутри
input_idsтокены строки; длиннее block_size — обрезаются, короче — дополняются pad_token_id справа
attention_mask1 для настоящих токенов, 0 для паддинга
labelsкопия input_ids, на паддинге -100 — такие позиции не входят в loss
КлассКогда токенизируетОсобенность
TextDatasetодин раз, в конструкторе—
StreamingTextDatasetпри каждом обращениистроки по-прежнему лежат в памяти списком
TextWithSpecialTokensDatasetв конструктореadd_bos=True / add_eos=True добавляют BOS и EOS; при обрезке для них оставляется место
from llm.datasets.text_dataset import TextDataset
dataset = TextDataset(texts, tokenizer, block_size=32)
item = dataset[0]
print(item["input_ids"].shape, item["attention_mask"].sum(), (item["labels"] == -100).sum())
  • Каждая строка — один пример. Датасеты не склеивают тексты и не режут длинный текст на блоки: всё после block_size токенов теряется. Для длинных документов разбейте их на строки сами.
  • Сдвиг меток делает не датасет, а Trainer: метки — копия входа, логит позиции t сравнивается с меткой t + 1. Не сдвигайте метки сами, иначе модель будет учиться предсказывать токен через один.
  • Свой датасет должен возвращать input_ids и labels (и, если есть паддинг, attention_mask), с -100 на позициях, которые не должны входить в loss. Паддинг отмечайте по месту, а не сравнением с pad_token_id: pad может совпадать с настоящим токеном.

В experiments/shared/configs.py лежит встроенный корпус TRAIN_TEXTS — 15 коротких русских предложений; load_training_data() из experiments/shared/data.py делит его 80/20. Он нужен, чтобы проверить, что обучение и генерация работают, а не чтобы получить качество: на 12 строках модель запоминает обучающие данные, валидационный loss остаётся около ln V. Для настоящих экспериментов подставьте свой корпус — список строк.