0%

Pytorch

Snippets for Pytorch

新手码代码时不会之处,资料源于各个博客,参考链接位于最下方

数据集相关

torch.utils.data.DataLoader

  • DataLoader是一种可迭代对象,使用iter()访问,不能使用next()访问,通过iter()构建迭代器,然后可以使用next()访问
  • 和其他迭代对象一样,DataLoader也可以用for访问,并且一般用for X, y in DataLoader:进行可迭代对象的访问。
  • DataLoader本质上是一个iterable,并利用多进程来加速batch data的处理,使用yield来使用有限的内存。
  • DataLoader是一个高效,简洁,只管的网络输入数据结构,便于使用和扩展。

pytorch数据加载流程

  1. 创建一个Dataset对象
  2. 创建一个DataLoader对象
  3. 循环这个DataLoader对象,将 img,label加载到模型中训练
    伪代码如下(不严格遵守pytorch语法)
1
2
3
4
5
6
dataset = MyDataset(transform=trans)
dataloader = pytorch.utils.data.DataLoader(dataset)
num_epochs = get('n_epochs')
for epoch in range(num_epochs):
for X, y in dataloader:
# Load Data in Models

DataLoader详解

DataLoader的作用:将自定义的Datset根据batchsize的大小,是否shuffle等封装成一个batchsize大小的tensor,用于后续的训练。
DataLoader(object)的参数:

  • dataset(Dataset): 传入的数据集
  • batch_size(int, optional): 每个batch有多少个样本
  • shuffle(bool, optional): 在每个epoch开始的时候,对数据进行重新排序
  • sampler(Sampler, optional): 自定义从数据集中取样本的策略,如果指定这个参数,那么shuffle必须为False
  • batch_sampler(Sampler, optional): 与sampler类似,但是一次只返回一个batch的indices(索引),需要注意的是,一旦指定了这个参数,那么batch_size,shuffle,sampler,drop_last就不能再制定了(互斥——Mutually exclusive)
  • num_workers (int, optional): 这个参数决定了有几个进程来处理data loading。0意味着所有的数据都会被load进主进程。(默认为0)
  • collate_fn (callable, optional): 将一个list的sample组成一个mini-batch的函数
  • pin_memory (bool, optional): 如果设置为True,那么data loader将会在返回它们之前,将tensors拷贝到CUDA中的固定内存(CUDA pinned memory)中.
  • drop_last (bool, optional): 如果设置为True:这个是对最后的未完成的batch来说的,比如你的batch_size设置为64,而一个epoch只有100个样本,那么训练的时候后面的36个就被扔掉了;如果为False(默认),那么会继续正常执行,只是最后的batch_size会小一点。
  • timeout(numeric, optional): 如果是正数,表明等待从worker进程中收集一个batch等待的时间,若超出设定的时间还没有收集到,那就不收集这个内容了。这个numeric应总是大于等于0。默认为0
  • worker_init_fn (callable, optional): 每个worker初始化函数 If not None, this will be called on each worker subprocess with the worker id (an int in [0, num_workers - 1]) as input, after seeding and before data loading. (default: None)

一般重要的参数有:dataset, batch_size, shuffle, num_workers, drop_last

DataLoader参考

自定义数据集

  • 自定义的数据集必须继承自torch.utils.data.Datasettorch.utils.data.Dataset这个类是一个表示数据集的抽象类,负责处理索引(index)到样本(sample)映射的一个类(class)。Pytorch提供两种数据集: Map式数据集Iterable式数据集。这里我们只介绍前者
  • 自定义的数据集中必须包含以下两种方法:__getitem____len__。其中__getitem__用于获取索引对应的样本,__len__方法用于获取样本长度。
  • 接下来提供构建的范式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyDataset(torch.utils.data.Dataset):

def __init__(self, file_path, transform, unaligned):
#TODO: 加载各项参数,包括数据集路径,变换,是否通过index检索
self.file_path = file_path
self.transform = transform
self.unaligned = unaligned

def __getitem__(self, index):
item = self.file_path[index]
item = self.transform(item)
return item
def __len__(self):
return len(self.file_path)

训练过程相关

初始化:Model.apply(Initial)

为什么要初始化?

  1. 加快梯度下降的收敛速度
  2. 更有可能获得一个低模型误差,或者低泛化误差的模型
  3. 降低因为未初始化或初始化不当导致的梯度消失或者梯度爆炸问题。此情况会导致模型训练速度变慢,崩溃,甚至失败(如全连接层中,如果$W_{ij}$的每一个参数都很大,用Sigmoid作为激活函数,则会导致梯度消失)
  4. 随机初始化,可以打破对成性,从而保证不同的隐藏单元可以学习到不同的东西。

如何初始化

  1. 预训练初始化
  2. 全0初始化(线性回归,Logistics回归)
  3. 固定值初始化
    • BN层,$\gamma$为1,$\beta$为0
    • Bias通常用0初始化,对于ReLU激活的神经元,通常采用0.01偏置
  4. 固定方差初始化
    • 高斯分布
    • 均匀分布
  5. 方差放缩初始化
    • Xavier初始化(ReLU较差)
    • He初始化

代码

  • weights_init_normal函数
1
2
3
4
5
6
7
8
9
10
11
def weights_init_normal(m):

classname = m.__class__.__name__

if classname.find('Conv') != -1:
torch.nn.init.kaiming_normal_(m.weight.data, a=0.01, mode='fan_in', nonlinearity='leaky_relu')
#print(m.__class__.__name__, m.weight, m.bias)
elif classname.find('BatchNorm2d') != -1:
torch.nn.init.constant(m.weight.data, 1.0)
torch.nn.init.constant(m.bias.data, 0.0)
#print(m.__class__.__name__,m.weight, m.bias)
  • 训练初始化
1
model.apply(weights_init_normal)

模型验证相关

一些基本概念

  • 在正向传播时,需要求导的变量除了执行forward()之外,还会同时为反向传播做一些准备
  • 叶子节点:当一个tensor是用户创建时(即用torch.tensor()等函数定义的,而非通过计算获得的),他是一个叶子节点,当tensor时由其他运算操作产生时,不是叶子节点
  • backward()之后只有叶子节点相关的导数结果会被保存,而非叶子节点的导数结果会被抛弃(为了减小内存消耗)

中间层检验:hook

在对训练结果进行评判时,我们往往需要对网络的中间结果进行评估从而判断网络的注意力和权重分配等效果,通常有卷积核、特征图、梯度等。卷积核较后二者容易获得,但是特征图一经传递完成就会释放内存从而防止内存冗余,梯度方面,只保存节点梯度。在模型中增加中间结果保存操作可以实现中间结果的,但是会导致速度变慢、操作复杂、训练中产生大量数据等问题。所以需要用其他方法,而hook则是pytorch提供的一个较好的方法。包括以下函数:Tensor.register_hook(hook_fn),nn.Module.register_forward_hook(hook_fn),nn.Module.register_backward_hook(hook_fn)

Tensor.register_hook(hook_fn)

功能:注册一个反向传播的hook函数,用于自动记录Tensor的梯度

1
2
3
4
5
6
7
a = torch.Tensor([1,2]).require_grad_()
b = torch.Tensor([1,2]).require_grad_()
d = torch.Tensor([1,2]).require_grad_()
c = a + b
e = c * d
o = e.sum()
o.backward()

以上代码中,只有a,b,d为叶子节点,因此只有他们的导数被保留,其他导数,如c,d,e,o无导数,o.grad返回为None。下面通过register_hook方法给cdeo等非叶子节点的张量保留导数。

1
2
3
4
def hook_fn(grad):
print(grad)
e.register_hook(hook_fn)
o.backward()

在对o.backward()时,自动执行张量e所关联的hook_fn,从而直接将e的导数打印出来

总结:

  1. 在反向传播前,定义一个hook函数,描述对梯度的操作,函数名自拟(一般为hook_fn(grad)),参数只有grad,表示梯度;
  2. 对要获取梯度的张量进行Tensor.register_hook(hook_fn)注册
  3. 执行反向传播,执行的过程中自动调用hook_fn函数

nn.Module.register_forward_hook(hook_fn)&nn.Module.register_backward_hook(hook_fn)

这两个操作对象都是nn.Module类,如神经网络中的卷积层nn.Conv2d,全连接层nn.Linear等。

对于模型的中间模块,也可以视作中间节点(非叶子节点),其输出为特征图激活值,若要获取,则可以用hook功能

  • register_forward_hook是前向传播的输出的,即特征图或激活值,而register_backward_hook是反向传播输出的,即梯度值

register_forward_hook(hook_fn)

对于hook_fn的定义如下

1
2
def forward_hook(module, input, output):
operations

module指的是模块,input指的是输入模块的内容,output指的是输出模块的内容。

register_backward_hook(hook_fn)

对于hook_fn的定义如下

1
2
def backward_hook(module, grad_in, grad_out):
operations

值得注意的是这里的grad_ingrad_out,这里的in和out都是相对于forward传递而言的。如线性模块$o=W\times x+b$,grad_out指的就是o的梯度;而grad_in因为有三个变量,因此他是包含三个元素的tuple

例子

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import torch
import torch.nn as nn
import numpy as np
import torchvision.transforms as transforms


class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(3,6,3,1,1)
self.relu1 = nn.ReLU()
self.pool1 = nn.MaxPool2d(2,2)
self.conv2 = nn.Conv2d(6,9,3,1,1)
self.relu2 = nn.ReLU()
self.pool2 = nn.MaxPool2d(2,2)
self.fc1 = nn.Linear(8*8*9, 120)
self.relu3 = nn.ReLU()
self.fc2 = nn.Linear(120,10)

def forward(self, x):
out = self.pool1(self.relu1(self.conv1(x)))
out = self.pool2(self.relu2(self.conv2(out)))
out = out.view(out.shape[0], -1)
out = self.relu3(self.fc1(out))
out = self.fc2(out)

return out


def backward_hook(module, grad_in, grad_out):
grad_block['grad_in'] = grad_in
grad_block['grad_out'] = grad_out


def farward_hook(module, inp, outp):
fmap_block['input'] = inp
fmap_block['output'] = outp


loss_func = nn.CrossEntropyLoss()

# 生成一个假标签以便演示
label = torch.empty(1, dtype=torch.long).random_(3)

# 生成一副假图像以便演示
input_img = torch.randn(1,3,32,32).requires_grad_()

fmap_block = dict() # 装feature map
grad_block = dict() # 装梯度

net = Net()

# 注册hook
net.conv2.register_forward_hook(farward_hook)
net.conv2.register_backward_hook(backward_hook)

outs = net(input_img)
loss = loss_func(outs, label)
loss.backward()

print('End.')

参考网站:PyTorch之HOOK——获取神经网络特征和梯度的有效工具