PyTorch 参数管理#

参考:parameters

以单隐藏层的多层感知机为例展开。

import torch
from torch import nn

net = nn.Sequential(nn.LazyLinear(8), 
                    nn.ReLU(), 
                    nn.LazyLinear(1))
X = torch.rand(size=(2, 4))
net(X)
tensor([[0.2668],
        [0.2172]], grad_fn=<AddmmBackward0>)

参数访问#

从已有模型中访问参数。当通过 Sequential 类定义模型时,可以通过索引来访问模型的任意层。这就像模型是列表一样,每层的参数都在其属性中。

net[2].state_dict()
OrderedDict([('weight',
              tensor([[ 0.0733,  0.2496,  0.1756,  0.3398, -0.2641,  0.2822,  0.1442, -0.0509]])),
             ('bias', tensor([-0.0469]))])

目标参数#

每个参数都表示为参数类的实例。要对参数执行运算,需要访问底层的数值。

type(net[2].bias), net[2].bias.data
(torch.nn.parameter.Parameter, tensor([-0.0469]))

参数是复合的对象,包含值、梯度和额外信息。除了值之外,还可以访问每个参数的梯度。

net[2].weight.grad == None # 未 backward
True

一次性访问所有参数#

方法一:

[(name, param.shape) for name, param in net.named_parameters()]
[('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.0469])

参数绑定#

希望跨多个层共享参数。

比如分配一个完全连接的层,然后使用它的参数专门设置另一个层的参数。在这里,需要在访问参数之前运行 forward 传播 net(X)

# 需要给共享层一个名称,以便可以引用它的参数
shared = nn.LazyLinear(8)
net = nn.Sequential(nn.LazyLinear(8), nn.ReLU(),
                    shared, nn.ReLU(),
                    shared, nn.ReLU(),
                    nn.LazyLinear(1))
net(X)
tensor([[0.1414],
        [0.1413]], grad_fn=<AddmmBackward0>)

检查参数是否一致:

print(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
tensor([True, True, True, True, True, True, True, True])

确保它们实际上是相同的对象,而不是具有相同的值:

print(net[2].weight.data[0] == net[4].weight.data[0])
tensor([True, True, True, True, True, True, True, True])

参数初始化#

默认情况下,PyTorch 通过从根据输入和输出维度计算的范围中绘图来统一初始化权重和偏差矩阵。PyTorch 的 init 模块提供了多种预设初始化方法。

内置初始化#

下面的代码将所有权重参数初始化为标准差为 \(0.01\) 的高斯随机变量,且将偏置参数设置为 \(0\)

def init_normal(module):
    if type(module) == nn.Linear:
        nn.init.normal_(module.weight, mean=0, std=0.01)
        nn.init.zeros_(module.bias)
net.apply(init_normal)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([ 0.0019, -0.0142, -0.0020, -0.0116]), tensor(0.))

还可以将所有参数初始化为给定的常数,比如初始化为 \(1\)

def init_constant(module):
    if type(module) == nn.Linear:
        nn.init.constant_(module.weight, 1)
        nn.init.zeros_(module.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
(tensor([1., 1., 1., 1.]), tensor(0.))

还可以对某些块应用不同的初始化方法。 例如,下面使用 Xavier 初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值 \(42\)

def init_xavier(module):
    if type(module) == nn.Linear:
        nn.init.xavier_uniform_(module.weight)
def init_42(module):
    if type(module) == nn.Linear:
        nn.init.constant_(module.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.3498, -0.5610, -0.6487, -0.1923])
tensor([[42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.],
        [42., 42., 42., 42., 42., 42., 42., 42.]])

自定义初始化#

比如自定义:

\[\begin{split} \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} \end{split}\]
def my_init(module):
    if type(module) == nn.Linear:
        print("Init", *[(name, param.shape)
                        for name, param in module.named_parameters()][0])
        nn.init.uniform_(module.weight, -10, 10)
        module.weight.data *= module.weight.data.abs() >= 5

net.apply(my_init)
net[0].weight[:2]
Init weight torch.Size([8, 4])
Init weight torch.Size([8, 8])
Init weight torch.Size([1, 8])
tensor([[-9.2266, -0.0000, -0.0000, -5.3230],
        [ 0.0000, -0.0000,  0.0000,  9.7535]], grad_fn=<SliceBackward0>)

注意,始终可以直接设置参数。

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
tensor([42.0000,  1.0000,  1.0000, -4.3230])

延迟初始化#

只有当对数据进行推理时才初始化完成。

import torch
from torch import nn

net = nn.Sequential(nn.LazyLinear(256), nn.ReLU(), nn.LazyLinear(10))

此时,网络不可能知道输入层权值的维度,因为输入维度仍然未知。因此,框架还没有初始化任何参数。

通过尝试访问下面的参数来确认。

net[0].weight
<UninitializedParameter>

接下来通过网络传递数据,使框架最终初始化参数。

X = torch.rand(2, 20)
net(X)

net[0].weight.shape
torch.Size([256, 20])

一旦知道了输入维度 \(20\),框架就可以通过代入值 \(20\) 来识别第一层权重矩阵的形状。在识别了第一层的形状后,框架继续到第二层,通过计算图如此循环,直到知道所有的形状。注意,在这种情况下,只有第一层需要延迟初始化,但是框架会按顺序初始化。一旦知道了所有的参数形状,框架就可以最终初始化参数。

下面的方法通过网络传入虚拟输入进行演练,以推断所有参数的形状,然后初始化参数。当不需要默认的随机初始化时,将稍后使用它。

from torch_book.utils import add_to_class
from torch_book.model import Module

@add_to_class(Module)
def apply_init(self, inputs, init=None):
    self.forward(*inputs)
    if init is not None:
        self.net.apply(init)

小技巧

延迟初始化很方便,允许框架自动推断参数形状,使修改体系结构变得容易,并消除一个常见的错误来源。

可以通过模型传递数据,使框架最终初始化参数。