【问题标题】:Why are lil_matrix and dok_matrix so slow compared to common dict of dicts?与普通字典相比,为什么 lil_matrix 和 dok_matrix 这么慢?
【发布时间】:2015-03-02 11:33:48
【问题描述】:

我想迭代地构建稀疏矩阵,并注意到根据 SciPy 文档有两个合适的选项:

LiL matrix:

类 scipy.sparse.lil_matrix(arg1, shape=None, dtype=None, copy=False)[source] 基于行的链表稀疏矩阵

这是构造稀疏矩阵的有效结构 逐渐增加。

DoK matrix:

类 scipy.sparse.dok_matrix(arg1, shape=None, dtype=None, copy=False)[source] 基于稀疏矩阵的键字典。

这是构造稀疏矩阵的有效结构 逐渐增加。

但是当我运行基准测试时,与构建值字典(稍后可以轻松转换为稀疏矩阵)相比,后者比使用任何稀疏矩阵快 10-20 倍型号:

from scipy.sparse import dok_matrix, lil_matrix
from timeit import timeit
from collections import defaultdict

def common_dict(rows, cols):
    freqs = defaultdict(lambda: defaultdict(int))
    for row, col in zip(rows, cols):
        freqs[row][col] += 1

    return freqs

def dok(rows, cols):
    freqs = dok_matrix((1000,1000))
    for row, col in zip(rows, cols):
        freqs[row,col] += 1

    return freqs

def lil(rows, cols):
    freqs = lil_matrix((1000,1000))
    for row, col in zip(rows, cols):
        freqs[row,col] += 1

    return freqs


def benchmark():
    cols = range(1000)
    rows = range(1000)

    res = timeit("common_dict({},{})".format(rows, cols), 
                 "from __main__ import common_dict", 
                 number=100)

    print("common_dict: {}".format(res))

    res = timeit("dok({},{})".format(rows, cols), 
                 "from __main__ import dok", 
                 number=100)

    print("dok: {}".format(res))

    res = timeit("lil({},{})".format(rows, cols), 
                 "from __main__ import lil", 
                 number=100)

    print("lil: {}".format(res))

结果:

benchmark()

common_dict: 0.11778324202168733
dok: 2.2927695910912007
lil: 1.3541790939634666

是什么导致了矩阵模型的这种开销,有什么方法可以加快它的速度吗?是否有使用 dok 或 lil 比普通字典更喜欢的用例?

【问题讨论】:

    标签: python numpy scipy


    【解决方案1】:

    当我将您的 += 更改为您的 2 个稀疏数组的 = 时:

    for row, col in zip(rows, cols):
        #freqs[row,col] += 1
        freqs[row,col] = 1
    

    他们各自的时间减半。最耗时的是索引。对于+=,它必须同时执行__getitem____setitem__

    当文档说doklil 更适合迭代构造时,他们的意思是扩展其底层数据结构比其他格式更容易。

    当我尝试使用您的代码创建 csr 矩阵时,我得到:

    /usr/lib/python2.7/dist-packages/scipy/sparse/compressed.py:690:SparseEfficiencyWarning:更改 csr_matrix 的稀疏结构是昂贵的。 lil_matrix 更有效。 稀疏效率警告)

    速度降低 30 倍。

    因此速度声明与csr 等格式相关,而不是与纯Python 或numpy 结构相关。

    您可能想查看dok_matrix.__get_item__dok_matrix.__set_item__ 的Python 代码,看看当您执行freq[r,c] 时会发生什么。


    构建dok 的更快方法是:

    freqs = dok_matrix((1000,1000))
    d = dict()
    for row, col in zip(rows, cols):
        d[(row, col)] = 1
    freqs.update(d)
    

    利用dok 是子类字典这一事实。请注意,dok 矩阵不是字典字典。它的键是像(50,50) 这样的元组。

    构造相同稀疏数组的另一种快速方法是:

    freqs = sparse.coo_matrix((np.ones(1000,int),(rows,cols)))
    

    也就是说,既然你已经有了rowscols数组(或范围),计算对应的data数组,然后构造稀疏数组。

    但如果您必须在增量增长步骤之间对矩阵执行稀疏运算,那么doklil 可能是您的最佳选择。


    稀疏矩阵是为线性代数问题开发的,例如求解具有大型稀疏矩阵的线性方程。几年前我在 MATLAB 中使用它们来解决有限差分问题。对于这项工作,计算友好的csr 格式是最终目标,coo 格式是一种方便的初始化格式。

    现在许多 SO scipy 稀疏问题来自scikit-learn 和文本分析问题。它们也用于生物数据库文件。但(data),(row,col) 定义方法仍然效果最好。

    因此,稀疏矩阵从未用于快速增量创建。字典和列表等传统 Python 结构在这方面要好得多。


    这是一个更快的dok 迭代,它利用了它的字典方法。 update 似乎和普通字典一样快。 get 大约是等效索引 (freq[row,col]) 的 3 倍。索引可能使用get,但必须有很多开销。

    def fast_dok(rows, cols):
        freqs = dok_matrix((1000,1000))
        for row, col in zip(rows,cols):
             i = freqs.get((row,col),0)
             freqs.update({(row,col):i+1})
        return freqs
    

    跳过get,直接做

             freqs.update({(row,col): 1)
    

    甚至更快 - 比 defaultdict 示例的 defaultdict 更快,几乎和简单的字典初始化一样快 ({(r, c):1 for r,c in zip(rows, cols)})

    【讨论】:

    • 在我的系统上,fast_dokcommon_dict 慢四倍,比 tuple_dict 慢八倍,这就是我所说的第一个示例。
    • 续:我不确定,为什么:这可能是因为您为每一对创建了一个dict,或者可能在撰写本文时dok_matrix 没有覆盖get(),现在呢?幸运的是,update() 还没有被覆盖,所以第一个解决方案有效并且速度非常快。一个警告:defaultdict 中的任何0s 也将由生成的dok_matrix 存储;幸运的是,可以将数据转换为例如csr_matrix,然后拨打eliminate_zeros()
    • Py3.6 有新的dict 代码(默认排序等),因此速度可能会发生变化。
    【解决方案2】:

    您的测试不公平有多种原因。首先,您将构建稀疏矩阵的开销作为定时循环的一部分。

    其次,可以说更重要的是,您应该使用设计用于使用的数据结构,同时对整个数组进行操作。也就是说,与其遍历行和列并每次加 1,不如简单地将 1 加到整个数组中。

    【讨论】:

    • 你的第一点很公平。我做了一些快速测试,性能差异并没有太大变化。我的主要想法是初始化 defaultdict 将是一个等效的初始化,因为我最感兴趣的是增量构建的性能,所以我没有创建 coo_matrix 作为最后一步。不过,关于您的第二点,我不太确定我是否理解。对整个数组加 1 是什么意思?
    • OP 没有向整个数组添加 1。他在主对角线上加 1,实际上构造了一个sparse.eye(1000)。但我认为这只是迭代分配的一个例子,而不是最终目标。
    • @hpaulj 是的,我应该在我的例子中更清楚。感谢您的澄清。
    • @hpaulj 是的,完全正确。不过,要点是您不应该遍历矩阵(或 numpy)数组。如果您确实发现必须这样做,那么您就失去了所有优势,正如问题所指出的那样。
    猜你喜欢
    • 1970-01-01
    • 2012-05-05
    • 2011-02-11
    • 1970-01-01
    • 2012-05-25
    • 1970-01-01
    • 2016-02-13
    • 2020-11-25
    • 1970-01-01
    相关资源
    最近更新 更多