1 votos

La retropropagación en el perceptrón multicapa (MLP) no converge

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.

2voto

User 007 Puntos 8

¿Has probado a aprender el decaimiento de la tasa? Esto reduce gradualmente el tamaño del paso para cada iteración, de modo que se toman pasos más grandes al principio y más pequeños después. Esto puede evitar que des saltos enormes rebotando una y otra vez alrededor del mínimo local pero sin llegar nunca a alcanzarlo.

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X