Below is a simplified shadow-model attack. We train a toy classifier on a small dataset of points, then train a shadow model on similar data to learn what "member confidence" looks like versus "non-member confidence." Finally an attack model decides — for each query — whether the output signals membership.
<!-- Membership inference demo: confidence-score histogram for members vs non-members -->
<p class="hint">Adjust the sample count and overfitting level, then click <b>Run Attack</b>. The attack trains a shadow model, picks the best confidence threshold, and classifies each sample as member or non-member. Green bars = correct; red bars = wrong.</p>
<div class="controls">
<label>Samples: <input id="nSamples" type="range" min="20" max="80" value="40" step="10">
<span id="nSamplesVal">40</span></label>
<label>Overfit level: <input id="overfit" type="range" min="1" max="5" value="3" step="1">
<span id="overfitVal">3</span></label>
<button id="runBtn" type="button">Run Attack</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="statusBox" class="status-box"></div>
<div class="chart-area">
<div class="chart-label">Members (training data)</div>
<canvas id="memberCanvas" width="420" height="100"></canvas>
<div class="chart-label">Non-members (unseen data)</div>
<canvas id="nonMemberCanvas" width="420" height="100"></canvas>
</div>
<div id="result" class="result"></div>
/* Styles for the membership inference attack demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .7rem; }
.controls label { font-size: .85rem; display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 90px; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status-box { font-size: .85rem; color: #555; min-height: 1.3em; margin-bottom: .4rem; }
.chart-area { margin: .3rem 0; }
.chart-label { font-size: .78rem; font-weight: 600; color: #888; margin: .3rem 0 .15rem; text-transform: uppercase; letter-spacing: .04em; }
canvas { display: block; width: 100%; max-width: 420px; height: 100px; border-radius: 6px; background: #f5f7f9; border: 1px solid #dde2e8; }
.result { font-size: 1rem; font-weight: 700; margin-top: .7rem; min-height: 1.4em; }
.result.good { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.neutral { color: #555; }
/* Shadow-model membership inference attack simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const nSamplesEl = document.getElementById('nSamples');
const nSamplesVal = document.getElementById('nSamplesVal');
const overfitEl = document.getElementById('overfit');
const overfitVal = document.getElementById('overfitVal');
const runBtn = document.getElementById('runBtn');
const resetBtn = document.getElementById('resetBtn');
const statusBox = document.getElementById('statusBox');
const resultEl = document.getElementById('result');
const mCtx = document.getElementById('memberCanvas').getContext('2d');
const nmCtx = document.getElementById('nonMemberCanvas').getContext('2d');
nSamplesEl.addEventListener('input', () => { nSamplesVal.textContent = nSamplesEl.value; });
overfitEl.addEventListener('input', () => { overfitVal.textContent = overfitEl.value; });
/* Seeded pseudo-random number generator (xorshift32) */
function rng(seed) {
let s = seed >>> 0;
return () => { s ^= s << 13; s ^= s >> 17; s ^= s << 5; return (s >>> 0) / 4294967296; };
}
/* Simulate target-model confidence for a training member */
function modelConfidence(x, overfit) {
/* Higher overfit level → higher average member confidence */
const base = 0.55 + 0.07 * overfit;
const noise = (Math.random() - 0.5) * 0.18;
return Math.min(0.99, Math.max(0.01, base + noise));
}
function nonMemberConfidence(overfit) {
/* Higher overfit level → lower average non-member confidence */
const base = 0.5 - 0.04 * overfit;
const noise = (Math.random() - 0.5) * 0.22;
return Math.min(0.99, Math.max(0.01, base + noise));
}
/* Attack model: find the threshold that maximises attack accuracy */
function attackThreshold(memberScores, nonMemberScores) {
/* Sweep all candidate thresholds; pick the one that gives best (TP+TN)/N */
const allScores = [...memberScores, ...nonMemberScores].sort((a, b) => a - b);
let bestAcc = 0, bestTau = 0.5;
for (const tau of allScores) {
const tp = memberScores.filter(s => s >= tau).length;
const tn = nonMemberScores.filter(s => s < tau).length;
const acc = (tp + tn) / (memberScores.length + nonMemberScores.length);
if (acc > bestAcc) { bestAcc = acc; bestTau = tau; }
}
return { tau: bestTau, acc: bestAcc };
}
/* Draw confidence bars on a canvas; dashed line = chosen threshold */
function drawBars(ctx, scores, threshold, isMembers) {
const W = ctx.canvas.width, H = ctx.canvas.height;
ctx.clearRect(0, 0, W, H);
const n = scores.length;
const barW = Math.max(3, Math.floor((W - 8) / n) - 2);
scores.forEach((s, i) => {
const x = 4 + i * ((W - 8) / n);
const h = Math.round(s * (H - 6)) + 1;
const predicted = s >= threshold;
/* Green if prediction matches ground truth, red otherwise */
const correct = predicted === isMembers;
ctx.fillStyle = correct ? '#22a64a' : '#e63946';
ctx.beginPath();
ctx.roundRect(x, H - h - 3, barW, h, 2);
ctx.fill();
});
/* Draw the chosen threshold as a dashed horizontal line */
const ty = H - Math.round(threshold * (H - 6)) - 3;
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(0, ty);
ctx.lineTo(W, ty);
ctx.stroke();
ctx.setLineDash([]);
}
function runAttack() {
const n = parseInt(nSamplesEl.value);
const overfit = parseInt(overfitEl.value);
statusBox.textContent = 'Running shadow-model attack…';
resultEl.className = 'result neutral';
resultEl.textContent = '';
/* Generate simulated confidence scores for members and non-members */
const memberScores = Array.from({ length: n }, () => modelConfidence(null, overfit));
const nonMemberScores = Array.from({ length: n }, () => nonMemberConfidence(overfit));
const { tau, acc } = attackThreshold(memberScores, nonMemberScores);
drawBars(mCtx, memberScores, tau, true);
drawBars(nmCtx, nonMemberScores, tau, false);
const pct = Math.round(acc * 100);
statusBox.textContent = i18nFmt('Attack complete. Threshold τ = %tau%.', { tau: tau.toFixed(2) });
if (pct >= 80) {
resultEl.className = 'result bad';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — strong membership signal! High overfitting leaks training data.', { pct });
} else if (pct >= 65) {
resultEl.className = 'result neutral';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — moderate signal. Some membership information leaks.', { pct });
} else {
resultEl.className = 'result good';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — weak signal. Low overfitting limits the leak.', { pct });
}
}
function resetDemo() {
mCtx.clearRect(0, 0, mCtx.canvas.width, mCtx.canvas.height);
nmCtx.clearRect(0, 0, nmCtx.canvas.width, nmCtx.canvas.height);
statusBox.textContent = '';
resultEl.textContent = '';
resultEl.className = 'result neutral';
}
runBtn.addEventListener('click', runAttack);
resetBtn.addEventListener('click', resetDemo);
/* Run the attack once on page load so the demo is not empty */
runAttack();
<!-- Membership inference demo: confidence-score histogram for members vs non-members -->
<p class="hint">Adjust the sample count and overfitting level, then click <b>Run Attack</b>. The attack trains a shadow model, picks the best confidence threshold, and classifies each sample as member or non-member. Green bars = correct; red bars = wrong.</p>
<div class="controls">
<label>Samples: <input id="nSamples" type="range" min="20" max="80" value="40" step="10">
<span id="nSamplesVal">40</span></label>
<label>Overfit level: <input id="overfit" type="range" min="1" max="5" value="3" step="1">
<span id="overfitVal">3</span></label>
<button id="runBtn" type="button">Run Attack</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="statusBox" class="status-box"></div>
<div class="chart-area">
<div class="chart-label">Members (training data)</div>
<canvas id="memberCanvas" width="420" height="100"></canvas>
<div class="chart-label">Non-members (unseen data)</div>
<canvas id="nonMemberCanvas" width="420" height="100"></canvas>
</div>
<div id="result" class="result"></div>
/* Styles for the membership inference attack demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem .8rem; align-items: center; margin-bottom: .7rem; }
.controls label { font-size: .85rem; display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 90px; }
button { font: 600 14px system-ui; padding: .42rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status-box { font-size: .85rem; color: #555; min-height: 1.3em; margin-bottom: .4rem; }
.chart-area { margin: .3rem 0; }
.chart-label { font-size: .78rem; font-weight: 600; color: #888; margin: .3rem 0 .15rem; text-transform: uppercase; letter-spacing: .04em; }
canvas { display: block; width: 100%; max-width: 420px; height: 100px; border-radius: 6px; background: #f5f7f9; border: 1px solid #dde2e8; }
.result { font-size: 1rem; font-weight: 700; margin-top: .7rem; min-height: 1.4em; }
.result.good { color: #0a7d33; }
.result.bad { color: #c92f3c; }
.result.neutral { color: #555; }
/* Shadow-model membership inference attack simulation */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const nSamplesEl = document.getElementById('nSamples');
const nSamplesVal = document.getElementById('nSamplesVal');
const overfitEl = document.getElementById('overfit');
const overfitVal = document.getElementById('overfitVal');
const runBtn = document.getElementById('runBtn');
const resetBtn = document.getElementById('resetBtn');
const statusBox = document.getElementById('statusBox');
const resultEl = document.getElementById('result');
const mCtx = document.getElementById('memberCanvas').getContext('2d');
const nmCtx = document.getElementById('nonMemberCanvas').getContext('2d');
nSamplesEl.addEventListener('input', () => { nSamplesVal.textContent = nSamplesEl.value; });
overfitEl.addEventListener('input', () => { overfitVal.textContent = overfitEl.value; });
/* Seeded pseudo-random number generator (xorshift32) */
function rng(seed) {
let s = seed >>> 0;
return () => { s ^= s << 13; s ^= s >> 17; s ^= s << 5; return (s >>> 0) / 4294967296; };
}
/* Simulate target-model confidence for a training member */
function modelConfidence(x, overfit) {
/* Higher overfit level → higher average member confidence */
const base = 0.55 + 0.07 * overfit;
const noise = (Math.random() - 0.5) * 0.18;
return Math.min(0.99, Math.max(0.01, base + noise));
}
function nonMemberConfidence(overfit) {
/* Higher overfit level → lower average non-member confidence */
const base = 0.5 - 0.04 * overfit;
const noise = (Math.random() - 0.5) * 0.22;
return Math.min(0.99, Math.max(0.01, base + noise));
}
/* Attack model: find the threshold that maximises attack accuracy */
function attackThreshold(memberScores, nonMemberScores) {
/* Sweep all candidate thresholds; pick the one that gives best (TP+TN)/N */
const allScores = [...memberScores, ...nonMemberScores].sort((a, b) => a - b);
let bestAcc = 0, bestTau = 0.5;
for (const tau of allScores) {
const tp = memberScores.filter(s => s >= tau).length;
const tn = nonMemberScores.filter(s => s < tau).length;
const acc = (tp + tn) / (memberScores.length + nonMemberScores.length);
if (acc > bestAcc) { bestAcc = acc; bestTau = tau; }
}
return { tau: bestTau, acc: bestAcc };
}
/* Draw confidence bars on a canvas; dashed line = chosen threshold */
function drawBars(ctx, scores, threshold, isMembers) {
const W = ctx.canvas.width, H = ctx.canvas.height;
ctx.clearRect(0, 0, W, H);
const n = scores.length;
const barW = Math.max(3, Math.floor((W - 8) / n) - 2);
scores.forEach((s, i) => {
const x = 4 + i * ((W - 8) / n);
const h = Math.round(s * (H - 6)) + 1;
const predicted = s >= threshold;
/* Green if prediction matches ground truth, red otherwise */
const correct = predicted === isMembers;
ctx.fillStyle = correct ? '#22a64a' : '#e63946';
ctx.beginPath();
ctx.roundRect(x, H - h - 3, barW, h, 2);
ctx.fill();
});
/* Draw the chosen threshold as a dashed horizontal line */
const ty = H - Math.round(threshold * (H - 6)) - 3;
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.beginPath();
ctx.moveTo(0, ty);
ctx.lineTo(W, ty);
ctx.stroke();
ctx.setLineDash([]);
}
function runAttack() {
const n = parseInt(nSamplesEl.value);
const overfit = parseInt(overfitEl.value);
statusBox.textContent = 'Running shadow-model attack…';
resultEl.className = 'result neutral';
resultEl.textContent = '';
/* Generate simulated confidence scores for members and non-members */
const memberScores = Array.from({ length: n }, () => modelConfidence(null, overfit));
const nonMemberScores = Array.from({ length: n }, () => nonMemberConfidence(overfit));
const { tau, acc } = attackThreshold(memberScores, nonMemberScores);
drawBars(mCtx, memberScores, tau, true);
drawBars(nmCtx, nonMemberScores, tau, false);
const pct = Math.round(acc * 100);
statusBox.textContent = i18nFmt('Attack complete. Threshold τ = %tau%.', { tau: tau.toFixed(2) });
if (pct >= 80) {
resultEl.className = 'result bad';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — strong membership signal! High overfitting leaks training data.', { pct });
} else if (pct >= 65) {
resultEl.className = 'result neutral';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — moderate signal. Some membership information leaks.', { pct });
} else {
resultEl.className = 'result good';
resultEl.textContent = i18nFmt('Attack accuracy %pct%% — weak signal. Low overfitting limits the leak.', { pct });
}
}
function resetDemo() {
mCtx.clearRect(0, 0, mCtx.canvas.width, mCtx.canvas.height);
nmCtx.clearRect(0, 0, nmCtx.canvas.width, nmCtx.canvas.height);
statusBox.textContent = '';
resultEl.textContent = '';
resultEl.className = 'result neutral';
}
runBtn.addEventListener('click', runAttack);
resetBtn.addEventListener('click', resetDemo);
/* Run the attack once on page load so the demo is not empty */
runAttack();
Comments
Loading comments...