第8章 生成式深度学习


深度学习不仅在于其强大的学习能力,更在于它的创新能力。我们通过构建判别模型来提升模型的学习能力,通过构建生成模型来发挥其创新能力。判别模型通常利用训练样本训练模型,然后利用该模型,对新样本x,进行判别或预测。而生成模型正好反过来,根据一些规则y,来生成新样本x。
生成式模型很多,本章主要介绍常用的两种:变分自动编码器(VAE)和生成式对抗网络(GAN)及其变种。虽然两者都是生成模型,并且通过各自的生成能力展现其强大的创新能力,但他们在具体实现上有所不同。GAN基于博弈论,目的是找到达到纳什均衡的判别器网络和生成器网络。而VAE基本根植贝叶斯推理,其目标是潜在地建模,从模型中采样新的数据。
本章主要介绍多种生成式网络,具体内容如下:
用变分自编码器生成图像
GAN简介
如何用GAN生成图像
比较VAE与GAN的异同
CGAN、DCGAN简介

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

变分自编码器是自编码器的改进版本,自编码器是一种无监督学习,但它无法产生新的内容,变分自编码器对其潜在空间进行拓展,使其满足正态分布,情况就大不一样了。

8.1.1 自编码器

自编码器是通过对输入X进行编码后得到一个低维的向量z,然后根据这个向量还原出输入X。通过对比X与X ̃的误差,再利用神经网络去训练使得误差逐渐减小,从而达到非监督学习的目的。图8-1 为自编码器的架构图:

图8-1 自编码器架构图
自编码器因不能随意产生合理的潜在变量,从而导致它无法产生新的内容。因为潜在变量Z都是编码器从原始图片中产生的。为解决这一问题,人们对潜在空间Z(潜在变量对应的空间)增加一些约束,使Z满足正态分布,由此就出现了VAE模型,VAE对编码器添加约束,就是强迫它产生服从单位正态分布的潜在变量。正是这种约束,把VAE和自编码器区分开来。

8.1.2变分自编码器

变分自编码器关键一点就是增加一个对潜在空间Z的正态分布约束,如何确定这个正态分布就成主要目标,我们知道要确定正态分布,只要确定其两个参数均值u和标准差σ。那么如何确定u、σ?用一般的方法或估计比较麻烦效果也不好,人们发现用神经网络去拟合,简单效果也不错。图8-2 为AVE的架构图。

图8-2 AVE架构图
在图8-2中,模块①的功能把输入样本X通过编码器输出两个m维向量(mu、log_var),这两个向量是潜在空间(假设满足正态分布)的两个参数(相当于均值和方差)。那么如何从这个潜在空间采用一个点Z?
这里假设潜在正态分布能生成输入图像,从标准正态分布N(0,I)中采样一个ε(模块②的功能),然后使
Z=mu+ exp⁡(log_var)*ε (8.1)
这也是模块③的主要功能。
Z是从潜在空间抽取的一个向量,Z通过解码器生成一个样本X ̃,这是模块④的功能。
这里ε是随机采样的,这就可保证潜在空间的连续性、良好的结构性。而这些特性使得潜在空间的每个方向都表示数据中有意义的变化方向。
以上这些步骤构成整个网络的前向传播过程,反向传播如何进行?要确定反向传播就涉及到损失函数了,损失函数是衡量模型优劣的主要指标。这里我们需要从以下两个方面进行衡量。
(1)生成的新图像与原图像的相似度;
(2)隐含空间的分布与正态分布的相似度。
度量图像的相似度一般采用交叉熵(如nn.BCELoss),度量两个分布的相似度一般采用KL散度(Kullback-Leibler divergence)。这两个度量的和构成了整个模型的损失函数。
以下是损失函数的具体代码,AVE损失函数的推导过程,有兴趣的读者可参考原论文:https://arxiv.org/pdf/1606.05908.pdf

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

前面我们介绍了AVE的架构和原理,至此对AVE的“蓝图”就有了大致了解,如何实现这个蓝图?这节我们将结合代码,用Pytorch实现AVE。此外,还包括在实现过程中需要注意的一些问题,为便于说明起见,数据集采用MNIST,整个网络结构如图8-3所示。

图8-3 AVE网络结构图
首先,我们简单介绍一下实现的具体步骤,然后,结合代码详细说明,如何用Pytorch一步步实现AVE。具体步骤如下:
(1)导入必要的包

(2)定义一些超参数

(3)对数据集进行预处理,如转换为Tensor,把数据集转换为循环、可批量加载的数据集;

(4)构建AVE模型,主要由encode和decode两部分组成;

(5)选择GPU及优化器

(6)训练模型,同时保存原图像与随机生成的图像;

(7)展示原图像及重构图像

这是迭代30次的结果

图8-4 AVE构建图片
图8-4中,奇数列为原图像,偶数列为原图像重构的图像。从这个结果可以看出重构图像效果还不错。图8-5为由潜在空间通过解码器生成的新图像,这个图像效果也不错。
(8)显示由潜在空间点Z生成的新图像

图8-5 AVE新图片
这里构建网络主要用全连接层,有兴趣的读者,可以把卷积层,如果编码层使用卷积层(如nn.Conv2d),解码器需要使用反卷积层(nn. ConvTranspose2d)。接下来我们介绍生成式对抗网络,并用该网络生成新数字,其效果将好于AVE生成的数字。

8.2 GAN简介

上节我们介绍了基于自动编码器的AVE,根据这个网络可以生成新的图像。这节我们将介绍另一种生成式网络,它是基于博弈论的,所以又称为生成式对抗网络(Generative adversarial nets,GAN)。它是2014年由Ian Goodfellow提出的,它要解决的问题是如何从训练样本中学习出新样本,训练样本是图片就生成新图片,训练样本是文章就输出新文章等等。
GAN既不依赖标签来优化,也不是根据对结果奖惩来调整参数。它是依据生成器和判别器之间的博弈来不断优化。打个不一定很恰当的比喻,就像一台验钞机和一台制造假币的机器之间的博弈,两者不断博弈,博弈的结果假币越来越像真币,直到验钞机无法识别一张货币是假币还是真币为止。这样说,还是有点抽象,接下来我们将从多个侧面进行说明。

8.2.1 GAN架构

VAE利用潜在空间,可以生成连续的新图像,不过因损失函数采用像素间的距离,所以图像有点模糊。能否生成更清晰的新图像呢?可以的,这里我们用GAN替换VAE的潜在空间,它能够迫使生成图像与真实图像在统计上几乎无法区别的逼真合成图像。
GAN的直观理解,可以想象一个名画伪造者想伪造一幅达芬奇的画作,开始时,伪造者技术不精,但他,将自己的一些赝品和达芬奇的作品混在一起,请一个艺术商人对每一幅画进行真实性评估,并向伪造者反馈,告诉他哪些看起来像真迹、哪些看起来不像真迹。
伪造者根据这些反馈,改进自己的赝品。随着时间的推移,伪造者技能越来越高,艺术商人也变得越来越擅长找出赝品。最后,他们手上就拥有了一些非常逼真的赝品。
这就是GAN的基本原理。这里有两个角色,一个是伪造者,另一个是技术鉴赏者。他们训练的目的都是打败对方。
因此,GAN从网络的角度来看,它由两部分组成。
(1)生成器网络:它一个潜在空间的随机向量作为输入,并将其解码为一张合成图像。
(2)判别器网络:以一张图像(真实的或合成的均可)作为输入,并预测该图像来自训练集还是来自生成器网络。图8-6 为其架构图。

图8-6 GAN架构图
如何不断提升判别器辨别是非的能力?如何使生成的图片越来越像真图片?这些都通过控制它们各自的损失函数来控制。
训练结束后,生成器能够将输入空间中的任何点转换为一张可信图像。与VAE不同的是,这个潜空间无法保证带连续性或有特殊含义的结构。
GAN的优化过程不像通常的求损失函数的最小值,而是保持生成与判别两股力量的动态平衡。因此,其训练过程要比一般神经网络难很多。

8.2.2 GAN的损失函数

从GAN的架构图(图8-6)可知,控制生成器或判别器的关键是损失函数,如何定义损失函数成为整个GAN的关键。我们的目标很明确,既要不断提升判断器辨别是非或真假的能力,又要不断提升生成器不断提升图片质量,使判别器越来越难判别。这些目标如何用程序体现?损失函数就能充分说明。
为了达到判别器的目标,其损失函数既要考虑识别真图片能力,又要考虑识别假图片能力,而不能只考虑一方面,故判别器的损失函数为两者的和,具体代码如下:
D表示判别器,G为生成器,real_labels,fake_labels分别表示真图片标签、假图片标签。images是真图片,z是从潜在空间随机采样的向量,通过生成器得到假图片。

生成器的损失函数如何定义,才能使其越来越向真图片靠近?以真图片为标杆或标签即可。具体代码如下:

8.3用GAN生成图像

为便于说明GAN的关键环节,这里我们弱化了网络和数据集的复杂度。数据集为MNIST、网络用全连接层。后续我们将用一些卷积层的实例来说明。

8.3.1判别器

获取数据,导入模块基本与AVE的类似,这里就不展开来说,详细内容大家可参考char-08代码模块。
定义判别器网络结构,这里使用LeakyReLU为激活函数,输出一个节点并经过Sigmoid后输出,用于真假二分类。

8.3.2 生成器

生成器与AVE的生成器类似,不同的地方是输出为nn.tanh,使用nn.tanh将使数据分布在[-1,1]之间。其输入是潜在空间的向量z,输出维度与真图片相同。

8.3.3 训练模型

8.3.4 可视化结果

可视化每次由生成器得到假图片,即潜在向量z通过生成器得到的图片。

图8-7 GAN的新图片
图8-7明显好于图8-5。AVE生成图片主要依据原图片与新图片的交叉熵,而GAN真假图片的交叉熵,同时还兼顾了不断提升判别器和生成器本身的性能上。

8.4 VAE与GAN的异同

VAE适合于学习具有良好结构的潜在空间,潜在空间有比较好的连续性,其中存在一些有特定意义的方向。VAE能够捕捉到图像的结构变化(倾斜角度、圈的位置、形状变化、表情变化等)。这也是VAE的一个好处,它有显式的分布,能够容易地可视化图像的分布,具体如图8-8所示。

图8-8 AVE得到的数据流形分布图
GAN生成的潜在空间可能没有良好结构,但GAN生成的图像一般比VAE的更清晰。

第7章 自然语言处理基础

7.7 Pytorch实现词性判别

我们知道每一个词都有词性,如train这个单词,可表示火车或训练等意思,具体表示为哪种词性,跟这个词所处的环境或上下文密切相关。要根据上下文来确定词性,正是循环网络擅长的事,因循环网络,尤其是LSTM或GRU网络,具有记忆功能。
这节将使用LSTM网络实现词性判别。

7.7.1 词性判别主要步骤

如何用LSTM对一句话里的各词进行词性标注?需要采用哪些步骤?这些问题就是这节将涉及的问题。用LSTM实现词性标注,我们可以采用以下步骤:
(1)实现词的向量化
假设有两个句子,作为训练数据,这两个句子的每个单词都已标好词性。当然我们不能直接把这两个语句直接输入LSTM模型,输入前需要把每个语句的单词向量化。假设这个句子共有5个单词,通过单词向量化后,就可得到序列[V_1, V_2, V_3, V_4, V_5],其中V_i表示第i个单词对应的向量。如何实现词的向量化?我们可以直接利用nn.Embedding层即可。当然在使用该层之前,需要把每句话对应单词或词性用整数表示。
(2)构建网络
词向量化之后,需要构建一个网络来训练,可以构建一个只有三层的网络,第一层为词嵌入层,第二层为LSTM层,最后一层用于词性分类的全连接层。
以下用Pytorch实现这些步骤。

7.7.2 数据预处理

(1)定义语句及词性
训练数据有两个语句,定义好每个词对应的词性。测试数据为一句话,没有指定词性。

(2)构建每个单词的索引字典
把每个单词用一个整数表示,将它们放在一个字典里。词性也如此。

手工设置词性的索引字典。
tag_to_ix = {"DET": 0, "NN": 1, "V": 2}

7.7.3 构建网络

构建训练网络,共三层,分别为嵌入层、LSTM层、全连接层。

其中有一个nn.Embedding(vocab_size, embed_dim)类,它是Module类的子类,这里它接受最重要的两个初始化参数:词汇量大小,每个词汇向量表示的向量维度。Embedding类返回的是一个形状为[每句词个数,词维度]的矩阵。nn.LSTM层的输入形状为(序列长度,批量大小,输入的大小),序列长度就是时间步序列长度,这个长度是可变的。F.log_softmax()执行的是一个Softmax回归的对数。
把数据转换为模型要求的格式,即把输入数据需要转换为torch.LongTensor张量。

7.7.4 训练网络

(1)定义几个超参数、实例化模型,选择损失函数、优化器等

(2)简单运行一次

['The', 'cat', 'ate', 'the', 'fish']
tensor([0, 1, 2, 3, 4])
tensor([[-1.4376, -0.9836, -0.9453],
[-1.4421, -0.9714, -0.9545],
[-1.4725, -0.8993, -1.0112],
[-1.4655, -0.9178, -0.9953],
[-1.4631, -0.9221, -0.9921]], grad_fn=)
(tensor([-0.9453, -0.9545, -0.8993, -0.9178, -0.9221], grad_fn=),
tensor([2, 2, 1, 1, 1]))
显然,这个结果不很理想。而下面我们循环多次训练该模型,精度将大大提升。
(3)训练模型

['The', 'cat', 'ate', 'the', 'fish']
tensor([[-4.9405e-02, -6.8691e+00, -3.0541e+00],
[-9.7177e+00, -7.2770e-03, -4.9350e+00],
[-3.0174e+00, -4.4508e+00, -6.2511e-02],
[-1.6383e-02, -1.0208e+01, -4.1219e+00],
[-9.7806e+00, -8.2493e-04, -7.1716e+00]], grad_fn=)
(tensor([-0.0494, -0.0073, -0.0625, -0.0164, -0.0008], grad_fn=),
tensor([0, 1, 2, 0, 1]))
这个精度为100%

7.7.5 测试模型

这里我们用另一句话,来测试这个模型

['They', 'ate', 'the', 'fish']
tensor([5, 2, 3, 4])
tensor([[-7.6594e+00, -5.2700e-03, -5.3424e+00],
[-2.6831e+00, -5.2537e+00, -7.6429e-02],
[-1.4973e-02, -1.0440e+01, -4.2110e+00],
[-9.7853e+00, -8.3971e-04, -7.1522e+00]], grad_fn=)
(tensor([-0.0053, -0.0764, -0.0150, -0.0008], grad_fn=),
tensor([1, 2, 0, 1]))
测试精度达到100%

7.8 用LSTM预测股票行情

这里采用沪深300指数数据,时间跨度为2010-10-10至今,选择每天最高价格。假设当天最高价依赖当天的前n(如30)天的沪深300的最高价。用LSTM模型来捕捉最高价的时序信息,通过训练模型,使之学会用前n天的最高价,判断当天的最高价(作为训练的标签值)。

7.8.1 导入数据

这里使用tushare来下载沪深300指数数据。可以用pip 安装tushare。

7.8.2 数据概览

(1)查看下载数据的字段、统计信息等。

图7-15 沪深300指数统计信息
从图7-15可知,共有2295条数据。
(2)可视化最高价数据

图7-16 可视化最高价

7.8.3 预处理数据

(1)生成训练数据

(2)规范化数据
#对数据进行预处理,规范化及转换为Tensor
df_numpy = np.array(df)

df_numpy_mean = np.mean(df_numpy)
df_numpy_std = np.std(df_numpy)

df_numpy = (df_numpy - df_numpy_mean) / df_numpy_std
df_tensor = torch.Tensor(df_numpy)

trainset = mytrainset(df_tensor)
trainloader = DataLoader(trainset, batch_size=batch_size, shuffle=False)

7.8.4 定义模型

这里使用LSTM网络,LSTM输出到一个全连接层。

7.8.5 训练模型

图7-17 batch-size=20的损失值变化情况
图7-17为batch-size=20时,损失值与迭代次数之间的关系,开始时振幅有点大,后面逐渐趋于平稳。如果batch-size变小,振幅可能更大。

7.8.6 测试模型

(1)使用测试数据,验证模型

(2)查看预测数据与源数据

图7-18 放大后预测数据与源数据比较
从图7-18 来看,预测结果还是不错的。

第6章 视觉处理基础

传统神经网络层之间都采用全连接方式,这种连接方式,如果层数较多,输入又是高维数据,其参数数量可能是一个天文数字。比如训练一张1000*1000像素的灰色图片,输入节点数就是1000*1000,如果隐含层节点是100,那么输入层到隐含层间的权重矩阵就是 1000000*100!如果还要增加隐含层,还要进行反向传播,那结果可想而知。这还不是全部,采用全连接方式还容易导致过拟合。
因此,为更有效处理像图片、视频、音频、自然语言等大数据,必须另辟蹊径。经过多年不懈努力,人们终于找到了一些有效方法或工具。其中卷积神经网络、循环神经网络就是典型代表。接下来我们将介绍卷积神经网络,下一章将介绍循环神经网络。
那卷积神经网络是如何解决天量参数、过拟合等问题的呢?卷积神经网络这么神奇,如何用代码实现?这章就是为解决这些问题而设的,本章主要内容为:
卷积神经网络简介
卷积定义
卷积运算
卷积层
池化层
现代经典网络架构
实例:用TensorFlow实现一个卷积神经网络

6.1卷积神经网络简介

卷积神经网路(Convolutional Neural Network, CNN)是一种前馈神经网络,对于CNN最早可以追溯到1986年BP算法的提出。1989年LeCun将其用到多层神经网络中,直到1998年LeCun提出LeNet-5模型,神经网络的雏形基本形成。在接下来近十年的时间里,卷积神经网络的相关研究处于低谷,原因有两个:一是研究人员意识到多层神经网络在进行BP训练时的计算量极大,当时的硬件计算能力完全不可能实现;二是包括SVM在内的浅层机器学习算法也开始崭露头角。
2006年,Hinton一鸣惊人,在《科学》上发表文章,CNN再度觉醒,并取得长足发展。2012年,ImageNet大赛上CNN夺冠。2014年,谷歌研发出20层的VGG模型。同年,DeepFace、DeepID模型横空出世,直接将LFW数据库上的人脸识别、人脸认证的正确率刷到99.75%,已超越人类平均水平。
卷积神经网路由一个或多个卷积层和顶端的全连通层(对应经典的神经网路)组成,同时也包括关联权重和池化层(pooling layer)等。图6-1就是一个卷积神经网络架构。

图6-1 卷积神经网络示意图
与其他深度学习结构相比,卷积神经网路在图像和语音识别方面能够给出更好的结果。这一模型也可以使用反向传播算法进行训练。相比其他深度、前馈神经网路,卷积神经网路用更少参数,却能获得更高性能。
图6-1为卷积神经网络的一般结构,其中包括卷积神经网络的常用层,如卷积层、池化层、全连接层和输出层;有些还包括其他层,如正则化层、高级层等。接下来我们就各层的结构、原理等进行详细说明。
图6-1是用一个比较简单的卷积神经网络对手写输入数据进行分类,由卷积层(Conv2d)、池化层(MaxPool2d)和全连接层(Linear)叠加而成。下面我们先用代码定义这个卷积神经网络,然后,介绍各部分的定义及原理。

6.2卷积层

卷积层是卷积神经网络的核心层,而卷积(Convolution)又是卷积层的核心。卷积我们直观的理解,就是两个函数的一种运算,这种运算称为卷积运算。这样说或许比较抽象,我们还是先抛开复杂概念,先从具体实例开始吧。图6-2 就是一个简单的二维空间卷积运算示例,虽然简单,但却包含了卷积的核心内容。

图6-2 在二维空间上的一个卷积运算
在图6-2中,输入和卷积核都是张量,卷积运算就是用卷积分别乘以输入张量中的每个元素,然后输出一个代表每个输入信息的张量。其中卷积核(kernel)又称为权重过滤器或简称过滤器(filter)。接下来我们把输入、卷积核推广到更高维空间上,输入由2x2矩阵,拓展为5x5矩阵,卷积核由一个标量拓展为一个3x3矩阵,如图6-3。这时该如何进行卷积呢?

图6-3 卷积神经网络卷积运算,生成右边矩阵中第1行第1列的数据
用卷积核中每个元素,乘以对应输入矩阵中的对应元素,这点还是一样,但输入张量为5x5矩阵,而卷积核为3x3矩阵,所以这里首先就要解决一个如何对应的问题,这个问题解决了,这个推广也就完成了。把卷积核作为在输入矩阵上一个移动窗口,对应关系就迎刃而解。
卷积核如何确定?卷积核如何在输入矩阵中移动?移动过程中出现超越边界如何处理?这种因移动可能带来的问题,接下来将进行说明。

6.2.1 卷积核

卷积核,从这个名字可以看出它的重要性,它是整个卷积过程的核心。比较简单的卷积核或过滤器有Horizontalfilter、Verticalfilter、Sobel filter等。这些过滤器能够检测图像的水平边缘、垂直边缘、增强图片中心区域权重等。过滤器的具体作用,我们通过以下一些图来说明。
(1)垂直边缘检测

图6-4 过滤器对垂直边缘的检测
这个过滤器是3x3矩阵(注,过滤器一般是奇数阶矩阵),其特点是有值的是第1列和第3列,第2列为0。经过这个过滤器作用后,就把原数据垂直边缘检测出来了。
(2)水平边缘检测

图6-5 水平过滤器检测水平边缘示意图
这个过滤器也是3x3矩阵,其特点是有值的是第1行和第3行,第2行为0。经过这个过滤器作用后,就把原数据水平边缘检测出来了。
(3)过滤器对图像水平边缘检测、垂直边缘检测的效果图

图6-6过滤器对图像水平边缘检测、垂直边缘检测后的效果图
以上这些过滤器是比较简单的,在深度学习中,过滤器的作用不仅在于检测垂直边缘、水平边缘等,还需要检测其他边缘特征。
过滤器如何确定呢?过滤器类似于标准神经网络中的权重矩阵W,W需要通过梯度下降算法反复迭代求得。同样,在深度学习学习中,过滤器也是需要通过模型训练来得到。卷积神经网络主要目的就是计算出这些filter的数值。确定得到了这些filter后,卷积神经网络的浅层网络也就实现了对图片所有边缘特征的检测。
这节简单说明了卷积核的生成方式及作用。假设卷积核已确定,卷积核如何对输入数据进行卷积运算呢?这将在下节进行介绍。

6.2.2步幅

如何实现对输入数据进行卷积运算?回答这个问题之前,我们先回顾一下图6-3。在图6-3的左边的窗口中,左上方有个小窗口,这个小窗口实际上就是卷积核,其中x后面的值就是卷积核的值。如第1行为:x1、x0、x1对应卷积核的第1行[1 0 1]。右边窗口中这个4是如何得到的呢?就是5x5矩阵中由前3行、前3列构成的矩阵各元素乘以卷积核中对应位置的值,然后累加得到的。即:1x1+1x0+1x1+0x0+1x1+1x0+0x1+0x0+1x1=4,右边矩阵中第1行第2列的值如何得到呢?我们只要把左图中小窗口往右移动一格,然后,进行卷积运算;第1行第3列,如此类推;第2行、第3行的值,只要把左边的小窗口往下移动一格,然后再往右即可。看到这里,如果还不很清楚,没关系,看图6-7就一目了然。

图6-7卷积神经网络卷积运算,生成右边矩阵中第2行第2列的数据
小窗口(实际上就是卷积核或过滤器)在左边窗口中每次移动的格数(无论是自左向右移动,或自上向下移动)称为步幅(strides),在图像中就是跳过的像素个数。上面小窗口每次只移动一格,故参数strides=1。这个参数也可以是2或3等数。如果是2,每次移动时就跳2格或2个像素,如下图6-8所示。

图6-8 strides=2 示意图
在小窗口移动过程中,其值始终是不变的,都是卷积核的值。换一句话来说,卷积核的值,在整个过程中都是共享的,所以又把卷积核的值称为共享变量。卷积神经网络采用参数共享的方法大大降低了参数的数量。
参数strides是卷积神经网络中的一个重要参数,在用PyTorch具体实现时,strides参数格式为单个整数或两个整数的元组(分别表示在height和width维度上的值)。
在图6-8中,小窗口如果继续往右移动2格,卷积核窗口部分在输入矩阵之外,如下图6-9。此时,该如何处理呢?具体处理方法就涉及到下节要讲的内容--填充(padding)。

图6-9小窗口移动输入矩阵外

6.2.3 填充

当输入图片与卷积核不匹配时或卷积核超过图片边界时,可以采用边界填充(padding)的方法。即把图片尺寸进行扩展,扩展区域补零。如图6-10。当然也可不扩展。

图6-10采用padding方法,对图片进行扩展,然后补零。
根据是否扩展padding又分为Same、Valid。采用Same方式时,对图片扩展并补0;采用Valid方式时,对图片不扩展。如何选择呢?在实际训练过程中,一般选择Same,使用Same不会丢失信息。设补0的圈数为p,输入数据大小为n,过滤器大小为f,步幅大小为s,则有:

6.2.4 多通道上的卷积

前面我们对卷积在输入数据、卷积核的维度上进行了扩展,但输入数据、卷积核都是单个,如果从图形的角度来说都是灰色的,没有考虑彩色图片情况。在实际应用中,输入数据往往是多通道的,如彩色图片就3通道,即R、G、B通道。对于3通道的情况如何卷积呢?3通道图片的卷积运算与单通道图片的卷积运算基本一致,对于3通道的RGB图片,其对应的滤波器算子同样也是3通道的。例如一个图片是6 x 6 x 3,分别表示图片的高度(height)、宽度(weight)和通道(channel)。过程是将每个单通道(R,G,B)与对应的filter进行卷积运算求和,然后再将3通道的和相加,得到输出图片的一个像素值。具体过程如图6-11所示。

图6-11 3通道卷积示意图
为了实现更多边缘检测,可以增加更多的滤波器组。图6-12就是两组过滤器Filter W0和Filter W1。7*7*3输入,经过两个3*3*3的卷积(步幅为2),得到了3*3*2的输出。另外我们也会看到图6-10中的Zero padding是1,也就是在输入元素的周围补了一圈0。Zero padding对于图像边缘部分的特征提取是很有帮助的,可以防止信息丢失。最后,不同滤波器组卷积得到不同的输出,个数由滤波器组决定。

图6-12多组卷积核的卷积运算示意图

6.2.5激活函数

卷积神经网络与标准的神经网络类似,为保证其非线性,也需要使用激活函数,即在卷积运算后,把输出值另加偏移量,输入到激活函数,然后作为下一层的输入,如图6-13所示。

图6-13卷积运算后的结果+偏移量输入到激活函数ReLU
常用的激活函数有:tf.sigmoid、tf.nn.relu 、tf.tanh、 tf.nn.dropout等,这些激活函数的详细介绍可参考本书第5章。

6.2.6卷积函数

卷积函数是构建神经网络的重要支架,通常Pytorch的卷积运算是通过nn.Conv2d来完成。下面先介绍nn.Conv2d的参数,及如何计算输出的形状(shape)。
(1) nn.Conv2d函数

主要参数说明:
in_channels(int)
输入信号的通道
out_channels(int)
卷积产生的通道
kerner_size(int or tuple)
卷积核的尺寸
stride(int or tuple, optional)
卷积步长
padding(int or tuple, optional)
输入的每一条边补充0的层数
dilation(int or tuple, optional)
卷积核元素之间的间距
groups(int, optional)
控制输入和输出之间的连接: group=1,输出是所有的输入的卷积;group=2,此时相当于有并排的两个卷积层,每个卷积层计算输入通道的一半,并且产生的输出是输出通道的一半,随后将这两个输出连接起来。
bias(bool, optional)
如果bias=True,添加偏置。其中参数kernel_size,stride,padding,dilation也可以是一个int的数据,此时卷积height和width值相同;也可以是一个tuple数组,tuple的第一维度表示height的数值,tuple的第二维度表示width的数值
(2)输出形状

当groups=1时

当groups=2时

当groups=3时

in_channels/groups必须是整数,否则报错。

6.2.7转置卷积

转置卷积(Transposed Convolution)在一些文献中也称之为反卷积(Deconvolution)或部分跨越卷积(Fractionally-strided Convolution)。何为转置卷积,它与卷积又有哪些不同?
通过卷积的正向传播的图像一般越来越小,是下采样(downsampled)。卷积的方向传播实际上就是一种转置卷积,它是上采样(up-sampling)。
我们先简单回顾卷积的正向传播是如何运算的,假设卷积操作的相关参数为:输入大小为4,卷积核大小为3,步幅为2,填充为0,即 (n=4,f=3,s=1,p=0),根据公式(6.2)可知,输出 o=2。
整个卷积过程,可用图6-14 表示:

图6-14 卷积运算示意图
对于上述卷积运算,我们把图6-14所示的3×3卷积核展成一个如下所示的[4,16]的稀疏矩阵 C, 其中非0元素 ωi,j 表示卷积核的第 i 行和第 j 列 。

我们再把4×4的输入特征展成[16,1]的矩阵 X,那么 Y=CX 则是一个[4,1]的输出特征矩阵,把它重新排列2×2的输出特征就得到最终的结果,从上述分析可以看出,卷积层的计算其实是可以转化成矩阵相乘。
反向传播时又会如何呢?首先从卷积的反向传播算法开始。假设损失函数为L,则反向传播时,对L关系的求导,利用链式法则得到:

由此,可得X=C^T Y ,即反卷积的操作就是要对这个矩阵运算过程进行逆运算。
转置卷积在生成式对抗网络(GAN)中使用很普遍,后续我们将介绍,图6-15为使用转置卷积的一个示例,它一个上采样过程。


图6-15 转置卷积示例
Pytorch二维转置卷积的格式为:

待续.............

第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在行动一一评价的环境中获得知识,改进行动方案以适应环境。
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-5最右边这个图,其模型或目标函数是一个4次多项式,因它把一些噪音数据也包括进来了,所以导致模型很复杂,实际上房价与房屋面积应该是2次多项式函数,如图5-5中间这个图。
如果要降低模型的复杂度,可以通过缩减它们的系数来实现,如把第3次、4次项的系数θ_3、θ_4缩减到接近于0即可。
在算法中如何实现呢?这个得从其损失函数或目标函数着手。
假设房屋价格与面积间模型的损失函数为:

这个损失函数是我们的优化目标,也就是说我们需要尽量减少损失函数的均方误差。
对于这个函数我们对它添加一些正则项,如加上 10000乘以θ_3 的平方,再加上 10000乘以θ_4的平方,得到如下函数:

这里取10000只是用来代表它是一个"大值",现在,如果要最小化这个新的损失函数,我们要让θ_3 和θ_4 尽可能小。因为,如果你在原有损失函数的基础上加上 10000乘以θ_3 这一项,那么这个新的损失函数将变得很大,所以,当最小化这个新的损失函数时,将使 θ3 的值接近于 0,同样θ_4 的值也接近于 0,就像我们忽略了这两个值一样。如果做到这一点(θ_3 和θ_4 接近 0 ),那么将得到一个近似的二次函数。如图5-6所示:

 

图5-6利用正则化提升模型泛化能力
希望通过上面的简单介绍,能给大家有个直观理解。传统意义上的正则化一般分为L0、L1、L2、L∞等。
Pytorch如何实现正则化呢?这里以实现L2为例,神经网络的L2正则化称为权重衰减(weight decay)。torch.optim集成了很多优化器,如SGD,Adadelta,Adam,Adagrad,RMSprop等,这些优化器自带的一个参数weight_decay,用于指定权值衰减率,相当于L2正则化中的λ参数,也就是式(5.3)中的λ。

5.3.2 dropout正则化

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


图 5-7基础网络Dropout为多个子网络
Dropout训练所有子网络组成的集合,其中子网络是从基本网络中删除非输出单元构建。我们从具有两个可见单元和两个隐藏单元的基本网络开始,这四个单元有十六个可能的子集。右图展示了从原始网络中丢弃不同的单元子集而形成的所有十六个子网络。在这个例子中,所得到的大部分网络没有输入单元或没有从输入连接到输出的路径。当层较宽时,丢弃所有从输入到输出的可能路径的概率变小,所以,这个问题对于层较宽的网络不是很重要。
较先进的神经网络基于一系列仿射变换和非线性变换,我们可以将一些单元的输出乘零,就能有效地删除一些单元。这个过程需要对模型进行一些修改,如径向基函数网络,单元的状态和参考值之间存在一定区别。为简单起见, 在这里提出乘零的简单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,具体构建网络代码如下:

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

把运行结果,通过tensorboardX在web显示,可看到图5-8的结果。

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

5.3.3 批量正则化

我们介绍了数据归一化,这个一般是针对输入数据而言。但在实际训练过程中,经常出现隐含层因数据分布不均,导致梯度消失或不起作用的情况。如采用sigmoid函数或tanh函数为激活函数时,如果数据分布在两侧,这些激活函数的导数就接近于0,这样一来,BP算法得到的梯度也就消失了。如何解决这个问题?
Sergey Ioffe和Christian Szegedy两位学者提出了批标准化(Batch Normalization)方法。Batch Normalization不仅可以有效解决梯度消失问题,而且还可以让调试超参数更加简单,在提高训练模型效率的同时,还可让神经网络模型更加“健壮”。Batch Normalization是如何做到这些的呢? 首先,我们介绍一下BN的算法流程:
输入:微批次(mini-batch) 数据:B={x_1,x_2⋯x_m}
学习参数:γ,β 类似于权重参数,可以通过梯度下降等算法求得。
其中x_i 并不是网络的训练样本,而是指原网络中任意一个隐藏层激活函数的输入,这些输入是训练样本在网络中前向传播得来的。
输出:{y_i=NB_(γ,β) (x_i)}
#求微批次样本均值:
μ_B ← 1/m 〖∑┴m〗┬(i=1) x_i (5.4)
#求微批次样本方差:
σ_B^2 ← 1/m 〖∑┴m〗┬(i=1) 〖(x〗_i-〖μ_B)〗^2 (5.5)
#对x_i进行标准化处理:
(x_i ) ̂ ← (x_i-μ_B)/√(σ_B^2+ϵ) (5.6)
#反标准化操作:
y_i=γ(x_i ) ̂+β≡NB_(γ,β) (x_i) (5.7)
BN是对隐藏层的标准化处理,它与输入的标准化处理Normalizing inputs是有区别的。Normalizing inputs使所有输入的均值为0,方差为1。而Batch Normalization可使各隐藏层输入的均值和方差为任意值。实际上,从激活函数的角度来说,如果各隐藏层的输入均值在靠近0的区域,即处于激活函数的线性区域,这样不利于训练好的非线性神经网络,而且得到的模型效果也不会太好。式(5.6)就起这个作用,当然它还有将归一化后的 x 还原的功能。BN一般用在哪里呢?BN应作用在非线性映射前,即对x=Wu+b做规范化时,在每一个全连接和激励函数之间。
何时使用BN呢?一般在神经网络训练时遇到收敛速度很慢,或梯度爆炸等无法训练的状况时,可以尝试用BN来解决。另外,在一般情况下,也可以加入BN来加快训练速度,提高模型精度,还可以大大提高训练模型的效率。BN具体功能有:
(1)可以选择比较大的初始学习率,让训练速度飙涨。以前还需要慢慢调整学习率,甚至在网络训练到一半的时候,还需要想着学习率进一步调小的比例选择多少比较合适,现在我们可以采用初始很大的学习率,然后学习率的衰减速度也很大,因为这个算法收敛很快。当然,这个算法即使你选择了较小的学习率,也比以前的收敛速度快,因为它具有快速训练收敛的特性。
(2)不用再去理会过拟合中drop out、L2正则项参数的选择问题,采用BN算法后,你可以移除这两项参数,或者可以选择更小的L2正则约束参数了,因为BN具有提高网络泛化能力的特性。
(3)再也不需要使用局部响应归一化层。
(4)可以把训练数据彻底打乱。
下面还是以房价预测为例,比较添加BN层与不添加BN层,两者在测试集上的损失值比较。下例为两者网络结构代码。

图5-9 为运行结果图

图5-9 BN层对测试数据的影响
从图5-9 可以看出,添加BN层对改善模型的泛化能力有一定帮助,不过没有dropout那么明显。这个神经网络比较简单,BN在一些复杂网络中,效果会更好。

5.3.4权重初始化

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

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

5.4 选择合适激活函数

激活函数在神经网络中作用有很多,主要作用是给神经网络提供非线性建模能力。如果没有激活函数,那么再多层的神经网络也只能处理线性可分问题。常用的激活函数有sigmoid、tanh、relu、softmax等。它们的图形、表达式、导数等信息如表5-2所示:
表5-2 激活函数各种属性

在搭建神经网络时,如何选择激活函数?如果搭建的神经网络层数不多,选择sigmoid、tanh、relu、softmax都可以;如果搭建的网络层次比较多,那就需要小心,选择不当就可导致梯度消失问题。此时一般不宜选择sigmoid、tanh激活函数,因它们的导数都小于1,尤其是sigmoid的导数在[0,1/4]之间,多层叠加后,根据微积分链式法则,随着层数增多,导数或偏导将指数级变小。所以层数较多的激活函数需要考虑其导数不宜小于1当然也不能大于1,大于1将导致梯度爆炸,导数为1最好,激活函数relu正好满足这个条件。所以,搭建比较深的神经网络时,一般使用relu激活函数,当然一般神经网络也可使用。此外,激活函数softmax由于〖∑ 〗┬i σ_i (z)=1,常用于多分类神经网络输出层。
激活函数在Pytorch中使用示例:

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

5.5 选择合适的损失函数

损失函数(Loss Function)在机器学习中非常重要,因为训练模型的过程实际就是优化损失函数的过程。损失函数对每个参数的偏导数就是梯度下降中提到的梯度,防止过拟合时添加的正则化项也是加在损失函数后面。损失函数用来衡量模型的好坏,损失函数越小说明模型和参数越符合训练样本。任何能够衡量模型预测值与真实值之间的差异的函数都可以叫做损失函数。在机器学习中常用的损失函数有两种,即交叉熵(Cross Entropy)和均方误差(Mean squared error,简称为MSE),分别对应机器学习中的分类问题和回归问题。
对分类问题的损失函数一般采用交叉熵,交叉熵反应的两个概率分布的距离(不是欧氏距离)。分类问题进一步又可分为多目标分类,如一次要判断100张图是否包含10种动物,或单目标分类。
回归问题预测的不是类别,而是一个任意实数。在神经网络中一般只有一个输出节点,该输出值就是预测值。反应的预测值与实际值之间距离可以用欧氏距离来表示,所以对这类问题我们通常使用均方差作为损失函数,均方差的定义如下:

Pytorch中已集成多种损失函数,这里介绍两个经典的损失函数,其他损失函数基本上是在它们的基础上的变种或延伸。
(1)torch.nn.MSELoss
具体格式:

计算公式:
l(x,y)=L=〖{l_1,l_2,⋯,l_N}〗^T,l_n=〖(x_n-y_n)〗^2,N是批量大小。
如果参数reduction为非None(缺省值为'mean'),则:
l(x,y)={█(mean(L),if reduction='mean'@sum(L),if reduction='sum')┤ (5.9)
x和y是任意形状的张量,每个张量都有n个元素,如果reduction取'none', l(x,y)将不是标量;如果取'sum', l(x,y)只是差平方的和,但不会除以n。
参数说明:
size_average,reduce将移除,主要看参数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。
一般格式:

计算公式:


weight(Tensor)- 为每个类别的loss设置权值,常用于类别不均衡问题。weight必须是float类型的tensor,其长度要于类别C一致,即每一个类别都要设置有weight。
代码示例

5.6 选择合适优化器

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

5.6.1传统梯度优化的不足

传统梯度更新算法为最常见、最简单的一种参数更新策略。其基本思想是:先设定一个学习率λ,参数沿梯度的反方向移动。假设需更新的参数为θ,梯度为g,则其更新策略可表示为:
θ←θ-λg (5.12)
这种梯度更新算法简洁,当学习率取值恰当时,可以收敛到全面最优点(凸函数)或局部最优点(非凸函数)。
但其不足也很明显,对超参数学习率比较敏感(过小导致收敛速度过慢,过大又越过极值点),如图5-11的右图所示。在比较平坦的区域,因梯度接近于0,易导致提前终止训练,如图5-11的左图所示,要选中一个恰当的学习速率往往要花费不少时间。

图5-11学习速率对梯度的影响
学习率除了敏感,有时还会因其在迭代过程中保持不变,很容易造成算法被卡在鞍点的位置,如图5-12所示。

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

 

图5-13在较平坦区域,梯度接近于0,优化算法因误判而提前终止迭代。

传统梯度优化方面的这些不足,在深度学习中会更加明显。为此,人们自然想到如何克服这些不足的问题。从式(6.26)可知,影响优化的无非两个因素:一个是梯度方向,一个是学习率。所以很多优化方法大多从这两方面入手,有些从梯度方向入手,如下节介绍的动量更新策略;有些从学习率入手,这涉及调参问题;还有从两方面同时入手,如自适应更新策略,接下来将介绍这些方法。

5.6.2动量算法

梯度下降法在遇到平坦或高曲率区域时,学习过程有时很慢。利用动量算法能比较好解决这个问题。动量算法与传统梯度下降优化的效果如图5-14所示。

图5-14使用或不使用动量算法的SGD效果比较,红色或振幅较小的为有动量梯度下降行为。
从图5-14 可以看出,不使用动量算法的SGD学习速度比较慢,振幅比较大;而使用动量算法的SGD,振幅较小,而且较快到达极值点。动量算法是如何做到这点的呢?
动量(momentum)是模拟物理里动量的概念,具有物理上惯性的含义,一个物体在运动时具有惯性,把这个思想运用到梯度下降计算中,可以增加算法的收敛速度和稳定性,具体实现如图5-15所示。

 

图5-15动量算法示意图
由图5-15,可知动量算法每下降一步都是由前面下降方向的一个累积和当前点的梯度方向组合而成。含动量的随机梯度下降法,其算法伪代码:
假设 batch_size=10, m=1000
初始化参数向量θ、学习率为λ、动量参数α、初始速度v
while 停止准则未满足 do
Repeat {
forj = 1, 11, 21, .., 991 {
更新梯度: g ̂←1/(batch_size) 〖 〖∑ 〗┬(i=j)〗┴(j+batch_size) ∇_θL(〖f(x〗^((i) ),θ),y^((i) ))
计算速度:v←αv-λg ̂
更新参数:θ←θ+v
}
}
end while
既然每一步都要将两个梯度方向(历史梯度、当前梯度)做一个合并再下降,那为什么不先按照历史梯度往前走那么一小步,按照前面一小步位置的“超前梯度”来做梯度合并呢?如此一来,可以先往前走一步,在靠前一点的位置(如图5-16中的C点)看到梯度,然后按照那个位置再来修正这一步的梯度方向,如下图5-16所示。这就得到动量算法的一种改进算法,称为Nesterov accelerated gradient 简称 NAG 算法。这种预更新方法能防止大幅振荡,不会错过最小值,并对参数更新更加敏感。

图5-16 NAG下降法示意图
NAG下降法的算法伪代码:
假设 batch_size=10, m=1000
初始化参数向量θ、学习率λ、动量参数α、初始速度v
while 停止准则未满足 do
更新超前点:θ ̃ ← θ+αv
Repeat {
forj = 1, 11, 21, .., 991 {
更新梯度(在超前点): g ̂←1/(batch_size) 〖 〖∑ 〗┬(i=j)〗┴(j+batch_size) ∇_θ ̃ L(〖f(x〗^((i) ),θ ̃),y^((i) ))
计算速度:v←αv-λg ̂
更新参数:θ←θ+v
}
}
end while
NAG动量法和经典动量法的差别就在B点和C点梯度的不同。动量法,更多关注梯度下降方法的优化,如果能从方向和学习率同时优化,效果或许更理想。事实也确实如此,而且这些优化在深度学习中显得尤为重要。接下来我们介绍几种自适应优化算法,这些算法同时从梯度方向及学习率进行优化,效果非常好。

5.6.3 AdaGrad算法

传统梯度下降算法对学习率这个超参数非常敏感,难以驾驭;对参数空间的某些方向也没有很好的方法。这些不足在深度学习中,因高维空间、多层神经网络等因素,常会出现平坦、鞍点、悬崖等问题,因此,传统梯度下降法在深度学习中显得力不从心。还好现在已有很多解决这些问题的有效方法。上节介绍的动量算法在一定程度缓解对参数空间某些方向的问题,但需要新增一个参数,而且对学习率的控制还不很理想。为了更好驾驭这个超参数,人们想出来多种自适应优化算法,使用自适应优化算法,学习率不再是一个固定不变值,它会根据不同情况自动调整来适用情况。这些算法使深度学习向前迈出一大步!这节我们将介绍几种自适应优化算法。
AdaGrad算法是通过参数来调整合适的学习率λ,能独立地自动调整模型参数的学习率,对稀疏参数进行大幅更新和对频繁参数进行小幅更新。因此,Adagrad方法非常适合处理稀疏数据。AdaGrad算法在某些深度学习模型上效果不错。但还有些不足,可能因其累积梯度平方导致学习率过早或过量的减少所致。
AdaGrad算法伪代码:
假设 batch_size=10, m=1000
初始化参数向量θ、学习率λ
小参数δ,一般取一个较小值(如〖10〗^(-7)),该参数避免分母为0
初始化梯度累积变量 r=0
while 停止准则未满足 do
Repeat {
forj = 1, 11, 21, .., 991 {
更新梯度: g ̂←1/(batch_size) 〖 〖∑ 〗┬(i=j)〗┴(j+batch_size) ∇_θL(〖f(x〗^((i) ),θ),y^((i) ))
累积平方梯度:r←r+g ̂⊙g ̂ #⊙表示逐元运算
计算速度:△θ← -λ/(δ+√r)⊙g ̂
更新参数:θ←θ+△θ
}
}
end while
由上面算法的伪代码可知:
(1)随着迭代时间越长,累积梯度r越大,从而学习速率λ/(δ+√r)随着时间就减小,在接近 目标值时,不会因为学习速率过大而越过极值点。
(2)不同参数之间学习速率不同,因此,与前面固定学习速率相比,不容易在鞍点卡住。
(3)如果梯度累积参数r比较小,则学习速率会比较大,所以参数迭代的步长就会比较大。 相反,如果梯度累积参数比较大,则学习速率会比较小,所以迭代的步长会比较小。

5.6.4 RMSProp算法

RMSProp算法修改AdaGrad,为的是在非凸背景下效果更好。针对梯度平方和累计越来越大的问题,RMSProp指数加权的移动平均代替梯度平方和。RMSProp为使用移动平均,引入了一个新的超参数ρ,用来控制移动平均的长度范围。
RMSProp算法伪代码:
假设 batch_size=10, m=1000
初始化参数向量θ、学习率λ、衰减速率ρ
小参数δ,一般取一个较小值(如〖10〗^(-7)),该参数避免分母为0
初始化梯度累积变量 r=0
while 停止准则未满足 do
Repeat {
forj = 1, 11, 21, .., 991 {
更新梯度: g ̂←1/(batch_size) 〖 〖∑ 〗┬(i=j)〗┴(j+batch_size) ∇_θL(〖f(x〗^((i) ),θ),y^((i) ))
累积平方梯度:r←ρr+(1-ρ)g ̂⊙g ̂
计算参数更新:△θ← -λ/(δ+√r)⊙g ̂
更新参数:θ←θ+△θ
}
}
end while
RMSProp算法在实践中已被证明是一种有效且实用的深度神经网络优化算法,在深度学习中得到广泛应用。

5.6.5 Adam算法

Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。
Adam是另一种学习速率自适应的深度神经网络方法,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习速率。Adam算法伪代码如下:
假设 batch_size=10, m=1000
初始化参数向量θ、学习率λ
矩估计的指数衰减速率ρ_1 和ρ_2在区间[0,1)内。
小参数δ,一般取一个较小值(如〖10〗^(-7)),该参数避免分母为0
初始化一阶和二阶矩变量 s=0,r=0
初始化时间步 t=0
while 停止准则未满足 do
Repeat {
forj = 1, 11, 21, .., 991 {
更新梯度: g ̂←1/(batch_size) 〖 〖∑ 〗┬(i=j)〗┴(j+batch_size) ∇_θL(〖f(x〗^((i) ),θ),y^((i) ))
t←t+1
更新有偏一阶矩估计:s ←ρ_1 s +(1-ρ_1)g ̂
更新有偏二阶矩估计:r← ρ_2 r +(1-ρ_2)g ̂⊙g ̂
修正一阶矩偏差:s ̂=s/(1-ρ_1^t )
修正二阶矩偏差:r ̂=r/(1-ρ_2^t )
累积平方梯度:r←ρr+(1-ρ)g ̂⊙g ̂
计算参数更新:△θ=-λ s ̂/(δ+√(r ̂ ))
更新参数:θ←θ+△θ
}
}
end while
前文介绍了深度学习的正则化方法,它是深度学习核心之一;优化算法也是深度学习的核心之一。优化算法很多,如随机梯度下降法、自适应优化算法等,那么具体使用时该如何选择呢?
RMSprop,Adadelta和Adam被认为是自适应优化算法,因为它们会自动更新学习率。而使用SGD时,必须手动选择学习率和动量参数,通常会随着时间的推移而降低学习率。
有时可以考虑综合使用这些优化算法,如采用先用Adam,然后用SGD优化方法,这个想法,实际上由于在训练的早期阶段SGD对参数调整和初始化非常敏感。因此,我们可以通过先使用Adam优化算法进行训练,这将大大节省训练时间,且不必担心初始化和参数调整,一旦用Adam训练获得较好的参数后,我们可以切换到SGD +动量优化,以达到最佳性能。采用这种方法有时能达到很好效果,如图5-17所示,迭代次数超过150后,用SGD效果好于Adam。

图5-17 迭代次数与测试误差间的对应关系

5.7GPU加速

深度学习涉及很多向量或多矩阵运算,如矩阵相乘、矩阵相加、矩阵-向量乘法等。深层模型的算法,如BP,Auto-Encoder,CNN等,都可以写成矩阵运算的形式,无须写成循环运算。然而,在单核CPU上执行时,矩阵运算会被展开成循环的形式,本质上还是串行执行。GPU(Graphic Process Units,图形处理器)的众核体系结构包含几千个流处理器,可将矩阵运算并行化执行,大幅缩短计算时间。随着NVIDIA、AMD等公司不断推进其GPU的大规模并行架构,面向通用计算的GPU已成为加速可并行应用程序的重要手段。得益于GPU众核(many-core)体系结构,程序在GPU系统上的运行速度相较于单核CPU往往提升几十倍乃至上千倍。
目前,GPU已经发展到了较为成熟的阶段。利用GPU来训练深度神经网络,可以充分发挥其数以千计计算核心的能力,在使用海量训练数据的场景下,所耗费的时间大幅缩短,占用的服务器也更少。如果对适当的深度神经网络进行合理优化,一块GPU卡相当于数十甚至上百台CPU服务器的计算能力,因此GPU已经成为业界在深度学习模型训练方面的首选解决方案。
如何使用GPU?现在很多深度学习工具都支持GPU运算,使用时只要简单配置即可。Pytorch支持GPU,可以通过to(device)函数来将数据从内存中转移到GPU显存,如果有多个GPU还可以定位到哪个或哪些GPU。Pytorch一般把GPU作用于张量(Tensor)或模型(包括torch.nn下面的一些网络模型以及自己创建的模型)等数据结构上。

5.7.1 单GPU加速

使用GPU之前,需要确保GPU是可以使用,可通过torch.cuda.is_available()的返回值来进行判断。返回True则具有能够使用的GPU。
通过torch.cuda.device_count()可以获得能够使用的GPU数量。
如何查看平台GPU的配置信息?在命令行输入命令nvidia-smi即可 (适合于Linux或Windows环境)。图5-18是GPU配置信息样例,从中可以看出共有2个GPU。


图5-18 GPU配置信息

把数据从内存转移到GPU,一般针对张量(我们需要的数据)和模型。
对张量(类型为FloatTensor或者是LongTensor等),一律直接使用方法.to(device)或.cuda()即可。

对于模型来说,也是同样的方式,使用.to(device)或.cuda来将网络放到GPU显存。

5.7.2 多GPU加速

这里我们介绍单主机多GPUs的情况,单机多GPUs主要采用的DataParallel函数,而不是DistributedParallel,后者一般用于多主机多GPUs,当然也可用于单机多GPU。
使用多卡训练的方式有很多,当然前提是我们的设备中存在两个及以上的GPU。
使用时直接用model传入torch.nn.DataParallel函数即可,如下代码:

这时,默认所有存在的显卡都会被使用。
如果你的电脑有很多显卡,但只想利用其中一部分,如只使用编号为0、1、3、4的四个GPU,那么可以采用以下方式:

或者

其中CUDA_VISIBLE_DEVICES 表示当前可以被Pytorch程序检测到的GPU。
下面为单机多GPU的实现代码。
(1)背景说明
这里使用波士顿房价数据为例,共506个样本,13个特征。数据划分成训练集和测试集,然后用data.DataLoader转换为可批加载的方式。采用nn.DataParallel并发机制,环境有2个GPU。当然,数据量很小,按理不宜用nn.DataParallel,这里只是为了说明使用方法。
(2)加载数据

(3)把数据转换为批处理加载方式
批次大小为128,打乱数据。

(4)定义网络

(5)把模型转换为多GPU并发处理格式

运行结果
Let's use 2 GPUs
DataParallel(
(module): Net1(
(layer1): Sequential(
(0): Linear(in_features=13, out_features=16, bias=True)
)
(layer2): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
)
(layer3): Sequential(
(0): Linear(in_features=32, out_features=1, bias=True)
)
)
)
(6)选择优化器及损失函数

(7)模型训练,并可视化损失值。

运行的部分结果
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
Outside: input size torch.Size([128, 13]) output_size torch.Size([128, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
In Model: input size torch.Size([64, 13]) output size torch.Size([64, 1])
Outside: input size torch.Size([128, 13]) output_size torch.Size([128, 1])
从运行结果可以看出,一个批次数据(batch-size=128)拆分成两份,每份大小为64,分别放在不同的GPU上。此时用GPU监控也可发现,两个GPU都同时在使用,如图5-19所示。

图5-19 同时使用多个GPU的情况
(8)通过web查看损失值的变化情况,如图5-20所示。

图5-20 并发运行训练损失值变化情况
图形中出现较大振幅,是由于采用批次处理,而且数据没有做任何预处理,对数据进行规范化应该更平滑一些,大家可以尝试一下。
单机多GPU也可使用DistributedParallel,它多用于分布式训练,但也可以用在单机多GPU的训练,配置比使用nn.DataParallel稍微麻烦一点,但是训练速度和效果更好一点。具体配置为:

单机运行时使用下列方法启动

5.7.3使用GPU注意事项

使用GPU可以提升训练的速度,但如果使用不当,可能影响使用效率,具体使用时要注意以下几点:
(1)GPU的数量尽量为偶数,奇数的GPU有可能会出现异常中断的情况;
(2)GPU很快,但数据量较小时,效果可能没有单GPU好,甚至还不如CPU;
(3)如果内存不够大,使用多GPU训练的时候可通过设置pin_memory为False,当然使用精度稍微低一点的数据类型有时也有效果。

5.8 小结

本章从机器学习的这个比深度学习更宽泛的概念出发,首先说明其基本任务、一般流程等,然后说明在机器学习中解决过拟合、欠拟合的一些常用技巧或方法。同时介绍了各种激活函数、损失函数、优化器等机器学习、深度学习的核心内容。最后说明在程序中如何设置GPU设备、如何用GPU加速训练模型等内容。这章是深度学习的基础,接下来我们将从视觉处理、自然语言处理、生成式网络等方面,深入介绍深度学习的基础又核心的内容。

第2章 Pytorch基础


Pytorch是Facebook团队于2017年1月发布的一个深度学习框架,虽然晚于TensorFlow、Keras等框架,但自发布之日起,其关注度就在不断上升,目前在GitHub上的热度已超过Theano、Caffe、MXNet等框架。
Pytorch 1.0版本推出后,增加了很多新功能,对原有内容进行了优化,并整合了caffe2,使用更方便,也大大增强其生产性,所以其热度在迅速上升。
Pytorch采用python语言接口来实现编程,非常容易上手。它就像带GPU的Numpy,与Python一样都属于动态框架。PyTorch继承了Torch灵活、动态的编程环境和用户友好的界面,支持以快速和灵活的方式构建动态神经网络,还允许在训练过程中快速更改代码而不妨碍其性能,支持动态图形等尖端AI模型的能力,是快速实验的理想选择。本章主要介绍Pytorch的一些基础且常用的概念和模块,具体包括如下内容:
为何选择Pytorch
Pytorch环境的安装与配置
Numpy与Tensor
Tensor与Autograd
使用Numpy实现机器学习
使用Tensor及antograd实现机器学习
使用TensorFlow架构

2.1 为何选择Pytorch?

PyTorch是一个建立在Torch库之上的Python包,旨在加速深度学习应用。它提供一种类似NumPy的抽象方法来表征张量(或多维数组),它可以利用GPU来加速训练。由于 PyTorch 采用了动态计算图(dynamic computational graph)结构,且基于tape的autograd 系统的深度神经网络。其它很多框架,比如 TensorFlow(TensorFlow2.0也加入了动态网络的支持)、Caffe、CNTK、Theano 等,采用静态计算图。 使用 PyTorch,通过一种我们称之为Reverse-mode auto-differentiation(反向模式自动微分)的技术,你可以零延迟或零成本地任意改变你的网络的行为。
torch是Pytorch中的一个重要包,它包含了多维张量的数据结构以及基于其上的多种数学操作。
自2015 年谷歌开源 TensorFlow以来,深度学习框架之争越来越激烈,全球多个看重 AI 研究与应用的科技巨头均在加大这方面的投入。Pytorch从 2017 年年初发布以来,PyTorch 可谓是异军突起,短时间内取得了一系列成果,成为其中的明星框架。最近Pytorch进行了一些较大的版本更新,0.4版本把Varable与Tensor进行了合并,增加了Windows的支持。1.0版本增加了JIT(全称Justintimecompilation,即时编译,它弥补了研究与生产的部署的差距)、更快的分布式、C++扩展等。
PyTorch 1.0 稳定版已发布,PyTorch 1.0 从 Caffe2 和 ONNX 移植了模块化和产品导向的功能,并将它们和 PyTorch 已有的灵活、专注研究的特性相结合。PyTorch 1.0 中的技术已经让很多 Facebook 的产品和服务变得更强大,包括每天执行 60 亿次文本翻译。
PyTorch由4个主要包组成:
torch:类似于Numpy的通用数组库,可将张量类型转换为torch.cuda.TensorFloat,并在GPU上进行计算。
torch.autograd:用于构建计算图形并自动获取梯度的包。
torch.nn:具有共享层和损失函数的神经网络库。
torch.optim:具有通用优化算法(如SGD,Adam等)的优化包。

2.2 安装配置

安装Pytorch时,请核查当前环境是否有GPU,如果没有,则安装CPU版;如果有,则安装GPU版本的。

2.2.1 安装CPU版Pytorch

安装CPU版的Pytorch比较简单,Pytorch是基于Python开发,所以如果没有安装Python需要先安装,然后再安装Pytorch。具体步骤如下:
(1)下载Python
安装Python建议采用anaconda方式安装,先从Anaconda的官网:https://www.anaconda.com/distribution, 如图2-1 所示。

图2-1 下载Anaconda界面
下载Anaconda3的最新版本,如Anaconda3-5.0.1-Linux-x86_64.sh,建议使用3系列,3系列代表未来发展。另外,下载时根据自己环境,选择操作系统等。
(2)在命令行,执行如下命令,开始安装Python:

(3)接下来根据安装提示,直接按回车即可。其间会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径(~/ anaconda3),然后就开始安装。
(4)安装完成后,程序提示是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加。添加以后,就可以使用python、ipython命令时自动使用Anaconda3的python环境。
(5)安装Pytorch
登录Pytorch官网:https://pytorch.org/,登录后,可看到图2-2 所示界面,然后选择对应项。

图2-2 Pytorch 安装界面

把第⑥项内容,复制到命令行,执行即可。

(6)验证安装是否成功
启动Python,然后执行如下命令,如果没有报错,说明安装成功!

2.2.2 安装GPU版Pytorch

安装GPU版本的Pytorch稍微复杂一点,除需要安装Python、Pytorch,还需要安装GPU的驱动(如英伟达的Nvidia)及cuda、cuDNN计算框架,主要步骤如下:
(1)安装NVIDIA驱动
下载地址:https://www.nvidia.cn/Download/index.aspx?lang=cn
登录可以看到图2-3的界面:

图2-3 NVIDIA的下载界面
选择产品类型、操作系统等,然后点击搜索按钮,进入下载界面。
安装完成后,在命令行输入:nvidia-smi 用来显示GPU卡的基本信息,如果出现图2-4,则说明安装成功。如果报错,则说明安装失败,请搜索其他安装驱动的方法。

图2-4 显示GPU卡的基本信息
(2)安装cuda
CUDA(Compute Unified Device Architecture),是英伟达公司推出的一种基于新的并行编程模型和指令集架构的通用计算架构,它能利用英伟达GPU的并行计算引擎,比CPU更高效的解决许多复杂计算任务。安装CUDA Driver时需与NVIDIA GPU Driver的版本一致,CUDA才能找到显卡。
(3)安装cuDNN
NVIDIA cuDNN是用于深度神经网络的GPU加速库。注册NVIDIA并下载cuDNN包:https://developer.nvidia.com/rdp/cudnn-archive。
(4)安装Python及Pytorch
这步与本书2.2.1小节 安装CPU版Pytorch相同,只是选择cuda时,不是None,而是对应cuda的版本号。如图2-5所示。

图2-5 安装GPU版Pytorch
(5)验证
验证Pytorch安装是否成功与本书2.2.1小节一样,如果想进一步验证Pytorch是否在使用GPU,可以运行以下一段测试GPU的程序test_gpu.py,如果成功的话,可以看到如图2-6的效果。

在命令行运行以下脚本:

如果可以看到如图2-6或图2-7的结果,说明安装GPU版Pytorch成功!

图2-6 运行test_gpu.py的结果
在命令行运行:nvidia-smi,可以看到如图2-7所示界面。

图2-7 含GPU进程的显卡信息

2.3 Jupyter Notebook环境配置

Jupyter Notebook是目前Python比较流行的开发、调试环境,此前被称为 IPython notebook,以网页的形式打开,可以在网页页面中直接编写代码和运行代码,代码的运行结果(包括图形)也会直接显示。如在编程过程中添加注释、目录、图像或公式等内容,Jupyter Notebook有以下特点:
编程时具有语法高亮、缩进、tab补全的功能。
可直接通过浏览器运行代码,同时在代码块下方展示运行结果。
以富媒体格式展示计算结果。富媒体格式包括:HTML,LaTeX,PNG,SVG等。
对代码编写说明文档或语句时,支持Markdown语法。
支持使用LaTeX编写数学性说明。
接下来介绍配置Jupyter Notebook的主要步骤。
(1)生成配置文件

将在当前用户目录下生成文件:.jupyter/jupyter_notebook_config.py
(2)生成当前用户登录jupyter密码
打开ipython, 创建一个密文密码

(3)修改配置文件

进行如下修改:

(4)启动jupyter notebook

在浏览器上,输入IP:port,即可看到如下类似界面。

接下来就可以在浏览器进行开发调试Pytorch、Python等任务了。

2.4 Numpy与Tensor

第1章我们介绍了Numpy,知道其存取数据非常方便,而且还拥有大量的函数,所以深得数据处理、机器学习者喜爱。这节我们将介绍Pytorch的Tensor,它可以是零维(又称为标量或一个数)、一维、二维及多维的数组。其自称为神经网络界的Numpy, 它与Numpy相似,它们共享内存,它们之间的转换非常方便和高效。不过它们也有不同之处,最大的区别就是Numpy 会把 ndarray 放在 CPU 中加速运算,而 由torch 产生的 tensor 会放在 GPU 中加速运算 (假设当前环境有GPU)。

2.4.1 Tensor概述

对tensor的操作很多,从接口的角度来划分,可以分为两类:
(1)torch.function,如torch.sum、torch.add等,
(2)tensor.function,如tensor.view、tensor.add等。
这些操作对大部分tensor都是等价的,如torch.add(x,y)与x.add(y)等价。实际使用中可以根据个人爱好选择。
如果从修改方式的角度,可以分为以下两类:
(1)不修改自身数据,如x.add(y),x的数据不变,返回一个新的tensor。
(2)修改自身数据,如x.add_(y)(运行符带下划线后缀),运算结果存在x中,x被修改。

运行结果
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])

2.4.2 创建Tensor

新建tensor的方法很多,可以从列表或ndarray等类型进行构建,也可根据指定的形状构建。常见的构建tensor的方法,可参考表2-1。
表2-1 常见的新建tensor方法

下面举例说明。

【说明】注意torch.Tensor与torch.tensor的几点区别
①torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),torch.tensor从数据中推断数据类型。
②torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。

运行结果
t1的值tensor([3.5731e-20]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor

根据一定规则,自动生成tensor的一些例子。

2.4.3 修改Tensor形状

在处理数据、构建网络层等过程中,经常需要了解Tensor的形状、修改Tensor的形状。
与修改Numpy的形状类似,修改tenor的形状也有很多类似函数,具体可参考表2-2。 表2-2 为tensor常用修改形状的函数。

以下为一些实例

【说明】torch.view与torch.reshpae的异同
①reshape()可以由torch.reshape(),也可由torch.Tensor.reshape()调用。view()只可由torch.Tensor.view()来调用。
②对于一个将要被view的Tensor,新的size必须与原来的size与stride兼容。否则,在view之前必须调用contiguous()方法。
③同样也是返回与input数据量相同,但形状不同的tensor。若满足view的条件,则不会copy,若不满足,则会copy
④如果您只想重塑张量,请使用torch.reshape。 如果您还关注内存使用情况并希望确保两个张量共享相同的数据,请使用torch.view。

2.4.4 索引操作

Tensor的索引操作与Numpy类似,一般情况下索引结果与源数据共享内存。从tensor获取元素除了可以通过索引,也可借助一些函数,常用的选择函数,可参考表2-3。
表2-3 常用选择操作函数

以下为部分函数的实现代码:

2.4.5 广播机制

本书1.7小节介绍了Numpy的广播机制,是向量运算的重要技巧。Pytorch也支持广播规则,以下通过几个示例进行说明。

2.4.6 逐元素操作

与Numpy一样,tensor也有逐元素操作(element-wise),操作内容相似,但使用函数可能不尽相同。大部分数学运算都属于逐元操作,逐元素操作输入与输出的形状相同。,常见的逐元素操作,可参考表2-4。
表2-4常见逐元素操作

【说明】这些操作均创建新的tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。
以下为部分逐元素操作代码实例。

2.4.7 归并操作

归并操作顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般不相同,而且往往是输入大于输出形状。归并操作可以对整个tensor,也可以沿着某个维度进行归并。常见的归并操作可参考表2-5。
表2-5 常见的归并操作

【说明】
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,缺省情况是False,即不保留。
以下为归并操作的部分代码

2.4.8 比较操作

比较操作一般进行逐元素比较,有些是按指定方向比较。常用的比较函数可参考表2-6。
表2-6 常用的比较函数

以下是部分函数的代码实现。

2.4.9 矩阵操作

机器学习和深度学习中存在大量的矩阵运算,用的比较多的有两种,一种是逐元素乘法,另外一种是点积乘法。Pytorch中常用的矩阵函数可参考表2-7。
表2-7 常用矩阵函数

【说明】
①torch的dot与Numpy的dot有点不同,torch中dot对两个为1D张量进行点积运算,Numpy中的dot无此限制。
②mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
③转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。

2.4.10 Pytorch与Numpy比较

Pytorch与Numpy有很多类似的地方,并且有很多相同的操作函数名称,或虽然函数名称不同但含义相同;当然也有一些虽然函数名称相同,但含义不尽相同。对此,有时很容易混淆,下面我们把一些主要的区别进行汇总,具体可参考表2-8。
表2-8 Pytorch与Numpy函数对照表

2.5 Tensor与Autograd

在神经网络中,一个重要内容就是进行参数学习,而参数学习离不开求导,Pytorch是如何进行求导的呢?
现在大部分深度学习架构都有自动求导的功能,Pytorch也不列外,torch.autograd包就是用来自动求导的。autograd包为张量上所有的操作提供了自动求导功能,而torch.Tensor和torch.Function为autograd上的两个核心类,他们相互连接并生成一个有向非循环图。接下来我们先简单介绍tensor如何实现自动求导,然后介绍计算图,最后用代码实现这些功能。

2.5.1 自动求导要点

autograd包为对tensor进行自动求导,为实现对tensor自动求导,需考虑如下事项:
(1)创建叶子节点(leaf node)的tensor,使用requires_grad参数指定是否记录对其的操作,以便之后利用backward()方法进行梯度求解。requires_grad参数缺省值为False,如果要对其求导需设置为True,与之有依赖关系的节点自动变为True。
(2)可利用requires_grad_()方法修改tensor的requires_grad属性。可以调用.detach()或with torch.no_grad():将不再计算张量的梯度,跟踪张量的历史记录。这点在评估模型、测试模型阶段常常使用。
(3)通过运算创建的tensor(即非叶子节点),会自动被赋于grad_fn属性。该属性表示梯度函数。叶子节点的grad_fn为None。
(4)最后得到的tensor执行backward()函数,此时自动计算各变在量的梯度,并将累加结果保存grad属性中。计算完成后,非叶子节点的梯度自动释放。
(5)backward()函数接受参数,该参数应和调用backward()函数的Tensor的维度相同,或者是可broadcast的维度。如果求导的tensor为标量(即一个数字),backward中参数可省略。
(6)反向传播的中间缓存会被清空,如果需要进行多次反向传播,需要指定backward中的参数retain_graph=True。多次反向传播时,梯度是累加的。
(7)非叶子节点的梯度backward调用后即被清空。
(8)可以通过用torch.no_grad()包裹代码块来阻止autograd去跟踪那些标记为.requesgrad=True的张量的历史记录。这步在测试阶段经常使用。
整个过程中,Pytorch采用计算图的形式进行组织,该计算图为动态图,它的计算图在每次前向传播时,将重新构建。其他深度学习架构,如TensorFlow、Keras一般为静态图。接下来我们介绍计算图,用图的形式来描述就更直观了,该计算图为有向无环图(DAG)。

2.5.2计算图

计算图是一种有向无环图像,用图形方式表示算子与变量之间的关系,直观高效。如图2-8所示,圆形表示变量,矩阵表示算子。如表达式:z=wx+b,可写成两个表示式:y=wx,则z=y+b,其中x、w、b为变量,是用户创建的变量,不依赖于其他变量,故又称为叶子节点。为计算各叶子节点的梯度,需要把对应的张量参数requires_grad属性设置为True,这样就可自动跟踪其历史记录。y、z是计算得到的变量,非叶子节点,z为根节点。mul和add是算子(或操作或函数)。由这些变量及算子,就构成一个完整的计算过程(或前向传播过程)。

图2-8 正向传播计算图
我们的目标是更新各叶子节点的梯度,根据复合函数导数的链式法则,不难算出各叶子节点的梯度。

Pytorch调用backward(),将自动计算各节点的梯度,这是一个反向传播过程,这个过程可用图2-9表示。在反向传播过程中,autograd沿着图2-9,从当前根节点z反向溯源,利用导数链式法则,计算所有叶子节点的梯度,其梯度值将累加到grad属性中。对非叶子节点的计算操作(或function)记录在grad_fn属性中,叶子节点的grad_fn值为None。

图2-9 梯度反向传播计算图
下面我们用代码实现这个计算图。

2.5.3 标量反向传播

假设x、w、b都是标量,z=wx+b,对标量z调用backward(),我们无需对backward()传入参数。以下是实现自动求导的主要步骤:
(1)定义叶子节点及算子节点

运行结果
x,w,b的require_grad属性分别为:False,True,True
(2)查看叶子节点、非叶子节点的其他属性

(3)自动求导,实现梯度方向传播,即梯度的反向传播。

2.5.4 非标量反向传播

2.5.3小节我们介绍了当目标张量为标量时,调用backward()无需传入参数。目标张量一般是标量,如我们经常使用的损失值Loss,一般都是一个标量。但也有非标量的情况,后面我们介绍的Deep Dream的目标值就是一个含多个元素的张量。如何对非标量进行反向传播呢?Pytorch有个简单的规定,不让张量(tensor)对张量求导,只允许标量对张量求导,因此,如果目标张量对一个非标量调用backward(),需要传入一个gradient参数,该参数也是张量,而且需要与调用backward()的张量形状相同。为什么要传入一个张量gradient?
传入这个参数就是为了把张量对张量求导转换为标量对张量求导。这有点拗口,我们举一个例子来说,假设目标值为loss=(y_1,y_2,…,y_m)传入的参数为v=(v_1,v_2,…,v_m),那么就可把对loss的求导,转换为对loss*v^T标量的求导。即把原来∂loss/∂x得到雅可比矩阵(Jacobian)乘以张量v^T,便可得到我们需要的梯度矩阵。
backward函数的格式为:

上面说的可能有点抽象,下面我们通过一个实例进行说明。
(1)定义叶子叶子节点及计算节点

(2)手工计算y对x的梯度
我们先手工计算一下y对x的梯度,为了验证Pytorch的backward的结果是否正确。
y对x的梯度是一个雅可比矩阵,各项的值,我们可通过以下方法进行计算。

(3)调用backward获取y对x的梯度

这个结果与我们手工运算的不符,显然这个结果是错误的,错在哪里呢?这个结果的计算过程是:

由此,错在v的取值错误,通过这种方式得的到并不是y对x的梯度。这里我们可以分成两步的计算。首先让v=(1,0)得到y_1对x的梯度,然后使v=(0,1),得到y_2对x的梯度。这里因需要重复使用backward(),需要使参数retain_graph=True,具体代码如下:

运行结果
tensor([[4., 3.],[2., 6.]])
这个结果与手工运行的式(2.5)结果一致。

2.6 使用Numpy实现机器学习

前面我们介绍了Numpy、Tensor的基础内容,对如何用Numpy、Tensor操作数组有了一定认识。为了加深大家对Pytorch是如何进行完成机器学习、深度学习,本章剩余章节将分别用Numpy、Tensor、autograd、nn及optimal实现同一个机器学习任务,比较他们之间的异同及各自优缺点,从而加深对Pytorch的理解。
首先,我们用最原始的Numpy实现有关回归的一个机器学习任务,不用Pytorch中的包或类。这种方法代码可能多一点,但每一步都是透明的,有利于理解每步的工作原理。
主要步骤包括:
首先,是给出一个数组x,然后基于表达式:y=3x^2+2,加上一些噪音数据到达另一组数据y。
然后,构建一个机器学习模型,学习表达式y=wx^2+b的两个参数w,b。利用数组x,y的数据为训练数据
最后,采用梯度梯度下降法,通过多次迭代,学习到w、b的值。
以下为具体步骤:
(1)导入需要的库

(2)生成输入数据x及目标数据y
设置随机数种子,生成同一个份数据,以便用多种方法进行比较。

(3)查看x,y数据分布情况

图2-10 Numpy实现的源数据
(4)初始化权重参数

(5)训练模型
定义损失函数,假设批量大小为100:

用代码实现上面这些表达式:

(6)可视化结果

运行结果:

图2-11 可视化Numpy学习结果

[[2.95859544]] [[2.10178594]]
从结果看来,学习效果还是比较理想的。

2.7 使用Tensor及antograd实现机器学习

2.6节可以说是纯手工完成一个机器学习任务,数据用Numpy表示,梯度及学习是自己定义并构建学习模型。这种方法适合于比较简单的情况,如果稍微复杂一些,代码量将几何级增加。是否有更方便的方法呢?这节我们将使用Pytorch的自动求导的一个包antograd,利用这个包及对应的Tensor,便可利用自动反向传播来求梯度,无需手工计算梯度。以下是具体实现代码。
(1)导入需要的库

(2)生成训练数据,并可视化数据分布情况

图2-12 可视化输入数据
(3)初始化权重参数

(4)训练模型

(5)可视化训练结果

运行结果:

图2-13 使用 antograd的结果
tensor([[2.9645]], requires_grad=True) tensor([[2.1146]], requires_grad=True)
这个结果与使用Numpy机器学习的差不多。

2.8 使用TensorFlow架构

2.6小节我们用Numpy实现了回归分析,2.7 我们用Pytorch的autograd及Tensor实现了这个任务。这节我们用深度学习的另一个框架TensorFlow实现该回归分析任务,大家可比较一下,使用不同架构之间的一些区别。为便于比较,这里使用TensorFlow的静态图(TensorFlow2.0 新增核心功能Eager Execution,并把Eager Execution变为 TensorFlow 默认的执行模式。这意味着 TensorFlow 如同 PyTorch 那样,由编写静态计算图全面转向了动态计算图)。
(1)导入库及生成训练数据

(2)初始化参数

(3)实现前向传播及损失函数

(4)训练模型

(5)可视化结果

最后五次输出结果:
损失值、权重、偏移量分别为0.0094,[2.73642],[2.1918662]
损失值、权重、偏移量分别为0.0065,[2.8078585],[2.1653984]
损失值、权重、偏移量分别为0.0050,[2.8592768],[2.1463478]
损失值、权重、偏移量分别为0.0042,[2.896286],[2.132636]
损失值、权重、偏移量分别为0.0038,[2.922923],[2.1227665]

图2-14 使用Tensorflow的结果
迭代2000次后,损失值达到0.0038,权重和偏移量分别2.92、2.12,与目标值3、2是比较接近了,当然如果增加迭代次数,精度将进一步提升。大家可以尝试一下。
TensorFlow使用静态图,其特点是先构造图形(如果不显式说明,TensorFlow会自动构建一个缺省图形),然后启动session,开始执行相关程序,这个时候程序才开始运行,前面都是铺垫,所以也没有运行结果。而Pytorch的动态图,动态最关键一点就是它是交互式的,而且执行每个命令马上就可看到结果,这对训练、发现问题、纠正问题非常方便,其构图是一个叠加过程或动态过程,期间我们可以随时添加内容。这些特征对于训练和调式过程无疑是非常有帮助的,这或许也是Pytorch为何在高校、科研院所深得大家喜爱的重要原因。

2.9 小结

本章主要介绍Pytorch的基础知识,这些内容是后续章节的重要支撑。首先介绍了Pytorch的安装配置,然后介绍了Pytorch的重要数据结构Tensor。Tensor类似于Numpy的数据结构,但Tensor提供GPU加速及自动求导等技术。最后分别用Numpy、Tensor、autograd、TensorFlow等技术分别实现同一个机器学习任务。

第3章 Pytorch神经网络工具箱

前面我们介绍了Pytorch的数据结构及自动求导机制,充分运行这些技术可以大大提高我们的开发效率。这章将介绍Pytorch的另一利器:神经网络工具箱。利用这个工具箱,设计一个神经网络就像搭积木一样,可以极大简化我们构建模型的任务。
本章主要讨论如何使用Pytorch神经网络工具箱来构建网络,我们可以学习如下内容:
 介绍神经网络核心组件
 如何构建一个神经网络
 详细介绍如何构建一个神经网络
 如何使用nn模块中Module及functional
 如何选择优化器
 动态修改学习率参数

3.1 神经网络核心组件

神经网络看起来很复杂,节点很多,层数多,参数更多。但核心部分或组件不多,把这些组件确定后,这个神经网络基本就确定了。这些核心组件包括:
(1)层:神经网络的基本结构,将输入张量转换为输出张量。
(2)模型:层构成的网络。
(3)损失函数:参数学习的目标函数,通过最小化损失函数来学习各种参数。
(4)优化器:如何是损失函数最小,这就涉及到优化器。
当然这些核心组件不是独立的,它们之间、以及它们与神经网络其他组件之间有密切关系。为便于大家理解,我们把这些关键组件及相互关系,用图3-1表示。

 

图3-1 神经网络关键组件及相互关系示意图
多个层链接在一起构成一个模型或网络,输入数据通过这个模型转换为预测值,然后损失函数把预测值与真实值进行比较,得到损失值(损失值可以是距离、概率值等),该损失值用于衡量预测值与目标结果的匹配或相似程度,优化器利用损失值更新权重参数,从而使损失值越来越小。这是一个循环过程,损失值达到一个阀值或循环次数到达指定次数,循环结束。
接下来利用Pytorch的nn工具箱,构建一个神经网络实例。nn中对这些组件都有现成包或类,可以直接使用,非常方便。

3.2实现神经网络实例

使用Pytorch构建神经网络使用的主要工具(或类)及相互关系,如图3-2所示。

图3-2 Pytorch实现神经网络主要工具及相互关系
从图3-2可知,构建网络层可以基于Module类或函数(nn.functional)。nn中的大多数层(layer)在functional中都有与之对应的函数。nn.functional中函数与nn.Module中的layer的主要区别是后者继承Module类,会自动提取可学习的参数。而nn.functional更像是纯函数。两者功能相同,性能也没有很大区别,那么如何选择呢?像卷积层、全连接层、dropout层等因含有可学习参数,一般使用nn.Module,而激活函数、池化层不含可学习参数,可以使用nn.functional中对应的函数。下面我们通过实例来说明如何使用nn构建一个网络模型。

3.2.1背景说明

这节将利用神经网络完成对手写数字进行识别的实例,来说明如何借助nn工具箱来实现一个神经网络,并对神经网络有个直观了解。在这个基础上,后续我们将对nn的各模块进行详细介绍。实例环境使用Pytorch1.0+,GPU或CPU,源数据集为MNIST。
主要步骤:
(1)利用Pytorch内置函数mnist下载数据
(2)利用torchvision对数据进行预处理,调用torch.utils建立一个数据迭代器
(3)可视化源数据
(4)利用nn工具箱构建神经网络模型
(5)实例化模型,并定义损失函数及优化器
(6)训练模型
(7)可视化结果
神经网络的结构如下:

3-3 神经网络结构图
使用两个隐含层,每层激活函数为Relu,最后使用torch.max(out,1)找出张量out最大值对应索引作为预测值。

3.2.2准备数据

(1)导人必要的模块

(2)定义一些超参数

(3)下载数据并对数据进行预处理

【说明】
①transforms.Compose可以把一些转换函数组合在一起;
②Normalize([0.5], [0.5])对张量进行归一化,这里两个0.5分别表示对张量进行归一化的全局平均值和方差。因图像是灰色的只有一个通道,如果有多个通道,需要有多个数字,如三个通道,应该是Normalize([m1,m2,m3], [n1,n2,n3])
③download参数控制是否需要下载,如果./data目录下已有MNIST,可选择False。
④用DataLoader得到生成器,这可节省内存。
⑤torchvision及data的使用第4章将详细介绍。

3.2.3可视化源数据

图3-4 MNIST源数据示例

3.2.4 构建模型

数据预处理之后,我们开始构建网络,创建模型。
(1)构建网络

(2)实例化网络

3.2.5 训练模型

训练模型,这里使用for循环,进行迭代。其中包括对训练数据的训练模型,然后用测试数据的验证模型。
(1)训练模型

最后5次迭代的结果
epoch: 15, Train Loss: 0.0047, Train Acc: 0.9995, Test Loss: 0.0543, Test Acc: 0.9839
epoch: 16, Train Loss: 0.0048, Train Acc: 0.9997, Test Loss: 0.0532, Test Acc: 0.9839
epoch: 17, Train Loss: 0.0049, Train Acc: 0.9996, Test Loss: 0.0544, Test Acc: 0.9839
epoch: 18, Train Loss: 0.0049, Train Acc: 0.9995, Test Loss: 0.0535, Test Acc: 0.9839
epoch: 19, Train Loss: 0.0049, Train Acc: 0.9996, Test Loss: 0.0536, Test Acc: 0.9836

这个神经网络的结构比较简单,只用了两层,也没有使用dropout层,迭代20次,测试准确率达到98%左右,效果还可以。不过,还是有提升空间,如果采用cnn,dropout等层,应该还可以提升模型性能。
(2)可视化训练及测试损失值

图3-5 MNIST数据集训练的损失值

3.3 如何构建神经网络?

上节我们用nn工具箱,搭建一个神经网络。步骤好像不少,但关键就是选择网络层,构建网络,然后选择损失和优化器。在nn工具箱中,可以直接引用的网络很多,有全连接层、卷积层、循环层、正则化层、激活层等等。假设这些层都定义好了,接下来就是如何组织或构建这些层?

3.3.1 构建网络层

在3.2小节实例中,我们采用torch.nn.Sequential()来构建网络层,这个有点类似Keras的models.Sequential(),使用起来就像搭积木一样,非常方便。不过,这种方法每层的编码是默认的数字,不易区分。
如果要对每层定义一个名称,我们可以采用Sequential的一种改进方法,在Sequential的基础上,通过add_module()添加每一层,并且为每一层增加一个单独的名字。
此外,还可以在Sequential基础上,通过字典的形式添加每一层,并且设置单独的层名称。
以下是采用字典方式构建网络的一个示例代码:

3.3.2 前向传播

定义好每层后,最后还需要通过前向传播的方式把这些串起来。这就是涉及如何定义forward函数的问题。forward函数的任务需要把输入层、网络层、输出层链接起来,实现信息的前向传导。该函数的参数一般为输入数据,返回值为输出数据。
在forward函数中,有些层来自nn.Module,也可以使用nn.functional定义。来自nn.Module的需要实例化,而使用nn.functional定义的可以直接使用。

3.3.3 反向传播

前向传播函数定义好以后,接下来就是梯度的反向传播。在第二章,介绍了实现梯度反向传播的方法。这里关键是利用复合函数的链式法则。深度学习中涉及很多函数,如果要自己手工实现反向传播,比较费时。好在Pytorch提供了自动反向传播的功能,使用nn工具箱,我们无需自己编写反向传播,直接让损失函数(loss)调用backward()即可,非常方便和高效!
在反向传播过程中,优化器是一个重要角色。优化方法很多,3.2节采用SGD优化器。此外,我们还可以选择其他优化器,3.7小节将介绍各种优化器的优缺点。

3.3.4 训练模型

层、模型、损失函数和优化器等都定义或创建好,接下来就是训练模型。训练模型时需要注意使模型处于训练模式,即调用model.train()。调用model.train()会把所有的module设置为训练模式。如果是测试或验证阶段,需要使模型处于验证阶段,即调用model.eval()。调用model.eval()会把所有的training属性设置为False。
缺省情况下梯度是累加的,需要手工把梯度初始化或清零,调用optimizer.zero_grad()即可。训练过程中,正向传播生成网络的输出,计算输出和实际值之间的损失值。 调用loss.backward()自动生成梯度,然后使用optimizer.step()执行优化器,把梯度传播回每个网络。
如果希望用GPU训练,需要把模型、训练数据、测试数据发送到GPU上,即调用.to(device)。如果需要使用多GPU进行处理,可使模型或相关数据引用nn.DataParallel。nn.DataParallel的具体使用在第4章将详细介绍。

3.4 nn.Module

前面我们使用autograd及Tensor实现机器学习实例时,需要做不少设置,如对叶子节点的参数requires_grad设置为True,然后调用backward,再从grad属性中提取梯度。对于大规模的网络,autograd太过于底层和繁琐。为了简单、有效解决这个问题,nn是一个有效工具。它是专门为深度学习设计的一个模块,而nn.Module是nn的一个核心数据结构。nn.Module可以是神经网络的某个层(layer),也可以是包含多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,生成自己的网络/层,如3.2小节实例中,我们定义的Net类就采用这种方法(class Net(torch.nn.Module))。nn中已实现了绝大多数层,包括全连接层、损失层、激活层、卷积层、循环层等等,这些层都是nn.Module的子类,能够自动检测到自己的Parameter,并将其作为学习参数,且针对GPU运行进行了CuDNN优化。

3.5 nn.functional

nn中的层,一类是继承了nn.Module,其命名一般为nn.Xxx(第一个是大写),如nn.Linear、nn.Conv2d、nn.CrossEntropyLoss等。另一类是nn.functional中的函数,其名称一般为nn.funtional.xxx,如nn.funtional.linear、nn.funtional.conv2d、nn.funtional.cross_entropy等。从功能来说两者相当,基于nn.Mudle能实现的层,使用nn.funtional也可实现,反之亦然,而且性能方面两者也没有太大差异。不过在具体使用时,两者还是有区别,主要区别如下:
(1)nn.Xxx继承于nn.Module,nn.Xxx 需要先实例化并传入参数,然后以函数调用的方式调用实例化的对象并传入输入数据。它能够很好的与nn.Sequential结合使用,而nn.functional.xxx无法与nn.Sequential结合使用。
(2)nn.Xxx不需要自己定义和管理weight、bias参数;而nn.functional.xxx需要你自己定义weight、bias,每次调用的时候都需要手动传入weight、bias等参数, 不利于代码复用。
(3)dropout操作在训练和测试阶段是有区别的,使用nn.Xxx方式定义dropout,在调用model.eval()之后,自动实现状态的转换,而使用nn.functional.xxx却无此功能。
总的来说,两种功能都是相同的,但PyTorch官方推荐:具有学习参数的(例如,conv2d, linear, batch_norm)采用nn.Xxx方式。没有学习参数的(例如,maxpool, loss func, activation func)等根据个人选择使用nn.functional.xxx或者nn.Xxx方式。3.2小节中使用激活层,我们采用F.relu来实现,即nn.functional.xxx方式。

3.6 优化器

Pytoch常用的优化方法都封装在torch.optim里面,其设计很灵活,可以扩展为自定义的优化方法。所有的优化方法都是继承了基类optim.Optimizer。并实现了自己的优化步骤。
最常用的优化算法就是梯度下降法及其各种变种,后续章节我们将介绍各种算法的原理,这类优化算法使用参数的梯度值更新参数。
3.2小节使用的随机梯度下降法(SGD)就是最普通的优化器,一般SGD并说没有加速效果, 3.2小节使用的SGD包含动量参数Momentum,它是SGD的改良版。
我们结合3.2小结内容,说明使用优化器的一般步骤为:
(1)建立优化器实例
导入optim模块,实例化SGD优化器,这里使用动量参数momentum(该值一般在(0,1)之间),是SGD的改进版,效果一般比不使用动量规则的要好。

以下步骤在训练模型的for循环中。
(2)向前传播
把输入数据传入神经网络Net实例化对象model中,自动执行forward函数,得到out输出值,然后用out与标记label计算损失值loss。

(3)清空梯度
缺省情况梯度是累加的,在梯度反向传播前,先需把梯度清零。

(4)反向传播
基于损失值,把梯度进行反向传播。

(5)更新参数
基于当前梯度(存储在参数的.grad属性中)更新参数。

3.7 动态修改学习率参数

修改参数的方式可以通过修改参数optimizer.params_groups或新建optimizer。新建optimizer比较简单,optimizer十分轻量级,所以开销很小。但是新的优化器会初始化动量等状态信息,这对于使用动量的优化器(momentum参数的sgd)可能会造成收敛中的震荡。所以,这里我们采用直接修改参数optimizer.params_groups。
optimizer.param_groups:长度1的list,optimizer.param_groups[0]:长度为6的字典,包括权重参数,lr,momentum等参数。

以下是3.2小节中动态修改学习率参数代码

3.8 优化器比较

Pytorch中的优化器很多,各种优化器一般都有其适应的场景,不过,像自适应优化器在深度学习中比较受欢迎,除了性能较好,鲁棒性性、泛化能力也更强。这里我们通过一个简单实例进行说明。
(1) 导入需要的模块

(2)生成数据

(3)构建神经网络

(4)使用多种优化器

(5)训练模型

(6)可视化结果

图3-6 多种优化器性能比较

3.9 小结

本章我们首先介绍了神经网络的核心组件,即层、模型、损失函数及优化器。然后,从一个完整实例开始,看Pytorch是如何使用其包、模块等来搭建、训练、评估、优化神经网络。最后详细剖析了Pytorch的工具箱nn以及基于nn的一些常用类或模块等,并用相关实例演示这些模块的功能。这章介绍了神经网络工具箱,下一章将介绍Pytorch的另一个强大工具箱,即数据处理工具箱。

第10章 Pandas基础


Python有了NumPy的Pandas,用Python处理数据就像使用Exel或SQL一样简单方便。
Pandas是基于NumPy的Python 库,它被广泛用于快速分析数据,以及数据清洗和准备等工作。可以把 Pandas 看作是 Python版的Excel或Table。Pandas 有两种数据结构:
Series和DataFrame,Pandas经过几个版本的更新,目前已经成为数据清洗、处理和分析的不二选择。
本章主要介绍Pandas的两个数据结构:
Serial简介
DataFrame简介

10.1 问题:Pandas有哪些优势?

科学计算方面NumPy是优势,但NumPy中没有标签,数据清理、数据处理就不是其强项了。而DataFrame有标签,就像SQL中的表一样,所以在数据处理方面DataFrame就更胜一筹了,具体包含以下几方面:
(1)读取数据方面
Pandas提供强大的IO读取工具,csv格式、Excel文件、数据库等都可以非常简便地读取,对于大数据,pandas也支持大文件的分块读取。
(2)在数据清洗方面
面对数据集,我们遇到最多的情况就是存在缺失值,Pandas把各种类型数据类型的缺失值统一称为NaN,Pandas提供许多方便快捷的方法来处理这些缺失值NaN。
(3)分析建模阶段
在分析建模阶段,Pandas自动且明确的数据对齐特性,非常方便地使新的对象可以正确地与一组标签对齐,由此,Pandas就可以非常方便地将数据集进行拆分-重组操作。
(4)结果可视化方面
结果展示方面,我们都知道Matplotlib是个数据视图化的好工具,Pandas与Matplotlib搭配,不用复杂的代码,就可以生成多种多样的数据视图。

10.2 Pandas数据结构

Pandas中两个最常用的对象是Series和DataFrame。使用pandas前,需导入以下内容:

Pandas主要采用Series和DataFrame两种数据结构。Series是一种类似一维数据的数据结构,由数据(values)及索引(indexs)组成,而DataFrame是一个表格型的数据结构,它有一组序列,每列的数据可以为不同类型(NumPy数据组中数据要求为相同类型),它既有行索引,也有列索引。

图10-1 DataFrame结构

10.3 Series

上章节我们介绍了多维数组(ndarray),当然,它也包括一维数组,Series类似一维数组,为啥还要介绍Series呢?或Series有哪些特点?
Series一个最大特点就是可以使用标签索引,序列及ndarray也有索引,但都是位置索引或整数索引,这种索引有很多局限性,如根据某个有意义标签找对应值,切片时采用类似[2:3]的方法,只能取索引为2这个元素等等,无法精确定位。
Series的标签索引(它位置索引自然保留)使用起来就方便多了,且定位也更精确,不会产生歧义。以下通过实例来说明。
(1)使用Series

0 1
1 3
2 6
3 -1
4 2
5 8
dtype: int64
(2)使用Series的索引

a 1
c 3
d 6
e -1
b 2
g 8
dtype: int64
(3)根据索引找对应值

10.4 DataFrame

DataFrame除了索引有位置索引也有标签索引,而且其数据组织方式与MySQL的表极为相似,除了形式相似,很多操作也类似,这就给操作DataFrame带来极大方便。这些是DataFrame特色的一小部分,它还有比数据库表更强大的功能,如强大统计、可视化等等。
DataFrame有几个要素:index、columns、values等,columns就像数据库表的列表,index是索引,values就是值。

图10-2 DataFrame结果

10.4.1 生成DataFrame

生成DataFrame有很多,比较常用的有导入等长列表、字典、numpy数组、数据文件等。

10.4.2 获取数据

获取DataFrame结构中数据可以采用obj[]操作、obj.iloc[]、obj.loc[]等命令。
(1)使用obj[]来获取列或行

(2)使用obj.loc[] 或obj.iloc[]获取行或列数据。
loc通过行标签获取行数据,iloc通过行号获取行数据。
loc 在index的标签上进行索引,范围包括start和end.
iloc 在index的位置上进行索引,不包括end.
这两者的主要区别可参考如下示例:

【说明】
除使用iloc及loc外,早期版本还有ix格式。pandas0.20.0及以上版本,ix已经丢弃,请尽量使用loc和iloc;

10.4.3 修改数据

我们可以像操作数据库表一样操作DataFrame,删除数据、插入数据、修改字段名、索引名、修改数据等,以下通过一些实例来说明。

图10-3 数据结构