Быстрый python numpy, где функциональность?

Я использую numpy, где функция много раз внутри нескольких контуров for, но она становится слишком медленной. Есть ли способы быстрее выполнять эту функцию? Я читал, что вы должны пытаться делать строки в циклах, а также делать локальные переменные для функций перед циклами for, но ничто не улучшает скорость на много (< 1%). len(UNIQ_IDS) ~ 800. emiss_data и obj_data - numpy ndarrays с формой = (2600, 500). Я использовал import profile, чтобы получить дескриптор, где расположены узкие места, а where в циклах for - большой.

import numpy as np
max = np.max
where = np.where
MAX_EMISS = [max(emiss_data[where(obj_data == i)]) for i in UNIQ_IDS)]

Ответ 1

Оказывается, что чистый цикл Python может быть намного быстрее, чем индексирование NumPy (или вызовы np.where) в этом случае.

Рассмотрим следующие альтернативы:

import numpy as np
import collections
import itertools as IT

shape = (2600,5200)
# shape = (26,52)
emiss_data = np.random.random(shape)
obj_data = np.random.random_integers(1, 800, size=shape)
UNIQ_IDS = np.unique(obj_data)

def using_where():
    max = np.max
    where = np.where
    MAX_EMISS = [max(emiss_data[where(obj_data == i)]) for i in UNIQ_IDS]
    return MAX_EMISS

def using_index():
    max = np.max
    MAX_EMISS = [max(emiss_data[obj_data == i]) for i in UNIQ_IDS]
    return MAX_EMISS

def using_max():
    MAX_EMISS = [(emiss_data[obj_data == i]).max() for i in UNIQ_IDS]
    return MAX_EMISS

def using_loop():
    result = collections.defaultdict(list)
    for val, idx in IT.izip(emiss_data.ravel(), obj_data.ravel()):
        result[idx].append(val)
    return [max(result[idx]) for idx in UNIQ_IDS]

def using_sort():
    uind = np.digitize(obj_data.ravel(), UNIQ_IDS) - 1
    vals = uind.argsort()
    count = np.bincount(uind)
    start = 0
    end = 0
    out = np.empty(count.shape[0])
    for ind, x in np.ndenumerate(count):
        end += x
        out[ind] = np.max(np.take(emiss_data, vals[start:end]))
        start += x
    return out

def using_split():
    uind = np.digitize(obj_data.ravel(), UNIQ_IDS) - 1
    vals = uind.argsort()
    count = np.bincount(uind)
    return [np.take(emiss_data, item).max()
            for item in np.split(vals, count.cumsum())[:-1]]

for func in (using_index, using_max, using_loop, using_sort, using_split):
    assert using_where() == func()

Ниже приведены эталоны: shape = (2600,5200):

In [57]: %timeit using_loop()
1 loops, best of 3: 9.15 s per loop

In [90]: %timeit using_sort()
1 loops, best of 3: 9.33 s per loop

In [91]: %timeit using_split()
1 loops, best of 3: 9.33 s per loop

In [61]: %timeit using_index()
1 loops, best of 3: 63.2 s per loop

In [62]: %timeit using_max()
1 loops, best of 3: 64.4 s per loop

In [58]: %timeit using_where()
1 loops, best of 3: 112 s per loop

Таким образом, using_loop (чистый Python) оказывается более чем на 11 раз быстрее, чем using_where.

Я не совсем уверен, почему чистый Python быстрее, чем NumPy. Я предполагаю, что чистая версия Python zips (да, каламбур) через оба массива один раз. Он использует тот факт, что, несмотря на все причудливое индексирование, мы действительно просто хотим посетить каждое значение один раз. Таким образом, он ставит проблему в том, что нужно точно определить, в какую группу попало каждое значение в emiss_data. Но это просто расплывчатая спекуляция. Я не знал, что это будет быстрее, пока я не сравню результаты.

Ответ 2

Можно использовать np.unique с return_index:

def using_sort():
    #UNIQ_IDS,uind=np.unique(obj_data, return_inverse=True)
    uind= np.digitize(obj_data.ravel(), UNIQ_IDS) - 1
    vals=uind.argsort()
    count=np.bincount(uind)

    start=0
    end=0

    out=np.empty(count.shape[0])
    for ind,x in np.ndenumerate(count):
        end+=x
        out[ind]=np.max(np.take(emiss_data,vals[start:end]))
        start+=x
    return out

Использование ответа @unutbu в качестве базовой линии для shape = (2600,5200):

np.allclose(using_loop(),using_sort())
True

%timeit using_loop()
1 loops, best of 3: 12.3 s per loop

#With np.unique inside the definition
%timeit using_sort()
1 loops, best of 3: 9.06 s per loop

#With np.unique outside the definition 
%timeit using_sort()
1 loops, best of 3: 2.75 s per loop

#Using @Jamie suggestion for uind
%timeit using_sort()
1 loops, best of 3: 6.74 s per loop

Ответ 3

Я считаю, что самый быстрый способ выполнить это - использовать операции groupby() в пакете pandas. Сравнивая с функцией @Ophion using_sort(), Pandas примерно в 10 раз быстрее:

import numpy as np
import pandas as pd

shape = (2600,5200)
emiss_data = np.random.random(shape)
obj_data = np.random.random_integers(1, 800, size=shape)
UNIQ_IDS = np.unique(obj_data)

def using_sort():
    #UNIQ_IDS,uind=np.unique(obj_data, return_inverse=True)
    uind= np.digitize(obj_data.ravel(), UNIQ_IDS) - 1
    vals=uind.argsort()
    count=np.bincount(uind)

    start=0
    end=0

    out=np.empty(count.shape[0])
    for ind,x in np.ndenumerate(count):
        end+=x
        out[ind]=np.max(np.take(emiss_data,vals[start:end]))
        start+=x
    return out

def using_pandas():
    return pd.Series(emiss_data.ravel()).groupby(obj_data.ravel()).max()

print('same results:', np.allclose(using_pandas(), using_sort()))
# same results: True

%timeit using_sort()
# 1 loops, best of 3: 3.39 s per loop

%timeit using_pandas()
# 1 loops, best of 3: 397 ms per loop

Ответ 4

Не можете ли вы просто сделать

emiss_data[obj_data == i]

? Я не уверен, почему вы используете where вообще.

Ответ 6

Если obj_data состоит из относительно небольших целых чисел, вы можете использовать numpy.maximum.at (начиная с v1.8.0):

def using_maximumat():
    n = np.max(UNIQ_IDS) + 1
    temp = np.full(n, -np.inf)
    np.maximum.at(temp, obj_data, emiss_data)
    return temp[UNIQ_IDS]