토크나이저

토크나이저 (Tokenizer)

토크나이저의 역할과 종류(ASCII/단어/BPE), 그리고 sentencepiece 라이브러리로 나만의 토크나이저를 만드는 방법을 살펴보는 페이지예요.

출처: 문서

본문

소개 (Introduction)

토크나이저의 역할은 요청(보통 문자열)을 모델이 입력으로 받을 토큰 ID 시퀀스로 변환하는 거예요. 토큰은 단일 문자부터 서브워드(subword)나 심볼까지 뭐든 될 수 있어요. 모델은 그에 해당하는 ID와 함께 토큰 어휘(vocabulary)를 갖고 있어요. 토크나이저의 역할은 문자열을 쪼개서 토큰 ID 시퀀스로 변환하는 것인데, 이 단계를 "encoding"이라고 해요. 그리고 토큰 ID 시퀀스를 다시 문자열로 변환하는 것도 토크나이저의 역할인데, 이 단계를 "decoding"이라고 해요. 토큰 ID 시퀀스가 주어지면 모델은 다음 토큰을 예측하려 해요.

단순함을 위해 토큰을 완전한 단어로 설명해 볼게요.

다음과 같은 문장이 있다고 상상해 보세요: "Hello, how are"

인코딩 (Encoding)

  1. 토크나이저는 먼저 문장을 여러 조각(토큰)으로 쪼개요. 예를 들어:
    • Hello,
    • how
    • are
  2. 토크나이저는 이어서 각 토큰에 해당하는 토큰 ID를 얻어요. 예를 들어:
    • Hello, -> 432
    • how -> 523
    • are -> 87 이 토큰 ID 시퀀스가 모델에 제공돼요.

디코딩 (Decoding)

  1. 모델은 다음 토큰을 예측하려 해요. 이렇게 예측했다고 해볼게요:
    • 75 그러면 전체 토큰 ID 시퀀스가 돼요:
    • 432 + 523 + 87 + 75
  2. 이 시퀀스는 다시 완전한 문자열로 디코딩돼요:
    • Hello, + how + are + you -> Hello, how are you

토크나이저의 종류 (Types of Tokenizers)

토크나이저를 만드는 방법은 다양하고 접근 방식도 여러 가지가 있어요...

ASCII/문자 기반 토크나이저 (ASCII/Character-Based Tokenizer)

간단한 토크나이저는 ASCII/문자 기반인데, 각 문자와 ASCII 심볼을 토큰으로 취급해요. 이 접근 방식은 직관적이지만 몇 가지 단점이 있어요:

  • 느린 예측 (Slow Prediction): 심볼을 하나씩 예측하는 건 비효율적이에요.
  • 통계적 오류 (Statistical Errors): 토큰의 세밀함(granularity) 때문에 오류가 나기 쉬워요.
  • 정보/시퀀스 길이 비율 (Information/Sequence Length Ratio): 시퀀스 길이당 정보 비율이 낮아요.

단어 기반 토크나이저 (Word-Based Tokenizer)

또 다른 종류는 단어 기반 토크나이저로, 각 단어를 토큰으로 취급해요. 직관적으로 보일 수 있지만 최적화되어 있지 않아요. 예를 들어 다음 단어들을 고려해 보세요:

  • "fast"
  • "fastest"
  • "faster"
  • "slow"
  • "slowest"
  • "slower"

단어 기반 토크나이저는 이걸 6개 토큰으로 변환해요. 하지만 더 효율적인 접근 방식은 서브워드 토큰을 사용하는 거예요:

  • "fast"
  • "est"
  • "er"
  • "slow"

4개 토큰만으로도 원래 6개 단어를 조합해서 재현할 수 있어요.

BPE와 그 이상 (BPE and More)

Byte Pair Encoding(BPE)은 토크나이제이션에 쓰이는 간단한 형태의 데이터 압축 기법이에요. 텍스트에서 가장 빈번한 bigram(연속 토큰 쌍)을 반복적으로 병합하는 방식으로 동작해요. 이 과정은 원하는 어휘 크기에 도달할 때까지 계속돼요. BPE는 어휘에 없는(out-of-vocabulary) 단어를 서브워드 단위로 쪼개서 처리하는 데 특히 유용해요!

예시 (Example)

BPE를 더 잘 이해하기 위해 간단한 예시를 볼게요. 다음 텍스트가 있다고 해볼게요:

low
lower
lowest
new
news
newer

각 문자를 별도의 토큰으로 시작할게요:

l o w
l o w e r
l o w e s t
n e w
n e w s
n e w e r

목표는 이 예시에서 어휘 크기 5(즉 5개의 유일한 토큰)로 토크나이저를 학습시키는 거예요.

Step 1: 초기 토큰 (Initial Tokens)
l, o, w, e, r, s, t, n

-> 8 tokens

Step 2: 가장 빈번한 Bigram 병합 (Merge the Most Frequent Bigram)

가장 빈번한 bigram은 l과 o예요. 이걸 병합해서 새 토큰 lo를 만들어요.

lo w
lo w e r
lo w e s t
n e w
n e w s
n e w e r
Step 3: 새 토큰 (New tokens)
lo, w, e, r, s, t, n

-> 7 tokens

Step 4: 다시 가장 빈번한 Bigram 병합 (Merge Again Most Frequent Bigram)

다음으로 가장 빈번한 bigram은 lo와 w예요. 이걸 병합해서 새 토큰 low를 만들어요.

low
low e r
low e s t
n e w
n e w s
n e w e r
Step 5: 새 토큰 (New tokens)
low, e, r, s, t, n, w

-> 7 tokens

이 과정을 반복... (Repeat this process...)

원하는 어휘 크기에 도달할 때까지 절차를 반복해요. 결국 이렇게 보일 거예요:

low
low er
low est
new
new s
new er
최종 토큰 (Final Tokens)
low, new, er, est, s

-> 5 tokens

모델을 위해 지식을 잘 압축하는 효율적인 토크나이저를 만드는 과정은 효율적인 언어 모델로 가는 핵심 단계이며, 고려해야 할 트레이드오프가 많아요.

토크나이제이션의 트레이드오프 (Trade-offs in Tokenization)

어휘 크기, 길이, 압축 비율 사이의 트레이드오프는 여러 이유로 LLM 세계에서 아주 중요해요:

  • 모델 효율성 (Model Efficiency): 작은 어휘 크기는 더 빠른 학습·추론 시간으로 이어질 수 있어요.
  • 메모리 사용량 (Memory Usage): 더 큰 어휘는 더 많은 메모리를 요구해요.
  • 일반화 (Generalization): 서브워드 토크나이제이션은 공통 서브워드 패턴을 포착해서 모델이 더 잘 일반화하도록 도울 수 있어요.

나만의 토크나이저 만들기 (Make your own Tokenizer)

sentencepiece 라이브러리와 BPE를 사용해서 토크나이저를 만들어 볼게요.

Step 1: 학습 데이터 얻기 (Obtain Training Data)

먼저 토크나이저를 학습시킬 큰 텍스트 데이터셋이 필요해요. 이 예시에서는 Wikipedia의 일부를 사용할게요.

from datasets import load_dataset

dataset = load_dataset("wikimedia/wikipedia", "20231101.en", split="train", streaming=True)

with open("wikipedia_subset.txt", "w") as f:
    for i, example in enumerate(dataset):
        f.write(example["text"] + "\n")
        if i >= 9999:
            break

Step 2: 토크나이저 학습 (Train the Tokenizer)

sentencepiece 라이브러리로 토크나이저를 빠르게 학습할 수 있어요.

import sentencepiece as spm

data_path = "wikipedia_subset.txt"
model_file = "wikipedia_tokenizer"
vocab_size = 512

def get_longest_sentence_length(file_path):
    max_length = 0
    with open(file_path, 'r') as file:
        for line in file:
            sentence_length = len(line.split())
            if sentence_length > max_length:
                max_length = sentence_length
    return max_length

longest_sentence_from_dataset = get_longest_sentence_length(data_path)

spm.SentencePieceTrainer.train(
    f'--input={data_path} --model_prefix={model_file} --vocab_size={vocab_size} '
    f'--model_type=bpe --max_sentence_length={longest_sentence_from_dataset}'
)

tokenizer = spm.SentencePieceProcessor(model_file + ".model")

Step 3: 예시 인코딩 (Encode Example)

토크나이저가 어떻게 동작하는지 간단한 예시를 인코딩해 볼게요.

text = "We love Mistral!"
token_ids = tokenizer.encode(text)
token_str = tokenizer.encode(text, out_type=str)
print(f"Token Strings: {token_str}")
print(f"Token IDs: {token_ids}")
print(f"Decoded Text: {tokenizer.decode(token_ids)}")
Token Strings: ['▁W', 'e', '▁l', 'ov', 'e', '▁M', 'ist', 'r', 'al', '!']
Token IDs: [111, 392, 58, 206, 392, 59, 92, 398, 19, 481]
Decoded Text: We love Mistral!

Note: sentencepiece는 인코딩할 때 기본적으로 더미 공백(dummy whitespace)을 접두어로 붙이므로, 이 점을 인지하는 게 중요해요.

더 큰 어휘 크기(예: vocab_size = 8192)로 새 토크나이저를 학습하면 어휘 크기의 영향을 빠르게 확인할 수 있어요.

Token Strings: ['▁We', '▁love', '▁M', 'ist', 'ral', '!']
Token IDs: [1820, 4859, 59, 92, 448, 8161]
Decoded Text: We love Mistral!

그게 전부가 아니에요 (That's not all)

토크나이저를 만드는 데는 더 많은 것이 있어요. 특히 컨트롤 토큰을 고려할 때요. 제대로 된 토크나이저를 만드는 것은 모델의 좋은 성능, 효율성, 압축 비율을 위한 중요한 단계예요!

더 알아보기 (Learn more)