专栏——深度学习入门笔记
推荐文章
——————————————————————————————————————————————————————
文章目录
本章的主题是神经网络的学习。这里所说的“学习”是指 从训练数据中自动获取最优权重参数 的过程。本章中,为了使神经网络能进行学习,将导入 损失函数 这一指标。而学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值,本章我们将介绍利用了 函数斜率的梯度法。
1. 从数据中学习
神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。这是非常了不起的事情!因为如果所有的参数都需要人工决定的话,工作量就太大了。在实际的神经网络中,参数的数量成千上万,在层数更深的深度学习中,参数的数量甚至可以上亿,想要人工决定这些参数的值是不可能的。本章将介绍神经网络的学习,即利用数据决定参数值的方法,并用Python实现对MNIST手写数字数据集的学习。
1.1 数据识别
人们以自己的经验和直觉为线索,通过反复试验推进工作。而机器学习的方法则 极力避免人为介人,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入。
思考一个问题:如何实现数字‘5’的识别,如下图,如果让我们自己来设计一个能将5正确分类的程序,就会意外地发现这是一个很难的问题。人可以简单地识别出5,但却很难明确说出是基于何种规律而识别出了5。此外,从图4-1中也可以看到,每个人都有不同的写字习惯,要发现其中的规律是一件非常难的工作。
因此,与其绞尽脑汁,从零开始想出一个可以识别 5 的算法,不如考虑通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输人图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。
如图4-2所示,神经网络直接学习图像本身。在第2个方法,即利用特征量和机器学习的方法中,特征量仍是由人工设计的,而在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。
神经网络的优点是对所有的问题都可以用同样的流程来解决。比如,不管要求解的问题是识别5,还是识别狗,抑或是识别人脸,神经网络都是通过不断地学习所提供的数据,尝试发现待求解的问题的模式。也就是说,与待处理的问题无关,神经网络可以将数据直接作为原始数据,进行 “端对端” 的学习。
1.2 训练数据和测试数据
机器学习中,一般将数据分为 训练数据 和 测试数据 两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为 监督数据。
泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。比如,在识别手写数字的问题中,泛化能力可能会被用在自动读取明信片的邮政编码的系统上。此时,手写数字识别就必须具备较高的识别“某个人”写的字的能力。注意这里不是“特定的某个人写的特定的文字”,而是“任意一个人写的任意文字”。如果系统只能正确识别已有的训练数据,那有可能是只学习到了训练数据中的个人的习惯写法。
因此,仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。顺便说一下,只对某个数据集过度拟合的状态称为 过拟合(over ftting)。避免过拟合也是机器学习的一个重要课题。
2. 损失函数
神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为 损失函数(loss function)。这个损失函数可以使用任意函数,但一般用 均方误差 和 交叉熵误差 等。
损失函数是表示神经网络性能的“恶劣程度"的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。以“性能的恶劣程度"为指标可能会使人感到不太自然,但是如果给损失函数.上一个负值,就可以解释为“在多大程度上不坏”,即“性能有多好”。并且,“使性能的恶劣程度达到最小”和“使性能的优良程度达到最大"是等价的,不管是用“恶劣程度”还是“优良程度",做的事情本质上都是一样的。
2.1 均方误差
可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squarederror)。均方误差如下式所示。
E = 1 2 ∑ k ( y k − t k ) 2 (4.1) E = \frac{1}{2}\sum_k(y_k - t_k)^2\tag{4.1} E=21k∑(yk−tk)2(4.1)
这里, y k y_k yk 表示神经网络的输出, t k t_k tk 表示监督数据, k k k 表示数据的维数。比如, y k y_k yk、 t t t_t tt 是由如下10个元素构成的数据。
>>> y = [0.1, 0.05,0.6,0.0,θ.05, 0.1, θ.θ,0.1, 0.0, 0.0]
>>> t = [0,0,1,0,0,0,0,0,0,0]
数组元素的索引从第一个 开始依次对应数字“0” “1”…这里,神经网络的输出 y 是 softmax函数 的输出。由于softmax函数的输出可以理解为 概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05,“2” 的概率是0.6等。t 是监督数据,将正确解标签设为1,其他均设为0。这里,标签“2”为1,表示正确解是“2”。将正确解标签表示为1,其他标签表示为 0 的表示方法称为 one-hot表示。
如式(4.1)所示,均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。现在,我们用Python来实现这个均方误差,实现方式如下所示。
def mean_squared_error(y, t):
return 0.5 * np.sum((y - t)**2)
这里,参数 y 和 t 是NumPy数组。
>>> #设“2"为正确解
>>>t=[0,θ,1,日,θ,0,0,θ,θ,θ]
>>>
>>> #例1:“2”的概率最高的情况(日.6)
>>> y = [0.1, 0.05, 0.6, 0.0, 0.05,0.1, 0.0, 0.1, 0.0,0.0]
>>> mean_squared_error(np.array(y), np.array(t) )
0.097500000000031
>>>
>>> #例2:“7"的概率最高的情况(日.6)
>>> y =[0.1, 0.05, 0.1, 0.0, 0.05,0.1, 0.0, 0.6, 0.0, 0.0]
>>> mean_squared_error(np. array(y), np.array(t))
0.59750000000000003
这里举了两个例子。第一个例子中,正确解是“2”,神经网络的输出的最大值是“2”;第二个例子中,正确解是“2”,神经网络的输出的最大值是“7”。如实验结果所示,我们发现第一个例子的损失函数的值更小,和监督数据之间的误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。
2.2 交叉熵误差
除了均方误差之外,**交叉熵误差(cross entropy error )**也经常被用作损失函数。交叉熵误差如下式所示。
E = − ∑ k t k l o g y k (4.2) E = -\sum_kt_klogy_k\tag{4.2} E=−k∑tklogyk(4.2)
这里,log 表示以 e 为底数的自然对数( l o g e log_e loge)。 y h y_h yh 是神经网络的输出, t k t_k tk 是正确解标签。并且, t k t_k tk 中只有正确解标签的索引为1,其他均为0(one-hot表示)。因此,式(4.2) 实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差是 -log0.6= 0.51;若“2"对应的输出是0.1,则交叉熵误差为 -log0.1 = 2.30。.也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
如图4-3所示,x等于1时,y为0;随着x向0靠近,y逐渐变小。因此,正确解标签对应的输出越大,式(4.2)的值越接近0;当输出为1时,交叉熵误差为0。此外,如果正确解标签对应的输出较小,则式(4.2)的值较大。
下面,我们来用代码实现交叉熵误差。
def cross_ entropy_error(y, t):
delta = 1e-7
return -np.sum(t * np.log(y + delta))
这里,参数 y 和 t 是NumPy数组。函数内部在计算 np.log 时,加上了一个微小值 delta。这是因为,当出现np.log(0)时,np.log(0) 会变为负无限大的-inf,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。下面,我们使用 cross. entropy. error(y, t) 进行一些简单的计算。
>>> t = [0,0,1,0,θ,0,θ,0,θ,0]
>>> y = [0.1, 0.05,0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> cross_entropy_error(np.array(y), np. array(t))
0.51082545709933802
>>>
>>> y = [0.1,0.05,0.1,0.0,0.05,0.1,θ.θ,0.6,θ.θ,0.0]
>>> cross_entropy_error(np.array(y), np.array(t))
2.3025840929945458
第一个例子中,正确解标签对应的输出为0.6,此时的交叉熵误差大约为0.51。第二个例子中,正确解标签对应的输出为0.1的低值,此时的交叉熵误差大约为2.3。由此可以看出,这些结果与我们]前面讨论的内容是一致的。
2.3 mini—batch学习
机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有100个的话,我们就要把这100个损失函数的总和作为学习的指标。
前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式(4.3)。
E = − 1 N ∑ n ∑ k t n k l o g y n k (4.3) E = -\frac{1}{N}\sum_n\sum_kt_{nk}log y_{nk}\tag{4.3} E=−N1n∑k∑tnklogynk(4.3)
这里,假设数据有N个, t n k t_{nk} tnk 表示第 n 个数据的第 k 个元素的值( y n k y_{nk} ynk 是神经网络的输出, t n k t_{nk} tnk 是监督数据)。式子虽然看起来有一些复杂,其实只是把求单个数据的损失函数的式(4.2)扩大到了N份数据,不过最后还要除以N进行正规化。通过除以N,可以求单个数据的 “平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。 比如,即便训练数据有1000个或10000个,也可以求得单个数据的平均损失函数。
另外,MNIST数据集的训练数据有60000个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据 (称为 mini-batch,小批量),然后对每个mini-batch进行学习。比如,从6000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。
下面我们来编写从训练数据中随机选择指定个数的数据的代码,以进行mini-batch学习。在这之前,先来看一下用于读人MNIST数据集的代码。
import sys, OS
sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist
(x_train, t_train), (x_test, t_test) = load mnist (normalize=True,one_ hot_ label=True)
print(x_train.shape) # (60000, 784)
print(t_train.shape) # (60000,10)
load_mnist函数是用于读人MNIST数据集的函数。读入数据时,通过设定参数 one_hot_label=True,可以得到one-hot表示(即仅正确解标签为1,其余为0的数据结构)。
读入上面的MNIST数据后,训练数据有60000个,输人数据是784维(28 x 28)的图像数据,监督数据是10维的数据。因此,上面的x_train,t_train的形状分别是(60000, 784) 和(60000,10)。
那么,如何从这个训练数据中随机抽取10笔数据呢?我们可以使用NumPy的 np. random. choice(),写成如下形式。
train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_ size, batch_size)
x_batch = x_train[batch_ mask]
t batch = t_train[batch_ mask]
使用np. random. choice()可以从指定的数字中随机选择想要的数字*。比如,np. random. choice(60000,10) 会从0到59999之间随机选择10个数字。如下面的实际代码所示,我们可以得到一个包含被选数据的索引的数组。
>>> np.random.choice(60000,10)
array([ 8013, 14666, 58210, 23832, 52091, 10153, 8107, 19410, 27260, 21411])
之后,我们只需指定这些随机选出的索引,取出mini-batch,然后使用这个mini-batch计算损失函数即可。
2.4 mini-batch版交叉熵误差的代码实现
如何实现对应mini-batch的交叉熵误差呢?只要改良一下之前实现的对应单个数据的交叉熵误差就可以了。这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输人)两种情况的函数。
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(t * np.log(y + 1e-7)) / batch_size
这里,y是神经网络的输出,t是监督数据。y的维度为1时,即求单个数据的交叉熵误差时,需要改变数据的形状。并且,当输人为mini-batch时,要用batch的个数进行正规化,计算单个数据的平均交叉熵误差。
此外,当监督数据是标签形式(非one-hot表示,而是像“2”“7”这样的标签)时,交叉熵误差可通过如下代码实现。
def cross_entropy_error(y, t):
if y.ndim == 1:
t = t.reshape(1, t.size)
y = y.reshape(1, y.size)
batch_size = y.shape[0]
return -np.sum(np. log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
作为参考,简单介绍一下 np.log( y[np. arange(batch_ size),t] )。 np.arange(batch_size) 会生成一个从0到batch_size-1的数组。比如当batch_ size 为5时,np. arange(batch_size) 会生成一个NumPy数组 [0, 1, 2, 3, 4]。因为t中标签是以[2, 7, 0, 9, 4]的形式存储的,所以y[np. arange(batch_ size) ,t]能抽出各个数据的正确解标签对应的神经网络的输出(在这个例子中,y[np.arange(batch_ size), t] 会生成NumPy数组[y[0,2], y[1,7], y[2,0],y[3,9], y[4,4]])。
2.5 为何要设定损失函数
对于这一疑问,我们可以根据 “导数” 在神经网络学习中的作用来回答。之后会详细说到,在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方,需要计算参数的导数(确切地讲是梯度),然后以这个导数为指引,逐步更新参数的值。
假设有一个神经网络,现在我们来关注这个神经网络中的某个权重参数。此时,对该权重参数的损失函数求导,表示的是 “如果稍微改变这个权重参数的值,损失函数的值会如何变化”。如果导数的值为负,通过使该权重参数向正方向改变,可以减小损失函数的值;反过来,如果导数的值为正,则通过使该权重参数向负方向改变,可以减小损失函数的值。不过,当导数的值为0时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。
损失函数通常使用的是sigmoid函数,而不是阶跃函数。阶跃函数就像“竹筒敲石”一样,只在某个瞬间产生变化。而sigmoid函数,如图4-4所示,不仅函数的输出(竖轴的值)是连续变化的,曲线的斜率(导数)也是连续变化的。也就是说,sigmoid函数的导数在任何地方都不为0。这对神经网络的学习非常重要。得益于这个斜率不会为0的性质,神经网络的学习得以正确进行。
3. 数值微分
梯度法使用梯度的信息决定前进的方向。本节将介绍梯度是什么、有什么性质等内容。在这之前,我们先来介绍一下导数。
3.1 导数
导数就是表示某个瞬间的变化量。它可以定义成下面的式子。
d
f
(
x
)
d
x
=
l
i
m
h
→
0
f
(
x
+
h
)
−
f
(
x
)
h
(4.4)
\frac{df(x)}{dx} = lim_{h→0}\frac{f(x + h) - f(x)}{h}\tag{4.4}
dxdf(x)=limh→0hf(x+h)−f(x)(4.4)
接下来,我们参考式(4.4),来实现求函数的导数的程序。
def numerical_ diff(f, x):
h=1e-4#日.0001
return (f(x+h) - f(x-h)) / (2*h)
如图4-5所示,数值微分含有误差。为了减小这个误差,我们可以计算函数f在(x + h)和(x- h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为 中心差分 (而(x + h)和x之间的差分称为 前向差分)。上面的代码就是基于该思想。
其次,h不能设置的太大,否则容易造成近似误差。如 h − 50 h^{-50} h−50在计算机中近似为0。
3.2 偏导数
接下来,我们看一下式(4.6)表示的函数。虽然它只是-一个计算参数的平方和的简单函数,但是请注意和上例不同的是,这里有两个变量。
f ( x 0 , x 1 ) = x 0 2 + x 1 2 (4.6) f(x_0,x_1)=x_0^2+x_1^2\tag{4.6} f(x0,x1)=x02+x12(4.6)
用python来实现:
def function_2(x):
return x[0]**2 + x[1]**2
这里,我们假定向参数输人了一个NumPy数组。函数的内部实现比较简单,先计算NumPy数组中各个元素的平方,再求它们的和( np. sum(**2) 也可以实现同样的处理)。我们来画一下这个函数的图像。结果如图4-8所示,是一个三维图像。
现在我们来求式(4.6)的导数。这里需要注意的是,式(4.6)有两个变量,所以有必要区分对哪个变量求导数,即对 x0 和 x1 两个变量中的哪一个求导数。另外,我们把这里讨论的有多个变量的函数的导数称为偏导数。用数学式表示的话,可以写成 ∂ f ∂ x 0 、 ∂ f ∂ x 1 \frac{∂f}{∂x_0}、\frac{∂f}{∂x_1} ∂x0∂f、∂x1∂f
如何求偏导?偏导数和单变量的导数一样,都是求某个地方的斜率。不过,偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为某个值。
4. 梯度
在刚才的例子中,我们按变量分别计算了 x0 和 x1 的偏导数。现在,我们希望一起计算 x0 和 x1 的偏导数。比如,我们来考虑求x0 = 3,x1 = 4时(xo, x1)的偏导数 ( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) (\frac{∂f}{∂x_0},\frac{∂f}{∂x_1}) (∂x0∂f,∂x1∂f)。另外,像 ( ∂ f ∂ x 0 , ∂ f ∂ x 1 ) (\frac{∂f}{∂x_0},\frac{∂f}{∂x_1}) (∂x0∂f,∂x1∂f) 这样的由全部变量的偏导数汇总而成的向量称为 梯度(gradient)。梯度可以像下面这样来实现。
def numerical_gradient(f, x):
h = 1e-4 # 0.0001
grad = np.zeros_like(x) #生成和x形状相同的数组
for idx in range(x.size):
tmp_val = x[idx]
# f(x+h)的计算
x[idx] = tmp_val + h
fxh1 = f(x)
# f(x-h)的计算
x[idx] = tmp_val.h
fxh2 = f(x)
grad[idx] = (fxh1.fxh2) / (2*h)
x[idx] = tmp_val #还原值
return grad
函数numerical gradient(f, x) 的实现看上去有些复杂,但它执行的处理和求单变量的数值微分基本没有区别。需要补充说明一下的是,np.zeros_like(x) 会生成一个形状和 x 相同、所有元素都为 0 的数组。
函数numerical. gradient(f, x) 中,参数 f 为函数,x 为NumPy数组,该函数对NumPy数组 x 的各个元素求数值微分。现在,我们用这个函数实际计算一下梯度。这里我们求点(3, 4)、(0,2)、(3, 0)处的梯度。
>>> numerical_gradient(function_2, np.array([3.0, 4.0]))
array([ 6., 8.])
>>> numerical_gradient( function_2, np.array([0.0, 2.0]))
array([ 0.,4.])
>>> numerical_gradient(function_2, np.array([3.0, θ.0]))
array([ 6.,0.])
如图4-9所示, f ( x 0 + x 1 ) = x 0 2 + x 1 2 f(x_0 +x_1)= x_0^2+ x_1^2 f(x0+x1)=x02+x12 的梯度呈现为有向向量(箭头)。观察图4-9,我们发现梯度指向函数 f ( x 0 , x 1 ) f(x_0,x_1) f(x0,x1) 的“最低处”(最小值),就像指南针一样,所有的箭头都指向同一点。其次,我们发现离“最低处”越远,箭头越大。
虽然图4-9中的梯度指向了最低处,但并非任何时候都这样。实际上,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向。这是一个非常重要的性质,请一定牢记!
4.1 梯度法
机器学习的主要任务是 在学习时寻找最优参数。同样地,神经网络也必须在学习时找到最优参数(权重和偏置)。这里所说的最优参数是指 损失函数取最小值时的参数。但是,一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是 梯度法。
这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。
虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的任务中,要以梯度的信息为线索,决定前进的方向。
此时梯度法就派上用场了。在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离, 然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。像这样,通过不断地沿梯度方向前进,逐渐减小函数值的过程就是 梯度法(gradientmethod)。梯度法是解决机器学习中最优化问题的常用方法,特别是在神经网络的学习中经常被使用。
根据目的是寻找最小值还是最大值,梯度法的叫法有所不同。严格地讲,寻找最小值的梯度法称为梯度下降法(gradientdescentmethod),寻找最大值的梯度法称为梯度上升法(gradient ascent method)。但是通过反转损失函数的符号,求最小值的问题和求最大值的问题会变成相同的问题,因此“下降”还是“上升"的差异本质上并不重要。一般来说,神经网络(深度学习)中,梯度法主要是指梯度下降法。
现在,我们尝试用数学式来表示梯度法,如式(4.7)所示。
x
0
=
x
0
−
η
∂
f
∂
x
0
x
1
=
x
1
−
η
∂
f
∂
x
1
(4.7)
x_0 = x_0 - η\frac{∂f}{∂x_0}\\\quad\\x_1 = x_1-η\frac{∂f}{∂x_1}\tag{4.7}
x0=x0−η∂x0∂fx1=x1−η∂x1∂f(4.7)
式(4.7)的 η 表示更新量,在神经网络的学习中,称为 学习率(learningrate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。
式(4.7)是表示更新一次的式子,这个步骤会反复执行。也就是说,每一步都按式(4.7)更新变量的值,通过反复执行此步骤,逐渐减小函数值。虽然这里只展示了有两个变量时的更新过程,但是即便增加变量的数量,也可以通过类似的式子(各个变量的偏导数)进行更新。
学习率需要事先确定为某个值,比如 0.01 或 0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了
下面,我们用Python来实现梯度下降法。
def gradient_ descent(f, init_x,lr=0.01, step_num=100):
x = init_x
for i in range(step_ num):
grad = numerical_gradient(f, x)
x -=lr* grad
return x
参数f是要进行最优化的函数,init_ x 是初始值,Ir 是学习率learning rate,step_ num是梯度法的重复次数。numerical_ gradient(f, x) 会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num 指定重复的次数。
使用这个函数可以求函数的极小值,顺利的话,还可以求函数的最小值。下面,我们就来尝试解决下面这个问题。
问题: 请用梯度法求 f ( x 0 + x 1 ) = x 0 2 + x 1 2 f(x_0 +x_1)=x_0^2 + x_1^2 f(x0+x1)=x02+x12 的最小值。
>>> def function_2(x):
return x[0]**2 + x[1]**2
>>> init_x = np.array([-3.0, 4.0])
>>> gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
array([ -6.11110793e-10, 8.14814391e-10])
这里,设初始值为 (-3.0,4.0), 开始使用梯度法寻找最小值。最终的结果是(-6.1e-10,8.1e-10), 非常接近(0, 0)。实际上,真的最小值就是(0, 0),所以说通过梯度法我们基本得到了正确结果。如果用图来表示梯度法的更新过程,则如图4-10所示。可以发现,原点处是最低的地方,函数的取值一点点在向其靠近。
像学习率这样的参数称为超参数。这是一种和神经网络的参数(权重和偏置)性质不同的参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是 人工设定的。一般来说,超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。
4.2 神经网络的梯度
神经网络的学习也要求梯度。这里所说的梯度是指损失函数关于权重参数的梯度。比如,有一个只有一个形状为2 x 3的权重 W 的神经网络,损失函数用 L 表示。此时,梯度可以用
∂
L
∂
W
\frac{∂L}{∂W}
∂W∂L 表示。用数学式表示的话,如下所示。
下面,我们以一个简单的神经网络为例,来实现求梯度的代码。为此,我们要实现一个名为simpleNet的类(保存在gradient_ simplenet.py文件中)。
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录中的文件而进行的设定
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient
class simpleNet:
def __init__(self):
self.W = np.random.randn(2,3) # 随机生成权重
def predict(self, x):
return np.dot(x, self.W)
def loss(self, x, t):
z = self.predict(x)
y = softmax(z) # 使用softmax算法得到输出值
loss = cross_entropy_error(y, t) # 使用交叉熵误差计算损失函数
return loss
这里导入了之前写的代码,可以去前面的笔记看详细源码。
simpleNet 类只有一个实例变量,即形状为2x3的权重参数。它有两个方法,一个是用于预测的predict(x),另一个是用于求损失函数值的loss(x,t)。这里参数 x 接收输人数据,t 接收正确解标签。现在我们来试着用一下这个simpleNet。
>>> net = simpLeNet()
>>> print(net.W) #权重参数
[[ 0.47355232, 0.9977393, 0.84668094],
[ 0.85557411, 0.03563661, 0.69422093]]
>>>
>>> x = np.array([0.6, 0.9])
>>> p = net.predict(x)
>>> print(p)
[ 1.05414809 0.63071653 1.1328074]
>>> np.argmax(p) #最大值的索引
2
>>> t = np.array([0, 0, 1]) #正确解标签
>>> net.loss(x, t)
0.92806853663411326
接下来求梯度。和前面一样,我们使用 numerical_ gradient(f, x) 求梯度(这里定义的函数f(W)的参数W是一个伪参数。因为numerical_ gradient(f, x)会在内部执行f(x),为了与之兼容而定义了f(W)。
>>> def f(W):
return net.loss(x, t)
>>> dW = numerical_gradient(f, net.W)
>>> print(dW)
[[ 0.21924763 0.14356247 -0.36281009]
[ 0.32887144 0.2153437 -0.54421514]]
numerical_ gradient(f, x) 的参数 f 是函数, x 是传给函数 f 的参数。因此,这里参数 x 取net.W,并定义一个计算损失函数的新函数 f,然后把这个新定义的函数传递给numerical gradient(f, x)。
numerical_ gradient(f, net.W) 的结果是dW,一个形状为2 x 3的二维数组。观察一下 dW 的内容,例如,会发现 ∂ L ∂ W \frac{∂L}{∂W} ∂W∂L中的 ∂ L ∂ w 11 \frac{∂L}{∂w_{11}} ∂w11∂L 的值大约是0.2,这表示如果将 w 11 w_{11} w11 增加h,那么损失函数的值会增加0.2h。再如, ∂ L ∂ w 23 \frac{∂L}{∂w_{23}} ∂w23∂L 对应的值大约是 -0.5,这表示如果将 w 23 w_{23} w23 增加 h,损失函数的值将减小 0.5h。因此,从减小损失函数值的观点来看, w 23 w_{23} w23应向正方向更新, w 11 w_{11} w11 应向负方向更新。至于更新的程度, w 23 w_{23} w23 比 w 11 w_{11} w11 的贡献要大。
5. 学习算法的实现
关于神经网络学习的基础知识,到这里就全部介绍完了。“损失函数”“mini-batch"“梯度”“梯度下降法” 等关键词已经陆续登场,这里我们来确认一下神经网络的学习步骤,顺便复习一下这些内容。神经网络的学习步骤如下所示。
前提
- 神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤。
步骤1 (mini-batch )
- 从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。
步骤2(计算梯度)
- 为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
步骤3(更新参数)
- 将权重参数沿梯度方向进行微小更新。
步骤4(重复)
- 重复步骤1、步骤2、步骤3。
神经网络的学习按照上面4个步骤进行。这个方法通过梯度下降法更新参数,不过因为这里使用的数据是随机选择的mini batch数据,所以又称为 随机梯度下降法(stochastic gradient descent)。“随机”指的是“随机选择的”的意思,因此,”随机梯度下降法是“对随机选择的数据进行的梯度下降法”。深度学习的很多框架中,随机梯度下降法一般由一个名为 SGD 的函数来实现。SGD来源于随机梯度下降法的英文名称的首字母。
下面,我们来实现手写数字识别的神经网络。这里以2层神经网络(隐藏层为1层的网络)为对象,使用MNIST数据集进行学习。
5.1 2层神经网络的类
首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类,实现过程如下所示。保存在two_layer_net.py文件中。
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
from common.functions import *
from common.gradient import numerical_gradient
class TwoLayerNet:
def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
# 初始化权重
self.params = {}
self.params['W1'] = weight_init_std * np.random.randn(input_size, hidden_size)
self.params['b1'] = np.zeros(hidden_size)
self.params['W2'] = weight_init_std * np.random.randn(hidden_size, output_size)
self.params['b2'] = np.zeros(output_size)
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# x:输入数据, t:监督数据
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=1)
t = np.argmax(t, axis=1)
accuracy = np.sum(y == t) / float(x.shape[0])
return accuracy
# x:输入数据, t:监督数据
def numerical_gradient(self, x, t):
loss_W = lambda W: self.loss(x, t)
grads = {}
grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
grads['b2'] = numerical_gradient(loss_W, self.params['b2'])
return grads
def gradient(self, x, t):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
grads = {}
batch_num = x.shape[0]
# forward
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
# backward
dy = (y - t) / batch_num
grads['W2'] = np.dot(z1.T, dy)
grads['b2'] = np.sum(dy, axis=0)
da1 = np.dot(dy, W2.T)
dz1 = sigmoid_grad(a1) * da1
grads['W1'] = np.dot(x.T, dz1)
grads['b1'] = np.sum(dz1, axis=0)
return grads
接着,我们来看一下 TwoLayerNet 的方法的实现。首先是__ init__ (self,input_ size,hidden_ size,output_ size) 方法,它是类的初始化方法(所谓初始化方法,就是生成TwoLayerNet实例时被调用的方法)。从第1个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。另外,因为进行手写数字识别时,输入图像的大小是784(28 x 28),输出为10个类别,所以指定参数input_ size=784、 output_ size=10, 将隐藏层的个数hidden_ size设置为一个合适的值即可。
此外,这个初始化方法会对权重参数进行初始化。如何设置权重参数的初始值这个问题是关系到神经网络能否成功学习的重要问题。后面我们会详细讨论权重参数的初始化,这里只需要知道,权重使用符合 高斯分布 的随机数进行初始化,偏置使用 0 进行初始化。predict(self,x) 和 accuracy(self, x, t) 的实现和上一章的神经网络的推理处理基本一样。另外, loss(self, x, t)是计算损失函数值的方法。这个方法会基于predict()的结果和正确解标签,计算 交叉熵误差。
剩下的 numerical. gradient(self, x, t) 方法会计算各个 参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。另外,gradient(self, x, t) 是下一章要实现的方法,该方法使用 误差反向传播法 高效地计算梯度。
numerical_gradient(self, x,t) 基于数值微分计算参数的梯度。下一章,我们会介绍一个高速计算梯度的方法,称为 误差反向传播法。用误差反向传播法求到的梯度和数值微分的结果基本一致, 但可以高速地进行处理。使用误差反向传播法计算梯度的 gradient(self,x,t) 方法会在下一章实现,不过考虑到神经网络的学习比较花时间,想节约学习时间的读者可以替换掉这里的numerical gradient(self, x,t),抢先使用gradient(self, x,t)!
5.2 mini_batch的实现
神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中 随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。下面,我们就以 TwoLayerNet 类为对象,使用MNIST数据集进行学习(保存在train_neuralnet.py文件中)。
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 为了导入父目录的文件而进行的设定
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
# 读入数据
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
iters_num = 10000 # 适当设定循环的次数
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
train_loss_list = []
train_acc_list = []
test_acc_list = []
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
# 计算梯度
#grad = network.numerical_gradient(x_batch, t_batch)
grad = network.gradient(x_batch, t_batch)
# 更新参数
for key in ('W1', 'b1', 'W2', 'b2'):
network.params[key] -= learning_rate * grad[key]
loss = network.loss(x_batch, t_batch)
train_loss_list.append(loss)
if i % iter_per_epoch == 0:
train_acc = network.accuracy(x_train, t_train)
test_acc = network.accuracy(x_test, t_test)
train_acc_list.append(train_acc)
test_acc_list.append(test_acc)
print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))
# 绘制图形
markers = {'train': 'o', 'test': 's'}
x = np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()
这里,mini-batch的大小为100,需要每次从60000个训练数据中随机取出100个数据(图像数据和正确解标签数据)。然后,对这个包含100笔数据的mini-batch求梯度,使用 随机梯度下降法(SGD) 更新参数。这里,梯度法的更新次数(循环的次数)为10000。每更新一次,都对训练数据计算损失函数的值,并把该值添加到数组中。用图像来表示这个损失函数的值的推移,如图4-11所示。
观察图4-11,可以发现随着学习的进行,损失函数的值在不断减小。这是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。也就是说,神经网络的确在学习!通过反复地向它浇灌(输人)数据,神经网络正在逐渐 向最优参数靠近。
5.3 基于测试数据的评价
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认 是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。
神经网络学习的最初目标是掌握 泛化能力,因此,要评价神经网络的泛化能力,就必须使用不包含在训练数据中的数据。下面的代码在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下训练数据和测试数据的识别精度。
epoch是一个单位。一个epoch表示学习中所有训练数据均被使用过一次时的更新次数。比如,对于10000笔训练数据,用大小为100笔数据的mini- batch进行学习时,重复随机梯度下降法100次,所有的训练数据就都被“看过”了”。此时,100次就是一个epoch。
在上面的代码中,每经过一个epoch,就对所有的训练数据和测试数据计算识别精度,并记录结果。之所以要计算每一个 epoch的识别精度,是因为如果在for语句的循环中一直计算识别精度,会花费太多时间。并且,也没有必要那么频繁地记录识别精度(只要从大方向上大致把握识别精度的推移就可以了)。因此,我们才会每经过一个epoch就记录一次训练数据的识别精度。
把从上面的代码中得到的结果用图表示的话,如图4-12所示。
图4-12中,实线表示训练数据的识别精度,虚线表示测试数据的识别精度。如图所示,随着epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
6. 小结
本章中,我们介绍了神经网络的学习。首先,为了能顺利进行神经网络的学习,我们导人了损失函数这个指标。以这个损失函数为基准,找出使它的值达到最小的权重参数,就是神经网络学习的目标。为了找到尽可能小的损失函数值,我们介绍了使用函数斜率的梯度法。
本章所学的内容
- 机器学习中使用的数据集分为训练数据和测试数据。
- 神经网络用训练数据进行学习,并用测试数据评价学习到的模型的泛化能力。
- 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。
- 利用某个给定的微小值的差分求导数的过程,称为数值微分。
- 利用数值微分,可以计算权重参数的梯度。