/**
* @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);
*/