토크나이징 (Tokenization)¶
텍스트를 모델이 처리할 수 있는 토큰 단위로 분리하는 과정. LLM 성능에 직접적인 영향을 미치는 중요한 전처리 단계.
왜 토크나이징이 필요한가¶
토크나이징 방법¶
| 방법 | 특징 | 사용 모델 |
|---|---|---|
| BPE | 빈도 기반 병합 | GPT, LLaMA |
| WordPiece | 우도 기반 병합 | BERT |
| Unigram | 확률 기반 분해 | T5, XLNet |
| SentencePiece | 언어 독립적 | 대부분의 다국어 모델 |
BPE (Byte Pair Encoding)¶
가장 널리 사용되는 서브워드 토크나이징.
학습 과정¶
예시¶
코퍼스: ["low", "lower", "newest", "widest"]
초기: ['l', 'o', 'w', 'e', 'r', 'n', 's', 't', 'i', 'd', '</w>']
Step 1: 'e' + 's' → 'es' (빈도 2)
Step 2: 'es' + 't' → 'est' (빈도 2)
Step 3: 'l' + 'o' → 'lo' (빈도 2)
Step 4: 'lo' + 'w' → 'low' (빈도 2)
...
최종 어휘: ['low</w>', 'low', 'est</w>', 'er</w>', ...]
구현¶
from collections import Counter, defaultdict
import re
class BPETokenizer:
def __init__(self, vocab_size=1000):
self.vocab_size = vocab_size
self.merges = {}
self.vocab = {}
def train(self, corpus):
# 초기 어휘: 문자 단위 + </w> (단어 끝)
word_freqs = Counter()
for word in corpus:
word_freqs[' '.join(list(word)) + ' </w>'] += 1
# 병합 반복
for i in range(self.vocab_size):
pairs = self._get_pairs(word_freqs)
if not pairs:
break
best_pair = max(pairs, key=pairs.get)
self.merges[best_pair] = ''.join(best_pair)
# 코퍼스 업데이트
word_freqs = self._merge_pair(word_freqs, best_pair)
def _get_pairs(self, word_freqs):
pairs = defaultdict(int)
for word, freq in word_freqs.items():
symbols = word.split()
for i in range(len(symbols) - 1):
pairs[(symbols[i], symbols[i+1])] += freq
return pairs
def _merge_pair(self, word_freqs, pair):
new_freqs = {}
bigram = ' '.join(pair)
replacement = ''.join(pair)
for word, freq in word_freqs.items():
new_word = word.replace(bigram, replacement)
new_freqs[new_word] = freq
return new_freqs
def encode(self, text):
tokens = list(text) + ['</w>']
while len(tokens) > 1:
pairs = [(tokens[i], tokens[i+1]) for i in range(len(tokens)-1)]
mergeable = [(p, self.merges.get(p)) for p in pairs if p in self.merges]
if not mergeable:
break
# 가장 먼저 학습된 병합 적용
best_pair = min(mergeable, key=lambda x: list(self.merges.keys()).index(x[0]))[0]
new_tokens = []
i = 0
while i < len(tokens):
if i < len(tokens)-1 and (tokens[i], tokens[i+1]) == best_pair:
new_tokens.append(''.join(best_pair))
i += 2
else:
new_tokens.append(tokens[i])
i += 1
tokens = new_tokens
return tokens
Byte-level BPE (GPT-2/GPT-3)¶
UTF-8 바이트 단위로 BPE 적용. 모든 텍스트 처리 가능.
# tiktoken (OpenAI)
import tiktoken
enc = tiktoken.get_encoding("cl100k_base") # GPT-4
tokens = enc.encode("Hello, world!")
text = enc.decode(tokens)
print(f"토큰: {tokens}")
print(f"복원: {text}")
# 토큰 개수 확인
print(f"토큰 수: {len(tokens)}")
WordPiece (BERT)¶
BPE와 유사하지만 우도(likelihood) 기반으로 병합.
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokens = tokenizer.tokenize("unbelievable")
# ['un', '##bel', '##ie', '##vable']
encoded = tokenizer.encode("Hello world", add_special_tokens=True)
# [101, 7592, 2088, 102] # [CLS] hello world [SEP]
Unigram LM¶
확률 모델 기반. 가장 높은 확률의 분할 선택.
# SentencePiece with Unigram
import sentencepiece as spm
# 학습
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='unigram',
vocab_size=32000,
model_type='unigram'
)
# 사용
sp = spm.SentencePieceProcessor()
sp.load('unigram.model')
tokens = sp.encode('Hello world', out_type=str)
ids = sp.encode('Hello world', out_type=int)
SentencePiece¶
언어 독립적 토크나이저. 공백을 특수 문자(▁)로 처리.
import sentencepiece as spm
# BPE 모델 학습
spm.SentencePieceTrainer.train(
input='corpus.txt',
model_prefix='bpe_model',
vocab_size=32000,
model_type='bpe',
character_coverage=0.9995, # 한국어 등 필요
user_defined_symbols=['<pad>', '<mask>'],
pad_id=0,
unk_id=1,
bos_id=2,
eos_id=3
)
# 로드 및 사용
sp = spm.SentencePieceProcessor()
sp.load('bpe_model.model')
text = "안녕하세요 세계"
tokens = sp.encode(text, out_type=str)
# ['▁안녕', '하세요', '▁세계']
ids = sp.encode(text, out_type=int)
decoded = sp.decode(ids)
Hugging Face Tokenizers¶
from transformers import AutoTokenizer
# 사전 학습된 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained("gpt2")
# 인코딩
encoded = tokenizer("Hello, world!", return_tensors="pt")
print(encoded['input_ids'])
# 디코딩
decoded = tokenizer.decode(encoded['input_ids'][0])
# 토큰 확인
tokens = tokenizer.tokenize("Hello, world!")
print(tokens)
# 특수 토큰
print(f"BOS: {tokenizer.bos_token}, ID: {tokenizer.bos_token_id}")
print(f"EOS: {tokenizer.eos_token}, ID: {tokenizer.eos_token_id}")
print(f"PAD: {tokenizer.pad_token}, ID: {tokenizer.pad_token_id}")
토크나이저 학습¶
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
# BPE 토크나이저 학습
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel()
trainer = trainers.BpeTrainer(
vocab_size=50000,
special_tokens=["<pad>", "<unk>", "<bos>", "<eos>"]
)
tokenizer.train(files=["corpus.txt"], trainer=trainer)
tokenizer.save("custom_tokenizer.json")
토크나이저 비교¶
| 모델 | 토크나이저 | 어휘 크기 |
|---|---|---|
| GPT-2 | BPE (Byte-level) | 50,257 |
| GPT-4 | BPE (cl100k) | 100,256 |
| BERT | WordPiece | 30,522 |
| LLaMA | SentencePiece (BPE) | 32,000 |
| T5 | SentencePiece (Unigram) | 32,000 |
토크나이징 최적화¶
토큰 효율성¶
def tokens_per_character(tokenizer, text):
tokens = tokenizer.encode(text)
return len(tokens) / len(text)
# 영어: 약 0.25-0.3 tokens/char
# 한국어: 약 0.5-0.7 tokens/char (토큰 효율 낮음)
패딩과 트런케이션¶
encoded = tokenizer(
texts,
padding='max_length', # 또는 'longest'
truncation=True,
max_length=512,
return_tensors='pt'
)
실무 트레이드오프¶
토크나이저 선택 기준¶
| 고려사항 | BPE | WordPiece | Unigram |
|---|---|---|---|
| 학습 속도 | 빠름 | 보통 | 느림 |
| 메모리 사용 | 낮음 | 낮음 | 중간 |
| OOV 처리 | 양호 | 양호 | 우수 |
| 다국어 지원 | 양호 | 양호 | 우수 |
| 구현 복잡도 | 낮음 | 중간 | 높음 |
언어별 토큰 효율성¶
def analyze_tokenization_efficiency(tokenizer, texts_by_lang):
"""언어별 토큰 효율성 분석"""
results = {}
for lang, text in texts_by_lang.items():
tokens = tokenizer.encode(text)
chars = len(text)
results[lang] = {
'tokens': len(tokens),
'chars': chars,
'ratio': len(tokens) / chars # 낮을수록 효율적
}
return results
# 예시 결과 (GPT-4 cl100k_base):
# 영어: 0.25 tokens/char
# 한국어: 0.67 tokens/char # 2.7배 비효율
# 일본어: 0.52 tokens/char
# 중국어: 0.45 tokens/char
시사점: 비영어권에서 API 비용이 2-3배 높아질 수 있음
커스텀 토크나이저 학습¶
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders
def train_custom_tokenizer(
corpus_files,
vocab_size=32000,
min_frequency=2,
special_tokens=None
):
"""도메인 특화 토크나이저 학습"""
if special_tokens is None:
special_tokens = ["<pad>", "<unk>", "<s>", "</s>"]
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=True)
tokenizer.decoder = decoders.ByteLevel()
trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
min_frequency=min_frequency,
special_tokens=special_tokens,
show_progress=True
)
tokenizer.train(files=corpus_files, trainer=trainer)
return tokenizer
# 의료/법률 등 도메인 특화 시 효율 30-50% 향상 가능
어휘 크기 선택¶
어휘 크기별 트레이드오프:
작은 어휘 (8K-16K):
+ 임베딩 크기 작음
+ OOV 거의 없음
- 시퀀스 길어짐 → 추론 느림
중간 어휘 (32K-50K):
+ 균형 잡힌 선택
+ 대부분의 LLM이 사용
큰 어휘 (100K+):
+ 시퀀스 짧음
+ 다국어에 유리
- 임베딩 메모리 증가
- Rare token 학습 어려움
흔한 실수¶
| 실수 | 증상 | 해결책 |
|---|---|---|
| 특수 토큰 불일치 | 생성 종료 안됨 | eos_token 확인 |
| 패딩 방향 오류 | attention 오류 | padding_side='left' (생성 시) |
| 토크나이저 버전 불일치 | 이상한 출력 | 모델과 토크나이저 버전 맞추기 |
| max_length 초과 | 입력 잘림 | truncation=True + 경고 로깅 |
성능 최적화¶
# Fast tokenizer 사용 (Rust 기반, 10-20x 빠름)
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("gpt2", use_fast=True)
# 배치 토크나이징
texts = ["text1", "text2", "text3"]
encoded = tokenizer(texts, padding=True, truncation=True, return_tensors="pt")
# 병렬 처리
from datasets import Dataset
dataset = Dataset.from_dict({"text": large_text_list})
tokenized = dataset.map(
lambda x: tokenizer(x["text"]),
batched=True,
num_proc=4 # 멀티프로세싱
)