RNN, LSTM, GRU
🎯 Цель
После прочтения этой главы:
- Знаете архитектуру NN для работы с sequence data (текст, time series, audio)
- Знаете разницу между RNN, LSTM, GRU и когда какой использовать
- Понимаете проблему vanishing gradient и решение LSTM
- Пишете time series forecasting и text classification
- Готовы к переходу на Transformers (в Месяце 4)
**Внимание:**Сейчас эра — Transformers(BERT, GPT, T5). RNN/LSTM во многих случаях устаревают. Но в time series всё ещё полезны и важны для истории/intuition NN.
Что нужно изучить
- RNN — основы Recurrent Neural Network
- Проблема Vanishing/Exploding Gradient
- LSTM — Long Short-Term Memory
- GRU — Gated Recurrent Unit
- Bidirectional RNN/LSTM
- Seq2Seq — encoder-decoder
- Attention mechanism(мост к Transformers)
- Time series forecasting — sliding window approach
- Text classification with LSTM
Библиотеки
pip install torch torchtext pandas
Важные темы
RNN — Recurrent Neural Network
Sequence: [x₁, x₂, x₃, ...]
x₁ x₂ x₃
│ │ │
▼ ▼ ▼
[RNN] ──h₁──> [RNN] ──h₂──> [RNN] ──h₃──>
h_t = tanh(W_h · h_{t-1} + W_x · x_t + b)
Основная идея: предыдущий hidden state (h_{t-1}) вместе с текущим input формирует новый state.
Проблема Vanishing Gradient
В длинных sequence gradient проходя через tanh многократно стремится к нулю — модель не может выучить дальние dependency.
**Решение — LSTM:**сохранение/удаление информации через специальные «gate».
LSTM — полная структура
cell state (C)
──────────────►
↑ ↑ ↑
│ │ │
[forget] [input] [output]
gate gate gate
│ │ │
└────┴────┘
↑
h_t-1, x_t
3 gate:
- **Forget gate (f):**что удалить из cell state
- **Input gate (i):**что новое добавить
- **Output gate (o):**каким будет следующий hidden state
GRU — упрощённый LSTM
- 2 gate (reset, update)
- Быстрее LSTM, меньше параметров
- Точность равна или близка LSTM
Что когда?
| Use case | Рекомендация |
|---|---|
| Text classification | LSTM/GRU bidirectional, или BERT (Месяц 4) |
| Time series forecasting | LSTM, или Prophet/N-BEATS |
| Sentiment analysis | BERT (transformer) |
| Translation | Transformer (T5, MarianMT) |
| Sequence generation | GPT-style transformer |
| Audio processing | Conv1D + LSTM или wav2vec |
**Правило:**Начинайте новый проект с transformer. RNN/LSTM используйте только по реальной причине (маленький dataset, real-time inference, simple time series).
Примеры кода
Простая RNN
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, num_classes):
super().__init__()
self.rnn = nn.RNN(input_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, num_classes)
def forward(self, x):
# x shape: (batch, seq_len, input_size)
out, hidden = self.rnn(x)
# out shape: (batch, seq_len, hidden_size)
# берём последний timestep
last_output = out[:, -1, :]
logits = self.fc(last_output)
return logits
model = SimpleRNN(input_size=10, hidden_size=64, num_classes=5)
x = torch.randn(32, 20, 10) # batch=32, seq_len=20, features=10
print(model(x).shape) # (32, 5)
LSTM — Time Series Forecasting
class LSTMForecaster(nn.Module):
def __init__(self, input_size=1, hidden_size=64, num_layers=2, output_size=1):
super().__init__()
self.lstm = nn.LSTM(
input_size, hidden_size,
num_layers=num_layers,
batch_first=True,
dropout=0.2 if num_layers > 1 else 0,
)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
# x shape: (batch, seq_len, input_size)
out, (h_n, c_n) = self.lstm(x)
# Последний timestep
last_output = out[:, -1, :]
return self.fc(last_output)
Sliding window approach
def create_sequences(data, seq_length):
"""1D time series → (X, y) pairs."""
X, y = [], []
for i in range(len(data) - seq_length):
X.append(data[i:i + seq_length])
y.append(data[i + seq_length])
return torch.tensor(X, dtype=torch.float32).unsqueeze(-1), torch.tensor(y, dtype=torch.float32)
# Пример — прогноз sin function
import numpy as np
data = np.sin(np.linspace(0, 100, 1000))
X, y = create_sequences(data, seq_length=20)
# X shape: (980, 20, 1), y shape: (980,)
Text classification with LSTM
class TextClassifierLSTM(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes, n_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
self.lstm = nn.LSTM(
embed_dim, hidden_dim,
num_layers=n_layers,
batch_first=True,
bidirectional=True,
dropout=0.3,
)
# Bidirectional → hidden_dim * 2
self.fc = nn.Linear(hidden_dim * 2, num_classes)
self.dropout = nn.Dropout(0.5)
def forward(self, x, lengths=None):
# x shape: (batch, seq_len) — token IDs
embedded = self.embedding(x)
if lengths is not None:
# Для variable length sequences
packed = nn.utils.rnn.pack_padded_sequence(
embedded, lengths.cpu(), batch_first=True, enforce_sorted=False
)
_, (hidden, _) = self.lstm(packed)
else:
_, (hidden, _) = self.lstm(embedded)
# Bidirectional final hidden: forward + backward
hidden = torch.cat([hidden[-2], hidden[-1]], dim=1)
hidden = self.dropout(hidden)
return self.fc(hidden)
Training loop (для sequence data)
def train_sequence_model(model, train_loader, val_loader, epochs=20, lr=1e-3):
device = next(model.parameters()).device
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss() # для forecasting; для classification CrossEntropy
for epoch in range(epochs):
model.train()
train_loss = 0
for X, y in train_loader:
X, y = X.to(device), y.to(device)
optimizer.zero_grad()
pred = model(X)
loss = criterion(pred.squeeze(), y)
loss.backward()
# ВАЖНО: gradient clipping для RNN
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
train_loss += loss.item()
# Eval
model.eval()
val_loss = 0
with torch.no_grad():
for X, y in val_loader:
X, y = X.to(device), y.to(device)
pred = model(X)
val_loss += criterion(pred.squeeze(), y).item()
print(f"Epoch {epoch+1}: train={train_loss/len(train_loader):.4f} "
f"val={val_loss/len(val_loader):.4f}")
Encoder-Decoder (Seq2Seq) preview
class Encoder(nn.Module):
def __init__(self, input_size, hidden_size):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True)
def forward(self, x):
_, (h, c) = self.lstm(x)
return h, c # context
class Decoder(nn.Module):
def __init__(self, output_size, hidden_size):
super().__init__()
self.lstm = nn.LSTM(output_size, hidden_size, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x, h, c):
out, (h, c) = self.lstm(x, (h, c))
return self.fc(out), h, c
# Seq2Seq:
# encoder(input) → context
# decoder(<START>, context) → output_1
# decoder(output_1, context) → output_2
# ...
Интеграция с backend
Time series forecasting API
from fastapi import FastAPI
from pydantic import BaseModel
import torch
import numpy as np
app = FastAPI()
model = LSTMForecaster()
model.load_state_dict(torch.load("forecaster.pt"))
model.eval()
class ForecastInput(BaseModel):
historical_values: list[float]
forecast_steps: int = 7
class ForecastOutput(BaseModel):
predictions: list[float]
@app.post("/forecast", response_model=ForecastOutput)
@torch.no_grad()
def forecast(data: ForecastInput):
# Last 20 values as input
history = torch.tensor(data.historical_values[-20:], dtype=torch.float32)
history = history.unsqueeze(0).unsqueeze(-1) # (1, 20, 1)
predictions = []
for _ in range(data.forecast_steps):
pred = model(history).item()
predictions.append(pred)
# Slide window: drop first, append prediction
history = torch.cat([history[:, 1:, :], torch.tensor([[[pred]]])], dim=1)
return ForecastOutput(predictions=predictions)
Text sentiment API (LSTM)
@app.post("/sentiment")
@torch.no_grad()
def predict_sentiment(text: str):
tokens = tokenizer(text, max_length=200, padding="max_length", truncation=True)
X = torch.tensor([tokens]).long()
logits = model(X)
probs = torch.softmax(logits, dim=1).squeeze()
labels = ["negative", "neutral", "positive"]
return {
"sentiment": labels[probs.argmax().item()],
"scores": {label: float(p) for label, p in zip(labels, probs)},
}
**Внимание:**Для production sentiment использование HuggingFace BERTмногократно даёт лучший результат. LSTM здесь для примера.
Ресурсы
- Andrej Karpathy — “The Unreasonable Effectiveness of RNNs”(blog)
- Colah’s blog — Understanding LSTMs(colah.github.io/posts/2015-08-Understanding-LSTMs)
- PyTorch Sequence tutorials
- “Deep Learning for Time Series Forecasting” — Jason Brownlee
- fast.ai NLP course(RNN и beyond)
🏋️ Упражнения
🟢 Easy
- Сравните число параметров
nn.RNN,nn.LSTM,nn.GRU. - Next-step forecasting для sinusoidal data через LSTM.
- Сравните Bidirectional LSTM и unidirectional результат.
🟡 Medium
- Time series: forecasting на 30 дней по реальным данным stock price (yfinance).
- Text classification: binary sentiment на IMDB reviews dataset через LSTM (80%+).
- Char-level RNN: character-level text generation в стиле Karpathy.
🔴 Hard
- Seq2Seq translation — запустите на маленьком датасете (English ↔ German маленький dataset).
- Attention mechanism — добавьте attention над LSTM (введение в transformer).
- Time series API — сравните Prophet vs LSTM, лучшую модель deploy через FastAPI.
Capstone
notebooks/month-03/06_rnn_timeseries.ipynb:
- Загрузите цену какой-нибудь акции через Yfinance(за 5 лет)
- Классический baseline: Prophet, ARIMA
- Ваша LSTM модель
- Сравнение forecasting accuracy на test set
- FastAPI endpoint
✅ Чек-лист
- Знаю разницу между RNN, LSTM, GRU
- Понимаю проблему vanishing gradient
- Знаю функции gate’ов LSTM
- Разница между Bidirectional и unidirectional
- Умею готовить данные для time series через sliding window approach
- Знаю, почему gradient clipping важен в RNN
- Text classification через LSTM
- Знаю, почему Transformers (Месяц 4) превосходят RNN и причины
Месяц 3 завершён! Изучите Упражнения и переходите к Месяц 4 — CV + NLP.