The demo below trains a bigram model on a small built-in corpus and lets you generate text one word at a time — or all at once. You can toggle Laplace (add-1) smoothing to see how it rescues zero-probability words.
<!-- N-gram bigram demo: build a model from a short corpus, then generate text word by word. -->
<p class="hint">A bigram model predicts the next word from the current word. The bars show probabilities — with smoothing off, unseen pairs get 0%. Toggle smoothing to rescue rare words, then step through or generate a full sentence.</p>
<div class="controls">
<label class="switch-label">
<input type="checkbox" id="smoothing"> Laplace smoothing (add-1)
</label>
</div>
<div class="model-row">
<span class="model-label">Current word:</span>
<span id="context-word" class="word-badge">—</span>
<span class="arrow">→</span>
<span class="model-label">Next word probs</span>
</div>
<div id="prob-bars" class="prob-bars" aria-label="Probability bars for next words"></div>
<div id="generated" class="generated-text" aria-live="polite"></div>
<div class="btns">
<button id="btn-step" type="button">Next word</button>
<button id="btn-gen" type="button">Generate sentence</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
/* Demo styles — bars, badges, generated text area. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { margin-bottom: .6rem; }
.switch-label { display: flex; align-items: center; gap: .4rem; cursor: pointer; font-size: .88rem; }
.switch-label input { width: 16px; height: 16px; cursor: pointer; }
.model-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.model-label { font-size: .8rem; color: #666; }
.word-badge { background: #1d3557; color: #fff; border-radius: 6px; padding: .15rem .55rem; font-weight: 700; font-size: .9rem; }
.arrow { font-size: 1.2rem; color: #999; }
.prob-bars { display: flex; flex-direction: column; gap: 3px; margin-bottom: .7rem; min-height: 60px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.bar-word { width: 80px; font-size: .8rem; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { flex: 1; background: #e8eef3; border-radius: 4px; height: 14px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 4px; transition: width .3s; }
.bar-fill.smoothed { background: #457b9d; }
.bar-pct { font-size: .75rem; color: #666; width: 38px; }
.generated-text { min-height: 2.4rem; background: #f0f4f8; border-radius: 8px; padding: .4rem .7rem; font-size: .9rem; line-height: 1.6; margin-bottom: .6rem; word-wrap: break-word; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .85rem; font-weight: 600; min-height: 1.2em; color: #0a7d33; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Small training corpus — 8 sentences about cats, dogs, and mats.
const CORPUS = [
'the cat sat on the mat',
'the cat ate the rat',
'the rat sat on the mat',
'the dog sat on the log',
'the dog ate the cat',
'a cat and a dog sat on the mat',
'a cat sat on a log',
'the log sat on the mat',
];
// Build unigram and bigram count tables from the corpus.
function buildBigram(sentences) {
const unigram = {};
const bigram = {};
for (const s of sentences) {
const words = ['<s>', ...s.split(' '), '</s>'];
for (let i = 0; i < words.length; i++) {
unigram[words[i]] = (unigram[words[i]] || 0) + 1;
if (i + 1 < words.length) {
const key = words[i] + '|' + words[i + 1];
bigram[key] = (bigram[key] || 0) + 1;
}
}
}
return { unigram, bigram };
}
// Compute next-word probabilities with optional Laplace (add-1) smoothing.
function nextProbs(model, word, smoothing) {
const { unigram, bigram } = model;
const vocab = Object.keys(unigram).filter(w => w !== '<s>');
const V = vocab.length;
const uCount = unigram[word] || 0;
const probs = {};
for (const w of vocab) {
const bCount = bigram[word + '|' + w] || 0;
probs[w] = smoothing ? (bCount + 1) / (uCount + V) : (uCount > 0 ? bCount / uCount : 0);
}
return probs;
}
// Weighted random sample from the probability distribution.
function sampleWord(probs) {
const items = Object.entries(probs).filter(([, p]) => p > 0);
if (!items.length) return '</s>';
const total = items.reduce((a, [, p]) => a + p, 0);
let r = Math.random() * total;
for (const [w, p] of items) { r -= p; if (r <= 0) return w; }
return items[items.length - 1][0];
}
const model = buildBigram(CORPUS);
const MAX_WORDS = 12;
let generated = [];
let currentWord = '<s>';
function getSmoothing() { return document.getElementById('smoothing').checked; }
function renderBars(word) {
const smoothing = getSmoothing();
const probs = nextProbs(model, word, smoothing);
const sorted = Object.entries(probs).filter(([w]) => w !== '<s>').sort(([, a], [, b]) => b - a).slice(0, 6);
const maxP = sorted.length ? sorted[0][1] : 1;
const container = document.getElementById('prob-bars');
container.innerHTML = '';
for (const [w, p] of sorted) {
const row = document.createElement('div');
row.className = 'bar-row';
const pct = maxP > 0 ? (p / maxP) * 100 : 0;
row.innerHTML =
'<span class="bar-word">' + w + '</span>' +
'<span class="bar-track"><span class="bar-fill' + (smoothing ? ' smoothed' : '') + '" style="width:' + pct.toFixed(1) + '%"></span></span>' +
'<span class="bar-pct">' + (p * 100).toFixed(1) + '%</span>';
container.appendChild(row);
}
document.getElementById('context-word').textContent = word === '<s>' ? 'START' : word;
}
function renderGenerated() {
document.getElementById('generated').textContent = generated.join(' ') || '(sentence will appear here)';
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + (cls || '');
}
function step() {
if (generated.length >= MAX_WORDS || currentWord === '</s>') {
setStatus('Sentence complete.', '');
return;
}
const smoothing = getSmoothing();
const probs = nextProbs(model, currentWord, smoothing);
const next = sampleWord(probs);
if (next === '</s>') {
setStatus('End of sentence.', '');
currentWord = '</s>';
renderBars(currentWord === '</s>' ? '<s>' : currentWord);
return;
}
generated.push(next);
currentWord = next;
renderGenerated();
renderBars(currentWord);
setStatus(i18nFmt('Generated %n% of %max% words.', { n: generated.length, max: MAX_WORDS }), '');
}
function generate() {
reset();
let safety = 0;
while (currentWord !== '</s>' && generated.length < MAX_WORDS && safety++ < 50) step();
}
function reset() {
generated = [];
currentWord = '<s>';
renderGenerated();
renderBars('<s>');
setStatus('', '');
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-gen').addEventListener('click', generate);
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('smoothing').addEventListener('change', () => renderBars(currentWord === '</s>' ? '<s>' : currentWord));
reset();
<!-- N-gram bigram demo: build a model from a short corpus, then generate text word by word. -->
<p class="hint">A bigram model predicts the next word from the current word. The bars show probabilities — with smoothing off, unseen pairs get 0%. Toggle smoothing to rescue rare words, then step through or generate a full sentence.</p>
<div class="controls">
<label class="switch-label">
<input type="checkbox" id="smoothing"> Laplace smoothing (add-1)
</label>
</div>
<div class="model-row">
<span class="model-label">Current word:</span>
<span id="context-word" class="word-badge">—</span>
<span class="arrow">→</span>
<span class="model-label">Next word probs</span>
</div>
<div id="prob-bars" class="prob-bars" aria-label="Probability bars for next words"></div>
<div id="generated" class="generated-text" aria-live="polite"></div>
<div class="btns">
<button id="btn-step" type="button">Next word</button>
<button id="btn-gen" type="button">Generate sentence</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
/* Demo styles — bars, badges, generated text area. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { margin-bottom: .6rem; }
.switch-label { display: flex; align-items: center; gap: .4rem; cursor: pointer; font-size: .88rem; }
.switch-label input { width: 16px; height: 16px; cursor: pointer; }
.model-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .5rem; flex-wrap: wrap; }
.model-label { font-size: .8rem; color: #666; }
.word-badge { background: #1d3557; color: #fff; border-radius: 6px; padding: .15rem .55rem; font-weight: 700; font-size: .9rem; }
.arrow { font-size: 1.2rem; color: #999; }
.prob-bars { display: flex; flex-direction: column; gap: 3px; margin-bottom: .7rem; min-height: 60px; }
.bar-row { display: flex; align-items: center; gap: 6px; }
.bar-word { width: 80px; font-size: .8rem; text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bar-track { flex: 1; background: #e8eef3; border-radius: 4px; height: 14px; overflow: hidden; }
.bar-fill { height: 100%; background: #1d3557; border-radius: 4px; transition: width .3s; }
.bar-fill.smoothed { background: #457b9d; }
.bar-pct { font-size: .75rem; color: #666; width: 38px; }
.generated-text { min-height: 2.4rem; background: #f0f4f8; border-radius: 8px; padding: .4rem .7rem; font-size: .9rem; line-height: 1.6; margin-bottom: .6rem; word-wrap: break-word; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .85rem; font-weight: 600; min-height: 1.2em; color: #0a7d33; }
.status.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Small training corpus — 8 sentences about cats, dogs, and mats.
const CORPUS = [
'the cat sat on the mat',
'the cat ate the rat',
'the rat sat on the mat',
'the dog sat on the log',
'the dog ate the cat',
'a cat and a dog sat on the mat',
'a cat sat on a log',
'the log sat on the mat',
];
// Build unigram and bigram count tables from the corpus.
function buildBigram(sentences) {
const unigram = {};
const bigram = {};
for (const s of sentences) {
const words = ['<s>', ...s.split(' '), '</s>'];
for (let i = 0; i < words.length; i++) {
unigram[words[i]] = (unigram[words[i]] || 0) + 1;
if (i + 1 < words.length) {
const key = words[i] + '|' + words[i + 1];
bigram[key] = (bigram[key] || 0) + 1;
}
}
}
return { unigram, bigram };
}
// Compute next-word probabilities with optional Laplace (add-1) smoothing.
function nextProbs(model, word, smoothing) {
const { unigram, bigram } = model;
const vocab = Object.keys(unigram).filter(w => w !== '<s>');
const V = vocab.length;
const uCount = unigram[word] || 0;
const probs = {};
for (const w of vocab) {
const bCount = bigram[word + '|' + w] || 0;
probs[w] = smoothing ? (bCount + 1) / (uCount + V) : (uCount > 0 ? bCount / uCount : 0);
}
return probs;
}
// Weighted random sample from the probability distribution.
function sampleWord(probs) {
const items = Object.entries(probs).filter(([, p]) => p > 0);
if (!items.length) return '</s>';
const total = items.reduce((a, [, p]) => a + p, 0);
let r = Math.random() * total;
for (const [w, p] of items) { r -= p; if (r <= 0) return w; }
return items[items.length - 1][0];
}
const model = buildBigram(CORPUS);
const MAX_WORDS = 12;
let generated = [];
let currentWord = '<s>';
function getSmoothing() { return document.getElementById('smoothing').checked; }
function renderBars(word) {
const smoothing = getSmoothing();
const probs = nextProbs(model, word, smoothing);
const sorted = Object.entries(probs).filter(([w]) => w !== '<s>').sort(([, a], [, b]) => b - a).slice(0, 6);
const maxP = sorted.length ? sorted[0][1] : 1;
const container = document.getElementById('prob-bars');
container.innerHTML = '';
for (const [w, p] of sorted) {
const row = document.createElement('div');
row.className = 'bar-row';
const pct = maxP > 0 ? (p / maxP) * 100 : 0;
row.innerHTML =
'<span class="bar-word">' + w + '</span>' +
'<span class="bar-track"><span class="bar-fill' + (smoothing ? ' smoothed' : '') + '" style="width:' + pct.toFixed(1) + '%"></span></span>' +
'<span class="bar-pct">' + (p * 100).toFixed(1) + '%</span>';
container.appendChild(row);
}
document.getElementById('context-word').textContent = word === '<s>' ? 'START' : word;
}
function renderGenerated() {
document.getElementById('generated').textContent = generated.join(' ') || '(sentence will appear here)';
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + (cls || '');
}
function step() {
if (generated.length >= MAX_WORDS || currentWord === '</s>') {
setStatus('Sentence complete.', '');
return;
}
const smoothing = getSmoothing();
const probs = nextProbs(model, currentWord, smoothing);
const next = sampleWord(probs);
if (next === '</s>') {
setStatus('End of sentence.', '');
currentWord = '</s>';
renderBars(currentWord === '</s>' ? '<s>' : currentWord);
return;
}
generated.push(next);
currentWord = next;
renderGenerated();
renderBars(currentWord);
setStatus(i18nFmt('Generated %n% of %max% words.', { n: generated.length, max: MAX_WORDS }), '');
}
function generate() {
reset();
let safety = 0;
while (currentWord !== '</s>' && generated.length < MAX_WORDS && safety++ < 50) step();
}
function reset() {
generated = [];
currentWord = '<s>';
renderGenerated();
renderBars('<s>');
setStatus('', '');
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-gen').addEventListener('click', generate);
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('smoothing').addEventListener('change', () => renderBars(currentWord === '</s>' ? '<s>' : currentWord));
reset();
Watch what changes when you enable smoothing: words that never appeared after the current word go from impossible to merely unlikely. The generated sentences become more varied — and sometimes delightfully odd — because smoothing spreads probability mass to every vocabulary item.
Comments
Loading comments...