Deep DiveSennrich·Haddow·Birch ACL 2016 원문 Section 3–6·Algorithm 1·Tables 1–3 직접 확인, 저자 공식 subword-nmt 저장소 commit 92d6139의 learn_bpe.py·apply_bpe.py·README 직접 확인, Python 3.9.6으로 저자 구현에 toy vocabulary를 넣어 10개 merge rule 학습·새 입력 적용·3개 rule 제한을 직접 실행

BPE는 단어를 외우지 않는다 — Subword와 토큰화의 시작

Sennrich·Haddow·Birch ACL 2016 원 논문을 기준으로 BPE가 희귀 단어 문제를 왜 문자 쌍 병합으로 바꾸었는지, 학습 corpus에서 merge rule을 만들고 새 문장에 순서대로 적용하는 내부 상태, 단어 경계·어휘·token id의 차이, 원 논문의 character BPE와 현재 subword-nmt 0.2 구현의 end-of-word 차이, Python 3.9.6으로 저자 구현을 직접 실행한 10회 병합·적용·복원, 성능과 LLM token budget의 교환, 실패·재현·운영·대안을 12살 독자가 설명할 수 있게 정리한다.

3단계 여덟 번째 논문. 이 글은 Rico Sennrich, Barry Haddow, Alexandra Birch의 ACL 2016 원 논문과 저자 공식 subword-nmt 구현을 직접 읽고 썼다. BPE(Byte Pair Encoding, 바이트 쌍 인코딩)는 문장을 통째 단어 목록으로 외우지 않고, 자주 붙어 다니는 작은 조각을 데이터에서 합쳐 고정 크기 어휘로 새 단어를 표현하는 규칙 학습 방법이다. 논문은 원래 압축 알고리즘의 byte 대신 character(문자)를 기본 조각으로 쓴다.

이 글에서 얻을 답과 범위

LLM(Large Language Model, 대규모 언어 모델)에 문장을 넣기 전에는 반드시 text를 정수 배열로 바꿔야 한다. 이때 안녕하세요, GraphRAG, 새로 만든 제품명처럼 학습 때 없던 문자열을 어떻게 고정된 model vocabulary(모델 어휘)로 보낼지가 tokenization(토큰화)의 문제다. BPE는 이 문제의 출발점 중 하나다.

이 글을 읽은 뒤에는 아래를 답할 수 있어야 한다.

  1. 단어 단위 모델이 rare word(희귀 단어)와 OOV(Out Of Vocabulary, 어휘 밖 단어)에서 왜 멈추는가.
  2. BPE가 처음에는 문자로 시작해 가장 자주 이웃한 pair(쌍)를 합치는 이유는 무엇인가.
  3. merge rule(병합 규칙) 파일은 vocabulary file(어휘 파일)과 어떻게 다르고, 새 문자열에는 어떤 순서로 적용되는가.
  4. BPE 조각은 morpheme(형태소)가 아닌데도 왜 유용하며, 왜 새 단어를 완벽히 이해한다는 뜻은 아닌가.
  5. 원 논문의 character BPE와 오늘의 byte-level BPE(바이트 단위 BPE), SentencePiece(문장 조각), WordPiece(워드피스), Unigram(유니그램) 모델은 어디가 다른가.
  6. vocabulary size(어휘 크기), sequence length(시퀀스 길이), model 비용을 어떤 기준으로 교환해야 하는가.
  7. tokenizer artifact(토크나이저 산출물)를 model checkpoint와 왜 한 묶음으로 운영해야 하는가.

범위는 논문의 NMT(Neural Machine Translation, 신경망 기계 번역) rare-word 해법과 그 BPE 알고리즘이다. Transformer(트랜스포머)의 attention(어텐션), GPT 계열의 byte-level BPE, 한국어 형태소 분석 전체를 이 글 하나로 대체하지 않는다. 특히 논문의 결과는 2015 WMT English→German·English→Russian 번역 설정의 결과이지, 2026년 LLM 품질 순위가 아니다. LLM에 연결하는 내용은 원 논문 사실과 현재 공식 tokenizer 구현을 분리하여 해석한다.

먼저 잡을 한 문장

BPE는 단어의 뜻을 찾는 알고리즘이 아니다. 학습 text에서 자주 함께 나타난 인접 조각을 합친 규칙표를 만들고, 낯선 단어도 남은 작은 조각으로 보내어 [UNK] 하나가 되는 일을 줄이는 알고리즘이다.

왜 필요한가 — 단어 목록은 현실의 새 문자열을 다 담지 못한다

단어 하나당 id 하나라는 약속의 실패

가장 단순한 tokenizer는 공백으로 나눈 단어마다 id를 부여한다.

학습 어휘
  "나는" → 17
  "데이터" → 92
  "파이프라인" → 304

새 입력
  "나는 GraphRAG를 만든다"
  → [17, ???, ???]

GraphRAG를이 training corpus(학습 말뭉치)에 없으면 word-level model(단어 단위 모델)은 보통 [UNK] 하나로 뭉개거나, 별도 사전·복사·음역 모델로 우회해야 했다. 그러나 사람 이름, 복합어, 오타가 아닌 새 제품명, 굴절된 단어는 계속 생긴다. 언어의 vocabulary는 열린 집합인데 neural model의 input embedding table(입력 임베딩 표)은 유한한 행만 가져야 하기 때문이다.

행을 무작정 늘려 50만 단어를 모두 넣어도 두 문제가 남는다.

선택좋아지는 점대가
큰 단어 어휘문장이 짧아진다희귀 단어 행은 거의 학습되지 않고 embedding·출력 층이 커진다
문자 하나씩거의 모든 새 단어를 철자로 보낼 수 있다문장이 매우 길어져 model이 먼 문자를 연결해야 한다
subword자주 쓰는 덩어리는 짧게, 낯선 단어는 조각으로 보낸다조각 경계가 의미 경계라는 보장은 없다

원 논문은 이 중간 지점을 찾았다. 논문은 NMT가 보통 30,000–50,000 word vocabulary에 묶이지만 번역은 open-vocabulary problem(열린 어휘 문제)이라고 지적한다. 이름은 복사·음역으로, 합성어는 구성 조각으로, 동계어와 차용어는 철자 변환으로 번역될 때가 있다.원 논문 1절·3절

단어를 잘게 자른다고 모두 해결되는 것은 아니다

internationalization을 문자 20개로 보내면 OOV는 줄지만 sequence가 길어진다. 원 논문도 encoder-decoder(인코더-디코더)의 시간 비용은 적어도 sequence length에 선형이고, 과도한 분할은 효율과 먼 거리 정보 전달을 해친다고 적는다. BPE의 질문은 “진짜 형태소를 맞힐까”가 아니라 다음이다.

작은 기본 조각은 언제나 보낼 수 있게 남긴다.
학습에서 자주 붙은 조각만 합쳐, 평균 길이는 줄인다.

이것이 frequency(빈도) 기반 압축 발상이다. 자주 나오는 tion이나 흔한 단어 전체는 한 조각이 되고, 한 번도 본 적 없는 GraphRAG는 이미 아는 더 작은 조각들의 열이 된다.

무엇인가 — 고정 어휘와 가변 길이를 함께 얻는 방법

token과 id와 embedding은 다른 물건이다

세 단어를 섞어 쓰면 나중에 model 오류를 못 찾는다.

이름역할
textlowest사람이 쓴 문자열
token 또는 subword unitlow, esttokenizer가 나눈 문자열 조각
token id418, 77model table을 찾는 정수 주소
embedding길이 d인 실수 vectorid가 고른 model 내부 숫자 묶음
merge rulel o, lo w어떤 이웃 조각을 어떤 우선순위로 붙일지 적은 규칙

BPE는 보통 정규화와 사전 분할 뒤의 subword model이다. BPE만으로 special token(특수 토큰) 추가, padding(패딩), truncation(자르기), id 배정, decoding(복원) 정책이 완성되지는 않는다. 현재 Hugging Face Tokenizers도 raw input이 Normalizer → PreTokenizer → Model → PostProcessor 순으로 흐르고, Model이 subword로 나누며 id로 바꾼다고 문서화한다.공식 Tokenizers pipeline 문서

논문의 BPE 정의

원래 byte pair encoding은 압축에서 가장 자주 함께 나타나는 byte pair를 새 byte로 치환한다. 논문은 이를 word segmentation(단어 분할)에 맞게 바꿨다.

  1. 각 단어를 문자열로 나눈다.
  2. word 끝을 알 수 있게 별도 </w> end-of-word marker(단어 끝 표식)를 붙인다.
  3. 전체 training vocabulary에서 인접 symbol pair 빈도를 센다.
  4. 가장 빈도 높은 pair (A, B)의 모든 출현을 새 symbol AB로 합친다.
  5. 정한 횟수만큼 3–4를 반복하고, 합친 pair의 순서 있는 목록을 저장한다.

처음 symbol vocabulary가 문자 집합이라면, merge를 M번 했을 때 후보 symbol 수는 대략 초기 문자 수 + M이다. 논문에서 M은 유일한 주 hyperparameter(하이퍼파라미터)다. 이 말은 “정확히 M개 token이 최종 model vocabulary에 남는다”는 뜻은 아니다. 실제 model은 빈도 cutoff(절단값), special token, source·target 분리 여부를 더 적용할 수 있다.

BPE가 하지 않는 일

  • researcherresearch + er로 나눴다고 해서 의미를 이해한 것은 아니다.
  • 학습 때 없던 문자 자체가 오면 원 논문의 character BPE도 자동 해결하지 못한다.
  • 같은 text라도 normalizer, 공백 규칙, pre-tokenizer, codes, vocab이 달라지면 id 열이 달라진다.
  • BPE가 번역·검색·RAG(Retrieval-Augmented Generation, 검색 증강 생성) 품질을 혼자 보장하지 않는다.

원 논문도 test time에 unknown character, 또는 학습에서 모든 출현이 더 큰 symbol로 합쳐져 남지 않은 symbol은 문제가 될 수 있고, 필요하면 merge를 재귀적으로 되돌려 해결할 수 있다고 명시한다.원 논문 3.2절 각주 3 그러므로 “BPE면 무조건 [UNK]가 없다”는 문장은 틀리다.

선행 개념 — 빈도표와 경계 표식이 만드는 학습 데이터

vocabulary는 단어 사전이 아니라 가중된 word type 표다

논문의 Algorithm 1은 문장 전체를 매번 다시 돌지 않는다. 공백 경계를 넘는 pair는 합치지 않으므로, 다음처럼 word type(서로 다른 단어)과 frequency를 표로 만든다.

low       5회  → l o w </w>
lower     2회  → l o w e r </w>
newest    6회  → n e w e s t </w>
widest    3회  → w i d e s t </w>

low가 5회면 그 안의 (l,o), (o,w), (w,</w>)도 각각 5만큼 pair count에 기여한다. 그래서 BPE가 배운 것은 “한 단어 안에서 자주 반복되는 인접 조각”이다. 공백을 넘어서 hello worldo + w를 합치는 원 논문의 알고리즘은 아니다.

word boundary가 없으면 복원이 깨진다

lowlowest를 그냥 문자만 이어 합치면 generation 뒤에 공백을 어디에 넣을지 잃는다. 논문은 </w>를 symbol로 두어 마지막 조각이 word-final(단어 끝)인지 구별했다. 예를 들어 lowerlow er</w>로 끝났다면 </w>을 제거하며 하나의 word를 닫을 수 있다.

중요한 확인 사항이 하나 있다. 원 논문의 Algorithm 1은 </w>를 별도 symbol로 시작한다. 그러나 저자 공식 subword-nmt의 현재 0.2 format은 마지막 문자에 붙인 d</w>로 시작한다. repository README는 이것이 word-internal un과 word-final und의 모호성을 줄이기 위한 의도적 호환성 변화이며, #version: 0.2 첫 줄로 식별한다고 설명한다.공식 README 162–184행

따라서 이 글의 직접 실행 출력은 논문 Figure 1의 문자 모양과 완전히 같을 필요가 없다. 알고리즘의 핵심은 같아도 artifact format이 달라졌다. 논문 재현에는 논문 당시 codes·전처리 버전을 고정해야 한다.

어휘 크기와 길이의 비용 식

V를 model vocabulary size, d를 embedding 차원, L을 한 문장의 token 수라고 하자.

[ \text{input embedding parameter 수}=V\times d \tag{1} ]

입력 embedding table만 세도 V가 두 배면 저장할 parameter가 두 배다. output projection(출력 분류 행렬)을 input embedding과 묶지 않는 model이라면 보통 V×d 규모가 하나 더 생긴다. 반대로 BPE를 너무 잘게 하면 L이 커진다. Transformer full self-attention(전체 자기 어텐션)은 한 층에서 token 쌍을 보므로 attention score 수가 에 비례한다.

[ \text{attention score 수} \propto L^2 \tag{2} ]

식 (2)는 2016 RNN 기반 원 논문의 측정식이 아니라 Transformer에 연결한 해석이다. 하지만 오늘 LLM에서 tokenizer를 model과 분리해 바꾸면 안 되는 이유를 설명한다. V를 작게 하려고 L을 과하게 늘리면 context window(문맥 창)와 latency(지연 시간)를 잃는다.

밑바닥 원리와 알고리즘 — 가장 흔한 이웃을 합치는 탐욕 규칙

작은 예제로 한 번 학습하기

아주 작은 vocabulary에서 (s,t</w>)가 9회로 가장 많다고 하자. 첫 merge는 다음이다.

before  newest  → n e w e s t</w>
merge   s + t</w> → st</w>
after   newest  → n e w e st</w>

다음 round에서는 다시 바뀐 symbol 열에서 모든 pair를 센다. e + st</w>가 높으면 est</w>가 된다. 그러므로 merge는 독립적인 빈도 순위표가 아니다. 앞 merge가 뒤 pair count를 바꾼다. 학습 결과는 다음처럼 순서 자체가 model이다.

0  s t</w>
1  e st</w>
2  l o
3  w est</w>

l oo w가 둘 다 codes에 있어도 새 word 적용 시에는 “어느 규칙이 더 먼저 학습되었는가”가 중요하다. 저자 구현의 encode는 현재 인접 pair 중 codes에 있는 것들을 모아 가장 작은 rule rank를 고른다.apply_bpe.py 276–342행

학습 의사코드

아래는 논문 Algorithm 1의 뜻을 보이기 위한 의사코드다. 실행한 저자 구현 자체는 다음 절의 명령을 사용했다.

vocab = weighted_character_sequences_with_eow(training_text)
codes = []

for _ in range(num_merges):
    pair_frequency = count_adjacent_pairs(vocab)
    best_pair = pair_with_largest_frequency(pair_frequency)
    vocab = replace_every_occurrence(vocab, best_pair, join(best_pair))
    codes.append(best_pair)

입력은 weighted vocabulary이고, 매 round의 중간 상태는 symbol 열이며, 출력은 (A, B) pair들의 ordered codes다. 같은 최고 빈도의 tie(동점)가 나면 어떤 pair를 먼저 택하는지도 결과를 바꾼다. 원 논문 Algorithm 1은 tie-break를 규정하지 않는다. 현재 저자 구현은 max(stats, key=lambda x: (stats[x], x))로 pair tuple까지 비교한다.learn_bpe.py 298–360행

새 단어 적용 알고리즘

학습 중 없던 lowest도 시작은 문자다.

input      l o w e s t</w>
codes      l o      rank 2
           lo w     rank 6
           e st</w> rank 1

result     low est</w>
serialized low@@ est

@@는 논문의 수학 기호가 아니라 subword-nmt가 “아직 word가 끝나지 않았다”를 사람이 보게 쓰는 separator(구분자)다. model은 보통 low@@est에 해당하는 id를 받는다. generation output을 text로 만들 때는 separator와 경계 정책을 정확히 역적용해야 한다.

시간과 공간 비용

논문 의사코드를 그대로 구현하면 merge마다 vocabulary의 symbol 열을 다시 훑어 pair를 세므로, M개 merge와 전체 symbol occurrence N에 대해 대략 O(MN)의 scan을 한다. 이 식은 경계 없는 corpus를 뜻하지 않고, frequency로 가중한 word-level symbol 열의 총량을 뜻한다.

현재 저자 구현은 statsindices를 들고 있다가 merge에 영향 받은 word만 갱신한다. update_pair_statistics의 주석도 겹치는 occurrence 근처 pair만 영향 받는다고 설명한다.learn_bpe.py 159–234행 그래서 실제 비용은 corpus의 반복 분포와 merge 수에 따라 달라진다. 적용 단계는 한 word에서 남은 인접 pair를 반복으로 찾고, cache(캐시)가 있으면 같은 word는 재사용한다. 이 구현은 cache를 self.cache에 저장한다.apply_bpe.py 70–85행

내부 구조와 실제 실행 흐름 — text에서 model id까지

학습 경로

training text
  → 공백 기준 word count
  → 문자와 word-end symbol 열
  → pair frequency와 pair index
  → 최고 빈도 pair merge 반복
  → codes file
  → 필요하면 segmented training text와 model vocabulary/id map

공식 구현은 get_vocabulary에서 text를 읽어 Counter로 word frequency를 만들고, learn_bpe에서 마지막 문자에 </w>를 붙인 tuple로 바꾼 뒤 statistics와 indices를 만든다.learn_bpe.py 77–136행 이 관찰은 “원 논문 BPE가 raw sentence 전체에서 byte bigram을 무작정 합친다”는 오해를 막는다. 이 구현은 먼저 공백으로 나눠 word vocabulary를 만들고 pair가 word boundary를 넘지 않게 한다.

추론 경로

raw request text
  → model이 요구한 normalization과 pre-tokenization
  → 같은 BPE codes를 rule rank 순서로 적용
  → token string
  → model vocab의 id lookup
  → embedding lookup
  → NMT 또는 LLM

codes만 있어도 충분하지 않다. Model은 id의 순서에 학습되었다. token="low" → id=418id=912로 바꾸면 같은 문자열도 다른 embedding row를 읽는다. 서버가 text를 주고받는 API라 해도 tokenizer revision, normalizer revision, vocab, merges, special-token map을 model checkpoint와 함께 versioning(버전 관리)해야 한다.

원 논문과 최신 계열을 섞지 않는 비교

방식기본 단위와 학습경계 처리이 글과의 관계
Sennrich BPE문자에서 빈번한 pair를 greedy merge사전 공백 분할과 </w>원 논문 그 자체
byte-level BPEbyte에서 BPE merge구현별 byte mapping·pre-tokenizer문자 밖 입력을 다루려는 후속 변형
WordPieceBPE와 비슷해 보이나 별도 training score·decode 정책## 등 구현 정책BPE와 이름만 같은 알고리즘으로 취급하면 안 됨
Unigram큰 후보 vocabulary에서 확률 model로 pruningmodel별 정책merge-only BPE와 학습 방향이 반대
SentencePieceBPE 또는 Unigram을 raw sentence에 적용 가능whitespace를 symbol로 보존language-specific pre-tokenization 의존을 줄이는 구현 계열

SentencePiece 공식 README는 BPE와 Unigram을 지원하고 raw sentence에서 학습할 수 있으며, whitespace를 로 보존해 복원 가능한 text를 만든다고 설명한다.SentencePiece 공식 README Hugging Face 공식 문서도 BPE, WordPiece, Unigram을 별도 Model로 구분한다.공식 Tokenizers model 문서 그러므로 pretrained LLM의 tokenizer를 읽을 때 “BPE 비슷함”에서 멈추지 말고 artifact format과 normalizer까지 확인해야 한다.

직접 검증과 재현 — 저자 구현으로 규칙을 만들고 적용하기

실행 환경과 입력

직접 실행 확인이다. 2026-07-24에 저자 subword-nmt repository commit 92d6139d07d30e12735a0af9e7f7f925ebe62c54을 clone했고, Python 3.9.6에서 실행했다. 논문 대규모 WMT training은 재현하지 않았다. 아래는 Algorithm 1의 상태 변화를 작게 보기 위한 입력이다.

# toy-vocab.txt — --dict-input 형식
low 5
lower 2
newest 6
widest 3

# toy-input.txt
low lower lowest newer widest
export PYTHONPATH=/tmp/bpe-research/subword-nmt

python3 -m subword_nmt.learn_bpe \
  --dict-input --input /tmp/bpe-research/toy-vocab.txt \
  --output /tmp/bpe-research/toy.codes \
  --symbols 10 --min-frequency 1 --verbose

python3 -m subword_nmt.apply_bpe \
  --codes /tmp/bpe-research/toy.codes \
  --input /tmp/bpe-research/toy-input.txt \
  --output /tmp/bpe-research/toy.output

python3 -m subword_nmt.apply_bpe \
  --codes /tmp/bpe-research/toy.codes --merges 3 \
  --input /tmp/bpe-research/toy-input.txt

실제 관찰 결과

== learned merge codes ==
#version: 0.2
s t</w>
e st</w>
l o
w est</w>
n e
ne west</w>
lo w</w>
w i
wi d
wid est</w>

== segmented input with all 10 rules ==
low lo@@ w@@ e@@ r lo@@ west ne@@ w@@ e@@ r widest

== same input with only first 3 rules ==
lo@@ w lo@@ w@@ e@@ r lo@@ w@@ est n@@ e@@ w@@ e@@ r w@@ i@@ d@@ est

관찰은 세 가지다.

  1. low는 frequency 5 덕분에 7번째 rule까지 거쳐 하나가 된다.
  2. training vocabulary에 없던 lowest[UNK]가 아니라 lo@@ west로 표현된다. westw est</w> rule에서 생겼다.
  3. rule을 10개에서 3개로 줄이면 같은 text의 길이가 길어진다. M이 vocabulary와 sequence length를 교환한다는 것을 실제로 볼 수 있다.

lowerlo@@ w@@ e@@ r가 된 것도 중요하다. 사람은 low + er를 기대할 수 있지만, 이 작은 corpus의 빈도 규칙은 그 형태소 경계를 보장하지 않는다. BPE는 language understanding(언어 이해)이 아니라 frequent adjacency compression(빈번한 인접 조각 압축)이다.

복원 검증

@@ 표기는 word-final 조각이 아닌 곳의 표시다. 공식 README도 sed -r 's/(@@ )|(@@ ?$)//g'로 original segmentation을 복원하는 예를 제공한다.공식 README 27–43행 단, 이 shell 치환은 이 저장소의 whitespace-tokenized format에 대한 예다. production tokenizer는 raw whitespace, normalization, special token을 어떻게 보존하는지 model별 decoder로 검증해야 한다.

재현이 달라질 때 보는 순서

1. codes 첫 줄과 commit을 확인한다. 0.1과 0.2의 </w> 표현이 다르다.
2. train corpus의 Unicode normalization·대소문자·공백 tokenizer가 같은지 확인한다.
3. merge count, min frequency, joint 또는 separate training을 확인한다.
4. vocab filtering과 threshold가 적용됐는지 확인한다.
5. token string뿐 아니라 final id 배열과 decode round-trip을 비교한다.

성능과 트레이드오프 — 압축률 하나로 tokenizer를 고르지 않는다

원 논문이 실제로 측정한 것

원 논문은 WMT 2015에서 English→German training 420만 sentence pair 약 1억 token, English→Russian 260만 pair 약 5천만 token을 사용했다. German training corpus 통계에서 unsegmented text는 1억 token과 175만 type, BPE는 1억 1200만 token과 6.3만 type이었다. 즉 BPE는 text token 수를 조금 늘리는 대신 type 수를 크게 줄였다.원 논문 Table 1

English→German newstest2015의 8-model ensemble에서 word dictionary back-off baseline은 24.2 BLEU, BPE-J90k는 24.7 BLEU와 54.1 CHRF3였다. English→Russian에서는 baseline 22.8 BLEU, BPE-J90k 24.1 BLEU와 53.0 CHRF3였다.원 논문 Tables 2–3 논문은 모든 subword ensemble이 back-off dictionary보다 0.3–1.3 BLEU, 0.6–2 CHRF3 높았다고 보고한다.

이 숫자는 “BPE vocabulary 90k가 항상 최적”이라는 규칙이 아니다. 논문도 vocabulary size 선택은 비교를 위해 다소 임의적이었고, 최적값은 language pair와 data size에 의존할 것이라 적었다. 또한 development set에서 다른 model 사이에 최대 1 BLEU 차이를 관찰했다. 외부 benchmark는 dataset, architecture, ensemble, decoding 설정을 붙여서만 읽어야 한다.

LLM에서 보는 세 가지 지표

지표왜 보는가나빠지면 생기는 일
평균 token 수와 p95 token 수context와 attention 비용긴 문서·한국어·코드에서 context가 빨리 찬다
[UNK]·byte fallback·invalid decode 비율문자 coverage와 decoder 안전성입력 손실, 검색 query와 원문 불일치
tokenizer round-trip 차이data fidelity(데이터 충실도)공백·정규화·특수 문자가 바뀌어 audit와 citation이 깨진다
model별 id paritycheckpoint 호환성같은 text가 다른 embedding으로 들어가 품질이 무너진다

token 당 비용만 낮추려고 merge를 크게 하면 vocabulary와 embedding memory가 커진다. 반대로 작은 vocabulary가 무조건 싸지도 않다. RAG에서는 chunk size를 character 수가 아니라 실제 target model token 수로 재고, retrieval 문서와 prompt template까지 포함한 token budget을 측정해야 한다. 이것은 원 논문이 직접 측정한 RAG 결론이 아니라, VL의 교환을 LLM 운영에 적용한 해석이다.

실패와 운영 기준 — tokenizer는 가벼워 보여도 model 계약이다

자주 터지는 실패

증상흔한 원인확인과 수정 방향
같은 문장이 배포 전후 다른 idcodes·vocab·normalizer 중 하나만 바뀜모든 tokenizer artifact hash를 checkpoint manifest에 기록하고 id fixture로 CI 검증
생성 text에 @@ 또는 이상한 공백decoder가 training serialization과 다름token string이 아니라 decode round-trip fixture를 추가
특정 언어·emoji에서 [UNK]character base vocabulary coverage 부족model이 허용하는 byte fallback 또는 문자 coverage 정책을 사전에 선택
번역에서 이름 글자 삽입·삭제source와 target segmentation 불일치joint BPE와 per-language vocabulary filter를 비교
token 수가 급증normalization/pre-tokenization 변경 또는 domain driftproduction text의 평균·p95 token 수를 release 전후 비교

원 논문은 English와 Russian의 독립 BPE가 Mirzayeva처럼 source·target segmentation을 다르게 해 음역 오류를 만들 수 있음을 예시로 보이고, joint BPE가 그 일관성을 높였다고 분석한다.원 논문 5.2절 Tables 4–5 저자 README도 joint learning은 name 복사·음역 일관성을 높일 수 있지만, 한 언어에서만 본 segment가 다른 언어에서는 unknown일 수 있어 --vocabulary와 threshold로 제한하라고 적는다.공식 README 45–85행

변경과 복구 원칙

  1. 이미 학습한 model의 tokenizer를 제자리에서 교체하지 않는다. id 의미가 바뀐다.
  2. 새 tokenizer는 새 model training 또는 정해진 vocabulary expansion 절차와 함께 version을 올린다.
  3. tokenizer.json 하나만 저장하지 말고 normalizer, pre-tokenizer, merges, vocab, special tokens, library version, SHA-256을 manifest에 남긴다.
  4. release 전에 대표 한국어·영어·코드·URL·emoji·공백 fixture의 text → ids → text 결과를 비교한다.
  5. rollback은 이전 checkpoint와 이전 tokenizer artifact를 쌍으로 되돌린다.

Tokenizer는 prompt injection을 막는 security boundary(보안 경계)가 아니다. token을 잘게 나눠도 model이 instruction을 해석하는 문제는 남는다. 다만 input을 잃지 않고 audit 가능하게 보존하는 것은 RAG와 agent(에이전트) 운영의 기본 계약이다.

대안과 선택 기준 — BPE를 고르는 질문은 언어와 model 계약에서 시작한다

상황먼저 검토할 선택이유
논문 원리와 고전 NMT 재현원 논문 character BPE와 당시 preprocessing논문 결과와 artifact를 같은 경계에서 비교
여러 문자 집합과 낯선 문자 coveragebyte-level BPE 또는 명시적 fallback 정책character vocabulary 밖 입력을 어떻게 보낼지 결정
공백 없는 언어와 raw input의 가역 복원SentencePiece BPE 또는 Unigrampre-tokenization과 whitespace 보존을 tokenizer 안에서 관리
BERT 계열 checkpoint 사용해당 checkpoint의 WordPiece artifactBPE로 바꾸면 id와 embedding 계약이 깨짐
고정 model 없이 새 corpus로 pretrainingBPE와 Unigram을 같은 data·same vocabulary budget에서 평가알고리즘 이름이 아니라 token 길이·coverage·downstream 품질로 선택
이미 배포된 LLM 호출제공 모델의 공식 tokenizer내 tokenizer로 잰 token 수는 과금·context와 다를 수 있음

SentencePiece README는 final vocabulary size를 직접 지정하는 방식이 subword-nmt의 merge operation 수 지정과 다르다고 구분한다.SentencePiece 공식 README 따라서 “32k vocabulary로 해 주세요”라는 요구도 구현에 따라 M=32,000이 아닐 수 있다.

흔한 오해와 최초 질문에 대한 답

BPE는 형태소 분석기인가

아니다. BPE는 가장 frequent한 문자 인접쌍을 탐욕적으로 합친다. 실행 예제의 lower → lo@@ w@@ e@@ r가 증거다. 의미 경계와 우연히 맞을 수는 있어도 보장하지 않는다.

BPE면 모든 새 단어를 이해하는가

아니다. 새 word를 이미 아는 조각으로 표현할 가능성을 높인다. 그 조각의 의미·번역·추론을 model이 배웠는지는 별도 학습 문제다. unknown character와 artifact coverage 문제도 남는다.

merge 수를 크게 하면 항상 더 좋은가

아니다. 긴 sequence는 줄지만 vocabulary, embedding, output 비용이 늘고 희귀 조각은 학습 신호가 적다. 원 논문도 최적 vocabulary size가 language pair와 data size에 의존한다고 한정했다.

내 RAG에 BPE를 새로 학습하면 검색이 좋아지는가

보통 아니다. 이미 배포된 embedding model과 generator는 자기 tokenizer의 id 계약으로 학습됐다. 새로운 BPE를 붙이면 그 model 입력이 호환되지 않는다. 먼저 공식 tokenizer로 document·query·prompt의 token 분포를 측정하고, 새 tokenizer 학습은 새 pretraining 또는 재학습 계획이 있을 때만 검토한다.

이 논문을 지금 읽어야 하는 이유는 무엇인가

LLM API를 쓰는 사람도 “문자 수”가 아니라 token budget을 설계하고, model마다 tokenizer가 다른 이유를 알아야 한다. BPE를 이해하면 RAG chunk가 왜 자주 잘리고, 한국어·코드·URL에서 비용이 왜 달라지며, tokenizer file 하나를 바꾸는 일이 왜 model 교체와 같은 급의 변경인지 원인부터 설명할 수 있다.

출처 및 검증 경로

1차 자료

현재 구현 비교 자료

직접 실행 기록

  • 환경 — macOS, Python 3.9.6, subword-nmt commit 92d6139d07d30e12735a0af9e7f7f925ebe62c54.
  • 입력 — 본문 toy-vocab.txt의 4개 word type과 frequency, toy-input.txt의 5개 word.
  • 명령 — 본문 learn_bpe 10 merges, apply_bpe 전체 rules와 --merges 3.
  • 관찰 — #version: 0.2 codes 10개, lowest → lo@@ west, 3개 rule에서 더 긴 token 열. 이 작은 실행은 원 논문 WMT 성능을 재현한 것이 아니라 규칙 학습·적용·길이 교환을 검증한 것이다.
대화

댓글

0
댓글을 불러오는 중입니다.