2.3 张量

2.3.1 张量的创建

张量(tensor)是PyTorch里基础的运算单位,类似于NumPy中的数组。但是,张量可以在GPU版本的PyTorch上运行,而NumPy中的数组只能在CPU版本的PyTorch上运行。因此,张量的运算速度更快。

下面,我们来创建一个张量。

   >>> x = torch.randn(2,2)
   >>> print(x)
   tensor([[-2.2093,  0.1976],
       [-0.9493,  1.2901]])

上面代码创建的是2×2的随机初始化张量,可以看出PyTorch的语法和函数与NumPy是非常类似的。还可以使用以下代码创建不同的张量。

根据Python列表创建张量,代码如下:

   >>> x = torch.tensor([[1, 2], [3, 4]])
   >>> print(x)
   tensor([[1, 2],
       [3, 4]])

创建一个全零的张量,代码如下:

   >>> x = torch.zeros(2,2)
   >>> print(x)
   tensor([[0., 0.],
       [0., 0.]])

基于现有的张量创建新的张量,代码如下:

   >>> x = torch.zeros(2,2)
   >>> y = torch.ones_like(x)
   >>> print(x)
   tensor([[0., 0.],
       [0., 0.]])
   >>> print(y)
   tensor([[1., 1.],
       [1., 1.]])

在创建张量的时候,还可以指定数据类型,例如创建一个长整型张量,代码如下:

   >>> x = torch.ones(2, 2, dtype=torch.long)
   >>> print(x)
   tensor([[1, 1],
       [1, 1]])

2.3.2 张量的数学运算

张量的数学运算与数组的数学运算相同,与NumPy的数学运算类似。下面通过几个例子来说明。

两个张量相加,代码如下:

   >>> x = torch.ones(2,2)
   >>> y = torch.ones(2,2)
   >>> z = x + y
   >>> print(z)
   tensor([[2., 2.],
       [2., 2.]])

也可以使用torch.add()实现张量相加,代码如下:

   >>> x = torch.ones(2,2)
   >>> y = torch.ones(2,2)
   >>> z = torch.add(x, y)
   >>> print(z)
   tensor([[2., 2.],
       [2., 2.]])

还可以使用.add_()实现张量的替换,代码如下:

   >>> x = torch.ones(2,2)
   >>> y = torch.ones(2,2)
   >>> y.add_(x)
   tensor([[2., 2.],
       [2., 2.]])
   >>> print(y)
   tensor([[2., 2.],
       [2., 2.]])

张量的乘法有两种形式,第一种是对应元素相乘,代码如下:

   >>> x = torch.tensor([[1, 2], [3, 4]])
   >>> y = torch.tensor([[1, 2], [3, 4]])
   >>> x.mul(y)
   tensor([[ 1,  4],
       [ 9, 16]])

第二种是矩阵相乘,这种形式更加常用,代码如下:

   >>> x = torch.tensor([[1, 2], [3, 4]])
   >>> y = torch.tensor([[1, 2], [3, 4]])
   >>> x.mm(y)
   tensor([[ 7, 10],
       [15, 22]])

2.3.3 张量与NumPy数组

PyTorch中的张量与NumPy数组可以互相转化,而且两者共享内存位置,如果一个发生改变,另一个也随之改变。

1.张量转换为NumPy数组

通过简单的.numpy()就可以将张量转换为NumPy数组,代码如下:

   >>> a = torch.ones(2,2)
   >>> b = a.numpy()
   >>> print(type(a))
   <class ′torch.Tensor′>
   >>> print(type(b))
   <class ′numpy.ndarray′>

此时,如果张量发生改变,对应的NumPy数组也有相同的变化,代码如下:

   >>> a.add_(1)
   tensor([[2., 2.],
      [2., 2.]])
   >>> print(b)
   [[2. 2.]
   [2. 2.]]
2.NumPy数组转换为张量

对于NumPy数组,可以通过torch.from_numpy()转化为张量,代码如下:

   >>> a = np.array([[1, 1], [1, 1]])
   >>> b = torch.from_numpy(a)
   >>> print(type(a))
   <class ′numpy.ndarray′>
   >>> print(type(b))
   <class ′torch.Tensor′>

同样,如果NumPy数组发生改变,对应的张量也有相同的变化,代码如下:

   >>> np.add(a, 1, out=a)
   array([[2, 2],
      [2, 2]])
   >>> print(b)
   tensor([[2, 2],
      [2, 2]], dtype=torch.int32)

2.3.4 CUDA张量

新建的张量默认保存在CPU里,如果安装了GPU版本的PyTorch,就可以将张量移动到GPU里,代码如下:

   >>> a = torch.ones(2,2)
   >>> if torch.cuda.is_available():
   ...     a_cuda = a.cuda()
   ...     print(a_cuda)
   tensor([[1., 1.],
      [1., 1.]], device=′cuda:0′)

2.4 自动求导

深度学习的算法在本质上是通过反向传播求导数(后面的章节将会详细介绍),此功能由PyTorch的自动求导(autograd)模块实现。关于张量的所有操作,自动求导模块都能为它们自动提供微分,避免了手动计算导数的复杂过程,可以节约大量的时间。

如果要让张量使用自动求导功能,只需要在定义张量的时候设置参数tensor.requires_grad=True即可。默认情况下,张量是没有自动求导功能的。

   >>> x = torch.ones(2, 2, requires_grad=True)
   >>> y = torch.ones(2, 2, requires_grad=True)
   >>> print(x.requires_grad)
   True
   >>> print(y.requires_grad)
   True

新建的张量xy的requires_grad参数值均为True。

2.4.1 返回值是标量

我们定义输出,代码如下:

   >>> z = x + y
   >>> z = z.mean()
   >>> print(z)
   tensor(2., grad_fn=<MeanBackward1>)

在PyTorch中,每个通过函数计算得到的变量都有一个.grad_fn属性。因为z是通过xy运算而来,所以具有grad_fn属性。如果现在要计算zx的偏导数以及zy的偏导数,首先要对z使用.backward()来定义反向传播,代码如下:

   >>> z.backward()

然后直接使用x.grad来计算,使用y.grad来计算,代码如下:

   >>> print(x.grad)
   tensor([[0.2500, 0.2500],
       [0.2500, 0.2500]])
   >>> print(y.grad)
   tensor([[0.2500, 0.2500],
       [0.2500, 0.2500]])

可以看到,只需要简单的两行语句,PyTorch的自动求导功能就可以计算出

2.4.2 返回值是张量

2.4.1小节的例子中,输出z是一个标量,不需要在backward()中指定任何参数。如果z是一个多维张量,则需要在backward()中指定参数,匹配相应的尺寸。

我们来看下面这个例子。因为返回值z不是一个标量,所以需要输入一个大小相同的张量作为参数,这里我们用ones_like()函数根据z生成一个张量。

   >>> x = torch.ones(2, 2, requires_grad=True)
   >>> y = torch.ones(2, 2, requires_grad=True)
   >>> z = 2 * x + 3 * y
   >>> z.backward(torch.ones_like(z))
   >>> print(x.grad)
   tensor([[2., 2.],
       [2., 2.]])
   >>> print(y.grad)
   tensor([[3., 3.],
       [3., 3.]])

2.4.3 禁止自动求导

我们可以使用with torch.no_grad()来禁止已经设置requires_grad=True的张量进行自动求导,这个方法在测试集测试准确率的时候会经常用到。例如:

   >>> print(x.requires_grad)
   True
   >>> print((2 * x).requires_grad)
   True
   >>> with torch.no_grad():
   ...     print((2 * x).requires_grad)
   ...
   False

2.5 torch.nn和torch.optim

PyTorch中,训练神经网络离不开两个重要的包:torch.nn和torch.optim。到目前为止本书还没有介绍神经网络,此处可以先来了解这两个非常重要的包,便于对后面内容的理解。注意,本小节的内容不会涉及神经网络的具体知识,仅介绍torch.nn和torch.optim的整体框架,便于读者理解。学习本节内容之前,最好有机器学习的基础,如线性回归、梯度下降算法等。

2.5.1 torch.nn

torch.nn是专门为神经网络设计的模块化接口,构建于自动求导模块的基础上,可用来定义和运行神经网络。可以理解为,torch.nn用于搭建一个模型,因此使用之前需要导入这个库,代码如下:

   >>> import torch
   >>> import torch.nn as nn

使用torch.nn来搭建一个模型的方法是定义一个类,代码如下:

上面的代码中,net_name就是类名,名字可以自由拟定。该类继承于父类nn.Module。nn.Module是torch.nn中十分重要的类,包含网络各层的定义及forward方法,避免从底层搭建网络的麻烦。self.fc = nn.Linear(1, 1)表示对模型的搭建,模型仅仅是一个全连接层(fc),也叫线性层。(1, 1)中的数字分别表示输入和输出的维度。这里,令输入和输出的维度都为1。学习神经网络的时候,可以在此处构建更加复杂的网络。该类还包含一个forward函数,因为模型仅仅是一层全连接层,所以out=self.fc(x)。最后,函数返回out。上面这段代码其实就是一个简单的线性回归模型。

使用该线性回归模型的时候,可以直接新建一个该模型的对象,代码如下:

   net = net_name()

2.5.2 torch.optim

torch.optim是一个实现了各种优化算法的库,包括最简单的梯度下降(Gradient Descent,GD)、随机梯度下降(Stochastic Gradient Descent,SGD)及其他更复杂的优化算法。直接输入以下命令即可导入torch.optim:

   >>> import torch.optim as optim

我们首先来了解PyTorch中的损失函数是如何定义的。损失函数用于计算每个实例的输出与真实样本的标签是否一致,并评估差距的大小。利用torch.nn可以很方便地定义损失函数。torch.nn包有多种不同的损失函数,最简单的是nn.MSELoss(),可以计算预测值与真实值的均方误差。

   criterion = nn.MSELoss()
   loss = criterion(output, target)

其中,output是预测值,target是真实值。

反向传播过程中,一般通过loss.backward()计算梯度。注意,每次迭代时梯度要先清零,否则会被累加计算。

优化算法有很多,最简单的就是使用随机梯度下降算法,只需要一行语句即可:

   optimizer = optim.SGD(net.parameters(), lr=0.01)

其中,net.parameters()表示模型参数,即待求参数。lr为学习率,这里设置为0.01。

总结一下,一次完整的前向传播加上反向传播需要用到torch.nn和torch.optim包。单次迭代对应的代码如下:

   optimizer.zero_grad()     # 梯度清零
   output = net(input)
   loss = criterion(output, target)
   loss.backward()
   optimizer.step()          # 完成更新

这里有一点需要注意,每次迭代开始,梯度都要被清零,即执行optimizer.zero_grad(),如果不清零的话,上次梯度会被累加。