nanye commited on
Commit
3f21703
·
1 Parent(s): dac4940

accuracy and loss plot

Browse files
Files changed (5) hide show
  1. app.js +52 -8
  2. index.html +7 -3
  3. sgd_classifier.py +45 -0
  4. styles.css +1 -0
  5. 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 { pyX.destroy(); pyY.destroy(); state.pyodide.globals.delete("preview_X"); state.pyodide.globals.delete("preview_y"); }
 
 
 
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
- <option>hinge (SVM)</option><option>squared_hinge</option>
80
- <option>log_loss (Logistic Regression)</option><option>squared_error (Adaline)</option>
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="train-heading"><h2>Training history</h2><span id="history-position">Step 0 of 0</span></div>
 
 
 
 
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"])