稀疏矩阵的最小表示

Posted

技术标签:

【中文标题】稀疏矩阵的最小表示【英文标题】:Minimal representation of sparse matrix 【发布时间】:2015-08-02 11:19:19 【问题描述】:

我有一个大的二进制稀疏矩阵(任何单元格都可以保存 0 或 1 作为值)。有时我想拍摄整个矩阵的快照。 快照必须尽可能小

矩阵表示 2d 地图和发生在区域中的事件,因此它更有可能具有看起来像示例 A 的快照而不是看起来像示例 B 的快照(它们都具有相同数量的“1”),尽管我需要在算法中支持这两个例子。

Example A:
000000000000
000000011000
001100111100
001111111100
000000111100
000001111100
000000000000

Example B:
010010010010
001000001000
010010100100
000100101010
001000010010
010010001100
001000010000

由于数据可以从单个“1”单元格到 100% 的单元格为“1”(在非常后面的情况下)我认为我需要使用多个算法 - 并且在加载要加载的数据时它与存储它的算法相同。

例如,当只有一个单元格时,我将只存储它的索引(以及“索引”算法的标识符),当 99% 的矩阵为“1”时,我会将其存储为位图(以及“位图”算法)。

所以我得到了一个像这样的通用算法:

    对于每个表示算法 - 表示矩阵 选择最小的表示 存储具有最小表示的数据

我的问题

    除了存储索引/位图之外,我还可以使用哪些算法? 是否有处理此问题的良好参考资料? 如何证明我的解决方案是最小可能的?

底线: 如何以最小的方式存储位图矩阵?

编辑 用例:我有一个稀疏矩阵,我需要在一个非常低的带速介质上传输。所以发送矩阵应该包含尽可能少的比特,假设介质两边的计算能力都很强。

【问题讨论】:

你无法证明它是最小的(如果你有一个准确的数据概率模型,你可以得到一个估计)。计算时间以及代码复杂性/开发工作量通常也是一个重要因素。最佳权衡取决于您的应用程序,请描述您的用例。 试试 gzip 之类的标准压缩程序,看看它们能带你走多远。 @Antoine - 添加了用例,现在让我们忽略代码复杂性和开发的问题。努力 @n.m. - 我可以这样做,但是大多数数据看起来像巨大的零矩阵上的“污点”这一事实是否有帮助? gzip太笼统了…… 它可能仍然足够好(或者不是,尝试一下)。 【参考方案1】:

数据压缩是一个很大的领域(您可以start here),您的问题没有明确的答案。如果我们知道如何以最佳速率压缩数据,就不会每年都有新的压缩算法了;)

话虽如此,您的建议是个好主意:从集合中选择最佳算法并在标题中识别它。事实上,大多数压缩格式都使用这种方案。

回答您的问题:

什么算法:

使用现有格式:想到 PNG、GIF 或 7zip。 无压缩:很明显,位图,但为了确保我们说的是同一件事,您需要对每个元素编码 1 位(而不是 1 字节)。在大多数语言中,位访问并不容易(对于大多数 bool/boolean 类型存储在一个完整字节上)。您必须使用按位运算、bitfields/bitsets 等专用语言功能或库。例如C++std::vector<bool> 实现了一个真正的位图(一个简单的bool[] 没有)。 熵编码:使用比不太可能的配置更少的存储来编码最可能的配置的想法。 稀疏存储,您所说的索引,但与您暗示的相反,当有很多 1 时,它很有用,但也有很多 0 (只是否定结果!)。只有一个0 或只有一个1 的情况是对称的,应该以同样的方式处理。备用存储的最坏情况是 1 和 0 一样多。 块编码,这意味着如果您知道1111 是一种可能的模式,您可以将其存储在少于 4 位的空间中。但这意味着另一个(不太可能)4 位模式将使用超过 4 位存储,因此在选择模式时要小心。有很多方法可以做到这一点:固定大小或可变大小的字,编码可以是静态的或依赖于数据的。一些示例包括:RLE、Huffman、LZ 变体(您最喜欢的压缩程序中使用的基于字典的算法) Arithmetic coding 基于相同的想法,即根据概率调整每个符号/块的存储空间,但它一次性对整个数据进行编码,而不是将其拆分为块,这通常会导致更好的编码方案。李> 由于您的数据是二维的,因此通过 2D 块而不是 1D 块来处理它是有意义的,例如,您可以对 8x8 切片进行编码。以JPEG 为例。 数据准备:在应用您的熵编码算法(实际减少数据大小的算法)之前,应用双射(双向函数,不会减少数据)通常很有趣size) 来编码你对数据模型的知识。在您的情况下,您说您的数据代表事件,并且这些事件通常是特别聚集的。有没有办法将其表示为事件列表+它们传播的方式?或者您可以使用一些图像压缩方案,例如Fourier transform 或某种wavelet transform。或者您可以简单地使用contour matrix(当两个单元格之间的值发生变化时为一个,当值恒定时为零),这可能会增加矩阵的稀疏性(它在您的示例A中确实如此)

极简证明:

没有一般最佳的压缩算法,因为压缩实际上取决于数据的概率分布。事实上,如果你知道你的矩阵总是一样的,你甚至不需要对它进行编码。你知道它是两个可能的矩阵之一,只需一位就足以编码它是哪一个。

在一般情况下,Shanon 的 entropy 估计了编码消息所需的理论最小位数:

min = E( -log2(P(message)) )

其中E 是对所有可能消息的期望,P 是概率函数。但在实践中,你不知道P,所以你能做到的最好的比以前最好的算法做得更好;)

一般而言,您尝试压缩数据的次数越多,您的程序在运行时资源(CPU 周期和内存)和开发工作量方面的成本就越高。

一个例子

只是在您的示例 1 上付诸实践,为您提供灵感——即使从 1 个示例进行设计是一个非常糟糕的主意,因为它几乎不能为您提供具有代表性的概率! p>

初始数据(我将始终省略大小标头,因为它将保持不变)——位图,84 位(25 个):

000000000000
000000011000
001100111100
001111111100
000000111100
000001111100
000000000000

在您的index 方案中,您输出一个列表position。如果进行概括,您可以使用position + pattern 的列表。例如,pattern 可能是连续的个数,因此您不需要为一个块输出多个位置。让我们更进一步,假设我们对 2D 模式进行编码,定义为:

3 位大小(0 到 7)

1 位形状:

0 表示一行 - 例如 1111 是大小为 4 的行;

1 表示正方形 - 例如:

11
11

是一个大小为 2 的正方形。

然后让我们说相对位置而不是绝对位置,这意味着每个position 都会编码自上一个位置以来您前进了多少。对于您的示例,我选择 5 位位置。以下是解码的工作原理:

-- block #1
00001 position +1
      absolute position from top-left, since this is the first block
001   pattern-size 1
0     pattern-shape is row
      for size 1, square and row are the same
      (which means my coding isn't optimal)
-- block #2
00100 position +4
      relative to last position 1, this means position 5
010   pattern-size 2
1     pattern-shape is square
-- decoded output below
0100011000...
0000011000...
0000000000...
.............

因此,使用此方案,您可以使用 45 位对数据进行编码:

10100 (position [0]+20 from top-left)
010 0 (size: 2, shape: row)
00110 (position [20]+6)
010 1 (size: 2, shape: square)
00100 (position +4)
100 1 (size: 4, shape: square)
01000 (position +8)
100 0 (size: 4, shape: row)
11011 (position +27)
001 0 (size 1, shape: row)

注意:通常您希望存储一个标头以了解块的数量(在本例中为 5 个),但您可以从文件/网络有效负载大小中推断出它。同样在这个例子中,我们只需要 3 个模式大小 (1,2,4),因此我们可以将大小存储在两个位上并将其提高到 2 次方,使有效负载为 40 位,但这使得方案过于专业。此外,必须至少有一个无意义的shape(这里有两个:000/0 和 000/1),以防position 中没有足够的位来编码之间的大“洞”模式。例如这个 20 x 3 矩阵:

10000000000000000000
00000000000000000000
00000000000000000001

在位置 0 和 59 有一个。由于我无法将 59 编码为 5 位跳转,因此我们必须跳转两次,我们将其编码为:

00000 (+0)  001 0 (a single 1)
11111 (+31) 000 0 (nothing)
11100 (+28) 001 0 (a single 1)

【讨论】:

感谢您的详细示例。您能否解释/提供 2D-RLE 算法的参考? @yossico:对不起,我想我只是编造了这个名字。我添加了更多示例和解释,希望这样更清楚。无论如何,它很可能不是最适合你的算法——只是一个合理的起点;) 我希望我能在我的面试中得到这个精彩的答案!【参考方案2】:

您已经提到了一些存储这些的明显方法 - 位图和1 单元格的索引。通过对“索引”方法的标识符和1 单元格的数量进行编码,然后是许多坐标对,可以轻松地扩展索引的想法。您甚至可以尝试通过按行(或列)分组来进行压缩,例如

rowNumber colNumber1 colNumber2 ... -1

使用-1 或其他一些标志值来指示行的结束。这可以为只有几个条目的大型矩阵节省大量空间。您还需要存储矩阵大小。

对于示例 A,您会得到(使用 0 索引,空格只是为了便于阅读)

7 12 //dimensions
1 7 8 -1 
2 2 3 6 7 8 9 -1
3 2 3 4 5 6 7 8 9 -1
4 6 7 8 9 -1
5 5 6 7 8 9 -1

对于您的情况,存储它的另一个示例可能如下 - 您必须进行试验,看看它在“压缩”信息方面有多成功。该算法的想法来自于在您的 A 示例中观察到,几乎所有行都只有一个由 1 组成的大连接部分,被 0 包围。

假设您的矩阵可以有任何维度,第一步就是对其进行编码。所以对于一个 n * m 矩阵,只需存储这两个整数。

接下来,对于每一行,以下内容如何:

将连接的 0 的数量存储在左侧。 存储在此之后连接的 1 的数量。 重复直到行尾。

要解码,您只需遵循如下流程:

对于每一行(在给定的行数中) 设count 为到目前为止读取的矩阵条目数。初始化为0next 为布尔值,表示下一个数字编码的值。初始化为false。 读取下一个数字 将等于next 的值插入相应的行。 翻转nextcount 增加该数字 循环直到count == m(如果count > m出现问题) 循环直到读取所有行

我希望我已经很好地解释了这个想法。正如我所说,我不知道它的表现如何,但它源于对你的例子的观察。要真正压缩它,您可以确保每个数字条目只需要尽可能多的位来计数(并且不高于)行宽(在我的伪代码中为m。)

示例 A 会出现类似这样的内容(空格只是为了便于阅读):

7 12 //dimensions
12
7 2 3
2 2 2 4 2
2 8 2
6 4 2
5 5 2
12

如果您对其进行优化,则每个数字只需要 4 位。

我不会尝试做示例 B,但它的表现会差很多

【讨论】:

注意:发明新方法可能是一项毫无意义的练习,因为那里有通用的压缩算法 - 但观察您案例的特定特征通常会导致优化,这是不可能的执行通用算法,仅仅是因为它对情况的了解比你少。 第一种方法的优点是它可以根据1 值的数量大致缩放,这很好且可预测。同样对其进行解码并将其存储在新矩阵中将花费与该数字成比例的时间(假设您的新矩阵条目默认为0)。第二个的优点是它随着行数线性扩展,但是对于给定的每行“块”的最大数量(我猜这对于许多情况来说可能是合理的假设)它是恒定的关于行长。解码时填充矩阵所需的时间将随矩阵的大小而变化。 P.s(对不起)我怀疑一个简单的位图在这种情况下几乎会表现得特别好。但是您始终可以将标准无损压缩算法应用于您输出的任何内容。在示例 A 中,位图需要 84 位(对于实际条目 - 通常您仍然需要存储矩阵的维度),而我的第二个算法(仅略微)将其提高到 76 位。如果您真的关心压缩,则可以在存储之前使用标准无损压缩遵循您选择的任何“手动”压缩。可能是个好方法。 这看起来像en.wikipedia.org/wiki/Run-length_encoding。单次运行的二进制值没有明确存储,但从交替运行中可以明显看出。每次运行的长度也可以编码为en.wikipedia.org/wiki/Variable-length_quantity 嘿,是的,我认为我们在这里重新发明了 RLE :) 这是一个很好的建议,可以尝试对游程进行 varint 编码 - 其值取决于原始游程的分布数组。

以上是关于稀疏矩阵的最小表示的主要内容,如果未能解决你的问题,请参考以下文章

稀疏矩阵--三元组表示法和十字链表示法

Scipy---6.稀疏矩阵

稀疏矩阵的压缩存储思想?

设计算法,将m*n稀疏矩阵转换成三元组表示,并分析其时间复杂度和空间复杂度

有效地找到稀疏矩阵的最小列的索引

稀疏矩阵及其压缩格式