引言
本着“凡我不能创造的,我就不能理解”的思想,本系列文章会基于纯Python以及NumPy从零创建自己的深度学习框架,该框架类似PyTorch能实现自动求导。
要深入理解深度学习,从零开始创建的经验非常重要,从自己可以理解的角度出发,尽量不使用外部完备的框架前提下,实现我们想要的模型。本系列文章的宗旨就是通过这样的过程,让大家切实掌握深度学习底层实现,而不是仅做一个调包侠。
前面我们介绍了权重衰退的L2参数范数惩罚,该方法能一定程度上抑制过拟合。但是如果网络模型变得很复杂时,仅用此方法就难以解决了。此时,我们通常会使用Dropout方法。
原理
Dropout(译为暂退法或丢弃法)意思是(在每次前向传播时随机)丢弃神经元(隐藏层和可见层)。
原论文中的可见层(visible layer)其实就是输入层,即原论文中说Dropout可以应用到输入上,可见下图。
不过这种丢弃是临时的,如上图(来自原始论文)所示。哪些神经元被丢弃是随机选择的。在训练时,每个神经元有概率的可能被丢底(为了和PyTorch保持一致,这里说的概率
指的是丢弃率,而不是保留率)。一般
或
,可作为一个超参数通过验证集来调整。
如上图所示,是一个标准的2个隐藏层的前馈网络。表示被丢弃的神经元,此时它上面不会有任何连线。由原始网络(a)经过Dropout得到的网络(b)可以看成是它的青春版(瘦身版)。训练时,每传递一次数据,都会随机地进行丢弃,每次丢弃的情况都是独立的。
在实现时,对神经网络来说,我们处理的是每层的输入向量(也可以看成是每个单元),使用Dropout后,该输入向量会有一些的元素随机地变成。通过以概率为
的伯努利分布(生成
或
)随机生成一个丢弃掩码(mask),如果掩码的值为
,则表示对应的输入元素得以保留,否则丢弃。
通常在测试时不应用Dropout。在训练时,由于存在Dropout,激活的神经元数变得少了,使得每层的输出比期望的要小。那么在测试时,由于没有Dropout,那么每层的权重应该变得更小。但一种更常用的做法是,我们在测试时不减小每层的权重,而是在训练时,让输出变大,即输出乘以进行扩大。数学的解释是,假设记每层输入值为
,那么训练时以丢弃率
被
替换,如下所示:
那么它的期望,即期望值保持不变。这样在测试时,可以让输入保持不变。
前面说的都是正向传播,在反向传播时,由于丢弃的神经元对网络不会有任何贡献,因此不会有梯度传递给它们。
下面我们就来看代码实现。
代码实现
def dropout(input: Tensor, p: float = 0.5):
'''
p: dropout ratio 丢弃率
'''
if Config.train:
# 丢弃掩码 1代表保留,0代表丢弃 以1-p的概率生成输出为1伯努利分布,做了input的元素个数这么多次实验
mask = np.random.binomial(1, 1-p, size=input.shape)
# 让输入乘上这个与之同shape的丢弃掩码,然后除以1-p进行缩放,这样在测试时,可以原样输出
return input * Tensor(mask, requires_grad=False) / (1 - p)
else:
return input
为什么有用
应用Dropout到神经网络上等同于从其中生成一个瘦身版网络,见上图(b)。假设一个神经网络有个神经元,每个神经元都有两种状态——丢弃与保留,那么共有
个可能的瘦身版子网络。每次带有Dropout的前向传播都可以看成是训练一个不同的子网络,这些子网络都共享原始网络的参数。那么,在测试时,最终的网络可以近似看成是集成了指数级个不同自网络的集成学习模型。训练带有Dropout同时在测试时使用这种近似平均方法可以显著地降低泛化误差。
原始论文提到了一个关于有性繁殖的类比:神经网络过拟合与每个神经元都过多的依赖于其他神经元有关(常见的是每一层都依赖于前一层的激活值),这种情况称为共适应性(co-adapting)。就像有性生殖会取父母双方各一半的基因一样,可能会打破一些共同适应的基因集,带来个人适应性上的不足,但从长远看增强了基因的混合能力。
复杂的共适应性在训练集上表现良好,但在未见的测试集上就不一定了,Dropout打破了共适应性使得在测试集上表现更好。
完整代码
完整代码笔者上传到了程序员最大交友网站上去了,地址: 👉 https://github.com/nlp-greyfoss/metagrad