Buckets:

rtrm's picture
|
download
raw
23.9 kB
# Обучение нового токенизатора на основе старого[[training-a-new-tokenizer-from-an-old-one]]
<CourseFloatingBanner chapter={6}
classNames="absolute z-10 right-0 top-0"
notebooks={[
{label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/en/chapter6/section2.ipynb"},
{label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/en/chapter6/section2.ipynb"},
]} />
Если языковая модель не доступна на интересующем вас языке или ваш корпус сильно отличается от того, на котором обучалась языковая модель, вам, скорее всего, придется заново обучать модель с нуля, используя токенизатор, адаптированный к вашим данным. Для этого потребуется обучить новый токенизатор на вашем наборе данных. Но что именно это значит? Когда мы впервые рассматривали токенизаторы в [Главе 2](../chapter2/1), мы увидели, что большинство моделей трансформеров используют _алгоритм токенизации по подсловам_. Чтобы определить, какие подслова представляют интерес и наиболее часто встречаются в корпусе, токенизатор должен внимательно изучить все тексты в корпусе - этот процесс мы называем *обучением*. Точные правила обучения зависят от типа используемого токенизатора, далее в этой главе мы рассмотрим три основных алгоритма.
<Youtube id="DJimQynXZsQ"/>
> [!WARNING]
> ⚠️ Обучение токенизатора - это не то же самое, что обучение модели! При обучении модели используется стохастический градиентный спуск, чтобы сделать потери немного меньше для каждого батча. Оно рандомизировано по своей природе (это означает, что вам нужно задать некоторое число seed, чтобы получить одинаковые результаты при повторном обучении). Обучение токенизатора - это статистический процесс, который пытается определить, какие подслова лучше всего выбрать для данного корпуса, а точные правила, используемые для их выбора, зависят от алгоритма токенизации. Это детерминированный процесс, то есть вы всегда получите одинаковые результаты при обучении одного и того же алгоритма на одном и том же корпусе.
## Сбор корпуса слов[[assembling-a-corpus]]
В 🤗 Transformers есть очень простой API, который можно использовать для обучения нового токенизатора с теми же характеристиками, что и у существующего: `AutoTokenizer.train_new_from_iterator()`. Чтобы увидеть это в действии, предположим, что мы хотим обучить GPT-2 с нуля, но на языке, отличном от английского. Нашей первой задачей будет собрать много данных на этом языке в обучающий корпус. Чтобы примеры были понятны всем, мы будем использовать не русский или китайский язык, а будем использовать специализированный английский: Python-код.
Библиотека [🤗 Datasets](https://github.com/huggingface/datasets) может помочь нам собрать корпус исходного кода Python. Мы воспользуемся обычной функцией `load_dataset()` для загрузки и кэширования набора данных [CodeSearchNet](https://huggingface.co/datasets/code_search_net). Этот набор данных был создан для конкурса [CodeSearchNet challenge](https://wandb.ai/github/CodeSearchNet/benchmark) и содержит миллионы функций из библиотек с открытым исходным кодом с GitHub на нескольких языках программирования. Здесь мы загрузим Python-часть этого набора данных:
```py
from datasets import load_dataset
# Загрузка может занять несколько минут, так что выпейте кофе или чай, пока ждете!
raw_datasets = load_dataset("code_search_net", "python")
```
Мы можем взглянуть на тренировочную часть датасета, чтобы узнать, к каким столбцам у нас есть доступ:
```py
raw_datasets["train"]
```
```python out
Dataset({
features: ['repository_name', 'func_path_in_repository', 'func_name', 'whole_func_string', 'language',
'func_code_string', 'func_code_tokens', 'func_documentation_string', 'func_documentation_tokens', 'split_name',
'func_code_url'
],
num_rows: 412178
})
```
Мы видим, что набор данных отделяет документацию от кода и предлагает токенизировать и то, и другое. Здесь мы просто используем колонку `whole_func_string` для обучения нашего токенизатора. Мы можем посмотреть пример одной из этих функций, обратившись к соответствующему индексу в части `train`:
```py
print(raw_datasets["train"][123456]["whole_func_string"])
```
который должен вывести следующее:
```out
def handle_simple_responses(
self, timeout_ms=None, info_cb=DEFAULT_MESSAGE_CALLBACK):
"""Accepts normal responses from the device.
Args:
timeout_ms: Timeout in milliseconds to wait for each response.
info_cb: Optional callback for text sent from the bootloader.
Returns:
OKAY packet's message.
"""
return self._accept_responses('OKAY', info_cb, timeout_ms=timeout_ms)
```
Первое, что нам нужно сделать, это преобразовать набор данных в _итератор_ списков текстов -- например, список списков текстов. Использование списков текстов позволит нашему токенизатору работать быстрее (обучение на батчах текстов вместо обработки отдельных текстов по одному), и это должен быть итератор, если мы хотим избежать необходимости держать в памяти все сразу. Если ваш корпус огромен, вы захотите воспользоваться тем, что 🤗 Datasets не загружает все в RAM, а хранит элементы набора данных на диске.
Следующее действие создаст список списков по 1 000 текстов в каждом, но загрузит все в память:
```py
# Не раскоментируйте следующую строку кода, если только ваш набор данных не маленький!
# training_corpus = [raw_datasets["train"][i: i + 1000]["whole_func_string"] for i in range(0, len(raw_datasets["train"]), 1000)]
```
Используя генератор Python, мы можем не загружать ничего в память Python до тех пор, пока это действительно не понадобится. Чтобы создать такой генератор, нужно просто заменить скобки на круглые скобки:
```py
training_corpus = (
raw_datasets["train"][i : i + 1000]["whole_func_string"]
for i in range(0, len(raw_datasets["train"]), 1000)
)
```
Эта строка кода не получает никаких элементов из набора данных; она просто создает объект, который можно использовать в цикле Python `for`. Тексты будут загружаться только тогда, когда они вам нужны (то есть когда вы находитесь на том шаге цикла `for`, где они требуются), и за один раз будет загружено только 1 000 текстов. Таким образом, вы не исчерпаете всю память, даже если обрабатываете огромный набор данных.
Проблема с объектом-генератором заключается в том, что он может быть использован только один раз. Поэтому вместо того, чтобы выдать нам список первых 10 цифр дважды:
```py
gen = (i for i in range(10))
print(list(gen))
print(list(gen))
```
мы получаем его один раз, а затем пустой список:
```python out
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
```
Поэтому мы определяем функцию, которая возвращает генератор:
```py
def get_training_corpus():
return (
raw_datasets["train"][i : i + 1000]["whole_func_string"]
for i in range(0, len(raw_datasets["train"]), 1000)
)
training_corpus = get_training_corpus()
```
Вы также можете определить свой генератор внутри цикла `for`, используя оператор `yield`:
```py
def get_training_corpus():
dataset = raw_datasets["train"]
for start_idx in range(0, len(dataset), 1000):
samples = dataset[start_idx : start_idx + 1000]
yield samples["whole_func_string"]
```
который выдает точно такой же генератор, как и предыдущий, но позволяет использовать более сложную логику, чем при работе с list comprehension.
## Обучение нового токенизатора[[training-a-new-tokenizer]]
Теперь, когда у нас есть корпус в виде итератора батчей текстов, мы готовы обучить новый токенизатор. Для этого нам сначала нужно загрузить токенизатор, который мы хотим использовать в паре с нашей моделью (здесь GPT-2):
```py
from transformers import AutoTokenizer
old_tokenizer = AutoTokenizer.from_pretrained("gpt2")
```
Несмотря на то, что мы собираемся обучить новый токенизатор, это хорошая идея сделать это, не начиная все с нуля. Таким образом, нам не придется ничего уточнять об алгоритме токенизации или специальных токенах, которые мы хотим использовать; наш новый токенизатор будет точно таким же, как GPT-2, и единственное, что изменится, - это словарный запас, который будет определен в результате обучения на нашем корпусе.
Сначала давайте посмотрим, как будет работать этот токенизатор с примером функции:
```py
example = '''def add_numbers(a, b):
"""Add the two numbers `a` and `b`."""
return a + b'''
tokens = old_tokenizer.tokenize(example)
tokens
```
```python out
['def', 'Ġadd', '_', 'n', 'umbers', '(', 'a', ',', 'Ġb', '):', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo',
'Ġnumbers', 'Ġ`', 'a', '`', 'Ġand', 'Ġ`', 'b', '`', '."', '""', 'Ċ', 'Ġ', 'Ġ', 'Ġ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
```
Этот токенизатор имеет несколько специальных символов, таких как `Ġ` и `Ċ`, которые обозначают пробелы и новые строки, соответственно. Как мы видим, это не слишком эффективно: токенизатор возвращает отдельные токены для каждого пробела, в то время как он мог бы группировать уровни отступов (поскольку наборы из четырех или восьми пробелов будут очень часто встречаться в коде). Он также немного странно разделил имя функции, не ожидая увидеть слова с символом `_`.
Давайте обучим новый токенизатор и посмотрим, решит ли он эти проблемы. Для этого мы воспользуемся методом `train_new_from_iterator()`:
```py
tokenizer = old_tokenizer.train_new_from_iterator(training_corpus, 52000)
```
Выполнение этой команды может занять много времени, если ваш корпус очень большой, но для данного набора данных с 1,6 ГБ текстов она работает молниеносно (1 минута 16 секунд на процессоре AMD Ryzen 9 3900X с 12 ядрами).
Обратите внимание, что `AutoTokenizer.train_new_from_iterator()` работает только в том случае, если используемый вами токенизатор является "быстрым" токенизатором. Как вы увидите в следующем разделе, библиотека 🤗 Transformers содержит два типа токенизаторов: одни написаны исключительно на Python, а другие (быстрые) опираются на библиотеку 🤗 Tokenizers, которая написана на языке программирования [Rust](https://www.rust-lang.org). Python - это язык, который чаще всего используется для приложений data science и deep learning, но когда что-то нужно распараллелить для быстроты, это приходится писать на другом языке. Например, матричные умножения, которые лежат в основе вычислений модели, написаны на CUDA, оптимизированной библиотеке языка C для GPU.
Обучение совершенно нового токенизатора на чистом Python было бы мучительно медленным, поэтому мы разработали библиотеку 🤗 Tokenizers. Обратите внимание, что так же как вам не нужно было изучать язык CUDA, чтобы выполнить свою модель на батче входных данных на GPU, вам не понадобится изучать Rust, чтобы использовать быстрый токенизатор. Библиотека 🤗 Tokenizers предоставляет привязки к Python для многих методов, которые внутренне вызывают некоторые части кода на Rust; например, для распараллеливания обучения вашего нового токенизатора или, как мы видели в [Главе 3](../chapter3/1), токенизации батча входных данных.
В большинстве моделей Transformer доступен быстрый токенизатор (есть некоторые исключения, о которых вы можете узнать [здесь](https://huggingface.co/transformers/#supported-frameworks)), а API `AutoTokenizer` всегда выбирает быстрый токенизатор, если он доступен. В следующем разделе мы рассмотрим некоторые другие особенности быстрых токенизаторов, которые будут очень полезны для таких задач, как классификация токенов и ответы на вопросы. Однако прежде чем погрузиться в эту тему, давайте попробуем наш новый токенизатор на предыдущем примере:
```py
tokens = tokenizer.tokenize(example)
tokens
```
```python out
['def', 'Ġadd', '_', 'numbers', '(', 'a', ',', 'Ġb', '):', 'ĊĠĠĠ', 'Ġ"""', 'Add', 'Ġthe', 'Ġtwo', 'Ġnumbers', 'Ġ`',
'a', '`', 'Ġand', 'Ġ`', 'b', '`."""', 'ĊĠĠĠ', 'Ġreturn', 'Ġa', 'Ġ+', 'Ġb']
```
Здесь мы снова видим специальные символы `Ġ` и `Ċ`, обозначающие пробелы и новые строки, но мы также видим, что наш токенизатор выучил некоторые токены, которые очень специфичны для корпуса функций Python: например, есть токен `ĊĠĠĠ`, который обозначает отступ, и токен `Ġ"""`, который обозначает три кавычки, с которых начинается doc-строка. Токенизатор также правильно разделил имя функции на `_`. Это довольно компактное представление; для сравнения, использование токенизатора простого английского языка для того же примера даст нам более длинное предложение:
```py
print(len(tokens))
print(len(old_tokenizer.tokenize(example)))
```
```python out
27
36
```
Давайте рассмотрим другой пример:
```python
example = """class LinearLayer():
def __init__(self, input_size, output_size):
self.weight = torch.randn(input_size, output_size)
self.bias = torch.zeros(output_size)
def __call__(self, x):
return x @ self.weights + self.bias
"""
tokenizer.tokenize(example)
```
```python out
['class', 'ĠLinear', 'Layer', '():', 'ĊĠĠĠ', 'Ġdef', 'Ġ__', 'init', '__(', 'self', ',', 'Ġinput', '_', 'size', ',',
'Ġoutput', '_', 'size', '):', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'weight', 'Ġ=', 'Ġtorch', '.', 'randn', '(', 'input', '_',
'size', ',', 'Ġoutput', '_', 'size', ')', 'ĊĠĠĠĠĠĠĠ', 'Ġself', '.', 'bias', 'Ġ=', 'Ġtorch', '.', 'zeros', '(',
'output', '_', 'size', ')', 'ĊĊĠĠĠ', 'Ġdef', 'Ġ__', 'call', '__(', 'self', ',', 'Ġx', '):', 'ĊĠĠĠĠĠĠĠ',
'Ġreturn', 'Ġx', 'Ġ@', 'Ġself', '.', 'weights', 'Ġ+', 'Ġself', '.', 'bias', 'ĊĠĠĠĠ']
```
В дополнение к токену, соответствующему отступу, здесь мы также видим токен для двойного отступа: `ĊĠĠĠĠĠĠĠĠĠ`. Специальные слова Python, такие как `class`, `init`, `call`, `self` и `return`, обрабатываются как один токен, и мы видим, что наряду с разделением на `_` и `.` токенизатор правильно разделяет даже имена с camel-case: `LinearLayer` обрабатывается как `["ĠLinear", "Layer"]`.
## Сохранение токенизатора[[saving-the-tokenizer]]
Чтобы убедиться, что мы сможем использовать его позже, нам нужно сохранить наш новый токенизатор. Как и для моделей, это делается с помощью метода `save_pretrained()`:
```py
tokenizer.save_pretrained("code-search-net-tokenizer")
```
В результате будет создана новая папка с именем *code-search-net-tokenizer*, в которой будут содержаться все файлы, необходимые токенизатору для загрузки. Если вы хотите поделиться этим токенизатором со своими коллегами и друзьями, вы можете загрузить его на Hub, войдя в свою учетную запись. Если вы работаете в блокноте, есть удобная функция, которая поможет вам в этом:
```python
from huggingface_hub import notebook_login
notebook_login()
```
Появится виджет, в котором вы можете ввести свои учетные данные для входа в Hugging Face. Если вы работаете не в блокноте, просто введите следующую строку в терминале:
```bash
huggingface-cli login
```
После того как вы авторизовались, вы можете опубликовать свой токенизатор, выполнив следующую команду:
```py
tokenizer.push_to_hub("code-search-net-tokenizer")
```
Это создаст новое хранилище в вашем пространстве имен с именем `code-search-net-tokenizer`, содержащее файл токенизатора. Затем вы можете загрузить токенизатор где угодно с помощью метода `from_pretrained()`:
```py
# Замените "huggingface-course" ниже своим реальным пространством имен, чтобы использовать свой собственный токенизатор
tokenizer = AutoTokenizer.from_pretrained("huggingface-course/code-search-net-tokenizer")
```
Теперь вы готовы обучить языковую модель с нуля и дообучить ее в соответствии с поставленной задачей! Мы займемся этим в [Главе 7](../chapter7), но сначала в этой главе мы рассмотрим быстрые токенизаторы и подробно изучим, что происходит при вызове метода `train_new_from_iterator()`.
<EditOnGithub source="https://github.com/huggingface/course/blob/main/chapters/ru/chapter6/2.mdx" />

Xet Storage Details

Size:
23.9 kB
·
Xet hash:
cb6b2e2b15b45f47899706087e9cdacfb9643ec5db581baf3b26116cb7f09976

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.