来源 | Deep Learning with PyTorch
作者 | Stevens, et al.
译者 | 杜小瑞
校对 | gongyouliu
编辑 | auroral-L
全文共3602字,预计阅读时间25分钟。
第五章 学习的机制(中)
4. 沿梯度往下走
4.1 减少损失
4.2 进行分析
4.3 通过迭代拟合模型
4.4 输入规范化
4.5 再次可视化
4. 沿梯度往下走
我们将使用梯度下降算法优化关于参数的损失函数。在本节中,我们将根据第一原理建立梯度下降的直觉,这在将来对我们有很大帮助。正如我们提到的,有一些方法可以更有效地解答我们的例子,但这些方法不适用于大多数深度学习任务。梯度下降实际上是一个非常简单的想法,它可以很好地扩展到具有数百万个参数的大型神经网络模型。
图 5.5 优化过程的卡通描述,其中一个人用w和b的旋钮搜索使损失减少的方向转动旋钮
让我们从一个心理图像开始,我们在图5.5中很方便地勾勒出了它。假设我们在一台有两个旋钮的机器前面,标有w和b。我们可以在屏幕上看到损失的值,我们被告知要最小化这个值。由于不知道旋钮对损失的影响,我们开始摆弄它们,并为每个旋钮决定哪个方向使损失减小。我们决定将两个旋钮朝着减少损失的方向旋转。假设我们远离最佳值:我们可能会看到损失迅速减少,然后随着接近最小值而减慢。我们注意到,在某个点上,损失再次上升,所以我们反转一个或两个旋钮的旋转方向。我们还了解到,当损失变化缓慢时,最好更精细地调整旋钮,以避免达到损失回升的程度。过了一会儿,我们终于收敛到最小值。
4.1 减少损失
梯度下降和我们刚才描述的场景没有太大区别。其思想是计算每个参数的损失变化率,并朝着减少损失的方向修改每个参数。就像我们摆弄旋钮时一样,我们可以通过在w和b上加一个小数字来估计变化率,并观察该区域的损失变化:
这就是说,在w和b的值附近,w的单位增加会导致损失的一些变化。如果变化为负,那么我们需要增加w以最小化损失,而如果变化为正,我们需要减少w。那么应该变化多少呢?对w应用一个与损失变化率成比例的变化是一个好主意,特别是当损失有几个参数时:我们应该调整那些对损失产生显著变化的参数。一般来说,缓慢地改变参数也是明智的,因为在距离当前w值附近的距离处,变化率可能会有很大的不同。因此,我们通常应该微调变化率参数。这个比例因子有很多名字;我们在机器学习中使用的是学习率:
我们可以对参数b做同样的事情:
这表示梯度下降的基本参数更新步骤。通过重复这些调整公式(如果我们选择一个足够小的学习率),我们将收敛到一个参数的最佳值,在给定的数据上计算的损失是最小的。我们将很快展示完整的迭代过程,但是我们刚刚计算变化率的方法相当粗糙,需要在继续之前进行升级。让我们看看原因和方法。
4.2 进行分析
通过对模型和损失的重复评估来计算变化率,以探索w和b附近的损失函数的行为,对于具有多个参数的模型来说并不能很好地进行扩展。而且,人们并不总是清楚这个范围应该有多大。在上一节中,我们选择delta等于0.1,但是作为w和b的函数,它完全取决于损失的形状。如果损失比起delta变化太快,我们就不太清楚损失在哪个方向上减少最大。
如果我们能使邻域变得无穷小,如图5.6所示,那会怎样?这正是当我们解析地取损失对一个参数的导数时所发生的情况。在一个有两个或多个参数的模型中,比如我们正在处理的那个,我们计算损失对每个参数的导数,然后把它们放在一个导数向量中,也就是梯度。
图 5.6 在离散位置评估下降方向时估计的下降方向与分析方向的差异
计算导数
为了计算损失对参数的导数,我们可以应用链式法则,计算损失对输入(即模型的输出)的导数,乘以模型对参数的导数:
回想一下,我们的模型是一个线性函数,我们的损失是平方和。让我们算出导数的表达式。回忆一下损失的定义:
记住dx^2/dx=2x,我们得到
将导数应用于模型
对于模型,回想一下我们的模型是
我们得到这些导数:
定义梯度函数
把所有这些放在一起,返回关于w和b的损失梯度的函数是:
用数学符号表示的相同想法如图5.7所示。同样,我们对所有数据点进行平均(即,求和并除以一个常数),以得到损失的每个偏导数对应的标量。
图 5.7 损失函数对权重的导数
4.3 通过迭代拟合模型
我们现在已经准备好了优化参数的一切。从一个参数的初始值开始,我们可以迭代地对其应用更新,以获得固定的迭代次数,或者直到w和b停止更改为止。停止迭代的判定准则可以有很多种;现在,我们使用固定的迭代次数来停止。
训练循环
既然谈到这,让我们介绍另一个术语。我们将训练迭代称为一个epoch,在此过程中,我们更新所有训练样本的参数。
完整的训练循环如下所示(code/p1ch5/1_parameter_estimation.ipynb):
本文中用于输出的实际日志逻辑更为复杂(参见同一notebook中的单元格15:http://mng.bz/pBB8)但这些差异对于理解本章的核心概念并不重要。
现在,让我们调用我们的训练循环:
过拟合
等等,发生什么事了?我们的训练过程真的失败了,导致损失成为inf。这是一个明显的迹象,表明params正在接收太大的更新,并且它们的值开始来回振荡,因为每次更新都超出了范围,下一次更是超出了范围。优化过程是不稳定的:它发散而不是收敛到最小值。我们希望看到params的更新越来越小,而不是越来越大,如图5.8所示。
图 5.8 顶部:在凸函数(抛物线状)上由于步长太大而导致优化过程发散。底部:小步优化最终收敛
我们如何限制学习率的幅度?嗯,看起来很简单。我们可以简单地选择一个较小的学习率,事实上,学习率是我们通常会改变的事情之一,当训练结果不如我们所愿。我们通常更改学习速率的数量级,因此我们可以尝试使用1e-3或1e-4,这将按数量级减少更新的速率。让我们使用1e-4,看看它是如何工作的:
很好,现在行为稳定了。但还有另一个问题:参数的更新非常小,因此损失的减少非常缓慢,最终会停滞。我们可以通过使学习率自适应来避免这个问题:也就是说,根据更新的大小而改变。有一些优化方案可以做到这一点,我们将在本章末尾的第5.5.2节中看到。
然而,在更新术语中还有另一个潜在的麻烦制造者:梯度本身。让我们回头看看优化过程中epoch 1的梯度。
4.4 输入规范化
我们可以看到,第一个epoch权重的梯度大约是偏差(bias)梯度的50倍。这意味着权重和偏差存在于不同比例的空间中。如果是这种情况,一个足够大的学习率,更新第一个刚刚好,但是对第二个的更新来说就改变太大了;而对第二个更新合适的学习率又对第一个的更新太小了。这意味着,除非我们对问题的公式有所改变,否则我们将无法更新我们的参数。我们可以对每个参数有单独的学习率,但是对于有许多参数的模型来说,这太麻烦了,无法满足这么多需求。
有一个更简单的方法来控制事情:改变输入,这样梯度就不会有太大的差别。粗略地说,我们可以确保输入的范围不会离–1.0到1.0太远。在我们的例子中,我们可以通过简单地将t_u乘以0.1来获得足够接近的结果:
这里,我们通过在变量名后面加一个n来表示t_u的规范化版本。此时,我们可以在标准化输入上运行训练循环:
即使我们将学习速率设置回1e-2,参数也不会在迭代更新期间爆炸。让我们看看梯度:它们的大小相似,所以对这两个参数使用一个单一的学习速率就可以了。我们也许可以做一个更好的标准化工作,而不是一个简单的10倍缩放,但既然这样做就解决了问题,我们先就这样处理。
注意
这里的规范化绝对有助于训练网络,但是你可以提出一个论点,即不需要严格地为这个特定问题优化参数。那绝对是真的!这个问题很小,有很多方法可以处理参数。然而,对于更大、更复杂的问题,规范化是一种简单有效的工具(如果不是关键的话!),可以改进模型收敛性。
让我们运行循环足够多的迭代次数,以看到参数中的更改变小。我们将把n_epochs改为5000:
很好:当我们沿着梯度下降的方向改变参数时,我们的损失减少了。它不完全归零;这可能意味着迭代的次数太少还不足以收敛到零,或者数据点不完全位于一条直线上。正如我们所预料的,我们的测量结果并不完全准确,或者读数中有噪音。
但是你看:w和b的值看起来非常像我们需要用来将摄氏度转换成华氏度的数字(考虑到我们之前将输入乘以0.1时的标准化)。精确值为w=5.5556和b=-17.7778。我们奇特的温度计一直显示华氏温度。这不是一个什么宏伟的工程,除了我们的梯度下降优化过程真的起作用了!
4.5 再次可视化
让我们重温一下我们刚开始做的事情:绘制数据。说真的,这是做数据科学的人应该做的第一件事。始终绘制数据的检查结果:
我们在这里使用一个名为argument unpacking的Python技巧:*params意味着将params的元素作为单个参数传递。在Python中,这通常是通过列表或元组来完成的,但是我们也可以使用PyTorch张量的argument unpacking,这些张量是沿着第一个维度拆分的。所以这里,model(t_un,*params)等价于model(t_un,params[0],params[1])。
此代码生成图5.9。我们的线性模型似乎是一个很好的数据模型。我们的测量结果似乎也有些不稳定。我们要么给验光师打电话要一副新眼镜,要么考虑把我们的高级温度计还给他。
图 5.9 线性拟合模型(实线)与输入数据(圆)的关系图
剩下的内容,请继续关注「数据与智能」~