🤖 AI/ML

크로스 어텐션

Cross-Attention

두 다른 시퀀스 간의 관계를 학습하는 어텐션. 인코더-디코더 연결에 사용.

📖 상세 설명

크로스 어텐션(Cross-Attention)은 두 개의 서로 다른 시퀀스 간의 관계를 학습하는 어텐션 메커니즘입니다. 셀프 어텐션(Self-Attention)이 하나의 시퀀스 내에서 토큰 간 관계를 계산하는 반면, 크로스 어텐션은 쿼리(Query)가 한 시퀀스에서, 키(Key)와 값(Value)이 다른 시퀀스에서 오는 구조입니다. 이를 통해 인코더-디코더 아키텍처에서 두 모달리티 간의 정보 교환이 가능해집니다.

크로스 어텐션은 2017년 "Attention Is All You Need" 논문에서 Transformer 아키텍처와 함께 소개되었습니다. 원래 기계 번역을 위해 설계되어, 디코더가 인코더의 출력을 참조할 수 있게 했습니다. 이후 이 개념은 이미지 캡셔닝, 텍스트-이미지 생성(Stable Diffusion), 비전-언어 모델(CLIP, GPT-4V), 음성 인식(Whisper) 등 멀티모달 AI의 핵심 구성요소가 되었습니다.

수학적으로 크로스 어텐션은 Attention(Q, K, V) = softmax(QK^T / sqrt(d_k))V로 표현됩니다. 여기서 Q는 디코더(타겟) 시퀀스에서 생성되고, K와 V는 인코더(소스) 시퀀스에서 생성됩니다. 예를 들어 Stable Diffusion에서 텍스트 프롬프트의 임베딩이 K와 V가 되고, 이미지의 잠재 표현이 Q가 되어 텍스트 조건에 맞는 이미지를 생성합니다. 이 과정에서 텍스트의 어떤 부분이 이미지의 어떤 영역에 영향을 주는지 학습됩니다.

2024-2025년 실무에서 크로스 어텐션은 RAG(Retrieval-Augmented Generation), 멀티모달 LLM, 비디오 생성 모델 등에서 핵심 역할을 합니다. 특히 Llama 3.2 Vision, Claude 3의 이미지 이해, Sora의 비디오 생성 등에서 텍스트와 시각 정보를 연결하는 데 사용됩니다. Flash Attention, Memory Efficient Attention 등의 최적화 기법과 결합하여 긴 시퀀스에서도 효율적으로 동작합니다.

💻 코드 예제

PyTorch로 구현한 크로스 어텐션 모듈입니다.

# 크로스 어텐션 구현
# pip install torch

import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class CrossAttention(nn.Module):
    """
    크로스 어텐션 모듈
    - query_dim: 쿼리 시퀀스의 차원 (디코더 측)
    - context_dim: 컨텍스트 시퀀스의 차원 (인코더 측)
    - heads: 멀티헤드 어텐션의 헤드 수
    """
    def __init__(self, query_dim, context_dim, heads=8, dim_head=64, dropout=0.0):
        super().__init__()
        self.heads = heads
        self.dim_head = dim_head
        inner_dim = heads * dim_head
        self.scale = dim_head ** -0.5

        # Q는 query(디코더)에서, K/V는 context(인코더)에서 생성
        self.to_q = nn.Linear(query_dim, inner_dim, bias=False)
        self.to_k = nn.Linear(context_dim, inner_dim, bias=False)
        self.to_v = nn.Linear(context_dim, inner_dim, bias=False)

        self.to_out = nn.Sequential(
            nn.Linear(inner_dim, query_dim),
            nn.Dropout(dropout)
        )

    def forward(self, x, context):
        """
        x: 쿼리 시퀀스 [batch, seq_len_q, query_dim]
        context: 컨텍스트 시퀀스 [batch, seq_len_ctx, context_dim]
        """
        batch_size = x.shape[0]

        # Q, K, V 계산
        q = self.to_q(x)  # [batch, seq_q, inner_dim]
        k = self.to_k(context)  # [batch, seq_ctx, inner_dim]
        v = self.to_v(context)  # [batch, seq_ctx, inner_dim]

        # 멀티헤드로 reshape
        q = q.view(batch_size, -1, self.heads, self.dim_head).transpose(1, 2)
        k = k.view(batch_size, -1, self.heads, self.dim_head).transpose(1, 2)
        v = v.view(batch_size, -1, self.heads, self.dim_head).transpose(1, 2)

        # 어텐션 스코어 계산
        attn = torch.matmul(q, k.transpose(-2, -1)) * self.scale
        attn = F.softmax(attn, dim=-1)

        # 어텐션 적용
        out = torch.matmul(attn, v)
        out = out.transpose(1, 2).reshape(batch_size, -1, self.heads * self.dim_head)

        return self.to_out(out)

# 사용 예시
batch_size = 2
decoder_seq_len = 10  # 디코더 시퀀스 길이
encoder_seq_len = 20  # 인코더 시퀀스 길이
query_dim = 512
context_dim = 768

# 크로스 어텐션 레이어 생성
cross_attn = CrossAttention(query_dim=query_dim, context_dim=context_dim)

# 입력 생성
decoder_hidden = torch.randn(batch_size, decoder_seq_len, query_dim)
encoder_output = torch.randn(batch_size, encoder_seq_len, context_dim)

# 크로스 어텐션 수행
output = cross_attn(decoder_hidden, encoder_output)
print(f"입력 (디코더): {decoder_hidden.shape}")
print(f"컨텍스트 (인코더): {encoder_output.shape}")
print(f"출력: {output.shape}")  # [2, 10, 512]

Hugging Face Transformers에서 크로스 어텐션 활용 예제입니다.

# 이미지 캡셔닝에서의 크로스 어텐션 활용
# pip install transformers pillow torch

from transformers import BlipProcessor, BlipForConditionalGeneration
from PIL import Image
import requests

# BLIP 모델 로드 (이미지-텍스트 크로스 어텐션 사용)
processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base")

# 샘플 이미지 로드
url = "http://images.cocodataset.org/val2017/000000039769.jpg"
image = Image.open(requests.get(url, stream=True).raw)

# 이미지 캡션 생성 (내부적으로 크로스 어텐션 사용)
inputs = processor(image, return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=50)

# 결과 디코딩
caption = processor.decode(outputs[0], skip_special_tokens=True)
print(f"생성된 캡션: {caption}")

# 크로스 어텐션 가중치 확인 (선택적)
with torch.no_grad():
    outputs = model(**inputs, output_attentions=True)
    # decoder의 cross_attentions 확인 가능
    if hasattr(outputs, 'cross_attentions') and outputs.cross_attentions:
        print(f"크로스 어텐션 레이어 수: {len(outputs.cross_attentions)}")

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"RAG 시스템에서 검색된 문서와 사용자 질문을 연결하는 데 크로스 어텐션을 활용하고 있습니다. 질문의 어떤 부분이 문서의 어떤 구절에 주목하는지 어텐션 맵으로 시각화하면 디버깅에도 도움이 됩니다."
💬 면접에서
"Stable Diffusion에서 텍스트 프롬프트가 이미지 생성에 영향을 주는 방식이 크로스 어텐션입니다. CLIP 텍스트 인코더의 출력이 K, V가 되고, UNet의 잠재 표현이 Q가 되어 프롬프트의 의미가 이미지 공간에 주입됩니다."
💬 기술 토론에서
"멀티모달 LLM에서 이미지 토큰과 텍스트 토큰을 연결할 때 크로스 어텐션이 핵심이에요. LLaVA는 이미지 패치를 언어 모델 공간에 프로젝션하고, 크로스 어텐션으로 시각 정보를 텍스트 생성에 조건화합니다."

⚠️ 흔한 실수 & 주의사항

Q, K, V 출처 혼동

크로스 어텐션에서 Query는 타겟(디코더) 시퀀스, Key/Value는 소스(인코더) 시퀀스에서 옵니다. 셀프 어텐션과 혼동하여 모두 같은 시퀀스에서 생성하면 크로스 어텐션이 아닙니다.

컨텍스트 길이 불일치

인코더와 디코더의 시퀀스 길이는 다를 수 있지만, 배치 크기와 임베딩 차원은 맞아야 합니다. 특히 멀티모달에서 이미지 패치 수와 텍스트 토큰 수가 다른 것은 정상입니다.

올바른 접근법

크로스 어텐션 구현 시 Q, K, V의 차원과 출처를 명확히 문서화하세요. 디버깅할 때는 어텐션 가중치를 시각화하여 모델이 올바른 관계를 학습하는지 확인하세요.

🔗 관련 용어

📚 더 배우기