仅使用“Sequential()”或“HybridSequential()”作为容器会有任何副作用吗?

Is there any side effect by using ‘Sequential()’ or ‘HybridSequential()’ as a container only?

我正在阅读有关 MxNet 的教程。编写者使用‘mxnet.gluon.nn.Sequential()’作为容器来存储一些块(见代码1);然后,他们重写了“def forward(self, x)”中块的连接(参见代码 2 和 3)。这样做有什么副作用吗?顺便问一下,‘Sequential()’和‘HybridSequential()’有什么区别。我尝试用一​​个列表来替换“顺序”,然后在初始化过程中收到以下警告。

“ToySSD.downsamplers” is a container with Blocks. Note that Blocks inside the list, tuple or dict will not be registered automatically. Make sure to register them using register_child() or switching to nn.Sequential/nn.HybridSequential instead.’

据我所知,如果你把一些块放在'mxnet.gluon.nn.Sequential()'或'mxnet.gluon.nn.HybridSequential()'中,这个动作就是告诉计算机这些块是连接的。但是,如果你在“forward”功能中设计块的关系,你就是在告诉计算机以另一种方式连接这些块。会导致混乱吗?如果我只在‘forward’中设计一些块连接,那么‘Sequential()’中其他没有在‘forward’函数中设计的块之间的关系是什么?

整个教程可以在here中找到。

代码1:

def toy_ssd_model(num_anchors, num_classes):
    downsamplers = nn.Sequential()
    for _ in range(3):
        downsamplers.add(down_sample(128))

    class_predictors = nn.Sequential()
    box_predictors = nn.Sequential()    
    for _ in range(5):
        class_predictors.add(class_predictor(num_anchors, num_classes))
        box_predictors.add(box_predictor(num_anchors))

    model = nn.Sequential()
    model.add(body(), downsamplers, class_predictors, box_predictors)
    return model

代码 2:

def toy_ssd_forward(x, model, sizes, ratios, verbose=False):    
    body, downsamplers, class_predictors, box_predictors = model
    anchors, class_preds, box_preds = [], [], []
    # feature extraction    
    x = body(x)
    for i in range(5):
        # predict
        anchors.append(MultiBoxPrior(
            x, sizes=sizes[i], ratios=ratios[i]))
        class_preds.append(
            flatten_prediction(class_predictors[i](x)))
        box_preds.append(
            flatten_prediction(box_predictors[i](x)))
        if verbose:
            print('Predict scale', i, x.shape, 'with', 
                  anchors[-1].shape[1], 'anchors')
        # down sample
        if i < 3:
            x = downsamplers[i](x)
        elif i == 3:
            x = nd.Pooling(
                x, global_pool=True, pool_type='max', 
                kernel=(x.shape[2], x.shape[3]))
    # concat data
    return (concat_predictions(anchors),
            concat_predictions(class_preds),
            concat_predictions(box_preds))

代码 3:

from mxnet import gluon
class ToySSD(gluon.Block):
    def __init__(self, num_classes, verbose=False, **kwargs):
        super(ToySSD, self).__init__(**kwargs)
        # anchor box sizes and ratios for 5 feature scales
        self.sizes = [[.2,.272], [.37,.447], [.54,.619], 
                      [.71,.79], [.88,.961]]
        self.ratios = [[1,2,.5]]*5
        self.num_classes = num_classes
        self.verbose = verbose
        num_anchors = len(self.sizes[0]) + len(self.ratios[0]) - 1
        # use name_scope to guard the names
        with self.name_scope():
            self.model = toy_ssd_model(num_anchors, num_classes)

    def forward(self, x):
        anchors, class_preds, box_preds = toy_ssd_forward(
            x, self.model, self.sizes, self.ratios, 
            verbose=self.verbose)
        # it is better to have class predictions reshaped for softmax computation       
        class_preds = class_preds.reshape(shape=(0, -1, self.num_classes+1))
        return anchors, class_preds, box_preds

在 Gluon 中,网络是使用 Block 构建的。如果某物不是 Block,则它不能成为 Gluon 网络的一部分。密集层是一个Block,卷积层是一个Block,池化层是一个Block,等等

有时您可能需要一个 Block,它不是 Gluon 中的预定义块,而是一系列预定义的 Gluon 块。例如,

Conv2D -> MaxPool2D -> Conv2D -> MaxPool2D -> Flatten -> Dense -> Dense

Gluon 没有执行上述操作序列的预定义块。但是 Gluon 确实有执行每个单独操作的块。因此,您可以创建自己的块,通过将预定义的 Gluon 块串在一起来执行上述操作序列。示例:

net = gluon.nn.HybridSequential()

with net.name_scope():

    # First convolution
    net.add(gluon.nn.Conv2D(channels=20, kernel_size=5, activation='relu'))
    net.add(gluon.nn.MaxPool2D(pool_size=2, strides=2))

    # Second convolution
    net.add(gluon.nn.Conv2D(channels=50, kernel_size=5, activation='relu'))
    net.add(gluon.nn.MaxPool2D(pool_size=2, strides=2))

    # Flatten the output before the fully connected layers
    net.add(gluon.nn.Flatten())

    # First fully connected layers with 512 neurons
    net.add(gluon.nn.Dense(512, activation="relu"))

    # Second fully connected layer with as many neurons as the number of classes
    net.add(gluon.nn.Dense(num_outputs))

当您创建这样的序列时,您可以使用 HybridSequentialSequential。要了解差异,您需要了解 difference between symbolic and imperative programming.

  • HybridBlock 是一个块,可以转换为符号图以加快执行速度。 HybridSequentialHybrid 个块的序列。
  • Blocks(不是混合的)是一个不能转换成符号图的块。 Sequential 是一系列非混合块。

一个区块是否是混合的取决于它是如何实现的。几乎所有预定义的 Gluon 块也是 HybridBlocks。有时有些块不能混合是有原因的。 Tree LSTM 就是一个例子。更常见的是,某些东西不是 Hybrid 只是因为编写它的人出于多种原因没有努力使其成为 Hybrid(例如:也许使其成为 Hybrid 不会带来很大的性能提升,或者可能很难使 block hybrid ).

请注意 SequentialHybridSequential 不仅仅是像 Python list 这样的容器。当您使用其中之一时,您实际上是在使用预先存在的块创建一个新的 Block。这就是为什么你不能用 Python list.

替换 Sequential

好的,所以你知道如何通过将预先存在的块串在一起来创建你自己的块。好的。如果您不想只通过一系列块传递数据怎么办?如果您想有条件地通过这些块之一传递数据怎么办?这是 ResNet 的示例:

class BasicBlockV1(HybridBlock):
    def __init__(self, channels, stride, downsample=False, in_channels=0, **kwargs):
        super(BasicBlockV1, self).__init__(**kwargs)
        self.body = nn.HybridSequential(prefix='')
        self.body.add(_conv3x3(channels, stride, in_channels))
        self.body.add(nn.BatchNorm())
        self.body.add(nn.Activation('relu'))
        self.body.add(_conv3x3(channels, 1, channels))
        self.body.add(nn.BatchNorm())
        if downsample:
            self.downsample = nn.HybridSequential(prefix='')
            self.downsample.add(nn.Conv2D(channels, kernel_size=1, strides=stride,
                                          use_bias=False, in_channels=in_channels))
            self.downsample.add(nn.BatchNorm())
        else:
            self.downsample = None

    def hybrid_forward(self, F, x):
        residual = x

        x = self.body(x)

        if self.downsample:
            residual = self.downsample(residual)

        x = F.Activation(residual+x, act_type='relu')

        return x

此代码使用预先存在的 Gluon 块创建一个新块。但它不仅仅是 运行 通过一些预先存在的块来处理数据。给定一些数据,该块通过主体 block 运行数据。但是,仅当此块是在 downsample 设置为 true 的情况下创建时,才通过 downsample 运行数据。然后它连接 bodydownsample 的输出以创建输出。正如您所看到的,发生的不仅仅是通过一系列块传递数据。这是当您通过子类化 HybridBlockBlock.

创建自己的块时

请注意,__init__ 函数创建了必要的块,forward 函数获取输入并通过在 __init__ 中创建的块运行输入。 forward 不会修改在 __init__ 中创建的块。它仅通过在 __init__.

中创建的块运行数据

在您引用的示例中,第一个代码块创建了 downsamplersclass_predictorsbox_predictors 等块。代码块 2 和 3 中的转发函数不会修改这些块。他们只是通过这些块传递输入数据。