트랜스포머 — 어텐션을 손으로 계산한다 · 토큰화와 어휘 · 실습
토크나이저를 손으로 만든다
목표
바이트 수준 BPE 를 표준 라이브러리만으로 직접 만든다. UTF-8 바이트에서 출발해 인접 쌍을 세고, 가장 흔한 쌍을 합치는 규칙을 배우고, 그 규칙을 배운 순서대로 적용해 부호화한 뒤 원문으로 정확히 되돌린다. 마지막에 어휘 크기를 바꿔 가며 토큰 수가 줄어드는 곡선을 재고, 같은 내용의 한국어 문단과 영어 문단을 하나의 어휘로 부호화해 토큰 수를 나란히 놓는다.
왜 중요한가
요금도 문맥 한도도 단위가 토큰인데, 토큰은 글자도 낱말도 아니다. 어떤 조각을 하나로 셀지는 어휘를 만들 때 정해지고 어휘는 모델마다 다르다. 그래서 "글자 수에 얼마를 곱하면 된다" 는 어림은 언어가 바뀌는 순간 무너진다. 한글 음절 하나는 UTF-8 로 3바이트라, 어휘가 작으면 한 글자가 토큰 셋이 된다.
이 실습은 실제 모델의 토크나이저를 부르지 않는다. 이 파드에는 transformers·tokenizers·tiktoken 이 없고 numpy 도 시스템 파이썬에는 없다. 대신 같은 알고리즘을 손으로 만들고, 여러분이 만든 어휘로 잰 숫자만 쓴다. 그래서 "어느 모델은 이 문장을 몇 토큰으로 센다" 같은 말은 여기서 하지 않는다.
알고리즘 자체는 한 문장이다 — 가장 자주 붙어 다니는 둘을 하나로 합치는 일을 어휘가 원하는 크기가 될 때까지 되풀이한다. 어려운 것은 세부다. 겹치는 자리를 어떻게 셀지, 동점을 어떻게 깰지, 부호화할 때 규칙을 어떤 순서로 적용할지가 정해져 있지 않으면 같은 글로 돌려도 어휘가 매번 달라진다.
채점기는 여러분이 적어 둔 설명을 믿지 않는다. 여러분의 모듈을 실제로 불러 매번 다른 입력으로 함수를 직접 두드려 보고, 채점기가 따로 계산한 값과 대조한다. 입력은 실행마다 바뀌므로 값을 외워 넣을 수 없다.
단계
1. /root/work/tf-token/bpe.py 에 표본 문단 SAMPLE_KO·SAMPLE_EN 과 to_ids(text)·from_ids(ids) 를 만드세요. 글을 UTF-8 바이트 목록으로 열고 다시 글로 되돌립니다.
2. count_pairs(ids) 를 더해 이웃한 두 개가 몇 번 붙어 나오는지 세게 하세요. 열쇠는 (앞, 뒤) 짝이고 겹치는 자리도 그대로 셉니다.
3. merge(ids, pair, new_id) 를 더해 그 짝이 나오는 자리를 새 번호 하나로 바꾸게 하세요. 왼쪽부터, 겹치지 않게 갑니다.
4. MIN_PAIR_COUNT = 2 와 train(text, vocab_size) 를 만들어 병합 규칙을 배운 순서대로 모으게 하세요. 새 번호는 256부터 하나씩 올라갑니다.
5. encode(text, merges) 를 만들어 배운 순서 그대로 병합을 적용하게 하세요.
6. decode(ids, merges) 를 만들어 번호를 끝까지 풀고 원문으로 되돌리게 하세요. 어휘에 없던 글자가 섞여도 원문과 한 글자도 달라지면 안 됩니다.
7. vocab_curve(text, sizes) 를 만들어 어휘 크기마다 같은 글이 몇 토큰이 되는지 재게 하세요. 돌려주는 값은 (어휘크기, 토큰수) 짝의 목록입니다.
8. 어휘를 두 벌 배워 같은 두 문단을 재고, /root/work/tf-token/token_report.json 과 /root/work/tf-token/token_report.md 에 결과를 기록하세요.
참고
- 실행 계약: 채점기는
/root/work/tf-token/bpe.py를 파이썬 모듈로 불러SAMPLE_KO·SAMPLE_EN·to_ids·from_ids·count_pairs·merge·MIN_PAIR_COUNT·train·encode·decode·vocab_curve를 직접 씁니다. 스크립트로 실행하지 않으므로if __name__ == "__main__"은 없어도 됩니다. - 표본 문단은 같은 내용을 두 언어로 적습니다.
SAMPLE_KO는 한글이 대부분인 300자 이상,SAMPLE_EN은 아스키만 쓰는 300자 이상이어야 합니다. 내용은 자유롭게 정하세요. to_ids(text)는text.encode("utf-8")의 바이트를 정수 목록으로 돌려줍니다. 글자 번호(ord)가 아닙니다.from_ids(ids)는 조각난 바이트가 들어와도 예외를 내지 않아야 합니다.decode의errors인자를 쓰세요.count_pairs([9, 9, 9])는{(9, 9): 2}입니다. 겹치는 자리를 그대로 셉니다.merge([5, 5, 5], (5, 5), 300)은[300, 5]입니다. 앞의 둘을 삼킨 뒤 남은 5 는 짝을 잃습니다. 넘겨받은 목록을 제자리에서 고치지 말고 새 목록을 만드세요.train의 한 바퀴: 인접 쌍을 세고, 가장 많이 나온 쌍을 고르고, 그 쌍을 새 번호로 합칩니다. 동점이면(앞, 뒤)가 사전순으로 작은 쌍을 고릅니다. 고른 쌍이MIN_PAIR_COUNT보다 적게 나오면 멈춥니다. 돌려주는 값은((앞, 뒤), 새번호)짝의 목록이고 순서가 곧 배운 순서입니다.train(text, 256)은 빈 목록입니다. 0 부터 255 는 이미 바이트가 쓰고 있어 새 번호를 만들 자리가 없습니다.encode는merges를 받은 순서대로 한 번씩 적용합니다. 정렬하거나 반복하지 마세요.decode는 새 번호를 두 개로 풀고, 그 둘 중에 또 새 번호가 있으면 다시 풉니다. 남는 것이 없을 때from_ids로 읽습니다.- 8단계 보고서는 어휘를 두 벌 배웁니다. 둘 다 크기는 512 입니다. 하나는
SAMPLE_KO와SAMPLE_EN을 줄바꿈 하나로 이어 붙인 글로 배운 공용 어휘이고, 다른 하나는SAMPLE_EN만으로 배운 영어 전용 어휘입니다. 두 어휘로 같은 두 문단을 각각 부호화해 토큰 수를 견줍니다. vocab_size는 상한일 뿐입니다. 합칠 만한 쌍이 떨어지면MIN_PAIR_COUNT에서 멈추므로 실제 규칙 수는 512 에서 256 을 뺀 값보다 적을 수 있습니다. 그래서shared_rules·en_only_rules에 실제로 배운 규칙 수를 적습니다.- 곡선은 이어 붙인 글에 대해 크기 256·320·384·512 로 잽니다.
- 이 파드에는 인터넷이 없습니다.
pip install은 되지 않고 transformers·tokenizers·tiktoken 도 없습니다. numpy 는/opt/onnx-lab/bin/python안에만 있으므로 시스템 파이썬에서는import numpy가 되지 않습니다. 표준 라이브러리만으로 충분합니다. - 공식 문서: [BPE 원논문](https://arxiv.org/abs/1508.07909) · [Hugging Face — Byte-Pair Encoding tokenization](https://huggingface.co/learn/llm-course/en/chapter6/5) · [Attention Is All You Need](https://arxiv.org/abs/1706.03762)
- 흔한 실수: 바이트 대신 글자 번호를 쓰기, 쌍을 두 칸씩 건너뛰며 세기, 합칠 때 한 칸씩만 전진해 겹쳐 삼키기, 동점 규칙을 안 정하기, 부호화할 때 규칙을 정렬하기, 복호화를 한 겹만 풀고 끝내기.
단계 8개
- 글을 바이트로 연다
- 이웃한 둘을 센다
- 한 쌍을 하나로 합친다
- 병합 규칙을 배운다
- 배운 순서대로 부호화한다
- 한 글자도 다르지 않게 되돌린다
- 어휘를 키우면 토큰이 준다
- 어휘가 누구의 글로 만들어졌는가