토큰화는 NLP 파이프라인의 가장 기본적인 단계입니다. 모델에 텍스트를 입력하려면 먼저 텍스트를 토큰(Token) 단위로 분할해야 합니다. 토큰화 방식에 따라 동일한 문장도 전혀 다른 입력이 되며, 이는 모델의 성능, 어휘 크기, 처리 속도에 직접적인 영향을 미칩니다.특히 현대 Transformer 모델들은 각각 고유한 토크나이저를 사용합니다. BERT는 WordPiece를, GPT는 BPE를, T5는 SentencePiece를 사용합니다. 토큰화를 이해해야 모델의 입력을 올바르게 준비하고, 토큰 수 제한을 관리하며, 토크나이저를 커스터마이징할 수 있습니다.
단어 단위 토큰화의 가장 큰 문제는 미등록어(OOV, Out-of-Vocabulary) 문제입니다. 학습 데이터에 없던 새로운 단어를 만나면 [UNK] 토큰으로 처리해야 하며, 이는 정보 손실을 의미합니다.
Copy
# 단어 단위 토큰화의 OOV 문제 예시vocabulary = {"나는", "학교에", "갑니다", "오늘"}text = "오늘 도서관에 갑니다"# "도서관에"는 어휘에 없으므로 [UNK]로 처리됩니다tokens = []for word in text.split(): if word in vocabulary: tokens.append(word) else: tokens.append("[UNK]") # 정보 손실!print(tokens) # ['오늘', '[UNK]', '갑니다']
서브워드 토큰화는 이 문제를 해결합니다. “도서관에”를 ["도서", "관", "에"]와 같이 분할하면, 학습 데이터에서 본 적 없는 단어도 아는 조각들의 조합으로 표현할 수 있습니다.
BPE는 가장 널리 사용되는 서브워드 토큰화 알고리즘입니다. GPT-2, GPT-3, GPT-4, LLaMA, RoBERTa 등이 BPE를 사용합니다.동작 원리:
모든 단어를 문자 단위로 분할합니다
가장 빈번하게 등장하는 인접 문자 쌍(pair)을 찾습니다
해당 쌍을 하나의 새로운 토큰으로 병합합니다
원하는 어휘 크기에 도달할 때까지 2-3을 반복합니다
Copy
# BPE 학습 과정을 간단히 시뮬레이션합니다corpus = { "l o w": 5, # "low"가 5번 등장 "l o w e r": 2, # "lower"가 2번 등장 "n e w e s t": 6, # "newest"가 6번 등장 "w i d e s t": 3 # "widest"가 3번 등장}# 반복 1: 가장 빈번한 쌍은 ("e", "s") - 9번 등장 (newest 6 + widest 3)# "n e w es t": 6, "w i d es t": 3# 반복 2: 가장 빈번한 쌍은 ("es", "t") - 9번 등장# "n e w est": 6, "w i d est": 3# 반복 3: 가장 빈번한 쌍은 ("l", "o") - 7번 등장# "lo w": 5, "lo w e r": 2# 이렇게 반복하면 자주 등장하는 서브워드가 하나의 토큰으로 병합됩니다
Copy
# HuggingFace Tokenizers로 BPE 토크나이저 학습하기from tokenizers import Tokenizer, models, trainers, pre_tokenizers# BPE 모델 기반 토크나이저를 생성합니다tokenizer = Tokenizer(models.BPE())tokenizer.pre_tokenizer = pre_tokenizers.Whitespace()# 학습 설정: 어휘 크기 1000, 특수 토큰 정의trainer = trainers.BpeTrainer( vocab_size=1000, special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])# 텍스트 파일로 토크나이저를 학습합니다tokenizer.train(files=["corpus.txt"], trainer=trainer)# 토큰화 실행output = tokenizer.encode("자연어 처리를 배우고 있습니다")print(output.tokens) # 토큰 목록print(output.ids) # 토큰 ID 목록
WordPiece는 Google이 개발한 서브워드 토큰화 알고리즘으로, BERT, DistilBERT, Electra 등에서 사용됩니다. BPE와 유사하지만, 병합할 쌍을 선택하는 기준이 다릅니다.BPE와의 차이: BPE는 가장 빈번한 쌍을 병합하지만, WordPiece는 병합 후 언어 모델의 가능도(Likelihood)를 가장 크게 높이는 쌍을 병합합니다. 즉, 단순 빈도가 아니라 통계적 유의미성을 기준으로 합니다.특징: 단어의 중간 부분에 해당하는 서브워드 앞에 ##을 붙여 단어 내부 위치임을 표시합니다.
Unigram 모델은 SentencePiece에서 기본으로 사용되는 알고리즘입니다. BPE와 반대 방향으로 작동합니다.동작 원리:
큰 어휘로 시작합니다 (모든 가능한 서브스트링 포함)
각 서브워드의 출현 확률을 계산합니다
제거했을 때 전체 가능도 감소가 가장 작은 서브워드를 제거합니다
원하는 어휘 크기에 도달할 때까지 반복합니다
BPE가 작은 어휘에서 시작하여 병합(Bottom-up)하는 반면, Unigram은 큰 어휘에서 시작하여 가지치기(Top-down)합니다.
Copy
# SentencePiece Unigram 모델 학습 예시import sentencepiece as spm# 모델 학습: Unigram 알고리즘 사용spm.SentencePieceTrainer.train( input="corpus.txt", model_prefix="unigram_model", vocab_size=8000, model_type="unigram", # BPE 대신 Unigram 사용 character_coverage=0.9995 # 한국어의 경우 높은 커버리지 필요)# 학습된 모델 로드 및 토큰화sp = spm.SentencePieceProcessor()sp.load("unigram_model.model")text = "자연어 처리를 배우고 있습니다"tokens = sp.encode_as_pieces(text)ids = sp.encode_as_ids(text)print(tokens) # ['▁자연어', '▁처리를', '▁배우고', '▁있습니다']print(ids) # [1234, 5678, ...]
SentencePiece는 Google이 개발한 언어 독립적 토큰화 도구입니다. T5, ALBERT, XLNet, LLaMA 등에서 사용됩니다.핵심 특징: 사전 토큰화(Pre-tokenization) 없이 원시 텍스트를 직접 처리합니다. 공백도 하나의 문자(▁, U+2581)로 취급하여, 띄어쓰기가 불규칙한 언어(일본어, 중국어)나 교착어(한국어)에서 특히 유리합니다.
Copy
# SentencePiece BPE 모델 학습import sentencepiece as spmspm.SentencePieceTrainer.train( input="korean_corpus.txt", model_prefix="ko_bpe", vocab_size=32000, model_type="bpe", character_coverage=0.9995, pad_id=0, unk_id=1, bos_id=2, eos_id=3)sp = spm.SentencePieceProcessor()sp.load("ko_bpe.model")# 공백이 ▁로 표시됩니다print(sp.encode_as_pieces("나는 학교에 갑니다"))# ['▁나는', '▁학교', '에', '▁갑', '니다']# 디코딩 시 ▁가 공백으로 복원됩니다ids = sp.encode_as_ids("나는 학교에 갑니다")print(sp.decode(ids)) # "나는 학교에 갑니다"
# 토크나이저의 전체 파이프라인을 확인합니다tokenizer = AutoTokenizer.from_pretrained("bert-base-multilingual-cased")text = "안녕하세요, 반갑습니다!"# 1단계: 토큰화 (텍스트 → 토큰)tokens = tokenizer.tokenize(text)print(f"토큰: {tokens}")# 2단계: 인코딩 (토큰 → ID)ids = tokenizer.convert_tokens_to_ids(tokens)print(f"토큰 ID: {ids}")# 3단계: 특수 토큰 추가 ([CLS], [SEP])encoded = tokenizer.encode(text, add_special_tokens=True)print(f"인코딩 (특수 토큰 포함): {encoded}")# 4단계: 디코딩 (ID → 텍스트)decoded = tokenizer.decode(encoded)print(f"디코딩: {decoded}")# 한 번에 처리하기 (실무에서 가장 많이 사용)batch = tokenizer( text, padding=True, # 패딩 추가 truncation=True, # 최대 길이 초과 시 자르기 max_length=128, # 최대 토큰 수 return_tensors="pt" # PyTorch 텐서로 반환)print(f"input_ids shape: {batch['input_ids'].shape}")print(f"attention_mask shape: {batch['attention_mask'].shape}")
Copy
# 배치 토큰화: 여러 문장을 한 번에 처리합니다texts = [ "오늘 날씨가 좋습니다", "내일은 비가 올 예정입니다", "주말에는 맑겠습니다"]batch = tokenizer( texts, padding=True, # 가장 긴 문장에 맞춰 패딩 truncation=True, max_length=64, return_tensors="pt")# 패딩된 결과를 확인합니다for i, text in enumerate(texts): tokens = tokenizer.convert_ids_to_tokens(batch["input_ids"][i]) print(f"문장 {i+1}: {tokens}") # [PAD] 토큰으로 길이가 맞춰진 것을 확인할 수 있습니다
토큰 수 관리: LLM API 호출 시 토큰 수가 비용과 직결됩니다. 토큰화 방식을 이해하면 비용을 최적화할 수 있습니다.
RAG 청크 분할: 문서를 검색 가능한 단위로 나눌 때, 토큰 수 기반 분할이 자주 사용됩니다.
Fine-Tuning: 새로운 도메인에 맞는 토크나이저를 학습하거나 확장하는 것이 모델 성능에 영향을 줍니다.
다국어 처리: 토크나이저의 어휘에 한국어가 충분히 포함되어 있는지가 한국어 성능을 좌우합니다.
왜 단어 단위 토큰화를 사용하지 않나요?
단어 단위 토큰화는 어휘 크기가 매우 커지고(수십만 개), 학습 데이터에 없는 단어를 처리하지 못합니다(OOV 문제). 서브워드 토큰화는 3만~5만 개의 적절한 어휘 크기로 거의 모든 단어를 표현할 수 있습니다.
토크나이저는 모델마다 고정되어 있나요?
네, 사전학습된 모델은 학습 시 사용한 토크나이저와 반드시 같은 토크나이저를 사용해야 합니다. 토큰과 ID의 매핑이 다르면 모델이 전혀 다른 입력을 받게 됩니다. AutoTokenizer.from_pretrained()을 사용하면 모델에 맞는 토크나이저가 자동으로 로드됩니다.
한국어 토큰화에서 BPE와 SentencePiece 중 어떤 것이 나은가요?
한국어의 경우 SentencePiece가 더 적합한 경우가 많습니다. 한국어는 띄어쓰기가 불규칙할 수 있는데, SentencePiece는 공백을 특별히 취급하지 않고 원시 텍스트를 직접 처리하므로 이 문제에 강건합니다. 다만, 실제로는 모델 선택에 따라 토크나이저가 결정되므로 직접 선택할 일은 드뭅니다.
토큰 수는 어떻게 미리 확인하나요?
tokenizer.encode(text)의 결과 길이로 확인할 수 있습니다. OpenAI의 경우 tiktoken 라이브러리를 사용하면 됩니다. LLM API 비용 산정이나 컨텍스트 윈도우 관리에서 토큰 수 확인은 필수적인 작업입니다.