想象你是业主,一套老房子的墙体施工图早已丢失。你请来装修队,却只能说清“家具怎么摆”:沙发靠东墙、书桌在南窗下、冰箱紧挨餐桌——而你想知道的是:墙在哪里,门朝哪边开?这就是 GES 算法面对的世界:手里只有观测数据(家具摆放),要反推变量间的因果结构(墙与门向)。 装修队的策略很朴素:先大胆试砌几堵墙(加边阶段),看居住体验(评分)是否变好;砌得差不多后,再回头拆掉让房子更拥挤的墙(删边阶段)。每一步都端着“改造评分表”(BIC 准则):改动让分涨就保留,让分跌就放弃。由于方案太多,装修队只能一堵一堵地试,这就是“贪婪”。 最后交付的不是唯一“定稿图”,而是一份“标准施工图”(CPDAG):能确定的墙画实线,方向未定的画虚线——纯观测数据下,有些方向天生无法区分。GES 的价值,正是在爆炸的候选结构中用“贪心 + 评分”逼近真实因果骨架。
对 AI 从业者而言,GES 是从观测数据自动学习因果结构的核心算法之一。掌握它能帮你把“相关关系”升级为“可验证的因果假设”,在因果推断、可解释 AI 和反事实分析岗位上获得核心竞争力。
可视化演示
展示GES贪婪等价搜索算法,从空图出发前向加边最大化BIC评分提升,再后向删边优化,在马尔可夫等价类CPDAG空间搜索最优因果结构。
本节配有交互动效(进入视口后自动加载,约 3KB)。
GES贪婪等价搜索 前向加边:从空图出发,每步选BIC增益最大的Insert操作 后向删边:从当前图出发,每步选BIC增益最大的Delete操作 等价类搜索:在CPDAG空间搜索,保证评分等价性 GES: Empty → Insert(max BIC) → Delete(max BIC) → CPDAG GES在等价类空间贪婪搜索最优因果结构
算法背景
装修队接到的第一个任务是勘察现场。他们面对一间“只摆家具、不见墙体”的房间:沙发、书桌、冰箱、餐桌彼此挨着,看起来都有些关系——可到底是“隔墙相望”还是“同一房间”?业主只给了几百张不同角度的照片(样本数据),没有任何施工记录。
因果发现要做的,正是从这类观测数据中恢复变量间的因果图:每个变量是一个“房间”,每条边是一堵“墙”,箭头方向是“门只能朝哪边开”。难题有两个:其一,观测数据只能暴露相关性,而相关不等于因果;其二,候选墙体方案的数量实在太惊人,远远超出人力枚举的极限。
对 p 个变量,可能的 DAG 结构数量随 p 呈超指数爆炸。大到什么程度呢?就像几十个房间的豪宅,可能的墙体布置比宇宙中的原子还多,逐一遍历绝无可能。于是 GES 选择了一条务实的路:从某个初始方案出发,每次只改动一堵墙,并借助评分判断改动好坏——这种“只看眼前一步”的策略,正是贪婪搜索。
之所以必须“贪婪”,就是因为户型方案海实在太大。装修队不奢望一步到位,而是相信“每一步都变好一点,最后总不会太差”——这正是 GES 的设计哲学。
- 结构空间全体候选 DAG 结构的数量级,决定了搜索的困难程度:
- 变量个数数据集中的变量数量,对应户型中待确定的“房间”数:
- 指数上界候选有向边总数在 p² 量级,每条边可取可不取,造成组合爆炸:
等价类概念
勘察几天后,装修队发现一个有趣的规律:有些墙体方案看起来完全不同,但住起来的采光和动线一模一样。比如客厅与书房之间的墙,无论门开在左边还是右边,只要“书房必须经过客厅才能到达”这一连通事实不变,居住体验就无法区分。
对应到因果图上:两个 DAG 只要骨架相同(把有向边全部变成无向边后的连通结构)、V 结构相同(A→B←C 这样“两头指向中间”的局部模式,且 A、C 之间没有边),它们在任意观测数据上产生的概率分布就完全相同——任何数据都无法把两者区分开。它们属于同一个马尔可夫等价类。
装修队从此不再纠结“门开左还是开右”:只要采光格局相同,就当同一类处理。最终交图时,把能确定方向的门画实线,把闹不清的画虚线——GES 输出的 CPDAG,就是这份诚实标注的“标准施工图”。
- 等价符号表示两个 DAG 在任意观测数据上产生相同分布,统计上不可区分:
- 骨架一致把有向边全部改成无向边后,变量间的连通关系完全相同:
- V 结构一致形如 A→B←C 的碰撞结构集合完全相同,这是方向能否确定的关键:
- 同时满足两个条件必须同时成立,缺一不可,共同定义马尔可夫等价:
评分函数
面对五花八门的墙体方案,装修队必须有一本统一的“改造评分表”,否则没法比较两堵新墙谁更值得砌。评分表回答两个问题:新方案住起来舒服吗?工程量大不大、成本高不高?
GES 使用的评分表是 BIC(贝叶斯信息准则,Bayesian Information Criterion)。它把“住得舒服”量化为对数似然:模型解释数据解释得越好,分越高;把“工程成本”量化为参数复杂度:图的边越多、条件概率表越庞大,扣分越狠。
BIC 属于“一致的”评分:样本量足够大时,真实等价类的评分永远不会被其他类压过。这一点是 GES 搜索能够收敛到正确方向的地基。加边与删边阶段,每走一步都在问同一句话:“这堵墙加(拆)之后,BIC 是涨还是跌?”
- 对数似然模型在最优参数估计下解释观测数据的对数概率,反映拟合度:
- 参数个数模型中全部条件概率分布的自由参数总数,反映结构复杂度:
- 惩罚力度样本量的对数乘以参数个数的一半,控制对复杂模型的扣分:
- 作差求分用拟合收益减去复杂度代价,得到最终的 BIC 评分:
加边阶段
装修队第一阶段叫“大胆砌墙”:在任意两个房间之间试着砌一堵新墙、开一扇新门(加一条有向边),每砌一堵立刻掏出评分表打分。注意不能砌出环形走廊——一旦 A 房经 B 房到 C 房再绕回 A 房,户型就不再是合理分区(不允许成环)。
加边阶段是典型的贪心:遍历所有候选边,分别计算“加上这条边后的 BIC 增量 ΔBIC”,挑出增量最大的那一条真正加上,然后重复这个过程。
当所有候选边带来的最大增量都不超过零时,加边阶段终止。此时图停在了一个“评分小山丘”上——但尚未登顶之前,谁也不知道这座山丘是不是最高的。就像装修队一口气砌了十几堵墙,偶尔也会砌错一堵:单看涨分,等整体格局定下来却成了采光杀手。别担心,第二阶段的“拆墙工”正在进场。
- 候选边当前图中尚未出现的一条有向边,即“待砌的新墙”:
- 增广图假设把候选边加入后得到的新因果图:
- 评分增量新图与旧图 BIC 评分的差值,衡量这一步改动值不值得:
- 接受条件只有增量大于零的边才会被采纳,否则放弃该候选:
删边阶段
砌墙阶段结束后,装修队并未交工,而是退后三步重新审视整张平面图:有些墙当初“单独看”是涨分的,但放进整体格局后反而成了累赘。于是“细心拆墙”开始:把每条已砌的墙轮流拆掉,看评分怎么变。
删边阶段同样贪心:对当前图中每条已存在的边 e,计算删除后的评分增量;如果删除某条边后 BIC 反而上升,就说明它是冗余边——拆!每轮只拆掉“删后增益最大”的那条边,直到拆任何边都不能让评分变得更好为止。
删边有个漂亮的理论性质:加边与删边的评分变化互为相反数——先把边 e 砌上再拆掉,收益正好抵消。这保证删边阶段不会把搜索“带偏”,而是对加边阶段留下的局部最优点做精细修正,在评分一致条件下能收敛到全局最优等价类。拆墙工人每拆一堵都要反问评分表:“拆了它,住得更舒服吗?”直到拆无可拆,装修队才确信自己真正站在了评分山顶。
- 现有边集当前图中所有已存在的有向边,即“已砌的墙”:
- 删除候选从当前图中临时去掉一条边后得到的新图:
- 删边增益删除后的评分减去删除前的评分,正值表示拆得值:
- 贪心选择遍历整个边集,选增益最大且为正的边优先拆除:
算法流程
把整场装修拍成一部短片:清晨的空房子(空图)→ 工人们来回比划、砌墙(加边阶段)→ 监理退后打量、下令拆墙(删边阶段)→ 最后交出一张标准施工图(CPDAG)。
GES 的完整流程是:第一步,从一个没有边的初始 DAG 出发;第二步,反复执行加边阶段,每次添加“能让 BIC 增量最大且不形成环”的边,直到无法再提分;第三步,反复执行删边阶段,每次删除“能让 BIC 提升最大”的边,直到无法再提分;第四步,把最终的 DAG 骨架与可定向的 V 结构整理成 CPDAG 输出。
两个阶段各自的终止条件完全对称——没有任何单边操作能继续提升评分:
从复杂度看,每轮都要枚举 O(p²) 个候选操作,每个候选做 O(n) 量级的评分拟合,整体开销约 O(k·p²·n)。如此简单的“导演指令”——涨分就动、不涨就停——却能从天文数字级的户型方案里摸出一张靠谱施工图,这正是贪心策略的力量。
- 候选操作集当前可执行的全部加边或删边操作:
- 最大增益所有候选操作中最大的评分变化值:
- 非正判断最大增益都不超过零,说明任何改动都不再有益:
- 阶段收敛触发该条件后,当前阶段停止并进入下一阶段:
优势局限
对比别的装修队,这套“先砌后拆”的打法优势明显:它不会一上来就拍脑袋拆掉一堆墙(像某些只做局部检验的队伍),而是先充分探索再回头修正,全局风险更小。更重要的是,它的评分表有统计保证。
当样本量足够大时,GES 能以概率 1 收敛到真实的因果等价类——这个性质称为一致性。换句话说,只要数据够多、模型假设合理,它几乎不会把因果骨架搞错。
局限同样真实:一是计算开销大,变量增多后候选枚举与评分拟合相当昂贵;二是贪心搜索可能陷入局部最优,数据有噪声时可能停在某个“小山顶”;三是最本质的一点——等价类内部的方向,仅靠观测数据永远无法区分。装修队不是神:它能确定哪些墙存在、哪些门确定朝向,但对那些“怎么画都说得通”的虚线墙,只能诚实标注“待业主砸墙验证”。这份诚实,正是因果发现与单纯相关性建模的最大区别。
- 样本量观测数据包含的记录条数,越多提供的信息越充分:
- 估计结果基于 n 条样本由 GES 学到的因果等价类:
- 真实答案数据背后真实存在的那一个因果等价类:
- 概率极限n 趋向无穷时,估计结果等于真实答案的概率趋近于 1:
对比应用
装修市场上还有一支风格迥异的队伍——PC 施工队。它不拿评分表,而是拿着“探测器”:对每一对房间做条件独立性检测,凡是被证明“中间隔着墙”(条件独立)的,直接拆掉候选墙;最后再根据 V 结构确定门朝哪边开。
PC 算法的思路是“由全到空”:从完全图开始逐步删边;GES 的思路是“由空到满再回头”:从空图开始逐步加边再删边。两者方法论不同:PC 依赖独立性检验的先后次序,误差可能累积;GES 依赖评分的一致性,对搜索路径更鲁棒,但计算量更大、更慢。
工程上,causal-learn、pcalg、TETRAD 都内置了 GES 与 PC 的实现,可直接调用。选哪支队伍?户型小、时间紧、数据质量高,PC 更省时;户型大、追求统计保证、算力充裕,GES 更稳妥。聪明的团队常常让两支队伍互相印证——它们都说“有墙”的地方,才真正值得砸墙验证。
- 候选数量每轮迭代要枚举的加边/删边候选在 p² 量级:
- 样本遍历每次评分计算需要扫过 n 条样本做回归拟合:
- 迭代轮数加边与删边两阶段的循环次数,通常远小于变量数:
- 总开销三者相乘给出整体的渐进计算复杂度:
AI 应用场景
金融风控中的可解释归因
阶段:可解释建模 / 因果特征工程
解决的问题:传统评分卡或树模型只能输出违约概率,说不清“为什么”。GES 从用户历史行为与征信观测数据中学习变量间的因果骨架,把“收入低→负债率高→逾期”这类因果链条显式画出来,为模型决策提供可复核的解释。
具体案例:某消费金融公司对 30 个特征、约 200 万样本运行 GES,输出 CPDAG 显示“负债率”是“逾期”的直接父节点,而“年龄”只是经由“收入”间接影响逾期。风控团队据此把负债率纳入贷前硬规则,拒绝率下降 12% 且坏账率没有上升。
电商转化瓶颈的因果诊断
阶段:归因分析 / 数据驱动决策
解决的问题:转化率下滑时,业务方常靠经验猜原因,容易把相关变量当成因果变量。用 GES 从埋点日志中学习“页面加载时长、跳出率、加购率、转化率”的因果图,定位真正的因变量。
具体案例:某电商团队对过去 90 天漏斗数据跑 GES,发现“加载时长→跳出率→转化率”是一条直达因果路径,而“促销位数量”与转化率只是相关关系。优化加载速度后转化率回升了 8%。
医学观察性研究的混杂识别
阶段:观察性数据分析 / 混杂控制
解决的问题:电子病历数据不允许随机实验,药物与疗效之间的因果关系常被年龄、并发症等混杂因素干扰。GES 帮助研究者先学习整个变量系统的因果图,再决定统计模型需要调整哪些变量。
具体案例:在回顾性队列研究中,用 GES 对“用药剂量、肝功能指标、年龄、转归”建模,CPDAG 提示“肝功能”是“用药”与“转归”的共同祖先,需纳入调整集,从而避免了混杂偏倚。
Python 演示
import itertoolsimport numpy as np def bic_score(data, parents, node): """node 在给定父节点集合下的 BIC 局部评分""" n = data.shape[0] y = data[:, node] if not parents: X = np.ones((n, 1)) beta = np.array([y.mean()]) else: X = np.column_stack([np.ones(n)] + [data[:, p] for p in parents]) beta, _, _, _ = np.linalg.lstsq(X, y, rcond=None) resid = y - X @ beta sigma2 = resid @ resid / n ll = -n / 2 * (np.log(2 * np.pi * sigma2) + 1) num_params = len(parents) + 1 return ll - num_params / 2 * np.log(n) class GES: """简化版 GES: 交替执行加边与删边, 直到评分无法提升""" def __init__(self, data): self.data = data self.num_vars = data.shape[1] self.adj = np.zeros((self.num_vars, self.num_vars), dtype=int) def dag_ok(self, adj): """拓扑排序判环: 能访问所有节点即为 DAG""" indeg = adj.sum(axis=0) stack = [i for i in range(self.num_vars) if indeg[i] == 0] visited = 0 while stack: u = stack.pop() visited += 1 for v in range(self.num_vars): if adj[u, v]: indeg[v] -= 1 if indeg[v] == 0: stack.append(v) return visited == self.num_vars def total_score(self, adj): """整图的 BIC 评分按变量分解求和""" score = 0.0 for v in range(self.num_vars): pa = [u for u in range(self.num_vars) if adj[u, v]] score += bic_score(self.data, pa, v) return score def run(self): improved = True while improved: improved = False base = self.total_score(self.adj) best_gain = 0.0 best_adj = None # 加边阶段: 枚举所有候选有向边 for u, v in itertools.permutations(range(self.num_vars), 2): if self.adj[u, v]: continue cand = self.adj.copy() cand[u, v] = 1 if not self.dag_ok(cand): continue gain = self.total_score(cand) - base if gain > best_gain: best_gain = gain best_adj = cand if best_adj is not None: self.adj = best_adj improved = True base = self.total_score(self.adj) best_gain = 0.0 best_adj = None # 删边阶段: 拆掉让评分提升的冗余边 for u, v in itertools.permutations(range(self.num_vars), 2): if not self.adj[u, v]: continue cand = self.adj.copy() cand[u, v] = 0 gain = self.total_score(cand) - base if gain > best_gain: best_gain = gain best_adj = cand if best_adj is not None: self.adj = best_adj improved = True return self.adj def generate_data(n_samples=500, seed=42): """合成线性高斯数据: 真实因果结构为 0->2, 1->2, 2->3""" rng = np.random.default_rng(seed) data = rng.normal(size=(n_samples, 4)) data[:, 2] = 0.8 * data[:, 0] + 0.7 * data[:, 1] + rng.normal(scale=0.3, size=n_samples) data[:, 3] = 0.9 * data[:, 2] + rng.normal(scale=0.3, size=n_samples) return data def main(): data = generate_data() model = GES(data) learned = model.run() true = np.zeros((4, 4), dtype=int) true[0, 2] = true[1, 2] = true[2, 3] = 1 sk_true = (true + true.T) > 0 sk_learned = (learned + learned.T) > 0 match = int(np.sum(sk_true & sk_learned)) print("真实结构(行->列表示因果边):") print(true) print("GES 学习到的结构:") print(learned) print("骨架匹配边数: %d / 3" % match) if __name__ == "__main__": main()
return ll - num_params / 2 * np.log(n)这就是 BIC 的“拟合减去惩罚”:惩罚项随参数个数和样本量增长,防止模型过度复杂。stack = [i for i in range(self.num_vars) if indeg[i] == 0]拓扑排序的起点:找所有“没有入边”的节点,为无环校验做准备。cand = self.adj.copy(); cand[u, v] = 1加边阶段构造候选图:临时添上一条边,但还没决定是否保留。if not self.dag_ok(cand): continue无环约束:砌墙不能砌出环形走廊,否则候选直接作废。gain = self.total_score(cand) - base计算单步评分增益,对应正文中的 ΔBIC。if gain > best_gain贪心决策:只记住当前增益最大的候选边。data[:, 2] = 0.8 * data[:, 0] + 0.7 * data[:, 1] + ...按已知 DAG 合成数据,让“真实答案”可知,便于验证学习效果。
常见误区
误区GES 输出的 CPDAG 里画着箭头,应该所有边的方向都确定了,可以直接拿去做因果解释了吧?
正解CPDAG 中只有 V 结构及其传递方向是确定的,其余边用虚线表示方向未定。同属一个等价类的 DAG 在观测数据上完全不可区分。就像装修图上画虚线的墙,在没有砸墙验证(干预)之前,谁也说不准门到底朝哪边开。
为何常见:看到箭头就默认方向确定,忽略了 CPDAG 中实线与虚线的语义差别。
误区BIC 评分高说明这张图与真实因果关系非常接近,可以放心用它做决策。
正解BIC 衡量的是“拟合度 − 复杂度”,是统计模型选择标准,不是因果正确性的裁判。一张高分图可能在因果方向上完全相反(例如等价类内 X→Y 与 Y→X 评分相同)。装修评分高只说明“住着顺眼且不超预算”,不代表墙的承重布局真的合理。
为何常见:把评分等同于因果真理,忽略了等价类中方向不可识别这一根本事实。
误区既然叫加边阶段,就一条一条加,把所有能加的边都加上,最后再删。
正解加边阶段是贪婪的:每步只加“评分增益最大”的一条边,只要最大增益 ≤ 0 就停止。大多数候选边加进来反而掉分,根本不会被选中。装修队不会把每面墙都砌上——有的墙砌了采光更差,评分表会立刻喊停。
为何常见:把“贪婪”误解为“统统都要”,事实上贪心恰恰是“择优录取、见好就收”。
误区GES 和 PC 都是因果发现算法,数据足够时它们输出的 CPDAG 应该一模一样。
正解GES 是评分搜索法,PC 是基于条件独立检验的约束法,理论基础不同,有限样本下对同一数据可能输出不同的 CPDAG。即便数据无限大且都一致,也只是“收敛到同一等价类”,不代表每一步细节相同。
为何常见:以为所有因果发现算法最终答案相同,忽视了方法论差异和有限样本带来的不确定性。
误区只要样本量足够大,GES 就一定能找到所有边和所有边的方向。
正解观测数据只能把因果结构识别到等价类级别。等价类内部的方向、以及未观测混杂导致的反向因果,是数据的物理极限。GES 的一致性结论说的是“收敛到真实等价类”,而不是“唯一确定每个方向”。想定方向,必须引入干预实验或领域知识。
为何常见:对一致性定理的误读:一致收敛 ≠ 全部方向可识别。
面试题
Q1. GES 算法的两个阶段顺序是什么?
- A.先删边再加边两阶段
- B.先加边再删边两阶段← 正确答案
- C.同时加边和删边同步
- D.只加边不需要删边
答案:B。GES 是“先加边后删边”的两阶段算法:加边阶段从空图出发大胆加边,删边阶段再回头剔除冗余边,最终以收敛的 CPDAG 输出。A、C、D 都与 GES 的两阶段设计不符,故选 B。
Q2. GES 中“等价类”指的是什么?
- A.骨架与 V 结构一致的 DAG 族← 正确答案
- B.评分完全相同的一批变量集
- C.边数完全相等的 DAG 集合
- D.条件概率全部相同的图组
答案:A。马尔可夫等价类的定义是“骨架相同且 V 结构相同”,这样的多个 DAG 在观测数据上不可区分。B 混淆了评分与等价类,C 忽略了方向和 V 结构,D 说的是参数而非结构,故选 A。
Q3. BIC 评分公式由哪两部分构成?
- A.似然项加上谱聚类惩罚项
- B.似然项减去参数复杂度惩罚项← 正确答案
- C.先验概率乘以似然函数项
- D.误差平方和加上常数项
答案:B。BIC 由对数似然项与复杂度惩罚项组成:拟合越好分越高,参数越多扣分越多,二者相减得到总分。A 的谱聚类、C 的先验、D 的误差平方和都不属于 BIC 的构成项,故选 B。
Q4. GES 加边阶段如何选择要加的边?
- A.任选一条无环候选边即可
- B.选择无环且评分增益最大的边← 正确答案
- C.选择边数最少的无环候选边
- D.随机抽一条边评估一次就加
答案:B。加边阶段每一步遍历所有候选边,做无环校验后选择“评分增益最大”的边添加,是典型的贪心策略。A、D 太随意,C 与评分无关,故选 B。
Q5. GES 相比 PC 算法最重要的特点是?
- A.基于评分的全局等价类搜索← 正确答案
- B.基于条件独立检验快速删边
- C.不需要对数据做任何统计假设
- D.一定能输出唯一确定的方向
答案:A。GES 是基于评分的搜索方法,在等价类空间上贪心加边删边,靠 BIC 一致性保证统计收敛;PC 则是基于条件独立的约束法。B 描述的是 PC,C 不成立(仍需可分解一致的评分假设),D 错在方向并非全部可识别,故选 A。
Q6. 删边阶段为什么有时删边反而涨分?
- A.删边后参数变少惩罚项减小← 正确答案
- B.删边后样本量自动增大了
- C.删边后数据拟合度必然提高
- D.删边后评分公式变化规律了
答案:A。删边看似“少了一堵墙”,但参数减少使 BIC 的惩罚项变小;若被删边原本就是冗余的,拟合损失很小,整体评分反而上升。B、C、D 都不符合 BIC 构成原理,故选 A。
Q7. CPDAG 中包含哪些信息?
- A.变量之间的邻接骨架← 正确答案
- B.一部分确定方向的边← 正确答案
- C.每个变量的完整参数表
- D.所有可能的边方向组合
答案:AB。CPDAG 保留骨架(无向底图)和已确定方向的边(来自 V 结构与传递),虚线边表示方向未定。它不包含参数表(C 错),也不枚举所有方向的组合(D 错),故选 A、B。
Q8. 下列哪些属于 GES 的局限性?
- A.可能陷入局部最优解← 正确答案
- B.无法区分等价类内方向← 正确答案
- C.可以完美处理隐藏混杂
- D.候选操作枚举开销较大← 正确答案
答案:ABD。GES 作为贪心搜索可能陷入局部最优;等价类内部方向不可识别;候选边枚举与评分拟合开销大。它并不能处理未观测混杂(C 错),故选 A、B、D。
Q9. GES 最终输出结果是什么形式?
- A.一个完全有向的 DAG
- B.一张部分有向无环图← 正确答案
- C.一份条件独立性列表
- D.一组评分值分布表
答案:B。GES 输出对应等价类的 CPDAG:能确定方向的边画实线,不能确定的画虚线,而不是某个唯一的完全有向 DAG。A、C、D 均不符合输出形式,故选 B。
Q10. 哪些情形会降低 GES 学习结果的准确性?
- A.样本量过小导致评分不稳← 正确答案
- B.存在未观测的混杂变量← 正确答案
- C.变量间关系高度非线性← 正确答案
- D.数据集变量名称不规范
答案:ABC。样本过小使评分不稳定,未观测混杂破坏因果可识别性,非线性关系超出线性评分假设,都会降低准确率。变量名称不影响学习结果(D 错),故选 A、B、C。