<div id="app">
<div id="devices-row">
<div class="device" id="dev0">
<div class="dev-label">Device A</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info0">โ</div>
</div>
<div class="device" id="dev1">
<div class="dev-label">Device B</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info1">โ</div>
</div>
<div class="device" id="dev2">
<div class="dev-label">Device C</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info2">โ</div>
</div>
</div>
<div id="global-box">
<div class="dev-label">Global Model (FedAvg)</div>
<canvas id="global-canvas" width="320" height="200"></canvas>
<div id="global-info">Round 0 โ click Train one round to start.</div>
</div>
<div id="controls">
<button id="btn-round">Train one round</button>
<button id="btn-reset" class="ghost">Reset</button>
<span id="round-counter">Round 0</span>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #f8f9fa; padding: 12px; }
#app { display: flex; flex-direction: column; gap: 12px; align-items: center; }
#devices-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.device { background: #fff; border: 1.5px solid #dde3ea; border-radius: 10px;
padding: 8px; display: flex; flex-direction: column; align-items: center; gap: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,.07); }
.dev-label { font-size: .75rem; font-weight: 700; color: #5a7088; text-transform: uppercase; letter-spacing: .05em; }
.dev-canvas { border-radius: 6px; border: 1px solid #eee; display: block; }
.dev-info { font-size: .72rem; color: #555; min-height: 1.1em; }
#global-box { background: #fff; border: 2px solid #1d3557; border-radius: 10px;
padding: 10px; display: flex; flex-direction: column; align-items: center; gap: 6px;
box-shadow: 0 2px 8px rgba(29,53,87,.12); }
#global-canvas { border-radius: 6px; border: 1px solid #cdd9e3; display: block; }
#global-info { font-size: .8rem; color: #333; font-weight: 600; min-height: 1.2em; }
#controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#round-counter { font-size: .8rem; color: #5a7088; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Federated Averaging (FedAvg) simulation.
// Model: logistic regression y = sigmoid(w0*x + w1*y + b).
// 3 devices each hold a private cluster of labeled 2-D points.
// Each round: local gradient descent steps โ average weights โ update global model.
const sigmoid = t => 1 / (1 + Math.exp(-t));
// โโ Dataset generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function makeCluster(cx, cy, label, n, seed) {
const pts = [];
let s = seed;
const rng = () => { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 0xffffffff; };
for (let i = 0; i < n; i++) {
// Box-Muller
const u = rng() || 1e-9, v = rng();
const z0 = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
const z1 = Math.sqrt(-2 * Math.log(u)) * Math.sin(2 * Math.PI * v);
pts.push({ x: cx + z0 * 0.18, y: cy + z1 * 0.18, label });
}
return pts;
}
// Three devices, each with two small clusters (one per class)
const DEVICES = [
// Device A: top-left region
[...makeCluster(0.22, 0.28, 1, 6, 7), ...makeCluster(0.38, 0.55, 0, 6, 13)],
// Device B: centre
[...makeCluster(0.50, 0.30, 1, 6, 31), ...makeCluster(0.62, 0.65, 0, 6, 53)],
// Device C: bottom-right region
[...makeCluster(0.65, 0.25, 1, 6, 71), ...makeCluster(0.78, 0.60, 0, 6, 97)],
];
// โโ Model โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const LR = 0.8; // learning rate
const LOCAL_STEPS = 8;
function predict(w, pt) { return sigmoid(w[0] * pt.x + w[1] * pt.y + w[2]); }
function localTrain(w, data, steps) {
const wc = [...w];
for (let s = 0; s < steps; s++) {
let g0 = 0, g1 = 0, g2 = 0;
for (const pt of data) {
const err = predict(wc, pt) - pt.label;
g0 += err * pt.x; g1 += err * pt.y; g2 += err;
}
const n = data.length;
wc[0] -= LR * g0 / n;
wc[1] -= LR * g1 / n;
wc[2] -= LR * g2 / n;
}
return wc;
}
function accuracy(w, data) {
let ok = 0;
for (const pt of data) { if ((predict(w, pt) >= 0.5 ? 1 : 0) === pt.label) ok++; }
return ok / data.length;
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let globalW = [0, 0, 0];
let round = 0;
function fedRound() {
// Each device does local training from current globalW
const localWs = DEVICES.map(data => localTrain([...globalW], data, LOCAL_STEPS));
// FedAvg: simple mean of weights
const n = localWs.length;
globalW = [0, 0, 0];
for (const w of localWs) { globalW[0] += w[0] / n; globalW[1] += w[1] / n; globalW[2] += w[2] / n; }
round++;
return localWs;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const RED = '#e63946', BLUE = '#457b9d', LIGHT_RED = '#ffb3b8', LIGHT_BLUE = '#a8d5ea';
function drawCanvas(canvas, w, data, small) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Background: colour the half-planes using the current model
const id = ctx.createImageData(W, H);
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const fx = px / W, fy = py / H;
const p = predict(w, { x: fx, y: fy });
const idx = (py * W + px) * 4;
if (p >= 0.5) { id.data[idx]=230; id.data[idx+1]=57; id.data[idx+2]=70; id.data[idx+3]=35; }
else { id.data[idx]=69; id.data[idx+1]=123; id.data[idx+2]=157; id.data[idx+3]=35; }
}
}
ctx.putImageData(id, 0, 0);
// Decision boundary: find x at the middle row
// w0*px + w1*py + w2 = 0 => draw the line
ctx.beginPath(); ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2;
for (let px = 0; px <= W; px++) {
const fx = px / W;
if (Math.abs(w[1]) > 1e-6) {
const fy = -(w[0] * fx + w[2]) / w[1];
const py = fy * H;
if (px === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
}
ctx.stroke();
// Points
const r = small ? 4 : 6;
for (const pt of data) {
const px = pt.x * W, py = pt.y * H;
ctx.beginPath(); ctx.arc(px, py, r, 0, 2 * Math.PI);
ctx.fillStyle = pt.label === 1 ? RED : BLUE;
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.fill(); ctx.stroke();
}
}
// โโ Render all โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function render(localWs) {
const devCanvases = document.querySelectorAll('.dev-canvas');
DEVICES.forEach((data, i) => {
const w = localWs ? localWs[i] : globalW;
drawCanvas(devCanvases[i], w, data, true);
const acc = accuracy(w, data);
document.getElementById('info' + i).textContent =
fmt('Local acc: %v%%', { v: (acc * 100).toFixed(0) });
});
// Global: all data
const allData = DEVICES.flat();
drawCanvas(document.getElementById('global-canvas'), globalW, allData, false);
const gacc = accuracy(globalW, allData);
document.getElementById('global-info').textContent =
fmt('Round %round% โ Global accuracy: %v%% (%length% points across all devices)', { round: round, v: (gacc * 100).toFixed(0), length: allData.length });
document.getElementById('round-counter').textContent = fmt('Round: %round%', { round: round });
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('btn-round').addEventListener('click', () => {
const localWs = fedRound();
render(localWs);
});
document.getElementById('btn-reset').addEventListener('click', () => {
globalW = [0, 0, 0]; round = 0;
render(null);
});
// Initial draw
render(null);
// Verification (node-compatible summary for test)
if (typeof module !== 'undefined') {
module.exports = { localTrain, accuracy, fedRound: () => {
const localWs = DEVICES.map(data => localTrain([...globalW], data, LOCAL_STEPS));
const n = localWs.length;
globalW = [0,0,0];
for (const w of localWs) { globalW[0]+=w[0]/n; globalW[1]+=w[1]/n; globalW[2]+=w[2]/n; }
round++;
return localWs;
}, globalW: () => globalW, round: () => round, DEVICES, sigmoid };
}
<div id="app">
<div id="devices-row">
<div class="device" id="dev0">
<div class="dev-label">Device A</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info0">โ</div>
</div>
<div class="device" id="dev1">
<div class="dev-label">Device B</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info1">โ</div>
</div>
<div class="device" id="dev2">
<div class="dev-label">Device C</div>
<canvas class="dev-canvas" width="140" height="120"></canvas>
<div class="dev-info" id="info2">โ</div>
</div>
</div>
<div id="global-box">
<div class="dev-label">Global Model (FedAvg)</div>
<canvas id="global-canvas" width="320" height="200"></canvas>
<div id="global-info">Round 0 โ click Train one round to start.</div>
</div>
<div id="controls">
<button id="btn-round">Train one round</button>
<button id="btn-reset" class="ghost">Reset</button>
<span id="round-counter">Round 0</span>
</div>
</div>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #f8f9fa; padding: 12px; }
#app { display: flex; flex-direction: column; gap: 12px; align-items: center; }
#devices-row { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
.device { background: #fff; border: 1.5px solid #dde3ea; border-radius: 10px;
padding: 8px; display: flex; flex-direction: column; align-items: center; gap: 4px;
box-shadow: 0 1px 4px rgba(0,0,0,.07); }
.dev-label { font-size: .75rem; font-weight: 700; color: #5a7088; text-transform: uppercase; letter-spacing: .05em; }
.dev-canvas { border-radius: 6px; border: 1px solid #eee; display: block; }
.dev-info { font-size: .72rem; color: #555; min-height: 1.1em; }
#global-box { background: #fff; border: 2px solid #1d3557; border-radius: 10px;
padding: 10px; display: flex; flex-direction: column; align-items: center; gap: 6px;
box-shadow: 0 2px 8px rgba(29,53,87,.12); }
#global-canvas { border-radius: 6px; border: 1px solid #cdd9e3; display: block; }
#global-info { font-size: .8rem; color: #333; font-weight: 600; min-height: 1.2em; }
#controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1.5px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
#round-counter { font-size: .8rem; color: #5a7088; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Federated Averaging (FedAvg) simulation.
// Model: logistic regression y = sigmoid(w0*x + w1*y + b).
// 3 devices each hold a private cluster of labeled 2-D points.
// Each round: local gradient descent steps โ average weights โ update global model.
const sigmoid = t => 1 / (1 + Math.exp(-t));
// โโ Dataset generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function makeCluster(cx, cy, label, n, seed) {
const pts = [];
let s = seed;
const rng = () => { s = (s * 1664525 + 1013904223) & 0xffffffff; return (s >>> 0) / 0xffffffff; };
for (let i = 0; i < n; i++) {
// Box-Muller
const u = rng() || 1e-9, v = rng();
const z0 = Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
const z1 = Math.sqrt(-2 * Math.log(u)) * Math.sin(2 * Math.PI * v);
pts.push({ x: cx + z0 * 0.18, y: cy + z1 * 0.18, label });
}
return pts;
}
// Three devices, each with two small clusters (one per class)
const DEVICES = [
// Device A: top-left region
[...makeCluster(0.22, 0.28, 1, 6, 7), ...makeCluster(0.38, 0.55, 0, 6, 13)],
// Device B: centre
[...makeCluster(0.50, 0.30, 1, 6, 31), ...makeCluster(0.62, 0.65, 0, 6, 53)],
// Device C: bottom-right region
[...makeCluster(0.65, 0.25, 1, 6, 71), ...makeCluster(0.78, 0.60, 0, 6, 97)],
];
// โโ Model โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const LR = 0.8; // learning rate
const LOCAL_STEPS = 8;
function predict(w, pt) { return sigmoid(w[0] * pt.x + w[1] * pt.y + w[2]); }
function localTrain(w, data, steps) {
const wc = [...w];
for (let s = 0; s < steps; s++) {
let g0 = 0, g1 = 0, g2 = 0;
for (const pt of data) {
const err = predict(wc, pt) - pt.label;
g0 += err * pt.x; g1 += err * pt.y; g2 += err;
}
const n = data.length;
wc[0] -= LR * g0 / n;
wc[1] -= LR * g1 / n;
wc[2] -= LR * g2 / n;
}
return wc;
}
function accuracy(w, data) {
let ok = 0;
for (const pt of data) { if ((predict(w, pt) >= 0.5 ? 1 : 0) === pt.label) ok++; }
return ok / data.length;
}
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let globalW = [0, 0, 0];
let round = 0;
function fedRound() {
// Each device does local training from current globalW
const localWs = DEVICES.map(data => localTrain([...globalW], data, LOCAL_STEPS));
// FedAvg: simple mean of weights
const n = localWs.length;
globalW = [0, 0, 0];
for (const w of localWs) { globalW[0] += w[0] / n; globalW[1] += w[1] / n; globalW[2] += w[2] / n; }
round++;
return localWs;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const RED = '#e63946', BLUE = '#457b9d', LIGHT_RED = '#ffb3b8', LIGHT_BLUE = '#a8d5ea';
function drawCanvas(canvas, w, data, small) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Background: colour the half-planes using the current model
const id = ctx.createImageData(W, H);
for (let py = 0; py < H; py++) {
for (let px = 0; px < W; px++) {
const fx = px / W, fy = py / H;
const p = predict(w, { x: fx, y: fy });
const idx = (py * W + px) * 4;
if (p >= 0.5) { id.data[idx]=230; id.data[idx+1]=57; id.data[idx+2]=70; id.data[idx+3]=35; }
else { id.data[idx]=69; id.data[idx+1]=123; id.data[idx+2]=157; id.data[idx+3]=35; }
}
}
ctx.putImageData(id, 0, 0);
// Decision boundary: find x at the middle row
// w0*px + w1*py + w2 = 0 => draw the line
ctx.beginPath(); ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2;
for (let px = 0; px <= W; px++) {
const fx = px / W;
if (Math.abs(w[1]) > 1e-6) {
const fy = -(w[0] * fx + w[2]) / w[1];
const py = fy * H;
if (px === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
}
ctx.stroke();
// Points
const r = small ? 4 : 6;
for (const pt of data) {
const px = pt.x * W, py = pt.y * H;
ctx.beginPath(); ctx.arc(px, py, r, 0, 2 * Math.PI);
ctx.fillStyle = pt.label === 1 ? RED : BLUE;
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.5;
ctx.fill(); ctx.stroke();
}
}
// โโ Render all โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function render(localWs) {
const devCanvases = document.querySelectorAll('.dev-canvas');
DEVICES.forEach((data, i) => {
const w = localWs ? localWs[i] : globalW;
drawCanvas(devCanvases[i], w, data, true);
const acc = accuracy(w, data);
document.getElementById('info' + i).textContent =
fmt('Local acc: %v%%', { v: (acc * 100).toFixed(0) });
});
// Global: all data
const allData = DEVICES.flat();
drawCanvas(document.getElementById('global-canvas'), globalW, allData, false);
const gacc = accuracy(globalW, allData);
document.getElementById('global-info').textContent =
fmt('Round %round% โ Global accuracy: %v%% (%length% points across all devices)', { round: round, v: (gacc * 100).toFixed(0), length: allData.length });
document.getElementById('round-counter').textContent = fmt('Round: %round%', { round: round });
}
// โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.getElementById('btn-round').addEventListener('click', () => {
const localWs = fedRound();
render(localWs);
});
document.getElementById('btn-reset').addEventListener('click', () => {
globalW = [0, 0, 0]; round = 0;
render(null);
});
// Initial draw
render(null);
// Verification (node-compatible summary for test)
if (typeof module !== 'undefined') {
module.exports = { localTrain, accuracy, fedRound: () => {
const localWs = DEVICES.map(data => localTrain([...globalW], data, LOCAL_STEPS));
const n = localWs.length;
globalW = [0,0,0];
for (const w of localWs) { globalW[0]+=w[0]/n; globalW[1]+=w[1]/n; globalW[2]+=w[2]/n; }
round++;
return localWs;
}, globalW: () => globalW, round: () => round, DEVICES, sigmoid };
}
Notice the key asymmetry: each device sees only its own cluster of points, yet after a few rounds the global boundary generalises across all three datasets. The averaging step โ called FedAvg โ is what makes this possible. More rounds bring the boundary closer to what you would get from pooling all data centrally, but communication cost grows with each round: that tradeoff is the heart of federated learning research.
Comments
Loading comments...