当前位置:   article > 正文

深度神经网络DNN(五)——损失函数_dnn loss

dnn loss

损失函数的概念

神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。
损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。

均方误差

可以用作损失函数的函数有很多,其中最有名的是均方误差(mean squared error)。均方误差如下式所示:
E = 1 2 ∑ k ( y k − t k ) 2 E=\frac{1}{2} \sum_{k}^{} (y_{k}-t_{k})^{2} E=21k(yktk)2
这里,yk是表示神经网络的输出,tk表示监督数据,k表示数据的维数。比如,在上一篇文章手写数字识别的例子中,yk、tk是由如下10 个元素构成的数据。

>>> y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
>>> t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
  • 1
  • 2

数组元素的索引从第一个开始依次对应数字“0”“1”“2”…… 这里,神经网络的输出y是softmax函数的输出。由于softmax函数的输出可以理解为概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05,“2”的概率是0.6等。t是监督数据,将正确解标签设为1,其他均设为0。这里,标签“2”为1,表示正确解是“2”。将正确解标签表示为1,其他标签表示为0 的表示方法称为one-hot表示
如上式所示,均方误差会计算神经网络的输出和正确解监督数据的各个元素之差的平方,再求总和。现在,我们用Python来实现这个均方误差,实现方式如下所示:

def mean_squared_error(y, t):
	return 0.5 * np.sum((y-t)**2)
  • 1
  • 2

这里,参数y和t是NumPy数组。现在,我们使用这个函数,来实际地计算一下:

# 设“2”为正确解
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 例1:“2”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(mean_squared_error(np.array(y), np.array(t)))

# 例2:“7”的概率最高的情况(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(mean_squared_error(np.array(y), np.array(t)))

# 运行结果
0.097500000000000031
0.59750000000000003
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

这里举了两个例子。第一个例子中,正确解是“2”,神经网络的输出的最大值是“2”;第二个例子中,正确解是“2”,神经网络的输出的最大值是“7”。如实验结果所示,我们发现第一个例子的损失函数的值更小,和监督数据之间的误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。

交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示:
E = − ∑ k t k l n ( y k ) E=-\sum_{k}^{} t_{k}ln(y_{k}) E=ktkln(yk)
yk是神经网络的输出,tk是正确解标签。并且,tk中只有正确解标签的索引为1,其他均为0(one-hot 表示)。因此,上式实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是0.6,则交叉熵误差
是−log 0.6 = 0.51;若“2”对应的输出是0.1,则交叉熵误差为−log 0.1 = 2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
我们用Python来生成自然对数的图像:

import matplotlib.pyplot as plt
import numpy as np
x=np.arange(0.01,1,0.01)
y=np.log(x)
plt.plot(x,y,label="y=lnx")
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

运行结果如下图:
在这里插入图片描述
如上图所示,x等于1时,y为0;随着x向0靠近,y逐渐变小。因此,正确解标签对应的输出越大,式子的值越接近0;当输出为1时,交叉熵误差为0。此外,如果正确解标签对应的输出较小,则式子的值较大。
下面,我们来用代码实现交叉熵误差:

def cross_entropy_error(y, t):
	delta = 1e-7
	return -np.sum(t * np.log(y + delta))
  • 1
  • 2
  • 3

这里,参数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, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print(cross_entropy_error(np.array(y), np.array(t)))

y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print(cross_entropy_error(np.array(y), np.array(t)))

# 运行结果
0.51082545709933802
2.3025840929945458
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

第一个例子中,正确解标签对应的输出为0.6,此时的交叉熵误差大约为0.51。第二个例子中,正确解标签对应的输出为0.1 的低值,此时的交叉熵误差大约为2.3。由此可以看出,这些结果与我们前面讨论的内容是一致的。

mini-batch学习

机器学习使用训练数据进行学习。使用训练数据进行学习,严格来说,就是针对训练数据计算损失函数的值,找出使该值尽可能小的参数。因此,计算损失函数时必须将所有的训练数据作为对象。也就是说,如果训练数据有100 个的话,我们就要把这100 个损失函数的总和作为学习的指标。
前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式:
E = − 1 N ∑ n ∑ k t n k l n ( y n k ) E=-\frac{1}{N} \sum_{n}^{} \sum_{k}^{} t_{nk}ln(y_{nk}) E=N1nktnkln(ynk)
这里, 假设数据有N个,tnk表示第n个数据的第k个元素的值(ynk是神经网络的输出,tnk是监督数据)。式子把求单个数据的损失函数的式子扩大到了N份数据,最后除以N进行正规化。通过除以N,可以求单个数据的平均损失函数。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有1000个或10000个,也可以求得单个数据的平均损失函数。
另外,MNIST数据集的训练数据有60000 个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为mini-batch, 小批量),然后对每个mini-batch进行学习。比如,从60000 个训练数据中随机选择100笔,再用这100笔数据进行学习。这种学习方式称为mini-batch学习。
下面我们来编写从训练数据中随机选择指定个数的数据的代码,以进行mini-batch学习。上一节介绍过,load_mnist函数是用于读入MNIST数据集的函数,它会读入训练数据和测试数据。那么,如何从这个训练数据中随机抽取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]
  • 1
  • 2
  • 3
  • 4
  • 5

使用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])
  • 1
  • 2

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里,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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

实现的要点是,由于one-hot表示中t为0的元素的交叉熵误差也为0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。因此,t为one-hot 表示时通过t * np.log(y) 计算的地方,在t 为标签形式时,可用np.log( y[np.arange(batch_size), t] )实现相同的处理。
作为参考,简单介绍一下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]])。

损失函数的优点

上面我们讨论了损失函数,可能有人要问:“为什么要导入损失函数呢?”也就是说,既然我们的目标是获得使识别精度尽可能高的神经网络,那不是应该把识别精度作为指标吗?
对于这一疑问,我们可以根据“导数”在神经网络学习中的作用来回答。
下一篇文章中会详细说到,在神经网络的学习中,寻找最优参数(权重和偏置)时,要寻找使损失函数的值尽可能小的参数。为了找到使损失函数的值尽可能小的地方,需要计算参数的导数(确切地讲是梯度),然后以这个导数为指引,逐步更新参数的值。
假设有一个神经网络,现在我们来关注这个神经网络中的某一个权重参数。此时,对该权重参数的损失函数求导,表示的是如果稍微改变这个权重参数的值,损失函数的值会如何变化。如果导数的值为负,通过使该权重参数向正方向改变,可以减小损失函数的值;反过来,如果导数的值为正,则通过使该权重参数向负方向改变,可以减小损失函数的值。不过,当导数的值为0时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。
之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数都会变为0,导致参数无法更新。为什么用识别精度作为指标时,参数的导数在绝大多数地方都会变成0呢?为了回答这个问题,我们来思考另一个具体例子。假设某个神经网络正确识别出了100笔训练数据中的32笔,此时识别精度为32%。如果以识别精度为指标,即使稍微改变权重参数的值,识别精度也仍将保持在32%,不会出现变化。也就是说,仅仅微调参数,是无法改善识别精度的。即便识别精度有所改善,它的值也不会像32.0123 . . .%这样连续变化,而是变为33%、34%这样的不连续的、离散的值。而如果把损失函数作为指标,则当前损失函数的值可以表示为0.92543 . . . 这样的值。并且,如果稍微改变一下参数的值,对应的损失函数也会像0.93432 . . . 这样发生连续性的变化
识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。阶跃函数的导数在绝大多数地方(除了0 以外的地方)均为0。也就是说,如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化
而sigmoid 函数,不仅输出(竖轴的值)是连续变化的,曲线的斜率(导数)也是连续变化的。也就是说,sigmoid 函数的导数在任何地方都不为0。这对神经网络的学习非常重要。得益于这个斜率不会为0 的性质,神经网络的学习得以正确进行。

小结

  • 神经网络的学习以损失函数为指标,更新权重参数,以使损失函数的值减小。
  • 常用的损失函数有均方误差与交叉熵误差等。
  • 损失函数应能处理批量数据
  • 损失函数相较于精确度的优势
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/392940
推荐阅读
相关标签
  

闽ICP备14008679号