Se utilizan los parámetros obtenidos en el second epoch:
\[ w_1=0.10000,\quad w_2=0.50000,\quad w_3=-0.69764,\quad w_4=0.30517 \] \[ w_5=0.21724,\quad w_6=0.42985,\quad b_1=b_2=b_3=0 \]
y la entrada: \[ (x_1,x_2)=(0,1),\quad y=1 \]
Hidden neuron \(h_1\): \[ z_1 = (0.1)(0) + (-0.69764)(1) = -0.69764 \] \[ h_1 = \frac{1}{1 + e^{-z_1}} = 0.3323 \]
Hidden neuron \(h_2\): \[ z_2 = (0.5)(0) + (0.30517)(1) = 0.30517 \] \[ h_2 = \frac{1}{1 + e^{-z_2}} = 0.5757 \]
Output neuron: \[ z_3 = (0.21724)(0.3323) + (0.42985)(0.5757) = 0.3197 \] \[ \hat{y} = \frac{1}{1 + e^{-z_3}} = 0.5792 \]
Loss: \[ E = \frac{1}{2}(1 - \hat{y})^2 = 0.08852 \]
El error continúa descendiendo (de \(0.0897\) en la época anterior a \(0.0885\)), lo que confirma que el modelo sigue ajustando sus pesos correctamente hacia el objetivo \(y=1\).
Output layer: \[ \frac{\partial E}{\partial z_3} = (\hat{y}-y)\hat{y}(1-\hat{y}) = -0.10255 \]
\[ \frac{\partial E}{\partial w_5} = -0.03408,\quad \frac{\partial E}{\partial w_6} = -0.05904 \]
Updated weights: \[ w_5 = 0.22576,\quad w_6 = 0.44461 \]
Hidden layer:
Neuron \(h_1\): \[ \frac{\partial E}{\partial z_1} = -0.00494 \] \[ w_3 = -0.69640 \]
Neuron \(h_2\): \[ \frac{\partial E}{\partial z_2} = -0.01077 \] \[ w_4 = 0.30786 \]
\[ w_1=0.10000,\quad w_2=0.50000 \] \[ w_3=-0.69640,\quad w_4=0.30786 \] \[ w_5=0.22576,\quad w_6=0.44461 \]
import matplotlib.pyplot as plt
# Valores del tercer epoch
w1 = 0.10000
w2 = 0.50000
w3 = -0.69764
w4 = 0.30517
w5 = 0.21724
w6 = 0.42985
b1 = 0
b2 = 0
b3 = 0
x1, x2 = 0, 1
y = 1
# Posiciones de nodos
pos = {
"x1": (0, 1),
"x2": (0, 0),
"h1": (2, 1),
"h2": (2, 0),
"y": (4, 0.5)
}
fig, ax = plt.subplots(figsize=(10, 5))
# Dibujar nodos
def draw_node(label, xy):
ax.scatter(*xy, s=1500)
ax.text(xy[0], xy[1], label, ha='center', va='center', fontsize=12, color='white')
draw_node("x1\n0", pos["x1"])
draw_node("x2\n1", pos["x2"])
draw_node("h1", pos["h1"])
draw_node("h2", pos["h2"])
draw_node("ŷ", pos["y"])
# Dibujar conexiones
def connect(p1, p2, text, offset=(0,0)):
ax.annotate("", xy=p2, xytext=p1,
arrowprops=dict(arrowstyle="->"))
xm = (p1[0]+p2[0])/2 + offset[0]
ym = (p1[1]+p2[1])/2 + offset[1]
ax.text(xm, ym, text, fontsize=10)
# Input -> Hidden
connect(pos["x1"], pos["h1"], f"w1 = {w1}", (0,0.1))
connect(pos["x1"], pos["h2"], f"w2 = {w2}", (0,-0.2))
connect(pos["x2"], pos["h1"], f"w3 = {w3}", (0,0.2))
connect(pos["x2"], pos["h2"], f"w4 = {w4}", (0,-0.1))
# Hidden -> Output
connect(pos["h1"], pos["y"], f"w5 = {w5}", (0,0.1))
connect(pos["h2"], pos["y"], f"w6 = {w6}", (0,-0.1))
# Bias (flechas verdes)
def bias_arrow(target, label):
ax.annotate(label, xy=target,
xytext=(target[0], target[1]+0.8),
arrowprops=dict(arrowstyle="->", color="green"),
color="green")
bias_arrow(pos["h1"], "b1 = 0")
bias_arrow(pos["h2"], "b2 = 0")
bias_arrow(pos["y"], "b3 = 0")
# Etiqueta salida
ax.text(pos["y"][0]+0.3, pos["y"][1], "y = 1", fontsize=12,
bbox=dict(boxstyle="round", fc="lightyellow"))
# Estética
ax.set_xlim(-0.5, 4.8)
## (-0.5, 4.8)
ax.set_ylim(-0.5, 1.8)
## (-0.5, 1.8)
ax.axis("off")
## (np.float64(-0.5), np.float64(4.8), np.float64(-0.5), np.float64(1.8))
plt.title("Neural Network - Third Epoch")
plt.show()
En esta sección, el ejemplo XOR anterior se automatiza usando Python. La implementación se realiza desde cero, sin bibliotecas de redes neuronales ni herramientas de diferenciación automática. Se utiliza la misma observación:
\[ (x_1,x_2)=(0,1), \quad y=1 \]
La red neuronal consta de dos neuronas de entrada, dos neuronas ocultas y una neurona de salida. La función de activación sigmoide se utiliza tanto en la capa oculta como en la capa de salida. La función de pérdida es el error cuadrático.
\[ E = \frac{1}{2}(y-\hat{y})^2 \]
The learning rate is:
\[ \eta = 0.25 \]
import pandas as pd
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 1000)
import numpy as np
import pandas as pd
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(a):
return a * (1 - a)
# Entrada
x1, x2 = 0, 1
y = 1
# Parámetros iniciales del primer epoch
w1 = 0.10000
w2 = 0.50000
w3 = -0.69764
w4 = 0.30517
w5 = 0.21724
w6 = 0.42985
b1 = 0.00000
b2 = 0.00000
b3 = 0.00000
eta = 0.25
epochs = 20
history = []
for epoch in range(1, epochs + 1):
# Forward pass
z1 = w1*x1 + w3*x2 + b1
z2 = w2*x1 + w4*x2 + b2
h1 = sigmoid(z1)
h2 = sigmoid(z2)
z3 = w5*h1 + w6*h2 + b3
y_hat = sigmoid(z3)
loss = 0.5 * (y - y_hat)**2
abs_error = abs(y - y_hat)
# Backpropagation
delta3 = (y_hat - y) * sigmoid_derivative(y_hat)
dw5 = delta3 * h1
dw6 = delta3 * h2
db3 = delta3
delta1 = delta3 * w5 * sigmoid_derivative(h1)
delta2 = delta3 * w6 * sigmoid_derivative(h2)
dw1 = delta1 * x1
dw3 = delta1 * x2
db1 = delta1
dw2 = delta2 * x1
dw4 = delta2 * x2
db2 = delta2
gradient_magnitude = np.sqrt(
dw1**2 + dw2**2 + dw3**2 + dw4**2 + dw5**2 + dw6**2 +
db1**2 + db2**2 + db3**2
)
# Actualización de parámetros
w1 = w1 - eta * dw1
w2 = w2 - eta * dw2
w3 = w3 - eta * dw3
w4 = w4 - eta * dw4
w5 = w5 - eta * dw5
w6 = w6 - eta * dw6
b1 = b1 - eta * db1
b2 = b2 - eta * db2
b3 = b3 - eta * db3
history.append({
"Epoch": epoch,
"z1": z1,
"z2": z2,
"z3": z3,
"h1": h1,
"h2": h2,
"y_hat": y_hat,
"y": y,
"Absolute Error": abs_error,
"Loss": loss,
"Gradient Magnitude": gradient_magnitude,
"w1": w1,
"w2": w2,
"w3": w3,
"w4": w4,
"w5": w5,
"w6": w6,
"b1": b1,
"b2": b2,
"b3": b3
})
results_20_epochs = pd.DataFrame(history)
results_20_epochs.round(2)
## Epoch z1 z2 z3 h1 h2 y_hat y Absolute Error Loss Gradient Magnitude w1 w2 w3 w4 w5 w6 b1 b2 b3
## 0 1 -0.70 0.31 0.32 0.33 0.58 0.58 1 0.42 0.09 0.12 0.1 0.5 -0.70 0.31 0.23 0.44 0.00 0.00 0.03
## 1 2 -0.70 0.31 0.36 0.33 0.58 0.59 1 0.41 0.08 0.12 0.1 0.5 -0.70 0.31 0.23 0.46 0.00 0.01 0.05
## 2 3 -0.69 0.32 0.39 0.33 0.58 0.60 1 0.40 0.08 0.12 0.1 0.5 -0.69 0.31 0.24 0.47 0.00 0.01 0.07
## 3 4 -0.69 0.32 0.43 0.33 0.58 0.61 1 0.39 0.08 0.11 0.1 0.5 -0.69 0.32 0.25 0.49 0.01 0.01 0.10
## 4 5 -0.69 0.33 0.46 0.33 0.58 0.61 1 0.39 0.07 0.11 0.1 0.5 -0.69 0.32 0.26 0.50 0.01 0.01 0.12
## 5 6 -0.69 0.33 0.50 0.34 0.58 0.62 1 0.38 0.07 0.11 0.1 0.5 -0.69 0.32 0.27 0.51 0.01 0.02 0.14
## 6 7 -0.68 0.34 0.53 0.34 0.58 0.63 1 0.37 0.07 0.11 0.1 0.5 -0.69 0.32 0.27 0.53 0.01 0.02 0.16
## 7 8 -0.68 0.34 0.56 0.34 0.58 0.64 1 0.36 0.07 0.10 0.1 0.5 -0.69 0.33 0.28 0.54 0.01 0.02 0.19
## 8 9 -0.68 0.35 0.60 0.34 0.59 0.64 1 0.36 0.06 0.10 0.1 0.5 -0.69 0.33 0.29 0.55 0.01 0.02 0.21
## 9 10 -0.67 0.35 0.63 0.34 0.59 0.65 1 0.35 0.06 0.10 0.1 0.5 -0.68 0.33 0.29 0.56 0.01 0.03 0.23
## 10 11 -0.67 0.36 0.66 0.34 0.59 0.66 1 0.34 0.06 0.09 0.1 0.5 -0.68 0.33 0.30 0.57 0.01 0.03 0.25
## 11 12 -0.67 0.36 0.68 0.34 0.59 0.66 1 0.34 0.06 0.09 0.1 0.5 -0.68 0.34 0.31 0.58 0.02 0.03 0.26
## 12 13 -0.67 0.37 0.71 0.34 0.59 0.67 1 0.33 0.05 0.09 0.1 0.5 -0.68 0.34 0.31 0.59 0.02 0.03 0.28
## 13 14 -0.66 0.37 0.74 0.34 0.59 0.68 1 0.32 0.05 0.09 0.1 0.5 -0.68 0.34 0.32 0.60 0.02 0.04 0.30
## 14 15 -0.66 0.38 0.77 0.34 0.59 0.68 1 0.32 0.05 0.08 0.1 0.5 -0.68 0.34 0.32 0.61 0.02 0.04 0.32
## 15 16 -0.66 0.38 0.79 0.34 0.59 0.69 1 0.31 0.05 0.08 0.1 0.5 -0.68 0.35 0.33 0.62 0.02 0.04 0.33
## 16 17 -0.66 0.39 0.82 0.34 0.60 0.69 1 0.31 0.05 0.08 0.1 0.5 -0.68 0.35 0.33 0.63 0.02 0.04 0.35
## 17 18 -0.66 0.39 0.84 0.34 0.60 0.70 1 0.30 0.05 0.08 0.1 0.5 -0.68 0.35 0.34 0.64 0.02 0.05 0.37
## 18 19 -0.65 0.40 0.87 0.34 0.60 0.70 1 0.30 0.04 0.08 0.1 0.5 -0.67 0.35 0.35 0.65 0.02 0.05 0.38
## 19 20 -0.65 0.40 0.89 0.34 0.60 0.71 1 0.29 0.04 0.07 0.1 0.5 -0.67 0.36 0.35 0.66 0.02 0.05 0.40
A lo largo de las épocas, el valor predicho \(\hat{y}\) aumenta aproximadamente de 0.58 a 0.71, acercándose al valor objetivo \(y=1\). Al mismo tiempo, la función de costo disminuye de aproximadamente 0.09 a 0.04, y la magnitud del gradiente se reduce progresivamente, lo que indica una convergencia estable.
Por otro lado, mediante backpropagation los parámetros se actualizan de forma gradual, donde los pesos de la capa de salida \(w_5\) y \(w_6\) presentan cambios más notorios, mientras que los pesos de la capa oculta se ajustan más lentamente. Este comportamiento refleja cómo la red neuronal aprende a partir del error, reduciendo progresivamente la diferencia entre la predicción y el valor real.
En esta sección se analiza el comportamiento del proceso de entrenamiento a través de diferentes gráficas.
import matplotlib.pyplot as plt
plt.figure()
plt.plot(results_20_epochs["Epoch"], results_20_epochs["Loss"], marker='o')
plt.xlabel("Epoch")
plt.ylabel("Cost")
plt.title("Evolución de la función de costo")
plt.grid(True)
plt.show()
En la gráfica de la función de costo se observa una disminución continua desde aproximadamente 0.09 hasta 0.04. Este comportamiento indica que el modelo está optimizando correctamente la función de pérdida, reduciendo progresivamente el error de predicción. La forma suave y monótona de la curva sugiere que el proceso de entrenamiento es estable y no presenta oscilaciones.
plt.figure()
plt.plot(results_20_epochs["Epoch"], results_20_epochs["Absolute Error"], marker='o')
plt.xlabel("Epoch")
plt.ylabel("Error absoluto")
plt.title("Evolución del error absoluto")
plt.grid(True)
plt.show()
La gráfica del error absoluto muestra una tendencia decreciente desde
aproximadamente 0.42 hasta 0.29. Esto confirma que la diferencia entre
el valor predicho y el valor real se reduce con cada iteración. Aunque
la disminución es gradual, evidencia que la red está aprendiendo de
manera consistente.
plt.figure()
plt.plot(results_20_epochs["Epoch"], results_20_epochs["Gradient Magnitude"], marker='o')
plt.xlabel("Epoch")
plt.ylabel("Magnitud del gradiente")
plt.title("Evolución del gradiente")
plt.grid(True)
plt.show()
En la gráfica de la magnitud del gradiente se observa una reducción
progresiva a lo largo de las épocas. Esto indica que las actualizaciones
de los parámetros son cada vez más pequeñas, lo cual es característico
de un proceso de convergencia. A medida que el modelo se acerca a un
mínimo de la función de costo, los gradientes tienden a disminuir.
plt.figure()
plt.plot(results_20_epochs["Epoch"], results_20_epochs["w5"], label="w5")
plt.plot(results_20_epochs["Epoch"], results_20_epochs["w6"], label="w6")
plt.xlabel("Epoch")
plt.ylabel("Valor")
plt.title("Evolución de pesos de salida")
plt.legend()
plt.grid(True)
plt.show()
Finalmente, la gráfica de los pesos de salida muestra un incremento
sostenido en los valores de \(w_5\) y
\(w_6\). Esto refleja que la red está
ajustando la importancia de las neuronas ocultas en la predicción final.
El hecho de que ambos pesos aumenten indica que ambas neuronas ocultas
contribuyen positivamente al resultado, reforzando la señal hacia el
valor objetivo.
En conjunto, todas las gráficas evidencian que la red neuronal está aprendiendo de manera adecuada, reduciendo el error, ajustando sus parámetros y acercándose progresivamente al valor esperado mediante el uso de backpropagation y descenso por gradiente.
En esta sección se analiza cómo diferentes valores iniciales de los pesos y sesgos afectan el proceso de entrenamiento de la red neuronal. Para ello, se repite el experimento anterior utilizando un conjunto diferente de parámetros iniciales y se comparan los resultados en términos de convergencia, error y estabilidad.
# Nuevo conjunto de parámetros iniciales
w1 = -0.3
w2 = 0.8
w3 = 0.2
w4 = -0.5
w5 = 0.1
w6 = -0.2
b1 = 0.0
b2 = 0.0
b3 = 0.0
eta = 0.25
epochs = 20
history_alt = []
for epoch in range(1, epochs + 1):
# Forward
z1 = w1*x1 + w3*x2 + b1
z2 = w2*x1 + w4*x2 + b2
h1 = sigmoid(z1)
h2 = sigmoid(z2)
z3 = w5*h1 + w6*h2 + b3
y_hat = sigmoid(z3)
loss = 0.5 * (y - y_hat)**2
abs_error = abs(y - y_hat)
# Backprop
delta3 = (y_hat - y) * sigmoid_derivative(y_hat)
dw5 = delta3 * h1
dw6 = delta3 * h2
db3 = delta3
delta1 = delta3 * w5 * sigmoid_derivative(h1)
delta2 = delta3 * w6 * sigmoid_derivative(h2)
dw1 = delta1 * x1
dw3 = delta1 * x2
db1 = delta1
dw2 = delta2 * x1
dw4 = delta2 * x2
db2 = delta2
gradient_magnitude = np.sqrt(
dw1**2 + dw2**2 + dw3**2 + dw4**2 + dw5**2 + dw6**2 +
db1**2 + db2**2 + db3**2
)
# Actualización
w1 -= eta * dw1
w2 -= eta * dw2
w3 -= eta * dw3
w4 -= eta * dw4
w5 -= eta * dw5
w6 -= eta * dw6
b1 -= eta * db1
b2 -= eta * db2
b3 -= eta * db3
history_alt.append({
"Epoch": epoch,
"y_hat": y_hat,
"Loss": loss,
"Absolute Error": abs_error,
"Gradient": gradient_magnitude
})
results_alt = pd.DataFrame(history_alt)
results_alt.round(5)
## Epoch y_hat Loss Absolute Error Gradient
## 0 1 0.49487 0.12758 0.50513 0.15207
## 1 2 0.50632 0.12186 0.49368 0.14861
## 2 3 0.51749 0.11641 0.48251 0.14510
## 3 4 0.52839 0.11121 0.47161 0.14155
## 4 5 0.53900 0.10626 0.46100 0.13799
## 5 6 0.54931 0.10156 0.45069 0.13444
## 6 7 0.55931 0.09710 0.44069 0.13091
## 7 8 0.56901 0.09288 0.43099 0.12742
## 8 9 0.57841 0.08887 0.42159 0.12398
## 9 10 0.58749 0.08508 0.41251 0.12061
## 10 11 0.59628 0.08149 0.40372 0.11730
## 11 12 0.60478 0.07810 0.39522 0.11407
## 12 13 0.61298 0.07489 0.38702 0.11092
## 13 14 0.62090 0.07186 0.37910 0.10786
## 14 15 0.62854 0.06899 0.37146 0.10489
## 15 16 0.63592 0.06628 0.36408 0.10200
## 16 17 0.64303 0.06371 0.35697 0.09921
## 17 18 0.64990 0.06129 0.35010 0.09650
## 18 19 0.65652 0.05899 0.34348 0.09389
## 19 20 0.66291 0.05682 0.33709 0.09136
Se observa que la elección de los parámetros iniciales tiene un impacto significativo en el proceso de aprendizaje de la red neuronal.
En el segundo experimento, se tiene que el valor inicial de la predicción es cercano a 0.49, más alejado del valor objetivo en comparación con el experimento original.
A lo largo de las épocas, la predicción aumenta progresivamente hasta aproximadamente 0.66, mostrando que el modelo logra aprender, pero a una velocidad diferente. La función de costo disminuye de 0.127 a 0.056, lo que indica una mejora significativa, aunque partiendo de un error inicial mayor.
El error absoluto también presenta una reducción sostenida, pasando de aproximadamente 0.50 a 0.33. Asimismo, la magnitud del gradiente disminuye gradualmente, lo que refleja un proceso de convergencia estable.
En comparación con el experimento original, este caso presenta una convergencia más lenta y un mayor error inicial, lo que evidencia que la elección de los parámetros iniciales influye directamente en la velocidad de aprendizaje y en la eficiencia del entrenamiento.
En esta sección se extiende la implementación anterior para entrenar la red neuronal usando dos observaciones de la tabla XOR. A diferencia del caso anterior, ahora el entrenamiento se realiza mediante una representación matricial, lo que permite procesar más de una observación al mismo tiempo.
Las observaciones utilizadas son:
\[ (0,0)\rightarrow 0 \]
\[ (0,1)\rightarrow 1 \]
Por lo tanto, la matriz de entrada y el vector objetivo son:
\[ X = \begin{bmatrix} 0 & 0 \\ 0 & 1 \end{bmatrix} \]
\[ y = \begin{bmatrix} 0 \\ 1 \end{bmatrix} \]
El entrenamiento queda de la siguiente manera:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Función Sigmoid
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(a):
return a * (1 - a)
# Matriz de entrada y vector
X = np.array([
[0, 0],
[0, 1]
])
y = np.array([
[0],
[1]
])
# Parámetros iniciales
W_hidden = np.array([
[0.10000, 0.50000],
[-0.70000, 0.30000]
])
b_hidden = np.array([[0.00000, 0.00000]])
W_output = np.array([
[0.20000],
[0.40000]
])
b_output = np.array([[0.00000]])
eta = 0.25
epochs = 20
history_two_obs = []
for epoch in range(1, epochs + 1):
# Forward pass
Z_hidden = X @ W_hidden + b_hidden
H = sigmoid(Z_hidden)
Z_output = H @ W_output + b_output
y_hat = sigmoid(Z_output)
# Loss
errors = y - y_hat
loss = np.mean(0.5 * errors**2)
abs_error = np.mean(np.abs(errors))
# Backward pass
delta_output = (y_hat - y) * sigmoid_derivative(y_hat)
dW_output = H.T @ delta_output / X.shape[0]
db_output = np.mean(delta_output, axis=0, keepdims=True)
delta_hidden = (delta_output @ W_output.T) * sigmoid_derivative(H)
dW_hidden = X.T @ delta_hidden / X.shape[0]
db_hidden = np.mean(delta_hidden, axis=0, keepdims=True)
gradient_magnitude = np.sqrt(
np.sum(dW_hidden**2) + np.sum(dW_output**2) +
np.sum(db_hidden**2) + np.sum(db_output**2)
)
# Actualización de parámetros
W_hidden -= eta * dW_hidden
b_hidden -= eta * db_hidden
W_output -= eta * dW_output
b_output -= eta * db_output
history_two_obs.append({
"Epoch": epoch,
"Mean Prediction": float(np.mean(y_hat)),
"Prediction obs 1": float(y_hat[0, 0]),
"Prediction obs 2": float(y_hat[1, 0]),
"Mean Absolute Error": float(abs_error),
"Loss": float(loss),
"Gradient Magnitude": float(gradient_magnitude),
"W_hidden_11": float(W_hidden[0, 0]),
"W_hidden_12": float(W_hidden[0, 1]),
"W_hidden_21": float(W_hidden[1, 0]),
"W_hidden_22": float(W_hidden[1, 1]),
"W_output_1": float(W_output[0, 0]),
"W_output_2": float(W_output[1, 0]),
"b_hidden_1": float(b_hidden[0, 0]),
"b_hidden_2": float(b_hidden[0, 1]),
"b_output": float(b_output[0, 0])
})
results_two_obs = pd.DataFrame(history_two_obs)
results_two_obs.round(5)
## Epoch Mean Prediction Prediction obs 1 Prediction obs 2 Mean Absolute Error Loss Gradient Magnitude W_hidden_11 W_hidden_12 W_hidden_21 W_hidden_22 W_output_1 W_output_2 b_hidden_1 b_hidden_2 b_output
## 0 1 0.57397 0.57444 0.57350 0.50047 0.12797 0.02656 0.1 0.5 -0.69942 0.30128 0.19555 0.39871 -0.00030 -0.00048 -0.00451
## 1 2 0.57225 0.57262 0.57187 0.50037 0.12780 0.02613 0.1 0.5 -0.69885 0.30255 0.19114 0.39749 -0.00059 -0.00095 -0.00893
## 2 3 0.57056 0.57084 0.57028 0.50028 0.12763 0.02571 0.1 0.5 -0.69830 0.30383 0.18677 0.39632 -0.00087 -0.00141 -0.01324
## 3 4 0.56891 0.56909 0.56873 0.50018 0.12746 0.02530 0.1 0.5 -0.69775 0.30511 0.18244 0.39520 -0.00113 -0.00186 -0.01747
## 4 5 0.56729 0.56737 0.56720 0.50008 0.12730 0.02490 0.1 0.5 -0.69721 0.30639 0.17816 0.39415 -0.00139 -0.00229 -0.02159
## 5 6 0.56570 0.56569 0.56571 0.49999 0.12715 0.02450 0.1 0.5 -0.69668 0.30768 0.17390 0.39315 -0.00164 -0.00272 -0.02563
## 6 7 0.56415 0.56404 0.56426 0.49989 0.12700 0.02412 0.1 0.5 -0.69617 0.30896 0.16969 0.39220 -0.00187 -0.00314 -0.02958
## 7 8 0.56262 0.56242 0.56283 0.49979 0.12686 0.02374 0.1 0.5 -0.69566 0.31025 0.16552 0.39130 -0.00210 -0.00355 -0.03343
## 8 9 0.56114 0.56083 0.56144 0.49970 0.12672 0.02338 0.1 0.5 -0.69516 0.31154 0.16138 0.39046 -0.00232 -0.00395 -0.03720
## 9 10 0.55968 0.55928 0.56007 0.49960 0.12658 0.02302 0.1 0.5 -0.69468 0.31283 0.15728 0.38967 -0.00253 -0.00434 -0.04088
## 10 11 0.55825 0.55776 0.55874 0.49951 0.12645 0.02267 0.1 0.5 -0.69421 0.31413 0.15321 0.38893 -0.00273 -0.00472 -0.04448
## 11 12 0.55685 0.55627 0.55744 0.49941 0.12632 0.02233 0.1 0.5 -0.69374 0.31542 0.14918 0.38824 -0.00292 -0.00509 -0.04800
## 12 13 0.55549 0.55481 0.55617 0.49932 0.12620 0.02200 0.1 0.5 -0.69329 0.31672 0.14518 0.38760 -0.00311 -0.00546 -0.05143
## 13 14 0.55415 0.55338 0.55492 0.49923 0.12608 0.02168 0.1 0.5 -0.69284 0.31802 0.14121 0.38701 -0.00329 -0.00581 -0.05479
## 14 15 0.55284 0.55197 0.55371 0.49913 0.12596 0.02136 0.1 0.5 -0.69241 0.31932 0.13728 0.38646 -0.00346 -0.00616 -0.05806
## 15 16 0.55156 0.55060 0.55252 0.49904 0.12585 0.02106 0.1 0.5 -0.69199 0.32063 0.13338 0.38596 -0.00362 -0.00650 -0.06126
## 16 17 0.55031 0.54925 0.55136 0.49895 0.12574 0.02076 0.1 0.5 -0.69158 0.32193 0.12952 0.38551 -0.00378 -0.00684 -0.06439
## 17 18 0.54908 0.54794 0.55023 0.49885 0.12563 0.02047 0.1 0.5 -0.69118 0.32324 0.12568 0.38510 -0.00393 -0.00716 -0.06744
## 18 19 0.54788 0.54665 0.54912 0.49876 0.12553 0.02019 0.1 0.5 -0.69079 0.32455 0.12188 0.38473 -0.00407 -0.00748 -0.07042
## 19 20 0.54671 0.54538 0.54804 0.49867 0.12543 0.01992 0.1 0.5 -0.69041 0.32586 0.11810 0.38441 -0.00420 -0.00780 -0.07333
El forward pass se expresa en forma matricial como:
\[ Z_h = XW_h + b_h \]
\[ H = \sigma(Z_h) \]
\[ Z_o = HW_o + b_o \]
\[ \hat{y} = \sigma(Z_o) \]
El backward pass también se calcula de forma matricial. Primero se obtiene el error local de la capa de salida:
\[ \delta_o = (\hat{y}-y)\hat{y}(1-\hat{y}) \]
Luego, los gradientes de la capa de salida son:
\[ \frac{\partial E}{\partial W_o} = \frac{1}{n}H^T\delta_o \]
\[ \frac{\partial E}{\partial b_o} = \frac{1}{n}\sum \delta_o \]
Para la capa oculta:
\[ \delta_h = (\delta_o W_o^T)H(1-H) \]
\[ \frac{\partial E}{\partial W_h} = \frac{1}{n}X^T\delta_h \]
\[ \frac{\partial E}{\partial b_h} = \frac{1}{n}\sum \delta_h \]
Finalmente, los parámetros se actualizan mediante descenso por gradiente:
\[ W = W - \eta \frac{\partial E}{\partial W} \]
\[ b = b - \eta \frac{\partial E}{\partial b} \]
Las gráficas también cambian:
plt.figure()
plt.plot(results_two_obs["Epoch"], results_two_obs["Loss"], marker="o")
plt.xlabel("Epoch")
plt.ylabel("Cost")
plt.title("Función de costo con dos observaciones XOR")
plt.grid(True)
plt.tight_layout()
plt.show()
En la gráfica de la función de costo se observa una disminución muy
leve, pasando aproximadamente de 0.128 a 0.125. Esto indica que el
modelo sí está aprendiendo, pero a una velocidad más lenta. A diferencia
del caso anterior, la red debe ajustar sus parámetros para minimizar el
error de dos observaciones simultáneamente, lo que hace el problema más
restrictivo.
plt.figure()
plt.plot(results_two_obs["Epoch"], results_two_obs["Mean Absolute Error"], marker="o")
plt.xlabel("Epoch")
plt.ylabel("Mean Absolute Error")
plt.title("Error absoluto promedio con dos observaciones XOR")
plt.grid(True)
plt.tight_layout()
plt.show()
La gráfica del error absoluto promedio se mantiene cercana a 0.5, con
una ligera tendencia decreciente. Esto sugiere que el modelo aún no
logra diferenciar completamente entre ambas salidas, ya que está
tratando de balancear la predicción entre un valor cercano a 0 y otro
cercano a 1.
plt.figure()
plt.plot(results_two_obs["Epoch"], results_two_obs["Gradient Magnitude"], marker="o")
plt.xlabel("Epoch")
plt.ylabel("Gradient Magnitude")
plt.title("Magnitud del gradiente con dos observaciones XOR")
plt.grid(True)
plt.tight_layout()
plt.show()
En la gráfica de la magnitud del gradiente se observa una disminución
progresiva, lo que indica que el proceso de optimización es estable. Sin
embargo, el hecho de que los gradientes disminuyan sin una reducción
significativa del error sugiere que el modelo se está moviendo hacia una
solución subóptima.
En conjunto, estas gráficas muestran que, aunque la red neuronal es capaz de aprender parcialmente, el uso de solo dos observaciones del problema XOR no es suficiente para capturar completamente el patrón no lineal. Esto evidencia la necesidad de utilizar el conjunto completo de datos para que el modelo pueda aprender adecuadamente la función XOR.
En esta sección se comparan los resultados obtenidos en los diferentes experimentos realizados: el entrenamiento con una sola observación, el mismo experimento con diferentes parámetros iniciales, y el entrenamiento con dos observaciones del problema XOR.
En el caso de una sola observación, la red neuronal logra aprender de manera efectiva, reduciendo significativamente la función de costo y el error absoluto a lo largo de las épocas. Esto ocurre porque el modelo solo necesita ajustarse a un único patrón, lo que facilita el proceso de optimización. Por otro lado, cuando se utilizan diferentes parámetros iniciales, se observa que la convergencia del modelo depende en gran medida de estos valores. Algunas inicializaciones conducen a un aprendizaje más rápido, mientras que otras generan un proceso más lento o con mayor error inicial. Esto demuestra que el punto de partida influye directamente en la eficiencia del entrenamiento.
En el caso del entrenamiento con dos observaciones, el comportamiento es más complejo. Aunque la función de costo y el error disminuyen, lo hacen de manera más lenta. Esto se debe a que el modelo debe encontrar parámetros que funcionen adecuadamente para ambas observaciones al mismo tiempo.Además, el número de observaciones afecta directamente el proceso de entrenamiento; con más datos, el modelo tiene más información para aprender, pero también enfrenta una tarea más difícil, ya que debe generalizar en lugar de ajustarse a un solo ejemplo. Esto puede hacer que la convergencia sea más lenta, pero conduce a un modelo más representativo.
En cuanto al uso de una capa oculta, el problema XOR requiere una capa oculta porque no es linealmente separable. Una red sin capa oculta no podría representar correctamente la relación entre las entradas y las salidas, esta permite introducir no linealidad mediante la función de activación, lo que hace posible modelar este tipo de problemas.
Finalmente, el uso de solo una parte de la tabla XOR limita la capacidad del modelo para aprender el patrón completo. Aunque la red puede ajustarse parcialmente, no logra capturar la estructura total del problema. Esto evidencia la importancia de utilizar el conjunto completo de datos para obtener un modelo adecuado.
El proceso de entrenamiento de una red neuronal puede entenderse como un mecanismo mediante el cual el modelo aprende a partir de sus errores. En cada iteración, la red genera una predicción \(\hat{y}\) a partir de las entradas. Esta predicción se compara con el valor real \(y\), lo que permite calcular el error.
A partir de esta diferencia, se define una función de pérdida que cuantifica qué tan lejos está la predicción del valor objetivo. En este caso, se utilizó la función de error cuadrático, la cual penaliza las diferencias entre \(\hat{y}\) y \(y\).
El siguiente paso consiste en calcular el gradiente de la función de pérdida con respecto a cada uno de los parámetros del modelo. Este gradiente indica la dirección en la cual los parámetros deben ajustarse para reducir el error. Mediante el algoritmo de backpropagation, este proceso se realiza desde la capa de salida hacia las capas anteriores, distribuyendo el error a lo largo de toda la red.
Una vez calculados los gradientes, los parámetros se actualizan utilizando descenso por gradiente. Este proceso ajusta los pesos y sesgos en pequeñas cantidades, moviendo el modelo en la dirección que reduce la función de pérdida.
Luego, a lo largo de múltiples épocas, este ciclo se repite: predicción, cálculo del error, obtención de gradientes y actualización de parámetros; como resultado, la red mejora progresivamente su capacidad de aproximar la función objetivo.
En este ejemplo, se observa cómo la predicción \(\hat{y}\) se acerca gradualmente al valor real \(y\), mientras que la función de pérdida y el error disminuyen. Esto evidencia que la red está aprendiendo de sus errores y ajustando sus parámetros de manera iterativa.
En síntesis, el aprendizaje en una red neuronal es un proceso continuo de corrección, en el que el modelo utiliza la información del error para modificar su comportamiento y mejorar su desempeño con cada iteración.
En esta sección se entrena la red neuronal utilizando la tabla completa del problema XOR. Las cuatro combinaciones posibles de entrada son:
\[ (0,0)\rightarrow 0,\quad (0,1)\rightarrow 1,\quad (1,0)\rightarrow 1,\quad (1,1)\rightarrow 0 \]
Por lo tanto, la matriz de entrada y el vector objetivo son:
\[ X = \begin{bmatrix} 0 & 0 \\ 0 & 1 \\ 1 & 0 \\ 1 & 1 \end{bmatrix} \]
\[ y = \begin{bmatrix} 0 \\ 1 \\ 1 \\ 0 \end{bmatrix} \]
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
def sigmoid(z):
return 1 / (1 + np.exp(-z))
def sigmoid_derivative(a):
return a * (1 - a)
# Full XOR dataset
X = np.array([
[0,0],
[0,1],
[1,0],
[1,1]
])
y = np.array([
[0],
[1],
[1],
[0]
])
# Parámetros iniciales
W_hidden = np.array([
[0.10, 0.50],
[-0.70, 0.30]
])
b_hidden = np.array([[0.0, 0.0]])
W_output = np.array([
[0.20],
[0.40]
])
b_output = np.array([[0.0]])
eta = 0.25
epochs = 200 # más épocas porque XOR completo es más difícil
history_full = []
for epoch in range(1, epochs + 1):
# Forward
Z_h = X @ W_hidden + b_hidden
H = sigmoid(Z_h)
Z_o = H @ W_output + b_output
y_hat = sigmoid(Z_o)
errors = y - y_hat
loss = np.mean(0.5 * errors**2)
# Backprop
delta_o = (y_hat - y) * sigmoid_derivative(y_hat)
dW_output = H.T @ delta_o / X.shape[0]
db_output = np.mean(delta_o, axis=0, keepdims=True)
delta_h = (delta_o @ W_output.T) * sigmoid_derivative(H)
dW_hidden = X.T @ delta_h / X.shape[0]
db_hidden = np.mean(delta_h, axis=0, keepdims=True)
# Actualización
W_hidden -= eta * dW_hidden
b_hidden -= eta * db_hidden
W_output -= eta * dW_output
b_output -= eta * db_output
history_full.append({
"Epoch": epoch,
"Loss": float(loss)
})
results_full = pd.DataFrame(history_full)
results_full.tail()
## Epoch Loss
## 195 196 0.124941
## 196 197 0.124941
## 197 198 0.124941
## 198 199 0.124941
## 199 200 0.124941
El costo final se estabiliza alrededor de \(0.1249\), lo cual indica que la red no logró aprender completamente el patrón XOR con esta configuración inicial y este número de épocas. Aunque la función de costo disminuye ligeramente al inicio, luego se mantiene prácticamente constante, sugiriendo que el modelo quedó atrapado en una solución subóptima. Esto se observa claramente en la gráfica de la función de costo - XOR completo que se muestra a continuación:
plt.figure()
plt.plot(results_full["Epoch"], results_full["Loss"])
plt.xlabel("Epoch")
plt.ylabel("Cost")
plt.title("Evolución de la función de costo - XOR completo")
plt.grid(True)
plt.tight_layout()
plt.show()
print("Predicciones finales:")
## Predicciones finales:
print(y_hat.round(3))
## [[0.496]
## [0.497]
## [0.506]
## [0.506]]
predictions = (y_hat > 0.5).astype(int)
print("Clasificación final:")
## Clasificación final:
print(predictions)
## [[0]
## [0]
## [1]
## [1]]
La clasificación final obtenida por la red neuronal es:
\[ \begin{bmatrix} 0 \\ 0 \\ 1 \\ 1 \end{bmatrix} \]
Sin embargo, esta no coincide con la tabla real del problema XOR:
\[ \begin{bmatrix} 0 \\ 1 \\ 1 \\ 0 \end{bmatrix} \]
Este resultado indica que el modelo no logró aprender correctamente el patrón no lineal del problema. En lugar de capturar la relación XOR, la red parece haber aprendido una regla más simple, similar a una separación lineal basada en una de las variables de entrada.
Adicionalmente, las predicciones continuas obtenidas se encuentran cercanas a 0.5 para todas las observaciones, lo que evidencia que el modelo no logra diferenciar claramente entre las clases. Este comportamiento es consistente con el valor de la función de costo observado, cercano a 0.125, el cual corresponde a una solución en la que la red produce predicciones aproximadamente constantes.
En consecuencia, aunque la arquitectura de la red tiene la capacidad teórica de representar la función XOR, en este experimento el modelo no logra converger a una solución adecuada. Esto resalta la importancia de factores como la inicialización de los parámetros, la tasa de aprendizaje y el número de épocas en el proceso de entrenamiento, ya que estos pueden determinar si el modelo alcanza o no una solución óptima.
Este documento fue desarrollado utilizando un entorno reproducible basado en R Markdown con integración de código en Python mediante la librería . Todos los resultados presentados, incluyendo tablas y gráficas, se generan automáticamente a partir del código incluido en el documento.
Para garantizar la reproducibilidad, se utilizaron versiones específicas de las librerías principales empleadas en la implementación. En particular, se utilizaron:
import sys
import numpy
import pandas
import matplotlib
import pandas as pd
versions = pd.DataFrame({
"Library": ["Python", "numpy", "pandas", "matplotlib"],
"Version": [
sys.version.split()[0],
numpy.__version__,
pandas.__version__,
matplotlib.__version__
]
})
versions
## Library Version
## 0 Python 3.10.20
## 1 numpy 2.2.6
## 2 pandas 2.3.3
## 3 matplotlib 3.10.8
El entorno de ejecución fue configurado mediante un entorno de Conda (), el cual contiene todas las dependencias necesarias para ejecutar correctamente el código. La conexión entre R y Python se realizó mediante la librería , asegurando que todos los chunks de Python se ejecuten dentro del mismo entorno controlado.
No se utilizaron librerías de redes neuronales ni herramientas de diferenciación automática como , o , cumpliendo así con los requisitos de la actividad.
Además, todos los datos utilizados en este trabajo son determinísticos y están definidos explícitamente dentro del documento, lo que permite replicar exactamente los resultados obtenidos sin depender de fuentes externas.
Finalmente, el documento puede ser compilado en formatos HTML y PDF, generando los mismos resultados siempre que se utilice el mismo entorno de ejecución y versiones de librerías.