Skip to content
psymon-ai
Go back

한국어 토크나이저 실험기 2 - 공백의 의미

0

Table of contents

Open Table of contents

지난 이야기: 프리토크나이저가 수상하다

1편은 형태소 가설에서 출발했다.

한국어는 조사와 어미가 붙는 언어다. 그래서 BPE가 형태소 경계를 조금 더 잘 알면 한국어 토큰 수가 줄어들 거라 생각했다. 학습할 때만 형태소 분석기를 쓰고, 배포할 때는 일반 BPE처럼 tokenizer.json 하나로 끝내는 방식이면 꽤 괜찮아 보였다.

실험 결과는 예상과 달랐다.

MorphBPE의 α를 바꿔도 거의 움직이지 않았다. 형태소 경계를 강제로 막으면 오히려 나빠졌다. SuperBPE도 독립 효과가 뚜렷하지 않았다. BPE가 이미 만든 vocab을 나중에 골라내는 방법도 merge chain 때문에 쉽게 깨졌다.

그 과정에서 남은 단서가 있었다.

같은 BPE라도 pre-tokenizer regex가 달라지면 결과가 크게 움직였다. 처음엔 단순 전처리라고 생각했던 부분이다. 한글은 한글끼리, 영어는 영어끼리, 숫자는 숫자끼리 적당히 잘라 주는 장치. 그 정도로만 봤다.

그런데 BPE는 pre-tokenizer가 만든 조각 안에서만 merge할 수 있다. pre-tokenizer가 GPT-4를GPT, -, 4, 로 갈라 버리면 BPE는 4를을 배울 수 없다. 말뭉치에 GPT-4를이 아무리 자주 나와도, 그 경계 너머 pair는 애초에 후보가 되지 않는다.

그래서 질문이 바뀌었다.

형태소 경계를 어떻게 넣을까?

라는 질문에서

BPE에게 어떤 문자열을 한 덩어리로 보여줘야 할까?

라는 질문으로.

이번 글은 그 질문을 붙잡고 regex를 하나씩 바꿔 본 기록이다. 결론부터 말하면, 가장 큰 변수는 형태소도, 영어 축약어도, 숫자 처리도 아니었다.

공백이었다.

Regex 실험실

실험 목표와 조건

이번 실험의 목표는 간단했다.

Hangul-onlyWhitespace-only 사이의 차이를 잘게 쪼개서, 무엇이 실제로 토큰 수를 줄이는지 확인하려 했다.

공정한 비교 실험을 위해 아래와 같이 설정했다.

  • 학습 데이터: 12GB 혼합 코퍼스. 바이트 기준 KO 85%, EN 8.5%, CODE 5.5%, MATH 1%
  • trainer: Hugging Face Rust BpeTrainer
  • vocab size: 36,000
  • 평가셋: 코퍼스에서 미리 추출한 5K줄
  • 바꾼 것: pre-tokenizer regex 하나

Regex 후보 목록

먼저 실험한 regex를 전부 보자.

Hangul-only: 한글만 묶기

[\p{Hangul}]+

가장 강하게 자르는 기준선이다. 한글 연속 구간만 pre-token으로 잡고, 영어, 숫자, 기호는 별도 조각으로 흘러간다.

예를 들어 GPT-4를 사용한다.를 보면 이렇게 갈라진다.

GPT | - | 4 | 를 | 사용한다 | .

이 방식은 4이 서로 만날 수 없다. BPE가 4를이라는 pair를 배울 기회가 처음부터 사라진다.

Hangul+Symbol: 한글과 기호를 묶기

[\p{Hangul}\p{P}\p{S}]+

Hangul-only에서 기호 경계만 풀었다. \p{P}는 punctuation, 즉 마침표, 쉼표, 하이픈 같은 문장부호다. \p{S}는 symbol, 즉 %, +, $ 같은 기호다.

한국어는 다., 요?, %를처럼 한글과 기호가 붙어 자주 나온다. Hangul+Symbol은 이런 결합이 가능하다.

Hangul+Number: 한글과 숫자를 묶기

[\p{Hangul}\p{N}]+

이번에는 숫자 경계만 풀었다. \p{N}은 number다.

2024년, 1월, 8잔 같은 단어 조합을 처리하기 위한 후보다. Hangul-only는 숫자와 한글을 가른다. Hangul+Number는 숫자와 한글이 붙은 단어를 허용한다.

Hangul+Number+Symbol: 한글, 숫자, 기호를 묶기

[\p{Hangul}\p{P}\p{S}\p{N}]+

Hangul+SymbolHangul+Number를 합친 설정이다. 한글과 기호, 숫자는 함께 볼 수 있지만 영어는 여전히 따로다.

처음에는 이 방식이 꽤 그럴듯해 보였다. 한국어 안에서 숫자와 기호를 결합하되, 영어와 한글은 나누기 때문이다. GPT-4를GPT-4를이 따로 움직이는 식이다.

Hangul+Latin: 한글과 영어를 묶기

[\p{Hangul}\p{Latin}]+

영어와 한글을 묶은 실험이다. \p{Latin}은 Latin script, 즉 일반적인 영문자를 말한다.

이 실험은 꽤 중요했다. “영어와 한국어를 같은 공간에 두면 좋아지는가”를 따로 확인하고 싶었다. 숫자와 기호는 분리하고, 영어 결합만 허용한다.

Whitespace-split: 공백만 경계로 쓰기

\S+

\S는 공백이 아닌 문자를 뜻한다. +는 하나 이상 반복이다. 그래서 \S+는 공백이 나오기 전까지의 문자열을 한 덩어리로 잡는다. 다만 공백은 별도다. 공백이 다음 토큰에 붙지 않는다. 이 점이 뒤에서 큰 문제로 돌아온다.

예를 들어 GPT-4를 사용한다.를 보면 이렇게 갈라진다.

GPT-4를 | 사용한다.

Whitespace-split + Script: 공백은 붙이고 script 분리는 유지

?[\p{Hangul}]+| ?[\p{Latin}]+| ?\p{N}+| ?[\p{P}\p{S}]+| ?\S

이 설정부터는 앞에 ?가 붙는다. 여기서 ?는 “공백이 있으면 뒤따르는 글자에 붙인다”는 뜻이다.

예를 들어 사용한다 앞에 공백이 있으면, 그 공백을 독립 토큰으로 두지 않고 + 사용한다처럼 다음 pre-token에 붙인다. ByteLevel BPE는 이런 앞 공백을 보통 Ġ 마커로 표현한다.

의도는 단순했다.

script 분리는 Hangul-only처럼 유지하되, 공백만 앞 단어에 붙이면 어떻게 될까?

Whitespace-split + Mixed: 한글+숫자와 영어 도메인 패턴 허용

?[\p{Hangul}\p{N}]+| ?[\p{Latin}\p{N}]+(?:[._+-][\p{Latin}\p{N}]+)*| ?[\p{P}\p{S}]+| ?\S

Whitespace-split + Script에서 한 걸음 더 나아갔다.

한글과 숫자는 함께 묶는다. 2024년, 1월, 90% 같은 표현을 살리기 위해서다. 영어 쪽은 scikit-learn, v1.2.3, A100 같은 도메인 패턴을 어느 정도 보호하려 했다.

이 regex 안의 (?:[._+-][\p{Latin}\p{N}]+)*는 점, 밑줄, 더하기, 하이픈 뒤에 영어 또는 숫자가 이어지는 패턴을 반복해서 받겠다는 뜻이다.

복잡해 보이지만 목적은 하나다. “앞 공백은 붙이되, 영어 코드/도메인 표현은 너무 잘게 깨지지 않게 하자.”

Whitespace-split + KoMixed: 한글+숫자+기호 중심으로 묶기

?[\p{Hangul}\p{P}\p{S}\p{N}]+| ?\p{Latin}+| ?\S+

Hangul+Number+Symbol은 한글, 기호, 숫자를 묶고 영어는 분리했다. 이 설정은 여기에 앞 공백 결합을 붙였다.

Whitespace-split + Domain: Latin 도메인 패턴 보호

?[\p{Hangul}\p{N}\p{P}\p{S}]+| ?[\p{Latin}\p{N}]+(?:[._+-][\p{Latin}\p{N}]+)*| ?\S+

Whitespace-split + KoMixed에 영어 도메인 패턴 보호를 추가했다.

GPT-4, v1.2.3, foo_bar, scikit-learn 같은 표현을 생각한 설정이다. 한국어 쪽은 한글, 숫자, 기호를 넓게 묶고, Latin 쪽은 영문자와 숫자, 그리고 특정 연결 기호를 허용한다.

Whitespace-only보다 조금 더 “조심스러운” regex다. 모든 비공백을 한 덩어리로 묶지는 않고, 영어 식별자 경계를 어느 정도 보존한다.

Whitespace-split + NumDomain: 숫자로 시작하는 Latin 패턴까지 보호

?\p{N}+\p{Latin}[\p{Latin}\p{N}]*(?:[._+-][\p{Latin}\p{N}]+)*| ?[\p{Latin}][\p{Latin}\p{N}]*(?:[._+-][\p{Latin}\p{N}]+)*| ?[\p{Hangul}\p{N}\p{P}\p{S}]+| ?\S+

Whitespace-split + Domain에서 하나 더 욕심을 냈다.

3D, 4K, 5G처럼 숫자로 시작하고 Latin 문자가 이어지는 표현을 따로 보호하려 했다. 실제 텍스트에는 이런 표현이 많다. 특히 기술 문서는 숫자와 영문이 섞인 짧은 표현이 자주 나온다.

이 regex는 꽤 복잡하다. 복잡한 만큼 좋아질까? 그게 이 실험의 질문이다.

Whitespace-only: 공백만 경계로 쓰고, 공백은 흡수

?\S+

가장 단순한 설정이다.

경계는 사실상 공백뿐이다. 공백이 있으면 그 뒤 문자열에 공백을 붙인 뒤 전체를 하나의 pre-token으로 묶는다. GPT-4를도, 2024년도, foo.bar()도 한 덩어리다.

처음 보면 너무 단순해 보인다. 한국어도, 영어도, 숫자도, 기호도 따로 나누지 않는다. 대신 BPE에게 가장 넓은 후보 공간을 준다.

이번 실험의 핵심이다. 똑똑하게 자르는 regex와 거의 자르지 않는 regex 중 어느 쪽이 실제로 더 나은가.

결과: 공백이 가장 큰 변수였다

Regex 공정 비교

결과는 선명했다.

이름로그명핵심 설계Fertility↓공백 단독%
Hangul-onlyR1한글만 묶음3.978223.7%
Hangul+SymbolR2한글+기호3.937624.0%
Hangul+NumberR3한글+숫자3.953623.9%
Hangul+Number+SymbolR4한글+숫자+기호3.890124.3%
Hangul+LatinR_new한글+영어3.978923.7%
Whitespace-splitR5\S+3.877524.3%
Whitespace-split + ScriptR6앞 공백 결합 + script 분리3.32580.3%
Whitespace-split + MixedR7앞 공백 결합 + 혼합어 일부 허용3.31030.3%
Whitespace-split + DomainR4_Vs+L앞 공백 결합 + Latin 도메인 보호3.25690.4%
Whitespace-split + NumDomainR4_Vs+L2Domain + 숫자 시작 Latin 보호3.25680.4%
Whitespace-onlyM_Vs ?\S+3.25200.4%
  • Fertility 값은 낮을수록 좋다.
  • 공백 단독%는 공백이 별도 토큰으로 남은 비율이다. ByteLevel BPE의 단독 Ġ 토큰을 기준으로 계산했으며, Ġ오늘처럼 앞 공백이 내용 토큰에 붙은 경우는 여기에 포함하지 않는다.

Hangul-only부터 Whitespace-split까지는 script 경계를 얼마나 풀어 주느냐의 실험이다. Hangul-only에서 Whitespace-split으로 가면 Fertility가 3.9782에서 3.8775로 낮아진다. 개선은 있다. 대략 2.53%다.

그런데 Hangul-only에서 Whitespace-split + Script로 가면 3.3258까지 떨어진다. script 분리는 그대로 두고 공백만 붙였는데 16.4%가 움직였다.

여기서 분위기가 바뀐다.

한글과 숫자를 묶을까, 기호를 묶을까, 영어를 묶을까. 이런 질문도 의미는 있다. 하지만 가장 큰 레버는 아니었다. 가장 큰 레버는 공백을 독립 토큰으로 둘지, 다음 단어에 붙일지였다.

비교 체인으로 보면 더 잘 보인다.

비교바뀐 것Fertility 변화해석
Hangul-only → Hangul+Symbol기호 결합 허용-1.02%다., %를 같은 표현이 조금 살아난다
Hangul-only → Hangul+Number숫자 결합 허용-0.62%2024년 같은 표현이 조금 살아난다
Hangul-only → Hangul+Number+Symbol기호+숫자 결합 허용-2.21%둘을 같이 풀면 소폭 개선
Hangul-only → Hangul+Latin영어만 결합 허용+0.02%영어만 풀어서는 거의 의미가 없다
Hangul-only → Whitespace-split비공백 전체 허용-2.53%script 경계 해제 효과의 총합
Hangul-only → Whitespace-split + Script앞 공백 결합만 추가-16.4%가장 큰 단일 변화
Hangul-only → Whitespace-only앞 공백 결합 + 전체 허용-18.3%이 실험의 최상위

여기서 Whitespace-split + Script는 공백을 이미 다음 pre-token에 붙인다. 다만 한글, Latin, 숫자, 기호는 여전히 나눈다. 예를 들어 GPT-4를 같은 표현 안에서도 GPT, -, 4, 사이에 경계가 남는다. 반면 Whitespace-only는 공백만 기준으로 삼기 때문에 GPT-4를 전체를 같은 pre-token 안에 둔다. 따라서 Whitespace-split + ScriptWhitespace-only의 차이 -2.22%는 공백 효과가 아니라, 문자 종류별 경계를 없애서 혼합 표현을 더 자유롭게 merge하게 만든 효과다.

regex를 복잡하게 만들어도 대부분 작은 차이에 머물렀다. Whitespace-split + DomainWhitespace-split + NumDomain은 사실상 같았다. 숫자로 시작하는 Latin 패턴을 따로 보호해도 추가 이득은 0에 가까웠다. 반면 Whitespace-only는 가장 단순한데 큰 변화를 만들었다.

이때부터 좋은 regex의 기준을 다시 고민했다.

좋은 regex란 인위적 규칙으로 많이 쪼개는 것이 아니라, BPE가 자유롭게 merge할 수 있는 공간을 제공하는 regex가 아닐까?

왜 Whitespace-only가 강했나

Whitespace-only가 하는 일은 두 가지다.

첫째, pre-tokenizer가 공백 전까지의 문자열을 하나의 pre-token으로 묶는다. 그래서 GPT-4를, 2024년, 90%를, 사용했다. 같은 표현 안에서 BPE가 자유롭게 pair를 볼 수 있다.

둘째, 앞 공백을 다음 pre-token에 붙인다.

ByteLevel BPE는 공백을 그냥 없애지 않는다. 보통 공백은 Ġ 같은 마커로 남는다. 공백을 독립적으로 두면 문장 안의 단어마다 공백 토큰이 따로 끼어든다.

예를 들어 다음 문장을 보자.

나는 오늘 GPT-4를 사용했다.

공백을 따로 두면 개념적으로 이런 구조가 된다.

나는 | Ġ | 오늘 | Ġ | GPT-4를 | Ġ | 사용했다.

공백이 세 번 등장한다. 짧은 문장은 별것 아닌 것처럼 보이지만, 문서 단위로 가면 공백 토큰은 계속 쌓인다.

Whitespace-only는 이 공백을 뒤 단어에 붙인다.

나는 | Ġ오늘 | ĠGPT-4를 | Ġ사용했다.

토큰 하나가 줄었다고 끝나는 문제가 아니다. BPE가 Ġ오늘, Ġ사용했다 같은 “문장 중간에 자주 나오는 단어 형태”를 직접 배울 수 있다. 공백과 단어가 늘 같이 나오는 패턴이라면, 둘을 분리해서 매번 비용을 내는 것보다 함께 묶는 편이 TPW에 유리하다.

이 점 때문에 Whitespace-splitWhitespace-only는 완전히 다르다.

둘 다 공백 전까지를 한 덩어리로 처리하지만, 하나는 공백을 밖에 남겨 두고 하나는 공백을 안으로 붙인다. 실험 전에는 이 차이를 그리 크게 보지 않았다. 결과를 보고 나서야 공백이 정말 비싼 문자라는 걸 알았다.

외부 regex와 코퍼스 비율 점검

이제 질문을 바꿨다.

직접 만든 regex끼리만 비교하면 우물 안 실험이 될 수 있다. 실제 LLM 계열 regex와 비교해야 했다.

비교한 대상은 다음과 같다.

Whitespace-only

?\S+

이번 실험의 가장 단순한 기준이다.

GPT-2 계열

's|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+

영어 축약어를 따로 잡고, 문자, 숫자, 기호, 공백을 나눈다. GPT-2류 BPE에서 익숙한 구조다. 영어에는 꽤 자연스럽지만, 한국어의 GPT-4를, 2024년, 90%를 같은 혼합 표현에는 경계를 많이 만든다.

LLaMA 3 / cl100k 계열

(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+

GPT-2보다 더 세밀하다. 숫자를 1~3자리로 자르고, 문자 앞에 붙은 기호 하나를 함께 잡는 branch가 있다. 범용 LLM에는 합리적인 선택일 수 있다. 다만 한국어 압축률만 놓고 보면 경계가 많다.

GPT-4o / o200k 계열

[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?i:'s|'t|'re|'ve|'m|'ll|'d)?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?i:'s|'t|'re|'ve|'m|'ll|'d)?|\p{N}{1,3}| ?[^\s\p{L}\p{N}]+[\r\n/]*|\s*[\r\n]+|\s+(?!\S)|\s+

o200k 계열은 Unicode case 카테고리를 더 명시적으로 나눈다. 대문자, 소문자, modifier, mark 등을 구분한다. 영어와 Latin script는 더 섬세하지만, 한글 처리 관점에선 cl100k와 큰 구조가 비슷하다.

Qwen3 계열

(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\r\n\p{L}\p{N}]?\p{L}+|\p{N}| ?[^\s\p{L}\p{N}]+[\r\n]*|\s*[\r\n]+|\s+(?!\S)|\s+

cl100k와 비슷하지만 숫자를 1자리씩 자르는 쪽에 가깝다.

결과는 흥미로웠다.

모델 regex핵심 차이tokens/char↓Whitespace-only 대비
Whitespace-only ?\S+0.4826기준
GPT-4 / LLaMA3cl100k 계열, 숫자 1~3자리0.5032+4.3%
Qwen3숫자 1자리 분리0.5274+9.3%

Qwen3의 숫자 분리는 손실이 컸다. 2024년 같은 표현이 2, 0, 2, 4, 쪽으로 쪼개지면 BPE가 2024년이나 4년을 배울 기회가 줄어든다. cl100k 계열은 문자·숫자·기호 branch를 더 세밀하게 나누면서 약간의 손실이 발생했다.

영어 비율 실험

한국어만 잘하는 토크나이저를 만들고 싶지는 않았다.

로컬 한국어 LLM이라도 영어, 코드, 수식은 필요하다. 실제 학습 데이터도 한국어만으로 채우기 어렵다. 그래서 코퍼스 비율을 바꿔 가며 확인했다.

아래 코퍼스들은 모두 같은 총량으로 맞추고, 언어와 도메인 비율만 바꿨다.

IDKOENCODEMATH의도
E0580%5%10%5%한국어 중심
E1075%10%10%5%한국어 중심, 영어 소량
E2065%20%10%5%균형 쪽으로 이동
E3055%30%10%5%실제 LM 후보에 가까운 혼합
E5035%50%10%5%영어 중심, 한국어 추가
E7510%75%10%5%영어 중심

먼저 한국어 TPW다.

TokenizerE05E10E20E30E50E75
Whitespace-only1.4771.4911.5151.5421.6041.788
GPT-21.6381.6501.6731.6911.7381.885
LLaMA31.6741.6861.7081.7281.7751.928
GPT4o1.6731.6841.7071.7261.7741.926

영어 비율이 높아질수록 한국어 TPW는 조금씩 나빠진다. 당연하다. vocab 슬롯을 영어가 더 많이 가져가기 때문이다. 그래도 Whitespace-only는 모든 비율에서 가장 낮았다.

영어 TPW도 봤다.

TokenizerE05E10E20E30E50E75
Whitespace-only1.3371.2731.2151.1841.1511.120
GPT-21.3681.3151.2721.2511.2261.204
LLaMA31.3811.3271.2841.2621.2371.213
GPT4o1.3771.3231.2791.2571.2311.207

영어에서도 Whitespace-only가 이겼다. 이건 조금 의외였다. 영어 축약어와 숫자 처리를 더 세밀하게 다루는 GPT 계열 regex가 영어에서 유리할 거라 생각했기 때문이다.

하지만 TPW만 보면 단순한 regex가 여전히 강했다. 이유는 비슷하다. Whitespace-only는 불필요한 사전 경계를 만들지 않는다. BPE가 말뭉치에서 자주 나오는 조합을 직접 고르게 둔다.

물론 이 결과를 “모든 언어에서 항상 Whitespace-only가 최고”라고 일반화하면 안 된다. 내가 검증한 범위 안에서, 같은 코퍼스와 같은 vocab size 조건에서 Whitespace-only가 강했다는 뜻이다.

다국어 7개 영역 평가

한영 비율 실험만으로는 부족했다. 그래서 한국어, 영어, 일본어, 중국어, 독일어, 아랍어, code를 같은 비중으로 둔 다국어 평가도 했다.

TokenizerkoenjazhdearcodeAVG
Whitespace-only1.9201.3550.4980.6211.5871.5702.3631.416
LLaMA32.0351.4140.5200.6321.6441.5992.5901.491
GPT4o2.0351.4080.5200.6321.6441.5922.6071.491
GPT-22.0051.4170.5250.6441.6591.5982.7241.510

Whitespace-only는 7개 영역 모두 가장 낮았다. 흥미로운 점은 공백을 사용하지 않는 일본어와 중국어도 Whitespace-only가 유리했다는 점이다. code도 이겼다. 기호를 세밀하게 자르는 regex가 code에 유리할 거라 생각했는데, 실제 코드 평가에선 Whitespace-only가 더 짧았다.

상용·공개 모델 토크나이저와 비교하기

여기서부터는 기대감이 커지기 시작했다.

내부 실험에서만 좋다면 그냥 실험실 안의 숫자다. 그런데 A.X-K1, HyperCLOVA X, EXAONE, LLaMA 3, Qwen 3 같은 실제 모델 토크나이저와 비교해도 앞서기 시작했다.

이 이름들이 낯설 수 있으니, 비교 대상부터 짚고 가자.

비교 대상

토크나이저만든 곳왜 비교가 중요한가
A.X-K1SKT519B MoE급 한국어 강점 모델의 토크나이저다. 공개 모델 카드도 다국어·코드 효율을 겨냥한 대형 BBPE tokenizer를 강조한다.
HyperCLOVA XNAVER한국어와 한국 문화권 사용성을 강하게 의식한 국내 대표 LLM 계열이다. 128K급 vocab을 쓴다.
EXAONE-3.5LG AI Research영어·한국어 bilingual 모델 시리즈다. 102K vocab으로 우리 102K 토크나이저와 직접 비교하기 좋다.
Solar-Open-100BUpstage100B급 공개 MoE 모델이다. 197K vocab이라 단순히 단어장이 작아서 진다는 변명을 하기 어렵다.
LLaMA 3, Gemma 3, Qwen 3Meta, Google, Alibaba전 세계적으로 널리 쓰이는 범용 LLM 토크나이저다. 한국어 특화는 아니지만 vocab 규모가 크다.

이들과 비교한다는 건 단순한 baseline 비교가 아니다. 상대는 실제 대형 모델을 운영하기 위해 만든 토크나이저다. vocab도 작지 않다. A.X-K1은 164K, HyperCLOVA X는 128K, EXAONE-3.5는 102K, Solar-Open-100B는 197K 규모다. 그런데 우리가 만든 Whitespace-only 64K가 A.X-K1, EXAONE-3.5보다 한국어 TPW가 낮다면 이야기가 달라진다.

내부 검증과 한국어 벤치마크 TPW

먼저 내부 validation data를 봤다. 여기서 쓴 검증셋은 eval_v2.jsonl 5K줄이고, MIXED_12G의 바이트 비율을 반영해 KO 78%, EN 12%, CODE 10%로 맞췄다. 실제 학습 데이터의 도메인 비율에 최대한 가깝게 만든 셋이다.

RankTokenizerVocabFertility↓
1Whitespace-only 128K128K2.7324
2Whitespace-only 102K102K2.8255
3Whitespace-only 64K64K3.0060
4A.X-K1145K3.0126
5HyperCLOVA X SEED 1.5B110K3.0489
6EXAONE-3.5102K3.1996
7Whitespace-only 36K36K3.2509
8LLaMA-2-Ko46K3.3561
9EEVE-Korean58K3.3576
10LLaMA 3 / Kanana128K3.3857
11Gemma 3262K3.4834
12Polyglot-Ko30K3.5708
13Qwen 3151K3.8438

실험 결과 Whitespace-only 128K, 102K, 64K가 모두 대규모 공개 모델 토크나이저보다 앞에 있었다.

하지만 이 결과만으로는 부족하다. 내부 검증셋은 아무리 조심해서 만들었어도 내가 만든 코퍼스 비율과 선택 방식의 영향을 받는다. 그래서 외부 한국어 벤치마크 문장에서도 같은 방향이 나오는지 확인했다. 평가셋은 KoBEST HellaSwag, COPA, BoolQ와 KLUE-NLI에서 각각 500개씩 뽑았다.

RankTokenizerVocabHellaCOPABoolQNLIAVG↓
1Whitespace-only 128K128K1.3381.3121.5061.4151.393
2Whitespace-only 102K102K1.3761.3541.5531.4521.434
3Whitespace-only 64K64K1.4731.4461.6591.5451.531
4Whitespace-only 36K36K1.6201.5911.8021.6881.675
5A.X-K1164K1.5101.6251.8601.8761.718
6Solar-Open-100B197K1.7171.8221.9331.9381.853
7HyperCLOVA X 32B128K1.9192.0322.2542.2772.120
8LLaMA-2-Ko46K2.0592.1772.3732.2882.224
9Polyglot-Ko30K2.1002.3792.3432.2522.268
10HyperCLOVA X SEED 1.5B110K2.1872.2542.3692.3162.281
11EXAONE-3.5102K2.0612.3252.3912.4302.302
12VAETKI137K2.2032.3652.4732.4962.385
13EEVE-Korean41K2.2372.4082.5332.5382.429
14Gemma 3262K2.4752.6832.7082.7422.652
15LLaMA 3128K2.7412.8782.8902.8942.851
16Kanana-Nano 2.1B128K2.7412.8782.8902.9442.863
17Qwen 3152K3.1043.1693.2483.2283.188

외부 벤치마크에서도 방향은 크게 달라지지 않았다. Whitespace-only 64K가 A.X-K1보다 vocab이 훨씬 작은데도 평균 TPW가 낮았다. Whitespace-only 128K는 LLaMA 3와 같은 128K vocab인데 평균 TPW가 1.393 대 2.851이었다. 한국어 벤치마크 문장만 놓고 보면 거의 두 배 가까운 차이다.

그래서 잠깐 이런 생각까지 했다.

혹시 한국어 TPW 기준 SOTA 토크나이저를 만든 건가?

적어도 “한국어를 더 짧게 표현하는 토크나이저”라는 목표만 놓고 보면 꽤 멀리 온 것 같았다.

실제로 토크나이저에 관한 논문이나 모델 소개 자료를 찾아보면 TPW, fertility, compression ratio처럼 토큰 수 기반 지표를 중요한 성능으로 제시하는 경우가 많다. 따라서 나도 이 실험 결과를 강하게 주장할 수도 있다.

하지만 냉정히 생각해보면 의심의 여지가 많다. ?\S+는 지극히 간단한 형태다. 만약 이를 사용한 토크나이저가 아무런 단점도 없이 최고 성능을 달성할 수 있다면, 지금까지 왜 아무도 사용하지 않았을까. 내가 생각지 못한 변수가 있는 건 아닐까?

이 질문에 답하려면 토크나이저 학습에서 멈추면 안 된다. 실제 언어 모델 학습까지 진행해야 한다.

그래서 이 글은 일부러 결론의 범위를 좁힌다.

TPW 기준으로는 Whitespace-only 계열이 강했다.

여기까지다.

2편 정리

이번 실험에서 확인한 것은 세 가지다.

첫째, regex는 BPE 앞단의 사소한 전처리가 아니었다. BPE가 볼 수 있는 pair 후보 자체를 정했다. pre-tokenizer가 경계를 만들면, 그 경계 너머 pair는 아무리 자주 나와도 merge 후보가 되지 않는다.

둘째, script 경계를 푸는 효과는 있었지만 가장 큰 변수는 아니었다. 한글과 기호를 묶고, 숫자를 묶고, 영어를 묶는 변화는 대체로 몇 퍼센트 안에서 움직였다. 반면 앞 공백 결합은 단독으로 16% 넘게 움직였다.

셋째, 상용·공개 모델 토크나이저와 비교해도 TPW 숫자는 강했다. 실제 도메인 비율을 반영한 내부 validation data에서 먼저 앞섰고, 외부 한국어 벤치마크에서도 같은 방향이 나왔다. 그래서 잠깐은 SOTA 토크나이저를 만든 것 같았다.

2편의 결론은 이렇게 정리할 수 있다.

한국어 토크나이저 TPW를 줄이는 데 가장 중요한 변수는 형태소 경계가 아니라 공백을 어떻게 다루느냐였다.

다만 이 결론에는 단서가 붙는다.

TPW 기준으로는.

하지만 언어 모델은 토큰 수만 보고 배우는 존재가 아니다. 긴 토큰을 잘 배워야 하고, 희귀 토큰 embedding도 충분히 업데이트해야 한다. 공백을 어떻게 표현하느냐도 단순한 비용 문제가 아니라, 단어 경계 신호와 loss 해석까지 건드린다.

그래서 다음 실험은 직접 LM을 학습하는 쪽으로 넘어갔다. 이 이야기는 3편과 4편의 중심이 된다.

다음 실험에서 확인하고 싶은 질문은 명확했다. TPW 우위가 LM 학습 성능으로도 이어질까?