Перейти к основному содержимому

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

Обработка последовательностей

Входные данные часто представляются в виде последовательности x1x2...xTx_1x_2...x_T, например,

  • динамика цен на акции,

  • динамика действий посетителя веб-сайта,

  • динамика погоды,

  • текст как последовательность слов,

  • речь как последовательность звуков,

  • видео как последовательность кадров.

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

Независимая обработка

Мы можем обрабатывать каждый элемент последовательности xx независимо, используя многослойный персептрон f(x)f(x):

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

Рассмотрим пример разметки частей речи для текста (part-of-speech tagging, POS), в которой каждому слову нужно сопоставить свою часть речи. Пусть мы обрабатываем фразу "I like to watch movies":

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

  • [watch] может быть как глаголом, так и существительным, например, во фразе "this is a very expensive watch".

  • [like] также может выступать как существительное, например, во фразе "he gave this post a like".

Если бы модель учитывала предыдущие слова (перед [like] стоит [I], перед [watch] стоит [to]), то предсказать часть речи ей было бы значительно проще!

Использование свёрток

Мы могли бы применить операцию свёртки, чтобы учесть левый контекст:

Но тогда был бы учтён контекст, состоящий лишь из одного левого соседа. Часто важен более длинный контекст, например для разрешения неоднозначности слова [watch] во фразе "I like to walk and watch movies".

Для учёта контекста из KK соседей можно было бы применить свёртку с более широким ядром или KK раз применить свёртку с двумерным ядром, однако

  • это приводит к усложнению вычислений пропорционально длине контекста,

  • используемый контекст всё равно ограничен.

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

Рекуррентные сети

Учитывая перечисленные недостатки, для анализа и обработки последовательностей чаще используются рекуррентные нейросети (Recurrent neural net, RNN), способные помнить (в теории) весь исторический контекст из предыдущих наблюдений и учитывать его вычислительно эффективно.

Для этого используется скрытое состояние (hidden state) htRDhh_t\in\mathbb{R}^{D_h}, хранящее контекст из всех ранее виденных наблюдений. Прогноз строится, учитывая не только текущее наблюдение xtx_t, но и hth_t, что обеспечивает зависимость прогноза от истории:

y^t=f(xt,ht)\hat{y}_t = f(x_t,h_t)

y^t\hat{y}_t часто не является итоговым прогнозом, а представляет собой лишь промежуточный эмбеддинг, по которому уже строится итоговый прогноз (применяя к нему многослойный персептрон или другую модель).

Само скрытое состояние пересчитывается на каждом шаге обработки последовательности рекуррентно по текущему наблюдению и своему значению на предыдущем шаге обработки:

ht=g(xt,ht1),t=1,2,3,...h_t=g(x_t,h_{t-1}), \quad t=1,2,3,...

Последовательный процесс генерации прогнозов y^1,y^2,...y^T\hat{y}_1,\hat{y}_2,...\hat{y}_T по входам x1,x2,...xTx_1,x_2,...x_T

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

При этом h0RDhh_0\in\mathbb{R}^{D_h} - тоже параметр сети, который можно задавать вектором из нулей или настраивать по данным.

Если разворачивать зависимость рекуррентной сети (network unfolding)

y^t=f(ht)=f(g(xt,ht1))=f(g(xt,g(xt1,ht2)))==f(g(xt,g(xt1,g(xt2,ht3))))=...=F(xt,xt1,...x1,h0),\widehat{y}_{t}=f(h_{t})=f(g(x_{t},h_{t-1}))=f(g(x_{t},g(x_{t-1},h_{t-2})))=\\=f(g(x_{t},g(x_{t-1},g(x_{t-2},h_{t-3}))))=...=F(x_{t},x_{t-1},...x_{1},h_{0}),

то можно увидеть, что y^t\hat{y}_t зависит от всех ранее виденных наблюдений xt,xt1,...x1x_{t},x_{t-1},...x_{1} и строится многократным применением одних и тех же преобразований g(),f()g(\cdot),f(\cdot), просто применённых к различным входным данным.

Таким образом, рекуррентная сеть - это обычная многослойная сеть у которой на каждом слое одинаковые параметры (weight sharing), а число слоёв совпадает с номером обрабатываемого элемента последовательности tt. То есть для длинного входа эта сеть может содержать очень большое число слоёв.

Проблемы настройки

Из-за того, что параметры сети на каждом слое одни и те же, настройка сети довольно неустойчива и имеет тенденцию скатываться в одну из крайностей:

  • сеть экспоненциально быстро забывает историю (проблема затухающих градиентов, vanishing gradient problem);

  • сеть экспоненциально быстро увеличивает вклад дальних наблюдений в прогноз, вследствие чего прогноз и настройка весов ведут себя неустойчиво (проблема взрывающихся градиентов, exploding gradient problem).

Для решения первой проблемы используются специальные архитектуры, такие как GRU и LSTM.

Для решения второй используется обрезка градиентов (gradient clipping) по абсолютному или относительному значению.

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

Простейшей рекуррентной сетью является сеть Элмана [1]:

ht=g(Wxhxt+Whhht1+bh)y^t=f(Whyht+by)\begin{aligned}h_{t} & =g\left(W_{xh}x_{t}+W_{hh}h_{t-1}+b_{h}\right)\\ \widehat{y}_{t} & =f\left(W_{hy}h_{t}+b_{y}\right) \end{aligned}

где

  • g(),f()g(\cdot),f(\cdot) - некоторые функции нелинейности

  • Wxh,Whh,WhyW_{xh},W_{hh},W_{hy} - обучаемые матрицы

  • bh,byb_h,b_y - обучаемые вектора смещений.

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

Настройка рекуррентной сети

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

Для этого вычисления переписываются в матричном виде. Для сети Элмана это будут

Ht=g(WxhXt+WhhHt1+bheT)Y^t=f(WhyHt+byeT)\begin{aligned}H_{t} & =g\left(W_{xh}X_{t}+W_{hh}H_{t-1}+b_{h}\mathbf{e}^T\right)\\ \widehat{Y}_{t} & =f\left(W_{hy}H_{t}+b_{y}\mathbf{e}^T\right) \end{aligned}

где

  • XtRD×BX_t\in\mathbb{R}^{D\times B} - матрица текущих входов для каждой последовательности (по столбцам)

  • HtRD×BH_t\in\mathbb{R}^{D\times B} - матрица текущих состояний для каждой последовательности (по столбцам)

  • e=[1,1,...,1]TRB\mathbf{e}=[1,1,...,1]^T\in\mathbb{R}^B

Для обучения по мини-батчам, последовательности должны быть одинаковой длины TT. Если входная последовательность короче TT, то она дополняется спец. токеном [PAD], обозначающим пустой вход. Например, при T=7T=7 и входной последовательности "I like to watch movies", она будет преобразована в [[I], [like], [to], [watch], [movies], [pad], [pad]].

Опционально перед первым [PAD] может ставиться спец. токен [EOS], чтобы в явном виде обозначить конец ввода.

Для того, чтобы как можно меньше операций производить вхолостую, обрабатывая пустой токен [PAD], входные последовательности предварительно сортируются по длине и обрабатываются блоками из последовательностей примерно одинаковой длины, как показано ниже:

На рисунке зелёным обозначены реальные токены последовательности, а жёлтым-дополнение токеном [PAD].

Настройка сети производится по развернутой рекуррентной сети (unfolded network), когда явно видна зависимость между выходом и всеми входами. Этот способ настройки известен как обратное распространение ошибки во времени (back propagation through time, BPTT) [2].

Обработка длинных последовательностей

Поскольку входная последовательность может быть произвольной длины, из-за чего вычисления градиента могут не поместиться в память, то разворачивание применяется не до самого начала входной последовательности, а не более, чем на KK шагов назад. Это называется обрезанное распространение ошибки во времени (truncated back propagation through time, truncated BPTT).

В этом методе сначала обрабатывается первый блок входов x1x2...xKx_1x_2...x_K, получая итоговое скрытое состояние hKh_K и выходы y^1y^2...y^K\hat{y}_1\hat{y}_2...\hat{y}_K, по которым считаются потери и проходит одна итерация градиентного метода оптимизации. Далее, из состояния hKh_K и входов xK+1xK+2...x2Kx_{K+1}x_{K+2}...x_{2K} генерируется новое скрытое состояние h2Kh_{2K} и выходы y^K+1y^K+2...y^2K\hat{y}_{K+1}\hat{y}_{K+2}...\hat{y}_{2K} по которым снова считаются ошибки и уточняются параметры сети. Так процесс продолжается, пока мы не дойдём до конца длинной последовательности.

Особенность truncated BPTT

Обратим внимание, что truncated BPTT не позволяет процессу оптимизации учитывать зависимости длиннее, чем KK шагов назад. С одной стороны, это препятствует учёту долгосрочных зависимостей, с другой - действует как регуляризатор, в явном виде не позволяющий модели переобучаться под чересчур долгосрочные зависимости, которые скорее всего будут ложными (false dependencies). Также это делает настройку сети более стабильной, уменьшая проблему взрывающихся градиентов (exploding gradient problem).

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

  1. собирается большое число текстов, которые объединяются в одну большую объединённую последовательность, разделённую токенами [EOS] (end of sequence).

  2. Далее рекуррентная сеть проходит по всей последовательности, и мы сохраняем скрытые состояния на каждом шаге.

  3. Формирование мини-батчей обучающих последовательностей происходит нарезкой объединённой последовательности либо на последовательные блоки, либо на случайные длины TT.

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

Для борьбы с переобучением при настройке сети используется регуляризация любым из ранее изученных методов. Можно ограничивать число слоёв и нейронов, накладывать L1/L2L_1/L_2 регуляризацию на веса, использовать аугментацию и другие приёмы.

Трансферное обучение

При обработке текстов рекуррентными сетями эмбеддинги слов можно брать из другой задачи, используя трансферное обучение. Например, из метода LSA, применённого к матрице совстречаемости слов. Или из языковой модели (language model) Word2vec или fastText. Однако, поскольку эмбеддинги слов - это точно такие же параметры модели, как и веса самой рекуррентной сети, то их можно настраивать вместе с самой моделью.

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

Учёт редких и новых слов

Для того, чтобы сократить число выучиваемых эмбеддингов слов можно заменить редко встречающиеся слова (например, меньше 3-х раз во всех документах) специальным токеном [unknown].

Помимо существенного сокращения числа выучиваемых параметров (поскольку в языке очень много редких слов из закона Ципфа), это дополнительно позволит модели органично обрабатывать новые слова во время применения на тестовой выборке - их достаточно тоже заменить токеном [unknown]!

:::

DropOut в рекуррентных сетях

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

Случайное отбрасывание нейронов вдоль оси итераций разрывает зависимости между последовательными наблюдениями, что препятствует настройке сети на эти зависимости. Поэтому в [3] предложено применять DropOut только между слоями многоуровневой обработки данных (naive RNN DropOut).

В [4] предложено накладывать DropOut не только вдоль уровней обработки, но и вдоль итераций, при этом для каждой последовательности генерировать и фиксировать одну и ту же маску для вертикальных и горизонтальных связей (variational RNN DropOut).

Оба способа проиллюстрированы ниже [4], где пунктиром обозначены переходы без отбрасывания нейронов, а сплошной линией - слои с DropOut, использующей DropOut-маску прореживания нейронов, обозначенную цветом (одному цвету отвечает одинаковая маска):

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

Входы и выходы рекуррентной сети

Если вход вещественный xtRDx_t\in\mathbb{R}^D (например, вектор текущих цен на выбранные акции на бирже), то он и используется как вход. Аналогично, если вещественный выход ytRDy_t\in\mathbb{R}^D (например, вектор ,будущих цен на акции), то он тоже напрямую используется как выход.

Если вход категориальный (принадлежит одной из CinC_{in} дискретных категорий), как, например, слово языка, то каждая категория кодируется своим DD-мерным эмбеддингом (вектором вещественных чисел), и вместо категории в качестве xtx_t подаётся соответствующий ей эмбеддинг. Как мы видели, эмбеддинги могут браться из другой задачи, т.е. использовать трансферное обучение (transfer learning). Например, для кодировки слов использовать Word2Vec, обученный решать задачу языкового моделирования. Либо обучать эмбеддинги под итоговую задачу вместе с остальными параметрами рекуррентной сети. В качестве инициализации такого обучения также можно брать эмбеддинги, полученные из другой задачи.

Если категорий мало, например при генерации текста не пословно, а посимвольно, в качестве эмбеддингов можно использовать one-hot кодирование.

Если выход категориальный (принадлежит одной из CoutC_{out} дискретных категорий), то выходом рекуррентной сети служит вектор из CoutC_{out} вещественных чисел, трактуемых как рейтинги соответствующих категорий. Далее к этим рейтингам применяется SoftMax преобразование, чтобы получить вероятности категорий. Итоговую категорию можно выбирать либо как самую вероятную, либо сэмплировать согласно предсказанному распределению.

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

Рекуррентная сеть как кодировщик

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

В частности, можно получить

  • эмбеддинг слова, пройдя по нему как по последовательности букв;

  • эмбеддинг параграфа, пройдя по нему как по последовательности слов;

  • эмбеддинг звука, пройдя по нему как по последовательности частот;

  • эмбеддинг видео, пройдя по нему как по последовательности фреймов.

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


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

Литература

  1. Elman J. L. Finding structure in time //Cognitive science. – 1990. – Т. 14. – №. 2. – С. 179-211.
  2. Werbos P. J. Backpropagation through time: what it does and how to do it //Proceedings of the IEEE. – 1990. – Т. 78. – №. 10. – С. 1550-1560.
  3. Wojciech Zaremba, Ilya Sutskever, and Oriol Vinyals. Recurrent neural network regularization. arXiv:1409.2329, 2014.
  4. Gal Y., Ghahramani Z. A Theoretically Grounded Application of Dropout in Recurrent Neural Networks, arXiv e-prints, p //arXiv preprint arXiv:1512.05287. – 2015.