3. 概率模型数值下溢的log域修复
问题定位:连乘为什么变成 0
一个含 12 个二值变量的朴素贝叶斯模型,联合概率为各因子概率的连乘。若每个因子概率约为 0.1,则联合概率约为 10⁻¹²;变量继续增多时,双精度浮点数的下溢阈值约为 5×10⁻³²⁴,连乘结果会直接变成 0。此时后验概率计算出现 0/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。必须先对每个因子单独取对数,再求和。