Нейронная сеть в Хаскеле

Я пытаюсь реализовать инфраструктуру нейронной сети на 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

Другие вопросы по тегам