콘텐츠로 이동

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