Я намагаюся реалізувати архітектуру нейронної мережі в 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 .
Отже, якщо хтось має уявлення про проблему або навіть просто перевіряє розумність, чи правильно я впроваджую алгоритм, я буду вдячний.