4. 条件概率表归一化错误的检测与修正
问题定位:手工录入的 CPT 行和不为 1
条件概率表(CPT)的每一行对应父变量的一种取值组合,行内各子变量取值的概率之和必须等于 1。手工录入时常见的错误是:漏填某个取值、把 0.3 写成 0.03、或者把两个取值写反导致行和变成 0.9 或 1.1。下面这张图展示了一个 3 行 CPT 的校验流程——逐行求和、与 1 比较、定位偏差行、按比例修正并复验。
校验脚本:逐行求和、定位、修正
下面用 Python 实现一个可复用的 CPT 校验器。输入是一个字典,键为父变量取值,值为子变量各取值的概率列表。脚本会逐行求和,标记偏差超过容差的行,并按比例缩放修正。
import math
def validate_cpt(cpt, tol=1e-6):
"""
cpt: dict[str, list[float]]
键 = 父变量取值,值 = 子变量各取值的概率列表
返回: (是否全部合法, 问题行列表, 修正后的 cpt)
"""
bad_rows = []
fixed = {}
for parent_val, probs in cpt.items():
s = sum(probs)
if abs(s - 1.0) > tol:
bad_rows.append((parent_val, probs, s))
# 按比例修正
fixed[parent_val] = [p / s for p in probs]
else:
fixed[parent_val] = list(probs)
return len(bad_rows) == 0, bad_rows, fixed
# 示例:手工录入的 CPT(父变量=天气,子变量=是否带伞)
cpt_raw = {
"晴": [0.2, 0.8],
"雨": [0.9, 0.3], # 错误行:和 = 1.2
"阴": [0.5, 0.5],
}
ok, bad, fixed = validate_cpt(cpt_raw)
print("全部合法:", ok)
for pv, probs, s in bad:
print(f"问题行: 父={pv}, 概率={probs}, 行和={s:.4f}")
print("\n修正后 CPT:")
for pv, probs in fixed.items():
print(f" {pv}: {[round(p, 4) for p in probs]}, 行和={sum(probs):.6f}")
运行输出(示例预期结果):
全部合法: False
问题行: 父=雨, 概率=[0.9, 0.3], 行和=1.2000
修正后 CPT:
晴: [0.2, 0.8], 行和=1.000000
雨: [0.75, 0.25], 行和=1.000000
阴: [0.5, 0.5], 行和=1.000000
修正策略选择:按比例缩放(p_i / Σp)保持行内各取值的相对大小关系,适合“录入笔误”类错误。如果怀疑是某个取值漏填或写错,应先核对原始数据再决定是否缩放,避免把真实错误“抹平”。对于二值变量,也可以只修正一个取值(如 p₂ = 1 − p₁),但需明确哪个取值更可信。
验证与边界检查
修正后必须复验:每行和与 1 的偏差不超过容差,且每个概率值落在 [0, 1] 区间。若原始数据中存在负值或大于 1 的值,按比例缩放无法修复,应直接报错并回溯录入源。
| 检查项 | 条件 | 处理方式 |
|---|---|---|
| 行和偏差 | |Σp − 1| > 1e-6 | 标记问题行,按比例缩放 |
| 概率越界 | p < 0 或 p > 1 | 报错,不自动修正,回溯录入源 |
| 空行 | 概率列表为空 | 报错,父变量取值缺少子变量分布 |
| 复验 | 修正后所有行 |Σp − 1| ≤ 1e-6 | 通过,输出修正后 CPT |
常见陷阱:浮点累加误差可能让 0.1 + 0.2 + 0.7 得到 0.9999999999999999,此时不应判为错误。容差 1e-6 足以覆盖双精度浮点的累加误差,又不会放过真实的录入错误(通常偏差在 0.01 以上)。