РЕДАКТИРОВАТЬ 3:
Окончательная (я думаю) версия, немного чище и быстрее, включающая идеи из max9111 ответа .
import numpy as np
from numba import as nb
@nb.njit()
def func1_jit(a, b, c, d):
# Precompute
exp_min = 5 - (a + b + c + d)
exp_max = b
exp = 2. ** np.arange(exp_min, exp_max + 1)
fact_e = np.empty((a + b - 2))
fact_e[0] = 1
for ei in range(1, len(fact_e)):
fact_e[ei] = ei * fact_e[ei - 1]
# Loops
B = 0
for ai in range(0, a):
for bi in range(0, b):
for ci in range(0, c):
for di in range(0, d):
for ei in range(0, ai + bi):
for fi in range(0, ci + di):
B += exp[ei - fi - ai - ci - di + 1 - exp_min] * (ei * ei - 2 * (ei * fi) - 7 * di) * fact_e[ei]
return B
Это уже довольно быстро, чем любой из предыдущих вариантов, но мы все еще не используем преимущества нескольких процессоров. Один из способов сделать это - внутри самой функции, например, распараллеливание внешнего цикла. Это добавляет некоторые накладные расходы при каждом вызове для создания потоков, поэтому для небольших входных данных фактически немного медленнее, но должно быть значительно быстрее для больших значений:
import numpy as np
from numba import as nb
@nb.njit(parallel=True)
def func1_par(a, b, c, d):
# Precompute
exp_min = 5 - (a + b + c + d)
exp_max = b
exp = 2. ** np.arange(exp_min, exp_max + 1)
fact_e = np.empty((a + b - 2))
fact_e[0] = 1
for ei in range(1, len(fact_e)):
fact_e[ei] = ei * fact_e[ei - 1]
# Loops
B = np.empty((a,))
for ai in nb.prange(0, a):
Bi = 0
for bi in range(0, b):
for ci in range(0, c):
for di in range(0, d):
for ei in range(0, ai + bi):
for fi in range(0, ci + di):
Bi += exp[ei - fi - ai - ci - di + 1 - exp_min] * (ei * ei - 2 * (ei * fi) - 7 * di) * fact_e[ei]
B[ai] = Bi
return np.sum(B)
Или, если у вас есть много точек, где вы хотите оценить функцию, вы также можете распараллелить на этом уровне. Здесь a_arr
, b_arr
, c_arr
и d_arr
- векторы значений, где должна оцениваться функция:
from numba import as nb
@nb.njit(parallel=True)
def func1_arr(a_arr, b_arr, c_arr, d_arr):
B_arr = np.empty((len(a_arr),))
for i in nb.prange(len(B_arr)):
B_arr[i] = func1_jit(a_arr[i], b_arr[i], c_arr[i], d_arr[i])
return B_arr
Наилучшая конфигурация зависит от ваших входных данных, схемы использования, аппаратного обеспечения и т. Д., Поэтому вы можете комбинировать различные идеи в соответствии с вашими требованиями.
РЕДАКТИРОВАТЬ 2:
На самом деле, забудь, что я говорил раньше. Лучше всего JIT-компилировать алгоритм, но более эффективно. Сначала вычислите дорогие части (я взял экспоненту и факториал), а затем передайте их скомпилированной циклической функции:
import numpy as np
from numba import njit
def func1(a, b, c, d):
exp_min = 5 - (a + b + c + d)
exp_max = b
exp = 2. ** np.arange(exp_min, exp_max + 1)
ee = np.arange(a + b - 2)
fact_e = scipy.special.factorial(ee)
return func1_inner(a, b, c, d, exp_min, exp, fact_e)
@njit()
def func1_inner(a, b, c, d, exp_min, exp, fact_e):
B = 0
for ai in range(0, a):
for bi in range(0, b):
for ci in range(0, c):
for di in range(0, d):
for ei in range(0, ai + bi):
for fi in range(0, ci + di):
B += exp[ei - fi - ai - ci - di + 1 - exp_min] * (ei * ei - 2 * (ei * fi) - 7 * di) * fact_e[ei]
return B
В моих экспериментах это самый быстрый вариант на сегодняшний день, и он требует немного дополнительной памяти (только предварительно вычисленные значения с линейным размером на входе).
a, b, c, d = 4, 6, 3, 4
# The original function
%timeit func1_orig(a, b, c, d)
# 2.07 ms ± 33.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# The grid-evaluated function
%timeit func1_grid(a, b, c, d)
# 256 µs ± 25 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
# The precompuation + JIT-compiled function
%timeit func1_jit(a, b, c, d)
# 19.6 µs ± 3.25 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Ну, всегда есть возможность оценить всю сетку целиком:
import numpy as np
import scipy.special
def func1(a, b, c, d):
ai, bi, ci, di, ei, fi = np.ogrid[:a, :b, :c, :d, :a + b - 2, :c + d - 2]
# Compute
B = (2.) ** (ei - fi - ai - ci - di + 1) * (ei ** 2 - 2 * (ei * fi) - 7 * di) * scipy.special.factorial(ei)
# Mask out of range elements for last two inner loops
m = (ei < ai + bi) & (fi < ci + di)
return np.sum(B * m)
print(func1(4, 6, 3, 4))
# 21769947.844726562
Я использовал scipy.special.factorial
, потому что, по-видимому, np.factorial
по некоторым причинам не работает с массивами.
Очевидно, что стоимость памяти будет расти очень быстро при увеличении параметров. Код на самом деле выполняет больше вычислений, чем необходимо, потому что два внутренних цикла имеют различное количество итераций, поэтому (в этом методе) вам нужно использовать наибольшее, а затем удалить то, что вам не нужно. Надеюсь, что векторизация восполнит это. Небольшой тест IPython:
a, b, c, d = 4, 6, 3, 4
# func1_orig is the original loop-based version
%timeit func1_orig(a, b, c, d)
# 2.9 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
# func1 here is the vectorized version
%timeit func1(a, b, c, d)
# 210 µs ± 6.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
EDIT:
Обратите внимание, что предыдущий подход также не является "все или ничего". Вы можете выбрать для оценки сетки только некоторые из циклов. Например, два самых внутренних цикла могут быть векторизованы так:
def func1(a, b, c, d):
B = 0
e = np.arange(a + b - 2).reshape((-1, 1))
f = np.arange(c + d - 2)
for ai in range(0, a):
for bi in range(0, b):
ei = e[:ai + bi]
for ci in range(0, c):
for di in range(0, d):
fi = f[:ci + di]
B += np.sum((2.) ** (ei - fi - ai - ci - di + 1) * (ei ** 2 - 2 * (ei * fi) - 7 * di) * scipy.special.factorial(ei))
return B
Это все еще имеет циклы, но оно избегает дополнительных вычислений, и требования к памяти намного ниже. Какой из них лучше, зависит от размеров входов, я думаю. В моих тестах с исходными значениями (4, 6, 3, 4) это даже медленнее, чем исходная функция; Кроме того, в этом случае кажется, что создание новых массивов для ei
и fi
в каждом цикле происходит быстрее, чем работа с фрагментом предварительно созданного. Однако, если вы умножите входное значение на 4 (14, 24, 12, 16), то это будет намного быстрее, чем оригинал (около х5), хотя все же медленнее, чем полностью векторизованный (около х3). С другой стороны, я мог бы вычислить значение для входа, масштабированное до десяти (40, 60, 30, 40) с этим (за ~ 5 минут), но не с предыдущим из-за памяти (я не проверял, как долго бы с оригинальной функцией). Использование @numba.jit
немного помогает, хотя и не сильно (не может использовать nopython
из-за функции факториала). Вы можете поэкспериментировать с векторизацией более или менее циклов в зависимости от размера ваших входных данных.