四、神经网络的学习(下)
目录
4.4 梯度
4.4.1 梯度法
4.4.2 神经网络的梯度
4.5 学习算法的实现
4.5.1 2层神经网络的类
4.5.2 mini-batch的实现
4.5.3 基于测试数据的评价
4.6 小结
4.4 梯度
在刚才的例子中,我们按变量分别计算了x0和x1的偏导数。现在,我们希望一起计算x0和x1的偏导数。比如,我们来考虑求x0 = 3, x1 = 4时(x0, x1)的偏导数
。另外,像
这样的由全部变量的偏导数汇总而成的向量称为梯度(gradient)。梯度可以像下面这样来实现
代码解释
def numerical_gradient(f, x):h = 1e-4 # 0.0001grad = np.zeros_like(x) # 生成和x形状相同的数组
f
: 这是您要求偏导数的目标函数,例如f(x) = x[0]**2 + x[1]**2
。x
: 这是您要计算的具体点,例如np.array([3.0, 4.0])
,对应x0=3, x1=4
。grad = np.zeros_like(x)
: 这里初始化了一个与x
形状完全相同的全零数组(例如[0., 0.]
),用于存储即将计算出的梯度向量(∂f/∂x0, ∂f/∂x1)
for idx in range(x.size):tmp_val = x[idx]
- 开始一个循环,遍历
x
中的每一个元素(每一个变量)。 idx=0
对应x0
,idx=1
对应x1
。tmp_val = x[idx]
: 保存变量的当前值(例如3.0
),因为后面要修改它来计算差分。
# f(x+h)的计算x[idx] = tmp_val + hfxh1 = f(x)
- 计算中心差分的第二步:将当前变量
x[idx]
的值减少一个微小量h
(例如x0
从3.0
变为2.9999
),其他变量保持不变。 - 然后计算函数
f
在这个新点x
的值fxh2
(例如f(2.9999, 4.0)
)。
grad[idx] = (fxh1 - fxh2) / (2*h)x[idx] = tmp_val # 还原值
- 计算偏导数:利用中心差分公式
(f(x+h) - f(x-h)) / (2h)
计算出函数f
在点x
处关于当前变量x[idx]
的偏导数。 - 将这个偏导数值存入梯度向量
grad
的对应位置grad[idx]
。 - 还原值:这是非常关键的一步!在计算完一个变量的偏导后,必须将
x[idx]
的值恢复为原始值 (tmp_val
),然后再开始计算下一个变量的偏导。这样才能保证每次计算都基于原始点(3, 4)
。
return grad
- 循环结束后,梯度向量
grad
的每个位置都存储了对应的偏导数。 - 最终返回完整的梯度向量
grad
,例如np.array([6.00000000000378, 7.999999999999119])
,分别代表∂f/∂x0
和∂f/∂x1
。
如何使用这个函数
对于您的例子 f(x) = x[0]**2 + x[1]**2
,求在 (3, 4)
处的梯度:
import numpy as np# 定义目标函数
def f(x):return x[0]**2 + x[1]**2# 定义要计算的点
x_point = np.array([3.0, 4.0])# 调用函数计算梯度
gradient_at_point = numerical_gradient(f, x_point)print(gradient_at_point)
# 输出将会是一个非常接近 [6., 8.] 的数组,例如 [6.00000000000378, 7.999999999999119]
函数numerical_gradient(f, x)中,参数f为函数,x为NumPy数组,该函数对NumPy数组x的各个元素求数值微分。现在,我们用这个函数实际计算一下梯度。这里我们求点(3, 4)、(0, 2)、(3, 0)处的梯度。
像这样,我们可以计算(x0, x1)在各点处的梯度。上例中,点(3, 4)处的梯度是(6, 8)、点(0, 2)处的梯度是(0, 4)、点(3, 0)处的梯度是(6, 0)。这个梯度意味着什么呢?
为了更好地理解,我们把
的梯度画在图上。不过,这里我们画的是元素值为负梯度B 的向量
后面我们将会看到,负梯度方向是梯度法中变量的更新方向。
如图4-9所示,
的梯度呈现为有向向量(箭头)。观察图4-9,我们发现梯度指向函数f(x0,x1)的“最低处”(最小值),就像指南针一样,所有的箭头都指向同一点。其次,我们发现离“最低处”越远,箭头越大。
虽然图 4-9 中的梯度指向了最低处,但并非任何时候都这样。实际上,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向 A。这是一个非常重要的性质
4.4.1 梯度法
机器学习的主要任务是在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。
这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
梯度方向是当前位置下降最快的方向,但它指向的只是最近的局部最低点,而非全局最低点。
函数的极小值、最小值以及被称为鞍点(saddle point)的地方,梯度为 0。极小值是局部最小值,也就是限定在某个范围内的最小值。鞍点是从某个方向上看是极大值,从另一个方向上看则是极小值的点。虽然梯度法是要寻找梯度为 0的地方,但是那个地方不一定就是最小值(也有可能是极小值或者鞍点)。此外,当函数很复杂且呈扁平状时,学习可能会进入一个(几乎)平坦的地区,陷入被称为“学习高原”的无法前进的停滞期。
在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)。
现在,我们尝试用数学式来表示梯度法,如式(4.7)所示。
式(4.7)的η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
式(4.7)是表示更新一次的式子,这个步骤会反复执行。也就是说,每一步都按式(4.7)更新变量的值,通过反复执行此步骤,逐渐减小函数值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过类似的式子(各个变量的偏导数)进行更新。
学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。
下面,我们用Python来实现梯度下降法。如下所示,这个实现很简单。
参数f是要进行最优化的函数,init_x是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。
使用这个函数可以求函数的极小值,顺利的话,还可以求函数的最小值。下面,我们就来尝试解决下面这个问题。
问题:请用梯度法求
的最小值。
这里,设初始值为(-3.0, 4.0),开始使用梯度法寻找最小值。最终的结果是(-6.1e-10, 8.1e-10),非常接近(0,0)。实际上,真的最小值就是(0,0),所以说通过梯度法我们基本得到了正确结果。如果用图来表示梯度法的更新过程,则如图4-10所示。可以发现,原点处是最低的地方,函数的取值一点点在向其靠近。
前面说过,学习率过大或者过小都无法得到好的结果。我们来做个实验验证一下。
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。也就是说,设定合适的学习率是一个很重要的问题。
像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。
4.4.2 神经网络的梯度
神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。
比如,有一个只有一个形状为2 × 3的权重W的神经网络,损失函数用L表示。此时,梯度可以用
表示。用数学式表示的话,如下所示。
下面,我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet的类
如何理解这段代码?
我们要建造一个机器(神经网络),它能把2种原料(输入特征)加工成3种产品(分类得分)。这个加工过程全靠一个配方表(权重矩阵
W
)。在机器刚造好时,我们完全不知道正确的配方是什么。
np.random.randn(2, 3)
就像是随机地生成了一张初始配方表,上面的各种成分搭配(权重值)都是随机填写的,有正有负,有的大有的小。接下来,神经网络的学习算法(如梯度下降)就会扮演“质检员”的角色:它用这张随机配方表试生产一次,看看产品(预测结果)和标准样品(真实标签)差多远(计算损失),然后根据差距来反推配方表应该如何修改(计算梯度并更新
W
)。通过成千上万次这样的“试生产-修改配方”的迭代,最终我们就能得到一张优秀的配方表(训练好的权重
W
),使我们的机器能高效地生产出正确的产品。
simpleNet类只有一个实例变量,即形状为2×3的权重参数。它有两个方法,一个是用于预测的predict(x),另一个是用于求损失函数值的loss(x,t)。这里参数x接收输入数据,t接收正确解标签。现在我们来试着用一下这个simpleNet。
接下来求梯度。和前面一样,我们使用numerical_gradient(f, x)求梯度(这里定义的函数f(W)的参数W是一个伪参数。因为numerical_gradient(f, x)会在内部执行f(x),为了与之兼容而定义了f(W))。
numerical_gradient(f, x) 的参数f是函数,x是传给函数f的参数。因此,这里参数x取net.W,并定义一个计算损失函数的新函数f,然后把这个新定义的函数传递给numerical_gradient(f, x)。
numerical_gradient(f, net.W)的结果是dW,一个形状为2 × 3的二维数组。
观察一下dW的内容,例如,会发现
中的的值大约是0.2,这表示如果将
增加h,那么损失函数的值会增加0.2h。再如,
对应的值大约是−0.5,这表示如果将
增加h,损失函数的值将减小0.5h。因此,从减小损失函数值的观点来看,
应向正方向更新,
应向负方向更新。至于更新的程度,
比
的贡献要大。
求出神经网络的梯度后,接下来只需根据梯度法,更新权重参数即可。
在下一节中,我们会以2层神经网络为例,实现整个学习过程。
4.5 学习算法的实现
神经网络的学习步骤如下所示。
神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini batch数据,所以又称为随机梯度下降法
4.5.1 2层神经网络的类
首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类,实现过程如下所示
loss(self, x, t)是计算损失函数值的方法。这个方法会基于predict()的结果和正确解标签,计算交叉熵误差。
剩下的numerical_gradient(self, x, t)方法会计算各个参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。另外,gradient(self, x, t)是下一章要实现的方法,该方法使用误差反向传播法高效地计算梯度。
numerical_gradient(self, x, t)基于数值微分计算参数的梯度。下一章,我们会介绍一个高速计算梯度的方法,称为误差反向传播法。用误差反向传播法求到的梯度和数值微分的结果基本一致,但可以
高速地进行处理。使用误差反向传播法计算梯度的gradient(self, x, t)方法会在下一章实现
4.5.2 mini-batch的实现
神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。
这里,mini-batch的大小为100,需要每次从60000个训练数据中随机取出100个数据(图像数据和正确解标签数据)。然后,对这个包含100笔数据的mini-batch求梯度,使用随机梯度下降法(SGD)更新参数。这里,梯度法的更新次数(循环的次数)为10000。每更新一次,都对训练数据计算损失函数的值,并把该值添加到数组中。
观察图4-11,可以发现随着学习的进行,损失函数的值在不断减小。这是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。也就是说,神经网络的确在学习!通过反复地向它浇灌(输入)数据,神经网络正在逐渐向最优参数靠近
4.5.3 基于测试数据的评价
根据图4-11呈现的结果,我们确认了通过反复学习可以使损失函数的值逐渐减小这一事实。不过这个损失函数的值,严格地讲是“对训练数据的某个mini-batch的损失函数”的值。训练数据的损失函数值减小,虽说是神经网络的学习正常进行的一个信号,但光看这个结果还不能说明该神经网络在其他数据集上也一定能有同等程度的表现。
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。
神经网络学习的最初目标是掌握泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下训练数据和测试数据的识别精度。
epoch是一个单位。一个 epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于 10000笔训练数据,用大小为 100笔数据的mini-batch进行学习时,重复随机梯度下降法 100次,所
有的训练数据就都被“看过”了。此时,100次就是一个 epoch。
为了正确进行评价,我们来稍稍修改一下前面的代码。与前面的代码不同的地方,我们用粗体来表示
在上面的例子中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个epoch的识别精度,是因为如果在for语句的循环中一直计算识别精度,会花费太多时间。并且,也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别 精度。
把从上面的代码中得到的结果用图表示的话,
4.6 小结
本章中,我们介绍了神经网络的学习。首先,为了能顺利进行神经网络的学习,我们导入了损失函数这个指标。以这个损失函数为基准,找出使它的值达到最小的权重参数,就是神经网络学习的目标。为了找到尽可能小的损失函数值,我们介绍了使用函数斜率的梯度法。
- 机器学习中使用的数据集分为训练数据和测试数据。
- 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力。
- 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。
- 利用某个给定的微小值的差分求导数的过程,称为数值微分。
- 利用数值微分,可以计算权重参数的梯度。
- 数值微分虽然费时间,但是实现起来很简单。下一章中要实现的稍微复杂一些的误差反向传播法可以高速地计算梯度。