Самый эффективный способ сопоставления функции над массивом numpy
Каков наиболее эффективный способ сопоставления функции над массивом numpy? То, как я это делал в своем текущем проекте, выглядит следующим образом:
import numpy as np
x = np.array([1, 2, 3, 4, 5])
# Obtain array of square of each element in x
squarer = lambda t: t ** 2
squares = np.array([squarer(xi) for xi in x])
Однако похоже, что он, вероятно, очень неэффективен, так как я использую понимание списка для создания нового массива в виде списка Python, прежде чем преобразовывать его обратно в массив numpy.
Можем ли мы сделать лучше?
Ответы
Ответ 1
Я проверил все предложенные методы плюс np.array(map(f, x))
с perfplot
( perfplot
небольшой проект).
Сообщение № 1: Если вы можете использовать NumPy нативные функции, сделайте это.
Если функция, которую вы пытаетесь векторизовать, уже векторизована (как пример x**2
в оригинальном посте), то использовать ее намного быстрее, чем что-либо еще (обратите внимание на масштаб журнала):
![enter image description here]()
Если вам действительно нужна векторизация, не имеет большого значения, какой вариант вы используете.
![enter image description here]()
Код для воспроизведения участков:
import numpy as np
import perfplot
import math
def f(x):
# return math.sqrt(x)
return np.sqrt(x)
vf = np.vectorize(f)
def array_for(x):
return np.array([f(xi) for xi in x])
def array_map(x):
return np.array(list(map(f, x)))
def fromiter(x):
return np.fromiter((f(xi) for xi in x), x.dtype)
def vectorize(x):
return np.vectorize(f)(x)
def vectorize_without_init(x):
return vf(x)
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(20)],
kernels=[
f,
array_for, array_map, fromiter, vectorize, vectorize_without_init
],
logx=True,
logy=True,
xlabel='len(x)',
)
Ответ 2
Как насчет использования numpy.vectorize
.
>>> import numpy as np
>>> x = np.array([1, 2, 3, 4, 5])
>>> squarer = lambda t: t ** 2
>>> vfunc = np.vectorize(squarer)
>>> vfunc(x)
array([ 1, 4, 9, 16, 25])
https://docs.scipy.org/doc/numpy/reference/generated/numpy.vectorize.html
Ответ 3
TL; DR
Как отмечает @user2357112, "прямой" метод применения функции - это всегда самый быстрый и простой способ отобразить функцию на массивах Numpy:
import numpy as np
x = np.array([1, 2, 3, 4, 5])
f = lambda x: x ** 2
squares = f(x)
Обычно избегайте использования np.vectorize
, поскольку он np.vectorize
и имеет (или имел) ряд проблем. Если вы работаете с другими типами данных, вы можете изучить другие методы, показанные ниже.
Сравнение методов
Вот несколько простых тестов для сравнения трех методов для сопоставления функции, этот пример используется с Python 3.6 и NumPy 1.15.4. Во-первых, настройки функций для тестирования:
import timeit
import numpy as np
f = lambda x: x ** 2
vf = np.vectorize(f)
def test_array(x, n):
t = timeit.timeit(
'np.array([f(xi) for xi in x])',
'from __main__ import np, x, f', number=n)
print('array: {0:.3f}'.format(t))
def test_fromiter(x, n):
t = timeit.timeit(
'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))',
'from __main__ import np, x, f', number=n)
print('fromiter: {0:.3f}'.format(t))
def test_direct(x, n):
t = timeit.timeit(
'f(x)',
'from __main__ import x, f', number=n)
print('direct: {0:.3f}'.format(t))
def test_vectorized(x, n):
t = timeit.timeit(
'vf(x)',
'from __main__ import x, vf', number=n)
print('vectorized: {0:.3f}'.format(t))
Тестирование с пятью элементами (отсортировано от самого быстрого до самого медленного):
x = np.array([1, 2, 3, 4, 5])
n = 100000
test_direct(x, n) # 0.265
test_fromiter(x, n) # 0.479
test_array(x, n) # 0.865
test_vectorized(x, n) # 2.906
С сотнями элементов:
x = np.arange(100)
n = 10000
test_direct(x, n) # 0.030
test_array(x, n) # 0.501
test_vectorized(x, n) # 0.670
test_fromiter(x, n) # 0.883
И с тысячами элементов массива или более:
x = np.arange(1000)
n = 1000
test_direct(x, n) # 0.007
test_fromiter(x, n) # 0.479
test_array(x, n) # 0.516
test_vectorized(x, n) # 0.945
Разные версии Python/NumPy и оптимизация компилятора будут иметь разные результаты, поэтому проведите аналогичный тест для вашей среды.
Ответ 4
С тех пор, как на этот вопрос был дан ответ, произошло много событий: вокруг чисел npr, numba и cython. Цель этого ответа - принять во внимание эти возможности.
Но сначала позвольте заявить очевидное: независимо от того, как вы отображаете Python-функцию на массив numpy, она остается функцией Python, что означает для каждой оценки:
- Элемент numpy-array должен быть преобразован в объект Python (например,
Float
). - все вычисления выполняются с Python-объектами, что означает наличие накладных расходов на интерпретатор, динамическую диспетчеризацию и неизменяемые объекты.
Таким образом, какой механизм используется для циклического прохождения массива, не играет большой роли из-за упомянутых выше издержек - он остается намного медленнее, чем использование простой векторизации.
Давайте посмотрим на следующий пример:
# numpy-functionality
def f(x):
return x+2*x*x+4*x*x*x
# python-function as ufunc
import numpy as np
vf=np.vectorize(f)
vf.__name__="vf"
np.vectorize
как представитель класса подходов чисто Python. Используя perfplot
(см. Код в приложении к этому ответу), мы получаем следующее время выполнения:
![enter image description here]()
Мы видим, что numpy-подход в 10-100 раз быстрее, чем версия на чистом Python. Вероятно, снижение производительности при больших размерах массивов связано с тем, что данные больше не помещаются в кэш.
Часто можно услышать, что производительность NumPy настолько хороша, насколько это возможно, потому что это чистый C под капотом. Тем не менее, есть много возможностей для совершенствования!
Векторизованная numpy-версия использует много дополнительной памяти и обращений к памяти. Numexp-library пытается упорядочить numpy-массивы и таким образом получить лучшее использование кэша:
# less cache misses than numpy-functionality
import numexpr as ne
def ne_f(x):
return ne.evaluate("x+2*x*x+4*x*x*x")
Приводит к следующему сравнению:
![enter image description here]()
Я не могу объяснить все на графике выше: вначале мы видим большие издержки для библиотеки numbersxpr, но поскольку она лучше использует кэш, она примерно в 10 раз быстрее для больших массивов!
Другой подход состоит в том, чтобы выполнить jit-компиляцию функции и, таким образом, получить настоящий UFunc на чистом C. Это подход Нумба:
# runtime generated C-function as ufunc
import numba as nb
@nb.vectorize(target="cpu")
def nb_vf(x):
return x+2*x*x+4*x*x*x
Это в 10 раз быстрее, чем оригинальный подход:
![enter image description here]()
Однако задача смущающе распараллеливается, поэтому мы также можем использовать prange
для параллельного вычисления цикла:
@nb.njit(parallel=True)
def nb_par_jitf(x):
y=np.empty(x.shape)
for i in nb.prange(len(x)):
y[i]=x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y
Как и ожидалось, параллельная функция медленнее для небольших входов, но быстрее (почти в 2 раза) для больших размеров:
![enter image description here]()
В то время как numba специализируется на оптимизации операций с numpy-массивами, Cython является более общим инструментом. Извлечь ту же производительность, что и с numba, сложнее - часто она снижается до llvm (numba) по сравнению с локальным компилятором (gcc/MSVC):
%%cython -c=/openmp -a
import numpy as np
import cython
#single core:
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_f(double[::1] x):
y_out=np.empty(len(x))
cdef Py_ssize_t i
cdef double[::1] y=y_out
for i in range(len(x)):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
#parallel:
from cython.parallel import prange
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_par_f(double[::1] x):
y_out=np.empty(len(x))
cdef double[::1] y=y_out
cdef Py_ssize_t i
cdef Py_ssize_t n = len(x)
for i in prange(n, nogil=True):
y[i] = x[i]+2*x[i]*x[i]+4*x[i]*x[i]*x[i]
return y_out
Cython приводит к несколько более медленным функциям:
![enter image description here]()
Заключение
Очевидно, что тестирование только для одной функции ничего не доказывает. Также следует иметь в виду, что для выбранной функции-примера пропускная способность памяти была узким местом для размеров, превышающих 10 ^ 5 элементов - таким образом, мы имели одинаковую производительность для numba, figurexpr и cython в этой области.
Тем не менее, исходя из этого исследования и своего опыта, я бы сказал, что numba, похоже, самый простой инструмент с наилучшими характеристиками.
График рабочего времени с перфлотом -package:
import perfplot
perfplot.show(
setup=lambda n: np.random.rand(n),
n_range=[2**k for k in range(0,24)],
kernels=[
f,
vf,
ne_f,
nb_vf, nb_par_jitf,
cy_f, cy_par_f,
],
logx=True,
logy=True,
xlabel='len(x)'
)
Ответ 5
squares = squarer(x)
Арифметические операции над массивами автоматически применяются элементарно, с эффективными циклами уровня C, которые исключают все служебные данные интерпретатора, которые будут применяться к петле уровня Python или пониманию.
Большинство функций, которые вы хотите применить к элементу NumPy, будут просто работать, хотя некоторые из них могут потребоваться изменения. Например, if
не работает по-разному. Вы хотите преобразовать их в конструкторы, такие как numpy.where
:
def using_if(x):
if x < 5:
return x
else:
return x**2
становится
def using_where(x):
return numpy.where(x < 5, x, x**2)
Ответ 6
Я верю в новую версию (я использую 1.13) numpy, вы можете просто вызвать функцию, передав массив numpy в fuction, который вы написали для скалярного типа, он автоматически применит вызов функции к каждому элементу над массивом numpy и возвратите вам еще один массив numpy
>>> import numpy as np
>>> squarer = lambda t: t ** 2
>>> x = np.array([1, 2, 3, 4, 5])
>>> squarer(x)
array([ 1, 4, 9, 16, 25])
Ответ 7
Кажется, никто не упомянул встроенный фабричный метод получения ufunc
в numpy пакете: np.frompyfunc
который я снова протестировал np.vectorize
и превзошел его примерно на 20-30%. Конечно, он будет работать хорошо, как предписано кодом C или даже numba
(который я не тестировал), но может стать лучшей альтернативой, чем np.vectorize
f = lambda x, y: x * y
f_arr = np.frompyfunc(f, 2, 1)
vf = np.vectorize(f)
arr = np.linspace(0, 1, 10000)
%timeit f_arr(arr, arr) # 307ms
%timeit vf(arr, arr) # 450ms
Я также проверил большие образцы, и улучшение пропорционально. Смотрите документацию также здесь
Ответ 8
Как упоминалось в этом сообщении, просто используйте генераторные выражения, например:
numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)
Ответ 9
Возможно, это напрямую не отвечает на этот вопрос, но я слышал, что numba может скомпилировать существующий код python в параллельные машинные инструкции. Я пересматриваю и пересматриваю этот пост, когда у меня есть шанс использовать его.
Ответ 10
Может быть, лучше использовать vectorize
def square(x):
return x**2
vfunc=vectorize(square)
vfunc([1,2,3,4,5])
output:array([ 1, 4, 9, 16, 25])