MNIST神经网络
现在我们有了这个update函数,我们可以用MNIST_nn替代这个Mnist_Logistic,并且从头构建一个神经网络。
class Mnist_NN(nn.Module):
def __init__(self):
super().__init__()
self.lin1 = nn.Linear(784, 50, bias=True)
self.lin2 = nn.Linear(50, 10, bias=True)
def forward(self, xb):
x = self.lin1(xb)
x = F.relu(x)
return self.lin2(x)
现在我们只需要两个线性层。在第一个里面,我们可以用一个大小是50的权值矩阵。我们要保证第二个线性层的输入数据大小也是50行,这样才能对应上。最后一层的输出大小应该是10,因为我们要预测的类别的数量是10。所以我们的forward函数构建了一个线性层:
- 运行一个线性层
- 计算ReLU
- 运行第二个线性层
现在我们实际上从头创建了一个神经网络。我们没有自己写nn.Linear,但是你可以自己写一个,或者可以直接做矩阵乘法。
然后,我们可以执行给model赋值的的Mnist——NN().cuda()方法,再对同样的update函数计算损失度,就是这样。
这里就可以看到为什么这类神经网络很简单。只要你有可以做梯度下降的函数,你就可以尝试不同的模型。你可以添加更多PyTorch里的功能。与其自己手工添加所有东西,为什么不直接用opt = optim.something?目前,我们用到得“something”是SGD。
然后你告诉PyTorch说,让它用SGD优化这些参数。所以,这里与其写作:
for p in model.parameters().p.sub_(lr*p.grad)
,不如直接用opt.step()。这是相同的东西。只是代码少了,做的事情还是一样的。有意思的是,现在你可以用Adam替代SGD,你甚至可以添加类似权重衰减的东西,PyTorch里有更多现成的东西。这就是我们为什么想用optim.各种东西。背后的原理实际也就是我们在fastai里做的。
如果你使用optim.SGD,图形是之前这样的:

如果我们换成另外一个优化器(optimizer),看看会发生什么:

损失值扩散(diverged)了。我们看到过一个学生做的很棒的散度图,展示了散度(divergence)是什么样的。这是在你训练时,它的样子。因为我们用了不同的优化器,我们需要用不同的学习率。你不能继续训练,因为当损失率扩散的时候,权重会很大很大或者很小很小,它们不会矫正。所以我们再做一遍。

好,这是一个比较好的学习率。看这里,我们在第200个epoch到达了0.5以下。之前,SGD的版本里,它都没能达到这个水平。那么发生了什么?什么是Adam。让我来告诉你们。
Adam
我们要在Excel里做梯度下降。这是一些随机生成的数据:

这是随机生成的X,这些Y都是用ax+b计算出来的,a是2,b是30。这就是我们要尝试拟合的数据。这是SGD:

我们需要用SGD来做。在我们lesson 2 SGD notebook里,我们把整个数据集做为一个batch来处理。在刚刚的notebook里,我们做了mini-batch。在这个Excel里,我们要做在线梯度下降(online gradient descent),就是每一行作为一个batch。batch的大小是1。
按照惯例,我们先为斜率(slope)和截距(intercept)随机选择一个值,我选择用1,这没什么影响。我把数据拷到这里。这是x,y,截距(C1),斜率(C2),它们的值是1。我会引用这个单元格(C1)。

用这个截距和斜率得到的预测值是14乘以1,再加1,结果是15(E4),这是方差(F4):

现在,我需要计算梯度,这样才能更新。有两种计算梯度的方式。一个是解析解,你可以直接在Wolfram Alpha或其他工具中查出梯度,你可以手工计算,或者用工具查,这是得到的梯度(I1,I2)单元格 。
或者你可以用有限差分(finite differencing),记住梯度是输入数值的变化幅度除以输入数值变化,这些变化都很小。让我们做一个小小的改动。
这里,我们在截距上加了0.01(G4),然后计算我们的损失,你可以看到我们的损失度下降了一点,那我们的导数就是这个差值除以0.01(H4):

这叫做有限差分。你可以随时用有限差分来求导。它很慢。在实践中我们不使用它,但它对验证一些东西很有用。我们可以对a(斜率)做相同的事,把它加0.01,用差值除以0.01。

我们可以直接用解析解的方式来计算。你可以看到est de/db和de/db非常接近,和我们期望的一样(对est de/da 和 de/da来说也是一样)。

所以梯度下降也就是,用当前的权值(slope,D列)减去学习率和梯度的乘积,得到这个结果(new a,M列。 new b,N列)。现在,我们可以把这个截距和斜率拷贝的下一行,再做一遍。做很多次,最后我们完成了一个epoch。


epoch的最后,我们可以说,“太棒了,这是我们的斜率,我们把M32的new a拷贝到存放斜率slope的位置(C2),这是我们的截距,我们把它拷贝到存放截距intercept的位置(C1),现在用它们做下一个epoch”。


复制粘贴有点麻烦,我创建了一个复杂的宏来帮你们做复制粘贴(我只是录制了它)。我创建了一个复杂的循环来做5次:
我把它添加到Run按钮,这样,如果我按run,它会执行,计算5遍,记录下每次的偏差。

这就是SGD。可以看到,它慢得令人发指,截距被设成了30,我们只更新到了1.57,它运行的太慢了。我们来加速它。
动量(Momentum)
我们可以用来加速它的第一个东西是这个叫动量的东西。还是原来那个电子表格文件。我去掉了有限差分的部分,它们用处不是很大,只留下解析解的部分。de/db是存放导数的,要用导数来做更新。

我做的是用导数乘以0.1,用上一轮的更新结果乘以0.9,把这两个加起来。换句话说,我做的更新不是基于导数,而是导数的1/10和上一次方向的90%。这叫做动量。想想在寻找最小值时发生了什么。

你在这里,你的学习率太小,你还是保持原来的步子(step)。如果你保持原来的步子,如果你还是在上次的结果上添加,你的步子会越来越大,直到幅度太大以至于跨过了最低点。现在,你的梯度将指向另外一个方向,这与momentum相反。这样你可以这个方向再走很小一步,然后反向向最低点小步靠近,然后像大步、大步、小步、小步...像这样。这就是动量算法。

如果一步走得太大,像这样,这种也很慢。那么你最后几步的平均值是这两个之间的中间位置,这是一个很普遍的想法,不是吗?你在第T次的这一步等于某个数(人们经常用alpha)乘以我要做的东西(这里是梯度)加上1减去alpha乘以你上一次的值:

这叫做指数加权移动平均数(exponentially weighted moving average)。为什么这样叫,你可以想下,这些会随着移动累计相乘,所以这也包含了,同理,也包含了。换句话说,最终变成了我想要的梯度() 加上前几步的加权平均值,其中时间上越近的值,拥有越高的权重。这个概念之后会不断出现。这就是动量。也就是我想顺着当前梯度前进,再加上前几步的指数加权平均值,这很有用,叫做含动量的梯度下降。

我们可以通过把这里(Adam)改成SGD加上动量参数来实现:
opt = optim.Adam(model.parameters(), lr)
opt = optim.SGD(model.parameters(), lr, momentum=0.9)
将动量设为常见的0.9。对基本的问题来说,它大概都是用0.9。这就是如何使用包含了动量来做梯度下降(do SGD with momentum)。还是一样,这并不是简化的版本,展示的就是SGD还是一样,你可以自己写。试着写出来。在lesson 2 SGD上添加动量是一个很好的作业。或者在这个做MNIST的notebook里不再用optim.,自己写一个包含动量的update函数。
RMSprop
有一个很酷的东西叫RMSProp。关于RMSProp,最酷的事情是Geoffrey Hinton提出了它,所有人都使用它。它特别流行,特别普遍。RMSProp的准确出处是Coursera在线MOOC,在那里他第一次提出了RMSProp,我喜欢这种在MOOC上出现而不是在论文里出现的新方法。
RMSProp和动量很像,但是这次,我们的指数加权移动平均数不是关于梯度更新值的,而是关于F8平方,它是梯度的平方。梯度平方乘以0.1加上前一个值乘以0.9。这是梯度平方的指数加权移动平均值。这个值代表什么呢?如果我的梯度很小,并且一直很小,它会是一个很小的数。如果我的梯度变化很大,它会是一个很大的数,或者如果它一直是很大的数值,那也将是一个很大的数字。

为什么这个问题有意思呢?因为当我们做更新时,我们让权重减去学习率乘以梯度除以上一次这个(RMSprop)的平方根。
换句话说,如果梯度一直很小,变化不大,那就做更大的跳跃。这是我们想要的,是吗?当我们观察到截距更新地这么慢时,很明显,你需要调试着让它更快些。
如果你现在运行这个,在5个epoch后,它已经达到3了。用基础(basic表格)的版本,在经过5的epoch时,它还是1.27。记住,我们目标是需要达到30。
Adam
显然,应该将RMSprop和动量结合起来一起使用,这个方法就是Adam,Adam就是追踪记录梯度平方的指数加权移动平均值(RMSProp),也追踪步长的指数加权移动平均值(动量)。这两个都除以RMSprop的平方根,并且,将上一步长在同一方向乘以0.9。这个变量是动量和RMSProp,它们被叫做Adam。看这个,5步,我们得到了25。

人们把这样的优化算法叫做动态学习率。很多人误解了它,说你不必设置学习率。你当然设置了。就如同尝试找到可以让跳跃变得更快的参数,或者持续按同一个方向更新的参数。这不代表你不需要学习率。我们还需要学习率。事实上,如果你再运行一遍这个,它会更好,但最终会在同一个位置上下波动。你可以看到学习率太高了。我们可以把它减小一点,再运行一会儿。现在很接近30了。
你可以看到,即使有了Adam,你还是需要学习率退火。
这是权重衰减和Adam。Adam非常快。
我们不想使用optim.之类的函数,来自己创建优化算法(optimizer)和所有这些东西。因为我们更倾向于使用learner,learner就是做这些东西的。还是一样,没有魔法。如果你创建一个learner,data是你的data bunch,Mnist_NN是PyTorch nn.Module实例,loss_funcc是损失函数,metrics是度量函数(metrics)。记住,度量函数就是要来打印出来的东西。就是这样。
learn = Learner(data, Mnist_NN(), loss_func=loss_func, metrics=accuracy)
learn.lr_find()
learn.recorder.plot()
然后你运行learn.lr_find,它记录下这个:

你可以用fit_one_cycle替代fit。这些函数很有帮助。
通过用lr_finder()可以找到一个好的学习率。看这里,我的损失度是0.13,而之前并没有得到比0.5低很多的损失值:
learn.fit_one_cycle(1, 1e-2)
Total time: 00:03
epoch train_loss valid_loss accuracy
1 0.148536 0.135789 0.960800 (00:03)

这些调整工具(微调)帮助我们取得了巨大的改进,不只是微小的提升。这只是一个epoch。
Fit_one_cycle
这个fit_one_cycle是做什么的呢?它究竟做了什么?这是它做的:
learn.recorder.plot_lr(show_moms=True)

我们之前看过左边这个图。提醒下,这是在画每个batch的学习率。记住,Adam有学习率,我们默认使用Adam(或者变种,我们可能会讲)。学习率开始时很低,它在前半部分不断增长,然后在后半部分逐渐递减。因为在最开始时,我们不知道我们的损失值在哪。我们在某个波动很大的函数空间里。如果你一开始时就大幅度跳跃,那些函数中起伏的地方会有很大的梯度,会把你带进非常夸张的损失值的位置。所以让我们从小学习率慢慢开始,这样你就会逐渐进入合理的函数空间。一旦进入合理的函数空间,你可以调大你的学习率,因为梯度已经处于预期的移动方向上了,如我们之前讨论好几次的,当你接近最终结果时,你需要用学习率退火机制来调节训练来达到最优的结果。但有趣的是,右边RW1 的图画的是动量图,而且每当学习率很小的时候,我们的动量就很高,这是为什么呢?因为如果你有一个很小的学习率,但是你持续同一个方向进行,这样不如前进地快一点对吗?但如果你跳跃的幅度太大,不要跳跃地太大,因为这可能把你带入很差的参数空间。然后当你接近尾声,你在进行微调,但实际上,如果你持续在同一个方向上一直走下去,不如加快学习率走得快一些。所以这个组合叫做one cycle。 这个方法既简单有有效。它能帮你达到super convergence(超收敛),让你的训练速度调高十倍。这是去年才发的论文。你们当中的一些人可能看过我上周对Leslie Smith的采访。他是一个令人惊奇的人,难以置信的谦虚,做了开创性研究,已经60多岁了,这些都令人振奋。
我还会演示一些其他的有趣的事情。当你用fastai画损失度时,它看起来不是这样的:

它是这样:
learn.recorder.plot_losses()

为什么?因为fastai为你计算了损失度的指数加权移动平均数。这个指数加权的概念,很方便,我经常使用它。它可以让图形更容易被解读。这意味着用fastai画出的图可能会有 一两个batch的滞后。当使用指数加权移动平均值时,有些许不好的地方,因为它需要使用历史数据来生成。但它可以让作出的图更简单易懂。
查看Tabular models笔记
该数据是用来预测谁的收入会更高。这是一个分类模型。我们有一系列类别变量(categorical variables)和一些连续变量,首先我意识到的是我们还不知道如何预测类别变量,目前为止我们还徘徊在一个简单的事实,即lesson5中的损失函数为nn.CrossEntropyLoss().这个函数式是什么?让我们来看看。当然,我们通过excel来找出答案。
交叉熵损失只是另一种损失函数,你已经学过一种损失函数了,也就是均方误差MSE,.但这不是一个适合这里使用的损失函数,因为在我们的例子里,对于MNIST数据,我们有10个可能的数字,10个激活值,每个激活值都对应要预测数字的概率。所以我们需要一个函数,能自信得预测出正确结果。并且又很低的损失值。而如果自信得预测成了错误结果,应该有很高的损失值,这是我们想要的,所以这里有一个例子,这里是cat vs dog,经过了one-hot编码。

这是猫狗的激活值

这是猫的概率和是狗的概率。0.5说明确定是猫还是狗,0.5的值对应的损失值为0.3,而预测为猫的概率为0.98对应的损失值为0.01。对于非常确定地预测出了错误的结果,所以损失值为1,我们该如何做呢?交叉熵损失就可以实现。

公式为:是否是猫乘以log(是猫的概率)【也就是乘以log(猫的激活值)】- 是否是狗乘以log(是狗的概率),也就是说,猫狗矩阵就是所有的one-hot矩阵,乘上你的所有的激活值,在求和。有趣的是,G列的数值和F列的数值是一样的,但它们的生成方法不同。(G列)我使用了if函数,因为这就是当取0时实际上没有加任何东西进来,所以函数内容实际上一样,也就是说,如果是猫,那就对是猫的概率去对数,如果它是一条狗,则反之。对(1-是猫的概率)取对数,换句话说就是log(是狗的概率)。所以这个one-hot编码乘以激活值再求和,在if函数里是一样的。你想想看,因为这实际上是一个矩阵乘法,从我们在嵌入矩阵学习中我们知道,这和索引查找法是一样的。所以你可以做交叉熵,也可以对应真实值,查找激活值的对数,这种方法的前提是,每行猫和狗的概率之后为1,这也是为什么你可以得到像这样奇怪的交叉熵数值,这也是为什么我之前说你按了“错误的按钮”,因为如果它们加起来不等于1,就麻烦了。所以你怎么确保它们加起来是1呢?确保和是1的方法是对最后一层使用正确的激活函数,而这里正确的激活函数是softmax。

softmax是一种激活函数,它的激活值的和为1,而且每个激活值都大于0,而且所有激活值都小于1,这就是我们想要的,也是我们所需要的。那怎么做到呢?比如说我们现在要预测5种事物中的一种,cat,dog,plane,fish,building,而outpot是我们的神经网络产生的,一组预测值,如果我把这组数变成e的指数会怎么样?这是一个对的方向,因为这会使结果大于0,所以我们有了一组大于0的数。对这些值求和,softmax列就是e(神经网络输出值)除以e(输出值)的求和,得到的softmax值总会小于1。

所以当我们要给多个类别做单一标记分类的时候,我们通常需要用softmax作为我们的激活函数,以及用交叉熵作为损失函数,因为它们通常都是一起使用,而且用起来也好用,pytorch让我们能够轻松地使用它们。你可能注意到在MNISTl例子里,我从没在里面添加softmax函数。因为如果你要求交叉熵损失的话,softmax实际上已经在损失函数里面了,所以它其实不仅仅是交叉熵损失,它实际上是先做了softmax然后在再算交叉熵损失。所以你可能已经注意到了这一点,有时你的模型预测值会看起来更像这些output列的数值,非常大的数值且有负号,而不是这些大于0小于1,且和为1的数值。原因是pytorch模型里并没有softmax函数,因为我们直接使用了交叉熵损失,所以你可能得自己写softmax函数。fastai已经越来越擅长了解什么时候回发生这类事情。一般情况下,如果我们能识别你用的损失函数,当你做预测时,我们会自动为你添加softmax函数,但如果你使用自定义的损失函数,比如你可能在后台调用了交叉熵损失或者类似的操作,那么你可能会生成像这样output列的预测值。
我们还有3分钟,我还要讲一个问题。下周,我们会在最初10分钟,结束表格数据部分,这个是表格模块里的forward:

它遍历了一组embedding。每个嵌入矩阵被赋值为e,你可以把它作为一个函数来使用,所以它会将每一个类别变量传给对应的嵌入矩阵,它会将它们连接在一起成为一个矩阵,然后执行一系列层计算,它们基本上都是线性层。然后会做sigmoid函数变换。这里只有两个知识点要学了。一个是dropout(随机失活),另一个是bn_cont,batch norm(批量归一化处理)。这是另外两个的正则化策略(regularization strategies)。Batch Norm做的不只是正则化,还做了其他事情,但主要是用来正则化。做正则化的基本方式是权重衰减、batch norm和dropout。你也可以用数据增强(data augmentation)来避免过拟合。下周课程开始的时候,我们会接触batch norm和dropout。我们也会学数据增强。然后,我们会学习什么是卷积。我们要学习一些新的计算机视觉架构和一些新的计算机视觉应用。但基本上,我们差不多了。你们已经知道了整个collab.py(fastai.collab)是怎么运行的。你们知道了为什么会有它、它是怎样的,也快要知道整个表格数据模型的主要功能。表格数据模型,如果你把它用在Rossmann的数据上,你会得到和我给你看的论文里一样的结果。你会得到第二名的结果。事实上,我们的结果会更好一点。下周,我会演示给你们看我做了哪些额外的实验,并找到了一些提升效果的小技巧。