<!-- HMM Part-of-Speech Tagger — Viterbi decoding demo -->
<p class="hint">A tiny HMM with hand-crafted probabilities runs Viterbi to label each word. Transition probabilities capture grammar patterns; emission probabilities capture word-to-tag likelihoods.</p>
<div class="controls">
<label for="sentence-select">Sentence:</label>
<select id="sentence-select">
<option value="0">The dog barks loudly</option>
<option value="1">Time flies like an arrow</option>
<option value="2">The cat sits on the mat</option>
<option value="3">She reads a book quietly</option>
</select>
<button id="tag-btn" type="button">Tag sentence</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div id="tags-row" class="tags-row" aria-live="polite"></div>
<div id="status" class="status" role="status"></div>
<div id="path-info" class="path-info"></div>
/* styles for the word cards and tag badges */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: 1rem; }
label { font-size: .88rem; font-weight: 600; color: #333; }
select { font: 14px system-ui, sans-serif; padding: .35rem .6rem; border: 1px solid #adb1b8;
border-radius: 6px; background: #fff; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.tags-row { display: flex; flex-wrap: wrap; gap: .5rem; min-height: 90px; align-items: flex-start; }
.word-card { display: flex; flex-direction: column; align-items: center; gap: .25rem;
opacity: 0; transform: translateY(8px);
transition: opacity .3s ease, transform .3s ease; }
.word-card.visible { opacity: 1; transform: translateY(0); }
.word-text { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.tag-badge { font: 700 11px system-ui, sans-serif; padding: .18rem .5rem; border-radius: 99px;
color: #fff; white-space: nowrap; }
.conf-bar-wrap { width: 52px; height: 5px; background: #e0e4ea; border-radius: 3px; overflow: hidden; }
.conf-bar { height: 100%; border-radius: 3px; transition: width .4s; }
.conf-text { font-size: .72rem; color: #666; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0 .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.path-info { font-size: .8rem; color: #555; line-height: 1.5; }
/* one color per POS tag for quick visual scanning */
.tag-DT { background: #457b9d; }
.tag-NN { background: #1d3557; }
.tag-NNS { background: #264653; }
.tag-VBZ { background: #e76f51; }
.tag-VBP { background: #f4a261; }
.tag-VB { background: #2a9d8f; }
.tag-RB { background: #e9c46a; color: #222 !important; }
.tag-IN { background: #6d6875; }
.tag-PRP { background: #b5838d; }
.tag-CD { background: #4a4e69; }
// Hidden Markov Model: states = POS tags, observations = words
// Viterbi finds the most likely tag sequence in O(T * N^2) time
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// the set of part-of-speech tags used in this model
const TAGS = ['DT','NN','NNS','VBZ','VBP','VB','RB','IN','PRP','CD'];
// transition probability matrix: P(tag_i | tag_{i-1})
// each row sums to ~1; <s> is the sentence-start state
const TRANS = {
'<s>': { DT:0.35, NN:0.10, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.03, IN:0.02, PRP:0.35, CD:0.06 },
DT: { DT:0.01, NN:0.55, NNS:0.30, VBZ:0.01, VBP:0.01, VB:0.01, RB:0.03, IN:0.02, PRP:0.01, CD:0.05 },
NN: { DT:0.02, NN:0.08, NNS:0.04, VBZ:0.45, VBP:0.10, VB:0.03, RB:0.05, IN:0.15, PRP:0.02, CD:0.06 },
NNS: { DT:0.02, NN:0.06, NNS:0.03, VBZ:0.05, VBP:0.50, VB:0.05, RB:0.05, IN:0.15, PRP:0.02, CD:0.07 },
VBZ: { DT:0.15, NN:0.08, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.05, RB:0.40, IN:0.15, PRP:0.05, CD:0.05 },
VBP: { DT:0.15, NN:0.08, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.05, RB:0.40, IN:0.15, PRP:0.05, CD:0.05 },
VB: { DT:0.20, NN:0.10, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.25, IN:0.15, PRP:0.10, CD:0.11 },
RB: { DT:0.05, NN:0.05, NNS:0.03, VBZ:0.20, VBP:0.20, VB:0.20, RB:0.10, IN:0.07, PRP:0.05, CD:0.05 },
IN: { DT:0.45, NN:0.10, NNS:0.08, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.05, IN:0.02, PRP:0.20, CD:0.06 },
PRP: { DT:0.03, NN:0.05, NNS:0.03, VBZ:0.25, VBP:0.35, VB:0.10, RB:0.05, IN:0.05, PRP:0.02, CD:0.07 },
CD: { DT:0.05, NN:0.30, NNS:0.35, VBZ:0.05, VBP:0.05, VB:0.03, RB:0.05, IN:0.05, PRP:0.02, CD:0.05 }
};
// emission probability table: P(word | tag)
const EMIT = {
the: { DT:0.90, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0, IN:0, PRP:0, CD:0 },
a: { DT:0.88, NN:0.02, NNS:0.01, VBZ:0, VBP:0, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
an: { DT:0.88, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0, IN:0.01, PRP:0, CD:0 },
dog: { DT:0, NN:0.85, NNS:0.05, VBZ:0.02, VBP:0.01, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
cat: { DT:0, NN:0.85, NNS:0.05, VBZ:0.02, VBP:0.01, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
mat: { DT:0, NN:0.80, NNS:0.06, VBZ:0.03, VBP:0.01, VB:0.04, RB:0, IN:0, PRP:0, CD:0 },
book: { DT:0, NN:0.78, NNS:0.05, VBZ:0.02, VBP:0.03, VB:0.08, RB:0, IN:0, PRP:0, CD:0 },
time: { DT:0, NN:0.60, NNS:0.10, VBZ:0.05, VBP:0.08, VB:0.12, RB:0, IN:0, PRP:0, CD:0 },
arrow: { DT:0, NN:0.85, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.03, RB:0, IN:0, PRP:0, CD:0 },
she: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0, IN:0, PRP:0.97, CD:0 },
barks: { DT:0, NN:0.15, NNS:0.10, VBZ:0.70, VBP:0.01, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
sits: { DT:0, NN:0.05, NNS:0.05, VBZ:0.80, VBP:0.05, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
reads: { DT:0, NN:0.05, NNS:0.05, VBZ:0.80, VBP:0.05, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
flies: { DT:0, NN:0.30, NNS:0.20, VBZ:0.40, VBP:0.05, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
like: { DT:0, NN:0.05, NNS:0.02, VBZ:0.05, VBP:0.10, VB:0.15, RB:0.05, IN:0.55, PRP:0, CD:0 },
on: { DT:0, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0.05, IN:0.90, PRP:0, CD:0 },
loudly: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0.96, IN:0, PRP:0, CD:0 },
quietly: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0.96, IN:0, PRP:0, CD:0 }
};
// fallback emission for unseen words
const UNK = { DT:0.02, NN:0.25, NNS:0.15, VBZ:0.10, VBP:0.10, VB:0.12, RB:0.08, IN:0.06, PRP:0.05, CD:0.07 };
const SENTENCES = [
'The dog barks loudly',
'Time flies like an arrow',
'The cat sits on the mat',
'She reads a book quietly'
];
// runs the Viterbi algorithm on an array of word strings
function viterbi(words) {
const n = words.length;
// dp[i][t] = best probability of sequence ending with tag t at position i
const dp = [];
const bp = [];
const emit0 = EMIT[words[0].toLowerCase()] || UNK;
const row0 = {}, back0 = {};
for (const t of TAGS) {
row0[t] = (TRANS['<s>'][t] || 0.001) * (emit0[t] || 0.001);
back0[t] = null;
}
dp.push(row0); bp.push(back0);
// fill the trellis left to right
for (let i = 1; i < n; i++) {
const emit = EMIT[words[i].toLowerCase()] || UNK;
const row = {}, back = {};
for (const t of TAGS) {
let best = -1, bestPrev = null;
for (const prev of TAGS) {
const score = dp[i-1][prev] * (TRANS[prev][t] || 0.0001) * (emit[t] || 0.001);
if (score > best) { best = score; bestPrev = prev; }
}
row[t] = best; back[t] = bestPrev;
}
dp.push(row); bp.push(back);
}
// backtrack to recover the best sequence
let bestTag = TAGS.reduce((a, b) => dp[n-1][a] > dp[n-1][b] ? a : b);
const seq = [bestTag];
for (let i = n-1; i > 0; i--) { bestTag = bp[i][bestTag]; seq.unshift(bestTag); }
// compute per-word confidence as fraction of total probability mass
const confs = dp.map((row, i) => {
const total = Object.values(row).reduce((a, b) => a + b, 0);
return total > 0 ? (row[seq[i]] / total) : 0;
});
return { seq, confs };
}
const tagsRow = document.getElementById('tags-row');
const statusEl = document.getElementById('status');
const pathInfo = document.getElementById('path-info');
function reset() {
tagsRow.innerHTML = '';
statusEl.textContent = '';
statusEl.className = 'status';
pathInfo.textContent = '';
}
function tagSentence() {
reset();
const idx = parseInt(document.getElementById('sentence-select').value, 10);
const sentence = SENTENCES[idx];
const words = sentence.split(' ');
const { seq, confs } = viterbi(words);
words.forEach((word, i) => {
const card = document.createElement('div');
card.className = 'word-card';
const wt = document.createElement('div'); wt.className = 'word-text'; wt.textContent = word;
const badge = document.createElement('div');
badge.className = 'tag-badge tag-' + seq[i]; badge.textContent = seq[i];
const barWrap = document.createElement('div'); barWrap.className = 'conf-bar-wrap';
const bar = document.createElement('div'); bar.className = 'conf-bar tag-' + seq[i];
bar.style.width = '0%';
barWrap.appendChild(bar);
const confTxt = document.createElement('div');
confTxt.className = 'conf-text';
confTxt.textContent = Math.round(confs[i] * 100) + '%';
card.append(wt, badge, barWrap, confTxt);
tagsRow.appendChild(card);
// stagger card animations for a word-by-word reveal
setTimeout(() => {
card.classList.add('visible');
bar.style.width = Math.round(confs[i] * 100) + '%';
}, i * 120);
});
setTimeout(() => {
const tagStr = seq.join(' → ');
statusEl.textContent = 'Tagging complete — see the most likely path below.';
statusEl.className = 'status ok';
pathInfo.textContent = i18nFmt('Best tag path: %path%', { path: tagStr });
}, words.length * 120 + 200);
}
document.getElementById('tag-btn').addEventListener('click', tagSentence);
document.getElementById('reset-btn').addEventListener('click', reset);
<!-- HMM Part-of-Speech Tagger — Viterbi decoding demo -->
<p class="hint">A tiny HMM with hand-crafted probabilities runs Viterbi to label each word. Transition probabilities capture grammar patterns; emission probabilities capture word-to-tag likelihoods.</p>
<div class="controls">
<label for="sentence-select">Sentence:</label>
<select id="sentence-select">
<option value="0">The dog barks loudly</option>
<option value="1">Time flies like an arrow</option>
<option value="2">The cat sits on the mat</option>
<option value="3">She reads a book quietly</option>
</select>
<button id="tag-btn" type="button">Tag sentence</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div id="tags-row" class="tags-row" aria-live="polite"></div>
<div id="status" class="status" role="status"></div>
<div id="path-info" class="path-info"></div>
/* styles for the word cards and tag badges */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: 1rem; }
label { font-size: .88rem; font-weight: 600; color: #333; }
select { font: 14px system-ui, sans-serif; padding: .35rem .6rem; border: 1px solid #adb1b8;
border-radius: 6px; background: #fff; cursor: pointer; }
button { font: 600 14px system-ui, sans-serif; padding: .4rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.tags-row { display: flex; flex-wrap: wrap; gap: .5rem; min-height: 90px; align-items: flex-start; }
.word-card { display: flex; flex-direction: column; align-items: center; gap: .25rem;
opacity: 0; transform: translateY(8px);
transition: opacity .3s ease, transform .3s ease; }
.word-card.visible { opacity: 1; transform: translateY(0); }
.word-text { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.tag-badge { font: 700 11px system-ui, sans-serif; padding: .18rem .5rem; border-radius: 99px;
color: #fff; white-space: nowrap; }
.conf-bar-wrap { width: 52px; height: 5px; background: #e0e4ea; border-radius: 3px; overflow: hidden; }
.conf-bar { height: 100%; border-radius: 3px; transition: width .4s; }
.conf-text { font-size: .72rem; color: #666; }
.status { font-size: .95rem; font-weight: 600; margin: .6rem 0 .3rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.path-info { font-size: .8rem; color: #555; line-height: 1.5; }
/* one color per POS tag for quick visual scanning */
.tag-DT { background: #457b9d; }
.tag-NN { background: #1d3557; }
.tag-NNS { background: #264653; }
.tag-VBZ { background: #e76f51; }
.tag-VBP { background: #f4a261; }
.tag-VB { background: #2a9d8f; }
.tag-RB { background: #e9c46a; color: #222 !important; }
.tag-IN { background: #6d6875; }
.tag-PRP { background: #b5838d; }
.tag-CD { background: #4a4e69; }
// Hidden Markov Model: states = POS tags, observations = words
// Viterbi finds the most likely tag sequence in O(T * N^2) time
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// the set of part-of-speech tags used in this model
const TAGS = ['DT','NN','NNS','VBZ','VBP','VB','RB','IN','PRP','CD'];
// transition probability matrix: P(tag_i | tag_{i-1})
// each row sums to ~1; <s> is the sentence-start state
const TRANS = {
'<s>': { DT:0.35, NN:0.10, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.03, IN:0.02, PRP:0.35, CD:0.06 },
DT: { DT:0.01, NN:0.55, NNS:0.30, VBZ:0.01, VBP:0.01, VB:0.01, RB:0.03, IN:0.02, PRP:0.01, CD:0.05 },
NN: { DT:0.02, NN:0.08, NNS:0.04, VBZ:0.45, VBP:0.10, VB:0.03, RB:0.05, IN:0.15, PRP:0.02, CD:0.06 },
NNS: { DT:0.02, NN:0.06, NNS:0.03, VBZ:0.05, VBP:0.50, VB:0.05, RB:0.05, IN:0.15, PRP:0.02, CD:0.07 },
VBZ: { DT:0.15, NN:0.08, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.05, RB:0.40, IN:0.15, PRP:0.05, CD:0.05 },
VBP: { DT:0.15, NN:0.08, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.05, RB:0.40, IN:0.15, PRP:0.05, CD:0.05 },
VB: { DT:0.20, NN:0.10, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.25, IN:0.15, PRP:0.10, CD:0.11 },
RB: { DT:0.05, NN:0.05, NNS:0.03, VBZ:0.20, VBP:0.20, VB:0.20, RB:0.10, IN:0.07, PRP:0.05, CD:0.05 },
IN: { DT:0.45, NN:0.10, NNS:0.08, VBZ:0.01, VBP:0.01, VB:0.02, RB:0.05, IN:0.02, PRP:0.20, CD:0.06 },
PRP: { DT:0.03, NN:0.05, NNS:0.03, VBZ:0.25, VBP:0.35, VB:0.10, RB:0.05, IN:0.05, PRP:0.02, CD:0.07 },
CD: { DT:0.05, NN:0.30, NNS:0.35, VBZ:0.05, VBP:0.05, VB:0.03, RB:0.05, IN:0.05, PRP:0.02, CD:0.05 }
};
// emission probability table: P(word | tag)
const EMIT = {
the: { DT:0.90, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0, IN:0, PRP:0, CD:0 },
a: { DT:0.88, NN:0.02, NNS:0.01, VBZ:0, VBP:0, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
an: { DT:0.88, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0, IN:0.01, PRP:0, CD:0 },
dog: { DT:0, NN:0.85, NNS:0.05, VBZ:0.02, VBP:0.01, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
cat: { DT:0, NN:0.85, NNS:0.05, VBZ:0.02, VBP:0.01, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
mat: { DT:0, NN:0.80, NNS:0.06, VBZ:0.03, VBP:0.01, VB:0.04, RB:0, IN:0, PRP:0, CD:0 },
book: { DT:0, NN:0.78, NNS:0.05, VBZ:0.02, VBP:0.03, VB:0.08, RB:0, IN:0, PRP:0, CD:0 },
time: { DT:0, NN:0.60, NNS:0.10, VBZ:0.05, VBP:0.08, VB:0.12, RB:0, IN:0, PRP:0, CD:0 },
arrow: { DT:0, NN:0.85, NNS:0.05, VBZ:0.01, VBP:0.01, VB:0.03, RB:0, IN:0, PRP:0, CD:0 },
she: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0, IN:0, PRP:0.97, CD:0 },
barks: { DT:0, NN:0.15, NNS:0.10, VBZ:0.70, VBP:0.01, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
sits: { DT:0, NN:0.05, NNS:0.05, VBZ:0.80, VBP:0.05, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
reads: { DT:0, NN:0.05, NNS:0.05, VBZ:0.80, VBP:0.05, VB:0.02, RB:0, IN:0, PRP:0, CD:0 },
flies: { DT:0, NN:0.30, NNS:0.20, VBZ:0.40, VBP:0.05, VB:0.01, RB:0, IN:0, PRP:0, CD:0 },
like: { DT:0, NN:0.05, NNS:0.02, VBZ:0.05, VBP:0.10, VB:0.15, RB:0.05, IN:0.55, PRP:0, CD:0 },
on: { DT:0, NN:0.01, NNS:0.01, VBZ:0, VBP:0, VB:0, RB:0.05, IN:0.90, PRP:0, CD:0 },
loudly: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0.96, IN:0, PRP:0, CD:0 },
quietly: { DT:0, NN:0.01, NNS:0, VBZ:0, VBP:0, VB:0, RB:0.96, IN:0, PRP:0, CD:0 }
};
// fallback emission for unseen words
const UNK = { DT:0.02, NN:0.25, NNS:0.15, VBZ:0.10, VBP:0.10, VB:0.12, RB:0.08, IN:0.06, PRP:0.05, CD:0.07 };
const SENTENCES = [
'The dog barks loudly',
'Time flies like an arrow',
'The cat sits on the mat',
'She reads a book quietly'
];
// runs the Viterbi algorithm on an array of word strings
function viterbi(words) {
const n = words.length;
// dp[i][t] = best probability of sequence ending with tag t at position i
const dp = [];
const bp = [];
const emit0 = EMIT[words[0].toLowerCase()] || UNK;
const row0 = {}, back0 = {};
for (const t of TAGS) {
row0[t] = (TRANS['<s>'][t] || 0.001) * (emit0[t] || 0.001);
back0[t] = null;
}
dp.push(row0); bp.push(back0);
// fill the trellis left to right
for (let i = 1; i < n; i++) {
const emit = EMIT[words[i].toLowerCase()] || UNK;
const row = {}, back = {};
for (const t of TAGS) {
let best = -1, bestPrev = null;
for (const prev of TAGS) {
const score = dp[i-1][prev] * (TRANS[prev][t] || 0.0001) * (emit[t] || 0.001);
if (score > best) { best = score; bestPrev = prev; }
}
row[t] = best; back[t] = bestPrev;
}
dp.push(row); bp.push(back);
}
// backtrack to recover the best sequence
let bestTag = TAGS.reduce((a, b) => dp[n-1][a] > dp[n-1][b] ? a : b);
const seq = [bestTag];
for (let i = n-1; i > 0; i--) { bestTag = bp[i][bestTag]; seq.unshift(bestTag); }
// compute per-word confidence as fraction of total probability mass
const confs = dp.map((row, i) => {
const total = Object.values(row).reduce((a, b) => a + b, 0);
return total > 0 ? (row[seq[i]] / total) : 0;
});
return { seq, confs };
}
const tagsRow = document.getElementById('tags-row');
const statusEl = document.getElementById('status');
const pathInfo = document.getElementById('path-info');
function reset() {
tagsRow.innerHTML = '';
statusEl.textContent = '';
statusEl.className = 'status';
pathInfo.textContent = '';
}
function tagSentence() {
reset();
const idx = parseInt(document.getElementById('sentence-select').value, 10);
const sentence = SENTENCES[idx];
const words = sentence.split(' ');
const { seq, confs } = viterbi(words);
words.forEach((word, i) => {
const card = document.createElement('div');
card.className = 'word-card';
const wt = document.createElement('div'); wt.className = 'word-text'; wt.textContent = word;
const badge = document.createElement('div');
badge.className = 'tag-badge tag-' + seq[i]; badge.textContent = seq[i];
const barWrap = document.createElement('div'); barWrap.className = 'conf-bar-wrap';
const bar = document.createElement('div'); bar.className = 'conf-bar tag-' + seq[i];
bar.style.width = '0%';
barWrap.appendChild(bar);
const confTxt = document.createElement('div');
confTxt.className = 'conf-text';
confTxt.textContent = Math.round(confs[i] * 100) + '%';
card.append(wt, badge, barWrap, confTxt);
tagsRow.appendChild(card);
// stagger card animations for a word-by-word reveal
setTimeout(() => {
card.classList.add('visible');
bar.style.width = Math.round(confs[i] * 100) + '%';
}, i * 120);
});
setTimeout(() => {
const tagStr = seq.join(' → ');
statusEl.textContent = 'Tagging complete — see the most likely path below.';
statusEl.className = 'status ok';
pathInfo.textContent = i18nFmt('Best tag path: %path%', { path: tagStr });
}, words.length * 120 + 200);
}
document.getElementById('tag-btn').addEventListener('click', tagSentence);
document.getElementById('reset-btn').addEventListener('click', reset);
Notice that the tagger doesn't look at each word in isolation — it picks the tag sequence that maximises the joint probability of words and tags together. That is why "flies" gets tagged as a verb in "Time flies like an arrow" but as a noun in another context: the surrounding words shift the balance of probabilities.
Comments
Loading comments...