BERT 개념이해 #4 Self-Attention

BERT 개념이해 #4 Self-Attention #

#2026-03-04


#1 Self-Attention이 풀려는 문제: 단어는 혼자서는 의미가 부족하다

사람은 문장을 읽을 때 단어를 따로따로 보지 않는다. “it” 같은 단어는 그 자체로는 거의 빈 껍데기다. “그것”이 무엇인지 알려면 앞뒤 문맥을 같이 봐야 한다. “The animal didn’t cross the street because it was too tired"에서 “it”이 가리키는 대상을 사람은 자연스럽게 “animal”로 잡는다. 하지만 컴퓨터는 문장을 단어 목록처럼만 보면 “it”이 무엇을 가리키는지 알 근거가 없다. Self-Attention은 바로 여기서 출발한다. 문장 속 각 단어가 문장의 다른 모든 단어를 한 번씩 바라보면서, “내가 의미를 만들 때 누구의 정보를 얼마나 가져올까?”를 계산하는 장치다. 이 계산 결과로 각 단어는 문맥이 섞인 새 벡터 표현을 얻게 되고, 그 새 표현 안에 “it은 animal 쪽에 더 기대고 있다” 같은 정보가 자연스럽게 포함된다.

#

#2 Attention의 직관: 검색창(Q)으로 색인(K)을 비교해 책 내용(V)을 가져온다

도서관에서 어떤 책을 찾을 때 우리는 “내가 원하는 것”을 머릿속에 갖고 있다. 이게 Query다. 그리고 책마다 제목, 태그, 키워드 같은 “요약 정보”가 있다. 이게 Key다. 마지막으로 책의 본문이 실제 정보다. 이게 Value다. 검색은 이렇게 된다. Query와 각 책의 Key를 비교해서 관련도를 점수로 만든 다음, 그 점수들을 확률처럼 바꿔서(Value를) 얼마나 가져올지 결정한다. 관련도가 높은 책의 Value를 더 많이 가져오고, 관련도가 낮은 책의 Value는 조금만 가져오거나 거의 무시한다. 결국 “찾고 싶은 것”이라는 기준으로 문서들을 가중합해서, 내가 원하는 정보를 조합해 얻는다.

Query  (Q): 내가 찾고 싶은 것 → "동물에 대한 책"
Key    (K): 각 책의 색인(제목, 태그) → "animal", "street", "tired"
Value  (V): 책의 실제 내용

검색 과정:
  Q와 각 K의 유사도 계산
    Q·K₁("animal") = 0.9  ← 매우 관련
    Q·K₂("street") = 0.1  ← 별로 관련 없음
    Q·K₃("tired")  = 0.3  ← 조금 관련
  → Softmax로 확률로 변환: [0.73, 0.05, 0.22]
  → 각 확률만큼 Value를 가져와 합산:
     0.73 × V("animal") + 0.05 × V("street") + 0.22 × V("tired")

Self-Attention은 이 구조를 문장 내부로 가져온 것이다. 외부에서 Query가 오는 게 아니라, 문장 속 각 토큰이 자기 자신을 Query로 만들어서 “나는 지금 누구를 참고해야 의미가 좋아질까?”를 스스로 묻는다. 그래서 “it” 토큰은 자기 Query로 문장 안의 Key들을 훑어보고, “animal” 쪽을 더 크게 참고하는 가중치를 만들 수 있다. 결과적으로 “it”의 새 표현은 “animal의 정보가 섞인 it”이 된다. 이게 컴퓨터가 공참조 같은 문제를 풀기 시작하는 방식이다.

일반 Attention: Q는 외부에서 옴 (예: 번역 모델의 디코더)
Self-Attention: Q, K, V 모두 같은 문장에서!

"it" 토큰이 Self-Attention을 수행:
  Q("it") · K("animal") → 높은 점수
  Q("it") · K("street") → 낮은 점수
  Q("it") · K("tired")  → 중간 점수

결과: "it"의 새 표현 = 주로 "animal" 정보 + 일부 "tired" 정보
     → "it"이 "animal"을 가리킨다는 것을 표현에 반영

#

#3 왜 Q/K/V로 굳이 나누나: 같은 벡터로 비교하면 늘 자기 자신이 이긴다

임베딩을 거친 각 토큰은 768차원 벡터 x로 표현된다. 여기서 초보자가 흔히 떠올리는 생각은 “그럼 이 x들끼리 그냥 내적해서 가까운 애를 찾으면 되지 않나?”다. 그런데 그렇게 하면 문제가 생긴다. 같은 벡터끼리의 내적은 항상 가장 크다. 즉 각 토큰은 “자기 자신”에게 attention이 과도하게 쏠리기 쉽다. 그러면 문맥을 섞는 장치가 아니라 “난 나만 볼래”가 되어버린다. 그리고 문맥을 섞어야 할 이유가 사라진다.

그래서 트랜스포머는 입력 벡터 x를 그대로 쓰지 않고, 서로 다른 관점의 선형 변환을 세 개 만든다. 하나는 Query를 만드는 변환 W_Q, 하나는 Key를 만드는 변환 W_K, 하나는 Value를 만드는 변환 W_V다. 같은 입력 x라도 W_Q로 바꾸면 “내가 무엇을 찾고 싶은가”라는 성격의 벡터가 되고, W_K로 바꾸면 “내가 어떤 특징을 가진 토큰인가”라는 색인 벡터가 되고, W_V로 바꾸면 “내가 전달할 내용은 무엇인가”라는 내용 벡터가 된다. 이 세 공간이 서로 다르기 때문에, 단순히 자기 자신이 항상 최고점이 되는 현상이 완화되고, “it은 animal을 더 찾는다” 같은 관계 학습이 가능해진다. 즉 Q/K/V 분리는 Attention을 단순한 유사도 매칭이 아니라 “관계 기반 정보 추출”로 바꾸는 핵심 설계다.

# 왜 변환이 필요한가
입력 x: (batch, seq_len, 768)
  "wall" = [0.78, -0.12, 0.56, ...]  (768차원)

이 벡터를 그대로 Q, K, V로 쓰면:
  Q = x, K = x, V = x → Q·Kᵀ = x·xᵀ
  → 같은 벡터끼리의 내적 = 항상 자기 자신이 최고 점수

해결: 다른 W_Q, W_K, W_V 행렬로 변환
  Q = x W_Q  (768 → 768)
  K = x W_K  (768 → 768)
  V = x W_V  (768 → 768)

  W_Q, W_K, W_V가 각각 다르므로
  → "찾는 것(Q)"과 "찾히는 것(K)"이 다른 공간에서 비교됨

#

#4 Scaled Dot-Product의 핵심: 유사도 행렬을 만들고 Softmax로 비율을 만든다

이제 계산을 한 번 머릿속으로 그려보자. 문장 길이가 L이면 토큰은 L개다. 각 토큰은 Query 벡터 하나를 가진다. 그리고 문장에 있는 모든 토큰은 Key 벡터도 하나씩 가진다. 어떤 토큰 i가 “다른 토큰 j를 얼마나 참고할까?”를 계산하려면 Q_i와 K_j의 내적을 보면 된다. 이걸 모든 i, j 조합에 대해 한 번에 계산하면 (L×L) 크기의 점수 행렬이 생긴다. 이 행렬의 i번째 줄은 “i 토큰이 문장 전체를 바라본 점수표”다. 이후 Softmax를 i번째 줄에 적용하면 그 줄이 확률 분포처럼 바뀐다. 즉 i 토큰은 문장 전체의 토큰들을 가중치로 가지게 된다. 그 가중치로 Value들을 가중합하면, i 토큰의 새로운 표현이 만들어진다. 이 새 표현은 “문장 전체에서 내가 필요한 정보를 적당히 섞어 만든 요약”이다.

여기서 스케일링 / √d_head는 수치 안정성을 위한 장치다. 내적은 차원이 커질수록 값이 커지는 경향이 있다. d_head가 64면 내적 값이 상대적으로 커지는데, 그 큰 값이 Softmax로 들어가면 Softmax가 너무 날카로워져서 한두 개 위치만 1에 가깝고 나머지는 0에 가까운 극단 분포가 되기 쉽다. 그러면 학습이 불안정해지고, 작은 변화에 결과가 크게 튄다. 그래서 내적을 √64=8로 나눠 점수의 규모를 적당히 눌러준다. 이것은 “Softmax가 숨 쉬는 범위”를 유지하기 위한 스케일 조절이라고 생각하면 된다.

실제 배치에서는 길이가 다른 문장들을 맞추기 위해 PAD를 붙인다. 그런데 PAD는 의미가 없는 빈칸이다. 만약 Self-Attention이 PAD까지 진짜 단어처럼 참고해버리면, 문맥은 의미 없는 값이 섞이면서 오염된다. 그래서 attention_mask가 필요하다. mask가 0인 위치, 즉 PAD 위치는 점수(scores)에 아주 큰 음수(-1e9 같은 값)를 더해준다. 그러면 Softmax를 통과한 뒤 그 위치의 확률이 사실상 0이 된다. 결과적으로 Value 가중합에서 PAD는 기여하지 못한다. 즉 “빈칸을 절대 보지 마”라는 강제 규칙을 수치적으로 구현한 것이다.

Self-Attention은 어떤 토큰이 어떤 토큰을 보는지에 대한 확률 분포를 만든다. 그런데 이 분포가 훈련 데이터에 과하게 맞춰지면 “이 상황이면 무조건 저 단어만 봐라” 같은 편향이 생길 수 있다. 그래서 Softmax로 만든 attention_probs에 Dropout을 걸어준다. 학습 중에는 일부 attention 연결이 랜덤하게 꺼지면서, 모델이 특정 연결 하나에만 의존하지 못한다. 그 결과 여러 단서들을 조합해 문맥을 이해하는 방향으로 학습된다. 말 그대로 “참조 관계 자체를 정규화”하는 효과가 있다.

입력: x ∈ (batch, L, 768)    L = 시퀀스 길이

[1] Q/K/V 프로젝션 (선형 변환)
  Q = x W_Q   (batch, L, 768) × (768, 768) → (batch, L, 768)
  K = x W_K   (batch, L, 768) × (768, 768) → (batch, L, 768)
  V = x W_V   (batch, L, 768) × (768, 768) → (batch, L, 768)

[2] 헤드별 분리
  Q → (batch, 12, L, 64)  [12개 헤드, 각 64차원]
  K → (batch, 12, L, 64)
  V → (batch, 12, L, 64)

[3] Scaled Dot-Product Attention (헤드별 독립 계산)
  scores = Q · Kᵀ / √64   → (batch, 12, L, L)
  ─────────────────────────────
  Q · Kᵀ: 각 위치 쌍의 유사도 행렬
    "wall"이 "bears"를 얼마나 참조할지
    크기: (L, L) — L²가지 쌍 모두 계산

  √64: 스케일링
    d_head이 클수록 내적 값이 커짐 → Softmax가 포화됨
    √d_head로 나눠 적절한 크기 유지

  mask: attention_mask 적용
    PAD 위치: score에 -∞ 더함 → Softmax 후 ≈ 0
    [PAD] 토큰이 attention에 기여하지 않도록

  weights = Softmax(scores)  → (batch, 12, L, L)
  weights = Dropout(weights, p=0.1)  ← Attention Dropout

  context = weights · V      → (batch, 12, L, 64)
    각 위치의 새 표현 = attention 가중치로 V를 가중합

[4] 헤드 합치기
  context → (batch, L, 768)  [12 × 64 = 768]

[5] Output 프로젝션
  output = context · W_O     → (batch, L, 768)
  → 12개 헤드의 정보를 다시 통합

#

#5 Multi-Head가 필요한 이유: 문장에는 ‘관계’가 한 종류가 아니다

문장을 이해하는 데 필요한 관계는 하나가 아니다. 어떤 때는 주어와 동사를 연결해야 하고, 어떤 때는 형용사가 꾸미는 명사를 찾아야 하고, 어떤 때는 “it”이 무엇을 가리키는지 공참조를 찾아야 하고, 어떤 때는 멀리 떨어진 단어끼리의 원거리 의존성을 잡아야 한다. 만약 Attention을 한 번만 하면, 그 한 번의 공간에서 모든 관계를 다 담아야 한다. 그건 한 사람이 동시에 12개의 역할을 다 하는 것과 비슷해서 표현이 섞이고 충돌하기 쉽다.

그래서 트랜스포머는 768차원을 12개로 쪼개서, 각 헤드가 64차원 공간에서 독립적으로 Attention을 하게 만든다. 중요한 건 “비용이 12배로 늘지 않는다”는 점이다. 12개 헤드가 각각 64차원씩 처리하고, 결과를 다시 붙이면 12×64가 되어 다시 768이 된다. 즉 전체 차원 수는 그대로 유지하면서, 서로 다른 관점의 Attention을 병렬로 수행하는 구조다. 그리고 마지막에 W_O로 한 번 더 섞어주면, 12개 관점에서 얻은 정보를 다시 통합한 최종 출력이 된다.

heads = 12, d_head = 64

각 헤드는 독립적인 W_Q, W_K, W_V를 가짐:
  Head 1: "주어-동사 관계에 집중"
  Head 2: "형용사-명사 수식 관계에 집중"
  Head 3: "지시어(it, they)가 가리키는 것에 집중"
  ...
  Head 12: "원거리 의존 관계에 집중"

왜 d_head=64인가:
  768 / 12 = 64
  → 전체 768차원을 12개 헤드가 64차원씩 나눠 담당
  → 총 계산량 = 12 × 64 = 768 (하나의 768차원과 동일한 비용)

왜 O(L²)인가: 모든 단어 쌍을 한 번에 비교하기 때문이다. Self-Attention이 강력한 이유는 “모든 단어가 모든 단어를 볼 수 있다”는 전역성이다. RNN은 순차적으로 읽어서 멀리 떨어진 단어의 정보가 약해지기 쉽고, CNN은 지역 창으로 보니 먼 관계를 직접 잡기 어렵다. Self-Attention은 L개의 토큰이 있으면 (L×L)개의 쌍을 한 번에 비교해서, 멀리 떨어진 관계도 단 한 번의 레이어에서 직접 연결할 수 있다. 대신 대가가 L² 연산이다. 그래서 max_length를 512에서 192로 줄이면 연산이 큰 폭으로 줄어드는 이유가 정확히 여기에 있다. “비용은 L의 제곱”이기 때문이다.

파라미터 관점에서 보면: 768→768 네 번이 Attention의 뼈대다. Self-Attention의 핵심 가중치는 W_Q, W_K, W_V, W_O 네 개다. 각각 768×768 크기의 선형 변환이므로 한 레이어에 대략 4×589,824 정도의 파라미터가 들어간다. 이게 레이어 하나당 약 2.36M 파라미터라는 계산으로 이어진다. 그리고 BERT는 이런 Attention 블록을 12번 쌓기 때문에 Attention 파라미터만 해도 상당한 비중을 차지한다. 재미있는 점은, Self-Attention이 “관계를 계산하는 방법”은 수학적으로는 단순한 내적과 Softmax와 가중합인데, 그 안에서 관계를 풍부하게 만들게 해주는 힘의 원천은 결국 이 선형 변환들이 학습되는 데서 나온다는 것이다. 즉 W_Q/W_K/W_V가 “무엇을 찾을지, 무엇이 색인인지, 무엇이 내용인지”를 학습하며 Attention의 의미를 만들어낸다.

W_Q: 768 × 768 = 589,824
W_K: 768 × 768 = 589,824
W_V: 768 × 768 = 589,824
W_O: 768 × 768 = 589,824
────────────────────────
합계: 589,824 × 4 = 2,359,296 ≈ 2.36M params (bias 제외)
     bias 포함: 2,362,368 ≈ 2.36M

× 12 레이어 = 28.3M params (전체 BERT Attention 파라미터)

#

#cf Self attention의 핵심 특성

특성 1: 모든 위치 쌍을 동시에 비교 (O(L²))
  → 긴 거리 의존성도 1번에 파악
  → CNN(지역적)이나 RNN(순차적)과 달리 전역적

특성 2: 위치 무관 (Position-Invariant)
  → 위치 정보는 Position Embedding에서 따로 추가
  → "it"이 앞에 있든 뒤에 있든 attention은 동일하게 계산
    (단, Q, K에 위치 벡터가 더해져 있으므로 위치 차이는 반영됨)

특성 3: 헤드마다 다른 관계 학습
  → Head 1: 구문론적 관계
  → Head 7: 공참조(coreference) 관계
  → 각 헤드가 자동으로 전문화됨

#

#정리

Self-Attention을 한 문장으로 요약하면 이렇다. 각 토큰은 Query로 “내가 지금 의미를 만들려면 무엇을 참고해야 하지?”를 묻고, 문장 안 모든 토큰의 Key와 비교해 관련도 점수를 만들고, Softmax로 “참조 비율”을 만든 뒤, 그 비율로 Value들을 가중합해서 새로운 표현을 만든다. 마스크는 PAD가 끼어들지 못하게 하고, 스케일링은 Softmax가 포화되지 않게 하고, Dropout은 특정 연결에 과의존하지 못하게 한다. Multi-Head는 관계의 종류가 여러 개라는 현실을 반영해, 서로 다른 관점을 동시에 학습하도록 만든다. 그 결과 “it” 같은 빈 단어도 문장 전체를 훑은 뒤 필요한 정보를 섞어, 문맥적으로 풍부한 표현으로 바뀐다.

  • Self-Attention = 문장 내 모든 단어 쌍의 관련성을 계산하여 문맥 반영
  • Q(Query)/K(Key)/V(Value): 다른 선형 변환으로 “찾는 것”, “찾히는 것”, “내용” 분리
  • Scaled Dot-Produc*: Q·Kᵀ / √d_head → 크기 안정화
  • 12-head: 768차원을 64차원×12로 나눠 12가지 다른 관계를 동시에 학습
  • Attention 비용: O(L²) → max_length=192가 512보다 7배 빠른 이유
  • 이 모델 파라미터: 각 BERT Layer당 2.36M, 12 레이어 합계 28.3M