参考的书籍是《深入浅出PyTorch——从模型到源码》,书中源代码仓库为https://github.com/zxjzxj9/PyTorchIntroduction

部分代码由于版本兼容性无法运行,文中会做出适当修改

学习路径

由于篇幅限制,我决定将源码学习分为多篇文章依次进行更新,以下为推荐的博客学习路径

  1. Pytorch学习(基础知识)
  2. Pytorch学习(运行逻辑)
  3. Pytorch高级应用

Pytorch常用层级

在深度学习神经网络中,整个神经网络的结构是由不同类型的层构建而来的,而层与层之间通过激活函数相连接

线性层

线性层,也称为全连接层,是最基础的深度学习模块,可以用于变换特征的维度

线性层实际上执行的是一个线性变换,在二维的情况下,相当于做了一个矩阵乘法和一个矩阵加法,即y=xW+b\pmb y = \pmb x\cdot \pmb W + \pmb b

当然,很多时候线性层的输入并不是一维或二维的张量,而是高维的张量

1
2
3
4
5
6
import torch
import torch.nn as nn

lm = nn.Linear(5, 10) # 输入特征5,输出特征10
t = torch.randn(4, 5) # 迷你批次大小4,特征大小5
print(lm(t).shape)

输出为

1
torch.Size([4, 10])

卷积层

卷积运算实际上是线性变换的一种,而且属于一种稀疏连接的线性变换(而全连接属于稠密连接的线性变换)

在Pytorch中,卷积又可以分为两种,第一类为正常的卷积,第二类为转置卷积,也称为反卷积

在实际应用过程中,对于通道数较多的数据的卷积,可以通过分组卷积的方式减少计算量

归一化层

在Pytorch中,几乎所有的归一化层都有类似下面公式的表示方式

y=γxE(x)Var(x)+ϵ+β\pmb y = \gamma\cdot\frac{\pmb x - E(\pmb x)}{\sqrt{Var(\pmb x)+\epsilon}} + \beta

其中x\pmb x为输入张量的值,ϵ\epsilon是一个小的浮点数常量(作用是防止分母为00),β\betaγ\gamma是可训练的向量参数,其元素的数目和输入张量的通道数目相等,它们直接的区别在于归一化的平均值E(x)E(\pmb x)和方差Var(x)Var(\pmb x)的计算方式不同

在实际应用过程中,批次归一化方法常常和卷积神经网络结合使用

经常的做法是卷积层和批次归一化层结合使用,即卷积层的输出结果直接输入到批次归一化层中,在这个情况下,因为批次归一化层会减去卷积层输出结果的平均值,对于卷积层来说,偏置的参数会在减去平均的过程中被消去,因此,可以直接在对应卷积模块的初始化中设置biasFalse,这样可以避免重复计算

池化层

为了能够同时减小计算量,并且得到比较小的输出,神经网络会使用池化层来对中间的特征张量进行降采样,减小图像的大小

池化层没有任何参数张量和缓存张量,在深度学习过程中仅相当于改变维度大小的模块

池化一般有三种方式:最大池化,平均池化和乘幂平均池化

需要注意的是,池化是一个下采样的过程,而反池化是池化的相反操作,是一个上采样的过程

但是由于缺乏数据,反池化需要和转置卷积进行配合,反池化将输入张量扩张为指定大小,而卷积来补充反池化中所缺少的信息

丢弃层

为了缓解过拟合,深度学习中常用的另一个手段是丢弃层

丢弃层的原理是随机减少神经元之间的链接,将稠密的连接变为稀疏的连接,减少神经元之间的连接关系,从而减少神经网络过拟合的倾向

模块的组合

在Pytorch中,一般会使用nn.Sequential来组合顺序调用的层级模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import torch.nn as nn

# 情况1. 使用参数来构建顺序模型
model = nn.Sequential(
nn.Conv2d(1,20,5),
nn.ReLU(),
nn.Conv2d(20,64,5),
nn.ReLU()
)

# 情况2. 使用顺序字典来构建顺序模型
model = nn.Sequential(OrderedDict([
('conv1', nn.Conv2d(1,20,5)),
('relu1', nn.ReLU()),
('conv2', nn.Conv2d(20,64,5)),
('relu2', nn.ReLU())
]))

第一种情况是按照调用顺序传入所有的模块,第二种情况是对每个模块指定一个名字,然后按照字典构建的顺序来调用这些模块

在深度学习模型的构建中,另一个涉及大量模块的情况是需要把一系列的模块存储在一个列表或一个字典中

在这种情况下,不能直接使用Python原生的列表和字典,否则调用包含这一系列模块中父模块的parametersnamed_parameters方法就不会含有这些模块的参数(因为本质上参数的获取是递归调用当前模块的子模块的参数获取,而列表和字典并不是当前模块的子模块,所以递归过程会在这种情况下中断)

为了能够正常使用parametersnamed_parameters方法,可以使用nn.ModuleListnn.ModuleDict来替代Python原生的列表和字典,对于这两个类,可以直接传入列表和字典来构造这两个模块,也可以传入空列表和空字典,然后使用append方法和字典的索引方法给模块列表和模块字典添加子元素

对于模块列表和模块字典,普通Python的数值和字符索引对这两个类分别适用

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

# 模块列表的使用方法
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.linears = nn.ModuleList([nn.Linear(10, 10) for i in range(10)])

def forward(self, x):
# 模块列表的迭代和使用方法与Python的普通列表一致
for i, l in enumerate(self.linears):
x = self.linears[i // 2](x) + l(x)
return x

# 模块字典的使用方法
class MyModule(nn.Module):
def __init__(self):
super(MyModule, self).__init__()
self.choices = nn.ModuleDict({
'conv': nn.Conv2d(10, 10, 3),
'pool': nn.MaxPool2d(3)
})
self.activations = nn.ModuleDict([
['lrelu', nn.LeakyReLU()],
['prelu', nn.PReLU()]
])

def forward(self, x, choice, act):
x = self.choices[choice](x)
x = self.activations[act](x)
return x

Pytorch自定义使用

Pytorch自定义激活函数和梯度

PyTorch 自定义激活函数继承的类是torch.autograd.Function,其内部需要定义两个静态方法:forwardbackward

1
2
3
4
5
6
7
8
9
10
class Func(torch.autograd.Function):

@staticmethod
def forward(ctx,input):
# 定义前向计算过程
return result
@staticmethod
def backward(ctx,grad_output):
# 定义反向计算过程
return grad_input

forward方法输入张量,返回输出张量,backward方法输入输出张量,返回输入张量,而其中的参数ctx是一个特殊的参数,用于在前向传播和反向传播之间共享张量

对于我们来说,经常用到的就是在ctx中保存输入张量的值,结合后一层的输出梯度来计算前一层的输入梯度

因为定义的类的前向计算函数和反向传播函数都是静态方法,因此这个类不需要实例化,可以直接当做函数来使用

下面的例子是关于GELU函数的,其具体表达式如下所示

GELU(x)=xσ(1.702x)GELU(x) = x\sigma(1.702x)

其中σ(x)\sigma(x)为Sigmoid函数

为了能够进行反向传播,首先需要知道函数对应的导数,GELU函数的导数如下所示

GELU(x)=σ(1.702x)+1.702xσ(1.702x)(1σ(1.702x))GELU'(x) = \sigma(1.702x) + 1.702x\cdot\sigma(1.702x)(1 - \sigma(1.702x))

1
2
3
4
5
6
7
8
9
10
11
12
13
# 同样可以通过 gelu = GELU.apply使用这个激活函数
class GELU(torch.autograd.Function):

@staticmethod
def forward(ctx, input):
ctx.input = input
return input * torch.sigmoid(1.702 * input)

@staticmethod
def backward(ctx, grad_output):
input = ctx.input
tmp = torch.sigmoid(1.702 * input)
return grad_output * (tmp + 1.702 * input * tmp * (1 - tmp))

然后我们可以用下面的代码进行检验

1
2
gelu = GELU.apply
print(torch.autograd.gradcheck(gelu, torch.randn(10, requires_grad=True, dtype=torch.double)))

若输出为True,则GELU激活函数输出正确

为了能够保持数值梯度的精度,需要使用双精度类型的张量作为测试的输入张量

正向传播和反向传播的钩子

在某些情况下,我们需要对深度学习模型的前向计算和反向传播进行一些修改,比如观察模型中某一层出现的异常值(如NaN或Inf)及其来源,或者对张量进行修改,则需要在模块中引入钩子来动态修改模块的行为

对于一个模块,可以有三种类型的钩子,分别在前向计算之前,前向计算之后和反向传播之后执行

第一种类型的钩子函数主要用在模块前向计算执行之前,其定义如下所示

1
2
3
4
5
6
7
8
9
10
# 模块执行之前的前向计算钩子的定义
# 定义nn.Module的一个实例模块
module = ...

def hook(module, input):
# 对模块权重或者输入进行操作的代码
# 函数结果可以返回修改后的张量或者None
return input

handle = module.register_forward_pre_hook(hook)

注册钩子函数时会返回一个handle句柄,通过调用句柄的remove方法可以将钩子函数移除

第二种类型的钩子函数是在模块前向计算之后执行,其定义如下所示

1
2
3
4
5
6
7
8
module = ...

def hook(module, input, output):
# 对模块权重或者输入/输出进行操作的代码
# 函数结果可以返回修改后的张量或者None
return output

handle = module.register_forward_hook(hook)

第三种类型的钩子函数是在模块反向传播之后执行,其定义如下所示

1
2
3
4
5
6
7
8
module = ...

def hook(module, grad_input, grad_output):
# 对模块权重或者输入/输出梯度进行操作的代码
# 函数结果可以返回修改后的张量或者None
return output

handle = module.register_backward_hook(hook)

下面是使用这些钩子函数的示例

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
import torch.nn as nn


def print_pre_shape(module, input):
print("模块前钩子")
print(module.weight.shape)
print(input[0].shape)


def print_post_shape(module, input, output):
print("模块后钩子")
print(module.weight.shape)
print(input[0].shape)
print(output[0].shape)


def print_grad_shape(module, grad_input, grad_output):
print("梯度钩子")
print(module.weight.grad.shape)
print(grad_input[0].shape)
print(grad_output[0].shape)


conv = nn.Conv2d(16, 32, kernel_size=(3, 3))
handle1 = conv.register_forward_pre_hook(print_pre_shape)
handle2 = conv.register_forward_hook(print_post_shape)
handle3 = conv.register_full_backward_hook(print_grad_shape)
input = torch.randn(4, 16, 128, 128, requires_grad=True)
ret = conv(input)
ret = ret.sum()
ret.backward()

输出为

1
2
3
4
5
6
7
8
9
10
11
模块前钩子
torch.Size([32, 16, 3, 3])
torch.Size([4, 16, 128, 128])
模块后钩子
torch.Size([32, 16, 3, 3])
torch.Size([4, 16, 128, 128])
torch.Size([32, 126, 126])
梯度钩子
torch.Size([32, 16, 3, 3])
torch.Size([4, 16, 128, 128])
torch.Size([4, 32, 126, 126])