模型构造、参数访问初始化共享
模型构造、参数访问初始化共享
4.1 模型构造
4.1.1 继承Module构造模型
4.1.2 Module子类
4.1.2.1 Sequential
4.1.2.2 ModuleList
4.1.2.3 ModuleDict
4.1.3 试着构建复杂模型
4.2 模型参数相关
4.2.1 访问参数
4.2.2 初始化模型参数
4.2.3 自定义初始化方法
4.2.4 共享参数
说明
4.1 模型构造
4.1.1 继承Module构造模型
Module类是nn模块里提供的一个模型构造类,是所有神经网络模块的基类,可以继承它来自定义模型。继承Module类构造多层感知机。定义的MLP类重载了Module类的__init__函数和forward函数。它们分别用于创建模型参数和定义前向计算。
%matplotlib inline
import torch
from IPython import display
import matplotlib.pyplot as plt
%config InlineBackend.figure_format = “png”
import numpy as np
import random
import sys
sys.path.append(“..”)
from d2lzh_pytorch import *
import os
os.environ[“KMP_DUPLICATE_LIB_OK”]=”TRUE”
class MLP(nn.Module):
# 声明带有模型参数的层,这里声明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Module的构造函数来进行必要的初始化。这样在构造实例时还可以指定其他函数
# 参数,如“模型参数的访问、初始化和共享”一节将介绍的模型参数params
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(784, 256)
self.act = nn.ReLU()
self.output = nn.Linear(256, 10)
# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
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)
net = MLP()
print(net)
net(X)
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.0387, -0.0412, 0.2092, 0.1522, -0.1067, 0.1860, -0.0786, 0.0300,
-0.0347, 0.1138],
[ 0.0488, 0.0427, 0.2431, 0.1437, -0.1839, 0.1328, -0.1228, 0.0328,
-0.1240, 0.0826]], grad_fn=<AddmmBackward>)
注意,这里并没有将Module类命名为Layer(层)或者Model(模型)之类的名字,这是因为该类是一个可供自由组建的部件。它的子类既可以是一个层(如PyTorch提供的Linear类),又可以是一个模型(如这里定义的MLP类),或者是模型的一个部分。我们下面通过两个例子来展示它的灵活性。
4.1.2 Module子类
我们刚刚提到,Module类是一个通用的部件。事实上,PyTorch还实现了继承自Module的可以方便构建模型的类: 如Sequential、ModuleList和ModuleDict等等
4.1.2.1 Sequential
当模型的前向计算为简单串联各个层的计算时,Sequential类可以通过更加简单的方式定义模型。这正是Sequential类的目的:它可以接收一个子模块的有序字典(OrderedDict)或者一系列子模块作为参数来逐一添加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): # 如果传入的是一个OrderedDict
for key, module in args[0].items():
self.add_module(key, module) # add_module方法会将module添加进self._modules(一个OrderedDict)
else: # 传入的是一些Module
for idx, module in enumerate(args):
self.add_module(str(idx), module)
def forward(self, input):
# self._modules返回一个 OrderedDict,保证会按照成员添加时的顺序遍历成员
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.2538, 0.0296, 0.2009, -0.2214, 0.1374, 0.1585, -0.1090, 0.2164,
-0.0628, 0.0303],
[ 0.1401, -0.1001, 0.1719, 0.0153, 0.0389, 0.1669, -0.1041, 0.2167,
0.0235, 0.1929]], grad_fn=<AddmmBackward>)
4.1.2.2 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)
)
4.1.2.3 ModuleDict
ModuleDict接收一个子模块的字典作为输入, 也可以类似字典那样进行添加访问操作:
和ModuleList一样,ModuleDict实例仅仅是存放了一些模块的字典,并没有定义forward函数需要自己定义。同样,ModuleDict也与Python的Dict有所不同,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)
# net(torch.zeros(1, 784)) # 会报NotImplementedError
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)
)
4.1.3 试着构建复杂模型
上面介绍的这些类可以使模型构造更加简单,且不需要定义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
在这个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(0.4051, 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)
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(-0.2525, grad_fn=<SumBackward0>)
4.2 模型参数相关
from torch.nn import init
net = nn.Sequential(nn.Linear(4, 3), nn.ReLU(), nn.Linear(3, 1))
print(net)
X = torch.rand(2,4)
Y = net(X).sum()
print(Y)
Sequential(
(0): Linear(in_features=4, out_features=3, bias=True)
(1): ReLU()
(2): Linear(in_features=3, out_features=1, bias=True)
)
tensor(-0.2687, grad_fn=<SumBackward0>)
4.2.1 访问参数
对于Sequential实例中含模型参数的层,我们可以通过Module类的parameters()或者named_parameters方法来访问所有参数(以迭代器的形式返回),后者除了返回参数Tensor外还会返回其名字。下面,访问多层感知机net的所有参数:
print(type(net.named_parameters()))
for name, param in net.named_parameters():
print(name, param)
<class ‘generator’>
0.weight Parameter containing:
tensor([[-0.1270, -0.4299, 0.4027, 0.4511],
[-0.3526, 0.3099, 0.1313, 0.1578],
[ 0.4411, -0.2710, -0.3702, -0.0549]], requires_grad=True)
0.bias Parameter containing:
tensor([ 0.2990, -0.1793, -0.2047], requires_grad=True)
2.weight Parameter containing:
tensor([[ 0.0835, -0.5109, -0.5549]], requires_grad=True)
2.bias Parameter containing:
tensor([-0.1805], requires_grad=True)
返回的名字自动加上了层数的索引作为前缀。 我们再来访问net中单层的参数。对于使用Sequential类构造的神经网络,我们可以通过方括号[]来访问网络的任一层。索引0表示隐藏层为Sequential实例*先添加的层。
因为这里是单层的所以没有了层数索引的前缀。另外返回的param的类型为torch.nn.parameter.Parameter,其实这是Tensor的子类,和Tensor不同的是如果一个Tensor是Parameter,那么它会自动被添加到模型的参数列表里
for name, param in net[0].named_parameters():
print(name, param.size(), type(param))
weight torch.Size([3, 4]) <class ‘torch.nn.parameter.Parameter’>
bias torch.Size([3]) <class ‘torch.nn.parameter.Parameter’>
#和Tensor不同的是如果一个Tensor是Parameter
#那么它会自动被添加到模型的参数列表里
class MyModel(nn.Module):
def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
self.weight1 = nn.Parameter(torch.rand(20, 20))
self.weight2 = torch.rand(20, 20)
def forward(self, x):
pass
n = MyModel()
for name, param in n.named_parameters():
print(name)
weight1
上面的代码中weight1在参数列表中但是weight2却没在参数列表中。
因为Parameter是Tensor,即Tensor拥有的属性它都有,比如可以根据data来访问参数数值,用grad来访问参数梯度。
4.2.2 初始化模型参数
PyTorch中nn.Module的模块参数都采取了较为合理的初始化策略,但我们经常需要使用其他方法来初始化权重。PyTorch的init模块里提供了多种预设的初始化方法
for name, param in net.named_parameters():
if ‘weight’ in name:
init.normal_(param,mean=0,std=0.01)
print(name,param.data)
0.weight tensor([[ 0.0036, -0.0119, -0.0024, 0.0030],
[-0.0020, 0.0009, -0.0042, -0.0017],
[-0.0095, -0.0018, 0.0102, -0.0183]])
2.weight tensor([[ 0.0076, 0.0040, -0.0136]])
#使用常数来初始化权重参数。
for name, param in net.named_parameters():
if ‘bias’ in name:
init.constant_(param, val=0)
print(name, param.data)
0.bias tensor([0., 0., 0.])
2.bias tensor([0.])
4.2.3 自定义初始化方法
有时候我们需要的初始化方法并没有在init模块中提供。这时,可以实现一个初始化方法,从而能够像使用其他初始化方法那样使用它。在这之前我们先来看看PyTorch是怎么实现这些初始化方法的,例如torch.nn.init.normal_:
def normal_(tensor, mean=0, std=1):
with torch.no_grad():
return tensor.normal_(mean, std)
可以看到这就是一个inplace改变Tensor值的函数,而且这个过程是不记录梯度的。 类似的我们来实现一个自定义的初始化方法。在下面的例子里,我们令权重有一半概率初始化为0,有另一半概率初始化为[−10,−5]和[5,10]两个区间里均匀分布的随机数。
def init_weight_(tensor):
with torch.no_grad():
tensor.uniform_(-10, 10)
tensor *= (tensor.abs() >= 5).float()
for name, param in net.named_parameters():
if ‘weight’ in name:
init_weight_(param)
print(name, param.data)
0.weight tensor([[ 5.7987, 6.0930, 0.0000, -5.6870],
[-5.9683, -0.0000, -0.0000, 6.2262],
[-0.0000, -0.0000, -6.4857, 0.0000]])
2.weight tensor([[-0., 0., -0.]])
4.2.4 共享参数
Module类的forward函数里多次调用同一个层。此外,如果我们传入Sequential的模块是同一个Module实例的话参数也是共享的:
linear = nn.Linear(1, 1, bias=False)
net = nn.Sequential(linear, linear)
print(net)
for name, param in net.named_parameters():
init.constant_(param, val=3)
print(name, param.data)
Sequential(
(0): Linear(in_features=1, out_features=1, bias=False)
(1): Linear(in_features=1, out_features=1, bias=False)
)
0.weight tensor([[3.]])
#在内存中,这两个线性层其实一个对象:
print(id(net[0]) == id(net[1]))
print(id(net[0].weight) == id(net[1].weight))
True
True
因为模型参数里包含了梯度,所以在反向传播计算时,这些共享的参数的梯度是累加的:
x = torch.ones(1, 1)
print(net(x))
y = net(x).sum()
print(y)
y.backward()
print(net[0].weight.grad) # 单次梯度是3,两次所以就是6\
tensor([[9.]], grad_fn=<MmBackward>)
tensor(9., grad_fn=<SumBackward0>)
tensor([[6.]])
说明
本博客是对如何使用pytorch用于深度学习 学习过程的记录和总结。
学习教程为:《动手学深度学习》和https://tangshusen.me/Dive-into-DL-PyTorch/#/
这里推荐这个网址,将动手学深度学习改为了Pytorch实现,很有意义!
代码是借鉴了学习教程并从自己写的Jupyter中导出的,复制进Jupyter可以运行