跳至主要内容

用 PyTorch 探索张量(Tensors)

在本教程中,您将了解张量、PyTorch,以及如何使用 PyTorch 创建一个简单的神经网络。
· 11分钟 阅读

使用 AI 探索

在深度学习中,关于张量这一核心数据结构的讨论屡见不鲜。连谷歌旗舰级机器学习库的名字里都带有“张量”:"TensorFlow"。张量是一种线性代数中的数据结构,与向量和矩阵类似,您也可以对张量进行算术运算。

另一方面,PyTorch 是由 Facebook 构建的 Python 包,提供两大高层特性:1)具有强大 GPU 加速的张量计算(类似 Numpy);2)基于“录像带式”自动微分系统构建的深度神经网络。

在本教程中,您将了解什么是张量,以及如何在 Python 中用 PyTorch 操作它们。

闲话少说,让我们从张量的简介开始。

张量简介

张量是对向量和矩阵的推广,可以直观地理解为多维数组。广受欢迎的深度学习著作《Deep Learning》(Goodfellow 等)中指出:

“一般情况下,按固定网格排列、具有可变轴数量的数字数组称为张量。”

标量是零阶或秩为零的张量。向量是一维或一阶张量,矩阵是二维或二阶张量。

下面的信息图以一种非常直观的方式描述了张量:

tensors infrographic

现在,我们以更清晰的方式来建立对张量的直觉。

张量是现代机器学习的基本构件。从本质上讲,它是一个数据容器,多数情况下容纳数字;有时也会包含字符串,但较为少见。可以把它想象成一个“数字桶”。

不过,人们常常把张量与多维数组混淆。按照 StackExchange 的说法:

张量和多维数组是不同类型的对象。前者是一类函数,后者是一种适合在坐标系中表示张量的数据结构。

从数学上看,张量被定义为多线性函数。多线性函数由多个向量变量构成。张量场则是取值为张量的函数。要获得严格的数学解释,您可以阅读这里。

因此,张量是需要您来定义的函数或容器。实际的计算发生在有数据输入时。您看到的数组或多维(1D、2D、…、ND)都可以视作通用的张量。

现在,我们来聊一聊张量记号。

张量记号与矩阵记号很相似:用大写字母表示张量,用带下标整数的小写字母表示张量中的标量值。

tensor notation

很多对标量、向量和矩阵可执行的运算,都可以重述为对张量执行。

作为一种工具,张量及其代数在物理和工程领域被广泛使用。在机器学习中,用于训练和运行深度学习模型的一系列术语与技术也可以用张量来描述。

认识 PyTorch

PyTorch 是一个基于 Python 的科学计算包,目标包括:

  • 替代 NumPy,利用 GPU 的算力。
  • 作为一个深度学习研究平台,提供最大程度的灵活性与速度。

我们快速总结一下 PyTorch 的独特特性:

  • PyTorch 提供了丰富的张量例程,以满足并加速您的科学计算需求,如切片、索引、数学运算、线性代数、归约等。而且运行很快。

  • PyTorch 有一种独特的构建神经网络的方式:像录音带一样使用并重放计算轨迹。

  • 多数框架(如 TensorFlow、Theano、Caffe 和 CNTK)对世界的看法是静态的。需要先搭好一个神经网络,然后一次又一次地复用相同结构。改变网络行为意味着要从头开始。

  • PyTorch 使用一种称为反向模式自动微分(Reverse-mode auto-differentiation)的技术,使您能够任意改变网络行为而没有任何延迟或开销。我们的灵感来自该主题的多篇研究论文,以及当前和过去的工作,如 autograd、Chainer 等。

(虽然这种技术并非 PyTorch 独有,但到目前为止它是其中实现速度最快的之一。您可以在速度与灵活性之间兼得,用于天马行空的研究。)

  • PyTorch 的框架开销极小。我们集成了诸如 Intel MKL 和 NVIDIA(CuDNN、NCCL)等加速库以最大化速度。在核心层面,其 CPU 与 GPU 张量和神经网络后端(TH、THC、THNN、THCUNN)被写成带 C99 API 的独立库。它们成熟稳健,已历经多年测试。

(因此,无论您运行的是小型还是大型神经网络,PyTorch 都非常快。)

  • 与 Torch 或某些替代方案相比,PyTorch 的内存使用极其高效。PyTorch 的创建者为 GPU 编写了自定义内存分配器,以确保您的深度学习模型尽可能节省内存。这让您能够训练比以往更大的深度学习模型。

这篇博客很好地比较了 PyTorch 和 Tensorflow。

补充:这篇 DataCamp 博文是入门 Tensorflow 的优秀起点。

安装 PyTorch

安装 PyTorch 十分直接。由于 PyTorch 支持高效的 GPU 计算,它能与您的 Cuda 驱动高效通信并加速执行。

要使用 PyTorch,您需要 torch 和 torchvision。下面以 Windows 环境为例进行安装。稍后您还会看到 Linux 和 Mac 的步骤。

我使用的是 Python 3.5 搭配 Anaconda。此外,我没有 CUDA。运行以下命令安装 torch 和 torchvision:

(请注意,PyTorch 不支持 Python 2.7。因此必须使用 Python 版本 => 3.5。)

如果您的机器启用了 CUDA,欢迎直接运行以下命令(此处以 CUDA 9.0 为例):

现在,来看一下在 Linux 环境下、Python 3.5 且不支持 CUDA 的安装步骤:

是的,您猜对了!它与 Windows 的步骤相同。

若支持 CUDA(9.0),那么步骤为:

  • pip3 install torch torchvision

在 Mac 环境下,使用 Python 3.5 且无 CUDA 支持,步骤为:

  • pip3 install torch torchvision

若需 CUDA 支持(9.0):

  • pip3 install torch torchvision

(MacOS 二进制包不支持 CUDA,如需 CUDA 请从源码安装)

希望此时您已经完成了 PyTorch 的安装!

现在,让我们直接用 PyTorch 开始一些张量算术。

用 PyTorch 进行张量算术

首先,导入所有需要的库。

from __future__ import print_function
import torch

如果 PyTorch 安装成功,运行以上代码行将不会报错。

现在,构造一个 5x3 的未初始化矩阵:

x = torch.rand(5, 3)
print(x)
tensor([[ 0.5991,  0.9365,  0.6120],
        [ 0.3622,  0.1408,  0.8811],
        [ 0.6248,  0.4808,  0.0322],
        [ 0.2267,  0.3715,  0.8430],
        [ 0.0145,  0.0900,  0.3418]])

构造一个填充为 0、数据类型为 long 的矩阵:

x = torch.zeros(5, 3, dtype=torch.long)
print(x)
tensor([[ 0,  0,  0],
        [ 0,  0,  0],
        [ 0,  0,  0],
        [ 0,  0,  0],
        [ 0,  0,  0]])

直接从数据构造一个张量:

x = torch.tensor([5.5, 3])
print(x)
tensor([ 5.5000,  3.0000])

如果您已经正确理解了张量,请在评论区告诉我 x 是哪种张量!

您可以基于已有张量创建新张量。这些方法会复用输入张量的属性,例如 dtype(数据类型),除非用户提供了新值:

x = x.new_ones(5, 3, dtype=torch.double)    
print(x)

x = torch.randn_like(x, dtype=torch.float)    
print(x)
tensor([[ 1.,  1.,  1.],
        [ 1.,  1.,  1.],
        [ 1.,  1.,  1.],
        [ 1.,  1.,  1.],
        [ 1.,  1.,  1.]], dtype=torch.float64)
tensor([[-1.2174,  1.1807,  1.4249],
        [-1.1114, -0.8098,  0.4003],
        [ 0.0780, -0.5011, -1.0985],
        [ 1.8160, -0.3778, -0.8610],
        [-0.7109, -2.0509, -1.2079]])

获取它的尺寸:

print(x.size())
torch.Size([5, 3])

请注意,torch.Size 实际上是一个元组,因此支持所有元组操作。

现在,让我们对张量做一次加法运算。

张量加法

对具有相同维度的两个张量进行按元素加法,会得到一个具有相同维度的新张量,其中每个标量值都等于父张量中对应标量的按元素求和。

tensor addition

# Syntax 1 for Tensor addition in PyTorch
y = torch.rand(5, 3)
print(x)
print(y)
print(x + y)
tensor([[-1.2174,  1.1807,  1.4249],
        [-1.1114, -0.8098,  0.4003],
        [ 0.0780, -0.5011, -1.0985],
        [ 1.8160, -0.3778, -0.8610],
        [-0.7109, -2.0509, -1.2079]])
tensor([[ 0.8285,  0.7619,  0.1147],
        [ 0.1624,  0.8994,  0.6119],
        [ 0.2802,  0.2950,  0.7098],
        [ 0.8132,  0.3382,  0.4383],
        [ 0.6738,  0.2022,  0.3264]])
tensor([[-0.3889,  1.9426,  1.5396],
        [-0.9490,  0.0897,  1.0122],
        [ 0.3583, -0.2061, -0.3887],
        [ 2.6292, -0.0396, -0.4227],
        [-0.0371, -1.8487, -0.8815]])
# Syntax 2 for Tensor addition in PyTorch
print(torch.add(x, y))
tensor([[-0.3889,  1.9426,  1.5396],
        [-0.9490,  0.0897,  1.0122],
        [ 0.3583, -0.2061, -0.3887],
        [ 2.6292, -0.0396, -0.4227],
        [-0.0371, -1.8487, -0.8815]])

接下来做张量减法。

张量减法

对具有相同维度的两个张量进行按元素相减,会得到一个具有相同维度的新张量,其中每个标量值等于父张量中对应标量的按元素差值。

tensor subtraction

现在来看看张量积:

张量积

对矩阵 mat1 和 mat2 执行矩阵乘法。

如果 mat1 是 (n×m) 张量,mat2 是 (m×p) 张量,那么输出将是 (n×p) 张量。

在 PyTorch 中您可以这样做张量积:

mat1 = torch.randn(2, 3)
mat2 = torch.randn(3, 3)
print(mat1)
print(mat2)
print(torch.mm(mat1, mat2))
tensor([[ 1.9490, -0.6503, -1.9448],
        [-0.7126,  1.0519, -0.4250]])
tensor([[ 0.0846,  0.4410, -0.0625],
        [-1.3264, -0.5265,  0.2575],
        [-1.3324,  0.6644,  0.3528]])
tensor([[ 3.6185, -0.0901, -0.9753],
        [-0.8892, -1.1504,  0.1654]])

请注意,torch.mm() 不会进行广播。我们来简单讨论一下广播。

广播

广播描述了在算术运算中,形状不同的数组如何被处理。在满足一定约束条件的前提下,较小的数组会被“广播”到较大的数组上,使它们的形状兼容。广播为数组运算提供了向量化手段,使循环在 C 中而非 Python 中执行。它不会无谓地复制数据,通常可以带来高效的算法实现。不过,在某些情况下,广播不是一个好主意,因为它会导致内存使用低效,从而拖慢计算。

如果满足以下规则,则两个张量是“可广播的”:

  • 每个张量至少有一个维度。
  • 从尾部维度开始逐个比较维度大小时,要么相等、要么其中一个为 1、要么其中一个维度不存在。

让我们用下面这段 PyTorch 代码来理解广播:

x=torch.empty(5,7,3)
y=torch.empty(5,7,3)
# same shapes are always broadcastable (i.e. the above rules always hold)
x=torch.empty((0,))
y=torch.empty(2,2)
# x and y are not broadcastable, because x does not have at least 1 dimension
# can line up trailing dimensions
x=torch.empty(5,3,4,1)
y=torch.empty(  3,1,1)
# x and y are broadcastable.
# 1st trailing dimension: both have size 1
# 2nd trailing dimension: y has size 1
# 3rd trailing dimension: x size == y size
# 4th trailing dimension: y dimension doesn't exist
# but:
>>> x=torch.empty(5,2,4,1)
>>> y=torch.empty(  3,1,1)
# x and y are not broadcastable, because in the 3rd trailing dimension 2 != 3

既然您已经对广播有了较为清晰的认识,我们来看看当两个张量“可广播”时,结果张量的形状会如何。

如果两个张量 x、y 是“可广播的”,则结果张量的尺寸按如下方式计算:

若 x 与 y 的维度数量不相等,则在维度较少的张量前端补 1,使其长度相等。然后,对每个维度,结果的维度大小等于 x 与 y 在该维度上的大小取最大值。例如:

# can line up trailing dimensions to make reading easier
x=torch.empty(5,1,4,1)
y=torch.empty(  3,1,1)
(x+y).size()
torch.Size([5, 3, 4, 1])
torch.Size([5, 3, 4, 1])
# but not necessary:
x=torch.empty(1)
y=torch.empty(3,1,7)
(x+y).size()
torch.Size([3, 1, 7])
torch.Size([3, 1, 7])
x=torch.empty(5,2,4,1)
y=torch.empty(3,1,1)
(x+y).size()
---------------------------------------------------------------------------

RuntimeError                              Traceback (most recent call last)

<ipython-input-17-72fb34250db7> in <module>()
      1 x=torch.empty(5,2,4,1)
      2 y=torch.empty(3,1,1)
----> 3 (x+y).size()


RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1

现在您已经掌握了这个概念!

张量积是您最常遇到的张量乘法形式,但也存在许多其他类型的张量乘法,比如张量点积和张量收缩。

在 Torch 张量与 NumPy 数组之间相互转换非常轻松,这一概念称为Numpy 桥。我们来看看。

Numpy 桥

Torch 张量和 NumPy 数组会共享其底层内存位置,更改其中一个会改变另一个。

将 Torch 张量转换为 NumPy 数组。

# A 1D tensor of 5 ones
a = torch.ones(5)
print(a)
tensor([ 1.,  1.,  1.,  1.,  1.])
# Convert the Torch tensor to a NumPy array
b = a.numpy()
print(b)
[1. 1. 1. 1. 1.]

看看 NumPy 数组的值是如何变化的。

在上面的部分,您完成了加法、减法和张量积等一些基础的张量算术。接下来,您将用 PyTorch 实现一个基础的神经网络。

用 PyTorch 实现一个简单的神经网络

如果您需要回顾神经网络,这篇 DataCamp 文章非常适合。您也可以查看以下链接:

在实现之前,我们来简单讨论一个概念:自动微分,这是 PyTorch 中所有神经网络的核心。它对于在反向传播过程中计算梯度尤其有用。

autograd 包为所有对张量的操作提供自动求导。它是一个“动态图”框架,这意味着您的反向传播由代码的运行方式决定,每次迭代都可以不同。

让我们用一个非常简单的代码示例来看看自动微分的实际效果。

# Create a Rank-2 tensor of all ones
x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[ 1.,  1.],
        [ 1.,  1.]])

做一次加法运算。

y = x + 2
print(y)
tensor([[ 3.,  3.],
        [ 3.,  3.]])

对 y 继续做一些运算。

z = y * y * 3
out = z.mean()

print(z, out)
tensor([[ 27.,  27.],
        [ 27.,  27.]]) tensor(27.)

现在进行反向传播,因为 out 是一个标量,调用 out.backward() 等价于 out.backward(torch.tensor(1)).

out.backward()

# print gradients d(out)/dx
print(x.grad)
tensor([[ 4.5000,  4.5000],
        [ 4.5000,  4.5000]])
automatic differentiation example

现在您了解了自动微分以及 PyTorch 如何处理它。接下来您将用 PyTorch 编写一个简单的神经网络。

您将创建一个具有一个隐藏层和一个输出单元的简单神经网络。您会在隐藏层使用ReLU 激活函数,在输出层使用 Sigmoid 激活函数。

首先需要导入 PyTorch 库。可以使用 torch.nn 包来构建神经网络。

import torch
import torch.nn as nn

然后定义各层的大小以及批大小:

n_in, n_h, n_out, batch_size = 10, 5, 1, 10

现在,创建一些虚拟的输入数据 x 和目标数据 y。您将使用 PyTorch 张量来存储这些数据。PyTorch 张量的用法和操作与 NumPy 数组类似,但额外的好处是它们可以在 GPU 上运行。出于本教程的目的,我们只在 CPU 上运行;当然,将其转到 GPU 上也很简单。

x = torch.randn(batch_size, n_in)
y = torch.tensor([[1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0]])

接下来,您只用一行代码来定义模型。

model = nn.Sequential(nn.Linear(n_in, n_h),
                     nn.ReLU(),
                     nn.Linear(n_h, n_out),
                     nn.Sigmoid())

这会创建一个形如 input -> linear -> relu -> linear -> sigmoid 的模型。还有另一种定义模型的方式,适用于更复杂、定制化的模型:通过在类中定义模型。相关内容可阅读这里。

现在是构建损失函数的时候了。我们将使用均方误差损失。

criterion = torch.nn.MSELoss()

另外别忘了定义优化器。本例中我们使用强大的随机梯度下降(SGD),学习率为 0.01。model.parameters() 会返回模型参数(权重和偏置)的迭代器。

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

现在,运行 50 个 epoch 的梯度下降。它依次完成前向传播、损失计算、反向传播以及参数更新。

for epoch in range(50):
    # Forward Propagation
    y_pred = model(x)
    # Compute and print loss
    loss = criterion(y_pred, y)
    print('epoch: ', epoch,' loss: ', loss.item())
    # Zero the gradients
    optimizer.zero_grad()

    # perform a backward pass (backpropagation)
    loss.backward()

    # Update the parameters
    optimizer.step()
epoch:  0  loss:  0.2399429827928543
epoch:  1  loss:  0.23988191783428192
epoch:  2  loss:  0.23982088267803192
epoch:  3  loss:  0.2397598922252655
epoch:  4  loss:  0.23969893157482147
epoch:  5  loss:  0.23963800072669983
epoch:  6  loss:  0.23957709968090057
epoch:  7  loss:  0.23951618373394012
epoch:  8  loss:  0.23945537209510803
epoch:  9  loss:  0.23939454555511475
epoch:  10  loss:  0.23933371901512146
epoch:  11  loss:  0.23927298188209534
epoch:  12  loss:  0.23921218514442444
epoch:  13  loss:  0.23915143311023712
epoch:  14  loss:  0.2390907108783722
epoch:  15  loss:  0.23903003334999084
epoch:  16  loss:  0.23896940052509308
epoch:  17  loss:  0.23890872299671173
epoch:  18  loss:  0.23884813487529755
epoch:  19  loss:  0.23878750205039978
epoch:  20  loss:  0.23872694373130798
epoch:  21  loss:  0.2386663407087326
epoch:  22  loss:  0.2386058121919632
epoch:  23  loss:  0.23854532837867737
epoch:  24  loss:  0.23848481476306915
epoch:  25  loss:  0.23842433094978333
epoch:  26  loss:  0.2383638620376587
epoch:  27  loss:  0.23830339312553406
epoch:  28  loss:  0.2382429838180542
epoch:  29  loss:  0.23818258941173553
epoch:  30  loss:  0.2381247729063034
epoch:  31  loss:  0.2380656749010086
epoch:  32  loss:  0.23800739645957947
epoch:  33  loss:  0.2379491776227951
epoch:  34  loss:  0.2378900945186615
epoch:  35  loss:  0.23783239722251892
epoch:  36  loss:  0.23777374625205994
epoch:  37  loss:  0.23771481215953827
epoch:  38  loss:  0.23765745759010315
epoch:  39  loss:  0.23759838938713074
epoch:  40  loss:  0.23753997683525085
epoch:  41  loss:  0.2374821901321411
epoch:  42  loss:  0.23742322623729706
epoch:  43  loss:  0.23736533522605896
epoch:  44  loss:  0.23730707168579102
epoch:  45  loss:  0.23724813759326935
epoch:  46  loss:  0.23719079792499542
epoch:  47  loss:  0.23713204264640808
epoch:  48  loss:  0.23707345128059387
epoch:  49  loss:  0.2370160073041916
  • y_pred 是通过模型前向传播得到的预测值。将其与目标值 y 一并传入损失函数 criterion,用于计算损失。

  • 接着,optimizer.zero_grad() 将所有梯度清零。这样做是为了防止之前的梯度不断累积。

  • 然后,loss.backward() 是 PyTorch 的核心魔法,使用其 Autograd 特性。Autograd 会基于动态创建的计算图,自动计算与所有参数相关的梯度。基本上,这一步就是执行梯度下降的反向传播。

  • 最后,调用 optimizer.step() 使用新的梯度对所有参数进行一次更新。

至此您已读到最后。在本文中,您从张量讲到自动微分等多个主题!您还使用 PyTorch 及其张量系统实现了一个简单的神经网络。

如果您想进一步学习 PyTorch 并更深入地探索,请查看 PyTorch 的官方文档与教程。它们写得非常好。您可以在这里找到。

以下是撰写本教程时参考的一些资料:

如果您有任何问题或想讨论的内容,欢迎在评论区留言!

如果您对进一步学习 Python 感兴趣,可以学习 DataCamp 的课程:Statistical Thinking in Python (Part 1)。

主题