本文对深度学习网络的每一层参数值查看、初始化和不同层参数值绑定进行详解,并通过两个例子对过程进行可视化,以帮助大家理解和使用。
定义模型
首先,我们先定义一个网络模型,此处为了方便演示,设置核大小和输入大小均为1。
import torch
from torch import nn
def autopad(k, p=None, d=1): # kernel, padding, dilation
"""Pad to 'same' shape outputs."""
if d > 1:
k = d * (k - 1) + 1 if isinstance(k, int) else [d * (x - 1) + 1 for x in k] # actual kernel-size
if p is None:
p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad
return p
class Conv(nn.Module):
"""Standard convolution with args(ch_in, ch_out, kernel, stride, padding, groups, dilation, activation)."""
default_act = nn.SiLU() # default activation
def __init__(self, c1, c2, k=1, s=1, p=None, g=1, d=1, act=True):
"""Initialize Conv layer with given arguments including activation."""
super().__init__()
self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p, d), groups=g, dilation=d, bias=False)
self.bn = nn.BatchNorm2d(c2)
self.act = self.default_act if act is True else act if isinstance(act, nn.Module) else nn.Identity()
def forward(self, x):
"""Apply convolution, batch normalization and activation to input tensor."""
return self.act(self.bn(self.conv(x)))
def forward_fuse(self, x):
"""Perform transposed convolution of 2D data."""
return self.act(self.conv(x))
class Net240302(nn.Module):
def __init__(self,s=1):
super().__init__()
self.A = nn.Sequential(
Conv(1,1,k=1),
Conv(1,2,k=1),
)
def forward(self,x):
y=self.A(x)
return y
x=torch.rand(1,1,2,2)
net=Net240302()
y=net(x)
y
运行输出为:
tensor([[[[ 0.7362, -0.2782],
[ 0.6974, -0.2390]],
[[ 0.7360, -0.2782],
[ 0.6973, -0.2389]]]], grad_fn=<SiluBackward0>)
查看参数
从已有模型中访问参数。因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问网络中A模块的第二个子块的的参数。
print(net.A[1].state_dict())
输出:
OrderedDict([('conv.weight', tensor([[[[-0.1933]]],
[[[-0.7397]]]])), ('bn.weight', tensor([1., 1.])), ('bn.bias', tensor([0., 0.])), ('bn.running_mean', tensor([-0.0042, -0.0160])), ('bn.running_var', tensor([0.9007, 0.9106])), ('bn.num_batches_tracked', tensor(1))])
我们可以从输出的结果中看到,这个卷积模块包含两个类型的参数,分别是卷积层的权重和BN层的均值、偏置、协方差等。此外, 两者都存储为单精度浮点数(float32)。 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。
查看指定参数
要对参数执行任何操作,首先我们需要访问底层的数值。 有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。 下面的代码从A模块中的第2个卷积模块提取卷积的权重, 提取后返回的是一个参数类实例,并进一步访问该参数的值。
print(type(net.A[1].conv.weight))
print(net.A[1].conv.weight)
print(net.A[1].conv.weight.data)
输出:
<class 'torch.nn.parameter.Parameter'>
Parameter containing:
tensor([[[[-0.1933]]],
[[[-0.7397]]]], requires_grad=True)
tensor([[[[-0.1933]]],
[[[-0.7397]]]])
参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。
一次访问全部参数
当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。 当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂, 因为我们需要递归整个树来提取每个子块的参数。 下面,我们将通过演示来比较访问第一个卷积模块的参数和访问所有模块的参数。
print(*[(name, param.shape) for name, param in net.A[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.A.named_parameters()])
输出:
('conv.weight', torch.Size([1, 1, 1, 1])) ('bn.weight', torch.Size([1])) ('bn.bias', torch.Size([1]))
('0.conv.weight', torch.Size([1, 1, 1, 1])) ('0.bn.weight', torch.Size([1])) ('0.bn.bias', torch.Size([1])) ('1.conv.weight', torch.Size([2, 1, 1, 1])) ('1.bn.weight', torch.Size([2])) ('1.bn.bias', torch.Size([2]))
可以看到,如果加索引,则会仅显示该索引层的参数,若不加索引,则显示所有模块的参数。在所有模块的参数中,同样的层会有不同的编号,如0.conv.weight
和1.conv.weight
,保持每一层标号的唯一性,在数百层的网络亦是如此。
参数初始化
深度学习框架提供默认随机初始化, 也允许我们创建自定义初始化方法, 满足我们通过其他规则实现初始化权重。默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵, 这个范围是根据输入和输出维度计算出的。 PyTorch的nn.init模块提供了多种预置初始化方法。
内置初始化
首先调用内置的初始化器。 下面的代码将所有权重参数初始化为标准差为0.01的高斯随机变量
def init_normal(m):
if isinstance(m, nn.Conv2d):
nn.init.normal_(m.weight, mean=0, std=0.01)
print("初始化前")
print(net.A[0].conv.weight) # 打印初始化前的权重
print("初始化")
net.A[0].conv.apply(init_normal) # 应用初始化函数
print("初始化后")
print(net.A[0].conv.weight) # 打印初始化后的权重,以验证是否成功
输出:
初始化前
Parameter containing:
tensor([[[[0.0044]]]], requires_grad=True)
初始化
初始化后
Parameter containing:
tensor([[[[0.0110]]]], requires_grad=True)
我们还可以将所有参数初始化为给定的常数,比如初始化为1。
def init_constant(m):
if isinstance(m, nn.Conv2d):
nn.init.constant_(m.weight, 1) # 将权重设置为1
print("初始化前")
print(net.A[0].conv.weight) # 打印初始化前的权重
print("初始化")
net.A[0].conv.apply(init_constant) # 应用初始化函数
print("初始化后")
print(net.A[0].conv.weight) # 打印初始化后的权重,以验证是否成功
输出:
初始化前
Parameter containing:
tensor([[[[0.2713]]]], requires_grad=True)
初始化
初始化后
Parameter containing:
tensor([[[[1.]]]], requires_grad=True)
参数绑定
有时我们希望在多个层间共享参数: 我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net1 = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),
shared, nn.ReLU(),
shared, nn.ReLU(),
nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
net1(X)
# 检查参数是否相同
print(net1[2].weight.data[0] == net1[4].weight.data[0])
net1[2].weight.data[0, 0] = 100
# 确保它们实际上是同一个对象,而不只是有相同的值
print(net1[2].weight.data[0] == net1[4].weight.data[0])
输出:
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
这个例子表明第三个和第五个神经网络层的参数是绑定的。 它们不仅值相等,而且由相同的张量表示。 因此,如果我们改变其中一个参数,另一个参数也会改变。