<!-- MDN demo: one input x maps to a bimodal distribution over y -->
<p class="hint">Drag the slider to change the input value <b>x</b>. The <b>blue curve</b> is the full MDN mixture distribution p(y|x). The dashed curves are the two individual Gaussian components. Notice how the distribution splits into two peaks in the middle — a plain regressor would predict their average.</p>
<div class="controls">
<label for="xslider">Input x = <strong id="xval">0.50</strong></label>
<input id="xslider" type="range" min="0" max="1" step="0.01" value="0.50">
</div>
<canvas id="chart" width="480" height="220"></canvas>
<div class="mode-info" id="modeinfo"></div>
<div class="btns">
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
/* demo layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
#xslider { flex: 1; min-width: 140px; max-width: 320px; accent-color: #1d3557; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; }
.mode-info { font-size: .85rem; color: #444; margin: .5rem 0; min-height: 2.4em; line-height: 1.5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Mixture Density Network demo */
/* The network outputs parameters for K=2 Gaussian components: pi (weight), mu (mean), sigma (std dev) */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Gaussian probability density function */
function gaussian(y, mu, sigma) {
const s2 = sigma * sigma;
return Math.exp(-0.5 * (y - mu) * (y - mu) / s2) / Math.sqrt(2 * Math.PI * s2);
}
/* Simulate MDN output for a given input x in [0,1]. */
/* Component weights, means, and sigmas vary smoothly with x to create a bimodal effect. */
function mdnParams(x) {
/* mixing weights: low mode dominates at x=0, high mode at x=1, equal in the middle */
const wLow = 0.5 + 0.45 * Math.cos(Math.PI * x);
const wHigh = 1 - wLow;
/* means: low mode near 0.15, high mode near 0.75, both drift slightly with x */
const muLow = 0.15 + 0.10 * x;
const muHigh = 0.75 + 0.10 * x;
/* sigmas: widest in the ambiguous middle, narrower at extremes */
const sigLow = 0.06 + 0.04 * (1 - Math.abs(2 * x - 1));
const sigHigh = 0.06 + 0.04 * (1 - Math.abs(2 * x - 1));
return [
{ pi: wLow, mu: muLow, sigma: sigLow },
{ pi: wHigh, mu: muHigh, sigma: sigHigh }
];
}
/* Draw the mixture distribution on the canvas */
function draw(x) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 16, bottom: 28, left: 36 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
ctx.clearRect(0, 0, W, H);
const comps = mdnParams(x);
const STEPS = 300;
const ys = Array.from({ length: STEPS }, (_, i) => i / (STEPS - 1));
/* evaluate full mixture PDF over y in [0,1] */
const pdf = ys.map(y => comps.reduce((s, c) => s + c.pi * gaussian(y, c.mu, c.sigma), 0));
const maxP = Math.max(...pdf);
const toCanvasX = y => PAD.left + y * plotW;
const toCanvasY = p => PAD.top + plotH - (p / (maxP * 1.1)) * plotH;
/* draw background grid */
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const cy = PAD.top + (i / 4) * plotH;
ctx.beginPath(); ctx.moveTo(PAD.left, cy); ctx.lineTo(PAD.left + plotW, cy); ctx.stroke();
}
for (let i = 0; i <= 5; i++) {
const cx = PAD.left + (i / 5) * plotW;
ctx.beginPath(); ctx.moveTo(cx, PAD.top); ctx.lineTo(cx, PAD.top + plotH); ctx.stroke();
}
/* draw axes */
ctx.strokeStyle = '#8899aa';
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + plotH); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top + plotH); ctx.lineTo(PAD.left + plotW, PAD.top + plotH); ctx.stroke();
/* draw individual Gaussian components as dashed lines */
const colors = ['#e63946', '#457b9d'];
comps.forEach((c, idx) => {
const compPdf = ys.map(y => c.pi * gaussian(y, c.mu, c.sigma));
ctx.beginPath();
ctx.strokeStyle = colors[idx];
ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]);
compPdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
ctx.setLineDash([]);
});
/* draw the full mixture PDF */
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
pdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
/* fill area under the mixture curve */
ctx.beginPath();
pdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.lineTo(toCanvasX(1), toCanvasY(0));
ctx.lineTo(toCanvasX(0), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(29,53,87,0.08)';
ctx.fill();
/* x-axis tick labels */
ctx.fillStyle = '#8899aa';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i <= 5; i++) {
const v = i / 5;
ctx.fillText(v.toFixed(1), toCanvasX(v), PAD.top + plotH + 14);
}
ctx.textAlign = 'right';
ctx.fillText('p(y|x)', PAD.left - 3, PAD.top + 6);
/* y-axis label */
ctx.textAlign = 'center';
ctx.fillStyle = '#8899aa';
ctx.fillText('y', toCanvasX(1) + 8, PAD.top + plotH + 4);
/* update mode info text */
const infoEl = document.getElementById('modeinfo');
const [c1, c2] = comps;
infoEl.innerHTML = i18nFmt('Mode 1 (red): weight %w1%%, mean y = %mu1% | Mode 2 (blue): weight %w2%%, mean y = %mu2%', {
w1: (c1.pi * 100).toFixed(0),
mu1: c1.mu.toFixed(2),
w2: (c2.pi * 100).toFixed(0),
mu2: c2.mu.toFixed(2)
});
}
/* wire up slider and reset button */
const slider = document.getElementById('xslider');
const xvalEl = document.getElementById('xval');
function update() {
const x = parseFloat(slider.value);
xvalEl.textContent = x.toFixed(2);
draw(x);
}
slider.addEventListener('input', update);
document.getElementById('resetBtn').addEventListener('click', () => {
slider.value = '0.50';
update();
});
update();
<!-- MDN demo: one input x maps to a bimodal distribution over y -->
<p class="hint">Drag the slider to change the input value <b>x</b>. The <b>blue curve</b> is the full MDN mixture distribution p(y|x). The dashed curves are the two individual Gaussian components. Notice how the distribution splits into two peaks in the middle — a plain regressor would predict their average.</p>
<div class="controls">
<label for="xslider">Input x = <strong id="xval">0.50</strong></label>
<input id="xslider" type="range" min="0" max="1" step="0.01" value="0.50">
</div>
<canvas id="chart" width="480" height="220"></canvas>
<div class="mode-info" id="modeinfo"></div>
<div class="btns">
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
/* demo layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .7rem; margin-bottom: .5rem; flex-wrap: wrap; }
.controls label { font-size: .9rem; font-weight: 600; white-space: nowrap; }
#xslider { flex: 1; min-width: 140px; max-width: 320px; accent-color: #1d3557; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f8fafc; }
.mode-info { font-size: .85rem; color: #444; margin: .5rem 0; min-height: 2.4em; line-height: 1.5; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .3rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Mixture Density Network demo */
/* The network outputs parameters for K=2 Gaussian components: pi (weight), mu (mean), sigma (std dev) */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Gaussian probability density function */
function gaussian(y, mu, sigma) {
const s2 = sigma * sigma;
return Math.exp(-0.5 * (y - mu) * (y - mu) / s2) / Math.sqrt(2 * Math.PI * s2);
}
/* Simulate MDN output for a given input x in [0,1]. */
/* Component weights, means, and sigmas vary smoothly with x to create a bimodal effect. */
function mdnParams(x) {
/* mixing weights: low mode dominates at x=0, high mode at x=1, equal in the middle */
const wLow = 0.5 + 0.45 * Math.cos(Math.PI * x);
const wHigh = 1 - wLow;
/* means: low mode near 0.15, high mode near 0.75, both drift slightly with x */
const muLow = 0.15 + 0.10 * x;
const muHigh = 0.75 + 0.10 * x;
/* sigmas: widest in the ambiguous middle, narrower at extremes */
const sigLow = 0.06 + 0.04 * (1 - Math.abs(2 * x - 1));
const sigHigh = 0.06 + 0.04 * (1 - Math.abs(2 * x - 1));
return [
{ pi: wLow, mu: muLow, sigma: sigLow },
{ pi: wHigh, mu: muHigh, sigma: sigHigh }
];
}
/* Draw the mixture distribution on the canvas */
function draw(x) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = { top: 18, right: 16, bottom: 28, left: 36 };
const plotW = W - PAD.left - PAD.right;
const plotH = H - PAD.top - PAD.bottom;
ctx.clearRect(0, 0, W, H);
const comps = mdnParams(x);
const STEPS = 300;
const ys = Array.from({ length: STEPS }, (_, i) => i / (STEPS - 1));
/* evaluate full mixture PDF over y in [0,1] */
const pdf = ys.map(y => comps.reduce((s, c) => s + c.pi * gaussian(y, c.mu, c.sigma), 0));
const maxP = Math.max(...pdf);
const toCanvasX = y => PAD.left + y * plotW;
const toCanvasY = p => PAD.top + plotH - (p / (maxP * 1.1)) * plotH;
/* draw background grid */
ctx.strokeStyle = '#dde4ea';
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const cy = PAD.top + (i / 4) * plotH;
ctx.beginPath(); ctx.moveTo(PAD.left, cy); ctx.lineTo(PAD.left + plotW, cy); ctx.stroke();
}
for (let i = 0; i <= 5; i++) {
const cx = PAD.left + (i / 5) * plotW;
ctx.beginPath(); ctx.moveTo(cx, PAD.top); ctx.lineTo(cx, PAD.top + plotH); ctx.stroke();
}
/* draw axes */
ctx.strokeStyle = '#8899aa';
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top); ctx.lineTo(PAD.left, PAD.top + plotH); ctx.stroke();
ctx.beginPath(); ctx.moveTo(PAD.left, PAD.top + plotH); ctx.lineTo(PAD.left + plotW, PAD.top + plotH); ctx.stroke();
/* draw individual Gaussian components as dashed lines */
const colors = ['#e63946', '#457b9d'];
comps.forEach((c, idx) => {
const compPdf = ys.map(y => c.pi * gaussian(y, c.mu, c.sigma));
ctx.beginPath();
ctx.strokeStyle = colors[idx];
ctx.lineWidth = 1.2;
ctx.setLineDash([4, 3]);
compPdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
ctx.setLineDash([]);
});
/* draw the full mixture PDF */
ctx.beginPath();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2.2;
pdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.stroke();
/* fill area under the mixture curve */
ctx.beginPath();
pdf.forEach((p, i) => {
const cx = toCanvasX(ys[i]), cy = toCanvasY(p);
i === 0 ? ctx.moveTo(cx, cy) : ctx.lineTo(cx, cy);
});
ctx.lineTo(toCanvasX(1), toCanvasY(0));
ctx.lineTo(toCanvasX(0), toCanvasY(0));
ctx.closePath();
ctx.fillStyle = 'rgba(29,53,87,0.08)';
ctx.fill();
/* x-axis tick labels */
ctx.fillStyle = '#8899aa';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
for (let i = 0; i <= 5; i++) {
const v = i / 5;
ctx.fillText(v.toFixed(1), toCanvasX(v), PAD.top + plotH + 14);
}
ctx.textAlign = 'right';
ctx.fillText('p(y|x)', PAD.left - 3, PAD.top + 6);
/* y-axis label */
ctx.textAlign = 'center';
ctx.fillStyle = '#8899aa';
ctx.fillText('y', toCanvasX(1) + 8, PAD.top + plotH + 4);
/* update mode info text */
const infoEl = document.getElementById('modeinfo');
const [c1, c2] = comps;
infoEl.innerHTML = i18nFmt('Mode 1 (red): weight %w1%%, mean y = %mu1% | Mode 2 (blue): weight %w2%%, mean y = %mu2%', {
w1: (c1.pi * 100).toFixed(0),
mu1: c1.mu.toFixed(2),
w2: (c2.pi * 100).toFixed(0),
mu2: c2.mu.toFixed(2)
});
}
/* wire up slider and reset button */
const slider = document.getElementById('xslider');
const xvalEl = document.getElementById('xval');
function update() {
const x = parseFloat(slider.value);
xvalEl.textContent = x.toFixed(2);
draw(x);
}
slider.addEventListener('input', update);
document.getElementById('resetBtn').addEventListener('click', () => {
slider.value = '0.50';
update();
});
update();
Drag the slider and watch what happens. When x is far to either side the network is confident: one Gaussian dominates. In the middle the distribution splits into two peaks — the MDN refuses to collapse them into a meaningless average, choosing instead to keep both hypotheses alive with their own weights. A plain regression network would predict the midpoint of those two peaks, which is rarely the true answer.
Comments
Loading comments...