如何有效地存储具有高冗余值的矩阵

时间:2010-06-23 04:18:17

标签: algorithm data-structures sparse-matrix matrix-multiplication

我有一个非常大的矩阵(100M行乘100M列),它们有很多重复的值,彼此相邻。例如:

8 8 8 8 8 8 8 8 8 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 4 8 8 1 1 1 1 1 8 8 8 8
8 8 8 8 8 8 8 8 8 8 8 8 8
8 8 3 3 3 3 3 3 3 3 3 3 3

我想要一个数据结构/算法来尽可能紧凑地存储像这样的基质。例如,上面的矩阵应该只占用O(1)空间(即使矩阵被任意拉大),因为只有一定数量的矩形区域,每个区域只有一个值。

重复发生在行和列之间,因此逐行压缩矩阵的简单方法不够好。 (这将需要至少O(num_rows)空间来存储任何矩阵。)

矩阵的表示也需要逐行访问,这样我就可以对列向量进行矩阵乘法。

14 个答案:

答案 0 :(得分:13)

您可以将矩阵存储为quadtree,其中的叶子包含单个值。将其视为价值的二维“运行”。

答案 1 :(得分:10)

现在我的首选方法。

好的,正如我在之前的答案行中提到的那样,矩阵A中每列中的相同条目将在矩阵AB中相乘得到相同的结果。如果我们能够维持这种关系,那么理论上我们可以显着加快计算速度(分析器是你的朋友)。

在这个方法中,我们维护矩阵的行*列结构。

每行都用任何方法压缩,可以足够快地解压缩,不会过多地影响乘法速度。 RLE可能就足够了。

我们现在有一个压缩行列表。

我们使用熵编码方法(如Shannon-Fano,Huffman或算术编码),但我们不用此压缩行中的数据,我们用它来压缩行集。 我们用它来编码行的相对频率。即我们对待行的方式与标准熵编码处理字符/字节的方式相同。

在此示例中,RLE压缩 a 行,而Huffman压缩整行的 set

因此,例如,给定以下矩阵(前缀为行号,霍夫曼用于解释)

0 | 8 8 8 8 8 8 8 8 8 8 8 8 8 |
1 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
2 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
3 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
4 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
5 | 8 4 8 8 1 1 1 1 1 8 8 8 8 |
6 | 8 8 8 8 8 8 8 8 8 8 8 8 8 |
7 | 8 8 3 3 3 3 3 3 3 3 3 3 3 |

运行长度编码

0 | 8{13}                    |
1 | 8{1} 4{1} 8{2} 1{5} 8{4} |
2 | 8{1} 4{1} 8{2} 1{5} 8{4} |
3 | 8{1} 4{1} 8{2} 1{5} 8{4} |
4 | 8{1} 4{1} 8{2} 1{5} 8{4} |
5 | 8{1} 4{1} 8{2} 1{5} 8{4} |
6 | 8{13}                    |
7 | 8{2} 3{11}               |

因此,0和6出现两次,1 - 5出现5次。 7只一次。

频率表

A: 5 (1-5) | 8{1} 4{1} 8{2} 1{5} 8{4} |
B: 2 (0,6) | 8{13}                    |
C: 1    7  | 8{2} 3{11}               |

霍夫曼树

    0|1
   /   \
  A    0|1
      /   \
     B     C

因此,在这种情况下,需要一位(对于每一行)编码行1-5,并使用2位来编码行0,6和7。

(如果运行时间超过几个字节,那么就在你进行RLE时建立的哈希上对freq进行计数。)

存储Huffman树,唯一字符串和行编码位流。

关于Huffman的好处是它具有唯一的前缀属性,所以你总能知道你何时完成。因此,给定位串10000001011,您可以从存储的唯一字符串和树重建矩阵A.编码的比特流告诉您行出现的顺序。

您可能希望研究自适应霍夫曼编码或其算术对应物。

看到A中具有相同列条目的行与AB相对于矢量B的相同结果相乘,您可以缓存结果并使用它而不是再次计算(如果可以的话,最好避免100M * 100M乘法)

链接到更多信息:

Arithmetic Coding + Statistical Modeling = Data Compression

Priority Queues and the STL

Arithmetic coding

Huffman coding

比较

<强>未压缩

    0   1   2   3   4   5   6   7
  =================================
0 | 3   3   3   3   3   3   3   3 |
  |-------+               +-------|
1 | 4   4 | 3   3   3   3 | 4   4 |
  |       +-----------+---+       |
2 | 4   4 | 5   5   5 | 1 | 4   4 |
  |       |           |   |       |
3 | 4   4 | 5   5   5 | 1 | 4   4 |
  |---+---|           |   |       |
4 | 5 | 0 | 5   5   5 | 1 | 4   4 |
  |   |   +---+-------+---+-------|
5 | 5 | 0   0 | 2   2   2   2   2 |
  |   |       |                   |
6 | 5 | 0   0 | 2   2   2   2   2 |
  |   |       +-------------------|
7 | 5 | 0   0   0   0   0   0   0 |
  =================================

= 64字节

<强>四叉树

    0   1   2   3   4   5   6   7
  =================================
0 | 3 | 3 |       |       | 3 | 3 |
  |---+---|   3   |   3   |---+---|
1 | 4 | 4 |       |       | 4 | 4 |
  |-------+-------|-------+-------|
2 |       |       | 5 | 1 |       |
  |   4   |   5   |---+---|   4   |
3 |       |       | 5 | 1 |       |
  |---------------+---------------|
4 | 5 | 0 | 5 | 5 | 5 | 1 | 4 | 4 |
  |---+---|---+---|---+---|---+---|
5 | 5 | 0 | 0 | 2 | 2 | 2 | 2 | 2 |
  |-------+-------|-------+-------|
6 | 5 | 0 | 0 | 2 | 2 | 2 | 2 | 2 |
  |---+---+---+---|---+---+---+---|
7 | 5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
  =================================

0 +- 0 +- 0 -> 3
  |    +- 1 -> 3
  |    +- 2 -> 4
  |    +- 3 -> 4
  +- 1      -> 3
  +- 2      -> 4
  +- 3      -> 5
1 +- 0      -> 3
  +- 1 +- 0 -> 3
  |    +- 1 -> 3
  |    +- 2 -> 4
  |    +- 3 -> 4
  +- 2 +- 0 -> 5
  |    +- 1 -> 1
  |    +- 2 -> 5
  |    +- 3 -> 1
  +- 3      -> 4
2 +- 0 +- 0 -> 5
  |    +- 1 -> 0
  |    +- 2 -> 5
  |    +- 3 -> 0
  +- 1 +- 0 -> 5
  |    +- 1 -> 5
  |    +- 2 -> 0
  |    +- 3 -> 2
  +- 2 +- 0 -> 5
  |    +- 1 -> 0
  |    +- 2 -> 5
  |    +- 3 -> 0
  +- 3 +- 0 -> 0
       +- 1 -> 2
       +- 2 -> 0
       +- 3 -> 0
3 +- 0 +- 0 -> 5
  |    +- 1 -> 1
  |    +- 2 -> 2
  |    +- 3 -> 2
  +- 1 +- 0 -> 4
  |    +- 1 -> 4
  |    +- 2 -> 2
  |    +- 3 -> 2
  +- 2 +- 0 -> 2
  |    +- 1 -> 2
  |    +- 2 -> 0
  |    +- 3 -> 0
  +- 3 +- 0 -> 2
       +- 1 -> 2
       +- 2 -> 0
       +- 3 -> 0

((1*4) + 3) + ((2*4) + 2) + (4 * 8) = 49 leaf nodes 
49 * (2 + 1) = 147 (2 * 8 bit indexer, 1 byte data)
+ 14 inner nodes -> 2 * 14 bytes (2 * 8 bit indexers)
= 175 Bytes

区域哈希

    0   1   2   3   4   5   6   7
  =================================
0 | 3   3   3   3   3   3   3   3 |
  |-------+---------------+-------|
1 | 4   4 | 3   3   3   3 | 4   4 |
  |       +-----------+---+       |
2 | 4   4 | 5   5   5 | 1 | 4   4 |
  |       |           |   |       |
3 | 4   4 | 5   5   5 | 1 | 4   4 |
  |---+---|           |   |       |
4 | 5 | 0 | 5   5   5 | 1 | 4   4 |
  |   + - +---+-------+---+-------|
5 | 5 | 0   0 | 2   2   2   2   2 |
  |   |       |                   |
6 | 5 | 0   0 | 2   2   2   2   2 |
  |   +-------+-------------------|
7 | 5 | 0   0   0   0   0   0   0 |
  =================================

0: (4,1; 4,1), (5,1; 6,2), (7,1; 7,7)         | 3
1: (2,5; 4,5)                                 | 1
2: (5,3; 6,7)                                 | 1
3: (0,0; 0,7), (1,2; 1,5)                     | 2
4: (1,0; 3,1), (1,6; 4,7)                     | 2
5: (2,2; 4,4), (4,0; 7,0)                     | 2

区域:(3 + 1 + 1 + 2 + 2 + 2)* 5     = 55字节{4字节矩形,1字节数据)

{查找表是一个排序数组,所以它不需要额外的存储空间}。

霍夫曼编码RLE

0   | 3 {8}                                 | 1
1   | 4 {2} | 3 {4} | 4 {2}                 | 2
2,3 | 4 {2} | 5 {3} | 1 {1} | 4 {2}         | 4
4   | 5 {1} | 0 {1} | 5 {3} | 1 {1} | 4 {2} | 5
5,6 | 5 {1} | 0 {2} | 2 {5}                 | 3
7   | 5 {1} | 0 {7}                         | 2


RLE Data:    (1 + 3+ 4 + 5 + 3 + 2) * 2 = 36
Bit Stream:   20 bits packed into 3 bytes = 3
Huffman Tree: 10 nodes * 3 = 30
= 69 Bytes

一个巨大的RLE流

3{8};4{2};3{4};4{4};5{3};1{1};4{4};5{3};1{1};4{2};5{1};0{1};
5{3};1{1};4{2};5{1};0{2};2{5};5{1};0{2};2{5};5{1};0{7}

= 2 * 23 = 46 Bytes

使用公共前缀折叠编码的一个巨型RLE流

3{8};
4{2};3{4};
4{4};5{3};1{1};
4{4};5{3};
1{1};4{2};5{1};0{1};5{3};
1{1};4{2};5{1};0{2};2{5};
5{1};0{2};2{5};
5{1};0{7}

0 + 0 -> 3{8};4{2};3{4};
  + 1 -> 4{4};5{3};1{1};

1 + 0 -> 4{2};5{1} + 0 -> 0{1};5{3};1{1};
  |                + 1 -> 0{2}
  |
  + 1 -> 2{5};5{1} + 0 -> 0{2};
                   + 1 -> 0{7}

3{8};4{2};3{4}           | 00
4{4};5{3};1{1}           | 01
4{4};5{3};1{1}           | 01
4{2};5{1};0{1};5{3};1{1} | 100
4{2};5{1};0{2}           | 101
2{5};5{1};0{2}           | 110
2{5};5{1};0{7}           | 111

Bit stream: 000101100101110111
RLE Data:  16 * 2 = 32
Tree:   : 5 * 2 = 10 
Bit stream: 18 bits in 3 bytes = 3
= 45 bytes

答案 2 :(得分:4)

如果您的数据非常正常,您可能会受益于以结构化格式存储它;例如您的示例矩阵可能存储为以下“填充矩形”指令列表:

(0,0)-(13,7) = 8
(4,1)-(8,5)  = 1

(然后要查找特定单元格的值,您将向后遍历列表,直到找到包含该单元格的矩形)

答案 3 :(得分:3)

正如Ira Baxter所说, 您可以将矩阵存储为四叉树,叶子包含单个值。

最简单的方法是使四叉树的每个节点覆盖一个区域2 ^ n x 2 ^ n, 每个非叶节点指向其4个大小为2 ^(n-1)x 2 ^(n-1)的子节点。

使用自适应四叉树可以获得稍微更好的压缩,允许不规则的细分。 然后每个非叶节点存储切点(B,G)并指向其4个子节点。 例如,如果某些非叶节点覆盖了从左上角的(A,F)到右下角的(C,H)的区域, 然后它的4个孩子覆盖区域 (A,F)至(B-1,G-1) (A,G)至(B-1,H) (B,F)至(C,G-1) (B,G)至(C,H)。

您将尝试为每个非叶节点选择(B,G)切割点,使其与数据中的某些实际划分对齐。

例如,假设你有一个矩阵,中间有一个小方块,里面有九个,其他地方都是零。

使用简单的两个四叉树幂,最终将得到至少21个节点:5个非叶节点,4个9叶节点和12个零叶节点。 (如果居中的小方块不是精确地距离左边缘和顶边缘的两个幂的距离,而不是本身的精确二次幂),你将得到更多的节点。 / p>

使用自适应四叉树,如果你足够聪明,可以在该方块的左上角选择根节点的切点,那么对于根的右下角的孩子,你可以在下方选择一个切点在方形的右角,你可以在9个节点中表示整个矩阵:2个非叶子节点,1个叶子节点用于9个节点,6个叶子节点用于零。

答案 4 :(得分:2)

你知道......间隔树吗?

间隔树是一种有效存储间隔的方法,然后查询它们。概括是Range Tree,可以适应任何维度。

在这里,您可以有效地描述矩形并为其附加值。当然,矩形可以重叠,这就是使它有效的原因。

0,0-n,n --> 8
4,4-7,7 --> 1
8,8-8,n --> 3

然后在查询某个特定位置的值时,会返回一个包含多个矩形的列表,需要确定最里面的一个:这是此位置的值。

答案 5 :(得分:1)

最简单的方法是在一个维度上使用游程编码,而不用担心其他维度。

(如果数据集不是那么庞大,将其解释为图像并使用标准的无损图像压缩方法也会非常简单 - 但是因为你必须努力使算法适用于稀疏矩阵,它不会那么简单。)

另一个简单的方法是尝试矩形泛光填充 - 从右上角的像素开始,然后将其增加到最大的矩形(宽度优先);然后将所有这些像素标记为“完成”并拍摄右上角剩余的像素,重复直到完成。 (您可能希望将这些矩形存储在某种BSP或四叉树中。)

一种非常有效的技术 - 不是最优的,但可能足够好 - 是使用二进制空间分区树,其中“空间”不是在空间上测量,而是由变化的数量来衡量。你会递归切割,以便你在左右两侧(或者顶部和底部 - 你想要保持正方形)有相同数量的变化,并且随着你的尺寸变小,所以你会削减尽可能多的尽可能改变。最终,你最终会切割出彼此分开的两个矩形,每个矩形都具有相同的数字;然后停下来(在X和y中用RLE编码会很快告诉你变化点在哪里。)

答案 6 :(得分:1)

对于尺寸为100M x 100M的矩阵,您对O(1)空间的描述令人困惑。当你有一个有限矩阵,那么你的大小是一个常数(除非生成矩阵的程序不会改变它)。因此,即使你将它与标量相乘,存储所需的空间量也是一个常数。绝对是读取和写入矩阵的时间不会是O(1)。

我可以想到稀疏矩阵可以减少存储这种矩阵所需的空间量。您可以将此稀疏矩阵写入文件并将其存储为tar.gz,这将进一步压缩数据。

我确实有一个问题,100M中的M表示什么?这是指兆字节/百万?如果是,则此矩阵大小将为100 x 10 ^ 6 x 100 x 10 ^ 6字节= 10 ^ 16/10 ^ 6 MB = 10 ^ 10/10 ^ 6 TB = 10 ^ 4 TB !!!你在用什么样的机器?

答案 7 :(得分:1)

我不确定为什么这个问题是社区维基,但事实如此。

我将依赖于您拥有线性代数应用程序的假设,并且您的矩阵具有矩形冗余类型。如果是这样,那么你可以比四叉树做得更好,比将矩阵切割成矩形更清晰(这通常是正确的想法)。

设M为你的矩阵,让v为你想要乘以M的向量,然后让 A是特殊矩阵

A = [1 -1  0  0  0]
    [0  1 -1  0  0]
    [0  0  1 -1  0]
    [0  0  0  1 -1]
    [0  0  0  0  1]

你还需要A的逆矩阵,我称之为B:

B = [1 1 1 1 1]
    [0 1 1 1 1]
    [0 0 1 1 1]
    [0 0 0 1 1]
    [0 0 0 0 1]

将矢量v乘以A是快速而简单的:您只需获取v的连续元素对的差异。将矢量v乘以B乘以快速且简单:Bv的条目是v的元素的部分和那你想用等式

Mv = B AMA B v

矩阵AMA是稀疏的:在中间,每个条目是M个4个条目的交替和,形成2 x 2平方。你必须在M的一个矩形的一角,这个交替的和是非零的。由于AMA是稀疏的,您可以将其非零条目存储在关联数组中,并使用稀疏矩阵乘法将其应用于向量。

答案 8 :(得分:0)

我对您显示的矩阵没有具体的答案。在有限元分析(FEA)中,您有包含冗余数据的矩阵。在我的研究生项目中实施FEA包时,我使用了天际线存储方法。

一些链接:

Intel page for sparse matrix storage

Wikipedia link

答案 9 :(得分:0)

首先要尝试的是现有的库和解决方案。让自定义格式最终处理您想要的所有操作需要做很多工作。稀疏矩阵是一个老问题,因此请务必阅读现有内容。

假设您找不到合适的内容,我建议使用基于行的格式。不要试图用超紧凑的表示来过于花哨,最终需要为代码中的每个小操作和错误进行大量处理。而是尝试分别压缩每一行。你知道你将不得不扫描每一行的矩阵向量乘法,让你自己很轻松。

我将从运行长度编码开始,看看它是如何工作的。一旦它工作,尝试添加一些技巧,如引用前一行的部分。因此,行可能被编码为:126个零,8个,直接从上面的行复制的1000个条目,32个零。对你的例子来说,这似乎非常有效。

答案 10 :(得分:0)

上述许多解决方案都很好。

如果您正在使用文件,请考虑面向文件 压缩工具,如压缩,bzip,zip,bzip2和朋友。 它们工作得很好,特别是如果数据包含冗余 ASCII字符。使用外部压缩工具消除了 代码中的问题和挑战,并将压缩 二进制和ASCII数据。

在您的示例中,您显示一个字符数字。 数字0-9可以用较小的四位表示 编码模式。您可以使用其他位 一个字节作为计数。四位为您提供额外的代码 逃避额外...但有一个谨慎到达 回到旧的Y2K错误,其中使用了两个字符 一年。来自一组的字节编码会给出 255年和相同的两个字节将跨越所有书面 历史,然后一些。

答案 11 :(得分:0)

您可能需要查看GIF format及其压缩算法。只需将您的矩阵视为位图......

答案 12 :(得分:0)

让我检查一下我的假设,如果没有其他原因,而不是指导我对这个问题的思考:

  1. 矩阵是高度冗余的,不一定是稀疏的。
  2. 我们希望最小化存储(在磁盘和RAM上)。
  3. 我们希望能够将A [m * n]乘以向量B [n * 1]以得到AB [m * 1]而不首先解压缩(至少不超过进行计算所需)。
  4. 我们不需要随机访问任何A [i * j]条目 - 所有操作都在矩阵上。
  5. 乘法在线完成(根据需要),因此必须尽可能高效。
  6. 矩阵是静态的。
  7. 可以尝试各种聪明的方案来检测矩形或自相似性等,但这最终会在进行乘法时损害性能。我提出了两个相对简单的解决方案。

    我将不得不向后退一步,所以请耐心等待我。

    如果数据主要偏向于水平重复,则以下情况可能会很好。

    将矩阵展平为数组(这实际上就是它存储在内存中的方式)。 E.g。

    A
    | w0 w1 w2 |
    | x0 x1 x2 |
    | y0 y1 y2 |
    | z0 z1 z2 |
    

    变为

    A’
    | w0 w1 w2 x0 x1 x2 y0 y1 y2 z0 z1 z2 |
    

    我们可以使用任何索引[i,j] = i * j.

    这一事实

    因此,当我们进行乘法运算时,我们迭代“矩阵”数组A',其中k = [0..m * n-1]并使用(k mod n)索引到向量B中,并使用向量AB转换为向量AB (k div n)。 “div”是整数除法。

    所以,例如,A[10] = z110 mod 3 = 110 div 3 = 3 A[3,1] = z1.

    现在,进行压缩。 我们进行磨机运行长度编码(RLE)的正常运行,但是对A'而不是A.对于扁平阵列,将会有更长的重复序列,因此压缩效果更好。然后在对运行进行编码之后,我们执行另一个过程,我们提取公共子串。我们可以做一种字典压缩形式,或者将运行数据处理成某种形式的空间优化图形,如基数树/后缀树或自己创建的合并顶部和尾部的设备。该图应该包含数据中所有唯一字符串的表示。您可以选择任意数量的方法将流分解为字符串:匹配前缀,长度或其他内容(最适合您的图形)但是在运行边界上执行,而不是字节,否则您的解码将变得更加复杂。当我们解压缩流时,图形成为状态机。

    我将使用位流和Patricia trie作为示例,因为它最简单,但您可以使用其他内容(每个状态更多位更改更好合并等等。通过{{查找论文3}})。

    为了压缩运行数据,我们针对图形构建了一个哈希表。该表将字符串映射到位序列。您可以通过遍历图形并将每个左分支编码为0并将右分支编码为1(任意选择)来完成此操作。

    处理运行数据并构建一个位字符串,直到在哈希表中得到匹配,输出位并清除字符串(这些位不在字节边界上,所以你可能需要缓冲直到你得到一个足够长的序列来写出来)。冲洗并重复,直到您处理完整的运行数据流。您存储图形和位流。比特流对字符串进行编码,而不是字节。

    如果您反转该过程,使用位流来遍历图形,直到到达叶子/终端节点,您将返回原始运行数据,您可以动态解码该数据以生成您乘以的整数流得到AB的向量B.每次运行耗尽时,都会读取下一位并查找其对应的字符串。我们不关心我们没有随机访问A,因为我们只需要它在B中(B可以是范围/间隔压缩但不需要)。

    因此,即使RLE偏向水平运行,我们仍然可以获得良好的垂直压缩,因为常见字符串只存储一次。

    我将在单独的答案中解释另一种方法,因为它太长了,但是由于矩阵A中的重复行与AB中的相同结果相乘,该方法实际上可以加速计算。 / p>

答案 13 :(得分:0)

你需要一个压缩算法尝试 RLE (运行长度编码),当数据是 高度冗余。