Крис Ола: как устроены LSTM
Подробный конспект эссе «Understanding LSTM Networks»: зачем рекуррентной сети отдельная ячейка памяти, как работают три гейта, чем отличаются peephole-связи и GRU — с формулами, числами и кодом на NumPy.
Коротко
Заголовок раздела «Коротко»- Обычная рекуррентная сеть (рекуррентная нейросетьрекуррентная нейросетьEN recurrent neural network (RNN)) теоретически может помнить далёкое прошлое, но на практике не учится связывать события, разнесённые на десятки шагов: градиент по пути назад затухает.
- LSTMLSTM (долгая краткосрочная память)EN long short-term memory (LSTM)RNN с ячейкой памяти c_t и тремя гейтами (Hochreiter, Schmidhuber, 1997). Ячейка обновляется сложением c_t = f·c_{t−1} + i·g — «остаточная связь во времени». добавляет к сети отдельную ячейку памяти — «конвейерную ленту», по которой информация едет почти без изменений. Ячейка меняется только сложением и поэлементным умножением.
- Что стереть, что записать и что показать наружу, решают три гейтагейт (вентиль)EN gate, gatingВектор из чисел от 0 до 1 (через сигмоиду), который поэлементно решает, какую долю сигнала пропустить. Из LSTM гейты перешли в SwiGLU, gated attention и Gated DeltaNet.: сигмоида выдаёт числа от 0 до 1, и они поэлементно умножаются на вектор.
- Вариантов LSTM много (peephole-связи, связанные гейты, GRUGRU (управляемый рекуррентный блок)EN gated recurrent unit (GRU)Упрощённая LSTM с двумя гейтами и без отдельной ячейки памяти (Cho и соавт., 2014).), и по сравнительным исследованиям они работают примерно одинаково.
- В конце Ола предсказывает следующий большой шаг — внимание (attention). Через два года из него вырос трансформер.
Как читать вместе с курсом
Заголовок раздела «Как читать вместе с курсом»Эссе короткое и почти без математики сверх уровня урока 3.4.3 «CNN и RNN». Лучше всего читать его сразу после этого урока: уравнения LSTM вы там уже видели, а Ола показывает, что за ними стоит, на картинках. Перед уроком 4.1.5, где вы обучаете char-RNN и LSTM своими руками, полезно перечитать раздел про гейты ещё раз.
Обозначения у Олы чуть отличаются от курса: ячейка у него — (в курсе ), кандидат на запись — (в курсе ), умножение по элементам — звёздочка (в курсе ). Смысл тот же.
1. Память у сети: петля и её развёртка
Заголовок раздела «1. Память у сети: петля и её развёртка»Ола начинает с наблюдения о людях: читая текст, мы понимаем каждое слово с учётом предыдущих, а не начинаем думать заново. Обычная полносвязная сеть так не умеет: она видит один вход и забывает его, как только выдала ответ.
Рекуррентная сеть решает это петлёй: кусок сети получает вход , выдаёт и передаёт своё состояние самому себе на следующий шаг. Петля выглядит загадочно, пока её не развернуть: тогда RNN — это цепочка одинаковых копий одной и той же сети, каждая передаёт «записку» следующей. Веса у всех копий общие.
flowchart LR x0["x₀"] --> A0["A"] --> h0["h₀"] x1["x₁"] --> A1["A"] --> h1["h₁"] x2["x₂"] --> A2["A"] --> h2["h₂"] A0 -->|"состояние"| A1 -->|"состояние"| A2
Такая цепочка естественно подходит для последовательностей: текста, звука, видео. К 2015 году RNN уже давали сильные результаты в распознавании речи, переводе, языковом моделировании и подписях к картинкам. Почти все эти результаты, подчёркивает Ола, получены не на простой RNN, а на LSTM. За примерами он отсылает к посту Андрея Карпатого «The Unreasonable Effectiveness of Recurrent Neural Networks» — тот самый char-RNN, который вы обучаете в уроке 4.1.5.
2. Проблема долгих зависимостей
Заголовок раздела «2. Проблема долгих зависимостей»Иногда для предсказания хватает пары последних слов. Во фразе «облака плывут по …» следующее слово «небу» очевидно, нужная информация стоит рядом, и простая RNN с этим справляется.
Но бывает иначе. Возьмём текст: «Я родился в Харбине и прожил там двадцать лет. … (ещё три абзаца о работе и переездах) … Поэтому я свободно говорю по-…». Ближайшие слова подсказывают, что дальше будет название языка, но какого — знает только начало текста. Разрыв между нужным фактом и местом, где он нужен, может быть огромным.
Ола пишет, что в теории RNN способна на такие связи: человек мог бы вручную подобрать ей веса для игрушечной задачи. Беда в том, что сама сеть этому не учится. Причины разобрали Хохрайтер (1991, диссертация на немецком) и Бенджио с соавторами (1994).
Почувствуйте масштаб на числах. Градиент от ошибки на шаге к шагу проходит раз через одну и ту же матрицу и производную . Пусть каждый такой переход уменьшает сигнал примерно вдвое. Тогда через 10 шагов останется исходного градиента, а через 50 шагов — , то есть ничего. Это затухание градиентовзатухание градиентовEN vanishing gradientsГрадиент первых слоёв — произведение множителей всех слоёв выше; если они меньше 1, он экспоненциально уменьшается с глубиной (0.9⁵⁰ ≈ 0.005), и нижние слои перестают учиться. из урока 3.4.3.
3. Главная идея: конвейер и вентили
Заголовок раздела «3. Главная идея: конвейер и вентили»Повторяющийся блок простой RNN устроен бедно: один слой с . У LSTM в блоке четыре слоя, которые взаимодействуют особым образом. Ола сразу предупреждает: не пугайтесь общей схемы, сначала разберёмся с обозначениями. На его рисунках:
- каждая линия несёт целый вектор;
- жёлтые прямоугольники — обучаемые слои нейросети (сигмоида или поверх линейного преобразования);
- розовые кружки — поэлементные операции (сложение, умножение);
- слияние двух линий — склейка векторов (конкатенация), разветвление — копирование.
Ключ ко всему — состояние ячейки , горизонтальная линия вдоль верха схемы. Ола сравнивает её с конвейерной лентой: она проходит через всю цепочку, и на неё действуют лишь мелкие линейные операции. Информации очень легко проехать по ней без изменений.
Менять содержимое ленты можно только через гейты (gates). Гейт — это слой с сигмоидой и поэлементное умножение после него. Сигмоида выдаёт для каждой компоненты число от 0 до 1: «0 — ничего не пропускать, 1 — пропустить всё». У LSTM три гейта, и все они охраняют ленту.
flowchart LR
Cp["C(t−1): старая память"] --> M1(("×"))
F["f(t): гейт забывания"] --> M1
M1 --> S(("+"))
I["i(t): входной гейт"] --> M2(("×"))
G["C̃(t): кандидаты"] --> M2
M2 --> S
S --> C["C(t): новая память"]
C --> T["tanh"] --> M3(("×"))
O["o(t): выходной гейт"] --> M3
M3 --> H["h(t): выход"]4. Ячейка по шагам
Заголовок раздела «4. Ячейка по шагам»Все четыре слоя получают на вход одно и то же — склейку прошлого выхода и нового входа .
Шаг 1. Что забыть
Заголовок раздела «Шаг 1. Что забыть»Слой «гейт забывания» (forget gate) смотрит на и и для каждой компоненты ячейки выдаёт число от 0 до 1:
Единица значит «оставить полностью», ноль — «стереть». Пример Олы из языковой модели: ячейка может хранить грамматический род текущего подлежащего, чтобы потом выбрать правильное местоимение. Появилось новое подлежащее — старый род пора забыть.
Шаг 2. Что записать
Заголовок раздела «Шаг 2. Что записать»Здесь две части. Сигмоидный «входной гейт» (input gate) решает, какие компоненты обновлять, а слой с готовит кандидатов — новые значения от −1 до 1:
В примере с подлежащим это запись рода нового подлежащего на место старого.
Шаг 3. Обновить ячейку
Заголовок раздела «Шаг 3. Обновить ячейку»Предыдущие шаги только решили, что делать; теперь это делается:
Читается так: «старую память умножаем на маску забывания и прибавляем кандидатов, умноженных на маску записи».
Посчитаем на числах. Пусть в ячейке две компоненты, . Гейт забывания : первую компоненту сохраняем, вторую стираем. Входной гейт : в первую ничего не пишем, во вторую пишем. Кандидаты . Тогда
Первая «ячейка памяти» пережила шаг нетронутой, вторая перезаписана. Настоящие гейты дают не ровно 0 и 1, а промежуточные значения, то есть смешивают старое и новое в нужной пропорции.
Шаг 4. Что показать наружу
Заголовок раздела «Шаг 4. Что показать наружу»Выход — это отфильтрованная версия ячейки. Сначала сигмоидный «выходной гейт» (output gate) решает, какие части показать. Потом ячейку пропускают через (чтобы значения легли в диапазон от −1 до 1) и умножают на гейт:
Пример Олы: модель только что увидела подлежащее, и дальше, возможно, будет глагол. Тогда наружу полезно выдать, единственное или множественное число у подлежащего, чтобы правильно согласовать глагол. При этом в самой ячейке может храниться гораздо больше.
Почему это спасает градиент
Заголовок раздела «Почему это спасает градиент»Ола в эссе почти не говорит о градиентах, но именно здесь разгадка (урок 3.4.3 разбирает её подробно). По прямому пути через ленту производная — без матриц и без производной . Если сеть научилась держать , то через 50 шагов от градиента останется , а при — около . Сравните с у простой RNN из раздела 2. Поэтому, по словам Олы, помнить надолго для LSTM — «практически поведение по умолчанию», а не то, чему её приходится мучительно учить.
Код: один шаг LSTM на NumPy
Заголовок раздела «Код: один шаг LSTM на NumPy»import numpy as np
def sigmoid(z): return 1 / (1 + np.exp(-z))
def lstm_step(x, h_prev, C_prev, W, b): """Один шаг LSTM в обозначениях Олы. x: вход (n_in,), h_prev и C_prev: прошлые состояния (n_h,) W: словарь из четырёх матриц формы (n_h, n_h + n_in), b: словарь сдвигов.""" hx = np.concatenate([h_prev, x]) # склейка [h_{t-1}, x_t] f = sigmoid(W["f"] @ hx + b["f"]) # гейт забывания: что стереть i = sigmoid(W["i"] @ hx + b["i"]) # входной гейт: что записать C_tilde = np.tanh(W["C"] @ hx + b["C"]) # кандидаты на запись C = f * C_prev + i * C_tilde # новая ячейка: сложение! o = sigmoid(W["o"] @ hx + b["o"]) # выходной гейт: что показать h = o * np.tanh(C) # новое скрытое состояние return h, C
rng = np.random.default_rng(0)n_in, n_h = 3, 2W = {k: rng.normal(0, 0.5, size=(n_h, n_h + n_in)) for k in "fiCo"}b = {k: np.zeros(n_h) for k in "fiCo"}b["f"] += 3.0 # большой сдвиг гейта забывания: f ≈ 0.95, ячейка почти не забывает
h, C = np.zeros(n_h), np.array([1.0, -1.0]) # в ячейке уже лежит «память»for t in range(5): x = rng.normal(size=n_in) h, C = lstm_step(x, h, C, W, b) print(f"t={t}: C={np.round(C, 3)}, h={np.round(h, 3)}")Ожидаемый вывод:
t=0: C=[ 1.437 -0.454], h=[ 0.588 -0.358]t=1: C=[ 1.232 -0.557], h=[ 0.362 -0.258]t=2: C=[ 1.198 -0.971], h=[ 0.509 -0.672]t=3: C=[ 0.932 -1.11 ], h=[ 0.307 -0.297]t=4: C=[ 0.513 -1.135], h=[ 0.226 -0.297]Веса здесь случайные, но знаки «памяти» (плюс в первой компоненте, минус во второй) переживают все пять шагов со случайными входами. Сдвиг гейта забывания — известный практический приём: в начале обучения ячейка по умолчанию помнит, а забывать сеть учится сама.
5. Варианты LSTM
Заголовок раздела «5. Варианты LSTM»Ола честно предупреждает: то, что он описал, — «довольно обычная» LSTM, но почти в каждой статье своя версия. Отличия небольшие, вот главные.
Peephole-связи (Герс и Шмидхубер, 2000). Гейтам разрешают «подглядывать» в саму ячейку: к входу каждого гейта добавляют (а выходному — уже обновлённую ):
и аналогично для и . Часто peephole ставят не на все гейты.
Связанные гейты забывания и записи. Вместо двух независимых решений — одно: забываем ровно там, куда пишем новое, и пишем только туда, где что-то забыли:
GRU (Чо и соавторы, 2014) — более заметная переделка. Гейты забывания и записи слиты в один гейт обновления , ячейка и скрытое состояние объединены, добавлен гейт сброса :
Модель проще LSTM, и к 2015 году она быстро набирала популярность.
Есть и другие варианты: Depth Gated RNN (Яо и соавторы, 2015) и совсем иной подход к долгим зависимостям — Clockwork RNN (Коутник и соавторы, 2014), где части сети обновляются с разной частотой.
Какой вариант лучше? Ола ссылается на два исследования. Грефф и соавторы (2015) сравнили популярные варианты и нашли, что они «примерно одинаковы». Йозефович и соавторы (2015) перебрали больше десяти тысяч архитектур RNN и нашли отдельные, которые на некоторых задачах обходят LSTM, но не в целом.
6. Что дальше: внимание
Заголовок раздела «6. Что дальше: внимание»В заключении Ола признаёт, что записанные системой уравнений LSTM выглядят устрашающе, и надеется, что пошаговый разбор сделал их доступнее. А дальше задаёт вопрос: будет ли следующий такой же большой шаг? По его словам, среди исследователей распространено мнение: «Да! Следующий шаг есть, и это внимание». Идея — позволить каждому шагу RNN выбирать, на какую часть большого набора информации смотреть. Например, подписывая картинку, сеть может для каждого слова смотреть на свою область изображения, как в работе Сюй и соавторов (2015, «Show, Attend and Tell»).
Ещё он упоминает Grid LSTM (Калхбреннер и соавторы, 2015) и генеративные модели на RNN (Грегор и соавторы — DRAW; Чанг и соавторы; Байер и Остендорфер, все 2015). Через год Ола вместе с Шаном Картером разовьёт тему внимания в статье для Distill — её полный перевод есть в разделе: «Внимание и дополненные рекуррентные сети». А то, как внимание вытеснило рекуррентность целиком, разобрано в уроках 4.4 и 3.4.3 («Почему победил трансформер»).
Что сделать руками
Заголовок раздела «Что сделать руками»- Откройте оригинал и пройдите все рисунки серии «LSTM3-focus-…» (по одному на шаг). Для каждого рисунка проговорите вслух, какая формула ему соответствует и какой гейт подсвечен.
- Запустите код выше. Поставьте
b["f"] = -3.0(гейт забывания почти закрыт) и посмотрите, как быстро исчезают знаки исходной памяти в . - Допишите в
lstm_stepвариант со связанными гейтами (). Сколько матриц весов стало? (Ответ: три вместо четырёх.) - Посчитайте число параметров
nn.LSTM(input_size=3, hidden_size=2)иnn.GRUс теми же размерами (sum(p.numel() for p in m.parameters())). Сверьте с формулой «4 или 3 набора матриц и сдвигов»; учтите, что PyTorch хранит по два вектора сдвигов на каждый гейт. - В уроке 4.1.5 сравните, насколько далёкий контекст используют RNN и LSTM, — это прямое экспериментальное подтверждение раздела 2.
Словарик
Заголовок раздела «Словарик»| English | Русский |
|---|---|
| recurrent neural network (RNN) | рекуррентная нейросеть |
| long-term dependencies | долгие (дальние) зависимости |
| cell state | состояние ячейки, ячейка памяти |
| gate | гейт, вентиль |
| forget / input / output gate | гейт забывания / входной / выходной гейт |
| candidate values | кандидаты на запись |
| pointwise operation | поэлементная операция |
| peephole connections | peephole-связи («подглядывающие» связи) |
| coupled gates | связанные гейты |
| gated recurrent unit (GRU) | GRU, управляемый рекуррентный блок |
| update gate / reset gate | гейт обновления / гейт сброса |
| vanishing gradients | затухание градиентов |
| attention | внимание |