现在我们得到了data bunch,我们可以用它创建一个tabular learner。这里我们要设置神经网络架构,对于这类表格模型,我们的网络架构严格来说,就是最基础的全连接层(fully connected)构成的。就像我们在这个图里画的一样:
输入=>矩阵相乘=>非线性激活=>矩阵相乘=>非线性激活=>矩阵相乘=>非线性激活 结束,就这样。值得注意的是,这个竞赛是三年前的,但是我还是没看到任何显著进展,至少在架构层面没有,使得我需要选取一个与三年前拿第三名的网络结构有所不同的新结构。我仍然在使用最简单的全连接层组成的神经网络来处理这个问题。
learn = tabular_learner(data, layers=[1000,500], ps=[0.001,0.01], emb_drop=0.04,
y_range=y_range, metrics=exp_rmspe)
现在中间层的权重矩阵会把1000个激活输入,转成500个激活输出,这意味这这个矩阵会有500,000个参数。对于一个只有几十万个样本的数据集来说,参数的数量太多了。这会过拟合。我们需要避免这种情况发生。确保不会过拟合的方法就是使用正则化(regularization),我们要做的不是减少参数数目,而是使用正则化。一种方式是用权重衰减,fastai会自动使用它,如果有需要的话,你可以修改它。这个例子证明,我们要用大量的正则化。我们会传入一个叫做ps的参数。这会提供dropout。还有这个emb_drop,这会做embedding dropout。
Dropout
我们来学习下什么是dropout。dropout是一种正则化。这是Nitish Srivastava的dropout论文,它是Srivastava在Geoffrey Hinton指导下做的硕士论文。
这张来自原始论文的图片很好地说明了它做了什么。第一张图是一个标准的全连接网络,每条线代表激活值和一个权重的乘法运算。当有多个箭头进入,就代表乘积之和。所以这个激活值(红圈里的),是所有这些激活输入和参数点积的和。这是一个标准的全连接神经网络的样子。
对于dropout,我们把这些去掉。我们随机地去掉一些比例的激活值,不是权重,不是参数。记住,神经网络里只有两类数量制:参数(也叫权重),和激活值。我们要去掉一些激活值。
可以看到,我们去掉激活值后,在求和项中与之关联的数值也都被去掉了。对每个mini batch,我们去掉一组不同的激活值。要去掉多少呢?我们按概率p决定是否丢去激活值。常用的p的值是0.5。这意味着什么?这个情况下,图中不仅仅随机消去了一些中间隐藏层的激活值,而且一些输入值也被随机删除了。删除输入值是一种少见的情况。通常我们只是删除隐藏层中的激活值。这样做的目的是什么呢?每次处理一个mini batch时,我随机去掉一些激活值。然后,在下一个mini batch中,我把它们放回来,去掉其他的激活值。
这意味这没有一个激活值可以一直记忆某些输入值,因为激活值记忆输入值是数据过拟合之后发生的现象。但数据过拟合时,模型的某一部分只会识别某个特定的图片,而不是学会识别图片的通用特征或特定特征。没有dropout的话,我们就很难是神经网络学会识别泛化特征。Geoffrey Hinton这样描述这个设想的初衷:
在银行里,柜员员工会不时换岗,我问他们为什么。他说他不知道,他们经常变动。我认为,这是因为要想骗过银行的话,需要员工们的配合。这让我意识到,在每个样本里随机移动神经元的不同部分会阻止阴谋,这会减少过拟合。
Hinton: Reddit AMA
他发现每次他去银行,所有的柜员和员工会不时换岗,他意识到这样做的原因一定是他们在预防欺诈。如果他们不停变动,没人可以在业务上如此精通到设计某种阴谋以此来欺诈银行。
当然也有人在其他时候文Hinton创造Dropout的灵感, 他会说是因为他想到了生物脉冲神经元的运行模式。
我们不清楚为什么神经元有脉冲。一种理论是它们想要跟多噪音来regularize,因为我们得到的参数数目远多于我们的数据点。Dropout的思想在于如果你有噪音激活,你就能够使用一个大得多的模型。
Hinton: O'Reilly
这可能跟他脑神经科学家的背景有关,认为脉冲神经元可能会对信号正则化有所助益,而dropout沿用了脉冲神将元的运行方式.
有趣的是,当人们被问到你算法的点子从哪里来的时。基本上点子都不是来自于数学,它们通常来源于直觉和现实事物的类比,等等。
所以真相来源于人们的头脑风暴,最后它们想到了dropout这个点子。重要的是,dropout非常的好用,我们可以在自己的模型中,加入dropout使得模型实现轻松泛化。当然太多的dropout会降低模型性能,你的模型就会欠拟合,因此你需要谨慎选择,不同隐藏层dropout的概率数值。
所以几乎所有的fastai学习器,都有一个参数交“ps”,这就是每一层的dropout的p值,你可以传入列表或代入一个整数。然后函数就会生成一个列表,把列表中的消去比例应用到每一个层中。
对于卷积神经网络可能会有些不同,比如你传入了一个整数,那么最后一层就会使用这个整数作为消去比例。而前面层的消去比例会是这个数值的一半, 我们基本上会使用前人调试好的最佳策略。当然你也可以使用自己的dropout列表,来获得想要的dropout消去比例。
Dropout和测试阶段
Dropout有一个有趣的特征,就是当我们讨论到训练阶段和测试阶段时,测试阶段也称为预测期,训练阶段就是我们做反演参数迭代的阶段,我们刚看了训练时间内dropout的工作方式,在测试时,我们会关闭dropout,我们不再使用dropout,是因为测试时我们希望得到的结果越精确越好,预测时,因为我们不再训练模型了,所以不用担心过拟合,因此我们会关掉dropout。但这意味着如果之前的p是0.5,那么有一半的激活值会被删除,这意味着全部保留(不做dropout)的话,那么激活值的总数会是原来的两倍。因此,他们在论文中建议,在测试时将所有权重乘以p。如果你有兴趣,可以研究一下pytorch的源代码,你可以找到实现dropout的C语言代码。
可以看到这段代码的表现相当有趣。首先代码生成伯努利分布,伯努利分布以的概率返回1,否则返回0。这里p时dropout的概率,1-p就是剩余激活值的概率。所以这行这段代码后最终得到1或0。
然后原地(记住在PyTorch里下划线代表“原地”)除以1-p。如果它是0,那值没什么变化,还是0。如果它是1,p是0.5,那么它的值就变成了2。最后,我们把输入原地乘以这个noise(它是dropout的mask)。然后原位(记住在PyTorch里下划线代表“原位操作”)除以1-p。如果它是0,那值没什么变化,还是0。如果它是1,p是0.5,那么它的值就变成了2。最后,我们把输入原位乘以这个noise(它是dropout的mask)。
也就是说,在PyTorch里,我们在测试时被没有用dropout。我们只在训练阶段用了,这意味着使用PyTorch时你不需要在测试时做什么额外处理。不仅是在PyTorch,这是一个很常见的模式。看下PyTorch的代码很有用,要实现dropout这个超酷超有用的功能,只要 3行代码,它们用的是C语言,我猜这是因为C语言执行时这样会快一点。很多库用Python也能很好地完成这个功能 ,效果也很好。你可以写一个自己的dropout层,它应该得到和这个一样的结果。这是一个很好的可以自己尝试的实验。看下你能不能用Python创建你自己的dropout层,看下能不能复现和这里来的dropout层一样的结果。
Lesson6笔记本
learn = tabular_learner(data, layers=[1000,500], ps=[0.001,0.01], emb_drop=0.04,
y_range=y_range, metrics=exp_rmspe)
这就是dropout。这里我们会在第一层用一个非常小的dropout(0.001),在下一层用一个比较小的dropout(0.01),然后会在embedding层用特定的dropout。为什么在embedding层用特定的dropout?如果你看下fastai内部的代码,这是我们的tabular模型:
在这段代码里,检查到如果存在嵌入,先循环调用每个嵌入,然后我们把它们拼接成一个矩阵,最后调用嵌入层dropout(emb_drop).嵌入层的dropout也就是个普通的dropout。这就是dropout模块的一个实例,这有点道理,对于连续变量,就是某一列对应的变量,你不会想把它给dropout了, 因为这相当于吧整个输入删掉了。这肯定不是你想要的的。但是对于一个嵌入层,嵌入层只是一个矩阵乘以一个one-hot编码矩阵。因此它只是神经网络众多分层中的一层,所以你可以非常合理地用dropout处理嵌入层的输出,因为你可以将dropout应用于那一层的激活值,所以,其实就是随机删除嵌入层的部分输出(部分激活值),这么做是符合逻辑的。
我们这样做的另外一个原因是一年前我在这个数据集上做了很多实验。我尝试了很多不同的方法,各种可能。你在这里可以看到。
我把它们都放在一个电子表格里(当然,是Microsoft Excel),把它们放进一个透视表(pivot table)来把它们汇总,以便找出哪些方法和超参数以及架构好一些,哪些不够好,我把它们画成小图表,就是各种超参数和架构组合的,训练曲线摘要图,我发现其中有一条曲线的预测精度一直都不错,曲线波动非常小,在它这里可以看到一条平滑曲线,这是我用fastai库做的,各种实验中的一个,嵌入层dropout是我发现的行之有效的方法之一,我们的代码是现在你看到的样子,也是因为这些实验结果导致的。它实际是这些实验经验的结合。
那为什么我要做这些实验呢?这实际上是因为受到了Kaggle获奖论文内容的启发,但是对于论文中的许多论述,我觉得是否还有其他的实现方法可选呢?为什么他们没有去试一下呢?所以我去试了,找到了哪些方法好用,哪些不好用,并进行了一些小改进。你也可以尝试一下这些实验,用不同的模型和架构做些实验,使用不同的dropout,设置不同的层数和激活值数量,等等。