如何理解深度学习中的层和块以及参数管理?

目录

一:回顾

二:层和块

三:自定义块

四:顺序块 

五:参数管理

 参数访问

目标参数 

一次性访问所有参数 

  从嵌套块收集参数

 参数初始化

 六:自定义层

 不带参数的层

 带参数的层

 七:总结

所有项目代码+UI界面


一:回顾

        上一篇实现了一个线性回归模型——房价预测,线性回归模型的目标是在给定的数据集中,找到一条最适合数据集的直线,可以表示为:y^​=w1​x1​+w2​x2​+...+wd​xd​+b,并且使用Adam优化算法来实现参数的优化。

二:层和块

         目前ResNet架构仍然是许多视觉任务的首选架构。 在其他的领域,如自然语言处理和语音, 层组以各种重复模式排列的类似架构现在也是普遍存在。

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

                                                多个层被组合成块,形成更大的模型。

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

         在构造自定义块之前,我们先回顾一下多层感知机的代码。 下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层, 然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。

import torch
from torch import nn
from torch.nn import functional as F
net = nn.Sequential(nn.Linear(10,256),nn.ReLU(),nn.Linear(256,10))
X = torch.rand(2,10)
net(X),X

输出:
(tensor([[-0.2447, -0.2982, -0.3246, -0.3395,  0.2648, -0.2493, -0.0795,  0.1658,
          -0.1377, -0.1824],
         [-0.2187, -0.2652, -0.3459, -0.3679,  0.0673, -0.2614, -0.0641,  0.1330,
          -0.1747, -0.1198]], grad_fn=<AddmmBackward0>),
 tensor([[0.5121, 0.9738, 0.5594, 0.1669, 0.6420, 0.1669, 0.7815, 0.3052, 0.7705,
          0.6726],
         [0.5216, 0.2642, 0.9573, 0.7035, 0.3446, 0.8479, 0.6267, 0.0406, 0.0528,
          0.3620]]))

        在这个例子中,我们通过实例化nn.Sequential来构建我们的模型, 层的执行顺序是作为参数传递的。 简而言之,nn.Sequential定义了一种特殊的Module, 即在PyTorch中表示一个块的类, 它维护了一个由Module组成的有序列表。 注意,两个全连接层都是Linear类的实例, Linear类本身就是Module的子类。 另外,到目前为止,我们一直在通过net(X)调用我们的模型来获得模型的输出。 这实际上是net.__call__(X)的简写。 这个前向传播函数非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。因为在PyTorch中,调用模型对象的__call__方法就可以进行前向传播计算,而在这个例子中,我们使用了简写形式net(X)来调用这个方法。

三:自定义块

        要想直观地了解块是如何工作的,最简单的方法就是自己实现一个。 在实现我们自定义块之前,我们简要总结一下每个块必须提供的基本功能。

在下面的代码片段中,我们从零开始编写一个块。 它包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层。 注意,下面的MLP类继承了表示块的类。 我们的实现只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数。

        我们首先看一下前向传播函数,它以X作为输入, 计算带有激活函数的隐藏表示,并输出其未规范化的输出值。 在这个MLP实现中,两个层都是实例变量。 要了解这为什么是合理的,可以想象实例化两个多层感知机(net1net2), 并根据不同的数据对它们进行训练。 当然,我们希望它们学到两种不同的模型。

        接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层。 注意一些关键细节: 首先,我们定制的__init__函数通过super().__init__() 调用父类的__init__函数, 省去了重复编写模版代码的痛苦。 然后,我们实例化两个全连接层, 分别为self.hiddenself.out。 注意,除非我们实现一个新的运算符, 否则我们不必担心反向传播函数或参数初始化, 系统将自动生成这些。

import torch
from torch import nn
from torch.nn import functional as F


# 块的一个主要优点是它的多功能性。 我们可以子类化块以创建层(如全连接层的类)、 整个模型(如上面的MLP类)或具有中等复杂度的各种组件。
# 我们在接下来的章节中充分利用了这种多功能性, 比如在处理卷积神经网络时。

#在nn.module里面定义了__call__会调用forward方法
class MLP(nn.Module):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Module的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如初始化参数params(稍后将介绍)
        # 通过super().__init__()调用父类的__init__函数
        super().__init__()  # 继承了很多全部大部分Module的参数初始化
        self.hidden = nn.Linear(20, 256)  # 隐藏层
        self.out = nn.Linear(256, 10)  # 输出层
        # nn.ReLU是一个层结构,必须放在nn.Module中,F.relu是一个函数

    # 前向传播函数,它以X作为输入, 计算带有激活函数的隐藏表示
    # 在nn.module里面定义了__call__会调用forward方法
    def forward(self, X):
        # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。
        return self.out(F.relu(self.hidden(X)))#激活后放入输出out里面

我们来试一下这个函数:


# nn.Sequential可以看作是特殊的module
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
# 输入特征x是两行20列的数据,所有输出就是两行10列(看代码)
X = torch.rand(2, 20)  # 二维:生成两行20列的【0-1】的均匀分布。三维:torch.rand(2,3,20)生成两组,三行20列的【0-1】的均匀分布

nets = MLP()
print(nets(X))

块的一个主要优点是它的多功能性。 我们可以子类化块以创建层(如全连接层的类)、 整个模型(如上面的MLP类)或具有中等复杂度的各种组件。 我们在接下来的章节中充分利用了这种多功能性, 比如在处理卷积神经网络时。


四:顺序块 

        现在我们可以更仔细地看看Sequential类是如何工作的, 回想一下Sequential的设计是为了把其他模块串起来。 为了构建我们自己的简化的MySequential, 我们只需要定义两个关键函数:

  1. 一种将块逐个追加到列表中的函数;

  2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。

下面的MySequential类提供了与默认Sequential类相同的功能。

class MySequential(nn.Module):
#     相当于初始化网络
    def __init__(self, *args):
        super().__init__()
        for idx, module in enumerate(args):
            print('args是什么?:',args)
            print('idx是什么?:',idx)
            print('module是什么?:',module)
            print()
            # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员
            # 变量_modules中。_module的类型是OrderedDict
            self._modules[str(idx)] = module#OrderedDict是拍好排的
    
    #当调用网络的时候就会把数据x传到这个搭建好的网络去运算了(参数可以是系统自动生成)
    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._modules.values():
            print('self._modules.values()是什么?:',self._modules.values())
            print('block是什么?:',block)
            print()
            X = block(X)
        return X
net1 = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
print(net1(X))

   __init__函数将每个模块逐个添加到有序字典_modules中。 读者可能会好奇为什么每个Module都有一个_modules属性? 以及为什么我们使用它而不是自己定义一个Python列表? 简而言之,_modules的主要优点是: 在模块的参数初始化过程中, 系统知道在_modules字典中查找需要初始化参数的子块。

        当MySequential的前向传播函数被调用时, 每个添加的块都按照它们被添加的顺序执行。 现在可以使用我们的MySequential类重新实现多层感知机。

net1 = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
print(net1(X))

五:参数管理

        在选择了架构并设置了超参数后,我们就进入了训练阶段。 此时,我们的目标是找到使损失函数最小化的模型参数值。 经过训练后,我们将需要使用这些参数来做出未来的预测。 此外,有时我们希望提取参数,以便在其他环境中复用它们, 将模型保存下来,以便它可以在其他软件中执行, 或者为了获得科学的理解而进行检查。

之前的介绍中,我们只依靠深度学习框架来完成训练的工作, 而忽略了操作参数的具体细节。 本节,我们将介绍以下内容:

  • 访问参数,用于调试、诊断和可视化;

  • 参数初始化;

  • 在不同模型组件间共享参数。

# 在选择了架构并设置了超参数后,我们就进入了训练阶段。 此时,我们的目标是找到使损失函数最小化的模型参数值。 经过训练后,我们将需要使用这些参数来做出未来的预测。
import torch
from torch import nn

net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
#(2,4)4对于nn来说是输出层
X = torch.rand(size=(2, 4))
print(net(X))

 

 参数访问

print(net[0].state_dict())#0的话就是第0层的神经网络的参数,输入是4行8个输出,所有w因为转置就变成8*4的矩阵

目标参数 

        注意,每个参数都表示为参数类的一个实例。 要对参数执行任何操作,首先我们需要访问底层的数值。 有几种方法可以做到这一点。有些比较简单,而另一些则比较通用。 下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。

# 我们可以通过索引来访问模型的任意层,这就像模型是一个列表一样,每层的参数都在其属性中。 如下所示,我们可以检查第二个全连接层的参数。
print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
print(net[2].weight.data)

        参数是复合的对象,包含值、梯度和额外信息。 这就是我们需要显式参数值的原因。 除了值之外,我们还可以访问每个参数的梯度。 在上面这个网络中,由于我们还没有调用反向传播,所以参数的梯度处于初始状态。

print(net[2].weight.grad == None)

一次性访问所有参数 

        当我们需要对所有参数执行操作时,逐个访问它们可能会很麻烦。 当我们处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂, 因为我们需要递归整个树来提取每个子块的参数。 下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层。

#一次性访问所有参数
print(*[(name, param.shape) for name, param in net[0].named_parameters()])#打印出指定层的
print(*[(name, param.shape) for name, param in net.named_parameters()])#打印出所有层的
print(net.state_dict()['2.bias'].data)
print(200*'*')

  从嵌套块收集参数

        让我们看看,如果我们将多个块相互嵌套,参数命名约定是如何工作的。 我们首先定义一个生成块的函数(可以说是“块工厂”),然后将这些块组合到更大的块中。


# 从嵌套块收集参数¶
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))
print(rgnet(X))
print(rgnet)
print(200*'*')

设计了网络后,我们看看它是如何工作的。

因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。 下面,我们访问第一个主要的块中、第二个子块的第一层的偏置项。

rgnet[0][1][0].bias.data

 参数初始化

        我们还可以对某些块应用不同的初始化方法。 例如,下面我们使用Xavier初始化方法初始化第一个神经网络层, 然后将第三个神经网络层初始化为常量值42。


def init_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(init_xavier)
net[2].apply(init_42)
print(net[0].weight.data[0])
print(net[2].weight.data)

关于net.apply的介绍: 在 PyTorch 中,net.apply() 函数是用于递归地将函数应用于神经网络中的每个子模块的方法,

 在上面的例子中,我们定义了一个函数 init_weights(m),该函数初始化了一个卷积层的权重和偏置项。我们然后使用 net.apply(init_weights) 将该函数应用于 net 模型中的所有子模块每次都传入module,当m是类型的conv2d的时候就初始化

输出:

 isinstance是类型判断语句:(如果第一个参数的类型是第二个参数的话,就返回true)

 在深度学习中,isinstance() 函数经常用于检查一个对象是否是某种类型的神经网络层或模型。例如,以下代码检查 layer 是否是 nn.Conv2d 类型的实例:

 六:自定义层

        深度学习成功背后的一个因素是神经网络的灵活性: 我们可以用创造性的方式组合不同的层,从而设计出适用于各种任务的架构。 例如,研究人员发明了专门用于处理图像、文本、序列数据和执行动态规划的层。 有时我们会遇到或要自己发明一个现在在深度学习框架中还不存在的层。 在这些情况下,必须构建自定义层。本节将展示如何构建自定义层。

 不带参数的层

        首先,我们构造一个没有任何参数的自定义层。 回忆一下在 5.1节对块的介绍, 这应该看起来很眼熟。 下面的CenteredLayer类要从其输入中减去均值。 要构建它,我们只需继承基础层类并实现前向传播功能。

import torch
import torch.nn.functional as F
from torch import nn


class CenteredLayer(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, X):
        return X - X.mean()

layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))

输出:
tensor([-2., -1.,  0.,  1.,  2.])

现在,我们可以将层作为组件合并到更复杂的模型中。

net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

        作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。 由于我们处理的是浮点数,因为存储精度的原因,我们仍然可能会看到一个非常小的非零数。

 带参数的层

        以上我们知道了如何定义简单的层,下面我们继续定义具有参数的层, 这些参数可以通过训练进行调整。 我们可以使用内置函数来创建参数,这些函数提供一些基本的管理功能。 比如管理访问、初始化、共享、保存和加载模型参数。 这样做的好处之一是:我们不需要为每个自定义层编写自定义的序列化程序。

现在,让我们实现自定义版本的全连接层。 回想一下,该层需要两个参数,一个用于表示权重,另一个用于表示偏置项。 在此实现中,我们使用修正线性单元作为激活函数。 该层需要输入参数:in_unitsunits,分别表示输入数和输出数。

class MyLinear(nn.Module):
    def __init__(self, in_units, units):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(in_units, units))
        self.bias = nn.Parameter(torch.randn(units,))
    def forward(self, X):
        linear = torch.matmul(X, self.weight.data) + self.bias.data
        return F.relu(linear)
linear = MyLinear(5, 3)
linear.weight

 我们可以使用自定义层直接执行前向传播计算

linear(torch.rand(2, 5))

 我们还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层。

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))

 七:总结

  • 我们可以通过基本层类设计自定义层。这允许我们定义灵活的新层,其行为与深度学习框架中的任何现有层不同。

  • 在自定义层定义完成后,我们就可以在任意环境和网络架构中调用该自定义层。

  • 层可以有局部参数,这些参数可以通过内置函数创建。

所有项目代码+UI界面

视频,笔记和代码,以及注释都已经上传网盘,放在主页置顶文章

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值