Реализация RNN с нуля на Python.

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

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

Что такое RNN?

Рекуррентная нейронная сеть - это нейронная сеть, которая специализируется на обработке последовательности данных x(t)= x(1), . . . , x(τ) с индексом временного шага t в диапазоне от 1 to τ. Для задач, требующих последовательного ввода, таких как речь и язык, часто лучше использовать RNN. В задаче НЛП, если вы хотите предсказать следующее слово в предложении, важно знать слова перед ним. RNN называются повторяющимися, потому что они выполняют одну и ту же задачу для каждого элемента последовательности, а результат зависит от предыдущих вычислений. Еще один способ думать о RNN - это то, что у них есть «память», которая фиксирует информацию о том, что было рассчитано на данный момент.

Архитектура. Давайте кратко рассмотрим базовую сеть RNN.

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

Входные данные: x (t) принимаются в качестве входных данных для сети на временном шаге t. Например, x1, может быть горячим вектором, соответствующим слову предложения.

Скрытое состояние : h (t) представляет скрытое состояние в момент времени t и действует как «память» сети. h (t) вычисляется на основе текущего ввода и скрытого состояния предыдущего временного шага: h(t)​ = f(U x(t)​ + W h(t1)​). Функция f рассматривается как нелинейное преобразование, такое как tanh, ReLU.

Веса: RNN имеет входные данные для скрытых соединений, параметризованных матрицей весов U, рекуррентные соединения от скрытого к скрытому, параметризованные матрицей весов W, и соединения со скрытыми выходами, параметризованные матрицей весов V. и все эти веса (U, V, W) распределяются во времени.

Вывод: o (t) иллюстрирует вывод сети. На рисунке я просто поставил стрелку после o (t), который также часто подвержен нелинейности, особенно когда сеть содержит следующие уровни ниже по течению.

Вперед

На рисунке не указан выбор функции активации для скрытых блоков. Прежде чем продолжить, сделаем несколько предположений: 1) мы предполагаем функцию активации гиперболического тангенса для скрытого слоя. 2) Мы предполагаем, что выходные данные дискретны, как если бы RNN использовалась для предсказания слов или символов. Естественный способ представления дискретных переменных - рассматривать выходные данные o как ненормализованные логарифмические вероятности каждого возможного значения дискретной переменной. Затем мы можем применить операцию softmax в качестве шага постобработки, чтобы получить вектор ŷ нормализованных вероятностей на выходе.

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

Это пример рекуррентной сети, которая отображает входную последовательность в выходную последовательность той же длины. Общие потери для данной последовательности x значений в паре с последовательностью y значений будут тогда просто суммой потерь за все временные шаги. Мы предполагаем, что выходы o(t) используются в качестве аргумента функции softmax для получения вектора ŷ вероятностей выхода. Мы также предполагаем, что убыток L - это отрицательная логарифмическая вероятность истинной цели y(t), заданная на данный момент входными данными.

Обратный проход

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

Вычислительные градиенты

Учитывая нашу функцию потерь L, нам нужно вычислить градиенты для наших трех весовых матриц U, V, W и членов смещения b, c и обновите их со скоростью обучения α. Подобно нормальному обратному распространению, градиент дает нам представление о том, как меняются потери по каждому параметру веса. Мы обновляем веса W, чтобы минимизировать потери с помощью следующего уравнения:

То же самое нужно проделать и с другими грузами U, V, b, c.

Давайте теперь вычислим градиенты с помощью BPTT для приведенных выше уравнений RNN. Узлы нашего вычислительного графа включают параметры U, V, W, b и c, а также последовательность узлов, проиндексированных t для x (t), h (t), o (t) и L (t). Для каждого узла n нам нужно вычислить градиент ∇nL рекурсивно на основе градиента, вычисленного в узлах, следующих за ним на графике.

Градиент по отношению к выходу o (t) вычисляется, предполагая, что o (t) используется в качестве аргумента функции softmax для получения вектора ŷ вероятностей по выходу. Мы также предполагаем, что потеря представляет собой отрицательную логарифмическую вероятность истинной цели y (t).

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

Давайте теперь поймем, как градиент течет через скрытое состояние h (t). Из приведенной ниже диаграммы ясно видно, что в момент времени t скрытое состояние h (t) имеет градиент, вытекающий как из текущего вывода, так и из следующего скрытого состояния.

Мы идем в обратном направлении, начиная с конца последовательности. На последнем временном шаге τ, h (τ) имеет только o (τ) в качестве потомка, поэтому его градиент прост:

Затем мы можем выполнить итерацию назад во времени для обратного распространения градиентов во времени от t = τ −1 до t = 1, отмечая, что h (t) (для t ‹τ) имеет в качестве потомков как o (t), так и h ( т + 1). Таким образом, его градиент определяется следующим образом:

Как только градиенты на внутренних узлах вычислительного графа получены, мы можем получить градиенты на узлах параметров. Расчет градиента с использованием цепного правила для всех параметров:

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

Реализация

Мы реализуем полную рекуррентную нейронную сеть с нуля, используя Python. Мы попытаемся построить модель генерации текста с использованием RNN. Мы обучаем нашу модель предсказанию вероятности появления символа с учетом предыдущих символов. Это генеративная модель. Учитывая существующую последовательность символов, мы выбираем следующий символ из предсказанных вероятностей и повторяем процесс, пока не получим полное предложение. Эта реализация от Андрея Карпати великого поста, создающего персонажа уровня RNN. Здесь мы шаг за шагом обсудим детали реализации.

Общие шаги, которые необходимо выполнить:

  1. Инициализировать весовые матрицы U, V, W из случайного распределения и смещения b, c нулями
  2. Прямое распространение для вычисления прогнозов
  3. Вычислить убыток
  4. Обратное распространение для вычисления градиентов
  5. Обновить веса на основе градиентов
  6. Повторите шаги 2–5.

Шаг 1. Инициализация

Чтобы начать реализацию базовой ячейки RNN, мы сначала определяем размеры различных параметров U, V, W, b, c.

Размеры. Предположим, мы выбрали размер словаря vocab_size= 8000 и размер скрытого слоя hidden_size=100. Тогда у нас есть:

Размер словаря может быть количеством уникальных символов для модели на основе символов или количеством уникальных слов для модели на основе слов.

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

Правильная инициализация весов, похоже, влияет на результаты тренировок, и в этой области проводилось много исследований. Оказывается, лучшая инициализация зависит от функции активации (в нашем случае tanh), и один из рекомендуемых подходов - это случайная инициализация весов в интервале from[ -1/sqrt(n), 1/sqrt(n)], где n - количество входящих соединений с предыдущего уровня.

Шаг 2: проход вперед

В соответствии с нашими уравнениями для каждой временной метки t мы вычисляем скрытое состояние hs [t] и выводим os [t], применяя softmax, чтобы получить вероятность следующего символа.

Вычисление softmax и числовая устойчивость:

Функция Softmax принимает N-мерный вектор действительных чисел и преобразует его в вектор действительных чисел в диапазоне (0,1), который в сумме дает 1. Отображение выполняется с использованием приведенной ниже формулы.

Реализация softmax:

Хотя это выглядит нормально, однако, когда мы вызываем этот softmax с большим числом, как показано ниже, он дает значения «nan».

Числовой диапазон чисел с плавающей запятой, используемых Numpy, ограничен. Для float64 максимальное представимое число порядка 10³⁰⁸. Возведение в степень в функции softmax позволяет легко превысить это число даже для входов довольно скромного размера. Хороший способ избежать этой проблемы - сделать входные данные не слишком большими или слишком маленькими. Есть небольшой математический трюк, подробности см. Здесь. Итак, наш softmax выглядит так:

Шаг 3: вычислить потери

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

  • M - количество возможных меток классов (уникальные символы в нашем словаре)
  • y - двоичный индикатор (0 или 1) того, является ли метка класса C правильной классификацией для наблюдения O
  • p - прогнозируемая модель вероятность того, что наблюдение

Шаг 4: обратный проход

Если мы обратимся к уравнениям BPTT, реализация будет в соответствии с уравнениями. Добавлено достаточно комментариев для понимания кода.

Проблемы

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

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

Это исчезающее ограничение градиента было преодолено различными сетями, такими как долгосрочная краткосрочная память (LSTM), закрытые рекуррентные блоки (GRU) и остаточные сети (ResNets), где первые два являются наиболее часто используемыми вариантами RNN в приложениях NLP.

Шаг 5. Обновите веса

Используя BPTT, мы рассчитали градиент для каждого параметра модели. пришло время обновить веса.

В оригинальной реализации Андрея Карпати для обновления градиента используется Adagrad. Adagrad работает намного лучше, чем SGD. Пожалуйста, проверьте и сравните оба.

Шаг 6. Повторите шаги 2–5.

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

Создать текст

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

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

Давайте посмотрим, как обучается наша RNN после нескольких эпох обучения.

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

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

Бонус: хотите визуализировать, что на самом деле происходит во время тренировки RNN, посмотрите здесь.

Надеюсь, это было полезно для вас. Спасибо за прочитанное.

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

[1] http://www.deeplearningbook.org/contents/rnn.html

[2] https://gist.github.com/karpathy/d4dee566867f8291f086

[3] http://www.wildml.com/2015/09/recurrent-neural-networks-tutorial-part-1-introduction-to-rnns/

[4] https://medium.com/towards-artificial-intelligence/whirlwind-tour-of-rnns-a11effb7808f