콘텐츠로 이동
Data Prep
상세

토크나이징 (Tokenization)

텍스트를 모델이 처리할 수 있는 토큰 단위로 분리하는 과정. LLM 성능에 직접적인 영향을 미치는 중요한 전처리 단계.

왜 토크나이징이 필요한가

문자 단위: 시퀀스 너무 김, 의미 파악 어려움
단어 단위: OOV(Out-of-Vocabulary) 문제, 어휘 크기 폭발
서브워드: 균형 잡힌 해결책 (현대 LLM 표준)

토크나이징 방법

방법 특징 사용 모델
BPE 빈도 기반 병합 GPT, LLaMA
WordPiece 우도 기반 병합 BERT
Unigram 확률 기반 분해 T5, XLNet
SentencePiece 언어 독립적 대부분의 다국어 모델

BPE (Byte Pair Encoding)

가장 널리 사용되는 서브워드 토크나이징.

학습 과정

1. 초기 어휘: 모든 문자 (+ 특수 토큰)
2. 코퍼스에서 가장 빈번한 토큰 쌍 찾기
3. 해당 쌍을 새 토큰으로 병합
4. 어휘 크기 목표까지 반복

예시

코퍼스: ["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) 기반으로 병합.

병합 기준: P(merged) / P(first) * P(second) 최대화

예: "un" + "##able" → "##unable"
(## 접두사: 단어 중간 토큰)
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

확률 모델 기반. 가장 높은 확률의 분할 선택.

P(x) = Π P(xi)

"likelihood" 분할 확률:
P(["like", "li", "hood"]) vs P(["like", "lihood"]) vs ...
# 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  # 멀티프로세싱
)

참고 자료