This commit is contained in:
martijn.diersmann@pm.me
2026-09-26 18:50:28 +02:00
commit e9533f7ae7
4 changed files with 1090 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
'use strict';
// Vluchtige Node-test: laadt app.js met een minimale DOM-stub en
// controleert of het neuraal netwerk echt leert (XOR / cirkel / spiraal).
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(path.join(__dirname, 'app.js'), 'utf8');
function makeCtx() {
const data = new Uint8ClampedArray(200 * 150 * 4);
return {
createImageData: () => ({ width: 200, height: 150, data }),
putImageData: () => {}, clearRect: () => {}, drawImage: () => {},
fillRect: () => {}, fillText: () => {}, beginPath: () => {}, arc: () => {},
fill: () => {}, stroke: () => {}, moveTo: () => {}, lineTo: () => {}, strokeRect: () => {}
};
}
function makeEl() {
return {
width: 460, height: 320, style: {}, textContent: '', innerHTML: '',
scrollTop: 0, scrollHeight: 0,
classList: { add() {}, remove() {}, toggle() { return false; } },
appendChild() {}, addEventListener() {}, setAttribute() {}, click() {},
getContext: makeCtx, parentElement: { clientWidth: 800 }, dataset: {}, value: '6',
toDataURL: () => 'data:image/png;base64,xx'
};
}
const documentStub = {
getElementById: () => makeEl(),
querySelectorAll: () => [],
createElement: () => makeEl()
};
const rafNoop = () => {};
const code = src + '\n;globalThis.__NN = NeuralNet; globalThis.__makeDataset = makeDataset;';
new Function('document', 'requestAnimationFrame', 'window', 'setTimeout', code)(
documentStub, rafNoop, { addEventListener() {} }, setTimeout
);
const NeuralNet = globalThis.__NN;
const makeDataset = globalThis.__makeDataset;
function trainOn(name, epochs, lr, hidden) {
const net = new NeuralNet([2, hidden, 1]);
const samples = makeDataset(name);
const idx = samples.map((_, i) => i);
let last = null;
for (let e = 0; e < epochs; e++) {
for (let i = idx.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const t = idx[i]; idx[i] = idx[j]; idx[j] = t;
}
let sum = 0;
for (const i of idx) sum += net.trainSample(samples[i].x, samples[i].y, lr);
last = sum / idx.length;
}
const test = makeDataset(name);
const n = Math.min(200, test.length);
let correct = 0;
for (let i = 0; i < n; i++) {
if ((net.predict(test[i].x) > 0.5 ? 1 : 0) === test[i].y) correct++;
}
return { acc: (correct / n) * 100, loss: last };
}
// Finale controle — zelfde instelling als app.js (Adam, lr 0.05)
function trial(name, hidden, epochs) {
const res = [];
for (let t = 0; t < 6; t++) res.push(trainOn(name, epochs, 0.05, hidden));
const worst = Math.min(...res.map((a) => a.acc));
return { worst, raw: res.map((a) => a.acc.toFixed(1) + '%').join(' ') };
}
const xor0 = trainOn('XOR', 0, 0.05, 10);
const xor = trial('XOR', 10, 250);
const cir = trial('CIRKEL', 14, 400);
const spi = trial('SPIRAAL', 30, 1400);
console.log('XOR 0ep :', xor0.acc.toFixed(1) + '%');
console.log('XOR 250ep :', xor.raw, ' (slechtste ' + xor.worst.toFixed(1) + '%)');
console.log('CIRKEL 400ep :', cir.raw, ' (slechtste ' + cir.worst.toFixed(1) + '%)');
console.log('SPIRAAL 1400ep :', spi.raw, ' (slechtste ' + spi.worst.toFixed(1) + '%)');
let fail = 0;
if (xor0.acc < 35 || xor0.acc > 70) { console.error('FAIL: XOR ongetraind moet ~50% zijn'); fail = 1; }
if (xor.worst < 85) { console.error('FAIL: XOR convergerde niet'); fail = 1; }
if (cir.worst < 90) { console.error('FAIL: Cirkel convergerde niet'); fail = 1; }
if (spi.worst < 85) { console.error('FAIL: Spiraal convergerde niet'); fail = 1; }
if (fail) {
console.error('VALIDATIE MISLUKT');
process.exit(1);
}
console.log('PASS: alle drie datasets leren betrouwbaar met Adam (lr 0.05).');
process.exit(0);
if (fail) process.exit(1);
console.log('PASS: app.js laadt schoon én het netwerk leert alle drie datasets.');
process.exit(0);