Skip to content

Apache MXNet - Python API ndarray

本章探讨 mxnet.ndarray (通常导入为 mx.nd),它是 Apache MXNet 中用于数值计算的核心库。NDArray 是在 MXNet 中处理数据的基础。

MXNet 的 NDArray 提供了一个多功能的 N 维数组对象,类似于 NumPy 数组,但具有专为深度学习和高性能计算量身定制的关键特性:

  • 硬件加速:NDArray 操作可以在各种硬件上运行,包括 CPU 和 NVIDIA GPU,从而显著加速大型计算。
  • 自动并行化:MXNet 的执行引擎会自动在可用的硬件资源上并行化 NDArray 操作。
  • 异步执行:操作通常异步执行,尤其是在 GPU 上,这使得 Python 前端可以在计算在后台运行时继续处理。

注意:MXNet 2.0 引入了 mxnet.numpy API,旨在提高与 NumPy 的兼容性。虽然 mxnet.ndarray (mx.nd) 仍然是基础库,并在旧代码库和内部广泛使用,但新项目可能更喜欢 mxnet.numpy,以获得更熟悉的 NumPy 风格体验。本教程重点介绍原始的 mxnet.ndarray API。

以下是如何从 Python 列表创建 NDArray:

import mxnet as mx
from mxnet import nd
# 创建一个一维 NDArray
x = nd.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
print(x)

输出:

<html>
<body>
<p>
[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
<NDArray 10 @cpu(0)>
</p>
</body>
</html>

二维 NDArray 示例:

# 创建一个二维 NDArray (3x10 矩阵)
y = nd.array([[1,2,3,4,5,6,7,8,9,10],
[11,12,13,14,15,16,17,18,19,20],
[21,22,23,24,25,26,27,28,29,30]])
print(y)

输出:

<html>
<body>
<p>
[[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]
[11. 12. 13. 14. 15. 16. 17. 18. 19. 20.]
[21. 22. 23. 24. 25. 26. 27. 28. 29. 30.]]
<NDArray 3x10 @cpu(0)>
</p>
</body>
</html>

让我们深入探讨 mxnet.ndarray API 的类、函数和参数。

类定义
NDArray(handle[, writable])表示多维、同构固定大小项目数组的主要数组对象。它可以驻留在 CPU 或 GPU 内存中。
CachedOp(sym[, flags])缓存操作符的句柄,内部用于优化带有 NDArray 输入的符号图的重复执行。通常不直接由终端用户使用。

mxnet.ndarray 模块提供了丰富的数值计算函数集,类似于 NumPy 中的函数。其中许多函数也可以作为 NDArray 对象本身的方法使用(例如,x.sum() 等同于 nd.sum(x))。

函数及参数定义
Activation(data, act_type=‘relu’, out=None, name=None)对输入按元素应用激活函数。支持的 act_type 包括 ‘relu’、‘sigmoid’、‘tanh’、‘softrelu’、‘softsign’。这常用于神经网络。
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, out=None, name=None)执行批标准化 (Batch Normalization)。通过数据的均值和方差对其进行标准化,然后应用学习到的缩放因子 (gamma) 和偏移量 (beta)。这对于稳定和加速深度神经网络的训练至关重要。
BilinearSampler(data, grid, cudnn_off=False, out=None, name=None)使用 grid 对输入特征图应用双线性采样 (Bilinear Sampling)。这是空间变换网络 (Spatial Transformer Networks) 中的关键组件,允许对特征图进行可学习的空间变换。grid 指定采样坐标。
BlockGrad(data, out=None, name=None)停止梯度计算。在反向传播期间,梯度不会通过此操作符流向其输入。这对于冻结网络部分或进行特定的对抗性训练设置非常有用。
cast(data, dtype, out=None, name=None)将输入 NDArray 的所有元素转换为新的数据类型 (dtype),例如 ‘float32’、‘float16’、‘int32’。

使用 BilinearSampler 对小型特征图进行上采样(放大)的示例。这里,我们定义一个用于缩放的仿射变换矩阵。

import mxnet as mx
from mxnet import nd
# 输入数据 (1 通道, 2x2 图像)
data = nd.array([[[[1, 2],
[3, 4]]]], ctx=mx.cpu())
# 用于 2 倍缩放的仿射矩阵 (grid 生成中的 0.5 表示放大 2 倍)
# [s_x, 0, 0 ]
# [0, s_y, 0 ] (针对相对于源的 target_shape)
# 我们想将 4x4 输出 grid 映射到 2x2 输入
affine_matrix = nd.array([[0.5, 0, 0],
[0, 0.5, 0]], ctx=mx.cpu())
affine_matrix = affine_matrix.reshape((1, 6)) # 批量大小为 1,展平的 2x3 矩阵
# 为 4x4 输出生成 grid,映射回 'data' 坐标
grid = nd.GridGenerator(data=affine_matrix, transform_type='affine', target_shape=(4, 4))
output = nd.BilinearSampler(data, grid)
print(output)

输出(值将被插值):

<html>
<body>
<p>
[[[[1. 1.25 1.75 2. ]
[1.5 1.75 2.25 2.5 ]
[2.5 2.75 3.25 3.5 ]
[3. 3.25 3.75 4. ]]]]
<NDArray 1x1x4x4 @cpu(0)>
</p>
</body>
</html>

使用带有 warp field 的 BilinearSampler 进行水平移动数据的示例。warp grid 中的正水平位移意味着从左侧采样。

import mxnet as mx
from mxnet import nd
# 输入数据 (1 通道, 4x4 图像)
data = nd.array([[[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]]]], ctx=mx.cpu())
# 创建一个 warp grid。warp_matrix 第一个通道中的值 1 表示移动 1 像素。
# 我们想将图像内容向左移动 1 像素,因此样本从右侧 1 像素处获取。
# Grid 值已归一化:-1 到 1。在 4 像素宽图像中移动 1 像素,在归一化坐标中为 1 * (2/4) = 0.5。
# x-flow field 中的正值将采样点向右移动。
flow_x = nd.full((1,1,4,4), 0.5, ctx=mx.cpu()) # 将采样点向右移动 1 像素
flow_y = nd.zeros((1,1,4,4), ctx=mx.cpu())
warp_matrix = nd.concat(flow_x, flow_y, dim=1) # 形状 (1, 2, H, W)
grid = nd.GridGenerator(data=warp_matrix, transform_type='warp', target_shape=(4,4)) # 对于 warp,target_shape 通常被忽略
output = nd.BilinearSampler(data, grid)
print("内容向左移动,最右侧列由于越界采样而变为零:")

输出(内容向左移动,最右侧列由于越界采样而变为零,BilinearSampler 通常将其视为零):

<html>
<body>
<p>
Data shifted (content moves left, zeros appear on right):
[[[[ 2. 3. 4. 0. ]
[ 6. 7. 8. 0. ]
[10. 11. 12. 0. ]
[14. 15. 16. 0. ]]]]
<NDArray 1x1x4x4 @cpu(0)>
</p>
</body>
</html>

使用 cast() 函数的示例:

original_array = nd.array([300.0, 10.1, 15.4, -1.0, -2.0])
casted_array = nd.cast(original_array, dtype='uint8')
print(f"原始: {original_array}")
print(f"转换为 uint8: {casted_array}")

输出(值被限制/环绕到 uint8 范围 [0, 255]):

<html>
<body>
<p>
Original: [300. 10.1 15.4 -1. -2. ] <NDArray 5 @cpu(0)>
Casted to uint8: [255 10 15 255 254] <NDArray 5 @cpu(0)>
</p>
</body>
</html>

mxnet.ndarray.contrib 包包含实验性 API 和功能。提供这些函数是为了让社区试用并提供反馈。它们可能会在未来版本中被提升到主要的 mxnet.ndarray API,或者被更改或移除。在生产环境中使用时请谨慎。

函数及参数定义
rand_zipfian(true_classes, num_sampled, range_max, out=None, name=None)从近似的 Zipfian 分布中抽取随机样本。常用于大规模分类任务的候选采样(例如,在自然语言处理中)。
foreach(body, data, init_states, name=None)在 NDArray 切片上执行类似 for 循环的计算。body 是一个 Python 函数,定义每次迭代的计算。这是一种表达某些序列操作的方式,这些操作仍然可以由 MXNet 引擎管理。
while_loop(cond, func, loop_vars, max_iterations=None, name=None)执行类似 while 循环的计算。cond 是确定循环条件的 Python 函数,func 定义循环内的计算,loop_vars 是迭代的变量。
cond(pred, then_func, else_func, name=None)执行 if-then-else 分支。pred 是一个 NDArray 标量条件;如果为真,则执行 then_func,否则执行 else_func。then_func 和 else_func 都是返回 NDArray 列表的 Python 函数。
isinf(data, out=None, name=None)执行按元素检查,确定 NDArray 元素是否为无穷大。返回布尔型 NDArray。
getnnz(data, axis=None, out=None, name=None)计算稀疏张量(特别是在 CPU 上的 CSR 格式)中非零存储值的数量。如果存储了显式零,也包括在内。
requantize(data, min_range, max_range, out_min_range=None, out_max_range=None, out_type=‘int8’, out=None, name=None)使用新的最小/最大阈值,将数据从 int32(及其最小/最大范围)重新量化到 int8 等较低精度类型。用于模型量化以进行推理优化。

使用 rand_zipfian 的示例:

from mxnet.contrib import nd as contrib_nd
true_classes = mx.nd.array([2, 7]) # 真类标签的批量
num_sampled = 3
range_max = 10 # 从 [0, 9] 采样
samples, expected_count_true, expected_count_sample = contrib_nd.rand_zipfian(
true_classes, num_sampled, range_max
)
print(f"样本:\n{samples}")
print(f"真类别的期望计数:\n{expected_count_true}")
print(f"采样类别的期望计数:\n{expected_count_sample}")

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

<html>
<body>
<p>
Samples:
[[0 1 4]
[3 0 1]]
<NDArray 2x3 @cpu(0)>
Expected count for true classes:
[0.411263 पैसा 0.23979962]
<NDArray 2 @cpu(0)>
Expected count for sampled classes:
[[0.73500365 0.5108256 0.3413174 ]
[0.37796447 0.73500365 0.5108256 ]]
<NDArray 2x3 @cpu(0)>
</p>
</body>
</html>

使用 while_loop 计算达到指定限制的类似斐波那契数列的示例:

from mxnet.contrib import nd as contrib_nd
# 循环条件:当前值 < 20
cond = lambda i, prev, curr: curr < 20
# 循环函数:更新 prev, curr;输出 curr
func = lambda i, prev, curr: ([curr], [i + 1, curr, prev + curr])
# 初始循环变量:(迭代计数, 前一个值, 当前值)
loop_vars = (mx.nd.array([0]), mx.nd.array([0]), mx.nd.array([1]))
outputs, states = contrib_nd.while_loop(cond, func, loop_vars, max_iterations=10)
print(f"每次迭代的输出(当前值):\n{outputs[0]}")
print(f"最终状态 (i, prev, curr):\n{states}")

输出:

<html>
<body>
<p>
Outputs from each iteration (current_value):
[[ 1.]
[ 1.]
[ 2.]
[ 3.]
[ 5.]
[ 8.]
[13.]]
<NDArray 7x1 @cpu(0)>
Final states (i, prev, curr):
[<NDArray 1 @cpu(0)>, <NDArray 1 @cpu(0)>, <NDArray 1 @cpu(0)>]
</p>
</body>
</html>

mxnet.ndarray.image 模块提供了专门用于对 NDArray 执行图像处理和增强任务的函数。这些函数常用于计算机视觉模型的数据预处理流水线中。

函数及参数定义
adjust_lighting(data, alpha, out=None, name=None)使用基于 PCA 的噪声调整图像光照,这是一种来自 AlexNet 的技术。alpha 是 PCA 分量乘数向量。
crop(data, x, y, width, height, out=None, name=None)裁剪图像 NDArray。输入形状通常是 (H x W x C) 或 (N x H x W x C)。x, y 是裁剪区域的左上角坐标。
normalize(data, mean=(0,0,0), std=(1,1,1), out=None, name=None)通过按通道减去 mean 并除以 std 来标准化形状为 (C x H x W) 或 (N x C x H x W) 的图像张量。
random_crop(data, size, interp=2, out=None, name=None)将图像随机裁剪到指定 size(宽度,高度)。如果源图像较小,可能会进行上采样。interp 控制插值方法。
random_lighting(data, alpha_std=0.05, out=None, name=None)根据 AlexNet 风格,向图像添加随机的基于 PCA 的光照噪声。alpha_std 控制噪声因子的标准差。
random_resized_crop(data, size, area=(0.08, 1.0), ratio=(0.75, 1.33), interp=2, out=None, name=None)裁剪图像的随机区域并将其调整为 size。裁剪区域和宽高比在指定的 area 和 ratio 范围内随机化。ImageNet 训练中常用的增强方法。
resize(data, size, keep_ratio=False, interp=2, out=None, name=None)将图像大小调整为目标 size(宽度,高度)。keep_ratio 保留宽高比。interp 指定插值方法。
to_tensor(data, out=None, name=None)将形状为 (H x W x C) 或 (N x H x W x C) 且像素值在 [0, 255] 范围内的图像 NDArray 转换为形状为 (C x H x W) 或 (N x C x H x W) 且通过除以 255 将值转换为 [0, 1] 范围内的张量。

使用 to_tensor 和 normalize 的示例:

import mxnet as mx
from mxnet import nd
import numpy as np # 用于创建初始 numpy 数组
# 创建一个 uint8 值在 [0, 255] 范围内的虚拟图像(高度=2,宽度=2,通道=3)
dummy_image_np = np.array([[[10, 20, 30], [40, 50, 60]],
[[70, 80, 90], [100, 110, 120]]], dtype=np.uint8)
img_nd = mx.nd.array(dummy_image_np)
print(f"原始图像 (HWC):\n{img_nd}")
# 转换为张量 (CHW, float32, 范围 [0,1])
img_tensor = nd.image.to_tensor(img_nd)
print(f"作为张量的图像 (CHW):\n{img_tensor}")
# 标准化张量
mean_vals = nd.array([0.485, 0.456, 0.406]) # 典型的 ImageNet 均值
std_vals = nd.array([0.229, 0.224, 0.225]) # 典型的 ImageNet 标准差
normalized_tensor = nd.image.normalize(img_tensor, mean=mean_vals, std=std_vals)
print(f"标准化后的张量:\n{normalized_tensor}")

输出(由于浮点精度,值将为近似值):

<html>
<body>
<p>
Original image (HWC):
[[[ 10 20 30]
[ 40 50 60]]
[[ 70 80 90]
[100 110 120]]]
<NDArray 2x2x3 @cpu(0)>
Image as tensor (CHW):
[[[0.03921569 0.15686275]
[0.27450982 0.39215687]]
[[0.07843138 0.19607843]
[0.3137255 0.43137255]]
[[0.11764706 0.23529412]
[0.3529412 0.47058824]]]
<NDArray 3x2x2 @cpu(0)>
Normalized tensor:
[[[-1.9469869 -1.4330131 ]
[-0.91903937 -0.40506548]]
[[-1.6856009 -1.1579224 ]
[-0.6302439 -0.10256535]]
[[-1.2815042 -0.76430035]
[-0.24709654 0.2701073 ]]]
<NDArray 3x2x2 @cpu(0)>
</p>
</body>
</html>

mxnet.ndarray.random 模块提供了用于生成带有从各种统计分布中抽取的随机数的 NDArray 的函数。这对于初始化模型参数、创建用于测试的随机数据或实现随机过程至关重要。

函数及参数定义
uniform(low=0, high=1, shape=None, dtype=None, ctx=None, out=None)从 [low, high) 上的均匀分布中生成样本。
normal(loc=0, scale=1, shape=None, dtype=None, ctx=None, out=None)从均值为 loc、标准差为 scale 的正态(高斯)分布中生成样本。
randn(*shape, **kwargs)从标准正态分布(均值 0,方差 1)中生成样本。shape 作为单独的参数传递。
exponential(scale=1, shape=None, dtype=None, ctx=None, out=None)从速率为 1/scale (lambda) 的指数分布中生成样本。
gamma(alpha, beta, shape=None, dtype=None, ctx=None, out=None)从形状为 alpha、尺度为 beta 的伽马分布中生成样本。
multinomial(data, shape=1, get_prob=False, out=None, dtype=‘int32’)从多项分布中生成样本。data 包含每个结果的概率。
negative_binomial(k, p, shape=None, dtype=None, ctx=None, out=None)从负二项分布中生成样本(在 r 次失败之前获得 k 次成功的次数,成功概率为 p)。k、p 的解释可能有所不同;请查阅 MXNet 文档以获取具体信息。
generalized_negative_binomial(mu, alpha, shape=None, dtype=None, ctx=None, out=None)从由均值 mu 和离散度 alpha 参数化的广义负二项分布中生成样本。
shuffle(data, out=None, name=None)沿 NDArray 的第一个轴打乱其元素。
randint(low, high, shape=None, dtype=‘int32’, ctx=None, out=None)从范围 [low, high) 的离散均匀分布中生成随机整数。
*_like functions (e.g., uniform_like, normal_like)这些函数(例如 normal_like(data, loc=0, scale=1)) 生成具有与输入 data NDArray 相同形状和上下文的随机样本,但值从指定的分布中抽取。

从均匀分布和正态分布中抽取样本:

from mxnet.ndarray import random as random_nd # 为了清晰起见使用别名
# 均匀分布
uniform_samples = random_nd.uniform(low=-1, high=1, shape=(2, 3))
print(f"均匀样本 [-1, 1]:\n{uniform_samples}")
# 正态分布
normal_samples = random_nd.normal(loc=0, scale=0.01, shape=(2,2))
print(f"正态样本 (均值 0, 标准差 0.01):\n{normal_samples}")
# randint
int_samples = random_nd.randint(low=0, high=10, shape=(5,), ctx=mx.cpu())
print(f"随机整数 [0, 9]:\n{int_samples}")

输出(实际随机值会因运行而异):

<html>
<body>
<p>
Uniform samples [-1, 1]:
[[-0.5206176 0.2503767 -0.95505637]
[-0.7613349 0.04832268 0.9208205 ]]
<NDArray 2x3 @cpu(0)>
Normal samples (mean 0, std 0.01):
[[ 0.00133301 -0.00306308]
[-0.00209531 0.00401994]]
<NDArray 2x2 @cpu(0)>
Random integers [0, 9]:
[8 3 0 9 2]
<NDArray 5 @cpu(0)>
</p>
</body>
</html>

mxnet.ndarray.utils 模块(通常直接在 mx.nd 下提供,用于像 zeros 这样的常用函数)提供了用于创建、保存和加载 NDArray 的实用函数。

函数及参数定义
zeros(shape, ctx=None, dtype=None, stype=‘default’)返回一个给定 shape 和 dtype、填充为零的新数组。stype 可以指定稀疏类型,如 ‘csr’ 或 ‘row_sparse’。
empty(shape, ctx=None, dtype=None, stype=‘default’)返回一个给定 shape 和 dtype 的新数组,不初始化其条目。内容将是任意内存值。如果您打算立即填充它,它比 zeros 更快。
array(source_array, ctx=None, dtype=None)从公开数组接口的对象(例如 Python 列表、NumPy 数组)创建 NDArray。
load(fname)从文件加载 NDArray 列表或字典。文件通常使用 mx.nd.save() 保存。
load_frombuffer(buf)从二进制字符串缓冲区(例如,从文件或网络读取的缓冲区)加载 NDArray 字典或列表。
save(fname, data)将 NDArray 列表或将字符串映射到 NDArray 的字典保存到二进制文件中。data 可以是单个 NDArray、NDArray 列表或 str -> NDArray 字典。

使用 zeros 创建数组以及使用 save/load 的示例:

# zeros 通常通过 mx.nd.zeros 访问
x = mx.nd.zeros((2, 3), ctx=mx.cpu(), dtype='float32')
print(f"零数组:\n{x}")
# 保存和加载 NDArray
a = mx.nd.array([1, 2, 3])
b = mx.nd.array([[4, 5], [6, 7]])
file_path = 'my_arrays.params' # 保存的 NDArray 的常见扩展名
# 保存为列表
mx.nd.save(file_path, [a, b])
loaded_list = mx.nd.load(file_path)
print(f"加载的列表: {loaded_list}")
# 保存为字典
mx.nd.save(file_path, {'first': a, 'second': b})
loaded_dict = mx.nd.load(file_path)
print(f"加载的字典: {loaded_dict}")

输出:

<html>
<body>
<p>
Zeros array:
[[0. 0. 0.]
[0. 0. 0.]]
<NDArray 2x3 @cpu(0)>
Loaded list: [<NDArray 3 @cpu(0)>, <NDArray 2x2 @cpu(0)>]
Loaded dict: {'first': <NDArray 3 @cpu(0)>, 'second': <NDArray 2x2 @cpu(0)>}
</p>
</body>
</html>