Self-Attention 쉽게 이해하기
지난 장에서 우리는 Attention이 무엇을 보는지 살펴보았습니다. 한 토큰의 표현을 만들 때 문장 안의 다른 토큰들을 얼마나 참고할지 계산하고, 그 비율에 따라 정보를 섞는 방식이 Attention이었습니다.
이번 장에서는 그중에서도 GPT의 심장에 가까운 Self-Attention을 자세히 보겠습니다.
Self-Attention은 이름만 보면 조금 낯섭니다. 스스로에게 주목한다니, 토큰 하나가 자기 자신만 바라본다는 뜻일까요? 그렇지는 않습니다. 여기서 self는 같은 입력 안에서라는 뜻에 가깝습니다. 즉, 문장 안의 토큰들이 같은 문장 안의 다른 토큰들을 서로 참고하며 자기 표현을 새로 만든다는 뜻입니다.
Self-Attention은 같은 문장 안의 토큰들이 서로를 바라보며, 각자의 문맥화된 표현을 만드는 계산이다.
GPT를 직접 만들어보려면 반드시 이 관문을 지나야 합니다. 토큰화, 임베딩, 컨텍스트 윈도우가 모델의 입구라면, Self-Attention은 그 안에서 문맥이 실제로 섞이기 시작하는 장소입니다.

Self-Attention에서 self의 의미
문장을 하나 보겠습니다.
새가 하늘을 날았다
이 문장을 토큰 세 개로 나누었다고 해 봅시다.
| 위치 | 토큰 |
|---|---|
| 1 | 새가 |
| 2 | 하늘을 |
| 3 | 날았다 |
처음에는 각 토큰이 자기 임베딩 벡터를 가집니다. 이 벡터는 토큰 자체의 기본 표현에 가깝습니다.
| 토큰 | 처음 표현 |
|---|---|
| 새가 | 새가라는 토큰의 벡터 |
| 하늘을 | 하늘을이라는 토큰의 벡터 |
| 날았다 | 날았다라는 토큰의 벡터 |
하지만 문장 안에서 "날았다"는 혼자 존재하지 않습니다. "새가" 날았고, "하늘을" 날았습니다. 따라서 "날았다"의 표현에는 앞의 두 토큰 정보가 섞이는 편이 좋습니다.
반대로 "새가"의 표현도 완전히 독립적이지 않습니다. 뒤에 "날았다"가 나오면 이 "새"는 생물로서의 새일 가능성이 커집니다. 만약 문장이 "새가 도착했다"라면 '새로운 것'의 줄임말처럼 해석될 여지도 문맥에 따라 생길 수 있습니다. 같은 토큰이라도 주변 토큰에 따라 표현이 달라지는 것입니다.
Self-Attention은 이 과정을 모든 토큰에 대해 수행합니다.
| 새로 만들 표현 | 참고할 수 있는 토큰 |
|---|---|
| 새가의 새 표현 | 새가, 하늘을, 날았다 |
| 하늘을의 새 표현 | 새가, 하늘을, 날았다 |
| 날았다의 새 표현 | 새가, 하늘을, 날았다 |
일단 일반적인 Self-Attention만 놓고 보면 각 토큰은 같은 문장 안의 모든 토큰을 볼 수 있습니다. 다만 GPT에서는 미래 토큰을 보면 안 되므로 뒤에서 causal mask를 씌웁니다. 지금은 먼저 기본 흐름부터 잡아 봅시다.
입력은 벡터들의 줄이다
Self-Attention의 입력은 문장이 아니라 벡터들의 줄입니다. 앞에서 배운 흐름을 다시 떠올리면 이렇습니다.
텍스트 → 토큰 → 토큰 ID → 임베딩 벡터
모델 안으로 들어온 문장은 이미 숫자 벡터로 바뀌어 있습니다. 예를 들어 토큰이 3개이고, 각 토큰을 4차원 벡터로 표현한다면 입력은 다음처럼 생겼다고 볼 수 있습니다.
| 토큰 | 벡터 예시 |
|---|---|
| 새가 | [0.2, 0.7, -0.1, 0.4] |
| 하늘을 | [0.6, -0.2, 0.3, 0.1] |
| 날았다 | [0.5, 0.4, 0.8, -0.3] |
실제 모델에서는 벡터 차원이 훨씬 큽니다. 수백, 수천, 또는 그 이상의 숫자로 토큰 하나를 표현할 수 있습니다. 하지만 원리는 같습니다. 토큰 하나는 숫자 목록 하나이고, 문장은 그 숫자 목록들이 순서대로 쌓인 형태입니다.
이 입력을 보통 행렬로 생각합니다.
X = 토큰 벡터들을 위에서 아래로 쌓은 행렬
토큰 수를 T, 벡터 차원을 C라고 하면 X의 모양은 다음과 같습니다.
X의 모양 = T × C
여기서 T는 컨텍스트 안의 토큰 개수이고, C는 각 토큰을 표현하는 숫자의 개수입니다. 예를 들어 토큰 3개, 벡터 차원 4라면 X는 3×4 행렬입니다.
Self-Attention은 이 X를 입력으로 받아, 같은 크기의 새 행렬을 출력합니다.
입력: 각 토큰의 기존 표현
출력: 문맥이 섞인 각 토큰의 새 표현
중요한 점은 토큰 개수가 유지된다는 것입니다. 토큰 3개가 들어가면 새 표현도 3개가 나옵니다. 다만 각 토큰 벡터의 내용이 문맥을 반영하도록 바뀝니다.
첫 단계: 토큰끼리 점수를 매긴다
Self-Attention의 첫 번째 일은 토큰끼리의 관련도를 점수로 만드는 것입니다.
"날았다"의 새 표현을 만든다고 해 봅시다. 이때 "날았다"는 "새가", "하늘을", "날았다"를 각각 얼마나 참고해야 할까요?
이를 위해 모델은 현재 토큰과 참고 후보 토큰 사이의 점수를 계산합니다.
| 현재 토큰 | 참고 후보 | Attention score 예시 |
|---|---|---|
| 날았다 | 새가 | 1.2 |
| 날았다 | 하늘을 | 0.9 |
| 날았다 | 날았다 | 1.5 |
이 값들은 아직 비율이 아닙니다. 그냥 원점수입니다. 점수가 클수록 더 강하게 참고할 가능성이 높다는 뜻입니다.
그렇다면 이 점수는 어디서 나올까요? 가장 단순하게는 두 벡터가 얼마나 비슷한 방향을 가리키는지 계산할 수 있습니다. 벡터끼리 곱해서 더하는 내적을 사용하면 됩니다. 내적은 두 벡터가 서로 얼마나 잘 맞물리는지 보는 간단한 방법입니다.
예를 들어 "날았다" 벡터와 "새가" 벡터를 비교해 하나의 점수를 만들고, "날았다" 벡터와 "하늘을" 벡터를 비교해 또 하나의 점수를 만듭니다. 이렇게 현재 토큰과 모든 후보 토큰을 비교하면 한 줄의 점수가 생깁니다.
날았다가 보는 점수 = [새가 점수, 하늘을 점수, 날았다 점수]
실제 GPT에서는 여기서 한 단계 더 나아가 Query, Key, Value라는 세 종류의 벡터를 만들어 사용합니다. 하지만 지금은 핵심 흐름을 잡는 것이 더 중요합니다. 다음 장에서 Query, Key, Value를 따로 분해해 볼 것이므로, 이번 장에서는 "토큰 벡터로 점수를 만들고, 그 점수로 정보를 섞는다"는 큰 흐름에 집중합시다.
두 번째 단계: softmax로 비율을 만든다
Attention score는 그대로 쓰기 어렵습니다. 방금 예시에서 점수는 1.2, 0.9, 1.5였습니다. 이 숫자들은 합이 1도 아니고, 음수가 나올 수도 있으며, 크기의 범위도 일정하지 않습니다.
그래서 Self-Attention은 이 점수들을 softmax라는 함수에 넣습니다. softmax는 여러 점수를 합이 1인 비율로 바꾸어 줍니다. 점수가 큰 항목에는 더 큰 비율을 주고, 점수가 작은 항목에는 더 작은 비율을 줍니다.
예를 들어 다음 점수가 있다고 합시다.
| 참고 후보 | 원점수 |
|---|---|
| 새가 | 1.2 |
| 하늘을 | 0.9 |
| 날았다 | 1.5 |
softmax를 적용하면 대략 다음과 같은 비율이 될 수 있습니다.
| 참고 후보 | softmax 후 비율 |
|---|---|
| 새가 | 0.32 |
| 하늘을 | 0.24 |
| 날았다 | 0.44 |
이제 숫자들의 합은 1입니다.
0.32 + 0.24 + 0.44 = 1.00
이 값들을 Attention weight라고 부릅니다. 우리말로는 주의 가중치, 참고 비율 정도로 이해하면 됩니다. "날았다"는 자기 새 표현을 만들 때 "새가" 정보를 0.32만큼, "하늘을" 정보를 0.24만큼, 자기 자신의 정보를 0.44만큼 섞겠다는 뜻입니다.
softmax가 중요한 이유는 선택을 딱 하나로 고정하지 않는다는 데 있습니다. 모델은 "새가"만 보거나 "하늘을"만 보는 것이 아닙니다. 여러 토큰을 조금씩 섞을 수 있습니다. 언어에서는 이런 부드러운 섞임이 매우 중요합니다. 어떤 단어는 주어와 관련되고, 동시에 목적어와도 관련되며, 자기 자신의 원래 의미도 어느 정도 유지해야 하기 때문입니다.
세 번째 단계: 가중합으로 새 표현을 만든다
이제 비율이 생겼으니 실제 정보를 섞을 차례입니다. 이때 사용하는 방식이 가중합입니다.
방금 얻은 Attention weight를 다시 봅시다.
| 참고 후보 | 비율 |
|---|---|
| 새가 | 0.32 |
| 하늘을 | 0.24 |
| 날았다 | 0.44 |
그러면 "날았다"의 새 표현은 다음처럼 만들어집니다.
새 "날았다" 표현
= 0.32 × "새가"의 정보
0.24 × "하늘을"의 정보
0.44 × "날았다"의 정보
이 문장이 Self-Attention의 핵심입니다.
토큰은 혼자서 새 표현을 만들지 않습니다. 문장 안의 다른 토큰들을 바라보고, 각 토큰의 정보를 정해진 비율만큼 섞어 새 표현을 만듭니다. 이렇게 만들어진 벡터는 처음 임베딩보다 더 문맥적입니다.
처음 "날았다" 벡터는 그저 "날았다"라는 토큰의 일반적인 표현이었습니다. 하지만 Self-Attention을 거친 "날았다" 벡터에는 "새가"와 "하늘을"이라는 정보가 섞여 있습니다. 따라서 다음 층으로 넘어갈 때 모델은 이 문장에서의 "날았다"를 더 풍부하게 다룰 수 있습니다.
같은 원리가 "새가"와 "하늘을"에도 적용됩니다. 즉, Self-Attention은 한 토큰만 갱신하는 과정이 아니라 모든 토큰의 표현을 동시에 갱신하는 과정입니다.
모든 토큰에 대해 같은 일을 반복한다
이번에는 세 토큰 모두에 대해 Attention weight가 계산되었다고 해 봅시다.
| 현재 토큰 참고 토큰 | 새가 | 하늘을 | 날았다 |
|---|---|---|---|
| 새가 | 0.50 | 0.20 | 0.30 |
| 하늘을 | 0.25 | 0.45 | 0.30 |
| 날았다 | 0.32 | 0.24 | 0.44 |
이 표의 한 줄은 현재 토큰 하나가 어떤 토큰들을 얼마나 참고하는지 나타냅니다. 첫 번째 줄은 "새가"의 새 표현을 만들 때 쓰는 비율입니다. 두 번째 줄은 "하늘을"의 새 표현을 만들 때 쓰는 비율입니다. 세 번째 줄은 "날았다"의 새 표현을 만들 때 쓰는 비율입니다.
각 줄의 합은 1입니다.
이제 각 토큰의 새 표현은 다음처럼 만들어집니다.
새 "새가" 표현
= 0.50 × "새가" 정보 + 0.20 × "하늘을" 정보 + 0.30 × "날았다" 정보
새 "하늘을" 표현
= 0.25 × "새가" 정보 + 0.45 × "하늘을" 정보 + 0.30 × "날았다" 정보
새 "날았다" 표현
= 0.32 × "새가" 정보 + 0.24 × "하늘을" 정보 + 0.44 × "날았다" 정보
이렇게 보면 Self-Attention이 하는 일이 꽤 분명해집니다. 문장 안의 모든 토큰이 서로를 바라보고, 각자 필요한 비율로 정보를 섞어 자기 표현을 다시 씁니다.
처음에는 모든 토큰이 각각 자기 명찰만 들고 있는 것과 비슷합니다. "나는 새가다", "나는 하늘을이다", "나는 날았다다"라고 말하는 수준입니다. Self-Attention을 지나면 각 토큰은 주변 이야기를 조금씩 들은 상태가 됩니다. "나는 이 문장에서 하늘을 나는 새와 관련된 날았다다"처럼 문맥을 품은 표현으로 바뀝니다.
GPT에서는 미래를 보면 안 된다
지금까지는 모든 토큰이 모든 토큰을 참고할 수 있다고 설명했습니다. 하지만 GPT에서는 중요한 제한이 있습니다.
GPT는 다음 토큰을 예측하는 모델입니다. 어떤 위치에서 다음 토큰을 맞혀야 한다면, 아직 나오지 않은 미래 토큰을 보면 안 됩니다. 정답지를 미리 보고 시험을 치르는 것과 같기 때문입니다.
예를 들어 다음 문장을 생각해 봅시다.
나는 라면을 먹었다
토큰을 다음처럼 나누겠습니다.
| 위치 | 토큰 |
|---|---|
| 1 | 나는 |
| 2 | 라면을 |
| 3 | 먹었다 |
GPT가 학습할 때는 각 위치에서 다음 토큰을 맞히는 연습을 합니다. "나는"을 보고 "라면을"을 예측하고, "나는 라면을"을 보고 "먹었다"를 예측합니다.
그런데 "나는" 위치의 표현을 만들 때 "라면을"이나 "먹었다"를 볼 수 있다면 문제가 생깁니다. 아직 예측해야 할 미래 정보를 이미 본 셈입니다. 그래서 GPT식 Self-Attention에서는 현재 위치보다 뒤에 있는 토큰을 가립니다. 이것이 causal mask입니다.
causal은 원인과 결과의 방향을 뜻합니다. 언어 생성에서는 앞의 토큰들이 뒤의 토큰을 만드는 원인이 되어야 합니다. 뒤의 토큰이 앞의 토큰 계산에 끼어들면 안 됩니다.
causal mask는 미래 칸을 0으로 만든다
causal mask를 표로 보면 아주 쉽습니다. 다음 토큰들이 있다고 합시다.
나는 / 라면을 / 먹었다
각 현재 토큰이 참고할 수 있는 토큰은 다음과 같습니다.
| 현재 토큰 참고 토큰 | 나는 | 라면을 | 먹었다 |
|---|---|---|---|
| 나는 | 볼 수 있음 | 볼 수 없음 | 볼 수 없음 |
| 라면을 | 볼 수 있음 | 볼 수 있음 | 볼 수 없음 |
| 먹었다 | 볼 수 있음 | 볼 수 있음 | 볼 수 있음 |
첫 번째 토큰 "나는"은 자기 자신만 볼 수 있습니다. 두 번째 토큰 "라면을"은 "나는"과 자기 자신을 볼 수 있습니다. 세 번째 토큰 "먹었다"는 앞의 모든 토큰과 자기 자신을 볼 수 있습니다.
이 제한을 Attention score에 적용하려면 어떻게 해야 할까요? 미래 위치의 점수를 아주 작은 값으로 만들어 버리면 됩니다. 보통 구현에서는 미래 칸에 음의 무한대에 가까운 값을 넣습니다. 그러면 softmax를 통과했을 때 그 위치의 비율이 0이 됩니다.
예를 들어 마스크 전 점수 행렬이 다음과 같다고 합시다. 숫자는 이해를 위한 예시입니다.
| 현재 토큰 참고 토큰 | 나는 | 라면을 | 먹었다 |
|---|---|---|---|
| 나는 | 0.8 | 1.1 | 0.4 |
| 라면을 | 0.7 | 1.3 | 0.9 |
| 먹었다 | 1.0 | 1.4 | 1.2 |
causal mask를 적용하면 미래 칸이 가려집니다.
| 현재 토큰 참고 토큰 | 나는 | 라면을 | 먹었다 |
|---|---|---|---|
| 나는 | 0.8 | -∞ | -∞ |
| 라면을 | 0.7 | 1.3 | -∞ |
| 먹었다 | 1.0 | 1.4 | 1.2 |
이 상태에서 softmax를 적용하면 가려진 칸의 비율은 0이 됩니다.
| 현재 토큰 참고 토큰 | 나는 | 라면을 | 먹었다 |
|---|---|---|---|
| 나는 | 1.00 | 0.00 | 0.00 |
| 라면을 | 0.35 | 0.65 | 0.00 |
| 먹었다 | 0.27 | 0.40 | 0.33 |
이제 "나는"은 미래를 보지 못합니다. "라면을"도 "먹었다"를 보지 못합니다. "먹었다"만 앞의 모든 토큰을 볼 수 있습니다. 이 규칙 덕분에 GPT는 학습할 때도, 생성할 때도 앞에서 뒤로 자연스럽게 텍스트를 이어 갈 수 있습니다.
여기서 중요한 점은 causal mask가 정보를 지우는 위치입니다. 마스크는 가중합을 한 뒤에 적용하는 것이 아니라, 보통 softmax 전에 score 단계에서 적용합니다. 그래야 미래 토큰의 비율이 정확히 0이 됩니다.
행렬로 보면 한 번에 계산된다
지금까지는 이해하기 쉽게 "날았다" 하나를 중심으로 설명했습니다. 하지만 실제 모델은 토큰 하나씩 느리게 계산하지 않습니다. 행렬 연산으로 모든 토큰의 관계를 한꺼번에 계산합니다.
입력 행렬 X가 있다고 합시다.
X의 모양 = T × C
여기서 T는 토큰 수, C는 벡터 차원입니다.
가장 단순한 Self-Attention에서는 점수 행렬을 다음처럼 만들 수 있습니다.
scores = X × Xᵀ
X는 T×C이고, Xᵀ는 C×T입니다. 두 행렬을 곱하면 결과는 T×T가 됩니다.
scores의 모양 = T × T
이 T×T 행렬이 바로 모든 토큰 쌍의 Attention score를 담은 표입니다. 행은 현재 토큰, 열은 참고 토큰입니다.
| 행렬 | 의미 | 모양 |
|---|---|---|
| X | 입력 토큰 벡터들 | T × C |
| Xᵀ | X를 뒤집은 행렬 | C × T |
| scores = X × Xᵀ | 토큰끼리의 점수 | T × T |
그다음 각 행에 softmax를 적용합니다. 그러면 각 현재 토큰이 참고 토큰들에 나누어 줄 비율이 됩니다.
weights = softmax(scores)
weights의 모양도 T×T입니다. 마지막으로 이 weights를 다시 입력 X와 곱합니다.
output = weights × X
weights는 T×T이고, X는 T×C입니다. 곱하면 결과는 T×C가 됩니다.
output의 모양 = T × C
즉, 입력과 출력의 모양이 같습니다. 토큰 수는 그대로이고, 각 토큰의 벡터 차원도 그대로입니다. 달라지는 것은 벡터 안의 내용입니다. 이제 각 토큰 벡터에는 문맥 정보가 섞여 있습니다.
Self-Attention의 전체 흐름은 다음처럼 정리할 수 있습니다.
토큰 벡터들을 모아 입력 행렬 X를 만든다.
X와 Xᵀ를 곱해 토큰끼리의 점수 행렬을 만든다.
GPT 방식에서는 미래 토큰을 보지 못하도록 causal mask를 적용한다.
softmax로 점수를 참고 비율로 바꾼다.
참고 비율과 토큰 벡터를 곱해 문맥화된 출력 벡터를 만든다.
구현으로 옮기면 뼈대는 거의 다음과 같습니다.
X = token_embeddings
scores = X @ X.T
scores = apply_causal_mask(scores)
weights = softmax(scores)
out = weights @ X물론 실제 GPT 구현은 이보다 조금 더 정교합니다. X를 그대로 쓰지 않고 Query, Key, Value를 만들고, 점수를 벡터 차원의 제곱근으로 나누어 안정화하며, 여러 개의 Attention을 병렬로 계산합니다. 하지만 지금 본 다섯 줄은 Self-Attention의 가장 중요한 골격입니다.
이 골격을 이해하면 나중에 코드를 보아도 덜 무섭습니다. 복잡해 보이는 Transformer 코드도 결국 "점수를 만들고, 마스크를 씌우고, softmax를 적용하고, 가중합한다"는 흐름을 반복하기 때문입니다.
왜 점수를 벡터끼리 곱해서 만들까
잠깐 멈춰서 이런 질문을 해볼 수 있습니다.
왜 하필 벡터끼리 곱해서 점수를 만들까요? 그냥 가까운 토큰을 더 많이 보면 안 될까요?
가까운 토큰이 중요한 경우는 많습니다. "맛있는 사과"처럼 바로 옆 단어가 강한 단서를 줄 때도 있습니다. 하지만 언어의 관계가 항상 거리로만 결정되지는 않습니다.
철수는 어제 영희가 사 온 케이크를 먹었다.
여기서 "먹었다"와 강하게 연결되는 대상은 "케이크"입니다. 그런데 "케이크"와 "먹었다" 사이에는 "를"이 있고, 앞에는 여러 수식어가 있습니다. 단순히 가까운 토큰만 보면 중요한 관계를 놓칠 수 있습니다.
Self-Attention은 위치가 아니라 벡터의 내용으로 관련도를 계산합니다. 학습이 잘되면 "먹었다"와 음식에 해당하는 토큰 사이의 점수가 커질 수 있고, "입금했다"와 돈 또는 계좌 관련 토큰 사이의 점수가 커질 수 있습니다. 즉, 거리보다 의미와 역할에 가까운 연결을 만들 수 있습니다.
물론 위치 정보가 필요 없다는 뜻은 아닙니다. "개가 사람을 물었다"와 "사람이 개를 물었다"는 같은 단어를 포함하지만 순서가 달라 의미가 달라집니다. 그래서 Transformer에는 위치 정보를 넣는 장치가 따로 필요합니다. 우리는 뒤에서 위치 임베딩을 따로 다룰 것입니다.
지금은 이렇게 이해하면 충분합니다. Self-Attention은 토큰의 위치만 보는 것이 아니라, 토큰 벡터들이 서로 얼마나 관련되어 보이는지를 계산해 문맥을 섞습니다.
Self-Attention의 출력은 더 똑똑한 토큰 표현이다
Self-Attention의 출력은 최종 답변이 아닙니다. "안녕하세요" 같은 텍스트가 바로 튀어나오는 단계도 아닙니다. 출력은 여전히 숫자 벡터입니다.
하지만 이 숫자 벡터는 입력 벡터와 성격이 다릅니다. 입력 벡터가 각 토큰의 기본 표현에 가까웠다면, 출력 벡터는 문맥을 반영한 표현입니다.
예를 들어 "배"라는 토큰을 생각해 봅시다.
배를 먹었다.
배를 탔다.
배가 아프다.
처음 임베딩 단계에서 "배"는 동일한 토큰으로 처리될 수 있습니다. 하지만 Self-Attention을 거치면 각 문장의 "배" 표현은 달라질 수 있습니다. "먹었다"를 많이 참고하면 과일에 가까운 표현이 되고, "탔다"를 많이 참고하면 탈것에 가까운 표현이 되며, "아프다"를 많이 참고하면 신체 부위에 가까운 표현이 됩니다.
이것이 문맥화된 표현의 힘입니다. LLM이 단어를 고정된 사전 항목처럼만 다루지 않고, 문장 안의 관계에 따라 다르게 처리할 수 있는 이유가 여기에 있습니다.
GPT에는 이런 Self-Attention 층이 하나만 있는 것이 아닙니다. 여러 층이 쌓입니다. 첫 번째 층에서 가까운 관계가 섞이고, 다음 층에서 더 넓은 관계가 섞이고, 또 다음 층에서 더 추상적인 패턴이 반영될 수 있습니다. 물론 실제로 각 층이 정확히 어떤 역할을 한다고 단순하게 나누기는 어렵지만, 층을 거치며 표현이 점점 변한다는 점은 중요합니다.
작은 예제로 전체 흐름 다시 보기
이제 처음부터 끝까지 한 번에 따라가 봅시다.
문장은 다음과 같습니다.
고양이 생선 먹었다
조사를 생략해 토큰 세 개만 남겼습니다.
| 위치 | 토큰 |
|---|---|
| 1 | 고양이 |
| 2 | 생선 |
| 3 | 먹었다 |
각 토큰은 임베딩 벡터로 바뀝니다.
| 토큰 | 벡터 이름 |
|---|---|
| 고양이 | x₁ |
| 생선 | x₂ |
| 먹었다 | x₃ |
이 벡터들을 쌓으면 입력 행렬 X가 됩니다.
X = [x₁
x₂
x₃]이제 X와 Xᵀ를 곱해 점수 행렬을 만듭니다.
| 현재 토큰 참고 토큰 | 고양이 | 생선 | 먹었다 |
|---|---|---|---|
| 고양이 | 1.4 | 0.8 | 0.6 |
| 생선 | 0.7 | 1.5 | 1.0 |
| 먹었다 | 1.2 | 1.6 | 1.1 |
GPT 방식이라면 causal mask를 적용합니다. 첫 번째 토큰은 뒤를 볼 수 없고, 두 번째 토큰은 세 번째 토큰을 볼 수 없습니다.
| 현재 토큰 참고 토큰 | 고양이 | 생선 | 먹었다 |
|---|---|---|---|
| 고양이 | 1.4 | -∞ | -∞ |
| 생선 | 0.7 | 1.5 | -∞ |
| 먹었다 | 1.2 | 1.6 | 1.1 |
이제 softmax로 비율을 만듭니다.
| 현재 토큰 참고 토큰 | 고양이 | 생선 | 먹었다 |
|---|---|---|---|
| 고양이 | 1.00 | 0.00 | 0.00 |
| 생선 | 0.31 | 0.69 | 0.00 |
| 먹었다 | 0.29 | 0.43 | 0.28 |
마지막으로 이 비율로 벡터를 섞습니다.
새 "고양이" 표현 = 1.00 × x₁
새 "생선" 표현 = 0.31 × x₁ + 0.69 × x₂
새 "먹었다" 표현 = 0.29 × x₁ + 0.43 × x₂ + 0.28 × x₃
이 결과를 보면 GPT식 Self-Attention의 특징이 잘 드러납니다. 앞쪽 토큰은 볼 수 있는 정보가 적고, 뒤쪽 토큰은 더 많은 과거 문맥을 볼 수 있습니다. "먹었다"는 "고양이"와 "생선"을 모두 참고할 수 있으므로, 이 문장에서 누가 무엇을 먹었는지와 관련된 표현을 만들 수 있습니다.
단, 이 숫자들은 설명을 위한 예시입니다. 실제 모델의 값은 학습된 가중치와 입력 문맥에 따라 달라집니다. 중요한 것은 숫자 자체가 아니라 흐름입니다.
점수 → 마스크 → softmax → 가중합 → 문맥화된 표현
이 다섯 단계를 기억하면 Self-Attention의 큰 그림은 잡은 것입니다.
Self-Attention은 왜 GPT의 관문인가
GPT를 직접 구현할 때 가장 먼저 만나는 어려움은 "문맥을 어떻게 섞을 것인가"입니다. 단순한 Bigram 모델은 바로 앞 글자나 토큰 정도만 보고 다음을 예측할 수 있습니다. 작은 신경망 언어 모델도 어느 정도 문맥을 사용할 수 있지만, 긴 범위의 토큰 관계를 유연하게 다루기는 쉽지 않습니다.
Self-Attention은 이 문제를 정면으로 다룹니다. 각 위치의 토큰이 앞의 토큰들을 직접 참고하면서 자기 표현을 바꿀 수 있게 해 줍니다. 그리고 이 계산은 행렬 곱으로 표현되기 때문에 GPU에서 효율적으로 처리하기 좋습니다.
앞으로 우리가 만들 Mini GPT에서도 핵심은 같습니다. ChatGPT급 거대 모델을 만드는 것이 아니라, ChatGPT의 핵심 원리를 이해하기 위한 작은 GPT를 만들 것입니다. 그 작은 GPT 안에서도 Self-Attention은 반드시 들어갑니다. 크기는 작아도 원리는 같습니다.
처음에는 표와 행렬이 조금 복잡해 보일 수 있습니다. 하지만 Self-Attention은 결국 매우 일관된 일을 합니다.
한 토큰이 묻습니다.
"내 새 표현을 만들려면, 앞의 어떤 토큰들을 얼마나 참고해야 하지?"
Self-Attention은 그 질문에 숫자로 답합니다. 그리고 그 숫자만큼 정보를 섞어 새 표현을 만듭니다.
이번 장에서 기억할 것
Self-Attention은 같은 문장 안의 토큰들이 서로를 참고하며 자기 표현을 갱신하는 방법입니다. 입력은 토큰 임베딩 벡터들의 행렬이고, 출력은 문맥이 섞인 새 벡터들의 행렬입니다. 토큰 수와 벡터 차원은 유지되지만, 각 벡터의 내용은 문맥을 반영하도록 바뀝니다.
계산 흐름은 다섯 단계로 볼 수 있습니다. 먼저 토큰 벡터들 사이의 Attention score를 만들고, GPT에서는 미래 토큰을 보지 못하도록 causal mask를 적용합니다. 그다음 softmax로 점수를 합이 1인 비율로 바꾸고, 그 비율을 사용해 토큰 벡터들을 가중합합니다. 이렇게 만들어진 결과가 각 토큰의 문맥화된 표현입니다.
행렬 관점에서는 X × Xᵀ로 점수 행렬을 만들고, softmax를 거쳐 나온 weight를 다시 X와 곱해 output을 만듭니다. 실제 GPT에서는 여기에 Query, Key, Value가 들어가고 Multi-Head Attention으로 확장되지만, 핵심 골격은 그대로입니다.
다음 장에서는 방금 살짝 언급한 Query, Key, Value를 자세히 살펴보겠습니다. 지금까지는 토큰 벡터 하나가 점수 계산과 정보 전달을 모두 맡는 것처럼 설명했습니다. 실제 Transformer에서는 "무엇을 찾는가", "무엇을 가지고 있는가", "어떤 정보를 전달할 것인가"를 나누어 계산합니다. 이 세 역할이 바로 Query, Key, Value입니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.