在深度学习的实现中,经常出现数组和矩阵的计算。NumPy 的数组类(numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使用这些方法。本节我们来简单介绍一下后面会用到的 NumPy。

NumPy 是外部库。这里所说的“外部”是指不包含在标准版 Python 中。因此,我们首先要导入 NumPy 库。

>>> import numpy as np

Python 中使用 import 语句来导入库。这里的 import numpy as np,直译的话就是“将 numpy 作为 np 导入”的意思。通过写成这样的形式,之后 NumPy 相关的方法均可通过 np 来调用。

要生成 NumPy 数组,需要使用 np.array() 方法。np.array() 接收 Python 列表作为参数,生成 NumPy 数组(numpy.ndarray)。

>>> x = np.array([1.0, 2.0, 3.0])
>>> print(x)
[ 1. 2. 3.]
>>> type(x)
<class 'numpy.ndarray'>

下面是 NumPy 数组的算术运算的例子。

>>> x = np.array([1.0, 2.0, 3.0])
>>> y = np.array([2.0, 4.0, 6.0])
>>> x + y  # 对应元素的加法
array([ 3.,  6., 9.])
>>> x - y
array([ -1.,  -2., -3.])
>>> x * y  # element-wise product
array([  2.,   8.,  18.])
>>> x / y
array([ 0.5,  0.5,  0.5])

这里需要注意的是,数组 x 和数组 y 的元素个数是相同的(两者均是元素个数为 3 的一维数组)。当 xy 的元素个数相同时,可以对各个元素进行算术运算。如果元素个数不同,程序就会报错,所以元素个数保持一致非常重要。另外,“对应元素的”的英文是 element-wise,比如“对应元素的乘法”就是 element-wise product。

NumPy 数组不仅可以进行 element-wise 运算,也可以和单一的数值(标量)组合起来进行运算。此时,需要在 NumPy 数组的各个元素和标量之间进行运算。这个功能也被称为广播(详见后文)。

>>> x = np.array([1.0, 2.0, 3.0])
>>> x / 2.0
array([ 0.5,  1. ,  1.5])

NumPy 不仅可以生成一维数组(排成一列的数组),也可以生成多维数组。比如,可以生成如下的二维数组(矩阵)。

>>> A = np.array([[1, 2], [3, 4]])
>>> print(A)
[[1 2]
 [3 4]]
>>> A.shape
(2, 2)
>>> A.dtype
dtype('int64')

这里生成了一个 2 × 2 的矩阵 A。另外,矩阵 A 的形状可以通过 shape 查看,矩阵元素的数据类型可以通过 dtype 查看。下面,我们来看一下矩阵的算术运算。

>>> B = np.array([[3, 0],[0, 6]])
>>> A + B
array([[ 4,  2],
       [ 3, 10]])
>>> A * B
array([[ 3,  0],
       [ 0, 24]])

和数组的算术运算一样,矩阵的算术运算也可以在相同形状的矩阵间以对应元素的方式进行。并且,也可以通过标量(单一数值)对矩阵进行算术运算。这也是基于广播的功能。

>>> print(A)
[[1 2]
 [3 4]]
>>> A * 10
array([[ 10, 20],
       [ 30, 40]])

 NumPy 数组(np.array)可以生成 N 维数组,即可以生成一维数组、二维数组、三维数组等任意维数的数组。数学上将一维数组称为向量,将二维数组称为矩阵。另外,可以将一般化之后的向量或矩阵等统称为张量(tensor)。本书基本上将二维数组称为“矩阵”,将三维数组及三维以上的数组称为“张量”或“多维数组”。

NumPy 中,形状不同的数组之间也可以进行运算。之前的例子中,在 2×2 的矩阵 A 和标量 10 之间进行了乘法运算。在这个过程中,如图 1-1 所示,标量 10 被扩展成了 2 × 2 的形状,然后再与矩阵 A 进行乘法运算。这个巧妙的功能称为广播(broadcast)。

图 1-1 广播的例子:标量 10 被当作 2 × 2 的矩阵

我们通过下面这个运算再来看一个广播的例子。

>>> A = np.array([[1, 2], [3, 4]])
>>> B = np.array([10, 20])
>>> A * B
array([[ 10, 40],
       [ 30, 80]])

在这个运算中,如图 1-2 所示,一维数组 B 被“巧妙地”变成了和二维数组 A 相同的形状,然后再以对应元素的方式进行运算。

图 1-2 广播的例子 2

综上,因为 NumPy 有广播功能,所以不同形状的数组之间也可以顺利地进行运算。

元素的索引从 0 开始。对各个元素的访问可按如下方式进行。

>>> X = np.array([[51, 55], [14, 19], [0, 4]])
>>> print(X)
[[51 55]
 [14 19]
 [ 0 4]]
>>> X[0]    # 第0行
array([51, 55])
>>> X[0][1] # (0,1)的元素
55

也可以使用 for 语句访问各个元素。

>>> for row in X:
...     print(row)
...
[51 55]
[14 19]
[0 4]

除了前面介绍的索引操作,NumPy 还可以使用数组访问各个元素。

>>> X = X.flatten()         # 将X转换为一维数组
>>> print(X)
[51 55 14 19  0  4]
>>> X[np.array([0, 2, 4])] # 获取索引为0、2、4的元素
array([51, 14,  0])

运用这个标记法,可以获取满足一定条件的元素。例如,要从 X 中抽出大于 15 的元素,可以写成如下形式。

>>> X > 15
array([ True,  True, False,  True, False, False], dtype=bool)
>>> X[X>15]
array([51, 55, 19])

对 NumPy 数组使用不等号运算符等(上例中是 X > 15),结果会得到一个布尔型的数组。上例中就是使用这个布尔型数组取出了数组的各个元素(取出 True 对应的元素)。

 Python 等动态类型语言一般比 C 和 C++ 等静态类型语言(编译型语言)运算速度慢。实际上,如果是运算量大的处理对象,用 C/C++ 写程序更好。为此,当 Python 中追求性能时,人们会用 C/C++ 来实现处理的内容。Python 则承担“中间人”的角色,负责调用那些用 C/ C++ 写的程序。NumPy 中,主要的处理也都是通过 C 或 C++ 实现的。因此,我们可以在不损失性能的情况下,使用 Python便利的语法。