Ефективна оцінка функції в кожній комірці масиву NumPy


124

Враховуючи масив A NumPy A , який найшвидший / найефективніший спосіб застосувати ту саму функцію, f , до кожної комірки?

  1. Припустимо , що ми будемо призначати A (I, J) в F (A (I, J)) .

  2. Функція, f , не має бінарного виводу, тому операції маскування (ing) не допоможуть.

Чи є "очевидною" ітерацією подвійний цикл (через кожну клітинку) оптимальне рішення?


Відповіді:


165

Ви можете просто векторизувати функцію, а потім застосувати її безпосередньо до масиву Numpy кожен раз, коли вам це потрібно:

import numpy as np

def f(x):
    return x * x + 3 * x - 2 if x > 0 else x * 5 + 8

f = np.vectorize(f)  # or use a different name if you want to keep the original f

result_array = f(A)  # if A is your Numpy array

Можливо, краще вказати явний тип виводу безпосередньо при векторизації:

f = np.vectorize(f, otypes=[np.float])

19
Я побоююсь, що векторизована функція не може бути швидшою за "ручну" подвійну циклічну ітерацію та призначення через усі елементи масиву. Тим більше, що він зберігає результат до новоствореної змінної (а не безпосередньо до початкового вводу). Дуже дякую за вашу відповідь :)
Пітер

1
@Peter: Ага, я бачу, що ви згадали про присвоєння результату колишньому масиву у своєму первісному запитанні. Вибачте, що пропустив це під час першого читання. Так, у цьому випадку подвійний цикл повинен бути швидшим. Але ви також пробували одну петлю на сплющеному вигляді масиву? Це може бути трохи швидше, оскільки ви економите невеликий цикл над головою, і Numpy потрібно робити поменше множення та додавання (для обчислення зміщення даних) при кожній ітерації. Плюс він працює для довільно розмірних масивів. Можливо повільніше на дуже малих масивах, тхо.
blubberdiblub

45
Зверніть увагу на попередження, наведене в vectorizeописі функції: Функція vectorize надається насамперед для зручності, а не для продуктивності. Реалізація по суті є циклом. Тож це, швидше за все, зовсім не прискорить процес.
Габріель

Зверніть увагу на те, як vectorizeвизначається тип повернення. Це призвело до помилок. frompyfuncтрохи швидше, але повертає масив об’єктів dtype. Обидва скаляри подачі, а не рядки чи стовпці.
hpaulj

1
@Gabriel Просто введення np.vectorizeмоєї функції (яка використовує RK45) дає мені прискорити коефіцієнт ~ 20.
Suuuehgi



0

Я вважаю, що я знайшов краще рішення. Ідея змінити функцію на універсальну функцію пітона (див. Документацію ), яка може здійснювати паралельні обчислення під кришкою.

Можна написати власний на замовлення ufuncна C, який, безумовно, є більш ефективним, або за допомогою np.frompyfuncвбудованого фабричного методу. Після тестування це більш ефективно, ніж 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 f_arr(arr, arr) # 450ms

Я також протестував більші зразки, і поліпшення пропорційне. Для порівняння результатів інших методів дивіться цей пост


0

Коли 2d-масив (або nd-масив) є C- або F-суміжним, то це завдання відображення функції на 2d-масив практично те саме, що завдання відображення функції на 1d-масив - ми просто повинні бачити це таким чином, наприклад, через np.ravel(A,'K').

Наприклад, тут обговорювались можливі рішення для 1d-масиву .

Однак, коли пам'ять 2d-масиву не є суміжним, тоді ситуація трохи складніша, тому що хотілося б уникнути можливих пропусків кешу, якщо вісь обробляється в неправильному порядку.

Numpy вже має механізм для обробки осей у найкращому порядку. Одна з можливостей використовувати цю техніку - це np.vectorize. Однак документація numpy про np.vectorizeте, що він "надається в першу чергу для зручності, а не для продуктивності" - повільна функція пітона залишається повільною функцією пітона з усіма пов'язаними накладними! Інше питання полягає в його величезному споживанні пам'яті - див., Наприклад, цей SO-пост .

Коли хочеться виконати функцію C, але використовувати машину numpy, хорошим рішенням є використання numba для створення ufuncs, наприклад:

# 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

Він легко перемагає, np.vectorizeале і тоді, коли виконується та сама функція, як множення / додавання чисельного масиву, тобто

# 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"

Додаток до цієї відповіді див. Для коду вимірювання часу:

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

Версія Numba (зелена) приблизно в 100 разів швидша за функцію python (тобто np.vectorize ), що не дивно. Але це також приблизно в 10 разів швидше, ніж numpy-функціональність, оскільки версія numbas не потребує проміжних масивів і, таким чином, використовує кеш ефективніше.


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

Наприклад, для трансцендентних функцій (наприклад exp, sin, cos) Numba не дає жодних - яких переваг по порівнянні з NumPy - х np.exp(немає тимчасових масивів , створених - основне джерело прискорення). Однак моя установка Anaconda використовує VML Intel для векторів більше 8192 - він просто не може це зробити, якщо пам'ять не є суміжною. Тому може бути краще скопіювати елементи в суміжну пам'ять, щоб мати можливість використовувати VML від Intel:

import numba as nb
@nb.vectorize(target="cpu")
def nb_vexp(x):
    return np.exp(x)

def np_copy_exp(x):
    copy = np.ravel(x, 'K')
    return np.exp(copy).reshape(x.shape) 

Для справедливості порівняння я вимкнув паралелізацію VML (див. Код у додатку):

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

Як бачимо, щойно VML запускається, накладні витрати на копіювання більш ніж компенсуються. Однак коли дані стають занадто великими для кешу L3, перевага мінімальна, оскільки завдання знову стає обмеженою пропускною здатністю пам'яті.

З іншого боку, numba також може використовувати SVML від Intel, як це пояснено у цій публікації :

from llvmlite import binding
# set before import
binding.set_option('SVML', '-vector-library=SVML')

import numba as nb

@nb.vectorize(target="cpu")
def nb_vexp_svml(x):
    return np.exp(x)

та використання VML з паралелізацією виходів:

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

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


Список:

А. Порівняння поліномної функції:

import perfplot
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        f,
        vf, 
        nb_vf
        ],
    logx=True,
    logy=True,
    xlabel='len(x)'
    ) 

Б. порівняння exp:

import perfplot
import numexpr as ne # using ne is the easiest way to set vml_num_threads
ne.set_vml_num_threads(1)
perfplot.show(
    setup=lambda n: np.random.rand(n,n)[::2,::2],
    n_range=[2**k for k in range(0,12)],
    kernels=[
        nb_vexp, 
        np.exp,
        np_copy_exp,
        ],
    logx=True,
    logy=True,
    xlabel='len(x)',
    )

0

Усі наведені вище відповіді добре порівнюються, але якщо вам потрібно скористатися спеціальною функцією для відображення, і у вас є numpy.ndarray, і вам потрібно зберегти форму масиву.

Я порівняв лише два, але він збереже форму ndarray. Я використовував масив з 1 мільйоном записів для порівняння. Тут я використовую квадратну функцію. Я представляю загальний випадок n n розмірного масиву. Для двовимірних просто зробіть iterдля 2D.

import numpy, time

def A(e):
    return e * e

def timeit():
    y = numpy.arange(1000000)
    now = time.time()
    numpy.array([A(x) for x in y.reshape(-1)]).reshape(y.shape)        
    print(time.time() - now)
    now = time.time()
    numpy.fromiter((A(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функцію квадратів користувача, скористайтеся будь-яким вибором. Якщо функція залежить від i, j того, чи є індекси масиву, ітерайте розмір масиву, як for ind in range(arr.size), використовуйте, numpy.unravel_indexщоб отримати i, j, ..на основі вашого індексу 1D та форми масиву numpy.unravel_index

Ці відповіді надихають моєю відповіддю на інше тут питання

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