ai/neural/training/activation-functions.js

/**
 * @fileoverview Aktivierungsfunktionen für das Neuronale Netz.
 * 
 * Jede Funktion ist als Objekt mit `fn` (Vorwärts) und `derivative` (Ableitung)
 * implementiert, um im Backpropagation-Schritt direkt die Ableitung berechnen zu können.
 * 
 * Unterstützte Funktionen: Sigmoid, ReLU, Tanh, Linear, Softmax.
 * Softmax wird separat behandelt, da sie über den gesamten Vektor operiert.
 *
 * @author Alexander Wolf
 * @see docs/architecture/NEURAL_NET_ARCHITECTURE.md
 */

/* global DebugConfig, DEBUG_DOMAINS */

/**
 * @typedef {Object} ActivationFunction
 * @property {string} name - Name der Funktion
 * @property {function(number): number} fn - Vorwärtsberechnung (Skalar)
 * @property {function(number, number=): number} derivative - Ableitung; erwartet Output und optional preActivation (z.B. ReLU)
 */

/**
 * Registry aller verfügbaren Aktivierungsfunktionen.
 * Zugriff über `ACTIVATION_FUNCTIONS[name]`.
 *
 * @type {Object<string, ActivationFunction>}
 */
const ACTIVATION_FUNCTIONS = {

    /**
     * Sigmoid: σ(x) = 1 / (1 + e^(-x))
     * Ausgabe: (0, 1) — ideal für binäre Klassifikation.
     * Achtung: Vanishing Gradient bei großen |x|.
     */
    sigmoid: {
        name: 'sigmoid',
        /**
         * @param {number} x - Eingabewert
         * @returns {number} σ(x) ∈ (0, 1)
         */
        fn(x) {
            // Numerisch stabil: Vermeide Overflow bei großem negativem x
            if (x >= 0) {
                const ex = Math.exp(-x);
                return 1 / (1 + ex);
            }
            const ex = Math.exp(x);
            return ex / (1 + ex);
        },
        /**
         * Ableitung: σ'(x) = σ(x) · (1 - σ(x))
         * Hinweis: Erwartet den BEREITS BERECHNETEN Output σ(x), nicht x selbst.
         * @param {number} output - Bereits berechneter σ(x)
         * @returns {number}
         */
        derivative(output) {
            return output * (1 - output);
        }
    },

    /**
     * ReLU: f(x) = max(0, x)
     * Standard für Hidden-Layers. Kein Vanishing Gradient für x > 0.
     */
    relu: {
        name: 'relu',
        /**
         * @param {number} x
         * @returns {number} max(0, x)
         */
        fn(x) {
            return x > 0 ? x : 0;
        },
        /**
         * Ableitung: 1 wenn x > 0, sonst 0 (bei x=0 definieren wir 0)
         * Hinweis: Benötigt den PRE-ACTIVATION Wert (z), nicht den Output.
         * @param {number} _output - Nicht verwendet bei ReLU
         * @param {number} preActivation - Der z-Wert vor Aktivierung
         * @returns {number}
         */
        derivative(_output, preActivation) {
            return preActivation > 0 ? 1 : 0;
        }
    },

    /**
     * Tanh: f(x) = (e^x - e^(-x)) / (e^x + e^(-x))
     * Ausgabe: (-1, 1) — zentriert um 0, besser als Sigmoid für Hidden-Layers.
     */
    tanh: {
        name: 'tanh',
        /**
         * @param {number} x
         * @returns {number} tanh(x) ∈ (-1, 1)
         */
        fn(x) {
            return Math.tanh(x);
        },
        /**
         * Ableitung: 1 - tanh²(x)
         * @param {number} output - Bereits berechneter tanh(x)
         * @returns {number}
         */
        derivative(output) {
            return 1 - output * output;
        }
    },

    /**
     * Linear (Identity): f(x) = x
     * Genutzt als Output-Aktivierung für Regression.
     */
    linear: {
        name: 'linear',
        /**
         * @param {number} x
         * @returns {number} x
         */
        fn(x) {
            return x;
        },
        /**
         * Ableitung: immer 1
         * @returns {number}
         */
        derivative() {
            return 1;
        }
    }
};

/**
 * Softmax-Funktion für Vektoren (gesamter Layer).
 * Wird separat behandelt, da sie über alle Neuronen eines Layers operiert
 * und nicht pro Neuron angewendet werden kann.
 *
 * @param {Float64Array|number[]} values - Rohwerte (logits) des Output-Layers
 * @returns {Float64Array} Wahrscheinlichkeitsverteilung (Summe = 1)
 */
function softmax(values) {
    const result = new Float64Array(values.length);

    // Numerische Stabilität: Subtrahiere Maximum
    let maxVal = -Infinity;
    for (let i = 0; i < values.length; i++) {
        if (values[i] > maxVal) maxVal = values[i];
    }

    let sumExp = 0;
    for (let i = 0; i < values.length; i++) {
        result[i] = Math.exp(values[i] - maxVal);
        sumExp += result[i];
    }

    for (let i = 0; i < values.length; i++) {
        result[i] /= sumExp;
    }

    return result;
}

/**
 * Maskiert illegale Züge und wendet Softmax an.
 * Besetzte Felder erhalten -∞ vor dem Softmax, damit ihre Wahrscheinlichkeit ≈ 0 wird.
 *
 * @param {Float64Array|number[]} rawOutput - Rohe Netzwerk-Ausgabe
 * @param {number[]} mask - Maske: 1 = erlaubt, 0 = blockiert
 * @returns {Float64Array} Wahrscheinlichkeitsverteilung über legale Aktionen
 */
function maskedSoftmax(rawOutput, mask) {
    const masked = new Float64Array(rawOutput.length);

    for (let i = 0; i < rawOutput.length; i++) {
        masked[i] = mask[i] ? rawOutput[i] : -1e9;
    }

    return softmax(masked);
}

/**
 * Heaviside Step Function: f(x) = 1 wenn x ≥ 0, sonst 0.
 * Klassische Perzeptron-Aktivierung (nicht-differenzierbar bei x=0).
 * Für das Perzeptron-Training wird stattdessen die Delta-Regel verwendet.
 */
const STEP_ACTIVATION = {
    name: 'step',
    /**
     * @param {number} x
     * @returns {number} 0 oder 1
     */
    fn(x) {
        return x >= 0 ? 1 : 0;
    },
    /**
     * Pseudo-Ableitung: 1 (überall), da Step nicht differenzierbar ist.
     * Wird bei der Delta-Regel nicht benötigt, aber für Konsistenz bereitgestellt.
     * @returns {number}
     */
    derivative() {
        return 1;
    }
};
ACTIVATION_FUNCTIONS.step = STEP_ACTIVATION;

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