Numpy是关于机器学习,数据处理的Python基础库,了解 NumPy 的工作原理可以提高你的编码效率,可以让你的代码看上去更加简洁和优雅。 NumPy 也可以GPU上运行代码而无需更改原始代码(或只做少量更改)。
NumPy 的核心概念是 多维数组。它的美妙之处在于,无论数组有多少维,大多数操作看起来都是一样的。但是 1D 和 2D 的情况有点特殊。文章由三部分组成:
粗看上去,NumPy 数组类似于 Python 列表。它们都用作容器,具有快速的原始获取和设置以及元素的插入和删除速度稍慢。
先看一个最简单的例子,Numpy能打败Python的list主要体现在算术计算上:
除此之外,NumPy 数组的优缺点主要体现在:
这里的大O表示运算的时间复杂度, O(N)表示运算的时间与数组的大小成正比。O*(1)表运算示时间一般不依赖于数组的大小。
创建 NumPy 数组的一种方法是通过将python的list 转换成numpy的数组:
NumPy 数组不能像 Python list 那样很方便的进行扩展(append):因为它在数组末尾没有保留空间以方便快速扩展。因此,通常的做法是先扩展 Python 列表然后将其转换为 NumPy 数组,或者使用 np.zeros,np.empty 预先分配必要的空间。
通常我们需要创建一个空数组来匹配现有数组的形状和元素类型 :
实际上,所有创建一个填充常量值的数组的函数都有_like
对应的函数:
NumPy 中创建序列数组初始化的函数有两个:
如果您需要一个外观相似的浮点数数组,例如[0., 1., 2.],您可以更改arange输出的类型:arange(3).astype(float),但还有更好的方法,由于arange函数是类型敏感的:如果您输入整数作为参数,它将生成整数,如果您输入浮点数(例如arange(3.)),它将生成浮点数。
但arange不是特别擅长处理浮点数:
np.arange(start,stop,step)通常情况下比较擅长处理整数,也可以处理浮动数,但是处理浮动数时可读性较差,因为当参数start,stop,step的小数位数不一致时,生产的结果有时难以理解(比如上图中的solution1的结果),因为浮点数没有连续的概念。 当需要生成浮点数序列数组时更为有效的方法是使用np.linspace(start,stop,num)方法,其中num必须是整数,np.linspace生成的浮点数序列并不是连续的,它只是在start,stop之间均匀的生成num个浮点数(包含start,stop在内)。即x∈[start,stop]。但是有一些numpy生产的浮动数序列并不是闭区间的,比如用numpy生成随机数序列:
在Numpy中还有一个用于生成随机数组的新接口。它的特点是:
当将数据放入数组后,NumPy 非常擅长提供简单的方法来返回部分或全部的结果:
上面介绍的多种通过多种花哨的索引来裁剪数组的方法,但是它们的结果实际上都是所谓的“视图(view)”:如果数据在被索引后发生变化,它们不会存储数据并反映原始数组中的变(赋值操作除外)。下面是对list和numpy数组进行切片和赋值后的结果差异的对比:
从 NumPy 数组获取数据的另一种非常有用的方法是布尔索引,它允许使用各种逻辑运算符:
numpy数组通过使用布尔索引的方式也可以对原数组进行赋值。为此衍生出来的两个常用的方法:np.where和np.clip
请注意,np.where使用一个参数返回一个数组元组(1D 情况下为 1 元组,2D 情况下为 2 元组等),因此您需要在上面的示例中写入np.where(a>5)[0]这种方式来返回你想要的数组。
向量化运算的速度是 Numpy最值得夸耀的地方。向量化运算达到了 c++ 运算的级别,使我们能够避免缓慢的 Python 循环的成本。NumPy 允许像普通数字一样操作整个数组:
和 Python 中一样,a//b 表示 整除(得到商),x**n 表示 xⁿ
当一个数组与一个数做加减乘除时,表示数组的每个元素都与这个数做相关的运算。(也称为广播):
大多数数学函数都有NumPy函数与之对应:
在numpy中向量的点积与叉积有自己运算符:
在numpy中也有三角函数运算符:
在numpy中还有多种不同的取整函数:
这里np.floor是向下取整,np.ceil是向上取整,而np.round 并非是“四舍五入”,而是“去奇存偶”,或者说 “4舍6入5凑偶”,与一般理解的四舍五入不同:当整数部分是偶数,小数部分是0.5时,向下取整,最后结果为偶数。当整数部分是奇数,小数部分是0.5时,则向上取整,最后结果为偶数。这样得到的结果在统计学上更精确。numpy的round函数采用的是这种方法。
numpy还有一些基本的统计函数:
这些函数中的每一个都有一个 nan-resistant 变体:例如nansum、nanmax等
从上面的公式可以看出,在总体均值未知时从样本估计 std 的最典型用例中,std
和都var
忽略贝塞尔校正并给出有偏差的结果。获得较少偏差估计的标准方法是n-1
在分母中使用ddof=1
('delta degrees of freedom'):
Pandas std默认使用贝塞尔校正
随着样本量的增加,贝塞尔校正的效果会迅速减弱。此外,它不是一个标准的通用解决方案,例如正态分布ddof=1.5
更好:
排序函数的功能少于 Python 对应函数:
在一维情况下,reversed
可以通过反转结果轻松补偿关键字的缺失。在 2D 中,它有点困难。
与Python的List不同,NumPy 数组没有index
方法。 所以查找元素是Numpy的一个痛点:
函数np.allclose(a, b)用于比较具有给定公差的浮点数组:
除此之外np.allclose在绝对和相对公差公式中还存在一些小问题,例如,对某些数存在allclose(a, b) != allclose(b, a)。这些问题已在math.isclose函数中得到解决。
NumPy 中曾经有一个专门的matrix类,但现在已弃用,因此我将交替使用矩阵和二维数组这两个词。
矩阵初始化语法类似于向量:
这里需要双括号,因为第二个位置参数是为(可选)dtype
(也接受整数)保留的。
随机矩阵生成也类似于向量:
还有一种更简洁的随机数生成方式:
二维索引语法比嵌套列表更方便:
和一维数组一样,上图的view表示,切片数组实际上并未进行任何复制。修改数组后,更改也将反映在切片中。
在许多操作(例如求和)中,我们需要告诉NumPy是否要跨行或跨列进行操作。为了使用任意维数的通用表示法,NumPy引入了axis的概念:axis参数实际上是所讨论索引的数量:第一个索引是axis=0,第二个索引是axis=1,等等。
因此在二维数组中,如果axis=0是按列,那么axis=1就是按行。
除了按元素工作的普通运算符(如 +、-、*、/、// 和 **)之外,还有一个计算矩阵乘积的 @ 运算符:
在第一部分中,我们已经看到向量乘积的运算,NumPy允许向量和矩阵之间,甚至两个向量之间进行元素的混合运算:
请注意,在最后一个示例中,它是一个对称的每元素乘法(笛卡尔积)。要使用非对称线性代数矩阵乘法计算外积,应颠倒操作数的顺序:
从上面的示例可以看出,在二维数组中,行向量和列向量被不同地对待。
默认情况下,一维数组在二维操作中被视为行向量。因此,将矩阵乘以行向量时,可以使用(n,)或(1,n),结果将相同。
如果需要列向量,则有转置方法对其进行操作:
能够从一维数组中生成二位数组列向量的两个操作是使用命令reshape重排和newaxis建立新索引:
这里的-1参数表示reshape自动计算第二个维度上的数组长度,None在方括号中充当np.newaxis的快捷方式,该快捷方式在指定位置添加了一个空axis。
因此,NumPy中总共有三种类型的向量:一维数组,二维行向量和二维列向量。这是两者之间显式转换的示意图:
flatten始终是一个副本,reshape (-1) 始终是一个视图,ravel尽可能是一个视图
根据规则,一维数组被隐式解释为二维行向量,因此通常不必在这两个数组之间进行转换,相应区域用灰色标出。
连接矩阵有两个主要函数:
这两个函数只堆叠矩阵或只堆叠向量时,都可以正常工作。但是当涉及一维数组与矩阵之间的混合堆叠时,vstack可以正常工作:hstack会出现尺寸不匹配错误。
因为如上所述,一维数组被解释为行向量,而不是列向量。解决方法是将其转换为列向量,或者使用column_stack自动执行:
堆叠的逆向操作是拆分:
所有split
风格都接受要拆分的索引列表作为参数,或单个整数作为参数,即大小相等的部分的数量:
矩阵可以通过两种方式完成复制:tile类似于复制粘贴,repeat类似于分页打印。
特定的列和行可以用delete进行删除:
逆运算是插入(insert)
:
append就像hstack一样,该函数无法自动转置一维数组,因此再次需要对向量进行转置或添加长度,或者使用column_stack代替:
实际上,如果您需要做的只是将常量值添加到数组的边界,那么(稍微有点复杂)pad
函数就足够了:
广播规会让使用 meshgrids 变得更简单。假设,您需要以下矩阵(但尺寸非常大):
两种明显的方法都很慢,因为它们使用 Python 循环。处理此类问题的MATLAB的方法是创建一个meshgrid:
该meshgrid
函数接受任意一组索引,该mgrid
只是切片并且,indices
只能生成完整的索引范围。fromfunction
如上所述,使用 I 和 J 参数调用提供的函数一次。
但实际上,在 NumPy 中有更好的方法。没有必要在整个 I 和 J 矩阵上花费内存(即使meshgrid
足够聪明,如果可能的话只存储对原始向量的引用)。只存储正确形状的向量就足够了,其余的由广播规则处理:
如果没有indexing=’ij’
参数,meshgrid
将更改参数的顺序:J, I= np.meshgrid(j, i)
—它是一种“xy”模式,可用于可视化 3D 绘图(请参阅文档中的示例)。
除了在二维或三维网格上初始化函数外,网格对于索引数组也很有用:
也适用于稀疏网格
就像 一样sum函数,所有其他统计函数 (min/max
, argmin/argmax
, mean/median/percentile
, std/var
)接受 axis 参数并相应地执行操作:
在二维及更高版本中的argmin
andargmax
函数有返回扁平化索引(最小值和最大值的第一个实例)的麻烦。要将其转换为两个坐标,unravel_index
需要一个函数 :
all和any两个函数也能使用axis参数:
尽管axis参数对上面列出的函数有帮助,但对 二维 排序没有帮助
这通常不是您希望对矩阵或电子表格进行排序的结果:axis
它根本不是key
参数的替代品。但幸运的是,NumPy 有几个辅助函数,允许按列排序——如果需要,也可以按多列排序:
1. a[a[:,0].argsort()]
按第一列对数组排序:
这里argsort
返回排序后原始数组的索引数组。
可以重复这个技巧,但必须注意下一次排序不会弄乱上一次的结果:a = a[a[:,2].argsort()]
a = a[a[:,1].argsort(kind='stable')]
a = a[a[:,0].argsort(kind='stable')]
2. 有一个辅助函数lexsort
可以按照上面描述的方式对所有可用列进行排序,但它总是按行执行,并且要排序的行的顺序是倒置的(即从下到上)所以它的用法有点麻烦,例如:
–a[np.lexsort(np.rot90(a))]
按从左到右的顺序按所有列排序。
–a[np.lexsort(np.rot90(a[:,[2,5]]))]
首先按第 2 列排序,然后(第 2 列中的值相等)按第 5 列排序。另一种形式是a[np.lexsort(np.flipud(a.T[[2,5]]))]
这里 flipud
在上下方向上翻转矩阵(准确地说,在axis=0
方向上,与 相同a[::-1,...]
,其中三个点表示“所有其他维度”——所以突然flipud
,而不是fliplr
,翻转一维数组)并rot90
旋转矩阵在正方向(即逆时针)旋转 90 度。flips 和 rot90 都返回视图。
3. 要使用 的order
参数sort
,首先将数组转换为结构化形式,然后对其进行排序,最后将其转换回普通(“非结构化”)形式:
这两种转换实际上都是视图,因此它们速度很快并且不需要任何额外的内存。但是功能u2s
和s2u
需要首先导入:
from numpy.lib.recfunctions
import unstructured_to_structured as u2s, structured_to_unstructured as s2u
4. 在 Pandas 中执行此操作可能是更好的选择,因为此特定操作在那里更具可读性且更不容易出错:
sort_values
(by=[2,5]).to_numpy():先
按第 2 列排序,然后按第 5 列排序。 通过重排一维向量或转换嵌套的Python列表来创建3维数组时,索引的含义为(z,y,x)。第一个索引是平面的编号,然后是该平面中的坐标:
但是此索引顺序不是通用的。处理RGB图像时,通常使用(y,x,z)顺序:前两个是像素坐标,最后一个是颜色坐标(Matplotlib中是RGB ,OpenCV中是BGR ):
这样,可以方便地引用特定像素:a[i,j]给出像素的RGB元组(i,j)。
因此,创建特定几何形状的实际命令取决于您正在处理的领域的约定:
显然,像 hstack
、vstack、dstack
这样的 NumPy 函数并不知道这些约定。其中硬编码的索引顺序是 (y,x,z),RGB 图像顺序:
堆叠RGB图像(这里只有两种颜色)
如果您的数据布局不同,使用concatenate
命令堆叠图像,在参数中为其提供显式索引号axis
会更方便:
如果您不方便用数轴(axis)来思考问题,可以将数组转换硬编码为hstack的形式:
这种转换没有发生实际的复制操作。它只是混合索引的顺序。
另一个混合索引顺序的操作是数组转置。检查它可能会让您对 3维数组更加熟悉。根据您决定的轴(axis)顺序,转置数组所有平面的实际命令将有所不同:对于通用数组,它交换索引 1 和 2,对于RGB图像,它交换0和1:
有趣的是,默认transpose
的轴参数axes
(以及唯一的a.T
操作模式)颠倒了索引顺序,这与上面描述的索引顺序约定都不一致。
广播也适用于更高维度,有关详细信息,请参阅我的简短文章“ NumPy 中的广播”。
最后,这里有一个函数可以在处理多维数组时为您节省大量的Python 循环,并可以使您的代码更简洁 — einsum
(爱因斯坦求和):
它沿着重复索引对数组求和。在这个特定的例子中 np.tensordot(a, b, axis=1),这两种情况都足够了,但在更复杂的情况下 einsum
可能会工作得更快,而且通常更容易编写和阅读——一旦你理解了它背后的逻辑。
如果您想测试您的 NumPy 技能,GitHub 上有一套棘手的众包100 个 NumPy 练习¹⁰。
本文来自于Medium的一篇国外博主的博客文章 作者:Lev Maximov
原文地址:https://betterprogramming.pub/numpy-illustrated-the-visual-guide-to-numpy-3b1d4976de1d
\