Third Epoch: Manual Computation

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()

Automated Training for 20 Epochs

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.

Sensitivity to Initial Parameters

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.

Training with Two XOR Observations

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.

Interpretation and Comparison

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.

Conceptual Reflection

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.

Full XOR Table

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.

Reproducibility

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.