How a text becomes a list of numbers — and why the model sees neither letters nor words, but pieces.
A neural network is, at bottom, a matrix-multiplying machine. Matrices are made of numbers. So the first thing that must happen to your text is a translation: letters become numbers. This step is called tokenization, and it happens before the model — it is not learned along with it; it is a fixed table, decided once and frozen forever.
The word token is older than language models, and has other meanings in IT: the credential a server issues after login, such as a JWT; the unit a compiler's lexical analyser splits off, such as a keyword; the asset recorded in a blockchain contract. In this course, a token is always a piece of text — a word, part of a word or a byte — with an integer attached. The course glossary makes this distinction for every term that has more than one meaning.
The question is: numbers for what? There are three historical answers, and understanding why the first two failed is the only way to understand why the third won.
Simple: build a dictionary. "casa"→41, "cachorro"→892, and so on. It works until the day a word shows up that is not in the dictionary — a proper name, a typo, a new slang word, a word in another language. Then the model writes <UNK> ("unknown") and loses the information forever. And the dictionary of real English, with names and variants, goes past a million entries — which would make the last layer of the model (which needs one neuron per possible word) bigger on its own than the entire model.
This solves the unknown: there are only ~100 characters, and any text fits. But it creates a worse problem. The word "internacionalização" becomes 19 positions instead of 1. Since the cost of attention grows with the square of the number of positions (you will see this in chapter 03), texts become much more expensive. And the model has to spend several layers just to rediscover that c-a-s-a put together means something.
Here is the insight. What if the vocabulary were made of pieces of words, chosen automatically by the statistics of the text? Common words ("the", "casa") get a single token. Rare words decompose into pieces that exist ("Rafhael" → R + af + ha + el). Nothing is ever unknown, and the text stays short.
That is BPE — Byte Pair Encoding. It is what GPT-2 uses, it is what RoBERTa uses, and variants of it are still used in many current models.
Think of a LEGO parts store that needs to sell the smallest possible number of part types, but still let you build anything. It will not sell only 1×1 bricks (you could build everything, but you would spend 10 thousand pieces per model). It will not sell a single "assembled Millennium Falcon" piece either (it only works for one model). It sells medium-sized pieces that show up a lot: walls, windows, wheels. BPE is the algorithm that discovers, by looking at thousands of already-built models, which medium-sized pieces are worth existing.
BPE is a tokenizer training algorithm. It runs once, over a giant corpus, and produces two things: a list of tokens (vocab.json) and an ordered list of merges (merges.txt). After that, it never changes again.
t appear followed by h? And h followed by e?t+h is the champion, the token th is created, and every t h in the corpus becomes th.th+e may become the champion, creating the.Corpus: the word banana repeated 5 times, and bandana 2 times. Let's follow three merges.
| Step | State of the text | Most frequent pair | Count | New token |
|---|---|---|---|---|
| 0 | b a n a n a ×5 b a n d a n a ×2 | — | — | alphabet: a,b,d,n |
| 1 | count pairs: an=10+4=14, na=10+2=12, ba=7… | a+n | 14 | an |
| 2 | b an an a ×5 b an d an a ×2 | an+a | 7 | ana |
| 3 | b an ana ×5 b an d ana ×2 | b+an | 7 | ban |
| end | ban ana ×5 ban d ana ×2 | "banana" costs 2 tokens; "bandana" costs 3. | ||
The model's vocabulary is a statistical portrait of the training corpus. GPT-2 was trained on WebText — 8 million English-language web pages, collected from links upvoted on Reddit. That is why English is efficient (1 token ≈ 0.75 words) and Portuguese is expensive (1 token ≈ 0.4 words): BPE never had a statistical reason to create the token ção (a very common Portuguese suffix). You will measure this in the lab, and it will sting a little.
Here is GPT-2's specific contribution, and it is elegant. The original BPE (used in machine translation, 2016) started from Unicode characters. Problem: there are more than 149 thousand Unicode characters. If the initial alphabet is already huge, little room is left in the vocabulary for the useful merges — and emojis or Chinese ideograms could still fall out and become <UNK>.
GPT-2's solution: don't start from characters, start from bytes. All digital text, in any language, is a sequence of UTF-8 bytes. And bytes only come in 256 possible values. So:
<UNK> token. It doesn't need one.When you inspect GPT-2's tokens, you will see things like Ġthe and ĠcasaĊ. It looks strange, but it is a simple convention.
Some bytes are control characters or whitespace — printing them directly would break the vocabulary files. So GPT-2 remaps these problematic bytes to visible, harmless Unicode characters:
| Real byte | Value | Becomes | Means |
|---|---|---|---|
| space | 32 (0x20) | Ġ | "a new word started here" |
| line break | 10 (0x0A) | Ċ | end of line |
| tab | 9 (0x09) | ĉ | tab |
It is purely cosmetic — Ġ is literally the space character wearing a hat. But it carries real information: the space is part of the token. Ġcasa (" casa", preceded by a space) and casa (glued to the previous word, as in "minhacasa") are different tokens, with different IDs and different embeddings.
If you write a prompt ending in a space — "O Brasil é " — the model has already received the Ġ on its own and now has to complete a token that begins without a space. That throws it into a strange region of token space and quality drops visibly. Never end a prompt with a space. Write "O Brasil é" and let the model produce the Ġ itself.
BPE training is a sequence of greedy choices. At each step
That pair is merged into a new token merges.txt list at position
Tokenizing a new text does not redo the counts. It only replays the merges, in the same order in which they were learned:
merges.txt is an ordered file, not a set.
And GPT-2's final vocabulary breaks down like this:
<|endoftext|> (ID 50256), used to separate documents in training and to signal the end of generation.
This is a point that confuses many people: RoBERTa uses exactly the same algorithm as GPT-2 — byte-level BPE, 50,000 merges. It was an explicit decision by the RoBERTa authors, who swapped BERT's WordPiece for GPT-2's BPE. But the special tokens and the corpus are different, so the IDs do not match.
GPT-1 (openai-gpt) | GPT-2 (whole family) | RoBERTa (detectors) | |
|---|---|---|---|
| Algorithm | BPE over characters + spaCy/ftfy | BPE over bytes | BPE over bytes |
| Vocabulary | 40,478 | 50,257 | 50,265 |
| Unknown token | <unk> exists | does not exist | does not exist |
| Upper case | everything lowercased | preserved | preserved |
| Special tokens | <unk> | <|endoftext|> | <s> </s> <pad> <unk> <mask> |
| Space marker | </w> at the end of the word | Ġ at the start | Ġ at the start |
| Wraps the sentence | no | no | <s> … </s> |
RoBERTa's 8 extra tokens (50,265 vs 50,257) are precisely the special ones it needs and GPT-2 does not: <pad> to fill batches of different lengths, <mask> for its training objective (chapter 10), and <s>/</s> to mark start and end — and <s> later becomes the vector that carries the summary of the whole sentence for classification (chapter 12).
# tokenizar_na_mao.py — GPT-2's BPE without magic from transformers import AutoTokenizer tok = AutoTokenizer.from_pretrained("openai-community/gpt2") texto = "O Brasil é gigante pela própria natureza." # 1) text -> list of IDs (what the model actually receives) ids = tok.encode(texto) print(ids) # [46, 8290, 2634, 20692, 12427, 27638, 355, 1050, 16864, 3450, 46844, 13] # 2) each ID back to the piece of text it represents for i in ids: print(f"{i:>6} {tok.decode([i])!r:<12} {tok.convert_ids_to_tokens([i])[0]}") # the 3rd field shows the raw form, with the Ġ for the space # 3) the metric that matters: how many tokens per word? palavras = len(texto.split()) print(f"{len(ids)} tokens para {palavras} palavras = {len(ids)/palavras:.2f} tokens/palavra")
The output of line 3 is the moment it clicks. In English, that number sits near 1.3. In Portuguese, near 2.4. In other words: the same text costs almost twice as many tokens in Portuguese — twice the memory, twice the time, half the useful context, and (in paid APIs) twice the price. It is not that the model "dislikes" Portuguese; it is that merges.txt never saw enough Portuguese to create good pieces.
cd GPT-2 .venv\Scripts\python.exe lab\01_tokenizer.py
This script does four things. Watch each one:
Ġ. Look for where the words split — almost always at the suffix." 2024" costs 1 token (it was a frequent year on the 2018 internet), but " 1234567" becomes 3 tokens, split into 123+45+67 — pieces with no arithmetic meaning whatsoever. The model never sees the whole number. That is literally why models of this generation got simple sums wrong: they do not do arithmetic on numbers, but on arbitrary fragments of digits chosen by text frequency.Why "Rafhael" costs more tokens than "the"; why Ġcasa and casa are different things; and why a model that sees 1+000 instead of 1000 has structural difficulty with arithmetic.
"antidisestablishmentarianism" (28 letters, English)? And on "pneumoultramicroscopicossilicovulcanoconiótico" (46 letters, Portuguese)? Write down both guesses, then run. The difference will be bigger than you expect."casa", " casa" and "Casa". How many distinct IDs show up? Why?<|endoftext|>. Run tok.decode([50256]). Now explain why it is the last ID, and not the first.The primary sources for the concepts in this chapter. The numbers in the text were measured in the lab; the ideas come from here.
Technical terms here carry their machine-learning meaning. The course glossary shows that meaning next to the others the same words have in IT.
Now the text is a list of integers. But an integer has no meaning — ID 41 is not "more" or "less" than 40.
In chapter 02 these integers become vectors, and that is where meaning is born.