动手学深度学习18- 模型构造基础Module类的操作手册
模型的构造
回顾之前的3.10章节中的多层感知机的简洁实现中,含但隐藏层的多层感知机的实现方法中,我们首先构造了Sequential实例,然后依次天机了两个全连接层。其中第一层的输出大小为256,即隐藏层的单元个数为256;第二层的输出大小为10,即输出层的单元个数为10。我们再上一章也使用了Sequential类构件模型。这里我们介绍另外一种基于Module类的模型构件方法:模型构件更加灵活。
通过Module类来构造模型
Module类是nn模块里提供的一个模型构造类。是所有神经网络模块 的基类,我们可以继承他来定义我们想要的模型。下面继承Module类构造本节开头提到的剁成感知机。这里定义的MLP类 重载了Module类的__init__函数和forward函数。它们分别用于创建模型参数和定义前向计算(正向传播)。
import torch
from torch import nn
class MLP(nn.Module):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self,**kwargs):
# 调用MLP父类Module的构造函数来进行必要的初始化。这样的构造实例时 还可以指定其他的函数
# 参数,可以 使用super重载初始化参数
super(MLP,self).__init__(**kwargs)
self.hidden = nn.Linear(784,256) # 隐藏层
self.act = nn.ReLU()
self.output = nn.Linear(256,10)
def forward(self,x):
a = self.act(self.hidden(x))
return self.output(a)
以上的MLP类中无须定义反向传播函数,系统将通过自动求梯度而自动生成反向传播所需的backward函数。
我, 可以通过实例化MLP类得到模型变量net。下面的代码初始化net并传入数据 X做一次前向计算。其中,net(X)会调用的MLP继承在Module类的__call__函数,这个函数将调用MLP类定义的forward函数来完成前向计算。
X= torch.rand(2,784)
#生成一个包含了从区间[0, 1)的均匀分布中抽取的一组随机数
# torhc.randn 标准正态分布,其中2,784 是对应行和列也就是**size
# print(X)
print(X.shape)
net = MLP()
print(net)
net(X)
tensor([[0.2540, 0.1617, 0.8079, ..., 0.1507, 0.4027, 0.2912],
[0.2456, 0.0723, 0.1746, ..., 0.3392, 0.4100, 0.4439]])
torch.Size([2, 784])
MLP(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(act): ReLU()
(output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[-0.2296, 0.0047, -0.0727, 0.1340, -0.2250, 0.0936, -0.0266, -0.0105,
0.0701, 0.1474],
[-0.3118, -0.0495, -0.1344, 0.1733, -0.2783, 0.0599, -0.2457, 0.0853,
0.0538, 0.0893]], grad_fn=<AddmmBackward>)
Module的子类既可以是一个层(如pytorch提供的Linear类),也可以是个模型(这里的定义的MLP)或者是模型的一部分。
Module的子类
Module类是一个通用的部件。事实上,还可以通过继承Pytorch的Module类构建其他的类,如Sequential,ModuleList,ModuleDict等类。首先我们先继承Module简单实现一个Sequential的类。
Sequential类
当模型的前向计算为简单的串联各个层的计算时,Sequential类可以通过更加简单的方式定义模型。这正是Sequential列的目的:他可以接收一个子模块的有序字典(OrderedDict)或者一系列的子模块作为参数来逐一添加Module的实例,而模型的前向计算就是将这些实例 按照添加的顺序逐一计算。
下面我们在继承Module的基础上实现一个跟Sequential类功能相同的MySequential类。
class MySequential(nn.Module):
from collections import OrderedDict
def __init__(self,*args):
super(MySequential,self).__init__()
if len(args)==1 and isinstance(args[0],OrderedDict):
for key,module in args[0].items():
self.add_module(key,module)
else: # 传入的是一些Module
for idx,module in enumerate(args):
self.add_module(str(idx),module)
def forward(self,input):
for module in self._modules.values():
input = module(input)
return input
我们用MySequential类来实现前面所描述的MLP类,并使用随机初始化的模型做一次前向计算。
net = MySequential(nn.Linear(784,256),
nn.ReLU(),
nn.Linear(256,10),
)
print(net)
net(X)
MySequential(
(0): Linear(in_features=784, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[-0.0520, -0.2588, -0.0551, -0.2209, -0.1325, 0.1073, 0.1471, -0.0518,
0.0895, -0.1300],
[ 0.0592, -0.1857, -0.1532, -0.0446, 0.0069, 0.0621, 0.1925, -0.1252,
0.0519, -0.1970]], grad_fn=<AddmmBackward>)
ModuleList 类
ModuleList接收衣蛾子模块的列表作为输入,然后可以类似于List那样进行append和extend操作.
net = nn.ModuleList([nn.Linear(784,256),nn.ReLU()])
net.append(nn.Linear(256,10)) # 类似于列表的操作
print(net[-1]) # 可以类似列表通过索引访问
print(net)
Linear(in_features=256, out_features=10, bias=True)
ModuleList(
(0): Linear(in_features=784, out_features=256, bias=True)
(1): ReLU()
(2): Linear(in_features=256, out_features=10, bias=True)
)
ModuleDict类
ModuleDict接收一个子模块的字典作为输入,然后可以类似字典的操作进行添加访问
net = nn.ModuleDict({'linear':nn.Linear(784,256),
'act':nn.ReLU(),
})
net['output'] = nn.Linear(256,10)
print(net['linear'])
print(net.output)
print(net)
Linear(in_features=784, out_features=256, bias=True)
Linear(in_features=256, out_features=10, bias=True)
ModuleDict(
(act): ReLU()
(linear): Linear(in_features=784, out_features=256, bias=True)
(output): Linear(in_features=256, out_features=10, bias=True)
)
构造复杂的模型
虽然上面介绍的集中方法可以使模型的构造更加简单,且不需要定义forward函数,但直接继承Module类可以极大的狂战模型的构造的灵活性。下面我们构造一个稍微复杂一点的网络FancyMLP。在这个网络中,我们通过get_constant函数创建训练中不被迭代的参数即常数参数。在前向计算中,除了使用创建的常数参数外,我们还会用了Tensor的函数和Python的控制流,并多次调用相同的层。
class FancyMLP(nn.Module):
def __init__(self, **kwargs):
super(FancyMLP, self).__init__(**kwargs)
self.rand_weight = torch.rand((20, 20), requires_grad=False) # 不可训练参数(常数参数)
self.linear = nn.Linear(20, 20)
def forward(self, x):
x = self.linear(x)
# 使用创建的常数参数,以及nn.functional中的relu函数和mm函数
x = nn.functional.relu(torch.mm(x, self.rand_weight.data) + 1)
# 复用全连接层。等价于两个全连接层共享参数
x = self.linear(x)
# 控制流,这里我们需要调用item函数来返回标量进行比较
while x.norm().item() > 1:
x /= 2
if x.norm().item() < 0.8:
x *= 10
return x.sum()
在这个FancyMLP模型中,我们使用常数权重rand_weight(不可训练的模型参数)、做了矩阵乘法操作torch.mm,并且复用了相同的Linear层,下面我们来测试该模型的前向计算。
X= torch.rand(2,20)
net = FancyMLP()
print(net)
net(X)
FancyMLP(
(linear): Linear(in_features=20, out_features=20, bias=True)
)
tensor(-3.7364, grad_fn=<SumBackward0>)
因为FancyMLP和Sequential都是Module的子类,我们 可以嵌套调用他们。
class NestMLP(nn.Module):
def __init__(self,**kwargs):
super(NestMLP,self).__init__(**kwargs)
self.net = nn.Sequential(nn.Linear(40,30),nn.ReLU())
def forward(self,x):
return self.net(x)
net = nn.Sequential(NestMLP(),nn.Linear(30,20),FancyMLP())
X = torch.rand(2,40)
print(net)
net(X)
# 评论 将40个特征变成30个,将30个变成20,in 和out都是20个特征,三个连续的模型
# 需要保持in于out的一致性。
Sequential(
(0): NestMLP(
(net): Sequential(
(0): Linear(in_features=40, out_features=30, bias=True)
(1): ReLU()
)
)
(1): Linear(in_features=30, out_features=20, bias=True)
(2): FancyMLP(
(linear): Linear(in_features=20, out_features=20, bias=True)
)
)
tensor(-2.2968, grad_fn=<SumBackward0>)
小结
- 可以通过继承Module类构造模型
- Sequential,ModuleList,ModuleDict都是继承自Module类
- Sequential等类可以使构建模型更加简单吗,但是直接继承Module可以极大的拓展构造模型的灵活性。