Loading [MathJax]/jax/element/mml/optable/BasicLatin.js

25 votos

¿Por qué una sola ReLU no puede aprender una ReLU?

Como continuación de Mi red neuronal ni siquiera puede aprender la distancia euclidiana Simplifiqué aún más e intenté entrenar un único ReLU (con peso aleatorio) a un único ReLU. Es la red más sencilla que existe y, sin embargo, la mitad de las veces no converge.

Si la suposición inicial está en la misma orientación que el objetivo, aprende rápidamente y converge al peso correcto de 1:

animation of ReLU learning ReLU

loss curve showing convergence points

Si la conjetura inicial es "al revés", se atasca en un peso de cero y nunca lo atraviesa hasta la región de menor pérdida:

animation of ReLU failing to learn ReLU

loss curve of ReLU failing to learn ReLU

closeup of loss curve at 0

No entiendo por qué. ¿No debería el descenso por gradiente seguir fácilmente la curva de pérdida hasta los mínimos globales?

Código de ejemplo:

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, ReLU
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt

batch = 1000

def tests():
    while True:
        test = np.random.randn(batch)

        # Generate ReLU test case
        X = test
        Y = test.copy()
        Y[Y < 0] = 0

        yield X, Y

model = Sequential([Dense(1, input_dim=1, activation=None, use_bias=False)])
model.add(ReLU())
model.set_weights([[[-10]]])

model.compile(loss='mean_squared_error', optimizer='sgd')

class LossHistory(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.losses = []
        self.weights = []
        self.n = 0
        self.n += 1

    def on_epoch_end(self, batch, logs={}):
        self.losses.append(logs.get('loss'))
        w = model.get_weights()
        self.weights.append([x.flatten()[0] for x in w])
        self.n += 1

history = LossHistory()

model.fit_generator(tests(), steps_per_epoch=100, epochs=20,
                    callbacks=[history])

fig, (ax1, ax2) = plt.subplots(2, 1, True, num='Learning')

ax1.set_title('ReLU learning ReLU')
ax1.semilogy(history.losses)
ax1.set_ylabel('Loss')
ax1.grid(True, which="both")
ax1.margins(0, 0.05)

ax2.plot(history.weights)
ax2.set_ylabel('Weight')
ax2.set_xlabel('Epoch')
ax2.grid(True, which="both")
ax2.margins(0, 0.05)

plt.tight_layout()
plt.show()

enter image description here

Ocurren cosas similares si añado sesgo: la función de pérdida 2D es suave y sencilla, pero si el relu empieza al revés, da vueltas y se atasca (puntos de partida rojos), y no sigue el gradiente hasta el mínimo (como hace con los puntos de partida azules):

enter image description here

Ocurren cosas similares si también añado peso de salida y sesgo. (Se volteará de izquierda a derecha, o de abajo a arriba, pero no ambas cosas).

22voto

user777 Puntos 10934

Hay una pista en sus parcelas de la pérdida en función de w . Estas parcelas tienen un "pliegue" cerca de w=0 : eso es porque a la izquierda de 0, el gradiente de la pérdida se desvanece a 0 (sin embargo, w=0 es una solución subóptima porque la pérdida es mayor allí que para w=1 ). Además, este gráfico muestra que la función de pérdida no es convexa (se puede trazar una línea que cruce la curva de pérdida en 3 o más puntos), lo que indica que debemos ser cautos al utilizar optimizadores locales como SGD. De hecho, el siguiente análisis muestra que cuando w se inicializa en negativo, es posible converger a una solución subóptima.

El problema de optimización es min

y estás usando optimización de primer orden para hacerlo. Un problema con este enfoque es que f tiene gradiente

f^\prime(x)= \begin{cases} w, & \text{if $x>0$} \\ 0, & \text{if $x<0$} \end{cases}

Cuando se empieza con w<0 tendrás que moverte al otro lado de 0 para acercarse a la respuesta correcta, que es w=1 . Esto es difícil de hacer, porque cuando tienes |w| muy, muy pequeño, el gradiente se volverá igualmente muy pequeño. Además, cuanto más te acerques a 0 desde la izquierda, ¡más lento será tu progreso!

Esta es la razón por la que en sus parcelas para inicializaciones que son negativas w^{(0)} <0 todas tus trayectorias se estancan cerca de w^{(i)}=0 . Esto es también lo que muestra tu segunda animación.

Esto está relacionado con el fenómeno de la relu moribunda; para más información, véase Mi red ReLU no se inicia

Un enfoque que podría tener más éxito sería utilizar una no linealidad diferente, como la función leaky relu, que no tiene el llamado problema de "gradiente evanescente". La función leaky relu es

g(x)= \begin{cases} x, & \text{if $x>0$} \\ cx, & \text{otherwise} \end{cases} donde c es una constante para que |c| es pequeño y positivo. La razón de que esto funcione es que la derivada no es 0 "a la izquierda".

g^\prime(x)= \begin{cases} 1, & \text{if $x>0$} \\ c, & \text{if $x < 0$} \end{cases}

Configuración c=0 es el relu ordinario. La mayoría de la gente elige c ser algo como 0.1 ou 0.3 . No he visto c<0 utilizado, aunque me interesaría ver un estudio sobre qué efecto tiene, si es que tiene alguno, en dichas redes. (Tenga en cuenta que para c=1, se reduce a la función de identidad; para |c|>1 , las composiciones de muchas capas de este tipo pueden provocar gradientes explosivos porque los gradientes se hacen mayores en las capas sucesivas).

Modificando ligeramente el código de OP se demuestra que el problema radica en la elección de la función de activación. Este código inicializa w sea negativo y utiliza el LeakyReLU en lugar del ordinario ReLU . La pérdida disminuye rápidamente a un valor pequeño, y el peso se desplaza correctamente a w=1 que es óptimo.

LeakyReLU fixes the problem

from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, ReLU
from tensorflow import keras
import numpy as np
import matplotlib.pyplot as plt

batch = 1000

def tests():
    while True:
        test = np.random.randn(batch)

        # Generate ReLU test case
        X = test
        Y = test.copy()
        Y[Y < 0] = 0

        yield X, Y

model = Sequential(
    [Dense(1, 
           input_dim=1, 
           activation=None, 
           use_bias=False)
    ])
model.add(keras.layers.LeakyReLU(alpha=0.3))
model.set_weights([[[-10]]])

model.compile(loss='mean_squared_error', optimizer='sgd')

class LossHistory(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.losses = []
        self.weights = []
        self.n = 0
        self.n += 1

    def on_epoch_end(self, batch, logs={}):
        self.losses.append(logs.get('loss'))
        w = model.get_weights()
        self.weights.append([x.flatten()[0] for x in w])
        self.n += 1

history = LossHistory()

model.fit_generator(tests(), steps_per_epoch=100, epochs=20,
                    callbacks=[history])

fig, (ax1, ax2) = plt.subplots(2, 1, True, num='Learning')

ax1.set_title('LeakyReLU learning ReLU')
ax1.semilogy(history.losses)
ax1.set_ylabel('Loss')
ax1.grid(True, which="both")
ax1.margins(0, 0.05)

ax2.plot(history.weights)
ax2.set_ylabel('Weight')
ax2.set_xlabel('Epoch')
ax2.grid(True, which="both")
ax2.margins(0, 0.05)

plt.tight_layout()
plt.show()

Otra capa de complejidad surge del hecho de que no nos movemos infinitesimalmente, sino en "saltos" finitos, y estos saltos nos llevan de una iteración a la siguiente. Esto significa que hay algunas circunstancias en las que los valores iniciales negativos de w no se atascan; estos casos surgen para combinaciones particulares de w^{(0)} y tamaños de paso de descenso de gradiente lo suficientemente grandes como para "saltar" por encima del gradiente de fuga.

He jugado un poco con este código y he descubierto que dejar la inicialización en w^{(0)}=-10 y cambiar el optimizador de SGD a Adam, Adam + AMSGrad o SGD + momentum no ayuda en nada. Además, cambiar de SGD a Adam en realidad ralentiza el progreso además de no ayudar a superar el gradiente evanescente en este problema.

Por otra parte, si cambia la inicialización a w^{(0)}=-1 et cambiar el optimizador a Adam (tamaño de paso 0,01), entonces usted puede realmente superar el gradiente de fuga. También funciona si utiliza w^{(0)}=-1 y SGD con impulso (tamaño de paso 0,01). Incluso funciona si se utiliza SGD vainilla (tamaño de paso 0,01) y w^{(0)}=-1 .

El código correspondiente figura a continuación; utilícelo opt_sgd ou opt_adam .

opt_sgd = keras.optimizers.SGD(lr=1e-2, momentum=0.9)
opt_adam = keras.optimizers.Adam(lr=1e-2, amsgrad=True)
model.compile(loss='mean_squared_error', optimizer=opt_sgd)

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