try ai
科普
编辑
分享
反馈
  • 可变长度编码

可变长度编码

SciencePedia玻尔百科
核心要点
  • 可变长度编码通过为较常见的符号分配较短的编码、为较罕见的符号分配较长的编码来提升数据效率。
  • 前缀属性是一条关键规则,它通过确保没有任何码字是另一个码字的前缀,来保证编码可以被即时、无歧义地解码。
  • 这种编码原理的应用超出了文件压缩的范畴,影响了 CPU 指令集设计、数据序列化和信号处理。
  • 香农熵定义了压缩的理论极限,代表了信源每个符号所需的最小平均比特数。
  • 尽管效率很高,可变长度编码也可能带来安全风险,因为在旁道攻击中,压缩后加密数据的长度可能会泄露信息。

引言

信息的本质在于传递消息,但我们如何才能最高效地传递信息呢?一个为每个符号赋予相同权重的僵化系统虽然简单,却十分浪费,尤其是当某些符号的出现频率远高于其他符号时。这种低效率正是可变长度编码所要解决的核心问题。可变长度编码基于一个简单的理念:为常见事物使用短描述,为罕见事物使用长描述。这一原则在数据压缩和通信领域带来了显著的效率提升,但其实现需要克服歧义性和理论极限等微妙挑战。

本文将深入探讨可变长度编码的世界,揭示这一优雅思想如何付诸实践。第一章“原理与机制”将剖析其核心机制,解释前缀码如何防止歧义,Kraft-McMillan 不等式如何规定可能性边界,以及香农熵如何为压缩设定最终基准。随后,“应用与跨学科联系”将探讨该原则的深远影响,展示它不仅能缩小文件体积,还能加速计算、改善数字媒体,甚至在密码学中引入意想不到的漏洞。

原理与机制

想象一下,你正在设计一种语言,但你只有两个字母可用:0 和 1。你的任务是创建一本词典,将来自信源——比如一颗观测气象模式的卫星——的消息翻译成 0 和 1 组成的数据流。你会怎么做呢?最直接的方法,一种数字化的平均主义,是给每条可能的消息一个等长的“单词”。这就是​​定长编码​​的本质。

统一编码的低效性

假设我们的卫星可以报告六种不同的大气状况:C1,C2,…,C6C_1, C_2, \dots, C_6C1​,C2​,…,C6​。要为每种状况赋予一个独一无二的二进制名称,我们需要找到单词可能的最短长度 LLL。当 L=1L=1L=1 时,我们可以创造两个名称(0, 1)。当 L=2L=2L=2 时,可以创造四个(00, 01, 10, 11)。要命名六样东西,我们至少需要六个独一无二的二进制字符串。长度为 LLL 的唯一字符串数量是 2L2^L2L。因此,我们需要满足 2L≥62^L \ge 62L≥6。能满足条件的最小整数 LLL 是 3,因为 22=42^2=422=4 太小,而 23=82^3=823=8 则足够。所以,我们的定长词典对传输的每一个符号都使用 3 个比特。

这种方法简单、可靠且易于解码。但它高效吗?如果我们的卫星报告状况 C1C_1C1​(比如“晴空万里”)的概率是 35%,而状况 C6C_6C6​(“宇宙射线异常”)的概率只有 5%,情况又会如何?我们的定长编码用同样的力气——3 个比特——来报告平凡和罕见的事件。这感觉就像在浪费口舌。

正是这一核心洞见引发了数据压缩领域的一场革命。如果某些消息比其他消息常见得多,为什么不给它们更短的名称呢?这就是​​可变长度编码​​背后简单而强大的思想。让我们以交通信号灯为例。绿灯亮的时间占 60%,红灯占 30%,黄灯仅占 10%。定长编码需要为每种状态分配 2 个比特(例如,绿灯=00,黄灯=01,红灯=10)。平均长度自然是 2 比特。但如果我们改用这个编码:绿灯=0,红灯=11,黄灯=10 呢?现在,60% 的时间我们只发送一个比特。我们发送的平均比特数是 (0.60×1)+(0.30×2)+(0.10×2)=1.4(0.60 \times 1) + (0.30 \times 2) + (0.10 \times 2) = 1.4(0.60×1)+(0.30×2)+(0.10×2)=1.4 比特。对于每次信号发送,这都节省了 substantial 的 0.60.60.6 比特。对于一个概率分布高度倾斜的信源——比如一个 80% 的时间都报告“正常”的传感器——这种策略的效率会显著提高,可能将数据负载减少 35% 或更多。我们通过放弃定长编码的僵化统一性,换来一个根据信源统计特性量身定制的灵活系统,从而实现了这种效率。

歧义的风险

但这种新获得的巧妙引入了一个微妙而危险的问题。当我们收到一长串 0 和 1,比如 101100111,我们如何知道一个符号的编码在哪里结束,下一个又从哪里开始?对于定长编码,这很简单:只需将数据流切成长度为 3 的块。但对于可变长度,边界是隐藏的。

假设一位工程师为三个符号提出了一个编码:S1→0S_1 \to 0S1​→0,S2→10S_2 \to 10S2​→10 和 S3→01S_3 \to 01S3​→01。乍一看,这似乎没问题。码字都是不同的。但当我们收到数据流 010 时会发生什么?解码器可能看到开头的 01 并将其识别为 S3S_3S3​,剩下 0,即 S1S_1S1​。所以消息是 S3S1S_3 S_1S3​S1​。但还存在另一种同样有效的解释:解码器可以将开头的 0 视为 S1S_1S1​,剩下 10,即 S2S_2S2​。消息也完全可能是 S1S2S_1 S_2S1​S2​。数据流 010 存在歧义。

这是一个灾难性的失败。该编码不是​​唯一可解码的​​。一个有用的编码必须保证任何有效的拼接流都只有一种可能的解析方式。我们的聪明才智让我们陷入了陷阱。我们需要一个规则,一个保证,来防止这种混淆。

优雅的解决方案:前缀属性

解决歧义问题最常见也最优雅的方案是强制执行​​前缀属性​​。如果词典中没有任何码字是其他任何码字的前缀,那么这种编码就称为​​前缀码​​(或即时码)。例如,在我们失败的例子中,0 是 01 的前缀。这是被禁止的。

考虑这个为太空探测器上四种仪器设计的前缀码:A →\to→ 0,B →\to→ 10,C →\to→ 110,D →\to→ 111。注意,0 不是任何其他编码的开头。10 不是任何其他编码的开头。以此类推。

现在,让我们尝试解码数据流 101100111。我们从左往右读。

  1. 1 是码字吗?不是。10 呢?是的,它是 B。我们被保证没有更长的码字以 10 开头,所以我们可以立即确定是 B。剩余的数据流是 1100111。
  2. 1 是码字吗?不是。11 呢?不是。110 呢?是的,它是 C。我们立即记录 C。剩余数据流:0111。
  3. 0 是码字吗?是的,它是 A。记录 A。剩余数据流:111。
  4. 1 是吗?不是。11?不是。111?是的,它是 D。记录 D。数据流为空。

解码后的消息毫无歧义地是 B[CAD](/sciencepedia/feynman/keyword/computer_aided_design)。不需要向前看或回溯。一旦你读完一个完整的码字,你就知道它是什么。这种“即时”特性非常强大,也是前缀码成为现实世界系统基石的原因,从你电脑上的文件(如 .zip 或 .jpg)到通过互联网传输的数据。

编码的通用预算

那么,只要满足前缀属性,我们就可以随意挑选任何一组码字长度吗?比如,如果我们有二十个符号,可以为每个符号都分配一个 1 比特的编码吗?当然不行。这里有一个基本的约束在起作用,一种信息守恒定律。这个定律被​​Kraft-McMillan 不等式​​所描述。

想象你有一个等于 1 的“编码预算”。对于二进制字母表(D=2D=2D=2),分配一个长度为 lil_ili​ 的码字会“花费”你预算的 2−li2^{-l_i}2−li​。一个长度为 1 的码字花费 2−1=122^{-1} = \frac{1}{2}2−1=21​。一个长度为 2 的码字花费 2−2=142^{-2} = \frac{1}{4}2−2=41​,以此类推。该不等式指出,对于任何唯一可解码的编码,所有码字的成本之和不能超过你的预算:

∑i=1MD−li≤1\sum_{i=1}^{M} D^{-l_i} \le 1i=1∑M​D−li​≤1

其中 MMM 是符号的数量,DDD 是编码字母表的大小(对于二进制,D=2D=2D=2)。

让我们看看实际应用。一个生物工程师团队想用一个包含 4 个元素(A, T, C, G)的合成 DNA 字母表来编码 20 种氨基酸,所以 D=4D=4D=4。他们提出了一个方案,包含 4 个长度为 1 的码字,8 个长度为 2 的码字,以及 8 个长度为 3 的码字。他们是否超支了预算?让我们计算总成本:

Cost=(4×4−1)+(8×4−2)+(8×4−3)=(4×14)+(8×116)+(8×164)=1+12+18=1.625\text{Cost} = (4 \times 4^{-1}) + (8 \times 4^{-2}) + (8 \times 4^{-3}) = \left(4 \times \frac{1}{4}\right) + \left(8 \times \frac{1}{16}\right) + \left(8 \times \frac{1}{64}\right) = 1 + \frac{1}{2} + \frac{1}{8} = 1.625Cost=(4×4−1)+(8×4−2)+(8×4−3)=(4×41​)+(8×161​)+(8×641​)=1+21​+81​=1.625

他们的成本 1.6251.6251.625 大于预算 1。Kraft-McMillan 定理以数学的确定性告诉我们,用这组长度构建一个唯一可解码的编码是不可能的。无论你多聪明,预算已经超支了。这个定理的美妙之处在于,它允许我们仅根据长度来判断一个编码的可行性,而无需看到实际的码字!

对完美的追求

我们知道如何构建高效、无歧义的编码。但极限在哪里?我们能将一个信源压缩多少?最好的编码是什么?答案由信息论之父 Claude Shannon 给出。他定义了一个量,称为信源的​​熵​​,通常用 HHH 表示。熵是衡量信源内在不确定性或“惊奇”程度的度量。它代表了任何唯一可解码编码所能达到的每个符号平均比特数的绝对理论最小值。

H=−∑i=1Mpilog⁡2(pi)H = -\sum_{i=1}^{M} p_i \log_2(p_i)H=−i=1∑M​pi​log2​(pi​)

对于我们那有六个符号的卫星,熵计算出来大约是 2.36 比特/符号。回想一下,我们的定长编码需要 3 比特/符号。一个巧妙的可变长度方案,比如由著名的​​霍夫曼算法​​生成的方案,可以达到 2.45 比特/符号的平均长度。这相对于定长编码是一个巨大的进步,并且非常接近香农熵的极限。霍夫曼平均长度与熵之间的微小差距被称为编码的​​冗余​​——这是我们因为每个符号必须使用整数个比特而付出的代价。

有没有可能实现零冗余?构建一个完全高效的编码?是的,在一种非常特殊而优美的情况下:当所有符号的概率都是 2 的负整数次幂时(例如,12,14,18,18,…\frac{1}{2}, \frac{1}{4}, \frac{1}{8}, \frac{1}{8}, \dots21​,41​,81​,81​,…)。在这种情况下,由 −log⁡2(pi)-\log_2(p_i)−log2​(pi​) 给出的每个符号的理想长度恰好是一个整数。对于概率为 pi=18=2−3p_i = \frac{1}{8} = 2^{-3}pi​=81​=2−3 的符号,理想长度是 −log⁡2(2−3)=3-\log_2(2^{-3}) = 3−log2​(2−3)=3 比特。霍夫曼算法可以精确地分配这些整数长度,从而使平均编码长度与信源熵完全相等。这个编码与信源完美匹配,不含任何冗余。这是数据压缩的终极体现。

现实的考量:权衡与后果

虽然可变长度编码在平均情况下提供了卓越的效率,但这种效率也伴随着实际的权衡。工程师不仅要考虑平均情况,还必须考虑最坏情况。

想象一个接收器有一个小的 40 比特输入缓冲区。解码器被设计为以稳定的速率处理比特,这个速率与定长编码的 3 比特/符号完美匹配。现在,我们切换到一个可变长度编码,其中最长的码字是 4 比特。如果我们遇到一长串不幸的罕见符号,它们都使用这个 4 比特的编码,会发生什么?每当一个符号到达,4 个比特涌入缓冲区,而解码器只消耗 3 个比特。每个周期,缓冲区的水位上升 1 比特。在 38 个这样的符号之后,缓冲区将接近满溢,下一个 4 比特码字的到来将导致它溢出。这是定长编码永远不会遇到的问题。平滑、可预测的数据流被换成了更高的平均吞吐量,但也因此产生了对数据突发的脆弱性。

此外,我们钟爱的前缀码并非唯一可解码的编码类型。存在一些编码,你可能需要向前看来解决歧义,但它们不是即时的。例如,编码 {1,10,100,…,10M−1}\{1, 10, 100, \dots, 10^{M-1}\}{1,10,100,…,10M−1} 是唯一可解码的,但在看到一个 1 后,你必须数清后面有多少个零才能知道是哪个符号,在最坏的情况下可能需要向前看 M−1M-1M−1 个比特。因此,选择一种编码不仅仅是关于抽象的效率;它是一个丰富的设计决策,涉及平均压缩率、系统复杂性、延迟以及对最坏情况行为的鲁棒性之间的权衡。用 0 和 1 为事物命名的简单行为,开启了一个充满深刻而实际挑战的世界。

应用与跨学科联系

我们花了一些时间探讨可变长度编码的机制,这个巧妙的想法是为常见事物使用短描述,为罕见事物使用长描述。乍一看,这似乎只是一个精巧的技巧,一种为缩小文件体积这一特定问题而生的简洁工程方案。但这就像说杠杆原理只是举起石头的好方法一样。事实远比这更优美、更深远。这种高效表示的原则不仅仅是一个技巧,它是一条基本法则,回响在从计算机设计到我们最机密通信安全的各种领域中。这是那些一旦理解,你就会开始随处看到的奇妙简单思想之一。

数据的原生语言:压缩与通信

让我们从最熟悉的领域开始:数据。当你“压缩”一个文件时,你本质上是在教计算机说一种更高效的语言。你不再使用像标准 ASCII 那样僵化、一刀切的字母表——其中常见的字母 'e' 和罕见的字母 'z' 占用完全相同的 8 个比特——而是创建了一种自定义的方言。在这种新的方言中,最频繁的字符和短语被赋予了更短的名称。

想象你正在为一艘深空探测器设计通信系统,它从观测中发回数据。探测器的电力非常宝贵,其传输带宽就像穿越浩瀚太空的一根细吸管。你的仪器测量宇宙背景辐射的波动,你很快发现数据并非随机噪声。某些测量值出现的频率远高于其他值。如果使用定长编码,给每个可能的测量值相同数量的比特,将是巨大的能源浪费。这就像用喊“antidisestablishmentarianism”(一个很长的英文单词)的力气和时长来喊“the”一样。通过创建可变长度编码,你可以轻声细语地报告常见结果,只在报告罕见、令人惊讶的结果时才耗费能量去“大喊”。总体结果是显著节省了电力和时间,使我们能够用相同的能量预算从宇宙的遥远角落接收更多的科学数据。

这个想法可以进一步提炼。有时,数据的结构不仅仅在于单个符号的频率,还在于数字本身的性质。考虑一个测量环境压力微小变化的传感器。大多数时候,变化将是零或一个非常小的整数。大的波动很罕见。在这种情况下,像霍夫曼编码这样的通用方案可能有效,但我们可以通过设计一种专门针对此类数字分布的编码来做得更好。这就是像 Rice 编码这类方法背后的思想,它们非常擅长紧凑地表示那些倾向于很小的数字流。这是另一层专业化,是朝着为数据试图讲述的故事创造最有效语言的又一步。

构建更智能的机器:从体系结构到算法

高效表示的原则不仅适用于静止或传输中的数据,它还被编织进了计算本身的结构中。让我们窥视一下中央处理器(CPU)的内部。CPU 执行程序,程序是一系列指令,如 ADD、MULTIPLY 或 LOAD_FROM_MEMORY。这些指令中的每一个都有一个名称,即它的“操作码”。一个简单的方法是给每个可能的操作码一个定长的二进制名称。但如果你分析任何典型的程序,你会发现某些指令,比如加载和存储数据,比其他指令(比如除法)使用得频繁得多。

一个让人联想到摩尔斯电码的巧妙想法是,用可变长度的操作码来设计指令集。我们可以给最常见的指令非常短的二进制名称,而将罕见的指令分配给更长的名称。其效果是,整个编译后的程序变得更小。这种“代码密度”不仅仅是美学上的胜利;它意味着程序在内存中占用的空间更少,并且至关重要的是,从内存中将指令提取到 CPU 中执行所需的带宽也更少。在一个光速和内存延迟是硬性物理限制的世界里,让我们的程序变小可以直接转化为让它们运行得更快。

这种哲学从硬件延伸到软件和数据格式的世界。当不同的计算机系统需要相互通信时,它们必须就其数据的通用语言达成一致。这被称为序列化。一种天真的方法是直接转储内存中的原始字节,但这充满了危险,因为存在诸如“字节序”——即机器是先存储数字的大端还是小端——等问题。一种更稳健、更高效的方法是对整数使用可变长度编码。例如,在记录事件或通过网络发送数据时,我们处理的大多数数字都很小。通过使用可变数量的字节来编码它们——小数字用一个字节,中等数字用两个字节,以此类推——我们可以节省大量的空间。这些方案特别巧妙的地方在于,字节顺序是由编码算法本身定义的,使其完全独立于机器的本地体系结构。它是一种自成一体的语言,一种任何机器都能说和无歧义理解的数字世界语,并且是驱动互联网的许多现代数据格式的核心。

对计算速度的追求将我们带到高性能计算中更高级的应用。许多大规模科学模拟,从模拟星系到设计飞机,都依赖于求解涉及巨大“稀疏”矩阵——即绝大部分元素为零的矩阵——的方程组。高效地存储和处理这些矩阵是一项巨大的挑战。一个主要的瓶颈就是将描述矩阵结构的数据从计算机主内存移动到 CPU。我们的原则再次前来救援。通过注意到这些矩阵中的非零元素通常以聚集或可预测的模式出现,我们可以压缩描述其位置的数据。通过结合使用增量编码(存储位置之间的差异而非绝对位置)和可变长度整数编码,我们可以缩小矩阵结构本身的表示。这意味着需要移动的数据更少,这反过来又意味着处理器等待的时间更少,计算的时间更多,从而加速了科学发现。

视与听的艺术:信号处理

我们自己的感官并不会平等对待所有信息。我们的眼睛对亮度的变化比对颜色的变化更敏感,我们的耳朵可以在嘈杂的房间里分辨出熟悉的声音。现代信号处理,这项促成从数码摄影到音乐流媒体等一切的技术,也学到了同样的教训。

以 JPEG 格式中的图像压缩为例。该过程通常从一个称为矢量量化的“有损”步骤开始。图像被分解成小的像素块,每个块都被其在一个预定义的典型块模式“码本”中最接近的匹配项所取代。这就像按数字填色,但调色板要丰富得多。这个阶段的输出不是图像本身,而是一系列指向每个块使用了哪个码本条目的索引流。现在,关键在于:这些索引的使用频率并不相等。某些模式(如一片蓝天或一块皮肤)比其他模式常见得多。因此,压缩的第二阶段就是获取这个索引流,并应用一个无损的可变长度编码,如霍夫曼编码。该系统将信号的“模糊”但有效的近似与该近似的完美高效表示结合在一起。

这种近似与编码之间的相互作用可以被带到一个更深的层次。在标准设置中,你首先设计你的量化器,然后,作为事后考虑,你压缩它的输出。但如果量化器知道它将与一个可变长度编码器一起工作呢?如果它们可以合作呢?这就是熵约束量化的领域。量化器的目标不仅仅是最小化失真,而是最小化失真和最终编码比特率的组合成本。这导致了一个显著的结果:量化器实际上会调整其决策边界。它会使概率更高的信号区域变大,而使概率较低的信号区域变小。为什么?因为通过使大概率信号变得更加可能(通过将更多输入空间归入它们的箱子),它降低了其输出的熵,使其更容易被后续的可变长度编码器压缩。这是一个美妙的反馈循环,系统在其中学会以一种不仅准确而且易于描述的方式来“看待”世界。

警示:压缩在安全领域的风险

到目前为止,我们一直在赞美这个奇妙的原则。但就像任何强大的工具一样,它也有其阴暗面,其应用需要智慧。正是那个让可变长度编码如此有用的特性——符号的概率与其编码长度之间的联系——在密码学的背景下可能成为一个致命的缺陷。

想象你正在发送一条秘密消息。你首先将其压缩,这很合理,然后使用像一次性密码本这样的理论上完美的方法对其进行加密。你可能相信你的系统是无懈可击的。但事实并非如此。问题在于,一个窃听者,即使他们无法破译你加密消息的内容,他们仍然可以观察到它的长度。

因为你使用了可变长度编码,压缩后消息的长度取决于原始消息。一条常见的、可预测的消息(如“黎明时分进攻”)可能会压缩成一个非常短的字符串,而一条不寻常的、看起来随机的消息可能会压缩成一个长得多的字符串。密文的长度与压缩后消息的长度相同。因此,仅通过观察加密流量的长度,对手就了解了关于原始未加密消息性质的一些信息!这是一种“旁道攻击”。它彻底打破了完美保密的承诺,该承诺要求密文绝对不能泄露任何关于明文的信息。这不仅仅是一个理论上的好奇心;基于这一原则的漏洞(如针对网络加密的 CRIME 和 BREACH 攻击)已经在现实世界中得到证实,迫使保护我们互联网流量的协议做出改变。

这是一个令人谦卑的教训。在我们追求效率的过程中,我们制造了一个微妙的信息泄露。我们精心优化的编码长度,成了地板下那颗泄密的心跳。它提醒我们,在科学和工程中,原则从不是在真空中应用的。背景决定一切。可变长度编码的优雅效率对于压缩来说是一个福音,是计算的基础,但对于安全来说却是一个潜在的灾难。理解其在何处适用以及为何如此,才是智慧的真正标志。