Точечное произведение двух разреженных матриц, влияющих только на нулевые значения

Я пытаюсь вычислить простой точечный продукт, но оставить ненулевые значения из исходной матрицы без изменений. Пример игрушки:

import numpy as np

A = np.array([[2, 1, 1, 2],
              [0, 2, 1, 0],
              [1, 0, 1, 1],
              [2, 2, 1, 0]])
B = np.array([[ 0.54331039,  0.41018682,  0.1582158 ,  0.3486124 ],
              [ 0.68804647,  0.29520239,  0.40654206,  0.20473451],
              [ 0.69857579,  0.38958572,  0.30361365,  0.32256483],
              [ 0.46195299,  0.79863505,  0.22431876,  0.59054473]])

ожидаемый результат:

C = np.array([[ 2.        ,  1.        ,  1.        ,  2.        ],
              [ 2.07466874,  2.        ,  1.        ,  0.73203386],
              [ 1.        ,  1.5984076 ,  1.        ,  1.        ],
              [ 2.        ,  2.        ,  1.        ,  1.42925865]])

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

A = sparse.rand(250000, 1700, density=0.001, format='csr')
B = sparse.rand(1700, 1700, density=0.02, format='csr')

одним из простых способов было бы просто установить значения с помощью индекса маски, например:

mask = A != 0
C = A.dot(B)
C[mask] = A[mask]

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

другой очевидный подход, я думаю, будет просто прибегать к итерации и пропускать замаскированные значения, но я бы не хотел отказываться от преимуществ умножения массива NumPy/scipy.

некоторые уточнения: меня на самом деле интересует какой-то особый случай, где B - это всегда квадрат, и поэтому, A и C имеют одинаковую форму. Так что если есть решение, которое не работает на произвольных массивах, но подходит в моем случае, это нормально.

обновление: некоторые попытки:

from scipy import sparse
import numpy as np

def naive(A, B):
    mask = A != 0
    out = A.dot(B).tolil()
    out[mask] = A[mask]
    return out.tocsr()


def proposed(A, B):
    Az = A == 0
    R, C = np.where(Az)
    out = A.copy()
    out[Az] = np.einsum('ij,ji->i', A[R], B[:, C])
    return out


%timeit naive(A, B)
1 loops, best of 3: 4.04 s per loop

%timeit proposed(A, B)
/usr/local/lib/python2.7/dist-packages/scipy/sparse/compressed.py:215: SparseEfficiencyWarning: Comparing a sparse matrix with 0 using == is inefficient, try using != instead.

/usr/local/lib/python2.7/dist-packages/scipy/sparse/coo.pyc in __init__(self, arg1, shape, dtype, copy)
    173                     self.shape = M.shape
    174 
--> 175                 self.row, self.col = M.nonzero()
    176                 self.data = M[self.row, self.col]
    177                 self.has_canonical_format = True

MemoryError: 

ЕЩЕ ОДНО ОБНОВЛЕНИЕ:

не мог сделать ничего более или менее полезного из Цитона, по крайней мере, не уходя слишком далеко от Python. Идея заключалась в том, чтобы оставить точечный продукт scipy и просто попытаться установить эти исходные значения так же быстро, как возможно, что-то вроде этого:

cimport cython


@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)
cpdef coo_replace(int [:] row1, int [:] col1, float [:] data1, int[:] row2, int[:] col2, float[:] data2):
    cdef int N = row1.shape[0]
    cdef int M = row2.shape[0]
    cdef int i, j
    cdef dict d = {}

    for i in range(M):
        d[(row2[i], col2[i])] = data2[i]

    for j in range(N):
        if (row1[j], col1[j]) in d:
            data1[j] = d[(row1[j], col1[j])]

это было немного лучше, чем моя предварительная "наивная" реализация (используя .tolil()), но следуя подходу hpaulj, лил может быть выброшен. Возможно, замена python dict чем-то вроде std::map поможет.

3 ответов


возможно, более чистая и быстрая версия вашего naive код:

In [57]: r,c=A.nonzero()    # this uses A.tocoo()

In [58]: C=A*B
In [59]: Cl=C.tolil()
In [60]: Cl[r,c]=A.tolil()[r,c]
In [61]: Cl.tocsr()

C[r,c]=A[r,c] дает предупреждение об эффективности, но я думаю, что это нацелено на то, чтобы люди делали такое назначение в цикле.

In [63]: %%timeit C=A*B
    ...: C[r,c]=A[r,c]
...
The slowest run took 7.32 times longer than the fastest....
1000 loops, best of 3: 334 µs per loop

In [64]: %%timeit C=A*B
    ...: Cl=C.tolil()
    ...: Cl[r,c]=A.tolil()[r,c]
    ...: Cl.tocsr()
    ...: 
100 loops, best of 3: 2.83 ms per loop

мой A маленький, только (250,100), но похоже, что туда и обратно в lil не экономия времени, несмотря на предупреждение.

маска с A==0 обязательно даст проблемы, когда A is редкий

In [66]: Az=A==0
....SparseEfficiencyWarning...
In [67]: r1,c1=Az.nonzero()

по сравнению с nonzero r на A этот r1 намного больше-индекс строки всех нулей в разреженной матрице; все, кроме 25 nonzeros.

In [70]: r.shape
Out[70]: (25,)

In [71]: r1.shape
Out[71]: (24975,)

если я индекс A С r1 я получаю гораздо больший массив. Фактически я повторяю каждую строку по количеству нулей в ней

In [72]: A[r1,:]
Out[72]: 
<24975x100 sparse matrix of type '<class 'numpy.float64'>'
    with 2473 stored elements in Compressed Sparse Row format>

In [73]: A
Out[73]: 
<250x100 sparse matrix of type '<class 'numpy.float64'>'
    with 25 stored elements in Compressed Sparse Row format>

я увеличил форму и количество ненулевых элементов примерно на 100 (количество столбцы.)

определение foo и копирование Divakar тесты:

def foo(A,B):
    r,c = A.nonzero()
    C = A*B
    C[r,c] = A[r,c]
    return C

In [83]: timeit naive(A,B)
100 loops, best of 3: 2.53 ms per loop

In [84]: timeit proposed(A,B)
/...
  SparseEfficiencyWarning)
100 loops, best of 3: 4.48 ms per loop

In [85]: timeit foo(A,B)
...
  SparseEfficiencyWarning)
100 loops, best of 3: 2.13 ms per loop

таким образом, моя версия имеет скромную скорость улучшения. Как выяснил Дивакар, изменение разреженности изменяет относительные преимущества. Я ожидаю, что размер также изменит их.

тот факт, что A.nonzero использует coo format, предполагает, что может быть возможно построить новый массив с этим форматом. Много sparse код строит новую матрицу через coo ценности.

In [97]: Co=C.tocoo()    
In [98]: Ao=A.tocoo()

In [99]: r=np.concatenate((Co.row,Ao.row))
In [100]: c=np.concatenate((Co.col,Ao.col))
In [101]: d=np.concatenate((Co.data,Ao.data))

In [102]: r.shape
Out[102]: (79,)

In [103]: C1=sparse.csr_matrix((d,(r,c)),shape=A.shape)

In [104]: C1
Out[104]: 
<250x100 sparse matrix of type '<class 'numpy.float64'>'
    with 78 stored elements in Compressed Sparse Row format>

этой C1 имеет, я думаю, те же ненулевые элементы, что и C построенный другими средствами. Но я думаю, что одно значение отличается, потому что r больше. В этом конкретном примере, C и A поделитесь одним ненулевым элементом и coo стиль ввода суммирует те, где как мы бы предпочли иметь A значения перезаписать все.

если вы можете терпеть это несоответствие, это более быстрый способ (по крайней мере для этого test case):

def bar(A,B):
    C=A*B
    Co=C.tocoo()
    Ao=A.tocoo()
    r=np.concatenate((Co.row,Ao.row))
    c=np.concatenate((Co.col,Ao.col))
    d=np.concatenate((Co.data,Ao.data))
    return sparse.csr_matrix((d,(r,c)),shape=A.shape)

In [107]: timeit bar(A,B)
1000 loops, best of 3: 1.03 ms per loop

взломал его! Ну, тут много материалов составляющей, характерные для разреженных матриц, которые я узнал по пути. Вот реализация, которую я мог бы собрать -

# Find the indices in output array that are to be updated  
R,C = ((A!=0).dot(B!=0)).nonzero()
mask = np.asarray(A[R,C]==0).ravel()
R,C = R[mask],C[mask]

# Make a copy of A and get the dot product through sliced rows and columns
# off A and B using the definition of matrix-multiplication    
out = A.copy()
out[R,C] = (A[R].multiply(B[:,C].T).sum(1)).ravel()   

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

во время выполнения теста

функции определения -

def naive(A, B):
    mask = A != 0
    out = A.dot(B).tolil()
    out[mask] = A[mask]
    return out.tocsr()

def proposed(A, B):
    R,C = ((A!=0).dot(B!=0)).nonzero()
    mask = np.asarray(A[R,C]==0).ravel()
    R,C = R[mask],C[mask]
    out = A.copy()
    out[R,C] = (A[R].multiply(B[:,C].T).sum(1)).ravel()    
    return out

тайминги -

In [57]: # Input matrices 
    ...: M,N = 25000, 170       
    ...: A = sparse.rand(M, N, density=0.001, format='csr')
    ...: B = sparse.rand(N, N, density=0.02, format='csr')
    ...: 

In [58]: %timeit naive(A, B)
10 loops, best of 3: 92.2 ms per loop

In [59]: %timeit proposed(A, B)
10 loops, best of 3: 132 ms per loop

In [60]: # Input matrices with increased sparse-ness
    ...: M,N = 25000, 170       
    ...: A = sparse.rand(M, N, density=0.0001, format='csr')
    ...: B = sparse.rand(N, N, density=0.002, format='csr')
    ...: 

In [61]: %timeit naive(A, B)
10 loops, best of 3: 78.1 ms per loop

In [62]: %timeit proposed(A, B)
100 loops, best of 3: 8.03 ms per loop

Python не является моим основным языком, но я думал, что это интересная проблема, и я хотел дать этому удар:)

предисловий:

import numpy
import scipy.sparse
# example matrices and sparse versions
A = numpy.array([[1, 2, 0, 1], [1, 0, 1, 2], [0, 1, 2 ,1], [1, 2, 1, 0]])
B = numpy.array([[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4]])
A_s = scipy.sparse.lil_matrix(A)
B_s = scipy.sparse.lil_matrix(B)

таким образом, вы хотите преобразовать оригинальную проблему:

C = A.dot(B)
C[A.nonzero()] = A[A.nonzero()]

к чему-то разреженному. Чтобы убрать это с пути, прямой" разреженный " перевод вышеизложенного:

C_s = A_s.dot(B_s)
C_s[A_s.nonzero()] = A_s[A_s.nonzero()]

но похоже, что вы не довольны этим, так как он сначала вычисляет все продукты dot, которые вас беспокоят это может быть неэффективно.

Итак, ваш вопрос: если вы сначала найдете нули и только оцените точечные продукты на этих элементах, это будет быстрее? Т. е. для плотной матрицы это может быть что-то вроде:

Xs, Ys = numpy.nonzero(A==0)
D = A[:]
D[Xs, Ys] = map ( lambda x,y: A[x,:].dot(B[:,y]), Xs, Ys)

давайте переведем это на разреженную матрицу. Моим главным камнем преткновения здесь было нахождение" нулевых " индексов; так как A_s==0 не имеет смысла для разреженных матриц, я нашел их так:

Xmax, Ymax = A_s.shape
DenseSize = Xmax * Ymax
Xgrid, Ygrid = numpy.mgrid[0:Xmax, 0:Ymax]
Ygrid = Ygrid.reshape([DenseSize,1])[:,0]
Xgrid = Xgrid.reshape([DenseSize,1])[:,0]
AllIndices = numpy.array([Xgrid, Ygrid])
NonzeroIndices = numpy.array(A_s.nonzero())
ZeroIndices = numpy.array([x for x in AllIndices.T.tolist() if x not in NonzeroIndices.T.tolist()]).T

если вы знаете лучше / быстрее, все значит, попробуй. Как только у нас есть нулевые индексы, мы можем сделать аналогичное отображение, как и раньше:

D_s = A_s[:]
D_s[ZeroIndices[0], ZeroIndices[1]] = map ( lambda x, y : A_s[x,:].dot(B[:,y])[0], ZeroIndices[0], ZeroIndices[1] )

что дает вам результат разреженной матрицы.

теперь я не знаю, быстрее это или нет. Я в основном сделал удар, потому что это была интересная проблема, и посмотреть, смогу ли я сделать это на python. На самом деле я подозреваю, что это может быть не быстрее, чем direct whole-matrix dotproduct, потому что он использует listcomprehensions и отображение в большом наборе данных (как вы говорите, вы ожидаете много ноли.) Но это!--27-- > is ответ на ваш вопрос "как я могу вычислять только точечные продукты для нулевых значений, не умножая матрицы в целом". Мне было бы интересно посмотреть, если вы попробуете это, как это сравнивается с точки зрения скорости на ваших наборах данных.

EDIT: я помещаю ниже пример версии "обработка блоков" на основе вышеизложенного, которая, я думаю, должна позволить вам обрабатывать ваш большой набор данных без проблем. Дайте мне знать, если это завод.

import numpy
import scipy.sparse
# example matrices and sparse versions
A = numpy.array([[1, 2, 0, 1], [1, 0, 1, 2], [0, 1, 2 ,1], [1, 2, 1, 0]])
B = numpy.array([[1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4]])
A_s = scipy.sparse.lil_matrix(A)
B_s = scipy.sparse.lil_matrix(B)

# Choose a grid division (i.e. how many processing blocks you want to create)
BlockGrid = numpy.array([2,2])

D_s = A_s[:] # initialise from A

Xmax, Ymax = A_s.shape
BaseBSiz = numpy.array([Xmax, Ymax]) / BlockGrid
for BIndX in range(0, Xmax, BlockGrid[0]):
  for BIndY in range(0, Ymax, BlockGrid[1]):
    BSizX, BSizY = D_s[ BIndX : BIndX + BaseBSiz[0], BIndY : BIndY + BaseBSiz[1] ].shape
    Xgrid, Ygrid = numpy.mgrid[BIndX : BIndX + BSizX, BIndY : BIndY + BSizY]
    Xgrid = Xgrid.reshape([BSizX*BSizY,1])[:,0]
    Ygrid = Ygrid.reshape([BSizX*BSizY,1])[:,0]
    AllInd = numpy.array([Xgrid, Ygrid]).T
    NZeroInd = numpy.array(A_s[Xgrid, Ygrid].reshape((BSizX,BSizY)).nonzero()).T + numpy.array([[BIndX],[BIndY]]).T
    ZeroInd = numpy.array([x for x in AllInd.tolist() if x not in NZeroInd.tolist()]).T
    #
    # Replace zero-values in current block
    D_s[ZeroInd[0], ZeroInd[1]] = map ( lambda x, y : A_s[x,:].dot(B[:,y])[0], ZeroInd[0], ZeroInd[1] )