安装环境

Pytorch 的安装可参考官方教程

查看安装版本:

1
2
import torch
print(torch.__version__) #注意是双下划线

Cuda

统一计算设备架构(Compute Unified Device Architecture, CUDA),是由NVIDIA推出的通用并行计算架构。通过Cuda,我们可以利用设备的GPU实现更高效的并行计算。

pytorch支持cuda,因此我们如果有此需求则需要为我们的设备安装cuda。

使⽤ nvidia-smi 命令查看当前Cuda版本。通过如下代码检验GPU是否可用:

1
2
3
import torch
print(torch.version.cuda)
print(torch.cuda.is_available())

注意:PyTorch的安装版本需要与系统的 Cuda 版本相匹配,见官方网站:Previous PyTorch Versions | PyTorch

Tensors

Tensors,译为张量,是一种特殊的数据结构,它在结构上和nn 维数组十分相似。

具有⼀个轴的张量对应数学上的向量(vector);具有两个轴的张量对应数学上的矩阵(matrix);具有两个轴以上的张量没有特殊的数学名称。

在 Pytorch 中,我们使用 Tensors 来给模型的输入输出以及参数进行编码,还可以跟踪计算图和计算梯度,可以在 GPU 或其他专用硬件上运行从而加速计算。除此之外,它的其他用法基本类似于 Numpy 中的 ndarrays。

创建 Tensor

直接创建

1
2
3
4
5
torch.arange(n) # 创建元素内容依次是 0~n 的1d向量
torch.zeros((m, n, l)) # 创建 m×n×l 大小的全0张量
torch.ones((m, n, l)) # 创建 m×n×l 大小的全1张量
torch.randn(m,n) # 创建 m×n 大小的张量, 每个元素服从于N(0,1)标准⾼斯分布
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) #根据python嵌套列表直接创建

从Numpy中创建

1
2
3
4
5
6
7
8
9

import torch
import numpy as np

x = np.array([1,2,3])
a = torch.from_numpy(x) # 跟随 numpy
b = torch.Tensor(x) # torch.Tensor()返回的是Float数据类型
c = torch.tensor(x) # torch.tensor()根据输入类型生成相应类型(也支持pandas数据)

🧬 torch.Tensor() 是默认张量类型 torch.FloatTensor() 的别名,与小写 t 作区分

🤖 默认类型也可进行自定义修改:

1
torch.set_default_tensor_type(torch.DoubleTensor)

当然,我们也可以通过 numpy() 方法 把 Tensor 转换为 numpy 类型:

1
2
3
numpy_array = x.numpy() # 如果tensor在CPU上
numpy_array = x.cpu.numpy() # 如果tensor在GPU上
print(type(numpy_array)) # 输出 : <class 'numpy.ndarray'>

导出&加载 Tensor

PyTorch中的 Tensor 可以保存成 .pt 或者 .pth 格式的文件,使用 torch.save()方法保存张量,使用 torch.load()来读取张量:

1
2
3
4
5
x = torch.rand(4,5)
torch.save(x, "./myTensor.pt")

y = torch.load("./myTensor.pt")
print(y)

更多:PyTorch教程:PyTorch中保存与加载tensor和模型详解

运算符

按元素运算

值得注意的是,在PyTorch Tensors里,形如 X = X+Y 的代码实际上会生成新的引用指向新开辟的内存 X ,旧 X 依然存在。而 X += YX[:] = X+Y 则直接对原内存数据进行更新

可以用 id() 函数进行验证:

1
2
3
4
5
6
7
8
9
10
11
import torch

X = torch.tensor([10])
Y = torch.tensor([10])
print(1,id(X))
X = X + Y
print(2,id(X))
X[:] = X + Y
print(3,id(X))
X += Y
print(4,id(X))

其他运算符,如 +, -, *, /** 以及 .exp(x),均默认是对张量按元素进行计算(而非矩阵意义上的)。

判断语句

类似于 X == Y 这样的判断语句,则返回和X,YX,Y 形状一样的二值张量,在xi=yix_i=y_i 的位置值为 True

求和运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
x = torch.arange(12, dtype=torch.float32)
y = x.clone() #深拷贝/开辟新内存给y, y的内容与x一致
y.reshape(3,4) # y的形状变为 3×4

# 1. 所有元素求和,返回标量
x.sum()

# 2. 指定结果是0轴,相当于按列求和,每一元素是y原每一列的元素之和,最终构成行向量
y.sum(axis=0)

# 3. 按行求和,每一元素是y原每一行的元素之和,最终构成列向量,但在torch中都是1d张量
y.sum(axis=1)

# 4. 等价于 y.sum()
y.sum(axis=[0, 1])

# 5. 保持维度不变/不降维
y.sum(axis=1, keepdims=True)

# 6. 按轴累加不降维
y.cumsum(axis=0) #相当于第二行的值=原第二行+原第一行,第三行的值相当于原前三行累加

均值计算

1
2
3
x.mean() # 计算均值

x.sum() / x.numel() # 二者等价

同样也可以根据指定参数 axis 的值来实现按某一轴(axis)/维度 求均值。

向量点积

向量x\mathbf x 与向量y\mathbf y 的点积可以表示为xTy\mathbf x^T\mathbf y(x,y)(\mathbf x,\mathbf y)。在数值上相当于二者按元素相乘并求和。

1
torch.dot(x, y) # 等价于 torch.sum(x * y)

矩阵相乘

在线性代数中,矩阵与向量的乘积 和 矩阵与矩阵的乘积 别无二致。也是因为线性代数里一般默认向量是指列向量。

而在 Torch 中,所谓向量就是指一维张量,默认轴 axis=0,所以可以看作“行向量”。因此,在 Torch 中独立出一个运算方法给矩阵与向量的乘积

1
2
3
torch.mv(A, x)
# 或
A @ x

最终返回的依然是一维张量。

而一般的矩阵乘法所用的语句是:

1
2
3
torch.mm(A, B)
# 或
A @ B

此外还有:

  • torch.mul(a, b) 是矩阵 ab 对应位相乘,即阿达玛积ab的形状必须相同;
  • torch.bmm(a, b) a batch matrix-matrix product,针对三维张量 ab ,要求它们的第0维(dim=0)的大小相等——批次大小,然后剩下的两个维度作矩阵乘法a (b,m,n) * b (b,n,l) -> output (b,m,l)
  • torch.matmul() 没有强制规定维度和大小,可以用利用广播机制进行不同维度的相乘操作

详见:torch.mul() 、 torch.mm() 及torch.matmul()的区别 - 简书

L2范数

Torch 中提供了直接计算向量/一维张量 的 L2范数的方法:

1
torch.norm(x)

事实上,对于矩阵的Frobenius范数,也同样适用,即可以使用 torch.norm(A) 计算矩阵AA 的Frobenius范数。

此外,根据L1范数的定义,我们也可以直接用 torch.abs(x).sum() 来计算向量的 L1范数。

🔔连结/拼接

Pandas.DataFrame 一样,我们也可以把多个张量连结(concatenate)在⼀起,把它们端对端地叠起来形成⼀个更⼤的张量。

1
2
3
4
5
X = torch.arange(12, dtype=torch.float32).reshape((3,4))

Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
1
2
3
4
5
6
7
8
9
10
11
12
# output

(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 1., 4., 3.],
[ 1., 2., 3., 4.],
[ 4., 3., 2., 1.]]), #按行拼接/dim=0

tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 4., 3., 2., 1.]])) #按列拼接/dim=1

🔔维度转换

升维和降维

1
2
3
4
5
6
7
8
squeeze

unsqueeze

view

reshape

交换维度

1
2
3
transpose

permute

(推荐)使用 einops

待更:https://blog.csdn.net/qq_37297763/article/details/120348764

自动求导

假设欲对函数y=2xTxy = 2\mathbf x^T\mathbf x 关于列向量x\mathbf x 求导。
事实上我们已经有先验知识了,即y=4x\nabla y=4\mathbf x,接下来使用 Torch 进行验证即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

import torch

x = torch.arange(4.0)
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
y = 2*torch.dot(x, x)

print(x.grad) # 开始时默认值是None

y.backward() # 调⽤反向传播函数来⾃动计算y关于x每个分量的梯度
print(x.grad)

# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值,等待下一次计算
x.grad.zero_()

在我们计算yy 关于x\mathbf x 的梯度之前,需要⼀个地⽅来存储梯度:x.requires_grad_(True)

重要的是,我们不会在每次对⼀个参数求导时都分配新的内存。因为我们经常会成千上万次地更新相同的参数,每次都分配新的内存可能很快就会将内存耗尽。
所以,我们调用的是带有下划线 _ 后缀的函数,这种可以改变 tensor 变量的操作都带有后缀,例如 x.copy_(y), x.t_() 都可以直接在原内存上改变 x 变量。

非标量反向传播

在PyTorch中有个简单的规定,不让张量对张量求导,只允许标量对张量求导。
因此,目标量对一个非标量调用 backward(),则需要传入一个gradient参数用于把张量对张量的求导转换为标量对张量的求导。

具体来说,其运作机制如下:
y=f(x)\mathbf y=\boldsymbol f(\mathbf x),其中y,x\mathbf y,\mathbf x 均为列向量,则在 PyTorch 中,给定 gradient 这个0-1向量:v,vi{0,1}\mathbf v,v_i\in\{0,1\} ,在 y.backward(v) 之后得到的 x.grad 为:

(vTy)=(yivi)\nabla(\mathbf v^T\mathbf y)=\nabla\left(\sum y_i· v_i\right)

因此,若想计算非标量y\mathbf y 关于x\mathbf x 实际的雅可比矩阵,则需要依次对viv_i 进行置1(其余置0)计算得到yiy_i 的梯度,最后拼接起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
x = torch.tensor([2, 3], dtype=torch.float, requires_grad=True)
y = torch.zeros(1, 2)

# 初始化雅克比矩阵
J = torch.zeros(2, 2)

# 定义y与x之间的映射关系
y[0, 0] = x[0] ** 2 + 3 * x[1]
y[0, 1] = x[1] ** 2 + 2 * x[0]

# 生成y1对x的梯度
y.backward(torch.Tensor([[1, 0]]), retain_graph=True)
J[0] = x.grad

x.grad.zero_()

# 生成y2对x的梯度
y.backward(torch.Tensor([[0, 1]]))
J[1] = x.grad

# 显示雅克比矩阵的值
print("J雅克比矩阵的值:{}".format(J))

注意:这里因为重复使用backward(),需要使参数retain_graph=True 避免计算图被清除。

分离计算

有时,我们希望将某些计算移动到记录的计算图之外。
例如,函数zz 包含有x\mathbf xyy ,而yy 本身又是x\mathbf x 的函数。我们想计算zz 关于x\mathbf x 的梯度,但不希望yy 作为x\mathbf x 的函数被纳入计算,而是作为常数计算。

举例说明,假设z=y+x,  y=x2z=y+x,\;y=x^2,当x=3x=3 时,有y=9y=9
我们希望计算得到zx=(9+x)x=3=1z'_x=(9+x)'|_{x=3}=1 ,而不是zx=(x2+x)x=3=7z_x'=(x^2+x)'|_{x=3}=7

为了应对这种问题,PyTorch提供了 detach() 方法,分离yy 来返回⼀个新变量uu,该变量与yy 具有相同的值,但丢弃了计算图中如何计算yy 的任何信息。

1
2
3
4
5
6
7
8
9
10
import torch

x = torch.tensor([3.], requires_grad=True)
y = x**2

u = y.detach()
z = u + x

z.backward()
print(x.grad == torch.tensor([1.])) # 返回 True

控制流的梯度

在 PyTorch 中,函数y=f(x)y=f(\mathbf x) 即使是很难写出清晰明朗的公式,只能利用 Python 的基本控制流语句,如for, while ,if 等,来对ff 进行定义,那么也同样能计算梯度。

例如:f={i=04xi+114,x5>114514i=05,x5114514\begin{aligned}f=\begin{cases}\sum_{i=0}^4 x_i+114,x_5>114514\\\sum_{i=0}^5,x_5\leq 114514\end{cases}\end{aligned} 这类复杂函数,我们可以在Python中定义:

1
2
3
4
5
6
7
8
9
def f(x):
y = 0
for i in range(5):
y += x[i] #先加前 0~4 项
if(x[5] > 114514):
y += 114
else:
y += x[5]
return y

x=(1,1,1,1,1,120000)T\mathbf x=(1,1,1,1,1,120000)^T 时,手工计算梯度可得,f=[1,1,1,1,1,0]\begin{aligned}\nabla f=[1,1,1,1,1,0]\end{aligned}

自动求导验证一下:

1
2
3
4
5
6
7

x = torch.tensor([1,1,1,1,1,120000], dtype=torch.float32, requires_grad=True)
y = f(x)

y.backward()
ans = (x.grad == torch.cat((torch.ones(5),torch.zeros(1)),dim=0))
print(ans) # 返回 True

数据集处理

批量读取加载器

在深度学习中,我们常常需要从大量原始数据集中取出样本数为B\cal B (batch size)的小批量样本数据,用于展示、梯度下降优化损失函数、可视化等等。

待更:https://blog.csdn.net/qq_45634934/article/details/125408600

构建数据集对象

为了应对自己的任务(而不是利用现有的开源数据集进行实验),我们需要构建自己的数据集从而在PyTorch中实现批量读取和后续的模型训练。

PyTorch的 torch.utils.Data.DataSet 是可用于后续任务的数据集抽象类。其源码如下:

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
class Dataset(object):
r"""An abstract class representing a :class:`Dataset`.

All datasets that represent a map from keys to data samples should subclass
it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching a
data sample for a given key. Subclasses could also optionally overwrite
:meth:`__len__`, which is expected to return the size of the dataset by many
:class:`~torch.utils.data.Sampler` implementations and the default options
of :class:`~torch.utils.data.DataLoader`.

.. note::
:class:`~torch.utils.data.DataLoader` by default constructs a index
sampler that yields integral indices. To make it work with a map-style
dataset with non-integral indices/keys, a custom sampler must be provided.
"""

def __getitem__(self, index):
raise NotImplementedError

def __add__(self, other):
return ConcatDataset([self, other])

# No `def __len__(self)` default?
# See NOTE [ Lack of Default `__len__` in Python Abstract Base Classes ]
# in pytorch/torch/utils/data/sampler.py

我们要想构建自己的数据集,就必须编写自己的数据集类,并且继承自 Datasets

源码还指出,我们必须在自己的数据集类里重载 __init__()__getitim__() ,前者一般用于连接文件路径和文件本身,后者 __getitim__() 应该编写支持数据集索引的函数,使得通过它可以直接读取指定索引下的数据。一般地,我们还需要实现 __len__() 返回数据集的样本数。

以图像数据集为例,下面是 CIFAR10 数据集的构建方法:

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
37
38
39
40
41
42
43
44
45
46
import json
import matplotlib.pyplot as plt
import numpy as np
from torch.utils.data import Dataset,DataLoader

class CIFAR10_IMG(Dataset):

def __init__(self, root, train=True, transform = None, target_transform=None):
super(CIFAR10_IMG, self).__init__()
self.train = train
self.transform = transform
self.target_transform = target_transform

#如果是训练则加载训练集,如果是测试则加载测试集
if self.train :
file_annotation = root + '/annotations/cifar10_train.json'
img_folder = root + '/train_cifar10/'
else:
file_annotation = root + '/annotations/cifar10_test.json'
img_folder = root + '/test_cifar10/'
fp = open(file_annotation,'r')
data_dict = json.load(fp)

#如果图像数和标签数不匹配说明数据集标注生成有问题,报错提示
assert len(data_dict['images'])==len(data_dict['categories'])
num_data = len(data_dict['images'])

self.filenames = []
self.labels = []
self.img_folder = img_folder
for i in range(num_data):
self.filenames.append(data_dict['images'][i])
self.labels.append(data_dict['categories'][i])

def __getitem__(self, index):
img_name = self.img_folder + self.filenames[index]
label = self.labels[index]

img = plt.imread(img_name)
img = self.transform(img) #可以根据指定的转化形式对数据集进行转换

#return回哪些内容,那么我们在训练时循环读取每个batch时,就能获得哪些内容
return img, label

def __len__(self):
return len(self.filenames)

文件读写

保存训练的模型可以使得将来在各种环境中模型得以使⽤(⽐如在部署中进⾏预测)。
此外,当运⾏⼀个耗时较⻓的训练过程时,最佳的做法是定期保存中间结果,以确保在服务器电源被不⼩⼼断掉时,不会损失⼏天的计算结果。

因此, PyTorch 同样提供了对张量、参数、以及整个模型的保存与加载功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
x = torch.arange(4)
y = torch.zeros(4)

# 保存和加载单个张量
torch.save(x, 'x-file')
x2 = torch.load('x-file')

# 张量列表
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')

# 字典
mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')


模型的读写

保存单个权重向量(或其他张量)确实有⽤,但是如果我们想保存整个模型,并在以后加载它们,单独保存每个向量则会变得很⿇烦。毕竟,我们可能有数百个参数散布在各处。因此,深度学习框架提供了内置函数来保存和加载整个⽹络。

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

import torch.nn as nn
import torch.nn.functional as F

# 以自定义的 MLP 模型为例
class MLP(nn.Module):
def __init__(self):
super().__init__()

self.hidden = nn.Linear(20, 256)
self.out = nn.Linear(256, 10)

def forward(self, X):
return self.out(F.relu(self.hidden(X)))

net = MLP() # 实例化一次模型


X = torch.randn(size=(2, 20))
Y = net(X)

# 将模型的参数存储在⼀个叫做“mlp.params”的⽂件中
torch.save(net.state_dict(), 'mlp.params')

# 实例化模型,直接导入刚刚保存的参数
clone = MLP()
clone.load_state_dict(torch.load('mlp.params'))
clone.eval() # 设置为评估模式

# 判断结果是否一致
Y_clone = clone(X)
Y_clone == Y # 返回true

深度学习训练模板

总体框架

自定义网络

为了实现复杂的神经网络搭建,深度学习中引⼊了神经⽹络块的概念。块(block)可以描述单个层、由多个层组成的组件或整个模型本⾝。

每个块必须提供的基本功能如下:

  1. 将输⼊数据作为其前向传播函数的参数。
  2. 通过前向传播函数来⽣成输出。请注意,输出的形状可能与输⼊的形状不同。
  3. 计算其输出关于输⼊的梯度,可通过其反向传播函数进⾏访问。通常这是⾃动发⽣的。
  4. 存储和访问前向传播计算所需的参数。
  5. 根据需要初始化模型参数

MLP 为例进行自定义:

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

import torch.nn as nn
import torch.nn.functional as F

# 自定义层
class MyLinear(nn.Module):

# 接收输入数in_units、输出数units
def __init__(self, in_units, units):
# 调⽤⽗类Module的构造函数来执⾏必要的初始化
super().__init__()

# 定义参数
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))

# 定义模型的前向传播,即如何根据输⼊X返回所需的模型输出
def forward(self, X):
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)

# 自定义块
class MLP(nn.Module):
def __init__(self):
super().__init__()

self.hidden = MyLinear(20, 256) # 隐藏层
self.out = MyLinear(256, 10) # 输出层

def forward(self, X):
return self.out(F.relu(self.hidden(X)))

访问参数

当通过 Sequential 类定义模型时,我们可以通过索引来访问模型的任意层:

1
2
3
4
5
6
7
8
9
10
print(net[2].state_dict()) #检查第三个网络层的参数

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data) #检查偏置
print(net.state_dict()['2.bias'].data) #与上一行等价

print(net[2].weight.grad) #检查权重梯度

print(*[(name, param.shape) for name, param in net.named_parameters()]) #⼀次性访问所有参数

参数的常见初始化

通过 net.apply() 可以实现很多个性化的参数初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

from torch.nn import init

def init_normal(m):
if type(m) == nn.Linear: # 对特定类型的网络进行处理
nn.init.normal_(m.weight, mean=0, std=0.01) # 高斯分布
nn.init.zeros_(m.bias) #置0

def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1) # 常数
nn.init.zeros_(m.bias)

def init_xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight) # Xavier初始化

net.apply(init_normal) # 以上函数均可选

# 当然依然可以直接赋值
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42

参数绑定

有时候我们需要共享模型参数,比如在循环神经网络中同一个层循环使用。事实上,模型参数其实就是一个 Tensor 的子类,也就是一个张量,要共享它,只需要多次调用同一个层即可。
因为调用同一个层,计算的时候就是使用的这个层代表的张量,所以多次调用同一个层相当于共享了模型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 我们需要给共享层⼀个名称,以便可以引⽤它的参数
shared = nn.Linear(8, 8)

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))

net(X)

# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100

# 确保它们实际上是同⼀个对象,⽽不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0]) # 返回全true

当参数绑定时,梯度会发⽣什么情况?
答案是由于模型参数包含梯度,因此在反向传播期间第⼆个隐藏层(即第三个神经⽹络层)和第三个隐藏层(即第五个神经⽹络层)的梯度会加在⼀起

优化器选取

Pytorch优化器全总结(四)常用优化器性能对比 含代码_优化器比较-CSDN博客

GPU加速

首先确保⾄少安装了⼀个NVIDIA GPU 并且下载安装了 NVIDIA 驱动 和 CUDA(本文开头已提及)

终端中可以使⽤ nvidia-smi 命令查看显卡信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.98 Driver Version: 535.98 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name TCC/WDDM | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 4070 WDDM | 00000000:01:00.0 Off | N/A |
| 0% 41C P8 6W / 200W | 0MiB / 12282MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| No running processes found |
+---------------------------------------------------------------------------------------+

在PyTorch中,每个数组都有⼀个设备(device),我们通常将其称为环境(context)。默认情况下,所有变量和相关的计算都分配给CPU。

CPU 和 GPU 可以⽤ torch.device('cpu')torch.device('cuda')表⽰;
如果有多个 GPU,我们使⽤ torch.device(f'cuda:{i}') 来表⽰第ii 块GPU(ii 从0开始),其中 cuda:0cuda 等价。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 查看可用 GPU 的数量
torch.cuda.device_count()

# 自定义两个函数以便于device的获取
def try_gpu(i=0):

"""如果存在,则返回gpu(i),否则返回cpu()"""

if torch.cuda.device_count() >= i + 1:
return torch.device(f'cuda:{i}')
return torch.device('cpu')

def try_all_gpus():

"""返回所有可⽤的GPU,如果没有GPU,则返回[cpu(),]"""

devices = [torch.device(f'cuda:{i}') for i in range(torch.cuda.device_count())]

return devices if devices else [torch.device('cpu')]

我们可以通过 .to(device) 方法将数据安排在 GPU 下进行处理和运算。

内置网络层

待更:Models and pre-trained weights — Torchvision 0.16 documentation (pytorch.org)

预训练模型修改

待更:https://blog.csdn.net/andyL_05/article/details/108930240

Warm-Up 预热学习率

学习率是神经网络训练中最重要的超参数之一,针对学习率的优化方式很多,而 Warmup 就是其中的一种。

Warmup是在 ResNet 论文中提到的一种学习率预热的方法,它在训练开始的时候先选择使用一个较小的学习率,训练了一些 epoches 或者 steps (比如 :4epoches,10000steps) 之后再修改为预先设置的学习率来进行训练。

由于刚开始训练时,模型的权重是随机初始化的,此时若选择一个较大的学习率可能带来模型的不稳定(振荡)。如果选择Warmup的方式,可以使得开始训练的一段时间内学习率较小,模型可以慢慢趋于稳定;等模型相对稳定后再选择预先设置的学习率进行训练,这又使得模型收敛速度变得更快,模型效果更佳。

更多原理:为什么训练的时候warm up这么重要?一文理解warm up原理 - 知乎

实现策略

constant warmup

Resnet 论文中使用一个110层的 ResNet 在 cifar10 上训练时,先用 0.01 的学习率训练直到训练误差低于80%(大概训练了400个steps),然后使用 0.1 的学习率进行训练。

gradual warmup

constant warmup的不足之处在于从一个很小的学习率一下变为比较大的学习率可能会导致训练误差突然增大。
于是2018年 Facebook 提出了gradual warmup来解决这个问题,即从最初的小学习率开始,每个step增大一点点,直到达到最初设置的比较大的学习率时,再采用最初设置的学习率进行训练。
接着,随着训练的进行,我们希望再逐渐减小学习率,也就是学习率此后是衰减的,这有助于使模型收敛速度变快,效果更佳。

这里的衰减策略又可以分为:指数衰减调整(Exponential) 和 余弦退火(CosineAnnealing)。

lr_scheduler模块

PyTorch 中的学习率调整策略通过 torch.optim.lr_scheduler 模块实现。

torch.optim.lr_scheduler.LambdaLR

1
torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda, last_epoch=-1)

根据下列公式逐步更新学习率:

η(t)=λ×η(0)\eta^{(t)}=\lambda\times\eta^{(0)}

其中,η(0)\eta^{(0)} 是初始学习率。

参数:

  • optimizer (Optimizer):要更改学习率的优化器;
  • lr_lambda(function or list):提供关于 epoch的函数λ\lambda ;或者是一个 List 给出各个parameter groups 的学习率更新用到的λ\lambda
  • last_epoch (int):最后一个 epochindex,如果是训练了很多个 epoch 后中断了,继续训练,这个值就等于加载的模型的 epoch。默认为 -1 表示从头开始训练,即从epoch=1开始。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Example

optimizer_1 = torch.optim.Adam(net_1.parameters(), lr = initial_lr)
scheduler_1 = LambdaLR(optimizer_1, lr_lambda=lambda epoch: 1/(epoch+1))

print("初始化的学习率:", optimizer_1.defaults['lr'])

for epoch in range(1, 11):
# other train operation
optimizer_1.zero_grad()
optimizer_1.step()
print("第%d个epoch的学习率:%f" % (epoch, optimizer_1.param_groups[0]['lr']))
scheduler_1.step() #必须

torch.optim.lr_scheduler.StepLR

1
torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)

每过 step_sizeepoch,做一次更新:

η(t)=γepochstep_size×η(0)\eta^{(t)}=\gamma^{\lfloor{\frac{epoch}{step\_size}}\rfloor}\times\eta^{(0)}

使用方法同上,此后不再给出示例。

torch.optim.lr_scheduler.MultiStepLR

1
2
torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1)

这是一个按需调整学习率的方法。与 StepLR只能每间隔 step_sizeepoch 再变更学习率不同,该方法变换学习率的时间是可以灵活自定义调节的——通过传入 milestiones 参数(列表类型),很适合后期调试使用:观察 loss 曲线,为每个实验定制学习率调整时机。

例如,取milestiones=[10,20,50,80],若当前 epoch=34,它坐落在区间[20,50)[20,50) 内,那么因子γ\gamma 的指数就是2222milestiones[x]==20 的索引 x

事实上,上述查找 epoch 处于第几个区间的方法可以用 Python 的 bisect.bisect_right(a, x) 实现——这个函数表示利用二分查找法查找元素xx有序序列aa 中的插入位置。

故更新公式可以写为:

η(t)=γbisect_right(milestones,  epoch)×η(0)\eta^{(t)}=\gamma^{\text{bisect\_right}(milestones,\;epoch)}\times\eta^{(0)}

ExponentialLR & CosineAnnealingLR

1
2
3
torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, last_epoch=-1)

torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0, last_epoch=-1)

即 指数衰减法 和 余弦退火法。

指数衰减法相当于每隔一个 epoch 就衰减一次,等同于 StepLR 方法中取 step_size=1

余弦退火法的更新公式如下:

η(t)=ηmin+(η(0)ηmin)×(1cosepochTmaxπ)\eta^{(t)}=\eta_{\min}+(\eta^{(0)}-\eta_{\min})\times\left(1-\cos{\frac{epoch}{T_{\max}}\pi}\right)

其中,\eta_\min 即参数 eta_min 表示最小学习率,T_\max 即参数 T_max 代表每隔 T_maxepoch 走过一个cosx\cos x 函数的1/21/2 周期,即学习率下降到最小值,然后再过 T_maxepoch 上升到学习率的最大值。(可回忆余弦函数的图像)

自适应调整学习率

1
torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=10, verbose=False, threshold=0.0001, threshold_mode='rel', cooldown=0, min_lr=0, eps=1e-08)

不依赖 epoch 的学习率调整方法只有 ReduceLROnPlateau

某指标不再变化(下降或升高)时才开始调整学习率,这是非常实用的学习率调整策略。
例如,当验证集的 loss 不再下降时,进行学习率调整;或者监测验证集的 accuracy,当其不再上升时,才调整学习率。

部分参数解释:

  • mode(str):只能是 minmax,指定是看指标上升还是下降;
  • factor(float):等同于其他方法中的 gamma
  • patience(int):在指标停止优化 patienceepoch 后减小lr
  • verbose(bool):如果为 True,在更新 lrprint一个更新信息,默认为False
  • threshold_mode (str):选择判断指标是否达最优的模式,有两种模式, relabs

pytorch_warmup模块

虽然 PyTorch 官方提供了学习率各种调整的方法,但是并没有自带 Warmup 的方法。
我们可以通过 pip install -U pytorch_warmup 可安装此模块。

作者也提供了不同的预热方法,可参见仓库 README.md。下面我们给出简单的使用方法(搭配 scheduler 使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import torch
import pytorch_warmup as warmup

optimizer = torch.optim.AdamW(params, lr=0.001, betas=(0.9, 0.999), weight_decay=0.01)

num_steps = len(dataloader) * num_epochs

lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=num_steps)

warmup_scheduler = warmup.UntunedLinearWarmup(optimizer) #Here

for epoch in range(1,num_epochs+1):
for batch in dataloader:
optimizer.zero_grad()
loss = ...
loss.backward()
optimizer.step()
with warmup_scheduler.dampening(): # Here
lr_scheduler.step()

超参数优化

超参数优化完整指南 (超长文) - 知乎 (zhihu.com)

PyTorch Lightning

PyTorch Lightning (PL) 是面向专业人工智能研究人员和机器学习工程师的深度学习框架。它继承自 PyTorch,是 PyTorch 再一次经过封装后得到的、更易于快速搭建模型的库。此外,它也更易于在多卡训练时进行调试工作。

官方文档:Welcome to ⚡ PyTorch Lightning — PyTorch Lightning documentation

此外,有学者针对 From PyTorch to PyTorchLightning 做了一些更加便于 PL 新手快速迁移 PyTorch 代码和搭建项目的工作。本章也将根据该方法进行简单总结。

LightningModule

PL 中的 LightningModule 直接将 PyTorch 的下列对应的相关代码统合起来了。

  1. 初始化 Initialization (__init__() and setup())
  2. 前向传播 Forward(forward())
  3. 训练循环体 Train Loop (training_step())
  4. 验证循环体 Validation Loop (validation_step())
  5. 测试循环体 Test Loop (test_step())
  6. 预测循环体 Prediction Loop (predict_step())
  7. 优化器与学习率 Optimizers and LR Schedulers (configure_optimizers())

模型构建

LightningModule 中的 __init__(), steup(), forward() 方法都是为模型服务的。

事实上,它们的代码填写方式和 PyTorch 几乎没有区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import torch
import lightning as pl
from lightning.pytorch.demos import Transformer

class LightningTransformer(pl.LightningModule):
def __init__(self, vocab_size):
super().__init__()

# (可选) 保存训练过程中的超参数到 self.hparams 属性中
self.save_hyperparameters()
# 定义模型.
self.model = ...

def forward(self, x):
return self.model(x)

循环体

LightningModule类中实现training_step()方法:

1
2
3
4
5
6
7
8
9
def training_step(self, batch, batch_idx):
inputs, target = batch
output = self(inputs)
loss = self.loss_function(output, target) #实现定义好的损失函数

# (可选) 显示epoch下的loss情况
self.log("train_loss", loss, on_step=True, on_epoch=True, prog_bar=True, logger=True)

return loss #至少必须返回loss用于后续的反向传播

在训练时,PL会根据training_step()自动通过下面这段伪代码进行训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# enable gradient calculation
torch.set_grad_enabled(True)

for batch_idx, batch in enumerate(train_dataloader):
loss = training_step(batch, batch_idx)

# clear gradients
optimizer.zero_grad()

# backward
loss.backward()

# update parameters
optimizer.step()

Trainer

Lightning 的 Trainer 的功能远不止 “训练” 这么简单。它还可以能处理所有循环体中的其他细节,比如:

  1. 自动开启或关闭梯度
    Automatically enabling/disabling grads
  2. 驱动训练集、验证集和测试集的迭代器
    Running the training, validation and test dataloaders
  3. 在适当的时机调用可自定义的 CallBack 函数
    Calling the Callbacks at the appropriate times
  4. 将批量数据和计算结果运行在适当的 CPU/GPU/TPU 上
    Putting batches and computations on the correct devices

模型训练

最基本的使用情形下,在主函数中,可以通过 .fit() 方式开启模型训练:

1
2
3
4
model = MyLightningModule()

trainer = Trainer()
Trainer.fit(model, train_dataloader=..., val_dataloaders=None, datamodule=None)

输入第一变量一定是LightningModule对象,然后可以跟一个 LigntningDataModule 对象,或一个普通的Train DataLoader用于训练。如果定义了验证循环体,也要有用于验证的 Val DataLoader

early stopping

Tuner

Tuner — PyTorch Lightning 2.1.2 documentation

1
2
tuner = Tuner(trainer)
tuner.lr_find(model=model, train_dataloaders=...)

LightningDataModule

CheckPoint

Callback

可以通过自定义函数,在训练过程中定义好的时间触发该函数。一个简单的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

from lightning.pytorch.callbacks import Callback

class PrintCallback(Callback):
def on_train_start(self, trainer, pl_module):
print("Training is started!")
def on_train_end(self, trainer, pl_module):
print("Training is done.")

# single callback
trainer = Trainer(callbacks=PrintCallback())

# a list of callbacks
trainer = Trainer(callbacks=[PrintCallback()])

模板架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root-
|-data
|-__init__.py
|-data_interface.py
|-xxxdataset1.py
|-xxxdataset2.py
|-...
|-model
|-__init__.py
|-model_interface.py
|-xxxmodel1.py
|-xxxmodel2.py
|-...
|-main.py
|-utils.py

模板架构:

  • 主目录下只放一个main.py文件和一个用于辅助的utils.py

  • datamodle两个文件夹中放入__init__.py文件,做成包。这样方便导入。两个init文件分别是:

    • from .data_interface import DInterface
    • from .model_interface import MInterface
  • data_interface 中建立一个class DInterface(pl.LightningDataModule):用作所有数据集文件的接口。__init__()函数中import相应Dataset类,setup()进行实例化,并老老实实加入所需要的的train_dataloaderval_dataloadertest_dataloader函数。这些函数往往都是相似的,可以用几个输入args控制不同的部分。

  • 同理,在model_interface 中建立class MInterface(pl.LightningModule):类,作为模型的中间接口。__init__()函数中import相应模型类,然后老老实实加入configure_optimizerstraining_stepvalidation_step等函数,用一个接口类控制所有模型。不同部分使用输入参数控制。

  • main.py函数只负责:

    • 定义parser,添加parse项。(注意如果你的模型或数据集文件的__init__函数中有需要外部控制的变量,如一个random_arg,你可以直接在main.py的Parser中添加这样一项,如parser.add_argument('--random_arg', default='test', type=str),两个Interface类会自动传导这些参数到你的模型或数据集类中。)
    • 选好需要的callback函数们,如自动存档,Early Stop,LR Scheduler等。
    • 实例化MInterfaceDInterfaceTrainer

完事。

需要注意的是,为了实现自动加入新model和dataset而不用更改Interface,model文件夹中的模型文件名应该使用snake case命名,如rdn_fuse.py,而文件中的主类则要使用对应的驼峰命名法,如RdnFuse

数据集data文件夹也是一样。

虽然对命名提出了较紧的要求,但实际上并不会影响使用,反而让你的代码结构更加清晰。希望使用时候可以注意这点,以免无法parse。

TensorBoard 可视化训练

待更:【pytorch】使用tensorboard进行可视化训练-CSDN博客
How to use TensorBoard with PyTorch — PyTorch Tutorials 2.1.1+cu121 documentation

部分提速技巧

改动一行代码,PyTorch训练三倍提速,这些「高级技术」是关键 - 机器之心

用上Pytorch Lightning的这六招,深度学习pipeline提速10倍! - 知乎

Pytorch如何加速,让GPU利用率维持较高水平? - 知乎

参考

  1. 动手学习深度学习|D2L Discussion - Dive into Deep Learning
  2. 60分钟快速入门 PyTorch - 知乎
  3. PyTorch中的非标量反向传播-CSDN博客
  4. pytorch之warm-up预热学习策略_pytorch warmup_还能坚持的博客-CSDN博客
  5. torch.optim.lr_scheduler:调整学习率-CSDN博客
  6. Pytorch Lightning 完全攻略 - 知乎