3. 어텐션: 새 토큰이 문맥을 얻는 과정
LLM 어텐션의 실제 계산 과정을 숫자로 추적한다. Q·K 내적이 주목도를 만들고, V의 가중합이 문맥을 만드는 과정. 멀티 헤드가 필요한 이유, 스케일링(√d), 잔차 연결까지.
2편에서 "OOM killer가 nginx를"이 5개의 2048차원 벡터가 되는 과정을 봤다. 각 벡터에는 위치 정보까지 반영되어 있다. 하지만 아직 각 토큰은 서로의 존재를 모른다. "OOM" 벡터는 뒤에 "killer"가 온다는 걸 모르고, "killer" 벡터는 앞에 "OOM"이 있다는 걸 모른다.
이 벡터들에 문맥을 만들어주는 게 어텐션이다.
2~4편에서는 부품의 동작 원리를 이해하는 데 집중한다. 여기서는 "OOM"이라는 토큰 하나가 어텐션을 통과하면 어떻게 되는지, 그리고 "killer"라는 두 번째 토큰이 추가되면 무슨 일이 벌어지는지까지 본다. 토큰이 5개로 늘어나는 건 5편에서 다룬다.
어텐션이 하는 일
한마디로 정리하면 이렇다.
인풋: 토큰의 벡터 (2048차원)
아웃풋: 같은 차원이지만, 이전 토큰들의 문맥이 반영된 벡터
어텐션의 역할:
새 토큰이 들어올 때마다, 이전에 있던 토큰들과의 관계를 계산해서
새 토큰의 벡터에 문맥 정보를 반영해주는 것.
레이어 1 어텐션을 거치면 → 바로 앞 토큰들과의 관계가 반영됨.
레이어 2 어텐션을 거치면 → 이미 1차 문맥이 담긴 벡터들 사이에서
더 깊은 관계가 반영됨.
...
레이어 36까지 반복 → 풍부한 문맥.1편에서 "어텐션은 토큰 간 정보를 교환한다"고 했다. 이번 편에서 그 계산 과정을 실제 숫자로 추적한다.
Q, K, V — 세 벡터를 만드는 이유
어텐션에 들어온 벡터에 학습된 행렬(W)을 곱해서 세 종류의 벡터를 만든다.
각 토큰의 벡터 × 학습된 행렬 → 세 종류의 벡터:
Q (Query): "나를 완성하려면 어떤 맥락이 필요하지?"
K (Key): "나는 이런 종류의 정보를 갖고 있어."
V (Value): "가져갈 수 있는 내 실제 정보."
×W_Q ×W_K ×W_V
┌────────┐ ┌────────┐ ┌────────┐
"OOM" 벡터 ───→ │ Q_OOM │ │ K_OOM │ │ V_OOM │
└────────┘ └────────┘ └────────┘
"killer" 벡터 ─→ │ Q_killer│ │ K_killer│ │ V_killer│
└────────┘ └────────┘ └────────┘
...5개 토큰 전부 동일하게.
W_Q, W_K, W_V: 각각 2048×2048 행렬 = 약 400만 개의 학습된 파라미터.왜 세 개가 필요한가? 같은 토큰이라도 "질문할 때"와 "답할 때" 다른 측면이 부각되어야 한다. "nginx"가 맥락을 찾을 때(Q)는 "나는 프로세스인데 뭔 일이 생겼지?" 방향이고, 다른 토큰에 정보를 줄 때(K/V)는 "나는 웹 서버야" 방향이다. 하나의 벡터로 두 역할을 동시에 잘 수행하기는 어렵다.
첫 번째 토큰: "OOM" — 참조할 문맥이 없다
"OOM"은 시퀀스의 첫 토큰이다. 앞에 아무것도 없다.
OOM의 Q가 참조할 수 있는 K: OOM의 K뿐.
OOM의 Q · OOM의 K = 어떤 점수
→ softmax → 1.00 (유일한 후보니까 100%)
→ OOM의 새 벡터 = 1.00 × OOM의 V = OOM의 V 그대로
첫 토큰은 어텐션을 거쳐도 거의 변하지 않는다.
참조할 문맥이 없으니까 당연하다.아무런 감흥이 없는 결과다. 어텐션의 진짜 가치는 두 번째 토큰부터 나온다.
두 번째 토큰: "killer" — 여기서 문맥이 생긴다
"killer"가 추가되면 비로소 어텐션이 의미 있는 일을 한다.
killer의 Q가 참조할 수 있는 K: OOM의 K + killer의 K.
(미래 토큰인 "가", "nginx", "를"의 K는 볼 수 없다.)
killer의 Q · OOM의 K
= (0.512 × -0.215) + (0.223 × 0.438) + (-0.341 × 0.612) + ...
(128번의 곱셈 → 합산. 헤드 1개 = 128차원)
= 14.7
÷ √128 = 14.7 / 11.3 = 1.30
↑ 스케일링: 차원이 클수록 내적 값이 커지므로
√차원으로 나눠서 softmax가 안정적으로 동작하도록 맞춘다.
killer의 Q · killer의 K
= ... = 11.2
÷ √128 = 0.99
softmax로 정규화 (합이 1이 되도록):
OOM: e^1.30 / (e^1.30 + e^0.99) = 0.58
killer: e^0.99 / (e^1.30 + e^0.99) = 0.42
→ killer는 OOM에 58%, 자기 자신에 42% 주목.Q·K 내적 후에 √128로 나누는 과정이 있다. 이걸 스케일링이라고 하는데, 차원이 클수록 내적 값이 커져서 softmax가 한쪽에 극단적으로 몰리는 걸 방지한다. 실질적으로 softmax의 안정성을 위한 장치다.
이 주목도(0.58, 0.42)로 V를 가중합한다.
killer의 새 벡터 = 0.58 × V_OOM + 0.42 × V_killer
= 0.58 × [0.127, -0.334, 0.556, ...]
+ 0.42 × [-0.423, 0.167, 0.712, ...]
──────────────────────────────────────
= [-0.104, -0.124, 0.621, ...]
→ "killer" 벡터에 "OOM" 정보가 58% 섞여 들어왔다.
→ 이제 이 벡터는 "OOM과 관련된 killer"라는 문맥을 담고 있다.여기서 핵심은 "killer"라는 단독 의미가 "OOM과 관련된 killer"라는 문맥을 가진 의미로 바뀌었다는 거다. "가", "nginx", "를"이 순서대로 추가되면, 매번 같은 과정이 반복된다. 새 토큰의 Q가 이전 모든 토큰의 K와 내적을 구하고, V의 가중합으로 새 벡터를 만든다. 이 과정이 5개, 100개, 10,000개로 어떻게 확장되는지는 5편에서 자세히 다룬다.
멀티 헤드 — 하나의 내적으로는 부족하다
위에서 "killer가 OOM에 58% 주목"이라고 했다. 이건 하나의 내적이 포착한 하나의 관계다. 그런데 "killer"와 "OOM" 사이에는 여러 종류의 관계가 동시에 존재한다.
관계 1: 의미적 관계 — "OOM killer"라는 복합어
관계 2: 문법적 관계 — 주어-동사 구조의 일부
관계 3: 인접 관계 — 바로 앞 토큰Q·K 내적 하나로는 이 세 관계 중 하나만 포착할 수 있다. 나머지는 놓친다. 그래서 헤드를 여러 개 만든다.
해결: 헤드를 여러 개 만든다.
3B 모델: 16개 헤드
7B 모델: 28개 헤드
각 헤드가 독립적인 W_Q, W_K, W_V를 갖는다.
→ 같은 입력을 받아도 서로 다른 관계를 학습하게 된다.
핵심: 왜 헤드마다 다른 관계를 포착하는가?
→ W_Q, W_K, W_V가 헤드마다 다른 파라미터이기 때문이다.
→ 학습 과정에서 "헤드 1은 문법을 잘 잡고, 헤드 2는 의미를 잘 잡으면
전체 손실이 줄어든다"는 방향으로 자연스럽게 역할이 분화된다.
→ 사람이 설계한 게 아니라 학습의 결과.구조를 그려보면 이렇다.
각 헤드의 차원 = 전체 차원 / 헤드 수
3B: 2048 / 16 = 128차원/헤드
입력 벡터 (2048차원)
│
├──→ 헤드 1 (128차원): Q₁·K₁→V₁ 가중합 "의미적 관계"
├──→ 헤드 2 (128차원): Q₂·K₂→V₂ 가중합 "문법적 관계"
├──→ 헤드 3 (128차원): Q₃·K₃→V₃ 가중합 "인접 관계"
├──→ ...
└──→ 헤드16 (128차원): Q₁₆·K₁₆→V₁₆ 가중합 "장거리 의존"
│
▼ 16개 출력을 이어 붙임 (concat)
[128 + 128 + ... + 128] = 2048차원
│
▼ × W_O (2048×2048) — 합산 행렬
최종 어텐션 출력 (2048차원)빠르기 때문에 멀티 헤드를 쓰는 게 아니다. 2048차원 싱글 헤드 1개와 128차원 16헤드의 계산량은 비슷하다. 멀티 헤드를 쓰는 이유는 하나의 내적으로는 하나의 관계만 포착할 수 있기 때문이다. 여러 헤드가 각각 다른 관계를 동시에 포착하고, W_O로 합쳐서 하나의 벡터로 만든다.
잔차 연결 — 원본을 보존한다
어텐션의 마지막 단계다. 어텐션이 만든 결과를 원래 벡터에 더한다.
어텐션 최종 출력 = 원래 벡터 + 어텐션 결과
원래 killer 벡터: [0.312, -0.721, 0.481, ...] (어텐션에 들어온 입력)
+ 어텐션이 만든 벡터: [-0.104, -0.124, 0.621, ...] (문맥 정보)
────────────────────────────────────────────────────
= 새 killer 벡터: [0.208, -0.845, 1.102, ...] (원본 + 문맥)왜 더하는가? 어텐션이 실수로 원본 정보를 날려버릴 수 있다. 더하면 최소한 원본은 보존된다. 36개 레이어를 거치면서 매번 "원본 + 이번 레이어에서 새로 얻은 정보"가 누적된다. 이 구조 덕분에 레이어를 깊게 쌓아도 학습이 안정적으로 된다.
어텐션 전체 흐름 요약
인풋: 토큰의 벡터 (2048차원)
│
│ ① 벡터에서 Q, K, V 생성 (×W_Q, ×W_K, ×W_V)
│
│ ② 새 토큰의 Q가 이전 토큰들의 K와 내적
│ → ÷√d로 스케일링
│ → 주목도 계산 (softmax)
│ → V의 가중합으로 새 벡터 생성
│ → 이걸 16개 헤드가 각각 독립적으로 수행
│
│ ③ 16개 헤드의 출력을 이어 붙이고 W_O로 합산
│
│ ④ 잔차 연결: 원래 벡터 + 어텐션 결과
▼
아웃풋: 같은 차원의 벡터 (2048차원, 이전 토큰들의 문맥이 반영된 상태)인풋과 아웃풋의 차원이 동일하다(2048). 이게 중요하다. 이 뒤에 FFN을 붙이고, 또 다음 레이어의 어텐션을 붙이고... 몇 개든 쌓을 수 있다는 뜻이다.
어텐션은 "토큰 간 관계를 파악"했다. 하지만 "이 관계가 무슨 의미인지"는 아직 모른다. "nginx가 OOM killer의 대상이다"까지는 파악했지만, "그래서 이게 서비스 장애라는 뜻이다"는 아직이다. 그 변환을 하는 게 다음 편의 FFN이다.
이 글이 어떠셨나요?
관련 포스트
1. LLM은 텍스트를 어떻게 처리하는가
LLM이 텍스트 한 줄을 받아서 다음 토큰을 예측하기까지의 전체 과정을 4단계로 따라간다. 토크나이저, 임베딩, 트랜스포머 레이어, 출력까지 "OOM killer가 nginx를" 한 문장이 모델 안에서 겪는 여행.
2026. 03. 03. 오후 10:00DevOps4. FFN: 지식이 저장된 곳
LLM의 FFN이 어텐션과 어떻게 역할을 나누는지. 확장→압축 구조가 왜 "지식 저장소"인지. 분산 표현의 원리, LayerNorm의 역할까지.
2026. 03. 24. 오후 10:00DevOps2. 입력 처리: 텍스트가 숫자가 되기까지
LLM에 텍스트가 들어가면 가장 먼저 일어나는 일. 토크나이저가 문자열을 정수로, 임베딩이 정수를 벡터로 변환하는 과정.
2026. 03. 10. 오후 10:00뉴스레터 구독
새 글이 올라오면 이메일로 알려드려요.