/**
* @fileoverview NeuralNetwork — Zentrales Netzwerk, das Layers verwaltet.
*
* Verantwortlich für:
* - Aufbau der Netzwerk-Topologie (Neuron → Layer → Network)
* - Forward-Pass durch alle Schichten
* - Backward-Pass (Backpropagation) mit Gradient-Descent
* - Snapshot-Erstellung für Live-Visualisierung über IframeBridge
* - Manuelle Manipulation von Gewichten/Biases durch den Nutzer
*
* KEINE DOM-Bezüge! Diese Klasse läuft sowohl im Main-Thread als auch im Web Worker.
* Die Kommunikation mit der View erfolgt ausschließlich über Snapshots, die der
* NNOrchestrator via IframeBridge weiterleitet.
*
* @author Alexander Wolf
* @see docs/architecture/NEURAL_NET_ARCHITECTURE.md
*/
/* global Layer, ACTIVATION_FUNCTIONS, LOSS_FUNCTIONS, softmax, maskedSoftmax,
DebugConfig, DEBUG_DOMAINS */
/**
* @typedef {Object} NetworkTopology
* @property {number[]} layers - Neuronen pro Layer [inputSize, hidden1, ..., outputSize]
* @property {string[]} [activations] - Aktivierungsfunktion pro Layer (ohne Input-Layer)
* @property {string} [outputActivation='softmax'] - Aktivierung des Output-Layers
* @property {string} [lossFunction='crossEntropy'] - Verlustfunktion
* @property {number} [learningRate=0.01] - Lernrate
* @property {string} [initMethod='xavier'] - Gewichts-Initialisierung
*/
/**
* @typedef {Object} NetworkSnapshot
* @property {number[]} topology - Layer-Größen [input, h1, ..., output]
* @property {string[]} activations - Aktivierungsfunktionen pro Layer
* @property {number} learningRate - Aktuelle Lernrate
* @property {number} totalParameters - Gesamtzahl der Parameter
* @property {LayerSnapshot[]} layers - Snapshots aller Layers (ohne Input-Layer)
* @property {number} epoch - Aktuelle Epoche (falls im Training)
* @property {number} loss - Aktueller Verlust
*/
/**
* @typedef {Object} TrainingSample
* @property {Float64Array|number[]} input - Eingabedaten
* @property {Float64Array|number[]} target - Zielwerte (One-Hot oder Regression)
*/
/**
* @typedef {Object} TrainingResult
* @property {number} epoch - Epoche
* @property {number} loss - Durchschnittlicher Verlust
* @property {number} accuracy - Genauigkeit (für Klassifikation)
*/
class NeuralNetwork {
/**
* Erstellt ein neues Neuronales Netz mit der gegebenen Topologie.
*
* @param {NetworkTopology} topology
* @example
* // XOR-Netz: 2 Inputs → 4 Hidden → 1 Output
* const net = new NeuralNetwork({
* layers: [2, 4, 1],
* activations: ['relu', 'sigmoid'],
* lossFunction: 'mse',
* learningRate: 0.1
* });
*
* @example
* // TTT-Netz: 9 Inputs → 36 Hidden → 36 Hidden → 9 Output (Softmax)
* const net = new NeuralNetwork({
* layers: [9, 36, 36, 9],
* activations: ['relu', 'relu', 'softmax'],
* lossFunction: 'crossEntropy',
* learningRate: 0.01
* });
*/
constructor(topology) {
const {
layers: layerSizes,
activations = [],
lossFunction = 'crossEntropy',
learningRate = 0.01,
initMethod = 'xavier'
} = topology;
if (layerSizes.length < 2) {
throw new Error('Netzwerk benötigt mindestens Input und Output Layer');
}
/**
* Topologie als Array der Layer-Größen.
* @type {number[]}
*/
this.topologyDef = [...layerSizes];
/**
* Lernrate für Gradient Descent.
* @type {number}
*/
this.learningRate = learningRate;
/**
* Aktuelle Verlustfunktion.
* @type {LossFunction}
*/
this.lossFunction = LOSS_FUNCTIONS[lossFunction] || LOSS_FUNCTIONS.crossEntropy;
/**
* Name der Verlustfunktion.
* @type {string}
*/
this.lossFunctionName = lossFunction;
/**
* Alle Layers (ohne Input-Layer — der Input-Layer ist kein echtes Layer-Objekt).
* Index 0 = erster Hidden-Layer, letzter Index = Output-Layer.
* @type {Layer[]}
*/
this.layers = [];
/**
* Aktivierungsnamen pro Layer (für Snapshot/Serialisierung).
* @type {string[]}
*/
this.activationNames = [];
/**
* Aktuelle Epoche (Trainingsfortschritt).
* @type {number}
*/
this.epoch = 0;
/**
* Letzter berechneter Verlust.
* @type {number}
*/
this.lastLoss = 0;
/**
* Letzter Input (für Snapshot nach Forward-Pass).
* @type {Float64Array|null}
*/
this.lastInput = null;
// Layers aufbauen
for (let i = 1; i < layerSizes.length; i++) {
const isOutputLayer = (i === layerSizes.length - 1);
const activationName = activations[i - 1] || (isOutputLayer ? 'sigmoid' : 'relu');
const useSoftmax = activationName === 'softmax';
this.activationNames.push(activationName);
this.layers.push(new Layer({
neuronCount: layerSizes[i],
inputCount: layerSizes[i - 1],
activation: useSoftmax ? 'linear' : activationName,
initMethod: initMethod,
useSoftmax: useSoftmax
}, i - 1));
}
this._logCreation();
}
// ========================================================================
// FORWARD PASS
// ========================================================================
/**
* Führt einen Forward-Pass durch das gesamte Netzwerk aus.
* Daten fließen sequenziell durch alle Layers.
*
* @param {Float64Array|number[]} input - Eingabedaten (Größe = topologyDef[0])
* @returns {Float64Array} Output des letzten Layers
*/
forward(input) {
this.lastInput = input instanceof Float64Array ? input : new Float64Array(input);
let currentOutput = this.lastInput;
for (const layer of this.layers) {
currentOutput = layer.forward(currentOutput);
}
return currentOutput;
}
/**
* Forward-Pass mit Maskierung illegaler Züge (für Game-KI).
* Wendet maskedSoftmax auf den Output an.
*
* @param {Float64Array|number[]} input - Eingabedaten
* @param {number[]} legalMask - 1 = erlaubter Zug, 0 = blockiert
* @returns {Float64Array} Maskierte Wahrscheinlichkeitsverteilung
*/
forwardMasked(input, legalMask) {
const rawOutput = this.forward(input);
const outputLayer = this.layers[this.layers.length - 1];
if (outputLayer.useSoftmax) {
// Softmax bereits angewendet, aber ohne Maskierung → neu berechnen
return maskedSoftmax(outputLayer.preActivations, legalMask);
}
return rawOutput;
}
// ========================================================================
// BACKWARD PASS (Backpropagation)
// ========================================================================
/**
* Führt einen Backward-Pass (Backpropagation) durch.
* Berechnet Gradienten und aktualisiert Gewichte mit Gradient Descent.
*
* @param {Float64Array|number[]} target - Zielwerte
* @returns {number} Der berechnete Verlust
*/
backward(target) {
const targetArray = target instanceof Float64Array ? target : new Float64Array(target);
const outputLayer = this.layers[this.layers.length - 1];
const output = outputLayer.outputs;
// 1. Loss berechnen
const loss = this.lossFunction.compute(output, targetArray);
this.lastLoss = loss;
// 2. Output-Layer Gradienten (dL/dz für Output-Neuronen)
const outputGrad = this.lossFunction.derivative(output, targetArray);
// 3. Deltas berechnen — rückwärts durch alle Layers
this._computeDeltas(outputGrad);
// 4. Gewichte aktualisieren
this._updateWeights();
return loss;
}
/**
* Berechnet die Delta-Werte (Fehlerterme) für alle Neuronen.
*
* Für den Output-Layer: δ_k = dL/dz_k (direkt aus der Loss-Ableitung)
* Für Hidden-Layers: δ_j = (Σ δ_k · w_jk) · f'(z_j)
*
* @param {Float64Array} outputGradient - Gradient des Output-Layers
* @private
*/
_computeDeltas(outputGradient) {
// Output-Layer: Delta = Loss-Gradient (bei Softmax+CrossEntropy: predicted - target)
const outputLayer = this.layers[this.layers.length - 1];
for (let k = 0; k < outputLayer.neuronCount; k++) {
if (outputLayer.useSoftmax) {
// Softmax + Cross-Entropy: Vereinfachte Ableitung
outputLayer.neurons[k].delta = outputGradient[k];
} else {
// Andere Aktivierungen: δ = dL/dy · f'(z)
const neuron = outputLayer.neurons[k];
const activationDeriv = outputLayer.activation.derivative(
neuron.output, neuron.preActivation
);
neuron.delta = outputGradient[k] * activationDeriv;
}
}
// Hidden-Layers: Rückwärts propagieren
for (let l = this.layers.length - 2; l >= 0; l--) {
const currentLayer = this.layers[l];
const nextLayer = this.layers[l + 1];
for (let j = 0; j < currentLayer.neuronCount; j++) {
// Summe der gewichteten Deltas aus dem nächsten Layer
let errorSum = 0;
for (let k = 0; k < nextLayer.neuronCount; k++) {
errorSum += nextLayer.neurons[k].delta * nextLayer.neurons[k].weights[j];
}
const neuron = currentLayer.neurons[j];
const activationDeriv = currentLayer.activation.derivative(
neuron.output, neuron.preActivation
);
neuron.delta = errorSum * activationDeriv;
}
}
}
/**
* Aktualisiert alle Gewichte und Biases mit Gradient Descent.
* w_new = w_old - learningRate · δ · input
* b_new = b_old - learningRate · δ
*
* @private
*/
_updateWeights() {
for (let l = 0; l < this.layers.length; l++) {
const layer = this.layers[l];
// Input für diesen Layer: Output des vorherigen Layers (oder Netzwerk-Input)
const prevOutput = l === 0 ? this.lastInput : this.layers[l - 1].outputs;
if (!prevOutput) continue; // lastInput ist null wenn forward() noch nicht aufgerufen wurde
for (let j = 0; j < layer.neuronCount; j++) {
const neuron = layer.neurons[j];
// Gewichte aktualisieren
if (!neuron.gradients) {
neuron.gradients = new Float64Array(neuron.weights.length);
}
for (let i = 0; i < neuron.weights.length; i++) {
const gradient = neuron.delta * prevOutput[i];
neuron.gradients[i] = gradient; // Für Visualisierung speichern
neuron.weights[i] -= this.learningRate * gradient;
}
// Bias aktualisieren
neuron.bias -= this.learningRate * neuron.delta;
}
}
}
// ========================================================================
// TRAINING
// ========================================================================
/**
* Trainiert das Netz mit einem einzelnen Datenpunkt.
*
* @param {Float64Array|number[]} input
* @param {Float64Array|number[]} target
* @returns {number} Verlust für diesen Datenpunkt
*/
trainSingle(input, target) {
this.forward(input);
return this.backward(target);
}
/**
* Trainiert das Netz mit einem Batch von Datenpunkten (eine Epoche).
* Stochastic Gradient Descent: Ein Update pro Sample.
*
* @param {TrainingSample[]} dataset - Array von {input, target} Paaren
* @returns {TrainingResult} Epochen-Statistik
*/
trainEpoch(dataset) {
let totalLoss = 0;
let correct = 0;
// Shuffle (Fisher-Yates) für stochastisches Training
const shuffled = this._shuffleArray(dataset);
for (const sample of shuffled) {
this.forward(sample.input);
const loss = this.backward(sample.target);
totalLoss += loss;
// Accuracy: Argmax des Outputs === Argmax des Targets
const outputLayer = this.layers[this.layers.length - 1];
const predictedIdx = this._argmax(outputLayer.outputs);
const targetIdx = this._argmax(sample.target);
if (predictedIdx === targetIdx) correct++;
}
this.epoch++;
const avgLoss = totalLoss / dataset.length;
const accuracy = correct / dataset.length;
this.lastLoss = avgLoss;
return {
epoch: this.epoch,
loss: avgLoss,
accuracy: accuracy
};
}
// ========================================================================
// SNAPSHOT & INTERAKTION
// ========================================================================
/**
* Erstellt einen vollständigen Snapshot des Netzwerks.
* Dieser wird über die IframeBridge an die Visualisierung gesendet.
*
* Alle Daten werden kopiert (Immutable Pattern, Convention §5).
*
* @returns {NetworkSnapshot}
*/
getSnapshot() {
return {
topology: [...this.topologyDef],
activations: [...this.activationNames],
learningRate: this.learningRate,
totalParameters: this.getTotalParameterCount(),
layers: this.layers.map(l => l.getSnapshot()),
epoch: this.epoch,
loss: this.lastLoss
};
}
/**
* Erstellt einen kompakten Snapshot (nur Gewichte und Outputs, ohne Gradienten).
* Für High-Frequency Updates während des Trainings.
*
* @returns {Object} Kompakter Snapshot
*/
getCompactSnapshot() {
return {
epoch: this.epoch,
loss: this.lastLoss,
layerOutputs: this.layers.map(l => new Float64Array(l.outputs)),
weights: this.layers.map(l =>
l.neurons.map(n => ({
w: new Float64Array(n.weights),
b: n.bias
}))
)
};
}
/**
* Setzt ein einzelnes Gewicht manuell.
*
* @param {number} layerIndex - Index des Layers (0-basiert, ohne Input-Layer)
* @param {number} neuronIndex - Index des Neurons im Layer
* @param {number} weightIndex - Index des Gewichts
* @param {number} value - Neuer Wert
*/
setWeight(layerIndex, neuronIndex, weightIndex, value) {
if (layerIndex < 0 || layerIndex >= this.layers.length) {
this._logError('Invalid layer index', { layerIndex });
return;
}
this.layers[layerIndex].setWeight(neuronIndex, weightIndex, value);
}
/**
* Setzt den Bias eines Neurons manuell.
*
* @param {number} layerIndex
* @param {number} neuronIndex
* @param {number} value
*/
setBias(layerIndex, neuronIndex, value) {
if (layerIndex < 0 || layerIndex >= this.layers.length) {
this._logError('Invalid layer index', { layerIndex });
return;
}
this.layers[layerIndex].setBias(neuronIndex, value);
}
/**
* Setzt die Lernrate dynamisch (für Nutzer-Interaktion via Bridge).
*
* @param {number} rate - Neue Lernrate (z.B. 0.001 bis 1.0)
*/
setLearningRate(rate) {
this.learningRate = Math.max(0.0001, Math.min(10, rate));
}
/**
* Gibt die Gesamtzahl aller Parameter (Gewichte + Biases) zurück.
*
* @returns {number}
*/
getTotalParameterCount() {
return this.layers.reduce((sum, layer) => sum + layer.getParameterCount(), 0);
}
/**
* Serialisiert das Netzwerk in ein JSON-kompatibles Objekt.
* Kann zum Speichern/Laden des trainierten Netzes verwendet werden.
*
* @returns {Object} Serialisiertes Netzwerk
*/
serialize() {
return {
topology: this.topologyDef,
activations: this.activationNames,
learningRate: this.learningRate,
lossFunction: this.lossFunctionName,
epoch: this.epoch,
layers: this.layers.map(layer => ({
neurons: layer.neurons.map(neuron => ({
weights: Array.from(neuron.weights),
bias: neuron.bias
}))
}))
};
}
/**
* Lädt Gewichte aus einem serialisierten Objekt.
*
* @param {Object} data - Serialisiertes Netzwerk (von serialize())
*/
deserialize(data) {
const d = /** @type {any} */ (data);
if (!d || !d.layers) {
this._logError('Invalid deserialization data', { data });
return;
}
for (let l = 0; l < this.layers.length && l < d.layers.length; l++) {
const layerData = d.layers[l];
const layer = this.layers[l];
for (let n = 0; n < layer.neuronCount && n < layerData.neurons.length; n++) {
const neuronData = layerData.neurons[n];
const neuron = layer.neurons[n];
neuron.weights.set(neuronData.weights);
neuron.bias = neuronData.bias;
}
}
this.epoch = d.epoch || 0;
this.learningRate = d.learningRate || this.learningRate;
}
// ========================================================================
// PRIVATE HELPERS
// ========================================================================
/**
* Fisher-Yates Shuffle für stochastisches Training.
* @param {TrainingSample[]} array
* @returns {TrainingSample[]}
* @private
*/
_shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
/**
* Index des größten Werts in einem Array.
* @param {Float64Array|number[]} arr
* @returns {number}
* @private
*/
_argmax(arr) {
let maxIdx = 0;
let maxVal = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > maxVal) {
maxVal = arr[i];
maxIdx = i;
}
}
return maxIdx;
}
/**
* @param {string} message
* @param {Object} payload
* @private
*/
_logError(message, payload) {
if (typeof DebugConfig !== 'undefined') {
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'error', message, payload);
}
}
/**
* Loggt die Netzwerk-Erstellung.
* @private
*/
_logCreation() {
if (typeof DebugConfig !== 'undefined') {
DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'debug',
'NeuralNetwork created:', {
topology: this.topologyDef,
activations: this.activationNames,
totalParams: this.getTotalParameterCount(),
learningRate: this.learningRate,
lossFunction: this.lossFunctionName
});
}
}
}
// Export für globalen Zugriff (Window UND Worker-Kontext)
(function(root) {
root.NeuralNetwork = NeuralNetwork;
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this);