Читать нас в Telegram

Введение

Fine-Tuning — это способ улучшить предварительно обученную модель, которая уже имеет некоторые знания, путем небольших корректировок. Тонкая настройка помогает модели лучше работать над конкретной задачей, не обучая ее с самого начала. О том, как именно происходит дообучение, можно почитать в нашем блоге о трансферном обучении

Зачем нужен fine-tuning в сфере обработки естественного языка

Во-первых, fine-tuning экономит ресурсы и время. Чтобы качественно обучить нейросеть, всей структуре языка нужны огромные вычислительные мощности,а также корпус всех текстов, которые удастся собрать с Интернета. Конечно, эта задача достаточно сложна, и обычному исследователю вряд ли удастся в одиночку создать целую языковую модель. Fine-tuning позволяет нам не изобретать велосипед, а разрабатывать что-то новое на основе уже полученных навыков. 

Во-вторых, fine-tuning — это весело! Языковую модель можно обучить генерировать тексты в самых разных стилях: от комментариев из Одноклассников до прозы Набокова. Все, что нам нужно — данные. Для fine-tuning достаточно нескольких мегабайтов текстов, что примерно эквивалентно 10-15 произведениям. 

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

Настройка окружения и выбор модели

Дообучение любых нейросетей требует вычислительные мощности, то есть GPU (видеокарты). Видеокарты позволяют эффективно распараллеливать вычисления, необходимые для обучения моделей. Чтобы выполнить наш гайд мог каждый, мы будем работать в Google Colab. Главное его преимущество — возможность бесплатно работать с видеокартой, которая подходит для работы с небольшими моделями.

Итак, первым делом надо выбрать GPU. Для этого пройдем по такому пути: «Изменить» —> «Настройки блокнота» —> «Системный ускоритель» —> выберите «GPU». Если у вас мощная видеокарта, можете выбрать любое удобное вам окружение.

 Наша задача — генерация в стиле Достоевского на русском языке. Для этого нам, конечно, нужна русскоязычная модель. Нам повезло — команда Сбера выложила модель ruGPT3. Эта языковая модель основана на архитектуре GPT-2: как она работает и чем хороша, мы рассказывали ранее. Существует четыре версии ruGPT3, которые различаются по размерам. Мы будемиспользовать самую маленькую модель, чтобы вместить ее в память Google Colab. 

Сначала нам нужно установить библиотеку transformers. Библиотека Transformers — это набор инструментов и функций для работы с моделями с архитектурой transformer. Простыми словами, эта библиотека помогает пользователям работать с нейросетями без необходимости писать много кода или иметь глубокие знания в области машинного обучения.

%%bash
git clone https://github.com/huggingface/transformers
cd transformers
pip install .

Кроме того, установим библиотеки datasets и evaluate. Эти библиотеки нужны для правильной работы обучения. Datasets приводят в нужный формат данные, а evaluate необходим для скрипта fine-tuning.

!pip3 install datasets
!pip3 install evaluate

Следующие шаги — это создание директории для нашей будущей модели, а также скачивание ruGPT3. Мы скачиваем эту языковую модель с GitHub Сбера. 

# Создадим папку, где будет храниться наша будущая модель
!mkdir models/
# Скачаем ruGPT3 и скрипт обучения
!wget https://raw.githubusercontent.com/sberbank-ai/ru-gpts/master/pretrain_transformers.py
!wget https://raw.githubusercontent.com/sberbank-ai/ru-gpts/master/generate_transformers.py
!wget https://raw.githubusercontent.com/huggingface/transformers/main/examples/pytorch/language-modeling/run_clm.py

Данные

Мы возьмем готовый корпус, состоящий из 34 произведений Достоевского. В него входит все Великое Пятикнижие кроме «Подростка», маленькие повести и рассказы. Этого объема (16.43 Мб) хватит, чтобы дообучить нейросеть переносить стиль. Поскольку мы работаем с нейронной сетью, никакой типичной предобработки не нужно: пунктуация и стоп-слова в нашем случае — важные для запоминания нейросетью элементы языка.

Скачаем готовый корпус и затем откроем его:

!wget https://gitlab.com/z00logist/artificial-dostoevsky/-/raw/main/data/corpus.txt
# Откроем датасет
with open('./corpus.txt', 'r') as file:
   dataset = file.read()

Данные из Интернета не всегда качественные, поэтому проверим, все ли с ними впорядке. Для этого посмотрим на первые символы корпуса.

# Убедимся, что с ним все впорядке
dataset[:1000]

Вывод будет таким:

Ох уж эти мне сказочники! Нет чтобы написать что-нибудь полезное, приятное, усладительное, а то всю подноготную в земле вырывают!.. Вот уж запретил бы им писать! Ну, на что это похоже: читаешь... невольно задумаешься, а там всякая дребедень и пойдет в голову; право бы, запретил им писать; так-таки просто вовсе бы запретил.Кн. В Ф. Одоевский Апреля.Бесценная моя Варвара Алексеевна! Вчера я был счастлив, чрезмерно счастлив, донельзя счастлив! Вы хоть раз в жизни, упрямица, меня послушались. Вечером, часов в восемь, просыпаюсь (вы знаете, маточка, что я часочек-другой люблю поспать после должности), свечку достал, приготовляю бумаги, чиню перо, вдруг, невзначай, подымаю глаза, право, у меня сердце нот так и запрыгало! Так вы-таки поняли, чего мне хотелось, чего сердчишку моему хотелось! Вижу, уголочек занавески у окна вашего загнут и прицеплен к горшку с бальзамином, точнехонько так, как я вам тогда намекал; тут же показалось мне, что и личико ваше мелькнуло у окна, что и вы ко мне из

Похоже, все хорошо! Мы видим эпиграф к «Бедным людям» и начало романа.

Скриншот с сайта Интернет Библиотеки Алексея Комарова

Скрипт Обучения

Авторы библиотеки transformers от HuggingFace написали специальный скрипт для обучения языковых моделей. Использование этого скрипта в нашем случае наиболее оптимальное, хотя ruGPT3 можно обучать и классическим способом, как, например, в этом блоге от Hugging Face.

Скрипт выглядит так: 

!python run_clm.py \
   --model_name_or_path sberbank-ai/rugpt3small_based_on_gpt2 \
   --train_file corpus.txt \
   --per_device_train_batch_size 8 \
   --block_size 2048 \
   --dataset_config_name plain_text \
   --do_train \
   --gradient_accumulation_steps 4 \
   --gradient_checkpointing True \
   --fp16 True \
   --optim adafactor \
   --num_train_epochs 7 \
   --output_dir models/essays \
   --overwrite_output_dir

Разберем построчно, что происходит в этом cкрипте.

  • !python run_clm.py: Эта строка — команда, которая запускает скрипт на языке Python с именем «run_clm.py» (то есть скрипт обучения ruGPT3). Восклицательный знак перед командой нужен в некоторых средах, таких как Jupyter Notebook или Google Colab. Он говорит среде выполнения, что это не обычный код на Python, а команда для выполнения в терминале. После этой команды мы указываем параметры обучения, которые выделены двумя дефисами.
  • model_name_or_path: путь к модели, которую хотим дообучить. В нашем случае — это ruGPT3small.
  • train_file: указывает файл с текстом для обучения модели, в данном случае «corpus.txt».
  • per_device_train_batch_size: число, равное количеству примеров из обучающей выборки, которые обрабатываются за один шаг модели. Обучение состоит из последовательности шагов, на каждом из которых набор примеров разный. Именно поэтому целесообразно делать batch_size как можно больше. Увеличение размера пакета может ускорить обучение, так как одновременно происходит обработка большего числа примеров, однако это требует больше памяти на GPU. В данном случае мы выбираем batch size = 8, что означает, что модель будет обрабатывать восемь обучающих примеров за раз. 
  • block_size: определяет максимальное количество токенов (слов или частей слов), которые модель может обрабатывать одновременно. Это ограничение связано с архитектурой модели и доступными вычислительными ресурсами. В данном случае мы выбираем размер блока = 2048 токенов. Если входные данные превышают этот размер, они будут обрезаны (truncation) или разделены на несколько блоков. Важно выбрать оптимальный размер блока, чтобы модель могла эффективно обрабатывать данные без потери информации и без излишнего использования ресурсов.
  • dataset_config_name: указывает тип конфигурации набора данных для обучения, у нас «plain_text» (обычный текст, текстовый файл).
  • do_train: говорит скрипту, что нужно выполнить обучение модели.
  • num_train_epochs: указывает количество эпох (полных проходов по обучающему набору данных) во время обучения. 
  • output_dir: задает директорию для сохранения обученной модели и ее настроек. Мы сохраняем в ту директорию, которую создали в самом начале ноутбука.

Отдельно стоит разобрать четыре параметра оптимизации: gradient_accumulation_steps, gradient_checkpointing, fp16, optim. Все эти параметры имеют цель либо ускорить процесс обучения, либо уменьшить вычислительные затраты, либо оптимизировать оба этих процесса одновременно. Мы не будем вдаваться в подробности их работы, поскольку это требует математического объяснения, но проговорим, для чего конкретно нужен каждый параметр. 

  • gradient_accumulation_steps — увеличение этого параметра сокращает необходимые для обучения вычислительные затраты, но при этом замедляет сам процесс.
  • gradient_checkpointing — это техника, позволяющая сократить потребление памяти во время обучения. Она особенно полезна при работе с моделями с большим числом параметров и недостаточным объемом памяти видеокарты.
  • fp16 — использование 16-битных чисел с плавающей точкой (half-precision) вместо стандартных 32-битных чисел (single-precision) для представления весов и градиентов модели. Это позволяет сократить объем используемой памяти и ускорить обучение, но может внести небольшую потерю точности.
  • optim — выбор алгоритма оптимизации. Наиболее распространенными оптимизаторы: Adam, Adagrad, RMSprop и SGD. По совету блога от HuggingFace мы выберем Adafactor, что значительно ускорило процесс обучения.

Запуская ячейку этого кода, мы ставим на обучение нашу модель. С этими параметрами оно займет примерно два часа. Пока можно пойти пить чай и иногда проверять, что все работает. 

Генерация

Итак, если наша ячейка выполнилась и на экране появился текст «training complete», значит, модель дообучилась. Теперь начинается самое интересное — проверка результатов, а точнее генерация.

Для начала нужно импортировать библиотеки numpy и torch, чтобы сделать результат генерации воспроизводимым. Кроме того, импортируем GPT2LMHeadModel и GPT2Tokenizer

  • GPT2LMHeadModel — это структура нейронной сети на базе архитектуры GPT-2, которая используется для работы с текстом.
  • GPT2Tokenizer — Токенизатор GPT2 разбивает текст на токены, которые модель GPT-2 может понимать и обрабатывать (encoding). Он также выполняет обратное преобразование из токенов в текст после того, как модель сгенерировала свой вывод (decoding). 
import numpy as np
import torch
from transformers import GPT2LMHeadModel, GPT2Tokenizer

# Сделаем генерацию воспроизводимой
np.random.seed(42)
torch.manual_seed(42)

Далее, загрузим токенизатор и модель из нашей директории. Модель также переводим на GPU.

tokenizer = GPT2Tokenizer.from_pretrained("models/essays")
model = GPT2LMHeadModel.from_pretrained("models/essays")

# Переведем работу модели на GPU
model.cuda()

Чтобы сгенерировать какой-либо текст, нам нужно предоставить модели затравку (prompt), то есть входной текст, который она должна продолжить. Эту подводку мы сначала кодируем на понятный модели «язык», а затем задаем параметры генерации. Как результат, мы получаем закодированный сгенерированный текст, который мы затем должны декодировать с помощью простой команды. Давайте рассмотрим, как это происходит в коде. Подаем текст, с которого хотим, чтобы начиналась генерация, например, «кофе».

# Задаем желаемое начала текста, то есть затравку
text = "Кофе"
# Закодируем затравку на "язык" модели
inpt = tokenizer.encode(text, return_tensors="pt")

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

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

out = model.generate(inpt.cuda(),
                     max_length=500,
                     repetition_penalty=6.0,
                     do_sample=True,
                     top_k=5,
                     top_p=0.95,
                     temperature=1,
                     no_repeat_ngram_size=2)

Мы заранее выбрали параметры, но вы можете сами попробовать изменять их и смотреть, как они влияют на вывод. Давайте разберем представленные аргументы более подробно:

  • inpt.cuda(): Входные данные (inpt) для модели, перенесенные на видеокарту (GPU) с помощью метода .cuda() для ускорения вычислений.
  • max_length=500: Максимальное количество токенов в сгенерированном тексте. В данном случае, модель будет генерировать текст не длиннее 500 токенов, что после GPT-токенизации на русском будет равняться примерно 300-350 словам.
  • repetition_penalty=6.0: Штраф за повторение, который уменьшает вероятность повтора слов или фраз. Чем выше значение, тем меньше повторов в сгенерированном тексте.
  • do_sample=True: этот параметр позволяет сделать генерацию более разнообразной. 
  • top_k=5: Ограничивает количество возможных следующих токенов до top_k (в нашем случае до 5) на каждом шаге генерации, основываясь на их вероятностях. На каждом из шагов выбирается один токен из этих кандидатов. 
  • top_p=0.95: Устанавливает порог вероятности для отсечения токенов, которые не должны быть учтены при генерации. В данном случае берется минимальный по размеру набор токенов, сумма вероятностей которого не меньше p = 95%.
  • temperature=1: Параметр температуры влияет на случайность генерации текста. Чем выше значение, тем более случайным и разнообразным и «креативным» будет текст. Чем ниже значение, тем более детерминированным и предсказуемым будет текст.
  • no_repeat_ngram_size=2: Запрещает повторение n-грамм определенного размера в сгенерированном тексте. В данном случае, модель не будет повторять биграммы, то есть последовательность из двух слов. 

Некоторые результаты

Поскольку мы не использовали токен, который показывает, где конец текста (eos token), наша модель генерирует бесконечно и мы сами определяем, где «нужные» границы.

Мы подготовили некоторые удачные примеры. Вот, например, что сгенерировала ruGPT3 с кофейной затравкой:

Кофею, а? Нет-с. Не надо; да и не нужно...

Модель уловила такие архаичные формы, как «кофею» и словоерс «нет-с». А вот что она сгенерировала с затравкой «Чай»:

Чайник? Нет, не чай! А вот что-с... да ведь ты и сам знаешь: я теперь на покой.

Заметна одинаковая структура: вопрос и отрицательный ответ. Похоже, чай и кофе находятся на одном семантическом поле, поэтому модель и генерирует похожие выводы.

Теперь посмотрим на более длинные примеры генерации:

Кажется, модель не уловила оформление диалогической речи, зато отлично поняла характерную для Достоевского экспрессию повествования: 

Я вышел за хлебом к булочнику. Я помню, как он схватил меня сзади и потащил куда-то; но я не хотел идти туда... И вдруг мы очутились на площади: это был тот самый сквер с фонтаном в саду у Марфы Петровны (там теперь ее дом). Вот этот сад! Это то самое место было тогда здесь во время пожара вместе со мною под судом? Так ты помнишь его?.. Ну что ж делать!.. Мы вошли прямо через калитку сада вниз до ворот дома Филиппова..

В этом примере отлично видна атмосфера тревоги, непостоянства и страха. Как будто, это отрывок из «Преступления и наказания», а героиня — Сонечка Мармеладова:

Она не знала, что и сказать. Она даже боялась за него: вдруг он опять станет для нее чужим? Он говорил с ней как-то странно...Я вам всё расскажу; я только хочу узнать правду! Вы знаете это или нет?.. А впрочем лучше уж ничего говорить нельзя было!..

И опять нерешительность и неопределенность…

Он вышел с видом необыкновенного достоинства и даже несколько робости в лице; но как бы не решаясь войти или уйти из комнаты: «А впрочем... пожалуй...» — подумал он про себя.- Впрочем что ж? Ведь это только начало! И зачем ему было приходить ко мне теперь?..

А в этом отрывке мы видим экзистенциальные и религиозные мотивы, типичные для Достоевского. Однако, под конец фрагмент становится немного несвязным:

Однажды я видел, что у него на лице написано было раскаяние; но это не так. Я заметил в его взгляде решимость и силу: ему хотелось спасти себя от позора или смерти... Нет-с! выслушайте меня до конца (франц.). Он был человек благородный по природе своей!.. Но как он смел? Что за охота говорить такие вещи?.. Как мог этот мальчишка осмелиться оскорблять старика таким тоном даже после такого признания?! И наконец последний вопрос для нас обоих мучительнее предыдущего раза нашего свидания накануне нашей разлуки..

В целом, генерации текста демонстрируют относительно неплохое качество, сохраняя характерный стиль и манеру автора. Однако есть некоторые недостатки:

  • Структура предложений может быть сложной и запутанной, что иногда затрудняет понимание текста.
  • Хотя текст содержит достаточно описательных деталей, некоторые сюжетные моменты и взаимоотношения персонажей не выглядят последовательными.
  • В тексте иногда возникают небольшие несогласованности — например, переходы между героями или временем — что может привести к некоторой неясности в событиях.

Несмотря на эти недостатки, генерация текста сохраняет атмосферу и настроение, характерное для произведений Достоевского, и успешно передает стилистические особенности его произведений (очень длинные предложения). 

В этом гайде мы разобрались, как дообучить языковую модель на своей обучающей выборке. Мы стремились создать универсальный код, который позволит вам продолжать экспериментировать с различными данными: наряду с Достоевским и литературными произведениями, вы также можете опробовать различные социальные данные, такие как комментарии и отзывы. Более того, мы использовали самую маленькую версию ruGPT3: если у вас есть графический процессор, вы можете запустить наш процесс файн-тюнинга на нем и сравнить качество генерации версиями Small и Medium.

Мы надеемся, что наше руководство помогло разобраться в том, как функционирует файн-тюнинг в области NLP, и продемонстрировало, что для небольших проектов достаточно использовать Google Colab и иметь небольшой объем данных!