这里记录一下pytorch神经网络参数管理方法(参数访问、参数初始化、参数绑定),方便自己和需要的朋友学习、查阅。

目录

一、参数访问

1.1 访问指定层的指定参数

1.2 访问某一层或整个网络的所有参数

1.3 访问嵌套块的指定参数

二、参数初始化

2.1 内置初始化

2.2 自定义初始化

三、参数绑定

四、全部测试代码


一、参数访问

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])