RNN, LSTM, GRU¶
논문 정보¶
| 모델 | 논문 | 저자 | 연도 |
|---|---|---|---|
| LSTM | Long Short-Term Memory | Sepp Hochreiter, Jurgen Schmidhuber | 1997 |
| GRU | Learning Phrase Representations using RNN Encoder-Decoder | Kyunghyun Cho et al. | 2014 |
개요¶
시퀀스 데이터¶
시퀀스 데이터는 순서가 중요한 데이터:
| 유형 | 예시 |
|---|---|
| 자연어 | 문장, 문서 |
| 시계열 | 주가, 센서 데이터, 날씨 |
| 음성 | 오디오 신호 |
| 비디오 | 프레임 시퀀스 |
| DNA | 염기 서열 |
RNN의 필요성¶
기존 신경망의 한계:
- 고정된 입력/출력 크기
- 시간 의존성 무시
- 순서 정보 손실
RNN (Recurrent Neural Network)¶
기본 구조¶
x_1 x_2 x_3 x_t
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ h_1 │────►│ h_2 │────►│ h_3 │ ─ ─►│ h_t │
└─────┘ └─────┘ └─────┘ └─────┘
│ │ │ │
▼ ▼ ▼ ▼
y_1 y_2 y_3 y_t
수식¶
은닉 상태 (Hidden State):
\[h_t = \tanh(W_{xh} x_t + W_{hh} h_{t-1} + b_h)\]
출력:
\[y_t = W_{hy} h_t + b_y\]
| 기호 | 차원 | 설명 |
|---|---|---|
| \(x_t\) | \((d,)\) | 시점 t의 입력 |
| \(h_t\) | \((h,)\) | 시점 t의 은닉 상태 |
| \(W_{xh}\) | \((h, d)\) | 입력-은닉 가중치 |
| \(W_{hh}\) | \((h, h)\) | 은닉-은닉 가중치 |
| \(W_{hy}\) | \((o, h)\) | 은닉-출력 가중치 |
Vanilla RNN 구현¶
import torch
import torch.nn as nn
class VanillaRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
self.i2h = nn.Linear(input_size + hidden_size, hidden_size)
self.h2o = nn.Linear(hidden_size, output_size)
self.tanh = nn.Tanh()
def forward(self, x, hidden):
combined = torch.cat([x, hidden], dim=1)
hidden = self.tanh(self.i2h(combined))
output = self.h2o(hidden)
return output, hidden
def init_hidden(self, batch_size):
return torch.zeros(batch_size, self.hidden_size)
# 시퀀스 처리
rnn = VanillaRNN(input_size=10, hidden_size=20, output_size=5)
batch_size, seq_len = 32, 15
hidden = rnn.init_hidden(batch_size)
x = torch.randn(seq_len, batch_size, 10)
outputs = []
for t in range(seq_len):
output, hidden = rnn(x[t], hidden)
outputs.append(output)
outputs = torch.stack(outputs) # (seq_len, batch, output_size)
Vanishing/Exploding Gradient 문제¶
문제 원인¶
BPTT (Backpropagation Through Time)에서 기울기 연쇄:
\[\frac{\partial \mathcal{L}}{\partial h_1} = \frac{\partial \mathcal{L}}{\partial h_T} \prod_{t=2}^{T} \frac{\partial h_t}{\partial h_{t-1}}\]
\[\frac{\partial h_t}{\partial h_{t-1}} = W_{hh}^T \cdot \text{diag}(f'(z_{t}))\]
| 조건 | 결과 |
|---|---|
| \(\|W_{hh}\| < 1\) | 기울기 소실 (Vanishing) |
| \(\|W_{hh}\| > 1\) | 기울기 폭발 (Exploding) |
영향¶
- 장기 의존성 학습 불가
- 초기 시점의 정보가 손실
- 수백 스텝 이상의 시퀀스 학습 어려움
해결책¶
| 방법 | 설명 |
|---|---|
| LSTM/GRU | 게이트 메커니즘으로 정보 흐름 제어 |
| Gradient Clipping | 기울기 크기 제한 |
| 초기화 | Orthogonal initialization |
| Skip Connection | Residual RNN |
LSTM (Long Short-Term Memory)¶
구조¶
┌─────────────────────────────────────────┐
│ Cell State │
C_{t-1} ────►│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │────► C_t
│ × + │
│ ▲ ▲ │
│ ┌────┴────┐ ┌────┴────┐ │
│ │ Forget │ │ Input │ │
│ │ Gate │ │ Gate │ │
│ │ f_t │ │ i_t │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ │ ~C_t │ │
│ │ │ (tanh) │ │
│ │ └────┬────┘ │
│ │ │ │
└────────┼──────────────┼─────────────────┘
│ │
h_{t-1} ──────────────┼──────────────┤
│ │
x_t ──────────────────┴──────────────┴────────────────►
│
┌────────────────┘
│
┌────┴────┐
│ Output │
│ Gate │
│ o_t │
└────┬────┘
│
▼
h_t ────►
게이트 수식¶
Forget Gate - 이전 정보 얼마나 잊을지:
\[f_t = \sigma(W_f \cdot [h_{t-1}, x_t] + b_f)\]
Input Gate - 새 정보 얼마나 저장할지:
\[i_t = \sigma(W_i \cdot [h_{t-1}, x_t] + b_i)\]
Candidate Cell State - 새로운 후보 정보:
\[\tilde{C}_t = \tanh(W_C \cdot [h_{t-1}, x_t] + b_C)\]
Cell State Update - 셀 상태 업데이트:
\[C_t = f_t \odot C_{t-1} + i_t \odot \tilde{C}_t\]
Output Gate - 출력 얼마나 내보낼지:
\[o_t = \sigma(W_o \cdot [h_{t-1}, x_t] + b_o)\]
Hidden State - 최종 은닉 상태:
\[h_t = o_t \odot \tanh(C_t)\]
기울기 흐름¶
Cell State를 통한 직접 경로:
\[\frac{\partial C_t}{\partial C_{t-1}} = f_t\]
Forget gate \(f_t \approx 1\)이면 기울기가 거의 그대로 전파됨 (고속도로 효과).
LSTM 파라미터 수¶
입력 크기 \(d\), 은닉 크기 \(h\)일 때:
\[\text{Params} = 4 \times (h \times d + h \times h + h) = 4 \times h \times (d + h + 1)\]
4는 forget, input, candidate, output 게이트 수.
GRU (Gated Recurrent Unit)¶
구조¶
LSTM보다 단순화된 구조 (2개 게이트):
Reset Gate:
\[r_t = \sigma(W_r \cdot [h_{t-1}, x_t] + b_r)\]
Update Gate:
\[z_t = \sigma(W_z \cdot [h_{t-1}, x_t] + b_z)\]
Candidate Hidden State:
\[\tilde{h}_t = \tanh(W_h \cdot [r_t \odot h_{t-1}, x_t] + b_h)\]
Hidden State Update:
\[h_t = (1 - z_t) \odot h_{t-1} + z_t \odot \tilde{h}_t\]
LSTM vs GRU 비교¶
| 특성 | LSTM | GRU |
|---|---|---|
| 게이트 수 | 3 (forget, input, output) | 2 (reset, update) |
| 상태 | Cell State + Hidden State | Hidden State만 |
| 파라미터 | 4h(d+h+1) | 3h(d+h+1) |
| 학습 속도 | 느림 | 빠름 |
| 성능 | 장기 의존성에 강함 | 짧은 시퀀스에 효율적 |
| 메모리 | 높음 | 낮음 |
PyTorch 구현¶
기본 LSTM 사용¶
import torch
import torch.nn as nn
# 단일 LSTM 레이어
lstm = nn.LSTM(
input_size=10,
hidden_size=20,
num_layers=2,
batch_first=True,
dropout=0.5,
bidirectional=False
)
# 입력: (batch, seq_len, input_size)
x = torch.randn(32, 15, 10)
# 출력: output, (h_n, c_n)
output, (h_n, c_n) = lstm(x)
print(f"Output shape: {output.shape}") # (32, 15, 20)
print(f"h_n shape: {h_n.shape}") # (2, 32, 20) - num_layers
print(f"c_n shape: {c_n.shape}") # (2, 32, 20)
양방향 LSTM¶
bilstm = nn.LSTM(
input_size=10,
hidden_size=20,
num_layers=2,
batch_first=True,
bidirectional=True
)
x = torch.randn(32, 15, 10)
output, (h_n, c_n) = bilstm(x)
print(f"Output shape: {output.shape}") # (32, 15, 40) - 양방향 concatenate
print(f"h_n shape: {h_n.shape}") # (4, 32, 20) - 2 layers * 2 directions
시퀀스 분류 모델¶
class LSTMClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim,
num_layers=2, dropout=0.5, bidirectional=True):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.lstm = nn.LSTM(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers > 1 else 0,
bidirectional=bidirectional
)
self.fc = nn.Linear(
hidden_dim * (2 if bidirectional else 1),
output_dim
)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# x: (batch, seq_len)
embedded = self.dropout(self.embedding(x)) # (batch, seq, embed)
output, (h_n, c_n) = self.lstm(embedded)
# 마지막 은닉 상태 사용 (양방향: 앞뒤 concatenate)
if self.lstm.bidirectional:
hidden = torch.cat([h_n[-2], h_n[-1]], dim=1)
else:
hidden = h_n[-1]
out = self.fc(self.dropout(hidden))
return out
model = LSTMClassifier(
vocab_size=10000,
embed_dim=300,
hidden_dim=256,
output_dim=2 # 이진 분류
)
x = torch.randint(0, 10000, (32, 100)) # (batch, seq_len)
output = model(x)
print(f"Output shape: {output.shape}") # (32, 2)
GRU 사용¶
class GRUClassifier(nn.Module):
def __init__(self, vocab_size, embed_dim, hidden_dim, output_dim,
num_layers=2, dropout=0.5):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embed_dim)
self.gru = nn.GRU(
embed_dim, hidden_dim, num_layers,
batch_first=True, dropout=dropout if num_layers > 1 else 0,
bidirectional=True
)
self.fc = nn.Linear(hidden_dim * 2, output_dim)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
embedded = self.dropout(self.embedding(x))
output, h_n = self.gru(embedded) # GRU는 cell state 없음
hidden = torch.cat([h_n[-2], h_n[-1]], dim=1)
out = self.fc(self.dropout(hidden))
return out
시계열 예측 예시¶
단변량 시계열 예측¶
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
import matplotlib.pyplot as plt
# 시계열 데이터 생성 (사인파 + 노이즈)
np.random.seed(42)
t = np.linspace(0, 100, 1000)
data = np.sin(t) + 0.1 * np.random.randn(len(t))
# 슬라이딩 윈도우로 데이터셋 생성
def create_sequences(data, seq_length, pred_length=1):
X, y = [], []
for i in range(len(data) - seq_length - pred_length + 1):
X.append(data[i:i+seq_length])
y.append(data[i+seq_length:i+seq_length+pred_length])
return np.array(X), np.array(y)
seq_length = 50
X, y = create_sequences(data, seq_length)
# Train/Test 분할
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
# Tensor 변환
X_train = torch.FloatTensor(X_train).unsqueeze(-1) # (N, seq, 1)
y_train = torch.FloatTensor(y_train)
X_test = torch.FloatTensor(X_test).unsqueeze(-1)
y_test = torch.FloatTensor(y_test)
train_loader = DataLoader(TensorDataset(X_train, y_train), batch_size=32, shuffle=True)
# 모델
class LSTMPredictor(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, batch_first=True)
self.fc = nn.Linear(hidden_size, output_size)
def forward(self, x):
out, _ = self.lstm(x)
out = self.fc(out[:, -1, :]) # 마지막 시점
return out
model = LSTMPredictor()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 학습
for epoch in range(100):
model.train()
total_loss = 0
for X_batch, y_batch in train_loader:
optimizer.zero_grad()
output = model(X_batch)
loss = criterion(output, y_batch)
loss.backward()
optimizer.step()
total_loss += loss.item()
if (epoch + 1) % 20 == 0:
print(f"Epoch {epoch+1}, Loss: {total_loss/len(train_loader):.6f}")
# 평가
model.eval()
with torch.no_grad():
predictions = model(X_test).numpy()
# 시각화
plt.figure(figsize=(12, 4))
plt.plot(y_test.numpy(), label='Actual')
plt.plot(predictions, label='Predicted', alpha=0.7)
plt.legend()
plt.title('LSTM Time Series Prediction')
plt.savefig('lstm_prediction.png', dpi=150)
plt.show()
다변량 시계열¶
class MultivariateLSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size, num_layers=2):
super().__init__()
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Sequential(
nn.Linear(hidden_size, hidden_size // 2),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden_size // 2, output_size)
)
def forward(self, x):
# x: (batch, seq_len, num_features)
out, _ = self.lstm(x)
out = self.fc(out[:, -1, :])
return out
# 예: 5개 특성 -> 1개 타겟 예측
model = MultivariateLSTM(input_size=5, hidden_size=128, output_size=1)
언제 쓰나?¶
LSTM/GRU 적합한 경우¶
- 시퀀스 분류 (감성 분석, 스팸 탐지)
- 시계열 예측 (주가, 센서 데이터)
- 시퀀스-투-시퀀스 (기계 번역, 요약)
- 음성 인식
- 손글씨 인식
한계와 대안¶
| 한계 | 설명 | 대안 |
|---|---|---|
| 긴 시퀀스 | O(n) 순차 처리 | Transformer (병렬) |
| 전역 의존성 | 먼 위치 참조 어려움 | Self-Attention |
| 학습 속도 | GPU 병렬화 제한 | 1D CNN + LSTM |
| 성능 | NLP에서 Transformer에 밀림 | BERT, GPT 등 |
RNN 계열 vs Transformer¶
| 관점 | RNN/LSTM | Transformer |
|---|---|---|
| 계산 | O(n) 순차 | O(n^2) 병렬 |
| 장기 의존성 | 제한적 | Self-Attention으로 해결 |
| 위치 정보 | 순서대로 처리 | Positional Encoding |
| NLP 성능 | 보통 | 우수 |
| 시계열 | 여전히 강함 | 도전 중 |
관련 문서¶
| 주제 | 링크 |
|---|---|
| 딥러닝 기초 | README.md |
| CNN | cnn.md |
| Attention | attention.md |
| Transformer | ../../architecture/transformer.md |
참고¶
- Hochreiter, S. & Schmidhuber, J. (1997). "Long Short-Term Memory"
- Cho, K. et al. (2014). "Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation"
- Understanding LSTM Networks (colah's blog): https://colah.github.io/posts/2015-08-Understanding-LSTMs/
- PyTorch RNN Tutorial: https://pytorch.org/tutorials/intermediate/char_rnn_classification_tutorial.html