Attention Transformer 어텐션 메커니즘 — Q·K·V부터 Masked·Cross·Multi-Head까지 완전 해부
ChatGPT가 긴 문장을 이해하는 원리, BERT가 문맥을 파악하는 비결, 번역 AI가 원문과 번역문을 연결하는 방법 — 그 모든 것의 뿌리가 바로 어텐션 메커니즘이다. 수식과 시각화로 낱낱이 분해한다.
어텐션은 왜
필요한가
Transformer 이전의 seq2seq 모델은 입력 전체를 고정 크기의 단일 벡터(Context Vector)에 압축했다. 100단어짜리 문장도, 1000단어짜리 문서도 같은 크기의 벡터 하나에 욱여넣어야 했다. 이 병목이 긴 문장에서 치명적인 성능 저하를 일으켰다.
Q · K · V
세 벡터의 정확한 의미
어텐션의 모든 것은 세 벡터로 시작한다. Q(Query), K(Key), V(Value). 이 이름은 데이터베이스 검색 시스템에서 차용한 비유다.
"내가 찾는 정보는?"
현재 처리 중인 토큰이 무엇을 알고 싶은지를 나타내는 벡터. 검색 엔진의 검색어에 해당한다.
예: "그녀는" → 다음에 오는 동사 정보 탐색"나는 어떤 정보를 가졌나?"
각 토큰이 자신이 어떤 정보를 제공할 수 있는지를 광고하는 벡터. 데이터베이스의 색인 키에 해당한다.
예: "달렸다" → 동사·움직임 정보 보유"실제 전달할 내용"
Q와 K의 유사도가 높을 때 실제로 전달되는 정보의 벡터. Key가 광고라면 Value는 실제 상품이다.
예: "달렸다"의 의미 표현 벡터핵심 수식 —
Scaled Dot-Product Attention
이 수식이 작동하는 4단계를 하나씩 정밀 해부한다.
내적 계산 (Dot Product) — QKᵀ
Q 행렬과 K 행렬의 전치(Transpose)를 내적한다. 결과는 [seq_len × seq_len] 정방 행렬 — 모든 토큰 쌍 간의 유사도 점수. 두 벡터가 같은 방향을 향할수록(의미가 유사할수록) 내적값이 크다. "파리에"(Key)가 "프랑스어"(Query)와 높은 점수를 가진다면, 두 단어가 강하게 연관됨을 의미한다.
스케일링 (Scaling) — ÷ √d_k
d_k의 제곱근으로 나눈다. 이유: d_k가 클수록 내적값이 기하급수적으로 커져 Softmax가 매우 작은 기울기(포화 구간)에 빠진다. 예를 들어 d_k = 64면 √64 = 8로 나누어 값을 안정화한다. 이 "scaled" 처리가 없으면 학습이 불안정해진다.
Softmax — 확률 분포로 정규화
각 행(Query 토큰)에 대해 Softmax를 적용한다. 결과: 각 행의 합이 1이 되는 확률 행렬 — 이것이 어텐션 가중치(Attention Weights)다. 각 Query가 다른 모든 토큰에 얼마나 주의를 기울일지의 확률. 값이 클수록 "이 토큰에 더 집중"한다는 의미.
Value 가중 합산 — × V
어텐션 가중치와 V 행렬을 곱한다. 결과: 각 Query 토큰의 최종 표현 — 주의를 많이 받은 토큰의 Value가 더 많이 반영된다. "프랑스어"의 새 표현에는 "파리에"의 Value가 높은 비중으로 섞이고, "나는"의 Value는 낮은 비중으로 섞인다. 이것이 문맥이 반영된 표현(Context-aware Representation)이다.
Softmax가 만드는
어텐션 가중치의 의미
어텐션 가중치를
직접 체험하다
실제 문장에서 어텐션이 어떻게 형성되는지 인터랙티브 히트맵으로 확인해보자. 버튼을 클릭해 다양한 시나리오를 탐색할 수 있다.
여러 시각 —
Multi-Head Attention
단일 어텐션은 하나의 관점만 본다. Multi-Head Attention은 Q, K, V를 h개의 부분으로 나눠 h개의 독립적인 어텐션을 병렬 계산하고 합친다.
GPT-3: h=96헤드, d_model=12288 → d_k = 12288/96 = 128 | GPT-4o: h=128 추정
미래를 가리다 —
Masked Self-Attention
GPT 같은 디코더 전용 모델이 텍스트를 생성할 때, 아직 생성하지 않은 미래 토큰을 미리 보는 것은 "정답지 보기"와 같다. Masked Self-Attention은 Score 행렬의 상삼각 부분을 -∞로 마스킹해 미래 토큰 참조를 차단한다.
인코더와 디코더를 잇다 —
Cross-Attention
번역 모델처럼 인코더-디코더 구조에서, 디코더는 자신이 생성할 출력뿐 아니라 인코더가 처리한 원문 정보를 참조해야 한다. Cross-Attention이 이 역할을 한다.
세 가지 어텐션
완전 비교
| 구분 | Self-Attention | Masked Self-Attention | Cross-Attention |
|---|---|---|---|
| Q 출처 | 자기 자신(같은 시퀀스) | 자기 자신(디코더) | 디코더 출력 |
| K·V 출처 | 자기 자신(같은 시퀀스) | 자기 자신(디코더) | 인코더 출력 |
| 마스킹 | 없음 — 양방향 | 있음 — 미래 차단 | 없음 — 전체 원문 참조 |
| 사용 위치 | 인코더 전체 | 디코더 첫 번째 서브레이어 | 디코더 두 번째 서브레이어 |
| 대표 모델 | BERT, RoBERTa | GPT, Claude, LLaMA | 원본 Transformer, T5, BART |
| 주요 역할 | 양방향 문맥 이해 | 순차적 텍스트 생성 | 원문↔번역 정보 연결 |
| 어텐션 패턴 | 완전한 정방 행렬 | 하삼각 행렬 | 직사각 행렬 (seq_dec × seq_enc) |
코드로 완성하는
Attention 구현
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
# ════════════════════════════════════════════════
# 핵심: Scaled Dot-Product Attention
# Attention(Q, K, V) = softmax(QKᵀ / √d_k) · V
# ════════════════════════════════════════════════
def scaled_dot_product_attention(
Q: torch.Tensor, # [B, H, S, d_k] Query
K: torch.Tensor, # [B, H, S, d_k] Key
V: torch.Tensor, # [B, H, S, d_v] Value
mask: torch.Tensor = None # Masked Attention용
):
d_k = Q.size(-1)
# ① QKᵀ 내적 + 스케일링 → [B, H, S_q, S_k]
scores = torch.matmul(Q, K.transpose(-2, -1)) / math.sqrt(d_k)
# ② Masking: 미래 토큰 -∞ 처리 (Masked Self-Attention)
if mask is not None:
scores = scores.masked_fill(mask == 0, float('-inf'))
# ③ Softmax → 어텐션 가중치 [B, H, S_q, S_k], 각 행의 합 = 1
attn_weights = F.softmax(scores, dim=-1)
# ④ 어텐션 가중치 × V → 최종 출력 [B, H, S_q, d_v]
output = torch.matmul(attn_weights, V)
return output, attn_weights # 가중치도 반환 (시각화용)
# ════════════════════════════════════════════════
# Multi-Head Attention (Self / Masked / Cross 통합)
# ════════════════════════════════════════════════
class MultiHeadAttention(nn.Module):
def __init__(self, d_model: int, n_heads: int):
super().__init__()
assert d_model % n_heads == 0, "d_model은 n_heads로 나눠져야 합니다"
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads # 헤드당 차원
# Q·K·V 투영 행렬 (학습 파라미터)
self.W_Q = nn.Linear(d_model, d_model, bias=False) # W_Q: d_model→d_model
self.W_K = nn.Linear(d_model, d_model, bias=False) # W_K: d_model→d_model
self.W_V = nn.Linear(d_model, d_model, bias=False) # W_V: d_model→d_model
self.W_O = nn.Linear(d_model, d_model, bias=False) # 출력 투영
def split_heads(self, x: torch.Tensor) -> torch.Tensor:
# [B, S, d_model] → [B, H, S, d_k] 로 헤드 분리
B, S, _ = x.shape
x = x.view(B, S, self.n_heads, self.d_k)
return x.transpose(1, 2) # [B, H, S, d_k]
def forward(
self,
query: torch.Tensor, # Q 소스: Self→자기자신, Cross→디코더
key: torch.Tensor = None, # K 소스: Self→자기자신, Cross→인코더
value: torch.Tensor = None, # V 소스: Self→자기자신, Cross→인코더
mask: torch.Tensor = None # Masked Self-Attention용
):
# Cross-Attention이면 key/value를 인코더 출력으로, 아니면 자기 자신
if key is None: key = query # Self-Attention
if value is None: value = query # Self-Attention
B = query.size(0)
# Q·K·V 투영 + 헤드 분리
Q = self.split_heads(self.W_Q(query)) # [B, H, S_q, d_k]
K = self.split_heads(self.W_K(key)) # [B, H, S_k, d_k]
V = self.split_heads(self.W_V(value)) # [B, H, S_k, d_v]
# 어텐션 계산 (모든 헤드 병렬)
attn_out, attn_weights = scaled_dot_product_attention(Q, K, V, mask)
# 헤드 합치기: [B, H, S, d_k] → [B, S, d_model]
attn_out = attn_out.transpose(1, 2).contiguous()
attn_out = attn_out.view(B, -1, self.d_model)
return self.W_O(attn_out), attn_weights # 출력 + 어텐션 가중치
# ════════════════════════════════════════════════
# Causal Mask 생성 (Masked Self-Attention용)
# ════════════════════════════════════════════════
def make_causal_mask(seq_len: int, device: str = 'cpu'):
"""하삼각 마스크: 미래 토큰 참조 차단"""
mask = torch.tril(torch.ones(seq_len, seq_len, device=device))
# [[1,0,0,0], ← "나는"은 자신만 참조
# [1,1,0,0], ← "AI를"은 자신+이전만
# [1,1,1,0], ← "공부"는 앞 3개
# [1,1,1,1]] ← "한다"는 모두 참조
return mask.unsqueeze(0).unsqueeze(0) # [1, 1, S, S] 브로드캐스트용
# ════ 사용 예시 ════
B, S, d_model, n_heads = 2, 8, 512, 8
mha = MultiHeadAttention(d_model, n_heads)
x_enc = torch.randn(B, S, d_model) # 인코더 입력
x_dec = torch.randn(B, S, d_model) # 디코더 입력
causal_mask = make_causal_mask(S)
# 1. Self-Attention (인코더용 — 마스크 없음)
out_self, w_self = mha(x_enc)
print(f"Self-Attention 출력: {out_self.shape}") # [2, 8, 512]
print(f"어텐션 가중치 형태: {w_self.shape}") # [2, 8, 8, 8]
# 2. Masked Self-Attention (디코더 첫 번째 레이어)
out_masked, _ = mha(x_dec, mask=causal_mask)
print(f"Masked Self-Attention 출력: {out_masked.shape}") # [2, 8, 512]
# 3. Cross-Attention (디코더 두 번째 레이어)
# Q=디코더, K·V=인코더 (핵심 차이!)
out_cross, _ = mha(query=x_dec, key=x_enc, value=x_enc)
print(f"Cross-Attention 출력: {out_cross.shape}") # [2, 8, 512]
어텐션을 이해하면
AI가 보인다
어텐션 메커니즘의 본질은 단순하다 — 모든 토큰이 다른 모든 토큰과의 관련성을 직접 계산한다. Q·K 내적으로 유사도를 측정하고, Softmax로 정규화하고, V를 가중 합산하는 세 단계가 전부다.
Self-Attention은 양방향 문맥을 이해하고(BERT), Masked Self-Attention은 과거만 보며 순차 생성하고(GPT), Cross-Attention은 두 시퀀스를 연결한다(번역). 이 세 가지 변형이 현대 AI의 모든 기능을 가능하게 한다.
다음 시리즈에서는 Positional Encoding과 Layer Normalization의 수학적 원리를 깊이 파고든다.
가치 보강: 2026년 5월 23일 기준
이 글은 독자가 바로 적용할 수 있는 기준을 더하기 위해 2026년 5월 23일 기준으로 보강했습니다. 단순 정보 나열보다 실제 예시, 확인 순서, 관련 글 연결을 함께 보는 것이 블로그 글의 가치를 높입니다.
실전 적용 예시
| 상황 | 어떻게 보면 좋은가 |
|---|---|
| 처음 읽을 때 | 글의 결론과 적용 대상을 먼저 확인합니다. |
| 실제로 쓸 때 | 내 상황에 맞는 예시만 골라 적용하고, 숫자나 정책은 원문을 확인합니다. |
| 다음 행동 | 관련 글을 이어 읽어 주제 전체 흐름을 잡습니다. |
읽고 바로 확인할 것
- 내 상황에 적용 가능한 글인지 확인했는가?
- 날짜, 정책, 요금, 게임 정보처럼 바뀌는 내용은 다시 확인했는가?
- 관련 글을 함께 읽어 맥락을 보완했는가?
- 글의 예시를 그대로 복사하지 않고 내 상황에 맞게 바꿨는가?
같이 보면 좋은 글
'AI란 무엇인가 > AI 기본' 카테고리의 다른 글
| 생성형 AI가 틀린 답을 하는 이유: 할루시네이션 쉽게 이해하기 (0) | 2026.05.22 |
|---|---|
| Positional Encoding&Layer Normalization (0) | 2026.04.04 |
| AI Transformer~~ (0) | 2026.03.31 |
| AI 파라미터란 무엇인가 — 종류·역할·차이점 완전 정복 (3) | 2026.03.30 |
| 대형 언어 모델 vs 멀티모달 모델 — 완전 정복 (LLM vs MLM) (0) | 2026.03.30 |