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.