Field Log · Entry

Self-Attention 입문: Q·K·V와 Scaled Dot-Product 손으로 계산하기 (6/10)

질문 토큰의 query가 모든 token key와 점수를 계산하고 value를 가중합하는 self-attention

오늘의 결론

  • attention은 현재 token의 query와 모든 참고 대상의 key를 비교해 value를 얼마나 섞을지 정합니다.
  • Q·K·V는 별도 원문 데이터가 아니라 같은 hidden state를 서로 다른 관점으로 projection한 벡터입니다.
  • √dₖ로 score를 나누고 softmax를 적용해 가중치를 만듭니다.
  • self-attention은 주어진 context 안에서 정보를 섞습니다. 외부 corpus를 찾아오는 retrieval과는 다릅니다.

“Attention은 중요한 단어에 집중한다”는 설명은 출발점으로 좋지만 구현을 설명하기에는 부족합니다. 무엇이 중요하다고 판단하는지, 어떤 정보를 가져오는지, 왜 Q·K·V가 세 개인지 알 수 없기 때문입니다.

핵심은 주소를 찾는 점수가져올 내용을 분리하는 것입니다. query와 key는 어디를 볼지 정하고, value는 선택된 위치에서 실제로 섞을 정보를 제공합니다.

Self-attention의 Q K V 계산 흐름

그림 1. 한 token의 query가 모든 token의 key와 score를 만들고, softmax weight만큼 value를 가중합해 새 표현을 만든다.


검색 비유로 Q·K·V 잡기

간단한 key-value store를 떠올려 봅시다.

query: "E17 원인"
key:   각 문서가 어떤 내용을 담는지 나타내는 색인
value: 실제로 가져올 문서 내용

attention에서도 비슷하게:

  • Query(Q): 현재 위치가 지금 찾고 싶은 정보
  • Key(K): 각 위치가 자신을 매칭하기 위해 내놓는 표지
  • Value(V): 그 위치가 선택됐을 때 실제로 전달할 정보

하지만 attention은 vector DB 검색이 아닙니다. Q·K·V는 보통 현재 layer의 hidden states에서 선형변환으로 만들어지고, sequence 안의 위치들을 서로 섞습니다.

Q·K·V는 어디서 생기는가

token hidden states를 행렬 X라고 하겠습니다.

X shape = [sequence_length, d_model]

Q = X W_Q
K = X W_K
V = X W_V

W_Q, W_K, W_V는 학습되는 parameter입니다. 같은 token 표현 X가 세 projection을 거치며 “무엇을 찾는가”, “어떻게 매칭되는가”, “무엇을 전달하는가”라는 다른 역할을 가집니다.

Q, K, V를 사람이 직접 만든 질문·키워드·문서라고 생각하면 안 됩니다. layer 내부의 연속 벡터이고 학습 중 task에 유용한 방식으로 조정됩니다.

Scaled dot-product attention 공식

한 번에 쓰면 다음 공식입니다.

Attention(Q, K, V)
= softmax(QKᵀ / √dₖ) V

왼쪽부터 순서대로 읽으면 됩니다.

  1. QKᵀ: 모든 query-key 쌍의 dot-product score
  2. / √dₖ: dimension에 따른 score 크기 조절
  3. softmax: 각 query 행을 합 1인 weight로 변환
  4. × V: weight만큼 value를 섞음

한 query를 손으로 계산하기

현재 token의 query가 q = [1, 1]이고, 세 token의 key와 value가 다음과 같다고 합시다.

k₁ = [1, 0]    v₁ = [1, 0]   # E17
k₂ = [1, 1]    v₂ = [0, 2]   # 냉각수
k₃ = [0, 1]    v₃ = [1, 1]   # 매뉴얼

1. Query와 key score

q · k₁ = 1
q · k₂ = 2
q · k₃ = 1

scores = [1, 2, 1]

2. Scale

key dimension dₖ = 2이므로 √2로 나눕니다.

scaled scores ≈ [0.707, 1.414, 0.707]

3. Softmax

weights ≈ [0.248, 0.503, 0.248]

현재 query는 두 번째 token에 약 50.3%, 첫 번째와 세 번째에 각각 약 24.8%의 weight를 줍니다.

4. Value의 weighted sum

output
= 0.248[1,0] + 0.503[0,2] + 0.248[1,1]
≈ [0.496, 1.254]

attention output은 가장 높은 value 하나를 복사하는 것이 아니라, 모든 value를 weight에 따라 섞은 새 벡터입니다.

왜 √dₖ로 나누는가

dimension이 커지면 독립적인 성분들의 dot product 크기도 커지는 경향이 있습니다. 큰 score가 softmax에 들어가면 분포가 지나치게 뾰족해지고 gradient가 작아질 수 있습니다.

√dₖ scaling은 score의 분산을 안정적인 범위로 맞춰 softmax가 학습하기 좋은 영역에 머물도록 돕습니다. 단순한 장식 상수가 아니라 수치 안정성을 위한 설계입니다.

Matrix shape로 확인하기

sequence length를 n, key dimension을 dₖ, value dimension을 dᵥ라고 하면:

행렬shape의미
Q[n, dₖ]각 위치의 query
K[n, dₖ]각 위치의 key
QKᵀ[n, n]모든 위치 쌍 score
V[n, dᵥ]전달할 value
Attention output[n, dᵥ]각 위치의 새 표현

[n, n] score matrix 때문에 표준 self-attention의 계산량과 메모리 사용은 sequence 길이에 대해 대략 제곱으로 증가합니다. context가 두 배라고 단순히 비용도 두 배라고 생각하면 안 되는 이유입니다.

Mask는 softmax 전에 적용한다

decoder의 causal attention은 미래 위치의 score에 매우 큰 음수, 개념적으로 -∞를 넣습니다.

scores before mask = [1.2, 0.7, 2.1, 1.4]
mask                = [  ○,   ○,   ×,   ×]
masked scores       = [1.2, 0.7, -∞, -∞]
softmax             = [0.62, 0.38, 0, 0]

softmax 뒤에 weight를 0으로 만드는 것보다, softmax 전에 mask해야 허용된 위치끼리 다시 합이 1이 됩니다. padding mask도 같은 원리로 유효하지 않은 위치를 제외합니다.

Self-attention과 cross-attention

Q·K·V가 어디서 왔는지에 따라 이름이 달라집니다.

종류Q 출처K·V 출처
self-attention같은 sequence같은 sequence문장 안 token끼리 문맥화
cross-attentiondecoder stateencoder output번역 decoder가 원문 참고

decoder-only LLM의 기본 블록은 causal self-attention을 사용합니다. encoder-decoder Transformer는 decoder가 encoder output을 읽는 cross-attention을 추가로 가집니다.

Multi-head는 왜 필요한가

attention head 하나는 하나의 Q·K·V projection 공간에서 정보를 섞습니다. multi-head attention은 여러 작은 head를 병렬로 계산한 뒤 연결합니다.

head₁ = Attention(Q₁, K₁, V₁)
head₂ = Attention(Q₂, K₂, V₂)
...
output = Concat(head₁, head₂, ...) W_O

각 head가 문법 관계, 멀리 떨어진 참조, 특정 패턴 등 서로 다른 관계를 학습할 여지를 줍니다. 다만 “7번 head는 항상 부정어 담당”처럼 head의 의미가 고정되었다고 단정해서는 안 됩니다.

Attention weight는 설명 가능성인가

attention weight를 시각화하면 모델이 어디를 참고했는지 힌트를 얻을 수 있습니다. 하지만 weight 하나만으로 모델의 최종 판단 원인을 완전히 설명한다고 볼 수는 없습니다.

  • layer와 head가 매우 많음
  • residual path로 원래 정보도 계속 흐름
  • value와 FFN에서 추가 변환
  • 높은 weight와 output 영향도가 항상 같지 않음

따라서 attention map은 디버깅 단서이지 완전한 causal explanation이 아닙니다.

Attention과 retrieval을 구분하자

두 방식 모두 “관련 정보를 찾는다”는 표현을 쓰지만 범위가 다릅니다.

Retrieval:
외부 corpus 수백만 문서 → top-k 후보를 context 안으로 가져옴

Attention:
이미 context 안에 들어온 token들 → layer마다 정보를 가중 혼합

retriever가 정답 문서를 context에 넣지 못하면 attention은 그 문서를 참고할 수 없습니다. 반대로 검색 문서를 너무 많이 넣으면 attention이 중요한 근거를 안정적으로 활용한다는 보장도 없습니다. RAG는 무엇을 context에 넣을지context 안에서 모델이 어떻게 처리할지가 이어진 시스템입니다.

10분 실습: NumPy로 attention 한 번 계산하기

import numpy as np

q = np.array([[1.0, 1.0]])  # [1, d_k]
k = np.array([
    [1.0, 0.0],
    [1.0, 1.0],
    [0.0, 1.0],
])
v = np.array([
    [1.0, 0.0],
    [0.0, 2.0],
    [1.0, 1.0],
])

scores = q @ k.T / np.sqrt(k.shape[-1])
weights = np.exp(scores - scores.max(axis=-1, keepdims=True))
weights = weights / weights.sum(axis=-1, keepdims=True)
output = weights @ v

print("scores :", scores)
print("weights:", weights)
print("output :", output)

k₂v₂의 값을 바꿔 보세요. key를 바꾸면 얼마나 선택되는지, value를 바꾸면 선택 후 어떤 정보가 전달되는지가 달라지는 것을 볼 수 있습니다.

자주 하는 오해

“Q는 사용자의 자연어 질문이다.”

일반적인 self-attention의 Q는 각 token hidden state의 projection입니다. 사용자 질문 전체와 같은 개념이 아닙니다.

“attention은 가장 중요한 token 하나를 고른다.”

softmax weight로 value들을 가중합합니다. 분포가 뾰족할 수는 있지만 기본 연산은 soft selection입니다.

“context window가 크면 모든 정보를 똑같이 잘 쓴다.”

수용 가능한 길이와 실제 활용 품질은 다릅니다. 위치, 관련 없는 내용, 학습 분포와 모델 구조가 영향을 줍니다.

스스로 확인하기

  1. query-key score와 value가 역할상 왜 분리되어 있는가?
  2. QKᵀ의 shape가 왜 [n, n]인가?
  3. causal mask는 softmax 전과 후 중 언제 적용해야 하는가?
  4. retrieval과 attention이 각각 찾는 범위는 어디인가?

다음 글에서는 attention만으로는 설명되지 않는 Transformer 전체 블록을 residual, normalization, FFN과 함께 조립합니다.

참고자료