Tokenisation and Word Embeddings
BPE, WordPiece, SentencePiece — how text becomes numbers. Word2Vec, GloVe, and contextual embeddings from BERT. The foundation of every NLP system.
Quick answer
Tokenisation turns text into integer token IDs before a model sees it. Subword methods such as Byte Pair Encoding start from characters and merge frequent pairs, handling rare and unseen words without a huge vocabulary. Embeddings then map each token ID to a dense vector that captures meaning.
- Word-level tokenisation struggles with unknown words; subword avoids it.
- BPE merges the most frequent adjacent pair repeatedly.
- Embeddings place similar meanings near each other in vector space.
from collections import Counter
tokens = list("low lower lowest")
pairs = Counter(zip(tokens, tokens[1:]))
best = pairs.most_common(1)[0]
print("most frequent adjacent pair:", best[0], "->", best[1], "times")
print("BPE would merge it into a single new token")Output
most frequent adjacent pair: ('l', 'o') -> 3 times
BPE would merge it into a single new tokenNeural networks only understand numbers. "Stripe declined my payment" is text. Before any model can process it, every character, subword, or word must become an integer. That conversion is tokenisation — and the choice of how to split text changes everything.
Tokenisation is the first step in every NLP pipeline. It converts raw text into a sequence of integer IDs that the model's embedding layer can look up. The tokenisation strategy determines the vocabulary size, how unknown words are handled, and how efficiently rare or domain-specific terms are represented.
A naive approach: split on spaces. "running" and "runs" and "ran" become three separate vocabulary entries with no shared representation. The model must learn from scratch that they are related. A character-level approach: every letter is a token. "running" becomes 7 tokens. Sequences become very long and the model struggles to learn word-level patterns. Subword tokenisation — used by every modern LLM — splits "running" into "run" and "##ning", sharing the "run" representation across all its inflections while handling unknown words gracefully by falling back to subword pieces.
Imagine a dictionary for a new language. You could have one entry per full word — huge dictionary, every form of every verb listed separately. Or one entry per letter — tiny dictionary, but reading requires spelling out every word. Subword tokenisation is like a dictionary of common syllables and word roots — compact vocabulary, still captures meaning, handles new words by combining known pieces.
GPT-4 uses ~100,000 BPE tokens. BERT uses ~30,000 WordPiece tokens. Both can represent any text — known words as single tokens, unknown words as sequences of subword pieces. Nothing is truly "out of vocabulary."
Four tokenisation strategies — word, character, subword, and byte-level
Byte Pair Encoding — the algorithm that powers GPT
BPE starts with a character-level vocabulary and iteratively merges the most frequent pair of adjacent tokens into a new token. After k merges you have a vocabulary of approximately k + n_chars tokens. The merge rules learned during training are applied at inference time to tokenise any new text — including text with words never seen before.
Word embeddings — dense vectors that capture semantic relationships
After tokenisation, each token ID is looked up in an embedding table — a matrix of shape (vocab_size, d_model). Each row is a dense vector representing that token. Tokens with similar meanings end up with similar vectors — "payment" and "transaction" are close in embedding space. "payment" and "bicycle" are far apart.
Word2Vec (2013) was the first widely-used word embedding method. It trains a shallow neural network to predict a word from its context (CBOW) or predict context from a word (Skip-gram). The learned weight matrix becomes the embedding table. The famous result: king − man + woman ≈ queen — arithmetic in embedding space reflects semantic relationships.
The fundamental limitation of Word2Vec and GloVe: each word has exactly one vector regardless of context. "Bank" has the same embedding whether it means a river bank or a financial institution. BERT-style contextual embeddings fix this — the embedding for each token depends on all surrounding tokens.
Contextual embeddings — the same word, different vectors based on context
Static embeddings (Word2Vec, GloVe) assign one fixed vector per word. BERT and its successors produce contextual embeddings — the vector for each token depends on the full surrounding context. "Bank" in "river bank" and "bank" in "bank transfer" get different vectors because BERT processes the entire sentence at once via self-attention before producing the embedding.
In practice, contextual embeddings from pretrained models are used in two ways. As features: run BERT, extract the [CLS] token or averaged token embeddings, use them as input to a classifier. As fine-tuned representations: run BERT, add a task head, fine-tune all parameters end-to-end on your labelled data. Fine-tuning almost always outperforms feature extraction but requires more compute.
nn.Embedding — the lookup table that connects tokenisation to neural networks
In PyTorch, the embedding table is nn.Embedding(vocab_size, d_model). It is a matrix of shape (vocab_size, d_model) — one row per token. A forward pass takes integer token IDs and returns the corresponding rows. It is mathematically equivalent to a one-hot encoding multiplied by a weight matrix — but implemented as a simple lookup for efficiency.
Every common tokenisation and embedding mistake — explained and fixed
Where tokenisation choices show up on the bill and in the bug tracker
Tokenisation looks like a preprocessing detail until it starts showing up in three places production teams actually care about: the API bill, the fairness of the product across languages, and mysterious similarity-search regressions that have nothing to do with the model itself.
Every commercial LLM API bills by token count, not by character or word count — and BPE-style tokenisers are trained on corpora dominated by English text. The direct consequence: the same sentence, carrying the same meaning, costs different numbers of tokens depending on what language it is written in, because the tokeniser's learned merge rules favour subword patterns that are common in the training corpus. A sentence that is 20 tokens in English can easily be 40 to 60 tokens in Hindi, Arabic, or Amharic under a tokeniser trained mostly on English and European-language text — meaning a non-English user pays roughly two to three times more per equivalent request, and burns through a fixed context window two to three times faster for the same amount of actual content.
This is not just a cost problem — it is a product fairness problem. A support chatbot with a fixed per-conversation token budget effectively gives non-English speakers a shorter conversation before hitting the same limit, and a document summarisation tool truncating at a fixed token count silently drops more of the original non-English document than the equivalent English one. Teams building multilingual products increasingly measure and report tokens-per-word by language during evaluation specifically to catch this before it reaches users, not just tokens-per-dollar overall.
Embedding model version mismatches are the second recurring production failure, and they hit similarity search rather than the LLM itself. A search or recommendation system that swaps its embedding model — upgrading from one sentence-transformer version to a newer one, or switching providers — without re-embedding every existing vector in the index ends up comparing vectors from two different, incompatible coordinate systems. The similarity scores it returns are not meaningfully wrong in an obvious way; they simply degrade quietly, because nothing about a cosine similarity calculation throws an error when the two vectors came from different models. The fix is always the same and always expensive: re-embed the entire corpus with the new model version before letting any query use it, and tag every stored vector with the exact model version that produced it so a future mismatch is caught before it ships rather than discovered from a support ticket.
Five things people get wrong about tokenisation and embeddings
BPE's merge rule is purely statistical: at every step it merges whichever adjacent pair of symbols occurs most frequently in the training corpus, with no notion of prefixes, suffixes, or roots. It sometimes produces splits that happen to line up with real morphology — "run" + "##ning" looks like a linguist's segmentation — but that is a byproduct of frequency, not a design goal. Feed it a corpus where "ing" almost never follows "runn" but "runni" is extremely common for some other reason, and BPE will happily merge "runni" instead, producing a split no linguist would ever propose. Treat subword tokens as a frequency-driven compression scheme, not a linguistically grounded morphological analysis.
Tokenisation and embedding are two entirely separate operations that happen to run back to back. Tokenisation is deterministic and discrete: given a fixed vocabulary and merge rules, the same input text always produces the exact same sequence of integer IDs, with no learning involved after the vocabulary is built. Embedding is a learned, continuous lookup: the token ID indexes into a trainable weight matrix, and that matrix's values are updated by gradient descent during training just like any other parameter. You can swap a model's embedding table entirely (freeze it, replace it with GloVe vectors, fine-tune it) without touching the tokeniser, and swap the tokeniser without changing how embeddings work — they are independent stages of the pipeline, not one fused step.
Subword tokenisation eliminates the literal [UNK] token for text expressible in the base character or byte alphabet — any string can eventually be broken down to individual characters or bytes, which are always in vocabulary. But that is not the same as eliminating the practical OOV problem. A domain term the tokeniser never saw during training — "thrombocytopenia," a rare API name, code in an unfamiliar language — gets fragmented into many small subword pieces instead of one clean token. The model then has to reconstruct the term's meaning from scratch, token by token, every time it appears, which measurably hurts performance even though no [UNK] ever shows up. The fix for a domain-heavy corpus is still to retrain or extend the tokeniser on domain data, not to rely on subwords alone.
This is one of the most widely repeated results in NLP, and it is real — but it is also a carefully chosen example from a space that mostly does not behave this cleanly. Systematic evaluation of Word2Vec analogies shows the arithmetic works reliably for a narrow set of relationship types (gender, some capital-city pairs) and degrades quickly outside them; many "obvious" analogies (bigger : big :: colder : cold, for instance) do not hold nearly as cleanly, and the result is sensitive to the exact training corpus and hyperparameters used. The honest takeaway is that embedding spaces capture some linear regularities as a side effect of the training objective, not that vector arithmetic is a dependable general-purpose reasoning tool over word meaning.
"Contextual" describes what happens to the vector after tokenisation — the same token ID produces a different final embedding depending on its surrounding tokens, because self-attention mixes information across the whole sequence before the representation is read out. It says nothing about the tokenisation step itself, which is exactly as fixed and deterministic for BERT as it is for Word2Vec: BERT ships with a static ~30,000-token WordPiece vocabulary built once during pretraining, and every input still has to be tokenised into IDs from that fixed vocabulary before anything contextual happens. Swap BERT's tokeniser for a different vocabulary and the model breaks entirely — contextuality lives in the encoder, not in the tokenisation stage.
Tokenisation and embeddings — 5 questions interviewers actually ask
Raw text first goes through the tokeniser, a fixed, deterministic algorithm (BPE, WordPiece, or similar) that splits text into subword pieces and maps each piece to an integer ID using a vocabulary built once ahead of time — this step involves no learning at inference time and always produces the same IDs for the same text. Those integer IDs are then used to index into the embedding table, nn.Embedding(vocab_size, d_model) — a learned weight matrix where row i is the dense vector for token ID i. That lookup is the actual entry point into the neural network; everything downstream (attention, feedforward layers) operates on those dense vectors, never on the raw text or the token IDs themselves. Getting this pipeline wrong — mismatched vocab sizes between tokeniser and embedding table, or using a different tokeniser than the one a pretrained model was trained with — silently produces garbage, because token ID 500 from one vocabulary means something completely different from token ID 500 in another.
BPE starts by treating every word as a sequence of individual characters, with an end-of-word marker to distinguish, say, "low" from the "low" inside "lower." It then repeatedly finds the most frequent adjacent pair of symbols across the entire corpus and merges that pair into a single new symbol — merge "l" and "o" into "lo" if that pair is the most common, then in the next iteration "lo" might merge with "w" into "low," and so on. After k merges, the vocabulary contains the original characters plus k learned multi-character tokens, and the same ordered list of merge rules is replayed at inference time to tokenise any new text — including words the tokeniser never saw, which just fall back further down the merge chain toward individual characters. It's frequency-based because the entire goal is compression efficiency and shared representations for common patterns: merging what's common first means the most frequent subwords get the shortest token sequences, and structurally similar words (running, runs, runner) end up sharing subword pieces even without any linguistic annotation.
Word-level tokenisation needs a vocabulary entry for every distinct word form — "run," "runs," "running," "ran" are four unrelated vocabulary slots with no shared representation, and any word not seen during vocabulary construction becomes an unrecoverable [UNK]. That forces either a huge vocabulary (hundreds of thousands of entries, most rarely used) or a severe OOV problem. Character-level tokenisation solves OOV completely — any string can be spelled out — but produces very long token sequences for the same text, which is expensive given attention's O(n²) cost, and forces the model to learn word-level meaning entirely from character sequences rather than getting any of that structure for free. Subword tokenisation is the practical middle ground: common words stay as a single token (cheap, like word-level), rare or unseen words decompose into a handful of known subword pieces (robust, like character-level), and the vocabulary size (tens of thousands) stays manageable — which is why every major LLM (GPT's BPE, BERT's WordPiece, SentencePiece elsewhere) converged on this approach independently.
A static embedding assigns exactly one fixed vector per vocabulary entry, computed once during training and never changed at inference time — "bank" has the same vector whether the sentence is about a river or a loan, because the lookup has no way to know which sentence it's in. A contextual embedding computes a different vector for the same token depending on the tokens around it, because the model runs the whole sequence through self-attention layers before producing the final representation for each position — "bank" near "river" and "bank" near "transfer" get measurably different vectors. Static embeddings are cheap, fast to look up, and fine for simple similarity tasks or as an initialisation when you have very little labelled data; contextual embeddings cost a full forward pass through a Transformer but capture polysemy and context-dependent meaning, and they almost always win on any task where meaning genuinely shifts with context, at the cost of far more compute per token.
First, check what a general-purpose tokeniser (GPT's or BERT's default) actually does to your domain text — run a sample through it and measure the average tokens-per-word; if domain terms are getting fragmented into many small pieces, that's the concrete symptom of a tokeniser mismatch, not a vague guess. From there, the two real options are: use or fine-tune a domain-pretrained model that shipped with its own tokeniser trained on similar data (BioBERT/PubMedBERT for medical text, LegalBERT for contracts, a code-specific tokeniser for source code) — the fastest path if one exists for your domain — or train a new BPE/WordPiece vocabulary directly on a representative corpus of your domain text and either train from scratch or carefully extend an existing model's vocabulary with the new merge rules. What you should not do is assume the default tokeniser is "good enough" just because it never emits a literal [UNK] — fragmentation without [UNK] is still a real, measurable cost to model quality.
Text is now numbers. Next: take a pretrained model and make it do your specific task.
You know how text becomes tokens and how tokens become dense vectors. The next step is using pretrained language models in production — loading BERT or RoBERTa from HuggingFace, fine-tuning on a labelled dataset, and deploying for inference. Lesson 50 covers the complete HuggingFace fine-tuning workflow — the Trainer API, evaluation, saving checkpoints, and serving predictions in production.
Trainer API, evaluation metrics, saving and loading checkpoints, and production inference — the complete fine-tuning workflow.
🎯 Key Takeaways
- ✓Tokenisation converts raw text to integer token IDs before any model processing. Word-level tokenisation suffers from OOV problems. Character-level produces very long sequences. Subword tokenisation (BPE, WordPiece, SentencePiece) is the standard — it never produces UNK by splitting unknown words into known subword pieces.
- ✓BPE starts with a character vocabulary and iteratively merges the most frequent adjacent pair into a new token. After k merges the vocabulary has approximately k + n_chars tokens. The same merge rules are applied deterministically at inference time — making BPE reproducible and fast.
- ✓GPT-family models use BPE (byte-level). BERT uses WordPiece — similar to BPE but merges are chosen to maximise the likelihood of the training corpus rather than frequency. Both produce ~30-100k token vocabularies. Always use the tokeniser that was trained with the model — never mix tokenisers.
- ✓Static word embeddings (Word2Vec, GloVe) assign one fixed vector per word regardless of context. Contextual embeddings (BERT, RoBERTa) produce different vectors for the same word in different contexts — "bank" near "river" gets a different vector than "bank" near "transfer". Contextual embeddings almost always produce better downstream task performance.
- ✓In PyTorch, nn.Embedding(vocab_size, d_model) is a lookup table of shape (vocab_size, d_model). padding_idx=0 ensures the padding token always returns a zero vector and receives no gradient. Always ensure the tokeniser vocab_size exactly matches the nn.Embedding num_embeddings parameter.
- ✓For production NLP: never train embeddings from scratch unless you have 100M+ tokens. Load pretrained embeddings (GloVe for static, BERT/RoBERTa for contextual). Fine-tune on your domain data. Always save the tokeniser alongside the model — a different tokeniser will produce different token IDs and break the model entirely.
Quick check
Why do modern tokenisers use subword units like BPE?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
