5.2 参数管理
目标
- 访问参数,用于调试、诊断和可视化;
- 参数初始化;
- 在不同模型组件间共享参数。
模型:单隐藏层的MLP
import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net(X)
tensor([[-0.0970],[-0.0827]], grad_fn=<AddmmBackward0>)
1 参数访问
从已有模型中访问参数: 检查第二个全连接层的参数
print(net[2].state_dict())
OrderedDict([('weight', tensor([[-0.0427, -0.2939, -0.1894, 0.0220, -0.1709, -0.1522, -0.0334, -0.2263]])), ('bias', tensor([0.0887]))])
结果信息:
- 这个全连接层包含两个参数,分别是该层的权重和偏置
- 两者都存储为单精度浮点数(float32)
note:参数名称允许唯一标识每个参数
1.1 访问目标参数
从第二个全连接层(即第三个神经网络层,第三层)提取偏置
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([0.0887], requires_grad=True)
tensor([0.0887])
net[2].weight.grad == None
1.2 一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
(‘weight’, torch.Size([8, 4])) (‘bias’, torch.Size([8]))
(‘0.weight’, torch.Size([8, 4])) (‘0.bias’, torch.Size([8])) (‘2.weight’, torch.Size([1, 8])) (‘2.bias’, torch.Size([1]))
net.state_dict()['2.bias'].data
tensor([0.0887])
解读
列表推导式
- 列表推导式
- 列表推导式 [(name, param.shape) for name, param in net[0].named_parameters()] 遍历该层的所有参数,提取参数的名称和形状。
print(*… ) 中的 * 用于解包列表,将列表中的元素作为多个参数传递给 print 函数,从而将每个参数的名称和形状单独打印出来。
注:列表推导式(List Comprehension)是 Python 中一种简洁、高效创建列表的方式。它允许你使用一行代码快速生成列表,而无需使用传统的循环结构。列表推导式的基本语法结构如:[表达式 for 变量 in 可迭代对象 if 条件]
示例:
squares = [x ** 2 for x in range(10)]
输出print(squares) # 输出:[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
- 带条件的列表推导式
- 示例 :生成一个包含 0 到 9 之间的偶数的平方的列表。
- 代码 :
squares_of_evens = [x ** 2 for x in range(10) if x % 2 == 0]
print(squares_of_evens) # 输出:[0, 4, 16, 36, 64]
- 嵌套循环的列表推导式
- 示例 :生成两个列表元素的组合。
combinations = [(x, y) for x in [1, 2, 3] for y in ['a', 'b', 'c']]
print(combinations)
输出:[(1, ‘a’), (1, ‘b’), (1, ‘c’), (2, ‘a’), (2, ‘b’), (2, ‘c’), (3, ‘a’), (3, ‘b’), (3, ‘c’)]
print(*… ) 中的 *
print(*… ) 中的 * 用于解包列表,将列表中的元素作为多个参数传递给 print 函数,从而将每个参数的名称和形状单独打印出来。
1.3 从嵌套块收集参数
1.3.1 在 PyTorch 中构建嵌套的神经网络块
#将输入数据通过两个全连接层和激活函数进行处理,最终输出维度为 4
def block1():return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),nn.Linear(8, 4), nn.ReLU())#将输入数据通过两个全连接层和激活函数进行处理,最终输出维度为 4
def block2():# 创建一个空的 `nn.Sequential` 容器 `net`net = nn.Sequential()# 使用 `for` 循环迭代 4 次,每次调用 `block1()` 函数创建一个新的 `block1` 块for i in range(4):# 在这里嵌套# `add_module` 方法将创建的块添加到 `net` 中,并指定模块的名称为 `'block i'`(其中 `i` 是循环索引)net.add_module(f'block {i}', block1())# `block2` 返回一个包含 4 个 `block1` 块的序列容器return net# 构建一个完整的神经网络模型,包含 block2 返回的块和一个最终的全连接层#`block2()` 返回一个包含 4 个 `block1` 块的序列容器。
# `nn.Linear(4, 1)` 是一个全连接层,输入维度为 4,输出维度为 1。
# `nn.Sequential` 将这两个部分按顺序组合成一个完整的模型。
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))#对输入数据 `X` 进行前向传播,通过整个网络计算输出
#输入数据首先通过 4 个 `block1` 块,每个块包含两个全连接层和激活函数,处理后的数据最终通过最后一个全连接层 `nn.Linear(4, 1)` 得到输出
rgnet(X)
tensor([[0.2596],
[0.2596]], grad_fn=)
1.3.2 执行过程
执行过程
假设输入数据 X
的形状为 (batch_size, 4)
,前向传播的具体执行过程如下:
-
第一个
block1
块- 输入:
X
(形状(batch_size, 4)
) - 第一层全连接层:
nn.Linear(4, 8)
,输出形状为(batch_size, 8)
- ReLU 激活函数:对输出应用 ReLU 激活,输出形状仍为
(batch_size, 8)
- 第二层全连接层:
nn.Linear(8, 4)
,输出形状为(batch_size, 4)
- ReLU 激活函数:对输出应用 ReLU 激活,输出形状仍为
(batch_size, 4)
- 输入:
-
第二个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
第三个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
第四个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
最终全连接层
- 输入:最后一个
block1
块的输出(形状(batch_size, 4)
) - 全连接层:
nn.Linear(4, 1)
,输出形状为(batch_size, 1)
- 输入:最后一个
向前传播流程
rgnet((0): block2((block 0): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 1): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 2): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 3): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU()))(1): Linear(4 -> 1)
)
最终构建的网络 rgnet
是一个由多个嵌套块组成的复杂神经网络。以下是其结构的详细说明:
网络结构
-
block2
块- 包含 4 个
block1
块,每个block1
块的结构如下:nn.Linear(4, 8)
:输入维度 4,输出维度 8 的全连接层。nn.ReLU()
:ReLU 激活函数。nn.Linear(8, 4)
:输入维度 8,输出维度 4 的全连接层。nn.ReLU()
:ReLU 激活函数。
- 包含 4 个
-
最终的全连接层
nn.Linear(4, 1)
:输入维度 4,输出维度 1 的全连接层。
层级关系
rgnet
是一个nn.Sequential
容器,包含两个主要部分:block2
和一个最终的全连接层。block2
本身也是一个nn.Sequential
容器,包含 4 个block1
块。- 每个
block1
块是一个nn.Sequential
容器,包含两个全连接层和两个 ReLU 激活函数。
具体结构
以下是 rgnet
的具体结构示意图:
rgnet((0): block2((block 0): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 1): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 2): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU())(block 3): block1((0): Linear(4 -> 8)(1): ReLU()(2): Linear(8 -> 4)(3): ReLU()))(1): Linear(4 -> 1)
)
前向传播流程
假设输入数据 X
的形状为 (batch_size, 4)
,以下是前向传播的具体流程:
-
第一个
block1
块- 输入:
X
(形状(batch_size, 4)
) - 第一层全连接层:
nn.Linear(4, 8)
,输出形状为(batch_size, 8)
- ReLU 激活函数:对输出应用 ReLU 激活,输出形状仍为
(batch_size, 8)
- 第二层全连接层:
nn.Linear(8, 4)
,输出形状为(batch_size, 4)
- ReLU 激活函数:对输出应用 ReLU 激活,输出形状仍为
(batch_size, 4)
- 输入:
-
第二个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
第三个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
第四个
block1
块- 输入:上一个块的输出(形状
(batch_size, 4)
) - 重复第一个块的处理过程,输出形状仍为
(batch_size, 4)
- 输入:上一个块的输出(形状
-
最终的全连接层
- 输入:最后一个
block1
块的输出(形状(batch_size, 4)
) - 全连接层:
nn.Linear(4, 1)
,输出形状为(batch_size, 1)
- 输入:最后一个
1.3.3 输出
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)
)
1.3.4 参数访问
rgnet[0][1][0].bias.data
tensor([ 0.1999, -0.4073, -0.1200, -0.2033, -0.1573, 0.3546, -0.2141, -0.2483])
2. 参数初始化
深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。
2.1 内置初始化
将所有权重参数初始化为标准差为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)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([1., 1., 1., 1.]), tensor(0.))
Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42
def init_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)net[0].apply(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
tensor([ 0.5236, 0.0516, -0.3236, 0.3794])
tensor([[42., 42., 42., 42., 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_将张量的值填充为从均匀分布中采样的随机数nn.init.uniform_(m.weight, -10, 10)# 对权重参数进行筛选和调整。具体来说,它会将权重参数矩阵 m.weight.data 中绝对值大于等于 5 的元素保留,而将绝对值小于 5 的元素设置为 0 m.weight.data *= m.weight.data.abs() >= 5# 由于 weight 为均匀分布,所以数值在[-5,5]的概率为1/2,[-10,-5]的概率为1/4,[5,10]的概率为1/4;net.apply(my_init)
net[0].weight[:2]
Init weight torch.Size([8, 4])
Init weight torch.Size([1, 8])
tensor([[5.4079, 9.3334, 5.0616, 8.3095],
[0.0000, 7.2788, -0.0000, -0.0000]], grad_fn=)
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
tensor([42.0000, 10.3334, 6.0616, 9.3095])
实现特定的初始化分布 :在前面的例子中,定义的初始化分布要求权重值以 1/4 的概率从 U(5,10) 中取值,以 1/2 的概率取 0,以 1/4 的概率从 U(−10,−5) 中取值。通过将权重初始化为 U(−10,10) 内的随机数,然后再执行 m.weight.data *= m.weight.data.abs() >=5,可以筛选出绝对值大于等于 5 的权重值(来自 U(5,10) 或 U(−10,−5) 的分布),而将绝对值小于 5 的权重值设置为 0,从而近似实现目标的初始化分布。
这种方法并不能完全精确地实现目标分布,因为从 U(−10,10) 中采样的权重值经过筛选后,可能会导致最终的权重值分布与目标分布存在一定差异。
3 参数绑定
定义一个稠密层,然后使用它的参数来设置另一个层的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU(),shared, nn.ReLU(),nn.Linear(8, 1))
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([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
3.1 定义共享层
shared = nn.Linear(8, 8)
首先定义了一个稠密层 shared = nn.Linear(8, 8),这个层的参数将被其他层共享。注意要给共享层一个名称,以便后续可以引用它的参数。
3.2 构建网络结构
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU(),shared, nn.ReLU(),nn.Linear(8, 1))
通过 nn.Sequential 构建了一个包含多个层的神经网络 net,其中包括两个 Linear 层和多个 ReLU 激活函数层。其中,第三个、第五个神经网络层(即第二个和第三个隐藏层)使用了 shared 层的参数。
3.3 检查参数是否相同
通过比较 net[2].weight.data[0] 和 net[4].weight.data[0],发现它们的值是相同的,这表明这两个层的权重参数是绑定在一起的
print(net[2].weight.data[0] == net[4].weight.data[0])
3.4 修改参数并验证绑定关系
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])
将 net[2].weight.data[0, 0] 设置为 100 后,再次比较 net[2].weight.data[0] 和 net[4].weight.data[0],发现它们的值仍然相同,这说明它们实际上是同一个对象,而不是仅仅有相同的值。
第一次比较的结果 tensor([True, True, True, True, True, True, True, True, True]) 表示第三个和第五个层的权重参数在初始化后是相同的。
第二次比较的结果同样显示为 tensor([True, True, True, True, True, True, True, True, True]),这进一步验证了它们共享的是同一个参数对象,因为当修改其中一个参数时,另一个参数也随之改变。
3.5 梯度累加
当参数被绑定时,在反向传播过程中,梯度会累加。由于模型参数包含梯度,所以在反向传播期间,第二个隐藏层(即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起,共同影响参数的更新。
总结来说,参数绑定是一种在深度学习中用于共享参数的技术,可以在多个层之间实现参数的共享,减少模型参数量,提高参数的利用率。在 PyTorch 中,通过定义共享层并将其用于多个位置,可以轻松实现参数绑定。同时,需要注意的是,绑定参数在反向传播时梯度会累加。