#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. Это очень, очень впечатляет, большое вам спасибо! Я все еще пытаюсь понять все, что вы сделали в ответе с трансляцией, транспонированием и созданием новой оси