Dlaczego żaden ReLU nie może nauczyć się ReLU?

15

Jako kontynuacja Mojej sieci neuronowej nie mogę nawet nauczyć się odległości euklidesowej , uprościłem jeszcze bardziej i próbowałem wyszkolić jedną jednostkę ReLU (o losowej wadze) do jednej jednostki ReLU. Jest to najprostsza z dostępnych sieci, a mimo to w połowie przypadków nie jest ona zbieżna.

Jeśli początkowe przypuszczenie jest w tej samej orientacji co cel, uczy się szybko i zbiega do prawidłowej masy 1:

animacja uczenia się ReLU ReLU

krzywa strat pokazująca punkty zbieżności

Jeśli początkowe przypuszczenie jest „wstecz”, utknie na wadze zerowej i nigdy nie przejdzie przez to do regionu o niższej stracie:

animacja ReLU nie nauczyła się ReLU

krzywa stratności ReLU nie nauczyła się ReLU

zbliżenie krzywej strat na 0

Nie rozumiem dlaczego. Czy spadek gradientu nie powinien łatwo podążać za krzywą strat do minimów globalnych?

Przykładowy kod:

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

wprowadź opis zdjęcia tutaj

Podobne rzeczy się zdarzają, jeśli dodam błąd: funkcja utraty 2D jest płynna i prosta, ale jeśli relu zaczyna się do góry nogami, krąży wokół i blokuje się (czerwone punkty początkowe) i nie podąża za gradientem do minimum (jak to dotyczy niebieskich punktów początkowych):

wprowadź opis zdjęcia tutaj

Podobne rzeczy się zdarzają, jeśli dodam również wagę wyjściową i odchylenie. (Będzie się obracać od lewej do prawej lub od dołu do góry, ale nie jedno i drugie.)

endolit
źródło
3
@Sycorax Nie, to nie jest duplikat, pyta o konkretny problem, a nie ogólną poradę. Spędziłem dużo czasu, redukując to do minimalnego, pełnego i możliwego do zweryfikowania przykładu. Nie usuwaj go tylko dlatego, że jest nieco podobny do jakiegoś innego zbyt szerokiego pytania. Jednym z kroków w zaakceptowanej odpowiedzi na to pytanie jest: „Najpierw zbuduj małą sieć z jedną ukrytą warstwą i sprawdź, czy działa poprawnie. Następnie stopniowo zwiększaj złożoność modelu i sprawdź, czy każdy z nich również działa”. Właśnie to robię i to nie działa.
endolith
2
Bardzo podoba mi się ta „seria” na NN stosowana do prostych funkcji: eats_popcorn_gif:
Cam.Davidson.Pilon
ReLU działa jak idealny prostownik, np. Dioda. Jest jednokierunkowy. Jeśli chcesz poprawić kierunek, rozważ użycie softplus, a następnie przełączenie na ReLU, gdy trening jest pozytywny, lub skorzystanie z innego wariantu, takiego jak ELU.
Carl
x<0x<0
1
x

Odpowiedzi:

14

ww=0w=0w=1w jest inicjowany jako ujemny, możliwe jest zbliżenie do rozwiązania nieoptymalnego.

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

i używasz do tego optymalizacji pierwszego rzędu. Problem z tym podejściem polega na tym, że ma gradientf

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

Kiedy zaczniesz od , będziesz musiał przejść na drugą stronę aby zbliżyć się do poprawnej odpowiedzi, czyli . Jest to trudne, ponieważ gdy maszbardzo, bardzo mały, gradient również zniknie znikomo. Co więcej, im bardziej zbliżasz się do 0 od lewej, tym wolniejszy będzie twój postęp!w<00w=1|w|

Dlatego na twoich wykresach dla inicjalizacji, które są ujemne , wszystkie trajektorie utknęły w pobliżu . To także pokazuje twoja druga animacja.w(0)<0w(i)=0

Jest to związane ze zjawiskiem reling umierania; w celu omówienia, patrz: Moja sieć ReLU nie uruchamia się

Podejście, które może być bardziej skuteczne, polegałoby na zastosowaniu innej nieliniowości, takiej jak nieszczelny relu, który nie ma tak zwanego problemu „znikającego gradientu”. Nieszczelna funkcja relu to

g(x)={x,if x>0cx,otherwise
gdzie jest stałą, więcjest mały i pozytywny. Powodem, dla którego działa to pochodna, nie jest 0 „po lewej”.c|c|

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

Ustawienie jest zwykłym relu. Większość ludzi wybiera jako lub . Nie widziałem zastosowanego , chociaż chciałbym zobaczyć badanie tego, jaki wpływ, jeśli w ogóle, ma on na takie sieci. (Zauważ, że dla ogranicza się to do funkcji tożsamości; dla kompozycje wielu takich warstw mogą powodować wybuchanie gradientów, ponieważ gradienty stają się większe w kolejnych warstwach.)c=0c0.10.3c<0c=1,|c|>1

Nieznaczna modyfikacja kodu OP zapewnia, że ​​problem leży w wyborze funkcji aktywacji. Ten kod inicjuje aby był ujemny i używa zwykłego . Strata szybko spada do niewielkiej wartości, a waga prawidłowo przesuwa się do , co jest optymalne.wLeakyReLUReLUw=1

LeakyReLU rozwiązuje 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()

Kolejna warstwa złożoności wynika z faktu, że nie poruszamy się w nieskończoność, lecz w skończonej liczbie „skoków”, które przenoszą nas od jednej iteracji do następnej. Oznacza to, że istnieją pewne okoliczności, w których ujemne początkowe wartości nie utkną; przypadki te powstają dla poszczególnych kombinacji i stopni kroku spadku gradientu wystarczająco dużego, aby „przeskoczyć” ponad zanikający gradient.w w(0)

Bawiłem się trochę z tym kodem i odkryłem, że pozostawienie inicjalizacji na i zmiana optymalizatora z SGD na Adama, Adama + AMSGrad lub pęd SGD + nic nie pomaga. Co więcej, zmiana z SGD na Adama faktycznie spowalnia postęp, a także nie pomaga przezwyciężyć zanikającego gradientu tego problemu.w(0)=10

Z drugiej strony, jeśli zmienisz inicjalizację na i zmienisz optymalizator na Adama (rozmiar kroku 0,01), wtedy możesz faktycznie pokonać znikający gradient. Działa również, jeśli używasz i SGD z pędem (wielkość kroku 0,01). Działa nawet, jeśli używasz waniliowego SGD (rozmiar kroku 0,01) i .w(0)=1 w(0)=1w(0)=1

Odpowiedni kod znajduje się poniżej; użyj opt_sgdlub 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 mówi Przywróć Monikę
źródło
Widziałem ten sam problem z LeakyReLU, ELU, SELU, kiedy miałem wagę wyjściową i stronniczość, ale nie jestem pewien, czy wypróbowałem te bez mocy wyjściowej. Będę sprawdzać
endolit
1
(Tak, masz rację, że LeakyReLU i ELU działają dobrze w tym przykładzie)
endolith
2
Oh, już rozumiem. To jest ten gradientu zejście funkcji straty, to po prostu, że funkcja utrata staje się płaski (0 gradient) na 0, gdy zbliża się od strony negatywnej, więc metoda gradientu prostego utknie tam. Teraz wydaje się to oczywiste. : D
endolith
2
Dokładnie. Zauważ, że twoje wykresy straty względem mają „załamanie” w pobliżu 0: to dlatego, że po lewej stronie 0 gradient straty zanika do 0 (jest to jednak rozwiązanie nieoptymalne, ponieważ strata jest tam wyższa niż jest dla ). Co więcej, wykres ten pokazuje, że funkcja straty nie jest wypukła (możesz narysować linię, która przecina krzywą strat w 3 lub więcej lokalizacjach), co oznacza, że ​​powinniśmy być ostrożni przy korzystaniu z lokalnych optymalizatorów, takich jak SGD. ww=0
Sycorax mówi Przywróć Monikę
2
Podczas korzystania z aktywacji relu, nawet SGD bez pędu może przejść nad krawędzią, jeśli rozmiar kroku jest wystarczająco duży dla dowolnej określonej wartości . w(i)
Sycorax mówi: Przywróć Monikę