训练神经网络时出现极小或NaN值

我试图在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向量),outtar是网络的实际输出和target(期望的)输出,das是每一层的激活导数。

在批处理模式下,outtar是矩阵(行是输出向量),而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)

这里,lfn与上面相同,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是该层的激活函数。

梯度下降算法确保为学习率传递一个负值。梯度下降的实际代码只是围绕gradmove的组合进行循环,并带有参数化的停止条件。

最后,这是一个均方误差损失函数的代码:

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

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

11940 次浏览

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

如果你看一下这个函数的图,你会发现这个函数的梯度在两端接近0(当输入值变得非常大或非常小时,曲线的斜率几乎是平坦的),所以在反向传播期间乘以或除以这个将会得到一个非常大或非常小的数字。当你穿过多个层时重复这样做会导致激活接近零或无穷大。由于backprop在训练过程中通过这样做来更新你的权重,你最终会在你的网络中得到很多零或无穷大。

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