---
title: "Attention Is All You Need 해부: Transformer가 RNN 없이 문장 전체를 읽는 법"
slug: "attention-is-all-you-need-deep-dive"
category: "AI / LLM"
topic: "ai-llm"
subtopic: "transformer"
tags: ["Transformer","Self-Attention","Multi-Head Attention","Sequence-to-Sequence","Paper Review"]
status: "published"
created: "2026-07-24"
updated: "2026-07-24"
summary: "Attention Is All You Need를 RNN의 순차 병목에서 시작해 scaled dot-product attention, multi-head, causal mask, positional encoding, encoder-decoder 실행 경로, 학습·추론·KV cache와 긴 문맥 한계까지 12살도 설명할 수 있게 해부한다."
kind: "Deep Dive"
evidence: "Attention Is All You Need 원 논문 arXiv v7·저자 공개 Tensor2Tensor 구현 고정 commit·Python 3.9 attention/causal-mask/PE 계산"
series: "LLM 시스템 지도 — 1단계"
---

> **1단계 첫 논문.** 이 글은 Vaswani 외, [*Attention Is All You Need*](https://arxiv.org/abs/1706.03762v7) 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`](https://github.com/tensorflow/tensor2tensor/tree/bafdc1b67730430d38d6ab802cbd51f9d053ba2e) 기준으로 확인했다.

## 이 글에서 얻을 답과 범위

이 논문 이전의 번역 모델은 대개 RNN(Recurrent Neural Network, 순환 신경망)이 단어를 왼쪽부터 하나씩 읽었다. Transformer는 이 질문을 던진다.

> **문장 속 모든 단어를 한꺼번에 놓고, 지금 단어가 무엇을 참고할지 직접 고르게 하면 순차 계산을 버릴 수 있지 않을까?**

답이 self-attention(자기 어텐션)이다. 각 토큰(token, 모델이 처리하는 문자 조각)이 같은 문장 속 다른 토큰에 점수를 매기고, 중요한 정보의 가중 평균을 가져온다. 하지만 attention만 쓰면 “첫 번째”와 “마지막”의 순서를 모른다. 그래서 positional encoding(위치 인코딩), 미래를 못 보게 하는 causal mask(인과 마스크), 여러 관점을 병렬로 보는 multi-head(멀티 헤드), 정보 변환을 위한 FFN(Feed-Forward Network, 피드포워드 네트워크)이 함께 필요하다.

이 글을 다 읽으면 다음을 직접 설명할 수 있어야 한다.

1. RNN의 무엇이 학습 병렬화와 긴 거리 의존성에 문제였는가?
2. `Attention(Q, K, V) = softmax(QKᵀ / √d_k)V`의 모든 기호는 무엇이며, 왜 `√d_k`로 나누는가?
3. self-attention, encoder-decoder attention, masked self-attention은 Q(Query, 질의)·K(Key, 키)·V(Value, 값)가 어디서 오는가?
4. decoder(디코더)가 학습 때는 병렬인데 생성 때는 한 토큰씩 순차인 이유는 무엇인가?
5. positional encoding, residual connection(잔차 연결), LayerNorm(레이어 정규화), FFN은 각각 어떤 실패를 막는가?
6. 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살 비유: 한 명의 전달자와 원탁회의

문장 `민수는 학교에 갔고, 그는 책을 읽었다.`를 이해한다고 해 보자.

```text
RNN: 앞 단어의 메모를 다음 단어에게 한 명씩 전달한다.
     민수 → 는 → 학교 → 에 → ... → 그는

Self-attention: “그는”이 모든 단어를 한꺼번에 보고
                “민수”에 큰 점수를 주어 정보를 가져온다.
```

RNN의 hidden state(은닉 상태) `h_t`는 이전 상태 `h_(t-1)`와 현재 입력 `x_t`로 만들어진다.

```text
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(시퀀스-대-시퀀스) 기계 번역 모델이다.

```text
영어 입력:   [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들의 가중 평균을 돌려준다.

```text
Query(Q): 지금 이 토큰이 찾는 질문 카드
Key(K):   각 토큰이 “나는 이런 정보”라고 붙인 색인 카드
Value(V): 실제로 가져올 내용 카드
```

예를 들어 `그는`의 Q가 `민수`의 K와 잘 맞으면, `민수`의 V에 큰 weight(가중치)를 준다. Q/K/V는 사람이 작성한 문법 규칙이 아니다. 같은 입력 표현 `X`에 서로 다른 학습 행렬을 곱해 만든다.

```text
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로 만든다.

```text
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`를 완전히 덮어쓰지 않고 다음처럼 더한다.

```text
y = LayerNorm(x + Sublayer(x))
```

residual connection은 원본 정보와 gradient가 지나는 짧은 길을 남긴다. LayerNorm은 **한 토큰 벡터의 feature 차원들**의 평균과 분산을 이용해 스케일을 안정화하고, 학습 가능한 `γ`, `β`로 다시 조절한다.

```text
μ = 평균(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의 핵심 식은 다음이다.

```text
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이고 서로 독립이라고 단순 가정해 보자.

```text
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이 되게 한다.

```text
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한 뒤 붙인다.

```text
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은 각 위치 벡터를 같은 작은 신경망으로 변환한다.

```text
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 한 층

```text
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은 한 칸 오른쪽으로 민다.

```text
decoder input: [<BOS>, 나는, 책을, 좋아한다]
training label: [나는, 책을, 좋아한다, <EOS>]
```

causal mask까지 있으므로 position i는 label i를 직접 볼 수 없다. 대신 앞 정답들만 보고 다음 정답을 예측한다. 모든 위치의 정답이 training data에 있으므로 **학습 때는 마스크를 둔 행렬 연산으로 모든 i를 동시에 계산**한다. 이것이 RNN 대비 큰 병렬화 이점이다.

### 추론은 왜 다시 한 토큰씩인가

실제 번역 때는 label이 없다.

```text
1. <BOS> 입력 → “나는” 확률 분포
2. <BOS>, 나는 입력 → “책을” 확률 분포
3. <BOS>, 나는, 책을 입력 → “좋아한다” 확률 분포
4. <EOS>가 나오거나 길이 제한이면 종료
```

그러므로 decoder output 생성은 autoregressive(자기회귀, 이전에 낸 토큰을 다음 입력으로 쓰는) 순차 과정이다. Transformer가 RNN의 **학습 내부 순차성**을 줄인 것은 맞지만, 원 논문도 §7에서 generation을 덜 순차적으로 만드는 일을 미래 과제로 남겼다.

### 공개 구현에서 확인한 실제 호출 흐름과 주의점

원 논문은 학습·평가 코드로 Tensor2Tensor를 가리킨다. 공개 저장소는 현재 deprecated(더 이상 주력 유지보수하지 않음)라고 명시하므로, 현대 production code의 권장 구현으로 쓰면 안 된다. 그래도 개념이 코드에서 어디로 내려가는지는 확인할 수 있다.

1. [`Transformer.body()`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/models/transformer.py#L186-L286)가 `encode()` 후 target을 `transformer_prepare_decoder()`에 넣고 `decode()`를 부른다.
2. [`transformer_prepare_decoder()`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/models/transformer.py#L1398-L1457)는 lower-triangle bias를 만들고 `shift_right_3d(targets)`를 적용한 뒤 timing signal을 더한다.
3. [`attention_bias_lower_triangle()`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/layers/common_attention.py#L976-L990)는 허용되지 않은 미래 위치에 `-1e9` bias를 더한다. 이는 논문의 `−∞ before softmax`와 같은 실용적 구현이다.
4. [`scaled_dot_product_attention_simple()`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/layers/common_attention.py#L6078-L6104)는 `q *= 1/sqrt(depth)`, `matmul(q, kᵀ)`, bias 추가, softmax, `matmul(weights, v)` 순서를 구현한다.

**소스 대조에서 발견한 경계:** 같은 저장소의 [`get_timing_signal_1d()`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/layers/common_attention.py#L430-L497)는 “published paper와 약간 다르다”고 주석으로 밝힌다. 따라서 이 글의 sinusoidal 식은 원 논문 §3.5를 우선하고, 저장소 HEAD를 2017 실험의 bit-for-bit(비트 단위 동일) 구현이라고 주장하지 않는다.

### KV cache는 추론의 같은 계산을 반복하지 않게 한다

decoder의 t번째 새 토큰을 만들 때 과거 토큰들의 K와 V를 매번 다시 투영하면 낭비다. Tensor2Tensor의 [`multihead_attention()` cache 경로](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/layers/common_attention.py#L4640-L4709)는 새 K/V를 기존 `cache["k"]`, `cache["v"]`에 이어 붙이고, encoder-decoder attention의 K/V도 별도 cache에서 재사용한다.

```text
새 토큰 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 수는 다음과 같다.

```text
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 글](/posts/deepseek-v3-technical-report-deep-dive)에서 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를 재현한 실험이 아니다.

```python
#!/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))
```

**직접 실행 결과:**

```text
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을 썼다.

```text
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(위치 인코딩)이다.

```text
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를 구분해 단위 테스트해야 한다.

```text
관찰할 것
  - 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를 처음부터 학습하는 것이 최단 경로는 아니다. 더 효율적인 학습 순서는 아래다.

```text
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차 자료

1. Ashish Vaswani et al., [*Attention Is All You Need*, arXiv:1706.03762v7](https://arxiv.org/abs/1706.03762v7), 2023-08-02. 이 글의 §1–§7, 식 (1)–(3), Figure 1–2, Table 1–3을 확인했다. 2017 학회 논문의 후속 arXiv 버전이며, 수치·구조 설명은 이 버전을 기준으로 했다.
2. TensorFlow, [Tensor2Tensor `transformer.py`, commit `bafdc1b`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/models/transformer.py) 및 [`common_attention.py`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/tensor2tensor/layers/common_attention.py). `body → prepare decoder → decode`, lower-triangle bias, scaled attention, KV cache 흐름을 대조했다. 저장소는 deprecated이며 일부 timing signal 구현이 원 논문과 약간 다르다는 주석을 직접 확인했다.
3. TensorFlow, [Tensor2Tensor README, commit `bafdc1b`](https://github.com/tensorflow/tensor2tensor/blob/bafdc1b67730430d38d6ab802cbd51f9d053ba2e/README.md). 저장소의 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 계산 검증이며 원 논문 학습의 재현이 아니다.
