¡Bienvenido de nuevo! En la Parte 1, vimos cómo una red neuronal podía sumar dos números utilizando una estructura feedforward básica. Al final del artículo, incluso te mostramos una implementación completa en C++ que incluía el pase hacia adelante, el bucle de entrenamiento, el cálculo de la pérdida y las actualizaciones de pesos, todo en código.
Esto naturalmente nos lleva a la siguiente pregunta: ¿cómo exactamente aprende una red neuronal a ajustar esos pesos por sí misma a través del entrenamiento? Eso es exactamente lo que exploraremos aquí en la Parte 2.
Antes de continuar, aquí está el código completo en C++ de la Parte 1 que implementa una red neuronal simple para aprender a sumar dos números. Incluye el pase hacia adelante, el cálculo de la pérdida, la retropropagación y las actualizaciones de pesos:
#include <iostream>
#include <cmath>
using namespace std;
// Función de activación Sigmoid
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
// Derivada de sigmoid (usada en retropropagación)
float sigmoid_derivative(float y) {
return y * (1 - y); // donde y = sigmoid(x)
}
int main() {
// Inicializar pesos y sesgos para 3 nodos ocultos
// Estos valores se eligen manualmente para mostrar el proceso de aprendizaje más claramente.
// Usar tanto pesos positivos como negativos ayuda a prevenir la simetría y proporciona un punto de partida diverso.
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f; // El nodo 1 comienza con influencia positiva de ambas entradas
float weight1_node2 = -0.5f, weight2_node2 = -0.5f, bias_node2 = 0.0f; // El nodo 2 comienza con influencia negativa, fomentando el equilibrio
float weight1_node3 = 0.25f, weight2_node3 = -0.25f, bias_node3 = 0.0f; // El nodo 3 es mixto, útil para capturar interacciones
// Inicializar pesos y sesgo de la capa de salida
float weight_out_node1 = 0.5f, weight_out_node2 = 0.5f, weight_out_node3 = 0.5f, bias_out = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000; // Número de veces que recorremos los datos de entrenamiento
// Bucle de entrenamiento
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
float x1 = a / 100.0f;
float x2 = b / 100.0f;
float target = (a + b) / 200.0f; // Normalizar la suma objetivo a [0, 1]
// ===== Pase hacia adelante =====
float z1 = x1 * weight1_node1 + x2 * weight2_node1 + bias_node1;
float h1 = sigmoid(z1);
float z2 = x1 * weight1_node2 + x2 * weight2_node2 + bias_node2;
float h2 = sigmoid(z2);
float z3 = x1 * weight1_node3 + x2 * weight2_node3 + bias_node3;
float h3 = sigmoid(z3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float error = target - y_pred;
total_loss += error * error; // Error cuadrático (pérdida)
// ===== Retropropagación - Capa de Salida =====
float delta_out = error; // Derivada de la pérdida respecto a y_pred
float grad_out_w1 = delta_out * h1; // Gradiente para el peso de salida 1
float grad_out_w2 = delta_out * h2; // Gradiente para el peso de salida 2
float grad_out_w3 = delta_out * h3; // Gradiente para el peso de salida 3
float grad_out_b = delta_out; // Gradiente para el sesgo de salida
// ===== Retropropagación - Capa Oculta =====
// Calcular cómo cada nodo oculto contribuyó al error
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1; // Gradiente para w1 de node1
float grad_w2_node1 = delta_h1 * x2; // Gradiente para w2 de node1
float grad_b_node1 = delta_h1; // Gradiente para el sesgo de node1
float grad_w1_node2 = delta_h2 * x1;
float grad_w2_node2 = delta_h2 * x2;
float grad_b_node2 = delta_h2;
float grad_w1_node3 = delta_h3 * x1;
float grad_w2_node3 = delta_h3 * x2;
float grad_b_node3 = delta_h3;
// ===== Actualizar Pesos de la Capa de Salida =====
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
// ===== Actualizar Pesos de la Capa Oculta =====
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
weight1_node2 += learning_rate * grad_w1_node2;
weight2_node2 += learning_rate * grad_w2_node2;
bias_node2 += learning_rate * grad_b_node2;
weight1_node3 += learning_rate * grad_w1_node3;
weight2_node3 += learning_rate * grad_w2_node3;
bias_node3 += learning_rate * grad_b_node3;
}
}
// Registrar la pérdida cada 100 épocas
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[Resumen] Época " << epoch + 1 << ": Pérdida = " << total_loss << endl;
}
// ===== Probar el modelo con entrada del usuario =====
float a, b;
cout << "
Prueba de predicción de suma (a + b)
Ingresa a: ";
cin >> a;
cout << "Ingresa b: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// Pase hacia adelante nuevamente con pesos entrenados
float h1 = sigmoid(x1 * weight1_node1 + x2 * weight2_node1 + bias_node1);
float h2 = sigmoid(x1 * weight1_node2 + x2 * weight2_node2 + bias_node2);
float h3 = sigmoid(x1 * weight1_node3 + x2 * weight2_node3 + bias_node3);
float y_pred = h1 * weight_out_node1 + h2 * weight_out_node2 + h3 * weight_out_node3 + bias_out;
float predicted_sum = y_pred * 200.0f;
// Salida del resultado
cout << "Suma predicha: " << predicted_sum << "
Suma real: " << (a + b) << endl;
return 0;
}
En esta parte, desglosaremos cómo aprende una red neuronal: desde la configuración de capas y funciones de activación hasta el ajuste de pesos a través de la retropropagación. También aprenderás cómo entrenamos el modelo utilizando tasas de aprendizaje y épocas, y exploraremos si una red puede ir más allá de tareas simples como la suma.
- Lo que realmente hacen los nodos y capas en una red neuronal.
- Cómo establecemos pesos iniciales y los ajustamos durante el entrenamiento.
- El papel de la tasa de aprendizaje y las épocas en la configuración del proceso de aprendizaje.
- Qué son las funciones de activación, cómo funcionan y cuándo usar sigmoid, tanh o ReLU.
- Cómo la retropropagación y las derivadas permiten que la red aprenda de los errores.
- Si una red neuronal puede ir más allá de tareas simples como la suma y aprender múltiples cosas.
¿Listo? ¡Vamos!
Nodos y Capas: ¿Qué está realmente pasando?
Un neurona (o nodo) es el bloque básico de construcción de una red neuronal. Cada neurona recibe valores de entrada, los multiplica por pesos, añade un sesgo y luego pasa el resultado a través de una función de activación para producir una salida. Es como un pequeño tomador de decisiones que transforma datos paso a paso.
Una capa es una colección de neuronas que operan al mismo nivel de la red. La información fluye de una capa a la siguiente. Los tres tipos principales de capas son:
- Capa de entrada: recibe los datos en bruto (por ejemplo, los números que deseas sumar).
- Capas ocultas: realizan transformaciones sobre la entrada, aprendiendo patrones a través de conexiones ponderadas.
- Capa de salida: produce la predicción o resultado final.

Cada neurona en una capa está conectada a las neuronas en la siguiente capa, y cada una de esas conexiones tiene un peso que la red aprende y ajusta durante el entrenamiento.
Piensa en cada nodo (neurona) en una red neuronal como un pequeño tomador de decisiones. Toma algunas entradas, las multiplica por sus "pesos", añade un "sesgo", y luego aplica una función de activación (como sigmoid, tanh o ReLU).
Más nodos = más potencia cerebral:
- Un pequeño número de nodos podría resolver rápidamente problemas simples.
- Más nodos significan que tu red puede entender patrones más complejos, pero demasiados pueden ralentizarla o hacer que se sobrepiense (sobreajuste)!
¿Qué pasa si apilamos capas? Puedes apilar múltiples capas ocultas, eso es una "red neuronal profunda". Cada capa aprende algo ligeramente más abstracto:
- La primera capa aprende patrones simples.
- La segunda capa combina esos patrones en ideas más complejas.
- Las capas tercera y posteriores construyen una comprensión cada vez más abstracta.
Pesos y Sesgos en Redes Neuronales
¿Qué es un peso en una red neuronal?
En una red neuronal, un peso es un valor que representa la fuerza de la conexión entre dos neuronas. Cuando los datos de entrada fluyen a través de la red, se multiplican por estos pesos. Un peso más alto significa que la entrada tiene una influencia más fuerte en la salida de la siguiente neurona. Durante el entrenamiento, estos pesos se ajustan para ayudar a la red a hacer mejores predicciones.
¿Qué es un sesgo en una red neuronal?
Un sesgo es un parámetro adicional en una red neuronal que permite que la activación de una neurona se desplace. Actúa como un offset o umbral que ayuda al modelo a ajustarse mejor a los datos al permitirle aprender patrones que no pasan por el origen. Al igual que los pesos, los sesgos se ajustan durante el entrenamiento para mejorar las predicciones.
¿Cómo establecemos los pesos iniciales?
Cuando comenzamos el entrenamiento, los pesos deben inicializarse a algunos valores. En nuestro ejemplo de código, simplemente los establecemos manualmente al principio:
float weight1_node1 = 0.5f, weight2_node1 = 0.5f, bias_node1 = 0.0f;
En sistemas más complejos, los pesos se inicializan típicamente de manera aleatoria usando valores pequeños (como entre -0.5 y 0.5). Esta aleatoriedad ayuda a prevenir la simetría, donde todas las neuronas aprenden lo mismo, y le da a la red una mejor oportunidad de descubrir patrones útiles. Las estrategias de inicialización comunes incluyen Xavier (Glorot) y He initialization, que están diseñadas para mantener una señal estable a medida que fluye a través de la red. Para experimentos simples como el nuestro, los valores manuales o pequeños aleatorios funcionan lo suficientemente bien.
¿Cómo ajustamos los pesos?
Durante el entrenamiento, así es como funciona el ajuste:
- La red hace una predicción usando los pesos actuales.
- Compara la predicción con la respuesta correcta (salida objetivo).
- Calcula cuán equivocada estuvo — este es el error o "pérdida".
- Utiliza la retropropagación para calcular cómo cada peso afectó el error.
- Cada peso se ajusta ligeramente para reducir el error para la próxima vez — este paso se realiza utilizando gradientes (derivadas) y una tasa de aprendizaje.
En nuestro ejemplo de código en C++, este proceso ocurre dentro de los bucles de entrenamiento anidados. Aquí hay una referencia rápida:
float error = target - y_pred;
float delta_out = error;
float grad_out_w1 = delta_out * h1;
float grad_out_w2 = delta_out * h2;
float grad_out_w3 = delta_out * h3;
float grad_out_b = delta_out;
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
float delta_h2 = delta_out * weight_out_node2 * sigmoid_derivative(h2);
float delta_h3 = delta_out * weight_out_node3 * sigmoid_derivative(h3);
float grad_w1_node1 = delta_h1 * x1;
float grad_w2_node1 = delta_h1 * x2;
float grad_b_node1 = delta_h1;
// Actualizar pesos
weight_out_node1 += learning_rate * grad_out_w1;
weight_out_node2 += learning_rate * grad_out_w2;
weight_out_node3 += learning_rate * grad_out_w3;
bias_out += learning_rate * grad_out_b;
weight1_node1 += learning_rate * grad_w1_node1;
weight2_node1 += learning_rate * grad_w2_node1;
bias_node1 += learning_rate * grad_b_node1;
Este fragmento demuestra cómo calculamos gradientes y los aplicamos para actualizar los pesos en ambas capas, de salida y oculta. Cada iteración utiliza los gradientes calculados a partir de la derivada de la función de activación y el error para empujar los pesos en la dirección que reduce la pérdida. A lo largo de miles de iteraciones, los pesos se vuelven gradualmente más precisos.
Funciones de Activación: Dándole personalidad a tus nodos
Las funciones de activación deciden si una neurona debe "disparar" o no. Se aplican al resultado de la suma ponderada y el sesgo en cada nodo, introduciendo no linealidad a la red, sin la cual, la red solo podría aprender relaciones lineales (no muy útiles para problemas complejos).
Aquí hay algunas funciones de activación comunes:
1. Sigmoid
- Fórmula:
1 / (1 + e^(-x))
- Rango de salida: 0 a 1
Caso de uso: Genial para clasificación binaria o capas de salida donde necesitas valores entre 0 y 1.
- Pros: Salida suave y acotada.
- Contras: Satura en los extremos (cerca de 0 o 1), ralentiza el aprendizaje debido a gradientes que se desvanecen.

2. Tanh
- Fórmula:
(e^x - e^(-x)) / (e^x + e^(-x))
Rango de salida: -1 a 1
- Caso de uso: Cuando deseas una salida centrada en cero.
- Pros: Gradientes más fuertes que sigmoid; aprendizaje más rápido en capas ocultas.
- Contras: Aún sufre de gradientes que se desvanecen en los extremos.

3. ReLU (Unidad Lineal Rectificada)
- Fórmula:
max(0, x)
- Rango de salida: 0 a infinito.
Caso de uso: Predeterminado para la mayoría de las capas ocultas en aprendizaje profundo.
- Pros: Cálculo rápido, ayuda con activación dispersa.
- Contras: Puede "morir" si las entradas son siempre negativas ("problema de ReLU moribunda").

¿Cuándo usar qué función de activación?
Activación | Rango de Salida | Bueno Para | Tipo de Capa Común | Notas |
---|---|---|---|---|
Sigmoid | 0 a 1 | Salida Binaria | Capa de salida | Puede causar gradiente que se desvanece |
Tanh | -1 a 1 | Capas ocultas centradas en cero | Capa oculta | Gradientes más fuertes que sigmoid |
ReLU | 0 a ∞ | La mayoría de las redes profundas y CNNs | Capa oculta | Rápido, eficiente, pero puede "morir" en negativos |
En nuestro código de ejemplo, usamos sigmoid para las capas ocultas. Funciona para demostraciones pequeñas, pero en redes más grandes, ReLU suele ser preferido para un mejor rendimiento y un entrenamiento más rápido.
Derivadas y Retropropagación: Cómo las redes aprenden de los errores
Imagina que estás vendado, de pie en una colina, y necesitas encontrar tu camino cuesta abajo. Sentirías alrededor con tu pie para detectar qué dirección baja, y luego te moverías en consecuencia.
Eso es lo que hacen las redes neuronales utilizando derivadas: verifican qué pequeños ajustes hacen que las predicciones sean más precisas.
Después de hacer una predicción, la red calcula cuán equivocada estuvo (llamamos a esto "pérdida"). Luego utiliza algo llamado retropropagación para rastrear ese error hacia atrás a través de la red, averiguando exactamente cómo cada peso contribuyó al error.
Aquí está el bucle de retropropagación simplificado:
- Pase hacia adelante: Adivina la respuesta.
- Calcular Pérdida: ¿Cuán equivocada fue esa adivinanza?
- Pase hacia atrás: Descubre cómo cada peso afectó ese error.
- Actualizar Pesos: Ajusta ligeramente los pesos para reducir futuros errores.
Este proceso se repite miles de veces, haciendo que la red se vuelva cada vez más inteligente.
Veamos una implementación simplificada en C++ de esta idea:
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
Esta es nuestra función de activación y su derivada. Se utilizan tanto en los pases hacia adelante como hacia atrás.
El bucle de entrenamiento utiliza dos entradas (a y b), las normaliza entre 0 y 1, y entrena la red para predecir su suma. Usamos 3 nodos ocultos y 1 nodo de salida. Aquí hay una parte clave del bucle:
float error = target - y_pred;
total_loss += error * error;
float delta_out = error;
...
float delta_h1 = delta_out * weight_out_node1 * sigmoid_derivative(h1);
Aquí es donde calculamos el error y usamos derivadas para empujar ajustes hacia atrás a través de la red: retropropagación en acción.
Al final del entrenamiento, permitimos que los usuarios ingresen dos números, y la red predice la suma. No está memorizando, está generalizando a partir de lo que ha aprendido.
Tasa de Aprendizaje y Épocas
Como habrás notado en el código, usamos dos hiperparámetros importantes para controlar el proceso de entrenamiento: tasa_de_aprendizaje y épocas.
¿Qué es una tasa de aprendizaje?
La tasa de aprendizaje determina cuán grande es cada paso al actualizar los pesos. Una tasa de aprendizaje pequeña (como 0.01) significa que la red hace ajustes pequeños cada vez, lo cual es más seguro pero más lento. Una tasa de aprendizaje grande podría acelerar las cosas, pero si es demasiado grande, la red podría pasarse y no aprender correctamente.
En nuestro código, usamos:
float learning_rate = 0.01f;
Esto nos da un equilibrio entre velocidad y estabilidad.
¿Qué es una época?
Una época es un pase completo a través de todo el conjunto de datos de entrenamiento. En nuestro ejemplo, para cada época, entrenamos la red usando cada combinación de a
y b
desde 0 hasta 99. Luego lo hacemos de nuevo para la siguiente época.
Entrenamos durante múltiples épocas para permitir que la red refine sus pesos una y otra vez.
- Si aumentas el número de épocas, la red tiene más oportunidades de mejorar, potencialmente reduciendo aún más la pérdida. Sin embargo, demasiadas épocas pueden llevar a sobreajuste, donde el modelo se adapta demasiado a los datos de entrenamiento y rinde mal en nuevas entradas.
- Si disminuyes el número de épocas, el entrenamiento será más rápido, pero la red podría no aprender lo suficiente y podría tener un rendimiento deficiente.
En el código, usamos:
int epochs = 1000;
Esto es típicamente un buen comienzo para problemas simples como aprender a sumar, pero puedes ajustar según qué tan rápido o lento disminuye la pérdida.
¿Puede una red neuronal aprender múltiples cosas?
Absolutamente. Las redes neuronales no están limitadas a aprender solo un tipo de tarea. De hecho, la misma arquitectura de red puede ser entrenada para hacer una variedad de cosas, como reconocer dígitos escritos a mano, traducir idiomas o incluso generar música, dependiendo de los datos que se le proporcionen y cómo se entrene.
En nuestro ejemplo, la red aprendió a sumar dos números, pero con diferentes datos de entrenamiento y una capa de salida modificada, también podría aprender a:
- Restar números
- Clasificar imágenes
- Predecir valores futuros en una serie temporal
Cuantas más tareas quieras que maneje la red, o más compleja sea la tarea, más neuronas y capas podrías necesitar. Un problema simple como la suma podría necesitar solo unos pocos nodos, mientras que tareas como el reconocimiento de imágenes o la traducción de idiomas pueden requerir redes mucho más profundas y anchas. Solo ten en cuenta: más complejidad no siempre es mejor; agregar demasiados nodos puede llevar a sobreajuste, donde la red memoriza en lugar de aprender a generalizar.
Esta es la versión mejorada de nuestra red neuronal donde la entrenamos para sumar y restar números de entrada utilizando una red compartida:
#include <iostream>
#include <cmath>
using namespace std;
// Función de activación Sigmoid (comprime la entrada en el rango [0, 1])
float sigmoid(float x) {
return 1.0f / (1.0f + exp(-x));
}
// Derivada de sigmoid (usada para calcular gradientes durante la retropropagación)
float sigmoid_derivative(float y) {
return y * (1 - y); // y = sigmoid(x)
}
int main() {
// === Pesos y Sesgos de la Capa Oculta (3 neuronas ocultas, 2 entradas cada una) ===
float w1_n1 = 0.5f, w2_n1 = 0.5f, b1 = 0.0f;
float w1_n2 = -0.5f, w2_n2 = -0.5f, b2 = 0.0f;
float w1_n3 = 0.25f, w2_n3 = -0.25f, b3 = 0.0f;
// === Pesos y Sesgos de la Capa de Salida (2 neuronas de salida: suma y resta) ===
float out_w1_sum = 0.5f, out_w2_sum = 0.5f, out_w3_sum = 0.5f, out_b_sum = 0.0f;
float out_w1_sub = -0.5f, out_w2_sub = -0.5f, out_w3_sub = 0.5f, out_b_sub = 0.0f;
float learning_rate = 0.01f;
int epochs = 1000;
// === Bucle de Entrenamiento ===
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
for (int a = 0; a < 100; ++a) {
for (int b = 0; b < 100; ++b) {
// === Normalizar valores de entrada ===
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// === Normalizar objetivos ===
float target_sum = (a + b) / 200.0f; // La suma varía de 0 a 198 → [0, ~1]
float target_sub = (a - b + 100.0f) / 200.0f; // La resta varía de -99 a +99 → [0, ~1]
// === Pase hacia adelante (Entrada → Capa Oculta) ===
float z1 = x1 * w1_n1 + x2 * w2_n1 + b1;
float h1 = sigmoid(z1);
float z2 = x1 * w1_n2 + x2 * w2_n2 + b2;
float h2 = sigmoid(z2);
float z3 = x1 * w1_n3 + x2 * w2_n3 + b3;
float h3 = sigmoid(z3);
// === Pase hacia adelante (Capa Oculta → Capa de Salida) ===
float y_pred_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
float y_pred_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;
// === Calcular Pérdida (Error Cuadrático) ===
float error_sum = target_sum - y_pred_sum;
float error_sub = target_sub - y_pred_sub;
total_loss += error_sum * error_sum + error_sub * error_sub;
// === Retropropagación - Capa de Salida (Gradientes para cada nodo de salida) ===
float delta_out_sum = error_sum;
float grad_out_w1_sum = delta_out_sum * h1;
float grad_out_w2_sum = delta_out_sum * h2;
float grad_out_w3_sum = delta_out_sum * h3;
float grad_out_b_sum = delta_out_sum;
float delta_out_sub = error_sub;
float grad_out_w1_sub = delta_out_sub * h1;
float grad_out_w2_sub = delta_out_sub * h2;
float grad_out_w3_sub = delta_out_sub * h3;
float grad_out_b_sub = delta_out_sub;
// === Retropropagación - Capa Oculta ===
float delta_h1 = (delta_out_sum * out_w1_sum + delta_out_sub * out_w1_sub) * sigmoid_derivative(h1);
float delta_h2 = (delta_out_sum * out_w2_sum + delta_out_sub * out_w2_sub) * sigmoid_derivative(h2);
float delta_h3 = (delta_out_sum * out_w3_sum + delta_out_sub * out_w3_sub) * sigmoid_derivative(h3);
float grad_w1_n1 = delta_h1 * x1;
float grad_w2_n1 = delta_h1 * x2;
float grad_b1 = delta_h1;
float grad_w1_n2 = delta_h2 * x1;
float grad_w2_n2 = delta_h2 * x2;
float grad_b2 = delta_h2;
float grad_w1_n3 = delta_h3 * x1;
float grad_w2_n3 = delta_h3 * x2;
float grad_b3 = delta_h3;
// === Actualizar Pesos de Salida ===
out_w1_sum += learning_rate * grad_out_w1_sum;
out_w2_sum += learning_rate * grad_out_w2_sum;
out_w3_sum += learning_rate * grad_out_w3_sum;
out_b_sum += learning_rate * grad_out_b_sum;
out_w1_sub += learning_rate * grad_out_w1_sub;
out_w2_sub += learning_rate * grad_out_w2_sub;
out_w3_sub += learning_rate * grad_out_w3_sub;
out_b_sub += learning_rate * grad_out_b_sub;
// === Actualizar Pesos Ocultos ===
w1_n1 += learning_rate * grad_w1_n1;
w2_n1 += learning_rate * grad_w2_n1;
b1 += learning_rate * grad_b1;
w1_n2 += learning_rate * grad_w1_n2;
w2_n2 += learning_rate * grad_w2_n2;
b2 += learning_rate * grad_b2;
w1_n3 += learning_rate * grad_w1_n3;
w2_n3 += learning_rate * grad_w2_n3;
b3 += learning_rate * grad_b3;
}
}
// === Imprimir Pérdida Cada 100 Épocas ===
if ((epoch + 1) % 100 == 0 || epoch == 0)
cout << "[Época " << epoch + 1 << "] Pérdida: " << total_loss << endl;
}
// === Fase de Prueba ===
float a, b;
cout << "\nPrueba de predicción\nIngresa a: ";
cin >> a;
cout << "Ingresa b: ";
cin >> b;
float x1 = a / 100.0f;
float x2 = b / 100.0f;
// Pase hacia adelante nuevamente para la entrada del usuario
float h1 = sigmoid(x1 * w1_n1 + x2 * w2_n1 + b1);
float h2 = sigmoid(x1 * w1_n2 + x2 * w2_n2 + b2);
float h3 = sigmoid(x1 * w1_n3 + x2 * w2_n3 + b3);
float y_sum = h1 * out_w1_sum + h2 * out_w2_sum + h3 * out_w3_sum + out_b_sum;
float y_sub = h1 * out_w1_sub + h2 * out_w2_sub + h3 * out_w3_sub + out_b_sub;
// Desnormalizar salidas
float predicted_sum = y_sum * 200.0f;
float predicted_sub = y_sub * 200.0f - 100.0f;
cout << "Suma predicha: " << predicted_sum << endl;
cout << "Suma real: " << (a + b) << endl;
cout << "Diferencia predicha: " << predicted_sub << endl;
cout << "Diferencia real: " << (a - b) << endl;
return 0;
}
Cerrando
Eso fue mucho para asimilar, pero bastante impresionante, ¿verdad? No solo hablamos de teoría; vimos cómo una red neuronal aprende a hacer algo real: sumar y restar números. Sin reglas codificadas, solo pesos ajustándose a sí mismos a través del entrenamiento. A lo largo del camino, conocimos tasas de aprendizaje, funciones de activación y la magia de la retropropagación que une todo.
En la próxima parte, daremos un paso atrás del código y profundizaremos en la historia detrás de todo esto. ¿De dónde vinieron las redes neuronales? ¿Por qué la IA ha explotado en solo los últimos años? ¿Qué cambió? Exploraremos cómo comenzó todo y por qué apenas está comenzando.