PyTorch 学习笔记

前言

PyTorch 是一个基于 python 的科学计算包,主要针对两类人群:

  • 作为 NumPy 的替代品,可以利用 GPU 的性能进行计算
  • 作为一个高灵活性、速度快的深度学习平台

安装 pytorch

你可以登录 官方网站 选择你想要的安装方式,然后 根据对应的安装命令安装即可。

有几点需要注意:

  • PyTorch 对应的 Python 包名为 torch 而非 pytorch
  • 若需使用 GPU 版本的 PyTorch, 需要先配置英伟达显卡驱动,再安装 PyTorch

PS: 为了方便最好是将 conda 和 pip 的软件源修改成内地源,这样的话,使用 conda 或者 pip 安装软件速度会快很多,你可以点击 这里 了解如何对 conda 和 pip 进行换源。

PPS: 如果只是为了学习,没有 N 卡,或者说显卡的显存较小,可以只安装 CPU 版本的 torch,对于数据集较小、规模不大的神经网络还是 ok 的。当然如果你想要 GPU 加速,安装 GPU 版本,那么就需要安装 cudacudnn,你可以点击 这里 了解更加具体的操作。

安装完成之后,就可以试着导入 torch,查看版本信息:

1
2
3
>>> import torch
>>> torch.__version__
1.9.0+cpu

张量(tensor)

tensor 中文意为张量,你可以将其理解为多维矩阵,类似 numpy 中的 ndarray,区别就是 tensor 可以在 GPU 上运行。

不必过多纠结于张量本身,只要理解就好。

创建张量

  • 使用现有数据创建张量

可以使用 torch.tensor() 构造函数从 list 或序列直接构造张量,你只需要把列表直接放进去即可:

1
2
3
4
>>> a = torch.tensor([[1,2,3],[4,5,6]])
>>> a
tensor([[1, 2, 3],
[4, 5, 6]])
  • 创建具有特定大小的张量

有许多构造张量的方法,如 zeros()full()ones()rand()arange()eye() 等,这里就不详细介绍了,使用方法与 numpy 类似,方法名也是类似的。

1
2
3
4
5
6
7
8
9
>>> b = torch.zeros(2, 3)        # 构造一个2×3大小全为0的矩阵
>>> b
tensor([[0., 0., 0.],
[0., 0., 0.]])

>>> c = torch.full((2, 3), 3) # 构造一个2×3大小全为3的矩阵
>>> c
tensor([[3, 3, 3],
[3, 3, 3]])
  • 通过已有的张量来生成新的张量

要创建与另一个张量具有相同大小(和相似类型)的张量,请使用 torch.*_like 张量创建操作。

1
2
3
4
>>> d = torch.ones_like(a)
>>> d
tensor([[1., 1., 1.],
[1., 1., 1.]])

在上面的例子中,我们根据张量 a 创建了张量 d,并保留了 a 的属性。

当然你也可以重新指定类型,就像这样:

1
>>> d_float = torch.ones_like(a, dtype=torch.float)

类似的,要创建与其他张量具有相似类型但大小不同的张量,使用 tensor.new_* 创建操作。

张量属性

从张量属性我们可以得到张量的维数、数据类型以及它们所存储的设备(CPU 或 GPU)。

1
2
3
4
5
6
7
8
9
10
11
12
>>> x = torch.rand(2,3)
tensor([[0.3985, 0.1727, 0.4334],
[0.3326, 0.0931, 0.8320]])

>>> x.shape # 可以使用与numpy相同的shape属性查看
torch.Size([2, 3])

>>> x.size() # 也可以使用size()函数
torch.Size([2, 3])

>>> x.dtype # 查看数据类型
torch.float32

张量运算

张量可以进行转置、索引、切片、数学运算等操作,使用方法也是与 numpy 类似

特别需要注意的是,自动赋值运算通常在方法后有 _ 作为后缀, 例如: x.copy_(y), x.t_()操作会改变 x 的取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> # 查看第一列元素
>>> x[:,1]
tensor([0.1727, 0.0931])

>>> # 按行求和
>>> torch.sum(x, dim=1)
tensor([1.0045, 1.2577])

>>> # 张量加法
>>> y = torch.ones(2,3)
>>> # 也可以写成 torch.add(x, y)
>>> x + y
tensor([[1.3985, 1.1727, 1.4334],
[1.3326, 1.0931, 1.8320]])

>>> # 以_为结尾的,均会改变调用值(直接修改 x)
>>> x.add_(y)
tensor([[1.3985, 1.1727, 1.4334],
[1.3326, 1.0931, 1.8320]])

与 numpy 的桥接

我们可以很简单的实现一个 Torch 张量和一个 NumPy 数组之间的相互转化。

需要注意的是,Torch 张量和 NumPy 数组将共享它们的底层内存位置,因此当一个改变时,另外一个也会改变

由张量变换为 Numpy array 数组

1
2
3
4
5
>>> t = torch.ones(5)
>>> n = t.numpy()
>>> t ; n
tensor([1., 1., 1., 1., 1.])
array([1., 1., 1., 1., 1.], dtype=float32)

由 Numpy array 数组转为张量

1
2
3
4
5
>>> n = np.ones(5)
>>> t = torch.from_numpy(n)
>>> t ; n
tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
array([1., 1., 1., 1., 1.])

自动求导(autograd)

torch.autograd 是 PyTorch 的自动差分引擎,可为神经网络训练提供支持。

具体来说,我们可以在张量创建时,通过设置 requires_grad 标识为 Ture ,那么 autograd 将会追踪对于该张量的所有操作,当完成计算后可以通过调用 backward(),来自动计算所有的梯度,并将其存储在各个张量的 .grad 属性中

可能有点不明所以,还是通过具体的例子来说明

简单实现

我们从一个最简单的例子开始:求 y=3x2y=3x^2x=3x=3 处的导数:

1
2
3
4
5
>>> x = torch.tensor(3., requires_grad=True)
>>> y = 3 * x**2
>>> y.backward()
>>> x.grad
tensor(18.)

dydxx=3=6xx=3=18\frac{\mathrm{d}y}{\mathrm{d}x}\vert_{x=3} = 6x\vert_{x=3} = 18

简单计算一下就可以发现,我们得到的结果是正确的。

类似的,求 z=3x2+2y2z=3x^2+2y^2x=3x=3y=4y=4 处的偏导:

1
2
3
4
5
6
7
>>> x = torch.tensor(3., requires_grad=True)
>>> y = torch.tensor(4., requires_grad=True)
>>> z = 3 * x**2 + 2 * y**2
>>> z.backward()
>>> x.grad ; y.grad
tensor(18.)
tensor(16.)

zxx=3=6xx=3=18\frac{\partial z}{\partial x}\vert_{x=3} = 6x\vert_{x=3} = 18

zyy=4=4yy=4=16\frac{\partial z}{\partial y}\vert_{y=4} = 4y\vert_{y=4} = 16

在上面的例子中,我们使用的都是标量,过程也很简单。

在深度学习中,我们更多的是考虑标量对向量/矩阵求导,因为损失函数一般都是一个标量,参数又往往是向量或者是矩阵。

还是来看一个例子,对前面的例子进行简单修改,增加 x 和 y 的维度,一样的方法,我们来看一看得到什么。

1
2
3
4
5
6
>>> x = torch.tensor([1., 2., 3.], requires_grad=True)
>>> y = torch.tensor([2., 3., 4.], requires_grad=True)
>>> z = 3 * x**2 + 2 * y**2
>>> z.backward()
Traceback (most recent call last):
RuntimeError: grad can be implicitly created only for scalar outputs

你会发现,光荣的报错了,翻译一下,也就是说只有标量才能对其他东西求导。

1
2
3
4
5
6
7
8
>>> x = torch.tensor([1., 2., 3.], requires_grad=True)
>>> y = torch.tensor([2., 3., 4.], requires_grad=True)
>>> z = 3 * x**2 + 2 * y**2
>>> L = z.mean()
>>> L.backward()
>>> x.grad ; y.grad
tensor([2., 4., 6.])
tensor([2.6667, 4.0000, 5.3333])

在上面的例子中,我们对 z 求了平均,让 L 成为一个标量,之后便可以进行同样的操作。

一些注意点:

  • 要想使 x 支持求导,必须让 x 为浮点类型,定义时应该是 [1., 2., 3.] 而不是 [1, 2, 3]
  • 在求导时,只能是标量对标量,或者标量对向量/矩阵求导。

一些其它问题

在官方文档中,有这么一段代码,它使用了 backward 中的 gradient 参数,刚开始没搞懂这是什么意思,为什么前面明明报错了,加进去一个参数又好了?

1
2
3
4
5
6
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
Q = 3*a**3 - b**2

external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)

就像上面说的,损失函数一般都是一个标量,我们直接通过 loss.backward() 即可。

但是,有时候我们可能会有多个输出值,比如 loss=[loss1,loss2,loss3],那么我们可以让 loss 的各个分量分别对 x 求导

1
loss.backward(torch.tensor([[1.0,1.0,1.0,1.0]]))

也可以实现权重的调整,只需要调整赋值即可:

1
loss.backward(torch.tensor([[0.1,1.0,10.0,0.001]]))

神经网络

torcn.nn 是专门为神经网络设计的模块化接口。构建于 autograd 之上,可以用来定义和运行神经网络。

下面假设你已经基本具备了神经网络的基础知识,你也可以点击 这里了解关系神经网络的基础知识。

自定义一个神经网络

torch.nn.Module 是所有神经网络模块的基类,我们可以通过继承它来编写我们自己的网络,只要继承 nn.Module,并实现它的 forward 方法,PyTorch 会根据 autograd,自动实现 backward 函数。

  1. 自定义一个类,该类继承自 nn.Module
  2. 在构造函数中要调用 nn.Module 的构造函数,super(Net, self).__init__()
  3. 在构造函数 __init__() 中添加具有可学习参数的层
  4. forward 中实现层之间的连接关系,也就是实现前向传播(forward 方法是必须要重写的)

下面是一个简单的网络示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
def __init__(self):
# 执行父类的构造函数
super(Net, self).__init__()

# 输入通道数为 1,输出通道数为 6,卷积核大小为 3×3
self.conv1 = nn.Conv2d(1, 6, kernel_size=3)
self.conv2 = nn.Conv2d(6, 16, kernel_size=3)

# 输入通道数为 576,输出通道数为 120
self.fc1 = nn.Linear(6 * 6 * 16, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# x -> conv1 -> relu -> 池化
x = self.conv1(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)

# x -> conv2 -> relu -> 池化
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, 2)

# 将向量压扁为1维,进入全连接
x = x.view(x.size()[0], -1)
# fc1 -> relu -> fc2 -> relu -> fc3
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

在上面的示例中,我们建立了一个简单的网络,首先让我们关注一下卷积层和全连接层是如何建立的

可学习参数的层

创建卷积层很简单,你只需要指定,输入通道数,输出通道数,卷积核大小

1
conv1 = nn.Conv2d(1, 6, kernel_size=3)

你也可以根据需要指定 stridepadding

class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)

全连接层也是类似的,指定输入通道数,输出通道数

1
fc1 = nn.Linear(6 * 6 * 16, 120)

class torch.nn.Linear(in_features, out_features, bias=True)

重写 forward

forward 中,你需要实现整个正向传播的过程,也就是把上面建立的网络连接起来

只需要定义 forward 函数,就可以使用 autograd 为您自动定义 backward 函数(计算梯度)

损失函数

损失函数用于计算模型的预测值与实际值之间的误差,PyTorch 同样预置了许多损失函数,https://pytorch.org/docs/stable/nn.html#loss-functions。

一个简单的例子是 nn.MSELoss,它会计算预测值和真实值的均方误差。

1
2
3
4
5
6
7
# 使用我们上面建立的网络
net = Net()
output = net(input)
target = Variable(torch.range(1, 10))

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

优化器

在反向传播计算完所有参数的梯度后,还需要使用优化方法来更新网络的权重和参数

torch.optim 中实现大多数的优化方法,例如 RMSProp、Adam、SGD 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
output = net(input)
target = Variable(torch.range(1, 10))

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

# 新建一个优化器,SGD只需要要调整的参数和学习率
optimizer = torch.optim.SGD(net.parameters(), lr = 0.01)

# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()

# 计算损失函数
loss.backward()

# 更新参数
optimizer.step()

这样,神经网络的数据的一个完整的传播就已经通过 PyTorch 实现了

参考资料