来源 | Natural Language Processing with PyTorch
作者 | Rao,McMahan
译者 | Liangchu
校对 | gongyouliu
编辑 | auroral-L
全文共6760字,预计阅读时间45分钟。
第一章 基础知识简介(下)
4. PyTorch 基础
4.1 PyTorch 安装
4.2 创建张量
4.3 张量类型和大小
4.4 张量操作
4.5 索引、切片和连接
4.6 张量和计算图
4.7 CUDA 张量
5. 总结
4. PyTorch基础
在本书中,我们广泛地使用 PyTorch 用于实现深度学习模型。PyTorch 是一个开源、社区驱动的深度学习框架。与 Theano、Caffe 和 TensorFlow 不同,PyTorch 实现了一种基于磁带式的自动微分方式,它允许我们动态地定义和执行计算图,这对于我们调试和用最少的代价构建复杂的模型非常有帮助。
动态计算图 VS 静态计算图
像 Theano、Caffe 和 TensorFlow 这样的静态框架要首先声明、编译和执行计算图。尽管这能带来非常高效的实现(在生产和移动配置中非常有用),但在研究和开发过程中可能会变得相当麻烦。像 Chainer、DyNet 和 PyTorch 这样的现代化框架实现了动态计算图,从而允许更灵活的命令式开发风格,不用在每次执行之前编译模型。动态计算图在建模 NLP 任务时特别有用,每个输入可能导致不同的图结构。
PyTorch 是一个优化的张量(tensor)操作库,它提供了一系列用于深度学习的包。这个库的核心是张量,它是一个包含一些多维数据的数学对象。0 阶张量就是一个数字,也即标量(scalar)。一阶张量(1st-order tensor)是一个数字数组,也即向量(vector)。类似地,二阶张量就是一个向量数组,也即矩阵(matrix)。因此,张量可以推广为标量的n维数组,如下图(1-7)所示:
在本节中,我们开始学习和使用PyTorch ,以熟悉其各种PyTorch 操作,包括:
• 创建张量
• 张量操作
• 索引、切片和与张量连接
• 使用张量计算梯度
• 使用带有GPU的 CUDA 张量
我们建议你现在已经安装了 PyTorch 并准备好了 Python 3.5+ notebook,并按照本节中的示例进行操作。我们也建议你完成本节后面的练习题。
4.1 PyTorch安装
第一步是在 pytorch.org 上选择你的系统首选项以在你的机器上安装 PyTorch。选择你的操作系统,然后选择包管理器(我们推荐Conda或Pip),然后选择你正在使用的 Python 版本(我们推荐 3.5及以上)。然后会生成一个命令,便于你执行安装PyTorch。在撰写本文时,Conda 环境下的安装命令如下:
conda install pytorch torchvision -c pytorch
注意
如果你有一个支持 CUDA 的图形处理单元(graphics processor unit,GPU),那么你应该选择合适的 CUDA 版本。要了解更多细节,请参考 pytorch.org上的安装说明。
4.2 创建张量
我们首先定义一个帮助函数describe(x),它总结了张量x的各种性质,比如张量的类型(type)、维数(dimension)和内容(content):
Input[0]:
def describe(x):
print("Type: {}".format(x.type()))
print("Shape/size: {}".format(x.shape))
print("Values: \n{}".format(x))
PyTorch 允许我们使用torch包以多种不同的方式创建张量。创建张量的一种方法是通过指定一个随机张量的维数来初始化它,如下例(1-3)所示:
示例 1-3:在 PyTorch 中使用torch.Tensor创建张量
Input[0]:
import torch
describe(torch.Tensor(2, 3))
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 3.2018e-05, 4.5747e-41, 2.5058e+25],
[ 3.0813e-41, 4.4842e-44, 0.0000e+00]])
我们也可以通过随机初始化值区间上的均匀分布[0,1)或标准正态分布创建一个张量,正如下例(1-4)所示。随机初始化的张量,比如均匀分布的张量,是很重要的,你会在后面的第三章和第四章看到这一点。
示例 1-4:创建随机初始化的张量
Input[0]:
import torch
describe(torch.rand(2, 3)) # uniform random
describe(torch.randn(2, 3)) # random normal
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.0242, 0.6630, 0.9787],
[ 0.1037, 0.3920, 0.6084]])
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[-0.1330, -2.9222, -1.3649],
[ 2.3648, 1.1561, 1.5042]])
我们还可以使用相同的标量填充张量以创建张量。要创建全 0 或全 1 张量,我们可以使用相应的内置函数。若要填充特定值张量,我们可以使用fill_()方法。任何带有下划线(_)的 PyTorch 方法都是指in-place operation,也就是说,它在不创建新对象的情况下修改该对象内容,如下例(1-5)所示:
示例 1-5:填充数字以创建张量
Input[0]:
import torch
describe(torch.zeros(2, 3))
x = torch.ones(2, 3)
describe(x)
x.fill_(5)
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 0., 0.],
[ 0., 0., 0.]])
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1., 1., 1.],
[ 1., 1., 1.]])
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 5., 5., 5.],
[ 5., 5., 5.]])
下例(1-6)演示如何通过使用 Python 列表以声明方式创建张量:
示例 1-6:从列表创建和初始化张量
Input[0]:
x = torch.Tensor([[1, 2, 3],
[4, 5, 6]])
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1., 2., 3.],
[ 4., 5., 6.]])
如前所示,值可以来自列表,也可以来自 NumPy 数组。当然,我们也可以将 PyTorch 张量转换为 NumPy 数组。注意,这个张量的类型是一个DoubleTensor,而不是默认的FloatTensor。这对应于 NumPy 随机矩阵的数据类型float64,如下例(1-7)所示:
示例 1-7:从 NumPy 创建和初始化张量
Input[0]:
import torch
import numpy as np
npy = np.random.rand(2, 3)
describe(torch.from_numpy(npy))
Output[0]:
Type: torch.DoubleTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.8360, 0.8836, 0.0545],
[ 0.6928, 0.2333, 0.7984]], dtype=torch.float64)
在处理使用 Numpy 格式数值的遗留库(legacy libraries)时,在 NumPy 和 PyTorch 张量之间进行转换的能力十分重要。
4.3 张量类型和大小
每个张量都有其相关的类型和大小。使用torch.Tensor构造函数时的默认张量类型是torch.FloatTensor。然而,你也可以在初始化时将张量转换为其他不同类型(float、long、double等)。有两种方法可用于指定初始化类型,一种是直接调用特定张量类型比如FloatTensor和LongTensor的构造函数,另一种是使用特殊的方法torch.tensor,并提供dtype,如下例(1-8)所示:
示例 1-8:张量属性
Input[0]:
x = torch.FloatTensor([[1, 2, 3],
[4, 5, 6]])
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1., 2., 3.],
[ 4., 5., 6.]])
Input[1]:
x = x.long()
describe(x)
Output[1]:
Type: torch.LongTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1, 2, 3],
[ 4, 5, 6]])
Input[2]:
x = torch.tensor([[1, 2, 3],
[4, 5, 6]], dtype=torch.int64)
describe(x)
Output[2]:
Type: torch.LongTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1, 2, 3],
[ 4, 5, 6]])
Input[3]:
x = x.float()
describe(x)
Output[3]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 1., 2., 3.],
[ 4., 5., 6.]])
我们使用张量对象的shape属性和size()方法以获得其维度的度量。这两种访问方法基本相同,在调试 PyTorch 代码时,检查张量的shape必不可少。
4.4 张量操作
创建张量之后,你可以像处理传统编程语言类型(如+、-、*和/)那样对它们进行操作。除了操作符之外,你还可以使用像.add()这样的函数,正如下例(1-9)所示,这些函数与操作符对应:
示例 1-9:张量操作:加法
Input[0]:
import torch
x = torch.randn(2, 3)
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.0461, 0.4024, -1.0115],
[ 0.2167, -0.6123, 0.5036]])
Input[1]:
describe(torch.add(x, x))
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.0923, 0.8048, -2.0231],
[ 0.4335, -1.2245, 1.0072]])
Input[2]:
describe(x + x)
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0.0923, 0.8048, -2.0231],
[ 0.4335, -1.2245, 1.0072]])
还有一些运算可用于张量的特定维数。你可能注意到了,对于二维张量,我们将行表示为维度 0,将列表示为维度 1,如下例(1-10)所示:
示例 1-10:基于维度的张量操作
Input[0]:
import torch
x = torch.arange(6)
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([6])
Values:
tensor([ 0., 1., 2., 3., 4., 5.])
Input[1]:
x = x.view(2, 3)
describe(x)
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
Input[2]:
describe(torch.sum(x, dim=0))
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([3])
Values:
tensor([ 3., 5., 7.])
Input[3]:
describe(torch.sum(x, dim=1))
Output[3]:
Type: torch.FloatTensor
Shape/size: torch.Size([2])
Values:
tensor([ 3., 12.])
Input[4]:
describe(torch.transpose(x, 0, 1))
Output[4]:
Type: torch.FloatTensor
Shape/size: torch.Size([3, 2])
Values:
tensor([[ 0., 3.],
[ 1., 4.],
[ 2., 5.]])
通常,我们需要执行更复杂的操作,包括索引(indexing)、切片(slicing)、连接(joining)和转变(mutation)的组合。与 NumPy 和其他库一样,PyTorch 也有内置函数,以简化该类张量操作。
4.5 索引、切片和连接
如果你使用NumPy,那么你可能很熟悉下例(1-11)中所示的 PyTorch 的索引和切片操作:
示例 1-11:张量的切片和索引
Input[0]:
import torch
x = torch.arange(6).view(2, 3)
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
Input[1]:
describe(x[:1, :2])
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([1, 2])
Values:
tensor([[ 0., 1.]])
Input[2]:
describe(x[0, 1])
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([])
Values:
1.0
下例(1-12)演示 PyTorch 中具有用于复杂索引和切片操作的函数,你可能对有效地访问张量的非连续位置感兴趣:
示例 1-12:复杂索引:张量的非连续索引
Input[0]:
indices = torch.LongTensor([0, 2])
describe(torch.index_select(x, dim=1, index=indices))
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 2])
Values:
tensor([[ 0., 2.],
[ 3., 5.]])
Input[1]:
indices = torch.LongTensor([0, 0])
describe(torch.index_select(x, dim=0, index=indices))
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 1., 2.],
[ 0., 1., 2.]])
Input[2]:
row_indices = torch.arange(2).long()
col_indices = torch.LongTensor([0, 1])
describe(x[row_indices, col_indices])
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([2])
Values:
tensor([ 0., 4.])
注意到索引是一个LongTensor,这是使用 PyTorch 函数进行索引的要求。我们还可以使用内置的连接函数通过指定张量和维度以连接张量,如下例(1-13)所示:
示例 1-13:连接张量
Input[0]:
import torch
x = torch.arange(6).view(2,3)
describe(x)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
Input[1]:
describe(torch.cat([x, x], dim=0))
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([4, 3])
Values:
tensor([[ 0., 1., 2.],
[ 3., 4., 5.],
[ 0., 1., 2.],
[ 3., 4., 5.]])
Input[2]:
describe(torch.cat([x, x], dim=1))
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 6])
Values:
tensor([[ 0., 1., 2., 0., 1., 2.],
[ 3., 4., 5., 3., 4., 5.]])
Input[3]:
describe(torch.stack([x, x]))
Output[3]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 2, 3])
Values:
tensor([[[ 0., 1., 2.],
[ 3., 4., 5.]],
[[ 0., 1., 2.],
[ 3., 4., 5.]]])
PyTorch 还在张量上实现了高效的线性代数操作,如乘(multiplication)、逆(inverse)和迹(trace),如下例(1-14)所示:
示例 1-14:张量上的线性代数:乘法
Input[0]:
import torch
x1 = torch.arange(6).view(2, 3)
describe(x1)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
Input[1]:
x2 = torch.ones(3, 2)
x2[:, 1] += 1
describe(x2)
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([3, 2])
Values:
tensor([[ 1., 2.],
[ 1., 2.],
[ 1., 2.]])
Input[2]:
describe(torch.mm(x1, x2))
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 2])
Values:
tensor([[ 3., 6.],
[ 12., 24.]])
到目前为止,我们已经探讨了创建和操作常PyTorch 张量对象的方法。就像编程语言(如 Python)中变量封装一块数据和关于数据的额外信息(如存储的内存地址),PyTorch 张量处理构建计算图时所需的簿记(bookkeeping),所需构建计算图对机器学习只是在实例化时通过启用一个布尔标志来实现。
4.6 张量和计算图
PyTorch的tensor类封装了数据(张量本身)和一系列操作,如代数操作、索引操作和重组(reshaping)操作。然而下面所示的例子(1-15)中,当requires_grad布尔标志在张量中被设置为True,启用簿记操作用于追踪张量梯度以及梯度函数,它们都被用于促进“监督学习范式”一节中所讨论的梯度学习。
示例 1-15:为梯度簿记创建张量
Input[0]:
import torch
x = torch.ones(2, 2, requires_grad=True)
describe(x)
print(x.grad is None)
Output[0]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 2])
Values:
tensor([[ 1., 1.],
[ 1., 1.]])
True
Input[1]:
y = (x + 2) * (x + 5) + 3
describe(y)
print(x.grad is None)
Output[1]:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 2])
Values:
tensor([[ 21., 21.],
[ 21., 21.]])
True
Input[2]:
z = y.mean()
describe(z)
z.backward()
print(x.grad is None)
Output[2]:
Type: torch.FloatTensor
Shape/size: torch.Size([])
Values:
21.0
False
当你使用requires_grad=True创建张量时,你需要 PyTorch 来管理计算梯度的簿记信息。首先,PyTorch 将跟踪前向传递的值。然后在计算结束时,使用单个标量来计算反向传递。反向传递是通过对一个张量使用backward()方法来初始化的,这个张量来自于一个损失函数的求值。反向传递为参与前向传递的张量对象计算梯度值。
一般来说,梯度是一个值,它表示函数输出相对于函数输入的斜率(slope)。在计算图设置中,模型中的每个参数都存在梯度,可以认为是该参数对误差信号的贡献。在 PyTorch 中,可以使用.grad成员变量访问计算图中节点的梯度。优化器使用.grad变量更新参数的值。
4.7 CUDA张量
到目前为止,我们一直在 CPU 内存上分配张量。在做线性代数运算时,如果你有一个 GPU,那么利用它可能很有意义。要利用 GPU,首先要分配 GPU 内存上的张量。对 GPU 的访问是通过一个名为 CUDA 的专用 API 进行的。CUDA API 是由 NVIDIA 创建的,并且仅限于在 NVIDIA GPU上使用。PyTorch 提供的 CUDA 张量对象在使用中与常规 CPU 绑定张量没有区别,只是在内部分配的方式上有所不同。
PyTorch 使得创建这些 CUDA 张量变得特别容易,它将张量从 CPU 传输到 GPU,同时维护其底层类型。PyTorch 中的首选方法是与设备无关(device agnostic),并编写在 GPU 和 CPU 上都能正常工作的代码。在下例(1-16)中,我们首先使用torch.cuda.is_available()来检查 GPU 是否可用,然后使用torch.device()以检索设备名。接着将实例化所有将用到的张量,并使用.to(device)方法将其移动到目标设备。
示例 1-16:创建 CUDA 张量
Input[0]:
import torch
print (torch.cuda.is_available())
Output[0]:
True
Input[1]:
# preferred method: device agnostic tensor instantiation
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print (device)
Output[1]:
cuda
Input[2]:
x = torch.rand(3, 3).to(device)
describe(x)
Output[2]:
Type: torch.cuda.FloatTensor
Shape/size: torch.Size([3, 3])
Values:
tensor([[ 0.9149, 0.3993, 0.1100],
[ 0.2541, 0.4333, 0.4451],
[ 0.4966, 0.7865, 0.6604]], device='cuda:0')
要操作CUDA 和非 CUDA 对象,我们要确保它们在同一设备上。如果我们不这样的话,计算就会中断,如下例(1-17)所示。当在计算不属于计算图的监视指标时,就会出现这种情况,所以当操作两个张量对象时,一定要确保它们在同一个设备上。
示例 1-17:混合 CUDA 张量和 CPU 绑定的张量
Input[0]
y = torch.rand(3, 3)
x + y
Output[0]
----------------------------------------------------------------------
RuntimeError Traceback (most recent call last)
1 y = torch.rand(3, 3)
----> 2 x + y
RuntimeError: Expected object of type torch.cuda.FloatTensor but found type torch.FloatTensor for argument #3 'other'
Input[1]
cpu_device = torch.device("cpu")
y = y.to(cpu_device)
x = x.to(cpu_device)
x + y
Output[1]
tensor([[ 0.7159, 1.0685, 1.3509],
[ 0.3912, 0.2838, 1.3202],
[ 0.2967, 0.0420, 0.6559]])
请记住:将数据从 GPU 来回移动的代价十分昂贵。因此,典型的过程包括在 GPU 上执行许多并行计算,然后将最终结果传输回 CPU。这将使得你充分利用 GPU。如果你有几个 CUDA 可见的设备(即多个GPU),最佳实践是在执行程序时使用CUDA_VISIBLE_DEVICES环境变量,如下所示:
CUDA_VISIBLE_DEVICES=0,1,2,3 python main.py
在本书中,我们不讨论并行性和多 GPU训练,但是它们在缩放实验中是必不可少的,有时甚至在训练大型模型时也是如此。我们建议您参考 PyTorch 文档和论坛,以获得关于这个主题的更多帮助和支持。
5. 总结
在本章中,我们介绍了本书的目标——自然语言处理(NLP)和深度学习——并详细探讨了监督学习范式。你现在应该熟悉或至少了解各种术语,例如观测(observation)、目标(target)、模型(model)、参数(parameter)、预测(prediction)、损失函数(loss function)、表示(representation)、学习/训练(learning/training)和推理(inference)。你还知道如何使用独热编码来编码学习任务的输入(观察和目标)。我们还研究了基于计数的表示,如 TF 和 TF-IDF。在开始学习PyTorch的过程中,我们首先探讨了什么是计算图,然后探讨了静态计算图和动态计算图,以及操纵 PyTorch 张量的操作。在第二章中,我们将对传统的 NLP 进行概述。如果你对于本书探讨的主题一无所知,那么这两章应该能为你奠定必要的基础,并为本书的其余部分做准备。