어텐션 메커니즘 — 결정적 돌파구

디코더(decoder)는 더 이상 압축된 요약(summary)을 가늘게 눈을 뜨고 들여다보지 않고, 원문(source) 전체를 바라보기 시작합니다. 이 이후의 거의 모든 것은 어텐션(attention)과 엔지니어링(engineering)의 조합일 뿐입니다.

유형: Build 언어: Python 선수 강의: Phase 5 · 09 (시퀀스-투-시퀀스 모델, Sequence-to-Sequence Models) 예상 시간: 약 45분

학습 목표

  • 바다나우 어텐션(Bahdanau attention)의 가중 평균(weighted average) 구조를 설명합니다.
  • 쿼리(query), 키(key), 값(value) 관점에서 고전적(classical) 어텐션을 해석합니다.
  • 가산(additive) 어텐션과 곱셈(multiplicative) 어텐션의 형상(shape)을 단계별로 추적합니다.
  • 어텐션 가중치를 모델의 설명(explanation)으로 사용할 때의 한계를 이해합니다.

문제

Lesson 09는 측정 가능한 실패로 끝났습니다. 장난감 복사 과제(toy copy task)에서 GRU 인코더-디코더(encoder-decoder)는 길이 5에서는 89% 정확도(accuracy)를 보이지만, 길이 80에서는 거의 무작위 수준(chance)으로 떨어집니다. 그 이유는 학습 버그(training bug)가 아니라 구조 자체에 있습니다. 인코더가 모은 모든 정보가 하나의 고정 크기 은닉 상태(fixed-size hidden state)에 압축되어야 하고, 디코더는 그 이외의 것을 전혀 볼 수 없기 때문입니다.

바다나우(Bahdanau), 조(Cho), 벤지오(Bengio)는 2014년에 세 줄로 요약되는 해결책을 제시했습니다. 디코더에게 마지막 인코더 상태만 건네지 말고, 모든 인코더 상태를 그대로 유지합니다. 디코더의 각 단계(step)에서는 "지금 인코더 위치 i를 얼마나 봐야 하는가?"를 가중치(weight)로 계산하고, 그 가중치를 이용해 인코더 상태의 가중 평균을 만듭니다. 이 가중 평균이 바로 문맥(context)이며, 디코더 단계마다 새로 만들어집니다.

이것이 아이디어의 전부입니다. 트랜스포머(Transformer)는 이를 확장했습니다. 셀프 어텐션(self-attention)은 같은 시퀀스 안에서 어텐션을 적용했고, 멀티 헤드 어텐션(multi-head attention)은 이를 병렬로 수행했습니다. 그러나 이미 2014년 버전의 어텐션만으로 병목(bottleneck)은 깨졌고, 그 다음 트랜스포머로의 전환은 개념적 도약이라기보다 엔지니어링의 영역에 가깝습니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.기본 인코더-디코더(encoder-decoder)에서 고정 크기 은닉 상태(fixed-size hidden state)가 입력 시퀀스가 길어질수록 병목이 되는 이유는 무엇인가요?

2.어텐션(attention)의 쿼리-키-값(query-key-value) 프레임워크에서 쿼리(query)의 역할은 무엇인가요?

0/2 답변 완료

개념

encoder hidden states (kept, not discarded) h1 h2 h3 h4 h5 Je ne suis pas chat s_{t-1} query score + softmax α_{t,i} = softmax( score(s_{t-1}, h_i) ) context c_t = Σ α_{t,i} · h_i weighted average of encoder states decoder step outputs next token no more fixed-size bottleneck. context reshapes every step.

디코더 단계 t마다 다음을 수행합니다.

  1. 이전 디코더 은닉 상태 s_{t-1}쿼리(query) 로 사용합니다.
  2. 이를 모든 인코더 은닉 상태 h_1, ..., h_T와 견주어 점수(score)를 매깁니다. 인코더 위치마다 스칼라(scalar) 하나가 나옵니다.
  3. 점수에 소프트맥스(softmax)를 적용해 합이 1이 되는 어텐션 가중치 α_{t,1}, ..., α_{t,T}를 얻습니다.
  4. 문맥 벡터(context vector) c_t = Σ α_{t,i} * h_i를 계산합니다. 인코더 상태의 가중 평균입니다.
  5. 디코더는 c_t와 직전 출력 토큰(token)을 받아 다음 토큰을 생성합니다.

핵심은 가중 평균입니다. 디코더가 JeI로 번역해야 할 때는 Je 위치의 인코더 상태에 높은 가중치를 둡니다. not을 만들어야 할 때는 pas 위치의 가중치가 커집니다. 문맥 벡터는 단계마다 새로 재구성됩니다.

형상(Shapes) — 모두를 무는 그 부분

어텐션 구현이 처음 시도에서 가장 자주 어긋나는 지점입니다. 천천히 읽으시기 바랍니다.

대상형상(Shape)메모
인코더 은닉 상태 H(T_enc, d_h)양방향 LSTM(BiLSTM)이면 d_h = 2 * d_hidden
디코더 은닉 상태 s_{t-1}(d_s,)벡터 한 개
어텐션 점수 e_{t,i}스칼라인코더 위치마다 하나
어텐션 가중치 α_{t,i}스칼라모든 i에 대한 소프트맥스 결과, 합이 1
문맥 벡터 c_t(d_h,)인코더 상태와 같은 형상

바다나우(가산, additive) 점수. e_{t,i} = v_α^T * tanh(W_a * s_{t-1} + U_a * h_i).

  • s_{t-1}의 형상은 (d_s,), h_i의 형상은 (d_h,)입니다.
  • W_a의 형상은 (d_attn, d_s), U_a의 형상은 (d_attn, d_h)입니다.
  • tanh 내부의 합은 형상이 (d_attn,)입니다.
  • v_α의 형상은 (d_attn,)이고, v_α와의 내적(inner product)이 스칼라 점수로 축소됩니다. 이것이 v_α가 하는 일입니다. 어떤 마법이 아니라, 어텐션 차원 벡터를 스칼라 점수로 바꾸는 사영(projection)일 뿐입니다.

루옹(곱셈, multiplicative) 점수. 세 가지 변형이 있습니다.

  • dot: e_{t,i} = s_t^T * h_i. d_s == d_h가 강제되는 조건입니다. 인코더가 양방향이면 이 방식은 피하시기 바랍니다.
  • general: e_{t,i} = s_t^T * W * h_i이며, W의 형상은 (d_s, d_h)입니다. 차원이 같아야 한다는 제약이 사라집니다.
  • concat: 사실상 바다나우 형태입니다. 앞의 두 방식이 더 가볍기 때문에 거의 쓰지 않습니다.

바다나우 / 루옹에서 짚어야 할 한 가지 함정. 바다나우는 현재 단어를 생성하기 이전 디코더 상태인 s_{t-1}을 사용하고, 루옹은 생성한 이후 상태인 s_t를 사용합니다. 둘을 섞어 쓰면 기울기(gradient)가 미묘하게 어긋나 디버깅(debugging)이 매우 어려워집니다. 한 논문을 골랐다면 그 논문의 규약(convention)을 끝까지 따르시기 바랍니다.

직접 만들기

Step 1: 가산(바다나우) 어텐션

import numpy as np


def additive_attention(decoder_state, encoder_states, W_a, U_a, v_a):
    projected_dec = W_a @ decoder_state
    projected_enc = encoder_states @ U_a.T
    combined = np.tanh(projected_enc + projected_dec)
    scores = combined @ v_a
    weights = softmax(scores)
    context = weights @ encoder_states
    return context, weights


def softmax(x):
    x = x - np.max(x)
    e = np.exp(x)
    return e / e.sum()

위 형상 표와 비교해 검증해보시기 바랍니다. encoder_states의 형상은 (T_enc, d_h), projected_enc(T_enc, d_attn), projected_dec(d_attn,)이라 브로드캐스트(broadcast)됩니다. combined(T_enc, d_attn), scores(T_enc,), weights(T_enc,), context(d_h,)입니다. 그대로 출시(ship)할 수 있는 형태입니다.

Step 2: 루옹의 dot과 general

def dot_attention(decoder_state, encoder_states):
    scores = encoder_states @ decoder_state
    weights = softmax(scores)
    return weights @ encoder_states, weights


def general_attention(decoder_state, encoder_states, W):
    projected = W.T @ decoder_state
    scores = encoder_states @ projected
    weights = softmax(scores)
    return weights @ encoder_states, weights

각 함수가 세 줄 정도로 끝납니다. 이 단순함이 루옹 논문이 빠르게 자리 잡은 이유입니다. 대부분의 과제에서 정확도는 비슷하면서 코드는 훨씬 적습니다.

Step 3: 숫자로 따라가 보는 예시

세 개의 인코더 상태가 대략 "cat", "sat", "mat"를 나타낸다고 가정하고, 디코더 상태가 첫 번째 상태와 가장 잘 정렬되어 있다면, 어텐션 분포는 위치 0에 집중됩니다. 디코더 상태를 세 번째 인코더 상태와 정렬되도록 바꾸면 가중치는 위치 2로 이동하고, 문맥 벡터도 그 흐름을 따라갑니다.

H = np.array([
    [1.0, 0.0, 0.2],
    [0.5, 0.5, 0.1],
    [0.1, 0.9, 0.3],
])

s_close_to_cat = np.array([0.9, 0.1, 0.2])
ctx, w = dot_attention(s_close_to_cat, H)
print("weights:", w.round(3))
weights: [0.464 0.305 0.231]

첫 번째 행이 가장 큰 가중치를 얻습니다. 그런 다음 디코더 상태를 세 번째 인코더 상태에 가깝게 움직여보면, 가중치가 이동하는 것을 직접 관찰할 수 있습니다. 어텐션은 결국 명시적인 정렬(alignment)입니다.

Step 4: 트랜스포머로 이어지는 다리인 이유

위 설명을 Q/K/V 언어로 번역하면 다음과 같습니다.

  • 쿼리(Query) = 디코더 상태 s_{t-1}
  • 키(Key) = 인코더 상태들 (점수를 계산할 대상)
  • 값(Value) = 인코더 상태들 (가중치를 곱해 합산할 대상)

고전적 어텐션에서는 키와 값이 같은 대상입니다. 셀프 어텐션은 둘을 분리해서, 하나의 시퀀스를 자기 자신에게 질의하게 만들고 K와 V에 서로 다른 학습된 사영을 적용합니다. 멀티 헤드 어텐션은 서로 다른 학습된 사영을 가지고 이 과정을 병렬로 수행합니다. 트랜스포머는 이 단계 전체를 여러 번 쌓고 RNN을 걷어내 버린 구조입니다.

수학은 동일합니다. 형상도 동일합니다. 바다나우 어텐션에서 스케일드 닷-프로덕트 어텐션(scaled dot-product attention)으로 넘어가는 교육적 도약은 대부분 표기(notation)의 변화에 지나지 않습니다.

사용하기

PyTorch와 TensorFlow는 어텐션을 직접 제공합니다.

import torch
import torch.nn as nn

mha = nn.MultiheadAttention(embed_dim=128, num_heads=8, batch_first=True)
query = torch.randn(2, 5, 128)
key = torch.randn(2, 10, 128)
value = torch.randn(2, 10, 128)

output, weights = mha(query, key, value)
print(output.shape, weights.shape)
torch.Size([2, 5, 128]) torch.Size([2, 5, 10])

이것이 트랜스포머의 어텐션 층(layer)입니다. 쿼리 배치는 5개 위치, 키/값 배치는 10개 위치, 각 벡터는 128차원이며 헤드(head)는 8개입니다. output은 문맥이 보강된 새 쿼리들이고, weights는 시각화 가능한 5x10 정렬 행렬(alignment matrix)입니다.

고전적 어텐션이 여전히 중요한 자리

  • 교육 목적. 단일 헤드, 단일 층, RNN 기반 버전은 모든 개념을 눈으로 직접 확인할 수 있게 해줍니다.
  • 트랜스포머가 들어가지 않는 온디바이스(on-device) 시퀀스 과제.
  • 2014-2017년의 논문을 읽을 때. 바다나우의 규약을 모르면 잘못 읽기 쉽습니다.
  • 기계 번역(machine translation, MT)에서의 세밀한 정렬(fine-grained alignment) 분석. 트랜스포머 모델에서도 원시 어텐션 가중치는 해석 도구로 쓰이며, 그 의미를 알아야 제대로 읽을 수 있습니다.

어텐션 가중치를 곧 설명으로 보는 함정

어텐션 가중치는 해석 가능해 보입니다. 위치별로 합이 1이고, 그래프로 그릴 수 있으며, 값이 크다는 것이 곧 "여기를 봤다"처럼 느껴집니다. 평가자들이 좋아할 만한 그림이기도 합니다.

하지만 보이는 것만큼 해석 가능하지는 않습니다. Jain과 Wallace(2019)는 일부 과제에서 어텐션 분포를 임의의 다른 분포로 바꾸어도 모델의 예측이 달라지지 않을 수 있음을 보였습니다. 어블레이션(ablation)이나 반사실(counterfactual) 검증 없이 어텐션 가중치를 추론의 근거로 제시하지 않도록 하시기 바랍니다.

산출물 만들기

outputs/prompt-attention-shapes.md로 저장합니다.

---
name: attention-shapes
description: Debug shape bugs in attention implementations.
phase: 5
lesson: 10
---

Given a broken attention implementation, you identify the shape mismatch. Output:

1. Which matrix has the wrong shape. Name the tensor.
2. What its shape should be, derived from (d_s, d_h, d_attn, T_enc, T_dec, batch_size).
3. One-line fix. Transpose, reshape, or project.
4. A test to catch regressions. Typically: assert `output.shape == (batch, T_dec, d_h)` and `weights.shape == (batch, T_dec, T_enc)` and `weights.sum(dim=-1) close to 1`.

Refuse to recommend fixes that silently broadcast. Broadcast-hiding bugs surface later as silent accuracy degradation, the worst kind of attention bug.

For Bahdanau confusion, insist the decoder input is `s_{t-1}` (pre-step state). For Luong, `s_t` (post-step state). For dot-product, flag dimension mismatch between query and key as the most common first-time error.

연습문제

  1. 쉬움. 인코더의 패딩 토큰(padding token)이 어텐션 가중치 0을 받도록 마스킹된 소프트맥스(masked softmax)를 구현합니다. 가변 길이(variable-length) 시퀀스가 섞인 배치(batch)에서 동작을 검증합니다.
  2. 중간. 루옹 general 형태에 멀티 헤드 어텐션을 추가합니다. d_hn_heads 그룹으로 나누고, 헤드별로 어텐션을 수행한 뒤 결과를 이어붙입니다(concatenate). 단일 헤드 경우가 이전 구현과 일치하는지 검증합니다.
  3. 어려움. Lesson 09의 장난감 복사 과제에 바다나우 어텐션을 붙인 GRU 인코더-디코더를 학습합니다. 시퀀스 길이별 정확도를 그래프로 그리고 어텐션이 없는 기준선(baseline)과 비교합니다. 길이가 길어질수록 격차가 벌어지는 것을 통해 어텐션이 병목을 풀어준다는 사실을 확인합니다.

핵심 용어

용어흔한 설명실제 의미
어텐션(Attention)무언가를 본다쿼리-키 유사도(similarity)로 계산한 가중치를 사용해, 값 시퀀스를 가중 평균하는 연산이다.
쿼리, 키, 값(Query, Key, Value)QKV세 가지 사영이다. Q는 묻고, K는 매칭 대상이며, V는 반환할 값이다.
가산 어텐션(Additive attention)바다나우피드포워드(feed-forward) 점수: v^T tanh(W q + U k).
곱셈 어텐션(Multiplicative attention)루옹 dot/general점수가 q^T k 또는 q^T W k. 더 저렴하며 대부분의 과제에서 정확도가 비슷하다.
정렬 행렬(Alignment matrix)예쁜 그림어텐션 가중치를 (T_dec, T_enc) 격자로 본 것. 모델이 무엇을 보았는지를 읽어내기 위한 도구다.

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

main
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 1개

attention-shapes

Debug shape bugs in attention implementations.

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.바다나우(Bahdanau, 가산) 어텐션에서 벡터 v_alpha의 형상(shape)은 (d_attn,)입니다. v_alpha의 역할은 무엇인가요?

2.양방향 LSTM(BiLSTM) 인코더(d_h = 2 * d_hidden)와 단방향 디코더(d_s = d_hidden)를 사용해 루옹(Luong) dot-product 어텐션을 구현했더니 내적 e = s^T h가 실패합니다. 원인은 무엇인가요?

3.동료가 어텐션 히트맵(heatmap)에서 특정 원문 토큰에 높은 가중치가 나타나는 것을 보여주며, 이것이 모델이 해당 토큰이 중요하다고 '이해'한다는 증거라고 주장합니다. 이 주장의 핵심 문제는 무엇인가요?

0/3 답변 완료

추가 문제 풀기

AI가 강의 내용을 바탕으로 새로운 문제를 생성합니다