从稀疏数组的每一行的列索引快速拆分数组

时间:2017-05-18 16:34:49

标签: python numpy scipy sparse-matrix

假设我有一个稀疏数组和一个密集数组,它具有相同的列数但行数较少:

from scipy.sparse import csr_matrix
import numpy as np

sp_arr = csr_matrix(np.array([[1,0,0,0,1],[0,0,1,0,0],[0,1,0,0,1],[0,0,0,1,1],[0,0,0,1,0]]))
arr = np.random.rand(10).reshape(2,5)
print(arr)
[[ 0.47027789  0.82510323  0.01321617  0.66264852  0.3618022 ]
 [ 0.80198907  0.36350616  0.10254934  0.65209401  0.094961  ]]

我想得到一个数组,其中包含索引的所有子矩阵,这些索引包含稀疏数组的每一行的值。例如,sp_arr中的数据索引如下:

0:[0,4] 1:[2] 2:[1,4] 3:[3,4] 4:[3]

我的输出应如下所示:

array([array([[ 0.47027789,  0.3618022 ],
       [ 0.80198907,  0.094961  ]]),
       array([[ 0.01321617],
       [ 0.10254934]]),
       array([[ 0.82510323,  0.3618022 ],
       [ 0.36350616,  0.094961  ]]),
       array([[ 0.66264852,  0.3618022 ],
       [ 0.65209401,  0.094961  ]]),
       array([[ 0.66264852],
       [ 0.65209401]])], dtype=object)

我可以使用以下代码创建它,但随着数组大小的扩大(在我的情况下很大),它变得非常慢。

output = np.empty(sp_arr.shape[0], dtype=object)
for row in range(sp_arr.shape[0]):
    output[row] = arr[:, sp_arr[row].indices]

我已经考虑过矢量化过程并沿着轴应用它,但是np.apply_along_axis不适用于稀疏矩阵,不幸的是,虽然这个例子足够小,可以使密集然后使用{{1}我的实际稀疏矩阵太大了(> 100Gb)。

我曾经想过,也许有一种奇特的方式来索引或使用像hsplit这样的东西用已经矢量化的方法实现这一点,但到目前为止我还没有运气。有没有什么可以实现这一点,只是逃避我?

更新

根据@Divakar的答案,这很棒,我找到了另一种方法来实现同样的事情,并且有一点点,可以忽略不计的改进。

@Divakars最佳答案是:

apply_along_axis

这使我的表现提高了50-60倍!但它有点难以阅读。

我发现,鉴于csr_matrix格式,您可以在此处使用def app2(sp_arr, arr): r,c = sp_arr.nonzero() idx = np.flatnonzero(r[1:] > r[:-1])+1 idx0 = np.concatenate(( [0] , idx, [r.size] )) arr_c = arr[:,c] return [arr_c[:,i:j] for i,j in zip(idx0[:-1], idx0[1:])] indices属性。

indptr

最后,性能在统计上是相同的(稀疏矩阵276538 x 33114上的改进不到50ms),但感觉更容易理解。 更重要这种方法确实包含根本没有值的行,而前一种方法则没有。这看起来似乎并不重要,但对于我的用例来说,它非常关键。

更新2

回应@EelcoHoogendoorn。问题是使用正规化方法并行实现替代最小二乘的一部分,我试图在python中实现。这来自经常引用的论文Large-scale Parallel Collaborative Filtering for the Netflix Prize这样做的正常方法是在各个流程中分配“评级”,“用户”和“项目”矩阵的副本。我认为如果我们预先构建所有项目子矩阵并将其分发给流程会发生什么会很有趣。这样,进程只需要分别返回一个用户或一个项目的特征列,这些特征列可以用于更新用户和项目矩阵。

上述问题实际上是我当前实施的瓶颈。根据你的评论,在这种情况下,我不相信转置是关键的,因为算法的一部分采用每个子矩阵的点积与其转置。

3 个答案:

答案 0 :(得分:1)

嗯,有两个选项 - np.splitloop comprehension。根据我的经验,我发现后者更快。但是,优先考虑的是通过尽可能多的预处理来完成循环理解中的最小化工作。

方法#1:使用np.split -

的第一种方法
# Get row, col indices
r,c = sp_arr.nonzero()

# Get intervaled indices for row indices. 
# We need to use these to cut the column indexed input array.
idx = np.flatnonzero(r[1:] > r[:-1])+1
out = np.split(arr[:,c], idx, axis=1)

示例输出 -

In [56]: [i.tolist() for i in out]
Out[56]: 
[[[0.47027789, 0.3618022], [0.80198907, 0.094961]],
 [[0.01321617], [0.10254934]],
 [[0.82510323, 0.3618022], [0.36350616, 0.094961]],
 [[0.66264852, 0.3618022], [0.65209401, 0.094961]],
 [[0.66264852], [0.65209401]]]

方法#2:第二种方法,应该在性能方面做得更好,我们将重复使用之前方法中的r,c,idx -

idx0 = np.concatenate(( [0] , idx, [r.size] ))
arr_c = arr[:,c]
out = [arr_c[:,i:j] for i,j in zip(idx0[:-1], idx0[1:])]

请参阅loop-comprehension简单地切片已编入索引的数组arr_c的数组。这是最小的,因此应该是好的。

运行时测试

方法 -

def org_app(sp_arr, arr):
    output = np.empty(sp_arr.shape[0], dtype=object)
    for row in range(sp_arr.shape[0]):
        output[row] = arr[:, sp_arr[row].indices]
    return output

def app1(sp_arr, arr):
    r,c = sp_arr.nonzero()
    idx = np.flatnonzero(r[1:] > r[:-1])+1
    return np.split(arr[:,c], idx, axis=1)

def app2(sp_arr, arr):
    r,c = sp_arr.nonzero()
    idx = np.flatnonzero(r[1:] > r[:-1])+1    
    idx0 = np.concatenate(( [0] , idx, [r.size] ))
    arr_c = arr[:,c]
    return [arr_c[:,i:j] for i,j in zip(idx0[:-1], idx0[1:])]

计时 -

In [146]: sp_arr = csr_matrix((np.random.rand(100000,100)>0.8).astype(int))
     ...: arr = np.random.rand(10,sp_arr.shape[1])
     ...: 

In [147]: %timeit org_app(sp_arr, arr)
     ...: %timeit app1(sp_arr, arr)
     ...: %timeit app2(sp_arr, arr)
     ...: 
1 loops, best of 3: 5.66 s per loop
10 loops, best of 3: 146 ms per loop
10 loops, best of 3: 105 ms per loop

答案 1 :(得分:0)

另一种方法是使用groupby

from itertools import groupby

rows, cols = sp_arr.nonzero()
out = [arr[:, [g[1] for g in group]] for _, group in groupby(zip(rows, cols), lambda x: x[0])]

答案 2 :(得分:0)

numpy_indexed包(免责声明:我是它的作者),允许你使用简单的单行做这样的事情:

import numpy_indexed as npi
r, c = sp_arr.nonzero()
s = group_by(r).split(arr.T[c])

性能应与目前接受的答案相似,但稍微慢一点。请注意,s的元素现在相对于原始布局进行转置,因此如果这是一个硬接口要求,则需要另外一个传递。

除了更简洁和可测试之外,我提出这种方法的原因是,如果您在稍高的级别描述问题,而不是您提出的子问题,很可能仍然可以找到更优雅的解决方案我们。根据我的经验,分裂操作很少是最终的结果,因此如果需要后续操作,这可能会揭示更多在更高抽象层次上表达逻辑的机会。