Pytorch 学习笔记

文章持续更新中
本篇用于记录深度学习框架 Pytorch 使用过程中遇到的问题,或分享记录一些算法实现过程中重复性的代码模板。

1 Pytorch 使用碰过的 bug

  • tensor 运算一定要在同一个设备中
  • nn.utils.clip_grad_norm 已弃用替换为 torch.nn.utils.clip_grad_norm_
  • windows 下 dataloader 不支持多线程读取数据
  • 注意tensornumpy的转换,在GPU上的 tensor 先转到CPU再转为 numpy

2 模型相关

注意
以下代码默认已经实例化一个名为 model 的网络模型。

2.1 model.train()

当训练和测试同一个model时,注意不要忘记在训练时将模型转换为训练模式, 执行该语句模型会转变为训练模式,从而开启模型的 DropoutBatchnormal 层,最好写在训练循环中。 源码中实际上是将 model 的每个子 module.train 都置为 True

2.2 model.eval()

测试模式,关闭 DropoutBatchnormal 层。 使用是需要在模型测试模块代码中加入 with torch.no_grad(): 来停止自动求梯度是为了加快GPU运算速度。

2.3 model.apply()

用于初始化模型参数,传入初始化函数,例如:

1
2
3
4
5
6
7
8
9
# Xavier初始化参数
def weight_init(m):
    if isinstance(m, nn.Linear):
        nn.init.xavier_normal_(m.weight)
        nn.init.constant_(m.bias, 0)


# 模型实例化后进行apply
model.apply(weight_init)

torch.nn.init 中包含多个初始化函数,如

  • .uniform_ 均匀分布
  • .normal_ 正态分布
  • .constant_ 常数
  • .xavier_uniform_ Xavier 初始化
  • .kaiming_uniform_ Kaiming 初始化

2.4 nn.utils.clip_grad_norm_()

训练时对模型参数进行梯度裁剪,防止梯度过大或者梯度消失,三个参数(模型参数, 梯度最大范围, 范数类型) 注意该方法只在训练时使用,并且需要在loss.backward()之后和optimizer.step()之间调用。例如:

1
grad_norm = nn.utils.clip_grad_norm_(model.paremeters(), max_norm=10)

2.5 引入与训练模型

例如引入resnet18 网络模型。

1
2
3
4
5
model = torchvision.models.resnet18(pretrained=True).to(device)
num_ftrs = model.fc.in_features
model.device = device
for param in model.parameters():
    param.requires_grad = True

3 优化器 optimizer 相关

优化其中需要传入模型的参数,优化器中保存的是模型参数的地址。

3.1 optimizer.zero_grad()

Pytorch 中的梯度计算不自动清零,在每次训练循环中要注意清空优化器中的梯度。 通常我们训练时都会使用 mini-batch 。每次迭代 dataloader 数据取出一个 batch 的 x 和 y , 因此如果在迭代 dataloader 时不将梯度清零,则 optimizer 中会累加上一个 batch 的梯度。

3.2 optimizer.step()

该方法用于参数更新,优化器会遍历整个参数列表,根据优化器种类更新每个参数。 注意 step 要在loss.backward()之后使用。 具体可查看如下 pytorch 源码,SGD 算法中更新步骤的源码实现,其中计算了动量,权重衰减等参数。

 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
def step(self, closure=None):
    """Performs a single optimization step.

    Args:
        closure (callable, optional): A closure that reevaluates the model
            and returns the loss.
    """
    loss = None
    if closure is not None:
        with torch.enable_grad():
            loss = closure()

    for group in self.param_groups:
        params_with_grad = []
        d_p_list = []
        momentum_buffer_list = []
        weight_decay = group['weight_decay']
        momentum = group['momentum']
        dampening = group['dampening']
        nesterov = group['nesterov']
        lr = group['lr']

        for p in group['params']:
            if p.grad is not None:
                params_with_grad.append(p)
                d_p_list.append(p.grad)

                state = self.state[p]
                if 'momentum_buffer' not in state:
                    momentum_buffer_list.append(None)
                else:
                    momentum_buffer_list.append(state['momentum_buffer'])

        F.sgd(params_with_grad,
              d_p_list,
              momentum_buffer_list,
              weight_decay=weight_decay,
              momentum=momentum,
              lr=lr,
              dampening=dampening,
              nesterov=nesterov)

3.3 动态学习率 torch.optim.lr_scheduler

pytorch 提供了动态改变学习率的方法,封装在 torch.optim.lr_scheduler 之中,我们也可以自定义 lambda 函数来自定义学习率的变化。
scheduler 更新需要在每个 batch 遍历完进行,也就是放到 epoch 的循环内。 栗子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
optimizer = SGD(model, 0.1)
scheduler1 = ExponentialLR(optimizer, gamma=0.9)
scheduler2 = MultiStepLR(optimizer, milestones=[30,80], gamma=0.1)

for epoch in range(20):
    for input, target in dataset:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)
        loss.backward()
        optimizer.step()
    scheduler1.step()
    scheduler2.step()

4 损失函数

这里用 MSELoss 举例: $$ \ell(x, y) = L = {l_1,\dots,l_N}^\top, \quad l_n = \left( x_n - y_n \right)^2 $$
$$ \ell(x, y) = \begin{cases} \operatorname{mean}(L), & \text{if reduction} = \text{‘mean’;} \\ \operatorname{sum}(L), & \text{if reduction} = \text{‘sum’.} \end{cases} $$ 如果我们设置了 MESLoss 中参数 reduction,默认情况下会自动除 batch 的大小。如果我们想要保存整个 batch 的 loss 求和,则将 reduction 改为 sum。

1
2
3
nn.MSELoss(reduction='mean')

nn.MSELoss(reduction='sum')

5 激活函数

激活函数继承自 Module 。

Softmax

$$ \text{Softmax}(x_{i}) = \frac{\exp(x_i)}{\sum_j \exp(x_j)} $$ 传入参数 dim ,之后会沿着该维度进行切片进行计算,使该维度的 tensor 和等于1,一般传入dim为-1。
一个直观的栗子:

 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
input = torch.randn((3, 4, 5, 6))
sum0 = torch.sum(input, dim=3)
print(sum0)
sm = nn.Softmax(dim=3)
output = sm(input)
sum = torch.sum(output, dim=3)
print(sum)

========输出===========
tensor([[[ 2.8838, -2.9647,  0.0305, -2.3780, -1.4375],
         [-0.6128,  0.2459, -5.2516, -1.2583, -1.8970],
         [-1.0280, -0.9360, -0.7921, -0.0837,  2.1275],
         [ 1.3429, -0.1291,  0.6525,  2.4980, -0.0516]],

        [[ 0.6494,  2.1635, -3.3102, -4.5285, -0.9680],
         [-1.8440,  1.4655,  4.3897, -2.6680,  4.8173],
         [ 2.2430, -3.5258, -0.0485,  0.0939,  1.1195],
         [ 0.2597, -1.4783, -3.3671,  4.4973,  1.5946]],

        [[ 0.2379, -1.7152,  3.8646,  1.6057,  2.2867],
         [ 4.6987,  2.9509,  1.0505, -2.3340, -0.2437],
         [-0.8529,  3.8930,  3.2201,  1.3334,  0.0092],
         [-2.1052, -2.3994, -0.4358,  0.7527,  4.1492]]])
tensor([[[1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000]],

        [[1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000]],

        [[1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000],
         [1.0000, 1.0000, 1.0000, 1.0000, 1.0000]]])

6 tensor

  • tensor 和 np 数组可以共享一块内存
  • 可用用列表[]和元祖()创建,但不可以用set{} 张量操作文档

6.1 tensor 中的索引操作(gather,scatter)

tensor.scatter_()举例: Tensor.scatter_(dim, index, src, reduce=None) Writes all values from the tensor src into self at the indices specified in the index tensor. For each value in src, its output index is specified by its index in src for dimension != dim and by the corresponding value in index for dimension = dim.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 官方文档的三维tensor操作
self[index[i][j][k]][j][k] = src[i][j][k]  # if dim == 0
self[i][index[i][j][k]][k] = src[i][j][k]  # if dim == 1
self[i][j][index[i][j][k]] = src[i][j][k]  # if dim == 2

# 用一个二维的例子

self[index[i][j]][j] = src[i][j]  # if dim == 0
self[i][index[i][j]]= src[i][j]  # if dim == 1

src = tensor(
[[ 0.3992,  0.2908,  0.9044,  0.4850,  0.6004],
[ 0.5735,  0.9006,  0.6797,  0.4152,  0.1732]])
    
idx = torch.tensor([[0, 1, 2, 0, 0], [2, 0, 0, 1, 2]])

self[idx[0][0]][0] = src[0][0] = 0.3992
self[idx[0][1]][1] = self[1][1] = src[0][1] = 0.2908
......

self = tensor(
       [[ 0.3992,  0.9006,  0.6797,  0.4850,  0.6004],
        [ 0.0000,  0.2908,  0.0000,  0.4152,  0.0000],
        [ 0.5735,  0.0000,  0.9044,  0.0000,  0.1732]])

6.2 tensor.detach()

用于具有梯度信息的 tensor,将 tensor 从计算图中分离,即将梯度的转播断开。 detach 会返回一个新 tensor ,共享同一块数据内存,但其 requires_grad=False。 例如:xxx.detach().cpu().numpy()

6.3 detach_()

不创建新的 tensor,直接在原来的 tensor 上修改。

6.4 tensor.item()

返回一个 python 支持的数字类型数据,只能返回标量,不能返回向量。该方法不需要考虑 tensor 是否在 CPU 或 GPU ,一般用于将 loss 转换为 numpy。

6.5 torch.max()

该 API 常用语取得预测最大值的索引,即第二个返回值。
torch.max() 的输入:

  • input tensor类型数据
  • dim 维度(0 表示每一列的最大值, 1 表示每一行求最大值)

返回值:

  • 函数会返回两个tensor,第一个tensor是每行的最大值;第二个tensor是每行最大值的索引。

6.6 维度转换 permute()

栗子:

1
2
3
4
5
6
7
import torch
x = torch.randn(2, 3, 5)
print(x.size())
print(x.permute(2, 0, 1).size())

>>>torch.Size([2, 3, 5])
>>>torch.Size([5, 2, 3])

permute()和view()区别

7 数据相关

7.1 Dataset

dataset 处理一个样本,返回一个样本的特征与标签,之后交给 dataloard 进行批处理。 自定义 Dataset 需要继承 Dataset 抽象类,实现 __init____len____getitem__

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def __init__(self, x, y=None, transform=None)
    一般传入
        数据集所在的目录或者数据集本身
        数据变换transform 预处理等
        target_transform标签预处理
        是否需要返回标签等
def __len__(self)
    返回数据总长度
def __getitem__(self, idx):
    根据指定的索引返回训练数据与标签返回字典或元祖形式括号可以省略)。
    其中也可进行数据变换增强等操作.

7.2 Dataloader

相当于 Dataset 的迭代器,他告诉模型用怎样的方式取去 dataset 的数据,其本身是一个可迭代对象。
常用参数:

  • dataset:实例化后的dataset对象
  • batch_size:批处理大小
  • shuffle:是否打乱。遍历完所有的batch后将整个dataloader打乱
  • sampler :数据采样方式,自定义的方式取样本,不可以指定shuffle(比如讲长度相似的样本取为一组)
  • num_workers :是否用多线程读取数据
  • collate_fn :对该批次的样本进行处理如pad
  • pin_memory :将tensor保存在GPU中
  • drop_last :如果batch_size不是数据集大小的整数倍,设置true则丢弃最后一批的数据
注意
注:pin_memory,通常情况下,数据在内存中要么以锁页的方式存在,要么保存在虚拟内存(磁盘)中,设置为 True 后,数据直接保存在锁页内存中,后续直接传入 cuda;否则需要先从虚拟内存中传入锁页内存中,再传入cuda,这样就比较耗时了,但是对于内存的大小要求比较高。 官方文档 一个十分清晰的源码解读教程

7.2.1 迭代 Dataloader

返回的数据形式与 dataset 里的 getitem 返回方式有关。

  • next 迭代返回一个 batch 数据
1
next(iter(dataloader))
  • enumerate 循环迭代
1
for idx, data in enumerate(dataloader)

7.3 len(dataloader) 返回值是什么?

1
len(dataloader) = dataset_size / batch_size

他返回了有多少个批次。
我们训练时一般是一个批次求一个loss,最后将所有loss相加,所以需要除批次的总数得到这次训练的loss。

7.3 torchvision.datasets.DatasetFolder

用于读图片类型分类别的数据,如每个类别的图片放在一个文件夹内。
如下文件结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
directory/
├── class_x
│   ├── xxx.ext
│   ├── xxy.ext
│   └── ...
│       └── xxz.ext
└── class_y
    ├── 123.ext
    ├── nsdf3.ext
    └── ...
    └── asd932_.ext

datafolder 中实现了 find_classes 方法,该方法会自动扫描文件目录,并将每个图片的路径和类别返回。
一个栗子:

1
2
3
4
5
data = DatasetFolder(path, loader=lambda x: Image.open(x), extensions="jpg", transform=tfm)

或者

data = DatasetFolder(path, loader=imgs_tfm, extensions="jpg", transform=data_tfm['test'])

其中 lambda 函数被 datafolder 设置为 loader 参数,并在 __getitem__ 的时候为每个样本数据执行一次 lambda 函数。如果有多个操作也可以传入一个函数地址。
源码如下:
其中 lodaer 即传入的 lambda 函数,在 getitem 中会调用传入的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def __getitem__(self, index: int) -> Tuple[Any, Any]:
    """
    Args:
        index (int): Index

    Returns:
        tuple: (sample, target) where target is class_index of the target class.
    """
    path, target = self.samples[index]
    sample = self.loader(path)
    if self.transform is not None:
        sample = self.transform(sample)
    if self.target_transform is not None:
        target = self.target_transform(target)

    return sample, target

def __len__(self) -> int:
    return len(self.samples)

7.4 pad_sequence() 数据做padding

该模块位于 from torch.nn.utils.rnn import pad_sequence ,主要用于对不等长的特征做 padding。
参数:

  • targets: list 矩阵,shape=[batch_size, N] ,N 与特征维度有关
  • batch_first:默认 batch_size 在第一维度
  • padding_value:填充的值

返回值:

  • [batch_size, M] M 为 batch 中的最大长度

使用案例:

1
2
3
4
5
6
7
8
def collate_batch(batch):
  # Process features within a batch.
  """Collate a batch of data."""
  mel, speaker = zip(*batch)
  # Because we train the model batch by batch, we need to pad the features in the same batch to make their lengths the same.
  mel = pad_sequence(mel, batch_first=True, padding_value=-20)    # pad log 10^(-20) which is very small value.
  # mel: (batch size, length, 40)
  return mel, torch.FloatTensor(speaker).long()

7.5 划分数据集 random_split()

该模块位于 torch.utils.data.random_split() 是 pytorch 提供的划分数据集函数。
参数:

  • dataset (Dataset) – 要划分的数据集。
  • lengths (sequence) – 要划分的长度。(直接给出想要分出两个数据集的长度即可)
  • generator (Generator) – 用于随机排列的生成器。

栗子:

1
2
3
trainlen = int(0.9 * len(dataset))
lengths = [trainlen, len(dataset) - trainlen]
trainset, validset = random_split(dataset, lengths)

8 代码模板

8.1 保存checkpoint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if epoch % 5 == 0:
    checkpoit = {
        'model': model.state_dict(),
        'optimizer': optimizer.state_dict(),
        'epoch': epoch,
        'best_acc': best_acc,
        'tr_loss_record': tr_loss_record,
        'tr_acc_record': tr_acc_record,
        'val_loss_record': val_loss_record,
        'val_acc_record': val_acc_record
    }
    torch.save(checkpoit, ckpt_path)

8.2 加载 checkpoint

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 恢复训练
if config['remuse']:
    print("恢复训练")
    checkpoit = torch.load(ckpt_path)
    start_epoch = checkpoit['epoch']
    model.load_state_dict(checkpoit['model'])
    optimizer.load_state_dict(checkpoit['optimizer'])
    best_acc = checkpoit['bset_acc']
    tr_loss_record = checkpoit['tr_loss_record']
    tr_acc_record = checkpoit['tr_acc_record']
    val_loss_record = checkpoit['val_loss_record']
    val_acc_record = checkpoit['val_acc_record']

参考

0%