Самый эффективный способ отобразить функцию на массив

338

Каков наиболее эффективный способ отобразить функцию на массиве? В моем текущем проекте я делал это следующим образом:

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, прежде чем преобразовывать его обратно в пустой массив.

Можем ли мы сделать лучше?

Райан
источник
10
почему не "квадраты = х ** 2"? У вас есть намного более сложная функция, которую вы должны оценить?
22 градусов
4
Как насчет только squarer(x)?
Жизнь
1
Может быть, это не прямой ответ на вопрос, но я слышал, что numba может скомпилировать существующий код Python в параллельные машинные инструкции. Я вернусь и пересмотрю этот пост, когда у меня действительно будет возможность использовать это.
把 友情 留 在 无 盐
x = np.array([1, 2, 3, 4, 5]); x**2работы
Акула Денг

Ответы:

283

Я проверил все предложенные методы плюс np.array(map(f, x))с perfplot(мой небольшой проект).

Сообщение № 1: Если вы можете использовать нативные функции numpy, сделайте это.

Если функция , которую вы пытаетесь уже векторизациями в векторной (как , x**2например , в исходном сообщении), используя это гораздо быстрее , чем все остальное (обратите внимание на логарифмическую шкалу):

введите описание изображения здесь

Если вам действительно нужна векторизация, не имеет большого значения, какой вариант вы используете.

введите описание изображения здесь


Код для воспроизведения сюжетов:

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],
    xlabel="len(x)",
)
Нико Шлёмер
источник
7
Вы, кажется, ушли f(x)из вашего заговора. Это может быть неприменимо для каждого f, но оно применимо здесь, и это легко самое быстрое решение, когда применимо.
user2357112 поддерживает Monica
2
Кроме того, ваш сюжет не поддерживает ваше заявление, которое vf = np.vectorize(f); y = vf(x)выигрывает для коротких входов.
user2357112 поддерживает Monica
После установки perfplot (v0.3.2) через pip ( pip install -U perfplot) я вижу сообщение: AttributeError: 'module' object has no attribute 'save'при вставке примера кода.
tsherwen
Как насчет ванили для петли?
Catiger3331
1
@Vlad просто используйте math.sqrt в качестве комментария.
Нико Шломер
138

Как насчет использования 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)
# Output : array([ 1,  4,  9, 16, 25])
satomacoto
источник
36
Это не более эффективно.
user2357112 поддерживает Monica
78
Из этого документа: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. В других вопросах я обнаружил, что это vectorizeможет удвоить скорость итерации пользователя. Но реальное ускорение происходит с реальными numpyоперациями с массивами.
hpaulj
2
Обратите внимание, что векторизация, по крайней мере, заставляет работать не-1d массивы
Eric
Но squarer(x)уже работал бы для не 1-мерных массивов. vectorizeтолько действительно имеет какое-либо преимущество перед пониманием списка (как тот, что в вопросе), а не над squarer(x).
user2357112 поддерживает Monica
79

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 , так как он неэффективен и имеет (или имел) ряд проблем . Если вы работаете с другими типами данных, вы можете изучить другие методы, показанные ниже.

Сравнение методов

Вот несколько простых тестов для сравнения трех методов для сопоставления функции, этот пример используется с 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 и оптимизация компилятора будут иметь разные результаты, поэтому проведите аналогичный тест для вашей среды.

Майк Т
источник
2
Если вы используете countаргумент и выражение генератора, то np.fromiterэто значительно быстрее.
juanpa.arrivillaga
3
Так, например, используйте'np.fromiter((f(xi) for xi in x), x.dtype, count=len(x))'
juanpa.arrivillaga
4
Вы не тестировали прямое решение f(x), которое на порядок превосходит все остальное .
user2357112 поддерживает Monica
4
Что если if fимеет 2 переменные и массив 2D?
Сигур
2
Меня смущает то, как версия 'f (x)' ("direct") на самом деле считается сопоставимой, когда OP спрашивал, как "отобразить" функцию на массив? В случае f (x) = x ** 2 ** выполняется numpy для всего массива, а не для каждого элемента. Например, если f (x) - «лямбда x: x + x», тогда ответ очень отличается, потому что numpy объединяет массивы вместо того, чтобы выполнять добавление для каждого элемента. Это действительно запланированное сравнение? Пожалуйста, объясните.
Andrew Mellinger
49

Вокруг есть числаxpr , numba и cython , цель этого ответа - принять во внимание эти возможности.

Но сначала давайте констатируем очевидное: независимо от того, как вы отображаете Python-функцию на массив numpy, она остается функцией Python, что означает для каждой оценки:

  • элемент numpy-array должен быть преобразован в объект Python (например, Float ).
  • все вычисления выполняются с Python-объектами, что означает наличие накладных расходов на интерпретатор, динамическую диспетчеризацию и неизменяемые объекты.

То, какой механизм используется для циклического прохождения массива, не играет большой роли из-за упомянутых выше издержек - он работает намного медленнее, чем использование встроенной функциональности numpy.

Давайте посмотрим на следующий пример:

# 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(см. Код в приложении к этому ответу), мы получаем следующее время выполнения:

введите описание изображения здесь

Мы можем видеть, что numpy-подход в 10-100 раз быстрее, чем в чистой версии Python. Вероятно, снижение производительности при больших размерах массивов связано с тем, что данные больше не помещаются в кэш.

Стоит также упомянуть, что он vectorizeтакже использует много памяти, поэтому часто использование памяти является узким местом (см. Соответствующий вопрос SO ). Также обратите внимание, что в документации numpy np.vectorizeговорится, что она «предоставляется в основном для удобства, а не для производительности».

При желании использовать другие инструменты, кроме написания C-расширения с нуля, существуют следующие возможности:


Часто можно услышать, что производительность 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")

Приводит к следующему сравнению:

введите описание изображения здесь

Я не могу объяснить все на графике выше: вначале мы видим большие издержки для библиотеки 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 раз быстрее, чем оригинальный numpy-подход:

введите описание изображения здесь


Однако задача смущающе распараллеливается, поэтому мы также можем использовать ее 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 раза) для больших размеров:

введите описание изображения здесь


В то время как 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 приводит к несколько более медленным функциям:

введите описание изображения здесь


Вывод

Очевидно, что тестирование только для одной функции ничего не доказывает. Также следует помнить, что для выбранной функции-примера пропускная способность памяти была узким местом для размеров, превышающих 10 ^ 5 элементов - таким образом, мы имели одинаковую производительность для numba, figurexpr и cython в этой области.

В конце концов, окончательный ответ зависит от типа функции, аппаратного обеспечения, Python-распределения и других факторов. Например , Анаконда-распределение использует Intel, VML для функций Numpy и , таким образом , превосходит по Numba (если он не использует SVML, увидеть этот SO-пост ) легко для трансцендентных функций , такие как exp, sin, cosи аналогичного - смотрите , например , следующий SO-пост .

Тем не менее, исходя из этого исследования и моего опыта, я до сих пор утверждаю, что нумба кажется самым простым инструментом с наилучшими характеристиками, если не задействованы трансцендентные функции.


График времени прохождения с перфлот-пакетом :

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)'
    )
Свинец
источник
1
Numba может использовать Intel SVML обычно, что приводит к довольно сравнимым временным интервалам по сравнению с Intel VML, но в версии с ошибками (0.43-0.47). Я добавил график производительности stackoverflow.com/a/56939240/4045774 для сравнения с вашим cy_expsum.
max9111
29
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)
user2357112 поддерживает Monica
источник
9

Во многих случаях numpy.apply_along_axis будет лучшим выбором. Это увеличивает производительность примерно в 100 раз по сравнению с другими подходами - и не только для тривиальных функций тестирования, но также и для более сложных композиций функций из numpy и scipy.

Когда я добавляю метод:

def along_axis(x):
    return np.apply_along_axis(f, 0, x)

к коду perfplot я получаю следующие результаты: введите описание изображения здесь

LyteFM
источник
Я чрезвычайно шокирован тем фактом, что большинство людей, кажется, не знают об этом простом, масштабируемом и встроенном без труда в течение многих лет ....
Билл Хуанг,
8

Я верю, что в более новой версии (я использую 1.13) numpy вы можете просто вызвать функцию, передав массив numpy функции, которую вы написали для скалярного типа, она автоматически применит вызов функции к каждому элементу массива 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])
Пейти Ли
источник
3
Это далеко не ново - так было всегда - это одна из основных особенностей Numpy.
Эрик
8
Это **оператор , который применяя вычисление для каждого элемента т t. Это обычный NumPy. Заворачивание в lambdaничего лишнего не делает.
hpaulj
Это не работает с утверждениями if, как показано в данный момент.
TriHard8
7

Кажется, никто не упомянул о встроенном заводском методе производства ufuncв упаковках с кусочками, 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

Я также проверил большие образцы, и улучшение пропорционально. Смотрите документацию также здесь

Вундербар
источник
1
Я повторил вышеупомянутые тесты синхронизации, а также обнаружил, что повышение производительности (по сравнению с np.vectorize) составляет около 30%
Джулиан - BrainAnnex.org
2

Как уже упоминалось в этом посте , просто используйте выражения генератора следующим образом:

numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)
Bannana
источник
2

Все приведенные выше ответы хорошо сравниваются, но если вам нужно использовать пользовательскую функцию для отображения, и у вас есть numpy.ndarray , и вам нужно сохранить форму массива.

У меня есть только два сравнения, но оно сохранит форму ndarray. Я использовал массив с 1 млн. Записей для сравнения. Здесь я использую квадратную функцию, которая также встроена в numpy и имеет большой прирост производительности, поскольку там, где это было необходимо, вы можете использовать функцию по вашему выбору.

import numpy, time
def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([x * x for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((x * x for x in y.reshape(-1)), y.dtype).reshape(y.shape)
    print(time.time() - now)
    now = time.time()
    numpy.square(y)  
    print(time.time() - now)

Вывод

>>> timeit()
1.162431240081787    # list comprehension and then building numpy array
1.0775556564331055   # from numpy.fromiter
0.002948284149169922 # using inbuilt function

Здесь вы можете ясно видеть, что numpy.fromiterработает замечательно, учитывая простой подход, и если доступна встроенная функция, пожалуйста, используйте это.

Rushikesh
источник