LLM 은 글자를 읽지 않습니다. 정수 배열을 읽습니다. "안녕하세요"를 넣으면 모델 앞단에서 [14307, 171731] 같은 숫자 열로 바뀌고, 모델은 이 숫자들 다음에 올 숫자를 예측합니다. 이 변환을 맡는 것이 토크나이저입니다. GPT 계열을 비롯한 대부분의 LLM 토크나이저는 BPE(Byte Pair Encoding)라는 알고리즘을 바탕으로 합니다.
토크나이저는 학습이 끝난 뒤 바뀌지 않는 부품이라 관심을 덜 받습니다. 그런데 API 요금이 토큰 수로 매겨지고, 컨텍스트 창도 토큰 수로 잽니다. 모델이 "strawberry 에 r 이 몇 개냐"는 질문에 약한 이유, 스트리밍 응답에서 한글이 � 로 깨지는 버그도 토크나이저 쪽에서 생깁니다. 이 글에서는 BPE 가 어휘를 어떻게 배우고, 새 문장을 어떤 순서로 쪼개는지 바이트 단위로 따라갑니다.
■ 왜 BPE 인가 — 단어 단위와 글자 단위 사이
신경망에 텍스트를 넣으려면 먼저 텍스트를 유한한 기호 집합(어휘)의 원소로 나눠야 합니다. 나누는 단위를 고르는 선택지는 크게 세 가지입니다.
단어 단위는 직관적입니다. 그런데 어휘가 끝없이 늘어납니다. "토큰", "토큰화", "토큰을", "토큰이"가 모두 다른 단어입니다. 어휘에 없는 단어는 [UNK](모름) 하나로 뭉개집니다. 신조어·오타·고유명사·코드가 다 여기에 걸립니다.
글자(또는 바이트) 단위는 어휘가 작고 모르는 단어가 없습니다. 대신 시퀀스가 길어집니다. 트랜스포머의 어텐션 비용은 시퀀스 길이에 따라 커지고, 컨텍스트 창도 그만큼 빨리 찹니다. GPT-2 논문은 바이트 단위 언어 모델이 대규모 데이터셋에서 단어 단위 모델과 경쟁하지 못한다고 적었고, 자기들 실험에서도 같은 격차를 봤다고 밝힙니다.
서브워드 단위가 그 사이에 있습니다. 자주 나오는 문자열은 통째로 한 토큰으로 두고, 드문 문자열은 더 작은 조각으로 나눕니다. 2015년 Sennrich 등은 원래 데이터 압축 기법이던 BPE 를 기계 번역의 서브워드 분할에 썼습니다(arXiv:1508.07909). GPT-2 논문은 BPE 를 "자주 나오는 기호 열에는 단어 수준 입력을, 드문 기호 열에는 글자 수준 입력을 쓰는 실용적인 중간 지점"이라고 설명합니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📷📷📷 [이미지 01] ← 이미지 폴더의 01-shot-1.png 파일을 여기에 넣으세요
캡션: BPE 를 신경망 기계 번역의 서브워드 분할에 처음 쓴 논문 (출처: arXiv)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GPT-2 는 여기서 한 걸음 더 나갔습니다. 유니코드 문자 단위로 BPE 를 돌리면 모든 문자열을 표현하기 위해 기본 어휘에만 13만 개가 넘는 기호가 필요합니다. 그래서 GPT-2 는 텍스트를 UTF-8 바이트로 바꾼 뒤 BPE 를 돌립니다. 기본 어휘가 256개로 끝나고, 어떤 문자열이든 [UNK] 없이 표현됩니다. 이것이 오늘날 LLM 이 널리 쓰는 바이트 수준 BPE 입니다.
■ 전체 흐름 — 문자열에서 임베딩까지
토크나이저 안에서 데이터는 다음 순서로 흐릅니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📷📷📷 [이미지 02] ← 이미지 폴더의 02-tokenizer-pipeline.png 파일을 여기에 넣으세요
캡션: 텍스트가 토큰 ID 로 바뀌어 모델에 들어가는 순서와 디코딩 경로
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 원문: 유니코드 문자열이 들어옵니다.
2. 사전 토큰화(pre-tokenization): 정규식으로 문자열을 조각냅니다. 병합은 이 조각 안에서만 일어납니다.
3. UTF-8 바이트 변환: 각 조각을 바이트 열로 바꿉니다. 영어 알파벳은 1바이트, 한글 한 글자는 3바이트입니다. '토'는 ED 86 A0 입니다.
4. BPE 병합: 학습 때 만든 병합 순위표를 따라 이웃한 바이트 쌍을 반복해서 합칩니다.
5. 토큰 ID: 합쳐진 바이트 조각마다 어휘표의 번호를 붙입니다.
6. 임베딩 조회: 모델은 (어휘 수 × 은닉 차원) 크기의 임베딩 행렬을 갖고 있습니다. 토큰 ID 는 이 행렬의 행 번호입니다. ID 가 266이면 266번째 행 벡터를 꺼냅니다.
7. 트랜스포머: 벡터 열을 처리해 다음 토큰 ID 별 확률을 냅니다.
디코딩은 반대 방향인데 훨씬 단순합니다. ID 마다 어휘표에서 바이트 조각을 찾아 이어 붙이고 UTF-8 로 해석하면 끝입니다. 병합 규칙이 필요 없습니다. tiktoken README 가 BPE 의 장점으로 "되돌릴 수 있고 손실이 없다"를 먼저 꼽는 이유입니다.
▶ 2단계를 자세히: 왜 정규식으로 먼저 자르나
바이트 열 전체에 BPE 를 바로 돌리면 문제가 생깁니다. GPT-2 논문에 예가 있습니다. BPE 는 빈도만 보고 탐욕적으로 합치기 때문에 dog., dog!, dog? 처럼 단어와 문장부호가 붙은 변형을 각각 토큰으로 만들어 버립니다. 한정된 어휘 자리를 낭비하는 셈입니다. 그래서 GPT-2 는 문자 범주(글자·숫자·기호)가 다르면 병합하지 않도록 막았습니다. 단, 공백은 예외로 두어 뒤 단어에 붙입니다. 논문은 이 예외가 압축 효율을 크게 높인다고 적었습니다.
이 규칙은 실제로 정규식 하나로 구현됩니다. tiktoken 소스의 GPT-2 패턴은 이렇습니다.
[코드: text]
'(?:[sdmt]|ll|ve|re)| ?\p{L}++| ?\p{N}++| ?[^\s\p{L}\p{N}]++|\s++$|\s+(?!\S)|\s
[/코드]
말로 풀면 다음과 같습니다. 영어 축약형('s, 'll 등)을 따로 떼고, 앞에 공백 하나가 붙을 수 있는 글자 덩어리(\p{L}), 숫자 덩어리(\p{N}), 기호 덩어리를 각각 한 조각으로 잡습니다. 그래서 I'm lowest 토큰 은 I · 'm · lowest · 토큰 네 조각이 됩니다. 공백이 단어 앞에 붙는다는 점을 기억해 두세요. 뒤에서 다시 나옵니다.
모델 세대마다 이 정규식도 달라졌습니다. cl100k_base 는 숫자를 \p{N}{1,3}, 곧 최대 세 자리씩 끊습니다. o200k_base 는 대문자·소문자 범주를 나눠 낙타 표기(CamelCase) 단어를 쪼개는 규칙을 더했습니다. 정규식이 다르면 같은 BPE 라도 결과가 달라집니다.
■ BPE 학습 — 가장 잦은 쌍을 하나씩 합친다
학습은 말뭉치에서 병합 규칙 목록을 뽑아내는 과정입니다. 신경망 학습이 아니라 빈도를 세는 단순한 반복문입니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📷📷📷 [이미지 03] ← 이미지 폴더의 03-bpe-training-loop.png 파일을 여기에 넣으세요
캡션: BPE 학습 루프 — 쌍 세기, 1등 쌍 병합, 전체 치환을 어휘 수가 찰 때까지 반복
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 말뭉치를 사전 토큰화해 조각별 빈도로 바꿉니다. 각 조각은 바이트 ID 튜플입니다. lower 가 50번 나오면 (32, 108, 111, 119, 101, 114) × 50 처럼 저장합니다.
2. 모든 조각에서 이웃한 두 기호의 쌍을 세고, 조각 빈도만큼 가중치를 줍니다.
3. 가장 많이 나온 쌍에 새 ID 를 줍니다. 첫 병합은 256번, 다음은 257번입니다. 이 번호가 곧 순위입니다.
4. 말뭉치 전체에서 그 쌍을 새 ID 하나로 바꿉니다. 조각 경계는 넘지 않습니다.
5. 어휘 수가 목표에 닿을 때까지 2~4를 되풀이합니다.
매 단계에서 고르는 쌍을 식으로 쓰면 이렇습니다.
$$
(a^, b^) = \arg\max_{(a,b)} \sum_{w} \text{freq}(w) \cdot \text{count}_w(a, b)
$$
말로 풀면 "모든 조각 w 에 대해, 그 조각 안에서 a 바로 뒤에 b 가 오는 횟수에 그 조각의 등장 빈도를 곱해 더하고, 그 합이 가장 큰 쌍을 고른다"입니다. 그게 전부입니다. 의미나 문법은 전혀 보지 않습니다. 순수하게 "같이 자주 붙어 다니는 것끼리 묶는다"입니다.
학습이 끝나면 결과물은 두 개입니다. 어휘표(ID → 바이트 열)와 병합 순위표(쌍 → 새 ID)입니다. 어휘 수는 256 + 병합 횟수 + 특수 토큰 수 입니다. GPT-2 는 어휘가 50,257개라고 논문에 적혀 있습니다. tiktoken 의 GPT-2 인코딩에서는 이 가운데 마지막 번호 50256이 특수 토큰 <|endoftext|> 입니다. 256 + 50,000번 병합 + 특수 토큰 1개라는 계산이 맞아떨어집니다.
■ BPE 인코딩 — 먼저 배운 규칙부터 적용한다
새 문장을 쪼갤 때는 학습 때 배운 병합을 배운 순서대로 다시 적용합니다. 이 점이 중요합니다. 가장 긴 토큰을 찾는 방식이 아닙니다.
구체적인 절차는 GPT-2 원본 encoder.py 와 tiktoken 의 Rust 코어가 같습니다.
1. 조각을 바이트 단위 목록으로 펼칩니다.
2. 지금 이웃한 모든 쌍의 순위를 순위표에서 찾습니다. 없는 쌍은 무한대입니다.
3. 순위가 가장 낮은(가장 먼저 배운) 쌍 하나를 합칩니다.
4. 합칠 쌍이 없을 때까지 2~3을 반복합니다.
아래 그림은 이 글의 예제 코드로 학습한 작은 토크나이저에서 lowest 가 어떻게 쪼개지는지 보여 줍니다.
[이미지: 그림/bpe-encode-by-rank.svg] " lowest" 에 병합 규칙을 순위 순으로 적용해 [266, 258] 이 되는 과정
바이트 7개에서 시작합니다. 순위 256 (w, e) 가 먼저 합쳐지고, 257 (l, o), 258 (s, t) 가 이어집니다. 259~263은 한글 바이트 병합이라 이 단어에는 해당하는 쌍이 없습니다. 264 (␣, lo) 가 합쳐져 ␣lo 가 되고, 266 (␣lo, we) 가 ␣lowe 를 만듭니다. (␣lowe, st) 는 순위표에 없으니 여기서 멈춥니다. 결과는 [266, 258] 입니다.
tiktoken 에는 실용적인 지름길이 하나 있습니다. 정규식으로 자른 조각이 어휘표에 통째로 있으면 병합을 건너뛰고 바로 ID 를 냅니다. 흔한 영어 단어는 대부분 이 경로를 탑니다. 병합 루프 자체는 조각이 n 개 단위, 병합이 m 번일 때 O(mn) 입니다. 소스 주석은 힙을 쓰면 O(m log n) 으로 줄일 수 있지만, n 이 대개 작아서 캐시 지역성이 더 중요하다고 설명합니다. 실제로 100바이트 이상 긴 조각에서만 힙 기반 구현으로 갈아탑니다.
■ 직접 해 보기 1 — 바이트 수준 BPE 를 70줄 안팎으로
외부 라이브러리 없이 돌아가는 최소 구현입니다. 파이썬 3.8 이상이면 됩니다. 표준 re 모듈은 \p{L} 을 모르기 때문에 정규식은 비슷한 뜻으로 단순화했습니다.
[코드: python]
import re
from collections import Counter
# 1) 사전 토큰화: 공백은 뒤 단어에 붙이고, 글자·숫자·기호 묶음을 나눕니다
PAT = re.compile(r" ?[^\W\d_]+| ?\d{1,3}| ?[^\s\w]+|\s+")
def pretokenize(text):
return [m.encode("utf-8") for m in PAT.findall(text)]
def count_pairs(words):
pairs = Counter()
for word, freq in words.items():
for a, b in zip(word, word[1:]):
pairs[(a, b)] += freq
return pairs
def merge(word, pair, new_id):
out, i = [], 0
while i < len(word):
if i + 1 < len(word) and (word[i], word[i + 1]) == pair:
out.append(new_id); i += 2
else:
out.append(word[i]); i += 1
return tuple(out)
def train(text, vocab_size):
# 2) 기본 어휘 = 바이트 256개. 단어는 바이트 ID 튜플로 시작합니다
words = Counter(tuple(w) for w in pretokenize(text))
vocab = {i: bytes([i]) for i in range(256)}
merges = {} # (a, b) -> 새 ID (= 순위)
for new_id in range(256, vocab_size):
pairs = count_pairs(words)
if not pairs:
break
best = max(pairs, key=pairs.get) # 3) 가장 잦은 쌍
merges[best] = new_id
vocab[new_id] = vocab[best[0]] + vocab[best[1]]
words = Counter({merge(w, best, new_id): f for w, f in words.items()})
return merges, vocab
def encode(text, merges):
ids = []
for piece in pretokenize(text):
word = tuple(piece)
while len(word) > 1:
# 4) 지금 붙어 있는 쌍 중 '먼저 배운' 병합부터 적용합니다
pair = min(zip(word, word[1:]), key=lambda p: merges.get(p, float("inf")))
if pair not in merges:
break
word = merge(word, pair, merges[pair])
ids.extend(word)
return ids
def decode(ids, vocab):
return b"".join(vocab[i] for i in ids).decode("utf-8", errors="replace")
corpus = ("low lower lowest newer newest wider widest " * 50
+ "토큰 토큰화 토크나이저 토큰을 토큰이 " * 30)
merges, vocab = train(corpus, 256 + 20)
for i in range(256, 266):
print(i, vocab[i])
for s in ["lowest", " lowest", " 토큰화"]:
ids = encode(s, merges)
print(repr(s), ids, [vocab[i] for i in ids])
assert decode(ids, vocab) == s # 손실 없이 되돌아옵니다
[/코드]
실행 결과입니다.
[코드: text]
256 b'we'
257 b'lo'
258 b'st'
259 b' \xed'
260 b' \xed\x86'
261 b' \xed\x86\xa0'
262 b' \xed\x86\xa0\xed'
263 b' \xed\x86\xa0\xed\x81'
264 b' lo'
265 b' \xed\x86\xa0\xed\x81\xb0'
'lowest' [257, 256, 258] [b'lo', b'we', b'st']
' lowest' [266, 258] [b' lowe', b'st']
' 토큰화' [265, 237, 153, 148] [b' \xed\x86\xa0\xed\x81\xb0', b'\xed', b'\x99', b'\x94']
[/코드]
세 가지가 보입니다. 첫째, 259~263번 토큰은 한글 글자의 중간에서 끊긴 바이트 조각입니다. BPE 는 글자 경계를 모릅니다. 265번에 이르러서야 토큰(공백 + 6바이트)이 한 토큰이 됩니다. 둘째, 같은 lowest 라도 앞에 공백이 있느냐에 따라 토큰 수와 ID 가 다릅니다. 셋째, 말뭉치에 자주 나오지 않은 '화'는 병합되지 않고 바이트 3개(237, 153, 148)로 남습니다. 그래도 [UNK] 없이 손실 없이 복원됩니다. 바이트 수준 BPE 의 핵심 성질입니다.
■ 직접 해 보기 2 — 실제 토크나이저 세 세대 비교
OpenAI 의 tiktoken 으로 실제 모델의 토크나이저를 비교합니다. pip install tiktoken 후 실행합니다. 첫 실행 때 인코딩 파일을 내려받습니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📷📷📷 [이미지 04] ← 이미지 폴더의 04-shot-2.png 파일을 여기에 넣으세요
캡션: OpenAI 의 BPE 토크나이저 구현 tiktoken 저장소 (출처: GitHub)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[코드: python]
import tiktoken
text = "안녕하세요, 토크나이저입니다."
for name in ["gpt2", "cl100k_base", "o200k_base"]:
enc = tiktoken.get_encoding(name)
ids = enc.encode(text)
print(f"{name:12} 어휘 {enc.n_vocab:>7,} 토큰 {len(ids):>2}개")
enc = tiktoken.get_encoding("o200k_base")
for s in ["strawberry", "12345678", "hello", " hello"]:
ids = enc.encode(s)
print(repr(s), ids, [enc.decode_single_token_bytes(t) for t in ids])
[/코드]
[코드: text]
gpt2 어휘 50,257 토큰 38개
cl100k_base 어휘 100,277 토큰 17개
o200k_base 어휘 200,019 토큰 10개
'strawberry' [302, 1618, 19772] [b'st', b'raw', b'berry']
'12345678' [7633, 19354, 4388] [b'123', b'456', b'78']
'hello' [24912] [b'hello']
' hello' [40617] [b' hello']
[/코드]
같은 17글자 한국어 문장이 GPT-2 토크나이저로는 38토큰, o200k_base 로는 10토큰입니다. 이 문장은 UTF-8 로 45바이트이니, GPT-2 에서는 한글 대부분이 사실상 바이트 단위에 가깝게 쪼개진 셈입니다. 어휘가 커지고 학습 말뭉치에 다국어 비중이 늘면서 한글 조각이 어휘에 많이 들어간 결과로 볼 수 있습니다. 표에서 숫자를 정리합니다(모두 위 코드로 직접 잰 값).
인코딩 | 쓰는 모델(tiktoken 기준) | 어휘 수(n_vocab) | 예문 토큰 수 | 12345678 분할
------------------------------------------------------------
gpt2 | GPT-2 | 50,257 | 38 | 123 · 45 · 678
cl100k_base | GPT-3.5·GPT-4 | 100,277 | 17 | 123 · 456 · 78
o200k_base | GPT-4o 계열 | 200,019 | 10 | 123 · 456 · 78
숫자 분할 열을 보세요. GPT-2 는 정규식이 숫자 덩어리를 통째로 넘기고 BPE 가 빈도대로 끊어서 123|45|678 이 됩니다. cl100k_base 이후는 정규식이 먼저 세 자리씩 잘라 주므로 항상 123|456|78 입니다. 숫자를 어떻게 자르느냐는 모델이 산술을 배우는 데 영향을 줄 수 있어서, 모델 설계 단계에서 따로 신경 쓰는 부분입니다.
■ BPE · WordPiece · Unigram 비교
BPE 만 있는 것은 아닙니다. BERT 는 WordPiece, T5 는 SentencePiece 의 Unigram 모델을 씁니다. Hugging Face LLM 강좌의 정리를 바탕으로 비교합니다.
항목 | BPE | WordPiece | Unigram
------------------------------------------------------------
학습 방향 | 작은 어휘에서 시작해 병합 규칙을 배움 | 작은 어휘에서 시작해 병합 | 큰 어휘에서 시작해 토큰을 제거
병합·제거 기준 | 쌍의 등장 빈도 | 쌍 빈도 ÷ (앞 빈도 × 뒤 빈도) | 지웠을 때 말뭉치 우도 손실이 작은 토큰
학습 결과물 | 병합 규칙 + 어휘 | 어휘만 | 어휘 + 토큰별 점수
인코딩 방식 | 배운 순서대로 병합 재적용 | 앞에서부터 가장 긴 어휘 일치 | 점수상 가장 그럴듯한 분할 선택
대표 모델 | GPT-2, GPT 계열 | BERT | T5
SentencePiece 는 알고리즘 이름이 아니라 도구 이름입니다. Kudo 와 Richardson 의 논문(arXiv:1808.06226)이 내세운 특징은 공백으로 미리 단어를 나누지 않고 원시 문장에서 바로 학습한다는 점입니다. 공백도 일반 기호(▁)로 다루기 때문에 띄어쓰기가 없는 언어에도 그대로 쓸 수 있습니다. 내부 알고리즘으로는 BPE 와 Unigram 을 고를 수 있습니다.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📷📷📷 [이미지 05] ← 이미지 폴더의 05-shot-3.png 파일을 여기에 넣으세요
캡션: Hugging Face LLM 강좌의 BPE 토큰화 장 (출처: Hugging Face)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 흔한 오해와 실수
"1토큰은 대략 한 단어(또는 한 글자)다." 언어와 토크나이저에 따라 다릅니다. tiktoken README 는 "실제로는 평균적으로 한 토큰이 약 4바이트"라고 적었는데, 영어처럼 1바이트 문자가 많은 텍스트 기준입니다. 한글은 한 글자가 3바이트라 같은 계산이 맞지 않습니다. 위 실험처럼 같은 문장도 토크나이저에 따라 토큰 수가 네 배 가까이 차이 납니다. 비용·컨텍스트 계산은 반드시 그 모델의 토크나이저로 세야 합니다. GPT-2 토크나이저로 세어 놓고 최신 모델 요금을 추정하면 크게 빗나갑니다.
"스트리밍으로 받은 토큰을 하나씩 문자열로 바꾸면 된다." 바이트 수준 BPE 에서 토큰 하나는 완성된 글자가 아닐 수 있습니다. 앞의 학습 예제에서 259~263번 토큰이 그랬습니다. GPT-2 토크나이저로 "토큰"을 인코딩하면 토큰 6개가 나오는데, 하나씩 디코딩하면 전부 � 입니다.
[코드: python]
import codecs
import tiktoken
enc = tiktoken.get_encoding("gpt2")
ids = enc.encode("토큰")
print(ids) # [169, 228, 254, 169, 223, 108]
# 잘못된 방식: 토큰마다 따로 문자열로 바꾸기
print([enc.decode([t]) for t in ids]) # ['�', '�', '�', '�', '�', '�']
# 바른 방식: 바이트를 이어 받아 완성된 글자만 내보내기
dec = codecs.getincrementaldecoder("utf-8")()
print([dec.decode(enc.decode_single_token_bytes(t)) for t in ids])
# ['', '', '토', '', '', '큰']
[/코드]
직접 토큰 단위로 스트림을 처리하는 서버나 클라이언트를 만든다면 바이트를 버퍼에 모았다가 UTF-8 이 완성될 때 내보내야 합니다. 영어로만 테스트하면 절대 드러나지 않는 버그입니다.
"앞뒤 공백은 아무래도 상관없다." 사전 토큰화가 공백을 뒤 단어에 붙이기 때문에 hello 와 hello 는 서로 다른 토큰입니다(o200k_base 에서 24912와 40617). 프롬프트 끝에 공백을 남기면 모델은 "공백 다음에 올 토큰"을 예측해야 합니다. 그런데 학습 데이터에서 공백은 대부분 다음 단어 토큰 안에 붙어 있었으므로, 공백으로 끝난 입력은 모델이 보기에 드문 모양입니다. 완성형(completion) 프롬프트를 짤 때 끝 공백을 조심해야 하는 이유입니다.
"모델이 철자를 못 세는 건 멍청해서다." 모델이 보는 것은 strawberry 가 아니라 st · raw · berry 세 개의 ID 입니다. 각 ID 안에 어떤 글자가 몇 개 있는지는 입력에 직접 드러나지 않습니다. 임베딩 학습 과정에서 간접적으로 익혀야 하는 정보입니다. 글자 단위 과제(철자 세기, 거꾸로 쓰기, 정확한 글자 수 맞추기)가 유독 약한 이유를 토큰화에서 찾을 수 있습니다.
"어휘는 클수록 좋다." 어휘가 커지면 같은 텍스트가 더 적은 토큰이 되어 컨텍스트와 연산을 아낍니다. Meta 는 Llama 3 가 어휘 128K 토크나이저를 써서 Llama 2 대비 토큰이 최대 15% 적게 나온다고 밝혔습니다. 대신 임베딩 행렬과 출력층이 어휘 수에 비례해 커지고, 드문 토큰은 학습 중에 거의 보지 못해 임베딩이 덜 학습됩니다. 공짜가 아니라 저울질입니다.
"토크나이저만 바꿔 끼우면 된다." 토큰 ID 는 임베딩 행렬의 행 번호입니다. 토크나이저를 바꾸면 같은 번호가 다른 문자열을 가리키게 되어 모델이 쓸모없어집니다. 파인튜닝 때 특수 토큰을 추가한다면 임베딩 행렬도 같이 늘려야 합니다.
■ 정리
- BPE 는 바이트 256개에서 출발해 "가장 자주 붙어 나오는 이웃 쌍"을 반복해서 합치며 어휘를 만들고, 인코딩 때는 그 병합을 배운 순서대로 다시 적용합니다.
- 실제 토크나이저의 성격은 BPE 자체보다 앞단의 사전 토큰화 정규식(공백·숫자·기호 처리)과 어휘 크기에서 크게 갈립니다.
- 토큰은 글자가 아니라 바이트 조각입니다. 토큰 수는 모델별 토크나이저로 세고, 스트리밍은 바이트를 모아서 디코딩해야 합니다.
■ 참고 자료
- Sennrich, Haddow, Birch, "Neural Machine Translation of Rare Words with Subword Units" — https://arxiv.org/abs/1508.07909
- Radford 외, "Language Models are Unsupervised Multitask Learners"(GPT-2 논문) 2.2절 Input Representation — https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf
- GPT-2 원본 토크나이저 encoder.py — https://github.com/openai/gpt-2/blob/master/src/encoder.py
- tiktoken 저장소 README — https://github.com/openai/tiktoken
- tiktoken 인코딩 정의(정규식·특수 토큰) — https://github.com/openai/tiktoken/blob/main/tiktoken_ext/openai_public.py
- tiktoken Rust 코어(병합 루프) — https://github.com/openai/tiktoken/blob/main/src/lib.rs
- Hugging Face LLM Course, Byte-Pair Encoding tokenization — https://huggingface.co/learn/llm-course/chapter6/5
- Hugging Face LLM Course, Normalization and pre-tokenization(알고리즘 비교 표) — https://huggingface.co/learn/llm-course/chapter6/4
- Hugging Face LLM Course, WordPiece tokenization — https://huggingface.co/learn/llm-course/chapter6/6
- Hugging Face LLM Course, Unigram tokenization — https://huggingface.co/learn/llm-course/chapter6/7
- Kudo, Richardson, "SentencePiece" — https://arxiv.org/abs/1808.06226
- Meta, "Introducing Meta Llama 3" — https://ai.meta.com/blog/meta-llama-3/
'[Ai Developer]' 카테고리의 다른 글
| 구조화된 출력(JSON 모드) 원리 — 제약 디코딩으로 형식 강제 (0) | 2026.10.13 |
|---|---|
| MXC·Gemini 에이전트·텔레그램 CVE — 이번 주 IT 뉴스 (0) | 2026.10.12 |