SentencePiece: Subword Tokenization with BPE and Unigram

Michael BrenndoerferApril 14, 202544 min read

Part of Language AI Handbook

SentencePiece trains BPE and unigram tokenizers directly on raw text. Covers whitespace handling, multilingual vocabularies, and model training.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

SentencePiece: Language-Agnostic Subword Tokenization

Most tokenization systems you encounter in practice are built on top of a preprocessing pipeline: you split on whitespace, apply language-specific rules, maybe run a custom normalizer, and only then feed text to the subword algorithm. This works reasonably well for English but breaks down quickly for Chinese, Japanese, Arabic, or any language where word boundaries are not marked by spaces. SentencePiece takes a different path. It treats raw text as a sequence of Unicode characters, handles whitespace like any other character, and learns token boundaries entirely from data. No language-specific rules required.

Think of SentencePiece as a camera that refuses to put any filter on before taking the picture. Traditional tokenization systems apply language-specific preprocessing that acts like a lens filter, adjusting and shaping the input before the core algorithm ever sees it. SentencePiece removes those filters. The raw image, unprocessed and unbiased, goes directly to the algorithm, which then discovers structure present in the data. This engineering choice means the same tokenizer code, without modification, can handle any human language.

This language-agnostic design is why SentencePiece has become the tokenization library of choice for many of the most important multilingual models: T5, ALBERT, mT5, XLM-RoBERTa, and LLaMA all use it. But SentencePiece is more than just a multilingual tokenizer. It provides a clean, framework-independent implementation of both BPE and the unigram language model, packages them behind a simple training API, and produces compact binary model files that can be deployed to any environment. A tokenizer trained once, in Python, can be loaded and used identically in C++, Java, or any other language that has a SentencePiece binding.

In the previous chapters we covered BPE and the unigram language model as abstract algorithms. This chapter shows you those same algorithms as they operate inside SentencePiece, including the engineering decisions that make them work across languages. We will start with the whitespace handling innovation that makes language-agnostic tokenization possible, then examine both training algorithms in the SentencePiece context, trace through a complete numerical example, and finally look at real production usage with the official library. By the end, you will understand how to use SentencePiece, why it works, and how to make informed decisions when you configure your own tokenizers.

Historical Context

SentencePiece was developed at Google by Taku Kudo and John Richardson and released in 2018. The immediate motivation was multilingual machine translation: Google's neural translation system needed a single tokenizer that could handle all 100-plus language pairs without maintaining separate preprocessing pipelines for each language family. Earlier work on subword tokenization, including BPE (Sennrich et al., 2016) and the unigram language model (Kudo, 2018), required pretokenized input. SentencePiece removed that requirement by incorporating whitespace encoding directly into the algorithm. The original paper, "SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing," demonstrated that pretokenization-free models achieved comparable or better quality on translation tasks while dramatically simplifying the engineering pipeline. Within a year of its release, SentencePiece had been adopted by virtually every major multilingual language model.

Why Pretokenization Is a Problem

Before you can understand SentencePiece's contribution, you need to understand the problem it solves. Pretokenization has been a standard step in NLP pipelines for decades, and its limitations are subtle enough that they often go unnoticed until you try to build something truly multilingual.

Traditional NLP pipelines use a process called pretokenization before applying any subword algorithm. Pretokenization is the step that splits raw text into a sequence of "words" using language-specific rules. For English, you might split on whitespace and punctuation. For German, you need to handle long compound words differently. For Chinese and Japanese, there are no spaces at all, so you need a separate word segmentation model just to define where words begin and end. For Arabic, you deal with clitics and morphological affixes that attach to word roots. Each language family requires specialized knowledge and specialized code.

These pretokenization rules introduce several problems that compound over time. First, they create language-specific dependencies. A tokenizer designed for English often produces garbage output when fed Japanese text, because the English tokenizer's rules about spaces and punctuation do not apply. Maintaining a separate tokenization system for each of the world's major languages is tedious and creates divergence in behavior that makes cross-lingual analysis unreliable. Second, language-specific rules make the tokenizer brittle: a word that was accidentally run together without a space becomes a single opaque unit that the subword algorithm cannot decompose. Third, pretokenization rules bake in assumptions about what a "word" is, assumptions that reflect the researchers' linguistic intuitions rather than the patterns a neural network needs to learn.

The deeper issue is conceptual. When you pretokenize, you commit to a particular segmentation of the input before training has even started. You are making a hard decision about word boundaries based on surface rules, then training the subword algorithm only within those boundaries. If your pretokenization is wrong or inconsistent, those errors propagate into every downstream model you train. Every model trained on that tokenizer inherits those errors and the idiosyncrasies of your pretokenization choices become baked into the representations that the model learns.

There is also a linguistic argument against pretokenization. Languages differ in their scripts and in how morphology works. English is relatively analytic: most meaning is carried by separate words ("walk", "walked", "walking"). Turkish is highly agglutinative: a single word can carry meaning that requires an entire English sentence to express. Finnish, Hungarian, and many other languages fall in between. Pretokenization rules designed for analytic languages do not transfer gracefully to agglutinative ones, because the concept of a "word" is fundamentally different.

SentencePiece eliminates the pretokenization step entirely. It feeds raw text directly to the subword algorithm, with no preprocessing except Unicode normalization. This means the algorithm can discover its own notion of meaningful units from data, including units that span what we might traditionally call word boundaries. In agglutinative languages, the algorithm naturally learns that word stems and common suffixes form useful token boundaries. In character-based languages like Chinese, it learns character-level and bigram-level units without needing an external word segmenter.

Whitespace as a Character: The ▁ Prefix

If you feed raw text to a BPE or unigram algorithm without any whitespace handling, you lose all information about where words begin. The sequence "language model" would be indistinguishable from "languagemodel". The merge algorithm would treat the space like any other character, potentially creating tokens that straddle word boundaries in ways that make no linguistic sense and that would be impossible to decode unambiguously back to the original text.

SentencePiece handles this with a simple but powerful encoding. Before applying any subword algorithm, it replaces every whitespace character with a special Unicode symbol: ▁ (U+2581, the "Lower One Eighth Block"). This symbol is then treated as a regular character that can participate in merges and appear in token boundaries. The choice of U+2581 is deliberate: this block character is visually distinct from all standard punctuation and letters, it is unlikely to appear in natural text, and it survives most Unicode normalization schemes unchanged.

The key insight is that after this substitution, word boundary information is no longer encoded in empty space between characters but instead as a visible character that can participate in the algorithm on equal footing with letters and punctuation. When SentencePiece tokenizes "natural language processing", it first transforms the text to "▁natural▁language▁processing". Now each word carries its boundary marker as a prefix. The algorithms can learn that ▁ usually starts tokens, because it is followed by a consistent pattern of characters, and tokens without ▁ are continuations of a word.

The ▁ Prefix in Practice

When you decode a sequence of SentencePiece tokens back to text, you replace ▁ with a space. The token "▁natural" becomes " natural" (with a leading space), and adjacent tokens without ▁ are concatenated directly. This makes encoding and decoding perfectly invertible: you always get back exactly the original text. This invertibility is non-trivial. Many tokenization schemes are lossy in the sense that whitespace normalization or punctuation handling changes the input. SentencePiece guarantees that decode(encode(text)) == text for any input.

This encoding solves the word boundary problem without requiring language-specific rules. For English, ▁ appears where spaces were. For Chinese, where characters are written without spaces, ▁ may appear less frequently and the algorithm learns different patterns. For Japanese, which mixes character-based scripts with occasional spaces, the algorithm adapts accordingly. The same mechanism works for all three languages because the only assumption it makes is that some whitespace characters exist in the input and should be preserved in the output.

One practical consequence is that in SentencePiece output, you will often see tokens like "▁the", "▁language", or "▁un". The leading ▁ is not decoration; it carries semantic information. A token like "▁un" (meaning "un" at the start of a word) is different from "un" (meaning "un" in the middle of a word like "function"). This distinction matters because the same string in different positions has different distributional properties in text. The prefix "un-" as in "unhappy" or "unavailable" always appears at word beginnings. The substring "un" in "function" or "running" appears in the middle. Treating them as distinct tokens allows the model to learn these different distributional properties separately, which ultimately leads to better representations.

Think of the ▁ prefix as a kind of morphological annotation that the algorithm receives for free, without any linguistic knowledge. Instead of a linguist labeling which token positions are word-initial, the algorithm learns it directly from the encoding. This is another example of a recurring theme in modern NLP: replacing hand-crafted features with representations that can be learned from data.

Training Algorithms

SentencePiece offers two algorithms for learning the vocabulary: BPE and the unigram language model. Both algorithms work on the ▁-encoded text and produce a vocabulary of subword tokens, but they approach the problem from opposite directions and embody fundamentally different philosophies about how to define a good vocabulary. Understanding both algorithms gives you better intuition for diagnosing tokenization problems and selecting configurations.

The algorithms we described in the previous chapters operate on pretokenized input, so the vocabulary they learn cannot create tokens that cross word boundaries. In SentencePiece, because the ▁ character is just another character, the algorithms can in principle create tokens like "ing▁the" that span a word ending and the beginning of the next. In practice this rarely happens for high-frequency patterns, because such cross-boundary patterns are not consistent enough to be worth merging. But the algorithm is free to make this choice based on data, not based on a rule we imposed.

Byte Pair Encoding in SentencePiece

BPE in SentencePiece works exactly as described in the earlier BPE chapter, but now operating on ▁-encoded text rather than pretokenized words. The key difference is that merges can cross what would traditionally be word boundaries, because the boundary information is encoded in the ▁ character itself rather than in empty space.

At each step, the algorithm counts every pair of adjacent tokens in the corpus and identifies the pair (a,b)(a, b) that appears most frequently. This is the pair whose frequency across the entire corpus, not just within individual words, is highest. The algorithm computes:

(a,b)=argmax(a,b)count(a,b)(a^*, b^*) = \arg\max_{(a,b)} \text{count}(a, b)

where:

  • (a,b)(a, b): a pair of adjacent tokens in the current representation of the training corpus
  • count(a,b)\text{count}(a, b): the number of times tokens aa and bb appear consecutively anywhere in the corpus
  • (a,b)(a^*, b^*): the optimal pair to merge, which becomes a new single token abab

Why does this greedy selection make sense? Notice that by merging the most frequent pair at each step, we are always reducing the total number of tokens in the corpus by the maximum possible amount in a single merge operation. Each merge of pair (a,b)(a, b) reduces the token count by exactly count(a,b)\text{count}(a, b) occurrences. Choosing the maximum-frequency pair maximizes this reduction. The greedy strategy does not guarantee the globally optimal vocabulary, but it is computationally tractable and produces high-quality vocabularies in practice.

The algorithm adds the merged token to the vocabulary and replaces all occurrences of the pair in the corpus. This continues until the vocabulary reaches the target size. The sequence of merges is saved as the model, and applying the same merges in order to any new text produces the same tokenization.

An important property of BPE in SentencePiece: the ▁ character participates in merges just like any other character. So the very first merges in a multilingual corpus might produce tokens like "▁t", "▁a", or even "▁the" as full tokens if those patterns are frequent enough. The algorithm does not treat ▁ as a special separator; it simply learns whatever patterns appear most frequently in the data. This means that in a well-trained model, most high-frequency words will appear as single tokens with the ▁ prefix, which is exactly the behavior you want.

Unigram Language Model

Where BPE builds vocabulary bottom-up through greedy merging, the unigram language model takes a top-down approach. It starts with a large initial vocabulary containing all substrings up to some maximum length, then prunes it down to the target size using the Expectation-Maximization (EM) algorithm. Think of BPE as sculpting by addition (you start with individual characters and add larger units) while unigram is sculpting by subtraction (you start with a vast vocabulary and remove the least useful pieces).

The fundamental question the unigram model answers is: given a vocabulary VV, what is the best way to tokenize any piece of text? And working backwards: which vocabulary VV gives us the best tokenizations across the entire training corpus?

To answer these questions formally, the goal is to find a vocabulary VV and associated token probabilities {P(x)}xV\{P(x)\}_{x \in V} that maximize the likelihood of the training corpus. The model assumes that tokens within a segmentation are independent (hence "unigram"), so the probability of a segmentation x=(x1,x2,,xk)\mathbf{x} = (x_1, x_2, \ldots, x_k) is simply the product of the individual token probabilities.

For a corpus of texts T1,T2,,TNT_1, T_2, \ldots, T_N, the objective is to maximize the total corpus log-likelihood:

L(V)=t=1NlogP(Tt)\mathcal{L}(V) = \sum_{t=1}^{N} \log P(T_t)

where:

  • L(V)\mathcal{L}(V): the log-likelihood of the corpus under vocabulary VV and its associated probability distribution
  • P(Tt)P(T_t): the probability of text TtT_t, summed over all possible segmentations

The probability of a text must account for the fact that there are many valid ways to segment it. Rather than committing to a single segmentation (as BPE does), the unigram model marginalizes over all possible segmentations, which is what makes it probabilistic rather than deterministic:

P(Tt)=xS(Tt)P(x)=xS(Tt)i=1xP(xi)P(T_t) = \sum_{\mathbf{x} \in S(T_t)} P(\mathbf{x}) = \sum_{\mathbf{x} \in S(T_t)} \prod_{i=1}^{|\mathbf{x}|} P(x_i)

where:

  • S(Tt)S(T_t): the set of all possible segmentations of text TtT_t using tokens from VV
  • P(x)P(\mathbf{x}): the probability of segmentation x\mathbf{x}, which equals the product of individual token probabilities under the unigram independence assumption
  • P(xi)P(x_i): the probability of the ii-th token in segmentation x\mathbf{x}

Why does this formula make sense? Notice that we are treating the text as having been generated by randomly choosing a segmentation, then generating each token independently according to its unigram probability. The marginal probability of observing the text is then the sum over all the ways that process could have produced it. This is exactly the setup of a hidden Markov model where the segmentation is the hidden variable. Maximizing this likelihood pushes the model to assign high probability to the most natural-looking segmentations while still maintaining a coherent probability distribution over all segmentations.

Since the sum over all segmentations is exponentially large (there are an exponential number of ways to segment even a moderately long string), computing it naively is intractable. The key computational tool is the Viterbi forward-backward algorithm, which computes the sum efficiently by dynamic programming in time proportional to the string length times the maximum token length.

Training alternates between two steps that mirror the E-step and M-step of standard EM. In the E-step, for each text the algorithm uses the forward-backward algorithm to compute expected token counts: how often, averaged over all possible segmentations weighted by their probability under the current model, does each token appear? These are called expected counts or soft counts because they are fractional rather than integer. In the M-step, the algorithm re-estimates token probabilities from these expected counts:

P(x)=expected_count(x)xVexpected_count(x)P(x) = \frac{\text{expected\_count}(x)}{\sum_{x' \in V} \text{expected\_count}(x')}

where:

  • expected_count(x)\text{expected\_count}(x): the fractional count for token xx across the corpus, derived from the E-step forward-backward computation
  • The denominator normalizes these counts into a valid probability distribution

After each EM iteration, the algorithm computes how much removing each token xx would reduce the corpus log-likelihood. This quantity, called the loss of token xx, measures how much the rest of the vocabulary can compensate for that token's absence. Tokens whose removal causes the smallest loss are least necessary and are candidates for pruning. Typically 10 to 20 percent of the vocabulary is removed per iteration, and the E-step and M-step cycle repeats until the target vocabulary size is reached.

The pruning criterion is elegant because it accounts for token interactions. A token that appears frequently but whose occurrences can always be covered by two shorter tokens has a low loss and will be pruned. A token that appears less frequently but covers sequences that would otherwise require many short tokens has a high loss and will be retained. The algorithm learns vocabulary coverage patterns, not just raw frequency.

Which Algorithm to Choose

Both BPE and unigram produce similar results in practice, and in fact many production models have been trained with both and achieved comparable downstream performance. But they have different properties that matter in specific settings, and understanding those properties helps you make an informed choice rather than just defaulting to one or the other.

BPE is deterministic: given a vocabulary, the tokenization of any text is uniquely determined by applying the merge rules in order. This consistency is useful for systems where reproducibility is critical and for any analysis that assumes a fixed tokenization. Given the same input and the same model, BPE always produces exactly the same output, making debugging straightforward. BPE also tends to be faster to apply at inference time because it is just a sequence of string replacement operations; no probability computation is needed.

The unigram model is probabilistic: for any text, there are multiple valid segmentations, each with an associated probability under the model. The most probable segmentation can be found with the Viterbi algorithm and is used as the default at inference time. But during training, SentencePiece can sample from this distribution rather than always taking the most probable segmentation. This technique, called subword regularization, acts as a form of data augmentation: the model sees the same text split in different ways across different training steps, which helps it learn that meaning is carried by morphemes rather than specific token boundaries.

Subword regularization is especially valuable for translation and other sequence-to-sequence tasks. If the model always sees "tokenization" split as ["token", "ization"], it may not learn that "token" carries the same meaning in "tokenize" and "tokenized". By occasionally sampling ["token", "iz", "ation"] or ["tokeniz", "ation"], the model sees more variation in how the same morphemes appear across contexts. Empirically, models trained with subword regularization generalize better to out-of-vocabulary words and morphological variants than models trained with deterministic tokenization.

For most practitioners building new models today, the choice between BPE and unigram is less important than the vocabulary size and the quality and size of the training corpus. If you need subword regularization for a translation or generation task, choose unigram. If you need guaranteed reproducibility or the fastest possible inference, choose BPE. If you are unsure, unigram is generally the safer default.

A Worked Example

Let's trace through exactly how SentencePiece processes a concrete example, using BPE mode. We'll use the sentence "natural language processing" and watch the algorithm build its vocabulary from scratch. Following a complete example numerically is the best way to make the abstract algorithm concrete before encountering it in code.

Step 1: Raw Text Encoding

The first transformation replaces spaces with ▁. SentencePiece also prepends a ▁ before the very first word, not just at internal space positions:

Input: "natural language processing" Encoded: "▁natural▁language▁processing"

This is the only preprocessing step. There is no lowercasing, no punctuation normalization, no stemming. The raw Unicode string (minus the space-to-▁ substitution) feeds directly into the algorithm. Every character, including digits, punctuation, and characters from non-Latin scripts, is preserved exactly as-is.

Step 2: Initialize with Characters

BPE starts with a vocabulary containing every unique character (or Unicode code point) in the ▁-encoded corpus. For our sentence, the initial vocabulary contains all the unique characters:

Initial vocabulary: {▁, a, c, e, g, i, l, n, o, p, r, s, t, u}

The full text is now represented as 29 individual character tokens, one for every character position. Every character is a separate unit; there is no notion of "word" or "morpheme" yet. The algorithm is starting from the most granular possible representation and will build structure from the bottom up.

Notice that ▁ is in the initial vocabulary as a regular character. It has no special status beyond the fact that it happens to encode word boundary information. The BPE algorithm treats it exactly the same as any letter.

Step 3: Iterative Merging

The algorithm counts every pair of adjacent characters in the corpus and merges the most frequent one. With a tiny single-sentence corpus, the counts might look like the table below. In a real training corpus with millions of sentences, the frequencies would be much larger but the principle is identical.

Pair frequency counts during the first BPE iteration. The pair (a, l) appears in "natural" (once, in "ural") and "language" (twice, in "anguage"), totaling 3 occurrences.
PairCount
(a, l)3
(a, n)2
(n, g)2
(i, n)1

The pair (a, l) wins with count 3. We merge it into a new token "al" and replace all occurrences in the corpus:

After merge 1 (a, l) → "al": [▁, n, al, t, u, r, al, ▁, l, a, n, g, u, a, g, e, ▁, p, r, o, c, e, s, s, i, n, g]

Notice the corpus now has 27 tokens instead of 29: two pairs of (a, l) were replaced, each saving one token. This is the compression benefit made concrete. The algorithm continues, perhaps next merging (a, n) → "an", then (n, g) → "ng", and eventually (i, n, g) → "ing" by first merging "in" then "ing". After enough merges, common morphological units emerge naturally: "-ing", "-tion", "▁un", and so on.

The key insight from tracing this example is that the algorithm never explicitly looks for morphological units. It just looks for frequent pairs. Morphological units emerge naturally because morphemes by definition are recurrent patterns. The suffix "-ing" appears in "processing", "understanding", "learning", and countless other words. That frequency makes it a natural merge candidate. The algorithm discovers morphological structure as a consequence of optimizing for frequency, not because it was designed to find morphemes.

Step 4: Tokenization at Inference

Once training is done, tokenizing new text means applying the learned merge operations in order. The text "▁natural▁language" starts as individual characters. Then we apply merge 1 (a, l) → "al", which converts "a, l" pairs to "al". We apply merge 2, then merge 3, and so on through the full list of merges in the order they were learned.

The same merge sequence learned during training produces a deterministic result on any input text. This ordered application of merges is what makes BPE fast at inference: it is just a sequence of string substitutions that can be implemented as a simple loop or compiled into efficient pattern-matching code. No probabilities need to be computed, no dynamic programming is needed. You just apply the rules in order.

One important detail: if new text contains a character that was not in the training corpus (and therefore not in the initial vocabulary), BPE cannot represent it. This is the unknown character problem. The unigram model's byte_fallback feature, which we will see in the code section, solves this by falling back to UTF-8 byte tokens for unknown characters.

Code Implementation

Let's implement SentencePiece from scratch, then use the production library. Building it yourself reveals exactly what is happening inside the library; using the production library shows how to configure it for real applications.

Installation

First, let's install the required packages:

In[3]:
Code
import subprocess

subprocess.run(["uv", "pip", "install", "sentencepiece"], capture_output=True)

Implementing BPE from Scratch

Our SimpleSentencePiece class implements the full BPE pipeline: preprocessing with ▁ markers, character-level initialization, iterative merging, and inference-time tokenization. Building this from scratch forces you to confront every design decision that the production library handles automatically.

In[4]:
Code
import collections
from typing import Dict, List, Tuple


class SimpleSentencePiece:
    """Minimal BPE tokenizer with SentencePiece-style whitespace handling."""

    def __init__(self, vocab_size: int = 100):
        self.vocab_size = vocab_size
        self.vocab: Dict[str, int] = {}
        self.merges: List[Tuple[Tuple[str, str], str]] = []

    def _preprocess(self, text: str) -> str:
        """Replace spaces with ▁ prefix to encode word boundaries."""
        return "" + text.replace(" ", "")

    def train(self, corpus: List[str]) -> None:
        """Train BPE on the corpus and learn merge operations."""
        # Preprocess all texts
        processed = [self._preprocess(text) for text in corpus]
        text_list = list(" ".join(processed))

        # Initialize vocabulary with unique characters
        chars = sorted(set(text_list))
        self.vocab = {ch: i for i, ch in enumerate(chars)}
        token_id = len(self.vocab)

        while len(self.vocab) < self.vocab_size:
            # Count frequencies of all adjacent pairs
            stats: Dict[Tuple[str, str], int] = collections.defaultdict(int)
            for i in range(len(text_list) - 1):
                stats[(text_list[i], text_list[i + 1])] += 1

            if not stats:
                break

            # Select and merge the most frequent pair
            best_pair = max(stats, key=stats.get)
            new_token = "".join(best_pair)
            self.vocab[new_token] = token_id
            self.merges.append((best_pair, new_token))
            token_id += 1

            # Apply merge to the running text representation
            i = 0
            while i < len(text_list) - 1:
                if (text_list[i], text_list[i + 1]) == best_pair:
                    text_list[i : i + 2] = [new_token]
                else:
                    i += 1

    def tokenize(self, text: str) -> List[str]:
        """Tokenize text by preprocessing then applying learned merges."""
        tokens = list(self._preprocess(text))
        for pair, new_token in self.merges:
            i = 0
            while i < len(tokens) - 1:
                if (tokens[i], tokens[i + 1]) == pair:
                    tokens[i : i + 2] = [new_token]
                else:
                    i += 1
        return tokens

    def encode(self, text: str) -> List[int]:
        """Convert text to token IDs."""
        tokens = self.tokenize(text)
        return [self.vocab.get(t, self.vocab.get("", 0)) for t in tokens]

    def decode(self, ids: List[int]) -> str:
        """Convert token IDs back to text."""
        id_to_token = {v: k for k, v in self.vocab.items()}
        tokens = [id_to_token.get(i, "") for i in ids]
        return "".join(tokens).replace("", " ").strip()

The _preprocess method does the single key transformation: it prepends ▁ and replaces every space with ▁. The train method initializes the vocabulary from individual characters, then iteratively finds and merges the most frequent pair. The tokenize method applies those learned merges in order to new text. The decode method simply reverses the ▁ substitution to recover the original string.

Training on a Small Corpus

Let's train on a small NLP-themed corpus and observe what patterns emerge. Even with a small corpus, the algorithm discovers structure:

In[5]:
Code
corpus = [
    "natural language processing",
    "machine learning models",
    "natural language understanding",
    "deep learning algorithms",
    "processing natural language",
    "understanding machine learning",
    "deep neural networks",
    "learning language models",
    "language model training",
    "neural network architecture",
]

sp = SimpleSentencePiece(vocab_size=60)
sp.train(corpus)

test_text = "natural language"
tokens = sp.tokenize(test_text)
token_ids = sp.encode(test_text)
decoded = sp.decode(token_ids)

initial_chars = len(set("".join([sp._preprocess(t) for t in corpus])))
num_merges = len(sp.merges)
compression_ratio = len(sp._preprocess(test_text)) / len(tokens)
Out[6]:
Console
Input text:         'natural language'
Preprocessed:       '▁natural▁language'
Tokens:             ['▁natural', '▁language']
Token IDs:          [44, 33]
Decoded:            'natural language'
Initial vocabulary: 19 unique characters
Final vocabulary:   60 tokens
Merges learned:     40
Compression ratio:  8.50x

The decoded output matches the original input exactly, confirming that the ▁ encoding is perfectly invertible. The compression ratio shows how many fewer tokens we need compared to individual characters. Even on this tiny corpus, the algorithm has compressed the representation by merging common patterns. Notice that the ▁ character appeared in the initial vocabulary as a regular character, and some of the learned merges likely include it as part of the pattern.

Examining Learned Merges

The merge sequence reveals exactly what patterns the algorithm discovered. Early merges capture the most frequent character pairs; later merges combine previously merged units into longer subwords. Let's look at the progression:

Out[7]:
Console
First 8 learned merges (most frequent character pairs):
   1. 'n' + 'g'  ->  'ng'
   2. '▁' + 'l'  ->  '▁l'
   3. 'i' + 'ng'  ->  'ing'
   4. '▁' + 'n'  ->  '▁n'
   5. 'd' + 'e'  ->  'de'
   6. 'u' + 'r'  ->  'ur'
   7. 'a' + 'l'  ->  'al'
   8. 'ur' + 'al'  ->  'ural'

Last 5 learned merges (longer subwords):
  36. 'pro' + 'c'  ->  'proc'
  37. 'proc' + 'e'  ->  'proce'
  38. 'proce' + 's'  ->  'proces'
  39. 'proces' + 's'  ->  'process'
  40. 'process' + 'ing'  ->  'processing'

The progression from single characters to morphological units illustrates how BPE discovers structure hierarchically. Early merges address the most common character bigrams, which in English-like text are often consonant-vowel pairs. Later merges build on these, creating longer tokens that correspond to common morphological units. The ▁ character participates in merges just like any other character, which is why many early tokens include the word-boundary prefix. A token like "▁la" in the vocabulary means "la" at the start of a word, a pattern that appears in "language", "language", and "learning".

Visualizing BPE Training Dynamics

To understand how vocabulary growth relates to compression efficiency, let's instrument the training loop and track both metrics across all merge iterations:

Out[9]:
Visualization
Dual-axis line chart of BPE training. The circle-marked vocabulary series rises linearly while the square-marked token-count series decreases faster at first and then slows.
BPE training dynamics showing vocabulary growth on the left axis and corpus compression on the right axis across merge iterations. Vocabulary size increases by one token per merge, while total token count in the corpus decreases as adjacent characters are merged into single units. The sharpest compression gains occur in the earliest merges, where the most frequent character pairs are combined.

The dual-axis view makes the trade-off visible. Each merge adds exactly one token to the vocabulary (the blue line rises in unit steps). The corpus compression (token count, orange) falls fastest at the beginning because those first merges target the most frequent character pairs, which appear throughout the corpus. As training continues, each additional merge produces smaller compression gains because it is addressing rarer patterns that appear in fewer positions. This diminishing return is fundamental to BPE: the vocabulary grows linearly, but the useful work each new token does decreases.

Vocabulary Size and Compression Trade-offs

The choice of vocabulary size is one of the most consequential decisions when configuring SentencePiece. A vocabulary that is too small forces the model to represent many words as long sequences of short tokens, which makes sequence lengths longer and harder for a transformer's attention mechanism to handle. A vocabulary that is too large creates a massive embedding table that consumes memory and makes the softmax over vocabulary at the output of a language model expensive. Let's empirically measure how compression efficiency changes as vocabulary grows:

Out[11]:
Visualization
Dual-axis line chart with a circle-marked series decreasing as sentences require fewer tokens and a square-marked compression-ratio series increasing as vocabulary grows. Both curves flatten at larger vocabulary sizes.
Compression efficiency as a function of vocabulary size. Average tokens per sentence on the left axis decreases as vocabulary grows, while compression ratio on the right axis increases. Both curves flatten, which demonstrates diminishing returns: the first 20 vocabulary entries (characters) serve as the baseline, and early merges provide the steepest gains.

The diminishing returns are clear. Moving from 20 to 40 tokens produces a significant compression gain. Moving from 60 to 75 produces much less. In production systems like BERT (30,522 tokens) or T5 (32,100 tokens), the vocabulary is large enough that most common English words receive their own token, but still small enough that the embedding table remains manageable. The typical range of 8,000 to 32,000 tokens represents the empirically discovered sweet spot where compression is good but the vocabulary remains tractable.

Using the Production SentencePiece Library

Our from-scratch implementation teaches the mechanics. For production use, the official sentencepiece library provides a much faster, more configurable implementation that handles edge cases properly. The library is written in C++ with Python bindings and can tokenize millions of tokens per second on a single CPU core.

First, we create a training corpus file and train a model:

In[12]:
Code
import os
import tempfile

import sentencepiece as spm

# Write corpus to temporary file
extended_corpus = corpus + [
    "tokenization is fundamental",
    "multilingual model training",
    "byte pair encoding algorithm",
    "unigram language model tokenizer",
    "subword regularization technique",
    "vocabulary size selection",
]

with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
    for line in extended_corpus:
        f.write(line + "\n")
    corpus_file = f.name

# Train with BPE
with tempfile.TemporaryDirectory() as tmpdir:
    model_prefix = os.path.join(tmpdir, "sp_model")
    spm.SentencePieceTrainer.train(
        input=corpus_file,
        model_prefix=model_prefix,
        vocab_size=200,
        model_type="bpe",
        character_coverage=1.0,
        pad_id=0,
        unk_id=1,
        bos_id=2,
        eos_id=3,
    )
    # Load and save the model bytes for later use
    with open(model_prefix + ".model", "rb") as f:
        model_bytes = f.read()

os.unlink(corpus_file)

# Load model from bytes
sp_prod = spm.SentencePieceProcessor()
sp_prod.load_from_serialized_proto(model_bytes)

Notice that training produces a binary .model file that encodes the complete tokenizer state: the vocabulary, the merge rules (for BPE) or the token probabilities (for unigram), and all configuration parameters. This file is everything you need to reproduce the exact same tokenization in any environment. You can load it in Python, C++, or any language with a SentencePiece binding.

In[13]:
Code
# Tokenize several texts
test_cases = [
    "natural language processing",
    "deep neural network",
    "multilingual tokenization",
    "unknown words like tokenizationing",
]

tokenization_results = []
for text in test_cases:
    pieces = sp_prod.encode_as_pieces(text)
    ids = sp_prod.encode_as_ids(text)
    decoded = sp_prod.decode_pieces(pieces)
    tokenization_results.append(
        {
            "text": text,
            "pieces": pieces,
            "ids": ids,
            "decoded": decoded,
            "n_tokens": len(pieces),
        }
    )
Out[14]:
Console
Production SentencePiece tokenization results:

Input:   'natural language processing'
Pieces:  ['▁natural', '▁language', '▁processing']
Tokens:  3
Decoded: 'natural language processing'

Input:   'deep neural network'
Pieces:  ['▁deep', '▁neural', '▁network']
Tokens:  3
Decoded: 'deep neural network'

Input:   'multilingual tokenization'
Pieces:  ['▁multilingual', '▁tokenization']
Tokens:  2
Decoded: 'multilingual tokenization'

Input:   'unknown words like tokenizationing'
Pieces:  ['▁un', 'k', 'n', 'o', 'w', 'n', '▁', 'word', 's', '▁l', 'i', 'k', 'e', '▁token', 'izat', 'io', 'ning']
Tokens:  17
Decoded: 'unknown words like tokenizationing'

The production library handles the ▁ prefix, BPE or unigram selection, special tokens (PAD, UNK, BOS, EOS), and multilingual text all transparently. Notice how "multilingual tokenization" is handled even though those exact words were not in the training corpus: the algorithm decomposes them into known subword units. This graceful degradation on unseen words is exactly why subword tokenization replaced word-level tokenization. Even a word that was never seen during training can be decomposed into known pieces.

Multilingual Tokenization

One of SentencePiece's most important properties is handling multiple scripts in a single vocabulary. A single vocabulary can cover dozens of languages simultaneously because SentencePiece works at the level of Unicode characters, not language-specific word lists. Let's see how it handles text from different languages by training on a multilingual corpus:

In[15]:
Code
multilingual_corpus = [
    # English
    "natural language processing",
    "machine learning models",
    "deep learning algorithms",
    # German
    "natürliche Sprachverarbeitung",
    "maschinelles Lernen",
    "tiefes Lernen",
    # Spanish
    "procesamiento de lenguaje natural",
    "aprendizaje automático",
    "redes neuronales",
    # French
    "traitement automatique du langage",
    "apprentissage automatique",
    "réseaux de neurones",
]

with tempfile.NamedTemporaryFile(
    mode="w", suffix=".txt", delete=False, encoding="utf-8"
) as f:
    for line in multilingual_corpus:
        f.write(line + "\n")
    multi_corpus_file = f.name

with tempfile.TemporaryDirectory() as tmpdir:
    model_prefix = os.path.join(tmpdir, "sp_multi")
    spm.SentencePieceTrainer.train(
        input=multi_corpus_file,
        model_prefix=model_prefix,
        vocab_size=300,
        model_type="unigram",
        character_coverage=1.0,
        byte_fallback=True,
    )
    with open(model_prefix + ".model", "rb") as f:
        multi_model_bytes = f.read()

os.unlink(multi_corpus_file)

sp_multi = spm.SentencePieceProcessor()
sp_multi.load_from_serialized_proto(multi_model_bytes)

# Test tokenization across languages
multi_test = [
    ("EN", "language model"),
    ("DE", "Sprachmodell"),
    ("ES", "modelo de lenguaje"),
    ("FR", "modèle de langage"),
]

multi_results = []
for lang, text in multi_test:
    pieces = sp_multi.encode_as_pieces(text)
    multi_results.append(
        {"lang": lang, "text": text, "pieces": pieces, "n": len(pieces)}
    )
Out[16]:
Console
Multilingual tokenization (unigram model, character_coverage=1.0):

[EN] 'language model'
      ['▁', 'l', 'a', 'n', 'g', 'u', 'age', '▁', 'm', 'o', 'd', 'e', 'l']
      (13 tokens)

[DE] 'Sprachmodell'
      ['▁', 'S', 'p', 'r', 'a', 'c', 'h', 'm', 'o', 'd', 'e', 'l', 'l']
      (13 tokens)

[ES] 'modelo de lenguaje'
      ['▁', 'm', 'o', 'd', 'e', 'l', 'o', '▁de', '▁', 'l', 'e', 'n', 'g', 'u', 'a', 'j', 'e']
      (17 tokens)

[FR] 'modèle de langage'
      ['▁', 'm', 'o', 'd', '<0xC3>', '<0xA8>', 'l', 'e', '▁de', '▁', 'l', 'a', 'n', 'g', 'age']
      (15 tokens)

A single vocabulary handles text from multiple languages simultaneously. The shared subword vocabulary allows multilingual models to transfer knowledge between languages: if the model learns that the English prefix "▁un-" signals negation, and it also sees similar negative prefixes in other languages, it can learn shared representations that help with cross-lingual transfer. This property is one of the reasons multilingual models like mBERT and XLM-RoBERTa can perform zero-shot cross-lingual transfer, applying knowledge learned in one language to tasks in a different language.

The character_coverage=1.0 parameter ensures every character in the training data is covered. The byte_fallback=True parameter ensures that characters not seen during training are encoded as their UTF-8 byte sequences rather than mapping to an unknown token. These two parameters together ensure consistent behavior across scripts.

Key Parameters

When training SentencePiece in production, several parameters have the largest impact on quality. Understanding what each parameter controls, and why you might want to change it, is essential for building models that work well on your specific data.

The key parameters for production use are:

  • vocab_size: The total number of subword tokens. Production models typically use 8,000 to 32,000 tokens. Larger values improve compression and reduce token count per sentence; smaller values reduce memory usage. The 32,100 used by T5 and the 32,000 used by LLaMA are common choices that reflect the empirical finding that this range provides good compression without an unwieldy embedding table.
  • model_type: Either 'bpe' for greedy frequency-based merging or 'unigram' for the probabilistic EM approach. BPE is slightly faster at inference; unigram supports subword regularization during training.
  • character_coverage: The fraction of characters in the training data to include. Set to 1.0 for multilingual models to ensure full character coverage. For language-specific models, 0.9995 is sometimes used to exclude the rarest characters that appear only in noise or foreign text.
  • byte_fallback: When True, any character not covered by the vocabulary is encoded as UTF-8 byte tokens. needed for less brittleness in multilingual settings. This ensures the tokenizer never produces unknown tokens.
  • pad_id, unk_id, bos_id, eos_id: IDs for special tokens. Setting these explicitly ensures consistent IDs across model versions and training runs. Setting to -1 disables that special token.

Subword Regularization in Depth

Subword regularization is a training technique unique to the unigram model that has proven surprisingly powerful in practice. It deserves its own extended discussion because it illustrates a broader principle: using the ambiguity of a system as a feature rather than a bug.

Standard tokenization is deterministic. Given a piece of text and a trained tokenizer, there is exactly one output: the most probable (or rule-derived) segmentation. During model training, every time the model sees the word "running", it sees the same token sequence. The model learns to associate that exact token sequence with the word's meaning.

Subword regularization changes this by sampling from the distribution of possible segmentations at training time. For "running", the most probable unigram segmentation might be ["▁running"] if the word appears frequently enough in the vocabulary, or ["▁run", "ning"] if not. With subword regularization and a sampling temperature α\alpha, the model sometimes sees ["▁running"], sometimes ["▁run", "n", "ing"], sometimes ["▁r", "unning"], and so on, each with probability proportional to the segmentation's probability raised to the power α\alpha.

This stochastic segmentation acts as a form of data augmentation. You can think of it as adding noise to the input representation: instead of always giving the model the same token sequence for a word, you give it a variety of plausible decompositions. The model must learn to produce the correct output regardless of how the input happens to be segmented, which forces it to learn that meaning is compositional across token boundaries.

The practical effect is most visible in three scenarios. First, for morphologically rich languages, regularization helps the model learn shared representations for morphologically related words even when they tokenize differently. Second, for translation, regularization reduces the brittleness of alignment: the model does not rely on both source and target text being segmented in exactly the one way. Third, for out-of-vocabulary words at inference time, a model trained with regularization handles unusual segmentations better because it has been trained to handle segmentation variability.

The sampling temperature α\alpha controls the strength of regularization. When α=1\alpha = 1, you sample proportionally to the segmentation probabilities. When α0\alpha \to 0, you always use the most probable segmentation (equivalent to standard deterministic tokenization). When α\alpha \to \infty, you sample uniformly from all segmentations. In practice, values around 0.1 to 0.5 tend to work well.

Multilingual Vocabulary Allocation

When training a multilingual SentencePiece model, one of the most important design decisions is how to allocate vocabulary capacity across languages. If you train on a corpus with 90% English and 10% Chinese, BPE will allocate most of its vocabulary to English subwords because they are statistically dominant. The Chinese script, which uses a much larger character set, may end up with poor coverage.

SentencePiece addresses this through two mechanisms. The character_coverage parameter controls which characters receive explicit vocabulary entries, but does not directly control how vocabulary is allocated across languages. For multilingual models, practitioners often use a technique called language sampling: they oversample low-resource languages during the training corpus preparation phase so that the tokenizer sees approximately equal amounts of each language. Google's mT5 model used this approach with a temperature parameter to control how aggressively low-resource languages were upsampled.

The byte_fallback mechanism provides a safety net. Any character that did not make it into the vocabulary due to limited coverage can still be represented as a sequence of UTF-8 byte tokens. A Chinese character encoded as byte fallback tokens requires 3 bytes and therefore 3 tokens, which is less efficient than a dedicated vocabulary entry but much better than mapping to a single unknown token and losing all character identity.

You can observe this trade-off empirically. A tokenizer trained primarily on English that uses byte fallback for Chinese will produce very long token sequences for Chinese text, because most Chinese characters are not in the vocabulary and each requires 3 byte tokens. A tokenizer trained on balanced multilingual data will produce much shorter sequences for Chinese because the most common characters have dedicated vocabulary entries. For applications that need to process Chinese text efficiently, training a balanced multilingual tokenizer or a Chinese-specific tokenizer is worth the engineering investment.

Limitations and Impact

SentencePiece changed how tokenization is done in production NLP systems, but it introduced new challenges alongside the solutions it provided. Understanding these limitations helps you anticipate problems before they appear in production and design systems that handle them reliably.

The ▁ prefix approach makes whitespace handling clean and invertible, but it means that token semantics depend on position within a word. The token "▁un" (un- at the start of a word) appears in "unavailable" and "uncertain", while "un" (in the middle of a word) appears in "function" and "running". These are technically different tokens in the SentencePiece vocabulary, which can fragment morphologically related words in ways that make learning harder. A model must learn independently that "▁un" in "▁unable" and "un" in "running" are not morphologically related, even though they look visually similar. More significantly, the model must learn that the "▁un" in "unable" does carry a shared meaning with "▁un" in "undo", while the "un" in "running" is unrelated. This disambiguation happens implicitly through training rather than through explicit morphological knowledge.

The lack of explicit pretokenization can also be a limitation in specialized domains. For code, natural language and programming syntax look very different: def calculate_loss(x): should probably not be tokenized the same way as prose. When SentencePiece is trained on mixed code and text corpora, it sometimes produces token boundaries that feel arbitrary, splitting identifiers at positions that make the code harder to reconstruct. The token "calculate" might be split as ["cal", "culate"] if the algorithm has not seen that word often enough to keep it whole. Specialized tokenizers for code (like those used by GitHub Copilot) often add domain-specific rules on top of the base subword approach, either through pretokenization of code identifiers or through specialized vocabulary entries for common programming keywords.

The byte_fallback feature solves the unknown-character problem but creates a special class of byte tokens (typically prefixed with <0x..>) that appear rarely in training data. Models can learn to handle these, but they are treated as less natural than the regular subword tokens. For very low-resource languages where most characters are treated as bytes, the tokenizer may effectively degrade to a character-level model, losing the compression benefits of subword tokenization. A sentence in a rare script might tokenize to three times as many tokens as an equivalent English sentence, which increases the computational cost of processing that language proportionally. This creates an unequal burden for low-resource languages, a problem that is only partially mitigated by vocabulary oversampling during training.

There is also a tension between vocabulary coverage and vocabulary size. Adding more tokens for better coverage of rare scripts necessarily takes slots away from common subwords. A vocabulary of 32,000 tokens sounds large until you consider that there are over 140,000 Unicode characters. Even a moderately multilingual model serving text from dozens of languages cannot give explicit vocabulary entries to every character in every script. The choices about what to include and what to relegate to byte fallback are engineering trade-offs with real consequences for model quality on under-represented languages.

Despite these limitations, SentencePiece's impact on the field has been substantial. Before SentencePiece, multilingual NLP required separate tokenization pipelines for each language family, and those pipelines had to be maintained, tested, and debugged independently. After SentencePiece, a single training run on a multilingual corpus produces a tokenizer that handles dozens of languages with no language-specific code. This made multilingual models like mBERT, XLM-RoBERTa, and mT5 practical to build. It shifted the bottleneck in multilingual NLP from tokenization to the availability of multilingual training data, which is a much more tractable problem to solve because data can be collected from the web.

The library also popularized a clear separation between the tokenizer as a standalone component and the neural model that uses it. A tokenizer is now commonly distributed as a compact .model file that can be loaded in any environment, inspected independently, and versioned separately from the model weights. This modularity has become standard practice: modern model releases on Hugging Face include their tokenizer files separately, allowing the tokenizer to be used independently for preprocessing, analysis, or as a component in systems that do not use the full neural model. This separation matters practically: if you need to preprocess data before training and also at inference time, you need only ship the tokenizer, not the entire model.

Summary

SentencePiece eliminates pretokenization by treating raw text as a sequence of Unicode characters and handling whitespace through the ▁ prefix encoding. This single design decision enables language-agnostic tokenization: the same code, the same algorithm, and the same vocabulary structure work for English, Chinese, Arabic, or any language without modification.

The ▁ prefix is the key mechanism. By replacing spaces with a visible character before applying the subword algorithm, SentencePiece preserves word boundary information in a form that the algorithm can learn from, without requiring any language-specific rules about what constitutes a word. The encoding is perfectly invertible, so you can always recover the original text from the tokens.

The library provides two algorithms: BPE, which builds vocabulary bottom-up by greedily merging the most frequent adjacent pairs at each step, and the unigram language model, which prunes top-down from a large initial vocabulary using EM. BPE is deterministic and fast at inference; unigram enables subword regularization, which improves downstream model quality for sequence-to-sequence tasks by training the model to handle multiple valid segmentations of the same text.

Key practical takeaways:

  • The ▁ prefix on tokens like "▁the" marks word starts; tokens without ▁ are continuations
  • Vocabulary size of 32,000 is a common production choice, balancing compression against embedding table size
  • Set character_coverage=1.0 and byte_fallback=True for multilingual models
  • The unigram model's probabilistic segmentation enables subword regularization as data augmentation
  • Saved .model files are portable: load them with sentencepiece in Python, C++, or any other environment

Many of the models you will encounter in the rest of this book, including T5, LLaMA, and the multilingual transformer families, use SentencePiece for tokenization. Understanding how it works under the hood will help you interpret tokenization behavior when you encounter unexpected splits, make informed choices about vocabulary size and algorithm selection when training your own models, and debug tokenization issues when they arise in production. The principles here, treating whitespace as data, learning structure from frequency, and separating the tokenizer from the model, will recur throughout the remaining chapters as we build increasingly powerful language systems.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about SentencePiece tokenization.

SentencePiece Tokenization Quiz

Question 1 of 80 of 8 completed
What is the primary reason SentencePiece eliminates the pretokenization step?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025sentencepiecesubword, author = {Michael Brenndoerfer}, title = {SentencePiece: Subword Tokenization with BPE and Unigram}, year = {2025}, url = {https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-15} }
APAAcademic
Michael Brenndoerfer (2025). SentencePiece: Subword Tokenization with BPE and Unigram. Retrieved from https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram
MLAAcademic
Michael Brenndoerfer. "SentencePiece: Subword Tokenization with BPE and Unigram." 2026. Web. September 15, 2026. <https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram>.
CHICAGOAcademic
Michael Brenndoerfer. "SentencePiece: Subword Tokenization with BPE and Unigram." Accessed September 15, 2026. https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram.
HARVARDAcademic
Michael Brenndoerfer (2025) 'SentencePiece: Subword Tokenization with BPE and Unigram'. Available at: https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram (Accessed: September 15, 2026).
SimpleBasic
Michael Brenndoerfer (2025). SentencePiece: Subword Tokenization with BPE and Unigram. https://mbrenndoerfer.com/writing/sentencepiece-subword-tokenization-bpe-unigram

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.