트랜스포머의 셀프 어텐션 이해하기

상솜공방·2026년 5월 5일

논문 리뷰

목록 보기
56/58
post-thumbnail

텍스트 인풋을 받아 셀프 어텐션을 하는 트랜스포머 레이어의 파이프라인을 위와 같이 그려보았다. 셀프 어텐션의 원리는 한 번 이해해둬도 자주 까먹어서 이번 기회에 깊게 정리하여 포스팅한다.

1. 전체 그림의 의미

내가 그린 도표는 문장

“리오넬 메시가 득점했다.”\text{``리오넬 메시가 득점했다.''}

가 트랜스포머 내부에서 어떻게 숫자 행렬로 바뀌고, 그 행렬이 셀프 어텐션을 통해 서로 정보를 주고받는지를 나타낸 것이다.

이 과정을 한 줄로 요약하면 다음과 같다.

문장토큰보캡 인덱스원핫 벡터임베딩 벡터QKV어텐션 행렬새로운 문맥 벡터\text{문장} \rightarrow \text{토큰} \rightarrow \text{보캡 인덱스} \rightarrow \text{원핫 벡터} \rightarrow \text{임베딩 벡터} \rightarrow Q K V \rightarrow \text{어텐션 행렬} \rightarrow \text{새로운 문맥 벡터}

여기서 핵심은 “단어를 숫자로 바꾼다”가 아니다. 더 정확히는 “각 토큰을 벡터 공간의 한 점으로 바꾼 뒤, 문장 안의 다른 토큰들과의 관계를 계산하여 문맥이 반영된 새로운 벡터로 갱신한다”이다.

내가 설정한 예시에서는 토큰이 다음과 같이 나뉜다고 하자.

[리오넬, 메시, 가, 득점, 했, 다][\text{리오넬},\ \text{메시},\ \text{가},\ \text{득점},\ \text{했},\ \text{다}]

즉 문장 길이는

n=6n=6

이다. 그림에서도 세로 방향으로 6개의 토큰이 존재한다.

2. 토크나이징

2.1 토큰이란 무엇인가

언어 모델은 문장을 문자 그대로 이해하지 않는다. 먼저 문장을 더 작은 단위로 자른다. 이 단위를 토큰이라고 한다.

예를 들어

“리오넬 메시가 득점했다.”\text{``리오넬 메시가 득점했다.''}

를 다음처럼 자른다고 하자.

t1=리오넬t_1=\text{리오넬}
t2=메시t_2=\text{메시}
t3=t_3=\text{가}
t4=득점t_4=\text{득점}
t5=t_5=\text{했}
t6=t_6=\text{다}

여기서 “메시가”를 그대로 하나의 토큰으로 둘 수도 있고, “메시”와 “가”로 나눌 수도 있다. 실제 언어 모델에서는 BPE, WordPiece, SentencePiece 같은 서브워드 토크나이저를 사용하기 때문에 꼭 형태소 단위로 나뉘지는 않는다.

다만 내가 그린 그림에서는 학습을 위해 단순하게

[리오넬,메시,,득점,,][\text{리오넬}, \text{메시}, \text{가}, \text{득점}, \text{했}, \text{다}]

로 나눈다고 가정하였다.

2.2 왜 토큰화가 필요한가

컴퓨터는 “리오넬”이라는 문자열 자체를 바로 계산할 수 없다. 그래서 각 토큰을 정수 번호로 바꾼다. 이때 필요한 것이 보캡(Vocab)이다.

보캡은 모델이 알고 있는 토큰 목록이다. 보통 LLM의 보캡에는 수만개의 단어가 있지만, 설명을 간단히 하기 위해 우리가 가진 보캡은 다음과 같다고 가정한다.

0:[PAD]1:[UNK]2:3:리오넬4:5:득점6:메시7:호날두8:패스9:10:11:\begin{aligned} 0 &: \text{[PAD]} \\ 1 &: \text{[UNK]} \\ 2 &: \text{가} \\ 3 &: \text{리오넬} \\ 4 &: \text{했} \\ 5 &: \text{득점} \\ 6 &: \text{메시} \\ 7 &: \text{호날두} \\ 8 &: \text{패스} \\ 9 &: \text{다} \\ 10 &: \text{골} \\ 11 &: \text{을} \end{aligned}

그러면 예시 문장은 다음 정수열로 바뀐다.

[리오넬,메시,,득점,,][3,6,2,5,4,9][\text{리오넬},\text{메시},\text{가},\text{득점},\text{했},\text{다}] \rightarrow [3,6,2,5,4,9]

그림의 왼쪽 부분이 바로 이 과정이다.

3. 보캡 인덱스에서 원핫 벡터로

3.1 원핫 벡터의 정의

아까 보캡의 크기는 V=12|V|=12라고 가정했다. 그러면 각 토큰은 원-핫 벡터가 되기 위해 길이가 12인 벡터로 표현된다.

예를 들어 “리오넬”의 인덱스가 3이라면, 원핫 벡터는 3번 위치만 1이고 나머지는 모두 0인 벡터다.

o리오넬=[0,0,0,1,0,0,0,0,0,0,0,0]\mathbf{o}_{\text{리오넬}} = [0,0,0,1,0,0,0,0,0,0,0,0]

“메시”의 인덱스가 6이라면

o메시=[0,0,0,0,0,0,1,0,0,0,0,0]\mathbf{o}_{\text{메시}} = [0,0,0,0,0,0,1,0,0,0,0,0]

이다.

원핫 벡터의 핵심은 “그 토큰이 보캡에서 몇 번째 토큰인지”만 표시한다는 것이다. 원핫 벡터 자체에는 의미 정보가 거의 없다. “리오넬”과 “메시”가 축구 선수 이름으로 관련 있다는 정보는 원핫 벡터에 들어 있지 않다.

3.2 문장 전체의 원핫 행렬

문장 길이가 6이고 보캡 크기가 12라면, 문장 전체는 다음 크기의 원핫 행렬로 표현된다.

OR6×12O \in \mathbb{R}^{6 \times 12}

각 행은 하나의 토큰을 나타낸다.

O=[o리오넬o메시oo득점oo]O = \begin{bmatrix} \mathbf{o}*{\text{리오넬}} \\ \mathbf{o}*{\text{메시}} \\ \mathbf{o}*{\text{가}} \\ \mathbf{o}*{\text{득점}} \\ \mathbf{o}*{\text{했}} \\ \mathbf{o}*{\text{다}} \end{bmatrix}

Oij={1,if j=token id of ti0,otherwiseO_{i j} = \begin{cases} 1, & \text{if } j=\text{token id of }t_i \\ 0, & \text{otherwise} \end{cases}

이다.

그림에서 가운데의 0과 1로 된 큰 행렬이 바로 이 원핫 행렬이다.

4. 원핫 벡터에서 임베딩 벡터로

4.1 임베딩 테이블

보캡 크기가 50,000이면 원핫 벡터의 길이도 50,000이 된다. 그런데 모든 벡터는 그중 단 하나의 값만 1이고 나머지는 전부 0으로 너무 희소하여 컴퓨터가 이해하기 어렵다. 그래서 원핫 벡터를 더 풍부한 정보를 담을 수 있는 벡터에 매핑하는 과정이 필요하며, 이를 임베딩 벡터라고 한다.

그림과 같은 임베딩 테이블을

ERV×dE \in \mathbb{R}^{|V| \times d}

라고 하자.

여기서

V=12|V|=12

이고 그림에서는 임베딩 차원을

d=512d=512

로 두었다.

그러면

ER12×512E \in \mathbb{R}^{12 \times 512}

이다.

이 임베딩 테이블의 각 행은 하나의 토큰에 대응하는 512차원 벡터다.

예를 들어

E3E_3

은 “리오넬”의 임베딩 벡터이고,

E6E_6

은 “메시”의 임베딩 벡터다.

4.2 원핫 벡터와 임베딩 테이블의 곱

원핫 벡터를 임베딩 테이블에 곱하면 해당 토큰의 임베딩 벡터가 선택된다.

“리오넬”의 원핫 벡터를

o리오넬\mathbf{o}_{\text{리오넬}}

이라고 하면

x리오넬=o리오넬E\mathbf{x}_{\text{리오넬}} = \mathbf{o}_{\text{리오넬}} E

이다.

원핫 벡터는 3번 위치만 1이므로

o리오넬E=0E0+0E1+0E2+1E3+0E4++0E11\mathbf{o}_{\text{리오넬}} E = 0E_0+0E_1+0E_2+1E_3+0E_4+\cdots+0E_{11}

이다. 따라서

o리오넬E=E3\mathbf{o}_{\text{리오넬}} E = E_3

이다.

즉 원핫 벡터와 임베딩 테이블의 곱은 사실상 “임베딩 테이블에서 해당 행을 꺼내는 연산”이다.

문장 전체에 대해서는

X=OEX=OE

가 된다.

여기서 크기는 다음과 같다.

OR6×12O \in \mathbb{R}^{6 \times 12}
ER12×512E \in \mathbb{R}^{12 \times 512}

이므로

X=OER6×512X = OE \in \mathbb{R}^{6 \times 512}

이다.

즉 문장 안의 6개 토큰이 각각 512차원 임베딩 벡터로 바뀐다.

X=[x리오넬x메시xx득점xx]R6×512X = \begin{bmatrix} \mathbf{x}*{\text{리오넬}} \\ \mathbf{x}*{\text{메시}} \\ \mathbf{x}*{\text{가}} \\ \mathbf{x}*{\text{득점}} \\ \mathbf{x}*{\text{했}} \\ \mathbf{x}*{\text{다}} \end{bmatrix} \in \mathbb{R}^{6 \times 512}

그림에서 위쪽의 색깔 있는 가로줄 6개가 바로 이 임베딩 행렬이다. 각 줄은 하나의 토큰 벡터이며, 색상을 비교해보면 원핫 벡터에 의해 각각에 대응하는 임베딩 벡터만 인덱싱해오는 것을 알 수 있다.

5. 포지션 임베딩

여기서 중요한 문제가 하나 있다. 단순히 토큰 임베딩만 만들면 모델은 토큰의 순서를 알기 어렵다.

예를 들어 다음 두 문장을 보자.

리오넬 메시가 득점했다\text{리오넬 메시가 득점했다}
메시 리오넬이 득점했다\text{메시 리오넬이 득점했다}

토큰 집합만 보면 비슷한 단어들이 들어 있다. 하지만 순서가 다르다. 언어에서는 순서가 의미를 크게 바꾼다. 따라서 그림에는 생략되어있지만, 트랜스포머는 보통 위치 임베딩을 더한다.

H(0)=X+PH^{(0)} = X + P

여기서

PR6×512P \in \mathbb{R}^{6 \times 512}

는 위치 임베딩이다.

P1P_1

은 첫 번째 위치의 벡터,

P2P_2

는 두 번째 위치의 벡터다.

따라서 실제 입력은 보통

H(0)=[x리오넬+p1x메시+p2x+p3x득점+p4x+p5x+p6]H^{(0)} = \begin{bmatrix} \mathbf{x}_{\text{리오넬}}+\mathbf{p}*1 \\ \mathbf{x}*{\text{메시}}+\mathbf{p}*2 \\ \mathbf{x}*{\text{가}}+\mathbf{p}*3 \\ \mathbf{x}*{\text{득점}}+\mathbf{p}*4 \\ \mathbf{x}*{\text{했}}+\mathbf{p}*5 \\ \mathbf{x}*{\text{다}}+\mathbf{p}_6 \end{bmatrix}

처럼 된다.

그림에서는 간단히 XX라고 표시했지만, 실제 트랜스포머에서는 이 XX가 토큰 임베딩과 위치 임베딩이 합쳐진 입력이라고 생각하면 된다.

6. 셀프 어텐션의 핵심 아이디어

6.1 셀프 어텐션이 하는 일

셀프 어텐션은 문장 안의 각 토큰이 다른 토큰들을 얼마나 참고할지 계산하는 연산이다.

예시 문장에서

[리오넬, 메시, 가, 득점, 했, 다][\text{리오넬},\ \text{메시},\ \text{가},\ \text{득점},\ \text{했},\ \text{다}]

가 있을 때,

“리오넬”과 “메시”는 함께 하나의 이름을 이룬다. 따라서 “리오넬”과 “메시”는 의미적 연관성이 크고, “메시”와 “리오넬”도 의미적 연관성이 크다.

반대로 “가”, “득점”, “했”, “다”는 이 이름 구성 관계에서는 큰 관련이 없다. 물론 실제 언어 모델 전체에서는 “가”는 주격 조사이고 “득점했다”는 술어이므로 의미상 관련이 있다. 하지만 여기서는 특정 어텐션 헤드가 마치 '개체명인식(NER)'처럼 사람 이름 등의 “고유명사 결합”만 담당한다고 가정한다.

즉 이 헤드의 관심사는 다음과 같다.

리오넬메시\text{리오넬} \leftrightarrow \text{메시}

이 관계를 강하게 잡고, 나머지 토큰과의 관계는 약하게 둔다.

6.2 Query, Key, Value의 직관

셀프 어텐션에서는 각 토큰 벡터에서 세 종류의 벡터를 만든다.

Q=XWQQ = XW^Q
K=XWKK = XW^K
V=XWVV = XW^V

여기서 QQ, KK, VV는 각각 Query, Key, Value다.

직관적으로 말하면 다음과 같다.

Query는 “비교할 토큰”를 나타낸다.

Key는 “비교 될 토큰”를 나타낸다.

Value는 “Query와 Key의 유사도를 기준으로 각 토큰들의 정보가 융합된 결과”를 나타낸다.

이렇게 각 토큰 쌍 사이의 관련도를 계산하는 것이 셀프 어텐션의 시작이다.

7. QKVQ K V 행렬 만들기

7.1 입력 행렬의 크기

입력 행렬을

XRn×dX \in \mathbb{R}^{n \times d}

라고 하자.

이 예시에서는

n=6n=6
d=512d=512

이므로

XR6×512X \in \mathbb{R}^{6 \times 512}

이다.

각 행은 하나의 토큰 벡터다.

X=[x1x2x3x4x5x6]X = \begin{bmatrix} \mathbf{x}_1 \\ \mathbf{x}_2 \\ \mathbf{x}_3 \\ \mathbf{x}_4 \\ \mathbf{x}_5 \\ \mathbf{x}_6 \end{bmatrix}

여기서

x1=x리오넬\mathbf{x}*1 = \mathbf{x}*{\text{리오넬}}
x2=x메시\mathbf{x}*2 = \mathbf{x}*{\text{메시}}

이다.

7.2 학습 가능한 선형 변환

트랜스포머는 입력 벡터 XX를 그대로 비교하지 않는다. 대신 세 개의 학습 가능한 행렬을 사용한다.

WQRd×dkW^Q \in \mathbb{R}^{d \times d_k}
WKRd×dkW^K \in \mathbb{R}^{d \times d_k}
WVRd×dvW^V \in \mathbb{R}^{d \times d_v}

멀티헤드 어텐션에서는 보통 헤드마다 원래 XX의 차원 dd보다 더 작은 차원을 쓴다. 예를 들어 d=512d=512, 헤드 수가 8이면 한 헤드의 차원은 보통

dk=dv=64d_k=d_v=64

가 된다.

단일 헤드 기준으로는 다음과 같이 계산한다.

Q=XWQQ = XW^Q
K=XWKK = XW^K
V=XWVV = XW^V

크기를 확인하면

XR6×512X \in \mathbb{R}^{6 \times 512}
WQR512×dkW^Q \in \mathbb{R}^{512 \times d_k}

이므로

QR6×dkQ \in \mathbb{R}^{6 \times d_k}

이다.

마찬가지로

KR6×dkK \in \mathbb{R}^{6 \times d_k}
VR6×dvV \in \mathbb{R}^{6 \times d_v}

이다.

8. 어텐션 스코어 계산

8.1 토큰 하나와 토큰 하나의 관련도

이제 각 토큰이 다른 토큰을 얼마나 참고할지 계산한다.

토큰 ii의 Query를

qi\mathbf{q}_i

라고 하고, 토큰 jj의 Key를

kj\mathbf{k}_j

라고 하자.

그러면 토큰 ii가 토큰 jj를 얼마나 참고할지는 내적값으로 계산한다.

qikj\mathbf{q}_i \cdot \mathbf{k}_j

내적은 두 벡터가 비슷한 방향을 향할수록 커진다.

따라서

q리오넬k메시\mathbf{q}*{\text{리오넬}} \cdot \mathbf{k}*{\text{메시}}

가 크다면, “리오넬”은 “메시”를 중요하게 참고한다.

반대로

q리오넬k\mathbf{q}*{\text{리오넬}} \cdot \mathbf{k}*{\text{가}}

가 작다면, “리오넬”은 “가”를 별로 참고하지 않는다.

8.2 전체 토큰 쌍에 대한 행렬 계산

이러한 토큰과 토큰간의 코사인 유사도 계산을 한 번에 할 수 있는 것이 바로 행렬 곱이다. 모든 토큰 쌍에 대해 이 값을 계산하면 행렬이 된다.

QKTQK^T

여기서

QR6×dkQ \in \mathbb{R}^{6 \times d_k}
KR6×dkK \in \mathbb{R}^{6 \times d_k}

이므로

KTRdk×6K^T \in \mathbb{R}^{d_k \times 6}

이다.

따라서

QKTR6×6QK^T \in \mathbb{R}^{6 \times 6}

이다.

6×66 \times 6 행렬의 (ij)(i j)번째 원소는

(QKT)ij=qikj(QK^T)_{ij} = \mathbf{q}_i \cdot \mathbf{k}_j

이다.

ii는 “ii번째 토큰이 다른 토큰들을 각각 얼마씩 참조하는가”를 나타낸다.

jj는 “jj번째 토큰이 다른 토큰들에게 각각 얼마씩 참고되는가”를 나타낸다.

그림의 빨간색 히트맵 행렬이 바로 이 관계를 나타낸다.

9. 왜 dk\sqrt{d_k}로 나누는가

9.1 스케일링 없는 내적의 문제

셀프 어텐션 공식은 단순히

QKTQK^T

가 아니라

QKTdk\frac{QK^T}{\sqrt{d_k}}

를 사용한다.

왜 이렇게 나눌까?

qi\mathbf{q}_ikj\mathbf{k}_j의 각 성분이 평균 0, 분산 1인 값이라고 단순화해서 생각하자.

qi=[qi1qi2,qidk]\mathbf{q}*i = [q*{i1} q_{i2}, \dots q_{id_k}]
kj=[kj1kj2,kjdk]\mathbf{k}*j = [k*{j1} k_{j2}, \dots k_{jd_k}]

그러면 내적은

qikj==1dkqikj\mathbf{q}_i \cdot \mathbf{k}_j = \sum_{\ell=1}^{d_k} q_{i\ell}k_{j\ell}

이다.

각 항 qikjq_{i\ell}k_{j\ell}의 분산이 대략 1이라고 하면, dkd_k개를 더한 값의 분산은 대략

dkd_k

가 된다.

즉 내적값의 표준편차는

dk\sqrt{d_k}

정도로 커진다.

차원 dkd_k가 커질수록 내적값도 너무 커질 수 있다.

9.2 softmax 포화 문제

어텐션에서는 이 내적값에 softmax를 적용한다.

softmax는 입력값 중 큰 값을 더 크게 강조한다.

예를 들어

[1,2,3][1,2,3]

에 softmax를 적용하면 적당히 분산된 확률이 나오지만,

[10,20,30][10,20,30]

에 softmax를 적용하면 가장 큰 값에 거의 모든 확률이 몰린다.

그러면 학습 초기에 어텐션이 너무 극단적으로 변하고, gradient가 불안정해진다.

그래서 내적값을

dk\sqrt{d_k}

로 나누어 스케일을 안정화한다.

따라서 스코어 행렬은

S=QKTdkS = \frac{QK^T}{\sqrt{d_k}}

가 된다.

10. softmax로 어텐션 가중치 만들기

10.1 스코어 행렬에서 확률 행렬로 바꾸기

앞에서 QKT/dkQK^T/\sqrt{d_k}를 통해 스코어 행렬을 만들었다.

S=QKTdkS=\frac{QK^T}{\sqrt{d_k}}

여기서

SR6×6S \in \mathbb{R}^{6 \times 6}

이다.

행과 열의 순서는 모두 다음과 같다고 하자.

[리오넬, 메시, 가, 득점, 했, 다][\text{리오넬},\ \text{메시},\ \text{가},\ \text{득점},\ \text{했},\ \text{다}]

스코어 행렬 SS의 원소 SijS_{ij}는 “ii번째 토큰이 jj번째 토큰을 얼마나 참고하고 싶은가”를 나타낸다.

하지만 SijS_{ij} 자체는 아직 확률이 아니다. 값이 음수일 수도 있고, 행의 합이 1도 아니다. 왜냐하면 코사인 유사도의 값은 [1,1][-1, 1] 사이의 값을 가지기 때문이다. 따라서 각 행마다 softmax를 적용해서 확률분포로 바꿀 필요가 있다.

A=softmax(S)A=\text{softmax}(S)

더 정확히는 각 행 ii에 대해

Aij=exp(Sij)=16exp(Si)A_{ij} = \frac{\exp(S_{ij})}{\sum_{\ell=1}^{6}\exp(S_{i\ell})}

이다.

여기서 AijA_{ij}는 “ii번째 토큰이 자신의 새 벡터를 만들 때 jj번째 토큰의 정보를 어느 정도 비율로 가져오는가”를 의미한다.

각 행은 확률분포이므로 반드시 다음을 만족한다.

j=16Aij=1\sum_{j=1}^{6} A_{ij}=1

즉 하나의 토큰이 나누어 가지는 관심의 총합은 항상 1이다.

10.2 어텐션 시나리오

그림 속 어텐션 모듈은 문장을 입력받아 사람 이름, 지명, 고유명사 등의 존재만 추출하는 '개체명 인식' 태스크로 학습되었다고 가정하자. 그렇다면 “리오넬”은 자기 자신을 가장 많이 참조하고, 그다음으로 “메시”를 많이 참조할 것이다.

즉 “리오넬” 입장에서 중요한 순서는 다음과 같다.

리오넬>메시,득점,,\text{리오넬} > \text{메시} \gg \text{가},\text{득점},\text{했},\text{다}

이것은 다음과 같은 언어적 상황을 반영한다.

“리오넬”은 그 자체로 하나의 고유명사 일부다. 그런데 바로 뒤에 “메시”가 오기 때문에, 모델은 “리오넬”이 단독 이름이 아니라 “리오넬 메시”라는 하나의 인명 표현의 일부라는 사실을 알아야 한다. 따라서 “리오넬”은 자기 자신의 정보도 강하게 유지하지만, 동시에 “메시”도 꽤 강하게 참고한다.

이때 “리오넬” 행의 softmax 이전 스코어가 다음과 같다고 하자.

S리오넬,:=[2.30, 1.79, 0, 0, 0, 0]S_{\text{리오넬},:} = [2.30,\ 1.79,\ 0,\ 0,\ 0,\ 0]

그러면 softmax는 다음과 같이 계산된다.

먼저 각 원소에 지수함수를 적용한다.

exp(2.30)9.97\exp(2.30)\approx 9.97
exp(1.79)5.99\exp(1.79)\approx 5.99
exp(0)=1\exp(0)=1

따라서 분모는

9.97+5.99+1+1+1+1=19.969.97+5.99+1+1+1+1=19.96

이다.

이제 각 값을 분모로 나누면 된다.

A리오넬,리오넬=9.9719.960.50A_{\text{리오넬},\text{리오넬}} = \frac{9.97}{19.96} \approx 0.50
A리오넬,메시=5.9919.960.30A_{\text{리오넬},\text{메시}} = \frac{5.99}{19.96} \approx 0.30
A리오넬,=119.960.05A_{\text{리오넬},\text{가}} = \frac{1}{19.96} \approx 0.05
A리오넬,득점=119.960.05A_{\text{리오넬},\text{득점}} = \frac{1}{19.96} \approx 0.05
A리오넬,=119.960.05A_{\text{리오넬},\text{했}} = \frac{1}{19.96} \approx 0.05
A리오넬,=119.960.05A_{\text{리오넬},\text{다}} = \frac{1}{19.96} \approx 0.05

따라서

A리오넬,:[0.50, 0.30, 0.05, 0.05, 0.05, 0.05]A_{\text{리오넬},:} \approx [0.50,\ 0.30,\ 0.05,\ 0.05,\ 0.05,\ 0.05]

가 된다.

이 예시의 핵심은 softmax가 단순히 큰 값을 고르는 연산이 아니라, 상대적 크기를 확률분포로 바꾸는 연산이라는 점이다. “리오넬”에 대한 스코어가 가장 크기 때문에 0.50이 되고, “메시”에 대한 스코어가 그다음으로 크기 때문에 0.30이 된다. 나머지 토큰들은 스코어가 낮기 때문에 각각 0.05 정도만 받는다.

11. 어텐션 행렬

11.1 전체 어텐션 행렬 구성

위 과정을 모든 행에 대해 진행하면 아래와 같은 어텐션 행렬이 만들어진다.

A=[0.500.300.050.050.050.050.300.500.050.050.050.050.050.050.700.050.050.100.050.050.050.700.100.050.050.050.050.100.650.100.050.050.050.050.100.70]A = \begin{bmatrix} 0.50 & 0.30 & 0.05 & 0.05 & 0.05 & 0.05 \\ 0.30 & 0.50 & 0.05 & 0.05 & 0.05 & 0.05 \\ 0.05 & 0.05 & 0.70 & 0.05 & 0.05 & 0.10 \\ 0.05 & 0.05 & 0.05 & 0.70 & 0.10 & 0.05 \\ 0.05 & 0.05 & 0.05 & 0.10 & 0.65 & 0.10 \\ 0.05 & 0.05 & 0.05 & 0.05 & 0.10 & 0.70 \end{bmatrix}

행과 열의 순서는 모두 다음과 같다.

[리오넬, 메시, 가, 득점, 했, 다][\text{리오넬},\ \text{메시},\ \text{가},\ \text{득점},\ \text{했},\ \text{다}]

각 행의 합은 1이다.

첫 번째 행은 “리오넬”이 무엇을 보는지를 나타낸다.

[0.50, 0.30, 0.05, 0.05, 0.05, 0.05][0.50,\ 0.30,\ 0.05,\ 0.05,\ 0.05,\ 0.05]

두 번째 행은 “메시”가 무엇을 보는지를 나타낸다.

[0.30, 0.50, 0.05, 0.05, 0.05, 0.05][0.30,\ 0.50,\ 0.05,\ 0.05,\ 0.05,\ 0.05]

즉 “리오넬”과 “메시”는 서로를 비교적 강하게 본다. 다만 자기 자신을 가장 많이 본다. 즉, 히트맵을 보면 대각 성분이 가장 강하고, “리오넬”과 “메시” 사이의 비대각 성분도 어느 정도 강한 형태다.

11.2 “리오넬” 행의 해석

첫 번째 행을 따로 쓰면 다음과 같다.

A리오넬,:=[0.50, 0.30, 0.05, 0.05, 0.05, 0.05]A_{\text{리오넬},:} = [0.50,\ 0.30,\ 0.05,\ 0.05,\ 0.05,\ 0.05]

이것은 “리오넬”의 벡터를 업데이트할 때, 다른 토큰들의 정보를 어느정도 비율로 섞을지 나타낸다.

5050% \quad \text{리오넬 자기 자신의 정보}
3030% \quad \text{메시의 정보}
55% \quad \text{가의 정보}
55% \quad \text{득점의 정보}
55% \quad \text{했의 정보}
55% \quad \text{다의 정보}

따라서 “리오넬”은 여전히 자기 자신의 정체성을 절반 가까이 유지하며 강하게 유지한다. 하지만 동시에 “메시” 정보를 꽤 많이 받아온다.

이때 “메시” 정보가 들어오는 이유는 “리오넬”과 “메시”가 서로 결합하여 하나의 이름을 이루기 때문이다.

리오넬+메시리오넬 메시\text{리오넬} + \text{메시} \rightarrow \text{리오넬 메시}

따라서 “리오넬”의 출력 벡터는 단순한 “리오넬”이 아니라, “뒤에 메시가 붙어서 리오넬 메시라는 인명을 이루는 리오넬”에 가까워진다.

11.3 “메시” 행의 해석

두 번째 행은 다음과 같다.

A메시,:=[0.30, 0.50, 0.05, 0.05, 0.05, 0.05]A_{\text{메시},:} = [0.30,\ 0.50,\ 0.05,\ 0.05,\ 0.05,\ 0.05]

이것은 “메시”의 벡터를 업데이트할 때 다음 비율로 토큰들 간의 정보를 섞는다는 뜻이다.

3030% \quad \text{리오넬의 정보}
5050% \quad \text{메시 자기 자신의 정보}
55% \quad \text{가의 정보}
55% \quad \text{득점의 정보}
55% \quad \text{했의 정보}
55% \quad \text{다의 정보}

즉 “메시”도 자기 자신의 의미를 가장 크게 유지하지만, “리오넬”을 강하게 참고한다.

이렇게 되면 “메시”의 출력 벡터는 단순한 “메시”가 아니라, “앞에 리오넬이 붙어서 리오넬 메시라는 인명을 이루는 메시”에 가까워진다.

11.4 “가”, “득점”, “했”, “다”와의 관련성이 작은 이유

우리가 상정한 시나리오에서 이 모델은 고유명사들을 뽑아내는 태스크를 하므로, 문장의 모든 문법 관계를 보는 것이 아니라, “리오넬”과 “메시”가 하나의 이름이라는 관계를 주로 본다. 따라서 이 헤드에서 “리오넬”은 “가”, “득점”, “했”, “다”를 강하게 볼 필요가 없다.

A리오넬,=0.05A_{\text{리오넬},\text{가}}=0.05
A리오넬,득점=0.05A_{\text{리오넬},\text{득점}}=0.05
A리오넬,=0.05A_{\text{리오넬},\text{했}}=0.05
A리오넬,=0.05A_{\text{리오넬},\text{다}}=0.05

마찬가지로 “메시”도 이 헤드에서는 “가”, “득점”, “했”, “다”를 약하게 본다.

A메시,=0.05A_{\text{메시},\text{가}}=0.05
A메시,득점=0.05A_{\text{메시},\text{득점}}=0.05
A메시,=0.05A_{\text{메시},\text{했}}=0.05
A메시,=0.05A_{\text{메시},\text{다}}=0.05

물론 실제 언어적으로는 “메시가”에서 “가”는 주격 조사이고, “득점했다”는 술어이므로 중요하지만, 이런 요소들을 모두 반영하면 어텐션 매커니즘을 설명하기 어려워져 간단한 시나리오를 가정한다.

12. 어텐션 가중치로 Value 벡터 섞기

12.1 최종 어텐션 출력

softmax를 통해 어텐션 행렬 AA를 얻으면, 이제 이 AA를 Value 행렬 VV에 곱한다.

Z=AVZ=AV

여기서

AR6×6A \in \mathbb{R}^{6 \times 6}

이고

VR6×dvV \in \mathbb{R}^{6 \times d_v}

이므로

ZR6×dvZ \in \mathbb{R}^{6 \times d_v}

이다.

즉 입력 토큰이 6개였으므로 출력 벡터도 6개다. 다만 각 출력 벡터는 자기 자신만의 정보가 아니라, 다른 토큰들의 정보를 가중합한 결과다.

각 토큰 ii의 출력 벡터는 다음과 같다.

zi=j=16Aijvj\mathbf{z}_i = \sum_{j=1}^{6}A_{ij}\mathbf{v}_j

여기서 vj\mathbf{v}_jjj번째 토큰의 Value 벡터다.

12.2 “리오넬”의 출력 벡터

“리오넬”에 해당하는 출력 벡터는 다음과 같다.

z리오넬=A리오넬,리오넬v리오넬+A리오넬,메시v메시+A리오넬,v+A리오넬,득점v득점+A리오넬,v+A리오넬,v\mathbf{z}_{\text{리오넬}} = A_{\text{리오넬},\text{리오넬}}\mathbf{v}*{\text{리오넬}} + A*{\text{리오넬},\text{메시}}\mathbf{v}*{\text{메시}} + A*{\text{리오넬},\text{가}}\mathbf{v}*{\text{가}} + A*{\text{리오넬},\text{득점}}\mathbf{v}*{\text{득점}} + A*{\text{리오넬},\text{했}}\mathbf{v}*{\text{했}} + A*{\text{리오넬},\text{다}}\mathbf{v}_{\text{다}}

내 그림과 같은 가중치를 대입하면 다음과 같다.

z리오넬0.50v리오넬+0.30v메시+0.05v+0.05v득점+0.05v+0.05v\mathbf{z}*{\text{리오넬}} \approx 0.50\mathbf{v}*{\text{리오넬}} + 0.30\mathbf{v}*{\text{메시}} + 0.05\mathbf{v}*{\text{가}} + 0.05\mathbf{v}*{\text{득점}} + 0.05\mathbf{v}*{\text{했}} + 0.05\mathbf{v}_{\text{다}}

이 식은 매우 중요하다.

이전의 x리오넬\mathbf{x}*{\text{리오넬}}은 “리오넬”이라는 토큰 하나의 임베딩이었다. 하지만 z리오넬\mathbf{z}*{\text{리오넬}}은 다르다. 이 벡터는 “리오넬” 자기 자신의 정보를 50% 유지하면서, “메시”의 정보를 30% 받아온다.

따라서 이 출력 벡터는 “메시와 결합해 리오넬 메시라는 이름을 이루는 리오넬”이라는 문맥 정보를 포함하게 된다.

12.3 “메시”의 출력 벡터

마찬가지로 “메시”의 출력 벡터는 다음과 같다.

z메시=A메시,리오넬v리오넬+A메시,메시v메시+A메시,v+A메시,득점v득점+A메시,v+A메시,v\mathbf{z}_{\text{메시}} = A_{\text{메시},\text{리오넬}}\mathbf{v}*{\text{리오넬}} + A*{\text{메시},\text{메시}}\mathbf{v}*{\text{메시}} + A*{\text{메시},\text{가}}\mathbf{v}*{\text{가}} + A*{\text{메시},\text{득점}}\mathbf{v}*{\text{득점}} + A*{\text{메시},\text{했}}\mathbf{v}*{\text{했}} + A*{\text{메시},\text{다}}\mathbf{v}_{\text{다}}

가중치를 대입하면 다음과 같다.

z메시0.30v리오넬+0.50v메시+0.05v+0.05v득점+0.05v+0.05v\mathbf{z}*{\text{메시}} \approx 0.30\mathbf{v}*{\text{리오넬}} + 0.50\mathbf{v}*{\text{메시}} + 0.05\mathbf{v}*{\text{가}} + 0.05\mathbf{v}*{\text{득점}} + 0.05\mathbf{v}*{\text{했}} + 0.05\mathbf{v}_{\text{다}}

따라서 “메시”의 출력 벡터도 “리오넬” 정보를 포함한다.

즉 이 헤드에서는 다음과 같은 상호 참조가 일어난다.

리오넬메시 정보\text{리오넬} \leftarrow \text{메시 정보}
메시리오넬 정보\text{메시} \leftarrow \text{리오넬 정보}

그 결과 두 토큰은 서로의 정보를 나누어 가지면서 하나의 인명 표현으로 묶인다.

13. 이 어텐션 행렬이 의미하는 문맥 형성

13.1 초기 임베딩과 어텐션 이후 벡터의 차이

초기 임베딩 행렬 XX의 각 행은 문맥이 약한 토큰 표현이다.

X=[x리오넬x메시xx득점xx]X = \begin{bmatrix} \mathbf{x}*{\text{리오넬}} \\ \mathbf{x}*{\text{메시}} \\ \mathbf{x}*{\text{가}} \\ \mathbf{x}*{\text{득점}} \\ \mathbf{x}*{\text{했}} \\ \mathbf{x}*{\text{다}} \end{bmatrix}

이 단계에서 “리오넬”과 “메시”는 각각 별개의 토큰 벡터다.

하지만 어텐션 이후 출력은 다음과 같다.

Z=[z리오넬z메시zz득점zz]Z = \begin{bmatrix} \mathbf{z}*{\text{리오넬}} \\ \mathbf{z}*{\text{메시}} \\ \mathbf{z}*{\text{가}} \\ \mathbf{z}*{\text{득점}} \\ \mathbf{z}*{\text{했}} \\ \mathbf{z}*{\text{다}} \end{bmatrix}

여기서 z리오넬\mathbf{z}_{\text{리오넬}}에는 “메시” 정보가 들어 있다.

z리오넬0.50v리오넬+0.30v메시+\mathbf{z}*{\text{리오넬}} \approx 0.50\mathbf{v}*{\text{리오넬}} + 0.30\mathbf{v}_{\text{메시}} + \cdots

그리고 z메시\mathbf{z}_{\text{메시}}에는 “리오넬” 정보가 들어 있다.

z메시0.30v리오넬+0.50v메시+\mathbf{z}*{\text{메시}} \approx 0.30\mathbf{v}*{\text{리오넬}} + 0.50\mathbf{v}_{\text{메시}} + \cdots

따라서 어텐션 이후의 두 벡터는 서로 독립적이지 않다. 그림을 보면 맨 위의 두 파란색과 초록색으로 상이했던 벡터가 정보 융합을 통해 비슷한 색상의 분포를 가진 벡터로 바뀐 것을 알 수 있다.

13.2 “리오넬 메시”라는 하나의 의미 단위 형성

이 과정을 통해 모델 내부에서는 다음과 같은 변화가 생긴다.

처음에는

리오넬\text{리오넬}

메시\text{메시}

가 분리된 토큰이다.

하지만 어텐션 이후에는 두 토큰이 서로 정보를 공유한다.

결과적으로 모델은 두 토큰을 별개로만 처리하지 않고, “리오넬 메시”라는 하나의 이름 표현으로 해석할 준비를 하게 된다.

즉 셀프 어텐션은 단어를 단순히 읽는 것이 아니라, 문장 안에서 어떤 토큰들이 서로 어떻게 묶여야 하는지를 계산한다.

14. 전체 공식 다시 정리

셀프 어텐션 전체 과정은 다음 공식 하나로 요약된다.

Attention(QKV)=softmax(QKTdk)V\text{Attention}(Q K V) = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right)V

여기서

A=softmax(QKTdk)A = \text{softmax} \left( \frac{QK^T}{\sqrt{d_k}} \right)

라고 두면

Attention(QKV)=AV\text{Attention}(Q K V)=AV

이다.

그림의 핵심은 바로 이 두 단계다.

첫째, QKT/dkQK^T/\sqrt{d_k}를 통해 토큰 사이의 관련도를 계산한다.

둘째, softmax를 통해 관련도를 확률분포로 만든다.

셋째, 그 확률분포를 VV에 곱해서 각 토큰의 새 표현을 만든다.

예시 문장에서는

[리오넬, 메시, 가, 득점, 했, 다][\text{리오넬},\ \text{메시},\ \text{가},\ \text{득점},\ \text{했},\ \text{다}]

중에서 “리오넬”과 “메시”가 하나의 이름을 이루므로, 이 둘 사이의 어텐션 값이 다른 토큰들보다 크게 나온다.

먼저 자기 자신에 대한 가중치가 가장 크다.

A리오넬,리오넬=0.50A_{\text{리오넬},\text{리오넬}}=0.50

그다음으로 “메시”에 대한 가중치가 크다.

A리오넬,메시=0.30A_{\text{리오넬},\text{메시}}=0.30

나머지 토큰들은 이 헤드의 관심 대상이 아니므로 작다.

A리오넬,=A리오넬,득점=A리오넬,=A리오넬,=0.05A_{\text{리오넬},\text{가}} = A_{\text{리오넬},\text{득점}} = A_{\text{리오넬},\text{했}} = A_{\text{리오넬},\text{다}} = 0.05

따라서 이 헤드는 “리오넬”의 의미를 보존하면서도 “메시” 정보를 섞어, 두 토큰이 하나의 고유명사 표현으로 해석되도록 만든다.

이후 이 가중치가 VV에 곱해지면 “리오넬”의 출력 벡터는 다음과 같이 만들어진다.

z리오넬0.50v리오넬+0.30v메시+0.05v+0.05v득점+0.05v+0.05v\mathbf{z}*{\text{리오넬}} \approx 0.50\mathbf{v}*{\text{리오넬}} + 0.30\mathbf{v}*{\text{메시}} + 0.05\mathbf{v}*{\text{가}} + 0.05\mathbf{v}*{\text{득점}} + 0.05\mathbf{v}*{\text{했}} + 0.05\mathbf{v}_{\text{다}}

즉 어텐션은 단순히 “리오넬과 메시가 관련 있다”고 표시하는 데서 끝나지 않는다. 실제로 “메시”의 정보를 “리오넬” 벡터 안으로 섞어 넣는다.

마찬가지로 “메시”도 “리오넬” 정보를 받아온다.

z메시0.30v리오넬+0.50v메시+0.05v+0.05v득점+0.05v+0.05v\mathbf{z}*{\text{메시}} \approx 0.30\mathbf{v}*{\text{리오넬}} + 0.50\mathbf{v}*{\text{메시}} + 0.05\mathbf{v}*{\text{가}} + 0.05\mathbf{v}*{\text{득점}} + 0.05\mathbf{v}*{\text{했}} + 0.05\mathbf{v}_{\text{다}}

따라서 셀프 어텐션 이후 모델 내부에서는 “리오넬”과 “메시”가 서로의 정보를 나누어 가진다. 이 때문에 두 토큰은 점차 “리오넬 메시”라는 하나의 인명 표현으로 묶인다. 이것이 내가 그림에서 softmax 이후 빨간 행렬이 VV에 곱해지는 부분의 의미다.

profile
상어 인형을 좋아하는 사람

1개의 댓글

comment-user-thumbnail
2026년 5월 16일

siuuu

답글 달기