第1章 NumPy基础

为何第1章介绍NumPy基础?在机器学习和深度学习中,图像、声音、文本等首先要数字化,如何实现数字化?数字化后如何处理?这些都涉及NumPy。NumPy是数据科学的通用语言,它是科学计算、矩阵运算、深度学习的基石。PyTorch中的重要概念张量(Tensor)与NumPy非常相似,它们之间可以方便地进行转换,掌握NumPy是学好PyTorch的重要基础,故我们把它列为全书第1章。
基于NumPy的运算有哪些优势?实际上Python本身含有列表(list)和数组(array),但对于大数据来说,这些结构有很多不足。因列表的元素可以是任何对象,因此列表中所保存的是对象的指针。例如为了保存一个简单的[1,2,3],都需要有3个指针和三个整数对象。对于数值运算来说这种结构显然比较浪费内存和CPU等宝贵资源。 至于array对象,它直接保存数值,和C语言的一维数组比较类似。但是由于它不支持多维,建立在上面的函数也不多,因此也不适合做数值运算。
NumPy(Numerical Python 的简称)的诞生弥补了这些不足,NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则为数组进行和处理提供了丰富的函数。
NumPy的主要特点:
1) ndarray,快速和节省空间的多维数组,提供数组化的算术运算和高级的广播功能。
2) 使用标准数学函数对整个数组的数据进行快速运算,而不需要编写循环。
3) 读取/写入磁盘上的阵列数据和操作存储器映像文件的工具。
4) 线性代数,随机数生成,和傅里叶变换的能力。
5) 集成C,C++,Fortran代码的工具。
本章主要内容如下:
♦ 把图像数字化
♦ 存取元素
♦ NumPy的算术运算
♦ 数组变形
♦ 批量处理
♦ 节省内存
♦ 通用函数
♦ 广播机制

1.1生成NumPy数组

NumPy是Python的第三方库,若要使用它,需要先导入NumPy。

导入NumPy后,可通过np.+Tab键查看可使用的函数,如图1-1所示。如果对其中一些函数的使用不很清楚,想看对应函数的帮助信息,可以在对应函数+?,再运行,就可很方便地看到使用函数的帮助信息。

图1-1 通过np.+Tab键查看可用函数
运行如下命令,便可查看函数abs的详细帮助信息。

NumPy不但强大,而且非常友好。接下来我们将介绍NumPy的一些常用方法,尤其是与机器学习、深度学习相关的一些内容。
NumPy封装了一个新的数据类型ndarray(n-dimensional array,n维数组),它是一个多维数组对象。该对象封装了许多常用的数学运算函数,方便我们做数据处理、数据分析等。如何生成ndarray呢?这里我们介绍生成ndarray的几种方式,如从已有数据中创建、利用random创建、创建特殊多维数组、使用arange函数等。
机器学习中图像、自然语言、语音等在输入模型之前,都需要数字化。这里我们用cv2把一个汽车图像(如图1-2所示)转换为NumPy多维数组,然后查看该多维数组的基本属性,具体代码如下:

运行结果如下:
数据类型:<class 'numpy.ndarray'>,形状:(675, 1200, 3)

图1-2 把轿车图像转换为NumPy

1.1.1 数组属性

在NumPy中,维度被称为轴,比如把轿车图像转换为一个NumPy之后的数组是1个三维数组,这个数组中有3个轴,这3个轴的长度分别为675、1200、3。
NumPy的ndarray对象有3个重要的属性。
 ndarray.ndim:数组的维度(轴)的个数。
 ndarray.shap:数组的维度,值是一个整数元祖,元祖的值代表其所对应的轴的长度。 比如对于二维数组,它用来表达这是个几行几列的矩阵,值为(x, y),其中x代表这个数组中有几行, y代表有几列。
 ndarray.dtype:数据类型,描述数组中元素的类型。
比如上面的img数组:

为更好地理解ndarray对象的3个重要属性,我们把一维数组、二维数组、三维数组进行可视化,如图1-3所示。

图1-3 多维数组的可视化表示。

1.1.2从已有数据中创建数组

直接对 Python 的基础数据类型(如列表、元组等) 进行转换来生成 ndarray:
1)将列表转换成 ndarray。

2)嵌套列表可以转换成多维 ndarray。

如果把上面示例中的列表换成元组,上述方法也同样适合。

1.1.3利用 random 模块生成数组

在深度学习中,我们经常需要对一些参数进行初始化。为了更有效地训练模型,提高模型的性能,有些初始化还需要满足一定条件,如满足正态分布或均匀分布等。这里我们介绍几种常用的方法,表1-1列举了 np.random 模块常用的函数。
1-1 np.random模块常用函数

下面我们来看看这些函数的具体使用方法:

运行结果如下:
生成形状(4, 4),值在0-1之间的随机数:
[[0.32033334 0.46896779 0.35755437 0.93218211]
[0.83150807 0.34724136 0.38684007 0.80832335]
[0.17085778 0.60505495 0.85251224 0.66465297]
[0.5351041 0.59959828 0.59819534 0.36759263]]

生成形状(3, 3),值在low-high之间的随机整数::
[[29 23 49]
[44 10 30]
[29 20 48]]

产生的数组元素是均匀分布的随机数:
[[2.16986668 1.43805178 2.84650421]
[2.59609848 1.96242833 1.02203859]
[2.64679581 1.30636158 1.42474749]]

生成满足正态分布的形状为(3, 3)的矩阵:
[[-0.26958446 -0.04919047 -0.86747396]
[-0.16477117 0.39098747 1.97640843]
[ 0.73003926 -1.03079529 -0.1624292 ]]
用以上方法生成的随机数是无法重现的,比如调用两次np.random.randn(3, 3), 输出结果一样的概率极低。如果我们想要多次生成同一份数据怎么办?我们可以使用np.random.seed函数设置种子。设置一个种子,然后调用随机函数产生一个数组,如果想要再次得到一个一模一样的数组,只要再次设置同样的种子就可以。

运行结果如下:
按指定随机种子,第1次生成随机数:
[[2 2]
[1 4]]
按相同随机种子,第2次生成的数据:
[[2 2]
[1 4]]

1.1.4创建特定形状的多维数组

在对参数进行初始化时,有时需要生成一些特殊矩阵,如全是0或1的数组或矩阵,这时我们可以利用np.zeros、np.ones、np.diag来实现,如表1-2所示。
表1-2 创建特定形状多维数组的函数

下面我们通过几个示例来说明:

运行结果如下:
******nd5******
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
******nd6******
[[1. 1. 1.]
[1. 1. 1.]
[1. 1. 1.]]
******nd7******
[[1. 0. 0.]
[0. 1. 0.]
[0. 0. 1.]]
******nd8******
[[1 0 0]
[0 2 0]
[0 0 3]]
有时我们可能需要把生成的数据暂时保存起来,以备后续使用。

运行结果如下:
[[0.41092437 0.5796943 0.13995076 0.40101756 0.62731701]
[0.32415089 0.24475928 0.69475518 0.5939024 0.63179202]
[0.44025718 0.08372648 0.71233018 0.42786349 0.2977805 ]
[0.49208478 0.74029639 0.35772892 0.41720995 0.65472131]
[0.37380143 0.23451288 0.98799529 0.76599595 0.77700444]]

1.1.5利用 arange、linspace 函数生成数组

一些情况下,我们还希望获得一组具有特定规律的数据,这时可以使用NumPy提供的arange、linspace函数实现。
arange 是 numpy 模块中的函数,其格式为:
arange([start,] stop[,step,], dtype=None)
其中start 与 stop 用于指定范围,step 设定步长,生成一个 ndarray,start 默认为 0,步长 step 可为小数。Python中的内置函数range的功能与此类似。

linspace 也是 numpy 模块中常用的函数,其格式为:
np.linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None)
它可以根据输入的指定数据范围以及等份数量,自动生成一个线性等分向量,其中endpoint (包含终点)默认为 True,等分数量num默认为 50。如果将retstep设置为 True,则会返回一个带步长的 ndarray。

值得一提的是,这里并没有像我们预期的那样,生成 0.1, 0.2, ... 1.0 这样步长为0.1的 ndarray,这是因为 linspace 必定会包含数据起点和终点,那么其步长则为(1-0) / 9 = 0.11111111。如果需要产生 0.1, 0.2, ... 1.0 这样的数据,只需要将数据起点 0 修改为 0.1 即可。
除了上面介绍到的 arange 和 linspace函数,NumPy还提供了 logspace 函数,该函数的使用方法与linspace的使用方法一样,读者不妨自己动手试一下。

1.2读取元素

上节介绍了生成ndarray的几种方法,当生成数据后,如何读取我们需要的数据呢?这节将介绍几种常用的读取数据的方法。

如果对上面这些获取方式还不是很清楚,没关系,下面以图形的方式加以说明,如图1-4所示,左边为表达式,右边为表达式获取的元素。注意,不同的边界表示不同的表达式。
0 1 2 3 4
5 6 7 8 9
10 11 12 13 14
15 16 17 18 19
20 21 22 23 24

图1-4 获取多维数组中的元素
除了可以通过指定索引标签获取数组中的部分元素外,还可以使用一些函数来实现,如可以通过random.choice函数从指定的样本中随机抽取数据。

运行结果如下:
随机可重复抽取
[[ 7. 22. 19. 21.]
[ 7. 5. 5. 5.]
[ 7. 9. 22. 12.]]
随机但不重复抽取
[[ 21. 9. 15. 4.]
[ 23. 2. 3. 7.]
[ 13. 5. 6. 1.]]
随机但按制度概率抽取
[[ 15. 19. 24. 8.]
[ 5. 22. 5. 14.]
[ 3. 22. 13. 17.]]

1.3 NumPy的算术运算

机器学习和深度学习中涉及大量的数组或矩阵运算,这节将重点介绍两种常用的运算。一种是对应元素相乘,又称为逐元乘法(Element-Wise Product),或哈达玛积(Hadamard Product),运算符为np.multiply(), 或 *。另一种是点积或内积元素,运算符为np.dot()。

1.3.1遂元素操作

遂元素操作(又称为对应元素相乘)是两个矩阵中对应元素乘积。np.multiply 函数用于数组或矩阵对应元素相乘,输出的大小与相乘数组或矩阵的大小一致,其格式如下:

其中x1、x2之间的对应元素相乘遵守广播规则,NumPy的广播规则将在1.8节介绍。下面我们通过一些示例来进一步说明。

矩阵A和B的对应元素相乘,如图1-5所示。

图1-5 对应元素相乘示意图
NumPy数组不仅可以和数组进行对应元素相乘,也可以和单一数值(或称为标量)进行运算。运算时,NumPy数组的每个元素和标量进行运算,其间会用到广播机制,例如:

运行结果如下:
[[ 2. 4.]
[-2. 8.]]
[[ 0.5 1. ]
[-0.5 2. ]]
由此,推而广之,数组通过一些激活函数后,输出与输入形状一致。

运行结果如下:
输入参数X的形状: (2, 3)
激活函数sigmoid输出形状: (2, 3)
激活函数relu输出形状: (2, 3)
激活函数softmax输出形状: (2, 3)

1.3.2 点积运算

点积(Dot Product)运算又称为内积运算,在NumPy中用np.dot表示,其一般格式为:
numpy.dot(a, b, out=None)
下面通过一个示例来说明dot的具体使用及注意事项。

运行结果如下:
[[21 24 27]
[47 54 61]]
以上运算可表示为如图1-6所示形式。

图1-6 矩阵的点积示意图,对应维度的元素个数需要保持一致
如图1-6所示,矩阵X1和矩阵X2进行点积运算,其中X1和X2对应维度(即X1的第2个维度与X2的第1个维度)的元素个数必须保持一致,此外,矩阵X3是由矩阵X1的行数与矩阵X2的列数构成的。
点积运算在神经网络中使用非常频繁,如图1-7所示的神经网络,输入I与权重矩阵W之间的运算就是点积运算。

图1-7 内积运算可视化示意图

1.4数组变形

在机器学习以及深度学习的任务中,我们通常需要将处理好的数据以模型能接收的格式发送给模型,然后由模型通过一系列运算,最终返回一个处理结果。然而,由于不同模型所接收的输入格式不一样,往往需要先对其进行一系列变形和运算,将数据处理成符合模型要求的格式。最常见的是矩阵或者数组的运算,我们经常会遇到需要把多个向量或矩阵按某轴方向合并,或展平(如在卷积或循环神经网络中,在全连接层之前,需要把矩阵展平)的情况。下面介绍几种常用数据变形方法。

1.4.1 修改数组的形状

修改指定数组的形状是NumPy中最常见的操作之一,常见的方法有很多,表1-3 列出了一些常用函数。
表1-3 NumPy中改变向量形状的一些函数
下面我们来看一些示例。
1)reshape函数。

运行结果如下:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
[[0 1]
[2 3]
[4 5]
[6 7]
[8 9]]
[[0 1 2 3 4]
[5 6 7 8 9]]
值得注意的是,reshape 函数支持只指定行数或列数,其余设置为-1即可。注意,所指定的行数或列数一定要能被整除,例如将上面代码修改为arr.reshape(3,-1)将报错,因为10不能被3整除。
2)resize函数。

运行结果如下:
[0 1 2 3 4 5 6 7 8 9]
[[0 1 2 3 4]
[5 6 7 8 9]]
3)T函数。

运行结果如下:
[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[ 0 4 8]
[ 1 5 9]
[ 2 6 10]
[ 3 7 11]]
4)ravel函数。
ravel 函数接收一个根据C语言格式(即按行优先排序)或者Fortran语言格式(即按列优先排序)来进行展平的参数,默认情况下是按行优先排序。

运行结果如下:
[[0 1 2]
[3 4 5]]
按照列优先,展平
[0 3 1 4 2 5]
按照行优先,展平
[0 1 2 3 4 5]
5)flatten(order='C')函数。
把矩阵转换为向量,展平方式默认是行优先(即参数order='C'),这种需求经常出现在卷积网络与全连接层之间。

运行结果如下:
[[4. 0. 8. 5.]
[1. 0. 4. 8.]
[8. 2. 3. 7.]]
[4. 0. 8. 5. 1. 0. 4. 8. 8. 2. 3. 7.]
Flatten(展平)运算,在神经网络中经常使用,一般在网络的后面需要把2维、3维等多维数组转换为一维数组,此时就需要用到展平这个操作,如图1-8所示。

图1-8 含flatten运算的神经网络示意图
6)squeeze函数。
squeeze函数是一个主要用于降维的函数,可以把矩阵中含1的维度去掉。

7)transpose函数
transpose函数主要用于对高维矩阵进行轴对换,在深度学习中经常用到,比如把图像表示颜色的RGB顺序改为GBR的顺序。

1.4.2 合并数组

合并数组也是最常见的操作之一,表1-4列举了常用的NumPy数组或向量合并的方法。
表1-4 常用的NumPy 数组合并的方法

[说明]
1)append、concatnate以及stack函数都有一个 axis 参数,用于控制数组合并是按行还是按列排序。
2)append和concatnate函数中待合并的数组必须有相同的行数或列数(满足一个即可)。
3)stack、hstack、dstack函数中待合并的数组必须具有相同的形状( shape)。
下面选择一些常用函数进行说明。
1.append
合并一维数组:

合并多维数组:

运行结果如下:
按行合并后的结果
[[0 1]
[2 3]
[0 1]
[2 3]]
合并后数据维度 (4, 2)
按列合并后的结果
[[0 1 0 1]
[2 3 2 3]]
合并后数据维度 (2, 4)
2.concatenate
沿指定轴连接数组或矩阵:

运行结果如下:
[[1 2]
[3 4]
[5 6]]
[[1 2 5]
[3 4 6]]
3.stack
沿指定轴堆叠数组或矩阵:

运行结果如下:
[[[1 2]
[3 4]]

[[5 6]
[7 8]]]
4.zip
zip是Python的一个内置函数,多用于张量运算中。

运行结果如下:
[1 2],[5 6]
[3 4],[7 8]
zip函数组合两个向量。

运行结果如下:
1,4
2,5
3,6

1.5 批量处理

在深度学习中,由于源数据都比较大,所以通常需要采用批处理。如利用批量来计算梯度的随机梯度法(SGD)就是一个典型应用。深度学习的计算一般比较复杂,加上数据量一般比较大,如果一次处理整个数据,往往出现资源瓶颈。为了更有效地计算,一般将整个数据集分成多个小批量。与处理整个数据集的另一个极端是每次处理一条记录,这种方法也不科学,因为一次处理一条记录无法充分发挥GPU、NumPy平行处理优势。因此,在实际使用中我们往往采用批量处理(mini-batch)。
如何把大数据拆分成多个批次呢?可采用如下步骤:
• 得到数据集
• 随机打乱数据
• 定义批大小
• 批处理数据集
下面通过一个示例来具体说明:

最后5行结果如下:
第9500批次,该批次的数据之和:17.63702580438092
第9600批次,该批次的数据之和:-1.360924607368387
第9700批次,该批次的数据之和:-25.912226239266445
第9800批次,该批次的数据之和:32.018136957835814
第9900批次,该批次的数据之和:2.9002576614446935
【说明】
批次从0开始,所以最后一个批次是9900。

1.6 节省内存

在NumPy操作数据过程中,有大量涉及变量、数组的操作,尤其在机器学习、深度学习中,参数越来越多,数据量也越来越大,如何有效保存、更新这些参数,将直接影响内存的使用。这里我们介绍几种节省内存的简单方法。
1. 使用X=X+Y与X += Y的区别
假设X、Y为向量或矩阵,这种操作在机器学习中非常普遍。两个表达式从数学角度来说是完全一样的,但对使用内存的开销来说,却完全不同。X += Y操作可减少内存开销。
下面我们用Python的id()函数来说明。id()函数提供了内存中引用对象的确切地址。 运行X = X+Y后,我们会发现id(X)指向另一个位置。 这是因为Python首先计算X+Y,为结果分配新的内存,然后使X指向内存中的这个新位置。

运行结果如下:
1852224075136
1852224037312
X在运行X=X+Y前后id不同,说明指向不同内存区域。

运行结果如下:
1852224018672
1852224018672
X在运行X+=Y前后id相同,说明指向一个内存区域。
2. X=X+Y与X[:]=X+Y的区别
实现代码如下:

运行结果如下:
1852224017152
1852224018672
X在运行X=X+Y前后id不同,说明指向不同内存区域。

X在运行X[:]=X+Y前后id相同,说明指向一个内存区域。

1.7通用函数

NumPy提供了两种基本的对象,即ndarray和ufunc对象。前面我们介绍了ndarray,本节将介绍NumPy的另一个对象通用函数——ufunc。ufunc是universal function的缩写,是一种能对数组的每个元素进行操作的函数。许多ufunc函数都是用C语言实现的,因此它们的计算速度非常快。此外,它们比math模块中函数更灵活。math模块的输入一般是标量,但NumPy中函数的输入可以是向量或矩阵,而利用向量或矩阵可以避免使用循环语句,这点在机器学习、深度学习中非常重要。表1-5列举了几个NumPy的常用通用函数。
表1-5 NumPy的常用通用函数


【说明】np.max,np.sum,np.min等函数中,都涉及一个有关轴的参数(即axis),该参数的具体含义,可参考图1-9。
-图1-9 可视化参数axis的具体含义

1.math与numpy函数的性能比较
实现代码如下:

运行结果如下:
math.sin: 0.5169950000000005
numpy.sin: 0.05381199999999886
由此可见,numpy.sin比math.sin快近10倍。
2.循环与向量运算比较
充分使用Python的NumPy库中的内建函数(built-in function),实现计算的向量化,可大大提高运行速度。NumPy库中的内建函数使用了SIMD指令。如下使用的向量化要比使用循环计算速度快得多。如果使用GPU,其性能将更强大,不过NumPy不支持GPU。

运行结果如下:
dot = 250215.601995
for loop----- Computation time = 798.3389819999998ms
dot = 250215.601995
verctor version---- Computation time = 1.885051999999554ms
从运行结果上来看,使用for循环的运行时间大约是向量运算的400倍。因此,深度学习算法中,一般都使用向量化矩阵运算。
3.np.where的使用
np.where()有两种使用方法,其功能类似于列表中的推导式。
(1)np.where(condition, x, y)
满足条件(condition)(可理解为非0),输出x,不满足输出y。其中condition、y和z都是数组,它的返回值是一个形状与condition相同的数组。
先看condition为一维的情况。

a的值: [-2 -1 0 1 2 3 4 5 6 7 8 9]
x的值 [ 1 1 -1 1 1 1 1 1 1 1 1 1]
如果condtion为二维的情况,实例如下:

运行结果:
z的值 [[1 8] [3 4]]
(2)np.where(condition)
只有条件 (condition),没有x和y,则输出满足条件 (即非0) 元素的索引 (等价于numpy.nonzero),以元组方式输出。

运行结果
c的值 (array([2, 3, 4], dtype=int64),)

运行结果
d的值 [[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
d的值 (array([1, 1, 2, 2, 2, 2], dtype=int64), array([2, 3, 0, 1, 2, 3], dtype=int64))

1.8 广播机制

NumPy的通用函数中要求输入的数组形状(shape)是一致的,当数组的形状不相等时,则会使用广播机制。不过,调整数组使得shape一样时,需满足一定规则,否则将出错。这些规则可归结为以下4条:
1)让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分都通过在前面加1补齐;如:a为2x3x2,b为3x2,则b向a看齐,在b的前面加1,变为1x3x2;
2)输出数组的shape是输入数组shape的各个轴上的最大值;
3)如果输入数组的某个轴和输出数组的对应轴的长度相同或者其长度为1时,这个数组能够用来计算,否则将出错;
4)当输入数组的某个轴的长度为1时,沿着此轴运算时都用(或复制)此轴上的第一组值。
广播在整个NumPy中用于决定如何处理形状迥异的数组;涉及算术运算包括(+,-,*,/…)。这些规则说的很严谨,但不直观,下面我们结合图形与代码进一步说明。
目的:A+B,其中A为4x1矩阵,B为一维向量 (3,)。
要实现A、B相加,需要做如下处理。
1)根据规则1,B需要向看齐,把B变为(1,3)。
2)根据规则2,输出的结果为各个轴上的最大值,即输出结果应该为(4,3)矩阵,
那么A如何由(4,1)变为(4,3)矩阵?B如何由(1,3)变为(4,3)矩阵?
3)根据规则4,用此轴上的第一组值(要主要区分是哪个轴),进行复制(但在实际处理中不是真正复制,否则太耗内存,而是采用其他对象如ogrid对象,进行网格处理)即可,
详细处理如图1-10所示。

图1-10 NumPy广播规则示意图
代码实现如下:

运行结果如下:
A矩阵的形状:(4, 1),B矩阵的形状:(3,)
C矩阵的形状:(4, 3)
[[ 0 1 2]
[10 11 12]
[20 21 22]
[30 31 32]]

1.9小结

本章主要介绍了NumPy的使用。机器学习、深度学习涉及很多向量与向量、向量与矩阵、矩阵与矩阵的运算,这些运算都离不开NumPy,NumPy为各种运算提供了各种高效方法,同时NumPy也是PyTorch张量运算的重要基础。