---
title: "번역할 다음 단어마다 입력 문장을 다시 보는 법 — Bahdanau Attention"
slug: "neural-machine-translation-attention-deep-dive"
category: "AI / LLM"
topic: "ai-llm"
subtopic: "ml-foundations"
tags: ["Attention","Bahdanau Attention","Neural Machine Translation","Seq2Seq","GRU","Bidirectional RNN","Soft Alignment","Paper Review"]
status: "published"
created: "2026-07-24"
updated: "2026-07-24"
summary: "Bahdanau·Cho·Bengio의 2015년 Neural Machine Translation by Jointly Learning to Align and Translate 원 논문을 12살 독자 기준으로 해부한다. 고정 길이 문장 벡터의 병목, 양방향 RNN annotation, 매 출력 단계마다 달라지는 context vector, additive soft alignment와 softmax, 논문이 실제 사용한 GRU decoder와 maxout 출력, GroundHog 공식 구현의 계산 흐름, WMT14 English-to-French 실험 조건과 BLEU의 경계, Python 3.9.6 수치 재현, 시간·메모리 비용과 Transformer·RAG와의 차이를 사실·직접 실행·해석으로 분리한다."
kind: "Deep Dive"
evidence: "Bahdanau·Cho·Bengio arXiv 1409.0473v7 원문과 ICLR 2015 본문·부록·표·그림 직접 확인, 논문이 연결한 lisa-groundhog/GroundHog 공식 저장소 commit 4d2433b의 RNNsearch 구현과 README·state.py 직접 확인, Python 3.9.6 표준 라이브러리로 Eq. (5)·(6) soft alignment와 Appendix A.1.1 gated decoder를 직접 계산해 attention 합·context weighted sum·projection cache·softmax shift invariance·hard alignment와의 차이를 assert로 검증"
series: "LLM 시스템 지도 — 3단계 Bottom-Up"
---

> **3단계 네 번째 논문.** 이 글은 Dzmitry Bahdanau, Kyunghyun Cho, Yoshua Bengio의 [Neural Machine Translation by Jointly Learning to Align and Translate 원문 PDF](https://arxiv.org/pdf/1409.0473)를 직접 읽고 썼다. arXiv 기록상 최초 제출은 2014-09-01, 이 글이 확인한 판본은 2016-05-19의 v7이며 논문 표지는 ICLR 2015 게재본이라고 적는다.[arXiv 서지 기록](https://arxiv.org/abs/1409.0473) 여기서 말하는 attention(어텐션)은 **번역할 target token 하나를 만들 때마다, source 문장의 어느 위치 정보를 얼마나 섞을지 계산하는 soft alignment(부드러운 정렬)** 이다.

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

이전 Seq2Seq(시퀀스에서 시퀀스로) 모델은 encoder(인코더)가 source sentence(입력 문장)를 읽어 fixed-length vector(고정 길이 벡터) 하나로 압축하고, decoder(디코더)가 그 벡터만 보고 번역을 시작했다. 짧은 문장에서는 그럴듯해도 긴 문장의 모든 세부를 한 메모장에 밀어 넣는 일은 불리하다. 이 논문은 decoder가 다음 단어를 낼 때마다 source 전체를 다시 훑어, 그 순간 필요한 위치의 정보를 가중합으로 가져오게 했다.

이 글을 끝까지 읽으면 다음을 답할 수 있어야 한다.

1. fixed vector 하나가 왜 long sentence(긴 문장)에서 bottleneck(병목)이 되는가.
2. encoder의 annotation(주석 벡터) (h_j), alignment energy(정렬 에너지) (e_{ij}), attention weight(주의 가중치) α, context vector(문맥 벡터) (c_i)가 각각 무엇인가.
3. 왜 α의 합이 1이고, 각 target 위치마다 다른 (c_i)가 생기는가.
4. 논문이 LSTM(장단기 메모리)을 썼다는 흔한 설명이 왜 정확하지 않은가. 이 논문 실험의 recurrent unit(순환 유닛)은 gated hidden unit, 오늘날 보통 GRU(Gated Recurrent Unit, 게이트 순환 유닛)라고 부르는 구조다.
5. soft alignment가 hard alignment(한 위치만 고르는 정렬)와 어떻게 다르고, gradient(기울기)가 왜 끝까지 흐르는가.
6. 원 논문의 WMT14 English-to-French 결과가 어떤 data, vocabulary, model, BLEU 조건의 수치인가.
7. Bahdanau attention, Transformer self-attention, RAG(Retrieval-Augmented Generation, 검색 증강 생성)가 같은 문제가 아닌 이유는 무엇인가.

**범위**는 원 논문 RNNsearch의 양방향 RNN encoder, additive attention(가산형 어텐션), GRU decoder, maxout output, 학습·번역·평가 조건, 공식 GroundHog 구현의 관련 실행 흐름이다. 현대 Transformer의 multi-head self-attention(다중 헤드 자기 어텐션), BPE(Byte Pair Encoding, 바이트 쌍 인코딩) 토큰화, 대규모 language model pretraining(사전학습)을 대신 설명하지 않는다. 이 글의 Python 예제는 원 논문 전체를 재학습하거나 BLEU를 재현한 결과가 아니라, 식 (5), (6), Appendix A.1.1의 **한 decoder step 수치 검증**이다.

### 먼저 잡을 한 문장

> **Bahdanau attention은 decoder가 다음 target token을 만들기 직전의 상태로 source 각 위치를 채점하고, 모든 위치를 확률적 가중합으로 섞은 새 context vector를 매 step 공급받게 만든 구조다.**

## 왜 필요한가 — 고정 길이 메모장 하나의 실패

### 기존 encoder-decoder는 문장 전체를 한 벡터에 접었다

기본 neural machine translation, NMT(신경망 기계번역)는 source (x=(x_1,\ldots,x_{T_x}))를 RNN(Recurrent Neural Network, 순환 신경망)으로 읽고 고정 길이 context (c)를 만든다. RNN은 현재 입력 (x_t)와 이전 hidden state(은닉 상태) (h_{t-1})에서 새 상태를 만든다.

\[
h_t=f(x_t,h_{t-1}), \qquad c=q(\{h_1,\ldots,h_{T_x}\})
\tag{1}
\]

| 기호 | 뜻 | 이 논문에서 중요한 점 |
| --- | --- | --- |
| (x_t) | t번째 source token의 입력 vector | source 길이는 (T_x)로 달라진다 |
| (h_t) | encoder가 t번째까지 읽은 state | token을 읽을 때마다 바뀐다 |
| (f) | RNN의 갱신 함수 | 논문 실험에서는 GRU 계열 unit을 쓴다 |
| (c) | decoder에 넘길 고정 길이 context | source가 길어도 크기가 늘지 않는다 |
| (q) | states에서 (c)를 만드는 함수 | 이전 Seq2Seq 예에서는 마지막 state를 쓸 수 있다 |

예를 들어 `The small red book fell from the old shelf .`라는 긴 source를 마지막 벡터 하나에 압축한 뒤, decoder가 프랑스어의 어떤 단어를 쓰든 같은 (c)만 보게 한다고 하자. 초반의 `book`을 번역할 때와 후반의 `shelf`를 번역할 때 필요한 정보가 다르다. 그런데 fixed vector는 “지금은 책 주변만 읽어라”는 주소를 제공하지 않는다.

~~~text
기본 Seq2Seq
source 전체 ──encoder──> 고정 벡터 c 하나 ──> decoder의 모든 출력 step

문제
입력이 길어져도 c의 칸 수는 같다.
decoder는 c 안에서 어느 source 위치가 필요한지 직접 지정할 수 없다.
~~~

원 논문은 바로 이 fixed-length vector를 병목으로 가정했고, 기본 encoder-decoder가 긴 문장에서 나빠지는 관찰을 출발점으로 삼았다.[원 논문 1절·2절](https://arxiv.org/pdf/1409.0473) “문장 뜻을 완벽히 이해하지 못해서” 같은 사람식 설명은 논문 사실이 아니다. 더 정확한 말은 **학습된 network가 길이에 무관한 하나의 vector로 필요한 정보를 전달해야 하는 구조적 제약이 있었다**는 것이다.

### 해결은 source를 버리지 않고 펼쳐 둔 메모 카드다

새 모델은 source 위치마다 annotation (h_j)를 남긴다. decoder는 target (y_i)를 만들 때 (h_1\)부터 (h_{T_x}\)까지 모두 채점하고, 그 순간 쓸 context (c_i)를 만든다.

~~~text
source:       the | small | red | book | fell
annotation:    h1 |    h2 |  h3 |   h4 |   h5

"책"에 해당하는 target token을 만들 차례
  → h1...h5를 각각 채점
  → h4 주변의 비중이 커질 수 있음
  → c_i = 모든 h를 비율대로 섞은 vector

다음 target token을 만들 차례
  → 직전 decoder state가 달라졌으므로 다시 채점
  → 전과 다른 c_(i+1)가 생김
~~~

12살 비유에서는, 한 사람이 source 문장을 읽고 단어마다 번호가 붙은 메모 카드를 책상에 놓아둔다. 번역가는 다음 단어를 쓰기 전에 카드를 전부 보고 “이번에는 4번 카드를 58%, 2번을 28% 보자”처럼 비율을 정한다. 카드를 한 장만 집는 것이 아니라 여러 장을 섞는다. 이것이 soft다.

## 무엇인가와 하지 않는 일

### RNNsearch는 정렬을 별도 정답으로 주지 않고 번역 손실로 함께 배운다

논문은 제안 모델을 **RNNsearch**라고 부른다. target 확률을 다음처럼 정의한다.

\[
p(y_i\mid y_1,\ldots,y_{i-1},x)
=g(y_{i-1},s_i,c_i),
\qquad
s_i=f(s_{i-1},y_{i-1},c_i)
\tag{2}
\]

| 값 | 뜻 |
| --- | --- |
| (y_i) | 지금 예측할 i번째 target token |
| (y_{i-1}) | 바로 전에 나온 token. training에서는 정답 prefix, generation에서는 model이 앞에서 고른 token |
| (s_i) | i번째 decoder state |
| (c_i) | i번째 target을 위해 그때그때 계산한 source context |
| (g) | target vocabulary의 확률분포를 내는 출력 함수 |
| (f) | decoder recurrent update 함수 |

논문은 traditional machine translation의 discrete alignment(이산 정렬)를 latent variable(관측하지 않는 숨은 선택)로 놓고 따로 추정하지 않는다. 모든 source annotation의 weighted sum을 바로 계산한다. 따라서 번역 loss에서 나온 gradient가 alignment model까지 미분 가능한 경로로 전달된다.[원 논문 3.1절](https://arxiv.org/pdf/1409.0473)

### 이 논문의 attention은 Transformer와 같은 단어를 쓰지만 같은 계산은 아니다

| 비교 | Bahdanau attention | Transformer attention |
| --- | --- | --- |
| 중심 구조 | bidirectional RNN encoder + recurrent decoder | attention block을 여러 layer 쌓은 Transformer |
| 누가 query 역할을 하나 | 이전 decoder state (s_{i-1}) | 보통 token별 Q(Query, 질의) vector |
| source 쪽 정보 | encoder annotation (h_j) | K(Key, 키), V(Value, 값) projection |
| 점수 함수 | 작은 feed-forward network의 additive score | scaled dot product가 대표적 |
| target 생성 | 한 token씩 recurrent하게 진행 | decoder는 autoregressive지만 training의 token 축 계산은 병렬화 가능 |
| 핵심 공통점 | 여러 위치의 값을 가중합해 context를 만든다 | 여러 위치의 값을 가중합해 context를 만든다 |

나중에 Transformer에서 Q/K/V를 배워도, 이 논문의 “현재 생성 상태로 입력 위치를 다시 읽는다”는 핵심은 그대로 이어진다. 다만 이 글의 α를 Transformer의 attention map과 동일한 의미나 동일한 구현이라고 부르면 틀린다.

### 하지 않는 일도 분명히 하자

| 하지 않는 일 | 이유 |
| --- | --- |
| source의 정답 단어 위치 하나를 확정 | α는 모든 위치에 연속값을 두는 soft weight다 |
| 어휘 밖 단어를 정상 번역 | 원 논문은 language마다 자주 나온 30,000 words만 두고 나머지를 `[UNK]`로 바꿨다 |
| 긴 문서 검색 | source 문장 안의 annotation을 읽는 것이지, 외부 corpus를 retrieval하지 않는다 |
| Transformer pretraining | 2015년 RNN NMT 구조이며 next-token 대규모 사전학습 논문이 아니다 |
| attention weight만으로 인과적 설명 증명 | 높은 α는 그 step에서 learned score가 컸다는 뜻이지, 사람이 생각하는 유일한 번역 근거의 증명은 아니다 |

## 선행 개념 — annotation, 양방향 RNN, softmax, GRU

### annotation은 단어 ID가 아니라 주변 문맥을 포함한 vector다

원 논문은 각 source 위치 (j)에 대해 forward RNN과 backward RNN의 state를 붙여 annotation을 만든다.

\[
h_j=[\overrightarrow{h_j};\overleftarrow{h_j}]
\tag{3}
\]

세미콜론이 아니라 대괄호와 `;`는 concatenate(이어 붙이기)다. (\overrightarrow{h_j})는 source 처음부터 (x_j)까지 읽은 state이고, (\overleftarrow{h_j})는 source 끝에서 거꾸로 (x_j)까지 읽은 state다.

~~~text
source:             the      red      book
forward RNN:       →h1      →h2      →h3
backward RNN:      ←h1      ←h2      ←h3
annotation h2:     [→h2 ; ←h2]

h2는 red라는 token ID 하나가 아니다.
앞쪽 the와 뒤쪽 book을 읽은 두 방향 state를 붙인 vector다.
~~~

그래서 `red`의 annotation에는 앞뒤 문맥이 들어간다. 원 논문은 RNN이 최근 입력을 더 잘 표현하는 경향 때문에 (h_j)가 특히 (x_j) 주변에 집중한다고 설명한다.[원 논문 3.2절](https://arxiv.org/pdf/1409.0473) “완벽한 양쪽 문맥 저장”은 보장하지 않는다. learned finite-dimensional vector의 성질이다.

### softmax는 점수를 합 1인 비율로 바꾼다

alignment model은 source 위치마다 raw score (e_{ij})를 낸다. score는 음수도 양수도 될 수 있고 합이 1일 필요가 없다. softmax는 이를 non-negative(음수가 아닌 값)이고 합이 1인 α로 바꾼다.

\[
\alpha_{ij}=
\frac{\exp(e_{ij})}
{\sum_{k=1}^{T_x}\exp(e_{ik})}
\tag{4}
\]

| 첨자 | 읽는 방법 |
| --- | --- |
| (i) | 지금 만들 target 위치 |
| (j) | source에서 보고 있는 위치 |
| (k) | 분모에서 모든 source 위치를 세는 임시 첨자 |
| (T_x) | source token 수 |
| α(_{ij}) | i번째 target step이 j번째 source annotation에 둔 정규화된 비중 |

각 i에 대해 (\sum_j\alpha_{ij}=1)이다. 따라서 논문은 α를 “(y_i)가 (x_j)에 align될 확률”로 해석할 수 있고, (c_i)를 그 정렬 분포 아래의 expected annotation(기대 annotation)으로 이해할 수 있다고 썼다. 이것은 모델 내부의 유용한 확률적 해석이지, 사람이 만든 gold alignment가 없더라도 무조건 진실이라는 뜻은 아니다.

### soft alignment는 모든 annotation을 미분 가능한 방식으로 섞는다

\[
c_i=\sum_{j=1}^{T_x}\alpha_{ij}h_j
\tag{5}
\]

hard alignment라면 가장 큰 score 위치 하나만 골라 (c_i=h_{\operatorname{argmax}_j\alpha_{ij}})로 만들 수 있다. 하지만 argmax는 작은 score 변화가 선택 위치를 갑자기 바꾸며 미분하기 어렵다. softmax weighted sum은 모든 α와 (h_j)에 대해 연속적으로 변한다. 번역의 negative log likelihood, NLL(음의 로그 우도) gradient가 output → decoder → (c_i) → α → alignment network → encoder까지 흐를 수 있는 이유다.

### 이 논문 실험은 LSTM이 아니라 GRU 계열 gated hidden unit을 사용했다

이전 Seq2Seq 논문은 LSTM을 사용했다. 그러나 Bahdanau 논문의 Appendix A.1.1은 Cho et al.의 gated hidden unit을 사용한다고 명시한다. 오늘날 보통 GRU라고 부르는 구조다. LSTM처럼 long-term dependency(장기 의존성)를 다루려는 gate가 있지만 cell state를 별도로 두지 않고 update gate와 reset gate를 쓴다.

\[
\begin{aligned}
z_i &= \sigma(W_z e(y_{i-1})+U_zs_{i-1}+C_zc_i)\\
r_i &= \sigma(W_r e(y_{i-1})+U_rs_{i-1}+C_rc_i)\\
\tilde{s}_i &= \tanh(W e(y_{i-1})+U[r_i\odot s_{i-1}]+Cc_i)\\
s_i &= (1-z_i)\odot s_{i-1}+z_i\odot\tilde{s}_i
\end{aligned}
\tag{6}
\]

| 값 | 12살 설명 | 기능 |
| --- | --- | --- |
| (e(y_{i-1})) | 방금 쓴 target 단어의 숫자 메모 | previous token embedding |
| (r_i) | 이전 decoder 메모를 후보 계산에서 얼마나 지울지 정하는 문 | reset gate |
| (z_i) | 이전 메모와 새 후보를 얼마나 섞을지 정하는 문 | update gate |
| (\tilde{s}_i) | 새로 써 볼 decoder 메모 후보 | candidate state |
| (c_i) | 이번 target step에서 source에서 가져온 메모 | attention context |
| σ | sigmoid. 각 칸을 0~1 사이로 바꾼다 | gate 값 생성 |
| ⊙ | 같은 칸끼리 곱한다 | vector별 독립 조절 |

(z_i=0)에 가까운 칸은 이전 (s_{i-1})을 보존하고, (z_i=1)에 가까운 칸은 후보 (\tilde{s}_i)를 더 쓴다. 실제 값은 vector 각 차원마다 다르다. 논문은 LSTM을 대신 쓸 수도 있다고 말하지만, **보고한 RNNsearch 실험의 실제 unit은 이 식**이다.[원 논문 Appendix A.1.1](https://arxiv.org/pdf/1409.0473)

## 밑바닥 원리 — target 한 칸을 만들 때 실제로 벌어지는 일

### additive alignment score를 먼저 만든다

원 논문 Appendix A.1.2는 alignment model을 한 hidden layer의 feed-forward network로 구체화했다.

\[
e_{ij}=a(s_{i-1},h_j)
=v_a^\top\tanh(W_as_{i-1}+U_ah_j)
\tag{7}
\]

여기서 (W_a)는 previous decoder state를, (U_a)는 source annotation을 같은 alignment hidden space로 옮긴다. 두 vector를 더하고 tanh를 적용한 뒤 (v_a^\top)로 숫자 하나를 낸다. dot-product attention처럼 단순 내적을 하는 구조가 아니어서 additive attention이라고 부른다.

논문 설정에서 decoder hidden size는 (n=1000), annotation은 forward와 backward 1000개 state를 이어 붙여 (2n=2000)차원이다. 따라서 (W_a\in\mathbb{R}^{n\times n}), (U_a\in\mathbb{R}^{n\times2n}), (v_a\in\mathbb{R}^{n})다. 논문은 (U_ah_j)가 target step i에 의존하지 않으므로 source를 읽은 뒤 한 번 미리 계산해 두면 된다고 명시한다.[원 논문 Appendix A.1.2](https://arxiv.org/pdf/1409.0473)

### 순서를 잘못 잡으면 (s_i)와 (c_i)가 서로 필요해 보인다

식 (2)를 처음 보면 (s_i)를 만들려면 (c_i)가 필요하고, (c_i)를 만들려면 decoder state가 필요하니 원형 참조처럼 보인다. alignment는 **새 (s_i)가 아니라 이전 (s_{i-1})** 를 쓴다. 순서는 다음처럼 닫힌다.

~~~text
이미 있는 값: source annotations h1...h_Tx, 이전 state s_(i-1), 이전 token y_(i-1)

1. e_ij = a(s_(i-1), h_j)를 source 모든 j에 대해 계산한다.
2. softmax로 alpha_i1...alpha_iTx를 만든다.
3. c_i = Σ alpha_ij h_j를 만든다.
4. y_(i-1), s_(i-1), c_i를 GRU 식에 넣어 s_i를 만든다.
5. output layer가 target vocabulary 확률을 내고 y_i를 training 또는 decoding 방식으로 다룬다.
~~~

### output layer도 context를 직접 본다

논문은 GRU state만으로 target 확률을 내지 않았다. Appendix A.2.2에서 previous state, previous target embedding, current context를 합쳐 (\tilde t_i)를 만들고, 두 값마다 큰 쪽을 고르는 maxout(맥스아웃) hidden layer를 거쳐 vocabulary softmax를 만든다.

\[
\tilde{t}_i=U_os_{i-1}+V_oEy_{i-1}+C_oc_i
\]

\[
t_i=[\max\{\tilde t_{i,2j-1},\tilde t_{i,2j}\}]_{j=1,\ldots,l}^\top,
\qquad
p(y_i\mid\cdots)\propto\exp(y_i^\top W_ot_i)
\tag{8}
\]

maxout은 두 숫자를 한 조로 보고 큰 값을 남기는 activation이다. 이 식의 요지는 “attention context는 GRU update에만 들어가는 부가 정보”가 아니라, output probability를 만드는 readout에도 직접 들어간다는 것이다. 원 논문의 exact tensor layout이나 modern framework API를 이 식 밖에서 단정하면 안 된다.

### source annotation부터 token 확률까지 한 흐름으로 연결하기

~~~text
source token IDs
  → source embedding E
  → forward GRU states, backward GRU states
  → h_j = [forward h_j ; backward h_j]  (각 source 위치 j)
  → initial decoder state s_0 = tanh(W_s backward h_1)

각 target step i
  → s_(i-1)와 모든 h_j로 alignment score e_ij 계산
  → softmax(e_i1 ... e_iTx) = alpha_i1 ... alpha_iTx
  → weighted sum으로 c_i 계산
  → previous target embedding, s_(i-1), c_i로 reset/update gate와 s_i 계산
  → previous state, previous token, c_i로 maxout readout 계산
  → vocabulary softmax
  → training: 정답 y_i의 NLL을 더함
  → generation: 후보 y_i를 선택하고 다음 step으로 감
~~~

초기 state 식 (s_0=\tanh(W_s\overleftarrow h_1))도 Appendix A.2.2에 있다. backward RNN은 source 끝에서 시작해 첫 source 위치까지 읽으므로 (\overleftarrow h_1)은 전체 source에 접근한 backward state다. source 전체를 처음부터 완전히 버린 것은 아니다. 이후에는 위치별 annotation sequence도 남아 있어 매 step retrieval한다.

## 내부 구조와 공식 GroundHog 구현의 실행 흐름

### 저장소는 논문 구현을 가리키지만, 내가 확인한 commit은 나중 snapshot이다

원 논문은 구현을 `lisa-groundhog/GroundHog`에 두었다고 연결한다. 저장소의 [NMT README](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/README.md)는 `experiments/nmt`가 논문의 RNNencdec와 RNNsearch 구현이라고 설명하며, `prototype_search_state`가 RNNsearch-50 설정이라고 적는다. 이 글은 해당 저장소 commit `4d2433b6ff03da63f3619ecb3e8c501901779cdb`를 직접 읽었다.

이 commit은 논문 뒤에 존재한 snapshot이다. 그러므로 아래 코드는 **논문과 연결된 공식 구현이 mechanism을 어떤 tensor 순서로 계산하는지 확인하는 근거**이지, 2015년 실험 binary와 완전히 동일한 실행 환경을 재현했다는 증거는 아니다.

| 코드 위치 | 확인한 일 |
| --- | --- |
| [`state.py:265-280`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/state.py#L265-L280) | `prototype_search_state`가 search를 켜고 forward·backward encoder와 length 50을 선택한다 |
| [`encdec.py:363-389`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L363-L389) | previous decoder state projection과 source annotation projection을 더하고, exp/normalization으로 weight를 만든 뒤 context weighted sum을 계산한다 |
| [`encdec.py:391-419`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L391-L419) | context를 input/reset/update signal에 더하고 gate로 hidden state를 갱신하며 선택 시 alignment를 반환한다 |
| [`encdec.py:462-494`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L462-L494) | source projection을 `scan` 바깥에서 미리 만들고 target step 반복의 non-sequence로 전달한다 |
| [`encdec.py:1081-1118`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L1081-L1118) | decoder transition 결과를 hidden state와 context, 필요하면 alignment로 분리한다 |

핵심 부분을 논문 식에 맞춰 읽으면 이렇다.

~~~python
# encdec.py의 의미를 줄여 쓴 의사 코드다. 원본의 Theano tensor 문법은 생략했다.
p_from_h = previous_decoder_state @ B_hp      # W_a s_(i-1)
p_from_c = source_annotations @ A_cp          # U_a h_j, source마다 한 번 계산
energy = exp(tanh(p_from_h + p_from_c) @ D_pe)
alpha = energy / energy.sum(source_axis)      # Eq. (6)과 같은 정규화
context = (source_annotations * alpha).sum(source_axis)  # Eq. (5)

state_input += context_projection(context)
reset_input += context_reset_projection(context)
update_input += context_update_projection(context)
# 그 뒤 reset gate, candidate, update gate로 다음 hidden state를 계산한다.
~~~

원본 code는 `exp(score)`를 먼저 만들고 source 축 합으로 나눠 softmax와 같은 수학 결과를 만든다. 아래 내 toy verifier는 score가 매우 클 때 overflow(지수값 넘침)를 피하려고 max score를 빼는 stable softmax(안정 softmax)를 쓴다. 이것은 수치적으로 안전한 검증용 구현이며, 위 GroundHog snapshot이 그 안정화 코드를 이미 썼다는 주장이 아니다.

### batch padding mask는 attention에서도 반드시 적용돼야 한다

실제 batch에서는 문장 길이가 다르다. 짧은 문장 뒤에는 padding(채움 칸)을 붙인다. GroundHog 구현은 `c_mask`가 있을 때 energy에 mask를 곱한 뒤 normalization한다.[`encdec.py:375-384`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L375-L384) padding annotation에 α가 배정되면 context가 가짜 입력으로 오염된다.

mask가 모두 0인 source를 decoder에 넣으면 denominator가 0이 된다. 논문 task에서는 빈 source translation을 허용하는 설계를 설명하지 않으므로, production에서는 source token이 하나 이상인지 먼저 검증하고 mask의 각 row에 유효 위치가 있는지 test해야 한다. 이는 논문 보고 수치가 아니라, 식 (4)와 code normalization에서 도출되는 구현 경계다.

## 직접 검증 — 세 annotation의 soft alignment와 GRU 한 step

### 실행 환경과 범위

다음 코드는 macOS의 `Python 3.9.6`에서 표준 라이브러리만 사용해 직접 실행했다. 입력은 3개 source annotation, 2차원 previous decoder state, 고정한 작은 matrix다. 학습 data, GPU, Theano, trained weight는 쓰지 않는다. 목표는 다음 다섯 가지다.

1. Eq. (4)의 α가 1로 합쳐지는지.
2. Eq. (5)의 context가 α와 annotation의 weighted sum인지.
3. (U_ah_j) cache가 매번 다시 계산한 score와 같은지.
4. 모든 score에 같은 상수를 더해도 softmax가 변하지 않는지.
5. α의 argmax annotation 하나와 soft context가 같지 않을 수 있는지.

아래 파일 전체를 `verify_bahdanau_attention.py`로 저장하고 실행한다.

~~~bash
python3 --version
python3 verify_bahdanau_attention.py
~~~

~~~python
import math

def dot(a, b):
    assert len(a) == len(b)
    return sum(x * y for x, y in zip(a, b))

def mat_vec(matrix, vector):
    return [dot(row, vector) for row in matrix]

def add(*vectors):
    assert len({len(vector) for vector in vectors}) == 1
    return [sum(values) for values in zip(*vectors)]

def weighted_sum(weights, vectors):
    assert len(weights) == len(vectors)
    return [sum(weight * vector[d] for weight, vector in zip(weights, vectors))
            for d in range(len(vectors[0]))]

def softmax(scores):
    # max를 빼도 softmax 값은 같고, exp overflow는 피한다.
    maximum = max(scores)
    exps = [math.exp(score - maximum) for score in scores]
    total = sum(exps)
    return [value / total for value in exps]

def score(previous_state, annotation, wa, ua, va):
    # Appendix A.1.2: v_a^T tanh(W_a s_(i-1) + U_a h_j)
    hidden = add(mat_vec(wa, previous_state), mat_vec(ua, annotation))
    return dot(va, [math.tanh(value) for value in hidden])

def attend(previous_state, annotations, wa, ua, va):
    scores = [score(previous_state, h, wa, ua, va) for h in annotations]
    alpha = softmax(scores)
    return scores, alpha, weighted_sum(alpha, annotations)

def sigmoid(x):
    return 1.0 / (1.0 + math.exp(-x))

def gru_step(previous_word, previous_state, context):
    # Appendix A.1.1의 작은 2차원 GRU. matrix는 설명용으로 고정했다.
    w  = [[0.50, -0.20], [0.10, 0.40]]
    u  = [[0.30, 0.20], [-0.40, 0.10]]
    c  = [[0.20, 0.30], [-0.10, 0.50]]
    wz = [[0.25, -0.15], [0.05, 0.35]]
    uz = [[0.20, 0.10], [-0.20, 0.20]]
    cz = [[0.15, 0.25], [0.30, -0.10]]
    wr = [[-0.10, 0.20], [0.30, 0.10]]
    ur = [[0.25, -0.30], [0.15, 0.20]]
    cr = [[0.10, -0.20], [-0.25, 0.15]]

    z = [sigmoid(v) for v in add(mat_vec(wz, previous_word),
                                  mat_vec(uz, previous_state), mat_vec(cz, context))]
    r = [sigmoid(v) for v in add(mat_vec(wr, previous_word),
                                  mat_vec(ur, previous_state), mat_vec(cr, context))]
    reset_state = [gate * state for gate, state in zip(r, previous_state)]
    candidate = [math.tanh(v) for v in add(mat_vec(w, previous_word),
                                            mat_vec(u, reset_state), mat_vec(c, context))]
    new_state = [(1.0 - gate) * old + gate * new
                 for gate, old, new in zip(z, previous_state, candidate)]
    return z, r, new_state

def show(name, values):
    print(name + ': [' + ', '.join(f'{v:.6f}' for v in values) + ']')

# 3개 source annotation h_j, 그리고 Eq. (7)의 작은 parameter.
annotations = [[-0.90, 0.10], [0.50, 0.90], [0.90, -0.40]]
wa = [[0.90, -0.20], [0.10, 0.80]]
ua = [[0.70, 0.10], [-0.20, 0.60]]
va = [1.20, -0.70]
earlier_state = [-0.60, 0.30]
later_state = [0.80, -0.30]

scores_a, alpha_a, context_a = attend(earlier_state, annotations, wa, ua, va)
scores_b, alpha_b, context_b = attend(later_state, annotations, wa, ua, va)
print('== Eq. 5/6: same source, different previous decoder state ==')
show('scores for earlier state', scores_a)
show('alpha for earlier state', alpha_a)
show('context for earlier state', context_a)
show('scores for later state', scores_b)
show('alpha for later state', alpha_b)
show('context for later state', context_b)

assert math.isclose(sum(alpha_a), 1.0, abs_tol=1e-12)
assert context_a == weighted_sum(alpha_a, annotations)
assert alpha_a != alpha_b
assert all(math.isclose(a, b, abs_tol=1e-12)
           for a, b in zip(alpha_a, softmax([score + 100.0 for score in scores_a])))

# U_a h_j는 target step과 무관하므로 cache 가능하다.
cached_source = [mat_vec(ua, h) for h in annotations]
state_projection = mat_vec(wa, earlier_state)
cached_scores = [dot(va, [math.tanh(v) for v in add(state_projection, source)])
                 for source in cached_source]
assert all(math.isclose(a, b, abs_tol=1e-12) for a, b in zip(scores_a, cached_scores))

hard_context = annotations[max(range(len(alpha_a)), key=alpha_a.__getitem__)]
assert context_a != hard_context
z, r, next_state = gru_step([0.40, -0.30], earlier_state, context_a)
print('\n== Appendix A.1.1: one gated decoder update ==')
show('update gate z', z)
show('reset gate r', r)
show('new decoder state s_i', next_state)
print('checks passed')
~~~

### 실제 실행 결과

~~~text
Python 3.9.6
scores for earlier state: [-1.285436, -0.576168, 0.152847]
alpha for earlier state: [0.138008, 0.280502, 0.581491]
context for earlier state: [0.539385, 0.033656]
scores for later state: [0.134497, 0.816551, 1.420296]
alpha for later state: [0.151619, 0.299892, 0.548489]
context for later state: [0.507130, 0.065669]
update gate z: [0.536018, 0.563025]
reset gate r: [0.427320, 0.482558]
new decoder state s_i: [-0.107652, 0.131049]
checks passed
~~~

첫 state에서 α는 약 `[0.138, 0.281, 0.581]`이다. 세 번째 annotation 비중이 가장 크지만 1은 아니다. hard context라면 `[0.90, -0.40]` 하나를 쓸 텐데, soft context는 세 annotation을 섞은 `[0.539385, 0.033656]`다. 또 같은 source annotation인데 previous decoder state만 바꾸자 α와 context가 바뀌었다. 이것이 “각 target step마다 다른 (c_i)”라는 식의 실제 숫자 의미다.

예제의 `assert`는 성공했다. 특히 `U_a h_j`를 미리 계산한 cached score가 매번 full formula로 계산한 score와 일치했고, 모든 score에 100을 더한 softmax도 바뀌지 않았다. 이 결과는 이 고정 toy parameter에 대한 **직접 실행 결과**다. 실제 trained RNNsearch가 자연어 번역에서 같은 수치를 낸다는 뜻은 아니다.

## 원 논문의 학습·평가 조건과 성능을 정확히 읽기

### data와 vocabulary의 경계

원 논문은 WMT14 English-French parallel corpus를 사용했다. 열거한 원본 corpus 합계는 850M words이고, Axelrod et al. data selection 후 348M words를 사용했다. 별도 monolingual data는 쓰지 않았다고 한다. validation은 `news-test-2012`와 `news-test-2013`, test는 training에 없던 3,003문장의 `news-test-2014`다.[원 논문 4.1절](https://arxiv.org/pdf/1409.0473)

tokenization 뒤에는 각 language에서 빈도가 높은 30,000 words만 shortlist로 남기고 나머지는 `[UNK]`로 mapped했다. lowercasing이나 stemming은 적용하지 않았다. 이 조건은 지금의 subword tokenizer와 다르다. 그러므로 `[UNK]`가 없는 subset 결과와 전체 결과를 섞으면 안 된다.

### model과 train 조건

| 항목 | 원 논문의 보고값 |
| --- | --- |
| RNNsearch encoder | forward GRU 1,000 hidden units + backward GRU 1,000 hidden units |
| RNNsearch decoder | 1,000 hidden units |
| target embedding (m) | 620 |
| maxout hidden size (l) | 500 |
| alignment hidden size (n'\) | 1,000 |
| batch | 80 sentences |
| optimizer | minibatch SGD(Stochastic Gradient Descent, 확률적 경사 하강법) + Adadelta |
| decoding | trained model 뒤 beam search(빔 탐색) |
| main text training 설명 | 각 model을 약 5일 training |

Appendix B의 더 구체적인 table은 RNNsearch-30을 Titan Black에서 113시간, RNNsearch-50을 Quadro K-6000에서 111시간, 더 오래 학습한 RNNsearch-50?를 252시간으로 기록한다. Adadelta hyperparameter(하이퍼파라미터)는 \(\epsilon=10^{-6}\), \(\rho=0.95\)이고 gradient L2 norm을 1 이하로 정규화했다. “약 5일”만 복사해 모든 variant가 같은 시간·GPU였다고 말하면 틀린다.[원 논문 Appendix B, Table 2](https://arxiv.org/pdf/1409.0473)

### Table 1의 BLEU는 전체와 no-UNK를 분리해야 한다

BLEU(Bilingual Evaluation Understudy, 기계번역 출력과 reference의 n-gram 겹침을 보는 지표)는 한 문장이 문법적으로 맞는지 완전히 판정하지 않는다. 원 논문 Table 1은 다음을 보고했다.

| Model | All BLEU | No UNK BLEU |
| --- | ---: | ---: |
| RNNencdec-30 | 13.93 | 24.19 |
| RNNsearch-30 | 21.50 | 31.44 |
| RNNencdec-50 | 17.82 | 26.71 |
| RNNsearch-50 | 26.75 | 34.16 |
| RNNsearch-50? | 28.45 | 36.15 |
| Moses phrase-based system | 33.30 | 35.63 |

`No UNK`은 candidate와 reference 모두 unknown word가 없는 문장만 평가하고, model이 `[UNK]`를 낼 수 없게 한 column이다. RNNsearch-50?의 **36.15는 no-UNK column에서 Moses 35.63과 비교 가능한 값**이다. 전체 `All`에서는 RNNsearch-50? 28.45가 Moses 33.30보다 낮다. 원 논문 abstract의 “phrase-based system에 comparable”을 “전체 BLEU에서 이겼다”로 바꾸면 안 된다.[원 논문 Table 1·5.1절](https://arxiv.org/pdf/1409.0473)

Figure 2에서는 fixed-vector RNNencdec의 BLEU가 source length가 길어질수록 크게 떨어지고 RNNsearch가 더 견고하다고 보고한다. 특히 저자들은 RNNsearch-50이 length 50 이상에서도 deterioration을 보이지 않았다고 해석했다. 이 figure는 2014 WMT task·30k word vocabulary·해당 training 조건에서의 결과이지, “attention이면 모든 long context 문제가 사라진다”는 일반 법칙이 아니다.

## 성능과 트레이드오프 — 고정 병목을 풀면 매번 읽는 비용이 생긴다

### 시간과 메모리 비용

source annotation을 한 번 만든 뒤, target step (i)마다 모든 (T_x) source annotation을 score한다. alignment hidden dimension을 (d_a)라 하면, attention score와 weighted sum의 대략적 비용은 다음과 같다.

| 단계 | 대략 비용 | 왜 |
| --- | --- | --- |
| bidirectional RNN encoding | (O(T_x)) recurrent steps | source를 양방향으로 순차 처리 |
| source projection (U_ah_j) cache | (O(T_xd_a\cdot2n)) | source 하나당 한 번 |
| target별 alignment + weighted sum | (O(T_xd_a+T_x\cdot2n)) | target step마다 source 전 위치를 훑음 |
| sentence 전체 attention | (O(T_xT_y)) 위치 쌍 | (T_y)번의 target step 각각에 (T_x)개 source 위치 |
| annotations 저장 | (O(T_x\cdot2n)) | decoder가 계속 읽을 source states |

Big-O(입력 크기에 따른 증가율)는 matrix 차원을 숨긴 요약이다. 실제로는 matrix multiply, batch packing, GPU memory bandwidth, beam width가 latency를 크게 바꾼다. 문장 번역에서 source는 보통 수십 token이라 저자는 15~40 word 길이에는 이 계산이 큰 문제가 아니라고 봤다. 그러나 논문 Section 6은 (T_x\times T_y)회 alignment를 계산해야 하므로 긴 task에 제약이 될 수 있다고 명시했다.[원 논문 6절](https://arxiv.org/pdf/1409.0473)

### cache는 계산량을 줄이되 source annotation 저장은 필요하다

(U_ah_j)는 i가 아닌 j만 따라 달라진다. 그래서 source projection은 decoder loop 전에 cache한다. GroundHog도 `fprop`에서 `p_from_c`를 먼저 계산하고 [`theano.scan`](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L462-L494)에 넘긴다. 대신 source annotation과 projection을 memory에 보관해야 한다. fixed vector 하나만 넘기던 model보다 memory 경로가 늘어난 대가다.

### 품질과 해석의 trade-off

| 얻는 것 | 치르는 것 |
| --- | --- |
| source 위치별 정보를 필요할 때 다시 읽음 | 매 target step마다 source 전체 score |
| soft alignment를 번역 loss 하나로 joint training | alignment matrix를 gold alignment처럼 과신하면 안 됨 |
| word order와 phrase 길이가 달라도 여러 위치를 섞음 | word-level 30k vocabulary의 UNK 문제는 남음 |
| fixed vector 병목 완화 | RNN의 source·target 순차성은 남음 |
| attention map을 관찰할 수 있음 | 관찰 가능한 weight가 곧 인과적 설명은 아님 |

## 실패와 한계, 운영에서 확인할 것

### 실패 1 — α의 행과 열을 거꾸로 읽는다

attention matrix를 heatmap으로 그릴 때 보통 target을 세로축, source를 가로축으로 놓는다. 원 논문 Figure 3도 x-axis가 English source words, y-axis가 generated French words이며 pixel이 α(_{ij})라고 설명한다. i는 target, j는 source다.[원 논문 Figure 3 caption](https://arxiv.org/pdf/1409.0473) 이를 뒤집으면 “어느 target이 어느 source를 봤나”를 반대로 해석한다.

**디버깅 순서.** 한 target step (i)를 고른 뒤 그 행의 α 합이 1인지 확인한다. source padding position weight가 0인지 확인한다. 그 다음 weighted sum (c_i)를 직접 다시 계산한다.

### 실패 2 — padding mask를 빼먹는다

batch에서 짧은 source 문장 뒤 padding이 있고 score가 아무리 작아도 softmax는 모든 위치에 양수 비중을 줄 수 있다. 유효하지 않은 위치 score를 softmax 전에 제거해야 한다. 구현에 따라 invalid score를 매우 큰 음수로 바꾸거나, GroundHog처럼 exponentiated energy에 mask를 곱할 수 있다. 어떤 방식이든 **정규화 분모에도 padding이 들어가지 않는지** test해야 한다.

### 실패 3 — attention을 가져왔는데 vocabulary 병목을 무시한다

이 논문은 unknown source/target word를 `[UNK]` 하나로 합쳤다. attention이 정확히 해당 source 위치를 크게 봐도 source ID 자체가 `[UNK]`라면 이름·희귀 단어의 세부 문자를 보존하지 못한다. subword tokenization, copy mechanism, open-vocabulary 처리 같은 문제는 이 논문의 해결 범위 밖이다.

### 실패 4 — generation에서 정답 prefix가 계속 주어진다고 착각한다

training loss 계산에서는 gold target (y_{i-1})가 있다. serving에서는 없다. decoder가 앞 step에서 잘못 고른 token이 다음 state와 attention query를 바꾼다. beam search는 여러 prefix를 유지해 이 오류를 줄이려는 근사 탐색이지, attention 자체가 decoding error를 제거하는 장치는 아니다.

### 실패 5 — attention map을 설명의 끝으로 삼는다

α는 decoder state와 annotation에서 얻은 normalized score다. annotation 자체에는 양방향 RNN의 문맥이 이미 섞여 있고, GRU state·previous token·maxout output도 target 확률에 들어간다. “α가 0.58인 단어가 답의 유일한 원인”이라고 말할 근거는 없다. map은 model behavior를 검사할 출발점이다. source mask, output token, score, context, loss까지 함께 봐야 한다.

### 운영 경계

2015 GroundHog code는 Theano와 Python 2 시대 구현이며 README도 많은 위치에 `float32`가 hardcoded되어 있다고 경고한다.[NMT README Known Issues](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/README.md#L134-L139) 이 repository를 지금 production serving에 그대로 택하라는 뜻이 아니다. 이 글에서의 역할은 논문식이 실제 code의 score → normalize → weighted context → gates 흐름으로 이어지는지 검증하는 것이다.

현대 production에서 attention 계열 model을 운영한다면 최소한 다음을 기록한다.

| 관찰값 | 왜 보나 |
| --- | --- |
| source/target length 분포 | (T_xT_y) attention 비용과 latency 증가를 조기에 본다 |
| padding mask 후 α 합과 invalid weight | data collator·mask bug를 찾는다 |
| UNK 또는 fallback 비율 | vocabulary/data coverage 문제를 본다 |
| beam width별 latency·품질 | 탐색 비용과 품질의 교환을 측정한다 |
| attention entropy | 지나치게 균등하거나 한 위치로 붕괴한 weight를 관찰하는 보조 신호다 |
| gold/reference 기반 translation metric | attention map이 아닌 실제 output 품질을 평가한다 |

entropy가 높다고 나쁘고 낮다고 좋다는 단일 임계값은 없다. language pair, 문장 길이, tokenization, target 위치에 따라 달라진다. 지표는 deployment 전 validation data와 오류 사례로 baseline을 잡아 해석한다.

## 대안과 선택 기준

| 선택지 | 무엇을 바꾸나 | 쓸 때 | 이 논문과의 관계 |
| --- | --- | --- | --- |
| fixed-vector RNN encoder-decoder | source 전체를 vector 하나로 넘김 | 아주 짧은 sequence의 역사적 baseline | 이 논문이 완화하려 한 병목 |
| Bahdanau additive attention | decoder state로 source annotations를 매 step soft-search | attention의 탄생 문제를 배울 때, RNN NMT를 이해할 때 | 이 글의 대상 |
| Luong dot-product attention | score 함수를 더 단순하게 둠 | RNN attention 변형을 비교할 때 | 같은 encoder-decoder 계보의 다른 score 선택 |
| Transformer cross-attention | Q/K/V projection, multi-head, 병렬화 가능한 layer | 현대 LLM·번역 architecture | Bahdanau의 “위치별 읽기”를 다른 architecture로 확장 |
| RAG | external index에서 document chunk를 검색 | knowledge가 model input 밖에 있고 갱신·근거가 필요할 때 | source sentence annotation과 외부 retrieval은 다름 |

현대 LLM을 만들려는 사람에게는 Transformer가 실무 출발점이다. 그럼에도 Bahdanau 논문을 건너뛰지 않는 이유는, “왜 model이 token을 생성할 때마다 입력 위치를 다시 읽어야 했나”를 가장 작은 구조로 보여 주기 때문이다. 반대로 대규모 문서 QA에 GraphRAG나 vector DB를 붙이려는 경우 이 모델의 α를 그대로 retrieval ranking이라고 부르면 안 된다. annotation은 한 input sentence를 encoder가 만든 내부 vector이고, retrieval index는 많은 document에서 후보를 찾아오는 별도 system이다.

## 흔한 오해와 처음 질문에 대한 답

### “Attention은 Transformer가 처음 만든 것 아닌가?”

아니다. Transformer는 attention만으로 sequence transduction을 만들고 scaled dot-product, multi-head attention을 널리 쓰게 한 2017 architecture다. 이 2015 논문은 RNN encoder-decoder에서 target마다 source 부분을 soft-search하는 attention을 제안했다. 같은 단어 아래의 구현·병렬성·score function은 다르다.

### “이 논문은 LSTM 번역기인가?”

정확히는 아니다. 논문은 LSTM을 사용할 수 있다고 하지만 Appendix A.1.1과 실험 architecture는 Cho et al.의 gated hidden unit, 즉 GRU 계열 update/reset gate를 쓴다. 이전 Seq2Seq 논문의 LSTM과 혼동하면 gate 식과 state 수가 달라진다.

### “α가 가장 큰 source word가 target word의 번역 원인인가?”

그렇게 단정할 수 없다. α는 learned alignment score를 softmax로 정규화한 weight다. source phrase 하나가 여러 target token으로 번역될 수 있고, 반대로 여러 source annotation을 섞을 수 있다. decoder state와 output layer도 결과에 관여한다.

### “soft alignment는 source 한 단어와 target 한 단어의 일대일 대응인가?”

아니다. 한 target step의 α는 source 전체에 분포하고, target마다 새로 계산된다. 원 논문도 `the man`이 프랑스어 `l'homme`로 번역되는 사례처럼 서로 다른 phrase 길이와 non-monotonic alignment를 다룰 수 있다고 설명한다.[원 논문 5.2절](https://arxiv.org/pdf/1409.0473)

### “Attention이 있으면 긴 문장 문제가 해결됐나?”

fixed vector 하나에 모두 압축하는 부담은 완화했다. 하지만 (T_x\times T_y) alignment 계산, RNN의 순차 실행, word vocabulary의 UNK, training length와 다른 long input 문제는 남는다. 논문도 long task에서 계산량 제약을 적었다.

### “RAG도 중요한 문장을 가중치로 고르니 같은가?”

비슷한 비유는 가능하지만 system boundary가 다르다. Bahdanau attention은 이미 주어진 source sentence의 dense annotation을 differentiable weighted sum으로 읽는다. RAG는 보통 수많은 외부 chunk 중 일부를 retrieval하고 model input에 넣는다. index build, freshness, access control, citation, retrieval recall은 이 논문의 attention 식만으로 해결되지 않는다.

## 출처 및 검증 경로

### 1차 자료

- Bahdanau, Cho, Bengio. [Neural Machine Translation by Jointly Learning to Align and Translate, arXiv 1409.0473v7](https://arxiv.org/pdf/1409.0473). 이 글의 식 (1)~(8), RNNsearch, GRU 선택, data/model/training 조건, Table 1·2, Figure 2·3, 한계의 1차 근거다.
- Bahdanau, Cho, Bengio. [arXiv 서지·판본 기록](https://arxiv.org/abs/1409.0473). 제출·개정 시점 확인에 사용했다.
- lisa-groundhog. [GroundHog NMT README, commit 4d2433b](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/README.md). 논문 연결 구현의 code structure, RNNsearch-50 prototype, legacy runtime 경계 확인에 사용했다.
- lisa-groundhog. [RNNsearch attention/GRU step, `encdec.py:299-419`, commit 4d2433b](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/encdec.py#L299-L419). score→normalize→weighted context→gates의 실제 호출 흐름 확인에 사용했다.
- lisa-groundhog. [RNNsearch prototype, `state.py:265-280`, commit 4d2433b](https://github.com/lisa-groundhog/GroundHog/blob/4d2433b6ff03da63f3619ecb3e8c501901779cdb/experiments/nmt/state.py#L265-L280). `search`, bidirectional encoder, sequence length 50 설정 확인에 사용했다.

### 보조 자료와 이 글의 직접 실행

- 이 글의 `verify_bahdanau_attention.py`는 Python 3.9.6에서 직접 실행했다. 고정 toy parameter로 Eq. (4)·(5)·(7)과 Appendix A.1.1의 GRU 한 step을 수치 검증한 결과이며, 원 논문 BLEU 재현이나 trained checkpoint 검증은 아니다.
- Sutskever, Vinyals, Le. [Sequence to Sequence Learning with Neural Networks](https://arxiv.org/abs/1409.3215). fixed-vector encoder-decoder가 Bahdanau 논문의 직접적인 비교 맥락임을 연결하는 보조 원문이다.
- Vaswani et al. [Attention Is All You Need](https://arxiv.org/abs/1706.03762). Transformer attention과 본 논문을 같은 구현으로 혼동하지 않기 위한 비교 원문이다.
