#python #performance #numpy #cython
#питон #Производительность #numpy #китон
Вопрос:
Я написал следующий код на чистом python, описание того, что он делает, находится в docstrings:
import numpy as np
from scipy.ndimage.measurements import find_objects
import itertools
def alt_indexer(arr):
"""
Returns a dictionary with the elements of arr as key
and the corresponding slice as value.
Note:
This function assumes arr is sorted.
Example:
>>> arr = [0,0,3,2,1,2,3]
>>> loc = _indexer(arr)
>>> loc
{0: (slice(0L, 2L, None),),
1: (slice(2L, 3L, None),),
2: (slice(3L, 5L, None),),
3: (slice(5L, 7L, None),)}
>>> arr = sorted(arr)
>>> arr[loc[3][0]]
[3, 3]
>>> arr[loc[2][0]]
[2, 2]
"""
unique, counts = np.unique(arr, return_counts=True)
labels = np.arange(1,len(unique) 1)
labels = np.repeat(labels,counts)
slicearr = find_objects(labels)
index_dict = dict(itertools.izip(unique,slicearr))
return index_dict
Поскольку я буду индексировать очень большие массивы, я хотел ускорить операции с помощью cython, вот эквивалентная реализация:
import numpy as np
cimport numpy as np
def _indexer(arr):
cdef tuple unique_counts = np.unique(arr, return_counts=True)
cdef np.ndarray[np.int32_t,ndim=1] unique = unique_counts[0]
cdef np.ndarray[np.int32_t,ndim=1] counts = unique_counts[1].astype(int)
cdef int start=0
cdef int end
cdef int i
cdef dict d ={}
for i in xrange(len(counts)):
if i>0:
start = counts[i-1] start
end=counts[i] start
d[unique[i]]=slice(start,end)
return d
Тесты
Я сравнил время, затраченное на выполнение обеих операций:
In [26]: import numpy as np
In [27]: rr=np.random.randint(0,1000,1000000)
In [28]: %timeit _indexer(rr)
10 loops, best of 3: 40.5 ms per loop
In [29]: %timeit alt_indexer(rr) #pure python
10 loops, best of 3: 51.4 ms per loop
Как вы можете видеть, улучшения в скорости минимальны. Я понимаю, что мой код уже был частично оптимизирован с тех пор, как я использовал numpy.
Есть ли узкое место, о котором я не знаю? Не должен ли я вместо этого использовать np.unique
и писать свою собственную реализацию?
Спасибо.
Комментарии:
1.
cython
циклы выполняются быстрее, если их можно перевести в чистыйC
. В вашем случае цикл по-прежнему используетnumpy.unique
и словарь Python, и объекты среза.
Ответ №1:
При arr
наличии неотрицательных, не очень больших и повторяющихся int
чисел вот альтернативный подход, использующий np.bincount
для имитации того же поведения, что и np.unique(arr, return_counts=True)
—
def unique_counts(arr):
counts = np.bincount(arr)
mask = counts!=0
unique = np.nonzero(mask)[0]
return unique, counts[mask]
Тест во время выполнения
Случай №1 :
In [83]: arr = np.random.randint(0,100,(1000)) # Input array
In [84]: unique, counts = np.unique(arr, return_counts=True)
...: unique1, counts1 = unique_counts(arr)
...:
In [85]: np.allclose(unique,unique1)
Out[85]: True
In [86]: np.allclose(counts,counts1)
Out[86]: True
In [87]: %timeit np.unique(arr, return_counts=True)
10000 loops, best of 3: 53.2 µs per loop
In [88]: %timeit unique_counts(arr)
100000 loops, best of 3: 10.2 µs per loop
Случай №2:
In [89]: arr = np.random.randint(0,1000,(10000)) # Input array
In [90]: %timeit np.unique(arr, return_counts=True)
1000 loops, best of 3: 713 µs per loop
In [91]: %timeit unique_counts(arr)
10000 loops, best of 3: 39.1 µs per loop
Пример № 3: давайте запустим случай с unique
некоторыми отсутствующими числами в диапазоне от минимального до максимального и сверим результаты с np.unique
версией в качестве проверки работоспособности. В этом случае у нас не будет много повторяющихся чисел, и, как таковой, не ожидается улучшения производительности.
In [98]: arr = np.random.randint(0,10000,(1000)) # Input array
In [99]: unique, counts = np.unique(arr, return_counts=True)
...: unique1, counts1 = unique_counts(arr)
...:
In [100]: np.allclose(unique,unique1)
Out[100]: True
In [101]: np.allclose(counts,counts1)
Out[101]: True
In [102]: %timeit np.unique(arr, return_counts=True)
10000 loops, best of 3: 61.9 µs per loop
In [103]: %timeit unique_counts(arr)
10000 loops, best of 3: 71.8 µs per loop
Комментарии:
1. Хорошее решение! к сожалению, целые числа начинаются с 1000000. И в некоторых случаях массив будет содержать значения с плавающей запятой.
2. @snowleopard Да, тогда мы должны искать другие способы оптимизации.
3. Но вы правы, похоже, что np.unique нужно чем-то заменить. Что мне действительно нужно, так это набор фрагментов, и кажется, что есть некоторые избыточные операции.