Проблема векторизации функции

#python #numpy #vectorization

Вопрос:

Для проекта мне нужно сгенерировать образец из функции. Я хотел бы иметь возможность генерировать эти образцы как можно быстрее.

У меня есть такой пример (в окончательной версии function лямбда будет указана в аргументах) Цель состоит в том, чтобы сгенерировать ys n точек, расположенных xs между start линиями и stop использующих лямбду function .

 def get_ys(coefficients, num_outputs=20, start=0., stop=1.):
    function = lambda x, args: args[0]*(x-args[1])**2   args[2]*(x-args[3])   args[4]
    xs = np.linspace(start, stop, num=num_outputs, endpoint=True)
    ys = [function(x, coefficients) for x in xs]
    return ys
 
 %%time
n = 1000
xs = np.random.random((n,5))
ys = np.apply_along_axis(get_ys, 1, xs)

Wall time: 616 ms
 

Я пытаюсь его векторизовать и обнаружил numpy.apply_along_axis

 %%time
for i in range(1000):
    xs = np.random.random(5)
    ys = get_ys(xs)

Wall time: 622 ms
 

К сожалению, это все еще довольно медленно :/

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

Спасибо!

Изменить: пример ввода/вывода:

 xs = np.ones(5)
ys = get_ys(xs)

[1.0, 0.9501385041551247, 0.9058171745152355, 0.8670360110803323, 0.8337950138504155,0.8060941828254848, 0.7839335180055402, 0.7673130193905817, 0.7562326869806094, 0.7506925207756232, 0.7506925207756232, 0.7562326869806094, 0.7673130193905817, 0.7839335180055401, 0.8060941828254847, 0.8337950138504155,  0.8670360110803323, 0.9058171745152354, 0.9501385041551246, 1.0]
 

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

1. apply_along_axis это удобный способ применения функции, которая принимает только 1d массив к 3d (или большему) массиву. Для 2d проще просто выполнить итерацию по одному измерению. В любом случае это не инструмент для ускорения, и, следовательно, это не то, что мы обычно подразумеваем под «векторизацией». «векторизация» обычно означает переписывание вашей функции таким образом, чтобы она работала с 2d-массивом, используя numpy методы, которые работают с многомерными массивами. Здесь нет короткого пути. Вы должны изучить numpy основы.

2. Иначе говоря — apply все равно звоню get_ys 1000 раз. Он не компилирует его или иным образом не ускоряет его.

3. @Gulzar, он может быть запущен с указанным кодом. например get_ys(np.arange(5)) , возвращает список из 20 значений. Это не вопрос отладки, а скорее вопрос переписывания.

4. @hpaulj О, я этого не знал, я предполагал, что это будет векторизовано, моя ошибка. Я попытался использовать функцию numpy.vectorize on, но она вставляла один элемент за раз вместо строки, поэтому я подумал, что мне нужно использовать эту функцию..

5. np.vectorize передает скалярные значения, а не строки. И в нем есть отказ от ответственности за производительность. apply.... у меня тоже должен быть такой.

Ответ №1:

 def get_ys(coefficients, num_outputs=20, start=0., stop=1.):
    function = lambda x, args: args[0]*(x-args[1])**2   args[2]*(x-args[3])   args[4]
    xs = np.linspace(start, stop, num=num_outputs, endpoint=True)
    ys = [function(x, coefficients) for x in xs]
    return ys
 

Вы пытаетесь обойти вызов get_ys 1000 раз, по одному разу для каждой строки xs .

К чему это приведет, чтобы пройти xs в целом get_ys ? Другими словами, что, если coefficients бы было (n,5) вместо (5,)?

xs есть (20,), и ys будет то же самое (верно)?

Лямбда — это запись, ожидающая скалярного x и (5,) аргументов. Можно ли его изменить для работы с (20,) x и (n,5) args ?

В качестве первого шага, что function производит, если дано xs ? То есть вместо

 ys = [function(x, coefficients) for x in xs]

ys = function(xs, coefficients)
 

Как написано, ваш код повторяет (на медленных скоростях Python) n (1000) строк и 20 linspace . Так function называется 20 000 раз. Вот что делает ваш код медленным.

Давайте попробуем это изменить

Пример выполнения с вашей функцией:

 In [126]: np.array(get_ys(np.arange(5)))
Out[126]: 
array([-2.        , -1.89473684, -1.78947368, -1.68421053, -1.57894737,
       -1.47368421, -1.36842105, -1.26315789, -1.15789474, -1.05263158,
       -0.94736842, -0.84210526, -0.73684211, -0.63157895, -0.52631579,
       -0.42105263, -0.31578947, -0.21052632, -0.10526316,  0.        ])
 

Замените понимание списка всего одним вызовом на function :

 In [127]: def get_ys1(coefficients, num_outputs=20, start=0., stop=1.):
     ...:     function = lambda x, args: args[0]*(x-args[1])**2   args[2]*(x-args[3])   args[4]
     ...: 
     ...:     xs = np.linspace(start, stop, num=num_outputs, endpoint=True)
     ...:     ys = function(xs, coefficients)
     ...:     return ys
     ...: 
     ...: 
 

Те же значения:

 In [128]: get_ys1(np.arange(5))
Out[128]: 
array([-2.        , -1.89473684, -1.78947368, -1.68421053, -1.57894737,
       -1.47368421, -1.36842105, -1.26315789, -1.15789474, -1.05263158,
       -0.94736842, -0.84210526, -0.73684211, -0.63157895, -0.52631579,
       -0.42105263, -0.31578947, -0.21052632, -0.10526316,  0.        ])
 

Сравнительные тайминги:

 In [129]: timeit np.array(get_ys(np.arange(5)))
345 µs ± 16.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
In [130]: timeit get_ys1(np.arange(5))
89.2 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
 

Вот что мы подразумеваем под «векторизацией» — заменой итераций уровня python (понимание списка) эквивалентом, который позволяет более полно использовать методы numpy массива.

Я подозреваю,что мы можем перейти к работе с a (n , 5) coefficients , но этого должно быть достаточно, чтобы вы начали.

полностью векторизованный

По broadcasting (n,5) против (20,) я могу получить функцию, в которой нет никаких циклов python:

 def get_ys2(coefficients, num_outputs=20, start=0., stop=1.):
    function = lambda x, args: args[:,0]*(x-args[:,1])**2   args[:,2]*(x-args[:,3])   args[:,4]
    xs = np.linspace(start, stop, num=num_outputs, endpoint=True)
    ys = function(xs[:,None], coefficients)
    return ys.T
 

И с (1,5) входом:

 In [156]: get_ys2(np.arange(5)[None,:])
Out[156]: 
array([[-2.        , -1.89473684, -1.78947368, -1.68421053, -1.57894737,
        -1.47368421, -1.36842105, -1.26315789, -1.15789474, -1.05263158,
        -0.94736842, -0.84210526, -0.73684211, -0.63157895, -0.52631579,
        -0.42105263, -0.31578947, -0.21052632, -0.10526316,  0.        ]])
 

С вашим тестовым случаем:

 In [146]: n = 1000
     ...: xs = np.random.random((n,5))
     ...: ys = np.apply_along_axis(get_ys, 1, xs)
In [147]: ys.shape
Out[147]: (1000, 20)
 

Два раза:

 In [148]: timeit ys = np.apply_along_axis(get_ys, 1, xs)
     ...: 
106 ms ± 303 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [149]: timeit ys = np.apply_along_axis(get_ys1, 1, xs)
     ...: 
88 ms ± 98.3 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
 

и проверяю это

 In [150]: ys2 = get_ys2(xs)
In [151]: ys2.shape
Out[151]: (1000, 20)
In [152]: np.allclose(ys, ys2)
Out[152]: True
In [153]: timeit ys2 = get_ys2(xs)
424 µs ± 484 ns per loop (mean ± std. dev. of 7 runs, 1000 loops each)
 

Это соответствует значениям и значительно повышает скорость.

В новой функции args теперь может быть (n,5). И если x это (20,1), то результат (20,n), который я переношу на обратном пути.

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

1. Это очень, очень впечатляет, большое вам спасибо! Я все еще пытаюсь понять все, что вы сделали в ответе с трансляцией, транспонированием и созданием новой оси