Эпоха 2 · Фундамент · 1997

11 LSTM

Long Short-Term Memory · Hochreiter & Schmidhuber · Neural Computation
🟧 оригинал выборочно~2–3 чоригинал ↗
Суть за 20 секунд. RNN, решающая исчезающий градиент. Ячейка хранит состояние с аддитивным путём («constant error carousel»), а три гейта решают, что забыть, что записать и что выдать. Учит зависимости на сотни шагов — рабочая лошадка последовательностей до трансформеров.

Контекст

Обычная RNN держит контекст в скрытом состоянии, но при backprop через много шагов градиент экспоненциально затухает (или взрывается) → длинные зависимости не учатся. Хохрайтер диагностировал это в дипломе 1991-го; LSTM — лекарство.

Идея и механизм

Ячейка хранит состояние ct с аддитивным путём во времени. Поток управляется тремя гейтами-сигмоидами (значения 0..1):

ft, it, ot = σ(·)    c̃t = tanh(·)
ct = ft ⊙ ct−1 + it ⊙ c̃t    ht = ot ⊙ tanh(ct)

forget решает, сколько старого состояния стереть; input — сколько нового записать; output — сколько состояния выпустить наружу. Сеть сама учится, когда помнить, когда забывать, когда выдавать.

мат. анализ Почему градиент не затухает: аддитивный путь

Ключ — производная состояния по предыдущему состоянию. Из ct = ft ⊙ ct−1 + …:

∂ct∂ct−1 = ft  (прямой путь — доминирующий член)

Градиент сквозь T шагов — это произведение:

∂cT∂c0 = ∏t=1T ft

Гейт ft обучаемый: сеть может выставить ft ≈ 1 и сохранить градиент на сотни шагов (это и есть «constant error carousel»). Сравните с обычной RNN, где ∂ht/∂ht−1 = W⊤ diag(σ′): повторное произведение таких матриц сжимается (собственные числа < 1 → затухание) или взрывается (> 1). Аддитивная структура плюс гейт — вот и всё лекарство. Строго говоря, ∂ct/∂ct−1 = ft — это прямой путь; полный якобиан содержит ещё член через гейты, которые сами зависят от ht−1 (а значит от ct−1). Но доминирует именно прямой аддитивный путь — он и спасает градиент.

NumPy Один шаг LSTM-ячейки
import numpy as np
sig = lambda z: 1 / (1 + np.exp(-z))

def lstm_cell(x, h, c, W, b):
    z = W @ np.concatenate([x, h]) + b   # один матвек на все гейты
    i, f, g, o = np.split(z, 4)
    i, f, o = sig(i), sig(f), sig(o)
    g = np.tanh(g)
    c = f * c + i * g       # забыть старое + записать новое
    h = o * np.tanh(c)      # что выпустить наружу
    return h, c
c₍ₜ₋₁₎ × + cₜ конвейер состояния f i·g o гейты управляются [xₜ, h₍ₜ₋₁₎]
Состояние едет по «конвейеру»: гейт забывания (×f) стирает лишнее, входной гейт (+i·g) дописывает новое, выходной (o) решает, что выдать. Аддитивный путь хранит градиент.
Аналогия. Конвейерная лента с тремя клапанами. По ленте едет «память». Клапан forget сбрасывает с ленты ненужное, клапан input подкладывает новое, клапан output зачерпывает с ленты то, что нужно прямо сейчас. Сама лента почти не тормозит груз — поэтому информация (и градиент) доезжает издалека.

Почему это важно

До трансформеров LSTM — основная рабочая лошадка для последовательностей: перевод, речь, рукопись, временные ряды. Seq2seq (#21) и первый attention (#22) построены на LSTM. А идея «аддитивный путь спасает градиент» аукнется в residual-связях ResNet (#27) и Transformer.

Связи

→ ведёт к21. Seq2Seq

Seq2seq ставит два LSTM «спина к спине» (энкодер и декодер) и получает машинный перевод end-to-end. LSTM — кирпич, из которого собрана вся ранняя архитектура «последовательность → последовательность».

↔ родственник27. ResNet

Один и тот же приём в двух обличьях: аддитивный «короткий путь» (ct = ct−1 + … в LSTM; y = x + F(x) в ResNet) не даёт градиенту затухать сквозь много шагов/слоёв.

↔ заменяется32. Transformer

LSTM связывает далёкие элементы последовательно — по одному шагу за раз. Transformer связывает любые две позиции напрямую и параллельно через self-attention, сняв и проблему скорости, и трудность очень длинных зависимостей.

Вопросы пытливого ума

Если гейты решают всё, почему бы не задать forget = 1 всегда — пусть помнит всё?

Тогда состояние никогда не очищается и захламляется устаревшим контекстом, мешая новому. Сила LSTM именно в обучаемом балансе: где-то помнить долго (f≈1), где-то быстро сбрасывать (f≈0). Кстати, исходный LSTM forget-гейта не имел — его добавили позже (Gers, 2000), и без него сети как раз страдали от «незабывания».

Чем GRU отличается от LSTM и почему иногда лучше?

GRU объединяет состояние и выход в один вектор и использует два гейта вместо трёх — проще, меньше параметров, быстрее. На многих задачах качество сопоставимо; на каких-то LSTM чуть лучше за счёт отдельной памяти. Практический выбор обычно решается эмпирически, разница невелика.

Раз LSTM решил длинные зависимости, зачем понадобился Transformer?

LSTM решил затухание градиента, но не последовательность вычислений: шаг t ждёт шага t−1, поэтому обучение не параллелится и медленно на длинных входах. И «сотни шагов» всё же не «десятки тысяч токенов». Transformer убирает рекуррентность вовсе — отсюда и скорость, и масштаб контекста.

Что читать в оригинале

Читать ключевое — устройство ячейки и гейтов плюс аргумент про constant-error-carousel; устаревшие эксперименты статьи можно пропустить.