数学重学 - 27 信息论基础

这是 数学重学路线图 阶段五的子页面

信息论基础:用数学衡量”信息”

信息论是香农(Claude Shannon)在 1948 年创建的

核心问题:信息怎么度量?通信极限在哪?

对安全、大数据、后端都有直接应用

一、信息量:越不可能,信息越大

直觉

“明天太阳从东边升起” → 信息量 = 0(废话,你早就知道了)

“明天太阳从西边升起” → 信息量极大(这也太震惊了)

不可能发生的事,一旦发生,传达的信息量越大

公式

I(x) = -log₂ P(x)

P(x) 是事件发生的概率

单位:比特(bit)

例子

抛硬币正面朝上:P = 0.5,I = -log₂(0.5) = 1 bit

掷骰子出6:P = 1/6,I = -log₂(1/6) ≈ 2.58 bit

必然事件:P = 1,I = -log₂(1) = 0 bit(果然是废话)

为什么用 log?

独立事件的信息量可加:A和B同时发生的信息量 = I(A) + I(B)

P(A∩B) = P(A)·P(B),所以 -log(P(A)·P(B)) = -log(P(A)) - log(P(B))

log 把乘法变加法,完美适配

Python 计算信息量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np

def information(p):
"""计算信息量(单位:bit)"""
if p <= 0 or p > 1:
raise ValueError("概率必须在 (0, 1] 范围内")
return -np.log2(p)

events = {
"抛硬币正面": 0.5,
"掷骰子出6": 1/6,
"生日在某天": 1/365,
"彩票中奖": 1/10_000_000,
"必然事件": 1.0,
}

for name, prob in events.items():
info = information(prob)
print(f"{name:12s}: P={prob:.8f}, 信息量={info:.2f} bit")

二、熵(Entropy):不确定性的度量

直觉

熵衡量一个随机变量的不确定性有多大

或者说:平均来看,每次观测能获得多少信息

公式

H(X) = -Σ P(x) · log₂ P(x)

就是信息量的期望值(加权平均)

关键性质

最大熵 = 均匀分布

所有结果等概率 → 不确定性最大

公平硬币:H = 1 bit(最大)

双面都是正面的硬币:H = 0 bit(毫无不确定性)

最小熵 = 确定事件

H = 0,完全没有不确定性

熵的范围:0 ≤ H(X) ≤ log₂(n),n 是可能结果数

直觉比喻

熵 = 惊喜度的平均值

高熵 → 经常被惊到 → 很难预测

低熵 → 很少被惊到 → 容易预测

Python 计算熵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import numpy as np

def entropy(probs):
"""计算离散分布的熵"""
probs = np.array(probs, dtype=float)
probs = probs[probs > 0] # 去掉0概率
return -np.sum(probs * np.log2(probs))

# 公平硬币
print(f"公平硬币: H = {entropy([0.5, 0.5]):.4f} bit") # 1.0

# 不公平硬币
print(f"偏硬币(0.9/0.1): H = {entropy([0.9, 0.1]):.4f} bit") # 0.469

# 公平骰子
print(f"公平骰子: H = {entropy([1/6]*6):.4f} bit") # 2.585

# 均匀分布 vs 集中分布
uniform = [0.25, 0.25, 0.25, 0.25]
concentrated = [0.97, 0.01, 0.01, 0.01]
print(f"均匀分布: H = {entropy(uniform):.4f} bit") # 2.0
print(f"集中分布: H = {entropy(concentrated):.4f} bit") # 0.242

熵的可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import numpy as np
import matplotlib.pyplot as plt

# 二元熵函数:H(p) = -p*log(p) - (1-p)*log(1-p)
p = np.linspace(0.001, 0.999, 500)
h = -p * np.log2(p) - (1 - p) * np.log2(1 - p)

plt.figure(figsize=(8, 5))
plt.plot(p, h, 'b-', linewidth=2)
plt.xlabel('P(正面)')
plt.ylabel('熵 H (bit)')
plt.title('二元熵函数:p=0.5时熵最大')
plt.axvline(x=0.5, color='r', linestyle='--', alpha=0.5)
plt.grid(True)
plt.show()

三、交叉熵:用错误编码的代价

直觉

假设真实分布是 P,但你以为是 Q

你用 Q 来编码 P 的消息,平均需要多少 bit?

这就是交叉熵

公式

H(P, Q) = -Σ P(x) · log₂ Q(x)

注意:是 P 的概率乘 Q 的 log

性质

H(P, Q) ≥ H(P)(交叉熵总是 ≥ 真实熵)

当 Q = P 时,交叉熵 = 真实熵(最优编码)

Q 偏离 P 越远,交叉熵越大

机器学习中的交叉熵损失

分类任务中:P = 真实标签分布,Q = 模型预测分布

最小化交叉熵 = 让模型预测尽量接近真实分布

这就是为什么分类用交叉熵损失而不是 MSE

Python 交叉熵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np

def cross_entropy(p, q):
"""交叉熵 H(P, Q)"""
p = np.array(p, dtype=float)
q = np.array(q, dtype=float)
q = np.clip(q, 1e-15, 1) # 防止 log(0)
return -np.sum(p * np.log2(q))

# 真实分布
p = [0.7, 0.2, 0.1]

# 好的预测
q_good = [0.65, 0.25, 0.10]
# 差的预测
q_bad = [0.33, 0.33, 0.34]

print(f"真实熵 H(P): {cross_entropy(p, p):.4f} bit")
print(f"好预测 H(P, Q): {cross_entropy(p, q_good):.4f} bit")
print(f"差预测 H(P, Q): {cross_entropy(p, q_bad):.4f} bit")
# 差预测的交叉熵更大 = 损失更大 = 模型更差

四、KL 散度:两个分布有多”不同”

直觉

衡量用 Q 近似 P 时”浪费”了多少 bit

KL 散度 = 交叉熵 - 真实熵

公式

KL(P || Q) = Σ P(x) · log₂(P(x) / Q(x))

等价于:KL(P || Q) = H(P, Q) - H(P)

关键性质

KL ≥ 0,等号成立当且仅当 P = Q

不对称KL(P || Q) ≠ KL(Q || P)

所以 KL 不是真正的”距离”(不满足对称性)

Python 计算 KL 散度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import numpy as np
from scipy.stats import entropy as scipy_entropy

def kl_divergence(p, q):
"""KL(P || Q)"""
p = np.array(p, dtype=float)
q = np.array(q, dtype=float)
q = np.clip(q, 1e-15, 1)
return np.sum(p * np.log2(p / q))

p = [0.7, 0.2, 0.1]
q = [0.33, 0.33, 0.34]

print(f"KL(P || Q) = {kl_divergence(p, q):.4f} bit")
print(f"KL(Q || P) = {kl_divergence(q, p):.4f} bit") # 不一样!

# scipy 自带(注意 scipy 用自然对数 ln)
kl_scipy = scipy_entropy(p, q) # 单位是 nat
kl_bits = kl_scipy / np.log(2) # 转成 bit
print(f"scipy KL = {kl_bits:.4f} bit")

五、互信息:两变量共享多少信息

直觉

知道了 X,对猜测 Y 有多大帮助?

互信息 = 0 → X 和 Y 独立,互不相干

互信息大 → 知道一个对猜另一个帮助很大

公式

I(X; Y) = H(X) - H(X|Y) = H(X) + H(Y) - H(X, Y)

与相关系数的区别

相关系数只能衡量线性关系

互信息能捕捉任意关系(包括非线性)

Python 互信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sklearn.metrics import mutual_info_score
import numpy as np

# 完全相关
x = [0, 0, 1, 1, 2, 2]
y = [0, 0, 1, 1, 2, 2]
print(f"完全相关的互信息: {mutual_info_score(x, y):.4f}")

# 完全独立
np.random.seed(42)
x_rand = np.random.randint(0, 3, 10000)
y_rand = np.random.randint(0, 3, 10000)
print(f"独立变量的互信息: {mutual_info_score(x_rand, y_rand):.4f}")

# 部分相关
y_partial = x_rand.copy()
noise_idx = np.random.choice(10000, 3000, replace=False)
y_partial[noise_idx] = np.random.randint(0, 3, 3000)
print(f"部分相关的互信息: {mutual_info_score(x_rand, y_partial):.4f}")

六、数据压缩:熵是压缩极限

核心思想

数据有冗余(熵 < 最大可能熵)→ 可以压缩

压缩极限 = 熵,不可能压到比熵还小

这就是香农的信源编码定理

霍夫曼编码

思路:高频字符用短编码,低频字符用长编码

保证前缀无歧义(没有一个编码是另一个的前缀)

是最优的前缀编码

Python 手动实现霍夫曼编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import heapq
from 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_huffman_tree(text):
freq = Counter(text)
heap = [HuffmanNode(ch, f) for ch, f in freq.items()]
heapq.heapify(heap)

while len(heap) > 1:
left = heapq.heappop(heap)
right = heapq.heappop(heap)
merged = HuffmanNode(None, left.freq + right.freq)
merged.left = left
merged.right = right
heapq.heappush(heap, merged)

return heap[0]

def get_codes(node, prefix="", codes=None):
if codes is None:
codes = {}
if node.char is not None:
codes[node.char] = prefix or "0"
return codes
get_codes(node.left, prefix + "0", codes)
get_codes(node.right, prefix + "1", codes)
return codes

# 示例
text = "aaabbbccddddddeeeeeeeeee"
tree = build_huffman_tree(text)
codes = get_codes(tree)

print("字符 | 频率 | 编码")
print("-" * 30)
freq = Counter(text)
for ch, code in sorted(codes.items()):
print(f" {ch} | {freq[ch]:2d} | {code}")

# 压缩效果
original_bits = len(text) * 8 # 每字符 8 bit (ASCII)
compressed_bits = sum(len(codes[ch]) for ch in text)
print(f"\n原始大小: {original_bits} bit")
print(f"压缩后: {compressed_bits} bit")
print(f"压缩率: {compressed_bits/original_bits:.1%}")

# 理论极限 = 熵
import numpy as np
probs = [f / len(text) for f in freq.values()]
H = -sum(p * np.log2(p) for p in probs)
print(f"熵(理论极限): {H:.4f} bit/字符")
print(f"实际平均编码长度: {compressed_bits/len(text):.4f} bit/字符")

七、安全中的信息论

密码强度 = 熵

密码熵越高,越难破解

密码熵 = log₂(字符空间 ^ 密码长度)

8位纯数字:log₂(10⁸) ≈ 26.6 bit(很弱)

12位混合大小写+数字+特殊:log₂(94¹²) ≈ 78.7 bit(还行)

Python 密码熵计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import math
import string

def password_entropy(password):
"""计算密码的熵(bit)"""
charset_size = 0
has_lower = any(c in string.ascii_lowercase for c in password)
has_upper = any(c in string.ascii_uppercase for c in password)
has_digit = any(c in string.digits for c in password)
has_special = any(c in string.punctuation for c in password)

if has_lower: charset_size += 26
if has_upper: charset_size += 26
if has_digit: charset_size += 10
if has_special: charset_size += 32

if charset_size == 0:
return 0

entropy = len(password) * math.log2(charset_size)
return entropy

passwords = [
"12345678", # 纯数字
"password", # 纯小写
"Password1", # 混合
"P@ssw0rd!2024", # 强密码
"correct horse battery staple", # 长密码
]

print(f"{'密码':<35s} {'熵(bit)':>8s} {'强度'}")
print("-" * 60)
for pwd in passwords:
ent = password_entropy(pwd)
strength = "弱" if ent < 40 else "中" if ent < 60 else "强" if ent < 80 else "很强"
print(f"{pwd:<35s} {ent:>8.1f} {strength}")

随机数质量评估

好的随机数生成器产生的序列熵应接近最大熵

如果熵偏低 → 随机数有规律 → 可被预测 → 不安全

隐写分析

正常图片的像素分布有一定的熵

隐写术会改变像素分布 → 熵发生变化

通过检测熵的异常来发现隐写内容

八、大数据中的信息论

特征选择(互信息法)

计算每个特征与目标变量的互信息

互信息高的特征更有价值

比相关系数更强大(能捕捉非线性关系)

决策树分裂(信息增益)

决策树选择分裂特征的标准:信息增益

信息增益 = H(父节点) - Σ权重 · H(子节点)

选信息增益最大的特征来分裂

1
2
3
4
5
6
7
8
9
10
11
12
import numpy as np
from sklearn.datasets import load_iris
from sklearn.feature_selection import mutual_info_classif

# 用互信息做特征选择
iris = load_iris()
mi_scores = mutual_info_classif(iris.data, iris.target, random_state=42)

print("特征互信息得分:")
for name, score in zip(iris.feature_names, mi_scores):
bar = "█" * int(score * 30)
print(f" {name:20s}: {score:.4f} {bar}")

数据压缩

日志数据有大量重复 → 高冗余 → 高压缩率

加密数据熵接近最大 → 几乎不可压缩

这就是为什么要先压缩再加密(反过来效果很差)

九、后端中的信息论

日志压缩

日志有大量重复模式 → 熵远低于最大熵 → 可以高效压缩

gzip/zstd 等压缩算法本质上在利用低熵

传输优化

信息论告诉你:信道容量 = 带宽上限

压缩到接近熵 → 最大化信道利用率

缓存策略

高熵(不可预测)的访问模式 → 缓存命中率低

低熵(可预测)的访问模式 → 缓存命中率高

可以用熵来评估缓存策略的理论上限

十、练习题

题目1:信息量计算

一副 52 张扑克牌,随机抽一张。抽到红心 A 的信息量是多少 bit?

答案

P(红心A) = 1/52

I = -log₂(1/52) = log₂(52) ≈ 5.70 bit

题目2:熵的比较

两个骰子:A 是公平骰子(六面等概率),B 是灌铅骰子(6 出现概率 0.5,其他各 0.1)。谁的熵大?计算两者的熵。

答案

1
2
3
4
5
6
7
8
9
10
import numpy as np

def entropy(probs):
probs = np.array(probs)
return -np.sum(probs * np.log2(probs))

h_a = entropy([1/6]*6)
h_b = entropy([0.1, 0.1, 0.1, 0.1, 0.1, 0.5])
print(f"公平骰子: H = {h_a:.4f} bit") # 2.585
print(f"灌铅骰子: H = {h_b:.4f} bit") # 2.161

公平骰子熵更大(不确定性更高)

题目3:交叉熵应用

为什么机器学习分类任务用交叉熵损失而不是 MSE?从信息论角度解释。

答案

交叉熵直接衡量预测分布和真实分布的差异

当预测概率接近 0 但真实标签是 1 时,交叉熵惩罚极大(-log(0) → ∞)

MSE 对这种情况的惩罚力度不够((1-0)² = 1,有限值)

交叉熵的梯度在错误预测时更大,训练更快

题目4:密码安全

你的系统要求密码至少 60 bit 熵。如果只允许小写字母 + 数字(36 个字符),密码至少要多少位?

答案

每个字符的熵 = log₂(36) ≈ 5.17 bit

需要位数 = 60 / 5.17 ≈ 11.6

所以至少需要 12 位

题目5:Python 实践

写一个函数,输入一段文本,输出每个字符的霍夫曼编码以及理论压缩率

答案

参考本页”霍夫曼编码”部分的完整代码

压缩率 = 霍夫曼编码总 bit 数 / (字符数 × 8)

理论极限 = 熵 / 8(每字符)

十一、总结与关联

信息论核心概念:

信息量 → 事件的惊讶程度

→ 平均惊讶程度(不确定性)

交叉熵 → 用错误模型的代价

KL 散度 → 两个分布的”距离”

互信息 → 两变量的关联度

关联笔记

26-优化与梯度下降 交叉熵作为损失函数

← 数学重学/18-概率基础 概率是信息论的地基

28-密码学数学进阶 密码学中的熵和信息安全


上一章 目录 下一章
26-优化与梯度下降 数学重学路线图 28-密码学数学进阶