Pourquoi un seul ReLU ne peut-il pas apprendre un ReLU?

15

Comme suivi de Mon réseau de neurones ne peut même pas apprendre la distance euclidienne, j'ai encore simplifié et essayé de former un seul ReLU (avec un poids aléatoire) à un seul ReLU. Il s'agit du réseau le plus simple qui existe et pourtant, la moitié du temps, il ne parvient pas à converger.

Si la supposition initiale est dans la même orientation que la cible, elle apprend rapidement et converge vers le poids correct de 1:

animation de ReLU apprenant ReLU

courbe de perte montrant les points de convergence

Si la supposition initiale est "à l'envers", elle reste bloquée à un poids de zéro et ne la traverse jamais jusqu'à la région de perte la plus faible:

animation de ReLU échouant à apprendre ReLU

courbe de perte de ReLU ne réussissant pas à apprendre ReLU

gros plan de la courbe de perte à 0

Je ne comprends pas pourquoi. La descente de gradient ne devrait-elle pas suivre facilement la courbe de perte jusqu'aux minima globaux?

Exemple de code:

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()

entrez la description de l'image ici

Des choses similaires se produisent si j'ajoute un biais: la fonction de perte 2D est lisse et simple, mais si la relu démarre à l'envers, elle tourne autour et se coince (points de départ rouges), et ne suit pas le gradient au minimum (comme ça fait pour les points de départ bleus):

entrez la description de l'image ici

Des choses similaires se produisent si j'ajoute également le poids et le biais de sortie. (Il basculera de gauche à droite ou de bas en haut, mais pas les deux.)

endolith
la source
3
@Sycorax Non, ce n'est pas un doublon, il pose des questions sur un problème spécifique, pas des conseils généraux. J'ai passé beaucoup de temps à réduire cela à un exemple minimal, complet et vérifiable. Veuillez ne pas le supprimer simplement parce qu'il est vaguement similaire à une autre question trop large. L'une des étapes de la réponse acceptée à cette question est "Tout d'abord, créez un petit réseau avec une seule couche cachée et vérifiez qu'il fonctionne correctement. Ajoutez ensuite de manière incrémentielle la complexité supplémentaire du modèle et vérifiez que chacun d'eux fonctionne également." C'est exactement ce que je fais et cela ne fonctionne pas.
endolith
2
J'apprécie vraiment cette "série" sur NN appliquée à des fonctions simples: eats_popcorn_gif:
Cam.Davidson.Pilon
ReLU fonctionne comme un redresseur idéal, par exemple une diode. C'est unidirectionnel. Si vous souhaitez que la direction soit correcte, envisagez d'utiliser softplus, puis de passer à ReLU lorsque la formation est positive, ou d'utiliser une autre variante comme les ELU.
Carl
x<0x<0
1
X

Réponses:

14

ww=0w=0w=1w est initialisé pour être négatif, il est possible de converger vers une solution sous-optimale.

minw,bf(x)y22f(x)=max(0,wx+b)

f

f(x)={w,if x>00,if x<0

w<00w=1|w|

w(0)<0w(i)=0

Ceci est lié au phénomène relu mourant; pour une discussion, voir Mon réseau ReLU ne parvient pas à se lancer

Une approche qui pourrait être plus efficace consisterait à utiliser une non-linéarité différente telle que la relu qui fuit, qui n'a pas le problème dit de "gradient de fuite". La fonction relu qui fuit est

g(x)={x,if x>0cx,otherwise
c|c|

g(x)={1,if x>0c,if x<0

c=0c0.10.3c<0c=1,|c|>1 , les compositions de plusieurs de ces couches peuvent provoquer une explosion des gradients car les gradients deviennent plus grands dans les couches successives.)

wLeakyReLUReLUw=1

LeakyReLU résout le problème

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()

w w(0) et de tailles de pas de descente de gradient suffisamment grandes pour "sauter" sur le gradient de fuite.

w(0)=10

D'un autre côté, si vous changez l'initialisation en w(0)=1 w(0)=1w(0)=1

Le code correspondant est ci-dessous; utiliser opt_sgdou 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)
Sycorax dit de réintégrer Monica
la source
J'ai vu le même problème avec LeakyReLU, ELU, SELU quand j'avais un poids et un biais de sortie, mais je ne sais pas si j'ai essayé ceux sans sortie. Je vais vérifier
endolith
1
(Oui, vous avez raison de dire que LeakyReLU et ELU fonctionnent bien pour cet exemple)
endolith
2
Oh je comprends. Il fait une descente en gradient de la fonction de perte, c'est juste que la fonction de perte devient plate (gradient 0) à 0 en s'approchant du côté négatif, de sorte que la descente en gradient y reste bloquée. Maintenant, cela semble évident. : D
endolith
2
ww=0
2
Lors de l'utilisation de l'activation relu, même SGD sans élan peut passer sur la lèvre si la taille du pas est suffisamment grande pour quelle que soit la valeur spécifique dew(i)