具有重叠切片的NumPy就地操作

时间:2017-03-31 23:46:19

标签: python numpy in-place

考虑这种不合时宜的操作:

>>> b = numpy.asarray([1, 2, 3])
>>> b[1:] = b[1:] - b[:-1]
>>> b
array([1, 1, 1])

现在考虑就地操作:

>>> a = numpy.asarray([1, 2, 3])
>>> a[1:] -= a[:-1]
>>> a
array([1, 1, 2])

他们给出了不同的结果,这是我没想到的。

更新:似乎这种行为发生了变化;现在他们给出了相同的结果。

我会假设NumPy会以正确的顺序(向后)进行减法,这样他们就可以得到与异地减法相同的结果。

我的问题是:这是NumPy部分的预期行为,还是一个错误,或结果未定义?

2 个答案:

答案 0 :(得分:1)

未定义,或至少难以理解,可能是最好的答案。另一个SO答案声称

a[1:] -= a[:-1]
解释器将

翻译成类似

的内容
a.__setitem__(slice(1,None), a.__getitem__(slice(1,None)).
   __isub__(a.__getitem__(slice(None,-1))))

In [171]: a=np.arange(10)
In [172]: a[1:] -= a[:-1]
In [173]: a
Out[173]: array([0, 1, 1, 2, 2, 3, 3, 4, 4, 5])

评估如下:

In [175]: for i in range(9):
     ...:     a[i+1] = a[i+1]-a[i]

我可以从嵌套的__setitem__等表达式中看到它是如何得到的。我尝试使用np.nditer复制它。

你提到的反面是

In [178]: for i in range(8,-1,-1):
     ...:     a[i+1] = a[i+1]-a[i]

我没有numpy能够推断出需要这样的反向迭代的任何方式。 __setitem__的第二个参数通过前向迭代评估得很好。缓冲该术语是唯一简单的解决方案。

引入.at ufunc方法是为了解决a[idx] += b等表达式中的缓冲问题。特别是当idx有重复时。应该对a的影响是累积的,还是仅应用最后一个实例。

在您的示例中,如果表现得像a[1:] - a[:-1]

In [165]: a=np.arange(10)
In [166]: idx=np.arange(1,10)
In [167]: np.add.at(a, idx, -a[:-1])
In [168]: a
Out[168]: array([0, 1, 1, 1, 1, 1, 1, 1, 1, 1])

那是因为add.at的第三个参数在被使用之前被完全评估了。它是临时副本。我从其他测试中得知add.at比普通a[idx] +=慢。 [我对于什么'缓冲'有点困惑。 add.at正在绕过;这与在这里出现问题的明显缺乏缓冲有什么不同?]

为什么要使用+=表示法?只是为了使代码更紧凑?还是希望加快速度?但如果速度是目标,我们是否希望numpy抛出一些额外的缓冲,只是为了让它更安全?

等同于nditer

a[1:] -= a[:-1]
In [190]: a=np.arange(10)
In [191]: it = np.nditer([a[1:],a[1:],a[:-1]], op_flags=['readwrite'])
In [192]: for i,j,k in it:
     ...:     print(i,j,k)
     ...:     i[...] = j-k
     ...:     print(i)

1 1 0
1
2 2 1
1
3 3 1
2
4 4 2
2
5 5 2
3
6 6 3
...

迭代可以简化

In [197]: it = np.nditer([a[1:],a[:-1]], op_flags=['readwrite'])
In [198]: for i,j in it:
     ...:     i[...] -= j

因为它是一个视图,a[:-1]的迭代值将反映前一个循环中所做的更改。

我不确定c版本nditer正在使用数组+=表达式,但nditer的意图是整合迭代编码成一个统一的框架。

另一个有趣的观察是,如果我定义

idx = array([1, 2, 3, 4, 5, 6, 7, 8, 9])

然后

a[idx] -= a[:-1]
a[1:] -= a[idx-1]
a[idx] -= a[idx-1]

全部提供所需的array([0, 1, 1, 1, 1, 1, 1, 1, 1, 1])。换句话说,-=的两边都必须是观看/切片'。这必须是add.at绕过的缓冲。 a[idx-1]是一个副本是显而易见的。那个a[idx]-=抛出一个缓冲区,而a[1:]-=却不是这样。

答案 1 :(得分:1)

此行为以前未定义,但是since NumPy 1.13.0中,输入和输出重叠的操作现在的行为就好像首先复制输入一样。引用发行说明:

  

由于数据依赖性问题,ufunc输入和输出操作数与内存重叠的操作在以前的NumPy版本中产生了不确定的结果。在NumPy 1.13.0中,现在将此类操作的结果定义为与没有内存重叠的等效操作相同。

     

受影响的操作现在可以临时复制,以消除数据依赖性。由于检测这些情况在计算上很昂贵,因此使用了一种启发式方法,在极少数情况下可能会导致不必要的临时副本。对于数据依赖关系足够简单以进行启发式分析的操作,即使阵列重叠也不会创建临时副本,即使可以推断出这些副本也不是必需的。例如,np.add(a, b, out=a)将不涉及副本。

Here is the relevant issue on GitHub