第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 数据结构

10.4.4 汇总统计

Pandas有一组常用的统计方法,可以根据不同轴方向进行统计,当然也可按不同的列或行进行统计,非常方便。
常用的统计方法有:
表10-1 Pandas统计方法

以下通过实例来说明这些方法的使用
(1)把csv数据导入pandas

(2)查看df的统计信息

【说明】
var:表示方差: σ^2=∑▒〖(X-μ)〗^2/N (10.1)
即各项-均值的平方求和后再除以N 。
std:表示标准差,是var的平方根。

10.4.5选择部分列

这里选择学生代码、课程代码、课程名称、程程成绩,注册日期等字段

10.4.6删除重复数据

如果有重复数据(对df1的所有列),则删除最后一条记录。

10.4.7补充缺省值

(1)用指定值补充NaN值
这里要求把stat_date的缺省值(NaN)改为'2018-09-01'

(2)可视化,并在图形上标准数据

结果为:

导入一些库及支持中文的库

画图

运行结果


图10-4 可视化结果

10.4.8从MySQL中获取数据

(1)从MySQL数据库中获取学生基本信息表
Python连接MySQL数据库是通过pymysql这个桥梁,这个Python的第三方库,需要安装,安装命令如下:

以下是从数据库获取学习具体代码:

(2)查看df_info前3行数据

(3)选择前两个字段

(4)df2 与df_info1 根据字段stud_code 进行内关联

(5)对df3 根据字段stud_code,sub_code进行分组,并求平均每个同学各科的平均成绩。

【备注】
如果需要合计各同学的成绩,可用如下语句。

(6)选择数学分析课程,并根据成绩进行降序。

(7)取前5名

注:DataFrame数据结构的函数或方法有很多,大家可以通过df.[Tab键]方式查看,具体命令的使用方法,如df.count(),可以在Ipython命令行下输入:?df.count() 查看具体使用,退出帮助界面,按q即可。

10.4.9把pandas数据写入excel

把pandas数据写入excel中的sheet中

10.4.10 应用函数及映射

我们知道数据库中有很多函数可用作用于表中元素,DataFrame也可将函数(内置或自定义)应用到各列或行上,而且非常方便和简洁,具体可用通过DataFrame的apply,使或applymap或map,也可以作用到元素级。以下通过实例说明具体使用。

10.4.11 时间序列

pandas最基本的时间序列类型就是以时间戳(时间点)(通常以python字符串或datetime对象表示)为索引的Series:

索引为日期的DataFrame数据的索引、选取以及子集构造

10.4.12 数据离散化

如何离散化连续性数据?在一般开发语言中,可以通过控制语句来实现,但如果分类较多时,这种方法不但繁琐,效率也比较低。在Pandas中是否有更好方法?如果有,又该如何实现呢?
pandas有现成方法,如cut或qcut等,不需要编写代码,至于如何使用还是通过实例来说明。

现在需要对age字段进行离散化, 划分为(20,30],(30,40],(40,50].

10.4.13 交叉表

我们平常看到的数据格式大多像数据库中的表,如购买图书的基本信息:
表10-2 客户购买图书信息

这样的数据比较规范,比较适合于一般的统计分析。但如果我们想查看客户购买各种书的统计信息,就需要把以上数据转换为如下格式:
表10-3 客户购买图书的对应关系

我们观察一下不难发现,把表10-3中书代码列旋转为行就得到表2数据。如何实现行列的互换呢?编码能实现,但比较麻烦,还好,pandas提供了现成的方法或函数,如stack、unstack、pivot_table函数等。以下以pivot_table为例说明具体实现。

实现行列互换,把书代码列转换为行或索引

10.5 后续思考

(1)生成一个类似下例的DataFrame,C,D两列为随机数,可以与下表中数据不一致。

(2)求第(1)题的DataFrame中,C列的平均值,最大值。
(3)从第(1)题的dataframe中得到如下结果:

10.6 小结

本章介绍了Pandas的两个数据类型:Series和DataFrame。Series 是一种一维的数据类型,其中的每个元素都有各自的标签。DataFrame 是一个二维的、表格型的数据结构。Pandas 的Dataframe可以储存许多不同类型的数据,并且每个轴都有标签,可以把它当作一个Series 的字典。Pandas在数据清理、数据处理、数据可视化等方面有比较明显优势。