一篇文章显得略长,本文对应原书6-10章。序言、前言、第1-5章,请参考Part 1,第11-15章,请参考Part 3。
自适应统计编码
位置对熵的重要性
统计编码有一个问题:在编码开始之前都需要遍历一次数据,以计算出各符号出现的概率。
随之而来有两个局限性:
- 因为数据集的不同部分有着不同的概率特征,随着要压缩的数据集的变大,统计编码的结果与熵的偏差也会越来越大;
- 流数据如音频流或视频流,由于整个数据集没有结尾的概念,无法遍历两次。
实际数据中总会存在某种类型的局部偏态(Locality Dependent Skewing),比如某些符号、想法或单词集中出现在数据集的某个子区间里,而在其他子区间则不会出现一次。
数据压缩领域内一个重要的理论,即局部性很重要(locality matters)。
问题变成,如何根据局部特性以最佳的方式分割数据流?
如果期望的熵与实际的符号平均二进制位数
之间出现显著差异,则统计编码算法会重置概率表,并使用重置后的概率表进行编码。这种具有适应数据流熵的局部特性能力的统计编码算法,通常被称为动态或自适应统计编码算法。
自适应VLC编码
统计压缩有3个步骤:
- 遍历数据流并计算各个符号的出现概率;
- 根据概率为符号生成VLC;
- 再次遍历数据流并输出对应的码字。
动态创建VLC表
自适应算术编码
要将算术编码变成自适应的很容易,这主要是因为其编码步骤与概率表之间的交互很简单。只要编码器与解码器在更新概率的正确顺序上达成一致,就能根据需要更新概率表。
自适应哈夫曼编码
哈夫曼树结构的处理比较复杂。最简单的想法,每遇到一个符号就去重新生成一棵完整的哈夫曼树;但这样做会极大地影响算法的性能。
自适应哈夫曼算法的做法是,在读取和处理符号时调整现有的树,包括如下操作:
- 更新概率;
- 对树的大量结点变换位置并重新排序,以使它们与概率的变化同步;
- 使树的结构满足哈夫曼树的要求。
感觉和平衡二叉树在插入、删除结点后为了保持树的平衡,而进行的左旋转、右旋转比较类似。
自适应哈夫曼算法贡献者:Faller、高德纳、Vitter。
选择
动态改进的优点:
- 有生成符号码字对应表的能力,无须将符号码字对应表显式地存储在数据流中。数据流变小后,计算性能就能有所提高;
- 有实时压缩数据的能力,无须再将整个数据集作为一个整体来处理。可有效地处理更大数据集,甚至都不用事先知道要处理的数据集有多大;
- 有适应信息局部性的能力,即邻近的符号会对码字的长度有影响,这可以显著提高压缩率。
字典转换
字典转换,Dictionary Transforms,所有的主流压缩算法(如GZIP、7-Zip)都会在核心转换步骤中使用字典转换。
基本字典转换
统计压缩主要关注数据流中单个符号的出现概率,且没有考虑周围可能出现的符号。忽略真实数据的基本属性:上下文及词语的组合,或者简单地说就是短语。
字典转换:给定源数据流,首先构建出单词字典(而不是符号字典),然后再将统计压缩应用到字典中的单词上。其目标不是替代统计编码,只是先应用到数据流上的一个转换,而后统计编码就能更有效地工作。
字典转换实际是一个数据流的预处理阶段,经过预处理后,生成的数据集更小,比源数据流压缩率更高。当能识别出那些经常重复使用的长字符串,并为它们分配最短的码字时,字典转换的效率最高。
所以问题变成,如何找出单词。
需要分词(tokenization)技术。
很难弄清楚怎样分析字符串才能创建出最佳大小的单词
,因此,不适用于任何类型的实时处理。
LZ算法
LZ,缩写取自两位研究员Abraham Lempel和Jacob Ziv名字,他们于1987年和1988年提出几种解决理想分词
问题的方法,因此称为LZ77和LZ78。
LZ77和LZ78算法产生一系列的衍生算法,包括GIF图像格式中使用的LZW(即Lempel-Ziv-Welch)算法,应用于7-Zip、xz等压缩工具的LZM(即Lempel-Ziv-Markov chain)算法。这些算法也同样应用于DEFLATE这样的压缩算法中,而DEFLATE又应用于PNG图像格式、PKZIP、GZIP等压缩工具及zlib库中。
LZ算法尝试在读取字符串过程中,向前寻找当前单词是否出现过(能否匹配)而进行分词。
LZ算法将数据流分成两部分:
- 数据流的左半部分通常被称为
搜索缓冲区
(search buffer),包含的是已经读过并处理过的符号; - 数据流的右半部分则被称为
先行缓冲区
(look ahead buffer),包含的是将要编码的符号。
搜索缓冲区通常只会包含32KB已经处理过的字符。在处理大规模数据或流数据时,引入滑动窗口
(sliding window)思想。滑动窗口的好处:
- 查找匹配时,不会出现性能问题;
- 考虑到局部性原理,即在给定的数据集中相关的数据很可能分布在相似的局部区域。
它可以和统计编码结合使用。可以将记号中的偏移量、长度值以及字面值分开后,再按照类型合并,组成单独的偏移量集、长度值集和字面值集,然后再对这些数据集进行统计压缩。
3个数据集:
- 偏移量集
- 长度值集
- 字面值集
变体
LZ77算法及其变体图谱:
LZ78算法及其变体图谱:
注:各个算法标注的数字表示其提出的年度。
LZ77,也叫LZ1算法,会将先行缓冲区中下一个符号的字面值作为第三个值输出。
LZSS与LZ77的主要差别:LZ77算法中,字典引用可能会比其替换的字符串还长;在LZSS中,如果被替换的字符串长度值小于收支平衡点,则这样的引用就会被忽略。此外,LZSS还会用一个标志位来区分后面的数据是字面值还是偏移量--长度值二元组这样的引用。很多流行的压缩工具比如PKZip、ARJ、RAR、ZOO和LHarc使用LZSS算法,并将其作为主要的压缩算法。值得一提的是,Game BoyAdvance游戏机的BIOS就自带解码改进后的LZSS格式补丁等功能。
LZ78或LZ2,不用距搜索缓冲区结尾的偏移量来指示匹配的位置,而是根据输入流创建字典然后再引用。
LZW:Lempel-Ziv-Welch,由Terry Welch于1984年提出,采用LZ78算法的思想,其工作原理:
- LZW算法将字典初始化为包含所有可能的输入字符,如果用到了清空和停止符号(clear and stop codes),那么这两个符号也包括在其中;
- 该算法扫描输入字符串以寻找更长的连续子串,直到它发现该子串在字典中不存在;
- 当发现这样的子串时,去掉它的最后一个符号(这样它就变成当前字典中最长的子串),然后从字典中找出其索引并输出;
- 将该子串(此时包括最后一个符号)加入字典作为新的词条;
- 将该子串的最后一个符号作为起点,重新扫描下一个子串。
用这种方法,连续更长的子串就会作为新的词条加入字典,同时也让后续字符串编码为单值输出成为可能。该算法最适用于那些连续出现重复的数据,因为在数据的初始部分,基本看不到什么压缩,但是随着数据的增多,压缩率逐渐趋于最大值。
LZW算法成为首个在计算机中广泛采用的通用数据压缩方法。公共领域程序"compress"也采用了LZW算法,并在1986年前后就基本成为UNIX系统的标准应用程序。
后来,compress从很多UNIX分发中消失:
- 侵犯LZW的专利权;
- GZIP的压缩率更高,使用的是基于LZ77的DEFLATE算法。
尽可能了解数据
潜在的输入数据集的量是很大的,而每个数据集都可能以一种特殊的方式去响应一种算法。对数据集越了解,你就越能从中选择出最适合的LZ变换。
上下文数据转换
数据变换的方法有很多种,但其中有3种对现代的数据压缩来说最为重要:
- RLE:Run Length Encoding,行程编码
- DC:Delta Coding
- BWT:Burrows-Wheeler transform,伯罗斯--惠勒变换
RLE
主要针对的是连续出现的相同符号聚类的现象,它会用包含符号值及其重复出现次数的元组,来替换某个符号一段连续的行程(run)
RLE算法最适用于大多数符号都连续重复出现的数据集。
需要将数据集分成两部分:字面值流和行程长度流。
通常认为RLE是单字符上下文模型,也就是说,对任何给定的符号,在编码时我们都只考虑它的前一个符号,如果这两个符号是相同的,那么行程继续;如果不相同,那么当前行程终止。
TurboRLE,RLE的变体。
增量编码
增量编码的思想:给定一组数据,相关的或相似的数据往往会集中在一起。如果这样,计算两个相邻值之间的差,就可以用其中一个值以及该差值来表示另外一个值。
增量编码的目的就是缩小数据集的变化范围。更确切地说,是为了减少表示数据集中的每个值所需要的二进制位数。
原始编码:[1,3,10,8,6]
增量编码:[1,3-1,10-3,8-10,6-8]→[1,2,7,-2,-2]
最适用于处理时间序列数据、音频和图像数据。
XOR增量编码
减法增量编码算法的问题是,结果中可能会出现负数,进而产生各种问题:
- 在存储时需要额外的二进制位;
- 可能会增大数据的变化范围。
可通过使用按位异或运算(bitwise exclusive OR,XOR)代替减法运算来解决这一问题。XOR会独立地对每个二进制位进行操作。XOR是一种逻辑运算,仅当两个输入不相同时结果为真。
还是以[1,3,10,8,6]
为例,计算步骤示意:
参照系增量编码
修正的参照系增量编码
PFOR:Patched Frame of Reference Delta Coding,工作原理如下步骤所示:
- 选择一个位宽度;
- 遍历数据并用位对每个值编码;
- 当遇到的值所需的编码位数大于时,将这样的离群值存储在单独的位置。
两个问题:
- 如何选择 b b b值:使大多数值在编码时需要的位数不超过 b b b,并且可以通过它识别出那些离群值。
- 怎样处理离群值
压缩增量编码后的数据
文本有效性
可以工作,但是由于英语文本中使用最多的是还是字母表中两头的字母,因此得到的数据中会出现很多正负数交替的情况。对于英语文本,LZ这样的算法可能会做得更好。
MTF
前移编码(move-to-front coding,MTF)。
消除捣乱符号的影响
MTF的问题:有一些捣乱的符号会打乱前面存在的符号流。一种解决方法:不是一读到某个符号就将它移到最前面,而是采取一些探索式方法慢慢地将它移到最前面。
压缩MTF
BWT
所有其他的压缩算法通常可以归为两类:统计压缩(即VLC)和字典压缩如LZ78
数据建模
多上下文编码算法:考虑最后观察到的几个符号以确定当前符号的理想编码位数。这种基于统计观察的相邻关系,通常也被称作预测编码器。也可以认为是统计压缩器的加强(on-steroids)版。它将自适应模型和多种符号码字对应表结合起来,根据前面观察到的符号,为当前符号生成尽可能短的码字。
马尔可夫链
马尔可夫链,Markov Chain,定义:马尔可夫链是一种离散的随机过程,其未来的状态只取决于现在,而与过去的历史无关。
二阶上下文,second-order context;一阶数据,first order data,context-1 data;二阶数据,second order data,context-2 data。例子如下:
类似地,有三阶上下文和三阶数据。
每个上下文都在某种程度上描述状态之间的转移,也可将它可视化为一棵树,每个节点代表一个活动,每次转移则有相应的概率。
一棵三阶马尔可夫链树形图
事件选择概率:Probabilistic Event Selection,
PageRank算法就是以马尔可夫链为基础,状态是互联网上总数大约400亿的网页,转移则是网页之间的链接。这个算法就是为了计算如果用户随机浏览,他看到每个网页的概率是多少。
马尔可夫链与压缩
部分匹配预测算法
PPM:Prediction by Partial Matching,部分匹配预测算法,John Cleary与和Ian Witten于1984年提出的马尔可夫链算法的一种具体实现。在内存消耗与计算性能方面表现都还不错。与马尔可夫链类似,PPM算法同样通过前 N N N个符号的上下文来决定第 N + 1 N+1 N+1个符号最有效的编码方式。
简单马尔可夫链会采用比较直接的实现方式,即读取当前符号并判断它是否是现有链条的延续,PPM算法的实现恰好相反。给定输入流中的当前符号,PPM算法会向前扫描 N N N个符号,并根据前 N N N个符号的上下文来决定当前符号的出现概率。如果在 N N N个符号的上下文中,当前符号的出现概率为0,PPM算法就会将上下文符号的个数减少为 N − 1 N-1 N−1。为如果没有在任何上下文中发现匹配,就会做出固定的预测。
如果一个符号之前没有出现过,那么PPM算法会在输出最后一个字面值符号之前将 N N N个转义码输出。这一算法很多变体(包括PPMA、PPMB、PPMC、PPMP和PPMX)之间的主要区别,就在于它们在处理转义码过程中的细微差别。
单词查找树
要实现PPM算法,我们遇到的首要问题是如何创建一种数据结构,可以将从输入流中读取的每个字符的所有上下文(阶)存储起来,并且在需要时能快速定位到。在简单的情况下,可以通过一种被称为单词查找树(trie)的特殊树结构来实现这样的需求,这种树的每个分支都表示一种上下文。
字符的压缩
选择一个合理的 N N N值
PPM算法会选择一个值,然后再根据这样的上下文长度去寻找匹配。如果没有找到匹配,就会选择更短的上下文继续寻找。这样看来,似乎上下文越长(也就是的取值越大),预测的效果越好。然而,大多数PPM算法的实现在综合考虑所需内存、处理速度以及压缩率后,将的值设定为5或者6。
一些PPM算法变体,如PPM*,尝试设置很大的 N N N值。缺点是:需要一种新的查找树结构,需要的计算资源也远比PPM多;优点是其结果比PPM算法好,能多节省约6%的存储空间。
处理未知的符号
零频问题:Zero Frequency Problem,。
几种解决方法:
- 使用拉普拉斯估计(Laplace estimator),赋给所有"从没见过"的符号相同的伪计数值1;
- PPMD:从没见过的符号每使用一次,伪计数值就加1。即,PPMD算法是这样估算新出现符号的概率的,即新符号的概率等于不同符号的个数与观察到的所有符号的出现次数之比。
- PPMZ:刚开始时它的处理方式与PPM* 相同,都试图在阶上下文下找出当前符号的匹配。当找不到这样的匹配时,它就会换上完全不同的算法局部阶估计法(Local-Order-Estimator),而使用的还是基本的PPM模型,只不过预测的算法完全不同。
上下文混合算法
上下文混合算法,Context Mixing,为了找出给定符号的最佳编码,会使用两个或更多的统计模型。
上下文混合算法的两个问题:
- 应该使用什么样的模型?
- 应该怎样将这些模型结合起来?
模型类型
LZ、RLE、增量编码、BWT等算法基于的假设:数据的相邻性与它的最佳编码方式有关。
相邻性和局部性都只是上下文的最简单形式,而绝不是唯一的形式。
作为上下文混合算法的先驱压缩器之一,PAQ包含以下模型:
- n n n元语法: n − g r a m s n-grams n−grams,上下文是指在被预测符号之前的个字符,与 PPM算法相同;
- 整词 n n n元语法:whole-word n − g r a m s n-grams n−grams,忽略大小写和非字母字符(对文本文件很有用);
- 稀疏上下文:如,被预测符号之前的第二个和第四个字节(对某些二进制文件很有用);
- 模拟上下文:由前面的8位或16位字节的高字节位组成,对多媒体文件很有用;
- 二维上下文:对图像、表和电子表格很有用,行的长度由找出的重复字节模式的步长决定;
- 只针对特定文件类型的特殊模型:如x86可执行文件,BMP、TIFF或JPEG格式的图片。
PAQ在大文本压缩基准测试中经常排名靠前,最新版本之一ZPAQ在压缩人类DNA信息的比赛中获得第二名。
混合类型
将不同模型的输出结合起来有以下两种方法:
- 线性混合:Linear Mixing,将各个模型的预测值加权平均的过程,最终的值则取决于证据权重。没有反馈回路来说明在预测如何压缩数据时我们赋给一个模型的权重是否正确。因此,当输入数据流变化时,模型的权重保持不变,最终得到的结果不但没有压缩,反而比原来需要的空间还大。
- 逻辑混合:使用神经网络来更新权重,更新的依据则是哪个模型在过去给出最准确的预测。可解决线性混合的问题,但也有缺点:在进行数据压缩时,它需要消耗大量的内存,同时运行的时间也较长。
下一代技术
由于需要大量的内存和运行时间,这就使得上下文混合算法很难适用于移动设备。只有当需要处理的数据量很大、数据很复杂并且一直在变化时,上下文混合算法才能真正发挥作用。
换个话题
压缩可以分为两类:
- 多媒体数据压缩:Media Specific Compression
- 通用压缩:General Purpose Compression
多媒体数据压缩
多媒体数据压缩工具是专门设计用来压缩图像、音频、视频等多媒体数据的。
有损压缩指的是为了使数据压缩得更小,可以牺牲多媒体的质量这样的数据转换。有损数据转换的种类特别多,每一种都针对特定的多媒体文件(针对图像文件的就不太适用于音频文件)和内容类型(灰度图像与全彩图像使用的压缩算法同样不同)。
通用压缩
通用压缩工具是设计用来压缩除多媒体数据以外的其他数据。像DEFLATE、GZIP、BZIP2、LZMA和PAQ等算法,都是将各种无损转换结合起来,用来压缩诸如文本、源代码、序列化数据、二进制内容等其他不能使用有损压缩工具压缩的非多媒体文件。
大文本压缩基准测试。
谷歌对GZIP算法的改进已产生一系列压缩工具,如Snappy、Zopfli、Gipfeli、Brotli,这些工具的努力方向:更好的压缩率、更小的内存需求、更快的解压速度,每个侧重点不同。
标准的HTTP协议栈允许数据包使用GZIP和BZIP、Brotli(需要服务器端和客户端都支持)。
回报率递减困境:通过观察基准测试,经过大量努力后,只能在现有算法的基础上提高2%~10%。
实践中的数据压缩
实践出真知。