因果人工智能——基于因果约束对DAG进行测试

39 阅读1小时+

本章内容概述

  • 使用d-分离(d-separation)来推理因果关系如何限制条件独立性
  • 利用NetworkX和pgmpy进行d-分离分析
  • 使用条件独立性检验来反驳因果DAG
  • 在存在潜变量时反驳因果DAG
  • 使用并应用因果发现算法中的约束

我们的因果DAG,或者任何因果模型,都是对现实世界的一组假设的表达。通常,这些假设是可以通过数据进行检验的。如果我们检验了某个假设,结果发现它不成立,那么我们的因果模型就是错误的。换句话说,我们的检验“证伪”或“反驳”了我们的模型。遇到这种情况时,我们会回到起点,设计一个更好的模型,并再次尝试反驳它。我们不断重复这个循环,直到得到一个能经受住反驳的稳健模型。

本章将聚焦于利用基于统计条件独立的检验来测试我们的因果DAG。随着你对可嵌入因果模型的假设以及这些假设允许做出的推断了解得越来越深入,你会学到更多测试和反驳模型的新方法。本章介绍的条件独立性检验工作流程,也可以应用于你未来可能提出的新检验方法。

4.1 因果关系如何引发条件独立性

因果关系以特定方式限制数据,其中之一就是强制某些变量之间存在条件独立性。这种被强制的条件独立性为我们提供了利用统计独立性检验来测试模型的方法;如果我们发现两个变量实际上是相关的,而DAG却表明它们应该条件独立,那么我们的DAG就是错误的。

本章中,我们将使用这些统计独立性检验来测试因果DAG,包括在数据中存在潜变量时,对观测变量函数的独立性检验。最后,我们将探讨这些思想如何支持因果发现算法,这些算法试图直接从数据中学习因果DAG。

但在此之前,先看看因果关系是如何引发条件独立性的。再次考虑我们的血型例子,如图4.1所示。你父亲的血型是你的直接原因,而你的父亲的父亲(祖父)的血型是你的间接原因。尽管祖父的血型是你血型的原因,但在给定你父亲的血型条件下,你的祖父血型与自己血型是条件独立的。

image.png

图4.1 因果关系引发条件独立性。给定你父亲的血型(直接原因),你的血型与你祖父的血型(间接原因)是条件独立的。

我们从因果关系中知道,父母的血型完全决定了孩子的血型。你的祖父和祖母的血型完全决定了你父亲的血型,但你父亲和母亲的血型完全决定了你的血型。一旦知道了你父亲的血型,你祖父的血型就无法提供更多信息。换句话说,在已知父母血型的条件下,你祖父的血型与你的血型是独立的。

4.1.1 碰撞变量(Collider)

现在我们来考虑碰撞变量,这是一种因果关系引发变量之间依赖的有趣方式,而这些变量通常是独立的。考虑图4.2中的经典例子。洒水器开关状态和是否下雨,都会导致草地变湿,但知道洒水器是关闭的,并不能帮助你预测是否在下雨。换句话说,洒水器的状态和下雨与否是独立的。但当你知道草地是湿的,再知道洒水器是关闭的,这就告诉你一定是在下雨。所以,虽然洒水器的状态和是否下雨本身是独立的,但在已知草地状态的条件下,它们变成条件相关的。

image.png

图4.2 洒水器开或关以及是否下雨都会导致草地湿或不湿。知道洒水器关闭并不能帮助你预测是否下雨——洒水器状态和降雨状态是独立的。但在已知草地是湿的情况下,知道洒水器关闭则告诉你一定是在下雨——在给定草地状态的条件下,洒水器状态和降雨状态是条件相关的。

在这个例子中,“湿草”是一个碰撞变量(collider):它是至少由两个相互独立的原因共同导致的效果。碰撞变量之所以有趣,是因为它展示了因果变量可以是独立的,但如果我们条件于一个共同的效应变量,它们就会变得相关。用条件独立的术语来说,父节点(原因)是独立的(洒水器 ⊥ 雨),但在观测(条件于)子节点之后,它们变得相关(洒水器湿草洒水器 ⟂ 雨 | 湿草)。

另一个例子,我们再来看血型,如图4.3所示。

image.png

图4.3 母亲和父亲通常没有血缘关系,因此知道母亲的血型不能帮助预测父亲的血型。但如果我们知道母亲的血型和孩子的血型,就能缩小对父亲血型的可能范围。

如果假设母亲和父亲彼此无关,那么母亲的血型无法告诉我们任何关于父亲血型的信息——(母亲的血型 ⊥ 父亲的血型)。但假设我们知道孩子的血型是B,这是否有助于我们利用母亲的血型来预测父亲的血型呢?

为了解答这个问题,来看图4.4中的标准血型对照表。我们发现,如果母亲的血型是A,孩子的血型是B,那么父亲可能的血型是B或AB。

image.png

图4.4 如果知道孩子的血型,了解母亲的血型可以帮助缩小父亲血型的范围。

单独知道母亲的血型并不能告诉我们父亲的血型信息。但如果加上孩子的血型信息(碰撞变量),我们就可以将父亲的血型可能性从四种缩小到两种。换句话说,(母亲的血型 ⊥ 父亲的血型),但一旦条件于孩子的血型,母亲和父亲的血型就变得相关了。

碰撞变量在因果推断的多个环节都会出现。在第4.6节,我们将看到碰撞变量在因果发现任务中的重要性——即从数据中学习因果DAG。当我们在第7章和第11章讨论因果效应时,也会看到“错误地调整碰撞变量”如何在推断因果效应时引入不希望出现的“碰撞偏差”。

目前,我们先注意到,碰撞变量有时与我们的统计直觉相悖,因为它们描述了因果逻辑如何导致两件本来独立的事物,在条件于第三个或更多变量时“突然”变得相关。

4.1.2 用因果图抽象独立性

image.png

图4.5 在因果效应推断中,我们关注的是如何通过统计方法量化一个原因(处理)对一个结果(效应)的影响程度。混杂变量是治疗和结果之间非因果相关性的来源。因果效应推断需要对混杂变量进行“调整”。d-分离是指导我们如何进行调整的理论基础。

在上一节中,我们利用血型遗传的基本规则展示了因果关系如何引发条件独立性。如果我们想编写代码,帮助我们在不同领域中进行因果推断,就需要一种抽象方法,将因果关系映射为条件独立性,而不依赖于特定领域的规则。“d-分离”解决了这个问题。

d-分离和d-连接指的是我们如何利用图结构推理条件独立性。这些概念乍看之下新颖,但它们将成为你进行基于图的因果推理时最重要的工具之一。稍微提前剧透第7章,考虑图4.5中展示的因果效应推断问题。在因果推断中,你关心的是如何统计地量化一个原因(通常称为“处理”)对一个结果(“效应”)的影响。

正如你在第3章看到的,你可以用变量在因果推断任务中的角色来描述DAG中的变量。在因果效应推断任务中,一个角色是混杂变量。混杂变量是治疗和效应之间非因果相关性的共同原因。为了估计治疗对结果的因果效应,我们必须“调整”混杂变量。这样做的理论依据是通过“d-分离”路径 {治疗 ← 混杂变量 → 结果},聚焦于路径 {治疗 → 结果}。

4.2 d-分离与条件独立

回顾前几章的几个概念:

  • 因果DAG是数据生成过程(DGP)的模型。
  • DGP包含一个联合概率分布。
  • 因果关系会引发联合概率分布中变量间的独立性和条件独立性。
  • d-分离(d-separation)和d-连接(d-connection)是用于推理因果DAG所建模的联合概率分布中条件独立性的图形抽象。这一概念涉及因果DAG中的节点及其间的路径;这些节点和路径被称为“d-连接”或“d-分离”,其中“d”代表“方向性”(directional)。意思是“图中这些节点是d-分离的”这一图形描述对应于“这些变量条件独立”这一概率论描述。d-分离不是用来陈述什么变量导致什么变量,而是用来判断DAG中变量间的路径是否表明联合概率分布中变量间存在依赖或独立。

我们希望建立这种对应关系,因为相比直接推理概率分布,推理图结构更简单;追踪节点间路径比学习高阶概率论课程更易。同时回想第2章,图是算法和数据结构的基础,统计建模也受益于条件独立假设。

4.2.1 d-分离:简化因果分析的入口

假设我们有一个条件独立的陈述,表示在给定变量集合 Z 时,变量 U 和 V 条件独立(即 UVZU \perp V \mid Z)。我们的任务是用纯图论语言定义一个对应的陈述,写作 UGVZU \perp_G V \mid Z,读作“在图 G 中,U 和 V 被 Z d-分离”。

这里,Z 表示一组称为d-分离集或“阻断点”的节点。对应到条件独立,Z 是我们条件化的变量集。我们的目标是定义d-分离,使得集合 Z 中的节点在某种意义上“阻断”了DAG因果结构中所暗示的 U 和 V 之间的依赖。

接下来,令 P 为一条路径,即两个节点之间通过一系列相连的边(和节点)构成的路径。路径上节点是否在数据中被观测到无关紧要(数据的作用后面会讲)。我们定义的“路径”不依赖于边的方向,例如路径 {xyz},{xyz},{xyz},{xyz}\{x → y → z \}, \{x ← y → z \}, \{x ← y ← z \},\{x → y ← z \}都是从 x 到 z 的路径。

最后,回顾碰撞变量结构。碰撞变量结构指的是形如 xyzx \to y \leftarrow z的模式,中间节点 y(碰撞点)有两条入边。

我们现在来定义d-分离。若所有连接 u 和 v 的路径均被 Z d-分离(阻断),则称 u 和 v 被 Z d-分离;若存在任一条路径未被 Z d-分离,则称 u 和 v 是d-连接的。

路径上的d-分离定义
路径 P 被节点集合 Z d-分离,当且仅当满足以下任一条件:

  1. P 包含链结构 imji \to m \to j,且中间节点 mZm \in Z
  2. P 包含链结构 imji \leftarrow m \leftarrow j,且中间节点 mZm \in Z
  3. P 包含父节点-子节点-父节点结构 imji \leftarrow m \to j,且中间(父)节点 mZm \in Z

暂且停一下。条件1到3描述了三节点间边的各种方向排列。如果只考虑这三条,路径中只要有节点在集合 Z 中,路径就被d-分离了。这样很好理解:如果DAG中任意两点间有路径相连,则它们d-连接(依赖);若所有路径都被集合 Z 中的节点阻断,则d-分离(独立)。

但是,碰撞变量让第四条条件与前三条相悖:

  1. 路径 P 包含碰撞结构 imji→m←j,且中间节点 mZm∉Z,且 m 的所有后代节点都不在 Z 中。

这条规则体现了d-分离如何捕捉两个本来独立(d-分离)的变量在条件于碰撞变量时变得相关的情况。

许多作者会混淆d-分离和条件独立。请牢记两者的区别:G\perp_G​ 是针对图的,而 是针对分布的。区分它们很重要,因为正如你将在本章后面看到的,我们将用d-分离测试因果假设与数据中条件独立的统计证据是否一致。

image.png

让我们通过几个例子来理解。

链式结构示例 I→M→JI \to M \to JI→M→J

考虑图4.6中的DAG,其中路径 P 为 uimjvu \to i \to m \to j \to v。这条路径默认是d-连接的。现在令 Z={m,k}Z = \{m, k\}。路径 P 包含链结构 imji \to m \to j,且中间节点 mZm \in Z。如果我们在 Z 上进行阻断,第一个条件得到满足,因此 u 和 v 被d-分离。

对于部分情况,一个有助于理解d-分离的比喻是电子电路。没有碰撞变量的路径是d-连接的,就像闭合电路,电流畅通无阻。在路径上的节点被“阻断”时,相当于“断开电路”,电流无法通过。阻断集合 Z(特别是阻断在 m 上,mZm \in Z)就像断开了电路,如图4.7所示。

image.png

图4.7 该路径默认是d-连接的,但在 mZm \in Z上阻断后,路径被d-分离,形象地说就是断开了电路(“\in”表示“属于”)。

链式结构示例 IMJI \leftarrow M \to J

现在考虑图4.8中的DAG,其中路径 P 是 uimjvu \leftarrow i \leftarrow m \to j \to v。这条路径默认同样是d-连接的。请注意,d-连接有时与因果关系的方向相反。

在图4.7中,u 到 v 的d-连接路径顺着因果方向进行:uiuiu \to i(u \leftarrow i),然后是 imimi \to m(i \leftarrow m),接着是 mjm \to j,再到jv j \to v

但在这里,我们有两个逆因果方向的步骤,分别是 uiuiu \to i(u \leftarrow i)imim i \to m(i \leftarrow m)

image.png

图4.8 集合 {m}\{m\} 是否对路径 uimjvu \leftarrow i \leftarrow m \to j \to v 进行d-分离?

假设我们在集合 Z 上进行阻断,且 Z 仅包含节点 m。那么条件3得到满足,路径被d-分离,如图4.9所示。

image.png

图4.9 这条从 u 到 v 的路径默认也是d-连接的,尽管它包含一些逆因果方向的步骤(u 到 i,以及 i 到 m)。同样,阻断集合 Z 中的 m 会对路径进行d-分离,形象地说就是断开了电路。

碰撞变量让d-分离变得复杂

第四条准则关注碰撞变量模式 imji \to m \leftarrow j:路径 P 包含碰撞结构 imji \to m \leftarrow j,且中间节点 m 不在阻断集合 Z 中,并且 m 的任何后代节点也不在 Z 中。

让我们回到血型的例子。这里的 i 和 j 是父母的血型,m 是孩子的血型。我们看到碰撞变量有点奇怪,因为条件于碰撞变量(孩子的血型)会使两个原本独立的变量(父母的血型)变得相关。这种奇怪的性质使得d-分离初看起来有点难以理解。图4.10展示了碰撞变量如何影响d-分离。

image.png

图4.10 碰撞变量使得d-连接变得复杂。对于路径上的一个节点 m,如果 m 不是碰撞变量,则该路径默认是d-连接的,当你在 m 上阻断时路径被d-分离。如果 m 是碰撞变量,则该路径默认是d-分离的,当你在 m 上阻断时路径变为d-连接。

关于碰撞变量,以下规律成立:

  • 两个节点之间的所有路径默认都是d-连接的,除非路径中存在碰撞变量模式。含有碰撞变量的路径默认是d-分离的。
  • 在d-连接路径上阻断任一节点,都会使路径d-分离,除非该节点是碰撞变量。
  • 阻断碰撞变量节点本身,或者阻断该碰撞变量的后代节点,都会使路径d-连接。

image.png

图4.11 集合 {m}\{m\}(或 {k}\{k\}{m,k}\{m, k\})是否对路径 uimjvu \to i \to m \leftarrow j \to v进行d-分离?

用电路比喻来说,碰撞变量就像一个打开的开关,会阻止电路中电流的流动。当路径上有碰撞变量时,碰撞点会阻断所有通过它的电流。碰撞变量相当于断开电路。阻断碰撞变量就像关闭开关,此时之前不能通过的电流现在可以通过(即变成d-连接)。

在图4.11中的DAG里,路径 uimjvu \to i \to m \leftarrow j \to v默认是d-连接的吗?不是,因为路径包含碰撞结构mimj m(i \to m \leftarrow j)

现在考虑如果阻断集合 Z 包含 m 会发生什么。在这种情况下,条件4被违反,路径变成d-连接,如图4.12所示。

image.png

图4.12 这条从 u 到 v 的路径默认是d-分离的,因为它包含一个碰撞变量 m。碰撞变量类似于断开的电路。阻断 m 或其任何后代节点会使路径变成d-连接,形象地说就是关闭了电路。

如果阻断集合 Z 中没有 m,但包含 k(或者同时包含 m 和 k),路径同样会变成d-连接。阻断碰撞变量的后代节点导致d-连接的机制与阻断碰撞变量本身相同。

你能猜到为什么吗?因为碰撞变量的后代与碰撞变量是d-连接的。用因果术语来说,我们之前看到,已知母亲的血型,观察孩子的血型(碰撞变量)可能揭示父亲的血型。假设我们不是观察孩子的血型,而是观察孙子的血型(即孩子的孩子的血型)。孙子的血型能够帮助缩小对孩子血型的范围,从而间接缩小对父亲血型的范围。换句话说,如果给定孩子的血型,母亲和父亲的血型是相关的,而孙子的血型提供了关于孩子血型的信息,那么给定孙子的血型,母亲和父亲的血型也是相关的。

d-分离与节点集合

d-分离不仅适用于节点对,也适用于节点集合对。在符号 uvZu \perp v \mid Z中,Z 可以是一组阻断节点,u 和 v 也可以是节点集合。我们通过阻断两集合成员之间所有d-连接路径来d-分离两个节点集合。其他基于图的因果概念,比如do-演算,也可推广到节点集合。如果记住这一点,我们就能从单个节点建立直觉,而这种直觉能够推广到节点集合。

当阻断集合 Z 是单节点集合 {m}\{m\} 时,该集合足以阻断图4.7中的路径 vuimjv→vu \to i \to m \to j \to v 和图4.8中的路径 uimjvu \leftarrow i \leftarrow m \to j \to v。总体来看,集合 {i},{m},{j},{i,m},{i,j},{m,j},{i,m,j}\{i\}, \{m\}, \{j\}, \{i, m\}, \{i, j\}, \{m, j\}, \{i, m, j\} 都能对这两条路径上的 u 和 v 进行d-分离。然而,{i},{m},{j}\{i\}, \{m\}, \{j\} 是最小的d-分离集合,意味着所有其他d-分离集合至少包含其中一个。最小的d-分离集合足以d-分离两个节点。

在推理d-分离和实现相关算法时,我们希望关注寻找最小d-分离集合;如果 UVZU \perp V \mid Z UVZ,WU \perp V \mid Z, W 都成立,我们不想浪费精力在 UVZ,WU \perp V \mid Z, W 上。

4.2.2 多路径d-分离的示例

假设我们想要对两个节点进行d-分离。通常这两个节点之间存在多条d-连接的路径。要实现d-分离,我们需要找到阻断每一条路径的节点集合。让我们通过一些例子来演示。

找到最小d-分离集合

在一个边更多的大图中,两个节点之间的路径数量可能非常多。但通常较长的路径会作为阻断较短路径的副作用而被阻断。因此,我们可以先从较短的路径开始,逐步处理尚未被阻断的较长路径,直到没有未被阻断的路径为止。

例如,在图4.13中,节点 U 和 V 是d-连接的。哪些节点集合能够完全阻断它们?

在图4.13中,U 和 V 通过以下路径d-连接:

  • UIV U \to I \to V
  • UJVU \to J \to V
  • UJIVU \to J \to I \to V

首先,我们可以通过阻断节点 I 来d-分离路径 UIVU \to I \to V。接着,通过阻断节点 J 来d-分离路径 UJVU \to J \to V。此时,我们发现阻断集合 {I,J}\{I, J\} 已经同时d-分离了路径 UJIVU \to J \to I \to V,任务完成。

另一个例子,如何在图4.14中对节点 U 和 V 进行d-分离呢?

image.png

image.png

节点 U 和 V 之间有许多路径。让我们先列举三条最短路径:

  • UIVU \leftarrow I \to V
  • UMVU \leftarrow M \to V
  • UKVU \leftarrow K \to V

我们需要至少阻断集合 {I,M,K}\{I, M, K\} 中的一个节点,才能对这三条路径进行d-分离。注意,节点 U 还有另一个父节点 J,通过 J 有几条路径通向 V,但其中只有两条路径尚未被d-分离:UJLVUJKLVU \leftarrow J \to L \to V 和 U \leftarrow J \to K \leftarrow L \to V。节点 J 和 L 都可以阻断这些路径,因此我们可以用最小阻断集合 {I,M,K,J}{I,M,K,L}\{I, M, K, J\} 或 \{I, M, K, L\} 来d-分离 U 和 V。

注意,路径 UJKLVU \leftarrow J \to K \leftarrow L \to V 是d-连接的,因为我们最初将路径上的碰撞变量 K 加入了阻断集合。接下来,我们来看这种现象的另一个例子。

当阻断一条路径时,另一条路径可能变成d-连接

当你试图通过阻断某个节点(该节点在另一条路径上是碰撞变量)来d-分离 U 和 V 之间的一条路径时,可能会导致那条另一条路径变成d-连接。只要你接着对那条路径采取额外的阻断措施,这样是没有问题的。

为了说明这一点,考虑图4.15中的图。这个图足够简单,我们可以列举出所有路径。

image.png

图4.15 阻断节点 M 会阻断路径 UMVU \leftarrow M \to V,但会使路径 UIMJVU \leftarrow I \to M \leftarrow J \to V 变成d-连接,因为 M 是 I 和 J 之间的碰撞变量。因此,我们还需要额外阻断 I 或 J 才能d-分离路径 UIMJVU \leftarrow I \to M \leftarrow J \to V

让我们从这三条d-连接路径开始:

  • UMVU \leftarrow M \to V
  • UIMVU \leftarrow I \to M \to V
  • UMJVU \leftarrow M \leftarrow J \to V

路径 UIMJVU \leftarrow I \to M \leftarrow J \to V不是d-连接路径,因为 MMM 是该路径上的碰撞变量。

阻断这三条d-连接路径的最简单方法是阻断节点 M。然而,如果阻断了这个碰撞变量,路径 UIMJVU \leftarrow I \to M \leftarrow J \to V 会变成d-连接,所以我们还需要阻断 I 或 J。换句话说,最小的d-分离集合是 {I,M}{J,M}\{I, M\} 和 \{J, M\}

4.2.3 代码中的d-分离

如果你对d-分离仍有疑惑,不用担心。我们已经定义了描述图中节点路径的四个条件,这正是可以用图形库来实现的功能。在Python中,图形库NetworkX已经实现了d-分离的工具。你可以使用这些工具,在不同图上尝试实验,培养对d-分离的直觉。

设置你的环境

以下代码使用了pgmpy 0.1.24版本,pandas版本为2.0.3。

让我们验证之前图4.15中因果DAG的d-分离分析。

from networkx import is_d_separator      #1
from pgmpy.base import DAG     #2

dag = DAG([     #2
    ('I', 'U'),     #2
    ('I', 'M'),     #2
    ('M', 'U'),  #2
    ('J', 'V'),    #2
    ('J', 'M'),     #2
    ('M', 'V')     #2
])     #2

print(is_d_separator(dag, {"U"}, {"V"}, {"M"}))     #3
print(is_d_separator(dag, {"U"}, {"V"}, {"M", "I", "J"}))    #4
print(is_d_separator(dag, {"U"}, {"V"}, {"M", "I"}))     #5
print(is_d_separator(dag, {"U"}, {"V"}, {"M", "J"}))    #5

注释说明:

  • #1 NetworkX图形库实现了针对NetworkX图对象(如DiGraph,有向图)的d-分离算法。
  • #2 DAG是BayesianNetwork类的基类,其基类是NetworkX的DiGraph,因此is_d_separator函数适用于DAG和BayesianNetwork对象。
  • #3 构建图4.11中的图。在碰撞变量 M 上阻断会阻断路径 UMVU \leftarrow M \to V,但会使路径 UIMJVU \leftarrow I \to M \leftarrow J \to Vd-连接,因此返回False。
  • #4 在 M 上阻断会阻断路径 UMVU \leftarrow M \to V 并使路径 UIMJVU \leftarrow I \to M \leftarrow J \to Vd-连接,但可以通过阻断 I 和 J 来阻断该路径,因此返回True。
  • #5 阻断 I 和 J 都是多余的。最小的d-分离集合是 {"M","I"}{"M","J"}\{"M", "I"\} 和 \{"M", "J"\}

pgmpy的DAG类中还有一个 get_independencies 方法,可以枚举图中所有为真的最小d-分离语句。

from pgmpy.base import DAG

dag = DAG([
    ('I', 'U'),
    ('I', 'M'),
    ('M', 'U'),
    ('J', 'V'),
    ('J', 'M'),
    ('M', 'V')
])

dag.get_independencies()    #1
  • #1 获取图中所有为真的最小d-分离语句。

get_independencies 方法返回如下结果(根据环境不同,输出顺序可能略有差异):

(I ⊥ J)
(I ⊥ V | J, M)
(I ⊥ V | J, U, M)
(V ⊥ I, U | J, M)
(V ⊥ U | I, M)
(V ⊥ I | J, U, M)
(V ⊥ U | J, M, I)
(J ⊥ I)
(J ⊥ U | I, M)
(J ⊥ U | I, M, V)
(U ⊥ V | J, M)
(U ⊥ J, V | I, M)
(U ⊥ V | J, M, I)
(U ⊥ J | I, M, V)

注意,get_independencies 这个函数名有些误导;它实际上返回的是d-分离语句,而非条件独立语句。再次强调,不要混淆因果图中的d-分离和DGP所隐含的联合概率分布中的条件独立。牢记这一点将有助于你完成下一步任务:使用d-分离基于数据中条件独立的统计证据来检验DAG的因果假设。

4.3 反驳因果DAG

我们已经了解了如何构建因果DAG。当然,我们希望找到一个能够很好拟合数据的因果模型,因此现在我们要用数据来评估这个因果DAG。我们可以使用标准的拟合优度和预测统计量来评估拟合情况,但这里我们重点讨论如何用数据反驳我们的因果DAG,借助数据来证明我们的模型是错误的。

统计模型是为了拟合数据中的曲线和模式。没有“正确”的统计模型,只有拟合数据较好的模型。相比之下,因果模型超越数据,对数据生成过程(DGP)做出因果断言,而这些断言要么是真,要么是假。作为因果建模者,我们既要寻找拟合良好的模型,也要积极尝试反驳模型的因果断言。

反驳与波普尔

通过反驳来构建DAG的思路符合20世纪哲学家卡尔·波普尔(Karl Popper)提出的可证伪理论框架。波普尔因其科学哲学贡献而著名,尤其是他的证伪理论。波普尔认为,科学理论不能被证明为真,但可以被测试并可能被证伪,换句话说,就是被反驳。

我们采用一种“波普尔式”的建模方法,这意味着我们不仅仅想找到一个符合证据的模型,而是积极寻找能反驳我们模型的证据。一旦发现这样的证据,我们就放弃原模型,构建更好的模型,并不断重复这个过程。

d-分离是我们用来反驳的第一个工具。假设你构建了一个因果DAG,它隐含某种条件独立性。接着你在数据中寻找变量间存在依赖的证据——而你的DAG表明这些变量应该是条件独立的。如果找到了这样的证据,你就反驳了你的DAG。然后你回头修改因果DAG,直到在你的数据范围内无法再反驳它。

完成这一过程后,你就可以进入后续的因果推断流程。但请保持这种反驳的心态。如果你反复使用同一个因果DAG,你应始终寻找新的方法来反驳和迭代它。实际目标不是找到“真实”的DAG,而是找到一个难以被反驳的DAG。

4.3.1 重新审视因果马尔可夫性质

回顾一下,我们看到因果马尔可夫性质有两个方面:

  • 局部马尔可夫性质——在给定节点的父节点时,该节点与其非后代节点条件独立。
  • 马尔可夫分解性质——联合概率分布可以分解为各变量在给定其因果DAG中直接父节点条件下的条件分布的乘积。

现在我们将引入这一性质的第三个方面,称为全局马尔可夫性质。该性质表明,因果DAG中的d-分离蕴含联合概率分布中的条件独立性。用符号表示为

image.png

通俗地说,这个符号表示“如果在图 G 中,节点 U 和 V 被 Z d-分离,那么在给定 Z 的条件下,它们是条件独立的。”请注意,因果马尔可夫性质的三个方面只要有一个成立,那么三个方面都会成立。

全局马尔可夫性质为我们提供了一种直接反驳因果模型的方法。我们可以利用d-分离来设计统计检验,以检验条件独立性的存在。如果统计检验未通过,即拒绝了模型。

4.3.2 利用条件独立检验进行反驳

统计评估条件独立性的方法有多种,其中最直接的就是进行条件独立性的统计检验。pgmpy和其他库都使得运行条件独立检验变得相对简单。让我们再次回顾运输模型,如图4.16所示。

image.png

图4.16 运输模型。年龄(A)和性别(S)决定教育水平(E)。教育水平影响职业(O)和居住地(R)。职业和居住地共同影响交通方式(T)。

回顾我们的运输模型,我们收集了以下观测数据:

  • 年龄(A) — 记录为“young”(年轻),适用于29岁及以下个体;“adult”(成年人),适用于30到60岁(含)个体;“old”(老年),适用于61岁及以上个体。
  • 性别(S) — 个体自报性别,记录为男性(“M”)、女性(“F”)或其他(“O”)。
  • 教育水平(E) — 个体完成的最高教育或培训水平,记录为高中(“high”)或大学学位(“uni”)。
  • 职业(O) — 员工(“emp”)或自雇人士(“self”)。
  • 居住地(R) — 个体所在城市人口规模,记录为小城市(“small”)或大城市(“big”)。
  • 交通方式(T) — 个体偏好的交通工具,记录为汽车(“car”)、火车(“train”)或其他(“other”)。

在图中,EGTO,RE \perp_G T \mid O, R。因此我们测试条件独立性陈述 ETO,RE \perp T \mid O, R。统计假设检验包含零假设(记为 H0H_0​)和备择假设(记为 HaH_a​)。针对条件独立的统计假设检验通常将零假设定义为条件独立,备择假设为不条件独立。

统计假设检验利用 N 个关于变量 U,V,ZU, V, Z(来自探索性数据集)的观测值计算统计量。以下代码加载运输数据。加载后创建两个DataFrame,一个包含所有数据,一个仅包含前30行,以观察样本大小对显著性检验的影响。

代码示例4.3 加载运输数据

import pandas as pd
survey_url = "https://raw.githubusercontent.com/altdeep/causalML/master/datasets/transportation_survey.csv"
fulldata = pd.read_csv(survey_url)

data = fulldata[0:30]     #1
print(data[0:5])
#1 仅选取30条数据进行演示

这条 print(data[0:5]) 语句打印DataFrame的前五行:

       A  S     E    O      R      T
0  adult  F  high  emp  small  train
1  young  M  high  emp    big    car
2  adult  M   uni  emp    big  other
3    old  F   uni  emp    big    car
4  young  F   uni  emp    big    car

大多数条件独立性检验库实现的是频率学派的假设检验。此类检验根据统计量是否超过某阈值,得出支持零假设或备择假设的结论。这里“频率学派”指检验产生的统计量称为p值,阈值称为显著性水平,通常为0.05或0.01。

当p值大于显著性水平时,支持条件独立的零假设 H0H_0​;反之,支持变量依赖的备择假设 HaH_a​。这种频率学派方法保证了显著性水平是错误拒绝零假设(即错误判定依赖)概率的上限。

大多数软件库提供条件独立性检验工具,计算p值时会做特定数学假设。例如,我们可以运行一个假设检验统计量理论上服从卡方分布的条件独立检验,并据此推导p值。以下代码执行该检验。

代码示例4.4 条件独立性的卡方检验

from pgmpy.estimators.CITests import chi_square     #1
significance = .05     #2

result = chi_square(    #3
    X="E", Y="T", Z=["O", "R"],     #3
    data=data,     #3
    boolean=False,   #3
    significance_level=significance     #3
)     #3
print(result)

注释说明:

  • #1 导入卡方检验函数。
  • #2 设置显著性水平为0.05。
  • #3 当 boolean=False 时,检验返回三元组,包含卡方统计量、对应的p值(本例为0.56)和自由度(用于计算p值)。

该代码打印三元组 (1.1611111111111112, 0.5595873983053805, 2),依次为卡方统计量、p值和自由度。由于p值大于显著性水平,该检验支持条件独立的零假设。换言之,本次检验未提供反驳我们模型的证据。

我们也可以将 chi_square 函数的 boolean 参数设置为 True,让函数返回简单的True或False。p值大于显著性水平返回True(支持条件独立),否则返回False(支持变量依赖)。

代码示例4.5 布尔型卡方检验结果

from pgmpy.estimators.CITests import chi_square     #1
significance = .05     #2
result = chi_square(     #3
    X="E", Y="T", Z=["O", "R"],   #3
    data=data,    #3
    boolean=True,    #3
    significance_level=significance  #3   
)    #3
print(result)

输出结果为 True

接下来,我们遍历运输图中所有的d-分离语句,并逐一测试。以下脚本将打印每个d-分离语句及对应条件独立检验的结果。

代码示例4.6 针对每条d-分离语句运行卡方检验

from pprint import pprint
from pgmpy.base import DAG
from pgmpy.independencies import IndependenceAssertion
from pgmpy.estimators.CITests import chi_square

dag = DAG([
    ('A', 'E'),
    ('S', 'E'),
    ('E', 'O'),
    ('E', 'R'),
    ('O', 'T'),
    ('R', 'T')
])
dseps = dag.get_independencies()    

def test_dsep(dsep):
    test_outputs = []
    for X in list(dsep.get_assertion()[0]):
        for Y in list(dsep.get_assertion()[1]):
            Z = list(dsep.get_assertion()[2])
            test_result = chi_square(
                X=X, Y=Y, Z=Z,
                data=data,
                boolean=True,
                significance_level=significance
            )
            assertion = IndependenceAssertion(X, Y, Z)
            test_outputs.append((assertion, test_result))
    return test_outputs

results = [test_dsep(dsep) for dsep in dseps.get_assertions()]
results = dict([item for sublist in results for item in sublist])
pprint(results)

结果是d-分离语句列表,以及数据是否支持(或未反驳)该语句的布尔值:

{(O ⊥ A | R, E, T, S): True,
 (S ⊥ R | E, T, A): True,
 (S ⊥ O | E, T, A): True,
 (T ⊥ S | R, O, A): True,
 (S ⊥ O | R, E): True,
 (R ⊥ O | E): False,
 (S ⊥ O | E, A): True,
 (S ⊥ R | E, A): True,
 (S ⊥ R | E, T, O, A): True,
 (S ⊥ R | E, O, A): True,
 (O ⊥ A | E, T): True,
 (S ⊥ O | R, E, T): True,
 (R ⊥ O | E, S): False, 
 …
 (T ⊥ A | E, S): True}

我们来统计通过检验的数量。

代码示例4.7 计算d-分离语句中通过检验的比例

num_pass = sum(results.values())
num_dseps = len(dseps.independencies)
num_fail = num_dseps - num_pass
print(num_fail / num_dseps)

输出为 0.2875,即约29%的d-分离语句在数据中缺乏相应的条件独立证据。

这个数字看起来较高,但正如我们将在4.4节看到的,这一统计指标依赖于数据规模及其他因素。我们希望将其与其他候选DAG的结果进行比较。目前,下一步是检查这些明显存在依赖的情况——而我们的DAG却预期它们应条件独立。如果依赖的证据充分,我们需要考虑如何改进因果DAG以加以解释。

之前用过的 chi_square 函数构造了特定的检验统计量,该统计量理论上服从卡方分布——这是用来计算p值的分布。卡方分布是经典分布之一,类似于正态分布或伯努利分布。卡方分布在离散变量统计中很常见,因为在离散情形下,有多种检验统计量理论上服从卡方分布,或随着数据量增大趋近于卡方分布。总体来说,独立性检验有各种不同的检验统计量及其分布。pgmpy通过调用SciPy的stats库提供了多种选项。

一个常见的担忧是检验所做的假设较强。例如,一些连续变量间的条件独立检验假设变量间的依赖为线性关系。另一种方法是使用置换检验,这是一种不依赖于经典检验分布的算法,通过重新排列数据来构造p值。置换检验假设较少,但计算代价较高。

4.3.3 有些检验比其他检验更重要

之前的分析测试了因果DAG所隐含的所有d-分离关系。但对你来说,有些d-分离可能比其他的更重要。有些依赖关系和条件独立关系对后续的因果推断分析至关重要,而另一些则对该分析完全没有影响。

例如,考虑图4.17,我们在第3.3节中曾经提到过。我们在图中加入变量 Z,因为可能想将其用作估计因果效应时的“工具变量”。

image.png

图4.17 中,Z、X0 和 X1 被包含在有向无环图(DAG)中,因为它们在分析 U 对 Y 的因果效应时起到了有用的作用。它们的作用依赖于条件独立性,因此检验它们确实能起到这些作用非常重要。

我们将在第11章深入讨论工具变量(instrumental variables)。目前,简单说一下,为了让 Z 成为一个工具变量,它必须与 W0、W1 和 W2 独立。因此我们会特别关注对这一假设的检验。

4.4 条件独立性检验的注意事项

如前所述,条件独立性检验可能是检验你所提出的因果DAG所隐含条件独立约束最直观的方法。然而,使用统计检验来验证因果DAG存在若干注意事项。根据我的经验,这些问题常常会让分析师偏离其最终的目标——解答因果问题。在本节,我将重点说明其中的一些注意事项,并提出一些条件独立检验的替代方案。主要结论是:统计检验是构建DAG的重要工具,但正如任何统计方法一样,它不是万能的(这本身也没问题)。

4.4.1 统计检验总有一定的错误概率

我提到过,d-分离(d-separation)时不要“将地图当作地形”,d-分离不等同于条件独立。换句话说,如果你的模型是因果的良好表示,则d-分离蕴含条件独立。

同理,条件独立也不等同于统计上对条件独立的证据。数据生成过程(DGP)的因果结构对联合概率分布施加条件独立约束。但你无法直接“看到”联合分布及其中的独立性,只能看到(并基于)从该分布中采样得到的数据。

像预测、分类或任何其他统计模式识别过程一样,检测数据中独立性的程序也会犯错。可能出现假阴性,即两变量实际上条件独立,但统计检验却判断为依赖;也可能出现假阳性,即统计检验判断两变量条件独立,实际上它们不独立。

4.4.2 用传统条件独立(CI)检验测试因果DAG存在缺陷

我说用于反驳的条件独立检验“存在缺陷”,是因为它们违背了科学统计假设检验的精神。比如你认为发现了股票价格中的某个模式,你有偏见倾向于认为它非巧合,因为若是如此,你就能赚钱。为严谨避免偏见,备择假设认为模式真实且可利用,零假设是该模式只是随机噪声。频率学派检验假设零假设为真,并给出 p 值,量化随机噪声形成类似模式的概率。检验迫使你只有当 p 值非常小才拒绝模式是随机的假设。大多数主流统计库就是针对这类场景设计。

但在你提出因果模型时,你也倾向相信它是真的。但因果模型诱导的是条件独立,而条件独立本质上是“无模式”。这时零假设和备择假设应当对调:备择假设是模型正确且不存在模式(数据中任何模式证据只是虚假相关),零假设是存在模式。实现这种检验是可能的,但数学上不简单,且大多数主流统计库(如SciPy)不支持这种用例。

折中方案是继续用传统检验,将零假设条件独立视为一种启发式——一种经验问题解决技术,虽不理论严谨,但足以达到“足够好”的解决方案。

4.4.3 p 值会随数据规模变化

传统条件独立检验的结论依赖显著性阈值。如果 p 值低于该阈值,你倾向认为变量依赖,反之则倾向条件独立。阈值选择较为任意,人们常用0.1、0.05或0.01。

问题是 p 值统计量会随数据规模变化。其它条件相同,数据越大,p 值越小。换句话说,数据越大,越容易看起来变量间存在依赖。如果数据量大,p 值更可能低于阈值,导致数据看起来否定了DAG所隐含的条件独立,即使条件独立是真的。

举例来说,4.3.2节中对 E ⊥ T | O, R 的检验样本数为30,p值为0.56。在我们的数据中,E ⊥ T | O, R 是模拟的真实事实,因此若检验得出反对结论,是检验本身的统计问题,而非数据质量问题。下面的自助法(bootstrap)统计分析将展示 p 值随数据量增大而下降的趋势。

以下是用于此分析的代码说明和流程:

from numpy import mean, quantile

def sample_p_val(data_size, data, alpha):
    bootstrap_data = data.sample(n=data_size, replace=True)
    result = chi_square(
        X="E", Y="T", Z=["O", "R"],
        data=bootstrap_data,
        boolean=False,
        significance_level=alpha
    )
    p_val = result[1]
    return p_val

def estimate_p_val(data_size, data=fulldata, boot_size=1000, α=.05):
    samples = [
        sample_p_val(data_size, data=fulldata, alpha=α)
        for _ in range(boot_size)
    ]
    positive_tests = [p_val > α for p_val in samples]
    prob_conclude = mean(positive_tests)
    p_estimate = mean(samples)
    quantile_05, quantile_95 = quantile(samples, [.05, .95])
    lower_error = p_estimate - quantile_05
    higher_error = quantile_95 - p_estimate
    return p_estimate, lower_error, higher_error, prob_conclude

data_size = range(30, 1000, 20)
result = list(zip(*[estimate_p_val(size) for size in data_size]))
  • 该函数根据给定数据规模,从完整数据中有放回地随机采样指定数量样本,进行卡方独立性检验并返回p值。
  • 通过多次采样(1,000次)获得p值分布,计算均值和90%置信区间,以及结论为“支持条件独立”的概率(即正确拒绝错误依赖的概率)。

接着,我们对结果进行可视化:

import numpy as np
import matplotlib.pyplot as plt

p_vals, lower_bars, higher_bars, probs_conclude_indep = result
plt.title('数据规模 vs. p值(E 与 T 条件于 O 和 R 的独立性)')
plt.xlabel("数据样本数")
plt.ylabel("期望 p 值")
error_bars = np.array([lower_bars, higher_bars])
plt.errorbar(
    data_size,
    p_vals,
    yerr=error_bars,
    ecolor="grey",
    elinewidth=.5
)
plt.hlines(significance, 0, 1000, linestyles="dashed")
plt.show()

plt.title('给定数据规模下支持条件独立的概率')
plt.xlabel("数据样本数")
plt.ylabel("支持条件独立的概率")
plt.plot(data_size, probs_conclude_indep)

图4.18展示了第一个图。下降的曲线表示不同数据规模下的期望p值,垂直线为90%自助法置信区间。当样本数达到1000时,期望p值低于显著性阈值,意味着检验倾向得出 ETO,RE ⊥ T | O, R 不成立的结论。

image.png

图4.18 展示了样本量与条件独立性检验中 ETO,RE \perp T \mid O, R 期望 p 值的关系(实线)。垂直线为误差条,表示90%的自助法(bootstrap)置信区间。水平虚线表示显著性水平0.05,p 值高于该线时,我们倾向接受条件独立的零假设,低于该线时则拒绝。

随着样本量增加,p 值最终会越过这条线。因此,我们的反驳分析结果取决于数据规模。

需要注意的是,置信区间的下界在样本量远未达到1000之前就已穿越显著性阈值,这表明即使在更小的数据规模下,我们也很可能错误地拒绝 ETO,RE \perp T \mid O, R的真实结论。图4.19中表现得更为明显,即随着数据规模增大,支持真实结论的概率反而降低。

image.png

你可能会认为,随着数据量增加,算法是在检测 E 和 T 之间以前因数据不足而无法发现的微妙依赖关系。事实并非如此,因为这份交通数据是经过模拟生成的,保证了 E⊥T∣O,RE \perp T \mid O, RE⊥T∣O,R 是绝对成立的。这种情况下,更多数据反而导致我们拒绝独立性,因为更多数据会引入更多虚假相关——实际上不存在的模式。

一个因果模型关于它描述的数据生成过程(DGP)中的因果关系,要么是对的,要么是错的。模型所隐含的条件独立性,要么存在,要么不存在。即便条件独立性确实存在,统计检验仍可能在数据量无限增大时错误地支持依赖。

再次强调,如果把条件独立检验看作是用来反驳因果DAG的一种启发式方法,那么这种对数据规模敏感的现象不必让我们过于担忧。无论数据规模和显著性阈值如何变化,当存在条件独立和不存在条件独立时,p值的相对差异仍将明显且巨大。

4.4.4 多重比较问题

在统计假设检验中,做的检验越多,累计的错误率也越高。对因果DAG中每一个d-分离关系都做检验时也是如此。统计学中将此称为多重比较问题。解决办法有很多,比如使用假发现率(false discovery rate)控制。如果你熟悉这类方法,应用它们无妨。若想深入了解,请参考本章注释中的网址:www.altdeep.ai/p/causalaib…,那里有关于因果建模中假发现率的相关参考。但我还是建议你把传统的条件独立检验看成一种启发式工具,目的是辅助你构建一个好的因果DAG。将注意力集中在这个目标和后续的因果推断分析上,避免陷入统计检验严谨性的小坑。

4.4.5 条件独立检验在机器学习中的挑战

常用的条件独立检验库通常只能处理一维变量,并且变量间的相关关系相对简单。pgmpy 的条件独立检验(底层调用 SciPy)也不例外。近年来,出现了若干适用于更复杂分布的非参数检验方法,如基于核函数的条件独立检验。如果你对此感兴趣,可以从 PyWhy 库中的 PyWhy-Stats 模块开始尝试。

然而,机器学习中变量往往是多维的,比如向量、矩阵、张量。例如,因果DAG中的一个变量可能代表一张由像素构成的图像矩阵。更重要的是,这些变量间的统计关联往往是非线性的。

一个解决思路是关注预测能力。如果两个变量独立,它们无法相互预测。假设有两个预测模型 M1 和 M2,M1 使用 Z 预测 Y,M2 使用 X 和 Z 预测 Y。预测变量可以是多维的。如果 XYZX \perp Y \mid Z,那么 X 对 Y 的预测信息不应超过 Z 已提供的信息。于是,可以通过比较 M2 和 M1 的预测准确率来检验条件独立。当两个模型表现相近时,有条件独立的证据。需要注意的是,要避免 M2 因过拟合而“作弊”,这又是虚假相关可能侵入分析的另一种途径。

4.4.6 总结思考

条件独立检验是一个广泛且细致的话题。你的目标是利用检验来反驳你的因果DAG,而不是打造一个完美无缺的条件独立检验套件。我建议建立一个“足够好”的检验流程,然后集中精力构建你的DAG,并利用该DAG进行后续的因果推断分析。举例来说,如果我有连续变量和离散变量混合的数据,宁愿将连续变量离散化(比如把年龄这个连续时间变量转成年龄区间),再用简单的卡方检验,也不愿费力实现一个能兼容多种数据类型的复杂检验,从而保证项目推进。

4.5 在存在潜变量的情况下反驳因果DAG

利用条件独立性检验来测试DAG的方法存在潜变量问题。如果因果DAG中的某个变量是潜变量(即数据中未观测到的变量),我们就无法对涉及该变量的条件独立性进行检验。这是一个重大问题;如果某个变量是数据生成过程(DGP)中的重要组成部分,我们不能仅因为无法用该变量进行独立性检验就将其排除在DAG之外。

举例说明,见图4.20。该图表示吸烟行为(S)同时受香烟价格(C)和遗传因素(用 D 表示,代表“DNA”)的影响,遗传因素决定了个体对尼古丁成瘾的易感程度。同样,这些遗传因素也会影响患肺癌(L)的可能性。在这个模型中,吸烟对肺癌的影响是通过肺部焦油积累(T)这一中介变量实现的。

image.png

图4.20 展示了一个因果DAG,表示吸烟对癌症的影响。遗传变量(D)呈灰色,因为数据中未观测到该变量,因此无法对涉及D的条件独立性进行检验。不过,我们可以检验其他类型的约束。

如果我们能观测到所有这些变量的数据,就可以针对以下d-分离关系运行条件独立性检验:(C ⊥G T | S)、(C ⊥G L | D, T)、(C ⊥G L | D, S)、(C ⊥G D)、(S ⊥G L | D, T)和(T ⊥G D | S)。但假设我们没有遗传变量(D)的数据,例如测量遗传特征需要昂贵且侵入性的实验室检测。在我们列出的六个d-分离中,唯一不涉及D的是(C ⊥G T | S)。这样我们可行的条件独立检验从六个减少到一个。

一般而言,一个提出的因果模型会对联合概率分布产生各种可用数据检验的含义。图结构隐含的条件独立是一类可检验的含义。但即使存在潜变量,模型的某些含义依然是可检验的。在本节中,我们将探讨如何利用这类与潜变量相关的约束检验一个DAG。

4.5.1 一个适用于潜变量的可检验含义示例

因果马尔可夫假设指出,d-分离蕴含数据中的条件独立。到目前为止,我们主要探讨了变量间的直接条件独立,但当一些变量是潜变量时,图结构可能隐含观测变量函数之间的条件独立。这类隐含被文献称为“Verma约束”,但我会用更通俗的“函数约束”来表示。

举例来说,图4.20中包含潜变量D的DAG具有以下函数约束(暂时不用担心它是如何推导出来的):

image.png

正如 d-分离关系 (CGTS)(C \perp_G T \mid S)蕴含观测联合分布中条件独立陈述 (CTS)(C \perp T \mid S)应当成立一样,函数约束 (CGh(L,C,T)))(C \perp_G h(L, C, T))) 蕴含在观测联合分布中,变量 C 与某个关于 L,C,TL, C, T 的函数 h()h(\cdot) 是独立的。由于这两个约束都不涉及潜变量 D,因此它们都是可检验的。这样,我们就有了两个可执行的检验,而不是只有一个。

函数 h()h(\cdot) 由两部分组成:

  • P(lc,s,t)P(l \mid c, s, t) 是一个函数,返回在给定 C=c、S=s 和 T=t 条件下,L=l 的概率(假设 l 为“真”表示“患肺癌”,为“假”表示“未患肺癌”)。
  • P(sc)P(s \mid c) 是一个函数,返回在给定香烟价格 C=c 条件下,S=s 的概率(假设 s 表示吸烟量,分为“低”、“中”、“高”三个等级)。

函数 h()h(\cdot) 对 S 的所有取值进行求和。根据该DAG,函数的输出是一个随机变量,应该与 C 独立。h(l,c,t)h(l, c, t) 是由 P(lc,s,t)P(l \mid c, s, t)P(sc)P(s \mid c) 构成的函数,用概率函数来思考独立性可能感觉有些抽象,但请记住,独立关系本身就是联合概率分布的函数。

接下来,我们将根据数据拟合 P(lc,s,t)P(l \mid c, s, t)P(sc)P(s \mid c) 的模型,并检验这一独立性关系。但首先,我们会看看有哪些库能够像 pgmpy 的 get_independencies 方法那样,从DAG中枚举函数约束(如 CGh(L,C,T)C \perp_G h(L, C, T)),就像我们枚举 d-分离关系一样。

4.5.2 关于测试函数约束的库与视角

我们如何推导像 C⊥Gh(L,C,T)C \perp_G h(L, C, T)C⊥G​h(L,C,T) 这样的函数约束?和 d-分离一样,我们可以从图中算法化地推导出这类约束。一个实现是在 R 语言的 causaleffect 库中的 verma.constraints 函数。该函数接受标记了潜变量节点的DAG,返回一组可测试的约束,就像 pgmpy 的 get_independencies 一样。对于 Python,Y0(读作“why-not”)库提供了 r_get_verma_constraints 函数(截至版本0.2.10),这是调用 causaleffect R代码的包装器。这里我不展示 Python 代码,因为需要安装 R,但你可以访问 www.altdeep.ai/causalAIboo… 获取相关库和参考资料链接。


函数约束的数学直觉与建议

本节的目标仅是展示即便存在潜变量,也有方法测试你的因果模型。函数约束是其中一种方法,但我们不应只盯着这种可检验含义的特定形式。更重要的是避免陷入只接受数据完全观测的DAG的危险思维。

简单介绍数学直觉:局部马尔可夫性质指出,给定一个节点的父节点,该节点与其非后代节点条件独立。基于此,我们定义了 d-分离的图形准则来找出满足条件的节点集合,并编写图算法枚举所有 d-分离,从而找出可做的条件独立性检验。

对某节点 XXX 来说,“孤儿堂兄”指的是与 XXX 共享潜变量祖先但不是其后代的节点。非正式地说,潜变量版本的局部马尔可夫性质是:一个节点在给定其最近的观测祖先、孤儿堂兄以及这些堂兄的最近观测祖先时,与其非后代条件独立。类似 d-分离,我们也可以推导图形准则,识别满足这一性质的情况。

回顾联合概率分布的因式分解,每个因子是节点给定父节点的条件概率。函数约束中涉及的概率函数(如 h(l,c,t)h(l,c,t)h(l,c,t) 中的 P(l∣c,s,t)P(l|c,s,t)P(l∣c,s,t) 和 P(s∣c)P(s|c)P(s∣c))是将因式分解对潜变量边缘化并做概率运算后的结果。

如果想深入了解,参考 www.altdeep.ai/p/causalaib… 中列出的文献。但请牢记前节提醒——目标是用检验反驳DAG,快速进入因果推断分析,别在统计、数学和理论细节中钻牛角尖。

既然我们得到了新的可测试含义 C⊥Gh(L,C,T)C \perp_G h(L,C,T)C⊥G​h(L,C,T),接下来就尝试测试它。


4.5.3 测试函数约束

为了测试 CGh(L,C,T)C \perp_G h(L,C,T),我们需要计算

h(l,c,t)=sP(lc,s,t)P(sc)h(l,c,t) = \sum_{s} P(l|c,s,t) \cdot P(s|c)

对数据中的每一条记录都要计算该值,这就需要我们对 P(l∣c,s,t) 和 P(s∣c)建模。可用多种建模方法,这里示例用朴素贝叶斯分类器,这样可以利用 pgmpy 和 pandas 库。

步骤如下:

  1. 对成本 C 离散化,使其成为离散变量。
  2. 用 pgmpy 拟合朴素贝叶斯分类器分别建模 P(l∣c,s,t) 和 P(s∣c)。
  3. 编写函数输入 L, C, T,计算 h(L,C,T)。
  4. 将该函数应用于数据每一行,得到 h(L,C,T) 新列。
  5. 对 h(L,C,T) 列与 C 列做独立性检验。

环境配置

以下代码用 pgmpy 版本 0.1.19,因 0.1.24 及以前版本(写作时的最新版本)存在朴素贝叶斯推断的已知BUG。若用其他方法计算概率,可不必拘泥此版本。为保证稳定性,建议配合 pandas 1.4.3 使用。若你已安装更新版本,可能需要卸载后重装或新建 Python 环境。详情及代码示例见 www.altdeep.ai/p/causalaib…

代码示例(部分)

from functools import partial
import numpy as np
import pandas as pd

data_url = "https://raw.githubusercontent.com/altdeep/causalML/master/datasets/cigs_and_cancer.csv"
data = pd.read_csv(data_url)     # 读取数据

# 离散化成本C为三类
cost_lower = np.quantile(data["C"], 1/3)
cost_upper = np.quantile(data["C"], 2/3)

def discretize_three(val, lower, upper):
    if val < lower:
        return "Low"
    if val < upper:
        return "Med"
    return "High"

data_disc = data.assign(
    C=lambda df: df['C'].map(
        partial(discretize_three, lower=cost_lower, upper=cost_upper)
    )
)
data_disc = data_disc.assign(
    L=lambda df: df['L'].map(str)
)
print(data_disc)

输出示例:

CSTL
HighMedLowTrue
MedHighHighFalse
............

建模 P(l∣c,s,t)

from pgmpy.inference import VariableElimination
from pgmpy.models import NaiveBayes

model_L_given_CST = NaiveBayes()
model_L_given_CST.fit(data_disc, 'L')
infer_L_given_CST = VariableElimination(model_L_given_CST)

def p_L_given_CST(L_val, C_val, S_val, T_val):
    result_out = infer_L_given_CST.query(
        variables=["L"],
        evidence={'C': C_val, 'S': S_val, 'T': T_val},
        show_progress=False
    )
    var_outcomes = result_out.state_names["L"]
    var_values = result_out.values
    prob = dict(zip(var_outcomes, var_values))
    return prob[L_val]

建模 P(s∣c)

model_S_given_C = NaiveBayes()
model_S_given_C.fit(data_disc, 'S')
infer_S_given_C = VariableElimination(model_S_given_C)

def p_S_given_C(S_val, C_val):
    result_out = infer_S_given_C.query(
        variables=['S'],
        evidence={'C': C_val},
        show_progress=False
    )
    var_names = result_out.state_names["S"]
    var_values = result_out.values
    prob = dict(zip(var_names, var_values))
    return prob[S_val]

计算函数 h(L,C,T)

def h_function(L, C, T):
    summ = 0
    for s in ["Low", "Med", "High"]:
        summ += p_L_given_CST(L, C, s, T) * p_S_given_C(s, C)
    return summ

生成所有可能组合并计算 h

ctl_outcomes = pd.DataFrame(
    [        (C, T, L)        for C in ["Low", "Med", "High"]
        for T in ["Low", "High"]
        for L in ["False", "True"]
    ],
    columns=['C', 'T', 'L']
)

h_dist = ctl_outcomes.assign(
    h_func = ctl_outcomes.apply(
        lambda row: h_function(row['L'], row['C'], row['T']),
        axis=1
    )
)
print(h_dist)

将 h 合并到数据中

df_mod = data_disc.merge(h_dist, on=['C', 'T', 'L'], how='left')
print(df_mod)

输出示例:

CSTLh_func
HighMedLowTrue0.504475
MedHighHighFalse0.369767
...............

可视化 CCC 与 h(L,C,T)h(L,C,T)h(L,C,T) 的独立性

由于 C 已离散,理论上 h 是连续变量,为了观察两者关系,我们用箱型图可视化 h(L,C,T)h(L,C,T) 关于 C 的分布。

df_mod.boxplot("h_func", "C")

这会生成图4.21。

image.png

图4.21 是一个箱线图,横轴为成本(C),纵轴为函数 h(L, C, T)(标签为“Sum product”)。不同成本水平下的和积分布重叠,支持函数约束中这两者独立的断言。

图中横轴代表成本的不同水平(低、中、高),纵轴表示和的取值。箱线图中,每个箱体代表对应成本水平下和积的分布,箱体上下边缘为分布的四分位数,中间的线为中位数,较短的水平线表示最大值和最小值(例如低成本组的中位数、上四分位和最大值较为接近)。总体来看,和积的分布在不同成本水平间变化不大,这正是独立性应有的表现。

我们还可以使用方差分析(ANOVA)方法得到 p 值,这次采用 F 检验而非卡方检验。以下代码使用 statsmodels 库运行 ANOVA 测试。

注:“PR(>F)” 表示给定变量(此处为 C)观察到的 F 统计量至少与数据中计算出的 F 统计量一样大(假设变量与和积独立)的概率,即 p 值。

from statsmodels.formula.api import ols
import statsmodels.api as sm

model = ols('h_func ~ C', data=df_mod).fit()
aov_table = sm.stats.anova_lm(model, typ=2)
print(aov_table["PR(>F)"]["C"])

model = ols('h_func ~ T', data=df_mod).fit()
aov_table = sm.stats.anova_lm(model, typ=2)
print(aov_table["PR(>F)"]["T"])

model = ols('h_func ~ L', data=df_mod).fit()
aov_table = sm.stats.anova_lm(model, typ=2)
print(aov_table["PR(>F)"]["L"])
  • 这是用 statsmodels 库做 ANOVA 的示例代码。
  • 对变量 C 的检验返回较高的 p 值,支持(未能否定)h(L, C, T) 与 C 独立的断言。
  • 作为合理性检验,我们对 T 和 L 也进行了同样的测试。与 C 不同,T 和 L 不应与 h(L, C, T) 独立,结果如预期,p 值远小于显著水平,显示出依赖关系。

打印 C 对应的 p 值约为 0.1876,表明我们无法拒绝独立性的零假设,数据支持该约束。对 T 和 L 的测试 p 值较小,均低于通常的 0.1 阈值,说明 h(L, C, T) 与 T,L 有依赖性。

4.5.4 关于可检验含义的总结思考

如果一个 DAG 是数据生成过程的良好因果模型,则其 d-分离和函数约束隐含联合概率分布中应存在某些条件独立关系。我们可以通过统计检验这些条件独立性来反驳 DAG。

更广义地说,因果模型对联合概率分布有不同的数学含义,其中部分可被检验。例如,若模型假设因果关系为线性,可以在数据中寻找非线性证据(第6章将深入讨论函数因果假设)。当然,也可以通过实验来反驳模型的假设(第7章将介绍)。

随着我们对因果建模的进步,也将更擅长测试和反驳因果模型。但请记住,不要让统计和数学的细节分散你构建“足够好”模型并继续目标因果推断的注意力。

4.6 因果发现入门(及其风险)

在之前的流程中,我们提出了一个因果DAG,考虑了该DAG对观测联合分布隐含的约束(如条件独立),然后用数据检验了这些约束。那么,反过来呢?如果我们先从数据中寻找因果约束的统计证据,然后构建一个与这些约束一致的因果DAG呢?

这就是所谓的因果发现任务:从数据中进行因果DAG的统计学习。本节将简要介绍因果发现,并讲解使用这类算法时需要了解的内容。

警惕因果发现的虚假承诺

因果发现算法常被宣传为神奇工具,声称能把任何数据集(无论质量多差)都转换为因果DAG。这种虚假承诺会让人忽视对数据生成过程(DGP)建模以及对候选模型的反驳,从而导致实践中难以找到稳定的应用场景。本节将从算法工作原理和其局限性出发,而非一一介绍算法细节,最后提供如何有效将这些算法纳入分析流程的建议。

4.6.1 因果发现的主要方法

因果发现有几种方法:

  • 基于约束的方法(Constraint-based algorithms) :如前所述,从数据中的条件独立证据逆向推导图结构。
  • 基于评分的方法(Score-based algorithms) :将因果DAG视为数据的解释模型,寻找拟合优度(goodness-of-fit)高的因果DAG。
  • 结构因果模型(Structural Causal Models) :假设父节点与子节点间函数关系的额外约束,第6章会详细介绍。

因果DAG的可能空间是离散的。有一类方法尝试将该空间“软化”为连续空间,利用连续优化技术。自动微分库在深度学习中的普及加速了这一趋势。

因DAG空间庞大,整合先验知识以限制空间规模十分重要,通常表现为指定必须存在或必须不存在的边,或对图结构采用贝叶斯先验。

部分因果发现算法支持实验数据,需告知算法哪些变量是实验设定的(即第7章将讨论的“干预”变量)。

若想用 Python 开始因果发现,推荐 PyWhy 相关库,如 causal-learn 和 DoDiscover。

4.6.2 因果发现、因果忠实性与潜变量假设

因果马尔可夫性质假设:若DAG为真,则DAG中的 d-分离关系蕴含变量联合概率中的条件独立性:

image.png

因果忠实性(简称“忠实性”)是其逆命题——联合分布中的条件独立蕴含图中的 d-分离关系:

image.png

因果发现与忠实性违背

在第4.4节中,我们利用马尔可夫性质来检验候选DAG:给定一个在DAG中成立的d-分离关系,我们用统计检验去验证该d-分离所隐含的条件独立是否在数据中得到支持。

假设你想反向构建图结构:先在数据中检测出某个条件独立的证据,再限制候选DAG空间为与该d-分离一致的图。如此迭代,逐步缩小候选DAG空间。一些发现算法会执行类似的流程,而这些算法的前提正是忠实性假设。

注:将条件独立的证据匹配到d-分离关系的算法通常称为“基于约束”的发现算法,著名的例子是PC算法。基于约束的算法寻找与因果证据一致的DAG。

问题出在“忠实性违背”——指联合概率分布中的条件独立不对应真实DAG中的d-分离关系。一个简单的忠实性违背例子是三变量系统,其联合分布可分解为:

P(x,y,z)=P(x,y)P(y,z)P(x,z)P(x, y, z) = P(x, y) P(y, z) P(x, z)

即不论某变量取何值,另外两个变量之间的关联总是相同。你可能在数据中发现这种奇特的独立性,但用DAG的d-分离无法表示它。(如果你不信,可以尝试一下。)

研究人员对这类特例感到担忧,因为它意味着依赖忠实性假设的发现算法无法推广到所有分布。使用这些算法即意味着假设忠实性在你的问题域成立,而这无法被检验。然而,忠实性违背通常不是实际因果发现中最大的难题,最大难题往往是潜变量。

潜变量的挑战

更麻烦的是,大多数因果发现算法再次遭遇潜变量问题。举例来说,假设真实因果DAG是图4.22所示。

image.png

图4.22 假设这是真实的因果DAG。在这里,变量B、C和D在给定A的条件下彼此条件独立。

在该DAG中,B、C和D在已知A的情况下相互条件独立。现在假设数据中没有观测到A,即A是潜变量。由于A是潜变量,因果发现算法无法进行如 BCAB \perp C \mid A 这样的条件独立性检验。算法会检测到B、C和D之间存在依赖,但找不到在给定A时三者之间的条件独立性,可能最终返回如图4.23所示的DAG,以反映这些检测结果。

image.png

图4.23 如果变量 A 是潜变量,则无法进行以 A 为条件的条件独立性检验。算法会检测到 B、C 和 D 之间存在依赖,但找不到在给定 A 时的条件独立,可能最终返回如图所示的图结构。

解决该问题的方法是,在发现算法中提供关于潜变量结构的强领域特定假设。一些通用的发现算法支持潜变量假设(例如 causal-learn 库中就有部分支持),但这比较少见,因为让用户方便地指定领域特定假设,同时又能跨领域泛化,难度较大。

4.6.3 等价类与部分有向无环图(PDAG)

假设我们的算法能正确恢复数据中的所有真实条件独立陈述,并将其映射回真实的 d-分离集合(即因果忠实性成立)。现在面临的问题是,多种因果DAG可能具有相同的 d-分离集合。这组候选DAG称为马尔可夫等价类(Markov equivalence class)。真实的因果DAG可能只是该等价类中众多成员之一。

例如,假设图4.24左侧的DAG是真实DAG。图中右侧的DAG与真实图在 A 和 T 之间的边方向不同,但它们具有相同的 d-分离集合。事实上,我们也可以改变 {L, S} 和 {B, S} 之间边的方向,只要不引入新的碰撞节点(collider)(如 {L → S ← B}),否则碰撞节点的改变会改变 d-分离集合,从而不属于同一等价类。

image.png

图4.24 假设左侧的DAG是真实的因果DAG,右侧(错误的)DAG属于同一个马尔可夫等价类。中间的PDAG代表该等价类,其中无向边表示等价类成员之间在该边方向上存在分歧。

一些因果发现算法会返回部分有向无环图(PDAG),比如图4.24中间的DAG。PDAG中的无向边表示等价类成员在边的方向上存在不同意见。这很有用,因为我们得到了等价类的图形表示,算法也可以在PDAG空间中搜索,而不是更大的DAG空间。

碰撞节点与因果发现

碰撞节点在因果发现中非常重要,因为它们允许仅凭统计依赖的证据来确定DAG中边的方向。假设我们利用数据尝试构建图4.24中的真实DAG,发现数据中存在A与T之间的依赖关系。根据马尔可夫等价的概念,这种依赖证据不足以确定边的方向。一般来说,数据中的依赖和独立证据能暗示边的存在,但不能确定其方向。

碰撞节点是例外。仅凭独立与依赖的证据,就能检测出碰撞节点结构,如 {TEL}\{T \to E \leftarrow L\}:如果数据表明T与L独立,但在条件于E时变为依赖,这就表明存在一个由有向边组成的碰撞节点。

碰撞节点还能强制确定其外的边方向。例如,考虑图4.23中真实DAG里E与X之间的边。我们从以下数据证据推断这条边的存在:

  • E与X依赖。
  • T与X依赖。
  • 在条件于E时,T与X独立。

一条E与X之间的边符合上述证据,但应该是 EXE \to X 还是 EXE \leftarrow X?此时碰撞节点 {TEL}\{T \to E \leftarrow L\} 有帮助;它已确定边 TET \to E,若加上 EXE \leftarrow X,将导致另一个碰撞节点 {TEX}\{T \to E \leftarrow X\}。该碰撞节点会暗示T与X独立,但在条件于E时变依赖,这与前述证据冲突。故排除该可能性,确定边为 EXE \to X

某些因果发现算法实质上就是算法化地运用这类逻辑。但请记住,当潜变量导致观测变量之间依赖时,这类逻辑会失效。

此外,PDAG和马尔可夫等价类仅捕捉编码相同条件独立约束的DAG之间的等价性。如果你想找满足更多约束假设(如给定先验条件下具有相同后验概率的所有图),PDAG可能不够用。

仅凭条件独立,数据无法区分马尔可夫等价类的成员,因为相同的d-分离集合意味着相同的条件独立证据。这就是因果识别不足的例子——当数据和因果假设不足以唯一确定因果答案(此处即“正确的因果DAG是什么?”)时的情况。第10章将深入探讨因果识别。

4.6.4 如何看待因果发现

在第4.3节,我主张用现成的假设检验库测试因果诱导约束(如条件独立)应更多被视为反驳因果DAG的启发式方法,而非严格的统计验证程序。同样,对于实际用户,现成的因果发现算法应被视为人驱动因果DAG构建过程中的探索性数据分析工具。你越能将各种领域知识和潜变量知识输入这些算法,结果越好。但即便如此,它们仍会产生明显错误。正如假设检验一样,避免陷入“修复”发现算法错误的怪圈。应把因果发现当作构建良好因果DAG和后续因果推断分析的一个不完美工具。

总结

  • 因果建模会对联合概率分布施加条件独立性约束。d-分离提供了条件独立性约束的图形化表示。
  • 理解 d-分离对于推理因果效应和处理其他因果查询非常重要。
  • 虽然碰撞节点可能使 d-分离变得复杂,但你可以通过使用 NetworkX 和 pgmpy 中的 d-分离函数来培养直觉。
  • 使用传统的条件独立性检验库来测试 d-分离存在挑战:检验对样本量敏感,在许多机器学习场景中表现不佳,且其假设与实际不完全一致。
  • 由于这些挑战,最好将利用现成条件独立检验库反驳DAG的尝试视为一种启发式方法。应专注于构建一个“难以反驳”的良好因果DAG,并进入后续的因果推断任务。避免过分纠结统计假设检验的理论严谨性。
  • 存在潜变量时,因果DAG仍可能对观测变量的函数具有可检验的含义。
  • 因果发现是指利用统计算法从数据中恢复因果DAG。
  • 因果忠实性假设联合概率分布中的条件独立对应真实因果DAG中的一组真实d-分离。
  • 马尔可夫等价类是一组具有相同d-分离集合的DAG。假设你已得到真实的d-分离集合,真实因果DAG通常与其他错误DAG共享该集合。
  • 因果发现对潜变量尤其敏感。
  • 你能越多地用先验假设(如潜变量结构、必存在或必不存在的边)约束因果推断,效果越好。
  • 因果发现算法是构建因果DAG过程中的有用探索性数据分析工具,但不能替代该过程。再次强调,应关注构建良好因果DAG并进入后续因果推断分析的总体目标。避免试图“修复”因果发现算法以避免其在你的领域产生明显错误。