总结:学习了层和块内容中的顺序块和自定义块
层和块
之前首次介绍神经网络时,我们关注的是具有单一输出的线性模型。在这里,整个模型只有一个输出。注意,单个神经网络(1)接受一些输入;(2)生成相应的标量输出;(3)具有一组相关参数,更新这些参数可以优化某目标函数。
然后,当考虑具有多个输出的网络时,我们利用矢量化算法来描述整层神经元。像单个神经元一样,层(1)接受一组输入,(2)生成相应的输出,(3)由一组可调整参数描述。
当我们使用softmax回归时,一个单层本身就是模型。然而,即使我们随后引入了多层感知机,我们仍然可以认为该模型保留了上面所说的基本架构。对于多层感知机而言,整个模型及其组成层都是这种架构。整个模型接受原始输入(特征),生成输出(预测),并包含一些参数(所有组成层的参数集合)。
同样,每个单独的层接收输入(由前一层提供),生成输出(到下一层的输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号进行更新。事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。
例如,在计算机视觉中广泛流行的ResNet‐152架构就有数百层,这些层是由层(groupsoflayers)的重复模式组成。
目前ResNet架构仍然是许多视觉任务的首选架构。在其他的领域,如自然语言处理和语音,层组以各种重复模式排列的类似架构现在也是普遍存在。为了实现这些复杂的网络,我们引入了神经网络块的概念。
块可以描述单个层、由多个层组成的组件或整个模型本身。使用块进行抽象的一个好处是可以将一些块组合成更大的组件,这一过程通常是递归的,如图1.1所示。通过定义代码来按需生成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经网络。
图1.1多个层被组合成块,形成更大的模型
从编程的角度来看,块由类(class)表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。
最后,为了计算梯度,块必须具有反向传播函数。在定义我们自己的块时,由于自动微分(在2.5节中引入)提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。
下面的代码生成一个网络,其中包含一个具有256个单元和ReLU激活函数的全连接隐藏层,然后是一个具有10个隐藏单元且不带激活函数的全连接输出层。
import torch from torch import nn from torch.nn import functional as F net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) X = torch.rand(2, 20) net(X)tensor([[0.0343,0.0264,0.2505,-0.0243,0.0945,0.0012,-0.0141,0.0666,-0.0547,-0.0667],[0.0772,-0.0274,0.2638,-0.0191,0.0394,-0.0324,0.0102,0.0707,-0.1481,-0.1031]], grad_fn=<AddmmBackward0>)
自定义块
class MLP(nn.Module): # 用模型参数声明层。这里,我们声明两个全连接的层 def __init__(self): # 调用MLP的父类Module的构造函数来执行必要的初始化。 # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍) super().__init__() self.hidden = nn.Linear(20, 256) # 隐藏层 self.out = nn.Linear(256, 10) # 输出层 # 定义模型的前向传播,即如何根据输入X返回所需的模型输出 def forward(self, X): # 注意,这里我们使用ReLU的函数版本,其在nn.functional模块中定义。 return self.out(F.relu(self.hidden(X)))net = MLP() net(X)tensor([
[0.0669,0.2202,-0.0912,-0.0064,0.1474,-0.0577,-0.3006,0.1256,-0.0280,0.4040],
[0.0545,0.2591,-0.0297,0.1141,0.1887,0.0094,-0.2686,0.0732,-0.0135,0.3865]
], grad_fn=<AddmmBackward0>)
顺序块
class MySequential(nn.Module): def __init__(self, *args): super().__init__() for idx, module in enumerate(args): # 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员 # 变量_modules中。_module的类型是OrderedDict self._modules[str(idx)] = module def forward(self, X): # OrderedDict保证了按照成员添加的顺序遍历它们 for block in self._modules.values(): X = block(X) return X__init__函数将每个模块逐个添加到有序字典_modules中。读者可能会好奇为什么每个Module都有一个_modules属性?以及为什么我们使用它而不是自己定义一个Python列表?简而言之,_modules的主要优点是:在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。
当MySequential的前向传播函数被调用时,每个添加的块都按照它们被添加的顺序执行。现在可以使用我们的MySequential类重新实现多层感知机。
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10)) net(X)tensor([
[2.2759e-01,-4.7003e-02,4.2846e-01,-1.2546e-01,1.5296e-01,1.8972e-01,9.7048e-02,4.5479e-04,-3.7986e-02,6.4842e-02],
[2.7825e-01,-9.7517e-02,4.8541e-01,-2.4519e-01,-8.4580e-02,2.8538e-01,3.6861e-02,2.9411e-02,-1.0612e-01,1.2620e-01]
],grad_fn=<AddmmBackward0>)