Below is a short sentence with a hidden gold annotation. Click any word to cycle its label through O (Other), B-PER (person start), I-PER (person continuation), B-ORG (organization start), and I-ORG (organization continuation). The score panel shows how well your labeling agrees with typical CRF transition and emission features.
<!-- CRF Sequence Labeling demo — click each word to cycle its BIO tag -->
<p class="hint">Click any word to cycle its label: O, B-PER, I-PER, B-ORG, I-ORG. The score rewards correct labels (+10) and valid BIO transitions (+3). Red borders mark illegal transitions.</p>
<div id="sentence" class="sentence" role="group" aria-label="Labeled sentence — click words to change their tags"></div>
<div class="score-panel">
<div class="score-row">
<span class="score-label">Score:</span>
<span id="score-value" class="score-value">0</span>
<span class="score-bar-wrap"><span id="score-bar" class="score-bar"></span></span>
</div>
<div id="violations" class="violations"></div>
</div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Reset</button>
<button id="btn-reveal" type="button">Show gold labels</button>
</div>
<div id="legend" class="legend">
<span class="tag-O">O</span> Other
<span class="tag-B-PER">B-PER</span> <span class="tag-I-PER">I-PER</span> Person
<span class="tag-B-ORG">B-ORG</span> <span class="tag-I-ORG">I-ORG</span> Organization
</div>
/* CRF demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.sentence { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: .8rem; }
.token { display: inline-flex; flex-direction: column; align-items: center; cursor: pointer;
border-radius: 8px; padding: 6px 10px 5px; border: 2px solid #cdd9e3;
background: #f0f4f8; transition: all .12s; user-select: none; }
.token:hover { border-color: #5a87c0; }
.token .word { font: 700 15px ui-monospace, monospace; }
.token .tag { font-size: .7rem; font-weight: 600; margin-top: 3px; padding: 1px 5px;
border-radius: 4px; letter-spacing: .03em; }
/* Tag color coding */
.token[data-tag="O"] .tag { background: #e8eef3; color: #5a7088; }
.token[data-tag="B-PER"] .tag, .token[data-tag="I-PER"] .tag { background: #d4f0e0; color: #1a6e3c; }
.token[data-tag="B-ORG"] .tag, .token[data-tag="I-ORG"] .tag { background: #dde7fb; color: #1d3a8a; }
.token.violation { border-color: #e63946; }
.score-panel { display: flex; flex-direction: column; gap: 4px; margin-bottom: .7rem; }
.score-row { display: flex; align-items: center; gap: 8px; }
.score-label { font-size: .85rem; color: #555; min-width: 80px; }
.score-value { font: 700 15px system-ui; min-width: 28px; text-align: right; }
.score-bar-wrap { flex: 1; height: 10px; background: #e0e7ef; border-radius: 5px; overflow: hidden; }
.score-bar { height: 100%; background: #1d8a4e; border-radius: 5px; width: 0; transition: width .3s; }
.violations { font-size: .8rem; color: #c0392b; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .78rem; color: #555; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.legend .tag-O { background: #e8eef3; color: #5a7088; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.legend .tag-B-PER, .legend .tag-I-PER { background: #d4f0e0; color: #1a6e3c; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.legend .tag-B-ORG, .legend .tag-I-ORG { background: #dde7fb; color: #1d8a4e; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.tag-I-PER { background: #d4f0e0; color: #1a6e3c; }
.tag-B-ORG { background: #dde7fb; color: #1d3a8a; }
.tag-I-ORG { background: #dde7fb; color: #1d3a8a; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The input sentence (tokens)
const WORDS = ['Marie', 'Curie', 'worked', 'at', 'the', 'University', 'of', 'Paris'];
// Gold (reference) BIO labels for this sentence
const GOLD = ['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG'];
const LABELS = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG'];
// Current user labeling, starts all-O
let current = WORDS.map(() => 'O');
// Allowed BIO transitions (valid CRF label pairs)
// I-X after O is illegal; I-X after B-Y or I-Y where X!=Y is also illegal
const ALLOWED_TRANS = new Set([
'O->O', 'O->B-PER', 'O->B-ORG',
'B-PER->I-PER', 'B-PER->O', 'B-PER->B-ORG', 'B-PER->B-PER',
'I-PER->I-PER', 'I-PER->O', 'I-PER->B-ORG', 'I-PER->B-PER',
'B-ORG->I-ORG', 'B-ORG->O', 'B-ORG->B-PER', 'B-ORG->B-ORG',
'I-ORG->I-ORG', 'I-ORG->O', 'I-ORG->B-PER', 'I-ORG->B-ORG'
]);
// Compute score and collect BIO violations
function computeScore() {
let score = 0;
const violations = [];
// Emission score: +10 for each token matching the gold label
current.forEach((tag, i) => { if (tag === GOLD[i]) score += 10; });
// Transition score: +3 for each consecutive valid BIO pair
for (let i = 0; i < current.length - 1; i++) {
const trans = current[i] + '->' + current[i + 1];
if (ALLOWED_TRANS.has(trans)) score += 3;
else violations.push(i18nFmt('%a% then %b% is not a valid BIO transition.', { a: current[i], b: current[i + 1] }));
}
return { score, violations };
}
function render() {
const container = document.getElementById('sentence');
container.innerHTML = '';
WORDS.forEach((word, i) => {
const tok = document.createElement('span');
tok.className = 'token';
tok.dataset.tag = current[i];
tok.title = 'Click to change label';
tok.innerHTML = '<span class="word">' + word + '</span><span class="tag">' + current[i] + '</span>';
tok.addEventListener('click', () => {
const idx = LABELS.indexOf(current[i]);
current[i] = LABELS[(idx + 1) % LABELS.length];
updateAll();
});
container.appendChild(tok);
});
updateScore();
}
function updateScore() {
const { score, violations } = computeScore();
const maxScore = WORDS.length * 10 + (WORDS.length - 1) * 3;
document.getElementById('score-value').textContent = score;
document.getElementById('score-bar').style.width = Math.round((score / maxScore) * 100) + '%';
const vEl = document.getElementById('violations');
if (violations.length) {
vEl.textContent = i18nFmt('%n% invalid transition(s):', { n: violations.length }) + ' ' + violations[0];
} else {
vEl.textContent = score >= maxScore * 0.85 ? 'Great labeling — all transitions valid!' : 'Keep adjusting to improve the score.';
}
// Highlight tokens whose outgoing transition is illegal
document.querySelectorAll('.token').forEach((tok, i) => {
const trans = i < current.length - 1 ? current[i] + '->' + current[i + 1] : null;
tok.classList.toggle('violation', trans !== null && !ALLOWED_TRANS.has(trans));
});
}
function updateAll() {
document.querySelectorAll('.token').forEach((tok, i) => {
tok.dataset.tag = current[i];
tok.querySelector('.tag').textContent = current[i];
});
updateScore();
}
document.getElementById('btn-reset').addEventListener('click', () => {
current = WORDS.map(() => 'O');
render();
});
document.getElementById('btn-reveal').addEventListener('click', () => {
current = [...GOLD];
updateAll();
});
render();
<!-- CRF Sequence Labeling demo — click each word to cycle its BIO tag -->
<p class="hint">Click any word to cycle its label: O, B-PER, I-PER, B-ORG, I-ORG. The score rewards correct labels (+10) and valid BIO transitions (+3). Red borders mark illegal transitions.</p>
<div id="sentence" class="sentence" role="group" aria-label="Labeled sentence — click words to change their tags"></div>
<div class="score-panel">
<div class="score-row">
<span class="score-label">Score:</span>
<span id="score-value" class="score-value">0</span>
<span class="score-bar-wrap"><span id="score-bar" class="score-bar"></span></span>
</div>
<div id="violations" class="violations"></div>
</div>
<div class="btns">
<button id="btn-reset" type="button" class="ghost">Reset</button>
<button id="btn-reveal" type="button">Show gold labels</button>
</div>
<div id="legend" class="legend">
<span class="tag-O">O</span> Other
<span class="tag-B-PER">B-PER</span> <span class="tag-I-PER">I-PER</span> Person
<span class="tag-B-ORG">B-ORG</span> <span class="tag-I-ORG">I-ORG</span> Organization
</div>
/* CRF demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.sentence { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: .8rem; }
.token { display: inline-flex; flex-direction: column; align-items: center; cursor: pointer;
border-radius: 8px; padding: 6px 10px 5px; border: 2px solid #cdd9e3;
background: #f0f4f8; transition: all .12s; user-select: none; }
.token:hover { border-color: #5a87c0; }
.token .word { font: 700 15px ui-monospace, monospace; }
.token .tag { font-size: .7rem; font-weight: 600; margin-top: 3px; padding: 1px 5px;
border-radius: 4px; letter-spacing: .03em; }
/* Tag color coding */
.token[data-tag="O"] .tag { background: #e8eef3; color: #5a7088; }
.token[data-tag="B-PER"] .tag, .token[data-tag="I-PER"] .tag { background: #d4f0e0; color: #1a6e3c; }
.token[data-tag="B-ORG"] .tag, .token[data-tag="I-ORG"] .tag { background: #dde7fb; color: #1d3a8a; }
.token.violation { border-color: #e63946; }
.score-panel { display: flex; flex-direction: column; gap: 4px; margin-bottom: .7rem; }
.score-row { display: flex; align-items: center; gap: 8px; }
.score-label { font-size: .85rem; color: #555; min-width: 80px; }
.score-value { font: 700 15px system-ui; min-width: 28px; text-align: right; }
.score-bar-wrap { flex: 1; height: 10px; background: #e0e7ef; border-radius: 5px; overflow: hidden; }
.score-bar { height: 100%; background: #1d8a4e; border-radius: 5px; width: 0; transition: width .3s; }
.violations { font-size: .8rem; color: #c0392b; min-height: 1.2em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .78rem; color: #555; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.legend .tag-O { background: #e8eef3; color: #5a7088; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.legend .tag-B-PER, .legend .tag-I-PER { background: #d4f0e0; color: #1a6e3c; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.legend .tag-B-ORG, .legend .tag-I-ORG { background: #dde7fb; color: #1d8a4e; padding: 1px 6px; border-radius: 4px; font-weight: 600; }
.tag-I-PER { background: #d4f0e0; color: #1a6e3c; }
.tag-B-ORG { background: #dde7fb; color: #1d3a8a; }
.tag-I-ORG { background: #dde7fb; color: #1d3a8a; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The input sentence (tokens)
const WORDS = ['Marie', 'Curie', 'worked', 'at', 'the', 'University', 'of', 'Paris'];
// Gold (reference) BIO labels for this sentence
const GOLD = ['B-PER', 'I-PER', 'O', 'O', 'O', 'B-ORG', 'I-ORG', 'I-ORG'];
const LABELS = ['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG'];
// Current user labeling, starts all-O
let current = WORDS.map(() => 'O');
// Allowed BIO transitions (valid CRF label pairs)
// I-X after O is illegal; I-X after B-Y or I-Y where X!=Y is also illegal
const ALLOWED_TRANS = new Set([
'O->O', 'O->B-PER', 'O->B-ORG',
'B-PER->I-PER', 'B-PER->O', 'B-PER->B-ORG', 'B-PER->B-PER',
'I-PER->I-PER', 'I-PER->O', 'I-PER->B-ORG', 'I-PER->B-PER',
'B-ORG->I-ORG', 'B-ORG->O', 'B-ORG->B-PER', 'B-ORG->B-ORG',
'I-ORG->I-ORG', 'I-ORG->O', 'I-ORG->B-PER', 'I-ORG->B-ORG'
]);
// Compute score and collect BIO violations
function computeScore() {
let score = 0;
const violations = [];
// Emission score: +10 for each token matching the gold label
current.forEach((tag, i) => { if (tag === GOLD[i]) score += 10; });
// Transition score: +3 for each consecutive valid BIO pair
for (let i = 0; i < current.length - 1; i++) {
const trans = current[i] + '->' + current[i + 1];
if (ALLOWED_TRANS.has(trans)) score += 3;
else violations.push(i18nFmt('%a% then %b% is not a valid BIO transition.', { a: current[i], b: current[i + 1] }));
}
return { score, violations };
}
function render() {
const container = document.getElementById('sentence');
container.innerHTML = '';
WORDS.forEach((word, i) => {
const tok = document.createElement('span');
tok.className = 'token';
tok.dataset.tag = current[i];
tok.title = 'Click to change label';
tok.innerHTML = '<span class="word">' + word + '</span><span class="tag">' + current[i] + '</span>';
tok.addEventListener('click', () => {
const idx = LABELS.indexOf(current[i]);
current[i] = LABELS[(idx + 1) % LABELS.length];
updateAll();
});
container.appendChild(tok);
});
updateScore();
}
function updateScore() {
const { score, violations } = computeScore();
const maxScore = WORDS.length * 10 + (WORDS.length - 1) * 3;
document.getElementById('score-value').textContent = score;
document.getElementById('score-bar').style.width = Math.round((score / maxScore) * 100) + '%';
const vEl = document.getElementById('violations');
if (violations.length) {
vEl.textContent = i18nFmt('%n% invalid transition(s):', { n: violations.length }) + ' ' + violations[0];
} else {
vEl.textContent = score >= maxScore * 0.85 ? 'Great labeling — all transitions valid!' : 'Keep adjusting to improve the score.';
}
// Highlight tokens whose outgoing transition is illegal
document.querySelectorAll('.token').forEach((tok, i) => {
const trans = i < current.length - 1 ? current[i] + '->' + current[i + 1] : null;
tok.classList.toggle('violation', trans !== null && !ALLOWED_TRANS.has(trans));
});
}
function updateAll() {
document.querySelectorAll('.token').forEach((tok, i) => {
tok.dataset.tag = current[i];
tok.querySelector('.tag').textContent = current[i];
});
updateScore();
}
document.getElementById('btn-reset').addEventListener('click', () => {
current = WORDS.map(() => 'O');
render();
});
document.getElementById('btn-reveal').addEventListener('click', () => {
current = [...GOLD];
updateAll();
});
render();
Notice that some transitions feel "wrong" even before checking the score — I-ORG right after O is a red flag, because the BIO convention says a continuation tag must follow a matching begin tag. A CRF learns exactly these cross-position constraints from data, encoding them as transition features in the model's log-linear score.
Comments
Loading comments...