Implementación en NUMPY de Retropropagación (Backpropagation) para un MLP simple
Notas Temas de Aprendizaje , Mariano Rivera, CIMAT © 2022
Problema: Clasificar imágenes de dos dígitos MNIST
Mariano Rivera
Versión Oct 2023
Imagen de portada generada con Stable Diffusion con el prompt: “an artistic interpretation of the backpropagation algorithm”.
Derivación del algoritmo de Backpropagation
Un Perceptrón multicapa (Multilayer Perceptrón, MLP) de una sóla capa oculta corresponde a realizar las siguientes operaciones hacia adelante:
(1)
y0=xz1=W1y0+b1y1=ϕ1(z1)z2=W2y1+b2y2=ϕ2(z2)y^=y2
Dado que el problema que nos planteamos es del tipo clasificación binaria, usaremos la Entropía Cruzada Binaria como función de costo:
(2)
L(y,y^2)=−i∑[yilog(y^i)+(1−yi)log(1−y^i)]
Donde la suma la realizamos sobre todos los datos.
Entonces:
- Gradientes de la pérdida con respecto a los pesos W2 y bias b2:
(3)
∂W2∂L=∂z2∂L∂W2∂z2=[y2−ϕ2(z2)]⊤y1∂b2∂L=∂z2∂L∂b2∂z2=[y2−ϕ2(z2)](1)
- Gradientes de la pérdida con respecto a los pesos W1 y bias b1:
(4)
∂W1∂L=∂z1∂L∂W1∂z1=∂z2∂L∂z1∂z2∂W1∂z1=[y2−ϕ2(z2)]⊤W2ϕ′(z1)y0∂b1∂L=∂z2∂L∂z1∂z2∂b1∂z1=[y2−ϕ2(z2)]⊤W2ϕ′(z1)(1)
Los detalles de ∂L/∂z2 se presentan en el Apéndice, al final de estas notas. Para una derivación mas general ver Redes Multicapa y el algoritmo de Backpropagation
Luego, implementaremos el algoritmo de Descenso de gradiente simple sobre todo el conjunto de entrenamiento para actualizar los parámetros: W1,W2,b1 y b2. Por ejemplo:
(5)
θt+1=θt−α∂θt∂L.
En nuestra implementación usaremos la sigmoide como función de activación:
(6)
ϕ(z)=1+exp(−z)1
cuya derivada es ϕ′(z)=ϕ(z)[1−ϕ(z)]; ver Apéndice.
Ejemplo de aplicación
import numpy as np
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
Cargar la base de datos MNIST
mnist = fetch_openml("mnist_784")
X, y = mnist.data, mnist.target.astype(int)
X = X[(y == 0) | (y == 1)]
y = y[(y == 0) | (y == 1)]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train = X_train / 255.0
X_test = X_test / 255.0
print(X_train.shape, y_train.shape)
print(X_test.shape, y_test.shape)
(11824, 784) (11824,)
(2956, 784) (2956,)
Función de activación
def phi(x):
return 1 / (1 + np.exp(-x))
Inicializar pesos y biases
input_size = X_train.shape[1]
hidden_size = 64
output_size = 1
W_1 = 0.1*np.random.randn(input_size, hidden_size,)
b_1 = 0.1*np.zeros((1, hidden_size))
W_2 = 0.1*np.random.randn(hidden_size,output_size,)
b_2 = 0.1*np.zeros((1,output_size))
Parámetros de entrenamiento
learning_rate = 1e-4
epochs = 30
Ciclo de entrenamiento
Cada ciclo de entrenamiento consiste en realizar los siguientes pasos:
- Evaluar la el preceptrón multicapa (modelo) con los datos en el lote actual. A este paso se denomina Propagación hacia adelante.
- Evaluar la función de pérdida.
- Calcular las derivadas del modelo respecto a sus parámetros. Paso de Retropropagación, pues reusa parte de los cálculos obtenidos en el paso 1.
- Ajustar los parámetros del modelo con el algoritmo de aprendizaje
Estos pasos se repiten hasta que alcance el criterio de paro (por iteraciones o por magnitud del gradiente). La implementacion en numpy se presenta a continuación.
Nota. En nuestros cálculos usamos las operaciones con forma de transpuesta y⊤W⊤ en vez de la estándar Wy dado que la primera emplea los datos en su formato original y simplifica la implementación.
y_0 = X_train
y = np.expand_dims(y_train, axis=-1)
Losses=[]
for epoch in range(epochs):
z_1 = y_0 @ W_1 + b_1
y_1 = phi(z_1)
z_2 = y_1 @ W_2 + b_2
y_2 = phi(z_2)
loss = -np.mean(y * np.log(y_2) + (1 - y) * np.log(1 - y_2))
delta_2 = y_2-y
delta_1 = np.dot(delta_2, W_2.T) * (y_1*(1-y_1))
grad_W2 = np.dot(y_1.T, delta_2)
grad_b2 = np.sum(delta_2)[0]
grad_W1 = np.dot(y_0.T, delta_1)
grad_b1 = np.sum(delta_1)[0]
W_2 -= learning_rate * grad_W2
b_2 -= learning_rate * grad_b2
W_1 -= learning_rate * grad_W1
b_1 -= learning_rate * grad_b1
if epoch % 1 == 0:
print(f"Epoch {epoch}, Loss: {loss}")
Losses.append([epoch,loss])
Losses = np.array(Losses)
Epoch 0, Loss: 0.7805168192126555
Epoch 1, Loss: 1.8910299637556995
Epoch 2, Loss: 2.516437887729284
Epoch 3, Loss: 0.3938748583958368
...
Epoch 28, Loss: 0.018629451129235922
Epoch 29, Loss: 0.018139000372665838
import matplotlib.pyplot as plt
plt.figure(figsize=(10,3))
plt.plot(Losses[:,0],Losses[:,1])
plt.title('Evolución de la pérdida durante entrenamiento')
Text(0.5, 1.0, 'Evolución de la pérdida durante entrenamiento')
Prueba
z_1 = X_test @ W_1 + b_1
y_1 = phi(z_1)
z_2 = y_1 @ W_2 + b_2
y_2 = phi(z_2)
predictions = np.squeeze((y_2 >= 0.5).astype(int))
y_test = np.array(y_test)
accuracy = np.mean(predictions == y_test)
print(f"Exactitud de Prueba (Test accuracy): {accuracy * 100}%")
Exactitud de Prueba (Test accuracy): 99.86468200270636%
Ejercicios Recomendados
- Complete la derivación para un perceptrón multicapa; con k>1 capas ocultas.
- Ensaye con otras funciones de activación y use distintas para ϕ1 y ϕ2.
- Implemente una estrategia de descenso estocástico.
- Evalué con otros algoritmos de entrenamiento; p.ej. Nesterov y Adam.
- Generalice el clasificador para mas clases de imágenes de dígitos: las diez clases en MNIST.
Apéndice. Derivada de la función de costo, Eq. (2)
L(y,y^2)=i∑l(ϕ([z2]i);yi).
Luego
∂z∂l(ϕ(z);y)=−∂z∂[ylog(ϕ(z)))+(1−y)log(1−ϕ(z))]=−ϕ(z)yϕ′(z)+(1−y)1−ϕ(z)ϕ′(z)=−y[1−ϕ(z)]+(1−y)ϕ(z)=−y+yϕ(z)+ϕ(z)−yϕ(z)=ϕ(z)−y.
Donde hemos usado
∂z∂ϕ(z)=∂z∂1+e−z1=(1+e−z)2e−z=(1+e−z1)(1+e−z1+e−z−1)=(1+e−z1)(1+e−z1+e−z−1+e−z1)=ϕ(z)[1−ϕ(z)].