Yoshua Bengio实验室MILA开放面向初学者的PyTorch教程

MILA 实验室近日在 GitHub 上开启了一个初学者入门项目,旨在帮助 MILA 新生快速掌握机器学习相关的实践基础。目前该项目已经提供了一系列的 PyTorch 入门资料,并从张量、自动微分、图像识别、神经机器翻译和生成对抗网络等方面详细阐述。

项目地址:https://github.com/mila-udem/welcome_tutorials

PyTorch 是 Torch 在 Python 上的衍生,它本质上是 Numpy 的替代者,而且支持 GPU 加速深度神经网络的训练。如果读者熟悉 Numpy、Python 以及常见的深度学习概念(卷积层、循环层、SGD 等),那么我们可以容易上手 PyTorch。下面,我们将跟随 MILA 的教程共同探索 PyTorch 的基本概念与操作。

Torch 张量库与基本操作

正如 PyTorch 文档所说,如果我们熟悉 NumPy 的多维数组,那么 Torch 张量的很多操作我们能轻易地掌握。PyTorch 提供了 CPU 张量和 GPU 张量,并且极大地加速了计算的速度。

从张量的构建与运行就能体会到 PyTorch 相比 TensorFLow 需要声明张量、初始化张量要简洁地多。以下语句将随机初始化一个 5×3 的二维张量,因为 PyTorch 是一种动态图,所以它声明和真实赋值是同时进行的。

torch.Tensor(5, 3)
---------------------------------------
2.4878e+04  4.5692e-41  2.4878e+04
4.5692e-41 -2.9205e+19  4.5691e-41
1.2277e-02  4.5692e-41 -4.0170e+19
4.5691e-41  1.2277e-02  4.5692e-41
0.0000e+00  0.0000e+00  0.0000e+00
[torch.FloatTensor of size 5x3]

若我们希望随机初始化的张量服从某些分布,那么我们可以直接对张量对象使用一些方法。如下初始化的张量将服从均匀分布:

torch.Tensor(5, 3).uniform_(-1, 1)
---------------------------------------------
-0.2767 -0.1082 -0.1339
-0.6477  0.3098  0.1642
-0.1125 -0.2104  0.8962
-0.6573  0.9669 -0.3806
0.8008 -0.3860  0.6816
[torch.FloatTensor of size 5x3]

在 PyTorch 中,torch.Tensor 是一种多维矩阵,其中每个元素都是一个单一的数据类型,且该构造函数默认的为 torch.FloatTensor。以下是具体张量的类型:


除了直接定义维度,一般我们还可以从 Python 列表或 NumPy 数组中创建张量。而且根据 Python 列表和元组等数据结构的习惯,我们可以使用相似的索引方式进行取值或赋值等。以下通过 Python 列表创建一个 Torch 张量,并通过索引赋值:

>>> torch.FloatTensor([[1, 2, 3], [4, 5, 6]])
1  2  3
4  5  6
[torch.FloatTensor of size 2x3]
>>> print(x[1][2])
6.0
>>> x[0][1] = 8
>>> print(x)
1  8  3
4  5  6
[torch.FloatTensor of size 2x3]

除了 Python 列表,NumPy 数组也可以直接构建 Torch 张量。

z = torch.LongTensor([[1, 3], [2, 9]])
print(z.type())
# Cast to numpy ndarray
print(z.numpy().dtype)
--------------------------------------------
torch.LongTensor
int64

若 x 为我们定义的 5×3 Torch 张量,且初始化数值服从-1 到 1 的均匀分布,那么我们可以执行很多基础的数学运算。以下执行了一个简单的矩阵间对应元素乘积。

x = torch.Tensor(5, 3).uniform_(-1, 1)
y = x * torch.randn(5, 3)
print(y)
---------------------------------------------
0.2200 -0.0368  0.4494
-0.2577 -0.0343  0.1587
-0.7503 -0.1729  0.0453
0.9296 -0.1067 -0.6402
-0.3276  0.0158 -0.0552
[torch.FloatTensor of size 5x3]

PyTorch 同样支持广播(Broadcasting)操作,一般它会隐式地把一个数组的异常维度调整到与另一个算子相匹配的维度以实现维度兼容。为了定义两个形状是否是可兼容的,PyTorch 会从最后开始往前逐个比较它们的维度大小。在这个过程中,如果两者的对应维度相同,或者其一(或者全是)等于 1,则继续进行比较,直到最前面的维度。若不满足这两个条件,程序就会报错。如下展示了 PyTorch 的广播操作:

print (x.size())
y = x + torch.randn(5, 1)
print(y)
---------------------------------------------
torch.Size([5, 3])
0.1919 -0.5006 -1.2410
-0.8080  0.1407 -0.6193
-1.6629 -0.1580 -0.3921
1.0395  0.7069 -0.1459
1.9027  1.4343  1.2299
[torch.FloatTensor of size 5x3]

在实践中,我们经常需要转换一个张量的维度以适配不同的运算。例如在卷积层与全连接层的连接中,我们必须将卷积层的三维张量转化为一维向量,因此我们才能进一步执行全连接操作。如下展示了 PyTorch 中常见的一些维度变换方法,我们不仅可以使用 view() 方法改变张量的维度,还可以使用 transpose() 方法转换各维度的位置。

y = torch.randn(5, 10, 15)
print(y.size())
print(y.view(-1, 15).size())  # Same as doing y.view(50, 15)
print(y.view(-1, 15).unsqueeze(1).size()) # Adds a dimension at index 1.
print(y.view(-1, 15).unsqueeze(1).squeeze().size())
# If input is of shape: (Ax1xBxCx1xD)(Ax1xBxCx1xD) then the out Tensor will be of shape: (AxBxCxD)(AxBxCxD)
print()
print(y.transpose(0, 1).size())
print(y.transpose(1, 2).size())
print(y.transpose(0, 1).transpose(1, 2).size())
print(y.permute(1, 2, 0).size())
-----------------------------------------------------
torch.Size([5, 10, 15])
torch.Size([50, 15])
torch.Size([50, 1, 15])
torch.Size([50, 15])
torch.Size([10, 5, 15])
torch.Size([5, 15, 10])
torch.Size([10, 15, 5])
torch.Size([10, 15, 5])

正如 PyTorch 在官网上所说,PyTorch 是一个张量和动态神经网络 Python 库,它有着极其强大的 GPU 加速性能。我们一般可以直接定义 GPU 张量,也可以由 CPU 张量转化为 GPU 张量。如下,我们定义了两个 GPU 张量,并对这两个张量执行矩阵乘法。当然,我们也可以如下所示将 CPU 张量转换为 GPU 张量。

x = torch.cuda.HalfTensor(5, 3).uniform_(-1, 1)
y = torch.cuda.HalfTensor(3, 5).uniform_(-1, 1)
torch.matmul(x, y)
-----------------------------------------------------
0.2456  1.1543  0.5376  0.4358 -0.0369
0.8247 -0.4143 -0.7188  0.3953  0.2573
-0.1346  0.7329  0.5156  0.0864 -0.1349
-0.3555  0.3135  0.3921 -0.1428 -0.1368
-0.4385  0.5601  0.6533 -0.2793 -0.5220
[torch.cuda.HalfTensor of size 5x5 (GPU 0)]
# 以下转化CPU张量为GPU张量
x = torch.FloatTensor(5, 3).uniform_(-1, 1)
print(x)
x = x.cuda(device=0)
print(x)
x = x.cpu()
print(x)

PyTorch 自动求导机制

这一部分将概述 Autograd 如何工作并记录操作。了解 Autograd 并不是绝对必要的,但 MILA 第二部分着重讲解它,因为它可以帮助我们编写更高效、简洁的程序,并可以帮助我们进行调试。MILA 实验室的 PyTorch Autograd 教程改编自 PyTorch 文档,因此更详细的 Autograd 资料可以查看 PyTorch 文档。

PyTorch 文档地址:http://pytorch.org/docs/master/

TensorFlow、Caffe 和 CNTK 等大多数框架都是使用的静态计算图,开发者必须建立或定义一个神经网络,并重复使用相同的结构来执行模型训练。改变网络的模式就意味着我们必须从头开始设计并定义相关的模块。但是通过 PyTorch,我们使用了一种称为反向模式的自动微分技术(Reverse-mode auto-differentiation),它可以让开发者简单且低成本地改变神经网络的工作方式。按照文档所述,PyTorch 的灵感来自于自动微分的几篇研究论文,以及当前和过去的工作,如 autograd,Chainer 等。虽然这种技术并不是 PyTorch 独有的,但它是迄今为止最快的实现之一,因此它也为研究工作提供了最好的速度和灵活性。

Autograd 如何编码执行历史

从概念上来说,Autograd 会维护一个图并记录对变量执行的所有运算。这会产生一个有向无环图,其中叶结点为输入向量,根结点为输出向量。通过从根结点到叶结点追踪图的路径,我们可以轻易地使用链式法则自动计算梯度。以下展示了PyTorch的动态计算图。


图片源自  PyTorch 文档

在内部,Autograd 将这个图表征为 Function 对象的图,并且可以应用 apply() 计算评估图的结果。在计算前向传播中,当 Autograd 在执行请求的计算时,它还会同时构建一个表征梯度计算的图,且每个 Variable 的 .grad_fn 属性就是这个图的输入单元。在前向传播完成后,我们可以在后向传播中根据这个动态图来计算梯度。

以下展示了通过 backward() 和 torch.autograd.grad 计算梯度的方法,其中 torch.eq() 评估表达式是不是相等,即 x.grad 的计算结果是不是等于 2x。

x = Variable(torch.Tensor(5, 3).uniform_(-1, 1), requires_grad=True)
y = Variable(torch.Tensor(5, 3).uniform_(-1, 1), requires_grad=True)
z = x ** 2 + 3 * y
z.backward(gradient=torch.ones(5, 3))
# eq computes element-wise equality
torch.eq(x.grad, 2 * x)
----------------------------------------------------------------------
Variable containing:
1  1  1
1  1  1
1  1  1
1  1  1
1  1  1
[torch.ByteTensor of size 5x3]

以下展示了对 y 求导的结果,即 dz/dy。从上面 z 的定义可知结果应该是 3,那么以下展示了该计算过程:

y.grad
-------------------------------
Variable containing:
3  3  3
3  3  3
3  3  3
3  3  3
3  3  3
[torch.FloatTensor of size 5x3]

前面是使用 backward() 求解变量的梯度,后面我们也可以使用 torch.autograd.grad 计算梯度。如下所示,我们使用另外一种方式求解同一个函数的梯度。

x = Variable(torch.Tensor(5, 3).uniform_(-1, 1), requires_grad=True)
y = Variable(torch.Tensor(5, 3).uniform_(-1, 1), requires_grad=True)
z = x ** 2 + 3 * y
dz_dx = torch.autograd.grad(z, x, grad_outputs=torch.ones(5, 3))
dz_dy = torch.autograd.grad(z, y, grad_outputs=torch.ones(5, 3))

该项目在后面还提供了一系列的入门级教程,例如 Torch 神经网络 API 的简介、通过卷积网络和 ResNet 进行图像识别任务、神经机器翻译的基础和生成对抗网络的实现等。以下将只简要地展示这些章节所述的内容。

在 Torch 神经网络 API 简介中,MILA 展示了很多常用的 API 及意义,如下展示了部分 torch.nn 的 API。

我们一般可以使用 torch.nn 包构建神经网络,下面提供了一些 API 的表达及意义:

  • 线性层- nn.Linear、nn.Bilinear
  • 卷积层 - nn.Conv1d、nn.Conv2d、nn.Conv3d、nn.ConvTranspose2d
  • 非线性激活函数- nn.Sigmoid、nn.Tanh、nn.ReLU、nn.LeakyReLU
  • 池化层 - nn.MaxPool1d、nn.AveragePool2d
  • 循环网络 - nn.LSTM、nn.GRU
  • 归一化 - nn.BatchNorm2d
  • Dropout - nn.Dropout、nn.Dropout2d
  • 嵌入 - nn.Embedding
  • 损失函数 - nn.MSELoss、nn.CrossEntropyLoss、nn.NLLLoss

而在图像识别分类任务的章节中,MILA 先描述了如何构建一个简单的图像分类模型。然后再简单地构建了一个残差模块和残差网络,该残差网络可用于 CIFAR 图像识别。

以下给出了简单卷积网络的整体架构,在该架构中,我们可以简单地堆叠一些层级而完成强大的分类模型。这种贯序模型的结构十分清晰,我们可以直接重复地堆叠卷积层、Dropout 层、激活层和最大池化层完成整个推断结构。

class Classifier(nn.Module):
   """Convnet Classifier"""
   def __init__(self):
       super(Classifier, self).__init__()
       self.conv = nn.Sequential(
           # Layer 1
           nn.Conv2d(in_channels=1, out_channels=16, kernel_size=(3, 3), padding=1),
           nn.Dropout(p=0.5),
           nn.ReLU(),
           nn.MaxPool2d(kernel_size=(2, 2), stride=2),
           # Layer 2
           nn.Conv2d(in_channels=16, out_channels=32, kernel_size=(3, 3), padding=1),
           nn.Dropout(p=0.5),
           nn.ReLU(),
           nn.MaxPool2d(kernel_size=(2, 2), stride=2),
           # Layer 3
           nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3, 3), padding=1),
           nn.Dropout(p=0.5),
           nn.ReLU(),
           nn.MaxPool2d(kernel_size=(2, 2), stride=2),
           # Layer 4
           nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3, 3), padding=1),
           nn.Dropout(p=0.5),
           nn.ReLU(),
           nn.MaxPool2d(kernel_size=(2, 2), stride=2)
       )
       # Logistic Regression
       self.clf = nn.Linear(128, 10)
   def forward(self, x):
       return self.clf(self.conv(x).squeeze())

在后面部分中,MILA 同样详细展示了如何构建机器翻译系统和生成对抗网络。我们可以按照教程定义一个 DCGAN 生成器,该生成器的架构为:


 (deconv1): ConvTranspose2d(128, 128, kernel_size=(4, 4), stride=(1, 1), bias=False)
 (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
 (deconv2): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
 (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
 (deconv3): ConvTranspose2d(64, 32, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
 (bn3): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True)
 (deconv4): ConvTranspose2d(32, 1, kernel_size=(4, 4), stride=(2, 2), padding=(3, 3), bias=False)

根据 MILA 教程定义的 DCGAN 判别器架构为:


(conv1): Conv2d(1, 32, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(32, eps=1e-05, momentum=0.1, affine=True)
(conv2): Conv2d(32, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
(conv3): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
(bn3): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
(conv4): Conv2d(128, 1, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)

以上是该教程的基本内容,更加详细的内容请查看 MILA PyTorch 教程与 PyTorch 官方教程和文档。从 MILA 对该项目的描述中,它是一个为 MILA 新生准备的入门级教程,所以我们有理由相信与期待该项目还会继续完善与更新。


入门PyTorch实现入门MILA深度学习框架
3