Skip to content

Apache MXNet - Python API Symbol

本章介绍 mxnet.symbol API (通常导入为 mx.sym),它使得 Apache MXNet 能够进行符号式编程。符号式编程涉及首先定义一个计算图,然后可以对其进行编译、优化和执行。

MXNet 的 Symbol API 允许您通过组合符号操作来构建计算图。与命令式编程(例如 NDArray,操作立即执行)不同,符号式编程将图定义与执行分离。

关键特性和优势:

  • 计算图:符号代表图中的节点(操作或变量)。此图定义了数据和计算的流向。
  • 优化:定义后,图可以由 MXNet 的引擎分析和优化(例如,内存优化、操作融合),然后再执行。
  • 部署:符号图可以轻松地序列化并部署到各种环境(例如,移动设备、服务器),而无需定义它们的 Python 代码。
  • 声明式特性:您声明要计算 什么,而不是如何一步步计算。

在现代 MXNet 中,特别是在使用 Gluon API 时,用户通常不会广泛地从头构建纯符号图。相反,他们使用 gluon.HybridBlock,它允许以命令式方式定义模型。然后,通过调用 .hybridize(),HybridBlock 的前向传播在内部被转换为符号图,以获得性能优势。然而,理解 mx.symbol 对于理解这种混合化如何工作以及用于高级用例至关重要。

简单的符号表达式 z = x + y 示例:

import mxnet as mx
# 创建符号变量(数据的占位符)
x = mx.sym.Variable('x')
y = mx.sym.Variable('y')
# 通过对 'x' 和 'y' 应用 '+' 操作符构建新的符号 'z'
z = x + y
print(f"Symbol x: {x}")
print(f"Symbol y: {y}")
print(f"Symbol z (x+y): {z}")

输出显示了符号的名称和 z 的操作:

<html>
<body>
<p>
Symbol x: <Symbol x>
Symbol y: <Symbol y>
Symbol z (x+y): <Symbol _plus0>
</p>
</body>
</html>

要执行此符号图,您通常会将其与实际数据 (NDArray) 和执行器绑定,或者更常见的是在 Gluon HybridBlock 中使用它。

类定义
Symbol(handle)MXNet 符号 API 中表示节点或整个计算图的主要类。它可以表示变量、常量或操作。

mxnet.symbol 中的许多函数与 mxnet.ndarray 中的函数相似,但它们作用于 Symbol 对象来构建图。它们不立即执行计算,而是返回表示操作结果的新 Symbol 对象。

函数及参数定义
Activation(data, act_type=‘relu’, name=None, attr=None, out=None, **kwargs)创建一个应用激活函数的符号。data 是输入 Symbol。
BatchNorm(data, gamma, beta, moving_mean, moving_var, eps=0.001, momentum=0.9, fix_gamma=False, use_global_stats=False, output_mean_var=False, axis=1, cudnn_off=False, min_calib_range=None, max_calib_range=None, name=None, attr=None, out=None, **kwargs)创建用于批标准化的符号。
BilinearSampler(data, grid, cudnn_off=False, name=None, attr=None, out=None, **kwargs)创建用于双线性采样的符号。
BlockGrad(data, name=None, attr=None, out=None, **kwargs)创建一个符号,该符号停止对其输入 data 符号进行梯度计算。
cast(data, dtype, name=None, attr=None, out=None, **kwargs)创建一个符号,该符号将其输入 data 符号转换为新的 dtype。
zeros(shape, dtype=‘float32’, ctx_group=None, **kwargs)创建一个符号,表示给定 shape 和 dtype 的零数组。
ones(shape, dtype=‘float32’, ctx_group=None, **kwargs)创建一个符号,表示由一组成的数组。
full(shape, val, dtype=‘float32’, ctx_group=None, **kwargs)创建一个符号,表示填充了标量 val 的数组。
arange(start, stop=None, step=1.0, repeat=1, infer_range=True, dtype=‘float32’, ctx_group=None, **kwargs)创建一个符号,表示一系列等间距的值。
linspace(start, stop, num, endpoint=True, name=None, attr=None, out=None, **kwargs)创建一个符号,表示在指定区间 [start, stop] 内的 num 个等间距数字。
histogram(data, bins, range=None, name=None, attr=None, out=None, **kwargs)创建一个符号,该符号计算输入 data 符号的直方图。
power(base, exp, name=None, attr=None, out=None, **kwargs)按元素求幂。base 和 exp 可以是 Symbols 或标量。
softmax(data, axis=-1, temperature=None, dtype=None, name=None, attr=None, out=None, **kwargs)应用 softmax 函数,通常用作多类分类的输出激活。(注意:SoftmaxActivation 已过时;softmax 是首选)。

符号式使用 mx.sym.power:

import mxnet as mx
import numpy as np
# 标量幂(由 Python 在定义时计算,而不是符号操作)
# print(mx.sym.power(3, 5)) # 这将导致错误,因为 mx.sym.power 期望符号,或者 base/exp 是符号
# 符号变量的幂
x_sym = mx.sym.Variable('x_data')
y_sym = mx.sym.Variable('y_data')
# x^3 的符号
z_pow_x_cubed = mx.sym.power(x_sym, 3)
# 4^y 的符号
z_pow_4_y = mx.sym.power(4, y_sym) # 标量底数,符号指数
# x^y 的符号
z_pow_x_y = mx.sym.power(x_sym, y_sym)
print(f"Symbol for x^3: {z_pow_x_cubed}")
print(f"Symbol for 4^y: {z_pow_4_y}")
print(f"Symbol for x^y: {z_pow_x_y}")
# 要执行,绑定数据并使用执行器(或在 Gluon HybridBlock 中使用)
# 评估示例(旧式风格,仅用于说明绑定)
x_nd = mx.nd.array([1, 2, 3], ctx=mx.cpu())
executor_x_cubed = z_pow_x_cubed.simple_bind(ctx=mx.cpu(), x_data=x_nd.shape)
output_x_cubed = executor_x_cubed.forward(x_data=x_nd)[0]
print(f"x={x_nd.asnumpy()} 的 x^3 结果:{output_x_cubed.asnumpy()}")

输出:

<html>
<body>
<p>
Symbol for x^3: <Symbol _power_scalar0>
Symbol for 4^y: <Symbol _rpower_scalar0>
Symbol for x^y: <Symbol _power0>
Result of x^3 for x=[1. 2. 3.]: [ 1. 8. 27.]
</p>
</body>
</html>

使用 mx.sym.softmax(优于 SoftmaxActivation)的示例:

data_sym = mx.sym.Variable('input_data')
softmax_output_sym = mx.sym.softmax(data_sym, axis=1) # 对每一行的列应用 Softmax
print(f"Softmax 符号: {softmax_output_sym}")
# 评估示例
input_nd = mx.nd.array([[2., 0.9, -0.5, 4., 8.],
[4., -.7, 9., 2., 0.9]], ctx=mx.cpu())
# 使用 simple_bind 进行评估(说明性示例)
# 实际上,Gluon 在其框架内处理执行。
executor_softmax = softmax_output_sym.simple_bind(ctx=mx.cpu(), input_data=input_nd.shape)
output_softmax = executor_softmax.forward(input_data=input_nd)[0]
print(f"Softmax 输出:\n{output_softmax.asnumpy()}")

输出:

<html>
<body>
<p>
Softmax symbol: <Symbol softmax0>
Softmax output:
[[2.4258138e-03 8.0748333e-04 1.9912292e-04 1.7924475e-02 9.7864312e-01]
[6.6843745e-03 6.0796250e-05 9.9204916e-01 9.0463174e-04 3.0112563e-04]]
</p>
</body>
</html>

与 ndarray.contrib 类似,mxnet.symbol.contrib 提供实验性符号操作。这些操作可能会在未来的 MXNet 版本中更改或移除。

函数及参数定义
rand_zipfian(true_classes, num_sampled, range_max, name=None, attr=None, out=None, **kwargs)rand_zipfian 的符号版本,用于在计算图中从近似的 Zipfian 分布中抽取样本。
foreach(body, data, init_states, name=None)foreach 的符号版本,用于在图中定义类似循环的计算。
while_loop(cond, func, loop_vars, max_iterations=None, name=None)while_loop 的符号版本,用于在图中定义条件迭代计算。
cond(pred, then_branch, else_branch, name=None)cond 的符号版本,用于在图中定义条件分支。then_branch 和 else_branch 是返回 Symbols 的函数。
getnnz(data, axis=None, name=None, attr=None, out=None, **kwargs)计算稀疏张量 Symbol 中非零元素的符号操作符。
requantize(data, min_range, max_range, min_calib_range=None, max_calib_range=None, out_type=‘int8’, name=None, attr=None, out=None, **kwargs)用于数据重新量化的符号操作符,通常用于量化图推理。
index_copy(old_tensor, index_vector, new_tensor, name=None, attr=None, out=None, **kwargs)符号式地将元素从 new_tensor 复制到 old_tensor 的指定 index_vector 位置。
interleaved_matmul_encdec_qk(queries, keys, num_heads, name=None, attr=None, out=None, **kwargs)计算多头注意力(encoder-decoder)中 query-key 投影的矩阵乘法。这是 Transformer 模型的一种专用操作。

符号式使用 mx.sym.contrib.rand_zipfian:

from mxnet.symbol import contrib as contrib_sym
true_cls_sym = mx.sym.Variable('true_cls_data')
num_sampled = 5
range_max = 10
zipfian_group = contrib_sym.rand_zipfian(true_cls_sym, num_sampled, range_max)
# rand_zipfian 返回一组符号:samples, expected_count_true, expected_count_sample
samples_sym = zipfian_group[0]
print(f"Zipfian 样本的符号: {samples_sym}")
# 评估示例
true_cls_nd = mx.nd.array([3, 1], ctx=mx.cpu())
# 对于分组符号,绑定每个元素或使用处理分组的包装器。
# 为了简单起见,这里仅展示对 'samples' 部分的绑定。
# 注意:正确绑定分组符号可能更复杂。
executor_zipfian = samples_sym.simple_bind(ctx=mx.cpu(), true_cls_data=true_cls_nd.shape)
output_zipfian = executor_zipfian.forward(true_cls_data=true_cls_nd)[0]
print(f"评估的 Zipfian 样本(会因运行而异):\n{output_zipfian.asnumpy()}")

输出(实际样本会因运行而异):

<html>
<body>
<p>
Symbol for Zipfian samples: <Symbol rand_zipfian0_output0>
Evaluated Zipfian samples (will vary):
[[0 1 2 4 5]
[0 2 3 4 5]]
<NDArray 2x5 @cpu(0)>
</p>
</body>
</html>

mxnet.symbol.image 模块提供图像处理函数的符号版本,类似于 mxnet.ndarray.image。这些函数用于在计算图内定义图像增强和预处理步骤,这有利于性能,因为这些操作可以由图执行器进行融合或优化。

提供了诸如 adjust_lighting、crop、normalize、random_crop、random_lighting、random_resized_crop、resize、to_tensor 等函数,它们存在于 mx.sym.image 中,其行为与其在 mx.nd.image 中的对应函数类似,但它们作用于 Symbols 来构建图。

img_h, img_w, img_c = 224, 224, 3
# 图像批量(许多图像操作采用 N H W C 格式)的符号占位符
image_data_sym = mx.sym.Variable('image_batch_data', shape=(None, img_h, img_w, img_c))
# 符号式 to_tensor (HWC -> CHW, 缩放 0-255 到 0-1)
# 注意:symbol API 中的 to_tensor 可能根据特定版本/操作符期望 NCHW 或 NHWC 格式。
# 对于此示例,假设它处理 NHWC -> NCHW 并进行缩放。
tensor_sym = mx.sym.image.to_tensor(image_data_sym)
# 符号式标准化
mean_rgb = (0.485*255, 0.456*255, 0.406*255) # 0-255 范围内数据的均值
std_rgb = (0.229*255, 0.224*255, 0.225*255) # 0-255 范围内数据的标准差
# to_tensor 后,数据范围是 0-1,因此均值/标准差应针对 0-1 范围。
normalized_sym = mx.sym.image.normalize(tensor_sym,
mean=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225))
print(f"标准化图像数据的符号: {normalized_sym}")
# 这个 `normalized_sym` 随后可以输入到符号式 CNN 定义中。
# 为了评估,需要将其与实际图像数据绑定。
# dummy_image_batch_nd = mx.nd.random.uniform(0, 255, (1, img_h, img_w, img_c), ctx=mx.cpu())
# exec_norm = normalized_sym.simple_bind(ctx=mx.cpu(), image_batch_data=dummy_image_batch_nd.shape)
# output_norm = exec_norm.forward(image_batch_data=dummy_image_batch_nd)[0]
# print(f"标准化输出的形状:{output_norm.shape}") # 期望形状:(批量大小, 序列长度, 输出维度)

输出:

<html>
<body>
<p>
Symbol for normalized image data: <Symbol normalize0>
</p>
</body>
</html>

mxnet.symbol.random 模块提供了创建 Symbols 的函数,这些 Symbols 在执行时会生成随机数。这对于向模型添加随机性(例如 dropout)或在非常特定的场景下符号式初始化参数很有用(尽管通常参数初始化为 NDArray)。

提供了诸如 uniform、normal、randn、poisson、exponential、gamma、multinomial、negative_binomial、generalized_negative_binomial、shuffle、randint 以及各种 *_like 函数。它们接受形状参数和分布参数,返回表示随机采样操作的 Symbol。

# 符号式随机均匀生成
rand_unif_sym = mx.sym.random.uniform(low=-1, high=1, shape=(2, 3))
print(f"均匀随机数的符号: {rand_unif_sym}")
# 变量的符号式打乱
data_to_shuffle = mx.sym.Variable('shuffle_input')
shuffled_data_sym = mx.sym.random.shuffle(data_to_shuffle)
print(f"打乱数据的符号: {shuffled_data_sym}")
# 打乱操作的评估示例
input_nd_for_shuffle = mx.nd.array([[1,2,3],[4,5,6],[7,8,9]], ctx=mx.cpu())
exec_shuffle = shuffled_data_sym.simple_bind(ctx=mx.cpu(), shuffle_input=input_nd_for_shuffle.shape)
output_shuffled = exec_shuffle.forward(shuffle_input=input_nd_for_shuffle)[0]
print(f"原始数据:\n{input_nd_for_shuffle.asnumpy()}")
print(f"打乱的数据(会因运行而异):\n{output_shuffled.asnumpy()}")

输出(打乱的数据会因运行而异):

<html>
<body>
<p>
Symbol for uniform randoms: <Symbol random_uniform0>
Symbol for shuffled data: <Symbol random_shuffle0>
Original data:
[[1. 2. 3.]
[4. 5. 6.]
[7. 8. 9.]]
Shuffled data (will vary):
[[4. 5. 6.]
[7. 8. 9.]
[1. 2. 3.]]
</p>
</body>
</html>

MXNet 通过 mxnet.symbol.sparse(以及相应的 mxnet.ndarray.sparse 和 mxnet.numpy.sparse 模块)支持稀疏矩阵的操作。这对于处理高维稀疏数据的应用程序至关重要,例如推荐系统或具有大词汇表的自然语言处理。

稀疏 API 提供了用于常见任务的符号操作符:

函数及参数定义
ElementWiseSum(*args, **kwargs)符号式地按元素相加多个输入的稀疏或稠密 Symbols。比重复相加更高效。
Embedding(data, weight, input_dim, output_dim, dtype=‘float32’, sparse_grad=False, name=None, attr=None, out=None, **kwargs)创建 Embedding 层 Symbol。使用 weight 矩阵(即 Embedding 矩阵)将整数索引(通常来自稀疏输入 data)映射到稠密向量表示。sparse_grad=True 为权重启用稀疏梯度更新,这对于大型 Embedding 非常节省内存。
LinearRegressionOutput(data, label, grad_scale=1.0, name=None, attr=None, out=None, **kwargs)计算线性回归的平方损失。data 是预测值,label 是目标值。通常用作符号图中回归任务的最终层。
LogisticRegressionOutput(data, label, grad_scale=1.0, name=None, attr=None, out=None, **kwargs)应用 sigmoid 函数并计算逻辑回归损失。用于二分类。
MAERegressionOutput(data, label, grad_scale=1.0, name=None, attr=None, out=None, **kwargs)计算平均绝对误差 (Mean Absolute Error) 损失。
abs(data, name=None, attr=None, out=None, **kwargs)稀疏/稠密 Symbols 的按元素绝对值。
dot(lhs, rhs, transpose_a=False, transpose_b=False, name=None, attr=None, out=None, **kwargs)计算两个 Symbols 的点积,它们可以是稀疏或稠密的。支持对稀疏输入进行优化的矩阵乘法。
elemwise_add/sub/mul/div(lhs, rhs, name=None, attr=None, out=None, **kwargs)支持稀疏 Symbols 的按元素算术运算。
exp, sin, sign, sigmoid, etc.适用于稀疏/稠密 Symbols 的各种数学函数。
Optimizer-specific update functions (e.g., adagrad_update, adam_update, sgd_update)优化器更新规则的符号表示,在处理稀疏梯度时常由训练机制内部使用。

定义符号式 Embedding 层:

input_dim = 10000 # 词汇表大小
output_dim = 100 # Embedding 维度
batch_size = 4
sequence_length = 10
# 符号输入数据(例如,整数词索引)
# 形状:(批量大小, 序列长度) 或其他合适的形状
input_indices_sym = mx.sym.Variable('input_indices', shape=(batch_size, sequence_length))
# 符号式 Embedding 权重矩阵
embedding_weight_sym = mx.sym.Variable('embedding_weight', shape=(input_dim, output_dim))
# 创建 Embedding 符号
embedded_features_sym = mx.sym.Embedding(data=input_indices_sym,
weight=embedding_weight_sym,
input_dim=input_dim,
output_dim=output_dim,
sparse_grad=False) # 如果 input_dim 非常大,设置为 True 以进行稀疏更新
print(f"嵌入特征的符号: {embedded_features_sym}")
# 要使用它,需要将其与实际的 NDArray 数据(用于索引和权重)绑定。
# e.g., input_indices_nd = mx.nd.random.randint(0, input_dim, (batch_size, sequence_length), ctx=mx.cpu())
# embedding_weight_nd = mx.nd.random.uniform(-0.01, 0.01, (input_dim, output_dim), ctx=mx.cpu())
# executor = embedded_features_sym.simple_bind(ctx=mx.cpu(), \
# input_indices=input_indices_nd.shape, \
# embedding_weight=embedding_weight_nd.shape)
# output_embedded = executor.forward(is_train=False, \
# input_indices=input_indices_nd, \
# embedding_weight=embedding_weight_nd)[0]
# print(f"嵌入输出的形状:{output_embedded.shape}") # 期望形状:(批量大小, 序列长度, 输出维度)

这个符号式 embedded_features_sym 随后将连接到神经网络定义中的后续层(例如 RNN、Transformer 模型)。