4.4 信源编码与率失真理论
前几节的熵与散度都是描述性的量。本节讨论它们的一个操作含义:压缩的极限。一个数据源产生的符号序列,在允许无损恢复的前提下,最少需要多少比特表示每个符号。这个问题的答案由熵给出,而且这个极限是可以逼近的,构造方法也已经成熟(Huffman 编码、算术编码、字典编码)。
本节分两条线索。第一条是无损压缩:先建立编码的语言(码字、前缀码、唯一可译码),给出码长必须满足的 Kraft 不等式,再证明信源编码定理(香农第一定理)给出的界
4.4.1 无损压缩的问题设定
编码与码字
信源编码(source coding)把信源符号映射为二进制串。设信源取值于
压缩的目标是在保证可恢复的前提下最小化
唯一可译与前缀码
可恢复性有两个层次。唯一可译码(uniquely decodable code)要求任意两条不同的符号序列编码后也不相同,因此接收端总能还原原始序列。前缀码(prefix code,也称即时码)要求任何一个码字都不是另一个码字的前缀,接收端可以在读到完整码字时立刻译码,不需要缓冲。
两类码的关系是包含:前缀码一定是唯一可译码,反之不然。前缀码的便利在于译码过程是流式的,实现简单,因此在实践中占主导。存在唯一可译但非前缀的码,其平均长度可以更短,但译码需要等待整个序列,实用价值有限。
4.4.2 Kraft 不等式
陈述
给定一组正整数码长
这就是 Kraft 不等式。它的含义是:把码字空间看作一棵二叉树的叶节点,深度为
证明思路
必要性用树结构论证。深度为
充分性的构造用贪心:把码长按升序排列,依次为每个码字分配最左侧的可用叶子。由于总配额不超过
Kraft 不等式的作用是把设计前缀码的问题简化为选择码长的问题:只要码长满足不等式,就一定存在对应的前缀码,无需关心具体码字如何分配。这一简化是信源编码定理证明的关键一步。
4.4.3 信源编码定理
定理的内容
信源编码定理(source coding theorem,香农第一定理)断言:对离散无记忆信源,任何唯一可译码的平均码长满足
且存在前缀码使
下界部分说明熵是压缩的极限,无论算法多复杂都无法突破;上界部分说明这个极限可以在
下界的证明
用 Kraft 不等式配合一个不等式即可。由 Kraft 不等式
把
上界的证明
取码长
两边按
还需验证这组码长满足 Kraft 不等式:
其中用到
达到极限的途径
上界中的
import numpy as np
def huffman_code_lengths(probs):
"""构造 Huffman 编码并返回各符号的码长与平均码长"""
probs = np.asarray(probs, dtype=float)
nodes = [(p, [i]) for i, p in enumerate(probs)]
lengths = np.zeros(len(probs), dtype=int)
while len(nodes) > 1:
nodes.sort(key=lambda t: t[0])
(p1, s1), (p2, s2) = nodes[0], nodes[1]
for i in s1 + s2:
lengths[i] += 1
nodes = nodes[2:] + [(p1 + p2, s1 + s2)]
return lengths
def entropy(p):
p = np.asarray(p, dtype=float)
p = p[p > 0]
return float(-np.sum(p * np.log2(p)))
probs = np.array([0.4, 0.2, 0.15, 0.1, 0.08, 0.05, 0.02])
L = huffman_code_lengths(probs)
avg = float(np.dot(probs, L))
H = entropy(probs)
print(f"各符号码长 = {L}")
print(f"平均码长 = {avg:.4f} 比特/符号")
print(f"熵 = {H:.4f} 比特/符号")
print(f"冗余 = {avg - H:.4f} 比特/符号(应小于 1)")4.4.4 三种实用的编码方法
Huffman 编码
Huffman 编码(Huffman coding)构造最优前缀码,步骤是反复合并概率最小的两个符号:建立叶节点,每次取出概率最小的两个节点合并为父节点(概率相加),重复到只剩一个根节点,再自上而下分配比特。
它的最优性可以证明:对任意其他前缀码,Huffman 码的平均码长不大于它。证明用交换论证:最优前缀码中概率最小的两个符号必然对应最深的一层且互为兄弟(否则把更深的码字与它们交换可以缩短平均码长),因此可以把这两个符号合并后递归处理,与构造过程一致。
Huffman 编码的局限有三条。第一,它要求知道信源的概率分布,且分布变化后需要重新构造码表。第二,逐符号编码存在 4.4.3 节分析的
算术编码
算术编码(arithmetic coding)把整个符号序列映射到
区间宽度正是序列概率的乘积,因此码长直接等于序列的自信息。把所有可能序列的概率乘起来看,平均码长收敛到熵率,不含逐符号取整的损失。
算术编码的两个工程要点。第一,区间需要以有限精度表示,实践中用整数区间与重归一化操作避免下溢出;第二,需要一个终止符号或显式给出序列长度,接收端才能确定何时停止。与 Huffman 相比,算术编码的优势在概率分布不均匀时更明显,它也是现代压缩工具(文本、图像、视频编码)中的核心组件。
import numpy as np
def arithmetic_encode_length(seq, probs):
"""按算术编码的区间收缩计算所需比特数(不实现整数化)"""
low, high = 0.0, 1.0
cum = np.concatenate([[0.0], np.cumsum(probs)])
for s in seq:
width = high - low
high = low + width * cum[s + 1]
low = low + width * cum[s]
width = high - low
return -np.log2(width), width
probs = np.array([0.7, 0.2, 0.1])
rng = np.random.default_rng(9)
for n in [10, 100, 1000]:
seq = rng.choice(len(probs), size=n, p=probs)
bits, width = arithmetic_encode_length(seq, probs)
self_info = -np.sum(np.log2(probs[seq]))
print(f"n = {n:4d}:算术编码所需比特 = {bits:.3f},序列自信息 = {self_info:.3f},"
f"每符号 = {bits / n:.4f}(熵 = {-(probs * np.log2(probs)).sum():.4f})")
# 每符号比特数随 n 增大收敛到熵,没有逐符号取整的固定开销字典编码
前两种方法依赖已知的概率分布。字典编码(dictionary coding)不需要分布,通过记录重复出现的子串来压缩,适用于有长距离重复的数据(文本、日志、程序代码)。
LZ77 维护一个滑动窗口,对每个位置寻找窗口内最长的匹配串,用(距离,长度,下一个字符)三元组表示。LZ78 维护一个动态增长的短语字典,把新出现的短语加入字典并输出(短语编号,下一个字符)。两者构成的家族(包括其改进版本 LZW、DEFLATE 使用的 LZ77 变体)是通用压缩工具的基础,gzip、zip、png 都使用这一系算法。
字典编码的压缩率不直接由熵给出,因为它利用的是序列内部的结构而不是符号频率。当序列来自有限状态的源(如文本)时,压缩率渐近趋于源的条件熵率(4.1 节),这解释了为什么长文本上的压缩比更高。它的自适应特性使分布未知时也能工作,代价是对短序列或高熵数据几乎没有压缩效果。
4.4.5 微分熵与连续信源
定义与性质差异
连续随机变量的微分熵(differential entropy)定义为
它与离散熵有三个重要差别。第一,微分熵可以为负:均匀分布
连续情形下可操作的量是互信息与 KL 散度,它们由两个微分熵之差定义,单位依赖被抵消:
最大微分熵与熵功率不等式
给定方差约束的最大微分熵分布是正态分布:
这一结论与 4.3 节的最大熵推导一致,是正态分布在给定方差下引入假设最少的另一种表述。它在信道容量推导中反复使用。
熵功率不等式(entropy power inequality)给出独立随机变量之和的微分熵下界:
对独立的
4.4.6 率失真理论
问题设定
无损压缩要求完全恢复原始数据,实际中往往允许一定误差。率失真理论(rate-distortion theory)处理允许误差时的压缩极限。引入失真度量(distortion measure)
限制不超过给定水平,寻找所需的最小信息率。
率失真函数
率失真函数(rate-distortion function)定义为在失真不超过
互信息在这里的角色是信息率的度量:编码器把
单调不增:允许的失真越大,所需的信息率越小。
凸性:
在
香农下界与可达性:
高斯信源的率失真函数
设
以比特为单位时前面的系数为
推导思路用最大微分熵与反向信道论证:给定失真约束,重建误差
import numpy as np
# 伯努利信源在汉明失真下的率失真函数
def rate_distortion_bernoulli(p, n_grid=200):
"""计算 R(D),D 从 0 到 min(p, 1-p)"""
D_max = min(p, 1 - p)
Ds = np.linspace(1e-6, D_max, n_grid)
Rs = []
for D in Ds:
# 最优测试信道:以概率 alpha 翻转,选择使 I 最小的 alpha
from scipy import optimize
def mutual(alpha):
# 信道:P(hat != x) = alpha
# 联合分布
q = np.array([[p * (1 - alpha), p * alpha],
[(1 - p) * alpha, (1 - p) * (1 - alpha)]])
px = q.sum(axis=1, keepdims=True)
py = q.sum(axis=0, keepdims=True)
with np.errstate(divide="ignore", invalid="ignore"):
terms = np.where(q > 0, q * np.log2(np.where(q > 0, q / (px * py), 1.0)), 0.0)
return float(terms.sum())
# 失真 D = alpha
val = mutual(D)
Rs.append(val)
return Ds, np.array(Rs)
def entropy(p):
ps = np.array([p, 1 - p])
ps = ps[ps > 0]
return float(-np.sum(ps * np.log2(ps)))
p = 0.3
Ds, Rs = rate_distortion_bernoulli(p)
print(f"伯努利信源 p = {p},熵 H(X) = {entropy(p):.4f} 比特")
print(f"D = 0.000001 处 R ≈ {Rs[0]:.4f}(应接近 H(X))")
print(f"D = {Ds[-1]:.4f}(= min(p,1-p))处 R ≈ {Rs[-1]:.4f}(应接近 0)")
for D in [0.05, 0.1, 0.2]:
idx = np.argmin(np.abs(Ds - D))
print(f" D = {D:.2f}:R = {Rs[idx]:.4f} 比特/符号")
# R(D) 单调递减且在 D = 0 处等于熵,与理论一致
# 高斯信源的率失真函数
sigma2 = 4.0
for D in [0.5, 1.0, 2.0, 4.0]:
R = 0.5 * np.log2(sigma2 / D) if D < sigma2 else 0.0
print(f"高斯信源 sigma^2 = {sigma2},D = {D}:R = {max(R, 0):.4f} 比特")4.4.7 本节小结
压缩的两个极限
本节给出压缩的两条极限。无损压缩的极限是熵:Kraft 不等式把前缀码的设计归结为码长的选择,信源编码定理由此得到下界
压缩关心的是把一个信源的信息压到多少比特。与之对称的问题是:经过一条有噪声的信道,最多能可靠地传输多少信息。这一问题的答案由信道容量给出,它是输入与输出之间互信息的最大值,可靠传输的可行性同样由典型性理论保证。下一节讨论信道容量与编码定理,并给出高斯信道、并行信道与多用户情形的具体结论。
练习题
第 1 题 概念推导
设信源取值于
参考答案
构造:信源的熵为
取码长为
此时
达到等号的条件:由下界证明中的 KL 散度为零的条件,需要
Kraft 不等式验证:
恰好等于
第 2 题 计算推理
某文本文件包含
| 字符 | A | B | C | D | E |
|---|---|---|---|---|---|
| 频率 | 0.35 | 0.25 | 0.2 | 0.15 | 0.05 |
参考答案
熵:
逐项计算:
Huffman 构造:合并最小的两个(E: 0.05 与 D: 0.15)得到 0.2;此时概率集合为
一种可行的码长分配(自顶向下):A: 2,B: 2,C: 2,D: 3,E: 3。验证 Kraft:
平均码长:
冗余为
与固定长度编码的差距:固定长度编码每符号
第 3 题 代码验证
实现 Huffman 编码与算术编码,在一个偏斜分布上比较两者的平均码长;再用长序列验证算术编码的每符号码长收敛到熵,并说明 Huffman 编码在多符号块上如何缩小差距。
参考答案
import numpy as np
def huffman_lengths(probs):
probs = list(probs)
nodes = [(p, [i]) for i, p in enumerate(probs)]
lengths = [0] * len(probs)
while len(nodes) > 1:
nodes.sort(key=lambda t: t[0])
(p1, s1), (p2, s2) = nodes[0], nodes[1]
for i in s1 + s2:
lengths[i] += 1
nodes = nodes[2:] + [(p1 + p2, s1 + s2)]
return np.array(lengths)
def entropy(p):
p = np.asarray(p, dtype=float)
p = p[p > 0]
return float(-np.sum(p * np.log2(p)))
def arithmetic_bits(seq, probs):
low, high = 0.0, 1.0
cum = np.concatenate([[0.0], np.cumsum(probs)])
for s in seq:
w = high - low
high = low + w * cum[s + 1]
low = low + w * cum[s]
return -np.log2(high - low)
probs = np.array([0.8, 0.15, 0.05])
H = entropy(probs)
L = huffman_lengths(probs)
print(f"熵 = {H:.4f},Huffman 平均码长 = {np.dot(probs, L):.4f},冗余 = {np.dot(probs, L) - H:.4f}")
rng = np.random.default_rng(17)
for n in [10, 100, 1000, 10_000]:
seq = rng.choice(len(probs), size=n, p=probs)
print(f"n = {n:6d}:算术编码每符号 = {arithmetic_bits(seq, probs) / n:.4f} 比特(熵 = {H:.4f})")
# Huffman 的块编码:把两个符号拼成一个新符号
joint = np.outer(probs, probs).flatten()
L2 = huffman_lengths(joint)
print(f"\n块长 2 的 Huffman 平均码长/符号 = {np.dot(joint, L2) / 2:.4f}")
joint4 = np.outer(np.outer(probs, probs).flatten(), np.outer(probs, probs).flatten()).flatten()
L4 = huffman_lengths(joint4)
print(f"块长 4 的 Huffman 平均码长/符号 = {np.dot(joint4, L4) / 4:.4f}")输出显示三点。第一,单符号 Huffman 的冗余约
第 4 题 综合应用
某团队要在带宽受限的链路上传输传感器的时间序列数据,采样率为每秒
参考答案
所需比特率:由高斯信源的率失真函数,
乘以采样率:
放宽误差的影响:
比特率降为约
实际压缩率低于理论上限的三个原因:第一,率失真函数假设以很长的块编码并且能构造任意复杂的码本,实际编码器的块长与码本复杂度受限,存在编码冗余。第二,理论上限基于理想的失真度量与已知分布,实际数据的分布需要在线估计,估计误差会转化为额外的比特;时间序列如果存在样本间相关,理想做法是先做预测再对残差编码,如果没有利用相关性,比特率会明显偏高。第三,实际系统还要考虑抗噪与延迟:在不可靠链路上需要加入纠错冗余(下一节的信道编码),端到端延迟要求限制了块长。三条因素叠加,实际可达的压缩率通常只有理论上限的
常见错误
错误 1 · 声称压缩率可以低于熵
原因:信源编码定理给出的下界是严格的,任何唯一可译码的平均码长都不小于熵。看到某个工具把文件压到比估计的熵更小,通常是熵的估计有误(把有记忆的数据当作无记忆处理,或分布估计偏差),而不是突破了极限。对有记忆信源,正确的下界是熵率而不是单符号熵,后者可能远大于前者。
解决:估计熵之前先检查符号之间是否存在依赖。做法是对比
错误 2 · 在短序列上使用算术编码而不考虑终止开销
原因:算术编码的码长等于序列的负对数概率,理论上没有固定开销,但实现上需要额外的比特来标记序列结束(终止符号或长度信息)。短序列上这部分开销占比很小;若把理论值直接与实际字节数比较,会误判实现有缺陷。
解决:把终止开销单独度量并报告。对短序列(长度小于几十个符号),逐符号的 Huffman 或直接发送原始数据可能更划算,选择方案时按实际长度评估。诊断方式是在多个序列长度上比较实际比特数减去自信息和这一差值是否稳定。
错误 3 · 把微分熵当作可比较的绝对量
原因:微分熵依赖坐标的单位,换量纲会引入常数偏移,且取值可以为负。直接把两个不同数据源的微分熵数值拿来比较,结论没有意义。
解决:比较时使用互信息、KL 散度或条件微分熵之差,这些量在换单位时相互抵消。报告微分熵时注明单位与坐标定义;在涉及连续信道的推导中,微分熵通常以之差的形式出现,这一点可以避免误用。
错误 4 · 用率失真函数当作实际系统的设计目标而忽略其假设
原因:率失真函数的下界在块长趋于无穷、码本可任意复杂、信源分布已知且平稳的假设下取得。实际系统受延迟、复杂度与平稳性限制,把理论下界当作设计目标会导致系统无法实现,也会误判现有实现的效率。
解决:把率失真函数作为参照基准而非目标。计算理论下界与实际实现的比值,用它评估改进空间;同时考虑延迟与复杂度约束下的实际可达区域(率失真复杂度函数的思路)。若信源非平稳,还需要按时间段分别估计率失真曲线,或用在线自适应方法跟踪分布变化。