我试图在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是网络(每一层的权重矩阵和偏置向量),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是一个增量矩阵的列表,其中每一列都对应于小批中一行的增量。偏差的梯度是所有小批量中增量的平均值。g也是一样的,它对应于权重的梯度。

下面是实际的更新代码:

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只是捆绑了一个损失函数及其导数(用于计算输出层的delta)。

其余的代码在GitHub: NeuralNetwork上。

所以,如果有人对这个问题有见解,或者只是检查一下我是否正确地实现了算法,我会很感激。


你知道反向传播中的“消失”和“爆炸”梯度吗?我不太熟悉Haskell,所以我不能很容易地看到你的backprop到底在做什么,但它看起来确实像你在使用逻辑曲线作为你的激活函数。

If you look at the plot of this function you'll see that the gradient of this function is nearly 0 at the ends (as input values get very large or very small, the slope of the curve is almost flat), so multiplying or dividing by this during backpropagation will result in a very big or very small number. Doing this repeatedly as you pass through multiple layers causes the activations to approach zero or infinity. Since backprop updates your weights by doing this during training, you end up with a lot of zeros or infinities in your network.

解决方案:有很多方法可以帮助你解决梯度消失的问题,但是最简单的方法就是改变你所使用的激活函数的类型。ReLU是一个受欢迎的选择,因为它缓解了这个特定的问题(但可能会引入其他问题)。