1. 왜 시퀀스 모델이 필요한가?
CNN의 한계
CNN(Convolutional Neural Network)은 단일 이미지 영상 분석에는 탁월하지만, 순서가 중요한 데이터에서는 한계가 있습니다. 이런 데이터를 씨퀀스 데이터라고 합니다. 대표적인 시퀀스 데이터로는 시계열 데이터가 있습니다. 시퀀스 데이터의 대표적인 특징은 길이가 가변적입니다. 그리고 데이터의 순서가 매우 중요한 정보입니다.
이미지: 이미지내 공간 패턴이 중요
동영상: 이미지내 공간 패턴과 시간 패턴 모두 중요, 영상 프레임 순서 중요
언어: 단어의 순서를 바꾸면 의미가 완전히 달라짐
"나는 너를 좋아한다" ≠ "너를 나는 좋아한다" ≠ "좋아한다 나는 너를"
"나는 너를 좋아한다" → 고백
"너를 나는 좋아한다" → 강조 (뉘앙스 변화!)
"좋아한다 나는 너를" → 비문법적!
시퀀스 데이터의 예:
음성: 시간 순서대로 이어지는 파형
출처: 한국스마트제조산업협회
주가: 시간 순서대로 변화하는 값
자연어: "나는 좋은 학생 이다" → 단어 순서가 의미 결정
영상: 프레임 순서대로 이어지는 이미지
2. RNN (Recurrent Neural Network)
2.1 핵심 아이디어
RNN은 이전 출력을 다음 입력에 다시 넣는 구조입니다.
일반 신경망 (FC):
입력 → 은닉층 → 출력
→ 각 입력을 독립적으로 처리
→ 순서 정보 없음!
RNN:
입력₁ → 은닉층 → 출력₁
↓ (이전 상태 전달!)
입력₂ → 은닉층 → 출력₂
↓
입력₃ → 은닉층 → 출력₃
→ 이전 상태가 다음으로 전달 → 순서 정보 유지!
2.2 수식
h_t = tanh(W_hh × h_{t-1} + W_xh × x_t + b)
y_t = W_hy × h_t
h_t: 현재 시점의 Hidden State ("기억")
h_{t-1}: 이전 시점의 Hidden State
x_t: 현재 시점의 입력
y_t: 현재 시점의 출력
CNN에서의 비유:
CNN: 이미지를 공간적으로 처리 (가로×세로)
RNN: 시퀀스를 시간적으로 처리 (시간축)
CNN Conv 필터가 "공간적 패턴"을 포착하듯
RNN Hidden State가 "시간적 패턴"을 포착
2.3 PyTorch로 RNN 구현
import torch
import torch.nn as nn
class SimpleRNN(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
# RNN 핵심: 이전 Hidden + 현재 입력 → 새 Hidden
self.W_xh = nn.Linear(input_size, hidden_size) # 입력 → 은닉
self.W_hh = nn.Linear(hidden_size, hidden_size) # 은닉 → 은닉 (순환!)
self.W_hy = nn.Linear(hidden_size, output_size) # 은닉 → 출력
def forward(self, x):
"""
x: (batch_size, seq_len, input_size)
"""
batch_size, seq_len, _ = x.shape
# 초기 Hidden State = 0으로 시작
h = torch.zeros(batch_size, self.hidden_size, device=x.device)
outputs = []
for t in range(seq_len):
# 핵심 수식: h_t = tanh(W_xh × x_t + W_hh × h_{t-1})
h = torch.tanh(self.W_xh(x[:, t, :]) + self.W_hh(h))
y = self.W_hy(h)
outputs.append(y)
return torch.stack(outputs, dim=1) # (batch, seq_len, output_size)
# PyTorch 내장 RNN 사용 시:
rnn = nn.RNN(input_size=128, hidden_size=256, num_layers=2, batch_first=True)
output, h_n = rnn(x) # output: 모든 시점의 출력, h_n: 마지막 Hidden
2.4 RNN의 치명적 문제: 기울기 소실
긴 문장에서 문제 발생:
"나는 프랑스에서 태어나서 프랑스어를 잘하고 ... (100단어) ... 그래서 ___는 유창하다"
→ "프랑스어"를 기억해야!
RNN에서:
h_100 = tanh(W × h_99 + ...)
h_99 = tanh(W × h_98 + ...)
...
h_1 = tanh(W × h_0 + ...)
역전파 시 기울기:
∂h_100/∂h_1 = W^99 × (tanh 미분)^99
→ W < 1이면: 0에 수렴 (기울기 소실!)
→ W > 1이면: 무한대 (기울기 폭발!)
→ 먼 과거의 정보를 학습할 수 없음!
CNN에서의 유사한 문제:
CNN 깊이 문제:
100층 CNN → 기울기 소실 → 학습 불가!
해결: ResNet의 Skip Connection!
RNN 길이 문제:
100 시점 RNN → 기울기 소실 → 먼 과거 학습 불가!
해결: LSTM의 Gate 메커니즘!
3. LSTM (Long Short-Term Memory)
3.1 핵심 아이디어
LSTM은 RNN에 **"고속도로"**를 추가한 것입니다.
RNN: 정보가 매 시점 tanh를 통과 → 점점 약해짐 (일반 도로)
LSTM: Cell State라는 "고속도로" 추가!
→ 정보가 거의 변형 없이 먼 미래까지 전달 가능
→ 게이트(문)로 정보의 추가/삭제를 제어!
3.2 세 개의 게이트
LSTM의 3가지 게이트:
1. Forget Gate: "이전 기억 중 뭘 버릴까?"
f_t = σ(W_f × [h_{t-1}, x_t])
→ 0~1 값, 0이면 완전 삭제, 1이면 완전 보존
2. Input Gate: "새 정보 중 뭘 저장할까?"
i_t = σ(W_i × [h_{t-1}, x_t])
→ 새 후보 정보 중 얼마나 반영할지
3. Output Gate: "기억 중 뭘 출력할까?"
o_t = σ(W_o × [h_{t-1}, x_t])
→ Cell State에서 얼마나 꺼내서 출력할지
3.3 Cell State 업데이트
Step 1 - 기존 기억에서 불필요한 것 제거:
C_t = f_t × C_{t-1} (Forget Gate로 걸러냄)
Step 2 - 새 정보 추가:
candidate = tanh(W_c × [h_{t-1}, x_t]) (새 후보 정보)
C_t = C_t + i_t × candidate (Input Gate로 선택적 추가)
Step 3 - Hidden State 출력:
h_t = o_t × tanh(C_t) (Output Gate로 선택적 출력)
CNN에서의 비유:
Cell State = ResNet의 Skip Connection
→ 정보가 변형 없이 쭉 전달!
Gate = Attention의 가중치
→ "어떤 정보를 통과시킬지" 동적으로 결정!
3.4 PyTorch로 LSTM 구현
class SimpleLSTM(nn.Module):
def __init__(self, input_size, hidden_size, output_size):
super().__init__()
self.hidden_size = hidden_size
# 4개의 게이트를 하나의 Linear로 효율적 계산
# (forget, input, candidate, output) × hidden_size
self.gates = nn.Linear(input_size + hidden_size, 4 * hidden_size)
self.output_layer = nn.Linear(hidden_size, output_size)
def forward(self, x):
"""
x: (batch_size, seq_len, input_size)
"""
batch_size, seq_len, _ = x.shape
h = torch.zeros(batch_size, self.hidden_size, device=x.device)
c = torch.zeros(batch_size, self.hidden_size, device=x.device)
outputs = []
for t in range(seq_len):
# [h_{t-1}, x_t]를 합쳐서 입력
combined = torch.cat([h, x[:, t, :]], dim=1)
# 4개 게이트를 한번에 계산!
gate_values = self.gates(combined)
f, i, candidate, o = gate_values.chunk(4, dim=1)
# 게이트 활성화
f = torch.sigmoid(f) # Forget Gate
i = torch.sigmoid(i) # Input Gate
candidate = torch.tanh(candidate) # 새 후보 정보
o = torch.sigmoid(o) # Output Gate
# Cell State 업데이트
c = f * c + i * candidate # 기억 관리!
# Hidden State 출력
h = o * torch.tanh(c)
outputs.append(self.output_layer(h))
return torch.stack(outputs, dim=1)
# PyTorch 내장 LSTM 사용 시:
lstm = nn.LSTM(input_size=128, hidden_size=256, num_layers=2, batch_first=True)
output, (h_n, c_n) = lstm(x)
3.5 LSTM도 완벽하지 않다
LSTM의 개선점:
✅ 기울기 소실 문제 크게 완화!
✅ 수십~수백 시점의 장기 기억 가능!
여전한 한계:
❌ 순차 처리 → 병렬화 불가능!
❌ 매우 긴 시퀀스(수천 이상)에서는 여전히 약화
❌ 고정 크기 Hidden State에 모든 정보 압축
4. Attention 메커니즘
4.1 LSTM의 병목
번역: "나는 프랑스에서 태어난 학생입니다" → "I am a student born in France"
LSTM Encoder-Decoder:
Encoder: 전체 문장을 하나의 Hidden Vector에 압축!
"나는 프랑스에서 태어난 학생입니다"
→ h₁ → h₂ → h₃ → h₄ → h₅ → h₆ → [최종 벡터]
↓
Decoder: 이 벡터 하나로 전체 번역!
→ "I" → "am" → "a" → "student" → ...
문제:
문장이 길수록 하나의 벡터에 압축이 어려움!
정보 병목(Information Bottleneck)!
4.2 Attention의 해결
핵심 아이디어:
Decoder가 매번 Encoder의 모든 Hidden State를 참조!
→ 하나의 벡터 대신 전체를 보고, 관련 있는 곳에 "집중"!
"student"를 생성할 때:
Encoder Hidden States: [h₁, h₂, h₃, h₄, h₅, h₆]
Attention Score 계산:
h₁("나는") → 0.1 (관련 적음)
h₂("프랑스에서") → 0.05
h₃("태어난") → 0.05
h₄("학생") → 0.7 ← 가장 관련! 집중!
h₅("입니다") → 0.1
→ h₄에 집중해서 "student" 생성!
"France"를 생성할 때:
h₂("프랑스에서") → 0.8 ← 이번에는 여기 집중!
나머지 → 낮은 Score
CNN에서의 비유:
Attention = 적응형(Adaptive) Pooling
일반 Pooling: 모든 위치를 균등하게 평균
Attention: 중요한 위치에 높은 가중치로 가중 평균!
→ Spatial Attention, Channel Attention과 동일한 개념!
(Squeeze-and-Excitation, CBAM 등)
4.3 Attention Score 계산
Decoder의 현재 Hidden State: s_t (Query)
Encoder의 모든 Hidden States: [h₁, h₂, ..., h_N] (Keys)
Score 계산 (여러 방법):
1. Dot-product:
score(s_t, h_i) = s_tᵀ × h_i
2. Additive (Bahdanau):
score(s_t, h_i) = Vᵀ × tanh(W₁×s_t + W₂×h_i)
3. Scaled Dot-product (Transformer!):
score(s_t, h_i) = (s_tᵀ × h_i) / √d_model
4.4 PyTorch로 Attention 구현
class Attention(nn.Module):
"""Bahdanau (Additive) Attention"""
def __init__(self, hidden_size):
super().__init__()
self.W1 = nn.Linear(hidden_size, hidden_size) # Decoder hidden
self.W2 = nn.Linear(hidden_size, hidden_size) # Encoder hidden
self.V = nn.Linear(hidden_size, 1) # Score → 스칼라
def forward(self, decoder_hidden, encoder_outputs):
"""
decoder_hidden: (batch, hidden_size) - Decoder의 현재 상태
encoder_outputs: (batch, src_len, hidden_size) - Encoder 전체 출력
"""
# decoder_hidden을 시퀀스 차원으로 확장
decoder_hidden = decoder_hidden.unsqueeze(1)
# (batch, 1, hidden_size)
# Attention Score 계산
score = self.V(torch.tanh(
self.W1(decoder_hidden) + self.W2(encoder_outputs)
))
# score: (batch, src_len, 1)
# Softmax로 가중치 변환
attn_weights = torch.softmax(score.squeeze(-1), dim=-1)
# attn_weights: (batch, src_len)
# 가중 합산
context = torch.bmm(attn_weights.unsqueeze(1), encoder_outputs)
# context: (batch, 1, hidden_size)
return context.squeeze(1), attn_weights
4.5 LSTM + Attention 전체 구조
class Seq2SeqWithAttention(nn.Module):
def __init__(self, src_vocab, tgt_vocab, hidden_size):
super().__init__()
self.encoder_embed = nn.Embedding(src_vocab, hidden_size)
self.decoder_embed = nn.Embedding(tgt_vocab, hidden_size)
self.encoder = nn.LSTM(hidden_size, hidden_size, batch_first=True)
self.decoder = nn.LSTM(hidden_size * 2, hidden_size, batch_first=True)
self.attention = Attention(hidden_size)
self.output = nn.Linear(hidden_size, tgt_vocab)
def forward(self, src, tgt):
# Encoder
enc_embed = self.encoder_embed(src)
enc_outputs, (h, c) = self.encoder(enc_embed)
# Decoder (토큰별 순차 처리)
outputs = []
dec_input = self.decoder_embed(tgt[:, 0:1]) # <BOS>
for t in range(tgt.size(1) - 1):
# Attention으로 Encoder에서 관련 정보 추출!
context, attn_weights = self.attention(h[-1], enc_outputs)
# context + 현재 입력을 Decoder에 전달
dec_input_combined = torch.cat([dec_input.squeeze(1), context], dim=-1)
dec_output, (h, c) = self.decoder(dec_input_combined.unsqueeze(1), (h, c))
pred = self.output(dec_output.squeeze(1))
outputs.append(pred)
# 다음 입력 (Teacher Forcing)
dec_input = self.decoder_embed(tgt[:, t+1:t+2])
return torch.stack(outputs, dim=1)
5. Attention의 한계 → Transformer로
LSTM + Attention의 여전한 문제
문제 1: 순차 처리
Encoder LSTM: h₁ → h₂ → h₃ → h₄ → ...
→ h₄를 구하려면 h₁, h₂, h₃를 먼저 계산해야!
→ GPU 병렬 처리 불가능!
문제 2: Attention은 보조 역할
LSTM이 주역, Attention은 Decoder→Encoder 참조에만 사용
→ 입력 내부에서의 관계(Self-Attention)는 포착 못함!
문제 3: 고정 크기 Hidden State
LSTM Hidden: 256 또는 512차원
→ 아무리 긴 문장도 이 크기에 압축해야 함!
Transformer의 혁신
Transformer의 해결:
문제 1 해결: LSTM을 완전히 제거!
→ Attention만으로 처리 → 완전 병렬!
문제 2 해결: Self-Attention 도입!
→ 입력 내부에서도 Attention → 모든 관계 포착!
→ Encoder→Decoder뿐 아니라 Self(자기 자신) Attention!
문제 3 해결: 토큰별 독립 벡터!
→ Hidden State 하나에 압축할 필요 없음
→ 각 토큰이 자신만의 벡터를 가짐
→ 길이에 무관하게 정보 보존!
발전 흐름 요약
RNN (1986):
순차 처리로 시퀀스 이해
문제: 장기 기억 불가, 기울기 소실
LSTM (1997):
Gate + Cell State로 장기 기억 개선
문제: 여전히 순차 처리, 병렬화 불가
LSTM + Attention (2014):
Encoder 전체를 참조해서 정보 병목 해소
문제: 여전히 LSTM 의존, 순차 처리
Transformer (2017):
"Attention Is All You Need"
LSTM 완전 제거, Attention만으로 전부!
→ 완전 병렬, 장거리 의존성 완벽!
→ 현재 모든 LLM의 기반!
6. 정리: Transformer로 가기 위한 핵심 개념
| 개념 | 역할 | Transformer에서 |
|---|---|---|
| Hidden State | 시퀀스의 "기억" | → 각 토큰의 벡터 표현으로 대체 |
| Gate (LSTM) | 정보 흐름 제어 | → Attention 가중치로 대체 |
| Encoder-Decoder | 입력 이해 + 출력 생성 | → 거의 동일한 구조 유지! |
| Attention | 관련 정보에 집중 | → Self-Attention으로 확장! |
| 순차 처리 | 시간 순서 유지 | → Positional Encoding으로 대체 |
다음 글: [사전학습 2편] 단어를 벡터로 - 임베딩의 이해
본편: Transformer 구조 완전 정복