Оптимизация кода-количество вызовов функций в 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)