Tokenisation: words, characters, subwords

Tokenisation: words, characters, subwords

20 min read

The previous lesson, on the problem of representing language, ended with a loose thread: there we wrote the vocabulary VV as if its elements came ready-made. They don't. That list has to be built, and building it forces an earlier decision: where one unit of text ends and the next begins. Nobody makes that decision for you, and the language does not come with it settled. This is called tokenisation, and each unit it produces is a token.

Tokenising is choosing where to cut, and there are three natural answers. Cut by words, which is how you read. Cut by characters, which is how the file is stored. Or cut by word-pieces, by subwords: a middle path that can look unintuitive at first but that most modern language models use today. The explorable below cuts one sentence all three ways and shows, next to each cut, how many tokens that same text takes. Write a sentence of your own and compare the three numbers.

The same sentence, three cuts. See how many tokens each one produces, and what happens to a long word like "tokenisation".

Each cut produces a different vocabulary, and sequences of very different lengths. Hold on to that before going on: there is no true tokenisation waiting to be discovered.

The trade-off between vocabulary size and sequence length

Fix the alphabet Σ\Sigma first: the set of characters that can appear. For English that is the letters, the digits, the punctuation and the space, plus the accented letters that ride in on borrowed words like naïve and café.

That set has a trap worth disarming before counting anything. Unicode allows two spellings for the same accented letter: the ï in naïve can be a single character, or an i followed by a diaeresis drawn over the one before it. On screen they look the same; to Σ\Sigma they are different things, and one of the two takes two positions instead of one. So the text is normalised before tokenising, to the form in which each accented letter is a single character: its Normalization Form C (NFC). Without that step, two texts identical to the eye produce different tokenisations, and the vocabulary ends up holding stray diacritics.

A text isn't one character but a run of them, and to talk about all the strings that can be written over a set of symbols there is a standard notation, which we will use from here on. It is written with an asterisk: Σ\Sigma^{*} is the set of all finite strings made of characters from Σ\Sigma, the empty string included. naïve, tokenisation and The naïve boy plays. are all elements of Σ\Sigma^{*}; the asterisk says nothing about length, only that it is finite. When the empty string gets in the way we write Σ+\Sigma^{+}, which is the same thing without it. And the asterisk works for any set, not only the alphabet: VV^{*} is the set of finite sequences of tokens from VV.

With that, a text is an element of Σ\Sigma^{*} and a tokeniser is a function that turns it into a sequence of vocabulary elements:

τ:ΣV,VΣ+.\tau : \Sigma^{*} \to V^{*}, \qquad V \subset \Sigma^{+}.

Each token is therefore a non-empty string of characters, and the vocabulary VV is not declared by hand: τ\tau induces it by running over a corpus, the collection of texts gathered in advance to build it.

From that comes something the rest of the block takes for granted. Each entry of VV is a token, and what it is depends on τ\tau: with a word tokeniser the entries are whole words, and with a subword one they are word-pieces, so that unhappiness can span three. A vocabulary entry is therefore not the same thing as a word.

We ask two things of τ\tau. Determinism, for the same reason as in the previous lesson, on representing language: if τ\tau cut differently today than yesterday, what the model learned would stop matching what it receives. And reconstruction: concatenating the tokens has to give back the original text, save whatever convention was adopted for the spaces. That is what separates cutting from cleaning. A tokeniser that also lowercases everything, or strips the accents, no longer divides the text but alters it: after that résumé and resume are the same unit, and the distinction was lost before the model ever saw it. Everything τ\tau discards, the model will never see again.

Now the trade-off itself, which falls out of counting characters. Take a corpus with CC characters in all, summed over its documents, and apply a tokenisation τ\tau that breaks it into TT tokens u1,,uTu_1, \dots, u_T. Write u\lvert u \rvert for the length, in characters, of the token uu. The tokens are pieces of the text itself, so if τ\tau keeps every character (the reconstruction condition with no conventions), their lengths sum to the whole text:

i=1Tui=C.\sum_{i=1}^{T} \lvert u_i \rvert = C.

If it discards the spaces, the sum falls a little short of CC, and you will see that in the cell.

Write ˉ\bar{\ell} for the mean length of a token:

ˉ=1Ti=1Tui.\bar{\ell} = \frac{1}{T}\sum_{i=1}^{T} \lvert u_i \rvert.

The sum there is the one we set equal to CC a moment ago, so

ˉ=CT,or, what is the same,T=Cˉ.\bar{\ell} = \frac{C}{T}, \qquad \text{or, what is the same,} \qquad T = \frac{C}{\bar{\ell}}.

This identity sums up the first half of the trade-off. The text fixes CC, so TT and ˉ\bar{\ell} cannot vary independently. Choose shorter tokens and ˉ\bar{\ell} drops while the sequence holds more tokens; choose longer ones and the opposite happens. With a character tokeniser, ˉ=1\bar{\ell} = 1 and T=CT = C. With a word tokeniser, ˉ\bar{\ell} runs to around four or five characters, so the sequence comes out roughly that many times shorter.

And the other end of the trade-off, the size of VV? If we fix a length \ell, the number of distinct strings that can be formed over an alphabet Σ\Sigma is

Σ.\lvert \Sigma \rvert^{\ell}.

That is, the pool of candidates for the vocabulary grows exponentially with the length of the units. The language uses only a small fraction of those combinations (the vast majority of five-letter strings are not English words), but the tendency is the same: the longer the chosen units, the larger the space the vocabulary is drawn from, and so the larger it tends to be.

τ\tauˉ\bar{\ell}TTtypical V\lvert V \rvert
characters11CCdozens
subwords3344C/3.5\approx C/3.530,000–50,000, chosen in advance
words5\approx 5C/5\approx C/5hundreds of thousands, and growing
Why the word vocabulary is not only large, but unbounded

In that table, the word vocabulary is not only the largest; it is the one that never stops growing, and that is the difference that matters. The character vocabulary saturates: the distinct symbols of English number a few dozen, and a couple of pages of text bring out almost all of them, so a million more pages add practically none. The word vocabulary never saturates, and English keeps it growing without ever leaning on inflection to do so.

English inflects lightly. Take cat, cats; or walk, walks, walked, walking: four or five forms, where a heavily inflected language runs to dozens per verb. What grows the vocabulary without bound is elsewhere. Compounds form freely and close up into single tokens (database, firewall, smartphone), and two nouns can make a new one tomorrow. Proper nouns are an open set by definition: every person, place, product and brand is another entry, and new ones are minted daily. And the language coins words outright (cryptocurrency did not exist a generation ago), alongside the numbers, hashtags, URLs and misspellings that no dictionary ever held. Each one is a distinct entry of VV that shares nothing with the others, because the word vocabulary's only notion of identity is string equality: it has no idea that cat and cats are related. The list grows with every new document you read.

That endless growth is what turns the question of what to do with a word that is not in VV from a rare case into the normal one, and it is the subject of a later lesson, on vocabulary, frequency and out-of-vocabulary words.

Fixing the vocabulary and letting the algorithm choose the units

The answer to this trade-off is the middle row of the table, the subwords, and its V\lvert V \rvert cell says what it amounts to: chosen in advance. Instead of letting the vocabulary size fall out of the tokenisation, you fix it ahead of time (say, 30,000 tokens) and let an algorithm decide which units will fill it. The most widespread is byte-pair encoding (BPE): it starts by treating each character as a unit and repeatedly merges the most frequent adjacent pair in the corpus, until it reaches the fixed size. Frequent sequences end up fused into a single piece, and rare ones stay split into pieces that are themselves frequent. That is why a word the algorithm never saw still has a representation: it is written with known pieces. The details of BPE are out of scope here; the explorable above shows its result, which is what matters here.

Two tokenisers in Python, over the same corpus

The identity T=C/ˉT = C/\bar{\ell} is worth seeing as numbers rather than algebra. The cell tokenises a short English corpus both extreme ways, by words and by characters, and for each prints TT, the vocabulary size and the mean length. Look at three things: that TˉT \cdot \bar{\ell} reproduces CC, that the vocabulary sizes run opposite to TT, and the last two lines. Run it as it stands, then swap the corpus for a text of your own.

import unicodedata

CORPUS = " ".join([
"The boy teaches programming at the school.",
"The girl learns programming in the classroom.",
"Give me the book, please: it belongs to the teacher.",
"We go to the river and then to the sea every summer.",
"The children play, sing and learn while the sun goes down.",
"Teaching is not the same as learning, though they happen at once.",
])

def normalise(text):
# NFC: an accented letter stays a SINGLE character
return unicodedata.normalize("NFC", text)

def by_chars(text):
return list(normalise(text))

def by_words(text):
tokens, current = [], ""
for c in normalise(text):
if c.isalnum():
current = current + c
continue
if current:
tokens.append(current)
current = ""
if not c.isspace():
tokens.append(c) # punctuation is a token of its own
if current:
tokens.append(current)
return tokens

C = len(normalise(CORPUS))
print("C =", C, "characters")

for name, tokens in [("words", by_words(CORPUS)), ("characters", by_chars(CORPUS))]:
T = len(tokens)
mean = sum(len(t) for t in tokens) / T
print(name.rjust(10),
"| T =", str(T).rjust(3),
"| |V| =", str(len(set(tokens))).rjust(3),
"| mean =", round(mean, 2),
"| T x mean =", round(T * mean, 1))

print()
print(by_words("Don't go: it's the teacher's book."))

# And what happens the moment a sentence the corpus did not contain arrives
V = set(by_words(CORPUS))
new = by_words("The boy programs a cryptocurrency")
print("outside V:", [t for t in new if t not in V])

The first run downloads the Python interpreter (~15 MB). After that it stays in the browser cache and is reused across every lesson.

With characters, TˉT \cdot \bar{\ell} gives CC exactly. With words it falls short, and the difference is the spaces: the tokeniser threw them away, so the text can no longer be reconstructed from the tokens. That is the reconstruction condition broken for the sake of convenience, and it is why subword tokenisers mark, in one way or another, where the spaces were. The ## you see in the explorable marks that a piece continues the word before it (the absence of a space), and with that the space is recovered.

The second-to-last line cuts a sentence full of contractions and possessives: Don't comes out as three tokens (Don, an apostrophe, t), and the possessive 's is torn off teacher the same way. The tokeniser cuts at every character that is not a letter; it has no notion that an apostrophe can bind a contraction or a possessive, and it does not need one, as long as it always cuts the same way. The last line is more uncomfortable: of the five tokens of a new and perfectly ordinary sentence, three are not in VV.

Test your intuition

Four questions on what you have seen: the trade-off between vocabulary and length, what cutting below the word buys you, the trap Unicode has in store for a character tokeniser, and one count by hand.

You swap a word tokeniser for a character tokeniser, over the same corpus. What happens to the vocabulary size V\lvert V \rvert and to the length TT of each sequence?

The word unhappiness doesn't appear in the corpus you built VV from. But un, happi and ness do. Which tokenisers can represent it without treating it as an unknown unit? Tick all that apply.

Select every correct option. This is graded all-or-nothing: there is no partial credit.

The text arrives with the ï in naïve written as two code points: an i followed by a combining diaeresis. Without normalising to NFC first, a character tokeniser produces two tokens where there should be one.

How many tokens does a character tokeniser produce on the sentence The naïve boy plays.? Count the spaces and the final full stop too.

A margin of ±0 is accepted.


With the tokenisation settled, the vocabulary stops being an abstraction and becomes a concrete list of tokens. That list is always finite, because it is built from a corpus somebody gathered at some point. And here the demand for totality from the lesson on the problem of representing language comes back: rr had to be defined for any word that arrives. The last line of the cell shows the hole with nothing to soften it: cryptocurrency is not in VV, and on it rr is worth nothing, because it is not even defined.

That leaves three linked questions, and they are the three of the next lesson. How many distinct tokens really appear in a corpus, and how often? The answer has a very characteristic shape that decides everything else. Which of them make it into VV: keeping them all is expensive, because each needs its own representation r(w)r(w), but every token you leave out is text the model can no longer read. And what to do when one of the tokens left out arrives. That is the lesson on vocabulary, frequency and the problem of out-of-vocabulary words (OOV).

Further reading3 sources · 1 paper, 1 book, 1 interactive

Where this lesson comes from, and where to go next. None of it is needed to carry on with the course.

  • Neural Machine Translation of Rare Words with Subword Units
    paperSennrich, Haddow and Birch, 2016arXiv:1508.07909EN

    The BPE algorithm this lesson names but doesn't spell out: repeatedly merge the most frequent pair of pieces up to the fixed vocabulary size. Presented here for machine translation, not the general-purpose tokeniser it became.

  • Speech and Language Processing, ch. 2: Words and Tokens
    bookJurafsky and Martin, 3rd ed., free draftstanford.eduEN

    The reference chapter on tokenisation and normalisation: the same objects as this lesson (Unicode, NFC, BPE) with more examples and less derivation. The whole book is free.

  • Tokenizer Playground
    interactiveTaubytetaubyte.comEN

    Paste your text and watch how models like GPT-4, BERT or T5 split it, with the token count and the tokens-per-character ratio from the lesson. Try it in Spanish: you'll see the pieces multiply against English.