3. 概率模型数值下溢的log域修复

问题定位:连乘为什么变成 0

一个含 12 个二值变量的朴素贝叶斯模型,联合概率为各因子概率的连乘。若每个因子概率约为 0.1,则联合概率约为 10⁻¹²;变量继续增多时,双精度浮点数的下溢阈值约为 5×10⁻³²⁴,连乘结果会直接变成 0。此时后验概率计算出现 0/0,输出全为 0 或 NaN。

关键判断:单个因子概率正常,但联合概率随变量数指数衰减,这是数值下溢的典型特征,而非模型结构错误。

原始路径:概率域连乘 → 下溢为 0 因子 p₁ ≈ 0.1 因子 p₂ ≈ 0.1 因子 p₃ ≈ 0.1 … 因子 p₁₂ ≈ 0.1 联合概率 P ≈ 10⁻¹² → 0 下溢阈值 ≈ 5×10⁻³²⁴;变量数增加时连乘结果指数衰减,后验计算出现 0/0 → 输出全 0 或 NaN 修复路径:迁移到对数域 → log-sum-exp 归一化 log p₁ ≈ -2.30 log p₂ ≈ -2.30 log p₃ ≈ -2.30 … log p₁₂ ≈ -2.30 log P ≈ -27.6(有限) log-sum-exp 归一化(数值稳定) m = max(log Pᵢ) → log Z = m + ln Σ exp(log Pᵢ − m) → log 后验ᵢ = log Pᵢ − log Z 减去最大值 m 后指数项 ≤ 1,避免 exp 上溢;log Z 有限,后验概率可正常还原 输出:后验概率向量之和为 1,不再出现 0/0 或 NaN

修复实现:log 域连乘与 log-sum-exp

核心改动只有两点:把每个因子概率取对数后相加(而非相乘),再用 log-sum-exp 计算归一化常数。下面给出可直接运行的 Python 实现,包含原始路径与修复路径的对照。

import numpy as np

# 教学示例:12 个二值变量的因子概率(示例数据,非实测)
factors = np.array([0.1, 0.12, 0.09, 0.11, 0.1, 0.08,
                    0.13, 0.1, 0.11, 0.09, 0.12, 0.1])

# 原始路径:概率域连乘
joint_naive = np.prod(factors)
print("原始联合概率:", joint_naive)          # 预期约 1e-12 量级,变量更多时变为 0

# 修复路径:对数域求和
log_factors = np.log(factors)
log_joint = np.sum(log_factors)
print("log 联合概率:", log_joint)            # 有限值,约 -27.6

# log-sum-exp 归一化(对多个候选状态的 log 概率做归一化)
def log_sum_exp(log_probs):
    m = np.max(log_probs)
    return m + np.log(np.sum(np.exp(log_probs - m)))

# 示例:3 个候选状态的 log 联合概率
log_candidates = np.array([log_joint, log_joint - 1.5, log_joint - 3.0])
log_z = log_sum_exp(log_candidates)
log_posterior = log_candidates - log_z
posterior = np.exp(log_posterior)

print("log Z:", log_z)
print("后验概率:", posterior)
print("后验和:", posterior.sum())            # 预期为 1.0

结果判读与验证

  • 原始路径:joint_naive 在变量数较少时仍为正小数;变量数增大到约 12 个以上、因子概率约 0.1 时,结果趋近 0,后验计算出现 0/0。
  • 修复路径:log_joint 为有限负数(示例约 -27.6),log_z 同样有限,posterior 各元素为正且和为 1。
  • 一致性校验:对同一组候选状态,用高精度库(如 mpmath)计算概率域结果再取对数,应与 log 域结果在浮点误差范围内一致。
  • 边界检查:若某因子概率为 0,np.log(0) 产生 -inf,需在取对数前加极小值 eps 或显式处理零概率事件。
log-sum-exp 的关键是减去最大值 m:这样 exp(log Pᵢ − m) 的最大值为 1,不会上溢;同时 log Z 保持有限,后验概率可稳定还原。
不要只把连乘改成 np.log(np.prod(...))——乘积在取对数之前已经下溢为 0,log(0) 仍是 -inf。必须先对每个因子单独取对数,再求和。