Yes you should understand backprop-Andrej Karpathy
教授斯坦福大学CS231n(深度学习)课程的时候,我们在教最基础的反向传播时特意设计了一些需要编写详细计算过程的编程作业。学生们需要用原始的numpy来实现每一层的前向和后向传播步骤。不可避免地,有些学生在课堂留言板上抱怨:
“为啥现实世界中的框架,如Tensorflow,已经能帮你自动计算这些,我们还要手写反向传播计算过程呢?“
这看上去是一个十分明智的想法 —— 一旦课程结束你再也不会需要手写这些计算过程了,为什么还需要练习呢?我们只是为了娱乐而折磨学生吗?一些简单的回答或许可以勉强解释:“出于对知识的好奇,你值得知道”或者“万一以后你想改进核心算法”,但是这里有个更有力更实际的回答,我想通过下面一整篇文章来解释:
反向传播的问题是,它是一个Leaky abstraction(*具体可点链接看)
换言之,很容易陷入将学习过程抽象简化的陷阱——觉得自己可以简单地将任意层堆叠在一起,然后反向传播会在你的数据上“神奇地发挥作用”。因此,让我们看一些明显的例子,能用不直观的方式看出来并非如此。
带有前向传播(黑色箭头)和反向传播(红色箭头)的计算图(来源:https://kratzert.github.io/2016/02/12/understanding-the-gradient-flow-through-the-batch-normalization-layer.html)
Sigmoid函数中的梯度消失
让我们从这里轻松开始。从前有段时间,很流行在全连接层中使用sigmoid(或者tanh)等非线性函数。直到人们想到反向传播才意识到其中的一个棘手的问题,就是如果你很草率地设定初始值或者做数据预处理,这些非线性函数可能会“饱和”或者完全停止学习——你的训练损失值将会变得很平缓或者不再下降。如下例子,一个全连接层中的sigmoid计算(原始numpy):
import numpy as np
z = 1/(1 + np.exp(-np.dot(W, x))) # forward pass
dx = np.dot(W.T, z*(1-z)) # backward pass: local gradient for x
dW = np.outer(z*(1-z), x) # backward pass: local gradient for W
如果你的权重矩阵 [公式] 初始化的时候太大,矩阵乘法的输出的范围可能很大(如:-400至400之间),这会使得向量 [公式] 中的所有输出几乎都是二进制的(1或0)。但是如果是这样的话,sigmoid非线性的局部梯度 [公式] ,(不管 [公式] 是0或1)都会变成0(“梯度消失”),从而 [公式] 和 [公式] 的梯度(* [公式] , [公式] )也都会变成0。由于链式规则,从此点开始,其余的反向传播的值将全为零。
关于sigmoid函数另一个不明显却有趣的事实是,它的局部梯度( [公式] )的最大值是0.25(当 [公式] 时),这意味着每次梯度信号流过sigmoid时,其幅度总是减小四分之一(或更多)(*即上方右图中sigmoid的梯度函数所示)。如果你使用的是基础的SGD,那么网络较低层的训练速度将比较高层慢得多。
总的来说:如果你在网络中使用的是sigmoid或tanh非线性函数,并了解反向传播的话,你应时刻警惕你的初始化不会导致它们完全饱和。详情可见CS231n课程。
垂死的ReLU
另一个有趣的非线性是ReLU,它将神经元阈值限制为0。使用ReLU的全连接层的前向和后向传播将包括:
z = np.maximum(0, np.dot(W, x)) # forward pass
dW = np.outer(z > 0, x) # backward pass: local gradient for W
驻足观察一下你会发现,一个神经元在前向传播的过程中被限制在0以上(比如 [公式] 时,就不起作用),然后它的权重会变成0。这会造成所谓的“Dead ReLU”问题,即如果一个神经元不幸地被如此初始化它将不会有效用,或如果在训练该神经元的过程中神经元的权重大幅度更新至此区间,那么该神经元将永久死亡。这就像永久性的、不可挽回的脑损伤。有时你可以通过前向传播整个训练集,发现你的训练网络中大部分(例如40%)的神经元所有时刻都是0。
总的来说:如果你理解了反向传播,并且你的网络用的是ReLU,你会经常担忧Dead ReLU的问题。这些神经元在整个训练集中永远不会对任何样本开启,并且将永久死亡。神经元也可能在训练过程中死亡,通常因为激进的学习率。详情可见CS231n课程。
RNN中的梯度爆炸
原始RNN是反向传播的不直观的另一个很好的例子。我将从CS231n复制粘贴一张幻灯片,该幻灯片是简化的RNN,它不接受任何输入 [公式] ,仅是隐藏状态的循环计算(等效地,输入 [公式] 始终为0):
此RNN展开了 [公式] 个时间步长。凝视反向传播的过程时,你会发现在时间上向后经过所有隐藏状态的梯度信号始终被同一个矩阵(递归矩阵 [公式] )相乘,并散布在整个非线性反向传播过程中。
当一个数字 [公式] 乘以另一个数字 [公式] 时会发生什么(比如 [公式] )?当[公式] 时,它逐渐逼近0,当 [公式] 时,它趋近无穷。在RNN的反向传播中会发生相同的事情,除了 [公式] 是一个矩阵而不是一个数,因此我们不得不推理出它的最大特征值。
总的来说:如果你了解反向传播并且正在使用RNN,那你会对不得不进行梯度的修剪感到担心,或者你更喜欢使用LSTM。
实践中发现:DQN裁剪
让我们再来看一个例子,这实际上是这篇文章的灵感来源。昨天我正在TensorFlow中浏览Deep Q Learning的实践(看看其他人如何处理 [公式] 的numpy等效项,其中 [公式] 是整数向量——事实证明TF不支持这种琐碎的运算)。无论如何,我搜索了“dqn tensorflow”,单击了第一个链接,然后找到了核心代码。这是其中一段:
如果你对DQN熟悉,你能看到target_q_t,就是[reward * \gamma \argmax_a Q(s’,a)],然后有q_acted,也就是执行了动作的Q(s,a)。作者在这里将两者相减得到变量delta,然后他希望在第295行使用L2损失 tf.reduce_mean(tf.square())最小化。 目前为止一切都好。
问题出在第291行,作者试图对异常值保持鲁棒性,因此如果delta太大,他会使用tf.clip_by_value对其进行裁剪。这是很好的想法,并且从向前通过的角度来看很明智,但是如果您考虑反向传播,这会引入一个重大错误。
clip_by_value函数在min_delta到max_delta范围之外的局部梯度为0,因此,只要delta超过min / max_delta,在反向传播期间该梯度将恰好变为0。当他们可能试图修剪梯度以增加鲁棒性时,作者正在修剪原始Q delta。在这种情况下,正确的做法是使用Huber损失代替tf.square:
def clipped_error(x):
return tf.select(tf.abs(x) < 1.0,
0.5 * tf.square(x),
tf.abs(x) - 0.5) # condition, true, false
在TensorFlow中这有点毛病,因为我们想做的是在超过阈值时修剪梯度,但是由于我们无法直接干预梯度,我们只能迂回地定义Huber损失 。 在Torch中,这将更加简单。
我在DQN repo中提交了这个问题,此问题已得到及时解决。
总结
反向传播是一种leaky abstraction。 这是一种信用分配方案,具有不小的后果。如果你因为“ TensorFlow自动让我的网络能够学习”而忽略了它的工作原理,那么你就不会做好与它所带来的危险作斗争的准备,并且你在构建和调试神经网络时的效率将会大大降低。
好消息是,如果教得正确的话,反向传播并不难理解。我对这个话题有比较强烈的感觉,因为在我看来,95%教反向传播的材料都错了,充满了机械的数学公式。相反,我会推荐CS231n关于反向传播的课程,该课程强调直觉(是的,在无耻地给自己打广告)。另外,如果你有时间,可以尝试完成CS231n作业,让你可以手动编写反向传播的过程并帮你巩固理解。
就这么多了! 希望你对反向传播保持好奇,并仔细考虑反向传播的操作。另外,我知道这个帖子(无意间!)变成了多个CS231n的广告。 对此表示歉意:)

浙公网安备 33010602011771号