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
版本,那么就需要安装 cuda
和 cudnn
,你可以点击 这里 了解更加具体的操作。
安装完成之后,就可以试着导入 torch,查看版本信息:
代码语言:javascript复制>>> import torch
>>> torch.__version__
1.9.0 cpu
张量(tensor)
tensor 中文意为张量,你可以将其理解为多维矩阵,类似 numpy 中的 ndarray,区别就是 tensor 可以在 GPU 上运行。
不必过多纠结于张量本身,只要理解就好。
创建张量
- 使用现有数据创建张量
可以使用 torch.tensor()
构造函数从 list 或序列直接构造张量,你只需要把列表直接放进去即可:
>>> a = torch.tensor([[1,2,3],[4,5,6]])
>>> a
tensor([[1, 2, 3],
[4, 5, 6]])
- 创建具有特定大小的张量
有许多构造张量的方法,如 zeros()
,full()
,ones()
,rand()
,arange()
,eye()
等,这里就不详细介绍了,使用方法与 numpy 类似,方法名也是类似的。
>>> 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
张量创建操作。
>>> d = torch.ones_like(a)
>>> d
tensor([[1., 1., 1.],
[1., 1., 1.]])
在上面的例子中,我们根据张量 a
创建了张量 d
,并保留了 a
的属性。
当然你也可以重新指定类型,就像这样:
代码语言:javascript复制>>> d_float = torch.ones_like(a, dtype=torch.float)
类似的,要创建与其他张量具有相似类型但大小不同的张量,使用 tensor.new_*
创建操作。
张量属性
从张量属性我们可以得到张量的维数、数据类型以及它们所存储的设备(CPU 或 GPU)。
代码语言:javascript复制>>> 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
的取值。
>>> # 查看第一列元素
>>> 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 数组
代码语言:javascript复制>>> 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 数组转为张量
代码语言:javascript复制>>> 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^2y=3x2 在 x=3x=3x=3 处的导数:
代码语言:javascript复制>>> x = torch.tensor(3., requires_grad=True)
>>> y = 3 * x**2
>>> y.backward()
>>> x.grad
tensor(18.)
dydx∣x=3=6x∣x=3=18frac{mathrm{d}y}{mathrm{d}x}vert_{x=3} = 6xvert_{x=3} = 18dxdy∣x=3=6x∣x=3=18
简单计算一下就可以发现,我们得到的结果是正确的。
类似的,求 z=3x2 2y2z=3x^2 2y^2z=3x2 2y2 在 x=3x=3x=3 和 y=4y=4y=4 处的偏导:
代码语言:javascript复制>>> 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.)
∂z∂x∣x=3=6x∣x=3=18frac{partial z}{partial x}vert_{x=3} = 6xvert_{x=3} = 18∂x∂z∣x=3=6x∣x=3=18 ∂z∂y∣y=4=4y∣y=4=16frac{partial z}{partial y}vert_{y=4} = 4yvert_{y=4} = 16∂y∂z∣y=4=4y∣y=4=16
在上面的例子中,我们使用的都是标量,过程也很简单。
在深度学习中,我们更多的是考虑标量对向量/矩阵求导,因为损失函数一般都是一个标量,参数又往往是向量或者是矩阵。
还是来看一个例子,对前面的例子进行简单修改,增加 x 和 y 的维度,一样的方法,我们来看一看得到什么。
代码语言:javascript复制>>> 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
你会发现,光荣的报错了,翻译一下,也就是说只有标量才能对其他东西求导。
代码语言:javascript复制>>> 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
参数,刚开始没搞懂这是什么意思,为什么前面明明报错了,加进去一个参数又好了?
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
求导
loss.backward(torch.tensor([[1.0,1.0,1.0,1.0]]))
也可以实现权重的调整,只需要调整赋值即可:
代码语言:javascript复制loss.backward(torch.tensor([[0.1,1.0,10.0,0.001]]))
神经网络
torcn.nn 是专门为神经网络设计的模块化接口。构建于 autograd 之上,可以用来定义和运行神经网络。
下面假设你已经基本具备了神经网络的基础知识,你也可以点击 这里了解关系神经网络的基础知识。
自定义一个神经网络
torch.nn.Module
是所有神经网络模块的基类,我们可以通过继承它来编写我们自己的网络,只要继承 nn.Module
,并实现它的 forward
方法,PyTorch 会根据 autograd,自动实现 backward
函数。
- 自定义一个类,该类继承自
nn.Module
类 - 在构造函数中要调用
nn.Module
的构造函数,super(Net, self).__init__()
- 在构造函数
__init__()
中添加具有可学习参数的层 - 在
forward
中实现层之间的连接关系,也就是实现前向传播(forward
方法是必须要重写的)
下面是一个简单的网络示例:
代码语言:javascript复制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
在上面的示例中,我们建立了一个简单的网络,首先让我们关注一下卷积层和全连接层是如何建立的
可学习参数的层
创建卷积层很简单,你只需要指定,输入通道数,输出通道数,卷积核大小
代码语言:javascript复制conv1 = nn.Conv2d(1, 6, kernel_size=3)
你也可以根据需要指定 stride
,padding
。
class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
全连接层也是类似的,指定输入通道数,输出通道数
代码语言:javascript复制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
,它会计算预测值和真实值的均方误差。
# 使用我们上面建立的网络
net = Net()
output = net(input)
target = Variable(torch.range(1, 10))
criterion = nn.MSELoss()
loss = criterion(out, target)
优化器
在反向传播计算完所有参数的梯度后,还需要使用优化方法来更新网络的权重和参数
在 torch.optim
中实现大多数的优化方法,例如 RMSProp、Adam、SGD 等。
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 实现了
参考资料
- PyTorch 中文官方教程 1.7
- PyTorch 中文手册(pytorch handbook)
- PyTorch 深度学习:60 分钟入门
- PyTorch 简明笔记-Tensor 的自动求导(AoutoGrad)