当我们在学习或者工作中使用tensorflow,pytorch之类去复现网络的时候,一般没人去单独去写backward,因为这些优秀的框架已经给我们打好了根基,它们的一句话即可自动的帮我们去反向传播,我们负责写好forward就可。
我听到的更多的抱怨是来自实践的团队,“如果我们工作中根本不用去写反向传播,为什么要去学习如何自己写呢”,反方的观点则是诸如 “这些知识懂了的话理解神经网络会更深”,“你可能有机会去接触神经网络的更底层”等等,这些话确实很苍白无力。
但其实我们应该去写写并懂得反向传播的原因是因为它是一个有漏洞的方法。
简单来说,它很容易陷入学习过程中的一些陷阱,这些陷阱就发生在你手到擒来的写了个自己觉得完美无缺的网络,前向反向都如你意的过程中。
-
梯度消失(vanishing gradients)
现实中一个常用的做法就是用sigmoid或者tanh来跟在全连接层的后边做一个非线性的激活。这个技巧太过普通以至于往往人们认识不到学习过程中loss停留在一个值拒绝下降的原因就是因为他们在权重初始化的时候太过随意,导致反向传播不再起作用。
举个例子:一个简单的前向传播并用sigmoid激活
z = 1 / (1 + np.exp(-np.dot(W, x))) # 前向传播 dx = np.dot(W.T, z * (1 - z)) # 反向传播:计算x的梯度 dW = np.outer(z * (1 - z), x) # 反向传播:计算W的梯度
如果你将矩阵W的初始值设的过大,那么矩阵相乘wx+b之后将会得到一个很大范围的值(比如:-500 到 500),这个很大的值将使得向量z的值要么等于0要么等于1
那么接下来做反向传播求导的时候无论对x还是对W来说,z(1-z)*将会都等于0,也就是说x和W的梯度将都是0,那么根据链式法则,之后的反向传播的也都是0,整个网络的参数将不再更新。
另外一个比较有意思的事情就是sigmoid的梯度:sigmoid x (1 - sigmoid)最大值为0.25(z=0.5)。这就意味着每次反向传播经过sigmoid一次,它的大小将至少减少为原来的1/4,那么特别是达到网络的前几层的时候,反向传播的梯度将会越来越小。如果你恰巧又用了SGD,那么整个网络的训练将会更加的漫长。
-
神经元的死亡
另外一个比较有意思的是ReLU,它将小于0的值都过滤为0。
举个例子:
z = np.maximum(0, np.dot(W,x)) #前向传播
dW = np.outer(z > 0, x) # 反向传播:计算W的梯度
你可以看到当如果有神经元在前向传播后(z的值中)被ReLU激活后为0值时,它的权重的梯度将也为0,这时将会导致出现神经元的死亡的现象,这些神经元将永不再更新。出现这种情况的原因有两个,1个是ReLU神经元的权重被初始化的不够好,另一个是神经元的权重在训练的过程中被大幅更新,这些神经元将永久的处于死亡状态。它就像永久死亡的大脑的细胞。如果你在用ReLU,那么有时你就会发现在训练时大约有40%左右的神经元已经是0处于死亡的状态。
那么如果你懂得了反向传播的这个问题,你就可以很小心的去对待死亡ReLU的问题,对待这些对你训练集的所有数据都输出为0,永久的死去了神经元。神经元也会在训练的中途死掉,比如你设置了比较大的学习率。
-
梯度爆炸(exploding gradients)
假设有一个4层深的网络(三个隐藏层),并且每一层只有一个神经元,σ为sigmoid函数
那么来个反向传播的话
之后不断的相乘的结果就是网络的前层的梯度成指数级的增长。再拿这个超级大的梯度来更新我们的w的话,就导致网络的不稳定,在极端情况下,权重的值会变得特别大,以至于结果溢出,出现NAN值。
最后 反向传播并不是一个健壮的方法,如果你因为“Tensorflow或者Pytorch已经自动的帮我们实现了”就忽视它,那么你面对他带来的问题的时候讲无所适从,对于调试一个网络来说也会变得困难。庆幸的是反向传播理论并不是那么的不食人间烟火,只要你认真的关注并研究几篇文章,那么你很快会掌握它的。