Оператор If/else против функции Heaviside

#python-3.x #performance #numpy #if-statement #optimization

Вопрос:

В моем коде я должен учитывать различные вклады в отношении разных пороговых значений. В частности, у меня есть функция my_index , вывод которой должен быть сопоставлен с пороговыми Z_1, Z_2, Z_3 значениями, чтобы определить приращение к переменной my_value . В следующем MWE, для простоты, функция my_index является просто однородным генератором случайных чисел:

 import numpy as np

my_len = 100000
Z_1 = 0.2
Z_2 = 0.4
Z_3 = 0.7

first = 1

second = 2

third = -0.0003

my_value = 0

for i in range(my_len):

  my_index = np.random.uniform()

  my_value  = first*np.heaviside(my_index - Z_1,0)*np.heaviside(Z_2 - my_index,0)   second*np.heaviside(my_index - Z_3,0)   third*np.heaviside(Z_3 - my_index,0)

#if Z_1 < my_index < Z_2 add first
#if my_index > Z_3 add second
#if my_index < Z_3 add third
 

Я заменил if/else те, которые могли бы использоваться для порогов с помощью функции Heaviside, см.. Имейте в виду, что в моем исходном коде этот раздел кода должен быть повторен до 10^5 раз.

Мой вопрос в следующем: делает ли эта практика код быстрее? Или вызов функции heaviside ( np.heaviside ) лучше с точки зрения скорости, чем управление if/else?

Комментарии:

1. Что вы обнаружили, когда протестировали его?

Ответ №1:

 In [433]: x=np.arange(-10,10)
In [434]: x
Out[434]: 
array([-10,  -9,  -8,  -7,  -6,  -5,  -4,  -3,  -2,  -1,   0,   1,   2,
         3,   4,   5,   6,   7,   8,   9])
 

Правильное использование heaviside — предоставление x в виде массива, а не одного значения:

 In [436]: np.heaviside(x,.5)
Out[436]: 
array([0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.5, 1. , 1. ,
       1. , 1. , 1. , 1. , 1. , 1. , 1. ])
 

Эквивалент понимания списка:

 In [437]: [.5 if i==0 else (0 if i<0 else 1) for i in x]
Out[437]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.5, 1, 1, 1, 1, 1, 1, 1, 1, 1]
 

и создание массива из этого списка:

 In [438]: np.array([.5 if i==0 else (0 if i<0 else 1) for i in x])
Out[438]: 
array([0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0. , 0.5, 1. , 1. ,
       1. , 1. , 1. , 1. , 1. , 1. , 1. ])
 

Сравните времена:

 In [439]: timeit np.heaviside(x,.5)
2.5 µs ± 17.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [440]: timeit np.array([.5 if i==0 else (0 if i<0 else 1) for i in x])
15.1 µs ± 25.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Итерация по списку выполняется быстрее (чем по массиву).:

 In [441]: timeit np.array([.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()])
6.66 µs ± 195 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

и если мы пропустим преобразование обратно в список:

 In [442]: timeit [.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()]
2.28 µs ± 3.01 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Для гораздо большего массива heaviside производительность еще выше:

 In [445]: x=np.arange(-1000,1000)
In [446]: timeit [.5 if i==0 else (0 if i<0 else 1) for i in x.tolist()]
211 µs ± 7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [447]: timeit np.heaviside(x,.5)
13 µs ± 201 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Для генерации случайных чисел также быстрее использовать подход с использованием всего массива:

 In [448]: timeit [np.random.uniform() for _ in range(1000)]
4.62 ms ± 20.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [449]: timeit np.random.uniform(1000)
4.74 µs ± 171 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
 

Я также мог бы рассчитать время скалярного использования heaviside — это хуже, чем if/else в [446]:

 In [450]: timeit [np.heaviside(i,.5) for i in x]
8.64 ms ± 44.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
 

В сумме:

  • используйте код всего массива, где это возможно
  • при использовании итерации на уровне Python вместо этого используйте списки и скалярные методы

Комментарии:

1. Я замечаю, что для больших массивов хевисайд работает лучше. А как насчет программирования без ветвей?

Ответ №2:

Предполагая, что вы используете стандартный интерпретатор CPython, то выполнение подобного вызова функции Numpy np.heaviside , скорее всего, будет дороже, чем выполнение базовых условий. Однако и то, и другое очень неэффективно. Действительно, условные обозначения, как правило, работают медленно и могут быть заменены здесь реализацией без ветвей (добавление/умножение логических значений, преобразованных в целые числа). Наиболее важной оптимизацией является использование векторизации, поскольку Numpy спроектирован так, чтобы быть эффективным для относительно больших массивов, а не для скалярных значений (в основном из-за дополнительных внутренних проверок и вызовов функций). Вы можете сгенерировать все случайные значения в большом массиве, применить к нему функцию хевисайда несколько раз. Полученный код, безусловно, будет на 2 или 3 порядка быстрее!

Комментарии:

1. Это MWE, в котором я рассматривал индикатор просто как случайный параметр. Моя проблема не может быть векторизована. Однако реальной альтернативой является программирование без ветвей. Не могли бы вы, пожалуйста, предоставить реализацию программы без ветвей в этом MWE?