Transformer / Attention Mechanism

핵심 인사이트 (3줄 요약)

Transformer는 "Attention Is All You Need"(2017)에서 제안된 순전히 Attention 기반 신경망으로, RNN 없이도 시퀀스를 병렬 처리한다. Self-Attention은 문장 내 모든 토큰 쌍의 관계를 가중합으로 계산해 장거리 의존성을 포착한다. LLM·ViT·Stable Diffusion 등 현대 AI의 99%가 Transformer로 구동된다.


Ⅰ. 개요 (필수: 200자 이상)

개념: Transformer는 Encoder-Decoder 구조의 딥러닝 아키텍처로, Scaled Dot-Product Attention과 Multi-Head Attention을 핵심으로 한다. 입력 시퀀스의 각 토큰이 다른 모든 토큰과의 관계를 동적으로 계산하여 문맥을 파악한다.

💡 비유: Transformer는 "회의실에서 모든 참석자가 서로 동시에 대화" 같아요. 각자가 중요한 말에 선택적으로 집중하죠. "그것"이라는 단어가 나왔을 때, 문맥을 보고 "고양이"와 연결하는 것처럼요!

등장 배경 (필수: 3가지 이상 기술):

  1. 기존 문제점 - RNN/LSTM 한계: 순차 처리로 병렬화 불가능, 장거리 의존성 소실 (Vanishing Gradient)
  2. 기술적 필요성 - Attention 메커니즘: 2015년 Bahdanau가 Encoder-Decoder 간 중요 위치 집중 기법 도입 → 기계 번역 품질 향상
  3. 시장/산업 요구 - 대규모 언어 모델: 2017년 Google이 "Attention Is All You Need" 발표, RNN 완전 대체로 BERT, GPT 탄생

핵심 목적: 병렬 처리 가능한 시퀀스 모델링, 장거리 의존성 포착, 범용 사전학습 기반 모델


Ⅱ. 구성 요소 및 핵심 원리 (필수: 가장 상세하게)

Transformer 전체 구조 (필수: ASCII 아트):

┌─────────────────────────────────────────────────────────────────────────┐
│                    Transformer 아키텍처                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ENCODER (×N=6)                    DECODER (×N=6)                      │
│   ┌───────────────────┐            ┌───────────────────┐               │
│   │ Input Embedding   │            │ Output Embedding  │               │
│   │ + Pos Encoding    │            │ + Pos Encoding    │               │
│   └─────────┬─────────┘            └─────────┬─────────┘               │
│             │                                │                          │
│   ┌─────────▼─────────┐            ┌─────────▼─────────┐               │
│   │ Multi-Head Self   │            │ Masked Multi-Head │               │
│   │ Attention         │            │ Self-Attention    │               │
│   └─────────┬─────────┘            └─────────┬─────────┘               │
│             │                                │                          │
│   ┌─────────▼─────────┐            ┌─────────▼─────────┐               │
│   │ Add & Norm        │            │ Add & Norm        │               │
│   └─────────┬─────────┘            └─────────┬─────────┘               │
│             │                                │                          │
│   ┌─────────▼─────────┐            ┌─────────▼─────────┐               │
│   │ Feed Forward      │            │ Cross-Attention   │◄── Encoder 출력│
│   │ Network           │            │ (Encoder-Decoder) │               │
│   └─────────┬─────────┘            └─────────┬─────────┘               │
│             │                                │                          │
│   ┌─────────▼─────────┐            ┌─────────▼─────────┐               │
│   │ Add & Norm        │            │ Add & Norm        │               │
│   └─────────┬─────────┘            └─────────┬─────────┘               │
│             │                                │                          │
│             │                      ┌─────────▼─────────┐               │
│             │                      │ Feed Forward      │               │
│             │                      │ Network           │               │
│             │                      └─────────┬─────────┘               │
│             │                                │                          │
│             │                      ┌─────────▼─────────┐               │
│             │                      │ Add & Norm        │               │
│             │                      └─────────┬─────────┘               │
│             │                                │                          │
│             ▼                                ▼                          │
│   ┌───────────────────┐            ┌───────────────────┐               │
│   │ Encoder 출력      │──────────► │ Linear + Softmax  │               │
│   └───────────────────┘            │ (단어 확률 분포)   │               │
│                                    └───────────────────┘               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

구성 요소 상세 (필수: 표):

구성 요소역할/기능특징비유
Input Embedding토큰을 벡터로 변환d_model=512 차원이름표
Positional Encoding위치 정보 주입sin/cos 함수 사용좌석번호
Multi-Head Attention다중 관점 토큰 관계 학습h=8~16 헤드여러 각도 분석
Add & LayerNormResidual + 정규화그래디언트 흐름 개선원본 보존
Feed Forward Network위치별 독립 비선형 변환2층 MLP (d_ff=2048)심층 분석
Masked Attention미래 토큰 가림디코더 전용치팅 방지

Self-Attention 핵심 원리 (필수: ASCII 아트):

┌─────────────────────────────────────────────────────────────────────────┐
│                   Scaled Dot-Product Attention                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Attention(Q, K, V) = softmax(Q·Kᵀ / √d_k) · V                        │
│                                                                         │
│   Q (Query): "무엇을 찾고 싶은가?"                                      │
│   K (Key):   "나는 이런 정보를 가지고 있어"                             │
│   V (Value): "실제 내용"                                                │
│   √d_k:      온도 조절 (softmax 분산 안정화)                            │
│                                                                         │
│   ┌─────────────────────────────────────────────────────────────────┐  │
│   │                                                                 │  │
│   │   입력 X ────┬───► Wq ───► Q (Query)                           │  │
│   │             ├───► Wk ───► K (Key)                              │  │
│   │             └───► Wv ───► V (Value)                            │  │
│   │                                                                 │  │
│   │   Q × Kᵀ ───────► Attention Scores                            │  │
│   │       │                                                         │  │
│   │       ▼                                                         │  │
│   │   ÷ √d_k ───────► Scaled Scores                                │  │
│   │       │                                                         │  │
│   │       ▼                                                         │  │
│   │   Softmax ──────► Attention Weights (확률 분포)                 │  │
│   │       │                                                         │  │
│   │       ▼                                                         │  │
│   │   × V ──────────► Weighted Sum (문맥 반영 출력)                 │  │
│   │                                                                 │  │
│   └─────────────────────────────────────────────────────────────────┘  │
│                                                                         │
│   예시: "The cat sat on the mat"                                        │
│                                                                         │
│   "sat"에 대한 Self-Attention:                                          │
│   ┌────────────────────────────────────────────────────────────┐       │
│   │ 단어      │ Attention Weight │ 설명                        │       │
│   ├────────────────────────────────────────────────────────────┤       │
│   │ "cat"     │ 0.45             │ 주어 - 높은 관련성           │       │
│   │ "mat"     │ 0.30             │ 장소 - 중간 관련성           │       │
│   │ "the"     │ 0.10             │ 관사 - 낮은 관련성           │       │
│   │ "on"      │ 0.10             │ 전치사 - 낮은 관련성         │       │
│   │ "The"     │ 0.05             │ 시작 관사 - 매우 낮음        │       │
│   └────────────────────────────────────────────────────────────┘       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

[Multi-Head Attention]

MultiHead(Q,K,V) = Concat(head₁,...,headₕ) · Wₒ
where headᵢ = Attention(QWᵢQ, KWᵢK, VWᵢV)

각 헤드 = 다른 관점에서의 Attention
h=8 헤드 예시:
  Head 1: 문법적 관계 (주어-동사)
  Head 2: 의미적 관계 (동의어)
  Head 3: 위치 관계 (인접 단어)
  ...
  Head 8: 장거리 의존성

→ Concat 후 Wₒ로 혼합하여 종합 표현 생성

핵심 알고리즘/공식 (해당 시 필수):

[Self-Attention 복잡도]

시간 복잡도: O(n² · d)
공간 복잡도: O(n² + n·d)

n = 시퀀스 길이, d = 모델 차원

→ 시퀀스 길이 2배 → 메모리/시간 4배
→ Flash Attention: O(n) 메모리로 최적화

[Positional Encoding]

PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

특성:
- 위치마다 고유한 벡터
- 상대 위치 정보 포함 (sin(A-B) = sinA·cosB - cosA·sinB)
- 학습 없이 일반화 가능

[Layer Normalization]

LayerNorm(x) = γ · (x - μ) / σ + β

μ: 해당 층 평균, σ: 표준편차
γ, β: 학습 가능한 스케일/시프트

[Feed Forward Network]

FFN(x) = max(0, xW₁ + b₁)W₂ + b₂

d_model = 512, d_ff = 2048
→ 확장 후 축소 (bottleneck 구조)

코드 예시 (필수: Python 순수 구현):

from dataclasses import dataclass
from typing import List, Optional, Tuple
import math
import random

# ============================================================
# Transformer 구현 (NumPy-free 순수 Python)
# ============================================================

def softmax(x: List[float]) -> List[float]:
    """Softmax 함수"""
    max_x = max(x)
    exp_x = [math.exp(xi - max_x) for xi in x]
    sum_exp = sum(exp_x)
    return [e / sum_exp for e in exp_x]


def layer_norm(x: List[float], eps: float = 1e-6) -> List[float]:
    """Layer Normalization"""
    mean = sum(x) / len(x)
    variance = sum((xi - mean) ** 2 for xi in x) / len(x)
    std = math.sqrt(variance + eps)
    # γ=1, β=0 가정
    return [(xi - mean) / std for xi in x]


def gelu(x: float) -> float:
    """GELU 활성화 함수"""
    return 0.5 * x * (1 + math.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * x**3)))


@dataclass
class Matrix:
    """간단한 행렬 클래스"""
    data: List[List[float]]
    rows: int
    cols: int

    @classmethod
    def zeros(cls, rows: int, cols: int) -> 'Matrix':
        return cls([[0.0] * cols for _ in range(rows)], rows, cols)

    @classmethod
    def random(cls, rows: int, cols: int, scale: float = 0.02) -> 'Matrix':
        data = [[random.gauss(0, scale) for _ in range(cols)] for _ in range(rows)]
        return cls(data, rows, cols)

    def get(self, i: int, j: int) -> float:
        return self.data[i][j]

    def set(self, i: int, j: int, val: float) -> None:
        self.data[i][j] = val

    def row(self, i: int) -> List[float]:
        return self.data[i]

    def col(self, j: int) -> List[float]:
        return [self.data[i][j] for i in range(self.rows)]


def matmul(A: Matrix, B: Matrix) -> Matrix:
    """행렬 곱셈"""
    if A.cols != B.rows:
        raise ValueError(f"Shape mismatch: {A.rows}x{A.cols} @ {B.rows}x{B.cols}")
    result = Matrix.zeros(A.rows, B.cols)
    for i in range(A.rows):
        for j in range(B.cols):
            val = sum(A.get(i, k) * B.get(k, j) for k in range(A.cols))
            result.set(i, j, val)
    return result


def transpose(M: Matrix) -> Matrix:
    """전치 행렬"""
    result = Matrix.zeros(M.cols, M.rows)
    for i in range(M.rows):
        for j in range(M.cols):
            result.set(j, i, M.get(i, j))
    return result


class ScaledDotProductAttention:
    """Scaled Dot-Product Attention"""

    def __init__(self, d_k: int):
        self.d_k = d_k
        self.scale = math.sqrt(d_k)

    def forward(self, Q: Matrix, K: Matrix, V: Matrix,
                mask: Optional[Matrix] = None) -> Tuple[Matrix, Matrix]:
        """
        Q: (seq_len, d_k)
        K: (seq_len, d_k)
        V: (seq_len, d_v)
        Returns: output (seq_len, d_v), attention_weights (seq_len, seq_len)
        """
        seq_len = Q.rows

        # Attention Scores: Q @ K^T / sqrt(d_k)
        K_T = transpose(K)
        scores = matmul(Q, K_T)

        # 스케일링
        for i in range(scores.rows):
            for j in range(scores.cols):
                scores.set(i, j, scores.get(i, j) / self.scale)

        # 마스크 적용 (Decoder용)
        if mask is not None:
            for i in range(scores.rows):
                for j in range(scores.cols):
                    if mask.get(i, j) == 0:
                        scores.set(i, j, -1e9)

        # Softmax (행별)
        attention_weights = Matrix.zeros(seq_len, seq_len)
        for i in range(seq_len):
            row_scores = [scores.get(i, j) for j in range(seq_len)]
            row_probs = softmax(row_scores)
            for j in range(seq_len):
                attention_weights.set(i, j, row_probs[j])

        # Weighted Sum: Attention @ V
        output = matmul(attention_weights, V)

        return output, attention_weights


class MultiHeadAttention:
    """Multi-Head Attention"""

    def __init__(self, d_model: int = 512, n_heads: int = 8):
        self.d_model = d_model
        self.n_heads = n_heads
        self.d_k = d_model // n_heads
        self.d_v = d_model // n_heads

        # 각 헤드의 Q, K, V 투영 가중치
        self.W_Q = [Matrix.random(d_model, self.d_k) for _ in range(n_heads)]
        self.W_K = [Matrix.random(d_model, self.d_k) for _ in range(n_heads)]
        self.W_V = [Matrix.random(d_model, self.d_v) for _ in range(n_heads)]

        # 출력 투영
        self.W_O = Matrix.random(d_model, d_model)

        self.attention = ScaledDotProductAttention(self.d_k)

    def forward(self, X: Matrix, mask: Optional[Matrix] = None) -> Matrix:
        """
        X: (seq_len, d_model)
        Returns: (seq_len, d_model)
        """
        seq_len = X.rows
        head_outputs = []

        for h in range(self.n_heads):
            # Q, K, V 투영
            Q_h = matmul(X, self.W_Q[h])
            K_h = matmul(X, self.W_K[h])
            V_h = matmul(X, self.W_V[h])

            # Attention
            head_out, _ = self.attention.forward(Q_h, K_h, V_h, mask)
            head_outputs.append(head_out)

        # Concat heads
        concat = Matrix.zeros(seq_len, self.n_heads * self.d_v)
        for h, head_out in enumerate(head_outputs):
            for i in range(seq_len):
                for j in range(self.d_v):
                    concat.set(i, h * self.d_v + j, head_out.get(i, j))

        # 출력 투영
        output = matmul(concat, self.W_O)
        return output


class PositionWiseFeedForward:
    """Position-wise Feed Forward Network"""

    def __init__(self, d_model: int = 512, d_ff: int = 2048):
        self.W1 = Matrix.random(d_model, d_ff)
        self.W2 = Matrix.random(d_ff, d_model)
        self.b1 = [0.0] * d_ff
        self.b2 = [0.0] * d_model

    def forward(self, X: Matrix) -> Matrix:
        """X: (seq_len, d_model) -> (seq_len, d_model)"""
        # 첫 번째 선형 + GELU
        hidden = matmul(X, self.W1)
        for i in range(hidden.rows):
            for j in range(hidden.cols):
                hidden.set(i, j, gelu(hidden.get(i, j) + self.b1[j]))

        # 두 번째 선형
        output = matmul(hidden, self.W2)
        for i in range(output.rows):
            for j in range(output.cols):
                output.set(i, j, output.get(i, j) + self.b2[j])

        return output


class PositionalEncoding:
    """Positional Encoding"""

    def __init__(self, d_model: int = 512, max_len: int = 5000):
        self.d_model = d_model
        # 미리 계산된 위치 인코딩
        self.pe = self._compute_pe(max_len)

    def _compute_pe(self, max_len: int) -> Matrix:
        pe = Matrix.zeros(max_len, self.d_model)
        for pos in range(max_len):
            for i in range(self.d_model):
                if i % 2 == 0:
                    val = math.sin(pos / (10000 ** (i / self.d_model)))
                else:
                    val = math.cos(pos / (10000 ** ((i - 1) / self.d_model)))
                pe.set(pos, i, val)
        return pe

    def forward(self, X: Matrix) -> Matrix:
        """X: (seq_len, d_model) -> X + PE"""
        seq_len = X.rows
        result = Matrix.zeros(seq_len, X.cols)
        for i in range(seq_len):
            for j in range(X.cols):
                result.set(i, j, X.get(i, j) + self.pe.get(i, j))
        return result


class TransformerEncoderLayer:
    """Transformer Encoder Layer"""

    def __init__(self, d_model: int = 512, n_heads: int = 8, d_ff: int = 2048):
        self.self_attn = MultiHeadAttention(d_model, n_heads)
        self.ffn = PositionWiseFeedForward(d_model, d_ff)
        self.norm1 = lambda x: layer_norm(x)
        self.norm2 = lambda x: layer_norm(x)

    def forward(self, X: Matrix, mask: Optional[Matrix] = None) -> Matrix:
        # Self-Attention + Residual + Norm
        attn_out = self.self_attn.forward(X, mask)
        # Add & Norm
        for i in range(X.rows):
            row = [X.get(i, j) + attn_out.get(i, j) for j in range(X.cols)]
            normed = self.norm1(row)
            for j, val in enumerate(normed):
                X.set(i, j, val)

        # FFN + Residual + Norm
        ffn_out = self.ffn.forward(X)
        for i in range(X.rows):
            row = [X.get(i, j) + ffn_out.get(i, j) for j in range(X.cols)]
            normed = self.norm2(row)
            for j, val in enumerate(normed):
                X.set(i, j, val)

        return X


class SimpleTokenizer:
    """간단한 토크나이저"""

    def __init__(self, vocab: List[str]):
        self.vocab = vocab
        self.word2idx = {w: i for i, w in enumerate(vocab)}
        self.idx2word = {i: w for i, w in enumerate(vocab)}

    def encode(self, text: str) -> List[int]:
        words = text.lower().split()
        return [self.word2idx.get(w, 0) for w in words]  # 0 = <UNK>

    def decode(self, indices: List[int]) -> str:
        return ' '.join(self.idx2word.get(i, '<UNK>') for i in indices)


class MiniTransformer:
    """Mini Transformer for demonstration"""

    def __init__(self, vocab_size: int = 1000, d_model: int = 128,
                 n_heads: int = 4, n_layers: int = 2, d_ff: int = 512):
        self.d_model = d_model
        self.vocab_size = vocab_size

        # Token Embedding
        self.embedding = Matrix.random(vocab_size, d_model, scale=0.1)

        # Positional Encoding
        self.pos_encoding = PositionalEncoding(d_model)

        # Encoder Layers
        self.encoder_layers = [
            TransformerEncoderLayer(d_model, n_heads, d_ff)
            for _ in range(n_layers)
        ]

        # Output projection
        self.output_proj = Matrix.random(d_model, vocab_size, scale=0.1)

    def forward(self, token_ids: List[int]) -> Matrix:
        """Forward pass"""
        seq_len = len(token_ids)

        # Embedding lookup
        X = Matrix.zeros(seq_len, self.d_model)
        for i, tid in enumerate(token_ids):
            for j in range(self.d_model):
                X.set(i, j, self.embedding.get(tid, j))

        # Positional encoding
        X = self.pos_encoding.forward(X)

        # Encoder layers
        for layer in self.encoder_layers:
            X = layer.forward(X)

        # Output projection (logits)
        logits = matmul(X, self.output_proj)

        return logits

    def predict(self, token_ids: List[int]) -> List[int]:
        """예측 (argmax)"""
        logits = self.forward(token_ids)
        predictions = []
        for i in range(logits.rows):
            row = [logits.get(i, j) for j in range(logits.cols)]
            pred = row.index(max(row))
            predictions.append(pred)
        return predictions


# ============================================================
# Self-Attention 시각화
# ============================================================

def visualize_attention(tokens: List[str], attention_weights: Matrix) -> str:
    """Attention 가중치 시각화"""
    n = len(tokens)
    lines = ["\nAttention Weights Matrix:"]
    lines.append("      " + "  ".join(f"{t:>6}" for t in tokens))
    lines.append("-" * (7 + n * 8))

    for i, token in enumerate(tokens):
        weights = [attention_weights.get(i, j) for j in range(n)]
        row = f"{token:>5} " + "  ".join(f"{w:>6.3f}" for w in weights)
        lines.append(row)

    return "\n".join(lines)


# ============================================================
# 사용 예시
# ============================================================

if __name__ == "__main__":
    print("=" * 60)
    print("         Transformer 구현 데모")
    print("=" * 60)

    # 1. Self-Attention 데모
    print("\n1. Self-Attention 계산")
    print("-" * 40)

    # 간단한 예시: 3개 토큰, 4차원
    Q = Matrix([
        [1.0, 0.0, 0.5, 0.2],
        [0.1, 1.0, 0.3, 0.4],
        [0.5, 0.2, 1.0, 0.1]
    ], 3, 4)

    K = Matrix([
        [0.9, 0.1, 0.4, 0.3],
        [0.2, 0.8, 0.2, 0.5],
        [0.6, 0.3, 0.9, 0.0]
    ], 3, 4)

    V = Matrix([
        [1.0, 0.0],
        [0.0, 1.0],
        [0.5, 0.5]
    ], 3, 2)

    attention = ScaledDotProductAttention(d_k=4)
    output, weights = attention.forward(Q, K, V)

    tokens = ["The", "cat", "sat"]
    print(visualize_attention(tokens, weights))

    print("\n출력:")
    for i in range(output.rows):
        row = [output.get(i, j) for j in range(output.cols)]
        print(f"  {tokens[i]}: {row}")

    # 2. Mini Transformer 데모
    print("\n\n2. Mini Transformer Forward Pass")
    print("-" * 40)

    # 작은 어휘 사전
    vocab = ["<PAD>", "<UNK>", "the", "cat", "sat", "on", "mat", "dog", "ran"]
    tokenizer = SimpleTokenizer(vocab)

    # 모델 생성
    model = MiniTransformer(
        vocab_size=len(vocab),
        d_model=32,
        n_heads=2,
        n_layers=2,
        d_ff=64
    )

    # 입력 문장
    text = "the cat sat"
    token_ids = tokenizer.encode(text)
    print(f"입력: '{text}'")
    print(f"토큰 ID: {token_ids}")
    print(f"토큰: {[vocab[i] for i in token_ids]}")

    # Forward pass
    logits = model.forward(token_ids)
    print(f"\nLogits shape: {logits.rows} x {logits.cols}")
    print(f"Logits (첫 번째 토큰): {[f'{logits.get(0, j):.3f}' for j in range(min(5, logits.cols))]}...")

    # 예측
    predictions = model.predict(token_ids)
    print(f"\n예측 토큰 ID: {predictions}")
    print(f"예측 토큰: {[vocab[i] for i in predictions]}")

    print("\n" + "=" * 60)
    print("Transformer의 핵심:")
    print("1. Self-Attention: 모든 토큰이 서로 관계 계산")
    print("2. Multi-Head: 여러 관점에서 동시 분석")
    print("3. 병렬 처리: RNN 없이 시퀀스 처리")
    print("=" * 60)