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)