前言
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=A⊙B,其中 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【下篇】