ai/neural/core/neuron.js

/**
 * @fileoverview Neuron — Einzelne Recheneinheit des Neuronalen Netzes.
 * 
 * Ein Neuron hält seine Gewichte, seinen Bias und den letzten Aktivierungswert.
 * Alle numerischen Daten werden in Float64Array gespeichert (V8-optimiert).
 * 
 * Das Neuron bietet Hook-Callbacks, über die Gewichte und Aktivierungen
 * von außen ausgelesen werden können (für IframeBridge-Visualisierung).
 *
 * Architektur: Neuron → Layer → Network (hierarchisch)
 *
 * @author Alexander Wolf
 * @see docs/architecture/NEURAL_NET_ARCHITECTURE.md
 */

/* global DebugConfig, DEBUG_DOMAINS */

/**
 * @typedef {Object} NeuronSnapshot
 * @property {number} index - Index des Neurons im Layer
 * @property {Float64Array} weights - Aktuelle Gewichte
 * @property {number} bias - Aktueller Bias
 * @property {number} output - Letzter Aktivierungswert
 * @property {number} preActivation - Letzter z-Wert (vor Aktivierung)
 * @property {Float64Array|null} gradients - Letzte Gewichts-Gradienten (nach Backprop)
 */

class Neuron {
    /**
     * Erstellt ein neues Neuron mit zufällig initialisierten Gewichten.
     * 
     * @param {number} inputCount - Anzahl der Eingänge (= Gewichte)
     * @param {number} index - Position des Neurons im Layer (für Snapshot/Debug)
     * @param {Object} [options={}]
     * @param {string} [options.initMethod='xavier'] - Initialisierungsmethode: 'xavier', 'he', 'random'
     * @param {number} [options.initScale=1] - Skalierungsfaktor für Gewichte
     */
    constructor(inputCount, index, options = {}) {
        const { initMethod = 'xavier', initScale = 1 } = options;

        /**
         * Index des Neurons im Layer.
         * @type {number}
         */
        this.index = index;

        /**
         * Gewichte zum vorherigen Layer.
         * Pre-allokiert als Float64Array für V8-Optimierung.
         * @type {Float64Array}
         */
        this.weights = new Float64Array(inputCount);

        /**
         * Bias-Wert.
         * @type {number}
         */
        this.bias = 0;

        /**
         * Letzter berechneter Aktivierungswert (Output nach Aktivierungsfunktion).
         * @type {number}
         */
        this.output = 0;

        /**
         * Letzter Pre-Activation-Wert (z = Σ(w·x) + b, VOR der Aktivierungsfunktion).
         * Benötigt für ReLU-Ableitung im Backpropagation-Schritt.
         * @type {number}
         */
        this.preActivation = 0;

        /**
         * Delta-Wert aus dem Backpropagation-Schritt.
         * Wird temporär während des Backward-Pass gespeichert.
         * @type {number}
         */
        this.delta = 0;

        /**
         * Letzte Gewichts-Gradienten (optional, für Visualisierung).
         * @type {Float64Array|null}
         */
        this.gradients = null;

        // Gewichte initialisieren
        this._initializeWeights(inputCount, initMethod, initScale);
    }

    /**
     * Initialisiert Gewichte nach der gewählten Methode.
     * 
     * - Xavier: σ = √(2 / (fan_in + fan_out)) — gut für Sigmoid/Tanh
     * - He:     σ = √(2 / fan_in) — gut für ReLU
     * - Random: Gleichverteilung in [-scale, +scale]
     *
     * @param {number} inputCount - Anzahl der Eingänge (fan_in)
     * @param {string} method - 'xavier', 'he', 'random'
     * @param {number} scale - Skalierungsfaktor
     * @private
     */
    _initializeWeights(inputCount, method, scale) {
        let sigma;

        switch (method) {
            case 'he':
                sigma = Math.sqrt(2 / inputCount) * scale;
                break;
            case 'random':
                sigma = scale;
                break;
            case 'xavier':
            default:
                // Xavier: fan_out ist hier unbekannt, approximieren mit fan_in
                sigma = Math.sqrt(2 / (inputCount + 1)) * scale;
                break;
        }

        for (let i = 0; i < this.weights.length; i++) {
            this.weights[i] = this._gaussianRandom() * sigma;
        }
        this.bias = 0; // Bias startet bei 0 (Standard)
    }

    /**
     * Berechnet den Forward-Pass für dieses Neuron.
     * z = Σ(w_i · x_i) + b
     * output = activation(z)
     *
     * @param {Float64Array|number[]} inputs - Eingabewerte vom vorherigen Layer
     * @param {function(number): number} activationFn - Aktivierungsfunktion
     * @returns {number} Der Aktivierungswert
     */
    forward(inputs, activationFn) {
        let z = this.bias;
        for (let i = 0; i < this.weights.length; i++) {
            z += this.weights[i] * inputs[i];
        }

        this.preActivation = z;
        this.output = activationFn(z);
        return this.output;
    }

    /**
     * Setzt ein einzelnes Gewicht manuell (für Nutzer-Interaktion).
     *
     * @param {number} weightIndex - Index des Gewichts
     * @param {number} value - Neuer Wert
     * @throws {Error} Wenn der Index außerhalb des gültigen Bereichs liegt
     */
    setWeight(weightIndex, value) {
        if (weightIndex < 0 || weightIndex >= this.weights.length) {
            if (typeof DebugConfig !== 'undefined') {
                DebugConfig.log(DEBUG_DOMAINS.AI_NEURAL_NET, 'error',
                    'Invalid weight index:', { weightIndex, maxIndex: this.weights.length - 1 });
            }
            return;
        }
        this.weights[weightIndex] = value;
    }

    /**
     * Setzt den Bias manuell (für Nutzer-Interaktion).
     *
     * @param {number} value - Neuer Bias-Wert
     */
    setBias(value) {
        this.bias = value;
    }

    /**
     * Erstellt einen Snapshot des aktuellen Zustands.
     * Nutzt Kopien, um Immutabilität zu gewährleisten (Convention §5).
     *
     * @returns {NeuronSnapshot}
     */
    getSnapshot() {
        return {
            index: this.index,
            weights: new Float64Array(this.weights),
            bias: this.bias,
            output: this.output,
            preActivation: this.preActivation,
            gradients: this.gradients ? new Float64Array(this.gradients) : null
        };
    }

    /**
     * Gausssche Zufallszahl via Box-Muller-Transformation.
     * Erzeugt normalverteilte Werte mit μ=0, σ=1.
     *
     * @returns {number} Normalverteilter Zufallswert
     * @private
     */
    _gaussianRandom() {
        let u1, u2;
        do {
            u1 = Math.random();
        } while (u1 === 0); // Verhindere log(0)
        u2 = Math.random();
        return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
    }
}

// Export für globalen Zugriff (Window UND Worker-Kontext)
(function(root) {
    root.Neuron = Neuron;
})(typeof self !== 'undefined' ? self : typeof window !== 'undefined' ? window : this);