Week 2

Tokenizer

같은 문장도 tokenizer에 따라 다른 길이가 된다. 여기서는 BPE(byte-pair encoding)와 byte-level BPE 두 가지를 살펴본다. 텍스트에서 시작하는 BPE의 병합 원리를 익힌 뒤, 단위를 바이트로 바꾸면 무엇이 달라지는지 예제로 확인한다. 앞 노트에서는 tokenizer가 정해져 있다고 두고 “오늘 부산의 날씨는”을 ID 열(sequence)로 바꿨다. 이제는 그 조각과 ID를 어떻게 정하는가를 살펴본다.

1. 텍스트의 단위

1-1. 분할 단위

“부산대학교에서 공부합니다.”를 어떻게 나누면 좋을까? 먼저 어절 단위(word-level)문자 단위(character-level)를 비교해 보자. 여기서 어절 단위 분할은 whitespace tokenization을 뜻한다.

분할 방식“부산대학교에서”의 분할 예시
Word 단위부산대학교에서
Character 단위부 · 산 · 대 · 학 · 교 · 에 · 서
Subword 단위부산 · 대학교 · 에서

표의 ·는 조각 사이의 경계를 표시한다. Subword의 분할은 원리를 보여 주는 가상 예시다.

word 단위라면 “부산대학교에서”가 하나가 되지만, “부산대학교에는”은 또 다른 항목이다. 모든 조합을 어휘 집합(vocabulary)에 넣으려면 그 크기가 커지고, 처음 보는 조합은 처리하기 어렵다. character 단위라면 이미 아는 문자들을 조합해 새 단어도 표현할 수 있지만, 같은 문장의 토큰 수가 늘어나 모델이 처리할 시퀀스가 길어진다.

Subword tokenization은 자주 쓰는 문자열 조각을 vocabulary에 넣는다. 단, “subword”가 항상 의미 있는 형태소(morpheme)라는 뜻은 아니다. 토큰은 통계와 구현 규칙이 정한 경계이며, 사람이 생각하는 단어나 글자 경계와 다를 수 있다.1

Tokenizer를 고르면 모델이 처리할 시퀀스 길이와 vocabulary 크기가 함께 정해진다. 그래서 context 사용량, 학습 토큰 수 계산까지 영향을 준다.

같은 문장에 서로 다른 분할 규칙을 적용한 가상 예시를 보자. 배경색이 이어진 묶음 하나가 토큰 하나이며, 는 공백을 나타낸다.

같은 문장 부산대학교에서 공부합니다.를 가상 Tokenizer 1은 부산대학교 / 에서 / 공백과 공부 / 합니다 / 마침표의 5개 토큰으로 나눈다. Tokenizer 2는 부산 / 대 / 학교 / 에서 / 공백 / 공부 / 합 / 니다 / 마침표의 9개 토큰으로 나눈다. 각 토큰의 배경색이 다르게 표시되어 있다.
Tokenizer에 따라 같은 텍스트가 다르게 토큰화 될 수 있다.

2. BPE

2-1. 희귀 단어를 작은 조각으로 표현하기

Sennrich, Haddow, Birch의 Neural Machine Translation of Rare Words with Subword Units(ACL 2016)는 드물거나 처음 보는 단어를 고정된 subword vocabulary의 조합으로 번역하자는 접근을 제시했다. 이름·합성어처럼 단어 전체가 낯설어도 작은 단위는 재사용할 수 있다는 생각이다.2

이 논문의 BPE는 압축 알고리즘을 단어 분할에 적용한 것이다.

이름에 “Byte”가 들어가지만, 단위는 바이트가 아니라 문자다. 단어를 문자와 단어 끝 표시로 나누고, 자주 이웃하는 기호 쌍을 합쳐 긴 조각을 만든다.2 예를 들어 vocabulary에 등록된 조각 lower를 재사용하면 lower를 별도 항목으로 외우지 않아도 된다. 다만 BPE는 형태소의 의미를 분석해서 자르는 방법이 아니다. 어떤 조각을 만들지는 말뭉치의 인접 쌍 빈도가 결정한다.

2-2. Vocabulary와 병합 규칙 구성

BPE는 말뭉치(corpus)의 인접 쌍 빈도를 집계해 vocabulary와 병합 규칙을 구성한다. 사람이 정한 빈도 기반 병합 알고리즘을 따르되, 어떤 조각을 어떤 순서로 합칠지는 데이터에 따라 결정된다. 따라서 말뭉치나 목표 vocabulary 크기가 다르면 구성되는 tokenizer도 달라질 수 있다. 이후 새 문장을 토큰화할 때는 이미 구성한 vocabulary와 병합 규칙을 고정해 적용한다.3

예를 들어 같은 BPE 알고리즘과 목표 vocabulary 크기를 사용해도, 의료 문서 중심의 말뭉치(medical corpus)와 일반적인 글 중심의 말뭉치(general corpus)로 구성한 tokenizer는 달라질 수 있다. 의료 말뭉치에 cardiology, cardiovascular 같은 표현이 자주 등장한다면, 공통 조각인 cardio가 하나의 토큰으로 구성될 수 있다. 반면 이런 표현이 드문 일반 말뭉치에서는 다른 조각의 병합이 우선되어 cardio가 여러 토큰으로 나뉠 수 있다. 이는 특정 tokenizer의 실제 출력이 아닌 가상 예시이며, BPE가 의학적 의미를 이해해서가 아니라 말뭉치의 빈도가 다르기 때문에 생기는 차이다.

BPE는 인접한 기호 쌍을 반복적으로 병합(merge)하는 방법이다. 여기서는 병합 규칙을 직접 계산할 수 있는 작은 예제를 만든다. 규칙 구성에 사용할 텍스트 모음인 말뭉치가 다음과 같다고 하자.

문자열등장 횟수처음의 기호열
banana3b a n a n a
band2b a n d

banana 안에서 (a,n)은 두 번 나타나고, 이 문자열은 세 번 등장하므로 기여하는 빈도는 6이다. band의 두 번까지 합치면 8이다.

인접 쌍초기 빈도
(a,n)8
(n,a)6
(b,a)5
(n,d)2

가장 빈도가 높은 (a,n)을 새 기호 an으로 합친다.

banana × 3 : b an an a
band   × 2 : b an d

이제 빈도를 다시 계산한다. (b,an)은 5회, (an,an)(an,a)는 각각 3회, (an,d)는 2회다. 다음 병합은 (b,an) → ban이다.

banana × 3 : ban an a
band   × 2 : ban d

원하는 어휘 크기에 도달할 때까지 이 과정을 반복한다. 예제에서는 두 번만 병합하고 멈춘다. 실제로 규칙을 구성할 때는 동률일 때 어느 쌍을 먼저 고를지, 어떤 경계를 넘을 수 있는지 등도 구현 규칙으로 정해야 한다.

직관

병합은 새 조각을 vocabulary에 추가하는 과정이다. an을 만들었다고 기본 기호 an을 지우는 것은 아니다. 다른 문맥에서는 여전히 작은 기호가 필요하다.

2-3. 처음 보는 단어에 적용하기

두 병합 규칙 a + n → an, b + an → ban을 새 단어 bandana에 적용하면 ban · d · an · a가 된다. 단어 전체를 vocabulary에 추가하지 않아도 기존 조각들로 표현할 수 있다.

그러나 처음 보는 단어와 처음 보는 문자는 다르다. 기본 문자 vocabulary가 a, b, n, d뿐이라면 는 이 조각들을 아무리 조합해도 표현할 수 없다. 이 한계에서 byte-level BPE로 이어진다.3

3. Byte-level BPE

처음 보는 문자도 표현하려면 기본 단위를 어떻게 정해야 할까? Byte-level BPE는 문자를 UTF-8 바이트열로 바꾼 뒤 바이트에서 병합을 시작한다. 먼저 문자에 부여한 번호와 실제 바이트 표현의 관계를 살펴보자.

3-1. 문자와 바이트

Unicode: 문자에 번호를 부여하는 표준

Unicode는 세계 여러 언어의 문자와 기호를 일관되게 표현하기 위한 표준이다. 여기서는 먼저 문자와 고유한 번호를 연결한 거대한 코드표로 이해하자. 이 번호를 코드 포인트(code point)라고 하며, A에는 U+0041, 에는 U+AC00이 부여되어 있다. U+ 뒤의 값은 16진수로 적은 번호다.4

컴퓨터는 텍스트를 저장·전송할 때 0과 1의 비트열로 표현한다. 따라서 문자에 부여한 번호를 실제로 저장할 비트열로 바꾸는 규칙이 필요하다. 예를 들어 의 코드 포인트는 U+AC00이지만, 이를 몇 바이트에 어떤 형식으로 담을지는 인코딩(encoding) 방식에 따라 달라진다.

UTF-8: 코드 포인트를 저장·전송할 바이트열로 바꾸는 방식

UTF-8(Unicode Transformation Format의 8-bit encoding)은 이런 인코딩 방식 중 하나다. 바이트(byte)는 8 bits이다. UTF-8은 문자에 해당하는 코드 포인트 하나를 1-4개의 바이트로 표현한다. 바이트 자체의 크기가 바뀌는 것이 아니라, 사용하는 바이트의 개수가 달라지는 가변 길이 인코딩이다.5

문자Unicode 코드 포인트UTF-8 바이트열 (16진수)바이트 수
AU+0041411
éU+00E9C3 A92
U+AC00EA B0 803
🙂U+1F642F0 9F 99 824

즉, 가 → U+AC00 → EA B0 80문자 → 코드 포인트 → 저장할 바이트열의 관계다. 이 코드 포인트는 tokenizer가 vocabulary의 토큰에 부여하는 token ID와는 별개다.

코드 포인트가 바이트열로 바뀌는 구체적인 규칙이 궁금하다면 UTF-8 표준의 §3: 비트 배치표와 인코딩 절차를 참고하자. 코드 포인트의 비트를 정해진 바이트 형식에 나누어 담는 과정을 설명한다.

영문 기본 알파벳·숫자·기본 기호는 ASCII(American Standard Code for Information Interchange)와 같은 1바이트로 표현된다. 한글 완성형 음절과 자주 쓰는 한자는 보통 3바이트지만, 일부 한자는 4바이트다. 특수문자도 모두 4바이트인 것은 아니다. 또한 화면에 보이는 한 글자가 여러 코드 포인트로 구성될 수 있으므로, 조합된 이모지 하나는 4바이트를 넘을 수 있다.5

다음 코드는 외부 라이브러리 없이 실행할 수 있다.

for text in ["A", "é", "가", "🙂"]:
    raw = text.encode("utf-8")
    print(text, list(raw), raw.decode("utf-8"))

# A [65] A
# é [195, 169] é
# 가 [234, 176, 128] 가
# 🙂 [240, 159, 153, 130] 🙂

출력의 바이트 값은 10진수다. 예를 들어 [234, 176, 128]은 표의 EA B0 80과 같은 바이트열이다. encode는 문자열을 바이트열로, decode는 바이트열을 다시 문자열로 바꾼다.

3-2. 바이트에서 시작하는 BPE

Byte-level BPE도 자주 이웃하는 쌍을 합친다는 원리는 같다. 차이는 병합을 시작하는 기본 단위다. 영어 소문자만 다룬다고 가정하면, 문자 기반 BPE는 a, b, …, z를 기본 vocabulary로 둘 수 있다. 반면 byte-level BPE는 텍스트를 UTF-8 바이트열로 바꾸고, 00000000(2)00000000_{(2)}부터 11111111(2)11111111_{(2)}까지 가능한 바이트 값 256개를 모두 기본 vocabulary에 둔다. 아래첨자 (2)(2)는 이진수 표기임을 뜻하며, 각각은 문자 여덟 개가 아니라 바이트 하나의 값이다.

두 방식 모두 기본 기호에서 출발해 자주 이웃하는 조각을 병합하고 vocabulary를 확장한다. 따라서 256개는 최종 vocabulary 크기가 아니라 시작할 때의 기본 기호 수다. Byte-level BPE는 이 256개 바이트를 갖고 있으므로, 유효한 UTF-8 문자열이라면 규칙 구성에 사용한 말뭉치에 없던 문자도 표현할 수 있다.3

비교 항목BPEByte-level BPE
기본 기호기본 vocabulary에 포함한 문자들0–255의 바이트 값 256개
병합 대상인접한 문자 또는 문자 조각인접한 바이트 또는 바이트 조각
처음 보는 문자기본 vocabulary에 없으면 처리 장치가 필요UTF-8 바이트로 표현 가능
토큰 경계여기서는 코드 포인트 경계에서 나뉨한 코드 포인트의 바이트 사이에서도 나뉠 수 있음

서로 다른 문자도 바이트 조각을 공유할 수 있다. 예를 들어 🙂F0 9F 99 82, 😊F0 9F 98 8A로 표현된다. 말뭉치에서 F0 9F가 자주 이웃해 병합된다고 가정하면, 두 이모지는 각각 [F0 9F] [99] [82], [F0 9F] [98] [8A]처럼 같은 조각을 재사용한다. 이 표기는 16진수 바이트를 묶은 예시이며, 실제 tokenizer의 출력은 아니다. 256개 기본 바이트로 표현 범위를 확보하고, 자주 쓰는 바이트 조각을 병합해 시퀀스를 줄이는 것이 핵심이다. Wang 등의 Neural Machine Translation with Byte-Level Subwords는 이런 조각 공유와 작은 vocabulary의 효과를 기계번역에서 분석한다.6

앞의 banana, band는 ASCII 문자만 사용하므로 UTF-8에서 문자 하나가 바이트 하나다. 따라서 같은 두 번의 병합을 바이트에서 시작해도 an, ban을 얻는다. 이처럼 영어 예제만 보면 차이가 잘 드러나지 않지만, 는 문자 하나와 바이트 세 개라는 차이가 생긴다. 이후에는 이 바이트 기반 vocabulary로 ID 변환과 복원을 따라간다.

3-3. 인코딩

Tokenizer가 완성된 뒤에는 저장한 병합 순위(merge rank)와 vocabulary를 이용해 인코딩(encoding)한다. bananaband 말뭉치로 tokenizer를 만든 앞 예제를 살펴보자.

1순위: a + n  → an
2순위: b + an → ban

새 입력 bandana에 적용해 보자. 아래 그림은 입력 바이트에서 시작해 위쪽으로 병합 과정을 따라간다. ①에서 두 a + n 쌍이 각각 an이 되고, ②에서 첫 b + anban이 된다. 남은 조각을 합칠 규칙은 없으므로 최종 토큰은 ban · d · an · a다.

bandana의 입력 바이트 b a n d a n a에서 두 a n 쌍을 an으로 합친 뒤 첫 b와 an을 ban으로 합친다. 최종 토큰은 ban, d, an, a다.
예제의 byte-level BPE 병합 과정.

기본 바이트의 ID를 바이트 값과 같게 놓고, an에 256, ban에 257을 부여한 vocabulary로 각 단계를 정리하면 다음과 같다.

단계현재 조각토큰 수
바이트로 시작b · a · n · d · a · n · a7
1순위: a + nb · an · d · an · a5
2순위: b + anban · d · an · a4
ID로 변환[257, 100, 256, 97]4

이 예제에서는 각 병합을 규칙이 만들어진 순서대로 적용하면 된다. 실제 BPE encoder도 적용 가능한 병합의 우선순위를 이용한다. 규칙 구성과 인코딩의 차이는 Hugging Face 공식 튜토리얼에서도 단계별로 확인할 수 있다.3

Tokenizer도 조회 테이블(lookup table, LUT)을 사용한다. 앞 예제에서 ban을 ID 257로 바꾸는 것은 vocabulary 조회이고, a + n을 먼저 합칠지 결정할 때에는 저장된 병합 순위를 조회한다. 따라서 byte-level BPE tokenization은 분할·병합 알고리즘과 테이블 조회를 함께 수행하는 과정이다. 입력 전체를 테이블에서 한 번 찾는 것만으로 끝나지는 않는다.3

조회 대상입력 → 조회 결과역할
Tokenizer의 병합 규칙a + n → 병합 가능 여부와 우선순위현재 조각들 중 무엇을 먼저 합칠지 결정
Tokenizer의 vocabularyban257최종 바이트 조각을 token ID로 변환
모델의 embedding 테이블257 → 실수 벡터해당 ID의 임베딩 행을 꺼내 모델에 입력

3-4. 바이트 복원

디코딩(decoding)은 각 ID에 대응하는 바이트 조각을 순서대로 합치는 일이다.

257      100   256     97
b a n  +  d  + a n  +  a → b a n d a n a → "bandana"

주의

바이트 기반 tokenizer의 토큰 하나가 유효한 UTF-8 문자 하나를 담는다는 보장은 없다. 한 글자의 바이트가 여러 토큰에 나뉠 수 있다. 각각을 따로 문자로 바꾸면 깨져 보여도, 바이트를 합친 뒤 decoding하면 원래 문자열이 복원될 수 있다.

예를 들어 tokenizer가 의 바이트를 [234, 176][128] 두 토큰으로 나누었다고 하자. 두 조각 모두 단독으로는 완성된 UTF-8 문자가 아니다. 다음 코드는 tokenizer 구현 없이 이 복원 단계만 확인한다.4

parts = [bytes([234, 176]), bytes([128])]
print([part.decode("utf-8", errors="replace") for part in parts])
# ['�', '�']
print(b"".join(parts).decode("utf-8"))
# 가

errors="replace"는 해석할 수 없는 바이트를 대체 문자 로 표시하라는 설정이다. 이 표시 문자열 둘을 합치면 ��가 되므로 원문을 되찾을 수 없다. 문자로 바꾸기 전에 원래 바이트를 합쳐야 한다. 생성 결과를 조금씩 표시하는 프로그램도 아직 완성되지 않은 문자의 바이트를 보관했다가 뒤 조각과 함께 처리할 수 있다.

3-5. Vocabulary에 없는 문자열

새 입력 bandana는 규칙 구성에 사용한 말뭉치에 없었고, vocabulary에도 단어 전체가 등록되어 있지 않다. 그래도 ban · d · an · a처럼 이미 등록된 작은 조각의 조합으로 표현할 수 있다. 처음 보는 단어를 읽는 것과 새로운 token ID를 추가하는 것은 다르다.

어떤 조각으로도 표현할 수 없는 경우를 OOV(out-of-vocabulary) 문제라고 한다. 반면 256개 바이트를 모두 기본 vocabulary로 갖는 byte-level BPE는 유효한 UTF-8 문자열을 최소한 바이트 단위로 표현할 수 있다.3 예를 들어 라는 토큰이 없어도 UTF-8 바이트 [234, 176, 128]로 표현할 수 있다. 다만, 이 처리는 이미 가진 작은 토큰을 더 많이 사용하는 것이다. 따라서 표현은 가능해도 토큰 수가 길어질 수 있으며, 모델이 그 문자열의 뜻을 이해한다는 보장도 없다.

영어 중심의 말뭉치로 구성된 tokenizer를 사용하는 LLM에서는, 한국어 입력이 같은 의미의 영어 입력보다 더 긴 토큰 시퀀스(sequence)로 표현될 수 있다. Tokenizer 구성에 사용한 말뭉치에서 한국어 조각이 상대적으로 드물었다면, 자주 쓰는 한국어 표현도 큰 조각으로 등록되지 않아 여러 작은 토큰으로 나뉠 수 있기 때문이다.

반면 한국어 말뭉치를 더 활용해 자주 등장하는 한국어 조각을 vocabulary에 담으면, 같은 한국어 문장을 더 적은 토큰으로 표현하는 데 도움이 될 수 있다. 여기서 한국어를 “더 잘 압축한다”는 것은 더 짧은 토큰 시퀀스로 나타낸다는 뜻이다. 효과는 vocabulary 크기와 구성 방식에 따라 달라지므로 실제 토큰 수를 비교해야 하며, 이미 구성된 LLM의 tokenizer만 임의로 교체할 수 있다는 뜻은 아니다.

4. Tokenizer 설계

4-1. 전처리와 특수 토큰

앞 예제는 병합 알고리즘만 떼어 본 것이다. 실제 tokenizer를 재현하려면 다음 요소까지 보아야 한다.

Pre-tokenization. 문자열을 먼저 공백·문자·숫자·문장부호 등의 규칙으로 나눈 뒤, 조각 안에서 BPE를 적용할 수 있다. 그러면 단순히 빈도가 높다는 이유만으로 모든 경계를 넘어 병합하지 않는다. Llama 3 구현에는 이 분할을 위한 정규식(regular expression)과 tiktoken 기반 encoding 설정이 있다.7

Normalization. 대소문자, Unicode 정규화(normalization), 공백 등을 바꾸는 tokenizer도 있다. 모든 byte-level BPE가 동일한 정규화를 수행하는 것은 아니다. 코드를 다루는데 공백을 임의로 줄이거나, 문자열의 표기를 바꾸면 원래 정보가 사라질 수 있다.

Special token. 문서 시작·종료나 대화의 역할 구분은 별도의 ID로 표현할 수 있다. 출력 화면에 보이는 표기와 모델이 받는 special-token ID는 구분해야 한다. 일반 입력에 그 표기가 들어왔다고 항상 special token으로 해석하는 것도 아니다. 허용 여부는 tokenizer 호출 설정에 달려 있다.7

아래는 각 단계의 역할을 구분하는 예제다. 특정 모델의 실제 출력은 아니며, normalization과 pre-tokenization의 적용 여부·순서는 tokenizer 설정에 따른다.

설정입력 → 처리 결과BPE에 미치는 영향
소문자 normalizationBusanbusan대문자 정보가 사라진 입력에서 병합
공백 경계 pre-tokenizationbusan portbusan port두 조각의 경계를 넘는 병합을 금지
등록·허용된 special token 처리<END> → 예약된 ID 하나해당 표기를 별도 제어 토큰으로 인식

Unicode normalization은 같은 글자의 서로 다른 코드 포인트 표현을 맞추기도 한다. Python에서 "가""\u1100\u1161"은 각각 코드 포인트 1개와 2개이지만, NFC(normalization form C) 방식의 unicodedata.normalize("NFC", text)를 적용하면 같은 "가"가 된다.

서로 다른 환경에서 수집한 텍스트는 이처럼 화면에서 같아 보여도 내부 표현이 다를 수 있어, 정규화로 맞추기도 한다. 이런 처리를 한 tokenizer의 decode 결과는 정규화된 텍스트일 수 있다. 바이트 기반이라는 사실만으로 전처리 전 원문까지 항상 복원되는 것은 아니다.4

이 때문에 학습된 모델의 tokenizer 파일과 special-token 설정은 모델 가중치와 함께 보존해야 한다. ID 순서를 바꾸면 같은 번호가 다른 조각을 가리켜 embedding과 대응하지 않게 된다.

4-2. 어휘 크기와 길이

Vocabulary size는 tokenizer가 자동으로 알아내는 정답이라기보다 설계자가 정하는 목표 크기다. 기본 기호와 special token이 차지할 자리를 고려한 뒤, 자주 나타나는 쌍을 병합해 새 토큰을 추가한다. 목표 크기에 도달하거나 더 병합할 쌍이 없으면 멈춘다. 데이터 양·최소 빈도 같은 조건 때문에 실제 크기가 목표보다 작을 수도 있다.3

§3의 예제는 기본 바이트 256개에 anban 두 토큰을 추가했으므로 vocabulary가 258개다. Special token은 세지 않은 값이다.

앞 노트의 Llama 3는 일반 텍스트용 토큰 128,000개와 예약 항목을 포함한 special token 256개로 총 128,256개를 사용한다. 따라서 embedding 테이블과 LM head의 vocabulary 축도 128,256이다. Vocabulary 크기는 “모델이 아는 단어 수”가 아니라 ID를 부여한 토큰 항목 수다.7

자주 쓰는 조각을 더 많이 등록하면 같은 문장이 더 적은 토큰으로 표현될 수 있다. 예를 들어 어떤 문서를 1,200토큰 대신 900토큰으로 표현한다면, 같은 토큰 길이 제한 안에 더 많은 원문을 넣을 수 있다.

4-3. 실제 토큰화 예제

이제 “오늘 부산의 날씨는”을 실제 tokenizer에 넣어 보자. 아래는 공개 라이브러리 tiktoken 0.14.0의 cl100k_base encoding으로 실행한 결과다. 앞 노트의 Llama 3 설명용 가상 ID와는 별도의 tokenizer이며, special token은 추가하지 않는다. 실행에는 pip install tiktoken==0.14.0이 필요하고, 처음 실행할 때 공개 어휘 파일을 내려받는다.8

import tiktoken

enc = tiktoken.get_encoding("cl100k_base")
text = "오늘 부산의 날씨는"
ids = enc.encode(text)
print(ids)
# [58368, 15478, 246, 86503, 86157, 21028,
#  38295, 254, 168, 242, 101, 16969]
print(len(ids))
# 12
print(enc.decode(ids))
# 오늘 부산의 날씨는
print(enc.decode(ids) == text)
# True

문자열의 “늘”에 해당하는 부분은 ID 두 개로 나뉜다. 바이트 조각을 직접 확인하면 §3-4의 설명이 실제 출력과 연결된다.

parts = [enc.decode_single_token_bytes(i) for i in ids[1:3]]
print([list(part) for part in parts])
# [[235, 138], [152]]
print(b"".join(parts).decode("utf-8"))
# 늘

같은 원문도 tokenizer가 달라지면 ID와 토큰 수가 달라진다. 모델의 입력 길이나 처리량을 비교할 때에는 encoding 이름을 함께 기록하고, ID 전체를 decode해 원문이 복원되는지도 확인한다.

4-4. 심화 자료

Karpathy의 tokenizer 구현 영상은 아래에서 재생하거나 원본 페이지에서 볼 수 있다.

Andrej Karpathy, Let’s build the GPT Tokenizer (2024). UTF-8 바이트에서 BPE 병합 규칙을 구현하는 과정을 보여 주는 선택 실습 자료.

단계별 예제를 더 보고 싶다면 Hugman Sangkeun Jung의 Tokenization 방법론들에 대한 쉽고 직관적인 이해를 참고한다. 더 깊은 내용은 tokenizer 심화 자료를 참고한다.

Footnotes

  1. Mielke, S. J., et al. (2021). Between words and characters: A Brief History of Open-Vocabulary Modeling and Tokenization in NLP. arXiv:2112.10508v1.

  2. Sennrich, R., Haddow, B., & Birch, A. (2016). Neural Machine Translation of Rare Words with Subword Units. ACL 2016, pp. 1715–1725. ACL 원문. 2

  3. Hugging Face. Byte-Pair Encoding tokenization. LLM Course, Chapter 6. 2 3 4 5 6 7

  4. Python Software Foundation. Unicode HOWTO. 공식 문서. 2 3

  5. Unicode Consortium. FAQ: UTF-8, UTF-16, UTF-32 & BOM. 공식 문서. 2

  6. Wang, C., Cho, K., & Gu, J. (2020). Neural Machine Translation with Byte-Level Subwords. AAAI 2020, 34(05), pp. 9154–9160. 논문 원문.

  7. Meta. (2024). Llama 3 tokenizer reference implementation. tokenizer.py. 2 3

  8. OpenAI. tiktoken, v0.14.0. 공식 저장소.