动手学人工智能-深度学习计算2-参数管理

256 阅读6分钟

在深度学习模型训练过程中,模型的参数扮演着至关重要的角色。了解如何管理这些参数不仅能帮助我们更好地调试和优化模型,还能提升我们在多任务学习和复用模型时的效率。本文将详细讲解如何访问、初始化和共享模型参数,并结合示例进行演示,帮助你深入理解这些概念。

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_)
    • 使用均匀分布初始化权重,范围为 [6/(nin+nout),6/(nin+nout)][-\sqrt{6 / (n_{\text{in}} + n_{\text{out}})}, \sqrt{6 / (n_{\text{in}} + n_{\text{out}})}],其中 ninn_{\text{in}}noutn_{\text{out}} 分别是该层的输入和输出节点数。
  • 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. 小结

通过本文的学习,我们了解了如何访问、初始化和共享深度学习模型中的参数。在实践中,这些技巧对于调试和优化模型、提高复用性和可移植性都非常重要。你可以根据具体任务选择合适的参数管理方法,以获得更好的性能和效果。