Source: mlp.js

/**
 * @class La clase Matrix soporta operaciones fundamentales.
 */
class Matrix {
    constructor(rows, cols, values = 0) {
        this.rows = rows || 0;
        this.cols = cols || 0;
        if (values instanceof Array) {
            this.data = values.slice();
        } else if (values === "RANDOM") {
            this.data = Array(this.rows * this.cols).fill().map(() => Math.random() * 2 - 1);
        } else {
            this.data = Array(this.rows * this.cols).fill(values);
        }
    }
    /**
     * @description Realiza una multiplicación de matrices.
     * @param {Matrix} b Representa una matriz.
     * @returns {Matrix}
    */
    multiply(b) {
        if (this.cols !== b.rows) {
            console.error(`Error: Matrices no alineadas para la multiplicación. Dimensiones de la primera matriz: ${this.rows}x${this.cols}, dimensiones de la segunda matriz: ${b.rows}x${b.cols}`);
            console.log('Primera matriz:', this);
            console.log('Segunda matriz:', b);
            return null;
        }

        let result = new Matrix(this.rows, b.cols);

        for (let i = 0; i < this.rows; i++) {
            for (let j = 0; j < b.cols; j++) {
                let s = 0;
                for (let k = 0; k < this.cols; k++) {
                    s += this.data[i * this.cols + k] * b.data[k * b.cols + j];
                }
                result.data[i * result.cols + j] = s;
            }
        }
        return result;
    }
    /**
     * @description Regresa la transpuesta de una matriz.
     * @returns {Matrix}
    */
    transpose() {
        let result = new Matrix(this.cols, this.rows);
        for (let i = 0; i < this.rows; i++) {
            for (let j = 0; j < this.cols; j++) {
                result.data[j * this.rows + i] = this.data[i * this.cols + j];
            }
        }
        return result;
    }
    /**
     * @description Realiza una suma de matrices.
     * @param {Matrix} a Representa una matriz.
    */
    add(a) {
        if (this.rows !== a.rows || this.cols !== a.cols) {
            console.error(`Error: Matrices de diferentes dimensiones para la adición. Dimensiones de esta matriz: ${this.rows}x${this.cols}, dimensiones de la matriz a: ${a.rows}x${a.cols}`);
            return null;
        }
        for (let i = 0; i < this.data.length; i++) {
            this.data[i] += a.data[i];
        }
    }
    /**
     * @description Realiza resta de matrices.
     * @param {Matrix} a Representa una matriz.
    */
    subtract(a) {
        if (this.rows !== a.rows || this.cols !== a.cols) {
            console.error(`Error: Matrices de diferentes dimensiones para la sustracción. Dimensiones de esta matriz: ${this.rows}x${this.cols}, dimensiones de la matriz a: ${a.rows}x${a.cols}`);
            return null;
        }
        for (let i = 0; i < this.data.length; i++) {
            this.data[i] -= a.data[i];
        }
    }
    /**
     * @description Realiza un escalamiento de una matriz.
     * @param {Matrix} a Representa un escalar.
    */
    scalar(a) {
        for (let i = 0; i < this.data.length; i++) {
            this.data[i] *= a;
        }
    }
    /**
     * @description Producto de Hadamard que se hace elemento a elemento entre dos matrices del mismo tamaño.
     * @param {Matrix} b Representa una matriz.
     * @returns {Matrix}
    */
    hadamard(a) {
        if (this.rows !== a.rows || this.cols !== a.cols) {
            console.error(`Error: Matrices de diferentes dimensiones para el producto Hadamard. Dimensiones de esta matriz: ${this.rows}x${this.cols}, dimensiones de la matriz a: ${a.rows}x${a.cols}`);
            return null;
        }
        for (let i = 0; i < this.data.length; i++) {
            this.data[i] *= a.data[i];
        }
    }
    /**
     * @description Copia la información en una nueva matriz
     * @returns {Matrix}
    */
    copy() {
        return new Matrix(this.rows, this.cols, this.data);
    }
    /**
     * @description Aplica una función a cada elemento de la matriz.
     * @param {Function} func Representa una función.
    */
    foreach(func) {
        for (let i = 0; i < this.data.length; i++) {
            this.data[i] = func(this.data[i]);
        }
    }
}
/**
 * @class La clase MLP implementa un Perceptrón Multicapa.
 */
class MLP {
    constructor(inputSize = 4, hiddenSize = 4, outputSize = 3, learningRate = 0.1, iterations = 30) {
        console.log(`Inicializando MLP con inputSize: ${inputSize}, hiddenSize: ${hiddenSize}, outputSize: ${outputSize}`);
        this.inputsToHidden = new Matrix(hiddenSize, inputSize, "RANDOM");
        this.biasInputsToHidden = new Matrix(hiddenSize, 1, "RANDOM");
        this.hiddenToOutputs = new Matrix(outputSize, hiddenSize, "RANDOM");
        this.biasHiddenToOutputs = new Matrix(outputSize, 1, "RANDOM");
        this.lr = learningRate;
        this.it = iterations;
        this.activation = this.sigmoid;
        this.dActivation = this.dSigmoid;
        this.labelMap = {};
    }
    /**
     * @description Regresa el nombre del modelo usado.
     * @returns {string}
    */
    getClassifierName() {
        return "MLP";
    }
    /**
     * @description Convierte las etiquetas de las clases a formato numérico.
     * @param {Array} labels Las etiquetas de las clases
     * @param {number} numClasses Cantidad de clases.
     * @returns {Array}
    */
    static oneHotEncode(labels, numClasses) {
        return labels.map(label => {
            const oneHot = new Array(numClasses).fill(0);
            oneHot[label] = 1;
            return oneHot;
        });
    }
    /**
     * @description Convierte datos a numéricos.
     * @param {Array} data Representa una instancia de la base de datos.
     * @returns {number}
    */
    static convertToNumeric(data) {
        return data.map(row => row.map(value => {
            if (typeof value === 'string') {
                const match = value.match(/[-+]?[0-9]*\.?[0-9]+/g);
                return match ? parseFloat(match[0]) : 0;
            }
            return value;
        }));
    }
    /**
     * @description Crea mapa de etiquetas.
     * @param {Array} labels Representa las etiquetas de la base de datos.
     * @returns {object}
    */
    createLabelMap(labels) {
        const uniqueLabels = [...new Set(labels)];
        this.labelMap = uniqueLabels.reduce((map, label, index) => {
            map[label] = index;
            map[index] = label;
            return map;
        }, {});
    }
    /**
     * @description Entrena el modelo con un conjunto de datos de características (X) y etiquetas de clase (y).
     * @param {Array} X Representa las características de las instancias.
     * @param {Array} y Representa las etiquetas de clase correspondientes.
    */
    train(X, y) {
        if (!X.length || !y.length) {
            console.error('Error: Datos de entrenamiento no válidos.');
            return;
        }

        this.createLabelMap(y);
        const numClasses = this.hiddenToOutputs.rows;
        let inputs = MLP.convertToNumeric(X);
        let labels = MLP.oneHotEncode(y.map(label => this.labelMap[label]), numClasses);

        if (inputs.length !== labels.length) {
            console.error(`Error: La longitud de los inputs y las labels no coinciden. Inputs length: ${inputs.length}, Labels length: ${labels.length}`);
            return;
        }

        let it = 0;
        while (it < this.it) {
            let s = 0;
            for (let i = 0; i < inputs.length; i++) {
                const input = new Matrix(inputs[i].length, 1, inputs[i]);
                //console.log(`Input matrix for instance ${i}:`, input);
                if (input.rows === 0 || input.cols === 0) {
                    console.error('Error: Input matrix has invalid dimensions:', input);
                    return;
                }

                const hidden = this.inputsToHidden.multiply(input);
                //console.log(`Hidden matrix after input to hidden multiplication for instance ${i}:`, hidden);
                if (hidden === null) return;
                hidden.add(this.biasInputsToHidden);
                hidden.foreach(this.activation);
                //console.log(`Hidden matrix after activation for instance ${i}:`, hidden);

                const outputs = this.hiddenToOutputs.multiply(hidden);
                //console.log(`Output matrix after hidden to output multiplication for instance ${i}:`, outputs);
                if (outputs === null) return;
                outputs.add(this.biasHiddenToOutputs);
                outputs.foreach(this.activation);
                //console.log(`Output matrix after activation for instance ${i}:`, outputs);

                const outputErrors = new Matrix(labels[i].length, 1, labels[i]);
                //console.log(`Output errors matrix for instance ${i}:`, outputErrors);
                if (outputErrors.rows !== outputs.rows || outputErrors.cols !== outputs.cols) {
                    console.error(`Error: Matrices de diferentes dimensiones para la sustracción. Dimensiones de outputErrors: ${outputErrors.rows}x${outputErrors.cols}, dimensiones de outputs: ${outputs.rows}x${outputs.cols}`);
                    return;
                }
                outputErrors.subtract(outputs);
                for (let j = 0; j < outputErrors.data.length; j++) {
                    s += outputErrors.data[j] * outputErrors.data[j];
                }

                outputs.foreach(this.dActivation);
                if (outputs.rows !== outputErrors.rows || outputs.cols !== outputErrors.cols) {
                    console.error(`Error: Matrices de diferentes dimensiones para el producto Hadamard. Dimensiones de outputs: ${outputs.rows}x${outputs.cols}, dimensiones de outputErrors: ${outputErrors.rows}x${outputErrors.cols}`);
                    return;
                }
                outputs.hadamard(outputErrors);
                outputs.scalar(this.lr);
                //console.log(`Outputs matrix after Hadamard product and scalar for instance ${i}:`, outputs);

                const hiddenTranspose = hidden.transpose();
                const hiddenToOutputsDeltas = outputs.multiply(hiddenTranspose);
                if (hiddenToOutputsDeltas === null) return;
                this.hiddenToOutputs.add(hiddenToOutputsDeltas);
                this.biasHiddenToOutputs.add(outputs);
                //console.log(`Hidden to output weights and biases updated for instance ${i}:`, this.hiddenToOutputs, this.biasHiddenToOutputs);

                const hiddenErrors = this.hiddenToOutputs.transpose().multiply(outputErrors);
                if (hiddenErrors === null) return;
                hidden.foreach(this.dActivation);
                hidden.hadamard(hiddenErrors);
                hidden.scalar(this.lr);
                //console.log(`Hidden errors matrix after Hadamard product and scalar for instance ${i}:`, hiddenErrors);

                const inputTranspose = input.transpose();
                const inputsToHiddenDeltas = hidden.multiply(inputTranspose);
                if (inputsToHiddenDeltas === null) return;
                this.inputsToHidden.add(inputsToHiddenDeltas);
                this.biasInputsToHidden.add(hidden);
                //console.log(`Input to hidden weights and biases updated for instance ${i}:`, this.inputsToHidden, this.biasInputsToHidden);
            }
            it++;
            if (it % 100 === 0) {
                console.log(`Iteración ${it}, error acumulado: ${Math.sqrt(s)}`);
            }
        }
    }
    /**
     * @description Predice las clases de un nuevo conjunto de datos de características (X).
     * @param {Array} X Datos para predicción.
     * @param {Array} trueLabels Lista de las etiquetas correctas para los datos de entrada.
     * @returns {Array}
    */
    predict(X, trueLabels = []) {
        const inputs = MLP.convertToNumeric(X);
        return inputs.map((input, i) => {
            console.log(`Instancia a predecir ${i}:`, input);
            const inputsMatrix = new Matrix(input.length, 1, input);
            let hidden = this.inputsToHidden.multiply(inputsMatrix);
            if (hidden === null) return null;
            hidden.add(this.biasInputsToHidden);
            hidden.foreach(this.activation);
            let output = this.hiddenToOutputs.multiply(hidden);
            if (output === null) return null;
            output.add(this.biasHiddenToOutputs);
            output.foreach(this.activation);
            const predictedClass = this.classifyOutput(output.data);
            const predictedLabel = this.labelMap[predictedClass];
            console.log(`Predicción obtenida para la instancia ${i}:`, predictedLabel);
            if (trueLabels.length > 0) {
                console.log(`Etiqueta verdadera para la instancia ${i}:`, trueLabels[i]);
            }
            return predictedLabel;
        });
    }

    classifyOutput(output) {
        let maxIndex = 0;
        for (let i = 1; i < output.length; i++) {
            if (output[i] > output[maxIndex]) {
                maxIndex = i;
            }
        }
        return maxIndex;
    }
    /**
     * @description Realiza la función sigmoidal.
     * @param {Array} x Datos de activación.
     * @returns {Array}
    */
    sigmoid(x) {
        return 1 / (1 + Math.exp(-x));
    }
    /**
     * @description Derivada de la función sigmoidal.
     * @param {Array} x Dato de entrada.
     * @returns {Array}
    */
    dSigmoid(x) {
        return x * (1 - x);
    }
    /**
     * @description Crea una representación del modelo.
     * @returns {string}
    */
    getModelRepresentation() {
        const visualizeMatrix = (matrix, name) => {
            let result = `\n${name} (${matrix.rows}x${matrix.cols}):\n`;
            for (let i = 0; i < matrix.rows; i++) {
                for (let j = 0; j < matrix.cols; j++) {
                    result += this.getAsciiRepresentation(matrix.data[i * matrix.cols + j]);
                }
                result += '\n';
            }
            return result;
        };

        const weights1 = visualizeMatrix(this.inputsToHidden, 'Inputs to Hidden Weights');
        const bias1 = visualizeMatrix(this.biasInputsToHidden, 'Hidden Layer Biases');
        const weights2 = visualizeMatrix(this.hiddenToOutputs, 'Hidden to Outputs Weights');
        const bias2 = visualizeMatrix(this.biasHiddenToOutputs, 'Output Layer Biases');

        return weights1 + bias1 + weights2 + bias2;
    }
    /**
     * @description Genera una representación textual del modelo MLP en ASCII.
     * @param {Array} value Dato de entrada.
     * @returns {string}
    */
    getAsciiRepresentation(value) {
        const chars = [' ', '.', ':', '-', '=', '+', '*', '#', '%', '@'];
        const normalizedValue = (value + 1) / 2; // Normaliza el valor entre 0 y 1
        const index = Math.min(Math.floor(normalizedValue * chars.length), chars.length - 1);
        return chars[index];
    }    
}

/* EJEMPLO EN JAVASCRIPT
const mlp = new MLP(); // Usando los valores predeterminados para inputSize, hiddenSize y outputSize

// X representa las características de las instancias
const X = [
    [6.725, 3.725, 5.825, 2.125],
    [6.725, 2.525, 5.825, 2.125],
    // ... otras instancias
];

// y representa las etiquetas de clase correspondientes
const y = [
    'Iris-setosa', // Clase 1
    'Iris-setosa', // Clase 1
    // ... otras etiquetas
];

// Entrenar el modelo con los datos proporcionados
mlp.train(X, y);

// Ejemplo de datos para predicción:
const newInstances = [
    [6.725, 3.725, 5.825, 2.125],
    [6.725, 2.525, 5.825, 2.125],
    // ... otras nuevas instancias
];

// Realizar predicciones
const predictions = mlp.predict(newInstances, y);
console.log("Predicciones:", predictions);
*/