트랜스포머 — 어텐션을 손으로 계산한다 · 어텐션의 안쪽 · 이론
어텐션은 가중 평균이다
한 줄 요약
어텐션은 "지금 이 자리에서 어느 자리를 얼마나 볼까" 를 정해서 값들의 가중 평균을 내는 연산이다. 그게 전부다.
세 가지 이름
같은 입력에서 세 벡터를 만든다.
- Q(query) — 내가 찾는 것
- K(key) — 각 자리가 내세우는 표지
- V(value) — 그 자리가 실제로 내놓는 내용
도서관 비유가 정확하다. 질문(Q)을 들고 가서 책등의 제목(K)과 맞춰 보고, 잘 맞는 책의 내용(V)을 그 정도만큼 가져온다.
score = Q · Kᵀ / √d ← 얼마나 맞는가weight = softmax(score) ← 합이 1이 되게out = weight · V ← 가중 평균세 줄이다. 나머지는 전부 이 세 줄을 여러 번, 여러 방향으로 하는 것이다.
왜 √d 로 나누는가 — 숫자로
가장 자주 "그냥 그렇게 한다" 고 넘어가는 부분인데, 이유가 분명하다.
성분이 독립이고 분산이 1인 두 d 차원 벡터의 내적은 분산이 d 가 된다. d=64 면 점수가 대략 ±8 범위로 흩어진다.
소프트맥스에 ±8 짜리 점수가 들어가면 어떻게 되나? e⁸ / e⁻⁸ ≈ 9백만 이다. 가장 큰 하나가 거의 1을 다 가져가고 나머지는 0이 된다. 가중 평균이 아니라 그냥 "하나 고르기" 가 되어 버린다.
그러면 학습이 안 된다. 소프트맥스가 포화되면 기울기가 0에 가까워지기 때문이다.
√d 로 나누면 분산이 다시 1로 돌아온다. 이 실습의 2단계에서 이걸 직접 잰다 — 나누지 않았을 때 최대 확률이 얼마까지 올라가는지.
소프트맥스는 최댓값을 빼고 계산한다
def softmax(xs): m = max(xs) # 이 한 줄이 없으면 e = [exp(x - m) for x in xs] # exp(1000) 에서 터진다 s = sum(e) return [v / s for v in e]exp(x - max) 는 수학적으로 같은 값이지만 오버플로가 나지 않는다. 실무 코드에서 이 한 줄을 빼먹으면 큰 값이 들어오는 순간 inf 와 nan 이 나온다.
마스크 — 미래를 못 보게
언어 모델은 다음 토큰을 맞히도록 학습한다. 그런데 어텐션은 기본적으로 모든 자리를 다 본다. 답을 보고 답을 맞히는 셈이다.
그래서 점수 행렬의 위쪽 삼각형을 -inf 로 만든다.
k0 k1 k2q0 0.3 -inf -infq1 0.1 0.5 -infq2 0.2 0.1 0.4-inf 는 exp 를 통과하면 정확히 0이 된다. 0을 곱하는 게 아니라 소프트맥스 전에 -inf 를 더하는 것이 핵심이다 — 소프트맥스 후에 0을 곱하면 나머지 가중치의 합이 1이 아니게 된다.
이것이 "causal" 또는 "decoder" 마스크다. BERT 같은 인코더 모델에는 없다. 그래서 BERT 는 문장 전체를 보고 이해하는 데 강하고, GPT 는 이어 쓰는 데 강하다.
멀티헤드 — 왜 쪼개는가
d=64 를 한 번에 쓰는 대신 8개로 쪼개 d=8 짜리 어텐션을 8번 한 뒤 이어 붙인다. 계산량은 거의 같다.
왜? 하나의 소프트맥스는 한 종류의 관계만 표현할 수 있다. 가중치 합이 1이라 여러 곳을 동시에 강하게 볼 수 없다. 헤드를 나누면 어떤 헤드는 바로 앞 단어를, 어떤 헤드는 문장 앞의 주어를, 어떤 헤드는 따옴표 짝을 본다.
학습된 모델의 헤드를 열어 보면 실제로 그렇게 나뉘어 있다.
위치 인코딩 — 어텐션은 순서를 모른다
이게 처음 배울 때 가장 놀라운 부분이다.
어텐션에는 순서 개념이 전혀 없다. 입력 토큰을 섞으면 출력도 똑같이 섞여 나올 뿐, 값 자체는 변하지 않는다(permutation equivariant). "나는 너를 좋아해" 와 "너를 나는 좋아해" 가 구별되지 않는다.
그래서 위치 정보를 입력에 더해 준다.
- 사인/코사인(원논문) — 학습 파라미터가 없고 긴 길이로 외삽이 된다
- 학습형 임베딩(BERT, ViT) — 간단하지만 학습 때 본 길이를 넘지 못한다
- RoPE(LLaMA, Qwen 등 요즘 대부분) — Q·K 를 회전시켜 상대 위치를 자연스럽게 담는다. 길이 확장이 쉬워서 컨텍스트를 늘리는 요즘 모델들이 전부 이걸 쓴다
- ALiBi — 거리에 비례한 페널티를 점수에 더한다
5단계에서 이걸 직접 확인한다. 위치 인코딩 없이 입력을 섞으면 출력이 그대로 따라 섞이고, 더하면 그렇지 않다.
LayerNorm 과 잔차 연결
x = x + Attention(LayerNorm(x))x = x + FFN(LayerNorm(x))- 잔차(residual) — 깊이 쌓아도 기울기가 입력까지 도달한다. 이게 없으면 6층만 넘어도 학습이 안 된다
- LayerNorm — 각 토큰 벡터를 평균 0, 분산 1로 맞춘다. 배치가 아니라 특징 축으로 정규화하는 것이 BatchNorm 과의 차이이고, 그래서 배치 크기나 시퀀스 길이에 영향받지 않는다
LayerNorm 을 어텐션 앞에 두느냐 뒤에 두느냐(pre-LN vs post-LN)는 실제로 학습 안정성을 가른다. 원논문은 post-LN 이었지만 요즘은 거의 전부 pre-LN 이다 — 워밍업 없이도 학습이 되기 때문이다.
비용이 어디서 나오나
점수 행렬은 n × n 이다. 시퀀스 길이의 제곱으로 메모리와 계산이 는다. 컨텍스트 4k 를 8k 로 늘리면 4배다.
이걸 줄이려는 시도가 요즘 연구의 큰 줄기다.
- FlashAttention — 수학은 그대로 두고 GPU 메모리 접근 순서를 바꿔 실제 속도를 몇 배 올린다. 근사가 아니다
- GQA / MQA — K·V 헤드를 여러 Q 헤드가 공유해 추론 시 KV 캐시를 줄인다. 요즘 오픈 모델 대부분이 GQA 다
- MoE — 층마다 전문가 여럿 중 일부만 켜서, 파라미터는 크되 계산은 작게 유지한다
- 슬라이딩 윈도우 / 희소 어텐션 — 먼 자리는 아예 안 본다
정리
어텐션 자체는 세 줄이다. 나머지는 그 세 줄을 안정적으로, 싸게, 순서를 알면서 하는 방법에 관한 것이다. 다음 실습에서 그 세 줄을 직접 쓰고, 스케일링과 위치 인코딩이 없으면 무엇이 무너지는지 숫자로 본다.
현장에서
이 계산을 직접 짤 일은 거의 없습니다. 프레임워크가 다 해 주기 때문입니다. 그런데도 알아 두어야 하는 이유는 문제가 생겼을 때 어디를 볼지 가 여기서 갈리기 때문입니다.
학습 손실이 갑자기 nan 이 되면 대개 소프트맥스 앞의 값이 넘친 것이고, 마스크를 잘못 걸면 오류 없이 조용히 미래 토큰을 보며 학습해 평가 점수만 이상하게 좋게 나옵니다. 배치 크기를 바꿨는데 결과가 달라진다면 정규화 축을 의심해야 하고, 컨텍스트를 두 배로 늘렸더니 메모리가 네 배로 뛰는 것은 버그가 아니라 구조상 당연한 일입니다.
추론 서비스를 운영한다면 KV 캐시가 곧 메모리 예산입니다. 동시 요청 수와 최대 컨텍스트 길이를 곱한 값이 GPU 메모리에 들어가는지를 먼저 계산해 보고, 안 들어가면 GQA 를 쓰는 모델을 고르거나 컨텍스트 상한을 낮추는 것이 실제 선택지입니다.