Apache MXNet - NDArray
在本章中,我们将讨论 MXNet 的多维数组格式 ndarray。
使用 NDArray 处理数据
首先,我们将了解如何使用 NDArray 处理数据。以下是相同的先决条件 −
先决条件
要了解如何使用这种多维数组格式处理数据,我们需要满足以下先决条件:
在 Python 环境中安装 MXNet
Python 2.7.x 或 Python 3.x
实现示例
让我们借助下面给出的示例了解基本功能 −
首先,我们需要从 MXNet 导入 MXNet 和 ndarray,如下所示 −
import mxnet as mx from mxnet import nd
导入必要的库后,我们将使用以下基本功能:
带有 Python 列表的简单一维数组
示例
x = nd.array([1,2,3,4,5,6,7,8,9,10]) print(x)
输出
输出如下所示−
[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.] <NDArray 10 @cpu(0)>
带有 Python 列表的二维数组
示例
y = nd.array([[1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10], [1,2,3,4,5,6,7,8,9,10]]) print(y)
输出
输出如下所示 −
[[ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.] [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.] [ 1. 2. 3. 4. 5. 6. 7. 8. 9. 10.]] <NDArray 3x10 @cpu(0)>
创建 NDArray 而不进行任何初始化
在这里,我们将使用 .empty 函数创建一个有 3 行 4 列的矩阵。我们还将使用 .full 函数,该函数将采用额外的运算符来指定您想要在数组中填充的值。
示例
x = nd.empty((3, 4)) print(x) x = nd.full((3,4), 8) print(x)
输出
输出如下所示 −
[[0.000e+00 0.000e+00 0.000e+00 0.000e+00] [0.000e+00 0.000e+00 2.887e-42 0.000e+00] [0.000e+00 0.000e+00 0.000e+00 0.000e+00]] <NDArray 3x4 @cpu(0)> [[8. 8. 8. 8.] [8. 8. 8. 8.] [8. 8. 8. 8.]] <NDArray 3x4 @cpu(0)>
使用 .zeros 函数的全零矩阵
示例
x = nd.zeros((3, 8)) print(x)
输出
输出如下 −
[[0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0.] [0. 0. 0. 0. 0. 0. 0. 0.]] <NDArray 3x8 @cpu(0)>
使用 .ones 函数的全一矩阵
示例
x = nd.ones((3, 8)) print(x)
输出
输出如下 −
[[1. 1. 1. 1. 1. 1. 1. 1.] [1. 1. 1. 1. 1. 1. 1. 1.] [1. 1. 1. 1. 1. 1. 1. 1.]] <NDArray 3x8 @cpu(0)>
创建随机采样值的数组
示例
y = nd.random_normal(0, 1, shape=(3, 4)) print(y)
输出
输出如下所示 −
[[ 1.2673576 -2.0345826 -0.32537818 -1.4583491 ] [-0.11176403 1.3606371 -0.7889914 -0.17639421] [-0.2532185 -0.42614475 -0.12548696 1.4022992 ]] <NDArray 3x4 @cpu(0)>
查找每个 NDArray 的维度
示例
y.shape
输出
输出如下 −
(3, 4)
查找每个 NDArray 的大小
示例
y.size
输出
12
查找每个 NDArray 的数据类型
示例
y.dtype
Output
numpy.float32
NDArray 操作
在本节中,我们将向您介绍 MXNet 的数组操作。 NDArray 支持大量标准数学运算以及就地运算。
标准数学运算
以下是 NDArray 支持的标准数学运算 −
逐元素加法
首先,我们需要从 MXNet 导入 MXNet 和 ndarray,如下所示:
import mxnet as mx from mxnet import nd x = nd.ones((3, 5)) y = nd.random_normal(0, 1, shape=(3, 5)) print('x=', x) print('y=', y) x = x + y print('x = x + y, x=', x)
输出
输出如下所示−
x= [[1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.] [1. 1. 1. 1. 1.]] <NDArray 3x5 @cpu(0)> y= [[-1.0554522 -1.3118273 -0.14674698 0.641493 -0.73820823] [ 2.031364 0.5932667 0.10228804 1.179526 -0.5444829 ] [-0.34249446 1.1086396 1.2756858 -1.8332436 -0.5289873 ]] <NDArray 3x5 @cpu(0)> x = x + y, x= [[-0.05545223 -0.3118273 0.853253 1.6414931 0.26179177] [ 3.031364 1.5932667 1.102288 2.1795259 0.4555171 ] [ 0.6575055 2.1086397 2.2756858 -0.8332436 0.4710127 ]] <NDArray 3x5 @cpu(0)>
逐元素乘法
示例
x = nd.array([1, 2, 3, 4]) y = nd.array([2, 2, 2, 1]) x * y
输出
您将看到以下输出−
[2. 4. 6. 4.] <NDArray 4 @cpu(0)>
Exponentiation
示例
nd.exp(x)
输出
运行代码时,您将看到以下输出:
[ 2.7182817 7.389056 20.085537 54.59815 ] <NDArray 4 @cpu(0)>
矩阵转置计算矩阵与矩阵乘积
示例
nd.dot(x, y.T)
输出
下面给出了代码的输出 −
[16.] <NDArray 1 @cpu(0)>
就地操作
在上面的例子中,每次我们运行一个操作时,我们都会分配一个新的内存来承载其结果。
例如,如果我们写 A = A+B,我们将取消引用 A 曾经指向的矩阵,而是将其指向新分配的内存。让我们通过下面给出的示例来理解它,使用 Python 的 id() 函数 −
print('y=', y) print('id(y):', id(y)) y = y + x print('after y=y+x, y=', y) print('id(y):', id(y))
输出
执行后,您将收到以下输出 −
y= [2. 2. 2. 1.] <NDArray 4 @cpu(0)> id(y): 2438905634376 after y=y+x, y= [3. 4. 5. 5.] <NDArray 4 @cpu(0)> id(y): 2438905685664
实际上,我们也可以将结果赋值给之前分配的数组,如下所示−
print('x=', x) z = nd.zeros_like(x) print('z is zeros_like x, z=', z) print('id(z):', id(z)) print('y=', y) z[:] = x + y print('z[:] = x + y, z=', z) print('id(z) is the same as before:', id(z))
输出
输出如下所示 −
x= [1. 2. 3. 4.] <NDArray 4 @cpu(0)> z is zeros_like x, z= [0. 0. 0. 0.] <NDArray 4 @cpu(0)> id(z): 2438905790760 y= [3. 4. 5. 5.] <NDArray 4 @cpu(0)> z[:] = x + y, z= [4. 6. 8. 9.] <NDArray 4 @cpu(0)> id(z) is the same as before: 2438905790760
从上面的输出中,我们可以看到 x+y 在将结果复制到 z 之前仍会分配一个临时缓冲区来存储结果。所以现在,我们可以就地执行操作以更好地利用内存并避免临时缓冲区。为此,我们将指定每个运算符支持的 out 关键字参数,如下所示 −
print('x=', x, 'is in id(x):', id(x)) print('y=', y, 'is in id(y):', id(y)) print('z=', z, 'is in id(z):', id(z)) nd.elemwise_add(x, y, out=z) print('after nd.elemwise_add(x, y, out=z), x=', x, 'is in id(x):', id(x)) print('after nd.elemwise_add(x, y, out=z), y=', y, 'is in id(y):', id(y)) print('after nd.elemwise_add(x, y, out=z), z=', z, 'is in id(z):', id(z))
输出
执行上述程序后,您将得到以下结果 −
x= [1. 2. 3. 4.] <NDArray 4 @cpu(0)> is in id(x): 2438905791152 y= [3. 4. 5. 5.] <NDArray 4 @cpu(0)> is in id(y): 2438905685664 z= [4. 6. 8. 9.] <NDArray 4 @cpu(0)> is in id(z): 2438905790760 after nd.elemwise_add(x, y, out=z), x= [1. 2. 3. 4.] <NDArray 4 @cpu(0)> is in id(x): 2438905791152 after nd.elemwise_add(x, y, out=z), y= [3. 4. 5. 5.] <NDArray 4 @cpu(0)> is in id(y): 2438905685664 after nd.elemwise_add(x, y, out=z), z= [4. 6. 8. 9.] <NDArray 4 @cpu(0)> is in id(z): 2438905790760
NDArray 上下文
在 Apache MXNet 中,每个数组都有一个上下文,一个上下文可能是 CPU,而其他上下文可能是多个 GPU。当我们在多台服务器上部署工作时,情况可能会变得更糟。这就是为什么我们需要智能地将数组分配给上下文。这将最大限度地减少在设备之间传输数据所花费的时间。
例如,尝试按如下方式初始化数组 −
from mxnet import nd z = nd.ones(shape=(3,3), ctx=mx.cpu(0)) print(z)
输出
执行上述代码时,您应该看到以下输出 −
[[1. 1. 1.] [1. 1. 1.] [1. 1. 1.]] <NDArray 3x3 @cpu(0)>
我们可以使用 copyto() 方法将给定的 NDArray 从一个上下文复制到另一个上下文,如下所示 −
x_gpu = x.copyto(gpu(0)) print(x_gpu)
NumPy 数组与 NDArray
我们都熟悉 NumPy 数组,但 Apache MXNet 提供了自己的数组实现,名为 NDArray。实际上,它最初的设计类似于 NumPy,但有一个关键区别 −
关键区别在于 NumPy 和 NDArray 中执行计算的方式。 MXNet 中的每个 NDArray 操作都是以异步和非阻塞方式完成的,这意味着,当我们编写像 c = a * b 这样的代码时,该函数将被推送到执行引擎,它将开始计算。
这里,a 和 b 都是 NDArray。使用它的好处是,函数会立即返回,并且用户线程可以继续执行,尽管之前的计算可能尚未完成。
执行引擎的工作原理
如果我们谈论执行引擎的工作原理,它会构建计算图。计算图可能会重新排序或合并一些计算,但它始终遵循依赖顺序。
例如,如果在编程代码中稍后对"X"进行了其他操作,则一旦"X"的结果可用,执行引擎就会开始执行它们。执行引擎将为用户处理一些重要的工作,例如编写回调以启动后续代码的执行。
在 Apache MXNet 中,借助 NDArray,我们只需访问结果变量即可获得计算结果。代码流将被阻止,直到计算结果分配给结果变量。通过这种方式,它提高了代码性能,同时仍然支持命令式编程模式。
将 NDArray 转换为 NumPy 数组
让我们学习如何在 MXNet 中将 NDArray 转换为 NumPy 数组。
借助一些低级运算符组合高级运算符
有时,我们可以使用现有的运算符组装高级运算符。其中最好的例子之一是 np.full_like() 运算符,它在 NDArray API 中不存在。它可以很容易地用现有运算符的组合替换,如下所示:
from mxnet import nd import numpy as np np_x = np.full_like(a=np.arange(7, dtype=int), fill_value=15) nd_x = nd.ones(shape=(7,)) * 15 np.array_equal(np_x, nd_x.asnumpy())
输出
我们将获得类似以下的输出 −
True
查找具有不同名称和/或签名的类似运算符
在所有运算符中,有些运算符的名称略有不同,但功能相似。例如 nd.ravel_index() 和 np.ravel() 函数。同样,有些运算符可能具有相似的名称,但它们具有不同的签名。例如,np.split() 和 nd.split() 类似。
我们通过以下编程示例来理解:
def pad_array123(data, max_length): data_expanded = data.reshape(1, 1, 1, data.shape[0]) data_padded = nd.pad(data_expanded, mode='constant', pad_width=[0, 0, 0, 0, 0, 0, 0, max_length - data.shape[0]], constant_value=0) data_reshaped_back = data_padded.reshape(max_length) return data_reshaped_back pad_array123(nd.array([1, 2, 3]), max_length=10)
输出
输出如下所示 −
[1. 2. 3. 0. 0. 0. 0. 0. 0. 0.] <NDArray 10 @cpu(0)>
最小化阻塞调用的影响
在某些情况下,我们必须使用 .asnumpy() 或 .asscalar() 方法,但这将强制 MXNet 阻塞执行,直到可以检索结果。当我们认为该值的计算已经完成时,我们可以调用 .asnumpy() 或 .asscalar() 方法来最大限度地减少阻塞调用的影响。
实施示例
示例
from __future__ import print_function import mxnet as mx from mxnet import gluon, nd, autograd from mxnet.ndarray import NDArray from mxnet.gluon import HybridBlock import numpy as np class LossBuffer(object): """ Simple buffer for storing loss value """ def __init__(self): self._loss = None def new_loss(self, loss): ret = self._loss self._loss = loss return ret @property def loss(self): return self._loss net = gluon.nn.Dense(10) ce = gluon.loss.SoftmaxCELoss() net.initialize() data = nd.random.uniform(shape=(1024, 100)) label = nd.array(np.random.randint(0, 10, (1024,)), dtype='int32') train_dataset = gluon.data.ArrayDataset(data, label) train_data = gluon.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2) trainer = gluon.Trainer(net.collect_params(), optimizer='sgd') loss_buffer = LossBuffer() for data, label in train_data: with autograd.record(): out = net(data) # This call saves new loss and returns previous loss prev_loss = loss_buffer.new_loss(ce(out, label)) loss_buffer.loss.backward() trainer.step(data.shape[0]) if prev_loss is not None: print("Loss: {}".format(np.mean(prev_loss.asnumpy())))
输出
输出引用如下:
Loss: 2.3373236656188965 Loss: 2.3656985759735107 Loss: 2.3613128662109375 Loss: 2.3197104930877686 Loss: 2.3054862022399902 Loss: 2.329197406768799 Loss: 2.318927526473999