Zexian Li

Pytorch杂记

2020-09-17 · 14 min read

高级函数

功能 函数
求对数(e为底) torch.log(input, out=None)
求对数(2为底) torch.log2(input, out=None)
求对数(10为底) torch.log10(input, out=None)
求指数(e为底) torch.exp(tensor, out=None)
求次幂 torch.pow(input, exponent, out=None)
求绝对值 torch.abs(input, out=None)
三角函数 torch.cos() & torch.sin() & torch.tan()
反三角函数 torch.acos() & torch.asin() & torch.atan()
均值 torch.mean()
求和 torch.sum()
方差 torch.var()
标准差 torch.std()

model.state_dict()、model.parameters()和model.named_parameters()的区别

parameters()只包含模块的参数,即weight和bias(包括BN的)。
named_parameters()返回包含模块名和模块的参数的列表,列表的每个元素均是包含layer name和layer param的元组。layer param就是parameters()的内容。
state_dict()返回包含模块整个状态的字典,除model.parameters()外,还包括model.buffer()如running_mean、running_var等BN相关参数。保存参数时需保存state_dict()。
值得注意的是,model.state_dict()所存储的模型参数tensor的require_grad都是False,而model.named_parameters()模型参数中的require_grad属性都是True。

不同层设置不同学习率

head_params = list(map(id, self.model.head.parameters()))
base_params = filter(lambda p: id(p) not in head_params, self.model.parameters())
params = [
    {"params": base_params, 'name':'base', "lr": 3e-4, "weight_decay": 1e-5}
    {"params": self.model.head.parameters(), 'name':'head', "lr": 3e-5,"weight_decay": 1e-5},
]
self.optimG = AdamW(params)
# change LR in train process.
for param_group in self.optimG.param_groups:
    if param_group['name'] == 'base':
        param_group['lr'] = learning_rate
    else:
        param_group['lr'] = learning_rate / 10

加载模型

全部加载

model=ResNet()
model.load_state_dict(torch.load('ckpt.pth'))

加载部分权重

model=ResNet()
model_dict = model.state_dict()
pretrained_dict = torch.load('ckpt.pth'))
pretrained_dict = {k: v for k, v in pretrained_dict.items() if k in model_dict} 
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)

锁定参数

# method1. all parameters
for p in model.parameters():
    p.requires_grad = False
optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=0.0001, betas=(0.9, 0.999), eps=1e-08, weight_decay=1e-5)

# method2. some parameters
for k, v in model.named_parameters():
    if k in frozen_list:
        v.requires_grad=False

还有一种方法,就是在网络定义中的forward方法中,将需要冻结的层放在 torch.no_grad()下。

Pytorch使用scatter_函数将label变成one hot编码

在制作数据集时,常需要将index标签转换为one hot编码形式,较好的实现方式是通过scatter_函数。
scatter_函数的核心思想是,用给定数据input,基于维度dim和索引index去填充目标矩阵(dim轴上的数值由index确定):
tensor.scatter_(dim, index, src, reduce=None)
举一个最常见的示例:

>>> x = torch.rand(2, 5)
>>> x
 0.4319  0.6500  0.4080  0.8760  0.2355
 0.2609  0.4711  0.8486  0.8573  0.1029
[torch.FloatTensor of size 2x5]
>>> torch.zeros(3, 5).scatter_(0, torch.LongTensor([[0, 1, 2, 0, 0], [2, 0, 0, 1, 2]]), x)
 0.4319  0.4711  0.8486  0.8760  0.2355
 0.0000  0.6500  0.0000  0.8573  0.0000
 0.2609  0.0000  0.4080  0.0000  0.1029

使用2x5的矩阵去填充3x5的目标矩阵,当dim=0时,使用index填充0维度,则可将index转换成
[[(0,0),(1,1),(2,2),(0,3),(0,4)], [(2,0),(0,1),(0,2),(1,3),(2,4)]],再将源矩阵中的对应值填充到新矩阵index'的位置。
举例,将N*1的label转换成one hot编码格式,假设N=10,label类别共3类(以1,2,3标记):

N = 10
label = torch.randint(1, 4, (N, 1))
one_hot_label = (label - 1).long()
one_hot_label = torch.zeros(len(label), N).scatter_(1, one_hot_label, 1)

DataLoader中collate_fn的使用

Pytorch的DataLoader可选collate_fn参数,从而对每次读取的batch数据进行处理。
当batch选项关闭时,collate_fn函数会在读取每个独立的数据时均被调用,且默认的功能只是将Numpy数组转换成Pytorch tensor;
当batch选项开启时,collate_fn函数会在读取整个batch的数据时被调用,即将独立的数据组合至batch级别,且此过程中默认保留原有数据结构、将Numpy数组转换成Pytorch tensor。举例,设置torch.utils.data.Dataset在__getitem__方法中基于index读取一张图片和一个label,默认的collate_fn会使torch.utils.data.DataLoader返回一个包含batch_size个元组的列表,每个元组内含有一张图片和一个label,类似的列表会由迭代器不断返回,共len(data)/batch_size个(暂不考虑取整),以代码的形式可视化,返回值为[(image, label) for _ in range(batch_size)]
有特殊需求时才会改写collate_fn函数,例如在非第一维度外的维度进行concate、对不同长度的序列进行padding等。

nn.modules和nn.children的区别

nn.children()会返回模型的子元素,可用如list(model.children())方法查看(等价于用model.named_children()迭代器进行遍历),其中子元素既可能是单独的一层如Linear,也可能是Sequential;
nn.modules()会递归地返回所有的子元素,可用如list(model.modules())方法查看(等价于用model.named_modules()迭代器进行遍历),其中Sequential内包含的子元素也会被迭代地返回。

Pytorch内存管理

查看tensor内存状态:tensor.storage()
查看tensor间是否共享内存:id(tensor1.storage()) == id(tensor2.storage())
查看tensor元首素相对于storage地址的偏移量:tensor.storage_offset()
查看tensor内存空间是否连续:tensor.is_contiguous()
查看tensor内部的步长信息:tensor.stride()
Pytorch中的tensor分为头信息区(tensor)和存储区(storage),头信息区主要保存tensor的形状、步长、数据类型等信息,真正的数据则以一维连续数组的形式保存在存储区。事实上,绝大多数操作并不修改tensor的实际数据(即存储区的内容),而是修改tensor的头信息,这样操作既节约内存,处理速度也更快,例如使用索引截取部分tensor后新tensor的内存指向不会发生变化,只会改变对应的地址偏移量,而使用tensor.contiguous()方法会将数据复制到新的内存中,不再共享原有地址。

常用张量操作

torch.cat(tensors, dim=0, out=None)可对数据在指定维度进行拼接,新数据在该维度上通道数是原数据通道数之和,且此过程要求原数据在非指定维度的其他维度具有相同的shape。

import torch

a = torch.ones(2,3)
b = torch.ones(4,3) * 2
c = torch.ones(1, 3) * 3
print(torch.cat((a, b), 0)) # shape: 7 * 3

c = torch.ones(2, 4, 3)
d = torch.ones(2, 4, 2) * 2
print(torch.cat((c, d), 2)) # shape: 2 * 4 * 5

torch.stack(tensors, dim=0, out=None)是将多个矩阵在新的维度上进行堆叠(会增加新的维度),一般要求被堆叠矩阵的shape保持相同。其中dim参数的取值范围为0至原有通道数的左闭右闭集合。

import torch

a = torch.ones(2,3)
b = torch.ones(2,3) * 2
c = torch.ones(2,3) * 3
print(torch.stack((a, b, c), dim=2))

torch.transpose(input, dim0, dim1)返回将input维度dim0和dim1调换后的新tensor,能且仅能调换两个维度。该操作仅改变了数据的读取方式,并不改变内存存储,且output与input共享内存。既可以以torch.transpose形式调用,也可以以torch.Tensor.transpose形式调用。

import torch

a = torch.randn(2,3,4)
b = torch.transpose(a, 2, 1)
print(b.shape)  # b.shape: 2 * 4 * 3

b[0, :, :] = 0
print(a, b)     # 'a' will also change its data.

torch.Tensor.permute(*dims)返回将input维度重新排列后的新tensor,必须传入所有维度的索引且能进行多个维度的调换。仅可以以torch.Tensor.permute形式调用。

import torch

a = torch.randn(2, 3, 5)
b = a.permute(2, 0, 1)
print(b, b.shape)   # b.shape: 5 * 2 * 3

torch.tensor和torch.Tensor()的区别

torch.tensor()是python函数,会对数据部分做拷贝(而非引用),根据原始数据类型生成对应torch.LongTensor、torch.FloatTensor和torch.DoubleTensor等。
torch.Tensor()是python类,也是torch.FloatTensor()的简称,使用此会生成FloatTensor。
torch.tensor()内的输入即value,torch.Tensor()内的输入即shape,参考下例:

a = torch.tensor(2)     # tensor(2)
b = torch.tensor([2])   # tensor([2])
c = torch.Tensor(2)     # tensor([0., 0.])
d = torch.Tensor([2])   # tensor([2.])

其实这两者的关系很像numpy中的numpy.array和numpy.ndarray,前者是函数,后者是类对象,可通过前者创建后者。

model.eval()涉及的BN及Dropout内容

如果模型中存在BN(Batch Normalization)和Dropout,需要在训练时添加model.train(),测试时添加model.eval()。
训练时,BN层采用当前batch数据的均值和方差,进行Dropout以更新部网络分参数;
测试时,BN层采用全部训练数据的均值和方差,不进行Dropout,使用所有网络参数。

多GPU的处理机制

DataParallel:Pytorch下多GPU的处理机制为:
(1)在各个GPU上初始化模型;
(2)前向传播时,把batch数据分配到各个GPU上进行计算;
(3)将输出结果在主GPU上汇总,计算loss并反向传播,更新主GPU上模型的权值;
(4)将主GPU上的模型复制到其它GPU上。
DistributedDataParallel是进程间的通信,不会把某一GPU拉得过高。

torch.nn.Parameter()

torch.nn.Parameter()可以将一个不可训练的Tensor转换成可训练的Parameter(此时requires_grad默认为True),并将该Parameter绑定到module中,变成模型参数(model.parameters())的一部分,从而可以进行参数优化。但注意,Parameter也是tensor,符合一切tensor特性。
torch.tensor(a, requires_grad=True)则不尽相同,其只能将参数变成可训练的,但并未将其绑定到module的Parameters列表中。
具体例程如下所示:

import math
import numpy as np

import torch
import torch.nn as nn
from torch.optim import Adam

class NN_Network(nn.Module):
    def __init__(self,in_dim,hid,out_dim):
        super(NN_Network, self).__init__()
        self.linear1 = nn.Linear(in_dim,hid)
        self.linear2 = nn.Linear(hid,out_dim)
        self.linear1.weight = torch.nn.Parameter(torch.zeros(in_dim,hid))
        self.linear1.bias = torch.nn.Parameter(torch.ones(hid))
        self.linear2.weight = torch.nn.Parameter(torch.zeros(hid,out_dim))
        self.linear2.bias = torch.nn.Parameter(torch.ones(out_dim))
        self.test1 = torch.nn.Parameter(torch.zeros(10))
        self.test2 = torch.tensor([0. for i in range(10)], requires_grad=True)

    def forward(self, input_array):
        h = self.linear1(input_array)
        y_pred = self.linear2(h)
        return y_pred

in_d = 5
hidn = 2
out_d = 3
net = NN_Network(in_d, hidn, out_d)

for param in net.parameters():
    print(type(param.data), param.size())
# print(list(net.parameters()))
# ------------------------------------------
# <class 'torch.Tensor'> torch.Size([10])
# <class 'torch.Tensor'> torch.Size([5, 2])
# <class 'torch.Tensor'> torch.Size([2])
# <class 'torch.Tensor'> torch.Size([2, 3])
# <class 'torch.Tensor'> torch.Size([3])

Pytorch Conv1d

详细文档参考官方文档
在分析之前先回回顾Conv2d。在Conv2d中,当stride=2,kernel=2时,输入batch_size*C_in*100*100的图片,输出特征大小为batch_size*C_out*50*50,在长宽两个尺度上均有尺度的缩小。
而一维卷积不尽相同,只在特征尺度进行卷积。若输入特征大小为batch_size*C_in*length_in,则输出特征大小为batch_size*C_out*length_out,在此过程中仅对length这单一尺度进行stride、kernel_size和padding等方面的计算。
举例如下:

m = nn.Conv1d(in_channels=16,out_channels=33, kernel_size=3, stride=2)
inputs = torch.randn(20, 16, 50)
outputs = m(inputs)
# outputs.shape: torch.Size([20, 33, 24])

通过mask将不同样本传给不同的损失函数

如果不同样本需要传给不同的损失函数(例如batch内包含正负样本,希望正样本通过loss1和loss2,负样本通过loss2),不推荐在model的__forward__函数或loss函数中使用if判断样本状态(TF的静态图肯定无法支持,不确定Pytorch的动态图是否支持,就算支持定点反向传播,就算避免了可微上的问题,实际的运算速度也会很慢),建议直接使用1-0 mask对样本进行遮盖,mask可通过数值一致实现零梯度,避免了对应样本的反向传播,也可以蹭上框架中矩阵优化的东风。
加mask方式如下:

output = model(input)
mask = np.ones_like(gt_label)
mask[negative_sample] = 0
# mask.shape == output.shape == gt_label.shape
loss = loss_func(output * mask, gt_label * mask)

优化器相关

Pytorch中optim模块的优化器:

  1. 可使用weight_decay参数设置L2 loss。
  2. 可在模型参数位置传入列表以对多个模型的参数进行优化。
    optimizer = optim.Adam([model_coarse.parameters(), model_fine.parameters()], lr=opt.learning_rate, weight_decay=opt.weight_decay_l2)
Bad decisions make good stories.