首先,我们从MXNet导入np
(numpy
)模块和npx
(numpy_extension
)模块。 np
模块包含NumPy支持的函数; 而npx
模块包含一组扩展函数,用来在类似NumPy的环境中实现深度学习开发。 当使用张量时,几乎总是会调用set_np
函数,这是为了兼容MXNet的其他张量处理组件。
from mxnet import np, npx
npx.set_np()
张量表示由一个数值组成的数组,这个数组可能有多个维度。 具有一个轴的张量对应数学上的向量(vector); 具有两个轴的张量对应数学上的矩阵(matrix); 具有两个轴以上的张量没有特殊的数学名称。
首先,我们可以使用 arange
创建一个行向量 x
。这个行向量包含以0开始的前12个整数,它们默认创建为浮点数。张量中的每个值都称为张量的 元素(element)。例如,张量 x
中有 12 个元素。除非额外指定,新的张量将存储在内存中,并采用基于CPU的计算。
x = np.arange(12) x
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11.])
可以通过张量的shape
属性来访问张量(沿每个轴的长度)的形状 。
x.shape
(12,)
如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小(size)。 因为这里在处理的是一个向量,所以它的shape
与它的size
相同。
x.size
12
要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape
函数。 例如,可以把张量x
从形状为(12,)的行向量转换为形状为(3,4)的矩阵。 这个新的张量包含与转换前相同的值,但是它被看成一个3行4列的矩阵。 要重点说明一下,虽然张量的形状发生了改变,但其元素值并没有变。 注意,通过改变张量的形状,张量的大小不会改变。
X = x.reshape(3, 4) X
array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.]])
我们不需要通过手动指定每个维度来改变形状。 也就是说,如果我们的目标形状是(高度,宽度), 那么在知道宽度后,高度会被自动计算得出,不必我们自己做除法。 在上面的例子中,为了获得一个3行的矩阵,我们手动指定了它有3行和4列。 幸运的是,我们可以通过-1
来调用此自动计算出维度的功能。 即我们可以用x.reshape(-1,4)
或x.reshape(3,-1)
来取代x.reshape(3,4)
。
有时,我们希望使用全0、全1、其他常量,或者从特定分布中随机采样的数字来初始化矩阵。 我们可以创建一个形状为(2,3,4)的张量,其中所有元素都设置为0。代码如下:
np.zeros((2, 3, 4))
array([[[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]], [[0., 0., 0., 0.], [0., 0., 0., 0.], [0., 0., 0., 0.]]])
同样,我们可以创建一个形状为(2,3,4)
的张量,其中所有元素都设置为1。代码如下:
np.ones((2, 3, 4))
array([[[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]], [[1., 1., 1., 1.], [1., 1., 1., 1.], [1., 1., 1., 1.]]])
有时我们想通过从某个特定的概率分布中随机采样来得到张量中每个元素的值。 例如,当我们构造数组来作为神经网络中的参数时,我们通常会随机初始化参数的值。 以下代码创建一个形状为(3,4)的张量。 其中的每个元素都从均值为0、标准差为1的标准高斯分布(正态分布)中随机采样。
np.random.normal(0, 1, size=(3, 4))
array([[ 2.2122064 , 1.1630787 , 0.7740038 , 0.4838046 ], [ 1.0434405 , 0.29956347, 1.1839255 , 0.15302546], [ 1.8917114 , -1.1688148 , -1.2347414 , 1.5580711 ]])
我们还可以通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值。 在这里,最外层的列表对应于轴0,内层的列表对应于轴1。
np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
array([[2., 1., 4., 3.], [1., 2., 3., 4.], [4., 3., 2., 1.]])
运算符
我们的兴趣不仅限于读取数据和写入数据。 我们想在这些数据上执行数学运算,其中最简单且最有用的操作是按元素(elementwise)运算。 它们将标准标量运算符应用于数组的每个元素。 对于将两个数组作为输入的函数,按元素运算将二元运算符应用于两个数组中的每对位置对应的元素。 我们可以基于任何从标量到标量的函数来创建按元素函数。
在数学表示法中,我们将通过符号\(f: \mathbb{R} \rightarrow \mathbb{R}\) 来表示一元标量运算符(只接收一个输入)。 这意味着该函数从任何实数(\(\mathbb{R}\))映射到另一个实数。 同样,我们通过符号\(f: \mathbb{R}, \mathbb{R} \rightarrow \mathbb{R}\) 表示二元标量运算符,这意味着该函数接收两个输入,并产生一个输出。 给定同一形状的任意两个向量\(\mathbf{u}\)和\(\mathbf{v}\)和二元运算符\(f\), 我们可以得到向量\(\mathbf{c} = F(\mathbf{u},\mathbf{v})\)。 具体计算方法是\(c_i \gets f(u_i, v_i)\), 其中\(c_i\)、\(u_i\)和\(v_i\)分别是向量\(\mathbf{c}\)、\(\mathbf{u}\)和\(\mathbf{v}\)中的元素。 在这里,我们通过将标量函数升级为按元素向量运算来生成向量值 \(F: \mathbb{R}^d, \mathbb{R}^d \rightarrow \mathbb{R}^d\)。
对于任意具有相同形状的张量, 常见的标准算术运算符(+
、-
、*
、/
和**
)都可以被升级为按元素运算。 我们可以在同一形状的任意两个张量上调用按元素操作。 在下面的例子中,我们使用逗号来表示一个具有5个元素的元组,其中每个元素都是按元素操作的结果。
x = np.array([1, 2, 4, 8]) y = np.array([2, 2, 2, 2]) x + y, x - y, x * y, x / y, x ** y # **运算符是求幂运算
(array([ 3., 4., 6., 10.]), array([-1., 0., 2., 6.]), array([ 2., 4., 8., 16.]), array([0.5, 1. , 2. , 4. ]), array([ 1., 4., 16., 64.]))
“按元素”方式可以应用更多的计算,包括像求幂这样的一元运算符。
np.exp(x)
array([2.7182817e+00, 7.3890562e+00, 5.4598148e+01, 2.9809580e+03])
除了按元素计算外,我们还可以执行线性代数运算,包括向量点积和矩阵乘法。 我们将在 2.3节中解释线性代数的重点内容。
我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。 我们只需要提供张量列表,并给出沿哪个轴连结。 下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素) 和按列(轴-1,形状的第二个元素)连结两个矩阵时,会发生什么情况。 我们可以看到,第一个输出张量的轴-0长度(\(6\))是两个输入张量轴-0长度的总和(\(3 + 3\)); 第二个输出张量的轴-1长度(\(8\))是两个输入张量轴-1长度的总和(\(4 + 4\))。
X = np.arange(12).reshape(3, 4) Y = np.array([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]]) np.concatenate([X, Y], axis=0), np.concatenate([X, Y], axis=1)
(array([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [ 2., 1., 4., 3.], [ 1., 2., 3., 4.], [ 4., 3., 2., 1.]]), array([[ 0., 1., 2., 3., 2., 1., 4., 3.], [ 4., 5., 6., 7., 1., 2., 3., 4.], [ 8., 9., 10., 11., 4., 3., 2., 1.]]))
有时,我们想通过逻辑运算符构建二元张量。 以X == Y
为例: 对于每个位置,如果X
和Y
在该位置相等,则新张量中相应项的值为1。 这意味着逻辑语句X == Y
在该位置处为真,否则该位置为0。
X == Y
array([[False, True, False, True], [False, False, False, False], [False, False, False, False]])
对张量中的所有元素进行求和,会产生一个单元素张量。
X.sum()
array(66.)
广播机制
在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:首先,通过适当复制元素来扩展一个或两个数组, 以便在转换之后,两个张量具有相同的形状。 其次,对生成的数组执行按元素操作。
在大多数情况下,我们将沿着数组中长度为1的轴进行广播,如下例子:
a = np.arange(3).reshape(3, 1) b = np.arange(2).reshape(1, 2) a, b
(array([[0.], [1.], [2.]]), array([[0., 1.]]))
由于a
和b
分别是\(3\times1\)和\(1\times2\)矩阵,如果让它们相加,它们的形状不匹配。 我们将两个矩阵广播为一个更大的\(3\times2\)矩阵,如下所示:矩阵a
将复制列, 矩阵b
将复制行,然后再按元素相加。
a + b
array([[0., 1.], [1., 2.], [2., 3.]])
索引和切片
就像在任何其他Python数组中一样,张量中的元素可以通过索引访问。 与任何Python数组一样:第一个元素的索引是0,最后一个元素索引是-1; 可以指定范围以包含第一个元素和最后一个之前的元素。
如下所示,我们可以用[-1]
选择最后一个元素,可以用[1:3]
选择第二个和第三个元素:
X[-1], X[1:3]
(array([ 8., 9., 10., 11.]), array([[ 4., 5., 6., 7.], [ 8., 9., 10., 11.]]))
除读取外,我们还可以通过指定索引来将元素写入矩阵。
X[1, 2] = 9 X
array([[ 0., 1., 2., 3.], [ 4., 5., 9., 7.], [ 8., 9., 10., 11.]])
如果我们想为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值。 例如,[0:2,:]
访问第1行和第2行,其中“:”代表沿轴1(列)的所有元素。 虽然我们讨论的是矩阵的索引,但这也适用于向量和超过2个维度的张量。
X[0:2, :] = 12 X
array([[12., 12., 12., 12.], [12., 12., 12., 12.], [ 8., 9., 10., 11.]])
-
深度学习存储和操作数据的主要接口是张量(\(n\)维数组)。它提供了各种功能,包括基本数学运算、广播、索引、切片。