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:
1 |
Anaconda3-2019.03-Linux-x86_64.sh |
(3)接下来根据安装提示,直接按回车即可。其间会提示选择安装路径,如果没有特殊要求,可以按回车使用默认路径(~/ anaconda3),然后就开始安装。
(4)安装完成后,程序提示是否把anaconda3的binary路径加入到当前用户的.bashrc配置文件中,建议添加。添加以后,就可以使用python、ipython命令时自动使用Anaconda3的python环境。
(5)安装Pytorch
登录Pytorch官网:https://pytorch.org/,登录后,可看到图2-2 所示界面,然后选择对应项。
图2-2 Pytorch 安装界面
把第⑥项内容,复制到命令行,执行即可。
1 |
conda install pytorch-cpu torchvision-cpu -c 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的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#cat test_gpu.py import torch if __name__ == '__main__': #测试 CUDA print("Support CUDA ?: ", torch.cuda.is_available()) x = torch.tensor([10.0]) x = x.cuda() print(x) y = torch.randn(2, 3) y = y.cuda() print(y) z = x + y print(z) # 测试 CUDNN from torch.backends import cudnn print("Support cudnn ?: ",cudnn.is_acceptable(x)) |
在命令行运行以下脚本:
1 |
python test_gpu.py |
如果可以看到如图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)生成配置文件
1 |
jupyter notebook --generate-config |
将在当前用户目录下生成文件:.jupyter/jupyter_notebook_config.py
(2)生成当前用户登录jupyter密码
打开ipython, 创建一个密文密码
1 2 3 4 |
In [1]: from notebook.auth import passwd In [2]: passwd() Enter password: Verify password: |
(3)修改配置文件
1 |
vim ~/.jupyter/jupyter_notebook_config.py |
进行如下修改:
1 2 3 4 |
c.NotebookApp.ip='*' # 就是设置所有ip皆可访问 c.NotebookApp.password = u'sha:ce...刚才复制的那个密文' c.NotebookApp.open_browser = False # 禁止自动打开浏览器 c.NotebookApp.port =8888 #这是缺省端口,也可指定其他端口 |
(4)启动jupyter notebook
1 2 |
#后台启动jupyter:不记日志: nohup jupyter notebook >/dev/null 2>&1 & |
在浏览器上,输入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被修改。
1 2 3 4 5 6 7 8 9 |
import torch x=torch.tensor([1,2]) y=torch.tensor([3,4]) z=x.add(y) print(z) print(x) x.add_(y) print(x) |
运行结果
tensor([4, 6])
tensor([1, 2])
tensor([4, 6])
2.4.2 创建Tensor
新建tensor的方法很多,可以从列表或ndarray等类型进行构建,也可根据指定的形状构建。常见的构建tensor的方法,可参考表2-1。
表2-1 常见的新建tensor方法
下面举例说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import torch #根据list数据生成tensor torch.Tensor([1,2,3,4,5,6]) #根据指定形状生成tensor torch.Tensor(2,3) #根据给定的tensor的形状 t=torch.Tensor([[1,2,3],[4,5,6]]) #查看tensor的形状 t.size() #shape与size()等价方式 t.shape #根据已有形状创建tensor torch.Tensor(t.size()) |
【说明】注意torch.Tensor与torch.tensor的几点区别
①torch.Tensor是torch.empty和torch.tensor之间的一种混合,但是,当传入数据时,torch.Tensor使用全局默认dtype(FloatTensor),torch.tensor从数据中推断数据类型。
②torch.tensor(1)返回一个固定值1,而torch.Tensor(1)返回一个大小为1的张量,它是随机初始化的值。
1 2 3 4 5 |
import torch t1=torch.Tensor(1) t2=torch.tensor(1) print("t1的值{},t1的数据类型{}".format(t1,t1.type())) print("t2的值{},t2的数据类型{}".format(t2,t2.type())) |
运行结果
t1的值tensor([3.5731e-20]),t1的数据类型torch.FloatTensor
t2的值1,t2的数据类型torch.LongTensor
根据一定规则,自动生成tensor的一些例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import torch #生成一个单位矩阵 torch.eye(2,2) #自动生成全是0的矩阵 torch.zeros(2,3) #根据规则生成数据 torch.linspace(1,10,4) #生成满足均匀分布随机数 torch.rand(2,3) #生成满足标准分布随机数 torch.randn(2,3) #返回所给数据形状相同,值全为0的张量 torch.zeros_like(torch.rand(2,3)) |
2.4.3 修改Tensor形状
在处理数据、构建网络层等过程中,经常需要了解Tensor的形状、修改Tensor的形状。
与修改Numpy的形状类似,修改tenor的形状也有很多类似函数,具体可参考表2-2。 表2-2 为tensor常用修改形状的函数。
以下为一些实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import torch #生成一个形状为2x3的矩阵 x = torch.randn(2, 3) #查看矩阵的形状 x.size() #结果为torch.Size([2, 3]) #查看x的维度 x.dim() #结果为2 #把x变为3x2的矩阵 x.view(3,2) #把x展平为1维向量 y=x.view(-1) y.shape #添加一个维度 z=torch.unsqueeze(y,0) #查看z的形状 z.size() #结果为torch.Size([1, 6]) #计算Z的元素个数 z.numel() #结果为6 |
【说明】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 常用选择操作函数
以下为部分函数的实现代码:
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 |
import torch #设置一个随机种子 torch.manual_seed(100) #生成一个形状为2x3的矩阵 x = torch.randn(2, 3) #根据索引获取第1行,所有数据 x[0,:] #获取最后一列数据 x[:,-1] #生成是否大于0的Byter张量 mask=x>0 #获取大于0的值 torch.masked_select(x,mask) #获取非0下标,即行,列索引 torch.nonzero(mask) #获取指定索引对应的值,输出根据以下规则得到 #out[i][j] = input[index[i][j]][j] # if dim == 0 #out[i][j] = input[i][index[i][j]] # if dim == 1 index=torch.LongTensor([[0,1,1]]) torch.gather(x,0,index) index=torch.LongTensor([[0,1,1],[1,1,1]]) a=torch.gather(x,1,index) #把a的值返回到一个2x3的0矩阵中 z=torch.zeros(2,3) z.scatter_(1,index,a) |
2.4.5 广播机制
本书1.7小节介绍了Numpy的广播机制,是向量运算的重要技巧。Pytorch也支持广播规则,以下通过几个示例进行说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import torch import numpy as np A = np.arange(0, 40,10).reshape(4, 1) B = np.arange(0, 3) #把ndarray转换为Tensor A1=torch.from_numpy(A) #形状为4x1 B1=torch.from_numpy(B) #形状为3 #Tensor自动实现广播 C=A1+B1 #我们可以根据广播机制,手工进行配置 #根据规则1,B1需要向A1看齐,把B变为(1,3) B2=B1.unsqueeze(0) #B2的形状为1x3 #使用expand函数重复数组,分别的4x3的矩阵 A2=A1.expand(4,3) B3=B2.expand(4,3) #然后进行相加,C1与C结果一致 C1=A2+B3 |
2.4.6 逐元素操作
与Numpy一样,tensor也有逐元素操作(element-wise),操作内容相似,但使用函数可能不尽相同。大部分数学运算都属于逐元操作,逐元素操作输入与输出的形状相同。,常见的逐元素操作,可参考表2-4。
表2-4常见逐元素操作
【说明】这些操作均创建新的tensor,如果需要就地操作,可以使用这些方法的下划线版本,例如abs_。
以下为部分逐元素操作代码实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import torch t = torch.randn(1, 3) t1 = torch.randn(3, 1) t2 = torch.randn(1, 3) #t+0.1*(t1/t2) torch.addcdiv(t, 0.1, t1, t2) #计算sigmoid torch.sigmoid(t) #将t限制在[0,1]之间 torch.clamp(t,0,1) #t+2进行就地运算 t.add_(2) |
2.4.7 归并操作
归并操作顾名思义,就是对输入进行归并或合计等操作,这类操作的输入输出形状一般不相同,而且往往是输入大于输出形状。归并操作可以对整个tensor,也可以沿着某个维度进行归并。常见的归并操作可参考表2-5。
表2-5 常见的归并操作
【说明】
归并操作一般涉及一个dim参数,指定沿哪个维进行归并。另一个参数是keepdim,说明输出结果中是否保留维度1,缺省情况是False,即不保留。
以下为归并操作的部分代码
1 2 3 4 5 6 7 8 9 10 |
import torch #生成一个含6个数的向量 a=torch.linspace(0,10,6) #使用view方法,把a变为2x3矩阵 a=a.view((2,3)) #沿y轴方向累加,即dim=0 b=a.sum(dim=0) #b的形状为[3] #沿y轴方向累加,即dim=0,并保留含1的维度 b=a.sum(dim=0,keepdim=True) #b的形状为[1,3] |
2.4.8 比较操作
比较操作一般进行逐元素比较,有些是按指定方向比较。常用的比较函数可参考表2-6。
表2-6 常用的比较函数
以下是部分函数的代码实现。
1 2 3 4 5 6 7 8 9 |
import torch x=torch.linspace(0,10,6).view(2,3) #求所有元素的最大值 torch.max(x) #结果为10 #求y轴方向的最大值 torch.max(x,dim=0) #结果为[6,8,10] #求最大的2个元素 torch.topk(x,1,dim=0) #结果为[6,8,10],对应索引为tensor([[1, 1, 1] |
2.4.9 矩阵操作
机器学习和深度学习中存在大量的矩阵运算,用的比较多的有两种,一种是逐元素乘法,另外一种是点积乘法。Pytorch中常用的矩阵函数可参考表2-7。
表2-7 常用矩阵函数
【说明】
①torch的dot与Numpy的dot有点不同,torch中dot对两个为1D张量进行点积运算,Numpy中的dot无此限制。
②mm是对2D的矩阵进行点积,bmm对含batch的3D进行点积运算。
③转置运算会导致存储空间不连续,需要调用contiguous方法转为连续。
1 2 3 4 5 6 7 8 9 10 11 12 |
import torch a=torch.tensor([2, 3]) b=torch.tensor([3, 4]) torch.dot(a,b) #运行结果为18 x=torch.randint(10,(2,3)) y=torch.randint(6,(3,4)) torch.mm(x,y) x=torch.randint(10,(2,2,3)) y=torch.randint(6,(2,3,4)) torch.bmm(x,y) |
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)定义叶子节点及算子节点
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import torch #定义输入张量x x=torch.Tensor([2]) #初始化权重参数W,偏移量b、并设置require_grad属性为True,为自动求导 w=torch.randn(1,requires_grad=True) b=torch.randn(1,requires_grad=True) #实现前向传播 y=torch.mul(w,x) #等价于w*x z=torch.add(y,b) #等价于y+b #查看x,w,b页子节点的requite_grad属性 print("x,w,b的require_grad属性分别为:{},{},{}".format(x.requires_grad,w.requires_grad,b.requires_grad)) |
运行结果
x,w,b的require_grad属性分别为:False,True,True
(2)查看叶子节点、非叶子节点的其他属性
1 2 3 4 5 6 7 8 9 10 11 12 |
#查看非叶子节点的requres_grad属性, print("y,z的requires_grad属性分别为:{},{}".format(y.requires_grad,z.requires_grad)) #因与w,b有依赖关系,故y,z的requires_grad属性也是:True,True #查看各节点是否为叶子节点 print("x,w,b,y,z的是否为叶子节点:{},{},{},{},{}".format(x.is_leaf,w.is_leaf,b.is_leaf,y.is_leaf,z.is_leaf)) #x,w,b,y,z的是否为叶子节点:True,True,True,False,False #查看叶子节点的grad_fn属性 print("x,w,b的grad_fn属性:{},{},{}".format(x.grad_fn,w.grad_fn,b.grad_fn)) #因x,w,b为用户创建的,为通过其他张量计算得到,故x,w,b的grad_fn属性:None,None,None #查看非叶子节点的grad_fn属性 print("y,z的是否为叶子节点:{},{}".format(y.grad_fn,z.grad_fn)) #y,z的是否为叶子节点:, |
(3)自动求导,实现梯度方向传播,即梯度的反向传播。
1 2 3 4 5 6 7 8 9 10 11 12 |
#基于z张量进行梯度反向传播,执行backward之后计算图会自动清空, z.backward() #如果需要多次使用backward,需要修改参数retain_graph为True,此时梯度是累加的 #z.backward(retain_graph=True) #查看叶子节点的梯度,x是叶子节点但它无需求导,故其梯度为None print("参数w,b的梯度分别为:{},{},{}".format(w.grad,b.grad,x.grad)) #参数w,b的梯度分别为:tensor([2.]),tensor([1.]),None #非叶子节点的梯度,执行backward之后,会自动清空 print("非叶子节点y,z的梯度分别为:{},{}".format(y.grad,z.grad)) #非叶子节点y,z的梯度分别为:None,None |
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 |
backward(gradient=None, retain_graph=None, create_graph=False) |
上面说的可能有点抽象,下面我们通过一个实例进行说明。
(1)定义叶子叶子节点及计算节点
1 2 3 4 5 6 7 8 9 10 11 12 |
import torch #定义叶子节点张量x,形状为1x2 x= torch.tensor([[2, 3]], dtype=torch.float, requires_grad=True) #初始化Jacobian矩阵 J= torch.zeros(2 ,2) #初始化目标张量,形状为1x2 y = torch.zeros(1, 2) #定义y与x之间的映射关系: #y1=x1**2+3*x2,y2=x2**2+2*x1 y[0, 0] = x[0, 0] ** 2 + 3 * x[0 ,1] y[0, 1] = x[0, 1] ** 2 + 2 * x[0, 0] |
(2)手工计算y对x的梯度
我们先手工计算一下y对x的梯度,为了验证Pytorch的backward的结果是否正确。
y对x的梯度是一个雅可比矩阵,各项的值,我们可通过以下方法进行计算。
(3)调用backward获取y对x的梯度
1 2 3 |
y.backward(torch.Tensor([[1, 1]])) print(x.grad) #结果为tensor([[6., 9.]]) |
这个结果与我们手工运算的不符,显然这个结果是错误的,错在哪里呢?这个结果的计算过程是:
由此,错在v的取值错误,通过这种方式得的到并不是y对x的梯度。这里我们可以分成两步的计算。首先让v=(1,0)得到y_1对x的梯度,然后使v=(0,1),得到y_2对x的梯度。这里因需要重复使用backward(),需要使参数retain_graph=True,具体代码如下:
1 2 3 4 5 6 7 8 9 10 |
#生成y1对x的梯度 y.backward(torch.Tensor([[1, 0]]),retain_graph=True) J[0]=x.grad #梯度是累加的,故需要对x的梯度清零 x.grad = torch.zeros_like(x.grad) #生成y2对x的梯度 y.backward(torch.Tensor([[0, 1]])) J[1]=x.grad #显示jacobian矩阵的值 print(J) |
运行结果
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)导入需要的库
1 2 3 4 5 |
# -*- coding: utf-8 -*- import numpy as np %matplotlib inline from matplotlib import pyplot as plt |
(2)生成输入数据x及目标数据y
设置随机数种子,生成同一个份数据,以便用多种方法进行比较。
1 2 3 |
np.random.seed(100) x = np.linspace(-1, 1, 100).reshape(100,1) y = 3*np.power(x, 2) +2+ 0.2*np.random.rand(x.size).reshape(100,1) |
(3)查看x,y数据分布情况
1 2 3 |
# 画图 plt.scatter(x, y) plt.show() |
图2-10 Numpy实现的源数据
(4)初始化权重参数
1 2 3 |
# 随机初始化参数 w1 = np.random.rand(1,1) b1 = np.random.rand(1,1) |
(5)训练模型
定义损失函数,假设批量大小为100:
用代码实现上面这些表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
lr =0.001 # 学习率 for i in range(800): # 前向传播 y_pred = np.power(x,2)*w1 + b1 # 定义损失函数 loss = 0.5 * (y_pred - y) ** 2 loss = loss.sum() #计算梯度 grad_w=np.sum((y_pred - y)*np.power(x,2)) grad_b=np.sum((y_pred - y)) #使用梯度下降法,是loss最小 w1 -= lr * grad_w b1 -= lr * grad_b |
(6)可视化结果
1 2 3 4 5 6 7 |
plt.plot(x, y_pred,'r-',label='predict') plt.scatter(x, y,color='blue',marker='o',label='true') # true data plt.xlim(-1,1) plt.ylim(2,6) plt.legend() plt.show() print(w1,b1) |
运行结果:
图2-11 可视化Numpy学习结果
[[2.95859544]] [[2.10178594]]
从结果看来,学习效果还是比较理想的。
2.7 使用Tensor及antograd实现机器学习
2.6节可以说是纯手工完成一个机器学习任务,数据用Numpy表示,梯度及学习是自己定义并构建学习模型。这种方法适合于比较简单的情况,如果稍微复杂一些,代码量将几何级增加。是否有更方便的方法呢?这节我们将使用Pytorch的自动求导的一个包antograd,利用这个包及对应的Tensor,便可利用自动反向传播来求梯度,无需手工计算梯度。以下是具体实现代码。
(1)导入需要的库
1 2 3 4 |
import torch as t %matplotlib inline from matplotlib import pyplot as plt |
(2)生成训练数据,并可视化数据分布情况
1 2 3 4 5 6 7 8 9 10 |
t.manual_seed(100) dtype = t.float #生成x坐标数据,x为tenor,需要把x的形状转换为100x1 x = t.unsqueeze(torch.linspace(-1, 1, 100), dim=1) #生成y坐标数据,y为tenor,形状为100x1,另加上一些噪音 y = 3*x.pow(2) +2+ 0.2*torch.rand(x.size()) # 画图,把tensor数据转换为numpy数据 plt.scatter(x.numpy(), y.numpy()) plt.show() |
图2-12 可视化输入数据
(3)初始化权重参数
1 2 3 |
# 随机初始化参数,参数w,b为需要学习的,故需requires_grad=True w = t.randn(1,1, dtype=dtype,requires_grad=True) b = t.zeros(1,1, dtype=dtype, requires_grad=True) |
(4)训练模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
lr =0.001 # 学习率 for ii in range(800): # 前向传播,并定义损失函数loss y_pred = x.pow(2).mm(w) + b loss = 0.5 * (y_pred - y) ** 2 loss = loss.sum() # 自动计算梯度,梯度存放在grad属性中 loss.backward() # 手动更新参数,需要用torch.no_grad(),使上下文环境中切断自动求导的计算 with t.no_grad(): w -= lr * w.grad b -= lr * b.grad # 梯度清零 w.grad.zero_() b.grad.zero_() |
(5)可视化训练结果
1 2 3 4 5 6 7 8 |
plt.plot(x.numpy(), y_pred.detach().numpy(),'r-',label='predict')#predict plt.scatter(x.numpy(), y.numpy(),color='blue',marker='o',label='true') # true data plt.xlim(-1,1) plt.ylim(2,6) plt.legend() plt.show() print(w, b) |
运行结果:
图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)导入库及生成训练数据
1 2 3 4 5 6 7 8 |
# -*- coding: utf-8 -*- import tensorflow as tf import numpy as np #生成训练数据 np.random.seed(100) x = np.linspace(-1, 1, 100).reshape(100,1) y = 3*np.power(x, 2) +2+ 0.2*np.random.rand(x.size).reshape(100,1) |
(2)初始化参数
1 2 3 4 5 6 7 8 9 |
# 创建两个占位符,分别用来存放输入数据x和目标值y #运行计算图时,导入数据. x1 = tf.placeholder(tf.float32, shape=(None, 1)) y1 = tf.placeholder(tf.float32, shape=(None, 1)) # 创建权重变量w和b,并用随机值初始化. # TensorFlow 的变量在整个计算图保存其值. w = tf.Variable(tf.random_uniform([1], 0, 1.0)) b = tf.Variable(tf.zeros([1])) |
(3)实现前向传播及损失函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 前向传播,计算预测值. y_pred = np.power(x,2)*w + b # 计算损失值 loss=tf.reduce_mean(tf.square(y-y_pred)) # 计算有关参数w、b关于损失函数的梯度. grad_w, grad_b = tf.gradients(loss, [w, b]) #用梯度下降法更新参数. # 执行计算图时给 new_w1 和new_w2 赋值 # 对TensorFlow 来说,更新参数是计算图的一部分内容 # 而PyTorch,这部分是属于计算图之外. learning_rate = 0.01 new_w = w.assign(w - learning_rate * grad_w) new_b = b.assign(b - learning_rate * grad_b) |
(4)训练模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 已构建计算图, 接下来创建TensorFlow session,准备执行计算图. with tf.Session() as sess: # 执行之前需要初始化变量w、b sess.run(tf.global_variables_initializer()) for step in range(2000): # 循环执行计算图. 每次需要把x1,y1赋给x和y. # 每次执行计算图时,需要计算关于new_w和new_b的损失值, # 返回numpy多维数组 loss_value, v_w, v_b = sess.run([loss, new_w, new_b], feed_dict={x1: x, y1: y}) if step%200==0: #每200次打印一次训练结果 print("损失值、权重、偏移量分别为{:.4f},{},{}".format(loss_value,v_w,v_b)) |
(5)可视化结果
1 2 3 4 |
# 可视化结果 plt.figure() plt.scatter(x,y) plt.plot (x, v_b + v_w*x**2) |
最后五次输出结果:
损失值、权重、偏移量分别为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等技术分别实现同一个机器学习任务。