首先关注具有单隐藏层的多层感知机:
1 2 3 4 5 6
| 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)) net(X)
|
1 2
| tensor([[0.2631], [0.2516]], grad_fn=<AddmmBackward>)
|
参数访问
从已有模型中访问参数。 当通过Sequential
类定义模型时, 可以通过索引来访问模型的任意层。如下所示可以检查第二个全连接层的参数:
1
| print(net[2].state_dict())
|
1
| OrderedDict([('weight', tensor([[ 0.2981, -0.1294, -0.3337, -0.1456, -0.0394, -0.1074, -0.1130, -0.0029]])), ('bias', tensor([0.3518]))])
|
参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。
目标参数
每个参数都表示为参数类的一个实例。 要对参数执行任何操作,首先需要访问底层的数值。如下从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值:
1 2 3
| print(type(net[2].bias)) print(net[2].bias) print(net[2].bias.data)
|
1 2 3 4
| <class 'torch.nn.parameter.Parameter'> Parameter containing: tensor([0.3518], requires_grad=True) tensor([0.3518])
|
参数是复合的对象,包含值、梯度和额外信息。在上面这个网络中,由于我们还没有调用反向传播,所以参数的梯度处于初始状态。
1
| 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()])
|
1 2
| ('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]))
|
1
| net.state_dict()['2.bias'].data
|
从嵌套块收集参数
1 2 3 4 5 6 7 8 9 10 11 12
| 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)) rgnet(X)
|
1 2
| tensor([[-0.1783], [-0.1783]], grad_fn=<AddmmBackward>)
|
设计了网络后,我们看看它是如何工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| 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
| rgnet[0][1][0].bias.data
|
1
| tensor([-0.0445, -0.3696, 0.4152, 0.3950, -0.3007, -0.3715, -0.4471, 0.2216])
|
参数初始化
深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。
默认情况下,PyTorch 会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch 的nn.init
模块提供了多种预置初始化方法。
内置初始化
如下将所有权重参数初始化为标准差为 0.01 的高斯随机变量,且将偏置参数设置为 0:
1 2 3 4 5 6
| 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
| (tensor([ 0.0051, -0.0006, 0.0048, -0.0027]), tensor(0.))
|
还可以将所有参数初始化为给定的常数,比如初始化为 1:
1 2 3 4 5 6
| 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]
|
1
| (tensor([1., 1., 1., 1.]), tensor(0.))
|
还可以对某些块应用不同的初始化方法。如下使用 Xavier 初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值 42:
1 2 3 4 5 6 7 8 9 10 11
| def 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(xavier) net[2].apply(init_42) print(net[0].weight.data[0]) print(net[2].weight.data)
|
1 2
| tensor([-0.5318, -0.5612, 0.0279, 0.5014]) tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])
|
自定义初始化
1 2 3 4 5 6 7 8 9
| 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() >= 5
net.apply(my_init) net[0].weight[:2]
|
1 2 3 4 5
| Init weight torch.Size([8, 4]) Init weight torch.Size([1, 8])
tensor([[9.6765, 8.2187, -0.0000, -0.0000], [8.0768, -0.0000, -0.0000, 0.0000]], grad_fn=<SliceBackward>)
|
1 2 3
| net[0].weight.data[:] += 1 net[0].weight.data[0, 0] = 42 net[0].weight.data[0]
|
1
| tensor([42.0000, 9.2187, 1.0000, 1.0000])
|
参数绑定
有时我们希望在多个层间共享参数:可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
1 2 3 4 5 6 7 8 9 10 11 12
| 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])
|
1 2
| tensor([True, True, True, True, True, True, True, True]) tensor([True, True, True, True, True, True, True, True])
|
表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。
当参数绑定时,由于模型参数包含梯度,因此在反向传播期间第二个隐藏层(即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。
总结
- 我们有几种方法可以访问、初始化和绑定模型参数。
- 我们可以使用自定义初始化方法。