Построение языковой модели уровня персонажа с помощью Flax

Некоторые рождаются великими, некоторые достигают величия, а на кого-то возложено величие.

Уильям Шекспир, Двенадцатая ночь, или что угодно

Несколько месяцев назад Google Researcher представил Flax, новую восходящую звезду машинного обучения. С тех пор многое произошло, и предварительный релиз значительно улучшился. Мои собственные эксперименты с CNN на Flax приносят плоды, и я до сих пор удивляюсь гибкости по сравнению с Tensorflow. Сегодня я покажу вам применение RNN во Flax: Модель языка на уровне символов.

Во многих задачах обучения нам не нужно учитывать временные зависимости от предыдущих входных данных.

Но что мы можем сделать, если у нас нет независимых входных и выходных векторов фиксированного размера? Что, если у нас есть последовательности векторов? Решением являются рекуррентные нейронные сети. Они позволяют нам работать с последовательностями векторов, как описано ниже.

Рекуррентная нейронная сеть

На картинке выше вы можете увидеть различные типы входных и выходных архитектур:

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

Основное преимущество RNN заключается в том, что они полагаются не только на текущий ввод, но и на предыдущие вводы.

RNN - это ячейка с внутренним скрытым состоянием h, инициализированная нулями в зависимости от скрытого размера. На каждом временном шаге t мы вставляем вход x_t в нашу RNN-ячейку и обновляем также скрытое состояние. Теперь на следующем временном шаге t + 1 скрытое состояние снова инициализируется не нулями, а предыдущим скрытым состоянием. Таким образом, RNN позволяют хранить информацию о нескольких временных шагах и генерировать последовательности.

Модель языка на уровне персонажа

С нашими новыми знаниями мы хотим создать первое приложение для нашей RNN. Модель языка на уровне персонажей является основой для многих задач, например создание подписей к изображениям или текста. На вход RNN-ячеек поступают огромные фрагменты текста в виде последовательности символов. Теперь обучающая задача - научиться предсказывать следующего персонажа, учитывая последовательность предыдущих символов. Таким образом, мы генерируем по одному символу на каждом временном шаге t, а наши предыдущие символы - это x_t-1, x_t-2,….

В качестве примера возьмем слово FUZZY в качестве нашей обучающей последовательности, теперь словарь теперь {‘f’, ’u’, ’z’, ’y’}. Поскольку RNN работает только с векторами, мы конвертируем все символы в так называемые горячие векторы. Один горячий вектор состоит из нулей с единицей, основанной на позиции в словаре, для «Z» преобразованный вектор равен [0,0,1,0]. На следующем рисунке вы можете увидеть пример для данного ввода «FUZZ», и мы хотим предсказать конец слова «UZZY». Скрытый размер наших нейронов равен четырем, и мы хотим, чтобы зеленые числа в выходном слое были высокими, а красные - низкими.

Если вас интересует математика, лежащая в основе RNN, перейдите по ссылке.

Наконец, мы кодируем

Обратите внимание, что я объяснил некоторые основные концепции Flax в предыдущей статье о CNN. В качестве набора данных мы используем крошечный Шекспир, который состоит из таких разговоров:

ЭДВАРД:

Это даже так; но ты все еще Уорик.

Глостер:

Пойдем, Уорик, не торопись; встать на колени, преклонить колени: нет, когда? ударь сейчас, а то железо остынет.

Я снова использовал Google Colab для обучения, поэтому нам снова нужно установить необходимые PIP-пакеты:

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

Теперь мы готовы создать нашу RNN с нуля:

В реальной ситуации обучения, подобной этой, мы используем не ванильные клетки RNN, а клетки LSTM. Это дальнейшее развитие, которое может лучше решить проблему исчезающего градиента. Чтобы добиться более высокой точности, я использую три уложенных друг на друга LSTM-Cells. Очень важно, чтобы мы передавали вывод первой ячейки в следующую, а также инициализировали каждую LSTM-ячейку собственным скрытым состоянием. В противном случае мы потеряем из виду временные зависимости.

Выход последней LSTM-ячейки дан в нашем плотном слое. Плотный слой имеет размер нашего словаря. В нашем предыдущем примере с «FUZZY» количество нейронов было бы четыре. Если «FUZZ» задан как вход для нашей RNN, нейроны в лучшем случае должны выдавать такой результат, как [1.7,0.1, -1.0,3.1], потому что этот вывод указывает «Y» как наиболее вероятный символ.

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

Вот эти случаи:

  • Режим обучения, в котором мы хотим научиться предсказывать
  • Predict-Mode, где мы фактически отбираем некоторый текст

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

Каждая из наших последовательностей имеет длину 50 символов, а у нас есть словарь из 65 различных символов.

В качестве оптимизатора для нашей RNN я выбрал Adam Optimizer с начальной скоростью обучения 0,002 и уменьшением веса, чтобы избежать слишком больших весов.

Режим обучения

В режиме обучения мы загружаем пакет из 32 последовательностей в нашу RNN. Каждая последовательность взята из нашего набора данных и содержит две подпоследовательности, одна с символами от 0 до 49, а другая с символами от 1 до 50. С помощью этого простого разделения наша сеть может узнать наиболее вероятный следующий символ. В каждом пакете мы инициализируем скрытые состояния и передаем последовательности в нашу RNN.

В нашем методе обучения у нас есть две подфункции. Loss_fn вычисляет кросс-энтропийные потери, сравнивая выходные нейроны, интерпретированные как вектор, с желаемым одним горячим вектором. Итак, снова в нашем примере «FUZZY» у нас будет выход [1.7,0.1, -1.0,3.1] и один горячий вектор [0,0,0,1]. Теперь рассчитаем убыток по этой формуле:

Где y - наша метка, а y_hat - вывод логитов softmax. Мне пришлось немного переписать код из примера CNN, потому что теперь мы работаем с последовательностями, а не с простыми классами:

Другой метод на этапе обучения - exponential_decay. Я использую Adam-Optimizer с начальной скоростью обучения 0,002. Но каждые пять эпох я хочу снижать скорость обучения, чтобы избежать слишком сильных колебаний. Через каждые пять эпох коэффициент 0,97ˣ умножается на нашу начальную скорость обучения, x - это то, как часто мы достигли пяти эпох.

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

Режим предсказания

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

Этот метод называется «жадной выборкой», потому что мы всегда берем символ с наибольшей вероятностью в нашем выходном векторе. Есть лучшие методы выборки, такие как Beam-Search, которые я здесь не рассматриваю.

Цикл обучения и выборки

По крайней мере, мы можем вызывать все наши написанные функции в нашем цикле обучения и выборки.

Через каждые 10 эпох мы генерируем пример нашего текста, и вначале он выглядит очень повторяющимся:

пик моряков всех купцов значения значения значения значения значения значения значения ...

Но модель становится все лучше и лучше, и после 100 эпох обучения результат выглядит так, будто Шекспир все еще жив и пишет новые тексты!

Это смена уважаемой женщины в четверть короля, К этому опаснейшему солдату и удаче.

АНТОНИО:
Если бы она сочла зрелище в честь Луны, почему ...

Точность обучения после 100 эпох составляет 86,10%, а скорость обучения упала до 0,00112123.

Заключение

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

Flax, несмотря на его мощные и многочисленные инструменты, все еще находится на ранней стадии разработки, но они являются их способом разработки фреймворка, который мне нравится. Что было действительно гениальным, так это то, что мне нужно было лишь немного изменить мой «старый» код CNN, чтобы использовать RNN на существующей основе.

Но у Flax по-прежнему отсутствует собственный конвейер ввода, поэтому мне пришлось написать его с помощью Tensorflow. Вы можете найти код для создания набора данных и полную RNN в репозитории Github.

Если вы хотите попробовать мой код самостоятельно, загляните в мой Github Repo.

В противном случае могу порекомендовать Flax Github Repo и их документацию.

Изображения навеяны этим блогом.