This demo simulates a simple 3-class classification problem (Cat / Fox / Dog). The teacher is a confident model whose outputs have been softened with a temperature parameter. The student trains on either hard one-hot labels or the teacher's soft probabilities.
<div class="hint">
<strong>3-class problem:</strong> Cat / Fox / Dog. The <em>teacher</em> knows the true class and outputs soft probabilities.
Compare how a <em>student</em> learns from hard one-hot labels vs. the teacher's soft distribution.
</div>
<div id="scenario-row">
<label>True class:
<select id="true-class">
<option value="0">Cat</option>
<option value="1">Fox</option>
<option value="2">Dog</option>
</select>
</label>
<label>Temperature T:
<input id="temp" type="range" min="1" max="10" value="4" step="1">
<span id="temp-val">4</span>
</label>
</div>
<div id="panels">
<div class="panel">
<div class="panel-title teacher">Teacher output (soft)</div>
<div id="teacher-bars" class="bars"></div>
</div>
<div class="panel">
<div class="panel-title hard">Hard label (one-hot)</div>
<div id="hard-bars" class="bars"></div>
</div>
</div>
<div id="info-box"></div>
<div class="btns">
<button id="btn-soft" type="button">Train student on soft targets</button>
<button id="btn-hard" type="button">Train student on hard labels</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="student-panels" style="display:none">
<div class="panel">
<div class="panel-title soft-student">Student (soft training)</div>
<div id="soft-student-bars" class="bars"></div>
<div class="student-info" id="soft-info"></div>
</div>
<div class="panel">
<div class="panel-title hard-student">Student (hard training)</div>
<div id="hard-student-bars" class="bars"></div>
<div class="student-info" id="hard-info"></div>
</div>
</div>
* { 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-bottom: .8rem; line-height: 1.5; }
#scenario-row { display: flex; gap: 1.4rem; align-items: center; flex-wrap: wrap; margin-bottom: .9rem; }
#scenario-row label { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
select, input[type=range] { font-size: 14px; }
#panels, #student-panels { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: .8rem; }
.panel { border: 1px solid #d0d7de; border-radius: 8px; padding: .7rem .9rem; }
.panel-title { font-weight: 700; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .5rem; }
.panel-title.teacher { color: #1a7abf; }
.panel-title.hard { color: #888; }
.panel-title.soft-student { color: #0a7d33; }
.panel-title.hard-student { color: #c25700; }
.bar-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.bar-label { width: 34px; font-size: .82rem; font-weight: 600; flex-shrink: 0; }
.bar-track { flex: 1; height: 18px; background: #eef0f2; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .5s ease; }
.bar-fill.teacher-c { background: #4aa8e8; }
.bar-fill.hard-c { background: #bbb; }
.bar-fill.soft-student-c { background: #2db85a; }
.bar-fill.hard-student-c { background: #e07b30; }
.bar-pct { width: 38px; text-align: right; font-size: .82rem; font-variant-numeric: tabular-nums; }
#info-box { font-size: .88rem; color: #333; background: #f0f6ff; border-left: 3px solid #4aa8e8; padding: .5rem .7rem; border-radius: 4px; margin-bottom: .8rem; min-height: 2.4em; line-height: 1.5; }
.student-info { font-size: .8rem; color: #555; margin-top: .4rem; line-height: 1.4; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .88; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Knowledge Distillation demo.
// Simulates a 3-class teacher whose logits encode similarity (cat≈fox >> dog),
// then shows what a student learns from (a) soft teacher probabilities vs (b) hard one-hot labels.
// Core math: softmax with temperature, KL divergence, and a simplified gradient descent update.
const CLASSES = ['Cat', 'Fox', 'Dog'];
// Teacher's raw logits per true class (encodes that cat and fox are similar, dog is different)
const TEACHER_LOGITS = [
[3.0, 1.2, -1.0], // true class = Cat
[1.0, 3.0, -0.5], // true class = Fox
[-0.5, 0.2, 3.5], // true class = Dog
];
function softmax(logits, T) {
const scaled = logits.map(x => x / T);
const max = Math.max(...scaled);
const exps = scaled.map(x => Math.exp(x - max));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(x => x / sum);
}
function klDiv(p, q) {
// KL(p || q), where p = target, q = student
return p.reduce((acc, pi, i) => acc + (pi > 1e-10 ? pi * Math.log(pi / Math.max(q[i], 1e-10)) : 0), 0);
}
// Simplified student: starts from uniform, gradient descent on cross-entropy loss.
// Returns final probability distribution after 'steps' steps.
function trainStudent(target, steps, lr) {
// Student logits start at zero (uniform)
let logits = [0, 0, 0];
for (let s = 0; s < steps; s++) {
const probs = softmax(logits, 1);
// Gradient of cross-entropy loss w.r.t. logits: grad = probs - target
const grad = probs.map((p, i) => p - target[i]);
logits = logits.map((l, i) => l - lr * grad[i]);
}
return softmax(logits, 1);
}
let trueClass = 0;
let temperature = 4;
function getTeacherProbs() {
return softmax(TEACHER_LOGITS[trueClass], temperature);
}
function getHardLabel() {
return CLASSES.map((_, i) => i === trueClass ? 1.0 : 0.0);
}
function renderBars(containerId, probs, colorClass) {
const el = document.getElementById(containerId);
el.innerHTML = '';
probs.forEach((p, i) => {
const row = document.createElement('div');
row.className = 'bar-row';
const pct = (p * 100).toFixed(1);
row.innerHTML = `
<span class="bar-label">${CLASSES[i]}</span>
<div class="bar-track">
<div class="bar-fill ${colorClass}" style="width:${pct}%"></div>
</div>
<span class="bar-pct">${pct}%</span>
`;
el.appendChild(row);
});
}
function updateInfo() {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
const box = document.getElementById('info-box');
const secondBest = teacherProbs.map((p, i) => ({p, i}))
.sort((a, b) => b.p - a.p)[1];
box.innerHTML = fmt('Teacher sees <strong>%v%</strong>. With T=%temperature%, it assigns <strong>%v2%%</strong> to the true class and still gives <strong>%v3%%</strong> to <strong>%v4%</strong>. A hard label would give 100% / 0% / 0% — discarding the relational signal entirely.', { v: CLASSES[trueClass], temperature: temperature, v2: (teacherProbs[trueClass]*100).toFixed(1), v3: (secondBest.p*100).toFixed(1), v4: CLASSES[secondBest.i] });
}
function renderAll() {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
renderBars('teacher-bars', teacherProbs, 'teacher-c');
renderBars('hard-bars', hardLabel, 'hard-c');
updateInfo();
document.getElementById('student-panels').style.display = 'none';
}
document.getElementById('true-class').addEventListener('change', e => {
trueClass = parseInt(e.target.value);
renderAll();
});
document.getElementById('temp').addEventListener('input', e => {
temperature = parseInt(e.target.value);
document.getElementById('temp-val').textContent = temperature;
renderAll();
});
document.getElementById('btn-soft').addEventListener('click', () => {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
// Train student on soft targets
const softStudent = trainStudent(teacherProbs, 200, 0.1);
// Train student on hard labels
const hardStudent = trainStudent(hardLabel, 200, 0.1);
renderBars('soft-student-bars', softStudent, 'soft-student-c');
renderBars('hard-student-bars', hardStudent, 'hard-student-c');
const klSoft = klDiv(teacherProbs, softStudent);
const klHard = klDiv(teacherProbs, hardStudent);
document.getElementById('soft-info').innerHTML =
fmt('KL divergence from teacher: <strong>%klSoft%</strong>. Learned the relational structure.', { klSoft: klSoft.toFixed(4) });
document.getElementById('hard-info').innerHTML =
fmt('KL divergence from teacher: <strong>%klHard%</strong>. Overconfident on true class.', { klHard: klHard.toFixed(4) });
document.getElementById('student-panels').style.display = 'grid';
console.assert(klSoft < klHard,
fmt('Soft student should be closer to teacher than hard student: klSoft=%klSoft% klHard=%klHard%', { klSoft: klSoft, klHard: klHard }));
console.log('Soft student KL:', klSoft.toFixed(4), '| Hard student KL:', klHard.toFixed(4),
'| Soft better:', klSoft < klHard);
});
document.getElementById('btn-hard').addEventListener('click', () => {
// Same as btn-soft but only trains hard student first for contrast
document.getElementById('btn-soft').click();
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('student-panels').style.display = 'none';
renderAll();
});
renderAll();
<div class="hint">
<strong>3-class problem:</strong> Cat / Fox / Dog. The <em>teacher</em> knows the true class and outputs soft probabilities.
Compare how a <em>student</em> learns from hard one-hot labels vs. the teacher's soft distribution.
</div>
<div id="scenario-row">
<label>True class:
<select id="true-class">
<option value="0">Cat</option>
<option value="1">Fox</option>
<option value="2">Dog</option>
</select>
</label>
<label>Temperature T:
<input id="temp" type="range" min="1" max="10" value="4" step="1">
<span id="temp-val">4</span>
</label>
</div>
<div id="panels">
<div class="panel">
<div class="panel-title teacher">Teacher output (soft)</div>
<div id="teacher-bars" class="bars"></div>
</div>
<div class="panel">
<div class="panel-title hard">Hard label (one-hot)</div>
<div id="hard-bars" class="bars"></div>
</div>
</div>
<div id="info-box"></div>
<div class="btns">
<button id="btn-soft" type="button">Train student on soft targets</button>
<button id="btn-hard" type="button">Train student on hard labels</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="student-panels" style="display:none">
<div class="panel">
<div class="panel-title soft-student">Student (soft training)</div>
<div id="soft-student-bars" class="bars"></div>
<div class="student-info" id="soft-info"></div>
</div>
<div class="panel">
<div class="panel-title hard-student">Student (hard training)</div>
<div id="hard-student-bars" class="bars"></div>
<div class="student-info" id="hard-info"></div>
</div>
</div>
* { 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-bottom: .8rem; line-height: 1.5; }
#scenario-row { display: flex; gap: 1.4rem; align-items: center; flex-wrap: wrap; margin-bottom: .9rem; }
#scenario-row label { display: flex; align-items: center; gap: .4rem; font-weight: 600; }
select, input[type=range] { font-size: 14px; }
#panels, #student-panels { display: grid; grid-template-columns: 1fr 1fr; gap: .8rem; margin-bottom: .8rem; }
.panel { border: 1px solid #d0d7de; border-radius: 8px; padding: .7rem .9rem; }
.panel-title { font-weight: 700; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; margin-bottom: .5rem; }
.panel-title.teacher { color: #1a7abf; }
.panel-title.hard { color: #888; }
.panel-title.soft-student { color: #0a7d33; }
.panel-title.hard-student { color: #c25700; }
.bar-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .35rem; }
.bar-label { width: 34px; font-size: .82rem; font-weight: 600; flex-shrink: 0; }
.bar-track { flex: 1; height: 18px; background: #eef0f2; border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; border-radius: 4px; transition: width .5s ease; }
.bar-fill.teacher-c { background: #4aa8e8; }
.bar-fill.hard-c { background: #bbb; }
.bar-fill.soft-student-c { background: #2db85a; }
.bar-fill.hard-student-c { background: #e07b30; }
.bar-pct { width: 38px; text-align: right; font-size: .82rem; font-variant-numeric: tabular-nums; }
#info-box { font-size: .88rem; color: #333; background: #f0f6ff; border-left: 3px solid #4aa8e8; padding: .5rem .7rem; border-radius: 4px; margin-bottom: .8rem; min-height: 2.4em; line-height: 1.5; }
.student-info { font-size: .8rem; color: #555; margin-top: .4rem; line-height: 1.4; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:hover { opacity: .88; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Knowledge Distillation demo.
// Simulates a 3-class teacher whose logits encode similarity (cat≈fox >> dog),
// then shows what a student learns from (a) soft teacher probabilities vs (b) hard one-hot labels.
// Core math: softmax with temperature, KL divergence, and a simplified gradient descent update.
const CLASSES = ['Cat', 'Fox', 'Dog'];
// Teacher's raw logits per true class (encodes that cat and fox are similar, dog is different)
const TEACHER_LOGITS = [
[3.0, 1.2, -1.0], // true class = Cat
[1.0, 3.0, -0.5], // true class = Fox
[-0.5, 0.2, 3.5], // true class = Dog
];
function softmax(logits, T) {
const scaled = logits.map(x => x / T);
const max = Math.max(...scaled);
const exps = scaled.map(x => Math.exp(x - max));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(x => x / sum);
}
function klDiv(p, q) {
// KL(p || q), where p = target, q = student
return p.reduce((acc, pi, i) => acc + (pi > 1e-10 ? pi * Math.log(pi / Math.max(q[i], 1e-10)) : 0), 0);
}
// Simplified student: starts from uniform, gradient descent on cross-entropy loss.
// Returns final probability distribution after 'steps' steps.
function trainStudent(target, steps, lr) {
// Student logits start at zero (uniform)
let logits = [0, 0, 0];
for (let s = 0; s < steps; s++) {
const probs = softmax(logits, 1);
// Gradient of cross-entropy loss w.r.t. logits: grad = probs - target
const grad = probs.map((p, i) => p - target[i]);
logits = logits.map((l, i) => l - lr * grad[i]);
}
return softmax(logits, 1);
}
let trueClass = 0;
let temperature = 4;
function getTeacherProbs() {
return softmax(TEACHER_LOGITS[trueClass], temperature);
}
function getHardLabel() {
return CLASSES.map((_, i) => i === trueClass ? 1.0 : 0.0);
}
function renderBars(containerId, probs, colorClass) {
const el = document.getElementById(containerId);
el.innerHTML = '';
probs.forEach((p, i) => {
const row = document.createElement('div');
row.className = 'bar-row';
const pct = (p * 100).toFixed(1);
row.innerHTML = `
<span class="bar-label">${CLASSES[i]}</span>
<div class="bar-track">
<div class="bar-fill ${colorClass}" style="width:${pct}%"></div>
</div>
<span class="bar-pct">${pct}%</span>
`;
el.appendChild(row);
});
}
function updateInfo() {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
const box = document.getElementById('info-box');
const secondBest = teacherProbs.map((p, i) => ({p, i}))
.sort((a, b) => b.p - a.p)[1];
box.innerHTML = fmt('Teacher sees <strong>%v%</strong>. With T=%temperature%, it assigns <strong>%v2%%</strong> to the true class and still gives <strong>%v3%%</strong> to <strong>%v4%</strong>. A hard label would give 100% / 0% / 0% — discarding the relational signal entirely.', { v: CLASSES[trueClass], temperature: temperature, v2: (teacherProbs[trueClass]*100).toFixed(1), v3: (secondBest.p*100).toFixed(1), v4: CLASSES[secondBest.i] });
}
function renderAll() {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
renderBars('teacher-bars', teacherProbs, 'teacher-c');
renderBars('hard-bars', hardLabel, 'hard-c');
updateInfo();
document.getElementById('student-panels').style.display = 'none';
}
document.getElementById('true-class').addEventListener('change', e => {
trueClass = parseInt(e.target.value);
renderAll();
});
document.getElementById('temp').addEventListener('input', e => {
temperature = parseInt(e.target.value);
document.getElementById('temp-val').textContent = temperature;
renderAll();
});
document.getElementById('btn-soft').addEventListener('click', () => {
const teacherProbs = getTeacherProbs();
const hardLabel = getHardLabel();
// Train student on soft targets
const softStudent = trainStudent(teacherProbs, 200, 0.1);
// Train student on hard labels
const hardStudent = trainStudent(hardLabel, 200, 0.1);
renderBars('soft-student-bars', softStudent, 'soft-student-c');
renderBars('hard-student-bars', hardStudent, 'hard-student-c');
const klSoft = klDiv(teacherProbs, softStudent);
const klHard = klDiv(teacherProbs, hardStudent);
document.getElementById('soft-info').innerHTML =
fmt('KL divergence from teacher: <strong>%klSoft%</strong>. Learned the relational structure.', { klSoft: klSoft.toFixed(4) });
document.getElementById('hard-info').innerHTML =
fmt('KL divergence from teacher: <strong>%klHard%</strong>. Overconfident on true class.', { klHard: klHard.toFixed(4) });
document.getElementById('student-panels').style.display = 'grid';
console.assert(klSoft < klHard,
fmt('Soft student should be closer to teacher than hard student: klSoft=%klSoft% klHard=%klHard%', { klSoft: klSoft, klHard: klHard }));
console.log('Soft student KL:', klSoft.toFixed(4), '| Hard student KL:', klHard.toFixed(4),
'| Soft better:', klSoft < klHard);
});
document.getElementById('btn-hard').addEventListener('click', () => {
// Same as btn-soft but only trains hard student first for contrast
document.getElementById('btn-soft').click();
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('student-panels').style.display = 'none';
renderAll();
});
renderAll();
Comments
Loading comments...