Python笔记 #2 NumPy
本文最后更新于:2023年10月31日 下午
学习 Machine Learning 的时候发现需要用许多矩阵运算和画图的库,本文将以实用主义的方式记录每次遇到的新用法。
2021 年贵系的暑培新增了「科学计算」内容,本文部分内容参考了清华 LZJ 同学的教程,部分参考自 NumPy 中文官网 参考手册。本文将持续更新。
常用的库
计算机领域,有用的数据特指结构化的数据,即符合一定的语法规范,才能方便计算机处理。这些数据包括表格型的数据,多维数组型的数据,由键位列关联起来的多张表数据等等。针对这些数据,Python 都有相应库去处理。
数据处理:
NumPy:是 Numerical Python 的缩写,用于处理大规模多维数组,矩阵计算等,有成熟的 C 接口,很多 Python 的第三方库都基于 NumPy 实现(例如 Python 的计算机视觉库 cv2 )。
Pandas:提供了更高级的数据结构和函数,适用于处理表格化、结构化的数据。
科学计算:
- SciPy:Python 的科学计算库,可以处理积分微分、线性代数、最优化问题、信号处理、统计学等问题。
- Scikit-learn:Python 的机器学习工具包,可以处理机器学习的众多计算问题,如支持向量机,聚类,特征选择等问题。
- Statmodels:处理统计学和经济学问题的库,与 scikit-learn 相比包含更多经典模型。
数据可视化:
- Matplotlib:最经典的二维数据可视化库,结合 NumPy 使用可以成为 Matlab 的有力替代品。
自动化办公:
docx:处理 Doc 文档。
pptx:处理 Powerpoint 幻灯片。
openpyxl:处理 Excel 表格。
NumPy 基础
NumPy 底层是用 C 写的,所以可以很方便的操作内存,这点与 Python 的原生数据结构不同。因此,NumPy 在处理大型数据时速度更快,内存开销也更小(和原生 Python 相比)。
ndarray 对象
ndarray 是 NumPy 的核心数据结构,全称是 N-dimensional Array,N 维数组,一个快速、灵活的大型数据集容器,内部储存同一类型的数据。
ndarray 有两个重要属性,dtype
和 shape
,分别表示元素的数据类型和形状参数,此外还有 ndim
(维数), size
(元素个数), itemsize
(每个元素字节数) 等属性。
arr.dtype
、np.dtype(arr)
:返回arr
的数据类型,常见类型如下:
类型 | 描述 |
---|---|
int16/uint16 | 有符号和无符号 16 位整数 |
int32/uint32 | 有符号和无符号 32 位整数 |
int64/uint64 | 有符号和无符号 64 位整数 |
float32 | 标准单精度浮点数,兼容 C 的 float |
float64 | 标准双精度浮点数,兼容 C 的 double |
float128 | 拓展精度浮点数 |
complex128 | 基于 float64 的复数 |
bool | 布尔值,为 True 或 False |
arr2 = arr1.astype(np.float64)
:改变原数据类型为目标类型。arr.shape
、np.shape(arr)
:返回arr
的形状参数,通常赋值给变量(m, n)
,不同的参数含义如下:
arr | arr.shape | arr.dtype | 含义 |
---|---|---|---|
array(1) | () | int32 | 单个整数 |
array(1.) | () | float64 | 单个浮点数 |
array([1, 2]) | (2,) | int32 | 一维数组,2 个元素 |
array([[1], [2]]) | (2, 1) | int32 | 二维数组,2 行 1 列 |
array([[1, 2]]) | (1, 2) | int32 | 二维数组,1 行 2 列 |
array([[1, 2], [3, 4]]) | (2, 2) | int32 | 二维数组,2 行 2 列 |
array([[[1, 1], [1, 1]], [[1, 1], [1, 1]]]) | (2, 2, 2) | int32 | 三维数组 |
arr.shape[0]
、arr.shape[1]
:返回arr
的第 0 维、第 1 维的长度。arr.size
:返回元素个数,通常赋值给变量m
,用于生成等长的数组。np.set_printoptions(formatter={'float': '{: 0.6f}'.format})
:设置全局精度。
ndarray 构造
根据不同的需求,有各种构造 ndarray 的方法。
- 已有容器转化数组
生成 ndarray 的基本函数是 np.array()
,其参数可以是 list 、tuple 或另一个 ndarray,会自动识别类型。其函数接口如下:
1 |
|
样例测试如下:
1 |
|
- 生成已初始化数组
NumPy 自带初始化函数,用来生成默认数组,以下为样例:
1 |
|
需要注意的是,传入的实参为生成数组的 shape
,必须用元组表示!且生成的数组默认类型都为 float64。
若要生成高维数组,除了用 np.array()
拼接两个低维数组,通常都是用 np.empty()
实现。
- 基于范围生成数组
生成方式与 Matlab 类似,这里给出函数接口:
1 |
|
- 生成随机数组
常用的有三种方式,这里给出函数接口:
1 |
|
- 从外部文件导入
实践最常用的方式是从外部文件导入大量数据集,Numpy 自身的文件后缀为 .npy
,专用于 ndarray 的读写:
1 |
|
此外,Numpy 还有对 .txt 类型文件的读写方法,这里列出样例:
1 |
|
当 .txt 文件有空缺数据时,需要使用复杂度更高的函数:
1 |
|
导入其他格式文件或许需要用到其他辅助库:
1 |
|
ndarray 索引
ndarray 容器的赋值主要用索引完成,其基本特性:浅拷贝,即多个对象共用一块内存。NumPy 官方解释是,NumPy 主要用于处理大量数据,所以不希望用缺省复制的方式,否则会引起各种内存问题。
索引语法和原生 Python 类似,用数字和冒号表示左闭右开范围:
1 |
|
对于高维数组,如果想通过赋值实现降维,可以使用切片索引:
1 |
|
由第一个例子注意到,不同于原生 Python,高维数组还可以通过 [x,y,z]
的方式在不同维度之间索引,同时还可以用冒号加数字表示左闭右开范围。
特别注意最后两个例子,在第二维上虽然范围都是 0,但前者只用了数字,后者还用了冒号,导致结果的维数并不相同。这说明:使用冒号不会降维,哪怕确实范围中只有一列。
此外,NumPy 中还有一种布尔索引的有趣用法,不是本文重点,这里举个例子:
1 |
|
ndarray 变形
ndarray 可通过一些自带的函数改变形状(维度),以便完成一些复杂的矩阵运算。
arr.flatten()
:返回一份深拷贝的展开成一维的数组,默认按行优先展开order='C'
,用于矩阵时默认展开到(n,1)
。arr[:,np.newaxis]
:将一维arr
增加到二维数组(m,1)
。常用于生成列向量,才能点乘系数矩阵。arr[np.newaxis,:]
:将一维arr
增加到二维数组(1,m)
。np.c_[arr1, arr2]
:将二维数组按列相连,要求行数一致。如果对象是一维数组,视作(m,1)
的二维数组。np.column_stack([arr1, arr2])
:类似np.c_
的函数形式,要求传入数组列表。np.hstack([arr1,arr2])
:类似np.c_
的函数形式,要求传入数组列表。
np.r_[arr1, arr2]
:将二维数组按行相连,要求列数一致。如果对象是一维数组或常数,则仍拼成一维数组。np.row_stack([arr1, arr2])
:类似np.r_
的函数形式,要求传入数组列表。np.vstack([arr1,arr2])
:类似np.c_
的函数形式,要求传入数组列表。
np.reshape(arr, (5,5))
:调整形状,默认按行展开后填到新形状,要求规模匹配。如果形状元组里某一维填-1
,则会根据其他维度计算该维长度。如果只有一个-1
,则效果同arr.flatten()
。np.flip(arr, axis=0)
:将arr
在按行切片,并把行次序颠倒。如果不加axis
则会在所有维度都进行颠倒,常用于卷积核的翻转。np.concatenate((a1,a2,...), axis=0)
:高效地拼接大规模数组,axis
表示拼接时切片的维度,对于一维数组默认拼完还是一维。np.pad(arr, width, mode=‘edge’)
:在arr
的每个轴边缘填充,填充的长度width
为嵌套元组((before_1, after_1), ..., (before_N, after_N))
,mode
为填充的值,可以为边缘值、最大值、平均值等。
常用数学函数
ndarray 的另一特点是支持类标量语法的计算,标量会作用在数组的每一个元素上,例如:arr + 2
、arr * 10
、arr[:] = 2
、arr > 5
等。此外,还支持一系列运算函数,以下所有函数同样支持 数组变量.函数名(参数)
形式调用:
普通运算
np.sum(arr[1,:])
:对一维数组arr[1,:]
,求和,得到一个值。np.sum(arr, 0)
:对二维数组arr
,求每一列和,塌缩成(列数,)
。np.sum(arr, 1)
:对二维数组arr
,求每一行和,塌缩成(行数,)
。np.max(arr)
:对整个数组,求最大值,得到一个值。np.max(arr, 0)
:对二维数组arr
,求每一列最大值,塌缩成(列数,)
。np.argmax(arr,0)
:对二维数组arr
,求每一列最大值的索引,塌缩成(列数,)
。np.sin(arr)
、np.cos(arr)
、np.tan(arr)
:作用于每个元素求三角函数。np.arcsin(arr)
、np.arccos(arr)
、np.arctan(arr)
:作用于每个元素求反三角函数。np.arctan2(y, x)
:(x, y)
为坐标系上的一点,该函数可求出该点与原点连线到 x 轴的夹角,范围为[-pi, pi]
,通常使用np.arctan2(y, x) * 180 / np.pi
来求出夹角。np.power(arr, n)
:作用于每个元素求n
次幂。np.square(arr)
:作用于每个元素求平方,更快。np.sqrt(arr)
:作用于每个元素开根号,更快。
矩阵运算
arr1 @ arr2
:矩阵点乘,要求第一个矩阵的列数等于第二个矩阵的行数,一维数组视作二维数组的行向量或列向量。np.dot(arr1, arr2)
:同上,矩阵点乘np.matmul(arr1, arr2)
:同上,矩阵点乘,支持高维矩阵的 Broadcast。
arr1 * arr2
:对应位置相乘,即 Hadmard 积,要求两个矩阵各个维度长度相等。np.multiply(arr1, arr2)
:同上,对应位置相乘。
arr1.T
:求数组转置,数组没有求逆、共轭的函数。np.transpose(arr1)
:同上,不指定参数时求二维数组的转置,高维数组需用元组指定索引,表示相应的索引顺序交换。
线性代数
np.linalg.inv(arr)
:求逆矩阵,要求二维数组。np.linalg.pinv(arr)
:求伪(广义)逆矩阵,要求二维数组。np.linalg.det(arr)
:求行列式(标量),要求二维数组。np.linalg.solve(A,b)
:解形如 Ax=b 线性方程组。w = np.linalg.eigvals(arr)
:求矩阵特征值,w
为 (m,) 一维数组,存放特征值。w,v = np.linalg.eig(arr)
:求矩阵特征值,v
为 (m,m) 二维数组,每一列都是特征向量,与w
对应。np.linalg.svd(arr)
:求矩阵的 SVD 分解,arr
为 (m,n) 二维数组,三个返回值:U
为 (m,m),Sigma
为(m,n),VT
为 (n,n)。np.linalg.norm(arr, axis=0, ord=1)
:按列求向量 1 范数。np.linalg.norm(arr, axis=0, ord=2)
:按列求向量 2 范数,默认选项。np.linalg.norm(arr, axis=0, ord=np.inf)
:按列求向量无穷范数。
统计运算
np.mean(arr)
:求所有样本平均值。np.mean(arr, 0)
:对二维数组arr
,求每一列样本平均值,塌缩成(列数,)
。np.average(arr, weights=None)
:求样本加权平均值。np.std(arr)
:求所有样本标准差。np.std(arr, ddof=1)
:求所有样本无偏标准差。np.std(arr, axis=0, ddof=1)
:对二维数组arr
,求每一列样本无偏标准差,塌缩成(列数,)
。np.var(arr)
:求所有样本方差,相当于标准差的平方。np.cov(arr)
:对一维数组arr
,相当于求自身的协方差(退化为无偏方差),即无偏标准差的平方。np.cov(arr)
:对二维数组arr
,返回一个二维数组,元素[i,j]
代表arr[i]
与arr[j]
两列元素的协方差。
NumPy 进阶
matrix 对象
除了 ndarray,NumPy 针对二维数组还专门设置了一个矩阵对象。matrix 的大部分性质与 ndarray 无异,但多了一些功能函数:
np.mat([[1,2],[5,7]])
:声明矩阵,注意维数必须为 2。np.mat([1,2,3])
:会被强制转化为(1,3)
的二维矩阵。np.mat(arr1)
、np.asmatricx(arr1)
:从数组转到矩阵。np.asarray(mat1)
:从矩阵转到数组。mat1.T
、mat1.H
、mat1.I
:求转置矩阵、共轭矩阵、逆矩阵。mat1 * mat2
:点乘,要求第一个矩阵的列数等于第二个矩阵的行数。np.multiply(mat1, mat2)
:对应位置相乘,要求两个矩阵各个维度长度相等。
Broadcast 广播机制
NumPy 中对于两个 ndarray 的加减乘除都是标量操作,即对应位置元素之间的操作。并且,当两个数组的形状不同时,NumPy 自带的 Broadcast 机制会自动扩展数组进行操作,这是 NumPy 向量化运算的基石。
例如,在归一化变量时,我们使用:
1 |
|
很明显,data
和塌缩后的 np.mean(data, 0)
形状不相同,但仍可以相减。
广播兼容的条件有两种:
- 两个不同维度的数组的后缘维度(trailing dimension,即从末尾开始算起的维度)的轴长度相符。如:
(4,3)
与(3,)
;(5,6,7)
与(6,7)
。 - 两个相同维度的数组,但其中在某一维度上仅为 1。如
(4,5,6)
与(4,1,6)
、(4,5,1)
。
爱因斯坦求和
爱因斯坦求和法 np.einsum()
函数是一个非常强大的工具,用于执行各种复杂张量运算,属于是矩阵乘法的泛化版。它可以在不创建中间数组的情况下,高效地执行多种线性代数、张量操作和张量缩并操作。参数如下:
1 |
|
subscripts
:爱因斯坦求和约定字符串,用于指定操作的具体形式。operands
:输入张量或数组。
在爱因斯坦求和法中,我们约定:
- 如果一个字母在两个输入中都出现了,那么就执行逐分量的乘法;
- 如果输出中不包含一个字母,则在该维度上执行求和。
例如,当我们想实现一个内积操作时,可以简化为:
1 |
|
对两个三维数组进行对应位置相乘:
1 |
|
执行张量缩并操作,例如计算矩阵的迹:
1 |
|
在 Transformer 中,最复杂的运算操作是自注意力层,其中除了 softmax 之外的一切都可以使用 einsum 表示:
1 |
|
C-API
NumPy 提供了一个 C-API,使用户能够扩展系统并快速访问数组对象,以便在其他例程中使用。真正理解 C-API 的最好方法是阅读源代码,这里仅列举部分神奇的用法。
np.argpartition(arr, n)
:高效地将数组中的所有元素分割,分割点为结果下标n
的元素,左侧是较小数,右侧是较大数。注意仅返回分割后数组的索引,常用于检索算法中输出 Top N 结果:1
2
3
4
5
6
7>>> a = np.array([9, 4, 4, 3, 3, 9, 0, 4, 6, 0])
>>> print(np.argpartition(a, -5))
[6 9 4 3 7 2 1 5 8 0] # 结果的索引
>>> a[np.argpartition(a, -5)]
array([0, 0, 3, 3, 4, 4, 4, 9, 6, 9]) # 真正的结果
>>> a[np.argpartition(a, -5)[-5:]]
array([4, 4, 9, 6, 9]) # Top 5np.where(condition)
:找出满足条件condition
元素的坐标,返回一个 tuple,每一项是一个一维 ndarray,分别对应符合条件元素的各维坐标。简单的条件用布尔索引也可替代,常用于绘制散点图:1
2
3
4
5pos = np.where(y == 1)[0]
neg = np.where(y == 0)[0] # 返回 label 为 0 和 1 的样本点的索引
plt.scatter(X[pos, 0], X[pos, 1], marker="o", c='c')
plt.scatter(X[neg, 0], X[neg, 1], marker="x", c='r') # 绘制散点图X, Y = np.meshgrid(x, y)
:生成格点矩阵,x
和y
需是一维向量,输出的是一个x * y
格点矩阵的每个点的横纵坐标,X
和Y
是形状为(x, y)
的二维矩阵。常用于绘制等高线图:1
2
3
4
5u = np.linspace(-1, 1, 50)
v = np.linspace(-1, 1, 50)
U, V = np.meshgrid(u, v) # 生成 50x50 网格点矩阵
z = (map_feature(U.flatten(), V.flatten()) @ theta).reshape((50, 50)) # 计算等高线
plt.contour(u, v, z, [0], colors='r') # 绘制轮廓图,取高度为 0 的点np.concatenate((np.ravel(a), np.ravel(b)))
:扁平化参数,其中ravel(a)
是将多维数组变成一维数组,concatenate((arr1, arr2, ...),axis=0)
是将一维数组首尾相接。有些 SciPy 中的科学计算方法(梯度下降)需要一次性将所有参数线性输入,就可以用到这个。np.cumsum(arr, axis=None)
:对一维、二维等数组求前缀和,如果不加axis
参数则默认按行展开为一维数组后求前缀和;如果为 0 ,按照行累加,为 axis=1,按照列累加。1
2
3>>> a = np.array([[1,2,3], [4,5,6]])
>>> np.cumsum(a)
array([ 1, 3, 6, 10, 15, 21]) # 展开成一维数组np.argsort(arr)
:返回的是元素值从小到大排序后的索引,内置快排实现,如果要从大到小,只需将参数改为-arr
,或者在后面跟上[::-1]
表示逆序。常用于求完特征值和特征向量后进行排序,提取主成分:1
2
3
4
5
6
7
8
9
10
11>>> a = np.array([3,1,2,1,3,5])
>>> np.argsort(a)
array([1, 3, 2, 0, 4, 5]) # 升序
>>> np.argsort(-a)
array([5, 4, 0, 2, 3, 1]) # 降序
# PCA z
w, v = np.linalg.eig(M)
sorted_indices = np.argsort(-w)
eigenvalues = w[sorted_indices[:k]]
eigenvectors = v[:, sorted_indices[:k]].astype(np.float64).Tgrid = np.indices((H, W))
:返回一个给定shape=(2,H,W)
的序号网格数组,其中grid[0]
里的每个元素为该位置元素的行序号,grid[1]
为列序号,可以用于局部提取数组元素:1
2
3
4
5
6
7
8
9>>> x = np.arange(20).reshape((5,4))
>>> grid = np.indices((2,3))
# [[[0 0 0],
# [1 1 1]],
# [[0 1 2],
# [0 1 2]]]
>>> x[grid[0], grid[1]]
# [[0 1 2]
# [4 5 6]]np.allclose(arr1, arr2)
:用于检查两个数组是否每个元素都相似,返回布尔值,默认在 1e-5 的相对误差范围内。np.nonzero(arr)
:用于得到 N 维数组中非零元素的位置(数组索引),返回一个N 元元组,每一个元素都为一个一维数组,tuple[i][j]
代表第j
个非零元素的第i
维下标:1
2
3
4
5
6
7a =[[1,2,3],
[0,2,0],
[0,2,2]]
>>> np.nonzero(a)
# (array([0, 0, 0, 1, 2, 2], dtype=int64), array([0, 1, 2, 1, 1, 2], dtype=int64))
>>> indices = np.stack(np.nonzero(a)).T
# array([[0,0], [0,1], [0,2], [1,1], [2,1], [2,2]])np.all()
、np.any()
:用于判断整个数组中的元素是否全部满足条件、是否存在满足条件。本质上是实现了与、或运算。具体使用如下:1
2
3
4
5
6a = [1, 2, 3]
b = [1, 2, 4]
>>> (a == b).all()
# False
>>> (a == b).any()
# Truenp.convolve(arr1, arr2)
:对两个一维数组的卷积运算(模拟信号操作),其中arr2
会先翻转再平移。此外还可以选择模式设置是否存在边际效应。注:该方法实现简易,没有用到 FFT 优化,速度较慢。1
2>>> np.convolve([1, 2, 3], [4, 5, 6])
# array([4, 13, 28, 27, 18])