Las redes neuronales multicapa (MLP, por sus siglas en inglés) son una de las arquitecturas más básicas pero poderosas en el mundo del aprendizaje profundo. En este artículo, te mostraré cómo construir una red neuronal MLP en JavaScript desde cero. 🚀
Vamos a programar una red capaz de aprender la función XOR, que es un clásico ejemplo de problemas no lineales que requieren una capa oculta para resolverse correctamente.
Una MLP (Multilayer Perceptron) es un tipo de red neuronal artificial que consta de:
A diferencia de un perceptrón simple, que solo puede aprender funciones lineales, la MLP utiliza una función de activación (como la sigmoide) en sus neuronas ocultas para modelar relaciones más complejas.
A continuación, vamos a ver la implementación completa de una red neuronal MLP en JavaScript que puede aprender la función XOR.
NeuralNetwork
Vamos a definir una clase llamada NeuralNetwork
, que representará nuestra red neuronal con una capa de entrada, una capa oculta y una capa de salida.
class NeuralNetwork {
constructor(inputSize, hiddenSize, outputSize) {
this.inputSize = inputSize;
this.hiddenSize = hiddenSize;
this.outputSize = outputSize;
// Inicializar las neuronas y sus pesos
this.inputNeurons = Array.from({ length: this.inputSize }, () => ({
value: 0,
weights: Array.from({ length: this.hiddenSize }, () => Math.random() * 2 - 1)
}));
this.hiddenPerceptrons = Array.from({ length: this.hiddenSize }, () => ({
value: 0,
bias: Math.random() * 2 - 1,
weights: Array.from({ length: this.outputSize }, () => Math.random() * 2 - 1)
}));
this.outputNeurons = Array.from({ length: this.outputSize }, () => ({
value: 0,
bias: Math.random() * 2 - 1
}));
this.learningRate = 0.1;
}
En este constructor:
0
y pesos aleatorios hacia la capa oculta.
0
, un sesgo (bias
) y pesos aleatorios hacia la capa de salida.
0
y un sesgo (bias
).
0.1
.
La función sigmoide es una de las más utilizadas en redes neuronales porque permite normalizar los valores entre 0
y 1
, lo que facilita la interpretación de los resultados.
sigmoid(value) {
return 1 / (1 + Math.exp(-value));
}
sigmoidDerivative(value) {
return value * (1 - value);
}
sigmoid(value)
: Calcula el valor de la función sigmoide.
sigmoidDerivative(value)
: Calcula la derivada de la sigmoide, necesaria para el algoritmo de backpropagation.
El proceso de forward propagation consiste en tomar los valores de entrada y pasarlos a través de la red para obtener una predicción.
Predict(inputs) {
// Capa de entrada
for (let i = 0; i < this.inputSize; i++) {
this.inputNeurons[i].value = inputs[i];
}
// Capa oculta
for (let h = 0; h < this.hiddenSize; h++) {
let sum = this.hiddenPerceptrons[h].bias;
for (let i = 0; i < this.inputSize; i++) {
sum += this.inputNeurons[i].value * this.inputNeurons[i].weights[h];
}
this.hiddenPerceptrons[h].value = this.sigmoid(sum);
}
// Capa de salida
for (let o = 0; o < this.outputSize; o++) {
let sum = this.outputNeurons[o].bias;
for (let h = 0; h < this.hiddenSize; h++) {
sum += this.hiddenPerceptrons[h].value * this.hiddenPerceptrons[h].weights[o];
}
this.outputNeurons[o].value = this.sigmoid(sum);
}
return this.outputNeurons.map(neuron => neuron.value);
}
Aquí, pasamos los valores de entrada por la capa oculta y finalmente obtenemos una salida.
Para que la red aprenda, usamos el algoritmo de backpropagation, que ajusta los pesos minimizando el error.
Train(data, epochs) {
const m = data.input.length; // Número de muestras
for (let epoch = 0; epoch < epochs; epoch++) {
let totalError = 0;
for (let i = 0; i < m; i++) {
// Paso 1: Forward propagation
const prediction = this.Predict(data.input[i]);
// Paso 2: Calcular errores en la salida
const outputErrors = prediction.map((pred, o) => {
const error = data.output[i][o] - pred;
totalError += Math.pow(error, 2);
return error * this.sigmoidDerivative(pred);
});
// Paso 3: Backpropagation para la capa oculta
const hiddenErrors = Array.from({ length: this.hiddenSize }, (_, h) =>
outputErrors.reduce((sum, outputError, o) =>
sum + outputError * this.hiddenPerceptrons[h].weights[o], 0)
* this.sigmoidDerivative(this.hiddenPerceptrons[h].value)
);
// Actualizar pesos y biases entre capa oculta y salida
for (let o = 0; o < this.outputSize; o++) {
for (let h = 0; h < this.hiddenSize; h++) {
this.hiddenPerceptrons[h].weights[o] +=
this.learningRate * outputErrors[o] * this.hiddenPerceptrons[h].value;
}
this.outputNeurons[o].bias += this.learningRate * outputErrors[o];
}
// Actualizar pesos y biases entre entrada y capa oculta
for (let h = 0; h < this.hiddenSize; h++) {
for (let i = 0; i < this.inputSize; i++) {
this.inputNeurons[i].weights[h] +=
this.learningRate * hiddenErrors[h] * this.inputNeurons[i].value;
}
this.hiddenPerceptrons[h].bias += this.learningRate * hiddenErrors[h];
}
}
// Mostrar el error total al final de cada época
if (epoch % 1000 === 0) {
console.log(`Epoch ${epoch}, Error total: ${totalError / m}`);
}
}
}
Aquí:
1. Calculamos los errores de salida.
2. Propagamos esos errores hacia atrás y ajustamos los pesos de la red.
Ahora entrenamos la red con la función XOR:
const nn = new NeuralNetwork(2, 4, 1);
const trainingData = {
input: [
[0, 0],
[0, 1],
[1, 0],
[1, 1]
],
output: [
[0],
[1],
[1],
[0]
]
};
nn.Train(trainingData, 10000);
console.log(nn.Predict([0, 0])); // Cerca de 0
console.log(nn.Predict([1, 1])); // Cerca de 0
console.log(nn.Predict([0, 1])); // Cerca de 1
console.log(nn.Predict([1, 0])); // Cerca de 1
🎉 ¡Listo! Nuestra red ha aprendido la función XOR. ¡Increíble! 🚀
Jorge García
Fullstack developer