参考网站:http://zh.gluon.ai/
使用NDArray来处理数据
对于机器学习来说,处理数据往往是万事之开头。它包含两个部分:数据读取和当数据已经在内存里时如何处理。本章将关注后者。我们首先介绍NDArray
,这是MXNet
储存和变化数据的主要工具。如果你之前用过NumPy
,你会发现NDArray
和NumPy
的多维数组非常类似。当然,NDArray
提供更多的功能,首先是CPU和GPU的异步计算,其次是自动求导。这两点是的NDArray
能更好地支持机器学习。
让我们开始
我们先介绍最基本的功能。不用担心如果你不懂我们用到的数学操作,例如按元素加法,或者正态分布,我们会在之后的章节分别详细介绍。
我们首先从mxnet
导入ndarray
这个包
from mxnet import ndarray as nd
然后我们创建一个有3行和4列的2D数组(通常也叫矩阵),并且把每个元素初始化成0
nd.zeros((3, 4))
[[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]
[ 0. 0. 0. 0.]]
类似的,我们可以创建数组每个元素被初始化成1。
x = nd.ones((3, 4))
x
[[ 1. 1. 1. 1.]
[ 1. 1. 1. 1.]
[ 1. 1. 1. 1.]]
或者从python的数组直接构造
nd.array([[1,2],[2,3]])
[[ 1. 2.]
[ 2. 3.]]
我们经常需要创建随机数组,就是说每个元素的值都是随机采样而来,这个经常被用来初始化模型参数。下面创建数组,它的元素服从均值0方差1的正态分布。
y = nd.random_normal(0, 1, shape=(3, 4))
y
[[ 2.21220636 1.16307867 0.7740038 0.48380461]
[ 1.04344046 0.29956347 1.18392551 0.15302546]
[ 1.89171135 -1.16881478 -1.23474145 1.55807114]]
跟NumPy
一样,每个数组的形状可以通过.shape
来获取
y.shape
(3L, 4L)
它的大小,就是总元素个数,是形状的累乘。
y.size
12L
操作符
NDArray支持大量的数学操作符,例如按元素加法:
x + y
[[ 3.21220636 2.16307878 1.77400374 1.48380458]
[ 2.04344034 1.29956341 2.18392563 1.15302551]
[ 2.89171124 -0.16881478 -0.23474145 2.55807114]]
乘法:
x * y
[[ 2.21220636 1.16307867 0.7740038 0.48380461]
[ 1.04344046 0.29956347 1.18392551 0.15302546]
[ 1.89171135 -1.16881478 -1.23474145 1.55807114]]
指数运算:
nd.exp(y)
[[ 9.13585091 3.19976926 2.16843081 1.6222347 ]
[ 2.83896756 1.34926963 3.26717448 1.16535461]
[ 6.63070631 0.31073502 0.29090998 4.74965096]]
也可以转秩一个矩阵然后计算矩阵乘法:
nd.dot(x, y.T)
[[ 4.63309383 2.67995477 1.04622626]
[ 4.63309383 2.67995477 1.04622626]
[ 4.63309383 2.67995477 1.04622626]]
广播
当二元操作符左右两边ndarray形状不一样时,系统会尝试将其复制到一个共同的形状。例如a的第0维是3, b的第0维是1,那么a+b时会将b沿着第0维复制3遍:
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.]]
b:
[[ 0. 1.]]
a+b:
[[ 0. 1.]
[ 1. 2.]
[ 2. 3.]]
跟NumPy的转换
ndarray可以很方便同numpy进行转换
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.]]
替换操作
在前面的样例中,我们为每个操作新开内存来存储它的结果。例如,如果我们写y = x + y
, 我们会把y
从现在指向的实例转到新建的实例上去。我们可以用Python的id()
函数来看这个是怎么执行的:
x = nd.ones((3, 4))
y = nd.ones((3, 4))
before = id(y)
y = y + x
id(y) == before
False
我们可以吧结果通过[:]
写到一个之前开好的数组里:
z = nd.zeros_like(x)
before = id(z)
z[:] = x + y
id(z) == before
True
但是这里我们还是为x+y
创建了临时空间,然后在复制到z. 需要避免这个开销,我们可以使用操作符的全名版本中的out
参数:
nd.elemwise_add(x, y, out=z)
id(z) == before
True
如果可以现有的数组之后不会再用,我们也可以用复制操作符达到这个目的
before = id(x)
x += y
id(x) == before
True
使用autograd来自动求导
在机器学习中,我们通常使用梯度下降来更新模型参数来求解。损失函数关于模型参数的梯度指向一个可以降低损失函数值的方向,我们不断的沿着梯度的方向更新模型从而最小化损失函数。虽然梯度计算比较直观,但对于复杂的模型,例如多达数十层的神经网络,手动计算梯度非常困难。
为此MXNet提供autograd包来自动化求导过程。虽然大部分的深度学习框架要求编译计算图来自动求导,mxnet.autograd
可以对正常的命令式程序进行求导,它每次在后端实时创建计算图从而可以立即得到梯度的计算方法。
下面让我们一步步介绍这个包。我们先导入autograd
.
import mxnet.ndarray as nd
import mxnet.autograd as ag
为变量附上梯度
假设我们想对函数f = 2 * (x ^ 2)
求关于x
的导数。我们先创建变量x
,并赋初值。
x = nd.array([[1, 2], [3, 4]])
当进行求导的时候,我们需要一个地方来存x
的导数,这个可以通过NDArray的方法attach_grad()
来要求系统申请对应的空间。
x.attach_grad()
下面定义f
。默认MXNet不会自动记录和构建用于求导的计算图,我们需要使用autograd里的record()
函数来显式的要求MXNet记录我们需要求导的程序。
with ag.record():
y = x * 2
z = y * x
接下来我们可以通过z.backward()
来进行求导。如果z
不是一个标量,那么z.backward()
等价于nd.sum(z).backward()
.
z.backward()
现在我们来看求出来的导数是不是正确的。注意到y = x * 2
和z = x * y
,所以z
等价于2 * x * x
。它的导数那么就是dz/dx = 4 * x
。
x.grad == 4*x
[[ 1. 1.]
[ 1. 1.]]
对控制流求导
命令式的编程的一个便利之处是几乎可以对任意的可导程序进行求导,即使里面包含了Python的控制流。考虑下面程序,里面包含控制流for
和if
,但循环迭代的次数和判断语句的执行都是取决于输入的值。不同的输入会导致这个程序的执行不一样。(对于计算图框架来说,这个对应于动态图,就是图的结构会根据输入数据不同而改变)。
def f(a):
b = a * 2
while nd.norm(b).asscalar() < 1000:
b = b * 2
if nd.sum(b).asscalar() > 0:
c = b
else:
c = 100 * b
return c
我们可以跟之前一样使用record
记录和backward
求导。
a = nd.random_normal(shape=3)
a.attach_grad()
with ag.record():
c = f(a)
c.backward()
注意到给定输入a
,其输出f(a)=xa
,x
的值取决于输入a
。所以有df/da = x
,我们可以很简单地评估自动求导的导数:
a.grad == c/a
[ 1. 1. 1.]
到这里,我们已经熟悉了数据的处理和求导的初步使用,下一Part我们将开始回归方法的学习。