[AI] 어텐션 메커니즘(Attention Mechanism) 쉽게 이해하기

728x90

1. 어텐션이란?

어텐션(Attention)은 "지금 처리하는 부분과 관련이 깊은 정보에 더 집중하자"는 아이디어다. 사람이 글을 읽을 때 모든 단어를 똑같이 주목하지 않고, 맥락에 따라 특정 단어에 더 눈길이 가는 것과 같은 원리다.

질문: "서울의 인구는 몇 명이야?"

사람의 사고:
  "서울의"  → 아, 서울에 대한 질문이구나         ★★★ 주목
  "인구는"  → 인구를 묻고 있네                   ★★★ 주목
  "몇"     → 숫자를 원하는군                     ★★ 보통
  "명이야"  → 사람 수 단위                       ★ 참고

모델도 이렇게 토큰마다 "얼마나 중요한지" 점수를 매긴다.
이것이 어텐션이다.

2. 어텐션이 없던 시절의 문제

어텐션 등장 이전의 모델(RNN, LSTM 기반 Seq2Seq)은 입력 문장 전체를 하나의 고정 크기 벡터로 압축한 뒤 출력을 생성했다. 이 방식에는 치명적인 한계가 있었다.

번역 예시: "오늘 아침에 일어나서 커피를 마시고 회사에 갔다"

고정 벡터 방식 (어텐션 없음):
  [오늘, 아침에, 일어나서, 커피를, 마시고, 회사에, 갔다]
         │
         ▼
  ┌──────────────┐
  │ 고정 벡터 1개  │  ← 문장 전체를 여기에 욱여넣음
  │ [0.3, -0.1,  │     긴 문장일수록 앞부분 정보가 유실됨
  │  0.7, ...]   │
  └──────┬───────┘
         ▼
  "I went to the office after drinking coffee this morning"

  → 문장이 길어지면 앞쪽 정보("오늘 아침에")가 희미해짐
  → 이를 "정보 병목(bottleneck)" 문제라고 부름

3. 어텐션의 핵심 아이디어

어텐션은 이 문제를 "출력의 매 단계마다, 입력의 어느 부분을 참고할지 직접 선택하게 하자"로 해결했다.

어텐션 방식:

  "갔다"를 번역할 때:
    "오늘"     → 관련도 0.05  (거의 안 봄)
    "아침에"   → 관련도 0.03
    "일어나서" → 관련도 0.02
    "커피를"   → 관련도 0.05
    "마시고"   → 관련도 0.05
    "회사에"   → 관련도 0.45  ★ 집중!
    "갔다"     → 관련도 0.35  ★ 집중!

  → "회사에"와 "갔다"에 가중치를 크게 줘서 "went to the office" 생성
  → 고정 벡터 하나에 의존하지 않으므로 긴 문장도 정확하게 처리

4. Q, K, V — 어텐션의 세 가지 재료

Transformer에서 어텐션은 Query, Key, Value 세 가지 벡터로 작동한다. 도서관 비유로 이해하면 쉽다.

도서관에서 정보를 찾는 과정:

1) Query (질문):  "서울 인구에 대해 알고 싶어"
   → 내가 지금 알고 싶은 것

2) Key (책 제목): "한국 지리", "세계 인구 통계", "서울 역사", "요리 레시피"
   → 각 정보원이 "나는 이런 내용이야"라고 내건 라벨

3) Value (책 내용): 각 책의 실제 내용
   → Key에 대응하는 실제 정보

과정:
  Query와 각 Key를 비교 → 관련도 점수 산출
  → "한국 지리" 0.3, "세계 인구 통계" 0.4, "서울 역사" 0.25, "요리 레시피" 0.05
  → 점수에 따라 Value를 가중 합산
  → 인구 통계 내용을 많이, 요리 내용은 거의 안 섞어서 최종 답변 구성

실제 Transformer에서는 입력 토큰의 임베딩 벡터에 학습된 가중치 행렬을 곱해서 Q, K, V를 만든다.

입력 토큰 임베딩: X (각 토큰이 벡터로 표현된 행렬)

Q = X × W_Q   (질문 행렬)
K = X × W_K   (키 행렬)
V = X × W_V   (값 행렬)

W_Q, W_K, W_V는 학습을 통해 최적화되는 파라미터다.

5. 어텐션 계산 과정: 단계별로 따라가기

Scaled Dot-Product Attention의 수식은 다음과 같다.

Attention(Q, K, V) = softmax(QK^T / √d_k) × V

수식이 복잡해 보이지만, 단계별로 뜯어보면 직관적이다.

예시 상황

"고양이가 밥을 먹었다"라는 문장에서 "먹었다"가 다른 토큰들을 얼마나 참고하는지 계산해 보자.

Step 1: QK^T — 관련도 점수 계산

"먹었다"의 Query와 모든 토큰의 Key를 내적(dot product)한다. 내적값이 클수록 두 벡터의 방향이 비슷하다는 뜻이고, 이는 "관련이 깊다"는 의미다.

"먹었다"의 Q  ·  "고양이가"의 K  =  32.5  (← 누가 먹었는지 관련 높음)
"먹었다"의 Q  ·  "밥을"의 K      =  45.2  (← 뭘 먹었는지 관련 매우 높음)
"먹었다"의 Q  ·  "먹었다"의 K    =  28.1

Step 2: √d_k로 나누기 — 스케일링

내적값은 벡터 차원(d_k)이 클수록 값 자체가 커진다. 값이 너무 크면 softmax 결과가 극단적(0 또는 1에 가까움)이 되어 학습이 불안정해진다. 그래서 √d_k로 나눠 적절한 범위로 조정한다.

d_k = 64 (차원)  →  √64 = 8

32.5 / 8 = 4.06
45.2 / 8 = 5.65
28.1 / 8 = 3.51

여기서 이 스케일링된 값이 아주 커지면 지수(exponent) 범위 문제가 발생한다. 앞서 정밀도 문서에서 설명한 FP16 오버플로가 바로 이 지점에서 일어날 수 있다.

Step 3: Softmax — 확률로 변환

스케일링된 점수를 softmax에 넣어 합이 1인 확률 분포로 바꾼다.

softmax([4.06, 5.65, 3.51])

  e^4.06 = 58.0
  e^5.65 = 284.3
  e^3.51 = 33.4
  합계 = 375.7

  "고양이가" → 58.0 / 375.7 = 0.154  (15.4%)
  "밥을"     → 284.3 / 375.7 = 0.757  (75.7%)  ★ 가장 주목
  "먹었다"   → 33.4 / 375.7 = 0.089  (8.9%)

"먹었다"는 "밥을"에 75.7%의 주의를 기울인다. 직관적으로도 "무엇을 먹었는지"가 가장 관련이 깊으니 자연스럽다.

Step 4: Value 가중 합산 — 최종 결과

확률(가중치)을 각 토큰의 Value에 곱해서 합산한다.

최종 출력 = 0.154 × V_고양이가 + 0.757 × V_밥을 + 0.089 × V_먹었다

→ "밥을"의 정보가 가장 많이 반영된 새로운 벡터가 만들어짐
→ 이 벡터가 "먹었다"의 문맥을 반영한 표현(contextual representation)

6. Multi-Head Attention — 여러 관점에서 동시에 보기

어텐션 한 번으로는 하나의 관점밖에 포착하지 못한다. 예를 들어 "먹었다"에 대해 "뭘 먹었는지(목적어)"도 중요하고, "누가 먹었는지(주어)"도 중요하다. Multi-Head Attention은 이 문제를 해결한다.

"고양이가 생선 밥을 맛있게 먹었다"에서 "먹었다"가 주목하는 대상:

Head 1 (문법적 관계에 집중):
  "고양이가" ★★★  → 주어가 누구인지
  "밥을"     ★★   → 목적어가 뭔지

Head 2 (의미적 관계에 집중):
  "맛있게"   ★★★  → 어떻게 먹었는지
  "생선"     ★★   → 어떤 종류의 밥인지

Head 3 (순서/위치 관계에 집중):
  "밥을"     ★★★  → 바로 앞 단어
  "맛있게"   ★★   → 가까운 수식어

→ 각 Head의 결과를 이어붙이고(concat) 변환(linear)해서
  최종적으로 다양한 관점이 통합된 표현을 만든다

구조적으로는 Q, K, V를 Head 수만큼 분할해서 각각 독립적으로 어텐션을 수행한 뒤 합치는 방식이다.

예: 임베딩 차원 512, Head 수 8

전체 Q (512차원) → 8개로 분할 → 각 Head가 64차원씩 독립적으로 어텐션
                                  │
                          Head 1 결과 (64차원)
                          Head 2 결과 (64차원)
                          ...
                          Head 8 결과 (64차원)
                                  │
                                  ▼
                          Concat → 512차원 → Linear 변환 → 최종 출력

7. Self-Attention vs Cross-Attention

어텐션은 Q, K, V가 어디서 오느냐에 따라 두 종류로 나뉜다.

Self-Attention (자기 자신을 참고)

Q, K, V가 모두 같은 시퀀스에서 만들어진다. 문장 안의 토큰들이 서로를 참고한다.

입력: "나는 어제 산 책을 읽었다"

"읽었다"의 Self-Attention:
  → Q, K, V 모두 이 문장의 토큰들에서 생성
  → "책을"에 강하게 주목 (뭘 읽었는지)
  → "나는"에도 주목 (누가 읽었는지)

용도: GPT 같은 디코더, BERT 같은 인코더의 핵심 연산

Cross-Attention (다른 시퀀스를 참고)

Q는 현재 시퀀스, K와 V는 다른 시퀀스에서 가져온다.

번역: "나는 학생이다" → "I am a student"

"student"를 생성할 때:
  Q ← 디코더 (현재까지 생성한 "I am a")
  K, V ← 인코더 (원문 "나는 학생이다")

  → "학생이다"에 강하게 주목해서 "student" 생성

용도: 번역, 요약, 이미지 캡셔닝 등 "입력 → 출력" 변환 태스크

8. Causal Mask — GPT가 미래를 보지 못하게 하는 장치

GPT 계열(Qwen, LLaMA 등 포함)은 텍스트를 왼쪽에서 오른쪽으로 생성한다. 생성 시점에 아직 나오지 않은 미래 토큰을 참고하면 안 된다. 이를 위해 Causal Mask(인과 마스크)를 사용한다.

"고양이가 밥을 먹었다"

마스크 없는 어텐션 (양방향):
              고양이가  밥을  먹었다
  고양이가  [  ✓       ✓      ✓   ]   ← 미래도 볼 수 있음
  밥을      [  ✓       ✓      ✓   ]
  먹었다    [  ✓       ✓      ✓   ]

Causal Mask 적용 (단방향):
              고양이가  밥을  먹었다
  고양이가  [  ✓       ✗      ✗   ]   ← 자기 자신과 과거만 참고
  밥을      [  ✓       ✓      ✗   ]
  먹었다    [  ✓       ✓      ✓   ]

✗ 위치는 어텐션 점수를 -∞로 설정 → softmax 후 0이 됨
→ 해당 토큰의 정보가 완전히 차단됨

이렇게 해야 모델이 "다음 토큰 예측"이라는 과제를 정직하게 학습할 수 있다.


9. KV Cache — 추론 속도를 높이는 핵심 기법

GPT 계열 모델이 토큰을 하나씩 생성할 때, 매번 이전 토큰들의 K와 V를 처음부터 다시 계산하면 낭비가 심하다. 이전에 계산한 K, V를 캐시에 저장해두고 재사용하는 것이 KV Cache다.

"오늘 날씨가 좋다"를 생성하는 과정:

KV Cache 없이:
  "오늘" 생성        → Q,K,V 계산: [오늘]
  "날씨가" 생성      → Q,K,V 계산: [오늘, 날씨가]          ← 오늘을 다시 계산
  "좋다" 생성        → Q,K,V 계산: [오늘, 날씨가, 좋다]    ← 오늘, 날씨가를 또 계산
  → 중복 연산이 토큰 수의 제곱으로 늘어남

KV Cache 사용:
  "오늘" 생성        → K,V 계산 후 캐시에 저장: {오늘}
  "날씨가" 생성      → 새 토큰의 K,V만 계산, 캐시에 추가: {오늘, 날씨가}
  "좋다" 생성        → 새 토큰의 K,V만 계산, 캐시에 추가: {오늘, 날씨가, 좋다}
  → 매번 새 토큰 1개의 Q만 계산하면 됨. 나머지는 캐시에서 가져옴.

KV Cache는 추론 속도를 극적으로 높이지만, 시퀀스가 길어질수록 캐시 메모리가 커진다. 이것이 앞서 정밀도 문서에서 언급한 "KV Cache 양자화(FP8)"가 필요한 이유다.

KV Cache 메모리 계산 예시 (Qwen3-8B 기준):

레이어 수: 32
Head 수: 32 (각 64차원)
시퀀스 길이: 4096 토큰

FP16: 32 × 32 × 64 × 4096 × 2(K,V) × 2(bytes) ≈ 1 GB
FP8:  32 × 32 × 64 × 4096 × 2(K,V) × 1(byte)  ≈ 0.5 GB

→ FP8로 KV Cache를 양자화하면 같은 메모리로 2배 긴 시퀀스를 처리하거나,
  동시에 2배 많은 요청을 받을 수 있다.

10. 어텐션의 한계와 발전

10.1 연산 비용: O(n²) 문제

Self-Attention은 모든 토큰 쌍의 관련도를 계산하므로, 시퀀스 길이 n에 대해 O(n²)의 시간과 메모리가 필요하다.

시퀀스 길이별 어텐션 연산량:

  512 토큰   →  262,144 쌍    (기본)
  2,048 토큰 →  4,194,304 쌍  (16배)
  8,192 토큰 →  67,108,864 쌍 (256배)
  128K 토큰  →  약 164억 쌍    

→ 길이가 2배 되면 연산량은 4배로 증가

이 때문에 긴 문맥을 처리하기 위한 다양한 효율적 어텐션 기법이 연구되고 있다.

10.2 주요 효율적 어텐션 기법들

기법 핵심 아이디어 복잡도
Multi-Query Attention (MQA) K, V를 모든 Head가 공유 → KV Cache 크기 대폭 축소 O(n²) 유지, 메모리 절감
Grouped-Query Attention (GQA) K, V를 Head 그룹 단위로 공유 (MQA와 MHA의 중간) O(n²) 유지, 메모리 절감
Flash Attention 어텐션 연산을 GPU 메모리 계층에 최적화 (IO-aware) O(n²) 유지, 실제 속도 2~4배
Sparse Attention 모든 쌍이 아닌 일부만 계산 (패턴 기반) O(n√n) ~ O(n log n)
Linear Attention softmax를 커널 근사로 대체 O(n)

현재 LLM에서 가장 실용적으로 쓰이는 조합은 GQA + Flash Attention이다. Qwen3, LLaMA 3 등 최신 모델들이 이 구조를 채택하고 있다.


요약

어텐션의 전체 흐름:

입력 토큰 → 임베딩 → Q, K, V 생성
                      │
          ┌───────────┼───────────┐
          ▼           ▼           ▼
        Head 1      Head 2   ... Head N    (Multi-Head)
          │           │           │
     QK^T/√d → softmax → ×V (Causal Mask 적용)
          │           │           │
          └───────────┼───────────┘
                      ▼
                Concat + Linear
                      │
                      ▼
              문맥이 반영된 토큰 표현
                      │
              (× 32개 레이어 반복)
                      │
                      ▼
              다음 토큰 확률 분포 → 토큰 생성

어텐션은 결국 "지금 필요한 정보를 입력 전체에서 골라 가져오는" 메커니즘이며, Transformer와 LLM의 핵심 동작 원리다.


참고 자료

728x90