我正在尝试在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
向量),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 gamma = y'-y in gamma**2 / 2
f' (y,y') = (y'-y)
in Evaluator f f'
Evaluator
只是捆绑了一个损失函数和其导数(用于计算输出层的 delta)。
其余代码都放在 GitHub 上:NeuralNetwork。
任何人对问题有深入的了解,甚至只是检查一下我是否正确地实现了算法吗?
ce = x_j - log(sum_i(exp(x)))
计算方法,这样您就不会取指数的对数(这通常会生成NaN)。 - mdaoust