Attention이 등장한 이유를 한 문장으로 줄이면 이렇습니다. 모델이 지금 처리하는 토큰과 관련 있는 다른 토큰을 직접 바라보게 하자는 것입니다.
이제 한 걸음 더 들어가 보겠습니다. Self-Attention은 이름 그대로 '자기 자신에게 Attention을 적용하는 방식'입니다. 여기서 자기 자신이란 하나의 토큰만을 뜻하지 않습니다. 한 문장 안에 들어 있는 모든 토큰의 묶음을 뜻합니다. 즉, 문장 안의 각 토큰이 같은 문장 안의 다른 토큰들을 바라보며 자신의 표현을 새로 만드는 과정입니다.
예를 들어 다음 문장을 생각해 봅시다.
나는 사과를 맛있게 먹었다
먹었다라는 토큰은 혼자서는 단순히 어떤 행동을 나타냅니다. 하지만 문장 안에서는 나는과 연결되어 누가 먹었는지 알게 되고, 사과를과 연결되어 무엇을 먹었는지 알게 되며, 맛있게와 연결되어 어떤 방식으로 먹었는지 알게 됩니다. Self-Attention은 바로 이런 관계를 숫자로 계산합니다.
이 장의 목표는 Transformer의 심장에 해당하는 Self-Attention을 손으로 만질 수 있을 만큼 직관적으로 이해하는 것입니다.

Self-Attention은 문장 안의 회의다
문장 안에 토큰들이 모여 회의를 한다고 상상해 봅시다. 각 토큰은 처음에는 자기 정보만 들고 회의장에 들어옵니다.
나는은 '주어일지도 모르는 정보'를 들고 있고, 사과를은 '대상일지도 모르는 정보'를 들고 있으며, 먹었다는 '동작일지도 모르는 정보'를 들고 있습니다. 그런데 문장을 이해하려면 혼자 가진 정보만으로는 부족합니다. 먹었다는 무엇을 먹었는지 알아야 하고, 사과를은 어떤 행동의 대상인지 알아야 합니다.
Self-Attention은 각 토큰이 다른 토큰들에게 이렇게 묻는 과정입니다.
"지금 내 의미를 더 잘 이해하려면 누구의 정보를 참고해야 하지?"
그리고 그 답을 확률처럼 계산합니다. 어떤 토큰은 많이 참고하고, 어떤 토큰은 조금 참고합니다. 마지막에는 참고한 정보들을 섞어 새로운 벡터를 만듭니다. 이 새 벡터가 바로 문맥화된 표현입니다.
임베딩 장에서 우리는 토큰이 벡터로 바뀐다고 배웠습니다. 하지만 단순 임베딩은 보통 같은 토큰에 같은 벡터를 줍니다. 예를 들어 배라는 단어가 과일인지, 배를 타는 배인지, 사람의 배인지 문맥 없이 알기 어렵습니다. Self-Attention은 주변 토큰들을 함께 보면서 같은 토큰도 문맥에 따라 다른 표현이 되도록 만듭니다.
Query, Key, Value: 질문, 이름표, 내용
Self-Attention을 이해할 때 가장 먼저 만나는 세 단어가 있습니다. Query, Key, Value입니다. 처음 보면 괜히 어렵게 느껴지지만, 역할은 꽤 단순합니다.
각 토큰은 하나의 임베딩 벡터로 시작합니다. 그런데 Attention 계산을 하기 위해 이 벡터를 세 가지 벡터로 바꿉니다.
Query는 이 토큰이 찾고 싶은 정보입니다.
Key는 이 토큰이 어떤 정보를 가지고 있는지 나타내는 이름표입니다.
Value는 실제로 다른 토큰에게 전달할 내용입니다.
도서관에서 책을 찾는 장면으로 비유해 보겠습니다. Query는 내가 검색창에 입력한 질문입니다. Key는 책마다 붙어 있는 제목, 키워드, 분류표 같은 것입니다. Value는 실제 책 안에 담긴 내용입니다.
내가 검색한 Query와 어떤 책의 Key가 잘 맞으면, 그 책의 Value를 많이 참고합니다. 잘 맞지 않으면 조금만 참고합니다.
Self-Attention에서도 똑같습니다. 한 토큰의 Query가 다른 토큰들의 Key와 얼마나 잘 맞는지 계산합니다. 잘 맞는 토큰의 Value는 많이 가져오고, 덜 맞는 토큰의 Value는 적게 가져옵니다.
수식으로 쓰면 다음과 같습니다.
Q = XW_Q
K = XW_K
V = XW_V여기서 X는 문장 전체의 토큰 임베딩을 모아 놓은 행렬입니다. W_Q, W_K, W_V는 학습되는 가중치입니다. 즉, Query, Key, Value를 만드는 방법 자체도 모델이 학습합니다.
만약 문장에 토큰이 5개 있고, 각 토큰 임베딩의 차원이 8이라면 X의 모양은 다음과 같습니다.
X: (토큰 수, 임베딩 차원) = (5, 8)Query, Key, Value의 차원을 4로 만든다면 각각의 모양은 다음처럼 됩니다.
Q: (5, 4)
K: (5, 4)
V: (5, 4)중요한 점은 Query, Key, Value가 모두 같은 입력 X에서 나온다는 것입니다. 그래서 이것을 Self-Attention이라고 부릅니다. 문장 밖의 다른 정보가 아니라, 같은 문장 안의 토큰들끼리 서로를 바라보기 때문입니다.
Query와 Key로 관계 점수를 만든다
이제 각 토큰이 누구를 얼마나 참고할지 점수를 계산해야 합니다. 점수는 Query와 Key의 유사도로 만듭니다. 가장 흔히 사용하는 방식은 내적입니다.
내적은 두 벡터가 비슷한 방향을 바라볼수록 큰 값을 냅니다. 그래서 Query와 Key를 내적하면 "이 토큰이 저 토큰을 얼마나 참고할 만한가?"라는 점수로 사용할 수 있습니다.
전체 문장에 대해 한 번에 계산하면 다음과 같습니다.
scores = QK^T여기서 K^T는 Key 행렬을 뒤집은 전치 행렬입니다. Q의 모양이 (토큰 수, 차원)이고, K^T의 모양이 (차원, 토큰 수)이므로 결과는 다음 모양이 됩니다.
scores: (토큰 수, 토큰 수)이 행렬이 Self-Attention의 핵심입니다. 문장에 토큰이 4개라면 점수 행렬은 4 × 4가 됩니다. 각 행은 '현재 토큰이 다른 토큰들을 얼마나 바라보는가'를 뜻하고, 각 열은 '바라보는 대상 토큰'을 뜻합니다.
예를 들어 행이 먹었다이고 열이 사과를이라면, 그 값은 먹었다가 사과를을 얼마나 참고하는지 나타내는 점수입니다. 행이 사과를이고 열이 먹었다라면, 반대로 사과를이 먹었다를 얼마나 참고하는지 나타냅니다.
Self-Attention이 흥미로운 이유는 관계가 한 방향으로 고정되지 않는다는 데 있습니다. 먹었다는 목적어를 찾기 위해 사과를을 볼 수 있고, 사과를은 자신이 어떤 동작의 대상인지 알기 위해 먹었다를 볼 수 있습니다.
실제 Transformer에서는 점수를 그대로 쓰지 않고 보통 다음처럼 나눠 줍니다.
scores = QK^T / sqrt(d_k)d_k는 Key 벡터의 차원입니다. 왜 굳이 나눌까요? 차원이 커질수록 내적값이 너무 커질 수 있기 때문입니다. 점수가 지나치게 커지면 뒤에서 사용할 softmax가 한 토큰에만 거의 모든 비중을 몰아줄 수 있습니다. 그러면 학습이 불안정해질 수 있습니다. 그래서 sqrt(d_k)로 나누어 점수의 크기를 적당히 안정시킵니다.
softmax는 점수를 참고 비율로 바꾼다
점수만으로는 아직 정보를 섞을 수 없습니다. 점수는 음수일 수도 있고, 합이 1도 아닙니다. 우리는 "어떤 토큰을 몇 퍼센트 정도 참고할 것인가?"에 가까운 값이 필요합니다.
여기서 softmax가 등장합니다. softmax는 여러 점수를 받아서 합이 1인 값들로 바꿉니다. 값이 크면 더 큰 비중을 받고, 값이 작으면 작은 비중을 받습니다.
attention_weights = softmax(scores)이때 softmax는 각 행마다 적용됩니다. 왜냐하면 각 토큰마다 참고 대상들의 비율을 따로 정해야 하기 때문입니다.
예를 들어 먹었다가 다른 토큰들을 다음과 같이 참고한다고 해 봅시다.
나는 0.14
사과를 0.24
맛있게 0.24
먹었다 0.38이 값들의 합은 1입니다. 이 말은 먹었다의 새 표현을 만들 때 자기 자신을 가장 많이 참고하고, 사과를과 맛있게도 꽤 참고하며, 나는은 상대적으로 덜 참고한다는 뜻입니다.
그다음에는 이 비율을 이용해 Value를 섞습니다.
output = attention_weights × V즉, 최종 계산은 다음과 같이 정리할 수 있습니다.
Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V이 수식이 Transformer를 처음 볼 때 가장 자주 마주치는 Attention 공식입니다. 하지만 이제는 이 수식이 조금 덜 낯설 것입니다. 의미는 간단합니다.
Query와 Key로 누구를 볼지 정하고, softmax로 참고 비율을 만들고, 그 비율대로 Value를 섞는다.
이것이 Self-Attention입니다.
작은 문장으로 직접 계산해 보기
이제 아주 작은 예제로 Self-Attention을 구현해 보겠습니다. 여기서는 개념을 보기 위해 Query, Key, Value를 만드는 가중치를 단순히 단위행렬로 두겠습니다. 단위행렬은 입력을 거의 그대로 통과시키는 행렬입니다. 실제 모델에서는 이 가중치들이 학습을 통해 바뀝니다.
import torch
import torch.nn.functional as F
# 보기 좋게 출력하기
torch.set_printoptions(precision=3, sci_mode=False)
tokens = ['나는', '사과를', '맛있게', '먹었다']
# 각 토큰을 4차원 벡터로 표현한 장난감 예제입니다.
# 실제 모델에서는 이 벡터도 학습된 임베딩에서 나옵니다.
X = torch.tensor([
[1.0, 0.0, 0.0, 0.0], # 나는
[0.0, 1.0, 0.0, 1.0], # 사과를
[0.0, 0.0, 1.0, 1.0], # 맛있게
[0.0, 1.0, 1.0, 0.0], # 먹었다
])
d_model = X.shape[-1]
# 설명을 쉽게 하기 위해 W_Q, W_K, W_V를 단위행렬로 둡니다.
W_Q = torch.eye(d_model)
W_K = torch.eye(d_model)
W_V = torch.eye(d_model)
Q = X @ W_Q
K = X @ W_K
V = X @ W_V
scores = Q @ K.T / (d_model ** 0.5)
attention_weights = F.softmax(scores, dim=-1)
context = attention_weights @ V
print('scores')
print(scores)
print('nattention weights')
print(attention_weights)
print('ncontext vectors')
print(context)실행하면 대략 이런 Attention 비율을 볼 수 있습니다.
attention weights
tensor([[0.355, 0.215, 0.215, 0.215],
[0.143, 0.388, 0.235, 0.235],
[0.143, 0.235, 0.388, 0.235],
[0.143, 0.235, 0.235, 0.388]])마지막 행을 봅시다. 마지막 행은 먹었다가 각 토큰을 얼마나 참고하는지 나타냅니다.
나는 0.143
사과를 0.235
맛있게 0.235
먹었다 0.388이 장난감 예제에서는 먹었다가 자기 자신을 가장 많이 참고합니다. 그리고 사과를, 맛있게도 어느 정도 참고합니다. 이 비율로 Value 벡터들을 섞으면 먹었다의 새 표현이 만들어집니다.
처음 먹었다의 벡터는 다음이었습니다.
[0.0, 1.0, 1.0, 0.0]하지만 Attention을 거친 뒤에는 주변 토큰의 정보가 섞인 새 벡터가 됩니다.
[0.143, 0.623, 0.623, 0.470]이 숫자 하나하나를 언어적으로 완벽하게 해석할 필요는 없습니다. 중요한 것은 구조입니다. 먹었다라는 토큰의 표현이 더 이상 자기 자신만의 정보가 아니라는 점입니다. 이제 이 벡터에는 나는, 사과를, 맛있게의 정보가 섞여 있습니다.
이것이 문맥화된 표현입니다.
Self-Attention은 모든 토큰을 동시에 업데이트한다
RNN은 순서대로 토큰을 처리했습니다. 앞에서 뒤로 한 칸씩 이동하면서 은닉 상태를 넘겼습니다. 그래서 먼 거리에 있는 정보가 여러 단계를 거쳐 전달되어야 했습니다.
Self-Attention은 다릅니다. 모든 토큰 쌍의 관계를 한 번에 계산합니다. 토큰이 4개라면 4 × 4 관계를 만들고, 토큰이 100개라면 100 × 100 관계를 만듭니다.
이 방식의 장점은 분명합니다. 멀리 떨어진 토큰도 한 번에 직접 연결될 수 있습니다. 문장 맨 앞의 단어와 맨 뒤의 단어가 관련 있다면, Attention 점수를 통해 바로 서로를 바라볼 수 있습니다.
물론 대가도 있습니다. 토큰 수가 늘어나면 관계 행렬의 크기가 빠르게 커집니다. 토큰 수가 T일 때 Attention 점수 행렬은 T × T입니다. 그래서 긴 문서를 처리할 때 Self-Attention은 계산량과 메모리 사용량이 커집니다. 하지만 그럼에도 이 방식은 언어 모델의 흐름을 크게 바꾸었습니다. 순서대로 기억을 넘겨받는 방식에서, 필요한 토큰을 직접 바라보는 방식으로 중심이 이동했기 때문입니다.
Multi-Head Attention: 여러 시선으로 동시에 보기
지금까지는 하나의 Attention만 생각했습니다. 그런데 Transformer는 보통 Attention을 하나만 쓰지 않습니다. 여러 개의 Attention을 동시에 사용합니다. 이것을 Multi-Head Attention이라고 합니다.
여기서 head는 하나의 Attention 계산 단위를 뜻합니다. head가 8개라면, 모델은 서로 다른 8개의 방식으로 Query, Key, Value를 만들고 Attention을 계산합니다. 그다음 각 head의 결과를 이어 붙인 뒤 다시 한 번 선형 변환합니다.
왜 굳이 여러 head를 쓸까요? 문장 안의 관계가 하나만 있는 것이 아니기 때문입니다.
나는 사과를 맛있게 먹었다라는 문장에서도 여러 관계가 동시에 존재합니다. 나는과 먹었다 사이에는 주어와 동작의 관계가 있을 수 있습니다. 사과를과 먹었다 사이에는 목적어와 동작의 관계가 있을 수 있습니다. 맛있게와 먹었다 사이에는 수식 관계가 있을 수 있습니다.
하나의 Attention이 모든 관계를 한 번에 잘 잡아내기를 기대할 수도 있습니다. 하지만 여러 head를 두면 각 head가 서로 다른 관점에서 문장을 볼 여지가 생깁니다.
어떤 head는 가까운 토큰 관계를 볼 수 있습니다.
어떤 head는 멀리 떨어진 토큰 관계를 볼 수 있습니다.
어떤 head는 주어와 동사의 연결을 볼 수 있습니다.
어떤 head는 수식어와 중심 단어의 연결을 볼 수 있습니다.
물론 실제로 각 head가 항상 사람이 이해하기 쉬운 역할로 깔끔하게 나뉘는 것은 아닙니다. 하지만 구조적으로는 여러 관계를 병렬로 볼 수 있는 능력을 갖게 됩니다.
Multi-Head Attention의 흐름은 다음과 같습니다.
head_1 = Attention(Q_1, K_1, V_1)
head_2 = Attention(Q_2, K_2, V_2)
...
head_h = Attention(Q_h, K_h, V_h)
output = concat(head_1, head_2, ..., head_h)W_O각 head는 자기만의 W_Q, W_K, W_V를 가집니다. 그래서 같은 입력 문장을 보더라도 서로 다른 공간에서 관계를 계산합니다. 마지막의 W_O는 여러 head에서 나온 정보를 다시 하나의 벡터로 섞는 가중치입니다.
비유하자면 한 명의 독자가 문장을 읽는 대신, 여러 명의 독자가 각자 다른 관점으로 문장을 읽고 토론한 뒤 하나의 정리본을 만드는 것입니다.
Self-Attention이 모르는 것: 순서
Self-Attention은 토큰 사이의 관계를 잘 계산하지만, 그 자체만으로는 순서를 알기 어렵습니다. Attention 계산은 기본적으로 벡터들 사이의 유사도를 봅니다. 입력 토큰의 순서를 따로 알려 주지 않으면, 모델은 어떤 토큰이 첫 번째였고 어떤 토큰이 마지막이었는지 구분하기 어렵습니다.
예를 들어 다음 두 문장은 단어는 비슷하지만 의미가 다릅니다.
고양이가 강아지를 쫓았다
강아지가 고양이를 쫓았다단어 목록만 보면 고양이, 강아지, 쫓았다가 모두 등장합니다. 하지만 순서와 조사에 따라 누가 누구를 쫓았는지가 바뀝니다. Transformer는 이런 순서 정보를 반영하기 위해 위치 정보를 추가합니다. 이를 위치 인코딩 또는 위치 임베딩이라고 부릅니다.
즉, Self-Attention은 관계를 계산하는 강력한 장치지만, 문장의 순서 감각은 별도의 위치 정보와 함께 작동할 때 더 제대로 살아납니다. 다음 장에서 살펴볼 Transformer 블록은 이 Self-Attention에 위치 정보, 잔차 연결, 정규화, 피드포워드 네트워크를 결합해 더 안정적이고 깊은 모델을 만듭니다.
LLM에서 Self-Attention이 하는 일
LLM은 거대한 Transformer입니다. 그래서 LLM의 핵심에도 Self-Attention이 있습니다. 우리가 프롬프트를 입력하면 문장은 토큰으로 쪼개지고, 각 토큰은 임베딩 벡터가 됩니다. 그다음 여러 층의 Transformer 블록을 지나면서 각 토큰의 표현은 계속 문맥화됩니다.
초기 층에서는 가까운 단어 조합이나 간단한 패턴을 볼 수 있습니다. 더 깊은 층으로 갈수록 문장 전체의 흐름, 지시문, 앞에서 언급한 개념, 답변의 방향 같은 더 복잡한 정보가 반영될 수 있습니다.
예를 들어 사용자가 이렇게 물었다고 해 봅시다.
사과는 냉장고에 넣는 게 좋을까? 이유도 알려줘.
모델이 이유라는 토큰을 처리할 때는 앞의 사과, 냉장고, 넣는, 좋을까 같은 토큰들을 함께 참고해야 합니다. 단순히 다음 단어 하나만 보는 것이 아니라, 질문 전체의 의도를 반영한 표현을 만들어야 합니다. Self-Attention은 이런 문맥 연결을 가능하게 하는 핵심 메커니즘입니다.
다만 LLM에서 다음 토큰을 예측하는 Transformer는 보통 미래 토큰을 보지 못하도록 가립니다. 예를 들어 세 번째 토큰을 예측할 때 다섯 번째 토큰을 미리 보면 안 됩니다. 이런 제한을 causal mask라고 부릅니다. 마스크는 Attention 점수 중에서 보면 안 되는 위치를 막아 주는 장치입니다. 이 개념은 Transformer 구조를 이해할 때 다시 만나게 됩니다.
계산 흐름을 한 줄로 다시 보기
Self-Attention은 처음에는 복잡해 보입니다. Query, Key, Value, softmax, 가중합, multi-head 같은 말이 한꺼번에 나오기 때문입니다. 하지만 중심 흐름은 단순합니다.
모든 토큰이 벡터로 들어온다. 각 토큰은 Query, Key, Value로 변환된다. Query와 Key를 비교해 토큰 간 관계 점수를 만든다. 점수를 softmax로 참고 비율로 바꾼다. 그 비율대로 Value를 섞어 각 토큰의 새 표현을 만든다.
이 과정을 거치면 각 토큰은 더 이상 고립된 단어 조각이 아닙니다. 문장 안의 다른 토큰들과 관계를 맺은 표현이 됩니다. 이것이 Transformer가 문맥을 읽는 가장 중요한 방식입니다.
이제 우리는 Transformer의 심장부에 들어왔습니다. 다음 장에서는 이 Self-Attention이 실제 Transformer 블록 안에서 어떻게 배치되는지 살펴봅니다. Attention만으로 끝나는 것이 아니라, 잔차 연결로 정보를 보존하고, 정규화로 학습을 안정시키고, 피드포워드 네트워크로 각 토큰의 표현을 한 번 더 가공합니다. 그 구조를 이해하면 LLM이 딥러닝 위에 어떻게 세워지는지 훨씬 선명하게 보이기 시작합니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.