---
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"
  }
}
```
