Операции NumPy на месте с перекрывающимися срезами
Рассмотрим эту неуместную операцию:
>>> 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, или это ошибка, или результат не определен?
1 ответ
Это поведение ранее не было определено, но, начиная с NumPy 1.13.0, операции с перекрывающимися вводом и выводом теперь ведут себя так, как если бы вводы были скопированы первыми. Цитата из примечаний к выпуску:
Операции, в которых входные и выходные операнды ufunc имеют перекрытие памяти, приводили к неопределенным результатам в предыдущих версиях NumPy из-за проблем с зависимостью данных. В NumPy 1.13.0 результаты таких операций теперь определены как те же, что и для эквивалентных операций, где нет перекрытия памяти.
Затронутые операции теперь делают временные копии по мере необходимости для устранения зависимости данных. Поскольку обнаружение таких случаев требует больших вычислительных ресурсов, используется эвристика, которая в редких случаях может привести к ненужным временным копиям. Для операций, в которых зависимость данных достаточно проста для эвристического анализа, временные копии не будут создаваться, даже если массивы перекрываются, если можно вывести, копии не нужны. В качестве примера,
np.add(a, b, out=a)
не будет включать копии.
Неопределенный или, по крайней мере, трудный для понимания, может быть лучшим ответом. Еще один 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:]-=
нет, не так очевидно.