4信息论基础(第2版)

数据压缩定理

信源编码

无损压缩Huffman 编码信源编码定理Kraft 不等式

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第4章 数据压缩定理(Source Coding Theorem)

一、导读

4.1 本章学习目标

通过本章的学习,你将能够:

理解信源编码的基本问题:掌握无损压缩和有损压缩的区别,理解压缩率与失真之间的权衡

掌握香农信源编码定理:理解为什么熵是无损压缩的极限,掌握定理的证明思路

掌握Huffman编码算法:能够手动构建Huffman树,理解其最优性证明

理解算术编码的原理:掌握算术编码如何突破Huffman编码的限制,实现更接近熵的压缩

掌握LZ系列算法的思想:理解字典编码如何利用数据的重复结构进行压缩

理解率失真理论的基本概念:掌握率失真函数 $R(D)$ 的定义和性质

能够分析实际压缩算法的性能:理解为什么不同算法在不同数据上表现不同

理解通用信源编码的概念:掌握为什么可以在不知道信源分布的情况下实现接近最优的压缩

4.2 为什么本章如此重要

数据压缩是信息论最早也是最成功的应用之一。从ZIP文件到JPEG图片,从MP3音频到H.264视频,压缩技术无处不在。信源编码定理给出了压缩的理论极限,指导了半个多世纪以来压缩算法的发展。

理论重要性:

信源编码定理(也称为香农第一定理)是信息论的基石之一。它精确地回答了"数据可以被压缩到什么程度"这个基本问题:无损压缩的极限是熵 $H(X)$。这个结果看似简单,但其证明涉及典型序列、 Kraft不等式、前缀码等深刻概念。

实践重要性:

现代压缩标准(如DEFLATE、WebP、AV1)都建立在信源编码理论的基础上。理解这些理论有助于我们:

  • 选择合适的压缩算法(根据数据类型和压缩需求)
  • 理解压缩参数的影响(如质量因子、压缩级别)
  • 设计新的压缩方案(针对特定应用场景)

方法论重要性:

信源编码的证明技巧(如典型序列编码、随机码本构造)在信道编码和网络信息论中反复出现。 Kraft不等式和前缀码的概念在编译器设计、数据库索引等领域也有应用。

4.3 核心问题引导

在学习本章之前,请思考以下问题:

为什么有些文件可以高度压缩,而有些几乎不能压缩? 这与数据的什么属性有关?熵如何量化这种属性?

Huffman编码是最优的吗? 如果是,为什么还需要算术编码等其他算法?Huffman编码的局限性在哪里?

无损压缩和有损压缩的本质区别是什么? 为什么有损压缩可以实现更高的压缩比?

如果我不知道数据的概率分布,还能实现高效压缩吗? LZ77、LZW等通用算法是如何做到这一点的?

为什么JPEG对照片效果好,但对文字截图效果差? 这与率失真理论有什么关系?

压缩算法的时间复杂度和空间复杂度如何权衡? 为什么理论上最优的算法在实践中不一定最好?

前缀码为什么重要? 为什么我们需要码字之间互不为前缀?

率失真函数 $R(D)$ 的物理意义是什么? 如何解释它的凸性和单调性?


二、核心概念详解

4.1 信源编码问题形式化

4.1.1 基本设置

信源:一个离散无记忆信源(DMS, Discrete Memoryless Source)由字母表 $\mathcal{X}$ 和概率分布 $p(x)$ 定义。信源生成独立同分布的随机序列 $X_1, X_2, \ldots$。

编码器:将信源序列 $x^n = (x_1, \ldots, x_n)$ 映射到二进制串 $b^k = (b_1, \ldots, b_k)$:

$$f: \mathcal{X}^n \to \{0,1\}^*$$

译码器:将二进制串映射回信源序列:

$$g: \{0,1\}^* \to \mathcal{X}^n$$

无损压缩:要求 $g(f(x^n)) = x^n$ 对所有 $x^n$ 成立(或至少以概率1成立)。

压缩率:平均每符号的比特数:

$$R = \frac{k}{n} \text{ bits/symbol}$$

目标:在满足无损(或有界失真)的前提下,最小化压缩率 $R$。

4.1.2 前缀码与Kraft不等式

定义:一个码是前缀码(prefix code),如果没有任何码字是另一个码字的前缀。

例子:

  • 前缀码:$\{0, 10, 110, 111\}$ ✓
  • 非前缀码:$\{0, 01, 110\}$ ✗("0"是"01"的前缀)

前缀码的优势:

  • 即时可译:接收到完整码字后可以立即译码,无需等待后续比特
  • 唯一可译:任何二进制串最多有一种译码方式

Kraft不等式:

定理(Kraft不等式):设有一个码,码字长度为 $l_1, l_2, \ldots, l_m$。存在对应的前缀码当且仅当:

$$\sum_{i=1}^m 2^{-l_i} \leq 1$$

证明思路:

  • 必要性:考虑二叉树表示,码字对应叶子节点。深度为 $l_i$ 的叶子占用 $2^{-l_i}$ 的比例。所有叶子占比之和不能超过1。
  • 充分性:如果满足Kraft不等式,可以构造对应的二叉树,从而得到前缀码。

推论:对于任意唯一可译码(不一定是前缀码),Kraft不等式仍然成立(McMillan定理)。

代码示例4-1:验证Kraft不等式

python
import numpy as np

def verify_kraft_inequality(code_lengths):
    """
    验证Kraft不等式
    
    参数:
        code_lengths: 码字长度列表
    
    返回:
        Kraft和,以及是否满足不等式
    """
    kraft_sum = sum(2**(-l) for l in code_lengths)
    return kraft_sum, kraft_sum <= 1 + 1e-10

# 示例1:Huffman码(前缀码)
huffman_lengths = [1, 2, 3, 3]  # 例如概率[0.5, 0.25, 0.125, 0.125]
kraft_sum, valid = verify_kraft_inequality(huffman_lengths)
print("示例1:Huffman码")
print(f"  码长: {huffman_lengths}")
print(f"  Kraft和: {kraft_sum:.4f}")
print(f"  满足不等式? {valid}")
print()

# 示例2:定长码
fixed_lengths = [2, 2, 2, 2]  # 4个符号,每个2比特
kraft_sum, valid = verify_kraft_inequality(fixed_lengths)
print("示例2:定长码")
print(f"  码长: {fixed_lengths}")
print(f"  Kraft和: {kraft_sum:.4f}")
print(f"  满足不等式? {valid}")
print()

# 示例3:不满足Kraft不等式的情况
invalid_lengths = [1, 1, 1]  # 3个符号,每个1比特(不可能)
kraft_sum, valid = verify_kraft_inequality(invalid_lengths)
print("示例3:无效码")
print(f"  码长: {invalid_lengths}")
print(f"  Kraft和: {kraft_sum:.4f}")
print(f"  满足不等式? {valid}")

输出:

示例1:Huffman码
  码长: [1, 2, 3, 3]
  Kraft和: 1.0000
  满足不等式? True

示例2:定长码
  码长: [2, 2, 2, 2]
  Kraft和: 1.0000
  满足不等式? True

示例3:无效码
  码长: [1, 1, 1]
  Kraft和: 1.5000
  满足不等式? False

4.2 信源编码定理

4.2.1 定理陈述

定理(香农信源编码定理):设 $X_1, X_2, \ldots$ 是i.i.d.离散随机变量,熵为 $H(X)$。

正面( achievability):对于任意 $\epsilon > 0$,存在编码方案使得压缩率 $R < H(X) + \epsilon$,且错误概率趋于零(当 $n \to \infty$)。

反面(converse):任何压缩率 $R < H(X)$ 的编码方案必然有非零的错误概率(实际上,当 $n \to \infty$ 时,错误概率趋于1)。

直观理解:

  • 熵 $H(X)$ 是无损压缩的绝对极限
  • 可以通过块编码(一次编码多个符号)任意接近这个极限
  • 试图压缩到低于熵的水平必然导致信息丢失

4.2.2 证明思路

正面证明(典型序列编码):

选择足够大的块长 $n$

枚举典型集 $A_\epsilon^{(n)}$ 中的所有序列(约 $2^{nH(X)}$ 个)

为每个典型序列分配唯一的 $n(H(X)+\epsilon)$ 比特索引

非典型序列分配特殊标记

由于典型集概率趋近于1,错误概率趋近于0

反面证明(Kraft不等式 + Jensen不等式):

假设存在压缩率 $R < H(X)$ 的无损编码

由Kraft不等式,码长满足 $\sum 2^{-l_i} \leq 1$

平均码长 $L = \sum p_i l_i$

使用Jensen不等式证明 $L \geq H(X)$,矛盾

详细证明:

设码字长度为 $l_1, \ldots, l_m$,对应概率 $p_1, \ldots, p_m$。

平均码长:

$$L = \sum_{i=1}^m p_i l_i$$

由Kraft不等式,存在 $q_i = 2^{-l_i}/C$ 构成概率分布(其中 $C = \sum 2^{-l_i} \leq 1$)。

计算KL散度:

$$\begin{align*}

0 \leq D(p||q) &= \sum_i p_i \log_2 \frac{p_i}{q_i} \\

&= \sum_i p_i \log_2 p_i - \sum_i p_i \log_2 q_i \\

&= -H(X) - \sum_i p_i \log_2 \frac{2^{-l_i}}{C} \\

&= -H(X) + \sum_i p_i l_i + \log_2 C \\

&= L - H(X) + \log_2 C

\end{align*}$$

由于 $C \leq 1$,$\log_2 C \leq 0$,因此:

$$L \geq H(X)$$

证毕。

代码示例4-2:信源编码定理数值演示

python
import numpy as np

def source_coding_theorem_demo():
    """
    演示信源编码定理
    """
    
    # 二元信源
    p = 0.8
    H = -p * np.log2(p) - (1-p) * np.log2(1-p)
    
    print("信源编码定理演示")
    print("=" * 60)
    print(f"信源: P(X=0)={p}, P(X=1)={1-p}")
    print(f"熵 H(X) = {H:.4f} bits/symbol")
    print()
    
    # 不同块长的压缩率
    print("不同编码方案的压缩率:")
    print("-" * 60)
    
    # 方案1:逐符号编码(定长)
    R_fixed = np.ceil(np.log2(2))  # 2个符号需要1比特
    print(f"逐符号定长编码: R = {R_fixed:.4f} bits/symbol")
    
    # 方案2:Huffman编码(逐符号)
    # 对于二元信源,Huffman就是逐符号
    R_huffman = 1.0
    print(f"逐符号Huffman编码: R = {R_huffman:.4f} bits/symbol")
    
    # 方案3:块Huffman编码(n=2)
    n = 2
    # 可能的双符号组合及其概率
    probs_n2 = [p**2, p*(1-p), (1-p)*p, (1-p)**2]
    probs_n2.sort(reverse=True)
    
    # Huffman码长(近似)
    # 最高概率符号:1比特
    # 次高:2比特
    # 其余:3比特
    if len(probs_n2) == 4:
        lengths_n2 = [1, 2, 3, 3]
        avg_length_n2 = sum(l * pr for l, pr in zip(lengths_n2, probs_n2))
        R_block2 = avg_length_n2 / n
        print(f"块长为2的Huffman编码: R = {R_block2:.4f} bits/symbol")
    
    # 方案4:块长为8的Huffman编码(模拟)
    n = 8
    # 典型集大小约 2^(nH)
    typical_size = 2**(n * H)
    total_size = 2**n
    # 压缩率 ≈ log2(typical_size) / n = H
    R_block8 = np.log2(typical_size) / n
    print(f"块长为8的典型集编码: R ≈ {R_block8:.4f} bits/symbol")
    
    # 方案5:算术编码(接近熵)
    R_arithmetic = H + 0.01  # 略高于熵
    print(f"算术编码(模拟): R ≈ {R_arithmetic:.4f} bits/symbol")
    
    print()
    print(f"理论极限(熵): {H:.4f} bits/symbol")
    print()
    
    # 可视化压缩率随块长的变化
    import matplotlib.pyplot as plt
    
    block_sizes = [1, 2, 4, 8, 16, 32]
    rates = []
    
    for n in block_sizes:
        # 典型集编码的压缩率
        rate = H + 1/n  # 简化模型
        rates.append(rate)
    
    plt.figure(figsize=(10, 6))
    plt.plot(block_sizes, rates, 'bo-', linewidth=2, markersize=8, label='典型集编码')
    plt.axhline(y=H, color='r', linestyle='--', linewidth=2, label=f'熵 H={H:.4f}')
    plt.xlabel('块长 n', fontsize=12)
    plt.ylabel('压缩率 R (bits/symbol)', fontsize=12)
    plt.title('压缩率随块长的变化', fontsize=14)
    plt.legend(fontsize=11)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('compression_rate_vs_blocksize.png', dpi=150)
    plt.show()

source_coding_theorem_demo()

4.3 Huffman编码

4.3.1 算法描述

Huffman编码是一种构造最优前缀码的贪心算法。

算法步骤:

将所有符号按概率从小到大排序

取出概率最小的两个符号,合并为一个新节点,概率为两者之和

将新节点放回列表中

重复步骤2-3,直到只剩一个节点

从根节点向下,左分支标记0,右分支标记1,得到每个符号的码字

最优性:Huffman编码在所有前缀码中具有最小的平均码长。

时间复杂度:使用优先队列(堆),时间复杂度为 $O(m \log m)$,其中 $m$ 是符号数。

4.3.2 实例演示

例子:符号集 $\{a,b,c,d,e\}$,概率 $\{0.25, 0.25, 0.20, 0.15, 0.15\}$

构建过程:

步骤1: 合并d(0.15)和e(0.15) → de(0.30)
步骤2: 合并c(0.20)和de(0.30) → cde(0.50)  [取最小的两个]
       实际应该是: 合并c(0.20)和某个0.25 → ...

让我们重新计算:
初始: a(0.25), b(0.25), c(0.20), d(0.15), e(0.15)
合并d,e → de(0.30)
剩余: a(0.25), b(0.25), c(0.20), de(0.30)
合并c,a → ca(0.45)  [取最小的两个: c(0.20)和a或b(0.25)]
实际: 合并c(0.20)和a(0.25) → ca(0.45)
剩余: b(0.25), de(0.30), ca(0.45)
合并b,de → bde(0.55)
剩余: ca(0.45), bde(0.55)
合并ca,bde → root(1.0)

码字(从根向下):
a: 00 (或01, 取决于左右分配)
b: 10
c: 01
d: 110
e: 111

平均码长: 0.25×2 + 0.25×2 + 0.20×2 + 0.15×3 + 0.15×3 = 2.3 bits
熵: -0.25log(0.25)×2 - 0.20log(0.20) - 0.15log(0.15)×2 ≈ 2.28 bits
冗余: 2.3 - 2.28 = 0.02 bits

代码示例4-3:Huffman编码实现

python
import heapq
from collections import Counter

class HuffmanCoder:
    """Huffman编码器"""
    
    def __init__(self, frequencies):
        """
        初始化
        
        参数:
            frequencies: 符号频率字典 {symbol: frequency}
        """
        self.frequencies = frequencies
        self.codebook = {}
        self.reverse_codebook = {}
        self._build_tree()
    
    def _build_tree(self):
        """构建Huffman树"""
        # 创建优先队列
        heap = [[freq, [symbol, ""]] for symbol, freq in self.frequencies.items()]
        heapq.heapify(heap)
        
        while len(heap) > 1:
            # 取出最小的两个
            lo = heapq.heappop(heap)
            hi = heapq.heappop(heap)
            
            # 给左子树添加'0',右子树添加'1'
            for pair in lo[1:]:
                pair[1] = '0' + pair[1]
            for pair in hi[1:]:
                pair[1] = '1' + pair[1]
            
            # 合并
            merged = [lo[0] + hi[0]] + lo[1:] + hi[1:]
            heapq.heappush(heap, merged)
        
        # 提取码字
        remaining = heapq.heappop(heap)[1:]
        self.codebook = dict(remaining)
        self.reverse_codebook = {v: k for k, v in self.codebook.items()}
    
    def encode(self, data):
        """编码数据"""
        return ''.join(self.codebook[symbol] for symbol in data)
    
    def decode(self, encoded):
        """解码数据"""
        result = []
        current_code = ""
        for bit in encoded:
            current_code += bit
            if current_code in self.reverse_codebook:
                result.append(self.reverse_codebook[current_code])
                current_code = ""
        return ''.join(result)
    
    def average_code_length(self):
        """计算平均码长"""
        total_freq = sum(self.frequencies.values())
        avg_len = sum(
            len(code) * freq 
            for symbol, code in self.codebook.items()
            for s, freq in self.frequencies.items()
            if s == symbol
        ) / total_freq
        return avg_len
    
    def entropy(self):
        """计算熵"""
        total = sum(self.frequencies.values())
        probs = [freq / total for freq in self.frequencies.values()]
        return -sum(p * np.log2(p) for p in probs if p > 0)
    
    def efficiency(self):
        """计算编码效率"""
        H = self.entropy()
        L = self.average_code_length()
        return H / L if L > 0 else 0
    
    def display_codebook(self):
        """显示码本"""
        print("Huffman码本:")
        print("-" * 40)
        for symbol, code in sorted(self.codebook.items(), key=lambda x: len(x[1])):
            freq = self.frequencies[symbol]
            print(f"  '{symbol}': {code:10s} (频率: {freq})")
        print()
        print(f"平均码长: {self.average_code_length():.4f} bits")
        print(f"熵: {self.entropy():.4f} bits")
        print(f"效率: {self.efficiency()*100:.2f}%")


# 使用示例
if __name__ == "__main__":
    import numpy as np
    
    # 英文字母频率(简化版)
    english_freq = {
        'e': 127, 't': 91, 'a': 82, 'o': 75, 'i': 70,
        'n': 67, 's': 63, 'h': 61, 'r': 60, 'd': 43,
        'l': 40, 'c': 28, 'u': 28, 'm': 24, 'w': 24
    }
    
    coder = HuffmanCoder(english_freq)
    coder.display_codebook()
    
    # 编码示例
    message = "hello world"
    encoded = coder.encode(message.lower().replace(' ', ''))
    decoded = coder.decode(encoded)
    
    print(f"原始消息: hello world")
    print(f"编码后: {encoded[:30]}...")
    print(f"解码后: {decoded}")

4.3.3 Huffman编码的局限性

尽管Huffman编码是最优前缀码,但它有以下局限:

整数比特限制:每个符号的码长必须是整数,而最优码长 $-\log_2 p(x)$ 通常是小数。这导致冗余最多为1比特/符号。

需要知道概率分布:Huffman编码要求预先知道符号的概率分布。对于未知分布的数据,需要先统计或使用自适应方法。

块编码复杂度高:虽然块Huffman编码可以更接近熵,但符号组合数指数增长,存储和计算复杂度太高。

误差传播:如果传输过程中出现比特错误,可能导致后续所有符号译码错误。

算术编码通过允许分数比特来克服第一个局限,而LZ系列算法通过在线学习数据结构来克服第二个局限。

4.4 算术编码

4.4.1 基本原理

算术编码的核心思想是将整个消息编码为一个区间内的实数,而不是为每个符号分配独立的码字。

算法流程:

初始化区间 $[low, high) = [0, 1)$

对于每个输入符号:

- 根据符号的概率划分当前区间

- 选择对应符号的子区间作为新的 $[low, high)$

最终输出区间内的任意一个数(通常选择最短的二进制表示)

解码:

从编码值开始,反向确定每个符号对应的子区间

逐步缩小区间,恢复原始符号序列

优势:

  • 可以突破整数比特限制,平均码长可以小于1比特/符号
  • 对于高度偏斜的分布特别有效
  • 天然支持自适应概率模型

劣势:

  • 实现复杂度较高
  • 需要高精度浮点运算
  • 专利问题(历史上曾是障碍,现已过期)

4.4.2 实例演示

例子:符号集 $\{a, b, c\}$,概率 $\{0.5, 0.3, 0.2\}$,编码消息 "bac"

编码过程:

初始区间: [0, 1)

处理'b' (概率范围 [0.5, 0.8)):
  新区间: [0.5, 0.8)

处理'a' (概率范围 [0, 0.5)):
  子区间宽度: 0.8 - 0.5 = 0.3
  'a'占前50%: [0.5, 0.5 + 0.3×0.5) = [0.5, 0.65)

处理'c' (概率范围 [0.8, 1.0)):
  子区间宽度: 0.65 - 0.5 = 0.15
  'c'占最后20%: [0.5 + 0.15×0.8, 0.5 + 0.15×1.0) = [0.62, 0.65)

最终区间: [0.62, 0.65)
输出: 该区间内的一个数,如 0.625 = 0.101₂

代码示例4-4:简化算术编码器

python
class ArithmeticCoder:
    """简化算术编码器(用于教学)"""
    
    def __init__(self, probabilities):
        """
        初始化
        
        参数:
            probabilities: 概率字典 {symbol: probability}
        """
        self.probs = probabilities
        self.symbols = list(probabilities.keys())
        
        # 计算累积概率
        self.cum_probs = {}
        cum = 0
        for sym in self.symbols:
            self.cum_probs[sym] = (cum, cum + probabilities[sym])
            cum += probabilities[sym]
    
    def encode(self, message):
        """
        编码消息
        
        参数:
            message: 符号列表
        
        返回:
            编码后的区间中点
        """
        low, high = 0.0, 1.0
        
        for symbol in message:
            range_width = high - low
            sym_low, sym_high = self.cum_probs[symbol]
            
            high = low + range_width * sym_high
            low = low + range_width * sym_low
        
        # 返回区间中点
        return (low + high) / 2
    
    def decode(self, value, length):
        """
        解码
        
        参数:
            value: 编码值
            length: 原始消息长度
        
        返回:
            解码后的符号列表
        """
        result = []
        low, high = 0.0, 1.0
        
        for _ in range(length):
            range_width = high - low
            
            # 找到对应的符号
            for symbol in self.symbols:
                sym_low, sym_high = self.cum_probs[symbol]
                
                if low + range_width * sym_low <= value < low + range_width * sym_high:
                    result.append(symbol)
                    
                    # 更新区间
                    high = low + range_width * sym_high
                    low = low + range_width * sym_low
                    break
        
        return result


# 使用示例
if __name__ == "__main__":
    probs = {'a': 0.5, 'b': 0.3, 'c': 0.2}
    coder = ArithmeticCoder(probs)
    
    message = ['b', 'a', 'c']
    encoded = coder.encode(message)
    decoded = coder.decode(encoded, len(message))
    
    print("算术编码演示")
    print("=" * 40)
    print(f"原始消息: {message}")
    print(f"编码值: {encoded:.6f}")
    print(f"解码消息: {decoded}")
    print(f"匹配: {message == decoded}")

4.5 LZ系列算法

4.5.1 LZ77算法

LZ77(Lempel-Ziv 1977)是一种基于滑动窗口的字典编码算法。

核心思想:用指向之前出现过的字符串的指针来替代重复出现的模式。

编码单元:三元组 $(offset, length, next\_char)$

  • $offset$:距离当前位置的回溯距离
  • $length$:匹配的长度
  • $next\_char$:匹配后的下一个字符

算法流程:

维护一个滑动窗口(已处理的历史数据)

在窗口中查找与当前位置最长匹配

输出 $(offset, length, next\_char)$

移动窗口,继续处理

优点:

  • 不需要预先知道概率分布
  • 自动适应数据的局部结构
  • 实现相对简单

缺点:

  • 搜索匹配可能较慢(可用哈希优化)
  • 对于随机数据效果差

4.5.2 LZ78算法

LZ78构建一个显式的字典,存储之前见过的字符串。

算法流程:

初始化空字典

找到当前输入与字典中最长匹配的前缀

输出(字典索引,下一个字符)

将新字符串加入字典

4.5.3 LZW算法

LZW(Lempel-Ziv-Welch)是LZ78的改进版本,由Welch提出。

改进:

  • 字典初始化包含所有单字符
  • 只输出字典索引(不需要输出下一个字符)
  • 编码器和解码器独立构建相同的字典

应用:GIF图像格式、Unix compress命令

代码示例4-5:LZW编码器

python
class LZWCodec:
    """LZW编解码器"""
    
    def __init__(self):
        pass
    
    def encode(self, data):
        """
        LZW编码
        
        参数:
            data: 输入字符串
        
        返回:
            编码后的索引列表
        """
        # 初始化字典(所有单字符)
        dictionary = {chr(i): i for i in range(256)}
        next_code = 256
        
        result = []
        current = ""
        
        for char in data:
            combined = current + char
            if combined in dictionary:
                current = combined
            else:
                # 输出当前串的编码
                result.append(dictionary[current])
                # 将新串加入字典
                dictionary[combined] = next_code
                next_code += 1
                current = char
        
        # 输出最后一个串
        if current:
            result.append(dictionary[current])
        
        return result
    
    def decode(self, codes):
        """
        LZW解码
        
        参数:
            codes: 编码后的索引列表
        
        返回:
            解码后的字符串
        """
        # 初始化字典
        dictionary = {i: chr(i) for i in range(256)}
        next_code = 256
        
        result = []
        prev_string = chr(codes[0])
        result.append(prev_string)
        
        for code in codes[1:]:
            if code in dictionary:
                current_string = dictionary[code]
            elif code == next_code:
                # 特殊情况
                current_string = prev_string + prev_string[0]
            else:
                raise ValueError(f"Invalid code: {code}")
            
            result.append(current_string)
            
            # 更新字典
            dictionary[next_code] = prev_string + current_string[0]
            next_code += 1
            
            prev_string = current_string
        
        return ''.join(result)


# 使用示例
if __name__ == "__main__":
    codec = LZWCodec()
    
    original = "TOBEORNOTTOBEORTOBEORNOT"
    encoded = codec.encode(original)
    decoded = codec.decode(encoded)
    
    print("LZW编码演示")
    print("=" * 40)
    print(f"原始数据: {original}")
    print(f"编码后: {encoded}")
    print(f"解码后: {decoded}")
    print(f"匹配: {original == decoded}")
    print(f"原始长度: {len(original)} 字符")
    print(f"编码长度: {len(encoded)} 索引")

4.6 率失真理论简介

4.6.1 问题设定

有损压缩:允许一定的失真,以换取更高的压缩率。

失真度量:常用的有:

  • 汉明失真:$d(x, \hat{x}) = \mathbb{1}\{x \neq \hat{x}\}$
  • 平方误差:$d(x, \hat{x}) = (x - \hat{x})^2$

率失真函数:给定最大允许平均失真 $D$,最小可达压缩率 $R(D)$ 定义为:

$$R(D) = \min_{p(\hat{x}|x): \mathbb{E}[d(X,\hat{X})] \leq D} I(X; \hat{X})$$

4.6.2 率失真函数的性质

单调递减:$R(D)$ 随 $D$ 增加而减小

凸性:$R(D)$ 是凸函数

边界值:

- $R(0) = H(X)$(无损压缩)

- $R(D_{max}) = 0$,其中 $D_{max}$ 是最大可容忍失真

4.6.3 例子:伯努利信源的率失真函数

对于伯努利$(p)$ 信源,汉明失真:

$$R(D) = \begin{cases} H_b(p) - H_b(D) & 0 \leq D \leq \min(p, 1-p) \\ 0 & D > \min(p, 1-p) \end{cases}$$

其中 $H_b(p) = -p\log_2 p - (1-p)\log_2(1-p)$ 是二元熵函数。

代码示例4-6:率失真函数可视化

python
import numpy as np
import matplotlib.pyplot as plt

def binary_entropy(p):
    """二元熵函数"""
    if p == 0 or p == 1:
        return 0
    return -p * np.log2(p) - (1-p) * np.log2(1-p)

def rate_distortion_binary(p, D_values):
    """
    伯努利(p)信源的率失真函数(汉明失真)
    """
    R_values = []
    D_max = min(p, 1-p)
    
    for D in D_values:
        if D <= D_max:
            R = binary_entropy(p) - binary_entropy(D)
        else:
            R = 0
        R_values.append(max(0, R))  # 确保非负
    
    return R_values

# 可视化
p = 0.3
D_values = np.linspace(0, 0.5, 100)
R_values = rate_distortion_binary(p, D_values)

plt.figure(figsize=(10, 6))
plt.plot(D_values, R_values, 'b-', linewidth=2.5)
plt.xlabel('失真 D', fontsize=12)
plt.ylabel('率 R(D) (bits/symbol)', fontsize=12)
plt.title(f'率失真函数 (伯努利({p})信源, 汉明失真)', fontsize=14)
plt.grid(True, alpha=0.3)
plt.axhline(y=binary_entropy(p), color='r', linestyle='--', alpha=0.5, 
            label=f'H(X) = {binary_entropy(p):.4f}')
plt.axvline(x=min(p, 1-p), color='g', linestyle='--', alpha=0.5, 
            label=f'D_max = {min(p, 1-p):.4f}')
plt.legend(fontsize=11)
plt.tight_layout()
plt.savefig('rate_distortion.png', dpi=150)
plt.show()

print(f"伯努利({p})信源的率失真函数")
print(f"H(X) = {binary_entropy(p):.4f}")
print(f"D_max = {min(p, 1-p):.4f}")

三、程序员的视角

4.1 在实际压缩标准中的应用

DEFLATE(ZIP、GZIP、PNG):

  • 结合LZ77和Huffman编码
  • 先用LZ77找到重复模式,再用Huffman编码偏移量和长度

JPEG:

  • 离散余弦变换(DCT)→ 量化 → Huffman/算术编码
  • 率失真理论指导量化步长的选择

MP3/AAC:

  • 心理声学模型 → 频域变换 → 量化 → 熵编码
  • 利用人耳感知特性,在有损压缩中保持音质

4.2 选择压缩算法的建议

场景推荐算法原因
通用文本DEFLATE (gzip)成熟、快速、兼容性好
高压缩比LZMA (xz)更好的压缩率,适合归档
实时通信LZ4极快的解压速度
图像(无损)PNG广泛支持
图像(有损)JPEG/WebPWebP提供更好的压缩率
视频H.264/H.265/AV1专用硬件加速

4.3 常见陷阱

压缩加密数据:加密后的数据接近随机,几乎不可压缩。应先压缩再加密。

小文件的开销:压缩头信息和字典可能使压缩后的文件更大。

内存使用:某些算法(如LZMA)需要大量内存进行压缩/解压。

专利问题:某些算法(如LZW历史上)可能有专利限制。


四、代码实战

4.1 完整压缩性能比较工具

python
import zlib
import bz2
import lzma
import time
import os

class CompressionBenchmark:
    """压缩性能基准测试"""
    
    def __init__(self, data):
        self.data = data if isinstance(data, bytes) else data.encode()
        self.original_size = len(self.data)
    
    def benchmark(self, name, compress_func, decompress_func):
        """测试单个算法"""
        # 压缩
        start = time.time()
        compressed = compress_func(self.data)
        compress_time = time.time() - start
        
        # 解压
        start = time.time()
        decompressed = decompress_func(compressed)
        decompress_time = time.time() - start
        
        # 验证
        assert decompressed == self.data, f"{name}: 解压结果不匹配!"
        
        compressed_size = len(compressed)
        ratio = compressed_size / self.original_size
        
        return {
            'name': name,
            'original_size': self.original_size,
            'compressed_size': compressed_size,
            'ratio': ratio,
            'compress_time': compress_time,
            'decompress_time': decompress_time
        }
    
    def run_all(self):
        """运行所有算法测试"""
        results = []
        
        # zlib (DEFLATE)
        results.append(self.benchmark(
            'zlib (DEFLATE)',
            lambda d: zlib.compress(d),
            lambda d: zlib.decompress(d)
        ))
        
        # bz2
        results.append(self.benchmark(
            'bz2',
            lambda d: bz2.compress(d),
            lambda d: bz2.decompress(d)
        ))
        
        # lzma
        results.append(self.benchmark(
            'lzma',
            lambda d: lzma.compress(d),
            lambda d: lzma.decompress(d)
        ))
        
        # 打印结果
        print("压缩性能比较")
        print("=" * 70)
        print(f"{'算法':<20} {'压缩率':>10} {'压缩时间':>12} {'解压时间':>12}")
        print("-" * 70)
        
        for r in results:
            print(f"{r['name']:<20} {r['ratio']:>10.4f} {r['compress_time']:>12.6f}s {r['decompress_time']:>12.6f}s")
        
        return results


# 使用示例
if __name__ == "__main__":
    # 生成测试数据(英文文本)
    test_text = """
    Information theory is a mathematical framework for quantifying information.
    It was founded by Claude Shannon in 1948. The central concept is entropy,
    which measures the uncertainty or randomness of a random variable.
    Data compression is one of the most successful applications of information theory.
    From ZIP files to JPEG images, from MP3 audio to H.264 video, compression 
    technology is everywhere. The source coding theorem gives the theoretical limit 
    of compression, guiding the development of compression algorithms for over 
    half a century.
    """ * 100  # 重复以增加大小
    
    benchmark = CompressionBenchmark(test_text)
    benchmark.run_all()

4.2 自定义Huffman压缩工具

python
import pickle
import struct

class HuffmanFileCompressor:
    """基于Huffman的文件压缩器"""
    
    def __init__(self):
        self.coder = None
    
    def compress(self, input_path, output_path):
        """压缩文件"""
        # 读取文件
        with open(input_path, 'rb') as f:
            data = f.read()
        
        # 统计频率
        freq = {}
        for byte in data:
            freq[byte] = freq.get(byte, 0) + 1
        
        # 构建Huffman编码器
        from collections import Counter
        self.coder = HuffmanCoder(freq)
        
        # 编码
        encoded_bits = self.coder.encode(data)
        
        # 保存:[码本大小][码本][编码数据位数][编码数据]
        codebook_bytes = pickle.dumps(self.coder.codebook)
        encoded_bytes = self._bits_to_bytes(encoded_bits)
        
        with open(output_path, 'wb') as f:
            # 码本大小(4字节)
            f.write(struct.pack('>I', len(codebook_bytes)))
            # 码本
            f.write(codebook_bytes)
            # 编码数据位数(4字节)
            f.write(struct.pack('>I', len(encoded_bits)))
            # 编码数据
            f.write(encoded_bytes)
        
        original_size = len(data)
        compressed_size = os.path.getsize(output_path)
        print(f"压缩: {input_path} -> {output_path}")
        print(f"  原始大小: {original_size} 字节")
        print(f"  压缩大小: {compressed_size} 字节")
        print(f"  压缩率: {compressed_size/original_size*100:.2f}%")
    
    def decompress(self, input_path, output_path):
        """解压文件"""
        with open(input_path, 'rb') as f:
            # 读取码本大小
            codebook_size = struct.unpack('>I', f.read(4))[0]
            # 读取码本
            codebook_bytes = f.read(codebook_size)
            codebook = pickle.loads(codebook_bytes)
            # 读取编码数据位数
            num_bits = struct.unpack('>I', f.read(4))[0]
            # 读取编码数据
            encoded_bytes = f.read()
        
        # 重建编码器
        self.coder = type('obj', (object,), {
            'codebook': codebook,
            'reverse_codebook': {v: k for k, v in codebook.items()}
        })()
        
        # 解码
        encoded_bits = self._bytes_to_bits(encoded_bytes, num_bits)
        decoded = self._decode_bits(encoded_bits)
        
        with open(output_path, 'wb') as f:
            f.write(decoded)
        
        print(f"解压: {input_path} -> {output_path}")
    
    def _bits_to_bytes(self, bits):
        """比特列表转字节"""
        # 填充到8的倍数
        padding = (8 - len(bits) % 8) % 8
        bits = bits + '0' * padding
        
        result = bytearray()
        for i in range(0, len(bits), 8):
            byte = int(bits[i:i+8], 2)
            result.append(byte)
        
        return bytes(result)
    
    def _bytes_to_bits(self, data, num_bits):
        """字节转比特列表"""
        bits = ''
        for byte in data:
            bits += format(byte, '08b')
        return bits[:num_bits]
    
    def _decode_bits(self, bits):
        """解码比特流"""
        result = bytearray()
        current = ''
        for bit in bits:
            current += bit
            if current in self.coder.reverse_codebook:
                result.append(self.coder.reverse_codebook[current])
                current = ''
        return bytes(result)

五、深入思考

5.1 历史背景

Huffman编码由David Huffman在1952年作为MIT课程作业发明。有趣的是,他的教授Robert Fano之前已经提出了类似但不完全最优的方法。Huffman证明了其算法的最优性,这篇课程作业成为了经典论文。

LZ算法由Abraham Lempel和Jacob Ziv在1977-1978年发表,他们当时在以色列理工学院工作。LZW由Terry Welch在1984年改进。这些算法的革命性在于它们是"通用的"——不需要预先知道数据的统计特性。

算术编码的历史更复杂,多位研究者独立提出了类似想法。Peter Elias发明了早期的算术编码,但并未发表。IBM的Rissanen和Pasco做出了重要贡献。由于专利问题,算术编码在很长一段时间内未能广泛应用。

5.2 前沿方向

神经压缩:使用深度学习进行端到端的图像/视频压缩。神经网络学习数据的隐表示,然后对隐表示进行熵编码。这种方法在传统指标(PSNR)上可能不如传统方法,但在感知质量上表现更好。

学习辅助压缩:使用机器学习预测数据的局部统计特性,然后用自适应熵编码。这在屏幕内容压缩、基因组数据压缩等领域取得了进展。

语义压缩:不仅压缩比特,还压缩"意义"。允许更大的失真,只要语义信息保留。这对AI应用(如自动驾驶)可能有特殊价值。

5.3 延伸阅读

Cover & Thomas《信息论基础》第5章、第10章

Salomon《Data Compression: The Complete Reference》

MacKay《Information Theory, Inference, and Learning Algorithms》第4章


六、实验笔记

实验1:Huffman编码性能分析

目标:分析Huffman编码在不同分布下的压缩性能。

步骤:

生成具有不同熵值的随机数据(改变符号概率分布)

对每种数据进行Huffman编码

记录压缩率、熵、平均码长

绘制压缩率 vs 熵的关系图

预期结果:

  • 压缩率随熵增加而增加
  • 平均码长始终大于等于熵
  • 冗余(平均码长 - 熵)小于1比特/符号

实验2:LZ77滑动窗口大小影响

目标:研究LZ77算法中滑动窗口大小对压缩率的影响。

步骤:

实现可变窗口大小的LZ77编码器

使用不同类型的测试数据(文本、二进制、混合)

测试窗口大小从256字节到64KB的变化

记录压缩率和压缩/解压时间

思考题:

  • 窗口大小与压缩率的关系是单调的吗?
  • 不同类型的数据对窗口大小的敏感度有何不同?
  • 是否存在"最佳"窗口大小?

七、本章小结

本章系统介绍了数据压缩的理论和实践。

信源编码定理确立了熵作为无损压缩极限的地位。任何无损压缩算法的平均码长都不能低于熵,而通过块编码可以任意接近这个极限。

Huffman编码是最优前缀码,实现简单且效率高。但由于整数比特限制,它不能完全达到熵的极限。算术编码通过分数比特表示突破了这一限制,但实现更复杂。

LZ系列算法是通用压缩的代表,不需要预先知道数据统计特性。它们通过在线学习数据的重复结构来实现压缩,在实践中表现出色。DEFLATE(LZ77 + Huffman)是最成功的组合,被广泛应用于ZIP、GZIP、PNG等格式。

率失真理论为有损压缩提供了理论基础。率失真函数 $R(D)$ 给出了在给定失真约束下的最小压缩率。JPEG、MP3等有损压缩标准都隐含地利用了率失真权衡。

从程序员的角度看,理解这些理论有助于选择合适的压缩算法、理解压缩参数的影响,以及在必要时设计定制的压缩方案。虽然大多数应用可以直接使用现成的压缩库,但深入理解原理能够帮助我们做出更明智的技术决策。


八、习题

习题4.1(Huffman编码):给定符号集 $\{a,b,c,d,e\}$ 和概率 $\{0.3, 0.25, 0.2, 0.15, 0.1\}$:

  • (a) 构建Huffman树并给出码字
  • (b) 计算平均码长
  • (c) 计算熵和编码效率

习题4.2(Kraft不等式):判断以下码长集合是否可以构成前缀码:

  • (a) $\{1, 2, 2, 3\}$
  • (b) $\{1, 1, 2, 2\}$
  • (c) $\{2, 2, 2, 2, 2\}$

习题4.3(算术编码):使用概率 $\{a:0.5, b:0.3, c:0.2\}$ 对消息 "abc" 进行算术编码,给出最终区间。

习题4.4(LZW编码):对字符串 "ABABABAAB" 进行LZW编码,列出每一步的字典更新和输出。

习题4.5(率失真函数):对于均匀四元信源($p=[0.25,0.25,0.25,0.25]$)和汉明失真,推导 $R(D)$ 的表达式。

习题4.6(编程题):实现一个简易的游程编码(RLE)压缩器,测试它在二值图像数据上的压缩效果。

习题4.7(挑战题):证明对于任何前缀码,平均码长 $L$ 满足 $H(X) \leq L < H(X) + 1$。

习题4.8(应用题):假设你要压缩一个1MB的日志文件,其中90%的内容是重复的行。你会选择哪种压缩算法?为什么?

习题4.9(分析题):比较Huffman编码和算术编码在以下场景的表现:

  • (a) 高度偏斜的分布(如 $p=[0.99, 0.01]$)
  • (b) 接近均匀的分布
  • (c) 大量符号(如256个字节值)

习题4.10(开放性问题):研究现代图像压缩标准(如WebP、AVIF)与传统JPEG相比的优势,撰写一篇500字左右的分析报告,重点讨论它们如何利用信息论原理实现更好的压缩性能。

4/9数据压缩
0%