3장. Attention과 Encoder
√d_k 로 나누는 까닭
시험 점수를 합쳐 장학금을 나눈다고 해 봅시다. 과목이 4개일 때는 총점 차이가 몇 점이지만, 과목이 64개가 되면 총점 차이도 수십 점으로 벌어집니다. 그 총점을 그대로 "득표율"로 바꾸면 1등이 장학금을 거의 다 가져갑니다. 그래서 과목 수가 늘면 눈금을 줄여 비교합니다. 내적도 같습니다. 벡터 길이 d_k 가 커지면 더하는 칸이 많아져 점수가 커지고, softmax 가 한 낱말에게 거의 1, 나머지에게 거의 0 을 줍니다.
softmax 그림의 가운데 칸을 봅니다. 점수 차이 1.0 이 e^1 ≈ 2.7 배의 차이로 바뀝니다. 점수 차이가 2 면 e^2 ≈ 7.4 배, 4 면 e^4 ≈ 54.6 배입니다. 점수 차이가 조금만 벌어져도 비율 차이는 훨씬 크게 벌어진다는 뜻입니다. 그래서 점수 눈금을 1 안팎으로 맞춰 두어야 여러 낱말을 고루 참고할 수 있습니다.
나누기 전과 후
| 구분 | 점수 | softmax 비율 | 어떤 일이 생기나 |
|---|---|---|---|
| 나누기 전 (d_k = 16) | 8, 4, 0 | 0.982, 0.018, 0.000 | 첫 낱말이 거의 다 가져간다 |
| √16 = 4 로 나눈 뒤 | 2, 1, 0 | 0.665, 0.245, 0.090 | 셋을 모두 조금씩 참고한다 |
| 무엇이 나빠지나 | - | 거의 0 인 칸 | 그래디언트가 거의 0 이 되어 학습이 더뎌진다 |
| 무엇이 정하나 | - | - | 어디에 집중할지는 학습된 Q, K 가 정한다 |
나누기 전의 비율을 손으로 계산해 봅니다. e^8 ≈ 2981, e^4 ≈ 54.6, e^0 = 1 이고 합은 약 3036.6 입니다. 2981 ÷ 3036.6 ≈ 0.982 입니다. 나눈 뒤에는 e^2 ≈ 7.39, e^1 ≈ 2.72, e^0 = 1 의 합 11.11 로 나눠 0.665, 0.245, 0.090 입니다.
짧은 예시
import numpy as np
def softmax(x):
e = np.exp(x - x.max()) # 한 줄짜리 점수에 쓰는 softmax
return e / e.sum()
scores = np.array([8.0, 4.0, 0.0]) # d_k = 16 일 때 나왔다고 친 점수
d_k = 16
print(softmax(scores).round(3)) # 결과: [0.982 0.018 0. ]
print(softmax(scores / np.sqrt(d_k)).round(3)) # 결과: [0.665 0.245 0.09 ]
print(np.sqrt(64)) # 결과: 8.0 → 원 논문 d_k = 64 는 8 로 나눈다이 코드가 뜻하는 것
과목이 많아 총점이 크게 벌어진 상황에서, 눈금을 4 분의 1 로 줄였더니 장학금이 고루 나뉘는 모습입니다. 여기서 총점이 scores, 눈금 줄이기가 / np.sqrt(d_k), 장학금 비율이 softmax(...) 의 결과입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
softmax(scores) | 그대로 비율로 바꾼다 | 1등이 98% 를 가져간다 |
scores / np.sqrt(d_k) | √16 = 4 로 나눈다 | 눈금을 줄인다 |
np.sqrt(64) | 원 논문의 나누는 수 | d_k 64 → 8 로 나누기 |
차례(1등, 2등, 3등)는 그대로이고 비율의 쏠림만 줄었습니다. 나누기는 "누구를 볼지"를 바꾸지 않고 "얼마나 몰아줄지"만 누그러뜨립니다.
주의할 점
- 왜 하필 √d_k 일까요? 평균 0, 분산 1 인 숫자로 된 벡터 두 개를 내적하면 점수의 분산이 d_k 쯤 됩니다. 표준편차는 √d_k 이므로 √d_k 로 나누면 표준편차가 1 근처로 돌아옵니다. 실습 2에서 직접 확인합니다.
- 나누는 수가 너무 커도 문제입니다. 모든 점수가 0 에 가까워져 비율이 거의 같아지고, 어디에도 집중하지 못합니다.