playground: add loss history tracking and diagnostics panel
This commit is contained in:
parent
96a88f4b0f
commit
3039221165
4 changed files with 171 additions and 4 deletions
|
|
@ -285,6 +285,38 @@ html, body {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 3px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
color: #707070;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
color: #b5b5b5;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loss-plot-wrap {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loss-plot {
|
||||||
|
width: 100%;
|
||||||
|
height: 86px;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,9 @@ export class IML {
|
||||||
this.storedWeights = null;
|
this.storedWeights = null;
|
||||||
this.weightsRandomised = false;
|
this.weightsRandomised = false;
|
||||||
this.lastLoss = null;
|
this.lastLoss = null;
|
||||||
|
this.bestLoss = null;
|
||||||
|
this.lossHistory = [];
|
||||||
|
this.totalTrainingIterations = 0;
|
||||||
this.logFn = null;
|
this.logFn = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +165,7 @@ export class IML {
|
||||||
this.process();
|
this.process();
|
||||||
}
|
}
|
||||||
|
|
||||||
train() {
|
train(options = {}) {
|
||||||
// Restore weights if randomised
|
// Restore weights if randomised
|
||||||
if (this.weightsRandomised && this.storedWeights) {
|
if (this.weightsRandomised && this.storedWeights) {
|
||||||
this.mlp.setWeights(this.storedWeights);
|
this.mlp.setWeights(this.storedWeights);
|
||||||
|
|
@ -183,9 +186,21 @@ export class IML {
|
||||||
labels,
|
labels,
|
||||||
this.learningRate,
|
this.learningRate,
|
||||||
this.maxIterations,
|
this.maxIterations,
|
||||||
this.convergenceThreshold
|
this.convergenceThreshold,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const latestHistory = this.mlp.lastTrainingHistory || [];
|
||||||
|
if (latestHistory.length > 0) {
|
||||||
|
this.lossHistory.push(...latestHistory);
|
||||||
|
this.totalTrainingIterations += latestHistory.length;
|
||||||
|
if (this.lossHistory.length > 1200) {
|
||||||
|
this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200);
|
||||||
|
}
|
||||||
|
const runBest = Math.min(...latestHistory);
|
||||||
|
this.bestLoss = this.bestLoss === null ? runBest : Math.min(this.bestLoss, runBest);
|
||||||
|
}
|
||||||
|
|
||||||
// Run inference after training
|
// Run inference after training
|
||||||
const inputWithBias = [...this.inputState, 1.0];
|
const inputWithBias = [...this.inputState, 1.0];
|
||||||
const { output } = this.mlp.getOutput(inputWithBias);
|
const { output } = this.mlp.getOutput(inputWithBias);
|
||||||
|
|
|
||||||
|
|
@ -67,9 +67,11 @@ export class MLP {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per-sample SGD training (Train method from C++)
|
// Per-sample SGD training (Train method from C++)
|
||||||
train(features, labels, learningRate, maxIterations = 1000, minError = 0.00001) {
|
train(features, labels, learningRate, maxIterations = 1000, minError = 0.00001, options = {}) {
|
||||||
const sampleSizeRecip = 1 / features.length;
|
const sampleSizeRecip = 1 / features.length;
|
||||||
let loss = 0;
|
let loss = 0;
|
||||||
|
const history = [];
|
||||||
|
const onIteration = typeof options.onIteration === 'function' ? options.onIteration : null;
|
||||||
|
|
||||||
for (let iter = 0; iter < maxIterations; iter++) {
|
for (let iter = 0; iter < maxIterations; iter++) {
|
||||||
loss = 0;
|
loss = 0;
|
||||||
|
|
@ -89,6 +91,9 @@ export class MLP {
|
||||||
}
|
}
|
||||||
|
|
||||||
loss *= sampleSizeRecip;
|
loss *= sampleSizeRecip;
|
||||||
|
history.push(loss);
|
||||||
|
|
||||||
|
if (onIteration) onIteration(iter, loss);
|
||||||
|
|
||||||
if (this.progressCallback && (iter & 0x1F) === 0) {
|
if (this.progressCallback && (iter & 0x1F) === 0) {
|
||||||
this.progressCallback(iter, loss);
|
this.progressCallback(iter, loss);
|
||||||
|
|
@ -97,14 +102,17 @@ export class MLP {
|
||||||
if (loss < minError) break;
|
if (loss < minError) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastTrainingHistory = history;
|
||||||
return loss;
|
return loss;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch training with RMSProp (TrainBatch from C++)
|
// Batch training with RMSProp (TrainBatch from C++)
|
||||||
trainBatch(features, labels, learningRate, maxIterations = 1000, batchSize = 8, minError = 0.00001) {
|
trainBatch(features, labels, learningRate, maxIterations = 1000, batchSize = 8, minError = 0.00001, options = {}) {
|
||||||
const nSamples = features.length;
|
const nSamples = features.length;
|
||||||
const nBatches = Math.ceil(nSamples / batchSize);
|
const nBatches = Math.ceil(nSamples / batchSize);
|
||||||
let epochLoss = 0;
|
let epochLoss = 0;
|
||||||
|
const history = [];
|
||||||
|
const onIteration = typeof options.onIteration === 'function' ? options.onIteration : null;
|
||||||
|
|
||||||
for (let iter = 0; iter < maxIterations; iter++) {
|
for (let iter = 0; iter < maxIterations; iter++) {
|
||||||
epochLoss = 0;
|
epochLoss = 0;
|
||||||
|
|
@ -164,6 +172,8 @@ export class MLP {
|
||||||
}
|
}
|
||||||
|
|
||||||
epochLoss /= nBatches;
|
epochLoss /= nBatches;
|
||||||
|
history.push(epochLoss);
|
||||||
|
if (onIteration) onIteration(iter, epochLoss);
|
||||||
|
|
||||||
if (this.progressCallback) {
|
if (this.progressCallback) {
|
||||||
this.progressCallback(iter, epochLoss);
|
this.progressCallback(iter, epochLoss);
|
||||||
|
|
@ -172,6 +182,7 @@ export class MLP {
|
||||||
if (epochLoss < minError) break;
|
if (epochLoss < minError) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.lastTrainingHistory = history;
|
||||||
return epochLoss;
|
return epochLoss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
109
playground/js/ui/controls.js
vendored
109
playground/js/ui/controls.js
vendored
|
|
@ -35,6 +35,27 @@ export class Controls {
|
||||||
<span id="status-loss"></span>
|
<span id="status-loss"></span>
|
||||||
<span id="status-noise" class="hidden">Noise: 0.05</span>
|
<span id="status-noise" class="hidden">Noise: 0.05</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<span class="metric-label">Mode</span>
|
||||||
|
<span class="metric-value" id="metric-mode">examples</span>
|
||||||
|
<span class="metric-label">Joystick</span>
|
||||||
|
<span class="metric-value" id="metric-joystick">0.50, 0.50</span>
|
||||||
|
<span class="metric-label">Output Mean</span>
|
||||||
|
<span class="metric-value" id="metric-mean">0.000</span>
|
||||||
|
<span class="metric-label">Output Spread</span>
|
||||||
|
<span class="metric-value" id="metric-spread">0.000</span>
|
||||||
|
<span class="metric-label">Best Loss</span>
|
||||||
|
<span class="metric-value" id="metric-best-loss">-</span>
|
||||||
|
<span class="metric-label">Train Iters</span>
|
||||||
|
<span class="metric-value" id="metric-train-iters">0</span>
|
||||||
|
<span class="metric-label">Gamepad</span>
|
||||||
|
<span class="metric-value" id="metric-gamepad">Disconnected</span>
|
||||||
|
<span class="metric-label">Follow</span>
|
||||||
|
<span class="metric-value" id="metric-follow">Off</span>
|
||||||
|
</div>
|
||||||
|
<div class="loss-plot-wrap">
|
||||||
|
<canvas id="loss-plot" width="420" height="86"></canvas>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Mode toggle
|
// Mode toggle
|
||||||
|
|
@ -70,9 +91,97 @@ export class Controls {
|
||||||
this.el.querySelector('#status-examples').textContent = `Examples: ${exampleCount}`;
|
this.el.querySelector('#status-examples').textContent = `Examples: ${exampleCount}`;
|
||||||
if (loss !== null && loss !== undefined) {
|
if (loss !== null && loss !== undefined) {
|
||||||
this.el.querySelector('#status-loss').textContent = `Loss: ${loss.toFixed(5)}`;
|
this.el.querySelector('#status-loss').textContent = `Loss: ${loss.toFixed(5)}`;
|
||||||
|
} else {
|
||||||
|
this.el.querySelector('#status-loss').textContent = 'Loss: -';
|
||||||
}
|
}
|
||||||
if (noiseLevel !== undefined) {
|
if (noiseLevel !== undefined) {
|
||||||
this.el.querySelector('#status-noise').textContent = `Noise: ${noiseLevel.toFixed(3)}`;
|
this.el.querySelector('#status-noise').textContent = `Noise: ${noiseLevel.toFixed(3)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMetrics(metrics = {}) {
|
||||||
|
const set = (id, value) => {
|
||||||
|
const el = this.el.querySelector(id);
|
||||||
|
if (el) el.textContent = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (metrics.mode) set('#metric-mode', metrics.mode);
|
||||||
|
if (metrics.joystickX !== undefined && metrics.joystickY !== undefined) {
|
||||||
|
set('#metric-joystick', `${metrics.joystickX.toFixed(2)}, ${metrics.joystickY.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
if (metrics.outputMean !== undefined) set('#metric-mean', metrics.outputMean.toFixed(3));
|
||||||
|
if (metrics.outputSpread !== undefined) set('#metric-spread', metrics.outputSpread.toFixed(3));
|
||||||
|
if (metrics.bestLoss !== undefined && metrics.bestLoss !== null) {
|
||||||
|
set('#metric-best-loss', metrics.bestLoss.toFixed(5));
|
||||||
|
}
|
||||||
|
if (metrics.totalTrainingIterations !== undefined) {
|
||||||
|
set('#metric-train-iters', String(metrics.totalTrainingIterations));
|
||||||
|
}
|
||||||
|
if (metrics.gamepadConnected !== undefined) {
|
||||||
|
set('#metric-gamepad', metrics.gamepadConnected ? 'Connected' : 'Disconnected');
|
||||||
|
}
|
||||||
|
if (metrics.followMode !== undefined) {
|
||||||
|
set('#metric-follow', metrics.followMode ? 'On' : 'Off');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLossPlot(lossHistory = []) {
|
||||||
|
const canvas = this.el.querySelector('#loss-plot');
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.fillStyle = '#121212';
|
||||||
|
ctx.fillRect(0, 0, width, height);
|
||||||
|
ctx.strokeStyle = '#2b2b2b';
|
||||||
|
ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
|
||||||
|
|
||||||
|
if (!lossHistory.length) {
|
||||||
|
ctx.fillStyle = '#666';
|
||||||
|
ctx.font = '11px monospace';
|
||||||
|
ctx.fillText('Loss history appears after training.', 10, 18);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = lossHistory.slice(-420);
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
for (const v of points) {
|
||||||
|
if (v < min) min = v;
|
||||||
|
if (v > max) max = v;
|
||||||
|
}
|
||||||
|
const range = Math.max(max - min, 1e-9);
|
||||||
|
const leftPad = 6;
|
||||||
|
const rightPad = 6;
|
||||||
|
const topPad = 6;
|
||||||
|
const bottomPad = 14;
|
||||||
|
const plotW = width - leftPad - rightPad;
|
||||||
|
const plotH = height - topPad - bottomPad;
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#243523';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(leftPad, topPad + plotH / 2);
|
||||||
|
ctx.lineTo(width - rightPad, topPad + plotH / 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00ff88';
|
||||||
|
ctx.lineWidth = 1.6;
|
||||||
|
ctx.beginPath();
|
||||||
|
for (let i = 0; i < points.length; i++) {
|
||||||
|
const x = leftPad + (i / Math.max(points.length - 1, 1)) * plotW;
|
||||||
|
const yNorm = (points[i] - min) / range;
|
||||||
|
const y = topPad + (1 - yNorm) * plotH;
|
||||||
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.fillStyle = '#8a8a8a';
|
||||||
|
ctx.font = '10px monospace';
|
||||||
|
ctx.fillText(`min ${min.toFixed(5)}`, leftPad, height - 3);
|
||||||
|
ctx.fillText(`max ${max.toFixed(5)}`, width - rightPad - 78, height - 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue