Attention — ChatGPT가 ‘그 단어’가 누구를 가리키는지 아는 법

⏱ 정독 약 22분 · LLM 이론 집중코스 5편
이 글에서 잡는 것 3가지:
1. ChatGPT·Claude가 “그 단어가 문장 속 누구를 가리키는지”를 숫자로 알아내는 법
2. 그 유명한 한 줄 수식 softmax(Q·K^T/√d_k)·V고양이 문장 하나로 끝까지 읽어내기
3. 왜 LLM이 가끔 “이거”가 뭔지 헷갈리는지 — 그 한계의 정체


“울었다”의 주어가 고양이인 걸, 기계는 어떻게 아는가

문장 하나를 본다.

그 고양이는 배가 고파서 울었다.

사람은 “울었다”를 보는 순간, 머릿속에서 손이 자동으로 “고양이”로 간다. 누가 울었는지 묻지 않아도 안다.

그런데 기계한테 “울었다”는 그냥 숫자 덩어리 하나다. “고양이가 중요하다”는 직관이 0이다.

그러면 ChatGPT는, Claude는, 도대체 어떻게 “울었다”의 주어가 고양이라는 걸 알아낼까. 더 멀리 — 긴 문서를 붙여넣고 “이거 요약해줘”라고 칠 때, “이거”가 무엇인지는 어떻게 알까.

답은 한 단어다. Attention(어텐션).

이 한 줄을 풀기 위해 이 글 하나가 통째로 쓰인다. 수식은 딱 하나만 본다. 대신 그 하나는 고양이 문장으로 숫자까지 끝까지 따라간다.


Attention 이전 — 단어를 한 줄로 세워 읽던 시절

Attention이 왜 “태어났는지” 알려면, 그 전에 쓰던 방식이 어디서 아팠는지부터 봐야 한다.

2017년 전까지 주류는 RNN(Recurrent Neural Network) 이었다. RNN이 문장을 읽는 방식을 코드로 줄이면 네 줄이다.

state = [0, 0, 0, 0]          # "메모" 한 칸. 크기 고정.
for 단어 in 문장:
    state = 섞기(state, 단어)   # "메모 갱신". 그 자리에 덮어쓴다.
답 = 예측(state)              # 맨 끝 메모 하나로 답을 낸다

핵심은 가운데 줄이다. state라는 메모 한 칸을, 단어를 읽을 때마다 덮어쓴다.

놓치면 안 되는 게 셋.

① 메모는 크기가 고정이다. 단어가 5개든 5000개든 이 칸은 안 커진다.

② 단어를 따로 보관하지 않는다. “지금까지 읽은 걸 뭉뚱그린 요약값” 하나만 들고 간다.

③ 매 단어마다 그 한 칸을 갈아끼운다. 추가가 아니라 덮어쓰기.

숫자로 돌려보자. 첫 칸을 “고양이스러움”이라고 치면, 문장을 읽어 내려갈 때 메모는 이렇게 변한다.

시작        : state = [0,    0,    0,    0  ]
"고양이" 읽음: state = [0.9,  0.1,  0,    0.2]   ← 고양이 신호 0.9 들어옴
"배가"   읽음: state = [0.5,  0.7,  0.1,  0.2]   ← 0.9가 0.5로 깎임
"고파서" 읽음: state = [0.3,  0.5,  0.8,  0.1]   ← 0.5가 0.3으로
"울었다" 읽음: state = [0.15, 0.3,  0.6,  0.7]   ← 0.15. 거의 사라짐
RNN의 고정 메모 한 칸이 단어마다 덮어쓰이며 옛 신호가 0.9→0.5→0.15로 깎이는 그림
RNN의 고정 메모 한 칸이 단어마다 덮어쓰이며 옛 신호가 0.9→0.5→0.15로 깎이는 그림

이 구조엔 아픔이 둘 있었다.

아픔 ① — 순서대로만 읽으니 느리다. 두 번째 단어는 첫 번째가 끝나야 시작된다. GPU가 수천 개 있어도 줄을 세워야 한다. 그래서 모델을 크게 키우는 게 현실적으로 어려웠다.

아픔 ② — 긴 문장에서 앞이 흐려진다. 위 표의 고양이 신호를 세로로 따라가 보면 0.9에서 0.15까지 깎인다.

당시 사람들도 이 둘을 알고 있었다. 그래서 메모를 좀 더 똑똑하게 갱신하는 LSTM·GRU 같은 변종이 나왔다. 깜빡임은 늦췄지만, “한 칸에 욱여넣고 순서대로 읽는다” 는 전제 자체는 그대로였다. 근본은 안 바뀐 거다.

자기 검증 — 여기서 한 번 멈춰 보자. “RNN이 느린 이유”와 “RNN이 잘 잊는 이유”를 각각 한 문장으로 자기 입에서 말할 수 있는가. 못 하면 위 네 줄 코드로 돌아가면 된다. 느림 = 가운데 for가 순서를 강제. 잊음 = state 한 칸을 매번 덮어씀.

TL;DR — RNN은 “메모 한 칸을 매 단어마다 덮어쓰는” 구조라, 느리고(순서 강제) 옛 단어를 잘 잊는다(신호가 깎임). Attention은 이 둘을 동시에 깬다.


“희미해진다”는 건, 숫자가 진짜로 작아진다는 뜻

“잘 잊는다”는 말은 분위기 표현처럼 들린다. 그런데 진짜로 숫자가 작아지는 일이다. 고양이 신호만 떼어 세로로 보면 이렇다.

0.9  →  0.5  →  0.3  →  0.15  →  (더 길면 0에 수렴)

칸이 하나뿐인데 매 단어마다 덮어쓰니까, 첫 단어 신호가 단계를 지날수록 깎여서 거의 0이 된다. 이게 “희미해진다”의 정확한 뜻이다.

그래서 맨 끝에서 “울었다”의 답을 낼 때, 정작 주어 “고양이”를 참고하고 싶어도 메모 속 고양이 신호가 0.15로 쪼그라들어 있다. 못 끌어온다. 단어 사이가 멀수록 심해진다.

이게 추상적인 옛날이야기 같지만, 지금 매일 보는 화면에 흔적이 남아 있다.

Claude나 ChatGPT에 아주 긴 문서를 붙여넣고 질문하면, 가끔 중간쯤 내용을 놓친다. 업계에서 Lost in the Middle이라 부르는 현상이다. 뿌리가 바로 이 “먼 정보가 흐려진다” 문제의 후손이다.

모델 스펙에 “100만 토큰 컨텍스트” 같은 숫자가 자랑처럼 박히는 이유도 여기 있다. “앞 정보를 안 잃는 거리”를 늘리는 게 곧 경쟁력이라서다.


Attention 한 문장 — 누구를 얼마나 볼지 점수 매기기

RNN의 전제는 “순서대로 한 칸씩 읽어야 한다” 였다. 2017년 논문이 던진 발상은 이거 하나였다.

“왜 순서대로 읽어? 문장을 통째로 펼쳐놓고, 각 단어가 다른 모든 단어를 얼마나 볼지 한 번에 점수로 계산하면 되잖아.”

이게 Attention의 한 문장 정의다.

지금 보는 단어가, 같은 문장의 다른 단어들 중 누구를 얼마나 참고할지 0~1 점수로 매기는 것.

“울었다”를 처리할 때 — 고양이: 0.55, 배가: 0.18, 고파서: 0.18, 나머지: 0.09… 이런 식으로 비율을 매기고, 그 비율대로 정보를 섞어 온다. 모든 단어가 동시에. 줄을 세울 필요가 없다.

그게 전부다. 이 점수가 어디서 어떻게 나오는지가 이 글의 나머지다.


점수는 “두 화살표 사이”에서 나온다 — 내적

여기서 가장 많이 걸려 넘어진다. 그래서 정면으로 짚는다.

각 단어는 숫자 벡터 하나로 표현된다. 벡터는 원점에서 그 단어 위치로 가는 화살표라고 보면 된다.

자, 흔한 오해 하나.

⚠️ 흔한 오해: “각 단어의 화살표 하나로 점수가 나온다.”
아니다. 내적은 화살표 하나로는 안 된다. 반드시 두 개가 필요하다.

Attention 점수는 “원점→토큰 하나” 가 아니라, “단어 A의 화살표”와 “단어 B의 화살표” 사이에서 계산된다.

            ● 고양이
          ↗
        ↗        ← 두 화살표가 벌어진 각도가 작다 = 닮음
원점 ⦿━━━━━━━● 울었다
        ↘
          ↘
            ● 그   ← "울었다" 화살표와 각도가 크다 = 안 닮음
원점에서 뻗은 세 화살표(울었다·고양이·그), 울었다와 고양이는 각도가 작고 그와는 큰 그림
원점에서 뻗은 세 화살표(울었다·고양이·그), 울었다와 고양이는 각도가 작고 그와는 큰 그림

두 화살표의 “닮은 정도”를 재는 도구가 내적(dot product) 이다. 짝끼리 곱해서 다 더한다. 2칸 벡터로 직접 해보자.

울었다 = [1, 0]

고양이 = [0.9, 0.1] →  1×0.9 + 0×0.1 = 0.9   ← 같은 방향, 큼
그     = [0.1, 0.9] →  1×0.1 + 0×0.9 = 0.1   ← 거의 직각, 작음
반대   = [-1, 0]    →  1×(-1)+ 0×0   = -1     ← 반대 방향, 음수

규칙은 하나다. 두 화살표 각도가 작을수록(닮을수록) 내적이 크다. 그래서 내적이 “닮은 정도 = 관련도”로 쓰인다.

음수가 나온다는 점도 의미가 있다. 내적이 음수면 “이 둘은 반대 방향” = “참고하면 오히려 방해되는 관계”라는 신호다. 0은 “무관”. 양수가 클수록 “강하게 관련”. 하나의 숫자로 관련·무관·역관계를 다 표현하는 셈이다.

정리하면 이렇다.

“원점부터 토큰까지”는 화살표를 만드는 단계고, 내적은 그 화살표 둘을 맞대보는 단계다. 점수는 언제나 쌍(pair) 에서 나온다.


Q·K·V — 사실 이건 “검색”이다

이제 그 유명한 세 글자가 나온다. Q·K·V. 처음 보면 “왜 갑자기 세 글자야” 싶다.

먼저 “내가”의 정체부터 박자. Attention은 단어 하나를 새로 쓰는 장면이 쭉 이어지는 거다. 장면을 딱 하나 고정한다.

지금 장면: “울었다”를 새로 쓰는 중.

이 장면의 주인공은 “울었다”다. 나머지(그·고양이·배가)는 참고당하는 조연이다. 그러니 “내가 찾는 것”의 “내가” = “울었다”라는 단어. 장면이 바뀌어 “고양이”를 쓸 차례가 되면 “내가” = “고양이”가 된다.

이제 Q·K·V. 정공법으로 말하면 — 이건 원래 검색 용어다. 매일 쓰는 유튜브 검색으로 보자.

검색 정체 한 줄
Query (Q) 검색창에 친 글자 “고양이 우는 이유”라고 친 말
Key (K) 각 영상에 달린 제목·태그 영상이 검색에 걸리라고 달아둔 꼬리표
Value (V) 영상의 실제 내용 클릭하면 보게 되는 알맹이

검색이 도는 순서는 이렇다.

[1] 내가 친 검색어(Q)를, 모든 영상의 태그(K)와 하나씩 비교
[2] 잘 맞는 영상일수록 위로 (점수 높음)
[3] 그 영상의 실제 내용(V)을 본다
유튜브 검색창(Q) → 여러 영상의 태그(K) 매칭 → 선택된 영상 내용(V)을 가져오는 흐름
유튜브 검색창(Q) → 여러 영상의 태그(K) 매칭 → 선택된 영상 내용(V)을 가져오는 흐름

유튜브가 아니라 구글, 넷플릭스, 파일 검색 — 무엇이든 같은 구조다. 친 말(Q)을 색인(K)에 맞춰 보고, 맞으면 알맹이(V)를 꺼낸다. “검색”이라는 행위의 뼈대가 그대로 Q·K·V다.

이걸 문장 단어에 그대로 얹는다. “울었다”를 새로 쓰는 장면 = “울었다”가 검색을 한 번 돌리는 장면이다.

  • “울었다”의 Query = “내 주어랑 원인이 누구냐”는 검색어.

그리고 나머지 단어들은 각자 태그(K)와 내용(V) 을 들고 검색 대상으로 줄 서 있다.

단어 Key (걸리는 태그) Value (걸리면 줄 내용)
고양이 〈주어 후보〉 「주어는 고양이」
배가 〈원인 후보〉 「원인은 배고픔」
〈관사〉 「거의 정보 없음」

“울었다”의 검색어를 각 단어의 태그와 비교 → 고양이·배가가 잘 걸림 → 그 두 단어의 내용(V)을 많이 섞어서 “울었다”를 다시 쓴다.

Q·K·V는 어디서 나오나

한 가지 미리 막아둘 의문. “그 검색어(Q)와 태그(K)는 누가 정했나?”

각 단어는 처음에 벡터 하나로 시작한다. 거기에 세 개의 서로 다른 변환기를 통과시켜 Q·K·V 세 벌을 뽑는다. 이 변환기(흔히 W_Q, W_K, W_V로 쓴다)가 “무엇을 검색어로 삼고, 무엇을 태그로 내걸지” 를 정한다. 그리고 이 변환기는 학습으로 빚어진다 — 그 과정은 이 코스 2편(학습)의 주제라 여기선 “세 변환기가 이미 빚어져 있다”까지만 잡으면 된다.

왜 한 단어가 Q·K·V를 셋 다 갖나

자연스러운 의문이다. 답은 — 문장 안 모든 단어가 동시에, 서로를 검색하기 때문이다.

“울었다”가 검색할 땐 “울었다”가 검색하는 쪽이라 자기 Query를 쓴다. 그런데 “고양이”가 자기 수식어를 검색할 땐, 이번엔 “울었다”가 검색당하는 영상이 되어 “울었다”의 Key가 걸리고 Value가 건네진다.

그러니 모든 단어가 입장이 수시로 바뀐다. 어떤 순간엔 검색자, 어떤 순간엔 검색 대상.

Query = 남을 검색할 때 쓸 "내 검색어"
Key   = 남이 나를 검색할 때 "내가 걸릴 태그"
Value = 걸렸을 때 "내가 건넬 내용"

한 단어가 검색자이자 동시에 검색 대상이라서, Q·K·V 셋이 다 필요하다.

K랑 V를 왜 따로 두나

유튜브로 보면 당연하다. 영상 태그(K) 와 영상 내용(V) 은 딴 물건이다. 태그는 검색에 걸리라고 단 짧은 꼬리표고, 내용은 막상 보면 나오는 진짜 알맹이다.

“검색에 걸리기 좋은 형태”와 “실제로 전달할 내용”이 다르니까, 둘을 떼어 각각 따로 다듬는다. 그래야 모델이 “어떻게 하면 잘 검색되나(K)”와 “걸리면 무엇을 줄까(V)”를 독립적으로 학습할 수 있다.

자기 검증 — “고양이”의 K와 V가 각각 무엇이었는지 표를 안 보고 말해 보자. K = 〈주어 후보〉라는 태그, V = 「주어는 고양이」라는 알맹이. 이 둘이 다른 물건이라는 게 손에 잡히면, K/V 분리는 끝난 거다.


수식 딱 한 줄 — 우리가 손으로 다 한 것

이제 그 유명한 한 줄이 나온다.

Attention(Q, K, V) = softmax( Q·K^T / √d_k ) · V

처음 보면 “이게 끝?” 싶을 만큼 짧다. 그런데 한 글자씩 뜯어보면 전부 방금까지 한 얘기다.

조각 하는 일
Q·K^T 모든 검색어 × 모든 태그 = 점수표를 한 방에
/ √d_k 점수 크기를 진정 (안 그러면 다음 단계가 한 곳에 쏠림)
softmax 점수를 0~1 비율로 (← 3편 “추론”에서 푼 그 softmax)
· V 비율대로 내용을 가중합

(softmax 자체의 동작은 3편 「추론 — ChatGPT 작동 원리」에서 끝까지 풀었다. 여기선 그 결과만 가져다 쓴다.)

Q·K^T만 한 번 더 보자. 지금까지 우리는 “울었다 하나”가 검색하는 장면만 봤다. 실제론 모든 단어가 동시에 검색한다. 그걸 한 단어씩 N번 돌리면 느리다. 그래서 행렬 곱 한 번으로 묶는다.

# 한 단어씩 했다면 (이중 for문)
for i in 단어들:
    for j in 단어들:
        점수[i][j] = 내적( Q[i], K[j] )

# 똑같은 걸 한 줄로 (행렬 곱)
점수 = Q @ K^T

이중 for문을 행렬 곱 한 줄로 바꾼 것 = 모든 단어가 모든 단어를 동시에 검색한 결과를 한 방에 얻은 것이다. RNN처럼 줄 세울 필요가 없다. 아픔 ①(느림)이 여기서 풀린다. 그리고 행렬 곱은 GPU가 가장 잘하는 일이라, 이 한 줄이 곧 “GPU에 통째로 태울 수 있다”는 뜻이 된다.


고양이 문장으로 끝까지 — 숫자 관통

말로만 하면 안 박힌다. 문장 하나를 Q·K·V부터 마지막 가중합까지 숫자로 끝까지 따라가 보자.

약속 두 개. 문장을 4토큰으로 압축한다: 그 / 고양이 / 배고파서 / 울었다. 벡터는 2칸으로, 각 칸의 뜻을 [ 주체성, 원인성 ] 으로 정한다. 첫 칸은 “주어가 될 만함”, 둘째 칸은 “원인·상태 정보”.

무대 — 4토큰의 Q·K·V

토큰 Q (찾는 검색어) K (걸리는 태그) V (건넬 내용)
[0, 0] [0, 0] [0, 0]
고양이 [0.5, 1] [2, 0] [10, 0]
배고파서 [0, 0.5] [0, 2] [0, 10]
울었다 [2, 1] [0.5, 0.5] [1, 1]

고양이의 K=[2,0]은 “주체성 태그로 강하게 걸린다”, V=[10,0]은 “주어=고양이”를 건넨다. 울었다의 Q=[2,1]은 “주체(2)를 강하게, 원인(1)을 좀 찾는다”.

STEP 1 — Q·K^T로 점수표를 한 방에

행 = 검색하는 단어, 열 = 검색당하는 단어.

            그    고양이  배고파서  울었다
그          0.0    0.0    0.0     0.0
고양이       0.0    1.0    2.0     0.75
배고파서     0.0    0.0    1.0     0.25
울었다       0.0    4.0    2.0     1.5
4×4 attention 점수표 히트맵,
4×4 attention 점수표 히트맵, “울었다” 행에서 고양이 칸이 가장 진한 그림

행이 4개라는 점을 놓치지 말자. “울었다” 행만 일하는 게 아니라, 고양이 행·배고파서 행도 동시에 자기 검색을 돌린다. 한 번의 행렬 곱이 네 단어의 검색을 한꺼번에 처리한 거다. 여기선 가장 또렷한 “울었다” 행 [0, 4.0, 2.0, 1.5] 만 끝까지 따라간다.

“울었다” 행을 손으로 검산하면:

울었다 Q=[2,1]
  · 고양이 K=[2,0]   = 2×2 + 1×0 = 4.0
  · 배고파서 K=[0,2] = 2×0 + 1×2 = 2.0
  · 울었다 K=[.5,.5] = 2×.5+ 1×.5 = 1.5
  · 그 K=[0,0]       = 0

STEP 2 — /√d_k로 점수 진정

칸이 2개니 √2 ≈ 1.41로 나눈다.

[0, 4.0, 2.0, 1.5]  ÷ 1.41  =  [0, 2.83, 1.41, 1.06]

왜 이 짓을 하나. 나눈 것과 안 나눈 것의 결과를 눈으로 비교하면 보인다.

안 나누면 → 고양이 0.81,  배고파서 0.11   ← 고양이가 거의 독식
나누면   → 고양이 0.68,  배고파서 0.16   ← 1등 쏠림이 누그러져
                                          2등(원인)도 살아남음

√d_k로 나누는 건 “1등에게 확률이 다 쏠리지 않게 눌러주는 것”이다. 그래야 주어(고양이)뿐 아니라 원인(배고픔) 정보도 죽지 않고 섞인다. 벡터 칸 수(d_k)가 커질수록 내적값이 자연히 커지기 때문에, 그 크기를 차원에 맞춰 진정시키는 장치다. 논문이 이름에 일부러 scaled(스케일된)를 박아 넣은 이유가 이것이다.

STEP 3 — softmax로 비율 만들기

[0, 2.83, 1.41, 1.06]  →  softmax  →  그 0.04 | 고양이 0.68 | 배고파서 0.16 | 울었다 0.12

합은 1.00. “울었다”가 누구를 얼마나 볼지가 확정됐다.

STEP 4 — ·V로 비율대로 내용 가중합

0.04 × [0, 0]    = [0,    0   ]   ← 그: 정보 없음
0.68 × [10, 0]   = [6.80, 0   ]   ← 고양이: 주어 정보 듬뿍
0.16 × [0, 10]   = [0,    1.60]   ← 배고파서: 원인 정보 조금
0.12 × [1, 1]    = [0.12, 0.12]   ← 울었다: 자기 정보 조금
────────────────────────────────
            합 =   [6.92, 1.72]
비율(0.04/0.68/0.16/0.12)을 각 V에 곱해 더해 [6.92,1.72]가 되는 가중합 흐름
비율(0.04/0.68/0.16/0.12)을 각 V에 곱해 더해 [6.92,1.72]가 되는 가중합 흐름

[6.92, 1.72] 이 “울었다”의 새 벡터다. 원래 맥락 없던 동사 하나가, 이제 “주어는 고양이(6.92), 원인은 배고픔(1.72)이 녹아든 울었다” 로 다시 태어났다.

왜 이게 “고양이가 주어”를 뜻하나

새 벡터의 출처를 칸별로 쪼개 보면 분명해진다.

첫 칸 6.92  =  6.80 (고양이) + 0.12 (자기)      → 거의 다 고양이발 (98%)
둘째칸 1.72  =  1.60 (배고파서) + 0.12 (자기)    → 거의 다 배고픔발 (93%)

6.92가 큰 게 핵심이 아니다. 그 큰 값을 고양이의 V가 부어 넣었다는 게 핵심이다. 고양이가 0.68로 가장 큰 비중을 먹었으니, 고양이의 내용이 결과를 지배했다.

가장 많이 본 단어(고양이)의 내용이 새 벡터를 끌어당긴다. 그래서 새 벡터의 주체성 칸이 고양이로 꽉 찬다. 다음 단계가 이 벡터를 읽을 때 “울었다의 주어 = 고양이”로 해석한다.

“누가 울었어?”라고 물으면, 모델은 “울었다” 자리의 벡터를 꺼내 보고, 주체성 칸이 고양이로 채워져 있으니 “고양이”라고 답한다. “주어는 고양이”라는 사실이 [6.92, 1.72]라는 숫자 안에 새겨진 것이다.

FROM 바이브코딩 태일러 · 매주 월요일
바빠도 5분이면 읽는
이번 주 AI 흐름
태일러가 고른 AI 뉴스 TOP 3 · 벤치마크 순위 · 급상승 키워드 · 판단 기준 보너스. 광고 없는 클린 미디어입니다.

무료로 받아보기 →

선착순 100명 영구 무료 · 언제든 해지

0점인데 왜 0%가 아닌가 — softmax의 성질

STEP 3에서 날카로운 의문이 하나 생긴다. “그”의 점수는 0이었는데, softmax 후엔 왜 0.04가 남았을까?

softmax는 점수를 그냥 쓰는 게 아니라 지수함수 exp를 한 번 통과시킨다. 그리고 exp(0) = 1 이다. 0이 아니다.

점수        :  [0,     2.83,   1.41,   1.06 ]
exp 통과    :  [e^0,   e^2.83, e^1.41, e^1.06]
            =  [1.00,  16.95,  4.10,   2.89 ]
                 ↑ 점수 0 → e^0 = 1
합 = 24.94
비율(÷합)   :  [0.04,  0.68,   0.16,   0.12 ]
                 ↑ 1 / 24.94 = 0.04

직관으로 박자.

softmax에서 0점은 “관련 없음”이 아니라 그냥 바닥 근처일 뿐이다. 진짜로 0%가 되려면 점수가 음의 무한대(−∞) 여야 한다.

확인용 사고실험. 네 점수가 전부 0이면?

[0,0,0,0] → exp → [1,1,1,1] → 비율 → [0.25, 0.25, 0.25, 0.25]

전부 0점이면 균등하게 1/4씩 나눠 본다. “정보가 없으면 다 똑같이 조금씩 본다”는 거다. “그”가 0.04를 받은 건 0점이라서가 아니라, 다른 애들 점수가 높아 상대적으로 밀린 결과다.

그래서 생기는 성질 하나. softmax 결과는 절대 완전한 0이 안 된다. exp는 어떤 실수를 넣어도 양수를 뱉으니까. 즉 Attention은 모든 단어를 조금씩은 본다. 완전히 무시하는 단어가 없다.

(진짜 0%로 막고 싶을 땐 점수에 −무한대를 박아 넣는다. 그걸 쓰는 자리가 있는데 — 그건 다음 편에서 다룬다.)


거리가 아니라 내용이다 — 그리고 가끔 틀린다

마지막으로 가장 깊은 자리. 후보가 둘 이상일 때 어떻게 고르나.

문장을 살짝 바꿔보자.

주인이 부재해서 그 고양이는 배가 고파서 울었다.

이제 “울었다”의 주어 후보가 둘이다. 고양이도, 주인도 주어가 될 수 있다. 어떻게 고양이로 좁힐까.

여기서 먼저 무너뜨릴 오해 하나. “가까운 단어를 본다”가 아니다. 어순을 바꿔 “고양이는 주인이 부재해서 배가 고파서 울었다”로 써도, 고양이가 멀리 떨어져 있어도 주어로 잡힌다.

Attention은 거리를 점수에 직접 넣지 않는다. “울었다”의 검색어를 모든 단어와 똑같이 맞대본다. 1칸 옆이든 10칸 밖이든 동등하게. 이게 RNN을 이긴 결정적 지점이다.

그럼 무엇이 고양이와 주인을 가르나. 검색어(Q)에 문맥이 녹아있다.

“울었다”의 Query는 단순 “주어 찾기”가 아니라 “배가 고파서 우는 주체를 찾기” 다. “부재해서”의 Query는 “부재하는 주체를 찾기” 고. 검색어가 다르니 같은 후보 둘이 다르게 걸린다.

"울었다"가 검색 (배고파 우는 주체)   → 고양이 0.62 | 주인 0.20  → 고양이 당첨
"부재해서"가 검색 (부재하는 주체)     → 주인 0.70  | 고양이 0.12 → 주인 당첨
같은 두 후보(고양이·주인)가 검색어에 따라 다르게 걸리는 두 비율표
같은 두 후보(고양이·주인)가 검색어에 따라 다르게 걸리는 두 비율표

“부재해서”가 사람 주어를 선호하는 그 “압도적 확률”은 어디서 왔나. 수백억 문장으로 학습하면서 “부재하다+주어는 사람”이 압도적으로 많았던 통계가 검색어에 녹아든 것이다. Attention은 문법 규칙을 외운 게 아니라, “이런 검색어엔 이런 게 걸리더라”는 통계를 빨아들였다.

그리고 — 여기가 중요하다 — Attention은 틀릴 수 있다.

“부재의 주어를 고양이로, 울었다의 주어를 주인으로” 잡아도 문법적으로는 가능하다. 중의성이 높은 문장에선 진짜로 잘못된 연결을 잡을 수 있다.

⚠️ Attention은 문법 파서가 아니다. “문법적으로 가능한 여러 해석” 중에서 학습된 확률로 가장 그럴듯한 걸 고르는 장치다. 보통은 맞지만, 보장은 없다.


그래서 프롬프트에서 뭐가 달라지나

여기까지가 원리다. 그런데 이걸 알면 매일 쓰는 프롬프트가 실제로 달라진다.

① “이거/저거”를 줄인다. 긴 프롬프트에서 “이걸 그렇게 해줘” 라고 하면, LLM이 “이거”가 뭘 가리키는지 검색한다. 후보가 비등하면 틀린다. 대상을 또박또박 다시 쓰면 검색어(Q)가 또렷해져 정확도가 오른다.

② 중요한 지시를 멀리 떼어놓지 않는다. 긴 문서 한가운데 핵심을 묻으면 Lost in the Middle 에 걸리기 쉽다. 거리 자체가 점수를 깎는 건 아니지만, 아주 긴 맥락에선 실전상 중간이 흐려진다.

③ “왜 같은 질문에 답이 흔들리나”가 보인다. Attention은 확률 선택이다. 후보가 비등한 자리에선 매번 다른 쪽이 이길 수 있다. 모델이 “헷갈릴 만한 문장”을 내가 던졌는지 의심하게 된다.

원리를 모르면 이 셋은 “그냥 팁”이다. 원리를 알면 “왜” 가 붙어서, 처음 보는 상황에서도 스스로 판단할 수 있게 된다.


반론과 한계 — Attention이 만능은 아니다

정공법이라면 한계도 같이 말해야 한다.

  • 모든 단어를 조금씩 본다 = 잡음이 섞인다. softmax가 완전한 0을 안 만들기 때문에, 관련 없는 단어도 0.04쯤은 끼어든다. 문장이 길수록 이 “약한 잡음”이 쌓인다.
  • 점수는 통계지 진실이 아니다. “부재해서 → 사람”은 데이터의 다수결일 뿐, 그 문장의 진짜 의도와 다를 수 있다. 중의적이면 그대로 틀린다.
  • 한 번의 검색으로는 부족하다. 지금 본 건 Attention “한 번”이다. 실제 문장엔 주어·원인·시제·지시어가 동시에 얽혀 있어, 한 번의 검색으론 다 못 잡는다. 이걸 여러 벌·여러 층으로 쌓아 푸는 게 다음 편의 일이다.

한계를 안다는 건 약점이 아니라, 언제 모델을 의심해야 하는지를 아는 것이다.


핵심 3가지 정리

  1. Attention = 누구를 얼마나 볼지 점수 매기기. 점수는 두 단어 화살표의 내적(닮을수록 큼)에서 나오고, softmax로 비율이 된다.
  2. Q·K·V는 검색이다. Query(검색어)·Key(태그)·Value(내용). 한 단어가 검색자이자 검색 대상이라 셋 다 갖는다. 한 줄 수식 softmax(Q·K^T/√d_k)·V는 이 검색을 문장 전체에 행렬 곱으로 한 번에 돌린 것.
  3. 점수는 거리가 아니라 내용으로 정해진다. 그래서 먼 단어도 잡지만, 후보가 비등하면 틀릴 수도 있다. Attention은 문법 규칙이 아니라 학습된 확률 선택이다.

다음 편 예고 — Transformer

이 글은 Attention “한 번”까지다. 그런데 실제 모델은 이 검색을 한 번만 하지 않는다.

  • 한 번에 한 종류의 관계(주어)만 보면 원인·시제·지시어는 누가 보나?
  • “dog bites man”과 “man bites dog”처럼 순서가 뒤바뀌면?
  • 한 층으로 안 되면 몇 층을 쌓나?

이 검색을 여러 벌, 여러 층으로 쌓아 완성한 기계가 Transformer다. Attention이 엔진이라면, Transformer는 그 엔진을 얹어 완성한 자동차다. 다음 편에서 그 조립을 끝낸다.


참고: “Attention Is All You Need”(2017) · 3편 「추론 — ChatGPT 작동 원리」 · mathbullet 수식 해설 · Jay Alammar “The Illustrated Transformer”

著者: VibeCoding Tailor / 운영: 태일러의 은신처(shuntailor.net)

바이브코딩 태일러
바이브코딩 태일러
AI의 작동 원리와 비즈니스 적용을 일본어·한국어로 기록합니다. 매주 월요일 뉴스레터 발행 중.
뉴스레터 구독하기 →

댓글

JAKO