Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

어텐션

어텐션(attention)은 "지금 처리 중인 대상이 다른 어떤 요소들을 얼마나 참고해야 하는가"를 학습하는 메커니즘입니다. 2015년 기계번역에서 처음 제안된 뒤, 2017년 트랜스포머의 핵심 연산으로 자리 잡아 오늘날 GPT를 비롯한 모든 대형 언어모델의 토대가 되었습니다.

import numpy as np
import keras
from keras import ops, layers

print('Keras:', keras.__version__, '| backend:', keras.backend.backend())
Keras: 3.10.0 | backend: torch

1직관 — 질의·색인·값 (Query, Key, Value)

어텐션은 검색에 비유할 수 있습니다. 도서관에서 책을 찾는 상황을 떠올려 봅시다.

질의를 모든 색인과 견주어 얼마나 맞는지 점수를 매기고(=어텐션 점수), 그 점수를 가중치로 삼아 값들을 가중 평균해 가져옵니다. 잘 맞는 책의 내용은 많이, 덜 맞는 책은 적게 섞이는 것입니다. 어텐션의 모든 변형은 "점수를 어떻게 매기는가"만 다를 뿐, 이 점수 \to softmax \to 값의 가중합 골격은 같습니다.

아래 실습에서는 작은 행렬로 질의 QQ, 색인 KK, 값 VV 를 직접 만들어 씁니다.

rng = np.random.default_rng(0)
Q = rng.normal(size=(1, 2, 4)).astype("float32")   # (배치, 질의길이 Tq, 차원 d)
K = rng.normal(size=(1, 3, 4)).astype("float32")   # (배치, 색인길이 Tk, 차원 d)
V = rng.normal(size=(1, 3, 4)).astype("float32")   # (배치, 색인길이 Tk, 차원 d)
print("Q:", Q.shape, "| K:", K.shape, "| V:", V.shape)
Q: (1, 2, 4) | K: (1, 3, 4) | V: (1, 3, 4)

2가산 어텐션 (Additive Attention, 2015)

어텐션이 처음 제안된 형태입니다(Bahdanau et al. (2015)). RNN 기반 시퀀스-투-시퀀스 번역에서, 디코더가 매 시점 인코더의 어떤 위치를 볼지 학습하기 위해 도입되었습니다. 질의와 색인을 더한 뒤 비선형 함수를 거쳐 점수를 냅니다.

score(q,k)=vtanh(Wqq+Wkk).\text{score}(q,k) = v^{\top}\tanh(W_q\,q + W_k\,k).

여기서 Wq,Wk,vW_q, W_k, v 는 학습 파라미터입니다. 학습 가중치를 단위행렬로 두고 핵심만 남기면 score(q,k)=dtanh(q+k)d\text{score}(q,k)=\sum_d \tanh(q+k)_d 가 됩니다. 이 단순화된 형태를 직접 구현하고, 학습 스케일을 끈(use_scale=False) Keras 내장 계층과 비교합니다.

# 직접 구현: 모든 (질의, 색인) 쌍에 대해 tanh(q+k) 를 더해 점수를 만든다
점수 = ops.sum(ops.tanh(ops.expand_dims(Q, 2) + ops.expand_dims(K, 1)), axis=-1)  # (b, Tq, Tk)
가중치 = ops.softmax(점수, axis=-1)
출력 = ops.matmul(가중치, V)
print("어텐션 점수:", 점수.shape, "| 출력:", 출력.shape)

# Keras 내장 AdditiveAttention 과 대조 (정확히 일치해야 함)
참조 = layers.AdditiveAttention(use_scale=False)([Q, V, K])
assert np.allclose(ops.convert_to_numpy(출력), ops.convert_to_numpy(참조), atol=1e-5)
print("일치 — 직접 구현 == keras.layers.AdditiveAttention")
어텐션 점수: torch.Size([1, 2, 3]) | 출력: torch.Size([1, 2, 4])
일치 — 직접 구현 == keras.layers.AdditiveAttention

3내적 어텐션 (Dot-Product Attention, 2015)

같은 해 Luong 등이 제안한 더 효율적인 형태입니다(Luong et al. (2015)). 더하고 tanh\tanh 를 씌우는 대신, 질의와 색인의 내적(dot product) 으로 곧장 점수를 냅니다. 행렬곱 한 번으로 모든 쌍의 점수가 나오므로 빠릅니다.

질의 길이를 TQT_Q, 색인 길이를 TKT_K, 벡터 차원을 dd 라 하면 점수 행렬은 TQ×TKT_Q \times T_K 형태입니다.

QKRTQ×TK,QRTQ×d,KRTK×d.QK^{\top}\in\mathbb{R}^{T_Q\times T_K},\qquad Q\in\mathbb{R}^{T_Q\times d},\quad K\in\mathbb{R}^{T_K\times d}.

예를 들어 질의가 “transformer”, “attention” 두 단어이고 색인이 “전력, 영화, 음악, AI” 네 단어라면, 점수는 각 질의어가 각 색인어와 얼마나 관련되는지를 담은 2×42\times 4 행렬이 됩니다.

질의(Query)전력영화음악AI
transformer0.10.20.30.4
attention0.20.10.40.3
# 직접 구현: softmax(QKᵀ) V
점수 = ops.matmul(Q, ops.transpose(K, (0, 2, 1)))   # (b, Tq, Tk)
가중치 = ops.softmax(점수, axis=-1)
출력 = ops.matmul(가중치, V)
print("어텐션 점수:", 점수.shape, "| 출력:", 출력.shape)

# Keras 내장 Attention 과 대조
참조 = layers.Attention(use_scale=False, score_mode="dot")([Q, V, K])
assert np.allclose(ops.convert_to_numpy(출력), ops.convert_to_numpy(참조), atol=1e-5)
print("일치 — 직접 구현 == keras.layers.Attention")
어텐션 점수: torch.Size([1, 2, 3]) | 출력: torch.Size([1, 2, 4])
일치 — 직접 구현 == keras.layers.Attention

4스케일드 닷-프로덕트 어텐션 (Transformer, 2017)

트랜스포머(Vaswani et al. (2017))가 채택한 형태로, 내적 어텐션에 스케일링 한 단계를 더한 것입니다.

Attention(Q,K,V)=softmax ⁣(QKdk)V.\text{Attention}(Q,K,V)=\text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d_k}}\right)V.

dk\sqrt{d_k} 로 나누는가. 차원 dkd_k 가 커지면 내적값의 분산도 같이 커져, softmax 입력이 지나치게 큰 양수·음수로 벌어집니다. 그러면 softmax가 한쪽으로 포화(saturate)해 거의 0과 1만 내놓고, 그래디언트가 사라져 학습이 어려워집니다. 점수를 dk\sqrt{d_k} 로 나누면 분산이 안정되어 이 문제를 막습니다. 바로 이 연산이 GPT 의 자기어텐션 내부에서 쓰입니다.

d_k = Q.shape[-1]
점수 = ops.matmul(Q, ops.transpose(K, (0, 2, 1))) / ops.sqrt(float(d_k))
가중치 = ops.softmax(점수, axis=-1)
출력 = ops.matmul(가중치, V)
print("스케일드 닷-프로덕트 출력:", 출력.shape)

# 스케일 한 줄을 뺀 내적 어텐션과 비교하면 가중치 분포가 달라진다
print("스케일 적용 가중치:\n", np.round(ops.convert_to_numpy(가중치)[0], 3))
스케일드 닷-프로덕트 출력: torch.Size([1, 2, 4])
스케일 적용 가중치:
 [[0.327 0.217 0.456]
 [0.183 0.158 0.658]]

5셀프 어텐션 (Self-Attention)

지금까지는 질의와 색인이 서로 다른 시퀀스였습니다(예: 디코더의 질의로 인코더의 색인을 참조). 셀프 어텐션 은 질의·색인·값이 모두 같은 입력 시퀀스 에서 나옵니다. 즉 한 문장 안에서 각 단어가 다른 단어들과 어떻게 관련되는지를 학습합니다.

"배 타고 배 먹으니 배 아프다"라는 문장을 봅시다. 같은 "배"라도 문맥에 따라 탈것·과일·신체를 뜻합니다. 셀프 어텐션으로 각 단어가 다른 단어와 맺는 관계(어텐션 점수)를 구하면, 예컨대 다음과 같은 점수표를 얻습니다(값은 이해를 돕기 위한 예시).

질의/색인배(1)타고배(2)먹으니배(3)아프다
배(1)1.00.60.10.10.40.2
타고0.61.00.30.10.10.1
배(2)0.10.31.00.40.30.2
먹으니0.10.10.41.00.30.5
배(3)0.40.10.30.31.00.6
아프다0.20.10.20.50.61.0

"배(1)"은 "타고"와, "먹으니"는 “배(2)”·"아프다"와 강하게 묶입니다. 이 미정규화 점수를 행마다 softmax로 정규화하면 각 행의 합이 1인 확률 분포가 되어 "각 단어가 다른 단어를 얼마나 참고할지"가 정해집니다. 구현은 앞의 스케일드 닷-프로덕트와 같되, Q=K=VQ=K=V 로 같은 시퀀스를 넣기만 하면 됩니다.

임베딩 = rng.normal(size=(1, 6, 8)).astype("float32")   # 문장 6단어, 8차원 표현
Q = K = V = 임베딩                                       # 셀프 어텐션: 셋이 동일
d_k = 임베딩.shape[-1]

점수 = ops.matmul(Q, ops.transpose(K, (0, 2, 1))) / ops.sqrt(float(d_k))
가중치 = ops.softmax(점수, axis=-1)
출력 = ops.matmul(가중치, V)
print("자기어텐션 가중치:", 가중치.shape, "(행 합 = 1)")
print("행 합 확인:", np.round(ops.convert_to_numpy(ops.sum(가중치, axis=-1))[0], 3))
자기어텐션 가중치: torch.Size([1, 6, 6]) (행 합 = 1)
행 합 확인: [1. 1. 1. 1. 1. 1.]

6인과 어텐션 — GPT로 가는 다리

언어 생성 모델은 다음 단어를 예측할 때 미래를 보면 안 됩니다. 셀프 어텐션에 인과 마스크(causal mask) 를 씌워 각 위치가 자기 자신과 과거만 참고하도록 막습니다. 점수 행렬의 위쪽 삼각(미래)을 -\infty 로 채우면 softmax 후 그 가중치가 0이 됩니다.

T = 6
마스크 = ops.tril(ops.ones((T, T)))                       # 하삼각 1 = 참조 허용
print(ops.convert_to_numpy(마스크).astype(int))

가려진점수 = ops.where(마스크 == 0, float("-inf"), 점수)    # 미래를 -inf 로
인과가중치 = ops.softmax(가려진점수, axis=-1)
print("\n인과 가중치(첫 행은 자기 자신만 참고):")
print(np.round(ops.convert_to_numpy(인과가중치)[0], 3))
[[1 0 0 0 0 0]
 [1 1 0 0 0 0]
 [1 1 1 0 0 0]
 [1 1 1 1 0 0]
 [1 1 1 1 1 0]
 [1 1 1 1 1 1]]

인과 가중치(첫 행은 자기 자신만 참고):
[[1.    0.    0.    0.    0.    0.   ]
 [0.08  0.92  0.    0.    0.    0.   ]
 [0.036 0.02  0.944 0.    0.    0.   ]
 [0.083 0.065 0.041 0.811 0.    0.   ]
 [0.039 0.116 0.011 0.017 0.817 0.   ]
 [0.035 0.035 0.066 0.005 0.016 0.844]]

이 인과적 자기어텐션이 GPT인과적_자기어텐션 레이어에서 그대로 쓰입니다.

7멀티 헤드 어텐션 (Multi-Head Attention)

단어 사이의 관계는 한 종류가 아닙니다(문법적·의미적·위치적 등). 멀티헤드 는 표현 차원을 여러 헤드(head) 로 쪼개, 각 헤드가 서로 다른 관계 를 병렬로 학습하게 한 뒤 결과를 이어 붙입니다. 즉 같은 스케일드 닷-프로덕트 어텐션을 여러 부분공간에서 동시에 수행합니다.

def 멀티헤드_어텐션(Q, K, V, 헤드수):
    b, Tq, C = Q.shape; Tk = K.shape[1]; 헤드차원 = C // 헤드수
    def 헤드분리(x, T):                                    # (b, T, C) -> (b, 헤드수, T, 헤드차원)
        x = ops.reshape(x, (b, T, 헤드수, 헤드차원))
        return ops.transpose(x, (0, 2, 1, 3))
    q, k, v = 헤드분리(Q, Tq), 헤드분리(K, Tk), 헤드분리(V, Tk)
    점수 = ops.matmul(q, ops.transpose(k, (0, 1, 3, 2))) / ops.sqrt(float(헤드차원))
    가중치 = ops.softmax(점수, axis=-1)
    y = ops.matmul(가중치, v)                              # 헤드별 어텐션
    y = ops.transpose(y, (0, 2, 1, 3))
    return ops.reshape(y, (b, Tq, C))                      # 헤드 다시 이어 붙이기

Q = rng.normal(size=(1, 2, 8)).astype("float32")
K = V = rng.normal(size=(1, 3, 8)).astype("float32")
출력 = 멀티헤드_어텐션(Q, K, V, 헤드수=2)
print("멀티헤드 출력:", 출력.shape)
멀티헤드 출력: torch.Size([1, 2, 8])

이 구현이 올바른지, 헤드를 따로따로 계산해 이어 붙인 결과와 비교해 확인합니다.

헤드수, 헤드차원 = 2, 4
조각 = []
for h in range(헤드수):
    q = Q[:, :, h*헤드차원:(h+1)*헤드차원]
    k = K[:, :, h*헤드차원:(h+1)*헤드차원]
    v = V[:, :, h*헤드차원:(h+1)*헤드차원]
    점수 = ops.matmul(q, ops.transpose(k, (0, 2, 1))) / ops.sqrt(float(헤드차원))
    조각.append(ops.matmul(ops.softmax(점수, axis=-1), v))
참조 = ops.concatenate(조각, axis=-1)
assert np.allclose(ops.convert_to_numpy(출력), ops.convert_to_numpy(참조), atol=1e-5)
print("일치 — 멀티헤드 == 헤드별 어텐션을 이어 붙인 결과")
일치 — 멀티헤드 == 헤드별 어텐션을 이어 붙인 결과

실무에서는 이 모든 과정에 학습 가능한 투영(projection)을 더한 keras.layers.MultiHeadAttention 을 고수준 부품으로 바로 쓸 수 있습니다.

시퀀스길이, 벡터차원 = 20, 128
입력 = keras.Input(shape=(시퀀스길이, 벡터차원))
출력 = layers.MultiHeadAttention(num_heads=8, key_dim=벡터차원 // 8)(입력, 입력)
assert tuple(출력.shape[1:]) == (시퀀스길이, 벡터차원)
print("MultiHeadAttention 출력 형태:", 출력.shape)
MultiHeadAttention 출력 형태: (None, 20, 128)

GPT인과적_자기어텐션 은 바로 이 멀티헤드 구조에 인과 마스크를 더하고, Q·K·V 투영을 Dense 로 직접 만든 것입니다.

8정리

References
  1. Bahdanau, D., Cho, K., & Bengio, Y. (2015). Neural Machine Translation by Jointly Learning to Align and Translate. https://arxiv.org/abs/1409.0473
  2. Luong, M.-T., Pham, H., & Manning, C. D. (2015). Effective Approaches to Attention-based Neural Machine Translation. https://arxiv.org/abs/1508.04025
  3. Vaswani, A., Shazeer, N., Parmar, N., Uszkoreit, J., Jones, L., Gomez, A. N., Kaiser, L., & Polosukhin, I. (2017). Attention Is All You Need. https://arxiv.org/abs/1706.03762