python中稀疏矩阵的常用表示COO LIL CSR CSC【上篇】

前言

sklearn调用独热编码函数encoder.fit_transform()返回的是scipy.sparse._csr.csr_matrix类型。
torch的tensor也是一种matrix,各中差别让我感觉混乱。今天梳理一下,网上看到前辈有很好的帖子,翻译转载如下:

简介

稀疏矩阵是大多数元素的值为0的矩阵。如果Number of Non-Zero (NNZ)非零元素个数的占比小于0.5,则矩阵是稀疏的。
在这里插入图片描述
存储所有0元素是低效的,因此我们假设未声明的元素都是0。利用这种方法,稀疏矩阵可以比相应的密集矩阵表示方法执行更快的操作,并且使用更少的内存,这在处理数据科学中的大数据集时尤为重要。

今天,我们将研究 SciPy sparse package提供的所有不同实现。这个实现是模仿 np.Matrix 而不是 np.ndarray,因此仅限于2维数组,并且np.matrix里的像 A* B这样的矩阵乘法而不是orch.Tensor里的逐元素相乘

Matrix multiplication(矩阵乘法)和element-wise multiplication(逐元素乘法)是两种不同的矩阵运算。

Matrix multiplication是两个矩阵相乘得到一个新的矩阵,其中第一个矩阵的列数必须等于第二个矩阵的行数。矩阵乘法可以表示为 C = A B C = AB C=AB,其中 A A A B B B是两个矩阵, C C C是乘积矩阵。在矩阵乘法中,每个元素的计算都涉及到两个矩阵中的多个元素的加权和,因此这种运算通常会涉及到高复杂度的计算。【np.matrx使用的就是矩阵乘法】

Element-wise multiplication是两个矩阵中的对应元素相乘得到一个新的矩阵,其中两个矩阵必须具有相同的形状。元素乘积可以表示为 C = A ⊙ B C = A \odot B C=AB,其中 A A A B B B是两个矩阵, C C C是元素乘积矩阵。在逐元素乘法中,矩阵中的每个元素都是独立计算的,因此计算的复杂度要比矩阵乘法低得多。【而torch.Tensor使用的就是逐元素相乘】

n [0]: from scipy import sparse

In [1]: import numpy as np

In [2]: spmatrix = sparse.random(10, 10)

In [3]: spmatrix
Out[3]:
<10x10 sparse matrix of type '<class 'numpy.float64'>'
        with 1 stored elements in COOrdinate format>

In [4]: spmatrix.nnz / np.product(spmatrix.shape)  # sparsity
Out[4]: 0.01

构造矩阵

不同的稀疏矩阵格式各有优缺点。一个好的起点是寻找对构造这些矩阵有效的格式。一般来说你都是从其中一种格式开始,然后转换成另一种格式用于计算。

坐标矩阵(Coordinate Matrix)

要理解的最简单的稀疏格式可能是 COO 格式(Coordinate Matrix)。此变体使用三个子数组来存储元素值及其坐标位置。

在这里插入图片描述

In [5]: row = [1, 3, 0, 2, 4]

In [6]: col = [1, 4, 2, 3, 3]

In [7]: data = [2, 5, 9, 1, 6]

In [8]: coo = sparse.coo_matrix((data, (row, col)), shape=(6, 7))

In [9]: print(coo)  # coordinate-value format
# (1, 1)        2
# (3, 4)        5
# (0, 2)        9
# (2, 3)        1
# (4, 3)        6

In [10]: coo.todense()  # coo.toarray() for ndarray instead
Out[10]:
matrix([[0, 0, 9, 0, 0, 0, 0],
        [0, 2, 0, 0, 0, 0, 0],
        [0, 0, 0, 1, 0, 0, 0],
        [0, 0, 0, 0, 5, 0, 0],
        [0, 0, 0, 6, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0]])

随着矩阵大小的增加,节省的内存消耗是相当可观的。在稀疏结构中管理数据的成本是固定的,这与密集矩阵的情况不同。由于需要管理子阵列而产生的开销可以忽略不计,大数据场景下,这种稀疏结构使其成为某些数据集的一个很好的选择。

In [11]: def memory_usage(coo):
    ...:    # data memory and overhead memory
    ...:    coo_mem = (sum(obj.nbytes for obj in [coo.data, coo.row, coo.col])
    ...:               + sum(obj.__sizeof__() for obj in [coo, coo.data, coo.row, coo.col]))
    ...:    print(f'Sparse: {coo_mem}')
    ...:    mtrx = coo.todense()
    ...:    mtrx_mem = mtx.nbytes + mtrx.__sizeof__()
    ...:    print(f'Dense: {mtrx_mem}')

In [12]: memory_usage(coo)
# Sparse: 480
# Dense: 448

In [13]: coo.resize(100, 100)

In [14]: memory_usage(coo)
# Sparse: 480
# Dense: 80112

键值字典矩阵(Dictionary of Keys Matrix)

Dictionary Of Keys (DOK)与 COO 非常相似,只是它的子类 dict 将坐标数据信息存储为键-值对。由于它使用哈希表作为存储,因此在任何给定位置标识值都需要固定的查找时间。如果需要内置字典的功能,可以使用这种格式,但是要注意,哈希表比数组占用更多的内存。

In [15]: dok = sparse.dok_matrix((10, 10))

In [16]: dok[(3, 7)] = 42  # store value 42 at coordinate (3, 7)

In [17]: dok[(9, 5)]  # zero elements are accessible
Out[17]: 0.0

In [18]: dok.keys() | dok.transpose().keys()  # union of key views
Out[18]: {(3, 7), (7, 3)}

In [19]: isinstance(dok, dict)
Out[19]: True

注意: 使用从 dict 继承的方法时要小心潜在的问题; 它们并不总是表现良好。

# 例如

In [20]: out_of_bounds = (999, 999)  # 定义一个元组,表示矩阵中一个超出范围的索引

In [21]: dok[out_of_bounds] = 1  # 此行代码按预期会引发IndexError,因为out_of_bounds是一个超出范围的索引

IndexError: Index out of bounds. # 正常执行了

In [22]: dok.setdefault(out_of_bounds)  # 该行代码被静默地忽略了,没有抛出异常...

In [23]: dok.toarray()  # ...直到这里。该行代码才引发ValueError异常,因为矩阵的行索引超出了矩阵维度的范围。

ValueError: row index exceeds matrix dimensions # dok.toarray()返回的异常

In [24]: dok.pop(out_of_bounds)  # 通过删除超出范围的点来解决问题

In [25]: sparse.dok_matrix.fromkeys([..., ..., ...])  # 一整个无语了就,作者这里原文用了一个don't let me started,就是我都抱怨烦了这个问题。这行代码存在问题,它会提醒我缺少一个必需的参数'arg1'

TypeError: __init__() missing 1 required positional argument: 'arg1' # 缺少一个必需的参数'arg1'的报错

链表矩阵(Linked List Matrix)

插入数据的最灵活的格式是使用LInked List (LIL)链表矩阵。可以通过 NumPy 的索引和切片语法来设置数据,从而快速填充矩阵。在作者看来,LIL 是从头构造稀疏矩阵的最酷的稀疏格式。
请添加图片描述LIL 将信息存储在 LIL.rows 中,其中每个列表表示一个行索引,列表中的元素匹配列。在并行数组lil.data中,存储 NNZ 值。但是与其他稀疏格式不同的是,这些子数组不能显式地传递给构造函数; LIL 矩阵必须从空状态或从现有的密集或稀疏矩阵中生成。下面是用于构建 LIL 矩阵的各种技术的示例。


In [26]: lil = sparse.lil_matrix((6, 5), dtype=int)  # 创建一个6x5的稀疏矩阵,元素数据类型为int,格式是lil,

In [27]: lil[(0, -1)] = -1  # 给一个单独的点复制

In [28]: lil[3, (0, 4)] = [-2] * 2  # 设置两个点

In [29]: lil.setdiag(8, k=0)  # 设置主对角线上的元素为8

In [30]: lil[:, 2] = np.arange(lil.shape[0]).reshape(-1, 1) + 1  # 设置整个第3列

In [31]: lil.toarray()  # 将稀疏矩阵转换为密集矩阵输出
Out[31]:
array([[ 8,  0,  1,  0, -1],
       [ 0,  8,  2,  0,  0],
       [ 0,  0,  3,  0,  0],
       [-2,  0,  4,  8, -2],
       [ 0,  0,  5,  0,  8],
       [ 0,  0,  6,  0,  0]])

那么缺点是什么?它利用了底层的 (jagged arrays)锯齿数组(又译:不规则数组),这需要 np.dtype (object)。这比矩形数(rectangular array)组消耗更多的内存,所以如果数据足够大,您可能会被迫使用 COO 而不是 LIL。简而言之,IL 矩阵虽然存在一些缺点,但仍然是一个非常棒的选择!

In [32]: lil.rows  # 查看 LIL 矩阵的行索引

Out[32]:
array([list([0, 2, 4]), list([1, 2]), list([2]), list([0, 2, 3, 4]),
       list([2, 4]), list([2])], dtype=object)

In [33]: lil.data[:, np.newaxis]  # 展示jagged锯齿结构/不规则结构

Out[33]:
array([[list([8, 1, -1])],
       [list([8, 2])],
       [list([3])],
       [list([-2, 4, 8, -2])],
       [list([5, 8])],
       [list([6])]], dtype=object)

顺便说一句,链表矩阵LIL是一个用词不当,因为它不使用链表幕后!LIL 实际上使用了 Python 的 List,它是一个动态数组,所以不管文档中怎么说,它实际上应该被称为 List Matrix List。(错过了给它命名为 LOL 的机会…)

In [34]: sparse.lil.__doc__  # 读取lili模块的文档

Out[34]: 'LInked List sparse matrix class\n' # 我是一个链表稀疏矩阵唷(其实才不是嘞!)

中断

CSR,CSC的介绍见python中稀疏矩阵的常用表示COO LIL CSR CSC【下篇】

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值