strides 如何帮助在 numpy 中遍历数组?

Posted

技术标签:

【中文标题】strides 如何帮助在 numpy 中遍历数组?【英文标题】:How strides help in traversing an array in numpy? 【发布时间】:2022-01-18 19:52:32 【问题描述】:
arr = np.arange(16).reshape((2, 2, 4))

arr.strides
(32, 16, 4)

所以,据我所知,我相信在记忆中它会类似于下图。 步幅与轴一起标记(在箭头上)。

这就是我认为在使用命令转置其中一个轴之后的结果:

arr.transpose((1, 0, 2))

我知道内存块没有变化,但我无法理解跨步究竟如何帮助遍历内存块中的数组以生成预期的数组。 (是不是倒序遍历不同轴的元素?)

[[[ 0  1  2  3]
[ 4  5  6  7]] 
[[ 8  9 10 11]
[12 13 14 15]]]

我尝试通过 C 中的官方 numpy 代码来理解,但我无法理解。

如果有人能以更简单的方式提供解释,那就太好了。

【问题讨论】:

【参考方案1】:

根据我的经验,编写 C 代码的工作量太大。简单地找到相关功能是最困难的部分。 strides 无论尺寸或“转置”如何,都一样工作。

从更简单的开始,例如 (2,3) 数组,其转置步长为 (8,24)。想象一下穿过平面 [0,1,2...]。

样本数组,大小为 1 字节,因此顺序步长仅为 1

In [635]: x=np.arange(6,dtype='uint8')
In [636]: x
Out[636]: array([0, 1, 2, 3, 4, 5], dtype=uint8)
In [637]: x.strides
Out[637]: (1,)

现在重塑它:

In [638]: y=x.reshape(2,3)
In [639]: y
Out[639]: 
array([[0, 1, 2],
       [3, 4, 5]], dtype=uint8)
In [640]: y.strides
Out[640]: (3, 1)

要遍历列,我们仍然只是通过共享的x 数据缓冲区一步一步。往下走,我们从 3、0 到 3、2 到 5 步进。

In [641]: z = y.transpose()
In [642]: z
Out[642]: 
array([[0, 3],
       [1, 4],
       [2, 5]], dtype=uint8)
In [643]: z.strides
Out[643]: (1, 3)

现在我们一步一步往下走,一步一步跨过去。

但是你从图片中推断出相同的结果(我们应该正式忽略 :))

那么你的问题是什么?如果您在处理某些特定的 C 代码时遇到问题,您需要展示它,或者至少引用它。

遍历数组的方法有很多种。对于 flatiter strides 无关紧要,它只是一次通过数据缓冲区中的一项。

但是,例如,如果我们在第一个维度上进行迭代,

In [687]: for i in y:print(i)
[0 1 2]
[3 4 5]

以 3 步获取下一行。

In [688]: for i in z:print(i)
[0 3]
[1 4]
[2 5]

而这一步是 1。

【讨论】:

很抱歉,我知道我也应该嵌入 C 代码的链接。这一次,我已经能够解决我的疑问。感谢您的帮助!【参考方案2】:

除非明确要求(例如使用axis 参数),否则Numpy 总是从最大的轴迭代到最小的轴(即降序)。因此,在您的示例中,它首先读取内存中偏移量 0 处的视图项目,然后添加轴 2 的步幅(此处为 4)并读取下一个项目,依此类推,直到到达轴的末尾。然后将轴 1 的步幅添加一次,并再次重复上一个循环,以此类推其他轴。

内部 Numpy C 代码的行为如下:

// Numpy array are not strongly typed internally.
// The raw memory buffer is always contiguous.
char* data = view.rawData;

const size_t iStride = view.stride[0];
const size_t jStride = view.stride[1];
const size_t kStride = view.stride[2];

for(int i=0 ; i<view.shape[0] ; ++i) 
    for(int j=0 ; j<view.shape[1] ; ++j) 
        for(int k=0 ; k<view.shape[2] ; ++k) 
            // Compute the offset from the view strides
            const size_t offset = iStride * i + jStride * j + kStride * k;

            // Extract an item at the memory offset
            Type item = (Type*)(data + offset);

            // Do something with item here (eg. print it)
        
    

当您应用转置时,Numpy 会更改步幅,以便交换 iStridejStride。它还会更新形状(view.shape[0]view.shape[1] 也交换了)。代码将像两个循环被交换一样执行,只是内存访问效率较低,因为它们不太连续。这是一个例子:

arr = np.arange(16).reshape((2, 2, 4))
arr.strides   # (32, 16, 4)
view = arr.transpose((1, 0, 2))
view.strides  # (16, 32, 4)  <--  note that 16 and 32 have been swapped

请注意,步幅以字节(而不是项目数)为单位。

【讨论】:

非常感谢!这帮助我消除了疑惑。

以上是关于strides 如何帮助在 numpy 中遍历数组?的主要内容,如果未能解决你的问题,请参考以下文章

无法将符号张量 (lstm_15/strided_slice:0) 转换为 numpy 数组

滚动统计性能:pandas vs. numpy strides

如何理解外行的 numpy strides?

检查numpy数组是不是连续?

Numpy如何遍历数组的列?

[读书笔记] Python 数据分析 高级NumPy