왜 도메인 텍스트에서 토큰이 낭비되는가

사전학습된 토크나이저는 웹 일반 코퍼스 분포에 맞춰 병합 규칙을 학습한다. 그래서 의료 코드, 화학식, 로그 포맷, 한국어 전문용어처럼 학습 분포에 드물게 등장한 문자열은 여러 서브워드로 잘게 쪼개진다. 예를 들어 acetaminophen이나 gRPC-Web 같은 토큰이 5~8조각으로 나뉘면, 같은 정보를 담는 데 더 많은 토큰을 쓰게 된다.

이 낭비는 곧 비용이다. 입력이 길어지면 프롬프트 요금이 오르고, 컨텍스트 윈도가 빨리 차며, 어텐션 연산이 시퀀스 길이 제곱으로 늘어 지연도 커진다. 파인튜닝에서도 학습 스텝당 처리하는 유효 문서 수가 줄어든다.

먼저 측정하라: fertility

개선 전에 현재 상태를 수치화해야 한다. 핵심 지표는 fertility, 즉 단어당 평균 토큰 수다. 도메인 코퍼스 표본으로 측정한다.

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

def fertility(texts):
    n_tok = n_word = 0
    for t in texts:
        n_tok += len(tok(t, add_special_tokens=False)["input_ids"])
        n_word += len(t.split())
    return n_tok / n_word

sample = open("domain_corpus.txt").read().splitlines()
print(f"fertility = {fertility(sample):.3f}")

일반 영어는 fertility가 1.2~1.4 수준이지만, 전문 도메인이나 한국어에서는 2.0을 넘기도 한다. 이 값이 높을수록 어휘 확장 효과가 크다.

어휘 확장 절차

도메인 코퍼스로 별도 토크나이저를 학습해 자주 등장하는 조각을 뽑고, 기존 토크나이저에 병합한다. 이미 존재하는 토큰은 건너뛴다.

from tokenizers import ByteLevelBPETokenizer

# 1) 도메인 전용 BPE 학습
new_bpe = ByteLevelBPETokenizer()
new_bpe.train(files=["domain_corpus.txt"], vocab_size=8000, min_frequency=50)

# 2) 기존 어휘에 없는 토큰만 추려 추가
base_vocab = set(tok.get_vocab().keys())
candidates = [t for t in new_bpe.get_vocab()
              if t not in base_vocab]
added = tok.add_tokens(candidates[:4000])
print(f"added {added} tokens, new size = {len(tok)}")

추가 개수는 무한정 늘리지 말고, fertility 개선 곡선이 완만해지는 지점에서 멈춘다. 보통 2,000~8,000개 범위에서 비용 대비 효과가 가장 좋다.

모델 임베딩 리사이즈와 초기화

토큰을 추가하면 모델의 임베딩 행렬 크기가 맞지 않는다. 반드시 리사이즈하고, 새 행은 무작위가 아니라 기존 서브워드 임베딩의 평균으로 초기화하는 것이 학습 안정성에 좋다.

import torch

model.resize_token_embeddings(len(tok))
emb = model.get_input_embeddings().weight.data

for t in candidates[:added]:
    tid = tok.convert_tokens_to_ids(t)
    # 새 토큰을 기존 방식으로 분해한 조각들의 평균으로 초기화
    piece_ids = tok(t.replace("Ġ", " "),
                    add_special_tokens=False)["input_ids"]
    if piece_ids:
        emb[tid] = emb[piece_ids].mean(0)

무작위 초기화 시 초기 손실이 튀고 수렴이 느려진다. 평균 초기화는 새 토큰이 관련 의미 근처에서 학습을 시작하게 해준다. 초기화 후 짧은 계속학습(continued pretraining)으로 새 임베딩을 안착시키면 다운스트림 성능 저하를 막을 수 있다.

효과와 비용 비교

항목확장 전확장 후
fertility2.11.4
동일 문서 평균 토큰1,000670
입력 요금 / 지연기준약 33% 감소
임베딩 파라미터기준+어휘수×hidden

토큰 수가 줄면 입력 비용과 지연이 비례해 낮아진다. 대신 임베딩과 출력 헤드가 커져 메모리가 소폭 늘고, 새 토큰 학습을 위한 계속학습 비용이 든다.

주의점

  • 임베딩 미학습 위험: 토큰만 추가하고 계속학습을 생략하면 새 토큰이 무의미한 벡터로 남아 오히려 품질이 떨어진다.
  • 추론 스택 동기화: vLLM, TGI 등 서빙 엔진과 클라이언트가 모두 새 tokenizer.json을 써야 한다. 버전 불일치는 조용한 디코딩 오류를 만든다.
  • tied embedding: 입력·출력 임베딩을 공유하는 모델은 출력 헤드도 함께 커지므로 메모리 증가를 미리 계산한다.
  • 과도한 확장 금지: 희귀 토큰까지 넣으면 대부분 학습 데이터에 거의 안 나와 낭비다. min_frequency로 걸러라.

정리하면, 도메인 토큰 효율은 측정(fertility) → 선별적 확장 → 평균 초기화 → 계속학습 순으로 접근할 때 안정적으로 얻어진다. 무작정 어휘를 늘리기보다, 실제 코퍼스 분포에 근거해 필요한 만큼만 추가하는 것이 핵심이다.