森林里的蚂蚁要搬家了。蚁后给了工头一句话:“把所有家当装进尽量少的箱子,但一件也不能丢。”工头头疼地看着仓库:三百片完全相同的树叶散落各处,几十颗同样大小的松果躺在不同的角落。他先把相同的树叶叠成一摞——箱子立刻空出一半。可当他向蚁后汇报“还能不能更小”时,老数学家蚂蚁说话了:“不管你的打包技术多厉害,这些家当有一个'理论最小体积',这个数值由家当本身的构成方式决定,谁也突破不了。”这个“理论最小体积”,就是熵。它回答了压缩界的终极问题:数据最多能压到多小?压缩的本质是消除冗余,而信息论给出了这个极限的数学表达式。在这篇文章里,我们会顺着蚂蚁搬家队的打包故事,一步步看清熵是什么、信源编码定理如何设下天花板、霍夫曼编码和算术编码这两代“打包术”怎么逼近极限,以及 ZIP 和机器学习里的表征学习,为什么本质上是同一个“整理行李”的动作。
这一节知识能让 AI 从业者理解模型压缩、特征降维和存储优化的理论基础——知道“极限在哪”,才不会在调参时白费力气。
目录
可视化演示
展示香农熵、信源编码与数据压缩的核心原理,通过符号概率分布和编码树直观理解信息压缩的理论基础。
本节配有交互动效(进入视口后自动加载,约 3KB)。
信息论与数据压缩 香农熵:H(X)=-∑p(x)log p(x)衡量信息量 信源编码:将符号映射为变长码字实现压缩 压缩率:编码后比特数与原始比特数之比 H(X) ≤ L_avg < H(X)+1 香农定理保证平均码长接近熵
压缩的本质:整理家当的第一步
蚂蚁工头第一次整理行李,发现仓库里散落着三百片完全相同的树叶。如果他给每片树叶单独装箱,那就白白浪费了 299 份“重复的劳动”。聪明的做法是把它们叠成一摞,用一个标签写上“三百片树叶”。数据压缩做的事情一模一样:找到这种重复的摆放方式,把它合并掉——在信息论里,这种重复和多余被统称为冗余。压缩率就是衡量整理效果的那把尺子:整理后的箱数相比原来少了几倍。
从蚂蚁的仓库回到计算机的世界,任何可以被压缩的文件里都藏着冗余:文本中反复出现的“the“是冗余,图片中大块相同的颜色是冗余,音频里没有声息的停顿也是冗余。无损压缩做到”箱子里一件东西都没少,解压后和原来一字不差”;有损压缩则主动丢掉感知上最不重要的东西,就像搬家时扔掉生了虫的旧木凳。两者共享同一把尺子——压缩率。
整理的第一步永远是“找重复、合并同类”。没有这一步,后面所有编码技巧都无从谈起;而一旦完成了这一步,我们就会追问一个更深刻的问题:一件家当究竟“多重”才算正常?这就是下一节的信息量。
- 原始大小压缩前文件占据的存储空间,对应蚂蚁搬家前占用的仓库体积。
- 压缩后大小压缩算法输出的实际规模,对应整理打包后的箱子体积。
- 比值含义两者相除得到倍数。比率越高,说明冗余消除得越彻底。
信息量与熵:给每件家当称“稀有度”
蚂蚁情报员负责记录仓库里每件物品的价值。他发现,判断一件物品“值多少信息”,不看它本身有多大,而看它出现的概率有多低。每天都能见到的松果,看到它一点都不意外,携带的信息量几乎为零;而一块几十年一遇的琥珀,一旦出现就让所有蚂蚁围过来——它的信息量极高。情报员把这个“意外程度”量化成一个公式:概率越低,意外程度越高,信息量越大。
单件物品的意外程度只是起点。蚁后真正关心的,是整间仓库平均每件物品的意外程度——如果把仓库里所有符号的“信息量”按概率加权平均,就得到熵。熵的单位同样是比特,它表示“平均每个符号最少需要多少比特才能无损表达”。仓库里的物品种类越均匀、越没有规律,熵就越大,打包也就越困难。
熵的大小直接决定了搬家难度:全是树叶的仓库熵接近 0,一个标签就能打包;而什么都有、比例平均的仓库熵就大,必须为每种物品单独准备编码。蚂蚁们现在有了“家当混乱度”的尺子,下一步要问:最好的打包师到底能做到多小?
- 求和遍历$\sum_{x \in S}$ 表示遍写信源 $S$ 里的每一个符号。
- 权重相乘$p(x)\log_2 p(x)$ 把每个符号的意外程度乘以它出现的权重。
- 整体取负每一项都是负数,对所有项求和后取负,得到非负的熵值。
- 熵的含义$H(S)$ 是平均每个符号的信息比特数,也是任何无损压缩无法突破的“理论体积”。
信源编码定理:打包师的天花板
蚁后听说有“理论体积”这回事后不服气,请来了全森林最聪明的打包师:“你是不是箱子不够小,才说什么极限?”打包师摇头:“任何无损打包,平均每件物品占用的标签长度都一定不小于熵。这不是手艺问题,是数学上的能量守恒。”这句“打包界的能量守恒”,就是信息论里最著名的信源编码定理,也叫香农第一定理。
定理说的是两件事:第一,任何无失真的编码方案,平均码长 $L$ 都不可能低于信源熵 $H(S)$;第二,只要算法足够聪明,就总能把平均码长压到比熵多不到 1 比特的范围内。换句话说,熵是地板,而“比地板高 1 比特”是聪明打包师能摸到的天花板。
这条定理让搬家公司从此有了预算表:先算熵,再定 KPI。ZIP 压完的文件再大,也不会小过熵给出的极限;如果有一天你发现某个算法把数据压得比熵还小,那只有两种可能——要么概率模型估错了,要么压缩其实是有损的。理解了这个天花板,我们才能欣赏接下来两代打包术的各自高明之处。
- 平均码长$L$ 表示平均每个符号占用的比特数,是打包师真实达到的成绩。
- 熵下限$H(S)$ 是理论最小平均码长,违反它就意味着丢失信息。
- 逼近上界存在一种构造让 $L$ 比 $H(S)$ 大不到 1 比特,这是“够聪明”的保证。
霍夫曼编码:贪心打包法
工头听完定理,决定亲手设计一套打包方案。他的思路很朴素:把仓库里最稀有的两堆物品先合并成一个“大柜子”,给这个柜子统一贴一张标签,下次再遇到它们就只要进柜子内部找;然后重复这个动作,不断把当前最稀有的两堆合并。从下往上看,所有物品被组织成一棵“行李树”,每件物品的标签就是它从树根一路走向自己的那串 0 和 1。这就是霍夫曼编码。
这套方法妙在两点:其一,它是贪心的——每次都只处理当前最小的两个概率,却能得到全局最优;其二,它天然满足前缀码性质——没有任何一个码字是另一个码字的前缀,所以解码时不需要分隔符,扫一遍 0/1 串就能无歧义地还原。高频物品被放在树的高处,路径短;稀有物品被埋在树的深处,路径长,平均码长因此被压到接近熵。
蚂蚁们按这套“行李树”打包后,箱子数量果然大幅减少。但细心的情报员发现一个遗憾:当符号概率不是 2 的整数次幂时,比如 0.15、0.08 这种“不整”的概率,每个符号至少要占用 1 个整数比特位,总会浪费一点点空间。有没有办法连这一点点也省下来?下一节,工头决定不再给每件物品单独贴标签,而把整堆行李看成一个整体来编码。
- 符号概率$p_i$ 是第 $i$ 个符号出现的概率,它决定物品在树上位置的高低。
- 码字长度$l_i$ 是第 $i$ 个符号分到的二进制路径长度。
- 加权平均把每个符号的码长按概率加权求和,得到真实的平均码长。
- 区间保证霍夫曼的平均码长一定落在 $[H(S), H(S)+1)$ 之间,摸到了定理给的上限。
算术编码:一根刻度尺装下全仓库
霍夫曼编码就像给每件物品单独贴标签,标签的长度必须是整数个比特。而工头在某个深夜想到了一个更疯狂的主意:干脆不给单件物品贴标签,把整堆行李当作一个整体,在一条从 0 到 1 的刻度尺上定位它。他规定:读入第一件物品时,按它的概率把 [0,1) 切成几段,选中属于它的那一段;读入第二件物品时,再在刚刚选中段里按概率切一刀。物品一件件进来,区间一段段缩小,最终整个序列就对应刻度尺上一段极小的区间。
解码时只需要知道这个区间里的任意一个小数,就能反向一步一步还原出整串物品。因为输出的是“整个序列在尺子上的位置”,而不是每个符号各自的整数码长,算术编码可以为很小概率的符号也分配“比 1 比特更少”的平均空间,因此它比霍夫曼更贴近熵极限——在上面的动效里,ARI 那根柱子几乎压到了红线。
蚂蚁们用刻度尺把整仓库行李“折叠”成一个点,理论上终于贴近了熵线。但工头很快发现,这套方法虽然精妙,却要维护一张巨大的概率表。回到实际工程,还有没有更简单粗暴、又能处理海量数据的招?有——ZIP 靠的不是精雕细刻,而是“重复的绝不重复打包”。
- 初始区间第一个符号到来前,可用区间宽度为整个刻度尺,即 $W_0 = 1$。
- 逐次缩窄每读入一个符号,区间宽度乘上它的概率,区间越缩越小。
- 总长度换算把所有负对数相加,得到定位最终区间所需的比特数。
- 逼近熵值这个长度除以符号数 $N$,就是平均码长,它约等于熵,从而突破整数比特限制。
ZIP与LZ77:金牌打包队的组合拳
真到了搬家那天,工头发现一个比“符号频率”更明显的浪费:仓库里有一百片完全相同的树叶排成一排。如果一片片编码,就算每个标签只有 1 比特也要花 100 比特。工头灵机一动,在货单上写了一行字:“从第 3 排开始,连续 100 片树叶,和前面第 50 排的完全一样。”这一行字就取代了 100 次编码。这就是 LZ77 的思想:用一块滑动窗口记住最近见过的内容,一旦发现重复串,就输出一个“距离 + 长度”对,告诉解码器“去前面哪个位置复制多长一段”。
ZIP 实际使用的 DEFLATE 算法,正是 LZ77 和霍夫曼编码的组合拳:先用滑动窗口把重复子串替换成短小的 (距离,长度) 记号,再用霍夫曼编码压缩这些记号本身。解码时滑动窗口不断回放历史,得到的数据与原始文件逐字节一致——所以 ZIP 是标准的无损压缩。文件里重复越多,压缩率越惊人,这也解释了为什么代码文本能压 5 到 10 倍,而已经高度随机的加密数据几乎压不动。
ZIP 的成功证明:在工程世界里,与其费尽心思给每个符号贴最优标签,不如先把明摆着的重复清掉。而信息论的眼光还可以走得更远——当压缩的对象从文本变成神经网络的表征时,蚂蚁整理的就不再是仓库,而是数据本身的“气质”。
- 距离$d$ 是重复子串距离当前位置的偏移量,代表“去前面第几排找”。
- 长度$l$ 是匹配到的连续重复长度,代表“复制多少片树叶”。
- 左边开销$\log_2 d + \log_2 l$ 是记一次位置和长度大约需要的比特数。
- 右边对比$8l$ 是把 $l$ 个字符按每字符 8 比特原样输出的成本,$l$ 越大优势越明显。
压缩与表征学习:让 AI 学会拎重点
如果蚁后想让远方的蚂蚁朋友快速了解“我们仓库的整体气质”,她不会念完整张清单,而是说:“东西很多,树叶为主,偏绿色,整体很轻。”这几个关键词就是仓库的表征——一种高度压缩后的抽象。机器学习中的表征学习做的是同一件事:把高维输入数据压成一个低维向量,让后续任务只依赖这个“压缩包”。
信息论为表征学习提供了一把精确的手术刀——信息瓶颈。它要求模型学到的表征 $Z$ 与原始输入 $X$ 的互信息尽量小(压得越狠越好),同时保证 $Z$ 与任务标签 $Y$ 的互信息足够大(有用的信息不能丢)。这完美对应了有损压缩的质量权衡:把文件压得越小越好,但不能让解压后失真到无法使用。自编码器、量化模型、embedding 降维,本质上都在实践这条法则。
蚂蚁用几个词概括仓库,AI 用几个比特抓住数据的本质。从熵到信源编码定理,从霍夫曼到算术编码,再到 ZIP 和表征学习,压缩从来不只是“变小”的技术,而是一套关于“什么值得保留、什么可以舍弃”的数学哲学。理解了熵,你就同时理解了文件压缩的极限和深度学习表征的边界。
- 输入与表征$X$ 是原始数据,$Z$ 是模型学到的压缩表征。
- 压缩目标$I(X;Z)$ 是表征和输入的互信息,最小化它意味着丢掉尽可能多的冗余。
- 保留约束$I(Z;Y)$ 是表征与标签的互信息,要求它不低于阈值 $I_c$,保证任务可用。
- 整体类比这个目标函数就是数学化的“越省越好,但关键信息必须留下”。
AI 应用场景
日志与冷数据存储优化
阶段:数据预处理与存储
解决的问题:服务器日志里 90% 内容是重复的模板和固定前缀,直接落盘既浪费存储又拖慢检索。传统方案是买更大的磁盘,成本高且治标不治本。利用 LZ77 结合霍夫曼的 DEFLATE 压缩,可在写入前把日志流压掉 80%–90%,读取时再无损还原。
具体案例:某公司的 ELK 日志管道每天产生 20 TB 原始日志,启用压缩后实际落盘只有 3 TB。用户查询时由后端透明解压,业务侧完全无感,一年省下数百万存储费用。
大模型量化与权重压缩
阶段:模型部署与推理优化
解决的问题:7B 参数模型以 FP32 存储需要 28 GB 显存,单卡几乎跑不动。模型量化本质是一种有损压缩:把连续的 32 位浮点权重量化到 8 位整数,用熵的视角评估信息损失,在保持精度损失小于 1% 的前提下把模型压到约 1/4 大小。
具体案例:推理团队用 GPTQ 方法把 7B 模型权重从 FP16 量化到 INT4,单卡即可加载,推理吞吐提升 3 倍,评测分数几乎不掉。这里的“率失真权衡”正是压缩理论在 ML 部署里的直系后代。
Embedding 向量降维与检索
阶段:表征学习与向量检索
解决的问题:推荐系统要把数亿用户的交互行为编码成 embedding 向量用于召回,768 维向量直接存储需要几十 TB。借鉴信息瓶颈思想,用一个带瓶颈层的自编码器把 768 维压到 64 维,同时用对比学习保证压缩后向量仍保留与点击行为的相关性。
具体案例:某短视频推荐团队将用户向量从 768 维压到 64 维后,向量检索内存减少 8 倍,召回率只下降 0.3 个百分点,且线上延迟大幅下降。压缩的不只是文件,还有模型对世界的理解。
Python 演示
import heapqfrom collections import Counter class HuffmanNode: def __init__(self, char, freq): self.char = char self.freq = freq self.left = None self.right = None def __lt__(self, other): return self.freq < other.freq def build_tree(text): freq = Counter(text) heap = [HuffmanNode(ch, f) for ch, f in freq.items()] heapq.heapify(heap) while len(heap) > 1: a = heapq.heappop(heap) b = heapq.heappop(heap) parent = HuffmanNode(None, a.freq + b.freq) parent.left = a parent.right = b heapq.heappush(heap, parent) return heap[0] def build_codes(node, prefix="", codes=None): if codes is None: codes = {} if node.char is not None: codes[node.char] = prefix else: build_codes(node.left, prefix + "0", codes) build_codes(node.right, prefix + "1", codes) return codes def encode(text, codes): return "".join(codes[ch] for ch in text) def decode(bits, root): res = [] node = root for b in bits: node = node.left if b == "0" else node.right if node.char is not None: res.append(node.char) node = root return "".join(res) if __name__ == "__main__": text = "hello huffman compression" root = build_tree(text) codes = build_codes(root) bits = encode(text, codes) original_bits = len(text) * 8 compressed_bits = len(bits) print("code table:", codes) print("original bits:", original_bits) print("compressed bits:", compressed_bits) print("compression ratio: %.2f%%" % (100 * compressed_bits / original_bits)) print("lossless roundtrip:", decode(bits, root) == text)
heap = [HuffmanNode(ch, f) for ch, f in freq.items()]把每个字符包装成一个节点,放进最小堆,相当于蚂蚁把每种物品单独成一堆。while len(heap) > 1:反复取出最小两堆合并,对应“先处理最稀有的两件家当”。parent = HuffmanNode(None, a.freq + b.freq)合成一个空字符的内部节点,它的频率是两个子节点之和。parent.left = a` 与 `parent.right = b把两个子节点挂到新节点上,行李树逐渐成形。codes[node.char] = prefix走到叶子节点时,把从根到叶的 0/1 路径记为码字。def encode(text, codes)查码表把每个字符替换成比特串,完成压缩。original_bits = len(text) * 8假设一个字符原本占 8 比特,这是压缩前的成本。print("lossless roundtrip:", decode(bits, root) == text)解码后再比对原文,验证无损。
常见误区
误区我觉得数据压缩就是不断优化算法,只要算法够强,文件总能压得更小,不存在什么极限。
正解香农的信源编码定理明确指出,平均码长不可能低于信息熵。熵由数据本身的概率分布决定,与算法无关。如果把一个完美压缩过的文件继续压小,说明原来的概率模型没建准,或者压缩过程偷偷丢了信息。搬家公司再厉害,也不能让箱子容积低于家当的“理论体积”。
为何常见:生活中我们总能看到“还有更新压缩算法”的新闻,误以为压缩没有边界,其实这些新闻压的都是冗余尚未榨干的旧算法。
误区霍夫曼编码是最优编码,既然是“最优”,那它的平均码长就应该刚好等于熵。
正解霍夫曼是“最优前缀码”,但最优前缀码的平均码长只能保证落在 $[H, H+1)$ 之间。只有当所有符号概率恰好都是 2 的负整数次幂(如 0.5、0.25、0.125)时,它才能精确达到熵。对任意不规整的概率分布,每个符号至少一比特的整数限制会让它高出熵一点点,算术编码就是为消除这点冗余而生的。
为何常见:“最优”两个字太有迷惑性,让人忽略了它是在“整数比特码字”这个约束下的最优。
误区ZIP 能把文件压小这么多,肯定是把文件里那些“没用的内容”偷偷去掉了,解压出来应该和原文件有差别吧?
正解ZIP 属于无损压缩,解压后与原始文件逐字节一致。它靠的是 LZ77 滑动窗口发现重复子串,再用霍夫曼编码压缩这些记号。它消除的是“冗余”,不是“信息”。你可以用代码里的 roundtrip 验证:解码结果与原文完全相同。所谓“删掉内容”,是有损压缩 JPEG 或 MP3 才做的事。
为何常见:人们常把“压缩”和“有损压缩”混为一谈,因为日常接触的图片、音乐大多是 JPEG 和 MP3,形成了错误印象。
误区熵代表信息量,那熵越大的文件信息量越多,应该更有价值、更难压缩是好事。
正解熵衡量的是“不确定性”和“冗余的稀缺程度”。熵越大,说明数据越接近随机,压缩空间越小。而很多高价值数据恰恰熵很小:一份全篇重复相同格式的财务报表熵很低,但它记录的是关键业务事实。压缩视角下,熵大=难压=比特贵,不代表价值高。
为何常见:“信息量”这个日常词汇自带正面色彩,让人误以为量越大越好,忽略了它在压缩语境下是“成本”的度量。
面试题
Q1. 一个发生概率为 0.5 的事件,其信息量是多少比特?
- A.2 比特
- B.1 比特← 正确答案
- C.0.5 比特
- D.0 比特
答案:B。信息量定义为 $I(x)=-\log_2 p(x)$。当 $p=0.5$ 时,$-\log_2 0.5=1$ 比特。这里把“意外程度”换成掷硬币:正反面各一半,朝上的结果只需 1 位二进制就能描述,所以是 1 比特。
Q2. 香农信源编码定理指出,无损压缩的平均码长:
- A.不高于熵值
- B.不小于熵值← 正确答案
- C.恰好等于熵值
- D.与熵值无关
答案:B。信源编码定理的核心不等式是 $L \ge H(S)$,平均码长不能低于熵。可以理解为熵是搬家打包的理论最小容积,手艺再好也不能把箱子压缩到低于家当的“理论体积”。
Q3. 霍夫曼编码生成的码字集合属于下列哪一类编码?
- A.前缀码← 正确答案
- B.定长码
- C.汉明码
- D.卷积码
答案:A。霍夫曼编码保证没有码字是另一个码字的前缀,所以属于前缀码,解码时不需要分隔符。汉明码和卷积码是纠错码,定长码不会让高频符号变短,都不是霍夫曼的特征。
Q4. 若某信源的熵是 2.5 比特,一个设计良好的霍夫曼编码,其平均码长最可能接近:
- A.1.5 比特
- B.2.6 比特← 正确答案
- C.3.5 比特
- D.4.5 比特
答案:B。霍夫曼编码的平均码长 $L$ 满足 $H(S) \le L < H(S)+1$,即落在 $[2.5, 3.5)$ 区间。1.5 比特低于熵不可能,3.5 不满足严格小于,4.5 更远,只有 2.6 比特合理。
Q5. 与霍夫曼编码相比,算术编码的主要优势是:
- A.编码速度总是更快
- B.无需事先知道概率分布
- C.输出码长更贴近熵极限← 正确答案
- D.一定能将文件压到零
答案:C。算术编码把整条序列映射到区间上的一个小数,不受“每个符号至少 1 个整数比特”的限制,因此平均码长能更贴近熵。它仍然需要概率分布,速度通常比霍夫曼慢,且不可能压到零。
Q6. LZ77 滑动窗口编码的核心输出内容是:
- A.字符频率统计表
- B.重复子串的距离与长度← 正确答案
- C.完整的霍夫曼树
- D.每个字符的定长码
答案:B。LZ77 在滑动窗口内查找已出现的重复子串,输出 (距离,长度) 对,告诉解码器去前面哪个位置复制多长。频率表和霍夫曼树属于第二阶段的统计编码,不是 LZ77 的输出。
Q7. 信息瓶颈方法中,学习表征 Z 的目标是:
- A.最小化 I(X;Z) 并保持 I(Z;Y)← 正确答案
- B.最大化 I(X;Z) 并压缩标签
- C.最小化 I(Z;Y) 并最大化 H(Z)
- D.让 Z 与 X 完全保持一致
答案:A。信息瓶颈公式为 $\min I(X;Z) \text{ s.t. } I(Z;Y) \ge I_c$,即压缩输入表征的同时保留对任务有用的信息。如果让 Z 与 X 完全一致,就是没压缩;如果压缩标签,任务信息就丢了。
Q8. 下列压缩算法或格式中,属于无损压缩的有:
- A.ZIP 文件压缩← 正确答案
- B.JPEG 图片压缩
- C.FLAC 音频编码← 正确答案
- D.PNG 图像存储← 正确答案
答案:ACD。ZIP 用 DEFLATE 无损压缩;FLAC 是无损音频编码;PNG 采用无损压缩存储图像。JPEG 会丢弃人眼不敏感的高频信息,属于有损压缩,解压后无法恢复原始像素。
Q9. 关于霍夫曼编码,下列说法正确的有:
- A.高频符号获得较短码字← 正确答案
- B.码字集合满足前缀性← 正确答案
- C.平均码长距熵不超过一比特← 正确答案
- D.均匀分布时一定达到熵极值
答案:ABC。高频符号路径短、低频路径长;前缀性保证无歧义解码;信源编码定理保证 $L < H+1$。D 错误:均匀分布如 5 个等概率符号,熵约 2.32 比特,但霍夫曼需要 3 位定长码,达不到熵。
Q10. 关于算术编码,下列说法正确的有:
- A.用 [0,1) 区间中的小数表示整条序列← 正确答案
- B.每读入一个符号就按概率缩小当前区间← 正确答案
- C.输出码长在任意情况下都严格等于熵
- D.精度受限时可能出现解码失败← 正确答案
答案:ABD。算术编码确实把整串符号折叠成区间里的一个点,并逐符号把区间宽度乘以概率。由于精度有限,逼近熵但极少严格等于熵;一旦区间小到浮点无法区分,解码就会失败。C 的“严格等于”过于绝对。