Let's build the GPT Tokenizer (by. Andrej Karpathy) - (2)

Taekyung Ahn·2024년 3월 24일

강연모음

목록 보기
3/6
post-thumbnail

동영상 원본 : https://youtu.be/zduSFxRajkE?si=qR88n9wmarjPcAWA

colab 원본 : https://colab.research.google.com/dri...

게시글 내 모든 이미지와 코드의 출처는 위 영상과 colab 주소에 있습니다:)

이전 포스팅에서는 영상의 앞부분이었던 토크나이저의 개념과 쓰임에 대해서 다뤄봤는데요, 이번 포스팅은 토크나이저의 인코딩과 디코딩 등 실제 쓰임과 가까운 내용을 이야기했던 영상 뒷 부분을 다뤄보려고 합니다.

Data와 Merge 그리고 성능의 관계

Merge가 되는 횟수는 훈련에 들어가는 data의 밀도(density)를 결정합니다. 만약, 훈련하려는 데이터에 일본어 데이터가 많다면 일본어는 더 많은 빈도로 merge될 것이고, 이는 같은 길이의 원 데이터라도 훨씬 짧은 token sequence로 표현될 수 있다는 뜻이기도 합니다. 언어모델이 일정 길이의 token sequence만 받을 수 있다고 했을때, 모델이 context를 훨씬 효과적으로 학습할 수 있습니다.

Decoding

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

Encoding

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를 찾아 인코딩하는 과정을 진행합니다.

Regex Patterns for splits across categories

영상에서는 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 코드에서 확인이 가능합니다.

Special tokens

이 파트에서는 토크나이저 안에 ‘<|endoftext|>’ 같은 특정한 목적에 의한 special token이 포함되어 있다는 내용을 설명합니다. 이 부분은 추가적인 설명이 필요하지 않을 것 같아 넘어가겠습니다.

Tokenizer package - minbpe

이 파트에서는 GPT4에 실제로 쓰인 minbpe 패키지를 소개하면서 실제로 토크나이징이 어떻게 진행되었는지 보캡을 보여줍니다. 그리고 Mistral과 Llama에 실제로 쓰인 sentencepiece 패키지도 소개합니다.

Tokenizer package - 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하는 파트로 구성되어 있는데요, 뒷부분도 재밌게 풀어 설명해주십니다. 개인적으로는 영어가 아닌 다른 언어의 예시로 한국어가 계속 등장하는게 볼수록 신기하네요. 그럼 다음 포스팅에서 찾아뵙겠습니다:)

profile
정리좋아휴먼의 기록

0개의 댓글