Я намагаюся реалізувати архітектуру нейронної мережі в Haskell і використовувати її на MNIST.
Я використовую hmatrixпакет для лінійної алгебри. Моя навчальна база побудована за допомогою pipesпакету.
Мій код компілюється і не дає збою. Але проблема полягає в тому, що певні комбінації розміру шару (скажімо, 1000), розміру міні-партії та швидкості навчання породжують NaNзначення в обчисленнях. Після деякої перевірки я бачу, що 1e-100в активаціях з часом з’являються надзвичайно малі значення (порядок ). Але, навіть коли цього не відбувається, навчання все одно не працює. Немає покращення щодо його втрати або точності.
Я перевірив і перевірив свій код, і я не знаю, що може бути коренем проблеми.
Ось тренінг зворотного розповсюдження, який обчислює дельти для кожного шару:
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це функція втрат, nє мережа ( weightматриця і biasвектор для кожного шару), outі tarє фактичний вихід мережі і target( по бажанню) виходу, і dasє похідними активації кожного шару.
У пакетному режимі out, tarє матриці (рядки є вихідними векторами), і dasявляють собою список матриць.
Ось фактичний розрахунок градієнта:
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)
Тут, lfі nтакі самі, як вище, iє вхідними даними і tє цільовим результатом (як у пакетній формі, так і в матрицях).
squeezeперетворює матрицю у вектор шляхом підсумовування по кожному рядку. Тобто це dsсписок матриць дельт, де кожен стовпець відповідає дельтам для рядка міні-партії. Отже, градієнти для ухилів є середнім значенням дельт за всю міні-партію. Те саме gs, що відповідає градієнтам ваг.
Ось фактичний код оновлення:
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- це швидкість навчання. FCє конструктором шару і afє функцією активації для цього шару.
Алгоритм градієнтного спуску гарантує передачу негативного значення швидкості навчання. Фактичний код для градієнтного спуску - це просто петля навколо композиції gradта move, з параметризованою умовою зупинки.
Нарешті, ось код для функції середньоквадратичної втрати помилок:
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 просто поєднує функцію втрат та її похідну (для обчислення дельти вихідного шару).
Решта коду розміщена на GitHub: NeuralNetwork .
Отже, якщо хтось має уявлення про проблему або навіть просто перевіряє розумність, чи правильно я впроваджую алгоритм, я буду вдячний.