Машинное обучение
Рекуррентные сети: RNN, LSTM, GRU
В 2016 году Google полностью заменил свою систему перевода - вместо разбора фраз по правилам запустил нейросеть на основе LSTM. Качество перевода улучшилось за одно обновление больше, чем за предыдущие десять лет разработки. Секрет оказался в способности LSTM "помнить" контекст целого предложения: подлежащее в начале влияет на глагол в конце, а тон первой фразы определяет перевод последней. Как нейросеть научилась удерживать такие длинные зависимости, если обычные рекуррентные сети забывают всё через 10-20 шагов?
- **Машинный перевод** - Google Neural Machine Translation (GNMT) использовал 8-слойный Bidirectional LSTM для кодирования исходного предложения и однонаправленный LSTM для генерации перевода, обрабатывая 100+ миллиардов слов за день
- **Распознавание речи** - Apple Siri, Google Assistant и Amazon Alexa используют LSTM/GRU для преобразования аудиосигнала в текст, обрабатывая сотни временных шагов спектрограммы в реальном времени
- **Финансовые прогнозы** - GRU и LSTM применяются для предсказания цен акций и обнаружения аномалий в транзакциях, где паттерны во временных рядах содержат зависимости на разных временных масштабах
Предварительные знания
Как сети научили запоминать
В 1990 году когнитивист Джеффри Элман предложил простую рекуррентную сеть со слоем контекста, который возвращал предыдущее скрытое состояние обратно в сеть, давая ей короткую память о том, что было раньше. Эти сети Элмана умели моделировать последовательности, но плохо удерживали информацию на много шагов из-за затухания градиентов. Решение появилось в 1997 году у Зеппа Хохрайтера и Юргена Шмидхубера: их Long Short-Term Memory (LSTM) добавила управляемые вентилями ячейки памяти, способные переносить информацию через сотни шагов. В 2014 году Кёнхён Чо с коллегами предложили GRU - упрощённый вентильный блок с меньшим числом параметров, часто не уступавший LSTM, и вентильная рекуррентность стала основой моделирования последовательностей, пока её не сменило внимание.
Моделирование последовательностей
Свёрточные сети отлично работают с изображениями, но что если данные - это **последовательность**, где важен порядок? Текст "собака укусила человека" и "человек укусил собаку" содержат одни и те же слова, но смысл противоположный. Временной ряд температуры за неделю, аудиозапись голоса, последовательность ДНК - везде порядок элементов несёт критическую информацию. Обычная нейросеть (fully connected) принимает на вход вектор фиксированной длины и не учитывает порядок. **Рекуррентная нейросеть (RNN)** решает эту проблему: она обрабатывает элементы один за другим, передавая "память" о предыдущих шагах через скрытое состояние (hidden state).
На каждом временном шаге t RNN получает два входа: текущий элемент последовательности x_t и скрытое состояние с предыдущего шага h_{t-1}. Формула простая: **h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b)**. Здесь W_hh - матрица весов для скрытого состояния ("что помнить из прошлого"), W_xh - матрица весов для входа ("что взять из нового"), tanh сжимает результат в диапазон [-1, 1]. Скрытое состояние h_t - это сжатое представление всей истории от начала последовательности до шага t.
Главная проблема vanilla RNN - **затухание градиентов (vanishing gradients)**. При обратном распространении через время (Backpropagation Through Time, BPTT) градиенты умножаются на матрицу W_hh на каждом шаге. Если собственные значения W_hh меньше 1, градиент экспоненциально уменьшается. После 10-20 шагов градиент становится настолько мал, что сеть не может обучиться зависимостям на длинных расстояниях. Например, в предложении "Я вырос в России, ходил в русскую школу, потом уехал учиться за границу, и до сих пор свободно говорю на ... (русском)" - RNN забудет "Россию" к моменту предсказания языка.
**Проблема затухания градиентов - наглядно:** Градиент на шаге 0 при длине последовательности T: `grad ~ (W_hh)^T * (1 - tanh^2(...))` Если W_hh дает множитель 0.9 на каждом шаге: - 10 шагов: 0.9^10 = 0.35 (градиент потерял 65%) - 20 шагов: 0.9^20 = 0.12 (потерял 88%) - 50 шагов: 0.9^50 = 0.005 (потерял 99.5%) Сеть "забывает" ранние элементы. Это фундаментальное ограничение vanilla RNN, которое решает LSTM.
Почему vanilla RNN не может выучить зависимости между элементами, разделёнными 50+ шагами в последовательности?
LSTM - долгая память
**Long Short-Term Memory (LSTM)** была предложена Хохрайтером и Шмидхубером в 1997 году для решения проблемы затухания градиентов. Ключевая идея: добавить **cell state (состояние ячейки)** - отдельную линию передачи информации, которая проходит через всю последовательность почти без изменений. Аналогия: представьте конвейерную ленту на заводе. Предметы (информация) двигаются по ленте от начала до конца. На каждой станции (временном шаге) рабочие могут: **убрать** ненужные предметы (forget gate), **положить** новые (input gate), **взять** что-то для текущей задачи (output gate). Но сама лента двигается непрерывно - информация сохраняется.
**Forget gate** решает, какую информацию выбросить из cell state. Он смотрит на предыдущее скрытое состояние h_{t-1} и текущий вход x_t и выдаёт числа от 0 до 1 для каждого элемента cell state: 0 - "полностью забыть", 1 - "полностью сохранить". Например, при обработке текста: если модель встретила новое подлежащее, forget gate может сбросить информацию о старом подлежащем.
**Input gate** решает, какую новую информацию записать в cell state. Он состоит из двух частей: sigmoid-слой i_t выбирает, какие значения обновить (0 или 1), а tanh-слой создаёт вектор-кандидат c~_t с новыми значениями (от -1 до 1). Результат: i_t * c~_t - только отобранные новые значения добавляются к cell state. Обновление cell state: **c_t = f_t * c_{t-1} + i_t * c~_t** - это аддитивная операция, и именно она решает проблему затухания градиентов.
**Почему LSTM решает проблему затухания градиентов?** В vanilla RNN: h_t = tanh(W * h_{t-1} + ...) - мультипликативное обновление. Градиент умножается на W на каждом шаге. В LSTM: c_t = f_t * c_{t-1} + i_t * c~_t - **аддитивное обновление**. Градиент по c_t относительно c_{t-1} равен f_t (значение forget gate). Если f_t близко к 1, градиент проходит практически без изменений через любое количество шагов. Это как разница между: - 0.9 * 0.9 * 0.9 * ... (50 раз) = 0.005 (vanilla RNN) - 1.0 * 1.0 * 1.0 * ... (50 раз) = 1.0 (LSTM с f=1) Cell state - это "магистраль" для градиентов, аналогично skip connections в ResNet.
**Output gate** решает, какую часть cell state выдать наружу как скрытое состояние h_t. Cell state содержит долговременную память, но не вся она нужна на текущем шаге. Output gate фильтрует: **h_t = o_t * tanh(c_t)**. Например, cell state может хранить информацию о роде существительного (мужской/женский), но output gate выдаст её только когда нужно согласовать прилагательное.
Какую роль играет cell state в LSTM и почему он решает проблему затухания градиентов?
GRU - упрощённый LSTM
**Gated Recurrent Unit (GRU)** была предложена Чо и коллегами в 2014 году как упрощённая альтернатива LSTM. Главная идея: объединить forget gate и input gate в один **update gate**, а также убрать отдельный cell state - вся информация хранится в скрытом состоянии h_t. Вместо трёх гейтов у GRU только два: **reset gate** (что забыть из прошлого при создании кандидата) и **update gate** (баланс между старой и новой информацией). Меньше гейтов - меньше параметров - быстрее обучение.
**Reset gate** (r_t) контролирует, сколько прошлой информации использовать при создании нового кандидата. Когда r_t близко к 0, GRU "забывает" прошлое и создаёт кандидата почти исключительно на основе текущего входа - как будто начинает с чистого листа. Когда r_t близко к 1, прошлое полностью учитывается. **Update gate** (z_t) - это баланс между старым и новым: z_t = 0 означает "полностью сохранить старое состояние", z_t = 1 - "полностью заменить новым". Обратите внимание: (1 - z_t) + z_t = 1, то есть это всегда взвешенное среднее.
**LSTM vs GRU - сравнение:** - LSTM: 3 гейта (forget, input, output) + cell state + hidden state - GRU: 2 гейта (reset, update) + только hidden state Параметры (для hidden_size=256, input_size=128): - LSTM: 4 * (256 * 256 + 256 * 128 + 256) = 394,240 - GRU: 3 * (256 * 256 + 256 * 128 + 256) = 295,680 GRU на ~25% меньше параметров, чем LSTM. В формуле LSTM '4' - это 4 матрицы (forget, input, candidate, output). В формуле GRU '3' - это 3 матрицы (reset, update, candidate).
На практике разница между LSTM и GRU зачастую минимальна. Исследования (Chung et al., 2014; Greff et al., 2017) показывают, что ни одна архитектура не доминирует на всех задачах. GRU немного быстрее и лучше на маленьких датасетах за счёт меньшего числа параметров. LSTM стабильнее на очень длинных последовательностях благодаря отдельному cell state. Если у вас нет веских причин выбрать конкретную архитектуру - попробуйте обе и выберите по результатам валидации.
Чем update gate в GRU принципиально отличается от гейтов LSTM?
Bidirectional и Deep RNN
Обычная RNN (и LSTM/GRU) обрабатывает последовательность слева направо: на шаге t модель знает только x_0, x_1, ..., x_t. Но во многих задачах контекст **справа** не менее важен. Пример: в предложении "Я съел яблоко Apple" слово "Apple" можно понять только из правого контекста (если он есть) или общего смысла. В задаче Named Entity Recognition предложение "Apple выпустила новый iPhone" - слово "Apple" однозначно компания только благодаря слову "iPhone" справа. **Bidirectional RNN** решает это: она запускает **две** независимые RNN - одну слева направо, другую справа налево - и объединяет их выходы.
**Deep RNN (Stacked RNN)** - ещё один способ увеличить мощность модели: уложить несколько слоёв RNN друг на друга. Выход первого слоя становится входом для второго, и так далее. Каждый слой извлекает всё более абстрактные представления: первый слой может захватить простые паттерны (ударения, интонацию), второй - слова и фразы, третий - смысловые конструкции. На практике 2-3 слоя обычно достаточно; больше слоёв редко дают улучшение и значительно замедляют обучение.
**Teacher forcing** - важная техника обучения RNN для задач генерации (перевод, генерация текста). Проблема: при обучении без teacher forcing модель использует свои собственные предсказания как вход на следующем шаге. Если на шаге 3 она ошиблась, ошибка накапливается на шагах 4, 5, 6 и далее (error accumulation). Teacher forcing решает это: на каждом шаге подаём **правильный ответ** (ground truth) вместо предсказания модели. Это стабилизирует обучение, но создаёт разрыв между обучением и инференсом (exposure bias). Компромисс: **scheduled sampling** - постепенно уменьшаем долю teacher forcing в процессе обучения.
**Практические советы для RNN/LSTM/GRU:** 1. **Gradient clipping** - обязателен. Без него возможен gradient explosion (обратная сторона vanishing gradient). Типичное значение: clip_grad_norm = 1.0-5.0 2. **Правильная инициализация** - forget gate bias в LSTM инициализируйте значением 1.0-2.0 (чтобы сеть по умолчанию "помнила") 3. **Dropout** - используйте между слоями, НЕ внутри рекуррентных соединений (variational dropout - исключение) 4. **Длина последовательности** - для очень длинных последовательностей (>500 шагов) используйте truncated BPTT: обрезайте градиент каждые K шагов 5. **Packed sequences** - в PyTorch используйте pack_padded_sequence для эффективной обработки батчей с разной длиной
RNN/LSTM устарели после появления трансформеров и больше нигде не используются
LSTM и GRU остаются лучшим выбором для задач с небольшими последовательностями, ограниченными данными, и на встроенных устройствах, где ресурсы ограничены
Трансформеры требуют квадратичной памяти по длине последовательности (O(n^2)) и огромных датасетов для обучения. LSTM обрабатывает последовательность за O(n) памяти и хорошо работает на малых данных. В промышленных системах: датчики IoT, мобильные устройства, edge computing - LSTM часто единственный практичный вариант. Кроме того, LSTM проще в отладке и интерпретации, чем многоголовое внимание трансформеров.
Итоги
- **Vanilla RNN** передаёт скрытое состояние h_t между шагами через формулу h_t = tanh(W_hh * h_{t-1} + W_xh * x_t + b), но страдает от затухания градиентов - после 10-20 шагов информация о ранних элементах теряется
- **LSTM** решает проблему через cell state с аддитивными обновлениями и три гейта: forget (что забыть), input (что запомнить), output (что выдать) - градиенты проходят по cell state без экспоненциального затухания
- **GRU** упрощает LSTM до двух гейтов (reset и update), убирает отдельный cell state, имеет на 25% меньше параметров и обучается быстрее - на практике результаты сопоставимы с LSTM
- **Bidirectional RNN** обрабатывает последовательность в обоих направлениях, что критично для задач с полным контекстом (NER, классификация), но неприменим для генерации в реальном времени
- **От перевода по правилам к LSTM:** как в истории с Google Translate, рекуррентные сети научились удерживать контекст целого предложения - и хотя трансформеры сегодня доминируют на больших данных, LSTM остаётся незаменим для компактных моделей и ограниченных ресурсов
Связанные темы
Рекуррентные сети - мост между классическими нейросетями и современными архитектурами для последовательностей:
- Трансформеры — Архитектура, которая заменила RNN/LSTM в большинстве NLP-задач: механизм внимания (attention) позволяет обращаться к любому элементу последовательности напрямую, без передачи через скрытое состояние. Но требует O(n^2) памяти вместо O(n) у LSTM
- Sequence-to-Sequence модели — Архитектура encoder-decoder на основе LSTM/GRU для задач, где вход и выход - последовательности разной длины (перевод, саммаризация). Encoder сжимает вход в вектор, decoder генерирует выход
- Word Embeddings — Векторные представления слов (Word2Vec, GloVe) - стандартный вход для RNN/LSTM в задачах NLP. Преобразуют дискретные токены в плотные векторы, которые LSTM может обрабатывать
- CNN — Свёрточные сети обрабатывают пространственные паттерны (изображения), RNN - временные последовательности. Гибридные архитектуры (CNN + LSTM) используются для видео: CNN извлекает признаки из кадров, LSTM моделирует временные зависимости
Вопросы для размышления
- Почему аддитивное обновление cell state в LSTM (c_t = f*c_{t-1} + i*c~) решает проблему затухания градиентов, а мультипликативное обновление в vanilla RNN (h_t = tanh(W*h_{t-1})) - нет? Проведите аналогию с ResNet и skip connections.
- GRU связывает забывание и запоминание через один update gate: (1-z)*старое + z*новое. В каких случаях независимые forget и input гейты LSTM дают преимущество перед этим связанным механизмом?
- Если Bidirectional LSTM видит контекст в обоих направлениях, почему его нельзя использовать для генерации текста? Как трансформеры решают эту проблему, разделяя encoder и decoder?
Связанные уроки
- ml-29-cnn — Общая основа из нейросетей и backprop
- ml-31-transformers — Трансформеры заменили RNN на длинных последовательностях
- ml-36-seq2seq — RNN - классическая основа seq2seq
- ml-27-activation-functions — Гейты используют sigmoid и tanh
- stat-13-time-series — Оба моделируют временные зависимости
- alg-28-lcs