稀疏矩阵的内存使用(SCIPY)

hwamh0ep  于 2023-08-05  发布在  其他
关注(0)|答案(2)|浏览(117)

我原以为scipy的稀疏矩阵使用的内存要比朴素的列表表示少得多,但实验证明我错了。在下面的代码片段中,我构建了一个随机二进制矩阵,其中大约75%为零,然后将内存使用情况与scipy.sparse(简化的IPython会话)中的每个可用表示进行比较:

# %load_ext memory_profiler, from scipy import sparse, etc.
# ...
%memit M = random_binary_matrix(5000, 5000)  # contains ints
peak memory: 250.36 MiB, increment: 191.77 MiB

In : sum(line.count(0) for line in M) / (len(M) * len(M[0]))
Out: 0.75004468

%memit X_1 = sparse.bsr_matrix(M)
peak memory: 640.49 MiB, increment: 353.76 MiB
%memit X_2 = sparse.coo_matrix(M)
peak memory: 640.71 MiB, increment: 286.09 MiB
%memit X_3 = sparse.csc_matrix(M)
peak memory: 807.51 MiB, increment: 357.53 MiB
%memit X_4 = sparse.csr_matrix(M)
peak memory: 840.04 MiB, increment: 270.91 MiB
%memit X_5 = sparse.dia_matrix(M)
peak memory: 1075.20 MiB, increment: 386.87 MiB
%memit X_6 = sparse.dok_matrix(M)
peak memory: 3059.86 MiB, increment: 1990.62 MiB
%memit X_7 = sparse.lil_matrix(M)
peak memory: 2774.67 MiB, increment: 385.39 MiB

字符串
我做错什么了吗?我是否错过了什么(包括这些替代表示的要点)?
还是memory_profiler,或者我对它缺乏理解,应该受到责备?特别是,“峰值记忆”和“增量”之间的关系有时似乎是可疑的:初始化X_2会使内存使用量增加286.09 MiB,但峰值内存使用量仅略高于执行该行之前的水平。
如果重要的话:我正在运行Debian 12、Python 3.11.2、IPython 8.5.0、scipy 1.10.1、memory_profiler 0.61.0

6ljaweal

6ljaweal1#

从密集矩阵创建稀疏矩阵会导致完全填充的稀疏矩阵(包括显式零)。这是因为scipy不知道您希望用于排除接近零的值的容差。可以说,它应该附带一个工厂方法来为您完成这项工作,但我们总是可以手动构建一个。
稀疏矩阵带有许多不同的构造函数参数组合。看起来您只想排除显式的零。对于这个,像这样的东西应该工作得很好:

nonzeros = M.nonzero()
# argument pattern csr_matrix((data, (row_ind, col_ind)), [shape=(M, N)])
X_4 = sparse.csr_matrix((M[nonzeros], nonzeros), M.shape)

字符串
对于其他公差,您可以使用以下内容:

tolerance = 0.1
nonzeros = (abs(M) > tolerance).nonzero()
X_4 = sparse.csr_matrix((M[nonzeros], nonzeros), M.shape)


您也可以先创建一个完全填充的稀疏矩阵,然后在其上调用eliminate_zeros(),但这会导致暂时更高的内存使用率。

iyzzxitl

iyzzxitl2#

在不添加memit的情况下,使用更小的维度,下面是一个coo矩阵:

In [60]: M = sparse.random(10,10, .25, 'coo').astype(bool).astype(int)
In [61]: M
Out[61]: 
<10x10 sparse matrix of type '<class 'numpy.int64'>'
    with 25 stored elements in COOrdinate format>

字符串
它的稠密等价物:

In [62]: M.A
Out[62]: 
array([[0, 1, 0, 0, 0, 0, 0, 1, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
       [0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
       [1, 0, 1, 1, 1, 1, 0, 0, 0, 1],
       [1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
       [0, 1, 1, 1, 0, 1, 0, 0, 0, 0],
       [1, 0, 0, 0, 0, 0, 1, 0, 0, 0],
       [1, 0, 0, 1, 0, 0, 1, 1, 0, 0],
       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
In [63]: np.count_nonzero(M.A)
Out[63]: 25
In [64]: M.A.nbytes
Out[64]: 800


主数组的内存使用-数据和坐标:

In [65]: M.data.nbytes+M.row.nbytes+M.col.nbytes
Out[65]: 400


转换为csr可以通过将M.row替换为indptr来减少内存使用。我说可以,因为这取决于形状。CSC也一样。

In [66]: Mr=M.tocsr()
In [67]: Mr.data.nbytes+Mr.indptr.nbytes+Mr.indices.nbytes
Out[67]: 344


对于“random”矩阵,bsr没有帮助:

In [68]: Mb = M.tobsr()
In [69]: Mb
Out[69]: 
<10x10 sparse matrix of type '<class 'numpy.int64'>'
    with 25 stored elements (blocksize = 1x1) in Block Sparse Row format>


lil的内存更难估计,因为它将信息存储在对象dtype数组中,如列表:

In [70]: Mi = M.tolil()
In [72]: Mi.data, Mi.rows
Out[72]: 
(array([list([1, 1]), list([1, 1]), list([1, 1]), list([1]),
        list([1, 1, 1, 1, 1, 1]), list([1, 1]), list([1, 1, 1, 1]),
        list([1, 1]), list([1, 1, 1, 1]), list([])], dtype=object),
 array([list([1, 7]), list([8, 9]), list([3, 8]), list([8]),
        list([0, 2, 3, 4, 5, 9]), list([0, 2]), list([1, 2, 3, 5]),
        list([0, 6]), list([0, 3, 6, 7]), list([])], dtype=object))


dok是一个字典:

In [73]: Md =M.todok()
In [74]: Md.keys()
Out[74]: dict_keys([(0, 1), (0, 7), (1, 8), (1, 9), (2, 3), (2, 8), (3, 8), (4, 0), (4, 2), (4, 3), (4, 4), (4, 5), (4, 9), (5, 0), (5, 2), (6, 1), (6, 2), (6, 3), (6, 5), (7, 0), (7, 6), (8, 0), (8, 3), (8, 6), (8, 7)])


这些都没有考虑转换期间的内存使用。从dense到coo需要一个nonzero调用来获取所有非零坐标。如果这些数组是正确的类型,则从data/row/col输入进行coo可能不会增加任何内存使用。从coo中生成企业社会责任可能需要对坐标进行词法排序。大多数数学都是以CSR格式完成的,因此格式之间可能有很多隐藏的转换。lil和dok对于迭代输入是好的,但是在其他方面是慢的并且是更存储密集的。

相关问题