Cha*_*ois 108 algorithm haskell neural-network hmatrix
我正在尝试在Haskell中实现神经网络架构,并在MNIST上使用它.
我正在使用hmatrix线性代数包.我的培训框架是使用pipes包构建的.
问题是,我的代码编译不会崩溃.
但是对于一个,层NaN,小批量大小和学习速率的某些组合产生1e-100计算中的值.经过一些检查,我发现if激活中最终会出现非常小的值(顺序).但是,即使没有发生这种情况,培训仍然无效.它的损失或准确性没有任何改善.
我检查并重新检查了我的代码,我不知道问题的根源是什么.
这是反向传播训练,它计算每一层的增量:
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)
Run Code Online (Sandbox Code Playgroud)
n为损耗函数,weight是网络(bias矩阵和out向量的每个层),tar并且target是网络和所述的实际输出das(期望的)输出,并且out各自层的激活衍生物.在批处理模式中tar,das是矩阵(行是输出向量),并且lf是矩阵列表.
这是实际的梯度计算:
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)
Run Code Online (Sandbox Code Playgroud)
这里n和i以上相同,t是输入并且squeeze是目标输出(以批量形式,作为矩阵).
ds通过对每一行求和将矩阵转换为向量.也就是说,gs是一个增量矩阵列表,其中每列对应于一个小批量行的增量.因此,偏差的梯度是所有小批量的增量的平均值.同样的事情lr,对应于权重的梯度.
这是实际的更新代码:
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))
Run Code Online (Sandbox Code Playgroud)
FC是学习率.af是层构造函数,是该层grad的激活函数.
梯度下降算法确保传递学习率的负值.梯度下降的实际代码只是围绕一个组合的循环,move并Evaluator带有参数化停止条件.
最后,这是均方误差丢失函数的代码:
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'
Run Code Online (Sandbox Code Playgroud)
hmatrix 只是捆绑一个损失函数及其导数(用于计算输出层的增量).
其余的代码在GitHub:NeuralNetwork上
所以,如果有人对这个问题有所了解,或者甚至只是通过理智检查我正确地实现了算法,我将不胜感激.
你知道反向传播中的“消失”和“爆炸”梯度吗?我对 Haskell 不太熟悉,所以我无法轻易看出你的反向传播到底在做什么,但它看起来确实像你正在使用逻辑曲线作为你的激活函数。
如果您查看该函数的绘图,您会发现该函数的梯度在末端接近 0(当输入值变得非常大或非常小时,曲线的斜率几乎是平坦的),因此乘法或除法在反向传播过程中,这将导致一个非常大或非常小的数字。当您穿过多个层时重复执行此操作会导致激活值接近零或无穷大。由于反向传播会在训练期间通过执行此操作来更新您的权重,因此最终您的网络中会出现大量零或无穷大。
解决方案:您可以搜索大量方法来解决梯度消失问题,但尝试的一件简单的事情是将您使用的激活函数类型更改为非饱和函数。ReLU 是一个流行的选择,因为它可以缓解这个特定问题(但可能会引入其他问题)。