2.1 Numpy
几乎所有的Python科学计算库都使用Numpy封装数组与矩阵运算,其重要性对于机器学习开发者来说不言而喻。本节不是 Numpy 的完全参考手册,但是引入足够的必要知识能够帮助读者顺利阅读本书代码,并使读者有能力在需要时快速查找在线手册。这个目标同样适用于后面的其他工具。
首先,通过pip3命令安装Numpy:
在使用Numpy的程序中笔者习惯性地将其重命名为np,本书后续代码沿用这一约定。目前为止Numpy的pip稳定版是1.14.5,也是本书学习与使用的版本。
2.1.1 Numpy与Scipy的分工
Numpy 与 Scipy 都是 Python 的基础运算库,在学习之前有必要明确两者的异同点。按照Scipy官方网站的定义,在理想情况下,Numpy应该只包含多维数组数据结构本身和一些围绕其进行的基本操作:读取、排序、变形等;而Scipy是利用Numpy的基础数据结构进行数学运算,比如线性代数、概率分布、傅里叶运算等。
但由于 Numpy 是由 Numeric、numarray 等开源库逐步发展而来的,出于历史兼容的原因Numpy也开发了相当多的数学运算封装,在这些运算中很多是与Scipy互相重复的,比如Numpy和Scipy都有线性代数运算函数。
思考:开发者如何选择Numpy和Scipy呢?
对于这个问题可以考虑如下几个方面:
◎ 用Numpy管理数据多维数组结构。
◎ 虽然Scipy的多数运算库在Numpy中也有实现,但通常Scipy中的版本提供了更强大的功能,所以对新项目开发来说,选择Scipy进行除加减乘除外的其他高级数学运算。
◎ 很多历史开源代码选择了Numpy的数学运算,其调用形式与Scipy中的方式类似。所以掌握Scipy的开发者可以顺利读懂Numpy的数学运算,无须特别学习Numpy中的庞大运算库。
◎ 对于新运算功能的开发往往被包含在Scipy中,而不是Numpy。
出于这几点,本节围绕Numpy数组类型ndarray讲解多维数组的数据结构和基本操作,而将数学运算方面的大量内容放在Scipy中讲解。
2.1.2 ndarray构造
Numpy是一组围绕ndarray数组及其相关运算定义的函数库。从功能上说,ndarray数组提供与Python原生列表类型(list)几乎相同的功能,但使用ndarray可以提高程序的开发和运行效率。比如,用Python list做两个数组元素间乘法的代码如下:
以上代码中需要用一个循环执行逐个元素的相乘。而使用Numpy数组时的代码是:
以上代码直接使用ndarray的乘法运算进行元素相乘,不但简洁易懂,而且由于Numpy本身主要由C语言编写,因此在代码执行效率上也快很多。这个速度优势在数组元素不多时体现不明显,但对于图像处理等程序动辄几十万的数组长度来说优势是巨大的。
1.给定值生成ndarray
ndarray是Numpy的核心对象,numpy.array()函数是它最简单的构造方式。ndarray的完整名称是numpy.ndarray,容易混淆的是,它的对象描述名称是array,比如:
说明:在不同的书籍与文章中,有时ndarray类型也被称为Numpy数组。
与Python数组不同的是,在ndarray中一个数组的所有元素必须是相同数据类型,因此在初始化列表中发现不同类型的元素时需要做类型提升:
其他一些技巧还包括:
2.快速生成构造
可以用empty()、eye()、ones()、zeros()等函数构造未初始化、由0和1组成的数组或矩阵:
以上代码中用整数列表指定初始化的数组维数,比如np.ones([2,3,4])用于初始化2×3×4的三维数组。这个整数列表就是Numpy中所谓的 shape属性,很多 Numpy函数都用shape作为参数。当给shape参数传入整数时(比如n),其作用与传入列表[n,]一样,都是指一个一维数组。
3.插值构造
arange()、linspace()、logspace()等函数可以在给定的取值范围内用步长、比值等规则构造数组序列,比如:
4.随机采样构造
除了普通的随机采样,Numpy还提供了大量的按照某种分布随机产生ndarray元素的方法,比如:
类似 random.normal()的按概率分布生成数组的函数还有很多,比如 random.beta()、random.dirichlet()、random.poisson()等。
Numpy 中还有很多数组构造函数,比如从其他源对象转换成 ndarray 的 asarray()、asmatrix()、copy()、frombuffer()、fromfile()、fromiter()等,直接构建矩阵的diag()、tril()、triu()、vander()等,这里不再一一举例。
提示:在 Numpy中还定义了一个 ndarray子类 matrix,即矩阵。它继承了 ndarray的所有特性,只是维度被固定为二维,本书不再对其进行单独讨论。
2.1.3 数据类型
ndarray中的元素类型没有使用Python的类型系统,而是提供了一套更丰富的类型系统。开发者既可以使用Numpy已有的基本数据类型(dtype),也可以自定义新的元素数据类型。
1.基本数据类型
Numpy的所有类型可以通过如下代码获得完整列表:
对不同大小的数值定义不同的类型,使得开发者有机会选择恰当的元素大小,以免造成不必要的空间浪费。在ndarray的构造函数中可以用dtype参数指定元素类型,比如:
对于已有的ndarray对象,也可以通过读取其dtype属性获得元素类型。
使用astype()函数可以转换ndarray数组的元素类型:
2.自定义数据类型
开发者可以利用基本数据类型定义新的组合数据类型。比如,如果元素有两个数据段age和gender:
代码中建立了新的组合数据类型type_array,该类型的两个子字段分别为uint8和bool类型。然后初始化了该类型的一个一维数组a,可以通过下标索引访问子元素:
2.1.4 访问与修改
Numpy 提供了切片、下标索引、循环迭代等多种方式用于访问和修改(slicing、indexing、iteration、unique)数组元素。
1.切片
Numpy 在 ndarray 中保留了 Python 对列表变量的切片操作(slicing)用来读取数组内容。切片的基本语法是一个 i:j:k索引器,其中 i是起始索引,j是结束索引,k是步长。比如:
i、j、k的取值允许是负值,其含义是从倒数索引,比如:
切片操作不仅可以读取数组,还可以用来直接修改元素内容:
2.下标访问
除切片外,Numpy还允许直接使用下标列表读取ndarray数组的内容:
也可以用布尔类型下标列表访问:
其中布尔列表的长度必须与数组x长度相同。
3.迭代对于一维数组,可以直接迭代ndarray以访问所有元素:
对于多维数组,可以使用ndenumerate()函数在迭代的同时获得元素下标:
4.唯一列表
通过unique()函数可以生成非重复元素数组,还可以返回元素正向与反向构造的索引、元素计数等,比如:
其中第一个返回值是结果数组,第二个返回值是结果数组中每个元素在原数组中的索引,第三个返回值是原数组中每个元素在结果数组中的索引,第四个返回值是结果数组中每个元素在原数组中的计数。
2.1.5 轴
不仅是Numpy,轴(axis)的概念在Matplot等其他Python科学计算包中也随处可见。在Numpy中它是很多排序、变形操作的基础。本小节用举例的方式来讨论轴的用法。
1.排序
Numpy排序函数sort()原型如下:
其中a是输入数组;axis是轴,可以取值None或者整型;另外两个参数kind和order指定排序方法和排序字段。理解axis的含义是灵活运用该函数的关键。
轴概念的产生是由于需要处理多维数组,与维度(ndim)息息相关。Numpy维度概念与Python列表中的维度相同,比如:
轴的概念就是指从零开始的“数组的第几个维度”,sort()函数中的axis参数用于指定按哪个维度排序,比如图2-1是二维数组的排序示例。
图2-1 轴与sort()函数
当对sort()的axis设置None时,函数先将多维数组平铺,然后返回所有元素总的结果:
2.求和
另一个以axis作为参数的例子是求和函数sum():
一个2×3×2的三维数组在对轴1进行聚合操作后变成2×2的二维数组。
3.转置
数组转置操作的函数原型:
在Numpy中数组转置操作是维度顺序的调整。比较好理解的是二维数组的转置:
对于多维数组的转置则可能有多种变换方法,比如轴的顺序原来是0/1/2,转置后可以变换为1/2/0、2/0/1或1/0/2等。这时就可以通过 axes参数来指定目标轴的顺序,具体实现代码如下。
注意:axes是axis的英文复数形式,在查找手册时可以根据名字就判断出为该参数传递数值列表还是单个数值。
2.1.6 维度操作
本小节学习常用的ndarray形状与维度操作(shape and dimension)。
1.变形(changing shape)
最典型的变形操作是reshape(),它可以将已有数组变换成任何维度的数组:
可以将变形理解为先把所有元素平铺再按需求组装的过程,需要注意变换后的数组元素总数必须与原数组中的相同,本例中即为4×3==2×2×3。另外还有函数 ravel()、ndarray.flat()、ndarray.flatten()等对数组进行平铺的变形函数。
还可以通过flip()函数反转某个轴的数组内容:
与 flip()类似的还有适用于矩阵数组的 fliplr()与flipud()函数。它们不是反转某个轴,而是按矩阵对角线进行反转。
2.维度操作(changing number of dimensions)
此类操作允许对数组增加或减少维度,其中 atleast_1d()、atleast_2d()、atleast_3d()用于将任意对象置于有至少1个维度的数组中,通常用于数组初始化,比如:
expand_dims()用于在指定轴增加维度:
squeeze()函数可以看成expand_dims()的反函数,它移除长度为1的维度:
2.1.7 合并与拆分
Numpy中有一组进行合并与拆分(joining and splitting)ndarray数组的函数,它们是拼接函数stack()、concatenate()、column_stack()等,拆分函数split()、array_split()等。
1.合并
column_stack()是将多个一维数组合并成二维数组/矩阵的函数:
concatenate()与stack()进行更灵活的拼接:
以上代码给出了concatenate()与stack()的区别:concatenate()是在已有维度上拼接,本例中输入数组与输出数组都是二维;stack()是在拼接的同时建立新的维度,其中stack()要求两个输入数组的形状必须相同。另外与stack()类似的函数还有vstack()、hstack()、dstack()等,分别在指定维度拼接数组。
2.拆分
数组拆分的典型函数是splite(),其原型为:
其中a是输入数组,indices_or_sections是划分方式,a既可以设为一个整数也可以是一个数列,理解indices_or_sections是运用拆分的关键,举例如下:
axis参数用于指定拆分的轴,比如:
另外Numpy中还有hsplit()、vsplit()、dsplit()等函数用于指定轴的数组拆分,比如vstack()与split(axi=1)的作用相同。
2.1.8 增与删
Numpy通过delete()、insert()、append()三个函数进行元素增删(adding and remove)操作:
append()与insert()类似,但是只能在轴的末尾插入,无法指定位置。
2.1.9 全函数
Numpy 中的 ufunc 是一组能对数组中每个元素进行批量操作的全函数。ufunc 内部通过C语言来实现,所以速度非常快。举例如下:
其中np.add()就是一个ufunc,它实现了两个数组中每个元素的加法。开发者还可以直接对 ndarray调用算数运算符使用 ufunc,比如 np.add(a,b)与 a×b 等价,类似的情况还有np.subtract()、np.multiple()、np.divide()等。
ufunc 的成员函数非常多,它们可以分为如下几个大类(在用到的时候可以查找在线手册)。
◎ 算数运算:比如pow()、mod()、fabs()、log()、exp()、sqrt()等。
◎ 三角函数:sin()、cos()、tan()、arcsin()等。
◎ 比特操作:bitwise_and()、bitwise_or()、invert()、bitwise_xor()、left_shift()等。
◎ 比较:greater()、greater_equal()、less()、not_equal()、logical_and()等。
◎ 浮点运算:isfinit()、fmod()、floor()、ceil()等。
这些函数与同名的Python库函数意义相同,只是ufunc函数可以批量操作数组元素。
2.1.10 广播
ufunc 函数的使用很自然地引出了另外一个话题:当两个操作出现被操作数组在维度与形状上不一致的情况时该如何处理。Numpy 定义了处理这种情况的一套机制,即广播(broadcasting)。广播机制可以通过四条规则来描述。
◎ 当多个输入数组的维度不相同时,小维度的数组会补齐到大维度,被增加的那个轴长度被设为1。比如两个输入数组分别是一维和二维时,一维输入数组也会被转换为二维数组。
◎ 输出数组中各个维度的长度是输入数组中各维度的最大值。
◎ 输入数组的每个轴上要么长度与输出数组相匹配,要么长度为1,否则计算会出错。
◎ 输入数组中长度为1的轴中的数据将被用作与其他输入数组该轴上的所有数据进行计算。
上述规则使得计算不同形状的ndarray非常方便,笔者认为广播能力是Numpy能够如此被广泛应用的最主要原因,灵活运用广播规则正是提高 Python 大数据计算开发效率的关键所在。通过如下例子解释上述规则: