Zipf 법칙은 말뭉치에서 낱말을 많이 나온 순으로 세우면 빈도가 순위에 반비례한다는 관찰입니다. 순위와 빈도에 둘 다 로그를 씌우면 기울기 인 직선이 됩니다. 토크나이저가 자른 조각에도 같은 말이 흔히 붙습니다. 「서브워드도 Zipf를 따르므로 드문 조각이 길게 늘어선다」는 식입니다.
이 글은 그 말을 숫자로 확인합니다. 토크나이저 다섯 개로 한국어와 영어 위키백과 문단을 6MB씩 자르고, 순위-빈도 곡선의 기울기를 전 구간과 자릿수 구간으로 나눠 쟀습니다. 그다음 같은 말뭉치에서 BPE를 어휘 500부터 64,000까지 직접 학습시켜, 어휘를 키우면 분포가 어떻게 기우는지 봤습니다.
결과는 셋입니다. 순위 101,000의 가운데 두 자릿수는 기울기 −0.80−1.00으로 Zipf에 가까웠습니다. 꼬리까지 넣은 기울기는 −1.33~−2.27로 흩어졌고, 같은 토크나이저에서 말뭉치만 8배로 키워도 −1.10에서 −1.39로 움직였습니다. 어휘를 키울수록 분포가 한쪽으로 쏠린다는 논문의 관찰은 그대로 재현됐습니다.
BPE가 조각을 어떻게 만드는지는 서브워드 토크나이저가 맡고, 같은 글이 토크나이저마다 토큰 몇 개가 되는지는 한국어 토큰세가 맡습니다. 이 글은 분포의 모양만 다룹니다.
재현하려는 주장
논문의 두 문장
Gowda와 May의 「Finding the Optimal Vocabulary Size for Neural Machine Translation」(arXiv 2004.02334, Findings of EMNLP 2020)을 골랐습니다. 번역 모델의 출력층을 「조각 하나를 고르는 분류기」로 보고, BPE 어휘 크기가 그 분류기의 클래스 불균형을 어떻게 바꾸는지 따진 논문입니다. 이 글이 다시 재는 문장은 둘입니다.
첫째는 서론의 전제입니다. 「자연어 말뭉치에서 한 종류의 순위는 대략 그 빈도에 반비례한다」고 적고, BPE로 쪼갠 뒤에도 그 불균형이 남는다고 결론에서 말합니다. 순위-빈도 로그 기울기로 옮기면 근처라는 뜻입니다.
둘째는 어휘 크기에 대한 관찰입니다. 논문은 불균형을 두 값으로 잽니다. 불균형 D는 관측한 분포가 균등분포에서 얼마나 떨어졌는지를 0~1로 적은 값이고, F95% 값은 많이 나온 순으로 95% 안에 드는 종류 가운데 가장 드문 것의 빈도입니다. 어휘를 키우면 문장이 짧아지는 대신 D가 오르고 F95%가 내려간다는 것, 그리고 「문장 길이가 짧으면서 F95%가 100 근처 이상인 어휘」가 최적에 가깝다는 규칙이 논문의 관찰입니다.
여기서 는 종류 수, 는 종류 가 전체 토큰에서 차지하는 비율입니다.
판정 기준
계획은 「기울기 값과 결정계수, 그리고 직선이 성립하는 순위 구간」으로 판정하고 꼬리를 잘라 내지 말라고 정했습니다. 꼬리를 자르면 어떤 데이터든 직선처럼 보이기 때문입니다. 그래서 같은 분포에서 넷을 함께 냅니다.
- 전 구간 기울기와 — 나온 종류 전부를 점 하나씩으로 놓고 그은 직선입니다.
- 자릿수 구간별 기울기 — 순위 1
9, 1099, 100999, 1,0009,999, 10,000 이상에서 따로 그은 직선입니다. - 가운데 기울기 — 순위 10~1,000에서 그은 직선입니다.
- 직선 구간 — 가운데 직선에서 로그 0.1(1.26배) 넘게 벗어나지 않는 순위의 범위입니다. 아래쪽은 1~9위 가운데 벗어난 마지막 순위 다음부터, 위쪽은 1,000위 뒤에서 처음 벗어나는 순위 직전까지입니다.
전 구간 직선에 경계해야 할 점이 하나 있습니다. 종류 2만 개를 점으로 찍으면 그 95%가 1,000위 뒤에 있습니다. 한 번만 나온 종류가 끝에서 수평 계단을 이루므로, 전 구간 기울기는 사실상 꼬리의 모양이 정합니다.
측정
말뭉치와 토크나이저
말뭉치는 실험대 글들이 써 온 두 질의응답 데이터의 지문입니다. KorQuAD 1.0 학습 분할과 SQuAD 1.1 학습 분할 모두 위키백과 문단이라 갈래가 같습니다. 대역이 아니므로 내용이 같지는 않습니다.
크기는 UTF-8 바이트로 맞췄습니다. 한글 한 자가 3바이트이고 영어 한 자가 1바이트라, 같은 6MB면 한국어 255만 자와 영어 599만 자가 됩니다. 한국어 한 자가 영어 두세 자의 정보를 싣는다는 어림과 대체로 맞는 비율입니다. 문장 수는 마침표·물음표·느낌표 뒤 공백으로 어림해 34,159개와 40,189개였습니다. 논문이 「작은 데이터」로 부른 3만 문장 규모입니다.
토크나이저는 한국어 토큰세에서 토큰 없이 받히는 것이 확인된 목록에서 다섯을 골랐습니다. 한국어 전용 둘(klue/roberta-base 32K, A.X 4.0 Light 102K)과 다국어 셋(mBERT 119K, Qwen2.5 151K, multilingual-e5-small 250K)입니다.
재현 블록
pip install transformers datasets numpy
import time, re, numpy as np
from collections import Counter
from datasets import load_dataset
from transformers import AutoTokenizer
t0 = time.perf_counter()
def corpus(rid, limit=6_000_000): # 위키백과 문단을 UTF-8 6MB까지 (한국어 약 255만 자, 영어 약 599만 자)
out, b = [], 0
for c in dict.fromkeys(load_dataset(rid, split="train")["context"]):
if b >= limit: break
out.append(c); b += len(c.encode())
return out
C = {"ko": corpus("KorQuAD/squad_kor_v1"), "en": corpus("rajpurkar/squad")}
SENT = re.compile(r"[.!?](?:\s|$)")
for L, t in C.items():
print(f"{L}: 문단 {len(t):,} 글자 {sum(map(len, t)):,} 문장(어림) {sum(len(SENT.findall(x)) for x in t):,}")
def zipf(cnt): # 순위-빈도 로그-로그. 꼬리를 자르지 않고 전 구간에 한 직선, 그리고 자릿수 구간마다 기울기
f = np.sort(np.fromiter(cnt.values(), float))[::-1]; r = np.arange(1, len(f) + 1)
x, y = np.log10(r), np.log10(f)
a, b = np.polyfit(x, y, 1); r2 = 1 - ((y - a * x - b) ** 2).sum() / ((y - y.mean()) ** 2).sum()
band = [np.polyfit(x[m], y[m], 1)[0] if (m := (r >= lo) & (r < lo * 10)).sum() > 5 else np.nan for lo in (1, 10, 100, 1000, 10000)]
m = (r >= 10) & (r <= 1000); am, bm = np.polyfit(x[m], y[m], 1)
bad = np.abs(y - am * x - bm) > 0.1 # 가운데 직선에서 1.26배 넘게 벗어난 순위
lo = r[:9][bad[:9]].max() + 1 if bad[:9].any() else 1
hi = r[1000:][bad[1000:]].min() - 1 if bad[1000:].any() else r[-1]
return len(f), a, r2, band, am, lo, hi, (f == 1).mean()
TOKS = ["klue/roberta-base", "google-bert/bert-base-multilingual-cased", "skt/A.X-4.0-Light",
"Qwen/Qwen2.5-7B-Instruct", "intfloat/multilingual-e5-small"]
print(f"{'토크나이저':<14}{'언어':>3}{'어휘':>8}{'토큰 수':>10}{'쓰인 종류':>8}{'전 구간 기울기':>10}{'R²':>7}"
f" {'자릿수 구간별 기울기 1·10·100·1K·10K~':<32}{'10~1K':>7}{'직선 구간':>14}{'한 번만':>7}")
for name in TOKS:
tk = AutoTokenizer.from_pretrained(name); tk.model_max_length = 10**9
for L, t in C.items():
cnt = Counter(i for ids in tk(t, add_special_tokens=False)["input_ids"] for i in ids)
V, a, r2, band, am, lo, hi, h1 = zipf(cnt)
bs = " ".join(f"{v:6.2f}" if v == v else " -" for v in band)
print(f"{name.split('/')[1][:14]:<14}{L:>4}{len(tk):>9,}{sum(cnt.values()):>11,}{V:>9,}{a:>10.3f}{r2:>9.3f}"
f" {bs:<34}{am:>7.3f}{f'{lo}~{hi:,}':>14}{h1:>8.1%}")
# 같은 토크나이저, 말뭉치만 줄이면: 전 구간 기울기와 직선 구간이 표본 크기를 따라 움직이는가
tk = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct"); tk.model_max_length = 10**9
for L, t in C.items():
for share in (0.125, 0.25, 0.5, 1.0):
sub = t[: int(len(t) * share)]
cnt = Counter(i for ids in tk(sub, add_special_tokens=False)["input_ids"] for i in ids)
V, a, r2, band, am, lo, hi, h1 = zipf(cnt)
print(f"Qwen {L} 문단 {len(sub):>5,} 토큰 {sum(cnt.values()):>9,} 종류 {V:>6,} 전 구간 {a:.3f} (R² {r2:.3f}) 10~1K {am:.3f} 직선 {lo}~{hi:,} 한 번만 {h1:.1%}")
print(f"elapsed={time.perf_counter() - t0:.0f}s")
python3 zipf.py
실제 출력
진행 표시줄과 허브 경고는 뺐습니다.
ko: 문단 4,987 글자 2,551,344 문장(어림) 34,159
en: 문단 8,893 글자 5,988,545 문장(어림) 40,189
토크나이저 언어 어휘 토큰 수 쓰인 종류 전 구간 기울기 R² 자릿수 구간별 기울기 1·10·100·1K·10K~ 10~1K 직선 구간 한 번만
roberta-base ko 32,000 1,343,726 25,038 -1.468 0.926 -0.26 -1.15 -0.91 -1.21 -2.85 -0.967 8~6,097 13.8%
roberta-base en 32,000 2,357,713 2,338 -2.265 0.779 -0.51 -0.61 -1.44 -6.35 - -1.160 10~1,000 5.9%
bert-base-mult ko 119,547 1,536,811 13,410 -2.031 0.945 -0.56 -0.61 -1.00 -2.68 0.00 -0.862 10~1,128 37.5%
bert-base-mult en 119,547 1,309,022 27,194 -1.488 0.890 -0.72 -1.18 -0.76 -1.07 -3.03 -0.855 10~8,044 17.9%
A.X-4.0-Light ko 102,400 1,128,630 49,376 -1.334 0.938 -0.80 -0.99 -0.79 -0.98 -1.94 -0.802 7~5,848 23.1%
A.X-4.0-Light en 102,400 1,397,061 25,305 -1.462 0.898 -0.60 -1.18 -0.82 -1.14 -3.11 -0.901 10~7,057 13.6%
Qwen2.5-7B-Ins ko 151,665 1,851,479 12,597 -2.014 0.952 -0.33 -0.68 -1.19 -2.34 0.00 -0.996 10~1,000 42.7%
Qwen2.5-7B-Ins en 151,665 1,307,558 38,952 -1.393 0.941 -0.60 -1.25 -0.79 -1.09 -2.12 -0.912 10~8,828 22.5%
multilingual-e ko 250,002 1,446,360 16,130 -1.948 0.929 -0.67 -0.80 -0.88 -2.49 -1.13 -0.811 4~1,390 32.6%
multilingual-e en 250,002 1,408,748 26,224 -1.576 0.915 -0.59 -1.08 -0.76 -1.29 -2.85 -0.850 10~4,632 20.3%
Qwen ko 문단 623 토큰 232,772 종류 4,183 전 구간 -1.872 (R² 0.911) 10~1K -1.015 직선 10~1,000 한 번만 36.3%
Qwen ko 문단 1,246 토큰 473,171 종류 6,219 전 구간 -1.961 (R² 0.933) 10~1K -1.011 직선 10~1,000 한 번만 39.6%
Qwen ko 문단 2,493 토큰 922,761 종류 8,615 전 구간 -2.026 (R² 0.941) 10~1K -0.993 직선 10~1,000 한 번만 42.8%
Qwen ko 문단 4,987 토큰 1,851,479 종류 12,597 전 구간 -2.014 (R² 0.952) 10~1K -0.996 직선 10~1,000 한 번만 42.7%
Qwen en 문단 1,111 토큰 180,943 종류 17,904 전 구간 -1.097 (R² 0.973) 10~1K -0.922 직선 10~5,596 한 번만 41.5%
Qwen en 문단 2,223 토큰 315,020 종류 23,388 전 구간 -1.183 (R² 0.967) 10~1K -0.905 직선 10~6,206 한 번만 36.2%
Qwen en 문단 4,446 토큰 579,711 종류 29,902 전 구간 -1.269 (R² 0.960) 10~1K -0.903 직선 10~6,992 한 번만 30.0%
Qwen en 문단 8,893 토큰 1,307,558 종류 38,952 전 구간 -1.393 (R² 0.941) 10~1K -0.912 직선 10~8,828 한 번만 22.5%
elapsed=29s
기울기 판정
전 구간 직선
꼬리까지 넣은 기울기는 열 줄에서 −1.334부터 −2.265까지 흩어졌고, 는 0.779~0.952였습니다. 「Zipf의 기울기는 」을 이 숫자로 확인하려 하면 어느 줄도 맞지 않습니다.
이 기울기가 분포의 성질이 아니라는 것은 출력 아래쪽 여덟 줄이 보여 줍니다. Qwen2.5 하나로 영어 문단을 1,111개에서 8,893개로 8배 늘리자 전 구간 기울기가 −1.097에서 −1.393으로 내려갔습니다. 같은 토크나이저, 같은 갈래의 글인데 표본 크기만으로 0.3이 움직였습니다. 한국어도 −1.872에서 −2.014로 갔습니다.
원인은 꼬리의 계단입니다. 말뭉치가 커지면 한 번만 나온 종류가 늘어나며 곡선이 오른쪽으로 길어지고, 길어진 꼬리는 전 구간 직선을 더 가파르게 끌어내립니다. 말뭉치 크기를 밝히지 않은 「기울기 −1.4」는 비교할 수 있는 숫자가 아닙니다.
가운데 두 자릿수
순위 10~1,000에서 그은 기울기는 사정이 다릅니다. 같은 8배 증가에서 Qwen2.5 영어는 −0.922·−0.905·−0.903·−0.912로 0.02 안에 머물렀고, 한국어는 −1.015에서 −0.996이었습니다. 표본을 키워도 흔들리지 않으니 이것이 분포의 성질에 가까운 값입니다.
그 값은 한국어 −0.802~−0.996, 영어 −0.850~−0.912입니다. 영어를 글자 단위로밖에 못 자르는 klue/roberta-base의 영어 줄(−1.160)만 빼면 열 줄 중 아홉이 보다 조금 평평한 쪽에 섰습니다. 논문의 「대략 반비례」는 이 구간에서 성립합니다.
맨 앞 19위는 −0.26−0.80으로 더 평평합니다. 가장 흔한 조각 몇 개의 빈도가 순위만큼 벌어지지 않고 서로 비슷하다는 뜻입니다. 반대쪽 1,000위 뒤는 토크나이저와 언어에 따라 −0.98부터 −6.35까지 갈렸습니다.
직선이 끝나는 자리
가장 크게 갈린 것은 기울기가 아니라 직선이 끝나는 순위였습니다. 다국어 토크나이저 셋은 한국어에서 직선이 1,0001,390위에서 끝났습니다. 같은 토크나이저가 영어에서는 4,6328,828위까지 갔습니다. 한국어 전용 둘은 한국어에서 5,848위와 6,097위까지 이어졌습니다.
이 차이는 쓰인 종류 수와 같이 움직입니다. Qwen2.5는 어휘 151,665개 가운데 한국어 6MB에서 12,597개(8.3%)만 썼고, 그중 42.7%가 한 번만 나왔습니다. A.X는 어휘 102,400개 가운데 49,376개(48.2%)를 썼습니다. 같은 글을 Qwen2.5는 1,851,479토큰으로, A.X는 1,128,630토큰으로 잘랐으니 Qwen2.5가 한국어를 더 잘게, 더 적은 종류로 자른 것입니다. 적은 종류가 많은 토큰을 나눠 가지면 곡선의 머리와 가운데가 위로 부풀고, 쓸 종류가 바닥나는 자리에서 급히 꺾입니다. 그림의 붉은 곡선이 1,000위 근처에서 꺾이는 자리가 거기입니다. 어떤 조각이 꺾이는 자리 뒤에 남는지는 이 글에서 뽑아 보지 않았습니다.
한국어와 영어
계획이 물은 것은 「한국어에서 기울기가 영어와 다른가」였습니다. 같은 토크나이저의 가운데 기울기를 한국어에서 영어를 빼서 견주면 mBERT −0.007, A.X +0.099, Qwen2.5 −0.084, e5 +0.039입니다. 한국어가 평평한 토크나이저와 가파른 토크나이저가 섞여 부호가 일정하지 않습니다.
표본을 8배 바꿀 때 가운데 기울기가 움직인 폭이 0.02 안이었으니, 0.08~0.10의 차이는 표본 잡음보다 큽니다. 그러나 방향이 토크나이저마다 다르므로 「한국어는 기울기가 다르다」고 말할 수는 없습니다. 언어보다 토크나이저가 정하는 값입니다. 언어 사이에서 일관되게 갈린 것은 앞 절의 직선이 끝나는 자리, 곧 그 토크나이저가 한국어 조각을 얼마나 품었느냐였습니다.
어휘 크기
재현 블록
논문의 두 번째 관찰은 같은 말뭉치에서 어휘만 바꿔야 잴 수 있습니다. 공개 토크나이저는 어휘와 학습 말뭉치가 함께 달라지므로, tokenizers로 바이트 단위 BPE를 위 6MB에 직접 학습시켰습니다.
pip install tokenizers datasets numpy
import time, numpy as np
from collections import Counter
from datasets import load_dataset
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
t0 = time.perf_counter()
def corpus(rid, limit=6_000_000): # zipf.py와 같은 문단
out, b = [], 0
for c in dict.fromkeys(load_dataset(rid, split="train")["context"]):
if b >= limit: break
out.append(c); b += len(c.encode())
return out
C = {"ko": corpus("KorQuAD/squad_kor_v1"), "en": corpus("rajpurkar/squad")}
def measure(f): # Gowda & May(2020)의 D와 F95%, 그리고 10~1K 순위 기울기
f = np.sort(np.asarray(f, float))[::-1]; p = f / f.sum(); K = len(f)
D = 0.5 * np.abs(p - 1 / K).sum() # 균등분포에서 떨어진 정도, 0~1
F95 = f[int(np.ceil(0.95 * K)) - 1] # 상위 95% 안에서 가장 드문 종류의 빈도
r = np.arange(1, K + 1); m = (r >= 10) & (r <= 1000)
a = np.polyfit(np.log10(r[m]), np.log10(f[m]), 1)[0]
return K, D, F95, a, (f < 100).mean()
print(f"{'언어':<3}{'어휘':>7}{'쓰인 종류':>9}{'문단당 토큰':>9}{'D':>8}{'F95%':>7}{'100번 미만':>9}{'10~1K 기울기':>11}")
for L, t in C.items():
base = None
for V in (500, 1000, 2000, 4000, 8000, 16000, 32000, 64000):
tok = Tokenizer(models.BPE()); tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tok.train_from_iterator(t, trainers.BpeTrainer(vocab_size=V, show_progress=False,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()))
cnt = Counter(i for e in tok.encode_batch(t) for i in e.ids)
K, D, F95, a, rare = measure(list(cnt.values()))
mu = sum(cnt.values()) / len(t); base = base or mu
print(f"{L:<4}{V:>7,}{K:>10,}{mu:>8.1f} ({mu / base:.2f}){D:>7.3f}{F95:>7.0f}{rare:>9.1%}{a:>11.3f}")
print(f"elapsed={time.perf_counter() - t0:.0f}s")
python3 vocab.py
실제 출력
언어 어휘 쓰인 종류 문단당 토큰 D F95% 100번 미만 10~1K 기울기
ko 500 431 578.5 (1.00) 0.403 40 7.7% -1.631
ko 1,000 923 426.9 (0.74) 0.387 104 4.9% -1.094
ko 2,000 1,917 348.0 (0.60) 0.432 82 5.6% -0.762
ko 4,000 3,912 298.6 (0.52) 0.483 43 28.1% -0.725
ko 8,000 7,893 261.5 (0.45) 0.516 20 69.4% -0.708
ko 16,000 15,824 231.5 (0.40) 0.536 10 87.4% -0.718
ko 32,000 31,521 206.6 (0.36) 0.553 5 95.3% -0.750
ko 64,000 62,148 185.4 (0.32) 0.571 2 98.4% -0.802
en 500 441 334.6 (1.00) 0.480 17 17.7% -2.257
en 1,000 941 267.2 (0.80) 0.484 47 9.2% -1.403
en 2,000 1,941 223.7 (0.67) 0.505 51 8.8% -0.897
en 4,000 3,928 189.9 (0.57) 0.530 29 20.1% -0.863
en 8,000 7,897 164.8 (0.49) 0.558 12 70.1% -0.836
en 16,000 15,706 147.6 (0.44) 0.605 5 88.6% -0.821
en 32,000 30,386 136.5 (0.41) 0.671 2 95.2% -0.839
en 64,000 56,243 130.0 (0.39) 0.735 1 97.7% -0.873
elapsed=60s
D와 F95%
어휘 1,000부터는 논문이 말한 방향이 그대로 나왔습니다. 어휘를 64배 키우는 동안 한국어 D는 0.387에서 0.571로, 영어 D는 0.484에서 0.735로 한 번도 꺾이지 않고 올랐습니다. F95%는 한국어 104에서 2로, 영어 47에서 1로 내려갔습니다(영어 1,000→2,000에서 47→51로 한 번 오른 것만 예외입니다). 문단당 토큰은 한국어 426.9에서 185.4로 줄었습니다. 논문이 「문장이 짧아지는 이득과 불균형이 커지는 손해의 맞교환」이라 부른 것이 우리 말뭉치에서도 그대로입니다.
어휘 500 줄은 이 흐름에서 벗어납니다. 바이트 단위 BPE는 바이트 256개를 기본 알파벳으로 깔고 시작하므로, 어휘 500이면 병합이 250번도 안 됩니다. 한글 세 바이트 가운데 드물게 나오는 바이트가 따로 종류로 남아 F95%를 끌어내리고 D를 올립니다. 쓰인 종류가 431·441개라 「10~1K 기울기」도 431위까지만 그은 값이므로 다른 줄과 견주지 않습니다.
가운데 기울기는 어휘 2,000부터 64,000까지 한국어 −0.708~−0.802, 영어 −0.821~−0.897에 머물렀습니다. 어휘를 키운다고 Zipf 쪽으로 가거나 멀어지지 않았습니다. 곡선의 가운데 기울기는 그대로 두고, 오른쪽으로 길어지며 드문 종류를 늘린 것이 D가 오른 이유입니다.
꺾이는 지점
「100번 미만」 열이 맞교환이 꺾이는 자리를 보여 줍니다. 어휘 2,000까지는 종류의 94% 이상이 100번 넘게 나옵니다(100번 미만이 한국어 5.6%, 영어 8.8%). 4,000에서 28.1%·20.1%가 되고, 8,000에서 69.4%·70.1%로 뜁니다. 같은 2,000→8,000 사이에 문단당 토큰은 한국어 25%, 영어 26%만 줄었습니다.
한 줄로 적으면 이렇습니다. 이 6MB 말뭉치에서는 어휘 2,000까지가 공짜이고, 4,000부터 드문 종류가 늘기 시작해 8,000에서는 종류의 70%가 100번도 안 나옵니다. 문장 길이를 4분의 1 줄이는 값으로 클래스 대부분을 드물게 만든 셈입니다.
논문의 규칙(F95%가 100 근처 이상인 가장 큰 어휘)을 이 표에 대면 100을 넘는 것은 한국어 1,000 하나이고, 「근처」를 넓게 잡아도 한국어 2,000(82)까지입니다. 영어는 어느 어휘도 51을 넘지 못합니다. 같은 논문이 3만~130만 문장 규모에서 8,000이 좋은 맞교환이었다고 적은 것과 이 판정은 어긋납니다. 우리 말뭉치의 어휘 8,000에서 F95%는 20과 12입니다. 논문의 번역 데이터에서 같은 규모의 F95%가 얼마였는지는 그림에만 있어 숫자로 대조하지 못했습니다.
결론과 한계
논문값과 우리 값
| 논문의 문장 | 우리 측정 | 판정 |
|---|---|---|
| 순위는 빈도에 대략 반비례 | 순위 10 |
가운데 두 자릿수에서 재현 |
| (전 구간 직선) | −1.33~−2.27, 0.78~0.95, 말뭉치 8배에 0.3 움직임 | 표본 크기가 정하는 값 |
| 어휘를 키우면 D가 오른다 | 한국어 0.387→0.571, 영어 0.484→0.735 (어휘 1K→64K) | 재현 |
| 어휘를 키우면 F95%가 내려간다 | 한국어 104→2, 영어 47→1 | 재현 |
| F95% 100 근처가 최적 근처 | 한국어 1K~2K, 영어 해당 없음 — 논문의 「3만 문장에 8K」와 어긋남 | 번역 품질이 없어 판정 못 함 |
확인되지 않은 것
- 논문의 본론은 번역 품질(BLEU)이고 이 글은 번역 모델을 학습시키지 않았습니다. D와 F95%가 논문 방향으로 움직인다는 것까지만 확인했고, 그 움직임이 품질을 정한다는 주장은 다시 재지 못했습니다.
- 한국어와 영어 말뭉치는 대역이 아닙니다. 같은 위키백과 갈래를 같은 바이트만큼 잘랐을 뿐이라, 언어 차이와 내용 차이를 가르지 못합니다.
- 문장 수는 문장부호로 어림한 값입니다. 논문의 「3만 문장」과 견준 것은 규모의 자릿수만 맞춘 것입니다.
- D는 관측된 종류로 계산했습니다. 공개 토크나이저처럼 어휘의 대부분이 안 쓰이는 경우에는 어휘 전체로 계산한 D와 크게 다를 수 있어, 공개 토크나이저 표에는 D를 싣지 않았습니다.
- 직선 구간의 판정선(로그 0.1)과 가운데 구간(10~1,000위)은 우리가 정한 것입니다. 선을 바꾸면 구간의 끝이 움직입니다.
- 토크나이저는 다섯, 말뭉치는 언어마다 하나입니다. 대화체나 코드가 섞인 글에서는 꼬리의 모양이 다를 수 있습니다.
환경
| 항목 | 값 |
|---|---|
| OS | Linux 6.18.44 x86_64 (glibc 2.39) |
| CPU / RAM | Intel Xeon @ 2.10GHz, 4 vCPU / 15GB |
| Python | 3.11.17 |
| 패키지 | transformers 5.19.0, tokenizers 0.23.3, datasets 5.1.0, numpy 2.4.6 (PyTorch 없이 토크나이저만 사용) |
| 토크나이저 | klue/roberta-base (02f94ba5), google-bert/bert-base-multilingual-cased (3f076fdb), skt/A.X-4.0-Light (ba21c20e), Qwen/Qwen2.5-7B-Instruct (a09a3545), intfloat/multilingual-e5-small (614241f6) |
| 데이터 | KorQuAD/squad_kor_v1 (01aad238), rajpurkar/squad (7b6d24c4) 학습 분할 지문 |
| 논문 | Gowda & May, arXiv 2004.02334 (ar5iv HTML로 정의와 관찰 문장 확인) |
| 실행 시간 | zipf.py 29초, vocab.py 60초 |
| 측정일 | 2026-10-14 (KST) |
위 출력은 패키지를 새로 깐 빈 가상환경에서 다시 돌린 것이고, 처음 돌린 가상환경의 출력과 시간을 뺀 모든 숫자가 같았습니다. BPE 학습도 두 번 모두 같은 표를 냈습니다.
읽어주셔서 감사합니다. 😊

