纽木内构件

numpy数组的内部组织

它有助于了解如何在封面下处理numpy数组,以帮助更好地了解numpy。这一部分不会详细介绍。那些希望了解全部细节的人可以参考特拉维斯·奥列芬特的书《 NumPy 指南》。

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

  1. 基本数据元素的字节大小

  2. 数据缓冲区中数据的开始(相对于数据缓冲区开始的偏移量)。

  3. 尺寸的数量和每个尺寸的尺寸

  4. 每个维度元素之间的分离(“跨步”)。这不必是元素大小的倍数

  5. 数据的字节顺序(可能不是本机字节顺序)

  6. 缓冲区是否为只读

  7. 有关基本数据元素解释的信息(通过dtype对象)。基本数据元素可以像int或float一样简单,也可以是复合对象(例如,类似结构)、固定字符字段或python对象指针。

  8. 数组是解释为C顺序还是Fortran顺序。

这种安排允许非常灵活地使用数组。它允许的一件事是简单地更改元数据以更改数组缓冲区的解释。更改数组的字节顺序是一个简单的更改,不需要重新排列数据。数组的形状可以很容易地更改,而不需要更改数据缓冲区中的任何内容或任何数据复制。

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

通常,这些新版本的数组元数据(但相同的数据缓冲区)是数据缓冲区中的新“视图”。有一个不同的ndarray对象,但它使用相同的数据缓冲区。这就是为什么如果要对数据缓冲区进行新的、独立的拷贝,就必须使用.copy()方法强制拷贝。

数组中的新视图意味着数据缓冲区的对象引用计数增加。如果原始数组对象的其他视图仍然存在,那么简单地去掉它不会删除数据缓冲区。

多维数组索引顺序问题

索引多维数组的正确方法是什么?在您得出关于多维数组索引的唯一且正确的方法的结论之前,了解这是一个令人困惑的问题的原因是值得的。本节将详细解释numpy索引的工作原理,以及为什么我们采用我们对图像所做的约定,以及何时采用其他约定可能是合适的。

首先要理解的是,索引二维数组有两个相互冲突的约定。矩阵表示法使用第一个索引来指示选择哪一行,第二个索引指示选择哪一列。这与面向几何的图像惯例相反,人们通常认为第一个索引表示X位置(即列),第二个索引表示Y位置(即行)。这本身就是很多混乱的根源;面向矩阵的用户和面向图像的用户期望在索引方面有两种不同的东西。

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

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

最后,您对Fortran或C所做的操作取决于哪个更重要,而不是重新排序数据或保留索引约定。对于大型图像,重新排序数据可能很昂贵,而且索引约定经常被颠倒以避免这种情况。

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

如果这是真的,为什么不选择与您最期望的匹配的索引顺序呢?尤其是,为什么不定义行顺序图像来使用图像约定?(这有时被称为Fortran约定与C约定,因此在numpy中用于数组排序的“c”和“fortran”顺序选项)这样做的缺点是潜在的性能损失。按顺序访问数据是很常见的,要么隐式地在数组操作中访问,要么显式地循环访问图像的行。完成后,将以非最佳顺序访问数据。当第一个索引递增时,实际发生的情况是,在内存中相隔很远的元素被顺序访问,通常内存访问速度很差。例如,对于二维图像“im”,定义为im [0, 10] 表示x=0,y=10时的值。为了与通常的python行为保持一致,那么im [0] 表示x=0的列。然而,由于数据是按行顺序存储的,所以这些数据将分布在整个数组中。尽管numpy的索引具有灵活性,但它不能真正掩盖这样一个事实:由于数据顺序,基本操作变得效率低下,或者获取连续子数组仍然很难(例如,im [:,0] 第一排,vs im [0] 因此,不能使用诸如for row in im之类的成语;因为im中的col可以工作,但不会生成连续的列数据。

事实证明,numpy在处理ufunc时非常聪明,可以确定哪个索引是内存中变化最快的索引,并将其用于最内部的循环。因此,对于UFUNC来说,在大多数情况下,这两种方法都没有很大的内在优势。另一方面,将.flat与Fortran有序数组一起使用将导致非最佳内存访问,因为扁平数组中的相邻元素(实际上是迭代器)在内存中不是连续的。

事实上,列表和其他序列上的python索引自然会导致从外部到内部的排序(第一个索引得到最大的分组,第二个索引得到下一个最大的,最后一个索引得到最小的元素)。因为图像数据通常是按行存储的,所以这与被索引的最后一个项目所在的行中的位置相对应。

如果您确实想使用Fortran排序,请认识到有两种方法可以考虑:1)接受第一个索引并不是内存中变化最快的索引,并让您的所有I/O例程在从内存到磁盘或反之亦然时对数据重新排序,或者使用Numpy的机制将第一个索引映射到变化最快的数据。如果可能的话,我们推荐前者。后者的缺点是,numpy的许多函数将在不使用fortran排序的情况下生成数组,除非您小心使用“order”关键字。这样做会非常不方便。

否则,我们建议在访问数组元素时简单地学习颠倒索引的常规顺序。当然,它违背了粒度,但它更符合Python语义和数据的自然顺序。