当我们第一次引入神经网络时,我们专注于具有单一输出的线性模型。在这里,整个模型只包含一个神经元。请注意,单个神经元 (i) 接受一组输入;(ii) 生成相应的标量输出;(iii) 有一组相关参数,可以更新这些参数以优化一些感兴趣的目标函数。然后,一旦我们开始考虑具有多个输出的网络,我们就利用矢量化算法来表征整个神经元层。就像单个神经元一样,层 (i) 采用一组输入,(ii) 生成相应的输出,并且 (iii) 由一组可调参数描述。当我们进行 softmax 回归时,单层本身就是模型。然而,即使我们随后引入了 MLP,
有趣的是,对于 MLP,整个模型及其组成层都共享这种结构。整个模型接受原始输入(特征),生成输出(预测),并拥有参数(来自所有构成层的组合参数)。同样,每个单独的层摄取输入(由前一层提供)生成输出(后续层的输入),并拥有一组可调参数,这些参数根据从后续层向后流动的信号进行更新。
虽然您可能认为神经元、层和模型为我们提供了足够的抽象来开展我们的业务,但事实证明,我们经常发现谈论比单个层大但比整个模型小的组件很方便。例如,在计算机视觉领域广受欢迎的 ResNet-152 架构拥有数百层。这些层由层组的重复图案组成。一次一层地实现这样的网络会变得乏味。这种担忧不仅仅是假设——这样的设计模式在实践中很常见。上面提到的 ResNet 架构赢得了 2015 年 ImageNet 和 COCO 计算机视觉识别和检测竞赛(He et al. , 2016)并且仍然是许多视觉任务的首选架构。层以各种重复模式排列的类似架构现在在其他领域无处不在,包括自然语言处理和语音。
为了实现这些复杂的网络,我们引入了神经网络模块的概念。模块可以描述单个层、由多个层组成的组件或整个模型本身!使用模块抽象的一个好处是它们可以组合成更大的工件,通常是递归的。如图 6.1.1所示。通过定义代码以按需生成任意复杂度的模块,我们可以编写出奇紧凑的代码并仍然实现复杂的神经网络。
从编程的角度来看,模块由类表示。它的任何子类都必须定义一个前向传播方法,将其输入转换为输出,并且必须存储任何必要的参数。请注意,某些模块根本不需要任何参数。最后,为了计算梯度,模块必须具有反向传播方法。幸运的是,由于自动微分(在2.5 节中介绍)在定义我们自己的模块时提供了一些幕后魔法,我们只需要担心参数和前向传播方法。
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
首先,我们重新审视用于实现 MLP 的代码(第 5.1 节)。以下代码生成一个网络,该网络具有一个具有 256 个单元和 ReLU 激活的全连接隐藏层,后跟一个具有 10 个单元的全连接输出层(无激活函数)。
torch.Size([2, 10])
在这个例子中,我们通过实例化一个 来构造我们的模型 nn.Sequential
,层按照它们应该被执行的顺序作为参数传递。简而言之,nn.Sequential
定义了一种特殊的Module
,在 PyTorch 中呈现模块的类。它维护一个有序的 constituent 列表Module
。请注意,两个完全连接的层中的每一个都是该类的一个实例,Linear
该类本身是 的子类Module
。前向传播 ( forward
) 方法也非常简单:它将列表中的每个模块链接在一起,将每个模块的输出作为输入传递给下一个模块。请注意,到目前为止,我们一直在通过构造调用我们的模型 net(X)
以获得它们的输出。这实际上只是 net.__call__(X)
.
(2, 10)
In this example, we constructed our model by instantiating an nn.Sequential
, assigning the returned object to the net
variable. Next, we repeatedly call its add
method, appending layers in the order that they should be executed. In short, nn.Sequential
defines a special kind of Block
, the class that presents a module in Gluon. It maintains an ordered list of constituent Block
s. The add
method simply facilitates the addition of each successive Block
to the list. Note that each layer is an instance of the Dense
class which is itself a subclass of Block
. The forward propagation (forward
) method is also remarkably simple: it chains each Block
in the list together, passing the output of each as input to the next. Note that until now, we have been invoking our models via the construction net(X)
to obtain their outputs. This is actually just shorthand for net.forward(X)
, a slick Python trick achieved via the Block
class’s __call__
method.
(2, 10)
TensorShape([2, 10])
In this example, we constructed our model by instantiating an keras.models.Sequential
, with layers in the order that they should be executed passed as arguments. In short, Sequential
defines a special kind of keras.Model
, the class that presents a module in Keras. It maintains an ordered list of constituent Model
s. Note that each of the two fully connected layers is an instance of the Dense
class which is itself a subclass of Model
. The forward propagation (call
) method is also remarkably simple: it chains each module in the list together, passing the output of each as input to the next. Note that until now, we have been invoking our models via the construction net(X)
to obtain their outputs. This is actually just shorthand for net.call(X)
, a slick Python trick achieved via the module class’s __call__
method.
6.1.1. 自定义模块
也许培养关于模块如何工作的直觉的最简单方法是我们自己实现一个。在我们实现自己的自定义模块之前,我们先简单总结一下每个模块必须提供的基本功能:
-
摄取输入数据作为其前向传播方法的参数。
-
通过让前向传播方法返回一个值来生成输出。请注意,输出可能具有与输入不同的形状。例如,我们上面模型中的第一个全连接层接收任意维度的输入,但返回 256 维度的输出。
-
计算其输出相对于其输入的梯度,可以通过其反向传播方法访问。通常这会自动发生。
-
存储并提供对执行前向传播计算所需的那些参数的访问。
-
根据需要初始化模型参数。
在下面的代码片段中,我们从头开始编写一个模块,对应于一个包含 256 个隐藏单元的隐藏层和一个 10 维输出层的 MLP。请注意,MLP
下面的类继承了代表模块的类。我们将严重依赖父类的方法,仅提供我们自己的构造函数(__init__
Python 中的方法)和前向传播方法。
class MLP(nn.Module):
def __init__(self):
# Call the constructor of the parent class nn.Module to perform
# the necessary initialization
super().__init__()
self.hidden = nn.LazyLinear(256)
self.out = nn.LazyLinear(10)
# Define the forward propagation of the model, that is, how to return the
# required model output based on the input X
def forward(self, X):
return self.out(F.relu(self.hidden(X)))
class MLP(nn.Block):
def __init__(self):
# Call the constructor of the MLP parent class nn.Block to perform
# the necessary initialization
super().__init__()
self.hidden = nn.Dense(256, activation='relu')
self.out = nn.Dense(10)
# Define the forward propagation of the model, that is, how to return the
# required model output based on the input X
def forward(self, X):
return self.out(self.hidden(X))
class MLP(tf.keras.Model):
def __init__(self):
# Call the constructor of the parent class tf.keras.Model to perform
# the necessary initialization
super().__init__()
self.hidden = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)
self.out = tf.keras.layers.Dense(units=10)
# Define the forward propagation of the model, that is, how to return the
# required model output based on the input X
def call(self, X):
return self.out(self.hidden((X)))
让我们首先关注前向传播方法。请注意,它以 X
输入为输入,应用激活函数计算隐藏表示,并输出其对数。在这个MLP
实现中,两层都是实例变量。要了解为什么这是合理的,想象一下实例化两个 MLPnet1
和net2
,并在不同的数据上训练它们。自然地,我们希望它们代表两种不同的学习模型。
我们在构造函数中实例化 MLP 的层,随后在每次调用前向传播方法时调用这些层。注意几个关键细节。首先,我们的自定义方法通过让我们免于重述适用于大多数模块的样板代码的痛苦来__init__
调用父类的方法。然后我们实例化我们的两个完全连接的层,将它们分配给 和。请注意,除非我们实现一个新层,否则我们不必担心反向传播方法或参数初始化。系统会自动生成这些方法。让我们试试这个。__init__
super().__init__()
self.hidden