add.at
предназначен для случаев, когда индексы содержат дубликаты, а +=
не дает желаемого результата.
In [44]: A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
In [45]: A[idx]+=1
In [46]: A
Out[46]: array([1, 1, 1, 1, 0]) # the duplicates in idx are ignored
С add.at
:
In [47]: A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
In [48]: np.add.at(A, idx, 1)
In [49]: A
Out[49]: array([1, 2, 3, 4, 0])
Тот же результат, что и при явной итерации:
In [50]: A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
In [51]: for i in idx: A[i]+=1
In [52]: A
Out[52]: array([1, 2, 3, 4, 0])
Некоторые тайминги:
In [53]: %%timeit A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
...: A[idx]+=1
3.65 µs ± 13.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [54]: %%timeit A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
...: np.add.at(A, idx, 1)
6.47 µs ± 24.8 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [55]: %%timeit A = np.zeros(5,int); idx = np.array([0,1,1,2,2,2,3,3,3,3])
...: np.add.at(A, idx, 1)
...: for i in idx: A[i]+=1
15.6 µs ± 41.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
add.at
медленнее, чем +=
, но лучше, чем итерация Python.
Мы могли бы протестировать такие варианты, как A[:4]+1
, A[:4]+=1
и т. д.
В любом случае, не используйте вариант add.at
, если он вам не нужен.
редактировать
Ваш пример немного упрощен:
In [108]: x = np.zeros(2*10-1)
...: for i in range(10):
...: x[i:i+10] += 1
...:
In [109]: x
Out[109]:
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 9., 8., 7.,
6., 5., 4., 3., 2., 1.])
Итак, вы добавляете значения к перекрывающимся срезам. Мы могли бы заменить срезы массивом:
In [110]: x = np.zeros(2*10-1)
...: for i in range(10):
...: x[np.arange(i,i+10)] += 1
...:
In [111]: x
Out[111]:
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 9., 8., 7.,
6., 5., 4., 3., 2., 1.])
Не нужно добавлять свой «медленный» случай, add.at
с кусочками, потому что индексы не имеют дубликатов.
Создание всех индексов. +=
не работает из-за буферизации
In [112]: idx=np.arange(10); idx=(idx[:,None]+idx).ravel()
In [113]: y=np.zeros(2*10-1)
...: y[idx]+=1
In [114]: y
Out[114]:
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
1., 1.])
add.at
решает, что:
In [115]: y=np.zeros(2*10-1)
...: np.add.at(y, idx, 1)
In [116]: y
Out[116]:
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 9., 8., 7.,
6., 5., 4., 3., 2., 1.])
И полная итерация Python:
In [117]: y=np.zeros(2*10-1)
...: for i in idx: y[i]+=1
In [118]:
In [118]: y
Out[118]:
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 9., 8., 7.,
6., 5., 4., 3., 2., 1.])
Теперь немного таймингов.
Базовая линия:
In [119]: %%timeit
...: x = np.zeros(2*10-1)
...: for i in range(10):
...: x[i:i+10] += 1
...:
50.5 µs ± 177 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Расширенная индексация несколько замедляет это:
In [120]: %%timeit
...: x = np.zeros(2*10-1)
...: for i in range(10):
...: x[np.arange(i,i+10)] += 1
...:
75.2 µs ± 79.9 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Если бы это сработало, один += с расширенной индексацией был бы самым быстрым:
In [121]: %%timeit
...: idx=np.arange(10); idx=(idx[:,None]+idx).ravel()
...: y=np.zeros(2*10-1)
...: y[idx]+=1
...:
...:
17.5 µs ± 693 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Полная итерация Python примерно такая же, как и в случае с зацикленным аранжем:
In [122]: %%timeit
...: idx=np.arange(10); idx=(idx[:,None]+idx).ravel()
...: y=np.zeros(2*10-1)
...: for i in idx: y[i]+=1
...:
...:
76.3 µs ± 2.51 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
add.at
медленнее, чем плоский +=, но все же лучше, чем базовая линия:
In [123]: %%timeit
...: idx=np.arange(10); idx=(idx[:,None]+idx).ravel()
...: y=np.zeros(2*10-1)
...: np.add.at(y, idx,1)
...:
...:
29.4 µs ± 21.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
В моем меньшем тесте лучше всего показал себя ваш slower
. Но возможно, что он не так хорошо масштабируется, как базовый/быстрый. Ваш indices
намного больше. Часто для очень больших массивов итерация по одному измерению выполняется быстрее из-за снижения нагрузки на управление памятью.
person
hpaulj
schedule
29.04.2021
ufunc.at
эквивалентно операциям на месте. То, что вы связали, - это сравнение междуA += B
иA + B
. Я могу понять, почему последний здесь работает медленнее, потому что он должен выделять временный массив. - person Kevin   schedule 29.04.2021np.add.at
без необходимости использует расширенную индексацию, которая создает копию данных. Кто-нибудь может подтвердить эту теорию? - person Kevin   schedule 29.04.2021add.at
полезен, когда в индексируемом массиве есть дубликаты, а+=
не соответствует итеративной альтернативе. Это должно быть описано в документах. - person hpaulj   schedule 29.04.2021add.at
— это итеративное решение? - person Kevin   schedule 29.04.2021numpy
повторяется, но быстрые вещи делают это скрытно в скомпилированном коде.add.at
дает понять, что у+=
есть проблемы, потому что решение буферизовано. Фактическиtemp=a[idx]+b
, за которым следуетa[idx]=temp
.add.at
делает это без буферизации, поэтому могут накапливаться повторяющиеся изменения. Это медленнее, чем с буферизацией, но все же быстрее, чем итерация на уровне Python. - person hpaulj   schedule 29.04.2021slower
, который я считаю допустимым вариантом использования дляadd.at
. Как видите, итерацию на уровне Python можно удалить, используя повторяющиеся индексы. Хотя это выглядит более читабельно, кажется, что это не быстрее, чем использование итерационных решений на уровне Python. Это потому, чтоfast
иslow
используютslice
? - person Kevin   schedule 29.04.2021slower
может работать медленнее из-за проблем с управлением памятью. - person hpaulj   schedule 30.04.2021