이미지 로딩 중...
AI Generated
2025. 11. 24. · 6 Views
Attention Mechanism 완벽 이해
딥러닝의 핵심 기술인 Attention Mechanism을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 실제 코드 예제와 함께 번역, 이미지 처리 등 실무에서 어떻게 활용되는지 알아봅니다.
목차
- Attention의 기본 개념
- Scaled Dot-Product Attention
- Multi-Head Attention
- Self-Attention
- Positional Encoding
- Masked Attention
- Cross-Attention
- Attention의 시간 복잡도와 최적화
- Attention Visualization과 해석
- Attention의 실무 활용 사례
1. Attention의 기본 개념
시작하며
여러분이 영어 문장을 한국어로 번역할 때 이런 상황을 겪어본 적 있나요? "The cat sat on the mat"을 번역하려면 "고양이", "앉았다", "매트 위에"처럼 단어들을 적절히 연결해야 합니다.
기계가 이걸 어떻게 할까요? 전통적인 방법으로는 문장 전체를 하나의 숫자 덩어리로 압축했습니다.
마치 긴 이야기를 한 줄로 요약하는 것처럼요. 그런데 이렇게 하면 중요한 정보가 많이 사라집니다.
특히 문장이 길어질수록 앞쪽 내용을 까먹게 되죠. 바로 이럴 때 필요한 것이 Attention입니다.
Attention은 "지금 이 순간, 어떤 단어에 집중해야 할까?"를 스스로 판단합니다. 마치 여러분이 책을 읽을 때 중요한 부분은 천천히, 덜 중요한 부분은 빠르게 읽는 것처럼요.
개요
간단히 말해서, Attention은 입력 데이터의 어느 부분이 중요한지 가중치를 계산하는 메커니즘입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하면, 번역 시스템에서 "I love you"를 "나는 너를 사랑해"로 번역할 때 "love"는 "사랑해"와 강하게 연결되어야 하고, "you"는 "너를"과 연결되어야 합니다.
예를 들어, 챗봇이 긴 대화 내용 중에서 지금 답변에 필요한 정보만 골라내는 경우에 매우 유용합니다. 기존에는 RNN이나 LSTM으로 순차적으로 정보를 처리했다면, 이제는 모든 입력을 동시에 보면서 중요도를 계산할 수 있습니다.
이게 바로 Transformer의 핵심이죠. Attention의 핵심 특징은 첫째, 동적으로 중요도를 계산한다는 점, 둘째, 병렬 처리가 가능하다는 점, 셋째, 긴 문장에서도 정보 손실이 적다는 점입니다.
이러한 특징들이 ChatGPT, BERT 같은 최신 AI 모델의 기반이 되었기 때문에 매우 중요합니다.
코드 예제
import numpy as np
# Query, Key, Value 벡터 (간단한 예시)
query = np.array([1, 0, 1]) # 찾고자 하는 정보
keys = np.array([[1, 0, 0], [0, 1, 0], [1, 1, 0]]) # 각 단어의 특징
values = np.array([[1, 2], [3, 4], [5, 6]]) # 각 단어의 실제 값
# Attention Score 계산: Query와 Key의 유사도
scores = np.dot(keys, query) # [1, 0, 1] - 첫 번째와 세 번째가 중요
print(f"Attention Scores: {scores}")
# Softmax로 확률 변환 (합이 1이 되도록)
attention_weights = np.exp(scores) / np.sum(np.exp(scores))
print(f"Attention Weights: {attention_weights}")
# 가중치를 적용하여 최종 출력 계산
output = np.dot(attention_weights, values)
print(f"Output: {output}")
설명
이것이 하는 일: Attention은 세 가지 요소(Query, Key, Value)를 사용해서 "지금 무엇에 집중할까?"를 계산합니다. 마치 도서관에서 책을 찾을 때 검색어(Query)를 입력하면 책 제목(Key)과 비교해서 가장 관련 있는 책의 내용(Value)을 가져오는 것과 같습니다.
첫 번째로, Query와 Key의 내적(dot product)을 계산합니다. 이 과정은 "얼마나 비슷한가?"를 숫자로 나타냅니다.
예를 들어, query=[1,0,1]과 key=[1,0,0]의 내적은 1이고, key=[0,1,0]의 내적은 0입니다. 숫자가 클수록 더 관련이 있다는 뜻입니다.
그 다음으로, Softmax 함수가 실행되면서 이 점수들을 확률로 변환합니다. Softmax는 모든 값을 0과 1 사이로 바꾸고, 합이 1이 되도록 만듭니다.
이렇게 하면 "30%는 첫 번째 단어에, 70%는 세 번째 단어에 집중해야 해"처럼 명확한 비율을 얻을 수 있습니다. 마지막으로, 이 가중치를 Value에 곱해서 가중 평균을 계산합니다.
중요한 Value는 크게, 덜 중요한 Value는 작게 반영됩니다. 최종적으로 "지금 이 순간 필요한 정보"만 압축된 벡터를 만들어냅니다.
여러분이 이 코드를 사용하면 긴 문장에서도 관련 있는 부분만 골라낼 수 있고, 번역이나 요약 같은 작업의 정확도를 크게 높일 수 있습니다. 또한 모델이 "왜 이렇게 판단했는지" 가중치를 통해 설명할 수 있어서 해석 가능한 AI를 만드는 데도 유용합니다.
실전 팁
💡 Query는 "무엇을 찾을까?", Key는 "나는 무엇인가?", Value는 "내 실제 내용은 무엇인가?"로 기억하면 개념이 명확해집니다.
💡 내적(dot product) 값이 너무 커지면 Softmax가 극단적인 값(0 또는 1)을 만들어내므로, 실전에서는 sqrt(d_k)로 나눠서 스케일링합니다.
💡 Attention 가중치를 시각화하면 모델이 어디에 집중하는지 한눈에 볼 수 있어서 디버깅에 매우 유용합니다.
💡 배치 처리 시 3D 텐서(batch_size, seq_len, d_model)로 확장해야 하므로, numpy보다 PyTorch나 TensorFlow를 사용하는 것이 편합니다.
💡 Self-Attention(자기 자신에게 Attention)과 Cross-Attention(다른 시퀀스에 Attention)을 구분해서 사용하면 Encoder-Decoder 구조를 이해하기 쉽습니다.
2. Scaled Dot-Product Attention
시작하며
여러분이 Attention을 실제로 구현하다 보면 이런 문제를 만납니다. Query와 Key의 차원이 커질수록(예: 512차원) 내적 값이 너무 커져서 Softmax가 거의 0 또는 1만 출력하게 됩니다.
이걸 "gradient vanishing" 문제라고 합니다. 이 문제는 학습이 제대로 안 되는 원인이 됩니다.
Softmax의 출력이 극단적이면 미분값(gradient)이 거의 0이 되어서 가중치 업데이트가 안 되거든요. 마치 가파른 절벽에서는 한 발짝도 못 움직이는 것처럼요.
바로 이럴 때 필요한 것이 Scaled Dot-Product Attention입니다. 단순히 내적 결과를 sqrt(d_k)로 나눠주는 것만으로 이 문제를 해결합니다.
이렇게 하면 Softmax 입력값이 적절한 범위에 들어와서 학습이 안정적으로 진행됩니다.
개요
간단히 말해서, Scaled Dot-Product Attention은 기본 Attention에 스케일링을 추가한 버전입니다. 수식으로는 Attention(Q,K,V) = softmax(QK^T / sqrt(d_k))V로 표현됩니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, BERT나 GPT 같은 대형 모델은 512차원 이상의 임베딩을 사용합니다. 스케일링 없이는 학습이 거의 불가능할 정도로 불안정해집니다.
예를 들어, 번역 모델을 학습시킬 때 loss가 발산(NaN)하는 경우가 많은데, 이게 바로 스케일링 누락 때문인 경우가 많습니다. 기존에는 내적 값이 [-100, 100] 범위로 커졌다면, 이제는 [-10, 10] 정도로 조절됩니다.
이 작은 차이가 Softmax의 안정성에 큰 영향을 미칩니다. 핵심 특징은 첫째, 계산 비용이 거의 안 늘어난다는 점(나눗셈 한 번), 둘째, 차원이 커져도 안정적이라는 점, 셋째, Transformer의 표준이 되었다는 점입니다.
이러한 특징들이 대규모 언어 모델을 가능하게 만들었기 때문에 필수적으로 알아야 합니다.
코드 예제
import numpy as np
def scaled_dot_product_attention(Q, K, V):
"""
Q: Query 행렬 (batch_size, seq_len, d_k)
K: Key 행렬 (batch_size, seq_len, d_k)
V: Value 행렬 (batch_size, seq_len, d_v)
"""
d_k = Q.shape[-1] # Key의 차원
# QK^T 계산: Query와 Key의 유사도
scores = np.matmul(Q, K.transpose(-2, -1)) # (batch, seq_len, seq_len)
# 스케일링: sqrt(d_k)로 나누기
scores = scores / np.sqrt(d_k)
# Softmax로 확률 변환
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# Value에 가중치 적용
output = np.matmul(attention_weights, V)
return output, attention_weights
# 테스트 예시
Q = np.random.randn(1, 4, 64) # 1개 배치, 4개 단어, 64차원
K = np.random.randn(1, 4, 64)
V = np.random.randn(1, 4, 64)
output, weights = scaled_dot_product_attention(Q, K, V)
print(f"Output shape: {output.shape}")
설명
이것이 하는 일: 이 함수는 Query, Key, Value 세 개의 행렬을 받아서 Attention을 계산하되, 스케일링을 통해 수치적 안정성을 확보합니다. 첫 번째로, Query와 Key의 전치 행렬(transpose)을 곱합니다.
이 과정에서 (seq_len, seq_len) 크기의 점수 행렬이 나옵니다. 각 원소는 "i번째 단어가 j번째 단어와 얼마나 관련 있는가?"를 나타냅니다.
예를 들어, "The cat sat"에서 "cat"과 "sat"의 관련도가 점수로 나타나죠. 그 다음으로, 이 점수를 sqrt(d_k)로 나눕니다.
d_k가 64라면 8로 나누는 것입니다. 왜 제곱근일까요?
수학적으로 d_k차원 벡터의 내적은 평균적으로 d_k에 비례해서 커지기 때문에, sqrt(d_k)로 나누면 분산이 1 정도로 유지됩니다. 이렇게 하면 Softmax 입력값이 너무 극단적이지 않게 됩니다.
세 번째로, Softmax를 적용해서 각 행의 합이 1이 되도록 만듭니다. 이제 "cat"을 표현할 때 "The"에 20%, "cat" 자신에게 50%, "sat"에 30%의 가중치를 준다는 식으로 해석할 수 있습니다.
마지막으로, 이 가중치를 Value 행렬에 곱해서 최종 출력을 만듭니다. 중요한 단어의 Value는 크게, 덜 중요한 단어의 Value는 작게 반영되어 새로운 표현이 만들어집니다.
여러분이 이 코드를 사용하면 BERT, GPT, T5 같은 최신 모델과 동일한 방식으로 Attention을 구현할 수 있습니다. 또한 attention_weights를 반환하므로 모델이 어디에 집중했는지 시각화할 수 있어서, 모델의 동작을 이해하고 디버깅하는 데 큰 도움이 됩니다.
실전 팁
💡 d_k는 보통 64 또는 128을 사용하며, 너무 크면 계산량이 늘고 너무 작으면 표현력이 떨어집니다.
💡 Softmax 계산 시 numerical stability를 위해 최댓값을 빼주는 trick(scores - np.max(scores))을 추가하면 overflow를 방지할 수 있습니다.
💡 실전에서는 Masking을 추가해서 미래 단어를 보지 못하게 하거나(GPT) padding 토큰을 무시합니다(BERT).
💡 배치 연산을 위해 numpy보다 torch.matmul이나 tf.matmul을 사용하면 GPU 가속이 가능해서 100배 이상 빨라집니다.
💡 Attention 가중치를 저장했다가 heatmap으로 그리면 "번역할 때 어느 단어를 참고했는지" 직관적으로 볼 수 있습니다.
3. Multi-Head Attention
시작하며
여러분이 영어 문장을 분석할 때 이런 생각을 해본 적 있나요? "cat"이라는 단어는 문법적으로는 주어이고, 의미적으로는 동물이고, 감정적으로는 귀여운 느낌을 줍니다.
한 가지 관점만으로는 충분하지 않습니다. 단일 Attention은 하나의 관점만 볼 수 있습니다.
예를 들어, "문법적 관계"에만 집중하면 "의미적 유사성"을 놓칠 수 있습니다. 마치 한쪽 눈만 뜨고 사물을 보면 입체감이 없는 것처럼요.
바로 이럴 때 필요한 것이 Multi-Head Attention입니다. 여러 개의 Attention을 동시에 수행해서 다양한 관점을 모두 포착합니다.
8개의 head가 있다면 8가지 다른 관점에서 문장을 분석하는 것이죠.
개요
간단히 말해서, Multi-Head Attention은 Attention을 여러 번 병렬로 수행한 후 결과를 합치는 메커니즘입니다. 각 head는 독립적으로 학습되어 서로 다른 패턴을 찾아냅니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 단일 Attention은 표현력의 한계가 있습니다. 예를 들어, "은행"이라는 단어가 "금융기관"인지 "강가"인지는 문맥에 따라 다른데, 여러 head가 있으면 하나는 금융 관련 단어에, 다른 하나는 지리 관련 단어에 집중할 수 있습니다.
챗봇이 사용자 의도를 파악할 때 이런 다각도 분석이 매우 유용합니다. 기존에는 d_model=512 차원의 단일 Attention을 사용했다면, 이제는 8개의 head로 나눠서 각각 d_k=64 차원의 Attention을 수행합니다.
계산량은 비슷하지만 표현력은 훨씬 좋아집니다. 핵심 특징은 첫째, 각 head가 다른 패턴을 학습한다는 점, 둘째, 병렬 처리가 가능해서 빠르다는 점, 셋째, Transformer의 필수 구성 요소라는 점입니다.
이러한 특징들이 BERT가 다양한 NLP 작업에서 뛰어난 성능을 내는 비결입니다.
코드 예제
import numpy as np
def multi_head_attention(Q, K, V, num_heads=8):
"""
Q, K, V: (batch_size, seq_len, d_model)
num_heads: head 개수 (보통 8 또는 16)
"""
batch_size, seq_len, d_model = Q.shape
d_k = d_model // num_heads # 각 head의 차원
# Q, K, V를 num_heads개로 분할
Q = Q.reshape(batch_size, seq_len, num_heads, d_k).transpose(0, 2, 1, 3)
K = K.reshape(batch_size, seq_len, num_heads, d_k).transpose(0, 2, 1, 3)
V = V.reshape(batch_size, seq_len, num_heads, d_k).transpose(0, 2, 1, 3)
# 결과: (batch_size, num_heads, seq_len, d_k)
# 각 head마다 Scaled Dot-Product Attention 수행
scores = np.matmul(Q, K.transpose(-2, -1)) / np.sqrt(d_k)
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
output = np.matmul(attention_weights, V)
# head들을 다시 합치기
output = output.transpose(0, 2, 1, 3).reshape(batch_size, seq_len, d_model)
return output
# 테스트
Q = np.random.randn(2, 10, 512) # 2개 배치, 10개 단어, 512차원
K, V = Q.copy(), Q.copy()
output = multi_head_attention(Q, K, V, num_heads=8)
print(f"Output shape: {output.shape}") # (2, 10, 512)
설명
이것이 하는 일: 이 함수는 하나의 큰 Attention을 여러 개의 작은 Attention으로 분할해서 동시에 실행하고, 결과를 다시 합칩니다. 마치 여러 전문가가 동시에 문서를 분석하는 것과 같습니다.
첫 번째로, Q, K, V를 num_heads개로 분할합니다. 예를 들어, 512차원을 8개 head로 나누면 각 head는 64차원을 담당합니다.
reshape과 transpose를 사용해서 (batch, num_heads, seq_len, d_k) 형태로 변환하는데, 이렇게 하면 각 head가 독립적으로 계산됩니다. numpy의 broadcasting 덕분에 8개 head가 동시에 처리됩니다.
그 다음으로, 각 head마다 Scaled Dot-Product Attention을 수행합니다. 8개 head가 있다면 8개의 attention_weights가 생성되는데, 각각 다른 단어 쌍에 집중합니다.
예를 들어, head 1은 "주어-동사" 관계에, head 2는 "형용사-명사" 관계에, head 3은 "의미적 유사성"에 집중할 수 있습니다. 세 번째로, 각 head의 출력을 다시 원래 차원으로 합칩니다.
transpose와 reshape를 역순으로 적용해서 (batch, seq_len, d_model) 형태로 돌아갑니다. 이 과정을 "concatenation"이라고 하며, 8개의 64차원 벡터가 하나의 512차원 벡터로 합쳐집니다.
마지막으로, 실전에서는 여기에 Linear 레이어를 추가해서 최종 출력을 만듭니다. 이 Linear 레이어는 head들의 정보를 적절히 섞어주는 역할을 합니다.
여러분이 이 코드를 사용하면 단일 Attention보다 훨씬 풍부한 표현을 얻을 수 있습니다. Transformer 논문에 따르면, 8-head Attention이 single-head보다 BLEU 점수(번역 품질)가 2-3점 높습니다.
또한 각 head의 가중치를 시각화하면 "이 head는 품사에, 저 head는 의미에 집중한다"는 걸 발견할 수 있어서 모델의 내부 동작을 이해하는 데 큰 도움이 됩니다.
실전 팁
💡 num_heads는 보통 8 또는 16을 사용하며, d_model이 num_heads로 나누어떨어져야 합니다(512를 8로 나누면 64).
💡 각 head의 attention_weights를 저장해서 시각화하면 어떤 head가 어떤 패턴을 학습했는지 알 수 있습니다.
💡 실전에서는 Q, K, V를 분할하기 전에 Linear 변환(학습 가능한 가중치 행렬)을 적용해서 표현력을 높입니다.
💡 GPU 메모리가 부족하면 num_heads를 줄이는 것보다 d_model을 줄이는 게 효과적입니다(예: 512→256).
💡 Attention 계산 시 (seq_len × seq_len) 행렬을 만들므로, 문장이 길면(>1024) 메모리가 급증합니다. 이때 Sparse Attention이나 Linformer 같은 효율적 변형을 고려하세요.
4. Self-Attention
시작하며
여러분이 "The cat sat on the mat"이라는 문장을 읽을 때, "it"이 무엇을 가리키는지 어떻게 알까요? 앞의 "cat"을 참조해야 합니다.
문장 내 단어들은 서로 영향을 주고받으며 의미가 결정됩니다. 전통적인 RNN은 순차적으로 처리해서 느리고, CNN은 고정된 범위만 볼 수 있어서 제한적입니다.
특히 "첫 번째 단어가 열 번째 단어와 관련 있다"는 식의 장거리 의존성을 포착하기 어렵습니다. 바로 이럴 때 필요한 것이 Self-Attention입니다.
문장 자기 자신에게 Attention을 적용해서 모든 단어 쌍의 관계를 한 번에 계산합니다. "cat"이 "sat"과 얼마나 관련 있는지, "the"가 "mat"과 얼마나 관련 있는지 전부 파악하는 것이죠.
개요
간단히 말해서, Self-Attention은 Query, Key, Value가 모두 같은 입력에서 나오는 Attention입니다. 즉, 문장이 스스로를 참조해서 각 단어의 새로운 표현을 만듭니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 문맥을 이해하는 데 필수적입니다. 예를 들어, "사과를 먹었다"에서 "사과"는 과일이지만, "사과를 했다"에서는 사죄의 의미입니다.
Self-Attention을 통해 "먹었다"와 "했다"라는 주변 단어를 보고 의미를 구분할 수 있습니다. 감정 분석, 질의응답, 텍스트 생성 등 거의 모든 NLP 작업에 사용됩니다.
기존에는 "왼쪽에서 오른쪽으로" 또는 "위에서 아래로" 같은 순차적 처리를 했다면, 이제는 모든 단어가 동시에 서로를 참조합니다. 이게 Transformer가 RNN보다 빠르고 정확한 이유입니다.
핵심 특징은 첫째, 위치에 상관없이 모든 단어 쌍을 고려한다는 점, 둘째, 병렬 처리가 가능해서 GPU를 효율적으로 사용한다는 점, 셋째, 해석 가능성이 높아서 attention map으로 모델의 판단 근거를 볼 수 있다는 점입니다. 이러한 특징들이 BERT를 11개 NLP 작업에서 최고 성능으로 만들었습니다.
코드 예제
import numpy as np
def self_attention(X, d_k=64):
"""
X: 입력 시퀀스 (seq_len, d_model)
d_k: Key 차원 (Query, Key 차원과 동일)
"""
seq_len, d_model = X.shape
# Q, K, V는 모두 같은 입력 X에서 생성 (실전에서는 학습 가능한 가중치 사용)
Q = X.copy() # Query: "무엇을 찾을까?"
K = X.copy() # Key: "나는 무엇인가?"
V = X.copy() # Value: "내 실제 내용"
# Self-Attention 계산
scores = np.matmul(Q, K.T) / np.sqrt(d_k) # (seq_len, seq_len)
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# 각 단어의 새로운 표현 생성
output = np.matmul(attention_weights, V) # (seq_len, d_model)
return output, attention_weights
# 예시: "The cat sat" 문장의 3개 단어 임베딩
sentence = np.random.randn(3, 128) # 3개 단어, 128차원
output, weights = self_attention(sentence, d_k=128)
print(f"Input shape: {sentence.shape}")
print(f"Output shape: {output.shape}")
print(f"Attention weights:\n{weights}") # 어느 단어가 어느 단어를 참조했는지
설명
이것이 하는 일: Self-Attention은 입력 문장의 각 단어가 다른 모든 단어를 참조해서 새로운 표현을 만듭니다. 마치 회의에서 모든 사람이 다른 사람의 의견을 듣고 자기 의견을 업데이트하는 것과 같습니다.
첫 번째로, 입력 X에서 Q, K, V를 만듭니다. 이 예제에서는 단순히 복사했지만, 실전에서는 X에 학습 가능한 가중치 행렬(W_Q, W_K, W_V)을 곱해서 생성합니다.
이렇게 하면 모델이 학습 과정에서 "Query는 이런 관점으로, Key는 저런 관점으로 보는 게 좋다"는 걸 배웁니다. 그 다음으로, Q와 K의 전치행렬을 곱해서 (seq_len, seq_len) 크기의 점수 행렬을 만듭니다.
이 행렬의 (i, j) 원소는 "i번째 단어가 j번째 단어와 얼마나 관련 있는가?"를 나타냅니다. 예를 들어, "The cat sat"에서 "cat"(2번 단어)이 "sat"(3번 단어)을 참조하는 정도가 점수로 나타납니다.
세 번째로, Softmax로 각 행을 확률로 변환합니다. 각 단어는 다른 모든 단어에 대한 가중치 분포를 갖게 됩니다.
"cat"이 "The"에 10%, 자기 자신에게 50%, "sat"에 40% 집중한다는 식이죠. 대각선 값이 높으면 자기 자신에 집중하는 것이고, 비대각선이 높으면 다른 단어를 많이 참조하는 것입니다.
마지막으로, 이 가중치를 Value에 곱해서 최종 출력을 만듭니다. 각 단어의 새로운 표현은 "자기 자신 + 관련 있는 다른 단어들의 정보"가 섞인 결과입니다.
이렇게 만들어진 표현은 원래 임베딩보다 훨씬 풍부한 문맥 정보를 담고 있습니다. 여러분이 이 코드를 사용하면 단어의 의미를 문맥에 맞게 조정할 수 있습니다.
BERT는 Self-Attention을 12층 쌓아서 사용하는데, 층을 거듭할수록 더 추상적이고 복잡한 관계를 학습합니다. 또한 attention_weights를 분석하면 "이 모델이 왜 이 문장을 긍정으로 판단했는지" 같은 해석이 가능해서, 금융이나 의료 같은 신뢰가 중요한 분야에서 특히 유용합니다.
실전 팁
💡 Self-Attention의 시간 복잡도는 O(n²)이므로 문장이 길면(n>1024) 계산량이 급증합니다. Longformer, BigBird 같은 효율적 변형을 고려하세요.
💡 대각선 값(자기 자신에 대한 attention)이 너무 높으면 정보 교환이 안 되므로, residual connection(잔차 연결)으로 원래 입력을 더해줍니다.
💡 Position Encoding을 추가하지 않으면 "cat sat"과 "sat cat"을 구분 못하므로, 실전에서는 반드시 위치 정보를 넣어야 합니다.
💡 Masking을 사용하면 GPT처럼 "미래 단어를 보지 못하게" 할 수 있습니다(attention_weights[i, j] = -inf if j > i).
💡 여러 층의 attention_weights를 모두 시각화하면 층마다 다른 패턴(1층은 인접 단어, 5층은 장거리 의존성)을 학습한다는 걸 발견할 수 있습니다.
5. Positional Encoding
시작하며
여러분이 Self-Attention을 사용하다 보면 이상한 문제를 발견합니다. "I love you"와 "you love I"가 똑같은 결과를 만들어냅니다.
단어의 순서가 완전히 무시되는 것이죠. 이 문제는 Self-Attention의 근본적인 한계입니다.
Attention은 모든 단어 쌍을 동시에 보기 때문에 "첫 번째", "두 번째" 같은 순서 정보가 없습니다. RNN은 순서대로 읽어서 자동으로 위치를 알았지만, Transformer는 그렇지 않습니다.
바로 이럴 때 필요한 것이 Positional Encoding입니다. 각 단어의 임베딩에 위치 정보를 더해주는 것이죠.
마치 책의 각 단어에 페이지 번호를 적어주는 것처럼, 모델이 단어의 순서를 알 수 있게 해줍니다.
개요
간단히 말해서, Positional Encoding은 각 단어의 위치를 나타내는 벡터를 임베딩에 더하는 기법입니다. 원본 Transformer 논문에서는 sin/cos 함수를 사용한 공식을 제안했습니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 언어는 순서가 중요합니다. "개가 고양이를 쫓았다"와 "고양이가 개를 쫓았다"는 완전히 다른 의미입니다.
예를 들어, 기계 번역에서 어순이 바뀌면 의미가 완전히 달라지므로, Positional Encoding 없이는 제대로 된 번역이 불가능합니다. 질의응답 시스템에서도 "누가 누구를 ~했다"를 구분하려면 위치 정보가 필수입니다.
기존에는 RNN이 순차 처리로 자동으로 위치를 파악했다면, 이제는 명시적으로 위치 정보를 벡터로 만들어서 더해줍니다. 계산 비용은 거의 없지만(덧셈만) 효과는 엄청납니다.
핵심 특징은 첫째, sin/cos 함수를 사용해서 긴 문장에도 대응할 수 있다는 점, 둘째, 고정된 공식이라 학습이 필요 없다는 점(요즘은 학습 가능한 방식도 사용), 셋째, 상대적 위치 관계를 표현할 수 있다는 점입니다. 이러한 특징들이 Transformer가 모든 순서 정보를 제대로 이해하게 만들어줍니다.
코드 예제
import numpy as np
def positional_encoding(seq_len, d_model):
"""
seq_len: 시퀀스 길이 (단어 개수)
d_model: 임베딩 차원 (512, 768 등)
"""
# 위치별, 차원별 encoding 행렬 초기화
pe = np.zeros((seq_len, d_model))
for pos in range(seq_len): # 각 위치마다
for i in range(0, d_model, 2): # 짝수 차원마다
# sin 함수로 짝수 차원 채우기
pe[pos, i] = np.sin(pos / (10000 ** (i / d_model)))
# cos 함수로 홀수 차원 채우기
if i + 1 < d_model:
pe[pos, i + 1] = np.cos(pos / (10000 ** (i / d_model)))
return pe
# 예시: 10개 단어, 512차원
pos_encoding = positional_encoding(seq_len=10, d_model=512)
print(f"Positional Encoding shape: {pos_encoding.shape}")
# 실제 사용: 임베딩에 더하기
word_embeddings = np.random.randn(10, 512) # 단어 임베딩
input_with_position = word_embeddings + pos_encoding # 위치 정보 추가
print(f"Final input shape: {input_with_position.shape}")
설명
이것이 하는 일: 이 함수는 각 위치(0, 1, 2, ...)마다 고유한 벡터를 만들어서 원래 단어 임베딩에 더해줍니다. 위치가 다르면 벡터도 달라지므로 모델이 순서를 구분할 수 있습니다.
첫 번째로, (seq_len, d_model) 크기의 0 행렬을 만듭니다. 예를 들어, 10개 단어와 512차원이면 (10, 512) 행렬이 됩니다.
각 행은 하나의 위치를 나타내고, 각 열은 임베딩의 한 차원을 나타냅니다. 그 다음으로, 각 위치와 차원마다 sin 또는 cos 값을 계산합니다.
공식이 복잡해 보이지만 핵심은 간단합니다. pos/(10000^(i/d_model))에서 pos가 커질수록(뒤쪽 단어일수록) 값이 변하고, i가 작을수록(낮은 차원일수록) 빠르게 진동합니다.
짝수 차원은 sin, 홀수 차원은 cos를 사용하는데, 이렇게 하면 각 위치가 유일한 패턴을 갖습니다. 세 번째로, 왜 sin/cos를 사용할까요?
첫째, 주기 함수라서 비슷한 위치는 비슷한 벡터를 갖습니다. 둘째, 학습 때 보지 못한 긴 문장에도 적용 가능합니다(extrapolation).
셋째, sin(a+b) = sin(a)cos(b) + cos(a)sin(b) 같은 삼각함수 공식 덕분에 상대적 위치를 선형 변환으로 표현할 수 있습니다. 마지막으로, 이렇게 만든 positional encoding을 원래 word embedding에 더합니다.
"cat"이라는 단어는 첫 번째 위치에 있을 때와 다섯 번째 위치에 있을 때 다른 입력 벡터를 갖게 되어, Self-Attention이 순서를 고려할 수 있게 됩니다. 여러분이 이 코드를 사용하면 Transformer가 어순을 제대로 이해하게 됩니다.
실험 결과, Positional Encoding 없이는 BLEU 점수가 10점 이상 떨어집니다. 또한 sin/cos 패턴을 시각화하면 낮은 차원은 빠르게 진동하고(단어 단위), 높은 차원은 천천히 진동해서(문장 단위) 다양한 스케일의 위치 정보를 담는다는 걸 볼 수 있습니다.
실전 팁
💡 원본 Transformer는 고정된 sin/cos를 사용했지만, BERT는 학습 가능한 positional embedding을 사용해서 더 좋은 성능을 냅니다.
💡 10000이라는 상수는 최대 문장 길이를 고려한 값인데, 더 긴 문장을 다루려면 이 값을 키워야 합니다.
💡 Positional Encoding은 임베딩과 같은 차원이어야 하므로, d_model=512라면 positional encoding도 512차원이어야 합니다.
💡 상대적 위치가 중요한 작업(예: 음악 생성)에는 Relative Positional Encoding을 사용하면 더 좋습니다.
💡 시각화할 때 첫 2-3개 차원만 그려보면 위치마다 뚜렷한 패턴이 보이는데, 이게 모델이 순서를 구분하는 신호입니다.
6. Masked Attention
시작하며
여러분이 GPT로 문장을 생성할 때 이런 문제를 생각해본 적 있나요? "The cat"까지 생성했을 때 다음 단어를 예측하려면 "cat" 이후의 단어를 보면 안 됩니다.
그건 정답을 미리 보는 셀수 없는 것과 같으니까요. 그런데 Self-Attention은 모든 단어를 동시에 봅니다.
학습할 때 "The cat sat"을 입력하면 "sat"을 예측할 때 "sat" 자체를 볼 수 있게 되어 버립니다. 이러면 모델이 제대로 학습하지 못하고, 생성할 때도 문제가 생깁니다.
바로 이럴 때 필요한 것이 Masked Attention입니다. 미래의 단어를 "가려서" 볼 수 없게 만드는 것이죠.
마치 시험 문제지의 뒷부분을 손으로 가리고 앞부분만 보는 것처럼, 이미 생성된 단어만 참조할 수 있게 합니다.
개요
간단히 말해서, Masked Attention은 attention score에 마스크를 적용해서 특정 위치를 참조하지 못하게 하는 기법입니다. GPT 같은 autoregressive 모델에서 필수적입니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 텍스트 생성 모델은 왼쪽에서 오른쪽으로 단어를 하나씩 만들어냅니다. 예를 들어, "오늘 날씨가 ___"를 생성할 때 빈칸 이후의 단어를 보면 안 됩니다.
Masked Attention이 없으면 모델이 "cheating"하게 되어, 학습은 빨라 보이지만 실제 생성할 때는 엉망인 결과가 나옵니다. 코드 자동완성, 챗봇 응답 생성 등 모든 생성 작업에 사용됩니다.
기존에는 RNN이 순차적으로 처리해서 자연스럽게 미래를 볼 수 없었다면, Transformer는 병렬 처리하기 때문에 명시적으로 마스크를 씌워야 합니다. 핵심 특징은 첫째, -inf(음의 무한대)를 사용해서 Softmax 출력을 0으로 만든다는 점, 둘째, 학습과 추론에서 동일하게 작동한다는 점, 셋째, 삼각형 마스크(upper triangular)를 사용한다는 점입니다.
이러한 특징들이 GPT-3가 일관성 있는 긴 텍스트를 생성하게 만들어줍니다.
코드 예제
import numpy as np
def masked_attention(Q, K, V):
"""
Q, K, V: (batch_size, seq_len, d_k)
미래 단어를 볼 수 없게 masking 적용
"""
seq_len = Q.shape[1]
d_k = Q.shape[-1]
# Attention score 계산
scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(d_k)
# 결과: (batch_size, seq_len, seq_len)
# 마스크 생성: 상삼각 행렬 (upper triangular)
# mask[i, j] = True if j > i (미래 위치)
mask = np.triu(np.ones((seq_len, seq_len)), k=1).astype(bool)
# 미래 위치에 -inf 적용 (Softmax 후 0이 됨)
scores[:, mask] = -1e9 # -inf 대신 -1e9 사용 (numerical stability)
# Softmax 적용
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# Value에 가중치 적용
output = np.matmul(attention_weights, V)
return output, attention_weights
# 예시: 5개 단어 시퀀스
Q = np.random.randn(1, 5, 64)
K, V = Q.copy(), Q.copy()
output, weights = masked_attention(Q, K, V)
print(f"Attention weights (masked):\n{weights[0]}")
# 상삼각 부분이 0이 되어야 함
설명
이것이 하는 일: 이 함수는 Self-Attention을 수행하되, 각 위치가 자신보다 뒤에 있는 위치를 참조하지 못하게 막습니다. i번째 단어는 0~i번째 단어만 볼 수 있고, i+1번째 이후는 볼 수 없습니다.
첫 번째로, 일반적인 Scaled Dot-Product Attention처럼 Q와 K를 곱해서 점수 행렬을 만듭니다. 이 시점에서는 (seq_len, seq_len) 행렬의 모든 원소가 의미 있는 값입니다.
예를 들어, 3번째 단어가 5번째 단어를 참조하는 점수도 계산되어 있습니다. 그 다음으로, numpy.triu 함수로 상삼각 행렬을 만듭니다.
k=1 파라미터는 "대각선 위쪽"을 의미하는데, 즉 mask[i, j] = True if j > i입니다. 이 마스크가 True인 위치는 "미래"를 의미하므로 가려야 합니다.
예를 들어, 5개 단어면 [[0,1,1,1,1], [0,0,1,1,1], ...] 같은 패턴이 됩니다. 세 번째로, 마스크가 True인 위치에 매우 작은 값(-1e9)을 넣습니다.
-inf를 직접 쓰면 numerical error가 날 수 있어서 -1e9 같은 큰 음수를 사용합니다. Softmax는 exp(-1e9) ≈ 0을 만들어내므로, 결과적으로 해당 위치의 가중치가 0이 됩니다.
마지막으로, Softmax를 적용하면 각 행에서 미래 위치의 가중치는 0이 되고, 과거와 현재 위치의 가중치만 남습니다. 예를 들어, 3번째 단어는 0, 1, 2, 3번째 단어에만 가중치를 주고 4, 5번째는 완전히 무시합니다.
이렇게 만들어진 output은 "과거와 현재만 보고 다음을 예측"하는 패턴을 학습하게 됩니다. 여러분이 이 코드를 사용하면 GPT처럼 순차적으로 텍스트를 생성하는 모델을 만들 수 있습니다.
Masked Attention 덕분에 모델은 학습 때와 생성 때 동일하게 작동하며, "한 번에 한 단어씩" 생성하는 자연스러운 패턴을 배웁니다. 또한 attention_weights를 시각화하면 하삼각 행렬 패턴이 보이는데, 이게 제대로 마스킹되었다는 증거입니다.
실전 팁
💡 -inf 대신 -1e9를 사용하는 이유는 exp(-inf)가 NaN을 만들 수 있기 때문입니다. -1e9면 충분히 작아서 Softmax 후 거의 0이 됩니다.
💡 Padding 토큰도 마스킹해야 하므로, 실전에서는 causal mask(미래 가리기)와 padding mask(빈 칸 가리기)를 합쳐 사용합니다.
💡 생성할 때는 한 번에 한 단어씩 추가되므로, 매번 전체 시퀀스를 다시 계산하는 대신 KV cache를 사용하면 10배 이상 빨라집니다.
💡 BERT는 양방향(모든 단어를 볼 수 있음)이고 GPT는 단방향(마스킹 사용)이므로, 작업에 따라 적절한 모델을 선택하세요.
💡 Attention 가중치를 시각화하면 하삼각 행렬 패턴이 명확히 보여야 합니다. 상삼각에 값이 있다면 마스킹 버그입니다.
7. Cross-Attention
시작하며
여러분이 번역 모델을 만든다고 상상해보세요. "I love you"를 "나는 너를 사랑해"로 번역할 때, "사랑해"를 생성하는 순간 영어의 어느 단어를 봐야 할까요?
당연히 "love"를 봐야겠죠. Self-Attention은 같은 언어 내에서만 작동합니다.
영어는 영어끼리, 한국어는 한국어끼리만 참조할 수 있습니다. 그런데 번역, 이미지 캡셔닝, 질의응답 같은 작업은 두 개의 다른 시퀀스를 연결해야 합니다.
바로 이럴 때 필요한 것이 Cross-Attention입니다. Query는 한 시퀀스(예: 한국어)에서, Key와 Value는 다른 시퀀스(예: 영어)에서 가져옵니다.
마치 통역사가 한국어를 말하면서(Query) 영어 원문(Key, Value)을 계속 참조하는 것처럼요.
개요
간단히 말해서, Cross-Attention은 Query와 Key/Value가 서로 다른 소스에서 나오는 Attention입니다. Encoder-Decoder 구조에서 Decoder가 Encoder의 출력을 참조할 때 사용됩니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 두 도메인 간의 연결이 필수적인 작업이 많습니다. 예를 들어, 영어→한국어 번역에서 "나는"을 생성할 때 영어의 "I"를 봐야 하고, "사랑해"를 생성할 때는 "love"를 봐야 합니다.
이미지 캡셔닝에서는 "고양이"라는 단어를 생성할 때 이미지의 고양이 영역을 봐야 하죠. 질의응답, 문서 요약, 비디오 설명 등 거의 모든 sequence-to-sequence 작업에 사용됩니다.
기존에는 Encoder의 마지막 출력 하나만 Decoder에 전달했다면(context vector), 이제는 Encoder의 모든 시점을 Decoder가 동적으로 참조할 수 있습니다. 이게 Attention 메커니즘의 원래 동기였습니다.
핵심 특징은 첫째, Query와 Key/Value의 출처가 다르다는 점, 둘째, Decoder의 각 단계마다 Encoder 전체를 다시 본다는 점, 셋째, "어느 입력 부분이 이 출력과 관련 있는가?"를 명확히 보여준다는 점입니다. 이러한 특징들이 Transformer를 번역에서 RNN보다 5-10 BLEU 점수 높게 만들었습니다.
코드 예제
import numpy as np
def cross_attention(Q_decoder, K_encoder, V_encoder):
"""
Q_decoder: Decoder의 Query (batch, target_len, d_k)
K_encoder: Encoder의 Key (batch, source_len, d_k)
V_encoder: Encoder의 Value (batch, source_len, d_v)
"""
d_k = Q_decoder.shape[-1]
# Query(decoder)와 Key(encoder)의 유사도 계산
scores = np.matmul(Q_decoder, K_encoder.transpose(0, 2, 1)) / np.sqrt(d_k)
# 결과: (batch, target_len, source_len) - 출력 단어 × 입력 단어
# Softmax로 확률 변환
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# Value(encoder)에 가중치 적용
output = np.matmul(attention_weights, V_encoder)
return output, attention_weights
# 예시: 영어(3단어) → 한국어(4단어) 번역
encoder_output = np.random.randn(1, 3, 64) # "I love you"의 인코딩
decoder_query = np.random.randn(1, 4, 64) # "나는 너를 사랑해"를 생성 중
output, weights = cross_attention(decoder_query, encoder_output, encoder_output)
print(f"Output shape: {output.shape}") # (1, 4, 64)
print(f"Attention weights shape: {weights.shape}") # (1, 4, 3)
print(f"Attention weights:\n{weights[0]}")
# 각 한국어 단어가 어느 영어 단어에 집중했는지 보여줌
설명
이것이 하는 일: 이 함수는 Decoder가 생성하는 각 단어(Query)가 Encoder의 입력 전체(Key, Value)를 참조해서 어느 부분이 중요한지 찾아냅니다. 마치 번역할 때 원문을 계속 들여다보는 것과 같습니다.
첫 번째로, Decoder의 Query와 Encoder의 Key를 곱해서 점수 행렬을 만듭니다. 이 행렬의 크기는 (target_len, source_len)인데, 예를 들어 한국어 4단어 × 영어 3단어 = (4, 3) 행렬이 됩니다.
(i, j) 원소는 "i번째 한국어 단어가 j번째 영어 단어와 얼마나 관련 있는가?"를 나타냅니다. 그 다음으로, Softmax를 각 행에 적용합니다.
이제 각 한국어 단어는 영어 3단어에 대한 확률 분포를 갖습니다. 예를 들어, "사랑해"는 [0.1, 0.8, 0.1]처럼 "love"(2번째 단어)에 80% 집중할 수 있습니다.
이게 바로 Attention의 핵심입니다. 세 번째로, 이 가중치를 Encoder의 Value에 곱합니다.
"사랑해"는 "love"의 표현을 주로 가져오고, "I"와 "you"는 조금만 가져오는 식입니다. 최종 출력은 "영어 원문의 관련 부분을 가중 평균한 것"이 됩니다.
마지막으로, 이렇게 만들어진 출력은 Decoder의 다음 레이어로 전달됩니다. Decoder는 Self-Attention(이미 생성한 한국어끼리 참조), Cross-Attention(영어 원문 참조), Feed-Forward 순서로 구성되는데, Cross-Attention이 두 언어를 연결하는 다리 역할을 합니다.
여러분이 이 코드를 사용하면 Transformer 번역 모델의 핵심을 구현한 것입니다. attention_weights를 시각화하면 "alignment"가 보이는데, 예를 들어 "나는"이 "I"와 강하게 연결되고, "사랑해"가 "love"와 강하게 연결된다는 걸 확인할 수 있습니다.
이는 통계적 기계 번역의 alignment 개념을 자동으로 학습한 것으로, 신경망이 스스로 번역 규칙을 발견했다는 증거입니다.
실전 팁
💡 Cross-Attention은 Decoder의 각 레이어마다 반복되므로, 12층이면 12번 실행됩니다. 계산량이 많으니 효율에 주의하세요.
💡 Encoder의 padding 토큰은 마스킹해야 합니다. 빈 부분에 attention을 주면 의미 없는 정보가 섞입니다.
💡 Attention 가중치를 heatmap으로 그리면 "어느 영어 단어가 어느 한국어 단어로 번역되었는지" 시각적으로 볼 수 있어 디버깅에 유용합니다.
💡 Image-to-Text 작업에서는 Encoder가 CNN이고 Key/Value가 이미지 feature인데, 원리는 동일하게 Cross-Attention으로 연결됩니다.
💡 질의응답에서 Query는 질문, Key/Value는 문서인데, attention_weights가 "답의 근거가 문서 어디에 있는지"를 보여줘서 설명 가능한 AI를 만들 수 있습니다.
8. Attention의 시간 복잡도와 최적화
시작하며
여러분이 Transformer로 긴 문서를 처리하다 보면 이런 문제를 만납니다. 512 토큰은 잘 되는데, 1024 토큰만 되어도 GPU 메모리가 부족하거나 엄청 느려집니다.
왜 이런 일이 생길까요? Attention의 시간 복잡도는 O(n²)입니다.
문장 길이가 2배가 되면 계산량은 4배, 메모리는 4배가 필요합니다. n=512면 512×512=262,144 개의 점수를 계산하지만, n=2048이면 4,194,304 개가 됩니다.
GPU 메모리가 금방 바닥나는 것이죠. 바로 이럴 때 필요한 것이 Attention 최적화 기법입니다.
Sparse Attention, Linformer, Performer 같은 방법들은 O(n log n) 또는 O(n)으로 복잡도를 줄여줍니다. 긴 문서, 고해상도 이미지, 긴 비디오 같은 대규모 데이터를 처리할 때 필수적입니다.
개요
간단히 말해서, Attention 최적화는 모든 단어 쌍을 보지 않고 중요한 부분만 선택적으로 보는 기법들입니다. 정확도는 조금 희생하지만 속도와 메모리를 크게 개선합니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 실무에서는 긴 입력을 다루는 경우가 많습니다. 예를 들어, 법률 문서(수천 토큰), 소설 전체, 1시간짜리 비디오의 프레임들을 처리하려면 표준 Attention으로는 불가능합니다.
의료 기록 분석, 코드 전체 이해, 긴 대화 맥락 유지 등에서 필수입니다. GPT-4가 32K 토큰을 처리할 수 있는 것도 이런 최적화 덕분입니다.
기존에는 n=512 정도가 한계였다면, 이제는 n=4096 이상도 처리할 수 있습니다. Longformer는 4096, BigBird는 4096, Reformer는 64K 토큰까지 가능합니다.
핵심 특징은 첫째, Local Attention(인접 단어만 보기), Sparse Attention(일부만 샘플링), Low-rank Approximation(저차원 근사) 등 다양한 접근법이 있다는 점, 둘째, 작업에 따라 적절한 방법을 선택해야 한다는 점, 셋째, 정확도와 속도의 trade-off가 있다는 점입니다. 이러한 특징들이 대규모 언어 모델의 발전을 가능하게 만들었습니다.
코드 예제
import numpy as np
def sparse_attention(Q, K, V, window_size=64):
"""
Local Window Attention: 각 단어는 주변 window_size 범위만 참조
시간 복잡도: O(n × window_size) ≈ O(n) if window_size는 상수
"""
batch_size, seq_len, d_k = Q.shape
# 전체 attention scores 계산 (설명용, 실전에서는 sparse 연산 사용)
scores = np.matmul(Q, K.transpose(0, 2, 1)) / np.sqrt(d_k)
# Local window mask 생성
mask = np.ones((seq_len, seq_len), dtype=bool)
for i in range(seq_len):
# i번째 단어는 [i-window_size, i+window_size] 범위만 볼 수 있음
start = max(0, i - window_size)
end = min(seq_len, i + window_size + 1)
mask[i, start:end] = False
# Window 밖은 -inf 처리
scores[:, mask] = -1e9
# Softmax 적용
attention_weights = np.exp(scores) / np.sum(np.exp(scores), axis=-1, keepdims=True)
# Value에 가중치 적용
output = np.matmul(attention_weights, V)
return output, attention_weights
# 예시: 512 토큰, window_size=64
Q = np.random.randn(1, 512, 64)
K, V = Q.copy(), Q.copy()
output, weights = sparse_attention(Q, K, V, window_size=64)
print(f"Output shape: {output.shape}")
print(f"Sparse pattern: {np.sum(weights[0] > 1e-8, axis=1)[:10]}")
# 각 단어가 참조하는 단어 개수 (약 128개, 전체 512개가 아님)
설명
이것이 하는 일: 이 함수는 각 단어가 모든 단어를 보는 대신, 주변 window_size 범위 내의 단어만 참조하도록 제한합니다. 계산량과 메모리가 크게 줄어듭니다.
첫 번째로, 표준 Attention처럼 Q와 K를 곱해서 점수 행렬을 만듭니다. 하지만 이 행렬의 대부분을 버릴 것입니다.
실전에서는 sparse matrix 연산을 사용해서 처음부터 필요한 부분만 계산하지만, 여기서는 이해를 위해 전체를 계산한 후 마스킹합니다. 그 다음으로, Local Window Mask를 만듭니다.
각 단어 i는 [i-64, i+64] 범위만 볼 수 있도록 설정합니다. 예를 들어, 100번째 단어는 36~164번 단어만 참조하고, 나머지 400개 단어는 무시합니다.
이렇게 하면 계산량이 512×512에서 512×128로 4배 줄어듭니다. 세 번째로, 왜 Local Window가 효과적일까요?
자연어에서 대부분의 의존성은 가까운 단어 사이에 있습니다. "고양이가 쥐를 잡았다"에서 "고양이"와 "잡았다"는 가깝지만, 100단어 떨어진 단어와는 관련이 적습니다.
실험 결과, window_size=64~128이면 정확도 손실이 1% 미만입니다. 네 번째로, 더 발전된 방법들도 있습니다.
Longformer는 Local + Global(중요 토큰은 모든 위치 참조), BigBird는 Local + Random + Global, Linformer는 Key와 Value를 저차원으로 투영(512→64)합니다. 각각 장단점이 있어서 작업에 맞게 선택해야 합니다.
마지막으로, 메모리 측면에서 보면 더 극적입니다. 표준 Attention은 (batch, heads, seq_len, seq_len) 텐서를 저장해야 하는데, seq_len=4096이면 4096²×4(float32) = 64MB per head입니다.
16 heads면 1GB가 필요하죠. Sparse Attention은 이를 10분의 1로 줄일 수 있습니다.
여러분이 이 코드를 사용하면 긴 문서를 처리할 수 있게 됩니다. 법률 문서 분석(수천 토큰), 책 전체 요약, 긴 대화 맥락 유지 같은 작업이 가능해집니다.
다만 window_size를 너무 작게 하면(예: 16) 장거리 의존성을 놓칠 수 있으니, 작업에 맞게 조정해야 합니다. 실험적으로 64~256이 적절한 경우가 많습니다.
실전 팁
💡 Hugging Face의 Longformer, BigBird 같은 라이브러리를 사용하면 최적화된 구현을 바로 쓸 수 있습니다.
💡 작업 특성에 따라 다른 패턴이 필요합니다: 번역은 Local+Global, 분류는 Global만, QA는 Local+Random이 효과적입니다.
💡 Flash Attention은 알고리즘 최적화로 표준 Attention을 2-4배 빠르게 만듭니다. 패턴 변경 없이 속도만 올리고 싶다면 이걸 쓰세요.
💡 메모리가 부족하면 gradient checkpointing과 함께 사용하면 메모리를 절반으로 줄일 수 있습니다(속도는 약간 느려짐).
💡 Attention 패턴을 시각화하면 어느 부분을 보는지 확인할 수 있는데, 대각선 주변에 집중되어 있으면 Local Attention이 적절하다는 신호입니다.
9. Attention Visualization과 해석
시작하며
여러분이 BERT로 감정 분석을 한다고 해봅시다. 모델이 "이 영화는 끔찍했다"를 부정으로 분류했는데, "왜?"라고 물으면 대답할 수 있나요?
일반 딥러닝 모델은 "블랙박스"라서 설명이 어렵습니다. 그런데 Attention은 다릅니다.
Attention 가중치를 보면 "모델이 어느 단어에 집중했는지" 숫자로 나옵니다. 0.8이라는 가중치는 "이 단어가 매우 중요했다"는 뜻이죠.
이게 Attention의 큰 장점입니다. 바로 이럴 때 유용한 것이 Attention Visualization입니다.
가중치를 히트맵, 그래프, 화살표로 그려서 모델의 판단 과정을 "눈으로 볼 수 있게" 만듭니다. 의료, 금융, 법률 같은 분야에서는 AI의 설명 가능성이 필수인데, Attention이 그 핵심입니다.
개요
간단히 말해서, Attention Visualization은 attention_weights 행렬을 시각적으로 표현해서 모델이 어디에 집중했는지 보여주는 기법입니다. 디버깅, 모델 이해, 신뢰성 확보에 사용됩니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 모델이 틀렸을 때 "왜 틀렸는지" 알아야 수정할 수 있습니다. 예를 들어, 번역 모델이 "bank"를 "은행"으로 잘못 번역했다면, attention을 보고 "아, 'river'를 못 봤네, 그래서 '강가'를 놓쳤구나"라고 파악할 수 있습니다.
의료 진단 AI에서 "이 환자를 고위험으로 판단한 이유는 병력의 이 부분 때문"이라고 설명할 수 있어야 의사가 신뢰하고 사용합니다. 기존에는 LIME, SHAP 같은 별도의 설명 도구가 필요했다면, Attention은 모델 자체가 이미 가중치 형태로 설명을 제공합니다.
추가 계산 없이 바로 사용할 수 있습니다. 핵심 특징은 첫째, 추가 비용 없이 모델 내부의 정보를 활용한다는 점, 둘째, 단어 수준의 세밀한 해석이 가능하다는 점, 셋째, Multi-head의 각 head가 다른 패턴을 학습한다는 걸 시각화로 발견할 수 있다는 점입니다.
이러한 특징들이 Attention을 단순한 성능 향상 도구가 아닌 "해석 가능한 AI"의 핵심 기술로 만들어줍니다.
코드 예제
import numpy as np
import matplotlib.pyplot as plt
def visualize_attention(attention_weights, source_tokens, target_tokens):
"""
attention_weights: (target_len, source_len) - Cross-Attention 가중치
source_tokens: 입력 단어 리스트 ["I", "love", "you"]
target_tokens: 출력 단어 리스트 ["나는", "너를", "사랑해"]
"""
fig, ax = plt.subplots(figsize=(10, 8))
# Heatmap 그리기
im = ax.imshow(attention_weights, cmap='Blues', aspect='auto')
# 축 레이블 설정
ax.set_xticks(range(len(source_tokens)))
ax.set_yticks(range(len(target_tokens)))
ax.set_xticklabels(source_tokens, rotation=45)
ax.set_yticklabels(target_tokens)
# 각 셀에 가중치 값 표시
for i in range(len(target_tokens)):
for j in range(len(source_tokens)):
text = ax.text(j, i, f"{attention_weights[i, j]:.2f}",
ha="center", va="center", color="black")
ax.set_title("Cross-Attention: 어느 영어 단어를 참조했는가?")
ax.set_xlabel("Source (English)")
ax.set_ylabel("Target (Korean)")
plt.colorbar(im, ax=ax)
plt.tight_layout()
plt.savefig("attention_visualization.png")
print("Saved to attention_visualization.png")
# 예시 데이터
attention = np.array([
[0.8, 0.1, 0.1], # "나는" -> "I"에 80% 집중
[0.1, 0.1, 0.8], # "너를" -> "you"에 80% 집중
[0.1, 0.8, 0.1] # "사랑해" -> "love"에 80% 집중
])
source = ["I", "love", "you"]
target = ["나는", "너를", "사랑해"]
visualize_attention(attention, source, target)
설명
이것이 하는 일: 이 함수는 attention_weights 행렬을 히트맵으로 그려서 "각 출력 단어가 어느 입력 단어를 참조했는지" 시각적으로 보여줍니다. 색이 진할수록 가중치가 크다는 뜻입니다.
첫 번째로, matplotlib의 imshow를 사용해서 (target_len, source_len) 행렬을 이미지로 변환합니다. Blues 컬러맵을 사용하면 0은 하얀색, 1은 진한 파란색으로 표시됩니다.
예를 들어, [0.8, 0.1, 0.1] 행은 첫 번째 셀이 진하게 나타나서 "여기에 집중했다"는 걸 한눈에 알 수 있습니다. 그 다음으로, 축 레이블에 실제 단어를 표시합니다.
x축은 입력("I", "love", "you"), y축은 출력("나는", "너를", "사랑해")입니다. 이렇게 하면 "아, '사랑해'가 'love'를 주로 봤구나"처럼 직관적으로 이해할 수 있습니다.
세 번째로, 각 셀에 숫자를 표시해서 정확한 가중치 값을 볼 수 있게 합니다. 0.8이라고 적혀 있으면 "80% 집중"이라는 뜻입니다.
이 예시에서는 대각선 패턴("나는"→"I", "사랑해"→"love", "너를"→"you")이 명확하게 보이는데, 이게 올바른 번역 정렬(alignment)입니다. 네 번째로, 실전에서는 Multi-head Attention의 각 head를 따로 그려봐야 합니다.
8개 head가 있다면 8개의 히트맵을 그리는 것이죠. 그러면 "head 1은 문법 관계에, head 2는 의미 관계에 집중한다" 같은 흥미로운 패턴을 발견할 수 있습니다.
BERT 연구에서 실제로 특정 head가 "주어-동사" 관계를 학습한다는 게 밝혀졌습니다. 마지막으로, Self-Attention을 시각화할 때는 (seq_len, seq_len) 정사각 행렬이 나옵니다.
대각선이 진하면 "자기 자신에 집중", 비대각선이 진하면 "다른 단어를 참조"라는 뜻입니다. GPT의 Masked Attention을 그려보면 하삼각 행렬 패턴이 명확히 보입니다.
여러분이 이 코드를 사용하면 모델을 "블랙박스"에서 "화이트박스"로 만들 수 있습니다. 실제 프로젝트에서 "왜 이 문장을 부정으로 판단했나요?"라는 질문에 "이 단어들에 집중했기 때문입니다"라고 히트맵과 함께 답할 수 있습니다.
또한 모델이 학습한 패턴을 발견할 수 있어서, 언어학 연구나 모델 개선에도 유용합니다.
실전 팁
💡 BertViz, exBERT 같은 라이브러리를 사용하면 인터랙티브한 시각화(마우스 오버로 가중치 표시)를 쉽게 만들 수 있습니다.
💡 모든 head를 동시에 보려면 subplot으로 grid를 만들어서 한 화면에 8x12=96개 head를 그릴 수 있습니다(BERT-base 기준).
💡 Attention 가중치가 너무 분산되어 있으면(모든 값이 0.1~0.2) 모델이 혼란스러워한다는 신호입니다. Dropout이나 학습률 조정을 고려하세요.
💡 특정 head가 항상 대각선만 보면(자기 자신에만 집중) 해당 head가 제대로 학습 안 된 것일 수 있습니다. Head pruning으로 제거해도 성능이 안 떨어질 수 있습니다.
💡 금융이나 의료 분야에서는 attention 기반 설명을 PDF 보고서로 만들어서 "AI가 왜 이 결정을 내렸는지" 문서화하는 것이 규제 준수에 도움이 됩니다.
10. Attention의 실무 활용 사례
시작하며
여러분이 지금까지 배운 Attention 이론을 실제 프로젝트에 어떻게 적용할까요? "개념은 알겠는데 실무에서 언제 쓰지?"라는 질문이 생길 수 있습니다.
Attention은 이미 여러분이 매일 사용하는 서비스에 녹아있습니다. Google 번역, ChatGPT, Gmail의 스마트 답장, YouTube 자동 자막, 사진 앱의 얼굴 인식 등 수많은 곳에서 작동하고 있습니다.
단순히 연구 논문의 개념이 아니라 실제 제품의 핵심 기술이죠. 바로 이 섹션에서는 Attention의 대표적인 실무 활용 사례를 살펴봅니다.
어떤 문제를 해결하는지, 어떻게 구현하는지, 성능은 어떤지 구체적으로 알아보겠습니다.
개요
간단히 말해서, Attention은 번역, 요약, 질의응답, 이미지 캡셔닝, 음성 인식 등 거의 모든 sequence 작업에 사용됩니다. 각 분야마다 조금씩 다른 방식으로 적용됩니다.
왜 이 개념이 필요한지 실무 관점에서 설명하면, 이론만 알고 적용 못 하면 의미가 없습니다. 예를 들어, 고객 리뷰를 분석하는 AI를 만든다면 BERT + Self-Attention을 사용하고, 챗봇을 만든다면 GPT + Masked Attention을 사용하고, 번역기를 만든다면 Transformer + Cross-Attention을 사용합니다.
문제에 맞는 Attention 패턴을 선택하는 것이 핵심입니다. 기존에는 각 작업마다 별도의 모델 구조가 필요했다면, 이제는 Attention 기반 모델(BERT, GPT, T5)을 fine-tuning해서 대부분의 작업을 해결합니다.
이게 Transfer Learning의 핵심입니다. 핵심 특징은 첫째, Pre-trained 모델을 사용하면 데이터가 적어도 좋은 성능을 낸다는 점, 둘째, 한 모델로 여러 작업을 처리할 수 있다는 점(Multi-task Learning), 셋째, Hugging Face 같은 라이브러리로 몇 줄만에 구현 가능하다는 점입니다.
이러한 특징들이 Attention을 AI 개발의 표준으로 만들었습니다.
코드 예제
from transformers import pipeline
# 1. 감정 분석 (Self-Attention 기반 BERT)
sentiment_analyzer = pipeline("sentiment-analysis")
result = sentiment_analyzer("이 제품은 정말 훌륭합니다!")
print(f"감정 분석: {result}") # POSITIVE, score: 0.99
# 2. 질의응답 (Cross-Attention으로 문서와 질문 연결)
qa_pipeline = pipeline("question-answering")
context = "Transformer는 2017년 Google이 발표한 모델입니다."
question = "Transformer는 언제 발표되었나요?"
answer = qa_pipeline(question=question, context=context)
print(f"답변: {answer['answer']}") # "2017년"
# 3. 텍스트 생성 (Masked Attention 기반 GPT)
generator = pipeline("text-generation", model="gpt2")
prompt = "인공지능의 미래는"
generated = generator(prompt, max_length=50, num_return_sequences=1)
print(f"생성된 텍스트: {generated[0]['generated_text']}")
# 4. 번역 (Encoder-Decoder with Cross-Attention)
translator = pipeline("translation_en_to_ko")
translation = translator("I love artificial intelligence")
print(f"번역: {translation[0]['translation_text']}")
# 실무에서는 이렇게 간단하게 사용 가능!
설명
이것이 하는 일: 이 코드는 Hugging Face의 pipeline을 사용해서 네 가지 대표적인 Attention 활용 사례를 보여줍니다. 각각 내부적으로 다른 Attention 패턴을 사용합니다.
첫 번째로, 감정 분석은 Self-Attention 기반 BERT를 사용합니다. 입력 문장의 모든 단어가 서로를 참조해서 "훌륭합니다"가 긍정적인 단어이고 "정말"이 그 강도를 높인다는 걸 파악합니다.
실무에서는 고객 리뷰 분석, SNS 모니터링, 여론 조사 등에 사용됩니다. F1 score 90% 이상을 쉽게 달성할 수 있습니다.
그 다음으로, 질의응답은 Cross-Attention을 사용합니다. 질문 "언제"와 문서의 "2017년"을 연결해서 답을 찾아냅니다.
attention_weights를 보면 "언제"가 "2017년"에 높은 가중치를 준다는 걸 확인할 수 있습니다. 실무에서는 고객 지원 챗봇, 문서 검색, FAQ 자동화 등에 사용됩니다.
SQuAD 데이터셋에서 사람보다 높은 정확도를 냅니다. 세 번째로, 텍스트 생성은 Masked Attention 기반 GPT를 사용합니다.
"인공지능의 미래는"이라는 프롬프트를 받아서 한 단어씩 생성하는데, 각 단계에서 이미 생성된 부분만 참조합니다. 실무에서는 콘텐츠 생성, 코드 자동완성, 이메일 답장 추천 등에 사용됩니다.
GPT-4는 변호사 시험, 의사 시험을 통과할 정도의 성능을 냅니다. 네 번째로, 번역은 Encoder-Decoder 구조를 사용합니다.
Encoder는 영어를 Self-Attention으로 이해하고, Decoder는 Cross-Attention으로 영어를 참조하면서 한국어를 생성합니다. 실무에서는 Google 번역, Papago 같은 서비스에 사용됩니다.
Transformer는 RNN 기반 번역보다 BLEU 점수가 5-10점 높습니다. 마지막으로, 이 외에도 이미지 캡셔닝(Vision Transformer + Cross-Attention), 음성 인식(Whisper), 추천 시스템(Self-Attention for user history), 시계열 예측(Temporal Attention) 등 수많은 분야에서 Attention이 사용됩니다.
핵심은 "무엇과 무엇을 연결할 것인가?"를 정의하는 것입니다. 여러분이 이 코드를 사용하면 복잡한 Attention 이론을 직접 구현하지 않고도 강력한 AI를 만들 수 있습니다.
실무에서는 대부분 Pre-trained 모델을 fine-tuning해서 사용하므로, 이론을 이해하고 적절한 모델을 선택하는 것이 더 중요합니다. 예를 들어, 분류 작업은 BERT, 생성 작업은 GPT, 양쪽 다 필요하면 T5를 사용하는 식입니다.
실전 팁
💡 Hugging Face Model Hub에는 10만 개 이상의 Pre-trained 모델이 있으니, 직접 학습하기 전에 비슷한 작업의 모델을 찾아보세요.
💡 Fine-tuning할 때는 학습률을 작게(1e-5 ~ 5e-5) 설정해야 Pre-trained 가중치가 망가지지 않습니다.
💡 데이터가 1000개 미만이면 Few-shot Learning이나 Prompt Engineering을 고려하세요. GPT-3는 예시 몇 개만으로도 작동합니다.
💡 실시간 서비스에서는 모델 크기가 중요합니다. DistilBERT(BERT의 60% 크기)나 TinyBERT(15% 크기)를 사용하면 속도가 10배 빨라집니다.
💡 Attention 가중치를 로깅해서 모니터링하면 모델이 이상하게 작동할 때(예: 특정 단어만 계속 보기) 빠르게 발견할 수 있습니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
범주형 변수 시각화 완벽 가이드 Bar Chart와 Count Plot
데이터 분석에서 가장 기본이 되는 범주형 변수 시각화 방법을 알아봅니다. Matplotlib의 Bar Chart부터 Seaborn의 Count Plot까지, 실무에서 바로 활용할 수 있는 시각화 기법을 배워봅니다.
이변량 분석 완벽 가이드: 변수 간 관계 탐색
두 변수 사이의 관계를 분석하는 이변량 분석의 핵심 개념과 기법을 배웁니다. 상관관계, 산점도, 교차분석 등 데이터 분석의 필수 도구들을 실습과 함께 익혀봅시다.
단변량 분석 분포 시각화 완벽 가이드
데이터 분석의 첫걸음인 단변량 분석과 분포 시각화를 배웁니다. 히스토그램, 박스플롯, 밀도 그래프 등 다양한 시각화 방법을 초보자도 쉽게 이해할 수 있도록 설명합니다.
데이터 타입 변환 및 정규화 완벽 가이드
데이터 분석과 머신러닝에서 가장 기초가 되는 데이터 타입 변환과 정규화 기법을 배워봅니다. 실무에서 자주 마주치는 데이터 전처리 문제를 Python으로 쉽게 해결하는 방법을 알려드립니다.
이상치 탐지 및 처리 완벽 가이드
데이터 속에 숨어있는 이상한 값들을 찾아내고 처리하는 방법을 배워봅니다. 실무에서 자주 마주치는 이상치 문제를 Python으로 해결하는 다양한 기법을 소개합니다.