
1.4 开始学习神经网络
现在,我们知道神经网络是深度学习的核心,是现代计算机视觉的强大工具。但它们到底是什么呢?它们是如何工作的?下面,我们将不仅讨论它们效率背后的理论解释,而且还将直接将这些知识用于一个识别任务的简单网络的实现和应用。
1.4.1 建立神经网络
人工神经网络(Artificial Neural Network,ANN)或神经网络(Neural Network,NN)是强大的机器学习工具,擅长处理信息、识别常见模式或检测新模式以及模拟复杂的过程。这些优势得益于它们的结构,我们接下来将揭示这一点。
模拟神经元
众所周知,神经元是思想和反应的基本单元。但是它们实际上是如何工作的,以及应如何模拟它们,对研究人员来说并不是显而易见的。
生物启发
的确,人工神经网络灵感多少来自动物大脑的工作模式。大脑是一个由神经元组成的复杂网络,每个神经元相互传递信息,并将感官输入(如电信号和化学信号)转化为思想和行动。每个神经元的电输入来自它的树突,树突是一种细胞纤维,它将来自突触(与前一神经元相连的节点)的电信号传递到神经元胞体(神经元的主体)。如果累积的电刺激超过特定阈值,细胞就会被激活,电脉冲通过细胞的轴突(神经元的输出电缆,连接其他神经元的突触)进一步传播到下一个神经元。因此,每个神经元都可以被看作是一个非常简单的信号处理单元,一旦堆叠在一起,就可以实现我们现在的思想。
数学模型
受其生物表示的启发(见图1-11),人工神经元有几个输入(每个数据都有一个序号),将输入累加在一起,最后使用一个激活函数(activation function)来获得输出信号,输出可以传递给网络中的下一个神经元(这可以视为一个有向图)。

图1-11 简化的生物神经元(左)和人工神经元(右)
通常以加权方式计算输入求和。每个输入都是按照特定于输入的权重放大或缩小的。这些权重是在网络训练阶段进行优化调整的参数,以使神经元对适当的特征做出反应。通常,另一个参数(神经元的偏置)也被训练并用于这个求和过程。它的值只是作为偏移量加到加权和中。
我们来快速地用数学方法表示这个过程。假设我们有一个神经元,它有两个输入值,x0和x1。这些值的加权系数分别为w0和w1,可选的偏置为b。为了简化,将输入值表示为水平向量x,将权重表示为垂直向量w:

因此,整个运算可以简单地表示为:
z=x·w+b
这一步很简单,不是吗?两个向量之间的点积负责加权求和:

现在输入已经被缩放和相加成结果z了,我们需要对它应用激活函数来得到神经元的输出。回到与生物神经元的类比,它的激活函数将是一个二元函数,当y超过阈值t时,返回一个电脉冲(即1),否则返回0(通常情况下t=0)。如果将其用数学公式表示,那么激活函数y=f(z)可以表示为:

阶跃函数是原始感知机的关键组成部分,但研究人员早期就已经引入了更高级的激活函数,它们具有更好的特性,如非线性(用于对更复杂的行为建模)和连续可微性(这对于训练过程很重要,将在后面解释)。最常见的激活函数如下:
·sigmoid函数:
·双曲正切函数:
·修正线性单元:
上述常见激活函数的示意图如图1-12所示。

图1-12 常见激活函数示意图
对于所有的神经网络,基本都是以上的逻辑!这样我们就模拟了一个简单的人工神经元。它能够接收一个信号,处理它,并输出一个值,这个值可以被前向传递(前向传递是机器学习中常用的术语)给其他神经元,从而构建一个网络。
将多个没有非线性激活函数的神经元链接起来,本质上仍相当于一个神经元。例如,如果有一个参数为wA和bA的线性神经元,其后链接一个参数为wB和bB的线性神经元,那么
yB=wB·yA+bB=wB·(wA·x+bA)+bB=w·x+b
其中,w=wA·wB,b=bA+bB。因此,如果想要创建复杂的模型,非线性激活函数是必要的。
实现
以上模型可以简便地基于Python实现(使用numpy进行向量和矩阵运算):

如上段代码所示,这是对我们之前定义的数学模型的直接改编。使用这个人工神经元也很简单。我们来实例化一个感知机(使用阶跃函数为激活方法的神经元),并通过它前向传递一个随机输入:


在进入下一节开始扩大它们的规模之前,建议先花点时间用不同的输入和神经元参数进行一些实验。
将神经元分层组合在一起
通常,神经网络被组织成多层,也就是说,每层的神经元通常接收相同的输入并应用相同的操作(例如,尽管每个神经元首先用自己特定的权重来对输入求和,但是它们应用相同的激活函数)。
数学模型
在网络中,信息从输入层流向输出层,中间有一个或多个隐藏层。在图1-13中,3个神经元A、B、C分别属于输入层,神经元H属于输出层或激活层,神经元D、E、F、G属于隐藏层。第一层输入x的维度为2,第二层(隐藏层)将前一层的三个激活值作为输入,以此类推。这类每个神经元均连接到前一层的所有值的网络,被称为全连接或稠密网络。

图1-13 有两个输入值和一个最终输出的三层神经网络
同样,通过用向量和矩阵表示这些元素来简化计算表达。以下操作由第一层完成:
zA=x·wA+bA
zB=x·wB+bB
zC=x·wC+bC
这可以表示为:
z=x·W+b
为了得到前面的方程,必须定义如下的变量:

因此,第一层的激活函数可以写成一个向量y=f(z)=(f(zA) f(zB) f(zC)),它可以作为输入向量直接传递到下一层,以此类推,直到最后一层。
实现
与单个神经元一样,这个模型也可以用Python实现。事实上,我们甚至不需要对Neuron类做太多的编辑:

我们只需要改变一些变量的维度来反映一个层内神经元的多样性。有了这个实现,每层甚至可以一次处理多个输入!传递一个列向量x(向量形状是1×s,其中s是x中数值的个数)或一组列向量(向量形状为n×s,其中n是样本的数量)不会改变任何关于矩阵的计算,并且网络的层都将正确输出叠加的结果(假设每一行与b相加):


一组输入数据通常称为一批(batch)。
有了这个实现,只需将全连接的层连接在一起就可以构建简单的神经网络。
将网络应用于分类
我们已经知道了如何定义层,但还没有将其初始化并连接到计算机视觉网络中。为了演示如何做到这一点,我们将处理一个著名的识别任务。
设置任务
对手写数字的图像进行分类(即识别图像中是否包含0或1等)是计算机视觉中的一个历史性问题。修正的美国国家标准与技术研究院(Modified National Institute of Standards and Technology,MNIST)数据集(http://yann.lecun.com/exdb/mnist/)包含70 000张灰度数字图像(像素为28×28),多年来一直作为参考,方便研究人员通过这个识别任务测试他们的算法(Yann LeCun和Corinna Cortes享有这个数据集的所有版权,数据集如图1-14所示)。

图1-14 MNIST数据集中每个数字的10个样本
对于数字分类,我们需要的是一个将这些图像中的一个作为输入并返回一个输出向量,该向量表示网络认为的这些图像与每个类对应的概率。输入向量有28×28=784个值,而输出有10个值(对于从0到9的10个不同数字)。在输入和输出之间,由我们来定义隐藏层的数量和它们的大小。要预测图像的类别,只需通过网络前向传递图像向量,收集输出,然后返回置信度得分最高的类别即可。
这些置信度得分通常被转化为概率值,以简化后续的计算或解释。例如,假设一个分类网络给类“狗”赋值为9,给另一个类“猫”赋值为1。这就是说,根据这个网络,图像显示的是狗的概率为9/10,显示的是猫的概率为1/10。
在实现解决方案之前,先通过加载MNIST数据来完成用于训练和测试算法的数据准备。为了简单起见,我们将使用由Marc Garcia开发的Python mnist模块(https://github.com/datapythonista/mnist)(根据BSD 3-Clause的新/修订许可,已经安装在本章的源目录中):

关于数据集预处理和可视化的更详尽操作可以在本章的源代码中找到。
实现网络
对于神经网络本身,我们必须把层组合在一起,并添加一些算法在整个网络上进行前向传递,然后根据输出向量来预测分类。在实现各层之后,下面的代码就不言自明了:


我们刚刚实现了一个前馈神经网络,可以用来分类!是时候把它应用到我们的问题上了:

我们的准确率只有12.06%。这可能看起来令人失望,因为它的准确性仅略好于随机猜测。但是这是有意义的——因为此时的网络是由随机参数定义的。我们需要根据用例来训练它,这就是下一节中需要处理的任务。
1.4.2 训练神经网络
神经网络是一种特殊的算法,因为它们需要训练,也就是说,它们需要通过从可用的数据中学习来针对特定的任务进行参数优化。一旦网络被优化至在训练数据集上表现良好,它们就可以在新的、类似的数据上使用,从而提供令人满意的结果(如果训练正确的话)。
在解决MNIST任务之前,我们将介绍一些理论背景,涵盖不同的学习策略,并介绍实际中是如何进行训练的。然后,将直接把这些概念应用到示例中,以便我们的简单网络最终学会如何解决识别任务!
学习策略
当涉及神经网络学习时,根据任务和训练数据的可用性,主要有三种学习范式。
有监督学习
有监督学习(supervised learning)可能是最常见的,当然也是最容易掌握的学习范式。当我们想要教会神经网络两种模式之间的映射关系(例如,将图像映射到它们的类标签或它们的语义掩膜)时,适合使用有监督学习。它需要访问一个包含图像及其真值标签(例如每张图像的类信息或语义掩膜)的训练数据集。
这样一来,训练就很简单了:
·将图像提供给网络,并收集其结果(即预测标签)。
·评估网络的损失,即将预测结果与真值标签进行比较时的错误程度。
·相应地调整网络参数以减少损失。
·重复以上操作直至网络收敛,也就是说,直到它在这批训练数据上不能再进一步改进为止。
这种学习策略确实可以形容为“有监督的”,因为有一个实体(即我们)通过对每个预测结果提供反馈(根据真值标签计算出的损失)来监督网络的训练情况,以便该算法可以通过重复训练(观察某次训练是正确的或错误的,然后再试一次)来学习。
无监督学习
然而,当没有任何真值信息可用时,如何训练网络?答案是采用无监督学习(unsupervised learning)。它的思想是创建一个函数,仅根据网络的输入和相应的输出来计算网络的损失。
这种策略非常适用于聚类(将具有相似属性的图像分组在一起)或压缩(减少内容大小,同时保留一些属性)等应用程序。对于聚类,损失函数可以衡量来自某一类相似图像与其他类图像的比较情况。对于压缩,损失函数可以衡量压缩后的数据与原始数据相比,重要属性的保留程度。
无监督学习需要一些关于用例的专业知识,才能提出有意义的损失函数。
强化学习
强化学习(reinforcement learning)是一种交互式策略。智能体在环境中导航(例如,机器人在房间中移动或电子游戏角色通过关卡)。智能体有一个预定义的、可执行的动作列表(走、转、跳等),并且在每个动作之后,它会进入一个新的状态。有些状态可以带来“奖励”,这些奖励可以是即时的,也可以是延迟的,可以是正面的,也可以是负面的(例如,游戏角色获得额外物品时的正面奖励,游戏角色被敌人击中时的负面奖励)。
在每个时刻,神经网络只提供来自环境的观察(例如,机器人的视觉输入或视频游戏屏幕)和奖励反馈(胡萝卜和大棒)。由此,它必须了解什么能带来更高的奖励并据此为智能体制定最佳的短期或长期策略。换句话说,它必须评估出能使其最终奖励最大化的一系列行为。
强化学习是一个强大的学习范式,但是很少用于计算机视觉用例。虽然我们鼓励机器学习爱好者学习更多的知识,但在这里我们不会再做进一步介绍。
训练时间
不管学习策略是什么,大致的训练步骤都是一样的。给定一些训练数据,网络进行预测并接收一些反馈(如损失函数的结果),然后用这些反馈更新网络的参数。然后重复这些步骤,直到无法进一步优化网络为止。本节将详细介绍并实现这个过程,从损失计算到优化权重。
评估损失
损失函数的目标是评估网络在其当前权重下的性能。更正式地说,这个函数将预测效果表示为网络参数(比如它的权重和偏置)的函数。损失越小,针对该任务的参数就越好。
因为损失函数代表了网络的目标(例如,返回正确的标签,压缩图像同时保留内容,等等),所以有多少任务就有多少不同的函数。尽管如此,有些损失函数比其他函数更常用一些,比如平方和函数,也称为L2损失函数(基于L2范数),它在有监督学习中无处不在。这个函数简单地计算输出向量y的每个元素(网络估计的每个类的概率)和真值ytrue向量(除了正确的类之外,该目标向量中对应的其余每个类都是空值)的每个元素之间的差的平方:

还有许多其他性质不同的损失函数,如计算矢量之间绝对差的L1损失,如二进制交叉熵(Binary Cross-Entropy,BCE)损失,它把预测概率转换为对数,然后与预期的值进行比较:

对数运算将概率从[0,1]转换为[-∞,0],因此将结果乘以-1,神经网络在学习如何正确预测时损失值的区间就可以变换为[0,+∞]。请注意,交叉熵函数也可以应用于多分类的问题(不仅仅局限于两类)。
人们通常会将损失值除以向量中元素的数量,也就是说,计算平均值而不是损失总和。均方误差(Mean-Square Error,MSE)是L2损失的平均值,平均绝对误差(Mean-Absolute Error,MAE)是L1损失的平均值。
现在,我们将以L2损失为例,并在后面的理论解释和MNIST分类器训练中用到它。
反向传播损失
如何更新网络参数,才能使其损失最小?对于每个参数,我们需要知道的是改变它的值会如何影响损失。如果知道哪些变化会使损失减少,那么只需重复应用这些变化,直到损失达到最小即可。这正是损失函数梯度的原理,也是梯度下降的过程。
在每次训练迭代中,计算损失函数对网络各参数的导数。这些导数表示需要对参数进行哪些小的更改(由于梯度表示函数上升的方向,而我们希望最小化它,因此这里有一个为-1的系数)。它可以看作是沿着损失函数关于每个参数的斜率逐步下降,因此这个迭代过程被称为梯度下降(见图1-15)。

图1-15 优化神经网络参数P的梯度下降过程示意图
现在的问题是,如何计算所有这些导数(即斜率值,以作为每个参数的函数)?这时链式法则就派上用场了。并不需要太深入的微积分知识,链式法则可以告诉我们,关于k层参数的导数可以简单地用该层的输入和输出值(xk,yk),以及k+1层的导数来计算。更正式地说,对于该层的权值Wk,有以下公式:

式中,l′k+1是k+1层对输入的导数,xk+1=yk,fk′是这层激活函数的导数,xT是x的转置。请注意,zk表示k层的加权和的结果(即在该层的激活函数输入之前),其定义见1.4.1节。最后,符号表示两个向量或矩阵之间对应元素相乘,它也被称为哈达玛积。如下式所示,哈达玛积基本就是将各对应元素成对相乘:

回到链式法则,对偏置的导数也可以用类似的方法计算:

最后,为便于你能够掌握得更加详尽,还有以下等式:

这些计算可能看起来很复杂,但我们只需要理解它们所代表的内涵——我们可以一层一层地、逆向地计算每个参数如何递归地影响损失(使用某一层的导数来计算前一层的导数)。我们也可以通过将神经网络表示为计算图,即作为数学运算的图形链接在一起,来说明该概念。(执行第一层的加权求和,将其结果传递给第一个激活函数,然后将输出传递给第二层进行操作,以此类推)。因此,计算整个神经网络关于某些输入的结果就包含了通过这个计算图来前向传递数据,而获得关于它的每个参数的导数则包含了将产生的损失在计算图中的向后传播,因此这个过程被称为反向传播。
为了从输出层开始这个过程,我们需要损失本身对输出值的导数(参考前面的方程)。因此,损失函数的推导是很容易的。例如,L2损失的导数为:

正如之前提到的,一旦知道了每个参数的损失的导数,只需要相应地更新它们即可:

如上所示,在更新参数之前,导数通常要乘以一个因子。这个因子被称为学习率。它有助于控制在每次迭代中更新每个参数的强度。较大的学习率可能允许网络学习得更快,但有可能使步伐太大造成网络错过最小损失值。因此,应该谨慎地设置它的值。完整的训练过程如下:
1)选择n幅图像用于下一次训练并将它们输入到网络中。
2)利用链式法则求出对各层参数的导数,计算并反向传播损失。
3)根据相应的导数值更新参数(根据学习率控制更新尺度)。
4)重复步骤1~3来遍历整个训练集。
5)重复步骤1~4,直到收敛或直到迭代完固定的次数为止。
整个训练集上的一次完整迭代(步骤1~4)称为一轮(epoch)。如果n=1,则在剩余的图像中随机选取训练样本,这个过程称为随机梯度下降(Stochastic Gradient Descent,SGD),它易于实现和可视化,但速度较慢(更新次数较多)、噪声较大。人们倾向于选择小批量随机梯度下降(mini-batch stochastic gradient descent)。它意味着使用更多的(n更大)值(受计算机能力的限制),这时的梯度是具有n个随机训练样本的每个小批(或更简单地称为批)上的平均梯度(这样噪声更小)。
如今,不管n为多少,SGD这个术语都已被广泛使用。
在本节中,我们讨论了如何训练神经网络。是时候把这些知识付诸实践了!
训练网络分类
到目前为止,我们只实现了网络及其层的前馈功能。首先,更新FullyConnectedLayer类,以便添加反向传播和优化算法:

本节中提供的代码经过了简化,并去掉了注释,以保持合适的长度。完整的源代码可以在本书的GitHub库中找到,同时还可找到一个Jupyter Notebook,它将所有内容连接在了一起。
现在,我们需要通过一层一层地加载算法以实现反向传播和优化,并利用最终的算法来覆盖完整的训练(步骤1~5),因此相应地对SimpleNetwork类进行更新:


一切准备就绪!我们可以训练神经网络,并看看它的表现如何:

如果你的计算机有足够的算力来完成这个训练(这个简单的实现没有利用GPU),那么就将得到该神经网络,它能够以94.8%的准确率对手写数字进行分类!
训练注意事项:欠拟合和过拟合
我们邀请你尝试一下刚刚实现的框架,尝试不同的超参数(层大小、学习率、批大小等)。选择合适的网络拓扑(以及其他超参数)可能需要大量的调整和测试。虽然输入层和输出层的大小是由用例情况(例如,对于分类,输入大小是图像的像素数量,而输出大小是待预测的类的数量)决定的,隐藏层仍应该经过精心设计。
举例来说,如果网络的层数太少或层太小,那么准确率可能会停滞不前。这意味着网络是欠拟合的,也就是说,它没有足够的参数来处理复杂任务。在这种情况下,唯一的解决方案是采用更适合用例的新架构。
另一方面,如果网络太复杂或训练数据集太小,网络可能会针对训练数据产生过拟合。这意味着该网络将很好地适应训练分布(即其特定的噪声、细节等),但不能泛化到新的样本(因为这些新图像可能有稍微不同的噪声)。图1-16展示了这两个问题之间的区别。最左侧的回归方法没有足够的参数来模拟数据的变化,而最右侧的方法则由于参数太多,失去了泛化能力。

图1-16 欠拟合和过拟合的常见示意图
虽然收集更大、更多样化的训练数据集似乎是过拟合的合理解决方案,但在实践中并不总是可行的(例如,由于目标对象存在访问限制)。另一种解决方案是调整网络或其训练,以限制网络学习的细节。这些方法将在第3章详细介绍,届时还会在该章介绍其他先进的神经网络解决方案。