본문 바로가기

딥러닝/딥러닝 아키텍쳐

Transformer / Attention

들어가며

BERT, GPT, ChatGPT 등 요즘 화제가 되는 모델들이 모두 Transformer 구조를 기반으로 한다. 2017년 "Attention Is All You Need" 논문 하나가 NLP 판도를 완전히 바꿨다. RNN/LSTM의 한계를 어떻게 극복했는지, Attention이 무엇인지부터 차근차근 정리해둔다.


RNN의 한계와 Transformer의 등장

RNN/LSTM의 문제점:
1. 순차 처리: t번째 단어를 처리하려면 t-1번째가 끝나야 함
   → GPU 병렬 처리 불가능 → 학습이 느림

2. 장거리 의존성: 시퀀스가 길수록 먼 정보 손실

3. 병목: 전체 시퀀스를 고정 크기 벡터로 압축
   → 긴 문장에서 정보 손실
Transformer의 핵심 아이디어:
- 순차 처리 없이 모든 위치를 동시에 처리 (병렬화)
- 모든 단어 쌍의 관계를 직접 계산
- 거리에 관계없이 관련성 높은 단어에 집중

Attention 메커니즘

직관적 이해

문장: "The animal didn't cross the street because it was too tired"

"it"이 무엇을 가리키는지 이해하려면:
- "animal"과 "it"의 관계: 매우 높음
- "street"와 "it"의 관계: 낮음
- "tired"와 "it"의 관계: 중간

Attention은 각 단어가 다른 단어들에 얼마나 "주목"해야 하는지
가중치로 표현한다.

Attention 공식

Attention(Q, K, V) = softmax(QK^T / √d_k) * V

Q (Query) : 현재 처리 중인 단어 "무엇을 찾고 있나?"
K (Key)   : 각 단어의 레이블 "나는 이런 정보야"
V (Value) : 각 단어의 실제 내용 "나의 실제 값은 이거야"

√d_k : 스케일링 인수 (기울기 소실 방지)
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    d_k = Q.size(-1)

    # 유사도 점수: Q와 K의 내적
    scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)

    if mask is not None:
        scores = scores.masked_fill(mask == 0, float('-inf'))

    attn_weights = F.softmax(scores, dim=-1)
    output = torch.matmul(attn_weights, V)

    return output, attn_weights

# 예시
Q = torch.randn(2, 1, 5, 64)
K = torch.randn(2, 1, 5, 64)
V = torch.randn(2, 1, 5, 64)

output, weights = scaled_dot_product_attention(Q, K, V)
print(f"Attention 출력: {output.shape}")    # (2, 1, 5, 64)
print(f"가중치 합: {weights[0,0,0].sum():.4f}")  # 1.0

Multi-Head Attention

여러 관점에서 동시에 Attention을 계산한다.

단일 Attention은 하나의 관점에서만 관계를 봄.
Multi-Head는 여러 다른 관점에서 동시에 관계를 파악한다.
예: "bank" → Head1: 문법적 관계 / Head2: 의미적 관계 (강둑? 은행?) / Head3: 위치적 관계

class MultiHeadAttention(nn.Module):
    def __init__(self, d_model, num_heads):
        super().__init__()
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        self.W_q = nn.Linear(d_model, d_model)
        self.W_k = nn.Linear(d_model, d_model)
        self.W_v = nn.Linear(d_model, d_model)
        self.W_o = nn.Linear(d_model, d_model)

    def split_heads(self, x):
        # (batch, seq, d_model) → (batch, heads, seq, d_k)
        batch_size, seq_len, _ = x.shape
        x = x.view(batch_size, seq_len, self.num_heads, self.d_k)
        return x.transpose(1, 2)

    def forward(self, Q, K, V, mask=None):
        batch_size = Q.size(0)

        Q = self.split_heads(self.W_q(Q))
        K = self.split_heads(self.W_k(K))
        V = self.split_heads(self.W_v(V))

        attn_output, attn_weights = scaled_dot_product_attention(Q, K, V, mask)

        attn_output = attn_output.transpose(1, 2).contiguous()
        attn_output = attn_output.view(batch_size, -1, self.d_model)

        return self.W_o(attn_output), attn_weights

mha = MultiHeadAttention(d_model=512, num_heads=8)
x = torch.randn(2, 10, 512)
output, weights = mha(x, x, x)  # Self-Attention: Q=K=V
print(f"MHA 출력: {output.shape}")     # (2, 10, 512)
print(f"헤드 가중치: {weights.shape}") # (2, 8, 10, 10)

Positional Encoding

Transformer는 순차 처리를 하지 않기 때문에 단어의 위치 정보를 별도로 주입해야 한다.

class PositionalEncoding(nn.Module):
    def __init__(self, d_model, max_seq_len=5000, dropout=0.1):
        super().__init__()
        self.dropout = nn.Dropout(dropout)

        pe = torch.zeros(max_seq_len, d_model)
        position = torch.arange(0, max_seq_len).unsqueeze(1).float()
        div_term = torch.exp(
            torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model)
        )

        # 짝수 인덱스: sin, 홀수 인덱스: cos
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)

        self.register_buffer('pe', pe.unsqueeze(0))

    def forward(self, x):
        return self.dropout(x + self.pe[:, :x.size(1)])

Transformer 인코더 블록

class TransformerEncoderBlock(nn.Module):
    def __init__(self, d_model, num_heads, d_ff, dropout=0.1):
        super().__init__()
        self.attention = MultiHeadAttention(d_model, num_heads)
        self.ff = nn.Sequential(
            nn.Linear(d_model, d_ff),
            nn.GELU(),
            nn.Dropout(dropout),
            nn.Linear(d_ff, d_model),
            nn.Dropout(dropout)
        )
        self.norm1 = nn.LayerNorm(d_model)
        self.norm2 = nn.LayerNorm(d_model)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, mask=None):
        # 1. Self-Attention + Add & Norm
        attn_out, _ = self.attention(x, x, x, mask)
        x = self.norm1(x + self.dropout(attn_out))

        # 2. Feed-Forward + Add & Norm
        ff_out = self.ff(x)
        x = self.norm2(x + self.dropout(ff_out))
        return x

block = TransformerEncoderBlock(d_model=512, num_heads=8, d_ff=2048)
x = torch.randn(2, 10, 512)
print(f"인코더 블록 출력: {block(x).shape}")  # (2, 10, 512)

BERT 스타일 텍스트 분류

class BERTStyleClassifier(nn.Module):
    def __init__(self, vocab_size, d_model, num_heads, d_ff,
                 num_layers, num_classes, max_seq_len=512):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, d_model)
        self.pos_encoding = PositionalEncoding(d_model, max_seq_len)
        self.layers = nn.ModuleList([
            TransformerEncoderBlock(d_model, num_heads, d_ff)
            for _ in range(num_layers)
        ])
        self.norm = nn.LayerNorm(d_model)
        self.classifier = nn.Sequential(
            nn.Linear(d_model, d_model // 2),
            nn.GELU(),
            nn.Dropout(0.1),
            nn.Linear(d_model // 2, num_classes)
        )

    def forward(self, x, mask=None):
        x = self.embedding(x) * math.sqrt(self.embedding.embedding_dim)
        x = self.pos_encoding(x)
        for layer in self.layers:
            x = layer(x, mask)
        x = self.norm(x)
        # [CLS] 토큰 (첫 번째 토큰)으로 분류
        return self.classifier(x[:, 0, :])

model = BERTStyleClassifier(
    vocab_size=30000, d_model=256, num_heads=8,
    d_ff=1024, num_layers=6, num_classes=5
)
x = torch.randint(0, 30000, (4, 128))
print(f"출력: {model(x).shape}")  # (4, 5)

Causal Mask (GPT 스타일)

GPT 같은 언어 생성 모델에서는 미래 토큰을 볼 수 없도록 마스킹한다.

def create_causal_mask(seq_len):
    return torch.tril(torch.ones(seq_len, seq_len))

mask = create_causal_mask(5)
print(mask)
# tensor([[1., 0., 0., 0., 0.],
#         [1., 1., 0., 0., 0.],
#         [1., 1., 1., 0., 0.],
#         [1., 1., 1., 1., 0.],
#         [1., 1., 1., 1., 1.]])
# 각 토큰은 자신과 이전 토큰만 볼 수 있음

BERT vs GPT 구조 차이

BERT (Bidirectional Encoder):
- 인코더만 사용
- 양방향 Attention (앞뒤 문맥 모두 봄)
- 마스크 언어 모델링으로 사전학습
- 텍스트 분류, QA, NER 등에 적합

GPT (Generative Decoder):
- 디코더만 사용
- 단방향 Attention (이전 토큰만 봄)
- 다음 토큰 예측으로 사전학습
- 텍스트 생성, 대화에 적합

T5, BART:
- 인코더-디코더 모두 사용
- 번역, 요약 등 Seq2Seq 태스크

정리

개념 설명 핵심 포인트
Attention 단어 간 관련성 가중치 계산 Q, K, V 구조
Self-Attention 자기 자신의 시퀀스 내 관계 모든 위치 동시 처리
Multi-Head Attention 여러 관점에서 Attention 다양한 관계 포착
Positional Encoding 위치 정보 주입 sin/cos 함수 활용
Feed-Forward 위치별 비선형 변환 d_ff = d_model * 4
Add & Norm Residual + LayerNorm 학습 안정화
Causal Mask 미래 토큰 가리기 GPT 스타일 생성
BERT 양방향 인코더 이해 태스크
GPT 단방향 디코더 생성 태스크

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

사전학습 모델 활용 (Transfer Learning)  (0) 2026.05.16
RNN / LSTM (시퀀스, 텍스트)  (0) 2026.05.16
CNN (합성곱 신경망)  (0) 2026.05.15