NLP Course documentation

Создание токенизатора, блок за блоком

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Создание токенизатора, блок за блоком

Ask a Question Open In Colab Open In Studio Lab

Как мы уже видели в предыдущих разделах, токенизация состоит из нескольких этапов:

  • Нормализация (любая необходимая очистка текста, например, удаление пробелов или подчеркиваний, нормализация Unicode и т. д.)
  • Предварительная токенизация (разделение входного текста на слова).
  • Прогон входных данных через модель (использование предварительно токенизированных слов для создания последовательности токенов)
  • Постобработка (добавление специальных токенов токенизатора, генерация маски внимания и идентификаторов типов токенов)

В качестве напоминания вот еще один взгляд на общий процесс:

The tokenization pipeline.

Библиотека 🤗 Tokenizers была создана для того, чтобы предоставить несколько вариантов каждого из этих шагов, которые вы можете смешивать и сочетать между собой. В этом разделе мы рассмотрим, как можно создать токенизатор с нуля, а не обучать новый токенизатор на основе старого, как мы делали в разделе 2. После этого вы сможете создать любой токенизатор, который только сможете придумать!

Точнее, библиотека построена вокруг центрального класса Tokenizer, а строительные блоки сгруппированы в подмодули:

  • normalizers содержит все возможные типы нормализаторов текста Normalizer, которые вы можете использовать (полный список здесь).
  • pre_tokenizers содержит все возможные типы предварительных токенизаторов PreTokenizer, которые вы можете использовать (полный список здесь).
  • models содержит различные типы моделей Model, которые вы можете использовать, такие как BPE, WordPiece и Unigram (полный список здесь).
  • trainers содержит все различные типы Trainer, которые вы можете использовать для обучения модели на корпусе (по одному на каждый тип модели; полный список здесь).
  • post_processors содержит различные типы постпроцессоров PostProcessor, которые вы можете использовать (полный список здесь).
  • decoders содержит различные типы декодеров Decoder, которые вы можете использовать для декодирования результатов токенизации (полный список здесь).

Весь список блоков вы можете найти здесь.

Получение корпуса текста

Для обучения нашего нового токенизатора мы будем использовать небольшой корпус текстов (чтобы примеры выполнялись быстро). Шаги по сбору корпуса аналогичны тем, что мы делали в начале этой главы, но на этот раз мы будем использовать набор данных WikiText-2:

from datasets import load_dataset

dataset = load_dataset("wikitext", name="wikitext-2-raw-v1", split="train")


def get_training_corpus():
    for i in range(0, len(dataset), 1000):
        yield dataset[i : i + 1000]["text"]

Функция get_training_corpus() - это генератор, который выдает батч из 1000 текстов, которые мы будем использовать для обучения токенизатора.

🤗 Токенизаторы также можно обучать непосредственно на текстовых файлах. Вот как мы можем сгенерировать текстовый файл, содержащий все тексты/входы из WikiText-2, который мы можем использовать локально:

with open("wikitext-2.txt", "w", encoding="utf-8") as f:
    for i in range(len(dataset)):
        f.write(dataset[i]["text"] + "\n")

Далее мы покажем вам, как блок за блоком построить собственные токенизаторы BERT, GPT-2 и XLNet. Это даст нам пример каждого из трех основных алгоритмов токенизации: WordPiece, BPE и Unigram. Начнем с BERT!

Создание токенизатора WordPiece с нуля

Чтобы создать токенизатор с помощью библиотеки 🤗 Tokenizers, мы начнем с инстанцирования объектов Tokenizer и model, затем установим для их атрибутов normalizer, pre_tokenizer, post_processor и decoder нужные нам значения.

Для этого примера мы создадим Tokenizer с моделью WordPiece:

from tokenizers import (
    decoders,
    models,
    normalizers,
    pre_tokenizers,
    processors,
    trainers,
    Tokenizer,
)

tokenizer = Tokenizer(models.WordPiece(unk_token="[UNK]"))

Мы должны указать unk_token, чтобы модель знала, что возвращать, когда она встречает символы, которых раньше не видела. Другие аргументы, которые мы можем задать здесь, включают vocab нашей модели (мы собираемся обучать модель, поэтому нам не нужно его задавать) и max_input_chars_per_word, который определяет максимальную длину для каждого слова (слова длиннее переданного значения будут разбиты на части).

Первым шагом токенизации является нормализация, поэтому начнем с нее. Поскольку BERT широко используется, существует BertNormalizer с классическими параметрами, которые мы можем установить для BERT: lowercase и strip_accents, которые не требуют пояснений; clean_text для удаления всех управляющих символов и замены повторяющихся пробелов на один; и handle_chinese_chars, который расставляет пробелы вокруг китайских символов. Чтобы повторить токенизатор bert-base-uncased, мы можем просто установить этот нормализатор:

tokenizer.normalizer = normalizers.BertNormalizer(lowercase=True)

Однако, как правило, при создании нового токенизатора у вас не будет доступа к такому удобному нормализатору, уже реализованному в библиотеке 🤗 Tokenizers, поэтому давайте посмотрим, как создать нормализатор BERT вручную. Библиотека предоставляет нормализатор Lowercase и нормализатор StripAccents, и вы можете комбинировать несколько нормализаторов с помощью Sequence:

tokenizer.normalizer = normalizers.Sequence(
    [normalizers.NFD(), normalizers.Lowercase(), normalizers.StripAccents()]
)

Мы также используем нормализатор Unicode NFD, поскольку в противном случае нормализатор StripAccents не сможет правильно распознать акцентированные символы и, следовательно, не удалит их.

Как мы уже видели ранее, мы можем использовать метод normalize_str() нормализатора, чтобы проверить, как он влияет на данный текст:

print(tokenizer.normalizer.normalize_str("Héllò hôw are ü?"))
hello how are u?

Далее если вы протестируете две версии предыдущих нормализаторов на строке, содержащей символ Unicode u"\u0085", то наверняка заметите, что эти два нормализатора не совсем эквивалентны.
Чтобы не усложнять версию с normalizers.Sequence, мы не включили в нее Regex-замены, которые требует BertNormalizer, когда аргумент clean_text установлен в True, что является поведением по умолчанию. Но не волнуйтесь: можно получить точно такую же нормализацию без использования удобного BertNormalizer, добавив два normalizers.Replace в последовательность нормализаторов.

Далее следует этап предварительной токенизации. Опять же, есть готовый BertPreTokenizer, который мы можем использовать:

tokenizer.pre_tokenizer = pre_tokenizers.BertPreTokenizer()

Или мы можем создать его с нуля:

tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()

Обратите внимание, что токенизатор Whitespace разделяет пробельные символы и все символы, которые не являются буквами, цифрами или символом подчеркивания, поэтому технически он разделяет пробельные символы и знаки пунктуации:

tokenizer.pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")
[('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
 ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

Если вы хотите выполнять разделение только по пробельным символам, то вместо этого следует использовать предварительный токенизатор WhitespaceSplit:

pre_tokenizer = pre_tokenizers.WhitespaceSplit()
pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")
[("Let's", (0, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre-tokenizer.', (14, 28))]

Как и в случае с нормализаторами, вы можете использовать Sequence для комбинирования нескольких предварительных токенизаторов:

pre_tokenizer = pre_tokenizers.Sequence(
    [pre_tokenizers.WhitespaceSplit(), pre_tokenizers.Punctuation()]
)
pre_tokenizer.pre_tokenize_str("Let's test my pre-tokenizer.")
[('Let', (0, 3)), ("'", (3, 4)), ('s', (4, 5)), ('test', (6, 10)), ('my', (11, 13)), ('pre', (14, 17)),
 ('-', (17, 18)), ('tokenizer', (18, 27)), ('.', (27, 28))]

Следующий шаг в конвейере токенизации - обработка входных данных с помощью модели. Мы уже указали нашу модель в инициализации, но нам все еще нужно обучить ее, для чего потребуется WordPieceTrainer. Главное, что нужно помнить при инстанцировании тренера в 🤗 Tokenizers, это то, что вам нужно передать ему все специальные токены, которые вы собираетесь использовать - иначе он не добавит их в словарь, поскольку их нет в обучающем корпусе:

special_tokens = ["[UNK]", "[PAD]", "[CLS]", "[SEP]", "[MASK]"]
trainer = trainers.WordPieceTrainer(vocab_size=25000, special_tokens=special_tokens)

Помимо указания vocab_size и special_tokens, мы можем задать min_frequency (количество раз, которое должен встретиться токен, чтобы быть включенным в словарь) или изменить continuing_subword_prefix (если мы хотим использовать что-то отличное от ##).

Чтобы обучить нашу модель с помощью итератора, который мы определили ранее, достаточно выполнить эту команду:

tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

Мы также можем использовать текстовые файлы для обучения нашего токенизатора, что будет выглядеть следующим образом (предварительно мы повторно инициализируем модель с пустым WordPiece):

tokenizer.model = models.WordPiece(unk_token="[UNK]")
tokenizer.train(["wikitext-2.txt"], trainer=trainer)

В обоих случаях мы можем проверить работу токенизатора на тексте, вызвав метод encode():

encoding = tokenizer.encode("Let's test this tokenizer.")
print(encoding.tokens)
['let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.']

Полученное encoding представляет собой Encoding, которое содержит все необходимые результаты работы токенизатора в разных атрибутах: ids, type_ids, tokens, offsets, attention_mask, special_tokens_mask и overflowing.

Последний шаг в конвейере токенизации - постобработка. Нам нужно добавить токен [CLS] в начале и токен [SEP] в конце (или после каждого предложения, если у нас есть пара предложений). Для этого мы будем использовать TemplateProcessor, но сначала нам нужно узнать идентификаторы токенов [CLS] и [SEP] в словаре:

cls_token_id = tokenizer.token_to_id("[CLS]")
sep_token_id = tokenizer.token_to_id("[SEP]")
print(cls_token_id, sep_token_id)
(2, 3)

Чтобы написать шаблон для TemplateProcessor, мы должны указать, как обрабатывать одно предложение и пару предложений. Для обоих случаев мы указываем специальные токены, которые мы хотим использовать; первое (или одиночное) предложение представлено $A, а второе предложение (если кодируется пара) представлено $B. Для каждого из них (специальных токенов и предложений) мы также указываем соответствующий идентификатор типа токена (token type ID) после двоеточия.

Таким образом, классический шаблон BERT определяется следующим образом:

tokenizer.post_processor = processors.TemplateProcessing(
    single=f"[CLS]:0 $A:0 [SEP]:0",
    pair=f"[CLS]:0 $A:0 [SEP]:0 $B:1 [SEP]:1",
    special_tokens=[("[CLS]", cls_token_id), ("[SEP]", sep_token_id)],
)

Обратите внимание, что нам нужно передать идентификаторы специальных токенов, чтобы токенизатор мог правильно преобразовать их в их идентификаторы.

Как только это будет добавлено, вернемся к нашему предыдущему примеру:

encoding = tokenizer.encode("Let's test this tokenizer.")
print(encoding.tokens)
['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '.', '[SEP]']

И на паре предложений мы получаем правильный результат:

encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences.")
print(encoding.tokens)
print(encoding.type_ids)
['[CLS]', 'let', "'", 's', 'test', 'this', 'tok', '##eni', '##zer', '...', '[SEP]', 'on', 'a', 'pair', 'of', 'sentences', '.', '[SEP]']
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]

Мы почти закончили создание этого токенизатора с нуля - остался последний шаг - добавить декодер:

tokenizer.decoder = decoders.WordPiece(prefix="##")

Давайте проверим его на нашем предыдущем encoding:

tokenizer.decode(encoding.ids)
"let's test this tokenizer... on a pair of sentences."

Отлично! Мы можем сохранить наш токенизатор в единственном JSON-файле следующим образом:

tokenizer.save("tokenizer.json")

Затем мы можем загрузить этот файл в объект Tokenizer с помощью метода from_file():

new_tokenizer = Tokenizer.from_file("tokenizer.json")

Чтобы использовать этот токенизатор в 🤗 Transformers, мы должны обернуть его в PreTrainedTokenizerFast. Мы можем использовать либо общий класс, либо, если наш токенизатор соответствует существующей модели, использовать этот класс (здесь BertTokenizerFast). Если вы используете этот урок для создания нового токенизатора, вам придется использовать первый вариант.

Чтобы обернуть токенизатор в PreTrainedTokenizerFast, мы можем либо передать собранный нами токенизатор как tokenizer_object, либо передать сохраненный файл токенизатора как tokenizer_file. Главное помнить, что нам придется вручную задавать все специальные токены, поскольку класс не может определить из объекта tokenizer, какой токен является токеном маски, токеном [CLS] и т. д.:

from transformers import PreTrainedTokenizerFast

wrapped_tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=tokenizer,
    # tokenizer_file="tokenizer.json", # В качестве альтернативы можно загрузить из файла токенизатора.
    unk_token="[UNK]",
    pad_token="[PAD]",
    cls_token="[CLS]",
    sep_token="[SEP]",
    mask_token="[MASK]",
)

Если вы используете определенный класс токенизатора (например, BertTokenizerFast), вам нужно будет указать только специальные токены, которые отличаются от токенов по умолчанию (здесь их нет):

from transformers import BertTokenizerFast

wrapped_tokenizer = BertTokenizerFast(tokenizer_object=tokenizer)

Затем вы можете использовать этот токенизатор, как и любой другой токенизатор 🤗 Transformers. Вы можете сохранить его с помощью метода save_pretrained() или загрузить на хаб с помощью метода push_to_hub().

Теперь, когда мы рассмотрели, как создать токенизатор WordPiece, давайте сделаем то же самое для токенизатора BPE. Мы будем двигаться немного быстрее, поскольку вы знаете все шаги, и подчеркнем только различия.

Создание токенизатора BPE с нуля

Теперь давайте создадим токенизатор GPT-2. Как и в случае с токенизатором BERT, мы начнем с инициализации Tokenizer с моделью BPE:

tokenizer = Tokenizer(models.BPE())

Также, как и в случае с BERT, мы могли бы инициализировать эту модель словарем, если бы он у нас был (в этом случае нам нужно было бы передать vocab и merges), но поскольку мы будем обучать с нуля, нам не нужно этого делать. Нам также не нужно указывать unk_token, потому что GPT-2 использует byte-level BPE, который не требует этого.

GPT-2 не использует нормализатор, поэтому мы пропускаем этот шаг и переходим непосредственно к предварительной токенизации:

tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)

Опция, которую мы добавили к ByteLevel, заключается в том, чтобы не добавлять пробел в начале предложения (в противном случае это происходит по умолчанию). Мы можем посмотреть на предварительную токенизацию примера текста, как было показано ранее:

tokenizer.pre_tokenizer.pre_tokenize_str("Let's test pre-tokenization!")
[('Let', (0, 3)), ("'s", (3, 5)), ('Ġtest', (5, 10)), ('Ġpre', (10, 14)), ('-', (14, 15)),
 ('tokenization', (15, 27)), ('!', (27, 28))]

Далее следует модель, которую нужно обучить. Для GPT-2 единственным специальным токеном является токен конца текста:

trainer = trainers.BpeTrainer(vocab_size=25000, special_tokens=["<|endoftext|>"])
tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

Как и в случае с WordPieceTrainer, а также vocab_size и special_tokens, мы можем указать min_frequency, если хотим, или если у нас есть суффикс конца слова (например, </w>), мы можем задать его с помощью end_of_word_suffix.

Этот токенизатор также может быть обучен на текстовых файлах:

tokenizer.model = models.BPE()
tokenizer.train(["wikitext-2.txt"], trainer=trainer)

Давайте посмотрим на пример токенизации текста:

encoding = tokenizer.encode("Let's test this tokenizer.")
print(encoding.tokens)
['L', 'et', "'", 's', 'Ġtest', 'Ġthis', 'Ġto', 'ken', 'izer', '.']

Мы применяем постобработку на уровне байтов для токенизатора GPT-2 следующим образом:

tokenizer.post_processor = processors.ByteLevel(trim_offsets=False)

Опция trim_offsets = False указывает постпроцессору, что мы должны оставить смещения токенов, начинающихся с ‘Ġ’, как есть: таким образом, начало смещения будет указывать на пробел перед словом, а не на первый символ слова (поскольку пробел технически является частью токена). Давайте посмотрим на результат с текстом, который мы только что закодировали, где 'Ġtest' - это токен с индексом 4:

sentence = "Let's test this tokenizer."
encoding = tokenizer.encode(sentence)
start, end = encoding.offsets[4]
sentence[start:end]
' test'

Наконец, мы добавляем декодер на уровне байтов:

tokenizer.decoder = decoders.ByteLevel()

и мы сможем перепроверить, правильно ли он работает:

tokenizer.decode(encoding.ids)
"Let's test this tokenizer."

Отлично! Теперь, когда мы закончили, мы можем сохранить токенизатор, как раньше, и обернуть его в PreTrainedTokenizerFast или GPT2TokenizerFast, если мы хотим использовать его в 🤗 Transformers:

from transformers import PreTrainedTokenizerFast

wrapped_tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=tokenizer,
    bos_token="<|endoftext|>",
    eos_token="<|endoftext|>",
)

или:

from transformers import GPT2TokenizerFast

wrapped_tokenizer = GPT2TokenizerFast(tokenizer_object=tokenizer)

В качестве последнего примера мы покажем вам, как создать токенизатор Unigram с нуля.

Создание токенизатора Unigram с нуля

Теперь давайте построим токенизатор XLNet. Как и в предыдущих токенизаторах, мы начнем с инициализации Tokenizer с моделью Unigram:

tokenizer = Tokenizer(models.Unigram())

Опять же, мы могли бы инициализировать эту модель словарем, если бы он у нас был.

Для нормализации XLNet использует несколько замен (которые пришли из SentencePiece):

from tokenizers import Regex

tokenizer.normalizer = normalizers.Sequence(
    [
        normalizers.Replace("``", '"'),
        normalizers.Replace("''", '"'),
        normalizers.NFKD(),
        normalizers.StripAccents(),
        normalizers.Replace(Regex(" {2,}"), " "),
    ]
)

Он заменяет и with и любую последовательность из двух или более пробелов на один пробел, а также удаляет ударения в токенезируемых текстах.

Предварительный токенизатор, который должен использоваться для любого токенизатора SentencePiece, - это Metaspace:

tokenizer.pre_tokenizer = pre_tokenizers.Metaspace()

Мы можем посмотреть на предварительную токенизацию примера текста, как было показано ранее:

tokenizer.pre_tokenizer.pre_tokenize_str("Let's test the pre-tokenizer!")
[("▁Let's", (0, 5)), ('▁test', (5, 10)), ('▁the', (10, 14)), ('▁pre-tokenizer!', (14, 29))]

Далее следует модель, которую нужно обучить. В XLNet довольно много специальных токенов:

special_tokens = ["<cls>", "<sep>", "<unk>", "<pad>", "<mask>", "<s>", "</s>"]
trainer = trainers.UnigramTrainer(
    vocab_size=25000, special_tokens=special_tokens, unk_token="<unk>"
)
tokenizer.train_from_iterator(get_training_corpus(), trainer=trainer)

Очень важный аргумент, который не стоит забывать для UnigramTrainer - это unk_token. Мы также можем передавать другие аргументы, специфичные для алгоритма Unigram, такие как shrinking_factor для каждого шага удаления токенов (по умолчанию 0.75) или max_piece_length для указания максимальной длины данного токена (по умолчанию 16).

Этот токенизатор также может быть обучен на текстовых файлах:

tokenizer.model = models.Unigram()
tokenizer.train(["wikitext-2.txt"], trainer=trainer)

Давайте посмотрим на токенизацию примера текста:

encoding = tokenizer.encode("Let's test this tokenizer.")
print(encoding.tokens)
['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.']

Особенностью XLNet является то, что он помещает токен <cls> в конец предложения с идентификатором типа 2 (чтобы отличить его от других токенов). В результате он помещается слева. Мы можем разобраться со всеми специальными токенами и идентификаторами типов токенов с помощью шаблона, как в BERT, но сначала нам нужно получить идентификаторы токенов <cls> и <sep>:

cls_token_id = tokenizer.token_to_id("<cls>")
sep_token_id = tokenizer.token_to_id("<sep>")
print(cls_token_id, sep_token_id)
0 1

Шаблон выглядит следующим образом:

tokenizer.post_processor = processors.TemplateProcessing(
    single="$A:0 <sep>:0 <cls>:2",
    pair="$A:0 <sep>:0 $B:1 <sep>:1 <cls>:2",
    special_tokens=[("<sep>", sep_token_id), ("<cls>", cls_token_id)],
)

И мы можем проверить, как это работает, закодировав пару предложений:

encoding = tokenizer.encode("Let's test this tokenizer...", "on a pair of sentences!")
print(encoding.tokens)
print(encoding.type_ids)
['▁Let', "'", 's', '▁test', '▁this', '▁to', 'ken', 'izer', '.', '.', '.', '<sep>', '▁', 'on', '▁', 'a', '▁pair', 
  '▁of', '▁sentence', 's', '!', '<sep>', '<cls>']
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2]

Наконец, мы добавляем декодер Metaspace:

tokenizer.decoder = decoders.Metaspace()

и мы закончили работу с этим токенизатором! Мы можем сохранить токенизатор, как и раньше, и обернуть его в PreTrainedTokenizerFast или XLNetTokenizerFast, если мы хотим использовать его в 🤗 Transformers. При использовании PreTrainedTokenizerFast следует обратить внимание на то, что помимо специальных токенов, нам нужно указать библиотеке 🤗 Transformers на то, чтобы они располагались слева:

from transformers import PreTrainedTokenizerFast

wrapped_tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=tokenizer,
    bos_token="<s>",
    eos_token="</s>",
    unk_token="<unk>",
    pad_token="<pad>",
    cls_token="<cls>",
    sep_token="<sep>",
    mask_token="<mask>",
    padding_side="left",
)

Или альтернативно:

from transformers import XLNetTokenizerFast

wrapped_tokenizer = XLNetTokenizerFast(tokenizer_object=tokenizer)

Теперь, когда вы увидели, как различные блоки используются для создания существующих токенизаторов, вы должны быть в состоянии написать любой токенизатор, который вы хотите, с помощью библиотеки 🤗 Tokenizers и использовать его в 🤗 Transformers.