본문 바로가기
TYLER SONGBlog
← Blog index
LLM Research6 min read

주간 리뷰 (Week 5): BPE와 Attention을 백지에서 다시 짜 보면 내가 모르는 곳이 드러난다

이번 주에 배운 토크나이저(BPE)와 Scaled Dot-Product Attention을 참고 자료 없이 처음부터 다시 구현하는 복습 날이다. 새 개념은 없고, 직접 짜면서 "이해한 줄 알았던 부분"을 찾아내는 것이 목표다. 마지막에는 attention 히트맵을 그려 결과가 맞는지 눈으로 확인하고, 틀린 알고리즘 문제도 다시 푼다.

By 송민성

오늘의 목표

오늘은 새로 배우는 것이 없다. 이번 주 내용인 NLP 기초(텍스트를 숫자로 바꾸는 과정)와 Attention(문장 속 단어들이 서로를 얼마나 참고할지 계산하는 방법)을 스스로 설명하고 다시 구현할 수 있는지 확인한다. 노트나 이전 코드를 보지 않고 BPE와 Scaled Dot-Product Attention을 백지에서 써 내려가는 것이 핵심이다. 막히는 지점이 곧 내일부터 보강할 목록이 된다.

개념과 직관

오늘은 새 학습이 없으므로, 이번 주 개념을 "빈 종이에 설명할 수 있는가"로 점검한다. 아래 요약을 보지 않고 먼저 자기 말로 써 본 다음 비교하자.

1) 토큰화와 BPE

  • 컴퓨터는 글자를 직접 다루지 못하고 숫자만 다룬다. 그래서 텍스트를 작은 조각인 토큰(token)으로 쪼개고, 각 토큰에 번호를 붙인다.
  • 단어 단위로 쪼개면 처음 보는 단어를 처리할 수 없고, 글자 단위로 쪼개면 문장이 너무 길어진다.
  • BPE(Byte Pair Encoding, 자주 붙어 나오는 문자 쌍을 반복해서 하나의 토큰으로 합치는 방법)는 그 중간을 택한다.
  • 비유: 레고 블록 놓기와 비슷하다. 처음엔 낱개 블록(글자)뿐인데, 자주 붙어 다니는 두 블록을 아예 접착해 새 블록으로 만든다. 이 과정을 정해 둔 횟수만큼 되풀이한다.

BPE 학습 한 단계를 텍스트로 쓰면 이렇다.

text
1. 말뭉치의 모든 단어를 글자 단위로 쪼갠다.
2. 인접한 쌍의 등장 횟수를 센다.
3. 가장 많이 나온 쌍을 하나의 새 토큰으로 합친다.
4. 2~3을 원하는 횟수만큼 반복한다.

2) Attention

  • 문장의 각 토큰이 "나는 지금 어떤 토큰들을 얼마나 참고해야 하지?"를 계산하는 장치다.
  • 비유: 도서관에서 질문(Query)을 들고 책 등에 붙은 제목표(Key)와 비교해 관련 있는 책을 고른 뒤, 그 책 내용(Value)을 관련도만큼 섞어서 읽는 것이다.
  • 최소 수식은 다음 한 줄이다.
text
Attention(Q, K, V) = softmax(Q * K의 전치 / sqrt(d_k)) * V
  • Q * K의 전치: 모든 토큰 쌍의 유사도 점수표가 나온다. 점수가 클수록 서로 관련이 깊다.
  • sqrt(d_k)로 나누는 이유: 벡터 차원 d_k가 커지면 내적 값이 커져서 softmax가 한쪽으로 극단적으로 쏠린다. 나누면 점수 크기가 안정된다.
  • softmax(점수들을 0~1 사이 확률처럼 바꿔 합이 1이 되게 하는 함수): 각 행이 "누구를 얼마나 볼지"의 비율이 된다.
  • 마지막으로 이 비율로 V를 가중합해 새 표현을 얻는다.

스스로 점검할 질문 4개

  1. BPE에서 합칠 쌍을 고르는 기준은 무엇인가?
  2. Q, K, V의 shape은 각각 어떻게 되고, 점수표의 shape은 무엇인가?
  3. softmax는 어느 축(행/열)에 적용하는가?
  4. sqrt(d_k)를 빼면 어떤 일이 생기는가?

코드로 직접 해보기

규칙: 이전 코드와 노트를 열지 않는다. 막히면 5분은 스스로 고민한 뒤에만 확인한다. 확인한 부분은 따로 표시해 둔다.

Step 1. BPE를 직접 구현한다 (라이브러리 없이)

python
from collections import Counter def get_pair_counts(words): # words: {("l","o","w"): 5, ...} 형태 (글자 튜플 -> 빈도) pairs = Counter() for word, freq in words.items(): for a, b in zip(word, word[1:]): pairs[(a, b)] += freq return pairs def merge_pair(words, pair): merged = {} for word, freq in words.items(): new_word = [] i = 0 while i < len(word): if i < len(word) - 1 and (word[i], word[i + 1]) == pair: new_word.append(word[i] + word[i + 1]) i += 2 else: new_word.append(word[i]) i += 1 merged[tuple(new_word)] = freq return merged corpus = ["low", "low", "lower", "newest", "newest", "widest"] words = Counter(tuple(w) for w in corpus) words = dict(words) for step in range(5): pairs = get_pair_counts(words) best = max(pairs, key=pairs.get) words = merge_pair(words, best) print(step, best, words)

실행 후 확인할 것은 두 가지다. 매 단계에서 어떤 쌍이 선택되는지 손으로 계산한 결과와 같은지, 그리고 단계가 진행될수록 토큰 조각이 길어지는지 본다.

Step 2. Scaled Dot-Product Attention을 구현한다

먼저 numpy로 원리를 보인다.

python
import numpy as np def softmax(x, axis=-1): x = x - x.max(axis=axis, keepdims=True) # 수치 안정성을 위해 최댓값을 뺀다 e = np.exp(x) return e / e.sum(axis=axis, keepdims=True) def attention(Q, K, V): d_k = Q.shape[-1] scores = Q @ K.T / np.sqrt(d_k) # (seq, seq) weights = softmax(scores, axis=-1) # 각 행의 합 = 1 return weights @ V, weights # (seq, d_v), (seq, seq) np.random.seed(0) seq, d = 4, 8 Q = np.random.randn(seq, d) K = np.random.randn(seq, d) V = np.random.randn(seq, d) out, w = attention(Q, K, V) print(out.shape, w.shape) print(w.sum(axis=-1)) # [1. 1. 1. 1.] 이 나와야 한다

Step 3. 셀프 체크

  • out.shape이 (4, 8), w.shape이 (4, 4)인가?
  • w.sum(axis=-1)이 모두 1인가? (1이 아니면 softmax 축이 틀린 것이다.)
  • np.sqrt(d_k) 나누기를 지우고 w를 출력하면 값이 더 한쪽으로 쏠리는가?

오늘의 실험

목표: attention 히트맵을 직접 재현하고, 숫자를 그림으로 읽는 연습을 한다.

python
import matplotlib.pyplot as plt tokens = ["나는", "오늘", "학교에", "갔다"] fig, ax = plt.subplots() im = ax.imshow(w, cmap="viridis") ax.set_xticks(range(len(tokens))) ax.set_yticks(range(len(tokens))) ax.set_xticklabels(tokens) ax.set_yticklabels(tokens) ax.set_xlabel("Key (참고 대상)") ax.set_ylabel("Query (질문하는 토큰)") fig.colorbar(im) plt.savefig("attention_heatmap.png")

한글이 깨지면 폰트 설정 문제이므로 일단 영어 토큰(["I", "went", "to", "school"])으로 바꿔 진행한다.

바꿔 볼 것 (한 번에 하나씩)

  1. sqrt(d_k)로 나누기를 켠 경우와 끈 경우
  2. d 값을 8, 64, 512로 키운 경우 (나누기를 끄면 쏠림이 얼마나 심해지는가)
  3. 랜덤 시드를 바꾼 경우

관찰할 것: 각 행의 색이 한 칸에 몰리는지, 고르게 퍼지는지를 본다. 행 합이 1이라는 사실이 그림에서 어떻게 보이는지도 생각해 본다.

기록 방법 (표 형태로 노트에 남긴다)

설정행별 최댓값 평균관찰 메모
d=8, 스케일 O
d=8, 스케일 X
d=512, 스케일 O
d=512, 스케일 X

행별 최댓값 평균은 w.max(axis=-1).mean()으로 구한다. 값이 1에 가까울수록 한 토큰에 쏠렸다는 뜻이다. 수치는 직접 실행해서 채우고, 예상과 달랐던 점을 한 줄 적는다.

왜 이걸 배우나 (LLM / Post-training 연결)

BPE는 LLM이 텍스트를 읽고 쓰는 입출구이고, Attention은 Transformer 모델의 핵심 연산이다. 이후 모델 구조, 파인튜닝, 평가 데이터를 다룰 때 토큰 수·시퀀스 길이·attention 비용이 계속 등장한다. LLM Post-training 업무에서도 학습 데이터의 토큰 길이 분포를 살피고, 모델이 왜 그렇게 출력했는지 분석할 때 오늘 만든 감각이 기반이 된다.

초보자가 막히는 지점

  • softmax 축 실수: axis=-1이 아니라 0을 쓰면 열 기준으로 합이 1이 된다. 항상 w.sum(axis=-1)로 검증한다.
  • K 전치 누락: Q @ K로 쓰면 shape 에러가 난다. (seq, d) @ (d, seq)가 되도록 K.T를 쓴다.
  • BPE 동점 처리: 빈도가 같은 쌍이 여럿이면 어느 것을 고르느냐에 따라 결과가 달라진다. 틀린 게 아니라 규칙의 차이이므로, 선택 규칙을 정해 두고 일관되게 쓴다.
  • BPE 병합 루프 버그: 합친 뒤 i += 2를 빼먹으면 같은 위치를 다시 보거나 무한 루프에 빠진다.
  • "보고 베끼면 이해한 것"이라는 착각: 막혀서 코드를 봤다면, 닫고 다시 처음부터 써 본다. 두 번째에 막힘 없이 쓰면 그때 이해한 것이다.
  • 히트맵을 해석하려는 과욕: 랜덤 Q, K, V로 그린 히트맵에는 의미 있는 패턴이 없다. 오늘의 목적은 계산이 맞는지 확인하는 것이다.

알고리즘 (병행 1시간)

이번 주에 틀린 문제를 다시 푼다. 풀이 전에 예전 풀이를 보지 않고 처음부터 풀어 본다.

풀이 후 기록할 것

  • 접근: 어떤 아이디어로 풀었는지 2~3줄로 쓴다. (예: 해시맵으로 빈도 세기, 투 포인터 등)
  • 시간복잡도: 입력 크기 n에 대해 O(?)인지, 공간복잡도는 어떤지 적는다.
  • 틀린 이유: 개념을 몰랐는지, 엣지 케이스(빈 입력, 중복 값 등)를 놓쳤는지, 구현 실수였는지 분류한다.
  • 재풀이 결과: 이번에는 혼자 풀었는지, 힌트를 봤는지 표시한다. 힌트를 봤다면 다음 주에 한 번 더 푸는 목록에 넣는다.

시간이 남으면 틀린 이유 분류를 한 주 치 모아 가장 많은 유형 하나를 골라 다음 주 보강 계획에 적는다.