Найефективніший спосіб відображення функції на масиві numpy


337

Що є найефективнішим способом відображення функції через масивний масив? Я це робив у своєму поточному проекті таким чином:

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.

Чи можемо ми зробити краще?


10
чому б не "квадрати = x ** 2"? Чи є у вас набагато складніша функція, яку потрібно оцінити?
градуси

4
А як щодо squarer(x)?
Життя

1
Можливо, це не відповідає прямо на питання, але я чув, що numba може компілювати існуючий код python в паралельні інструкції на машині. Я перегляну та перегляну цю посаду, коли у мене буде справді можливість скористатися цим.
把 友情 留 在 无 盐

x = np.array([1, 2, 3, 4, 5]); x**2твори
Shark Deng

Відповіді:


281

Я протестував всі запропоновані методи плюс 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 підтримує Моніку

2
Крім того, ваш сюжет не підтримує вашу претензію на vf = np.vectorize(f); y = vf(x)виграш за короткий внесок.
user2357112 підтримує Моніку

Після установки 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])

36
Це не більш ефективно.
user2357112 підтримує Моніку

78
Від цього документа: The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop. В інших питаннях я виявив, що vectorizeможе подвоїти швидкість ітерації користувача. Але реальна швидкість відбувається з numpyопераціями реального масиву.
hpaulj

2
Зауважте, що vectorize дійсно примушує роботу працювати для не-1d масивів
Ерік

Але squarer(x)вже працював би для не 1d масивів. vectorizeтільки справді є якась перевага перед розумінням списку (як те, що йдеться у питанні), а не над squarer(x).
user2357112 підтримує Моніку

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

З 100 елементами:

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

І з 1000 елементами масиву або більше:

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 підтримує Моніку

4
А як бути, якщо fє 2 змінні, а масив 2D?
Сигур

2
Мене бентежить, як версія "f (x)" ("пряма") насправді вважається порівнянною, коли ОП запитувала, як "відобразити" функцію через масив? У випадку f (x) = x ** 2 ** виконується нумером на всьому масиві, а не на основі елемента. Наприклад, якщо f (x) - "лямбда x: x + x", то відповідь сильно відрізняється, оскільки numpy об'єднує масиви замість того, щоб робити додавання для кожного елемента. Це дійсно призначене порівняння? Будь ласка, поясніть.
Ендрю Мелінгер

49

Навколо є numexpr , numba та cython , метою цієї відповіді є врахування цих можливостей.

Але спершу давайте констатуємо очевидне: незалежно від того, як відображати функцію Python на масиві numpy, вона залишається функцією Python, що означає для кожної оцінки:

  • елемент numpy-масив повинен бути перетворений на об'єкт Python (наприклад, a 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вибирається як представник класу функцій чистого пітона. Використовуючи perfplot(див. Код у додатку цієї відповіді), ми отримуємо такі тривалість виконання:

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

Ми можемо бачити, що numpy-підхід на 10x-100x швидший, ніж у чистому варіанті python. Зниження продуктивності для більших розмірів масиву, мабуть, пояснюється тим, що дані більше не підходять до кешу.

Варто також зазначити, що vectorizeтакож використовується багато пам'яті, тому часто використання пам'яті - це шийка (див. Пов’язане SO-питання ). Також зауважте, що документація numpy про np.vectorizeте, що вона "надається в першу чергу для зручності, а не для виконання".

Інші інструменти слід використовувати, коли потрібна продуктивність, окрім написання розширення C з нуля, є такі можливості:


Часто можна почути, що продуктивність настільки ж хороша, наскільки це виходить, адже це чисто C під кришкою. І все ж є багато можливостей для вдосконалення!

Векторизована numpy-версія використовує багато додаткової пам'яті та доступу до пам'яті. Бібліотека Numexp намагається встановити рядки 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")

Приводить до такого порівняння:

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

Я не можу пояснити все на сюжеті вище: ми можемо побачити більший накладний обсяг для бібліотеки numexpr на початку, але оскільки він краще використовує кеш, це приблизно в 10 разів швидше для великих масивів!


Інший підхід полягає в тому, щоб jit-компілювати функцію і таким чином отримати справжній чистий-C UFunc. Це підхід numba:

# 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 разів швидше, ніж оригінальний підхід:

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


Однак завдання є незручно паралельним, таким чином ми також могли б використати 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, numexpr та cython в цьому регіоні.

Зрештою, остаточна відповідь залежить від типу функції, апаратного забезпечення, розподілу Python та інших факторів. Наприклад, Anaconda-дистрибуція використовує VML від Intel для функцій numpy і, таким чином, перевершує numba (якщо тільки він не використовує SVML, дивіться цю публікацію SO ) легко для трансцендентних функцій , такі як exp, sin, cosі аналогічним - дивіться , наприклад , наступний SO-пост .

Проте, з цього дослідження та з мого досвіду, я зазначив, що numba, здається, є найпростішим інструментом з найкращою продуктивністю, доки не задіяні трансцендентні функції.


Складання графіків роботи за допомогою пакету perfplot:

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)

8

Я вірю, що в новій версії (я використовую 1.13) numpy ви можете просто викликати функцію, передавши numpy масив в fuction, який ви написали для скалярного типу, він автоматично застосує виклик функції до кожного елемента через 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
Це далеко не нове - це було завжди - це одна з основних особливостей нумету.
Ерік

8
Це **оператор , який застосовуючи обчислення для кожного елемента т t. Це звичайний нуд. Загортання його lambdaне робить нічого зайвого.
hpaulj

Це не спрацьовує, якщо показано заяви, як зараз.
TriHard8

8

У багатьох випадках numpy.apply_along_axis стане найкращим вибором. Це збільшує продуктивність приблизно в 100 разів порівняно з іншими підходами - і не тільки для тривіальних тестових функцій, але і для складніших композиційних функцій з numpy та scipy.

Коли я додаю метод:

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

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


Я надзвичайно шокований тим фактом, що більшість людей, здається, не усвідомлюють цього простого, масштабованого та вбудованого непродуманого протягом стількох років ....
Білл Хуан

7

Здається, ніхто не згадав про вбудований заводський метод виготовлення ufuncв нумерованому пакеті: np.frompyfuncякий я перевірив ще раз np.vectorizeі перевершив його приблизно на 20 ~ 30%. Звичайно, він буде добре працювати зі встановленим кодом С або навіть 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%
Julian - BrainAnnex.org

2

Як згадувалося в цій публікації , просто використовуйте генераторні вирази на зразок:

numpy.fromiter((<some_func>(x) for x in <something>),<dtype>,<size of something>)

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чудові роботи, враховуючи простий підхід, і якщо є вбудована функція, будь ласка, використовуйте це.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.