/**
* @fileoverview Perceptron-Controller – Steuerlogik für das Perceptron-Playground.
*
* Aufgaben:
* 1. Dataset auswählen & Datentabelle rendern (mit Vorhersagespalte)
* 2. Ein einzelnes Neuron (2 Inputs → 1 Output, Step-Aktivierung) steuern
* 3. Phasenbasiertes Delta-Regel-Training (Schritt / Auto / Speed-Slider)
* 4. Decision Boundary, Gleichung, Fehler-Highlights live updaten
* 5. Lernregel-Visualisierung mit Gewichts-Transition
* 6. Fehler-pro-Epoche Chart
* 7. Gaussian-Generator Integration
*
* Training läuft direkt im Main-Thread (nur 1 Neuron, ~20 Datenpunkte →
* kein Web-Worker nötig).
*
* @module perceptron-controller
* @author Alexander Wolf
*/
'use strict';
/* global PerceptronVisualizer, PERCEPTRON_DATASETS, GaussianGenerator,
Neuron, ACTIVATION_FUNCTIONS, NN_CONSTANTS, DebugConfig, DEBUG_DOMAINS */
/**
* Training Phases for step-through visualization.
* @enum {number}
*/
const TRAIN_PHASE = {
IDLE: 0,
SELECT_POINT: 1, // Randomly select misclassified point
UPDATE_W1: 2, // Show Δw₁ calculation
UPDATE_W2: 3, // Show Δw₂ calculation
APPLY_WEIGHTS: 4, // Apply new weights, show old→new
RETRAIN_VIZ: 5, // Update decision line, table predictions, error chart
};
/**
* @class PerceptronController
* @description Orchestriert Perceptron-Training, Visualisierung und UI.
*/
class PerceptronController {
/**
* @param {Object} ids – DOM-Element-IDs
* @param {string} ids.plotContainer
* @param {string} ids.archContainer
* @param {string} ids.equationContainer
* @param {string} ids.errorChartContainer
* @param {string} ids.dataTableBody
* @param {string} ids.dataTableWrapper
* @param {string} ids.tableTitle
* @param {string} ids.learningRuleDisplay
* @param {string} ids.datasetSelector
* @param {string} ids.learnRateInput
* @param {string} ids.maxEpochsInput
* @param {string} ids.speedSlider
* @param {string} ids.speedLabel
* @param {string} ids.trainBtn
* @param {string} ids.stepBtn
* @param {string} ids.resetBtn
* @param {string} ids.epochDisplay
* @param {string} ids.errorDisplay
* @param {string} ids.accuracyDisplay
* @param {string} ids.gaussianConfig
* @param {string} ids.commentsDisplay
* @param {string} ids.errorChartToggle
*/
constructor(ids) {
/** @private */
this._ids = ids;
const C = typeof NN_CONSTANTS !== 'undefined' ? NN_CONSTANTS : {};
// --- Hyperparameter ---
/** @private */
this._learningRate = C.PERCEPTRON_DEFAULT_LEARN_RATE || 0.1;
/** @private */
this._maxEpochs = C.PERCEPTRON_MAX_EPOCHS || 500;
/** @private */
this._trainDelayMin = C.PERCEPTRON_TRAIN_DELAY_MIN_MS || 10;
/** @private */
this._trainDelayMax = C.PERCEPTRON_TRAIN_DELAY_MAX_MS || 1200;
/** @private */
this._trainDelay = C.PERCEPTRON_TRAIN_DELAY_MS || 200;
/** @private - Accuracy threshold (0-100%), 100 means full convergence */
this._accuracyThreshold = 100;
// --- State ---
/** @private @type {string} */
this._currentDatasetKey = 'schwimmen';
/** @private @type {PerceptronDataset|null} */
this._dataset = null;
/** @private @type {Neuron|null} */
this._neuron = null;
/** @private */
this._epoch = 0;
/** @private */
this._totalError = 0;
/** @private */
this._isTraining = false;
/** @private */
this._trainTimerId = null;
// Phase-based step state
/** @private */
this._currentPhase = TRAIN_PHASE.IDLE;
/** @private */
this._selectedPointIdx = -1;
/** @private @type {{ w1: number, w2: number, bias: number }|null} */
this._prevWeights = null;
/** @private */
this._currentError = 0;
// --- Visualizer ---
/** @private @type {PerceptronVisualizer|null} */
this._viz = null;
// --- DOM Refs (cached) ---
/** @private */
this._dom = {};
this._init();
}
// ========================================================================
// INIT
// ========================================================================
/** @private */
_init() {
this._cacheDom();
this._bindEvents();
this._viz = new PerceptronVisualizer({
plotContainerId: this._ids.plotContainer,
archContainerId: this._ids.archContainer,
equationContainerId: this._ids.equationContainer,
errorChartContainerId: this._ids.errorChartContainer,
});
// Listen for manual input from architecture diagram
const archEl = document.getElementById(this._ids.archContainer);
if (archEl) {
archEl.addEventListener('arch-manual-input', (e) => {
this._handleManualInput(e.detail.x1, e.detail.x2);
});
}
this._loadDataset(this._currentDatasetKey);
this._resetNeuron();
this._debugLog('debug', 'Controller initialisiert', {
dataset: this._currentDatasetKey,
learningRate: this._learningRate,
});
}
/** @private */
_cacheDom() {
const ids = this._ids;
this._dom = {
tableBody: document.getElementById(ids.dataTableBody),
tableWrapper: document.getElementById(ids.dataTableWrapper),
tableTitle: document.getElementById(ids.tableTitle),
learnRuleDisp: document.getElementById(ids.learningRuleDisplay),
datasetSelect: document.getElementById(ids.datasetSelector),
learnRate: document.getElementById(ids.learnRateInput),
maxEpochs: document.getElementById(ids.maxEpochsInput),
speedSlider: document.getElementById(ids.speedSlider),
speedLabel: document.getElementById(ids.speedLabel),
trainBtn: document.getElementById(ids.trainBtn),
stepBtn: document.getElementById(ids.stepBtn),
resetBtn: document.getElementById(ids.resetBtn),
epochDisplay: document.getElementById(ids.epochDisplay),
errorDisplay: document.getElementById(ids.errorDisplay),
accuracyDisplay: document.getElementById(ids.accuracyDisplay),
gaussianConfig: document.getElementById(ids.gaussianConfig),
errorChartContainer: document.getElementById(ids.errorChartContainer),
plotContainer: document.getElementById(ids.plotContainer),
commentsDisplay: document.getElementById(ids.commentsDisplay),
accuracyThreshold: document.getElementById(ids.accuracyThreshold),
};
// Tab system
this._tabButtons = document.querySelectorAll('.tab-btn');
this._tabContents = document.querySelectorAll('.tab-content');
/** @private */ this._sortCol = null;
/** @private */ this._sortAsc = true;
}
/** @private */
_bindEvents() {
const d = this._dom;
if (d.datasetSelect) {
d.datasetSelect.addEventListener('change', () => {
this._currentDatasetKey = d.datasetSelect.value;
this._loadDataset(this._currentDatasetKey);
this._resetNeuron();
});
}
if (d.trainBtn) d.trainBtn.addEventListener('click', () => this.toggleTrain());
if (d.stepBtn) d.stepBtn.addEventListener('click', () => this.manualStep());
if (d.resetBtn) d.resetBtn.addEventListener('click', () => this._fullReset());
// Lernrate live
if (d.learnRate) {
d.learnRate.addEventListener('change', () => {
this._learningRate = parseFloat(d.learnRate.value) || 0.1;
});
}
if (d.maxEpochs) {
d.maxEpochs.addEventListener('change', () => {
this._maxEpochs = parseInt(d.maxEpochs.value, 10) || 500;
});
}
// Accuracy threshold
if (d.accuracyThreshold) {
d.accuracyThreshold.addEventListener('change', () => {
this._accuracyThreshold = parseFloat(d.accuracyThreshold.value) || 100;
});
}
// Speed slider
if (d.speedSlider) {
d.speedSlider.addEventListener('input', () => {
const val = parseInt(d.speedSlider.value, 10);
// Invert: slider left = slow (high delay), right = fast (low delay)
this._trainDelay = this._trainDelayMax - val + this._trainDelayMin;
this._updateSpeedLabel();
});
}
// Gaussian generator
this._initGaussianControls();
// Tab switching
this._initTabs();
// Table header sort
this._initTableSort();
}
// ========================================================================
// DATASET
// ========================================================================
/**
* @private
* @param {string} key
*/
_loadDataset(key) {
this._dataset = PERCEPTRON_DATASETS[key];
if (!this._dataset) return;
const isGenerator = this._dataset.isGenerated === true;
this._renderTable();
this._viz.setDataset(this._dataset);
this._updateTableTitle();
this._toggleGaussianConfig(isGenerator);
this._debugLog('debug', 'Dataset loaded', {
name: this._dataset.name,
points: this._dataset.points.length,
});
}
/**
* Updates the table panel header with dataset name/description.
* @private
*/
_updateTableTitle() {
if (!this._dom.tableTitle || !this._dataset) return;
const ds = this._dataset;
this._dom.tableTitle.textContent =
`📊 ${ds.icon || ''} ${ds.name}`;
}
/** @private */
_renderTable() {
const tbody = this._dom.tableBody;
if (!tbody || !this._dataset) return;
const ds = this._dataset;
tbody.innerHTML = '';
// Update header if feature names changed
const thead = tbody.closest('table')?.querySelector('thead');
if (thead) {
const fn = ds.featureNames || ds.axisLabels;
thead.innerHTML = `<tr>
<th data-sort="idx">#</th>
<th data-sort="x">${fn.x}</th>
<th data-sort="y">${fn.y}</th>
<th data-sort="label">Label</th>
<th data-sort="pred">ŷ</th>
</tr>`;
this._initTableSort();
}
// Get sorted indices
const indices = ds.points.map((_, i) => i);
if (this._sortCol) {
indices.sort((a, b) => {
const ptA = ds.points[a];
const ptB = ds.points[b];
let va, vb;
switch (this._sortCol) {
case 'idx': va = a; vb = b; break;
case 'x': va = ptA.x; vb = ptB.x; break;
case 'y': va = ptA.y; vb = ptB.y; break;
case 'label': va = ptA.label; vb = ptB.label; break;
default: va = a; vb = b;
}
return this._sortAsc ? va - vb : vb - va;
});
}
indices.forEach((i) => {
const pt = ds.points[i];
const tr = document.createElement('tr');
tr.setAttribute('data-idx', i);
const classLabel = ds.classLabels[pt.label];
const cssClass = pt.label === 0 ? 'class-0' : 'class-1';
tr.innerHTML = `
<td>${i + 1}</td>
<td>${pt.x.toFixed(1)}</td>
<td>${pt.y.toFixed(1)}</td>
<td><span class="label-badge ${cssClass}">${classLabel}</span></td>
<td class="pred-cell">—</td>
`;
tbody.appendChild(tr);
});
}
/**
* Updates prediction column and row highlights.
* @private
*/
_updateTablePredictions() {
const tbody = this._dom.tableBody;
if (!tbody || !this._neuron || !this._dataset) return;
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx'), 10);
if (isNaN(idx)) return;
const pt = this._dataset.points[idx];
if (!pt) return;
const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
const predCell = row.querySelector('.pred-cell');
const isCorrect = pred === pt.label;
if (predCell) {
const predLabel = this._dataset.classLabels[pred] || String(pred);
predCell.innerHTML = `<span class="pred-badge ${isCorrect ? 'correct' : 'wrong'}">${predLabel}</span>`;
}
// Row classification highlight
row.classList.toggle('row-error', !isCorrect);
row.classList.toggle('row-correct', isCorrect && this._epoch > 0);
// Selected row
row.classList.toggle('row-selected', idx === this._selectedPointIdx);
// Auto-scroll to selected row
if (idx === this._selectedPointIdx && this._selectedPointIdx >= 0) {
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
}
// ========================================================================
// NEURON
// ========================================================================
/** @private */
_resetNeuron() {
this._stopTraining();
this._neuron = new Neuron(2, 0, { initMethod: 'random', initScale: NN_CONSTANTS.PERCEPTRON_INIT_SCALE });
this._epoch = 0;
this._currentPhase = TRAIN_PHASE.IDLE;
this._selectedPointIdx = -1;
this._prevWeights = null;
// Compute initial error count (random weights usually misclassify some)
this._computeErrorCount();
this._updateViz();
this._updateStats();
this._updateTablePredictions();
this._viz.clearErrors();
this._viz.clearErrorHistory();
this._viz.highlightSelectedPoint(-1);
this._clearLearningRuleDisplay();
this._updateComments();
}
/**
* Counts current misclassified points and updates _totalError.
* @private
* @returns {number} Number of errors
*/
_computeErrorCount() {
if (!this._neuron || !this._dataset) return 0;
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const points = this._dataset.points;
let errors = 0;
for (let i = 0; i < points.length; i++) {
const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
if (pred !== points[i].label) errors++;
}
this._totalError = errors;
return errors;
}
/**
* Returns current accuracy in percent.
* @returns {number}
* @private
*/
_currentAccuracy() {
if (!this._dataset || this._dataset.points.length === 0) return 100;
const total = this._dataset.points.length;
return (total - this._totalError) / total * 100;
}
// ========================================================================
// PHASE-BASED STEP TRAINING
// ========================================================================
/**
* Executes one manual step-through phase.
* Called by the Step button or by auto-training.
*
* @param {boolean} [fastMode=false] - Skip visualization phases
*/
manualStep(fastMode = false) {
if (!this._neuron || !this._dataset) return;
if (fastMode) {
this._executeFullStep();
return;
}
// Advance through phases
switch (this._currentPhase) {
case TRAIN_PHASE.IDLE:
case TRAIN_PHASE.RETRAIN_VIZ:
this._phaseSelectPoint();
break;
case TRAIN_PHASE.SELECT_POINT:
this._phaseShowW1Update();
break;
case TRAIN_PHASE.UPDATE_W1:
this._phaseShowW2Update();
break;
case TRAIN_PHASE.UPDATE_W2:
this._phaseApplyWeights();
break;
case TRAIN_PHASE.APPLY_WEIGHTS:
this._phaseRetrainViz();
break;
default:
this._phaseSelectPoint();
}
}
/**
* Phase 1: Select a random misclassified point.
* @private
*/
_phaseSelectPoint() {
this._currentPhase = TRAIN_PHASE.SELECT_POINT;
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const points = this._dataset.points;
// Find all misclassified
const misclassified = [];
for (let i = 0; i < points.length; i++) {
const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
if (pred !== points[i].label) {
misclassified.push(i);
}
}
if (misclassified.length === 0) {
// Converged!
this._epoch++;
this._totalError = 0;
this._updateStats();
this._viz.pushErrorHistory(0);
this._showLearningRuleText(
`<div class="rule-phase-title">✅ Konvergiert!</div>` +
`<div class="rule-explanation">Alle ${points.length} Punkte werden korrekt klassifiziert.</div>`
);
this._currentPhase = TRAIN_PHASE.IDLE;
return;
}
// Check accuracy threshold (< 100%)
if (this._accuracyThreshold < 100) {
const accuracy = (points.length - misclassified.length) / points.length * 100;
if (accuracy >= this._accuracyThreshold) {
this._epoch++;
this._totalError = misclassified.length;
this._updateStats();
this._viz.pushErrorHistory(misclassified.length);
this._showLearningRuleText(
`<div class="rule-phase-title">✅ Zielgenauigkeit erreicht!</div>` +
`<div class="rule-explanation">Genauigkeit ${accuracy.toFixed(1)}% ≥ Ziel ${this._accuracyThreshold}% ` +
`(${misclassified.length} von ${points.length} falsch).</div>`
);
this._currentPhase = TRAIN_PHASE.IDLE;
return;
}
}
// Random selection
this._selectedPointIdx = misclassified[Math.floor(Math.random() * misclassified.length)];
const pt = points[this._selectedPointIdx];
const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
this._currentError = pt.label - pred;
// Save old weights
this._prevWeights = {
w1: this._neuron.weights[0],
w2: this._neuron.weights[1],
bias: this._neuron.bias,
};
// Visual updates
this._viz.highlightSelectedPoint(this._selectedPointIdx);
this._viz.updateArchInputValues(pt.x, pt.y, null);
this._updateTablePredictions();
const fl = this._featureLabel;
this._showLearningRuleText(
`<div class="rule-phase-title">Phase 1 — Punkt auswählen</div>` +
`<div class="rule-explanation">Ein <strong>falsch klassifizierter</strong> Punkt wird zufällig gewählt.</div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">Punkt</td><td>#${this._selectedPointIdx + 1} (${pt.x.toFixed(1)}, ${pt.y.toFixed(1)})</td></tr>` +
`<tr><td class="rule-label">Soll (y)</td><td>${pt.label}</td></tr>` +
`<tr><td class="rule-label">Ist (ŷ)</td><td>${pred}</td></tr>` +
`<tr><td class="rule-label">Fehler δ</td><td>${pt.label} − ${pred} = <span class="rule-part part-delta">${this._currentError}</span></td></tr>` +
`</table>` +
`<div class="rule-hint">δ > 0 → Gewichte müssen <em>erhöht</em> werden<br>δ < 0 → Gewichte müssen <em>verringert</em> werden</div>`
);
this._updatePhaseIndicator(1);
}
/**
* Phase 2: Show w₁ update formula.
* @private
*/
_phaseShowW1Update() {
this._currentPhase = TRAIN_PHASE.UPDATE_W1;
const pt = this._dataset.points[this._selectedPointIdx];
const lr = this._learningRate;
const delta = this._currentError;
const oldW1 = this._prevWeights.w1;
const dw1 = lr * delta * pt.x;
const newW1 = oldW1 + dw1;
// Compute sum for architecture display
const sum = oldW1 * pt.x + this._prevWeights.w2 * pt.y + this._prevWeights.bias;
this._viz.updateArchInputValues(pt.x, pt.y, sum);
const fLabel = this._featureLabel('x');
this._showLearningRuleText(
`<div class="rule-phase-title">Phase 2 — Gewicht w₁ berechnen</div>` +
`<div class="rule-explanation">Die Delta-Regel: <code>w₁<sup>neu</sup> = w₁<sup>alt</sup> + η · δ · x₁</code></div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">Lernrate η</td><td><span class="rule-part part-eta">${lr}</span></td></tr>` +
`<tr><td class="rule-label">Fehler δ</td><td><span class="rule-part part-delta">${delta}</span></td></tr>` +
`<tr><td class="rule-label">${fLabel}</td><td><span class="rule-part part-input">${pt.x.toFixed(1)}</span></td></tr>` +
`<tr><td class="rule-label">Δw₁</td><td>${lr} · ${delta} · ${pt.x.toFixed(1)} = <strong>${dw1.toFixed(4)}</strong></td></tr>` +
`</table>` +
`<div class="rule-weight-transition">` +
`w₁: <span class="weight-old">${oldW1.toFixed(3)}</span> → <span class="weight-new">${newW1.toFixed(3)}</span>` +
`</div>`
);
this._updatePhaseIndicator(2);
}
/**
* Phase 3: Show w₂ and bias update formula.
* @private
*/
_phaseShowW2Update() {
this._currentPhase = TRAIN_PHASE.UPDATE_W2;
const pt = this._dataset.points[this._selectedPointIdx];
const lr = this._learningRate;
const delta = this._currentError;
const oldW2 = this._prevWeights.w2;
const dw2 = lr * delta * pt.y;
const newW2 = oldW2 + dw2;
const oldBias = this._prevWeights.bias;
const db = lr * delta;
const newBias = oldBias + db;
const fLabel = this._featureLabel('y');
this._showLearningRuleText(
`<div class="rule-phase-title">Phase 3 — Gewicht w₂ & Bias</div>` +
`<div class="rule-explanation">Gleiche Regel für w₂ und den Bias (x₀ = 1).</div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">Δw₂</td><td>${lr} · ${delta} · ${pt.y.toFixed(1)} = <strong>${dw2.toFixed(4)}</strong></td></tr>` +
`<tr><td class="rule-label">Δb</td><td>${lr} · ${delta} · 1 = <strong>${db.toFixed(4)}</strong></td></tr>` +
`</table>` +
`<div class="rule-weight-transition">` +
`w₂: <span class="weight-old">${oldW2.toFixed(3)}</span> → <span class="weight-new">${newW2.toFixed(3)}</span><br>` +
`b: <span class="weight-old">${oldBias.toFixed(3)}</span> → <span class="weight-new">${newBias.toFixed(3)}</span>` +
`</div>`
);
this._updatePhaseIndicator(3);
}
/**
* Phase 4: Apply the weight updates.
* @private
*/
_phaseApplyWeights() {
this._currentPhase = TRAIN_PHASE.APPLY_WEIGHTS;
const pt = this._dataset.points[this._selectedPointIdx];
const lr = this._learningRate;
const delta = this._currentError;
// Apply
this._neuron.weights[0] += lr * delta * pt.x;
this._neuron.weights[1] += lr * delta * pt.y;
this._neuron.bias += lr * delta;
// Show old → new in arch diagram
this._viz.updateArchWeights(
this._neuron.weights[0],
this._neuron.weights[1],
this._neuron.bias,
{ oldW1: this._prevWeights.w1, oldW2: this._prevWeights.w2, oldBias: this._prevWeights.bias }
);
this._showLearningRuleText(
`<div class="rule-phase-title">Phase 4 — Gewichte anwenden</div>` +
`<div class="rule-explanation">Die berechneten Änderungen werden auf das Neuron übertragen.</div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">w₁</td><td><span class="weight-old">${this._prevWeights.w1.toFixed(3)}</span> → <span class="weight-new">${this._neuron.weights[0].toFixed(3)}</span></td></tr>` +
`<tr><td class="rule-label">w₂</td><td><span class="weight-old">${this._prevWeights.w2.toFixed(3)}</span> → <span class="weight-new">${this._neuron.weights[1].toFixed(3)}</span></td></tr>` +
`<tr><td class="rule-label">b</td><td><span class="weight-old">${this._prevWeights.bias.toFixed(3)}</span> → <span class="weight-new">${this._neuron.bias.toFixed(3)}</span></td></tr>` +
`</table>` +
`<div class="rule-hint">Beobachte, wie sich die Decision Boundary im Diagramm verschiebt!</div>`
);
this._updatePhaseIndicator(4);
}
/**
* Phase 5: Update vis after weight change.
* @private
*/
_phaseRetrainViz() {
this._currentPhase = TRAIN_PHASE.RETRAIN_VIZ;
this._epoch++;
this._updateViz();
this._updateTablePredictions();
// Count errors for chart
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const points = this._dataset.points;
const errorIndices = [];
for (let i = 0; i < points.length; i++) {
const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
if (pred !== points[i].label) errorIndices.push(i);
}
this._totalError = errorIndices.length;
this._viz.highlightErrors(errorIndices);
this._viz.pushErrorHistory(errorIndices.length);
this._viz.highlightSelectedPoint(-1);
this._selectedPointIdx = -1;
this._updateStats();
const total = this._dataset.points.length;
const correct = total - errorIndices.length;
const accuracy = (correct / total * 100).toFixed(1);
this._showLearningRuleText(
`<div class="rule-phase-title">Phase 5 — Ergebnis prüfen</div>` +
`<div class="rule-explanation">Das Neuron wird mit allen Datenpunkten erneut getestet.</div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">Epoche</td><td>${this._epoch}</td></tr>` +
`<tr><td class="rule-label">Fehler</td><td>${errorIndices.length} / ${total}</td></tr>` +
`<tr><td class="rule-label">Genauigkeit</td><td>${accuracy}%</td></tr>` +
`</table>` +
(errorIndices.length > 0
? `<div class="rule-hint">Noch Fehler vorhanden → nächste Epoche beginnt mit Phase 1.</div>`
: `<div class="rule-hint rule-success">Perfekt! Alle Punkte korrekt klassifiziert. ✅</div>`)
);
// Reset arch weights to normal display
this._viz.updateArchWeights(
this._neuron.weights[0],
this._neuron.weights[1],
this._neuron.bias
);
this._viz.updateArchInputValues(null, null, null);
this._updatePhaseIndicator(5);
this._updateComments();
this._debugLog('debug', 'Step complete', {
epoch: this._epoch,
errors: errorIndices.length,
w1: this._neuron.weights[0],
w2: this._neuron.weights[1],
bias: this._neuron.bias,
});
}
/**
* Executes a full training step without phase visualization.
* Used for fast auto-training.
* @private
*/
_executeFullStep() {
if (!this._neuron || !this._dataset) return;
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const points = this._dataset.points;
const lr = this._learningRate;
// Find misclassified
const misclassified = [];
for (let i = 0; i < points.length; i++) {
const pred = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
if (pred !== points[i].label) misclassified.push(i);
}
if (misclassified.length === 0) {
this._epoch++;
this._totalError = 0;
this._updateStats();
this._updateViz();
this._viz.pushErrorHistory(0);
return;
}
// Random misclassified point
const idx = misclassified[Math.floor(Math.random() * misclassified.length)];
const pt = points[idx];
const pred = this._neuron.forward(new Float64Array([pt.x, pt.y]), stepFn);
const error = pt.label - pred;
// Delta rule update
this._neuron.weights[0] += lr * error * pt.x;
this._neuron.weights[1] += lr * error * pt.y;
this._neuron.bias += lr * error;
this._epoch++;
// Recount errors
const errorIndices = [];
for (let i = 0; i < points.length; i++) {
const p = this._neuron.forward(new Float64Array([points[i].x, points[i].y]), stepFn);
if (p !== points[i].label) errorIndices.push(i);
}
this._totalError = errorIndices.length;
this._updateViz();
this._updateStats();
this._updateTablePredictions();
this._viz.highlightErrors(errorIndices);
this._viz.pushErrorHistory(errorIndices.length);
this._updateComments();
}
// ========================================================================
// AUTO-TRAINING
// ========================================================================
/** Startet / stoppt das automatische Training. */
toggleTrain() {
if (this._isTraining) {
this._stopTraining();
} else {
this._startTraining();
}
}
/** @private */
_startTraining() {
if (this._isTraining) return;
this._isTraining = true;
if (this._dom.trainBtn) this._dom.trainBtn.textContent = '⏸ Stopp';
if (this._dom.stepBtn) this._dom.stepBtn.disabled = true;
if (this._dom.datasetSelect) this._dom.datasetSelect.disabled = true;
// Compute initial error count so _trainLoop doesn't falsely see 0
this._computeErrorCount();
// Already converged before training even started?
if (this._totalError === 0) {
this._showLearningRuleText('Alle Punkte bereits korrekt klassifiziert! ✅');
this._stopTraining();
return;
}
// Already at accuracy threshold?
if (this._accuracyThreshold < 100 && this._currentAccuracy() >= this._accuracyThreshold) {
this._showLearningRuleText(
`Zielgenauigkeit ${this._accuracyThreshold}% bereits erreicht ` +
`(aktuell: ${this._currentAccuracy().toFixed(1)}%). ✅`
);
this._stopTraining();
return;
}
// Reset phase for auto
this._currentPhase = TRAIN_PHASE.IDLE;
this._trainLoop();
}
/** @private */
_stopTraining() {
this._isTraining = false;
if (this._trainTimerId !== null) {
clearTimeout(this._trainTimerId);
this._trainTimerId = null;
}
if (this._dom.trainBtn) this._dom.trainBtn.textContent = '▶ Trainieren';
if (this._dom.stepBtn) this._dom.stepBtn.disabled = false;
if (this._dom.datasetSelect) this._dom.datasetSelect.disabled = false;
}
/** @private */
_trainLoop() {
if (!this._isTraining) return;
const isFast = this._trainDelay <= this._trainDelayMin * 3;
if (isFast) {
// Fast mode: do multiple full steps per tick
const batchSize = NN_CONSTANTS.PERCEPTRON_FAST_BATCH_SIZE;
for (let i = 0; i < batchSize; i++) {
this._executeFullStep();
if (this._totalError === 0 || this._epoch >= this._maxEpochs
|| this._currentAccuracy() >= this._accuracyThreshold) break;
}
} else {
// Visual mode: step through phases
this.manualStep(false);
}
// Stop conditions
if (this._totalError === 0) {
this._showLearningRuleText(`Konvergiert nach ${this._epoch} Epochen! ✅`);
this._showErrorChart();
this._stopTraining();
return;
}
if (this._accuracyThreshold < 100 && this._currentAccuracy() >= this._accuracyThreshold) {
this._showLearningRuleText(
`Zielgenauigkeit ${this._accuracyThreshold}% erreicht nach ${this._epoch} Epochen ` +
`(aktuell: ${this._currentAccuracy().toFixed(1)}%). ✅`
);
this._showErrorChart();
this._stopTraining();
return;
}
if (this._epoch >= this._maxEpochs) {
this._showLearningRuleText(`Max. Epochen (${this._maxEpochs}) erreicht. Keine Konvergenz. ⚠️`);
this._showErrorChart();
this._stopTraining();
return;
}
const delay = isFast ? this._trainDelayMin : this._trainDelay;
this._trainTimerId = setTimeout(() => this._trainLoop(), delay);
}
// ========================================================================
// VIZ UPDATE
// ========================================================================
/** @private */
_updateViz() {
if (!this._neuron) return;
const w1 = this._neuron.weights[0];
const w2 = this._neuron.weights[1];
const b = this._neuron.bias;
this._viz.updateDecisionLine(w1, w2, b);
this._viz.updateArchWeights(w1, w2, b);
}
/** @private */
_updateStats() {
const d = this._dom;
if (d.epochDisplay) d.epochDisplay.textContent = this._epoch;
if (d.errorDisplay) d.errorDisplay.textContent = this._totalError;
if (d.accuracyDisplay && this._dataset) {
const total = this._dataset.points.length;
const correct = total - this._totalError;
d.accuracyDisplay.textContent = `${(correct / total * 100).toFixed(1)}%`;
}
}
// ========================================================================
// LEARNING RULE DISPLAY
// ========================================================================
/**
* @private
* @param {string} html
*/
_showLearningRuleText(html) {
if (!this._dom.learnRuleDisp) return;
this._dom.learnRuleDisp.innerHTML = `<div class="rule-formula">${html}</div>`;
}
/** @private */
_clearLearningRuleDisplay() {
if (!this._dom.learnRuleDisp) return;
this._dom.learnRuleDisp.innerHTML =
'<div class="rule-formula">' +
'w<sub>i,neu</sub> = w<sub>i,alt</sub> + η · (y − ŷ) · x<sub>i</sub>' +
'</div>';
}
/**
* @private
* @param {number} phase 1-5
*/
_updatePhaseIndicator(phase) {
if (!this._dom.learnRuleDisp) return;
let existing = this._dom.learnRuleDisp.querySelector('.phase-indicator');
if (!existing) {
existing = document.createElement('div');
existing.className = 'phase-indicator';
this._dom.learnRuleDisp.appendChild(existing);
}
let html = '';
for (let i = 1; i <= 5; i++) {
const cls = i < phase ? 'done' : (i === phase ? 'active' : '');
html += `<div class="phase-dot ${cls}" title="Phase ${i}"></div>`;
}
existing.innerHTML = html;
}
/**
* @private
* @param {'x'|'y'} axis
* @returns {string}
*/
_featureLabel(axis) {
if (!this._dataset) return axis === 'x' ? 'x₁' : 'x₂';
if (this._dataset.featureNames) return this._dataset.featureNames[axis];
return this._dataset.axisLabels ? this._dataset.axisLabels[axis] : (axis === 'x' ? 'x₁' : 'x₂');
}
// ========================================================================
// SPEED SLIDER
// ========================================================================
/** @private */
_updateSpeedLabel() {
if (!this._dom.speedLabel) return;
if (this._trainDelay <= this._trainDelayMin * 3) {
this._dom.speedLabel.textContent = 'Turbo';
} else if (this._trainDelay > this._trainDelayMax * 0.7) {
this._dom.speedLabel.textContent = 'Langsam';
} else {
this._dom.speedLabel.textContent = 'Mittel';
}
}
// ========================================================================
// GAUSSIAN GENERATOR
// ========================================================================
/** @private */
_initGaussianControls() {
const panel = this._dom.gaussianConfig;
if (!panel) return;
const btn = panel.querySelector('.btn-generate');
if (btn) {
btn.addEventListener('click', () => this._generateGaussianDataset());
}
// Bind value displays to sliders
panel.querySelectorAll('input[type="range"]').forEach((slider) => {
const valEl = slider.parentElement?.querySelector('.slider-value');
if (valEl) {
slider.addEventListener('input', () => {
valEl.textContent = slider.value;
});
}
});
}
/** @private */
_generateGaussianDataset() {
if (typeof GaussianGenerator === 'undefined') return;
const panel = this._dom.gaussianConfig;
if (!panel) return;
const val = (id) => parseFloat(panel.querySelector(`#${id}`)?.value || '0');
const cluster0 = {
muX: val('g-mu-x0'), muY: val('g-mu-y0'),
sigmaX: val('g-sigma-x0'), sigmaY: val('g-sigma-y0'),
n: parseInt(panel.querySelector('#g-n0')?.value || '10', 10),
label: 0,
};
const cluster1 = {
muX: val('g-mu-x1'), muY: val('g-mu-y1'),
sigmaX: val('g-sigma-x1'), sigmaY: val('g-sigma-y1'),
n: parseInt(panel.querySelector('#g-n1')?.value || '10', 10),
label: 1,
};
const points = GaussianGenerator.generateTwoClusters(cluster0, cluster1);
// Update dataset
const ds = PERCEPTRON_DATASETS.generisch;
ds.points = points;
// Adjust ranges to fit generated data
const allX = points.map((p) => p.x);
const allY = points.map((p) => p.y);
const margin = 1;
ds.xRange = [Math.floor(Math.min(...allX) - margin), Math.ceil(Math.max(...allX) + margin)];
ds.yRange = [Math.floor(Math.min(...allY) - margin), Math.ceil(Math.max(...allY) + margin)];
this._loadDataset('generisch');
this._resetNeuron();
}
/**
* Toggles between table view and gaussian generator config.
* @private
* @param {boolean} show
*/
_toggleGaussianConfig(show) {
const panel = this._dom.gaussianConfig;
if (!panel) return;
panel.classList.toggle('visible', show);
// Hide table wrapper when generator is active, show when not
if (this._dom.tableWrapper) {
this._dom.tableWrapper.classList.toggle('hidden', show);
}
}
// ========================================================================
// TAB SYSTEM
// ========================================================================
/** @private */
_initTabs() {
if (!this._tabButtons || this._tabButtons.length === 0) return;
this._tabButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const tabId = btn.getAttribute('data-tab');
this._switchTab(tabId);
});
});
}
/**
* Switches to the specified tab.
* @param {string} tabId
*/
_switchTab(tabId) {
if (!this._tabButtons) return;
this._tabButtons.forEach((b) => b.classList.toggle('active', b.getAttribute('data-tab') === tabId));
this._tabContents.forEach((c) => c.classList.toggle('active', c.id === tabId));
}
/**
* Shows the error chart tab (used automatically after convergence).
* @private
*/
_showErrorChart() {
this._switchTab('tab-error');
}
// ========================================================================
// TABLE SORT
// ========================================================================
/** @private */
_initTableSort() {
const thead = this._dom.tableBody?.closest('table')?.querySelector('thead');
if (!thead) return;
thead.querySelectorAll('th[data-sort]').forEach((th) => {
th.style.cursor = 'pointer';
th.addEventListener('click', () => {
const col = th.getAttribute('data-sort');
if (this._sortCol === col) {
this._sortAsc = !this._sortAsc;
} else {
this._sortCol = col;
this._sortAsc = true;
}
// Update sort indicators
thead.querySelectorAll('th[data-sort]').forEach((h) => {
h.classList.remove('sort-asc', 'sort-desc');
});
th.classList.add(this._sortAsc ? 'sort-asc' : 'sort-desc');
this._renderTable();
this._updateTablePredictions();
});
});
}
// ========================================================================
// COMMENTS
// ========================================================================
/**
* Updates the comments tab with contextual information.
* @private
*/
_updateComments() {
const el = this._dom.commentsDisplay;
if (!el) return;
const comments = [];
// Dataset info
if (this._dataset) {
const ds = this._dataset;
const c0 = ds.points.filter((p) => p.label === 0).length;
const c1 = ds.points.filter((p) => p.label === 1).length;
comments.push({
title: `Datensatz: ${ds.name}`,
text: `${ds.points.length} Datenpunkte (${c0}× ${ds.classLabels[0]}, ${c1}× ${ds.classLabels[1]}).`,
type: 'info'
});
}
// Training status
if (this._epoch > 0 && this._neuron) {
const accuracy = this._currentAccuracy().toFixed(1);
const w1 = this._neuron.weights[0];
const w2 = this._neuron.weights[1];
const b = this._neuron.bias;
comments.push({
title: `Epoche ${this._epoch}`,
text: `Genauigkeit: ${accuracy}%, Fehler: ${this._totalError}. ` +
`Aktuelle Gewichte: w₁=${w1.toFixed(3)}, w₂=${w2.toFixed(3)}, b=${b.toFixed(3)}.`,
type: this._totalError === 0 ? 'success' : 'info'
});
// Check if decision line is in visible area
if (Math.abs(w2) > 1e-10) {
const slope = -(w1 / w2);
const intercept = -(b / w2);
const [xMin, xMax] = this._dataset.xRange;
const [yMin, yMax] = this._dataset.yRange;
const yAtXmin = slope * xMin + intercept;
const yAtXmax = slope * xMax + intercept;
const lineInView = (yAtXmin >= yMin && yAtXmin <= yMax) || (yAtXmax >= yMin && yAtXmax <= yMax) || (yAtXmin < yMin && yAtXmax > yMax) || (yAtXmin > yMax && yAtXmax < yMin);
if (!lineInView) {
comments.push({
title: '⚠️ Decision Boundary nicht sichtbar',
text: 'Die Entscheidungslinie liegt derzeit außerhalb des sichtbaren Bereichs. Das Neuron hat noch keine guten Gewichte gefunden.',
type: 'warning'
});
}
} else if (Math.abs(w1) > 1e-10) {
const xConst = -b / w1;
const [xMin, xMax] = this._dataset.xRange;
if (xConst < xMin || xConst > xMax) {
comments.push({
title: '⚠️ Decision Boundary nicht sichtbar',
text: 'Die vertikale Entscheidungslinie liegt außerhalb des sichtbaren Bereichs.',
type: 'warning'
});
}
}
// Convergence comment
if (this._totalError === 0) {
comments.push({
title: '✅ Konvergiert!',
text: `Das Perceptron klassifiziert alle ${this._dataset.points.length} Punkte korrekt nach ${this._epoch} Epochen.`,
type: 'success'
});
}
}
// Render comments
let html = '';
if (comments.length === 0) {
html = '<div class="comment-hint">Hier erscheinen Erklärungen zum aktuellen Trainingsfortschritt.</div>';
} else {
html = comments.map((c) =>
`<div class="comment-item ${c.type === 'warning' ? 'comment-warning' : c.type === 'success' ? 'comment-success' : ''}">` +
`<div class="comment-title">${c.title}</div>` +
`<div>${c.text}</div>` +
`</div>`
).join('');
}
el.innerHTML = `<div class="comment-section">${html}</div>`;
}
// ========================================================================
// MANUAL INPUT (from architecture diagram)
// ========================================================================
/**
* Handles manual input values entered in the architecture diagram neurons.
* Computes forward pass and displays result.
* @private
* @param {number} x1
* @param {number} x2
*/
_handleManualInput(x1, x2) {
if (!this._neuron) return;
const stepFn = ACTIVATION_FUNCTIONS.step.fn;
const sum = this._neuron.weights[0] * x1 + this._neuron.weights[1] * x2 + this._neuron.bias;
const pred = this._neuron.forward(new Float64Array([x1, x2]), stepFn);
this._viz.updateArchInputValues(x1, x2, sum);
this._showLearningRuleText(
`<div class="rule-phase-title">🔎 Manuelle Eingabe</div>` +
`<div class="rule-explanation">Vorwärtsdurchlauf mit deinen Werten:</div>` +
`<table class="rule-detail-table">` +
`<tr><td class="rule-label">x₁</td><td>${x1.toFixed(1)}</td></tr>` +
`<tr><td class="rule-label">x₂</td><td>${x2.toFixed(1)}</td></tr>` +
`<tr><td class="rule-label">Σ</td><td>${this._neuron.weights[0].toFixed(3)}·${x1.toFixed(1)} + ${this._neuron.weights[1].toFixed(3)}·${x2.toFixed(1)} + ${this._neuron.bias.toFixed(3)} = <strong>${sum.toFixed(3)}</strong></td></tr>` +
`<tr><td class="rule-label">θ(Σ)</td><td>${sum >= 0 ? 'Σ ≥ 0 → 1' : 'Σ < 0 → 0'}</td></tr>` +
`<tr><td class="rule-label">ŷ</td><td><strong>${pred}</strong> (${this._dataset?.classLabels?.[pred] || pred})</td></tr>` +
`</table>`
);
}
// ========================================================================
// FULL RESET
// ========================================================================
/** @private */
_fullReset() {
this._stopTraining();
this._loadDataset(this._currentDatasetKey);
this._resetNeuron();
}
// ========================================================================
// DEBUG LOGGING
// ========================================================================
/**
* @private
* @param {string} level
* @param {string} msg
* @param {Object} [payload]
*/
_debugLog(level, msg, payload) {
if (typeof DebugConfig !== 'undefined' && typeof DEBUG_DOMAINS !== 'undefined') {
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, level, msg, payload);
}
}
}
// ===== Universeller Export =====
(function (root) {
root.PerceptronController = PerceptronController;
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this);