Поэлементный максимум разреженной матрицы Scipy и вектора с трансляцией

#python #numpy #scipy #sparse-matrix #elementwise-operations

#python #numpy #scipy #разреженная матрица #поэлементные операции

Вопрос:

Мне нужен быстрый поэлементный максимум, который сравнивает каждую строку разреженной матрицы scipy размером n на m по элементам с разреженной матрицей размером 1 на m. Это отлично работает в Numpy с использованием np.maximum(mat, vec) широковещательной передачи via Numpy.

Однако у Scipy .maximum() нет широковещательной передачи. Моя матрица большая, поэтому я не могу преобразовать ее в массив numpy.

Мой текущий обходной путь заключается в циклическом переборе множества строк mat mat[row,:].maximum(vec) . Этот большой цикл снижает эффективность моего кода (это нужно делать много раз). Мое медленное решение находится во втором фрагменте кода ниже — есть ли лучшее решение?

 # Example
import numpy as np
from scipy import sparse

mat = sparse.csc_matrix(np.arange(12).reshape((4,3)))

vec = sparse.csc_matrix([-1, 5, 100])

# Numpy's np.maximum() gives the **desired result** using broadcasting (but it can't handle sparse matrices):
numpy_result = np.maximum( mat.toarray(), vec.toarray() )
print( numpy_result )
# [[  0   5 100]
#  [  3   5 100]
#  [  6   7 100]
#  [  9  10 100]]

# Scipy only compares the top row of mat to vec (no broadcasting!):
scipy_result = mat.maximum(vec)
print( scipy_result.toarray() )
# [[  0   5 100]
#  [  3   4   5]
#  [  6   7   8]
#  [  9  10  11]]

#Reversing the order of mat and vec in the call to vec.maximum(mat) results in a single row output, and also frequently seg faults (!):
  

Увеличенный пример и текущее решение для тестирования скорости

 import numpy as np
from scipy import sparse
import timeit

mat = sparse.csc_matrix(  sparse.random(20000, 4000, density=.01, data_rvs=lambda s: np.random.randint(0, 5000, size=s))  )

vec = sparse.csc_matrix(  sparse.random(1, 4000, density=.01, data_rvs=lambda s: np.random.randint(0, 5000, size=s))  )

def sparse_elementwise_maximum(mat, vec):
    output = sparse.lil_matrix(mat.shape)
    for row_idx in range( mat.shape[0] ):
        output[row_idx] = mat[row_idx,:].maximum(vec)
    return output

# Time it
num_timing_loops = 3.0
starttime = timeit.default_timer()
for _ in range(int(num_timing_loops)):
    sparse_elementwise_maximum(mat, vec)
print('time per call is:', (timeit.default_timer() - starttime)/num_timing_loops, 'seconds')
# 15 seconds per call (way too slow!)
  

Редактировать
Я принимаю ответ Макса, поскольку вопрос касался конкретно высокопроизводительного решения, а решение Max предлагает огромные ускорения в 1000-2500 раз для различных входных данных, которые я пробовал, за счет большего количества строк кода и компиляции Numba. Однако для общего использования однострочник Даниэля Ф. — отличное решение, предлагающее ускорение в 10-50 раз на примерах, которые я пробовал — я, вероятно, буду использовать для многих других вещей.

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

1. Хорошо, странно, делать это с csr_matrix или coo_matrix сбой моего ядра. csc_matrix работает, хотя. Кто-нибудь может повторить?

2. Да — просто отредактировано, чтобы сделать их csc_matrix, чтобы пример мог работать для других. У меня также были некоторые сбои при использовании csr_matrix с функцией .maximum() (в частности, ошибки seg).

3. Хорошо, возможно, потребуется несколько внутренних мастеров, чтобы выяснить, что происходит в скомпилированном scipy коде. Наберитесь терпения, одному из них может потребоваться некоторое время, чтобы увидеть это.

4. Итак, насколько я могу судить, каждая форма разреженной матрицы scipy отличается от csc_matrix возврата к csr_matrix for doing maximum , так что, по крайней мере, это только одна основная причина. Я не могу следовать функции cs_matrix ‘s _maxumum_minimum_ , или как maximum она ее вызывает. Похоже, что if dense_check(other): этот вызов должен вызывать a ValueError , поскольку это массив. Может быть, там что-то закорочено

5. @max9111 Re: разреженность, она варьируется, но разумные значения составляют от 0,001 до 0,005 доли ненулевых элементов.

Ответ №1:

Низкоуровневый подход

Как всегда, вы можете подумать о том, как создается правильный формат разреженной матрицы для этой операции, для csr-матриц основными компонентами являются shape, data_arr, indexes и ind_ptr. С этими частями объекта scipy.sparse.csr это довольно просто, но, возможно, требует немного времени для реализации эффективного алгоритма на скомпилированном языке (C, C , Cython, Python-Numba). В своей реализации я использовал Numba, но перенос его на C должен быть легко возможен (изменения синтаксиса) и, возможно, избежать нарезки.

Реализация (первая попытка)

 import numpy as np
import numba as nb

# get all needed components of the csr object and create a resulting csr object at the end
def sparse_elementwise_maximum_wrap(mat,vec):
    mat_csr=mat.tocsr()
    vec_csr=vec.tocsr()

    shape_mat=mat_csr.shape
    indices_mat=mat_csr.indices
    indptr_mat=mat_csr.indptr
    data_mat=mat_csr.data
    indices_vec=vec_csr.indices
    data_vec=vec_csr.data

    res=sparse_elementwise_maximum_nb(indices_mat,indptr_mat,data_mat,shape_mat,indices_vec,data_vec)
    res=sparse.csr_matrix(res, shape=shape_mat)
    return res

@nb.njit(cache=True)
def sparse_elementwise_maximum_nb(indices_mat,indptr_mat,data_mat,shape_mat,vec_row_ind,vec_row_data):
    data_res=[]
    indices_res=[]
    indptr_mat_res=[]

    indptr_mat_=0
    indptr_mat_res.append(indptr_mat_)

    for row_idx in range(shape_mat[0]):
        mat_row_ind=indices_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]
        mat_row_data=data_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]

        mat_ptr=0
        vec_ptr=0
        while mat_ptr<mat_row_ind.shape[0] and vec_ptr<vec_row_ind.shape[0]:
            ind_mat=mat_row_ind[mat_ptr]
            ind_vec=vec_row_ind[vec_ptr]

            #value for both matrix and vector is present
            if ind_mat==ind_vec:
                data_res.append(max(mat_row_data[mat_ptr],vec_row_data[vec_ptr]))
                indices_res.append(ind_mat)
                mat_ptr =1
                vec_ptr =1
                indptr_mat_ =1

            #only value for the matrix is present vector is assumed 0
            elif ind_mat<ind_vec:
                if mat_row_data[mat_ptr] >0:
                    data_res.append(mat_row_data[mat_ptr])
                    indices_res.append(ind_mat)
                    indptr_mat_ =1
                mat_ptr =1

            #only value for the vector is present matrix is assumed 0
            else:
                if vec_row_data[vec_ptr] >0:
                    data_res.append(vec_row_data[vec_ptr])
                    indices_res.append(ind_vec)
                    indptr_mat_ =1
                vec_ptr =1

        for i in range(mat_ptr,mat_row_ind.shape[0]):
            if mat_row_data[i] >0:
                data_res.append(mat_row_data[i])
                indices_res.append(mat_row_ind[i])
                indptr_mat_ =1
        for i in range(vec_ptr,vec_row_ind.shape[0]):
            if vec_row_data[i] >0:
                data_res.append(vec_row_data[i])
                indices_res.append(vec_row_ind[i])
                indptr_mat_ =1
        indptr_mat_res.append(indptr_mat_)

    return np.array(data_res),np.array(indices_res),np.array(indptr_mat_res)
  

Реализация (оптимизирована)

При таком подходе списки заменяются динамически изменяемым массивом. Я увеличил размер выходных данных с шагом 60 МБ. При создании csr-объекта также не создается копия данных, только ссылки. Если вы хотите избежать перегрузки памяти, вам нужно скопировать массивы в конце.

 @nb.njit(cache=True)
def sparse_elementwise_maximum_nb(indices_mat,indptr_mat,data_mat,shape_mat,vec_row_ind,vec_row_data):
    mem_step=5_000_000
    #preallocate memory for 5M non-zero elements (60 MB in this example)
    data_res=np.empty(mem_step,dtype=data_mat.dtype)
    indices_res=np.empty(mem_step,dtype=np.int32)
    data_res_p=0

    indptr_mat_res=np.empty((shape_mat[0] 1),dtype=np.int32)
    indptr_mat_res[0]=0
    indptr_mat_res_p=1
    indptr_mat_=0

    for row_idx in range(shape_mat[0]):
        mat_row_ind=indices_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]
        mat_row_data=data_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]

        #check if resizing is necessary
        if data_res.shape[0]<data_res_p shape_mat[1]:
            #add at least memory for another mem_step elements
            size_to_add=mem_step
            if shape_mat[1] >size_to_add:
                size_to_add=shape_mat[1]

            data_res_2   =np.empty(data_res.shape[0]    size_to_add,data_res.dtype)
            indices_res_2=np.empty(indices_res.shape[0] size_to_add,indices_res.dtype)
            for i in range(data_res_p):
                data_res_2[i]=data_res[i]
                indices_res_2[i]=indices_res[i]
            data_res=data_res_2
            indices_res=indices_res_2

        mat_ptr=0
        vec_ptr=0
        while mat_ptr<mat_row_ind.shape[0] and vec_ptr<vec_row_ind.shape[0]:
            ind_mat=mat_row_ind[mat_ptr]
            ind_vec=vec_row_ind[vec_ptr]

            #value for both matrix and vector is present
            if ind_mat==ind_vec:
                data_res[data_res_p]=max(mat_row_data[mat_ptr],vec_row_data[vec_ptr])
                indices_res[data_res_p]=ind_mat
                data_res_p =1
                mat_ptr =1
                vec_ptr =1
                indptr_mat_ =1

            #only value for the matrix is present vector is assumed 0
            elif ind_mat<ind_vec:
                if mat_row_data[mat_ptr] >0:
                    data_res[data_res_p]=mat_row_data[mat_ptr]
                    indices_res[data_res_p]=ind_mat
                    data_res_p =1
                    indptr_mat_ =1
                mat_ptr =1

            #only value for the vector is present matrix is assumed 0
            else:
                if vec_row_data[vec_ptr] >0:
                    data_res[data_res_p]=vec_row_data[vec_ptr]
                    indices_res[data_res_p]=ind_vec
                    data_res_p =1
                    indptr_mat_ =1
                vec_ptr =1

        for i in range(mat_ptr,mat_row_ind.shape[0]):
            if mat_row_data[i] >0:
                data_res[data_res_p]=mat_row_data[i]
                indices_res[data_res_p]=mat_row_ind[i]
                data_res_p =1
                indptr_mat_ =1
        for i in range(vec_ptr,vec_row_ind.shape[0]):
            if vec_row_data[i] >0:
                data_res[data_res_p]=vec_row_data[i]
                indices_res[data_res_p]=vec_row_ind[i]
                data_res_p =1
                indptr_mat_ =1
        indptr_mat_res[indptr_mat_res_p]=indptr_mat_
        indptr_mat_res_p =1

    return data_res[:data_res_p],indices_res[:data_res_p],indptr_mat_res
  

Максимальная память, выделенная в начале

Производительность и удобство использования этого подхода сильно зависят от входных данных. При таком подходе выделяется максимальная память (это может легко вызвать ошибки нехватки памяти).

 @nb.njit(cache=True)
def sparse_elementwise_maximum_nb(indices_mat,indptr_mat,data_mat,shape_mat,vec_row_ind,vec_row_data,shrink_to_fit):
    max_non_zero=shape_mat[0]*vec_row_data.shape[0] data_mat.shape[0]
    data_res=np.empty(max_non_zero,dtype=data_mat.dtype)
    indices_res=np.empty(max_non_zero,dtype=np.int32)
    data_res_p=0

    indptr_mat_res=np.empty((shape_mat[0] 1),dtype=np.int32)
    indptr_mat_res[0]=0
    indptr_mat_res_p=1
    indptr_mat_=0

    for row_idx in range(shape_mat[0]):
        mat_row_ind=indices_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]
        mat_row_data=data_mat[indptr_mat[row_idx]:indptr_mat[row_idx 1]]

        mat_ptr=0
        vec_ptr=0
        while mat_ptr<mat_row_ind.shape[0] and vec_ptr<vec_row_ind.shape[0]:
            ind_mat=mat_row_ind[mat_ptr]
            ind_vec=vec_row_ind[vec_ptr]

            #value for both matrix and vector is present
            if ind_mat==ind_vec:
                data_res[data_res_p]=max(mat_row_data[mat_ptr],vec_row_data[vec_ptr])
                indices_res[data_res_p]=ind_mat
                data_res_p =1
                mat_ptr =1
                vec_ptr =1
                indptr_mat_ =1

            #only value for the matrix is present vector is assumed 0
            elif ind_mat<ind_vec:
                if mat_row_data[mat_ptr] >0:
                    data_res[data_res_p]=mat_row_data[mat_ptr]
                    indices_res[data_res_p]=ind_mat
                    data_res_p =1
                    indptr_mat_ =1
                mat_ptr =1

            #only value for the vector is present matrix is assumed 0
            else:
                if vec_row_data[vec_ptr] >0:
                    data_res[data_res_p]=vec_row_data[vec_ptr]
                    indices_res[data_res_p]=ind_vec
                    data_res_p =1
                    indptr_mat_ =1
                vec_ptr =1

        for i in range(mat_ptr,mat_row_ind.shape[0]):
            if mat_row_data[i] >0:
                data_res[data_res_p]=mat_row_data[i]
                indices_res[data_res_p]=mat_row_ind[i]
                data_res_p =1
                indptr_mat_ =1
        for i in range(vec_ptr,vec_row_ind.shape[0]):
            if vec_row_data[i] >0:
                data_res[data_res_p]=vec_row_data[i]
                indices_res[data_res_p]=vec_row_ind[i]
                data_res_p =1
                indptr_mat_ =1
        indptr_mat_res[indptr_mat_res_p]=indptr_mat_
        indptr_mat_res_p =1

    if shrink_to_fit==True:
        data_res=np.copy(data_res[:data_res_p])
        indices_res=np.copy(indices_res[:data_res_p])
    else:
        data_res=data_res[:data_res_p]
        indices_res=indices_res[:data_res_p]

    return data_res,indices_res,indptr_mat_res

# get all needed components of the csr object and create a resulting csr object at the end
def sparse_elementwise_maximum_wrap(mat,vec,shrink_to_fit=True):
    mat_csr=mat.tocsr()
    vec_csr=vec.tocsr()

    shape_mat=mat_csr.shape
    indices_mat=mat_csr.indices
    indptr_mat=mat_csr.indptr
    data_mat=mat_csr.data
    indices_vec=vec_csr.indices
    data_vec=vec_csr.data

    res=sparse_elementwise_maximum_nb(indices_mat,indptr_mat,data_mat,shape_mat,indices_vec,data_vec,shrink_to_fit)
    res=sparse.csr_matrix(res, shape=shape_mat)
    return res
  

Тайминги

Numba имеет накладные расходы на компиляцию или некоторые накладные расходы для загрузки функции из кэша. Не рассматривайте первый вызов, если вы хотите получить время выполнения, а не компиляцию время выполнения.

 import numpy as np
from scipy import sparse

mat = sparse.csr_matrix(  sparse.random(20000, 4000, density=.01, data_rvs=lambda s: np.random.randint(0, 5000, size=s))  )
vec = sparse.csr_matrix(  sparse.random(1, 4000, density=.01, data_rvs=lambda s: np.random.randint(0, 5000, size=s))  )

%timeit output=sparse_elementwise_maximum(mat, vec)
#for csc input
37.9 s ± 224 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
#for csr input
10.7 s ± 90.8 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

#Daniel F
%timeit sparse_maximum(mat, vec)
164 ms ± 1.74 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

#low level implementation (first try)
%timeit res=sparse_elementwise_maximum_wrap(mat,vec)
89.7 ms ± 2.51 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

#low level implementation (optimized, csr)
%timeit res=sparse_elementwise_maximum_wrap(mat,vec)
16.5 ms ± 122 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

#low level implementation (preallocation, without copying at the end)
%timeit res=sparse_elementwise_maximum_wrap(mat,vec)
16.5 ms ± 122 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

#low level implementation (preallocation, with copying at the end)
%timeit res=sparse_elementwise_maximum_wrap(mat,vec)
16.5 ms ± 122 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit res=sparse_elementwise_maximum_wrap(mat,vec,shrink_to_fit=False)
14.9 ms ± 110 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit res=sparse_elementwise_maximum_wrap(mat,vec,shrink_to_fit=True)
21.7 ms ± 399 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

#For comparison, copying the result takes
%%timeit
np.copy(res.data)
np.copy(res.indices)
np.copy(res.indptr)
7.8 ms ± 47.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  

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

1. Это потрясающе. На больших входных данных я получаю ускорение ~ 2000x. Вчера я впервые попытался написать это в Numba (никогда раньше не использовал Numba) и в итоге получил что-то намного медленнее, чем оригинал — очевидно, это был вопрос мастерства. Вопрос: основываясь на моих очень ограниченных знаниях C , могу ли я работать еще быстрее, выполняя многопоточность по строкам mat, поскольку каждая строка независима?

2. @cataclysmic Да, это должно быть возможно, но я думаю, что для получения действительно эффективного кода необходимы сложные знания C . Основная проблема реализации Numba, описанной выше, заключается не в отсутствии распараллеливания, а в сборе результатов в Numba-списках. Может быть, я найду немного времени вечером, чтобы оптимизировать это (должно дать еще один коэффициент в два или более)

3. Удивительно, но я думаю, что я, должно быть, делаю что-то не так — я не могу воспроизвести ваши тайминги. После компиляции как исходной, так и оптимизированной версий я получаю 0,0643 секунды для оригинала и 0,0488 секунды для оптимизированной, что лучше, но не 5-кратное ускорение, которое вы получаете для оптимизированной и оригинальной. Я вижу, что ваш код вызывает sparse_elementwise_maximum_wrap(), но я вижу sparse_elementwise_maximum_2_wrap из исходной версии выше — возможно, эта функция изменилась? Еще раз спасибо, кстати, за эти удивительные реализации.

4. Также я должен был добавить в исходный код вызов np.random.seed(12345) непосредственно перед генерацией mat и vec, чтобы они всегда были одинаковыми для согласованности времени.

5. @cataclysmic Шаг памяти был слишком мал. Это значение обычно должно быть адаптировано к реальной проблеме. Оболочка остается неизменной для обеих функций.

Ответ №2:

scipy.sparse матрицы не транслируются. Вообще. Поэтому, если вы не сможете найти какой-либо способ работы с indices and inpts (у меня нет), вы застряли в стекировании. Лучшее, что я могу понять, это просто для vstack ваших vec s, пока они не будут иметь ту же форму, mat что и . Кажется, это дает хорошее ускорение, хотя и не объясняет странность с csr segfault.

 #using `mat` and `vec` from the speed test
def sparse_maximum(mat, vec):
    vec1 = sparse.vstack([vec for _ in range(mat.shape[0])])
    return mat.maximum(vec1)

# Time it
num_timing_loops = 3.0
starttime = timeit.default_timer()
sparse_maximum(mat, vec)
print('time per call is:', (timeit.default_timer() - starttime)/num_timing_loops, 'seconds')
# I was getting 11-12 seconds on your original code
time per call is: 0.514533479333295 seconds
  

Доказательство того, что он работает с исходными матрицами:

 vec = sparse.vstack([vec for _ in range(4)])

print(mat.maximum(vec).todense())
[[  0   5 100]
 [  3   5 100]
 [  6   7 100]
 [  9  10 100]]
  

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

1. Это дает мне 15-кратное ускорение в примере в моем вопросе и больше похоже на 50-кратное ускорение для большей (50k X 10k) матрицы. Спасибо за публикацию! Я согласен, что построение vstack vecs размером с мат кажется мне «неправильным» (с точки зрения эффективности), но, возможно, обойти это невозможно.

Ответ №3:

Глядя на maximum метод и код, особенно на _binopt метод, /scipy/sparse/compressed.py становится очевидным, что он может работать со скаляром other . Для разреженной other он создает новую разреженную матрицу (того же формата и формы), Используя indptr значения и т. Д. Если другой имеет ту же форму, он работает:

 In [55]: mat = sparse.csr_matrix(np.arange(12).reshape((4,3)))
In [64]: mat.maximum(mat)
Out[64]: 
<4x3 sparse matrix of type '<class 'numpy.int64'>'
    with 11 stored elements in Compressed Sparse Row format>
  

Сбой заключается в том, что другая — 1d разреженная матрица:

 In [65]: mat.maximum(mat[0,:])
Segmentation fault (core dumped)
  

mat.maximum(mat[:,0]) выполняется без ошибок, хотя я не уверен в значениях. mat[:,0] будет иметь тот же размер indptr .

Я думал mat.maximum(mat[:,0]) , что это даст ту же ошибку, если mat было csc , но это не так.

Давайте будем честными, такого рода операции не являются сильной стороной для разреженных матриц. Ядром его математики является умножение матриц. Это то, для чего они изначально были разработаны — разреженные задачи линейной алгебры, такие как конечная разность и конечный элемент.

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

1. Спасибо. Я использую scipy.sparse матрицы для умножения матриц. Очевидно, что эта часть кода выполняется очень быстро (намного быстрее, чем массивы numpy). Однако для одного из точечных произведений необходима матрица, которая является поэлементным максимумом (mat, vec).

2. vec Существенно ли увеличиваются ненулевые значения к ненулевым значениям результатов? Если vec имеет нули в том же месте, mat что maximum и это, потенциально намного проще, чем если может превратить 0 в mat ненулевое значение. Изменения в разреженности a csr/csc стоят дорого, говоря относительно.

3. У меня только что была та же идея! Да, нули в vec также являются нулями в mat (т. Е. 0-столбцы), и у меня уже есть индексы этих нулей из другой операции. Я написал версию numpy, которая намного быстрее, чем моя предыдущая версия numpy. Все еще не уверен, как написать оптимальную версию scipy (я относительно новичок в scipy, поэтому все еще пытаюсь понять, как делать что-то эффективно)

4. Способ numpy: `np.maximum(mat[:,nonzero_cols_booleanvec] , vec [nonzero_cols_boolean_vec] )’ Прямо сейчас я пробую версию, которая выполняет нарезку в scipy, а затем преобразует в массивы numpy (что делает mat и vec намного уже = меньше памяти, чем плотные версии full mat amp;полный vec, потому что они разреженные, и мы сохраняем ненулевые значения только после нарезки), затем использует np.maximum. Вероятно, есть что-то намного лучшее без этого уродливого кастинга, но я этого не знаю.