卷 I · 惊CH 02深度 2/24

熵:平均每次有多意外

单条消息的意外我们会算了。但一条通信线路要连着跑一整天,你关心的是平均值。这个平均值有个名字叫熵,而它是这本书后面所有事情的地板。

H = −Σ p log₂ p压缩的地板真语料

▷ 先猜一下

一枚做了手脚的硬币,90% 出正面、10% 出反面。抛一次,平均带来多少比特的信息?

A 1 比特(有两种结果)B 0.9 比特C 约 0.47 比特D 约 0.1 比特

这个数字很重要,因为它就是「你为什么能压缩文件」的全部答案。

把意外做个加权平均

上一章我们有了单个结果的信息量 −log₂ p。现在把每种结果的信息量,按它出现的概率加权平均:

H(X) = Σ p(x) · ( −log₂ p(x) )
     = −Σ p(x) log₂ p(x)                单位:比特 / 每个符号

# 读法一:这个信息源平均每吐一个符号,带来多少比特。
# 读法二:要确定下一个符号是什么,平均最少要问几个是非问题。
# 读法三(第 5 章会证明):这就是无损压缩的地板,一比特都过不去。

这个量叫(entropy)。名字是冯·诺依曼建议香农用的——据说理由有点狡猾:「没人真正知道熵是什么,所以辩论的时候你总是占上风。」这个玩笑流传很广,真假难辨,但它无意中说中了一件事:熵这个词在物理和信息两个领域各活了一次,而很多人以为那是两个东西。它们不是。第 22 章会把它们接回同一个常数上。

手算一遍那枚偏心硬币

H = −0.9 × log₂ 0.9  −  0.1 × log₂ 0.1

  log₂ 0.9 = −0.152        →  −0.9 × (−0.152) = 0.137
  log₂ 0.1 = −3.322        →  −0.1 × (−3.322) = 0.332

H = 0.137 + 0.332 = 【0.469 比特】

不到半个比特。

看这两项各自的意思,很有味道:「出正面」这件事占了 90% 的时间,但只贡献了 0.137 比特——因为它太常见了,每次只值 0.152 比特。「出反面」只占 10% 的时间,却贡献了 0.332 比特——因为它罕见,一出现就值 3.322 比特。

◆ 熵是一场拉锯

一个符号对熵的贡献 = 它有多常见 × 它有多意外。而这两者天生相反。

所以熵在两头都会掉到 0:全是同一个结果(不意外),或者某个结果永远不出现(不常见)。熵最大的地方在正中间——所有结果一样可能的时候。

下面这台计算器每一格的宽度就是概率,格底下那个数字就是它值多少比特。熵就是这些数字按宽度加权的平均:

这 0.469 比特意味着什么

它意味着:如果你用一个字节存一次抛硬币的结果,你浪费了 87.5%;就算你聪明地用 1 个比特存,你还是浪费了 53%。

你花的:      1.000 比特 / 次
真正需要的:  0.469 比特 / 次
浪费:        0.531 比特 / 次  =  53.1%

# 抛一百万次:
你花的:      1000000 比特  =  122.07 KB
理论下限:     468996 比特  =   57.25 KB
# 有一半以上的硬盘是白买的。

「可是一次抛硬币怎么可能只用 0.469 个比特?比特不是整数吗?」

好问题,而且它正是第 7 章和第 8 章的全部内容。剧透:单独存一次确实做不到,但连着存很多次就可以——你不给每一次分配整数个比特,你给整条序列分配一个数。

拿真语料算一遍

上面那台计算器里有两档是真语料,不是编的数字。左右按一下对比着看:

语料符号数不同符号均匀的话实际零阶熵
英文(26 字母 + 空格 + 标点)2053274.755 bit4.037 bit
中文(同一段话的中译)6122217.788 bit7.113 bit
随机 DNA(四种碱基)200042.000 bit1.999 bit

三行各说明一件事。

DNA 那行:熵几乎顶到了天花板。四种碱基几乎等概率出现,所以 2 比特一个碱基就是地板,任何压缩器都别想再省。(真实基因组不是随机的,能省一点,但也省不了多少——第 9 章会拿它当反面教材。)

英文那行:4.037 比特。比均匀的 4.755 低了 0.7 比特,因为 ez 常见得多。但注意这只用了「单个字母出现多少次」这一点信息,完全没用上下文——它不知道 q 后面几乎一定跟 u,不知道 th 后面大概率是 e。把上下文用上之后这个数会掉多少?第 11 章那张表会给你看:掉到 1.4 比特左右

中文那行藏着一个陷阱

中文单字的熵 7.113 比特,比英文单字符的 4.037 高得多。很容易得出「中文信息密度高」或者「中文更难压缩」这种结论。

两个结论都不能这么下,因为两边的「一个符号」不是一回事。

同一段话,中文用 612 个字讲完,英文用 2053 个字符。把地板乘出来:

中文:  612 字 × 7.113 比特/字     = 4353 比特 = 【545 字节】
英文: 2053 字符 × 4.037 比特/字符 = 8287 比特 = 【1036 字节】

# 同样一段话,中文的信息论地板不到英文的一半。
# 而如果你按 UTF-8 存中文:612 字 × 3 字节 = 1836 字节。
# UTF-8 在这段文字上浪费了 70%。
✗ 这个直觉是错的
熵高 = 信息量大 = 难压缩。所以中文比英文难压。 熵的单位是比特每符号。换一种切分方式,「符号」变了,这个数就变了,而整条消息的总比特数不变

要比较两种语言,得比每条消息或者每个意思的熵,不能比每个符号的熵。

这个坑在工程里天天出现。「我们的模型 perplexity 从 20 降到 8 了!」——如果同时把分词器从字换成了子词,这两个数字根本不可比。比较困惑度之前,先确认两边的「一个符号」是同一个东西。

▸ 在现实里:你日志里那个 level 字段

一条典型的服务日志长这样:{"level":"INFO","msg":...}

假设你的日志里 INFO 占 95%、WARN 占 4%、ERROR 占 1%。这个字段的熵是:

H = 0.95×0.074 + 0.04×4.644 + 0.01×6.644
  = 0.0703 + 0.1858 + 0.0664
  = 【0.3225 比特】

# 而你存的是字符串 "INFO" —— 四个 ASCII 字符 = 32 比特。
# 浪费了 99 倍。

# 一百万行日志,光这一个字段就多花:
   (32 − 0.3225) × 1000000 / 8 / 1024 / 1024 = 【3.8 MB】

这就是为什么这些东西存在,而且它们做的其实是同一件事:

  • Protobuf / Avro / Thrift:字段名不进数据,只传字段号。因为「这个位置是 level 字段」这件事,schema 已经说了,重复一遍是 0 比特的信息
  • 列式存储的字典编码(Parquet、ClickHouse):把 INFO 映射成一个小整数,整列共用一本字典。
  • 结构化日志 + 采样:INFO 全量存本来就不划算,因为它几乎不带信息。

共同的判据只有一句:这个字段的熵是多少,你花了多少比特存它。两者之间的比值就是你能省下的上限。

熵的两个边界

熵有两个硬边界,都很好记:

0  ≤  H(X)  ≤  log₂ n           n = 有几种可能的结果

# 左边取等:某个结果概率是 1,其它全是 0。
#           完全确定,一个问题都不用问。
# 右边取等:所有结果概率相同。
#           这时你没有任何可利用的偏好,只能老老实实二分。

右边那个上界值得多看一眼,因为它有个很实用的读法:熵告诉你,这个信息源「相当于一个几面的均匀骰子」。

熵 H  →  等效面数 = 2^H            这个数叫【困惑度】(perplexity)

公平硬币   H = 1.000  →  2 面      # 就是它自己
偏心硬币   H = 0.469  →  1.38 面   # 比一枚硬币还容易猜
公平骰子   H = 2.585  →  6 面      # 就是它自己
英文单字符 H = 4.037  →  16.4 面   # 27 个字母,但难度只相当于 16 面骰子
中文单字   H = 7.113  →  138.5 面  # 221 个不同的字,难度相当于 139 面

「等效多少面骰子」这个说法在第 11 章会正式登场,用来衡量语言模型。你以后每看到一个模型的 perplexity 数字,脑子里应该浮现的就是这个骰子。

∑ 算一遍:熵是怎么变成「问几个问题」的

说熵是「平均要问几个是非问题」,听着像比喻。它不是,我们把它算给你看。

四种结果 A、B、C、D,概率 0.5、0.25、0.125、0.125。熵是:

H = 0.5×1 + 0.25×2 + 0.125×3 + 0.125×3 = 【1.75 比特】

现在设计一套问法:先问「是 A 吗?」——一半的时候一个问题就完事。不是 A,再问「是 B 吗?」——又解决四分之一。还不是,问「是 C 吗?」——剩下的全部解决。

A(50% 的时候):1 个问题
B(25% 的时候):2 个问题
C(12.5%)    :3 个问题
D(12.5%)    :3 个问题

平均 = 0.5×1 + 0.25×2 + 0.125×3 + 0.125×3 = 【1.75 个问题】

一模一样。而且这不是巧合——注意每种结果的概率都是 2 的幂,所以 −log₂p 正好是整数,问题数可以精确匹配上。

第 6 章那台霍夫曼编码器干的就是这件事:它自动帮你设计这套问法。而当概率不是 2 的幂时会发生什么——那是第 7 章的事,也是这套办法唯一的病。

为什么熵这个概念比它看起来重要

因为它是第一个把「我不知道」变成一个可以加减乘除的量的东西。

在 1948 年之前,「不确定」是一个形容词。之后它是一个带单位的数,可以比大小、可以做加法、可以在等式两边移项、可以证明不等式。凡是一个模糊的词被换成一个带单位的量,那个领域就会在接下来几十年里被彻底重写。信息论、热力学、概率论都发生过这件事。

下面这些,全都是熵这个量长出来的:

  • 压缩率的地板(第 5 章)——不是「目前最好的算法能到这里」,是「谁也过不去」。
  • 信道容量(第 16 章)——一条有噪声的线,一秒能可靠送多少,是一个确定的数。
  • 交叉熵损失(第 10 章)——你训练模型时优化的那个东西,字面上就是这个。
  • 信息增益(第 19 章)——决策树决定先砍哪一刀的依据。
  • 兰道尔极限(第 22 章)——擦掉一个比特要散多少热,一个有焦耳单位的数。
◇ 结账
C:0.469 比特

选 A 的人抓的是「有几种可能」,选 B 的人抓的是「概率 0.9」,选 D 的人抓的是「反面只有 10%」。

三个直觉都很自然,而正确答案是它们都不是——0.469 = 0.9 × 0.152 + 0.1 × 3.322,是「常见但便宜」和「罕见但贵」两项加起来的结果。

而这 0.469 和你实际花掉的 1 比特之间那道 53% 的缝,就是接下来整整一卷(第 5–9 章)要去把它榨干的东西。

这一章的一句话

熵是平均意外,也是「平均最少要问几个是非问题」,还是无损压缩的地板——这三句话说的是同一个数。而你现在存文件的方式,离那个地板还差得很远。

下一章:上面所有的熵都是我算给你的,用的是我选的语料和我算的频率。但如果熵真的取决于观察者,那你自己的熵是多少?下一章有一个香农 1951 年设计的实验,你可以亲手把它做一遍——一个字一个字往下猜,猜完之后,你会得到一个属于你自己的数字。而做完这个实验,你会发现「这段文字的熵是多少」这个问题,其实问得不完整