The demo below uses a small set of pre-computed word vectors (dimension 10, trained on a toy corpus). Each word lives at a point in high-dimensional space; the demo projects the two most informative directions onto your screen.
<!-- Word analogy explorer — three selects for A, B, C; canvas shows the projected space -->
<p class="hint">Analogy arithmetic in action: pick three words A, B, C and compute A − B + C. The nearest word in the pre-trained vector space is the answer. Classic example: king − man + woman ≈ queen.</p>
<div class="controls">
<div class="row">
<label>Compute: A − B + C =</label>
<div class="analogy-row">
<select id="sel-a">options for word A</select>
<span class="op">−</span>
<select id="sel-b">options for word B</select>
<span class="op">+</span>
<select id="sel-c">options for word C</select>
<span class="op">=</span>
<span id="result-word" class="result-word">?</span>
</div>
</div>
<div class="btns">
<button id="btn-run" type="button">Compute analogy</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="status" class="status"></div>
<canvas id="canvas" width="420" height="240"></canvas>
<p class="caption">2D projection of the 10-dimensional word vectors (first two principal components).</p>
/* Styles for the word embedding analogy explorer */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
.controls { margin-bottom: .5rem; }
.row { margin-bottom: .4rem; }
label { font-size: .82rem; font-weight: 600; color: #555; display: block; margin-bottom: .3rem; }
.analogy-row { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
select { font: 14px system-ui; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; background: #f9f9f9; cursor: pointer; }
.op { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
.result-word { font-weight: 700; font-size: 1.05rem; color: #0a7d33; min-width: 4rem; padding: .2rem .5rem; background: #e8f5ec; border-radius: 6px; text-align: center; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .3rem 0 .4rem; color: #555; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
canvas { border: 1px solid #dde3ea; border-radius: 8px; display: block; max-width: 100%; }
.caption { font-size: .78rem; color: #888; margin: .3rem 0 0; text-align: center; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pre-computed 10-dimensional word vectors (toy corpus, not real Word2Vec weights).
// Vectors are hand-tuned so that gender and royalty axes are clearly separated.
const VECS = {
king: [ 0.82, 0.61, 0.15,-0.40, 0.30, 0.55,-0.10, 0.20, 0.70,-0.25],
queen: [ 0.75, 0.70, 0.10,-0.35, 0.28,-0.48,-0.08, 0.18, 0.65,-0.20],
man: [ 0.60, 0.20,-0.15,-0.50, 0.40, 0.62,-0.05, 0.10, 0.25,-0.18],
woman: [ 0.55, 0.30,-0.12,-0.45, 0.38,-0.55,-0.03, 0.08, 0.22,-0.14],
prince: [ 0.80, 0.55, 0.18,-0.38, 0.32, 0.52,-0.12, 0.22, 0.68,-0.22],
princess:[ 0.72, 0.65, 0.12,-0.33, 0.30,-0.46,-0.09, 0.20, 0.62,-0.18],
paris: [ 0.10, 0.15, 0.90, 0.75,-0.20, 0.05, 0.60,-0.30,-0.10, 0.40],
france: [ 0.05, 0.10, 0.85, 0.80,-0.18, 0.08, 0.55,-0.28,-0.08, 0.38],
berlin: [ 0.12, 0.18, 0.88, 0.70,-0.22, 0.06, 0.58,-0.32,-0.12, 0.42],
germany: [ 0.08, 0.12, 0.83, 0.78,-0.20, 0.09, 0.53,-0.30,-0.09, 0.40],
dog: [-0.20, 0.55,-0.30, 0.30, 0.80,-0.10, 0.25, 0.60,-0.15, 0.20],
cat: [-0.18, 0.52,-0.28, 0.32, 0.78,-0.08, 0.23, 0.58,-0.13, 0.18],
puppy: [-0.22, 0.58,-0.32, 0.28, 0.82,-0.12, 0.27, 0.62,-0.17, 0.22],
kitten: [-0.19, 0.54,-0.29, 0.31, 0.79,-0.09, 0.24, 0.59,-0.14, 0.19],
};
const WORDS = Object.keys(VECS);
const DIM = VECS.king.length;
// Vector arithmetic and cosine similarity helpers.
function dot(a, b) { let s = 0; for (let i = 0; i < DIM; i++) s += a[i] * b[i]; return s; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function cosine(a, b) { const d = norm(a) * norm(b); return d < 1e-9 ? 0 : dot(a, b) / d; }
function add(a, b) { return a.map((x, i) => x + b[i]); }
function sub(a, b) { return a.map((x, i) => x - b[i]); }
// Find the nearest word to a query vector, excluding the three input words.
function nearest(vec, exclude) {
let best = null, bestSim = -2;
for (const w of WORDS) {
if (exclude.includes(w)) continue;
const s = cosine(vec, VECS[w]);
if (s > bestSim) { bestSim = s; best = w; }
}
return { word: best, sim: bestSim };
}
// Project 10-D vectors to 2D using the top two principal components (power iteration).
// Cached after the first call so re-draws are fast.
function project2D() {
const pts = WORDS.map(w => VECS[w]);
const mean = Array(DIM).fill(0);
pts.forEach(p => p.forEach((v, i) => { mean[i] += v / pts.length; }));
const centered = pts.map(p => p.map((v, i) => v - mean[i]));
// Two rounds of power iteration to find the first two principal directions.
let v1 = centered[0].slice();
for (let iter = 0; iter < 60; iter++) {
let proj = centered.map(p => dot(p, v1));
let nv = Array(DIM).fill(0);
proj.forEach((s, i) => centered[i].forEach((x, d) => { nv[d] += s * x; }));
const n = norm(nv); v1 = nv.map(x => x / n);
}
const scores1 = centered.map(p => dot(p, v1));
const residual = centered.map((p, i) => p.map((x, d) => x - scores1[i] * v1[d]));
let v2 = residual[1].slice();
for (let iter = 0; iter < 60; iter++) {
let proj = residual.map(p => dot(p, v2));
let nv = Array(DIM).fill(0);
proj.forEach((s, i) => residual[i].forEach((x, d) => { nv[d] += s * x; }));
const n = norm(nv); v2 = nv.map(x => x / n);
}
const scores2 = residual.map(p => dot(p, v2));
return { scores1, scores2, v1, v2, mean };
}
let projection = null;
function getProj() { if (!projection) projection = project2D(); return projection; }
function projectWord(vec) {
const { v1, v2, mean } = getProj();
const c = vec.map((x, i) => x - mean[i]);
return { x: dot(c, v1), y: dot(c, v2) };
}
// Draw all words on canvas; highlight analogy words and the result vector.
function draw(resultVec, resultWord, analogyWords) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const { scores1, scores2 } = getProj();
const allX = scores1, allY = scores2;
const pad = 40;
const minX = Math.min(...allX) - 0.1, maxX = Math.max(...allX) + 0.1;
const minY = Math.min(...allY) - 0.1, maxY = Math.max(...allY) + 0.1;
const toCanvasX = x => pad + (x - minX) / (maxX - minX) * (W - 2 * pad);
const toCanvasY = y => H - pad - (y - minY) / (maxY - minY) * (H - 2 * pad);
// Draw each word as a dot, bigger and darker for analogy words and the result.
WORDS.forEach((w, i) => {
const cx = toCanvasX(scores1[i]);
const cy = toCanvasY(scores2[i]);
const isAnalogy = analogyWords.includes(w);
const isResult = w === resultWord;
ctx.beginPath();
ctx.arc(cx, cy, isAnalogy || isResult ? 6 : 3.5, 0, 2 * Math.PI);
ctx.fillStyle = isResult ? '#0a7d33' : isAnalogy ? '#1d3557' : '#9fb2c8';
ctx.fill();
ctx.font = `${isAnalogy || isResult ? '700' : '400'} 11px system-ui`;
ctx.fillStyle = isResult ? '#0a7d33' : isAnalogy ? '#1d3557' : '#888';
ctx.fillText(w, cx + 8, cy + 4);
});
// Draw the raw result vector (before nearest-word lookup) as a red marker.
if (resultVec) {
const rp = projectWord(resultVec);
const rx = toCanvasX(rp.x), ry = toCanvasY(rp.y);
ctx.beginPath(); ctx.arc(rx, ry, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946'; ctx.fill();
ctx.font = '700 11px system-ui'; ctx.fillStyle = '#e63946';
ctx.fillText('✦ ' + 'result vector', rx + 8, ry + 4);
// Dashed line from word A to the result vector.
if (analogyWords.length > 0) {
const ap = projectWord(VECS[analogyWords[0]]);
const ax = toCanvasX(ap.x), ay = toCanvasY(ap.y);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(rx, ry);
ctx.strokeStyle = '#e6394680'; ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]); ctx.stroke(); ctx.setLineDash([]);
}
}
}
// Populate each select element with all vocabulary words.
function populate(selId, defaultWord) {
const sel = document.getElementById(selId);
sel.innerHTML = '';
WORDS.forEach(w => {
const o = document.createElement('option');
o.value = w; o.textContent = w;
if (w === defaultWord) o.selected = true;
sel.appendChild(o);
});
}
populate('sel-a', 'king');
populate('sel-b', 'man');
populate('sel-c', 'woman');
draw(null, null, []);
document.getElementById('btn-run').addEventListener('click', () => {
const a = document.getElementById('sel-a').value;
const b = document.getElementById('sel-b').value;
const c = document.getElementById('sel-c').value;
// Analogy arithmetic: result = vec(A) - vec(B) + vec(C).
const result = add(sub(VECS[a], VECS[b]), VECS[c]);
const { word: ans, sim } = nearest(result, [a, b, c]);
document.getElementById('result-word').textContent = ans;
const pct = Math.round(sim * 100);
document.getElementById('status').textContent = i18nFmt('%a% − %b% + %c% ≈ %ans% (cosine similarity %pct%%)', { a, b, c, ans, pct });
document.getElementById('status').className = 'status ok';
draw(result, ans, [a, b, c]);
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('sel-a').value = 'king';
document.getElementById('sel-b').value = 'man';
document.getElementById('sel-c').value = 'woman';
document.getElementById('result-word').textContent = '?';
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
draw(null, null, []);
});
<!-- Word analogy explorer — three selects for A, B, C; canvas shows the projected space -->
<p class="hint">Analogy arithmetic in action: pick three words A, B, C and compute A − B + C. The nearest word in the pre-trained vector space is the answer. Classic example: king − man + woman ≈ queen.</p>
<div class="controls">
<div class="row">
<label>Compute: A − B + C =</label>
<div class="analogy-row">
<select id="sel-a">options for word A</select>
<span class="op">−</span>
<select id="sel-b">options for word B</select>
<span class="op">+</span>
<select id="sel-c">options for word C</select>
<span class="op">=</span>
<span id="result-word" class="result-word">?</span>
</div>
</div>
<div class="btns">
<button id="btn-run" type="button">Compute analogy</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<div id="status" class="status"></div>
<canvas id="canvas" width="420" height="240"></canvas>
<p class="caption">2D projection of the 10-dimensional word vectors (first two principal components).</p>
/* Styles for the word embedding analogy explorer */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; background: #fff; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
.controls { margin-bottom: .5rem; }
.row { margin-bottom: .4rem; }
label { font-size: .82rem; font-weight: 600; color: #555; display: block; margin-bottom: .3rem; }
.analogy-row { display: flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
select { font: 14px system-ui; padding: .3rem .5rem; border: 1px solid #ccc; border-radius: 6px; background: #f9f9f9; cursor: pointer; }
.op { font-weight: 700; font-size: 1.1rem; color: #1d3557; }
.result-word { font-weight: 700; font-size: 1.05rem; color: #0a7d33; min-width: 4rem; padding: .2rem .5rem; background: #e8f5ec; border-radius: 6px; text-align: center; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .3rem 0 .4rem; color: #555; }
.status.ok { color: #0a7d33; }
.status.err { color: #c92f3c; }
canvas { border: 1px solid #dde3ea; border-radius: 8px; display: block; max-width: 100%; }
.caption { font-size: .78rem; color: #888; margin: .3rem 0 0; text-align: center; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pre-computed 10-dimensional word vectors (toy corpus, not real Word2Vec weights).
// Vectors are hand-tuned so that gender and royalty axes are clearly separated.
const VECS = {
king: [ 0.82, 0.61, 0.15,-0.40, 0.30, 0.55,-0.10, 0.20, 0.70,-0.25],
queen: [ 0.75, 0.70, 0.10,-0.35, 0.28,-0.48,-0.08, 0.18, 0.65,-0.20],
man: [ 0.60, 0.20,-0.15,-0.50, 0.40, 0.62,-0.05, 0.10, 0.25,-0.18],
woman: [ 0.55, 0.30,-0.12,-0.45, 0.38,-0.55,-0.03, 0.08, 0.22,-0.14],
prince: [ 0.80, 0.55, 0.18,-0.38, 0.32, 0.52,-0.12, 0.22, 0.68,-0.22],
princess:[ 0.72, 0.65, 0.12,-0.33, 0.30,-0.46,-0.09, 0.20, 0.62,-0.18],
paris: [ 0.10, 0.15, 0.90, 0.75,-0.20, 0.05, 0.60,-0.30,-0.10, 0.40],
france: [ 0.05, 0.10, 0.85, 0.80,-0.18, 0.08, 0.55,-0.28,-0.08, 0.38],
berlin: [ 0.12, 0.18, 0.88, 0.70,-0.22, 0.06, 0.58,-0.32,-0.12, 0.42],
germany: [ 0.08, 0.12, 0.83, 0.78,-0.20, 0.09, 0.53,-0.30,-0.09, 0.40],
dog: [-0.20, 0.55,-0.30, 0.30, 0.80,-0.10, 0.25, 0.60,-0.15, 0.20],
cat: [-0.18, 0.52,-0.28, 0.32, 0.78,-0.08, 0.23, 0.58,-0.13, 0.18],
puppy: [-0.22, 0.58,-0.32, 0.28, 0.82,-0.12, 0.27, 0.62,-0.17, 0.22],
kitten: [-0.19, 0.54,-0.29, 0.31, 0.79,-0.09, 0.24, 0.59,-0.14, 0.19],
};
const WORDS = Object.keys(VECS);
const DIM = VECS.king.length;
// Vector arithmetic and cosine similarity helpers.
function dot(a, b) { let s = 0; for (let i = 0; i < DIM; i++) s += a[i] * b[i]; return s; }
function norm(a) { return Math.sqrt(dot(a, a)); }
function cosine(a, b) { const d = norm(a) * norm(b); return d < 1e-9 ? 0 : dot(a, b) / d; }
function add(a, b) { return a.map((x, i) => x + b[i]); }
function sub(a, b) { return a.map((x, i) => x - b[i]); }
// Find the nearest word to a query vector, excluding the three input words.
function nearest(vec, exclude) {
let best = null, bestSim = -2;
for (const w of WORDS) {
if (exclude.includes(w)) continue;
const s = cosine(vec, VECS[w]);
if (s > bestSim) { bestSim = s; best = w; }
}
return { word: best, sim: bestSim };
}
// Project 10-D vectors to 2D using the top two principal components (power iteration).
// Cached after the first call so re-draws are fast.
function project2D() {
const pts = WORDS.map(w => VECS[w]);
const mean = Array(DIM).fill(0);
pts.forEach(p => p.forEach((v, i) => { mean[i] += v / pts.length; }));
const centered = pts.map(p => p.map((v, i) => v - mean[i]));
// Two rounds of power iteration to find the first two principal directions.
let v1 = centered[0].slice();
for (let iter = 0; iter < 60; iter++) {
let proj = centered.map(p => dot(p, v1));
let nv = Array(DIM).fill(0);
proj.forEach((s, i) => centered[i].forEach((x, d) => { nv[d] += s * x; }));
const n = norm(nv); v1 = nv.map(x => x / n);
}
const scores1 = centered.map(p => dot(p, v1));
const residual = centered.map((p, i) => p.map((x, d) => x - scores1[i] * v1[d]));
let v2 = residual[1].slice();
for (let iter = 0; iter < 60; iter++) {
let proj = residual.map(p => dot(p, v2));
let nv = Array(DIM).fill(0);
proj.forEach((s, i) => residual[i].forEach((x, d) => { nv[d] += s * x; }));
const n = norm(nv); v2 = nv.map(x => x / n);
}
const scores2 = residual.map(p => dot(p, v2));
return { scores1, scores2, v1, v2, mean };
}
let projection = null;
function getProj() { if (!projection) projection = project2D(); return projection; }
function projectWord(vec) {
const { v1, v2, mean } = getProj();
const c = vec.map((x, i) => x - mean[i]);
return { x: dot(c, v1), y: dot(c, v2) };
}
// Draw all words on canvas; highlight analogy words and the result vector.
function draw(resultVec, resultWord, analogyWords) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const { scores1, scores2 } = getProj();
const allX = scores1, allY = scores2;
const pad = 40;
const minX = Math.min(...allX) - 0.1, maxX = Math.max(...allX) + 0.1;
const minY = Math.min(...allY) - 0.1, maxY = Math.max(...allY) + 0.1;
const toCanvasX = x => pad + (x - minX) / (maxX - minX) * (W - 2 * pad);
const toCanvasY = y => H - pad - (y - minY) / (maxY - minY) * (H - 2 * pad);
// Draw each word as a dot, bigger and darker for analogy words and the result.
WORDS.forEach((w, i) => {
const cx = toCanvasX(scores1[i]);
const cy = toCanvasY(scores2[i]);
const isAnalogy = analogyWords.includes(w);
const isResult = w === resultWord;
ctx.beginPath();
ctx.arc(cx, cy, isAnalogy || isResult ? 6 : 3.5, 0, 2 * Math.PI);
ctx.fillStyle = isResult ? '#0a7d33' : isAnalogy ? '#1d3557' : '#9fb2c8';
ctx.fill();
ctx.font = `${isAnalogy || isResult ? '700' : '400'} 11px system-ui`;
ctx.fillStyle = isResult ? '#0a7d33' : isAnalogy ? '#1d3557' : '#888';
ctx.fillText(w, cx + 8, cy + 4);
});
// Draw the raw result vector (before nearest-word lookup) as a red marker.
if (resultVec) {
const rp = projectWord(resultVec);
const rx = toCanvasX(rp.x), ry = toCanvasY(rp.y);
ctx.beginPath(); ctx.arc(rx, ry, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e63946'; ctx.fill();
ctx.font = '700 11px system-ui'; ctx.fillStyle = '#e63946';
ctx.fillText('✦ ' + 'result vector', rx + 8, ry + 4);
// Dashed line from word A to the result vector.
if (analogyWords.length > 0) {
const ap = projectWord(VECS[analogyWords[0]]);
const ax = toCanvasX(ap.x), ay = toCanvasY(ap.y);
ctx.beginPath(); ctx.moveTo(ax, ay); ctx.lineTo(rx, ry);
ctx.strokeStyle = '#e6394680'; ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]); ctx.stroke(); ctx.setLineDash([]);
}
}
}
// Populate each select element with all vocabulary words.
function populate(selId, defaultWord) {
const sel = document.getElementById(selId);
sel.innerHTML = '';
WORDS.forEach(w => {
const o = document.createElement('option');
o.value = w; o.textContent = w;
if (w === defaultWord) o.selected = true;
sel.appendChild(o);
});
}
populate('sel-a', 'king');
populate('sel-b', 'man');
populate('sel-c', 'woman');
draw(null, null, []);
document.getElementById('btn-run').addEventListener('click', () => {
const a = document.getElementById('sel-a').value;
const b = document.getElementById('sel-b').value;
const c = document.getElementById('sel-c').value;
// Analogy arithmetic: result = vec(A) - vec(B) + vec(C).
const result = add(sub(VECS[a], VECS[b]), VECS[c]);
const { word: ans, sim } = nearest(result, [a, b, c]);
document.getElementById('result-word').textContent = ans;
const pct = Math.round(sim * 100);
document.getElementById('status').textContent = i18nFmt('%a% − %b% + %c% ≈ %ans% (cosine similarity %pct%%)', { a, b, c, ans, pct });
document.getElementById('status').className = 'status ok';
draw(result, ans, [a, b, c]);
});
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('sel-a').value = 'king';
document.getElementById('sel-b').value = 'man';
document.getElementById('sel-c').value = 'woman';
document.getElementById('result-word').textContent = '?';
document.getElementById('status').textContent = '';
document.getElementById('status').className = 'status';
draw(null, null, []);
});
Select a starting word, an analogy pair, and watch the arithmetic play out in the projected plane. The answer word is whichever word in the vocabulary sits closest (by cosine similarity) to the result vector — usually the one you'd expect.
Comments
Loading comments...