概述与定位

一句话定位: 因果发现算法实现中存在多个常见陷阱,本笔记系统总结典型bug和超参数调优策略。

** prerequisites **

  • 因果发现算法基础(PC、GES、NOTEARS)
  • 数值优化基础
  • Python调试经验
  • 线性代数基础

核心直觉

因果发现算法的实现比看起来困难得多——不仅涉及核心算法逻辑,还涉及大量数值细节。忽略这些数值细节会导致:(1)结果违反DAG约束;(2)算法不收敛;(3)假阳性/假阴性率异常高。

为什么需要系统调试的核心直觉:因果发现算法是敏感性较高的算法——数据分布的变化、超参数的选择、随机种子的设置都会显著影响结果。系统化的调试流程和超参数范围知识,是成功复现论文结果的关键。

调参策略的核心直觉:不同算法的超参数有不同物理含义——alpha控制CI测试的严格程度,lambda控制NOTEARS的稀疏性,max_iter控制优化收敛性。理解这些物理含义,才能有针对性地调参。


常见Bug分类

1. DAG约束相关

Bug: NOTEARS返回的W违反DAG约束

原因: 优化未收敛,或约束惩罚系数太小

诊断:

from scipy.linalg import expm
 
def check_dag(W):
    """检查W是否对应DAG"""
    n = W.shape[0]
    M = expm(W * W)  # matrix exponential
    h = np.trace(M) - n
    return h, h < 1e-6
 
h, is_dag = check_dag(W)
print(f"DAG constraint value: {h}, Is DAG: {is_dag}")

解决方案:

  1. 增加迭代次数 max_iter
  2. 增加约束惩罚系数
  3. 使用更小的学习率
  4. 添加W的shrinkage: W = W * (1 - shrinkage)

Bug: PC算法输出包含循环边

原因: 骨架学习和方向确定步骤之间不一致

诊断:

import networkx as nx
 
def check_cycles(G):
    """检查图是否有循环"""
    try:
        nx.find_cycle(G, directed=True)
        return True  # 有循环
    except nx.NetworkXNoCycle:
        return False  # 无循环

解决方案: 使用拓扑排序验证,或在方向确定后增加循环检测

2. CI测试相关

Bug: PC算法遗漏真边(假阴性)

原因: alpha太小或样本量不足

诊断:

# 比较不同alpha的结果
for alpha in [0.01, 0.05, 0.1, 0.2]:
    skeleton = pc_skeleton(data, alpha=alpha)
    print(f"alpha={alpha}, n_edges={count_edges(skeleton)}")

解决方案:

  1. 放宽alpha: 0.05 → 0.1
  2. 增加样本量
  3. 使用更鲁棒的CI测试(GP-based)

Bug: PC算法产生过多假边(假阳性)

原因: alpha太大或CI测试不准确

诊断:

# 比较骨架和ground truth
shd = structural_hamming_distance(skeleton, true_dag)
print(f"SHD: {shd}")

解决方案:

  1. 收紧alpha: 0.1 → 0.01
  2. 使用Fisher’s Z变换进行CI测试
  3. 增加条件集大小的上限

3. 数值稳定性相关

Bug: 矩阵求逆数值不稳定

原因: 协方差矩阵接近奇异

诊断:

# 检查条件数
cond = np.linalg.cond(cov_matrix)
print(f"Condition number: {cond}")
if cond > 1e10:
    print("Warning: Matrix is nearly singular")

解决方案:

# 添加正则化
cov_reg = cov_matrix + 1e-6 * np.eye(n)
cov_inv = np.linalg.inv(cov_reg)

Bug: NOTEARS梯度爆炸

原因: 学习率太大

诊断:

# 监控W的范数
W_norm = np.linalg.norm(W)
print(f"W norm: {W_norm}")
if np.isnan(W_norm) or W_norm > 1e10:
    print("Warning: Gradient explosion")

解决方案:

# 使用梯度裁剪
grad = np.clip(grad, -1.0, 1.0)
# 或降低学习率
lr = 0.001  # 降低到 0.0001

4. 数据相关

Bug: 不同数据规模下算法表现差异大

原因: 算法对数据量敏感度不同

诊断:

# 测试不同数据规模
for n_samples in [100, 500, 1000, 5000]:
    data = generate_synthetic(n_samples=n_samples)
    skel = pc_skeleton(data)
    shd = evaluate(skel, true_dag)
    print(f"n={n_samples}, SHD={shd}")

解决方案:

  1. PC算法: 至少需要 样本
  2. NOTEARS: 样本量越大越稳定
  3. GES: 对数据量相对鲁棒

超参数调优指南

NOTEARS调参

参数范围物理含义调参建议
lambda10.01-1.0L1正则强度,控制稀疏性从0.1开始,逐步减小
max_iter100-10000最大迭代次数设置5000保证收敛
w_threshold0.0-0.3边阈值,控制稀疏性从0.2开始调
h_tolerance1e-6-1e-3DAG约束容差通常默认1e-6即可

GES调参

参数范围物理含义调参建议
scoring_funcBIC/MDL评分函数通常用BIC
max_sz3-5最大父节点数从3开始,太大计算慢
epsilon1e-8-1e-4插入删除阈值影响搜索精度

PC算法调参

参数范围物理含义调参建议
alpha0.01-0.2CI测试显著性水平从0.05开始
indep_test’fisherz’/‘gp’CI测试方法gp更鲁棒但更慢
max_cond_set3-10最大条件集大小太大会组合爆炸

调试流程

标准调试流程

1. 数据验证
   - 检查数据是否包含缺失值
   - 检查数据方差是否接近零
   - 检查数据是否标准化

2. 随机种子验证
   - 设置所有随机种子
   - 多次运行检查结果稳定性

3. 超参数扫描
   - 使用默认超参数运行
   - 在推荐范围内扫描
   - 检查结果是否合理

4. 与baseline比较
   - 与gcastle/bnlearn实现比较
   - 与ground truth比较

5. 梯度检查(如适用)
   - 使用数值梯度验证分析梯度
   - 检查梯度是否合理

复现注意事项

关键检查点

  1. 数据预处理: 标准化通常必要,但需记录方法
  2. 随机种子: 所有随机操作都需要设置种子
  3. 结果验证: 检查结果是否满足DAG约束
  4. 性能基线: 与成熟库的结果对比

常见陷阱

  • 遗漏随机种子: 导致结果不可复现
  • alpha选择不当: 显著影响PC算法结果
  • lambda选择不当: NOTEARS对lambda敏感
  • 数据泄露: 训练测试集划分不当

稳定性测试

def stability_test(algorithm, data, n_runs=10):
    """测试算法稳定性"""
    results = []
    for seed in range(n_runs):
        np.random.seed(seed)
        result = algorithm(data)
        results.append(result)
    
    # 计算结果的一致性
    consensus = compute_consensus(results)
    return consensus

本章要点总结

  • DAG约束违反是NOTEARS最常见的问题
  • CI测试的alpha显著影响PC算法结果
  • 数值稳定性是实现质量的关键指标
  • 矩阵条件数检查防止求逆不稳定
  • 梯度裁剪防止NOTEARS梯度爆炸
  • 超参数范围因算法而异,需针对性调优
  • 多次运行验证算法稳定性
  • 与成熟库对比验证实现正确性
  • 数据预处理标准化需记录方法
  • 调试流程应系统化逐步排查

关键词

因果发现调试 DAG约束 NOTEARS PC算法 超参数调优 数值稳定性 bug修复 稳定性测试 参数选择 算法调试