본문 바로가기

딥러닝/딥러닝 아키텍쳐

RNN / LSTM (시퀀스, 텍스트)

들어가며

CNN이 이미지의 공간적 구조를 다루는 도구라면, RNN은 시간적 순서가 있는 데이터를 다루는 도구다. 텍스트, 시계열 데이터, 음성 등 순서가 중요한 데이터에 사용된다. 웹 개발로 비유하면 RNN은 이전 API 응답 결과를 다음 요청에 활용하는 것처럼 이전 상태를 기억하면서 처리하는 구조다.


왜 RNN인가?

일반 신경망은 입력이 서로 독립적이라고 가정한다. 하지만 텍스트는 단어 순서가 의미를 결정한다.

"나는 밥을 먹었다"
"밥을 나는 먹었다"
→ 같은 단어지만 순서에 따라 의미가 달라짐

"I love you"       → 긍정
"I don't love you" → 부정
→ "love"만 보면 모름, 앞뒤 맥락이 중요함

RNN 기본 구조

import numpy as np

class RNNCell:
    def __init__(self, input_size, hidden_size):
        self.W_x = np.random.randn(hidden_size, input_size) * 0.01
        self.W_h = np.random.randn(hidden_size, hidden_size) * 0.01
        self.b = np.zeros(hidden_size)

    def forward(self, x, h_prev):
        # h_t = tanh(W_x * x_t + W_h * h_{t-1} + b)
        h_t = np.tanh(self.W_x @ x + self.W_h @ h_prev + self.b)
        return h_t

# 시퀀스 처리
cell = RNNCell(input_size=4, hidden_size=8)
h = np.zeros(8)   # 초기 은닉 상태

sequence = np.random.randn(5, 4)
for t in range(5):
    h = cell.forward(sequence[t], h)
    print(f"t={t}: h[0]={h[0]:.4f}")

# 마지막 은닉 상태 = 전체 시퀀스를 요약한 벡터
시간 흐름 →
x_0 → [RNN] → h_0 → [RNN] → h_1 → [RNN] → h_2 → h_T
       ↑              ↑              ↑
      h_-1            h_0            h_1

같은 가중치 W_x, W_h를 모든 시간 단계에서 공유한다.

PyTorch에서 RNN

import torch
import torch.nn as nn

rnn = nn.RNN(
    input_size=4,
    hidden_size=8,
    num_layers=2,        # RNN 레이어 수
    batch_first=True,    # 입력: (batch, seq, feature)
    dropout=0.2,
    bidirectional=False
)

x = torch.randn(4, 10, 4)   # (배치, 시퀀스, 특성)
h0 = torch.zeros(2, 4, 8)   # (num_layers, batch, hidden)

output, h_n = rnn(x, h0)
print(f"output: {output.shape}")  # (4, 10, 8) 각 시점의 출력
print(f"h_n:    {h_n.shape}")     # (2, 4, 8) 최종 은닉 상태

RNN의 문제점: 장기 의존성

"나는 어제 서울에서 맛있는 음식을 먹었고 오늘은 배가 아프다"
"나는"과 "배가 아프다" 사이의 관계를 학습해야 하는데, 시퀀스가 길수록 앞부분 정보가 점점 희미해진다. 이게 장기 의존성(Long-term Dependency) 문제다.


LSTM (Long Short-Term Memory)

RNN의 장기 의존성 문제를 해결하기 위해 1997년에 제안됐다. 셀 상태(Cell State)와 은닉 상태(Hidden State) 두 가지를 유지하고 세 개의 게이트로 정보 흐름을 제어한다.

LSTM 게이트 구현

class LSTMCell:
    def __init__(self, input_size, hidden_size):
        self.W = np.random.randn(4 * hidden_size, input_size + hidden_size) * 0.01
        self.b = np.zeros(4 * hidden_size)
        self.hidden_size = hidden_size

    def sigmoid(self, x):
        return 1 / (1 + np.exp(-x))

    def forward(self, x, h_prev, c_prev):
        combined = np.concatenate([h_prev, x])
        gates = self.W @ combined + self.b
        hs = self.hidden_size

        f_t = self.sigmoid(gates[0*hs:1*hs])  # Forget gate: 무엇을 잊을지
        i_t = self.sigmoid(gates[1*hs:2*hs])  # Input gate: 무엇을 기억할지
        g_t = np.tanh(gates[2*hs:3*hs])       # Gate gate: 새로운 정보
        o_t = self.sigmoid(gates[3*hs:4*hs])  # Output gate: 무엇을 출력할지

        # 셀 상태 업데이트: 이전 정보 유지 + 새 정보 추가
        c_t = f_t * c_prev + i_t * g_t
        h_t = o_t * np.tanh(c_t)

        return h_t, c_t

게이트의 직관적 이해

Forget Gate (f_t): "이전 정보 중 무엇을 잊을까?"
  → 0에 가까우면 이전 셀 상태 삭제
  → 1에 가까우면 이전 셀 상태 유지
  → 새로운 주제로 전환될 때 이전 문맥을 잊음

Input Gate (i_t): "새 정보 중 무엇을 기억할까?"
  → 0에 가까우면 새 정보 무시
  → 1에 가까우면 새 정보 저장

Gate Gate (g_t): "어떤 새로운 정보를 추가할까?"
  → tanh로 -1~1 범위의 새로운 후보값

Output Gate (o_t): "셀 상태 중 무엇을 출력할까?"
  → 현재 셀 상태에서 어떤 부분을 출력할지 결정

PyTorch에서 LSTM

lstm = nn.LSTM(
    input_size=4,
    hidden_size=128,
    num_layers=2,
    batch_first=True,
    dropout=0.3,
    bidirectional=False
)

x = torch.randn(4, 20, 4)
h0 = torch.zeros(2, 4, 128)
c0 = torch.zeros(2, 4, 128)

output, (h_n, c_n) = lstm(x, (h0, c0))
print(f"output: {output.shape}")  # (4, 20, 128)
print(f"h_n:    {h_n.shape}")     # (2, 4, 128)
print(f"c_n:    {c_n.shape}")     # (2, 4, 128)

LSTM으로 텍스트 분류

class TextClassifierLSTM(nn.Module):
    def __init__(self, vocab_size, embed_dim, hidden_size,
                 num_layers, num_classes, dropout=0.3):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, embed_dim, padding_idx=0)
        self.lstm = nn.LSTM(
            embed_dim, hidden_size,
            num_layers=num_layers,
            batch_first=True,
            dropout=dropout if num_layers > 1 else 0,
            bidirectional=True   # 양방향 LSTM
        )
        self.fc = nn.Linear(hidden_size * 2, num_classes)  # 양방향이면 *2
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        embedded = self.dropout(self.embedding(x))
        output, (h_n, c_n) = self.lstm(embedded)

        # 양방향: 순방향 + 역방향 마지막 레이어 합침
        h_forward = h_n[-2]
        h_backward = h_n[-1]
        h_combined = torch.cat([h_forward, h_backward], dim=1)

        return self.fc(self.dropout(h_combined))

model = TextClassifierLSTM(
    vocab_size=20000, embed_dim=256,
    hidden_size=128, num_layers=2, num_classes=5
)
x = torch.randint(0, 20000, (4, 100))
print(f"출력: {model(x).shape}")  # (4, 5)

GRU (Gated Recurrent Unit)

LSTM의 간소화 버전이다. 게이트를 2개로 줄여서 파라미터가 적고 빠르다.

# PyTorch GRU
gru = nn.GRU(input_size=4, hidden_size=128, batch_first=True)
x = torch.randn(4, 20, 4)
output, h_n = gru(x)
print(f"GRU output: {output.shape}")  # (4, 20, 128)
LSTM vs GRU 선택:

LSTM:
- 더 복잡한 장기 의존성에 유리
- 파라미터 더 많음 (학습 느림)
- 긴 시퀀스에서 일반적으로 더 좋음

GRU:
- 학습이 빠름 (파라미터 25% 적음)
- 짧은~중간 시퀀스에서 LSTM과 비슷한 성능
- 데이터가 적을 때 유리

현재 NLP: Transformer 기반 (BERT, GPT)
현재 시계열: LSTM/GRU 또는 Transformer 혼용

시계열 예측 예시

class TimeSeriesLSTM(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, output_size):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers,
                           batch_first=True, dropout=0.2)
        self.fc = nn.Linear(hidden_size, output_size)
        self.hidden_size = hidden_size
        self.num_layers = num_layers

    def forward(self, x):
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)

        out, _ = self.lstm(x, (h0, c0))
        # 마지막 시점의 출력으로 예측
        return self.fc(out[:, -1, :])

# 주가 예측: 과거 30일 데이터로 다음날 가격 예측
model = TimeSeriesLSTM(
    input_size=5,    # 시가, 고가, 저가, 종가, 거래량
    hidden_size=64,
    num_layers=2,
    output_size=1    # 다음날 종가
)

x = torch.randn(32, 30, 5)  # 배치32, 과거30일, 특성5
print(f"예측값: {model(x).shape}")  # (32, 1)

정리

개념 설명 핵심 포인트
RNN 이전 상태를 다음 입력에 활용 시퀀스 데이터 처리
장기 의존성 먼 시점 간 관계 학습 어려움 RNN의 근본적 한계
LSTM 셀 상태 + 3개 게이트 장기 의존성 해결
Forget Gate 이전 정보를 잊는 비율 새 주제 전환 시 활용
Input Gate 새 정보 저장 비율 중요한 새 정보 저장
Output Gate 출력할 정보 선택 현재 시점 출력 결정
GRU LSTM 간소화 버전 빠르고 파라미터 적음
Bidirectional 양방향 처리 앞뒤 문맥 모두 활용

'딥러닝 > 딥러닝 아키텍쳐' 카테고리의 다른 글

사전학습 모델 활용 (Transfer Learning)  (0) 2026.05.16
Transformer / Attention  (0) 2026.05.16
CNN (합성곱 신경망)  (0) 2026.05.15