【问题标题】:Lossless compression in small blocks with precomputed dictionary使用预计算字典对小块进行无损压缩
【发布时间】:2010-11-07 12:27:22
【问题描述】:

我有一个应用程序,我在其中读取和写入数亿次小数据块(几百字节)。我想根据示例数据文件生成一个压缩字典,并在我读写小块时永远使用该字典。我倾向于 LZW 压缩算法。维基百科页面 (http://en.wikipedia.org/wiki/Lempel-Ziv-Welch) 列出了用于压缩和解压缩的伪代码。修改它看起来相当简单,以便字典创建是一个单独的代码块。所以我有两个问题:

  1. 我是在正确的轨道上还是有更好的方法?
  2. 为什么 LZW 算法在解压步骤中会添加到字典中?我可以省略它,否则我的字典会失去效率吗?

谢谢。

更新:现在我认为理想的情况是找到一个库,让我可以将字典与压缩数据分开存储。有这样的东西吗?

更新:我最终采用了http://www.enusbaum.com/blog/2009/05/22/example-huffman-compression-routine-in-c 的代码并对其进行了调整。我是该页面上 cmets 中的 Chris。我将我的模组通过电子邮件发送回了那个博客作者,但我还没有收到回复。我使用该代码看到的压缩率一点也不令人印象深刻。这可能是由于 8 位树的大小。

更新:我将其转换为 16 位,压缩效果更好。它也比原始代码快得多。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace Book.Core
{
  public class Huffman16
  {
    private readonly double log2 = Math.Log(2);

    private List<Node> HuffmanTree = new List<Node>();

    internal class Node
    {
      public long Frequency { get; set; }
      public byte Uncoded0 { get; set; }
      public byte Uncoded1 { get; set; }
      public uint Coded { get; set; }
      public int CodeLength { get; set; }
      public Node Left { get; set; }
      public Node Right { get; set; }

      public bool IsLeaf
      {
        get { return Left == null; }
      }

      public override string ToString()
      {
        var coded = "00000000" + Convert.ToString(Coded, 2);
        return string.Format("Uncoded={0}, Coded={1}, Frequency={2}", (Uncoded1 << 8) | Uncoded0, coded.Substring(coded.Length - CodeLength), Frequency);
      }
    }

    public Huffman16(long[] frequencies)
    {
      if (frequencies.Length != ushort.MaxValue + 1)
      {
        throw new ArgumentException("frequencies.Length must equal " + ushort.MaxValue + 1);
      }
      BuildTree(frequencies);
      EncodeTree(HuffmanTree[HuffmanTree.Count - 1], 0, 0);
    }

    public static long[] GetFrequencies(byte[] sampleData, bool safe)
    {
      if (sampleData.Length % 2 != 0)
      {
        throw new ArgumentException("sampleData.Length must be a multiple of 2.");
      }
      var histogram = new long[ushort.MaxValue + 1];
      if (safe)
      {
        for (int i = 0; i <= ushort.MaxValue; i++)
        {
          histogram[i] = 1;
        }
      }
      for (int i = 0; i < sampleData.Length; i += 2)
      {
        histogram[(sampleData[i] << 8) | sampleData[i + 1]] += 1000;
      }
      return histogram;
    }

    public byte[] Encode(byte[] plainData)
    {
      if (plainData.Length % 2 != 0)
      {
        throw new ArgumentException("plainData.Length must be a multiple of 2.");
      }

      Int64 iBuffer = 0;
      int iBufferCount = 0;

      using (MemoryStream msEncodedOutput = new MemoryStream())
      {
        //Write Final Output Size 1st
        msEncodedOutput.Write(BitConverter.GetBytes(plainData.Length), 0, 4);

        //Begin Writing Encoded Data Stream
        iBuffer = 0;
        iBufferCount = 0;
        for (int i = 0; i < plainData.Length; i += 2)
        {
          Node FoundLeaf = HuffmanTree[(plainData[i] << 8) | plainData[i + 1]];

          //How many bits are we adding?
          iBufferCount += FoundLeaf.CodeLength;

          //Shift the buffer
          iBuffer = (iBuffer << FoundLeaf.CodeLength) | FoundLeaf.Coded;

          //Are there at least 8 bits in the buffer?
          while (iBufferCount > 7)
          {
            //Write to output
            int iBufferOutput = (int)(iBuffer >> (iBufferCount - 8));
            msEncodedOutput.WriteByte((byte)iBufferOutput);
            iBufferCount = iBufferCount - 8;
            iBufferOutput <<= iBufferCount;
            iBuffer ^= iBufferOutput;
          }
        }

        //Write remaining bits in buffer
        if (iBufferCount > 0)
        {
          iBuffer = iBuffer << (8 - iBufferCount);
          msEncodedOutput.WriteByte((byte)iBuffer);
        }
        return msEncodedOutput.ToArray();
      }
    }

    public byte[] Decode(byte[] bInput)
    {
      long iInputBuffer = 0;
      int iBytesWritten = 0;

      //Establish Output Buffer to write unencoded data to
      byte[] bDecodedOutput = new byte[BitConverter.ToInt32(bInput, 0)];

      var current = HuffmanTree[HuffmanTree.Count - 1];

      //Begin Looping through Input and Decoding
      iInputBuffer = 0;
      for (int i = 4; i < bInput.Length; i++)
      {
        iInputBuffer = bInput[i];

        for (int bit = 0; bit < 8; bit++)
        {
          if ((iInputBuffer & 128) == 0)
          {
            current = current.Left;
          }
          else
          {
            current = current.Right;
          }
          if (current.IsLeaf)
          {
            bDecodedOutput[iBytesWritten++] = current.Uncoded1;
            bDecodedOutput[iBytesWritten++] = current.Uncoded0;
            if (iBytesWritten == bDecodedOutput.Length)
            {
              return bDecodedOutput;
            }
            current = HuffmanTree[HuffmanTree.Count - 1];
          }
          iInputBuffer <<= 1;
        }
      }
      throw new Exception();
    }

    private static void EncodeTree(Node node, int depth, uint value)
    {
      if (node != null)
      {
        if (node.IsLeaf)
        {
          node.CodeLength = depth;
          node.Coded = value;
        }
        else
        {
          depth++;
          value <<= 1;
          EncodeTree(node.Left, depth, value);
          EncodeTree(node.Right, depth, value | 1);
        }
      }
    }

    private void BuildTree(long[] frequencies)
    {
      var tiny = 0.1 / ushort.MaxValue;
      var fraction = 0.0;

      SortedDictionary<double, Node> trees = new SortedDictionary<double, Node>();
      for (int i = 0; i <= ushort.MaxValue; i++)
      {
        var leaf = new Node()
        {
          Uncoded1 = (byte)(i >> 8),
          Uncoded0 = (byte)(i & 255),
          Frequency = frequencies[i]
        };
        HuffmanTree.Add(leaf);
        if (leaf.Frequency > 0)
        {
          trees.Add(leaf.Frequency + (fraction += tiny), leaf);
        }
      }

      while (trees.Count > 1)
      {
        var e = trees.GetEnumerator();
        e.MoveNext();
        var first = e.Current;
        e.MoveNext();
        var second = e.Current;

        //Join smallest two nodes
        var NewParent = new Node();
        NewParent.Frequency = first.Value.Frequency + second.Value.Frequency;
        NewParent.Left = first.Value;
        NewParent.Right = second.Value;

        HuffmanTree.Add(NewParent);

        //Remove the two that just got joined into one
        trees.Remove(first.Key);
        trees.Remove(second.Key);

        trees.Add(NewParent.Frequency + (fraction += tiny), NewParent);
      }
    }

  }

}

用法示例:

从样本数据创建字典:

var freqs = Huffman16.GetFrequencies(File.ReadAllBytes(@"D:\nodes"), true);

使用给定字典初始化编码器:

var huff = new Huffman16(freqs);

并进行一些压缩:

var encoded = huff.Encode(raw);

并解压:

var raw = huff.Decode(encoded);

【问题讨论】:

  • 您的块大小是否一致?
  • 另外,我已经对一个示例数据文件进行了 RAR 编辑,并确认数据是高度可压缩的(~9x 压缩比)。
  • 不,它们的大小不一致。
  • 此外,这些块是随机访问的,因此它们必须能够完全独立地进行压缩/解压缩。
  • 大部分压缩将来自标记化(用单个 # 替换数据字符串) - Huffman 是二阶改进,只有在输入标记的频率变化很大时才能正常工作。在这种情况下,例如,一些令牌的长度为 2 或 3 位,而另一些令牌的长度为 12 位。

标签: algorithm compression


【解决方案1】:

我发现这种方法对于重复的日志条目和我想探索使用的东西非常有趣。

您能否分享在您的用例中使用这种方法的压缩统计数据,以便我可以将其与其他替代方法进行比较?

您是否考虑过让通用词典随着时间的推移而增长,或者这不是一个有效的选择?

【讨论】:

  • 抱歉,我没有任何统计数据。就我而言,我不需要字典来增长。
【解决方案2】:

在我看来,最难的部分是如何构建静态字典。您不想使用从样本数据构建的 LZW 字典。 LZW 浪费了大量时间学习,因为它无法比解压缩器更快地构建字典(令牌只会在压缩器第二次看到时使用,因此解压缩器可以在第一次看到时将其添加到字典中) .这样做的另一面是它向字典中添加了可能永远不会使用的东西,以防万一字符串再次出现。 (例如,要拥有 'stackoverflow' 的标记,您还将拥有 'ac'、'ko'、've'、'rf' 等的条目......)

但是,查看来自 LZ77 算法的原始令牌流可能效果很好。您只会看到至少出现两次的字符串的标记。然后,您可以构建一个最常见的标记/字符串列表以包含在您的字典中。

一旦你有了静态字典,使用 LZW sans 字典更新似乎是一个简单的实现,但为了获得最佳压缩,我会考虑使用静态 Huffman 表而不是传统的 12 位固定大小令牌(如 George Phillips建议)。 LZW 字典将为您可能从未真正编码过的所有子字符串刻录标记(例如,如果您可以编码 'stackoverflow',则会有 'st'、'sta'、'stac'、'stack'、' stacko'等)。

在这一点上,它真的不是 LZW - 让 LZW 聪明的是解压缩器如何构建与压缩器仅用于查看压缩数据流的相同字典。你不会使用的东西。但是所有 LZW 实现都有一个字典已满且不再更新的状态,这就是您将它与静态字典一起使用的方式。

【讨论】:

    【解决方案3】:

    我会查看您的数据,看看是否有明显的原因使其如此易于压缩。你也许可以做一些比 LZ78 简单得多的事情。我已经完成了 LZ77(回顾)和 LZ78(字典)。

    尝试对您的数据运行 LZ77。 LZ77 没有字典,所以你可以直接使用图书馆。 Deflate 是 LZ77 的实现。

    您使用通用字典的想法很好,但如果不做一些测试,很难知道这些文件是否彼此相似或只是自相似。

    【讨论】:

    • 我试过放气。小到只有几百字节的数据,就不太好了。
    • 根据维基百科,“只要整个字典可用,LZ78 解压就允许随机访问输入。”这听起来很适合我的应用程序,但我不知道有哪个库允许这样的操作。
    • 是的。你需要冻结一本字典。在 LZ77 中要做的等效的事情是在真实数据之前放置假数据,从而允许回溯到假数据。假数据可能只是几个代表性文件。
    【解决方案4】:

    LZW 在解压过程中添加到字典中,以确保它与压缩器具有相同的字典状态。否则解码将无法正常工作。

    但是,如果您当时处于修复字典的状态,是的,您不需要添加新代码。

    您的方法会运行得相当好,并且很容易使用现有工具来制作原型并衡量结果。即压缩示例文件,然后将示例和测试数据压缩在一起。后者的大小减去前者的大小将是块的预期压缩大小。

    LZW 是一种快速构建字典并提供不错结果的聪明方法。但是对典型数据块进行更彻底的分析可能会生成更高效的字典。

    LZW 表示压缩数据的方式还有改进的空间。例如,可以根据预期的使用频率对每个字典引用进行 Huffman 编码,使其更接近最佳长度。为了真正达到最优,代码应该进行算术编码。

    【讨论】:

    • 啊,您对#2 的回答非常有道理,事后看来似乎很明显。谢谢。
    【解决方案5】:
    1. 正确的做法是使用库——几乎所有现代语言都有一个压缩库。 C#、Python、Perl、Java、VB.net,无论你使用什么。

    2. LZW 通过依赖先前输入的字典来节省一些空间。它有一个初始字典,当您解压缩某些内容时,您会将它们添加到字典中——因此字典正在增长。 (这里省略了一些细节,但这是大体思路)

    您可以通过提供整个(完整)字典作为初始字典来省略此步骤。但这会花费一些空间。

    【讨论】:

    • 我可以使用压缩库。我不认为我会找到一个可以让我对字典进行如此低级访问的一个。你有例子吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-07-10
    • 2021-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多