Если вы когда-либо пытались настроить модель на основе трансформаторов, такую ​​​​как GPT2, вы, должно быть, столкнулись с множеством проблем, как и я. Будь то подготовка набора данных, создание загрузчика данных, добавление токенов, создание масок внимания или создание пользовательского цикла обучения (если вы такой же стойкий пользователь PyTorch, как и я), нам требуется довольно много времени, чтобы в состоянии сделать это. Кроме того, отсутствие онлайн-ресурсов (на момент написания этого блога), которые точно соответствовали бы нашему варианту использования, является большой проблемой для нас в достижении того, что мы хотим!

Недавно я столкнулся с FastAI и был очень впечатлен его возможностями и простыми в использовании API. В этой статье мы рассмотрим, как можно настроить языковую модель на основе GPT2 для набора данных wikitext-2.

Примечание. Код и несколько пояснений взяты из официальной документации FastAI.

Библиотека Трансформеров

Начнем с установки библиотеки трансформаторов:

!pip install -Uq transformers

Здесь мы настроим предварительно обученную модель GPT2 и настроим викитекст-2. Давайте импортируем GPT2LMHeadModel и GPT2Tokenizer для подготовки данных.

from transformers import GPT2LMHeadModel, GPT2TokenizerFast

Давайте загрузим предварительно обученные веса модели GPT2 и токенизатора, используя следующий код:

pretrained_weights = 'gpt2'
tokenizer = GPT2TokenizerFast.from_pretrained(pretrained_weights)
model = GPT2LMHeadModel.from_pretrained(pretrained_weights)

Если вы ранее не работали с объектом токенизатора, токенизаторы в HuggingFace обычно выполняют токенизацию и числовую обработку за один шаг.

ids = tokenizer.encode('Hi my name is Sahil Sheikh')
print(ids)

# output: [17250, 616, 1438, 318, 22982, 346, 30843]

Предложение кодируется в список «чисел», где каждое число представляет собой токен в предложении.

Чтобы декодировать предложение обратно в его исходную форму, мы можем использовать:

tokenizer.decode(ids)
# output: Hi my name is Sahil Sheikh

Давайте сначала протестируем модель, прежде чем настраивать ее с помощью нашего набора данных. Для тестирования нашей модели мы будем использовать PyTorch (мой любимый :D).

import torch
t = torch.LongTensor(ids)[None]
preds = model.generate(t)

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

Строка t = torch.LongTensor(ids)[None] создает тензор из списка ids (закодированное предложение) и изменяет его форму. [None] используется для добавления дополнительного измерения к тензору.

Прогнозы по умолчанию имеют длину 20:

# You can check the shape of the predictions and the predictions itself using
# the below code
print(preds.shape,preds[0])

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

tokenizer.decode(preds[0].numpy())
# output: Hi my name is Sahil Sheikh, I am a Muslim. 

# This is literally what GPT2 returned xD

Преодоление разрыва с помощью фастая

Теперь давайте посмотрим, как мы можем использовать fastai для тонкой настройки этой модели на wikitext-2, используя все обучающие утилиты (поиск скорости обучения, политика 1cycle и т. д.).

from fastai.text.all import *

Затем мы загружаем набор данных (если его нет), он поставляется в виде двух файлов csv:

path = untar_data(URLs.WIKITEXT_TINY)
path.ls()

Вы можете просмотреть данные с помощью метода .head():

df_train = pd.read_csv(path/'train.csv', header=None)
df_valid = pd.read_csv(path/'test.csv', header=None)
df_train.head()

Далее собираем все тексты в один numpy массив (поскольку с фастаем так будет проще):

all_texts = np.concatenate([df_train[0].values, df_valid[0].values])

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

В фасте Transform можно определить:

  • метод encodes, который применяется при вызове преобразования
  • метод decodes, который применяется, когда вы вызываете метод decode преобразования, если вам нужно декодировать что-либо для демонстрации (например, преобразование идентификаторов в текст здесь)
  • метод setups, который устанавливает некоторое внутреннее состояние Transform (здесь он не нужен, поэтому мы его пропустим)
class TransformersTokenizer(Transform):
    def __init__(self, tokenizer): self.tokenizer = tokenizer
    def encodes(self, x): 
        toks = self.tokenizer.tokenize(x)
        return tensor(self.tokenizer.convert_tokens_to_ids(toks))
    def decodes(self, x): return TitledStr(self.tokenizer.decode(x.cpu().numpy()))

Затем вы можете сгруппировать свои данные с этим Transform, используя TfmdLists. Он содержит как набор для обучения, так и набор для проверки. Мы указываем индексы обучающего набора и набора проверки с помощью splits (здесь все первые индексы до len(df_train), а затем все остальные индексы):

Мы можем посмотреть оба декодирования, используя метод «show_at»:

show_at(tls.train, 0)

Библиотека fastai ожидает, что данные будут собраны в объект DataLoaders. Мы можем получить его, используя метод dataloaders. Мы должны указать размер пакета и длину последовательности, чтобы определить загрузчик данных. Мы будем тренироваться с последовательностями размером 256 (GPT2 использовал длину последовательности 1024, вы можете увеличить размер с 256 до 1024, если ваш GPU позволяет):

bs,sl = 4,256
dls = tls.dataloaders(bs=bs, seq_len=sl)

Другой способ собрать данные — предварительно обработать тексты раз и навсегда и использовать преобразование только для декодирования тензоров в тексты:

def tokenize(text):
    toks = tokenizer.tokenize(text)
    return tensor(tokenizer.convert_tokens_to_ids(toks))

tokenized = [tokenize(t) for t in progress_bar(all_texts)]

Нам также нужно изменить Tokenizer, например:

class TransformersTokenizer(Transform):
    def __init__(self, tokenizer): self.tokenizer = tokenizer
    def encodes(self, x): 
        return x if isinstance(x, Tensor) else tokenize(x)
        
    def decodes(self, x): return TitledStr(self.tokenizer.decode(x.cpu().numpy()))

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

tls = TfmdLists(tokenized, TransformersTokenizer(tokenizer), splits=splits, dl_type=LMDataLoader)
dls = tls.dataloaders(bs=bs, seq_len=sl)

Тонкая настройка модели

Модель HuggingFace вернет кортеж в выходных данных с фактическими прогнозами и некоторыми дополнительными активациями. Чтобы работать внутри тренировочного цикла fastai, нам нужно удалить те, которые используют Callback: мы используем их, чтобы изменить поведение тренировочного цикла.

Здесь нам нужно написать событие after_pred и заменить self.learn.pred (которое содержит прогнозы, которые будут переданы в функцию потерь) только его первым элементом. В обратных вызовах есть ярлык, который позволяет вам получить доступ к любому из базовых атрибутов Learner, поэтому мы можем написать self.pred[0] вместо self.learn.pred[0]. Этот ярлык работает только для чтения, а не для записи, поэтому мы должны написать self.learn.pred справа (иначе мы бы установили атрибут pred в Callback).

class DropOutput(Callback):
    def after_pred(self): self.learn.pred = self.pred[0]

Теперь мы готовы создать наш Learner, который представляет собой объект fastai, группирующий данные, модель и функцию потерь и отвечающий за обучение или вывод модели. Поскольку мы находимся в настройках языковой модели, мы передаем недоумение в качестве метрики, и нам нужно использовать только что определенный обратный вызов. Наконец, мы используем смешанную точность, чтобы сохранить каждый бит памяти, который мы можем (и если у вас есть современный графический процессор, это также ускорит обучение):

learn = Learner(dls, model, loss_func=CrossEntropyLossFlat(), cbs=[DropOutput], metrics=Perplexity()).to_fp16()

Мы можем сделать Learn.lr_find(), чтобы найти оптимальную скорость обучения для процесса тонкой настройки.

Кривая скорости обучения предлагает выбрать что-то между 1e-4 и 1e-3.

Чтобы начать процесс тонкой настройки, запустите следующий код:

learn.fit_one_cycle(1, 1e-4)

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

Вот и все!

Заключение

Нам, специалистам по данным, нелегко оставаться в курсе ВСЕХ существующих библиотек. Моя цель - найти такие библиотеки и API и представить их вам, чтобы вы могли использовать их, когда ПРИХОДИТ ПОДХОДЯЩЕЕ ВРЕМЯ :D

Если вам понравилась статья, подписывайтесь на меня на Medium :D

Спасибо :D