信息的度量:自信息与信息熵

一句话概述

信息论是香农在1948年创立的"通信数学理论",它回答了一个根本问题:如何定量地衡量"信息"?自信息量化了单个事件的"惊讶程度"——越不可能发生的事,一旦发生携带的信息量就越大;信息熵则是随机变量整体不确定性的平均度量,是信息论最重要、最核心的概念。从数据压缩到机器学习,从决策树分裂到熵正则化,信息熵的思想无处不在。

💡 核心要点:①信息不是"内容"而是"不确定性的消除",信息量取决于事件概率的对数;②自信息 = -log(p),概率越小信息量越大——太阳从东边升起没有信息,中彩票大奖信息量巨大;③信息熵是自信息关于概率的加权平均,表示随机变量整体的不确定性程度;④熵在AI中是决策树分裂准则、损失正则化项和模型不确定性度量的核心工具。

教学与演示

一、信息是什么——消除不确定性的度量

是什么(定义):在信息论中,信息的定义与语义无关——信息是"不确定性的减少"。当你面对一个随机事件的结果不确定时,收到消息后不确定性被消除了多少,这条消息就携带了多少信息。香农用概率来量化这种不确定性:一个事件发生的概率越小,它一旦发生所带来的"惊讶"就越大,信息量也就越大。

大白话 想象你有一个朋友,她每天都发朋友圈说"今天又是活着的一天"——这没有任何信息,因为你早知道她还活着。但如果有一天她突然发"我今天中了五百万",这就是爆炸性信息!为什么?因为这件事发生的概率极小,一旦发生就消除了巨大的不确定性。信息的本质不是"说了什么",而是"消除了多少不确定"。

为什么(原理):信息的量化必须满足三个直觉条件。第一,越不可能发生的事件包含信息量越大——确凿无疑的事(概率为1)信息量为0。第二,信息量总是非负的——收到消息不会让你更不确定。第三,独立事件同时发生的信息量等于各自信息量之和——比如独立地知道"今天下雨"和"股票涨了",两条消息的总信息量应该相加。这三个条件只能由对数函数满足,因此信息量定义为概率的负对数。

怎么做(实现)

import numpy as np

# ==================== 信息的基本度量:自信息 ====================

def self_information(p):
    """
    计算自信息(以 bit 为单位,底数为 2)
    自信息表示一个概率为 p 的事件发生时携带的信息量
    
    参数:
        p: 事件发生的概率,范围 [0, 1]
    返回:
        自信息值(单位:比特 bit)
    """
    if p <= 0:
        return float('inf')  # 不可能事件,信息量无穷大(理论上)
    if p >= 1:
        return 0.0  # 必然事件,信息量为 0
    return -np.log2(p)  # 以 2 为底的对数

# ==================== 不同概率事件的自信息对比 ====================
# 模拟一系列概率从高到低的事件,观察信息量的变化
probabilities = [1.0, 0.5, 0.25, 0.1, 0.01, 0.001, 0.0001]
print("事件概率 vs 自信息量(单位:bit)")
print("=" * 50)
print(f"{'概率 p':>10} | {'自信息 I(p)':>12} | 解读")
print("-" * 50)
for p in probabilities:
    info = self_information(p)
    # 从生活场景解读信息量
    if p >= 1.0:
        note = "必然发生,毫无信息"
    elif p > 0.3:
        note = "常见事件,信息量较低"
    elif p > 0.05:
        note = "不太常见,有一定信息量"
    elif p > 0.001:
        note = "罕见事件,信息量较大"
    else:
        note = "极罕见事件,信息量巨大"
    print(f"{p:>10.4f} | {info:>12.4f} | {note}")

# ==================== 验证信息量的可加性 ====================
# 两个独立事件同时发生的信息量 = 各自信息量之和
p_a = 0.5   # 事件 A 概率,如抛硬币正面
p_b = 1/6   # 事件 B 概率,如掷骰子得到 6
# 两个独立事件同时发生的概率
p_both = p_a * p_b  # 独立事件:同时发生的概率相乘

info_a = self_information(p_a)      # 事件 A 的自信息
info_b = self_information(p_b)      # 事件 B 的自信息
info_both = self_information(p_both)  # 两事件同时发生的自信息
info_sum = info_a + info_b          # 自信息之和

print(f"\n验证信息可加性:")
print(f"  事件A概率={p_a:.4f}, 自信息={info_a:.4f} bit")
print(f"  事件B概率={p_b:.4f}, 自信息={info_b:.4f} bit")
print(f"  同时发生概率={p_both:.4f}, 自信息={info_both:.4f} bit")
print(f"  自信息之和 = {info_sum:.4f} bit")
print(f"  是否相等: {np.isclose(info_both, info_sum)}  # -log2(p_a*p_b) = -log2(p_a) - log2(p_b)")

# ==================== 底数的影响:bit vs nat vs hartley ====================
p = 0.25
print(f"\n不同对数底数下的自信息(p=0.25):")
print(f"  以 2 为底 (bit):   {-np.log2(p):.4f}  # 信息论中最常用")
print(f"  以 e 为底 (nat):   {-np.log(p):.4f}   # 数学推导中常用自然对数")
print(f"  以 10 为底 (hartley): {-np.log10(p):.4f} # 通信工程中偶用")
print("(不同底数之间只差一个常数倍,本质相同)")
自信息公式\(I(x) = -\log_2 p(x)\)

什么用(应用):自信息让我们能定量回答"这条消息有多少信息"——在数据压缩中,出现概率越高的符号用越短的编码(哈夫曼编码的数学基础正是自信息);在通信系统中,自信息帮助工程师确定最小传输带宽;在密码学中,正确密钥的自信息量很大,错误密钥的为0,信息论安全依赖于此。

哪些坑(缺点):自信息只衡量单个事件的"惊讶程度",无法刻画随机变量整体的不确定性;它的值取决于准确知道事件概率,实际问题中概率往往未知需要估计;对数函数在 p=0 处无定义,对于几乎不可能的事件,自信息会趋于无穷大——这在理论上是合理的,但在工程实践中需要截断处理。

二、自信息——单个事件的信息量

是什么(定义):自信息(Self-Information)是信息论中最基本的度量单位,定义为 I(x) = -log₂ p(x)。其中 p(x) 是离散随机变量 X 取值为 x 的概率。自信息的核心直觉是:越是意料之外的事情,一旦发生,携带的信息就越丰富。一个必然发生的事件(p=1)信息量为零,因为你知道它会发生,它的发生没有消除任何不确定性。

大白话 抛一枚硬币,正面朝上的自信息是 1 bit——因为你需要 1 个二进制位(0 或 1)来记录这个结果。但如果你抛一枚"不公平"的硬币,正面概率 0.9,那正面的自信息就只有 0.152 bit——因为"你早就猜到是正面了",它带给你的惊讶很小。反过来,如果概率只有 0.1 的反面出现了,自信息高达 3.32 bit——这就是"意外之喜"的数学化表达。

为什么(原理):自信息采用对数形式的原因可以从两个角度理解。从通信角度:如果每个符号的概率为 1/n,那么需要用 log₂(n) 个二进制位来区分 n 个等可能符号——这直接对应自信息。从数学角度:Khinchin 在1953年证明了,满足单调递减、非负、可加三个公理的信息度量函数必须是对数形式。因此自信息是对数函数的必然选择,不是人为约定。

怎么做(实现)

import numpy as np

# ==================== 自信息的深入探索 ====================

# 场景1:天气预测中的自信息
# 某地天气分布:晴 60%、多云 25%、雨 10%、雪 5%
weather_probs = {
    '晴': 0.60,
    '多云': 0.25,
    '雨': 0.10,
    '雪': 0.05
}

print("天气预测中的自信息分析")
print("=" * 55)
print(f"{'天气':>6} | {'概率':>8} | {'自信息(bit)':>12} | 解读")
print("-" * 55)
for weather, prob in weather_probs.items():
    info = -np.log2(prob)
    if info > 4:
        note = "非常意外!需要 4+ bit 来编码"
    elif info > 3:
        note = "比较意外,需要 3+ bit 来编码"
    elif info > 1:
        note = "有一定意外性"
    else:
        note = "意料之中,编码开销小"
    print(f"{weather:>6} | {prob:>8.2%} | {info:>12.4f} | {note}")

# ==================== 场景2:编码长度与自信息的关系 ====================
# 最优编码长度 = 自信息(香农第一定理的核心结论)
print(f"\n自信息与最优编码长度的关系")
print(f"概率 0.5  → 自信息 {(-np.log2(0.5)):.1f} bit → 用 1 个二进制位编码即可(0 或 1)")
print(f"概率 1/4 → 自信息 {(-np.log2(0.25)):.1f} bit → 用 2 个二进制位编码(00, 01, 10, 11)")
print(f"概率 1/8 → 自信息 {(-np.log2(1/8)):.1f} bit → 用 3 个二进制位编码")
print(f"结论:概率为 1/2^n 的事件,最优编码长度恰好是 n bit")

# ==================== 场景3:自信息的可加性在通信中的应用 ====================
# 发送一条长度为 n 的消息,每个符号独立的分布相同
# 整条消息的信息量 = n × 每个符号的平均信息量(熵)

def message_self_info(symbol_probs, message_indices):
    """
    计算一条消息的总自信息量
    
    参数:
        symbol_probs: 每个符号的概率列表
        message_indices: 消息中各符号的索引列表
    返回:
        消息的总自信息(bit)
    """
    total_info = 0.0
    for idx in message_indices:
        p = symbol_probs[idx]
        total_info += -np.log2(p)  # 累加各符号的自信息
    return total_info

# 假设有 4 个符号,概率分别为 [0.5, 0.25, 0.125, 0.125]
sym_probs = [0.5, 0.25, 0.125, 0.125]
# 发送消息:符号0(概率0.5)、符号1(概率0.25)、符号3(概率0.125)
msg = [0, 1, 3]
total = message_self_info(sym_probs, msg)
print(f"\n消息的自信息总量: {total:.4f} bit")
print(f"(0.5→{(-np.log2(0.5)):.1f}) + (0.25→{(-np.log2(0.25)):.1f}) + (0.125→{(-np.log2(0.125)):.1f}) = {total:.1f} bit")

# ==================== 场景4:用抛硬币实验理解自信息的几何意义 ====================
np.random.seed(42)

# 抛一枚不公平硬币 10000 次,p(正面) = 0.8
p_head = 0.8
n_tosses = 10000
tosses = np.random.choice([1, 0], size=n_tosses, p=[p_head, 1-p_head])
# 1 表示正面,0 表示反面

n_heads = np.sum(tosses)
n_tails = n_tosses - n_heads
print(f"\n抛硬币实验 (p_head={p_head}, 次数={n_tosses}):")
print(f"  正面次数: {n_heads}, 反面次数: {n_tails}")
print(f"  正面自信息: {(-np.log2(p_head)):.4f} bit")
print(f"  反面自信息: {(-np.log2(1-p_head)):.4f} bit")
print(f"  正面总信息: {n_heads * (-np.log2(p_head)):.1f} bit")
print(f"  反面总信息: {n_tails * (-np.log2(1-p_head)):.1f} bit")
print(f"  全部信息:   {(n_heads * (-np.log2(p_head)) + n_tails * (-np.log2(1-p_head))):.1f} bit")
print(f"  (这个值除以 10000 就是该硬币的熵)")
自信息的概率相关公理\(I(x) = f(p(x)), \quad f(1) = 0, \quad f(p_1 p_2) = f(p_1) + f(p_2)\)

什么用(应用):自信息直接应用于哈夫曼编码——出现概率高的符号用短码表示(自信息小),概率低的符号用长码表示(自信息大),从而实现最优无损压缩。在异常检测中,如果一个事件的概率极低(自信息极大),它很可能是一个异常;在网络流量分析中,罕见的请求模式对应高自信息,可能是攻击信号。

哪些坑(缺点):自信息的定义依赖于准确知道概率分布,而实际系统中概率是估计出来的,估计误差会导致信息度量的偏差;自信息是点度量,只能描述单个事件,无法直接用于比较不同随机变量之间的信息特性;在一些连续系统中(如高斯分布),微分熵可以取负值,这打破了"信息量非负"的直觉。

三、信息熵——平均信息量

是什么(定义):信息熵(Entropy)是随机变量不确定性的度量,定义为该随机变量所有可能取值自信息的数学期望。对于离散随机变量 X 服从分布 P,熵 H(X) = -Σ p(x) log₂ p(x)。熵越大,随机变量的不确定性越高,你越无法预测它的取值;熵越小,随机变量越接近确定性。

大白话 如果你面对一个完全公平的骰子,六个面概率均等,你完全猜不到下一次投出什么——熵最大。如果骰子被"做了手脚",99%出六点,你几乎每次都能猜对——熵极小。信息熵就是把这种"你有多不确定"给算出一个数来。公平骰子熵约为 2.585 bit,作弊骰子熵可能只有 0.081 bit。

为什么(原理):熵的公式可以从多个角度推导。一是从自信息的期望直接得出——随机变量的"平均惊讶程度"。二是从编码角度——熵给出了无损编码该随机变量所需的最短平均码长(香农第一定理/无噪声编码定理)。三是组合论角度——熵衡量了概率分布的"均匀程度",均匀分布熵最大。这三个角度殊途同归,证明了熵作为不确定性度量的唯一性和合理性。

怎么做(实现)

import numpy as np

# ==================== 信息熵的计算 ====================

def entropy(probs, base=2):
    """
    计算离散概率分布的熵
    
    参数:
        probs: 概率分布数组,要求和为 1,每个元素 >= 0
        base: 对数的底数,默认 2(单位 bit)
    返回:
        熵值
    """
    # 过滤掉概率为 0 的项(0 * log(0) 在极限意义下为 0)
    probs = np.array(probs)
    probs = probs[probs > 0]  # 去除零概率事件
    # 熵公式: H = -Σ p_i * log(p_i)
    if base == 2:
        h = -np.sum(probs * np.log2(probs))
    elif base == np.e:
        h = -np.sum(probs * np.log(probs))
    else:
        h = -np.sum(probs * np.log(probs)) / np.log(base)
    return h

# ==================== 不同分布的熵比较 ====================

# 分布1:均匀分布(熵最大,最不确定)
p_uniform_2 = np.array([0.5, 0.5])        # 公平硬币
p_uniform_6 = np.array([1/6]*6)           # 公平骰子
p_uniform_10 = np.array([0.1]*10)         # 10面均匀骰子

# 分布2:确定性分布(熵为0,完全确定)
p_deterministic = np.array([1.0, 0.0, 0.0])

# 分布3:中间分布(部分确定)
p_skewed_coin = np.array([0.9, 0.1])      # 不公平硬币
p_skewed_coin2 = np.array([0.999, 0.001]) # 极不公平硬币

distributions = [
    ("确定性事件  [1, 0, 0]", p_deterministic),
    ("极偏硬币    [0.999, 0.001]", p_skewed_coin2),
    ("偏倚硬币    [0.9, 0.1]", p_skewed_coin),
    ("公平硬币    [0.5, 0.5]", p_uniform_2),
    ("公平骰子    [1/6 × 6]", p_uniform_6),
    ("10面均匀骰  [0.1 × 10]", p_uniform_10),
]

print("不同概率分布的熵比较(单位:bit)")
print("=" * 55)
print(f"{'分布描述':<22} | {'熵':>8} | {'归一化熵':>10} | 说明")
print("-" * 55)
for desc, probs in distributions:
    h = entropy(probs)
    n = len(probs)
    max_h = np.log2(n)  # 最大可能熵
    normalized = h / max_h if max_h > 0 else 0
    if h < 0.001:
        note = "完全确定,零不确定性"
    elif h < 1:
        note = "接近确定,不确定性很低"
    elif h < 2:
        note = "有一定不确定性"
    else:
        note = "高度不确定"
    print(f"{desc:<22} | {h:>8.4f} | {normalized:>10.4f} | {note}")

# ==================== 熵的最大值 ====================
# 对于 n 个可能取值的随机变量,熵的最大值是 log2(n),当且仅当均匀分布时取得
print(f"\n数学事实:n 个取值的随机变量,最大熵 = log2(n)")
for n in [2, 4, 8, 16, 256]:
    print(f"  n={n:>3}: 最大熵 = {np.log2(n):.4f} bit")

# ==================== 代码验证:熵的非负性 ====================
# 熵总是 ≥ 0,当且仅当分布是确定性的(某个概率为1)时等于0
for desc, probs in distributions:
    h = entropy(probs)
    assert h >= -1e-10, f"熵应为非负,但得到 {h}"  # 允许微小浮点误差
print(f"\n已验证:所有分布的熵都 ≥ 0 ✓")

# ==================== 熵的另一个重要视角:编码效率 ====================
# 香农第一定理:最优前缀码的平均码长 L 满足 H ≤ L < H+1
print(f"\n熵与编码效率:")
print(f"  均匀硬币 H={entropy(p_uniform_2):.4f} bit → 最优平均码长 ≈ 1 bit (与熵相等)")
print(f"  偏倚硬币 H={entropy(p_skewed_coin):.4f} bit → 最优平均码长 < 1 bit (熵更小)")
print(f"  极偏硬币 H={entropy(p_skewed_coin2):.4f} bit → 几乎不需要 bit 来编码!")
print(f"  结论:熵越小,数据越可压缩")
信息熵公式\(H(X) = -\sum_{x \in \mathcal{X}} p(x) \log_2 p(x)\)
熵的三个基本性质\(0 \leq H(X) \leq \log_2 |\mathcal{X}|\)

什么用(应用):熵在AI中是决策树的核心工具——ID3和C4.5算法使用信息增益(父节点熵减去子节点加权熵)来选择最佳分裂特征;在特征工程中,低熵特征通常信息量少,应该考虑剔除;在文本分析中,词语的熵反映了它的广泛程度——"的"字熵很高(到处都有),专业术语熵很低(只在特定领域出现);信息熵还是交叉熵损失函数的重要组成部分。

哪些坑(缺点):熵对分布估计误差敏感——如果从有限样本估计概率,得到的熵会偏向低估(Miller-Madow修正);对于连续分布,离散熵的定义不直接适用,需要用微分熵(但微分熵可能为负);熵是"粗粒度"的度量——两个截然不同的分布可以有相同的熵,熵只衡量均匀程度而不衡量形状。

四、计算熵的Python代码

是什么(定义):本节用numpy从零实现熵的计算,覆盖离散熵、联合熵条件熵的完整计算流程,并用模拟数据演示熵在数据分析中的实际应用价值。我们将构建一个通用熵计算工具,验证熵的数学性质,并展示熵如何自动化地揭示数据中的规律。

大白话 前面我们学懂了熵的数学定义,现在来"动手算"。就像你学了勾股定理后真正去量三角形边长验证一样——我们要写Python程序来算熵,看看"均匀分布熵最大"、"确定分布熵为零"这些性质是否真的成立。然后用熵来分析一个模拟的用户行为数据,看看哪些行为"信息量大"、哪些"信息量小"。

为什么(原理):实现熵计算的关键在于正确处理边界情况。特别是零概率事件——0 × log(0) 在极限意义下为 0(因为 xlog(x)→0 当 x→0+),但直接计算会出现 NaN。NP.log2(0) 返回 -inf,与 0 相乘得到 NaN。正确做法是过滤掉概率为零的项,或者使用 np.where 安全处理。另外,验证概率和为1也很重要——如果用户输入的概率和不归一,应先做归一化。

怎么做(实现)

import numpy as np

# ==================== 完整的熵计算工具包 ====================

class EntropyCalculator:
    """信息熵计算工具,支持离散熵、联合熵、条件熵"""
    
    def __init__(self, base=2):
        """
        初始化熵计算器
        
        参数:
            base: 对数底数,2=bit, e=nat, 10=hartley
        """
        self.base = base
    
    def discrete_entropy(self, prob_vec):
        """
        计算离散分布的熵
        参数: prob_vec - 概率向量,应归一化
        返回: 熵值
        """
        prob_vec = np.array(prob_vec, dtype=float)
        # 归一化:确保概率和为 1
        prob_sum = np.sum(prob_vec)
        if abs(prob_sum - 1.0) > 1e-8:
            prob_vec = prob_vec / prob_sum  # 自动归一化
        
        # 只保留非零概率(0*log(0) = 0)
        prob_vec = prob_vec[prob_vec > 0]
        
        if self.base == 2:
            return -np.sum(prob_vec * np.log2(prob_vec))
        elif self.base == np.e:
            return -np.sum(prob_vec * np.log(prob_vec))
        else:
            return -np.sum(prob_vec * np.log(prob_vec)) / np.log(self.base)
    
    def empirical_entropy(self, data):
        """
        从数据样本估计熵
        参数: data - 一维标签数组
        返回: 经验熵
        """
        # 统计每个值出现的频率
        values, counts = np.unique(data, return_counts=True)
        probs = counts / len(data)  # 频率 → 概率估计
        return self.discrete_entropy(probs)
    
    def joint_entropy(self, prob_matrix):
        """
        计算联合熵 H(X, Y)
        参数: prob_matrix - 二维联合概率矩阵,行表示X取值,列表示Y取值
        返回: 联合熵
        """
        prob_matrix = np.array(prob_matrix, dtype=float)
        # 扁平化处理
        flat_probs = prob_matrix.flatten()
        return self.discrete_entropy(flat_probs)
    
    def conditional_entropy(self, prob_matrix):
        """
        计算条件熵 H(Y|X)
        H(Y|X) = H(X,Y) - H(X)
        参数: prob_matrix - 联合概率矩阵
        返回: 条件熵
        """
        prob_matrix = np.array(prob_matrix, dtype=float)
        # H(X, Y)
        h_xy = self.joint_entropy(prob_matrix)
        # H(X): 边缘分布
        p_x = np.sum(prob_matrix, axis=1)  # 按行求和
        h_x = self.discrete_entropy(p_x)
        return h_xy - h_x

# ==================== 实战:分析用户行为数据的熵 ====================
np.random.seed(42)

# 模拟 200 个用户的颜色偏好(5种颜色)
n_users = 200
colors_true = np.random.choice(
    ['红', '蓝', '绿', '黄', '紫'],
    size=n_users,
    p=[0.40, 0.30, 0.15, 0.10, 0.05]  # 非均匀分布
)

calc = EntropyCalculator(base=2)

print("=" * 60)
print("用户颜色偏好数据的熵分析")
print("=" * 60)

# 1. 从数据估计熵
h_colors = calc.empirical_entropy(colors_true)
print(f"\n颜色偏好的经验熵: {h_colors:.4f} bit")
print(f"最大可能熵(均匀分布): {np.log2(5):.4f} bit")
print(f"熵占比: {h_colors/np.log2(5)*100:.1f}%(越接近100%越均匀)")

# 2. 对比:如果偏好是均匀分布
colors_uniform = np.random.choice(
    ['红', '蓝', '绿', '黄', '紫'],
    size=n_users,
    p=[0.2, 0.2, 0.2, 0.2, 0.2]
)
h_uniform = calc.empirical_entropy(colors_uniform)
print(f"\n均匀分布数据的经验熵: {h_uniform:.4f} bit")

# 3. 模拟年龄和颜色偏好的联合分布
ages = np.random.choice(['青年', '中年', '老年'], size=n_users, p=[0.4, 0.35, 0.25])
# 计算联合频数矩阵
age_labels = ['青年', '中年', '老年']
color_labels = ['红', '蓝', '绿', '黄', '紫']

# 构建联合计数矩阵
joint_counts = np.zeros((3, 5))
for i in range(n_users):
    age_idx = age_labels.index(ages[i])
    color_idx = color_labels.index(colors_true[i])
    joint_counts[age_idx, color_idx] += 1

joint_probs = joint_counts / n_users  # 联合概率矩阵

print(f"\n年龄-颜色联合概率矩阵:")
print(f"{'':>8}", end="")
for c in color_labels:
    print(f"{c:>8}", end="")
print()
for i, age_lbl in enumerate(age_labels):
    print(f"{age_lbl:>8}", end="")
    for j in range(5):
        print(f"{joint_probs[i][j]:>8.4f}", end="")
    print()

# 4. 计算联合熵和条件熵
h_joint = calc.joint_entropy(joint_probs)
h_cond = calc.conditional_entropy(joint_probs)
h_age = calc.empirical_entropy(ages)

print(f"\n联合熵 H(年龄, 颜色): {h_joint:.4f} bit")
print(f"年龄的边缘熵 H(年龄):  {h_age:.4f} bit")
print(f"颜色的边缘熵 H(颜色):  {h_colors:.4f} bit")
print(f"条件熵 H(颜色|年龄):   {h_cond:.4f} bit")
print(f"验证关系: H(年龄)+H(颜色|年龄) = {h_age + h_cond:.4f} (vs H(联合)={h_joint:.4f})")

# 5. 验证熵的核心性质
print(f"\n验证熵的数学性质:")
# 非负性
assert h_colors >= 0, "熵必须 ≥ 0"
print(f"  ✓ 非负性: H = {h_colors:.4f} ≥ 0")
# 上界
assert h_colors <= np.log2(5), "熵不能超过 log2(n)"
print(f"  ✓ 上界性: H = {h_colors:.4f} ≤ log2(5) = {np.log2(5):.4f}")

print(f"\n数据分析结论:")
print(f"  颜色偏好熵 ({h_colors:.2f} bit) < 最大熵 ({np.log2(5):.2f} bit)")
print(f"  说明用户颜色偏好不是均匀的,存在明显的偏向性")
print(f"  条件熵 {h_cond:.2f} bit < 边缘熵 {h_colors:.2f} bit")
print(f"  说明知道了年龄后,对颜色偏好的不确定性减少了——年龄和颜色有关联!")
经验熵估计\(\hat{H}(X) = -\sum_{x \in \mathcal{X}} \frac{n_x}{N} \log_2 \frac{n_x}{N}\)

什么用(应用):学了如何计算熵之后,你可以在任何数据分析任务中快速评估变量的信息含量。在实践中:用经验熵衡量分类标签的平衡程度——熵越接近 log2(k) 说明类别越平衡;在特征工程中,计算每个特征的熵来判断其区分度;在文本挖掘中,用熵来衡量词语的专有程度,构建TF-IDF的变体。

哪些坑(缺点):从有限样本估计熵存在系统性偏差——样本量小时熵被低估(可用偏置修正如加入 (m-1)/(2N) 项);连续变量的熵估计需要做离散化(分箱),箱宽的选择会影响结果;熵对异常值敏感——如果数据中混入了噪声标签,熵会被"撑大";经验熵不支持置信区间计算,需要自举法(Bootstrap)来估计不确定性。

五、AI中的熵——决策树、熵正则化

是什么(定义):在AI和机器学习中,信息熵是多个核心算法和技术的数学基础。在决策树中,熵用于衡量节点"纯度"——纯节点熵为零(所有样本属于同一类别),混合节点熵高(各类别混杂)。信息增益 = 父节点熵 - 子节点加权熵,算法选择使信息增益最大的特征来分裂。熵正则化则在损失函数中加入熵惩罚项,鼓励模型做更"保守"的预测,防止过度自信。

大白话 决策树做分类就像"提问猜人"游戏。你每问一个问题(特征)就排除了一部分可能。好的问题是"删除最多可能性的问题"——这在数学上就是选择了"信息增益最大"的特征。而熵正则化呢?相当于告诉模型"别太嚣张"——即使你90%确定这是只猫,也请保留10%的谦逊空间,这样模型在面对没见过的数据时更稳健。

为什么(原理):决策树使用熵作为分裂准则的数学直觉来自香农编码理论——熵越小的集合越"纯",描述它所需的编码长度越短。信息增益衡量了某个特征在多大程度上"解释"了标签的混乱程度。熵正则化(如Label Smoothing)的理论基础是:模型对训练样本的"绝对自信"(预测概率接近1)往往意味着过拟合——它把训练数据的噪声也当成了确定性的规律。引入熵项让模型在正确和保守之间找到平衡。

怎么做(实现)

import numpy as np

# ==================== 1. 决策树中的信息增益 ====================

def entropy_from_labels(labels):
    """从类别标签计算熵"""
    _, counts = np.unique(labels, return_counts=True)
    probs = counts / len(labels)
    probs = probs[probs > 0]
    return -np.sum(probs * np.log2(probs))

def information_gain(parent_labels, feature_values):
    """
    计算某个特征的信息增益
    
    参数:
        parent_labels: 当前节点的样本标签
        feature_values: 对应样本在该特征上的取值(离散)
    返回:
        信息增益值
    """
    h_parent = entropy_from_labels(parent_labels)  # 父节点熵
    
    # 对特征的每个取值,计算子节点熵并加权
    unique_vals = np.unique(feature_values)
    h_children_weighted = 0.0
    n_total = len(parent_labels)
    
    for val in unique_vals:
        # 属于该取值的样本索引
        mask = feature_values == val
        n_child = np.sum(mask)
        child_labels = parent_labels[mask]
        # 子节点熵 × 子节点权重
        h_child = entropy_from_labels(child_labels)
        h_children_weighted += (n_child / n_total) * h_child
    
    # 信息增益 = 父节点熵 - 加权子节点熵
    gain = h_parent - h_children_weighted
    return gain

# 模拟数据:10个样本,2个类别,1个特征
np.random.seed(42)

# 类别标签
y = np.array(['是', '否', '是', '是', '否', '是', '否', '否', '是', '否'])

# 特征1:天气 ['晴', '雨', '晴', '晴', '雨', '晴', '雨', '雨', '晴', '雨']
weather = np.array(['晴', '雨', '晴', '晴', '雨', '晴', '雨', '雨', '晴', '雨'])

# 特征2:温度 ['高', '高', '中', '高', '低', '中', '低', '高', '中', '低']
temperature = np.array(['高', '高', '中', '高', '低', '中', '低', '高', '中', '低'])

print("=" * 55)
print("决策树中的信息增益示例")
print("=" * 55)
print(f"类别分布: 是={np.sum(y=='是')}, 否={np.sum(y=='否')}")
print(f"父节点熵 H(类别) = {entropy_from_labels(y):.4f} bit\n")

# 计算天气特征的信息增益
gain_weather = information_gain(y, weather)
print(f"特征'天气'的信息增益: {gain_weather:.4f} bit")

# 计算温度特征的信息增益
gain_temp = information_gain(y, temperature)
print(f"特征'温度'的信息增益: {gain_temp:.4f} bit")

# 决策树会选择信息增益更大的特征来分裂
best_feature = "天气" if gain_weather > gain_temp else "温度"
print(f"\n决策树选择的分裂特征: {best_feature}(信息增益更大)")

# ==================== 2. 熵正则化:Label Smoothing ====================

def cross_entropy_loss(y_true_probs, y_pred_probs):
    """
    交叉熵损失(用于分类)
    参数: y_true_probs - one-hot真实标签概率
          y_pred_probs - 模型预测概率
    """
    # 避免 log(0)
    eps = 1e-12
    y_pred_probs = np.clip(y_pred_probs, eps, 1 - eps)
    return -np.sum(y_true_probs * np.log(y_pred_probs))

def label_smoothing(one_hot_labels, epsilon=0.1):
    """
    Label Smoothing:将 one-hot 标签平滑为 "软标签"
    原始: [1, 0, 0] → 平滑后: [1-ε+ε/K, ε/K, ε/K]
    其中 K 是类别数
    
    效果: 增加标签分布的熵,防止模型"过度自信"
    """
    K = one_hot_labels.shape[1]  # 类别数
    # 每个类别分得 ε/K 的"信任度"
    smoothed = one_hot_labels * (1 - epsilon) + epsilon / K
    return smoothed

# 3个类别,one-hot标签为第0类
y_onehot = np.array([1.0, 0.0, 0.0])
K = len(y_onehot)
eps = 0.1

y_smoothed = label_smoothing(y_onehot, epsilon=eps)

print(f"\n{'='*55}")
print("Label Smoothing / 熵正则化")
print("=" * 55)
print(f"原始one-hot标签: {y_onehot}")
print(f"  熵 = {entropy_from_labels(y_onehot + 1e-10):.4f} bit (几乎为0)")
print(f"\n平滑后标签 (ε={eps}): {y_smoothed.round(4)}")
print(f"  熵 = {entropy(y_smoothed):.4f} bit (增加了不确定性)")

# 对比:硬标签 vs 软标签下的交叉熵损失
# 假设模型预测: 高度自信(过拟合风险)vs 适度自信
pred_overconfident = np.array([0.99, 0.005, 0.005])   # 过度自信
pred_moderate = np.array([0.85, 0.10, 0.05])           # 适度自信

print(f"\n不同预测下的交叉熵损失:")
print(f"{'标签类型':<18} | {'过度自信预测':>16} | {'适度自信预测':>16}")
print("-" * 55)
loss_hard_over = cross_entropy_loss(y_onehot, pred_overconfident)
loss_hard_mod = cross_entropy_loss(y_onehot, pred_moderate)
print(f"{'硬标签(one-hot)':<18} | {loss_hard_over:>16.4f} | {loss_hard_mod:>16.4f}")

loss_soft_over = cross_entropy_loss(y_smoothed, pred_overconfident)
loss_soft_mod = cross_entropy_loss(y_smoothed, pred_moderate)
print(f"{'软标签(smoothed)':<18} | {loss_soft_over:>16.4f} | {loss_soft_mod:>16.4f}")

print(f"\n结论:")
print(f"  硬标签下:过度自信的预测损失更小 → 鼓励过度自信")
print(f"  软标签下:过度自信反而可能更差 → 防止过度自信")
print(f"  这就是熵正则化的核心思想!")

# ==================== 3. 熵作为不确定性度量 ====================
# 在主动学习(Active Learning)中,熵用来选择"最不确定"的样本
predictions = {
    '样本A': np.array([0.98, 0.01, 0.01]),  # 非常确定
    '样本B': np.array([0.60, 0.25, 0.15]),  # 比较确定
    '样本C': np.array([0.40, 0.35, 0.25]),  # 不太确定
    '样本D': np.array([0.34, 0.33, 0.33]),  # 非常不确定
}

print(f"\n{'='*55}")
print("主动学习中的熵:选择最不确定的样本标注")
print("=" * 55)
print(f"{'样本':<10} | {'预测概率':>22} | {'熵':>8} | 不确定度")
print("-" * 55)
for name, pred in predictions.items():
    h = entropy(pred)
    print(f"{name:<10} | {str(pred.round(2)):>22} | {h:>8.4f} | {'★' * int(h*3)}")

# 主动学习选择熵最大的样本(样本D)优先标注
max_entropy_sample = max(predictions, key=lambda k: entropy(predictions[k]))
print(f"\n主动学习优先标注: {max_entropy_sample}(熵最大,模型最不确定)")
信息增益\(IG(D, A) = H(D) - \sum_{v \in \text{Values}(A)} \frac{|D_v|}{|D|} H(D_v)\)
熵正则化损失\(\mathcal{L}_{reg} = \mathcal{L}_{cross\_entropy} - \lambda \cdot H(p_\theta)\)

什么用(应用):熵在AI中的应用深度远超多数人的想象。除了决策树和正则化外:在聚类算法中,可以用熵评估聚类纯度;在半监督学习中,熵最小化原则将未标注数据的低熵预测作为优化目标;在强化学习中,策略熵激励智能体保持探索行为避免过早收敛;在生成对抗网络中,判别器的熵反映了真假样本的难区分程度;在大语言模型评估中,困惑度(perplexity) = 2^H 直接源于熵。

哪些坑(缺点):信息增益偏向于选择取值多的特征(如"ID编号"有1000个值,每个子节点只有一个样本,熵为零,信息增益极大但毫无泛化能力)——C4.5用增益率解决了这个问题;熵正则化需要仔细调节 λ 系数——太小没有效果,太大会使模型"躺平"不学习;熵在类别众多时的随机波动较大,小样本下可能出现"伪信息增益";主动学习中依赖熵选择样本可能被异常值误导——某些高熵样本只是噪声而非不确定区域。

概念关系图谱

概念核心含义与AI的关系关联概念
自信息单个事件携带的信息量,概率越小信息量越大交叉熵损失函数中对数项的数学来源信息熵、编码长度
信息熵随机变量整体的平均不确定性决策树分裂准则、熵正则化、模型不确定性度量自信息、条件熵、交叉熵
经验熵从数据样本中估计的熵从训练数据中直接计算信息增益信息熵、极大似然估计
均匀分布最大熵当所有结果等可能时熵达到最大解释了为什么平衡数据集对分类更好最大熵原理、拉普拉斯原理
条件熵已知某个变量后剩余的不确定性特征有效性评估、特征选择联合熵、互信息、信息增益
信息增益分裂前后熵的减少量决策树ID3/C4.5的核心分裂准则熵、条件熵、增益率
熵正则化/Label Smoothing在损失函数中加入熵惩罚项防止过拟合现代深度学习中防止过度自信的标准技巧KL散度、交叉熵、置信度校准

重点答疑

Q1: 为什么自信息要用对数函数?有没有其他函数也能做这件事?

香农在1948年提出信息论时,并没有从"对数函数是唯一选择"出发——他先定义了信息应该满足的三条公理(单调性、非负性、可加性),然后证明了只有对数函数能同时满足这三条。具体来说:f(p₁p₂) = f(p₁) + f(p₂) 这个函数方程的解只能是 f(p) = k·log(p) —— 这就是柯西函数方程的一个标准结论。所以对数不是"选择"的,而是"推导"出的必然结果。如果不用对数,自信息就无法满足独立事件信息的可加性——比如你收到两条独立的消息,总信息量就不再是各自信息量之和。

Q2: 熵的单位"bit"和我们平时说的"比特"(计算机存储单位)是同一个东西吗?

既是也不是。计算机中的 bit 是一个二进制位,物理上存储 0 或 1。信息论中的 bit 是信息量的度量,1 bit 对应"在两个等可能选项中做一次选择所需要的信息"。两者的联系是:一个公平硬币抛掷结果的自信息恰好是 1 bit,而记录这个结果恰好需要 1 个存储 bit。但这只是巧合——一个概率 0.1 的事件自信息约 3.32 bit,但并不意味着需要 3.32 个存储位来记录它。信息论 bit 是"不确定性的度量",存储 bit 是"物理载体"。两者的桥梁是香农第一定理:最优编码的平均长度趋近于熵。

Q3: 熵能取负数吗?熵为零的时候意味着什么?

对于离散随机变量,熵绝不可能是负数——因为 0 ≤ p(x) ≤ 1,-log p(x) ≥ 0,加权平均自然也 ≥ 0。熵 = 0 意味着随机变量实际上是确定性的:存在某个 x 使得 p(x)=1,其余为 0。在这种状态下,没有任何不确定性——你 100% 知道会发生什么。这是"最不随机"的分布。另一个极端:熵最大时对应均匀分布,这是"最随机"的状态。注意:对于连续随机变量,微分熵(differential entropy)可以为负——例如在 [0, 0.5] 上的均匀分布微分熵 = -1 bit。这是因为连续域的"信息密度"和离散域的"信息量"有着本质区别。

Q4: 信息熵和物理学中的"熵"(热力学熵)有关系吗?

有深刻的关系。香农在命名"信息熵"时,据说受到了冯·诺依曼的建议——冯·诺依曼说:"你应该叫它熵,有两个原因。第一,你的不确定性函数在统计力学中已经以那个名字出现了。第二,也是更重要的——没人真正懂熵是什么,所以你在争论中永远占优势。"这段轶事虽有争议,但反映了两个熵之间的真正联系:热力学熵衡量物理系统的"混乱程度"(微观状态的对数),信息熵衡量随机变量的"不确定性"。两者在数学形式上完全一致(都涉及∑p·log p),而且在统计力学中,玻尔兹曼熵 S = k·lnΩ 和信息熵只差一个常数倍(玻尔兹曼常数 k·ln 2)。Maxwell妖悖论和兰道尔原理更是证明了信息和物理熵之间的可转换关系。

Q5: 在实际机器学习项目中,熵作为一个指标具体怎么用?

三个最常见的场景。(1) 探索性数据分析(EDA)阶段:计算每个分类标签的熵,熵接近 log2(K) 说明类别均衡,否则需要考虑类别不平衡问题;计算每个特征的信息增益,快速筛选有区分度的特征。(2) 模型训练阶段:监控训练过程中预测分布的熵——如果熵快速趋于零,说明模型过于自信可能过拟合;如果熵始终很高,说明模型学习不充分。(3) 模型评估阶段:对于概率输出模型(如Softmax分类器),计算每个样本预测的熵,高熵样本是"模型不确定的样本",可以作为人工复核或主动学习的候选。几个具体的数值参考:对于 K=10 的多分类问题,均匀分布熵 = log2(10) ≈ 3.32 bit,良好训练的模型典型预测熵在 0.1~0.5 bit 之间。

章节单词汇总

英文音标术语/释义
entropy/ˈentrəpi/信息熵,随机变量不确定性的度量
self-information/self ˌɪnfərˈmeɪʃn/自信息,单个事件的信息量
bit/bɪt/比特,信息量的基本单位
nat/næt/奈特,以e为底的信息量单位
uncertainty/ʌnˈsɜːrtnti/不确定性
probability/ˌprɑːbəˈbɪləti/概率
information gain/ˌɪnfərˈmeɪʃn ɡeɪn/信息增益
label smoothing/ˈleɪbl ˈsmuːðɪŋ/标签平滑,熵正则化技术
one-hot/wʌn hɑːt/独热编码
active learning/ˈæktɪv ˈlɜːrnɪŋ/主动学习

面试练习

Q1 [单选] 信息熵 H(X) 衡量的是什么?

  • A. 随机变量取值的大小
  • B. 随机变量的平均不确定性
  • C. 随机事件的概率大小
  • D. 随机变量的取值个数
解答:信息熵是随机变量不确定性的度量,定义为自信息的数学期望 H(X) = -Σp(x)log₂p(x)。它不关心具体的取值大小,而是描述"我们对X的取值有多不确定"。取值个数只影响熵的上界,不是熵本身。

Q2 [单选] 一个随机变量有4种可能取值,概率分别为 (0.5, 0.25, 0.125, 0.125),其熵是多少 bit?

  • A. 1.0
  • B. 1.5
  • C. 1.75
  • D. 2.0
解答:H = -(0.5·log₂0.5 + 0.25·log₂0.25 + 0.125·log₂0.125 + 0.125·log₂0.125) = -(0.5·(-1) + 0.25·(-2) + 0.125·(-3) + 0.125·(-3)) = 0.5 + 0.5 + 0.375 + 0.375 = 1.75 bit。

Q3 [单选] 在决策树中,信息增益为零意味着什么?

  • A. 该特征的取值最多
  • B. 该特征与标签完全无关
  • C. 使用该特征分裂后,子节点的熵加权和等于父节点熵
  • D. 父节点的熵为零
解答:信息增益 IG = H(parent) - Σ(|Dv|/|D|)·H(Dv) = 0 意味着加权子节点熵恰好等于父节点熵——即该特征对分类完全没有帮助,分裂后的子节点和父节点一样混乱。这时决策树不会选择这个特征分裂。

Q4 [单选] 以下哪个随机变量的熵最大?

  • A. 概率分布为 (0.9, 0.1) 的二元变量
  • B. 概率分布为 (0.5, 0.5) 的二元变量
  • C. 概率分布为 (1/3, 1/3, 1/3) 的三元变量
  • D. 概率分布为 (1.0, 0.0) 的二元变量
解答:A的熵 = 0.469 bit,B的熵 = 1 bit,C的熵 = log₂3 ≈ 1.585 bit,D的熵 = 0 bit。均匀分布时熵最大,且取值越多最大熵也越大。三元均匀分布的熵 > 二元均匀分布 > 偏倚分布 > 确定性分布。

Q5 [多选] 以下哪些是信息熵的数学性质?

  • A. 非负性:H(X) ≥ 0
  • B. 上界性:H(X) ≤ log₂|𝒳|
  • C. 线性性:H(aX) = aH(X)
  • D. 凹性:H(λP+(1-λ)Q) ≥ λH(P)+(1-λ)H(Q)
解答:A正确,熵永远非负(离散情况)。B正确,熵的最大值不超过log₂(取值个数)。C错误,熵不是线性的——H(aX)对于缩放变换没有简单的线性关系。D正确,熵是概率分布的凹函数,两个分布的混合比各自加权平均熵更大(不确定性增加)。

Q6 [单选] 关于自信息 I(x) = -log₂p(x),以下说法正确的是?

  • A. 当 p(x)=1 时,I(x)=0
  • B. 当 p(x)=0.5 时,I(x)=0.5 bit
  • C. I(x) 与 p(x) 成正比
  • D. I(x) 表示事件发生的可能性
解答:p=1时 I=0(必然事件无信息),A正确。p=0.5时 I = -log₂(0.5) = 1 bit,不是0.5,B错误。自信息与p(x)成反比而非正比(概率越小信息量越大),C错误。I(x)表示事件的信息量而非可能性,D错误。

Q7 [多选] 关于经验熵估计,以下哪些说法是正确的?

  • A. 小样本下经验熵会系统性低估真实熵
  • B. 用频率替代概率是一种极大似然估计
  • C. 经验熵永远等于真实熵
  • D. 零频数对应的项贡献为0
解答:A正确,稀有事件在样本中未出现导致低估。B正确,频率是概率的MLE(极大似然估计)。C错误,经验熵是真实熵的估计,有偏差。D正确,0·log(0)在极限意义下为0。

Q8 [单选] Label Smoothing(标签平滑)的作用是什么?

  • A. 提高模型的训练速度
  • B. 防止模型对训练标签过度自信,增加预测分布的熵
  • C. 减少模型的参数量
  • D. 增加训练数据的数量
解答:Label Smoothing将硬标签(one-hot)变为软标签(如 [0.9, 0.05, 0.05]),迫使模型不要对任何一个类别赋予100%的信心。它增加了标签分布的熵,使模型更加保守和鲁棒,有效缓解过拟合。不改变训练速度、参数量和训练数据量。

Q9 [单选] 在主动学习(Active Learning)中,通常选择哪种样本优先标注?

  • A. 模型预测置信度最高的样本
  • B. 模型预测分布熵最大的样本
  • C. 模型预测置信度最低的样本
  • D. 随机选择的样本
解答:主动学习的核心策略是"不确定性采样"——选择模型最不确定的样本来标注,以最大化每次标注的信息收益。熵最大的样本意味着模型对类别的判断最犹豫,标注后能提供最多的学习信号。注意B和C看似相似但B更准确:最低置信度和最大熵通常一致但并非等价——当有多个类别时,最大熵是最合理的"不确定性"度量。

Q10 [多选] 以下哪些算法或技术直接使用信息熵?

  • A. 决策树ID3算法的分裂准则
  • B. t-SNE降维中的困惑度(perplexity)
  • C. 强化学习中的策略熵奖励(entropy bonus)
  • D. 评估语言模型的困惑度(perplexity)
解答:A正确,ID3直接使用信息增益(基于熵)。B正确,t-SNE中困惑度 = 2^H(Pi),其中H是条件分布的熵。C正确,熵奖励 = -λ·Σπ(a|s)logπ(a|s),鼓励策略保持探索。D正确,语言模型的困惑度 = 2^H,H是每个词的平均交叉熵。四个选项都正确——熵在AI中的应用非常广泛。