Рекуррентные сети
- О чём эта тема
- Все сети траектории до сих пор получали вход целиком и сразу. Рекуррентные сети читают вход последовательно — слово за словом, отсчёт за отсчётом — и хранят внутреннее состояние, зависящее от всего прочитанного. Разбираются простой рекуррентный слой и его проблема затухания градиента, схемы применения, 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)\):
где \(W\) — весовая матрица входа, \(R\) — рекуррентная весовая матрица, а функцией активации \(g\) для простых рекуррентных слоёв обычно служит гиперболический тангенс (конспект 1, формула (1.7)). Обе матрицы можно склеить в одну и вычислять (12.1) одним матричным умножением над склейкой векторов \(x(t)\) и \(h(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-many): последовательность → последовательность. При задержке \(d > 0\) (асинхронный случай) выход начинает выдаваться после прочтения первых \(d\) входов — так работает перевод: первое слово перевода нельзя выбрать, не прочитав начало фразы. Синхронный случай \(d = 0\) — например, разметка видео кадр за кадром:
Один ко многим (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)\) — какие обновить; кандидат на обновление считается через tanh:
Новое состояние памяти — старое, частично стёртое, плюс новое, частично записанное (\(\odot\) — поэлементное умножение):
а скрытое состояние — отфильтрованная выходным вентилем память:
Ключ к устойчивому градиенту — формула (12.4): память обновляется сложением, а не прогоном через матрицу с нелинейностью, поэтому градиент течёт по цепочке \(C(t)\) без экспоненциального затухания — тот же приём, что остаточная связь ResNet (конспект 7, формулы (7.1)–(7.2)), только изобретённый на два десятилетия раньше.
4. GRU: упрощённая ячейка
GRU (англ. Gated Recurrent Unit; Чо и соавт., 2014) — упрощение LSTM: отдельного состояния ячейки нет, вентилей два (обновления и сброса), параметров заметно меньше:
Эмпирическое сравнение (Чунг и соавт., 2014) показало, что GRU на многих задачах не уступает LSTM при меньшей стоимости; универсального победителя нет — обе ячейки остаются стандартными строительными блоками для последовательностей.
5. Реализация на Keras
Всем трём ячейкам соответствуют слои Keras с одинаковым интерфейсом:
| Ячейка | Слой Keras |
|---|---|
| простая RNN (12.1) | SimpleRNN(units) |
| LSTM (12.2)–(12.5) | LSTM(units) |
| GRU | GRU(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) вычисляется из текущего входа и собственного выхода на предыдущем шаге (формула (12.1)). Обратные связи реализуют внутреннее состояние — память, благодаря которой текущий выход зависит от всех предыдущих входов.
-
Градиент сквозь время — произведение локальных производных r·g'(·) по всем шагам: при |r·g'| < 1 оно затухает экспоненциально, при |r| > 1 состояние насыщает tanh и производная снова гибнет. Влияние ранних входов на поздние выходы становится исчезающе малым — учиться не на чем.
-
Вентиль забывания f(t) решает, какие части состояния ячейки стереть; входной вентиль i(t) — какие обновить кандидатом C̃(t); выходной вентиль o(t) — какую часть памяти показать в скрытом состоянии h(t) (формулы (12.2)–(12.5)).
-
Оба обновляются сложением, а не прогоном через матрицу с нелинейностью: C(t) = f⊙C(t−1) + i⊙C̃ (формула (12.4)) и y = F(x) + x (конспект 7, формула (7.1)). Слагаемое-«магистраль» позволяет градиенту течь через много шагов без экспоненциального затухания.
-
Для «многие ко многим» и для стекирования рекуррентных слоёв: слой возвращает все состояния h(1..T), а не только финальное h(T). Без него следующий рекуррентный слой получит один вектор вместо последовательности.
-
Когда смысл элемента зависит и от последующих элементов — прежде всего в обработке текста. Два рекуррентных слоя читают последовательность в противоположных направлениях, и выход строится из обоих состояний: прошлое учитывает h(t), будущее — h'(t).