深度学习:从数据中学习 and 损失函数

一、从数据中学习

神经网络的特征就是可以从数据中学习。所谓“从数据中学习”,是指可以由数据自动决定权重参数的值。这是非常了不起的事情!因为如果所有的参数都需要人工决定的话,工作量就太大了。在实际的神经网络中,参数的数量成千上万,在层数更深的深度学习中,参数的数量甚至可以上亿,想要人工决定这些参数的值是不可能的。本章将介绍神经网络的学习,即利用数据决定参数值的方法,并用Python实现对MNIST手写数字数据集的学习。

对于线性可分问题。根据“感知机收敛定理”,通过有限次数的学习,线性可分问题是可解的。但是,非线性可分问题则无法通过(自动)学习来解决。

1.1 数据驱动

数据是机器学习的命根子。从数据中寻找答案、从数据中发现模式、根据数据讲故事……这些机器学习所做的事情,如果没有数据的话,就无从谈起。这种数据驱动的方法,也可以说脱离了过往以人为中心的方法。机器学习的方法极力避免人为介入,尝试从收集到的数据中发现答案(模式)。神经网络或深度学习则比以往的机器学习方法更能避免人为介入。

现在我们来思考一个具体的问题,比如如何实现数字“5”的识别。数字5是下图所示的手写图像,我们的目标是实现能区别是否是5的程序。

在这里插入图片描述

如果让我们自己来设计一个能将5正确分类的程序,就会意外地发现这是一个很难的问题。人可以简单地识别出5,但却很难明确说出是基于何种规律而识别出了5。此外,每个人都有不同的写字习惯,要发现其中的规律是一件非常难的工作。

因此,与其绞尽脑汁,从零开始想出一个可以识别5的算法,不如考虑通过有效利用数据来解决这个问题。一种方案是,先从图像中提取特征量,再用机器学习技术学习这些特征量的模式。这里所说的“特征量”是指可以从输入数据(输入图像)中准确地提取本质数据(重要的数据)的转换器。图像的特征量通常表示为向量的形式。在计算机视觉领域,常用的特征量包括SIFT、SURF和HOG等。使用这些特征量将图像数据转换为向量,然后对转换后的向量使用机器学习中的SVM、KNN等分类器进行学习。

机器学习的方法中,由机器从收集到的数据中找出规律性。与从零开始想出算法相比,这种方法可以更高效地解决问题,也能减轻人的负担。但是需要注意的是,将图像转换为向量时使用的特征量仍是由人设计的。对于不同的问题,必须使用合适的特征量(必须设计专门的特征量),才能得到好的结果。比如,为了区分狗的脸部,人们需要考虑与用于识别5的特征量不同的其他特征量。也就是说,即使使用特征量和机器学习的方法,也需要针对不同的问题人工考虑合适的特征量。

到这里,我们介绍了两种针对机器学习任务的方法。将这两种方法用图来表示,如下图所示。图中还展示了神经网络(深度学习)的方法,可以看出该方法不存在人为介入。

在这里插入图片描述

神经网络直接学习图像本身。在第2个方法,即利用特征量和机器学习的方法中,特征量仍是由人工设计的,而在神经网络中,连图像中包含的重要特征量也都是由机器来学习的。

1.2 训练数据和测试数据

机器学习中,一般将数据分为训练数据和测试数据两部分来进行学习和实验等。首先,使用训练数据进行学习,寻找最优的参数;然后,使用测试数据评价训练得到的模型的实际能力。为什么需要将数据分为训练数据和测试数据呢?因为我们追求的是模型的泛化能力。为了正确评价模型的泛化能力,就必须划分训练数据和测试数据。另外,训练数据也可以称为监督数据。

泛化能力是指处理未被观察过的数据(不包含在训练数据中的数据)的能力。获得泛化能力是机器学习的最终目标。比如,在识别手写数字的问题中,泛化能力可能会被用在自动读取明信片的邮政编码的系统上。此时,手写数字识别就必须具备较高的识别“某个人”写的字的能力。注意这里不是“特定的某个人写的特定的文字”,而是“任意一个人写的任意文字”。如果系统只能正确识别已有的训练数据,那有可能是只学习到了训练数据中的个人的习惯写法。

因此,仅仅用一个数据集去学习和评价参数,是无法进行正确评价的。这样会导致可以顺利地处理某个数据集,但无法处理其他数据集的情况。顺便说一下,只对某个数据集过度拟合的状态称为过拟合(overfitting)。避免过拟合也是机器学习的一个重要课题。

二、损失函数

如果有人问你现在有多幸福,你会如何回答呢?一般的人可能会给出诸如“还可以吧”或者“不是那么幸福”等笼统的回答。如果有人回答“我现在的幸福指数是10.23”的话,可能会把人吓一跳吧。因为他用一个数值指标来评判自己的幸福程度。

这里的幸福指数只是打个比方,实际上神经网络的学习也在做同样的事情。神经网络的学习通过某个指标表示现在的状态。然后,以这个指标为基准,寻找最优权重参数。和刚刚那位以幸福指数为指引寻找“最优人生”的人一样,神经网络以某个指标为线索寻找最优权重参数。神经网络的学习中所用的指标称为损失函数(loss function)。这个损失函数可以使用任意函数,但一般用均方误差和交叉熵误差等。

损失函数是表示神经网络性能的“恶劣程度”的指标,即当前的神经网络对监督数据在多大程度上不拟合,在多大程度上不一致。以“性能的恶劣程度”为指标可能会使人感到不太自然,但是如果给损失函数乘上一个负值,就可以解释为“在多大程度上不坏”,即“性能有多好”。并且,“使性能的恶劣程度达到最小”和“使性能的优良程度达到最大”是等价的,不管是用“恶劣程度”还是“优良程度”,做的事情本质上都是一样的。

2.1 均方误差

可以用作损失函数的函数有很多,其中最有名的是均方误差MSE(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
这里, y k y_k yk 是表示神经网络的输出, t k t_k tk 表示监督数据, k k k 表示数据的维数。

比如, y k y_k yk t k t_k 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]

数组元素的索引从第一个开始依次对应数字“0”“1”“2”……这里,神经网络的输出 y y y 是 softmax 函数的输出。由于 softmax 函数的输出可以理解为概率,因此上例表示“0”的概率是0.1,“1”的概率是0.05,“2”的概率是0.6等。 t t 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)

这里,参数 y y y t t 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]
mean_squared_error(np.array(y), np.array(t))
0.09750000000000003

# 例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]
mean_squared_error(np.array(y), np.array(t))
0.59750000000000003

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

2.2 交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示:
E = − ∑ k t k log ⁡ y k E = - \sum_{k} t_{k} \log y_{k} E=ktklogyk
这里, log ⁡ \log log 表示以 e e e 为底数的自然对数( log ⁡ e \log_e loge)。 y k y_k yk 是神经网络的输出, t k t_k tk 是正确解标签。并且, t k t_k tk 中只有正确解标签的索引为 1,其他均为 0(one-hot 表示)。因此,式(4.2)实际上只计算对应正确解标签的输出的自然对数。比如,假设正确解标签的索引是“2”,与之对应的神经网络的输出是 0.6,则交叉熵误差是 − log ⁡ 0.6 = 0.51 -\log 0.6 = 0.51 log0.6=0.51; 若“2”对应的输出是 0.1,则交叉熵误差为 − log ⁡ 0.1 = 2.30 -\log 0.1 = 2.30 log0.1=2.30。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。

自然对数的图像如下图所示。

在这里插入图片描述

x x x 等于 1 时, y y y 为 0;随着 x x x 向 0 靠近, y y 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 y y t t 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]
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.0, 0.6, 0.0, 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 个损失函数的总和作为学习的指标。

前面介绍的损失函数的例子中考虑的都是针对单个数据的损失函数。如果要求所有训练数据的损失函数的总和,以交叉熵误差为例,可以写成下面的式子。
E = − 1 N ∑ n ∑ k t n k log ⁡ y n k E = - \frac{1}{N} \sum_{n} \sum_{k} t_{nk} \log y_{nk} E=N1nktnklogynk
这里, 假设数据有 N N N 个, t n k t_{nk} tnk 表示第 n n n 个数据的第 k k k 个元素的值( y n k y_{nk} ynk 是神经网络的输出, t n k t_{nk} tnk 是监督数据)。式子虽然看起来有一些复杂,其实只是把求单个数据的损失函数的式子(即上个式子)扩大到了 N N N 份数据,不过最后还要除以 N N N 进行正规化。通过除以 N N N,可以求单个数据的“平均损失函数”。通过这样的平均化,可以获得和训练数据的数量无关的统一指标。比如,即便训练数据有 1000 个或 10000 个,也可以求得单个数据的平均损失函数。

另外,MNIST 数据集的训练数据有 60000 个,如果以全部数据为对象求损失函数的和,则计算过程需要花费较长的时间。再者,如果遇到大数据,数据量会有几百万、几千万之多,这种情况下以全部数据为对象计算损失函数是不现实的。因此,我们从全部数据中选出一部分,作为全部数据的“近似”。神经网络的学习也是从训练数据中选出一批数据(称为 mini-batch,小批量),然后对每个 mini-batch 进行学习。比如,从 60000 个训练数据中随机选择 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 数据集的函数。dataset/mnist.py 会读入训练数据和测试数据。读入数据时,通过设定参数 one_hot_label=True,可以得到 one-hot 表示(即仅正确解标签为 1,其余为 0 的数据结构)。

读入上面的 MNIST 数据后,训练数据有 60000 个,输入数据是 784 维(28×28)的图像数据,监督数据是 10 维的数据。因此,上面的 x_traint_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 计算损失函数即可。

计算电视收视率时,并不会统计所有家庭的电视机,而是仅以那些被选中的家庭为统计对象。比如,通过从关东地区随机选择 1000 个家庭计算收视率,可以近似地求得关东地区整体的收视率。这 1000 个家庭的收视率,虽然严格上不等于整体的收视率,但可以作为整体的一个近似值。和收视率一样,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

实现的要点是,由于 one-hot 表示中 $ t $ 为 0 的元素的交叉熵误差也为 0,因此针对这些元素的计算可以忽略。换言之,如果可以获得神经网络在正确解标签处的输出,就可以计算交叉熵误差。

作为参考,简单介绍一下 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 时,无论权重参数向哪个方向变化,损失函数的值都不会改变,此时该权重参数的更新会停在此处。

之所以不能用识别精度作为指标,是因为这样一来绝大多数地方的导数都会变为 0,导致参数无法更新。话说得有点多了,我们来总结一下上面的内容。

在进行神经网络的学习时,不能将识别精度作为指标。因为如果以识别精度为指标,则参数的导数在绝大多数地方都会变为 0。

为什么用识别精度作为指标时,参数的导数在绝大多数地方都会变成 0 呢?为了回答这个问题,我们来思考另一个具体例子。假设某个神经网络正确识别出了 100 笔训练数据中的 32 笔,此时识别精度为 32%。如果以识别精度为指标,即使稍微改变权重参数的值,识别精度也仍将保持在 32%,不会出现变化。也就是说,仅仅微调参数,是无法改善识别精度的。即便识别精度有所改善,它的值也不会像 32.0123…% 这样连续变化,而是变为 33%、34% 这样的不连续的、离散的值。而如果把损失函数作为指标,则当前损失函数的值可以表示为 0.92543… 这样的值。并且,如果稍微改变一下参数的值,对应的损失函数也会像 0.93432… 这样发生连续性的变化。

识别精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续地、突然地变化。作为激活函数的阶跃函数也有同样的情况。出于相同的原因,如果使用阶跃函数作为激活函数,神经网络的学习将无法进行。

如图所示,阶跃函数的导数在绝大多数地方(除了 0 以外的地方)均为 0。也就是说,如果使用了阶跃函数,那么即便将损失函数作为指标,参数的微小变化也会被阶跃函数抹杀,导致损失函数的值不会产生任何变化。

阶跃函数就像“竹筒敲石”一样,只在某个瞬间产生变化。而 sigmoid 函数,如图所示,不仅函数的输出(竖轴的值)是连续变化的,曲线的斜率(导数)也是连续变化的。也就是说,sigmoid 函数的导数在任何地方都不为 0。这对神经网络的学习非常重要。得益于这个斜率不会为 0 的性质,神经网络的学习得以正确进行。

在这里插入图片描述


推荐我的相关专栏:


在这里插入图片描述

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter-Lu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值