本节主要介绍PyTorch的基本概念(如Tensor和Variable)、自动微分和PyTorch的核心模块。
和其他深度学习框架一样,PyTorch在实现过程中也提出了自己的概念,包括张量。PyTorch中的张量用Tensor表示。初学者可能不知道张量是什么。其实,张量可以简单地理解为一个多维数组,类似于NumPy中的ndarray对象。
多维数组可以用相册来形象地解释。假设小米有相册A,相册A包含 N 张图片,每张图片的宽度为 W 、高度为 H 。由于是彩色图片,在计算机中用RGB三通道表示(所有颜色都可以用红、绿、蓝三原色按照不同的比例调配出来,红、绿、蓝三通道不同的像素亮度值叠加在一起就呈现出不同的颜色,所以整张图片呈现出彩色),所以通道数为3。
PyTorch采用四维数组表示这个相册,形如[ N , W , H , C ],其中 C 表示通道数。这种多维数组表示的好处可以从计算机组成原理说起,现代计算机都是多核多处理器的,支持多线程和多进程,非常适合矩阵的并行计算,而且计算一批数据和计算一个数据的调度时间是差不多的,因此科学计算往往都是基于矩阵的计算,并且会指定一个适当的Batch。例如,PyTorch视觉处理中通常将Batch指定为64、128或256,这也是为了充分利用计算机资源而考虑的。基于张量的乘法运算如图1.8所示。
张量就是多维数组,并且提供了CPU设备和GPU设备的支持。如果机器上有GPU设备,就可以在Tensor上调用cuda方法,将Tensor数据加载到GPU设备中运行,提升运行速度。张量上提供了很多有用的方法,这些方法和NumPy中的方法类似,使用过NumPy的读者肯定会觉得这些方法既眼熟又亲切。Torch上的算子多达上百个,对于如此多的算子,读者没有必要全部记住,只需要在使用的时候查字典即可。借助help(function)即可快速查看常用方法的定义及使用。
图1.8 基于张量的乘法运算
Tensor可以和NumPy进行无缝转换,在Tensor上可以使用numpy方法将Tensor转换为NumPy中的ndarray对象;ndarray对象也可以通过Torch提供的form_numpy方法转换成Tensor对象。如图1.9所示,Tensor多维数组上常用的算子也很多,读者学会查字典即可。
图1.9 Tensor多维数组上常用的算子
创建张量需要借助Torch提供的Tensor方法或from_numpy方法,理论上可以创建任意维度的多维数组,但最常用的Tensor不超过五维,常见的多维数组如图1.10所示。
除了维度,还可以指定每个维度上的SIZE,如a=torch.Tensor(3,4,5,8),表示创建了一个四维数组,因为Tensor有4个参数,每个参数表示对应维度的大小,第一维度的大小为3,第二维度的大小为4,以此类推。因此,变量a可以表示Batch数为3、宽度为4、高度为5、通道数为8的特征图(Feature Map,图像卷积运算产生的中间特征)。
图1.10 常见的多维数组
在一个Tensor上可以使用shape方法获取该张量的维度及每个维度的Size。下面依次介绍零维到五维Tensor及其使用场景。
1.零维标量
物理学中对标量的解释是只有大小没有方向,如温度、质量、湿度等。PyTorch中将只包含一个元素的变量称为标量。
2.一维向量
数学中表示既有大小又有方向的量为一维向量。PyTorch中用Array数组表示向量,如scores=torch.Tensor([88,89,91,91,99,100]),表示考试分数的集合。
3.二维矩阵
矩阵表示多条记录形成的表格,如学生成绩表。它的特点是多行、多列。矩阵是科学计算中最常见的数据结构。例如,Sklearn中的鸢尾花数据集是150条不同品种鸢尾花的数据记录,包含4个特征,分别是花瓣的长度、宽度,以及花萼的长度、宽度。这是一个典型的表格数据,通过load_iris方法可以导入该数据集,可以使用from_numpy方法创建Tensor。
4.三维立方体
将多个二维矩阵叠加在一起就可以形成三维立方体。三维Tensor常用于表示图片数据[width,height,channel],如图1.11所示。
5.四维多立方体
四维Tensor常见于批量的图片数据,如之前介绍的相册就是多张图片的叠加,最常见的形式为[batch,width,height,channel]。
图1.11 图片的张量表示
6.五维表示多个四维Tensor的叠加
五维Tensor常见于视频数据,视频按帧数划分,如50fps(帧/秒),如果一个视频的时长为1 min,每个单位时间有50张图片,共有60个这样的单位时间,frames=60,那么其表现形式为[frames,batch,width,height,channel]。
PyTorch中另外一个和Tensor相关的概念是Variable变量。Variable是对Tensor的封装,是一种特殊的张量,位于torch.autograd自动微分模块中,是为实现自动微分而提出的一种特殊的数据结构。在PyTorch 0.4.1之前的版本中,Variable的应用非常广泛。但是到PyTorch 1.0之后,这个数据结构被标注为deprecated,表示已经过时,并且Variable中的grad和grad_fn等属性也被转移到Tensor中,这个改变进一步精简了接口。由于市面上还存在大量老版本的代码,所以本书也对Variable进行简单介绍。创建Variable需要借助torch.autograd中提供的Variable方法,它接收Tensor类型的参数。下面通过FloatTensor创建Variable。
PyTorch 1.0之后的版本通过Variable方法创建的变量的返回值仍然是Tensor,这说明Variable已经被弃用,所有在Variable上的属性和方法在Tensor上都有。这是版本升级后需要注意的地方。截至目前,对Variable仍然是兼容的。Variable上的方法和Tensor类似,也非常丰富,多达数百个,因此在使用这些算子时要学会查字典,弄不明白的算子可以使用Python中的Help命令查看文档。Variable上常见的算子如图1.12所示。
图1.12 Variable上常见的算子
神经网络计算优化的过程大体上可以分为前向传播计算损失和反向传播更新梯度。如果没有自动微分的支持,就需要手动计算偏导数,然后更新权重参数。这对于数学基础较差的读者来说可能太难。而PyTorch基于Aten构建的自动微分系统能为我们完成这一切,因为梯度的计算对于调用者来说是透明的,这大大降低了手写梯度函数带来的高门槛。
假设有一份房价数据,我们打算通过 Y = WX + b 的方式拟合房价。在这个线性方程中, W 和 b 是需要计算的参数,而 X 和 Y 由训练数据提供。训练开始时我们随机初始化 W 和 b ,当输入 X 训练数据后,可以通过 WX + b 这样的线性方程计算出一个 Y *值。 Y *值和真正的 Y 值存在明显的误差,因为随机初始化 W 和 b ,所以可以用 Y *和 Y 计算出一个损失值Loss。根据该损失值对 W 和 b 计算偏导数,而偏导数正好可以表示 W 和 b 对误差的贡献程度,因此可以使用计算出来的偏导数更新权重参数 W 和 b ,使它们向着误差减小的方向靠近。经过几次迭代之后, W 和 b 就被更新到足够能拟合数据的程度,这时停止迭代。此时我们的模型就学习到了 Y = WX + b 的权重参数,而在该过程中,PyTorch自动微分系统自动完成了偏导数的计算。
实际上,PyTorch自动微分系统比上面的例子要复杂得多,它需要适配所有的函数类型,而函数类型是非常多的,但有了自动微分系统之后,大部分函数偏导数的计算都可以自动完成,包括计算复杂的矩阵偏导数。只有在少数情况下需要扩展Function。
在PyTorch中,为了能够支持有向无环图(Directed Acyclic Graph,DAG)的构建和反向传播的链式法则,引入了一种特殊的数据结构,这个数据结构正是Tensor。在PyTorch 0.4.1之前,这个数据结构是Variable。例如, y =sin( a × X + b )× X 的计算图,在计算图中所有的输入表示叶子节点,而输出则是根节点。当计算拓扑沿着有向无环图进行计算得出output后,通过output计算出损失值,由链式法则可以将梯度传回叶子节点,从而更新权重参数。在PyTorch的底层实现中,除了输入、输出,所有的节点都是一个Function对象。在这个对象中实现了从in到out的计算逻辑,并且可以通过apply()执行。从输入到输出的前向传播过程中,自动微分系统一边执行前向计算,一边搭建graph计算图,而图的节点是用于计算梯度的函数,这些函数保存在Tensor的.grad_fn中。当反向传播时,会用Tensor中记录的.grad_fn来计算相应的梯度。有向无环图的计算拓扑图如图1.13所示,该拓扑图展示了方程式sin( a × X + b )× X 的计算过程,计算节点为Function对象,图中的*表示乘号。
图1.13 有向无环图的计算拓扑图
Tensor中有requires_grad属性,代表是否需要计算梯度,这个属性的默认值为False。可以通过requires_grad参数显式地指定Tensor需要计算梯度。
在有向无环图计算图的构建过程中,Tensor是否需要计算梯度应遵循以下几个原则。
●如果计算拓扑中的输入有一个Tensor是需要计算梯度的,则依赖该Tensor所计算出来的Tensor也是需要计算梯度的。
●如果所有的输入都不需要计算梯度,则使用输入计算出的Tensor也不需要计算梯度。
requires_grad属性在迁移学习中进行模型微调的时候特别有用,因为在迁移学习中我们需要冻结学习好的参数,而对于需要微调的参数则指定requires_grad为True,使其在迭代过程中更新和学习。例如,使用ResNet18预训练模型进行迁移学习,通过模型上的parameters方法获取所有计算拓扑中的Parameter对象,设置requires_grad属性为False,这样在训练时这些参数就会被冻结,不会在反向传播过程中更新。我们只需要修改最后的FC(Full Connection),使其适合自己的业务需求,在进行二分类时将FC输出层的神经单元更改为2即可。Linear()函数单元的Parameter对象的requires_grad属性的默认值为True,因此这个模型就变为二分类模型。
任何好的软件系统必定是经过精心设计和抽象的,PyTorch脱胎于2012年使用Lua语言编写的Torch项目,该项目无论是计算架构设计还是扩展性都是一流的,特别是动态图更是让人耳目一新。PyTorch将系统的实现抽象为不同的模块,最核心的模块是Tensor,它是整个动态图的基础,自动微分及反向传播都需要借助该模块。Torch为创建Tensor提供了多种方法,如torch.randn、torch.randint等。Tensor上实现了大量的方法,如sum、argmin、argmax等。Tensor也是torch.nn模块构建网络的核心,网络构建好之后通过网络的parameters方法可以获取网络的所有参数,并且可以通过遍历这些参数对象改变requires_grad属性来达到冻结参数的目的,这在迁移学习中特别有用。torch.nn模块通过functional子模块提供了大量方法,如conv、ReLU、pooling、softmax等。这些方法在网络的forward中调用,结果输出值由torch.nn.xxxLoss损失函数计算损失。torch.nn模块中提供了很多可选的损失函数,如MSELoss、CrossEntropyLoss等,将损失传递给优化模块,优化模块提供了大量的优化器,常见的有SGD、Adam等。优化器会根据损失计算并更新梯度,达到优化的目的,最终将更新parameters。PyTorch的核心模块如图1.14所示。
图1.14 PyTorch的核心模块
除了这些核心模块,PyTorch还提供了JIT模块,在运行时动态编译代码;multiprocessing并行处理模块实现数据的并行处理;torch.utils.data模块提供数据的加载接口,这些模块在后续章节中会进行介绍。