1단계 첫 논문. 이 글은 Vaswani 외, Attention Is All You Need arXiv v7(2023-08-02)을 직접 읽고 썼다. 대상은 2017년 원래의 encoder-decoder Transformer(인코더-디코더 변환기) 다. 오늘의 GPT(Generative Pre-trained Transformer, 생성형 사전학습 Transformer), BERT(Bidirectional Encoder Representations from Transformers, Transformer의 양방향 인코더 표현), Llama와 이름은 이어지지만 구조·학습 데이터·정렬 방식까지 같다는 뜻은 아니다. 공개된 저자 코드가 가리키는 Tensor2Tensor도 고정 commit
bafdc1b기준으로 확인했다.
이 글에서 얻을 답과 범위
이 논문 이전의 번역 모델은 대개 RNN(Recurrent Neural Network, 순환 신경망)이 단어를 왼쪽부터 하나씩 읽었다. Transformer는 이 질문을 던진다.
문장 속 모든 단어를 한꺼번에 놓고, 지금 단어가 무엇을 참고할지 직접 고르게 하면 순차 계산을 버릴 수 있지 않을까?
답이 self-attention(자기 어텐션)이다. 각 토큰(token, 모델이 처리하는 문자 조각)이 같은 문장 속 다른 토큰에 점수를 매기고, 중요한 정보의 가중 평균을 가져온다. 하지만 attention만 쓰면 “첫 번째”와 “마지막”의 순서를 모른다. 그래서 positional encoding(위치 인코딩), 미래를 못 보게 하는 causal mask(인과 마스크), 여러 관점을 병렬로 보는 multi-head(멀티 헤드), 정보 변환을 위한 FFN(Feed-Forward Network, 피드포워드 네트워크)이 함께 필요하다.
이 글을 다 읽으면 다음을 직접 설명할 수 있어야 한다.
- RNN의 무엇이 학습 병렬화와 긴 거리 의존성에 문제였는가?
Attention(Q, K, V) = softmax(QKᵀ / √d_k)V의 모든 기호는 무엇이며, 왜√d_k로 나누는가?- self-attention, encoder-decoder attention, masked self-attention은 Q(Query, 질의)·K(Key, 키)·V(Value, 값)가 어디서 오는가?
- decoder(디코더)가 학습 때는 병렬인데 생성 때는 한 토큰씩 순차인 이유는 무엇인가?
- positional encoding, residual connection(잔차 연결), LayerNorm(레이어 정규화), FFN은 각각 어떤 실패를 막는가?
- Transformer의
O(n²)긴 문맥 비용과 KV cache(Key-Value cache, 키-값 캐시)는 제품 지연 시간·GPU 메모리와 어떻게 연결되는가?
범위: 원 논문의 기계 번역 Transformer와 그 공개 Tensor2Tensor 구현 경로를 설명한다. 2017 논문의 실험을 재학습하거나 최신 거대 언어 모델(Large Language Model, LLM) 성능을 재현하지는 않는다. BERT의 encoder-only(인코더 전용), GPT의 decoder-only(디코더 전용), RoPE(Rotary Position Embedding, 회전 위치 임베딩), FlashAttention은 이후 글에서 다룰 별도 발전이다.
먼저 외울 한 문장
Transformer는 각 토큰이 문장 전체의 다른 토큰을 한 번에 참고하는 self-attention을 층층이 쌓고, 위치 정보와 미래 차단 마스크를 더해 RNN 없이도 번역·생성을 수행한 모델이다.
왜 필요한가: RNN은 문장을 줄 서서 읽어야 했다
12살 비유: 한 명의 전달자와 원탁회의
문장 민수는 학교에 갔고, 그는 책을 읽었다.를 이해한다고 해 보자.
RNN: 앞 단어의 메모를 다음 단어에게 한 명씩 전달한다.
민수 → 는 → 학교 → 에 → ... → 그는
Self-attention: “그는”이 모든 단어를 한꺼번에 보고
“민수”에 큰 점수를 주어 정보를 가져온다.
RNN의 hidden state(은닉 상태) h_t는 이전 상태 h_(t-1)와 현재 입력 x_t로 만들어진다.
h_t = f(h_(t-1), x_t)
그러므로 t번째 단어를 계산하려면 t-1번째가 끝나야 한다. 문장 한 개 안에서도 단어 수 n만큼 순차 단계가 필요하다. GPU(Graphics Processing Unit, 그래픽 처리 장치)는 같은 종류의 큰 행렬 계산을 동시에 많이 할 때 강한데, RNN의 이 줄서기는 그 장점을 제한한다.
논문 확인: 원 논문 §1은 RNN·LSTM(Long Short-Term Memory, 장단기 기억)·GRU(Gated Recurrent Unit, 게이트 순환 유닛) 기반 sequence transduction(입력 시퀀스를 출력 시퀀스로 바꾸는 작업)의 순차 계산이 긴 문장에서 병렬화를 막는다고 설명한다. Transformer는 recurrence(순환)와 convolution(합성곱)을 모두 제거하고 attention만으로 입력·출력의 전역 의존성을 연결했다.
긴 거리 문제는 “기억 공간”만의 문제가 아니다
RNN이 민수 정보를 그는까지 옮기려면 여러 상태 전이를 거친다. 학습 시 오차의 gradient(기울기)도 그 긴 경로를 거꾸로 지나가야 한다. 멀리 떨어진 관계일수록 학습 신호가 약해지거나 불안정해질 수 있다.
Transformer의 self-attention 한 층에서는 위치 i가 위치 j를 직접 본다. 두 위치 사이의 최대 path length(신호가 지나가는 단계 수)가 O(1)이다. 원 논문 Table 1은 self-attention 층의 순차 연산 수를 O(1), RNN을 O(n)으로 비교한다. 단, 이 이득에는 모든 쌍을 비교하는 n × n 표의 비용이 따라온다. “병렬이므로 무조건 싸다”는 결론은 틀리다.
무엇인가: 원래 Transformer는 번역용 encoder-decoder다
입력을 읽는 쪽과 답을 쓰는 쪽
원 논문의 Transformer는 sequence-to-sequence(시퀀스-대-시퀀스) 기계 번역 모델이다.
영어 입력: [I] [love] [books] [<EOS>]
│
Encoder 6층
│ 각 영어 토큰의 문맥 표현(memory)
▼
Decoder 6층
│
한국어 출력: [<BOS>] [나는] [책을] [좋아한다] [<EOS>]
- encoder(인코더): 입력 전체를 읽어 각 입력 토큰의 문맥 표현을 만든다.
- decoder(디코더): 이미 쓴 출력과 encoder memory를 보고 다음 출력 토큰을 하나씩 예측한다.
<BOS>(Beginning Of Sequence, 시작 토큰): 출력 시작을 알리는 특수 토큰이다.<EOS>(End Of Sequence, 종료 토큰): 출력 종료를 알리는 특수 토큰이다.
여기서 “Transformer = GPT”가 아니다. GPT 계열은 보통 decoder만 남긴 decoder-only 구조이고, BERT 계열은 encoder만 쓰는 encoder-only 구조다. 같은 attention 원리를 다른 입출력 문제에 맞춰 잘라 쓴 친척들이다.
원 논문 base 설정을 먼저 지도에 꽂기
원 논문 §3과 Table 3의 base model은 다음 크기다.
| 항목 | base model 값 | 뜻 |
|---|---|---|
| encoder layers / decoder layers | 6 / 6 | 같은 블록을 6번씩 쌓는다. |
d_model | 512 | 토큰 벡터의 폭, 즉 채널 수다. |
attention heads h | 8 | 서로 다른 8개 관점으로 attention한다. |
d_k, d_v | 64, 64 | 각 head의 key/query와 value 폭이다. 512 / 8 = 64다. |
d_ff | 2048 | FFN 안쪽에서 잠시 넓히는 차원이다. |
| dropout | 0.1 | 학습 중 일부 연결을 무작위로 끈다. |
| label smoothing | 0.1 | 정답 확률을 100%로 몰지 않게 한다. |
| parameter count | 약 65M | Table 3의 base 행 기준이다. |
이 숫자는 2017 번역 실험의 설정이다. 현대 7B, 70B, 671B 모델의 하이퍼파라미터를 이 표로 설명하면 안 된다.
선행 개념: 벡터·확률·행렬 shape부터 닫기
1. 토큰은 숫자 ID에서 embedding 벡터가 된다
컴퓨터는 books라는 문자열을 바로 곱셈하지 못한다. 먼저 tokenizer(토크나이저)가 문자열을 정수 ID로 나눈 뒤, embedding(임베딩, 단어 의미를 담은 실수 벡터) 표에서 d_model=512 길이 벡터를 찾는다.
원 논문은 WMT(Workshop on Machine Translation, 기계 번역 평가) 2014 English-German에서 약 450만 문장 쌍을 BPE(Byte Pair Encoding, 바이트 쌍 인코딩)로 처리하고, source-target가 공유하는 약 3만 7천 토큰 vocabulary(어휘표)를 썼다(§5.1). BPE는 자주 함께 나오는 문자 조각을 합쳐 un + break + able처럼 희귀 단어도 작은 조각으로 표현하는 방법이다. 단어 전체만 어휘에 넣으면 새 단어에 약하고, 문자 하나씩만 쓰면 시퀀스가 너무 길어지는 사이를 고른다.
embedding만 있으면 고양이 개와 개 고양이는 같은 두 벡터의 집합이라 순서를 모른다. 그래서 다음 절의 위치 벡터를 더한다.
2. Q, K, V는 도서관 검색 비유다
attention은 query 하나와 key-value 쌍 묶음을 받아 value들의 가중 평균을 돌려준다.
Query(Q): 지금 이 토큰이 찾는 질문 카드
Key(K): 각 토큰이 “나는 이런 정보”라고 붙인 색인 카드
Value(V): 실제로 가져올 내용 카드
예를 들어 그는의 Q가 민수의 K와 잘 맞으면, 민수의 V에 큰 weight(가중치)를 준다. Q/K/V는 사람이 작성한 문법 규칙이 아니다. 같은 입력 표현 X에 서로 다른 학습 행렬을 곱해 만든다.
X: [batch B, sequence length n, d_model]
Q = XW_Q, K = XW_K, V = XW_V
B는 한 GPU에서 함께 계산하는 문장 수다. n은 토큰 수다. 행렬 곱을 한 번에 하므로 학습에서 문장 안 모든 위치의 Q/K/V를 동시에 만들 수 있다.
3. softmax는 점수를 합이 1인 비율로 바꾼다
숫자 점수 [2, 1, 0]은 확률이 아니다. softmax는 큰 점수를 더 큰 비율로 만들고 모든 비율 합을 1로 만든다.
softmax(z_i) = exp(z_i) / Σ_j exp(z_j)
attention weight가 [0.7, 0.2, 0.1]이면 출력은 세 V를 70%, 20%, 10% 섞은 벡터다. 이 weight는 “원인”이나 “증명”이 아니라 모델의 계산 계수다. attention map 하나만 보고 모델이 문법을 완벽히 이해했다고 단정하면 안 된다.
4. residual connection과 LayerNorm은 깊은 층을 학습 가능하게 한다
각 sublayer(하위 층)는 입력 x를 완전히 덮어쓰지 않고 다음처럼 더한다.
y = LayerNorm(x + Sublayer(x))
residual connection은 원본 정보와 gradient가 지나는 짧은 길을 남긴다. LayerNorm은 한 토큰 벡터의 feature 차원들의 평균과 분산을 이용해 스케일을 안정화하고, 학습 가능한 γ, β로 다시 조절한다.
μ = 평균(x의 d개 feature), σ² = 분산(x의 d개 feature)
LayerNorm(x) = γ × (x - μ) / sqrt(σ² + ε) + β
원 논문은 LayerNorm(x + Sublayer(x))인 post-LN(post-LayerNorm, 잔차를 더한 뒤 정규화) 순서를 썼다. 이후 모델들이 pre-LN(pre-LayerNorm, 정규화 후 sublayer) 등 다른 순서를 쓰므로, “Transformer는 항상 이 순서”라고 외우면 안 된다.
밑바닥 원리: scaled dot-product attention을 손으로 계산하기
식 (1)의 모든 기호
원 논문 §3.2.1의 핵심 식은 다음이다.
Attention(Q, K, V) = softmax(QKᵀ / √d_k) V
| 기호 | shape 예시 | 하는 일 |
|---|---|---|
Q | [n_q, d_k] | 질문 위치마다 무엇을 찾는지 |
K | [n_k, d_k] | 각 후보 위치가 무엇을 갖고 있는지 |
V | [n_k, d_v] | 후보에서 실제로 가져올 정보 |
QKᵀ | [n_q, n_k] | 모든 질문-후보 쌍의 유사도 표 |
√d_k | 스칼라 | 점수 크기를 안정화하는 나눗셈 |
softmax(...) | [n_q, n_k] | 각 질문 행의 합이 1인 가중치 |
| 결과 | [n_q, d_v] | 가중 평균된 새 표현 |
QKᵀ에서 i행 j열은 i번째 query와 j번째 key의 dot product(내적)다. 둘이 비슷한 방향이면 큰 양수, 관련이 적으면 작은 값이 된다.
왜 √d_k로 나누는가
q와 k의 각 성분이 평균 0, 분산 1이고 서로 독립이라고 단순 가정해 보자.
q · k = q₁k₁ + q₂k₂ + ... + q_dk k_dk
Var(q · k) = d_k
Std(q · k) = √d_k
d_k가 커질수록 내적의 절댓값이 커진다. 그러면 softmax가 거의 [1, 0, 0, ...]처럼 포화되어 작은 변화에도 gradient가 거의 사라질 수 있다. 원 논문은 이 현상을 피하려고 √d_k로 나눴다고 설명한다. d_k=64면 √d_k=8이다. 이 한 줄이 large model에서 학습 안정성을 지키는 이유다.
mask는 softmax 전에 더해야 한다
decoder의 i번째 위치는 아직 생성하지 않은 i+1 이후 토큰을 보면 안 된다. 원 논문은 illegal connection의 attention logit(softmax 직전 점수)을 −∞로 두어 softmax 후 weight가 0이 되게 한다.
causal mask for length 4
query 0: [ 0, -∞, -∞, -∞]
query 1: [ 0, 0, -∞, -∞]
query 2: [ 0, 0, 0, -∞]
query 3: [ 0, 0, 0, 0]
softmax 후에 미래 weight를 0으로 만들면 남은 weight 합이 1보다 작아진다. 값의 크기가 몰래 줄고 확률 분포가 아니게 된다. 이 작은 순서 차이가 training leakage(학습 시 미래 정답을 훔쳐봄)를 막는 핵심이다.
multi-head는 여러 개의 작은 검색기를 병렬로 둔다
한 head가 512차원 전체에서 하나의 가중 평균만 만들면 서로 다른 관계가 섞일 수 있다. 원 논문은 Q/K/V를 h번 다른 행렬로 투영해 병렬 attention한 뒤 붙인다.
head_i = Attention(QW_i^Q, KW_i^K, VW_i^V)
MultiHead(Q, K, V) = Concat(head_1, ..., head_h) W^O
base model에서는 h=8, 각 head의 d_k=d_v=64다. 그래서 8개 head를 합친 폭은 8 × 64 = 512이고 마지막 W^O가 다시 d_model 공간으로 섞는다. head는 “첫째는 주어, 둘째는 목적어”처럼 사람에게 지정된 역할이 아니다. 학습 결과 서로 다른 representation subspace(표현 부분공간)·위치 관계를 볼 기회를 주는 구조다.
FFN은 토큰별로 깊게 가공하는 작업대다
attention이 “어디의 정보를 가져올지”를 정하면 FFN은 각 위치 벡터를 같은 작은 신경망으로 변환한다.
FFN(x) = max(0, xW₁ + b₁)W₂ + b₂
512 → 2048 → 512 # base model
이 FFN은 위치 간 정보를 섞지 않고 각 토큰에 독립적으로 적용된다. 위치를 섞는 것은 attention, 각 위치의 feature를 비선형으로 조합하는 것은 FFN이라고 나누어 기억하면 된다. 원 논문은 ReLU(Rectified Linear Unit, 음수를 0으로 만드는 활성화 함수)를 썼다. 이후 LLM의 다른 활성화 함수와 같다고 하면 안 된다.
내부 구조와 실제 실행 흐름: 학습은 병렬, 생성은 순차
encoder 한 층, decoder 한 층
Encoder layer (원 논문 post-LN)
x
├─ Multi-head self-attention(Q=K=V=x)
├─ Add & LayerNorm
├─ Position-wise FFN
└─ Add & LayerNorm → encoder memory
Decoder layer
y
├─ Masked multi-head self-attention(Q=K=V=y)
├─ Add & LayerNorm
├─ Encoder-decoder attention(Q=decoder, K=V=encoder memory)
├─ Add & LayerNorm
├─ Position-wise FFN
└─ Add & LayerNorm → next-token logits → softmax
세 종류 attention을 Q/K/V 출처로 구분하면 헷갈리지 않는다.
| 위치 | Q | K, V | 볼 수 있는 범위 |
|---|---|---|---|
| encoder self-attention | encoder 이전 층 | encoder 이전 층 | 입력 전체(패딩 제외) |
| decoder masked self-attention | decoder 이전 층 | decoder 이전 층 | 현재와 과거 출력만 |
| encoder-decoder attention | decoder 현재 표현 | encoder 최종 memory | 입력 전체(패딩 제외) |
학습의 shift-right: 답을 외우는 부정행위를 막는다
정답 target이 [나는, 책을, 좋아한다, <EOS>]라면 학습 decoder input은 한 칸 오른쪽으로 민다.
decoder input: [<BOS>, 나는, 책을, 좋아한다]
training label: [나는, 책을, 좋아한다, <EOS>]
causal mask까지 있으므로 position i는 label i를 직접 볼 수 없다. 대신 앞 정답들만 보고 다음 정답을 예측한다. 모든 위치의 정답이 training data에 있으므로 학습 때는 마스크를 둔 행렬 연산으로 모든 i를 동시에 계산한다. 이것이 RNN 대비 큰 병렬화 이점이다.
추론은 왜 다시 한 토큰씩인가
실제 번역 때는 label이 없다.
1. <BOS> 입력 → “나는” 확률 분포
2. <BOS>, 나는 입력 → “책을” 확률 분포
3. <BOS>, 나는, 책을 입력 → “좋아한다” 확률 분포
4. <EOS>가 나오거나 길이 제한이면 종료
그러므로 decoder output 생성은 autoregressive(자기회귀, 이전에 낸 토큰을 다음 입력으로 쓰는) 순차 과정이다. Transformer가 RNN의 학습 내부 순차성을 줄인 것은 맞지만, 원 논문도 §7에서 generation을 덜 순차적으로 만드는 일을 미래 과제로 남겼다.
공개 구현에서 확인한 실제 호출 흐름과 주의점
원 논문은 학습·평가 코드로 Tensor2Tensor를 가리킨다. 공개 저장소는 현재 deprecated(더 이상 주력 유지보수하지 않음)라고 명시하므로, 현대 production code의 권장 구현으로 쓰면 안 된다. 그래도 개념이 코드에서 어디로 내려가는지는 확인할 수 있다.
Transformer.body()가encode()후 target을transformer_prepare_decoder()에 넣고decode()를 부른다.transformer_prepare_decoder()는 lower-triangle bias를 만들고shift_right_3d(targets)를 적용한 뒤 timing signal을 더한다.attention_bias_lower_triangle()는 허용되지 않은 미래 위치에-1e9bias를 더한다. 이는 논문의−∞ before softmax와 같은 실용적 구현이다.scaled_dot_product_attention_simple()는q *= 1/sqrt(depth),matmul(q, kᵀ), bias 추가, softmax,matmul(weights, v)순서를 구현한다.
소스 대조에서 발견한 경계: 같은 저장소의 get_timing_signal_1d()는 “published paper와 약간 다르다”고 주석으로 밝힌다. 따라서 이 글의 sinusoidal 식은 원 논문 §3.5를 우선하고, 저장소 HEAD를 2017 실험의 bit-for-bit(비트 단위 동일) 구현이라고 주장하지 않는다.
KV cache는 추론의 같은 계산을 반복하지 않게 한다
decoder의 t번째 새 토큰을 만들 때 과거 토큰들의 K와 V를 매번 다시 투영하면 낭비다. Tensor2Tensor의 multihead_attention() cache 경로는 새 K/V를 기존 cache["k"], cache["v"]에 이어 붙이고, encoder-decoder attention의 K/V도 별도 cache에서 재사용한다.
새 토큰 t의 decoder self-attention
Q: 새 토큰 하나만 계산
K,V: cache의 과거 0..t-1 + 새 토큰의 K,V
base decoder 6층, 생성 128토큰, d_model=512, batch 1을 단순 가정하면 K와 V의 scalar 수는 다음과 같다.
2(K,V) × 6(layers) × 128(tokens) × 512 = 786,432 scalars
FP32(Floating Point 32, 32비트 부동소수점, 4 bytes): 3.0 MiB(Mebibyte, 2²⁰ bytes)
FP16(Floating Point 16, 16비트 부동소수점, 2 bytes): 1.5 MiB
이것은 가중치·activation·workspace를 뺀 cache만의 계산이다. 현대 LLM은 층·head·문맥·동시 요청이 훨씬 커서 KV cache가 배포 용량의 핵심이 된다. 이전 DeepSeek-V3 글에서 MLA가 이 cache를 압축하려 한 이유도 여기서 출발한다.
직접 검증: attention, causal mask, 위치 인코딩을 숫자로 끝까지 보기
검증 범위와 환경
Tensor2Tensor 전체는 오래된 TensorFlow 의존성과 대규모 번역 데이터를 요구하므로 이 Mac에서 원 논문의 WMT 학습을 재현하지 않았다. 대신 논문 식 (1), causal mask 순서, §3.5 positional encoding, decoder KV cache 크기를 외부 라이브러리 없는 Python으로 계산했다.
- 환경: macOS 26.4.1, Python 3.9.6
- 스크립트:
/tmp/attention-is-all-you-need-research/verify_attention_mechanics.py - SHA-256(Secure Hash Algorithm 256-bit, 파일 무결성 해시):
f88d37a3214e8d477628278e6abf24f7366e65b8c1530b3fff1b93cd2c03f29d - 입력: 3개 위치,
d_k=2의 Q/K와 2차원 V - 경계: 이것은 작은 행렬 계산 검증이다. Tensor2Tensor·원 논문 training·BLEU를 재현한 실험이 아니다.
#!/usr/bin/env python3
import math
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def softmax(values):
finite = [x for x in values if x != float("-inf")]
pivot = max(finite)
exps = [0.0 if x == float("-inf") else math.exp(x - pivot)
for x in values]
total = sum(exps)
return [x / total for x in exps]
def weighted_sum(weights, values):
return [sum(weight * value[dimension] for weight, value in zip(weights, values))
for dimension in range(len(values[0]))]
def scaled_attention(q, k, v, causal):
scale = math.sqrt(len(k[0]))
all_scores, all_weights, all_outputs = [], [], []
for query_index, query in enumerate(q):
scores = [dot(query, key) / scale for key in k]
if causal:
scores = [score if key_index <= query_index else float("-inf")
for key_index, score in enumerate(scores)]
weights = softmax(scores) # mask는 softmax 전에 적용한다.
all_scores.append(scores)
all_weights.append(weights)
all_outputs.append(weighted_sum(weights, v))
return all_scores, all_weights, all_outputs
def positional_encoding(position, d_model):
result = []
for dimension in range(d_model):
i = dimension // 2
angle = position / (10000 ** ((2 * i) / d_model))
result.append(math.sin(angle) if dimension % 2 == 0 else math.cos(angle))
return result
q = k = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
v = [[10.0, 0.0], [0.0, 10.0], [10.0, 10.0]]
_, unmasked_weights, unmasked_outputs = scaled_attention(q, k, v, causal=False)
_, causal_weights, causal_outputs = scaled_attention(q, k, v, causal=True)
print("scale sqrt(d_k):", round(math.sqrt(2), 6))
print("unmasked position-0 weights:", [round(x, 6) for x in unmasked_weights[0]])
print("unmasked position-0 output:", [round(x, 6) for x in unmasked_outputs[0]])
print("causal position-0 weights:", [round(x, 6) for x in causal_weights[0]])
print("causal position-0 output:", [round(x, 6) for x in causal_outputs[0]])
print("causal position-1 weights:", [round(x, 6) for x in causal_weights[1]])
print("causal position-1 output:", [round(x, 6) for x in causal_outputs[1]])
print("causal position-2 weights:", [round(x, 6) for x in causal_weights[2]])
print("causal position-2 output:", [round(x, 6) for x in causal_outputs[2]])
# 틀린 구현: softmax 후 미래 weight를 지워 합계가 1이 아니다.
wrong_after_softmax = [unmasked_weights[0][0], 0.0, 0.0]
print("wrong post-softmax mask sum:", round(sum(wrong_after_softmax), 6))
print("wrong post-softmax position-0 output:",
[round(x, 6) for x in weighted_sum(wrong_after_softmax, v)])
print("PE(pos=0, d_model=4):",
[round(x, 6) for x in positional_encoding(0, 4)])
print("PE(pos=1, d_model=4):",
[round(x, 6) for x in positional_encoding(1, 4)])
layers, generated_tokens, d_model = 6, 128, 512
cache_scalars = 2 * layers * generated_tokens * d_model
print("decoder KV cache scalars (6 layers, 128 tokens, d_model=512):", cache_scalars)
print("cache MiB at fp32 / fp16:",
round(cache_scalars * 4 / 2**20, 3), round(cache_scalars * 2 / 2**20, 3))
직접 실행 결과:
scale sqrt(d_k): 1.414214
unmasked position-0 weights: [0.401112, 0.197776, 0.401112]
unmasked position-0 output: [8.022242, 5.988879]
causal position-0 weights: [1.0, 0.0, 0.0]
causal position-0 output: [10.0, 0.0]
causal position-1 weights: [0.330238, 0.669762, 0.0]
causal position-1 output: [3.302385, 6.697615]
causal position-2 weights: [0.248255, 0.248255, 0.50349]
causal position-2 output: [7.517449, 7.517449]
wrong post-softmax mask sum: 0.401112
wrong post-softmax position-0 output: [4.011121, 0.0]
PE(pos=0, d_model=4): [0.0, 1.0, 0.0, 1.0]
PE(pos=1, d_model=4): [0.841471, 0.540302, 0.01, 0.99995]
decoder KV cache scalars (6 layers, 128 tokens, d_model=512): 786432
cache MiB at fp32 / fp16: 3.0 1.5
첫 위치는 causal mask가 없으면 미래의 1·2번 V를 섞어 본다. mask를 softmax 전에 넣으면 첫 위치는 자기 V [10, 0]만 본다. 반대로 softmax 뒤에 미래 weight를 0으로 지우면 합계가 0.401112라서 가중 평균이 아니게 된다. 이 검증은 “마스크를 왜 logits에 더하는가”를 코드 없이도 확인하게 한다.
성능과 트레이드오프: 병렬화는 n² 표를 공짜로 만들지 않는다
원 논문의 성능 수치는 당시 조건에 묶여 있다
논문 확인: §5에서 WMT 2014 English-German은 약 450만 문장 쌍, English-French는 3,600만 문장 쌍으로 학습했다. base는 8 NVIDIA P100 GPU에서 약 100,000 steps/12시간, big은 300,000 steps/3.5일을 보고한다. Adam optimizer(Adaptive Moment Estimation, 적응형 모멘트 추정)는 β₁=0.9, β₂=0.98, ε=1e-9, warmup steps 4,000을 썼다.
learning_rate = d_model^-0.5 × min(step^-0.5,
step × warmup_steps^-1.5)
초기에는 학습률을 선형으로 올리고, 이후에는 1/√step 비율로 낮춘다. 원 논문 Table 2는 English-German big의 BLEU(Bilingual Evaluation Understudy, 기계 번역 n-gram 일치 지표) 28.4를 보고한다. English-French 값에는 원문 내부 불일치가 있다. abstract와 Table 2는 41.8, §6.1 본문은 41.0이라고 쓴다. 이 글은 어느 한 값을 임의로 정정하지 않는다. 둘 모두 당시 데이터·토크나이저·beam search·비교 대상에 묶인 번역 지표다.
attention 행렬의 시간·메모리 비용
길이 n, 표현 폭 d인 self-attention 층은 QKᵀ로 n × n 점수 표를 만든다. 원 논문 Table 1의 self-attention 층 복잡도는 O(n² × d), 메모리도 attention weights 때문에 대략 O(n²)로 증가한다. FFN까지 포함하면 위치별 d → d_ff → d 변환 비용 O(n × d × d_ff)도 더해야 한다.
| 장점 | 비용/한계 |
|---|---|
| 학습에서 모든 위치 Q/K/V를 병렬 계산 | 문맥 길이 2배면 attention 표는 약 4배 |
| 멀리 떨어진 토큰도 한 층에서 직접 연결 | 긴 문서·다중 요청에서 GPU 메모리 급증 |
| attention weight를 관찰할 수 있음 | weight만으로 인과적 설명·정확성을 보장하지 않음 |
| decoder의 cache로 과거 K/V 재계산 감소 | cache 자체가 문맥·동시 요청에 비례해 커짐 |
| multi-head로 여러 관계를 분리할 기회 | head 수를 늘린다고 단조롭게 좋아지지 않음 |
원 논문 Table 3에서도 head를 1개로 줄인 변형은 base보다 BLEU가 낮았고, 너무 많은 head도 낮아졌다. “head는 많을수록 좋다”가 아니라 d_model, head 당 차원, 데이터, 최적화가 함께 맞아야 한다.
regularization은 부속품이 아니다
dropout은 학습 중 일부 activation을 무작위로 끄고, label smoothing은 정답 token probability 목표를 1.0 대신 조금 퍼뜨린다. 논문은 label smoothing ε_ls=0.1이 perplexity(모델이 다음 토큰을 얼마나 놀라는지 보는 지표)는 나쁘게 하지만 정확도와 BLEU는 개선한다고 보고한다. 그래서 train loss가 가장 낮은 모델 하나만 보고 제품 품질을 판단하면 안 된다.
실패, 한계, 장애와 운영 기준: “attention만”으로 제품이 완성되지는 않는다
1. 순서 정보를 빼면 단어 가방이 된다
self-attention만 쓰고 positional encoding을 빼면 같은 token 집합의 순서를 구분할 근거가 없다. 원 논문 §3.5는 다음 sinusoidal encoding(사인·코사인 위치 인코딩)을 embedding에 더했다. 아래 식의 PE는 Positional Encoding(위치 인코딩)이다.
PE(pos, 2i) = sin(pos / 10000^(2i / d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i / d_model))
pos는 토큰 위치, i는 차원 쌍 번호다. 서로 다른 주기의 파동을 512차원에 넣어 위치를 구별한다. 원 논문은 고정 거리 k에 대해 PE(pos+k)를 PE(pos)의 선형 함수로 표현할 수 있어 상대 위치를 배우기 좋을 것이라 가정했고, learned positional embedding과 비슷한 결과를 보고했다. 이는 긴 길이를 자동으로 일반화한다는 보증이 아니다.
2. causal mask 하나가 빠지면 평가 점수는 거짓말이 된다
decoder training에서 미래 target을 보면 next-token prediction은 쉽게 맞힌다. 그러나 실제 생성에는 미래 target이 없다. 그러므로 padding mask(패딩 위치 차단)와 causal mask를 구분해 단위 테스트해야 한다.
관찰할 것
- query position i의 future attention weight 합 = 0인가?
- pad token에 attention이 가지 않는가?
- train(전체 target)와 infer(생성된 prefix)의 tokenizer/BOS/EOS가 같은가?
- golden sentence를 beam_size=1과 beam search로 재현할 수 있는가?
3. beam search는 품질·지연 시간·길이를 바꾼다
논문 §6.1은 beam size 4와 length penalty α=0.6을 썼다. beam search(빔 탐색)는 한 시점의 후보 하나만 고르는 대신 상위 후보 여러 개를 유지해 다음 토큰을 펼친다. 후보가 많으면 품질이 좋아질 수 있지만, 추론 비용과 지연 시간이 늘고 길이 선호도도 바뀐다. 모델 score만 비교하려면 decoder, temperature/beam, length penalty, max output length를 같이 기록해야 한다.
4. 공개 historical source를 그대로 production에 설치하지 않는다
Tensor2Tensor README는 저장소가 deprecated이며 successor로 Trax를 권한다고 쓴다. 이는 이 글의 원리 검증에는 유용하지만 2026 production 환경의 추천 런타임은 아니다. 특히 오래된 TensorFlow 의존성, 보안 패치, GPU kernel, tokenizer, mixed precision, serving cache 구현은 현재 스택과 다를 수 있다.
해석: 새 서비스는 최신 프레임워크의 유지되는 구현을 쓰되, 이 논문의 불변 검사 항목—mask before softmax, shape, shift-right, padding, position, cache, decoding 조건—을 테스트로 옮긴다. 라이브러리 이름을 외우는 것보다 이 실행 계약을 지키는 것이 중요하다.
대안, 비교와 선택 기준: 어떤 Transformer 조각을 쓸 것인가
| 구조 | 입력을 보는 방식 | 출력 생성 | 맞는 대표 문제 | 원 논문과의 관계 |
|---|---|---|---|---|
| encoder-decoder | encoder가 입력 전체 bidirectional attention | decoder가 causal 생성 | 번역, 요약, 입력→출력 변환 | 원래 Transformer |
| encoder-only | 입력 전체를 서로 봄 | 보통 분류/표현 추출 | 검색 embedding, 분류, 마스킹 복원 | encoder 절반을 사용 |
| decoder-only | 과거만 보며 self-attention | 다음 토큰 생성 | 챗봇, code generation, LLM | decoder의 masked self-attention 중심 |
| RNN/LSTM | 이전 상태를 순차 전달 | 순차 | 작고 짧은 스트림, 특수 시계열 | 병렬화·장거리 경로가 불리 |
| local/sparse attention | 일부 이웃/선택 위치만 봄 | 구조에 따라 다름 | 매우 긴 문맥 | n² 완화를 위한 후속 선택 |
백엔드·DBA(Database Administrator, 데이터베이스 관리자) 출신인 내가 가져갈 것
해석: 지금 바로 6층 Transformer를 처음부터 학습하는 것이 최단 경로는 아니다. 더 효율적인 학습 순서는 아래다.
1. token → embedding → Q/K/V → softmax → mask를 작은 배열로 직접 계산한다.
2. RAG에서 “문서 chunk가 query에 왜 선택됐는가”를 embedding·retrieval·reranking으로 분리한다.
3. LLM API(Application Programming Interface, 응용 프로그램 프로그래밍 인터페이스)를 쓸 때 context length, input/output token, KV cache, p95 latency(상위 5%를 제외한 지연 시간),
timeout, retry, cost를 데이터베이스/API 지표처럼 기록한다.
4. structured output은 모델 문장에 맡기지 않고 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) Schema 검증·권한·트랜잭션을
애플리케이션에서 강제한다.
5. 그 뒤 작은 encoder/decoder 모델을 fine-tune 또는 추론해 mask·tokenizer·평가를 실습한다.
Transformer는 데이터 파이프라인을 대신하지 않는다. 모델에 들어갈 문서의 수집·정제·chunking·권한 필터·embedding version·평가 데이터·관측 가능성이 제품 성패를 좌우한다. 그러나 그 모든 설계에서 “어떤 token이 어떤 context를 보고, 그 대가는 n²와 cache로 얼마인가?”를 답할 수 있게 만든 출발점이 이 논문이다.
흔한 오해와 최초 질문에 대한 답
Q1. “attention만 있으면 순서도 자동으로 알지 않나?”
아니다. self-attention은 순서 없는 집합처럼 토큰을 섞어 볼 수 있다. 원 논문은 positional encoding을 embedding에 더해 위치를 넣었다. causal mask는 순서를 알려 주는 장치가 아니라 미래 정보 누설을 막는 장치다.
Q2. “Transformer는 병렬이니 답도 한 번에 생성하나?”
학습은 대부분 병렬, autoregressive decoder 생성은 순차다. target을 아는 training에서는 모든 위치를 mask와 함께 계산한다. inference에서는 다음 정답을 모르므로 이전에 생성한 토큰을 받아 한 칸씩 진행한다.
Q3. “self-attention은 문장 전체를 보니 긴 문맥에 항상 유리하나?”
아니다. 직접 연결은 장점이지만 attention score 표가 n²으로 커진다. 문맥 길이·동시 요청·head·precision·KV cache를 계산하지 않으면 OOM(Out Of Memory, 메모리 부족)과 tail latency가 먼저 온다.
Q4. “attention weight가 크면 그 단어가 답의 진짜 근거인가?”
아니다. 그것은 특정 head·층의 가중 평균 계수다. 모델 전체의 인과적 설명, 사실성, 안전성 증명이 아니다. RAG(Retrieval-Augmented Generation, 검색 증강 생성) 근거는 별도의 source citation과 retrieval evaluation으로 검증한다.
Q5. “원 논문 모델을 그대로 쓰면 GPT나 Llama를 이해한 것인가?”
기초는 잡지만 충분하지 않다. 원 논문은 6+6층 번역 Transformer, post-LN, sinusoidal PE, ReLU FFN, BPE다. 현대 decoder-only LLM은 학습 규모, tokenizer, 위치 표현, normalization 순서, activation, attention kernel, MoE(Mixture of Experts, 전문가 혼합), 사후학습이 크게 다르다. 다만 Q/K/V·mask·residual·FFN·cache를 이해하면 다음 변화를 비교할 좌표가 생긴다.
Q6. “이 논문을 읽었으니 수식을 외우면 되나?”
아니다. 최소 목표는 다음 세 가지다. (1) mask를 softmax 전에 넣어야 하는 이유를 숫자로 말하기, (2) encoder/decoder/cross attention의 Q/K/V 출처를 표 없이 말하기, (3) 문맥 길이와 KV cache가 비용을 바꾸는 이유를 계산하기. 이 세 가지가 되면 논문을 단순 암기가 아니라 시스템 설계 언어로 이해한 것이다.
출처
1차 자료
- Ashish Vaswani et al., Attention Is All You Need, arXiv:1706.03762v7, 2023-08-02. 이 글의 §1–§7, 식 (1)–(3), Figure 1–2, Table 1–3을 확인했다. 2017 학회 논문의 후속 arXiv 버전이며, 수치·구조 설명은 이 버전을 기준으로 했다.
- TensorFlow, Tensor2Tensor
transformer.py, commitbafdc1b및common_attention.py.body → prepare decoder → decode, lower-triangle bias, scaled attention, KV cache 흐름을 대조했다. 저장소는 deprecated이며 일부 timing signal 구현이 원 논문과 약간 다르다는 주석을 직접 확인했다. - TensorFlow, Tensor2Tensor README, commit
bafdc1b. 저장소의 deprecated 상태를 확인했다.
직접 실행
/tmp/attention-is-all-you-need-research/verify_attention_mechanics.py를 macOS 26.4.1, Python 3.9.6에서 실행했다. SHA-256과 실제 출력은직접 검증절에 남겼다. 이 실행은 작은 attention·mask·position·cache 계산 검증이며 원 논문 학습의 재현이 아니다.
댓글