모델은 글자를 읽지 않는다
앞 장에서 우리는 LLM이 긴 답변을 한 번에 쓰는 것이 아니라, 다음 토큰을 하나씩 예측하며 문장을 이어간다는 것을 보았습니다. 이제 자연스럽게 이런 질문이 생깁니다.
그런데 모델은 애초에 "오늘 점심은"이라는 글자를 어떻게 읽을까?
사람에게 문장은 곧 의미입니다. "고양이는 상자를 좋아한다"라는 문장을 보면, 우리는 고양이와 상자와 좋아한다는 관계를 바로 떠올립니다. 하지만 컴퓨터에게 이 문장은 처음부터 의미가 아닙니다. 컴퓨터가 계산할 수 있는 것은 문자열 자체가 아니라 숫자입니다.
LLM도 마찬가지입니다. 겉으로는 글을 읽고 글로 답하지만, 내부에서는 문장이 아니라 숫자가 흐릅니다. 더 정확히 말하면 LLM의 모든 계산은 정수 ID, 벡터, 행렬, 텐서 위에서 일어납니다. 텐서는 숫자들을 담는 다차원 배열이라고 생각하면 됩니다. 1차원 숫자 목록은 벡터, 2차원 숫자 표는 행렬, 그보다 더 복잡한 숫자 묶음까지 포괄해서 텐서라고 부릅니다.
우리가 입력한 문장은 대략 다음 과정을 거쳐 모델 안으로 들어갑니다.
원문 문자열
→ 토큰으로 쪼개기
→ 각 토큰을 정수 ID로 바꾸기
→ 정수 ID 시퀀스를 텐서로 만들기
→ 임베딩 벡터로 바꾸기
→ Transformer가 계산하기이번 장에서 집중할 부분은 앞쪽입니다.
텍스트 → 토큰 → 정수 ID → 텐서이 흐름을 이해하면 LLM이 왜 "문장"이 아니라 "토큰 수"를 기준으로 비용을 계산하는지, 왜 같은 내용도 언어에 따라 토큰 수가 달라지는지, 왜 숫자나 이모지나 코드가 때때로 이상하게 처리되는지 감을 잡을 수 있습니다.

텍스트는 먼저 잘게 쪼개진다
LLM은 원문 문자열을 통째로 처리하지 않습니다. 먼저 텍스트를 작은 조각으로 나눕니다. 이 조각을 토큰(token)이라고 부릅니다. 토큰은 모델이 읽고 쓰는 기본 단위입니다.
예를 들어 다음 문장이 있다고 해봅시다.
I love cats.가장 단순하게는 이렇게 나눌 수 있습니다.
["I", "love", "cats", "."]하지만 실제 LLM의 토큰화는 이보다 조금 더 복잡합니다. 어떤 모델은 공백을 토큰에 포함하기도 하고, 어떤 모델은 자주 나오는 단어를 하나의 토큰으로 유지하지만 드문 단어는 여러 조각으로 나누기도 합니다. 예를 들어 tokenization이라는 단어는 어떤 토크나이저에서는 하나의 토큰이 아니라 token과 ization 같은 하위 조각으로 나뉠 수 있습니다.
여기서 토크나이저(tokenizer)라는 도구가 등장합니다. 토크나이저는 텍스트를 모델이 처리할 수 있는 형태로 바꿔주는 변환기입니다. Hugging Face의 LLM Course에서도 토크나이저의 역할을 "텍스트를 모델이 처리할 수 있는 숫자 데이터로 번역하는 것"으로 설명합니다.1
토크나이저는 보통 두 가지 일을 차례로 합니다.
텍스트를 토큰으로 나눈다.
각 토큰을 어휘 사전에 있는 정수 ID로 바꾼다.
중요한 점은 토큰화 방식이 모델마다 다를 수 있다는 것입니다. 같은 문장이라도 GPT 계열 모델, BERT 계열 모델, Llama 계열 모델이 서로 다르게 쪼갤 수 있습니다. 그래서 어떤 모델을 사용할 때는 그 모델이 학습될 때 사용한 토크나이저를 함께 사용해야 합니다. 모델이 A 방식으로 쪼갠 텍스트로 학습했는데, 우리가 B 방식으로 쪼개서 넣으면 모델 입장에서는 낯선 입력을 받는 셈이 됩니다.
문자, 단어, 하위 단어
텍스트를 쪼개는 방법은 여러 가지입니다. 가장 쉽게 떠올릴 수 있는 방식은 문자 단위입니다.
"cat" → ["c", "a", "t"]문자 단위 토큰화의 장점은 단순하다는 것입니다. 영어 알파벳, 숫자, 문장 부호처럼 필요한 문자만 어휘 사전에 넣으면 대부분의 단어를 표현할 수 있습니다. 처음부터 모르는 단어가 나와도 문자 조합으로 나타낼 수 있습니다. "supercalifragilisticexpialidocious"처럼 긴 단어도 결국 문자로 쪼개면 처리할 수 있습니다.
하지만 단점도 큽니다. 문장 하나가 너무 많은 토큰으로 늘어납니다. internationalization 같은 단어를 문자 단위로 나누면 토큰이 스무 개가 넘습니다. 모델은 더 긴 시퀀스를 처리해야 하고, 계산량도 늘어납니다. 게다가 문자 하나는 의미가 약합니다. c 하나만 보고 고양이를 떠올리기는 어렵습니다.
반대로 단어 단위 토큰화도 생각할 수 있습니다.
"I love cats" → ["I", "love", "cats"]단어 단위는 직관적입니다. 사람이 문장을 읽는 방식과 비슷해 보입니다. 토큰 수가 문자 단위보다 훨씬 적고, 각 토큰이 어느 정도 의미를 갖습니다. 하지만 이 방식에도 문제가 있습니다. 세상에는 단어가 너무 많습니다. 고유명사, 신조어, 오타, 합성어, 전문 용어까지 모두 어휘 사전에 넣으려면 어휘가 끝없이 커집니다. cat과 cats, run과 running도 처음에는 전혀 다른 토큰으로 취급됩니다.
그래서 현대 LLM은 대체로 하위 단어(subword) 기반 토큰화를 사용합니다. 하위 단어 방식은 문자 단위와 단어 단위의 중간쯤에 있습니다. 자주 나오는 단어는 하나의 토큰으로 유지하고, 드물거나 긴 단어는 의미 있는 조각으로 나눕니다. Hugging Face 문서도 하위 단어 토큰화가 자주 쓰이는 단어는 유지하고 드문 단어는 더 작은 조각으로 분해하는 원리에 기반한다고 설명합니다.2
예를 들어 이런 식입니다.
"cats" → ["cat", "s"]
"running" → ["run", "ning"]
"unbelievable" → ["un", "believ", "able"]실제 결과는 토크나이저마다 다릅니다. 어떤 토크나이저는 running을 하나로 볼 수도 있고, 어떤 토크나이저는 더 잘게 나눌 수도 있습니다. 중요한 것은 원리입니다. 하위 단어 방식은 너무 큰 어휘 사전을 피하면서도, 문자 단위보다 효율적으로 의미 있는 조각을 다루려는 절충안입니다.
현대 LLM에서 자주 만나는 BPE, WordPiece, SentencePiece 같은 이름은 모두 이런 문제를 풀기 위한 토큰화 방식입니다. BPE는 자주 함께 등장하는 문자나 바이트 조각을 반복적으로 합쳐 어휘를 만드는 방식이고, WordPiece는 BERT 계열 모델에서 널리 알려진 하위 단어 방식입니다. 이름이 조금 낯설어도 지금은 이렇게만 기억하면 됩니다.
현대 LLM은 대체로 단어를 그대로 읽지 않는다. 자주 나오는 조각을 기준으로 텍스트를 토큰으로 나눈다.
토큰은 정수 ID가 된다
토큰으로 나누기만 해서는 아직 모델이 계산할 수 없습니다. 토큰은 여전히 문자열이기 때문입니다. 이제 각 토큰을 숫자로 바꿔야 합니다.
토크나이저에는 어휘 사전(vocabulary)이 있습니다. 어휘 사전은 모델이 알고 있는 토큰 목록입니다. 각 토큰에는 고유한 정수 ID가 붙어 있습니다.
예를 들어 아주 작은 어휘 사전이 있다고 합시다.
| 토큰 | 정수 ID |
|---|---|
<pad> | 0 |
오늘 | 1 |
점심 | 2 |
은 | 3 |
김치 | 4 |
찌개 | 5 |
입니다 | 6 |
. | 7 |
그러면 다음 문장은 이렇게 바뀔 수 있습니다.
오늘 점심은 김치찌개입니다.토큰화 결과를 단순화해서 다음처럼 가정해봅시다.
["오늘", "점심", "은", "김치", "찌개", "입니다", "."]각 토큰을 어휘 사전의 ID로 바꾸면 다음과 같습니다.
[1, 2, 3, 4, 5, 6, 7]이제 드디어 모델이 다룰 수 있는 숫자 시퀀스가 되었습니다.
하지만 여기서 오해하면 안 되는 지점이 있습니다. 오늘의 ID가 1이고 점심의 ID가 2라고 해서, 점심이 오늘보다 두 배 더 큰 의미를 가진다는 뜻은 아닙니다. 토큰 ID는 의미의 크기나 순서를 나타내는 숫자가 아닙니다. 그냥 어휘 사전에서 "몇 번째 항목인가"를 가리키는 번호입니다.
비유하자면 학생 번호와 비슷합니다. 7번 학생이 3번 학생보다 두 배 똑똑하다는 뜻이 아닙니다. 단지 출석부에서 7번이라는 뜻입니다. 토큰 ID도 마찬가지입니다. cat의 ID가 9246이고 dog의 ID가 5679라고 해서, 그 숫자 자체가 고양이와 강아지의 의미 관계를 담고 있는 것은 아닙니다.
의미 관계는 다음 단계에서 만들어집니다. 모델은 정수 ID를 보고 임베딩 벡터라는 숫자 묶음을 꺼냅니다. 이 벡터가 학습을 통해 조정되면서 토큰 사이의 의미적 관계가 점차 담기게 됩니다. 임베딩은 다음 장들에서 더 자세히 다루겠습니다. 지금은 토큰 ID가 "의미 있는 숫자"가 아니라 "벡터를 찾기 위한 주소"에 가깝다고 이해하면 좋습니다.
정수 시퀀스는 텐서가 된다
이제 문장은 정수 목록이 되었습니다.
[1, 2, 3, 4, 5, 6, 7]하지만 모델은 보통 문장 하나만 처리하지 않습니다. 학습할 때는 수많은 문장을 묶어서 처리하고, API에서도 내부적으로 입력을 일정한 구조로 만들어 계산합니다. 그래서 정수 목록은 텐서 형태로 정리됩니다.
예를 들어 세 문장이 있다고 합시다.
문장 A: 오늘 점심은 김치찌개입니다.
문장 B: 내일은 비가 옵니다.
문장 C: 안녕하세요.각 문장을 토큰 ID로 바꾸면 길이가 서로 다를 수 있습니다.
A: [1, 2, 3, 4, 5, 6, 7]
B: [8, 3, 9, 10, 6, 7]
C: [11, 7]컴퓨터가 여러 문장을 한 묶음으로 빠르게 계산하려면 길이를 맞추는 편이 좋습니다. 그래서 짧은 문장 뒤에 <pad> 같은 패딩 토큰을 붙입니다.
A: [1, 2, 3, 4, 5, 6, 7]
B: [8, 3, 9, 10, 6, 7, 0]
C: [11, 7, 0, 0, 0, 0, 0]이제 모양이 맞는 숫자 표가 되었습니다.
[
[1, 2, 3, 4, 5, 6, 7],
[8, 3, 9, 10, 6, 7, 0],
[11, 7, 0, 0, 0, 0, 0]
]이런 형태가 모델 입력의 기본 모습입니다. 실제 프레임워크에서는 이를 PyTorch 텐서나 TensorFlow 텐서로 다룹니다. 보통 모양은 다음처럼 표현합니다.
(batch_size, sequence_length)배치 크기는 한 번에 처리하는 문장 개수이고, 시퀀스 길이는 각 문장의 토큰 수입니다. 위 예시는 세 문장을 한 번에 처리하고 각 문장의 길이를 7로 맞췄으므로 다음과 같은 모양입니다.
(3, 7)여기서 패딩 토큰은 실제 의미가 없는 자리 채우기입니다. 그래서 모델에게 "이 부분은 진짜 입력이 아니니 너무 신경 쓰지 마"라고 알려주는 장치가 필요합니다. 이것이 attention mask입니다. 실제 토큰은 1, 패딩은 0으로 표시하는 식입니다.
input_ids:
[11, 7, 0, 0, 0, 0, 0]
attention_mask:
[ 1, 1, 0, 0, 0, 0, 0]이 장에서는 attention mask를 깊게 이해할 필요는 없습니다. 지금은 다양한 길이의 문장을 같은 모양의 숫자 배열로 만들기 위해 패딩이 쓰이고, 패딩 자리를 구분하기 위해 마스크가 함께 쓰인다는 정도만 기억하면 됩니다.
같은 문장도 모델마다 다르게 보인다
토큰화에서 가장 재미있는 점은, 우리가 보기에는 같은 문장이라도 모델마다 전혀 다르게 보일 수 있다는 것입니다.
예를 들어 다음 문장을 생각해봅시다.
Using a Transformer network is simpleHugging Face의 예시에서 BERT 토크나이저는 이 문장을 다음과 비슷하게 나눕니다.3
["Using", "a", "transform", "##er", "network", "is", "simple"]여기서 ##er는 앞의 토큰에 이어 붙는 조각이라는 표시입니다. 즉 Transformer라는 단어를 transform과 er로 나눈 것입니다. 같은 단어라도 어휘 사전에 Transformer 전체가 없거나, 토크나이저의 규칙상 더 작은 조각으로 나누는 편이 적절하다고 판단되면 이렇게 쪼개집니다.
GPT 계열 토크나이저는 또 다른 방식으로 공백을 다룰 수 있습니다. 어떤 토큰은 앞의 공백까지 포함합니다. 예를 들어 " the"처럼 공백이 붙은 토큰과 "the"처럼 공백이 없는 토큰이 서로 다른 ID를 가질 수 있습니다. 사람이 보기에는 "the"라는 같은 단어처럼 보여도, 모델 입장에서는 앞에 공백이 있는지 없는지가 다른 토큰으로 나타날 수 있습니다.
이 때문에 프롬프트의 작은 차이가 토큰화 결과를 바꿀 수 있습니다. 줄바꿈 하나, 공백 하나, 문장 부호 하나가 토큰 수와 토큰 ID를 바꿀 수 있습니다. 물론 대부분의 경우 큰 문제가 되지는 않습니다. 하지만 구조화 출력, 코드 생성, 긴 문서 처리, 비용 최적화처럼 세밀한 제어가 필요한 상황에서는 이런 차이가 중요해집니다.
한국어, 코드, 이모지는 어떻게 나뉠까
영어 문장은 공백을 기준으로 단어 경계가 비교적 뚜렷합니다. 하지만 모든 언어가 영어처럼 생긴 것은 아닙니다. 한국어는 조사와 어미가 붙고, 중국어와 일본어는 공백 사용 방식이 영어와 다릅니다. 이모지는 한 글자처럼 보이지만 내부적으로는 여러 바이트로 표현될 수 있습니다. 코드는 괄호, 점, 언더스코어, 들여쓰기, 따옴표 같은 기호가 매우 중요합니다.
다음 네 가지 입력을 생각해봅시다.
Hello, world!
안녕하세요, 반갑습니다.
for i in range(10): print(i)
오늘 기분은 😊입니다.사람에게는 모두 자연스러운 텍스트입니다. 하지만 토크나이저에게는 각각 다른 성격의 입력입니다.
영어 문장은 자주 등장하는 단어와 공백 패턴 덕분에 비교적 적은 토큰으로 표현될 수 있습니다. 한국어 문장은 사용하는 토크나이저가 한국어 데이터를 얼마나 잘 반영했는지에 따라 토큰 수가 크게 달라질 수 있습니다. 코드 조각은 공백, 괄호, 콜론, 줄바꿈이 모두 의미를 가지므로 일반 문장과 다른 방식으로 쪼개질 수 있습니다. 이모지는 하나의 토큰으로 처리될 수도 있지만, 토크나이저에 따라 여러 조각으로 나뉠 수도 있습니다.
그래서 "글자 수가 짧으니 토큰 수도 적겠지"라고 단정하면 위험합니다. 어떤 문자열은 짧아 보여도 토큰이 많이 나올 수 있고, 어떤 긴 영어 표현은 자주 등장하는 패턴이라 적은 토큰으로 압축될 수 있습니다.
특히 한국어 사용자는 이 점을 체감할 때가 많습니다. 영어로는 짧게 토큰화되는 표현이 한국어에서는 더 많은 토큰으로 나뉘기도 합니다. 이는 특정 언어가 "나쁘다"는 뜻이 아니라, 토크나이저의 어휘 사전과 학습 데이터 분포가 어떤 조각을 자주 만났는지에 따라 효율이 달라진다는 뜻입니다.
숫자는 생각보다 까다롭다
숫자는 더 흥미로운 문제를 만듭니다. 사람에게 12345는 하나의 수입니다. 우리는 왼쪽부터 읽으면서도 자리값을 이해합니다. 1은 만의 자리, 2는 천의 자리, 3은 백의 자리라는 식입니다.
하지만 LLM에게 숫자는 텍스트입니다. 12345가 하나의 토큰으로 들어갈 수도 있고, 123과 45로 나뉠 수도 있고, 1, 2, 3, 4, 5처럼 한 자리씩 나뉠 수도 있습니다. 어떤 방식으로 나뉘는지는 토크나이저의 어휘와 규칙에 달려 있습니다.
이 차이는 단순한 표현상의 문제가 아닙니다. 숫자가 어떻게 쪼개지는지는 모델의 계산과 추론 난이도에도 영향을 줄 수 있습니다. 예를 들어 큰 수를 왼쪽에서 오른쪽으로 읽는 모델은 앞자리 숫자를 볼 때 아직 전체 자릿수를 모를 수 있습니다. 1을 봤을 때 그것이 1인지, 10의 일부인지, 1000000의 첫 자리인지는 뒤를 더 봐야 분명해집니다. 숫자 표현 방식이 LLM의 산술 능력에 영향을 줄 수 있다는 연구도 있으며, NumeroLogic 논문은 숫자 앞에 자릿수 정보를 붙이는 방식이 숫자 추론에 도움이 될 수 있다고 제안합니다.4
물론 우리가 일반적인 LLM API를 사용할 때 숫자 표기법을 매번 바꿔야 한다는 뜻은 아닙니다. 중요한 것은 LLM이 숫자를 계산기처럼 직접 "이해"하는 것이 아니라, 텍스트 조각의 시퀀스로 처리한다는 사실입니다. 그래서 정확한 계산이 필요하다면 모델에게 암산을 맡기기보다 계산 도구를 연결하는 편이 안전합니다. 뒤에서 Function Calling과 Tool Use를 배울 때 이 문제가 다시 등장합니다.
토큰 수는 곧 비용과 길이다
LLM 서비스를 만들 때 토큰 수는 아주 현실적인 의미를 갖습니다. 토큰 수는 다음 세 가지를 직접 결정합니다.
API 비용
컨텍스트 윈도우 사용량
입력과 출력의 최대 길이
LLM API는 대체로 글자 수가 아니라 토큰 수를 기준으로 비용을 계산합니다. 입력 토큰이 많을수록 비용이 늘고, 출력 토큰이 많을수록 비용이 늘어납니다. "짧은 문장 하나"라고 생각했는데 토큰 수가 많으면 예상보다 비용이 커질 수 있습니다.
또 하나 중요한 개념이 컨텍스트 윈도우(context window)입니다. 컨텍스트 윈도우는 모델이 한 번에 볼 수 있는 토큰의 최대 범위입니다. GPT-2 설정 예시를 보면 어휘 크기는 50,257이고 최대 위치 수는 1,024로 정의되어 있습니다.5 오늘날의 모델들은 이보다 훨씬 긴 컨텍스트를 제공하기도 하지만, 원리는 같습니다. 모델마다 한 번에 처리할 수 있는 토큰 수에는 제한이 있습니다.
예를 들어 어떤 모델의 컨텍스트 윈도우가 8,000토큰이라고 합시다. 사용자의 입력 문서가 7,500토큰이면 모델이 답변을 생성할 수 있는 여유는 500토큰밖에 남지 않습니다. 여기에 시스템 메시지, 개발자 지시문, 대화 기록, 검색으로 가져온 문서 조각까지 포함되면 공간은 더 줄어듭니다.
RAG를 만들 때도 이 문제가 매우 중요합니다. 문서를 많이 가져오면 답변에 필요한 근거가 풍부해질 것 같지만, 너무 많이 가져오면 컨텍스트 윈도우를 꽉 채워 정작 답변할 공간이 줄어듭니다. 반대로 너무 적게 가져오면 필요한 근거를 놓칠 수 있습니다. 결국 좋은 LLM 서비스는 "무엇을 넣을까"뿐 아니라 "얼마나 많은 토큰으로 넣을까"도 설계해야 합니다.
그래서 실무에서는 같은 의미라면 더 간결한 표현이 유리할 때가 많습니다. 프롬프트가 짧고 명확할수록 비용과 속도 면에서 이점이 생깁니다. 다만 무조건 짧게만 쓰는 것이 답은 아닙니다. 필요한 조건을 빼먹어 모델이 엉뚱한 답을 내면, 오히려 재시도 비용이 더 커집니다. 핵심은 짧게 쓰는 것이 아니라 불필요한 토큰을 줄이되 필요한 정보는 남기는 것입니다.
직접 확인해보기: 토큰과 ID 보기
이제 실제로 텍스트가 어떻게 숫자가 되는지 확인해봅시다. Python 환경이 있다면 Hugging Face Transformers를 사용할 수 있습니다. 아직 실습 환경을 준비하지 않았다면 코드를 완벽히 이해하지 않아도 괜찮습니다. 지금은 출력의 모양만 보면 됩니다.
from transformers import AutoTokenizer
model_name = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_name)
text = "Using a Transformer network is simple"
# 1. 텍스트를 토큰으로 나누기
tokens = tokenizer.tokenize(text)
print(tokens)
# 2. 토큰을 정수 ID로 바꾸기
ids = tokenizer.convert_tokens_to_ids(tokens)
print(ids)
# 3. 모델 입력 형태로 한 번에 만들기
encoded = tokenizer(text, return_tensors="pt")
print(encoded["input_ids"])
print(encoded["attention_mask"])실행하면 대략 다음과 같은 흐름을 볼 수 있습니다.
텍스트:
Using a Transformer network is simple
토큰:
['Using', 'a', 'transform', '##er', 'network', 'is', 'simple']
정수 ID:
[7993, 170, 11303, 1200, 2443, 1110, 3014]tokenizer(text, return_tensors="pt")를 호출하면 토큰화와 ID 변환뿐 아니라, 모델에 넣기 좋은 텐서 형태까지 만들어줍니다. 출력에는 input_ids가 들어 있고, 필요에 따라 attention_mask도 함께 들어 있습니다. Hugging Face 문서에서도 인코딩은 토큰화와 input ID 변환의 두 단계로 이루어진다고 설명합니다.6
GPT 계열 토크나이저도 확인해볼 수 있습니다.
from transformers import AutoTokenizer
model_name = "openai-community/gpt2"
tokenizer = AutoTokenizer.from_pretrained(model_name)
texts = [
"Hello, world!",
"안녕하세요, 반갑습니다.",
"for i in range(10): print(i)",
"오늘 기분은 😊입니다.",
"123456789"
]
for text in texts:
ids = tokenizer.encode(text)
pieces = [tokenizer.decode([i]) for i in ids]
print("원문:", text)
print("토큰 수:", len(ids))
print("ID:", ids)
print("조각:", pieces)
print("-" * 40)여기서 가장 재미있는 부분은 pieces입니다. 각 ID를 다시 디코딩해서 모델이 본 조각을 확인합니다. 어떤 입력은 예상보다 잘게 쪼개지고, 어떤 입력은 생각보다 큰 덩어리로 묶일 것입니다. 한국어, 코드, 숫자, 이모지를 바꿔 넣어보면 토크나이저가 세상을 어떻게 조각내는지 조금씩 보이기 시작합니다.
OpenAI 모델을 사용할 때는 OpenAI의 토크나이저 도구나 tiktoken 같은 패키지로 토큰 수를 확인할 수 있습니다. OpenAI 문서도 프로그래밍 방식으로 토큰화를 다루려면 Python용 tiktoken 패키지를 확인하라고 안내합니다.7
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
text = "오늘 기분은 😊입니다."
ids = enc.encode(text)
pieces = [enc.decode([i]) for i in ids]
print(len(ids))
print(ids)
print(pieces)실제 서비스 개발에서는 이런 확인 습관이 생각보다 중요합니다. 프롬프트가 너무 길어졌을 때 어디서 토큰이 많이 쓰이는지, RAG에서 문서 조각 하나가 몇 토큰인지, 한국어 안내문과 영어 안내문 중 어느 쪽이 더 효율적인지 직접 확인할 수 있기 때문입니다.
디코딩: 숫자는 다시 텍스트가 된다
지금까지는 텍스트가 숫자로 바뀌는 방향을 보았습니다.
텍스트 → 토큰 → 정수 ID생성 과정에서는 반대 방향도 일어납니다. 모델은 다음 토큰의 ID를 예측합니다. 예를 들어 다음 ID를 골랐다고 합시다.
[1, 2, 3, 4, 5, 6, 7]토크나이저는 이 ID들을 다시 토큰 문자열로 바꾸고, 토큰 조각들을 이어 붙여 사람이 읽을 수 있는 텍스트를 만듭니다.
정수 ID → 토큰 → 텍스트이 과정을 디코딩(decoding)이라고 합니다.
ids = tokenizer.encode("Hello, world!")
text = tokenizer.decode(ids)
print(ids)
print(text)LLM이 답변을 화면에 보여줄 때도 내부적으로는 이런 일이 반복됩니다. 모델이 토큰 ID 하나를 고릅니다. 그 ID를 텍스트 조각으로 바꿉니다. 화면에 이어 붙입니다. 다시 다음 ID를 고릅니다. 그래서 스트리밍 응답에서는 글자가 한 글자씩 나오는 것처럼 보이지만, 실제 단위는 글자가 아니라 토큰일 때가 많습니다.
여기서도 토큰화의 흔적이 나타납니다. 어떤 때는 한 글자씩 나타나는 것 같고, 어떤 때는 단어 덩어리가 한꺼번에 나타나는 것처럼 보입니다. 그것은 모델이 생성한 토큰 하나가 어떤 텍스트 조각에 해당하느냐에 따라 달라집니다.
토큰 ID 다음에는 임베딩이 기다린다
이번 장의 핵심은 단순하지만 중요합니다.
LLM은 텍스트를 직접 계산하지 않는다. 텍스트는 먼저 토큰으로 나뉘고, 각 토큰은 정수 ID가 되며, 이 ID 시퀀스가 텐서 형태로 모델에 들어간다.
하지만 아직 한 가지 중요한 질문이 남아 있습니다.
정수 ID는 단지 출석번호 같은 것이라고 했습니다. 그렇다면 모델은 어떻게 고양이와 강아지가 비슷하고, 고양이와 양자역학은 덜 비슷하다는 것을 알게 될까요? ID 숫자만 보면 그런 관계는 보이지 않습니다. 고양이의 ID가 15342이고 강아지의 ID가 9821이라고 해서 두 단어의 의미가 가까운지 먼지 알 수 없습니다.
이제 다음 단계가 필요합니다. 토큰 ID는 모델 내부의 거대한 표에서 한 줄을 찾는 데 사용됩니다. 그 한 줄에는 수십 개, 수백 개, 때로는 수천 개의 숫자가 들어 있습니다. 이 숫자 묶음이 바로 임베딩 벡터입니다.
토큰 ID는 주소이고, 임베딩 벡터는 그 주소에서 꺼내 온 실제 표현입니다. Transformer가 본격적으로 계산하는 대상은 ID 자체가 아니라 이 벡터들입니다.
다음 장에서는 토큰화와 어휘 사전을 조금 더 자세히 들여다봅니다. 텍스트를 어디서 자를지, 어떤 조각을 어휘 사전에 넣을지, 왜 어떤 단어는 하나의 토큰이 되고 어떤 단어는 여러 토큰으로 쪼개지는지 살펴보겠습니다. 그 다음에는 드디어 토큰 ID가 의미를 품은 벡터로 바뀌는 임베딩의 세계로 들어갑니다.
참고
1Hugging Face LLM Course: Tokenizers
2Hugging Face LLM Course: Subword tokenization
3Hugging Face LLM Course: Tokenization example
4NumeroLogic: Number Encoding for Enhanced LLMs' Numerical Reasoning
5GPT-2 documentation on Hugging Face Transformers
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.