本文是对 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻译与整理,英语基础比较好的同学推荐阅读原汁原味的博客。
文章目录
PyTorch是一个深度学习框架和一个科学计算包,这是PyTorch核心团队对PyTorch的描述,PyTorch的科学计算方面主要是PyTorch张量(tensor)库和相关张量运算的结果。
A tensor is an n-dimensional array (ndarray)
。PyTorch的torch.Tensor
对象就是由Numpy的ndarray
对象创建来的,两者之间的转换十分高效。PyTorch中内置了对GPU的支持,如果我们在系统上安装了GPU,那么使用PyTorch将张量在GPU之间来回移动是非常容易的一件事情。
1. PyTorch简介
PyTorch的首次发布是在2016年10月,在PyTorch创建之前,还有一个叫做Torch(火炬)的框架。Torch是一个已经存在了很长时间的机器学习框架,它基于Lua编程语言。PyTorch和这个Lua版本(称为Torch)之间的联系是存在的,因为许多维护Lua版本的开发人员也参与了PyTorch的开发工作。你可能听说过PyTorch是由Facebook创建和维护的,这是因为PyTorch在创建时,Soumith Chintala(创始人)在Facebook AI Research工作。
下表列出了PyTorch包及其相应的说明。这些是我们在本系列中构建神经网络时将学习和使用的主要PyTorch组件。
Package | Description |
---|---|
torch | 顶层的PyTorch包和tensor库 |
torch.nn | 一个子包,用于构建神经网络的模块和可扩展类 |
torch.autograd | 一个子包,支持PyTorch中所有微分张量运算 |
torch.nn.functional | 一种函数接口,包含构建神经网络的操作,如损失函数、激活函数和卷积操作。 |
torch.optim | 一个子包,包含标准优化操作,如SGD和Adam |
torch.utils | 一个子包,包含数据集和数据加载器等实用工具类,使数据预处理更容易 |
torchvision | 提供对流行数据集、模型体系结构和图像转换的访问的包 |
为了优化神经网络,我们需要计算导数,为了进行计算,深度学习框架使用所谓的计算图(computational graphs
),计算图用于描述神经网络内部张量上发生的函数运算操作。
PyTorch使用一个称为动态计算图的计算图,这意味着计算图是在创建操作时动态生成的,这与在实际操作发生之前就已完全确定的静态图形成对比。正因为如此,许多深度学习领域的前沿研究课题都需要动态图,或者从动态图中获益良多。
2. GPU相关介绍
GPU是一种擅长处理特定计算(specialized computations)的处理器。这与中央处理器(CPU)形成对比,中央处理器是一种善于处理一般计算(general computations)的处理器。CPU是在我们的电子设备上支持大多数典型计算的处理器。
GPU的计算速度可能比CPU快得多。 然而,这并非总是如此。 GPU相对于CPU的速度取决于所执行的计算类型。最适合GPU的计算类型是可以并行完成的计算。
并行计算(paraller computing
)是一种将特定计算分解成可以同时进行的独立的较小计算的计算方式,然后重新组合或同步计算结果,以形成原来较大计算的结果。
一个较大的任务可以分解成的任务数量取决于特定硬件上包含的内核数量。核心是在给定处理器中实际执行计算的单元,CPU通常有4个、8个或16个核心,而GPU可能有数千个。
有了这些工作知识,我们可以得出结论,并行计算是使用GPU完成的,我们还可以得出结论,最适合使用GPU解决的任务是可以并行完成的任务。如果计算可以并行完成,我们可以使用并行编程方法和GPU加速计算。
现在我们把目光转移到神经网络上,看看为什么GPU在深度学习中被大量使用。 我们刚刚看到GPU非常适合并行计算,而关于GPU的事实就是深度学习使用GPU的原因。
Neural networks are embarrassingly parallel.
指的是一个任务分解为几个子任务之后,在不同处理器上执行该子任务,而这些子任务之间不会相互依赖,也就说明该任务十分适合于并行计算,也被称为embarrassingly parallel.
我们用神经网络所做的许多计算可以很容易地分解成更小的计算,这样一组更小的计算就不会相互依赖了。卷积操作就是这样一个例子。
![](https://img-blog.csdnimg.cn/20191115113603751.gif)
- 蓝色区域(底部): Input channel
- 阴影区域(底部): Filter
- 绿色区域(顶部): Output channel
对于蓝色输入通道上的每个位置,3 x 3过滤器都会进行计算,将蓝色输入通道的阴影部分映射到绿色输出通道的相应阴影部分。在动画中,这些计算一个接一个地依次进行。但是,每个计算都是独立于其他计算的,这意味着任何计算都不依赖于任何其他计算的结果。因此,所有这些独立的计算都可以在GPU上并行进行,从而产生整个输出通道,加速我们的卷积过程。
3. CUDA相关介绍
Nvidia是一家设计GPU的技术公司,他们创建了CUDA作为一个软件平台,与GPU硬件适配,使开发人员更容易使用Nvidia GPU的并行处理能力来构建加速计算的软件。Nvidia GPU是支持并行计算的硬件,而CUDA是为开发人员提供API的软件层。
开发人员通过下载CUDA工具包来使用CUDA,伴随工具包一起的是专门的库,如 cuDNN, CUDA Deep Neural Network library.
在PyTorch中利用CUDA非常简单。如果我们希望在GPU上执行特定的计算,我们可以通过在数据结构(tensors)上调用cuda()
来指示PyTorch这样做。
假设我们有以下代码:
> t = torch.tensor([1,2,3])
> t
tensor([1, 2, 3])
默认情况下,以这种方式创建的tensor对象在CPU上。因此,我们使用这个张量对象所做的任何操作都将在CPU上执行。
现在,要把张量移到GPU上,我们只需要写:
> t = t.cuda()
> t
tensor([1, 2, 3], device='cuda:0')
由于可以在CPU或GPU上有选择地进行计算,因此PyTorch的用途非常广泛。
GPU是不是总是比CPU更好呢?答案是否定的。
GPU只对特定的(专门的)任务更快。它也会遇到某些瓶颈,例如,将数据从CPU移动到GPU的成本很高(耗时),因此在这种情况下,如果计算任务本身就很简单,还把它转移到GPU上进行计算,那么总体性能可能会降低。
把一些相对较小的计算任务转移到GPU上不会使我们的速度大大加快,而且可能确实会减慢我们的速度。GPU对于可以分解为许多较小任务的任务非常有效,如果计算任务已经很小,那么将任务移到GPU上就不会有太多收获。
4. 张量定义
神经网络中的输入、输出和变换都是用tensor
表示的,在神经网络编程中大量使用了tensor
。
张量的概念是其他更具体概念的数学概括,让我们看看张量的一些具体实例:
- number
- scalar
- array
- vector
- 2d-array
- matrix
我们来把上面的张量实例分成两组:
- number, array, 2d-array
- scalar, vector, matrix
第一组中的三个术语(数字、数组、二维数组)是计算机科学中常用的术语,而第二组(标量、矢量、矩阵)是数学中常用的术语。
我们经常看到这种情况,不同的研究领域对同一概念使用不同的词。在深度学习中,我们通常把这些都称为tensor
。
Indexes requried | Computer science | Mathematics |
---|---|---|
0 | number | scala |
1 | array | vector |
2 | 2d-array | matrix |
n | nd-array | nd-tensor |
5. 张量的秩、轴和形状
在深度学习中,秩、轴和形状是我们最关心的tensor
属性,这些概念建立在一个又一个的基础上,从秩开始,然后是轴,再到形状,请注意这三者之间的关系。
我们在这里引入rank
这个词,是因为它在深度学习中经常被用到,它指的是给定张量中的维数,一个张量的秩告诉我们需要多少索引来引用张量中的某一个特定元素。
如果我们有一个张量,想要表示某一个特定的维度,那么在深度学习中使用轴(Axis
)这个词。
每个轴的长度告诉我们每个轴上有多少索引可用,假设我们有一个张量 t,我们知道第一个轴的长度为3,而第二个轴的长度为4。
我们可以索引第一个轴的每一个元素像这样:
t[0]
t[1]
t[2]
由于第二轴的长度为4,所以我们可以沿着第二轴标出4个位置。这对于第一轴的每个索引都是成立的,所以我们有:
t[0][0]
t[1][0]
t[2][0]
t[0][1]
t[1][1]
t[2][1]
t[0][2]
t[1][2]
t[2][2]
t[0][3]
t[1][3]
t[2][3]
张量的形状是由每个轴的长度决定的,所以如果我们知道给定张量的形状,那么我们知道每个轴的长度,这告诉我们每个轴有多少个索引可用。
我们结合一个实例来看看形状(shape
)是如何计算的。
> a = torch.tensor([[[[1]],[[2]],[[3]]]])
> print(a)
tensor([[[[1]],
[[2]],
[[3]]]])
如何来计算它的形状呢,首先我们数一下中括号的个数,得知这是一个四维张量,然后由外而内,去掉最外层的中括号之后,得到 tensor([[[1]], [[2]], [[3]]])
,此时只有一个最外层的中括号,那我们的shape变为 ( 1 , , , ) (1, \;,\; ,) (1,,,);同理,我们继续去除最外层的中括号,得到 tensor([[1]], [[2]], [[3]])
,此时有三个最外层的中括号,那我们的shape则变为 ( 1 , 3 , , ) (1, 3,\; ,) (1,3,,),因为每一个维度的形状是相同的,于是我们继续来只需要看其中一个维度即可,即 tensor([[1]])
;去除最外层的中括号,得到 tensor([1])
,此时只有一个最外层的中括号,shape变为 ( 1 , 3 , 1 , ) (1, 3,1 ,) (1,3,1,);最后再去除一个中括号,得到 tensor(1)
,shape变为 ( 1 , 3 , 1 , 1 ) (1, 3,1 ,1) (1,3,1,1),以上就是得到张量形状的全部过程。
> print(a.shape)
torch.Size([1, 3, 1, 1])
6. CNN中的张量
CNN输入的形状,通常有4个维度,也就是说我们有一个秩为4的四阶张量,张量中的每一个索引对应着一个轴,每一个轴都代表着输入数据的某种实际特征,我们从右到左,来理解CNN输入的张量中,每个维度的含义。
原始图像数据以像素的形式出现,用数字表示,并使用高度和宽度两个维度进行布局,所以我们需要 width 和 height 两个轴。
下一个轴表示图像的颜色通道数,灰度图的通道数为1,RGB图的通道数为3,这种颜色通道的解释仅适用于输入张量,后续 feature map 中的通道都不是代表颜色。
也就是说,张量中的最后三个轴,表示着一个完整的图像数据。在神经网络中,我们通常处理成批的样本,而不是单个样本,所以最左边的轴的长度,告诉我们一批中有多少个样本。
假设给定张量的形状为 [3,1,28,28] ,那么我们可以确定,一个批次中有三幅图像,每张图像的颜色通道数为3,宽和高都是28。 [Batch, Channels, Height, Width]
我们接下来看看张量被卷积层变换后,颜色通道轴的解释是如何变化的。
假设我们有一个 tensor 的形状为 [1,1,28,28],当它经过一个卷积层之后,张量的宽和高,以及通道数量都会发生改变,输出的通道数即对应着卷积层中卷积核的个数。
输出的通道不再解释为颜色通道,而是 feature map 的修改通道(modified channels),使用 feature 一词是因为卷积层的输出,代表图像中的特定特征,例如边缘,这些映射随着网络在训练过程中的学习而出现,并且随着我们深入网络而变得更加复杂。
7. torch.Tensor类
我们可以用下面的方式,构建一个 torch.Tensor
类的实例:
> t = torch.Tensor()
> type(t)
torch.Tensor
每一个 torch.Tensor
对象都有3个属性:
> print(t.dtype)
> print(t.device)
> print(t.layout)
torch.float32
cpu
torch.strided
我们来详细看一下dtype
有哪些属性:
Data type | dtype | CPU tensor | GPU tensor |
---|---|---|---|
32-bit floating point | torch.float32 | torch.FloatTensor | torch.cuda.FloatTensor |
64-bit floating point | torch.float64 | torch.DoubleTensor | torch.cuda.DoubleTensor |
16-bit floating point | torch.float16 | torch.HalfTensor | torch.cuda.HalfTensor |
8-bit integer (unsigned) | torch.uint8 | torch.ByteTensor | torch.cuda.ByteTensor |
8-bit integer (signed) | torch.int8 | torch.CharTensor | torch.cuda.CharTensor |
16-bit integer (signed) | torch.int16 | torch.ShortTensor | torch.cuda.ShortTensor |
32-bit integer (signed) | torch.int32 | torch.IntTensor | torch.cuda.IntTensor |
64-bit integer (signed) | torch.int64 | torch.LongTensor | torch.cuda.LongTensor |
注意,每种类型有一个CPU和GPU版本。关于张量数据类型,需要记住的一点是,张量之间的张量运算必须发生在具有相同类型数据的张量之间。
device
用来表示张量的数据是存储在 CPU 上还是 GPU 上。它决定来张量计算的位置在哪里。我们可以用 索引 的方式来指定设备的编号:
> device = torch.device('cuda:0')
> device
device(type='cuda', index=0)
使用多个设备时,需要记住的一点是张量之间的张量运算必须发生在同一设备上的张量之间。
layout
属性用来指定张量在内存中是如何存储的。
在 PyTorch 中,可以通过以下四种方式将一个 array-like
的对象转成 torch.Torch
的对象。
torch.Tensor(data)
torch.tensor(data)
torch.as_tensor(data)
torch.from_numpy(data)
我们可以直接创建一个 Python list
类型的 data,不过 numpy.ndarray
是一个更加常见的选择,如下所示:
> data = np.array([1,2,3])
> type(data)
numpy.ndarray
然后再用上面的四种方式来创建一个 torch.Torch
的对象:
> o1 = torch.Tensor(data)
> o2 = torch.tensor(data)
> o3 = torch.as_tensor(data)
> o4 = torch.from_numpy(data)
> print(o1)
> print(o2)
> print(o3)
> print(o4)
tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)
除了第一个之外,其他的输出(o2、o3、o4)似乎都产生了相同的张量,第一个输出(o1)在数字后面有圆点,表示数字是浮点数,而后面三个选项的类型是int32。
> type(2.)
float
> type(2)
int
当然,PyTorch 也内置了一些不需要通过数据转换,直接构成张量的方式。
> print(torch.eye(2))
tensor([
[1., 0.],
[0., 1.]
])
> print(torch.zeros([2,2]))
tensor([
[0., 0.],
[0., 0.]
])
> print(torch.ones([2,2]))
tensor([
[1., 1.],
[1., 1.]
])
> print(torch.rand([2,2]))
tensor([
[0.0465, 0.4557],
[0.6596, 0.0941]
])
8. 创建Tensor的方法对比
先来看看 torch.tensor()
和 torch.Tensor()
这两种方法的区别:
> data = np.array([1,2,3])
> type(data)
numpy.ndarray
> o1 = torch.Tensor(data)
> o2 = torch.tensor(data)
> print(o1)
> print(o2)
tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)
torch.Tensor()
是 torch.Tensor 类的构造函数,torch.tensor()
是工厂函数,它构造 torch.Tensor 对象并将它们返回给调用方,这是一种创建对象的软件设计模式,另一个区别就是前者默认的数据类型是浮点型,而后者是整型。数据类型可以显示地指定,不指定的话,可以通过传入的数据类型来推断。四种构造方法中只有 torch.Tensor()
函数不可以显示指定dtype
。
> torch.tensor(data, dtype=torch.float32)
> torch.as_tensor(data, dtype=torch.float32)
我们再来看看几种方法创建 tensor 时,对传入的 data 采取的是拷贝还是共享的方式。
> print('old:', data)
old: [1 2 3]
> data[0] = 0
> print('new:', data)
new: [0 2 3]
> print(o1)
> print(o2)
> print(o3)
> print(o4)
tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)
tensor([0, 2, 3], dtype=torch.int32)
tensor([0, 2, 3], dtype=torch.int32)
可以发现,torch.Tensor()
和 torch.tensor()
这两个函数都是对输入数据进行了拷贝,而 torch.as_tensor()
和 torch.from_numpy()
这两个函数则是对输入数据进行了共享的方式。与复制数据相比,共享数据效率更高,占用的内存更少,因为数据不会写入内存中的两个位置。
如果我们想把 torch.Tensor 对象转成 ndarray 类型的话,采取下面这种方式:
> print(type(o3.numpy()))
> print(type(o4.numpy()))
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>
不过这个方法只适用于 torch.as_tensor()
和 torch.from_numpy()
这两种方式创建的 torch.Tensor 对象,我们再来进一步比较一下这两个方法的区别。
torch.from_numpy()
函数只接受 numpy.ndarray 类型的输入,而torch.as_tensor()
函数可以接受各种类似Python数组的对象,包括其他PyTorch张量。
综上所述,我们更推荐 torch.tensor()
和 torch.as_tensor()
这两个函数,前者是一种直接调用的方式,后者是在需要调参的时候采用的方式。
关于内存共享的机制,还有一些需要提的点:
- 由于numpy.ndarray对象是在CPU上分配的,因此当使用GPU时,
torch.as_tensor()
函数必须将数据从CPU复制到GPU。 torch.as_tensor()
的内存共享不适用于内置的Python数据结构,如列表。torch.as_tensor()
的调用要求开发人员了解共享特性。这是必要的,这样我们就不会在没有意识到变更会影响多个对象的情况下无意中对底层数据进行不必要的更改。- 当 numpy.ndarray 对象和张量对象之间有许多来回操作时,
torch.as_tensor()
性能的优越性会更大。
9. tensor的reshape、squeeze和cat操作
假设我们现在有一个秩为 2 、形状为 3 * 4 的张量:
> t = torch.tensor([
[1,1,1,1],
[2,2,2,2],
[3,3,3,3]
], dtype=torch.float32)
在 PyTorch 中,我们有两种获取张量形状的方法:
> t.size()
torch.Size([3, 4])
> t.shape
torch.Size([3, 4])
我们还可以采用下面的方式来获取张量的元素数:
> torch.tensor(t.shape).prod()
tensor(12)
> t.numel()
12
于是我们可以进行 reshape
操作:
> t.reshape([1,12])
tensor([[1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.]])
> t.reshape([2,6])
tensor([[1., 1., 1., 1., 2