[CS300 #272] 어텐션과 트랜스포머 — 모든 단어가 모든 단어를 본다
컴퓨터공학 300 주제 시리즈의 272번째 글이다. 전체 지도는 여기.
한 줄 요약
어텐션은 각 위치가 다른 모든 위치에 “얼마나 주목할지”를 쿼리·키의 내적으로 계산해 값(value)의 가중 평균을 가져오는 연산이고, 트랜스포머는 순환 없이 이 어텐션과 피드포워드 층만 쌓아 시퀀스를 병렬로 처리하는 구조다.
왜 필요한가
앞 글에서 본 RNN 에는 두 가지 한계가 있었다. 먼 거리의 정보가 사라지고, 시간 방향으로 병렬화가 안 된다.
Bahdanau 외(2014)는 번역에서 디코더가 단어를 낼 때마다 인코더의 모든 위치를 다시 보고 필요한 곳에 가중치를 주는 어텐션을 붙여 첫 번째 문제를 크게 줄였다. Vaswani 외(2017)의 “Attention Is All You Need” 는 한 걸음 더 나가 순환을 아예 없앴다. 그 결과 시퀀스 전체를 한 번에 행렬 연산으로 처리할 수 있게 되었고, GPU 에서 대규모 학습이 가능해졌다. 오늘날의 대규모 언어 모델은 모두 이 구조를 바탕으로 한다.
핵심 개념
쿼리, 키, 값
검색에 비유하면 이해가 쉽다.
- 쿼리(Q): 내가 찾는 것. “이 단어의 뜻을 정하려면 무엇이 필요한가”
- 키(K): 각 위치가 내건 색인. “나는 이런 정보를 가졌다”
- 값(V): 실제로 가져갈 내용.
입력 X(토큰 수 T × 차원 d)에 학습되는 행렬 세 개를 곱해 만든다. Q = X·Wq, K = X·Wk, V = X·Wv.
스케일드 닷-프로덕트 어텐션
Attention(Q, K, V) = softmax( Q·Kᵀ / √d_k ) · V
- Q·Kᵀ: T×T 점수 행렬. (i, j) 는 i 번째 토큰이 j 번째 토큰과 얼마나 관련 있는가.
- √d_k 로 나누기: 차원이 크면 내적의 분산이 d_k 에 비례해 커지고, 소프트맥스가 한 칸에 몰려 기울기가 거의 0 이 된다. 그것을 막는다.
- 소프트맥스: 각 행을 합이 1 인 가중치로.
- ·V: 가중치대로 값을 섞는다.
인과 마스크
언어 모델은 다음 토큰을 예측하므로, i 번째 위치가 i 보다 뒤를 보면 정답을 훔쳐보는 셈이다. 점수 행렬의 위쪽 삼각형을 −∞ 로 채워 소프트맥스 뒤 0 이 되게 한다. GPT 계열(디코더 전용)이 이 방식이고, BERT(인코더 전용)는 마스크 없이 양방향을 본다.
멀티헤드
어텐션을 h 개 병렬로 돌린다. 각 헤드는 d/h 차원의 작은 Q·K·V 를 쓰고, 결과를 이어 붙여 다시 선형 변환한다. 한 헤드는 문법 관계를, 다른 헤드는 지시어가 가리키는 대상을 보는 식으로 서로 다른 관계를 동시에 잡는다.
트랜스포머 블록
x ─┬─▶ LayerNorm ─▶ 멀티헤드 어텐션 ─▶ (+) ─┬─▶ LayerNorm ─▶ FFN ─▶ (+) ─▶ 다음 블록
└─────────────── 잔차 연결 ──────────────┘ └──────── 잔차 연결 ────┘
- FFN: 위치마다 독립적으로 적용하는 2층 MLP. 어텐션이 “정보를 모으는” 일이라면 FFN 은 “모은 정보를 가공하는” 일이다.
- 잔차 연결 + 정규화: 깊게 쌓아도 학습이 안정되게 한다. 그림은 요즘 흔한 Pre-LN 배치이고, 원 논문은 정규화를 덧셈 뒤에 두었다.
- 위치 정보: 어텐션 자체는 순서를 모른다(토큰을 섞어도 같은 결과의 순열). 그래서 위치 인코딩을 더한다. 원 논문은 사인·코사인 함수를, 최근 모델은 회전 위치 임베딩(RoPE) 같은 상대 위치 방식을 많이 쓴다.
비용
점수 행렬이 T×T 이므로 시간과 메모리가 시퀀스 길이의 제곱에 비례한다. 컨텍스트를 두 배로 늘리면 어텐션 비용은 네 배다. 긴 문맥 처리를 위한 연구(희소 어텐션, 메모리 효율적인 구현 등)가 계속되는 이유다. 추론할 때는 이전 토큰의 K, V 를 저장해 두고 재사용하는데 이것을 KV 캐시라 한다.
| RNN | 트랜스포머 | |
|---|---|---|
| 먼 위치까지 경로 길이 | O(T) | O(1) |
| 학습 시 병렬화 | 어려움 | 쉬움 |
| 길이에 대한 비용 | O(T) | O(T²) |
직접 해 보기
토큰 4개, 차원 8 로 어텐션을 직접 계산하고, 인과 마스크와 √d 스케일링의 효과를 본다.
import numpy as np
np.set_printoptions(precision=3, suppress=True)
rng = np.random.default_rng(0)
T, d = 4, 8 # 토큰 4개, 차원 8
X = rng.normal(size=(T, d))
Wq, Wk, Wv = (rng.normal(size=(d, d)) / np.sqrt(d) for _ in range(3))
def softmax(z):
z = z - z.max(-1, keepdims=True)
e = np.exp(z); return e / e.sum(-1, keepdims=True)
def attention(X, causal=False):
Q, K, V = X @ Wq, X @ Wk, X @ Wv
scores = Q @ K.T / np.sqrt(d)
if causal:
scores = np.where(np.tril(np.ones((T, T))) == 1, scores, -np.inf)
A = softmax(scores)
return A @ V, A
_, A = attention(X)
print("양방향 어텐션 가중치(행 합=1):"); print(A)
_, A = attention(X, causal=True)
print("인과 마스크 어텐션 가중치:"); print(A)
# 스케일링이 없으면 softmax 가 한쪽으로 쏠린다
big = rng.normal(size=512) @ rng.normal(size=(512, 5))
print("나눔 없음 최대 가중치:", softmax(big).max().round(3), " sqrt(d) 로 나눔:", softmax(big/np.sqrt(512)).max().round(3))
실행 결과:
양방향 어텐션 가중치(행 합=1):
[[0.344 0.127 0.332 0.198]
[0.775 0.004 0.053 0.168]
[0.252 0.278 0.286 0.184]
[0.216 0.278 0.211 0.295]]
인과 마스크 어텐션 가중치:
[[1. 0. 0. 0. ]
[0.995 0.005 0. 0. ]
[0.309 0.34 0.351 0. ]
[0.216 0.278 0.211 0.295]]
나눔 없음 최대 가중치: 1.0 sqrt(d) 로 나눔: 0.454
- 양방향 행렬은 모든 칸이 0 이 아니다. 모든 토큰이 모든 토큰을 본다. 각 행의 합은 1 이다.
- 인과 마스크를 걸면 대각선 위가 정확히 0 이 된다. 첫 토큰은 자기 자신만 본다(1.0). 마지막 행은 볼 수 있는 범위가 같으므로 양방향과 똑같다.
- 마지막 줄: 512 차원 벡터의 내적을 그대로 소프트맥스에 넣으면 한 칸이 1.0 을 독차지한다. √512 로 나누면 0.454 로 분포가 펴진다. 소프트맥스가 포화되면 기울기가 사라져 학습이 멈추므로, 이 나눗셈 하나가 학습 가능성을 좌우한다.
현업에서는
- 모든 LLM 의 비용 구조가 여기서 나온다. 긴 문서를 통째로 넣으면 어텐션 비용과 KV 캐시 메모리가 급증한다. 서빙 시스템이 컨텍스트 길이에 따라 요금·지연이 달라지는 이유다.
- KV 캐시 = 메모리 압박. 동시 요청 수 × 컨텍스트 길이 × 층 수만큼 캐시가 쌓인다. GPU 메모리가 모델 가중치보다 캐시로 먼저 차는 일이 흔하다.
- 비전·음성·단백질까지. 이미지를 패치로 잘라 토큰처럼 다루는 ViT, 음성 인식, 단백질 구조 예측에도 트랜스포머가 쓰인다. 입력을 토큰 시퀀스로 바꿀 수 있으면 같은 구조가 적용된다.
확인 문제
- 어텐션에서 √d_k 로 나누는 이유는?
- 디코더 전용 언어 모델에 인과 마스크가 필요한 이유는?
- 위치 인코딩이 없으면 트랜스포머는 “개가 사람을 문다”와 “사람이 개를 문다”를 구별할 수 있는가?
- 컨텍스트 길이를 4배로 늘리면 어텐션 점수 행렬의 크기는 몇 배가 되는가?
- RNN 대비 트랜스포머가 학습을 빠르게 하는 핵심 이유는?
풀이
- 차원이 클수록 내적의 분산이 커져 소프트맥스가 포화되고 기울기가 사라지는 것을 막기 위해서다.
- 학습할 때 각 위치가 다음 토큰을 예측해야 하는데, 뒤쪽을 보면 정답을 미리 보는 셈이 되기 때문이다.
- 구별하지 못한다. 어텐션은 순열 등변이라, 입력 순서를 바꾸면 출력도 같은 방식으로 자리만 바뀔 뿐 각 단어가 얻는 표현은 같다. 누가 주어인지 알 수 없다.
- 16배. T×T 이므로.
- 순환 의존이 없어 시퀀스의 모든 위치를 한 번의 행렬 연산으로 병렬 처리할 수 있다.
더 읽을거리 (References)
- A. Vaswani 외, “Attention Is All You Need”, 2017. arXiv:1706.03762
- D. Bahdanau, K. Cho, Y. Bengio, “Neural Machine Translation by Jointly Learning to Align and Translate”, 2014. arXiv:1409.0473
- PyTorch 공식 문서, torch.nn.MultiheadAttention
- J. Su 외, “RoFormer: Enhanced Transformer with Rotary Position Embedding”, 2021. arXiv:2104.09864