NumPy视觉指南
从头开始学习NumPy
> Image credit: Author
NumPy是一个基本库,受(PyTorch)的启发,大多数广泛使用的Python数据处理库都是基于(pandas)构建的,或者可以与(TensorFlow,Keras等)有效地共享数据。了解NumPy的工作方式也可以提高您在这些库中的技能。也可以在GPU¹上不做任何改动或进行最少改动即可运行NumPy代码。
NumPy的中心概念是n维数组。这样做的好处是,不管数组有多少维,大多数操作看起来都是一样的。但是一维和二维情况有些特殊。本文包括三个部分:
· 向量,一维数组
· 矩阵,二维数组
· 3D以上
我从JayAlammar²撰写了一篇很棒的文章' NumPy的视觉介绍'作为起点,大大扩展了其覆盖范围,并修改了一些细微差别。
Numpy数组与Python列表
乍一看,NumPy数组类似于Python列表。它们都可以用作容器,具有快速获取和设置项目以及较慢地插入和移除元素的功能。
NumPy数组拍子列表的简化示例是算术运算:
除此之外,NumPy数组是:
· 更紧凑,尤其是在一个以上的维度上
· 可以向量化操作时比列表快
· 将元素追加到末尾比列表慢
· 通常是同质的:只能快速处理一种类型的元素
这里的O(N)表示完成操作所需的时间与数组的大小成正比(请参见Big-O备忘单³网站),以及O *(1)(即所谓的'摊销'的O(1)))表示时间通常不取决于数组的大小(请参见Python时间复杂度wiki页面)
1.向量,一维数组
向量初始化
创建NumPy数组的一种方法是转换Python列表。该类型将从列表元素类型中自动推导:
确保输入同质的列表,否则您将得到dtype ='object',它会消除速度,只保留NumPy中包含的语法糖。
NumPy数组无法像Python列表那样增长:在数组末尾没有保留空间以方便快速追加。因此,通常的做法是增长一个Python列表,并在准备就绪时将其转换为NumPy数组,或者使用np.zeros或np.empty预先分配必要的空间:
通常需要创建一个空数组,以形状和元素类型匹配现有数组:
实际上,所有创建用常量值填充的数组的函数都具有_like对应项:
NumPy中有多达两个函数用于具有单调序列的数组初始化:
如果您需要类似[0.,1.,2.]的浮点数组,则可以更改arange输出的类型:arange(3).astype(float),但是有更好的方法。arange函数对类型敏感:如果将int作为参数输入,它将生成int,并且如果输入浮点数(例如arange(3。)),则将生成浮点数。
但是arange在处理浮点数方面并不是特别擅长:
这个0.1对我们来说似乎是一个有限的十进制数,但对计算机而言却不是:二进制,它是一个无穷小数,必须四舍五入到一个错误的地方。这就是为什么将小数部分加到范围的步通常不是一个好主意:您可能会遇到一个错误的错误。您可以使间隔的末尾落入非整数步数(解决方案1),但这会降低可读性和可维护性。这是linspace派上用场的地方。它不受舍入错误的影响,并始终生成您要求的元素数量。但是,linspace有一个常见的陷阱。它计算点,而不是间隔,因此最后一个参数始终是您通常会想到的加一个。因此它是11,而不是上面示例中的10。
为了进行测试,通常需要生成随机数组:
向量索引
一旦将数据存储在数组中,NumPy便会出色地提供简单的方法来将其取回:
除花式索引外,以上介绍的所有索引方法实际上都是所谓的'视图':它们不存储数据,并且如果原始数组在被索引后发生更改,则不会反映原始数组中的更改。
所有这些方法,包括花式索引,都是可变的:如上所述,它们允许通过分配来修改原始数组的内容。此功能通过切片来打破复制数组的习惯:
从NumPy数组中获取数据的另一种超级有用的方法是布尔索引,它允许使用各种逻辑运算符:
> any and all act just like their python peers, but don't short-circuit
小心点;3 <= a <= 5之类的Python'三元'比较在这里不起作用。
如上所述,布尔索引也是可写的。它有两个常见的用例,它们是专用功能:过度重载的np.where函数(请参阅下面的两种含义)和np.clip。
向量运算
算术是NumPy速度最耀眼的地方之一。向量运算符已转移到c ++级别,使我们避免了慢Python循环的代价。NumPy允许像普通数字一样操作整个数组:
As usual in Python, a//b means a div b (quotient from division), x**n means xⁿ
将加法或减法将int提升为浮点数的方式相同,将标量提升(也称为广播)至数组:
大多数数学函数都有NumPy对应项,可以处理矢量:
标量产品具有自己的运算符:
您也不需要三角函数的循环:
数组可以四舍五入为一个整体:
> floor rounds to -∞, ceil to +∞ and around — to the nearest integer (.5 to even)
名称np.around只是引入的np.round的别名,以避免从numpy import *写入时遮盖Python的回合(而不是更常见的导入numpy作为np)。您也可以使用a.round()。
NumPy还可以执行以下基本统计信息:
> Each of these functions has a nan-resistant variant: eg nansum, nanmax, etc
排序功能比Python对应功能具有更少的功能:
在一维情况下,可以通过反转结果来轻松地补偿反向关键字的缺失。在2D中,它有些棘手(功能要求⁵)。
搜索向量中的元素
与Python列表相反,NumPy数组没有索引方法。相应的功能请求⁶已经悬挂了一段时间。
> The square brackets in the definition of index() mean that either j or both i and j can be omitted
· 查找元素的一种方法是np.where(a == x)[0] [0],它既不优雅也不快速,因为它需要遍历数组的所有元素,即使要查找的项位于开始。
· 一种更快的方法是使用Numba⁷加速next((i的i [0],np中的v.ndenumerate(a),如果v == x),-1)(否则,在最坏的情况下,它的速度比)。
· 一旦对数组进行排序,情况就会变得更好:v = np.searchsorted(a,x);如果a [v] == x,则返回v,否则-1的复杂度为O(log N)确实非常快,但首先需要O(N log N)的时间才能排序。
实际上,通过在C中实现搜索来加速搜索不是问题。问题是浮点比较。这是一项对于任意数据开箱即用的工作。
比较浮点数
函数np.allclose(a,b)比较具有给定公差的浮点数组
> There is no silver bullet!
· np.allclose假定所有比较数字的典型比例为1。例如,如果使用纳秒,则需要将默认atol参数值除以1e9:np.allclose(1e-9,2e-9,atol = 1e-17)== False。
· math.isclose不对要比较的数字做任何假设,而是依靠用户提供一个合理的abs_tol值(采用默认的np.allclose atol值1e-8足以满足典型小数位数为1的数字):math.isclose(0.1 + 0.2–0.3,abs_tol = 1e-8)==真。
除此之外,np.allclose在绝对和相对公差的公式中还有一些小问题,例如,对于某些a,b allclose(a,b)!= allclose(b,a)。这些问题已在(标量)函数math.isclose中解决(稍后介绍)。要了解更多信息,请查看GitHub上出色的浮点指南⁸和相应的NumPy问题⁹。
2.矩阵,二维数组
NumPy中曾经有一个专用的矩阵类,但现在已弃用,因此我将交替使用矩阵和2D数组一词。
矩阵初始化语法与向量相似:
这里需要双括号,因为第二个位置参数是为(可选)dtype(它也接受整数)保留的。
随机矩阵的生成也类似于矢量的生成:
二维索引语法比嵌套列表更方便:
'视图'符号表示切片数组时实际上并未进行任何复制。修改数组后,更改也将反映在切片中。
轴参数
在许多操作(例如,求和)中,您需要告诉NumPy是否要跨行或跨列进行操作。为了具有适用于任意数量维的通用表示法,NumPy引入了axis的概念:axis参数的值实际上是所讨论的索引的数量:第一个索引是axis = 0,第二个是axis = 1,依此类推。因此在2D中,轴= 0是列方向,轴= 1是指行方向。
矩阵算术
除了普通的运算符(例如+,-,*,/,//和**)可以逐个元素地工作之外,还有一个@运算符可计算矩阵乘积:
作为在第一部分中已经看到的从标量广播的概括,NumPy允许向量和矩阵之间,甚至两个向量之间的混合运算:
> Broadcasting in 2D
行向量和列向量
从上面的示例可以看出,在2D上下文中,行向量和列向量被不同地对待。这与通常的NumPy做法相反,后者通常会尽可能使用一种类型的1D数组(例如a [:,j]-2D数组a的第j列是1D数组)。默认情况下,一维数组在2D操作中被视为行向量,因此,将矩阵乘以行向量时,可以使用形状(n,)或(1,n)-结果将相同。如果您需要列向量,则有几种方法可以从一维数组中进行处理,但是令人惊讶的是,转置不是其中一种:
能够从1D数组中生成2D列向量的两个操作是使用newaxis重新塑形和索引:
这里的-1参数告诉reshape自动计算尺寸尺寸之一,方括号中的None用作np.newaxis的快捷方式,它将在指定位置添加一个空轴。
因此,NumPy中共有三种类型的向量:1D数组,2D行向量和2D列向量。这是两者之间显式转换的示意图:
根据广播规则,一维数组被隐式解释为二维行向量,因此通常不必在这两个数组之间进行转换-因此,相应区域被阴影化。
矩阵操作
连接数组有两个主要功能:
这两个仅适用于堆叠矩阵或仅适用于向量,但在将一维数组和矩阵进行混合堆叠时,只有vstack可以按预期工作:hstack生成维度不匹配错误,因为如上所述,一维数组被解释作为行向量,而不是列向量。解决方法是将其转换为行向量,或使用专门的column_stack函数自动执行此操作:
堆叠的逆向分裂:
可以通过两种方式完成矩阵复制:拼贴的行为类似于复制粘贴,重复的行为类似于分页打印:
特定的列和行可以像这样删除:
相反的操作是insert:
像hstack一样,append函数无法自动转置1D数组,因此,再次需要重新调整向量的形状或添加维,或者需要使用column_stack来代替:
实际上,如果您需要做的就是向数组的边界添加常量值,那么(稍微有点复杂)的pad函数就足够了:
网状网格
广播规则使使用网格网格的工作更加简单。假设您需要以下矩阵(但尺寸很大):
两种明显的方法很慢,因为它们使用Python循环。MATLAB处理此类问题的方法是创建一个网格:
meshgrid函数接受任意一组索引mgrid-仅切片和索引只能生成完整的索引范围。如上所述,fromfunction仅使用I和J参数调用提供的函数一次。
但是实际上,在NumPy中有一种更好的方法。无需在整个I和J矩阵上花费内存(即使meshgrid足够聪明,仅在可能的情况下仅存储对原始向量的引用)。仅存储正确形状的矢量就足够了,广播规则将处理其余的内容:
如果没有indexing ='ij'参数,则meshgrid将更改参数的顺序:J,I = np.meshgrid(j,i)-这是一种' xy'模式,可用于可视化3D绘图(请参见docs)。
除了在二维或三维网格上初始化函数外,网格还可以用于索引数组:
> Works with sparse meshgrids, too
矩阵统计
就像sum一样,所有其他统计函数(min / max,argmin / argmax,mean / median / percentile,std / var)都接受axis参数并相应地起作用:
np.amin is an alias of np.min to avoid shadowing the Python min when you write 'from numpy import *'
2D及更高版本中的argmin和argmax函数令人讨厌返回平坦索引(最小和最大值的第一个实例)。要将其转换为两个坐标,需要一个unravel_index函数:
量词all和any也都知道axis参数:
矩阵排序
尽管axis参数对于上面列出的函数很有用,但对2D排序却没有帮助:
排序矩阵或电子表格通常不是您所希望的:轴绝不能替代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.flipud(a [2,5] .T))]首先按第2列排序,然后(按第2列的值相等)按第5列排序。np.lexsort(np.flipud(aT))]按从左到右的顺序对所有列进行排序。
在这里,flipud沿上下方向翻转矩阵(准确地说,沿axis = 0方向,与a [::-1,…]相同,其中三个点表示'所有其他维度'),因此突然翻转(而不是fliplr)翻转1D数组)。
3.还有一个order参数可以排序,但是如果从普通(非结构化)数组开始,则既不快速也不容易使用。
4.在熊猫中执行此操作可能是一个更好的选择,因为该特定操作在此处更具可读性且不易出错:– pd.DataFrame(a).sort_values(by = [2,5])。to_numpy()按第2列,然后按第5列排序。– pd.DataFrame(a).sort_values()。to_numpy()按从左到右的顺序按所有列排序。
3. 3D及以上
通过重塑1D向量或转换嵌套的Python列表来创建3D数组时,索引的含义为(z,y,x)。第一个索引是平面的编号,然后坐标在该平面上移动:
此索引顺序很方便,例如用于保留一堆灰度图像:a [i]是引用第i个图像的快捷方式。
但是此索引顺序不是通用的。使用RGB图像时,通常使用(y,x,z)顺序:第一个是两个像素坐标,最后一个是颜色坐标(Matplotlib中的RGB,OpenCV中的BGR):
这样可以方便地引用特定像素:a [i,j]给出(i,j)像素的RGB元组。
因此,创建某种几何形状的实际命令取决于您正在使用的域的约定:
显然,诸如hstack,vstack或dstack之类的NumPy函数并不了解这些约定。其中硬编码的索引顺序是(y,x,z),RGB图像顺序:
> Stacking RGB images (only two colors here)
如果您的数据布局不同,则使用concatenate命令堆叠图像,并在axis参数中为其提供显式索引号,将更加方便:
> Stacking generic 3D arrays
如果您不方便考虑轴数,可以将数组转换为硬编码为hstack和co的形式:
这种转换是便宜的:没有实际的复制发生。它只是动态混合索引的顺序。
混合索引顺序的另一个操作是数组转置。检查它可能会让您对3D阵列更加熟悉。根据您决定的轴顺序,转置数组所有平面的实际命令会有所不同:对于通用数组,它交换索引1和2,对于RGB图像,它交换0和1:
有趣的是,用于转置的默认轴参数(以及唯一的a.T操作模式)会颠倒索引顺序,这与上述两个索引顺序约定都不相符。
最后,这是一个函数,可以在处理多维数组时为您节省很多Python循环,并使您的代码更简洁— einsum(爱因斯坦求和):
它将沿重复索引的数组求和。在此特定示例中,np.tensordot(a,b,axis = 1)在两种情况下都足够,但是在更复杂的情况下,einsum可能工作得更快,并且通常更容易读写,只要您了解其背后的逻辑即可。
如果您想测试自己的NumPy技能,可以在GitHub上进行一组棘手的众筹,其中包括100个NumPy练习。
如果我错过了您最喜欢的NumPy功能,请告诉我,我将尽力配合使用!
(本文由闻数起舞翻译自Lev Maximov的文章《NumPy Illustrated: The Visual Guide to NumPy》,转载请注明出处,原文链接:https://medium.com/better-programming/numpy-illustrated-the-visual-guide-to-numpy-3b1d4976de1d)