作者归档:feiguyun

5.4 选择合适激活函数

激活函数在神经网络中作用有很多,主要作用是给神经网络提供非线性建模能力。如果没有激活函数,那么再多层的神经网络也只能处理线性可分问题。作为神经网络的激活函数,一般需要如下3个条件。
1)非线性:为提高模型的学习能力,如果是线性,那么再多层都相当于只有两层效果。
2)可微性:有时可以弱化,在一些点存在偏导即可。
3)单调性:保证模型简单。
常用的激活函数有sigmoid、tanh、relu、softmax等。它们的图形、表达式、导数等信息如表5-2所示。
在搭建神经网络时,如何选择激活函数?如果搭建的神经网络层数不多,选择sigmoid、tanh、relu、softmax都可以;如果搭建的网络层次比较多,那就需要小心,选择不当就可导致梯度消失问题。此时一般不宜选择sigmoid、tanh激活函数,因它们的导数都小于1,尤其是sigmoid的导数在[0,1/4]之间,多层叠加后,根据微积分链式法则,随着层数增多,导数或偏导将指数级变小。所以层数较多的激活函数需要考虑其导数不宜小于1当然也不能大于1,大于1将导致梯度爆炸,导数为1最好,激活函数relu正好满足这个条件。所以,搭建比较深的神经网络时,一般使用relu激活函数,当然一般神经网络也可使用。此外,激活函数softmax由于\sum_i \delta_i(z)=1,常用于多分类神经网络输出层。
激活函数在PyTorch中使用示例:

激活函数输入维度与输出维度是一样的。激活函数的输入维度一般包括批量数N,即输入数据的维度一般是4维,如(N,C,W,H)。

5.5 选择合适的损失函数

损失函数(Loss Function)在机器学习中非常重要,因为训练模型的过程实际就是优化损失函数的过程。损失函数对每个参数的偏导数就是梯度下降中提到的梯度,防止过拟合时添加的正则化项也是加在损失函数后面。损失函数用来衡量模型的好坏,损失函数越小说明模型和参数越符合训练样本。任何能够衡量模型预测值与真实值之间的差异的函数都可以叫做损失函数。在机器学习中常用的损失函数有两种,即交叉熵(Cross Entropy)和均方误差(Mean Squared Error,MSE),分别对应机器学习中的分类问题和回归问题。
对分类问题的损失函数一般采用交叉熵,交叉熵反应的两个概率分布的距离(不是欧氏距离)。分类问题进一步又可分为多目标分类,如一次要判断100张图是否包含10种动物,或单目标分类。
回归问题预测的不是类别,而是一个任意实数。在神经网络中一般只有一个输出节点,该输出值就是预测值。反应的预测值与实际值之间距离可以用欧氏距离来表示,所以对这类问题我们通常使用均方差作为损失函数,均方差的定义如下:
 MSE=\frac{\sum_{i=1}^n (y_i - y_{i}')^2}{n} \tag{5.8}
PyTorch中已集成多种损失函数,这里介绍两个经典的损失函数,其他损失函数基本上是在它们的基础上的变种或延伸。
1.torch.nn.MSELoss
具体格式:

计算公式:
L(x,y)=(l_1,l_2,\cdots,l_N)^T,l_n=(x_n-y_n)^2
其中,N是批量大小。
如果参数reduction为非None(默认值为'mean'),则:
L(x,y)=\begin{cases} mean(L),if reduction='mean'\\ sum(L),if reduction='sum')\end{cases} \tag{5.9}
x和y是任意形状的张量,每个张量都有n个元素,如果reduction取'none', L(x,y)将不是标量;如果取'sum', L(x,y)只是差平方的和,但不会除以n。
参数说明:
size_average,reduce在PyTorch官方的后续版本中将移除,主要看参数reduction,reduction可以取'none','mean','sum',默认值为'mean'。如果size_average,reduce取了值,将覆盖reduction的值。
代码示例:

2.torch.nn.CrossEntropyLoss
交叉熵损失(Cross-Entropy Loss)又称为对数似然损失(Log-likelihood Loss)、对数损失;二分类时还可称为逻辑斯谛回归损失(Logistic Loss)。在Pytroch里,它不是严格意义上的交叉熵损失函数,而是先将input经过softmax激活函数,将向量“归一化”成概率形式,然后再与target计算严格意义上的交叉熵损失。在多分类任务中,经常采用softmax激活函数+交叉熵损失函数,因为交叉熵描述了两个概率分布的差异,然而神经网络输出的是向量,并不是概率分布的形式。所以需要softmax激活函数将一个向量进行“归一化”成概率分布的形式,再采用交叉熵损失函数计算损失值。
一般格式:

计算公式:
 loss(x,class)=-log(\frac {exp(x[class])}{(\sum_j exp(x[j]))})=-x[class]+log(\sum_j exp (x[j])) \tag{5.10}
如果带上权重参数weight,则:
loss(x,class)=weight[class](-x[class]+log(\sum_j exp(x[j])))\tag{5.11}
weight(Tensor)- 为每个类别的loss设置权值,常用于类别不均衡问题。weight必须是float类型的tensor,其长度要于类别C一致,即每一个类别都要设置有weight。
代码示例:

5.6 使用合适的优化器

优化器在机器学习、深度学习中往往起着举足轻重的作用,同一个模型,因选择不同的优化器,性能有可能相差很大,甚至导致一些模型无法训练。所以,了解各种优化器的基本原理非常必要。本节重点介绍各种优化器或算法的主要原理,及各自的优点或不足。

5.6.1传统梯度优化的不足

传统梯度更新算法为最常见、最简单的一种参数更新策略。其基本思想是:先设定一个学习率\lambda,参数沿梯度的反方向移动。假设基于损失函数L(f(x,\theta),y),其中\theta需更新的参数,梯度为g,则其更新策略的伪代码如下所示:

这种梯度更新算法简洁,当学习率取值恰当时,可以收敛到全面最优点(凸函数)或局部最优点(非凸函数)。
但其不足也很明显,对超参数学习率比较敏感(过小导致收敛速度过慢,过大又越过极值点),如图5-12的右图所示。在比较平坦的区域,因梯度接近于0,易导致提前终止训练,如图5-12的左图所示,要选中一个恰当的学习速率往往要花费不少时间。
图5-12学习速率对梯度的影响
学习率除了敏感,有时还会因其在迭代过程中保持不变,很容易造成算法被卡在鞍点的位置,如图5-13所示。

图5-13算法卡在鞍点示意图
另外,在较平坦的区域,因梯度接近于0,优化算法往往因误判,还未到达极值点,就提前结束迭代,如图5-14所示。

图5-14在较平坦区域,梯度接近于0,优化算法因误判而提前终止迭代
传统梯度优化方面的这些不足,在深度学习中会更加明显。为此,人们自然想到如何克服这些不足的问题。从本小节前文更新策略的伪代码可知,影响优化的主要因素有:
• 优化训练数据集的大小
• 优化梯度方向
• 优化学习率
所以很多优化方法大多从这些方面入手,数据集优化方面采用批量随机梯度下降方法,从梯度方向优化方面有动量更新策略;有些从学习率入手,这涉及自适应问题;还有从两方面同时入手等方法,接下来将介绍这些方法。

5.6.2 批量随机梯度下降法

梯度下降法是非常经典的算法,训练时,如果使用全训练集,虽然可获得较稳定的值,但比较耗费资源,尤其当训练数据比较大时;另一个极端时,每次训练时用一个样本(又称为随机梯度下降法),这种训练方法振幅较大,也比较耗时,如图5-15所示。
图5-15 随机梯度下降法的损失值变化示意图
这种方法虽然资源消耗较少,但很耗时时间,因这种方法无法充分发挥深度学习程序库中高度优化的矩阵运算的优势。为更有效训练模型,我们采用一种折中方法,即批量随机下降法。这种梯度下降方法有两个特点:一是批量,另一个是随机性。如何实现批量随机下降呢? 其伪代码如下:
其中x^{(i)}和小批量数据集的所有元素都是从训练集中随机抽出的,这样梯度的预期将保持不变,相对于随机梯度下降,批量随机梯度下降降低了收敛波动性,即降低了参数更新的方差,使得更新更加稳定,这些因素都有利于提升其收敛效果,如图5-16所示。
图5-16 批量随机梯度下降法的损失值变化示意图

5.6.3 动量算法

梯度下降法在遇到平坦或高曲率区域时,学习过程有时很慢。利用动量算法能比较好解决这个问题。动量算法与传统梯度下降优化的效果,我们以求解函数f(x_1,x_2)=0.05x_1^2+2x_1^2极值为例,使用梯度下降法和动量算法分别进行迭代求解,具体迭代过程如图5-17、图5-18所示(图实现代码请参考本书第5章代码部分)。
图5-17 梯度下降法的迭代轨迹 图5-18 使用动量项的迭代轨迹
从图5-17 可以看出,不使用动量算法的SGD学习速度比较慢,振幅比较大;图5-18 可以看出,使用动量算法的SGD,振幅较小,而且较快到达极值点。动量算法是如何做到这点的呢?
动量(momentum)是模拟物理里动量的概念,具有物理上惯性的含义,一个物体在运动时具有惯性,把这个思想运用到梯度下降计算中,可以增加算法的收敛速度和稳定性,具体实现如图5-19所示。

图5-19动量算法示意图
由图5-19所示,可知动量算法每下降一步都是由前面下降方向的一个累积和当前点的梯度方向组合而成。含动量的随机梯度下降法,其算法伪代码如下:
批量随机梯度下降法PyTorch代码实现:

其中parameters是模型参数,假设模型为model,则parameters指model. parameters()。
具体使用动量算法时,动量项的计算公式如下:
v_k=\alpha v_{k-1}+(-\lambda \hat g(\theta_k)\tag{5.12}
如果按时间展开,则第k次迭代使用了从1到k次迭代的所有负梯度值,且负梯度按动量系数α指数级衰减,相当于使用了移动指数加权平均,具体展开过程如下:
由此可知,当在比较平缓处,但α=0.5、0.9时,将是分别是梯度下降法的2倍、10倍。
使用动量算法,不但可加速迭代速度,还可能跨过局部最优找到全局最优,如图5-20所示。
图5-20 使用动量算法的潜在优势

5.6.4 Nesterov动量算法

既然每一步都要将两个梯度方向(历史梯度、当前梯度)做一个合并再下降,那为什么不先按照历史梯度往前走那么一小步,按照前面一小步位置的“超前梯度”来做梯度合并呢?如此一来,可以先往前走一步,在靠前一点的位置(如图5-21中的C点)看到梯度,然后按照那个位置再来修正这一步的梯度方向,如图5-21所示。

图5-21 Nesterov下降法示意图
这就得到动量算法的一种改进算法,称为NAG(Nesterov Accelerated Gradient)算法,也称Nesterov动量算法。这种预更新方法能防止大幅振荡,不会错过最小值,并对参数更新更加敏感。如图5-22所示。
图5-22 Nesterov加速梯度下降法
NAG下降法的算法伪代码如下:

NAG算法的PyTorch实现如下:

NAG动量法和经典动量法的差别就在B点和C点梯度的不同。动量法,更多关注梯度下降方法的优化,如果能从方向和学习率同时优化,效果或许更理想。事实也确实如此,而且这些优化在深度学习中显得尤为重要。接下来我们介绍几种自适应优化算法,这些算法同时从梯度方向及学习率进行优化,效果非常好。

5.6.5 AdaGrad算法

传统梯度下降算法对学习率这个超参数非常敏感,难以驾驭;对参数空间的某些方向也没有很好的方法。这些不足在深度学习中,因高维空间、多层神经网络等因素,常会出现平坦、鞍点、悬崖等问题,因此,传统梯度下降法在深度学习中显得力不从心。还好现在已有很多解决这些问题的有效方法。上节介绍的动量算法在一定程度缓解对参数空间某些方向的问题,但需要新增一个参数,而且对学习率的控制还不很理想。为了更好驾驭这个超参数,人们想出来多种自适应优化算法,使用自适应优化算法,学习率不再是一个固定不变值,它会根据不同情况自动调整来适用情况。这些算法使深度学习向前迈出一大步!这节我们将介绍几种自适应优化算法。
AdaGrad算法是通过参数来调整合适的学习率λ,能独立地自动调整模型参数的学习率,对稀疏参数进行大幅更新和对频繁参数进行小幅更新,如图5-23所示。因此,Adagrad方法非常适合处理稀疏数据。AdaGrad算法在某些深度学习模型上效果不错。但还有些不足,可能因其累积梯度平方导致学习率过早或过量的减少所致。
图5-23 AdaGrad 算法迭代过程示意图
AdaGrad算法伪代码如下:
由上面算法的伪代码可知:
•随着迭代时间越长,累积梯度r越大,从而学习速率\frac{\lambda}{\delta+\sqrt{r}}随着时间就减小,在接近目标值时,不会因为学习速率过大而越过极值点。
•不同参数之间学习速率不同,因此,与前面固定学习速率相比,不容易在鞍点卡住。
•如果梯度累积参数r比较小,则学习速率会比较大,所以参数迭代的步长就会比较大。相反,如果梯度累积参数比较大,则学习速率会比较小,所以迭代的步长会比较小。
AdaGrad算法的PyTorch实现代码:

5.6.6 RMSProp算法

RMSProp算法修改AdaGrad,为的是在非凸背景下的效果更好,在凸函数可能振幅较大,如图5-24所示。对梯度平方和累计越来越大的问题,RMSProp指数加权的移动平均代替梯度平方和。RMSProp为使用移动平均,引入了一个新的超参数ρ,用来控制移动平均的长度范围。
图5-24 RMSProp迭代过程示意图
RMSProp算法伪代码如下:
RMSProp算法在实践中已被证明是一种有效且实用的深度神经网络优化算法,在深度学习中得到广泛应用。
RMSProp算法PyTorch实现代码如下:

5.6.7 Adam算法

Adam(Adaptive Moment Estimation,自适应矩估计)算法本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳,如图5-25所示。
图5-25 Adam算法迭代过程示意图
Adam算法是另一种学习速率自适应的深度神经网络方法,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习速率。Adam算法伪代码如下:
Adam算法的PyTorch实现如下。

5.6.8 Yogi算法

Adam算法综合了动量算法及自适应算法的优点,是深度学习常用的算法,但也存在一些问题:即使在凸环境下,当 r的第二力矩估计值爆炸时,它可能无法收敛。为此可通过改进r和优化参数初始化等方法来解决。 其中通过改进r是一种有效方法,即把
这就是Yogi更新。
Yogi算法的PyTorch代码如下:

前文介绍了深度学习的正则化方法,它是深度学习核心之一;优化算法也是深度学习的核心之一。优化算法很多,如随机梯度下降法、自适应优化算法等,那么具体使用时该如何选择呢?
RMSprop、Nesterov、Adadelta和Adam被认为是自适应优化算法,因为它们会自动更新学习率。而使用SGD时,必须手动选择学习率和动量参数,通常会随着时间的推移而降低学习率。
有时可以考虑综合使用这些优化算法,如采用先用Adam,然后用SGD优化方法,这个想法,实际上由于在训练的早期阶段SGD对参数调整和初始化非常敏感。因此,我们可以通过先使用Adam优化算法进行训练,这将大大节省训练时间,且不必担心初始化和参数调整,一旦用Adam训练获得较好的参数后,我们可以切换到SGD +动量优化,以达到最佳性能。采用这种方法有时能达到很好效果,如图5-26所示,迭代次数超过150后,用SGD效果好于Adam。
图5-26 迭代次数与测试误差间的对应关系

5.6.9 使用优化算法实例

这里使用MNIST数据集,使用自定义的优化算法实现图像的分类任务。为便于比较这里使用梯度下降法及动量算法两种优化算法。数据集的导入及预处理可参考本书的3.5节或参考本书第5章对应代码。
1)算法定义。可参考本书第5.6.2和5.6.3节中PyTorch实现算法部分。
2)定义模型。

3)定义损失函数。

4)加载数据。这里使用批量大小为128的训练数据集。

5)训练模型。

如果使用动量算法,只要把sgd(net.parameters(),1e-2)改为sgd_momentum(net.parameters(), vs, 1e-2, 0.9)即可。
6)可视化两种优化算法的运行结果。

运行结果如图5-27所示。
图5-27 梯度下降法与动量算法的比较
从图5-27可知,动量算法的优势还是非常明显的。完整代码及其他优化算法的实现请参考本书第5章的代码及数据部分。

第5 章 机器学习基础

第一部分我们介绍了NumPy、Tensor、nn等内容,这些内容是继续学习PyTorch的基础。有了这些基础,学习第二部分就容易多了。第二部分我们将介绍深度学习的一些基本内容,以及如何用PyTorch解决机器学习、深度学习的一些实际问题。
深度学习是机器学习的重要分支,也是机器学习的核心,但深度学习是在机器学习基础上发展起来的,因此理解机器学习的基本概念、基本原理对理解深度学习大有裨益。
机器学习的体系很庞大,限于篇幅,本章主要介绍基本知识及与深度学习关系比较密切的内容,如果读者希望进一步学习机器学习的相关知识,建议参考周志华老师编著的《机器学习》或李航老师编著的《统计学习方法》。
本章先介绍机器学习中常用的监督学习、无监督学习等,然后介绍神经网络及相关算法,最后介绍传统机器学习中的一些不足及优化方法等,本章主要内容包括:
♦机器学习的基本任务
♦机器学习的一般流程
♦解决过拟合、欠拟合的一些方法
♦选择合适的激活函数、损失函数、优化器等
♦GPU加速

5.1 机器学习的基本任务

机器学习的基本任务一般分为四大类,监督学习、无监督学习、半监督学习和强化学习。监督学习、无监督学习比较普遍,大家也比较熟悉。常见的分类、回归等属于监督学习,聚类、降维等属于无监督学习。半监督学习和强化学习的发展历史虽没有前两者这么悠久,但发展势头非常迅猛。图5-1 说明了四种分类的主要内容。

图5-1 机器学习的基本任务

5.1.1 监督学习

监督学习是最常见的一种机器学习类型,其任务的特点就是给定学习目标,这个学习目标又称为标签或或标注或实际值等,整个学习过程就是围绕如何使预测与目标更接近。近些年,随着深度学习的发展,分类除传统的二分类、多分类、多标签分类之外,分类也出现一些新内容,如目标检测、目标识别、图像分割等是监督学习重要内容。监督学习过程如图5-2所示。

图5-2 监督学习的一般过程

5.1.2 无监督学习

监督学习的输入数据中有标签或目标值,但在实际生活中,有很多数据是没有标签的,或者标签代价很高。这些没有标签的数据也可能包含很重要规则或信息,从这类数据中学习到一个规则或规律的过程称为无监督学习。在无监督学习中,我们通过推断输入数据中的结构来建模,模型包括关联学习、降维、聚类等。

5.1.3 半监督学习

半监督是监督学习与无监督学习相结合的一种学习方法。半监督学习使用大量的未标记数据,同时由部分使用标记数据进行模式识别。半监督学习目前正越来越受到人们的重视。
自编码器是一种半监督学习,其生成的目标就是未经修改的输入。语言处理中根据给定文本中词预测下一个词,也是半监督学习的例子。
对抗生成式网络也是一种半监督学习,给定一些真图像或语音,然后,通过对抗生成网络生成一些与真图像或语音逼真的图形或语音。

5.1.4 强化学习

强化学习是机器学习的一个重要分支,是多学科多领域交叉的一个产物。强化学习主要包含四个元素,智能体(Agent),环境状态,行动,奖励, 强化学习的目标就是获得最多的累计奖励。
强化学习把学习看作试探评价过程,Agent选择一个动作作用于环境,环境在接收该动作后状态发生变化,同时产生一个强化信号(奖或惩)反馈给Agent,然后再由Agent根据强化信号和环境的当前状态选择下一个动作,选择的原则是使Agent受到正强化(奖)的概率增大。选择的动作不仅影响立即强化值,也影响下一时刻的状态和最终的强化值。
强化学习不同于监督学习,主要表现在教师信号上,强化学习中由环境提供的强化信号是Agent对所产生动作的好坏作一种评价,而不是告诉Agent如何去产生正确的动作。由于外部环境提供了很少的信息,因此Agent必须靠自身的经历进行学习。通过这种方式,Agent在行动—评价的环境中获得知识,改进行动方案以适应环境。
AlphaGo Zero带有强化学习内容,它完全摒弃了人类知识,碾压了早期版本的AlphaGo,更足显强化学习和深度学习结合的巨大威力。

5.2 机器学习一般流程

机器学习一般流程首先需要明确目标、收集数据、探索数据、预处理数据,对数据处理后,接下来开始构建和训练模型、评估模型,然后优化模型等步骤,图5-3 为机器学习一般流程图。
图5-3 机器学习一般流程图
通过这个图形可直观了解机器学习的一般步骤或整体框架,接下来我们就各部分分别加以说明。

5.2.1 明确目标

在实施一个机器学习项目之初,定义需求、明确目标、了解要解决的问题以及目标涉及的范围等非常重要,它们直接影响后续工作的质量甚至成败。明确目标,首先需要明确大方向,比如当前需求是分类问题还是预测问题或聚类问题等。清楚大方向后,需要进一步明确目标的具体含义。如果是分类问题,还需要区分是二分类、多分类或多标签分类;如果是预测问题,要区别是标量预测还是向量预测;其他方法类似。确定问题,明确目标有助于选择模型架构、损失函数及评估方法等。
当然,明确目标还包含需要了解目标的可行性,因为并不是所有问题都可以通过机器学习来解决。

5.2.2收集数据

目标明确后,接下来就是了解数据。为解决这个问题,需要哪些数据?数据是否充分?哪些数据能获取?哪些无法获取?这些数据是否包含我们学习的一些规则等等,都需要全面把握。
接下来就是收集数据,数据可能涉及不同平台、不同系统、不同部分、不同形式等,对这些问题的了解有助于确定具体数据收集方案、实施步骤等。
能收集的数据尽量实现自动化、程序化。

5.2.3 数据探索与预处理

收集到的数据,不一定规范和完整,这就需要对数据进行初步分析或探索,然后根据探索结果与问题目标,确定数据预处理方案。
对数据探索包括了解数据的大致结构、数据量、各特征的统计信息、整个数据质量情况、数据的分布情况等。为了更好体现数据分布情况,数据可视化是一个不错方法。
通过对数据探索后,可能发会现不少问题:如存在缺失数据、数据不规范、数据分布不均衡、存在奇异数据、有很多非数值数据、存在很多无关或不重要的数据等等。这些问题的存在直接影响数据质量,为此,数据预处理工作应该就是接下来的重点工作,数据预处理是机器学习过程中必不可少的重要步骤,特别是在生产环境中的机器学习,数据往往是原始、未加工和处理过,数据预处理常常占据整个机器学习过程的大部分时间。
数据预处理过程中,一般包括数据清理、数据转换、规范数据、特征选择等工作。

5.2.4 选择模型及损失函数

数据准备好以后,接下就是根据目标选择模型。模型选择上可以先用一个简单、自己比较熟悉的一些方法来实现,用这个方法开发一个原型或比基准更好一点的模型。通过这个简单模型有助于你快速了解整个项目的主要内容。
•了解整个项目的可行性、关键点
•了解数据质量、数据是否充分等
•为你开发一个更好模型奠定基础
在模型选择时,一般不存在某种对任何情况都表现很好的算法(这种现象又称为没有免费的午餐)。因此在实际选择时,一般会选用几种不同方法来训练模型,然后比较它们的性能,从中选择最优的那个。
模型选择后,还需要考虑以下几个关键点:
•最后一层是否需要添加softmax或sigmoid激活层
•选择合适损失函数
•选择合适的优化器
表5-1 列出了常见问题类型最后一层激活函数和损失函数的对应关系,供大家参考。
表5-1 根据问题类型选择损失函数

5.2.5 评估及优化模型

模型确定后,还需要确定一种评估模型性能的方法,即评估方法。评估方法大致有以下三种:
•留出法(Holdout):留出法的步骤相对简单,直接将数据集划分为两个互斥的集合,其中一个集合作为训练集,另一个作为测试。在训练集上训练出模型后,用测试集来评估测试误差,作为泛化误差的估计。使用留出法,还有一种更好的方法就是把数据分成三部分:训练数据集、验证数据集、测试数据集。训练数据集用来训练模型,验证数据集用来调优超参数,测试集用来测试模型的泛化能力。数据量较大时可采用这种方法。
•K折交叉验证:不重复地随机将训练数据集划分为k个,其中k-1个用于模型训练,剩余的一个用于测试。
•重复的K折交叉验证:当数据量比较小,数据分布不很均匀时可以采用这种方法。
使用训练数据构建模型后,通常使用测试数据对模型进行测试,测试模型对新数据的
测试。如果对模型的测试结果满意,就可以用此模型对以后的进行预测;如果测试结果不满意,可以优化模型。优化的方法很多,其中网格搜索参数是一种有效方法,当然我们也可以采用手工调节参数等方法。如果出现过拟合,尤其是回归类问题,可以考虑正则化方法来降低模型的泛化误差。

5.3 过拟合与欠拟合

前面我们介绍了机器学习的一般流程,模型确定后,开始训练模型,然后对模型进行评估和优化,这个过程往往是循环往复的。训练模型过程经常出现刚开始训练时,训练和测试精度不高(或损失值较大),通过增加迭代次数或优化,使得训练精度和测试精度继续提升的情况,如果出现这种情况,当然最好。但也会出现随着训练迭代次数增加或不模型断优化,训练精度或损失值继续改善,测试精度或损失值不降反升的情况。如图5-4 所示。

图5-4 训练误差与测试误差
出现这种情况时,说明我们的优化过头了,把训练数据中一些无关紧要甚至错误的模式也学到了。这就是我们通常说的出现过拟合了。如何解决这类问题?机器学习中有很多方法,这些方法又统称为正则化,接下来我们介绍一些常用的正则化方法。

5.3.1 权重正则化

如何解决过拟合问题呢?正则化是有效方法之一。正则化不仅可以有效降低高方差,还有利于降低偏差。何为正则化?在机器学习中,很多被显式地用来减少测试误差的策略,统称为正则化。正则化旨在减少泛化误差而不是训练误差。为使大家对正则化的作用及原理有个直观印象,先看正则化示意图(见图5-5)。
图5-5 正则化示意图
图5-5是根据房屋面积(Size)预测房价(Price)的回归模型。正则化是如何解决模型过复杂这个问题的呢?主要是通过正则化使参数变小甚至趋于原点。在图5-5c所示,其模型或目标函数是一个4次多项式,因它把一些噪声数据也包括进来了,导致模型很复杂,实际上房价与房屋面积应该是2次多项式函数,如图5-5b所示。
如果要降低模型的复杂度,可以通过缩减它们的系数来实现,如把第3次、4次项的系数\theta_3,\theta_4缩减到接近于0即可。
在算法中如何实现呢?这个得从其损失函数或目标函数着手。假设房屋价格与面积间模型的损失函数为:
\underset{\theta}{min}\frac{1}{2m}\sum_{i=1}^m (h_\theta (x^{(i)}) -y^{(i)})^2 \tag{5.1}
这个损失函数是我们的优化目标,也就是说我们需要尽量减少损失函数的均方误差。
对于这个函数我们对它添加一些正则项,如加上 10000乘以\theta_3 的平方,再加上 10000乘以\theta_4的平方,得到如下函数:
\underset{\theta}{min}\frac{1}{2m}\sum_{i=1}^m (h_\theta (x^{(i)}) -y^{(i)})^2+10000*\theta_{3}^2+10000*\theta_{4}^2 \tag{5.2}
这里取10000只是用来代表它是一个“大值”,现在,如果要最小化这个新的损失函数,我们要让\theta_3\theta_4尽可能小。因为如果你在原有损失函数的基础上加上 10000乘以\theta_3 这一项,那么这个新的损失函数将变得很大,所以,当最小化这个新的损失函数时,将使 \theta_3的值接近于 0,同样\theta_4的值也接近于 0,就像我们忽略了这两个值一样。如果做到这一点(\theta_3\theta_4 接近 0 ),那么将得到一个近似的二次函数。如图5-6所示。

图5-6利用正则化提升模型泛化能力
希望通过上面的简单介绍,能给大家有个直观理解。传统意义上的正则化一般分为L_0,L_1,L_2,L_{\infty}等。
PyTorch如何实现正则化呢?这里以实现L_2为例,神经网络的L_2正则化称为权重衰减(weight decay)。torch.optim集成了很多优化器,如SGD,Adadelta,Adam,Adagrad,RMSprop等,这些优化器自带的一个参数weight_decay,用于指定权值衰减率,相当于L2正则化中的\lambda参数,也就是式(5.3)中的\lambda
\underset{\theta}{min}\frac{1}{2m}\sum_{i=1}^m (h_\theta (x^{(i)} -y^{(i)}))^2 +\lambda ||w||^2 \tag{5.3}

5.3.2 dropout正则化

dropout是Srivastava等人在2014年的一篇论文中,提出的一种针对神经网络模型的正则化方法 “Dropout: A Simple Way to Prevent Neural Networks from Overfitting”。
dropout在训练模型中是如何实现的呢?dropout的做法是在训练过程中按一定比例(比例参数可设置)随机忽略或屏蔽一些神经元。这些神经元被随机“抛弃”,也就是说它们在正向传播过程中对于下游神经元的贡献效果暂时消失了,反向传播时该神经元也不会有任何权重的更新。所以,通过传播过程,dropout将产生和L_2范数相同的收缩权重的效果。
随着神经网络模型的不断学习,神经元的权值会与整个网络的上下文相匹配。神经元的权重针对某些特征进行调优,会产生一些特殊化。周围的神经元则会依赖于这种特殊化,如果过于特殊化,模型会因为对训练数据过拟合而变得脆弱不堪。神经元在训练过程中的这种依赖于上下文的现象被称为复杂的协同适应(complex co-adaptation)。
加入了dropout以后,输入的特征都有可能会被随机清除,所以该神经元不会再特别依赖于任何一个输入特征,也就是说不会给任何一个输入设置太大的权重。网络模型对神经元特定的权重不那么敏感,这反过来又提升了模型的泛化能力,不容易对训练数据过拟合。
dropout训练的集成包括所有从基础网络除去非输出单元形成子网络,如图5-7所示。


图 5-7基础网络dropout为多个子网络
dropout训练所有子网络组成的集合,其中子网络是从基本网络中删除非输出单元构建的。我们从具有两个可见单元和两个隐藏单元的基本网络开始,这4个单元有16个可能的子集。右图展示了从原始网络中丢弃不同的单元子集而形成的所有16个子网络。在这个例子中,所得到的大部分网络没有输入单元或没有从输入连接到输出的路径。当层较宽时,丢弃所有从输入到输出的可能路径的概率会变小,所以,这个问题对于层较宽的网络不是很重要。
较先进的神经网络一般包括一系列仿射变换和非线性变换,我们可以将一些单元的输出乘零,就能有效地删除一些单元。这个过程需要对模型进行一些修改,如径向基函数网络、单元的状态和参考值之间存在一定区别。为简单起见, 在这里提出乘零的简单dropout算法,被简单地修改后,可以与其他操作一起工作。
dropout在训练阶段和测试阶段是不同的,一般在训练中使用,测试不使用。不过测试时没有神经元被丢弃,此时有更多的单元被激活,为平衡起见,一般将输出按dropout rate比例缩小。
如何或何时使用dropout呢?以下是一般原则。
1)通常丢弃率控制在20%~50%比较好,可以从20%开始尝试。如果比例太低则起不到效果,比例太高则会导致模型的欠拟合。
2)在大的网络模型上应用。当dropout用在较大的网络模型时,更有可能得到效果的提升,模型有更多的机会学习到多种独立的表征。
3)在输入层和隐含层都使用dropout。对于不同的层,设置的keep_prob也不同,一般来说神经元较少的层,会设keep_prob为1.0或接近于1.0的数;神经元多的层,则会将keep_prob设置的较小,如0.5或更小。
4)增加学习速率和冲量。把学习速率扩大10~100倍,冲量值调高到0.9~0.99。
5)限制网络模型的权重。较大的学习速率往往易导致大的权重值。对网络的权重值做最大范数的正则化,被证明能提升模型性能。
以下我们通过实例来比较使用dropout和不使用dropout对训练损失或测试损失的影响。
数据还是房屋销售数据,构建网络层,添加两个dropout,具体构建网络代码如下:

获取测试集上不同损失值的代码如下:

通过TensorBoard在Web显示运行结果,如图5-8所示。

图5-8 dropout对测试损失值的影响
从图5-8 可以看出,添加dropout层,对提升模型的性能或泛化能力,效果还是比较明显的。

5.3.3 批量归一化

前面我们介绍了数据归一化,这个一般是针对输入数据而言。但在实际训练过程中,经常出现隐含层因数据分布不均,导致梯度消失或不起作用的情况。如采用sigmoid函数或tanh函数为激活函数时,如果数据分布在两侧,这些激活函数的导数就接近于0,这样一来,BP算法得到的梯度也就消失了。如何解决这个问题?
Sergey Ioffe和Christian Szegedy两位学者提出了批量归一化(Batch Normalization,BN)方法。批量归一化不仅可以有效解决梯度消失问题,而且还可以让调试超参数更加简单,在提高训练模型效率的同时,还可让神经网络模型更加“健壮”。批量归一化是如何做到这些的呢? 首先,我们介绍一下批量归一化的算法流程:

批量归一化是对隐含层的标准化处理,它与输入的标准化处理Normalizing inputs是有区别的。Normalizing inputs使所有输入的均值为0,方差为1,而批量归一化可使各隐含层输入的均值和方差为任意值。实际上,从激活函数的角度来说,如果各隐含层的输入均值在靠近0的区域,即处于激活函数的线性区域,这样不利于训练好的非线性神经网络,而且得到的模型效果也不会太好。式(5.6)就起这个作用,当然它还有将归一化后的 x 还原的功能。批量归一化一般用在哪里呢?批量归一化应作用在非线性映射前,即对x=Wu+b做规范化时,在每一个全连接和激励函数之间。
何时使用批量归一化呢?一般在神经网络训练时遇到收敛速度很慢,或梯度爆炸等无法训练的状况时,可以尝试用批量归一化来解决。另外,在一般情况下,也可以加入批量归一化来加快训练速度,提高模型精度,还可以大大提高训练模型的效率。批量归一化的具体功能有:
1)可以选择比较大的初始学习率,让训练速度飙涨。以前还需要慢慢调整学习率,甚至在网络训练到一半的时候,还需要想着学习率进一步调小的比例选择多少比较合适,现在我们可以采用初始很大的学习率,然后学习率的衰减速度也很大,因为这个算法收敛很快。当然,这个算法即使你选择了较小的学习率,也比以前的收敛速度快,因为它具有快速训练收敛的特性。
2)不用再去理会过拟合中drop out、L2正则项参数的选择问题,采用BN算法后,你可以移除这两项参数,或者可以选择更小的L2正则约束参数了,因为BN具有提高网络泛化能力的特性。
3)再也不需要使用局部响应归一化层。
4)可以把训练数据彻底打乱。
下面还是以房价预测为例,比较添加BN层与不添加BN层,两者在测试集上的损失值比较。下例为两者网络结构代码。

图5-9 为运行结果图。

图5-9 BN层对测试数据的影响
从图5-9 可以看出,添加BN层对改善模型的泛化能力有一定帮助,不过没有dropout那么明显。这个神经网络比较简单,BN在一些复杂网络中的效果会更好。
BN方法对批量大小(batchsize)比较敏感,由于每次计算均值和方差是单个节点的一个批次上,所以如果批量大小太小,则计算的均值、方差不足以代表整个数据分布。BN实际使用时需要计算并且保存某一层神经网络批次的均值和方差等统计信息,对于对一个固定深度的正向神经网络(如深度神经网络(DNN)、卷积神经网络(CNN)),使用BN很方便。但对于深度不固定的神经网络,如循环神经网络(RNN)来说,BN的计算很麻烦,而且效果也不很理想。此外,BN算法对训练和测试两个阶段不一致,也会影响模型的泛化效果。对于不定长的神经网络人们想到另一种方法,即层归一化(Layer Normalization,LN)算法。

5.3.4 层归一化

层归一化对同一层的每个样本进行正则化,不依赖于其他数据,因此可以避免 BN 中受小批量数据分布影响的问题。不同的输入样本有不同的均值和方差,它比较适合于样本是不定长或网络深度不固定的场景,如RNN、NLP等方面。
BN是纵向计算,而LN是横向计算,另外BN是对单个节点(或特征)的一个批次进行计算,而LN是基于同一层不同节点(或不同特征)的一个样本进行计算。两者之间的区别可用图5-10直观表示。

图5-10 BN与LN的异同

5.3.5权重初始化

深度学习为何要初始化?传统机器学习算法中很多不是采用迭代式优化,因此需要初始化的内容不多。但深度学习的算法一般采用迭代方法,而且参数多、层数也多,所以很多算法不同程度受到初始化的影响。
初始化对训练有哪些影响?初始化能决定算法是否收敛,如果初始化不适当,初始值过大可能会在正向传播或反向传播中产生爆炸的值;如果太小将导致丢失信息。对收敛的算法适当的初始化能加快收敛速度。初始值选择将影响模型收敛局部最小值还是全局最小值,如图5-11所示,因初始值的不同,导致收敛到不同的极值点。另外,初始化也可以影响模型的泛化。

图5-11 初始点的选择影响算法是否陷入局部最小点
如何对权重、偏移量进行初始化?初始化这些参数是否有一般性原则?常见的参数初始化有零值初始化、随机初始化、均匀分布初始、正态分布初始和正交分布初始等。一般采用正态分布或均匀分布的初始值,实践表明正态分布、正交分布、均匀分布的初始值能带来更好的效果。
继承nn.Module的模块参数都采取了较合理的初始化策略,一般情况使用其默认初始化策略就够了。当然,如果你要想修改,PyTorch也提供了nn.init模块,该模块提供了常用的初始化策略,如xavier、kaiming等经典初始化策略,使用这些初始化策略有利于激活值的分布呈现更有广度或更贴近正态分布。xavier一般用于激活函数是S型(如sigmoid、tanh)的权重初始化,kaiming更适合与激活函数为ReLU类的权重初始化。

第6章 机器学习基础

在本书的第一部分,我们介绍了NumPy、TensorFlow基础等内容,这些内容是继续学习TensorFlow的基础。在第二部分,我们将介绍深度学习的一些基本内容,以及如何用TensorFlow解决机器学习、深度学习的一些实际问题。
深度学习是机器学习的重要分支,也是机器学习的核心,它是在机器学习的基础上发展起来的,因此在了解深度学习之前,理解机器学习的基本概念、基本原理会对对理解深度学习大有裨益。
机器学习的体系很庞大,限于篇幅,本章主要介绍基本知识及其与深度学习关系比较密切的内容,如果读者希望进一步学习机器学习的相关知识,建议参考周志华老师的《机器学习》或李航老师的《统计学习方法》。
本章先介绍机器学习中常用的监督学习、无监督学习等,然后介绍神经网络及相关算法,最后介绍传统机器学习中的一些不足及优化方法等,主要内容如下:
♦机器学习的一般流程
♦监督学习
♦无监督学习
♦机器学习实例

6.1 机器学习的一般流程

机器学习一般流程包括明确目标、收集数据、探索数据、预处理数据,对数据处理后,接下来开始构建和训练模型、评估模型,然后优化模型等步骤,图6-1为机器学习一般流程图。
图6-1 机器学习一般流程图
通过这个图形可直观了解机器学习的一般步骤或整体架构,接下来我们就各部分分别加以说明。

6.1.1 明确目标

在实施一个机器学习项目之初,定义需求、明确目标、了解要解决的问题以及目标涉及的范围等非常重要,它们直接影响后续工作的质量甚至成败。明确目标,首先需要明确大方向,比如当前需求是分类问题还是预测问题或聚类问题等。清楚大方向后,需要进一步明确目标的具体含义。如果是分类问题,还需要区分是二分类、多分类或多标签分类;如果是预测问题,要区别是标量预测还是向量预测;其他方法类似。确定问题,明确目标有助于选择模型架构、损失函数及评估方法等。
当然,明确目标还包含需要了解目标的可行性,因为并不是所有问题都可以通过机器学习来解决。

6.1.2 收集数据

目标明确后,接下来就是了解数据。为解决这个问题,需要哪些数据?数据是否充分?哪些数据能获取?哪些无法获取?这些数据是否包含我们学习的一些规则等等,都需要全面把握。
接下来就是收集数据,数据可能涉及不同平台、不同系统、不同部分、不同形式等,对这些问题的了解有助于确定具体数据收集方案、实施步骤等。
能收集的数据尽量实现自动化、程序化。

6.1.3 数据探索与预处理

收集到的数据,不一定规范和完整,这就需要对数据进行初步分析或探索,然后根据探索结果与问题目标,确定数据预处理方案。
对数据探索包括了解数据的大致结构、数据量、各特征的统计信息、整个数据质量情况、数据的分布情况等。为了更好体现数据分布情况,数据可视化是一个不错方法。
通过对数据探索后,可能发会现不少问题:如存在缺失数据、数据不规范、数据分布不均衡、存在奇异数据、有很多非数值数据、存在很多无关或不重要的数据等等。这些问题的存在直接影响数据质量,为此,数据预处理工作应该就是接下来的重点工作,数据预处理是机器学习过程中必不可少的重要步骤,特别是在生产环境中的机器学习,数据往往是原始、未加工和处理过,数据预处理常常占据整个机器学习过程的大部分时间。
数据预处理过程中,一般包括数据清理、数据转换、规范数据、特征选择等工作。

6.1.4 模型选择

数据准备好以后,接下就是根据目标选择模型。模型选择上可以先用一个简单、自己比较熟悉的一些方法来实现一个原型或比基准更好一点的模型。通过这个简单模型有助于你快速了解整个项目的主要内容。
•了解整个项目的可行性、关键点。
•了解数据质量、数据是否充分等。
•为你开发一个更好模型奠定基础。
在进行模型选择时,一般不存在某种对任何情况都表现很好的算法(这种现象又称为没有免费的午餐)。因此在实际选择时,一般会选用几种不同方法来训练模型,然后比较它们的性能,从中选择最优的那个。
模型选择好后,还需要考虑以下几个关键点:
•最后一层是否需要添加softmax或sigmoid激活层;
•选择合适损失函数。
表6-1 列出了常见问题类型最后一层激活函数和损失函数的对应关系,供大家参考。
表6-1 根据问题类型选择损失函数

问题类型 最后一层激活函数 损失函数
回归模型 tf.losses.(或tf.keras.losses.)

mean_squared_error(函数形式,简写为 mse)

MeanSquaredError(类形式,简写为MSE)

SVM hinge
二分类模型 binary_crossentropy(函数形式)

BinaryCrossentropy(类形式)

多分类模型 label是类别序号编码 categorical_crossentropy
label进行了独热编码 sparse_categorical_crossentropy
自定义损失函数 自定义损失函数接收两个张量y_true,y_pred作为输入参数,并输出一个标量作为损失函数值; 也继承tf.keras.losses.Loss类,重写call方法实现损失的计算逻辑,从而得到损失函数的类的实现。

6.1.5 模型评估

模型确定后,还需要确定一种评估模型性能的方法,即评估方法。评估方法大致有以下三种:
•留出法(holdout):留出法的步骤相对简单,直接将数据集划分为两个互斥的集合,其中一个集合作为训练集,另一个作为测试。在训练集上训练出模型后,用测试集来评估测试误差,作为泛化误差的估计。使用留出法,还有一种更好的方法就是把数据分成三部分:训练数据集、验证数据集、测试数据集。训练数据集用来训练模型,验证数据集用来调优超参数,测试集用来测试模型的泛化能力。数据量较大时可采用这种方法,整个过程可用图6-2表示。

图6-2 留出法实施步骤
•K折交叉验证:不重复地随机将训练数据集划分为k个,其中k-1个用于模型训练,剩余的一个用于测试,具体如图6-3所示。

图6-3 K折交叉验证法的实施步骤
•重复的K折交叉验证:当数据量比较小,数据分布不很均匀时可以采用这种方法。
使用训练数据构建模型后,通常使用测试数据对模型进行测试,测试模型对新数据的
测试。如果对模型的测试结果满意,就可以用此模型对以后的进行预测;如果测试结果不满意,可以优化模型。优化的方法很多,其中网格搜索参数是一种有效方法,当然我们也可以采用手工调节参数等方法。如果出现过拟合,尤其是回归类问题,可以考虑正则化方法来降低模型的泛化误差。

6.1.6 评估指标

评估指标及其概述如表6-2所示。
表6-2 评估指标概述

问题类型 最后一层激活函数 评估指标
回归模型 tf.losses.(或tf.keras.losses.)

mean_squared_error(函数形式,简写为 mse)

MeanSquaredError(类形式,简写为MSE)

二分类模型  

 

 

要求y_true(label)为onehot编码形式

要求y_true(label)为序号编码形式

Accuracy (准确率)

Precision (精确率)

Recall (召回率)

CategoricalAccuracy(分类准确率,与Accuracy含义相同

SparseCategoricalAccuracy (稀疏分类准确率,与Accuracy含义相同

多分类模型 要求y_true(label)为onehot编码形式 TopKCategoricalAccuracy (多分类TopK准确率)
要求y_true(label)为序号编码形式 SparseTopKCategoricalAccuracy (稀疏多分类TopK准确率)

6.2 监督学习

机器学习大致可分为监督学习(Supervised learning)、无监督学习(Unsupervised learning)和半监督学习(Semi-supervised learning)。这节主要介绍监督学习有关算法。
监督学习的数据集一般含有很多特征或属性,数据集中的样本都有对应标签或目标值。监督学习的任务就是根据这些标签,学习调整分类器的参数,使其达到所要求性能的过程。简单来说,就是由已知推出未知。监督学习过程如图6-4所示。

图6-4 监督学习过程

6.2.1 线性回归

线性回归是线性模型中的一种,在介绍线性回归之前,我们先简单介绍一下线性模型。线性模型是监督学习中比较简单的一种模型虽然简单,但却非常有代表性,而且是线性模型其他算法的很好入口。
线性模型的任务是:在给定样本数据集上(假设该数据集特征数为n),学习得到一个模型或一个函数f(z),使得对任意输入特征向量X=\left(x_1,x_2,\cdots,x_n \right)^T,f(z)能表示为X的线性函数,即满足:
z=w_1 x_1+w_2 x_2+\cdots+w_n x_n+b ,则有:
f(z)=f(w_1 x_1+w_2 x_2+\cdots+w_n x_n+b) \tag{6.1}
其中w_i \left(1\le i\le n\right),b为模型参数,这些参数需要在训练过程学习或确定。
把式(6.1)写成矩阵的形式:
(z)=f(W^T X+b) \tag{6.2}
其中 W=(w_1,w_2,\cdots,w_n)^T
线性模型可以用于分类、回归等学习任务,具体包括线性回归、逻辑回归等算法,另外我们从式(6.2)可以看出,它与单层神经网络(又称为感知机,如图6-5)的表达式一致,因此,人们往往也把单层神经网络纳入线性模型范围中。单层神经网络属于神经网络,具体将在本书第7章介绍。

图6-5单层单个神经元
回归一般用于预测,当然也可用于分类,下节的逻辑回归就是用于分类任务。线性回归是回归学习中的一种,其任务就是在给定的数据集D中,通过学习得到一个线性模型或线性函数f(x),使得数据集与函数f(x)之间具有式(6.1)的关系,如果把这种函数关系可视化就是图6-6。

图6-6 线性回归示意图
图6-6中这条直线是如何训练出来的呢?要画出这条直线(f(x)=wx+b),就需要知道直线的两个参数:w和b。如何求w和b?那就需要用到所有的已知条件:样本(含x和目标值或标签),样本用字符可表示为:{(x^1,y^1),(x^2,y^2),\cdots,(x^m,y^m)}
这里假设共有m个样本,每个样本由输入特征向量x^i和目标值y^i构成,其中x^i 一般是向量,如x^i=(x_1,x_2,\cdots,x_n )^T,y^i是目标值(又称为实际值或标签),
它是一个实数,即y\in R。为简单起见,我们假设x^i是一维的,所以样本就是图6-6中的各个点。
根据这些点,如何拟合预测函数或直线:f(x)=wx+b 呢?通过这些点,可以画出很多类似的直线,在这些直线中哪条直线最能反应这些样本的特点呢?
这里就涉及一个衡量标准问题,通常用一个代价函数来表示:
L(w,b)=\frac {1}{2}\sum_{i=1}^m (f(x^i)-y^i)^2 \tag{6.3}
式(6.3)直观的解释就是,这m个样本点的预测值f(x^i)与实际值y^i的距离最小。满足这个条件的直线应该就是最好的。
f(x^i)=wx^i+b,把它代入式(6.3)就是:
L(w,b)=\frac{1}{2}\sum_{i=1}^m ((wx^i+b)-y^i)^2 \tag{6.4}
所以上述拟合直线问题,就转换为求代价函数L(w,b)的最小值问题。求解式(6.4)的最小值问题,我们可以使用:
1)利用迭代法,每次迭代沿梯度的反方向(如图6-7),逐步靠近或收敛最小值点;
2)利用最小二乘法,直接求出参数w,b。
我们通常采用第一种方法,具体实现请参考6.5节,第二种方法计算量比较大而且复杂。这里就不再展开。

图6-7梯度下降法

6.2.2 逻辑回归

上节我们介绍了线性回归,利用线性回归来拟合一条直线,这条直线就是一个函数或一个模型,根据这个函数我们就可以对新输入数据x进行预测,即根据输入x,预测其输出值y。这是一个典型的回归问题。线性模型除了用于回归,也可用于分类。最常用的方法就是逻辑回归(Logistic Regression)。分类顾名思义就是根据数据集的特点划分成几类,其输出为有限的离散值,如{A,B,C}、{是,否}、{0,1}等。图6-8为逻辑回归分类可视化示意图。

图6-8逻辑回归分类
其任务就是在数据集D={若干圆点、若干小方块}中,找出一条直线或曲线,把这两类点区分开。划分结果,在直线一边尽可能为同一类的点,如圆点,在直线另一边尽可能是另一类的点,如小方块,如图6-8所示。目标明确以后,如何求出拟合分类直线或这条直线的表达式呢?当然这条直线的表达式不像式(6.1),其输出结果最好为是或否,或为0或1,其表达式为:
f(z)=\begin{cases} 0,&z=w^T x+b\le 0 \\ 1,&z= w^T x+b\gt 0 \end{cases}\tag{6.5}
其中w^T x+b=0称为划分边界。
式(6.5)虽然结果很完备,如果能这样当然最好,不过因这个函数不连续,所以我们一般转向次优的方案,采用sigmoid函数:
f(z)=\frac{1}{1+e^{-z}} \tag{6.6}
其对应的函数曲线如图6-9所示。
图6-9 sigmoid函数曲线
该函数将输出数据压缩为0到1之间的范围内,这也是概率取值范围,如此便可将分类问题转换为一个概率问题来处理。把z=w^T x+b代入式(6.6)便可得到逻辑回归的预测函数:
 f(z)=\frac{1}{1+e^{-(w^T x+b)}} \tag{6.7}
对于二分类问题,我们可以用预测y=1或0的概率表示为:
p(y=1|x;w,b)=f(z) \tag{6.8}
p(y=0|x;w,b)=1-f(z) \tag{6.9}
模型的函数表达式确定后,剩下就是如何去求解模型中的参数(如w、b)。与线性回归一样,要确定参数,需要使用代价函数。逻辑回归属于分类问题,此时就不宜用式(6.3)的代价函数。
如果还用式(6.3)为代价函数,这样我们会发现L(w,b)为非凸函数,此时就存在很多局部极值点,就无法用梯度迭代得到最终的参数,因此分类问题通常采用对数最大似然作为代价函数:
\begin{aligned}L(w,b)&=log(\prod_{i=1}^m p(y^i|x^i;w,b))\\&=\sum_{i=1}^m log(p(y^i|x^i;w,b))\end{aligned}\tag{6.10}
这节我们介绍了线性模型中线性回归和逻辑回归及其简单应用。分类中使用的数据集比较理想的线性数据集。但实际上生活中很多数据集是非线性数据集,对非线性数据集我们该如何划分呢?下节我们将介绍一种强大的分类器——支持向量机(SVM),它不但可以处理线性数据集,也可以处理非线性数据集。

6.2.3 树回归

前面我们介绍了线性回归、逻辑回归等,这节将介绍树回归。在介绍树回归之前,先简单介绍一下决策树。决策树一般用来做分类,但也有一些决策树可用来做回归预测,如CART决策树,有些场景用决策树做回归效果还更好。如图6-10,把决策树转换为回归模型示意图。

图6-10 把树形结构转换为回归模型示意图
图6-11对比了使用线性回归与树回归的可视化效果,在该场景,树回归的性能高于线性回归的性能。

图6-11线性回归与树回归的可视化效果比较

6.2.4 支持向量机

支持向量机(Support Vector Machine,SVM),在处理线性数据集、非线性数据集时都有较好效果。在机器学习或者模式识别领域可谓无人不知,无人不晓。在20世纪八九十年代,支持向量机曾和神经网络一决雌雄,独领风骚几十年,甚至有人把神经网络八九十年代的再度沉寂归功于它。
它的强大或神奇与它采用的相关技术不无关系,如最大类间隔、松弛变量、核函数等等,使用这些技术使其在众多机器学习方法中脱颖而出。即使几十年过去了,仍风采依旧,究其原因,与其高效、简洁、易用的特点分不开。其中一些处理思想与当今深度学习技术有很大关系,如使用核方法解决非线性数据集分类问题的思路,类似于带隐含层的神经网络,可以说两者有同工异曲之妙。
用支持向量机进行分类,目的与逻辑回归类似得到一个分类器或分类模型,不过它的分类器是一个超平面(如果数据集是二维,这个超平面就是直线,三维数据集,就是平面,以此类推),这个超平面把样本一分为二,当然,这种划分不是简单划分,需要使正例和反例之间的间隔最大。间隔最大,其泛化能力就最强。如何得到这样一个超平面?下面我们通过一个二维空间例子来说明。
1.最优间隔分类器
SVM的分类器为超平面,何为超平面?哪种超平面是我们所需要的?我们先看图6-12的几个超平面或直线。

图6-12 SVM 超平面
如何获取最大化分类间隔?分类算法的优化目标通常是最小化分类误差,但对SVM而言,优化的目标是最大化分类间隔。所谓间隔是指两个分离的超平面间的距离,其中在超平面H1和H2上的训练样本又称为支持向量(support vector)。
假设一个二维空间的数据集分布如图6-13所示(图中的样本点有些是圆点,有些是方块)。

图6-13 支持向量机样本点
其中,H1:W^T X+b=1;H2:W^T X+b=-1 ;H:W^T X+b=0
W=[w1,w2],X=[x1,x2] or [x,y]
接下来我们需要用这些样本去训练学习一个线性分类器(超平面),这里就是直线H:f(x)=sgn(W^Tx + b)
也就是W^Tx + b大于0时,输出+1,小于0时,输出-1,其中sgn()表示取符号。而g(x) =W^Tx + b=0就是我们要寻找的分类超平面(即直线H),
如上图6-8所示。我们需要这个超平面尽可能分隔这两类,即这个分类面到这两个类的最近的那些样本的距离相同,
而且最大。为了更好的说明这个问题,假设我们在上图6-8中找到了两个和这个超平面并行且距离相等的超平面:H1:y = W^Tx + b=+1H2:y = W^Tx + b=-1
这时候我们就需要两个条件:
1)没有任何样本在这两个平面之间;
2)这两个平面的距离需要最大。
有了超平面以后,我们就可以对数据集进行划分。不过还有一个关键问题,如何把非线性数据集转换为线性数据集?这就是下节要介绍的内容,利用核函数技术,把非线性数据集映射到一个更高或无穷维的空间,在新空间转换为线性数据集。
2.核函数
核函数如何把线性不可分数据集转换为线性可分数据集呢?为了给大家一个直观的认识,我们先来看图6-14,直观感受一下SVM的核威力。

图6-14 SVM 核函数数据集映射
图6-14左边为一个线性不可分数据集,中间为一个核函数\phi
\phi(x1,x2)=(z1,z2,z3)=(x1,x2,{x1}^2+{x2}^2) \tag{6.11}
其功能就是将左边在二维空间线性不可分的数据集,映射到三维空间后变成了一个线性可分数据集,其中超平面可以把数据分成上下两部分。这里使用的核为一个多项式核:k(x,y)=(x^T y+c)^d。除了用多项式核,还可以用其他核,如:
•径向基核函数(Radial Basis Function),又称为高斯核:
k(x,y)=exp ?(-\frac{||x-y||^2}{2\delta^2 }) \tag{6.12}
•sigmoid核
 k(x,y)=tanh(\alpha x^T+c) \tag{6.13}
SVM添加核函数,就相当于在神经网络中添加了隐含层,所以SVM曾经风靡一时。但目前已被神经网络,尤其是深度学习所超越,原因很多,SVM核函数比较难选择是重要原因。此外,灵活性、扩展性也不如神经网络。神经网络将在第7章介绍。

6.2.5 朴素贝叶斯分类器

概率论是许多机器学习算法的基础,所以熟悉这一主题非常重要。朴素贝叶斯是一种基于概率论来构建分类器的经典方法。
一些概率模型,在监督学习的样本集中能获得非常好的分类效果。在许多实际应用中,朴素贝叶斯模型参数估计使用最大似然估计方法。
1.极大似然估计
概率模型的训练过程就是一个参数估计过程,对于参数估计,在统计学界有两个派别,即频率派和贝叶斯派,提出了不同的思想和解决方案,这些方法各有自己的特点和理论依据。频率派认为参数虽然未知,但却是固定的,因此,可以通过优化似然函数等准则来确定;贝叶斯派认为参数是随机值,因为没有观察到,与一个随机数也没有什么区别,因此参数可以有分布,也就是说,可以假设参数服从一个先验分布,然后基于观察到的数据计算参数的后验分布。
本节介绍的极大似然估计(Maximum Likelihood Estimate,MLE)属于频率派。如何求解极大似然估计呢?以下为求解MLE的一般过程:
1) 写出似然函数。
假设我们有一组含m个样本数据集X={x^{(1)},x^{(2)},\cdots,x^{(m)}},由分布P(x;θ)独立生成,则参数θ对于数据集X的似然函数为:
\prod_{i=1}^m p(x^{(i)};\theta)\tag{6.14}
概率乘积不方便计算,而且连乘容易导致数值下溢,为了得到一个便于计算的等价问题,通常使用对数似然。
2) 对似然函数取对数,整理得:
log\prod_{i=1}^m p(x^{(i)};\theta)=\sum_{i=1}^m logp(x^{(i)};\theta) \tag{6.15}
3) 利用梯度下降法,求解参数θ。根据式(6.15),极大似然估计可表示为: \hat{\theta}=\underset{\theta}{argmax}\sum_{i=1}^m logp(x^{(i)};\theta)\tag{6.16}
2.朴素贝叶斯分类器
前面我们介绍了利用逻辑回归(LR)、支持向量机(SVM)进行分类,这里我们介绍一种新的分类方法。这种方法基于贝叶斯定理,同时假设样本的各特征之间是独立且互不影响,这就是朴素贝叶斯分类器,假设特征互相独立是称其为“朴素”的重要原因。
利用LR或SVM进行分类的一般步骤为:
1)定义模型函数:y=f(x)或p(y|x)。
2)定义代价函数。
3)利用梯度下降方法,求出模型函数中的参数。
如果我们用朴素贝叶斯分类器进行分类,其步骤是否与LR或SVM的一样呢?要回答这个问题,我们首先看一下朴素贝叶斯分类器的主要思想。
假设给定输入数据x及类别c,在给定输入数据x的条件下,求属于类别c的概率p(c|x),那么朴素贝叶斯公式可表示为:
p(c|x)=\frac{p(x|c)p(c)}{p(x)}\tag{6.17}
朴素贝叶斯分类的基本思想就是,通过求取p(c)和p(x|c)来求p(c|x),而不是直接求p(c|x),利用式(6.17)求最优分类,p(x)对所有类别都是相同的。
输入数据x一般含有多个特征或多个属性,假设有n个特征,即x=(x_1,x_2,\cdots,x_n) ,那么计算p(x|c)比较麻烦,需要计算在条件c下的联合分布。好在朴素贝叶斯有“属性条件独立性”这个假设,计算p(x|c)时就方便多了。p(x|c)就可以写成:
p(x|c)=p((x_1,x_2,\cdots,x_n)|c)=\prod_{i=1}^n p(x_i |c)\tag{6.18}
假设类别集合为Y,p(x)对所有类别都相同,因此输入数据x条件下,最优分类就可表示为:
\underset {c\in Y}{argmax} p(c)\prod_{i=1}^n p(x_i |c)\tag{6.19}
由此,我们可以看出,利用贝叶斯分类器进行分类,不是首先定义P(c|x),而是先求出P(x|c),然后求出不同分类下的最优值,期间不涉及求代价函数。

6.2.6 集成学习

集成学习(Aggregation)为什么能起到1+1>2的效果?集成学习的原理与盲人摸象这个故事揭示的道理类似,即综合多个盲人的观点就能得到一个更全面的观点。
我们知道艺术来源于生活,又高于生活,实际上很多算法也是如此。在介绍集成学习之前,我们先看一下生活中的集成学习。假如你有m个朋友,每个朋友向你推荐明天某支股票是涨还是跌,对应的建议分别是t_1,t_2,⋯,t_m。那么你该选择哪个朋友的建议呢?你可能会采用如下几种方法。
•第一种方法,是从m个朋友中选择一个最受信任,对股票预测能力最强的,直接听从其建议就好。这是一种普遍的做法。
•第二种方法,如果每个朋友在股票预测方面都是比较厉害的,都有各自的专长,那么就同时考虑m个朋友的建议,将所有结果做个投票,一人一票,最终决定出对该支股票的预测。
•第三种方法,如果每个朋友水平不一,有的比较厉害,投票比重应该更大一些;有的比较差,投票比重应该更小一些。那么,仍然对m个朋友进行投票,只是每个人的投票权重不同。
•第四种方法与第三种方法类似,但是权重不是固定的,根据不同的条件,给予不同的权重。比如,如果是传统行业的股票,那么给这方面比较厉害的朋友较高的投票权重,如果是服务行业,那么就给这方面比较厉害的朋友较高的投票权重。
上述四种方法都是将不同人不同意见融合起来的方式,接下来我们来讨论如何将这些做法对应到机器学习中去。集成学习的思想与这个例子类似,即把多个人的想法结合起来,以得到一个更好、更全面的想法。
集成学习的主要思想:对于一个比较复杂的任务,综合许多人的意见来进行决策往往比一家独大好,正所谓集思广益。其过程如图6-15所示。
图6-15集成学习示例
接下来介绍两种典型的集成学习:装袋(Bagging)算法和提升分类器(Boosting)。
1. Bagging
Bagging即套袋法,其算法过程如下。
1) 从原始样本集中抽取训练集。每轮从原始样本集中使用Bootstraping的方法抽取n个训练样本。Bootstrapping算法是指利用有限的样本经由多次重复抽样,重新建立起足以代表母体样本分布之新样本。在训练集中,有些样本可能被多次抽取到,而有些样本可能一次都没有被抽中。共进行k轮抽取,得到k个训练集。这k个训练集之间是相互独立的。
2)每次使用一个训练集得到一个模型,k个训练集共得到k个模型。训练时我们可以根据具体问题采用不同的分类或回归方法,如决策树、单层神经网络等。
3)针对分类问题:将上步得到的k个模型采用投票的方式得到分类结果;针对回归问题,计算上述模型的均值并将其作为最后的结果。
随机森林是Bagging算法的典型代表,在随机森林中,每个树模型都是装袋采样训练的。另外,特征也是随机选择的,最后对于训练好的树也是随机选择的。
这种处理的结果是随机森林的偏差增加的很少,而由于对弱相关树模型的结果进行平均,方差也得以降低,最终得到一个方差小,偏差也小的模型。
2. Boosting
Boosting是指通过算法集合将弱学习器转换为强学习器。Boosting的主要原则是训练一系列的弱学习器,所谓弱学习器是指仅比随机猜测好一点点的模型,例如较小的决策树,训练的方式是利用加权的数据。在训练的早期对于错分数据给予较大的权重。
对于训练好的弱分类器,如果是分类任务按照权重进行投票,而对于回归任务进行加权,然后再进行预测。boosting和bagging的区别在于是对加权后的数据利用弱分类器依次进行训练。
Boosting是一族可将弱学习器提升为强学习器的算法,这族算法的工作机制类似,分析如下:
•先从初始训练集训练出一个基学习器;
•再根据基学习器的表现对训练样本分布进行调整,使得先前基学习器做错的训练样本在后续受到更多关注;
•基于调整后的样本分布来训练下一个基学习器;
•重复进行上述步骤,直至基学习器数目达到事先指定的值T,最终将这T个基学习器进行加权结合。
下面我们通过一些图形来说明。
1)假设我们有如下样本,如图6-16所示。

图6-16 最初样本图
2)对以上数据集进行替代式分类。第1次分类,得到以下划分,如图16-17所示。

图6-17 第1次划分后的样本图
第2次分类,得到以下划分,如图6-18所示。

图6-18 第2次划分后的样本图
在图6-18中被正确测的点有较小的权重(尺寸较小),而被预测错误的点(+)则有较大的权重(尺寸较大)。
第3次分类,得到以下划分,如同6-19所示。

图6-19 第3次划分后的样本图
在图6-19中被正确测的点有较小的权重(尺寸较小),而被预测错误的点(-)则有较大的权重(尺寸较大)。
第4次综合以上分类,得到最后的分类结果。如图6-20所示。

图6-20 最后的分类结果
基于Boosting思想的算法主要有AdaBoost、GBDT、XGBoost等。

6.3 无监督学习

上节我们介绍了监督学习,监督学习的输入数据中有标签或目标值,但在实际生活中,有很多数据是没有标签的,或者标签代价很高,对这些没有标签的数据该如何学习呢?这类问题可用机器学习中的无监督学习。在无监督学习中,我们通过推断输入数据中的结构来建模。如通过提取一般规律,或通过数学处理系统地减少冗余,或者根据相似性组织数据等,这分别对应无监督学习的关联学习、降维、聚类。无监督学习方法很多,限于篇幅我们这里只介绍两种典型的无监督算法:主成分分析与k均值(k-means)算法。

6.3.1主成分分析

主成分分析(Principal Components Analysis,PCA)是一种数据降维技术,可用于数据预处理。如果我们获取的原始数据维度很大,比如1000个特征,在这1000个特征中可能包含了很多无用的信息或者噪声,真正有用的特征可能只有50个或更少,那么我们可以运用PCA算法将1000个特征降到50个特征。这样不仅可以去除无用的噪声,节省计算资源,还能保持模型性能变化不大。如何实现PCA算法呢?
为便于理解,我们从直观上来理解就是,将数据从原多维特征空间转换到新的维数更低的特征空间中。例如原始的空间是三维的(x,y,z),x、y、z分别是原始空间的三个基,我们可以通过某种方法,用新的坐标系(a,b,c)来表示原始的数据,那么a、b、c就是新的基,它们组成新的特征空间。在新的特征空间中,可能所有的数据在c上的投影都接近于0,即可以忽略,那么我们就可以直接用(a,b)来表示数据,这样数据就从三维的(x,y,z)降到了二维的(a,b)。如何求新的基(a,b,c)?以下介绍求新基的一般步骤:
1)对原始数据集做标准化处理;
2)求协方差矩阵;
3)计算协方差矩阵的特征值和特征向量;
4)选择前k个最大的特征向量,k小于原数据集维度;
5)通过前k个特征向量组成了新的特征空间,设为W;
6)通过矩阵W,把原数据转换到新的k维特征子空间。

6.3.2 k均值算法

前面我们介绍了分类,那聚类与分类有何区别呢?分类是根据一些给定的已知类别标识的样本,训练模型,使它能够对未知类别的样本进行分类。这属于监督学习。而聚类指事先并不知道任何样本的类别标识,希望通过某种算法来把一组未知类别的样本划分成若干类别,它属于无监督学习。K均值算法就是典型的聚类算法。那么k均值算法如何实现聚类呢?它的基本思想是:
1)适当选择k个类的初始中心;
2)在第i次迭代中,对任意一个样本,求其到K个中心的距离,将该样本归到距离最短的中心所在的类。
3)利用均值等方法更新该类的中心值;
4)对于所有的K个聚类中心,如果利用(2)(3)的迭代法更新后,值保持不变,则迭代结束,否则继续迭代。
5)对同一类簇中的对象相似度极高,不同类簇中的数据相似度极低。
图6-21为聚类结果示意图。

图6-21 k-means聚类示意图
对k均值算法,如果原数据很多,其计算量非常大,对于大数据是否有更好的方法呢?每次处理聚类算法时,采用少批量而不是所有数据进行训练,Scikit-learn提供的小批量k均值(Mini Batch K-Means)算法,这种方法非常高效,在深度学习计算梯度下降时,经常采用类似方法,称为随机梯度下降法,具体内容第7章将介绍。

6.4 数据预处理

特征工程是机器学习任务中一项重要内容,而数据预处理又是特征工程的核心内容,本节将介绍基于Python、Pandas及Sklearn的几种数据预处理工具。
首先,我们简单介绍机器学习中经常出现的特征工程中数据预处理,然后,用葡萄酒数据集实现一个完整的机器学习任务。

6.4.1 处理缺失值

机器学习中经常遇到缺失值的情况,遇到缺失数据,如果简单的删除,可能因此删除一些有用信息,更重要的可能删除数据的历史轨迹。所以,我们往往采用补填方式进行处理。补填的方法很多,如用0补填,用所在列的平均数、中位数、自定义数等补填,或缺失值的相邻项进行补填等。以下说明如何用所在列的平均数来补填缺失数据。

结果显示:

查看有缺失值的个数及对应列。

A 0
B 0
C 1
D 1
dtype: int64
说明C、D列各有一个缺失值。用缺失值所在列的平均值补填。

运行结果:
array([[ 1. , 2. , 3. , 4. ],
[ 5. , 6. , 7.5, 8. ],
[ 10. , 11. , 12. , 6. ]])

6.4.2 处理分类数据

机器学习的数据集中往往包含很多分类数据,其中有些是有序的(即有大小的区别,如衣服的型号,M<X<XL),有些是无序的(即没有大小的区别,如表示颜色的类别等)。对这些类别的如果处理不当,进行影响模型性能,尤其涉及几何距离方面的机器学习任务中。为此,我们一般把有序类别直接转换成整数,而把无序类别转换为独热编码。如果把无序类别也转换为整数,如红色转换为1,黄色转换为2,蓝色转换为3等,这样就给颜色类别赋予大小的含义了,实际上颜色应该更大小无关,从而破坏其本来属性。

运行结果

把有序特征型号变为整数

运行结果

然后把无序特征颜色变为one-hot编码。

运行结果

主要颜色由1列变为3列,这3列中只有一个1,其余都是0。
【说明】把无序类别数据转换为热编码,对涉及距离计算类算法(如线性回归、KNN等算法)尤其重要要。对概率或比率类算法(如决策树、朴素贝叶斯等算法)直接转换为整数即可,不一定需要转换为热编码。

6.5 机器学习实例

这里以葡萄酒(wine)数据集为例,因数据集不大,可以直接从网上下载。
葡萄酒数据集中的数据包括三种酒的13种不同成分的数量。文件中,每行代表一种酒的样本,共有178个样本;一共有14列,其中,第一个属性是类标识符,分别是1/2/3来表示,代表葡萄酒的三个分类。后面的13列为每个样本的对应属性的样本值。剩余的13个属性是,酒精、苹果酸、灰、灰分的碱度、镁、总酚、黄酮类化合物、非黄烷类酚类、原花色素、颜色强度、色调、稀释葡萄酒的OD280/OD315、脯氨酸。其中第1类有59个样本,第2类有71个样本,第3类有48个样本。 具体属性描述如表6-3所示。
表6-3 葡萄酒数据集属性

英文字段 中文字段
Class label 类别
Alcohol 酒精
Malic acid 苹果酸
Ash
Alcalinity of ash 灰的碱度
Magnesium
Total phenols 总酚
Flavanoids 类黄酮
Nonflavanoid phenols 非类黄酮酚
Proanthocyanins 原花青素
Color intensity 色彩强度
Hue 色调
OD280/OD315 of diluted wines  稀释酒的OD280 / OD315
Proline  脯氨酸

1)从网上下载数据。

运行结果如下:
类别 [1 2 3]

2)对数据进行标准化处理。

3)使用逻辑回归进行训练。

运行结果如下:
Training accuracy: 0.9838709677419355
Test accuracy: 0.9814814814814815
说明准确率还不错。
4)可视化各特征的权重系统。

运行结果如图6-22所示。

图6-22 各特征随着惩罚系数c变化的情况
5)查看选择的特征数量与模型的准确率的关系。先来定义数据预处理函数。

可视化特征数与准确率之间的关系。

运行结果如图6-23所示。

图6-23 特征数与准确率之间的关系
6)可视化各特征对模型的贡献率。使用随机森林算法,计算特征对模型的贡献。

运行结果如图6-24所示

图6-24 各特征的贡献率
说明酒精这个特征贡献最大,其次是苹果酸、灰等特征。

6.6 小结

机器学习是深度学习的基础,机器学习的很多方法在深度学习中得到进一步拓展,如机器学习中正则化方法、核函数、优化方法等在深度学习中都有。本章主要介绍机器学习的基本原理,为深度学习打下一个基础。

本书代码及数据下载百度盘

提取码:w42f
包括代码+数据+PPT文档等,大小约:3G

第一部分 基础篇

第1章 NumPy基础

1.1生成NumPy数组
1.2读取元素
1.3 NumPy的算术运算
1.4数组变形
1.5 批量处理
1.6 节省内存
1.7通用函数
1.8 广播机制
1.9小结

第2章 PyTorch基础

2.1 为何选择PyTorch
2.2 安装配置
2.3 Jupyter Notebook环境配置
2.4 NumPy与Tensor
2.5 Tensor与Autograd
2.6 使用NumPy实现机器学习
2.7 使用Tensor及Autograd实现机器学习
2.8 使用优化器及自动微分
2.9 把数据集转换带批量的迭代器
2.10 使用TensorFlow2架构实现机器学习
2.11 小结

第3章 PyTorch神经网络工具箱

3.1 神经网络核心组件
3.2 PyTorch构建神经网络的主要工具
3.3 构建模型
3.4 训练模型
3.5实现神经网络实例
3.6 小结

第4章 PyTorch数据处理工具箱

4.1 数据处理工具箱概述
4.2 utils.data简介
4.3 torchvision简介
4.4 可视化工具
4.5 小结

第二部分 深度学习基础

第5 章 机器学习基础

5.1 机器学习的基本任务
5.2 机器学习一般流程
5.3 过拟合与欠拟合

第5章 机器学习基础(5.1至5.3小节)

5.4 选择合适激活函数
5.5 选择合适的损失函数
5.6 使用合适的优化器

第5章 机器学习基础(5.4至5.6小节)

5.7 GPU加速
5.8 小结

第5章 机器学习基础(5.7至5.8小节)

第6章 视觉处理基础

6.1从全连接层到卷积层
6.2卷积层
6.3池化层
6.4现代经典网络
6.5 PyTorch实现CIFAR10多分类
6.6 使用模型集成方法提升性能
6.7使用现代经典模型提升性能
6.8 小结

第7章 自然语言处理基础

7.1 从语言模型到循环神经网络
7.2 正向传播与随时间反向传播
7.3 现代循环神经网络
7.4 循环神经网络的PyTorch实现
7.5文本数据处理
7.6词嵌入
7.7 PyTorch实现词性判别
7.8 用LSTM预测股票行情
7.9 几种特殊结构
7.10循环神经网络应用场景
7.11 小结

第8章 注意力机制

8.1 注意力机制概述
8.2 带注意力机制的编码器-解码器架构
8.3 Transformer架构
8.4 使用PyTorch实现Transformer
8.5 小结

第9 章 目标检测与语义分割

9.1目标检测及主要挑战
9.2 优化候选框的几种算法
9.3典型的目标检测算法
9.4 语义分割
9.5 小结

第10章 生成式深度学习

10.1 用变分自编码器生成图像
10.2 GAN简介
10.3用GAN生成图像
10.4 VAE与GAN的异同
10.5 CGAN
10.6 DCGAN
10.7 提升GAN训练效果的一些技巧
10.8 小结

第三部分 深度学习实战

第11章 人脸检测与识别

11.1 人脸检测与识别的一般流程
11.2 人脸检测
11.3特征提取
11.4人脸识别
11.5 使用PyTorch实现人脸检测与识别
11.6 小结

第12章 迁移学习实例

12.1 迁移学习简介
12.2 特征提取
12.3 数据增强
12.4 微调实例
12.5 清除图像中的雾霾
12.6 小结

第13章 神经网络机器翻译实例

13.1 使用PyTorch实现带注意力的解码器
13.2 使用注意力机制实现中英文互译
13.3 小结

第14 章 使用ViT进行图像分类

14.1 项目概述
14.2 数据预处理
14.3 生成输入数据
14.4 构建编码器模型
14.5 训练模型
14.6 小结

第15 章 语义分割实例

15.1 数据概览
15.2 数据预处理
15.3 构建模型
15.4 训练模型
15.5 测试模型
15.6 保存与恢复模型
15.7 小结

第16章 实战生成式模型

16.1 Deep Dream模型
16.2 风格迁移
16.3 使用PyTorch实现图像修复
16.4 使用PyTorch实现DiscoGAN
16.5 小结

第17章 AI新方向:对抗攻击

17.1对抗攻击简介
17.2常见对抗样本生成方式
17.3 使用PyTorch实现对抗攻击
17.4 对抗攻击和防御方法
17.5 小结

第18章 强化学习

18.1 强化学习简介
18.2 Q-Learning 原理
18.3 用PyTorch实现Q-Learning
18.4 SARSA 算法
18.5 小结

第19章 深度强化学习

19.1 DSN算法原理
19.2 用PyTorch实现 DQN算法
19.3 小结
附录A PyTorch 0.4版本变更
附录B AI在各行业的最新应用
附录C einops及einsum简介

第5章 可视化

俗话说得好,“一图胜千言”,可见图像给我们带来的震撼效果。生活如此,机器学习也如此,图的直观、简单明了同样给我不一样的感觉和理解。那么,如何把数据变成图?如何把一些比较隐含的规则通过图像展示出来呢?
本章主要介绍几个基于Python、TensorFlow开发的可视化的强大工具,具体包括:
♦ matplotlib
♦ pyecharts
♦ Tensorboard

5.1 matplotlib

matplotlib 是 Python 中最著名的2D绘图库,它提供了与 matlab 相似的 API,十分适合交互式绘图,简单明了,功能强大,而且可以方便地作为绘图控件,嵌入 GUI 应用程序中。下面我们进入matplotlib的世界,开始我们的数据可视化之旅。

5.1.1 matplotlib的基本概念

在介绍matplotlib前,首先要保证环境中安装了Python。建议使用Anaconda安装,因为Anaconda安装包中包含很多常用的工具包,如matplotlib、NumPy、Pandas、Sklearn等,并且后续的更新维护也非常方便。
在绘制我们的第一个图形之前,我们先来了解几个matplotlib的非常重要的概念,以帮助我们更快地理解matplotlib的各种API,以及能让你和你的同事使用一种大家都能听得懂的语言以及术语进行沟通。
matplotlib设置坐标主要参数配置详细说明及示例说明如下。
1)导入绘图相关模块;
2)生成数据;
3)plot绘制图形,(选 - 线条设置)设置线linestyle或标记marker;
4)(选 - 坐标轴设置 - 添加坐标标签)给x轴添加标签xlabel和y轴添加标签ylabel;
5)(选 - 坐标轴设置 - 添加坐标刻度)设置x轴的刻度xlim()和y轴的刻度ylim();
6)(选 - 图例设置label)设置图例legend();
7)输出图形show()。
下面来看一个使用matplotlib绘图的实例,具体如下:

使用matplotlib对数据进行可视化的示例的运行结果如图5-1所示。
图5-1 使用matplotlib对数据进行可视化
也可以把图5-1拆成两个图,代码如下。

把图5-1拆成两个图的运行结果如图5-2所示。
图5-2 把图5-1拆成两个图

5.1.2 使用matplotlib绘制图表

matplotlib能绘制出各种各样的图表,所以开发人员可根据需要展示的数据格式、内容以及要用图表来达到的效果来选择合适的图形种类。下面我们通过日常工作中最常用的4种图表来做一个演示。
1.柱状图
柱状图是指用一系列高度不等的纵向条纹或者线段直观地显示统计报告来帮助人们理解数据的分布情况。在绘制柱状图时,我们可以使用plt.bar(x,y,tick_label),给出x,y坐标值,同时给出x坐标轴上对应刻度的含义等,示例如下。

绘制出的柱状图如图5-3所示。
图5-3 柱状图
2. 折线图
折线图通常用来显示随时间变化而变化的连续的数据,它非常适用于展示在相等的时间间隔下的数据的变化趋势。比如,使用折线图展示一个系统从2010年到2020年的每年的注册人数。在绘制折线图时,我们可以使用plt.plot()。 下面我们用折线图来显示系统注册人数的变化情况。

绘制出的折线图如图5-4所示。
图5-4 折线图
从图5-4中我们可以直观地看到,系统的注册人数在2011年进入了一个谷值,而2014是峰值。
3. 饼图
饼图常常用来显示一个数据系列中各项的大小及其在整体中的占比。比如我们可以用下面的饼图来展示每个人的月收入,并显示他们的月收入占总体收入的比例。

绘制出的饼图如图5-5所示。

图5-5 饼图
4. 散点图
散点图是指在回归分析中数据点在坐标系平面上的分布图,用于表示因变量随自变量变化而变化的大致趋势,从而帮助我们根据其中的关系选择合适的函数对数据点进行拟合。下面我们绘制一张身高和体重关系的散点图。

绘制出的散点图如图5-6所示。
图5-6 散点图
除了上述介绍的4种图形,matplotlib还可以绘制其他图形,比如线箱图、极限图、气泡图等。感兴趣的读者可以自行查阅matplotlib的网站或者源代码,以了解更多内容。

5.1.3 使用rcParams

rcParams用于存放matplotlib的图表全局变量,我们可以用它来设置全局的图表属性,当然在进行具体图表绘制的时候,我们也可以对全局变量进行覆盖。下面介绍几个常用的全局变量。注意,如果想在图表中显示中文内容,比如显示中文标题,则需要在matplotlib的全局变量rcParams里进行设置。
1)没设置rcParams属性。

运行结果如图5-7所示。

图5-7 没有设置rcParams属性的情况
如图5-7所示,中文标题没有正确显示,而是随机变成几个方框。此时,通过rcParams设置文字属性即可使标题正确显示。

运行结果如图5-8所示。

图5-8 设置rcParams属性的情况
更多关于rcParams的设置问题,请参照matplotlib官网(https://matplotlib.org/stable/api/matplotlib_configuration_api.html#matplotlib.RcParams)。

5.2 pyecharts

我们接下来要介绍的pyecharts正是Python版本的eCharts。
相较于经典的matplotlib,pyecharts可以在保证易用、简洁、交互性的基础上让开发人员绘制出种类更加丰富(比如3D,和地图模块的集成)、样式更加新颖的图表。下面我们先来看如何安装pyecharts 。

5.2.1 pyecharts安装

pyecharts 是一个用于生成 ECharts 图表的类库,官网为https://pyecharts.org/。
pyecharts有两个大的版本,v0.5.x 以及 v1.x。其中, v0.5.x 支持 Python 2.7 以及Python 3.4, v1.x 支持Python 3.6及以上版本。考虑到v0.5版本已经不再维护,而且大多数公司已经升级到Python 3.7及以上版本,所以本节只介绍1.x版本,并且以最新版v1.9为基础进行讲解。pyecharts安装主要有两种方式,通过源码或者pip安装,这里以pip安装为例进行讲解:

【说明】安装pyecharts时,可改用国内的安装源,如清华安装源,以提高下载速度,具体代码如下:

5.2.2 使用pyecharts绘制图表

我们先来用一个简单的例子直观地了解如何使用pyecharts绘图,体会它的便利性和优雅。

绘制出的pyecharts的柱状图如图5-9所示。

图5-9 pyecharts的柱状图
上述代码显示了苏州XX渔具店在2020年和2021年各种子品类的销售金额。 首先我们创建了一个Bar类型的图表,添加了X轴(add_xaxis)来代表各种品类,之后添加了两个Y轴的数据(add_yaxis)来代表2020年以及2021年的业绩。为了让图表更加容易理解,我们增加了标题以及副标题(title以及subtitle)。
用Pyecharts画的柱状图非常优雅,当然,用它画其他图形同样如此。绘制出的图形如下:
1. 仪表盘(Gauge)
我们第一个例子来模拟汽车的仪表盘,仪表盘上显示这辆汽车的最高时速,以及当前行驶速度,汽车仪表盘还会使用醒目的红色提醒驾驶员不要超速行驶,我们把这些信息一并添加到我们需要绘制的图形里面。

运行结果:
图5-10 仪表盘
大家可以从上图看到,仪表盘图形(Gauge)非常适合展示进度或者占比信息,通常我们会把几个仪表盘图形组合成一个组合图表进行展示,这样能让使用者对全局的信息有个快速的了解。不如,我们可以用几个仪表盘图形展示我们集群里面各个节点的健康状态,它们的CPU的使用率,IO的吞吐是不是在一个可承受的范围内等等。
2、地理坐标系(Geo)
这几年,各大app推出一个显示用户出行轨迹的应用广受各位旅游达人以及飞人的喜欢,在一张中国地图或者世界地图上,用箭头代表自己的飞行路径,线段的粗细代表了飞行这条航线的频率,让用户对自己过去一年的行踪有个直观的认识,也当做是在朋友圈凡尔赛的资料。接下来,我们用pyecharts来大概模拟这个功能。

运行结果:
图5-11 从上海出发的飞行路线图
pyecharts内嵌了中国以及各个省份的矢量图,可以方便的绘制出你想要的区域,使用者可以通过使用坐标或者城市名称的形式标定出具体的位置,进而用不同的颜色代表特殊的含义。

5.3 TensorBoard

在我们学习神经网络和使用TensorFlow的大多数时候,大多数人都会感觉很吃力,因为整个过程不可见。我们很难理清楚神经网络内部的结构,以及数据的流转情况,这给神经网络原理的理解和进一步使用带来了很大的挑战。TensorBoard就是为了解决整个问题而研发出来,它是TensorFlow内置的一个可视化工具,它利用TensorFlow运行时所产生的日志文件可视化指标,如培训和验证数据的损失和准确性、权重和偏差、模型图等,从而极大地帮助我们进行调试和优化等工作。
那么,我们应该如何使用TensorBoard呢?首先,我们来定义一个简单的计算图。
1)导入数据。

2)构建模型。

3)训练模型

在Windows的命令行启动Tensorboard 服务,指定日志读写路径,如果是linux环境,请根据实际情况,修改logdir的值。

(1)Tensorboard Scalar
将显示了每个循环(epoch)后损失和准确度的变化——当整个数据集通过神经网络正向和反向传播时——随着训练的进行,了解损失和准确度很重要,因为了解这些指标在什么时候趋于稳定,有助于防止过拟合。本例对应的准确率随迭代次数变化的情况,如图5-12所示。
图5-12 使用Tensorboard显示的标量(Scalar)图
(2)Tensorboard Graphs
模型图显示了模型的设计,默认情况下,操作(op)级别图为默认值,可以通过在标记上选择其他级别,左边的Tag使用默认状态,可以看到如图5-13所示结果。
图5-13 Tag改为Default看到的计算图
如左边的Tag更改为“Keras”。操作级别图显示TensorFlow对程序的理解,可以看到如图5-14所示的主干图。

图5-14Tag改为Keras看到的计算图
(3)Tensorboard Distribution
用于显示了张量的分布,显示每个循环(epoech)权重和偏差的分布情况。

图5-15 权重和偏差的分布图
(4)Tensorboard Histograms
直方图用于显示张量的分布,显示每个循环(epoech)中权重和偏差的分布情况。

图5-16 权重和偏差的分布图

5.4小结

可视化往往能锦上添花,所以无论是数据分析还是深度学习,都非常重视可视化。这里我们介绍了3种可视化工具,matplotlib是基础、常用、适应范围广;pyecharts功能强大,容易制成动态图像,外观丰富多彩;Tensorboard是TensorFlow为深度学习定制一个可视化工具。

第4章 TensorFlow数据处理

在机器学习、深度学习的实际项目中,由于数据都是真实数据,这些数据可能涉及多个来源地,存在大量不统一、不规范、缺失,甚至错误的数据,因此,在实际项目中,数据处理环节的挑战很大,往往占据大部分时间,且这个环节的输出质量直接影响模型的性能。
面对这个棘手问题,TensorFlow为我们提供了有效解决方法,这就是tf.data这个API。使用TensorFlow 2.0提供的tf.data可以有效进行数据预处理,同时通过构建数据流或数据管道,大大提高开发效率及数据质量。本章主要涉及如下内容:
♦ tf.data简介
♦ 构建Dataset的常用方法
♦ 如何生成自己的TFRecord数据
♦ 数据增强方法

4.1 tf.data简介

tf.data是TensorFlow提供的构建数据管道的一个工具,与PyTorch的utils.data类似。使用tf.data构建数据集(Dataset),可以使构建和管理数据管道更加方便。
tf.data API的结构如图4-1所示,最上边为Dataset基类,实例化为Iterator。TextLineDataset(处理文本)、TFRecordDataset(处理存储于硬盘的大量数据,不进行内存读取)、FixedLengthRecordDataset(二进制数据的处理)继承自Dataset,这几个类的方法大体一致,主要包括数据读取、元素变换、过滤,数据集拼接、交叉等。Iterator是Dataset中迭代方法的实例化,主要用于对数据进行访问。它有单次、可初始化、可重新初始化、可馈送等4种迭代方法,可实现对数据集中元素的快速迭代,供模型训练使用。因此,只要掌握Dataset以及Iterator的方法,即可清楚TensorFlow的数据读取方法。
图4-1 tf.data API的架构图
接下来主要介绍Dataset的基本属性、创建方法,Iterator的使用等内容。

4.2 构建数据集的常用方法

tf.data.Dataset表示一串元素(element),其中每个元素包含一个或多个Tensor对象。例如:在一个图像流水线(pipeline)中,一个元素可以是单个训练样本,它们带有一个表示图像数据的张量和一个标签组成的数据对(pair)。有两种不同的方式构建一个数据集,具体如下。
 直接从 Tensor 创建数据集(例如 Dataset.from_tensor_slices());当然 NumPy 也是可以的,TensorFlow 会自动将其转换为 Tensor。
 通过对一个或多个 tf.data.Dataset 对象的变换(例如 Dataset.batch())来创建数据集。
这两类构建方法又可以进一步分为7种方法,如表4-1所示。
表4-1 构建数据集的常用方法

数据格式 读取方法 备注
从NumPy数组读取 tf.data.Dataset.from_tensor_slices 当数据较小时
从Python Generator读取 tf.data.Dataset.from_generator  
从文本数据读取 tf.data.TextLineDataset  
从CSV数据读取 tf.data.experimental.CsvDataset  
从 TFRecord data读取 tf.data.TFRecordDataset TFRecord 是TensorFlow中自带的,它是一种方便储存比较大的数据集的数据格式(二进制格式),当内存不足时,我们可以将数据集制作成TFRecord格式的再将其解压读取。
从二进制文件读取数据 tf.data.FixedLengthRecordDataset  
从文件集中读取数据 tf.data.Dataset.list_files()  

1)直接从内存中读取(如NumPy数据),可使用tf.data.Dataset.from_tensor_slices()。
2)使用一个 Python 生成器 (Generator) 初始化,从生成器中读取数据可以使用tf.data.Dataset.from_generator()。
3)读取文本数据,可使用tf.data.TextLineDataset()。
4)读取cvs数据,可使用tf.data.experimental.make_csv_dataset()。
5)从TFrecords格式文件读取数据,可使用tf.data.TFRecordDataset()。
6)从二进制文件读取数据,可用tf.data.FixedLengthRecordDataset()。
7)从文件集中读取数据,可使用tf.data.Dataset.list_files()。

4.2.1 从内存中读取数据

从内存中读取数据的方法适用于数据较少,可直接存储于内存中的情况,其主要包括tf.data.Datasets.from_tensor_slices方法和tf.data.Datasets..from_generator(从生成器读取)方法。它从内存中读取数据,输入参数可以是NumPy的多维数组,也可以是TensorFlow的张量,还可以是Python的列表(list)、元祖以及字典等。
1.从NumPy中读取数据
如果输入数据为NumPy或tf.Tensor,可使用 Dataset.from_tensor_slices()读取。

2. 从迭代器中读取数据
可以读取NumPy、张量数据,也可以读取迭代器中的数据。迭代器中的数据虽然也在内存中,但所耗的资源较小。在使用tf.data.Dataset.from_generator()方法构建数据集时,我们需要提供3个参数(generator、output_types、output_shapes),其中generator参数必须支持iter()协议,也就是generator需要具有迭代功能,推荐使用Python yield。

4.2.2 从文本中读取数据

内存数据一般较小,很多情况下,我们需要加载的数据保存在文本文件中,比如日志文件。tf.data.TextLineDataset可以帮助我们从文本文件中读取数据,源文件中(比如日志信息)的一行代表一个样本。 tf.data.TextLineDataset 的方法签名为:tf.data.TextLineDataset(filenames, compression_type=None, buffer_size=None, num_parallel_reads=None) ,其中:
• filenames :一系列将要读取的文本文件的路径+名字。
• compression_type :文件的压缩格式,tf.data.TextLineDataset可以从压缩文件中直接读取数据或者将数据写入压缩文件中以节省磁盘空间。它的默认值是None,表示不压缩。
• buffer_size :一次读取的字节数量,如果不指定,则将由TensorFlow根据一定策略选择。
• num_parallel_reads :如果有多个文件需要读取,用num_parallel_reads可以指定同时读取文件的数量。默认情况下按照文件顺序一个一个读取。
为了演示通过TextLineDataset读取数据的过程,这里以一个数据文件(test.txt)为例。

运行结果如下:
tf.Tensor(b'Suzhou, JiangSu, 1', shape=(), dtype=string)
tf.Tensor(b'Wuxi, JiangSu, 0', shape=(), dtype=string)

实际上,csv文件也是一个文本文件,TextLineDataset也可以读取csv文件的数据,但是根据上面的例子我们知道,tf.data.TextLineDataset会将文件中的每一行数据读成一个张量,如果我们想保留csv文件中数据的结构(比如第一列代表了什么,第二列代表了什么),可以使用tf.data.experimental.make_csv_dataset函数。这里以泰坦尼克生存预测数据为例进行简单说明,第一行为标题。

此外,可以指定在读取数据时只读取我们感兴趣的列,通过传入参数select_columns 来实现。
在train.csv文件中,survived列是标签数据。可以通过指定label_name来分离样本数据和标签数据。

4.2.3 读取TFrecord格式文件

TFRecord 是 TensorFlow 自带的一种数据格式,是一种二进制文件。它是TensorFlow 官方推荐的数据保存格式,其数据的存储、读取操作更加高效。具体来说,TFRecord的优势可概括为:
1)支持多种数据格式;
2)更好的利用内存,方便复制和移动;
3)将二进制数据和标签(label)存储在同一个文件中。
TFRecord 格式文件的存储形式会很合理地帮我们存储数据。TFRecord 内部使用了 Protocol Buffer 二进制数据编码方案,它只占用一个内存块,只需要一次性加载一个二进制文件的方式即可,简单,快速,尤其对大型训练数据很友好。当我们的训练数据量比较大的时候,TFRecord可以将数据分成多个 TFRecord 文件,以提高处理效率。
假设有一万张图像, TFRecord 可以将其保存成 5 个.tfrecords 文件(具体保存成几个文件,要看文件大小),这样我们在读取数据时,只需要进行5 次数据读取。如果把这一万张图像保存为NumPy格式数据,则需要进行10000次数据读取。
我们可以使用tf.data.TFRecordDataset类读取TFRecord文件。

TFRecord格式非常高效,接下来我们将详细介绍如何把自己的数据转换为TFRecord格式的数据,以及转换的具体步骤等内容。

4.3 如何生成自己的TFRecord格式数据

上节我们介绍了TFRecord格式的一些优点,那么,如何把一般格式的数据,如图像、文本等格式数据,转换为TFRecord格式数据呢?这里通过介绍一个把有关小猫和小狗的jpg格式的图像转换为TFRecord文件,然后读取转换后的数据的例子,帮助你了解如何生成自己的TFRecord格式数据。

4.3.1 把数据转换为TFRecord格式的一般步骤

把非TFRecord格式数据转换为TFRecord格式数据的一般步骤,如图4-2所示。
图4-2 转换成TFRecord格式的一般步骤
在数据转换过程中,Example是TFReocrd的核心,TFReocrd包含一系列Example,每个Example可以认为是一个样本。Example是Tensorflow的对象类型,可通过tf.train.example来使用。
Example含义: 如表4-2所示,假设有样本(x,y) :输入x 和 输出y一起叫作样本。这里每个x是六维向量,每个y是一维向量。
1)表征 (representation):x集合了代表个人的全部特征。其中特征 (feature):x中的某个维度:如学历,年龄,职业。是某人的一个特点。
2)标签 (label):y为输出。
表4-2 一个样本格式

要存储表4-2中的数据,通常我们把输入表征x与标签y分开进行保存。假设有100个样本,把所有输入存储在100x6的numpy矩阵中,把标签存储在100x1的向量中。
Example协议块格式如下:

以TFRecord方式存储,输入和标签将以字典方式存放在一起,具体格式如图4-3所示。
图4-3 TFRecord文件的存储格式

4.3.2 加载TFRecord文件流程

生成TFRecord文件后,我们可以通过tf.data来生成一个迭代器,设置每次调用都返回一个大小为batch_size的batch。可以通过TensorFlow的两个重要的函数读取TFRecord文件,如图4-4所示,分别是读取器(Reader)tf.data.TFRecordDataset和解码器(Decoder)tf.io.parse_single_example。
图4-4 加载TFRecord文件流程

4.3.3 代码实现

生成TFRecord格式数据的完整过程为:
• 先把源数据(可以是文本、图像、音频、Embedding等,这里是小猫、小狗的图像)导入内存(如NumPy);
• 把内存数据转换为TFRecord格式数据;
• 读取TFRecord数据。
1)导入模块及数据。

2)把数据转换为TFRecord格式。

构建Example时,这个tf.train.Feature()函数可以接收3种数据,具体如下。
• bytes_list: 可以存储string 和byte两种数据类型。
• float_list: 可以存储float(float32)与double(float64) 两种数据类型。
• int64_list: 可以存储bool、 enum、int32、uint32、int64、uint64。
对于只有一个值的数据(比如label)可以用float_list或int64_list,而像图像、视频、Embedding这种列表型的数据,通常转化为bytes格式储存。
3)从TFRecord读取数据。这里使用 tf.data.TFRecordDataset类来读取TFRecord文件。TFRecordDataset 对于标准化输入数据和优化性能十分有用。可以使用tf.io.parse_single_example函数对每个样本进行解析(tf.io.parse_example用于对批量样本进行解析)。 注意,这里的 feature_description 是必需的,因为数据集使用计算图方式执行,需要这些描述来构建它们的形状和类型签名。
可以使用 tf.data.Dataset.map 方法将函数应用于数据集的每个元素。

4)可视化读取的数据。

解码TFRecord数据的示意图如图4-5所示。
图4-5 解码TFRecord数据的示意图

4.4 数据增强方法

在训练模型时,我们采用的图像数据越多,建立有效、准确的模型的概率就越大。如果没有理想的海量数据,我们应该怎么办呢?此时我们可以采取数据增强方法来增加数据集的大小以及多样性。数据增强(Data Augmentation)是指对图像进行随机的旋转、裁剪、随机改变图像的亮度和对比度以及对数据进行标准化(数据的均值为0,方差为1)等一系列操作。
如图4-6所示,我们可以通过不同的数据增强方法制作出6张标签相同(都是狗)的图,从而增加我们学习的样本。
图4-6通过TensorFlow数据增强方法得到不同状态的图像

4.4.1 常用的数据增强方法

常见的数据增强方法可以分为几何空间变换方法以及像素颜色变换方法两大类。几何空间变换包括对图像进行翻转、剪切、缩放等,像素颜色变换包括增加噪声、进行颜色扰动、锐化、浮雕、模糊等。
在TensorFlow中,我们可以结合使用数据增强方法与数据流水线,通常有以下两种方式:
1)利用TensorFlow的预处理方法定义数据增强函数然后通过Sequential类加以应用;
2)利用tf.image的内置方法手动创建数据增强路由。
相比较而言,第一种方式更加简单,第二种方式相对复杂,但是更加灵活。接下来,我们以第二种方式为例进行演示,顺便探索tf.image为我们提供了哪些便利的函数。我们可以通过dir(tf.image)查看它的内置方法。
之前我们提到,图像的左右翻转可以使用tf.image.flip_left_right或者tf.image.random_flip_left_right来实现,而图像的上下翻转可以使用tf.image.flip_up_down或者tf.image.random_flip_up_down实现。更多的图像转换方法,可以查阅API文档。
下面我们用一个完整的例子来演示如何在Tensorflow流水线中配合使用tf.image来进行数据增强。首先我们定义一个函数来读取图像数据。
1)定义导入数据函数。

2)可视化数据增强效果。

运行结果如图4-7所示。

图4-7 通过数据增强方法得到不同位置的图像
3)定义数据增强函数。

4.4.2 创建数据处理流水线

在把数据应用于深度学习模型训练时,我们往往需要对数据进行各种预处理以满足模型输入参数的需要。tf.data提供了一系列有用的API来帮我们进行数据转换,比较常用的数据转换方法列举如下。
• map: 将(自定义或者TensorFlow定义)转换函数应用到数据集的每一个元素。
• filter:选择数据集中符合条件的一系列元素。
• shuffle: 对数据按照顺序打乱。
• repeat: 将数据集中的数据重复N次,如果没有指定N,重复无限次。
• take: 采样,取从开始起的N个元素。
• batch: 构建批次,每次放一个批次。
具体实现如下:

为了更加直观地显示出数据增强的效果,我们用matplotlib画出我们做过数据转换后的图像,具体实现如下:

经过处理后的图像如图4-8所示。

图4-8 经过处理后的图像

4.5 小结

数据预处理、数据增强等方法是机器学习和深度学习中经常用到的方法,我们在使用这些方法时也会遇到一定的挑战,尤其对于一些不规范的数据,数据预处理显得尤为重要。而数据增强方法是扩充数据量、丰富数据多样性的有效方法。对数据预处理、数据增强,TensorFlow都提供了很多内置方法。

第3章 TensorFlow构建模型的方法

第2章我们用TensorFlow的自动微分及优化器等方法,实现了一个比较简单的回归问题。在本章,我们将继续对如何高效构建模型、训练模型做进一步的说明。根据构建模型的方式,本章将分为如下3种方法:
♦ 低阶API建模
♦ 中阶API建模
♦ 高阶API建模

3.1 利用低阶API构建模型

用TensorFlow低阶API构建模型主要包括张量操作、计算图及自动微分等操作,这种方法灵活性高,如果构建模型继承tf.Module,还可以轻松实现保存模型及跨平台部署。为提高模型运行效率,我们还可以使用@tf.function装饰相关函数,使之转换为自动图。为了更好地掌握本节的相关内容,这里以分类项目为例。

3.1.1 项目背景

这里以CIFAR-10为数据集,数据导入和预处理使用自定义函数,为更有效地处理数据,这里使用tf.data工具。有关tf.data的详细使用将在第4章将介绍,这里不再详述。构建模型只使用TensorFlow的低阶API,如tf.Variable、tf.nn.relu、自动微分等。然后自定义训练过程,最后保存和恢复模型。
CIFAR-10为小型数据集,一共包含10个类别的 RGB 彩色图像:飞机(airplane)、汽车(automobile)、鸟类(bird)、猫(cat)、鹿(deer)、狗(dog)、蛙类(frog)、马(horse)、船(ship)和卡车(truck)。图像的尺寸为 32×32(像素),3个通道 ,数据集中一共有 50000 张训练圄片和 10000 张测试图像。CIFAR-10数据集有3个版本,这里使用Python版本。

3.1.2 导入数据

1)导入需要的模块。

2)定义导入单批次的函数。因数据源分成几个批次,这里定义一个导入各批次的函数。

3)导入整个数据集。

4)指定数据文件所在路径。

5)说明类别及对应索引,并随机可视化其中5张图像。

随机抽取CIFAR-10数据集中5张图的结果如图3-1所示。

图3-1 随机抽取CIFAR-10数据集中5张图

3.1.3 预处理数据

1)对数据进行简单处理。对数据进行规范化,并设计相关超参数等。

2)使用TensorFlow的数据预处理工具tf.data,使预处理过程打包成为一个管道。

dataset 中 shuffle()、repeat()、batch()、prefetch()等函数的主要功能如下。
1)repeat(count=None) 表示重复此数据集 count 次,实际上,我们看到 repeat 往往是接在 shuffle 后面的。为何要这么做,而不是反过来,先 repeat 再 shuffle 呢? 如果shuffle 在 repeat 之后,epoch 与 epoch 之间的边界就会模糊,出现未遍历完数据,已经计算过的数据又出现的情况。
2)shuffle(buffer_size, seed=None, reshuffle_each_iteration=None) 表示将数据打乱,数值越大,混乱程度越大。为了完全打乱,buffer_size 应等于数据集的数量。
3)batch(batch_size, drop_remainder=False) 表示按照顺序取出 batch_size 大小数据,最后一次输出可能小于batch ,如果程序指定了每次必须输入进批次的大小,那么应将drop_remainder 设置为 True 以防止产生较小的批次,默认为 False。
4)prefetch(buffer_size) 表示使用一个后台线程以及一个buffer来缓存batch,提前为模型的执行程序准备好数据。一般来说,buffer的大小应该至少和每一步训练消耗的batch数量一致,也就是 GPU/TPU 的数量。我们也可以使用AUTOTUNE来设置。创建一个Dataset便可从该数据集中预提取元素,注意:examples.prefetch(2) 表示将预取2个元素(2个示例),而examples.batch(20).prefetch(2) 表示将预取2个元素(2个批次,每个批次有20个示例),buffer_size 表示预提取时将缓冲的最大元素数返回 Dataset。
使用prefetch可以把数据处理与模型训练的交互方式由图3-2变为图3-3。
图3-2 未使用prefetch的数据处理流程
图3-3 使用prefetch后的数据处理流程

3.1.4 构建模型

使用tf.Module 封装变量及其计算,可以使用任何Python对象,有利于保存模型和跨平台部署使用。因此,可以基于TensorFlow开发任意机器学习模型(而非仅仅是神经网络模型),并实现跨平台部署使用。
1)构建模型。构建一个继承自tf.Module的模型,修改基类的构造函数,把需要初始化的变量放在__init__构造函数中,把参数变量的正向传播过程放在__call__方法中。__call__方法在模型实例化时,将自动调用。为提供更快的运行效率,通常用@tf.function进行装饰。

2)定义损失函数和评估函数。

3.1.5 训练模型

1)实现反向传播。这里使用自动微分机制,并使用优化器实现梯度的自动更新,具体过程如下:
• 打开一个 GradientTape() 作用域;
• 在此作用域内,调用模型(正向传播)并计算损失;
• 在作用域之外,检索模型权重相对于损失的梯度;
• 根据梯度使用优化器来更新模型的权重;
• 利用优化器进行反向传播(更新梯度)。

2)定义训练过程。

这是最后6次的运行结果:
step: 35000, loss: 0.910988, accuracy: 0.734375
step: 36000, loss: 0.790101, accuracy: 0.765625
step: 37000, loss: 0.753428, accuracy: 0.750000
step: 38000, loss: 0.658011, accuracy: 0.781250
step: 39000, loss: 0.817612, accuracy: 0.718750
step: 40000, loss: 0.723336, accuracy: 0.718750
3)可视化运行过程。

随着迭代次数增加,模型准确率的变化如图3-4所示。

图3-4 随着迭代次数增加,模型准确率的变化

3.1.6 测试模型

对模型进行测试。

运行结果如下:
0.535256
构建模型只使用了全连接层,没有对网络进行优化,测试能达到这个效果也不错。后续我们将采用数据增强、卷积神经网络等方法进行优化。

3.1.7 保存恢复模型

1)保存模型。

2)恢复模型。

3)利用恢复的模型进行测试。

运行结果如下:
0.535256
由结果可知,它与原模型的测试结果完全一样!

3.2 利用中阶API构建模型

用TensorFlow的中阶API构建模型,主要使用TensorFlow或tf.keras提供的各种模型层、损失函数、优化器、数据管道、特征列等,无须自己定义网络层、损失函数等。如果定制性要求不高,这将大大提高构建模型的效率。利用中阶API构建模型时需要继承tf.Module,它是各种模型层的基类。为更好地掌握相关内容,还是使用3.1节的数据集,架构相同,只是把定义层改为直接使用tf.keras提供的层、优化器、评估函数等。
利用中阶API构建模型的导入数据、预处理数据等部分,与3.1节的相应部分一样,这里不再赘述。下面主要介绍两种方法的不同之处。

3.2.1构建模型

用TensorFlow的中阶API构建模型,需要使用TensorFlow或tf.keras提供的各种模型层、损失函数、优化器、数据管道、特征列等,无须自己定义网络层、损失函数等。因数据为图像,这里使用全连接层,故第一层使用Flatten层,把数据展平。

3.2.2创建损失评估函数

损失函数使用tf.keras.metrics.Mean类,评估函数使用tf.keras.metrics.SparseCategoricalAccuracy类,使用该类标签无须转换为独热编码。

3.2.3训练模型

1)定义训练函数。

2)训练模型。

最后6次的运行结果:
train: step 45001, Loss: 1.279, Accuracy: 54.51%
train: step 46001, Loss: 1.276, Accuracy: 54.63%
train: step 47001, Loss: 1.272, Accuracy: 54.76%
train: step 48001, Loss: 1.269, Accuracy: 54.88%
train: step 49001, Loss: 1.266, Accuracy: 55.00%
train: step 50001, Loss: 1.263, Accuracy: 55.12%
3)可视化训练结果。

使用中阶API构建模型的可视化结果如图3-5所示。

图3-5 使用中阶API构建模型的可视化结果
4)测试模型。

运行结果如下:
Test Loss: 1.385, Test Accuracy: 52.14%

利用中阶API构建模型、实现训练,比直接使用低阶API简化了不少,但编码量还是比较大,尤其是定义评估函数、训练过程等,接下来我们介绍一种更简单、高效的方法,即下节将介绍的利用高阶API构建模型。

3.3利用高阶API构建模型

TensorFlow的高阶API主要是指tf.keras.models提供的模型的类接口。目前tf.keras为官方推荐的高阶API。 使用ff.keras接口构建模型的方式有3种。
• 序列API(Sequential API)模式,把多个网络层的线性堆叠构建模型。
• 函数式API(Functional API)模式,可构建任意结构模型。
• 子类模型API(Model Subclassing API)模式,使用继承Model基类的子类模型可构建自定义模型。
这里先使用序列API按层顺序构建模型,其他模式构建模型将在第7章详细介绍。
导入与数据预处理过程与3.1节一样,这里不再赘述,具体请参考本书代码及数据部分。

3.3.1 构建模型

这里主要使用tf.keras.models及tf.keras.layers高阶类,构建模型采用序列API方法,这种方法就像搭积木一样,非常直观和简单。首先实例化Sequential类,然后需要使用add方法把各层按序叠加在一起,并基于优化器、损失函数等方法编译模型,最后输入数据训练模型。这个过程可用图3-6来直观描述。

图3-6 序列API构建模型流程图
导入需要的库,构建模型。

显示模型各层及其结构,如图3-7所示。:
图3-7 显示模型各层及其结构
构建Sequential模型时,第一层需要知道输入数据的形状,其他各层只要指明输出形状即可,输入形状可自动推导。因此,Sequential的第一层需要接收一个关于输入数据形状(shape)的参数。那么,如何指定第一层的输入形状呢?有以下几种方法来为第一层指定输入数据的形状。
• 传递一个input_shape参数给第一层。它是一个表示形状的元组 (一个由整数或None组成的元组,其中 None表示可能为任何正整数)。在input_shape中不包含数据批次(batch)大小。
• 有些2维层,如全连接层(Dense),支持通过指定其输入维度input_dim来隐式指定输入数据的形状,input_dim是一个整数类型的数据。一些3维的时域层支持通过参数input_dim和input_length来指定输入shape。
• 对某些网络层,如果需要为输入指定一个固定大小的批量值(batch_size),可以传递batch_size参数到一个层中。例如你想指定输入张量的batch大小是32,数据shape是(6,8),则需要传递batch_size=32和input_shape=(6,8)给一个层,那么每一批输入的形状就为 (32,6,8)。

3.3.2 编译及训练模型

训练模型之前,需要对模型进行编译配置,这是通过compile方法完成的。它接收三个参数。
• 优化器(optimizer):可以是内置优化器的字符串标识符,如 rmsprop 或 adagrad,也可以是Optimizer类的对象。
• 损失函数(loss):模型最小化的目标函数。它可以是内置损失函数的字符串标识符,如 categorical_crossentropy 或 mse等,也可以是自定义的损失函数。
• 评估标准(metrics):可以是内置的标准字符串标识符,也可以是自定义的评估标准函数。对于分类问题,一般将评估标准设置为 metrics = ['accuracy']。
1)编译模型。

2)训练模型。这里采用回调机制定时保存模型。

最后两次的迭代结果:
Epoch 19/20
704/704 - 2s - loss: 1.6917 - accuracy: 0.3892 - val_loss: 1.6291 - val_accuracy: 0.4120
Epoch 00019: val_loss did not improve from 1.62898
Epoch 20/20
704/704 - 2s - loss: 1.6903 - accuracy: 0.3869 - val_loss: 1.6163 - val_accuracy: 0.4154
Epoch 00020: val_loss improved from 1.62898 to 1.61627, saving model to MLP.best_weights.hdf5

最后,根据模型训练的结果自动保存最好的那个模型。
3)可视化运行结果。

使用高阶API构建模型的可视化结果如图3-8所示。

图3-8 使用高阶API的运行结果

3.3.3测试模型

输入如下代码测试模型。

运行结果如下:
Test accuracy:0.4329

3.3.4 保存恢复模型

1)选择最好的模型参数恢复模型。

2)查看网络结构。

运行结果与图3-7的网络结构完全一致。
3)检查其准确率(accuracy)。

运行结果如下:
313/313 - 1s - loss: 1.6150 - accuracy: 0.4329
Restored model, accuracy: 43.29%
由结果可知,其结果与预测结果完全一致!

3.4 小结

本章介绍了用几种常用的API构建网络和训练模型的方法。这些API从封装程度来划分,可分为低阶API、中阶API和高阶API。低阶API基本用TensorFlow实现,自定义层、损失函数等;中阶API使用层模块(如tf.keras.layers.Dense)、内置的损失函数、优化器等构建模型;高阶API的封装程度最高,使用tf.keras构建模型、训练模型。这3种方法各有优缺点,低阶API代码量稍多一些,但定制能力较高,而用高阶API构建模型和训练模型的代码比较简洁,但定制能力稍弱。实际上,我们的大部分任务基本都可以用高阶API来实现,尤其对初学者,使用高阶API构建模型、训练模型是首选。

第2章 TensorFlow张量及其计算

2015年11月,Google首次宣布开源TensorFlow,经过多次迭代,2017年2月,Google发布了更加稳定并且性能更加强劲的TensorFlow 1.0。2019年10月,Google正式发布了TensorFlow 2.0, 在1.0的基础上,2.0版本的TensorFlow有以下的增强。
1)对开发者更加友好,默认为即时执行模式(Eager mode,或称为动态图模式),TensorFlow代码现在可以像正常的Python代码一样运行,这大大提高了人们的开发效率。
2)在需要提高性能的地方可利用@tf.function切换成Autograph模式。
3)Keras已经成为TensorFlow 2.0版本的官方高级API,并推荐使用tf.keras。
4)清理了大量的API,以简化和统一TensorFlow API。
5)改进tf.data功能,基于tf.data API,可使用简单的代码来构建复杂的输入。
6)提供了更加强大的跨平台能力, 通过TensorFlow Lite,我们可以在Andiord,iOS以及各种嵌入式系统里面部署和运行模型。通过TensorFlow.js,我们可以将模型部署在JavaScript环境里面。本章将从以下几个方面介绍TensorFlow基础内容。
♦ 简单说明TensorFlow 2+的安装;
♦ 层次架构;
♦ 张量与变量;
♦ 动态计算图;
♦ 自动图;
♦ 自动微分;
♦ 损失函数、优化器等;
♦ 通过实例把这些内容贯穿起来。

2.1 安装配置

TensorFlow支持多种环境,如Linux、Windows、Mac等,本章主要介绍基于Linux系统的TensorFlow安装。TensorFlow的安装又分为CPU版和GPU版。CPU版相对简单一些,无须安装显卡驱动CUDA和基于CUDA的加速库cuDNN等;GPU版的安装步骤更多一些,需要安装CUDA、cuDNN等。不过,无论选择哪种安装版本,我们推荐使用Anaconda作为Python环境,因为这样可以避免大量的兼容性和依赖性问题,而且使用其中的conda进行后续更新维护也非常方便。接下来我将简单介绍如何安装TensorFow,更详细的安装内容请参考附录A。

2.1.1 安装Anaconda

Anaconda内置了数百个Python经常使用的库。其包含的科学包有:conda、NumPy、Scipy、Pandas、IPython Notebook等,还包括机器学习或数据挖掘的库,如Scikit-learn。Anaconda是目前最好的Python安装环境,它不但便于安装,也便于后续版本升级维护。因此,本书推荐安装Anaconda。
Anaconda有Windows、Linux、MacOS等版本,这里我们以Windows环境为例,包括以下TensorFlow也是基于Windows。基于Linux的安装请参考附A。
1.下载安装包
打开Anaconda的官网(https://www.anaconda.com/products/individual),可看到如图2-1 所示的界面。

图2-1 Aanaconda下载界面
选择Python 3.8,64-Bit Graphical Installer 大小为477 MB。下载后可得类似如下文件:
Anaconda3-2021.05-Windows-x86_64.exe
2.安装
点击文件Anaconda3-2021.05-Windows-x86_64.exe即可,安装基本按默认或推荐选项即可,最后为提示是否“Add Anaconda to my PATH environment variable”,勾选,系统自动把Anaconda的安装目录写入PATH环境变量中。至此,安装结束。
3.验证
打开“Anaconda prompt”输入conda list,安装成功后可以看到已安装的库。

2.1.2 安装TensorFlow CPU版

在Windows安装TensorFlow CPU版比较简单,可以用conda或pip进行安装。
1. 使用conda安装
使用conda安装将自动安装TensorFlow依赖的模块,但因conda能安装的最新版本与TensorFlow最新版本有点滞后,如果要安装最新版本,可使用pip安装。
打开Anaconda prompt界面,先用search命令,查看用conda能安装的版本:

图2-2展示了运行结果的最后几行。
图2-2 查看conda能安装的TensorFlow(CPU版本)各版本
使用conda安装,选择版本号和安装源。如安装2.5.0 版本,使用豆瓣源。

然后启动Jupyter Notebook 服务,验证安装是否成功。在Jupyter Notebook中输入以下代码,如果没有报错信息,且显示已安装TensorFlow的版本为2.5.0,说明安装成功。

2.1.3 安装TensorFlow GPU版

安装TensorFlow GPU版的步骤相对多一些,这里采用一种比较简洁的方法。目前TensorFlow对CUDA的支持比较好,所以在安装GPU版之前,首先需要安装一块或多块GPU显卡。本节以NVIDIA显卡为例,当然也可以使用其他显卡。
接下来我们需要安装:
• 显卡驱动
• CUDA
• cuDNN
其中CUDA(Compute Unified Device Architecture)是英伟达公司推出的一种基于新的并行编程模型和指令集架构的通用计算架构,它能利用英伟达GPU的并行计算引擎,比CPU更高效地解决许多复杂计算任务。NVIDIA cuDNN是用于深度神经网络的GPU加速库,它强调性能、易用性和低内存开销。NVIDIA cuDNN可以集成到更高级别的机器学习架构中,其插入式设计可以让开发人员专注于设计和实现神经网络模型,而不是调整性能,也可以在GPU上实现高性能并行计算。目前大部分深度学习架构使用cuDNN来驱动GPU计算。
这里假设Windows上的显卡及驱动已安装好,如果你还没有安装显卡及驱动,可参考附录A了解安装方法。
1.查看显卡信息
如果安装好GPU显卡及驱动,在Anaconda prompt端输入nvidia-smi命令可看到如2-3所示的GPU信息。
图2-3 显示GPU及驱动的相关信息
2.安装CUDA和cuDNN
1)进入NVIDIA官网(https://developer.nvidia.com/cuda-toolkit-archive)下载对应版本的CUDA并安装。
2)进入NVIDIA官网下载对应版本的cuDNN并解压缩。解压后的文件如图2-4所示。
图2-4 下载cuDNN并解压
3.将文件复制到对应目录
将cuDNN中的bin、include和lib文件拷贝到CUDA的安装对应目录,如:C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1 。
4. 验证
在命令行输入:

如果能看到如下类似信息,说明安装成功!
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2020 NVIDIA Corporation
Built on Tue_Sep_15_19:12:04_Pacific_Daylight_Time_2020
Cuda compilation tools, release 11.1, V11.1.74
Build cuda_11.1.relgpu_drvr455TC455_06.29069683_0
5.安装tensorflow-gpu
直接使用国内的源进行安装,这样下载速度比较快。

安装验证:

运行结果如下:
2.5
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
如果出现类似信息,说明TensorFlow-GPU安装成功!

2.2 层次架构

TensorFlow 2.0从低到高可以分成如下6个层次架构,如图2-5所示。
图2-5 TensorFlow 2.0的层次架构图
• 最底层为硬件层,TensorFlow支持CPU、GPU或TPU。
• 第2层为网络通信层,TensorFlow支持包括gRPC、RDMA(Remote Direct Memroy Access)、GDR(GPU Dirrect)和MPI4种通信协议。
• 第3层为C++实现的内核,如实现矩阵运算、卷积运算等,内核可以跨平台分布运行。
• 第4层为Python实现的各种操作符,TensorFlow提供了封装C++内核的低级API,主要包括各种张量操作算子、计算图、自动微分等,其中大部分继承自基类tf.Module,具体包括tf.Variable,tf.constant,tf.function, tf.GradientTape,tf.nn.softmax等。
• 第5层为Python实现的模型组件,TensorFlow对低级API进行了函数封装,主要包括各种模型层,损失函数,优化器,数据管道,特征列等。如tf.keras.layers,tf.keras.losses,tf.keras.metrics,tf.keras.optimizers,tf.data.DataSet等。
• 第6层为Python实现的各种模型,一般为TensorFlow按照面向对象方式封装的高级API,主要为tf.keras.models提供的模型的类接口。
接下来就各层的主要内容进行说明。

2.3 张量

张量(Tensor)是具有统一类型(通常是整型或者浮点类型)的多维数组,它和NumPy里面的ndarray非常相似。TensorFlow的张量(tf.Tensor)的基本属性与ndarray类似,具有数据类型和形状维度等,同时TensorFlow提供了丰富的操作库(tf.add,tf.matmul,tf.linalg.inv等),它们使用和生成tf.Tensor。 tf.Tensor与NumPy还可以互相转换。除这些相似点之外,tf.Tensor与NumPy也有很多不同之处,最大的不同就是NumPy只能在CPU上计算,没有实现GPU加速计算,而tf.Tensor不但可以在CPU上计算,也可以在GPU加速计算。接下来,我们就张量的基本属性、基本操作进行简单说明,同时总结了tf.Tensor与NumPy的异同点。

2.3.1 张量的基本属性

张量有几个重要的属性。
• 形状(shape):张量的每个维度的长度,与NumPy数组的shape一样。
• 维度/轴(axis):可以理解为数组的维度,例如,二维数组或者三维数组等。
• 秩(rank): 张量的维度数量,可用ndim查看。
• 大小(size): 张量的总的项数,也就是所有元素的数量,与NumPy数组的size一样。
• 数据类型(dtype): 张量元素的数据类型,如果在创建张量时不指定,TensorFlow会自动选择合适的数据类型。
接下来通过一些实例进行说明。用tf.constant生成各种维度的张量:

我们看一个4维的张量及相关属性。

这是一个秩为4,形状为(3,2,4,5)的张量,各轴的大小与张量形状之间的对应关系,可参考图2-6。
图2-6 张量各轴大小与张量形状之间对应关系
轴一般按照从全局到局部的顺序进行排序:首先是批次轴,随后是空间维度,最后是每个位置的特征。这样,在内存中,特征向量就会位于连续的区域。

2.3.2 张量切片

张量切片与NumPy切片一样,也是基于索引。切片或者索引是Python语言中针对字符串、元祖或者列表进行读写的魔法方法,在第1章介绍NumPy的时候也提到过,针对NumPy数组,我们也可以进行索引或者切片操作。同样的,我们也可以对TensorFlow里面的张量进行索引或者切片操作,并且遵循Python语言或者说NumPy数组的索引规则。
• 索引从下标0开始。
• 负索引按照倒叙进行索引,比如 -1表示倒数第一个元素。
• 切片的规则是start:stop:step。
• 通过制定多个索引,可以对多维度张量进行索引或者切片。
示例如下:

2.3.3 操作形状

与NumPy中的reshape、transpose函数一样,TensorFlow也提供reshape、transpose函数帮助我们操作张量形状。
通过重构可以改变张量的形状。重构的速度很快,资源消耗很低,因为不需要复制底层数据,只是形成一个新的视图,原张量并没有改变。

数据在内存中的布局保持不变,同时使用请求的形状创建一个指向同一数据的新张量。TensorFlow 采用 C 样式的“行优先”内存访问顺序,即最右侧的索引值递增对应于内存中的单步位移。

运行结果如下:
tf.Tensor(
[ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
24 25 26 27 28 29], shape=(30,), dtype=int32)

一般来说,tf.reshape 唯一合理的用途是用于合并或拆分相邻轴(或添加/移除 1维)。 对于3x2x5 张量,重构为(3x2)x5或者3x(2x5) 都是合理的,因为切片不会混淆。

运行结果如下:
tf.Tensor(
[[ 0 1 2 3 4]
[ 5 6 7 8 9]
[10 11 12 13 14]
[15 16 17 18 19]
[20 21 22 23 24]
[25 26 27 28 29]], shape=(6, 5), dtype=int32)

tf.Tensor(
[[ 0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
[20 21 22 23 24 25 26 27 28 29]], shape=(3, 10), dtype=int32)

重构可以处理总元素个数相同的任何新形状,但是如果不遵从轴的顺序,则不会发挥任何作用。利用 tf.reshape 无法实现轴的交换,所以要交换轴,需要使用 tf.transpose。

2.4 变量

深度学习在训练模型时,用变量来存储和更新参数。建模时它们需要被明确地初始化,模型训练后它们必须被存储到磁盘。这些变量的值可在之后模型训练和分析时被加载。变量通过tf.Variable类进行创建和跟踪,对变量执行运算可以改变其值。可以利用特定运算读取和修改变量的值,也可以通过使用张量或者数组的形式创建新的变量:

运行结果如下:
<tf.Variable 'Variable:0' shape=(2, 3) dtype=int32, numpy= array([[1, 2, 3], [4, 5, 6]])>

<tf.Variable 'Variable:0' shape=(2, 3) dtype=int32, numpy= array([[1, 2, 3], [4, 5, 6]])>

变量与常量的定义方式以及操作行为都十分相似,实际上,它们都是tf.Tensor支持的一种数据结构。与常量类似,变量也有数据类型 dtype 和形状 shape,也可以与NumPy数组相互交换,并且大部分能够作用于常量的运算操作都可以应用于变量,除了进行形状变形(变量的reshape方法会生成一个新的常量)。示例如下:

运行结果如下:
<class 'tensorflow.python.ops.resource_variable_ops.ResourceVariable'>

[[1 2 3]
[4 5 6]]

<class 'tensorflow.python.framework.ops.EagerTensor'>

<tf.Variable 'Variable:0' shape=(2, 3) dtype=int32, numpy= array([[1, 2, 3], [4, 5, 6]])>

2.5 NumPy与tf.tensor比较

前文提到, NumPy数组与TenosrFlow中的张量(即tf.tensor)有很多相似地方,而且可以互相转换。表2-1总结了NumPy与tf.tensor的异同点。
表2-1 NumPy与tf.tensor的异同点

操作类别 NumPy TensorFlow 2+
数据类型 np.ndarray tf.Tensor
np.float32 tf.float32
np.float64 tf.double
np.int64 tf.int64
从已有数据构建 np.array([3.2, 4.3], dtype=np.float16) a=tf.constant([3.2, 4.3], dtype=tf.float16)#常量

v=tf.Variable([3.2, 4.3], dtype=tf.float16)#变量

x.copy() tf.identity(x);

tf.tile(a,(n,m))# 元组里的每个数值对应该轴复制次数

np.concatenate tf.concat((a,b),axis)# 待拼接的轴对应的维度数值可以不等,但其他维度形状需一致

tf.stack((a,b),axis)# 带堆叠张量的所有维度数值必须相等

线性代数 np.dot #内积

np.multiply(*)#逐元素相乘或哈达玛积

tf.matmul(x, y, name=None) 或(@)#内积

tf.multiply(x, y, name=None),或(*)#逐元素相乘或哈达玛积

属性 x.ndim x.ndim  #查看rank
x.shape x.shape
x.size tf.size(x)
改变形状 x.reshape tf.reshape(x,(n,(-1)))#-1表示自动计算其他维度
np.transpose(x, [新的轴顺序] ) tf.transpose(x, [新的轴顺序] )
x.flatten() tf.reshape(x,[-1]);

tf.keras.layers.Flatten()

维度增减 np.expand_dims(arr, axis) tf.expend_dims(a,axis)
np.squeeze(arr, axis) tf.squeeze(a,axis),#如果不声明axis,那么将压缩所有数值为1的维度。
类型转换 np.floor(x) x=tf.cast(x,dtype=XX)

x=x.numpy()=>np.array

比较 np.less tf.less(x,threshold)
np.less_equal tf.less_equal(x, threshold)
np.greater_equal tf.greater_equal(x, threshold)
随机种子 np.random.seed tf.random.set_seed(n)

它们可以互相转换,具体分析如下:
• 通过使用 np.array 或 tensor.numpy 方法,可以将TensorFlow张量转换为 NumPy 数组;
• tf.convert_to_tensor把Python对象(NumPy,list、tuple等),或使用tf.constant、tf.Variable把Python对象转换为TensorFlow张量。

2.6 动态计算图

TensorFlow有3种计算图:TensorFlow1.0时代的静态计算图,TensorFlow 2.0时代的动态计算图和Autograph。静态计算图,需要先使用TensorFlow的各种算子创建计算图,再开启一个会话(Session)执行计算图。 而在TensorFlow 2.0时代,默认采用的是动态计算图,即每使用一个算子后,该算子会被动态加入隐含的默认计算图中立即执行并获取返回结果,而无须执行Session。
使用动态计算图(即Eager Excution立即执行)的好处是方便调试程序,执行TensorFlow代码犹如执行Python代码一样,而且可以使用Python,非常便捷。不过使用动态计算图的坏处是运行效率相对会低一些,因为在执行动态图期间会有许多次Python进程和TensorFlow的C++进程之间的通信。而静态计算图不通过Python这个中间环节,基本在TensorFlow内核上使用C++代码执行,效率更高。
为了兼顾速度与性能,在TensorFlow 2.0中可以使用@tf.function装饰器将普通Python函数转换成对应的TensorFlow计算图构建代码。与执行静态图方式类似,使用@tf.function构建静态图的方式叫作Autograph(自动图),更多详细内容将在2.7节介绍。

2.6.1 静态计算图

在TensorFlow 1.0中,静态计算图的使用过程一般分两步:第1步是定义计算图,第2步是在会话中执行计算图。

以上代码在TensorFlow 2.0环境中将报错,因为该环境中已取消了占位符(placeholder)及会话(Session)等内容,不过为考虑对老版本Tensorflow 1.0的兼容性,在tf.compat.v1子模块中保留了对TensorFlow 1.0那种静态计算图构建风格的支持,但是加上tf.compat.v1来对老版本提供支持的这种方式并不是官方推荐使用的方式。

运行结果如下:
735.0

2.6.2 动态计算图

以上代码如果采用动态计算图的方式实现,需要做如下处理。
1)把占位符改为其他张量,如tf.constant或tf.Variable。
2)无须显式创建计算图。
3)无须变量的初始化。
4)无须执行Session,把sess.run中的feed_dict改为传入函数的参数,fetches改为执行函数即可。
采用TensorFlow 2.0动态图执行的方式,代码如下:

运行结果如下:
735.0

2.7 自动图

与静态计算图相比,动态计算图虽然调试编码效率高但是执行效率偏低,TensorFlow 2.0 之后的自动图(AutoGraph)可以将动态计算图转换成静态计算图,兼顾开发效率和执行效率。通过给函数添加@tf.function装饰器就可以实现AutoGraph功能,但是在编写函数时需要遵循一定的编码规范,否则可能达不到预期的效果,这些编码规范主要包括如下几点。
• 避免在函数内部定义变量(tf.Variable)。
• 函数体内应尽可能使用TensorFlow中的函数而不是Python语言自有函数。比如使用tf.print而不是print,使用tf.range而不是range,使用tf.constant(True)而不是True。
• 函数体内不可修改该函数外部的Python列表或字典等数据结构变量。

用@tf.fuction装饰2.6.2节的函数,把动态计算图转换为自动图。

运行代码,出现如下错误信息:
ValueError: tf.function-decorated function tried to create variables on non-first call
这是为什么呢?
报错是因为函数定义中定义了一个tf.Variable变量。实际上,在动态模式中,这个对象就是一个普通的Python对象,在定义范围之外会被自动回收,然后在下次运行时被重新定义,因此不会有错误。但是现在tf.Variable定义了一个持久的对象,如果函数被@tf.function修饰,动态模型被禁止,tf.Variable定义的实际上是图中的一个节点,而这个节点不会被自动回收,且图一旦编译成功,不能再创建变量。故执行函数时会报这个错误。
那么,如何避免这样的错误呢?方法有多种,列举如下。
1)把tf.Variable变量移到被@tf.function装饰的函数外面。

运行结果如下:
735.0
在函数外部定义tf.Variable变量,你可能会感觉这个函数有外部变量依赖,封装不够完美。那么,是否有两全其美的方法呢?利用类的封装性就可以完美解决这个问题,即创建一个包含该函数的类,并将相关的tf.Variable创建放在类的初始化方法中。
2)通过封装成类方法来解决这个问题。

运行结果如下:
735.0
其他两个规范比较好理解,后面章节将详细说明。

2.8 自动微分

机器学习,尤其是其中的深度学习,通常依赖反向传播求梯度来更新网络参数,求梯度通常非常复杂且容易出错。 可喜的是TensorFlow深度学习架构帮助我们自动地完成了求梯度运算。 Tensorflow一般使用梯度磁带tf.GradientTape来记录正向运算过程,然后使用反播磁带自动得到梯度值。 这种利用tf.GradientTape求微分的方法叫作Tensorflow的自动微分机制,其基本流程如图2-7所示。
图2-7 TensorFlow自动微分的流程图
以下通过一些示例进行说明:

对常量张量也可以求导,需要增加watch。例如:

利用tape嵌套方法,可以求二阶导数。

默认情况下,只要调用GradientTape.gradient方法,系统将自动释放 GradientTape保存的资源。要在同一计算图中计算多个梯度,可创建一个 persistent=True 的梯度磁带,这样便可以对 gradient 方法进行多次调用。最后用del显式方式删除梯度磁带,例如:

梯度磁带会自动监视 tf.Variable,但不会监视 tf.Tensor。如果无意中将变量(tf.Variable)变为常量(tf.Tensor)(如tf.Variable 与一个tf.Tensor相加,其和就变成常量了),梯度磁带将不再监控tf.Tensor。 为避免这种情况,可使用 Variable.assign 给tf.Variable赋值,示例如下:

运行结果如下:
tf.Tensor(1.0, shape=(), dtype=float32)
None

如果在函数的计算中有TensorFlow 之外的计算(如使用NumPy算法),则梯度带将无法记录梯度路径;同时,如果变量的值为整数,则无法求导。例如:

2.9 损失函数

TensorFlow内置很多损失函数(又称为目标函数),在tf.keras模块中有很多,这里仅列出一些常用的模块及功能说明,且有些还容易搞混,所以这里就此做进一步说明。
用于分类的损失函数如下所示。
• binary_crossentropy(二元交叉熵):用于二分类,参数from_logits说明预测值是否是logits(logits没有使用sigmoid激活函数全连接的输出),类实现形式为 BinaryCrossentropy
• categorical_crossentropy(类别交叉熵):用于多分类,要求标签(label)为独热(One-Hot)编码(如:[0,1,0]),类实现形式为CategoricalCrossentropy
• sparse_categorical_crossentropy(稀疏类别交叉熵):用于多分类,要求label为序号编码形式(一般取整数),类实现形式为 SparseCategoricalCrossentropy
• hinge(合页损失函数): 用于二分类,最著名的应用是作为支持向量机SVM的损失函数,类实现形式为 Hinge。
用于回归的损失函数如下所示。
• mean_squared_error(平方差误差损失):用于回归,简写为 mse, 类实现形式为 MeanSquaredError 和 MSE。
• mean_absolute_error (绝对值误差损失):用于回归,简写为 mae, 类实现形式为 MeanAbsoluteError 和 MAE。
• mean_absolute_percentage_error (平均百分比误差损失):用于回归,简写为 mape, 类实现形式为 MeanAbsolutePercentageError 和 MAPE。
• Huber(Huber损失):只有类实现形式,用于回归,介于mse和mae之间,对异常值进行比较。鲁棒,相对mse有一定的优势。

2.10优化器

优化器(optimizer)在机器学习中占有重要地位,它是优化目标函数的核心算法。在进行低阶编程时,我们通常使用apply_gradients方法把优化器传入变量和对应梯度,从而对给定变量进行迭代,或者直接使用minimize方法对目标函数进行迭代优化。
在实现中高级阶API编程时,我们往往会在编译时将优化器传入Keras的Model,通过调用model.fit实现对Loss的的迭代优化。优化器与tf.Variable一样,一般需要在@tf.function外创建。
tf.keras.optimizers和tf.optimizers 完全相同,tf.optimizers.SGD即tf.keras.optimizers.SGD。最常用的优化器列举如下。
1.随机梯度下降法(Stochastic Gradient Descent,SGD)
tf.keras.optimizers.SGD默认参数为纯SGD, 其语法格式为:

设置momentum参数不为0,SGD实际上变成SGDM, 如果仅考虑一阶动量, 设置 nesterov为True后SGDM变成NAG,即 Nesterov Accelerated Gradient,在计算梯度时计算的是向前走一步所在位置的梯度。
2.自适应矩估计 (Adaptive Moment Estimation,Adam)
tf.keras.optimizers.Adam, 其语法格式为:

它是自适应(所谓自适应主要是自适应学习率)优化器的典型代表,同时考虑了一阶动量和二阶动量,可以看成是在RMSprop的基础上进一步考虑了一阶动量。自适应类的优化器还有Adagrad、RMSprop、Adadelta等。

2.11 TensorFlow 2.0实现回归实例

在1.7节,我们用纯NumPy实现一个回归实例,这里我们使用TensorFlow 2.0中的自动微分来实现。数据一样,目标一样,但实现方法不一样,大家可以进行比较。
1. 生成数据
这些内容与1.7节的内容一样,只是需要把NumPy数据转换TensorFlow格式的张量或变量。

运行结果如图2-8所示。

图2-8 回归使用的数据图形
2. 把numpy转换为TensorFlow 2.0格式的张量或变量

3.定义回归模型

4.自定义损失函数

5.使用自动微分及自定义梯度更新方法

对模型进行训练。

比较拟合程度。

使用TensorFlow实现回归问题的拟合结果如图2-9所示。
图2-9 使用TensorFlow实现回归问题的拟合结果
6.使用自动微分及优化器
虽然上述梯度计算采用自动微分的方法,但梯度更新采用自定义方式,如果损失函数比较复杂,自定义梯度难度会徒增,是否有更好的方法呢?使用优化器可以轻松实现自动微分、自动梯度更新,而这正是方向传播的核心内容。
使用优化器(optimizer)的常见方法有3种,介绍如下。
1)使用apply_gradients方法:先计算损失函数关于模型变量的导数,然后将求出的导数值传入优化器,使用优化器的 apply_gradients 方法迭代更新模型参数以最小化损失函数。
2)用minimize方法:minimize(loss, var_list) 计算loss所涉及的变量(tf.Varialble)组成的列表或者元组,即tf.trainable_variables(),它是compute_gradients()和apply_gradients()这两个方法的简单组合。用代码可表示如下:

3)在编译时将优化器传入Keras的Model,通过调用model.fit实现对loss的的迭代优化。具体实例可参考本书3.3节。
下面,我们先来了解如何使用优化器的apply_gradients方法。

训练模型。

使用自动微分的拟合结果如图2-10所示。

图2-10 使用自动微分的拟合结果
由此可见,使用优化器不但可以使程序更简洁,也可以使模型更高效!
接下来,我们使用优化器的minimize(loss, var_list) 方法更新参数。

训练模型。

使用优化器的拟合结果如图2-11所示。
图2-11 使用优化器的拟合结果
综上,使用优化器的minimize方法更简洁。

2.12 GPU加速

深度学习的训练过程一般会非常耗时,多则通常需要几个小时或者几天来训练一个模型。如果数据量巨大、模型复杂,甚至需要几十天来训练一个模型。一般情况下,训练模型的时间主要耗费在准备数据和参数迭代上。当准备数据成为训练模型的主要瓶颈时,我们可以使用多线程来加速。当参数迭代成为训练模型的主要瓶颈时,我们可以使用系统的GPU(或TPU)资源来加速。
如果没有额外的标注,TensorFlow将自动决定是使用CPU还是GPU。 如果有必要,TensorFlow也可以在CPU和GPU内存之间复制张量。
查看系统的GPU资源以及张量的存放位置(系统内存还是GPU):

在必要时,我们可以显式地指定希望的常量的存储位置以及是使用CPU还是GPU进行科学计算。如果没有显式地指定,TensorFlow将自动决定在哪个设备上执行,并且把需要的张量复制到对应的设备上。但是,在需要的时候,我们也可以用tf.device这个上下文管理器来指定设备。下面通过一个列子来说明。

从上面的日志中我们可以发现,在数据量不是很大的情况下(比如矩阵大小在100100以内),使用GPU运算并没有太多的优势,这是因为前期的张量准备以及复制耗费了太多时间。但是随着数据量的逐步增加,GPU的运算速度的优势逐步体现出来,在我们的数据是一个10000 * 10000 的矩阵的时候,CPU和GPU的运算速度会有1000倍的差距。我们把数据罗列在图表上会更加直观:

CPU与GPU耗时比较图如图2-12所示。
图2-12 CPU与GPU耗时比较图

2.13小结

首先简单介绍TensorFlow 2+版本的安装,然后介绍了TensorFlow的一些基本概念,如张量、变量以及计算图的几种方式等,同时与TensorFlow 1+版本的对应概念进行了比较。随后,对TensorFlow几个核心内容,如自动微分、损失函数、优化器等进行说明。为帮助大家更好地理解这些概念和方法,最后通过几个相关实例进行详细说明。

本书代码及数据下载百度盘

提取码:xscp
文件大小约:1.3G

第一部分 TensorFlow基础

第1章 NumPy基础

第2章 TensorFlow张量及其计算

2.1 安装配置
2.2 层次架构
2.3 张量
2.4 变量
2.5 NumPy与tf.tensor比较
2.6 动态计算图
2.7 自动图
2.8 自动微分
2.9 损失函数
2.10优化器
2.11 TensorFlow 2.0实现回归实例
2.12 GPU加速
2.13小结

第3章 TensorFlow构建模型的方法

3.1 利用低阶API构建模型
3.2 利用中阶API构建模型
3.3利用高阶API构建模型
3.4 小结<

第4章 TensorFlow数据处理

4.1 tf.data简介
4.2 构建数据集的常用方法
4.3 如何生成自己的TFRecord格式数据
4.4 数据增强方法
4.5 小结

第5章 可视化

5.1 matplotlib
5.2 pyecharts
5.3 TensorBoard
5.4小结

第二部分 深度学习基础

第6章 机器学习基础

6.1 机器学习的一般流程
6.2 监督学习
6.3 无监督学习
6.4 数据预处理
6.5 机器学习实例
6.6 小结

第7章 神经网络基础

7.1 单层神经网络

7.2 多层神经网络

7.3 激活函数

7.4 正向和反向传播

7.5 解决过拟合问题

7.6 选择优化算法

7.7 使用tf.keras构建神经网络

7.8 小结

第8章 视觉处理基础

8.1 从全连接层到卷积层

8.2 卷积层

8.3 池化层

8.4 现代经典网络

8.5 卷积神经网络分类实例

8.6小结

第9章 自然语言处理基础

9.1 语言模型到循环神经网络

9.2 正向传播与随时间反向传播

9.3 现代循环神经网络

9.4 几种特殊架构

9.5 RNN应用场景

9.6 循环神经网络实践

9.7小结

第10 章 注意力机制

10.2 带注意力机制编码器-解码器架构

10.3 可视化Transformer架构

10.4 用TensorFlow实现Transformer

10.5 小结

第11 章 目标检测

11.1目标检测及主要挑战

11.2 优化候选框的几种算法

11.3目标检测典型算法

11.4 小结

第12章 生成式深度学习

12.1 用变分自编码器生成图像

12.2 GAN简介

12.3用GAN生成图像

12.4 VAE与GAN的异同

12.5 CGAN

12.6 提升GAN训练效果的一些技巧

12.7 小结

第三部分 深度学习实践

第13章 实战生成式模型

13.1 Deep Dream模型

13.2 风格迁移

13.3 小结

第14章 目标检测实例

14.1 数据集简介

14.2 准备数据

14.3 训练模型

14.4 测试模型

14.5 小结

第15 章 人脸检测与识别

15.1 人脸识别简介

15.2 项目概况

15.3 项目详细实施步骤

15.4 小结

第16章 文本检测与识别实例

16.1 项目架构说明

16.2 项目实施步骤

16.3小结

第17章 基于Transformer的对话实例

17.1 数据预处理

17.2 构建注意力模块

17.3 构建Transformer

17.4 定义损失函数

17.5 初始化并编译模型

17.6 测试评估模型

17.7 小结

第18章 Transformer处理图像实例

18.1导入数据

18.2 预处理数据

18.3构建模型

18.4编译、训练模型

18.5可视化运行结果

18.6 小结

第四部分 强化学习

第19章 强化学习基础

19.1 强化学习基础

19.2 时序差分算法

19.3 Q-Leaning算法

19.4 SARSA 算法

19.5 DSN算法原理

19.6 小结

第20章 强化学习实践

20.1 SARSA算法实例

20.2 Q-Learning算法实例

20.3 用TensorFlow实现 DQN算法

20.4 小结

附录A TensorFlow-gpu 2+升级安装配置

附录B TensorFlow1.x升级到TensorFlow 2.x


第7章 矩阵分解的应用

矩阵分解的过程是对数据的压缩,期间通过点积运算,把数据之间的关系,通过几个更简单的矩阵来呈现,因此可以得到更加精炼的表示并去除掉一些干扰和异常。其中SVD在信息检索(隐性语义索引)、图像压缩、推荐系统、目标检测、金融服务等领域都有应用

7.1 SVD应用于推荐系统

推荐系统中往往需要计算物品之间(或用户之间)的相似度,然后把相似度最大值给用没有评分的物品,以此作为对用户对新物品的喜爱程度。
但物品与用户构成的矩阵通常较大而且稀疏,直接基于原矩阵计算物品或用户相似度,效率和效果都不很理想,所以很多专家开始寻找其他方法,其中SVD或改进版本,经实践检验,不失为一个较好方法。具体做法如下:
(1)假设物品-用户矩阵为A

(2)对A进行SVD

(3)定义计算物品之间相似度的函数

(4)计算压缩后的V(该矩阵对应物品信息),并计算物品2与其它物品之间的相似度。
A=U\Sigma V^T 可得V=A^T U\Sigma

运行结果:
物品2与物品0的相似度:0.9999996158310511
物品2与物品1的相似度:0.999999886642839
物品2与物品3的相似度:0.5092740977256969
物品2与物品4的相似度:0.5100370458545834
物品2与物品5的相似度:0.5109165651201468
物品2与物品6的相似度:0.5053846892190899
物品2与物品7的相似度:0.999381759332992
物品2与物品8的相似度:0.9999915344456692
物品2与物品9的相似度:0.5111029802612934
物品2与物品10的相似度:0.5691678300499748

采用SVD方法计算物品之间的相似度简单高效,特别适合一些大又稀疏的矩阵。如果要计算用户之间的相似度,可以利用U矩阵。

7.2 SVD应用于图像压缩

利用公式:
A_{m\times n}=U_{m\times m} \Sigma_{m\times n} V_{n\times n}^T\approx U_{m\times k} \Sigma_{k\times k} V_{k\times n}^T
对原图进行压缩,选择k\lq min(m,n)实现对图像的压缩。具体步骤如下:
(1)导入需要的库

(2)可视化原图(大家可用自己的图替换)

运行结果:
(414, 500, 3)

(3)对图像进行压缩
该图片的通道数为3,说明是一张彩色图像。对彩色图像进行SVD时,分别对R、G、B三个通道进行。

(4)运行

运行结果

这里取前10个奇异值,得到保留原图像的信息量达98%。

7.3 SVD目标检测中

在Fast R-CNN目标检测或图像处理中,输入或输出图像像素一般较大,而且要处理的图像很多,如果用全连接,则参数量将非常大。这时我们可以采用矩阵压缩技术,这种压缩,既能保留原图像的巨大部分信息,又可大大降维矩阵的维度,从而大大降低计算量。如下图所示。
其中k\ll min(n,m)
对y=Wx中权重参数矩阵W用UΣV^T替换,即:
y=Wx\approx U\Sigma_k V^T x=U(\Sigma_k V^T x)
从上式可以很容易的看出,用两个全连接层近似替代一个全连接层,两个全连接层的参数分别为\Sigma_k V^T和U,如下图所示:
当k比较小时,如k\ll min(n,m),替换后的参数将由m\times n降低到k(m+n),计算量也降低不少。

SVD应用非常广泛,如利用视频中背景与前景变化情况,可用SVD从视频中删除背景
在NLP领域,可用上下词生成的共现矩阵,实现Wword Embedding功能等等。