Траектория «Нейросети» · конспект 12 из 12

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

О чём эта тема
Все сети траектории до сих пор получали вход целиком и сразу. Рекуррентные сети читают вход последовательно — слово за словом, отсчёт за отсчётом — и хранят внутреннее состояние, зависящее от всего прочитанного. Разбираются простой рекуррентный слой и его проблема затухания градиента, схемы применения, LSTM и GRU, слои Keras для последовательностей. Перевод и адаптация лекции Й. Маухера (HdM Stuttgart) о рекуррентных сетях.
Аннотация
Конспект начинается с задач, где вход — последовательность переменной длины: текст, временной ряд, звук. Вводится простой рекуррентный слой: скрытое состояние обновляется из текущего входа и предыдущего состояния одной матричной операцией. На интерактивном тренажёре показывается главная беда такой памяти — влияние ранних входов затухает экспоненциально (или взрывается), что делает длинные зависимости невыучиваемыми. Затем перечисляются схемы применения рекуррентных сетей: «многие к одному», «многие ко многим», «один ко многим» и двунаправленные сети. Центральная часть посвящена LSTM — ячейке с отдельным состоянием-памятью и тремя вентилями, решающей проблему затухания, — и её упрощению GRU. Завершается конспект слоями Keras для последовательностей и классификацией отзывов IMDB сетью Embedding + LSTM.
Пререквизиты
Конспект 1 (нейрон, весовая матрица, tanh), конспект 2 (градиент, обратное распространение), конспект 3 (Keras, Sequential), конспект 7, раздел 5 (остаточные связи — родственная идея), конспект 11 (эмбеддинги).
Мотивация
Полносвязные и свёрточные сети требуют вход фиксированного размера и рассматривают его элементы одновременно. Но предложение, показания датчика, аудиозапись — последовательности переменной длины, где смысл текущего элемента зависит от предыдущих: слово «ключ» в тексте про двери и в тексте про реки означает разное. Нужна архитектура с памятью о прочитанном — это и есть рекуррентная сеть (англ. Recurrent Neural Network, RNN).

1. Простой рекуррентный слой

В отличие от сетей прямого распространения, рекуррентный слой имеет связи не только вперёд, но и назад — на самого себя. Обратные связи реализуют внутреннее состояние — память. На каждом шаге \(t\) выход слоя \(h(t)\) вычисляется из текущего входа \(x(t)\) и собственного выхода на предыдущем шаге \(h(t-1)\):

\[ h(t) = g\bigl(W x(t) + R\, h(t-1) + b\bigr), \tag{12.1}\]

где \(W\) — весовая матрица входа, \(R\) — рекуррентная весовая матрица, а функцией активации \(g\) для простых рекуррентных слоёв обычно служит гиперболический тангенс (конспект 1, формула (1.7)). Обе матрицы можно склеить в одну и вычислять (12.1) одним матричным умножением над склейкой векторов \(x(t)\) и \(h(t-1)\). Так текущий выход зависит от текущего входа и всех предыдущих — сеть моделирует связи между последовательными элементами:

Схема простого рекуррентного слоя: входы x(t) подаются последовательно, скрытое состояние h(t) вычисляется из входа и предыдущего состояния
Простой рекуррентный слой: состояние \(h^1(t)\) зависит от входа \(x(t)\) и предыдущего состояния \(h^1(t-1)\). Из лекции курса

Обучается рекуррентная сеть тем же обратным распространением (конспект 2), только сеть предварительно «разворачивают» во времени: цепочка шагов превращается в глубокую сеть с общими весами, по которой градиент течёт от конца последовательности к началу (обратное распространение сквозь время, англ. backpropagation through time).

Тренажёр: память рекуррентного нейрона

вход x(1..12) — нажмите, чтобы переключить 0/1

Скалярный рекуррентный нейрон h(t) = tanh(x(t) + r·h(t−1)) — данные иллюстративные. Столбики — состояние h(t) по шагам: после единицы на входе память «звенит» и затухает со скоростью, зависящей от r. Внизу — точное влияние первого входа на последний выход (произведение локальных производных, как в обратном распространении): при r заметно меньше 1 оно исчезающе мало́, при r > 1 состояние насыщается tanh — и производная снова гибнет. Длинные зависимости простой RNN практически не выучиваются.

Типичная ошибка Используют простой рекуррентный слой (SimpleRNN) для длинных последовательностей. Из-за затухания градиента сквозь время сеть выучивает только короткие зависимости — на последовательностях в десятки и сотни шагов нужны LSTM или GRU (разделы 3–4).

2. Схемы применения

Рекуррентный слой можно сочетать со входами и выходами по-разному; сложилась стандартная классификация схем.

Многие к одному (many-to-one): последовательность входов → один выход. Пример — классификация текста: слова подаются по одному, состояние \(h(t)\) накапливает смысл, и по финальному состоянию \(h(T)\) полносвязный слой предсказывает класс. Так же устроен прогноз временного ряда по окну предыстории:

Схема many-to-one: последовательность входов сворачивается в одно финальное состояние и один выход
Многие к одному
Пример классификации текста рекуррентной сетью: слова подаются по одному, финальное состояние определяет класс отзыва
Классификация текста. Из лекции курса

Многие ко многим (many-to-many): последовательность → последовательность. При задержке \(d > 0\) (асинхронный случай) выход начинает выдаваться после прочтения первых \(d\) входов — так работает перевод: первое слово перевода нельзя выбрать, не прочитав начало фразы. Синхронный случай \(d = 0\) — например, разметка видео кадр за кадром:

Схема many-to-many с задержкой: выходная последовательность начинается после d шагов входной
Асинхронный случай (перевод)
Схема синхронного many-to-many: на каждый вход сразу выдаётся выход
Синхронный случай (разметка кадров)
Схема one-to-many: один вход порождает последовательность выходов
Один ко многим (описание изображения)

Один ко многим (one-to-many): один вход → последовательность. Классический пример — описание изображения словами: вход (вектор признаков картинки, извлечённый CNN из конспекта 7) задаёт первое состояние, дальше сеть разворачивает из него фразу.

Двунаправленная сеть (bidirectional RNN) читает последовательность дважды — слева направо и справа налево — и объединяет два состояния: \(h(t)\) учитывает прошлое, \(h'(t)\) — будущее. Для языка это естественно: смысл слова зависит и от последующих слов. Двунаправленной может быть RNN любого типа — простая, LSTM или GRU.

3. LSTM: память с вентилями

LSTM (англ. Long Short-Term Memory; Хохрайтер, Шмидхубер, 1997) решает проблему затухания, разделяя две роли: рабочие вычисления и долгую память. Помимо скрытого состояния \(h(t)\) ячейка хранит состояние ячейки \(C(t)\) — «конвейер памяти», по которому информация течёт сквозь время почти без преобразований. Управляют памятью три вентиля — маленькие подсети с сигмоидой на выходе (числа от 0 до 1 — «сколько пропустить»):

\[ f(t),\; i(t),\; o(t) = \sigma\bigl(W^{\{f,i,o\}} x(t) + R^{\{f,i,o\}} h(t-1) + b^{\{f,i,o\}}\bigr) \tag{12.2}\]

Вентиль забывания \(f(t)\) решает, какие части памяти стереть; входной вентиль \(i(t)\) — какие обновить; кандидат на обновление считается через tanh:

\[ \underline{C}(t) = \tanh\bigl(W^{C} x(t) + R^{C} h(t-1) + b^{C}\bigr) \tag{12.3}\]

Новое состояние памяти — старое, частично стёртое, плюс новое, частично записанное (\(\odot\) — поэлементное умножение):

\[ C(t) = f(t) \odot C(t-1) + i(t) \odot \underline{C}(t) \tag{12.4}\]

а скрытое состояние — отфильтрованная выходным вентилем память:

\[ h(t) = o(t) \odot \tanh\bigl(C(t)\bigr) \tag{12.5}\]
Схема ячейки LSTM: вентиль забывания, входной вентиль с кандидатом обновления и выходной вентиль управляют состоянием ячейки C(t)
Ячейка LSTM: вертикальная красная линия — состояние-память \(C(t)\), три вентиля управляют стиранием, записью и чтением. Из лекции курса

Ключ к устойчивому градиенту — формула (12.4): память обновляется сложением, а не прогоном через матрицу с нелинейностью, поэтому градиент течёт по цепочке \(C(t)\) без экспоненциального затухания — тот же приём, что остаточная связь ResNet (конспект 7, формулы (7.1)–(7.2)), только изобретённый на два десятилетия раньше.

4. GRU: упрощённая ячейка

GRU (англ. Gated Recurrent Unit; Чо и соавт., 2014) — упрощение LSTM: отдельного состояния ячейки нет, вентилей два (обновления и сброса), параметров заметно меньше:

Схема ячейки GRU с двумя вентилями — сброса и обновления — без отдельного состояния ячейки
Ячейка GRU: два вентиля вместо трёх, память совмещена со скрытым состоянием. Из лекции курса

Эмпирическое сравнение (Чунг и соавт., 2014) показало, что GRU на многих задачах не уступает LSTM при меньшей стоимости; универсального победителя нет — обе ячейки остаются стандартными строительными блоками для последовательностей.

5. Реализация на Keras

Всем трём ячейкам соответствуют слои Keras с одинаковым интерфейсом:

ЯчейкаСлой Keras
простая RNN (12.1)SimpleRNN(units)
LSTM (12.2)–(12.5)LSTM(units)
GRUGRU(units)
двунаправленная обёрткаBidirectional(LSTM(units))

По умолчанию рекуррентный слой возвращает только финальное состояние \(h(T)\) — схема «многие к одному». Параметр return_sequences=True заставляет его вернуть все состояния \(h(1),\ldots,h(T)\) — это нужно для схемы «многие ко многим» и для стекирования рекуррентных слоёв друг на друга.

Классификация тональности отзывов IMDB (50 000 рецензий на фильмы, два класса) по схеме «многие к одному»; слова кодируются эмбеддингами — теми же обучаемыми векторами, что метки в cGAN (конспект 11):

from tensorflow.keras.datasets import imdb
from tensorflow.keras.preprocessing.sequence import pad_sequences
from tensorflow.keras import layers, models

# отзывы уже закодированы номерами слов; оставляем 10 000 самых частых
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=10000)

# последовательности разной длины выравниваются до 200 слов
x_train = pad_sequences(x_train, maxlen=200)
x_test = pad_sequences(x_test, maxlen=200)

model = models.Sequential([
    layers.Embedding(10000, 32),      # номер слова -> обучаемый вектор длины 32
    layers.LSTM(64),                  # многие к одному: вся рецензия -> вектор h(T)
    layers.Dense(1, activation='sigmoid'),
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
model.fit(x_train, y_train, epochs=5, batch_size=128,
          validation_data=(x_test, y_test))

# стек из двух рекуррентных слоёв: первому нужен return_sequences=True
deep = models.Sequential([
    layers.Embedding(10000, 32),
    layers.LSTM(64, return_sequences=True),  # отдаёт все h(1..T) следующему слою
    layers.LSTM(32),
    layers.Dense(1, activation='sigmoid'),
])
Типичная ошибка Ставят два рекуррентных слоя подряд без return_sequences=True у первого. Первый слой вернёт один вектор \(h(T)\) вместо последовательности, и второму рекуррентному слою будет нечего читать — Keras сообщит о несовпадении размерностей входа.
Типичная ошибка Считают, что финальное состояние h(T) «помнит» всю последовательность равномерно. Даже у LSTM память ограничена: на очень длинных текстах начало последовательности представлено всё слабее — это ограничение в итоге привело к механизму внимания и трансформерам, вытеснившим рекуррентные сети из обработки языка.

Контрольные вопросы