Vectorización hacia atrás¶

Pensemos en una red neuronal de alimentación hacia adelante (feedforward) de cualquier número de capas.

Sean:

  • $A$ la matriz de valores de activación, un ejemplar por cada renglón.
  • $W$ la matriz de pesos que conectan dos capas sucesivas.
  • $B$ el vector de sesgos (o los pesos conectados a $a_0$).

Para calcular el gradiente, es necesario calcular primero la propagación hacia adelante.

Alimentación hacia adelante¶

La fórmula general vectorizada para calcular el valor de la capa siguiente se puede expresar como: \begin{align*} A^{(l+1)} = g(A^{(l)} W^{(l)} + B^{(l)}) \end{align*}

Ejemplo¶

Sea la función booleana XAND:

$x_1$ $x_2$ $XAND$
0 0 1
0 1 0
1 0 0
1 1 1

Dada una red XAND

Función de activación logística¶

In [2]:
import numpy as np
In [3]:
def sigma(z):
    return 1 / (1 + np.exp(-z))

Propagación hacia adelante¶

In [4]:
# Propagación hacia adelante

## Matrices
W0 = np.array([[-5.12, 3.38],
               [-5.1, 3.37]])
B0 = np.array([[1.72, -5.25]])

W1 = np.array([[6.6],
               [6.5]])
B1 = np.array([[-3.2]])
## Activaciones
A0 = X = np.array([[0, 0],
                   [0, 1],
                   [1, 0],
                   [1, 1]])
A1 = sigma(np.dot(A0, W0) + B0)
A2 = sigma(np.dot(A1, W1) + B1)
In [10]:
print(A1)
[[8.48128836e-01 5.22012569e-03]
 [3.29263948e-02 1.32388874e-01]
 [3.22954647e-02 1.33541723e-01]
 [2.03426978e-04 8.17574476e-01]]
In [11]:
print(A2)
[[0.91920404]
 [0.10696175]
 [0.10728019]
 [0.89240796]]

Gradiente hacia atrás¶

Es verdad, para esta red, usando redondeo, ya tenemos la respuesta perfecta, pero la función de error aún nos reporta un valor y podemos calcular su gradiente con respecto a los pesos de la red.

Cálculo del gradiente del error hacia atrás¶

Función de error:¶

\begin{align} J(\Theta) = - \frac{1}{m} \left[ \sum_{i=1}^m \sum_{k=1}^{s_L} y_k^{(i)} \log(a^{(L)}_k) + (1 - y_k^{(i)}) \log(1 - A^{(L)}_k) \right] \end{align}

vectorizando: \begin{align} J(\Theta) = - \frac{1}{m} \left[ \sum_{k=1}^{s_L} Y^T \log(A^{(L)}) + (1 - Y)^T \log(1 - A^{(L)}) \right] \end{align}

In [63]:
# Valores deseados
Y = np.array([[1],
              [0],
              [0],
              [1]])
m = X.shape[0]

# Error
e = - np.sum(np.dot(Y.T, np.log(A2)) + np.dot((1 - Y).T, np.log(1 - A2))) / m
In [64]:
print("Error = ", e)
Error =  0.10617250539997253

Gradiente¶

Para calcular el gradiente con respecto a los pesos es necesario ir hacia atrás:

  • Error cometido por la última capa $L$ \begin{align} \Delta^{(L)} &= Y - A^{(L)} \end{align}
In [37]:
Delta_2 = Y - A2
In [38]:
print(Delta_2)
[[ 0.08079596]
 [-0.10696175]
 [-0.10728019]
 [ 0.10759204]]
  • Error cometido por cualquier capa $l-1$ \begin{align} \Delta^{(l-1)} &= \Delta^{(l)}\left(W^{(l-1)}\right)^T \circ g'(Z^{(l-1)}) \end{align}

Observa que el sesgo no aparece explícitamente porque no hay un error en su valor de activación, sin embargo su efecto está presente en el valor de $Z^{(l-1)}$.

Para la función sigmoide: \begin{align} g'(Z^{(l-1)}) &= A^{(l-1)} \circ (1-A^{(l-1)}) \end{align}

In [65]:
gp_1 = A1 * (1 - A1)
Delta_1 = np.dot(Delta_2, W1.T) * gp_1
In [66]:
# Error de cada neurona en la capa 1 para cada ejemplar
print(Delta_1)
[[ 0.0686864   0.00272716]
 [-0.02247896 -0.07985801]
 [-0.02212829 -0.08068588]
 [ 0.00014443  0.10430531]]
  • Componentes del gradiente con respecto a los pesos en $W^{l-1}$ \begin{align} \nabla^{(l-1)} &= -\frac{1}{m}\left(A^{(l-1)}\right)^T \Delta^{(l)} \end{align}

Parámetros W1 y B1¶

In [39]:
# Gradiente para los pesos que conectan las capas 1 y 2

G_W1 = - np.dot(A1.T, Delta_2) / m
In [40]:
print(G_W1)
[[-0.01539019]
 [-0.01497484]]
In [54]:
# Gradiente para el sesgo, pensemos en que A = 1
# Esto produce una suma sobre las columnas

G_B1 = - np.sum(Delta_2, axis=0, keepdims=True) / m
In [55]:
print(G_B1)
[[0.00646349]]

Parámetros W0 y B0¶

In [43]:
# Gradiente para los pesos que conectan las capas 0 y 1

G_W0 = - np.dot(A0.T, Delta_1) / m
In [44]:
print(G_W0)
[[ 0.00549597 -0.00590486]
 [ 0.00558363 -0.00611183]]
In [56]:
# Gradiente para el sesgo sobre la capa 1

G_B0 = - np.sum(Delta_1, axis=0, keepdims=True) / m
In [57]:
print(G_B0)
[[-0.00605589  0.01337785]]

Verificación¶

Para saber si implementamos correctamente el cálculo del gradiente realizaremos un cálculo alternativo mediante pequeñas perturbaciones a cada peso de la red. Si los valores de ambos cálculos son semejantes, entonces podemos confiar en nuestra implementación.

In [62]:
params = [np.copy(W0), np.copy(B0), np.copy(W1), np.copy(B1)]
def forward():
    global params
    # Mismas entradas A0
    A1 = sigma(np.dot(A0, params[0]) + params[1])
    A2 = sigma(np.dot(A1, params[2]) + params[3])
    return A2 
g_params = [None, None, None, None]
epsilon = 0.0001
for ind in range(len(params)):
    param = params[ind]
    g_param = np.zeros(param.shape)
    for i in range(param.shape[0]):
        for j in range(param.shape[1]):
            temp = param[i,j]

            param[i,j] = temp - epsilon
            A2 = forward()
            e_min = - np.sum(np.dot(Y.T, np.log(A2)) + np.dot((1 - Y).T, np.log(1 - A2))) / m

            param[i,j] = temp + epsilon
            A2 = forward()
            e_max = - np.sum(np.dot(Y.T, np.log(A2)) + np.dot((1 - Y).T, np.log(1 - A2))) / m

            g_param[i,j] = (e_max - e_min) / (2 * epsilon)
            param[i,j] = temp
        g_params[ind] = g_param
for g_p in g_params:
    print(g_p)
[[ 0.00549597 -0.00590486]
 [ 0.00558363 -0.00611183]]
[[-0.00605589  0.01337785]]
[[-0.01539019]
 [-0.01497484]]
[[0.00646349]]
In [58]:
print(G_W0)
print(G_B0)
print(G_W1)
print(G_B1)
[[ 0.00549597 -0.00590486]
 [ 0.00558363 -0.00611183]]
[[-0.00605589  0.01337785]]
[[-0.01539019]
 [-0.01497484]]
[[0.00646349]]

@veroarriola