
동영상 원본 : https://youtu.be/zduSFxRajkE?si=qR88n9wmarjPcAWA
colab 원본 : https://colab.research.google.com/dri...
게시글 내 모든 이미지와 코드의 출처는 위 영상과 colab 주소에 있습니다:)
이전 포스팅에서는 영상의 앞부분이었던 토크나이저의 개념과 쓰임에 대해서 다뤄봤는데요, 이번 포스팅은 토크나이저의 인코딩과 디코딩 등 실제 쓰임과 가까운 내용을 이야기했던 영상 뒷 부분을 다뤄보려고 합니다.
Merge가 되는 횟수는 훈련에 들어가는 data의 밀도(density)를 결정합니다. 만약, 훈련하려는 데이터에 일본어 데이터가 많다면 일본어는 더 많은 빈도로 merge될 것이고, 이는 같은 길이의 원 데이터라도 훨씬 짧은 token sequence로 표현될 수 있다는 뜻이기도 합니다. 언어모델이 일정 길이의 token sequence만 받을 수 있다고 했을때, 모델이 context를 훨씬 효과적으로 학습할 수 있습니다.
vocab = {idx: bytes([idx]) for idx in range(256)}
for (p0, p1), idx in merges.items():
vocab[idx] = vocab[p0] + vocab[p1]
def decode(ids):
# given ids (list of integers), return Python string
tokens = b"".join(vocab[idx] for idx in ids)
text = tokens.decode("utf-8", errors="replace")
return text
print(decode([97])) # output : 'a'
그렇다면 실제 token id를 저희가 아는 utf-8문자로 변환하는 디코딩을 진행하면 어떻게 될까요? 당연히 vocab에 저장되었던 문자가 출력됩니다.
print(decode([128])) # output : UnicodeDecodeError
하지만 특정 token id는 동일한 decode 함수에 입력했을 때 문자로 변환되지 못하고 에러가 발생합니다. 영상에서 보여준 128은 대표적으로 unicode 디코딩 규칙상 하나의 문자로 변환될 수 없다고 합니다. 128은 이진법으로 10000000인데 10xx..은 Byte1 coversion 범위에 포함되지 않기 때문이죠. 독립적인 하나의 문자로 디코딩될 수 없다는 의미입니다.

출처 : https://en.wikipedia.org/wiki/UTF-8
def encode(text):
# given a string, return list of integers (the tokens)
tokens = list(text.encode("utf-8"))
while len(tokens) >= 2:
stats = get_stats(tokens)
pair = min(stats, key=lambda p: merges.get(p, float("inf")))
if pair not in merges:
break # nothing else can be merged
idx = merges[pair]
tokens = merge(tokens, pair, idx)
return tokens
인코딩은 반대로 utf-8문자를 token id로 변환시키는 과정입니다. 이때, vocab에는 merge가 먼저 된 pair가 작은 token id를 가지고 있을 것이기 때문에, token id가 작은 순서대로 merge pair를 찾아 인코딩하는 과정을 진행합니다.
영상에서는 GPT2 논문의 2.2 Input representation 파트에서 이 부분을 구체적으로 설명하기 때문에 추천한다고 합니다. 특정 문자열을 token id로 변환하기 위해 1차적으로 문자열을 split하는 과정을 regex를 통해 진행하는데요, split을 잘 진행해야 처음 토크나이징을 진행할 때 의미 있는 문자열끼리 묶여서 인코딩될 수 있을 것입니다. 만약, 의미 없는 문자 연쇄들이 묶여 하나의 token id로 변환된다면 merge도 이상하게 진행될 테고, 훈련에서도 언어모델이 context를 제대로 학습하지 못할 것입니다.
import regex as re
gpt2pat = re.compile(r"""'s|'t|'re|'ve|'m|'ll|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")
print(re.findall(gpt2pat, "Hello've world123 how's are you!!!?"))
그래서 토크나이징을 진행하기 전 regex를 사용해서 의미 없는 문자들은 따로 분리하고, 의미 있는 문자 연쇄들로 묶일 수 있게 분리하는 작업을 진행합니다.

그리고 영상 앞 부분에서 언급했던 파이썬 코드를 다시 보여주면서, gpt2의 regex가 파이썬 코드를 비효율적으로 분리하는 결과를 코드로 직접 보여줍니다. 대신 GPT4에서는 regex를 수정해서 파이썬 코드를 효율적으로 토크나이징했기 때문에 파이썬 코드를 훨씬 잘 이해한다고도 이야기합니다. 이 regex는 titoken github 코드에서 확인이 가능합니다.
이 파트에서는 토크나이저 안에 ‘<|endoftext|>’ 같은 특정한 목적에 의한 special token이 포함되어 있다는 내용을 설명합니다. 이 부분은 추가적인 설명이 필요하지 않을 것 같아 넘어가겠습니다.
이 파트에서는 GPT4에 실제로 쓰인 minbpe 패키지를 소개하면서 실제로 토크나이징이 어떻게 진행되었는지 보캡을 보여줍니다. 그리고 Mistral과 Llama에 실제로 쓰인 sentencepiece 패키지도 소개합니다.
https://youtu.be/zduSFxRajkE?t=5896

여기서 흥미로웠던 부분은 Llama2가 훈련에 사용하지 않은 한국어를 어떻게 토크나이징하는지 직접 예시를 들어 설명하는 부분입니다. 옵션 값에서 byte_fallback 값을True 로 설정하면 토크나이저가 unknown token을 만났을 때 byte로 변환하여 token id로 매핑하고, False로 설정하면 byte token들이 사라지고 한국어가 하나의 <unk> 토큰으로 묶이게 됩니다.

또 add_dummy_prefix를 True 설정하면 원래는 다른 token id로 매핑되는 문장 맨 앞에 오는 단어(”world”)와 공백 문자가 앞에 오는 단어(”hello world”의 “ world”)를 동일한 token id로 매핑될 수 있게 합니다. sentencepiece 옵션값들에 대한 자세한 설명은 깃헙 코드에서 더 확인할 수 있습니다.
이번 포스팅에는 영상의 43분부터 약 1시간 43분까지의 내용을 다뤄보았습니다. 영상의 남은 뒷부분은 tokenizer 자체에 대한 설명보다 tokenizer와 관련한 여러 부가적인 내용과 영상을 wrap-up하는 파트로 구성되어 있는데요, 뒷부분도 재밌게 풀어 설명해주십니다. 개인적으로는 영어가 아닌 다른 언어의 예시로 한국어가 계속 등장하는게 볼수록 신기하네요. 그럼 다음 포스팅에서 찾아뵙겠습니다:)