Spaces:
Running
Running
accuracy and loss plot
Browse files- app.js +52 -8
- index.html +7 -3
- sgd_classifier.py +45 -0
- styles.css +1 -0
- test_sgd_classifier.py +8 -0
app.js
CHANGED
|
@@ -17,9 +17,11 @@ const BUILTIN_DATASETS = {
|
|
| 17 |
],
|
| 18 |
};
|
| 19 |
|
| 20 |
-
const state = { points: structuredClone(BUILTIN_DATASETS['easy-linear']), datasetMode: "easy-linear", redoPoints: [], hoveredPoint: -1, label: "A", model: null, pendingUpdate: null, step: 0, permutation: [], cursor: 0, nextBatch: [], rngState: 42, history: [], viewIndex: -1, previewVersion: 0, previewPromise: null, busy: false, animating: false, ready: false };
|
| 21 |
const canvas = document.querySelector("#plot");
|
| 22 |
const ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
| 23 |
const ui = Object.fromEntries([...document.querySelectorAll("[id]")].map(el => [el.id, el]));
|
| 24 |
|
| 25 |
function modelOptions() {
|
|
@@ -35,7 +37,7 @@ function modelOptions() {
|
|
| 35 |
function randomSeed() { return (Number.parseInt(ui["random-seed"].value, 10) || 0) >>> 0; }
|
| 36 |
|
| 37 |
function resetModel(message = "Model reset. Take a step to begin training.") {
|
| 38 |
-
state.model = null; state.pendingUpdate = null; state.step = 0; state.permutation = []; state.cursor = 0; state.nextBatch = []; state.history = []; state.viewIndex = -1;
|
| 39 |
state.rngState = randomSeed();
|
| 40 |
queueNextBatch();
|
| 41 |
ui["step-count"].textContent = "Step 0";
|
|
@@ -96,7 +98,7 @@ function formatNumber(value) {
|
|
| 96 |
function formatVector(vector) { return `[${vector.map(formatNumber).join(", ")}]`; }
|
| 97 |
|
| 98 |
function visibleSnapshot() {
|
| 99 |
-
return state.history[state.viewIndex] || { model: state.model, pendingUpdate: state.pendingUpdate, nextBatch: state.nextBatch, step: state.step };
|
| 100 |
}
|
| 101 |
|
| 102 |
function renderParameters(snapshot = visibleSnapshot()) {
|
|
@@ -109,7 +111,7 @@ function renderParameters(snapshot = visibleSnapshot()) {
|
|
| 109 |
}
|
| 110 |
|
| 111 |
function captureSnapshot() {
|
| 112 |
-
const snapshot = structuredClone({ model: state.model, pendingUpdate: state.pendingUpdate, nextBatch: state.nextBatch, step: state.step });
|
| 113 |
if (state.history.at(-1)?.step === state.step) state.history[state.history.length - 1] = snapshot;
|
| 114 |
else state.history.push(snapshot);
|
| 115 |
state.viewIndex = state.history.length - 1;
|
|
@@ -123,7 +125,7 @@ function renderView() {
|
|
| 123 |
ui["history-position"].textContent = `Step ${snapshot.step} of ${Math.max(0, state.history.length - 1)}`;
|
| 124 |
const count = snapshot.nextBatch.length;
|
| 125 |
ui["batch-summary"].textContent = count ? `Next batch: ${count} point${count === 1 ? "" : "s"}` : "No batch selected";
|
| 126 |
-
updateControls(); draw();
|
| 127 |
}
|
| 128 |
|
| 129 |
async function refreshPreview(reportError = true) {
|
|
@@ -139,20 +141,30 @@ async function refreshPreview(reportError = true) {
|
|
| 139 |
}
|
| 140 |
const X = state.nextBatch.map(i => [state.points[i].x, state.points[i].y]);
|
| 141 |
const y = state.nextBatch.map(i => state.points[i].label);
|
|
|
|
|
|
|
| 142 |
const pyX = state.pyodide.toPy(X), pyY = state.pyodide.toPy(y);
|
|
|
|
| 143 |
state.pyodide.globals.set("preview_X", pyX); state.pyodide.globals.set("preview_y", pyY);
|
|
|
|
| 144 |
try {
|
| 145 |
const result = state.pyodide.runPython(`
|
| 146 |
preview_coef, preview_intercept = model.get_update(preview_X, preview_y, classes=["A", "B"])
|
|
|
|
| 147 |
{"coef": model.coef_.tolist(), "intercept": model.intercept_.tolist(),
|
| 148 |
-
"update_coef": preview_coef.tolist(), "update_intercept": preview_intercept.tolist()
|
|
|
|
| 149 |
`);
|
| 150 |
const values = result.toJs({ dict_converter: Object.fromEntries }); result.destroy();
|
| 151 |
if (version !== state.previewVersion) return;
|
| 152 |
state.model = { coef: values.coef, intercept: values.intercept };
|
| 153 |
state.pendingUpdate = { coef: values.update_coef, intercept: values.update_intercept };
|
|
|
|
| 154 |
captureSnapshot();
|
| 155 |
-
} finally {
|
|
|
|
|
|
|
|
|
|
| 156 |
} catch (error) {
|
| 157 |
if (version === state.previewVersion) { state.pendingUpdate = null; renderView(); if (reportError) setMessage(error instanceof Error ? error.message : String(error), "error"); }
|
| 158 |
}
|
|
@@ -221,6 +233,37 @@ async function startAnimation() {
|
|
| 221 |
|
| 222 |
function pauseAnimation() { state.animating = false; updateControls(); }
|
| 223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
function dimensions() {
|
| 225 |
const rect = canvas.getBoundingClientRect(), ratio = window.devicePixelRatio || 1;
|
| 226 |
if (canvas.width !== Math.round(rect.width * ratio) || canvas.height !== Math.round(rect.height * ratio)) { canvas.width = Math.round(rect.width * ratio); canvas.height = Math.round(rect.height * ratio); }
|
|
@@ -303,6 +346,7 @@ function selectTab(name) {
|
|
| 303 |
const configure = name === "configure";
|
| 304 |
ui["configure-tab"].classList.toggle("active", configure); ui["train-tab"].classList.toggle("active", !configure);
|
| 305 |
ui["configure-panel"].classList.toggle("active", configure); ui["train-panel"].classList.toggle("active", !configure);
|
|
|
|
| 306 |
}
|
| 307 |
ui["configure-tab"].addEventListener("click", () => selectTab("configure"));
|
| 308 |
ui["train-tab"].addEventListener("click", () => selectTab("train"));
|
|
@@ -312,4 +356,4 @@ ui["history-reset"].addEventListener("click", () => { if (state.history.length)
|
|
| 312 |
ui.animate.addEventListener("click", startAnimation); ui.pause.addEventListener("click", pauseAnimation);
|
| 313 |
for (const id of ["loss","learning-rate","batch-size","random-seed","batch-selection"]) ui[id].addEventListener("change",()=>{if(id==="batch-selection")ui["sampling-help"].textContent=ui[id].value==="random"?"Each step independently samples a subset without replacement.":"Each point is used once before the dataset is shuffled again.";resetModel("Configuration changed. The model was reset.");});
|
| 314 |
window.addEventListener("keydown",event=>{if(event.code==="Space"&&!event.repeat&&!/INPUT|SELECT|TEXTAREA/.test(document.activeElement.tagName)){event.preventDefault();trainOneStep();}});
|
| 315 |
-
new ResizeObserver(draw).observe(canvas.parentElement); queueNextBatch(); updateControls(); draw(); initializeRuntime();
|
|
|
|
| 17 |
],
|
| 18 |
};
|
| 19 |
|
| 20 |
+
const state = { points: structuredClone(BUILTIN_DATASETS['easy-linear']), datasetMode: "easy-linear", redoPoints: [], hoveredPoint: -1, label: "A", model: null, pendingUpdate: null, metrics: null, step: 0, permutation: [], cursor: 0, nextBatch: [], rngState: 42, history: [], viewIndex: -1, previewVersion: 0, previewPromise: null, busy: false, animating: false, ready: false };
|
| 21 |
const canvas = document.querySelector("#plot");
|
| 22 |
const ctx = canvas.getContext("2d");
|
| 23 |
+
const metricsCanvas = document.querySelector("#metrics-chart");
|
| 24 |
+
const metricsCtx = metricsCanvas.getContext("2d");
|
| 25 |
const ui = Object.fromEntries([...document.querySelectorAll("[id]")].map(el => [el.id, el]));
|
| 26 |
|
| 27 |
function modelOptions() {
|
|
|
|
| 37 |
function randomSeed() { return (Number.parseInt(ui["random-seed"].value, 10) || 0) >>> 0; }
|
| 38 |
|
| 39 |
function resetModel(message = "Model reset. Take a step to begin training.") {
|
| 40 |
+
state.model = null; state.pendingUpdate = null; state.metrics = null; state.step = 0; state.permutation = []; state.cursor = 0; state.nextBatch = []; state.history = []; state.viewIndex = -1;
|
| 41 |
state.rngState = randomSeed();
|
| 42 |
queueNextBatch();
|
| 43 |
ui["step-count"].textContent = "Step 0";
|
|
|
|
| 98 |
function formatVector(vector) { return `[${vector.map(formatNumber).join(", ")}]`; }
|
| 99 |
|
| 100 |
function visibleSnapshot() {
|
| 101 |
+
return state.history[state.viewIndex] || { model: state.model, pendingUpdate: state.pendingUpdate, metrics: state.metrics, nextBatch: state.nextBatch, step: state.step };
|
| 102 |
}
|
| 103 |
|
| 104 |
function renderParameters(snapshot = visibleSnapshot()) {
|
|
|
|
| 111 |
}
|
| 112 |
|
| 113 |
function captureSnapshot() {
|
| 114 |
+
const snapshot = structuredClone({ model: state.model, pendingUpdate: state.pendingUpdate, metrics: state.metrics, nextBatch: state.nextBatch, step: state.step });
|
| 115 |
if (state.history.at(-1)?.step === state.step) state.history[state.history.length - 1] = snapshot;
|
| 116 |
else state.history.push(snapshot);
|
| 117 |
state.viewIndex = state.history.length - 1;
|
|
|
|
| 125 |
ui["history-position"].textContent = `Step ${snapshot.step} of ${Math.max(0, state.history.length - 1)}`;
|
| 126 |
const count = snapshot.nextBatch.length;
|
| 127 |
ui["batch-summary"].textContent = count ? `Next batch: ${count} point${count === 1 ? "" : "s"}` : "No batch selected";
|
| 128 |
+
updateControls(); draw(); drawMetrics();
|
| 129 |
}
|
| 130 |
|
| 131 |
async function refreshPreview(reportError = true) {
|
|
|
|
| 141 |
}
|
| 142 |
const X = state.nextBatch.map(i => [state.points[i].x, state.points[i].y]);
|
| 143 |
const y = state.nextBatch.map(i => state.points[i].label);
|
| 144 |
+
const allX = state.points.map(point => [point.x, point.y]);
|
| 145 |
+
const allY = state.points.map(point => point.label);
|
| 146 |
const pyX = state.pyodide.toPy(X), pyY = state.pyodide.toPy(y);
|
| 147 |
+
const pyAllX = state.pyodide.toPy(allX), pyAllY = state.pyodide.toPy(allY);
|
| 148 |
state.pyodide.globals.set("preview_X", pyX); state.pyodide.globals.set("preview_y", pyY);
|
| 149 |
+
state.pyodide.globals.set("all_X", pyAllX); state.pyodide.globals.set("all_y", pyAllY);
|
| 150 |
try {
|
| 151 |
const result = state.pyodide.runPython(`
|
| 152 |
preview_coef, preview_intercept = model.get_update(preview_X, preview_y, classes=["A", "B"])
|
| 153 |
+
metrics = model.evaluate(all_X, all_y)
|
| 154 |
{"coef": model.coef_.tolist(), "intercept": model.intercept_.tolist(),
|
| 155 |
+
"update_coef": preview_coef.tolist(), "update_intercept": preview_intercept.tolist(),
|
| 156 |
+
"loss": metrics["loss"], "accuracy": metrics["accuracy"]}
|
| 157 |
`);
|
| 158 |
const values = result.toJs({ dict_converter: Object.fromEntries }); result.destroy();
|
| 159 |
if (version !== state.previewVersion) return;
|
| 160 |
state.model = { coef: values.coef, intercept: values.intercept };
|
| 161 |
state.pendingUpdate = { coef: values.update_coef, intercept: values.update_intercept };
|
| 162 |
+
state.metrics = { loss: values.loss, accuracy: values.accuracy };
|
| 163 |
captureSnapshot();
|
| 164 |
+
} finally {
|
| 165 |
+
pyX.destroy(); pyY.destroy(); pyAllX.destroy(); pyAllY.destroy();
|
| 166 |
+
for (const name of ["preview_X", "preview_y", "all_X", "all_y"]) state.pyodide.globals.delete(name);
|
| 167 |
+
}
|
| 168 |
} catch (error) {
|
| 169 |
if (version === state.previewVersion) { state.pendingUpdate = null; renderView(); if (reportError) setMessage(error instanceof Error ? error.message : String(error), "error"); }
|
| 170 |
}
|
|
|
|
| 233 |
|
| 234 |
function pauseAnimation() { state.animating = false; updateControls(); }
|
| 235 |
|
| 236 |
+
function drawMetrics() {
|
| 237 |
+
const rect = metricsCanvas.getBoundingClientRect();
|
| 238 |
+
if (!rect.width || !rect.height) return;
|
| 239 |
+
const ratio = window.devicePixelRatio || 1;
|
| 240 |
+
metricsCanvas.width = Math.round(rect.width * ratio); metricsCanvas.height = Math.round(rect.height * ratio);
|
| 241 |
+
metricsCtx.setTransform(ratio, 0, 0, ratio, 0, 0); metricsCtx.clearRect(0, 0, rect.width, rect.height);
|
| 242 |
+
const data = state.history.filter(snapshot => snapshot.metrics);
|
| 243 |
+
const pad = { left: 34, right: 34, top: 10, bottom: 22 };
|
| 244 |
+
const width = rect.width - pad.left - pad.right, height = rect.height - pad.top - pad.bottom;
|
| 245 |
+
const bottom = pad.top + height;
|
| 246 |
+
metricsCtx.font = "9px system-ui"; metricsCtx.fillStyle = "#77847d"; metricsCtx.strokeStyle = "#e4e9e5"; metricsCtx.lineWidth = 1;
|
| 247 |
+
for (let fraction = 0; fraction <= 1; fraction += 0.5) {
|
| 248 |
+
const y = pad.top + height * fraction; metricsCtx.beginPath(); metricsCtx.moveTo(pad.left,y); metricsCtx.lineTo(pad.left+width,y); metricsCtx.stroke();
|
| 249 |
+
}
|
| 250 |
+
if (!data.length) { metricsCtx.textAlign="center"; metricsCtx.fillText("Metrics appear after initialization",rect.width/2,rect.height/2); return; }
|
| 251 |
+
const maxLoss = Math.max(...data.map(snapshot => snapshot.metrics.loss), 1e-9) * 1.08;
|
| 252 |
+
const pointX = index => data.length === 1 ? pad.left + width / 2 : pad.left + index / (data.length - 1) * width;
|
| 253 |
+
const lossY = value => bottom - value / maxLoss * height;
|
| 254 |
+
const accuracyY = value => bottom - value * height;
|
| 255 |
+
metricsCtx.textAlign="right"; metricsCtx.fillStyle="#8b5cf6"; metricsCtx.fillText(maxLoss.toFixed(2),pad.left-5,pad.top+3); metricsCtx.fillText("0",pad.left-5,bottom+3);
|
| 256 |
+
metricsCtx.textAlign="left"; metricsCtx.fillStyle="#159267"; metricsCtx.fillText("100%",pad.left+width+5,pad.top+3); metricsCtx.fillText("0%",pad.left+width+5,bottom+3);
|
| 257 |
+
metricsCtx.textAlign="center"; metricsCtx.fillStyle="#77847d"; metricsCtx.fillText("step",pad.left+width/2,rect.height-3);
|
| 258 |
+
const drawSeries = (color, getY) => {
|
| 259 |
+
metricsCtx.strokeStyle=color; metricsCtx.lineWidth=2; metricsCtx.beginPath();
|
| 260 |
+
data.forEach((snapshot,index)=>{const x=pointX(index),y=getY(snapshot.metrics);if(index)metricsCtx.lineTo(x,y);else metricsCtx.moveTo(x,y);}); metricsCtx.stroke();
|
| 261 |
+
data.forEach((snapshot,index)=>{const x=pointX(index),y=getY(snapshot.metrics),active=index===state.viewIndex;metricsCtx.beginPath();metricsCtx.arc(x,y,active?5:2.5,0,Math.PI*2);metricsCtx.fillStyle=active?"white":color;metricsCtx.fill();metricsCtx.strokeStyle=color;metricsCtx.lineWidth=active?2.5:1;metricsCtx.stroke();});
|
| 262 |
+
};
|
| 263 |
+
drawSeries("#8b5cf6", metrics => lossY(metrics.loss));
|
| 264 |
+
drawSeries("#159267", metrics => accuracyY(metrics.accuracy));
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
function dimensions() {
|
| 268 |
const rect = canvas.getBoundingClientRect(), ratio = window.devicePixelRatio || 1;
|
| 269 |
if (canvas.width !== Math.round(rect.width * ratio) || canvas.height !== Math.round(rect.height * ratio)) { canvas.width = Math.round(rect.width * ratio); canvas.height = Math.round(rect.height * ratio); }
|
|
|
|
| 346 |
const configure = name === "configure";
|
| 347 |
ui["configure-tab"].classList.toggle("active", configure); ui["train-tab"].classList.toggle("active", !configure);
|
| 348 |
ui["configure-panel"].classList.toggle("active", configure); ui["train-panel"].classList.toggle("active", !configure);
|
| 349 |
+
if (!configure) requestAnimationFrame(drawMetrics);
|
| 350 |
}
|
| 351 |
ui["configure-tab"].addEventListener("click", () => selectTab("configure"));
|
| 352 |
ui["train-tab"].addEventListener("click", () => selectTab("train"));
|
|
|
|
| 356 |
ui.animate.addEventListener("click", startAnimation); ui.pause.addEventListener("click", pauseAnimation);
|
| 357 |
for (const id of ["loss","learning-rate","batch-size","random-seed","batch-selection"]) ui[id].addEventListener("change",()=>{if(id==="batch-selection")ui["sampling-help"].textContent=ui[id].value==="random"?"Each step independently samples a subset without replacement.":"Each point is used once before the dataset is shuffled again.";resetModel("Configuration changed. The model was reset.");});
|
| 358 |
window.addEventListener("keydown",event=>{if(event.code==="Space"&&!event.repeat&&!/INPUT|SELECT|TEXTAREA/.test(document.activeElement.tagName)){event.preventDefault();trainOneStep();}});
|
| 359 |
+
new ResizeObserver(draw).observe(canvas.parentElement); new ResizeObserver(drawMetrics).observe(metricsCanvas); queueNextBatch(); updateControls(); draw(); initializeRuntime();
|
index.html
CHANGED
|
@@ -76,8 +76,8 @@
|
|
| 76 |
<label for="loss">Loss</label>
|
| 77 |
<select id="loss">
|
| 78 |
<option>perceptron</option>
|
| 79 |
-
|
| 80 |
-
|
| 81 |
<option>huber</option><option>modified_huber</option>
|
| 82 |
<option>epsilon_insensitive</option><option>squared_epsilon_insensitive</option>
|
| 83 |
</select>
|
|
@@ -102,7 +102,11 @@
|
|
| 102 |
</div>
|
| 103 |
<div id="train-panel" class="tab-panel">
|
| 104 |
<section class="control-section train-section">
|
| 105 |
-
<div class="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
<div class="parameter-panel" aria-live="polite">
|
| 107 |
<div class="parameter-block">
|
| 108 |
<span>Class A parameters</span><small>Weight vector and bias for the positive class</small>
|
|
|
|
| 76 |
<label for="loss">Loss</label>
|
| 77 |
<select id="loss">
|
| 78 |
<option>perceptron</option>
|
| 79 |
+
<option>squared_error</option><option>log_loss</option>
|
| 80 |
+
<option>hinge</option><option>squared_hinge</option>
|
| 81 |
<option>huber</option><option>modified_huber</option>
|
| 82 |
<option>epsilon_insensitive</option><option>squared_epsilon_insensitive</option>
|
| 83 |
</select>
|
|
|
|
| 102 |
</div>
|
| 103 |
<div id="train-panel" class="tab-panel">
|
| 104 |
<section class="control-section train-section">
|
| 105 |
+
<div class="metrics-panel">
|
| 106 |
+
<div class="metrics-heading"><h2>Metrics</h2><div><span class="metric-key loss-key"></span>Loss <span class="metric-key accuracy-key"></span>Accuracy</div></div>
|
| 107 |
+
<canvas id="metrics-chart" aria-label="Loss and accuracy over training steps"></canvas>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="train-heading"><h2>Parameters</h2><span id="history-position">Step 0 of 0</span></div>
|
| 110 |
<div class="parameter-panel" aria-live="polite">
|
| 111 |
<div class="parameter-block">
|
| 112 |
<span>Class A parameters</span><small>Weight vector and bias for the positive class</small>
|
sgd_classifier.py
CHANGED
|
@@ -137,6 +137,51 @@ class SGDClassifier:
|
|
| 137 |
outside, 2 * np.sign(residual) * (np.abs(residual) - self.epsilon), 0.0
|
| 138 |
)
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
def _regularization_gradient(self):
|
| 141 |
if self.penalty is None or self.alpha == 0:
|
| 142 |
return np.zeros_like(self.coef_)
|
|
|
|
| 137 |
outside, 2 * np.sign(residual) * (np.abs(residual) - self.epsilon), 0.0
|
| 138 |
)
|
| 139 |
|
| 140 |
+
def _loss_values(self, scores, targets):
|
| 141 |
+
"""Element-wise loss corresponding to :meth:`_loss_gradient`."""
|
| 142 |
+
margin = targets * scores
|
| 143 |
+
if self.loss == "hinge":
|
| 144 |
+
return np.maximum(0.0, 1 - margin)
|
| 145 |
+
if self.loss == "log_loss":
|
| 146 |
+
return np.logaddexp(0.0, -margin)
|
| 147 |
+
if self.loss == "modified_huber":
|
| 148 |
+
return np.where(
|
| 149 |
+
margin >= 1,
|
| 150 |
+
0.0,
|
| 151 |
+
np.where(margin >= -1, (1 - margin) ** 2, -4 * margin),
|
| 152 |
+
)
|
| 153 |
+
if self.loss == "squared_hinge":
|
| 154 |
+
return np.maximum(0.0, 1 - margin) ** 2
|
| 155 |
+
if self.loss == "perceptron":
|
| 156 |
+
return np.maximum(0.0, -margin)
|
| 157 |
+
|
| 158 |
+
residual = scores - targets
|
| 159 |
+
absolute = np.abs(residual)
|
| 160 |
+
if self.loss == "squared_error":
|
| 161 |
+
return 0.5 * residual**2
|
| 162 |
+
if self.loss == "huber":
|
| 163 |
+
return np.where(
|
| 164 |
+
absolute <= self.epsilon,
|
| 165 |
+
0.5 * residual**2,
|
| 166 |
+
self.epsilon * (absolute - 0.5 * self.epsilon),
|
| 167 |
+
)
|
| 168 |
+
if self.loss == "epsilon_insensitive":
|
| 169 |
+
return np.maximum(0.0, absolute - self.epsilon)
|
| 170 |
+
return np.maximum(0.0, absolute - self.epsilon) ** 2
|
| 171 |
+
|
| 172 |
+
def evaluate(self, X, y):
|
| 173 |
+
"""Return mean loss and accuracy over an entire labelled dataset."""
|
| 174 |
+
X, y = self._check_X_y(X, y)
|
| 175 |
+
if not hasattr(self, "coef_"):
|
| 176 |
+
raise RuntimeError("The classifier has not been initialized.")
|
| 177 |
+
if X.shape[1] != self.n_features_in_:
|
| 178 |
+
raise ValueError(f"X must have {self.n_features_in_} features.")
|
| 179 |
+
scores = X @ self.coef_.T + self.intercept_
|
| 180 |
+
targets = np.where(y[:, None] == self.classes_[None, :], 1.0, -1.0)
|
| 181 |
+
loss = float(np.mean(self._loss_values(scores, targets)))
|
| 182 |
+
predictions = self.classes_[np.argmax(scores, axis=1)]
|
| 183 |
+
return {"loss": loss, "accuracy": float(np.mean(predictions == y))}
|
| 184 |
+
|
| 185 |
def _regularization_gradient(self):
|
| 186 |
if self.penalty is None or self.alpha == 0:
|
| 187 |
return np.zeros_like(self.coef_)
|
styles.css
CHANGED
|
@@ -31,6 +31,7 @@ h2 { margin: 0; font-size: 15px; letter-spacing: -.01em; }.plot-toolbar p { marg
|
|
| 31 |
.control-card { overflow: hidden; align-self: stretch; min-height: 0; }.control-section { padding: 16px 20px 18px; border-bottom: 1px solid var(--line); }.control-section:last-child { border-bottom: 0; }
|
| 32 |
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 7px; border-bottom: 1px solid var(--line); background: #f7f9f6; }.tab { border: 0; color: var(--ink-soft); background: transparent; }.tab.active { color: var(--green); background: white; box-shadow: 0 1px 4px rgba(20,50,35,.12); }.tab-panel { display: none; }.tab-panel.active { display: block; height: calc(100% - 51px); overflow-y: auto; }
|
| 33 |
.train-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 13px; }.train-heading span { color: var(--ink-soft); font-size: 10px; }
|
|
|
|
| 34 |
.parameter-panel { border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: #f8faf7; }
|
| 35 |
.parameter-block { padding: 12px; }.parameter-block + .parameter-block { border-top: 1px solid var(--line); }.parameter-block > span { display: block; color: #435149; font-size: 11px; font-weight: 800; }.parameter-block small { display: block; color: var(--ink-soft); font-size: 9px; margin: 3px 0 9px; }
|
| 36 |
.parameter-block code { display: block; color: #35433b; font: 10px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }.parameter-block.update-values { background: rgba(255,202,58,.08); }.update-values code { color: #775b12; }
|
|
|
|
| 31 |
.control-card { overflow: hidden; align-self: stretch; min-height: 0; }.control-section { padding: 16px 20px 18px; border-bottom: 1px solid var(--line); }.control-section:last-child { border-bottom: 0; }
|
| 32 |
.tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; padding: 7px; border-bottom: 1px solid var(--line); background: #f7f9f6; }.tab { border: 0; color: var(--ink-soft); background: transparent; }.tab.active { color: var(--green); background: white; box-shadow: 0 1px 4px rgba(20,50,35,.12); }.tab-panel { display: none; }.tab-panel.active { display: block; height: calc(100% - 51px); overflow-y: auto; }
|
| 33 |
.train-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 13px; }.train-heading span { color: var(--ink-soft); font-size: 10px; }
|
| 34 |
+
.metrics-panel { margin-bottom: 15px; border: 1px solid var(--line); border-radius: 9px; padding: 10px; background: white; }.metrics-heading { display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; }.metrics-heading > div { color: var(--ink-soft); font-size: 9px; display: flex; align-items: center; gap: 5px; }.metric-key { width: 13px; height: 3px; display: inline-block; border-radius: 2px; margin-left: 6px; }.loss-key { background: #8b5cf6; }.accuracy-key { background: #159267; }#metrics-chart { display: block; width: 100%; height: 135px; }
|
| 35 |
.parameter-panel { border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: #f8faf7; }
|
| 36 |
.parameter-block { padding: 12px; }.parameter-block + .parameter-block { border-top: 1px solid var(--line); }.parameter-block > span { display: block; color: #435149; font-size: 11px; font-weight: 800; }.parameter-block small { display: block; color: var(--ink-soft); font-size: 9px; margin: 3px 0 9px; }
|
| 37 |
.parameter-block code { display: block; color: #35433b; font: 10px/1.65 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }.parameter-block.update-values { background: rgba(255,202,58,.08); }.update-values code { color: #775b12; }
|
test_sgd_classifier.py
CHANGED
|
@@ -44,6 +44,14 @@ def test_get_update_previews_without_applying_it():
|
|
| 44 |
np.testing.assert_allclose(model.intercept_, intercept_update)
|
| 45 |
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def test_train_step_can_declare_classes_missing_from_batch():
|
| 48 |
model = SGDClassifier()
|
| 49 |
model.train_step([[1.0, 2.0]], ["a"], classes=["a", "b"])
|
|
|
|
| 44 |
np.testing.assert_allclose(model.intercept_, intercept_update)
|
| 45 |
|
| 46 |
|
| 47 |
+
def test_evaluate_returns_dataset_loss_and_accuracy():
|
| 48 |
+
X, y = dataset(samples=30)
|
| 49 |
+
model = SGDClassifier(loss="log_loss").fit(X, y)
|
| 50 |
+
metrics = model.evaluate(X, y)
|
| 51 |
+
assert metrics["loss"] >= 0
|
| 52 |
+
assert 0 <= metrics["accuracy"] <= 1
|
| 53 |
+
|
| 54 |
+
|
| 55 |
def test_train_step_can_declare_classes_missing_from_batch():
|
| 56 |
model = SGDClassifier()
|
| 57 |
model.train_step([[1.0, 2.0]], ["a"], classes=["a", "b"])
|