본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2026. 4. 13. · 0 Views
Day 3 문자 단위 토크나이저 만들기
LLM이 텍스트를 이해하려면 먼저 문자를 숫자로 변환해야 합니다. 텍스트를 쪼개고, 각 조각에 번호를 매기고, 다시 원래 텍스트로 복원하는 토크나이저를 직접 만들어봅니다.
목차
- 토크나이제이션의 역할과 필요성
- 문자 단위와 서브워드 단위 차이
- 텍스트 파일에서 전체 문자 집합 추출
- stoi와 itos 딕셔너리 만들기
- encode 함수 구현 텍스트를 정수로 변환
- decode 함수 구현 정수를 텍스트로 복원
1. 토크나이제이션의 역할과 필요성
김개발 씨는 어제 PyTorch 기본기를 마친 뒤, 드디어 본격적인 LLM 구현에 들어가려던 참이었습니다. 그런데 잠깐, 모델이 텍스트를 그대로 이해할 수 있을까요?
컴퓨터는 숫자만 알지 않나요?
**토크나이제이션(Tokenization)**은 텍스트를 모델이 처리할 수 있는 숫자 시퀀스로 변환하는 과정입니다. 마치 도서관 사서가 책 제목을 분류 번호로 바꿔주는 것과 같습니다.
이 변환 없이는 언어모델이 텍스트를 학습하거나 생성할 수 없습니다.
다음 코드를 살펴봅시다.
# 토크나이제이션이 왜 필요한지 확인해보기
text = "Hello LLM"
# 컴퓨터가 텍스트를 직접 처리할 수 없다
# print(text + 1) # TypeError 발생!
# 텍스트를 숫자(아스키 코드)로 변환하면 연산 가능
numbers = [ord(c) for c in text]
print(numbers) # [72, 101, 108, 108, 111, 32, 76, 76, 77]
# 숫자를 다시 텍스트로 복원
restored = ''.join(chr(n) for n in numbers)
print(restored) # "Hello LLM"
"LLM 바닥부터 만들기: 30일 완성 코스"의 세 번째 날입니다. 어제 우리는 PyTorch의 텐서 연산과 자동 미분 기능을 살펴보며, 모델 학습의 기초 도구를 익혔습니다.
오늘은 그 도구를 활용하기 전에 반드시 거쳐야 할 관문, 바로 텍스트를 숫자로 바꾸는 작업을 다룹니다. 김개발 씨는 어제 밤늦게까지 PyTorch 텐서를 다뤄보며 신났습니다.
드디어 언어모델을 만들 수 있겠다는 기대감이 커지던 참이었습니다. 아침 출근하자마자 곧바로 모델 코드를 작성하려고 했죠.
그런데 박시니어 씨가 커피를 한 잔 건네며 말했습니다. "잠깐, 모델에 텍스트를 그대로 넣을 수 있다고 생각해?" 김개발 씨는 잠시 멈칫했습니다.
맞다. PyTorch 텐서는 숫자만 담을 수 있는데, 텍스트를 어떻게 넣지?
박시니어 씨가 화이트보드에 크게 TOKENIZATION이라고 적었습니다. "언어모델이 텍스트를 이해하려면, 먼저 텍스트를 숫자로 변환해야 해.
이 과정이 바로 토크나이제이션이야." 토크나이제이션이란 정확히 무엇일까요? 쉽게 비유하자면, 외국어 책을 번역할 때 먼저 단어 하나하나를 사전에서 찾아 번역하는 과정과 같습니다.
컴퓨터에게 "Hello"라는 단어는 그저 화면에 표시되는 기호일 뿐, 의미를 가진 데이터가 아닙니다. 하지만 72, 101, 108, 108, 111이라는 숫자로 변환하면 컴퓨터가 수학적으로 처리할 수 있게 됩니다.
이 변환은 왜 필수적일까요? 언어모델의 핵심은 next token prediction입니다.
앞의 토큰들을 보고 다음 토큰을 예측하는 구조죠. 하지만 "앞의 토큰들"이 숫자가 아니라면, 모델은 이를 텐서로 담을 수도 없고, 행렬 곱셈으로 처리할 수도 없습니다.
softmax 함수로 확률을 계산하는 것도 불가능합니다. 쉽게 말해, 토크나이제이션은 텍스트와 수학 사이의 번역기 역할을 합니다.
인간의 언어를 기계의 언어(숫자)로 변환해주는 다리인 셈이죠. 위의 코드를 살펴봅시다.
먼저 ord(c) 함수는 각 문자를 아스키 코드(정수)로 변환합니다. 'H'는 72, 'e'는 101로 바뀝니다.
이렇게 하면 텍스트가 숫자 리스트가 되어 PyTorch 텐서로 변환할 수 있습니다. 반대로 chr(n)은 숫자를 다시 문자로 복원합니다.
실제 현업에서는 이 단순한 변환을 훨씬 정교하게 처리합니다. OpenAI의 GPT 시리즈는 BPE(Byte Pair Encoding)라는 방식을 사용하고, Google의 모델들은 SentencePiece를 사용합니다.
하지만 모든 토크나이저의 기본 원리는 같습니다. 텍스트를 숫자로 바꾸고, 필요할 때 다시 텍스트로 복원하는 것.
주의할 점이 있습니다. 토크나이제이션 방식에 따라 모델이 학습하는 "단어"의 단위가 달라집니다.
너무 큰 단위로 쪼개면 어휘집이 거대해지고, 너무 작은 단위로 쪼개면 시퀀스가 길어집니다. 이 trade-off는 나중에 서브워드 토크나이제이션을 다룰 때 자세히 살펴보겠습니다.
김개발 씨는 고개를 끄덕였습니다. "아, 그러니까 모델을 만들기 전에 먼저 번역기를 만들어야 하는 거군요!" 박시니어 씨가 미소를 지었습니다.
"정확해. 그리고 오늘 우리는 가장 기본이 되는 버전부터 직접 만들어볼 거야."
실전 팁
💡 - 토크나이제이션은 텍스트를 숫자로, 숫자를 텍스트로 변환하는 양방향 과정입니다
- 언어모델은 오직 숫자만 처리할 수 있으므로 이 변환 단계는 선택이 아닌 필수입니다
2. 문자 단위와 서브워드 단위 차이
김개발 씨는 토크나이제이션의 필요성을 이해했지만, 또 다른 궁금증이 생겼습니다. "텍스트를 숫자로 바꾼다는 건 알겠는데, 텍스트를 얼마나 큰 단위로 쪼개야 할까요?
글자 단위? 단어 단위?"
토크나이제이션에는 여러 전략이 있습니다. **문자 단위(Character-level)**는 각 글자를 하나의 토큰으로 처리하고, **서브워드 단위(Subword-level)**는 자주 등장하는 문자 조합을 하나의 토큰으로 묶습니다.
마치 글자를 하나씩 읽는 것과 단어 단위로 읽는 것의 차이와 같습니다.
다음 코드를 살펴봅시다.
# 문자 단위 vs 서브워드 단위 비교
text = "hello world"
# 문자 단위: 각 글자를 개별 토큰으로
char_tokens = list(text)
print(char_tokens)
# ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
# 서브워드 단위(예시): 자주 쓰이는 조합을 묶기
# "he", "llo", "wor", "ld" 같은 패턴을 하나의 토큰으로
subword_tokens = ["he", "llo", " ", "wor", "ld"]
print(subword_tokens)
# ['he', 'llo', ' ', 'wor', 'ld']
# 어휘집 크기 비교
print(f"문자 단위 토큰 수: {len(char_tokens)}") # 10
print(f"서브워드 단위 토큰 수: {len(subword_tokens)}") # 5
김개발 씨의 질문은 아주 정확한 포인트를 찌르고 있었습니다. 텍스트를 숫자로 바꾸는 것은 이해했는데, 도대체 "얼마나 큰 단위로" 쪼개야 하는지가 관건이죠.
박시니어 씨가 화이트보드에 세 가지 방식을 적었습니다. 문자 단위, 단어 단위, 서브워드 단위.
"각각 장단점이 있어. 먼저 단어 단위부터 볼까?" 박시니어 씨가 말했습니다.
단어 단위는 "I love programming"을 ["I", "love", "programming"]으로 쪼개는 방식입니다. 직관적이지만, 문제가 있습니다.
모든 단어를 사전에 등록해야 하고, 처음 보는 단어(Unknown token)가 등장하면 처리할 수 없다는 치명적인 단점이 있죠. "그럼 문자 단위는?" 김개발 씨가 물었습니다.
문자 단위는 "I love programming"을 ['I', ' ', 'l', 'o', 'v', 'e', ...]처럼 글자 하나하나로 쪼개는 방식입니다. 어휘집이 매우 작아집니다.
영어라면 대소문자 구분 없이 26개의 알파벳加上 몇 개의 기호만으로 모든 텍스트를 표현할 수 있죠. Unknown token 문제도 사라집니다.
아무리 새로운 단어가 등장해도, 결국 알파벳 조합이니까요. 하지만 단점도 뚜렷합니다.
시퀀스가 길어집니다. "programming"이라는 11글자 단어가 11개의 토큰이 되어버리죠.
시퀀스가 길어지면 모델이 처리해야 할 토큰 수가 늘어나고, 계산 비용도 증가합니다. 또한 글자 단위로는 단어의 의미를 파악하기 어렵습니다.
't', 'h', 'e'가 각각 따로 들어오면 이것이 "the"라는 단어인지 알아채는 데 시간이 더 걸리죠. "그래서 등장한 게 서브워드 방식이야." 박시니어 씨가 설명했습니다.
서브워드 방식은 문자 단위와 단어 단위의 중간 지점입니다. 자주 등장하는 문자 조합을 하나의 토큰으로 묶고, 드문 조합은 문자 단위로 처리합니다.
예를 들어 "ing"이 자주 등장한다면 이를 하나의 토큰으로 묶는 식이죠. GPT 시리즈가 사용하는 BPE(Byte Pair Encoding)가 대표적인 서브워드 방식입니다.
위의 코드에서 확인할 수 있듯이, 문자 단위는 "hello world"를 10개의 토큰으로 처리하는 반면, 서브워드 방식은 5개로 줄일 수 있습니다. 시퀀스 길이가 절반으로 줄어드는 셈이죠.
그렇다면 우리는 어떤 방식을 사용해야 할까요? "오늘은 가장 기본이 되는 문자 단위부터 구현해볼 거야." 박시니어 씨가 말했습니다.
"문자 단위는 구조가 단순해서 토크나이제이션의 핵심 원리를 이해하기에 딱 맞아. 나중에 서브워드 방식으로 확장하는 것도 훨씬 쉬워질 거야." 김개발 씨는 고개를 끄덕이며 노트를 꺼냈습니다.
기초부터 차근차근, 확실히 배우는 것이 가장 빠른 길이라는 것을 이미 알고 있었으니까요.
실전 팁
💡 - 문자 단위 토크나이제이션은 어휘집이 작고 구현이 단순하지만, 시퀀스가 길어지는 단점이 있습니다
- 서브워드 방식은 BPE, WordPiece, SentencePiece 등 다양한 알고리즘이 있으며, GPT 계열은 BPE를 사용합니다
3. 텍스트 파일에서 전체 문자 집합 추출
방식을 정했으니 이제 실전입니다. 김개발 씨는 작업할 텍스트 파일을 열었고, 박시니어 씨는 첫 번째 단계로 "사용되는 모든 문자를 파악하자"고 말했습니다.
도대체 몇 종류의 문자가 등장하는 걸까요?
토크나이저를 만들려면 먼저 처리 대상 텍스트에 등장하는 모든 고유 문자의 집합을 알아야 합니다. 이 집합이 곧 모델의 어휘집(Vocabulary)이 되며, 각 문자에 고유한 번호를 할당하는 기준이 됩니다.
마치 새로운 언어를 배울 때 먼저 사용되는 알파벳을 파악하는 것과 같습니다.
다음 코드를 살펴봅시다.
# 훈련용 텍스트 파일에서 문자 집합 추출하기
with open('input.txt', 'r', encoding='utf-8') as f:
text = f.read()
# 등장하는 모든 고유 문자 추출 (정렬)
chars = sorted(set(text))
vocab_size = len(chars)
print(f"전체 문자 수: {vocab_size}")
print(f"문자 집합: {chars}")
# 예: 전체 문자 수: 65
# 문자 집합: ['\n', '!', ' ', '$', '&', "'", ',', '-', '.', ...
김개발 씨는 노트북 앞에 앉아 input.txt 파일을 열었습니다. 이 파일에는 훈련에 사용할 텍스트가 들어 있습니다.
Shakespeare의 작품이라고 하죠. 텍스트가 꽤 길지만, 사실 등장하는 문자의 종류는 생각보다 적습니다.
박시니어 씨가 옆에서 화면을 가리키며 설명했습니다. "첫 단계는 간단해.
이 텍스트에 등장하는 모든 문자의 종류를 파악하는 거야. 중복 없이, 한 번씩만." 쉽게 비유하자면, 이 과정은 레스토랑의 메뉴판을 만드는 것과 같습니다.
손님들이 주문할 수 있는 모든 종류의 요리를 먼저 파악해야 메뉴판을 만들 수 있죠. 마찬가지로 모델이 처리할 수 있는 모든 문자의 종류를 먼저 파악해야 합니다.
코드를 한 줄씩 살펴봅시다. 먼저 open('input.txt', 'r', encoding='utf-8')으로 텍스트 파일을 읽어옵니다.
여기서 utf-8 인코딩을 명시하는 것이 중요합니다. 한글이나 특수문자가 포함된 텍스트를 제대로 읽으려면 반드시 필요한 설정입니다.
다음으로 set(text)가 핵심입니다. Python의 set은 중복을 자동으로 제거합니다.
"hello"라는 텍스트라면 {'h', 'e', 'l', 'o'}가 되죠. 'l'이 두 번 등장하지만 set은 하나만 남깁니다.
그 다음 sorted()로 정렬합니다. 정렬은 필수는 아니지만, 결과를 확인하기 훨씬 편해집니다.
또한 매번 실행할 때마다 같은 순서로 결과가 나오므로 재현성도 보장되죠. 마지막으로 vocab_size = len(chars)로 어휘집의 크기를 구합니다.
Shakespeare 텍스트의 경우 보통 60~70개 정도의 고유 문자가 등장합니다. 영문 알파벳 대소문자 52개에 숫자, 공백, 특수문자가 합쳐진 크기죠.
"이 숫자가 중요해." 박시니어 씨가 말했습니다. "vocab_size는 모델의 입력 차원과 출력 차원을 결정해.
즉, 모델이 구별할 수 있는 토큰의 총 종류수가 되는 거야." 김개발 씨가 물었습니다. "한글 텍스트라면 어휘집이 훨씬 커지겠죠?" 맞습니다.
한글은 자음과 모음의 조합으로 수천 개의 음절이 존재합니다. 문자 단위 토크나이제이션을 사용하면 어휘집 크기가 영어보다 훨씬 커지게 됩니다.
이것이 바로 문자 단위 방식의 한계이자, 서브워드 방식이 필요한 이유 중 하나입니다. 하지만 지금은 Shakespeare 텍스트로 시작하겠습니다.
어휘집이 작고 단순하니, 토크나이제이션의 원리를 이해하기에 최적의 데이터입니다. 김개발 씨는 코드를 실행해보고 화면에 출력된 문자 집합을 확인했습니다.
생각보다 적은 종류의 문자였지만, 이 작은 집합이 곧 모델의 전체 세계가 될 것이라니 신기했습니다.
실전 팁
💡 - set()을 사용하면 중복 없는 고유 문자 집합을 쉽게 추출할 수 있습니다
- vocab_size는 모델의 입출력 차원을 결정하므로 토크나이저 설계에서 가장 중요한 숫자입니다
4. stoi와 itos 딕셔너리 만들기
문자 집합을 추출했으니, 이제 각 문자에 번호를 매길 차례입니다. 김개발 씨는 이 과정이 마치 학교에서 학생에게 번호를 나눠주는 것과 같다고 느꼈습니다.
철저한 번호 매기기, 그것이 오늘의 핵심입니다.
stoi(string-to-integer) 딕셔너리는 문자를 정수로 매핑하고, itos(integer-to-string) 딕셔너리는 정수를 다시 문자로 매핑합니다. 이 두 딕셔너리는 토크나이저의 핵심 데이터 구조로, 인코딩과 디코딩 모두에서 사용됩니다.
마치 양방향 번역 사전과 같습니다.
다음 코드를 살펴봅시다.
# 문자와 정수 사이의 매핑 딕셔너리 생성
# stoi: string to integer (문자 -> 정수)
stoi = { ch: i for i, ch in enumerate(chars) }
# itos: integer to string (정수 -> 문자)
itos = { i: ch for i, ch in enumerate(chars) }
# 매핑 확인
print(stoi['a']) # 예: 39 (문자 'a'의 번호)
print(itos[39]) # 'a' (번호 39에 해당하는 문자)
# 인코딩 테스트
encoded = [stoi[c] for c in "hi"]
print(encoded) # 예: [46, 47]
# 디코딩 테스트
decoded = ''.join(itos[i] for i in [46, 47])
print(decoded) # "hi"
어휘집을 만들었으니, 이제 각 문자에 고유한 번호를 할당해야 합니다. 이 번호가 바로 모델이 인식하는 "토큰 ID"가 됩니다.
박시니어 씨가 말했습니다. "생각해봐.
학교에서 학생들에게 번호를 줄 때, 1번부터 순서대로 주잖아? 여기서도 마찬가지야.
각 문자에 0번부터 순서대로 번호를 줄 거야." 이 과정을 쉽게 비유하자면, 비밀 암호표를 만드는 것과 같습니다. A=1, B=2, C=3...
이런 식으로 문자와 숫자를 1대1로 매칭하는 표를 만드는 것이죠. 이 표가 있으면 문자를 보고 숫자를 찾을 수도 있고, 숫자를 보고 문자를 찾을 수도 있습니다.
코드의 핵심은 enumerate() 함수입니다. enumerate(chars)는 정렬된 문자 리스트의 각 요소에 인덱스를 붙여줍니다.
예를 들어 chars가 ['!', ' ', 'a', 'b', ...]라면, (0, '!'), (1, ' '), (2, 'a'), (3, 'b'), ...这样的 튜플 시퀀스를 만들어줍니다. stoi 딕셔너리는 딕셔너리 컴프리헨션으로 한 줄에 만듭니다.
{ ch: i for i, ch in enumerate(chars) }는 "각 문자 ch를 키로, 인덱스 i를 값으로 하는 딕셔너리를 만들어라"라는 의미입니다. itos는 반대 방향입니다.
{ i: ch for i, ch in enumerate(chars) }로 인덱스를 키로, 문자를 값으로 하는 딕셔너리를 만듭니다. stoi와 itos는 서로 역함수 관계에 있습니다.
itos[stoi['a']]는 항상 'a'를 반환해야 하죠. "왜 굳이 두 개를 만들어요?" 김개발 씨가 물었습니다.
"하나만 있으면 되지 않나요?" 좋은 질문입니다. 인코딩(텍스트를 숫자로)할 때는 stoi가 필요하고, 디코딩(숫자를 텍스트로)할 때는 itos가 필요합니다.
모델은 숫자를 입력받고 숫자를 출력하므로, 출력된 숫자를 다시 읽을 수 있는 텍스트로 바꾸려면 itos가 필수적입니다. 실제로 테스트해보면 stoi['h']가 46을 반환하고, itos[46]이 다시 'h'를 반환하는 것을 확인할 수 있습니다.
이 완벽한 양방향 변환이야말로 토크나이저가 제대로 동작한다는 증거입니다. 주의할 점이 있습니다.
이 매핑은 텍스트에 따라 달라집니다. Shakespeare 텍스트에서는 'a'가 39번일 수 있지만, 다른 텍스트에서는 다른 번호를 가질 수 있습니다.
따라서 토크나이저를 저장하고 공유할 때는 stoi와 itos 딕셔너리도 반드시 함께 저장해야 합니다. 김개발 씨는 두 딕셔너리를 출력해보며 매핑이 정확히 1대1로 이루어지는 것을 확인했습니다.
작지만 완벽한 번역 사전이 완성된 것이죠.
실전 팁
💡 - stoi와 itos는 항상 쌍으로 만들고, 함께 저장해야 합니다
enumerate()를 활용하면 반복문 없이 깔끔하게 매핑 딕셔너리를 만들 수 있습니다
5. encode 함수 구현 텍스트를 정수로 변환
매핑 표가 완성되었습니다. 이제 실제로 텍스트를 숫자 리스트로 변환하는 encode 함수를 만들 차례입니다.
김개발 씨는 드디어 모델이 이해할 수 있는 형태로 텍스트를 변환하는 마법을 직접 구현하게 됩니다.
encode 함수는 입력 텍스트의 각 문자를 stoi 딕셔너리를 통해 정수로 변환하여 정수 리스트를 반환합니다. 이 정수 리스트가 바로 언어모델에 입력으로 들어가는 토큰 시퀀스입니다.
마치 텍스트를 모스 부호로 변환하는 것과 같습니다.
다음 코드를 살펴봅시다.
# encode 함수: 텍스트를 정수 시퀀스로 변환
def encode(text):
"""문자열을 정수 리스트로 인코딩합니다"""
return [stoi[ch] for ch in text]
# decode 함수: 정수 시퀀스를 텍스트로 복원
def decode(indices):
"""정수 리스트를 문자열로 디코딩합니다"""
return ''.join(itos[i] for i in indices)
# 인코딩과 디코딩 테스트
text = "hello"
encoded = encode(text)
decoded = decode(encoded)
print(f"원본: {text}") # "hello"
print(f"인코딩: {encoded}") # [46, 47, 50, 50, 53]
print(f"디코딩: {decoded}") # "hello"
print(f"일치: {text == decoded}") # True
드디어 핵심입니다. 매핑 표를 만들었으니, 이제 실제로 텍스트를 숫자로 변환하는 함수를 구현합니다.
박시니어 씨가 키보드를 가리키며 말했습니다. "encode 함수는 생각보다 아주 단순해.
각 문자를 stoi에서 찾아서 숫자로 바꾸는 것, 그게 전부야." 맞습니다. encode 함수는 단 한 줄로 구현됩니다.
[stoi[ch] for ch in text]는 입력 텍스트의 각 문자 ch를 순회하면서 stoi 딕셔너리에서 해당 문자의 정수를 찾아 리스트로 만듭니다. 쉽게 비유하자면, 이 과정은 암호화와 같습니다.
평문(텍스트)을 암호표(stoi)를 이용해 암호문(정수 리스트)으로 바꾸는 것이죠. "hello"라는 평문이 [46, 47, 50, 50, 53]이라는 암호문으로 변환됩니다.
리스트 컴프리헨션을 사용하면 for 문을 명시적으로 작성할 필요가 없습니다. 다음 두 코드는 완전히 동일한 결과를 만듭니다.
첫 번째는 컴프리헨션 버전: [stoi[ch] for ch in text] 두 번째는 일반 for 문 버전: result = [] for ch in text: result.append(stoi[ch]) 컴프리헨션이 더 간결하고 파이썬다운(Pythonic) 코드이므로, 실무에서도 자주 사용됩니다. 인코딩된 결과를 확인해보면, "hello"가 [46, 47, 50, 50, 53]으로 변환됩니다.
여기서 'l'이 두 번 등장하므로 50도 두 번 나오는 것을 알 수 있습니다. 길이도 원본 텍스트와 동일하게 5입니다.
문자 단위 토크나이제이션에서는 토큰 수와 문자 수가 항상 같습니다. "이 숫자들이 바로 모델에 들어가는 입력이에요?" 김개발 씨가 물었습니다.
"맞아. 하지만 리스트 그대로가 아니라 PyTorch 텐서로 변환한 뒤에 들어가." 박시니어 씨가 대답했습니다.
torch.tensor(encoded) 한 줄이면 리스트가 텐서가 됩니다. 주의할 점이 있습니다.
encode 함수에 stoi에 없는 문자를 넣으면 KeyError가 발생합니다. 훈련 데이터에 없던 새로운 문자가 테스트 시에 등장할 수 있는데, 이런 경우를 처리하려면 예외 처리를 추가하거나 Unknown token을 정의해야 합니다.
하지만 지금은 Shakespeare 텍스트만 다루므로 이 문제는 걱정하지 않아도 됩니다. encode 함수는 아주 작지만, 언어모델 파이프라인에서 가장 첫 번째 단계를 담당하는 중요한 함수입니다.
모든 텍스트가 이 함수를 거쳐 숫자가 되고, 그 숫자가 모델의 세계로 들어갑니다. 김개발 씨는 encode와 decode를 번갈아 실행해보며, 원본 텍스트가 완벽하게 복원되는 것을 확인했습니다.
text == decoded가 True를 반환하는 것을 보고 안도의 한숨을 내쉬었습니다.
실전 팁
💡 - encode 함수는 한 줄의 리스트 컴프리헨션으로 구현할 수 있을 만큼 단순합니다
- 훈련 데이터에 없는 문자가 입력되면 KeyError가 발생하므로, 프로덕션 환경에서는 예외 처리가 필요합니다
6. decode 함수 구현 정수를 텍스트로 복원
인코딩으로 텍스트를 숫자로 바꿨으니, 이제 반대 과정도 필요합니다. 모델이 숫자를 출력했을 때, 우리가 읽을 수 있는 텍스트로 바꿔주는 decode 함수입니다.
김개발 씨는 이 함수가 있어야 비로소 모델이 "말"을 할 수 있다는 것을 깨달았습니다.
decode 함수는 모델이 출력한 정수 시퀀스를 itos 딕셔너리를 통해 다시 텍스트로 변환합니다. encode와 decode가 쌍을 이루어야 토크나이저로서 제 기능을 합니다.
마치 암호문을 다시 평문으로 읽어내는 복호화 과정과 같습니다.
다음 코드를 살펴봅시다.
# encode/decode 완전한 동작 확인
text = "to be or not to be"
# 인코딩: 텍스트 -> 정수 리스트
tokens = encode(text)
print(f"토큰 수: {len(tokens)}") # 18 (공백 포함)
print(f"토큰 ID: {tokens}")
# PyTorch 텐서로 변환 (모델 입력용)
import torch
tensor_input = torch.tensor(tokens, dtype=torch.long)
print(f"텐서 형태: {tensor_input.shape}") # torch.Size([18])
print(f"텐서 타입: {tensor_input.dtype}") # torch.int64
# 디코딩: 정수 리스트 -> 텍스트
restored = decode(tokens)
print(f"복원 텍스트: {restored}") # "to be or not to be"
assert text == restored, "인코딩-디코딩 일치 불일치!"
encode 함수를 만들었으니, 이제 decode 함수를 만들 차례입니다. 사실 우리는 이미 위에서 decode 함수를 함께 정의했지만, 이번에는 이 함수가 왜 중요한지, 어떻게 모델과 연결되는지 더 깊이 살펴보겠습니다.
박시니어 씨가 말했습니다. "encode만 있고 decode가 없으면 어떻게 될까?
모델이 숫자를 출력하는데, 우리는 그 숫자를 읽을 수 없잖아. decode는 모델의 출력을 인간의 언어로 번역해주는 역할이야." 비유하자면, encode는 한국어를 영어로 번역하는 것이고, decode는 영어를 다시 한국어로 번역하는 것입니다.
번역과 재번역이 정확하다면, 원본과 복원본은 완벽히 일치해야 합니다. assert text == restored가 이를 검증하는 코드입니다.
decode 함수의 구현도 encode만큼 단순합니다. ''.join(itos[i] for i in indices)는 각 정수 i를 itos 딕셔너리에서 찾아 문자로 바꾼 뒤, 빈 문자열로 이어 붙입니다.
여기서 ''.join()이 핵심입니다. itos[i]가 각각의 문자를 반환하므로, 이것들을 하나의 문자열로 합쳐야 합니다.
join에 빈 문자열 ''을 구분자로 넣으면 문자 사이에 아무것도 없이 이어붙여집니다. 만약 '|'.join()을 사용하면 "h|e|l|l|o"처럼 됩니다.
코드를 실행해보면 "to be or not to be"가 18개의 토큰으로 인코딩됩니다. 공백도 하나의 문자이므로 토큰에 포함됩니다.
이 18개의 정수가 torch.tensor()로 변환되어 모델의 입력 텐서가 됩니다. 여기서 dtype=torch.long이 중요합니다.
PyTorch의 CrossEntropyLoss(분류 문제에서 사용하는 손실 함수)는 long 타입(64비트 정수)의 레이블을 요구합니다. 만약 float 타입으로 넣으면 에러가 발생합니다.
토큰 ID는 항상 정수이므로, 이 설정은 자연스럽습니다. "assert문은 왜 넣었어요?" 김개발 씨가 물었습니다.
assert는 조건이 거짓이면 프로그램을 즉시 중단시키는 검증문입니다. encode와 decode가 정확히 쌍을 이루지 않으면, 즉 인코딩했다가 디코딩했을 때 원본과 다르면, 무언가 심각한 버그가 있는 것입니다.
이를 조기에 발견하기 위한 안전장치죠. 실제 모델 학습 과정에서의 흐름을 정리하면 다음과 같습니다.
첫째, 텍스트를 encode로 정수 리스트로 변환합니다. 둘째, 정수 리스트를 PyTorch 텐서로 변환합니다.
셋째, 텐서를 모델에 입력합니다. 넷째, 모델이 다음 토큰의 확률 분포를 출력합니다.
다섯째, 확률이 가장 높은 토큰 ID를 선택합니다. 여섯째, 선택된 토큰 ID를 decode로 텍스트로 변환합니다.
이 여섯 단계가 바로 언어모델이 텍스트를 생성하는 전체 과정입니다. 그리고 그 시작과 끝에 encode와 decode가 자리 잡고 있습니다.
김개발 씨는 encode와 decode를 여러 문장에 테스트해보았습니다. "Hello, World!", "To be, or not to be", 심지어 줄바꿈이 포함된 문장까지.
모든 경우에 원본이 완벽하게 복원되었습니다. 박시니어 씨가 만족스러운 표정으로 말했습니다.
"토크나이저 완성이야. 이제 이걸로 훈련 데이터를 준비할 수 있어.
내일은 이 토큰들을 활용해서 학습용 샘플을 만들어볼 거야."
실전 팁
💡 - dtype=torch.long은 토큰 ID를 텐서로 변환할 때 반드시 지정해야 합니다
- encode 후 decode 했을 때 원본과 항상 일치해야 하며, assert문으로 이를 검증하는 습관을 들이세요
- 이 카드뉴스는 "LLM 바닥부터 만들기: 30일 완성 코스" 코스의 3/30편입니다. 다음 편에서는 토크나이저로 변환한 데이터를 활용해 학습용 샘플을 만드는 방법을 다룹니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
LLM 핵심 원리 함수 호출 환각 임베딩 완벽 가이드
LLM의 세 가지 핵심 개념인 함수 호출(Function Calling), 환각(Hallucination), 임베딩(Embedding)을 중급 개발자 관점에서 실무 중심으로 설명합니다. 에이전트 AI 엔지니어가 반드시 알아야 할 원리와 실전 팁을 담았습니다.
LLM 기본 사항 토큰 컨텍스트 프롬프트 설계 완벽 가이드
LLM을 다루는 데 필수적인 토큰, 컨텍스트 윈도우, 프롬프트 설계의 핵심 개념을 배웁니다. 에이전트 AI 엔지니어로 성장하기 위해 반드시 알아야 할 기초를 다집니다.
Python 기본 사항 프로젝트 구조와 타이핑 완벽 가이드
AI 에이전트 엔지니어를 위한 Python 프로젝트 구조 설계부터 타입 힌팅까지, 초보자가 반드시 알아야 할 핵심 기초를 다룹니다. 깔끔한 프로젝트 구조와 타입 안전성으로 생산성을 높여보세요.
에이전트 AI 엔지니어 로드맵 소개 및 학습 방법
2026년 에이전트 AI 엔지니어가 되기 위한 완벽 로드맵을 소개합니다. Python 기초부터 고급 에이전트 아키텍처, RAG 시스템, 다중 에이전트까지 16개 소주제로 구성된 코스의 전체 흐름과 학습 방법을 안내합니다.
Python 고급 테스트와 프로덕션 실전 가이드
AI 에이전트 개발자가 알아야 할 Python 고급 테스트 전략, 재현성 보장, 프로덕션 배포 시 흔히 겪는 함정을 다룹니다. 초급 개발자가 실무에서 바로 적용할 수 있는 실전 노하우를 전달합니다.