使用 Apache MXNet Gluon 创建神经网络:从 Dense 层到自定义 Block 与 HybridBlock 实战指南
2026/9/20 22:22:42 网站建设 项目流程

使用 Apache MXNet Gluon 创建神经网络:从 Dense 层到自定义 Block 与 HybridBlock 实战指南

【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mx/mxnet

本教程是 MXNet 快速入门系列的第 2 步,聚焦于使用 Gluon 高阶 API 创建神经网络。你将学会从单个Dense全连接层出发,依次掌握nn.Sequential快速堆叠网络、基于nn.Block定制任意网络架构(含经典 LeNet)、用Parameter编写自定义层,以及利用HybridBlock在命令式与符号式编程之间自由切换并获得性能提升,最后掌握模型的保存、加载与可视化方法。读完本文,你可以独立用 Gluon 搭建、训练、导出并部署一个神经网络模型。

前置知识:本文默认你已完成本系列 Step 1: Manipulate data with NP on MXNet,熟悉mxnet.np的数组操作。

准备工作:导入必要的包

在 MXNet 中,np包(mxnet.np)提供与 NumPy 一致的数组接口,而神经网络的层与容器则位于 Gluon 的两个模块中:

  1. mxnet.gluon.nn:由 MXNet 团队维护的正式神经网络层模块;
  2. mxnet.gluon.contrib.nn:由社区贡献的实验性模块。

使用下面的命令导入本步所需的包,并调用npx.set_np()将 MXNet 切换到 numpy 风格模式:

from mxnet import np, npx from mxnet.gluon import nn npx.set_np() # Change MXNet to the numpy-like mode.

从源码结构看,gluon.nn中按类别组织了大量内置层:顺序容器(SequentialHybridSequential)、基础层(DenseActivationDropoutFlatten)、卷积层(Conv1D/2D/3D)、池化层(MaxPoolAvgPool系列)、归一化层(BatchNormLayerNorm等)、嵌入层与高级激活层(LeakyReLUELUGELU等),完整清单可查阅 gluon.nn API 文档。

创建神经网络的第一个层:Dense

最基础的神经网络层是Dense 层(全连接层 / 稠密连接层):输入层的每个节点都与下一层的每个节点相连。创建一个输出维度为 5 的 Dense 层:

layer = nn.Dense(5) layer # output: Dense(-1 -> 5, linear)

输出中的-1表示输入层大小在初始化时尚未指定,会被推迟到第一次前向传播时根据输入数据形状自动推断。

如果你事先知道输入维度,可以直接通过in_units参数指定:

layer = nn.Dense(5, in_units=3) layer

Dense 层实现的计算为:

$$output = \sigma(W \cdot X + b)$$

其中 $W$ 为权重矩阵、$b$ 为偏置向量、$\sigma$ 为激活函数。可以通过activation参数为层附加激活函数:

layer = nn.Dense(5, in_units=3, activation='relu')

从源码来看,Dense类定义在 python/mxnet/gluon/nn/basic_layers.py,其完整签名支持更多参数:use_bias(默认True,是否使用偏置)、flatten(默认True,是否将输入除第一维外的所有维度展平)、dtype(默认'float32')、weight_initializerbias_initializer(默认偏置初始化为'zeros')。其forward内部通过npx.fully_connected完成矩阵乘加运算后再套用激活函数。

初始化权重与前向传播

创建网络后需要先初始化权重。Gluon 默认的初始化方式是在 $[-0.7, 0.7]$ 区间内均匀随机取值:

layer.initialize()

Note:关于初始化的深入探讨会在本系列的后续教程中展开。

初始化完成后即可向网络输入数据执行前向传播(forward pass)。下面的例子创建形状为(10, 3)的随机输入x,送入层中计算输出:

x = np.random.uniform(-1, 1, (10, 3)) layer(x)

输入为(10, 3),Dense 层输出形状为(10, 5)

当你未指定in_units参数时,系统会在首次前向传播时根据输入数据自动推断输入维度。这一点在源码中由Dense.infer_shape实现(basic_layers.py):若flatten=True,会将除 batch 维外所有维度尺寸相乘得到in_units;若flatten=False,则取最后一维作为输入维度。

可以通过layer.params查看该层全部参数:

layer.params

权重和偏置可以通过.data()方法访问:

layer.weight.data()

使用 nn.Sequential 将层链成网络

Sequential提供了一种快速搭建网络的途径,适用于"层像煎饼一样堆叠"的常见网络结构:每一层的输出直接作为下一层的输入。使用net.add()依次添加层即可。下面用它把前面的 Dense 层组合成一个 3 层多层感知机(MLP):

net = nn.Sequential() net.add(nn.Dense(5, in_units=3, activation='relu'), nn.Dense(25, activation='relu'), nn.Dense(2)) net

层按照添加顺序排列,索引从 0 开始,可以用[]下标访问指定层:

net[1]

从实现上看,Sequential继承自Block(basic_layers.py):add()内部将每个子块追加到_layers列表并调用register_child注册;forward()则按序执行所有子块并把上一块的输出作为下一块的输入。源码还提供了一条性能提示:当Sequential的所有子层都是HybridBlock时,会给出警告并建议改用HybridSequential以获得最佳性能——这正是后文要介绍的混合式容器。

灵活创建自定义网络架构:nn.Block

nn.Sequential只能使用gluon.nn中现成的层,且预定义了顺序执行的前向函数。当内置层无法满足需求(例如要搭建 ResNet 这类由可复用复杂组件构成的网络)时,就需要自定义架构。

在 Gluon 中,每一个神经网络层都由基类nn.Block定义Block的核心职责是定义一个forward方法:接收输入x,产生输出。它既可以简单到只施加一个激活函数,也可以组合多个层乃至多个 Block,构建 ResNet 级别的复杂网络。

自定义 Block 只需实现两个方法:

  • __init__:创建所需的层;
  • forward:定义前向计算逻辑。

先看一个最小骨架:

class Net(nn.Block): def __init__(self): super().__init__() def forward(self, x): return x

再看一个完整的 MLP:

class MLP(nn.Block): def __init__(self): super().__init__() self.dense1 = nn.Dense(5, activation='relu') self.dense2 = nn.Dense(25, activation='relu') self.dense3 = nn.Dense(2) def forward(self, x): layer1 = self.dense1(x) layer2 = self.dense2(layer1) layer3 = self.dense3(layer2) return layer3 net = MLP() net

每个层内部的参数保存在Parameter类中,可通过params()方法访问:

net.dense1.params

从 block.py 的源码可以看出Block的设计精巧之处:它的__setattr__会被重载——当你把子BlockParameter赋值为实例属性时,会自动完成注册(register_child/ 加入_reg_params),因此collect_params()可以递归收集整棵网络树上的全部参数,这也是后续save_parametersexport等功能的基础。

使用 Parameter 创建自定义层(Blocks API)

MXNet 用Parameter类保存每一层的参数。内置层(如 Dense)内部正是通过Parameter创建权重和偏置的;如果你想在现有层基础上增加额外计算,可以基于Parameter自己实现。

先实例化参数,用shape参数指定形状(例如形状(5, -1),其中 -1 表示维度待推断):

from mxnet.gluon import Parameter weight = Parameter("custom_parameter_weight", shape=(5, -1)) bias = Parameter("custom_parameter_bias", shape=(5, -1)) weight, bias

Parameter还包含grad_req参数,用于指定该参数梯度的捕获方式。默认值为grad_req='write',即每次梯度写入 grad 时都会更新。底层机制上,这让 Gluon 知道需要对其底层数组调用.attach_grad()

基于此,可以创建自己的全连接自定义层——用nn.Block的骨架实现一个不带激活函数的w*x + b计算:

class custom_layer(nn.Block): def __init__(self, out_units, in_units=0): super().__init__() self.weight = Parameter("weight", shape=(in_units, out_units), allow_deferred_init=True) self.bias = Parameter("bias", shape=(out_units,), allow_deferred_init=True) def forward(self, x): return np.dot(x, self.weight.data()) + self.bias.data()

注意allow_deferred_init=TrueParameter允许在对应数据尚未实例化之前就被创建。例如实例化 Block 时各参数的形状还需要推断,Parameter会等待形状确定后再分配内存。

dense = custom_layer(3, in_units=5) dense.initialize() dense(np.random.uniform(size=(4, 5)))

类似地,可以用nn.Block完整复现经典的LeNet网络——前两段使用内置Conv2D+MaxPool2D,后接 Dense 层,并将最后一层换成上面自定义的custom_layer

class LeNet(nn.Block): def __init__(self): super().__init__() self.conv1 = nn.Conv2D(channels=6, kernel_size=3, activation='relu') self.pool1 = nn.MaxPool2D(pool_size=2, strides=2) self.conv2 = nn.Conv2D(channels=16, kernel_size=3, activation='relu') self.pool2 = nn.MaxPool2D(pool_size=2, strides=2) self.dense1 = nn.Dense(120, activation="relu") self.dense2 = nn.Dense(84, activation="relu") self.dense3 = nn.Dense(10) def forward(self, x): x = self.conv1(x) x = self.pool1(x) x = self.conv2(x) x = self.pool2(x) x = self.dense1(x) x = self.dense2(x) x = self.dense3(x) return x lenet = LeNet()

使用自定义层作为最后一层的 LeNet 变体:

class LeNet_custom(nn.Block): def __init__(self): super().__init__() self.conv1 = nn.Conv2D(channels=6, kernel_size=3, activation='relu') self.pool1 = nn.MaxPool2D(pool_size=2, strides=2) self.conv2 = nn.Conv2D(channels=16, kernel_size=3, activation='relu') self.pool2 = nn.MaxPool2D(pool_size=2, strides=2) self.dense1 = nn.Dense(120, activation="relu") self.dense2 = nn.Dense(84, activation="relu") self.dense3 = custom_layer(10, 84) def forward(self, x): x = self.conv1(x) x = self.pool1(x) x = self.conv2(x) x = self.pool2(x) x = self.dense1(x) x = self.dense2(x) x = self.dense3(x) return x lenet_custom = LeNet_custom()

用 28×28 的单通道随机图像同时跑通两个 LeNet,验证自定义层与内置层行为一致:

image_data = np.random.uniform(-1, 1, (1, 1, 28, 28)) lenet.initialize() lenet_custom.initialize() print("Lenet:") print(lenet(image_data)) print("Custom Lenet:") print(lenet_custom(image_data))

同样可以用.data方法访问任意一层的权重与偏置,例如访问第一层的权重和第六层的偏置形状:

lenet.conv1.weight.data().shape, lenet.dense1.bias.data().shape

使用预定义(预训练)架构

如果不想从零搭建模型,而是希望在常见数据集上用经典模型快速复现或作为 baseline,Gluon 的Model Zoo直接提供了开箱即用的现成架构,例如计算机视觉领域的 ResNet、自然语言处理领域的 BERT 等(对应 Gluon CV / Gluon NLP 模型库)。在仓库内可通过 gluon 模型库目录 查看已收录的视觉模型实现。

使用示例——加载预训练的 ResNet50 v2 并做一次前向传播:

from mxnet.gluon import model_zoo net = model_zoo.vision.resnet50_v2(pretrained=True) net.hybridize() dummy_input = np.ones(shape=(1, 3, 224, 224)) output = net(dummy_input) output.shape

为网络选择编程范式:命令式还是符号式?

MXNet 的 Gluon API 采用**命令式编程(Imperative)范式:对熟悉 Python 的用户而言,原型开发快、易于调试、控制流自然。但在后端,MXNet 也可以通过符号式 / 声明式编程(Symbolic / Declarative)**把网络转换为静态图,从而对算子进行底层优化。静态图的缺点是灵活性受限:任何逻辑都必须编码为图中特殊的算子(如scanwhile_loopcond),而且难以调试。

如何兼顾符号式编程的性能与命令式编程的灵活、易调试?答案是HybridBlock

HybridBlock既可以用真实输入、真实函数以完全命令式的方式运行,也可以作用于占位符以符号方式运行。Gluon 将大部分细节隐藏在底层,通常只有在自己编写层时才需要了解其工作原理。

先创建混合式顺序容器:

net_hybrid_seq = nn.HybridSequential() net_hybrid_seq.add(nn.Dense(5, in_units=3, activation='relu'), nn.Dense(25, activation='relu'), nn.Dense(2)) net_hybrid_seq

调用hybridize()即可编译并优化HybridSequential

net_hybrid_seq.hybridize()

从源码看,HybridSequential(basic_layers.py)与Sequential结构一致,但继承自HybridBlock;而hybridize()定义在 block.py,它会递归地把整棵网络树转化为符号化图。此外,仓库内 MLP 等内置层全部继承自HybridBlock,意味着任何完全由内置层构成的网络,都可以通过一次.hybridize()调用获得编译加速。

使用 Parameter 创建自定义层(HybridBlocks API)

在实例化自定义层时,如果通过in_units指定了输入维度,权重会按该形状初始化;如果输入维度未知,则形状被推迟到第一次前向传播时确定。对于自定义HybridBlock层,可以定义infer_shape()方法让形状在运行时被推断:

class CustomLayer(nn.HybridBlock): def __init__(self, out_units, in_units=-1): super().__init__() self.weight = Parameter("weight", shape=(in_units, out_units), allow_deferred_init=True) self.bias = Parameter("bias", shape=(out_units,), allow_deferred_init=True) def forward(self, x): print(self.weight.shape, self.bias.shape) return np.dot(x, self.weight.data()) + self.bias.data() def infer_shape(self, x): print(self.weight.shape, x.shape) self.weight.shape = (x.shape[-1], self.weight.shape[1]) dense = CustomLayer(3) dense.initialize() dense(np.random.uniform(size=(4, 5)))

性能对比:hybridize 前后的速度差

为了直观感受混合化带来的加速,可以对比网络在 hybridize 前后各执行 1000 次前向传播的耗时:

from time import time def benchmark(net, x): y = net(x) start = time() for i in range(1, 1000): y = net(x) return time() - start x_bench = np.random.normal(size=(1, 512)) net_hybrid_seq = nn.HybridSequential() net_hybrid_seq.add(nn.Dense(256, activation='relu'), nn.Dense(128, activation='relu'), nn.Dense(2)) net_hybrid_seq.initialize() print('Before hybridizing: %.4f sec' % (benchmark(net_hybrid_seq, x_bench))) net_hybrid_seq.hybridize() print('After hybridizing: %.4f sec' % (benchmark(net_hybrid_seq, x_bench)))

再进一步,HybridBlockBlockAPI 的混合版本。与BlocksAPI 类似,只需为HybridBlock定义接收输入xforward函数,MXNet 会在后端负责模型的混合化,无需改动代码即可切换到符号式范式:

from mxnet.gluon import HybridBlock class MLP_Hybrid(HybridBlock): def __init__(self): super().__init__() self.dense1 = nn.Dense(256, activation='relu') self.dense2 = nn.Dense(128, activation='relu') self.dense3 = nn.Dense(2) def forward(self, x): layer1 = self.dense1(x) layer2 = self.dense2(layer1) layer3 = self.dense3(layer2) return layer3 net_hybrid = MLP_Hybrid() net_hybrid.initialize() print('Before hybridizing: %.4f sec' % (benchmark(net_hybrid, x_bench))) net_hybrid.hybridize() print('After hybridizing: %.4f sec' % (benchmark(net_hybrid, x_bench)))

只要一个HybridBlock的前向计算完全由其他HybridBlock组成,就可以通过调用该块的.hybridize()方法编译这整段网络。由于 MXNet 的全部预定义层都是HybridBlock,完全由预定义层组成的网络调用.hybridize()后即可编译并以更快的速度运行。

保存与加载模型

BlocksAPI 支持在训练期间或训练结束后保存模型,便于部署推理、避免重新训练,也支持用 Python 等训练生态丰富的语言训练、再用其他语言做推理。MXNet 提供两种保存方式:

  1. 仅保存 / 加载模型权重(参数);
  2. 同时保存 / 加载模型权重与网络架构。

方式一:仅保存 / 加载模型参数

使用save_parametersload_parameters方法保存、加载模型权重。以最简单的layer为例(这里保存的是训练后的模型参数):

file_name = 'layer.params' layer.save_parameters(file_name)

要加载参数,需要先重建模型结构,再载入参数:

def build_model(): layer = nn.Dense(5, in_units=3, activation='relu') return layer layer_new = build_model()
layer_new.load_parameters('layer.params')

Notesave_parametersload_parameters适用于基于Block(而非HybridBlock)构建的模型。这类模型可能有复杂架构,且在运行期间可能发生变化——例如用 if-else 条件语句在两个不同架构之间选择。

方式二:同时保存 / 加载模型参数与架构

对于基于HybridBlock的模型,网络架构是静态的、运行期间不会改变,因此模型参数架构都可以通过exportimports方法一并保存、加载。

对上面的MLP_Hybrid调用export导出模型。从 block.py 中export的实现 看,export会把网络架构导出为.json文件、模型参数导出为.params文件(命名规则为{path}-symbol.json{path}-{epoch 四位数}.params),并支持epoch参数指定保存的轮次编号:

net_hybrid.export('MLP_hybrid')

运行后会生成MLP_hybrid-symbol.jsonMLP_hybrid-0000.params两个文件。注意:export要求先对该块调用过hybridize()并至少完成一次前向传播(否则会抛出RuntimeError,提示先 hybridize 再 forward),因为导出的符号图正是混合化缓存的结果。

加载时使用gluon.nn.SymbolBlock.imports,指定符号文件、输入名列表与参数文件:

import warnings with warnings.catch_warnings(): warnings.simplefilter("ignore") net_loaded = nn.SymbolBlock.imports("MLP_hybrid-symbol.json", ['data'], "MLP_hybrid-0000.params", device=None)

加载完成后即可直接对数据执行前向传播:

net_loaded(x_bench)

可视化模型结构:summary()

Block.summary()方法可以查看块的形状参数与参数信息。当多个块组合成一个模型后,对模型调用summary()可以查看每个块的摘要、参数总数以及块在模型中的顺序。实现上,Block.summary()(block.py)需要执行一次数据前向传播以构建捕获形状与参数所需的图;另外该方法应在hybridize()之前调用,因为hybridize会把图转换为符号图,可能改变算子以进行最优计算。

看下面三个例子:单层网络layer、未混合化的 LeNet、以及混合化的 MLP Hybrid 网络。

layer.summary(x)
lenet.summary(image_data)

layerlenet都未混合化,可以轻松打印摘要;而net_hybrid之前已 hybridize,直接调用net_hybrid.summary(x_bench)会抛出AssertionError。要为混合化网络打印摘要,需要新建一个同架构的实例,先打印摘要再 hybridize:

net_hybrid_summary = MLP_Hybrid() net_hybrid_summary.initialize() net_hybrid_summary.summary(x_bench) net_hybrid_summary.hybridize()

下一步

至此你已经掌握了用 Gluon 创建、定制、混合化、保存与可视化神经网络的完整流程。接下来可以学习如何自动计算梯度,进入 Step 3: Automatic differentiation with autograd。本系列后续教程(Step 4: 训练组件、Step 6: 训练神经网络)会在此基础上展开损失函数、优化器与完整训练循环的讲解。

参考阅读

  • gluon.nn 模块 API 文档:内置层完整清单
  • Dense 层源码实现:含in_units推断逻辑与全部构造参数
  • Sequential / HybridSequential 源码实现:顺序容器的注册与执行机制
  • Block / HybridBlock 源码实现:基类设计、参数自动注册与collect_params
  • Parameter 类源码实现:参数声明、grad_req与延迟初始化
  • 模型导出实现:export生成.json.params文件的细节

【免费下载链接】mxnetLightweight, Portable, Flexible Distributed/Mobile Deep Learning with Dynamic, Mutation-aware Dataflow Dep Scheduler; for Python, R, Julia, Scala, Go, Javascript and more项目地址: https://gitcode.com/gh_mirrors/mx/mxnet

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询