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

330

我正在尝试在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是网络的实际输出和目标(期望)输出,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)

在这里,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

任何人对问题有深入的了解,甚至只是检查一下我是否正确地实现了算法吗?


17
谢谢,我会研究一下。但我认为这不是正常行为。据我所知,其他用Haskell或其他语言实现的我正在尝试做的事情(简单的前馈全连接神经网络),似乎没有这样做。 - Charles Langlois
17
@Charles: 你是否真的用所说的其他实现尝试过自己的网络和数据集?根据我的经验,当神经网络不适合解决问题时,反向传播算法很容易失控。如果你对自己的反向传播实现有疑问,可以将其输出与朴素梯度计算(当然是在小型神经网络上)进行比较,这比反向传播更难出错。 - shinobi
5
MNIST通常不是一个分类问题吗?为什么你在使用MES?你应该使用softmax交叉熵(从logits计算)吧? - mdaoust
6
@CharlesLanglois,也许这不是你的问题(我看不懂代码),但对于分类问题,“均方误差”并不是凸函数,这可能会解释为什么会出现卡住的情况。 "logits"只是“对数几率”的一种花哨说法:使用来自此处的 ce = x_j - log(sum_i(exp(x))) 计算方法,这样您就不会取指数的对数(这通常会生成NaN)。 - mdaoust
6
恭喜您成为截至2020年1月依然没有得到任何赞或被采纳的回答,但却获得了最高票数的问题! - hongsy
显示剩余18条评论
2个回答

5
你了解在反向传播中的“消失”和“爆炸”梯度吗?我对Haskell不太熟悉,因此我不能很容易地看出你的反向传播实际上在做什么,但看起来你正在使用逻辑曲线作为激活函数。如果您查看此功能的图表,您将看到该功能的梯度在两端几乎为0(随着输入值变得非常大或非常小,曲线的斜率几乎是平坦的),因此在反向传播期间进行乘法或除法会导致非常大或非常小的数字。当您通过多个层时重复执行此操作会导致激活近似于零或无穷大。由于反向传播在训练期间通过这种方式更新权重,因此网络中会出现大量零或无穷大的情况。

解决方案:有很多方法可以解决消失梯度问题,但一个简单的尝试是更改您使用的激活函数类型为非饱和型。ReLU是一种流行的选择,因为它缓解了这个问题(但可能引入其他问题)。

0
您可以尝试使用梯度裁剪来解决消失问题。如果您使用方差为零的数字列训练NN,则可能会得到NAN值,因此首先应用预处理。但是,这在MNIST中不会发生,如果您想要检查,请查看此存储库https://github.com/eduard2diaz/CNN_from_scratch

预处理是一种用于防止方差为0的好例子是什么? - ryanwebjackson
例如,最近我正在帮助一个朋友训练一个神经网络,用于预测在执行斯特拉森算法时,不同大小的矩阵在NVIDIA平台上的功耗。我们使用了Keras库,对数据集进行了标准化处理,并根据问题的规模将数据集分割成了多个部分。但是我们忘记了每个实例中的那一列会有相同的值,所以在几个迭代周期后出现了NaN值。 - Eduardo Roque
例如,最近我正在帮助一位朋友训练一个神经网络,用于预测在执行斯特拉森算法时,不同大小的矩阵在NVIDIA平台上的功耗。我们使用了Keras框架,对数据集进行了标准化处理,并根据问题的规模进行了分割,但是忘记了每个实例中的那一列会有相同的值,所以在几个周期之后得到了NaN的结果。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接