Самый эффективный способ сопоставления функции над массивом 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)',
    )

Ответ 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])