PyTorch学习笔记
在实验机器上配置好了CUDA、cuDNN和PyTorch,开始上手PyTorch。持续更新。
PyTorch学习笔记
0 学习资源
该教程内容简洁易读,且支持最新的PyTorch1.0正式版。以下是我学习及梳理的笔记。
PyTorch官方文档
1 背景
1.1 Jupyter Notebook
上述的学习资源中包含大量的Jupyter Notebook示例,需要安装Jupyter Notebook运行。
我安装了流行的科学计算包管理平台Anaconda3,即自带安装了Jupyter Notebook。
git clone 该教程后,在该教程根目录运行命令:
1 | jupyter notebook |
即可在弹出的窗口中看到当前教程文件夹内的目录结构,点击打开各个ipynb即可。
1.2 PyTorch
PyTorch实际上就是基于Python的科学计算包,服务于以下两种场景:
- 作为NumPy的替代品,可以使用GPU的强大计算能力(GPU加速的张量计算)
- 提供最大的灵活性和高速的深度学习研究平台(包含自动求导系统的深度神经网络)
1 | # 首先要引入相关的包 |
1.2.1 与Torch, Lua的关系
Torch是一个与Numpy类似的张量(Tensor)操作库,与Numpy不同的是Torch对GPU支持的很好。
Lua是Torch的上层包装。
PyTorch和Torch使用包含所有相同性能的C库:TH, THC, THNN, THCUNN,并且它们将继续共享这些库。PyTorch和Torch都使用的是相同的底层,只是使用了不同的上层包装语言。
注:LUA虽然快,但是太小众了,所以才会有PyTorch的出现。
2 张量
张量(Tensor)是PyTorch里面基础的运算单位,与Numpy的ndarray相同都表示的是一个多维的矩阵。 与ndarray的最大区别就是,PyTorch的Tensor可以在 GPU 上运行,而 numpy 的 ndarray 只能在 CPU 上运行,在GPU上运行大大加快了运算速度。
第零阶张量 (r = 0) 为标量 (Scalar),第一阶张量 (r = 1) 为向量 (Vector), 第二阶张量 (r = 2) 则成为矩阵 (Matrix),第三阶以上的统称为多维张量。
2.1 构建张量
1 | import torch |
注意:
torch.rand
方法是随机化[0,1)区间内的服从均匀分布的浮点数。torch.randn
方法是随机化输出零均值、单位方差的服从正态分布的浮点数。
2.2 数据类型
Tensor的基本数据类型有五种:
- 32位浮点型:torch.FloatTensor (默认)
- 64位整型:torch.LongTensor
- 32位整型:torch.IntTensor
- 16位整型:torch.ShortTensor
- 64位浮点型:torch.DoubleTensor
除以上数字类型外,还有 byte和char型。
1 | tensor = torch.tensor([3.1415926]) |
2.3 基本运算
1 | y = torch.rand(5, 3) |
2.4 基本操作
1 | # 选取 |
2.5 与NumPy互转
注意:Tensor和numpy对象共享内存,所以他们之间的转换很快,而且几乎不会消耗什么资源。但这也意味着,如果其中一个变了,另外一个也会随之改变。
1 | # Torch Tensor转NumPy Array |
2.6 CUDA Tensor
在支持CUDA的NVIDIA GPU设备上,可以在GPU上建立Tensor进行运算。
2.6.1 .cuda和.cpu方法
一般情况下可以使用.cuda
方法将tensor移动到GPU,这步操作需要CUDA设备支持。
使用.cpu
方法可以把tensor移动到CPU。
1 | cpu_a=torch.rand(4, 3) |
2.6.2 .to方法
使用.to
方法 可以将Tensor移动到任何设备中。
例1:
1 | # is_available 函数判断是否有cuda可以使用 |
例2:
1 | #使用torch.cuda.is_available()来确定是否有cuda设备 |
3 Autograd自动求导
深度学习的算法本质上是通过反向传播求导数,而PyTorch的autograd模块则实现了此功能。在Tensor上的所有操作,autograd都能为它们自动提供微分,避免了手动计算导数的复杂过程。
3.1 标量
我读了教程和官方文档,发现还是有个例子会更快理解。
这篇简书文章的例子有助于快速理解,值得一读。有趣的是,该文的例子中,函数值y和y对x在x1=1时的偏导均为4根号2,即5.6569。
1 | from torch.autograd import Variable |
在这个例子中,可以看到,当我们需要运行反向传播(Back
Propagation)算法时,直接调取Variable.grad
即可得知其梯度值。这就是PyTorch的Autograd机制自动求导的结果。
注:从0.4起, Variable 正式合并入Tensor, Variable
本来实现的自动微分功能,Tensor就能支持。读者还是可以使用Variable(tensor)
,
但是这个操作其实什么都没做。所以,以后的代码建议直接使用Tensor,因为官方文档中已经将Variable设置成过期模块要想使得Tensor使用autograd功能,只需要设置tensor.requries_grad=True
。
3.2 向量
上述的例子中,因为y是标量(scalar),所以y.backward()
相当于y.backward(torch.tensor(1))
。但如果y
是向量时,y.backward()
需要输入参数grad_tensors
表示下降梯度向量。
1 | import torch |
这篇文章做了详细的向量autograd的分析:
3.3 禁用autograd
如果.requires_grad=True
但是你又不希望进行autograd的计算,
那么可以将变量包裹在 with torch.no_grad()
中:
1 | print(x.requires_grad) # True |
这个方法在测试集测试准确率的时候回经常用到。
4 神经网络
详细文档参阅PyTorch官方文档关于nn
包的信息。
4.1 定义网络
使用torch.nn
包来构建神经网络。
nn
包依赖autograd
包来定义模型并求导。
一个nn.Module
包含各个层和一个forward(input)
方法,该方法返回output
。
除了nn
别名以外,我们还引用了nn.functional
,这个包中包含了神经网络中使用的一些常用函数,这些函数的特点是,不具有可学习的参数(如ReLU,pool,DropOut等),这些函数可以放在构造函数中,也可以不放,但是这里建议不放。
一般情况下我们会将nn.functional 设置为大写的F,这样缩写方便调用
1 | import torch |
在模型中必须要定义 forward
函数,backward
函数(用来计算梯度)会被autograd
自动创建。 可以在
forward
函数中使用任何针对 Tensor 的操作。
net.parameters()
返回可被学习的参数(权重)列表和值
1 | params = list(net.parameters()) |
测试随机输入32×32。 注:这个网络(LeNet)期望的输入大小是32×32,如果使用MNIST数据集来训练这个网络,请把图片大小重新调整到32×32。
1 | input = torch.randn(1, 1, 32, 32) # samples, channels, height, width |
将所有参数的梯度缓存清零,然后进行随机梯度的的反向传播:
1 | net.zero_grad() # 否则梯度(.grad)会累加到已存在的梯度上 |
注意:torch.nn
只支持mini-batch输入
torch.nn
只支持小批量输入。整个
torch.nn
包都只支持小批量样本,而不支持单个样本。
例如,nn.Conv2d
接受一个4维的张量,
每一维分别是sSamples * nChannels * Height * Width(样本数*通道数*高*宽)
。
如果你有单个样本,只需使用 input.unsqueeze(0)
来添加其它的维数。
4.2 损失函数
一个损失函数接受一对 (output, target) 作为输入,计算一个值来估计网络的输出和目标值相差多少。
output就是神经网络的输出结果,target就是数据的标记值。
nn
包中有很多不同的损失函数。
nn.MSELoss
是一个比较简单的损失函数,它计算输出和目标间的均方误差,
例如:
1 | output = net(input) # 1×10 tensor |
当反向传播计算loss
时,读取.grad_fn
属性,就可以看到一个图(graph):
1 | input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d |
此时,如果调用loss.backward()
函数,整个图都会去求loss
的微分,图中属性requires_grad=True
的张量的.grad
属性会累计梯度。
反向几步查看这几步的函数:
1 | print(loss.grad_fn) # MSELoss |
4.3 反向传播
调用loss.backward()
获得反向传播的误差。
但是在调用前需要清除已存在的梯度,否则梯度将被累加到已存在的梯度。
现在,我们将调用loss.backward()
,并查看conv1
层的偏差(bias)项在反向传播前后的梯度。
1 | net.zero_grad() # 清除梯度 |
4.4 更新权重
在实践中最简单的权重更新规则是随机梯度下降(SGD):
1 | weight = weight - learning_rate * gradient |
我们可以使用简单的Python代码实现这个规则:
1 | learning_rate = 0.01 |
但是当使用神经网络是想要使用各种不同的更新规则时,比如SGD、Nesterov-SGD、Adam、RMSProp等,PyTorch中构建了一个包torch.optim
实现了所有的这些规则。
使用它们非常简单:
1 | import torch.optim as optim |
5 数据加载与预处理
PyTorch通过torch.utils.data
对一般常用的数据加载进行了封装,可以很容易地实现多线程数据预读和批量加载。
并且torchvision
已经预先实现了常用图像数据集,包括CIFAR-10、ImageNet、COCO、MNIST、LSUN等数据集,可通过torchvision.datasets
方便的调用。
5.1 Dataset
Dataset
是一个抽象类,
为了能够方便的读取,需要将要使用的数据包装为Dataset
类。
自定义的Dataset
需要继承它并且实现两个成员方法:
__getitem__()
该方法定义每次怎么获取数据__len__()
该方法返回数据集的总长度
下面我们使用kaggle上的一个竞赛bluebook for bulldozers自定义一个数据集,为了方便介绍,我们使用里面的数据字典来做说明(因为条数少)
1 | from torch.utils.data import Dataset |
5.2 DataLoader
DataLoader
为我们提供了对Dataset
的读取操作,常用参数有:
batch_size
(每个batch的大小)shuffle
(是否进行shuffle操作,即在每轮epoch时是否对数据重新洗牌)num_workers
(加载数据的时候使用几个子进程)
1 | dl = torch.utils.data.DataLoader(ds_demo, batch_size=10, shuffle=True, num_workers=0) |
5.3 torchvision
torchvision
是PyTorch中专门用来处理图像的库。
5.3.1 torchvision.datasets
torchvision.datasets
可以理解为PyTorch团队自定义的Dataset
,这些Dataset
帮我们提前处理好了很多的图片数据集,我们拿来就可以直接使用:
- MNIST
- COCO
- Captions
- Detection
- LSUN
- ImageFolder
- Imagenet-12
- CIFAR
- STL10
- SVHN
- PhotoTour
我们可以直接使用,示例如下:
1 | import torchvision.datasets as datasets |
5.3.2 torchvision.models
torchvision
不仅提供了常用图片数据集,还提供了训练好的模型,可以加载之后,直接使用,或者在进行迁移学习
torchvision.models
模块的 子模块中包含以下模型结构。
- AlexNet
- VGG
- ResNet
- SqueezeNet
- DenseNet
1 | #我们直接可以使用训练好的模型,当然这个与datasets相同,都是需要从服务器下载的 |
5.3.3 torchvision.transforms
transforms
模块提供了一般的图像转换操作类,用作数据的处理和增广。
1 | from torchvision import transforms as transforms |
肯定有人会问:(0.485, 0.456, 0.406), (0.2023, 0.1994, 0.2010) 这几个数字是什么意思?
官方的这个帖子有详细的说明: https://discuss.pytorch.org/t/normalization-in-the-mnist-example/457/21 这些都是根据ImageNet训练的归一化参数,可以直接使用,我们认为这个是固定值就可以
6 图像分类实例
基于CIFAR10数据集,实现一个图像分类器实例。
训练一个典型的图像分类分类器依次按照下列顺序进行:
- 使用
torchvision
加载和归一化CIFAR10训练集和测试集 - 定义一个卷积神经网络
- 定义损失函数
- 在训练集上训练网络
- 在测试集上测试网络
6.1 处理数据
一般情况下处理图像、文本、音频和视频数据时,可以使用标准的Python包来加载数据到一个numpy数组中。
然后把这个数组转换成 torch.*Tensor
。
- 图像可以使用 Pillow, OpenCV
- 音频可以使用 scipy, librosa
- 文本可以使用原始Python和Cython来加载,或者使用 NLTK或 SpaCy 处理
特别的,对于图像任务,我们创建了一个包
torchvision
,它包含了处理一些基本图像数据集的方法。这些数据集包括
Imagenet, CIFAR10, MNIST 等。除了数据加载以外,torchvision
还包含了图像转换器, torchvision.datasets
和
torch.utils.data.DataLoader
。
torchvision
包不仅提供了巨大的便利,也避免了代码的重复。
在这个教程中,我们使用CIFAR10数据集,它有如下10个类别 :‘airplane’, ‘automobile’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’。CIFAR-10的图像都是 3x32x32大小的,即,3颜色通道,32x32像素。
1 | import torch |
可以通过matplotlib
库来绘图查看数据集中的图片
1 | import matplotlib.pyplot as plt |
6.2 定义卷积神经网络模型
从之前的神经网络一节复制神经网络代码,并修改为输入3通道图像。
1 | import torch.nn as nn |
6.3 定义损失函数与优化器
我们使用交叉熵作为损失函数,使用带动量的随机梯度下降。
1 | import torch.optim as optim |
6.4 训练神经网络模型
有趣的时刻开始了。 我们只需在数据迭代器上循环,将数据输入给网络,并优化。
1 | for epoch in range(2): # 多轮(epoch)训练 |
6.5 测试神经网络模型
我们在整个训练集上进行了2轮(epoch)训练,但是我们需要检查网络是否从数据集中学习到有用的东西。 通过预测神经网络输出的类别标签与实际情况标签进行对比来进行检测。 如果预测正确,我们把该样本添加到正确预测列表。 第一步,显示测试集中的图片并熟悉图片内容。
1 | dataiter = iter(testloader) |
结果看起来不错,至少比随机选择要好,随机选择的正确率为10%。 似乎网络学习到了一些东西。
在识别哪一个类的时候好,哪一个不好呢?
1 | class_correct = list(0. for i in range(10)) |
6.6 在GPU上训练
6.6.1 检查GPU支持
1 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") |
本节的其余部分假定device
是CUDA设备。
6.6.2 神经网络模型载入CUDA
torch.nn.Module.to
将递归遍历所有模块并将模块的参数和缓冲区转换成CUDA张量:
1 | net.to(device) # 把神经网络模型载入CUDA |
6.6.3 输入数据载入CUDA
记住:inputs 和 targets 也要转换。
1 | inputs, labels = inputs.to(device), labels.to(device) |
为什么我们没注意到GPU的速度提升很多?那是因为网络非常的小。
实践:
尝试增加你的网络的宽度(第一个nn.Conv2d
的第2个参数,第二个nn.Conv2d
的第一个参数,它们需要是相同的数字),看看你得到了什么样的加速。