在选择了模型架构,并设置了超参数之后,就进入了训练阶段,此时,我们的目标是找到使损失函数最小化的模型参数值。 经过训练后,我们将需要使用这些参数来做出未来的预测。
此外,有时我们希望提取参数,以便在其他环境中复用它们, 将模型保存下来,以便它可以在其他软件中执行, 或者为了获得科学的理解而进行检查。
本章将介绍:
- 访问参数、用于调试、诊断和可视化
- 参数初始化
- 在不同模型组件间共享参数
以单隐藏层的多层感知机为例:
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)
运行结果:
1 参数访问
从已有模型中访问参数。 当通过Sequential类定义模型时, 我们可以通过索引来访问模型的任意层。 这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查第二个全连接层的参数。
print(net[2].state_dict())
运行结果:
输出了权重矩阵和偏置
2 目标参数
也可以选择性访问,如下代码:
print(type(net[2].bias))
print(net[2].weight)
print(net[2].bias.data)
运行结果:
或者使用下面的代码:
net.state_dict()['2.bias'].data
运行结果:
3 一次性访问所有参数
当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦,因为需要递归整个树来提取每个子块的参数,如下代码:
print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
.named_parameters()
:调用这个方法会返回一个迭代器,其中每个元素都是一个元组,包含两个值:参数的名字(字符串类型)和参数本身(一个张量)。*
操作符:在print
函数前使用*是为了将列表中的元素解包,这样print
函数就可以直接打印出列表中的每个元素,而不是整个列表对象。
运行结果
4 从嵌套快收集参数
如果我们有多个块互相嵌套,那如何获取呢?我们先假设一个嵌套网络:
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 netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
设计好网络之后,我们可以通过print
查看网络结果:
print(rgnet)
假设我们访问(0)
块中的block 0
块中的(0)
层的偏置项
rgnet[0][1][0].bias.data
5 参数初始化
默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的nn.init模块提供了多种预置初始化方法。
5.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]
我们还可以将所有参数初始化为给定的常数,比如初始化为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)
net[0].weight.data[0], net[0].bias.data[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)# 对第一个使用Xavier初始化
net[2].apply(init_42)# 对第三个使用常量初始化
print(net[0].weight.data[0])
print(net[2].weight.data)
5.2 自定义初始化
如果框架中没有我们需要的初始化方法,则需要自定义初始化,例如我们将使用下面的分布做初始化: w ∼ { U ( 5 , 10 ) 可能性 1 4 0 可能性 1 2 U ( − 10 , − 5 ) 可能性 1 4 \begin{split}\begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned}\end{split} w∼⎩ ⎨ ⎧U(5,10)0U(−10,−5) 可能性 41 可能性 21 可能性 41
即:
情况1:
- w 从均匀分布 U ( 5 , 10 ) 中取值 w从均匀分布U(5,10)中取值 w从均匀分布U(5,10)中取值
- 概率为 1 4 概率为\frac{1}{4} 概率为41
情况2:
- w = 0 w=0 w=0
- 概率为 1 2 概率为\frac{1}{2} 概率为21
情况3:
- w 从均匀分布 U ( − 10 , − 5 ) 中取值 w从均匀分布U(-10,-5)中取值 w从均匀分布U(−10,−5)中取值
- 概率为 1 4 概率为\frac{1}{4} 概率为41
使用下面代码来展示:
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() >= 5net.apply(my_init)
net[0].weight[:2]
1、nn.init.uniform_(m.weight, -10, 10)
将权重参数m.weight
初始化为从均匀分布中 U ( − 10 , 10 ) U(-10,10) U(−10,10)中抽取的值
2、然后通过m.weight.data *= m.weight.data.abs() >= 5
这串代码实现了概率分布的效果,让我们逐步分析:
m.weight.data.abs()
:这个操作计算m.weight
中每个元素的绝对值,返回一个与m.weight
形状相同的张量,其中每个元素是原权重的绝对值。m.weight.data.abs() >= 5
:这个操作生成一个布尔张量,其中每个元素表示对应位置的权重绝对值是否大于或等于 5。结果是一个与 m.weight 形状相同的布尔张量,值为 True 或 False。m.weight.data *= m.weight.data.abs() >= 5
:这个操作将 m.weight 中的每个元素与其对应的布尔值相乘。在 Python 和 PyTorch 中,布尔值 True 被视为 1,False 被视为 0。
因此,如果某个权重的绝对值大于或等于 5,布尔值为 True,乘法结果不变;如果某个权重的绝对值小于 5,布尔值为 False,乘法结果为 0。
概率分析:
- 在 -10 到 10 之间的均匀分布中,权重落在 -10 到 -5 之间的概率是 25%(因为区间长度为 5,总区间长度为 20)。
- 权重落在 5 到 10 之间的概率是 25%。
- 权重落在 -5 到 5 之间的概率是 50%(因为区间长度为 10,总区间长度为 20)。
经过 m.weight.data *= m.weight.data.abs() >= 5
操作后:
- 权重在 -10 到 -5 之间的概率仍然是 25%,因为这些权重被保留。
- 权重在 5 到 10 之间的概率仍然是 25%,因为这些权重被保留。
- 权重在 -5 到 5 之间的概率变为 0,因为这些权重被设置为 0,因此,权重为 0 的概率是 50%。
6 参数绑定
有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
# 我们需要给共享层一个名称,以便可以引用它的参数
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])
输出结果:
共享参数通常可以节省内存