5.1. 层和块
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in SageMaker Studio Lab

之前首次介绍神经网络时,我们关注的是具有单一输出的线性模型。 在这里,整个模型只有一个输出。 注意,单个神经网络 (1)接受一些输入; (2)生成相应的标量输出; (3)具有一组相关 参数(parameters),更新这些参数可以优化某目标函数。

然后,当考虑具有多个输出的网络时, 我们利用矢量化算法来描述整层神经元。 像单个神经元一样,层(1)接受一组输入, (2)生成相应的输出, (3)由一组可调整参数描述。 当我们使用softmax回归时,一个单层本身就是模型。 然而,即使我们随后引入了多层感知机,我们仍然可以认为该模型保留了上面所说的基本架构。

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

事实证明,研究讨论“比单个层大”但“比整个模型小”的组件更有价值。 例如,在计算机视觉中广泛流行的ResNet-152架构就有数百层, 这些层是由层组(groups of layers)的重复模式组成。 这个ResNet架构赢得了2015年ImageNet和COCO计算机视觉比赛 的识别和检测任务 (He et al., 2016)。 目前ResNet架构仍然是许多视觉任务的首选架构。 在其他的领域,如自然语言处理和语音, 层组以各种重复模式排列的类似架构现在也是普遍存在。

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

../_images/blocks.svg

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

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

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

from mxnet import np, npx
from mxnet.gluon import nn

npx.set_np()

net = nn.Sequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()

X = np.random.uniform(size=(2, 20))
net(X)
[07:03:07] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
array([[ 0.06240272, -0.03268593,  0.02582653,  0.02254182, -0.03728798,
        -0.04253786,  0.00540613, -0.01364186, -0.09915452, -0.02272738],
       [ 0.02816677, -0.03341204,  0.03565666,  0.02506382, -0.04136416,
        -0.04941845,  0.01738528,  0.01081961, -0.09932579, -0.01176298]])

在这个例子中,我们通过实例化nn.Sequential来构建我们的模型, 返回的对象赋给net变量。 接下来,我们反复调用net变量的add函数,按照想要执行的顺序添加层。 简而言之,nn.Sequential定义了一种特殊类型的Block, 即在Gluon中表示块的类,它维护Block的有序列表。 add函数方便将每个连续的Block添加到列表中。 请注意,每层都是Dense类的一个实例,Dense类本身就是Block的子类。 到目前为止,我们一直在通过net(X)调用我们的模型来获得模型的输出。 这实际上是net.forward(X)的简写, 这是通过Block类的__call__函数实现的一个Python技巧。 前向传播(forward)函数非常简单:它将列表中的每个Block连接在一起, 将每个Block的输出作为输入传递给下一层。

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>)

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

import tensorflow as tf

net = tf.keras.models.Sequential([
    tf.keras.layers.Dense(256, activation=tf.nn.relu),
    tf.keras.layers.Dense(10),
])

X = tf.random.uniform((2, 20))
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[-0.1256689 ,  0.03234727, -0.41341984,  0.05109158, -0.11376685,
         0.1656029 ,  0.13811918, -0.0332518 , -0.28478232, -0.04640551],
       [ 0.00945267,  0.01088307, -0.3047434 ,  0.05576317,  0.08904827,
         0.11957583,  0.10018335,  0.07535183, -0.17810427, -0.03584548]],
      dtype=float32)>

在这个例子中,我们通过实例化keras.models.Sequential来构建我们的模型, 层的执行顺序是作为参数传递的。 简而言之,Sequential定义了一种特殊的keras.Model, 即在Keras中表示一个块的类。 它维护了一个由Model组成的有序列表, 注意两个全连接层都是Model类的实例, 这个类本身就是Model的子类。 前向传播(call)函数也非常简单: 它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。 注意,到目前为止,我们一直在通过net(X)调用我们的模型来获得模型的输出。 这实际上是net.call(X)的简写, 这是通过Block类的__call__函数实现的一个Python技巧。

import warnings

warnings.filterwarnings(action='ignore')
import paddle
from paddle import nn
from paddle.nn import functional as F

net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))

X = paddle.rand([2, 20])
net(X)
Tensor(shape=[2, 10], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[ 0.07959973,  0.08434670,  0.28861243,  0.02199294, -0.07369962,
         -0.32505932, -0.12516798, -0.15013230,  0.13497017,  0.59734315],
        [-0.04754683,  0.34689084,  0.27686706, -0.01812172, -0.01948128,
         -0.13436005, -0.20257708, -0.07067576,  0.01238458,  0.61258602]])

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

5.1.1. 自定义块

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

  1. 将输入数据作为其前向传播函数的参数。

  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收任意维的输入,但是返回一个维度256的输出。

  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  4. 存储和访问前向传播计算所需的参数。

  5. 根据需要初始化模型参数。

  1. 将输入数据作为其前向传播函数的参数。

  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。

  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  4. 存储和访问前向传播计算所需的参数。

  5. 根据需要初始化模型参数。

  1. 将输入数据作为其前向传播函数的参数。

  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收任意维的输入,但是返回一个维度256的输出。

  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  4. 存储和访问前向传播计算所需的参数。

  5. 根据需要初始化模型参数。

  1. 将输入数据作为其前向传播函数的参数。

  2. 通过前向传播函数来生成输出。请注意,输出的形状可能与输入的形状不同。例如,我们上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出。

  3. 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。通常这是自动发生的。

  4. 存储和访问前向传播计算所需的参数。

  5. 根据需要初始化模型参数。

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

class MLP(nn.Block):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self, **kwargs):
        # 调用MLP的父类Block的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__(**kwargs)
        self.hidden = nn.Dense(256, activation='relu')  # 隐藏层
        self.out = nn.Dense(10)  # 输出层

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def forward(self, X):
        return self.out(self.hidden(X))
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)))
class MLP(tf.keras.Model):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用MLP的父类Model的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)
        super().__init__()
        # Hiddenlayer
        self.hidden = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)
        self.out = tf.keras.layers.Dense(units=10)  # Outputlayer

    # 定义模型的前向传播,即如何根据输入X返回所需的模型输出
    def call(self, X):
        return self.out(self.hidden((X)))
class MLP(nn.Layer):
    # 用模型参数声明层。这里,我们声明两个全连接的层
    def __init__(self):
        # 调用`MLP`的父类Layer的构造函数来执行必要的初始化。
        # 这样,在类实例化时也可以指定其他函数参数,例如模型参数`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)))

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

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

我们来试一下这个函数:

net = MLP()
net.initialize()
net(X)
array([[-0.03989594, -0.1041471 ,  0.06799038,  0.05245074,  0.02526059,
        -0.00640342,  0.04182098, -0.01665319, -0.02067346, -0.07863817],
       [-0.03612847, -0.07210436,  0.09159479,  0.07890771,  0.02494172,
        -0.01028665,  0.01732428, -0.02843242,  0.03772651, -0.06671704]])
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>)
net = MLP()
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[ 0.14015174,  0.17783523,  0.03422496,  0.23184124,  0.310251  ,
         0.14864878, -0.5013749 , -0.0734642 , -0.03820562, -0.12923583],
       [ 0.47990555,  0.42501003,  0.10588682,  0.03492985,  0.2023867 ,
         0.25548872, -0.50454026, -0.39808106, -0.00930042, -0.17727089]],
      dtype=float32)>
net = MLP()
net(X)
Tensor(shape=[2, 10], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[-0.02695144, -0.07686033, -0.17291248, -0.10180631,  0.07875662,
          0.17861664, -0.18333656, -0.05037620, -0.04392751,  0.03499488],
        [ 0.06521086, -0.10377733, -0.22762510, -0.01807910,  0.12862802,
          0.31453782, -0.13465761,  0.01630586, -0.17804416, -0.00087316]])

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

5.1.2. 顺序块

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

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

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

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

class MySequential(nn.Block):
    def add(self, block):
    # 这里,block是Block子类的一个实例,我们假设它有一个唯一的名称。我们把它
    # 保存在'Block'类的成员变量_children中。block的类型是OrderedDict。
    # 当MySequential实例调用initialize函数时,系统会自动初始化_children
    # 的所有成员
        self._children[block.name] = block

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for block in self._children.values():
            X = block(X)
        return X

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

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字典中查找需要初始化参数的子块。

class MySequential(tf.keras.Model):
    def __init__(self, *args):
        super().__init__()
        self.modules = []
        for block in args:
            # 这里,block是tf.keras.layers.Layer子类的一个实例
            self.modules.append(block)

    def call(self, X):
        for module in self.modules:
            X = module(X)
        return X
class MySequential(nn.Layer):
    def __init__(self, *layers):
        super(MySequential, self).__init__()
        # 如果传入的是一个tuple
        if len(layers) > 0 and isinstance(layers[0], tuple):
            for name, layer in layers:
                # add_sublayer方法会将layer添加到self._sub_layers(一个tuple)
                self.add_sublayer(name, layer)
        else:
            for idx, layer in enumerate(layers):
                self.add_sublayer(str(idx), layer)

    def forward(self, X):
        # OrderedDict保证了按照成员添加的顺序遍历它们
        for layer in self._sub_layers.values():
            X = layer(X)
        return X

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

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

net = MySequential()
net.add(nn.Dense(256, activation='relu'))
net.add(nn.Dense(10))
net.initialize()
net(X)
array([[-0.07645682, -0.01130233,  0.04952145, -0.04651389, -0.04131573,
        -0.05884133, -0.0621381 ,  0.01311472, -0.01379425, -0.02514282],
       [-0.05124625,  0.00711231, -0.00155935, -0.07555379, -0.06675334,
        -0.01762914,  0.00589084,  0.01447191, -0.04330775,  0.03317726]])
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>)
net = MySequential(
    tf.keras.layers.Dense(units=256, activation=tf.nn.relu),
    tf.keras.layers.Dense(10))
net(X)
<tf.Tensor: shape=(2, 10), dtype=float32, numpy=
array([[ 0.4245665 ,  0.1554529 , -0.06504549,  0.0987289 , -0.08489662,
         0.16747624,  0.20746413,  0.05763938, -0.16166216,  0.382744  ],
       [ 0.47101185, -0.0233981 ,  0.21728408,  0.14111494, -0.18493696,
         0.08736669,  0.13651624,  0.3103686 , -0.09440522,  0.35759482]],
      dtype=float32)>
net = MySequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
Tensor(shape=[2, 10], dtype=float32, place=Place(cpu), stop_gradient=False,
       [[ 0.08128382, -0.06904822, -0.27352425, -0.19023108,  0.31156605,
         -0.03084018, -0.26239276,  0.12302527, -0.02516832,  0.27859986],
        [ 0.15454747, -0.19200316, -0.15801765, -0.17941876,  0.19492081,
          0.21714795, -0.44244379,  0.07100448,  0.24661955,  0.33095008]])

请注意,MySequential的用法与之前为Sequential类编写的代码相同 (如 4.3节 中所述)。

5.1.3. 在前向传播函数中执行代码

Sequential类使模型构造变得简单, 允许我们组合新的架构,而不必定义自己的类。 然而,并不是所有的架构都是简单的顺序架构。 当需要更强的灵活性时,我们需要定义自己的块。 例如,我们可能希望在前向传播函数中执行Python的控制流。 此外,我们可能希望执行任意的数学运算, 而不是简单地依赖预定义的神经网络层。

到目前为止, 我们网络中的所有操作都对网络的激活值及网络的参数起作用。 然而,有时我们可能希望合并既不是上一层的结果也不是可更新参数的项, 我们称之为常数参数(constant parameter)。 例如,我们需要一个计算函数 \(f(\mathbf{x},\mathbf{w}) = c \cdot \mathbf{w}^\top \mathbf{x}\)的层, 其中\(\mathbf{x}\)是输入, \(\mathbf{w}\)是参数, \(c\)是某个在优化过程中没有更新的指定常量。 因此我们实现了一个FixedHiddenMLP类,如下所示:

class FixedHiddenMLP(nn.Block):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # 使用get_constant函数创建的随机权重参数在训练期间不会更新(即为常量参数)
        self.rand_weight = self.params.get_constant(
            'rand_weight', np.random.uniform(size=(20, 20)))
        self.dense = nn.Dense(20, activation='relu')

    def forward(self, X):
        X = self.dense(X)
        # 使用创建的常量参数以及relu和dot函数
        X = npx.relu(np.dot(X, self.rand_weight.data()) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.dense(X)
        # 控制流
        while np.abs(X).sum() > 1:
            X /= 2
        return X.sum()
class FixedHiddenMLP(nn.Module):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变
        self.rand_weight = torch.rand((20, 20), requires_grad=False)
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数
        X = F.relu(torch.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:
            X /= 2
        return X.sum()
class FixedHiddenMLP(tf.keras.Model):
    def __init__(self):
        super().__init__()
        self.flatten = tf.keras.layers.Flatten()
        # 使用tf.constant函数创建的随机权重参数在训练期间不会更新(即为常量参数)
        self.rand_weight = tf.constant(tf.random.uniform((20, 20)))
        self.dense = tf.keras.layers.Dense(20, activation=tf.nn.relu)

    def call(self, inputs):
        X = self.flatten(inputs)
        # 使用创建的常量参数以及relu和matmul函数
        X = tf.nn.relu(tf.matmul(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数。
        X = self.dense(X)
        # 控制流
        while tf.reduce_sum(tf.math.abs(X)) > 1:
            X /= 2
        return tf.reduce_sum(X)
class FixedHiddenMLP(nn.Layer):
    def __init__(self):
        super().__init__()
        # 不计算梯度的随机权重参数。因此其在训练期间保持不变。
        self.rand_weight = paddle.rand([20, 20])
        self.linear = nn.Linear(20, 20)

    def forward(self, X):
        X = self.linear(X)
        # 使用创建的常量参数以及relu和mm函数。
        X = F.relu(paddle.tensor.mm(X, self.rand_weight) + 1)
        # 复用全连接层。这相当于两个全连接层共享参数。
        X = self.linear(X)
        # 控制流
        while X.abs().sum() > 1:
            X /= 2
        return X.sum()

在这个FixedHiddenMLP模型中,我们实现了一个隐藏层, 其权重(self.rand_weight)在实例化时被随机初始化,之后为常量。 这个权重不是一个模型参数,因此它永远不会被反向传播更新。 然后,神经网络将这个固定层的输出通过一个全连接层。

注意,在返回输出之前,模型做了一些不寻常的事情: 它运行了一个while循环,在\(L_1\)范数大于\(1\)的条件下, 将输出向量除以\(2\),直到它满足条件为止。 最后,模型返回了X中所有项的和。 注意,此操作可能不会常用于在任何实际任务中, 我们只展示如何将任意代码集成到神经网络计算的流程中。

net = FixedHiddenMLP()
net.initialize()
net(X)
array(0.52637565)
net = FixedHiddenMLP()
net(X)
tensor(0.1862, grad_fn=<SumBackward0>)
net = FixedHiddenMLP()
net(X)
<tf.Tensor: shape=(), dtype=float32, numpy=0.7753998>
net = FixedHiddenMLP()
net(X)
Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [-0.06332191])

我们可以混合搭配各种组合块的方法。 在下面的例子中,我们以一些想到的方法嵌套块。

class NestMLP(nn.Block):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.net = nn.Sequential()
        self.net.add(nn.Dense(64, activation='relu'),
                     nn.Dense(32, activation='relu'))
        self.dense = nn.Dense(16, activation='relu')

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

chimera = nn.Sequential()
chimera.add(NestMLP(), nn.Dense(20), FixedHiddenMLP())
chimera.initialize()
chimera(X)
array(0.97720534)
class NestMLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)
tensor(0.2183, grad_fn=<SumBackward0>)
class NestMLP(tf.keras.Model):
    def __init__(self):
        super().__init__()
        self.net = tf.keras.Sequential()
        self.net.add(tf.keras.layers.Dense(64, activation=tf.nn.relu))
        self.net.add(tf.keras.layers.Dense(32, activation=tf.nn.relu))
        self.dense = tf.keras.layers.Dense(16, activation=tf.nn.relu)

    def call(self, inputs):
        return self.dense(self.net(inputs))

chimera = tf.keras.Sequential()
chimera.add(NestMLP())
chimera.add(tf.keras.layers.Dense(20))
chimera.add(FixedHiddenMLP())
chimera(X)
<tf.Tensor: shape=(), dtype=float32, numpy=0.7313081>
class NestMLP(nn.Layer):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),
                                 nn.Linear(64, 32), nn.ReLU())
        self.linear = nn.Linear(32, 16)

    def forward(self, X):
        return self.linear(self.net(X))

chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
chimera(X)
Tensor(shape=[1], dtype=float32, place=Place(cpu), stop_gradient=False,
       [0.07904987])

5.1.4. 效率

读者可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题全局解释器锁 是众所周知的。 在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。

提高Python速度的最好方法是完全避免使用Python。 Gluon这样做的一个方法是允许混合式编程(hybridization),这将在后面描述。 Python解释器在第一次调用块时执行它。 Gluon运行时记录正在发生的事情,以及下一次它将对Python调用加速。 在某些情况下,这可以大大加快运行速度, 但当控制流(如上所述)在不同的网络通路上引导不同的分支时,需要格外小心。 我们建议感兴趣的读者在读完本章后,阅读混合式编程部分( 12.1节 )来了解编译。

读者可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题全局解释器锁 是众所周知的。 在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。

读者可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题全局解释器锁 是众所周知的。 在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。

你可能会开始担心操作效率的问题。 毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、 代码执行和许多其他的Python代码。 Python的问题全局解释器锁 是众所周知的。 在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。

5.1.5. 小结

  • 一个块可以由许多层组成;一个块可以由许多块组成。

  • 块可以包含代码。

  • 块负责大量的内部处理,包括参数初始化和反向传播。

  • 层和块的顺序连接由Sequential块处理。

5.1.6. 练习

  1. 如果将MySequential中存储块的方式更改为Python列表,会出现什么样的问题?

  2. 实现一个块,它以两个块为参数,例如net1net2,并返回前向传播中两个网络的串联输出。这也被称为平行块。

  3. 假设我们想要连接同一网络的多个实例。实现一个函数,该函数生成同一个块的多个实例,并在此基础上构建更大的网络。