来源:《动手深度学习2》
1、层和块的概念:
当我们刚开始学习神经⽹络时,我们关注的是具有单⼀输出的线性模型。在这⾥,整个模型只由⼀个神经元组成。注意,单个神经元(1)接受⼀些输⼊;(2)⽣成相应的标量输出;(3)具有⼀组相关参数(parameters),这些参数可以更新以优化某些感兴趣的⽬标函数。然后,当我们开始考虑具有多个输出的⽹络,我们就利⽤⽮量化算法来描述整层神经元。像单个神经元⼀样,层(1)接受⼀组输⼊,(2)⽣成相应的输出,(3)由⼀组可调整参数描述。当我们使⽤softmax回归时,⼀个单层本⾝就是模型。然而,即使我们随后引⼊了多层感知机,我们仍然可以认为该模型保留了上⾯所说的基本结构。
有趣的是,对于多层感知机而⾔,整个模型及其组成层都是这种结构。整个模型接受原始输⼊(特征),⽣成输出(预测),并包含⼀些参数(所有组成层的参数集合)。同样,每个单独的层接收输⼊(由前⼀层提供)⽣成输出(到下⼀层的输⼊),并且具有⼀组可调参数,这些参数根据从下⼀层反向传播的信号进⾏更新。虽然你可能认为神经元、层和模型为我们的业务提供了⾜够的抽象,但事实证明,我们经常发现谈论⽐单个层⼤但⽐整个模型小的组件更⽅便。例如,在计算机视觉中⼴泛流⾏的ResNet-152结构就有数百层。这些层是由层组的重复模式组成。⼀次只实现⼀层这样的⽹络会变得很乏味。这种问题不是我们幻想出来的,这种设计模式在实践中很常⻅。上⾯提到的ResNet结构赢得了2015年ImageNet和COCO计算机视觉⽐赛的识别和检测任务,⽬前ResNet结构仍然是许多视觉任务的⾸选结构。在其他的领域,如⾃然语⾔处理和语⾳,层以各种重复模式排列的类似结构现在也是普遍存在。
为了实现这些复杂的⽹络,我们引⼊了神经⽹络块的概念。块可以描述单个层、由多个层组成的组件或整个模型本⾝。使⽤块进⾏抽象的⼀个好处是可以将⼀些块组合成更⼤的组件,这⼀过程通常是递归的。这⼀点在 下图 中进⾏了说明。通过定义代码来按需⽣成任意复杂度的块,我们可以通过简洁的代码实现复杂的神经⽹络。
从编程的⻆度来看,块由类(class)表⽰。它的任何⼦类都必须定义⼀个将其输⼊转换为输出的正向传播函数,并且必须存储任何必需的参数。注意,有些块不需要任何参数。最后,为了计算梯度,块必须具有反向传播函数。幸运的是,在定义我们⾃⼰的块时,由于⾃动微分提供了⼀些后端实现,我们只需要考虑正向传播函数和必需的参数。
2、模型构造:
2.1 继承Module来构造模型:
import torch
from torch import nn
class MLP(nn.Module):
# 申明带有模型参数的层,这里申明了两个全连接层
def __init__(self, **kwargs):
# 调用MLP父类Block的构造函数进行必要的初始化,这样在构造实例时还可以指定其他函数
super(MLP, self).__init__(**kwargs)
self.hidden = nn.Linear(784, 256) # 隐藏层
self.act = nn.ReLU()
self.output = nn.Linear(256, 10) # 输出层
# 定义模型的前向计算,即如何根据输入x计算返回所需要的模型输出
def forward(self, x):
a = self.act(self.hidden(x))
return self.output(a)
X = torch.rand(2, 784)
net = MLP()
print(net)
net(X)
输出:
MLP(
(hidden): Linear(in_features=784, out_features=256, bias=True)
(act): ReLU()
(output): Linear(in_features=256, out_features=10, bias=True)
)
tensor([[-0.1798, -0.2253, 0.0206, -0.1067,