一、从数据中学习
神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。
数据是机器学习的命根子。从数据中寻找答案、从数据中发现模式、根据数据讲故事……这些机器学习所做的事情,如果没有数据的话,就无从谈起。因此,数据是机器学习的核心。这种数据驱动的方法,也可以说是脱离了以人为中心的方法。
与以往解决方法的思路不同,机器学习的方法极力避免人为介入,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入。
举一个例子:识别手写数字的问题。一种方案是先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。也就是说,即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。而神经网络直接学习图像本身。在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。其关系如下图所示:
深 度 学 习 有 时 也 称 为 端 到 端 机 器 学 习(end-to-end machinelearning)。这里所说的端到端是指从一端到另一端的意思,也就是从原始数据(输入)中获得目标结果(输出)的意思。神经网络的优点是对所有的问题都可以用同样的流程来解决。
机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。将数据分为训练数据与测试数据的原因是要追求泛化能力。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。因此,仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。只对某个数据集过度拟合的状态称为过拟合(over fitting)。
二、损失函数
神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。。神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。
1.均方误差
可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squared error)。均方误差如下式所示。
这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。需要注意的是,将正确解标签表示为1,其他标签表示为0的表示方法称为one-hot表示。
均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。其代码实现如下:
def mean_squared_error(y,t): #求解均方误差
return 0.5*np.sum((y-t)**2)
2.交叉熵误差
除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示。
这里,log表示以e为底数的自然对数,yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot表示)。因此,其实际上只计算对应正确解标签的输出的自然对数。由此可知,交叉熵误差的值是由正确解标签所对应的输出结果决定的。正确解标签对应的输出越大,式子的值越接近0;当输出为1时,交叉熵误差为0。
其代码实现如下:
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,这样一来就会导致后续计算无法进行。作为保护性对策,添加一个微小值可以防止负无限大的发生。
3.mini-batch学习
机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子。
这里,假设数据有N个,tnk表示第n个数据的第k个元素的值(ynk是神经网络的输出,tnk是监督数据)。最后还要除以N进行正规化。通过除以N,可以求单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。
如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch,小批量),然后对每个mini-batch进行学习。比如,从60000个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。
那么,如何从这个训练数据中随机抽取10笔数据呢?我们可以使用NumPy的np.random.choice(),写成如下形式。
使用np.random.choice()可以从指定的数字中随机选择想要的数字。比如,np.random.choice(60000, 10)会从0到59999之间随机选择10个数字。之后,我们只需指定这些随机选出的索引,取出mini-batch,然后使用这个mini-batch计算损失函数即可。
这里,我们来实现一个可以同时处理单个数据和批量数据(数据作为batch集中输入)两种情况的函数。代码如下:
def cross_entropy_error(y,t): #实现mini-batch交叉熵误差(one-hot形式)
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 corss_entropy_error(y,t): #数据以标签形式的mini-batch交叉熵误差
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 #通过arange取出特定的数据
4.为什么要设定损失函数
既然我们的目标是获得使识别精度尽可能高的神经网络,那不是应该把识别精度作为指标吗?在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方,需要计算参数的导数(确切地讲是梯度),然后以这个导数为指引,逐步更新参数的值。对该权重参数的损失函数求导,表示的是“如果稍微改变这个权重参数的值,损失函数的值会如何变化”。如果导数的值为负,通过使该权重参数向正方向改变,可以减小损失函数的值,反之亦同。当导数的值为0时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。
之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数都会变为0,导致参数无法更新。识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。
但是对于sigmoid函数。不仅函数的输出(竖轴的值)是连续变化的,曲线的斜率(导数)也是连续变化的。也就是说,sigmoid函数的导数在任何地方都不为0。这对神经网络的学习非常重要。得益于这个斜率不会为0的性质,神经网络的学习得以正确进行。
三、数值微分
1.导数
导数的定义如下:
导数的含义是,x的“微小变化”将导致函数f(x)的值在多大程度上发生变化。
在代码实现上。有两个优化方法。
一个是避免舍入误差(rounding error)。所谓舍入误差,是指因省略小数的精细部分的数值(比如,小数点第8位以后的数值)而造成最终的计算结果上的误差。所以微小值不能取得太小,一般使用10−4就可以得到正确的结果。
第二个是“真的导数”对应函数在x处的斜率(称为切线),但上述实现中计算的导数对应的是(x + h)和x之间的斜率,如下图所示。为了减小这个误差,我们可以计算函数f在(x + h)和(x − h)之间的差分。因为这种计算方法以x为中心,计算它左右两边的差分,所以也称为中心差分(而(x + h)和x之间的差分称为前向差分)。
则其代码实现如下:
def numerical_diff(f,x): #求数值微分
h=1e-4 #0.0001
return (f(x+h)-f(x-h))/ (2*h)
如上所示,利用微小的差分求导数的过程称为数值微分(numerical differentiation)。而基于数学式的推导求导数的过程,则用“解析性”(analytic)一词,称为“解析性求解”或者“解析性求导”。解析性求导得到的导数是不含误差的“真的导数”。
2.偏导数
对于有两个变量的函数,如下面的函数。我们有必要区分对哪个变量求导数,即对x0和x1两个变量中的哪一个求导数。另外,我们把这里讨论的有多个变量的函数的导数称为偏导数。
偏导数和单变量的导数一样,都是求某个地方的斜率。不过,偏导数需要将多个变量中的某一个变量定为目标变量,并将其他变量固定为某个值。
四、梯度
由全部变量的偏导数汇总而成的向量称为梯度(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的各个元素求数值微分。
将梯度呈现为有向向量(箭头)可得下图。
虽然图中的梯度指向了最低处,但并非任何时候都这样。实际上,梯度会指向各点处的函数值降低的方向。更严格地讲,梯度指示的方向是各点处的函数值减小最多的方向,这个性质在高等数学中易证。
1.梯度法
机器学习的主要任务是在学习时寻找最优参数。这里所说的最优参数是指损失函数取最小值时的参数。一般而言,损失函数很复杂,参数空间庞大,我们不知道它在何处能取得最小值。而通过巧妙地使用梯度来寻找函数最小值(或者尽可能小的值)的方法就是梯度法。
这里需要注意的是,梯度表示的是各点处的函数值减小最多的方向。因此,无法保证梯度所指的方向就是函数的最小值或者真正应该前进的方向。实际上,在复杂的函数中,梯度指示的方向基本上都不是函数值最小处。虽然梯度的方向并不一定指向最小值,但沿着它的方向能够最大限度地减小函数的值。因此,在寻找函数的最小值(或者尽可能小的值)的位置的任务中,要以梯度的信息为线索,决定前进的方向。
在梯度法中,函数的取值从当前位置沿着梯度方向前进一定距离,然后在新的地方重新求梯度,再沿着新梯度方向前进,如此反复,不断地沿梯度方向前进。通过不断地沿梯度方向前进,逐渐减小函数值的过程就是梯度法(gradient method)。
使用数学式表示梯度法如下图所示。
式子中的η表示更新量,在神经网络的学习中,称为学习率(learning rate)。学习率决定在一次学习中,应该学习多少,以及在多大程度上更新参数。学习率需要事先确定为某个值,比如0.01或0.001。一般而言,这个值过大或过小,都无法抵达一个“好的位置”。在神经网络的学习中,一般会一边改变学习率的值,一边确认学习是否正确进行了。
该式表示更新一次的式子,这个步骤会反复执行。每一步都按该式更新变量的值,通过反复执行此步骤,逐渐减小函数值。
其代码实现如下:
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是初始值,lr是学习率learning rate,step_num是梯度法的重复次数。numerical_gradient(f,x)会求函数的梯度,用该梯度乘以学习率得到的值进行更新操作,由step_num指定重复的次数。使用这个函数可以求函数的极小值,顺利的话,还可以求函数的最小值。
下面是初始值为(-3.0, 4.0),开始使用梯度法寻找最小值的图形。
实验结果表明,学习率过大的话,会发散成一个很大的值;反过来,学习率过小的话,基本上没怎么更新就结束了。设定合适的学习率是一个很重要的问题。
像学习率这样的参数称为超参数。相对于神经网络的权重参数是通过训练数据和学习算法自动获得的,学习率这样的超参数则是人工设定的。一般超参数需要尝试多个值,以便找到一种可以使学习顺利进行的设定。
2.神经网络的梯度法
一个只有一个形状为2 × 3的权重W的神经网络,损失函数用L表示。此时,梯度可以用下图表示。
这里的重点是,梯度的形状和W相同。
我们以一个简单的神经网络为例来实现求梯度的代码。
我们先实现一个名为simpleNet的类。代码如下:
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)
loss = cross_entropy_error(y, t)
return loss
simpleNet类只有一个实例变量,即形状为2×3的权重参数。它有两个方法,一个是用于预测的predict(x),另一个是用于求损失函数值的loss(x,t)。这里参数x接收输入数据,t接收正确解标签。
接下来求梯度。和前面一样,我们使用numerical_gradient(f, x)求梯度(这里定义的函数f(W)的参数W是一个伪参数。因为numerical_gradient(f, x)会在内部执行f(x),为了与之兼容而定义了f(W))。
f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)
求出神经网络的梯度后,接下来只需根据梯度法,更新权重参数即可。
五、学习算法的实现
神经网络的学习步骤如下所示。
前提 神经网络存在合适的权重和偏置,调整权重和偏置以便拟合训练数据的过程称为“学习”。神经网络的学习分成下面4个步骤。
步骤1(mini-batch)从训练数据中随机选出一部分数据,这部分数据称为mini-batch。我们的目标是减小mini-batch的损失函数的值。
步骤2(计算梯度)为了减小mini-batch的损失函数的值,需要求出各个权重参数的梯度。梯度表示损失函数的值减小最多的方向。
步骤3(更新参数)将权重参数沿梯度方向进行微小更新。
步骤4(重复)重复步骤1、步骤2、步骤3。
神经网络的学习按照上面4个步骤进行。因为这里使用的数据是随机选择的mini batch数据,所以又称为随机梯度下降法(stochastic gradient descent)。随机梯度下降法是“对随机选择的数据进行的梯度下降法”。
下面我们以2层神经网络为例子。
1.2层神经网络的类
首先,我们将这个2层神经网络实现为一个名为TwoLayerNet的类。其中所含的变量与方法如下图所示。详细代码在最后。
TwoLayerNet类有params和grads两个字典型实例变量。params变量中保存了权重参数。params变量中保存的权重参数会用在推理处理(前向处理)中。此外,与params变量对应,grads变量中保存了各个参数的梯度。使用numerical_gradient()方法计算梯度后,梯度的信息将保存在grads变量中。
接着是TwoLayerNet的方法的实现。首先是__init__(self, input_size, hidden_size, output_size)方法,它是类的初始化方法(所谓初始化方法,就是生成TwoLayerNet实例时被调用的方法)。从第1个参数开始,依次表示输入层的神经元数、隐藏层的神经元数、输出层的神经元数。
此外,这个初始化方法会对权重参数进行初始化。此处权重使用符合高斯分布的随机数进行初始化,偏置使用0进行初始化。另外,loss(self, x, t)是计算损失函数值的方法。这个方法会基于predict()的结果和正确解标签,计算交叉熵误差。
剩下的numerical_gradient(self, x, t)方法会计算各个参数的梯度。根据数值微分,计算各个参数相对于损失函数的梯度。
2.mini-batch的实现
神经网络的学习的实现使用的是前面介绍过的mini-batch学习。所谓mini-batch学习,就是从训练数据中随机选择一部分数据(称为mini-batch),再以这些mini-batch为对象,使用梯度法更新参数的过程。
这里,mini-batch的大小为100,需要每次从60000个训练数据中随机取出100个数据(图像数据和正确解标签数据)。下图是每次循环后损失函数的变化。
可以发现随着学习的进行,损失函数的值在不断减小。这是学习正常进行的信号,表示神经网络的权重参数在逐渐拟合数据。通过反复地向它浇灌(输入)数据,神经网络正在逐渐向最优参数靠近。
3.基于测试数据的评价
上面的环节我们确认了通过反复学习可以使损失函数的值逐渐减小这一事实。不过这个损失函数的值,严格地讲是“对训练数据的某个mini-batch的损失函数”的值。
神经网络的学习中,必须确认是否能够正确识别训练数据以外的其他数据,即确认是否会发生过拟合。过拟合是指,虽然训练数据中的数字图像能被正确辨别,但是不在训练数据中的数字图像却无法被识别的现象。
一般在进行学习的过程中,会定期地对训练数据和测试数据记录识别精度。这里,每经过一个epoch,我们都会记录下训练数据和测试数据的识别精度。epoch是一个单位。一个 epoch表示学习中所有训练数据均被使用过一次时的更新次数。
将得到的结果制成图表如下。
如图所示,随着epoch的前进(学习的进行),我们发现使用训练数据和测试数据评价的识别精度都提高了,并且,这两个识别精度基本上没有差异(两条线基本重叠在一起)。因此,可以说这次的学习中没有发生过拟合的现象。
详细代码:
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 = []
#平均每个epoch的重复次数 一个epoch表示学习中所有训练数据均被使用过一次时的更新次数
iter_per_epoch = max(train_size / batch_size, 1)
for i in range(iters_num):
#获取mini-batch
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)
#每一个epoch的识别精度
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()