数据压缩入门级原理与应用^^

229 阅读20分钟

1 最原始的数据压缩——摩斯密码

1.1 最初的电报

电报设备本身的操作方法很简单:按下电报按钮就能进行连接并通过电线传输电流、松开按钮,电流传输就中断,分为长流和短流。其实这与二进制极其相似,即使早在19世纪二进制编码还没有发明时,这套系统已经在应用同样的思想来传递信息了。所以电报想传输内容,就需要为每一个字母附上对应的编码,26个英文字母则需要5位的二进制来表示,例如A为00001,Z为11010,0为短按,1为长按。那么对于“编码器”,也就是听电报的人,他是很容易处理的,只需要把听到的所有电报内容,用5位分割开,就能得到所有的字符。

1.2 出现的小问题

这种简单易用的方式,往往也会有一些痛点。在英文中,每个字符的使用频率是不同的,E一般是最常用的,在文章中大概能占到百分之五,而X则几乎用不到,这就导致使用最频繁的和使用最不频繁的占用的编码长度一样。如果可以将使用频率最多的字符用短编码,使用频率少的用长编码,能很清晰的预知到,会减少内容整体的编码长度,降低打字机的损耗。

kim image

1.3 摩斯密码底层的原理

最终基于此原理,美国人摩尔斯在1837年发明的了摩斯密码,根据字母使用频率分布不同长度的编码。摩尔斯码为英语字母表中的每一个字符都分配了或长或短的脉冲,一个字母用得越频繁,其编码也就越短、越简单。因此,英语中最常用的字母“E”的编码最短,用一个点表示;而字母“X”的编码毫无疑问则很长;解决了打字机、人力损耗的问题,即降低了按按钮的次数,这就是最早期的采取符号分配变长编码(VLC)方式做的数据压缩。

VLC全称为variable-length code,可变长度编码。

在早期对信息论的研究中,克劳德•香农(他是摩尔斯码方面的专家)正是利用了这一概念,由此创造了一个新的技术领域“数据压缩”的第一代技术,这些都是在VLC的启发下产生的。

2 信息熵

2.1 为什么频率越大,越没用

信息论的一个核心观点就是:字符概率越大,意味着他的信息越没有含义。其实这乍一听是有点不科学的,我们平常都会认为:出现的越频繁越重要。但对于信息论和数据压缩来说是完全相反的。

我们这里举一个例子,例如钓鱼,鱼咬钩是小概率事件,鱼没有咬钩是大概率事件,我们把咬钩比做1,没咬钩比做0,用二进制表示就是:

0000000000000000000000000000000000100000000000000000

我们可以发现,在很长的时间里没有什么有用的信息,真正有用的信息偶尔才会出现,也就说明符号出现的越频繁,这个符号对整个数据集的信息贡献就会越小。

2.2 信息熵

信息论的奠定者——香农博士,通过数学概念给出了一个数学公式,来计算一个字符集每个符号的平均所需最小二进制位数:称作熵

公式具体实现感兴趣的可以去了解下,我之前推过一遍有点忘记了。。大概的原理就是:熵和每个字符出现的概率有关。

2.3 信息熵的小例子

有图可知,频率越大,熵越大,即每个符号所需的最小二进制位数越小。而频率相差不大的,熵为最大。

假设一个数据集,【A,B,C,D】,共一千个。每个字符各占了250个,对应表格最后一行,那理论上我们只能分配每个字符为两位,A为00,B为01,C为10,D为11。

再假设一个数据集,【a,b,b,c,c,c,d,d,d,d,d,d,d...d(994个)】,对应表格第一行。我们发现,好像也只能分配每个字符为两位,A为00,B为01,C为10,D为11。相比于【A,B,C,D】,分配的位数一致。

为什么频率大的情况下,长度没有发生变化呢。因为在数学的角度中,第一种情况熵为0.06,即每个字符分配0.06位,但是实际计算机中,我们无法取到小数的二进制位数,一定是一位以上,所以在实际中和数学中是有差异的。

2.4 熵的意义

熵是数学意义上的最小位数,也就是数据压缩理论上的极限。上面讲的例子,由于每个字符都是等长,导致熵大的字符集和熵小的字符串长度一致。可我们如果运用了摩斯密码类似的方式,即VLC,就可以做到最简单的数据压缩。

例如第一个例子,由于频率完全一致,所以只能采用定长编码,最后总位数为1000*2=2000位。

第二个例子,我们将频率最高的D置为0,第二高的C置为1,B、A置为10,11。最后总位数为9941+31+22+12=1005位。

压缩比大概为百分之五十,这就是最简单的VLC压缩算法。

实际上这种VLC是不存在的,因为编码器无法解析这种编码,具体原因后面再讲。

虽然信息论说熵是理论上的极限,但其实熵是可以被突破的,这是因为香农在定义信息熵的时候只关注了概率问题,而没有关注顺序,大部分情况下我们的数据顺序是至关重要的。顺序是内容的基本信息,两条数据即使符号种类和概率完全一样,但是由于顺序不一样,代表的含义就不同。例如plepa和apple,他们的字符完全一样,但是由于顺序不同,第一个就乱码,第二个含义就是苹果。当然一般我们的字符串没有这么简单,能够接近熵就是我们的最大希望了,举这个例子只是想证明熵是可以被突破的。

3 VLC(variable-length code)

VLC全称为variable-length code,可变长度编码。

在早期对信息论的研究中,克劳德•香农(他是摩尔斯码方面的专家)正是利用了这一概念,由此创造了一个新的技术领域“数据压缩”的第一代技术,这些都是在VLC的启发下产生的。

3.1 VLC编码原则

2.4讲的VLC编码例子,实际是不存在的,因为解码器无法通过这种编码工作起来。假设列表为[A,B,C,D],编码成[0 1 00 10]。在解码时,由于各个字符的二进制位数都不同,解码器是不知道每个字符对应的二进制位数是多少,所以在扫描[0 1 00 10]的1时,编码器会产生歧义,他不知道这个字符是B,还是没有扫描完,其实是10为D。

这种分歧就导致,我们的编码必须有一套原则而不能够为了降低长度随便编码。这个原则就是前缀原则,即任何一个符号的编码都不可以是其他编码的前缀,保证编码器能够不产生歧义。

还以ABCD为例,这次我们用表格的编码,可以发现满足了前缀原则,编码器可以分辨出到哪个二进制位时结束该字符的扫描。

3.2 VLC的局限

只要满足前缀原则,编码是可以随意变化的。这就导致每个VLC算法的压缩率不同,所以想生成极高效的VLC算法很困难,在数据压缩的早期,只能通过VLC算法进行数据压缩,需要消耗掉大量数学家宝贵的精力和时间,在过去的几十年,数学家发明了几百种VLC编码。

并且编码和概率强关联,导致同一种VLC算法在不同字符集下的压缩率是不同的,极端情况下,反而会出现压缩后比压缩前大的情况,所以VLC编码都对各个符号的概率做了假定。当我们拿到数据集的时候需要算出概率并去匹配VLC编码。

4 统计编码

上面讲的VLC的局限,在我们这种使用者理解中还是可以接受的,我们只需要算出所有字符的概率去找最匹配的VLC编码就可以了,数学家消耗多少时间和我们没有什么关系。但是最大的问题就是,有可能该字符集算出来的概率和现有的VLC算法匹配不上,导致没有VLC编码可用,这确实会影响到我们的使用,看起来我们不得不自定义一个压缩编码了。

所以基于此,出现了统计编码。相比于VLC,不会将字符转换为特定的编码,而是通过各个字符的概率分布,自定义一套码字集,类似于自定义的VLC。准确来说,统计编码的含义为:统计编码算法通过数据集中符号出现的概率来进行编码使结果尽可能与熵接近。

4.1 哈夫曼编码

哈夫曼编码可能是生成自定义VLC最直接、最有名的方法。给定一组符号及其出现频率,该方法能生成一组符号平均长度最短的VLC。它的工作原理是将数据排序后建立决策树(decision tree),然后从树干一直往下直到树叶为止。

举一个例子:

kim image

我们已知数据集和字符对应的频率,接下来要通过这个表格构造哈夫曼树。

kim image

先将符号及其出现的频次写在便签纸上,再根据频次对符号进行排序,然后自下而上建一棵二叉哈夫曼树,并称它们为哈夫曼树的叶子。

kim image

接下来,选择出现频次最小的两个符号,并将它们往下移一层,然后拿出新的便签将两者合并作为它们的父节点,上面写上两个符号的组合及频次相加之和。接下来重复该过程获得最终的哈夫曼树,并给所有左子树赋值0,为所有右子树赋值1。

kim image

我们最终得到了一套基于哈夫曼树的自定义编码,A:0,B:10,C:11。由于哈夫曼树性质,他天生具有前缀性质,所以可以作为VLC编码。也许它相比于数学家推算几十年出来的VLC算法效率低,但是它可以保证所有概率情况下的列表都能够进行数据压缩,且能够接近于熵。

在过去的50多年里,人们已经对哈夫曼编码进行了大量的分析,不但产生了各种能在特定性能或内存阈值下工作的变体,也有针对特定概率分布的各种变体。关于哈夫曼算法及其复杂性和优化方法,已经出版了不少图书。也许传统VLC方法能够接近于极限压缩,但是由于它概率写死的极端情况,已经淡出了数据压缩的主流。

4.2 哈夫曼编码的局限

我们也许能够给一个数据集简单高效的自定义编码,但由于计算机性质,每个字符的二进制位数一定是整数,这就导致和理想熵差距较大。例如上文2.3举的例子,即使我们用了哈夫曼自定义编码,也无法达到最理想的0.06熵的压缩比。

而且我们的哈夫曼编码集需要传递给解码器,如果编码集很大,在传输和解码效率上可能也会有性能瓶颈。

哈夫曼编码最接近熵的情况,就是每个字符的概率都为2的负整数次幂,例如二分之一,八分之一,这样能够通过二叉哈夫曼树得到最高效的哈夫曼编码。

4.3 算术编码

算术编码的解决了哈夫曼编码的弊端,包括需要给每一个字符一个整数编码的情况,还需要传递编码集等。简单来说,就是将整个数据集生成一个长度很长的数字。与字符串相比,表示这个数需要的二进制位数要少一些。例如,字符串“TOBEORNOT”可以用数236712来表示(我随便编的一个数),而ceil(lb(236 712)) = 18,即只需要18个二进制位就能表示。相比而言,如果用ASCII码来表示“TOBEORNOT”,则需要56个二进制位。

然而实际上不会随意编一个数当成编码。。算术编码会根据输入流,通过一系列复杂的步骤计算出那个数。选择这个数的诀窍,实际上是对二分查找算法进行了改进,

4.4 如何选出字符串对应的算术编码

kim image

假设一个字符串GGB,按照出现的频率划分区域,并且采取递归的方式。最后得出GGB在[0.825,0.85)区间,我们在其中随便选一个数作为他的编码就可以了。例如我选0.83,当解码的时候直接找0.83在哪个最后递归出来的位置,就能解码出字符串。

解码器一定知道他是个小数,所以可以直接传83给解码器,最后的位数为7,即每个字符平均使用2.33。

4.5 实际应用

21世纪初算术编码的专利到期之后,算术编码得到了大量的使用,并变得流行起来,人们对不少实现方法进行了修改以适应特定的编码解码器,如JPG和H.264编码解码器所使用的二进制版本,当然JPG和H264已经算是有损压缩了,刚才我们聊的其实全部是无损压缩,而对于多媒体的有损压缩,极其复杂,每个编码都有不同的规则,这里不多叙述。

哈夫曼编码和算术编码一直有着不同支持者的争议。然而因为计算机变得越来越快(并且算术压缩的专利已经到期),所以算术压缩已成为目前的主流算法。它不仅应用在大多数的多媒体编码器中,甚至有了有效的硬件实现,最后还是算术编码战胜了哈夫曼编码。

4.6 自适应编码

上面介绍的VLC、哈夫曼编码、算术编码都需要算出数据集中全部字符的概率,即需要遍历整个数据集。这就会引出几个问题:

  1. 在大数据中,数据量极大,将全部数据遍历一遍获取概率的代价是很大的。
  2. 不支持流运算,因为流运算无法获得全部字符的概率。
  3. 这些算法忽略了局部性,即在整个数据集的不同区间内,概率是不同的,那么这些区间的最佳压缩(熵)也是不同的。如果不考虑局部性,而是采取一个统一概率的算法,有可能做不到更好的压缩率。

所以出现了自适应编码,简单来说就是针对于流运算,每次出现的字符都需要去修改哈夫曼树或者算术编码的概率区间,达到最佳的压缩率。

4.7 自适应编码的优势

  1. 有生成符号码字对应表的能力,无须将符号码字对应表显式地存储在数据流中。数据流变小后,计算性能就能有所提高,但更重要的是下面两个优点。
  2. 有实时压缩数据的能力,无须再将整个数据集作为一个整体来处理。这让我们可以有效地处理更大的数据集,甚至都不用事先知道要处理的数据集有多大。
  3. 有适应信息局部性的能力,即邻近的符号会对码字的长度有影响,这可以显著提高压缩率。

5 字典编码

上面提到的统计编码,本质都是在关注每个字符的出现频率,处理大量非结构化混乱的字符集是比较出色的。但是实际上我们真实的数据往往都是通过各种词组组合起来的,而统计编码忽略了这一重要特征,于是出现了字典编码。而对于字典编码的重要性和应用场景,事实上今天所有的主流压缩算法(比如GZIP或者7-Zip)都会在核心转换步骤中使用字典转换,包括clickhouse的LZ4压缩算法、hive的deflate压缩算法、mysql的LZ77压缩算法,GZIP使用的是基于LZ77的DEFLATE算法,都是基于字典编码。

5.1 字典转换

字典编码和统计编码的核心区别,就是字典编码将词组映射成编码,而统计编码是将字符映射成编码。但是这并不代表着字典编码可以替代统计编码,现在高效的压缩算法往往会将数据通过切分单词,然后字典编码进行预处理,构建出单词字典,再进行统计编码。

5.2 LZ算法

1977年,两位研究人员提出了几种解决“理想分词”问题的方法。这些算法根据提出的年份分别被命名为LZ77 和LZ78,它们在找出最佳分词方面非常高效,30多年来还没有其他算法可以取代它们。

5.2.1 编码

5.2.2 解码

5.2.3 再编码

LZ算法真正吸引人的地方还在于它可以和统计编码结合使用。可以将记号中的偏移量、长度值以及字面值分开后,再按照类型合并,组成单独的偏移量集、长度值集和字面值集,然后再对这些数据集进行统计压缩。例如,可以将前面例子输出的记号集[0,0,T][0,0,O][0,0,B][0,0,E][3,1][0,0,R][0,0,N][3,1][8,1][9,4] 分成以下3个数据集。

偏移量集 0,0,0,0,3,0,0,3,8,9

长度值集 0,0,0,0,1,0,0,1,1,4

字面值集 T,O,B,E,R,N

这三个数据集的性质不同,相应的处理方法也不同。然后进行统计编码。

5.3 优点

通过以上推算,我们发现当词组重复率高的情况下,产生的字面值集就越少,词组转换为的数组就越多,将一个很长的词组转换为2个数字,足以证明他的高压缩率。

5.4 局部性的充分利用

上文讲过,由于数据局部性质,数据集的每个区间的频率是不同的,对于字典编码来说还有一个局部性质,就是往往相似的词组都会距离很近。基于此原理,也包括如果搜索缓冲区太大内存吃不消等等原因,产生了滑动窗口LZ算法,其实就是将搜索缓冲区和先行缓冲区生个一个滑动窗口,避免搜索缓冲区太大。

6 上下文转换

这块算法也很重要,但是它与上面的相比来说更加简单,所以我们可以长话短说的来介绍下。

6.1 RLE

假设一个数据集(和2.3一致),【a,b,b,c,c,c,d,d,d,d,d,d,d...d(994个)】,如果用RLE编码怎么表示呢?

a(1)b(2)c(3)d(994)

最朴实的编码就是这样,将一个1000个字符转换为18个字符。

6.2 增量编码

数值型数据算是最令人讨厌的数据类型之一,这是因为大多数时候,他们没有什么规律,我们找不到可以利用的统计信息。这时候可以采用增量编码应对数值的集合。

【1,3,5,7,10】=》【1,2,2,2,3】

每一位都是差值,最后结果减少了一个字符。

6.3 缺点

虽然编码很简单,但是缺点也很明显,这些算法对于数据集的要求太高了。

RLE明显针对于大量重复的字符,在重复字符连续且很多的情况下,有着极高效的压缩率。但是如果是uuid这种几乎没有任何重复的情况下,每个字符都要加一个(1),反而增加了长度。

增量编码也是同理,如果出现【1,3,1】这种,最后得到的是【1,2,-2】,反而增加了一个负数,想存储负数,需要每个二进制数增加一位表示正负。

7 简单讲下有损压缩

刚才讲的都是无损压缩,对于我们平常使用的数据,是不允许有错误的,所以必须使用无损压缩。而对于多媒体图片、视频这种占用空间很大的文件,我们为了网络性能,往往会采取有损压缩。例如《霍比特人》一书只有95022个单词,如果假定平均每个单词由5个字母组成,那么这本书大约有475 110个字母。也就是说,一张1024×1024的图片所占用的空间,可以用来存放约6本《霍比特人》这样篇幅的书。

这也是为什么大多数多媒体数据压缩工具使用有损压缩算法。有损压缩指的是为了使数据压缩得更小,可以牺牲多媒体的质量这样的数据转换。例如,一张1024×1024的图片,如果使用8个二进制位来表示红绿蓝这3种颜色通道,那么每个像素就需要24个二进制位,因此整个图片的大小为3 MB。然而,如果每个颜色通道只用4个二进制位表示,那么每个像素就只需要12个二进制位,整个图片占用的空间也就只有1.5 MB,但同时也降低了图片的色彩质量

每个像素有8个二进制位压缩为4个二进制位,就导致每个像素点的颜色种类从1600万种降低为4096种,我们人眼可以分辨一百万以上的颜色种类,所以这种压缩会影响我们的观感,但节省的资源也是巨大的。

8 参考

《数据压缩入门》