Day 5: Attention 메커니즘 심화
Transformer의 핵심은 Attention입니다. 오늘은 Query/Key/Value의 의미부터 Multi-Head Attention, 그리고 위치 인코딩까지 numpy로 직접 구현하며 완전히 이해합니다.
Query, Key, Value의 직관적 이해
도서관 비유로 설명하면:
- Query(Q): 내가 찾고 싶은 것 (검색어)
- Key(K): 각 책의 제목/태그 (색인)
- Value(V): 실제 책의 내용 (콘텐츠)
Q와 K의 유사도를 계산하고, 그 유사도에 비례하여 V의 내용을 가져옵니다.
Scaled Dot-Product Attention 구현
import numpy as np
def scaled_dot_product_attention(Q, K, V, mask=None):
"""
Q: (seq_len, d_k) - 쿼리
K: (seq_len, d_k) - 키
V: (seq_len, d_v) - 값
mask: 디코더에서 미래 토큰을 가리는 용도
"""
d_k = K.shape[-1]
# 내적 후 스케일링 (d_k가 크면 내적값도 커져서 softmax가 극단적으로 됨)
scores = np.matmul(Q, K.T) / np.sqrt(d_k)
# 마스킹: 디코더에서 미래 토큰을 보지 못하게
if mask is not None:
scores = np.where(mask == 0, -1e9, scores)
# Softmax로 확률 분포 생성
exp_scores = np.exp(scores - np.max(scores, axis=-1, keepdims=True))
weights = exp_scores / np.sum(exp_scores, axis=-1, keepdims=True)
return np.matmul(weights, V), weights
# 4개 토큰, 8차원
seq_len, d_k = 4, 8
Q = np.random.randn(seq_len, d_k)
K = np.random.randn(seq_len, d_k)
V = np.random.randn(seq_len, d_k)
# 인과적 마스크 (GPT 스타일: 이전 토큰만 참조)
causal_mask = np.tril(np.ones((seq_len, seq_len)))
print(f"인과적 마스크:\n{causal_mask.astype(int)}")
output, weights = scaled_dot_product_attention(Q, K, V, mask=causal_mask)
print(f"어텐션 가중치:\n{weights.round(3)}")
Multi-Head Attention 구현
def multi_head_attention(x, num_heads, d_model):
"""
여러 개의 어텐션 헤드를 병렬로 실행
각 헤드가 서로 다른 관계 패턴을 학습
"""
d_k = d_model // num_heads
seq_len = x.shape[0]
outputs = []
for head in range(num_heads):
# 각 헤드마다 별도의 Q, K, V 투영 가중치
W_q = np.random.randn(d_model, d_k) * 0.1
W_k = np.random.randn(d_model, d_k) * 0.1
W_v = np.random.randn(d_model, d_k) * 0.1
Q = np.matmul(x, W_q)
K = np.matmul(x, W_k)
V = np.matmul(x, W_v)
head_output, _ = scaled_dot_product_attention(Q, K, V)
outputs.append(head_output)
# 모든 헤드의 출력을 연결
concatenated = np.concatenate(outputs, axis=-1)
# 최종 선형 투영
W_o = np.random.randn(d_model, d_model) * 0.1
return np.matmul(concatenated, W_o)
d_model = 64
num_heads = 8 # 64 / 8 = 8차원씩 각 헤드가 담당
x = np.random.randn(4, d_model)
output = multi_head_attention(x, num_heads, d_model)
print(f"Multi-Head Attention 출력: {output.shape}")
# 헤드 1: 주어-동사 관계, 헤드 2: 형용사-명사 관계, ...
위치 인코딩: Sinusoidal vs RoPE
import numpy as np
def sinusoidal_position_encoding(max_len, d_model):
"""원본 Transformer의 위치 인코딩"""
pe = np.zeros((max_len, d_model))
position = np.arange(max_len)[:, np.newaxis]
div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
pe[:, 0::2] = np.sin(position * div_term) # 짝수 차원: sin
pe[:, 1::2] = np.cos(position * div_term) # 홀수 차원: cos
return pe
def apply_rope(x, position):
"""RoPE (Rotary Position Embedding) - Llama, GPT-NeoX 등에서 사용"""
d = x.shape[-1]
freqs = 1.0 / (10000 ** (np.arange(0, d, 2) / d))
angles = position * freqs
# 짝수/홀수 차원을 회전
cos_vals = np.cos(angles)
sin_vals = np.sin(angles)
x_even, x_odd = x[..., 0::2], x[..., 1::2]
rotated_even = x_even * cos_vals - x_odd * sin_vals
rotated_odd = x_even * sin_vals + x_odd * cos_vals
result = np.zeros_like(x)
result[..., 0::2] = rotated_even
result[..., 1::2] = rotated_odd
return result
# Sinusoidal 위치 인코딩 확인
pe = sinusoidal_position_encoding(max_len=10, d_model=16)
print(f"위치 인코딩 형태: {pe.shape}")
print(f"위치 0과 1의 차이: {np.linalg.norm(pe[0] - pe[1]):.3f}")
print(f"위치 0과 9의 차이: {np.linalg.norm(pe[0] - pe[9]):.3f}")
# 가까운 위치일수록 벡터가 유사
Attention은 “어떤 토큰이 어떤 토큰에 집중해야 하는가”를 학습합니다. Multi-Head는 여러 관점에서 동시에 이를 수행하여 더 풍부한 표현을 만듭니다.
오늘의 연습문제
np.sqrt(d_k)로 스케일링하는 이유를 수학적으로 설명해보세요. d_k가 64일 때와 스케일링을 하지 않을 때 softmax 출력이 어떻게 달라지는지 실험하세요.- Multi-Head Attention에서 헤드 수를 1, 4, 8, 16으로 바꿔보고 d_k가 어떻게 달라지는지 확인하세요. 헤드 수가 너무 많으면 어떤 문제가 생길까요?
- Sinusoidal 위치 인코딩과 RoPE의 가장 큰 차이점을 정리하고, 왜 최신 모델들이 RoPE를 선호하는지 조사해보세요.