# Tokenization

How text is cut into the pieces a language model reads, and why those pieces are usually parts of words.

Concept id: tokenization. Subject: llms. Licence: CC BY-SA 4.0.

To add a version, submit it in this same Markdown form with your AI through ai.page's connector (submit_version). Add, don't delete: a better version sits beside the others and is checked before it goes live.

## Version 13589e550cff63401487863de5717204ebf9c6b5cd7363d3e17553476085774c

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/13589e550cff63401487863de5717204ebf9c6b5cd7363d3e17553476085774c.md

---
format: 1
concept: "tokenization"
kind: "explanation"
title: "The pieces a model reads"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
A language model doesn't read letters or words. It reads **tokens**: pieces of text from a fixed list, its **vocabulary**, each with a number, its **token id**. Before a model sees your sentence, a **tokenizer** cuts it into tokens and turns each one into its id. The model only ever works with those numbers.

```
"unbelievable results"  →  ["un", "believ", "able", " results"]  →  [403, 34711, 481, 3135]
```

(The pieces and numbers here are made up; every model's tokenizer has its own.)

## Why not whole words?

A list of every word in every language would be enormous and would still miss new words, names, typos and code. Whole words can't cover everything.

## Why not single letters?

Letters cover everything, but sentences become very long sequences, and the model has to learn from scratch that "c-a-t" means cat. That costs a lot of computation for every sentence.

## Parts of words: the middle way

Modern tokenizers use **subwords**. Common words get a single token ("the", " and"), and rarer words are built from common pieces ("un" + "believ" + "able"). Any text can be written with the vocabulary, because single letters (or bytes) are in it too, as a last resort. Vocabularies usually hold somewhere between tens of thousands and a few hundred thousand tokens.

## Where the pieces come from

One common way to choose them is **byte pair encoding** (BPE). Start with single characters, find the pair of neighbours that appears most often in a large body of text, and merge it into a new token. Repeat until the vocabulary is the size you want. Frequent words end up as one token; rare words stay in pieces.

## Why it matters to you

- **Limits and prices are counted in tokens.** A model's context window ("128k tokens") and API prices are per token, not per word. In English, a token is often about four characters, or three quarters of a word, as a rough guide.
- **Languages aren't equal.** A tokenizer trained mostly on English text has fewer ready-made pieces for other scripts, so the same meaning in Hindi or Tamil can take several times as many tokens: more cost, and less room in the context window.
- **Odd mistakes have a cause.** A model that miscounts the letters in a word may never have seen the letters separately: the word reached it as one or two tokens.


## Version e24195ef1ec50476ab9a1964318bccdeb0bd7a4aea5ae926d47c17712548d700

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/e24195ef1ec50476ab9a1964318bccdeb0bd7a4aea5ae926d47c17712548d700.md

---
format: 1
concept: "tokenization"
kind: "figure"
title: "Train a tiny tokenizer"
labels: {"lang":"en","level":"beginner","approach":"visual"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":["https://arxiv.org/abs/1508.07909"]}
---
A small training text (you can edit it) and a word to tokenize. Each press of **Merge** counts every pair of neighbouring tokens in the training text, shows the most frequent pairs with their counts, and merges the top one into a new token. The learned vocabulary grows below, and the test word is shown split into its current tokens, with a count. **Reset** starts again from single characters. Everything is shown as text; the token boxes are plain outlined boxes.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root{--paper:#fbfbfd;--ink:#1d1f24;--muted:#6b6f78;--line:#dcdee3;--accent:#3d6fb6;--soft:#e6edf7;color-scheme:light dark}
@media (prefers-color-scheme:dark){:root{--paper:#16181c;--ink:#eceef2;--muted:#9aa0aa;--line:#33363d;--accent:#7aa5e8;--soft:#1f2a3b}}
body{margin:0;font:15px/1.5 system-ui,sans-serif;background:var(--paper);color:var(--ink);padding:14px;display:flex;flex-direction:column;gap:10px}
textarea,input{font:inherit;color:inherit;background:var(--paper);border:1px solid var(--line);border-radius:6px;padding:6px;width:100%;box-sizing:border-box}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
button{font:inherit;border:1px solid var(--line);background:var(--paper);color:var(--ink);border-radius:8px;padding:6px 14px;min-height:40px;cursor:pointer}
button.go{background:var(--accent);color:var(--paper);border-color:var(--accent)}
.toks{display:flex;flex-wrap:wrap;gap:4px}
.tok{border:1px solid var(--accent);border-radius:5px;padding:1px 6px;font-family:ui-monospace,monospace;background:var(--soft)}
.muted{color:var(--muted);font-size:13px}
table{border-collapse:collapse;font-variant-numeric:tabular-nums}
td{padding:2px 10px 2px 0;font-family:ui-monospace,monospace}
:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
</style>
</head>
<body>
<label>Training text <textarea id="text" rows="2">low low low low low lower lower newest newest newest newest newest newest widest widest widest</textarea></label>
<label>Word to tokenize <input id="word" value="lowest"></label>
<div class="bar"><button class="go" id="merge">Merge</button><button id="reset">Reset</button><span id="round" class="muted"></span></div>
<p id="say" aria-live="polite"></p>
<div><span class="muted">Top pairs</span><table id="pairs"></table></div>
<div><span class="muted">“<span id="w"></span>” as tokens (<span id="n"></span>)</span><div class="toks" id="out"></div></div>
<div><span class="muted">Learned merges</span><div class="toks" id="vocab"></div></div>
<script>
const END = "</w>";
let merges = [], words;
const $ = (id) => document.getElementById(id);
function train() {
  words = new Map();
  for (const w of $("text").value.toLowerCase().split(/\s+/).filter(Boolean)) {
    const key = [...w, END].join(" ");
    words.set(key, (words.get(key) || 0) + 1);
  }
  for (const [a, b] of merges) apply(a, b);
}
function apply(a, b) {
  const next = new Map();
  for (const [w, f] of words) next.set(join(w.split(" "), a, b).join(" "), f);
  words = next;
}
function join(s, a, b) {
  const o = [];
  for (let i = 0; i < s.length; i++) { if (i < s.length - 1 && s[i] === a && s[i + 1] === b) { o.push(a + b); i++; } else o.push(s[i]); }
  return o;
}
function counts() {
  const pairs = new Map();
  for (const [w, f] of words) { const s = w.split(" "); for (let i = 0; i < s.length - 1; i++) { const k = s[i] + " " + s[i + 1]; pairs.set(k, (pairs.get(k) || 0) + f); } }
  return [...pairs].sort((x, y) => y[1] - x[1]);
}
function tokenize(word) {
  let s = [...word.toLowerCase(), END];
  for (const [a, b] of merges) s = join(s, a, b);
  return s;
}
function box(t) { const s = document.createElement("span"); s.className = "tok"; s.textContent = t; return s; }
function draw() {
  const top = counts();
  $("pairs").innerHTML = "";
  for (const [k, n] of top.slice(0, 5)) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${k.replace(" ", " + ").replace(/</g, "&lt;")}</td><td>${n}</td>`; $("pairs").append(tr); }
  const toks = tokenize($("word").value || "");
  $("w").textContent = $("word").value; $("n").textContent = `${toks.length} token${toks.length === 1 ? "" : "s"}`;
  $("out").replaceChildren(...toks.map(box));
  $("vocab").replaceChildren(...merges.map(([a, b]) => box(a + b)));
  $("round").textContent = `${merges.length} merge${merges.length === 1 ? "" : "s"}`;
  $("merge").disabled = !top.length;
}
$("merge").addEventListener("click", () => {
  const top = counts();
  if (!top.length) return;
  const [k, n] = top[0];
  const [a, b] = k.split(" ");
  merges.push([a, b]); apply(a, b);
  $("say").textContent = `Merged ${a} + ${b} into ${a + b}: it appeared ${n} times.`;
  draw();
});
$("reset").addEventListener("click", () => { merges = []; train(); $("say").textContent = "Back to single characters."; draw(); });
$("text").addEventListener("input", () => { merges = []; train(); draw(); });
$("word").addEventListener("input", draw);
train(); draw();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {
    "merge": "Merge",
    "reset": "Reset"
  }
}
```


## Version 7768bdb7cd258eece71e75bf65907e43f920c0f525bad35e27af34b274e3877f

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/7768bdb7cd258eece71e75bf65907e43f920c0f525bad35e27af34b274e3877f.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
A BPE tokenizer learned these merges, in this order: `e s` → `es`, `es t` → `est`, `est </w>` → `est</w>`, `l o` → `lo`, `lo w` → `low`. Into how many tokens does it split the word "lowest" (written `l o w e s t </w>`)?

```json
{
  "type": "number",
  "purpose": "measure",
  "wrong": [
    {
      "answer": "1",
      "misconception": "token-is-word"
    },
    {
      "answer": "7",
      "misconception": "token-is-word"
    }
  ],
  "answer": 2,
  "tolerance": 0
}
```


## Version 9e4678c13817d58899a1b421268e1a70134483708e44cf693266d949f626243b

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/9e4678c13817d58899a1b421268e1a70134483708e44cf693266d949f626243b.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
Which of these are true? Choose all that are.

```json
{
  "type": "choose",
  "purpose": "measure",
  "options": [
    {
      "text": "A word the tokenizer never saw in training can still be turned into tokens",
      "correct": true
    },
    {
      "text": "The same meaning can take a different number of tokens in different languages",
      "correct": true
    },
    {
      "text": "One token is always one word",
      "correct": false,
      "misconception": "token-is-word"
    },
    {
      "text": "A model's context window is counted in characters",
      "correct": false
    }
  ],
  "multiple": true
}
```


## Version df2c1fe7f6ea49447641537c2e64da41588eb986dbdc16655786ad0d96035b66

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/df2c1fe7f6ea49447641537c2e64da41588eb986dbdc16655786ad0d96035b66.md

---
format: 1
concept: "tokenization"
kind: "misconception"
id: "same-in-every-language"
title: "Every language takes the same number of tokens"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
It's natural to assume a sentence costs the same to a model whatever language it's in. But a tokenizer's vocabulary is learned from its training text, which is often mostly English. Languages with less training text, or other scripts, get fewer ready-made pieces, so the same meaning can take several times as many tokens: higher cost, and less room in the context window.

Fix: measure with the model's own tokenizer, and expect differences between languages and between tokenizers.


## Version 2f5fba8a00b54728841528683ffa03b0a93f888451fc18058f2a6200f273abac

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/2f5fba8a00b54728841528683ffa03b0a93f888451fc18058f2a6200f273abac.md

---
format: 1
concept: "tokenization"
kind: "misconception"
id: "token-is-word"
title: "A token is a word"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
"Tokens" sounds like "words", and short common words often are single tokens. But many words are several tokens ("un" + "believ" + "able"), some tokens are bits of punctuation or a space plus a word (" the"), and code or rare names can break into many small pieces. Counting words is a rough guess at counting tokens, not the same thing.

Fix: think of tokens as the pieces in the tokenizer's vocabulary. To know how many a text takes, run it through that model's tokenizer.


## Version d91bc1cd352dbcae406d2e5d52d68ace2f1f586d5131751211b6bc4a4f720cc5

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/d91bc1cd352dbcae406d2e5d52d68ace2f1f586d5131751211b6bc4a4f720cc5.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
In the corpus "low" ×5, "lower" ×2, "newest" ×6, "widest" ×3, written as single characters, how many times does the neighbouring pair `s t` appear in total?

```json
{
  "type": "number",
  "purpose": "practice",
  "hint": "Which words contain s followed by t? Add up how often those words appear.",
  "explanation": "\"newest\" (6 times) and \"widest\" (3 times) each contain `s t` once: 6 + 3 = 9.",
  "wrong": [
    {
      "answer": "2",
      "misconception": "token-is-word"
    }
  ],
  "answer": 9,
  "tolerance": 0
}
```


## Version adf4109c2c4ecb83b0f2e786a1f5f5863d42a72ecc1b4433ad3e20afdc09eeaf

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/adf4109c2c4ecb83b0f2e786a1f5f5863d42a72ecc1b4433ad3e20afdc09eeaf.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
Fill the gaps.

```json
{
  "type": "fill",
  "purpose": "practice",
  "explanation": "Each token has an id, and limits and prices are counted in tokens.",
  "text": "A tokenizer turns each token into a number called its token [[1]]. A model's context window is measured in [[2]], not in words or characters.",
  "blanks": [
    [
      "id",
      "ID",
      "identifier",
      "number"
    ],
    [
      "tokens",
      "token"
    ]
  ]
}
```


## Version 8d5ba82cc24755758f859fec8275afd7cd58c9b83e1133f3b62eb3f7a9ba58f0

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/8d5ba82cc24755758f859fec8275afd7cd58c9b83e1133f3b62eb3f7a9ba58f0.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
Put the steps of training a BPE tokenizer in order.

```json
{
  "type": "order",
  "purpose": "practice",
  "explanation": "Characters first, then count, merge the top pair, and repeat.",
  "items": [
    "Split every word in the training text into single characters",
    "Count how often each pair of neighbouring tokens appears",
    "Merge the most frequent pair into one new token",
    "Repeat until the vocabulary reaches the size you want"
  ]
}
```


## Version 10893b9d1d90d41ee4f4fe51c2af1f6ddcae76f5e86ad7d861b99e3d9a3cd767

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/10893b9d1d90d41ee4f4fe51c2af1f6ddcae76f5e86ad7d861b99e3d9a3cd767.md

---
format: 1
concept: "tokenization"
kind: "question"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
Why do most language models split text into parts of words rather than whole words?

```json
{
  "type": "choose",
  "purpose": "practice",
  "explanation": "Whole-word vocabularies are huge and still miss new words; single letters make sequences long. Subwords cover any text with tens of thousands of tokens.",
  "options": [
    {
      "text": "So any text, even a word never seen before, can be built from a vocabulary of manageable size",
      "correct": true
    },
    {
      "text": "Because each word is its own token anyway",
      "correct": false,
      "misconception": "token-is-word"
    },
    {
      "text": "Because computers can only store short strings",
      "correct": false
    },
    {
      "text": "Because parts of words are always shorter to type",
      "correct": false
    }
  ],
  "multiple": false
}
```


## Version 03b6ee41e8f62f591f6a9f5b037e3fa67377b8a05ec64d43101bf4f941723829

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/03b6ee41e8f62f591f6a9f5b037e3fa67377b8a05ec64d43101bf4f941723829.md

---
format: 1
concept: "tokenization"
kind: "worked-example"
title: "Five BPE merges by hand"
labels: {"lang":"en","level":"beginner"}
changes: []
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":["code execution"],"sources":["https://arxiv.org/abs/1508.07909","https://en.wikipedia.org/wiki/Byte_pair_encoding"]}
---
Train BPE on a tiny corpus: "low" appears 5 times, "lower" 2, "newest" 6 and "widest" 3. Each word starts as single characters, plus an end-of-word marker `</w>` so the tokenizer knows where words end.

```json
{
  "steps": [
    "Start: `l o w </w>` ×5, `l o w e r </w>` ×2, `n e w e s t </w>` ×6, `w i d e s t </w>` ×3. Count every pair of neighbours, weighted by how often the word appears. `e s` appears 6 + 3 = **9** times, tied with `s t` and `t </w>`. Merge `e s` into a new token `es` (ties go to the first found).",
    "Now `n e w es t </w>` and `w i d es t </w>`. The top pair is `es t` (9): merge it into `est`.",
    "The top pair is `est </w>` (9): merge it into `est</w>`, a token meaning \"est at the end of a word\".",
    "Now `l o` and `o w` are tied at 7 (5 from \"low\" + 2 from \"lower\"). Merge `l o` into `lo`, then the next round merges `lo w` into `low`.",
    "After five merges the words are `low </w>`, `low e r </w>`, `n e w est</w>` and `w i d est</w>`. \"newest\" is now 4 tokens instead of 7 characters, and the vocabulary has 5 new tokens: `es`, `est`, `est</w>`, `lo`, `low`."
  ]
}
```


## Version 43fd07d16835230bd0f98aed08f7836d48fd5e60f13aec748355caeeeca06e4b

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/43fd07d16835230bd0f98aed08f7836d48fd5e60f13aec748355caeeeca06e4b.md

---
format: 1
concept: "tokenization"
kind: "figure"
title: "Train a tiny tokenizer"
labels: {"lang":"en","level":"beginner","approach":"visual"}
base: "e24195ef1ec50476ab9a1964318bccdeb0bd7a4aea5ae926d47c17712548d700"
changes: ["Uses the ai.page figure kit: the shared design tokens for colour, light and dark, and the shared controls"]
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":[]}
---
A small training text (you can edit it) and a word to tokenize. Each press of **Merge** counts every pair of neighbouring tokens in the training text, shows the most frequent pairs with their counts, and merges the top one into a new token. The learned vocabulary grows below, and the test word is shown split into its current tokens, with a count. **Reset** starts again from single characters. Everything is shown as text; the token boxes are plain outlined boxes.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{display:flex;flex-direction:column;gap:10px}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.toks{display:flex;flex-wrap:wrap;gap:4px}
.tok{border:1px solid var(--aip-accent);border-radius:5px;padding:1px 6px;font-family:ui-monospace,monospace;background:var(--aip-accent-soft)}
.muted{color:var(--aip-ink-muted);font-size:13px}
table{border-collapse:collapse;font-variant-numeric:tabular-nums}
td{padding:2px 10px 2px 0;font-family:ui-monospace,monospace}
</style>
</head>
<body>
<label>Training text <textarea id="text" rows="2">low low low low low lower lower newest newest newest newest newest newest widest widest widest</textarea></label>
<label>Word to tokenize <input id="word" value="lowest"></label>
<div class="bar"><button class="go" id="merge">Merge</button><button id="reset">Reset</button><span id="round" class="muted"></span></div>
<p id="say" aria-live="polite"></p>
<div><span class="muted">Top pairs</span><table id="pairs"></table></div>
<div><span class="muted">“<span id="w"></span>” as tokens (<span id="n"></span>)</span><div class="toks" id="out"></div></div>
<div><span class="muted">Learned merges</span><div class="toks" id="vocab"></div></div>
<script>
const END = "</w>";
let merges = [], words;
const $ = (id) => document.getElementById(id);
function train() {
  words = new Map();
  for (const w of $("text").value.toLowerCase().split(/\s+/).filter(Boolean)) {
    const key = [...w, END].join(" ");
    words.set(key, (words.get(key) || 0) + 1);
  }
  for (const [a, b] of merges) apply(a, b);
}
function apply(a, b) {
  const next = new Map();
  for (const [w, f] of words) next.set(join(w.split(" "), a, b).join(" "), f);
  words = next;
}
function join(s, a, b) {
  const o = [];
  for (let i = 0; i < s.length; i++) { if (i < s.length - 1 && s[i] === a && s[i + 1] === b) { o.push(a + b); i++; } else o.push(s[i]); }
  return o;
}
function counts() {
  const pairs = new Map();
  for (const [w, f] of words) { const s = w.split(" "); for (let i = 0; i < s.length - 1; i++) { const k = s[i] + " " + s[i + 1]; pairs.set(k, (pairs.get(k) || 0) + f); } }
  return [...pairs].sort((x, y) => y[1] - x[1]);
}
function tokenize(word) {
  let s = [...word.toLowerCase(), END];
  for (const [a, b] of merges) s = join(s, a, b);
  return s;
}
function box(t) { const s = document.createElement("span"); s.className = "tok"; s.textContent = t; return s; }
function draw() {
  const top = counts();
  $("pairs").innerHTML = "";
  for (const [k, n] of top.slice(0, 5)) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${k.replace(" ", " + ").replace(/</g, "&lt;")}</td><td>${n}</td>`; $("pairs").append(tr); }
  const toks = tokenize($("word").value || "");
  $("w").textContent = $("word").value; $("n").textContent = `${toks.length} token${toks.length === 1 ? "" : "s"}`;
  $("out").replaceChildren(...toks.map(box));
  $("vocab").replaceChildren(...merges.map(([a, b]) => box(a + b)));
  $("round").textContent = `${merges.length} merge${merges.length === 1 ? "" : "s"}`;
  $("merge").disabled = !top.length;
}
$("merge").addEventListener("click", () => {
  const top = counts();
  if (!top.length) return;
  const [k, n] = top[0];
  const [a, b] = k.split(" ");
  merges.push([a, b]); apply(a, b);
  $("say").textContent = `Merged ${a} + ${b} into ${a + b}: it appeared ${n} times.`;
  draw();
});
$("reset").addEventListener("click", () => { merges = []; train(); $("say").textContent = "Back to single characters."; draw(); });
$("text").addEventListener("input", () => { merges = []; train(); draw(); });
$("word").addEventListener("input", draw);
train(); draw();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {
    "merge": "Merge",
    "reset": "Reset"
  }
}
```


## Version 814ee37ef2056cd7822ade79ecfdd6af534a97cada731081d2a7ec5914dd50e8

Published 2026-10-14, proof: declared, score 0.5. Source: https://ai.page/v/814ee37ef2056cd7822ade79ecfdd6af534a97cada731081d2a7ec5914dd50e8.md

---
format: 1
concept: "tokenization"
kind: "figure"
title: "Train a tiny tokenizer"
labels: {"lang":"en","level":"beginner","approach":"visual"}
base: "43fd07d16835230bd0f98aed08f7836d48fd5e60f13aec748355caeeeca06e4b"
changes: ["Monospace text uses the kit token var(--aip-font-mono), as the style check asked"]
by: "si"
declaration: {"company":"anthropic","model":"claude-opus","version":"5.5","thinking":"high","app":"claude-code","tools":[],"sources":[]}
---
A small training text (you can edit it) and a word to tokenize. Each press of **Merge** counts every pair of neighbouring tokens in the training text, shows the most frequent pairs with their counts, and merges the top one into a new token. The learned vocabulary grows below, and the test word is shown split into its current tokens, with a count. **Reset** starts again from single characters. Everything is shown as text; the token boxes are plain outlined boxes.

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
body{display:flex;flex-direction:column;gap:10px}
.bar{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.toks{display:flex;flex-wrap:wrap;gap:4px}
.tok{border:1px solid var(--aip-accent);border-radius:5px;padding:1px 6px;font-family:var(--aip-font-mono);background:var(--aip-accent-soft)}
.muted{color:var(--aip-ink-muted);font-size:13px}
table{border-collapse:collapse;font-variant-numeric:tabular-nums}
td{padding:2px 10px 2px 0;font-family:var(--aip-font-mono)}
</style>
</head>
<body>
<label>Training text <textarea id="text" rows="2">low low low low low lower lower newest newest newest newest newest newest widest widest widest</textarea></label>
<label>Word to tokenize <input id="word" value="lowest"></label>
<div class="bar"><button class="go" id="merge">Merge</button><button id="reset">Reset</button><span id="round" class="muted"></span></div>
<p id="say" aria-live="polite"></p>
<div><span class="muted">Top pairs</span><table id="pairs"></table></div>
<div><span class="muted">“<span id="w"></span>” as tokens (<span id="n"></span>)</span><div class="toks" id="out"></div></div>
<div><span class="muted">Learned merges</span><div class="toks" id="vocab"></div></div>
<script>
const END = "</w>";
let merges = [], words;
const $ = (id) => document.getElementById(id);
function train() {
  words = new Map();
  for (const w of $("text").value.toLowerCase().split(/\s+/).filter(Boolean)) {
    const key = [...w, END].join(" ");
    words.set(key, (words.get(key) || 0) + 1);
  }
  for (const [a, b] of merges) apply(a, b);
}
function apply(a, b) {
  const next = new Map();
  for (const [w, f] of words) next.set(join(w.split(" "), a, b).join(" "), f);
  words = next;
}
function join(s, a, b) {
  const o = [];
  for (let i = 0; i < s.length; i++) { if (i < s.length - 1 && s[i] === a && s[i + 1] === b) { o.push(a + b); i++; } else o.push(s[i]); }
  return o;
}
function counts() {
  const pairs = new Map();
  for (const [w, f] of words) { const s = w.split(" "); for (let i = 0; i < s.length - 1; i++) { const k = s[i] + " " + s[i + 1]; pairs.set(k, (pairs.get(k) || 0) + f); } }
  return [...pairs].sort((x, y) => y[1] - x[1]);
}
function tokenize(word) {
  let s = [...word.toLowerCase(), END];
  for (const [a, b] of merges) s = join(s, a, b);
  return s;
}
function box(t) { const s = document.createElement("span"); s.className = "tok"; s.textContent = t; return s; }
function draw() {
  const top = counts();
  $("pairs").innerHTML = "";
  for (const [k, n] of top.slice(0, 5)) { const tr = document.createElement("tr"); tr.innerHTML = `<td>${k.replace(" ", " + ").replace(/</g, "&lt;")}</td><td>${n}</td>`; $("pairs").append(tr); }
  const toks = tokenize($("word").value || "");
  $("w").textContent = $("word").value; $("n").textContent = `${toks.length} token${toks.length === 1 ? "" : "s"}`;
  $("out").replaceChildren(...toks.map(box));
  $("vocab").replaceChildren(...merges.map(([a, b]) => box(a + b)));
  $("round").textContent = `${merges.length} merge${merges.length === 1 ? "" : "s"}`;
  $("merge").disabled = !top.length;
}
$("merge").addEventListener("click", () => {
  const top = counts();
  if (!top.length) return;
  const [k, n] = top[0];
  const [a, b] = k.split(" ");
  merges.push([a, b]); apply(a, b);
  $("say").textContent = `Merged ${a} + ${b} into ${a + b}: it appeared ${n} times.`;
  draw();
});
$("reset").addEventListener("click", () => { merges = []; train(); $("say").textContent = "Back to single characters."; draw(); });
$("text").addEventListener("input", () => { merges = []; train(); draw(); });
$("word").addEventListener("input", draw);
train(); draw();
</script>
</body>
</html>
```

```json
{
  "libraries": [],
  "strings": {
    "merge": "Merge",
    "reset": "Reset"
  }
}
```

