По данным IMDb, в нем более полумиллиона названий фильмов и почти 5,5 миллионов обзоров пользователей, каждый из которых имеет рейтинг от 1 до 10 и текстовый обзор; Я хочу использовать эти обзоры для обучения модели, которая может угадывать настроения будущих обзоров. Тип обработки естественного языка называется текстовой классификацией. Большая часть работы здесь взята из Урока 8 курса FastAI, который я не могу рекомендовать всем, кто интересуется глубоким обучением. Следуйте вместе с блокнотом, используемым для обучения модели.

Предварительная обработка

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

Есть два шага:

  1. Токенизация: создайте список слов/подслов для корпуса.
  2. Нумеризация: преобразуйте слова в числа, а их положение — в индекс.

Токенизация

Кажется довольно простым: передать текст токенизатору и начать обучение? К сожалению, нам нужно обрабатывать такие вещи, как пунктуация и переносы. Существуют разные способы разбиения текста на слова, но я сосредоточусь здесь на том, который основан на Word. К счастью, существуют токенизаторы слов, один из которых называется spacy. Он знает, как обрабатывать слова, такие как «это».

first(spacy(["It's really sunny on Monday"]))
>> ['It', "'s", 'really', 'sunny', 'on', 'Monday']

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

tok = Tokenizer(spacy)(["It's really sunny on Monday"]); tok
>> ['xxbos', 'xxmaj', 'it', "'s", 'really', 'sunny', 'on', 'xxmaj', monday']

xxbos означает начало потока (bos), а xxmaj — заглавную букву. Такие слова, как «Оно» и «оно», для нас одно и то же, но для модели они уникальны. Эти специальные токены анализируют их как одинаковые, но с токеном с заглавной буквы, чтобы различать их.

Нумерация

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

num = Numericalize(min_freq=3, max_vocab=60000)
num.setup(tok)
num(tok)
>> tensor([ 4, 11, 435, 434, ... ])

Сохранение всех слов нецелесообразно, потому что словарный запас будет слишком большим, его ограничивает свойство max_vocab, а min_freq требует, чтобы слова появлялись несколько раз перед добавлением. Этот тензор теперь может быть загружен непосредственно в слой внедрения сети.

RNN

Рекуррентные нейронные сети используются в НЛП, потому что они взвешивают положение текста в последовательности. Основная идея заключается в том, что первый уровень сети использует встраивание первого слова; второй использует встраивание второго слова в сочетании с выходными активациями первого слова; третий использует вложение третьего слова с активациями второго слова. Активации предыдущего слова называются скрытым состоянием.

Вместо того, чтобы явно объявлять каждый уровень, они реорганизуют их в цикл for — это то, что повторяется. Ограничение цикла for — это количество слоев.

Первый класс выше, LanguageModel, явно передает следующему слою каждой сети предыдущие активации; LanguageModelRecurrent использует цикл для реализации того же самого. Приведенная выше модель обеспечивает точность 50 %.

Состояние и сигнал

В forward() скрытое состояние h сохраняет предыдущие активации; затем он сбрасывается на ноль, отбрасывая потенциально важную информацию о словах, которые дают больше контекста обзору. Перемещение сброса в init() решает эту проблему за счет сброса только при создании экземпляра объекта. Это сохраняет состояние, но открывает другую проблему: взрыв градиента. Сохранение состояния создает слой для каждого токена в корпусе, которых может быть 60 000, что требует вычислений градиента для каждого слоя, что замедляет обучение. Эту проблему решает отбрасывание всех градиентов, кроме первых трех; это называется усеченным обратным распространением во времени (tBPTT).

Вторая вышеприведенная реализация RNN добавляет сигнал, перебирая длину последовательности sl вместо трех слов. Увеличение длины предложения дает больше контекста; потенциально повышая точность — это называется сигнализацией. Добавление состояния увеличило точность до 57 %, а добавление дополнительного сигнала — до 64 %.

Многослойные и LSTM

Многослойные RNN берут выходные данные первого слоя и вводят их в следующий, предоставляя модели более длительный временной горизонт для обучения и улучшая понимание текста. Учитывая это, точность должна значительно улучшиться, но вместо этого она снизилась до 48%, что на 16% меньше, чем в нашей последней однослойной модели. Причиной этого падения являются явления, называемые взрывным и исчезающим градиентами. Многократное умножение матриц приводит к снижению их точности из-за того, что каждое число с плавающей запятой имеет только 32 бита; поскольку числа с плавающей запятой отклоняются от 1, они теряют точность, поскольку для хранения их значения требуется больше битов. Прохождение матрицы через два слоя приводит к тому, что ее числа расходятся с ее фактическим значением при каждом умножении. Если градиенты слишком малы, алгоритм не обновляется; слишком большие, и их обновления слишком радикальны.

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

Оранжевые прямоугольники на диаграмме выше представляют слои в сети, tanh – гиперболический tan, а другой – сигмоидальный. Масштабирование выходных данных от 0 до 1 для sigmoid и от -1 до +1 для tanh решает проблему взрывающихся градиентов. состояние ячейкиуправляет LSTM,который обновляется с помощью желтых кружков на диаграмме; произведение их входов определяет состояние: обновить тех, кто ближе к 1; отбросить ближе к 0. Сеть теперь может поддерживать долговременную память слов, что облегчает понимание длинных предложений.

init()определяет каждый вентиль, используемый в LSTM, а forward() реализует их. Удобно, что в PyTorch есть встроенный класс, так что не нужно все это писать. Благодаря LSTM точность многослойной модели увеличилась с 48% до 81% — довольно большой скачок! Потери при проверке модели были намного выше, чем при обучении, что свидетельствует о переобучении данных.

Регуляризация

В традиционных моделях ML этот шаг достаточно прост: к функции потерь добавляется только член регуляризации:

В НЛП процесс немного сложнее: некоторые подходы используют увеличение данных, перевод текста на другой язык и его повторный перевод в фразовые предложения другим способом; это открытая область исследований, которая выходит за рамки данной статьи.

Выпадение

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

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

Dropout реализован в PyTorch:

class Dropout(Module):
  def __init__(self):
    self.p = p
  
  def forward(self, x):
    if not self.training:
      return x
    mask.new(*x.shape).bernoulli_(1-p)
    return x * mask.div_(1-p)

Привязка веса

Входные вложения языковой модели (первый слой) сопоставляют английские слова с активациями, а выходные активации — с английскими словами. Установка одинаковых значений может повысить точность. Вот бумага.

self.h_o.weight = self.i_h.weight

FastAI предоставляет класс TextLearner, который сделает за нас большую часть работы:

learn = TextLearner(dls, LanguageModelLSTM(len(vocab), 64, 2, 0.4),
                loss_func=CrossEntropyLossFlat(), metrics=accuracy)
learn.fit_one_cycle(15, 1e-2, wd=0.1)

Точность увеличилась с 81 % до 87 %; это было бы лучше всего в мире пять лет назад! Создатели FastAI обучили модель, используя те же методы, что и выше, чтобы достичь точности 94% — только недавно побитой.

Прогноз

FastAI предоставляет набор данных IMDb с 25 000 поляризованных обзоров, доступ к которым можно получить с помощью довольно простого синтаксиса:

path = untar_data(URLs.IMDB)

DataBlockиспользует path для загрузки примеров в модель.

dls_clas = DataBlock(
  blocks=(TextBlock.from_folder(path), CategoryBlock),
  get_y=parent_label,
  get_items=partial(get_text_files, folders=['train', 'test']),
  splitter=GrandparentSplitter(valid_name='test')
).dataloaders(path, path=path, bs=128, seq_len=72)

При создании модели используются DataBlock,AWD_LSTM: регуляризованная архитектура LSTM и drop_multi: глобальное отсев. to_fp16()преобразует все 32-битные числа с плавающей запятой в 16-битные, что ускоряет обучение.

l3 = text_classifier_learner(
  dls_clas, 
  AWD_LSTM,
  drop_mult=0.5,
  metrics=accuracy
).to_fp16()

Я рекомендую сохранить обученную модель и словарный запас в pickle файлах, так как на их обучение на графических процессорах Colab ушло более двух часов. Загрузка модели и создание прогнозов выполняется ниже:

l3 = l3.load('/path/to/my_saved_model')
l3.predict('That was terrible!')
 >> ('neg', tensor(0), tensor([0.8067, 0.1933]))

Хостинг

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

использованная литература