Оптимизация кода-количество вызовов функций в Python
я хотел бы знать, как я мог бы преобразовать эту проблему, чтобы уменьшить накладные расходы np.sum()
вызовы функций в моем коде.
у меня есть input
матрицы, скажем shape=(1000, 36)
. Каждая строка представляет узел на графике. У меня есть операция, которую я делаю, которая повторяет каждую строку и делает элементарное добавление к переменному числу других строк. Эти" другие " строки определяются в словаре nodes_nbrs
записей, для каждой строки в список строк, которые должны быть суммируются вместе. Пример такой:
nodes_nbrs = {0: [0, 1],
1: [1, 0, 2],
2: [2, 1],
...}
здесь, узле 0
будет преобразован в сумму узлов 0
и 1
. Узел 1
будет преобразован в сумму узлов 1
, 0
и 2
. И так далее для остальных узлов.
текущий (и наивный) способ, который я в настоящее время реализовал, как таковой. Сначала я создаю нулевой массив конечной формы, которую я хочу, а затем повторяю каждую пару ключ-значение в nodes_nbrs
словарь:
output = np.zeros(shape=input.shape)
for k, v in nodes_nbrs.items():
output[k] = np.sum(input[v], axis=0)
этот код все круто и отлично в небольших тестах (shape=(1000, 36)
), но на больших тестах (shape=(~1E(5-6), 36)
), для завершения требуется ~2-3 секунды. В конечном итоге мне приходится делать эту операцию тысячи раз, поэтому я пытаюсь увидеть, есть ли более оптимизированный способ сделать это.
после выполнения профилирования линии, я заметил, что ключ убийца называет np.sum
функции снова и снова, которая занимает около 50% от общего времени. Есть ли способ я могу исключить это наверху? Или есть другой способ оптимизировать это?
кроме того, вот список вещей, которые я сделал, и (очень кратко) свои результаты:
- A
cython
версия: исключаетfor
тип петли проверяя накладные расходы, принятое уменьшение 30% во времени. Сcython
версияnp.sum
занимает около 80% от общего времени, а не 50%. - предварительно объявить
np.sum
как переменнаяnpsum
, а затем вызватьnpsum
внутриfor
петли. Никакой разницы с оригиналом. - заменить
np.sum
Сnp.add.reduce
, и назначьте это переменнойnpsum
, а затем вызватьnpsum
внутриfor
петли. ~10% сокращение времени настенных часов, но затем несовместимо сautograd
(объяснение ниже в разреженных матрицах). -
numba
JIT-ing: не пытался больше, чем добавление декоратора. Никаких улучшений, но и не старался. - преобразование
nodes_nbrs
словарь в плотныйnumpy
двоичный массив (1s и 0s), а затем выполните одноnp.dot
операции. Хорошо в теории, плохо на практике, потому что для этого потребуется квадратная матрицаshape=(10^n, 10^n)
, который квадратичен в использовании памяти.
вещи, которые я не пробовал, но не решаюсь сделать так:
-
scipy
разреженные матрицы: я используюautograd
, который не поддерживает автоматическое дифференцированиеdot
операцияscipy
негусто матрицы.
для тех, кто интересуется, это по сути операция свертки на графовых данных. Забавно разрабатывать это для аспирантуры, но также несколько разочаровывает быть на переднем крае знаний.
2 ответов
если scipy.sparse-это не вариант, один из способов приблизиться к этому-массировать данные, чтобы вы могли использовать векторизованные функции для всего в скомпилированном слое. Если вы измените словарь соседей на двумерный массив с соответствующими флагами для отсутствующих значений, вы можете использовать np.take
для извлечения данных, которые вы хотите, а затем сделать один sum()
звонок.
вот пример что я имею в виду:
import numpy as np
def make_data(N=100):
X = np.random.randint(1, 20, (N, 36))
connections = np.random.randint(2, 5, N)
nbrs = {i: list(np.random.choice(N, c))
for i, c in enumerate(connections)}
return X, nbrs
def original_solution(X, nbrs):
output = np.zeros(shape=X.shape)
for k, v in nbrs.items():
output[k] = np.sum(X[v], axis=0)
return output
def vectorized_solution(X, nbrs):
# Make neighbors all the same length, filling with -1
new_nbrs = np.full((X.shape[0], max(map(len, nbrs.values()))), -1, dtype=int)
for i, v in nbrs.items():
new_nbrs[i, :len(v)] = v
# add a row of zeros to X
new_X = np.vstack([X, 0 * X[0]])
# compute the sums
return new_X.take(new_nbrs, 0).sum(1)
теперь мы можем подтвердить что результаты игры:
>>> X, nbrs = make_data(100)
>>> np.allclose(original_solution(X, nbrs),
vectorized_solution(X, nbrs))
True
и мы можем все увидеть ускорение:
X, nbrs = make_data(1000)
%timeit original_solution(X, nbrs)
%timeit vectorized_solution(X, nbrs)
# 100 loops, best of 3: 13.7 ms per loop
# 100 loops, best of 3: 1.89 ms per loop
переход к большим размерам:
X, nbrs = make_data(100000)
%timeit original_solution(X, nbrs)
%timeit vectorized_solution(X, nbrs)
1 loop, best of 3: 1.42 s per loop
1 loop, best of 3: 249 ms per loop
это примерно в 5-10 раз быстрее, который может быть достаточно хорошо для ваших целей (хотя это будет сильно зависеть от точных характеристик nbrs
словарь).
Edit: просто для удовольствия я попробовал несколько других подходов, один из которых использует numpy.add.reduceat
, С помощью pandas.groupby
, и с помощью scipy.sparse
. Кажется, что векторизованный подход, который я первоначально предложил выше, является, вероятно, лучшим выбором. Вот они, для справки:
from itertools import chain
def reduceat_solution(X, nbrs):
ind, j = np.transpose([[i, len(v)] for i, v in nbrs.items()])
i = list(chain(*(nbrs[i] for i in ind)))
j = np.concatenate([[0], np.cumsum(j)[:-1]])
return np.add.reduceat(X[i], j)[ind]
np.allclose(original_solution(X, nbrs),
reduceat_solution(X, nbrs))
# True
-
import pandas as pd
def groupby_solution(X, nbrs):
i, j = np.transpose([[k, vi] for k, v in nbrs.items() for vi in v])
return pd.groupby(pd.DataFrame(X[j]), i).sum().values
np.allclose(original_solution(X, nbrs),
groupby_solution(X, nbrs))
# True
-
from scipy.sparse import csr_matrix
from itertools import chain
def sparse_solution(X, nbrs):
items = (([i]*len(col), col, [1]*len(col)) for i, col in nbrs.items())
rows, cols, data = (np.array(list(chain(*a))) for a in zip(*items))
M = csr_matrix((data, (rows, cols)))
return M.dot(X)
np.allclose(original_solution(X, nbrs),
sparse_solution(X, nbrs))
# True
и все тайминги вместе:
X, nbrs = make_data(100000)
%timeit original_solution(X, nbrs)
%timeit vectorized_solution(X, nbrs)
%timeit reduceat_solution(X, nbrs)
%timeit groupby_solution(X, nbrs)
%timeit sparse_solution(X, nbrs)
# 1 loop, best of 3: 1.46 s per loop
# 1 loop, best of 3: 268 ms per loop
# 1 loop, best of 3: 416 ms per loop
# 1 loop, best of 3: 657 ms per loop
# 1 loop, best of 3: 282 ms per loop
на основе работы над последними редкими вопросами, например чрезвычайно медленная операция строки суммы в разреженной матрице LIL в Python
вот как ваша проблема может быть решена с помощью разреженных матриц. Этот метод можно применить и к плотным. Идея в том, что разреженный sum
реализовано как матричное произведение со строкой (или столбцом) 1s. Индексирование разреженных матриц происходит медленно,но матричное произведение является хорошим кодом C.
в этом случае я собираюсь построить матрица умножения, которая имеет 1s для строк, которые я хочу суммировать-различный набор 1s для каждой записи в словаре.
пример матрицы:
In [302]: A=np.arange(8*3).reshape(8,3)
In [303]: M=sparse.csr_matrix(A)
словарь выбор:
In [304]: dict={0:[0,1],1:[1,0,2],2:[2,1],3:[3,4,7]}
создайте разреженную матрицу из этого словаря. Возможно, это не самый эффективный способ построения такой матрицы, но этого достаточно, чтобы продемонстрировать идею.
In [305]: r,c,d=[],[],[]
In [306]: for i,col in dict.items():
c.extend(col)
r.extend([i]*len(col))
d.extend([1]*len(col))
In [307]: r,c,d
Out[307]:
([0, 0, 1, 1, 1, 2, 2, 3, 3, 3],
[0, 1, 1, 0, 2, 2, 1, 3, 4, 7],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
In [308]: idx=sparse.csr_matrix((d,(r,c)),shape=(len(dict),M.shape[0]))
выполните сумму и посмотрите на результат (как плотный array):
In [310]: (idx*M).A
Out[310]:
array([[ 3, 5, 7],
[ 9, 12, 15],
[ 9, 11, 13],
[42, 45, 48]], dtype=int32)
вот оригинал для сравнения.
In [312]: M.A
Out[312]:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17],
[18, 19, 20],
[21, 22, 23]], dtype=int32)