想象你是新上任的电话调查员小张,负责绘制一个老小区的“直拨线路图”。每户人家是一个变量,你的目标只有一个:查明哪些人家之间拉着直拨电话线(存在直接因果关联),哪些只是靠中间人传话(间接关联)。你进不了房门、动不了线缆,只能靠一次次通话测试:让某一户暂时保持静默,再听另外两户之间还有没有动静。如果静默一开启,A、B 两家的消息就彻底切断,说明它们之间没有直拨线,只是经由那户中转;如果消息依然畅通,你就有理由相信两家之间藏着一条实线。所有两两测试做完,剩下的线路就是一张骨架图。 这套思路,在人工智能的“因果发现”研究里叫骨架发现阶段,是许多因果结构学习算法的第一步:不急着判断方向,先用统计学手段筛出所有可能存在直接因果关系的变量对,搭出无向骨架,为下一步定向打好地基。接下来,我会沿着小张的排查故事,抽丝剥茧地认识条件独立性、d 分离、V 结构、主流算法流程与优化策略——你会发现,因果 AI 的底层直觉,其实就是一次全城范围的“隔离测试”。

岗位关联

掌握骨架发现阶段的原理与算法,你就能从观测数据中还原因果结构的核心线索,让 AI 从“预测相关”进阶到“解释因果”,在医疗、金融、推荐系统等场景做出更稳健、可解释的决策。

目录
  1. 可视化演示
  2. 骨架发现概述
  3. 条件独立性原理
  4. 马尔可夫与忠诚性
  5. d分离准则
  6. 对撞子与V结构
  7. PC算法骨架学习
  8. 独立性检验方法
  9. 复杂度与优化
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示约束式因果发现中从完全连通图通过条件独立性检验逐步删边得到无向骨架、条件集大小递增策略、以及分离集记录与v-结构方向确定的过程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

完全连通图骨架发现 n个节点两两相连构成完全图,共n(n-1)/2条边 通过条件独立性检验逐步删除独立变量对之间的边 X⊥Y|S → 删除X-Y边,记录分离集S(X,Y) 最终输出稀疏无向骨架

骨架发现概述

小张第一天上班,队长把一沓住户档案拍在桌上:“别急着判断谁指挥谁——先把谁和谁之间有直拨电话线摸清楚。”这份任务书背后,是现代 AI 同样要面对的课题:当我们手里只有一堆观测数据(住户档案),如何还原出变量之间真正的因果结构?问题的第一步,并不是给每条关系标注方向,而是先回答一个更基础的问题:哪些变量之间存在直接关联?这正是骨架发现阶段的使命。

在因果图模型里,“直拨线”叫无向边,“住户”叫变量或节点。把所有变量间的直接关联画在一张图上,且不标注箭头方向,就得到一张骨架图。基于约束的因果发现方法(如 PC 算法、FCI 算法)都采用“先骨架、后定向”的两阶段策略:骨架发现先通过条件独立性检验筛掉所有“假关联”,留下候选的直接因果边;后续的定向阶段再利用 V 结构等规则给边补上方向。

骨架发现的目标可以写成一张图:

小张把队长的意思翻译成了自己的排查原则:先假设所有住户两两都有直拨线,然后拼命找“中间人”拆台——找到一个能让两户完全断联的中间人,这条线就划掉。留下来的线,就是那张他要在全城挂图的“骨架”。

Gskel=(V,E),vi−vj∈E  ⟺  ∄ S⊆V∖{vi,vj},  vi⊥ ⁣ ⁣ ⁣⊥vj∣SG_{\text{skel}} = (V, E), \qquad v_i - v_j \in E \iff \not\exists\, \mathbf{S} \subseteq V \setminus \{v_i, v_j\}, \; v_i \perp\!\!\!\perp v_j \mid \mathbf{S}
简单说:两位住户只要在任何“中间人组合”下都无法完全断联,就在骨架图上连一条线。
  1. 骨架图的符号$G_{\text{skel}}$ 表示骨架图,$V$ 是所有变量(住户)的集合,$E$ 是“确有直拨线”的无向边集合。
    Gskel=(V,E)G_{\text{skel}} = (V, E)
  2. 边的存在判定$v_i - v_j \in E$ 表示变量 $v_i$ 与 $v_j$ 之间存在一条待定方向的边。
    vi−vj∈Ev_i - v_j \in E
  3. 条件独立记号$v_i \perp\!\!\!\perp v_j \mid \mathbf{S}$ 表示在给定变量集合 $\mathbf{S}$ 后二者独立;$\not\exists$ 表示“找不到任何这样的集合”。
    vi⊥ ⁣ ⁣ ⁣⊥vj∣Sv_i \perp\!\!\!\perp v_j \mid \mathbf{S}
  4. 全子集扫描$\mathbf{S} \subseteq V \setminus \{v_i, v_j\}$ 表示 $\mathbf{S}$ 取自“除 $v_i, v_j$ 外的任意变量子集”;只要存在某个子集能让二者独立,就说明连线是经由他人中转的假象,不能入 $E$。
    S⊆V∖{vi,vj}\mathbf{S} \subseteq V \setminus \{v_i, v_j\}

条件独立性原理

小张测试工具的核心,是“隔离测试”:让中间住户 C 暂停收发(把 C 的值固定住),再竖起耳朵听 A、B 两户之间还有没有动静。如果 C 一静下来,A、B 立刻断联,说明 A、B 之间的“消息往来”全靠 C 中转——它们之间没有直拨线。

这个“隔离测试”在统计学里就是条件独立性:在给定变量 Z 之后,X 与 Y 是否依然互相依赖。若固定 Z 后 X 与 Y 变得独立,说明 X 与 Y 原有的相关性全部来自 Z 的“传导”或“共同影响”。注意这里“固定”不是物理干预,而是根据观测数据把样本按 Z 的取值分组,再在每一组内考察 X 与 Y 的关系。

条件独立有严格的概率定义:

小张给传话记录本写了一行批注:“隔离测试成立,即条件独立成立;条件独立成立,直拨线就存疑。”每一对住户都要过这一关,通关的才配留在骨架图上。

X⊥ ⁣ ⁣ ⁣⊥Y∣Z  ⟺  P(X,Y∣Z)=P(X∣Z) P(Y∣Z)X \perp\!\!\!\perp Y \mid Z \iff P(X, Y \mid Z) = P(X \mid Z)\, P(Y \mid Z)
意思是:已知 Z 的取值后,X 对预测 Y 不再提供任何额外信息,就像 C 沉默后 A 的房间对 B 的房间毫无影响。
  1. 左端记号$X \perp\!\!\!\perp Y \mid Z$ 读作“在给定 $Z$ 条件下,$X$ 与 $Y$ 相互独立”,这是骨架发现里最核心的询问。
    X⊥ ⁣ ⁣ ⁣⊥Y∣ZX \perp\!\!\!\perp Y \mid Z
  2. 条件联合分布$P(X, Y \mid Z)$ 是在 $Z$ 取某值的前提下,$X$ 与 $Y$ 同时取各值的概率。
    P(X,Y∣Z)P(X, Y \mid Z)
  3. 乘积分解$P(X \mid Z)\, P(Y \mid Z)$ 表示把联合概率拆成两个边缘条件概率的乘积——这正是“互不影响”的数学化身。
    P(X∣Z) P(Y∣Z)P(X \mid Z)\, P(Y \mid Z)
  4. 等价含义$\iff$ 说明分解式与独立性记号完全等价,是统计检验可以直接验证的对象。
      ⟺  \iff

马尔可夫与忠诚性

不过,小张很快发现,光有测试工具还不够。队长给他下了两条“工作纪律”,没有它们,再多的测试也会白做。第一条纪律叫“按图施工”:真实的线路图长什么样,数据里就应该表现出相应的独立关系;第二条纪律叫“图上必通”:凡是图纸上画了的线路,都真实通电,不许出现“画了却因巧合而不通”的怪事。

这两条纪律在因果图模型中有响亮的名字:因果马尔可夫条件和忠诚性假设。因果马尔可夫条件说的是:在一个因果图中,给定某节点的所有父节点(直接因)后,它与所有非后代节点在分布上条件独立。忠诚性假设则反过来:数据中出现的每一个条件独立关系,都应当能在图上用 d 分离解释,而不是某些路径效应恰好互相抵消,导致“图上虽连、数据却独立”。

因果马尔可夫条件记作:

小张把两条纪律贴在了工具箱上:只要线路图真实且没有“意外失灵”,他就能顺着数据里的独立性线索,安全地反推出线路图本身。这正是骨架发现能够成立的底气。

X⊥ ⁣ ⁣ ⁣⊥NonDesc(X)∣Pa(X)X \perp\!\!\!\perp \mathrm{NonDesc}(X) \mid \mathrm{Pa}(X)
一位住户的消息,在已知“直接上线”是谁之后,不会再受那些不经过该住户间接牵连的远房邻居影响。
  1. 关注节点$X$ 是图中任意一个变量节点,对应一位住户。
    XX
  2. 父节点集合$\mathrm{Pa}(X)$ 是直接指向 $X$ 的变量集合,即 $X$ 的“直接上线”。
    Pa(X)\mathrm{Pa}(X)
  3. 非后代集合$\mathrm{NonDesc}(X)$ 是所有不沿着箭头链落在 $X$ 下游的节点集合。
    NonDesc(X)\mathrm{NonDesc}(X)
  4. 整体论断已知 $\mathrm{Pa}(X)$ 后,$X$ 与 $\mathrm{NonDesc}(X)$ 中每个节点都条件独立——这保证局部因果能“由父传子”,而不会凭空越级相关。
    X⊥ ⁣ ⁣ ⁣⊥NonDesc(X)∣Pa(X)X \perp\!\!\!\perp \mathrm{NonDesc}(X) \mid \mathrm{Pa}(X)

d分离准则

现在小张面前不止一个中间人,而是一整条传话链:A 传给 B,B 传给 C,C 再传给 D。他需要一种“看地图就知道哪段能被阻断”的功夫——这就是 d 分离(d-separation)。

d 分离是从图结构本身判断“给定节点集 Z 后,X 与 Y 之间的所有路径是否都被阻断”的准则。它只研究三种基本结构:链式 $X \rightarrow Z \rightarrow Y$ 和分叉式 $X \leftarrow Z \rightarrow Y$ 中,Z 是路径的“咽喉”,给定 Z 后路径被阻断;对撞式 $X \rightarrow Z \leftarrow Y$ 则相反,Z 是“汇合点”,给定 Z 反而会打开路径。d 分离的意义在于:在忠诚性假设下,图上 d 分离等价于分布上的条件独立,于是“看图断案”与“数据检验”完美对接。

链式与分叉结构在给定 Z 后的阻断特性可以写成:

小张从此学会先看图、再实测:只要路径上存在这样的“咽喉点”且被纳入条件集,这条路径就不会再传递相关性。他因此省下了大量盲目测试的时间。

(X→Z→Y)  ∨  (X←Z→Y)  ⟹  X⊥ ⁣ ⁣ ⁣⊥Y∣Z(X \rightarrow Z \rightarrow Y) \;\vee\; (X \leftarrow Z \rightarrow Y) \;\Longrightarrow\; X \perp\!\!\!\perp Y \mid Z
只要 Z 是路径中间唯一的“总机”或“共同源头”,把它一按静音,X 和 Y 之间的消息就被彻底阻断。
  1. 链式结构$X \rightarrow Z \rightarrow Y$ 表示 X 的消息经 Z 传到 Y,Z 是必经总机。
    X→Z→YX \rightarrow Z \rightarrow Y
  2. 分叉结构$X \leftarrow Z \rightarrow Y$ 表示 Z 是 X 与 Y 的共同原因,Z 一停,两边同时沉默。
    X←Z→YX \leftarrow Z \rightarrow Y
  3. 析取记号$\vee$ 表示“两种结构任意一种成立即可”。
    ∨\vee
  4. 阻断结论$\Longrightarrow$ 之后的条件独立式,说明给定 Z 时 X 与 Y 一定条件独立——这是骨架发现删边的图论依据。
    X⊥ ⁣ ⁣ ⁣⊥Y∣ZX \perp\!\!\!\perp Y \mid Z

对撞子与V结构

有一天小张遇到一栋奇怪的楼:A、B 两家从无来往,但只要 C 家的广播一响,A 家和 B 家的通话记录就会同时暴增——仿佛两个互不相识的人,因为共同被 C 点名而产生了“虚假熟络”。这就是对撞子现象,也是因果图中最反直觉的结构。

当两条边从 X 和 Y 同时指向 Z 时,Z 被称为对撞子,结构记为 V 结构($X \rightarrow Z \leftarrow Y$)。X 与 Y 本来相互独立(没有共同原因、也没有传递路径),但一旦在统计上“给定 Z”(比如只看 C 家广播响时的样本),X 与 Y 反而会表现出统计依赖。这种“越控越相关”的现象也被称为辩解效应(explaining away)。

V 结构的关键特性用公式表示:

小张把这条写进手册最显眼处:遇到对撞结构,千万别把 Z 放进条件集去测试 X 与 Y 的独立性,否则会把一对本来无边的节点误判成有边。这个规律反过来还给定向阶段提供了重要线索——发现“不加条件独立、加某条件反而相关”的三人组,几乎就锁定了一个 V 结构。

(X→Z←Y)  ⟹  X⊥ ⁣ ⁣ ⁣⊥Y  ∧  X̸ ⁣⊥ ⁣ ⁣ ⁣⊥Y∣Z(X \rightarrow Z \leftarrow Y) \;\Longrightarrow\; X \perp\!\!\!\perp Y \;\wedge\; X \not\!\perp\!\!\!\perp Y \mid Z
A、B 平时井水不犯河水;可一旦知道 C 的状态,A 便能“解释”C 的部分异常,从而间接透露 B 的信息——这就是条件集的“反作用力”。
  1. 对撞结构$X \rightarrow Z \leftarrow Y$ 表示两箭头同时指向 $Z$,$Z$ 在路径上扮演“汇合点”。
    X→Z←YX \rightarrow Z \leftarrow Y
  2. 边际独立$X \perp\!\!\!\perp Y$ 表示在不给定任何条件时,$X$ 与 $Y$ 相互独立。
    X⊥ ⁣ ⁣ ⁣⊥YX \perp\!\!\!\perp Y
  3. 条件依赖$X \not\!\perp\!\!\!\perp Y \mid Z$ 表示给定 $Z$ 后二者反而不独立。
    X̸ ⁣⊥ ⁣ ⁣ ⁣⊥Y∣ZX \not\!\perp\!\!\!\perp Y \mid Z
  4. 合取的用意$\wedge$ 把两个结论并置,强调“控住对撞子会制造虚假相关”,这是选择条件集时必须避开的陷阱。
    ∧\wedge

PC算法骨架学习

小张把散落各处的技巧整合成了一套标准作业程序:第一轮先测“不加中间人”的场景;若两户始终相关,第二轮加一位中间人再测;还分不开,就加两位……直到无人可加。这套从浅到深、逐层加码的排查法,正是 PC 算法骨架发现阶段的灵魂。

PC 算法从一张完全无向图出发——假设所有变量两两相连。随后以“条件集大小 k”为轮次,从 k=0 开始逐轮递增。每一轮中,对于图上仍相连的每一对节点 X、Y,从 X 当前的邻居集合中取出大小为 k 的子集作为条件集,做条件独立性检验;只要找到某一个条件集使 X、Y 独立,就立即删掉这条边,并记录这个“分离集”,供后续定向阶段使用。由于删边会缩小邻域,后续轮次的条件集组合数也随之减少,形成良性循环。

删边的判定可以写作:

小张的作业手册因此变得极其清爽:从“零中间人”试起,删得越多,后面的组合越少。他用这个办法在三天内排查完一个 60 户的小区,而隔壁组用“一上来就试所有中间人组合”的老办法,一个月还没出结果。

X−Y∈Ek−1∖Ek  ⟺  ∃ S⊆Adj(X)∖{Y},  ∣S∣=k,  X⊥ ⁣ ⁣ ⁣⊥Y∣SX - Y \in E_{k-1} \setminus E_k \iff \exists\, \mathbf{S} \subseteq \mathrm{Adj}(X) \setminus \{Y\},\; |\mathbf{S}| = k,\; X \perp\!\!\!\perp Y \mid \mathbf{S}
第 k 轮新删掉的边,就是那些在“大小为 k 的中间人组合”中找到了拆台证据的边。
  1. 删除的边$E_{k-1} \setminus E_k$ 表示第 k-1 轮还在、第 k 轮被删掉的边的集合。
    Ek−1∖EkE_{k-1} \setminus E_k
  2. 候选条件集$\mathrm{Adj}(X) \setminus \{Y\}$ 是 X 当前邻居里去掉 Y 后的集合,条件集只能从这里面取。
    Adj(X)∖{Y}\mathrm{Adj}(X) \setminus \{Y\}
  3. 规模限制$|\mathbf{S}| = k$ 说明本轮只尝试“恰好 k 个中间人”的组合,是“逐层加码”的数学表达。
    ∣S∣=k|\mathbf{S}| = k
  4. 存在性量词$\exists$ 表示只要有一个组合能让 X、Y 独立,这条边就立刻被删除。
    ∃ S\exists\, \mathbf{S}

独立性检验方法

工欲善其事,必先利其器。小张的背包里有三件套:测离散信号用的“万用表”、测连续信号用的“示波器”,以及应付复杂信号的“频谱仪”。对应到统计世界,就是离散变量检验、连续变量检验和非参数检验。

当变量是离散的(如“是否患病”“基因型”),常用卡方独立性检验或互信息检验,它们在列联表上比较观测频数与期望频数的偏差;当变量近似服从多元高斯分布时,偏相关检验是最有效率的选择——先控制掉条件集中变量的影响,再检验剩余相关是否显著为零;而面对分布未知或混合类型的复杂数据,核化的条件互信息检验等非参数方法更稳健,但计算代价也更高。

连续变量最常用的偏相关系数公式是:

小张每次开工前都会先看住户档案的类型:离散的拿万用表、连续的拿示波器、搞不清楚的用频谱仪慢慢扫。选错工具轻则测不准,重则把直拨线误判成无关联,整个骨架图都会跟着歪。

ρXY⋅Z=ρXY−ρXZ ρYZ(1−ρXZ2)(1−ρYZ2)\rho_{XY \cdot Z} = \frac{\rho_{XY} - \rho_{XZ}\,\rho_{YZ}}{\sqrt{(1 - \rho_{XZ}^{2})(1 - \rho_{YZ}^{2})}}
把 X、Y 与 Z 之间的两条间接通道“拆掉”后,剩下的相关系数如果接近 0,就说明 X、Y 之间没有直接关联。
  1. 普通相关$\rho_{XY}$ 是 X 与 Y 的皮尔逊相关系数,反映二者总相关,既有直接贡献也有 Z 的间接贡献。
    ρXY\rho_{XY}
  2. 与 Z 的相关$\rho_{XZ}\, \rho_{YZ}$ 分别刻画 X、Y 各自与 Z 的相关强度,是“间接贡献”的来源。
    ρXZ ρYZ\rho_{XZ}\, \rho_{YZ}
  3. 分子扣减$\rho_{XY} - \rho_{XZ}\,\rho_{YZ}$ 从总相关里扣除经由 Z 的间接相关。
    ρXY−ρXZ ρYZ\rho_{XY} - \rho_{XZ}\,\rho_{YZ}
  4. 分母归一$\sqrt{(1 - \rho_{XZ}^{2})(1 - \rho_{YZ}^{2})}$ 把结果标准化到区间 $[-1, 1]$,便于与 0 比较做显著性检验。
    (1−ρXZ2)(1−ρYZ2)\sqrt{(1 - \rho_{XZ}^{2})(1 - \rho_{YZ}^{2})}

复杂度与优化

任务量开始失控了。若小区有 300 户,两两组合超过 4.4 万对,每对还要试各种规模、各种组合的“中间人”条件集。小张意识到:排查路线不优化,干到退休也交不了差。

骨架发现的复杂度由变量总数 p 和图中最大邻域规模 K 共同决定。最坏情况下,算法需要尝试几乎所有条件集组合,独立性检验次数高达 $O(p^K)$ 量级;当 p 上百、K 偏大时,计算量是天文数字。好在有三条优化路径:其一是“先删后查”,利用低阶检验先砍掉大量边,邻域缩小后高阶组合数骤减;其二是“设界止增”,给条件集规模 k 设一个上限 K,超过即停止加码;其三是“分片并行”,不同节点对的检验彼此独立,可以开多个线程同时排查。

最坏情况复杂度可以写成:

小张的最终策略是“先易后难、先删后查、多人分片”:先用无条件和一阶条件快速清掉大半边,再对剩下的“顽固分子”上高阶条件,同时拉来三个同事分片区并行排查。一个原本要跑两周的活,压缩到三天完成——骨架图按时挂上了墙。

Tmax⁡=O ⁣(pK),K=max⁡v∈V∣Adj(v)∣T_{\max} = O\!\left(p^{K}\right), \qquad K = \max_{v \in V} |\mathrm{Adj}(v)|
节点数 p 是底数、最大邻居数 K 是指数,K 每增 1,工作量就按 p 倍翻——所以尽早缩邻域才是王道。
  1. 最坏工作量$T_{\max}$ 表示极端情形下需要执行的独立性检验次数上界。
    Tmax⁡T_{\max}
  2. 变量规模$p$ 是图中节点(变量)总数,底数越大,复杂度膨胀越快。
    pp
  3. 邻域上限$K = \max_{v \in V} |\mathrm{Adj}(v)|$ 是“邻居最多的节点”的邻居个数,它代表了条件集规模可能达到的最大值。
    K=max⁡v∈V∣Adj(v)∣K = \max_{v \in V} |\mathrm{Adj}(v)|
  4. 复杂度含义$O(p^K)$ 说明只要 K 保持很小,即使 p 很大也能接受;反之 K 一大,再多的样本和算力都会被拖垮。
    O(pK)O(p^{K})

AI 应用场景

基因调控网络推断

阶段:模型解释与知识发现

解决的问题:基因表达数据动辄上万维,基因间共享同一调控通路会产生大量虚假相关,传统相关性网络把“因同一条通路而共表达”误判为“直接调控”。骨架发现通过条件独立性检验剔除间接共表达,得到更可信的候选调控骨架。

具体案例:某生物信息团队对 500 个免疫相关基因的表达谱运行简化版 PC 骨架发现,先得到一张无向调控骨架,候选靶点从一万多对压缩到两千余对,再结合定向阶段锁定关键转录因子,后续湿实验验证命中率显著提升。

金融风险传导分析

阶段:特征工程与可解释建模

解决的问题:宏观经济指标(利率、汇率、油价、股指)互相纠缠,回归系数受共线性干扰,难以判断谁真正影响谁。骨架发现在控制其他宏观变量后寻找仍然相关的指标对,帮助风控团队识别风险传导的“直拨线”。

具体案例:某量化风控团队对 20 年日频宏观数据做骨架发现,发现“油价与化工板块”在给定“通胀预期”后独立性显著,说明二者关联主要由通胀预期间接传导;据此删除了冗余特征,模型可解释性与稳定性同时提升。

推荐系统行为归因

阶段:因果推荐与用户理解

解决的问题:协同过滤捕捉的是“行为相关”而非“行为因果”,容易把“同时流行所以都买”与“看了 A 才买 B”混为一谈。骨架发现从用户行为序列中筛出直接关联的行为对,为可解释推荐提供依据。

具体案例:内容平台对“曝光—点击—深度阅读—付费”四类行为做骨架发现,结果显示付费的直接前因是“深度阅读”而非“曝光”,据此把推荐目标从点击率优化转为深度阅读率优化,订阅转化率上升。

Python 演示

python77 行
import numpy as npfrom itertools import combinationsfrom scipy.stats import norm, t as t_dist  class PcSkeleton:    """简化版PC算法骨架发现, 使用偏相关独立性检验"""     def __init__(self, data, alpha=0.05):        self.data = np.asarray(data, dtype=float)        self.n, self.p = self.data.shape        self.alpha = alpha        self.skel = {i: set(range(self.p)) - {i} for i in range(self.p)}        self.sep_sets = {}        self.gc_count = 0     def _partial_corr(self, i, j, cond):        idx = [i, j] + list(cond)        C = np.corrcoef(self.data[:, idx].T)        if len(cond) == 0:            return C[0, 1]        prec = np.linalg.inv(C)        num = -prec[0, 1]        den = np.sqrt(prec[0, 0] * prec[1, 1])        return num / den     def _p_value(self, rho, k):        if k == 0:            z = np.sqrt(self.n - 3) * np.arctanh(rho)            return 2 * (1 - norm.cdf(np.abs(z)))        df = self.n - k - 2        t_stat = rho * np.sqrt(df) / np.sqrt(1 - rho * rho)        return 2 * (1 - t_dist.cdf(np.abs(t_stat), df=df))     def _is_indep(self, i, j, cond):        rho = self._partial_corr(i, j, cond)        return self._p_value(rho, len(cond)) > self.alpha     def fit(self):        k = 0        while k <= self.p - 2:            changed = False            for i in range(self.p):                for j in list(self.skel[i]):                    candidates = [c for c in combinations(self.skel[i] - {j}, k)]                    for cond in candidates:                        self.gc_count += 1                        if self._is_indep(i, j, cond):                            self.skel[i].discard(j)                            self.skel[j].discard(i)                            key = (min(i, j), max(i, j))                            self.sep_sets[key] = set(cond)                            changed = True                            break            if not changed:                break            k += 1        return self.skel, self.sep_sets  def generate_demo_data(n=600, seed=42):    rng = np.random.RandomState(seed)    x = rng.randn(n)    z = 0.6 * x + 0.8 * rng.randn(n)    y = 0.5 * z + 0.8 * rng.randn(n)    w = 0.7 * y + 0.7 * rng.randn(n)    return np.column_stack([x, z, y, w])  if __name__ == "__main__":    data = generate_demo_data()    model = PcSkeleton(data, alpha=0.05)    skel, sep = model.fit()    edges = [(i, j) for i in skel for j in skel[i] if i < j]    print("真实结构: x -> z -> y -> w")    print("发现骨架边:", sorted(edges))    print("实际检验次数:", model.gc_count)
目的:用偏相关检验驱动简化版 PC 骨架发现,从观测数据中还原真实因果结构中的无向骨架。
  1. self.skel = {i: set(range(self.p)) - {i} for i in range(self.p)}从完全图出发,每个变量先与所有其他变量相连。
  2. def _partial_corr(self, i, j, cond):计算控制住 cond 后 i 与 j 的偏相关,即“隔离中间人后的剩余关联”。
  3. prec = np.linalg.inv(C)对相关矩阵求逆,用精度矩阵直接解出偏相关系数。
  4. def _p_value(self, rho, k):把偏相关转成 p 值,大于显著性水平则判定为条件独立。
  5. for cond in combinations(self.skel[i] - {j}, k):枚举 i 的邻居中大小为 k 的全部条件集,对应“逐层加中间人”。
  6. self.skel[i].discard(j)一旦找到条件独立证据就删边,同时删去对称方向的边。
  7. self.sep_sets[key] = set(cond)记录分离集,它将在后续定向阶段发挥关键作用。
  8. if not changed: break本轮没有任何删边就停止增大 k,避免无谓的更高阶检验。
  9. edges = [(i, j) for i in skel for j in skel[i] if i < j]输出去重后的无向边列表,即骨架。

常见误区

误区我用 PC 算法跑完骨架发现,拿到一张无向图,就直接给每条边画上箭头,当作因果结论去汇报。

正解骨架只回答“谁和谁有边”,不回答“箭头朝哪边”。定向需要在后续阶段利用 V 结构与 Meek 规则完成;未经定向擅自标注方向,等于小张只知道两户有直拨线,却硬编造“谁先打给谁”,十有八九会出错。

为何常见:骨架图已经是一张“图”,视觉上很像因果图,初学者容易忽略无向与有向的本质差别。

误区我总觉得条件集设得越大,控制掉的干扰越彻底,骨架一定越准,所以直接从 k=5 开始跑。

正解条件集增大让组合数爆炸式增长,且在高维条件集下样本被切得太碎,偏相关估计方差剧增、检验功效崩溃。PC 算法坚持“从 k=0 逐级增大”,正是为了用最小的充分条件集删边——就像隔离测试中只要锁住一个关键总机就足以断联,没必要把全楼人都关进静音室。

为何常见:“控制变量更多更严谨”是普遍的统计直觉,但忽略了高维条件下有限样本带来的维数灾难。

误区只要把样本量堆到几十万,骨架发现肯定能恢复真实的因果无向图。

正解骨架发现的正确性还依赖忠诚性假设:数据中的每个条件独立都必须由图的 d 分离结构产生。若真实参数恰好让两条路径的效应相互抵消,数据会呈现出“图上有边却检验独立”的假象,再多样本也无济于事。就像图纸上明明画了线,但线路老化恰好断路,测试结果永远显示“无关联”。

为何常见:人们常常把“统计一致性”理解为无条件成立,忽视了因果推断中模型假设的不可检验性。

误区数据里混着连续、二值、计数变量,我懒得区分,全部丢给偏相关检验跑一遍。

正解偏相关检验隐含多元高斯假设,对二值、计数或重尾数据会严重失真;离散变量应改用卡方、似然比或互信息类检验,混合类型需要条件互信息或核方法。好比小张用万用表去量水管,读数再漂亮也不是水压。

为何常见:数据分析新手习惯把所有数值一视同仁地用线性工具处理,对统计检验的适用条件缺乏敏感。

面试题

Q1. 骨架发现阶段的直接输出是什么?

单选题 · easy

  • A.带方向的完整因果图
  • B.不含方向的无向骨架← 正确答案
  • C.变量间的联合概率分布
  • D.每个变量的期望和方差

答案:B。骨架发现只回答“哪些变量对之间存在直接关联”,输出是无向图,对应小张查出的“直拨线地图”——只知道谁和谁有线,不知道谁打给谁。A 是定向阶段的产物,C、D 是统计量而非因果结构。

Q2. 在给定 Z 的条件下,X 与 Y 相互独立,以下哪个符号最合适?

单选题 · easy

  • A.$X \perp\!\!\!\perp Y$
  • B.$X \perp\!\!\!\perp Y \mid Z$← 正确答案
  • C.$X \rightarrow Y$
  • D.$X - Y$

答案:B。B 中的竖线加 Z 表示“给定 Z 后”,正是条件独立的记号,对应小张的隔离测试;A 是无条件独立,C 是有向边,D 是无向边,都不是条件独立的表达。

Q3. PC 算法骨架发现中,条件集大小 k 的初始值是?

单选题 · mid

  • A.k = 0,从无中间人开始逐层增大← 正确答案
  • B.k = 1,至少设一位中间人
  • C.k 等于变量总数的一半
  • D.k 等于最大邻域规模

答案:A。PC 算法从完全图出发,先做无条件独立性检验(k=0),删掉一批边后再逐级增大 k。B、C、D 都会让算法在早期就陷入大量高阶组合检验,既慢又容易因样本不足而误判。

Q4. 关于 V 结构 X→Z←Y 的说法正确的是?

多选题 · mid

  • A.X 与 Y 边际上相互独立← 正确答案
  • B.给定 Z 后 X 与 Y 可能变得相关← 正确答案
  • C.Z 被称为对撞子← 正确答案
  • D.X 与 Y 之间必然存在一条边

答案:ABC。V 结构里 X、Y 没有共同原因也没有传递路径,边际独立;但 Z 是对撞子,一旦给定 Z,X 可以“解释”Z 从而透露 Y 的信息,产生条件依赖。A、B、C 都正确,D 说必须有边是错误的——V 结构恰恰没有 X-Y 边。

Q5. 忠诚性假设被违背时,可能出现的情况有?

多选题 · mid

  • A.图上有边但在数据中检验为独立← 正确答案
  • B.图上无边但在数据中检验为相关← 正确答案
  • C.骨架发现结果与真实图不一致← 正确答案
  • D.算法会直接报错退出程序

答案:ABC。忠诚性假设保证“条件独立等价于 d 分离”。违背分两种情况:效应抵消导致“有边却独立”,或恰好相等导致“无边却相关”,二者都会让骨架删边出错(A、B、C 对)。D 是工程实现问题,与统计假设无关。

Q6. 对连续型高斯变量做条件独立性检验,最常用的方法是?

单选题 · easy

  • A.卡方独立性检验
  • B.偏相关显著性检验← 正确答案
  • C.文本词频统计检验
  • D.方差分析 F 检验

答案:B。连续高斯场景下偏相关检验效率最高:先扣除条件集的影响,再检验剩余相关是否为 0,就像用示波器直接测“扣除干扰后的信号”。卡方适用于离散列联表,C 用于文本,D 用于多组均值比较。

Q7. 下列哪些做法能有效降低骨架发现的计算复杂度?

多选题 · hard

  • A.先用低阶检验尽早删边以缩小邻域← 正确答案
  • B.给条件集规模 k 设置合理上限← 正确答案
  • C.将不同节点对的检验并行化← 正确答案
  • D.增大条件集规模以覆盖所有组合

答案:ABC。A 让邻域变小,后续组合数骤减;B 避免 k 无限增长导致复杂度按 p^k 爆炸;C 利用各边检验之间的独立性做并行加速。D 恰好相反,会让检验组合爆炸,属于典型的高阶暴力法。

Q8. 链式结构 X→Z→Y 中,在给定 Z 后 X 与 Y 的关系是?

单选题 · mid

  • A.路径被阻断,二者条件独立← 正确答案
  • B.路径被打开,二者条件相关
  • C.二者必定完全同分布
  • D.Z 对二者没有任何影响

答案:A。链式结构中 Z 是“咽喉总机”,给定 Z 相当于让总机静默,X 到 Y 的传话被阻断,二者条件独立。B 是对撞结构的行为,C 与独立性无关,D 与前提矛盾。

Q9. 因果马尔可夫条件指出,给定变量的父节点后,该变量与什么集合独立?

单选题 · hard

  • A.与它的所有祖先节点
  • B.与它的所有后代节点
  • C.与它的所有非后代节点← 正确答案
  • D.与它的所有邻接节点

答案:C。马尔可夫条件是“局部因果”的数学化:X 的产生只直接依赖父节点,给定父节点后,X 与其他所有非后代都条件独立。A 中祖先包含父节点本身,给定后没有意义;B 中后代恰恰与 X 相关;D 中邻接包含后代邻居,说法不完整。

Q10. 假设真实结构为 X→Y←Z,且 X 与 Z 通过共同原因 W 连接(X←W→Z)。骨架发现中,若要正确删除 X-Z 之间的边,应选择什么条件集?

单选题 · extreme

  • A.条件集取 {Y},因为对撞子可以阻断 X 与 Z
  • B.条件集取 {W},因为公共原因 W 能阻断路径← 正确答案
  • C.条件集取空集,因为边际独立即可
  • D.条件集取 {Y, W},必须同时包含二者

答案:B。X 与 Z 之间有一条经由 W 的路径,给定共同原因 W 后路径被阻断,X 与 Z 条件独立,可删边。若取 {Y} 会打开 X→Y←Z 这条对撞路径,X 与 Z 反而相关;空集时二者经 W 相关;{Y, W} 虽含 W 但对撞路径也被打开,仍不独立。此题综合考察 d 分离与对撞子的陷阱。