神经网络模型的层和块——【torch学习笔记】

本文介绍了深度学习中层和块的概念,包括单个层、组件和模型的抽象,以及如何通过块构成复杂网络。重点讲解了块的递归组合、多层感知器的模块化、自定义块和连续块的实现,以及如何利用Python控制流创建可扩展的模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

层和块

引用翻译:《动手学深度学习》

为了实现复杂的网络,我们引入了神经网络块的概念。 块(block)可以描述单个层、由多个层组成的组件或整个模型本身。 使用块进行抽象的一个好处是可以将一些块组合成更大的组件, 这一过程通常是递归的。

对于多层感知机而言,整个模型及其组成层都是这种架构。 整个模型接受原始输入(特征),生成输出(预测), 并包含一些参数(所有组成层的参数集合)。 同样,每个单独的层接收输入(由前一层提供), 生成输出(到下一层的输入),并且具有一组可调参数, 这些参数根据从下一层反向传播的信号进行更新。

事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。

帮助推动深度学习的关键组件之一是强大的软件。与半导体设计类似,工程师们从指定晶体管到逻辑电路再到编写代码,我们现在见证了深度网络设计的类似进展。在前面的章节中,我们看到了从设计单个神经元到整个神经元层的转变。

然而,当我们有152个层时,即使按层设计网络也会很繁琐,就像He等人在2016年提出的用于计算机视觉问题的ResNet-152那样。这样的网络有相当程度的规律性,它们由重复(或至少是类似设计)的层块组成。这些块然后形成了更复杂的网络设计的基础。

简而言之,块是一个或多个层的组合。这种设计得到了代码的帮助,它可以根据需要生成这种块,就像乐高工厂生成的块可以被组合成可怕的人工制品。我们从非常简单的模块开始,即多层感知器的模块。一个常见的策略是构建一个两层的网络,如下所示。

import torch
from torch import nn
from torch.autograd import Variable
import torch.nn.functional as F
net = nn.Sequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))
x = torch.randn(2,20)
net(x)
tensor([[ 0.0259, -0.0882,  0.1119, -0.1585, -0.0911,  0.1426, -0.0533,  0.2907,
         -0.2684, -0.0755],
        [-0.3949, -0.0375, -0.1352, -0.5339, -0.0366, -0.2347, -0.3211, -0.2683,
         -0.4857, -0.2530]], grad_fn=<AddmmBackward>)

块是一个或多个层的组合。上面的代码生成了一个具有256个单元的隐藏层的网络,然后是ReLU激活和另外10个单元支配输出。特别是,我们使用nn.Sequential构造器生成一个空的网络,然后将这两个层插入其中。

下面我们将解释从定义层到定义块(一个或多个层)所需的各种步骤。为了开始,我们需要对软件进行一些推理。对于大多数意图和目的来说,一个块的行为非常像一个花哨的层。也就是说,它提供以下功能。

1、它需要摄取数据(输入)。

2、它需要产生一个有意义的输出。这通常被编码在我们称之为前向函数的地方。它允许我们通过net(X)调用一个块获得所需的输出。幕后发生的事情是,它调用forward来进行前向传播。

4、在调用后向时,它需要产生一个与其输入有关的梯度。一般来说,这是自动的。

5、它需要存储区块所固有的参数。例如,上面的块包含两个隐藏层,我们需要一个地方来存储它的参数。

6、很明显,它还需要根据需要初始化这些参数。

一、一个自定义块

nn.Module类提供了我们所需的大部分功能。它是 nn 模块中提供的一个模型构造器,我们可以继承它来定义我们想要的模型。下面继承Block类来构造本节开头提到的多层感知器。这里定义的MLP类重写了Block类的init和forward函数。它们分别用于创建模型参数和定义前向计算。前向计算也就是前向传播。

class MLP(nn.Module):
    # 声明一个带有模型参数的层。在这里,我们声明两个全连接层
    def __init__(self, **kwargs):
        # 调用MLP父类Module的构造函数来进行必要的初始化。通过这种方式,
        # 在构造实例时也可以指定其他的函数参数,比如模型参数params,在下面的章节中会有所描述
        super(MLP, self).__init__(**kwargs)
        self.hidden = nn.Sequential(nn.Linear(20,256),nn.ReLU())  # Hidden layer
        self.output = nn.Linear(256,10)  # Output layer
    # 定义模型的正向计算,也就是如何根据输入的x返回所需的模型输出
    def forward(self, x):
        return self.output(self.hidden(x))

让我们再仔细看一下。前向方法通过评估隐藏层self.hidden(x)和随后评估输出层self.output( …)来调用一个网络。这就是我们在这个块的前向传递中所期望的。

为了让该块知道它需要评估什么,我们首先需要定义层。这就是init方法的作用。它首先初始化所有与块相关的参数,然后构建必要的层。这就把相应的层和所需的参数附加到类中。注意,不需要在类中定义一个反向传播方法。系统会通过自动寻找梯度来自动生成反向传播所需的反向方法。这同样适用于初始化方法,它是自动生成的。

net1 = MLP()
x = torch.randn(2,20)
net1(x)
tensor([[ 0.0105,  0.1501,  0.0600,  0.1323,  0.1560, -0.0089, -0.2808, -0.1873,
         -0.0897, -0.0632],
        [-0.0975,  0.0499, -0.0119,  0.1543,  0.1187, -0.0591, -0.1541, -0.1315,
         -0.1765, -0.0034]], grad_fn=<AddmmBackward>)

二、一个连续的块

Block类是一个描述数据流的通用组件。事实上,Sequential类是从Block类派生出来的:当模型的前向计算是每层计算的简单串联时,我们可以用更简单的方式定义模型。

Sequential类的目的是提供一些有用的便利函数。特别是,添加方法允许我们一个一个地添加串联的Block子类实例,而模型的前向计算是按照添加的顺序一个一个地计算这些实例。

下面,实现一个MySequential类,它具有与Sequential类相同的功能。这可能有助于你更清楚地理解序列类的工作原理。

class MySequential(nn.Sequential):
    def __init__(self, **kwargs):
        super(MySequential, self).__init__(**kwargs)

    def add_module(self, block):
        # 这里,block是一个Block子类的实例,我们假设它有一个唯一的名字。
        # 我们将其保存在Block类的成员变量_children中,其类型为OrderedDict。
        # 当MySequential实例调用初始化函数时,系统会自动初始化_children的所有成员
        self._modules[block] = block
        
    def forward(self, x):
        # OrderedDict保证了成员将按照它们被添加的顺序被遍历。
        # 通过对字典遍历进行串联
        for block in self._modules.values():
            x = block(x)
        return x

其核心是add方法。它将任何块添加到有序的子字典中。然后,当前向传播被调用时,这些块被依次执行。让我们看看MLP现在是什么样子。

net = MySequential()
net.add_module(nn.Linear(20,256))
net.add_module(nn.ReLU())
net.add_module(nn.Linear(256,10))  # 添加到有序的子字典
x = torch.randn(2,20)
net(x)
tensor([[-0.1179,  0.1587, -0.1411, -0.1516,  0.3740, -0.1992, -0.0926,  0.0747,
         -0.1095, -0.1455],
        [-0.2971,  0.1281, -0.1680, -0.1337,  0.6045, -0.1480,  0.1353,  0.0316,
         -0.0474, -0.0564]], grad_fn=<AddmmBackward>)
type(net)  # 此时net的类型,不可print
__main__.MySequential

三、代码块

尽管 Sequential 类可以使模型的构建更容易,而且你不需要定义前进方法,但直接继承 Block 类可以大大扩展模型构建的灵活性。特别是,我们将在前进方法中使用 Python 的控制流。在这时,我们需要引入另一个概念,即常量参数。这些是在调用 backprop 时不使用的参数。这听起来很抽象,但实际上是这样的。假设我们有一些函数

f ( x , w ) = 3 ⋅ w ⊤ x . f(x,w)=3⋅w⊤x. f(x,w)=3wx.

在这种情况下,3是一个常数参数。我们可以把3改成其他的东西,比如说c,通过

f ( x , w ) = c ⋅ w ⊤ x . f(x,w)=c⋅w⊤x. f(x,w)=cwx.

其实没有什么变化,只是我们可以调整c的值。就w和x而言,它仍然是一个常数。然而,由于Pytorch事先不知道这一点,所以值得帮它一把(这也使代码更快,因为我们没有让Pytorch引擎在一个没有变化的参数上进行疯狂的追逐)。让我们看看这在实践中是什么样子的。

class FancyMLP(nn.Sequential):
    def __init__(self, **kwargs):
        super(FancyMLP, self).__init__(**kwargs)
        
        # 用get_constant创建的随机权重参数在训练中不会被迭代(即常量参数)
        # self.rand_weight = self.parameters.get_constant('rand_weight',nd.random.uniform(shape=(20, 20))) 
        # 在pytorch中,如果传入的输入形状与定义的模块块相同,你可以使用任何模块
        
        self.dense1=nn.Sequential(nn.Linear(20,20),nn.ReLU())
        
        self.rand_weight=nn.Parameter(torch.empty(20,20).uniform_(0, 1) )
    
        self.dense = nn.Sequential(nn.Linear(20,256),nn.ReLU())
        
        self.register_buffer('random_weights', self.rand_weight)
        
    def forward(self, x):
        x = self.dense1(x)
        # 使用创建的常数参数,以及NDArray的relu和dot函数
        print(x.shape)
        # 在pytorch中,点积dot适用于1d张量,2d张量使用torch.mm或torch.bmm。
        x = F.relu(torch.mm(x, Variable(self.random_weights).data) + 1)
        # 重复使用全连接层。这相当于与两个全连接层共享参数
        x = self.dense(x)
        # 在控制流中,我们需要调用asscalar来返回标量进行比较。
        while x.norm().item() > 1:
            print('x.norm().item()',x.norm().item())
            print('x.norm()',x.norm())
            x /= 2
        if x.norm().item() < 0.8:
            print('x.norm().item()',x.norm().item())
            print('x.norm()',x.norm())
            x *= 10
        return x.sum()
        

在这个FancyMLP模型中,我们使用了恒定的权重Rand_weight(注意,它不是一个模型参数),进行了一个矩阵乘法运算(nd.dot<),并重新使用了同一个密集层。

请注意,这与使用两套不同参数的密集层有很大不同。

相反,我们两次使用了同一个网络。在深度网络中,当人们想表达一个网络的多个部分共享相同的参数时,往往也会说参数是绑定的。让我们看看,如果我们构建它并通过它输入数据会发生什么。

net = FancyMLP()
print(net)
x = torch.randn(2,20)
FancyMLP(
  (dense1): Sequential(
    (0): Linear(in_features=20, out_features=20, bias=True)
    (1): ReLU()
  )
  (dense): Sequential(
    (0): Linear(in_features=20, out_features=256, bias=True)
    (1): ReLU()
  )
)
torch.Size([2, 20])
x.norm().item() 32.54374694824219
x.norm() tensor(32.5437, grad_fn=<CopyBackwards>)
x.norm().item() 16.271873474121094
x.norm() tensor(16.2719, grad_fn=<CopyBackwards>)
x.norm().item() 8.135936737060547
x.norm() tensor(8.1359, grad_fn=<CopyBackwards>)
x.norm().item() 4.067968368530273
x.norm() tensor(4.0680, grad_fn=<CopyBackwards>)
x.norm().item() 2.0339841842651367
x.norm() tensor(2.0340, grad_fn=<CopyBackwards>)
x.norm().item() 1.0169920921325684
x.norm() tensor(1.0170, grad_fn=<CopyBackwards>)
x.norm().item() 0.5084960460662842
x.norm() tensor(0.5085, grad_fn=<CopyBackwards>)

net(x)结果输出:

tensor(64.3372, grad_fn=<SumBackward0>)

我们没有理由不把这些建立网络的方式混合起来。

显然,下面的例子更像是一个嵌合体,或者说是一个Rube Goldberg机器。也就是说,它结合了从单个区块构建一个区块的例子,而这些区块又可能是区块本身。

此外,我们甚至可以在同一个正向函数中结合多种策略。为了证明这一点,这里有一个网络。

class NestMLP(nn.Sequential):
    def __init__(self, **kwargs):
        super(NestMLP, self).__init__(**kwargs)
        self.net = nn.Sequential(nn.Linear(20,64),nn.ReLU(),nn.Linear(64, 32),nn.ReLU())
        self.dense = nn.Sequential(nn.Linear(32,20),nn.ReLU())

    def forward(self, x):
        return self.dense(self.net(x))

chimera = nn.Sequential()
chimera.add_module("Linear1",nn.Linear(20,20))
chimera.add_module("NestNlp",NestMLP())
chimera.add_module("FancyMLP",FancyMLP())
print(chimera)
Sequential(
  (Linear1): Linear(in_features=20, out_features=20, bias=True)
  (NestNlp): NestMLP(
    (net): Sequential(
      (0): Linear(in_features=20, out_features=64, bias=True)
      (1): ReLU()
      (2): Linear(in_features=64, out_features=32, bias=True)
      (3): ReLU()
    )
    (dense): Sequential(
      (0): Linear(in_features=32, out_features=20, bias=True)
      (1): ReLU()
    )
  )
  (FancyMLP): FancyMLP(
    (dense1): Sequential(
      (0): Linear(in_features=20, out_features=20, bias=True)
      (1): ReLU()
    )
    (dense): Sequential(
      (0): Linear(in_features=20, out_features=256, bias=True)
      (1): ReLU()
    )
  )
)
torch.Size([2, 20])

chimera(x)结果输出:

tensor(12.6640, grad_fn=<SumBackward0>)

四、汇编

热心的读者可能开始担心这个的效率了。毕竟,在这个应该是高性能的深度学习库中,我们有很多字典查询、代码执行和很多其他Pythonic的事情在进行。

Python的全局解释器锁的问题是众所周知的。在深度学习的背景下,这意味着我们有一个超级快的GPU(或多个GPU),它可能不得不等待,直到运行Python的一个微不足道的CPU核心有机会告诉它下一步该怎么做。

这显然是可怕的,有很多方法可以解决这个问题。提高Python速度的最好方法是完全避免它。

Pytorch通过允许混合化来做到这一点。在它里面,Python 解释器在第一次调用时就执行了这个块。Pytorch运行时记录正在发生的事情,并在下一次调用Python时将其短路。

这在某些情况下可以大大加快事情的进展,但需要注意控制流的问题。我们建议有兴趣的读者在读完本章后跳到涵盖杂交和编译的章节。

五、摘要

  • 层是块

  • 许多层可以是一个块

  • 许多块可以是一个块

  • 代码可以是一个块

  • 块负责大量的内务工作,如参数初始化、反推和相关问题。

  • 层和块的顺序连接由同名的顺序块来处理。

六、练习

1、当调用一个其父类不在父类的init函数中的init方法时,你会得到什么样的错误信息?

2、如果你删除FancyMLP类中的item函数,会出现什么样的问题?

3、如果你将NestMLP类中Sequential实例定义的self.net改为self.net = [nn.Sequential(nn.Linear(64,256),nn.ReLU()), nn.Sequential(nn.Linear(32,256),nn.ReLU()]会出现什么样的问题?]

4、实现一个块,该块将两个块作为参数,例如net1和net2,并在前向传递中返回两个网络的串联输出(这也被称为并行块)。

5、假设你想串联同一个网络的多个实例。实现一个工厂函数,生成同一块的多个实例,并从中建立一个更大的网络。

### 关于李沐深度学习笔记中的序列模型 #### 数据生成与预处理 为了研究序列模型,在实验中使用正弦函数加上一定量的可加性噪声来创建具有时间特性的数据集[^2]。这些数据的时间跨度设定为从1到1000个时间单位。 #### 输入输出关系构建 对于每一个时间点\(t\),目标变量\(y_t\)被设置成等于当前时刻的特征值\(x_t\);而输入向量\(x_t\)则由前\(\tau\)个时间步的数据组成,即\[x_t=[x_{t-\tau},...,x_{t-1}]\]。 #### 模型结构设计 采用了一个相对基础的设计——一个多感知器(MLP),它包含了两全连接神经网络以及中间使用的ReLU作为激活函数,并利用均方误差(MSE)作为损失函数来进行优化过程。 ```python import torch.nn as nn class SimpleRNN(nn.Module): def __init__(self, input_size, hidden_size, output_size): super(SimpleRNN, self).__init__() self.hidden_layer_1 = nn.Linear(input_size * tau, hidden_size) self.relu_activation = nn.ReLU() self.output_layer = nn.Linear(hidden_size, output_size) def forward(self, x): out = self.hidden_layer_1(x.view(-1)) out = self.relu_activation(out) out = self.output_layer(out) return out ``` #### 训练流程概述 经过一系列迭代之后,该简单架构能够学会捕捉给定序列内部存在的模式并据此做出合理预测。 #### 预测性能评估 除了单步向前看之外,还探讨了如何实现更长远范围内的多步乃至K步外推能力测试。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值