在深度学习模型训练过程中,模型的参数扮演着至关重要的角色。了解如何管理这些参数不仅能帮助我们更好地调试和优化模型,还能提升我们在多任务学习和复用模型时的效率。本文将详细讲解如何访问、初始化和共享模型参数,并结合示例进行演示,帮助你深入理解这些概念。
1. 参数访问
模型的参数包括权重和偏置,通常这些参数在网络中是以层的形式组织的。使用深度学习框架(如PyTorch),我们可以通过多种方法来访问这些参数。首先,我们来看一个简单的网络模型示例,并从中提取参数。
1.1 访问模型的层参数
假设我们定义了一个具有单隐藏层的多层感知机(MLP),代码如下:
import torch
from torch import nn
net = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 1)
)
X = torch.rand(size=(2, 4))
print(net(X))
"""
tensor([[-0.0241],
[-0.0680]], grad_fn=<AddmmBackward0>)
"""
在这个网络中,我们有两个全连接层。我们可以通过下标索引来访问这些层的参数。例如,访问第二个全连接层的参数,可以使用:
from pprint import pprint
pprint(net[2].state_dict())
输出的结果会包含该层的权重和偏置:
OrderedDict([('weight',
tensor([[-0.2753, 0.2646, 0.1241, 0.3173, 0.1059, 0.1603, 0.0890, 0.0554]])),
('bias', tensor([-0.1769]))])
这里我们看到,该层的权重和偏置都以张量的形式存在,并且都存储为浮动精度(float32)类型。
1.2 访问单一参数
每个参数(如权重或偏置)本身是一个复合对象,包含了其数值、梯度等信息。要提取参数的数值,我们可以使用.data属性。例如,提取第二个全连接层的偏置:
print(type(net[2].bias)) # <class 'torch.nn.parameter.Parameter'>
print(net[2].bias)
"""
Parameter containing:
tensor([-0.1134], requires_grad=True)
"""
print(net[2].bias.data) # tensor([-0.1134])
- “Parameter containing” 表示该对象包含某些参数(例如模型的权重或偏置)
1.3 一次性访问所有参数
如果我们需要同时访问所有的参数,可以使用 named_parameters() 方法,它会列出模型中所有层的名称及其对应的参数。比如:
for name, parm in net.named_parameters():
print(name, parm.shape)
print(parm)
print('-' * 70)
这将输出:
0.weight torch.Size([8, 4])
Parameter containing:
tensor([[-0.2725, -0.3014, -0.0171, 0.3765],
[-0.0011, 0.2146, -0.2464, -0.0642],
[ 0.1258, 0.4297, 0.1078, -0.1847],
[-0.2725, -0.4197, 0.1106, 0.1669],
[-0.3968, 0.2001, -0.3576, -0.2678],
[ 0.2813, -0.0061, -0.2298, -0.2815],
[ 0.4617, 0.0035, 0.3447, -0.2951],
[-0.3451, 0.1828, 0.3609, -0.2830]], requires_grad=True)
----------------------------------------------------------------------
0.bias torch.Size([8])
Parameter containing:
tensor([ 0.4076, 0.3002, -0.4728, 0.4319, 0.1131, 0.4168, 0.4548, 0.3726],
requires_grad=True)
----------------------------------------------------------------------
2.weight torch.Size([1, 8])
Parameter containing:
tensor([[-0.0185, -0.0178, 0.0514, -0.0675, 0.1043, -0.1932, 0.0014, 0.1806]],
requires_grad=True)
----------------------------------------------------------------------
2.bias torch.Size([1])
Parameter containing:
tensor([0.3103], requires_grad=True)
----------------------------------------------------------------------
这种方法特别适合处理较大或复杂的模型,其中包含多个层和参数。
1.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 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].bias.data)
输出的偏置数据如下:
tensor([-0.0140, -0.4541, -0.3257, 0.1100, -0.2222, 0.0812, 0.2502, 0.1304])
2. 参数初始化
初始化参数是深度学习中非常重要的步骤。如果初始化不当,可能会导致模型收敛缓慢,甚至无法收敛。PyTorch提供了多种常见的初始化方法,包括默认初始化和自定义初始化。
2.1 内置初始化
PyTorch为常见的层提供了预设的初始化方法。下面是如何使用 标准高斯分布 初始化权重,并将偏置初始化为零的例子:
net = nn.Sequential(
nn.Linear(4, 8),
nn.ReLU(),
nn.Linear(8, 1)
)
def init_normal(m):
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, mean=0, std=0.01)
nn.init.zeros_(m.bias)
net.apply(init_normal)
输出结果显示了权重和偏置的初始化状态:
print(net[0].weight.data)
"""
tensor([[-0.0006, 0.0029, 0.0085, 0.0023],
[-0.0149, 0.0076, -0.0151, -0.0008],
[ 0.0048, 0.0017, -0.0113, 0.0013],
[ 0.0018, -0.0023, -0.0064, -0.0059],
[-0.0109, -0.0024, -0.0052, -0.0046],
[ 0.0037, 0.0060, -0.0054, -0.0039],
[ 0.0184, -0.0035, 0.0147, 0.0029],
[ 0.0043, -0.0056, -0.0115, 0.0004]])
"""
print(net[0].bias.data)
"""
tensor([0., 0., 0., 0., 0., 0., 0., 0.])
"""
2.2 自定义初始化
如果默认的初始化方法不符合我们的需求,我们也可以定义自定义的初始化方法。例如,使用均匀分布对权重进行初始化,并设置特定的规则:
def my_init(m):
if isinstance(m, nn.Linear):
nn.init.uniform_(m.weight, -10, 10)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
这会根据自定义的规则调整参数值。
print(net[0].weight.data)
"""
tensor([[-0.0000, -9.3257, -8.3078, 9.2833],
[ 0.0000, 5.0431, 0.0000, -0.0000],
[ 5.4107, 5.2934, -9.1508, -0.0000],
[ 0.0000, 0.0000, -6.4744, 0.0000],
[-7.5632, -6.3391, 0.0000, -0.0000],
[-9.3502, 0.0000, 9.2711, 0.0000],
[-8.3392, -6.3863, 0.0000, -7.2430],
[ 7.2809, 0.0000, 0.0000, 0.0000]])
"""
2.3 结合多种初始化方法
有时我们希望为模型的不同部分使用不同的初始化方法。下面的示例展示了如何对不同的层应用不同的初始化:
def init_xavier(m):
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
def init_42(m):
if isinstance(m, nn.Linear):
nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_42)
- Xavier均匀初始化 (
xavier_uniform_)- 使用均匀分布初始化权重,范围为 ,其中 和 分别是该层的输入和输出节点数。
constant_方法会将指定的张量的所有元素都设置为同一个常数值。这个常数值可以由用户指定,默认通常是 0。
print(net[0].weight.data)
"""
tensor([[-0.4288, -0.0551, 0.5148, -0.6777],
[ 0.4790, 0.6359, -0.2876, -0.5548],
[ 0.4360, -0.0825, -0.4318, -0.2367],
[ 0.2139, 0.4846, -0.2292, -0.3446],
[ 0.1689, -0.1191, 0.4892, 0.5820],
[ 0.0284, -0.2763, -0.4984, -0.6381],
[ 0.4041, -0.6052, 0.3808, -0.1873],
[-0.1200, -0.5748, 0.1224, -0.1868]])
"""
print(net[2].weight.data)
"""
tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
"""
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)
)
通过这种方式,我们在模型的不同部分使用相同的参数。你可以通过以下代码验证参数是否绑定:
print(net[2].weight.data == net[4].weight.data)
"""
tensor([[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True],
[True, True, True, True, True, True, True, True]])
"""
print(net[2].bias.data == net[4].bias.data)
"""
tensor([True, True, True, True, True, True, True, True])
"""
当我们修改其中一个参数时,另一个参数也会随之改变:
net[2].weight.data[0, 0] = 100
print(net[2].weight.data[0])
"""
tensor([ 1.0000e+02, -4.3048e-02, -2.2559e-01, -2.5617e-01, 2.4563e-01,
7.6674e-02, 1.1903e-01, 2.4937e-01])
"""
print(net[2].weight.data[0] == net[4].weight.data[0])
"""
tensor([True, True, True, True, True, True, True, True])
"""
4. 小结
通过本文的学习,我们了解了如何访问、初始化和共享深度学习模型中的参数。在实践中,这些技巧对于调试和优化模型、提高复用性和可移植性都非常重要。你可以根据具体任务选择合适的参数管理方法,以获得更好的性能和效果。