是否可以在 Numpy 矢量化广播操作期间访问当前索引?

Posted

技术标签:

【中文标题】是否可以在 Numpy 矢量化广播操作期间访问当前索引?【英文标题】:Is it possible to access the current indices during a Numpy vectorized broadcasting operation? 【发布时间】:2019-02-15 04:05:16 【问题描述】:

我想在 Numpy 中使用花哨的索引、矢量化和/或广播来加速单个数组上的函数。对于数组中的每个值,我需要进行涉及相邻值的计算。因此,在我的矢量化操作中,我需要访问当前索引,以便获取它周围的索引。考虑以下简单的数组操作:

x = np.arange(36).reshape(6, 6)
y = np.zeros((6, 6))
y[:] = x + 1

我想使用类似的语法,但不是简单的增量,我想做一些事情,比如将相邻索引处的所有值添加到矢量化循环中的当前值。例如,如果索引 [i, j] == 7 周围的区域看起来像

3 2 5
2 7 6
5 5 5

我希望[i, j] 的计算值为3 + 2 + 5 + 2 + 7 + 6 + 5 + 5 + 5,并且我想对所有索引[i, j] 这样做。

这是一个简单的嵌套 for 循环(或单个 for 循环,对每个索引使用 np.sum)...但如果可能,我想使用广播和/或花式索引。对于 Numpy 语法来说,这可能是一个太复杂的问题,但我觉得应该是可能的。

本质上归结为:在广播操作期间如何引用当前索引?

【问题讨论】:

虽然可以访问索引 - 只需构建索引数组并将它们合并到您的计算中 - 这样做并不是使用相邻元素值的好方法。 @user2357112 你能详细说明为什么这不是一个好方法吗? 一旦你得到了索引,你想象你会用它们做什么来获取相邻元素的值?我能想象到的大多数事情要么是不可行的,要么需要显式的 Python 循环,要么会产生数组格式的相邻值,而无需使用显式索引就可以更有效地构建。 简短的回答是,这不是你在 numpy 中做类似事情的方式。 @user2357112 “可以在不使用显式索引的情况下更有效地构建。”这就是我正在寻找的方法——我不在乎使用花哨的索引,除非它能让我达到目标。本质上,我想做我所描述的,但使用 Numpy 的快速编译库而不是慢速 Python 循环。我不接受使用索引数组的想法。 【参考方案1】:

从一维示例开始:

x = np.arange(10)

您必须做出选择:是否丢弃边缘,因为它们没有两个邻居?如果这样做,您基本上可以一步创建输出数组:

result = x[:-2] + x[1:-1] + x[2:]

请注意,所有三个加数都是视图,因为它们使用简单的索引。您希望尽可能避免花哨的索引,因为它通常涉及复制。

如果您更喜欢保留边,可以预先分配输出缓冲区并直接添加到其中:

result = x.copy()
result[:-1] += x[1:]
result[1:] += x[:-1]

这两种情况的基本思想是要将操作应用于所有个相邻元素,只需将数组移动 +/-1。你不需要知道任何索引,或者做任何花哨的事情。越简单越好。

希望您能了解如何将其推广到 2D 案例。与在 -1、0、1 之间移动的单个索引不同,您在它们两者之间的 -1、0、1 的每个可能组合中都有两个索引。

附录

以下是无 egde 结果的通用方法:

from itertools import product
def sum_shifted(a):
    result = np.zeros(tuple(x - 2 for x in a.shape), dtype=a.dtype)
    for index in product([slice(0, -2), slice(1, -1), slice(2, None)], repeat=a.ndim):
        result += a[index]
    return result

这个实现有点初级,因为它不检查没有尺寸或形状

请注意,对于 1D 情况,循环将运行 3 次,2D 为 9 次,ND 为 3N。这是我发现一个明确的 for 循环适合 numpy 的一种情况。与在大型阵列上完成的工作相比,循环非常小,对于小型阵列来说足够快,并且肯定比为 3D 案例手动编写所有 27 种可能性要好。

还有一点需要注意的是连续索引是如何生成的。在 Python 中,带有冒号的索引(如 x[1:2:3])被转换为相对未知的 slice 对象:slice(1, 2, 3)。因为(几乎)所有带逗号的东西都被解释为一个元组,所以表达式x[1:2, ::-1, :2] 中的索引完全等同于(slice(1, 2), slice(None, None, -1), slice(None, 2))。该循环恰好生成这样一个表达式,每个维度都有一个元素。所以结果实际上是跨所有维度的简单索引。

如果您想保留边缘,可以使用类似的方法。唯一显着的区别是您需要同时索引输入和输出数组:

from itertools import product
def sum_shifted(a):
    result = np.zeros_like(a)
    for r_index, a_index in zip(product([slice(0, -1), slice(None), slice(1, None)], repeat=a.ndim),
                                product([slice(1, None), slice(None), slice(0, -1)], repeat=a.ndim)):
        result[r_index] += a[a_index]
    return result

这是因为itertools.product 保证了迭代的顺序,所以两个压缩的迭代器将保持同步。

【讨论】:

是的,我想这会让我到达那里。但是,现在我想起来了,我应该把代码写成一个循环,然后用 Numba 编译。 “读代码比写代码多,”伟大的 Guido 说——并且向后弯腰编写晦涩难懂的代码来做简单的概念是非常反 Python 的。 (但我发现自己经常这样做!)试图避免使用 Numba 或 Cython。 @physmom:如果你有 Numba,那么对许多高级 NumPy 问题的最佳答案就是“只需编写 for 循环并将其扔给 Numba”。 这是一个使用快速整体阵列构建块的示例。实际上,这是非常 Pythonic 的。列表和字典是我们尽可能将其用作整个对象的构建块。 numpy 更进一步。 MATLAB 过去也需要这种思维方式,但现在它的 JIT 可以让您恢复到迭代思维,而不会造成太多性能损失。【参考方案2】:

试试这个:

x = np.arange(36).reshape(6, 6)
y = np.zeros((6, 6))
for i in range(x.shape[0]):
    for j in range(x.shape[1]):
        if i>0 and i<x.shape[0]-1 and j>0 and j<x.shape[1]-1:
            y[i,j]=x[i,j]+x[i-1,j]+x[i,j-1]+x[i-1,j-1]+x[i+1,j]+x[i,j+1]+x[i+1,j+1]+x[i-1,j+1]+x[i+1,j-1]
        if j==0:
            if i==0:
                y[i,j]=x[i,j]+x[i,j+1]+x[i+1,j+1]+x[i+1,j]
            elif i==x.shape[0]-1:
                y[i,j]=x[i,j]+x[i,j+1]+x[i-1,j+1]+x[i-1,j]
            else:
                y[i,j]=x[i,j]+x[i,j+1]+x[i+1,j+1]+x[i+1,j]+x[i-1,j]+x[i-1,j+1]

        if j==x.shape[1]-1:
            if i==0:
                y[i,j]=x[i,j]+x[i,j-1]+x[i+1,j-1]+x[i+1,j]
            elif i==x.shape[0]-1:
                y[i,j]=x[i,j]+x[i,j-1]+x[i-1,j-1]+x[i-1,j] 
            else:
                y[i,j]=x[i,j]+x[i,j-1]+x[i-1,j-1]+x[i+1,j]+x[i-1,j]+x[i+1,j-1]
        if i==0 and j in range(1,x.shape[1]-1):
            y[i,j]=x[i,j]+x[i,j-1]+x[i+1,j-1]+x[i+1,j]+x[i+1,j+1]+x[i,j+1]
        if i==x.shape[0]-1 and j in range(1,x.shape[1]-1):
            y[i,j]=x[i,j]+x[i,j-1]+x[i-1,j-1]+x[i-1,j]+x[i-1,j+1]+x[i,j+1]
print(y)

【讨论】:

以上是关于是否可以在 Numpy 矢量化广播操作期间访问当前索引?的主要内容,如果未能解决你的问题,请参考以下文章

NumPy入门讲座:实战演练

登高自卑 | 我的NumPy笔记

通俗易懂的解释numpy中的广播

是否可以在 numpy 中对这个计算进行矢量化?

学习基础知识:数组和矢量计量Numpy

numpy中二维数组上的矢量化移动窗口