Mi simple perceptrón multicapa (MLP) totalmente conectado que estoy escribiendo con fines académicos me está causando privación del sueño.
No puedo averiguar por qué mi MLP aprende mal, incluso si intento resolver problemas de monje donde el rendimiento (en términos de precisión) se entiende bien.
Me pregunto si la causa real es la forma en que está implementado el algoritmo de retropropagación.
El código es simple, aquí doy las funciones principales que implementan el backprop.
Aquí el forward
función:
def forward(self, x, y, n):
"""
x: the input vector representig the input signal
y: the target vector, the true output
n: the lenght of vectors ``x`` and ``y``
"""
o = self.propagateSignal(x)
loss = self.objective.apply(y, o)/n
penalty = self._lambda*np.sum([np.sum(np.linalg.norm(layer.W, 2)**2) for layer in self.nnet])
return (loss + penalty)
donde propagateSignal
se define como:
def propagateSignal(self, x):
_h = x
for layer in self.nnet:
_h = layer.apply(_h)
return _h
A continuación, el paso de la regla de la cadena implementado por el backward
función
def backward(self, y, n):
# after the forward computation, compute the gradient on the output layer
o = self.nnet[-1].h # last layer's output
g = self.objective.gradient(y, o)/n
for layer, grad in zip(reversed(self.nnet), reversed(self.gradients)): # starting backward we reconstruct error with respect to each weight
# convert the gradient on the layer's output into a gradient into
# the pre-nonlinearity activation (element-wise (``hadamard``) multiplication if f is elementwise)
g = g*layer.activation.dh(layer.a)
# compute gradients on weights and biases (including the regularization term,
# where needed):
grad.nablab += g
grad.nablaW += np.outer(g, layer._h) + 2*self._lambda*layer.W
# propagate the gradients w.r.t. the next lower-level hidden layer's activations
g = np.dot(layer.W.T, g)
y finalmente el descent
utilizada para actualizar los pesos:
def descent(self):
for layer, grad in zip(self.nnet, self.gradients):
nablaW_new = self.alpha*grad.nablaW_old + self.eta*(grad.nablaW)
nablab_new = self.alpha*grad.nablab_old + self.eta*(grad.nablab)
layer.W = layer.W - nablaW_new
layer.b = layer.b - nablab_new
grad.nablaW_old, grad.nablab_old = nablaW_new, nablab_new
# we clear-up the sum of gradients with respect the last seen example in the case of online mode or
# in the last epoch (complete pass on dataset) for batch-mode
for grad in self.gradients:
grad.cleargrad()
[EDITAR]:
En aras de la claridad, emparejo el código con las matemáticas. Como ya he señalado las matemáticas se toman de "Aprendizaje profundo" libro de Goodfellow-Bengio-Courville en las páginas 208-209 de Capítulo 6 .
Veamos primero qué ocurre en el forward
fase.
Sea $W^{(i)}$ y $b^{(i)}$ , $i\in\{1,...,l\}$ son, respectivamente, las matrices de pesos y los vectores de sesgo de cada capa del modelo. Sea también $x$ sea la señal de entrada y $y$ la salida de destino.
Sea $ h^{(0)} = x $ entonces $\forall~ k = 1,..., l$ :
$$ a^{(k)} = b^{(k)} + W^{(k)}h^{k-1} $$
$$ h^{(k)} = f(a^{(k)}) $$
def propagateSignal(self, x):
_h = x
for layer in self.nnet:
_h = layer.apply(_h)
return _h
Sea $\hat{y} = h^{(l)}$ sea la salida de la última capa, entonces la función de coste total podría calcularse de la siguiente manera
$$ J = L(y, \hat{y}) + \lambda\Omega(\theta) $$
donde $L$ es la función de pérdida añadida a un regularizador $\Omega(\theta)$ definido sobre las variables libres de la red (pesos y sesgos).
def forward(self, x, y, n):
"""
x: the input vector representig the input signal
y: the target vector, the true output
n: the lenght of vectors ``x`` and ``y``
"""
o = self.propagateSignal(x)
loss = self.objective.apply(y, o)/n
penalty = self._lambda*np.sum([np.sum(np.linalg.norm(layer.W, 2)**2 + np.linalg.norm(layer.b))**2 for layer in self.nnet])
return (loss + penalty)
Veamos entonces el backward
parte.
Después del cálculo hacia delante, calcule el gradiente en la capa de salida:
$ g \leftarrow \nabla_{\hat{y}}J = \nabla_{\hat{y}}L(y, \hat{y})$
o = self.nnet[-1].h # last layer's output
g = self.objective.gradient(y, o)/n
Entonces $\forall~k=l,l-1,...,1: $
Convierte el gradiente en la salida de la capa en un gradiente en la activación de pre-nonlinealidad (multiplicación elemento a elemento si f es elemento a elemento):
$ g \leftarrow \nabla_{a^{(k)}}J = g\odot f'(a^{(k)}) $
g = g*layer.activation.dh(layer.a)
Calcular gradientes en pesos y sesgos (incluyendo el término de regularización, cuando sea necesario):
$ \nabla_{b^{(k)}} J = g + \lambda\nabla_{b^{(k)}}\Omega(\theta) $
grad.nablab += g + 2*self._lambda*layer.b
$ \nabla_{W^{(k)}} J = gh^{(k-1)T} + \lambda\nabla_{W^{(k)}}\Omega(\theta)$
grad.nablaW += np.outer(g, layer._h) + 2*self._lambda*layer.W
Propagar los gradientes con respecto a las activaciones de la capa oculta inmediatamente inferior.
$ g \leftarrow \nabla_{h^{(k-1)}}J = W^{(k)T}g$
A continuación, las ponderaciones se actualizan mediante descent
como se muestra al principio.
[EDITAR]:
No sé si las matemáticas que he utilizado y su aplicación son correctas o no. Intentando resolver las tareas de clasificación dadas en "Los problemas del Monje" no veo la convergencia esperada en el conjunto de pruebas, es decir, por ejemplo, una precisión del 100% en el problema Monje 1. Lo que es más extraño es que el error de validación/prueba es a veces o siempre (¿dependiendo de cómo se inicialicen los pesos?) inferior al de entrenamiento.
Las capas ocultas y la capa de salida tienen funciones de activación sigmoideas, y minimizo la pérdida euclídea media.
Así que quiero estar seguro de lo que he implementado.