From 2bf5df968fc05f89768c0ec14401064d14c6741e Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Tue, 9 Jan 2024 20:30:43 +0300 Subject: [PATCH 01/24] Completed the translation of the first part of Chapter 7 into Russian. --- chapters/ru/_toctree.yml | 13 + chapters/ru/chapter7/1.mdx | 38 ++ chapters/ru/chapter7/2.mdx | 981 ++++++++++++++++++++++++++++++++ chapters/ru/chapter7/3.mdx | 1044 +++++++++++++++++++++++++++++++++++ chapters/ru/chapter7/4.mdx | 1002 +++++++++++++++++++++++++++++++++ chapters/ru/chapter7/5.mdx | 1073 ++++++++++++++++++++++++++++++++++++ 6 files changed, 4151 insertions(+) create mode 100644 chapters/ru/chapter7/1.mdx create mode 100644 chapters/ru/chapter7/2.mdx create mode 100644 chapters/ru/chapter7/3.mdx create mode 100644 chapters/ru/chapter7/4.mdx create mode 100644 chapters/ru/chapter7/5.mdx diff --git a/chapters/ru/_toctree.yml b/chapters/ru/_toctree.yml index 597730c46..fa28f0fca 100644 --- a/chapters/ru/_toctree.yml +++ b/chapters/ru/_toctree.yml @@ -113,6 +113,19 @@ title: Тест в конце главы quiz: 6 +- title: 7. Основные задачи NLP + sections: + - local: chapter7/1 + title: Введение + - local: chapter7/2 + title: Классификация токенов + - local: chapter7/3 + title: Дообучение модели маскированного языкового моделирования + - local: chapter7/4 + title: Перевод + - local: chapter7/5 + title: Суммаризация + - title: Глоссарий sections: - local: glossary/1 diff --git a/chapters/ru/chapter7/1.mdx b/chapters/ru/chapter7/1.mdx new file mode 100644 index 000000000..43df2873c --- /dev/null +++ b/chapters/ru/chapter7/1.mdx @@ -0,0 +1,38 @@ + + +# Введение[[introduction]] + + + +В [Главе 3] (/course/chapter3) вы узнали, как дообучить модель для классификации текстов. В этой главе мы рассмотрим следующие общие задачи NLP: + +- Классификация токенов (Token classification) +- Маскированное языковое моделирование (Masked language modeling, например, BERT) +- Резюмирование текста (Summarization) +- Перевод (Translation) +- Предварительное обучение каузального языкового моделирования (Causal language modeling, например, GPT-2) +- Ответы на вопросы (Question answering) + +{#if fw === 'pt'} + +Для этого вам понадобится использовать все, что вы узнали об API `Trainer` и библиотеке 🤗 Accelerate в [Главе 3](/course/chapter3), библиотеке 🤗 Datasets в [Главе 5](/course/chapter5) и библиотеке 🤗 Tokenizers в [Главе 6](/course/chapter6). Мы также загрузим наши результаты в хаб моделей, как мы делали это в [Главе 4](/course/chapter4), так что это действительно глава,в которой все собирается воедино! + +Каждый раздел можно читать независимо друг от друга, и в нем вы узнаете, как обучить модель с помощью API `Trainer` или с помощью собственного цикла обучения, используя 🤗 Accelerate. Вы можете пропустить любую часть и сосредоточиться на той, которая вас больше всего интересует: API `Trainer` отлично подходит для того, чтобы дообучить или обучить вашу модель, не беспокоясь о том, что происходит за кулисами, а цикл обучения с `Accelerate` позволит вам легче настроить любую часть, которую вы хотите. + +{:else} + +Для этого вам понадобится использовать все, что вы узнали об обучении моделей с помощью Keras API в [Главе 3](/course/chapter3), библиотеке 🤗 Datasets в [Главе 5](/course/chapter5) и библиотеке 🤗 Tokenizers в [Главе 6](/course/chapter6). Мы также загрузим наши результаты в хаб моделей, как мы делали это в [Главе 4](/course/chapter4), так что это действительно глава, в которой все собирается воедино! + +Каждый раздел можно читать самостоятельно. + +{/if} + + + + +Если вы будете читать разделы по порядку, то заметите, что в них довольно много общего в коде и тексте. Повторение сделано намеренно, чтобы вы могли погрузиться (или вернуться позже) в любую интересующую вас задачу и найти полный рабочий пример. + + diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx new file mode 100644 index 000000000..7f7aa7b65 --- /dev/null +++ b/chapters/ru/chapter7/2.mdx @@ -0,0 +1,981 @@ + + +# Классификация токенов[[token-classification]] + +{#if fw === 'pt'} + + + +{:else} + + + +{/if} + +Первое приложение, которое мы рассмотрим, - это классификация токенов. Эта общая задача охватывает любую проблему, которую можно сформулировать как "присвоение метки каждому токену в предложении", например: + +- **Распознавание именованных сущностей (Named entity recognition - NER)**: Поиск сущностей (например, лиц, мест или организаций) в предложении. Это можно сформулировать как приписывание метки каждому токену, имея один класс для сущности и один класс для "нет сущности". +- **Морфологическая разметка (Part-of-speech tagging - POS)**: Пометить каждое слово в предложении как соответствующее определенной части речи (например, существительное, глагол, прилагательное и т. д.). +- **Выделение токенов (Chunking)**: Поиск токенов, принадлежащих одной и той же сущности. Эта задача (которая может быть объединена с POS или NER) может быть сформулирована как присвоение одной метки (обычно `B-`) всем токенам, которые находятся в начале фрагмента текста, другой метки (обычно `I-`) - токенам, которые находятся внутри фрагмента текста, и третьей метки (обычно `O`) - токенам, которые не принадлежат ни к одному фрагменту. + + + +Конечно, существует множество других типов задач классификации токенов; это лишь несколько показательных примеров. В этом разделе мы дообучим модель (BERT) для задачи NER, которая затем сможет вычислять прогнозы, подобные этому: + + + + +One-hot encoded labels for question answering. + + + +Вы можете найти модель, которую мы обучим и загрузим на хаб, и перепроверить ее предсказания [здесь](https://huggingface.co/huggingface-course/bert-finetuned-ner?text=My+name+is+Sylvain+and+I+work+at+Hugging+Face+in+Brooklyn). + +## Подготовка данных[[preparing-the-data]] + +Прежде всего, нам нужен набор данных, подходящий для классификации токенов. В этом разделе мы будем использовать [набор данных CoNLL-2003](https://huggingface.co/datasets/conll2003), который содержит новости от Reuters. + + + +💡 Если ваш набор данных состоит из текстов, часть которых состоит из слов с соответствующими метками, вы сможете адаптировать описанные здесь процедуры обработки данных к своему набору данных. Обратитесь к [Главе 5] (/course/chapter5), если вам нужно освежить в памяти то, как загружать собственные данные в `Dataset`. + + + +### Датасет CoNLL-2003[[the-conll-2003-dataset]] + +Для загрузки датасета CoNLL-2003 мы используем метод `load_dataset()` из библиотеки 🤗 Datasets: + +```py +from datasets import load_dataset + +raw_datasets = load_dataset("conll2003") +``` + +Это позволит загрузить и кэшировать датасет, как мы видели в [Главе 3](/course/chapter3) для датасета GLUE MRPC. Изучение этого объекта показывает нам присутствующие столбцы и части тренировочного, проверочного и тестового наборов: + +```py +raw_datasets +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'], + num_rows: 14041 + }) + validation: Dataset({ + features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'], + num_rows: 3250 + }) + test: Dataset({ + features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'], + num_rows: 3453 + }) +}) +``` + +В частности, мы видим, что датасет содержит метки для трех задач, о которых мы говорили ранее: NER, POS и chunking. Существенным отличием от других датасетов является то, что входные тексты представлены не как предложения или документы, а как списки слов (последний столбец называется `tokens`, но он содержит слова в том смысле, что это предварительно токинизированные входные данные, которые еще должны пройти через токенизатор для токенизации по подсловам). + +Давайте посмотрим на первый элемент обучающего набора: + +```py +raw_datasets["train"][0]["tokens"] +``` + +```python out +['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'] +``` + +Поскольку мы хотим выполнить распознавание именованных сущностей, мы изучим теги NER: + +```py +raw_datasets["train"][0]["ner_tags"] +``` + +```python out +[3, 0, 7, 0, 0, 0, 7, 0, 0] +``` + +Это метки в виде целых чисел, готовые для обучения, но они не всегда полезны, когда мы хотим проанализировать данные. Как и в случае с классификацией текста, мы можем получить доступ к соответствию между этими целыми числами и названиями меток, посмотрев на атрибут `features` нашего датасета: + +```py +ner_feature = raw_datasets["train"].features["ner_tags"] +ner_feature +``` + +```python out +Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None) +``` + +Таким образом, этот столбец содержит элементы, которые являются последовательностями `ClassLabel`. Тип элементов последовательности указан в атрибуте `feature` этого `ner_feature`, и мы можем получить доступ к списку имен, посмотрев на атрибут `names` этого `feature`: + +```py +label_names = ner_feature.feature.names +label_names +``` + +```python out +['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'] +``` + +Мы уже видели эти метки при изучении конвейера `token-classification` в [Главе 6] (/course/chapter6/3), но для краткости напомним: + +- `O` означает, что слово не соответствует какой-либо сущности. +- `B-PER`/`I-PER` означает, что слово соответствует началу/находится внутри сущности персоны *person*. +- `B-ORG`/`I-ORG` означает, что слово соответствует началу/находится внутри сущности *organization*. +- `B-LOC`/`I-LOC` означает, что слово соответствует началу/находится внутри сущности *location*. +- `B-MISC`/`I-MISC` означает, что слово соответствует началу/находится внутри сущности *miscellaneous*. + +Теперь декодирование меток, которые мы видели ранее, дает нам следующее: + +```python +words = raw_datasets["train"][0]["tokens"] +labels = raw_datasets["train"][0]["ner_tags"] +line1 = "" +line2 = "" +for word, label in zip(words, labels): + full_label = label_names[label] + max_length = max(len(word), len(full_label)) + line1 += word + " " * (max_length - len(word) + 1) + line2 += full_label + " " * (max_length - len(full_label) + 1) + +print(line1) +print(line2) +``` + +```python out +'EU rejects German call to boycott British lamb .' +'B-ORG O B-MISC O O O B-MISC O O' +``` + +В качестве примера смешивания меток `B-` и `I-`, вот что дает тот же код для элемента обучающего множества с индексом 4: + +```python out +'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .' +'B-LOC O O O O B-ORG I-ORG O O O B-PER I-PER O O O O O O O O O O O B-LOC O O O O O O O' +``` + +Как мы видим, сущностям, состоящим из двух слов, например "European Union" и "Werner Zwingmann", присваивается метка `B-` для первого слова и метка `I-` для второго. + + + +✏️ **Попробуйте!** Выведите те же два предложения с метками POS или chunking. + + + +### Обработка данных[[processing-the-data]] + + + +Как обычно, наши тексты должны быть преобразованы в идентификаторы токенов, прежде чем модель сможет понять их смысл. Как мы видели в [Главе 6](/course/chapter6/), существенным отличием задачи классификации токенов является то, что у нас есть предварительно токенизированные входные данные. К счастью, API токенизатора справляется с этим довольно легко; нам просто нужно предупредить `tokenizer` специальным флагом. + +Для начала давайте создадим объект `tokenizer`. Как мы уже говорили, мы будем использовать предварительно обученную модель BERT, поэтому начнем с загрузки и кэширования соответствующего токенизатора: + +```python +from transformers import AutoTokenizer + +model_checkpoint = "bert-base-cased" +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +``` + +Вы можете заменить `model_checkpoint` на любую другую модель из [Hub](https://huggingface.co/models) или на локальную папку, в которой вы сохранили предварительно обученную модель и токенизатор. Единственное ограничение - токенизатор должен быть создан с помощью библиотеки 🤗 Tokenizers, поэтому существует "быстрая" версия. Вы можете увидеть все архитектуры, которые поставляются с быстрой версией в [этой большой таблице](https://huggingface.co/transformers/#supported-frameworks), а чтобы проверить, что используемый вами объект `tokenizer` действительно поддерживается 🤗 Tokenizers, вы можете посмотреть на его атрибут `is_fast`: + +```py +tokenizer.is_fast +``` + +```python out +True +``` + +Для токенизации предварительно токинизированного ввода мы можем использовать наш `tokenizer`, как обычно, просто добавив `is_split_into_words=True`: + +```py +inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True) +inputs.tokens() +``` + +```python out +['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]'] +``` + +Как мы видим, токенизатор добавил специальные токены, используемые моделью (`[CLS]` в начале и `[SEP]` в конце), и оставил большинство слов нетронутыми. Слово `lamb`, однако, было токенизировано на два подслова, `la` и `##mb`. Это вносит несоответствие между нашими входными данными и метками: список меток состоит всего из 9 элементов, в то время как наши входные данные теперь содержат 12 токенов. Учесть специальные токены легко (мы знаем, что они находятся в начале и в конце), но нам также нужно убедиться, что мы выровняли все метки с соответствующими словами. + +К счастью, поскольку мы используем быстрый токенизатор, у нас есть доступ к суперспособностям 🤗 Tokenizers, что означает, что мы можем легко сопоставить каждый токен с соответствующим словом (как показано в [Глава 6](/course/chapter6/3)): + +```py +inputs.word_ids() +``` + +```python out +[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None] +``` + +Немного поработав, мы сможем расширить список меток, чтобы он соответствовал токенам. Первое правило, которое мы применим, заключается в том, что специальные токены получают метку `-100`. Это связано с тем, что по умолчанию `-100` - это индекс, который игнорируется в функции потерь, которую мы будем использовать (кросс-энтропия). Затем каждый токен получает ту же метку, что и токен, с которого началось слово, в котором он находится, поскольку они являются частью одной и той же сущности. Для токенов, находящихся внутри слова, но не в его начале, мы заменяем `B-` на `I-` (поскольку такие токены не являются началом сущности): + +```python +def align_labels_with_tokens(labels, word_ids): + new_labels = [] + current_word = None + for word_id in word_ids: + if word_id != current_word: + # Начало нового слова! + current_word = word_id + label = -100 if word_id is None else labels[word_id] + new_labels.append(label) + elif word_id is None: + # Специальный токен + new_labels.append(-100) + else: + # То же слово, что и предыдущий токен + label = labels[word_id] + # If the label is B-XXX we change it to I-XXX + if label % 2 == 1: + label += 1 + new_labels.append(label) + + return new_labels +``` + +Давайте опробуем это на нашем первом предложении: + +```py +labels = raw_datasets["train"][0]["ner_tags"] +word_ids = inputs.word_ids() +print(labels) +print(align_labels_with_tokens(labels, word_ids)) +``` + +```python out +[3, 0, 7, 0, 0, 0, 7, 0, 0] +[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100] +``` + +Как мы видим, наша функция добавила `-100` для двух специальных токенов в начале и в конце и новый `0` для нашего слова, которое было разбито на две части. + + + +✏️ **Попробуйте!** Некоторые исследователи предпочитают назначать только одну метку на слово и присваивать `-100` другим подтокенам в данном слове. Это делается для того, чтобы длинные слова, часть которых состоит из множества субтокенов, не вносили значительный вклад в потери. + + + +Чтобы предварительно обработать весь наш датасет, нам нужно провести токенизацию всех входных данных и применить `align_labels_with_tokens()` ко всем меткам. Чтобы воспользоваться преимуществами скорости нашего быстрого токенизатора, лучше всего токенизировать много текстов одновременно, поэтому мы напишем функцию, которая обрабатывает список примеров и использует метод `Dataset.map()` с параметром `batched=True`. Единственное отличие от нашего предыдущего примера заключается в том, что функция `word_ids()` должна получить индекс примера, идентификаторы слов которого нам нужны, с учётом того что входными данными для токенизатора являются списки текстов (или, в нашем случае, списки слов), поэтому мы добавляем и это: + +```py +def tokenize_and_align_labels(examples): + tokenized_inputs = tokenizer( + examples["tokens"], truncation=True, is_split_into_words=True + ) + all_labels = examples["ner_tags"] + new_labels = [] + for i, labels in enumerate(all_labels): + word_ids = tokenized_inputs.word_ids(i) + new_labels.append(align_labels_with_tokens(labels, word_ids)) + + tokenized_inputs["labels"] = new_labels + return tokenized_inputs +``` + +Обратите внимание, что мы еще не добавляли во входные данные дополняющие токены; мы сделаем это позже, при создании батчей с помощью коллатора данных. + +Теперь мы можем применить всю эту предварительную обработку к другим частям нашего датасета: + +```py +tokenized_datasets = raw_datasets.map( + tokenize_and_align_labels, + batched=True, + remove_columns=raw_datasets["train"].column_names, +) +``` + +Мы сделали самую сложную часть! Теперь, когда данные прошли предварительную обработку, само обучение будет выглядеть примерно так, как мы делали это в [Главе 3](/course/chapter3). + +{#if fw === 'pt'} + +## Дообучение модели с помощью API `Trainer`[[fine-tuning-the-model-with-the-trainer-api]] + +Фактический код, использующий `Trainer`, будет таким же, как и раньше; единственные изменения - это способ объединения данных в батч и функция вычисления метрики. + +{:else} + +## Дообучение модели с помощью Keras[[fine-tuning-the-model-with-keras]] + +Фактический код, использующий Keras, будет очень похож на предыдущий; единственные изменения - это способ объединения данных в батч и функция вычисления метрики. + +{/if} + + +### Сопоставление данных[[data-collation]] + +Мы не можем просто использовать `DataCollatorWithPadding`, как в [Главе 3](/course/chapter3), потому что в этом случае дополняются только входные данные (идентификаторы входов, маска внимания и идентификаторы типов токенов). Здесь наши метки должны быть дополнены точно так же, как и входы, чтобы они оставались одного размера, используя `-100` в качестве значения, чтобы соответствующие прогнозы игнорировались при вычислении потерь. + +Все это делает [`DataCollatorForTokenClassification`](https://huggingface.co/transformers/main_classes/data_collator.html#datacollatorfortokenclassification). Как и `DataCollatorWithPadding`, он принимает `токенизатор`, используемый для предварительной обработки входных данных: + +{#if fw === 'pt'} + +```py +from transformers import DataCollatorForTokenClassification + +data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer) +``` + +{:else} + +```py +from transformers import DataCollatorForTokenClassification + +data_collator = DataCollatorForTokenClassification( + tokenizer=tokenizer, return_tensors="tf" +) +``` + +{/if} + +Чтобы проверить его на нескольких примерах, мы можем просто вызвать его на списке примеров из нашего токенизированного обучающего набора: + +```py +batch = data_collator([tokenized_datasets["train"][i] for i in range(2)]) +batch["labels"] +``` + +```python out +tensor([[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100], + [-100, 1, 2, -100, -100, -100, -100, -100, -100, -100, -100, -100]]) +``` + +Давайте сравним это с метками для первого и второго элементов в нашем датасете: + +```py +for i in range(2): + print(tokenized_datasets["train"][i]["labels"]) +``` + +```python out +[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100] +[-100, 1, 2, -100] +``` + +{#if fw === 'pt'} + +Как мы видим, второй набор меток был дополнен до длины первого с помощью значения `-100`. + +{:else} + +Наш коллатор данных готов к работе! Теперь давайте используем его для создания датасета `tf.data.Dataset` с помощью метода `to_tf_dataset()`. Вы также можете использовать `model.prepare_tf_dataset()`, чтобы сделать это с меньшим количеством кода - вы увидите это в некоторых других разделах этой главы. + +```py +tf_train_dataset = tokenized_datasets["train"].to_tf_dataset( + columns=["attention_mask", "input_ids", "labels", "token_type_ids"], + collate_fn=data_collator, + shuffle=True, + batch_size=16, +) + +tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset( + columns=["attention_mask", "input_ids", "labels", "token_type_ids"], + collate_fn=data_collator, + shuffle=False, + batch_size=16, +) +``` + + + Next stop: the model itself. + +{/if} + +{#if fw === 'tf'} + +### Определение модели[[defining-the-model]] + +Поскольку мы работаем над проблемой классификации токенов, мы будем использовать класс `TFAutoModelForTokenClassification`. Главное, что нужно помнить при определении этой модели, - это передать информацию о количестве имеющихся у нас меток. Проще всего передать это число с помощью аргумента `num_labels`, но если мы хотим получить красивый виджет инференса, подобный тому, что мы видели в начале этого раздела, то лучше задать правильные соответствия меток. + +Они должны быть заданы двумя словарями, `id2label` и `label2id`, которые содержат отображение идентификатора в метку и наоборот: + +```py +id2label = {i: label for i, label in enumerate(label_names)} +label2id = {v: k for k, v in id2label.items()} +``` + +Теперь мы можем просто передать их в метод `TFAutoModelForTokenClassification.from_pretrained()`, и они будут заданы в конфигурации модели, затем правильно сохранены и загружены в Hub: + +```py +from transformers import TFAutoModelForTokenClassification + +model = TFAutoModelForTokenClassification.from_pretrained( + model_checkpoint, + id2label=id2label, + label2id=label2id, +) +``` + +Как и при определении `TFAutoModelForSequenceClassification` в [Главе 3](/course/chapter3), при создании модели выдается предупреждение о том, что некоторые веса не были использованы (веса из предварительно обученной головы), а другие веса инициализированы случайно (веса из новой головы классификации токенов), и что эту модель нужно обучить. Мы сделаем это через минуту, но сначала давайте перепроверим, что наша модель имеет правильное количество меток: + +```python +model.config.num_labels +``` + +```python out +9 +``` + + + +⚠️ Если у вас есть модель с неправильным количеством меток, то при последующем вызове `model.fit()` вы получите непонятную ошибку. Это может вызвать раздражение при отладке, поэтому обязательно выполните эту проверку, чтобы убедиться, что у вас есть ожидаемое количество меток. + + + +### Дообучение модели[[fine-tuning-the-model]] + +Теперь мы готовы к обучению нашей модели! Однако сначала нам нужно сделать еще немного работы: войти в Hugging Face и определить гиперпараметры обучения. Если вы работаете в блокноте, есть удобная функция, которая поможет вам в этом: + +```python +from huggingface_hub import notebook_login + +notebook_login() +``` + +Появится виджет, в котором вы можете ввести свои учетные данные для входа в Hugging Face. + +Если вы работаете не в блокноте, просто введите следующую строку в терминале: + +```bash +huggingface-cli login +``` + +После входа в аккаунт мы можем подготовить все необходимое для компиляции нашей модели. 🤗 Transformers предоставляет удобную функцию `create_optimizer()`, которая создаст вам оптимизатор `AdamW` с соответствующими настройками затухания весов и затухания скорости обучения, что позволит улучшить качество вашей модели по сравнению со встроенным оптимизатором `Adam`: + +```python +from transformers import create_optimizer +import tensorflow as tf + +# Обучение со смешанной точностью float16 +# Закомментируйте эту строку, если вы используете GPU, которому это не принесет никаких преимуществ +tf.keras.mixed_precision.set_global_policy("mixed_float16") + +# Количество шагов обучения - это количество примеров в датасете, разделенное на размер батча, затем умноженное +# на общее количество эпох. Обратите внимание, что tf_train_dataset здесь - это разбитое на батчи tf.data.Dataset, +# а не оригинальный датасет Hugging Face, поэтому его len() уже равен num_samples // batch_size. +num_epochs = 3 +num_train_steps = len(tf_train_dataset) * num_epochs + +optimizer, schedule = create_optimizer( + init_lr=2e-5, + num_warmup_steps=0, + num_train_steps=num_train_steps, + weight_decay_rate=0.01, +) +model.compile(optimizer=optimizer) +``` + +Обратите внимание, что мы не указываем аргумент `loss` в `compile()`. Это связано с тем, что модели могут вычислять потери внутри себя - если вы компилируете без потерь и предоставляете свои метки во входном словаре (как мы делаем в наших датасетах), то модель будет обучаться, используя эти внутренние потери, которые будут соответствовать задаче и типу выбранной вами модели. + +Далее мы определяем `PushToHubCallback` для загрузки нашей модели в Hub во время обучения модели с помощью этого обратного вызова: + +```python +from transformers.keras_callbacks import PushToHubCallback + +callback = PushToHubCallback(output_dir="bert-finetuned-ner", tokenizer=tokenizer) + +model.fit( + tf_train_dataset, + validation_data=tf_eval_dataset, + callbacks=[callback], + epochs=num_epochs, +) +``` + +С помощью аргумента `hub_model_id` можно указать полное имя репозитория, в который вы хотите передать модель (в частности, этот аргумент нужно использовать, чтобы передать модель в организацию). Например, когда мы отправили модель в [организацию `huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/bert-finetuned-ner"`. По умолчанию используемое хранилище будет находиться в вашем пространстве имен и называться в соответствии с заданной вами выходной директорией, например `"cool_huggingface_user/bert-finetuned-ner"`. + + + +💡 Если выходной каталог, который вы используете, уже существует, он должен быть локальным клоном репозитория, в который вы хотите выполнить push. Если это не так, вы получите ошибку при вызове `model.fit()` и должны будете задать новое имя. + + + +Обратите внимание, что во время обучения каждый раз, когда модель сохраняется (здесь - каждую эпоху), она загружается на хаб в фоновом режиме. Таким образом, при необходимости вы сможете возобновить обучение на другой машине. + +На этом этапе вы можете использовать виджет инференса на Model Hub, чтобы протестировать свою модель и поделиться ею с друзьями. Вы успешно дообучили модель для задачи классификации токенов - поздравляем! Но насколько хороша наша модель на самом деле? Чтобы выяснить это, нам следует оценить некоторые метрики. + +{/if} + + +### Метрики[[metrics]] + +{#if fw === 'pt'} + +Чтобы `Trainer` вычислял метрику каждую эпоху, нам нужно определить функцию `compute_metrics()`, которая принимает массивы прогнозов и меток и возвращает словарь с именами и значениями метрик. + +Традиционно для оценки прогнозирования классификации токенов используется библиотека [*seqeval*](https://github.com/chakki-works/seqeval). Чтобы использовать эту метрику, сначала нужно установить библиотеку *seqeval*: + +```py +!pip install seqeval +``` + +Мы можем загрузить ее с помощью функции `evaluate.load()`, как мы это делали в [Главе 3](/course/chapter3): + +{:else} + +Традиционно для оценки прогнозирования классификации токенов используется библиотека [*seqeval*](https://github.com/chakki-works/seqeval). Чтобы использовать эту метрику, сначала нужно установить библиотеку *seqeval*: + +```py +!pip install seqeval +``` + +Мы можем загрузить ее с помощью функции `evaluate.load()`, как мы это делали в [Главе 3](/course/chapter3): + +{/if} + +```py +import evaluate + +metric = evaluate.load("seqeval") +``` + +Эта метрика ведет себя не так, как стандартная accuracy: на самом деле она принимает списки меток как строки, а не как целые числа, поэтому нам нужно полностью декодировать прогноз и метки перед передачей их в метрику. Давайте посмотрим, как это работает. Сначала мы получим метки для нашего первого обучающего примера: + +```py +labels = raw_datasets["train"][0]["ner_tags"] +labels = [label_names[i] for i in labels] +labels +``` + +```python out +['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O'] +``` + +Затем мы можем создать фальшивые прогнозы для них, просто изменив значение в индексе 2: + +```py +predictions = labels.copy() +predictions[2] = "O" +metric.compute(predictions=[predictions], references=[labels]) +``` + +Обратите внимание, что метрика принимает список прогнозов (не только один) и список меток. Вот результат: + +```python out +{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2}, + 'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1}, + 'overall_precision': 1.0, + 'overall_recall': 0.67, + 'overall_f1': 0.8, + 'overall_accuracy': 0.89} +``` + +{#if fw === 'pt'} + +Она возвращает огромное количество информации! Мы получаем оценки precision, recall и F1 для каждой отдельной сущности, а также в целом. Для расчета метрик мы сохраним только общую оценку, но вы можете настроить функцию `compute_metrics()` так, чтобы она возвращала все метрики, которые вы хотите получить. + +Эта функция `compute_metrics()` сначала берет argmax логитов, чтобы преобразовать их в прогнозы (как обычно, логиты и вероятности расположены в том же порядке, поэтому нам не нужно применять softmax). Затем нам нужно преобразовать метки и прогнозы из целых чисел в строки. Мы удаляем все значения, для которых метка равна `-100`, а затем передаем результаты в метод `metric.compute()`: + +```py +import numpy as np + + +def compute_metrics(eval_preds): + logits, labels = eval_preds + predictions = np.argmax(logits, axis=-1) + + # Удаляем игнорируемый индекс (специальные токены) и преобразуем в метки + true_labels = [[label_names[l] for l in label if l != -100] for label in labels] + true_predictions = [ + [label_names[p] for (p, l) in zip(prediction, label) if l != -100] + for prediction, label in zip(predictions, labels) + ] + all_metrics = metric.compute(predictions=true_predictions, references=true_labels) + return { + "precision": all_metrics["overall_precision"], + "recall": all_metrics["overall_recall"], + "f1": all_metrics["overall_f1"], + "accuracy": all_metrics["overall_accuracy"], + } +``` + +Теперь, когда это сделано, мы почти готовы к определению нашего `Trainer`. Нам просто нужна `model`, чтобы дообучить ее! + +{:else} + +Она возвращает огромное количество информации! Мы получаем оценки precision, recall и F1 для каждой отдельной сущности, а также в целом. Теперь давайте посмотрим, что произойдет, если мы попробуем использовать реальные прогнозы модели для вычисления реальных оценок. + +TensorFlow не любит конкатенировать наши прогнозы, поскольку они имеют переменную длину последовательности. Это означает, что мы не можем просто использовать `model.predict()` - но это нас не остановит. Мы будем получать прогнозы по батчу за раз и конкатенировать их в один большой длинный список по мере продвижения, отбрасывая токены `-100`, которые указывают на маскирование/дополнение, а затем вычислять метрики для списка в конце: + +```py +import numpy as np + +all_predictions = [] +all_labels = [] +for batch in tf_eval_dataset: + logits = model.predict_on_batch(batch)["logits"] + labels = batch["labels"] + predictions = np.argmax(logits, axis=-1) + for prediction, label in zip(predictions, labels): + for predicted_idx, label_idx in zip(prediction, label): + if label_idx == -100: + continue + all_predictions.append(label_names[predicted_idx]) + all_labels.append(label_names[label_idx]) +metric.compute(predictions=[all_predictions], references=[all_labels]) +``` + + +```python out +{'LOC': {'precision': 0.91, 'recall': 0.92, 'f1': 0.91, 'number': 1668}, + 'MISC': {'precision': 0.70, 'recall': 0.79, 'f1': 0.74, 'number': 702}, + 'ORG': {'precision': 0.85, 'recall': 0.90, 'f1': 0.88, 'number': 1661}, + 'PER': {'precision': 0.95, 'recall': 0.95, 'f1': 0.95, 'number': 1617}, + 'overall_precision': 0.87, + 'overall_recall': 0.91, + 'overall_f1': 0.89, + 'overall_accuracy': 0.97} +``` + +Как ваша модель показала себя по сравнению с нашей? Если вы получили похожие цифры, значит, ваше обучение прошло успешно! + +{/if} + +{#if fw === 'pt'} + +### Определение модели[[defining-the-model]] + +Поскольку мы работаем над проблемой классификации токенов, мы будем использовать класс `AutoModelForTokenClassification`. Главное, что нужно помнить при определении этой модели, - это передать информацию о количестве имеющихся у нас меток. Проще всего передать это число с помощью аргумента `num_labels`, но если мы хотим получить красивый виджет инференса, подобный тому, что мы видели в начале этого раздела, то лучше задать правильное сопоставление меток. + +Оно должно задаваться двумя словарями, `id2label` и `label2id`, которые содержат соответствие между идентификатором и меткой и наоборот: + +```py +id2label = {i: label for i, label in enumerate(label_names)} +label2id = {v: k for k, v in id2label.items()} +``` + +Теперь мы можем просто передать их в метод `AutoModelForTokenClassification.from_pretrained()`, и они будут заданы в конфигурации модели, а затем правильно сохранены и загружены в Hub: + +```py +from transformers import AutoModelForTokenClassification + +model = AutoModelForTokenClassification.from_pretrained( + model_checkpoint, + id2label=id2label, + label2id=label2id, +) +``` + +Как и в случае определения `AutoModelForSequenceClassification` в [Главе 3](/course/chapter3), при создании модели выдается предупреждение о том, что некоторые веса не были использованы (те, что были получены из предварительно обученной головы), а другие инициализированы случайно (те, что были получены из новой головы классификации токенов), и что эту модель необходимо обучить. Мы сделаем это через минуту, но сначала давайте перепроверим, что наша модель имеет правильное количество меток: + +```python +model.config.num_labels +``` + +```python out +9 +``` + + + +⚠️ Если у вас есть модель с неправильным количеством меток, то при последующем вызове метода `Trainer.train()` вы получите непонятную ошибку (что-то вроде "CUDA error: device-side assert triggered"). Это главная причина ошибок, о которых сообщают пользователи, поэтому обязательно выполните эту проверку, чтобы убедиться, что у вас есть ожидаемое количество меток. + + + +### Дообучение модели[[fine-tuning-the-model]] + +Теперь мы готовы к обучению нашей модели! Нам осталось сделать две последние вещи, прежде чем мы определим наш `Trainer`: войти в Hugging Face и определить наши аргументы для обучения. Если вы работаете в блокноте, есть удобная функция, которая поможет вам в этом: + +```python +from huggingface_hub import notebook_login + +notebook_login() +``` + +Появится виджет, в котором вы можете ввести свои учетные данные для входа в Hugging Face. + +Если вы работаете не в ноутбуке, просто введите следующую строку в терминале: + +```bash +huggingface-cli login +``` + +Как только это будет сделано, мы сможем определить наши `TrainingArguments`: + +```python +from transformers import TrainingArguments + +args = TrainingArguments( + "bert-finetuned-ner", + evaluation_strategy="epoch", + save_strategy="epoch", + learning_rate=2e-5, + num_train_epochs=3, + weight_decay=0.01, + push_to_hub=True, +) +``` + +Большинство из них вы уже видели: мы задаем некоторые гиперпараметры (например, скорость обучения, количество эпох для обучения и затухание весов) и указываем `push_to_hub=True`, чтобы указать, что мы хотим сохранить модель и оценить ее в конце каждой эпохи, а также что мы хотим загрузить наши результаты в Model Hub. Обратите внимание, что с помощью аргумента `hub_model_id` можно указать имя репозитория, в который вы хотите передать модель (в частности, этот аргумент нужно использовать, чтобы передать модель в организацию). Например, когда мы передавали модель в [организацию`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/bert-finetuned-ner"` в `TrainingArguments`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, так что в нашем случае это будет `"sgugger/bert-finetuned-ner"`. + + + +💡 Если выходной каталог, который вы используете, уже существует, он должен быть локальным клоном репозитория, в который вы хотите передать модель. Если это не так, вы получите ошибку при определении вашего `Trainer` и должны будете задать новое имя. + + + +Наконец, мы просто передаем все в `Trainer` и запускаем обучение: + +```python +from transformers import Trainer + +trainer = Trainer( + model=model, + args=args, + train_dataset=tokenized_datasets["train"], + eval_dataset=tokenized_datasets["validation"], + data_collator=data_collator, + compute_metrics=compute_metrics, + tokenizer=tokenizer, +) +trainer.train() +``` + +Обратите внимание, что во время обучения каждый раз, когда модель сохраняется (здесь - каждую эпоху), она загружается в Hub в фоновом режиме. Таким образом, при необходимости вы сможете возобновить обучение на другой машине. + +После завершения обучения мы используем метод `push_to_hub()`, чтобы убедиться, что загружена самая последняя версия модели: + +```py +trainer.push_to_hub(commit_message="Training complete") +``` + +Эта команда возвращает URL только что выполненного commit, если вы хотите его проверить: + +```python out +'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed' +``` + + `Trainer` также создает черновик карточки модели со всеми результатами оценки и загружает его. На этом этапе вы можете использовать виджет инференса на Model Hub, чтобы протестировать свою модель и поделиться ею с друзьями. Вы успешно дообучили модель для задачи классификации токенов - поздравляем! + + Если вы хотите более глубоко погрузиться в цикл обучения, мы покажем вам, как сделать то же самое с помощью 🤗 Accelerate. + +## Индивидуальный цикл обучения[[a-custom-training-loop]] + +Теперь давайте рассмотрим полный цикл обучения, чтобы вы могли легко настроить нужные вам части. Он будет очень похож на тот, что мы делали в [Главе 3](/course/chapter3/4), с некоторыми изменениями для оценки. + +### Подготовка всего к обучению[[preparing-everything-for-training]] + +Сначала нам нужно создать `DataLoader` для наших датасетов. Мы используем наш `data_collator` в качестве `collate_fn` и перемешиваем обучающий набор, но не валидационный: + +```py +from torch.utils.data import DataLoader + +train_dataloader = DataLoader( + tokenized_datasets["train"], + shuffle=True, + collate_fn=data_collator, + batch_size=8, +) +eval_dataloader = DataLoader( + tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8 +) +``` + +Затем мы повторно инстанцируем нашу модель, чтобы убедиться, что мы не продолжаем дообучать модель, а снова начинаем с предварительно обученной модели BERT: + +```py +model = AutoModelForTokenClassification.from_pretrained( + model_checkpoint, + id2label=id2label, + label2id=label2id, +) +``` + +Тогда нам понадобится оптимизатор. Мы будем использовать классический `AdamW`, который похож на `Adam`, но с исправлениями в способе применения затухания весов: + +```py +from torch.optim import AdamW + +optimizer = AdamW(model.parameters(), lr=2e-5) +``` + +Когда у нас есть все эти объекты, мы можем отправить их в метод `accelerator.prepare()`: + +```py +from accelerate import Accelerator + +accelerator = Accelerator() +model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader +) +``` + + + +🚨 Если вы обучаетесь на TPU, вам нужно будет перенести весь код, начиная с ячейки выше, в специальную функцию обучения. Подробнее смотрите [Главу 3](/course/chapter3). + + + +Теперь, когда мы отправили наш `train_dataloader` в `accelerator.prepare()`, мы можем использовать его длину для вычисления количества шагов обучения. Помните, что это всегда нужно делать после подготовки загрузчика данных, так как этот метод изменит его длину. Мы используем классический линейный планировшик скорости обучения до 0: + +```py +from transformers import get_scheduler + +num_train_epochs = 3 +num_update_steps_per_epoch = len(train_dataloader) +num_training_steps = num_train_epochs * num_update_steps_per_epoch + +lr_scheduler = get_scheduler( + "linear", + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=num_training_steps, +) +``` + +Наконец, чтобы передать нашу модель в Hub, нам нужно создать объект `Repository` в рабочей папке. Сначала авторизуйтесь в Hugging Face, если вы еще не авторизованы. Мы определим имя репозитория по идентификатору модели, который мы хотим присвоить нашей модели (не стесняйтесь заменить `repo_name` на свой собственный выбор; он просто должен содержать ваше имя пользователя, что и делает функция `get_full_repo_name()`): + +```py +from huggingface_hub import Repository, get_full_repo_name + +model_name = "bert-finetuned-ner-accelerate" +repo_name = get_full_repo_name(model_name) +repo_name +``` + +```python out +'sgugger/bert-finetuned-ner-accelerate' +``` + +Затем мы можем клонировать этот репозиторий в локальную папку. Если она уже существует, эта локальная папка должна быть существующим клоном репозитория, с которым мы работаем: + +```py +output_dir = "bert-finetuned-ner-accelerate" +repo = Repository(output_dir, clone_from=repo_name) +``` + +Теперь мы можем загрузить все, что сохранили в `output_dir`, вызвав метод `repo.push_to_hub()`. Это поможет нам загружать промежуточные модели в конце каждой эпохи. + +### Цикл обучения[[training-loop]] + +Теперь мы готовы написать полный цикл обучения. Чтобы упростить его оценочную часть, мы определяем функцию `postprocess()`, которая принимает прогнозы и метки и преобразует их в списки строк, как того ожидает наш объект `metric`: + +```py +def postprocess(predictions, labels): + predictions = predictions.detach().cpu().clone().numpy() + labels = labels.detach().cpu().clone().numpy() + + # Удаляем игнорируемый индекс (специальные токены) и преобразуем в метки + true_labels = [[label_names[l] for l in label if l != -100] for label in labels] + true_predictions = [ + [label_names[p] for (p, l) in zip(prediction, label) if l != -100] + for prediction, label in zip(predictions, labels) + ] + return true_labels, true_predictions +``` + +Затем мы можем написать цикл обучения. После определения прогресс-бара, чтобы следить за ходом обучения, цикл состоит из трех частей: + +- Само обучение представляет собой классическую итерацию по `train_dataloader`, прямой проход по модели, затем обратный проход и шаг оптимизатора. +- Оценка, в которой есть новшество после получения выходов нашей модели на батче: поскольку два процесса могли дополнять входы и метки до разных форм, нам нужно использовать `accelerator.pad_across_processes()`, чтобы сделать прогнозы и метки одинаковой формы перед вызовом метода `gather()`. Если мы этого не сделаем, оценка либо завершится с ошибкой, либо зависнет навсегда. Затем мы отправляем результаты в `metric.add_batch()` и вызываем `metric.compute()` после завершения цикла оценки. +- Сохранение и загрузка, где мы сначала сохраняем модель и токенизатор, а затем вызываем `repo.push_to_hub()`. Обратите внимание, что мы используем аргумент `blocking=False`, чтобы указать библиотеке 🤗 Hub на выполнение push в асинхронном процессе. Таким образом, обучение продолжается нормально, а эта (длинная) инструкция выполняется в фоновом режиме. + +Вот полный код цикла обучения: + +```py +from tqdm.auto import tqdm +import torch + +progress_bar = tqdm(range(num_training_steps)) + +for epoch in range(num_train_epochs): + # Обучение + model.train() + for batch in train_dataloader: + outputs = model(**batch) + loss = outputs.loss + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + progress_bar.update(1) + + # Оценка + model.eval() + for batch in eval_dataloader: + with torch.no_grad(): + outputs = model(**batch) + + predictions = outputs.logits.argmax(dim=-1) + labels = batch["labels"] + + # Необходимо добавить предсказания и метки для gather + predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100) + labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100) + + predictions_gathered = accelerator.gather(predictions) + labels_gathered = accelerator.gather(labels) + + true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered) + metric.add_batch(predictions=true_predictions, references=true_labels) + + results = metric.compute() + print( + f"epoch {epoch}:", + { + key: results[f"overall_{key}"] + for key in ["precision", "recall", "f1", "accuracy"] + }, + ) + + # Сохранение и загрузка + accelerator.wait_for_everyone() + unwrapped_model = accelerator.unwrap_model(model) + unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save) + if accelerator.is_main_process: + tokenizer.save_pretrained(output_dir) + repo.push_to_hub( + commit_message=f"Training in progress epoch {epoch}", blocking=False + ) +``` + +Если вы впервые видите модель, сохраненную с помощью 🤗 Accelerate, давайте посмотрим на три строки кода, которые идут вместе с ним: + +```py +accelerator.wait_for_everyone() +unwrapped_model = accelerator.unwrap_model(model) +unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save) +``` + +Первая строка не требует пояснений: она указывает всем процессам подождать, пока все не окажутся на этой стадии, прежде чем продолжить работу. Это нужно для того, чтобы убедиться, что у нас одна и та же модель в каждом процессе перед сохранением. Затем мы берем `unwrapped_model`, которая является базовой моделью, которую мы определили. Метод `accelerator.prepare()` изменяет модель для работы в распределенном обучении, поэтому у нее больше не будет метода `save_pretrained()`; метод `accelerator.unwrap_model()` отменяет этот шаг. Наконец, мы вызываем `save_pretrained()`, но указываем этому методу использовать `accelerator.save()` вместо `torch.save()`. + +После того как это будет сделано, у вас должна получиться модель, выдающая результаты, очень похожие на те, что были обучены с помощью `Trainer`. Вы можете посмотреть модель, которую мы обучили с помощью этого кода, на [*huggingface-course/bert-finetuned-ner-accelerate*](https://huggingface.co/huggingface-course/bert-finetuned-ner-accelerate). А если вы хотите протестировать какие-либо изменения в цикле обучения, вы можете напрямую реализовать их, отредактировав код, показанный выше! + +{/if} + +## Использование дообученной модели[[using-the-fine-tuned-model]] + +Мы уже показали вам, как можно использовать модель, которую мы дообучили на Model Hub, с помощью виджета инференса. Чтобы использовать ее локально в `pipeline`, нужно просто указать соответствующий идентификатор модели: + +```py +from transformers import pipeline + +# Замените это на свою собственную контрольную точку +model_checkpoint = "huggingface-course/bert-finetuned-ner" +token_classifier = pipeline( + "token-classification", model=model_checkpoint, aggregation_strategy="simple" +) +token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.") +``` + +```python out +[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18}, + {'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45}, + {'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}] +``` + +Отлично! Наша модель работает так же хорошо, как и модель по умолчанию для этого конвейера! diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx new file mode 100644 index 000000000..934a38243 --- /dev/null +++ b/chapters/ru/chapter7/3.mdx @@ -0,0 +1,1044 @@ + + +# Дообучение модели маскированного языкового моделирования[[fine-tuning-a-masked-language-model]] + +{#if fw === 'pt'} + + + +{:else} + + + +{/if} + +Для многих приложений NLP, в которых используются модели-трансформеры, можно просто взять предварительно обученную модель из Hugging Face Hub и дообучить ее непосредственно на ваших данных для решения поставленной задачи. При условии, что корпус, использованный для предварительного обучения, не слишком отличается от корпуса, используемого для дообучения, трансферное обучение обычно дает хорошие результаты. + +Однако есть несколько случаев, когда перед обучением специфичной для конкретной задачи головы необходимо дообучить языковые модели на ваших данных. Например, если ваш датасет содержит юридические контракты или научные статьи, ванильная модель трансформер, такая как BERT, обычно рассматривает специфические для области слова в вашем корпусе как редкие токены, и результирующее качество может быть менее чем удовлетворительным. Дообучив языковую модель на данных из домена, вы сможете повысить качество работы во многих последующих задачах, а это значит, что обычно этот шаг нужно выполнить только один раз! + +Этот процесс дообучить предварительно обученную языковую модель на данных из домена обычно называют _доменной адаптацией (domain adaptation)_. Он был популяризирован в 2018 году благодаря [ULMFiT](https://arxiv.org/abs/1801.06146), которая стала одной из первых нейронных архитектур (основанных на LSTM), заставивших трансферное обучение действительно работать в NLP. Пример доменной адаптации с помощью ULMFiT показан на изображении ниже; в этом разделе мы сделаем нечто подобное, но с трансформером вместо LSTM! + +
+ULMFiT. + +
+ +К концу этого раздела у вас будет [модель маскированного языкового моделирования](https://huggingface.co/huggingface-course/distilbert-base-uncased-finetuned-imdb?text=This+is+a+great+%5BMASK%5D.) на Hub, которая позволяет автоматически дописывать предложения, как показано ниже: + + + +Давайте погрузимся в работу! + + + + + +🙋 Если термины "маскированное моделирование языка (masked language modeling)" и "предварительно обученная модель (pretrained model)" кажутся вам незнакомыми, загляните в [Главу 1](/course/chapter1), где мы объясняем все эти основные понятия, сопровождая их видеороликами! + + + +## Выбор предварительно обученной модели для маскированного моделирования языка[[picking-a-pretrained-model-for-masked-language-modeling]] + +Для начала давайте выберем подходящую предварительно обученную модель для маскированного языкового моделирования. Как показано на следующем скриншоте, вы можете найти список кандидатов, применив фильтр "Fill-Mask" на [Hugging Face Hub](https://huggingface.co/models?pipeline_tag=fill-mask&sort=downloads): + +
+Hub models. +
+ +Хотя модели семейства BERT и RoBERTa являются наиболее загружаемыми, мы будем использовать модель под названием [DistilBERT](https://huggingface.co/distilbert-base-uncased) +которая может быть обучена гораздо быстрее и практически без потерь в качестве. Эта модель была обучена с помощью специальной техники, называемой [_дистилляцией знаний (knowledge distillation)_](https://en.wikipedia.org/wiki/Knowledge_distillation), когда большая "модель-учитель", такая как BERT, используется для обучения "модели-ученика", имеющей гораздо меньше параметров. Объяснение деталей дистилляции знаний завело бы нас слишком далеко в этом разделе, но если вам интересно, вы можете прочитать об этом в [_Natural Language Processing with Transformers_](https://www.oreilly.com/library/view/natural-language-processing/9781098136789/) (в просторечии известном как учебник по Трансформерам). + +{#if fw === 'pt'} + +Let's go ahead and download DistilBERT using the `AutoModelForMaskedLM` class: + +```python +from transformers import AutoModelForMaskedLM + +model_checkpoint = "distilbert-base-uncased" +model = AutoModelForMaskedLM.from_pretrained(model_checkpoint) +``` + +Мы можем узнать, сколько параметров имеет эта модель, вызвав метод `num_parameters()`: + +```python +distilbert_num_parameters = model.num_parameters() / 1_000_000 +print(f"'>>> DistilBERT number of parameters: {round(distilbert_num_parameters)}M'") +print(f"'>>> BERT number of parameters: 110M'") +``` + +```python out +'>>> DistilBERT number of parameters: 67M' +'>>> BERT number of parameters: 110M' +``` + +{:else} + +Давайте загрузим DistilBERT, используя класс `AutoModelForMaskedLM`: + +```python +from transformers import TFAutoModelForMaskedLM + +model_checkpoint = "distilbert-base-uncased" +model = TFAutoModelForMaskedLM.from_pretrained(model_checkpoint) +``` + +Мы можем узнать, сколько параметров имеет эта модель, вызвав метод `summary()`: + +```python +model.summary() +``` + +```python out +Model: "tf_distil_bert_for_masked_lm" +_________________________________________________________________ +Layer (type) Output Shape Param # +================================================================= +distilbert (TFDistilBertMain multiple 66362880 +_________________________________________________________________ +vocab_transform (Dense) multiple 590592 +_________________________________________________________________ +vocab_layer_norm (LayerNorma multiple 1536 +_________________________________________________________________ +vocab_projector (TFDistilBer multiple 23866170 +================================================================= +Total params: 66,985,530 +Trainable params: 66,985,530 +Non-trainable params: 0 +_________________________________________________________________ +``` + +{/if} + +Имея около 67 миллионов параметров, DistilBERT примерно в два раза меньше, чем базовая модель BERT, что примерно соответствует двукратному ускорению обучения - неплохо! Теперь посмотрим, какие виды токенов эта модель предсказывает как наиболее вероятные завершения небольшого образца текста: + +```python +text = "This is a great [MASK]." +``` + +Как люди, мы можем представить множество вариантов использования токена `[MASK]`, например "day (день)", "ride (поездка)" или "painting (картина)". Для предварительно обученных моделей прогнозы зависят от корпуса, на котором обучалась модель, поскольку она учится улавливать статистические закономерности, присутствующие в данных. Как и BERT, DistilBERT был предварительно обучен на датасетах [English Wikipedia](https://huggingface.co/datasets/wikipedia) и [BookCorpus](https://huggingface.co/datasets/bookcorpus), поэтому мы ожидаем, что прогнозы для `[MASK]` будут отражать эти домены. Для прогнозирования маски нам нужен токенизатор DistilBERT для создания входных данных для модели, поэтому давайте загрузим и его с хаба: + +```python +from transformers import AutoTokenizer + +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +``` + +Теперь, имея токенизатор и модель, мы можем передать наш текстовый пример в модель, извлечь логиты и вывести 5 лучших кандидатов: + +{#if fw === 'pt'} + +```python +import torch + +inputs = tokenizer(text, return_tensors="pt") +token_logits = model(**inputs).logits +# Определеним местоположения [MASK] и извлечем его логиты +mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1] +mask_token_logits = token_logits[0, mask_token_index, :] +# Выберем кандидатов [MASK] с наибольшими логитами +top_5_tokens = torch.topk(mask_token_logits, 5, dim=1).indices[0].tolist() + +for token in top_5_tokens: + print(f"'>>> {text.replace(tokenizer.mask_token, tokenizer.decode([token]))}'") +``` + +{:else} + +```python +import numpy as np +import tensorflow as tf + +inputs = tokenizer(text, return_tensors="np") +token_logits = model(**inputs).logits +# Определеним местоположения [MASK] и извлечем его логиты +mask_token_index = np.argwhere(inputs["input_ids"] == tokenizer.mask_token_id)[0, 1] +mask_token_logits = token_logits[0, mask_token_index, :] +# Выберем кандидатов [MASK] с наибольшими логитами +# Мы выполняем отрицание массива перед argsort, чтобы получить самые большие, а не самые маленькие логиты +top_5_tokens = np.argsort(-mask_token_logits)[:5].tolist() + +for token in top_5_tokens: + print(f">>> {text.replace(tokenizer.mask_token, tokenizer.decode([token]))}") +``` + +{/if} + +```python out +'>>> This is a great deal.' +'>>> This is a great success.' +'>>> This is a great adventure.' +'>>> This is a great idea.' +'>>> This is a great feat.' +``` + +Из результатов видно, что предсказания модели относятся к повседневным терминам, что, пожалуй, неудивительно, учитывая основу английской Википедии. Давайте посмотрим, как мы можем изменить эту область на что-то более нишевое - сильно поляризованные обзоры фильмов! + + +## Датасет[[the-dataset]] + +Для демонстрации адаптации к домену мы используем знаменитый [Large Movie Review Dataset](https://huggingface.co/datasets/imdb) (или сокращенно IMDb) - корпус кинорецензий, который часто используется для тестирования моделей анализа настроения. Дообучив DistilBERT на этом корпусе, мы ожидаем, что языковая модель адаптирует свой словарный запас от фактических данных Википедии, на которых она была предварительно обучена, к более субъективным элементам кинорецензий. Мы можем получить данные из Hugging Face Hub с помощью функции `load_dataset()` из 🤗 Datasets: + +```python +from datasets import load_dataset + +imdb_dataset = load_dataset("imdb") +imdb_dataset +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['text', 'label'], + num_rows: 25000 + }) + test: Dataset({ + features: ['text', 'label'], + num_rows: 25000 + }) + unsupervised: Dataset({ + features: ['text', 'label'], + num_rows: 50000 + }) +}) +``` + +Мы видим, что части `train` и `test` состоят из 25 000 отзывов каждая, а часть без меток, называемая `unsupervised`, содержит 50 000 отзывов. Давайте посмотрим на несколько примеров, чтобы получить представление о том, с каким текстом мы имеем дело. Как мы уже делали в предыдущих главах курса, мы используем функции `Dataset.shuffle()` и `Dataset.select()` для создания случайной выборки: + +```python +sample = imdb_dataset["train"].shuffle(seed=42).select(range(3)) + +for row in sample: + print(f"\n'>>> Review: {row['text']}'") + print(f"'>>> Label: {row['label']}'") +``` + +```python out + +'>>> Review: This is your typical Priyadarshan movie--a bunch of loony characters out on some silly mission. His signature climax has the entire cast of the film coming together and fighting each other in some crazy moshpit over hidden money. Whether it is a winning lottery ticket in Malamaal Weekly, black money in Hera Pheri, "kodokoo" in Phir Hera Pheri, etc., etc., the director is becoming ridiculously predictable. Don\'t get me wrong; as clichéd and preposterous his movies may be, I usually end up enjoying the comedy. However, in most his previous movies there has actually been some good humor, (Hungama and Hera Pheri being noteworthy ones). Now, the hilarity of his films is fading as he is using the same formula over and over again.

Songs are good. Tanushree Datta looks awesome. Rajpal Yadav is irritating, and Tusshar is not a whole lot better. Kunal Khemu is OK, and Sharman Joshi is the best.' +'>>> Label: 0' + +'>>> Review: Okay, the story makes no sense, the characters lack any dimensionally, the best dialogue is ad-libs about the low quality of movie, the cinematography is dismal, and only editing saves a bit of the muddle, but Sam" Peckinpah directed the film. Somehow, his direction is not enough. For those who appreciate Peckinpah and his great work, this movie is a disappointment. Even a great cast cannot redeem the time the viewer wastes with this minimal effort.

The proper response to the movie is the contempt that the director San Peckinpah, James Caan, Robert Duvall, Burt Young, Bo Hopkins, Arthur Hill, and even Gig Young bring to their work. Watch the great Peckinpah films. Skip this mess.' +'>>> Label: 0' + +'>>> Review: I saw this movie at the theaters when I was about 6 or 7 years old. I loved it then, and have recently come to own a VHS version.

My 4 and 6 year old children love this movie and have been asking again and again to watch it.

I have enjoyed watching it again too. Though I have to admit it is not as good on a little TV.

I do not have older children so I do not know what they would think of it.

The songs are very cute. My daughter keeps singing them over and over.

Hope this helps.' +'>>> Label: 1' +``` + +Да, это точно рецензии на фильмы, и если вы достаточно взрослый, то можете даже понять комментарий в последней рецензии о том, что у вас есть VHS-версия 😜! Хотя нам не понадобятся эти метки для языкового моделирования, мы уже видим, что `0` обозначает отрицательный отзыв, а `1` - положительный. + + + +✏️ **Попробуйте!** Создайте случайную выборку из части `unsupervised` и проверьте, что метки не являются ни `0`, ни `1`. В процессе работы вы также можете проверить, что метки в частях `train` и `test` действительно равны `0` или `1` - это полезная проверка здравомыслия, которую каждый практикующий NLP должен выполнять в начале нового проекта! + + + +Теперь, когда мы вкратце ознакомились с данными, давайте перейдем к их подготовке к моделированию языка по маске. Как мы увидим, есть несколько дополнительных шагов, которые необходимо сделать по сравнению с задачами классификации последовательностей, которые мы рассматривали в [Главе 3](/course/chapter3). Поехали! + +## Предварительная обработка данных[[preprocessing-the-data]] + + + +Как для авторегрессивного, так и для масочного моделирования языка общим шагом предварительной обработки является объединение всех примеров, а затем разбиение всего корпуса на части одинакового размера. Это сильно отличается от нашего обычного подхода, когда мы просто проводим токенизацию отдельных примеров. Зачем конкатенируем все вместе? Причина в том, что отдельные примеры могут быть обрезаны, если они слишком длинные, и это приведет к потере информации, которая может быть полезна для задачи языкового моделирования! + +Итак, для начала мы проведем обычную токенизацию нашего корпуса, но _без_ задания параметра `truncation=True` в нашем токенизаторе. Мы также возьмем идентификаторы слов, если они доступны ((а они будут доступны, если мы используем быстрый токенизатор, как описано в [Главе 6](/course/chapter6/3)), поскольку они понадобятся нам позже для маскирования целых слов. Мы обернем это в простую функцию, а пока удалим столбцы `text` и `label`, поскольку они нам больше не нужны: + +```python +def tokenize_function(examples): + result = tokenizer(examples["text"]) + if tokenizer.is_fast: + result["word_ids"] = [result.word_ids(i) for i in range(len(result["input_ids"]))] + return result + + +# Используйте batched=True для активации быстрой многопоточности! +tokenized_datasets = imdb_dataset.map( + tokenize_function, batched=True, remove_columns=["text", "label"] +) +tokenized_datasets +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['attention_mask', 'input_ids', 'word_ids'], + num_rows: 25000 + }) + test: Dataset({ + features: ['attention_mask', 'input_ids', 'word_ids'], + num_rows: 25000 + }) + unsupervised: Dataset({ + features: ['attention_mask', 'input_ids', 'word_ids'], + num_rows: 50000 + }) +}) +``` + +Поскольку DistilBERT - это BERT-подобная модель, мы видим, что закодированные тексты состоят из `input_ids` и `attention_mask`, которые мы уже видели в других главах, а также из `word_ids`, которые мы добавили. + +Теперь, когда мы провели токенизацию рецензий на фильмы, следующий шаг - сгруппировать их вместе и разбить результат на части. Но какого размера должны быть эти части? В конечном итоге это будет зависеть от объема доступной памяти GPU, но хорошей отправной точкой будет узнать, каков максимальный размер контекста модели. Это можно сделать путем проверки атрибута `model_max_length` токенизатора: + +```python +tokenizer.model_max_length +``` + +```python out +512 +``` + +Это значение берется из файла *tokenizer_config.json*, связанного с контрольной точкой; в данном случае мы видим, что размер контекста составляет 512 токенов, как и в случае с BERT. + + + +✏️ **Попробуйте!** Некоторые модели трансформеров, например [BigBird](https://huggingface.co/google/bigbird-roberta-base) и [Longformer](hf.co/allenai/longformer-base-4096), имеют гораздо большую длину контекста, чем BERT и другие ранние модели трансформеров. Инстанцируйте токенизатор для одной из этих контрольных точек и проверьте, что `model_max_length` согласуется с тем, что указано в карточке модели. + + + +Поэтому для проведения экспериментов на GPU, подобных тем, что стоят в Google Colab, мы выберем что-нибудь поменьше, что может поместиться в памяти: + +```python +chunk_size = 128 +``` + + + +Обратите внимание, что использование небольшого размера фрагмента может быть вредным в реальных сценариях, поэтому следует использовать размер, соответствующий сценарию использования, к которому вы будете применять вашу модель. + + + +Теперь наступает самое интересное. Чтобы показать, как работает конкатенация, давайте возьмем несколько отзывов из нашего обучающего набора с токенизацией и выведем количество токенов в отзыве: + +```python +# С помощью среза получаем список списков для каждого признака +tokenized_samples = tokenized_datasets["train"][:3] + +for idx, sample in enumerate(tokenized_samples["input_ids"]): + print(f"'>>> Review {idx} length: {len(sample)}'") +``` + +```python out +'>>> Review 0 length: 200' +'>>> Review 1 length: 559' +'>>> Review 2 length: 192' +``` + +Затем мы можем объединить все эти примеры с помощью простого dictionary comprehension, как показано ниже: + +```python +concatenated_examples = { + k: sum(tokenized_samples[k], []) for k in tokenized_samples.keys() +} +total_length = len(concatenated_examples["input_ids"]) +print(f"'>>> Concatenated reviews length: {total_length}'") +``` + +```python out +'>>> Concatenated reviews length: 951' +``` + +Отлично, общая длина подтвердилась - теперь давайте разобьем конкатенированные обзоры на части размером `chunk_size`. Для этого мы перебираем признаки в `concatenated_examples` и используем list comprehension для создания срезов каждого признака. В результате мы получаем словарь фрагментов для каждого признака: + +```python +chunks = { + k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)] + for k, t in concatenated_examples.items() +} + +for chunk in chunks["input_ids"]: + print(f"'>>> Chunk length: {len(chunk)}'") +``` + +```python out +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 128' +'>>> Chunk length: 55' +``` + +Как видно из этого примера, последний фрагмент, как правило, будет меньше максимального размера фрагмента. Существует две основные стратегии решения этой проблемы: + +* Отбросить последний фрагмент, если он меньше `chunk_size`. +* Дополнить последний фрагмент до длины, равной `chunk_size`. + +Мы воспользуемся первым подходом, поэтому обернем всю описанную выше логику в одну функцию, которую можно применить к нашим токенизированным датасетам: + +```python +def group_texts(examples): + # Конкатенируем все тексты + concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()} + # Вычисляем длину конкатенированных текстов + total_length = len(concatenated_examples[list(examples.keys())[0]]) + # Отбрасываем последний фрагмент, если он меньше chunk_size + total_length = (total_length // chunk_size) * chunk_size + # Разбиваем на фрагменты длиной max_len + result = { + k: [t[i : i + chunk_size] for i in range(0, total_length, chunk_size)] + for k, t in concatenated_examples.items() + } + # Создаем новый столбец меток + result["labels"] = result["input_ids"].copy() + return result +``` + +Обратите внимание, что на последнем шаге `group_texts()` мы создаем новый столбец `labels`, который является копией столбца `input_ids`. Как мы вскоре увидим, это связано с тем, что при моделировании языка по маске цель состоит в прогнозировании случайно замаскированных токенов во входном батче, и, создавая столбец `labels`, мы предоставляем базовую истину для нашей языковой модели, на которой она будет учиться. + +Теперь давайте применим `group_texts()` к нашим токенизированным датасетам, используя нашу надежную функцию `Dataset.map()`: + +```python +lm_datasets = tokenized_datasets.map(group_texts, batched=True) +lm_datasets +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['attention_mask', 'input_ids', 'labels', 'word_ids'], + num_rows: 61289 + }) + test: Dataset({ + features: ['attention_mask', 'input_ids', 'labels', 'word_ids'], + num_rows: 59905 + }) + unsupervised: Dataset({ + features: ['attention_mask', 'input_ids', 'labels', 'word_ids'], + num_rows: 122963 + }) +}) +``` + +Вы можете видеть, что группировка и последующее разбиение текстов на фрагменты позволили получить гораздо больше примеров, чем наши первоначальные 25 000 примеров для частей `train` и `test`. Это потому, что теперь у нас есть примеры с _непрерывными токенами (contiguous tokens)_, которые охватывают несколько примеров из исходного корпуса. В этом можно убедиться, поискав специальные токены `[SEP]` и `[CLS]` в одном из фрагментов: + +```python +tokenizer.decode(lm_datasets["train"][1]["input_ids"]) +``` + +```python out +".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless" +``` + +В этом примере вы можете увидеть два перекрывающихся обзора фильмов: один - о старшей школе, другой - о бездомности. Давайте также посмотрим, как выглядят метки при моделировании языка по маске: + +```python out +tokenizer.decode(lm_datasets["train"][1]["labels"]) +``` + +```python out +".... at.......... high. a classic line : inspector : i'm here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn't! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless" +``` + +Как и ожидалось от нашей функции `group_texts()` выше, это выглядит идентично декодированным `input_ids` - но как тогда наша модель может чему-то научиться? Нам не хватает ключевого шага: вставки токенов `[MASK]` в случайные позиции во входных данных! Давайте посмотрим, как это можно сделать на лету во время дообучения с помощью специального коллатора данных. + +## Дообучение DistilBERT с помощью API `Trainer`[[fine-tuning-distilbert-with-the-trainer-api]] + +Дообучить модель моделирования языка по маске почти то же самое, что и дообучить модель классификации последовательностей, как мы делали в [Главе 3] (/course/chapter3). Единственное отличие заключается в том, что нам нужен специальный коллатор данных, который может случайным образом маскировать некоторые токены в каждом батче текстов. К счастью, 🤗 Transformers поставляется со специальным `DataCollatorForLanguageModeling`, предназначенным именно для этой задачи. Нам нужно только передать ему токенизатор и аргумент `mlm_probability`, который указывает, какую долю токенов нужно маскировать. Мы выберем 15 % - это количество используется для BERT и является распространенным выбором в литературе: + +```python +from transformers import DataCollatorForLanguageModeling + +data_collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm_probability=0.15) +``` + +Чтобы увидеть, как работает случайное маскирование, давайте отправим несколько примеров в коллатор данных. Поскольку он ожидает список `dict`, где каждый `dict` представляет собой один фрагмент непрерывного текста, мы сначала выполняем итерацию над датасетом, прежде чем отправить батч коллатору. Мы удаляем ключ `"word_ids"` для этого коллатора данных, поскольку он его не ожидает: + +```python +samples = [lm_datasets["train"][i] for i in range(2)] +for sample in samples: + _ = sample.pop("word_ids") + +for chunk in data_collator(samples)["input_ids"]: + print(f"\n'>>> {tokenizer.decode(chunk)}'") +``` + +```python output +'>>> [CLS] bromwell [MASK] is a cartoon comedy. it ran at the same [MASK] as some other [MASK] about school life, [MASK] as " teachers ". [MASK] [MASK] [MASK] in the teaching [MASK] lead [MASK] to believe that bromwell high\'[MASK] satire is much closer to reality than is " teachers ". the scramble [MASK] [MASK] financially, the [MASK]ful students whogn [MASK] right through [MASK] pathetic teachers\'pomp, the pettiness of the whole situation, distinction remind me of the schools i knew and their students. when i saw [MASK] episode in [MASK] a student repeatedly tried to burn down the school, [MASK] immediately recalled. [MASK]...' + +'>>> .... at.. [MASK]... [MASK]... high. a classic line plucked inspector : i\'[MASK] here to [MASK] one of your [MASK]. student : welcome to bromwell [MASK]. i expect that many adults of my age think that [MASK]mwell [MASK] is [MASK] fetched. what a pity that it isn\'t! [SEP] [CLS] [MASK]ness ( or [MASK]lessness as george 宇in stated )公 been an issue for years but never [MASK] plan to help those on the street that were once considered human [MASK] did everything from going to school, [MASK], [MASK] vote for the matter. most people think [MASK] the homeless' +``` + +Отлично, сработало! Мы видим, что токен `[MASK]` был случайным образом вставлен в различные места нашего текста. Это будут токены, которые наша модель должна будет предсказать в процессе обучения - и прелесть коллатора данных в том, что он будет случайным образом вставлять `[MASK]` в каждом батче! + + + +✏️ **Попробуйте!** Запустите приведенный выше фрагмент кода несколько раз, чтобы увидеть, как случайное маскирование происходит на ваших глазах! Также замените метод `tokenizer.decode()` на `tokenizer.convert_ids_to_tokens()`, чтобы увидеть, что иногда маскируется один токен из данного слова, а не все остальные. + + + +{#if fw === 'pt'} + +Одним из побочных эффектов случайного маскирования является то, что наши метрики оценки не будут детерминированными при использовании `Trainer`, поскольку мы используем один и тот же коллатор данных для обучающего и тестового наборов. Позже, когда мы рассмотрим дообучение с помощью 🤗 Accelerate, мы увидим, как можно использовать гибкость пользовательского цикла оценки, чтобы заморозить случайность. + +{/if} + +При обучении моделей для моделирования языка с маской можно использовать один из методов - маскировать целые слова, а не только отдельные токены. Такой подход называется _маскированием целых слов (whole word masking)_. Если мы хотим использовать маскирование целых слов, нам нужно будет самостоятельно создать коллатор данных. Коллатор данных - это просто функция, которая берет список примеров и преобразует их в батч, так что давайте сделаем это прямо сейчас! Мы будем использовать идентификаторы слов, вычисленные ранее, для создания карты между индексами слов и соответствующими токенами, затем случайным образом определим, какие слова нужно маскировать, и применим эту маску к входным данным. Обратите внимание, что все метки - `-100`, кроме тех, что соответствуют словам-маскам. + +{#if fw === 'pt'} + +```py +import collections +import numpy as np + +from transformers import default_data_collator + +wwm_probability = 0.2 + + +def whole_word_masking_data_collator(features): + for feature in features: + word_ids = feature.pop("word_ids") + + # Создаем отображение между словами и соответствующими индексами токенов + mapping = collections.defaultdict(list) + current_word_index = -1 + current_word = None + for idx, word_id in enumerate(word_ids): + if word_id is not None: + if word_id != current_word: + current_word = word_id + current_word_index += 1 + mapping[current_word_index].append(idx) + + # Случайно маскируем слова + mask = np.random.binomial(1, wwm_probability, (len(mapping),)) + input_ids = feature["input_ids"] + labels = feature["labels"] + new_labels = [-100] * len(labels) + for word_id in np.where(mask)[0]: + word_id = word_id.item() + for idx in mapping[word_id]: + new_labels[idx] = labels[idx] + input_ids[idx] = tokenizer.mask_token_id + feature["labels"] = new_labels + + return default_data_collator(features) +``` + +{:else} + +```py +import collections +import numpy as np + +from transformers.data.data_collator import tf_default_data_collator + +wwm_probability = 0.2 + + +def whole_word_masking_data_collator(features): + for feature in features: + word_ids = feature.pop("word_ids") + + # Создаем отображение между словами и соответствующими индексами токенов + mapping = collections.defaultdict(list) + current_word_index = -1 + current_word = None + for idx, word_id in enumerate(word_ids): + if word_id is not None: + if word_id != current_word: + current_word = word_id + current_word_index += 1 + mapping[current_word_index].append(idx) + + # Случайно маскируем слова + mask = np.random.binomial(1, wwm_probability, (len(mapping),)) + input_ids = feature["input_ids"] + labels = feature["labels"] + new_labels = [-100] * len(labels) + for word_id in np.where(mask)[0]: + word_id = word_id.item() + for idx in mapping[word_id]: + new_labels[idx] = labels[idx] + input_ids[idx] = tokenizer.mask_token_id + feature["labels"] = new_labels + + return tf_default_data_collator(features) +``` + +{/if} + +Далее мы можем опробовать ее на тех же примерах, что и раньше: + +```py +samples = [lm_datasets["train"][i] for i in range(2)] +batch = whole_word_masking_data_collator(samples) + +for chunk in batch["input_ids"]: + print(f"\n'>>> {tokenizer.decode(chunk)}'") +``` + +```python out +'>>> [CLS] bromwell high is a cartoon comedy [MASK] it ran at the same time as some other programs about school life, such as " teachers ". my 35 years in the teaching profession lead me to believe that bromwell high\'s satire is much closer to reality than is " teachers ". the scramble to survive financially, the insightful students who can see right through their pathetic teachers\'pomp, the pettiness of the whole situation, all remind me of the schools i knew and their students. when i saw the episode in which a student repeatedly tried to burn down the school, i immediately recalled.....' + +'>>> .... [MASK] [MASK] [MASK] [MASK]....... high. a classic line : inspector : i\'m here to sack one of your teachers. student : welcome to bromwell high. i expect that many adults of my age think that bromwell high is far fetched. what a pity that it isn\'t! [SEP] [CLS] homelessness ( or houselessness as george carlin stated ) has been an issue for years but never a plan to help those on the street that were once considered human who did everything from going to school, work, or vote for the matter. most people think of the homeless' +``` + + + +✏️ **Попробуйте!** Запустите приведенный выше фрагмент кода несколько раз, чтобы увидеть, как случайное маскирование происходит на ваших глазах! Также замените метод `tokenizer.decode()` на `tokenizer.convert_ids_to_tokens()`, чтобы увидеть, что токены из данного слова всегда маскируются вместе. + + + +Теперь, когда у нас есть два колатора данных, остальные шаги по дообучению стандартны. Обучение может занять много времени в Google Colab, если вам не посчастливилось получить мифический GPU P100 😭, поэтому мы сначала уменьшим размер обучающего набора до нескольких тысяч примеров. Не волнуйтесь, мы все равно получим довольно приличную языковую модель! Быстрый способ уменьшить размер датасета в 🤗 Datasets - это функция `Dataset.train_test_split()`, которую мы рассматривали в [Главе 5](/course/chapter5): + +```python +train_size = 10_000 +test_size = int(0.1 * train_size) + +downsampled_dataset = lm_datasets["train"].train_test_split( + train_size=train_size, test_size=test_size, seed=42 +) +downsampled_dataset +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['attention_mask', 'input_ids', 'labels', 'word_ids'], + num_rows: 10000 + }) + test: Dataset({ + features: ['attention_mask', 'input_ids', 'labels', 'word_ids'], + num_rows: 1000 + }) +}) +``` + +Это автоматически создаст новые части `train` и `test`, с размером обучающего набора в 10 000 примеров и валидацией в 10 % от этого количества - не стесняйтесь увеличить это значение, если у вас мощный GPU! Следующее, что нам нужно сделать, это авторизоваться на Hugging Face Hub. Если вы выполняете этот код в блокноте, вы можете сделать это с помощью следующей служебной функции: + +```python +from huggingface_hub import notebook_login + +notebook_login() +``` + +которая отобразит виджет, где вы можете ввести свои учетные данные. В качестве альтернативы можно выполнить команду: + +``` +huggingface-cli login +``` + +в вашем любимом терминале и авторизуйтесь там. + +{#if fw === 'tf'} + +После того как мы авторизовались, мы можем создавать наши датасеты `tf.data`. Для этого мы воспользуемся методом `prepare_tf_dataset()`, который использует нашу модель для автоматического инференса того, какие столбцы должны войти в датасет. Если вы хотите контролировать, какие именно столбцы будут использоваться, вы можете использовать метод `Dataset.to_tf_dataset()` вместо этого. Для простоты мы будем использовать стандартный коллатор данных, но вы также можете попробовать коллатор для маскировки слов целиком и сравнить результаты в качестве упражнения: + +```python +tf_train_dataset = model.prepare_tf_dataset( + downsampled_dataset["train"], + collate_fn=data_collator, + shuffle=True, + batch_size=32, +) + +tf_eval_dataset = model.prepare_tf_dataset( + downsampled_dataset["test"], + collate_fn=data_collator, + shuffle=False, + batch_size=32, +) +``` + +Далее мы задаем гиперпараметры обучения и компилируем нашу модель. Мы используем функцию `create_optimizer()` из библиотеки 🤗 Transformers, которая возвращает нам оптимизатор `AdamW` с линейным затуханием скорости обучения. Мы также используем встроенные потери модели, которые используются по умолчанию, если в качестве аргумента `compile()` не указаны потери, и устанавливаем точность обучения на `"mixed_float16"`. Обратите внимание, что если вы используете Colab GPU или другой GPU, который не имеет поддержки ускорения float16, вам, вероятно, следует закомментировать эту строку. + +Кроме того, мы настроили `PushToHubCallback`, который будет сохранять модель в Hub после каждой эпохи. Вы можете указать имя репозитория, в который хотите отправить модель, с помощью аргумента `hub_model_id` (в частности, этот аргумент нужно использовать, чтобы отправить модель в организацию). Например, чтобы отправить модель в организацию [`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/distilbert-finetuned-imdb"`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, так что в нашем случае это будет `"lewtun/distilbert-finetuned-imdb"`. + +```python +from transformers import create_optimizer +from transformers.keras_callbacks import PushToHubCallback +import tensorflow as tf + +num_train_steps = len(tf_train_dataset) +optimizer, schedule = create_optimizer( + init_lr=2e-5, + num_warmup_steps=1_000, + num_train_steps=num_train_steps, + weight_decay_rate=0.01, +) +model.compile(optimizer=optimizer) + +# Train in mixed-precision float16 +tf.keras.mixed_precision.set_global_policy("mixed_float16") + +model_name = model_checkpoint.split("/")[-1] +callback = PushToHubCallback( + output_dir=f"{model_name}-finetuned-imdb", tokenizer=tokenizer +) +``` + +Теперь мы готовы запустить `model.fit()`, но перед этим давайте вкратце рассмотрим _перплексию (perplexity)_, которая является общей метрикой для оценки качества работы языковых моделей. + +{:else} + +После того как мы авторизовались, мы можем указать аргументы для `Trainer`: + +```python +from transformers import TrainingArguments + +batch_size = 64 +# Показываем потери при обучении для каждой эпохи +logging_steps = len(downsampled_dataset["train"]) // batch_size +model_name = model_checkpoint.split("/")[-1] + +training_args = TrainingArguments( + output_dir=f"{model_name}-finetuned-imdb", + overwrite_output_dir=True, + evaluation_strategy="epoch", + learning_rate=2e-5, + weight_decay=0.01, + per_device_train_batch_size=batch_size, + per_device_eval_batch_size=batch_size, + push_to_hub=True, + fp16=True, + logging_steps=logging_steps, +) +``` + +Здесь мы изменили несколько параметров по умолчанию, включая `logging_steps`, чтобы обеспечить отслеживание потерь при обучении в каждой эпохе. Мы также использовали `fp16=True`, чтобы включить обучение со смешанной точностью, что дает нам еще большее улучшение скорости. По умолчанию `Trainer` удаляет все столбцы, которые не являются частью метода `forward()` модели. Это означает, что если вы используете коллатор для маскировки слов целиком, вам также нужно установить `remove_unused_columns=False`, чтобы не потерять колонку `word_ids` во время обучения. + +Обратите внимание, что с помощью аргумента `hub_model_id` можно указать имя репозитория, в который вы хотите отправить модель (в частности, этот аргумент нужно использовать, чтобы отправить модель в ырепозиторий организации). Например, когда мы отправили модель в репозиторий организации [`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/distilbert-finetuned-imdb"` в `TrainingArguments`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданной вами выходной директорией, так что в нашем случае это будет `"lewtun/distilbert-finetuned-imdb"`. + +Теперь у нас есть все компоненты для создания `Trainer`. Здесь мы просто используем стандартный `data_collator`, но вы можете попробовать коллатор для маскирования слов целиком и сравнить результаты в качестве упражнения: + +```python +from transformers import Trainer + +trainer = Trainer( + model=model, + args=training_args, + train_dataset=downsampled_dataset["train"], + eval_dataset=downsampled_dataset["test"], + data_collator=data_collator, + tokenizer=tokenizer, +) +``` + +Теперь мы готовы запустить `trainer.train()`, но перед этим давайте вкратце рассмотрим _перплексию (perplexity), которая является общей метрикой для оценки качества языковых моделей. + +{/if} + +### Перплексия языковых моделей[[perplexity-for-language-models]] + + + +В отличие от других задач, таких как классификация текстов или ответов на вопросы, где для обучения нам дается помеченный корпус, при языковом моделировании у нас нет никаких явных меток. Как же определить, какая языковая модель является хорошей? Как и в случае с функцией автокоррекции в вашем телефоне, хорошая языковая модель - это та, которая присваивает высокую вероятность грамматически правильным предложениям и низкую вероятность бессмысленным предложениям. Чтобы лучше представить себе, как это выглядит, вы можете найти в Интернете целые подборки "неудач автокоррекции", где модель в телефоне человека выдает довольно забавные (и часто неуместные) завершения! + +{#if fw === 'pt'} + +Если предположить, что наш тестовый набор состоит в основном из грамматически правильных предложений, то одним из способов измерения качества нашей языковой модели является подсчет вероятностей, которые она присваивает следующему слову во всех предложениях тестового набора. Высокие вероятности указывают на то, что модель не "удивлена" и не "перплексирована" не виденными ранее примерами, и предполагают, что она усвоила основные грамматические закономерности языка. Существуют различные математические определения перплексии, но то, которое мы будем использовать, определяет ее как экспоненту потери перекрестной энтропии. Таким образом, мы можем вычислить перплексию нашей предварительно обученной модели, используя функцию `Trainer.evaluate()` для вычисления потерь кросс-энтропии на тестовом наборе, а затем взяв экспоненту результата: + +```python +import math + +eval_results = trainer.evaluate() +print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}") +``` + +{:else} + +Если предположить, что наш тестовый набор состоит в основном из грамматически правильных предложений, то одним из способов измерения качества нашей языковой модели является подсчет вероятностей, которые она присваивает следующему слову во всех предложениях тестового набора. Высокие вероятности указывают на то, что модель не "удивлена" или не "перплексирована" не виденными ранее примерами, и предполагают, что она выучила основные грамматические закономерности языка. Существуют различные математические определения перплексии, но то, которое мы будем использовать, определяет ее как экспоненту потери перекрестной энтропии. Таким образом, мы можем вычислить перплексию нашей предварительно обученной модели, используя метод `model.evaluate()` для вычисления потери кросс-энтропии на тестовом наборе, а затем взяв экспоненту результата: + +```python +import math + +eval_loss = model.evaluate(tf_eval_dataset) +print(f"Perplexity: {math.exp(eval_loss):.2f}") +``` + +{/if} + +```python out +>>> Perplexity: 21.75 +``` + +Более низкий показатель перплексии означает лучшую языковую модель, и мы видим, что наша начальная модель имеет несколько большое значение. Давайте посмотрим, сможем ли мы снизить его, дообучив модель! Для этого сначала запустим цикл обучения: + +{#if fw === 'pt'} + +```python +trainer.train() +``` + +{:else} + +```python +model.fit(tf_train_dataset, validation_data=tf_eval_dataset, callbacks=[callback]) +``` + +{/if} + +и затем вычислить результирующую перплексию на тестовом наборе, как и раньше: + +{#if fw === 'pt'} + +```python +eval_results = trainer.evaluate() +print(f">>> Perplexity: {math.exp(eval_results['eval_loss']):.2f}") +``` + +{:else} + +```python +eval_loss = model.evaluate(tf_eval_dataset) +print(f"Perplexity: {math.exp(eval_loss):.2f}") +``` + +{/if} + +```python out +>>> Perplexity: 11.32 +``` + +Неплохо - это довольно значительное уменьшение перплексии, что говорит о том, что модель узнала что-то новое об области рецензий на фильмы! + +{#if fw === 'pt'} + +После завершения обучения мы можем отправить карточку модели с информацией об обучении в Hub (контрольные точки сохраняются во время самого обучения): + +```python +trainer.push_to_hub() +``` + +{/if} + + + +✏️ **Попробуйте!** Запустите обучение, описанное выше, после замены коллатора данных на коллатор маскирующий все слово. Получили ли вы лучшие результаты? + + + +{#if fw === 'pt'} + +В нашем случае нам не нужно было делать ничего особенного с циклом обучения, но в некоторых случаях вам может понадобиться реализовать некоторую пользовательскую логику. Для таких случаев вы можете использовать 🤗 Accelerate - давайте посмотрим! + +## Дообучение DistilBERT с помощью 🤗 Accelerate[[fine-tuning-distilbert-with-accelerate]] + +Как мы видели на примере `Trainer`, дообучение модели маскированного языкового моделирования очень похоже на пример классификации текста из [Главы 3](/course/chapter3). Фактически, единственной особенностью является использование специального коллатора данных, о котором мы уже рассказывали ранее в этом разделе! + +Однако мы видели, что `DataCollatorForLanguageModeling` также применяет случайное маскирование при каждой оценке, поэтому мы увидим некоторые колебания в наших оценках перплексии при каждом цикле обучения. Один из способов устранить этот источник случайности - применить маскирование _один раз_ ко всему тестовому набору, а затем использовать стандартный коллатор данных в 🤗 Transformers для сбора батчей во время оценки. Чтобы увидеть, как это работает, давайте реализуем простую функцию, которая применяет маскирование к батчу, подобно нашей первой работе с `DataCollatorForLanguageModeling`: + +```python +def insert_random_mask(batch): + features = [dict(zip(batch, t)) for t in zip(*batch.values())] + masked_inputs = data_collator(features) + # Создаем новый "маскированный" столбец для каждого столбца в датасете + return {"masked_" + k: v.numpy() for k, v in masked_inputs.items()} +``` + +Далее мы применим эту функцию к нашему тестовому набору и удалим столбцы без маскирования, чтобы заменить их столбцами с маскированием. Вы можете использовать маскирование целых слов, заменив `data_collator` выше на соответствующий, в этом случае вам следует удалить первую строку здесь: + +```py +downsampled_dataset = downsampled_dataset.remove_columns(["word_ids"]) +eval_dataset = downsampled_dataset["test"].map( + insert_random_mask, + batched=True, + remove_columns=downsampled_dataset["test"].column_names, +) +eval_dataset = eval_dataset.rename_columns( + { + "masked_input_ids": "input_ids", + "masked_attention_mask": "attention_mask", + "masked_labels": "labels", + } +) +``` + +Затем мы можем настроить загрузчики данных как обычно, но для оценочного набора мы будем использовать `default_data_collator` из 🤗 Transformers: + +```python +from torch.utils.data import DataLoader +from transformers import default_data_collator + +batch_size = 64 +train_dataloader = DataLoader( + downsampled_dataset["train"], + shuffle=True, + batch_size=batch_size, + collate_fn=data_collator, +) +eval_dataloader = DataLoader( + eval_dataset, batch_size=batch_size, collate_fn=default_data_collator +) +``` + +Здесь мы следуем стандартным шагам 🤗 Accelerate. Первым делом загружаем свежую версию предварительно обученной модели: + +``` +model = AutoModelForMaskedLM.from_pretrained(model_checkpoint) +``` + +Затем нужно указать оптимизатор; мы будем использовать стандартный `AdamW`: + +```python +from torch.optim import AdamW + +optimizer = AdamW(model.parameters(), lr=5e-5) +``` + +Теперь, имея эти объекты, мы можем подготовить все для обучения с помощью объекта `Accelerator`: + +```python +from accelerate import Accelerator + +accelerator = Accelerator() +model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader +) +``` + +Теперь, когда наша модель, оптимизатор и загрузчики данных настроены, мы можем задать планировщик скорости обучения следующим образом: + +```python +from transformers import get_scheduler + +num_train_epochs = 3 +num_update_steps_per_epoch = len(train_dataloader) +num_training_steps = num_train_epochs * num_update_steps_per_epoch + +lr_scheduler = get_scheduler( + "linear", + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=num_training_steps, +) +``` + +Осталось сделать еще одну последнюю вещь перед обучением: создать репозиторий модели на Hugging Face Hub! Мы можем использовать библиотеку 🤗 Hub, чтобы сначала сгенерировать полное имя нашего репозитория: + +```python +from huggingface_hub import get_full_repo_name + +model_name = "distilbert-base-uncased-finetuned-imdb-accelerate" +repo_name = get_full_repo_name(model_name) +repo_name +``` + +```python out +'lewtun/distilbert-base-uncased-finetuned-imdb-accelerate' +``` + +затем создадим и клонируем репозиторий, используя класс `Repository` из 🤗 Hub: + +```python +from huggingface_hub import Repository + +output_dir = model_name +repo = Repository(output_dir, clone_from=repo_name) +``` + +После этого остается только написать полный цикл обучения и оценки: + +```python +from tqdm.auto import tqdm +import torch +import math + +progress_bar = tqdm(range(num_training_steps)) + +for epoch in range(num_train_epochs): + # Обучение + model.train() + for batch in train_dataloader: + outputs = model(**batch) + loss = outputs.loss + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + progress_bar.update(1) + + # Оценка + model.eval() + losses = [] + for step, batch in enumerate(eval_dataloader): + with torch.no_grad(): + outputs = model(**batch) + + loss = outputs.loss + losses.append(accelerator.gather(loss.repeat(batch_size))) + + losses = torch.cat(losses) + losses = losses[: len(eval_dataset)] + try: + perplexity = math.exp(torch.mean(losses)) + except OverflowError: + perplexity = float("inf") + + print(f">>> Epoch {epoch}: Perplexity: {perplexity}") + + # Сохранение и загрузка + accelerator.wait_for_everyone() + unwrapped_model = accelerator.unwrap_model(model) + unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save) + if accelerator.is_main_process: + tokenizer.save_pretrained(output_dir) + repo.push_to_hub( + commit_message=f"Training in progress epoch {epoch}", blocking=False + ) +``` + +```python out +>>> Epoch 0: Perplexity: 11.397545307900472 +>>> Epoch 1: Perplexity: 10.904909330983092 +>>> Epoch 2: Perplexity: 10.729503505340409 +``` + +Круто, мы смогли оценить перплексию для каждой эпохи и обеспечить воспроизводимость нескольких циклов обучения! + +{/if} + +## Использование нашей дообученной модели[[using-our-fine-tuned-model]] + +Вы можете взаимодействовать с дообученной моделью либо с помощью ее виджета на Hub, либо локально с помощью `pipeline` из 🤗 Transformers. Давайте воспользуемся последним вариантом, чтобы загрузить нашу модель с помощью конвейера `fill-mask`: + +```python +from transformers import pipeline + +mask_filler = pipeline( + "fill-mask", model="huggingface-course/distilbert-base-uncased-finetuned-imdb" +) +``` + +Затем мы можем передать конвейеру наш пример текста " This is a great [MASK]" и посмотреть, каковы 5 лучших прогнозов: + +```python +preds = mask_filler(text) + +for pred in preds: + print(f">>> {pred['sequence']}") +``` + +```python out +'>>> this is a great movie.' +'>>> this is a great film.' +'>>> this is a great story.' +'>>> this is a great movies.' +'>>> this is a great character.' +``` + +Отлично - наша модель явно адаптировала свои веса, чтобы прогнозировать слова, которые сильнее ассоциируются с фильмами! + + + +На этом мы завершаем наш первый эксперимент по обучению языковой модели. В [разделе 6](/course/en/chapter7/6) вы узнаете, как обучить авторегрессионную модель типа GPT-2 с нуля; загляните туда, если хотите посмотреть, как можно предварительно обучить свою собственную модель трансформера! + + + +✏️ **Попробуйте!** Чтобы оценить преимущества адаптации к домену, дообучите классификатор на метках IMDb как для предварительно обученных, так и для дообученных контрольных точек DistilBERT. Если вам нужно освежить в памяти классификацию текстов, ознакомьтесь с [Главой 3] (/course/chapter3). + + diff --git a/chapters/ru/chapter7/4.mdx b/chapters/ru/chapter7/4.mdx new file mode 100644 index 000000000..f1362724f --- /dev/null +++ b/chapters/ru/chapter7/4.mdx @@ -0,0 +1,1002 @@ + + +# Перевод[[translation]] + +{#if fw === 'pt'} + + + +{:else} + + + +{/if} + +Теперь давайте погрузимся в перевод. Это еще одна [задача преобразования последовательности в последовательность (sequence-to-sequence)](/course/chapter1/7), что означает, что это проблема, которую можно сформулировать как переход от одной последовательности к другой. В этом смысле задача очень близка к [резюмированию (summarization)](/course/chapter7/6), и вы можете приспособить то, что мы здесь рассмотрим, к другим задачам преобразования последовательности в последовательность, таким как: + +- **Перенос стиля (Style transfer)**: Создание модели, которая *переводит* тексты, написанные в определенном стиле, в другой (например, из формального в повседневный или из шекспировского английского в современный). +- **Генеративные ответы на вопросы (Generative question answering)**: Создание модели, которая генерирует ответы на вопросы, учитывая контекст + + + +Если у вас есть достаточно большой корпус текстов на двух (или более) языках, вы можете обучить новую модель перевода с нуля, как мы это сделаем в разделе по [казуальному языковому моделированию (causal language modeling)] (/course/chapter7/6). Однако быстрее будет дообучить существующую модель перевода, будь то многоязычная модель типа mT5 или mBART, которую нужно дообучить для конкретной пары языков, или даже модель, специализированная для перевода с одного языка на другой, которую нужно дообучить для конкретного корпуса. + +В этом разделе мы дообучим модель Marian, предварительно обученную переводу с английского на французский (поскольку многие сотрудники Hugging Face говорят на обоих этих языках), на датасете [KDE4](https://huggingface.co/datasets/kde4), который представляет собой набор локализованных файлов для приложений [KDE](https://apps.kde.org/). Модель, которую мы будем использовать, была предварительно обучена на большом корпусе французских и английских текстов, взятых из [Opus dataset](https://opus.nlpl.eu/), который фактически содержит датасет KDE4. Но даже если модель, которую мы используем, видела эти данные во время предварительного обучения, мы увидим, что после дообучения мы сможем получить ее лучшую версию. + +Когда мы закончим, у нас будет модель, способная делать прогнозы, подобные этому: + + + + +One-hot encoded labels for question answering. + + + +Как и в предыдущих разделах, вы можете найти актуальную модель, которую мы обучим и загрузим на Hub, используя приведенный ниже код, и перепроверить ее предсказания [здесь](https://huggingface.co/huggingface-course/marian-finetuned-kde4-en-to-fr?text=This+plugin+allows+you+to+automatically+translate+web+pages+between+several+languages.). + +## Подготовка данных[[preparing-the-data]] + +Чтобы дообучить или обучить модель перевода с нуля, нам понадобится датасет, подходящий для этой задачи. Как уже упоминалось, мы будем использовать [датасет KDE4](https://huggingface.co/datasets/kde4) , но вы можете легко адаптировать код для использования своих собственных данных, если у вас есть пары предложений на двух языках, с которых вы хотите переводить и на которые хотите переводить. Обратитесь к [Главе 5](/course/chapter5) если вам нужно вспомнить, как загружать пользовательские данные в `Dataset`. + +### Датасет KDE4[[the-kde4-dataset]] + +Как обычно, мы загружаем наш датасет с помощью функции `load_dataset()`: + +```py +from datasets import load_dataset + +raw_datasets = load_dataset("kde4", lang1="en", lang2="fr") +``` + +Если вы хотите работать с другой парой языков, вы можете указать их по кодам. Всего для этого датасета доступно 92 языка; вы можете увидеть их все, развернув языковые теги в его [карточке датасета](https://huggingface.co/datasets/kde4). + +Language available for the KDE4 dataset. + +Давайте посмотрим на датасет: + +```py +raw_datasets +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['id', 'translation'], + num_rows: 210173 + }) +}) +``` + +У нас есть 210 173 пары предложений, но в одной части, поэтому нам нужно создать собственный проверочный набор. Как мы видели в [Главе 5] (/course/chapter5), у `Dataset` есть метод `train_test_split()`, который может нам помочь. Мы зададим seed для воспроизводимости: + +```py +split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20) +split_datasets +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['id', 'translation'], + num_rows: 189155 + }) + test: Dataset({ + features: ['id', 'translation'], + num_rows: 21018 + }) +}) +``` + +Мы можем переименовать ключ `"test"` в `"validation"` следующим образом: + +```py +split_datasets["validation"] = split_datasets.pop("test") +``` + +Теперь давайте рассмотрим один элемент датасета: + +```py +split_datasets["train"][1]["translation"] +``` + +```python out +{'en': 'Default to expanded threads', + 'fr': 'Par défaut, développer les fils de discussion'} +``` + +Мы получаем словарь с двумя предложениями на запрошенной паре языков. Одна из особенностей этого датасета, наполненного техническими терминами в области компьютерных наук, заключается в том, что все они полностью переведены на французский язык. Однако французские инженеры при разговоре оставляют большинство специфических для компьютерных наук слов на английском. Например, слово "threads" вполне может встретиться во французском предложении, особенно в техническом разговоре; но в данном датасете оно переведено как более правильное "fils de discussion". Используемая нами модель, предварительно обученная на большем корпусе французских и английских предложений, выбирает более простой вариант - оставить слово как есть: + +```py +from transformers import pipeline + +model_checkpoint = "Helsinki-NLP/opus-mt-en-fr" +translator = pipeline("translation", model=model_checkpoint) +translator("Default to expanded threads") +``` + +```python out +[{'translation_text': 'Par défaut pour les threads élargis'}] +``` + +Другой пример такого поведения можно увидеть на примере слова "plugin", которое официально не является французским словом, но большинство носителей языка поймут его и не станут переводить. +В датасете KDE4 это слово было переведено на французский как более официальное "module d'extension": + +```py +split_datasets["train"][172]["translation"] +``` + +```python out +{'en': 'Unable to import %1 using the OFX importer plugin. This file is not the correct format.', + 'fr': "Impossible d'importer %1 en utilisant le module d'extension d'importation OFX. Ce fichier n'a pas un format correct."} +``` + +Однако наша предварительно обученная модель придерживается компактного и знакомого английского слова: + +```py +translator( + "Unable to import %1 using the OFX importer plugin. This file is not the correct format." +) +``` + +```python out +[{'translation_text': "Impossible d'importer %1 en utilisant le plugin d'importateur OFX. Ce fichier n'est pas le bon format."}] +``` + +Будет интересно посмотреть, сможет ли наша дообученная модель уловить эти особенности датасета (спойлер: сможет). + + + + + +✏️ **Попробуйте!** Еще одно английское слово, которое часто используется во французском языке, - "email". Найдите в обучающем датасете первый образец, в котором используется это слово. Как оно переводится? Как предварительно обученная модель переводит то же английское предложение? + + + +### Предварительная обработка данных[[processing-the-data]] + + + +Вы уже должны знать, как это делается: все тексты нужно преобразовать в наборы идентификаторов токенов, чтобы модель могла понять их смысл. Для этой задачи нам понадобится токенизация как входных данных, так и целевых. Наша первая задача - создать объект `tokenizer`. Как отмечалось ранее, мы будем использовать предварительно обученную модель Marian English to French. Если вы будете пробовать этот код с другой парой языков, обязательно адаптируйте контрольную точку модели. Организация [Helsinki-NLP](https://huggingface.co/Helsinki-NLP) предоставляет более тысячи моделей на разных языках. + +```python +from transformers import AutoTokenizer + +model_checkpoint = "Helsinki-NLP/opus-mt-en-fr" +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint, return_tensors="pt") +``` + +Вы также можете заменить `model_checkpoint` на любую другую модель из [Hub](https://huggingface.co/models) или локальной папки, в которой вы сохранили предварительно обученную модель и токенизатор. + + + +💡 Если вы используете многоязыковой токенизатор, такой как mBART, mBART-50 или M2M100, вам нужно задать языковые коды ваших входных и целевых данных в токенизаторе, задав правильные значения параметрам `tokenizer.src_lang` и `tokenizer.tgt_lang`. + + + +Подготовка наших данных довольно проста. Нужно помнить только об одном: необходимо убедиться, что токенизатор обрабатывает целевые значения на выходном языке (здесь - французском). Вы можете сделать это, передав целевые данные в аргумент `text_targets` метода `__call__` токенизатора. + +Чтобы увидеть, как это работает, давайте обработаем по одному примеру каждого языка из обучающего набора: + +```python +en_sentence = split_datasets["train"][1]["translation"]["en"] +fr_sentence = split_datasets["train"][1]["translation"]["fr"] + +inputs = tokenizer(en_sentence, text_target=fr_sentence) +inputs +``` + +```python out +{'input_ids': [47591, 12, 9842, 19634, 9, 0], 'attention_mask': [1, 1, 1, 1, 1, 1], 'labels': [577, 5891, 2, 3184, 16, 2542, 5, 1710, 0]} +``` + +Как мы видим, в выходных данных содержатся входные идентификаторы, связанные с английским предложением, а идентификаторы, связанные с французским, хранятся в поле `labels`. Если вы забудете указать, что выполняете токенизацию меток, они будут токенизированы входным токенизатором, что в случае с моделью Marian не приведет ни к чему хорошему: + +```python +wrong_targets = tokenizer(fr_sentence) +print(tokenizer.convert_ids_to_tokens(wrong_targets["input_ids"])) +print(tokenizer.convert_ids_to_tokens(inputs["labels"])) +``` + +```python out +['▁Par', '▁dé', 'f', 'aut', ',', '▁dé', 've', 'lop', 'per', '▁les', '▁fil', 's', '▁de', '▁discussion', ''] +['▁Par', '▁défaut', ',', '▁développer', '▁les', '▁fils', '▁de', '▁discussion', ''] +``` + +Как мы видим, при использовании английского токенизатора для предварительной обработки французского предложения получается гораздо больше токенов, поскольку токенизатор не знает ни одного французского слова (кроме тех, которые также встречаются в английском языке, например "discussion"). + +Поскольку `inputs` - это словарь с нашими обычными ключами (идентификаторы входных данных, маска внимания и т. д.), последним шагом будет определение функции предварительной обработки, которую мы будем применять к датасетам: + +```python +max_length = 128 + + +def preprocess_function(examples): + inputs = [ex["en"] for ex in examples["translation"]] + targets = [ex["fr"] for ex in examples["translation"]] + model_inputs = tokenizer( + inputs, text_target=targets, max_length=max_length, truncation=True + ) + return model_inputs +``` + +Обратите внимание, что мы установили одинаковую максимальную длину для наших входов и выходов. Поскольку тексты, с которыми мы имеем дело, довольно короткие, мы используем 128. + + + +💡 Если вы используете модель T5 (точнее, одну из контрольных точек `t5-xxx`), модель будет ожидать, что текстовые данные будут иметь префикс, указывающий на поставленную задачу, например `translate: English to French:`. + + + + + +⚠️ Мы не обращаем внимания на маску внимания целевых значений, так как модель не будет этого ожидать. Вместо этого метки, соответствующие токенам дополнения, должны быть заданы как `-100`, чтобы они игнорировались при вычислении потерь. Это будет сделано нашим коллатором данных позже, так как мы применяем динамическое дополнение (dynamic padding), но если вы используете дополнение (padding) здесь, вы должны адаптировать функцию предварительной обработки данных, чтобы установить все метки, соответствующие токену дополнения, в `-100`. + + + +Теперь мы можем применить эту функцию предварительной обработки ко всем частям нашего датасета за один раз: + +```py +tokenized_datasets = split_datasets.map( + preprocess_function, + batched=True, + remove_columns=split_datasets["train"].column_names, +) +``` + +Теперь, когда данные прошли предварительную обработку, мы готовы дообучить нашу предварительно обученную модель! + +{#if fw === 'pt'} + +## Дообучение модели с помощью API `Trainer`[[fine-tuning-the-model-with-the-trainer-api]] + +Фактический код, использующий `Trainer`, будет таким же, как и раньше, с одним лишь небольшим изменением: мы используем [`Seq2SeqTrainer`](https://huggingface.co/transformers/main_classes/trainer.html#seq2seqtrainer), который является подклассом `Trainer`, что позволит нам правильно работать с оценкой, используя метод `generate()` для предсказания выходов на основе входов. Мы рассмотрим это более подробно, когда будем говорить о вычислении метрик. + +Прежде всего, нам нужна реальная модель, которую нужно дообучить. Мы воспользуемся обычным API `AutoModel`: + +```py +from transformers import AutoModelForSeq2SeqLM + +model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint) +``` + +{:else} + +## Дообучение модели с Keras[[fine-tuning-the-model-with-keras]] + +Прежде всего, нам нужна актуальная модель, которую нужно дообучить. Мы воспользуемся обычным API `AutoModel`: + +```py +from transformers import TFAutoModelForSeq2SeqLM + +model = TFAutoModelForSeq2SeqLM.from_pretrained(model_checkpoint, from_pt=True) +``` + + + +💡 Контрольная точка `Helsinki-NLP/opus-mt-en-fr` имеет только веса PyTorch, поэтому +вы получите ошибку, если попытаетесь загрузить модель без использования аргумента +`from_pt=True` в методе `from_pretrained()`. Когда вы указываете +`from_pt=True`, библиотека автоматически загрузит и преобразует +веса из PyTorch для вас. Как видите, очень просто переключаться между +фреймворками в 🤗 Transformers! + + + +{/if} + +Обратите внимание, что в этот раз мы используем модель, которая была обучена на задаче перевода и фактически уже может быть использована, поэтому нет предупреждения о пропущенных или новых инициализированных весах. + +### Сопоставление данных[[data-collation]] + +Для динамического батча нам понадобится коллатор данных для работы с дополнением (padding). Мы не можем просто использовать `DataCollatorWithPadding`, как в [Главе 3](/course/chapter3), потому что в этом случае будут заполнены только входы (идентификаторы входов, маска внимания и идентификаторы типов токенов). Наши метки также должны быть дополнены до максимальной длины, встречающейся в метках. И, как уже говорилось, добовляемое значение, используемое для дополнения меток, должно быть `-100`, а не добавочный токен токенизатора, чтобы эти добавочные значения игнорировались при вычислении потерь. + +Все это делает [`DataCollatorForSeq2Seq`](https://huggingface.co/transformers/main_classes/data_collator.html#datacollatorforseq2seq). Как и `DataCollatorWithPadding`, он принимает `tokenizer`, используемый для препроцессирования входных данных, а также `model`. Это связано с тем, что данный коллатор данных также будет отвечать за подготовку входных идентификаторов декодера, которые представляют собой сдвинутые версии меток со специальным токеном в начале. Поскольку для разных архитектур этот сдвиг выполняется по-разному, `DataCollatorForSeq2Seq` должен знать объект `model`: + +{#if fw === 'pt'} + +```py +from transformers import DataCollatorForSeq2Seq + +data_collator = DataCollatorForSeq2Seq(tokenizer, model=model) +``` + +{:else} + +```py +from transformers import DataCollatorForSeq2Seq + +data_collator = DataCollatorForSeq2Seq(tokenizer, model=model, return_tensors="tf") +``` + +{/if} + +Чтобы протестировать его на нескольких примерах, мы просто вызываем его на списке примеров из нашего токинезированного обучающего набора: + +```py +batch = data_collator([tokenized_datasets["train"][i] for i in range(1, 3)]) +batch.keys() +``` + +```python out +dict_keys(['attention_mask', 'input_ids', 'labels', 'decoder_input_ids']) +``` + +Мы можем проверить, что наши метки были дополнены до максимальной длины батча, с использованием `-100`: + +```py +batch["labels"] +``` + +```python out +tensor([[ 577, 5891, 2, 3184, 16, 2542, 5, 1710, 0, -100, + -100, -100, -100, -100, -100, -100], + [ 1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817, + 550, 7032, 5821, 7907, 12649, 0]]) +``` + +Также мы можем взглянуть на идентификаторы входов декодера и убедиться, что они являются сдвинутыми версиями меток: + +```py +batch["decoder_input_ids"] +``` + +```python out +tensor([[59513, 577, 5891, 2, 3184, 16, 2542, 5, 1710, 0, + 59513, 59513, 59513, 59513, 59513, 59513], + [59513, 1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, + 817, 550, 7032, 5821, 7907, 12649]]) +``` + +Вот метки для первого и второго элементов в нашем датасете: + +```py +for i in range(1, 3): + print(tokenized_datasets["train"][i]["labels"]) +``` + +```python out +[577, 5891, 2, 3184, 16, 2542, 5, 1710, 0] +[1211, 3, 49, 9409, 1211, 3, 29140, 817, 3124, 817, 550, 7032, 5821, 7907, 12649, 0] +``` + +{#if fw === 'pt'} + +Мы передадим этот `data_collator` в `Seq2SeqTrainer`. Далее давайте рассмотрим метрику. + +{:else} + +Теперь мы можем использовать этот `data_collator` для преобразования каждого из наших датасетов в `tf.data.Dataset`, готовый к обучению: + +```python +tf_train_dataset = model.prepare_tf_dataset( + tokenized_datasets["train"], + collate_fn=data_collator, + shuffle=True, + batch_size=32, +) +tf_eval_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=data_collator, + shuffle=False, + batch_size=16, +) +``` + +{/if} + + +### Метрики[[metrics]] + + + +{#if fw === 'pt'} + +Свойство, которое `Seq2SeqTrainer` добавляет к своему суперклассу `Trainer`, - это возможность использовать метод `generate()` во время оценки или предсказания. Во время обучения модель будет использовать `decoder_input_ids` с маской внимания, гарантирующей, что она не будет использовать токены после токена, который пытается предсказать, чтобы ускорить обучение. Во время инференса мы не сможем использовать эти данные, так как у нас не будет меток, поэтому было бы неплохо оценить нашу модель с аналогичной настройкой. + +Как мы видели в [Главе 1](/course/chapter1/6), декодер выполняет инференс, предсказывая токены один за другим - то, что за кулисами реализовано в 🤗 Transformers методом `generate()`. Тренер `Seq2SeqTrainer` позволит нам использовать этот метод для оценки, если мы установим `predict_with_generate=True`. + +{/if} + +Традиционной метрикой, используемой для перевода, является [BLEU score](https://en.wikipedia.org/wiki/BLEU), представленная в [статье 2002 года](https://aclanthology.org/P02-1040.pdf) Кишором Папинени и др. BLEU score оценивает, насколько близки переводы к своим меткам. Он не измеряет разборчивость или грамматическую правильность сгенерированных моделью результатов, но использует статистические правила, чтобы гарантировать, что все слова в сгенерированных результатах также встречаются в целевых. Кроме того, существуют правила, наказывающие повторы одних и тех же слов, если они не повторяются в целевых словах (чтобы модель не выводила предложения типа `"the the the the the the the"), и вывод предложений, которые короче, чем целевые (чтобы модель не выводила предложения типа `"the"`). + +Один из недостатков BLEU заключается в том, что она предполагает, что текст уже прошел токенизацию, что затрудняет сравнение оценок между моделями, использующими различные токенизаторы. Поэтому сегодня для сравнения моделей перевода чаще всего используется метрика [SacreBLEU](https://github.com/mjpost/sacrebleu), которая устраняет этот недостаток (и другие) путем стандартизации этапа токенизации. Чтобы использовать эту метрику, сначала нужно установить библиотеку SacreBLEU: + +```py +!pip install sacrebleu +``` + +Затем мы можем загрузить ее с помощью `evaluate.load()`, как мы это делали в [Главе 3](/course/chapter3): + +```py +import evaluate + +metric = evaluate.load("sacrebleu") +``` + +Эта метрика принимает тексты в качестве входных и целевых данных. Она рассчитана на использование нескольких приемлемых целей, так как часто существует несколько приемлемых переводов одного и того же предложения - в используемом нами датасете есть только один, но в NLP нередко встречаются датасеты, дающие несколько предложений в качестве меток. Таким образом, прогнозы должны представлять собой список предложений, а ссылки - список списков предложений. + +Попробуем рассмотреть пример: + +```py +predictions = [ + "This plugin lets you translate web pages between several languages automatically." +] +references = [ + [ + "This plugin allows you to automatically translate web pages between several languages." + ] +] +metric.compute(predictions=predictions, references=references) +``` + +```python out +{'score': 46.750469682990165, + 'counts': [11, 6, 4, 3], + 'totals': [12, 11, 10, 9], + 'precisions': [91.67, 54.54, 40.0, 33.33], + 'bp': 0.9200444146293233, + 'sys_len': 12, + 'ref_len': 13} +``` + +Это дает оценку BLEU 46,75, что довольно хорошо - для сравнения, оригинальная модель Transformer в статье ["Attention Is All You Need" paper](https://arxiv.org/pdf/1706.03762.pdf) достигла оценки BLEU 41,8 на аналогичной задаче перевода с английского на французский! (Более подробную информацию об отдельных метриках, таких как `counts` и `bp`, можно найти в репозитории [SacreBLEU](https://github.com/mjpost/sacrebleu/blob/078c440168c6adc89ba75fe6d63f0d922d42bcfe/sacrebleu/metrics/bleu.py#L74)). С другой стороны, если мы попробуем использовать два плохих типа предсказаний (много повторов или слишком короткие), которые часто получаются в моделях перевода, мы получим довольно плохие оценки BLEU: + +```py +predictions = ["This This This This"] +references = [ + [ + "This plugin allows you to automatically translate web pages between several languages." + ] +] +metric.compute(predictions=predictions, references=references) +``` + +```python out +{'score': 1.683602693167689, + 'counts': [1, 0, 0, 0], + 'totals': [4, 3, 2, 1], + 'precisions': [25.0, 16.67, 12.5, 12.5], + 'bp': 0.10539922456186433, + 'sys_len': 4, + 'ref_len': 13} +``` + +```py +predictions = ["This plugin"] +references = [ + [ + "This plugin allows you to automatically translate web pages between several languages." + ] +] +metric.compute(predictions=predictions, references=references) +``` + +```python out +{'score': 0.0, + 'counts': [2, 1, 0, 0], + 'totals': [2, 1, 0, 0], + 'precisions': [100.0, 100.0, 0.0, 0.0], + 'bp': 0.004086771438464067, + 'sys_len': 2, + 'ref_len': 13} +``` + +Оценка может варьироваться от 0 до 100, причем чем больше, тем лучше. + +{#if fw === 'tf'} + +Чтобы получить из результатов модели тексты, которые может использовать метрика, мы воспользуемся методом `tokenizer.batch_decode()`. Нам нужно только очистить все знаки `-100` в метках; токенизатор автоматически сделает то же самое для дополняющего токена. Определим функцию, которая берет нашу модель и датасет и вычисляет на нем метрики. Мы также используем трюк, который значительно повышает производительность, - компиляцию нашего кода генерации с помощью [XLA](https://www.tensorflow.org/xla), ускоренного компилятора линейной алгебры TensorFlow. XLA применяет различные оптимизации к графу вычислений модели, что приводит к значительному увеличению скорости и использования памяти. Как описано в блоге Hugging Face [blog](https://huggingface.co/blog/tf-xla-generate), XLA лучше всего работает, когда наши входные формы не слишком сильно варьируются. Чтобы справиться с этим, мы разделим наши входные данные на части, кратные 128, и создадим новый датасет с коллатором с дополнением, а затем применим декоратор `@tf.function(jit_compile=True)` к нашей функции генерации, который обозначит всю функцию для компиляции с помощью XLA. + +```py +import numpy as np +import tensorflow as tf +from tqdm import tqdm + +generation_data_collator = DataCollatorForSeq2Seq( + tokenizer, model=model, return_tensors="tf", pad_to_multiple_of=128 +) + +tf_generate_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=generation_data_collator, + shuffle=False, + batch_size=8, +) + + +@tf.function(jit_compile=True) +def generate_with_xla(batch): + return model.generate( + input_ids=batch["input_ids"], + attention_mask=batch["attention_mask"], + max_new_tokens=128, + ) + + +def compute_metrics(): + all_preds = [] + all_labels = [] + + for batch, labels in tqdm(tf_generate_dataset): + predictions = generate_with_xla(batch) + decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) + labels = labels.numpy() + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + decoded_preds = [pred.strip() for pred in decoded_preds] + decoded_labels = [[label.strip()] for label in decoded_labels] + all_preds.extend(decoded_preds) + all_labels.extend(decoded_labels) + + result = metric.compute(predictions=all_preds, references=all_labels) + return {"bleu": result["score"]} +``` + +{:else} + +Чтобы получить из результатов модели тексты, которые может использовать метрика, мы воспользуемся методом `tokenizer.batch_decode()`. Нам просто нужно очистить все значения `-100` в метках (токенизатор автоматически сделает то же самое для дополняющего токена): + +```py +import numpy as np + + +def compute_metrics(eval_preds): + preds, labels = eval_preds + # В случае, если модель возвращает больше, чем предсказанные логиты + if isinstance(preds, tuple): + preds = preds[0] + + decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True) + + # Заменяем -100 в метках, так как мы не можем их декодировать + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + + # Немного простой постобработки + decoded_preds = [pred.strip() for pred in decoded_preds] + decoded_labels = [[label.strip()] for label in decoded_labels] + + result = metric.compute(predictions=decoded_preds, references=decoded_labels) + return {"bleu": result["score"]} +``` + +{/if} + +Теперь, когда все готово, мы готовы дообучить нашу модель! + + +### Дообучение модели[[fine-tuning-the-model]] + +Первый шаг - войти в Hugging Face, чтобы загрузить результаты в Model Hub. В блокноте есть удобная функция, которая поможет вам в этом: + +```python +from huggingface_hub import notebook_login + +notebook_login() +``` + +Появится виджет, в котором вы можете ввести свои учетные данные для входа в Hugging Face. + +Если вы работаете не в блокноте, просто введите следующую строку в терминале: + +```bash +huggingface-cli login +``` + +{#if fw === 'tf'} + +Прежде чем начать, давайте посмотрим, какие результаты мы получим от нашей модели без какого-либо обучения: + +```py +print(compute_metrics()) +``` + +``` +{'bleu': 33.26983701454733} +``` + +Как только это будет сделано, мы сможем подготовить все необходимое для компиляции и обучения нашей модели. Обратите внимание на использование `tf.keras.mixed_precision.set_global_policy("mixed_float16")` - это укажет Keras обучать с использованием float16, что может дать значительное ускорение на GPU, поддерживающих эту функцию (Nvidia 20xx/V100 или новее). + +```python +from transformers import create_optimizer +from transformers.keras_callbacks import PushToHubCallback +import tensorflow as tf + +# Количество шагов обучения - это количество примеров в датасете, разделенное на размер батча, затем умноженное +# на общее количество эпох. Обратите внимание, что tf_train_dataset здесь - это батч tf.data.Dataset, +# а не оригинальный датасет Hugging Face, поэтому его len() уже равен num_samples // batch_size. +num_epochs = 3 +num_train_steps = len(tf_train_dataset) * num_epochs + +optimizer, schedule = create_optimizer( + init_lr=5e-5, + num_warmup_steps=0, + num_train_steps=num_train_steps, + weight_decay_rate=0.01, +) +model.compile(optimizer=optimizer) + +# Train in mixed-precision float16 +tf.keras.mixed_precision.set_global_policy("mixed_float16") +``` + +Далее мы определяем обратный вызов `PushToHubCallback` для загрузки нашей модели в Hub во время обучения, как мы видели в [разделе 2]((/course/chapter7/2)), а затем мы просто подгоняем модель с помощью этого обратного вызова: + +```python +from transformers.keras_callbacks import PushToHubCallback + +callback = PushToHubCallback( + output_dir="marian-finetuned-kde4-en-to-fr", tokenizer=tokenizer +) + +model.fit( + tf_train_dataset, + validation_data=tf_eval_dataset, + callbacks=[callback], + epochs=num_epochs, +) +``` + +Обратите внимание, что с помощью аргумента `hub_model_id` можно указать имя репозитория, в который вы хотите отправить модель (в частности, этот аргумент нужно использовать, чтобы отправить модель в организацию). Например, когда мы отправили модель в организацию [`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/marian-finetuned-kde4-en-to-fr"` в `Seq2SeqTrainingArguments`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, поэтому здесь это будет `"sgugger/marian-finetuned-kde4-en-to-fr"` (это модель, на которую мы ссылались в начале этого раздела). + + + +💡 Если выходной каталог, который вы используете, уже существует, он должен быть локальным клоном репозитория, в который вы хотите выполнить push. Если это не так, вы получите ошибку при вызове `model.fit()` и должны будете задать новое имя. + + + +Наконец, давайте посмотрим, как выглядят наши метрики после завершения обучения: + +```py +print(compute_metrics()) +``` + +``` +{'bleu': 57.334066271545865} +``` + +На этом этапе вы можете использовать виджет инференса на Model Hub, чтобы протестировать свою модель и поделиться ею с друзьями. Вы успешно дообучили модель для задачи перевода - поздравляем! + +{:else} + +Когда это сделано, мы можем определить наши `Seq2SeqTrainingArguments`. Как и в случае с `Trainer`, мы используем подкласс `TrainingArguments`, который содержит еще несколько дополнительных полей: + +```python +from transformers import Seq2SeqTrainingArguments + +args = Seq2SeqTrainingArguments( + f"marian-finetuned-kde4-en-to-fr", + evaluation_strategy="no", + save_strategy="epoch", + learning_rate=2e-5, + per_device_train_batch_size=32, + per_device_eval_batch_size=64, + weight_decay=0.01, + save_total_limit=3, + num_train_epochs=3, + predict_with_generate=True, + fp16=True, + push_to_hub=True, +) +``` + +Помимо обычных гиперпараметров (таких как скорость обучения, количество эпох, размер батча и некоторое затухание веса), здесь есть несколько отличий по сравнению с тем, что мы видели в предыдущих разделах: + +- Мы не задаем никаких регулярных оценок, так как оценка занимает много времени; мы просто оценим нашу модель один раз до обучения и после. +- Мы установили `fp16=True`, что ускоряет обучение на современных GPU. +- Мы устанавливаем `predict_with_generate=True`, как обсуждалось выше. +- Мы используем `push_to_hub=True` для загрузки модели в Hub в конце каждой эпохи. + +Обратите внимание, что в аргументе `hub_model_id` можно указать полное имя розитория, в который вы хотите отправить модель (в частности, этот аргумент нужно использовать, чтобы отправить модель в организацию). Например, когда мы отправили модель в организацию [`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/marian-finetuned-kde4-en-to-fr"` в `Seq2SeqTrainingArguments`. По умолчанию используемый розиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, поэтому в нашем случае это будет `"sgugger/marian-finetuned-kde4-en-to-fr"` (это модель, на которую мы ссылались в начале этого раздела). + + + +💡 Если выходной каталог, который вы используете, уже существует, он должен быть локальным клоном того розитория, в который вы хотите выполнить push. Если это не так, вы получите ошибку при определении вашего `Seq2SeqTrainer` и должны будете задать новое имя. + + + + +Наконец, мы просто передаем все в `Seq2SeqTrainer`: + +```python +from transformers import Seq2SeqTrainer + +trainer = Seq2SeqTrainer( + model, + args, + train_dataset=tokenized_datasets["train"], + eval_dataset=tokenized_datasets["validation"], + data_collator=data_collator, + tokenizer=tokenizer, + compute_metrics=compute_metrics, +) +``` + +Перед обучением мы сначала посмотрим, какую оценку получила наша модель, чтобы проверить, не ухудшаем ли мы результаты, дообучив ее. Выполнение этой команды займет некоторое время, поэтому во время ее выполнения можно выпить кофе: + +```python +trainer.evaluate(max_length=max_length) +``` + +```python out +{'eval_loss': 1.6964408159255981, + 'eval_bleu': 39.26865061007616, + 'eval_runtime': 965.8884, + 'eval_samples_per_second': 21.76, + 'eval_steps_per_second': 0.341} +``` + +Оценка BLEU в 39 баллов не так уж плохо, что говорит о том, что наша модель уже хорошо справляется с переводом английских предложений на французский. + +Далее следует обучение, которое также займет некоторое время: + +```python +trainer.train() +``` + +Обратите внимание, что во время обучения каждый раз, когда модель сохраняется (здесь - каждую эпоху), она загружается на Hub в фоновом режиме. Таким образом, при необходимости вы сможете возобновить обучение на другой машине. + +После завершения обучения мы снова оцениваем нашу модель - надеемся, мы увидим некоторое улучшение в показателе BLEU! + +```py +trainer.evaluate(max_length=max_length) +``` + +```python out +{'eval_loss': 0.8558505773544312, + 'eval_bleu': 52.94161337775576, + 'eval_runtime': 714.2576, + 'eval_samples_per_second': 29.426, + 'eval_steps_per_second': 0.461, + 'epoch': 3.0} +``` + +Это улучшение почти на 14 пунктов, что очень хорошо. + +Наконец, мы используем метод `push_to_hub()`, чтобы убедиться, что загружена последняя версия модели. Тренер также создает карту модели со всеми результатами оценки и загружает ее. Эта карта модели содержит метаданные, которые помогают хабу моделей выбрать виджет для демонстрации инференса. Обычно ничего не нужно указывать, так как он сам определяет нужный виджет по классу модели, но в данном случае один и тот же класс модели может быть использован для всех видов задач, связанных с последовательностями, поэтому мы указываем, что это модель перевода: + +```py +trainer.push_to_hub(tags="translation", commit_message="Training complete") +``` + +Эта команда возвращает URL-адрес только что выполненного коммита, если вы хотите его просмотреть: + +```python out +'https://huggingface.co/sgugger/marian-finetuned-kde4-en-to-fr/commit/3601d621e3baae2bc63d3311452535f8f58f6ef3' +``` + +На этом этапе вы можете использовать виджет инференса на Model Hub, чтобы протестировать свою модель и поделиться ею с друзьями. Вы успешно дообучили модель для задачи перевода - поздравляем! + +Если вы хотите более глубоко погрузиться в цикл обучения, мы покажем вам, как сделать то же самое с помощью 🤗 Accelerate. + +{/if} + +{#if fw === 'pt'} + +## Индивидуальный цикл обучения[[a-custom-training-loop]] + +Теперь давайте рассмотрим полный цикл обучения, чтобы вы могли легко настроить нужные вам части. Он будет выглядеть примерно так же, как мы делали это в [Главе 2](/course/chapter7/2) и [Главе 3](/course/chapter3/4). + +### Подготовка всего к обучению[[preparing-everything-for-training]] + +Вы уже видели все это несколько раз, поэтому мы пройдемся по коду довольно быстро. Сначала мы создадим `DataLoader` из наших датасетов, после чего установим для датасетов формат `"torch"`, чтобы получить тензоры PyTorch: + +```py +from torch.utils.data import DataLoader + +tokenized_datasets.set_format("torch") +train_dataloader = DataLoader( + tokenized_datasets["train"], + shuffle=True, + collate_fn=data_collator, + batch_size=8, +) +eval_dataloader = DataLoader( + tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8 +) +``` + +Затем мы повторно инстанцируем нашу модель, чтобы убедиться, что мы не продолжаем дообучение предыдущей модели, а начинаем с предварительно обученной модели: + +```py +model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint) +``` + +Тогда нам понадобится оптимизатор: + +```py +from transformers import AdamW + +optimizer = AdamW(model.parameters(), lr=2e-5) +``` + +Когда у нас есть все эти объекты, мы можем отправить их в метод `accelerator.prepare()`. Помните, что если вы хотите обучаться на TPU в блокноте Colab, вам нужно будет перенести весь этот код в функцию обучения, которая не должна выполнять ни одной ячейки, инстанцирующей `Accelerator`. + +```py +from accelerate import Accelerator + +accelerator = Accelerator() +model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader +) +``` + +Теперь, когда мы отправили наш `train_dataloader` в `accelerator.prepare()`, мы можем использовать его длину для вычисления количества шагов обучения. Помните, что это всегда нужно делать после подготовки загрузчика данных, так как этот метод изменит длину `DataLoader`. Мы используем классический линейный планировшик скорости обучения до 0: + +```py +from transformers import get_scheduler + +num_train_epochs = 3 +num_update_steps_per_epoch = len(train_dataloader) +num_training_steps = num_train_epochs * num_update_steps_per_epoch + +lr_scheduler = get_scheduler( + "linear", + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=num_training_steps, +) +``` + +Наконец, чтобы отправить нашу модель в Hub, нам нужно создать объект `Repository` в рабочей папке. Сначала войдите в Hub Hugging Face, если вы еще не авторизованы. Мы определим имя розитория по идентификатору модели, который мы хотим присвоить нашей модели (не стесняйтесь заменить `repo_name` на свой собственный выбор; оно просто должно содержать ваше имя пользователя, что и делает функция `get_full_repo_name()`): + +```py +from huggingface_hub import Repository, get_full_repo_name + +model_name = "marian-finetuned-kde4-en-to-fr-accelerate" +repo_name = get_full_repo_name(model_name) +repo_name +``` + +```python out +'sgugger/marian-finetuned-kde4-en-to-fr-accelerate' +``` + +Затем мы можем клонировать этот розиторий в локальную папку. Если она уже существует, эта локальная папка должна быть клоном того розитория, с которым мы работаем: + +```py +output_dir = "marian-finetuned-kde4-en-to-fr-accelerate" +repo = Repository(output_dir, clone_from=repo_name) +``` + +Теперь мы можем загрузить все, что сохранили в `output_dir`, вызвав метод `repo.push_to_hub()`. Это поможет нам загружать промежуточные модели в конце каждой эпохи. + +### Цикл обучения[[training-loop]] + +Теперь мы готовы написать полный цикл обучения. Чтобы упростить его оценочную часть, мы определяем эту функцию `postprocess()`, которая принимает прогнозы и метки и преобразует их в списки строк, которые будет ожидать наш объект `metric`: + +```py +def postprocess(predictions, labels): + predictions = predictions.cpu().numpy() + labels = labels.cpu().numpy() + + decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) + + # Заменим -100 в метках, так как мы не можем их декодировать. + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + + # Немного простой постобработки + decoded_preds = [pred.strip() for pred in decoded_preds] + decoded_labels = [[label.strip()] for label in decoded_labels] + return decoded_preds, decoded_labels +``` + +Цикл обучения очень похож на циклы в [разделе 2](/course/chapter7/2) и [главе 3](/course/chapter3), с некоторыми отличиями в части оценки - так что давайте сосредоточимся на этом! + +Первое, что следует отметить, это то, что мы используем метод `generate()` для вычисления прогнозов, но это метод нашей базовой модели, а не обернутой модели 🤗 Accelerate, созданной в методе `prepare()`. Поэтому мы сначала развернем модель, а затем вызовем этот метод. + +Второй момент заключается в том, что, как и в случае с [классификацией токенов](/course/chapter7/2), два процесса могут дополнить входы и метки до разных форм, поэтому мы используем `accelerator.pad_across_processes()`, чтобы сделать прогнозы и метки одинаковыми по форме перед вызовом метода `gather()`. Если мы этого не сделаем, оценка либо выдаст ошибку, либо зависнет навсегда. + +```py +from tqdm.auto import tqdm +import torch + +progress_bar = tqdm(range(num_training_steps)) + +for epoch in range(num_train_epochs): + # Обучение + model.train() + for batch in train_dataloader: + outputs = model(**batch) + loss = outputs.loss + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + progress_bar.update(1) + + # Оценка + model.eval() + for batch in tqdm(eval_dataloader): + with torch.no_grad(): + generated_tokens = accelerator.unwrap_model(model).generate( + batch["input_ids"], + attention_mask=batch["attention_mask"], + max_length=128, + ) + labels = batch["labels"] + + ## Необходимо дополнить прогнозы и метки + generated_tokens = accelerator.pad_across_processes( + generated_tokens, dim=1, pad_index=tokenizer.pad_token_id + ) + labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100) + + predictions_gathered = accelerator.gather(generated_tokens) + labels_gathered = accelerator.gather(labels) + + decoded_preds, decoded_labels = postprocess(predictions_gathered, labels_gathered) + metric.add_batch(predictions=decoded_preds, references=decoded_labels) + + results = metric.compute() + print(f"epoch {epoch}, BLEU score: {results['score']:.2f}") + + # Сохранение и загрузка + accelerator.wait_for_everyone() + unwrapped_model = accelerator.unwrap_model(model) + unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save) + if accelerator.is_main_process: + tokenizer.save_pretrained(output_dir) + repo.push_to_hub( + commit_message=f"Training in progress epoch {epoch}", blocking=False + ) +``` + +```python out +epoch 0, BLEU score: 53.47 +epoch 1, BLEU score: 54.24 +epoch 2, BLEU score: 54.44 +``` + +После этого у вас должна получиться модель, результаты которой будут очень похожи на модель, обученную с помощью `Seq2SeqTrainer`. Вы можете проверить модель, которую мы обучили с помощью этого кода, на [*huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate*](https://huggingface.co/huggingface-course/marian-finetuned-kde4-en-to-fr-accelerate). А если вы хотите протестировать какие-либо изменения в цикле обучения, вы можете реализовать их напрямую, отредактировав код, показанный выше! + +{/if} + +## Использование дообученной модели[[using-the-fine-tuned-model]] + +Мы уже показали вам, как можно использовать модель, которую мы дообучили на Model Hub, с помощью виджета инференса. Чтобы использовать ее локально в `pipeline`, нам просто нужно указать соответствующий идентификатор модели: + +```py +from transformers import pipeline + +# Замените это на свою собственную контрольную точку +model_checkpoint = "huggingface-course/marian-finetuned-kde4-en-to-fr" +translator = pipeline("translation", model=model_checkpoint) +translator("Default to expanded threads") +``` + +```python out +[{'translation_text': 'Par défaut, développer les fils de discussion'}] +``` + +Как и ожидалось, наша предварительно обученная модель адаптировала свои знания к корпусу, на котором мы ее дообучили, и вместо того, чтобы оставить английское слово "threads" в покое, она теперь переводит его на официальный французский вариант. То же самое относится и к слову " plugin ": + +```py +translator( + "Unable to import %1 using the OFX importer plugin. This file is not the correct format." +) +``` + +```python out +[{'translation_text': "Impossible d'importer %1 en utilisant le module externe d'importation OFX. Ce fichier n'est pas le bon format."}] +``` + +Еще один отличный пример доменной адаптации! + + + +✏️ **Попробуйте!** Что возвращает модель для примера со словом "email", который вы определили ранее? + + diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx new file mode 100644 index 000000000..fbe935615 --- /dev/null +++ b/chapters/ru/chapter7/5.mdx @@ -0,0 +1,1073 @@ + + +# Суммаризация[[summarization]] + +{#if fw === 'pt'} + + + +{:else} + + + +{/if} + + +В этом разделе мы рассмотрим, как модели Transformer могут быть использованы для сжатия длинных документов в краткое изложение - задача, известная как _суммаризация текста (text summarization)_. Это одна из самых сложных задач NLP, поскольку она требует целого ряда способностей, таких как понимание длинных отрывков и генерация связного текста, отражающего основные темы документа. Однако при правильном подходе суммаризация текста - это мощный инструмент, который может ускорить различные бизнес-процессы, избавив экспертов домена от необходимости детального прочтения длинных документов. + + + +Хотя на [Hugging Face Hub](https://huggingface.co/models?pipeline_tag=summarization&sort=downloads) уже существуют различные дообученные модели для суммаризации, почти все они подходят только для англоязычных документов. Поэтому, чтобы добавить изюминку в этот раздел, мы обучим двухязыковую модель для английского и испанского языков. К концу этого раздела у вас будет [модель](https://huggingface.co/huggingface-course/mt5-small-finetuned-amazon-en-es), способная к суммаризации отзывов покупателей, как показано здесь: + + + +Как мы увидим, эти резюме кратки, поскольку они составляются на основе названий, которые покупатели указывают в своих отзывах о товаре. Для начала давайте соберем подходящий двуязыковой корпус для этой задачи. + +## Подготовка многоязыкового корпуса[[preparing-a-multilingual-corpus]] + +Для создания нашей двуязыковой суммаризации мы будем использовать [Multilingual Amazon Reviews Corpus](https://huggingface.co/datasets/amazon_reviews_multi). Этот корпус состоит из отзывов о товарах Amazon на шести языках и обычно используется для тестирования многоязыковых классификаторов. Однако, поскольку каждый отзыв сопровождается коротким заголовком, мы можем использовать заголовки в качестве целевых резюме для обучения нашей модели! Чтобы начать работу, давайте загрузим английские и испанские подмножества из Hugging Face Hub: + +```python +from datasets import load_dataset + +spanish_dataset = load_dataset("amazon_reviews_multi", "es") +english_dataset = load_dataset("amazon_reviews_multi", "en") +english_dataset +``` + +```python out +DatasetDict({ + train: Dataset({ + features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'], + num_rows: 200000 + }) + validation: Dataset({ + features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'], + num_rows: 5000 + }) + test: Dataset({ + features: ['review_id', 'product_id', 'reviewer_id', 'stars', 'review_body', 'review_title', 'language', 'product_category'], + num_rows: 5000 + }) +}) +``` + +Как видите, для каждого языка есть 200 000 отзывов в части `train` и по 5 000 отзывов в частях `validation` и `test`. Интересующая нас информация о рецензиях содержится в столбцах `review_body` и `review_title`. Давайте рассмотрим несколько примеров, создав простую функцию, которая берет случайную выборку из обучающего множества с помощью методов, изученных в [Главе 5](/course/chapter5): + +```python +def show_samples(dataset, num_samples=3, seed=42): + sample = dataset["train"].shuffle(seed=seed).select(range(num_samples)) + for example in sample: + print(f"\n'>> Title: {example['review_title']}'") + print(f"'>> Review: {example['review_body']}'") + + +show_samples(english_dataset) +``` + +```python out +'>> Title: Worked in front position, not rear' +'>> Review: 3 stars because these are not rear brakes as stated in the item description. At least the mount adapter only worked on the front fork of the bike that I got it for.' + +'>> Title: meh' +'>> Review: Does it’s job and it’s gorgeous but mine is falling apart, I had to basically put it together again with hot glue' + +'>> Title: Can\'t beat these for the money' +'>> Review: Bought this for handling miscellaneous aircraft parts and hanger "stuff" that I needed to organize; it really fit the bill. The unit arrived quickly, was well packaged and arrived intact (always a good sign). There are five wall mounts-- three on the top and two on the bottom. I wanted to mount it on the wall, so all I had to do was to remove the top two layers of plastic drawers, as well as the bottom corner drawers, place it when I wanted and mark it; I then used some of the new plastic screw in wall anchors (the 50 pound variety) and it easily mounted to the wall. Some have remarked that they wanted dividers for the drawers, and that they made those. Good idea. My application was that I needed something that I can see the contents at about eye level, so I wanted the fuller-sized drawers. I also like that these are the new plastic that doesn\'t get brittle and split like my older plastic drawers did. I like the all-plastic construction. It\'s heavy duty enough to hold metal parts, but being made of plastic it\'s not as heavy as a metal frame, so you can easily mount it to the wall and still load it up with heavy stuff, or light stuff. No problem there. For the money, you can\'t beat it. Best one of these I\'ve bought to date-- and I\'ve been using some version of these for over forty years.' +``` + + + +✏️ **Попробуйте!** Измените random seed в команде `Dataset.shuffle()`, чтобы изучить другие отзывы в корпусе. Если вы владеете испанским языком, посмотрите на некоторые отзывы в `spanish_dataset`, чтобы понять, похожи ли их названия на разумные резюме. + + + +Эта выборка демонстрирует разнообразие отзывов, которые обычно можно найти в сети, - от положительных до отрицательных (и все, что между ними!). Хотя пример с названием "meh" не очень информативен, остальные названия выглядят как достойные резюме самих отзывов. Обучение модели суммаризации всех 400 000 отзывов заняло бы слишком много времени на одном GPU, поэтому вместо этого мы сосредоточимся на создании резюме для одного домена продуктов. Чтобы получить представление о том, какие домены мы можем выбрать, давайте преобразуем `english_dataset` в `pandas.DataFrame` и вычислим количество отзывов по каждой категории товаров: + +```python +english_dataset.set_format("pandas") +english_df = english_dataset["train"][:] +# Show counts for top 20 products +english_df["product_category"].value_counts()[:20] +``` + +```python out +home 17679 +apparel 15951 +wireless 15717 +other 13418 +beauty 12091 +drugstore 11730 +kitchen 10382 +toy 8745 +sports 8277 +automotive 7506 +lawn_and_garden 7327 +home_improvement 7136 +pet_products 7082 +digital_ebook_purchase 6749 +pc 6401 +electronics 6186 +office_product 5521 +shoes 5197 +grocery 4730 +book 3756 +Name: product_category, dtype: int64 +``` + +Самые популярные товары в английском датасете - это бытовые товары, одежда и беспроводная электроника. Однако, чтобы поддержать тему Amazon, давайте сосредоточимся на суммаризации отзывов о книгах - в конце концов, именно для этого компания и была основана! Мы видим две категории товаров, которые подходят для этой цели (`book` и `digital_ebook_purchase`), поэтому давайте отфильтруем датасеты на обоих языках только для этих товаров. Как мы видели в [Главе 5](/course/chapter5), функция `Dataset.filter()` позволяет нам очень эффективно разделять датасет на части, поэтому мы можем определить простую функцию для этого: + +```python +def filter_books(example): + return ( + example["product_category"] == "book" + or example["product_category"] == "digital_ebook_purchase" + ) +``` + +Теперь, когда мы применим эту функцию к `english_dataset` и `spanish_dataset`, результат будет содержать только те строки, в которых присутствуют категории книг. Прежде чем применить фильтр, давайте изменим формат `english_dataset` с `"pandas"` обратно на `"arrow"`: + +```python +english_dataset.reset_format() +``` + +Затем мы можем применить функцию фильтрации, и в качестве проверки работоспособности давайте посмотрим на выборку отзывов, чтобы убедиться, что они действительно посвящены книгам: + +```python +spanish_books = spanish_dataset.filter(filter_books) +english_books = english_dataset.filter(filter_books) +show_samples(english_books) +``` + +```python out +'>> Title: I\'m dissapointed.' +'>> Review: I guess I had higher expectations for this book from the reviews. I really thought I\'d at least like it. The plot idea was great. I loved Ash but, it just didnt go anywhere. Most of the book was about their radio show and talking to callers. I wanted the author to dig deeper so we could really get to know the characters. All we know about Grace is that she is attractive looking, Latino and is kind of a brat. I\'m dissapointed.' + +'>> Title: Good art, good price, poor design' +'>> Review: I had gotten the DC Vintage calendar the past two years, but it was on backorder forever this year and I saw they had shrunk the dimensions for no good reason. This one has good art choices but the design has the fold going through the picture, so it\'s less aesthetically pleasing, especially if you want to keep a picture to hang. For the price, a good calendar' + +'>> Title: Helpful' +'>> Review: Nearly all the tips useful and. I consider myself an intermediate to advanced user of OneNote. I would highly recommend.' +``` + +Хорошо, мы видим, что отзывы не совсем о книгах и могут относиться к таким вещам, как календари и электронные приложения, например OneNote. Тем не менее, домен кажется подходящим для обучения модели суммаризации. Прежде чем мы рассмотрим различные модели, подходящие для этой задачи, нам осталось подготовить данные: объединить английские и испанские отзывы в один объект `DatasetDict`. 🤗Datasets предоставляет удобную функцию `concatenate_datasets()`, которая (как следует из названия) стекирует два объекта `Dataset` друг на друга. Таким образом, чтобы создать двуязыковой датасет, мы пройдемся по каждой части, объединим датасеты для этой части и перемешаем результат, чтобы наша модель не была слишком переобучена для одного языка: + +```python +from datasets import concatenate_datasets, DatasetDict + +books_dataset = DatasetDict() + +for split in english_books.keys(): + books_dataset[split] = concatenate_datasets( + [english_books[split], spanish_books[split]] + ) + books_dataset[split] = books_dataset[split].shuffle(seed=42) + +# Посмотрим на несколько примеров +show_samples(books_dataset) +``` + +```python out +'>> Title: Easy to follow!!!!' +'>> Review: I loved The dash diet weight loss Solution. Never hungry. I would recommend this diet. Also the menus are well rounded. Try it. Has lots of the information need thanks.' + +'>> Title: PARCIALMENTE DAÑADO' +'>> Review: Me llegó el día que tocaba, junto a otros libros que pedí, pero la caja llegó en mal estado lo cual dañó las esquinas de los libros porque venían sin protección (forro).' + +'>> Title: no lo he podido descargar' +'>> Review: igual que el anterior' +``` + +Это определенно похоже на смесь английских и испанских обзоров! Теперь, когда у нас есть тренировочный корпус, осталось проверить распределение слов в рецензиях и их заголовках. Это особенно важно для задач суммаризации, где короткие эталонные резюме в данных могут склонять модель в сторону вывода только одного или двух слов в сгенерированных резюме. На графиках ниже показаны распределения слов, и мы видим, что названия сильно перекошены в сторону 1-2 слов: + +
+Word count distributions for the review titles and texts. + +
+ +Чтобы справиться с этой проблемой, мы отфильтруем примеры с очень короткими заголовками, чтобы наша модель могла создавать более интересные резюме. Поскольку мы имеем дело с английскими и испанскими текстами, мы можем использовать грубую эвристику для разделения названий по символам пробела, а затем использовать наш надежный метод `Dataset.filter()` следующим образом: + +```python +books_dataset = books_dataset.filter(lambda x: len(x["review_title"].split()) > 2) +``` + +Теперь, когда мы подготовили корпус, давайте рассмотрим несколько возможных моделей Transformer, которые можно было бы дообучить на его основе! + +## Модели для суммаризации текста[[models-for-text-summarization]] + +Если задуматься, то суммаризация текста - это задача, похожая на машинный перевод: у нас есть текст, например рецензия, который мы хотели бы "перевести" в более короткую версию, передающую основные особенности исходного текста. Соответственно, большинство моделей Transformer для суммаризации используют архитектуру кодер-декодер, с которой мы впервые столкнулись в [Глава 1] (/course/chapter1), хотя есть и исключения, например семейство моделей GPT, которые также могут использоваться для суммаризации в условиях few-shot настроек. В следующей таблице перечислены некоторые популярные предварительно обученные модели, которые можно дообучить для суммаризации. + +| Модель Transformer | Описание | Многоязычная? | +| :---------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------: | +| [GPT-2](https://huggingface.co/gpt2-xl) | Хотя GPT-2 обучен как авторегрессивная языковая модель, вы можете заставить его генерировать резюме, добавляя "TL;DR" в конце входного текста. | ❌ | +| [PEGASUS](https://huggingface.co/google/pegasus-large) | Использует цель предварительного обучения для предсказания замаскированных предложений в текстах с несколькими предложениями. Эта задача предварительного обучения ближе к суммаризации, чем к классическому языковому моделированию демонстрирует высокие результаты в популярных бенчмарках. + | ❌ | +| [T5](https://huggingface.co/t5-base) | Универсальная трансформерная архитектура, которая формулирует все задачи в рамках преобразования текста в текст; например, входной формат модели для суммаризации документа - `summarize: ARTICLE`. | ❌ | +| [mT5](https://huggingface.co/google/mt5-base) | Многоязыковая версия T5, предварительно обученная на многоязыковом корпусе Common Crawl (mC4), охватывающем 101 язык. | ✅ | +| [BART](https://huggingface.co/facebook/bart-base) | Новая архитектура Transformer с кодером и стеком декодеров, обученных восстанавливать поврежденный входной сигнал, сочетает в себе схемы предварительного обучения BERT и GPT-2. | ❌ | +| [mBART-50](https://huggingface.co/facebook/mbart-large-50) | Многоязыковая версия BART, предварительно обученная на 50 языках. | ✅ | + +Как видно из этой таблицы, большинство моделей Transformer для суммаризации (и вообще для большинства задач NLP) являются монолингвистическими. Это хорошо, если ваша задача на "высокоресурсном" языке, таком как английский или немецкий, но не очень хорошо для тысяч других языков, используемых по всему миру. К счастью, существует класс многоязыковых моделей Transformer, таких как mT5 и mBART, которые приходят на помощь. Эти модели предварительно обучаются с помощью языкового моделирования, но с изюминкой: вместо обучения на корпусе одного языка они обучаются совместно на текстах более чем на 50 языках одновременно! + +Мы сосредоточимся на mT5, интересной архитектуре, основанной на T5, которая была предварительно обучена на фреймворке "текс-в-текст" (text-to-text). В T5 каждая задача NLP формулируется в терминах префикса подсказки, например `summarize:`, который определяет, что модель должна адаптировать сгенерированный текст к подсказке. Как показано на рисунке ниже, это делает T5 чрезвычайно универсальным, поскольку вы можете решать множество задач с помощью одной модели! + +
+Different tasks performed by the T5 architecture. + +
+ +В mT5 не используются префиксы, но она обладает многими универсальными возможностями T5 и имеет многоязыковое преимущество. Теперь, когда мы выбрали модель, давайте посмотрим, как подготовить данные для обучения. + + + + +✏️ **Попробуйте!** После того как вы проработаете этот раздел, посмотрите, насколько хорошо mT5 сравнится с mBART, дообучив его тем же методам. Чтобы получить бонусные очки, вы также можете попробовать дообучить T5 только на английских рецензиях. Поскольку в T5 есть специальный префикс запроса, вам нужно будет добавить `summarize:` к входным примерам на следующих шагах предварительной обработки. + + + +## Предварительная обработка данных[[preprocessing-the-data]] + + + +Наша следующая задача - токенизация и кодирование отзывов и их заголовков. Как обычно, мы начинаем с загрузки токенизатора, связанного с контрольной точкой предварительно обученной модели. В качестве контрольной точки мы будем использовать `mt5-small`, чтобы можно было дообучить модель за разумное время: + +```python +from transformers import AutoTokenizer + +model_checkpoint = "google/mt5-small" +tokenizer = AutoTokenizer.from_pretrained(model_checkpoint) +``` + + + +💡 На ранних стадиях ваших NLP-проектов хорошей практикой является обучение класса "маленьких" моделей на небольшой выборке данных. Это позволит вам быстрее отлаживать и итерировать модели, чтобы создать сквозной рабочий процесс. Когда вы будете уверены в результатах, вы всегда сможете увеличить масштаб модели, просто изменив контрольную точку модели! + + + +Давайте протестируем токенизатор mT5 на небольшом примере: + +```python +inputs = tokenizer("I loved reading the Hunger Games!") +inputs +``` + +```python out +{'input_ids': [336, 259, 28387, 11807, 287, 62893, 295, 12507, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]} +``` + +Здесь мы видим знакомые нам `input_ids` и `attention_mask`, с которыми мы столкнулись в наших первых экспериментах по дообучению еще в [Главе 3] (/course/chapter3). Давайте декодируем эти входные идентификаторы с помощью функции токенизатора `convert_ids_to_tokens()`, чтобы понять, с каким токенизатором мы имеем дело: + +```python +tokenizer.convert_ids_to_tokens(inputs.input_ids) +``` + +```python out +['▁I', '▁', 'loved', '▁reading', '▁the', '▁Hung', 'er', '▁Games', ''] +``` + +Специальный символ Юникода `▁` и токен конца последовательности `` указывают на то, что мы имеем дело с токенизатором SentencePiece, который основан на алгоритме сегментации Unigram, рассмотренном в [Главе 6] (/course/chapter6). Unigram особенно полезен для многоязычных корпусов, поскольку он позволяет SentencePiece не зависеть от ударений, пунктуации и того факта, что во многих языках, например в японском, нет пробельных символов. + +Для токенизации нашего корпуса нам придется столкнуться с одной тонкостью, связанной с сумризацией: поскольку наши метки также являются текстом, возможно, что они превышают максимальный размер контекста модели. Это означает, что нам нужно применять усечение как к обзорам, так и к их заголовкам, чтобы не передавать в модель слишком длинные данные. Токенизаторы в 🤗 Transformers предоставляют интересный аргумент `text_target`, который позволяет вам токенизировать метки параллельно с входными данными. Вот пример того, как обрабатываются входные и целевые данные для mT5: + +```python +max_input_length = 512 +max_target_length = 30 + + +def preprocess_function(examples): + model_inputs = tokenizer( + examples["review_body"], + max_length=max_input_length, + truncation=True, + ) + labels = tokenizer( + examples["review_title"], max_length=max_target_length, truncation=True + ) + model_inputs["labels"] = labels["input_ids"] + return model_inputs +``` + +Давайте пройдемся по этому коду, чтобы понять, что происходит. Первое, что мы сделали, это определили значения `max_input_length` и `max_target_length`, которые устанавливают верхние границы длины наших обзоров и заголовков. Поскольку тело обзора обычно намного больше заголовка, мы соответственно изменили эти значения. + +С помощью `preprocess_function()` можно провести токенизацию всего корпуса с помощью удобной функции `Dataset.map()`, которую мы часто использовали на протяжении всего курса: + +```python +tokenized_datasets = books_dataset.map(preprocess_function, batched=True) +``` + +Теперь, когда корпус был предварительно обработан, давайте посмотрим на некоторые метрики, которые обычно используются для суммаризации. Как мы увидим, не существует серебряной пули, когда дело доходит до измерения качества сгенерированного машиной текста. + + + +💡 Возможно, вы заметили, что выше в функции `Dataset.map()` мы использовали `batched=True`. Это кодирует примеры в батчах по 1 000 (по умолчанию) и позволяет использовать возможности многопоточности быстрых токенизаторов в 🤗 Transformers. По возможности, попробуйте использовать `batched=True`, чтобы получить максимальную отдачу от препроцессинга! + + + + +## Метрики для суммаризации текста[[metrics-for-text-summarization]] + + + +По сравнению с большинством других задач, которые мы рассматривали в этом курсе, измерение качества работы задач по созданию текста, таких как суммаризация или перевод, не так просто. Например, для рецензии типа "I loved reading the Hunger Games" существует множество правильных резюме, таких как "I loved the Hunger Games" или "Hunger Games is a great read". Очевидно, что применение какого-то точного соответствия между сгенерированным резюме и меткой не является хорошим решением - даже люди не справятся с такой метрикой, потому что у каждого из нас свой стиль написания. + +Для суммаризации одной из наиболее часто используемых метрик является [оценка ROUGE](https://en.wikipedia.org/wiki/ROUGE_(metric)) (сокращение от Recall-Oriented Understudy for Gisting Evaluation). Основная идея этой метрики заключается в сравнении сгенерированного резюме с набором эталонных резюме, которые обычно создаются людьми. Чтобы сделать ее более точной, предположим, что мы хотим сравнить следующие два резюме: + +```python +generated_summary = "I absolutely loved reading the Hunger Games" +reference_summary = "I loved reading the Hunger Games" +``` + +Одним из способов их сравнения может быть подсчет количества перекрывающихся слов, которых в данном случае будет 6. Однако это несколько грубовато, поэтому вместо этого ROUGE основывается на вычислении оценок _precision_ и _recall_ для перекрытия. + + + +🙋 Не волнуйтесь, если вы впервые слышите о precision и recall - мы вместе разберем несколько наглядных примеров, чтобы все стало понятно. Эти метрики обычно встречаются в задачах классификации, поэтому, если вы хотите понять, как определяются precision и recall в этом контексте, мы рекомендуем ознакомиться с `scikit-learn` [руководством](https://scikit-learn.org/stable/auto_examples/model_selection/plot_precision_recall.html). + + + +Для ROUGE recall измеряет, насколько эталонное резюме соответствует сгенерированному. Если мы просто сравниваем слова, recall можно рассчитать по следующей формуле: + +$$ \mathrm{Recall} = \frac{\mathrm{Number\,of\,overlapping\, words}}{\mathrm{Total\, number\, of\, words\, in\, reference\, summary}} $$ + +Для нашего простого примера выше эта формула дает идеальный recall 6/6 = 1; то есть все слова в эталонном резюме были получены моделью. Это может показаться замечательным, но представьте, если бы сгенерированное нами резюме было "I really really loved reading the Hunger Games all night". Это тоже дало бы идеальный recall, но, возможно, было бы хуже, поскольку было бы многословным. Чтобы справиться с этими сценариями, мы также вычисляем precision, которая в контексте ROUGE измеряет, насколько сгенерированное резюме было релевантным: + +$$ \mathrm{Precision} = \frac{\mathrm{Number\,of\,overlapping\, words}}{\mathrm{Total\, number\, of\, words\, in\, generated\, summary}} $$ + +Если применить это к нашему подробному резюме, то precision составит 6/10 = 0,6, что значительно хуже, чем precision 6/7 = 0,86, полученная при использовании более короткого резюме. На практике обычно вычисляют и precision, и recall, а затем F1-score (среднее гармоническое из precision и recall). Мы можем легко это сделать с помощью 🤗 Datasets, предварительно установив пакет `rouge_score`: + +```py +!pip install rouge_score +``` + +а затем загрузить метрику ROUGE следующим образом: + +```python +import evaluate + +rouge_score = evaluate.load("rouge") +``` + +Затем мы можем использовать функцию `rouge_score.compute()`, чтобы рассчитать все метрики сразу: + +```python +scores = rouge_score.compute( + predictions=[generated_summary], references=[reference_summary] +) +scores +``` + +```python out +{'rouge1': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)), + 'rouge2': AggregateScore(low=Score(precision=0.67, recall=0.8, fmeasure=0.73), mid=Score(precision=0.67, recall=0.8, fmeasure=0.73), high=Score(precision=0.67, recall=0.8, fmeasure=0.73)), + 'rougeL': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92)), + 'rougeLsum': AggregateScore(low=Score(precision=0.86, recall=1.0, fmeasure=0.92), mid=Score(precision=0.86, recall=1.0, fmeasure=0.92), high=Score(precision=0.86, recall=1.0, fmeasure=0.92))} +``` + +Ого, в этом выводе много информации - что же она означает? Во-первых, 🤗 Datasets действительно вычисляет доверительные интервалы для precision, recall и F1-score; это `low`, `mid`, и `high` атрибуты, которые вы можете здесь увидеть. Кроме того, 🤗 Dataset вычисляет различные оценки ROUGE, которые основаны на различных типах детализации текста при сравнении сгенерированных и эталонных резюме. Вариант `rouge1` представляет собой перекрытие униграмм - это просто модный способ сказать о перекрытии слов, и это именно та метрика, которую мы обсуждали выше. Чтобы убедиться в этом, давайте извлечем `среднее` значение наших оценок: + +```python +scores["rouge1"].mid +``` + +```python out +Score(precision=0.86, recall=1.0, fmeasure=0.92) +``` + +Отлично, показатели precision и recall совпадают! А как насчет других показателей ROUGE? `rouge2` измеряет перекрытие биграмм (считайте, что это перекрытие пар слов), а `rougeL` и `rougeLsum` измеряют самые длинные совпадающие последовательности слов, ища самые длинные общие подстроки в сгенерированных и эталонных резюме. Слово "sum" в `rougeLsum` означает, что эта метрика вычисляется для всего резюме, в то время как `rougeL` вычисляется как среднее по отдельным предложениям. + + + +✏️ **Попробуйте!** Создайте свой собственный пример сгенерированного и эталонного резюме и посмотрите, согласуются ли полученные оценки ROUGE с ручным расчетом по формулам precision и recall. Для получения бонусных очков разбейте текст на биграммы и сравните precision и recall для метрики `rouge2`. + + + +Мы будем использовать эту оценку ROUGE для отслеживания эффективности нашей модели, но перед этим давайте сделаем то, что должен сделать каждый хороший NLP-практик: создадим сильную, но простую базовую модель! + +### Создание сильного базового уровня[[creating-a-strong-baseline]] + +Для суммаризации текста обычно берут первые три предложения статьи, что часто называют базвым уровнем _lead-3_. Мы могли бы использовать символы полной остановки для отслеживания границ предложений, но это не поможет при использовании таких аббревиатур, как " U.S." или "U.N.". -- поэтому вместо этого мы воспользуемся библиотекой `nltk`, которая включает в себя лучший алгоритм для таких случаев. Вы можете установить пакет с помощью `pip` следующим образом: + +```python +!pip install nltk +``` + +а затем скачайте правила пунктуации: + +```python +import nltk + +nltk.download("punkt") +``` + +Далее мы импортируем токенизатор предложений из `nltk` и создадим простую функцию для извлечения первых трех предложений в обзоре. При суммаризации текста принято отделять каждое предложение новой строкой, поэтому давайте включим эту функцию и протестируем ее на обучающем примере: + +```python +from nltk.tokenize import sent_tokenize + + +def three_sentence_summary(text): + return "\n".join(sent_tokenize(text)[:3]) + + +print(three_sentence_summary(books_dataset["train"][1]["review_body"])) +``` + +```python out +'I grew up reading Koontz, and years ago, I stopped,convinced i had "outgrown" him.' +'Still,when a friend was looking for something suspenseful too read, I suggested Koontz.' +'She found Strangers.' +``` + +Похоже, что это работает, так что теперь давайте реализуем функцию, которая извлекает эти "резюме" из датасета и вычисляет оценку ROUGE для базового уровня: + +```python +def evaluate_baseline(dataset, metric): + summaries = [three_sentence_summary(text) for text in dataset["review_body"]] + return metric.compute(predictions=summaries, references=dataset["review_title"]) +``` + +Затем мы можем использовать эту функцию для вычисления оценок ROUGE на валидационном множестве и немного приукрасить их с помощью Pandas: + +```python +import pandas as pd + +score = evaluate_baseline(books_dataset["validation"], rouge_score) +rouge_names = ["rouge1", "rouge2", "rougeL", "rougeLsum"] +rouge_dict = dict((rn, round(score[rn].mid.fmeasure * 100, 2)) for rn in rouge_names) +rouge_dict +``` + +```python out +{'rouge1': 16.74, 'rouge2': 8.83, 'rougeL': 15.6, 'rougeLsum': 15.96} +``` + +Мы видим, что оценка `rouge2` значительно ниже, чем у остальных; скорее всего, это отражает тот факт, что заголовки рецензий обычно лаконичны, и поэтому базовый уровень lead-3 слишком многословен. Теперь, когда у нас есть хороший базовый уровень для работы, давайте дообучим mT5! + +{#if fw === 'pt'} + +## Дообучение mT5 с API `Trainer`[[fine-tuning-mt5-with-the-trainer-api]] + +Дообучение модели суммаризации очень похоже на другие задачи, которые мы рассмотрели в этой главе. Первое, что нам нужно сделать, это загрузить предварительно обученную модель из контрольной точки `mt5-small`. Поскольку суммаризация - это задача преобразования последовательности в последовательность, мы можем загрузить модель с помощью класса `AutoModelForSeq2SeqLM`, который автоматически загрузит и кэширует веса: + +```python +from transformers import AutoModelForSeq2SeqLM + +model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint) +``` + +{:else} + +## Дообучение mT5 с Keras[[fine-tuning-mt5-with-keras]] + +Дообучение модели суммаризации очень похоже на другие задачи, которые мы рассмотрели в этой главе. Первое, что нам нужно сделать, это загрузить предварительно обученную модель из контрольной точки `mt5-small`. Поскольку суммаризация - это задача преобразования последовательности в последовательность, мы можем загрузить модель с помощью класса `TFAutoModelForSeq2SeqLM`, который автоматически загрузит и кэширует веса: + +```python +from transformers import TFAutoModelForSeq2SeqLM + +model = TFAutoModelForSeq2SeqLM.from_pretrained(model_checkpoint) +``` + +{/if} + + + +💡 Если вы задаетесь вопросом, почему вы не видите предупреждений о необходимости дообучить модель для последующей задачи, то это потому, что для задач "последовательность-в-последовательность" мы сохраняем все веса сети. Сравните это с нашей моделью классификации текста из [Главы 3](/course/chapter3), где голова предварительно обученной модели была заменена на случайно инициализированную сеть. + + + +Следующее, что нам нужно сделать, это войти в Hugging Face Hub. Если вы выполняете этот код в ноутбуке, вы можете сделать это с помощью следующей полезной функции: + +```python +from huggingface_hub import notebook_login + +notebook_login() +``` + +которая отобразит виджет, где вы можете ввести свои учетные данные. Также вы можете запустить эту команду в терминале и войти в систему там: + +``` +huggingface-cli login +``` + +{#if fw === 'pt'} + +Для вычисления оценки ROUGE в процессе обучения нам понадобится генерировать резюме. К счастью, 🤗 Transformers предоставляет специальные классы `Seq2SeqTrainingArguments` и `Seq2SeqTrainer`, которые могут сделать это за нас автоматически! Чтобы увидеть, как это работает, давайте сначала определим гиперпараметры и другие аргументы для наших экспериментов: + +```python +from transformers import Seq2SeqTrainingArguments + +batch_size = 8 +num_train_epochs = 8 +# Show the training loss with every epoch +logging_steps = len(tokenized_datasets["train"]) // batch_size +model_name = model_checkpoint.split("/")[-1] + +args = Seq2SeqTrainingArguments( + output_dir=f"{model_name}-finetuned-amazon-en-es", + evaluation_strategy="epoch", + learning_rate=5.6e-5, + per_device_train_batch_size=batch_size, + per_device_eval_batch_size=batch_size, + weight_decay=0.01, + save_total_limit=3, + num_train_epochs=num_train_epochs, + predict_with_generate=True, + logging_steps=logging_steps, + push_to_hub=True, +) +``` + +Здесь аргумент `predict_with_generate` был задан, чтобы указать, что мы должны генерировать резюме во время оценки, чтобы мы могли вычислить баллы ROUGE для каждой эпохи. Как обсуждалось в [Главе 1](/course/chapter1), декодер выполняет инференс, предсказывая токены по одному, и это реализуется методом модели `generate()`. Задание `predict_with_generate=True` указывает `Seq2SeqTrainer` на использование этого метода для оценки. Мы также скорректировали некоторые гиперпараметры по умолчанию, такие как скорость обучения, количество эпох и затухание весов, и задали параметр `save_total_limit`, чтобы сохранять только 3 контрольные точки во время обучения - это сделано потому, что даже "маленькая" версия mT5 использует около Гигабайта места на жестком диске, и мы можем сэкономить немного места, ограничив количество копий, которые мы сохраняем. + +Аргумент `push_to_hub=True` позволит нам отправить модель в Hub после обучения; вы найдете розиторий под своим профилем пользователя в месте, определенном `output_dir`. Обратите внимание, что вы можете указать имя розитория, в который хотите отправить модель, с помощью аргумента `hub_model_id` (в частности, вам нужно использовать этот аргумент, чтобы отправить модель в организацию). Например, когда мы отправили модель в организацию [`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/mt5-finetuned-amazon-en-es"` в `Seq2SeqTrainingArguments`. + +Следующее, что нам нужно сделать, это предоставить тренеру функцию `compute_metrics()`, чтобы мы могли оценить нашу модель во время обучения. Для суммаризации это немного сложнее, чем просто вызвать `rouge_score.compute()` для прогнозов модели, поскольку нам нужно _декодировать_ выводы и метки в текст, прежде чем мы сможем вычислить оценку ROUGE. Следующая функция делает именно это, а также использует функцию `sent_tokenize()` из `nltk` для разделения предложений резюме символом новой строки: + +```python +import numpy as np + + +def compute_metrics(eval_pred): + predictions, labels = eval_pred + # Декодируем сгенерированные резюме в текст + decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) + # Заменяем -100 в метках, поскольку мы не можем их декодировать + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + # Декодируем эталонные резюме в текст + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + # ROUGE ожидает символ новой строки после каждого предложения + decoded_preds = ["\n".join(sent_tokenize(pred.strip())) for pred in decoded_preds] + decoded_labels = ["\n".join(sent_tokenize(label.strip())) for label in decoded_labels] + # Вычисляем оценки ROUGE + result = rouge_score.compute( + predictions=decoded_preds, references=decoded_labels, use_stemmer=True + ) + # Извлекаем медианные оценки + result = {key: value.mid.fmeasure * 100 for key, value in result.items()} + return {k: round(v, 4) for k, v in result.items()} +``` + +{/if} + +Далее нам нужно определить коллатор данных для нашей задачи преобразования последовательности в последовательность. Поскольку mT5 является моделью трансформера кодер-декодер, одна из тонкостей подготовки наших батчей заключается в том, что во время декодирования нам нужно сдвинуть метки вправо на единицу. Это необходимо для того, чтобы декодер видел только предыдущие метки, а не текущие или будущие, которые модели было бы легко запомнить. Это похоже на то, как маскированное самовнимание применяется к входным данным в задаче типа [каузального языкового моделирования](/course/chapter7/6). + +К счастью, 🤗 Transformers предоставляет коллатор `DataCollatorForSeq2Seq`, который будет динамически дополнять входные данные и метки за нас. Чтобы инстанцировать этот коллатор, нам просто нужно предоставить `tokenizer` и `model`: + +{#if fw === 'pt'} + +```python +from transformers import DataCollatorForSeq2Seq + +data_collator = DataCollatorForSeq2Seq(tokenizer, model=model) +``` + +{:else} + +```python +from transformers import DataCollatorForSeq2Seq + +data_collator = DataCollatorForSeq2Seq(tokenizer, model=model, return_tensors="tf") +``` + +{/if} + +Давайте посмотрим, что выдает этот коллатор, когда ему передается небольшой батч примеров. Во-первых, нам нужно удалить столбцы со строками, потому что коллатор не будет знать, как вставлять эти элементы: + +```python +tokenized_datasets = tokenized_datasets.remove_columns( + books_dataset["train"].column_names +) +``` + +Давайте посмотрим, что выдает этот коллатор, когда ему передается небольшой батч примеров. Во-первых, нам нужно удалить столбцы со строками, потому что коллатор не будет знать, как вставлять эти элементы: + +```python +features = [tokenized_datasets["train"][i] for i in range(2)] +data_collator(features) +``` + +```python out +{'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]), 'input_ids': tensor([[ 1494, 259, 8622, 390, 259, 262, 2316, 3435, 955, + 772, 281, 772, 1617, 263, 305, 14701, 260, 1385, + 3031, 259, 24146, 332, 1037, 259, 43906, 305, 336, + 260, 1, 0, 0, 0, 0, 0, 0], + [ 259, 27531, 13483, 259, 7505, 260, 112240, 15192, 305, + 53198, 276, 259, 74060, 263, 260, 459, 25640, 776, + 2119, 336, 259, 2220, 259, 18896, 288, 4906, 288, + 1037, 3931, 260, 7083, 101476, 1143, 260, 1]]), 'labels': tensor([[ 7483, 259, 2364, 15695, 1, -100], + [ 259, 27531, 13483, 259, 7505, 1]]), 'decoder_input_ids': tensor([[ 0, 7483, 259, 2364, 15695, 1], + [ 0, 259, 27531, 13483, 259, 7505]])} +``` + +Главное, что здесь нужно заметить, - это то, что первый пример длиннее второго, поэтому `input_ids` и `attention_mask` второго примера были дополнены справа токеном `[PAD]` (чей идентификатор равен `0`). Аналогично, мы видим, что `labels` были дополнены значением `-100`, чтобы функция потерь игнорировала токены дополнения. И наконец, мы видим новый `decoder_input_ids`, в котором метки сдвинуты вправо за счет вставки токена `[PAD]` в первую запись. + +{#if fw === 'pt'} + +Наконец-то у нас есть все необходимые ингредиенты для обучения! Теперь нам нужно просто создать тренер со стандартными аргументами: + +```python +from transformers import Seq2SeqTrainer + +trainer = Seq2SeqTrainer( + model, + args, + train_dataset=tokenized_datasets["train"], + eval_dataset=tokenized_datasets["validation"], + data_collator=data_collator, + tokenizer=tokenizer, + compute_metrics=compute_metrics, +) +``` + +и запустить наш цикл обучения: + +```python +trainer.train() +``` + +Во время обучения вы должны видеть, как потери при обучении уменьшаются, а оценка ROUGE увеличивается с каждой эпохой. После завершения обучения вы можете увидеть итоговую оценку ROUGE, выполнив команду `Trainer.evaluate()`: + +```python +trainer.evaluate() +``` + +```python out +{'eval_loss': 3.028524398803711, + 'eval_rouge1': 16.9728, + 'eval_rouge2': 8.2969, + 'eval_rougeL': 16.8366, + 'eval_rougeLsum': 16.851, + 'eval_gen_len': 10.1597, + 'eval_runtime': 6.1054, + 'eval_samples_per_second': 38.982, + 'eval_steps_per_second': 4.914} +``` + +Из оценок видно, что наша модель значительно превзошла базовый уровень lead-3 - отлично! Осталось отправить веса модели в Hub, как показано ниже: + +``` +trainer.push_to_hub(commit_message="Training complete", tags="summarization") +``` + +```python out +'https://huggingface.co/huggingface-course/mt5-finetuned-amazon-en-es/commit/aa0536b829b28e73e1e4b94b8a5aacec420d40e0' +``` + +Это позволит сохранить контрольную точку и файлы конфигурации в `output_dir`, а затем загрузить все файлы на Хаб. Указав аргумент `tags`, мы также гарантируем, что виджет на хабе будет предназначен для конвейера суммаризации, а не для конвейера генерации текста по умолчанию, связанного с архитектурой mT5 (более подробную информацию о тегах моделей можно найти в [🤗 документации по Hub](https://huggingface.co/docs/hub/main#how-is-a-models-type-of-inference-api-and-widget-determined)). Вывод `trainer.push_to_hub()` - это URL на хэш Git-коммита, так что вы можете легко увидеть изменения, которые были сделаны в розитории модели! + +В завершение этого раздела рассмотрим, как можно дообучить mT5 с помощью низкоуровневых функций, предоставляемых 🤗 Accelerate. + +{:else} + +Мы почти готовы к обучению! Нам нужно только преобразовать наши датасеты в `tf.data.Dataset` с помощью коллатора данных, который мы определили выше, а затем выолнить `compile()` и `fit()` модели. Сначала датасеты: + +```python +tf_train_dataset = model.prepare_tf_dataset( + tokenized_datasets["train"], + collate_fn=data_collator, + shuffle=True, + batch_size=8, +) +tf_eval_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=data_collator, + shuffle=False, + batch_size=8, +) +``` + +Теперь определяем гиперпараметры обучения и компилируем: + +```python +from transformers import create_optimizer +import tensorflow as tf + +# Количество шагов обучения - это количество примеров в датасете, разделенное на размер батча, затем умноженное +# на общее количество эпох. Обратите внимание, что tf_train_dataset здесь - это батч tf.data.Dataset, +# а не оригинальный датасет Hugging Face, поэтому его len() уже равен num_samples // batch_size. +num_train_epochs = 8 +num_train_steps = len(tf_train_dataset) * num_train_epochs +model_name = model_checkpoint.split("/")[-1] + +optimizer, schedule = create_optimizer( + init_lr=5.6e-5, + num_warmup_steps=0, + num_train_steps=num_train_steps, + weight_decay_rate=0.01, +) + +model.compile(optimizer=optimizer) + +# Обучение со смешанной точностью float16 +tf.keras.mixed_precision.set_global_policy("mixed_float16") +``` + +И наконец, мы подгоняем модель. Мы используем `PushToHubCallback` для сохранения модели на Hub после каждой эпохи, что позволит нам использовать ее позже для инференса: + +```python +from transformers.keras_callbacks import PushToHubCallback + +callback = PushToHubCallback( + output_dir=f"{model_name}-finetuned-amazon-en-es", tokenizer=tokenizer +) + +model.fit( + tf_train_dataset, validation_data=tf_eval_dataset, callbacks=[callback], epochs=8 +) +``` + +Мы получили некоторые значения потерь во время обучения, но на самом деле нам хотелось бы увидеть метрику ROUGE, которую мы вычисляли ранее. Чтобы получить эти метрики, нам нужно сгенерировать выходные данные модели и преобразовать их в строки. Давайте создадим несколько списков меток и прогнозов для сравнения с метрикой ROUGE (обратите внимание, что если вы получаете ошибки импорта в этом разделе, вам может понадобиться команда `!pip install tqdm`). Мы также используем трюк, который значительно повышает производительность, - компиляцию генерируемого кода с помощью [XLA](https://www.tensorflow.org/xla), ускоренного компилятора линейной алгебры TensorFlow. XLA применяет различные оптимизации к графу вычислений модели, что приводит к значительному увеличению скорости и использования памяти. Как описано в Hugging Face [блоге](https://huggingface.co/blog/tf-xla-generate), XLA работает лучше всего, когда формы наших входных данных не слишком сильно различаются. Чтобы справиться с этим, мы дополним наши входные данные до кратных 128 и создадим новый датасет с помощью дополняющего коллатора, а затем применим декоратор `@tf.function(jit_compile=True)` к нашей функции генерации, который помечает всю функцию для компиляции с помощью XLA. + +```python +from tqdm import tqdm +import numpy as np + +generation_data_collator = DataCollatorForSeq2Seq( + tokenizer, model=model, return_tensors="tf", pad_to_multiple_of=320 +) + +tf_generate_dataset = model.prepare_tf_dataset( + tokenized_datasets["validation"], + collate_fn=generation_data_collator, + shuffle=False, + batch_size=8, + drop_remainder=True, +) + + +@tf.function(jit_compile=True) +def generate_with_xla(batch): + return model.generate( + input_ids=batch["input_ids"], + attention_mask=batch["attention_mask"], + max_new_tokens=32, + ) + + +all_preds = [] +all_labels = [] +for batch, labels in tqdm(tf_generate_dataset): + predictions = generate_with_xla(batch) + decoded_preds = tokenizer.batch_decode(predictions, skip_special_tokens=True) + labels = labels.numpy() + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + decoded_preds = ["\n".join(sent_tokenize(pred.strip())) for pred in decoded_preds] + decoded_labels = ["\n".join(sent_tokenize(label.strip())) for label in decoded_labels] + all_preds.extend(decoded_preds) + all_labels.extend(decoded_labels) +``` + +Когда у нас есть списки строк меток и прогнозов, вычислить оценку ROUGE очень просто: + +```python +result = rouge_score.compute( + predictions=decoded_preds, references=decoded_labels, use_stemmer=True +) +result = {key: value.mid.fmeasure * 100 for key, value in result.items()} +{k: round(v, 4) for k, v in result.items()} +``` + +``` +{'rouge1': 31.4815, 'rouge2': 25.4386, 'rougeL': 31.4815, 'rougeLsum': 31.4815} +``` + + +{/if} + +{#if fw === 'pt'} + +## Дообучение mT5 с 🤗 Accelerate[[fine-tuning-mt5-with-accelerate]] + +Дообучение нашей модели с помощью 🤗 Accelerate очень похоже на пример с классификацией текста, который мы рассматривали в [Главе 3] (/course/chapter3). Основные отличия заключаются в необходимости явной генерации резюме во время обучения и определении способа вычисления оценок ROUGE (напомним, что `Seq2SeqTrainer` позаботился о генерации за нас). Давайте посмотрим, как мы можем реализовать эти два требования в 🤗 Accelerate! + +### Подготовка всего к обучению[[preparing-everything-for-training]] + +Первое, что нам нужно сделать, это создать `DataLoader` для каждой из наших частей. Поскольку загрузчики данных PyTorch ожидают батч тензоров, нам нужно задать формат `"torch"` в наших датасетах: + +```python +tokenized_datasets.set_format("torch") +``` + +Теперь, когда у нас есть датасеты, состоящие только из тензоров, следующее, что нужно сделать, - это снова инстанцировать `DataCollatorForSeq2Seq`. Для этого нам нужно предоставить свежую версию модели, поэтому давайте снова загрузим ее из нашего кэша: + +```python +model = AutoModelForSeq2SeqLM.from_pretrained(model_checkpoint) +``` + +Затем мы можем инстанцировать коллатор данных и использовать его для определения наших загрузчиков данных: + +```python +from torch.utils.data import DataLoader + +batch_size = 8 +train_dataloader = DataLoader( + tokenized_datasets["train"], + shuffle=True, + collate_fn=data_collator, + batch_size=batch_size, +) +eval_dataloader = DataLoader( + tokenized_datasets["validation"], collate_fn=data_collator, batch_size=batch_size +) +``` + +Следующее, что нужно сделать, это определить оптимизатор, который мы хотим использовать. Как и в других наших примерах, мы будем использовать `AdamW`, который хорошо работает для большинства задач: + +```python +from torch.optim import AdamW + +optimizer = AdamW(model.parameters(), lr=2e-5) +``` + +Наконец, мы передаем нашу модель, оптимизатор и загрузчики данных в метод `accelerator.prepare()`: + +```python +from accelerate import Accelerator + +accelerator = Accelerator() +model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare( + model, optimizer, train_dataloader, eval_dataloader +) +``` + + + +🚨 Если вы обучаете на TPU, вам нужно будет перенести весь приведенный выше код в специальную функцию обучения. Подробнее смотрите в [Главе 3](/course/chapter3). + + + +Теперь, когда мы подготовили наши объекты, осталось сделать три вещи: + +* Определить график скорости обучения. +* Реализовать функцию для постобработки резюме для оценки. +* Создать розиторий на Hub, в который мы можем отправить нашу модель. + +В качестве графика скорости обучения мы будем использовать стандартный линейный график из предыдущих разделов: + +```python +from transformers import get_scheduler + +num_train_epochs = 10 +num_update_steps_per_epoch = len(train_dataloader) +num_training_steps = num_train_epochs * num_update_steps_per_epoch + +lr_scheduler = get_scheduler( + "linear", + optimizer=optimizer, + num_warmup_steps=0, + num_training_steps=num_training_steps, +) +``` + +Для постобработки нам нужна функция, которая разбивает сгенерированные резюме на предложения, разделенные символами новой строки. Именно такой формат ожидает метрика ROUGE, и мы можем достичь этого с помощью следующего фрагмента кода: + +```python +def postprocess_text(preds, labels): + preds = [pred.strip() for pred in preds] + labels = [label.strip() for label in labels] + + # ROUGE ожидает символ новой строки после каждого предложения + preds = ["\n".join(nltk.sent_tokenize(pred)) for pred in preds] + labels = ["\n".join(nltk.sent_tokenize(label)) for label in labels] + + return preds, labels +``` + +Это должно показаться вам знакомым, если вы помните, как мы определяли функцию `compute_metrics()` для `Seq2SeqTrainer`. + +Наконец, нам нужно создать розиторий модели на Hugging Face Hub. Для этого мы можем использовать библиотеку 🤗Hub с соответствующим заголовком. Нам нужно только задать имя нашего розитория, а в библиотеке есть служебная функция для объединения идентификатора розитория с профилем пользователя: + +```python +from huggingface_hub import get_full_repo_name + +model_name = "test-bert-finetuned-squad-accelerate" +repo_name = get_full_repo_name(model_name) +repo_name +``` + +```python out +'lewtun/mt5-finetuned-amazon-en-es-accelerate' +``` + +Теперь мы можем использовать имя этого розитория для клонирования локальной версии в каталог результатов, в котором будут храниться результаты обучения: + +```python +from huggingface_hub import Repository + +output_dir = "results-mt5-finetuned-squad-accelerate" +repo = Repository(output_dir, clone_from=repo_name) +``` + +Это позволит нам отправить результаты в Hub, вызвав метод `repo.push_to_hub()` во время обучения! Теперь давайте завершим наш анализ, написав цикл обучения. + +### Цикл обучения[[training-loop]] + +Цикл обучения суммаризации очень похож на другие примеры 🤗 Accelerate, с которыми мы сталкивались, и состоит из четырех основных этапов: + +1. Обучение модели путем итерации по всем примерам в `train_dataloader` на каждой эпохе. +2. Генерация резюме моделью в конце каждой эпохи, сначала генерируются токены, а затем они (и эталонные резюме) декодируются в текст. +3. Вычисление оценок ROUGE с помощью тех же приемов, которые мы рассмотрели ранее. +4. Сохранение контрольных точек и отправка всего в Hub. Здесь мы полагаемся на полезный аргумент `blocking=False` объекта `Repository`, чтобы мы могли отправить контрольные точки на каждой эпохе _асинхронно_. Это позволяет нам продолжать обучение, не дожидаясь медленной загрузки, связанной с моделью размером в гигабайт! + +Эти шаги можно увидеть в следующем блоке кода: + +```python +from tqdm.auto import tqdm +import torch +import numpy as np + +progress_bar = tqdm(range(num_training_steps)) + +for epoch in range(num_train_epochs): + # Обучение + model.train() + for step, batch in enumerate(train_dataloader): + outputs = model(**batch) + loss = outputs.loss + accelerator.backward(loss) + + optimizer.step() + lr_scheduler.step() + optimizer.zero_grad() + progress_bar.update(1) + + # Оценка + model.eval() + for step, batch in enumerate(eval_dataloader): + with torch.no_grad(): + generated_tokens = accelerator.unwrap_model(model).generate( + batch["input_ids"], + attention_mask=batch["attention_mask"], + ) + + generated_tokens = accelerator.pad_across_processes( + generated_tokens, dim=1, pad_index=tokenizer.pad_token_id + ) + labels = batch["labels"] + + # Если мы не дополнили до максимальной длины, нам нужно дополнить и метки + labels = accelerator.pad_across_processes( + batch["labels"], dim=1, pad_index=tokenizer.pad_token_id + ) + + generated_tokens = accelerator.gather(generated_tokens).cpu().numpy() + labels = accelerator.gather(labels).cpu().numpy() + + # Заменяем -100 в метках, поскольку мы не можем их декодировать + labels = np.where(labels != -100, labels, tokenizer.pad_token_id) + if isinstance(generated_tokens, tuple): + generated_tokens = generated_tokens[0] + decoded_preds = tokenizer.batch_decode( + generated_tokens, skip_special_tokens=True + ) + decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True) + + decoded_preds, decoded_labels = postprocess_text( + decoded_preds, decoded_labels + ) + + rouge_score.add_batch(predictions=decoded_preds, references=decoded_labels) + + # Вычисляем метрики + result = rouge_score.compute() + # Извлекаем медианные оценки ROUGE + result = {key: value.mid.fmeasure * 100 for key, value in result.items()} + result = {k: round(v, 4) for k, v in result.items()} + print(f"Epoch {epoch}:", result) + + # Сохранение и загрузка + accelerator.wait_for_everyone() + unwrapped_model = accelerator.unwrap_model(model) + unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save) + if accelerator.is_main_process: + tokenizer.save_pretrained(output_dir) + repo.push_to_hub( + commit_message=f"Training in progress epoch {epoch}", blocking=False + ) +``` + +```python out +Epoch 0: {'rouge1': 5.6351, 'rouge2': 1.1625, 'rougeL': 5.4866, 'rougeLsum': 5.5005} +Epoch 1: {'rouge1': 9.8646, 'rouge2': 3.4106, 'rougeL': 9.9439, 'rougeLsum': 9.9306} +Epoch 2: {'rouge1': 11.0872, 'rouge2': 3.3273, 'rougeL': 11.0508, 'rougeLsum': 10.9468} +Epoch 3: {'rouge1': 11.8587, 'rouge2': 4.8167, 'rougeL': 11.7986, 'rougeLsum': 11.7518} +Epoch 4: {'rouge1': 12.9842, 'rouge2': 5.5887, 'rougeL': 12.7546, 'rougeLsum': 12.7029} +Epoch 5: {'rouge1': 13.4628, 'rouge2': 6.4598, 'rougeL': 13.312, 'rougeLsum': 13.2913} +Epoch 6: {'rouge1': 12.9131, 'rouge2': 5.8914, 'rougeL': 12.6896, 'rougeLsum': 12.5701} +Epoch 7: {'rouge1': 13.3079, 'rouge2': 6.2994, 'rougeL': 13.1536, 'rougeLsum': 13.1194} +Epoch 8: {'rouge1': 13.96, 'rouge2': 6.5998, 'rougeL': 13.9123, 'rougeLsum': 13.7744} +Epoch 9: {'rouge1': 14.1192, 'rouge2': 7.0059, 'rougeL': 14.1172, 'rougeLsum': 13.9509} +``` + +Вот и все! После запуска у вас будет модель и результаты, очень похожие на те, что мы получили с помощью `Trainer`. + +{/if} + +## Использование дообученной вами модели[[using-your-fine-tuned-model]] + +После того как вы отправили модель в Hub, вы можете работать с ней либо с помощью виджета инференса, либо с помощью объекта `pipeline`, как показано ниже: + +```python +from transformers import pipeline + +hub_model_id = "huggingface-course/mt5-small-finetuned-amazon-en-es" +summarizer = pipeline("summarization", model=hub_model_id) +``` + +Мы можем передать в наш конвейер несколько примеров из тестового набора (которые модель не видела), чтобы получить представление о качестве резюме. Для начала давайте реализуем простую функцию, которая будет показывать обзор, заголовок и сгенерированное резюме вместе: + +```python +def print_summary(idx): + review = books_dataset["test"][idx]["review_body"] + title = books_dataset["test"][idx]["review_title"] + summary = summarizer(books_dataset["test"][idx]["review_body"])[0]["summary_text"] + print(f"'>>> Review: {review}'") + print(f"\n'>>> Title: {title}'") + print(f"\n'>>> Summary: {summary}'") +``` + +Давайте посмотрим на один из английских примеров, которые мы получаем: + +```python +print_summary(100) +``` + +```python out +'>>> Review: Nothing special at all about this product... the book is too small and stiff and hard to write in. The huge sticker on the back doesn’t come off and looks super tacky. I would not purchase this again. I could have just bought a journal from the dollar store and it would be basically the same thing. It’s also really expensive for what it is.' + +'>>> Title: Not impressed at all... buy something else' + +'>>> Summary: Nothing special at all about this product' +``` + +Это не так уж плохо! Мы видим, что наша модель действительно способна выполнять _абстрактную_ суммуризацию, дополняя части обзора новыми словами. И, пожалуй, самый интересный аспект нашей модели - это то, что она билингвистическая, так что мы можем генерировать резюме и для испанских рецензий: + +```python +print_summary(0) +``` + +```python out +'>>> Review: Es una trilogia que se hace muy facil de leer. Me ha gustado, no me esperaba el final para nada' + +'>>> Title: Buena literatura para adolescentes' + +'>>> Summary: Muy facil de leer' +``` + +Резюме переводится как "Very easy to read" на английском языке, что, как мы видим, в данном случае было непосредственно взято из обзора. Тем не менее, это демонстрирует универсальность модели mT5 и дает вам представление о том, каково это - работать с многоязычным корпусом! + +Далее мы обратимся к несколько более сложной задаче: обучению языковой модели с нуля. From 3fda9eb09d60bd1adf7a538ade36c8a84522b8c8 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Tue, 9 Jan 2024 20:40:11 +0300 Subject: [PATCH 02/24] After run python utils/code_formatter.py --- chapters/ru/chapter7/3.mdx | 2 +- chapters/ru/chapter7/4.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 934a38243..99b6014eb 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -383,7 +383,7 @@ def group_texts(examples): concatenated_examples = {k: sum(examples[k], []) for k in examples.keys()} # Вычисляем длину конкатенированных текстов total_length = len(concatenated_examples[list(examples.keys())[0]]) - # Отбрасываем последний фрагмент, если он меньше chunk_size + # Отбрасываем последний фрагмент, если он меньше chunk_size total_length = (total_length // chunk_size) * chunk_size # Разбиваем на фрагменты длиной max_len result = { diff --git a/chapters/ru/chapter7/4.mdx b/chapters/ru/chapter7/4.mdx index f1362724f..870684687 100644 --- a/chapters/ru/chapter7/4.mdx +++ b/chapters/ru/chapter7/4.mdx @@ -552,7 +552,7 @@ import numpy as np def compute_metrics(eval_preds): preds, labels = eval_preds - # В случае, если модель возвращает больше, чем предсказанные логиты + # В случае, если модель возвращает больше, чем предсказанные логиты if isinstance(preds, tuple): preds = preds[0] From c6247014ff4d823ad8416bd24a71183398879594 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:02:15 +0300 Subject: [PATCH 03/24] Update chapters/ru/chapter7/1.mdx Extra space. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/1.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/1.mdx b/chapters/ru/chapter7/1.mdx index 43df2873c..b25a2e19c 100644 --- a/chapters/ru/chapter7/1.mdx +++ b/chapters/ru/chapter7/1.mdx @@ -7,7 +7,7 @@ classNames="absolute z-10 right-0 top-0" /> -В [Главе 3] (/course/chapter3) вы узнали, как дообучить модель для классификации текстов. В этой главе мы рассмотрим следующие общие задачи NLP: +В [Главе 3](/course/chapter3) вы узнали, как дообучить модель для классификации текстов. В этой главе мы рассмотрим следующие общие задачи NLP: - Классификация токенов (Token classification) - Маскированное языковое моделирование (Masked language modeling, например, BERT) From 37cd6129ce6dd570ff7ca7f760ef9dac682fced6 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:02:45 +0300 Subject: [PATCH 04/24] Update chapters/ru/chapter7/2.mdx Extra space. I didn't notice. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/2.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx index 7f7aa7b65..839259bdc 100644 --- a/chapters/ru/chapter7/2.mdx +++ b/chapters/ru/chapter7/2.mdx @@ -47,7 +47,7 @@ -💡 Если ваш набор данных состоит из текстов, часть которых состоит из слов с соответствующими метками, вы сможете адаптировать описанные здесь процедуры обработки данных к своему набору данных. Обратитесь к [Главе 5] (/course/chapter5), если вам нужно освежить в памяти то, как загружать собственные данные в `Dataset`. +💡 Если ваш набор данных состоит из текстов, часть которых состоит из слов с соответствующими метками, вы сможете адаптировать описанные здесь процедуры обработки данных к своему набору данных. Обратитесь к [Главе 5](/course/chapter5), если вам нужно освежить в памяти то, как загружать собственные данные в `Dataset`. From 7a8731112b487409b58cf0cb5ff737d8624e5f75 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:03:00 +0300 Subject: [PATCH 05/24] Update chapters/ru/chapter7/2.mdx Extra space. I didn't notice. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/2.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx index 839259bdc..489f48998 100644 --- a/chapters/ru/chapter7/2.mdx +++ b/chapters/ru/chapter7/2.mdx @@ -128,7 +128,7 @@ label_names ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'] ``` -Мы уже видели эти метки при изучении конвейера `token-classification` в [Главе 6] (/course/chapter6/3), но для краткости напомним: +Мы уже видели эти метки при изучении конвейера `token-classification` в [Главе 6](/course/chapter6/3), но для краткости напомним: - `O` означает, что слово не соответствует какой-либо сущности. - `B-PER`/`I-PER` означает, что слово соответствует началу/находится внутри сущности персоны *person*. From c4379456a53eb239a66ca762aacdd16162e4b50a Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:04:37 +0300 Subject: [PATCH 06/24] Update chapters/ru/chapter7/2.mdx Yes, indeed, I ate the space bar))))) Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/2.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx index 489f48998..c4036cbd0 100644 --- a/chapters/ru/chapter7/2.mdx +++ b/chapters/ru/chapter7/2.mdx @@ -719,7 +719,7 @@ args = TrainingArguments( ) ``` -Большинство из них вы уже видели: мы задаем некоторые гиперпараметры (например, скорость обучения, количество эпох для обучения и затухание весов) и указываем `push_to_hub=True`, чтобы указать, что мы хотим сохранить модель и оценить ее в конце каждой эпохи, а также что мы хотим загрузить наши результаты в Model Hub. Обратите внимание, что с помощью аргумента `hub_model_id` можно указать имя репозитория, в который вы хотите передать модель (в частности, этот аргумент нужно использовать, чтобы передать модель в организацию). Например, когда мы передавали модель в [организацию`huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/bert-finetuned-ner"` в `TrainingArguments`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, так что в нашем случае это будет `"sgugger/bert-finetuned-ner"`. +Большинство из них вы уже видели: мы задаем некоторые гиперпараметры (например, скорость обучения, количество эпох для обучения и затухание весов) и указываем `push_to_hub=True`, чтобы указать, что мы хотим сохранить модель и оценить ее в конце каждой эпохи, а также что мы хотим загрузить наши результаты в Model Hub. Обратите внимание, что с помощью аргумента `hub_model_id` можно указать имя репозитория, в который вы хотите передать модель (в частности, этот аргумент нужно использовать, чтобы передать модель в организацию). Например, когда мы передавали модель в [организацию `huggingface-course`](https://huggingface.co/huggingface-course), мы добавили `hub_model_id="huggingface-course/bert-finetuned-ner"` в `TrainingArguments`. По умолчанию используемый репозиторий будет находиться в вашем пространстве имен и называться в соответствии с заданным вами выходным каталогом, так что в нашем случае это будет `"sgugger/bert-finetuned-ner"`. From 2626bc500eeb22cd3ef5c04ea966259d1f7f8f59 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:05:12 +0300 Subject: [PATCH 07/24] Update chapters/ru/chapter7/5.mdx There's that extra space again. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/5.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index fbe935615..294c8a8f5 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -265,7 +265,7 @@ inputs {'input_ids': [336, 259, 28387, 11807, 287, 62893, 295, 12507, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1]} ``` -Здесь мы видим знакомые нам `input_ids` и `attention_mask`, с которыми мы столкнулись в наших первых экспериментах по дообучению еще в [Главе 3] (/course/chapter3). Давайте декодируем эти входные идентификаторы с помощью функции токенизатора `convert_ids_to_tokens()`, чтобы понять, с каким токенизатором мы имеем дело: +Здесь мы видим знакомые нам `input_ids` и `attention_mask`, с которыми мы столкнулись в наших первых экспериментах по дообучению еще в [Главе 3](/course/chapter3). Давайте декодируем эти входные идентификаторы с помощью функции токенизатора `convert_ids_to_tokens()`, чтобы понять, с каким токенизатором мы имеем дело: ```python tokenizer.convert_ids_to_tokens(inputs.input_ids) From cb440a6d0e674e152a23b9e5618c743e9eddbdf9 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:05:42 +0300 Subject: [PATCH 08/24] Update chapters/ru/chapter7/5.mdx There's that extra space again that I didn't notice. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/5.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index 294c8a8f5..4e71e1cb5 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -275,7 +275,7 @@ tokenizer.convert_ids_to_tokens(inputs.input_ids) ['▁I', '▁', 'loved', '▁reading', '▁the', '▁Hung', 'er', '▁Games', ''] ``` -Специальный символ Юникода `▁` и токен конца последовательности `` указывают на то, что мы имеем дело с токенизатором SentencePiece, который основан на алгоритме сегментации Unigram, рассмотренном в [Главе 6] (/course/chapter6). Unigram особенно полезен для многоязычных корпусов, поскольку он позволяет SentencePiece не зависеть от ударений, пунктуации и того факта, что во многих языках, например в японском, нет пробельных символов. +Специальный символ Юникода `▁` и токен конца последовательности `` указывают на то, что мы имеем дело с токенизатором SentencePiece, который основан на алгоритме сегментации Unigram, рассмотренном в [Главе 6](/course/chapter6). Unigram особенно полезен для многоязычных корпусов, поскольку он позволяет SentencePiece не зависеть от ударений, пунктуации и того факта, что во многих языках, например в японском, нет пробельных символов. Для токенизации нашего корпуса нам придется столкнуться с одной тонкостью, связанной с сумризацией: поскольку наши метки также являются текстом, возможно, что они превышают максимальный размер контекста модели. Это означает, что нам нужно применять усечение как к обзорам, так и к их заголовкам, чтобы не передавать в модель слишком длинные данные. Токенизаторы в 🤗 Transformers предоставляют интересный аргумент `text_target`, который позволяет вам токенизировать метки параллельно с входными данными. Вот пример того, как обрабатываются входные и целевые данные для mT5: From 068217e8d4b0db170f27e5306a570d0f6c0185dc Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:06:30 +0300 Subject: [PATCH 09/24] Update chapters/ru/chapter7/5.mdx Extra space. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/5.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index 4e71e1cb5..e7b112701 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -792,7 +792,7 @@ result = {key: value.mid.fmeasure * 100 for key, value in result.items()} ## Дообучение mT5 с 🤗 Accelerate[[fine-tuning-mt5-with-accelerate]] -Дообучение нашей модели с помощью 🤗 Accelerate очень похоже на пример с классификацией текста, который мы рассматривали в [Главе 3] (/course/chapter3). Основные отличия заключаются в необходимости явной генерации резюме во время обучения и определении способа вычисления оценок ROUGE (напомним, что `Seq2SeqTrainer` позаботился о генерации за нас). Давайте посмотрим, как мы можем реализовать эти два требования в 🤗 Accelerate! +Дообучение нашей модели с помощью 🤗 Accelerate очень похоже на пример с классификацией текста, который мы рассматривали в [Главе 3](/course/chapter3). Основные отличия заключаются в необходимости явной генерации резюме во время обучения и определении способа вычисления оценок ROUGE (напомним, что `Seq2SeqTrainer` позаботился о генерации за нас). Давайте посмотрим, как мы можем реализовать эти два требования в 🤗 Accelerate! ### Подготовка всего к обучению[[preparing-everything-for-training]] From fa12024fe95a599b9148fd06cc932b48c337719a Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:12:13 +0300 Subject: [PATCH 10/24] Update 5.mdx Translated the missing comment. --- chapters/ru/chapter7/5.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index e7b112701..a2930329b 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -508,7 +508,7 @@ from transformers import Seq2SeqTrainingArguments batch_size = 8 num_train_epochs = 8 -# Show the training loss with every epoch +# Выводим потери при обучении по каждой эпохе logging_steps = len(tokenized_datasets["train"]) // batch_size model_name = model_checkpoint.split("/")[-1] From 07373e64dcee1cd5d49b202ba245767e23abde9a Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:17:24 +0300 Subject: [PATCH 11/24] Update chapters/ru/chapter7/4.mdx Extra space. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/4.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/4.mdx b/chapters/ru/chapter7/4.mdx index 870684687..98843c395 100644 --- a/chapters/ru/chapter7/4.mdx +++ b/chapters/ru/chapter7/4.mdx @@ -29,7 +29,7 @@ -Если у вас есть достаточно большой корпус текстов на двух (или более) языках, вы можете обучить новую модель перевода с нуля, как мы это сделаем в разделе по [казуальному языковому моделированию (causal language modeling)] (/course/chapter7/6). Однако быстрее будет дообучить существующую модель перевода, будь то многоязычная модель типа mT5 или mBART, которую нужно дообучить для конкретной пары языков, или даже модель, специализированная для перевода с одного языка на другой, которую нужно дообучить для конкретного корпуса. +Если у вас есть достаточно большой корпус текстов на двух (или более) языках, вы можете обучить новую модель перевода с нуля, как мы это сделаем в разделе по [казуальному языковому моделированию (causal language modeling)](/course/chapter7/6). Однако быстрее будет дообучить существующую модель перевода, будь то многоязычная модель типа mT5 или mBART, которую нужно дообучить для конкретной пары языков, или даже модель, специализированная для перевода с одного языка на другой, которую нужно дообучить для конкретного корпуса. В этом разделе мы дообучим модель Marian, предварительно обученную переводу с английского на французский (поскольку многие сотрудники Hugging Face говорят на обоих этих языках), на датасете [KDE4](https://huggingface.co/datasets/kde4), который представляет собой набор локализованных файлов для приложений [KDE](https://apps.kde.org/). Модель, которую мы будем использовать, была предварительно обучена на большом корпусе французских и английских текстов, взятых из [Opus dataset](https://opus.nlpl.eu/), который фактически содержит датасет KDE4. Но даже если модель, которую мы используем, видела эти данные во время предварительного обучения, мы увидим, что после дообучения мы сможем получить ее лучшую версию. From 95bda7c413f0b98fa47e219a587706b677299cd8 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:20:28 +0300 Subject: [PATCH 12/24] Update 2.mdx Translated the missing comment in the code --- chapters/ru/chapter7/2.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx index c4036cbd0..1000eab4a 100644 --- a/chapters/ru/chapter7/2.mdx +++ b/chapters/ru/chapter7/2.mdx @@ -239,7 +239,7 @@ def align_labels_with_tokens(labels, word_ids): else: # То же слово, что и предыдущий токен label = labels[word_id] - # If the label is B-XXX we change it to I-XXX + # Если метка B-XXX, заменяем ее на I-XXX if label % 2 == 1: label += 1 new_labels.append(label) From 913a9b1efb91988fc374f6c525f4b6b5dc1b1ef5 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:29:39 +0300 Subject: [PATCH 13/24] Update 2.mdx Translated the missing sentence. --- chapters/ru/chapter7/2.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/2.mdx b/chapters/ru/chapter7/2.mdx index 1000eab4a..cae2eb4e6 100644 --- a/chapters/ru/chapter7/2.mdx +++ b/chapters/ru/chapter7/2.mdx @@ -390,7 +390,7 @@ tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset( ``` - Next stop: the model itself. + Следующая остановка: сама модель. {/if} From 5f5d4aaf7cc35aebfc1dcb7d80e21cfa1301113e Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:34:32 +0300 Subject: [PATCH 14/24] Update 3.mdx Translated the missing sentence. --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 99b6014eb..a51860d0b 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -60,7 +60,7 @@ {#if fw === 'pt'} -Let's go ahead and download DistilBERT using the `AutoModelForMaskedLM` class: +Давайте перейдем к загрузке DistilBERT с помощью класса `AutoModelForMaskedLM`: ```python from transformers import AutoModelForMaskedLM From 30ca4467842d7f323ed1b038a24618234aabd4cf Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:51:18 +0300 Subject: [PATCH 15/24] Update 3.mdx I agree, it sounds more neutral that way. --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index a51860d0b..f6dd2eadb 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -235,7 +235,7 @@ for row in sample: '>>> Label: 1' ``` -Да, это точно рецензии на фильмы, и если вы достаточно взрослый, то можете даже понять комментарий в последней рецензии о том, что у вас есть VHS-версия 😜! Хотя нам не понадобятся эти метки для языкового моделирования, мы уже видим, что `0` обозначает отрицательный отзыв, а `1` - положительный. +Да, это точно рецензии на фильмы, и если вы родились до 1990х, вам будет лучше понятен комментарий в последней рецензии о VHS-версии. 😜! Хотя нам не понадобятся эти метки для языкового моделирования, мы уже видим, что `0` обозначает отрицательный отзыв, а `1` - положительный. From 8c57d55bb5a3e5a2e59e994ee6a9eb565a734cb5 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:54:08 +0300 Subject: [PATCH 16/24] Update chapters/ru/chapter7/3.mdx An unnecessary parenthesis. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index f6dd2eadb..7bcc9b94f 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -251,7 +251,7 @@ for row in sample: Как для авторегрессивного, так и для масочного моделирования языка общим шагом предварительной обработки является объединение всех примеров, а затем разбиение всего корпуса на части одинакового размера. Это сильно отличается от нашего обычного подхода, когда мы просто проводим токенизацию отдельных примеров. Зачем конкатенируем все вместе? Причина в том, что отдельные примеры могут быть обрезаны, если они слишком длинные, и это приведет к потере информации, которая может быть полезна для задачи языкового моделирования! -Итак, для начала мы проведем обычную токенизацию нашего корпуса, но _без_ задания параметра `truncation=True` в нашем токенизаторе. Мы также возьмем идентификаторы слов, если они доступны ((а они будут доступны, если мы используем быстрый токенизатор, как описано в [Главе 6](/course/chapter6/3)), поскольку они понадобятся нам позже для маскирования целых слов. Мы обернем это в простую функцию, а пока удалим столбцы `text` и `label`, поскольку они нам больше не нужны: +Итак, для начала мы проведем обычную токенизацию нашего корпуса, но _без_ задания параметра `truncation=True` в нашем токенизаторе. Мы также возьмем идентификаторы слов, если они доступны (а они будут доступны, если мы используем быстрый токенизатор, как описано в [Главе 6](/course/chapter6/3)), поскольку они понадобятся нам позже для маскирования целых слов. Мы обернем это в простую функцию, а пока удалим столбцы `text` и `label`, поскольку они нам больше не нужны: ```python def tokenize_function(examples): From e1993b521c67068ce8aa250a1b52959307405d3a Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:55:56 +0300 Subject: [PATCH 17/24] Update chapters/ru/chapter7/3.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also an option, but we've translated it as "карточка модели" a lot of places. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 7bcc9b94f..61a371b26 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -301,7 +301,7 @@ tokenizer.model_max_length -✏️ **Попробуйте!** Некоторые модели трансформеров, например [BigBird](https://huggingface.co/google/bigbird-roberta-base) и [Longformer](hf.co/allenai/longformer-base-4096), имеют гораздо большую длину контекста, чем BERT и другие ранние модели трансформеров. Инстанцируйте токенизатор для одной из этих контрольных точек и проверьте, что `model_max_length` согласуется с тем, что указано в карточке модели. +✏️ **Попробуйте!** Некоторые модели трансформеров, например [BigBird](https://huggingface.co/google/bigbird-roberta-base) и [Longformer](hf.co/allenai/longformer-base-4096), имеют гораздо большую длину контекста, чем BERT и другие ранние модели трансформеров. Инстанцируйте токенизатор для одной из этих контрольных точек и проверьте, что `model_max_length` согласуется с тем, что указано в описании модели. From c2ad2f64e159cc1d23d4cd0eb66880740ab812ef Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:56:36 +0300 Subject: [PATCH 18/24] Update chapters/ru/chapter7/3.mdx Extra space. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 61a371b26..4017e6738 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -445,7 +445,7 @@ tokenizer.decode(lm_datasets["train"][1]["labels"]) ## Дообучение DistilBERT с помощью API `Trainer`[[fine-tuning-distilbert-with-the-trainer-api]] -Дообучить модель моделирования языка по маске почти то же самое, что и дообучить модель классификации последовательностей, как мы делали в [Главе 3] (/course/chapter3). Единственное отличие заключается в том, что нам нужен специальный коллатор данных, который может случайным образом маскировать некоторые токены в каждом батче текстов. К счастью, 🤗 Transformers поставляется со специальным `DataCollatorForLanguageModeling`, предназначенным именно для этой задачи. Нам нужно только передать ему токенизатор и аргумент `mlm_probability`, который указывает, какую долю токенов нужно маскировать. Мы выберем 15 % - это количество используется для BERT и является распространенным выбором в литературе: +Дообучить модель моделирования языка по маске почти то же самое, что и дообучить модель классификации последовательностей, как мы делали в [Главе 3](/course/chapter3). Единственное отличие заключается в том, что нам нужен специальный коллатор данных, который может случайным образом маскировать некоторые токены в каждом батче текстов. К счастью, 🤗 Transformers поставляется со специальным `DataCollatorForLanguageModeling`, предназначенным именно для этой задачи. Нам нужно только передать ему токенизатор и аргумент `mlm_probability`, который указывает, какую долю токенов нужно маскировать. Мы выберем 15 % - это количество используется для BERT и является распространенным выбором в литературе: ```python from transformers import DataCollatorForLanguageModeling From 1a195753968baef08a2bb1c42f6bb13f1dd696a5 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 20:58:46 +0300 Subject: [PATCH 19/24] Update 3.mdx Translated the missing comment in the code. --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 4017e6738..9b50962f7 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -671,7 +671,7 @@ optimizer, schedule = create_optimizer( ) model.compile(optimizer=optimizer) -# Train in mixed-precision float16 +# Обучение со смешанной точностью float16 tf.keras.mixed_precision.set_global_policy("mixed_float16") model_name = model_checkpoint.split("/")[-1] From 743ea976232e43defb7da70029da8e85805fd0c7 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 21:00:51 +0300 Subject: [PATCH 20/24] Update chapters/ru/chapter7/3.mdx Extra sapce. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/3.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/3.mdx b/chapters/ru/chapter7/3.mdx index 9b50962f7..0fcf0ff95 100644 --- a/chapters/ru/chapter7/3.mdx +++ b/chapters/ru/chapter7/3.mdx @@ -1039,6 +1039,6 @@ for pred in preds: -✏️ **Попробуйте!** Чтобы оценить преимущества адаптации к домену, дообучите классификатор на метках IMDb как для предварительно обученных, так и для дообученных контрольных точек DistilBERT. Если вам нужно освежить в памяти классификацию текстов, ознакомьтесь с [Главой 3] (/course/chapter3). +✏️ **Попробуйте!** Чтобы оценить преимущества адаптации к домену, дообучите классификатор на метках IMDb как для предварительно обученных, так и для дообученных контрольных точек DistilBERT. Если вам нужно освежить в памяти классификацию текстов, ознакомьтесь с [Главой 3](/course/chapter3). From 184cceb7f04717b9eb863b18857333babdb29fcd Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 21:01:40 +0300 Subject: [PATCH 21/24] Update chapters/ru/chapter7/4.mdx Extra space. Co-authored-by: Maria Khalusova --- chapters/ru/chapter7/4.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/4.mdx b/chapters/ru/chapter7/4.mdx index 98843c395..1704d7051 100644 --- a/chapters/ru/chapter7/4.mdx +++ b/chapters/ru/chapter7/4.mdx @@ -77,7 +77,7 @@ DatasetDict({ }) ``` -У нас есть 210 173 пары предложений, но в одной части, поэтому нам нужно создать собственный проверочный набор. Как мы видели в [Главе 5] (/course/chapter5), у `Dataset` есть метод `train_test_split()`, который может нам помочь. Мы зададим seed для воспроизводимости: +У нас есть 210 173 пары предложений, но в одной части, поэтому нам нужно создать собственный проверочный набор. Как мы видели в [Главе 5](/course/chapter5), у `Dataset` есть метод `train_test_split()`, который может нам помочь. Мы зададим seed для воспроизводимости: ```py split_datasets = raw_datasets["train"].train_test_split(train_size=0.9, seed=20) From 5eb4f4be1320f29454758cc7def43e1bb2e4f1a0 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 21:02:46 +0300 Subject: [PATCH 22/24] Update 4.mdx Translated the missing comment in the code. --- chapters/ru/chapter7/4.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/4.mdx b/chapters/ru/chapter7/4.mdx index 1704d7051..51c3b6121 100644 --- a/chapters/ru/chapter7/4.mdx +++ b/chapters/ru/chapter7/4.mdx @@ -626,7 +626,7 @@ optimizer, schedule = create_optimizer( ) model.compile(optimizer=optimizer) -# Train in mixed-precision float16 +# Обучение со смешанной точностью float16 tf.keras.mixed_precision.set_global_policy("mixed_float16") ``` From e7c59a56dd985228fe3670799d9492e986fae319 Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Wed, 10 Jan 2024 21:23:18 +0300 Subject: [PATCH 23/24] Update 5.mdx Added and translated the missing sentence: "Since the collator expects a list of dicts, where each dict represents a single example in the dataset, we also need to wrangle the data into the expected format before passing it to the data collator:" --- chapters/ru/chapter7/5.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index a2930329b..0ca6b02d2 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -589,7 +589,7 @@ tokenized_datasets = tokenized_datasets.remove_columns( ) ``` -Давайте посмотрим, что выдает этот коллатор, когда ему передается небольшой батч примеров. Во-первых, нам нужно удалить столбцы со строками, потому что коллатор не будет знать, как вставлять эти элементы: +Поскольку коллатор ожидает список словарей `dict`, где каждый словарь `dict` представляет один пример в датасете, нам также необходимо привести данные к ожидаемому формату, прежде чем передавать их коллатору: ```python features = [tokenized_datasets["train"][i] for i in range(2)] From e09613a9e11bf67136672b707c984421b7b01cba Mon Sep 17 00:00:00 2001 From: Artyom Boyko Date: Thu, 11 Jan 2024 20:39:15 +0300 Subject: [PATCH 24/24] Update 5.mdx Edit the display of the table on the course page. --- chapters/ru/chapter7/5.mdx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chapters/ru/chapter7/5.mdx b/chapters/ru/chapter7/5.mdx index 0ca6b02d2..21fc15dbe 100644 --- a/chapters/ru/chapter7/5.mdx +++ b/chapters/ru/chapter7/5.mdx @@ -210,8 +210,7 @@ books_dataset = books_dataset.filter(lambda x: len(x["review_title"].split()) > | Модель Transformer | Описание | Многоязычная? | | :---------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----------: | | [GPT-2](https://huggingface.co/gpt2-xl) | Хотя GPT-2 обучен как авторегрессивная языковая модель, вы можете заставить его генерировать резюме, добавляя "TL;DR" в конце входного текста. | ❌ | -| [PEGASUS](https://huggingface.co/google/pegasus-large) | Использует цель предварительного обучения для предсказания замаскированных предложений в текстах с несколькими предложениями. Эта задача предварительного обучения ближе к суммаризации, чем к классическому языковому моделированию демонстрирует высокие результаты в популярных бенчмарках. - | ❌ | +| [PEGASUS](https://huggingface.co/google/pegasus-large) | Использует цель предварительного обучения для предсказания замаскированных предложений в текстах с несколькими предложениями. Эта задача предварительного обучения ближе к суммаризации, чем к классическому языковому моделированию демонстрирует высокие результаты в популярных бенчмарках.| ❌ | | [T5](https://huggingface.co/t5-base) | Универсальная трансформерная архитектура, которая формулирует все задачи в рамках преобразования текста в текст; например, входной формат модели для суммаризации документа - `summarize: ARTICLE`. | ❌ | | [mT5](https://huggingface.co/google/mt5-base) | Многоязыковая версия T5, предварительно обученная на многоязыковом корпусе Common Crawl (mC4), охватывающем 101 язык. | ✅ | | [BART](https://huggingface.co/facebook/bart-base) | Новая архитектура Transformer с кодером и стеком декодеров, обученных восстанавливать поврежденный входной сигнал, сочетает в себе схемы предварительного обучения BERT и GPT-2. | ❌ |