의미 있는 Embedding과 PyTorch 코드로 Self-Attention 이해하기
Transformer를 처음 공부하면 거의 반드시 이런 의문이 생깁니다.
“Q, K, V는 도대체 어디에서 만들어지고, 왜 굳이 세 개로 나누는 걸까?”
많은 자료에서는 다음 공식부터 보여줍니다.
Q = XWQ
K = XWK
V = XWV
수식 자체는 어렵지 않습니다.
하지만 처음 공부하는 입장에서는 이런 의문이 남습니다.
“그래서 왜 Q와 K를 곱하는 거지?”
“WQ, WK, WV는 각각 무슨 일을 하는 거지?”
“결국 이 계산으로 Transformer가 무엇을 알아내는 거지?”
특히 Embedding과 가중치 행렬을 전부 랜덤 값으로 만들어 계산하면 숫자가 나오는 과정은 확인할 수 있지만, 왜 특정 단어의 Attention Score가 높은지 직관적으로 이해하기 어렵습니다.
그래서 이번에는 조금 다르게 접근해 보겠습니다.
Embedding의 각 차원에 사람이 이해할 수 있는 의미를 부여하고, Q, K, V가 각각 어떤 역할을 하는지 PyTorch 코드로 직접 확인해 보겠습니다.
1. 예제 문장
이번에 사용할 문장은 다음과 같습니다.
The student studies deep learning carefully.
토큰을 단순화하면 다음과 같습니다.
tokens = [
"The",
"student",
"studies",
"deep learning",
"carefully"
]
이 문장이 좋은 이유는 단어 사이의 의미 관계가 매우 명확하기 때문입니다.
student
│
│ 누가?
▼
studies
│
│ 무엇을?
▼
deep learning
│
│ 어떻게?
▼
carefully
studies라는 단어를 중심으로 생각해 보면 문장을 이해하기 위해 필요한 정보가 자연스럽게 나타납니다.
- student → 누가 공부하는가?
- deep learning → 무엇을 공부하는가?
- carefully → 어떻게 공부하는가?
따라서 Self-Attention이 제대로 문맥 관계를 표현한다면 studies는 이 단어들을 참고하는 형태로 나타날 수 있습니다.
student
↑
│
│
deep learning ← studies → carefully
이 관계를 실제 Q, K, V 계산으로 만들어 보겠습니다.
2. 먼저 Embedding에 의미를 부여해 보자
일반적인 예제에서는 다음처럼 랜덤한 Embedding을 사용하는 경우가 많습니다.
student = [0.82, -0.31, 0.14, 0.73]
studies = [-0.17, 0.91, 0.42, -0.26]
...
물론 실제 모델에서는 이런 고차원 벡터를 사용합니다.
하지만 처음 Attention을 공부하는 단계에서는 숫자 하나하나가 무엇을 의미하는지 알 수 없기 때문에 직관적으로 이해하기 어렵습니다.
이번 예제에서는 4차원 Embedding의 각 차원에 의미를 직접 부여해 보겠습니다.
| Dimension | 의미 |
|---|---|
| 1 | Person |
| 2 | Action |
| 3 | Knowledge |
| 4 | Method |
그러면 각 단어를 다음처럼 표현할 수 있습니다.
The
[0, 0, 0, 0]
student
[1, 0, 0, 0]
studies
[0, 1, 0, 0]
deep learning
[0, 0, 1, 0]
carefully
[0, 0, 0, 1]
PyTorch에서는 다음과 같이 작성할 수 있습니다.
import torch
import torch.nn.functional as F
import math
tokens = [
"The",
"student",
"studies",
"deep learning",
"carefully"
]
X = torch.tensor([
[0., 0., 0., 0.], # The
[1., 0., 0., 0.], # student
[0., 1., 0., 0.], # studies
[0., 0., 1., 0.], # deep learning
[0., 0., 0., 1.] # carefully
])
이제 숫자가 훨씬 직관적으로 보입니다.
student → Person
studies → Action
deep learning → Knowledge
carefully → Method
즉, 이번 예제에서 Embedding은 단순한 숫자 배열이 아니라 각 토큰이 어떤 의미적 특징을 가지고 있는지 나타내는 벡터라고 생각할 수 있습니다.
실제 Transformer에서는 Embedding의 각 차원에 사람이 직접
Person,Action,Knowledge와 같은 의미를 지정하지 않습니다. 여기서는 Q, K, V의 역할을 쉽게 이해하기 위한 교육용 예제로 의미를 단순화한 것입니다.
3. 같은 Embedding에서 Q, K, V가 만들어진다
Self-Attention에서는 동일한 입력 X를 세 개의 서로 다른 가중치 행렬에 통과시킵니다.
Q = X @ WQ
K = X @ WK
V = X @ WV
그림으로 표현하면 다음과 같습니다.

여기서 중요한 점은
WQ ≠ WK ≠ WV
라는 것입니다.
따라서 같은 student Embedding이라도 서로 다른 변환을 거치면서 Q, K, V에서는 다른 역할을 할 수 있습니다.
그렇다면 세 가지 역할은 무엇일까요?
4. Query(Q): 나는 어떤 정보를 찾고 싶은가?
Query는 쉽게 말하면
“현재 토큰이 다른 토큰으로부터 어떤 정보를 찾고 싶은가?”
를 나타내는 벡터라고 볼 수 있습니다.
현재 토큰을 studies라고 해보겠습니다.
The student [studies] deep learning carefully.
studies가 문맥을 풍부하게 이해하려면 어떤 정보가 필요할까요?
누가 공부하는가? → Person
무엇을 공부하는가? → Knowledge
어떻게 공부하는가? → Method
따라서 교육용 예제에서는 studies의 Query가 다음 특징들을 찾도록 설계할 수 있습니다.
WQ = torch.tensor([
[1.000, 0.000, 0.000, 0.000],
[4.605, 1.833, 6.628, 4.394],
[0.000, 0.000, 1.000, 0.000],
[0.000, 0.000, 0.000, 1.000]
])
studies의 Embedding은
[0, 1, 0, 0]
이므로
Q = X @ WQ
를 계산하면 studies의 Query는 다음과 같습니다.
Q(studies)
[4.605, 1.833, 6.628, 4.394]
각 차원을 의미적으로 해석해 보면 다음과 같습니다.
| Dimension | 의미 | 값 |
|---|---|---|
| Person | 누가 공부하는가 | 4.605 |
| Action | 현재 행동 | 1.833 |
| Knowledge | 무엇을 공부하는가 | 6.628 |
| Method | 어떻게 공부하는가 | 4.394 |
즉, 이 예제에서 studies는 특히 Knowledge 정보를 가장 강하게 찾도록 설계되어 있습니다.
5. Key(K): 나는 어떤 정보를 제공할 수 있는가?
Query가
“내가 찾고 싶은 정보”
라면 Key는
“각 토큰이 어떤 정보를 제공할 수 있는가”
라고 생각할 수 있습니다.
이번 예제에서는 이해하기 쉽게 WK를 Identity Matrix로 설정하겠습니다.
WK = torch.eye(4)
그러면
K = X @ WK
이므로 Key는 원래 의미적 특징을 그대로 유지합니다.
K(student)
=
[1, 0, 0, 0]
→ Person 정보를 제공
K(studies)
=
[0, 1, 0, 0]
→ Action 정보를 제공
K(deep learning)
=
[0, 0, 1, 0]
→ Knowledge 정보를 제공
K(carefully)
=
[0, 0, 0, 1]
→ Method 정보를 제공
이제 Query와 Key의 역할 차이가 보이기 시작합니다.
Q(studies)
"나는 Person, Knowledge, Method 정보가 필요해."
K(student)
"나는 Person 정보를 가지고 있어."
K(deep learning)
"나는 Knowledge 정보를 가지고 있어."
K(carefully)
"나는 Method 정보를 가지고 있어."
이제 Transformer가 해야 할 일은 간단합니다.
Query와 Key를 비교하면 됩니다.
6. Q와 K를 비교한다
Attention Score는 기본적으로 Query와 Key의 내적을 이용하여 계산합니다.
raw_scores = Q @ K.T
studies만 생각해 보면 다음 비교가 이루어집니다.
Q(studies) · K(The)
Q(studies) · K(student)
Q(studies) · K(studies)
Q(studies) · K(deep learning)
Q(studies) · K(carefully)
현재
Q(studies)
[4.605, 1.833, 6.628, 4.394]
입니다.
그리고
K(student)
[1, 0, 0, 0]
이므로 내적하면
4.605
가 됩니다.
deep learning은
K(deep learning)
[0, 0, 1, 0]
이므로
6.628
이 됩니다.
carefully는
K(carefully)
[0, 0, 0, 1]
이므로
4.394
가 됩니다.
즉,

studies가 가장 강하게 원하는 정보와 deep learning이 제공하는 정보가 가장 잘 맞기 때문에 높은 점수가 나오게 됩니다.
7. 왜 √dₖ로 나눌까?
실제 Transformer의 Attention에서는 단순히 QKᵀ만 계산하지 않습니다.
다음과 같이 계산합니다.
d_k = K.shape[-1]
scaled_scores = (Q @ K.T) / math.sqrt(d_k)
수식으로 표현하면
QKᵀ
────
√dₖ
입니다.
이번 예제에서는 차원이 4이므로
√4 = 2
가 됩니다.
따라서 점수를 2로 나눕니다.
왜 이렇게 할까요?
Embedding 차원이 커지면 Query와 Key의 내적값도 지나치게 커질 수 있습니다.
값이 너무 커지면 Softmax 결과가 특정 토큰에 지나치게 집중되고 Gradient가 불안정해질 수 있습니다.
따라서 √dₖ로 나누어 Attention Score의 크기를 조절합니다.
이것이 Transformer에서 사용하는 Scaled Dot-Product Attention입니다.
8. Softmax로 Attention Weight를 만든다
이제 Score에 Softmax를 적용합니다.
attention_weights = F.softmax(
scaled_scores,
dim=-1
)
그러면 studies에 대해 대략 다음과 같은 결과를 얻습니다.
| Token | Attention Weight |
|---|---|
| The | 0.02 |
| student | 0.20 |
| studies | 0.05 |
| deep learning | 0.55 |
| carefully | 0.18 |
이제 숫자의 의미가 상당히 직관적입니다.

studies가 문맥을 만들 때 가장 많이 참고하는 정보는
deep learning
입니다.
왜냐하면
“무엇을 공부하는가?”
라는 정보를 제공하기 때문입니다.
그다음에는
student
를 통해
“누가 공부하는가?”
라는 정보를 얻고,
carefully
를 통해
“어떻게 공부하는가?”
라는 정보를 얻습니다.
9. 그렇다면 V는 왜 필요할까?
여기까지 보면 한 가지 의문이 생길 수 있습니다.
“Q와 K로 어떤 단어가 중요한지 이미 알아냈는데 V는 왜 필요한가?”
여기서 Q와 K의 역할과 V의 역할을 구분해야 합니다.
Q와 K는
누구를 얼마나 참고할지 결정
하는 데 사용됩니다.
반면 V는
선택한 토큰으로부터 실제로 가져올 정보
입니다.
이번 예제에서는 다음과 같이 설정합니다.
WV = torch.tensor([
[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 3., 0.],
[0., 0., 0., 2.]
])
따라서
Person
→ 그대로 유지
Action
→ 그대로 유지
Knowledge
→ 3배 강조
Method
→ 2배 강조
가 됩니다.
예를 들어
deep learning
Embedding
[0, 0, 1, 0]
이 WV를 통과하면
V(deep learning)
[0, 0, 3, 0]
이 됩니다.
carefully는
V(carefully)
[0, 0, 0, 2]
가 됩니다.
즉, Attention Weight를 계산하기 위한 정보와 최종적으로 전달할 정보가 분리되어 있는 것입니다.
10. 마지막 단계: Value를 가중합한다
이제 Attention Weight를 V에 적용합니다.
output = attention_weights @ V
studies만 보면 개념적으로 다음과 같습니다.
Output(studies)
=
0.02 × V(The)
+
0.20 × V(student)
+
0.05 × V(studies)
+
0.55 × V(deep learning)
+
0.18 × V(carefully)
즉,
Attention Weight
×
Value
↓
Weighted Value
를 모두 더하는 것입니다.
그 결과 새로운 studies 벡터가 만들어집니다.
11. 이제 studies는 더 이상 혼자 있는 studies가 아니다
처음 studies의 Embedding은 단순했습니다.
studies
[0, 1, 0, 0]
↓
Action
즉, 단순히 행동을 나타내는 토큰이었습니다.
하지만 Self-Attention을 거치면서 상황이 달라집니다.

새로운 studies 벡터에는
누가 공부하는가
+
무엇을 공부하는가
+
어떻게 공부하는가
에 관한 정보가 함께 반영됩니다.
즉, Self-Attention의 핵심은 단어 하나를 독립적으로 보는 것이 아니라 주변 토큰과의 관계를 이용해 문맥이 반영된 새로운 표현을 만드는 것이라고 볼 수 있습니다.
12. 전체 과정을 한 번에 정리하면
전체 흐름을 다시 보면 다음과 같습니다.

Transformer의 유명한 Attention 공식도 결국 이 과정을 하나의 식으로 표현한 것입니다.
Attention(Q, K, V)
QKᵀ
= softmax( ───── ) V
√dₖ
처음 보면 복잡해 보이지만 역할을 나누어 보면 훨씬 간단합니다.
13. Q, K, V를 검색으로 비유하면
Q, K, V가 여전히 헷갈린다면 검색 엔진을 떠올려도 좋습니다.
Query
내가 찾고 싶은 정보
검색창에 입력하는 검색어와 비슷합니다.
Key
비교 대상이 제공할 수 있는 정보
어떤 문서가 내가 원하는 정보와 관련 있는지 판단하기 위한 단서라고 볼 수 있습니다.
Value
실제로 가져올 내용
관련성이 높은 대상을 찾았다면 그 대상으로부터 실제 정보를 가져와야 합니다.
따라서 아주 단순화하면 다음처럼 기억할 수 있습니다.
Q = 무엇을 찾을까?
K = 누가 그것을 가지고 있을까?
V = 그 사람이 실제로 가지고 있는 정보는 무엇일까?
그리고 Attention은
Q × K
를 이용해 누구를 참고할지 결정한 뒤
Attention Weight × V
를 통해 실제 정보를 가져옵니다.
14. 그렇다면 WQ, WK, WV는 누가 정할까?
여기서 매우 중요한 부분이 하나 남아 있습니다.
이번 예제에서는 이해를 돕기 위해 우리가 직접
WQ
WK
WV
를 설계했습니다.
하지만 실제 Transformer에서는 사람이 이렇게 지정하지 않습니다.
WQ, WK, WV는 모두 Learnable Parameters, 즉 학습 가능한 파라미터입니다.
PyTorch에서 실제로 구현한다면 보통 다음과 같은 형태입니다.
import torch.nn as nn
d_model = 512
Wq = nn.Linear(d_model, d_model, bias=False)
Wk = nn.Linear(d_model, d_model, bias=False)
Wv = nn.Linear(d_model, d_model, bias=False)
학습 과정에서
loss.backward()
optimizer.step()
가 반복되면서 WQ, WK, WV가 조금씩 업데이트됩니다.
즉, 사람이
student는 Person이다.
deep learning은 Knowledge다.
carefully는 Method다.
라고 직접 알려주는 것이 아닙니다.
모델이 수많은 데이터를 학습하면서 현재 목적에 유용한 관계를 포착할 수 있는 방향으로 가중치를 학습하는 것입니다.
15. 실제 Transformer에서는 조금 더 복잡하다
이번 예제에서는 이해를 위해 상당히 단순화했습니다.
실제 Transformer에서는 Embedding의 각 차원이
1번 = Person
2번 = Action
3번 = Knowledge
4번 = Method
처럼 사람이 읽을 수 있는 형태로 명확하게 나뉘어 있지 않습니다.
또한 Transformer가 실제로 머릿속에서
"studies니까 누가 공부하는지 찾아야겠다!"
라고 명시적으로 생각하는 것도 아닙니다.
대신 학습을 통해 특정 Attention Head가
- 주어와 동사의 관계
- 동사와 목적어의 관계
- 대명사와 선행사의 관계
- 가까운 단어와의 관계
- 위치적 관계
등 다양한 패턴을 포착할 수 있습니다.
따라서 이번 예제는
실제 Transformer 내부의 의미를 그대로 재현한 코드
라기보다는,
Q, K, V가 서로 다른 역할을 수행하는 이유를 직관적으로 보여주기 위한 교육용 모델
이라고 이해하는 것이 가장 정확합니다.
전체 코드:
import math
import torch
import torch.nn.functional as F
# ============================================================
# 0. 출력 형식 설정
# ============================================================
torch.set_printoptions(
precision=3,
sci_mode=False,
linewidth=120
)
# ============================================================
# 1. Sentence
# ============================================================
tokens = [
"The",
"student",
"studies",
"deep learning",
"carefully",
]
print("=" * 75)
print("Sentence")
print("=" * 75)
print("The student studies deep learning carefully.")
print("\nTokens")
print(tokens)
# ============================================================
# 2. Semantic Embedding
# ============================================================
#
# 각 차원에 명확한 의미를 부여합니다.
#
# Dimension 0: Person
# Dimension 1: Action
# Dimension 2: Knowledge
# Dimension 3: Method
#
# The -> 의미 역할 없음
# student -> Person
# studies -> Action
# deep learning -> Knowledge
# carefully -> Method
#
# 실제 Transformer의 임베딩 차원은 사람이 직접 의미를 정하지 않습니다.
# 학습 과정에서 모델이 유용한 특징을 스스로 형성합니다.
# 여기서는 Q, K, V의 원리를 쉽게 설명하기 위해 의미를 직접 부여합니다.
# ============================================================
semantic_dimensions = [
"Person",
"Action",
"Knowledge",
"Method",
]
X = torch.tensor([
[0.0, 0.0, 0.0, 0.0], # The
[1.0, 0.0, 0.0, 0.0], # student
[0.0, 1.0, 0.0, 0.0], # studies
[0.0, 0.0, 1.0, 0.0], # deep learning
[0.0, 0.0, 0.0, 1.0], # carefully
])
print("\n" + "=" * 75)
print("Semantic Embedding X")
print("=" * 75)
print(f"{'Token':16s}", end="")
for dimension in semantic_dimensions:
print(f"{dimension:12s}", end="")
print()
for token, vector in zip(tokens, X):
print(f"{token:16s}", end="")
for value in vector:
print(f"{value.item():<12.1f}", end="")
print()
# ============================================================
# 3. WQ: 현재 토큰이 무엇을 찾고 싶은가?
# ============================================================
#
# 핵심 아이디어:
#
# studies는 Action 토큰입니다.
#
# studies가 문맥을 이해하려면 다음 정보가 필요합니다.
#
# 1. 누가 공부하는가? -> Person
# 2. 자신은 어떤 행동인가? -> Action
# 3. 무엇을 공부하는가? -> Knowledge
# 4. 어떻게 공부하는가? -> Method
#
# 목표 Attention Weight:
#
# The 0.02
# student 0.20
# studies 0.05
# deep learning 0.55
# carefully 0.18
#
# Softmax 이전 logit을 역산하여 studies의 Query를 설계합니다.
#
# scaled_attention_score = raw_score / sqrt(d_k)
#
# d_k = 4이므로 sqrt(d_k) = 2입니다.
#
# target_weight에 log를 취한 뒤 기준값을 이동시키면:
#
# The 0.000
# student 2.303
# studies 0.916
# deep learning 3.314
# carefully 2.197
#
# 이를 scaling 이전 값으로 만들기 위해 2를 곱합니다.
#
# studies의 Query:
#
# [4.605, 1.833, 6.628, 4.394]
#
# 의미:
#
# Person 필요
# Action 조금 참고
# Knowledge 가장 중요
# Method 중요
# ============================================================
WQ = torch.tensor([
# Query output:
# Person Action Knowledge Method
[1.000, 0.000, 0.000, 0.000], # Person token의 Query
[4.605, 1.833, 6.628, 4.394], # Action token의 Query
[0.000, 0.000, 1.000, 0.000], # Knowledge token의 Query
[0.000, 0.000, 0.000, 1.000], # Method token의 Query
])
# ============================================================
# 4. WK: 각 토큰이 어떤 정보를 제공할 수 있는가?
# ============================================================
#
# student -> Person 정보를 제공
# studies -> Action 정보를 제공
# deep learning -> Knowledge 정보를 제공
# carefully -> Method 정보를 제공
#
# 여기서는 Key 공간을 쉽게 이해하기 위해 Identity Matrix를 사용합니다.
# ============================================================
WK = torch.eye(4)
# ============================================================
# 5. WV: 실제로 전달할 정보
# ============================================================
#
# Value는 Attention Score 계산에는 직접 사용되지 않습니다.
#
# Q와 K가 "누구를 얼마나 참고할지" 결정한 뒤,
# 실제로 가져오는 정보가 V입니다.
#
# 여기서는:
#
# Person -> 그대로 유지
# Action -> 그대로 유지
# Knowledge -> 3배 강조
# Method -> 2배 강조
# ============================================================
WV = torch.tensor([
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 3.0, 0.0],
[0.0, 0.0, 0.0, 2.0],
])
print("\n" + "=" * 75)
print("Learnable Projection Matrices")
print("=" * 75)
print("\nWQ: What information does the token want?")
print(WQ)
print("\nWK: What information can each token provide?")
print(WK)
print("\nWV: What information will actually be delivered?")
print(WV)
# ============================================================
# 6. Generate Q, K, V
# ============================================================
Q = X @ WQ
K = X @ WK
V = X @ WV
print("\n" + "=" * 75)
print("Q, K, V")
print("=" * 75)
print("\nQ = X @ WQ")
for token, vector in zip(tokens, Q):
print(f"{token:16s} {vector.numpy()}")
print("\nK = X @ WK")
for token, vector in zip(tokens, K):
print(f"{token:16s} {vector.numpy()}")
print("\nV = X @ WV")
for token, vector in zip(tokens, V):
print(f"{token:16s} {vector.numpy()}")
# ============================================================
# 7. Scaled Dot-Product Attention Score
# ============================================================
#
# Attention(Q, K, V)
#
# = softmax(QK^T / sqrt(d_k)) V
#
# QK^T:
# 각 Query가 모든 Key와 얼마나 잘 맞는지 계산합니다.
#
# sqrt(d_k):
# 차원이 커질수록 내적값이 지나치게 커지는 현상을 완화합니다.
# ============================================================
d_k = K.shape[-1]
raw_scores = Q @ K.T
scaled_scores = raw_scores / math.sqrt(d_k)
print("\n" + "=" * 75)
print("Raw Attention Scores: Q @ K.T")
print("=" * 75)
print(raw_scores)
print("\n" + "=" * 75)
print("Scaled Attention Scores: Q @ K.T / sqrt(d_k)")
print("=" * 75)
print(scaled_scores)
# ============================================================
# 8. Softmax
# ============================================================
#
# 각 행에 Softmax를 적용합니다.
#
# dim=-1:
# 하나의 Query가 모든 Key에 보내는 Attention Weight의 합이
# 1이 되도록 합니다.
# ============================================================
attention_weights = F.softmax(scaled_scores, dim=-1)
print("\n" + "=" * 75)
print("Attention Weights")
print("=" * 75)
print(f"{'Query':16s}", end="")
for token in tokens:
print(f"{token:16s}", end="")
print()
for query_token, row in zip(tokens, attention_weights):
print(f"{query_token:16s}", end="")
for weight in row:
print(f"{weight.item():<16.3f}", end="")
print()
# ============================================================
# 9. Contextual Output
# ============================================================
#
# Output = Attention Weight @ V
#
# 각 토큰의 새로운 표현은
# 모든 Value 벡터의 가중합입니다.
# ============================================================
output = attention_weights @ V
print("\n" + "=" * 75)
print("Contextual Output")
print("=" * 75)
for token, vector in zip(tokens, output):
print(f"{token:16s} {vector.numpy()}")
# ============================================================
# 10. studies 토큰 자세히 분석
# ============================================================
idx = tokens.index("studies")
print("\n")
print("=" * 75)
print("Detailed Analysis: studies")
print("=" * 75)
# ------------------------------------------------------------
# 10-1. Embedding
# ------------------------------------------------------------
print("\n1) Embedding of studies")
print("-" * 75)
print(X[idx])
print(
"\nstudies는 Action 차원이 활성화된 토큰입니다."
)
# ------------------------------------------------------------
# 10-2. Query
# ------------------------------------------------------------
print("\n2) Q(studies)")
print("-" * 75)
print(Q[idx])
print("\nInterpretation")
for dimension, value in zip(semantic_dimensions, Q[idx]):
print(f"{dimension:12s}: {value.item():.3f}")
print(
"\n즉, studies는 Knowledge 정보를 가장 강하게 찾고,\n"
"Person과 Method 정보도 함께 찾도록 설계되었습니다."
)
# ------------------------------------------------------------
# 10-3. Key
# ------------------------------------------------------------
print("\n3) Keys provided by all tokens")
print("-" * 75)
for token, key in zip(tokens, K):
print(f"{token:16s} {key.numpy()}")
# ------------------------------------------------------------
# 10-4. Dot Product
# ------------------------------------------------------------
print("\n4) Q(studies) · K(each token)")
print("-" * 75)
for token, key in zip(tokens, K):
raw_score = torch.dot(Q[idx], key)
scaled_score = raw_score / math.sqrt(d_k)
print(
f"{token:16s} "
f"raw score = {raw_score.item():6.3f} "
f"scaled score = {scaled_score.item():6.3f}"
)
# ------------------------------------------------------------
# 10-5. Attention Weight
# ------------------------------------------------------------
print("\n5) Attention Weight of studies")
print("-" * 75)
studies_attention = attention_weights[idx]
for token, weight in zip(tokens, studies_attention):
print(f"{token:16s} {weight.item():.3f}")
print(
f"\nSum of weights: {studies_attention.sum().item():.3f}"
)
# ------------------------------------------------------------
# 10-6. Weighted Value
# ------------------------------------------------------------
print("\n6) Weighted Values")
print("-" * 75)
weighted_values = studies_attention.unsqueeze(-1) * V
for token, weight, value, weighted_value in zip(
tokens,
studies_attention,
V,
weighted_values,
):
print(
f"{weight.item():.3f} × "
f"V({token:13s}) {value.numpy()} "
f"= {weighted_value.numpy()}"
)
# ------------------------------------------------------------
# 10-7. Final Output
# ------------------------------------------------------------
print("\n7) Final Output Vector of studies")
print("-" * 75)
print(output[idx])
print("\nDimension Interpretation")
for dimension, value in zip(semantic_dimensions, output[idx]):
print(f"{dimension:12s}: {value.item():.3f}")
# ============================================================
# 11. Human-readable Summary
# ============================================================
sorted_indices = torch.argsort(
studies_attention,
descending=True,
)
print("\n")
print("=" * 75)
print("What does 'studies' attend to?")
print("=" * 75)
for rank, token_idx in enumerate(sorted_indices, start=1):
token = tokens[token_idx]
weight = studies_attention[token_idx]
print(
f"{rank}. {token:16s} "
f"attention weight = {weight.item():.3f}"
)
print(
"\nConclusion:\n"
"'studies'는 무엇을 공부하는지 나타내는 'deep learning'을\n"
"가장 많이 참고합니다.\n\n"
"그다음으로 누가 공부하는지 알려주는 'student'와,\n"
"어떻게 공부하는지 알려주는 'carefully'를 참고합니다.\n\n"
"이 Value들을 가중합하여 문맥이 반영된 새로운\n"
"'studies' 벡터를 생성합니다."
)실행 결과:






마무리
이번 예제에서 가장 중요한 것은 복잡한 행렬 계산 자체가 아닙니다.
Q, K, V의 역할을 구분해서 이해하는 것입니다.
| 요소 | 역할 | 쉽게 말하면 |
|---|---|---|
| Embedding | 토큰의 기본 표현 | 나는 어떤 정보를 가지고 있는가 |
| Query | 찾고 싶은 정보 | 나는 무엇이 필요한가 |
| Key | 비교할 정보 | 나는 무엇을 제공할 수 있는가 |
| Value | 실제 전달할 정보 | 내가 실제로 전달할 내용 |
| Q × K | 관련성 계산 | 누구를 참고할까 |
| Softmax | 중요도 변환 | 얼마나 참고할까 |
| Weight × V | 정보 결합 | 실제 정보를 가져오자 |
따라서 Self-Attention을 한 문장으로 정리하면 다음과 같습니다.
Query와 Key를 비교하여 어떤 토큰을 얼마나 참고할지 결정하고, 그 중요도에 따라 Value를 가중합하여 문맥이 반영된 새로운 표현을 만드는 과정입니다.
예제 문장으로 다시 생각해 보면 더욱 간단합니다.
The student studies deep learning carefully.
studies라는 하나의 토큰이 Self-Attention을 거치면서
student
→ 누가?
deep learning
→ 무엇을?
carefully
→ 어떻게?
라는 주변 정보를 참고하게 됩니다.
그리고 그 정보들이 하나의 새로운 벡터에 결합됩니다.
studies
단순한 Action Embedding
↓
Self-Attention
↓
누가 + 무엇을 + 어떻게
↓
문맥이 반영된 studies 표현
바로 이 변화가 Self-Attention을 이해할 때 가장 중요한 포인트입니다.

