层、块以及参数管理

此Blog仅作为日常学习工作中记录使用,Blog中有不足之处欢迎指出

​ 在研究过程中发现,比单层神经网络大,比整个深度神经网络模型小的组件往往更具价值。以计算机视觉为例,ResNet-152具有数百层,这些层是由层组(groups of layers)的重复模式组成。

​ 块(block)可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。

​ 从编程的角度来看,块由类(class)表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函 数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向 传播函数。在定义我们自己的块时,由于深度学习算法框架的自动微分提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。

层与块

自定义块

每个块必须提供的基本功能:

  1. 将输入数据作为其前向传播函数的参数。
  2. 通过前向传播函数来生成输出。
  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。(自动实现)
  4. 存储和访问前向传播计算所需的参数。
  5. 根据需要初始化模型参数。(有内置初始化,也可自定义)

因此,在构建自定义块的时候,主要注意力在构造函数和前向函数上。

例子:

# 创建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,我们只需要定义两个关键函数:

  1. 一种将块逐个追加到列表中的函数;
  2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。
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中,第三层和第五层的参数进行绑定。

注:好处是,第三层和第五层的参数共享,在反向传播计算梯度的时候,这个两层的梯度会计算两次并叠加,从而加快梯度下降速度,加快训练速度。

posted @   AfroNicky  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示