这里记录一下pytorch神经网络参数管理方法(参数访问、参数初始化、参数绑定),方便自己和需要的朋友学习、查阅。
目录
一、参数访问
1.1 访问指定层的指定参数
首先构建一个多层感知机。
import torch
from torch import nn
net = nn.Sequential(nn.Linear(2, 4), nn.ReLU(), nn.Linear(4, 1))
X = torch.rand(size=(2, 2))
当通过nn.Sequential定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查任意一个全连接层的参数。
# 1 查看网络第一层(即第一个全连接层)的参数
print(net[0].state_dict())
# 2 查看网络第三层(即第二个全连接层)偏置参数的类型
print(type(net[2].bias))
# 3 查看网络第三层(即第二个全连接层)偏置参数
print(net[2].bias)
# 4 查看网络第三层(即第二个全连接层)偏置参数的值
print(net[2].bias.data)
# 5 查看网络第一层(即第一个全连接层)权重参数
print(net[0].weight)
# 6 查看网络第二层
print(net[1])
结果分别如下所示:
# 1
OrderedDict([('weight', tensor([[ 0.0854, 0.1861],
[ 0.5421, 0.2435],
[ 0.5745, 0.2469],
[ 0.4120, -0.4345]])), ('bias', tensor([ 0.3356, 0.4215, 0.2181, -0.2548]))])
# 2
<class 'torch.nn.parameter.Parameter'>
# 3
Parameter containing:
tensor([-0.1606], requires_grad=True)
# 4
tensor([-0.1606])
# 5
Parameter containing:
tensor([[-0.4710, 0.0820],
[-0.5563, 0.0728],
[ 0.1691, 0.2211],
[ 0.4279, -0.5597]], requires_grad=True)
# 6
ReLU()
可以看出,每个参数都表示为参数类的一个实例,要对参数执行任何操作,首先需要访问底层的数值。网络层数从0开始,即net[0]表示网络第一层,激活函数也是网络中的一层。
访问偏置使用basis属性,访问权重使用weight属性。参数是复合的对象,包含值、梯度和额外信息。若只想获取参数的值,要在basis或weight后加data属性。除了值之外,我们还可以访问每个参数的梯度。
print(net[2].weight.grad == None)
# 结果为 True
# 原因:由于还没有调用反向传播,所以参数的梯度处于初始状态
1.2 访问某一层或整个网络的所有参数
当需要对所有参数执行操作时,逐个访问它们可能会很麻烦,此时我们可以通过递归整个树来提取每个子块的参数。
# 1 访问第一层的所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# 2 访问网络所有层的全部参数
print(*[(name, param.shape) for name, param in net.named_parameters()])
结果如下:
# 1
('weight', torch.Size([4, 2])) ('bias', torch.Size([4]))
# 2
('0.weight', torch.Size([4, 2])) ('0.bias', torch.Size([4])) ('2.weight', torch.Size([1, 4])) ('2.bias', torch.Size([1]))
注意:激活函数没有参数,所以打印出来的网络的所有参数只有两个全连接层的参数。
此外,我们还可以通过下述方式访问网络参数。
# 访问第网络第三层的偏置参数的值
print(net.state_dict()['2.bias'].data)
结果如下:
tensor([-0.1089])
如果不使用nn.Sequential定义模型,而是自己定义一个类实现网络,则不能使用索引访问指定层的参数。如下所示:
class mlp(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.linear1 = nn.Linear(input_size, 4) # 全连接层
self.relu = nn.ReLU()
self.linear2 = nn.Linear(4, output_size) # 全连接层
def forward(self, x):
x = self.linear1(x)
return self.relu(self.linear2(x))
mlp_net= mlp(2, 1)
X = torch.rand(size=(2, 2))
# 如果使用索引访问会报错
print(mlp_net[0].state_dict())
此时会输出如下结果:
Traceback (most recent call last):
File "E:/SoftwareLearning/Python/Code/d2l-Train/ParameterManagement.py", line 46, in <module>
print(mlp_net[0].state_dict())
TypeError: 'mlp' object is not subscriptable
但可以通过下述方式进行访问:
# linear2为定义网络模型时自己起的某一全连接层的名称
print(mlp_net.state_dict()['linear2.bias'].data)
输出结果如下:
tensor([0.3709])
1.3 访问嵌套块的指定参数
首先构建一个嵌套块的网络。
def block1():
return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
# 在这里嵌套
net.add_module(f'block {i}', block1())
return net
# 实例化
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
# 查看网络结构
print(rgnet)
输出结果如下:
Sequential(
(0): Sequential(
(block 0): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 1): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 2): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
(block 3): Sequential(
(0): Linear(in_features=4, out_features=8, bias=True)
(1): ReLU()
(2): Linear(in_features=8, out_features=4, bias=True)
(3): ReLU()
)
)
(1): Linear(in_features=4, out_features=1, bias=True)
)
因为是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。下面,我们访问第一个主要的块中第二个子块的第一层的权重项。
print(rgnet[0][1][0].weight.data)
输出结果如下:
tensor([[ 0.3175, 0.0233, 0.3233, -0.0627],
[-0.0835, -0.3371, -0.4527, 0.0141],
[ 0.1070, 0.3952, 0.4051, 0.3921],
[ 0.1958, -0.3643, 0.4481, -0.3448],
[ 0.0446, -0.0256, 0.1490, 0.4568],
[-0.1352, -0.2099, -0.1225, -0.0413],
[ 0.3027, 0.2114, -0.4063, -0.0288],
[-0.4594, 0.0076, -0.2671, 0.2669]])
二、参数初始化
初始化对神经网络来说十分重要,良好的初始化能帮助模型快速收敛,对保持网络的数值稳定性至关重要。默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵,这个范围是根据输入和输出维度计算出的。PyTorch的nn.init
模块提供了多种预置初始化方法。
2.1 内置初始化
首先调用内置的初始化器对网络参数进行初始化。下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0。
# 将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0
def init_normal(m):
if type(m) == nn.Linear:
# 将权重参数初始化为标准差为0.01的高斯随机变量
nn.init.normal_(m.weight, mean=0, std=0.01)
# 将偏置参数初始化为0
nn.init.zeros_(m.bias)
net.apply(init_normal)
print(net[0].weight.data[0], '\n', net[0].bias.data[0])
输出结果如下:
tensor([-0.0142, -0.0054])
tensor(0.)
我们还可以将所有参数初始化为给定的常数,比如初始化为1。
# 将所有参数初始化为给定的常数
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_constant)
print(net[0].weight.data[0], net[0].bias.data[0])
输出结果如下:
tensor([1., 1.])
tensor(0.)
我们还可以对不同层应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
# 使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
输出结果如下:
tensor([ 0.9265, -0.1521])
tensor([[42., 42., 42., 42.]])
2.2 自定义初始化
有时,深度学习框架没有提供我们需要的初始化方法,此时就需要我们自定义初始化方法实现参数初始化。
# 自定义初始化
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10) # 均匀分布
# 如果绝对值大于5则参数不变,如果绝对值小于5则将参数置0
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
print(net[0].weight[:2])
输出结果如下:
Init weight torch.Size([4, 2])
Init weight torch.Size([1, 4])
tensor([[ 6.6987, -5.3545],
[ 6.6684, -6.3039]], grad_fn=<SliceBackward0>)
也可以根据需要直接对指定层的参数进行设置。
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
print(net[0].weight.data[0])
输出结果如下:
Init weight torch.Size([4, 2])
Init weight torch.Size([1, 4])
tensor([42.0000, 8.5777])
三、参数绑定
有时我们希望在多个层间共享参数,此时,我们可以定义一个全连接层,然后使用它的参数来设置另一个层的参数,实现参数共享。
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
print(net(X))
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
输出结果如下:
tensor([[0.4362],
[0.4562]], grad_fn=<AddmmBackward0>)
tensor([True, True, True, True])
tensor([True, True, True, True])
这个例子表明第三个和第五个神经网络层的参数是绑定的。它们不仅值相等,而且由相同的张量表示。因此,如果我们改变其中一个参数,另一个参数也会改变。你可能会思考:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
四、全部测试代码
全部测试代码如下。
import torch
from torch import nn
class mlp(nn.Module):
def __init__(self, input_size, output_size):
super().__init__()
self.linear1 = nn.Linear(input_size, 4) # 全连接层
self.relu = nn.ReLU()
self.linear2 = nn.Linear(4, output_size) # 全连接层
def forward(self, x):
x = self.linear1(x)
return self.relu(self.linear2(x))
def block1():
return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
nn.Linear(8, 4), nn.ReLU())
def block2():
net = nn.Sequential()
for i in range(4):
# 在这里嵌套
net.add_module(f'block {i}', block1())
return net
net = nn.Sequential(nn.Linear(2, 4), nn.ReLU(), nn.Linear(4, 1))
X = torch.rand(size=(2, 2))
print(net(X))
# 当通过Sequential类定义模型时,可以通过索引来访问模型的任意层。这就像模型是一个列表一样,每层的参数都在其属性中。
print(net[0].state_dict())
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
print(net[0].weight)
print(net[1])
print(net[2].weight.grad == None)
# 一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# 访问网络参数的另一种方式
print(net.state_dict()['2.bias'].data)
# 不使用nn.Sequential定义模型
mlp_net = mlp(2, 1)
X = torch.rand(size=(2, 2))
print(mlp_net(X))
# 不能使用索引访问某一层的参数,会报错
# print(mlp_net[0].state_dict())
print(mlp_net.state_dict()['linear2.bias'].data)
# 从嵌套块收集参数
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
print(rgnet)
# 访问第一个主要的块中、第二个子块的第一层的权重
print(rgnet[0][1][0].weight.data)
# 参数初始化
# 将所有权重参数初始化为标准差为0.01的高斯随机变量, 且将偏置参数设置为0
def init_normal(m):
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
# 将所有参数初始化为给定的常数
def init_constant(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.zeros_(m.bias)
net.apply(init_normal)
print(net[0].weight.data[0], '\n', net[0].bias.data[0])
net.apply(init_constant)
print(net[0].weight.data[0], '\n', net[0].bias.data[0])
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
# 使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42
net[0].apply(xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
# 自定义初始化
def my_init(m):
if type(m) == nn.Linear:
print("Init", *[(name, param.shape)
for name, param in m.named_parameters()][0])
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
print(net[0].weight[:2])
# 也可以直接设置参数
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
print(net[0].weight.data[0])
# 参数绑定
# 有时我们希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(4, 4)
net = nn.Sequential(nn.Linear(2, 4), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(4, 1))
print(net(X))
# 检查参数是否相同
print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])