- 参考:动手学深度学习
- 注意:由于本文是jupyter文档转换来的,代码不一定可以直接运行,有些注释是jupyter给出的交互结果,而非运行结果!!
文章目录
- 1. 模型构造
- 1.1 继承 `Module` 类来构造模型
- 1.2 `Module` 的子类
- 1.2.1 `Sequential` 类
- 1.2.2 `ModuleList` 类
- 1.2.3 `ModuleDict` 类
- 1.3 构造复杂模型
- 2. 模型参数的访问、初始化和共享
- 2.1 访问模型参数
- 2.2 初始化模型参数
- 2.2.1 使用 pytorch 自带的初始化方法
- 2.2.2 自定义初始化方法
- 2.3 共享模型参数
- 3. 自定义层
- 3.1 不含模型参数的自定义层
- 3.2 含模型参数的自定义层
1. 模型构造
- pytorch 没有特别明显地区别 Layer 和 Module 的区别,不管是自定义层、自定义块还是自定义模型,都通过
nn.Module
类实现,它是所有神经网络模块的基类 - 构造模型时一般有两种选择
- 对于简单的模型,直接使用 pytorch 提供的
nn.Module
子类进行构造,比如 Sequential
、ModuleList
和 ModuleDict
等等 - 对于复杂模型,自己继承
nn.Module
,再重新实现构造函数 __init__
(定义需要学习的层) 和 forward
方法(定义网络结构),pytorch 会通过自动求梯度功能自动生成反向传播所需的 backward
函数
1.1 继承 Module 类来构造模型
- 这里构造一个用于 Fashion-MNIST 分类的,含单隐藏层的多层感知机,它的输入尺寸为图像尺寸
,类别总数为 10,有两个全连接层
- 隐藏层,输出大小为256(即隐藏单元个数是256)
- 输出层,输出大小为10(即输出层单元个数是10)
实例化 MLP
类得到模型变量 net
,直接 net(X)
就会调用 MLP
继承自 Module
类的 __call__
函数,这个函数将调用 MLP
类定义的 forward
函数来完成前向计算
- 重写
__init__
和 forward
函数的一些技巧
-
__init__
传入 **kwargs
参数用来初始化父类 nn.Module
,这样在构造实例时还可以指定其他函数参数 - 有可学习参数的层(如全连接层、卷积层等)通常放在
__init__
- 没有可学习参数的层(如激活函数、dropout、BatchNormanation层)可以放在
__init__
中,也可以在 forward
方法里用 nn.functional
代替
1.2 Module 的子类
1.2.1 Sequential 类
-
Sequential
接收一个 “子模块的有序字典(OrderedDict
)”或者“一系列子模块”作为参数来逐一添加 Module
的实例,得到模型的 forward
方法将这些实例按添加的顺序逐一计算,适用于模型的前向计算为简单串联各个层的计算的情况 -
Sequential
中的每个层都是 Module
实例,可以用类似列表的 []
索引其中的某层,然后也可以类似上面访问其中参数
- 下面利用 1.1 节继承
Module
的方法手动模拟 Sequential
类
1.2.2 ModuleList 类
-
ModuleList
接收一个子模块的列表作为输入,可以类似 python List
那样进行 append
和 extend
操作,它的出现主要是为了让网络定义 forward
时更加灵活,见下面官网的例子
-
ModuleList
的特点有
- 仅仅是一个储存各种模块的列表,这些模块之间没有联系也没有顺序(所以不用保证相邻层的输入输出维度匹配),这和
Sequential
类不同 - 没有实现
forward
功能,需要自己实现(直接执行会报 NotImplementedError
)
- 不同于Python 内置 list,加入到
ModuleList
里面的所有模块的参数会被自动添加到整个网络中
Note:对得到的
Module
类实例调用 .parameters()
方法,这会返回一个生成器,可以按顺序查看网络的参数(同一层先返回 weight 再返回 bias)。详见下面 2.1 节
1.2.3 ModuleDict 类
-
ModuleDict
也是为了让网络定义 forward
时更加灵活而出现的,它接受一个子模块的字典作为输入,可以类似 python Dict
那样进行添加访问操作 -
ModuleDict
的特点有
-
ModuleDict
实例也是仅仅存放了一个模块的字典,这些模块之间没有联系也没有顺序 - 不会自动生成
forward
方法,需要自己定义(直接执行会报 NotImplementedError)
- 加入到
ModuleDict
里面的所有模块的参数会被自动添加到整个网络中,python 内置的 Dict 不会
1.3 构造复杂模型
- 本节通过继承
Module
类构造一个复杂网络 FancyMLP
。其中
- 通过
requires_grad=False
创建训练中不被迭代的参数(i.e. 常数参数) - 使用 Tensor 的函数和 Python 控制流多次调用相同的层
- 这里定义的
FancyMLP
作为 Module
的子类,可以进一步和 Sequential
等嵌套使用
2. 模型参数的访问、初始化和共享
- 先定义一个含单隐藏层的多层感知机,使用默认方式初始化它的参数,并做一次前向计算(jupyter 文档,后面会用到这里定义的
X
,Y
,net
等变量)
- 该网络结构如下所示
2.1 访问模型参数
- 在 1.2.1 节已经提到过,可以对
Module
实例调用 .parameters
方法得到一个模型参数的生成器,除此以外,还可以调用 .named_parameters
方法得到一个带名字(含层数前缀)的参数生成器,如下
- pytorch 不区分层和模型,用
[]
索引出 Sequential
中的某层,也可以类似上面访问其中参数
观察输出可见
- 只有一个层,参数名字中没有层数索引前缀了
- 返回的
param
类型为 torch.nn.parameter.Parameter
,这其实这是 Tensor
的子类,如果一个 Tensor
是 Parameter
,那么它会自动被添加到模型的参数列表里
-
Parameter
本质是 Tensor
,可以做 Tensor
能做的任何操作,比如用 .data
访问参数值,用 .grad
访问参数梯度等
2.2 初始化模型参数
- 模型参数初始化是很重要的一件事,考虑第2节最初的含一个隐藏层的神经网络,假设将每个隐藏单元的参数都初始化为相等的值,由于各个单元的激活函数也一致,那么
- 正向传播时每个隐藏单元计算并输出的值都相等
- 反向传播时每个隐藏单元的参数梯度值相等,因此这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此
可见这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。
- pytorch
nn.Module
中的模块参数都采取了较为合理的初始化策略,所以其实不考虑初始化问题也是可以的(不同类型的 layer 具体的初始化方式也不同,可以参考源码)。如果一定要用其他方式初始化参数,也有两种途径
2.2.1 使用 pytorch 自带的初始化方法
-
torch.nn.init
模块里提供了多种预设初始化方法,以下代码来自 Pytorch - nn.init 参数初始化方法
- 下面将权重参数初始化成服从
2.2.2 自定义初始化方法
- 如果我们需要的初始化方法没有在
init
模块中提供,可以自己手动实现一个初始化方法,从而能够像使用其他初始化方法那样使用它 - 我们先来看看 PyTorch 是怎么实现内置初始化方法的,例如
torch.nn.init.normal_
可以看到这就是一个 inplace 改变 Tensor 值的函数,而且这个过程是不记录梯度的。 我们可以类似地实现一个自定义初始化方法。下面我们令权重有一半概率初始化为 0,有另一半概率初始化为 和
- 另外考虑到
Parameter
本质是 Tensor
,我们还可以通过改变这些参数的 .data
,在不影响梯度的情况下改写模型参数值
2.3 共享模型参数
- 有时我们想在多个层之间共享参数,因为模型参数里包含了梯度,所以在反向传播计算时,这些共享的参数的梯度是累加的
- 有两种共享参数的方法
- 在
Module
的 forward
方法中多次调用同一个层
- 将同一个
Module
实例多次传入 Sequential
模块
3. 自定义层
- 现代术语“深度学习”诉诸于学习多层次组合这一普遍原理。理论上讲,“宽度学习”(使用足够宽的单隐藏层神经网络)也能拟合任何形状的目标函数,但是其需要的参数量要远多于将神经元进行层次组合的“深度学习”。“深度学习”的模型表示更加“高效”,这样模型复杂度就可以相对低,在不发生过拟合的前提下,尽量减少对数据量的要求
- pytorch 中不区分“层”、“块”和“模型”,都统一用
nn.Module
库实现
3.1 不含模型参数的自定义层
- 如果一个层中不含可训练参数,比如 ReLU、dropout、BatchNormanation 等,则它和之前 1.1 节定义的模型类似,可以通过继承
nn.Module
类并重写 __init__
和 forward
函数来实现。下面的 CenteredLayer
自定义了一个将输入减掉均值后输出的层,层的计算定义在 forward
函数里,该层不含模型参数
- 自定义层可以和其他层、块等混合使用,构造更复杂的模型
3.2 含模型参数的自定义层
- 2.1.1 节已经介绍了
Tensor
的子类 Parameter
,如果一个 Tensor
是 Parameter
,那么它会自动被添加到模型的参数列表里,所以在自定义含模型参数的层时,我们应该将参数定义成 Parameter
。具体来说有三种定义方法
- 直接像 2.1.1 节一样将参数定义为
Parameter
类的实例,如 self.weight1 = nn.Parameter(torch.rand(20, 20))
- 使用
ParameterList
定义参数的列表。它接收一个 Parameter
实例列表作为输入,得到一个 ParameterList
实例,使用的时候可以用索引来访问某个参数,也可像 python list 一样使用 append
和 extend
在列表后面新增参数
- 使用
ParameterDict
定义参数的字典。它接收一个 Parameter
实例的字典作为输入,得到一个 ParameterDict
实例,使用时可以按照字典的规则使用了,如使用 update()
新增参数,使用 keys()
返回所有键值,使用 items()
返回所有键值对等等。详见 官方文档
- 自定义层可以和其他层、块等混合使用,构造更复杂的模型