`
emily2ly
  • 浏览: 166646 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
社区版块
存档分类
最新评论

信息熵中的算术编码

阅读更多

算术编码 (转)
我们在上一章中已经明白,Huffman 编码使用整数个二进制位对符号进行编码,这种方法在许多情况下无法得到最优的压缩效果。假设某个字符的出现概率为 80%,该字符事实上只需要 -log2(0.8) = 0.322 位编码,但 Huffman 编码一定会为其分配一位 0 或一位 1 的编码。可以想象,整个信息的 80% 在压缩后都几乎相当于理想长度的 3 倍左右,压缩效果可想而知。

难道真的能只输出 0.322 个 0 或 0.322 个 1 吗?是用剪刀把计算机存储器中的二进制位剪开吗?计算机真有这样的特异功能吗?慢着慢着,我们不要被表面现象所迷惑,其实,在这一问题上,我们只要换一换脑筋,从另一个角度……哎呀,还是想不通,怎么能是半个呢?好了,不用费心了,数学家们也不过是在十几年前才想到了算术编码这种神奇的方法,还是让我们虚心地研究一下他们究竟是从哪个角度找到突破口的吧。

输出:一个小数

更神奇的事情发生了,算术编码对整条信息(无论信息有多么长),其输出仅仅是一个数,而且是一个介于 0 和 1 之间的二进制小数。例如算术编码对某条信息的输出为 1010001111,那么它表示小数 0.1010001111,也即十进制数 0.64。

咦?怎么一会儿是表示半个二进制位,一会儿又是输出一个小数,算术编码怎么这么古怪呀?不要着急,我们借助下面一个简单的例子来阐释算术编码的基本原理。为了表示上的清晰,我们暂时使用十进制表示算法中出现的小数,这丝毫不会影响算法的可行性。

考虑某条信息中可能出现的字符仅有 a b c 三种,我们要压缩保存的信息为 bccb。

在没有开始压缩进程之前,假设我们对 a b c 三者在信息中的出现概率一无所知(我们采用的是自适应模型),没办法,我们暂时认为三者的出现概率相等,也就是都为 1/3,我们将 0 - 1 区间按照概率的比例分配给三个字符,即 a 从 0.0000 到 0.3333,b 从 0.3333 到 0.6667,c 从 0.6667 到 1.0000。用图形表示就是:

+-- 1.0000 | Pc = 1/3 | | +-- 0.6667 | Pb = 1/3 | | +-- 0.3333 | Pa = 1/3 | | +-- 0.0000

现在我们拿到第一个字符 b,让我们把目光投向 b 对应的区间 0.3333 - 0.6667。这时由于多了字符 b,三个字符的概率分布变成:Pa = 1/4,Pb = 2/4,Pc = 1/4。好,让我们按照新的概率分布比例划分 0.3333 - 0.6667 这一区间,划分的结果可以用图形表示为:

+-- 0.6667 Pc = 1/4 | +-- 0.5834 | | Pb = 2/4 | | | +-- 0.4167 Pa = 1/4 | +-- 0.3333

接着我们拿到字符 c,我们现在要关注上一步中得到的 c 的区间 0.5834 - 0.6667。新添了 c 以后,三个字符的概率分布变成 Pa = 1/5,Pb = 2/5,Pc = 2/5。我们用这个概率分布划分区间 0.5834 - 0.6667:

+-- 0.6667 | Pc = 2/5 | | +-- 0.6334 | Pb = 2/5 | | +-- 0.6001 Pa = 1/5 | +-- 0.5834

现在输入下一个字符 c,三个字符的概率分布为:Pa = 1/6,Pb = 2/6,Pc = 3/6。我们来划分 c 的区间 0.6334 - 0.6667:

+-- 0.6667 | | Pc = 3/6 | | | +-- 0.6501 | Pb = 2/6 | | +-- 0.6390 Pa = 1/6 | +-- 0.6334

输入最后一个字符 b,因为是最后一个字符,不用再做进一步的划分了,上一步中得到的 b 的区间为 0.6390 - 0.6501,好,让我们在这个区间内随便选择一个容易变成二进制的数,例如 0.64,将它变成二进制 0.1010001111,去掉前面没有太多意义的 0 和小数点,我们可以输出 1010001111,这就是信息被压缩后的结果,我们完成了一次最简单的算术压缩过程。

怎么样,不算很难吧?可如何解压缩呢?那就更简单了。解压缩之前我们仍然假定三个字符的概率相等,并得出上面的第一幅分布图。解压缩时我们面对的是二进制流 1010001111,我们先在前面加上 0 和小数点把它变成小数 0.1010001111,也就是十进制 0.64。这时我们发现 0.64 在分布图中落入字符 b 的区间内,我们立即输出字符 b,并得出三个字符新的概率分布。类似压缩时采用的方法,我们按照新的概率分布划分字符 b 的区间。在新的划分中,我们发现 0.64 落入了字符 c 的区间,我们可以输出字符 c。同理,我们可以继续输出所有的字符,完成全部解压缩过程(注意,为了叙述方便,我们暂时回避了如何判断解压缩结束的问题,实际应用中,这个问题并不难解决)。

现在把教程抛开,仔细回想一下,直到你理解了算术压缩的基本原理,并产生了许多新的问题为止。

真的能接近极限吗?

现在你一定明白了一些东西,也一定有了不少新问题,没有关系,让我们一个一个解决。

首先,我们曾反复强调,算术压缩可以表示小数个二进制位,并由此可以接近无损压缩的熵极限,怎么从上面的描述中没有看出来呢?

算术编码实际上采用了化零为整的思想来表示小数个二进制位,我们确实无法精确表示单个小数位字符,但我们可以将许多字符集中起来表示,仅仅允许在最后一位有些许的误差。

结合上面的简单例子考虑,我们每输入一个符号,都对概率的分布表做一下调整,并将要输出的小数限定在某个越来越小的区间范围内。对输出区间的限定是问题的关键所在,例如,我们输入第一个字符 b 时,输出区间被限定在 0.3333 - 0.6667 之间,我们无法决定输出值得第一位是 3、4、5 还是 6,也就是说,b 的编码长度小于一个十进制位(注意我们用十进制讲解,和二进制不完全相同),那么我们暂时不决定输出信息的任何位,继续输入下面的字符。直到输入了第三个字符 c 以后,我们的输出区间被限定在 0.6334 - 0.6667 之间,我们终于知道输出小数的第一位(十进制)是 6,但仍然无法知道第二位是多少,也即前三个字符的编码长度在 1 和 2 之间。等到我们输入了所有字符之后,我们的输出区间为 0.6390 - 0.6501,我们始终没有得到关于第二位的确切信息,现在我们明白,输出所有的 4 个字符,我们只需要 1 点几个十进制位,我们唯一的选择是输出 2 个十进制位 0.64。这样,我们在误差不超过 1 个十进制位的情况下相当精确地输出了所有信息,很好地接近了熵值(需要注明的是,为了更好地和下面的课程接轨,上面的例子采用的是 0 阶自适应模型,其结果和真正的熵值还有一定的差距)。

小数有多长?

你一定已经想到,如果信息内容特别丰富,我们要输出的小数将会很长很长,我们该如何在内存中表示如此长的小数呢?

其实,没有任何必要在内存中存储要输出的整个小数。我们从上面的例子可以知道,在编码的进行中,我们会不断地得到有关要输出小数的各种信息。具体地讲,当我们将区间限定在 0.6390 - 0.6501 之间时,我们已经知道要输出的小数第一位(十进制)一定是 6,那么我们完全可以将 6 从内存中拿掉,接着在区间 0.390 - 0.501 之间继续我们的压缩进程。内存中始终不会有非常长的小数存在。使用二进制时也是一样的,我们会随着压缩的进行不断决定下一个要输出的二进制位是 0 还是 1,然后输出该位并减小内存中小数的长度。

静态模型如何实现?

我们知道上面的简单例子采用的是自适应模型,那么如何实现静态模型呢?其实很简单。对信息 bccb 我们统计出其中只有两个字符,概率分布为 Pb = 0.5,Pc = 0.5。我们在压缩过程中不必再更新此概率分布,每次对区间的划分都依照此分布即可,对上例也就是每次都平分区间。这样,我们的压缩过程可以简单表示为:

输出区间的下限 输出区间的上限 -------------------------------------------------- 压缩前 0.0 1.0 输入 b 0.0 0.5 输入 c 0.25 0.5 输入 c 0.375 0.5 输入 b 0.375 0.4375

我们看出,最后的输出区间在 0.375 - 0.4375 之间,甚至连一个十进制位都没有确定,也就是说,整个信息根本用不了一个十进制位。如果我们改用二进制来表示上述过程的话,我们会发现我们可以非常接近该信息的熵值(有的读者可能已经算出来了,该信息的熵值为 4 个二进制位)。

为什么用自适应模型?

既然我们使用静态模型可以很好地接近熵值,为什么还要采用自适应模型呢?

要知道,静态模型无法适应信息的多样性,例如,我们上面得出的概率分布没法在所有待压缩信息上使用,为了能正确解压缩,我们必须再消耗一定的空间保存静态模型统计出的概率分布,保存模型所用的空间将使我们重新远离熵值。其次,静态模型需要在压缩前对信息内字符的分布进行统计,这一统计过程将消耗大量的时间,使得本来就比较慢的算术编码压缩更加缓慢。

另外还有最重要的一点,对较长的信息,静态模型统计出的符号概率是该符号在整个信息中的出现概率,而自适应模型可以统计出某个符号在某一局部的出现概率或某个符号相对于某一上下文的出现概率,换句话说,自适应模型得到的概率分布将有利于对信息的压缩(可以说结合上下文的自适应模型的信息熵建立在更高的概率层次上,其总熵值更小),好的基于上下文的自适应模型得到的压缩结果将远远超过静态模型。

自适应模型的阶

我们通常用“阶”(order)这一术语区分不同的自适应模型。本章开头的例子中采用的是 0 阶自适应模型,也就是说,该例子中统计的是符号在已输入信息中的出现概率,没有考虑任何上下文信息。

如果我们将模型变成统计符号在某个特定符号后的出现概率,那么,模型就成为了 1 阶上下文自适应模型。举例来说,我们要对一篇英文文本进行编码,我们已经编码了 10000 个英文字符,刚刚编码的字符是 t,下一个要编码的字符是 h。我们在前面的编码过程中已经统计出前 10000 个字符中出现了 113 次字母 t,其中有 47 个 t 后面跟着字母 h。我们得出字符 h 在字符 t 后的出现频率是 47/113,我们使用这一频率对字符 h 进行编码,需要 -log2(47/113) = 1.266 位。

对比 0 阶自适应模型,如果前 10000 个字符中 h 的出现次数为 82 次,则字符 h 的概率是 82/10000,我们用此概率对 h 进行编码,需要 -log2(82/10000) = 6.930 位。考虑上下文因素的优势显而易见。

我们还可以进一步扩大这一优势,例如要编码字符 h 的前两个字符是 gt,而在已经编码的文本中 gt 后面出现 h 的概率是 80%,那么我们只需要 0.322 位就可以编码输出字符 h。此时,我们使用的模型叫做 2 阶上下文自适应模型。

最理想的情况是采用 3 阶自适应模型。此时,如果结合算术编码,对信息的压缩效果将达到惊人的程度。采用更高阶的模型需要消耗的系统空间和时间至少在目前还无法让人接受,使用算术压缩的应用程序大多数采用 2 阶或 3 阶的自适应模型。

转义码的作用

使用自适应模型的算术编码算法必须考虑如何为从未出现过的上下文编码。例如,在 1 阶上下文模型中,需要统计出现概率的上下文可能有 256 * 256 = 65536 种,因为 0 - 255 的所有字符都有可能出现在 0 - 255 个字符中任何一个之后。当我们面对一个从未出现过的上下文时(比如刚编码过字符 b,要编码字符 d,而在此之前,d 从未出现在 b 的后面),该怎样确定字符的概率呢?

比较简单的办法是在压缩开始之前,为所有可能的上下文分配计数为 1 的出现次数,如果在压缩中碰到从未出现的 bd 组合,我们认为 d 出现在 b 之后的次数为 1,并可由此得到概率进行正确的编码。使用这种方法的问题是,在压缩开始之前,在某上下文中的字符已经具有了一个比较小的频率。例如对 1 阶上下文模型,压缩前,任意字符的频率都被人为地设定为 1/65536,按照这个频率,压缩开始时每个字符要用 16 位编码,只有随着压缩的进行,出现较频繁的字符在频率分布图上占据了较大的空间后,压缩效果才会逐渐好起来。对于 2 阶或 3 阶上下文模型,情况就更糟糕,我们要为几乎从不出现的大多数上下文浪费大量的空间。

我们通过引入“转义码”来解决这一问题。“转义码”是混在压缩数据流中的特殊的记号,用于通知解压缩程序下一个上下文在此之前从未出现过,需要使用低阶的上下文进行编码。

举例来讲,在 3 阶上下文模型中,我们刚编码过 ght,下一个要编码的字符是 a,而在此之前,ght 后面从未出现过字符 a,这时,压缩程序输出转义码,然后检查 2 阶的上下文表,看在此之前 ht 后面出现 a 的次数;如果 ht 后面曾经出现过 a,那么就使用 2 阶上下文表中的概率为 a 编码,否则再输出转义码,检查 1 阶上下文表;如果仍未能查到,则输出转义码,转入最低的 0 阶上下文表,看以前是否出现过字符 a;如果以前根本没有出现过 a,那么我们转到一个特殊的“转义”上下文表,该表内包含 0 - 255 所有符号,每个符号的计数都为 1,并且永远不会被更新,任何在高阶上下文中没有出现的符号都可以退到这里按照 1/256 的频率进行编码。

“转义码”的引入使我们摆脱了从未出现过的上下文的困扰,可以使模型根据输入数据的变化快速调整到最佳位置,并迅速减少对高概率符号编码所需要的位数。

存储空间问题

在算术编码高阶上下文模型的实现中,对内存的需求量是一个十分棘手的问题。因为我们必须保持对已出现的上下文的计数,而高阶上下文模型中可能出现的上下文种类又是如此之多,数据结构的设计将直接影响到算法实现的成功与否。

在 1 阶上下文模型中,使用数组来进行出现次数的统计是可行的,但对于 2 阶或 3 阶上下文模型,数组大小将依照指数规律增长,现有计算机的内存满足不了我们的要求。

比较聪明的办法是采用树结构存储所有出现过的上下文。利用高阶上下文总是建立在低阶上下文的基础上这一规律,我们将 0 阶上下文表存储在数组中,每个数组元素包含了指向相应的 1 阶上下文表的指针,1 阶上下文表中又包含了指向 2 阶上下文表的指针……由此构成整个上下文树。树中只有出现过的上下文才拥有已分配的节点,没有出现过的上下文不必占用内存空间。在每个上下文表中,也无需保存所有 256 个字符的计数,只有在该上下文后面出现过的字符才拥有计数值。由此,我们可以最大限度地减少空间消耗。

资源

关于算术压缩具体的设计和实现请参考下面给出的示例程序。

程序 Arith-N 由 League for Programming Freedom 的 Mark Nelson 提供,由王笨笨在 Visual C++ 5.0 环境下编译、调试通过。

Arith-N 包含 Visual C++ 工程 ArithN.dsp 和 ArithNExpand.dsp,分别对应了压缩和解压缩程序 an.exe 与 ane.exe。

Arith-N 是可以在命令行指定阶数的 N 阶上下文自适应算术编码通用压缩、解压缩程序,由于是用作教程示例,为清晰起见,在某些地方并没有刻意进行效率上的优化。

 

(完)

create@2009-10-22

  • 大小: 13 KB
分享到:
评论

相关推荐

    信息论与编码课件信道信息熵哈夫曼编码算术编码游程码

    本课件集包含了这门课程的关键知识点,包括信源与信息熵、离散信道及信道容量、信源编码、有噪信道编码以及信息率失真函数等。下面将逐一解析这些核心概念。 首先,**信源与信息熵**是信息论的基础。信源是指信息的...

    熵编码方法_霍夫曼编码_visualc++_图像编码_算术编码_

    在给定的标题和描述中,提到了两种常见的熵编码方法:霍夫曼编码(Huffman Coding)和算术编码(Arithmetic Coding)。这两种编码方式都是无损压缩方法,即在解压后能完全恢复原始数据。 霍夫曼编码是一种基于频率...

    子带编码、算术编码、行程编码、图像融合(程序)

    算术编码是一种熵编码技术,用于无损或有损数据压缩。它通过将连续的概率分布映射到一个有限的数值区间,实现数据的高效编码。相比于常见的霍夫曼编码,算术编码能更精确地适应数据的概率分布,尤其适用于概率分布不...

    算术编码步骤图.pptx

    ## 3.4 熵编码 ### 3.4.1 变长编码 1952 年,哈夫曼提出变长编码方法:对...采用一个浮点数来代替一串输入符号,经算术编码后输出一个小于 1,大于或等于 0 的浮点数,在解码端被正确地唯一的解码,恢复原符号序列。

    算术编码.zip_arrangeh54_图像算术编码_图像编码_算术编码_算术编码 图像

    算术编码是数字图像处理领域中一种高效的数据压缩方法,主要应用于图像编码中。相比于传统的熵编码技术,如霍夫曼编码,算术编码能够更精确地表示数据的概率分布,从而在保持图像质量的同时,实现更高的压缩率。下面...

    算术编码源代码.rar

    算术编码是信息压缩领域的一种高效编码方法,与传统的熵编码技术如霍夫曼编码和游程编码有所不同。本文将详细解析算术编码的基本原理、实现细节以及在VC++环境下的应用。 首先,算术编码的核心思想是通过概率模型来...

    分布式算术编码.pdf

    当讨论分布式算术编码的发展前景时,考虑到其在无损压缩方面的优势,未来可能在信息传输、存储和处理中扮演重要角色。 在实际应用中,Slepian-Wolf问题的编码可以通过信道码或信源码实现。已有的很多实现方案使用...

    matlab算术编码代码

    与传统的熵编码如哈夫曼编码相比,算术编码能更精确地利用概率信息,尤其适用于概率分布不均匀的数据。 在MATLAB环境中实现算术编码,主要涉及到以下几个关键步骤: 1. **概率模型建立**:首先需要根据源数据建立...

    arithmatic coding_算术编码编码解码的c语言实现_

    在实际应用中,算术编码常与其他压缩技术结合使用,例如前缀编码(如霍夫曼编码)来处理无法精确表示的小概率事件,或者与熵编码(如LZ77、LZ78)相结合,以进一步提升压缩率。 总之,这个项目提供了一个C语言实现...

    算术编码的最终仿真结果

    算术编码的核心思想是将所有可能的信息表示为一个区间内的数值,这个区间的大小由各个符号出现的概率决定。具体而言,算术编码的过程可以分为以下几步: 1. **概率建模**:首先,需要构建一个概率模型,该模型定义...

    Arithmetic coding.rar_arithmetic coding_信息编码_算术编码_算术编码 matlab_算术

    在信息理论中,算术编码是基于熵编码的一种方法,它的核心思想是将每个可能的输入符号映射到一个开放的、不等宽的区间,这个区间的宽度对应于该符号出现的概率。在编码过程中,输入序列被转换为一系列的概率区间,并...

    算术编码,C语言实现

    算术编码是信息压缩领域的一种高效编码方法,与常见的熵编码技术如霍夫曼编码、PCM等并列。它的核心思想是将一个消息的概率分布转化为一个连续的实数区间,从而实现对数据的压缩。在C语言中实现算术编码,我们需要...

    xinxilun.rar_信息论与编码_算术编码

    2. **概率模型**:在算术编码中,首先需要建立输入数据的概率模型。这通常涉及到统计分析,例如使用频率分析来估计字符或符号出现的概率。 3. **编码过程**:编码开始时,初始编码区间设为[0, 1)。对每个输入符号,...

    熵编码方法_霍夫曼编码_visualc++_图像编码_算术编码_源码.zip

    算术编码的优势在于它能够更精确地利用概率分布,从而在理论上达到信息熵的极限,相比于霍夫曼编码,算术编码通常能提供更好的压缩率。在Visual C++实现中,你需要理解如何处理概率模型和编码区间,以及如何进行编码...

    超详细的算术编码matlab实现

    算术编码是信息压缩领域的一种高效编码方法,与常见的熵编码技术如霍夫曼编码、PCM等并列。本文将深入探讨算术编码在MATLAB环境中的实现,并结合提供的资源进行详细解析。 首先,让我们理解算术编码的基本原理。...

    算术编码 加权编码资料

    在视频编码中,由于相邻像素之间存在较强的统计依赖性,因此,加权编码可以根据已编码的像素信息更新每个像素的概率模型,从而提高编码效率。例如,在预测编码模式下,编码器可以根据先前的预测误差来调整当前像素的...

    SSBMmatrix.zip_算术编码_算术编码 matlab

    在传统的熵编码如哈夫曼编码中,频繁出现的符号被赋予较短的编码,而较少出现的符号则对应较长的编码。算术编码则更进一步,它不固定每个符号的编码长度,而是根据符号的概率动态调整编码区间。编码过程是这样的:将...

    数据压缩_C语言实现算术编码

    算术编码是一种高效的数据压缩方法,尤其适用于熵编码阶段,它在无损和有损压缩中都有广泛的应用。本文将深入探讨C语言实现的算术编码算法。 首先,我们需要理解算术编码的基本原理。算术编码通过连续概率模型对...

    suanshubianma.rar_suanshubianma_算术编码_算术编码matlab

    在这个"suanshubianma.rar_suanshubianma_算术编码_算术编码matlab"压缩包中,包含了一个名为"算术编码(Arithmetic Coding)源代码.txt"的文件,该文件提供了在MATLAB环境中实现算术编码的源代码。 算术编码的基本...

Global site tag (gtag.js) - Google Analytics