Нейронная сеть в Хаскеле
Я пытаюсь реализовать инфраструктуру нейронной сети на Haskell и использовать ее в MNIST, как личный проект.
Я использую пакет hmatrix для линейной алгебры. Моя учебная база построена с использованием пакета pipe.
Я новичок в машинном обучении, поэтому мое понимание теории ограничено, но, используя несколько учебных пособий и других ресурсов, я думаю, что смог поместить математику в код на Haskell.
Проблема в том, что мой код не работает. Он компилируется и не падает. Но, с одной стороны, определенные комбинации размера слоя (скажем, 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)
if
функция потерь, n
сеть (весовая матрица и вектор смещения для каждого слоя), out
а также tar
являются фактическим выходом сети и целевым (желаемым) выходом, и 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 γ = y'-y in (/ 2) $ γ**2
f' (y,y') = (y'-y)
in Evaluator f f'
Evaluator
просто связывает функцию потерь и ее производную (для вычисления дельты выходного слоя).
Остальная часть кода на GitHub: NeuralNetwork
Так что, если у кого-то есть понимание проблемы или даже просто проверка работоспособности, что я правильно реализую алгоритм, я был бы благодарен.
2 ответа
Вы знаете об "исчезающих" и "взрывающихся" градиентах при обратном распространении ошибки? Я не слишком хорошо знаком с Haskell, поэтому мне сложно понять, что именно делает ваш backprop, но похоже, что вы используете логистическую кривую в качестве функции активации.
Если вы посмотрите на график этой функции, вы увидите, что градиент этой функции почти равен 0 на концах (поскольку входные значения становятся очень большими или очень маленькими, наклон кривой почти плоский), поэтому умножение или деление из-за этого во время обратного распространения будет получено очень большое или очень маленькое число. Если делать это неоднократно, когда вы проходите через несколько слоев, активация приближается к нулю или бесконечности. Поскольку обратное распространение обновляет ваши веса, делая это во время тренировки, вы получаете много нулей или бесконечностей в вашей сети.
Решение: существует множество методов, которые вы можете найти, чтобы решить проблему исчезающего градиента, но одна простая вещь, которую можно попробовать, - изменить тип используемой вами функции активации на ненасыщающую. ReLU - популярный выбор, поскольку он смягчает эту конкретную проблему (но может вызвать другие).
Вы можете попытаться решить проблему, используя обрезку градиента, чтобы предотвратить исчезновение. Если вы обучаете свою NN с помощью числовых столбцов с нулевой дисперсией, вы можете получать значения NAN, поэтому сначала примените предварительную обработку. Однако с MNIST этого не происходит, если вы хотите проверить этот репозиторий https://github.com/eduard2diaz/CNN_from_scratch