Transformer를 공부하다 보면 반드시 등장하는 개념이 바로 Self-Attention입니다.
처음 보면 Query, Key, Value, Softmax 같은 용어 때문에 어렵게 느껴질 수 있지만, 핵심 아이디어는 생각보다 단순합니다.
문장 속 각 단어가 다른 단어를 얼마나 참고해야 하는지 점수로 계산하는 것
이번 글에서는 다음 문장을 예시로 Self-Attention을 이해해보겠습니다.
나는 오늘 사과를 먹었다
문장을 단어 단위로 나누면 다음과 같습니다.
나는 / 오늘 / 사과를 / 먹었다
1. Self-Attention이란?
사람은 문장을 읽을 때 각 단어를 독립적으로 이해하지 않습니다.
예를 들어,
나는 오늘 사과를 먹었다
에서 먹었다라는 단어를 보면 자연스럽게 다음과 같은 생각을 합니다.
무엇을 먹었지?
→ 사과를
즉, 먹었다라는 단어의 의미를 이해하기 위해 사과를이라는 단어를 함께 참고합니다.
Self-Attention도 비슷합니다.
각 단어가 문장 속 다른 단어들을 보고,
이 단어가 나에게 얼마나 중요한가?
를 계산합니다.
2. Attention Matrix로 표현하기
Self-Attention 결과는 보통 다음과 같은 행렬로 표현할 수 있습니다.

| Query ↓/Key → | 나는 | 오늘 | 사과를 | 먹었다 |
|---|---|---|---|---|
| 나는 | 0.55 | 0.20 | 0.10 | 0.15 |
| 오늘 | 0.20 | 0.50 | 0.10 | 0.20 |
| 사과를 | 0.05 | 0.10 | 0.65 | 0.20 |
| 먹었다 | 0.10 | 0.10 | 0.35 | 0.45 |
여기서 중요한 것은 행(Row) 과 열(Column) 입니다.
행(Row) = Query
현재 보고 있는 단어
열(Column) = Key
현재 단어가 참고하는 대상
예를 들어 먹었다 행을 보면 다음과 같습니다.
나는 0.10
오늘 0.10
사과를 0.35
먹었다 0.45
즉,
먹었다 → 사과를 : 0.35
먹었다 → 먹었다 : 0.45
입니다.
먹었다가 자기 자신을 가장 많이 참고하면서도, 사과를도 상당히 많이 참고하고 있다는 뜻입니다.
이렇게 하면 모델은
먹었다 → 무엇을 먹었다?
→ 사과를 먹었다
라는 문맥 관계를 학습할 수 있습니다.
3. 대각선 값은 왜 1이 아닐까?
Self-Attention 행렬을 처음 보면 이런 의문이 생길 수 있습니다.
자기 자신을 보는 값이면
대각선은 1이어야 하는 것 아닌가?
하지만 그렇지 않습니다.
Self-Attention에서 대각선은 단순히
자기 자신을 얼마나 참고하는가
를 의미합니다.
예를 들어 다음과 같은 값이 있을 수 있습니다.
사과를 → 사과를 = 0.65
0.65라고 해서 이상한 것이 아닙니다.
오히려 Self-Attention에서는 다른 단어와의 관계도 같이 고려해야 하기 때문에 대각선이 반드시 1일 필요가 없습니다.
만약 다음과 같다면 어떨까요?
사과를 → 나는 0
사과를 → 오늘 0
사과를 → 사과를 1
사과를 → 먹었다 0
이 경우 사과를은 자기 자신만 보고 다른 단어는 전혀 참고하지 않습니다.
그렇다면 문맥을 이해하는 Self-Attention의 장점이 크게 줄어들게 됩니다.
4. 대신 각 행의 합은 1이 된다
Self-Attention에서 더 중요한 특징은 다음입니다.
Softmax를 적용한 뒤에는 각 행의 합이 1이 됩니다.
예를 들어 먹었다 행을 보겠습니다.
0.10 + 0.10 + 0.35 + 0.45
계산하면
1.00
입니다.
즉,
나는 10%
오늘 10%
사과를 35%
먹었다 45%
처럼 전체 관심도를 100%로 나눠 갖는다고 생각하면 이해하기 쉽습니다.
5. Self-Attention은 어떻게 계산될까?
실제 Transformer에서는 단어 임베딩으로부터 세 가지 벡터를 만듭니다.
Query
Key
Value
보통 다음과 같이 표현합니다.
Q = XWQ
K = XWK
V = XWV
여기서 X는 단어 임베딩이고,
WQ
WK
WV
는 학습되는 가중치입니다.
Attention Score는 다음과 같이 계산합니다.
Q × Kᵀ
그리고 값이 너무 커지는 것을 방지하기 위해 √dk로 나눕니다.
QKᵀ / √dk
그다음 Softmax를 적용합니다.
Attention = Softmax(QKᵀ / √dk)
마지막으로 Value와 곱합니다.
Output = Attention × V
6. PyTorch로 Self-Attention 직접 계산해보기
이번에는 실제 숫자를 사용해 Self-Attention을 계산해보겠습니다.
먼저 4개의 단어가 있다고 가정하겠습니다.
import torch
import torch.nn.functional as F
import math
words = ["나는", "오늘", "사과를", "먹었다"]
각 단어가 3차원 임베딩을 가진다고 가정하겠습니다.
X = torch.tensor([
[1.0, 0.0, 1.0], # 나는
[0.0, 1.0, 1.0], # 오늘
[1.0, 1.0, 0.0], # 사과를
[1.0, 1.0, 1.0] # 먹었다
])
print(X)
print(X.shape)
출력 형태는 다음과 같습니다.
torch.Size([4, 3])
즉,
4 = 단어 개수
3 = embedding 차원
입니다.
7. Query와 Key 만들기
설명을 쉽게 하기 위해 이번 예제에서는 별도의 가중치 행렬을 사용하지 않고,
Q = X
K = X
V = X
라고 가정하겠습니다.
Q = X
K = X
V = X
실제 Transformer에서는 각각 서로 다른 가중치가 곱해집니다.
Q = XWQ
K = XWK
V = XWV
8. Q와 K를 곱하기
Attention Score는 다음처럼 계산합니다.
scores = Q @ K.T
전체 코드는 다음과 같습니다.
scores = Q @ K.T
print(scores)
K.T는 Key 행렬을 transpose한 것입니다.
Shape을 보면,
Q : [4, 3]
K.T : [3, 4]
이므로 결과는
[4, 4]
가 됩니다.
즉,
4개의 단어 × 4개의 단어
관계가 만들어집니다.
9. √dk로 나누기
Transformer의 Scaled Dot-Product Attention에서는 다음처럼 계산합니다.
d_k = Q.size(-1)
scaled_scores = scores / math.sqrt(d_k)
여기서 d_k는 Query와 Key의 차원입니다.
현재는
d_k = 3
입니다.
10. Softmax 적용하기
이제 Softmax를 적용합니다.
attention = F.softmax(scaled_scores, dim=-1)
print(attention)
여기서
dim=-1
은 각 행을 기준으로 Softmax를 적용한다는 뜻입니다.
즉, 각 Query가 모든 Key에 대해 가지는 Attention을 계산합니다.
11. 각 행의 합 확인하기
Softmax를 적용했기 때문에 각 행의 합은 1이 됩니다.
print(attention.sum(dim=-1))
출력은 다음과 비슷합니다.
tensor([
1.0000,
1.0000,
1.0000,
1.0000
])
이 부분이 매우 중요합니다.
대각선 = 1
이 아니라,
각 행 전체의 합 = 1
입니다.
12. 단어별 Attention 출력하기
조금 더 보기 쉽게 출력해보겠습니다.
for i, query_word in enumerate(words):
print(f"\nQuery: {query_word}")
for j, key_word in enumerate(words):
print(
f"{query_word} -> {key_word}: "
f"{attention[i, j].item():.4f}"
)
결과는 다음과 같은 형태가 됩니다.
Query: 나는
나는 -> 나는: 0.xxxx
나는 -> 오늘: 0.xxxx
나는 -> 사과를: 0.xxxx
나는 -> 먹었다: 0.xxxx
각 행이 현재 단어인 Query이고,
각 열이 참고 대상인 Key입니다.
13. 최종 Self-Attention Output 계산하기
Attention Score를 계산하는 것만으로 끝나는 것은 아닙니다.
최종적으로 Attention Matrix와 Value를 곱합니다.
output = attention @ V
print(output)
즉,
Output = Attention × Value
입니다.
쉽게 생각하면 다음과 같습니다.
기존 먹었다 벡터에,
나는 정보
오늘 정보
사과를 정보
먹었다 정보
를 Attention 비율만큼 섞어서 새로운 먹었다 벡터를 만드는 것입니다.
이 과정을 통해 단어는 혼자 존재하는 것이 아니라 문맥 정보를 포함한 벡터로 변하게 됩니다.
14. 전체 코드
지금까지의 코드를 한 번에 정리하면 다음과 같습니다.
import torch
import torch.nn.functional as F
import math
words = ["나는", "오늘", "사과를", "먹었다"]
# 단어 Embedding이라고 가정
X = torch.tensor([ [1.0, 0.0, 1.0], # 나는
[0.0, 1.0, 1.0], # 오늘
[1.0, 1.0, 0.0], # 사과를
[1.0, 1.0, 1.0] # 먹었다
])
# 설명을 위해 Q, K, V를 동일하게 사용
Q = X
K = X
V = X
# 1. Attention Score
scores = Q @ K.T
print("Raw Attention Score")
print(scores)
# 2. Scaling
d_k = Q.size(-1)
print("Q size: ", Q.size() )
print("K size: ", K.size())
print("d_k: ", d_k)
scaled_scores = scores / math.sqrt(d_k)
print("\nScaled Attention Score")
print(scaled_scores)
# 3. Softmax
attention = F.softmax( scaled_scores, dim=-1 )
print("\nAttention Matrix")
print(attention)
# 4. 각 행의 합 확인
print("\nRow Sum")
print(attention.sum(dim=-1))
# 5. 단어별 Attention 확인
for i, query_word in enumerate(words):
print(f"\nQuery: {query_word}")
for j, key_word in enumerate(words):
print(
f"{query_word} -> {key_word}: "
f"{attention[i, j].item():.4f}"
)
# 6. 최종 Self-Attention Output
output = attention @ V
print("\nSelf-Attention Output")
print(output)실행 결과:


15. 핵심만 다시 정리하기
Self-Attention을 이해할 때 다음 네 가지를 기억하면 됩니다.
1. 행은 Query이다
현재 보고 있는 단어
입니다.
2. 열은 Key이다
현재 단어가 참고하는 대상
입니다.
3. 대각선이 1일 필요는 없다
대각선은 단순히
자기 자신에게 주는 Attention
을 의미합니다.
따라서
0.4
0.5
0.7
등 어떤 값도 나올 수 있습니다.
4. Softmax 이후 각 행의 합은 1이다
예를 들어,
먹었다
↓
나는 0.10
오늘 0.10
사과를 0.35
먹었다 0.45
라면,
0.10 + 0.10 + 0.35 + 0.45
= 1.00
입니다.
마무리
Self-Attention의 핵심은 복잡한 수식 자체가 아닙니다.
결국 모델이 하는 일은 다음과 같습니다.
현재 단어를 이해하기 위해
문장 속 다른 단어를 얼마나 참고할 것인가?
이를 숫자로 계산하는 것이 Self-Attention입니다.
특히 다음 관계를 기억하면 Self-Attention Matrix를 훨씬 쉽게 읽을 수 있습니다.
Row = Query
Column = Key
Query → Key
현재 단어 → 참고할 단어
그리고 가장 중요한 점은,
대각선 = 1
이 아니라
각 행의 합 = 1
이라는 것입니다.
이 개념을 이해하면 이후 배우게 되는
Q, K, V
Scaled Dot-Product Attention
Multi-Head Attention
Causal Mask
Transformer
도 훨씬 쉽게 이해할 수 있습니다.

