在研究过程中发现,比单层神经网络大,比整个深度神经网络模型小的组件往往更具价值。以计算机视觉为例,ResNet-152具有数百层,这些层是由层组(groups of layers)的重复模式组成。
块(block)可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。
从编程的角度来看,块由类(class)表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函 数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向 传播函数。在定义我们自己的块时,由于深度学习算法框架的自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。
层与块
自定义块
每个块必须提供的基本功能:
- 将输入数据作为其前向传播函数的参数。
- 通过前向传播函数来生成输出。
- 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。(自动实现)
- 存储和访问前向传播计算所需的参数。
- 根据需要初始化模型参数。(有内置初始化,也可自定义)
因此,在构建自定义块的时候,主要注意力在构造函数和前向函数上。
例子:
# 创建my_block块
class my_block(nn.Module): # 继承自父类Module
def __init__(self): # 若想自定义层数和每层结构,可在__init__函数中携带入参
super().__init__() # 调用父类__init__构造函数,执行必要的初始化
# 创建了两层网络(net),由两层全连接层和两层Relu激活函数
# 并在前向函数中,使用一层全连接层输出
self.net = nn.Sequential()
self.net.add_module(nn.Linear(10, 20))
self.net.add_module(nn.ReLU())
self.net.add_module(nn.Linear(20, 30))
self.net.add_module(nn.ReLU())
self.linear = nn.Linear(30, 2)
def forward(self, X): # 前向函数
return self.linear(self.net(X))
net = my_block()
X = torch.rand(20,10)
print(net(X))
输出:
tensor([[ 0.0905, -0.0987],
[ 0.1018, -0.1237],
[ 0.0783, -0.1111],
[ 0.0693, -0.1341],
[ 0.0558, -0.1214],
[ 0.0822, -0.0971],
[ 0.1115, -0.1514],
[ 0.1314, -0.1436],
[ 0.0833, -0.1109],
[ 0.0837, -0.1035],
[ 0.0777, -0.1171],
[ 0.1149, -0.1599],
[ 0.0817, -0.1272],
[ 0.0921, -0.1114],
[ 0.0925, -0.1291],
[ 0.0855, -0.0905],
[ 0.0607, -0.1470],
[ 0.1040, -0.1015],
[ 0.0722, -0.1411],
[ 0.1086, -0.1517]], grad_fn=<AddmmBackward0>)
顺序块
Sequential类的设计是为了把其他模块串起 来。为了构建我们自己的简化的MySequential,我们只需要定义两个关键函数:
- 一种将块逐个追加到列表中的函数;
- 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
class my_sequential(nn.Module): # 同样需要继承自Module父类
def __init__(self,*args):
super().__init__()
for idx,module in enumerate(args):
self._modules[str(idx)] = module # _modules属性是一个有序字典(OrderedDict),定义在父类(Module)中的一个属性
# OrderedDict保证了按照成员添加的顺序遍历它们
def forward(self, X):
for block in self._modules.values():
X = block(X)
return X
net = nn.Sequential(
nn.Linear(10,20),
nn.ReLU(),
nn.Linear(20,3),
)
X = torch.rand(2,10)
net(X)
tensor([[-0.2760, -0.0287, 0.1601],
[-0.2675, -0.0232, 0.1811]], grad_fn=<AddmmBackward0>)
init__函数将每个模块逐个添加到有序字典_modules中。不创建新的python列表,而是用父类中 _modules的好处:在模块的参数初始化过程中,系统知道在 _modules字典中查找需要初始化参数的子块。
前向函数执行自定义代码
如果需要对网络参数和激活值以外的参数(常数参数)进行操作,或者对网络运算过程进行修改,则需要在前向函数中进行定义。
例子:
class forwoard_net(nn.Module):
def __init__(self):
super().__init__()
# 设置权重参数,该权重参数无需保留梯度,在训练期间权重值保持不变
self.rand_weight = torch.rand((20,20), requires_grad=False) # requries_grad属性设置为False,表示该参数无需保留梯度
self.linear = nn.Linear(20, 20) # 线性层
def forward(self, X):
X = self.linear(X) # 线性层
X = F.relu(torch.mm(X, self.rand_weight) + 1) # relu激活函数
X = self.linear(X) # 线性层
# 控制流
while X.abs().sum() > 1:
X /= 2
return X.sum()
net = forwoard_net()
X = torch.rand(2, 20)
net(X)
输出:
tensor(-0.0486, grad_fn=<SumBackward0>)
在上述代码中,实现了一个隐藏层X = F.relu(torch.mm(X, self.rand_weight) + 1)
,该隐藏层中,随机初始化了一组权重参数,并且该权重参数为常量参数,并不会被反向传播更新。
此外,在输出以后,前向函数还将对输出结果进行while循环,在输出结果的L1范数的结果大于1,将对输出结果除2,直到满足结果为止。
参数管理
参数访问
net = nn.Sequential(
nn.Linear(20, 15),
nn.ReLU(),
nn.Linear(15, 2),
)
X = torch.rand(2, 20)
net(X)
tensor([[-0.0823, -0.0976],
[-0.0775, -0.2395]], grad_fn=<AddmmBackward0>)
上述模型定义了两层全连接层以及一层relu激活层
print(net)
Sequential(
(0): Linear(in_features=20, out_features=15, bias=True)
(1): ReLU()
(2): Linear(in_features=15, out_features=2, bias=True)
)
访问目标参数
访问目标层的weight、bias和grad由多种方法:
net[2].state_dict() # 获取第二个全连接层的weight和bias
net[2].bias # 获取第二个全连接层的bias
net[2].bias.data # 获取第二个全连接层的bias
net[2].weight.grad # 获取第二个全连接层的梯度
注:上述net
中,net[0]
代表第一个全连接层,net[1]
为ReLU层,net[2]
为第二个全连接层。
访问所有参数
net.state_dict()
print(*[(name,param,param.shape,param.data) for name,param in net.named_parameters()])
注:第二种方式较为通用
嵌套块访问参数
class Block_1(nn.Module):
def __init__(self):
super().__init__()
self.bk_1_net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(), nn.Linear(64,20),nn.ReLU())
def forward(self,X):
return self.bk_1_net(X)
class Block_2(nn.Module):
def __init__(self):
super().__init__()
self.bk_2_net = nn.Sequential()
for i in range(5):
self.bk_2_net.add_module(f'block_{i}',Block_1())
self.linear = nn.Linear(20, 2)
def forward(self,X):
return self.linear(self.bk_2_net(X))
nestNet = nn.Sequential(Block_2(),nn.Linear(2,1))
X = torch.rand((300, 20))
y = nestNet(X)
创建块Block_1和Block_2,Block_1由两个全连接层和两个ReLU激活层构成,Block_2由5个Block_1嵌套,并将嵌套结果通过一个全连接层输出。
可通过如下操作获取某层的网络结构以及参数:
print(nestNet[0]) # 获取Block_2中嵌套部分
print(nestNet[0].bk_2_net[0]) # 获取嵌套部分的第一个Block_1
print(nestNet[0].bk_2_net[0].bk_1_net[2]) # 获取嵌套部分第一个Block_1的第二个全连接层
可通过对获取到的网络进行.weight
、.state_dict()
等操作,获取权重参数、偏置值以及梯度等
参数初始化
默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵,这个范围是根据输入和输出维度计算 出的。PyTorch的nn.init模块提供了多种预置初始化方法。
内置初始化
PyTorch有以下内置初始化器:
nn.init.normal_(m.weight,mean=0,std=0.01) # 将权重参数初始化为,标准差为0.01的高斯随机变量
nn.init.constant_(m.weight,1) # 将权重从参数初始化为1
nn.init.zeros_(m.weight) # 将权重参数初始化为0
nn.init.xavier_uniform(m.weight) # 使用xavier初始化方法初始化权重参数
nn.init.uniform_(m.weight) # 使用均匀分布初始化权重参数
使用例子:
# 创建初始化函数,将权重初始化为1,偏置初始化为0
# 初始化方法可以替换
# 可用于嵌套块的初始化
def init_weights(m):
if type(m) == nn.Sequential:
for block in m.children():
init_weights(block)
else:
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# 以嵌套块的nestNet模型为例
nestNet.apply(init_weight) # 使用初始化函数进行初始化
nestNet[0].apply(init_weight) # 嵌套层初始化
自定义初始化函数
例如采用以下分布初始化权重参数:
w = 0 (p = 1/2);w = U(5~10) (p = 1/4); w = U(-10~-5) (p = 1/4)
def my_init(m):
if type(m) == nn.Linear:
nn.init.uniform_(m.weight, -10,10) # 使用均匀分布将权重参数初始化为-10~10的均匀分布
# 均匀分布的情况下,权重参数小于5的概率为1/2,任意数乘False为0
m.weight.data *= m.weight.data.abs() >= 5
参数共享
shared = nn.Linear(8,8)
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),
shared,nn.ReLU(),
shared,nn.ReLU(),
nn.Linear(8,1))
创建共享层shared
,并在net
中,第三层和第五层的参数进行绑定。
注:好处是,第三层和第五层的参数共享,在反向传播计算梯度的时候,这个两层的梯度会计算两次并叠加,从而加快梯度下降速度,加快训练速度。
标签:初始化,nn,以及,self,管理,init,参数,net From: https://www.cnblogs.com/AfroNicky/p/18495956