反向传播 - Back Propagation
是前几天写 DNN 的时候遇到了一个问题,后来拿 torch 对拍,才找到的,这边也是记录一下
代码实现在这边: DNN
原理
反向传播是很自然的想法,在链式求导法中,就是由外向内递归到单变量的,因此偏导数其实是一整条路径上每一个函数对函数值变化的贡献
这是两个函数复合的情况,而 DNN 里其实也就是一系列函数的复合,例如对于这个(2+2)层的神经网络

其中
损失函数使用 MSE
那么当我们在反向传播时,例如对于
对于
类似的,对于
我们可以观察到,前面的部分是一致的,重复计算的部分,同时,这些偏导数都是可以在节点上依次从后往前算出最后得到的,因此可以将后面的偏导数传到前面的节点,而它的偏导数是关于它的输入的,因此在前向传播时,我们就能计算其偏导数。
梯度下降
想像一个凹函数(损失函数),比如
训练的过程,就是我们寻找损失函数最低点(极小值)的过程,那么我们可以想见,如果我们在极小值的一侧,我们可以往梯度的反方向走,最终就能到达极小值,这就是梯度下降的想法。
但是实际上,我们并不能知道要沿这个方向走多远,因此我们每次都选择走一小步,然后检查损失函数的变化,再次计算梯度。步长则是根据学习率确定。
对于一个权重
我遇到的问题
这个问题来源于我训练 XOR 的时候模型不收敛,理论上来说,虽然 sigmoid 的有效梯度区间比较小,但是你 epoch 数够了总是能训练出来的,后来我就一通验证,一开始我考虑是不是梯度算错了,我搁那手算梯度(还好层数少),发现是没有问题的,就找 LLM 写了个 torch 的板子,模型建的和我自己的一样。
我就去对拍,把 torch 的 weights 初始化和我的模型一致,再跑一个 epoch 对一遍参数更新情况,就遇到一个比较抽象的问题,就是如果每次都输入只有一个样本,那么我的参数更新和 torch 一致,而如果输入不止一个样本,参数更新就有问题了,我就去看我的梯度下降部分,这才发现问题。
其实这个项目是从一个计算图上面改过来的,这个计算图当时设计时,是考虑对每一组的输入、输出求偏导,而
MSE 是计算多组输入平均的损失,因此,对于每一个权重
这里也就解决了我之前的,一个困惑,也就是 torch 训练时,都要
optimizer.zero_grad()
,就是把累积的梯度清零,其实这个梯度就是这边累积的,批量梯度下降就是一个
epoch 清一次,而 SGD 就是一个样本一次 step
,然后
zero_grad
。
也就把 training 部分改成了这样
1 | void DNN::training(ld lr, size_t epoch, vector<vector<ld>> &inputs, vector<vector<ld>> &labels) { |
在跑每组样本时累积梯度,在跑完样本之后 step 进行参数更新