NumPy internals

Internal organization of numpy arrays

它有助于理解一点关于如何处理numpy数组在封面下有助于更好地理解numpy。本节不会详细介绍。那些希望了解完整细节的人可参考Travis Oliphant的“Numpy指南”。

Numpy数组由两个主要组件组成,原始数组数据(从现在开始,称为数据缓冲区)和有关原始数组数据的信息。数据缓冲区通常是人们认为的C或Fortran中的数组,一个包含固定大小数据项的连续(和固定)内存块。Numpy还包含一组重要的数据,描述如何解释数据缓冲区中的数据。此额外信息包含(除其他外):

  1. 基本数据元素的大小(以字节为单位)
  2. 数据缓冲区中的数据的开始(相对于数据缓冲区的开始的偏移)。
  3. 维度的数量和每个维度的大小
  4. 每个维度的元素之间的距离(“stride”)。这不必是元素大小的倍数
  5. 数据的字节顺序(可能不是原生字节顺序)
  6. 缓冲区是否为只读
  7. 关于基本数据元素的解释的信息(通过dtype对象)。基本数据元素可以像int或float一样简单,或者它可以是复合对象(例如,结构体),固定字符字段或Python对象指针。
  8. 数组是否被解释为C阶或Fortran阶。

这种安排允许非常灵活地使用数组。它允许的一个事情是元数据的简单更改,以更改数组缓冲区的解释。更改数组的字节顺序是一个简单的更改,不涉及重新排列数据。数组的形状可以非常容易地改变,而不改变数据缓冲器中的任何东西或根本不改变任何数据复制

除此之外,使得可能的是可以创建新的数组元数据对象,其使用相同的数据缓冲器来创建该数据缓冲器的新视图,其具有缓冲器的不同解释(例如,不同的形状,偏移,字节顺序,步幅等),但共享相同的数据字节。numpy中的许多操作只是这样,如切片。其他操作(如转置)不会在数组中移动数据元素,而是更改有关形状和步长的信息,以便数组的索引更改,但数据中的数据不会移动。

通常,这些新版本的数组元数据,但相同的数据缓冲区是新的“视图”到数据缓冲区。有一个不同的ndarray对象,但它使用相同的数据缓冲区。这就是为什么有必要通过使用.copy()方法强制复制,如果真的想要一个新的和独立的数据缓冲区的副本。

将新视图转换为数组意味着数据缓冲区的对象引用计数增加。只要取消原来的数组对象,就不会删除数据缓冲区,如果它的其他视图仍然存在。

Multidimensional Array Indexing Order Issues

什么是索引多维数组的正确方法?在你跳到结束关于一个和真正的方式索引多维数组之前,它理解为什么这是一个混乱的问题。本节将尝试详细解释numpy索引如何工作,以及为什么我们采用我们为图像做的约定,以及何时采用其他约定。

首先要理解的是,有两种冲突的约定用于索引2维数组。矩阵符号使用第一个索引来指示正在选择哪一行,第二个索引用于指示选择哪个列。这与图像的几何定向约定相反,其中人们通常认为第一索引表示x位置(即,列),第二索引表示y位置(即,行)。这只是造成混乱的根源;面向矩阵的用户和面向图像的用户期望有关索引的两个不同的事情。

第二个要理解的问题是索引如何对应于数组存储在内存中的顺序。在Fortran中,当移动通过存储在存储器中的二维数组的元素时,第一索引是最快变化的索引。如果你采用矩阵约定索引,那么这意味着矩阵一次存储一列(因为第一个索引移动到下一行,因为它改变)。因此,Fortran被认为是一个列主语言。C只是相反的约定。在C中,最后的索引随着存储在存储器中的数组移动而变化最快。因此C是行主语言。矩阵按行存储。注意,在这两种情况下,它假定正在使用用于索引的矩阵约定,即对于Fortran和C,第一索引是行。注意这个约定意味着索引约定是不变的,并且数据顺序改变以保持这样。

但这不是看它的唯一方法。假设有一个大的二维数组(图像或矩阵)存储在数据文件中。假设数据是按行存储的,而不是按列存储的。如果我们要保留索引约定(无论是矩阵还是图像),这意味着根据我们使用的语言,如果数据被读入内存,我们可能被迫重新排序,以保留索引约定。例如,如果我们在没有重新排序的情况下将行顺序数据读入内存,它将匹配C的矩阵索引约定,但不匹配Fortran。相反,它将匹配Fortran的映像索引约定,但不匹配C.对于C,如果使用以行顺序存储的数据,并且想要保留图像索引约定,则在读取存储器时必须重新排序数据。

最后,Fortran或C所依赖的哪个更重要,不重新排序数据或保留索引约定。对于大图像,重新排序数据可能是昂贵的,并且通常将索引约定反转以避免这种情况。

numpy的情况使这个问题更复杂。numpy数组的内部机制足够灵活,可以接受任何索引排序。可以简单地通过操作数组的内部步长信息来重新排序索引,而不需要重新排序数据。Numpy将知道如何将新的索引顺序映射到数据而不移动数据。

所以如果这是真的,为什么不选择匹配你最期望的索引顺序?特别是,为什么不定义行有序图像使用图像约定?(这有时被称为Fortran约定与C约定,因此是num组中数组排序的'C'和'FORTRAN'顺序选项。)这样做的缺点是潜在的性能损失。通常在数组操作中隐式地访问数据,或者通过循环遍历图像的行来显式地访问数据。当完成时,将以非最佳顺序访问数据。随着第一索引递增,实际发生的是在存储器中间隔开的元件被顺序地访问,通常存储器访问速度较差。例如,对于定义为使得im [0,10]表示x = 0,y = 10处的值的二维图像'im'。为了与通常的Python行为一致,那么im [0]将表示x = 0处的列。然而,该数据将被分布在整个数组上,因为数据以行顺序存储。尽管numpy的索引的灵活性,它不能真正地考虑事实基本操作被渲染为低效,因为数据顺序或获取连续子阵列仍然尴尬(例如,im [:,0]为第一行,vs im [ 0]),因此不能使用成语,如im中的row; for col in im does work,but does not yield continuous column data。

事实证明,numpy是聪明的,当处理ufuncs确定哪个索引是最快速变化的一个在内存中,并将其用于最内层循环。因此,对于ufuncs,在大多数情况下,任何一种方法都没有大的内在优势。另一方面,使用带有FORTRAN有序数组的.flat将导致非最优的存储器访问,因为扁平数组(迭代器,实际上)中的相邻元素在存储器中不连续。

事实上,列表和其他序列上的Python索引自然导致一个从外到内的顺序(第一个索引获得最大的分组,下一个最大,最后一个获得最小的元素)。由于图像数据通常按行存储,这对应于作为索引的最后一个项目的行内的位置。

如果你想使用Fortran顺序意识到有两种方法要考虑:1)接受第一个索引只是不是在内存中最快速变化,并且所有的I / O例程在从内存到磁盘时重新排序数据或者反之亦然,或者使用numpy机制将第一索引映射到最快变化的数据。如果可能,我们建议前者。后者的缺点是,许多numpy的函数将产生数组没有Fortran排序,除非你仔细使用'order'关键字。这样做会非常不方便。

否则,我们建议在访问数组的元素时,简单地学习反转索引的通常顺序。授予,它反对谷物,但它更符合Python语义和数据的自然顺序。