Próbuję zaimplementować architekturę sieci neuronowej w Haskell i używać jej na MNIST.
Używam hmatrix
pakietu do algebry liniowej. Moja struktura szkoleniowa jest zbudowana przy użyciupipes
pakietu.
Mój kod kompiluje się i nie ulega awarii. Ale problem polega na tym, że pewne kombinacje rozmiaru warstwy (powiedzmy, 1000), rozmiaru minibatchu i szybkości uczenia się powodują NaN
wartości w obliczeniach. Po krótkiej inspekcji widzę, że bardzo małe wartości (rzędu1e-100
) w końcu pojawiają się w aktywacjach. Ale nawet jeśli tak się nie stanie, trening nadal nie działa. Nie ma poprawy w zakresie utraty lub dokładności.
Sprawdziłem i ponownie sprawdziłem swój kod i nie wiem, co może być przyczyną problemu.
Oto trening wstecznej propagacji, który oblicza delty dla każdej warstwy:
backward lf n (out,tar) das = do
let δout = tr (derivate lf (tar, out)) -- dE/dy
deltas = scanr (\(l, a') δ ->
let w = weights l
in (tr a') * (w <> δ)) δout (zip (tail $ toList n) das)
return (deltas)
lf
jest funkcją strat, n
jest siecią ( weight
macierzą i bias
wektorami dla każdej warstwy) out
i tar
są faktycznym wyjściem sieci i target
(pożądanym) wyjściem, i das
są pochodnymi aktywacji każdej warstwy.
W trybie wsadowym out
, tar
są macierzami (wiersze są wektorami wyjściowymi) i das
jest listą macierzy.
Oto rzeczywiste obliczenie gradientu:
grad lf (n, (i,t)) = do
-- Forward propagation: compute layers outputs and activation derivatives
let (as, as') = unzip $ runLayers n i
(out) = last as
(ds) <- backward lf n (out, t) (init as') -- Compute deltas with backpropagation
let r = fromIntegral $ rows i -- Size of minibatch
let gs = zipWith (\δ a -> tr (δ <> a)) ds (i:init as) -- Gradients for weights
return $ GradBatch ((recip r .*) <$> gs, (recip r .*) <$> squeeze <$> ds)
Tutaj, lf
i n
są takie same jak powyżej, i
jest wejściem i t
jest wyjściem docelowym (oba w postaci wsadowej, jako macierze).
squeeze
przekształca macierz w wektor, sumując w każdym wierszu. Oznacza to, że ds
jest to lista macierzy delt, gdzie każda kolumna odpowiada deltom wiersza minibatchu. Tak więc, gradienty odchyleń są średnią delt na całej minibatch. To samo dotyczy gs
, co odpowiada gradientom wag.
Oto rzeczywisty kod aktualizacji:
move lr (n, (i,t)) (GradBatch (gs, ds)) = do
-- Update function
let update = (\(FC w b af) g δ -> FC (w + (lr).*g) (b + (lr).*δ) af)
n' = Network.fromList $ zipWith3 update (Network.toList n) gs ds
return (n', (i,t))
lr
to współczynnik uczenia się. FC
jest konstruktorem warstwy i af
funkcją aktywacji tej warstwy.
Algorytm zstępowania gradientu zapewnia przekazanie ujemnej wartości szybkości uczenia się. Rzeczywisty kod opadania gradientu to po prostu pętla wokół kompozycji grad
i move
ze sparametryzowanym warunkiem zatrzymania.
Na koniec, oto kod funkcji średniej kwadratowej utraty błędu:
mse :: (Floating a) => LossFunction a a
mse = let f (y,y') = let gamma = y'-y in gamma**2 / 2
f' (y,y') = (y'-y)
in Evaluator f f'
Evaluator
po prostu łączy funkcję straty i jej pochodną (do obliczenia delty warstwy wyjściowej).
Reszta kodu znajduje się na GitHub: NeuralNetwork .
Byłbym więc wdzięczny, gdyby ktoś miał wgląd w problem lub choćby po prostu sprawdził poczytalność, czy poprawnie implementuję algorytm.
źródło
ce = x_j - log(sum_i(exp(x)))
obliczeń z tego miejsca, aby nie brać dziennika wykładniczego (który często generuje wartości NaN)Odpowiedzi:
Czy wiesz o „znikających” i „eksplodujących” gradientach we wstecznej propagacji? Nie jestem zbyt zaznajomiony z Haskellem, więc nie mogę łatwo zobaczyć, co dokładnie robi twoja tylna podpórka, ale wygląda na to, że używasz krzywej logistycznej jako funkcji aktywacji.
Jeśli spojrzysz na wykres tej funkcji, zobaczysz, że gradient tej funkcji jest prawie 0 na końcach (ponieważ wartości wejściowe stają się bardzo duże lub bardzo małe, nachylenie krzywej jest prawie płaskie), więc mnożenie lub dzielenie przez to podczas wstecznej propagacji spowoduje bardzo dużą lub bardzo małą liczbę. Powtarzanie tego podczas przechodzenia przez wiele warstw powoduje, że aktywacje zbliżają się do zera lub nieskończoności. Ponieważ backprop aktualizuje twoje wagi, robiąc to podczas treningu, w twojej sieci jest dużo zer lub nieskończoności.
Rozwiązanie: istnieje wiele metod, których możesz szukać, aby rozwiązać problem znikającego gradientu, ale jedną łatwą rzeczą do wypróbowania jest zmiana typu używanej funkcji aktywacji na nienasycającą. ReLU jest popularnym wyborem, ponieważ łagodzi ten konkretny problem (ale może wprowadzić inne).
źródło