《深度学习入门:基于Python的理论与实现》第四章神经网络的学习—上篇(损失函数登场)

发布于:2025-04-22 ⋅ 阅读:(29) ⋅ 点赞:(0)

本章的主题是神经网络的学习。这里所说的“学习”是指从训练数据中自动获取最优权重参数的过程。本章中,为了使神经网络能进行学习,将导入损失函数这一指标。而学习的目的就是以该损失函数为基准,找出能使它的值达到最小的权重参数。为了找出尽可能小的损失函数的值,本章我们将介绍利用了函数斜率的梯度法。

4.1 从数据中学习

神经网络的一个重要特点就是它能够从数据中学习。所谓“从数据中学习”,是指神经网络能够通过数据自动调整它的权重和参数,而不需要我们手动去设置。

在实际应用中,手动设定参数非常困难,尤其是当数据量巨大,参数众多时。神经网络通过训练,不断调整参数,从而能够学习到数据中的规律。

举个例子,在第2章中提到的感知机,它可以通过有限次数的学习解决线性可分问题。但如果问题是非线性可分的,感知机就无法解决。而神经网络通过多层网络结构和非线性激活函数,可以处理更复杂的非线性问题。

**比如在手写数字识别的应用中,神经网络可以自动从图像中学习到如何区分数字,而不需要我们手动提取图像的特征。**这与传统的机器学习方法不同,后者需要我们自己设计特征(比如SIFT、SURF、HOG等),将图像转化为向量后,再用分类器进行学习。而神经网络的优势在于,它可以自动从数据中学习到重要的特征,避免了人工干预。

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

4.1.1 数据驱动

数据是机器学习的核心。如果没有数据,机器学习就无法进行。机器学习的本质就是从数据中寻找规律、发现模式。这种“数据驱动”的方法,改变了传统的人工设计方法。

在传统方法中,我们通常会根据经验和直觉来思考:“这个问题看起来可能有规律性?”“不对,应该是其他原因。”我们依赖人的经验和直觉,通过反复试验去推进问题的解决。而机器学习的方法则力图减少人为干预,尽可能让机器从收集到的数据中自己发现规律。

**例如,我们要实现数字“5”的识别。**这个问题看似简单,但让我们自己设计一个算法来识别“5”,你会发现其实很困难。每个人写的“5”都不一样,写法千差万别,而人虽然能很快识别出来,却难以说清楚到底是根据什么规律识别的。
在这里插入图片描述

因此,与其从头开始设计一个识别“5”的算法,不如通过有效利用数据来解决问题。一种常见的做法是,从图像中提取特征,然后用机器学习方法学习这些特征的模式。这些“特征”是从图像中提取出的一些重要信息(例如形状、边缘等),通常以向量的形式表示。常见的图像特征包括SIFT、SURF、HOG等,我们可以用这些特征来进行分类。

**但是,这种方法依然需要人为设计特征量。**不同的任务需要不同的特征,才能取得好的效果。例如,识别狗脸的特征就不同于识别数字“5”的特征。因此,虽然使用机器学习的方法可以提高效率,但仍然需要根据问题设计合适的特征。

而神经网络(深度学习)则不同,它能直接从数据本身学习到特征。在这种方法中,不再需要人为设计特征量,神经网络自动从数据中提取所有重要信息。

图4-2展示了两种方法:一是传统的机器学习方法,需要人工设计特征;二是神经网络方法,完全由机器自动学习特征。神经网络的这种“端到端学习”方式,是深度学习的一大优势。
在这里插入图片描述

深度学习(神经网络)也被称为“端到端机器学习”(end-to-end machine learning)

这里的“端到端”是指,从原始数据到最终结果,整个过程是自动学习的,完全不依赖人为干预。

举个例子,不管是识别数字“5”、狗脸还是人脸,神经网络都能通过不断地从数据中学习,找到解决问题的规律。也就是说,神经网络的优势在于,它可以处理任何类型的问题,只要有足够的数据,神经网络就能通过学习找到正确的模式。

4.1.2 训练数据和测试数据

在机器学习中,我们通常将数据分为两部分:训练数据和测试数据。

训练数据:用于训练模型,帮助模型学习规律。另外,训练数据也可以称为监督数据。

测试数据:用于评估训练后的模型性能,检查它对新数据的处理能力。

为什么要分成训练数据和测试数据呢?因为我们希望模型具备较强的“泛化能力”。泛化能力指的是模型能够处理从未见过的数据,也就是能够正确处理实际中可能遇到的各种数据。如果只用训练数据来训练和评估模型,可能会导致“过拟合”——模型只对训练数据有效,不能处理其他数据。

泛化能力是机器学习的最终目标。比如在识别手写数字的问题中,泛化能力就非常重要。我们希望模型不仅能识别一个人写的数字“5”,还能识别其他人写的“5”。如果模型只能识别训练数据中的特定样本,那它就过拟合了,无法应用于其他样本。

因此,为了正确评估模型的能力,我们必须将数据分为训练集和测试集。通过这种方法,我们可以确保模型不仅能处理训练数据,还能应对现实中的新数据。

小结

通过合理地划分训练数据和测试数据,我们可以避免过拟合,使模型具备更好的泛化能力。这样,模型在面对新数据时,能够保持较好的性能,解决实际问题。

4.2 损失函数

假设有人问你现在有多幸福,你会怎么回答?很多人可能会说:“还可以吧”或者“不是那么幸福”。但如果有人回答:“我的幸福指数是10.23”,可能会让人觉得很奇怪,因为他用一个具体的数字来衡量自己的幸福感。

这个“幸福指数”其实可以用来类比神经网络中的“损失函数”。在神经网络的学习过程中,我们通过损失函数来衡量当前模型的状态,并以此为标准来寻找最优的参数。

换句话说,损失函数就像是一个衡量“幸福感”的工具,通过它来告诉我们当前的模型性能如何,而我们要做的就是通过调整模型的参数,减少这个损失函数的值。

损失函数可以用不同的方式来定义,常见的有均方误差交叉熵误差


4.2.1 均方误差

在神经网络中,均方误差(Mean Squared Error, MSE)是最常用的损失函数之一。它的公式如下:

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 = [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]  # 目标标签是数字“2”

在这个例子中, y y y 是神经网络预测的各个数字的概率, t t t 是正确的标签(以one-hot编码表示)。均方误差计算的是神经网络预测值与实际标签之间的差距的平方和。

第一个例子中,正确解是“2”,神经网络的输出的最大值是“2”;计算结果如下:

import numpy as np

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

y = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
t = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

print(mean_squared_error(y, t))

输出结果为0.09750000000000003
第一个例子中,正确解是“7”,神经网络的输出的最大值是“2”;计算结果如下:

import numpy as np

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

y = np.array([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0])
t = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0])

print(mean_squared_error(y, t))

输出结果为0.5975
我们发现第一个例子的损失函数的值更小,和监督数据之间的误差较小。也就是说,均方误差显示第一个例子的输出结果与监督数据更加吻合。

4.2.2 交叉熵误差

除了均方误差之外,交叉熵误差(cross entropy error)也经常被用作损失函数。交叉熵误差如下式所示。

E = − ∑ k t k log ⁡ y k (4.2) E=-\sum_{k}t_{k}\log y_{k}\tag{4.2} E=ktklogyk(4.2)

这里,log表示以e为底数的自然对数 ( log ⁡   e   ) ∘   y k (\log_{\mathrm{~e~}})_{\circ}\mathrm{~}y_{k} (log e ) 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 -\log0.6=0.51 log0.6=0.51 ;若“2”对应的输出是0.1,则交叉熵误差为 − log ⁡ 0.1 = 2.30 -\log0.1=2.30 log0.1=2.30 。也就是说,交叉熵误差的值是由正确解标签所对应的输出结果决定的。
在这里插入图片描述

关键性质

  • 当正确解标签对应的输出越大,交叉熵误差越小:以为正确解标签对应的输出 y k y_{k} yk接近 1,那么 log ⁡ y k \log y_{k} logyk接近 0,因此交叉熵误差 L 也接近 0。
  • 当正确解标签对应的输出越小,交叉熵误差越大:
    如果正确解标签对应的输出 y k y_{k} yk 接近 0,那么 log ⁡ y k \log y_{k} logyk接近负无穷大,因此交叉熵误差 L 会变得很大。

交叉熵误差的实现

为了实现交叉熵误差的计算,需要考虑数值稳定性。具体来说,当 y k y_{k} yk非常接近 0 时, log ⁡ y k \log y_{k} logyk会变成负无穷大,导致计算无法进行。为了避免这种情况,通常会在 y k y_{k} yk中加上一个非常小的值(如 1e−7),以防止对数函数计算时出现负无穷大。

def cross_entropy_error(y, t):
    delta = 1e-7  # 添加一个非常小的值,防止np.log(0)导致负无穷大
    return -np.sum(t * np.log(y + delta))
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))# np.float64(0.510825457099338)

总结

  • 交叉熵误差的值越小,说明神经网络的输出越接近正确解标签,模型的性能越好。
  • 数值稳定性:通过在 y k y _{k} yk中加上一个非常小的值(如 1 0 − 7 10^{−7} 107 ),可以防止对数函数计算时出现负无穷大,确保计算的稳定性。
  • 代码实现:通过简单的 Python 函数,可以高效地计算交叉熵误差,用于神经网络的损失函数计算。

4.2.3 mini-batch学习

机器学习通过训练数据来进行学习。具体来说,学习过程就是计算损失函数的值,然后调整参数,使得损失函数的值尽可能小。因此,在计算损失函数时,必须考虑所有训练数据。比如,如果训练数据有100个样本,那么我们就需要计算这100个样本的损失函数,并将它们的总和作为学习的目标。

前面介绍的损失函数通常都是针对单个数据点的损失函数。例如,在交叉熵误差的例子中,我们是计算每个数据点的损失。但如果我们想计算所有训练数据的损失总和,就需要对每个数据点的损失进行求和。

比如,假设我们有N个数据,损失函数的总和可以表示为:

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

这里, t n k t_{nk} tnk 是监督数据, y n k y_{nk} ynk 是神经网络的输出, n n n 表示第n个数据, k k k 表示数据的第k个维度。虽然公式看起来比较复杂,实际上它只是将单个数据点的损失函数扩展到了所有数据。最后,我们通过除以N来进行正规化,得到一个与训练数据的数量无关的统一指标。

这样,通过将所有数据的损失平均化,我们得到一个关于整个数据集的“平均损失函数”,这就能帮助我们更客观地评估模型的表现。无论训练数据有多少个样本,计算出的平均损失函数都能作为统一的标准来评估模型。

但是,在训练神经网络时,我们不可能每次都用所有的数据计算损失函数,特别是当数据量非常大的时候。为了解决这个问题,我们通常会采用mini-batch学习。

mini-batch学习是指从训练数据中随机选取一个小批量(mini-batch)来计算损失函数,而不是每次都计算整个数据集的损失。这种方法既能减少计算量,又能加速训练过程。

比如,如果训练数据有60000个样本,我们可以随机选择100个样本来计算损失函数。

import numpy as np

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计算损失函数即可。

ps:有点类似于蒙卡中抽样的概念。

4.2.4 mini-batch版交叉熵误差的实现

我们之前实现的交叉熵误差是针对单个数据点的,但在实际训练过程中,我们通常会使用mini-batch学习方法,也就是每次只计算一个小批量(mini-batch)的数据损失。那么,如何实现适用于mini-batch的交叉熵误差呢?其实,只需要对之前的单个数据的交叉熵误差函数做一点修改即可。

下面是处理单个数据和mini-batch数据两种情况的交叉熵误差函数:

def cross_entropy_error(y, t):
    if y.ndim == 1:  # 如果只有一个样本
        t = t.reshape(1, t.size)  # 将t的形状调整为2D
        y = y.reshape(1, y.size)  # 将y的形状调整为2D
    batch_size = y.shape[0]  # 获取batch的大小
    return -np.sum(t * np.log(y + 1e-7)) / batch_size  # 计算交叉熵误差并进行平均

在这个函数中,y 是神经网络的输出,t 是标签数据。为了处理mini-batch情况,我们首先判断数据是否是单个样本(y.ndim == 1),如果是,就需要调整数据形状,以适应后续的计算。然后,计算交叉熵误差时,我们使用batch_size来正规化(即计算平均损失)。

此外,当监督数据是标签形式(例如,像“2”、“7”这样的标签,而不是one-hot编码时),我们可以做如下改动:

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

在这个版本中,我们通过y[np.arange(batch_size), t]来提取每个样本对应标签的输出值。因为y是一个概率分布,t是标签索引,这样我们就能选出每个样本正确类别的预测值。

比如当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]])。

4.2.5 为何要设定损失函数

可能有同学会问:“既然我们关注的是提高识别精度,为什么还需要损失函数?”

这个问题的答案很简单:损失函数是为了帮助我们调整参数,优化模型。

在神经网络的学习中,我们通过计算损失函数的导数(梯度)来调整参数。如果我们直接使用识别精度作为指标,模型的参数几乎不可能进行有效更新,因为精度的变化通常是离散的,不适合用来指导学习。相比之下,损失函数的值是连续变化的,可以通过梯度下降法不断优化模型。

总结来说,损失函数不仅帮助我们评估模型的性能,还为优化过程提供了必要的反馈。

中篇我们会介绍数学基础部分。


网站公告

今日签到

点亮在社区的每一天
去签到