LSTM и GRU 🧠
В уроке 339 ты разобрал обычную рекуррентную сеть (RNN) и упёрся в её главное ограничение: скрытое состояние $h_t=\tanh(W_h\cdot h_{t-1}+W_x\cdot x_t+b)$ обновляется через одну и ту же матрицу весов $W_h$ и одну и ту же нелинейность $\tanh$ на каждом шаге, а при обратном распространении ошибки через время (BPTT) градиент, дошедший до шага $t$, вынужден пройти обратно через десятки, а то и сотни таких одинаковых умножений подряд. Если собственные значения матрицы $W_h$ (умноженные на производную $\tanh$) хоть немного меньше единицы, градиент затухает экспоненциально с ростом числа шагов — сеть физически не может «дотянуться» обучающим сигналом до далёкого прошлого и потому не способна выучить долгосрочные зависимости в последовательности. Это не гипотетическая проблема: для последовательности в 50 шагов множитель $0{,}9^{50}\approx0{,}005$ — меньше одного процента от исходного сигнала.
Сегодняшний урок — это прямой архитектурный ответ на эту проблему, найденный за 18 лет до того, как та же самая идея заново «переоткрылась» в другом контексте. В уроке 337 ты уже видел похожую логику: ResNet решает проблему деградации глубоких сетей, добавляя короткий путь в обход слоёв, $y=F(x)+x$, чтобы градиент мог пройти напрямую, с коэффициентом ровно $1$, независимо от того, что происходит внутри $F$. LSTM (Long Short-Term Memory, долгая краткосрочная память) решает почти ту же самую математическую проблему — но не в измерении глубины сети, а в измерении времени последовательности. Вместо единственного скрытого состояния $h_t$, которое целиком пересчитывается заново на каждом шаге через нелинейность, LSTM вводит отдельную «ячейку памяти» $c_t$, которая передаётся от шага к шагу почти напрямую, преимущественно через сложение и поэлементное умножение, а не через повторяющееся умножение на одну и ту же матрицу весов. Это архитектурный родственник skip connection — только развёрнутый во времени, а не в глубине.
Чтобы ячейка памяти работала — чтобы сеть могла сама решать, что из старой памяти забыть, что добавить нового, а что вывести наружу как текущий ответ, — LSTM оборачивает эту ячейку тремя «вентилями» (gates), каждый из которых представляет собой обычный слой с сигмоидной активацией. Именно сигмоида, дающая значения строго между $0$ и $1$, превращает вентиль в интерпретируемый «кран»: $0$ значит «полностью перекрыть поток», $1$ значит «пропустить поток целиком», а промежуточные значения — частичный пропуск. В 2014 году появилась GRU (Gated Recurrent Unit, управляемый рекуррентный блок) — упрощённая версия той же идеи с двумя вентилями вместо трёх и без отдельной ячейки памяти, показывающая на практике качество, часто сравнимое с LSTM, при заметно меньшем числе параметров.
Для практикующего специалиста по Data Science эта тема — не архивная историческая справка, а фундамент, на котором держалась вся практическая обработка естественного языка и анализ временных рядов почти десятилетие. torch.nn.LSTM и torch.nn.GRU были стандартным выбором для машинного перевода, распознавания речи, прогнозирования временных рядов и анализа тональности текста с 2014 по примерно 2020 год, включая систему Google Neural Machine Translation (GNMT), запущенную в промышленную эксплуатацию в 2016 году. Лишь появление трансформеров (уроки 342–344) во многом потеснило рекуррентные архитектуры — и то не везде: для многих задач с временными рядами, для лёгких мобильных моделей и там, где данных или вычислительного бюджета немного, LSTM и GRU остаются вполне рабочим, а иногда и предпочтительным выбором по сей день. Понимание их механики — это понимание того, как вообще выглядит «память» в нейросети, а этот принцип прямо прослеживается и в архитектуре трансформеров, которую ты увидишь через два урока.
История
Идея LSTM появилась в 1997 году в статье Зеппа Хохрайтера (Sepp Hochreiter) и Юргена Шмидхубера (Jürgen Schmidhuber) «Long Short-Term Memory», опубликованной в журнале Neural Computation. Хохрайтер ещё в своей дипломной работе 1991 года (под руководством Шмидхубера) формально показал, откуда берётся затухание градиента в глубоких и рекуррентных сетях — по сути, тот же анализ, который ты видел в уроке 330 применительно к обычным многослойным сетям, только для RNN эта проблема ощущается острее, потому что число «эффективных слоёв» равно длине последовательности, а она может измеряться сотнями шагов. Оригинальная версия LSTM 1997 года уже содержала центральную идею — отдельную ячейку памяти с линейным (по сути) путём обновления через собственный вес, равный примерно единице, — но не имела forget gate: память могла только накапливаться, но не «забываться» явно, что на длинных последовательностях приводило к неограниченному росту значений ячейки. Забавная деталь: статью 1997 года изначально отклоняли рецензенты — идея казалась слишком сложной и не вписывалась в господствовавшую тогда исследовательскую моду.
Существенное дополнение появилось в 2000 году: Феликс Герс (Felix Gers), Шмидхубер и Фред Каммингс (Fred Cummins) в статье «Learning to Forget: Continual Prediction with LSTM» добавили forget gate — вентиль, который явно разрешает сети «забывать» ненужную информацию из ячейки памяти. Без этого дополнения ячейка неограниченно растёт при обучении на длинных последовательностях, а сеть быстро теряет способность к обучению. Именно эта расширенная версия — с тремя вентилями (forget, input, output) — стала тем, что сегодня называют «стандартной» архитектурой LSTM и что реализовано в torch.nn.LSTM. Несмотря на элегантность идеи, на протяжении почти десяти лет LSTM оставалась нишевым инструментом — вычислительных мощностей 1997–2007 годов для серьёзного применения на больших данных попросту не хватало, а сообщество глубокого обучения переживало период, который иногда называют «второй зимой ИИ».
Второй важный виток истории — 2014 год: Кёнхён Чо (Kyunghyun Cho), Барт ван Мерриенбур (Bart van Merriënboer), Дзёкин Гульчере (Çağlar Gülçehre), Йошуа Бенджио (Yoshua Bengio) и соавторы в статье «Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation» предложили GRU как более простую альтернативу LSTM для той же задачи — кодирования и декодирования последовательностей в машинном переводе (архитектура encoder-decoder, тема следующего урока 341). GRU объединяет forget и input gate LSTM в единый update gate, отказывается от отдельной ячейки памяти в пользу прямой работы со скрытым состоянием и тем самым сокращает число обучаемых весов примерно на четверть. В том же 2014 году вышла отдельная сравнительная статья Джуня Чуна (Junyoung Chung), Гульчере, Чо и Бенджио «Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling», которая экспериментально показала: на многих задачах GRU и LSTM демонстрируют сопоставимое качество, притом что GRU обучается быстрее за счёт меньшего числа параметров — универсального победителя между ними нет, выбор часто сводится к конкретной задаче и вычислительному бюджету.
Настоящий взрыв практического применения обеих архитектур пришёлся на 2014–2016 годы, когда вычислительных мощностей (GPU) и данных стало достаточно, чтобы обучать глубокие стеки LSTM на миллиардах слов. Кульминацией стал запуск Google Neural Machine Translation (GNMT) в 2016 году — промышленной системы машинного перевода на основе глубокого стека LSTM (восемь слоёв кодировщика и восемь слоёв декодировщика), которая за одно обновление сократила число ошибок перевода на 55–85% по сравнению с предыдущей системой на основе фразовых статистических моделей. С этого момента и вплоть до широкого распространения трансформеров после статьи «Attention Is All You Need» (2017 год) и последующих моделей BERT и GPT (2018–2019 годы, уроки 343–344) LSTM и GRU оставались фактическим стандартом почти для любой задачи, где данные представлены последовательностью: перевод, распознавание речи, генерация текста, прогноз временных рядов, анализ биржевых котировок.
Ячейка памяти: отдельный канал, скользящий почти напрямую сквозь время
Интуиция
Центральная архитектурная идея LSTM — отделить «то, что сеть помнит» от «того, что сеть выдаёт прямо сейчас». В обычной RNN это одна и та же сущность: скрытое состояние $h_t$ одновременно и хранит всю накопленную память, и служит непосредственным выходом на каждом шаге, из-за чего оно вынуждено каждый раз полностью пересчитываться через $\tanh$ — а именно повторяющаяся нелинейная трансформация и вызывает затухание градиента. LSTM разводит эти две роли по разным переменным: ячейка памяти $c_t$ отвечает за долговременное хранение информации и обновляется преимущественно линейно (сложением), а скрытое состояние $h_t$ остаётся «рабочим», видимым наружу представлением, которое каждый раз получается из $c_t$ через дополнительную фильтрацию.
Представь это как разницу между черновиком и чистовиком. Обычная RNN каждый раз переписывает единственный документ с нуля, стараясь удержать в нём всё важное из прошлого и одновременно оформить его как готовый ответ прямо сейчас — при частой перезаписи детали неизбежно теряются. LSTM ведёт отдельный черновик ($c_t$), в который информация точечно дописывается и точечно вычёркивается, но большая его часть остаётся нетронутой от версии к версии, а уже из черновика на каждом шаге делается краткая выжимка для чистовика ($h_t$) — то, что нужно показать «наружу» именно сейчас. Черновик может копить детали годами, чистовик формируется заново под текущую задачу.
Формула
Обновление ячейки памяти LSTM. Пусть $f_t$ — значение forget gate, $i_t$ — значение input gate, $\tilde c_t$ — «кандидат» на добавление в память (все три определяются в следующем разделе). Тогда ячейка памяти обновляется по формуле
$$c_t = f_t \odot c_{t-1} + i_t \odot \tilde c_t$$где $\odot$ — поэлементное произведение (не матричное умножение). Сравни это с обновлением скрытого состояния обычной RNN, $h_t=\tanh(W_h h_{t-1}+W_x x_t+b)$: там $h_{t-1}$ входит внутрь нелинейности $\tanh$ и матрицы $W_h$, а здесь $c_{t-1}$ входит в сумму снаружи любой нелинейности — единственное, что с ним происходит, это поэлементное умножение на число из интервала $(0,1)$.
Разбор примеров
Пример 1 (структурное сравнение путей обновления). В обычной RNN путь от $h_{t-1}$ к $h_t$ обязательно проходит через матричное умножение $W_h h_{t-1}$ и затем через $\tanh(\cdot)$ — оба этих шага меняют направление и масштаб вектора нетривиально, а производная $\tanh$ ограничена сверху значением $1$ и почти всюду меньше его. В LSTM путь от $c_{t-1}$ к $c_t$ — это $f_t\odot c_{t-1}$, то есть каждая координата $c_{t-1}$ просто умножается на своё собственное число из $(0,1)$ и складывается с новым вкладом $i_t\odot\tilde c_t$. Ни матричного умножения, разбрасывающего информацию между координатами, ни сжимающей нелинейности на этом конкретном пути нет — по структуре это то же самое, что и путь $x$ в остаточном блоке ResNet $y=F(x)+x$ из урока 337, только координаты здесь не пробрасываются буквально с коэффициентом $1$, а масштабируются обучаемым, зависящим от данных коэффициентом $f_t$.
Пример 2 (что произойдёт, если forget gate всегда равен единице). Предположим гипотетически, что сеть обучилась держать $f_t=1$ и $i_t=0$ на нескольких шагах подряд (то есть «не забывать» и «не добавлять»). Тогда по формуле $c_t=1\odot c_{t-1}+0\odot\tilde c_t=c_{t-1}$ — ячейка памяти в точности переносится с шага на шаг без единого изменения. Если, скажем, на шаге $t=5$ в ячейку было записано значение $c_5=(1{,}2,\,-0{,}7,\,0{,}3)$, а на шагах $6$–$20$ вентили держались в этом режиме, то на шаге $t=20$ ячейка по-прежнему будет содержать ровно $(1{,}2,\,-0{,}7,\,0{,}3)$ — информация «пролежала» пятнадцать шагов совершенно без искажений. Для обычной RNN такое в принципе невозможно: даже если веса подобраны так, чтобы приближённо сохранять сигнал, каждое прохождение через $\tanh$ вносит хоть небольшое, но искажение, которое накапливается.
Пример 3 (частичное забывание — не крайний случай, а типичный режим работы). На практике вентили редко принимают ровно $0$ или $1$ — типичны промежуточные значения. Пусть $c_{t-1}=(2{,}0,\,-1{,}0)$, $f_t=(0{,}9,\,0{,}2)$ (первая координата почти полностью сохраняется, вторая — почти полностью стирается), а вклад нового кандидата $i_t\odot\tilde c_t=(0{,}1,\,0{,}8)$. Тогда $c_t=(0{,}9\cdot2{,}0+0{,}1,\ 0{,}2\cdot(-1{,}0)+0{,}8)=(1{,}9,\ 0{,}6)$. Первая координата почти не изменилась (сеть решила, что эта информация всё ещё важна), вторая почти полностью заменилась новым значением (сеть решила, что старое значение устарело). Именно эта покоординатная, обучаемая избирательность — а не выбор «всё или ничего» — и есть источник практической гибкости LSTM.
Почему это важно
Отделение долговременной памяти $c_t$ от рабочего выхода $h_t$ — это не косметическое усложнение, а прямое устранение источника затухания градиента: путь, по которому сигнал ошибки может течь обратно во времени через $c_t$, состоит преимущественно из сложений и поэлементных умножений на числа, близкие к единице, а не из повторяющихся матричных умножений и сжимающих нелинейностей. Это тот же самый архитектурный приём, что и skip connection ResNet — добавить путь, по которому градиент проходит почти без искажений в обход основного, «трудного» преобразования, — только применённый не к глубине сети, а к её протяжённости во времени. Подробно математику этого эффекта для LSTM мы разберём в разделе про сигмоидные вентили и градиент; здесь важно зафиксировать сам архитектурный ход: две переменные вместо одной, и одна из них специально сконструирована так, чтобы информации в ней было легко «пролежать» много шагов подряд.
Три вентиля LSTM: forget, input, output
Интуиция
Ячейка памяти сама по себе — это просто число (вектор), которое можно менять. Чтобы решить, как именно его менять на каждом конкретном шаге, LSTM использует три вентиля, каждый из которых — маленькая нейросеть с сигмоидной активацией, смотрящая на предыдущее скрытое состояние $h_{t-1}$ и текущий вход $x_t$ и выдающая число от $0$ до $1$ для каждой координаты памяти. Forget gate отвечает на вопрос «какую долю старой памяти сохранить»; input gate отвечает на вопрос «какую долю нового кандидата на самом деле стоит добавить»; output gate отвечает на вопрос «какую долю (обработанной) памяти показать наружу прямо сейчас». Разделение на три отдельных решения — ключевая гибкость: сеть может, например, полностью забыть старую информацию и полностью показать новую, или сохранить всю старую память, но временно не показывать её наружу (придержать «про запас»), или добавить новую деталь в память, но пока не выводить её в ответ.
Формула
Три вентиля и кандидат LSTM (все зависят от $h_{t-1}$ и $x_t$):
$$f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f) \qquad \text{(forget gate)}$$$$i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i) \qquad \text{(input gate)}$$
$$o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o) \qquad \text{(output gate)}$$
$$\tilde c_t = \tanh(W_c \cdot [h_{t-1}, x_t] + b_c) \qquad \text{(кандидат в память)}$$
где $\sigma(x)=1/(1+e^{-x})$ — сигмоида, а $[h_{t-1}, x_t]$ обозначает конкатенацию векторов $h_{t-1}$ и $x_t$ в один более длинный вектор. Итоговое обновление памяти и выхода:
$$c_t = f_t \odot c_{t-1} + i_t \odot \tilde c_t, \qquad h_t = o_t \odot \tanh(c_t)$$Заметь: у трёх вентилей и кандидата — четыре разных набора весов $W_f, W_i, W_o, W_c$ (и четыре разных смещения) — они не делят параметры между собой, каждый учится своей узкой задаче независимо от остальных.
Разбор примеров
Пример 1 (полный численный проход через один шаг LSTM). Возьмём упрощённый скалярный LSTM (одна «координата» памяти — на практике это были бы векторы, но вся арифметика по каждой координате идентична). Пусть предыдущее состояние $h_{t-1}=0{,}6$, предыдущая память $c_{t-1}=1{,}5$, текущий вход $x_t=1{,}0$. Веса (подобраны для наглядности):
$$w_{fh}=0{,}5,\ w_{fx}=0{,}8,\ b_f=0{,}1 \qquad w_{ih}=0{,}6,\ w_{ix}=-0{,}3,\ b_i=0{,}0$$$$w_{oh}=0{,}4,\ w_{ox}=0{,}5,\ b_o=0{,}2 \qquad w_{ch}=-0{,}2,\ w_{cx}=0{,}9,\ b_c=0{,}1$$
Считаем forget gate: $w_{fh}h_{t-1}+w_{fx}x_t+b_f=0{,}5\cdot0{,}6+0{,}8\cdot1{,}0+0{,}1=0{,}3+0{,}8+0{,}1=1{,}2$, значит $f_t=\sigma(1{,}2)\approx0{,}7685$. Input gate: $0{,}6\cdot0{,}6+(-0{,}3)\cdot1{,}0+0{,}0=0{,}36-0{,}3=0{,}06$, значит $i_t=\sigma(0{,}06)\approx0{,}5150$. Output gate: $0{,}4\cdot0{,}6+0{,}5\cdot1{,}0+0{,}2=0{,}24+0{,}5+0{,}2=0{,}94$, значит $o_t=\sigma(0{,}94)\approx0{,}7191$. Кандидат: $-0{,}2\cdot0{,}6+0{,}9\cdot1{,}0+0{,}1=-0{,}12+0{,}9+0{,}1=0{,}88$, значит $\tilde c_t=\tanh(0{,}88)\approx0{,}7065$. Теперь обновляем память: $c_t=f_t\cdot c_{t-1}+i_t\cdot\tilde c_t=0{,}7685\cdot1{,}5+0{,}5150\cdot0{,}7065\approx1{,}1528+0{,}3639\approx1{,}5167$. И наконец скрытое состояние: $h_t=o_t\cdot\tanh(c_t)=0{,}7191\cdot\tanh(1{,}5167)\approx0{,}7191\cdot0{,}9083\approx0{,}6533$. Итог одного полного шага: $f_t\approx0{,}7685$, $i_t\approx0{,}5150$, $o_t\approx0{,}7191$, $\tilde c_t\approx0{,}7065$, $c_t\approx1{,}5167$, $h_t\approx0{,}6533$. Обрати внимание: forget gate оказался довольно высоким ($\approx0{,}77$) — сеть в основном сохранила старую память, слегка подмешав новую информацию через входной вентиль ($\approx0{,}52$).
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
h_prev, c_prev, x = 0.6, 1.5, 1.0
f_t = sigmoid(0.5 * h_prev + 0.8 * x + 0.1)
i_t = sigmoid(0.6 * h_prev - 0.3 * x + 0.0)
o_t = sigmoid(0.4 * h_prev + 0.5 * x + 0.2)
c_tilde = np.tanh(-0.2 * h_prev + 0.9 * x + 0.1)
c_t = f_t * c_prev + i_t * c_tilde
h_t = o_t * np.tanh(c_t)
print(f_t, i_t, o_t, c_tilde, c_t, h_t)
# 0.76852... 0.51499... 0.71910... 0.70642... 1.51663... 0.65326...
Пример 2 (крайний случай — вентили на границах интервала). Пусть на некотором шаге сеть выучила веса так, что $f_t\approx0{,}02$ (почти полное забывание), $i_t\approx0{,}98$ (почти полное принятие нового), $o_t\approx0{,}95$ (почти полный вывод наружу), $c_{t-1}=3{,}0$, $\tilde c_t=-1{,}5$. Тогда $c_t=0{,}02\cdot3{,}0+0{,}98\cdot(-1{,}5)=0{,}06-1{,}47=-1{,}41$ — старое значение памяти ($3{,}0$) почти полностью вытеснено новым кандидатом. Такое поведение типично на границе смысловых единиц последовательности: например, при обработке текста по словам сеть может резко «обнулять» память в момент точки конца предложения, начиная копить контекст заново для следующего предложения, — ровно это и делает низкое значение forget gate на конкретном шаге.
Пример 3 (независимость вентилей друг от друга на одном и том же шаге). Продолжим пример 1: там на одном и том же шаге получились $f_t\approx0{,}77$ (в основном сохранить), $i_t\approx0{,}52$ (наполовину добавить новое) и $o_t\approx0{,}72$ (в основном показать наружу). Если бы вентили не были независимыми, а, скажем, input gate был бы жёстко завязан на forget gate (например, $i_t=1-f_t$, как иногда упрощают в некоторых модификациях), сеть потеряла бы возможность одновременно и активно сохранять старую память, и активно добавлять новую информацию — а именно эта возможность нужна, когда, например, при чтении текста нужно и держать в памяти подлежащее предложения (сохранить), и сразу же заметить только что прочитанное важное прилагательное (добавить). Три независимых набора весов $W_f, W_i, W_o$ дают сети эту степень свободы: количество «сохранить» и количество «добавить» подбираются раздельно, под конкретные данные конкретной задачи.
Почему это важно
Три вентиля — это не произвольное усложнение архитектуры, а минимальный набор независимых решений, необходимых, чтобы память полноценно управлялась данными: что оставить, что добавить, что показать. Каждый вентиль — это обычный полносвязный слой с сигмоидой, ничего экзотического с точки зрения вычислений, и все три (вместе с кандидатом $\tilde c_t$) обучаются обычным градиентным спуском заодно со всей остальной сетью — сеть сама, без ручной настройки, находит, при каких условиях входа и предыдущего состояния каждый вентиль должен открываться, а при каких — закрываться. Именно эта обучаемая избирательность, а не какая-то одна «умная» формула, и объясняет, почему LSTM способна выучивать зависимости на десятки и сотни шагов вперёд там, где обычная RNN не справляется даже с зависимостями на 10–15 шагов.
Почему сигмоидные вентили решают проблему градиента
Интуиция
Ключевой математический эффект, ради которого всё это построено, — то, как вентили влияют на градиент при обратном распространении ошибки во времени. В обычной RNN градиент, проходя от шага $t$ назад к шагу $t-k$, умножается $k$ раз на одну и ту же (!) матрицу весов $W_h$ вместе с производной $\tanh$ — если собственные значения этого произведения хоть немного меньше единицы, результат уменьшается экспоненциально с ростом $k$, потому что множитель на каждом шаге один и тот же. В LSTM производная $c_t$ по $c_{t-1}$ — это, в главном приближении, просто значение forget gate: $\partial c_t/\partial c_{t-1}\approx f_t$. Разница принципиальна в двух местах: во-первых, $f_t$ — это не фиксированное число, а значение, вычисленное по текущим данным заново на каждом шаге (то есть оно может быть близко к единице именно тогда, когда нужно пронести информацию далеко, и близко к нулю, когда её пора отбросить); во-вторых, сигмоида, дающая $f_t\in(0,1)$, позволяет сети обучить forget gate так, чтобы он держался близко к $1$ столько шагов, сколько нужно для конкретной долгосрочной зависимости — то есть уровень сохранения сигнала стал предметом обучения, а не жёстко заданным свойством архитектуры.
Формула
Производная ячейки памяти по цепному правилу (главный член). Из $c_t=f_t\odot c_{t-1}+i_t\odot\tilde c_t$ по правилу дифференцирования произведения и суммы:
$$\frac{\partial c_t}{\partial c_{t-1}} = f_t + c_{t-1}\odot\frac{\partial f_t}{\partial c_{t-1}} + \ldots \approx f_t$$(дополнительные слагаемые через зависимость вентилей от $h_{t-1}$ существуют, но при анализе главного эффекта основное внимание уделяют прямому множителю $f_t$). При прохождении градиента через $k$ шагов подряд по пути ячейки памяти множитель — это произведение разных значений forget gate на разных шагах:
$$\frac{\partial c_t}{\partial c_{t-k}} \approx \prod_{j=t-k+1}^{t} f_j$$В отличие от обычной RNN, где аналогичное произведение — это $k$-кратное произведение одной и той же матрицы $W_h$ (с почти неизбежным экспоненциальным затуханием при собственных значениях меньше единицы), здесь каждый сомножитель $f_j\in(0,1)$ — самостоятельная, обучаемая, зависящая от данных величина, и сеть может подобрать веса вентиля так, чтобы $f_j$ держался близко к $1$ именно там, где нужна долгая память.
Разбор примеров
Пример 1 (числовой контраст: фиксированный вес RNN против обучаемого forget gate). Пусть в обычной RNN эффективный множитель градиента на каждом шаге (производная $\tanh$, умноженная на вес) равен $w=0{,}4$ — тогда через $30$ шагов множитель составит $0{,}4^{30}$, число порядка $10^{-12}$: градиент, по сути, полностью обнулился, и веса, отвечающие за события 30 шагов назад, практически не получают обучающего сигнала. Теперь предположим, что LSTM на той же задаче выучила держать forget gate около $f\approx0{,}95$ на всех этих 30 шагах (то есть сеть «решила», что эта информация важна надолго): $0{,}95^{30}=e^{30\ln0{,}95}=e^{30\cdot(-0{,}0513)}=e^{-1{,}539}\approx0{,}2146$ — то есть примерно $21{,}5\%$ исходного сигнала доходит до 30-го шага назад. Разница на много порядков: $10^{-12}$ против $0{,}215$ — именно это и означает фраза «LSTM решает проблему затухающего градиента» на практике (не устраняет её полностью в принципе, но делает управляемой и зависящей от того, чему сеть действительно должна научиться).
Пример 2 (чувствительность к значению forget gate — почему «близко к единице» так важно). Сравним $f=0{,}9$ и $f=0{,}99$ на горизонте в $50$ шагов. $0{,}9^{50}=e^{50\ln0{,}9}=e^{50\cdot(-0{,}10536)}=e^{-5{,}268}\approx0{,}00516$ — меньше одного процента сигнала. $0{,}99^{50}=e^{50\ln0{,}99}=e^{50\cdot(-0{,}01005)}=e^{-0{,}5025}\approx0{,}605$ — больше $60\%$ сигнала. Разница между $f=0{,}9$ и $f=0{,}99$ выглядит небольшой в исходном значении вентиля (девять сотых), но после $50$-кратного перемножения превращается в разницу почти в сто двадцать раз в итоговом множителе градиента. Именно поэтому сигмоида как функция активации вентилей удачна вдвойне: она не только даёт удобный диапазон $(0,1)$, но и позволяет сети через смещение $b_f$ обучить высокие значения forget gate (близкие к насыщению сигмоиды у единицы) там, где длинная память действительно нужна, — а такие значения, как видно из расчёта, качественно меняют дальность памяти сети.
Пример 3 (произведение разных значений вентиля вдоль конкретной последовательности). Пусть при обработке реального предложения forget gate по пяти последовательным шагам принял значения $f_1=0{,}9$, $f_2=0{,}85$, $f_3=0{,}95$, $f_4=0{,}99$, $f_5=0{,}4$ (например, шаг $5$ — это токен конца предложения, где сеть решила частично сбросить память). Произведение: $0{,}9\cdot0{,}85=0{,}765$; $0{,}765\cdot0{,}95\approx0{,}727$; $0{,}727\cdot0{,}99\approx0{,}719$; $0{,}719\cdot0{,}4\approx0{,}288$. Итоговый множитель градиента через эти пять шагов $\approx0{,}288$, то есть чуть меньше трети исходного сигнала дошло бы через путь ячейки памяти. Обрати внимание, что решающий вклад в затухание внёс именно последний, низкий множитель $f_5=0{,}4$ — четыре предыдущих высоких значения вентиля почти не ослабили сигнал, а один «разрыв памяти» сразу заметно его срезал. Это иллюстрирует, что сеть не обязана держать forget gate постоянно высоким — она обучается открывать и закрывать этот кран именно там, где это отражает реальную структуру данных (границы предложений, смена темы, конец смыслового блока).
Почему это важно
Сигмоидные вентили превращают вопрос «затухнет ли градиент» из фиксированного, наперёд заданного свойства архитектуры (как в обычной RNN, где ответ зависит только от собственных значений одной и той же обученной один раз матрицы $W_h$) в вопрос, на который сеть отвечает по-разному для каждого конкретного входа и каждого конкретного временного интервала. Это не математическое чудо, устраняющее проблему навсегда, — при неудачной инициализации или неудачном обучении вентили вполне могут держаться низкими и затухание всё равно произойдёт, — но это качественно другая ситуация: у оптимизатора появляется прямой, легко используемый рычаг (смещение $b_f$ вентиля, которое часто инициализируют положительным числом специально, чтобы форсировать высокие значения $f_t$ на старте обучения) для того, чтобы держать градиент живым именно тогда, когда задача этого требует. Это и есть содержательный ответ на вопрос «почему LSTM обучается на длинных последовательностях там, где обычная RNN не справляется» — не магия, а перенос ответственности за длину эффективной памяти с фиксированной архитектуры на обучаемые, зависящие от данных параметры.
GRU: управляемый рекуррентный блок как упрощение LSTM
Интуиция
GRU задаёт вопрос: а нужны ли для решения проблемы затухающего градиента все три вентиля и отдельная ячейка памяти, или похожего эффекта можно достичь дешевле? Ответ Чо и соавторов 2014 года — два вентиля вместо трёх и полный отказ от отдельной ячейки $c_t$: скрытое состояние $h_t$ в GRU одновременно играет и роль памяти, и роль выхода, как в обычной RNN, но его обновление устроено так, что путь от $h_{t-1}$ к $h_t$ по-прежнему в основном аддитивный, а не полностью перезаписывающий. Update gate $z_t$ решает, в какой пропорции смешать старое состояние и новый кандидат (эта роль по сути объединяет функции forget gate и input gate LSTM — если $z_t$ высокий, много нового заходит и много старого забывается, и наоборот). Reset gate $r_t$ решает, насколько сильно учитывать предыдущее состояние при формировании самого кандидата — это более тонкая настройка, позволяющая сети «временно забыть» прошлое именно при вычислении новой информации, не трогая при этом финальное смешивание.
Формула
Вентили и обновление GRU:
$$z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z) \qquad \text{(update gate)}$$$$r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r) \qquad \text{(reset gate)}$$
$$\tilde h_t = \tanh(W_h \cdot [r_t \odot h_{t-1},\ x_t] + b_h) \qquad \text{(кандидат нового состояния)}$$
$$h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde h_t$$
Обрати внимание на структуру последней формулы: она устроена как взвешенное среднее между старым состоянием и новым кандидатом с обучаемым, зависящим от данных весом $z_t$ — в отличие от LSTM, где $f_t$ и $i_t$ полностью независимы и их сумма не обязана равняться единице. У GRU три набора весов ($W_z, W_r, W_h$) вместо четырёх у LSTM.
Разбор примеров
Пример 1 (полный численный проход через один шаг GRU). Пусть $h_{t-1}=0{,}4$, $x_t=-0{,}5$. Веса: $w_{zh}=0{,}6,\ w_{zx}=0{,}4,\ b_z=0{,}1$; $w_{rh}=0{,}2,\ w_{rx}=-0{,}3,\ b_r=0{,}0$; $w_{hh}=0{,}5,\ w_{hx}=0{,}7,\ b_h=-0{,}2$. Update gate: $0{,}6\cdot0{,}4+0{,}4\cdot(-0{,}5)+0{,}1=0{,}24-0{,}2+0{,}1=0{,}14$, значит $z_t=\sigma(0{,}14)\approx0{,}5349$. Reset gate: $0{,}2\cdot0{,}4+(-0{,}3)\cdot(-0{,}5)+0{,}0=0{,}08+0{,}15=0{,}23$, значит $r_t=\sigma(0{,}23)\approx0{,}5572$. Кандидат: сначала $r_t\odot h_{t-1}=0{,}5572\cdot0{,}4\approx0{,}2229$; затем $w_{hh}\cdot0{,}2229+w_{hx}\cdot x_t+b_h=0{,}5\cdot0{,}2229+0{,}7\cdot(-0{,}5)-0{,}2\approx0{,}1114-0{,}35-0{,}2=-0{,}4386$, значит $\tilde h_t=\tanh(-0{,}4386)\approx-0{,}4116$. Итоговое состояние: $h_t=(1-0{,}5349)\cdot0{,}4+0{,}5349\cdot(-0{,}4116)\approx0{,}4651\cdot0{,}4+0{,}5349\cdot(-0{,}4116)\approx0{,}1860-0{,}2202\approx-0{,}0342$. Один шаг GRU потребовал вычислить всего два вентиля и один кандидат — против трёх вентилей и одного кандидата у LSTM из предыдущего раздела.
Пример 2 (интерпретация крайних значений update gate). Возьмём $h_{t-1}=5{,}0$ и $\tilde h_t=-2{,}0$ и сравним два случая. Если $z_t=0{,}9$ (высокое значение — сеть решила сильно обновить состояние): $h_t=(1-0{,}9)\cdot5{,}0+0{,}9\cdot(-2{,}0)=0{,}5-1{,}8=-1{,}3$ — итоговое состояние сильно сдвинулось к новому кандидату. Если $z_t=0{,}1$ (низкое значение — сеть решила почти не менять состояние): $h_t=0{,}9\cdot5{,}0+0{,}1\cdot(-2{,}0)=4{,}5-0{,}2=4{,}3$ — состояние осталось почти таким же, как было. Обрати внимание на симметрию этой формулы с LSTM: высокий $z_t$ в GRU играет одновременно роль низкого forget gate и высокого input gate LSTM (агрессивное обновление памяти), а низкий $z_t$ — роль высокого forget gate и низкого input gate (бережное сохранение). GRU жёстко связывает эти два решения в одно, тогда как LSTM оставляет их независимыми — именно в этом и состоит главное структурное упрощение.
Пример 3 (роль reset gate — когда его стоит закрывать почти до нуля). Пусть $r_t\approx0$: тогда произведение $r_t\odot h_{t-1}\approx0$, и формула кандидата упрощается до $\tilde h_t\approx\tanh(W_{hx}\cdot x_t+b_h)$ — кандидат вычисляется практически без оглядки на прошлое состояние, только по текущему входу. Такое поведение полезно ровно в те моменты, когда предыдущий контекст должен быть отброшен при формировании нового кандидата — например, при начале нового, тематически не связанного предложения в тексте: старое скрытое состояние всё ещё используется при финальном смешивании через $z_t$, но сама «свежая идея» $\tilde h_t$ формируется как будто с чистого листа. Такая тонкая настройка — то, чего у LSTM в явном виде нет: там кандидат $\tilde c_t$ всегда учитывает $h_{t-1}$ напрямую, без отдельного вентиля, регулирующего именно эту зависимость.
Почему это важно
GRU показывает, что для смягчения затухающего градиента не обязательна ровно та архитектура, которую предложили Хохрайтер и Шмидхубер, — важен сам принцип: аддитивный, а не полностью перезаписывающий путь обновления состояния плюс обучаемые сигмоидные коэффициенты, решающие, в какой пропорции смешивать старое и новое. По формуле $h_t=(1-z_t)h_{t-1}+z_t\tilde h_t$ производная $\partial h_t/\partial h_{t-1}$ в главном приближении равна $(1-z_t)$ — точно так же, как в LSTM $\partial c_t/\partial c_{t-1}\approx f_t$, это обучаемое число из $(0,1)$, а не фиксированный эффект повторного умножения на одну и ту же матрицу. Практическая выгода — на четверть меньше параметров (три матрицы весов вместо четырёх), а значит быстрее обучение и меньше риск переобучения на небольших датасетах, при качестве, которое эмпирически часто оказывается сопоставимым с LSTM, а иногда и лучше — но не универсально: для отдельных задач с очень длинными и сложными зависимостями отдельная ячейка памяти LSTM и независимость трёх вентилей всё ещё дают заметное преимущество.
LSTM vs GRU vs простая RNN: практическое сравнение
Три архитектуры решают одну и ту же задачу — обработку последовательностей — с разной степенью сложности механизма памяти, и выбор между ними на практике определяется конкретными ограничениями задачи, а не абстрактным «что лучше».
Сравнение по практическим критериям.
- Число обучаемых весовых матриц на слой: простая RNN — $1$; GRU — $3$ (update, reset, кандидат); LSTM — $4$ (forget, input, output, кандидат). При одинаковом размере скрытого состояния $m$ и размере входа $n$ число параметров растёт линейно с этими коэффициентами: LSTM требует примерно в $4/3\approx1{,}33$ раза больше весов, чем GRU, и в $4$ раза больше, чем простая RNN.
- Способность выучивать долгосрочные зависимости: простая RNN — слабая (десятки шагов на практике, часто меньше); GRU — сильная; LSTM — сильная, а на отдельных задачах с очень длинными и структурно сложными зависимостями — чуть сильнее GRU за счёт независимости трёх вентилей и отдельной, дольше живущей ячейки памяти.
- Скорость обучения и инференса (при равном размере скрытого состояния): простая RNN — самая быстрая, но малополезная из-за слабой памяти; GRU — быстрее LSTM примерно на 25–30% за счёт меньшего числа матричных умножений на шаг; LSTM — медленнее всего из трёх, но всё ещё на порядки быстрее полного перебора альтернатив без рекуррентности.
- Типичный сценарий выбора на практике: простая RNN — почти никогда не выбирается для реальных задач (годится разве что для учебных примеров или очень коротких последовательностей); GRU — небольшие датасеты, мобильные и встраиваемые приложения, ограниченный вычислительный бюджет, задачи, где скорость итерации экспериментов важнее последнего процента качества; LSTM — задачи с заведомо длинными и сложными зависимостями, достаточный вычислительный бюджет, ситуации, где ранее уже подтверждено эмпирическое превосходство LSTM над GRU на похожей задаче.
Важно подчеркнуть: ни теория, ни практика не дают универсального победителя между LSTM и GRU. Сравнительное исследование Чунга, Гульчере, Чо и Бенджио 2014 года и множество последующих работ показывают, что на одних задачах чуть лучше LSTM, на других — GRU, а разница в качестве часто меньше, чем разброс от случайной инициализации или подбора гиперпараметров. Разумная практическая стратегия — начать с GRU как более дешёвого варианта (особенно при ограниченном датасете или бюджете) и переходить на LSTM, если экспериментально видно, что более богатая, независимая система вентилей и отдельная ячейка памяти дают измеримый прирост качества именно на твоей задаче.
Практика: 30 заданий
Базовые задания (1–10)
Задание 1: Найди forget gate LSTM, если $h_{t-1}=1{,}0$, $x_t=1{,}0$, $w_{fh}=0{,}5$, $w_{fx}=0{,}3$, $b_f=0{,}2$.
Задание 2: Найди input gate LSTM при $h_{t-1}=1{,}0$, $x_t=1{,}0$, $w_{ih}=0{,}2$, $w_{ix}=0{,}1$, $b_i=0{,}2$.
Задание 3: Найди output gate LSTM при $h_{t-1}=1{,}0$, $x_t=1{,}0$, $w_{oh}=0{,}6$, $w_{ox}=0{,}7$, $b_o=0{,}2$.
Задание 4: Найди кандидата $\tilde c_t$ LSTM при $h_{t-1}=1{,}0$, $x_t=1{,}0$, $w_{ch}=0{,}4$, $w_{cx}=0{,}4$, $b_c=0{,}2$.
Задание 5: Используя результаты заданий 1, 2 и 4 ($f_t\approx0{,}7311$, $i_t\approx0{,}6225$, $\tilde c_t\approx0{,}7616$) и $c_{t-1}=2{,}0$, найди $c_t$.
Задание 6: Используя результат задания 3 ($o_t\approx0{,}8176$) и результат задания 5 ($c_t\approx1{,}9362$), найди $h_t$.
Задание 7 (машинное обучение): Почему для вентилей LSTM и GRU используется именно сигмоида, а не $\tanh$ или ReLU?
Задание 8: Что означает для памяти LSTM значение forget gate $f_t=0$ и что означает $f_t=1$?
Задание 9: Сколько независимых наборов весовых матриц (вентили плюс кандидат) у LSTM и сколько у GRU?
Задание 10: Число параметров одного слоя LSTM (со смещениями) при размере входа $n$ и размере скрытого состояния $m$ равно $4\cdot(m\cdot(n+m)+m)$. Посчитай для $n=50$, $m=100$.
Средние задания (11–20)
Задание 11: Выполни полный шаг LSTM: $h_{t-1}=0{,}3$, $c_{t-1}=-0{,}5$, $x_t=0{,}8$; веса: $w_{fh}=0{,}7,\ w_{fx}=0{,}2,\ b_f=-0{,}1$; $w_{ih}=0{,}3,\ w_{ix}=0{,}5,\ b_i=0{,}0$; $w_{oh}=0{,}5,\ w_{ox}=0{,}3,\ b_o=0{,}1$; $w_{ch}=-0{,}4,\ w_{cx}=0{,}6,\ b_c=0{,}0$. Найди $c_t$ и $h_t$.
Задание 12: Выполни полный шаг GRU: $h_{t-1}=0{,}4$, $x_t=-0{,}5$; веса: $w_{zh}=0{,}6,\ w_{zx}=0{,}4,\ b_z=0{,}1$; $w_{rh}=0{,}2,\ w_{rx}=-0{,}3,\ b_r=0{,}0$; $w_{hh}=0{,}5,\ w_{hx}=0{,}7,\ b_h=-0{,}2$. Найди $z_t$, $r_t$, $\tilde h_t$ и $h_t$.
Задание 13 (машинное обучение): Посчитай $0{,}9^{50}$ и $0{,}99^{50}$ и объясни, почему это иллюстрирует важность высоких значений forget gate для долгосрочных зависимостей.
Задание 14: Сравни затухание градиента простой RNN с фиксированным множителем $w=0{,}4$ и LSTM с forget gate $f=0{,}95$ на горизонте в $30$ шагов.
Задание 15 (машинное обучение): На вход torch.nn.LSTM(input_size=64, hidden_size=128, num_layers=1, batch_first=True) подаётся тензор формы $(32, 10, 64)$ (batch, seq_len, features). Какая форма у output, h_n и c_n?
Задание 16 (машинное обучение): В чём разница между ролью ячейки памяти $c_t$ и скрытого состояния $h_t$ в LSTM?
Задание 17: Посчитай число параметров LSTM-слоя (по формуле $4\cdot(m(n+m)+m)$) при $n=10$, $m=20$.
Задание 18: Посчитай число параметров GRU-слоя (по формуле $3\cdot(m(n+m)+m)$) при тех же $n=10$, $m=20$, и найди отношение к результату LSTM из задания 17.
Задание 19 (машинное обучение): В каких практических ситуациях стоит предпочесть GRU вместо LSTM?
Задание 20: Что такое peephole-соединения в LSTM и зачем они нужны?
Продвинутые задания (21–30)
Задание 21 (машинное обучение): Объясни, почему путь градиента через ячейку памяти LSTM принципиально отличается от пути градиента через скрытое состояние обычной RNN с точки зрения того, что именно перемножается на каждом шаге.
Задание 22: Forget gate по пяти последовательным шагам принял значения $0{,}9$, $0{,}85$, $0{,}95$, $0{,}99$, $0{,}4$. Найди итоговый множитель градиента через путь ячейки памяти за эти пять шагов.
Задание 23 (машинное обучение): Проведи параллель между ячейкой памяти LSTM и skip connection ResNet (урок 337): в чём сходство и в чём принципиальное отличие?
Задание 24: Реализуй один шаг LSTM-ячейки на NumPy по образцу разобранного примера.
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def lstm_cell_step(h_prev, c_prev, x, weights):
# weights — словарь с ключами w_fh, w_fx, b_f, w_ih, w_ix, b_i,
# w_oh, w_ox, b_o, w_ch, w_cx, b_c
# твой код здесь: посчитай f_t, i_t, o_t, c_tilde, c_t, h_t
pass
Задание 25 (машинное обучение): Объясни историческую роль LSTM и GRU в период 2014–2020 годов и почему их во многом потеснили трансформеры.
Задание 26: Update gate GRU $z_t=0{,}9$, старое состояние $h_{t-1}=5{,}0$, кандидат $\tilde h_t=-2{,}0$. Затем повтори расчёт для $z_t=0{,}1$. Сравни результаты.
Задание 27 (машинное обучение): На двух разных задачах обученная LSTM показывает forget gate в среднем $\approx0{,}98$ на первой задаче и $\approx0{,}3$ на второй. Что это говорит о структуре каждой задачи?
Задание 28: Что означает «стек» LSTM-слоёв (как в GNMT с восемью слоями), и почему для обучения глубоких стеков LSTM понадобился приём, знакомый по уроку 337?
Задание 29 (машинное обучение): Покажи, что отношение числа параметров GRU к LSTM всегда равно $3/4$ независимо от конкретных $n$ и $m$, и подтверди на примере $n=m=256$.
Задание 30 (машинное обучение): Мобильное приложение для предсказания следующего слова при наборе текста работает на устройстве с ограниченной памятью и процессором. Другая задача — классификация длинных юридических документов с доступом к GPU-серверу без жёстких ограничений. Что бы ты порекомендовал для каждой задачи и почему?
Частые ошибки
Ниже — ошибки, которые регулярно встречаются у тех, кто только начинает работать с LSTM и GRU, вместе с тем, как их избежать.
-
Ошибка: путать ячейку памяти $c_t$ и скрытое состояние $h_t$, считая их одним и тем же объектом. Правильно: это две разные переменные с разными ролями — $c_t$ хранит информацию почти без искажений, $h_t$ получается из неё дополнительной фильтрацией через $\tanh$ и output gate. Почему это важно: при работе с
torch.nn.LSTMзабвение об этом различии часто приводит к неверной интерпретации выходовh_nиc_nили к попытке использоватьc_nнапрямую как признак там, где нужен именноh_n. -
Ошибка: считать, что LSTM «полностью решает» проблему затухающего градиента, а не смягчает её. Правильно: forget gate ограничивает затухание, но не отменяет его полностью — при неудачном обучении вентиль вполне может держаться низким и затухание всё равно произойдёт; сама возможность держать высокие значения — это рычаг, а не гарантия. Почему это важно: на очень длинных последовательностях (сотни и тысячи шагов) даже LSTM может испытывать трудности, и понимание границ метода помогает вовремя рассмотреть альтернативы вроде attention-механизмов.
-
Ошибка: думать, что GRU — это просто «облегчённая» версия LSTM с автоматически худшим качеством. Правильно: качество GRU и LSTM эмпирически сопоставимо на многих задачах, победитель зависит от конкретных данных; экономия параметров GRU не означает потери качества по умолчанию. Почему это важно: слепой выбор LSTM «на всякий случай» без сравнения с GRU часто означает лишние вычисления без выигрыша в метрике.
-
Ошибка: забывать про порядок конкатенации и размерности при ручной реализации вентилей — путать $[h_{t-1}, x_t]$ с раздельными матрицами $W_h$ и $W_x$, что технически эквивалентно, но легко перепутать местами при переносе формул в код. Правильно: либо последовательно использовать одну объединённую матрицу на конкатенированный вектор, либо две отдельные матрицы с раздельным умножением — но не смешивать эти два подхода внутри одной реализации. Почему это важно: несогласованность размерностей — частый источник трудноуловимых ошибок при реализации LSTM/GRU «с нуля».
-
Ошибка: инициализировать смещение forget gate $b_f$ нулём (или, того хуже, отрицательным числом) по умолчанию. Правильно: на практике смещение forget gate часто инициализируют положительным числом (например, $1{,}0$), чтобы в начале обучения сеть по умолчанию держала $f_t$ близким к единице (сигмоида от положительного аргумента ближе к $1$) — это ускоряет обучение долгосрочных зависимостей, потому что сети не приходится с нуля «открывать» механизм памяти. Почему это важно: неудачная инициализация вентилей — частая, легко устранимая причина, по которой LSTM обучается заметно медленнее ожидаемого.
-
Ошибка: применять простую RNN там, где заведомо есть длинные зависимости, только потому что она проще в реализации. Правильно: как только в последовательности есть значимые зависимости на десятки шагов и больше, LSTM или GRU почти всегда практически обязательны — простая RNN на такой задаче будет систематически недообучаться из-за затухающего градиента. Почему это важно: экономия на архитектуре в такой ситуации оборачивается заметно худшим итоговым качеством модели, а не выигрышем в простоте.
Главное запомнить
-
LSTM (Long Short-Term Memory, долгая краткосрочная память) вводит отдельную ячейку памяти $c_t$, обновляемую преимущественно аддитивно, чтобы информация могла переноситься через много шагов времени почти без искажений.
-
Три вентиля LSTM — forget gate $f_t$, input gate $i_t$, output gate $o_t$ — независимо решают, что забыть из старой памяти, что добавить нового и что показать наружу как $h_t$.
-
Все вентили используют сигмоиду $\sigma(x)\in(0,1)$, что делает их интерпретируемыми «кранами» с частичным пропуском сигнала и предотвращает неограниченный рост или смену знака памяти.
-
Ключевая формула обновления памяти: $c_t=f_t\odot c_{t-1}+i_t\odot\tilde c_t$ — производная по $c_{t-1}$ в главном приближении равна $f_t$, а не фиксированному множителю, повторяемому на каждом шаге, как в обычной RNN.
-
Этот механизм структурно аналогичен skip connection ResNet (урок 337, $y=F(x)+x$) — оба добавляют путь для градиента в обход нелинейного преобразования, только ResNet делает это по глубине сети, а LSTM — по времени последовательности.
-
Разница с ResNet принципиальна: коэффициент skip connection ResNet зафиксирован и равен $1$, а коэффициент LSTM ($f_t$) — обучаемый и зависит от данных на каждом шаге.
-
GRU (Gated Recurrent Unit, управляемый рекуррентный блок) упрощает LSTM до двух вентилей (update $z_t$, reset $r_t$) без отдельной ячейки памяти, сокращая число параметров примерно на 25%.
-
Универсального победителя между LSTM и GRU по качеству не существует — выбор определяется конкретной задачей, размером датасета и вычислительным бюджетом.
-
С 2014 по примерно 2020 год LSTM и GRU были фактическим промышленным стандартом для NLP и временных рядов (включая Google Neural Machine Translation, 2016), пока их во многом не потеснили трансформеры за счёт параллелизуемости вычислений.
-
Затухание градиента LSTM и GRU смягчают, но не устраняют полностью — при неудачном обучении вентилей проблема всё ещё может проявляться на очень длинных последовательностях.
Связь с темами курса
Этот урок напрямую продолжает урок 339 о рекуррентных нейронных сетях: там была сформулирована проблема (затухающий градиент из-за повторяющегося умножения на одну и ту же матрицу $W_h$ и повторяющегося прохождения через $\tanh$), здесь дано её архитектурное решение. Формула скрытого состояния простой RNN $h_t=\tanh(W_h h_{t-1}+W_x x_t+b)$ и формулы вентилей LSTM/GRU используют одни и те же строительные блоки — линейное преобразование плюс нелинейность, — но принципиально иначе комбинируют их: у RNN преобразование единственное и полностью перезаписывающее, у LSTM и GRU — многочастное, с явно выделенным аддитивным путём для памяти.
Связь с уроком 337 про ResNet — не просто аналогия «для иллюстрации», а структурно точное соответствие. Формула остаточного блока $y=F(x)+x$ и формула ячейки памяти $c_t=f_t\odot c_{t-1}+i_t\odot\tilde c_t$ решают одну и ту же математическую задачу — создать путь, по которому градиент при обратном распространении не обязан проходить исключительно через сжимающие нелинейности и матричные умножения, — только ResNet разворачивает эту идею по оси глубины сети (от первого слоя к последнему), а LSTM разворачивает её по оси времени (от первого временного шага к последнему). Разница в степени «жёсткости» пути (фиксированный коэффициент $1$ у ResNet против обучаемого $f_t\in(0,1)$ у LSTM) — не случайность, а следствие разных задач: в ResNet проблема в том, что слой должен уметь легко выучить тождественное отображение, а не в том, что оно должно применяться избирательно; в LSTM же именно избирательность — какую часть памяти сохранить, а какую заменить — и есть главный содержательный смысл механизма. Когда позже в курсе (урок 337 уже показал это на примере трансформерного блока) ты встретишь блок трансформера с его собственными skip connection вокруг self-attention и feed-forward подслоёв, ты увидишь третье воплощение того же принципа — уже в архитектуре, которая во многом сменила LSTM и GRU на посту стандарта для последовательностей.
Наконец, урок 341 (Encoder-Decoder архитектура) непосредственно использует LSTM или GRU как строительный блок: именно в контексте задачи encoder-decoder для машинного перевода GRU и был впервые предложен Чо и соавторами в 2014 году, а знаменитая система GNMT 2016 года — это глубокий encoder-decoder стек именно на LSTM. Понимание механики вентилей и ячейки памяти из этого урока — необходимая база для следующего шага: как две рекуррентные сети (кодировщик и декодировщик) совместно решают задачу перевода одной последовательности в другую.
Интересные факты
-
Статью Хохрайтера и Шмидхубера 1997 года про LSTM поначалу отклоняли рецензенты — идея казалась излишне сложной по сравнению с господствовавшими тогда более простыми архитектурами, и потребовались годы, прежде чем сообщество осознало её значимость.
-
Forget gate — не часть оригинальной архитектуры 1997 года. Без него сеть могла только накапливать значения в ячейке памяти, и уже на умеренно длинных последовательностях это приводило к неограниченному росту $c_t$ и срыву обучения; вентиль забывания добавили только в 2000 году Герс, Шмидхубер и Каммингс.
-
Google Neural Machine Translation (GNMT), запущенная в 2016 году, использовала глубокий стек из восьми LSTM-слоёв кодировщика и восьми слоёв декодировщика со skip connections между слоями стека — и сократила количество ошибок перевода на 55–85% по сравнению с предыдущей статистической фразовой системой перевода за одно архитектурное обновление.
-
GRU была изобретена не как самостоятельная попытка «улучшить LSTM в вакууме», а как часть конкретной работы 2014 года о машинном переводе методом encoder-decoder — то есть с самого начала GRU создавалась и тестировалась именно на той задаче, которой посвящён следующий урок курса.
Лайфхаки
-
При реализации на PyTorch не пиши собственные циклы по времени вручную без необходимости —
torch.nn.LSTMиtorch.nn.GRUуже реализуют эффективный проход по всей последовательности (в том числе с использованием оптимизированных cuDNN-ядер на GPU), а ручная реализация полезна прежде всего для обучения себя механике, а не для продакшена. -
Если не уверен, что выбрать — GRU или LSTM, — начни с GRU: она обучается быстрее за счёт меньшего числа параметров, и если её качества достаточно для задачи, экономия вычислений того стоит; переходи на LSTM только если экспериментально видишь заметный прирост качества.
-
При обучении LSTM с нуля попробуй явно инициализировать смещение $b_f$ forget gate положительным числом (например, $1{,}0$) — это ускоряет ранние стадии обучения долгосрочных зависимостей, потому что вентиль по умолчанию открыт, а не закрыт.
-
Используй
bidirectional=Trueвtorch.nn.LSTM/torch.nn.GRU, когда вся последовательность доступна целиком заранее (например, при классификации целого текста, а не при потоковой генерации) — двунаправленный проход учитывает контекст и слева, и справа от каждой позиции, что часто заметно улучшает качество. -
Не забывай про градиентный клиппинг (
torch.nn.utils.clip_grad_norm_) даже с LSTM и GRU — вентили смягчают именно затухание градиента, а не его взрыв; на длинных последовательностях или при неудачной инициализации градиенты всё ещё могут расти неконтролируемо, и клиппинг остаётся стандартной защитной мерой. -
Для быстрой диагностики, действительно ли модель использует долгосрочную память, попробуй логировать средние значения forget gate по батчу во время обучения — устойчиво высокие значения ($>0{,}9$) на большинстве шагов обычно говорят о том, что сеть активно выучивает удержание долгосрочного контекста, а не просто «шумит» случайными весами.
Ты только что разобрал архитектуру, которая почти десятилетие была рабочей лошадкой всей прикладной обработки последовательностей — от переводчика в твоём телефоне до систем распознавания речи и прогноза финансовых рынков. Формулы вентилей и ячейки памяти, которые ты теперь умеешь считать вручную и реализовывать на NumPy и PyTorch, — это не музейный экспонат, а живой инструмент, который до сих пор оправданно выбирают там, где данных немного, а вычислительный бюджет ограничен. В следующем уроке ты увидишь, как пара таких рекуррентных сетей — кодировщик и декодировщик — совместно решает задачу перевода одной последовательности в другую, а ещё через пару уроков — как та же самая идея аддитивного пути для градиента, которую ты только что разобрал на примере ячейки памяти, всплывёт в основе архитектуры, которая во многом определила лицо современного ИИ.
Понял тему? Закрепи в боте! 🚀
Попрактикуйся на задачах и получи персональные рекомендации от AI
💪 Начать тренировку