参数管理
引用翻译:《动手学深度学习》
训练深度网络的最终目的是为一个给定的架构找到好的参数值。当一切都很标准时,torch.nn.Sequential类是一个完全好的工具。然而,很少有模型是完全标准的,大多数科学家都想建立一些新颖的东西。本节展示了如何操作参数。特别是我们将涵盖以下几个方面。
1、为调试、诊断、使其可视化或保存而访问参数,是了解如何使用自定义模型的第一步。
2、其次,我们要以特定的方式设置它们,例如为了初始化目的。我们讨论参数初始化器的结构。
3、最后,我们展示了如何通过构建共享一些参数的网络来好好利用这些知识。
像往常一样,我们从我们可靠的多层感知器的隐藏层开始。这将作为我们演示各种特征的选择。
import torch
import torch.nn as nn
net = nn.Sequential()
net.add_module('Linear_1', nn.Linear(20, 256, bias = False))
net.add_module('relu', nn.ReLU())
net.add_module('Linear_2', nn.Linear(256, 10, bias = False))
# init_weights函数初始化了我们的多层感知器的权重。
def init_weights(m):
if type(m) == nn.Linear:
# torch.nn.init.uniform_(tensor, a=0, b=1) 均匀分布
torch.nn.init.xavier_uniform_(m.weight) # xavier_uniform 初始化
# net.apply()将上述的权重初始化应用到我们的网络中。
net.apply(init_weights)
x = torch.randn(2,20) # 初始化一个形状为(2,20)的随机张量
net(x) # 正向计算
tensor([[-0.2317, -0.2656, -0.0401, -0.4802, -0.1889, -0.1938, -0.0283, -0.0264,
0.0874, -0.0913],
[-0.5336, -0.3225, -0.3935, 0.0444, 0.2943, 0.0468, -0.0306, -0.2153,
0.0810, -0.6742]], grad_fn=<MmBackward>)
torch的初始化函数
- 均匀分布
torch.nn.init.uniform_(tensor, a=0, b=1)
服从~U ( a , b ) U(a, b)U(a,b)
- 正态分布
torch.nn.init.normal_(tensor, mean=0, std=1)
服从~N ( m e a n , s t d ) N(mean, std)N(mean,std)
- 初始化为常数
torch.nn.init.constant_(tensor, val)
初始化整个矩阵为常数val
- xavier_uniform 初始化
torch.nn.init.xavier_uniform_(tensor, gain=1)使得网络中信息更好的流动,每一层输出的方差应该尽量相等。
一、参数访问
在序列类的情况下,我们可以轻松地访问参数,只需调用net.parameters。让我们在实践中通过检查参数来尝试一下。
print(net[0].parameters)
print(net[1].parameters)
print(net[2].parameters)
<bound method Module.parameters of Linear(in_features=20, out_features=256, bias=False)>
<bound method Module.parameters of ReLU()>
<bound method Module.parameters of Linear(in_features=256, out_features=10, bias=False)>
输出告诉我们一些事情。首先,有3个层;2个线性层和1个ReLU层,正如我们所期望的。输出还指明了我们所期望的线性层的形状。特别是参数的名称是非常有用的,因为它们使我们能够唯一地识别参数,即使是在一个有数百个层和非琐碎结构的网络中。另外,输出告诉我们,偏置是假的,因为我们指定了它。
二、有针对性的参数
为了对参数做一些有用的事情,我们需要访问它们。有几种方法可以做到这一点,从简单到一般。让我们看一下其中的一些
print(net[0].bias)
print(net.Linear_1.bias) # 可以通过名称来访问参数,例如Linear_1
None
None
它返回第一个线性层的偏置值。由于我们将偏置初始化为False,所以输出为None。我们也可以通过名称来访问参数,例如Linear_1。这两种方法是完全等价的,但第一种方法导致了更多可读的代码。
print(net.Linear_1.weight)
print(net[0].weight)
Parameter containing:
tensor([[-0.1001, -0.0466, 0.0356, ..., -0.0766, 0.0639, 0.1118],
[-0.0239, 0.1267, 0.0728, ..., 0.0152, 0.0814, 0.0845],
[-0.0845, 0.1245, 0.0755, ..., 0.1241, -0.0988, -0.0969],
...,
[-0.0436, 0.1032, 0.0760, ..., -0.0946, -0.0881, -0.0534],
[-0.0828, -0.0548, -0.0182, ..., 0.0545, 0.1288, 0.0158],
[-0.1297, 0.0077, 0.1301, ..., -0.0578, -0.1361, 0.0864]],
requires_grad=True)
Parameter containing:
tensor([[-0.1001, -0.0466, 0.0356, ..., -0.0766, 0.0639, 0.1118],
[-0.0239, 0.1267, 0.0728, ..., 0.0152, 0.0814, 0.0845],
[-0.0845, 0.1245, 0.0755, ..., 0.1241, -0.0988, -0.0969],
...,
[-0.0436, 0.1032, 0.0760, ..., -0.0946, -0.0881, -0.0534],
[-0.0828, -0.0548, -0.0182, ..., 0.0545, 0.1288, 0.0158],
[-0.1297, 0.0077, 0.1301, ..., -0.0578, -0.1361, 0.0864]],
requires_grad=True)
请注意,权重是不为零的。这是设计好的,因为我们对我们的网络应用了__Xavier initialization__初始化(Xavier初始化)。我们也可以计算出与参数有关的梯度。它的形状与权重相同。然而,由于我们还没有调用反向传播,输出是无。
print(net[0].weight.grad)
None
三、所有的参数都是一次性的
如上所述,访问参数可能有点乏味,特别是如果我们有更复杂的块,或块的块(甚至块的块的块),因为我们需要按照块的构造的相反顺序走过整个树。为了避免这种情况,块有一个方法_state_dict_,它在一个字典中抓取网络的所有参数,这样我们就可以轻松地遍历它。它通过遍历一个块的所有成分,并根据需要在子块上调用_state_dict_来做到这一点。为了看到区别,请看下面的例子。
print(net[0].state_dict) # 仅针对第一层的参数
print(net.state_dict) # 整个网络的参数
<bound method Module.state_dict of Linear(in_features=20, out_features=256, bias=False)>
<bound method Module.state_dict of Sequential(
(Linear_1): Linear(in_features=20, out_features=256, bias=False)
(relu): ReLU()
(Linear_2): Linear(in_features=256, out_features=10, bias=False)
)>
这为我们提供了访问网络参数的第三种方法。如果我们想得到第二个线性层的权重项的值,我们可以简单地使用这个。
net.state_dict()['Linear_1.weight']
tensor([[-0.1001, -0.0466, 0.0356, ..., -0.0766, 0.0639, 0.1118],
[-0.0239, 0.1267, 0.0728, ..., 0.0152, 0.0814, 0.0845],
[-0.0845, 0.1245, 0.0755, ..., 0.1241, -0.0988, -0.0969],
...,
[-0.0436, 0.1032, 0.0760, ..., -0.0946, -0.0881, -0.0534],
[-0.0828, -0.0548, -0.0182, ..., 0.0545, 0.1288, 0.0158],
[-0.1297, 0.0077, 0.1301, ..., -0.0578, -0.1361, 0.0864]])
四、Rube Goldberg再次出击
让我们看看,如果我们将多个区块嵌套在一起,参数命名惯例是如何工作的。为此,我们首先定义一个产生块的函数(可以说是一个块工厂),然后我们将这些块组合到更大的块中。
def block1():
net = nn.Sequential(nn.Linear(16, 32),
nn.ReLU(),
nn.Linear(32, 16),
nn.ReLU())
return net
def block2():
net = nn.Sequential()
for i in range(4):
net.add_module('block'+str(i), block1())
return net
rgnet = nn.Sequential()
rgnet.add_module('model',block2())
rgnet.add_module('Last_linear_layer', nn.Linear(16,10))
rgnet.apply(init_weights)
x = torch.randn(2,16)
print('rgnet(x):',rgnet(x)) # forward computation
print('rgnet:',rgnet)
rgnet(x): tensor([[ 0.2188, -0.2184, 0.7520, 0.0975, -0.1709, -0.3045, 0.2868, -0.1735,
-0.3061, -0.0471],
[ 0.2113, -0.1764, 0.6758, 0.0985, -0.1320, -0.2809, 0.2247, -0.1568,
-0.2409, -0.0644]], grad_fn=<AddmmBackward>)
rgnet: Sequential(
(model): Sequential(
(block0): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block1): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block2): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block3): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
)
(Last_linear_layer): Linear(in_features=16, out_features=10, bias=True)
)
现在我们已经完成了网络的设计,让我们看看它是如何组织的。state_dict为我们提供了这些信息,包括命名和逻辑结构方面的信息。
print('rgnet.parameters:\n',rgnet.parameters)
for param in rgnet.parameters():
print('param.size() 和 param.dtype:\t',param.size(), param.dtype) # 每一层都会输出size和dtype
rgnet.parameters:
<bound method Module.parameters of Sequential(
(model): Sequential(
(block0): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block1): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block2): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
(block3): Sequential(
(0): Linear(in_features=16, out_features=32, bias=True)
(1): ReLU()
(2): Linear(in_features=32, out_features=16, bias=True)
(3): ReLU()
)
)
(Last_linear_layer): Linear(in_features=16, out_features=10, bias=True)
)>
param.size() 和 param.dtype: torch.Size([32, 16]) torch.float32
param.size() 和 param.dtype: torch.Size([32]) torch.float32
param.size() 和 param.dtype: torch.Size([16, 32]) torch.float32
param.size() 和 param.dtype: torch.Size([16]) torch.float32
param.size() 和 param.dtype: torch.Size([32, 16]) torch.float32
param.size() 和 param.dtype: torch.Size([32]) torch.float32
param.size() 和 param.dtype: torch.Size([16, 32]) torch.float32
param.size() 和 param.dtype: torch.Size([16]) torch.float32
param.size() 和 param.dtype: torch.Size([32, 16]) torch.float32
param.size() 和 param.dtype: torch.Size([32]) torch.float32
param.size() 和 param.dtype: torch.Size([16, 32]) torch.float32
param.size() 和 param.dtype: torch.Size([16]) torch.float32
param.size() 和 param.dtype: torch.Size([32, 16]) torch.float32
param.size() 和 param.dtype: torch.Size([32]) torch.float32
param.size() 和 param.dtype: torch.Size([16, 32]) torch.float32
param.size() 和 param.dtype: torch.Size([16]) torch.float32
param.size() 和 param.dtype: torch.Size([10, 16]) torch.float32
param.size() 和 param.dtype: torch.Size([10]) torch.float32
由于各层是分层生成的,我们也可以相应地访问它们。例如,为了访问第一大块,在其内部访问第二子块,然后在其内部依次访问第一层的偏向,我们进行如下操作。
rgnet[0][1][0].bias.data # 类别json的结构,进行层级解析
tensor([ 0.2473, -0.2119, -0.2258, -0.0223, 0.1976, -0.1014, 0.1759, 0.1944,
0.2378, -0.1163, -0.2040, 0.2053, -0.1648, 0.2009, 0.1637, -0.0936,
-0.0420, -0.0799, 0.0532, 0.1738, 0.0988, 0.0652, -0.1181, -0.0317,
-0.1033, 0.0205, 0.1516, -0.1318, -0.0252, 0.2034, -0.0183, 0.1963])
五、参数初始化
现在我们知道了如何访问参数,让我们来看看如何正确地初始化它们。我们在《数值稳定性》一节中讨论了初始化的必要性。我们经常需要使用方法来初始化权重。PyTorch的init模块提供了各种预设的初始化方法,但如果我们想要一些不寻常的东西,我们需要做一些额外的工作。为了初始化单层的权重,我们使用__torch.nn.init__的一个函数。比如说
linear1 = nn.Linear(2,5,bias=True)
torch.nn.init.normal_(linear1.weight, mean=0, std =0.01) # 正态分布初始化
Parameter containing:
tensor([[ 0.0055, 0.0103],
[-0.0113, -0.0086],
[ 0.0092, 0.0130],
[-0.0112, -0.0057],
[-0.0082, 0.0087]], requires_grad=True)
如果我们想将所有参数初始化为1,我们可以简单地将初始化器改为Constant(1)
def init_weight(m):
if type(m) == nn.Linear:
torch.nn.init.normal_(m.weight) # 正态分布
net = nn.Sequential()
net.add_module('Linear_1', nn.Linear(2, 5, bias = False))
net.add_module('Linear_2', nn.Linear(5, 5, bias = False))
net.apply(init_weight)
print(net.state_dict()) # 输出每层的参数信息
print('net网络结构:\n',net)
OrderedDict([('Linear_1.weight', tensor([[ 0.7180, 0.0147],
[ 1.4062, -0.4186],
[-2.1873, -1.2932],
[ 0.7598, -0.8272],
[ 1.0651, -1.0501]])), ('Linear_2.weight', tensor([[ 0.5052, 0.1479, -1.0453, 2.5952, 2.4984],
[-1.0466, -1.2195, -1.6821, -0.5416, 0.4443],
[-0.1352, 0.6224, 0.1747, -1.8710, 0.7294],
[-0.3001, -0.5535, -1.5659, 0.2430, -0.9273],
[ 0.6440, -1.2982, 1.2207, -0.0210, 0.5043]]))])
net网络结构:
Sequential(
(Linear_1): Linear(in_features=2, out_features=5, bias=False)
(Linear_2): Linear(in_features=5, out_features=5, bias=False)
)
六、内置初始化
让我们从内置初始化器开始。下面的代码用高斯随机变量初始化了所有参数。
def gaussian_normal(m):
if type(m) == nn.Linear:
torch.nn.init.normal_(m.weight) # torch.nn.init.normal_(tensor, mean=0, std=1) 是正态,不加mean,std是高斯分布?
net.apply(gaussian_normal)
print(net[0].weight)
Parameter containing:
tensor([[ 0.5485, -1.5799],
[-1.9712, -1.5403],
[ 0.2594, 1.3496],
[-0.0527, -0.4650],
[ 0.2836, -2.0904]], requires_grad=True)
如果我们想把所有的参数都初始化为1,我们可以通过把初始化器改为torch.nn.init.constant(tensor,1)_来实现。
def ones(m):
if type(m) == nn.Linear:
torch.nn.init.constant_(m.weight, 1)
net.apply(ones)
print(net.state_dict())
OrderedDict([('Linear_1.weight', tensor([[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.],
[1., 1.]])), ('Linear_2.weight', tensor([[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.],
[1., 1., 1., 1., 1.]]))])
如果我们想以不同的方式只初始化一个特定的参数,我们可以简单地只为相应的子块(或参数)设置初始化器。
例如,下面我们将 second layer 第二层初始化为42的常量值,我们对第一层的权重使用Xavier初始化器。
# 先定义网络的块
block1 = nn.Sequential()
block1.add_module('Linear_1', nn.Linear(2,5,bias=False))
block2 = nn.Sequential()
block2.add_module('Linear_2', nn.Linear(5,5,bias=False))
# 对model添加块
model = nn.Sequential()
model.add_module('first', block1)
model.add_module('second', block2)
print('网络结构model:\n',model)
def xavier_normal(m):
if type(m) == nn.Linear:
torch.nn.init.xavier_uniform_(m.weight)
def init_42(m):
if type(m) == nn.Linear:
torch.nn.init.constant_(m.weight, 42)
block1.apply(xavier_normal)
block2.apply(init_42)
print('model网络参数:\n',model.state_dict())
网络结构model:
Sequential(
(first): Sequential(
(Linear_1): Linear(in_features=2, out_features=5, bias=False)
)
(second): Sequential(
(Linear_2): Linear(in_features=5, out_features=5, bias=False)
)
)
model网络参数:
OrderedDict([('first.Linear_1.weight', tensor([[-0.1454, 0.5888],
[ 0.4075, -0.0692],
[-0.4676, 0.9132],
[-0.6148, 0.7955],
[-0.6740, 0.8035]])), ('second.Linear_2.weight', 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.]]))])
七、自定义初始化
有时,我们需要的初始化方法并没有在init模块中提供。在这一点上,我们可以通过编写所需的函数来实现我们想要的实现,并使用它们来初始化权重。在下面的例子中,我们选择了一个明显的奇怪的非琐碎的分布,只是为了证明这一点。我们从以下分布中抽取系数:
# 类似矩阵计算的方式,通过for循环遍历,根据条件机械能重新赋值
def custom(m):
torch.nn.init.uniform_(m[0].weight, -10,10)
for i in range(m[0].weight.data.shape[0]):
for j in range(m[0].weight.data.shape[1]):
if m[0].weight.data[i][j]<=5 and m[0].weight.data[i][j]>=-5:
m[0].weight.data[i][j]=0
m = nn.Sequential(nn.Linear(5,5,bias=False))
custom(m)
print(m.state_dict())
OrderedDict([('0.weight', tensor([[-5.1040, 5.7689, 0.0000, -9.8498, 0.0000],
[ 0.0000, -9.0672, -6.1954, 9.9243, 9.6158],
[ 5.0936, 0.0000, 8.7101, 7.1981, 0.0000],
[ 6.6027, 0.0000, 6.3786, 0.0000, -5.5376],
[ 0.0000, 0.0000, 8.0790, 0.0000, 0.0000]]))])
如果连这个功能都不能满足要求,我们可以直接设置参数。由于.data返回的是一个张量,我们可以像其他矩阵一样访问它。
m[0].weight.data +=1 # 整体都加1
m[0].weight.data[0][0] = 42 # 类似矩阵直接赋值
m[0].weight.data
tensor([[42.0000, 6.7689, 1.0000, -8.8498, 1.0000],
[ 1.0000, -8.0672, -5.1954, 10.9243, 10.6158],
[ 6.0936, 1.0000, 9.7101, 8.1981, 1.0000],
[ 7.6027, 1.0000, 7.3786, 1.0000, -4.5376],
[ 1.0000, 1.0000, 9.0790, 1.0000, 1.0000]])
八、分享参数
在某些情况下,我们希望在多个层之间共享模型参数。例如,当我们想找到好的单词嵌入时,我们可能会决定在单词的编码和解码中都使用相同的参数。 让我们看看如何更优雅地做到这一点。在下文中,我们分配了一个线性层,然后多次使用它来分享权重。
如果我们传入Sequential的模块是同一个Module实例的话参数也是共享的
# 我们需要给共享层一个名字,以便我们可以引用它的参数
shared = nn.Sequential()
shared.add_module('linear_shared', nn.Linear(8,8,bias=False))
shared.add_module('relu_shared', nn.ReLU())
net = nn.Sequential(nn.Linear(20,8,bias=False),
nn.ReLU(),
shared,
shared,
nn.Linear(8,10,bias=False))
print('net网络结构:\n',net) # net网络结构
net.apply(init_weights)
print(net[2][0].weight==net[3][0].weight) # 应用判断语句,转化为True的形式,判断第二层、第三层网络参数是否相同
net网络结构:
Sequential(
(0): Linear(in_features=20, out_features=8, bias=False)
(1): ReLU()
(2): Sequential(
(linear_shared): Linear(in_features=8, out_features=8, bias=False)
(relu_shared): ReLU()
)
(3): Sequential(
(linear_shared): Linear(in_features=8, out_features=8, bias=False)
(relu_shared): ReLU()
)
(4): Linear(in_features=8, out_features=10, bias=False)
)
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]])
表明第二层和第三层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 在内存中,这两个线性层其实一个对象。
因此,如果我们改变其中一个参数,另一个参数也会改变。
你可能会思考:当参数绑定时,梯度会发生什么情况? 答案是由于模型参数包含梯度, 因此在反向传播期间第二个隐藏层和第三个隐藏层的梯度会加在一起。
九、摘要
我们有几种方法来访问、初始化和绑定模型参数。
我们可以使用自定义初始化。
PyTorch有一个复杂的机制,可以以独特的、分层的方式访问参数。
十、练习
1、使用:numref:chapter_model_construction中定义的FancyMLP,访问各层的参数。
2、看看PyTorch的文档,探索不同的初始化器。
3、尝试在net.apply(initialization)之后和net(x)之前访问模型参数,观察模型参数的形状。有什么变化?为什么?
4、构建一个包含共享参数层的多层感知器并训练它。在训练过程中,观察每个层的模型参数和梯度。
5、为什么共享参数是个好主意?