使用NDArray来处理数据

对于机器学习来说,处理数据往往是万事之开头。它包含两个部分:(i)数据读取,(ii)数据已经在内存中时如何处理。本章将关注后者。

我们首先介绍NDArray,这是MXNet储存和变换数据的主要工具。如果你之前用过NumPy,你会发现NDArrayNumPy的多维数组非常类似。当然,NDArray提供更多的功能,首先是CPU和GPU的异步计算,其次是自动求导。这两点使得NDArray能更好地支持机器学习。

让我们开始

我们先介绍最基本的功能。如果你不懂我们用到的数学操作也不用担心,例如按元素加法、正态分布;我们会在之后的章节分别从数学和代码编写的角度详细介绍。

我们首先从mxnet导入ndarray这个包

In [1]:
from mxnet import ndarray as nd

然后我们创建一个3行和4列的2D数组(通常也叫矩阵),并且把每个元素初始化成0

In [2]:
nd.zeros((3, 4))
Out[2]:

[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
<NDArray 3x4 @cpu(0)>

类似的,我们可以创建数组每个元素被初始化成1。

In [3]:
x = nd.ones((3, 4))
x
Out[3]:

[[ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]
 [ 1.  1.  1.  1.]]
<NDArray 3x4 @cpu(0)>

或者从python的数组直接构造

In [4]:
nd.array([[1,2],[2,3]])
Out[4]:

[[ 1.  2.]
 [ 2.  3.]]
<NDArray 2x2 @cpu(0)>

我们经常需要创建随机数组,即每个元素的值都是随机采样而来,这个经常被用来初始化模型参数。以下代码创建数组,它的元素服从均值0标准差1的正态分布。

In [5]:
y = nd.random_normal(0, 1, shape=(3, 4))
y
Out[5]:

[[ 2.21220636  0.7740038   1.04344046  1.18392551]
 [ 1.89171135 -1.23474145 -1.771029   -0.45138445]
 [ 0.57938355 -1.85608196 -1.9768796  -0.20801921]]
<NDArray 3x4 @cpu(0)>

NumPy一样,每个数组的形状可以通过.shape来获取

In [6]:
y.shape
Out[6]:
(3, 4)

它的大小,就是总元素个数,是形状的累乘。

In [7]:
y.size
Out[7]:
12

操作符

NDArray支持大量的数学操作符,例如按元素加法:

In [8]:
x + y
Out[8]:

[[ 3.21220636  1.77400374  2.04344034  2.18392563]
 [ 2.89171124 -0.23474145 -0.771029    0.54861557]
 [ 1.57938361 -0.85608196 -0.9768796   0.7919808 ]]
<NDArray 3x4 @cpu(0)>

乘法:

In [9]:
x * y
Out[9]:

[[ 2.21220636  0.7740038   1.04344046  1.18392551]
 [ 1.89171135 -1.23474145 -1.771029   -0.45138445]
 [ 0.57938355 -1.85608196 -1.9768796  -0.20801921]]
<NDArray 3x4 @cpu(0)>

指数运算:

In [10]:
nd.exp(y)
Out[10]:

[[ 9.13585091  2.16843081  2.83896756  3.26717448]
 [ 6.63070631  0.29090998  0.17015781  0.63674599]
 [ 1.78493774  0.15628375  0.13850074  0.81219143]]
<NDArray 3x4 @cpu(0)>

也可以转置一个矩阵然后计算矩阵乘法:

In [11]:
nd.dot(x, y.T)
Out[11]:

[[ 5.21357632 -1.56544352 -3.4615972 ]
 [ 5.21357632 -1.56544352 -3.4615972 ]
 [ 5.21357632 -1.56544352 -3.4615972 ]]
<NDArray 3x3 @cpu(0)>

我们会在之后的线性代数一章讲解这些运算符。

广播(Broadcasting)

当二元操作符左右两边ndarray形状不一样时,系统会尝试将其复制到一个共同的形状。例如a的第0维是3, b的第0维是1,那么a+b时会将b沿着第0维复制3遍:

In [12]:
a = nd.arange(3).reshape((3,1))
b = nd.arange(2).reshape((1,2))
print('a:', a)
print('b:', b)
print('a+b:', a+b)

a:
[[ 0.]
 [ 1.]
 [ 2.]]
<NDArray 3x1 @cpu(0)>
b:
[[ 0.  1.]]
<NDArray 1x2 @cpu(0)>
a+b:
[[ 0.  1.]
 [ 1.  2.]
 [ 2.  3.]]
<NDArray 3x2 @cpu(0)>

跟NumPy的转换

ndarray可以很方便地同numpy进行转换

In [13]:
import numpy as np
x = np.ones((2,3))
y = nd.array(x)  # numpy -> mxnet
z = y.asnumpy()  # mxnet -> numpy
print([z, y])
[array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.]], dtype=float32),
[[ 1.  1.  1.]
 [ 1.  1.  1.]]
<NDArray 2x3 @cpu(0)>]

替换操作

在前面的样例中,我们为每个操作新开内存来存储它的结果。例如,如果我们写y = x + y, 我们会把y从现在指向的实例转到新建的实例上去。我们可以用Python的id()函数来看这个是怎么执行的:

In [14]:
x = nd.ones((3, 4))
y = nd.ones((3, 4))

before = id(y)
y = y + x
id(y) == before
Out[14]:
False

我们可以把结果通过[:]写到一个之前开好的数组里:

In [15]:
z = nd.zeros_like(x)
before = id(z)
z[:] = x + y
id(z) == before
Out[15]:
True

但是这里我们还是为x+y创建了临时空间,然后再复制到z。需要避免这个开销,我们可以使用操作符的全名版本中的out参数:

In [16]:
nd.elemwise_add(x, y, out=z)
id(z) == before
Out[16]:
True

如果现有的数组不会复用,我们也可以用 x[:] = x + y ,或者 x += y 达到这个目的:

In [17]:
before = id(x)
x += y
id(x) == before
Out[17]:
True

截取(Slicing)

MXNet NDArray 提供了各种截取方法。截取 x 的 index 为 1、2 的行:

In [18]:
x = nd.arange(0,9).reshape((3,3))
print('x: ', x)
x[1:3]
x:
[[ 0.  1.  2.]
 [ 3.  4.  5.]
 [ 6.  7.  8.]]
<NDArray 3x3 @cpu(0)>
Out[18]:

[[ 3.  4.  5.]
 [ 6.  7.  8.]]
<NDArray 2x3 @cpu(0)>

以及直接写入指定位置:

In [19]:
x[1,2] = 9.0
x
Out[19]:

[[ 0.  1.  2.]
 [ 3.  4.  9.]
 [ 6.  7.  8.]]
<NDArray 3x3 @cpu(0)>

多维截取:

In [20]:
x = nd.arange(0,9).reshape((3,3))
print('x: ', x)
x[1:2,1:3]
x:
[[ 0.  1.  2.]
 [ 3.  4.  5.]
 [ 6.  7.  8.]]
<NDArray 3x3 @cpu(0)>
Out[20]:

[[ 4.  5.]]
<NDArray 1x2 @cpu(0)>

多维写入:

In [21]:
x[1:2,1:3] = 9.0
x
Out[21]:

[[ 0.  1.  2.]
 [ 3.  9.  9.]
 [ 6.  7.  8.]]
<NDArray 3x3 @cpu(0)>

总结

ndarray模块提供一系列多维数组操作函数。所有函数列表可以参见NDArray API文档

吐槽和讨论欢迎点这里