推理式人工智能-全-
推理式人工智能(全)
原文:
zh.annas-archive.org/md5/b32a89d10ca41281393b5058083c1870译者:飞龙
第一部分 概念基础
第一部分为理解并构建因果模型奠定了必要的基础。在这里,我将介绍来自统计学、概率建模、生成式机器学习和贝叶斯方法的关键概念,这些概念将作为本书因果建模方法的基石。本部分旨在为你提供开始使用机器学习工具解决因果问题的核心概念。
第一章:为什么因果 AI
本章涵盖
-
定义因果 AI 及其益处
-
将因果纳入机器学习模型
-
将因果应用到机器学习模型的一个简单例子
订阅流媒体平台如 Netflix 总是在寻找优化各种性能指标的方法。其中之一就是他们的流失率,即他们失去订阅者的速度。想象一下,你是一名 Netflix 的机器学习工程师或数据科学家,负责寻找减少用户流失的方法。针对这项任务,你可能会问哪些类型的因果问题(需要因果思考的问题)?
-
因果发现——给定关于谁流失和谁没有流失的详细数据,你能分析这些数据以找到导致流失的原因吗?因果发现研究什么原因导致什么。
-
估计平均处理效应(ATEs)——假设向用户推荐内容的算法是导致用户流失的原因;选择更好的算法可能会减少用户流失,但减少多少呢?量化平均而言,一个原因如何驱动一个效果的任务就是ATE 估计。例如,一些用户可能接触到算法的新版本,你可以测量这相对于基线算法对用户流失的影响有多大。
让我们深入一点。纪录片办公室(美国版)是 Netflix 上最受欢迎的节目之一。后来,Netflix 了解到 NBCUniversal 计划停止向 Netflix 许可该节目在美国流媒体播放,因此美国对办公室的流媒体播放将独家属于 NBCUniversal 的竞争对手流媒体平台 Peacock。鉴于该节目的受欢迎程度,流失率肯定受到影响,但影响有多大?
- 估计条件平均处理效应(CATEs)——失去办公室的影响对某些订阅者群体可能比其他群体更明显,但定义这些群体的属性是什么?一个属性当然是看过这个节目,但还有其他属性(人口统计信息、观看的其他内容等)。CATE 估计的任务是量化一个原因对特定人口群体中一个效果的影响程度。实际上,我们可能定义了多个群体,每个群体有不同的组内 ATE。CATE 估计任务的一部分是找到感兴趣的特定群体。
假设你拥有关于那些退出 Netflix 并注册 Peacock 以继续观看办公室的订阅者的可靠数据。对于这些用户中的某些人,推荐算法未能向他们展示办公室的可能替代品,如纪录片公园与游憩。这可能会引出不同类型的问题。
- 反事实推理和归因——如果算法在那些用户的仪表板上更突出地放置了《公园与游憩》,他们是否会继续使用 Netflix?这些反事实问题(“反”对“事实”即该节目在他们的仪表板上并不突出)对于归因(分配根本原因和结果的责任/责备)至关重要。
Netflix 与史蒂夫·卡瑞尔(《办公室》明星)和格雷格·丹尼尔斯(《办公室》的编剧、导演和制片人)合作,创建了 Netflix 原创内容《太空部队》。该剧在《办公室》移至 Peacock 几个月前发布。假设这部剧是 Netflix 试图为《办公室》粉丝保留订阅者的内容尝试。考虑创作此类节目会涉及到的决策:
-
因果决策理论——哪些演员/导演/编剧会吸引《办公室》粉丝继续订阅?哪些主题和内容?
-
因果机器学习——我们如何利用生成式 AI,例如大型语言模型,来创建节目脚本和试点,从而优化减少《办公室》粉丝流失的目标?
因果推断是将问题分解为这些类型的特定因果查询,然后使用数据来回答这些查询。因果 AI是关于构建自动化这种分析的算法。本书我们将探讨这两个问题领域。
1.1 什么是因果 AI?
要了解因果 AI 是什么,我们将从因果性和因果推断的基本概念开始,逐步深入。然后我们将回顾我们可以用因果 AI 解决的问题类型。
因果推理是人类理解、解释和关于世界做出决策的关键要素。每当我们在思考原因(“为什么会发生这样的事情?”)或结果(“如果我这样做会发生什么?”)时,我们都在进行因果推理。
在统计学和机器学习中,我们使用数据为我们的因果推理提供统计严谨性。但尽管因果关系驱动数据,仅凭统计相关性本身不足以从数据中得出因果结论。为此,我们必须转向因果推断。
统计(非因果)推断依赖于统计假设。即使在深度学习中,假设通常被称为“归纳偏差”,这也是正确的。同样,因果推断依赖于因果假设;因果推断指的是一系列理论和实用方法,这些方法通过因果假设来约束统计分析。
因果 AI指的是因果推断的自动化。我们可以利用机器学习算法,这些算法已经发展出自动化统计分析的稳健方法,并能够扩展到不同模态的大量数据。
人工智能的目标是自动化需要人类智能解决的问题的推理任务。人类在导航世界时严重依赖因果推理,尽管我们在因果推理上比在统计推理上更擅长,但我们的认知偏差仍然使我们的因果推理高度易出错。提高我们回答因果问题的能力是哲学家千年来的工作,科学家数百年的工作,以及统计学家数十年的工作。但现在,统计和计算进步的融合已经将重点从话语转向了我们可以基于数据进行训练并在软件中部署的算法。这是一个学习如何构建因果 AI 的迷人时期。
构建因果 AI 的基础关键定义
-
推理—从观察和数据中得出结论
-
假设—指导推理的约束
-
归纳偏差—假设的另一种说法,通常用来指代机器学习算法选择中隐含的假设
-
统计模型—使用统计假设来分析数据的框架
-
数据科学—一个跨学科领域,使用统计模型以及其他算法和技术从结构化和非结构化数据中提取见解和知识
-
因果推理—使用因果假设来指导结论的技术
-
因果模型—基于数据生成因果假设的统计模型
-
因果数据科学—使用因果模型提取因果见解的数据科学
-
因果 AI—使用因果模型自动执行因果推理任务的算法
1.2 本书如何处理因果推理
本书的目标是两个强大领域的融合:因果性和 AI。通过这次旅程的结束,你将具备以下技能:
-
设计具有因果能力的 AI 系统—利用 AI 的力量,但增加一层因果推理。
-
使用机器学习框架进行因果推理—利用 PyTorch 和其他 Python 库等工具,无缝地将因果建模集成到您的项目中。
-
构建自动化因果决策工具—实现因果决策算法,包括因果强化学习算法。
从历史上看,因果性和 AI 来自不同的研究领域,它们被应用于不同的问题,导致了具有不同技能集的专家、使用不同语言的书籍和具有不同抽象的库。本书是为任何希望将这些领域连接成一个综合技能集的人而写的。
关于因果推理有许多书籍,包括专注于 Python 中因果推理的书籍。以下小节讨论了使本书独特的某些特点。
1.2.1 重视 AI
本书专注于因果人工智能。我们将不仅涵盖因果推理对人工智能的相关性,或者机器学习如何扩展因果推理,还将关注实施。具体来说,我们将把因果模型与概率机器学习中的传统模型和训练程序相结合。
1.2.2 专注于技术、零售和商业
实践中的因果推理方法已从计量经济学、公共卫生、社会科学和其他难以进行随机实验的领域发展而来。因此,大多数书籍中的例子往往来自这些领域。相比之下,本书大量引用了来自技术、零售和商业的例子。
1.2.3 平行世界反事实及其他超越因果效应的查询
当许多人想到“因果推理”时,他们会想到估计因果效应,即平均处理效应(ATEs)和条件平均处理效应(CATEs)。这些当然很重要,但也有其他类型的因果查询。本书对这些其他类型给予了适当的关注。
例如,本书深入探讨了反事实的“平行世界”解释。在这种方法中,当某些原因和某些效果发生时,我们想象一个因果事件不同的平行宇宙。例如,假设你问,“我为了钱而结婚现在很伤心。如果我为了爱情而结婚,我会更快乐吗?”在我们的平行世界方法中,你会用你为了钱而结婚并感到悲伤的经历作为输入,对你在爱情婚姻的平行宇宙中的幸福感进行基于因果模型概率模拟。这种推理在决策中很有用。例如,它可能帮助你下次选择更好的伴侣。
希望这个关于爱情和遗憾的例子说明了这种“可能发生什么”的思考对人类认知的基本性(我们将在第八章和第九章看到更多应用实例)。因此,学习如何构建具有相同能力的 AI 是有意义的。但尽管它们很有用,一些反事实推理很难或无法验证(你不能证明如果你为了爱情而结婚你会更快乐)。大多数因果推理书籍只关注我们可以用数据和实验验证的狭隘的反事实集合,这忽略了反事实推理的许多有趣、认知科学相关和实际的应用案例。本书深入探讨这些用例。
1.2.4 推理商品化的假设
许多因果推断书籍深入探讨了各种因果效应估计器的统计推断螺钉和螺栓。但过去十年中开发深度学习框架的一个主要趋势是推断的商品化。这指的是像 PyTorch 这样的库如何抽象掉估计和推断的困难方面——如果你能以最小化可微损失函数的术语定义你的估计/推断问题,PyTorch 将处理其余部分。推断的商品化使用户能够专注于创建更加精细和强大的模型,例如表示数据生成过程因果结构的模型。
在这本书中,我们将专注于利用推理框架,以便你可以学习建模技术的通用观点。一旦你为你的领域找到正确的建模方法,你就可以使用其他资源深入研究任何感兴趣的统计算法。
1.2.5 用代码分解理论
本书的一个突出特点是它对因果推断理论中高级主题的处理方法。许多入门文本回避像识别、do-演算和因果层次定理这样的主题,因为它们很难。问题是,如果你想创建具有因果能力的 AI 算法,你需要对这些概念有直觉。
在这本书中,我们将通过依赖实现其基本抽象和算法的 Python 库,使这些主题易于理解。我们将通过在代码中操作这些原语来培养对这些高级主题的直觉。
1.3 因果性在现代人工智能工作流程中的作用
在构建具有因果能力的 AI 的未来版本中定位自己具有很大的价值,但本书涵盖的主题也将对当今常见的应用产生影响。在本节中,我们将回顾因果性如何增强这些应用中的某些方面。
1.3.1 更好的数据科学
大型科技公司和科技驱动的零售组织已经认识到因果推断的重要性,为精通此领域的人提供高薪。这是因为数据科学的本质——从数据中提取可操作的见解——本质上具有因果性。
当数据科学家检查电子商务网站上某个特征与销售额之间的相关性时,他们这样做是因为他们想知道该特征是否因果地推动了销售额。因果推断可以通过几种方式帮助回答这个问题。首先,它可以帮助他们设计一个实验来量化特征对销售额的因果效应,尤其是在无法进行完美随机实验的情况下。其次,如果提议的实验不可行,数据科学家可以使用过去的观察数据和来自相关但不同过去实验的数据来推断提议实验将产生的因果效应的价值,而无需实际运行该实验。最后,即使数据科学家在运行实验方面有完全的自由,因果推断也可以帮助选择要运行的实验和要测量的变量,从而最小化运行浪费或不具信息量的实验的机会成本。
1.3.2 更好的归因、信用分配和根本原因分析
因果推断也支持归因。营销中的“归因问题”可能最好由一句归因于广告先驱约翰·万纳梅克的引言来阐述:
我花在广告上的钱有一半是浪费的;麻烦的是我不知道是哪一半。
换句话说,很难知道是什么广告、促销或其他行动导致了特定的客户行为、销售额或其他关键业务结果。即使在万纳梅克时代数据比那时更丰富、更细粒度的在线营销中,归因仍然是一个挑战。例如,用户可能在看到广告后点击,但这是否是导致点击的单个广告浏览?或者他们无论如何都会点击?也许他们在多个渠道收到的所有点击推动的累积效应导致了点击。因果建模通过使用正式的因果逻辑来回答“为什么”问题,如“为什么这个用户点击?”来解决归因问题。
归因在其他领域也有其他名称,如“信用分配”和“根本原因分析”。核心意义是相同的;我们想了解为什么某个特定事件的结果发生了。我们知道一般的原因是什么,但想知道在特定情况下,某个特定原因在多大程度上应该受到责备。
1.3.3 更稳健、可分解和可解释的模型
对于使用机器学习来构建软件的组织,结合因果建模可以改善过程和产品。特别是,因果性通过使机器学习更加稳健、可分解和可解释来增加价值。
更稳健的机器学习
当模型训练的环境和模型部署的环境之间存在差异导致模型崩溃时,机器学习模型缺乏稳健性。因果性可以通过以下方式解决稳健性的不足:
-
过拟合—当学习算法过分重视训练数据中的虚假统计模式时,就会发生过拟合。因果方法可以将机器学习模型导向学习根植于因果关系的统计模式。
-
欠指定—当模型存在许多等效配置,在测试数据上表现相同但在部署环境中表现不同时,就会发生欠指定。欠指定的一个迹象是对模型配置的任意元素敏感,例如随机种子。因果推断可以告诉你何时因果预测是“识别”的(即,“未欠指定”),这意味着在假设和数据给定的情况下存在一个独特的答案。
-
数据漂移—随着时间的推移,你在部署模型的环境中的数据特征与训练数据特征不同或“漂移”。因果建模通过捕捉数据背后的因果不变性来解决这个问题。例如,假设你训练了一个使用海拔来预测平均温度的模型。如果你只使用来自高海拔城市的训练数据,如果模型成功拟合了海拔和温度之间的基于物理的因果关系,那么它应该仍然在低海拔城市中表现良好。
这就是为什么领先的科技公司部署因果机器学习技术——它们可以使他们的机器学习服务更加稳健。这也是为什么著名的深度学习研究人员正在追求将深度学习与因果推理相结合的研究。
更可分解的机器学习
因果模型分解为组件,具体来说是效应及其直接原因的元组,我将在第三章中正式定义。为了说明,让我们考虑一个简单的机器学习问题,即预测看到数字广告的个人是否会进行购买。
我们可以使用广告印象的各种特征(例如,广告被看到的次数、观看持续时间、广告类别、一天中的时间等)作为特征向量,并使用神经网络预测购买,如图 1.1 所示。模型隐藏层的权重相互依赖,因此模型不能简化为更小的独立组件。

图 1.1 使用与广告印象相关的特征来预测购买结果的简单多层感知器神经网络
另一方面,如果我们从因果关系的角度看待这个问题,我们可能会推断出一个广告印象可以驱动参与度,而参与度又决定了个人是否会进行购买。使用参与度指标作为另一个特征向量,我们可以训练图 1.2 中所示的模式。这个模型与领域的因果关系结构相一致(即广告印象导致参与度,参与度导致购买)。因此,它分解为两个组件:{广告印象,参与度}和{参与度,购买}。

图 1.2 一个捕捉广告印象如何驱动参与度,进而驱动购买的模型。该模型分解为{广告印象,参与度}和{参与度,购买}。
这种可分解性有几个好处:
-
模型的组件可以独立测试和验证。
-
模型的组件可以单独执行,从而更有效地利用现代云计算基础设施,并支持边缘计算。
-
当有额外的训练数据可用时,只有与数据相关的组件需要重新训练。
-
旧模型的组件可以用于针对新问题的新模型。
-
由于组件可以单独优化,因此对子优化的模型配置和超参数设置不太敏感。
因果模型的组件对应于你正在建模的领域的概念。这导致了下一个好处,即可解释性。
更可解释的机器学习
许多机器学习算法,尤其是深度学习算法,可以相当“黑盒”,这意味着内部工作方式不易理解,模型对给定输入产生输出的过程也不易解释。
相比之下,因果模型是高度可解释的,因为它们直接在建模领域中编码易于理解的因果关系。事实上,因果关系是解释的核心;解释一个事件意味着描述事件的起因以及它们是如何导致事件发生的。因果模型以你正在建模的领域的语言(语义解释)而不是以模型的架构(如“节点”和“激活”的句法解释)来提供解释。
考虑图 1.1 和图 1.2 中的示例。在图 1.1 中,只有输入特征和输出在领域内是可解释的;隐藏层的内部工作方式是不可解释的。因此,给定一个特定的广告印象,很难解释模型是如何得出特定的购买结果的。相比之下,图 1.2 中的示例明确提供了参与度来解释我们是如何从一个广告印象到一个购买结果的。
参与度与广告印象之间,以及购买与参与度之间的联系仍然是黑箱,但如果我们需要的话,我们可以在那些黑箱中添加额外的变量。我们只需要确保这样做的方式与我们对问题因果结构的假设相一致。
1.3.4 更公平的 AI
假设鲍勃申请了一笔商业贷款。一个机器学习算法预测鲍勃可能是一个不良贷款候选人,因此鲍勃被拒绝了。鲍勃是一个男性,他获取了银行的贷款数据,数据显示男性获得贷款申请批准的可能性较低。这是否是一个“不公平”的结果?
例如,如果我们说结果“不公平”,是因为算法做出预测是因为鲍勃是男性。为了成为一个“公平”的预测,它需要从与鲍勃偿还贷款能力相关的因素中制定,例如他的信用记录、他的业务领域或他可提供的抵押品。鲍勃的困境是另一个例子,说明了我们为什么希望机器学习是可解释的:这样我们就可以分析鲍勃申请中哪些因素导致了算法的决定。
假设训练数据来自贷款官员的历史决策,其中一些官员持有损害男性的性别偏见。例如,他们可能阅读了显示在财务困难时期男性更有可能违约的研究。基于这些研究,他们决定如果申请人是男性,就从他们的评分中扣除分数。
此外,假设在收集数据时,银行在社交媒体上宣传了贷款计划。当我们查看活动结果时,我们注意到回应广告的男性平均资格低于点击广告的女性。这种差异可能是因为该活动更好地针对了女性,或者因为在线广告拍卖中,当广告受众由资格较低的男性组成时,平均出价价格更低。图 1.3 绘制了可能影响贷款审批流程的各种因素,并区分了公平和不公平的原因。这些因素被绘制在一个有向无环图(DAG)中,这是一种表示因果关系的流行且有效的方法。我们将在整个书中使用 DAG 作为因果推理的主要工具。

图 1.3 一个因果有向无环图(DAG),展示了统计上对特定性别的偏见可能来自算法直接惩罚该性别(不公平)以及通过数字广告算法针对的申请人的性别差异间接产生(公平)。因果推断可以将偏见分解为公平和不公平的来源。
因此,数据中存在针对男性的两种可能的统计偏差来源。一种偏差来源是吸引到平均资质较低男性的在线广告,导致男性的拒绝率较高。另一种统计偏差来源是贷款官员的偏见。其中一种偏差来源可以说是“公平的”(很难责怪银行对数字广告算法的定位行为),而另一种来源是“不公平的”(我们可以责怪银行对性别歧视的贷款政策)。但当我们只看没有这种因果背景的训练数据时,我们看到的只是针对男性的统计偏差。学习算法在做出关于鲍勃的决定时复制了这种偏差。
解决这个问题的简单方法之一就是从训练数据中移除性别标签。但即使那些性别歧视的贷款官员没有看到关于个人性别的明确指示,他们也能从申请中的某些元素,如个人姓名中推断出来。这些贷款官员将他们的偏见观点编码成性别代理变量与贷款结果之间的统计相关性。机器学习算法会发现这种统计模式,并利用它来做出预测。结果,你可能会遇到算法对两个具有相同还款风险但性别不同的个体产生不同预测的情况,即使性别并不是预测的直接输入。部署这种算法实际上会放大那些贷款官员的偏见观点所造成的伤害。
由于这些原因,我们可以看到人们对广泛部署机器学习算法的许多担忧是有道理的。如果没有纠正,这些算法可能会通过放大我们社会产生的不公平结果来对我们社会产生负面影响。
因果分析在解析这类算法公平性问题方面是很有用的。在这个例子中,我们可以使用因果分析将统计偏差分解为“不公平”的性别歧视偏差和由于外部因素(如数字广告服务如何定位广告)导致的偏差。最终,我们可以使用因果建模来构建一个只考虑与个人是否能够偿还贷款有因果关系的变量的模型。
需要注意的是,仅仅因果推理不足以解决算法公平性问题。因果推理可以帮助将统计偏差分解为公平和不公平的部分。然而,即使这样,这也取决于所有相关方就概念和结果的定义达成一致,这通常是一项艰巨的任务。为了说明这一点,假设社交媒体广告活动向更多男性推送贷款广告,因为向男性推送广告的成本更低。因此,当印象来自男性时,广告活动可以通过较低的出价赢得在线广告位拍卖,结果导致更多男性看到广告,尽管其中许多男性并不适合贷款项目。这个过程是否不公平?结果是否不公平?在性别平衡结果和广告定价公平性之间,公平性的权衡是什么?是否应该让某些广告商因为旨在鼓励平衡结果的价格机制而支付更多?因果分析无法解决这些问题,但它可以帮助在技术细节上理解这些问题。
1.4 因果性如何推动下一波人工智能浪潮
将因果逻辑纳入机器学习正在引领人工智能的新进展。本书突出了人工智能的三个趋势领域:表示学习、强化学习和大型语言模型。这些因果人工智能的趋势让人联想到深度学习的早期阶段。当深度学习浪潮兴起时,已经与神经网络一起工作的人们在这个领域享受了新的机会,而机会的获取会带来更多机会的获取。下一波人工智能仍在形成中,但很明显,它将从根本上纳入因果性的某种表示。本书的目标是帮助您驾驭这股浪潮。
1.4.1 因果表示学习
许多最先进的深度学习方法试图学习被建模对象的几何表示。然而,这些方法在学习因果上有意义的表示方面存在困难。例如,考虑一个孩子用绳子拿着充满氦气的气球的视频。假设我们有一个对应图像的向量表示。如果向量表示具有因果意义,那么通过操纵向量去除孩子,并将操纵后的向量转换为新的视频,就会得到气球向上升起的描绘。因果表示学习是深度表示学习中的一个有希望的研究领域,目前仍处于早期阶段。本书在多个章节中提供了基于深度学习架构构建的因果模型示例,为因果 AI 这一令人兴奋的新增长领域的根本思想提供了介绍。
1.4.2 因果强化学习
在经典的强化学习中,学习代理摄入大量数据,像巴甫洛夫的狗一样学习;它们学习与良好结果正相关、与不良结果负相关的动作。然而,众所周知,相关性并不等同于因果性。因果强化学习可以突出那些导致更高奖励的动作与与高奖励最强烈相关的动作不同的案例。此外,它通过反事实推理(即提出像“如果代理使用不同的策略,他们将获得多少奖励?”这样的问题)解决了归因问题(即正确地将奖励归因于动作)。第十二章致力于因果强化学习和其他因果决策领域。
1.4.3 大型语言模型和基础模型
大型语言模型(LLMs)如 OpenAI 的 GPT、Google 的 Gemini 和 Meta 的 Llama,是经过大量文本和其他数据训练的具有数十亿参数的深度神经网络语言模型。这些模型可以生成高度连贯的自然语言、代码和其他模态的内容。它们是基础模型,意味着它们为构建更特定领域的机器学习模型和产品提供了基础。这些产品,如 Microsoft 365 Copilot,已经产生了巨大的商业影响。
一项新的研究领域和产品开发调查了大型语言模型(LLMs)回答因果问题和执行因果分析的能力。另一条研究路线是使用因果方法设计和训练具有优化因果能力的新 LLMs。在第十三章中,我们将探讨 LLMs 与因果关系的交汇点。
1.5 因果关系的机器学习入门
现在您已经看到了因果推理如何改善机器学习的多种方式,让我们看看将因果性融入 AI 模型的过程。为此,我们将使用在机器学习中经常使用的流行基准数据集:手写数字图像的 MNIST 数据集,每个图像都标有图像中实际代表的数字。图 1.4 展示了 MNIST 中数字的多个示例。

图 1.4 MNIST 数据集中的每一张图像都是一个手写数字的图像,并且每张图像都标有它所代表的数字。
MNIST 基本上是机器学习的“Hello World”。它主要用于实验不同的机器学习算法,并比较它们的相对强度。基本的预测任务是接受代表每个图像的像素矩阵作为输入,并返回正确的图像标签作为输出。让我们开始将因果思维融入应用于 MNIST 图像的概率机器学习模型的过程。
1.5.1 查询、概率和统计学
首先,我们将查看不包括因果推断的基本过程。机器学习可以使用关于感兴趣数量的概率进行分析。为此,概率机器学习模型学习该系统中所有变量的概率表示。我们可以使用三步过程,利用概率机器学习模型进行预测和决策。
-
提出问题——你想要回答什么问题?
-
写下数学公式——什么概率(或与概率相关的量)将回答给定证据或数据的问题?
-
进行统计分析——哪种统计分析将给你(或估计)那个量?
这些步骤(查询、估计量和估计值)有更正式的术语,但我们现在将避免使用行话。相反,我们将从一个简单的统计示例问题开始。你的第一步可能是“波士顿人的身高是多少?”对于第二步,你可能会决定知道波士顿所有居民的平均身高(在概率术语中,称为“期望值”)将回答你的问题。第三步可能涉及随机选择 100 名波士顿人并测量他们的平均身高;统计定理保证这个样本平均数是真实总体平均数的一个接近估计。
让我们将这个工作流程扩展到 MNIST 图像建模。
第一步:提出问题
假设我们正在查看图 1.5 中的 MNIST 图像,它可能是“4”也可能是“9”。在第一步中,我们明确地提出一个问题,例如“给定这个图像,图像中代表的是哪个数字?”

图 1.5 这是一张数字 4 或 9 的图像吗?MNIST 数据集的典型任务是给定图像对数字标签进行分类。
第二步:写下数学公式
在第二步中,我们想要找到一个概率量,它能回答给定证据或数据的问题。换句话说,我们想要找到一个可以用概率数学符号写下来的东西,它能回答第一步中的问题。对于图 1.5 的例子,证据或数据是图像。图像是 4 还是 9?让变量I代表图像,D代表数字。在概率符号中,我们可以写出给定图像I是
时,数字是 4 的概率为P(D=4|I=
),其中I=
是I等于某个图像向量表示的简写。我们可以将这个概率与P(D=9|I=
)进行比较,并选择具有更高概率的D值。将这一过程推广到所有十个数字,第二步中我们想要的数学量如图 1.6 所示。

图 1.6 根据图像选择概率最高的数字。
用普通英语来说,这就是“值d,它最大化了给定图像时D等于d的概率,”其中d是十个数字(0-9)中的一个。
第 3 步:进行统计分析
第 3 步使用统计分析来为我们在第 2 步中识别的数量分配一个数字。我们可以用无数种方法来做这件事。例如,我们可以训练一个深度神经网络,它以图像作为输入并预测数字作为输出;我们也可以设计神经网络为每个值d分配一个概率,即D=d。
1.5.2 因果关系与 MNIST
那么,因果关系如何在前一节的三个步骤分析中发挥作用呢?Yann LeCun 是深度学习工作的图灵奖得主(计算机科学的诺贝尔奖),同时也是 Meta 人工智能研究部门的负责人。他也是 MNIST 创建背后的三位研究人员之一。他在个人网站上讨论了 MNIST 数据的因果关系背景,yann.lecun.com/exdb/mnist/index.xhtml:
MNIST 数据库是由 NIST 的特别数据库 3 和特别数据库 1 构建的,其中包含手写数字的二值图像。NIST 最初将 SD-3 指定为训练集,SD-1 指定为测试集。然而,SD-3 比 SD-1 更干净、更容易识别。原因在于 SD-3 是在人口普查局员工中收集的,而 SD-1 是在高中生中收集的。从学习实验中得出合理的结论需要确保结果不受整个样本集中训练集和测试集选择的影响。因此,有必要通过混合 NIST 的数据集来构建一个新的数据库。
换句话说,作者混合了这两个数据集,因为他们认为,如果他们仅用高中生绘制的数字来训练机器学习模型,那么在应用于公务员绘制的数字时表现会不佳。然而,在现实世界的设置中,我们希望模型具有鲁棒性,能够在一种场景中学习并在另一种场景中预测,即使这些场景不同。例如,我们希望垃圾邮件过滤器在垃圾邮件发送者从尼日利亚王子转变为不丹公主时仍然有效。我们希望我们的自动驾驶汽车即使在停车标志上涂鸦时也能停下来。像洗牌一样洗数据在现实世界的设置中并不是轻易就能负担得起的奢侈。
因果建模利用了关于数字绘制背后的因果机制的知识,这将帮助模型在训练数据中的高中生和公务员之外,推广到测试数据中的高中生。图 1.7 展示了表示这一系统的因果 DAG。

图 1.7 表示 MNIST 图像生成过程的示例因果 DAG。节点代表数据生成过程中的对象,边对应这些对象之间的因果关系。
这个特定的有向无环图(DAG)设想作者决定了所绘数字的粗细和弯曲度,并且高中生倾向于有与公务员不同的书写风格。该图还假设作者的分类是导致他们所绘数字的原因。也许公务员写更多的 1、0 和 5,因为这些数字在人口普查工作中出现得更频繁,而高中生在数学课上做更多长除法时更常绘制其他数字(这与主题模型中“主题”导致文档中单词频率的类似想法)。最后,DAG 假设年龄是作者类型和图像的共同原因;你必须低于一定年龄才能上高中,高于一定年龄才能成为人口普查官员。
一种因果建模方法将使用这种因果知识来训练一个预测模型,该模型可以从高中训练数据外推到公务员测试数据。这样的模型将更好地推广到新的情况,其中作者类型和其他变量的分布与训练数据不同。
1.5.3 因果查询、概率和统计学
在本章的开头,我讨论了我们可以提出的各种因果问题,例如因果发现、量化因果效应和因果决策。我们可以通过我们对之前三步分析(提出问题、写下数学、进行统计分析)的因果变体来回答这些问题以及其他各种问题:
-
提出因果问题——你想要回答的问题是什么?
-
写下因果数学——给定证据或数据,什么概率(或期望)将回答因果问题?
-
进行统计分析——哪种统计分析将给你(或“估计”)那个因果量?
注意,第三步与原始三步相同。因果的细微差别出现在第一步和第二步。
第 1 步:提出因果问题
这些是我们关于我们的因果 MNIST 模型可以提出的一些因果问题的例子:
-
“作者类型(高中生与公务员)对具有 3 级厚度的数字 4 的图像外观有多大影响?”(第十一章中讨论了条件平均处理效应估计)。
-
假设笔画粗细是图像的一个原因,我们可能会问:“如果尽可能弯曲,2 看起来会是什么样子?”(这是第七章中讨论的干预预测)。
-
“给定一个图像,如果笔画弯曲度更重,它会有什么不同?”(参见第八章和第九章中讨论的反事实推理)。
-
“为了得到一个美学上理想的图像,笔画弯曲度应该是多少?”(第十二章中讨论了因果决策)。
让我们考虑第一个项目中的 CATE。CATE 估计是应用于普通表格数据的常见因果推断问题,但在 AI 计算机视觉问题的应用中却很少见到。
第 2 步:写下因果数学
因果推断理论告诉我们如何用数学方法形式化我们的因果问题。使用特殊的因果符号,我们可以将我们的 CATE 查询用以下方式数学形式化:

其中 E(.) 是一个期望算子。我们将在下一章回顾期望,但就现在而言,我们可以将其视为对图像像素的平均值。
前面的下标使用是一种特殊的符号,称为“反事实符号”,它代表了一种干预。实验中的随机分配是一种现实世界的干预,但有许多实验我们无法在现实世界中运行。例如,不可能进行一项试验,随机分配参与者要么成为高中生,要么成为人口普查局官员。尽管如此,我们想知道作者类型如何因果影响图像,因此我们依赖于因果模型及其表示干预的能力。
为了说明,图 1.8 可视化了 CATE 可能的样子。挑战在于推导出图 1.8 右侧的差分图像。因果推断理论帮助我们解决量化作者类型如何驱动图像时可能存在的年龄相关的“混杂”偏差。例如,do-calculus(第十章)是一组基于图的规则,允许我们从这个 DAG 算法性地推导出以下方程:

该方程的左侧定义了在第二步中用于 CATE 定义的期望——它是一个理论结构,捕捉了假设条件“如果作者类型被设置为‘w’”。但右侧是可操作的;它完全由我们可以使用机器学习方法在假设的 NIST 图像数据版本上标记作者年龄的术语组成。

图 1.8 展示了作者类型在图像上的一个示例 CATE 的可视化。它是单一干预(W=["high school")下期望图像与另一种干预(W=["bureaucrat")下期望图像的像素级差异,两种期望都是在数字 4 的特定厚度下的图像。
第 3 步:进行统计推断
第 3 步进行统计估计,并且有几种方法可以估计该方程右侧的量。例如,我们可以使用卷积神经网络来建模 E(I|W=w, A=a, D=d, T=t),并构建联合分布 P(A, D, T) 的概率模型。统计建模方法的选择涉及通常的统计权衡,例如易用性、偏差和方差、可扩展性到大量数据以及并行化。
其他书籍对第三步中首选的统计方法进行了详细阐述。我持强烈的观点,我们应该依赖统计建模和机器学习框架中的“推理商品化”趋势来处理第三步,而将重点放在提高第一步和第二步的技能上:弄清楚要问的正确问题,并以数学方式表示可能的因果关系。
如您在本节所见,我们对因果人工智能的探索是通过一个三步过程来构建的,因果思维的精髓在前两个步骤中尤为突出。第一步邀请我们构建正确的因果问题,而第二步则阐明了这些问题背后的数学原理。第三步利用了我们在传统统计预测和推理中非常熟悉的模式。
使用这种结构化方法,在接下来的章节中,我们将从纯粹的预测机器学习模型——例如您可能从 MNIST 中熟悉的深度潜在变量模型——过渡到因果机器学习模型,这些模型能为我们提供对因果问题的更深入见解和答案。首先,我们将回顾基础数学和机器学习原理。然后,在本书的第二部分,我们将深入探讨如何为第一步和第二步构建正确的问题并从数学上阐述它们。对于第三步,我们将利用 PyTorch 和其他先进库的力量,将因果概念与最前沿的统计学习算法相结合。
摘要
-
因果人工智能旨在通过因果逻辑增强统计学习和概率推理。
-
因果推断帮助数据科学家从观察数据(世界上绝大多数数据)和实验数据中提取更多的因果见解。
-
当数据科学家无法进行实验时,因果模型可以从观察数据中模拟实验。
-
他们可以使用这些模拟来进行因果推断,例如估计因果效应,甚至优先考虑在现实生活中运行的有趣实验。
-
因果推断通过算法反事实推理和归因帮助数据科学家改善他们组织中的决策。
-
因果推断还使机器学习更加稳健、可分解和可解释。
-
因果分析有助于正式分析预测算法中的公平性,并通过将普通统计偏差分解为其因果来源来构建更公平的算法。
-
推理的商品化是机器学习中的一个趋势,指的是像 PyTorch 这样的通用建模框架如何持续自动化统计学习和概率推理的细节。这一趋势减少了模型构建者对因果推理的正式和统计细节的专家知识需求,使他们能够专注于将领域专业知识转化为更好的问题域因果模型。
-
因果推理任务的类型包括因果发现、干预预测、因果效应估计、反事实推理、解释和归因。
-
我们构建和使用概率机器学习模型的方式可以扩展到在概率机器学习工具(如 PyTorch)中实现的因果生成模型。
第二章:概率生成建模基础
本章涵盖
-
概率模型基础
-
使用 pgmpy 和 Pyro 库进行计算概率
-
因果性统计学:数据、总体和模型
-
区分概率模型和主观贝叶斯主义
第一章阐述了学习如何编码因果 AI 的必要性。本章将介绍一些我们需要用概率机器学习来处理因果模型的基础知识,这大致指的是使用概率来模拟不确定性和模拟数据的机器学习技术。构建概率机器学习模型有一套灵活的尖端工具。本章将介绍概率、统计学、建模、推理甚至哲学中的概念,这些概念是我们用概率机器学习方法实现因果推理的关键思想。
本章不会对这些概念提供数学上的全面介绍。我将专注于本书其余部分所需的内容,并省略其他内容。任何寻求因果推理专长的数据科学家都不应忽视概率、统计学、机器学习和计算机科学的实际细微差别。有关推荐资源的章节注释,请参阅www.altdeep.ai/p/causalaibook,您可以在那里获得更深入的介绍或复习材料。
在本章中,我将介绍两个用于概率机器学习的 Python 编程库:
-
pgmpy是一个用于构建概率图模型的库。作为一个传统的图形建模工具,它比 Pyro 缺乏灵活性和尖端性,但更容易使用和调试。它所做的是,它做得很好。
-
Pyro是一个通用的概率机器学习库。它非常灵活,并利用 PyTorch 的尖端基于梯度的学习技术。
Pyro 和 pgmpy 是我们将在本书中使用的通用建模库。我们将使用的其他库是专门为因果推理设计的。
2.1 概率基础
让我们回顾一下您需要与本书一起工作的概率理论。我们将从一些基本的数学公理及其逻辑扩展开始,而不添加任何现实世界的解释。让我们从一个简单的三面骰子的具体想法开始(这些是存在的)。
2.1.1 随机变量和概率
一个 随机变量 是一个变量,其可能的值是随机现象的数值结果。这些值可以是离散的或连续的。在本节中,我们将关注离散情况。例如,代表三次掷骰子的离散随机变量的值可以是{1, 2, 3}。或者,在 Python 这样的 0 索引编程语言中,可能最好使用{0, 1, 2}。同样,代表抛硬币的离散随机变量可以有结果{0, 1}或{True, False}。图 2.1 说明了三面骰子。

图 2.1 三个面的骰子各自代表一个有三个离散结果的随机变量。
常见的表示方法是使用大写字母如 X、Y 和 Z 来表示随机变量。例如,假设 X 代表一个掷骰子的结果,结果为 {1, 2, 3},结果代表骰子面上的数字。X=1 和 X=2 分别代表掷出 1 和 2 的事件。如果我们想用一个变量来抽象特定的结果,我们通常使用小写字母。例如,我会用 “X=x” (例如,X=1)来表示事件 “我掷出了一个‘x’!” 其中 x 可以是 {1, 2, 3} 中的任何值。参见图 2.2。

图 2.2 中的 X 代表掷一个三面骰子的结果。如果骰子掷出 2,观察到的结果就是 X=2。
随机变量的每个结果都有一个 概率值。概率值通常被称为离散变量的 概率质量 和连续变量的 概率密度。对于离散变量,概率值介于零和一之间,将每个可能结果的概率值相加得到 1。对于连续变量,概率密度大于零,并且将概率密度在所有可能结果上积分得到 1。
给定一个结果为 {0, 1} 的随机变量,代表抛硬币,0 被分配的概率值是多少?1 呢?到目前为止,我们只知道这两个值介于零和一之间,并且它们的和为 1。要超出这个范围,我们必须讨论如何 解释 概率。不过,首先让我们澄清一些更多的概念。
2.1.2 概率分布和分布函数
概率分布函数 是一个将随机变量的结果映射到概率值的函数。例如,如果抛硬币的结果是 1(正面)且概率值是 0.51,分布函数将 1 映射到 0.51。我坚持使用标准的表示法 P(X=x),如 P(X=1) = 0.51。对于更长的表达式,当随机变量很明显时,我省略大写字母并保留结果,所以 P(X=x) 变为 P(x),而 P(X=1) 变为 P(1)。
如果随机变量有一个有限的离散结果集,我们可以用一个表格来表示概率分布。例如,一个代表结果 {1, 2, 3} 的随机变量可能看起来像图 2.3。

图 2.3 展示了离散分布的简单表格表示。
在这本书中,我采用常见的表示法 P(X) 来表示 X 所有可能结果的概率分布,而 P(X=x) 表示特定结果的概率值。为了在 pgmpy 中将概率分布实现为一个对象,我们将使用 DiscreteFactor 类。
列表 2.1 在 pgmpy 中实现离散分布表
from pgmpy.factors.discrete import DiscreteFactor
dist = DiscreteFactor(
variables=["X"], #1
cardinality=[3], #2
values=[.45, .30, .25], #3
state_names= {'X': ['1', '2', '3']} #4
)
print(dist)
1 因子中变量的名称列表
2 因子中每个变量的基数(可能结果的数量)
3 因子中每个变量可以取的值
4 一个字典,其中键是变量名,值是该变量结果的名称列表
此代码打印出以下内容:
+------+----------+
| X | phi(X) |
+======+==========+
| X(1) | 0.4500 |
+------+----------+
| X(2) | 0.3000 |
+------+----------+
| X(3) | 0.2500 |
+------+----------+
设置环境
此代码是用 pgmpy 版本 0.1.24 和 Pyro 版本 1.8.6 编写的。使用的 pandas 版本是 1.5.3。
请参阅www.altdeep.ai/p/causalaibook获取每个章节的 Jupyter 笔记本链接,包括代码和设置工作环境的说明。
2.1.3 联合概率和条件概率
通常,我们感兴趣的是对多个随机变量进行推理。假设,除了图 2.1 中的随机变量X外,还有一个额外的随机变量Y,其有两个结果{0, 1}。那么存在一个联合概率分布函数,它将X和Y的每个组合映射到一个概率值。

图 2.4 表格联合概率分布的简单表示
作为表格,它可能看起来像图 2.4。
DiscreteFactor对象也可以表示联合分布。
列表 2.2 在 pgmpy 中建模联合分布
joint = DiscreteFactor(
variables=['X', 'Y'], #1
cardinality=[3, 2], #2
values=[.25, .20, .20, .10, .15, .10], #3
state_names= {
'X': ['1', '2', '3'], #3
'Y': ['0', '1'] #3
}
)
print(joint) #4
1 现在我们有两个变量而不是一个。
2 X 有 3 个结果,Y 有 2 个。
3 现在有 2 个变量,因此我们为这两个变量的结果命名。
4 您可以通过查看打印的输出来查看值的顺序。
上述代码打印出以下输出:
+------+------+------------+
| X | Y | phi(X,Y) |
+======+======+============+
| X(1) | Y(0) | 0.2500 |
+------+------+------------+
| X(1) | Y(1) | 0.2000 |
+------+------+------------+
| X(2) | Y(0) | 0.2000 |
+------+------+------------+
| X(2) | Y(1) | 0.1000 |
+------+------+------------+
| X(3) | Y(0) | 0.1500 |
+------+------+------------+
| X(3) | Y(1) | 0.1000 |
+------+------+------------+
注意,概率值之和为 1。此外,当我们对Y进行边缘化(即“求和”或“积分”)时,我们恢复原始分布P(X),(也称为X的边缘分布)。图 2.5 中的行求和产生X的边缘分布。

图 2.5 对Y进行边缘化得到X的边缘分布。
边缘化方法将为我们对指定的变量求和。
print(joint.marginalize(variables=['Y'], inplace=False))
这将打印出以下输出:
+------+----------+
| X | phi(X) |
+======+==========+
| X(1) | 0.4500 |
+------+----------+
| X(2) | 0.3000 |
+------+----------+
| X(3) | 0.2500 |
+------+----------+
将inplace参数设置为False给我们一个新的边缘化表,而不是修改原始的联合分布表。

图 2.6 对X进行边缘化得到Y的边缘分布。
类似地,当我们对X进行列边缘化时,我们得到P(Y)。在图 2.6 中,对列中的X值求和给出Y的边缘分布。
print(joint.marginalize(variables=['X'], inplace=False))
这将打印出以下输出:
+------+----------+
| Y | phi(Y) |
+======+==========+
| Y(0) | 0.6000 |
+------+----------+
| Y(1) | 0.4000 |
+------+----------+
我将使用符号P(X, Y)来表示联合分布。我将使用P(X=x, Y=y)来表示结果概率,并且为了简便,我将写P(x, y)。例如,在图 2.6 中,P(X=1, Y=0) = P(1, 0) = 0.25。我们可以在任意数量的变量上定义联合分布;如果有三个变量{X, Y, Z},我将联合分布写为P(X, Y, Z)。
在这个联合概率分布的表格表示中,随着每个额外变量的增加,单元格的数量呈指数增长。有一些(但不多)是“规范”的联合概率分布(例如多元正态分布——我将在第 2.1.7 节中展示更多示例)。因此,在多元设置中,我们倾向于使用 条件概率 分布。
在给定 X 的条件下,Y 的条件概率是

直观上,P(Y|X=1) 指的是在 X 为 1 的条件下 Y 的概率分布。在分布的表格表示中,我们可以通过将联合概率分布表中的单元格除以边缘概率值来推导条件分布表,如图 2.7 所示。注意,图 2.7 中条件概率表的列现在总和为 1。

图 2.7 通过将联合分布的值除以边缘分布的值来推导条件概率分布的值。
pgmpy 库允许我们使用“/”运算符进行这个除法操作:
print(joint / dist)
那行产生了以下输出:
+------+------+------------+
| X | Y | phi(X,Y) |
+======+======+============+
| X(1) | Y(0) | 0.5556 |
+------+------+------------+
| X(1) | Y(1) | 0.4444 |
+------+------+------------+
| X(2) | Y(0) | 0.6667 |
+------+------+------------+
| X(2) | Y(1) | 0.3333 |
+------+------+------------+
| X(3) | Y(0) | 0.6000 |
+------+------+------------+
| X(3) | Y(1) | 0.4000 |
+------+------+------------+
此外,您还可以使用 TabularCPD 类直接指定条件概率分布表:
from pgmpy.factors.discrete.CPD import TabularCPD
PYgivenX = TabularCPD(
variable='Y', #1
variable_card=2, #2
values=[
[.25/.45, .20/.30, .15/.25], #3
[.20/.45, .10/.30, .10/.25], #3
],
evidence=['X'],
evidence_card=[3],
state_names = {
'X': ['1', '2', '3'],
'Y': ['0', '1']
})
**print(PYgivenX)**
1 条件分布有一个变量,而不是 ΔiscreteFactor 变量列表。
2 variable_card 是 Y 的基数。
3 列表中的元素对应于 Y 的结果。每个列表的元素对应于 X 的元素。
这会产生以下输出:
+------+--------------------+---------------------+------+
| X | X(1) | X(2) | X(3) |
+------+--------------------+---------------------+------+
| Y(0) | 0.5555555555555556 | 0.6666666666666667 | 0.6 |
+------+--------------------+---------------------+------+
| Y(1) | 0.4444444444444445 | 0.33333333333333337 | 0.4 |
+------+--------------------+---------------------+------+
variable_card 参数是 Y 的基数(意味着 Y 可以取出的结果数量),而 evidence_card 是 X 的基数。
条件化作为一个操作
在“条件概率”这个短语中,“条件”是一个形容词。将“条件”视为动词(一个动作)是有用的。您可以通过另一个随机变量 X 来条件化随机变量 Y。例如,在图 2.5 中,我可以将 Y 条件化为 X=1,本质上得到一个新的随机变量,其结果值与 Y 相同,但其概率分布与 P(Y|X=1) 相等。
对于有更多编程经验的人来说,将 X = 1 作为对事件 X == 1 的过滤来考虑;例如,“当 X == 1 时 Y 的概率分布是什么?”在这个意义上,过滤就像 SQL 查询中的 WHERE 子句。P(Y) 是当你的查询是 SELECT * FROM Y 时 Y 表中的行分布,而 P(Y|X=1) 是当你的查询是 SELECT * FROM Y WHERE X=1 时的行分布。
将“条件”视为一个动作有助于我们更好地理解概率机器学习库。在这些库中,你有代表随机变量的对象,条件是应用于这些对象的操作。正如你将看到的,将条件视为动作的想法也与核心因果建模概念“干预”形成鲜明对比,我们在其中“干预”一个随机变量。
Pyro 通过 pyro.condition 函数将条件操作化为一个操作。我们将在第三章中探讨这一点。
2.1.4 链式法则、全概率公式和贝叶斯法则
从概率的基本公理中,我们可以推导出概率的链式法则、全概率公式和贝叶斯法则。这些概率法则在概率建模和因果建模的背景下尤为重要,因此我们将简要介绍它们。
概率的链式法则表明我们可以将联合概率分解为条件概率的乘积。例如,P(X, Y, Z) 可以如下分解:

我们可以按任何我们喜欢的顺序进行分解。在上面的例子中,顺序是 X,然后是 Y,然后是 Z。然而,Y,然后 Z,然后 X,或者 Z,然后 X,然后 Y,以及其他顺序都是同样有效的。

链式法则在建模和计算方面都具有重要意义。实现一个代表 P(X, Y, Z) 的单一对象面临的挑战是,它需要将 X、Y 和 Z 的所有可能结果的组合映射到一个概率值。链式法则让我们可以将这个任务分解为对 P(X, Y, Z) 分解中每个因素的三个单独任务。
全概率公式允许你将边缘概率分布(单个变量的分布)与联合分布联系起来。例如,如果我们想从 X 和 Y 的分布,即 P(X, Y),推导出 X 的边缘分布,即 P(X),我们可以对 Y 进行求和。

在图 2.5 中,我们通过对行中的 Y 进行求和来得到 P(X)。在 X 是连续随机变量的情况下,我们通过对 Y 进行积分而不是求和。
最后,我们有贝叶斯法则:

我们通过将条件概率的原始定义应用于分子来推导这一点:

贝叶斯法则本身并不特别有趣——它是一个推导。更有趣的想法是贝叶斯主义,这是一种哲学,它使用贝叶斯法则来帮助模型者对其建模问题中的主观不确定性进行推理。我将在第 2.4 节中涉及这一点。
2.1.5 马氏假设和马氏核
当你有因子链时,建模的一个常见方法是使用 马尔可夫假设。这种建模方法对变量进行排序,并做出简化假设,即排序中的每个元素只依赖于它直接前面的元素。例如,再次考虑以下 P(x, y, z) 的分解:

如果我们应用马尔可夫假设,这将简化为:

这将使我们能够用 P(z|x, y) 替换 P(z|y),这更容易建模。在这本书中,当我们有一个使用马尔可夫假设简化过的因子时,比如 P(z|y),我们将它称为 马尔可夫核。
马尔可夫假设是统计学和机器学习中常见的简化假设;在考虑 Y 后,Z 可能 实际上 仍然依赖于 X,但我们 假设 这种依赖性很弱,我们可以在模型中安全地忽略它。我们将看到,马尔可夫假设是图形因果的关键,在那里我们将假设效应在给定其直接原因的情况下独立于其间接原因。
2.1.6 参数
假设我想在代码中实现一个概率分布的抽象表示,就像图 2.1 中的表格分布,我可以用于不同的有限离散结果。首先,如果我要模拟另一个三面骰子,它可能具有不同的概率值。我想要保持的是如图 2.8 所示的基本结构。

图 2.8 表格概率分布数据结构的框架
在代码中,我可以将其表示为一个具有两个参数的构造函数的对象类型,即 ρ[1] 和 ρ[2],如图 2.9 所示(“ρ”是希腊字母“rho”)。

图 2.9 向数据结构添加参数
第三个概率值是其他两个值的函数(而不是第三个参数,ρ[3]),是因为概率值必须加起来等于 1。值集 {ρ[1], ρ[2]} 是分布的参数。在编程术语中,我可以创建一个表示有三个值的表的类型。然后,当我想要一个新的分布时,我可以使用这两个参数作为参数构造这个类型的新实例。
最后,在我的三面骰子示例中,有三种结果,{1, 2, 3}。也许我想要我的数据结构处理不同的预指定数量的结果。在这种情况下,我需要一个表示结果数量的参数。让我们用希腊字母 kappa,Κ来表示它。我的参数化是 {Κ, ρ[1], ρ[2], … ρ**[Κ][–1]},其中 ρ**[Κ]是其他 ρ 参数总和的 1 减去。
在 pgmpy 类的 DiscreteFactor 和 TabularCPD 中,ρ**’s(rho)是传递给 values 参数的值的列表,而 Κ 对应于传递给 cardinality、variable_card 和 evidence_card 参数的值。一旦我们有了像 TabularCPD 这样的概率分布表示,我们就可以使用一组参数指定该分布的一个实例。
希腊字母与罗马字母
在这本书中,我使用罗马字母(A、B 和 C)来指代建模领域中代表对象的随机变量,例如“掷骰子”或“国内生产总值”,我使用希腊字母来表示所谓的 参数。在这个上下文中,参数是描述罗马字母变量概率分布的值。希腊字母和罗马字母之间的这种区别在统计学中并不那么重要;例如,贝叶斯统计学家将罗马和希腊字母都视为随机变量。然而,在因果建模中,这种区别很重要,因为罗马字母可以是原因和结果,而希腊字母用于表征原因和结果之间的统计关系。
2.1.7 概率分布的典型类别
存在几个常见的分布函数类别。例如,我们刚刚研究的表格示例是来自 分类分布 类别的例子。分类分布是在我们可以视为类别的离散结果上的分布,例如 {“冰淇淋”,“冷冻酸奶”,“雪芭”}。伯努利分布类是分类类的一个特殊情况,其中只有两种可能的结果。离散均匀分布是所有结果都有相同概率的分类分布。在实现中,分类分布可以直接定义在类别上(如“尾巴”和“头部”),或者定义在类别的索引上(如 0 和 1)。
离散与连续随机变量
对于离散随机变量,我们一直使用带有符号 P(X=x) 的概率分布函数。概率分布函数返回变量取特定值的概率。对于连续随机变量,我们也有 概率密度函数,它描述了在连续范围内观察任何结果的相对可能性,并且通过区间积分给出概率。
当我们遇到离散或连续参数化有特定情况时,我们会将其指出并使用 p(X=x) 来表示概率密度函数。然而,在这本书中,我们将专注于独立于我们是在离散还是连续环境中来构建我们的因果问题。我们将主要使用概率分布函数的符号 P(X=x),但请记住,因果思想在连续情况下同样适用。
对于连续的、有界的或无界的变量集合,还有其他适合的典型分布类别。例如,正态(高斯)分布类别展示了著名的“钟形曲线”。我在计算机科学的意义上使用术语“类别”(或者,可能更理想的是,“类型”),因为分布只有在我们分配了希腊字母参数后才会实现。对于一个正态(高斯)分布类别,概率密度函数是

在这里,μ和σ是参数。
图 2.10 是一个流行的图表,展示了几个常用的典型分布。分布之间的箭头突出了它们之间的关系(例如,伯努利分布是二项分布的特殊情况),我们在这里不会深入探讨。

图 2.10 一个流行的典型概率分布集合。边捕捉了分布之间的数学关系(我们不会在这里探讨)。浅色分布是离散的,深色分布是连续的。箭头表示存在一个转换,可以将一个分布转换为另一个分布。
参数类型
在概率建模环境中,对如何解释典型参数有一个直观的理解是有用的。为此,将分布中的概率视为必须分配给所有可能结果的稀缺资源。某些结果可能比其他结果得到更多,但最终,所有结果的总和或积分必须等于 1。参数描述了有限概率如何分配给结果。
作为类比,我们将使用一个固定人口的城市。城市的参数决定了人口所在的位置。位置参数,如正态分布的“μ”(μ是正态分布的均值,但并非所有位置参数都是均值),就像你在谷歌地图上搜索城市名称时掉下的针。这个针表征了一个精确的点,我们可以称之为“城市中心”。在一些城市,大多数人住在城市中心附近,离中心越远人口越少。但在其他城市,城市的其他非中心区域人口密集。尺度参数,如正态分布的“σ”(σ是正态分布的标准差,但并非所有尺度参数都是标准差参数),决定了人口分布的广度;洛杉矶具有高尺度参数。形状参数(及其倒数,速率参数)以不简单是平移(如位置参数所做的那样)或拉伸或缩小(如尺度参数所做的那样)分布的方式影响分布的形状。例如,考虑香港的偏斜形状,其市中心区域密集地聚集着摩天大楼,而更住宅化的九龙则拥有更矮的建筑,分布在更广阔的空间中。
Pyro 库提供了标准分布作为建模原语。Pyro 与离散分类分布表相对应的是Categorical对象。
列表 2.3 Pyro 中的标准参数
import torch
from pyro.distributions import Bernoulli, Categorical, Gamma, Normal #1
print(Categorical(probs=torch.tensor([.45, .30, .25]))) #2
print(Normal(loc=0.0, scale=1.0))
print(Bernoulli(probs=0.4))
print(Gamma(concentration=1.0, rate=2.0))
1 Pyro 包括常用的标准分布。
2 分类分布接受一个概率值列表,每个值对应一个结果。
这将打印出以下分布对象的表示:
Categorical(probs: torch.Size([3]))
Normal(loc: 0.0, scale: 1.0)
Bernoulli(probs: 0.4000)
Gamma(concentration: 1.0, rate: 2.0)
与提供概率值不同,log_prob方法将提供概率值的自然对数,因为对数概率在计算上比常规概率有优势。对数(取e^(l,其中l是对数概率))转换回概率尺度。例如,我们可以创建一个参数值为 0.4 的伯努利分布对象。
bern = Bernoulli(0.4)
该分布将 0.4 的概率分配给值 1.0。由于数值原因,我们通常使用概率值的自然对数进行工作。
我们可以使用数学库中的exp函数将对数概率转换回概率尺度:
lprob = bern.log_prob(torch.tensor(1.0))
import math
print(math.exp(lprob))
对数概率进行指数运算返回以下概率值:
0.3999999887335489
由于计算机计算中浮点精度相关的舍入误差,它接近但不等于 0.4。
使用标准分布的条件概率
很少有标准的分布被广泛用于描述单个随机变量的集合,例如随机向量或矩阵。然而,我们可以使用链式法则将联合概率分布分解为条件分布,这些条件分布可以用标准分布来表示。例如,我们可以用以下正态分布来表示Y在X和Z条件下的情况,

其中位置参数μ(x,z)是x和z的函数。以下是一个线性函数的例子:

其他函数,如神经网络,也是可能的。这些β参数在机器学习中通常被称为权重参数。
2.1.8 可视化分布
在概率建模和贝叶斯推理设置中,我们通常用视觉来概念化分布。在离散情况下,一个常见的可视化是条形图。例如,我们可以将图 2.3 中的概率可视化成图 2.11 中的条形图。请注意,这并不是直方图;我将在第 2.3 节中强调这种区别。

图 2.11 离散概率分布的可视化。分布的输出在水平轴上,概率在垂直轴上。
当分布的结果集不是有限集时,我们仍然使用可视化。例如,图 2.12 叠加了两个分布函数:一个离散的泊松分布和一个连续的正态(高斯)分布(我这样指定两个分布,使它们重叠)。离散泊松分布没有结果的上限(其下限是 0),但随着数字的增加,概率逐渐减少,导致柱状图越来越小,直到柱状图变得极其微小而无法绘制。我们通过在图中绘制概率分布函数作为曲线来可视化正态分布。正态分布没有上下限,但离中心越远,概率值越小。

图 2.12 一个连续的正态分布(实线)近似了一个离散的泊松分布(灰色柱状图)。同样,结果值在水平轴上,概率值在垂直轴上。
可视化条件概率分布涉及将每个条件变量映射到图像中的某个元素。例如,在图 2.13 中,X 是离散的,而 Y 在 X 条件下的分布是一个正态分布,其中位置参数是 X 的函数。

图 2.13 给定离散的 X,连续的 Y 条件概率分布的可视化。对于不同的 X 值,我们得到不同的 Y 分布。
由于 X 是离散的,将 X 映射到颜色并叠加 P(Y|X=1)、P(Y|X=2) 和 P(Y|X=3) 的曲线是最简单的。然而,如果我们想可视化 P(Y|X, Z),我们需要将 Z 映射到除颜色之外的美学元素,例如伪 3D 图像中的第三个轴或图像网格中的行。但是,我们只能向 2D 可视化添加有限的信息。幸运的是,条件独立性帮助我们减少了条件变量的数量。
2.1.9 独立性与条件独立性
两个随机变量如果,非正式地说,观察一个随机变量的结果不影响另一个变量的结果概率,即 P(y|x)* = **P(y*), 则它们是 独立 的。我们用 X ⊥ Y 表示这一点。如果两个变量不是独立的,它们是 相关 的。
在给定其他变量的情况下,两个相关变量可以成为 条件独立 的。例如,X ⊥ Y | Z 意味着 X 和 Y 可能是相关的,但给定 Z 时它们是条件独立的。换句话说,如果 X 和 Y 是相关的,并且 X ⊥ Y | Z,那么 P(y|x) ≠ P(y) 并不成立,但 P(y|x, z) = P(y|z) 是成立的。
独立性是一个强大的简化工具
独立性是简化概率分布表示的强大工具。考虑一个联合概率分布 P(W, X, Y, Z) 以表格形式表示。表格中的单元格数量将是 W、X、Y 和 Z 每个可能的输出的数量的乘积。我们可以使用链式法则将问题分解为因子 {P(W), P(X|W), P(Y|X, W), P(Z|Y, X, W)},但这些因子跨度的总参数数量不会改变,因此总复杂性保持不变。
然而,如果 X 与 W 正交?那么 P(X|W) 就简化为 P(X)。如果 Z 与 Y|X 正交?那么 P(Z|Y, X, W) 就简化为 P(Z|X, W)。每次我们都可以将成对的条件独立性条件作为联合概率分布的约束来施加,这样就可以大幅度降低分布的复杂性。实际上,在统计建模中的模型构建和评估、机器学习中的正则化以及“dropout”等深度学习技术,要么是直接要么是隐含地试图在数据的联合概率分布上施加条件独立性。
条件独立性和因果性
条件独立性是因果建模的基础。因果关系导致相关变量之间的条件独立性。例如,一个孩子的父母和祖父母的血型都是该孩子血型的原因;这些血型都是相关的。但你只需要父母的血型,即直接原因,就可以完全确定孩子的血型,如图 2.14 所示。在概率术语中,孩子的和祖父母的血型在给定父母的情况下条件独立。

图 2.14 因果性如何导致条件独立性。父母的血型导致孩子的血型。祖父的血型与孩子的血型相关(虚线)。但父母的血型是直接原因,完全决定了孩子的血型,如图 2.14 所示。这些直接原因使得孩子和祖父的血型在给定父母的情况下条件独立。
因果性导致条件独立性的事实使我们能够根据条件独立性的证据来学习和验证因果模型。在第四章中,我们将以正式术语探讨条件独立性和因果性之间的关系。
2.1.10 期望值
随机变量函数的 期望值 是函数可能输出值的加权平均值,其中权重是该结果的概率。


在可能结果为连续的情况下,期望值由积分定义。


我们将要计算的一些因果量将用期望来定义。这些量只关于期望进行推理,而不是关于期望是如何计算的。与连续情况下的积分微积分相比,使用离散期望的基本算术更容易获得对问题的直观理解。因此,在这本书中,当有选择时,我使用离散随机变量和离散期望的例子。这些例子中的因果逻辑都推广到连续情况。
期望有许多有趣的数学性质。在这本书中,我们关注的是在条件独立下条件期望的简化:如果 X ⊥ Y,那么 E(X|Y) = E(X)。如果 X ⊥ Y|Z,那么 E(X|Y,Z) = E(X|Z)。用更简单的话说,如果两个变量 (X 和 Y) 是独立的,我们对其中一个变量的期望不会随着另一个变量的信息而改变。如果它们的独立性在第三个变量 (Z) 的条件下成立,那么在知道第三个变量的情况下,我们对其中一个变量的期望不会受到另一个变量信息的影响。
除了这个之外,最重要的性质是期望的线性,意味着期望通过线性函数。以下是期望线性的一些有用的参考例子:
- 对于随机变量 X 和 Y:E(X + Y) = E(X) + E(Y) 和

-
对于常数 a 和 b:E(aX + b) = aE(X) + b
-
如果随机变量 X 只有 0 和 1 两种结果,并且 E(Y|X) = aX + b,那么 E(Y|X=1) – E(Y|X=0) = a。 (这是因为 a**1 + b – (a**0 + b) = a。剧透一下:这一点对于基于线性回归的因果效应推断技术非常重要。)
随机变量分布的均值是变量的期望值本身,如在 E(X) 中(即函数是恒等函数,f(X) = X)。在几个典型分布中,均值是参数的简单函数。在某些情况下,例如在正态分布中,位置参数等同于期望。但位置参数和期望并不总是相同的。例如,柯西分布有一个位置参数,但它的均值是未定义的。
在下一节中,你将学习如何使用计算方法来表示分布和计算期望。
2.2 计算概率
我们需要编码概率分布和期望值,从概率到我们的模型中使用。在上一节中,你看到了如何为三面骰子编码概率分布。但我们是怎样编码掷三面骰子的?我们如何编写代码来表示两个条件独立的骰子掷出的结果?在此同时,我们如何让计算机做计算期望值的数学运算?我们如何让一个所有事情都是确定性的计算机掷骰子,使得结果在掷之前是未知的?
2.2.1 概率的物理解释
假设我有一个三面骰子。我为骰子上的每个结果分配了一些概率值。这些概率值意味着什么?我如何解释它们?
假设我反复掷骰子,并记录每次看到每个结果出现的次数。首先,掷骰子是随机的,这意味着尽管我每次都以相同的方式掷骰子,但结果却各不相同。骰子的物理形状会影响这些计数;如果骰子的一个面比其他两个面大,那么这种大小差异会影响计数。当我重复掷骰子多次后,我看到某个特定结果的比例会收敛到一个数字。假设我用这个数字作为我的概率值。进一步,假设我将这个数字解释为每次掷骰子看到该结果的机会。
这个想法被称为物理(或频率主义)概率。物理概率意味着想象一个可重复的物理随机过程,该过程在可能的结果集中产生一个结果。我们使用在无限次重复随机过程时出现该结果的比例来分配概率值。然后我们将那个概率解释为该物理过程产生该结果的趋势。
2.2.2 随机生成
根据前面的物理概率定义,我们可以定义随机生成。在随机生成中,算法从给定的分布中随机选择一个结果。算法的选择受到物理概率的启发;它选择结果的方式是,如果我们无限次地运行该算法,它选择该结果的比例将等于该分布对该结果的概率值。
计算机是确定性的机器。如果我们对相同的输入反复运行计算机程序,它总是会返回相同的输出;它不能产生真正的随机(除非它有随机输入)。计算机必须使用确定性算法来模拟随机生成。这些算法被称为伪随机数生成器——它们从一个起始数字开始,称为随机种子,并返回一系列确定性的数字。这些算法在数学上保证一系列数字在统计上与理想随机生成的概念不可区分。
在符号表示中,我这样写随机生成:

这读作“x 是从 X 的概率分布中生成的。”
在随机生成中,“生成”的同义词包括“模拟”和“采样”。例如,在 pgmpy 中,DiscreteFactor 的 sample 方法执行随机生成。它返回一个 pandas DataFrame。请注意,由于这是随机生成,当你运行此代码时,你可能会得到不同的输出:

列表 2.4 在 pgmpy 中从 DiscreteFactor 模拟随机变量
from pgmpy.factors.discrete import DiscreteFactor
dist = DiscreteFactor(
variables=["X"],
cardinality=[3],
values=[.45, .30, .25],
state_names= {'X': ['1', '2', '3']}
)
dist.sample(n=1) #1
1 n 是你希望生成的实例数量。
这生成了图 2.15 所示的表格。

图 2.15 从 P(X) 生成一个实例创建了一个包含一行数据的 pandas DataFrame 对象。
我们也可以从联合概率分布中生成。
joint = DiscreteFactor(
variables=['X', 'Y'],
cardinality=[3, 2],
values=[.25, .20, .20, .10, .15, .10],
state_names= {
'X': ['1', '2', '3'],
'Y': ['0', '1']
}
)
joint.sample(n=1)
这生成了图 2.16 所示的表格。

图 2.16 从 P(*X**, * Y) 生成一个实例创建了一个包含一行数据的 pandas DataFrame 对象。
Pyro 还有一个用于规范分布的 sample 方法:
import torch
from pyro.distributions import Categorical
Categorical(probs=torch.tensor([.45, .30, .25])).sample()
这生成了一个从该分类分布的样本,即 0、1 或 2。
tensor(1.)
2.2.3 编码随机过程
当我们想要以特定方式生成值时,可以将自己的随机过程编写为代码。将随机过程编写为代码有时被称为 随机函数、概率子程序 或 概率程序。例如,考虑联合概率分布 P(X, Y, Z)。我们如何从这个联合分布中随机生成?遗憾的是,软件库通常不提供对任意联合分布的伪随机生成。
我们可以通过应用链式法则和,如果存在的话,条件独立性来解决这个问题。例如,我们可以如下分解:

假设 Y 在给定 X 的条件下与 Z 条件独立,那么:

最后,假设我们可以从 P(Z)、P(X|Z) 和 P(Y|X) 中采样,给定我们软件库中的基本随机生成函数。然后我们可以使用这种分解来组合一个采样算法:

这是一个我们可以在代码中执行的随机过程。首先,我们从 P(Z) 中生成一个 Z-结果 z。然后我们根据那个 z 条件化 X,并生成一个 X-结果 x。我们以相同的方式生成一个 Y-结果 y。最后,这个程序从联合分布 P(X, Y, Z) 中生成一个元组 {x, y, z}。
在 pgmpy 中,我们可以使用名为 BayesianNetwork 的类创建随机过程。
列表 2.5 在 pgmpy 和 Pyro 中创建随机过程
from pgmpy.factors.discrete.CPD import TabularCPD
from pgmpy.models import BayesianNetwork
from pgmpy.sampling import BayesianModelSampling
PZ = TabularCPD( #1
variable='Z', #1
variable_card=2, #1
values=[[.65], [.35]], #1
state_names = { #1
'Z': ['0', '1'] #1
}) #1
PXgivenZ = TabularCPD( #2
variable='X', #2
variable_card=2, #2
values=[ #2
[.8, .6], #2
[.2, .4], #2
], #2
evidence=['Z'], #2
evidence_card=[2], #2
state_names = { #2
'X': ['0', '1'], #2
'Z': ['0', '1'] #2
}) #2
PYgivenX = TabularCPD( #3
variable='Y', #3
variable_card=3, #3
values=[ #3
[.1, .8], #3
[.2, .1], #3
[.7, .1], #3
], #3
evidence=['X'], #3
evidence_card=[2], #3
state_names = { #3
'Y': ['1', '2', '3'], #3
'X': ['0', '1'] #3
}) #3
model = BayesianNetwork([('Z', 'X'), ('X', 'Y')]) #4
model.add_cpds(PZ, PXgivenZ, PYgivenX) #5
generator = BayesianModelSampling(model) #6
generator.forward_sample(size=1) #7
1 P(Z)
2 P(X|Z=z)
3 P(X|Z=z)
4 P(Y|X=x)
5 创建一个贝叶斯网络对象。参数是有向图的边,我们将在第三章中介绍。
6 将条件概率分布添加到模型中。
7 从贝叶斯网络对象创建一个贝叶斯模型采样对象。
8 从结果对象中采样
这将在 pandas DataFrame 中产生一行,如图 2.17 所示。

图 2.17 forward_sample 方法模拟了 X、Y 和 Z 作为一个 pandas DataFrame 中的行。
实现随机生成过程的随机过程非常强大,因为它允许从我们无法用清晰数学术语或单个典型分布表示的联合分布中生成。例如,虽然 pgmpy 与分类分布配合得很好,但 Pyro 给我们提供了使用典型分布组合的灵活性。
以下列表显示了先前随机过程的 Pyro 版本。它具有 Z、X 和 Y 之间的相同依赖关系,但具有不同的典型分布。
列表 2.6 在 Pyro 中使用典型分布的组合
import torch
from pyro.distributions import Bernoulli, Poisson, Gamma
z = Gamma(7.5, 1.0).sample() #1
x = Poisson(z).sample() #2
y = Bernoulli(x / (5+x)).sample() #3
print(z, x, y)
1 使用伽马分布表示 P(Z),并采样 z。
2 使用泊松分布表示 P(X|Z=z),位置参数为 z,并采样 x。
3 使用伯努利分布表示 P(Y|X=x)。概率参数是 x 的函数。
这将打印出一个示例集,例如以下内容:
tensor(7.1545) tensor(5.) tensor(1.)
Z 来自伽马分布,X 来自均值为 z 的泊松分布,而 Y 来自参数设置为 x 函数的伯努利分布。
使用编程语言实现随机函数让我们可以使用细微的条件控制流。考虑以下伪代码:
z ~ P(Z)
x ~ P(X|Z=z)
y = 0
for i in range(0, x){ #1
y_i ~ P(Y|X=x) #1
y += y_i #2
}
1 我们可以使用控制流,如这个 for 循环,来生成值。
2 y 是 for 循环中生成的值的总和。y 仍然依赖于 x,但通过细微的控制流。
在这里,y 仍然依赖于 x。然而,它被定义为 x 个单个随机成分的总和。在 Pyro 中,我们可能这样实现它。
列表 2.7 Pyro 中具有细微控制流的随机过程
import torch
from pyro.distributions import Bernoulli, Poisson, Gamma
z = Gamma(7.5, 1.0).sample()
x = Poisson(z).sample()
y = torch.tensor(0.0) #1
for i in range(int(x)): #1
y += Bernoulli(.5).sample() #1
print(z, x, y)
1 y 被定义为随机抛硬币的总和,因此 y 是从 P(Y|X=x) 生成的,因为抛掷次数取决于 x。
在 Pyro 中,最佳实践是将随机过程实现为函数。此外,使用 pyro.sample 函数生成,而不是使用分布对象的 sample 方法。我们可以将前面的 random_process 代码(列表 2.7)重写如下。
列表 2.8 使用函数进行随机过程和 pyro.sample
import torch
import pyro
def random_process():
z = pyro.sample("z", Gamma(7.5, 1.0))
x = pyro.sample("x", Poisson(z))
y = torch.tensor(0.0)
for i in range(int(x)):
y += pyro.sample(f"y{i}", Bernoulli(.5)) #1
return y
1 f"y{i}" 创建了 "y1"、"y2" 等名称。
pyro.sample 的第一个参数是一个字符串,用于为你要采样的变量分配一个名称。当我们开始在第三章中运行 Pyro 的推理算法时,原因将变得明显。
2.2.4 蒙特卡洛模拟和期望
蒙特卡洛算法 使用随机生成来估计从感兴趣的概率分布中的期望值。这个想法很简单。你有一些从 P(X) 中生成的方法。如果你想 E(X),生成多个 x,然后取这些 x 的平均值。如果你想 E(f(X)),生成多个 x,并将函数 f(.) 应用到每个 x 上,然后取平均值。蒙特卡洛算法甚至在 X 是连续的情况下也能工作。
在 pgmpy 中,你使用 sample 或 forward_sample 方法来生成一个 pandas DataFrame。然后你可以计算 pandas 的 mean 方法。
generated_samples = generator.forward_sample(size=100)
generated_samples['Y'].apply(int).mean()
在 Pyro 中,我们反复调用 random_process 函数。我们可以使用一个生成 100 个样本的 for 循环来对前面的 Pyro 生成器做这件事:
generated_samples = torch.stack([random_process() for _ in range(100)])
此代码在 Python 列表推导式中反复调用 random_process。回想一下,Pyro 扩展了 PyTorch,它返回的 y 值是一个张量。我使用 torch.stack 将这些张量列表转换成一个单独的张量。最后,我在这个张量上调用 mean 方法来获得 E(Y) 的蒙特卡洛估计。
generated_samples.mean()
当我运行这段代码时,我得到了大约 3.78 的值,但你可能会得到一个略有不同的值。
你想要了解的关于分布的大部分内容都可以用某个函数 f(X) 来表达。例如,如果你想了解 X 大于 10 的概率,你可以简单地生成一些 x,并将每个大于 10 的 x 转换为 1,否则为 0。然后你取 1 和 0 的平均值,得到的值将估计所需的概率。
为了说明,以下代码扩展了前面的代码块来计算 E(Y²)。
torch.square(generated_samples).mean()
当计算随机变量 X 的 E(f(X)) 时,记得先应用函数到样本上,然后取平均值来获得蒙特卡洛估计。如果你先对样本平均数应用函数,你将得到 f(E(X)) 的估计,这几乎总是不同的。
2.2.5 编程概率推理
假设我们用代码实现一个随机过程,从 P(X, Y, Z) 中生成结果 {x, y, z},如下所示:

此外,假设我们感兴趣的是从 P(Z|Y=3) 中生成。我们该如何做呢?我们的过程可以从 P(Z)、P(X|Z) 和 P(Y|Z) 中采样,但并不清楚我们如何从这些概率分布中得到 P(Z|Y)。
概率推理算法 通常将生成结果的随机过程和一些目标分布作为输入。然后,它们返回从该目标分布生成的方法。这类算法通常被称为贝叶斯推理算法,因为算法通常使用贝叶斯规则从 P(Y|Z) 转换到 P(Z|Y)。然而,与贝叶斯规则的联系并不总是明确的,所以我更喜欢“概率推理”而不是“贝叶斯推理算法”。
例如,一类简单的概率推理算法被称为接受/拒绝算法。将简单的接受/拒绝技术应用于从 P(Z|Y=3) 中生成的工作原理如下:
-
重复使用我们的 P(X, Y, Z) 生成器生成 {x, y, z}。
-
丢弃任何生成的结果,其中 y 不等于 3。
-
对于 Z 的结果集将具有分布 P(Z|Y=3)。
使用 Pyro 举例,让我们重写之前的 random_process 函数以返回 z 和 y。之后,我们将获得 E(Z|Y=3) 的蒙特卡洛估计。
列表 2.9 Pyro 中的蒙特卡洛估计
import torch
import pyro
from pyro.distributions import Bernoulli, Gamma, Poisson
def random_process():
z = pyro.sample("z", Gamma(7.5, 1.0))
x = pyro.sample("x", Poisson(z))
y = torch.tensor(0.0)
for i in range(int(x)):
y += pyro.sample(f"{i}", Bernoulli(.5))
return z, y #1
generated_samples = [random_process() for _ in range(1000)] #2
z_mean = torch.stack([z for z, _ in generated_samples]).mean() #3
print(z_mean)
1 这个新的 random_process 版本返回了 z 和 y。
2 使用列表推导生成 1000 个 z 和 y 的实例。
3 将单个 z 张量合并成一个张量,然后通过均值方法计算蒙特卡洛估计。
这段代码估计 E(Z)。由于 Z 是从伽马分布中模拟的,所以 Z 的真实均值 E(Z) 是形状参数 7.5 除以速率参数 1.0,即 7.5。
现在,为了估计 E(Z|Y=3),我们将过滤样本,只保留 Y 为 3 的样本。
z_given_y = torch.stack([z for z, y in generated_samples if y == 3])
print(z_given_y.mean())
这段代码的一次运行产生了 tensor(6.9088),但你的结果可能略有不同。如果结果 Y=3 发生频率较高,这个概率推理算法效果良好。如果这个结果很少见,算法将效率低下:我们需要生成许多样本以获得满足条件的样本,并且我们会丢弃许多样本。
对于概率推理,有各种其他算法,但这个主题对于我们深入探讨因果建模来说太丰富了,也太偏离主题了。尽管如此,以下算法值得我们在此书中提及。访问 www.altdeep.ai/p/causalaibook 获取有关使用 pgmpy 和 Pyro 进行推理的一些补充材料的链接。
概率权重方法
这些方法从联合概率分布生成结果,然后根据其在目标分布中的概率对其进行加权。然后我们可以使用这些权重通过蒙特卡洛估计进行加权平均。这类推理的流行变体包括重要性采样和逆概率重新加权,后者在因果推理中很受欢迎,并在第十一章中介绍。
使用概率图模型进行推理
概率图模型使用图来表示联合概率分布中的条件独立性。图的存在使得基于图的算法能够进行推理。两种知名的方法包括变量消除和信念传播。在图 2.5 和 2.6 中,我展示了你可以通过在概率表的列或行上求和来“消除”一个变量。变量消除使用图结构来最优地求和你要消除的变量,直到结果表代表目标分布。相比之下,信念传播是一个消息传递系统;图被用来形成相邻变量的不同“团”。例如,如果 P(Z|Y=1) 是目标分布,Y=1 是在团之间迭代传递的消息。每次收到消息时,团中的参数都会更新,然后消息被传递下去。最终,算法收敛,我们可以从这些更新的参数中推导出 Z 的新分布。
基于图的概率推理的一个吸引人的特点是,用户通常不需要自己实现它们;像 pgmpy 这样的软件会为你完成。虽然有一些理论上的注意事项,但在实践中通常并不重要。这个特性是我在第一章中强调的“推理商品化”趋势的一个例子。在这本书中,我们将使用因果图模型,这是一种特殊的概率图模型,它作为一个因果模型工作。这给了我们应用基于图的推理来解决因果问题的选项。
变分推理
在 变分推理 中,我们为一个新的随机过程编写代码,该过程从类似于目标分布的“近似分布”生成样本。这个随机过程具有参数,我们使用深度学习软件中常见的基于梯度的技术来优化这些参数。优化的目标函数试图最小化近似分布与目标分布之间的差异。
Pyro 是一种概率建模语言,它将变分推理视为主要的推理技术。它将来自近似分布的随机过程称为“引导函数”,一个熟练的 Pyro 程序员会擅长编写引导函数。然而,它还提供了一套“自动引导函数生成”的工具,这是推理商品化的另一个例子。
马尔可夫链蒙特卡洛
马尔可夫链蒙特卡洛(MCMC)是计算贝叶斯主义者中流行的一种推理算法。这些是接受/拒绝算法,其中每个新生成的结果都依赖于前一个(非拒绝)生成的结果。这产生了一系列结果,链中结果分布最终收敛到目标分布。哈密顿蒙特卡洛(HMC)是一个不需要用户实现生成器的流行版本。Pyro 以及类似的库,如 PyMC,实现了 HMC 和其他 MCMC 算法。
高级推理方法
生成模型的研究持续发展新的推理技术。例如,包括对抗推理、基于正态流的推理和基于扩散的推理等技术。这些技术的目标是高效地从机器学习问题中常见的复杂分布中进行采样。再次,参见www.altdeep.ai/p/causalaibook获取参考文献。在第六章中,我们将看到一个利用正态流的因果模型示例。本书采用的方法是利用第一章中讨论的“推理商品化”趋势,这样我们就可以构建利用这些算法的因果模型,以及随着发布的新算法。
2.3 数据、总体、统计和模型
到目前为止,我们已经讨论了随机变量和分布。现在我们将转向数据和统计。让我们先定义一些术语。你无疑对数据有一个概念,但让我们用本章中已经定义的术语来定义它。数据是一组随机变量或随机变量集的记录结果。统计量是从数据中计算出的任何东西。例如,当你使用训练数据训练神经网络时,学到的权重参数值是统计量,模型的预测也是如此(因为它们通过权重依赖于训练数据)。
生成特定数据流的现实世界因果过程被称为数据生成过程(DGP)。模型是对该过程的简化数学描述。统计模型是一个参数经过调整以使模型与数据中的统计模式相匹配的模型。
本节介绍了与数据和相关统计有关的一些核心概念,这些概念对于理解本书至关重要。
2.3.1 概率分布作为总体模型
在应用统计学中,我们从数据中提取统计洞察并将其推广到总体。例如,考虑第一章中描述的 MNIST 数字分类问题。假设在 MNIST 数据上训练分类模型的目的是将模型部署到软件中,该软件将数字化文本文档。在这种情况下,总体是软件未来将看到的所有文本中的所有数字。
人口是异质的,这意味着人口中的成员各不相同。虽然网站上的某个功能可能会提高用户群体的参与度,但平均而言,该功能可能会使某些亚群体用户的参与度降低,因此你希望将该功能定位到正确的亚群体。营销人员称之为“细分”。
在另一个例子中,一种药物可能对广大患者群体平均来说帮助不大,但某些亚群体可能会体验到好处。针对这些亚群体是精准医学领域的目标。
在概率模型中,我们使用概率分布来模拟人口。针对亚群体使用条件概率特别有用。例如,假设 P(E|F=True) 表示所有接触到网站功能的用户中的参与度数字分布。那么 P(E|F=True, G="millennial") 表示接触到该功能的用户中也是千禧一代的亚群体。
经典分布和随机过程作为人口模型
如果我们使用概率分布来模拟人口,对于给定的人口,我们应该使用哪些经典分布?图 2.18 包括了常见的分布以及它们通常模拟的现象。

图 2.18 常见经典分布的示例及其通常模拟的现象和数据类型
这些选择并非凭空而来。经典分布本身是从随机函数中推导出来的。例如,二项分布是连续一系列抛硬币的结果。当某事物是许多独立(或弱相关)的小变化相加的结果时,你会得到正态分布。等待时间分布捕捉了一个人必须等待某个事件(例如,设备故障或车祸)所需时间的分布。指数分布适用于等待时间,当你已经等待的时间对你还需要等待的时间没有影响时(例如,放射性原子衰变所需的时间)。如果事件发生的时间具有指数分布,那么在固定时间段内该事件发生的次数具有泊松分布。
在概率建模中,一个有用的技巧是思考创建目标人口所涉及的随机过程。然后可以选择适当的经典分布,或者使用代码实现随机过程,在代码逻辑中将各种经典分布作为原语。在这本书中,我们将看到这种推理与因果建模非常吻合。
样本、独立同分布和生成
通常,我们的数据不是整个群体的全部,而是来自群体的小子集。随机选择个体的行为被称为抽样。当数据是通过反复从群体中抽样创建时,得到的数据集被称为随机样本。如果我们能将数据视为随机样本,我们称这些数据为独立同分布(IID)。这意味着每个数据点的选择在抽样方式上是相同的,每个抽样都是独立于其他抽样发生的,并且它们都是从相同的群体分布中抽样的。图 2.19 说明了如何从群体中选取一个 IID 随机样本。

图 2.19 通过从群体中随机选择创建随机样本。个体是从群体中随机选择的,使得样本分布类似于群体分布。样本是同质且独立分布的(IID),这意味着样本成员是以相同的方式选择的,并且是否选择某个个体并不取决于是否选择了另一个个体。
抽样和 IID 数据的理念说明了使用概率分布来模拟群体的第二个好处。我们可以使用从该分布生成的方法来模拟从群体中抽样。我们可以通过首先编写表示群体的随机过程,然后将其与生成群体过程数据的进程组合,来表示 DGP,从而模拟 IID 抽样。
在 pgmpy 中,这就像生成多个样本一样简单。
generator.forward_sample(size=10)
这产生了显示在图 2.20 中的表格

图 2.20 由 pgmpy 中的模型生成十个数据点创建的 pandas DataFrame
Pyro 进行 IID 抽样的方法是pyro.plate。
列表 2.10 在 Pyro 中生成 IID 样本
import pyro
from pyro.distributions import Bernoulli, Poisson, Gamma
def model():
z = pyro.sample("z", Gamma(7.5, 1.0))
x = pyro.sample("x", Poisson(z))
with pyro.plate("IID", 10): #1
y = pyro.sample("y", Bernoulli(x / (5+x))) #2
return y
model()
1 pyro.plate 是一个用于生成条件独立样本的上下文管理器。这个 pyro.plate 实例将生成 10 个 IIΔ样本。
2 调用 pyro.sample 生成一个单个结果 y,其中 y 是一个包含 10 个 IIΔ样本的张量。
使用生成来模拟抽样在机器学习中特别有用,因为数据通常不是 IID。在第一章的 MNIST 示例中,原始 NIST 数据不是 IID——一块数据来自高中生,另一块来自政府官员。你可以在你的随机过程中将数字作者的标识作为一个变量来捕捉。然后,数据就会在该变量上成为 IID 条件。
不要把地图误认为是地形
再次考虑 MNIST 数据。该数据的人口相当模糊和抽象。如果该数字分类软件被多个客户许可,人口将是一个实际上无休止的数字流。在机器学习和统计学中,将数据推广到抽象的人口是常见场景,正如 R.A. Fisher 这位现代统计学奠基人在 Rothamsted Research 设计用于测试土壤类型对作物生长影响的实验时,他试图弄清楚如何将样本推广到未来作物的总体(尽可能少的样本数量)。
与模糊庞大的群体合作的问题在于,这可能导致将群体与概率分布心理上混淆的错误。不要这样做。不要将地图(用于模拟群体的分布)误认为是地形(群体本身)。
为了说明,考虑以下例子:在撰写本章的部分内容时,我在葡萄牙阿尔加维的 Silves 度假,这是一个拥有大城堡、深厚历史和绝佳徒步旅行地点的小镇。假设我对模拟 Silves 居民的身高感兴趣。
官方上,Silves 的人口为 11,000,所以让我们以这个数字作为基准真实值。这意味着 Silves 有 11,000 个不同的身高值。假设我亲自去到 Silves 的国家卫生中心,获取了一份每个居民身高的电子表格。那么我所拥有的数据不是随机抽样的人口子集——而是整个群体本身。
我可以对这一人群计算一个 直方图,如图 2.21 所示。直方图是人口或样本中值(在这种情况下,为身高)计数的可视化。对于连续值,如身高,我们计算有多少值落在某个范围或“区间”内。

图 2.21 显示所有 Silves 居民身高分布的直方图
这个直方图代表了整个群体分布。我可以通过将计数除以人数来使其看起来更像一个概率分布,如图 2.22 所示。

图 2.22 Silves 居民给定身高的比例直方图
某人可能会说这个分布遵循正态(高斯)概率分布,因为我们看到了一个钟形曲线,而且确实,正态分布适用于像身高这样的进化钟形现象。但这个说法并不完全准确。为了理解这一点,考虑所有正态分布都是为负数定义的(尽管这些数字可能只有极小的概率密度),而身高不能是负数。我们真正做的是使用正态分布作为一个 模型——作为对这个群体分布的 近似。
在另一个例子中,图 2.23 展示了简·奥斯汀小说中词类的真实分布。请注意,这并非基于她小说中页面的样本;我是从她所有六部完成小说中的 725,000 个词的词类分布中创建了这个可视化。

图 2.23 简·奥斯汀所有小说中词类的实际分布
作为模型师,我们使用规范分布来建模人群分布,但模型并不等同于人群分布。这个观点可能看起来像是微不足道的语义,但在大数据时代,我们经常可以推理整个群体,而不仅仅是随机样本。例如,流行的在线社交网络有数亿甚至数十亿用户。这是一个巨大的规模,而整个群体只需一个数据库查询即可获得。
在因果建模中,我们思考如何建模数据和人群的精确性非常有用。因果推断是关于人群的真实世界属性,而不仅仅是数据中的统计趋势。而我们想要回答的不同因果问题将需要我们将不同的因果假设融入我们的模型中,其中一些比其他假设更强或更难验证。
2.3.2 从观察数据到数据生成过程
在因果建模中,了解观察数据如何映射到数据中变量的联合概率分布,以及该联合概率分布如何映射回数据生成过程(DGP)非常重要。大多数模型师对这些实体之间的关系有一定程度的直觉,但在因果建模中我们必须明确。这种明确的理解很重要,因为在普通统计建模中,你模型的是联合分布(或其元素),而在因果建模中,你需要模型的是数据生成过程(DGP)。
从观察数据到经验联合分布
假设我们有一个如表 2.1 所示的五个数据点的数据集。
表 2.1 一个包含五个示例的简单数据集
| jenny_throws_rock | brian_throws_rock | window_breaks | |
|---|---|---|---|
| 1 | False | True | False |
| 2 | True | False | True |
| 3 | False | False | False |
| 4 | False | False | False |
| 5 | True | True | True |
我们可以对所有观察到的可观察结果进行计数,如表 2.2 所示。
表 2.2 每种可能结果组合的经验计数
| jenny_throws_rock | brian_throws_rock | window_breaks | counts | |
|---|---|---|---|---|
| 1 | False | False | False | 2 |
| 2 | True | False | False | 0 |
| 3 | False | True | False | 1 |
| 4 | True | True | False | 0 |
| 5 | False | False | True | 0 |
| 6 | True | False | True | 1 |
| 7 | False | True | True | 0 |
| 8 | True | True | True | 1 |
将结果数量(5)除以得到 经验联合分布,如表 2.3 所示。
表 2.3 数据的经验分布
| jenny_throws_rock | brian_throws_rock | window_breaks | proportion | |
|---|---|---|---|---|
| 1 | False | False | False | 0.40 |
| 2 | True | False | False | 0.00 |
| 3 | False | True | False | 0.20 |
| 4 | True | True | False | 0.00 |
| 5 | False | False | True | 0.00 |
| 6 | True | False | True | 0.20 |
| 7 | False | True | True | 0.00 |
| 8 | True | True | True | 0.20 |
因此,在离散结果的情况下,我们通过计数从数据到经验分布。
在连续情况下,我们可以计算经验分布的直方图、密度曲线或其他统计表示。你可以选择不同的统计方法来创建这些汇总,但这些都是同一基础经验分布的表示。
重要的是,经验联合分布不是数据中变量的实际联合分布。例如,我们看到经验分布中的一些结果在这五个数据点中从未出现过。它们出现的概率为零吗?更有可能的是,这些概率大于零,但我们没有看到这些结果,因为只采样了五个点。
作为类比,一个公平的骰子掷出 1 的概率是 1/6。如果你掷骰子五次,你几乎有(1–1/6)⁵=40%的概率在任何一次掷骰子中都不会看到 1。如果你真的这样做了,你不会想得出看到 1 的概率为零的结论。然而,如果你继续掷骰子,看到 1 的比例会收敛到 1/6。
备注:更精确地说,我们关于概率的频率主义解释告诉我们,将概率解释为当我们无限次掷骰子时得到 1 的比例。尽管是“无限次”,但在比例开始收敛到一个数字(1/6)之前,我们不需要掷很多次。
从经验联合分布到观测联合分布
观测联合概率分布是数据中观察到的变量的真实联合分布。假设表 2.4 显示了这些观察变量的真实观测联合概率分布。
表 2.4 假设这是真实的观测联合分布。
| jenny_throws_rock | brian_throws_rock | window_breaks | probability | |
|---|---|---|---|---|
| 1 | False | False | False | 0.25 |
| 2 | True | False | False | 0.15 |
| 3 | False | True | False | 0.15 |
| 4 | True | True | False | 0.05 |
| 5 | False | False | True | 0.00 |
| 6 | True | False | True | 0.10 |
| 7 | False | True | True | 0.10 |
| 8 | True | True | True | 0.20 |
从联合观测分布中采样产生经验联合分布,如图 2.24 所示。

图 2.24 从观测联合分布中采样产生观测数据和经验分布。
潜在变量:从观测联合分布到完整联合分布
在统计建模中,潜在变量 是那些在数据中未直接观察到但在统计模型中包含的变量。回到我们的数据示例,假设有一个第四个潜在变量,“冲击力强度”,如表 2.5 所示。
表 2.5 强度 _of_impact 列中的值是未见的“潜在”变量。
| jenny_throws_rock | brian_throws_rock | strength_of_impact | window_breaks | |
|---|---|---|---|---|
| 1 | False | True | 0.6 | False |
| 2 | True | False | 0.6 | True |
| 3 | False | False | 0.0 | False |
| 4 | False | False | 0.0 | False |
| 5 | True | True | 0.8 | True |
潜在变量模型在从机器学习到计量经济学到生物信息学等学科中都很常见。例如,在自然语言处理中,一个流行的概率潜在变量模型是主题模型,其中观测变量代表文档中单词和短语的出现,潜在变量代表文档的主题(例如,体育、政治、金融等)。
潜在变量被省略在观测联合概率分布中,因为正如其名所示,它们没有被观察到。观测变量和潜在变量的联合概率分布是完整的联合分布。要从完整的联合分布到观测联合分布,我们需要对潜在变量进行边缘化,如图 2.25 所示。

图 2.25 对潜在变量进行边缘化处理,得到观测联合分布。
从完整联合分布到数据生成过程
我使用以下 Python 代码编写了五个数据点的实际 DGP。
列表 2.11 代码形式的 DGP 示例
def true_dgp(jenny_inclination, brian_inclination, window_strength): #1
jenny_throws_rock = jenny_inclination > 0.5 #2
brian_throws_rock = brian_inclination > 0.5 #2
if jenny_throws_rock and brian_throws_rock: #3
strength_of_impact = 0.8 #3
elif jenny_throws_rock or brian_throws_rock: #4
strength_of_impact = 0.6 #4
else: #5
strength_of_impact = 0.0 #5
window_breaks = window_strength < strength_of_impact #6
return jenny_throws_rock, brian_throws_rock, window_breaks
1 输入变量反映了珍妮和布莱恩扔石头的意愿以及窗户的强度。
2 如果愿意,珍妮和布莱恩会扔石头。
3 如果珍妮和布莱恩都扔石头,总冲击力为 .8。
4 如果珍妮或布莱恩扔石头,总冲击力为 .6。
5 否则,没有人扔石头,冲击力为 0。
6 如果冲击力大于窗户的强度,窗户会破碎。
注意:一般来说,DGP 是未知的,我们的模型正在对其结构进行猜测。
在这个例子中,jenny_inclination、brian_inclination和window_strength是介于 0 和 1 之间的潜在变量。jenny_inclination代表 Jenny 的初始投掷欲望,brian_inclination代表 Brian 的初始投掷欲望,window_strength代表窗户玻璃的强度。这些都是导致数据中观测变量一个实例化的初始条件:(jenny_throws_ball、brian_throws_ball、window_breaks)。
我随后在以下五组潜在变量上调用了true_dgp函数:
initials = [
(0.6, 0.31, 0.83),
(0.48, 0.53, 0.33),
(0.66, 0.63, 0.75),
(0.65, 0.66, 0.8),
(0.48, 0.16, 0.27)
]
换句话说,以下 Python 中的for循环是产生五个数据点的实际采样过程:
data_points = []
for jenny_inclination, brian_inclination, window_strength in initials:
data_points.append(
true_dgp(
jenny_inclination, brian_inclination, window_strength
)
)
DGP 是生成数据的因果过程。注意,全联合概率分布中完全缺失的叙事元素;如果 Jenny 和 Brian 有投掷石头的倾向,他们会向窗户投掷石头,如果他们击中了窗户,窗户可能会破裂,这取决于他们是否投掷了石头以及窗户的强度。DGP 包含完整的联合概率分布,如图 2.26 所示。换句话说,联合概率分布是基于如何生成数据而由 DGP 产生的结果。

图 2.26 DGP 包含完整的联合分布。
总之,DGP 包含完整的联合分布,对完整联合分布进行边缘化产生观测联合分布。从这个分布中进行采样产生观测数据和相应的经验联合分布。当我们向下移动这个层次结构时,存在多对一的关系,这对因果建模和推理有影响。
层次结构中的多对一关系

图 2.27 当我们向下移动到层次结构时,存在多对一的关系。总之,存在多个与观测数据一致的 DGP。
当我们从 DGP 移动到完整的联合分布、观测联合分布和经验联合分布以及观测数据时,前一级别到后一级别之间存在多对一的关系,如图 2.27 所示。
同样,一个级别的对象与下一个级别上的多个对象是一致的:
-
可能存在多个与经验联合分布一致的观测联合分布。如果我们采样五个点,然后再采样五个点,我们将得到不同的数据集,从而得到不同的经验分布。
-
可能存在多个与一个观测联合分布一致的完整联合分布。这两个分布之间的区别是潜在变量。但如果我们对潜在变量的集合有不同的选择呢?例如,如果我们的观测分布是P(X, Y),那么如果我们的潜在变量集合是{Z, W},完整的联合分布将是P(X, Y, Z, W),或者如果我们的潜在变量集合是{Z, V},完整的联合分布将是P(X, Y, Z, V)。
-
可能存在多个与一个完整的联合概率分布一致的 DGP(数据生成过程)。假设在我们的窗口破坏例子中,珍妮有一个朋友伊莎贝尔,有时会怂恿珍妮扔石头,有时则不会,这影响了珍妮扔石头的倾向。这个 DGP 与原始的 DGP 不同,但伊莎贝尔的同伴压力的潜在变量与珍妮扔石头倾向之间的关系可能是这样的,即这个新的 DGP 导致了完全相同的联合概率分布。作为一个更简单的例子,假设我们观察的是三个骰子点数之和的分布。DGP 是掷三个骰子然后将它们相加。两个 DGP 可以在求和的顺序上有所不同;例如,(第一个 + 第二个) + 第三个或(第一个 + 第三个) + 第二个或(第二个 + 第三个) + 第一个。这些都会得到相同的分布。
那最后两个多对一关系是因果可识别性概念的基础,这是因果推理之所以困难的核心原因。正如俗话所说,这个概念是“相关性不等于因果性”的原因。
2.3.3 独立性统计测试
因果性将独立性和条件独立性强加给变量,因此我们依赖于条件独立性统计测试来构建和验证因果模型。
假设 X 和 Y 是独立的,或者给定 Z,X 和 Y 是条件独立的。如果我们有观察 X、Y 和 Z 的数据,我们可以运行一个独立性统计测试。标准的统计独立性程序返回一个测试统计量,该统计量量化了 X 和 Y 之间的统计关联,以及一个 p 值,该 p 值量化了在 X 和 Y 实际上给定 Z 条件独立的情况下,仅通过纯偶然得到这种程度或更极端的关联的概率。简单来说,这个测试量化了依赖性或独立性的统计证据。
表明有人犯下谋杀的证据并不等同于他们确实犯下谋杀的确定性事实。同样,表明两个变量之间独立性的统计证据并不等同于它们实际独立的实际事实。在这两种情况下,证据可以指向一个结论,但并不一定能最终证明它。例如,给定独立性是真实的,统计证据的强度可以因几个因素而异,例如数据的数量。而且,从这些测试中得出错误结论总是可能的。
记住,如果 X 和 Y 是独立的,那么 P(Y|X) 等同于 P(Y)。在预测术语中,这意味着 X 对 Y 没有预测能力。如果你不能使用经典统计测试(例如,如果 X 和 Y 是向量),那么你可以尝试训练一个预测模型并主观评估模型预测的好坏。
2.3.4 模型参数的统计估计
当我们“训练”或“拟合”一个模型时,我们试图估计模型参数的值,例如回归模型或神经网络中的权重。在统计建模和机器学习中,参数估计的目标是建模观测或联合概率分布。在因果建模中,目标是建模 DGP。这种区别对于做出良好的因果推断非常重要。
通过最大化似然估计
在非正式的术语和参数估计的背景下,似然是在给定参数向量候选值的情况下观察数据的概率。最大化似然意味着选择具有最高似然性的参数向量值。通常,我们通过最大化似然的对数而不是似然本身来进行工作,因为这从数学和计算上更容易实现;最大化似然的价值与最大化对数似然的价值相同。在特殊情况下,例如线性回归,最大似然估计有我们可以通过数学推导出的解,但在一般情况下,我们必须使用数值优化技术来找到解。在某些模型中,例如神经网络,找到最大化似然性的值是不切实际的,所以我们满足于一个相对似然性较高的候选值。
通过最小化其他损失函数和正则化估计
在机器学习中,有各种损失函数用于估计参数。最大化似然是最小化损失函数的一个特例,即负对数似然损失函数。
正则化是指在损失函数中添加额外的元素,以引导优化过程向更好的参数值靠拢。例如,L2 正则化将参数值的平方和的值添加到损失中。由于值的微小增加会导致值的平方增加更大,因此 L2 正则化有助于避免参数估计过大。
贝叶斯估计
贝叶斯估计将参数视为随机变量,并试图根据数据中观察到的变量来建模参数的条件分布(通常称为后验分布)。它是通过在参数上放置一个“先验概率分布”来实现的。先验分布有自己的参数,称为“超参数”,模型必须指定这些超参数。当模型中有潜在变量时,贝叶斯推理的目标是在给定观察变量的条件下,参数和潜在变量的联合分布。
如前所述,在这本书中,我使用希腊字母表示参数,罗马字母表示 DGP 中的变量,包括潜在变量。但对于贝叶斯统计学家来说,这种区别无关紧要;参数和潜在变量都是未知的,因此是推理的目标。
贝叶斯估计的一个主要优点是,你得到的不是参数的点值,而是参数的整个条件概率分布(更具体地说,你得到的是代表该分布的样本或参数值)。这个概率分布代表了参数值的不确定性,你可以将这种不确定性纳入模型预测或其他从模型中得出的推论中。
根据贝叶斯哲学,先验分布应该捕捉模型对参数真实值的主体信念。当我们把对 DGP 的因果结构和机制的信念转化为模型中的因果假设时,我们将在因果建模中做类似的事情。
估计器的统计和计算属性
由于估计参数有多种方法,让我们寻找比较估计方法质量的方法。假设我们想要估计的参数有一个真实值。统计学家思考估计方法如何很好地恢复这个真实值。具体来说,他们关心估计方法的偏差和一致性。估计器是一个随机变量,因为它来自数据(数据有分布),这意味着估计器有一个分布。如果该分布的均值等于它所估计的参数的真实值,则估计器是无偏的。一致性意味着你拥有的数据越多,估计值就越接近参数的真实值。在实践中,估计器的一致性比它是否无偏更重要。
计算机科学家知道,虽然理论上一致性很好,但要使估计方法能够处理“更多数据”比说起来容易。他们关心估计器的计算质量与数据量的关系。估计器是否随着数据量的增加而扩展?它是否可并行化?一个估计器可能是一致的,但当它在 iPhone 应用上运行时,它是否会在毫秒内收敛到真实值,而不会在这个过程中耗尽电池电量?
这本书将理解因果逻辑与因果参数估计器的统计和计算属性解耦。我们将专注于因果逻辑,并依赖像 DoWhy 这样的库,这些库使得统计和计算计算变得容易进行。
拟合优度与交叉验证
当我们估计参数时,我们可以计算各种统计量来告诉我们我们做得如何。一类统计量被称为拟合优度统计量。统计学家将拟合优度定义为量化模型与用于训练模型的所用数据拟合程度的统计量。这里还有一个定义:拟合优度统计量告诉你你的模型在多大程度上假装是用于训练模型的数据的 DGP。然而,正如我们所看到的,对于给定的数据集,存在多个可能的 DGP。拟合优度不会提供区分真实 DGP 的因果信息。
交叉验证统计通常表明你的模型在未接受过训练的数据上的预测效果如何。可能存在一个模型相对于其他模型具有相当好的拟合度,但仍然预测效果不佳。机器学习通常关注预测任务,因此倾向于使用交叉验证。然而,一个模型可以是一个好的预测器,并提供完全错误的因果推断。
2.4 决定论和主观概率
本节将探讨我们进行概率因果建模所需的哲学基础。在这本书中,我们将使用概率模型来建模因果模型。在训练模型时,我们可能希望使用贝叶斯参数估计程序。在进行因果推断时,我们可能希望使用概率推断算法。当我们进行因果决策时,我们可能希望使用贝叶斯决策理论。此外,结构因果模型(第六章)对模型中随机性可能发生的位置有严格的要求。这意味着明确区分贝叶斯主义、不确定性、随机性、概率建模和概率推断是很重要的。
第一个关键点是把 DGP 视为决定论的。第二个关键点是认为我们 DGP 模型中的概率是主观的。
2.4.1 决定论
抛掷岩石 DGP 的早期代码完全是决定论的;给定初始条件,输出是确定的。再次考虑我们对物理概率的定义:如果我掷骰子,为什么结果是随机的?
如果我拥有超人的灵巧、感知和心智处理能力,我可以在心中计算出骰子投掷的物理学并确定结果。这个决定论哲学思想本质上说 DGP 是决定论的。18 世纪法国学者皮埃尔-西蒙·拉普拉斯用一种称为拉普拉斯恶魔的思想实验来解释决定论。拉普拉斯想象了一个实体(恶魔),它知道宇宙中每个原子的精确位置和动量。有了这些知识,这个实体将能够以完全决定论的确定性知道宇宙的未来状态,因为它可以从(牛顿)力学的定律中计算出它们。换句话说,给定所有原因,结果 100%完全确定,毫无随机性。
为了明确起见,一些系统,当我们足够仔细地观察时,具有固有的随机元素(例如,量子力学、生物化学等)。然而,这种建模的哲学观点将适用于我们关心的大多数事物。
2.4.2 主观概率
在我们对概率的物理解释中,当我掷骰子时,概率代表了我对骰子滚动时所有粒子位置和动量的超人类知识的缺乏。换句话说,当我构建 DGP 的概率模型时,概率反映了我的知识缺乏。这种哲学思想被称为主观概率或贝叶斯概率。这种论点超越了贝叶斯法则和贝叶斯统计估计,认为模型中的概率代表了模型构建者对 DGP 的完全知识缺乏,并不代表 DGP 中的固有随机性。
主观概率扩展了我们对概率的“随机物理过程”解释。概率的物理解释对于简单的物理过程,如掷骰子、抛硬币或洗牌,效果很好。但是,当然,我们还想对许多难以被视为可重复物理过程的现象进行建模。例如,思想如何转化为言语,或者由于气候变化,新鲜水流增加对全球洋流系统的威胁。在这些情况下,我们仍然会使用随机生成来对这些现象进行建模。随机生成中使用的概率反映了,尽管我们作为模型构建者可能对数据生成过程的一些细节有所了解,但我们永远不会达到超人的确定性细节水平。
摘要
-
随机变量是一个变量,其可能值是随机现象的数值结果。
-
概率分布函数是一个将随机变量的结果映射到概率值的函数。联合概率分布函数将每个 X 和 Y 结果的组合映射到概率值。
-
我们从概率的基本公理中推导出链式法则、全概率定律和贝叶斯法则。这些规则在建模中非常有用。
-
马尔可夫假设意味着变量序列中的每个变量只依赖于直接在其前面的变量。这是统计建模中常见的简化假设,但在因果建模中起着重要作用。
-
经典分布类是分布的数学描述良好的表示。它们为我们提供了使概率建模灵活且相对容易的原语。
-
经典分布是通过一组参数实例化的,例如位置、尺度、速率和形状参数。
-
当我们构建模型时,知道哪些变量是独立的或条件独立的可以极大地简化模型。在因果建模中,独立性和条件独立性将在将相关性从因果关系分离中发挥关键作用。
-
具有有限个结果的随机变量的期望值是所有可能结果的加权平均值,其中权重是该结果的概率。
-
概率只是一个值。我们需要给这个值一个解释。概率的物理定义将概率映射到如果物理过程可以无限次重复运行,则结果发生的比例。
-
与概率的物理解释相反,贝叶斯对主观概率的解释将概率解释为信念,或者相反,不确定性。
-
当编码随机过程时,Pyro 允许你使用经典分布作为构建细微随机过程模型的原始元素。
-
蒙特卡洛算法使用随机生成来估计从感兴趣分布中的期望值。
-
流行的推理算法包括基于图模型的算法、概率加权、MCMC 和变分推理。
-
经典分布和随机过程可以作为我们希望模拟的人群的代理,以及我们想要从中进行推理的人群。条件概率是模拟异质子人群的绝佳方式。
-
不同的经典分布用于模拟不同的现象,例如计数、钟形曲线和等待时间。
-
从随机过程中生成是模拟现实生活独立同分布数据采样的良好模型。
-
给定一个数据集,可能存在多个数据生成过程(DGPs)可以生成该数据集。这个事实与从相关性中解析因果关系的挑战相关联。
-
统计独立性测试验证了关于潜在分布的独立性和条件独立性声明。
-
学习模型参数的方法有几种,包括最大似然估计和贝叶斯估计。
-
决定性意味着如果我们知道一个系统的所有信息,我们就可以以零误差预测其结果。主观概率是概率代表建模者对系统缺乏完整知识的一种观点。采用这些哲学观点将有助于我们理解因果 AI。
-
构建模型的一个好方法是分解联合分布,简化条件独立性的因子,然后将因子实现为随机过程。
-
一种强大的建模技术是使用概率分布来模拟人群,尤其是当你关心这些人群中的异质性时。
-
当我们使用概率分布来模拟人群时,我们可以将随机过程中的生成映射到从人群中采样。
-
虽然传统的统计建模模型观测联合分布或完整的联合分布,因果建模则模型化 DGP(数据生成过程)。
第二部分:构建和验证因果图
在第二部分,我们将专注于学习如何通过因果图来表示因果关系。我们还将学习如何使用数据来验证这些因果图,以及如何将它们与深度生成模型相结合。这一部分将使你具备系统地构建表示现实世界数据生成过程的因果结构,并实证验证这些结构的能力。
第三章:构建因果图形模型
本章涵盖
-
构建因果 DAG 来模拟 DGP
-
使用你的因果图作为沟通、计算和推理工具
-
在 pgmpy 和 Pyro 中构建因果 DAG
-
使用因果 DAG 作为支架训练概率机器学习模型
在本章中,我们将使用因果有向无环图(因果 DAG)构建数据生成过程(DGP)的第一个模型——一个没有环的有向图,其中边代表因果关系。我们还将探讨如何使用因果 DAG 作为支架来训练统计模型。
3.1 引入因果 DAG
假设我们可以将 DGP 划分为一组变量,其中给定的一组变量值代表 DGP 的可能状态。这些变量可以是离散的或连续的。它们可以是单变量,也可以是多变量向量或矩阵。
因果 DAG 是一个有向图,其中节点是这组变量,有向边代表它们之间的因果关系。当我们使用因果 DAG 来表示 DGP 时,我们假设边反映了 DGP 中的真实因果关系。
为了说明,回想一下第二章中的投掷石头 DGP。我们开始时,珍妮和布莱恩有一定倾向去砸窗户,窗户有一定的强度。如果其中任何一个人的砸窗倾向超过了一个阈值,他们就会砸。窗户是否破碎取决于他们是否砸或者两者都砸,以及窗户的强度。
设置你的环境
本章中的代码是用 pgmpy 版本 0.1.24,pyro-ppl 版本 1.8.6,和 DoWhy 版本 0.11.1 编写的。Python 的 Graphviz 库的版本 0.20.1 用于绘制 DAG 的图像,这取决于是否安装了核心 Graphviz 软件。如果你现在不想设置 Graphviz,请注释掉 Graphviz 代码。
请参阅本书的笔记www.altdeep.ai/p/causalaibook,以获取包含代码的 Jupyter 笔记本的链接。
我们现在将创建一个因果 DAG 来可视化这个过程。作为一个 Python 函数,DGP 在下面的列表中展示。
列表 3.1 DAG 投掷石头示例
def true_dgp(jenny_inclination, brian_inclination, window_strength): #1
jenny_throws_rock = jenny_inclination > 0.5 #2
brian_throws_rock = brian_inclination > 0.5 #2
if jenny_throws_rock and brian_throws_rock: #3
strength_of_impact = 0.8 #3
elif jenny_throws_rock or brian_throws_rock: #4
strength_of_impact = 0.6 #4
else: #5
strength_of_impact = 0.0 #5
window_breaks = window_strength < strength_of_impact #6
return jenny_throws_rock, brian_throws_rock, window_breaks
1 输入变量是介于 0 和 1 之间的数字。
2 如果珍妮和布莱恩有砸石头的倾向,他们会砸石头。
3 如果两人都砸石头,冲击力为 0.8。
4 如果其中一个人砸,冲击力为 0.6。
5 如果两人都不砸,冲击力为 0。
6 如果冲击力大于窗户强度,窗户会破碎。
图 3.1 展示了作为因果 DAG 的投掷石头 DGP。
在图 3.1 中,每个节点对应 DGP 中的一个随机变量。有向边对应因果关系(源节点是原因,目标节点是结果)。

图 3.1 表示投掷石头 DGP 的因果 DAG。在这个例子中,每个节点对应 DGP 中的一个随机变量。
3.1.1 案例研究:交通的因果模型
在本章中,我们将探讨人们日常通勤选择交通方式的模型。这个例子将做出过于强烈的假设(以至于接近冒犯),这将有助于说明模型构建的核心思想。你可以在www.altdeep.ai/p/causalaibook找到相关的代码和教程链接。
假设你是一名城市规划顾问,试图模拟人们的人口背景、他们居住的城市规模、他们的工作状态以及他们每天如何通勤到工作的决策之间的关系。
你可以将系统中的关键变量分解如下:
-
年龄 (A)—个人的年龄
-
性别 (S)—个人报告的性别(使用“S”而不是“G”,因为“G”通常保留用于 DAG)
-
教育 (E)—个人完成的教育或培训的最高水平
-
职业 (O)—个人的职业
-
居住地 (R)—个人居住的城市规模
-
出行 (T)—个人偏好的交通方式
然后,你可以考虑这些变量之间的因果关系,使用对该领域的了解。以下是一个可能的叙述:
-
教育标准在不同代人之间是不同的。对于老年人来说,高中学位就足以实现中产阶级的生活方式,但年轻人至少需要大学学位才能达到同样的生活方式。因此,年龄 (A) 是教育 (E) 的原因。
-
类似地,一个人的性别通常是他们决定追求更高教育水平的一个因素。因此,性别 (S) 是教育 (E) 的原因。
-
许多白领工作需要较高的教育水平。许多认证职业(例如,医生、律师或会计师)肯定需要较高的教育水平。因此,教育 (E) 是职业 (O) 的原因。
-
依赖于较高教育水平的白领工作往往集中在城市地区。因此,教育 (E) 是人们居住地点 (R) 的原因。
-
自雇人士可能在家工作,因此不需要通勤,而雇佣他人的人则需要。因此,职业 (O) 是交通 (T) 的原因。
-
大城市的人可能发现步行或使用公共交通通勤更方便,而小城市和镇上的人则依赖汽车出行。因此,居住地 (R) 是交通 (T) 的原因。
你可以根据你对该领域的了解,或者基于对该领域的研究来构建这个叙述。或者,你也可以咨询该领域的专家,例如专门研究这个领域的社会科学家。最后,你可以将这个叙述简化为图 3.2 所示的因果 DAG。

图 3.2 代表人们通勤到工作背后的因果因素的因果 DAG
你可以使用以下代码构建这个因果 DAG。
列表 3.2 在 pgmpy 中构建运输 DAG
from pgmpy.models import BayesianNetwork
model = BayesianNetwork( #1
[
('A', 'E'), #2
('S', 'E'), #2
('E', 'O'), #2
('E', 'R'), #2
('O', 'T'), #2
('R', 'T') #2
]
)
1 pgmpy 提供了一个 BayesianNetwork 类,我们可以将边添加到模型中。
2 将ΔAG 作为边(元组)列表输入。
3 将ΔAG 作为边(元组)列表输入。
pgmpy 中的BayesianNetwork对象建立在 NetworkX 的DiGraph类之上,NetworkX 是 Python 中首屈一指的图建模库。
因果抽象和因果表示学习
在建模中,抽象级别指的是模型中变量的详细程度和粒度。在图 3.2 中,数据中的变量和因果 DAG 中的变量之间存在映射,因为 DGP 生成数据的抽象级别和因果 DAG 的抽象级别是相同的。但是,数据中的变量可能处于不同的抽象级别。这在机器学习中尤为常见,我们经常处理低级特征,如像素。
当数据的抽象级别低于建模者想要工作的级别时,建模者必须使用领域知识推导出将作为 DAG 节点出现的高级抽象。例如,医生可能对像“肿瘤(存在/不存在)”这样的高级二元变量节点感兴趣,而数据本身包含来自医学成像技术的像素矩阵等低级变量。
那位医生必须查看数据集中的每一张图像,并手动标记高级肿瘤变量。或者,建模者可以使用分析手段(例如数学或逻辑)将低级抽象映射到高级抽象。此外,他们必须以保留 DGP 因果假设的方式来做。
从低级变量以因果严谨的方式创建高级变量的任务被称为因果抽象。在机器学习中,术语“特征工程”适用于从低级特征计算有用的高级特征的任务。因果抽象与因果严谨性的要求不同。你可以在本书的注释中找到一些因果抽象的信息,网址为www.altdeep.ai/p/causalaibook。
从数据中的低级抽象学习高级因果抽象的另一种方法是使用深度学习——这被称为因果表示学习。我们将在第五章简要介绍这个主题。
3.1.2 为什么使用因果 DAG?
因果 DAG 是因果性最著名的表示,但要理解其价值,思考其他建模因果性的方法是有用的。一种替代方法是使用数学模型,例如一组常微分方程或偏微分方程,这在物理学和工程学中很常见。另一种选择是使用计算模拟器,如气象学和气候科学中使用的。
与那些替代方案相比,因果 DAG 对 DGP 的数学理解要求要少得多。因果 DAG 只要求你以图形的形式指定什么导致什么。图形对人类来说很容易思考;它们是理解复杂领域的方法。
事实上,使用因果 DAG 作为 DGP 表示的几个好处包括:
-
DAGs 在沟通和可视化因果假设方面很有用。
-
我们有许多工具可以用于在 DAG 上进行计算。
-
因果 DAG 表示时间。
-
DAGs 将因果关系与条件独立性联系起来。
-
DAGs 可以为概率机器学习模型提供支架。
-
那些概率机器学习模型中的参数是模块化参数,并且它们编码了因果不变性。
让我们逐一回顾这些好处。
3.1.3 DAGs 在沟通和可视化因果假设方面很有用
因果 DAG 是一种强大的沟通工具。信息视觉沟通涉及以牺牲其他信息为代价突出重要信息。作为一个类比,考虑图 3.3 中的两个伦敦地铁图。左边的地图在地理上是准确的。右边的简单地图忽略了地理细节,专注于每个车站相对于其他车站的位置,这可能是找到在伦敦周围路线所需的所有信息。

图 3.3 图形表示是视觉沟通的一个强大用例。例如,左边的伦敦地铁图在地理上是准确的,而右边的则为了更清晰地表示每个车站相对于其他车站的位置而牺牲了这种准确性。对于乘客来说,后者比地理准确性的地图更有用。同样,因果 DAG 抽象掉了因果机制的大部分细节,以创建一个简单且易于视觉推理的表示。
类似地,因果有向图(DAG)突出了因果关系,而忽略了其他事物。例如,投掷石头的 DAG 忽略了珍妮和布莱恩的投掷如何结合在一起打破窗户的 if-then 条件逻辑。运输 DAG 没有提及我们正在处理的变量类型。我们应该将年龄(A)视为连续时间、整数年、如年轻/中年/老年这样的类别,还是 18-29、30-44、45-64 和>65 这样的区间?运输变量(T)的分类是什么?职业变量(O)是否可以是如{就业、工程师、在家工作}这样的多维元组?DAG 还未能捕捉到这些变量中哪些在数据中被观察到,以及该数据中的数据点数量。
因果 DAG 没有说明机制
因果有向图也没有可视化原因之间的交互。例如,在上一代人中,女性上大学的机会比男性少。在年轻一代中,情况相反。虽然年龄(A)和性别(S)都是教育(E)的原因,但你无法从 DAG 中看到关于年龄和性别如何相互作用影响教育的内容。
更普遍地说,DAGs 无法传达关于因果机制或如何原因影响效果的任何信息。它们只建立了因果的什么,即什么导致什么。以图 3.4 中的各种逻辑门为例。A和B的输入二进制值根据逻辑门类型的不同而决定输出不同。但如果我们用一个因果有向图来表示逻辑门,那么所有逻辑门都有相同的因果有向图。我们可以使用因果有向图作为捕捉这种逻辑的因果图形模型的支架,但我们无法看到在图中逻辑。

图 3.4 各种逻辑门都有相同的因果有向图。
这既是优点也是缺点。因果有向图(DAG)通过说明什么导致什么来简化问题,但并不说明如何。然而,在某些情况下(例如逻辑门),可视化如何将是可取的。
因果有向图代表了因果假设
因果有向图代表了模型者对 DGP 的假设和信念,因为我们大多数时候无法访问那个过程。因此,因果有向图使我们能够可视化我们的假设并将它们传达给他人。
除了这种可视化和沟通之外,因果有向图的益处是数学的和计算的(我将在下一个小节中解释这些)。因果推断研究人员在因果有向图的数学和计算属性在实践中的益处程度上意见不一。然而,大多数人同意可视化和沟通因果假设的基本益处。
因果有向图中编码的假设是强烈的。让我们再次看看图 3.2 中的运输 DAG,再次在图 3.5 中展示。考虑那个 DAG 的替代方案;在这个简单的六个节点系统中,我们可能画出多少个可能的 DAG?答案是 3,781,503,所以当我们使用因果有向图来沟通我们对这个系统的假设时,我们是在沟通我们对 3,781,502 个替代方案中的首选。

图 3.5 运输选择的因果有向图模型。这个 DAG 编码了关于这些变量如何以及如何不相互关联的强烈假设。
那么关于那些竞争的 DAG 呢?其中一些看起来是合理的。也许婴儿潮一代更喜欢小镇生活,而千禧一代更喜欢城市生活,这意味着应该有一个A → R的边。也许性别规范决定了某些职业和行业中的偏好和机会,这意味着有一个S → O的边。年龄和性别通过教育间接导致职业和居住地的假设是一个强大的假设,如果它是正确的,将提供有用的推理。
但如果我们的因果 DAG 是错误的怎么办?考虑到它有 3,781,502 个竞争对手,它很可能是错误的。在第四章中,我们将使用数据来展示我们选择的 DAG 中的因果假设何时不成立。
3.1.4 我们有许多工具可以用于在 DAG 上进行计算
有向图在数学和计算机科学中是研究得很好的对象,它们是一个基本的数据结构。计算机科学家已经使用图算法解决了许多具有理论保证的实用问题,这些保证了他们找到解决方案所需的时间。在数据科学和机器学习中常用的编程语言都有实现这些算法的库,例如 Python 中的 NetworkX。这些流行的库使得编写与因果 DAG 一起工作的代码变得更加容易。
我们可以将所有这些理论和工具应用于因果建模问题,当我们以因果有向无环图(DAG)的形式表示因果模型时。例如,在 pgmpy 中,我们可以使用数据来训练一个因果 DAG,从而得到一个有向因果图模型。有了这个模型,我们可以应用基于图的概率推理算法,例如信念传播,来估计图上变量定义的条件概率。有向图结构使得这些算法能够在典型设置下工作,而无需我们针对特定问题或任务进行配置。
在下一章中,我将介绍d-separation的概念,它是条件独立性的图形抽象,也是因果推理的 do-calculus 理论的基本思想。D-separation 完全是关于在有向图中寻找节点之间的路径,这是任何有价值的图形库默认情况下都使其变得容易的事情。实际上,条件独立性是因果 DAG 的第三个主要益处的关键思想。
3.1.5 因果 DAG 可以表示时间
因果 DAG 隐含地表示了时间。更技术地说,因果 DAG 提供了一个部分时间顺序,因为原因在时间上先于效果。
例如,考虑图 3.6 中的图。这个图描述了一个 DGP,其中云量的变化(多云)导致天气激活的喷水装置(喷水装置)和雨(雨)的状态发生变化,这两者又导致草地湿润状态(湿草)的变化。我们知道天气状态的变化会导致下雨和喷水装置激活,这两者又导致草地湿润状态的变化。然而,这只是一个部分时间排序,因为图没有告诉我们哪个先发生:喷水装置激活还是下雨。

图 3.6 表示某些草(湿或干)状态的因果 DAG。DAG 给出了其节点的一个部分时间排序,因为原因在时间上先于效果。
图 3.6 中的部分排序可能看起来很平凡,但考虑图 3.7 中的 DAG。可视化库可以使用图 3.7 左侧毛线球状 DAG 中的部分排序来创建右侧的更易读形式。

图 3.7 一个可视化库可以使用 DAG 的部分排序将左侧的毛线球状 DAG 展开成右侧更易读的形式。
有时我们需要一个因果 DAG 来更明确地表示时间。例如,我们可能在动态环境中建模因果关系,如强化学习中使用的模型。在这种情况下,我们可以通过定义和标记模型的变量来使时间明确,如图 3.8 所示。我们可以用像“Δ”这样的区间变量表示连续时间。第十二章将提供一些具体例子。

图 3.8 如果我们需要一个因果 DAG 来明确表示时间,我们可以在变量的定义和节点标记中使时间明确。我们可以用像“Δ”这样的区间变量表示连续时间。
因果 DAG 不允许有任何循环。在一些因果系统中,放松无环性约束是有意义的,例如具有反馈循环的系统,一些高级因果模型允许循环。但坚持简单的无环假设使我们能够利用因果 DAG 的好处。
如果你有循环,有时你可以通过时间展开循环并使时间明确来得到无环性。一个图X ⇄ Y可以展开为X[0]→Y[0] →X[1]→Y[1] . . . 。例如,你可能存在供应、价格和需求之间的循环,但也许你可以将其重写为时间 0 的价格影响时间 1 的供应和需求,然后这些又影响时间 2 的价格,等等。
3.1.6 DAGs 将因果关系与条件独立性联系起来
因果 DAG 的另一个好处是它允许我们利用因果关系来推理条件独立性。人类天生具有基于因果关系的推理能力——这就是因果 DAG 的第一个和第二个好处。但是,概率推理并不那么容易。因此,利用因果关系来推理条件独立性(一个来自概率的概念)是 DAG 的一个显著特征。

图 3.9 运输 DAG 中的因果关系编码了关于条件独立性的关键假设。
考虑运输 DAG,如图 3.9 所示。
DAG 中的六个变量具有联合分布 P(A,S,E,O,R,T)。回忆第二章中的链式法则,它说我们可以将任何联合概率分解为一系列条件概率因子。例如,

链式法则适用于任何变量的顺序。但我们将选择因果 DAG 的(部分)顺序,因为这种顺序与我们对 DGP 中变量因果流动的假设相一致。查看图 3.9,变量的顺序是 {(A, S), E, (O, R), T}。(A, S) 和 (O, R) 这对是无序的。如果我们任意选择一个顺序,让 A 在 S 之前,O 在 R 之前,我们得到以下结果:

接下来,我们将使用因果 DAG 进一步简化这个分解。每个因子都是一个条件概率,因此我们将通过在 DAG 中仅对每个节点进行其父节点的条件化来简化这些因子。换句话说,对于每个变量,我们将查看该变量在图中的直接父节点,然后我们将丢弃条件化符号(|)右侧的所有不是直接父节点的元素。如果我们只对父节点进行条件化,我们得到以下简化:

这里发生了什么?为什么因果 DAG 神奇地意味着我们可以说 P(s|a) 等于 P(s),并且 P(r|o,e,s,a) 简化为 P(r**|e)?如第二章所述,断言 P(s|a)=**P(s) 和 P(t|o,r,e,s,a)=**P(t|o,r) 等价于说 S 和 A 是独立的,且 T 在给定 O 和 R 的条件下独立于 E、S 和 A。换句话说,因果 DAG 为我们提供了一种在 DGP 中变量的联合概率分布上施加条件独立性约束的方法。
我们为什么要关心事物之间的条件独立性呢?条件独立性使得作为模型构建者的人生更加容易。例如,假设你想要用预测模型来建模运输变量T。由P(t|o,r,e,s,a)隐含的预测模型需要具有特征O、R、E、S和A,而由P(t|o,r)隐含的预测模型只需要特征O和R来预测T*。后者模型将具有更少的参数需要学习,具有更多的自由度,在内存中占用的空间更少,训练速度更快等。
但为什么因果有向图(DAG)让我们有权施加条件独立性呢?让我们对因果关系和条件独立性之间的联系建立一些直觉。考虑使用家庭成员的遗传数据来得出关于个人的结论的例子。例如,金州杀手是一个在加利福尼亚州被捕的连环杀手,他使用遗传家谱学被捕获。调查人员使用杀手在犯罪现场留下的 DNA 来识别公共数据库中的遗传亲属。然后,他们从那些亲属那里进行三角测量以找到杀手。
假设你有一个近亲和一个远亲在同一条血统线上。在我们已经考虑了那个近亲之后,远亲还能提供关于你的任何额外的遗传信息吗?让我们通过只关注血型来简化一下。假设近亲是你的父亲,而远亲是你的父亲的父亲,如图 3.10 所示。确实,你祖父的血型是你血型的原因。如果我们看到一组祖父/孙子血型对的大数据集,我们会看到相关性。然而,你父亲的血型是一个更直接的原因,你祖父的血型和你之间的联系是通过你的父亲传递的。所以,如果我们目标是预测你的血型,而我们已经有你父亲的血型作为预测因子,你父亲的父亲的血型就不能提供任何额外的预测信息。因此,你的血型和你的父亲的父亲的血型在给定你父亲的血型的情况下是条件独立的。

图 3.10 因果关系意味着条件独立性。你父亲的血型是祖父的血型的原因,而祖父的血型又是你血型的原因。给定你父亲的血型,你和你的父亲的父亲的血型在条件上是独立的,因为你的父亲的血型已经包含了祖父的血型关于你血型可能提供的一切信息。
因果关系使相关变量条件独立的方式被称为因果马尔可夫性质。在图形术语中,因果马尔可夫性质意味着在图中,变量在给定其父母的情况下,与其非后代(例如,祖先、叔叔/阿姨、堂兄弟姐妹等)条件独立。
这种“非后裔”定义的因果马尔可夫属性有时被称为局部马尔可夫属性。一个等效的表述被称为马尔可夫分解属性,这是如果你的因果 DAG 是真实的,你可以将联合概率分解为变量在因果 DAG 中给定其父变量的条件概率的性质:

如果我们的运输 DAG 是 DGP 的真实表示,那么局部马尔可夫属性应该成立。在下一章中,我们将看到如何使用数据来测试这个假设。
3.1.7 DAGs 可以为概率 ML 模型提供脚手架
在概率机器学习中,许多建模方法使用有向无环图(DAG)作为模型结构。例如,包括有向图模型(也称为贝叶斯网络)和潜在变量模型(例如,主题模型)。深度生成模型,如变分自编码器,通常有一个底层的有向图。
在因果图上构建概率机器学习模型的优势,显然是,你有一个概率因果机器学习模型。你可以在数据上训练它,你可以用它进行预测和其他推理,就像任何概率机器学习模型一样。此外,因为它建立在因果 DAG 之上,所以它是一个因果模型,你可以用它来进行因果推理。
从提供脚手架中获得的益处是,这些模型中的参数是模块化的,并编码了因果不变性。在探讨这一益处之前,让我们首先在运输 DAG 上构建一个图形模型。
Building a probabilistic machine learning model on a causal DAG
回想一下我们在运输 DAG 中关于变量排序的联合概率分布的分解。

我们有一组因子,{P(a), P(s), P(e|s,a), P(o|e), P(r|e), P(t|o,r)}。从现在起,我们将从第二章中的“马尔可夫核”术语出发,并称这些因子为因果马尔可夫核*。
我们将通过在代码中实现这些因果马尔可夫核并将它们组合成一个模型来构建我们的概率机器学习模型。我们为每个核的实现将能够返回一个概率值,给定输入参数。例如,P(a)将接受一个关于A的结果值并返回该结果的概率值。同样,P(t|o,r)将接受T,O和R的值并返回T=t的概率值,其中t是查询值。我们的实现还将能够从因果马尔可夫核中生成。为此,这些实现将需要将输入映射到输出的参数。我们将使用标准的统计学习方法来从数据中拟合这些参数。
3.1.8 在因果 DAG 上训练模型
考虑运输 DAG 的 DGP。这个过程会生成什么样的数据?
假设我们进行了一项涵盖 500 个人的调查,得到了这个 DAG 中每个变量的值。数据按照以下方式编码我们 DAG 中的变量:
-
年龄 (A)—记录为年轻(“年轻”)对于 29 岁及以下的人,成年(“成年”)对于 30 至 60 岁的人(包括 60 岁),以及 61 岁及以上的人为老年(“老年”)
-
性别 (S)—个人的自我报告性别,记录为男性(“M”)、女性(“F”)或其他(“O”)
-
教育 (E)—个人完成的教育或培训的最高水平,记录为高中(“高中”)或大学学位(“大学”)
-
职业 (O)—雇员(“雇员”)或自雇工人(“自雇”)
-
居住地 (R)—个人居住的城市的人口规模,记录为小(“小”)或大(“大”)
-
旅行 (T)—个人偏好的交通方式,记录为汽车(“汽车”)、火车(“火车”)或其他(“其他”)
标记因果抽象
在机器学习中,我们如何概念化模型中的变量非常重要。例如,ImageNet,一个包含 1400 万张图片的数据库,包含种族类别的时代错误和冒犯性标签。即使更名为不那么冒犯,种族类别本身在时间和文化中是流动的。在预测算法中应该使用哪些“正确”的标签?
我们如何定义我们的变量不仅仅是政治和人口普查表的问题。哲学家纳尔逊·古德曼的一个简单的思想实验展示了简单的标签变化如何将预测变为矛盾的预测。假设你经常寻找宝石并记录你找到的每一颗宝石的颜色。结果发现你数据集中的 100%的宝石都是绿色的。现在让我们定义一个新的标签“grue”,表示“如果在此之前观察到,则为绿色,否则为蓝色。”所以你的 100%的数据是“绿色”或“grue”,这取决于你的标签选择。现在假设你根据过去的数据进行预测。那么你可以根据所有过去翡翠都是绿色的数据预测下一颗翡翠将是绿色的,或者你可以根据所有过去翡翠都是“grue”(即蓝色)的数据预测下一颗翡翠将是“grue”。显然,你永远不会发明这样的荒谬标签,但这个思想实验足以表明推理取决于抽象。
在数据科学和机器学习中,我们经常被鼓励盲目地建模数据,不要考虑 DGP。我们被鼓励将变量名称视为电子表格中的列或数据库表中的属性。当可能时,最好选择适合推理问题的抽象,并根据该抽象收集或编码数据。当不可能时,请记住,你的分析结果将取决于其他人如何定义变量。
在第七章中,我将介绍“无操纵则无因果关系”这一概念——这一概念为如何定义因果变量提供了一个有用的启发式方法。
运输数据中的变量都是 分类 变量。在这个简单的分类情况下,我们可以依赖像 pgmpy. 这样的图形建模库。
列表 3.3 加载运输数据
import pandas as pd
url='https://raw.githubusercontent.com/altdeep/causalML/master/datasets
↪/transportation_survey.csv' #1
data = pd.read_csv(url)
data
1 我们将使用 read_csv 方法将数据加载到 pandas ΔataFrame 中。
这产生了图 3.11 中的 DataFrame。

图 3.11 运输模型下 DGP 数据的一个示例。在这种情况下,数据是 500 份调查回复。
我们在列表 3.2 中初始化的 BayesianNetwork 类有一个 fit 方法,它将学习我们的因果马尔可夫核的参数。由于我们的变量是分类的,我们的因果马尔可夫核将以 pgmpy 的 TabularCPD 类表示的条件概率表的形式存在。fit 方法将使用数据来拟合(学习)那些条件概率表的参数估计。
列表 3.4 为因果马尔可夫核学习参数
from pgmpy.models import BayesianNetwork
model = BayesianNetwork(
[
('A', 'E'),
('S', 'E'),
('E', 'O'),
('E', 'R'),
('O', 'T'),
('R', 'T')
]
)
model.fit(data) #1
causal_markov_kernels = model.get_cpds() #2
print(causal_markov_kernels) #2
1 BayesianNetwork 对象上的 fit 方法将从数据(pandas ΔataFrame)中估计参数。
2 检索并查看通过 fit 学习到的因果马尔可夫核。
这返回以下输出:
[<TabularCPD representing P(A:3) at 0x7fb030dd1050>,
<TabularCPD representing P(E:2 | A:3, S:2) at 0x7fb0318121d0>,
<TabularCPD representing P(S:2) at 0x7fb03189fe90>,
<TabularCPD representing P(O:2 | E:2) at 0x7fb030de85d0>,
<TabularCPD representing P(R:2 | E:2) at 0x7fb030dfa890>,
<TabularCPD representing P(T:3 | O:2, R:2) at 0x7fb0316c9110>]
让我们看看运输变量 T 的因果马尔可夫核的结构。我们可以从打印 causal_markov_kernels 列表看到,T 是列表中的最后一个项目。
cmk_T = causal_markov_kernels[-1]
print(cmk_T)
我们得到以下输出:
+----------+---------+----------+---------+----------+
| O | O(emp) | O(emp) | O(self) | O(self) |
+----------+---------+----------+---------+----------+
| R | R(big) | R(small) | R(big) | R(small) |
+----------+---------+----------+---------+----------+
| T(car) | 0.70343 | 0.52439 | 0.44444 | 1.0 |
+----------+---------+----------+---------+--------- +
| T(other) | 0.13480 | 0.08536 | 0.33333 | 0.0 |
+----------+---------+----------+---------+----------+
| T(train) | 0.16176 | 0.39024 | 0.22222 | 0.0 |
+----------+---------+----------+---------+----------+
注意,在这个打印输出中,我截断了数字,以便表格能适应页面。
cmk_T 是因果马尔可夫核 P(T**|O,R) 作为条件概率表(一种查找表)的实现,给定 T,O 和 R 的值,我们得到相应的概率质量值。例如,P(T=car|O=emp, R=big) = 0.7034。注意,这些都是条件概率。对于 O 和 R 的每一组值,都有三个 T 的结果的条件概率,它们的和为 1。例如,当 O=emp 和 R=big 时,P(T=car| O=emp, R=big) + (P(T=other| O=emp, R=big) + P(T=train| O=emp, R=big) = 1。
对于没有父节点的节点,因果马尔可夫核只是一个简单的概率表。例如,print(causal_markov_kernels[2]) 打印了 causal_markov_kernels 列表中的第三个项目,即性别 (S) 的因果马尔可夫核。
+------+-------+
| S(F) | 0.517 |
+------+-------+
| S(M) | 0.473 |
+------+-------+
| S(O) | 0.010 |
+------+-------+
fit 方法通过计算数据中每个类的比例来学习参数。或者,我们也可以使用其他参数学习技术。
3.1.9 参数学习的不同技术
我们有几种方法可以用来训练这些参数。让我们看看在条件概率表中训练参数的几种常见方法。
最大似然估计
我在BayesianNetwork模型对象上的fit方法中使用的学习算法是最大似然估计(在第二章中讨论)。它是默认的参数学习方法,因此我在调用fit时没有指定“最大似然”。一般来说,最大似然估计寻求最大化我们用于训练模型的观测数据的似然度的参数。在分类数据的上下文中,最大似然估计等同于取数据中计数的比例。例如,P(O=emp|E=high)的参数计算如下:

贝叶斯估计
在第二章中,我也介绍了贝叶斯估计。它通常在数学上难以处理,并且依赖于计算成本高昂的算法(例如,采样算法和变分推断)。一个关键例外是共轭先验的情况,其中先验分布和目标(后验)分布具有相同的规范形式。这意味着代码实现只需用简单的数学计算目标分布的参数值,无需复杂的贝叶斯推断算法。
例如,pgmpy 实现了用于分类结果的Dirichlet 共轭先验。对于P(O|E=high)中的每个O值,我们都有一个概率值,我们希望从数据中推断这些概率值。贝叶斯方法为这些值分配一个先验分布。对于概率值集合的先验,一个好的选择是Dirichlet 分布,因为它定义在单纯形上,即介于零和一之间的数字之和为 1。此外,它对分类分布(如P(O|E=high))是共轭的,这意味着参数值的后验分布也是一个 Dirichlet 分布。这意味着我们可以使用简单的数学计算概率值的点估计,结合数据中的计数和先验中的参数。pgmpy 为我们做这个数学计算。
列表 3.5 使用 Dirichlet 共轭先验的贝叶斯点估计
from pgmpy.estimators import BayesianEstimator #1
model.fit(
data,
estimator=BayesianEstimator, #2
prior_type="dirichlet",
pseudo_counts=1 #3
)
causal_markov_kernels = model.get_cpds() #4
cmk_T = causal_markov_kernels[-1] #4
print(cmk_T) #4
1 导入 BayesianEstimator 并在模型和数据上初始化它。
2 将估计器对象传递给 fit 方法。
3 pseudo_counts 指的是Δirichlet 先验的参数。
4 提取因果马尔可夫核并查看 P(T|O,R)。
上述代码打印以下输出:
+----------+--------------------+-----+--------------------+----------+
| O | O(emp) | ... | O(self) | O(self) |
+----------+--------------------+-----+--------------------+----------+
| R | R(big) | ... | R(big) | R(small) |
+----------+--------------------+-----+--------------------+----------+
| T(car) | 0.7007299270072993 | ... | 0.4166666666666667 | 0.5 |
+----------+--------------------+-----+--------------------+----------+
| T(other) | 0.1362530413625304 | ... | 0.3333333333333333 | 0.25 |
+----------+--------------------+-----+--------------------+----------+
| T(train) | 0.1630170316301703 | ... | 0.25 | 0.25 |
+----------+--------------------+-----+--------------------+----------+
与最大似然估计相比,具有 Dirichlet 先验的分类参数的贝叶斯估计作用像一个平滑机制。例如,最大似然参数估计表明,在小镇上的自雇人士中有 100%的人开车上班。这可能是极端的。当然,一些自雇人士骑自行车上班——我们只是没有设法调查他们中的任何一个人。一些小城市,如美国弗吉尼亚州的 Crystal City(人口 22,000),有地铁站。我敢打赌,那些城市中的至少几位企业家使用火车。
因果模型构建者和贝叶斯主义者
贝叶斯哲学超越了仅仅参数估计的范围。事实上,贝叶斯哲学与基于 DAG 的因果建模有很多共同之处。贝叶斯主义者试图将主观信念、不确定性和先验知识编码到模型中变量的“先验”概率分布上。因果模型者试图将关于 DGP(数据生成过程)的主观信念和先验知识编码成因果 DAG 的形式。这两种方法是可以兼容的。给定一个因果 DAG,你可以对在因果 DAG 之上构建的概率模型进行贝叶斯推断。你甚至可以对 DAG 本身进行贝叶斯推断,并计算可能 DAG 的概率分布!
我在这本书中专注于因果性,并将贝叶斯讨论保持在最低限度。但我们将使用 Pyro 库(及其 NumPy-JAX 替代库 NumPyro)来实现因果模型;这些库为模型以及参数提供了完整的贝叶斯推断支持。在第十一章中,我们将查看一个使用我们从零开始构建的因果图模型进行因果效应贝叶斯推断的例子。
其他参数估计技术
我们不需要使用条件概率表来表示因果马尔可夫核。在广义线性建模框架内存在用于建模分类结果的模型。对于运输模型中的某些变量,我们可能使用了非分类结果。例如,年龄可能在调查中被记录为整数结果。对于具有数值结果的变量,我们可能使用其他建模方法。您还可以使用神经网络架构来建模个体因果马尔可夫核。
参数假设指的是我们如何指定 DAG(有向无环图)中一个节点的结果(例如,类别或实数)以及我们如何将父节点映射到结果(例如,表格或神经网络)。请注意,因果 DAG 中编码的因果假设与因果马尔可夫核的参数假设是解耦的。例如,当我们假设年龄是教育水平的直接原因并将其编码到我们的 DAG 中作为一条边时,我们不必决定是否将年龄视为有序的类别集合、整数或自出生以来经过的秒数等。此外,我们也不必知道是否要使用条件分类分布或回归模型。这一步骤是在我们指定因果 DAG 并想要实现 P(E|A, S) 之后进行的。
类似地,当我们对一个训练好的因果模型进行预测和概率推理时,关于使用哪种推理或预测算法的考虑,虽然很重要,但与我们的因果问题相分离。这种分离简化了我们的工作。通常,我们可以在因果建模和推理方面独立于我们对统计学、计算贝叶斯和应用的机器学习的知识来构建我们的知识和技能集。
3.1.10 存在潜在变量时的参数学习
由于我们正在建模 DGP 而不是数据,因此因果 DAG 中的某些节点可能不会在数据中观察到。幸运的是,概率机器学习为我们提供了学习潜在变量因果马尔可夫核参数的工具。
使用 pgmpy 学习潜在变量
为了说明,假设交通调查数据中的教育变量没有被记录。pgmpy 通过使用一种称为结构化期望最大化的算法,为我们提供了一个学习潜在 E 的因果马尔可夫核参数的实用工具,这是一种最大似然参数学习的一个变体。
列表 3.6 使用潜在变量训练因果图模型
import pandas as pd
from pgmpy.models import BayesianNetwork
from pgmpy.estimators import ExpectationMaximization as EM
url='https://raw.githubusercontent.com/altdeep/causalML/master/datasets
↪/transportation_survey.csv' #1
data = pd.read_csv(url) #1
data_sans_E = data[['A', 'S', 'O', 'R', 'T']] #2
model_with_latent = BayesianNetwork(
[
('A', 'E'),
('S', 'E'),
('E', 'O'),
('E', 'R'),
('O', 'T'),
('R', 'T')
],
latents={"E"} #3
)
estimator = EM(model_with_latent, data_sans_E) #4
cmks_with_latent = estimator.get_parameters(latent_card={'E': 2}) #4
print(cmks_with_latent[1].to_factor()) #5
1 下载数据并将其转换为 pandas DataFrame。
2 保留除教育(E)之外的所有列。
3 训练模型时指出哪些变量是潜在的。
4 运行结构化期望最大化算法来学习 E 的因果马尔可夫核。你必须指出潜在变量的基数。
5 打印出学习到的 E 的因果马尔可夫核。为了可读性,将其打印为因子对象。
print 行打印一个因子对象。
+------+----------+------+--------------+
| E | A | S | phi(E,A,S) |
+======+==========+======+==============+
| E(0) | A(adult) | S(F) | 0.1059 |
+------+----------+------+--------------+
| E(0) | A(adult) | S(M) | 0.1124 |
+------+----------+------+--------------+
| E(0) | A(old) | S(F) | 0.4033 |
+------+----------+------+--------------+
| E(0) | A(old) | S(M) | 0.2386 |
+------+----------+------+--------------+
| E(0) | A(young) | S(F) | 0.4533 |
+------+----------+------+--------------+
| E(0) | A(young) | S(M) | 0.6080 |
+------+----------+------+--------------+
| E(1) | A(adult) | S(F) | 0.8941 |
+------+----------+------+--------------+
| E(1) | A(adult) | S(M) | 0.8876 |
+------+----------+------+--------------+
| E(1) | A(old) | S(F) | 0.5967 |
+------+----------+------+--------------+
| E(1) | A(old) | S(M) | 0.7614 |
+------+----------+------+--------------+
| E(1) | A(young) | S(F) | 0.5467 |
+------+----------+------+--------------+
| E(1) | A(young) | S(M) | 0.3920 |
+------+----------+------+--------------+
E 的结果为 0 和 1,因为算法不知道结果名称。也许 0 是“高”(高中)而 1 是“uni”(大学),但将默认结果从潜在变量估计方法映射到这些结果名称需要进一步的假设。
当存在潜在变量时,还有其他算法用于学习参数,包括一些使用特殊参数假设(即关于潜在变量如何与观测变量相关的函数假设)的算法。
潜在变量和识别
在统计推断中,我们说一个参数是“可识别的”,当理论上在给定数据中的无限多个示例的情况下,可以学习到它的真实值。如果更多的数据不能让你更接近学习它的真实值,那么它是“不可识别的”。不幸的是,你的数据可能不足以学习你的因果 DAG 中潜在变量的因果马尔可夫核。如果我们不关心表示因果关系,我们可以限制自己使用可从数据中识别的潜在变量的潜在变量图模型。但我们必须构建一个表示 DGP 的因果 DAG,即使我们无法根据数据识别潜在变量和参数。
话虽如此,即使你在你的因果模型中有不可识别的参数,你仍然可能能够识别出回答你因果问题的数量。实际上,因果推断方法的大部分内容都集中在稳健地估计因果效应(一个原因对效应的影响程度)上,即使存在潜在的“混杂因素”。我们将在第十一章中详细讨论这一点。另一方面,即使你的参数被识别,回答你因果问题的数量可能没有被识别。我们将在第十章中详细讨论因果识别。
3.1.11 使用训练好的因果概率机器学习模型进行推理
一组变量的概率机器学习模型可以使用计算推理算法来推断任何变量子集的条件的概率,给定其他变量的结果。我们使用变量消除算法来处理具有分类结果的定向图形模型(在第二章中介绍)。
例如,假设我们想要比较汽车驾驶员和火车乘客的教育水平。我们可以通过变量消除,一个表格图形模型的推理算法,来计算并比较当 T=car 和当 T=train 时的 P(E|T)。
列表 3.7 在训练好的因果图形模型上进行推理
from pgmpy.inference import VariableElimination #1
inference = VariableElimination(model)
query1 = inference.query(['E'], evidence={"T": "train"})
query2 = inference.query(['E'], evidence={"T": "car"})
print("train")
print(query1)
print("car")
print(query2)
1 VariableElimination 是一种针对图形模型的特定推理算法。
这将打印出“train”和“car”的概率表。
"train"
+---------+----------+
| E | phi(E) |
+=========+==========+
| E(high) | 0.6162 |
+---------+----------+
| E(uni) | 0.3838 |
+---------+----------+
"car"
+---------+----------+
| E | phi(E) |
+=========+==========+
| E(high) | 0.5586 |
+---------+----------+
| E(uni) | 0.4414 |
+---------+----------+
看起来汽车驾驶员比火车乘客更有可能拥有大学教育:(P(E='uni'|T='car') > P(E='uni'|T='train')。这个推断基于我们的基于 DAG 的因果假设,即大学教育间接决定了人们如何去工作。
在 Pyro 这样的工具中,您需要对推理算法进行更多手动操作。以下列表说明了使用名为重要性采样的概率推理算法推断 P(E|T="train") 的过程。首先,我们将指定模型。我们不会拟合参数,而是明确指定我们使用 pgmpy 拟合的参数值。
列表 3.8 在 Pyro 中实现训练好的因果模型
import torch
import pyro
from pyro.distributions import Categorical
A_alias = ['young', 'adult', 'old'] #1
S_alias = ['M', 'F'] #1
E_alias = ['high', 'uni'] #1
O_alias = ['emp', 'self'] #1
R_alias = ['small', 'big'] #1
T_alias = ['car', 'train', 'other'] #1
A_prob = torch.tensor([0.3,0.5,0.2]) #2
S_prob = torch.tensor([0.6,0.4]) #2
E_prob = torch.tensor([[[0.75,0.25], [0.72,0.28], [0.88,0.12]], #2
[[0.64,0.36], [0.7,0.3], [0.9,0.1]]]) #2
O_prob = torch.tensor([[0.96,0.04], [0.92,0.08]]) #2
R_prob = torch.tensor([[0.25,0.75], [0.2,0.8]]) #2
T_prob = torch.tensor([[[0.48,0.42,0.1], [0.56,0.36,0.08]], #2
[[0.58,0.24,0.18], [0.7,0.21,0.09]]]) #2
def model(): #3
A = pyro.sample('age', Categorical(probs=A_prob)) #3
S = pyro.sample('gender', Categorical(probs=S_prob)) #3
E = pyro.sample('education', Categorical(probs=E_prob[S][A])) #3
O = pyro.sample('occupation', Categorical(probs=O_prob[E])) #3
R = pyro.sample('residence', Categorical(probs=R_prob[E])) #3
T = pyro.sample('transportation', Categorical(probs=T_prob[R][O])) #3
Return {'A': A, 'S': S, 'E': E, 'O': O, 'R': R, 'T': T} #3
pyro.render_model(model) #4
1 分类分布只返回整数,因此将整数映射到分类结果名称是有用的。
2 为了简单起见,我们将使用 pgmpy 中 fit 方法学习到的参数的舍入版本(列表 3.4),尽管我们可以在训练过程中学习参数。
3 当我们在 Pyro 中实现模型时,我们通过代码逻辑隐式地指定了因果 ΔAG。
4 然后,我们可以使用 pyro.render_model() 生成隐含 ΔAG 的图。请注意,我们需要安装 Graphviz。
pyro.render_model 函数从 Pyro 模型中绘制了图 3.12 所示的隐含因果 DAG。

图 3.12 您可以使用 pyro.render_model() 函数在 Pyro 中可视化因果 DAG。这假设您已安装 Graphviz。
Pyro 提供了概率推理算法,例如重要性采样,我们可以将其应用于我们的因果模型。
列表 3.9 在 Pyro 中的因果模型上进行推理
import numpy as np
import pyro
from pyro.distributions import Categorical
from pyro.infer import Importance, EmpiricalMarginal #1
import matplotlib.pyplot as plt
conditioned_model = pyro.condition( #2
model, #3
data={'transportation':torch.tensor(1.)} #3
)
m = 5000 #4
posterior = pyro.infer.Importance( #5
conditioned_model, #5
num_samples=m
).run() #6
E_marginal = EmpiricalMarginal(posterior, "education") #7
E_samples = [E_marginal().item() for _ in range(m)] #7
E_unique, E_counts = np.unique(E_samples, return_counts=True) #8
E_probs = E_counts / m #8
plt.bar(E_unique, E_probs, align='center', alpha=0.5) #9
plt.xticks(E_unique, E_alias) #9
plt.ylabel('probability') #9
plt.xlabel('education') #9
plt.title('P(E | T = "train") - Importance Sampling') #9
1 我们将使用两个与推理相关的类,即 Importance 和 EmpiricalMarginal。
2 pyro.condition 是对模型的一种条件操作。
3 它接受模型和用于条件化的证据。证据是一个将变量名称映射到值的字典。在推理过程中需要指定变量名称,这就是为什么我们在 pyro.sample 的调用中有了名称参数。这里我们条件化 T="train"。
4 我们将运行一个推理算法,该算法将生成 m 个样本。
5 我使用一个名为重要性采样的推理算法。Importance 类构建了这个推理算法。它接受条件化模型和样本数量。
6 使用 run 方法运行随机过程算法。推理算法将从我们未条件化的变量的联合概率(除了 T 以外的所有变量)生成,给定我们条件化的变量(T)。
7 我们对教育条件的概率分布感兴趣,因此我们从后验中提取教育值。
8 基于这些样本,我们对 P(E|T=“train”)中的概率进行蒙特卡洛估计。
9 绘制学习概率的可视化。
这产生了图 3.13 中的图表。显示的概率接近于 pgmpy 模型的结果,尽管由于不同的算法和参数估计四舍五入到两位小数,它们略有不同。
这种概率推理还不是因果推理——我们将在第七章开始查看结合因果推理和概率推理的示例。在第八章中,你将看到如何使用概率推理来实现因果推理。现在,我们将探讨参数模块化的好处,以及参数如何编码因果不变性。

图 3.13 P(E|T=“train”)分布的可视化
3.2 因果不变性和参数模块化
假设我们感兴趣于建模海拔和温度之间的关系。这两者显然是相关的;你爬得越高,天气就越冷。然而,你知道温度不会导致海拔,或者加热城市内的空气不会使城市飞起。海拔是原因,温度是结果。
我们可以提出一个简单的因果 DAG,我们认为它捕捉了温度和海拔之间的关系,以及其他原因,如图 3.14 所示。让我们让A代表海拔,C代表云量,L代表纬度,S代表季节,T代表温度。图 3.14 中的 DAG 有五个因果马尔可夫核:{P(A), P(C), P(L), P(S), P(T|A, C, L, S)}。

图 3.14 室外温度的简单模型
要在 DAG 之上训练因果图模型,我们需要学习每个因果马尔可夫核的参数。
3.2.1 机制独立性和参数模块化
在我们温度有向无环图(DAG)中因果马尔可夫核的 DGP 中存在一些基本的动力学机制。例如,因果马尔可夫核P(T|A, C, L, S)是由基于物理机制的物理条件概率诱导的,其中海拔、云层覆盖、纬度和季节驱动温度。这种机制与确定云层覆盖的机制(根据我们的 DAG)是不同的。"机制独立性"指的是这种机制之间的区别。
机制的独立性导致了一种称为参数模块化的特性。在我们的模型中,对于每个因果马尔可夫核,我们选择因果马尔可夫核的参数化表示。如果P(T|A, C, L, S)和P(C)是不同的机制,那么我们对P(T|A, C, L, S)和P(C)的表示是不同机制的表示。这意味着我们可以改变一个表示,而不用担心这种改变如何影响其他表示。这种模块化在统计模型中是不常见的;你通常不能改变模型的一部分并期望另一部分不受影响。
这种独立性在训练过程中很有用。通常,当你训练一个模型时,你同时优化所有参数。参数模块化意味着你可以分别训练每个因果马尔可夫核的参数,或者将它们作为解耦集同时训练,这允许你在训练过程中享受一些维度降低。在贝叶斯术语中,参数集是先验独立的(尽管它们在后验中通常是相关的)。这为使用每个因果马尔可夫核参数集的独立先验分布提供了一个很好的因果理由。
3.2.2 因果迁移学习、数据融合和不变预测
你可能不是气候学家或气象学家。然而,你知道温度和海拔之间的关系与气压、气候、阳光等因素有关。你也知道,无论这种关系的物理性质是什么,在卡塔曼杜和埃尔帕索的物理性质是相同的。因此,当我们仅在卡塔曼杜收集的数据上训练因果马尔可夫核时,我们学习到的机制表示在卡塔曼杜和埃尔帕索之间是不变的。这种不变性有助于迁移学习;我们应该能够使用训练好的因果马尔可夫核来对埃尔帕索的温度进行推理。
当然,利用这种因果不变性的概念也有一些注意事项。例如,这假设你的因果模型是正确的,并且关于卡塔曼杜数据中潜在机制的足够信息可以有效地应用你在埃尔帕索学到的关于该机制的知识。
几种高级方法在很大程度上依赖于因果不变性和机制独立性。例如,因果数据融合 通过结合多个数据集来学习因果模型。因果迁移学习 使用因果不变性来使用训练数据域之外的数据进行因果推断。因果不变性预测 在预测任务中利用因果不变性。有关参考文献,请参阅www.altdeep.ai/p/causalaibook章节注释。
3.2.3 使用常识拟合参数
在温度模型中,我们对引起 P(T|A, C, L, S) 的机制有物理直觉。在非自然科学领域,如计量经济学和其他社会科学中,系统的“物理”更抽象,更难描述。幸运的是,我们可以在这些非自然科学领域依赖类似的基于不变性的直觉。在这些领域,我们仍然可以假设因果马尔可夫核对应于现实世界中的不同因果机制,假设模型是真实的。例如,回忆一下我们交通模型中的 P(T|O, R)。我们仍然假设潜在机制与其他机制不同;如果 P(T|O, R) 的潜在机制发生变化,只有 P(T|O, R) 应该改变——模型中的其他核不应该改变。如果某种东西改变了 P(R|E)(R 的因果马尔可夫核)的潜在机制,这种变化应该影响 P(R|E) 但不应影响 P(T|O, R) 的参数。
这种不变性可以帮助我们通过推理潜在的因果机制来估计参数,而无需进行统计学习。例如,让我们再次看看因果马尔可夫核 P(R|E)(回忆 R 是居住地,E 是教育)。让我们尝试通过推理来估计这个分布的参数,而不使用统计学习。
没有获得高中以上学位的人更有可能留在他们的家乡。然而,那些来自小镇并取得大学学位的人可能会搬到大城市,在那里他们可以应用他们的资历来获得更高薪的工作。
现在让我们来思考一下美国的 demographics。假设一个网络搜索告诉你,80% 的美国人口居住在城市地区(P(R=big) = .8),而 95% 的拥有大学学位的人居住在城市地区(P(R=big|E=uni) = .95)。此外,美国总体成年人口中有 25% 拥有大学学位(P(E=uni) = .25)。然后,通过一些简单的估算数学,你计算出你的概率值为 P(R=small|E=high)=.25,P(R=big|E=high) = .75,P(R=small|E=uni) = .05,和 P(R=big|E=uni) = .95。以这种方式计算参数的能力在数据不可用于参数学习时尤其有用。
3.3 你的因果问题定义了 DAG
当模型师第一次遇到问题时,通常已经有一组可用的数据,一个常见的错误是仅使用该数据中的变量来定义你的 DAG。让数据范围决定你的 DAG 是有吸引力的,因为你不必决定在 DAG 中包含哪些变量。但因果模型师建模的是 DGP,而不是数据。世界中的真实因果结构并不关心你的数据集中发生了什么。在你的因果 DAG 中,你应该包括与因果相关的变量,无论它们是否在你的数据集中。
但如果数据没有定义 DAG 的范围,那是什么定义的呢?虽然你的数据有一组固定的变量,但可能构成你的 DGP 的变量只受你的想象力所限。给定一个变量,你可以包括它的原因,这些原因的原因,这些原因的原因的原因,一直追溯到亚里士多德的“第一推动者”,即一切事物的单一原因。幸运的是,我们不需要追溯到那么远。让我们看看你可以用来选择在因果 DAG 中包含的变量的一个程序。
3.3.1 选择要包含在 DAG 中的变量
回想一下,存在几种类型的因果推断问题。正如我在第一章中提到的,因果效应推断是最常见的因果问题类型。我以因果效应推断为例,但这个工作流程适用于所有类型的因果问题。
-
包含与你的因果问题(s)相关的变量——第一步是包含所有与你的因果问题相关的变量。如果你打算问多个问题,包括与这些问题相关的所有变量。作为一个例子,考虑图 3.15。假设我们打算询问V对U和Y的因果效应。这些就成为我们在 DAG 中首先包含的变量。
-
在第 1 步中包含任何变量的共同原因——添加你在第一步中包含的变量的任何共同原因。在我们的例子中,你将从图 3.15 中的变量U、V和Y开始,追溯它们的因果谱系并识别共同的祖先。这些共同的祖先就是共同原因。在图 3.16 中,W[0]、W[1]和W[2]是V、U和Y的共同原因。
![figure]()
图 3.15 首先包括与你的因果问题(s)相关的变量。在这里,假设你感兴趣的是询问关于V、U和Y的问题。
![figure]()
图 3.16 满足因果充分性;包含来自第 1 步的变量的共同原因。
在正式术语中,一个变量是变量对 X 和 Y 的共同原因 Z,如果存在从 Z 到 X 的有向路径,该路径不包括 Y,以及从 Z 到 Y 的有向路径,该路径不包括 X。包括共同原因的正式原则称为 因果充分性。一组变量是因果充分的,如果它不排除该组中任何一对变量之间的任何共同原因。此外,一旦包含了一个共同原因,就不必在相同路径上包含更早的共同原因。例如,图 3.17 展示了我们可以如何排除变量的早期共同原因。
![图]()
图 3.17 一旦包含了一个共同原因,就不必在通往步骤 1 变量的相同路径上包含任何更早的共同原因。
在图 3.17 中,W[2] 在 W[0] 通向 Y 和 U 的路径上,但我们包含 W[0],因为它有自己的路径通向 V。相比之下,虽然 C 是 V、Y 和 U 的共同原因,但 W[0] 在 C 到 V、Y 和 U 的所有路径上,因此我们在包含 W[0] 后可以排除它。同样,W[2] 让我们可以排除 E,而 W[0] 和 W[2] 一起让我们可以排除 D。
-
包含在因果推断统计分析中可能有用的变量——现在我们包含在您想要进行的因果推断统计方法中可能有用的变量。例如,在图 3.18 中,假设您对估计 V 对 Y 的因果效应感兴趣。您可能希望包括可能的“工具变量”。我们将在本书的第四部分正式定义这些变量,但就目前而言,在因果效应问题中,工具 是一个感兴趣变量的父变量,并且它可以帮助统计估计因果效应。在图 3.18 中,Z 可以作为工具变量。您不需要包含 Z 以满足因果充分性,但您可能选择包含它以帮助量化因果效应。
![图]()
图 3.18 包含在因果推断统计分析中可能有用的变量。W 是混杂因素,Z 是工具变量,X 是效应修饰因子,Y 是结果,V 是治疗,而 U 是前门中介。
类似地,X[0] 和 X[1] 在分析 Y 的其他变异来源时也可能有用。我们可能可以利用它们来减少因果效应统计估计中的方差。或者,我们可能对因果效应的 异质性(因果效应在由 X[0] 和 X[1] 定义的群体子集中如何变化)感兴趣。我们将在第十一章中更详细地研究因果效应异质性。
-
包含帮助 DAG 传达完整故事的变量——最后,包含任何有助于 DAG 更好地作为沟通工具的变量。考虑图 3.19 中的共同原因 D。
![图]()
图 3.19 包含帮助 DAG 讲述完整故事的变量。在这个例子中,尽管在第二步(图 3.17)中排除了D,但我们仍然可能希望包含D,如果它具有沟通价值的话。
在图 3.17 中,我们得出结论,在包含共同原因W[0]和W[2]之后,可以排除共同原因D。但也许D是领域专家概念化领域的重要变量。虽然它对量化V对U和Y的因果效应没有帮助,但省略它可能会感觉有些不自然。如果是这样,包含它可能有助于 DAG 讲述更好的故事,通过展示关键变量如何与您包含的变量相关联。当您的因果 DAG 讲述一个令人信服的故事时,您的因果分析就更有说服力。
3.3.2 通过变量在推理中的作用包括因果 DAG 中的变量
许多因果推理专家倾向于不将他们的假设以因果 DAG 的形式写出,而是根据它们在因果推理计算中的角色指定一组相关变量。在计量经济学教学中,关注变量-角色-推理而不是因果 DAG 是常见的。这样的角色包括我之前已经介绍过的,如“共同原因”、“工具变量”和“效应调节因子”。同样,我们将在第十一章中正式定义这些。
目前,我想明确指出这并不是一个竞争性的范式。经济学家可能会说他们对V对U的因果效应感兴趣,条件是在某些“效应调节因子”下,并且他们计划使用“工具变量”来“调整”共同原因的影响。这些角色都对应于因果 DAG 中的结构;图 3.19 中U和V的共同原因包括W[0]、W[1]和W[2]。Z是一个工具变量,而X[0]和X[1]是效应调节因子。假设具有这些角色的变量对您的因果效应估计分析很重要,这隐含地假设您的 DGP 遵循具有这种结构的因果 DAG。
事实上,给定一组变量及其角色,我们可以在该集合上构建隐含的因果 DAG。DoWhy 因果推理库展示了如何做到这一点。
列表 3.10 基于因果效应推理中的角色创建 DAG
from dowhy import datasets
import networkx as nx
import matplotlib.pyplot as plt
sim_data = datasets.linear_dataset( #1
beta=10.0,
num_treatments=1, #2
num_instruments=2, #3
num_effect_modifiers=2, #4
num_common_causes=5, #5
num_frontdoor_variables=1, #6
num_samples=100,
)
dag = nx.parse_gml(sim_data['gml_graph']) #7
pos = { #7
'X0': (600, 350), #7
'X1': (600, 250), #7
'FD0': (300, 300), #7
'W0': (0, 400), #7
'W1': (150, 400), #7
'W2': (300, 400), #7
'W3': (450, 400), #7
'W4': (600, 400), #7
'Z0': (10, 250), #7
'Z1': (10, 350), #7
'v0': (100, 300), #7
'y': (500, 300) #7
} #7
options = { #7
"font_size": 12, #7
"node_size": 800, #7
"node_color": "white", #7
"edgecolors": "black", #7
"linewidths": 1, #7
"width": 1, #7
} #7
nx.draw_networkx(dag, pos, **options) #7
ax = plt.gca() #7
ax.margins(x=0.40) #7
plt.axis("off") #7
plt.show() #7
1 datasets.linear_ dataset 从指定的变量生成ΔAG。
2 添加一个处理变量,例如图 3.19 中的 V。
3 图 3.19 中的 Z 是一个工具变量的例子;一个导致处理的原因,但其到达结果的单一路径是通过处理。在这里,我们创建了两个工具变量。
4 图 3.19 中的 X [0]和 X [1]是帮助模型因果效应异质性的“效应调节因子”的例子。ΔoWhy 将这些定义为结果的其他原因(尽管它们不必是)。在这里,我们创建了两个效应调节因子。
5 我们添加了 5 个共同原因,例如图 3.19 中的三个 W [0]、W [1]和 W [2]。与图 3.19 中这些变量之间微妙的结构不同,这里的结构将会简单。
6 前门变量位于处理和效应之间的路径上,如图 3.19 中的 U。这里我们添加一个。
7 此代码提取图,创建绘图布局,并绘制图形。
8 此代码提取图,创建绘图布局,并绘制图形。
此代码生成图 3.20 中所示的 DAG。
这种基于角色的方法产生了一个简单的模板因果 DAG。它不会给你像图 3.19 中那样的细微差别,并且会排除我们在步骤 4 中添加的好的叙事变量,如图 3.19 中的D。但它足以应对预定义的因果效应查询。当与对 DAG 持怀疑态度但乐于讨论变量角色的人合作时,这是一个非常好的工具。但不要相信这种方法是无 DAG 的。DAG 只是隐含在角色指定的假设背后的。

图 3.20 通过指定变量在因果效应推理中的作用构建的因果 DAG
这种模板方法也可以用于其他因果查询。你还可以使用这种方法在第一步中获得一个基本的因果 DAG,然后你可以在此基础上构建一个更细微的图。
3.4 展望:模型测试和将因果图与深度学习相结合
在构建因果有向无环图(DAG)时,最大的问题是“如果我的因果 DAG 是错误的怎么办?”我们如何对我们的选定的 DAG 有信心?在下一章中,我们将探讨如何使用数据来对因果 DAG 进行压力测试。一个关键的见解是,虽然数据永远不能证明因果 DAG 是正确的,但它可以帮助显示它何时是错误的。你还将了解因果发现,这是一组从数据中学习因果 DAG 的算法。
在本章中,我们探讨了使用 pgmpy 在 DAG 结构上构建简单的因果图形模型。在整个书中,你将看到如何构建更复杂的因果图形模型,这些模型利用神经网络和自动微分。即使在那些更复杂的模型中,因果马尔可夫性质和 DAG 的好处,包括因果不变性和参数模块化,仍然会存在。
摘要
-
因果有向无环图(DAG)可以表示我们对数据生成过程(DGP)的因果假设。
-
因果 DAG 是可视化并传达你的因果假设的有用工具。
-
DAGs 是计算机科学中的基本数据结构,并且允许我们使用许多快速算法来处理因果推断任务。
-
DAGs 通过因果马尔可夫性质将因果关系与条件独立性联系起来。
-
DAGs 可以为概率机器学习模型提供支架。
-
我们可以使用各种统计参数学习方法在 DAG 之上训练概率模型。这包括最大似然估计和贝叶斯估计。
-
给定一个因果有向无环图(DAG),模型构建者可以从 DAG 中因果马尔可夫核的各种参数化中选择,范围从条件概率表到回归模型再到神经网络。
-
一个因果充分集包含该集合中所有对之间的共同原因。
-
你可以通过从一组感兴趣的变量开始,将其扩展到因果充分集,添加对因果推断分析有用的变量,最后添加任何有助于有向无环图(DAG)传达完整故事的变量来构建一个因果有向无环图(DAG)。
-
每个因果马尔可夫核代表一种独特的因果机制,它决定了子节点如何由其父节点决定(假设有向无环图(DAG)是正确的)。
-
“机制独立性”指的是机制如何与其他机制区分开来——对一个机制的更改不会影响其他机制。
-
当你在因果有向无环图(DAG)上构建生成模型时,每个因果马尔可夫核的参数代表了对潜在因果机制的编码。这导致了“参数模块化”,这使得你可以单独学习每个参数集,甚至可以使用常识推理来估计参数而不是数据。
-
每个因果马尔可夫核代表一个独特的因果机制,这为高级任务(如迁移学习、数据融合和不变预测)提供了可利用的不变性来源。
-
你可以通过变量在特定因果推断任务中扮演的角色来指定一个 DAG。
第四章:使用因果约束测试 DAG
本章涵盖
-
使用 d 分离来推理因果关系如何约束条件独立性
-
使用 NetworkX 和 pgmpy 进行 d 分离分析
-
使用条件独立性测试反驳因果 DAG
-
在存在潜在变量时反驳因果 DAG
-
使用和应用因果发现算法约束
我们的因果有向无环图(DAG)或任何因果模型,都捕捉了关于现实世界的一组假设。通常,这些假设可以通过数据来检验。如果我们检验一个假设,结果发现它不成立,那么我们的因果模型就是错误的。换句话说,我们的检验“证伪”或“反驳”了我们的模型。当这种情况发生时,我们回到起点,提出一个更好的模型,并再次尝试反驳它。我们重复这个循环,直到我们得到一个能够抵抗我们反驳尝试的模型。
在本章中,我们将专注于使用基于统计条件独立性的测试来检验我们的因果 DAG。随着你更多地了解我们可以放入因果模型中的假设,以及这些假设允许你做出的推断,你将学会新的测试和反驳模型的方法。本章中你将学习的运行条件独立性测试的工作流程可以应用于你可能提出的新的测试。
4.1 因果关系如何导致条件独立
因果关系以某种方式约束数据,其中之一是通过迫使变量条件独立。这种强制条件独立性为我们提供了一种使用独立性统计测试来用数据检验模型的方法;如果我们发现当 DAG 表明它们不应该依赖时,两个变量之间存在强烈的依赖证据,那么我们的 DAG 就是错误的。
在本章中,我们将使用这些统计独立性测试来测试我们的因果 DAG,包括当数据中的其他变量是潜在变量时,我们可以运行的关于观测变量函数的独立性测试。最后,我们将探讨这些想法如何使因果发现算法能够直接从数据中学习因果 DAG。
但在那之前,让我们看看因果关系是如何导致条件独立的。再次考虑我们的血型示例,如图 4.1 所示。你父亲的血型是你血型的直接原因,而你父亲的父亲的血型是间接原因。尽管是导致你血型的原因,但你的父亲的父亲的血型在给定你父亲血型的情况下与你血型条件独立。

图 4.1 因果关系导致条件独立。你的血型在给定你父亲血型(直接原因)的情况下与你父亲的父亲的血型(间接原因)条件独立。
我们可以从因果关系中得知这一点;父母的血型完全决定了孩子的血型。你父亲的和外祖母的血型完全决定了你父亲的血型,但你的父亲和母亲的血型完全决定了你的血型。一旦我们知道了你父亲的血型,就没有更多你外祖父的血型可以告诉我们了。换句话说,你的祖父母的血型在你父母的情况下是独立的。
4.1.1 碰撞器
现在我们来考虑碰撞器,这是一种因果如何导致通常独立的变量之间产生依赖关系的有趣方式。考虑图 4.2 中的典型例子。水龙头开启或关闭,以及是否下雨,是草地湿润与否的原因,但知道水龙头关闭并不能帮助你预测是否在下雨。换句话说,水龙头的状态和是否下雨是独立的。但是,当你知道草地是湿的,同时知道水龙头关闭就能告诉你一定是在下雨。所以,虽然水龙头的状态和下雨的有无是独立的,但在给定草地状态的情况下,它们变成了条件依赖的。

图 4.2 水龙头开启或关闭以及是否下雨会导致草地湿润或不湿润。知道水龙头关闭并不能帮助你预测是否在下雨——水龙头状态和下雨状态是独立的。但是,如果知道草地是湿的,那么知道水龙头关闭就能告诉你一定是下雨了——在水龙头状态和下雨状态是条件依赖的,给定草地的状态。
在这种情况下,“湿草地”是一个碰撞器:至少有两个独立原因的效果。碰撞器之所以有趣,是因为它们说明了因果变量可以是独立的,但如果我们基于一个共享的效果变量进行条件化,它们就会变得依赖。在条件独立术语中,父母的因果是独立的(水龙头 ⊥ 雨),但在观察到孩子(基于孩子进行条件化)之后,它们变得依赖(水龙头 ⟂̷ 雨 | 湿草地)。
作为另一个例子,让我们再次看看血型,如图 4.3 所示。

图 4.3 母亲和父亲通常是无关的,所以知道母亲的血型并不能帮助预测父亲的血型。但是,如果我们知道母亲的血型和孩子的血型,就能缩小父亲可能的血型范围。
如果我们假设母亲和父亲没有关系,母亲的血型并不能告诉我们父亲的血型——(母亲的血型 ⊥ 父亲的血型)。但是,如果我们知道孩子的血型是 B 型,这能帮助我们使用母亲的血型来预测父亲的血型吗?
为了回答这个问题,请查看图 4.4 中的标准血型表。我们看到,如果母亲血型是 A 型,孩子血型是 B 型,那么父亲可能的血型可能是 B 型和 AB 型。

图 4.4 知道母亲的血型可以帮助你在知道孩子的血型的情况下缩小父亲的血型范围。
仅知道母亲的血型并不能告诉我们关于父亲血型的任何信息。但如果我们添加关于孩子血型(碰撞体)的信息,我们可以将父亲的血型从四种可能性缩小到两种。换句话说,(母亲的血型 ⊥ 父亲的血型),但一旦我们对孩子的血型进行条件化,母亲和父亲的血型就会变得相关。
碰撞体出现在因果推断的各个部分。在第 4.6 节中,我们将看到碰撞体在因果发现任务中的重要性,我们试图从数据中学习一个因果 DAG。当我们查看第七章和第十一章中的因果效应时,我们将看到在推断因果效应时意外“调整”碰撞体如何引入不受欢迎的“碰撞体偏差”。
目前,我们只需注意,碰撞体可能与我们的统计直觉相矛盾,因为它们描述了因果逻辑如何导致两种事物独立,但在对第三个或更多变量进行条件化时“突然”变得相关。
4.1.2 使用因果图抽象独立性

图 4.5 在因果效应推断中,我们感兴趣的是在统计上量化一个原因(治疗)对效应(结果)的影响程度。混杂因素是常见的共同原因,是治疗和结果之间非因果相关性的来源。因果效应推断需要“调整”混杂因素。D 分离是告诉我们如何做到这一点的理论的核心。
在上一节中,我们使用了血型遗传的基本规则来展示因果关系如何导致条件独立性。如果我们想要编写能够帮助我们跨不同领域进行因果推断的代码,我们需要一个将因果关系映射到条件独立性的抽象,这个抽象不依赖于特定领域的规则。“D 分离”解决了这个问题。
D 分离和d-连接指的是我们如何使用图来推理条件独立性。这些概念乍一看是新颖的,但它们将成为你在基于图的因果推理中最重要的工具之一。作为第七章的一个小小的预告,考虑一下图 4.5 中展示的因果效应推断问题。在因果推断中,你感兴趣的是在统计上量化一个原因(通常称为“治疗”)对效应(“结果”)的影响程度。
正如你在第三章中看到的,你可以用因果推断任务中变量的角色来描述 DAG 中的变量。在因果效应推断的任务中,一个角色是混杂因素。混杂因素是治疗和效果之间非因果相关性的共同原因。为了估计治疗对结果的影响,我们必须“调整”混杂因素。这样做理论上的依据是基于“d 分离”路径{治疗 ← 混杂因素 → 结果}并聚焦于路径{治疗 → 结果}。
4.2 D 分离和条件独立性
回顾一下前几章中的以下思想:
-
因果 DAG 是数据生成过程(DGP)的一个模型。
-
DGP 包含一个联合概率分布。
-
因果关系在联合概率分布中诱导变量之间的独立性和条件独立性。
D 分离和 d 连接是用于推理因果 DAG 模型中联合概率分布的条件的独立性的图形抽象。这个概念指的是因果 DAG 中的节点和节点之间的路径;节点和路径是“d 连接”或“d 分离”的,其中“d”代表“方向”。这个想法是使一个像“这些节点在图中是 d 分离的”这样的陈述与一个像“这些变量在条件独立下”这样的陈述相对应。D 分离不是关于陈述什么导致什么的;它是关于 DAG 中变量之间的路径是否表明了在联合概率分布中这些变量之间是否存在依赖。
我们想要建立这种对应关系,因为推理图形比直接推理概率分布更容易;追踪节点之间的路径比上概率理论的高级课程更容易。此外,回想一下第二章的内容,图形是算法和数据结构的基础,统计建模从做出条件独立性假设中受益。
4.2.1 D 分离:简化因果分析的门户
假设我们有一个陈述,即U和V在给定Z的情况下是条件独立的(即U⊥V|Z)。我们的任务是定义一个纯粹以图形术语相对应的陈述。我们将这个陈述写成U⊥[G]V|Z,并读作“U和V在图G中由Z进行 d 分离。”
用Z代表一组称为 d 分离集或“阻塞者”的节点。在条件独立性的术语中,Z对应于一组我们对其条件化的变量。我们的目标是定义 d 分离,使得Z中的节点在某种意义上“阻塞”了由我们的 DAG 的因果结构所暗示的U和V之间的依赖。
接下来,让P成为一个路径,意味着两个节点之间的一系列连接边(和节点)。路径上的节点是否在你的数据中被观察到并不重要(我们将在后面看到数据是如何起作用的)。我们“路径”的定义不依赖于边的方向;例如,{x → y → z},{x ← y → z},{x ← y ← z}和{x → y ← z}都是x和z之间的路径。
最后,让我们重新审视碰撞器。碰撞器结构指的是像x → y ← z这样的模式,其中中间节点y(碰撞器)有入边。
我们现在定义 d 分离。首先,如果两个节点u和v之间的所有路径都被Z分离,那么这两个节点u和v被认为是 d 分离(阻塞)的。如果u和v之间的任何路径没有被 d 分离,那么u和v是 d 连接的。
让我们定义路径的 d 分离。一个路径P如果满足以下四个标准之一,则被认为是被节点集Z分离的。
-
P包含一个链i → m → j,其中中间节点m在Z中。
-
P包含一个链i ← m ← j,其中中间节点m在Z中。
-
P包含一个子父子结构i ← m → j,其中中间(父)节点m在Z中。
让我们暂停一下。标准 1-3 只是通过三种节点之间边的方向来分析。如果这样继续下去,那么如果P上的一个节点在集合Z中,那么P总是 d 分离的。那将会很棒,因为这将意味着如果 DAG 中存在任何路径,那么两个节点就是 d 连接的(即,相关的),如果所有这些路径都被集合Z中的节点阻塞,那么它们就是 d 分离的。
不幸的是,碰撞器使得第四个标准与其他标准相反:
-
- P包含一个碰撞器结构i → m ← j,其中中间节点m不在Z中,并且m的任何后代都不在Z中。
这第四个标准是 d 分离如何捕捉两个独立(d 分离)的项目在条件化一个碰撞器时如何变得相关。
许多作家混淆了 d 分离和条件独立性。请清晰地记住这种区别:⊥[G]指的是图,而⊥指的是分布。这很重要,因为正如你将在本章后面看到的,我们将使用 d 分离来测试我们的因果假设与数据中条件独立性的统计证据。

图 4.6 集合{m, k}是否 d 分离路径u → i → m → j → v?
让我们通过几个例子来分析。
以链 i → m → j 为例
考虑图 4.6 中的 DAG,其中P是u → i → m → j → v。这条路径默认是 d 连接的。现在让Z是集合{m, k}。P包含一个链i → m → j,并且m在Z中。如果我们阻塞在Z上,第一个标准就得到了满足,并且u和v是 d 分离的。
对于某些(但不是所有)情况,理解 d 分离的一个有用的类比是电子电路。没有对撞机的路径是 d 连接的,就像闭合电路,其中电流无阻碍地流动。“阻塞”路径上的节点会 d 分离路径,并“断开电路”,使电流无法流动。在 Z 上(特别是阻塞在 m 上,它在 Z 中)阻塞“断开电路”,如图 4.7 所示。

图 4.7 路径默认是 d 连接的,但阻塞在 m ∈ Z 上会 d 分离路径,并形象地“断开电路”(“∈”表示“在...中”)。
以链 i ← m → j 为例
现在考虑图 4.8 中的 DAG,其中 P 是 u ← i ← m → j → v。这条路径默认也是 d 连接的。请注意,d 连接可以与因果关系的方向相反。在图 4.7 中,从 u 到 v 的 d 连接路径是按照因果关系方向进行的:u 到 i (u ← i),然后 i 到 m (i ← m),然后 m 到 j (m → j),最后 j 到 v (j → v)。但在这里,我们有两个 反因果关系(即与因果关系方向相反)的步骤,即从 u 到 i (u ← i) 和 i 到 m (i ← m)。

图 4.8 是否集合 {m} d 分离路径 u ← i ← m → j → v?
假设我们阻塞在集合 Z 上,且 Z 只包含节点 m。那么条件 3 得到满足,路径是 d 分离的,如图 4.9 所示。

图 4.9 从 u 到 v 的这条路径默认也是 d 连接的,尽管它有一些步骤(u 到 i 和 i 到 m)是违反因果关系的。再次强调,阻塞在 m ∈ Z 上会 d 分离路径,并形象地“断开电路”。
对撞机使 d 分离变得奇怪
第四个标准关注对撞机模式 i → m ← j:P 包含一个 对撞机 结构,i → m ← j,其中中间节点 m 不在 Z 中,且 m 的任何后代都不在 Z 中。
让我们将这个例子与我们的血型例子联系起来。在这里,i 和 j 是父母的血型,而 m 是孩子的血型。我们看到了对撞机有点奇怪,因为基于对撞机(孩子的血型)的条件会导致两个独立事物(如父母的血型)之间的依赖关系。这种奇怪性使得 d 分离在第一眼看起来有点难以理解。图 4.10 说明了对撞机如何影响 d 分离。

图 4.10 对撞机使 d 连接变得复杂。给定路径上的一个节点 m,如果 m 不是一个对撞机,则路径默认是 d 连接的,当你阻塞在 m 上时,路径是 d 分离的。如果 m 是一个对撞机,则路径默认是 d 分离的,当你阻塞在 m 上时,路径是 d 连接的。
以下是对对撞机的描述:
-
除非路径有一个对撞机模式,否则两个节点之间的所有路径默认是 d 连接的。带有对撞机的路径默认是 d 分离的。
-
在 d 连接路径上的任何节点进行阻塞都将 d 分离该路径 除非该节点是碰撞节点。在碰撞节点上进行阻塞将默认情况下通过 d 连接路径,如下所示
-
将阻塞应用于该碰撞的后代。

图 4.11 集合{m}(或{k}或{m, k})是否 d 分离路径 u → i → m ← j → v?
在电路类比中,碰撞节点类似于一个开开关,它阻止电子电路中的电流流动。当路径有一个碰撞节点时,该节点会阻止所有电流通过。碰撞节点会中断电路。在碰撞节点上进行阻塞就像关闭开关一样,之前无法通过的电流现在可以通过了(d 连接)。
在图 4.11 的 DAG 中,路径 u → i → m ← j → v 是否默认情况下是 d 连接的?不,因为该路径包含一个碰撞结构 m (i → m ← j)。
现在考虑如果阻塞集 Z 包含 m 会发生什么。在这种情况下,条件 4 被违反,路径 变为 d 连接,如图 4.12 所示。

图 4.12 这个从 u 到 v 的路径默认情况下是 d 分离的,因为它包含一个碰撞节点 m。碰撞节点类似于一个开路。在 m 或其任何后代上进行阻塞将连接路径,并形象地闭合电路。
如果 Z 没有包含 m 但只包含 k(或者如果 Z 包含了 m 和 k),路径也会变为 d 连接。在碰撞节点的后代上进行阻塞与在碰撞节点上进行阻塞的方式相同。
你能猜到为什么吗?这是因为碰撞的后代与碰撞节点 d 连接。在因果术语中,我们看到了给定母亲的血型,观察孩子的血型(碰撞节点)可能会揭示父亲的血型。假设如果我们不是观察孩子的血型,而是观察孩子的孩子的血型(称为孙子的血型)。这个孙子的血型可以帮助缩小孩子的血型,从而缩小父亲的血型。换句话说,如果母亲和父亲的血型在孩子的血型给定的情况下是相关的,并且孙子的血型为你提供了关于孩子血型的信息,那么在孙子的血型给定的情况下,母亲和父亲的血型也是相关的。
D 分离和节点集
D 分离不仅适用于节点对,也适用于节点集对。在符号 u⊥v|Z 中,Z 可以是阻塞集,而 u 和 v 也可以是集。我们通过阻塞每个集合成员之间的所有 d 连接路径来 d 分离两个集合。其他基于图的因果概念,如 do-calculus,也推广到节点集。如果你记住这个事实,我们就可以在单个节点上建立直觉,而这种直觉可以推广到集合。
当阻塞集 Z 是单元素集 {*m*} 时,这个集合足以阻塞图 4.7 中的路径 u → i → m → j → v 和图 4.8 中的路径 u ← i ← m → j → v。总的来说,集合 {*i*}, {*m*}, {*j*}, {*i*, *m*}, {*i*, *j*}, {*m*, *j*} 和 {*i*, *m*, *j*} 都可以 d-分离这两条路径上的 u 和 v。然而,{*i*}, {*m*}, 和 {*j*} 是 最小 d-分离集,这意味着所有其他 d-分离集都至少包含这些集合中的一个。最小 d-分离集足以 d-分离这两个节点。在推理 d-分离和将其实现为算法时,我们希望专注于找到最小 d-分离集;如果 U⊥V|Z 和 U⊥V|Z, W 都为真,我们不想在 U⊥V|Z, W 上浪费精力。
4.2.2 d-分离多条路径的示例
假设我们想要 d-分离两个节点。通常在这些节点之间有多条 d-连接路径。为了 d-分离这些节点,我们需要找到能够 d-分离每条路径的阻塞器。让我们通过一些例子来了解一下。
寻找最小 d-分离集
在一个具有更多边的更大图中,两个节点之间的路径数量可能相当大。但通常较长的路径会作为阻塞较短路径的副作用而被阻塞。因此,我们可以从较短的路径开始,逐步到尚未被阻塞的较长路径,直到没有未阻塞的路径为止。
例如,在图 4.13 中,U 和 V 通过这些路径 d-连接。为了 d-分离它们,需要哪些节点集是必需的?
在图 4.13 中,U 和 V 通过这些路径 d-连接:
-
U → I → V
-
U → J → V
-
U → J → I → V
首先,我们可以通过在 I 上阻塞来 d-分离 U → I → V。然后,我们通过在 J 上阻塞来 d-分离 U → J → V。在这个时候,我们注意到我们的阻塞集合 {*I*, *J*} 已经 d-分离了 U → J → I → V,所以我们完成了。
在另一个例子中,我们如何在图 4.14 中 d-分离 U 和 V?

图 4.13 我们可以使用集合 {*I*, *J*} 来 d-分离 U 和 V。

图 4.14 我们可以使用集合 {*I*, *M*, *K*, *J*} 或 {*I*, *M*, *K*, *L*} 来 d-分离 U 和 V。
U 和 V 之间有许多路径。让我们首先列出三条最短的路径:
-
U ← I → V
-
U ← M → V
-
U ← K → V
我们至少需要在 {*I*, *M*, *K*} 上阻塞来 d-分离这三条路径。请注意,U 有另一个父节点 J,并且从 U 到 V 通过 J 有几条路径,但我们只有两条尚未 d-分离的路径;U ← J → L → V 和 U ← J → K ← L → V。J 和 L 都会阻塞这些路径,因此我们可以使用最小集合 {*I*, *M*, *K*, *J*} 或 {*I*, *M*, *K*, *L*} 来 d-分离 U 和 V。请注意,U ← J → K ← L → V 是 d-连接的,因为我们最初将 K,这条路径上的一个碰撞器,添加到我们的阻塞集中。接下来,我们来看另一个这种现象的例子。
当 d-分离一条路径时,会连接另一条路径
当你尝试通过在另一条路径上的碰撞节点上阻塞来 d-分离 U 和 V 之间的路径时,你可能会 d-连接那条其他路径。这是可以的,只要你采取额外的步骤来 d-分离那条路径。为了说明这一点,考虑图 4.15 中的图。这个图足够简单,我们可以枚举出所有的路径。

图 4.15 使用 M 进行阻塞将阻塞 U ← M → V 的路径,但会 d-连接 U ← I → M ← J → V 的路径,因为 M 是 I 和 J 之间的碰撞点。因此,我们需要在 I 或 J 上额外进行阻塞以 d-分离 U ← I → M ← J → V。
让我们从三个 d-连接路径开始:
-
U ← M → V
-
U ← I → M → V
-
U ← M ← J → V
我们还有一个路径 U ← I → M ← J → V,但那不是一个 d-连接路径,因为 M 是该路径上的一个碰撞点。
最简单的方法是使用一个节点阻塞所有这三个 d-连接路径,即在 M 上进行阻塞。然而,如果我们阻塞那个碰撞点,路径 U ← I → M ← J → V 将会 d-连接。因此,我们需要在 I 或 J 上额外进行阻塞。换句话说,我们的最小 d-分离集是 {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-分离分析。
列表 4.1 图 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 图对象(如 ΔiGraph(有向图))的 d-分离算法。
2 ΔAG 是 BayesianNetwork 类的基类。ΔAG 的基类是 NetworkX 的 ΔiGraph。因此,is_d_separator 将在 ΔAG(和 BayesianNetwork)类的对象上工作。
3 构建 4.11 图中的图。在碰撞点 M 上进行阻塞将阻塞 U ← M → V,但将 d-连接 U ← I → M ← J → V 的路径,所以这将打印 False。
4 在 M 上进行阻塞将阻塞 U ← M → V 并打开(d-连接)U ← I → M ← J → V,但我们可以用 I 和 J 来阻塞那条路径,所以这评估为 True。
5 同时在 I 和 J 上进行阻塞是多余的。最小 d-分离集是 {“M”, “I”} 和 {“M”, “J”}。
pgmpy 还在 DAG 类中有一个 get_independencies 方法,它枚举了给定图中的最小 d-分离状态。
列表 4.2 在 pgmpy 中枚举 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 获取 ΔAG 中所有为真的最小 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 世纪的哲学家,以其对科学哲学的贡献而闻名,特别是他的证伪理论。波普尔认为,科学理论不能被证明为真,但它们可以被测试,并可能被证伪,换句话说,反驳。
我们采用一种“波普尔式”的模型构建方法,这意味着我们不仅想要找到一个与证据拟合的模型。相反,我们积极寻找反驳我们模型证据。当我们找到它时,我们拒绝我们的模型,构建一个更好的模型,并重复。
D-分离是我们反驳的第一个工具。假设你构建了一个因果 DAG,并且它暗示了条件独立性。然后你寻找数据中的依赖性证据,在你的 DAG 中应该有条件独立性。如果你找到了这样的证据,你就反驳了你的 DAG。然后你回到因果 DAG 上进行迭代,直到在你的数据下你不能再反驳它为止。
一旦你完成了这个任务,你就可以继续进行下游的因果推理工作。但请记住这种反驳心态。如果你反复使用相同的因果 DAG,你应该始终寻求新的反驳和迭代方法。实际上,你的目标不是得到真正的 DAG,而是得到一个难以反驳的 DAG。
4.3.1 重新审视因果马尔可夫属性
回想一下,我们看到了因果马尔可夫属性的两个方面:
-
局部马尔可夫属性——一个节点在给定其父节点的情况下,与其非后裔节点条件独立。
-
马尔可夫分解性质—联合概率分布分解为变量的条件分布,给定因果 DAG 中的直接父节点。
现在,我们将介绍这个属性的第三个方面,称为全局马尔可夫性质。这个属性表明,因果 DAG 中的 d 分离意味着联合概率分布中的条件独立性。在符号上,我们写成

用简单的话说,那个符号读作“如果 U 和 V 在图 G 中由 Z 分离,那么在给定 Z 的条件下,它们是条件独立的。”请注意,如果因果马尔可夫性质的三个方面中的任何一个为真,那么它们都是真的。
全局马尔可夫性质为我们提供了一个直接反驳我们的因果模型的方法。我们可以使用 d 分离来指定存在条件独立性的统计检验。失败的检验反驳了模型。
4.3.2 使用条件独立性检验进行反驳
有多种方法可以统计地评估条件独立性,其中最明显的方法是使用条件独立性的统计检验。pgmpy 和其他库使得运行条件独立性检验相对容易。让我们重新审视运输模型,如图 4.16 所示。

图 4.16 运输模型。年龄(A)和性别(S)决定教育(E)。教育导致职业(O)和居住地(R)。职业和居住地导致交通(T)。
回想一下,对于我们的运输模型,我们能够收集以下观察结果:
-
年龄(A)—记录为 29 岁及以下个人的“年轻”(“young”),30 至 60 岁(含)个人的“成人”(“adult”),以及 61 岁及以上个人的“老年”(“old”)。
-
性别(S)—个人的自我报告性别,记录为男性(“M”)、女性(“F”)或其他(“O”)。
-
教育(E)—个人完成的教育或培训的最高水平,记录为高中(“high”)或大学学位(“uni”)。
-
职业(O)—雇员(“emp”)或自雇工人(“self”)。
-
居住地(R)—个人居住的城市的人口规模,记录为小(“small”)或大(“big”)。
-
旅行(T)—个人偏好的交通方式,记录为汽车(“car”)、火车(“train”)或其他(“other”)。
在图中,E ⊥[G] T | O, R。因此,让我们检验条件独立性陈述 E ⊥ T | O, R。统计假设检验有一个零假设(表示为 H[0])和一个备择假设(表示为 H[a])。对于条件独立性的统计假设检验,标准是零假设 H[0] 是条件独立性的假设,而 H[a] 是变量不是条件独立的假设。
统计假设测试使用 N 个观察值数据点 U、V 和 Z(来自探索性数据集)来计算一个统计量。以下代码加载了运输数据。加载后,它创建了两个 DataFrame,一个包含所有数据,另一个只包含前 30 行,这样我们可以看到样本大小对显著性测试的影响。
列表 4.3 加载运输数据
import pandas as pd
survey_url = "https://raw.githubusercontent.com/altdeep/causalML/master
[CA] /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
大多数条件独立性测试库将实现频率假设测试。这些测试将根据给定的统计量是否高于或低于某个阈值,得出有利于 H[0] 或 H[a] 的结论。“频率主义”在这个上下文中意味着测试产生的统计量被称为 p-值,而阈值被称为显著性水平,按照惯例通常是 .05 或 .01。
如果 p-值高于显著性阈值,则测试有利于条件独立性的零假设 H[0];如果低于阈值,则有利于备择假设 H[a]。这种频率主义方法是一种优化,保证了显著性水平是当 E 和 T 实际上条件独立时,得出有利于依赖性的结论的上限。
大多数软件库提供了条件独立性测试工具,在计算 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 将显著性水平设置为 .05。
3 当布尔参数设置为 False 时,测试返回三个元素的元组。前两个是卡方统计量和相应的 p 值 0.56。最后一个元素是卡方分布参数,称为自由度,这是计算 p 值所需的。
这将打印出元组 (1.1611111111111112, 0.5595873983053805, 2),其中值分别是卡方测试统计量、p-值和自由度。p-值大于显著性水平,因此此测试有利于条件独立性的零假设。换句话说,这个特定的测试没有提供反对我们模型的反证证据。
我们可以通过将 chi_square 函数的 boolean 参数设置为 True 直接跳到测试的结果。然后,如果 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)
1 导入卡方测试函数。
2 将显著性水平设置为 .05。
3 当布尔参数设置为 True 时,测试返回一个简单的 True 或 False 结果。如果 p 值大于显著性值,则表示支持条件独立性,它将返回 True。否则,它返回 False,表示支持依赖性。
这将打印出结果 True。现在让我们遍历从运输图可以推导出的所有 d-separation 声明,并逐一测试它们。以下脚本将打印出每个 d-separation 声明以及相应的条件独立性测试的结果。
列表 4.6 对每个 d-separation 声明运行卡方测试
from pprint import pprint
from pgmpy.base import DAG
from pgmpy.independencies import IndependenceAssertion
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-separation 声明及其是否支持(或未能反驳)该声明的数据证据的列表。
{(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-separation 的比例
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-separation 在数据中缺乏相应的条件独立性证据。
这个数字似乎很高,但正如我们将在 4.4 节中看到的,这个统计量取决于数据的大小和其他因素。我们将想要将其与其他候选 DAG 的结果进行比较。目前,下一步是检查这些明显的依赖性案例,其中我们的 DAG 表示应该存在条件独立性。如果依赖性的证据很强,我们需要考虑如何改进我们的因果 DAG 来解释它。
之前,我使用了 chi_square 函数,该函数使用卡方测试分布构建特定的测试统计量——用于计算 p-值的分布。卡方分布只是另一种典型分布,类似于正态分布或伯努利分布。卡方分布经常用于离散变量,因为在离散设置中,有几个测试统计量要么具有卡方分布,要么随着数据量的增加而越来越接近卡方分布。总的来说,独立性测试具有各种测试统计量和不同的测试分布。pgmpy 通过调用 SciPy 的 stats 库提供了几种选项。
一个常见的担忧是测试做出了强烈的假设。例如,一些连续变量之间的条件独立性测试假设变量之间的任何依赖性都是 线性的。另一种方法是使用 排列 测试,这是一种算法,它构建 p-值而不依赖于典型测试分布*。排列测试做出的假设较少,但计算成本较高。
4.3.3 一些测试比其他测试更重要
之前的分析测试了由因果 DAG 暗示的所有 d-separation。但有些 d-separation 可能比其他 d-separation 更重要。一些依赖关系和条件独立性关系对于下游因果推断分析至关重要,而其他关系则根本不影响该分析。
例如,考虑图 4.17,我们在第 3.3 节中已经讨论过。我们添加变量 Z 到图中,因为我们可能希望将其用作估计因果效应的“工具变量”。

图 4.17 中包含 Z,X[0] 和 X[1],因为它们在分析 U 对 Y 的因果效应中发挥了有用的作用。它们的作用取决于条件独立性,并且测试它们确实能够扮演这些角色是很重要的。
我们将在第十一章中深入讨论工具变量。现在,只需说,为了 Z 成为工具变量,它必须与 W[0]、W[1] 和 W[2] 独立。因此,我们将特别注意测试这个假设。
4.4 条件独立性测试的注意事项
正如我提到的,条件独立性测试可能是测试你提出的因果 DAG 中隐含的条件独立性约束的最明显的方法。然而,使用统计测试来测试因果 DAG 存在一些注意事项。根据我的经验,这些问题可能会让分析师偏离他们回答因果问题的最终目标。在本节中,我将强调一些这些注意事项,并提出一些条件独立性测试的替代方案。主要启示是,统计测试是构建你的 DAG 的重要工具,但与任何统计方法一样,它不是万能的(这是可以的)。
4.4.1 统计测试总是存在一些错误的可能性
我提到,使用 d-separation 时,我们不应该“把地图当作地形”;d-separation 与条件独立性不是一回事。相反,如果你的模型是因果性的良好表示,d-separation 意味着条件独立性。
同样,条件独立性并不等同于条件独立性的统计证据。DGP 的因果结构对联合概率分布施加了条件独立性约束。但你无法“看到”联合分布及其包含的独立性;你只能“看到”(并对从该分布中抽取的数据进行统计测试)。
就像预测、分类或任何其他统计模式识别过程一样,检测数据中这些独立性的方法可能会出错。你可能会得到假阴性,即一对变量实际上是条件独立的,但统计独立性测试却得出它们是相关的结论。你可能会得到假阳性,即统计独立性测试发现一对变量是条件独立的,而实际上它们并不是。
4.4.2 使用传统的 CI 测试来测试因果 DAG 是有缺陷的
我认为所提出的用于反驳的条件独立性检验是“有缺陷的”,因为它们违反了科学中统计假设检验的精神。假设你认为你在股票价格中发现了某种模式。你倾向于认为这种模式不仅仅是巧合,因为如果是巧合,你可以从中赚钱。为了严谨,不受到你的偏见的影响,你的备择假设应该是这种模式是真实的且可利用的,而零假设则是它只是随机噪声。频率派测试假设零假设是真实的,并给你一个p值,这个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 个数据点,产生了 0.56 的 p 值。在我们的数据中,E ⊥ T | O, R 是真实情况(通过模拟获得),因此如果测试得出与 E ⊥ T | O, R 相反的结论,那是因为测试的统计问题,而不是数据的质量。下面的自助统计分析将展示随着数据集大小的增加,p 值的估计如何下降。
首先,我们将编写一个 sample_p_value 函数,用于为给定数据大小抽取一个 p 值。下一个函数 estimate_p_value 将重复进行此抽样并计算平均 p 值、90%置信区间以及 p 值低于显著性阈值的概率,即拒绝 E ⊥ T | O, R 正确结论的概率。
列表 4.8 E ⊥ T | O, R 测试对样本大小的敏感性自助分析
from numpy import mean, quantile
def sample_p_val(data_size, data, alpha): #1
bootstrap_data = data.sample(n=data_size, replace=True) #1
result = chi_square( #1
X="E", Y="T", Z=["O", "R"], #1
data=bootstrap_data, #1
boolean=False, #1
significance_level = alpha #1
) #1
p_val = result[1] #1
return p_val #1
def estimate_p_val(data_size, data=fulldata, boot_size=1000, α=.05): #2
samples = [ #2
sample_p_val(data_size, data=fulldata, alpha=α) #2
for _ in range(boot_size) #2
] #2
positive_tests = [p_val > significance for p_val in samples] #3
prob_conclude = mean(positive_tests) #4
p_estimate = mean(samples) #4
quantile_05, quantile_95 = quantile(samples, [.05, .95]) #5
lower_error = p_estimate - quantile_05 #5
higher_error = quantile_95 - p_estimate #5
return p_estimate, lower_error, higher_error, prob_conclude
data_size = range(30, 1000, 20) #6
result = list(zip(*[estimate_p_val(size) for size in data_size])) #6
1 给定一定大小的数据集,此函数从完整数据集中随机抽取相应数量的行。然后运行卡方独立性检验并返回 p 值。
2 此函数执行一个“自助”过程,为给定数据大小抽取 1,000 个 p 值,并计算平均 p 值和 90% p 值置信区间。
3 计算支持条件独立性的测试结论的概率。
4 计算 p 值的平均值以获得自助平均值。
5 计算第 5 和第 95 个百分位数以获得 90%的自助置信区间。
6 运行自助分析。
最后,我们将可视化结果。我们将绘制数据大小与给定数据大小得到的 p 值的平均值和 90%置信区间的对比图。我们还将绘制在显著性水平为.05 的情况下,支持真实假设(E ⊥ T | O, R)的概率如何依赖于数据大小。
列表 4.9 可视化条件独立性测试对数据大小的依赖关系
import numpy as np
import matplotlib.pyplot as plt
p_vals, lower_bars, higher_bars, probs_conclude_indep = result #1
plt.title('Data size vs. p-value (Ind. of E & T | O & R)') #2
plt.xlabel("Number of examples in data") #2
plt.ylabel("Expected p-value") #2
error_bars = np.array([lower_bars, higher_bars]) #2
plt.errorbar( #2
data_size, #2
p_vals, #2
yerr=error_bars, #2
ecolor="grey", #2
elinewidth=.5 #2
) #2
plt.hlines(significance, 0, 1000, linestyles="dashed") #2
plt.show()
plt.title('Probability of favoring independence given data size') #3
plt.xlabel("Number of examples in data") #3
plt.ylabel("Probability of test favoring conditional independence") #3
plt.plot(data_size, probs_conclude_indep) #3
1 运行自助分析以获取 p 值的分位数和得出独立性支持的结论的概率。
2 绘制数据大小与 p 值的关系图。在较大的数据集大小下,预期的 p 值会低于阈值。
3 绘制数据大小与在.05 显著性水平下支持独立性的概率的关系图。
图 4.18 显示了第一个图表。下降的曲线是不同数据大小下的预期 p 值,垂直线表示显示 90%自助置信区间的误差线。当我们得到 1,000 个数据点的数据集时,预期的 p 值低于阈值,这意味着测试倾向于得出 E ⊥ T | O, R 是错误的结论。

图 4.18 条件独立性测试的样本量与预期 p-值的关系图(实线)。垂直线是误差线;它们表示 90%的 bootstrap 置信区间。水平虚线是 0.05 的显著性水平,在此之上我们倾向于接受条件独立性的零假设,在此之下我们拒绝它。随着样本量的增加,我们最终会越过这条线。因此,我们的反驳分析结果取决于数据规模。
注意到置信区间的下限在 1,000 之前就已经跨过了显著性阈值,这表明即使在更小的数据规模下,我们也有很好的机会拒绝 E ⊥ T | O, R 的真实结论。这在图 4.19 中更为明显,随着数据规模的增加,得出支持真实结论的概率降低。

图 4.19 随着数据规模的增加,得出支持这种(真实)条件独立性关系 E ⊥ T | O, R 的概率降低。
你可能会认为,随着数据规模的增加,算法能够检测到在较少数据下无法检测到的 E 和 T 之间的微妙依赖关系。并非如此,因为这种运输数据是模拟的,E ⊥ T | O, R 一定是真实的。这是一个数据更多反而导致我们拒绝独立性,因为更多数据导致更多虚假相关性的例子——这些模式实际上并不存在。
一个因果模型要么正确要么错误地描述了 DGP 中的因果关系。模型所暗示的条件独立性要么存在要么不存在。然而,如果这种条件独立性存在,当数据任意大时,测试仍然可以得出支持依赖性的结论。
再次强调,如果我们把条件独立性测试视为反驳我们的 DAG 的启发式方法,那么对数据规模的这种敏感性不应该让我们感到不安。无论数据规模和显著性阈值如何,当没有条件独立性时与当有条件独立性时,p-值之间的相对差异都将是很大且明显的。
4.4.4 多重比较问题
在统计假设检验中,你进行的测试越多,累积的测试错误就越多。当对因果有向图(DAG)中每个 d 分离进行测试时,情况也是如此。在统计学中,这个问题被称为多重比较问题。有解决多重比较问题的方法,例如使用假发现率。如果你熟悉这些方法,应用它们不会有害。如果你想了解更多,请参阅章节的笔记,在www.altdeep.ai/p/causalaibook中可以找到关于因果建模中假发现率的参考。但再次强调,我鼓励你将传统的条件独立性测试视为一种启发式方法,它有助于实现构建良好因果有向图(DAG)的最终目标。专注于这个目标,以及你将使用你的 DAG 进行的后续因果推断分析,并避免陷入统计测试的繁琐细节。
4.4.5 在机器学习设置中进行条件独立性测试的挑战
常用的条件独立性测试库通常仅限于具有相对简单相关模式的单变量。从 SciPy 导入的 pgmpy 的条件独立性测试也不例外。近年来,已经开发出针对更复杂分布的非参数测试,例如基于核的条件独立性测试。如果你对这类测试感兴趣,PyWhy 库中的 PyWhy-Stats 库是一个不错的起点。
然而,在机器学习中,变量通常具有多个维度,如向量、矩阵和张量。例如,因果有向图中的一个变量可能代表构成图像的像素矩阵。此外,这些变量之间的统计关联可能是非线性的。
一种解决方案是专注于预测。如果两件事物是独立的,它们就没有能力预测对方。假设我们有两个预测模型 M[1] 和 M[2]。M[1] 使用 Z 作为预测因子来预测 Y。M[2] 使用 X 和 Z 作为预测因子来预测 Y。预测因子可以有超过一维的维度。如果 X ⊥ Y | Z,那么任何 X 都没有比 Z 已经提供的更多关于 Y 的预测信息。因此,你可以通过比较 M[2] 和 M[1] 的模型预测准确度来测试 X ⊥ Y | Z。当模型表现相似时,我们就有了条件独立性的证据。请注意,你希望防止 M[2] 通过避免过度拟合来“作弊”,这又是虚假相关性可能悄悄进入我们分析的一种方式。
4.4.6 最后的想法
条件独立性测试是一个广泛且复杂的主题。你的目标是反驳你的因果 DAG,而不是创建一个条件独立性测试套件的柏拉图理想。我建议找到一个“足够好”的测试工作流程,然后专注于构建你的 DAG 并在下游因果推断中使用该 DAG。例如,如果我有一组连续和离散变量,那么我宁愿将我的连续变量离散化(例如,将出生时间以来的年龄转换为年龄组),然后使用普通的卡方检验,以保持事情顺利进行。
4.5 在潜在变量给定的情况下反驳因果 DAG
使用条件独立性测试 DAG 的方法存在潜在变量问题。如果我们的因果 DAG 中的一个变量是潜在的(在数据中未观察到),我们就无法运行涉及该变量的任何条件独立性测试。这是一个大问题;如果变量是 DGP 的重要部分,我们无法仅仅因为我们无法测试与该变量的独立性断言而将其排除在我们的 DAG 之外。
为了说明,考虑图 4.20 中的因果 DAG。此图表示吸烟行为 (S) 受香烟成本 (C) 以及遗传因素(用“DNA”中的 D 表示)的影响,这些因素使一个人更容易或更不容易上瘾于尼古丁。相同的遗传因素影响一个人患肺癌 (L) 的可能性。在这个模型中,吸烟对癌症的影响是通过肺部焦油积累 (T) 来中介的。

图 4.20 表示吸烟对癌症影响的因果 DAG。遗传变量 (D) 为灰色,因为它在数据中未观察到,因此我们无法运行涉及 D 的条件独立性测试。然而,我们可以测试其他类型的约束。
如果我们有观察所有这些变量的数据,我们可以运行以下 d-separations 的条件独立性测试:(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-separations 中,唯一不涉及 D 的是 (C ⊥[G] T | S)。我们从一个可行的条件独立性测试减少到只有一个,以测试我们的 DAG。
通常,一个提出的因果模型可以对可由数据测试的联合概率分布产生各种影响。由图结构隐含的条件独立性是一种可测试的影响。但模型的一些影响在潜在变量的情况下是可测试的。在本节中,我们将探讨如何测试一个具有这些潜在变量相关约束的 DAG。
4.5.1 一个与潜在变量一起工作的可测试含义的例子
因果马尔可夫假设表明,d-分离意味着数据中的条件独立性。到目前为止,我们已经探讨了变量之间的直接条件独立性,但当某些变量是潜在变量时,图可以暗示观察变量函数之间的条件独立性。这些含义在文献中被称为“Verma 约束”,尽管我将使用更口语化的“功能约束”。
为了说明,图 4.20 中的 DAG 具有以下功能约束(现在不用担心它是如何导出的):

正如 d-分离(C ⊥[G] T | S)意味着条件独立性陈述(C ⊥ T | S)应该对观测联合分布成立一样,功能约束(C ⊥[G] h(L, C, T))意味着在观测联合分布中,C与变量L,C和T的某个函数h(.)是独立的。这两个含义都是可测试的,因为它们不涉及D。我们现在有两个测试可以运行,而不是一个。
h(.)有两个组成部分:
-
P(l|c, s, t)是一个函数,它返回L = l(假设l对于“有肺癌”是“真”,对于“无肺癌”是“假”),给定C = c,S = s,和T = t的概率。
-
P(s|c)是一个函数,它返回S = s(假设s根据吸烟者吸烟的强度是“低”、“中”或“高”)的概率,给定香烟的成本C = c。
h(.)随后对所有S的值进行求和。该函数的输出是一个随机变量,根据 DAG,它应该与C独立。h(l, c, t)是P(l|c, s, t)和P(s|c)的函数,并且从概率函数的角度考虑独立性可能感觉有些奇怪。记住,独立性关系本身只是联合概率分布的函数。
接下来,我们将从数据中拟合P(l|c, s, t)和P(s|c)的模型,并测试这个独立性关系。但首先,我们将查看允许我们从有向无环图(DAG)中枚举像(C ⊥[G] h(L, C, T)这样的功能约束的库,就像我们可以使用 pgmpy 的get_independencies枚举 d-分离一样。
4.5.2 测试功能约束的库和视角
我们如何推导出像 C ⊥[G] h(L, C, T) 这样的功能约束?就像 d-separation 一样,我们可以从图中算法性地推导出这种约束。一个实现是在 causaleffect R 库中的 verma.constraints 函数。这个函数接受标记为潜变量的 DAG,并返回一组可测试的约束,就像 pgmpy 的 get_independencies 一样。对于 Python,Y0 库(发音为“why-not”)有一个 r_get_verma_constraints 函数(截至版本 0.2.10),这是一个包装器,调用 causaleffect 的 R 代码。这里我将省略 Python 代码,因为它需要安装 R,但请访问 www.altdeep.ai/causalAIbook 获取库和参考链接。
功能约束的数学直觉和一些建议
我们本节的目标只是表明,即使在存在潜在变量的情况下,也有测试你的因果模型的方法。功能约束是其中一种方法,但我们不想过分强调这种特定的可测试含义。更重要的是避免只限制 ourselves 只到数据中完全观察到的 DAG 的危险心态。
话虽如此,对于好奇的人来说,我将提供一个非常高级的数学直觉。回想一下,局部马尔可夫性质表明一个节点在给定其父节点的情况下对其非后代是条件独立的。从那里,我们推导出称为 d-separation 的图形标准,使我们能够找到适用此性质的一组节点,我们编写一个使用这些标准的图算法来枚举 d-separation,并使用该算法来枚举我们可以运行的某些条件独立性测试。
对于给定的节点 X,让我们说“孤儿堂兄弟”是指与 X 共享潜在祖先的非后代。用非正式的话说,这是一个局部马尔可夫性质的潜在变量类似物:一个节点在给定其最近的观察祖先、其孤儿堂兄弟以及这些堂兄弟的其他最近的观察祖先的情况下对其非后代是条件独立的。就像 d-separation 一样,我们可以推导出图形标准来识别适用此情况的个别案例。
回想一下,我们可以将联合概率分布分解,使得每个因子都是给定其父节点的节点结果的条件概率。当我们开始对潜在变量进行边缘化分解并执行后续的概率数学运算时,功能约束中的概率函数(如 h(l,c,t) 中的 P(l|c, s, t) 和 P(s|c) 项)就进入了画面。
如果你想深入了解,请参阅 www.altdeep.ai/p/causalaibook 中列出的参考文献。但请注意,前一部分的警告也适用于这里——*我们的目标是证伪我们的 DAG 并继续进行我们的目标因果推断**。在追求这个目标的过程中,要小心不要陷入统计、数学和理论上的兔子洞。
现在我们有一个新的可测试的蕴涵,形式为 C ⊥[G] h(L, C, T),让我们来测试它。
4.5.3 测试功能约束
要测试 (C ⊥[G] h(L, C, T)),我们必须计算 h(l, c, t) = ∑[S]P(l|c, s, t)P(s|c) 对于我们数据中的每个项目。这需要我们建模 P(l| c, s, t) 和 P(s|c)。我们可以选择几种建模方法,但我们将使用朴素贝叶斯分类器来举例,这样我们就可以继续使用 pgmpy 和 pandas 库。我们将采取以下步骤:
-
将成本 (C) 离散化,以便我们可以将其视为一个离散变量。
-
使用 pgmpy 将朴素贝叶斯分类器拟合到 P(l| c, s, t) 和 P(s|c)。
-
编写一个函数,该函数接受 L、C、T 的值并计算 h(L, C, T)。
-
将该函数应用于数据中的每一行,以获得新的 h(L, C, T) 值列。
-
在该列和 C 列之间运行独立性测试。
设置您的环境
以下代码使用 pgmpy 版本 0.1.19,因为截至编写时,版本 0.1.24 及之前的版本存在一个已报告的漏洞,可能会影响一些朴素贝叶斯分类器推理代码。如果您使用其他方法计算 P(l|c, s, t) 和 P(s|c),则无需执行此操作。为了稳定性,我们还将使用 pandas 版本 1.4.3,这是 pgmpy 0.1.19 当前的版本。请注意,如果您已安装 pgmpy 和 pandas 的后续版本,您可能需要在安装这些版本之前卸载它们,或者您可以启动一个新的 Python 环境。有关带有代码和设置工作环境说明的 Jupyter 笔记本链接,请访问 www.altdeep.ai/p/causalaibook。
首先,我们将导入数据。我们还将离散化香烟的成本 (C),使其更适合使用 pgmpy 进行建模。
列表 4.10 导入和格式化香烟和癌症数据
from functools import partial
import numpy as np
import pandas as pd
data_url = "https://raw.githubusercontent.com/altdeep/causalML/master
[CA] /datasets/cigs_and_cancer.csv"
data = pd.read_csv(data_url) #1
cost_lower = np.quantile(data["C"], 1/3) #2
cost_upper = np.quantile(data["C"], 2/3) #2
def discretize_three(val, lower, upper): #2
if val < lower: #2
return "Low" #2
if val < upper: #2
return "Med" #2
return "High" #2
#2
data_disc = data.assign( #2
C = lambda df: df['C'].map( #2
partial( #2
discretize_three, #2
lower=cost_lower, #2
upper=cost_upper #2
) #2
) #2
) #2
data_disc = data_disc.assign( #3
L = lambda df: df['L'].map(str), #3
) #3
print(data_disc)
1 将 CSV 文件加载到 pandas ΔataFrame 中。
2 将成本 (C) 离散化为三个级别的离散变量,以方便进行条件独立性测试。
3 将成本 (C) 离散化为三个级别的离散变量,以方便进行条件独立性测试。
4 将肺癌 (L) 从布尔值转换为字符串,以便条件独立性测试将其视为一个离散变量。
print(data_disc) 行打印出 data_disc DataFrame 的元素。
C S T L
0 High Med Low True
1 Med High High False
2 Med High High True
3 Med High High True
4 Med High High True
.. ... ... ... ...
95 Low High High True
96 High High High False
97 Low Low Low False
98 High Low Low False
99 Low High High True
[100 rows x 4 columns]
现在我们需要建模 P(l| c, s, t) 和 P(s|c)。我们将选择朴素贝叶斯分类器,这是一个“天真”地假设在 P(l| c, s, t) 的情况下,成本 (C)、吸烟 (S) 和焦油 (T) 在肺癌状态 (L) 给定的情况下是条件独立的概率模型。根据我们的因果图,这显然是不正确的,但如果我们只想找到一种计算给定 C、S 和 T 的 L 的概率值的好方法,那就没关系了。朴素贝叶斯分类器可以很好地做到这一点。
列表 4.11 适配 P(l| c, s, t) 的朴素贝叶斯分类器
from pgmpy.inference import VariableElimination
from pgmpy.models import NaiveBayes
model_L_given_CST = NaiveBayes() #1
model_L_given_CST.fit(data_disc, 'L') #1
infer_L_given_CST = VariableElimination(model_L_given_CST) #1
#1
def p_L_given_CST(L_val, C_val, S_val, T_val):#1
result_out = infer_L_given_CST.query( #1
variables=["L"], #1
evidence={'C': C_val, 'S': S_val, 'T': T_val}, #1
show_progress=False #1
) #1
var_outcomes = result_out.state_names["L"] #1
var_values = result_out.values #1
prob = dict(zip(var_outcomes, var_values)) #1
return prob[L_val] #1
1 我们将在 pgmpy 中使用朴素贝叶斯分类器来计算给定 C, S, 和 T 的值的 L 的概率值。在这种情况下,我们将使用变量消除。
2 我们将在 pgmpy 中使用朴素贝叶斯分类器来计算给定 C, S, 和 T 的值的 L 的概率值。在这种情况下,我们将使用变量消除。
现在,我们将对 P(s|c) 执行相同的操作。
列表 4.12 适配 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, T, C) 函数。以下代码使用 for 循环对 S 进行求和。
列表 4.13 合并模型以创建 h(L, T, C)
def h_function(L, C, T): #1
summ = 0 #2
for s in ["Low", "Med", "High"]: #2
summ += p_L_given_CST(L, C, s, T) * p_S_given_C(s, C) #2
return summ
1 实现 h(L, C, T)。
2 实现对 P(l|c,s,t) * P(s|c) 在 s 上的求和。
现在,我们将计算集合 {C, T, L} 的全部结果。给定这些结果,我们可以使用前面的函数计算每个组合的 h(L, C, T)。
列表 4.14 计算 C, T, 和 L 的结果组合
ctl_outcomes = pd.DataFrame(
[ #1
(C, T, L) #1
for C in ["Low", "Med", "High"] #1
for T in ["Low", "High"] #1
for L in ["False", "True"] #1
], #1
columns = ['C', 'T', 'L'] #1
)
1 计算 L, C, 和 T 的每个可能结果组合的这些值。首先,我们使用列表推导式创建一个包含所有组合的 DataFrame。
打印这些值显示了 C, T, 和 L 的所有结果组合。
C T L
0 Low Low False
1 Low Low True
2 Low High False
3 Low High True
4 Med Low False
5 Med Low True
6 Med High False
7 Med High True
8 High Low False
9 High Low True
10 High High False
11 High High True
对于这些结果中的每一个,我们将应用 h(L, C, T)。
列表 4.15 为 C, T, L 的每个结果计算 h(L, C, T)
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)
现在,对于 C, T, 和 L 的每个联合结果,我们都有一个 h(L, C, T) 的值。
C T L h_func
0 Low Low False 0.392395
1 Low Low True 0.607605
2 Low High False 0.255435
3 Low High True 0.744565
4 Med Low False 0.522868
5 Med Low True 0.477132
6 Med High False 0.369767
7 Med High True 0.630233
8 High Low False 0.495525
9 High Low True 0.504475
10 High High False 0.344616
11 High High True 0.655384
最后,我们将这个 h_func 分布合并到数据集中,使得对于我们的数据中的每一行,我们都能得到一个 h(L, C, T) 的值。
列表 4.16 合并以获取数据集中每行的 h(L, C, T) 值
df_mod = data_disc.merge(h_dist, on=['C', 'T', 'L'], how='left') #1
print(df_mod)
1 添加一个表示变量 h(L, C, T) 的列。
我们通过 print(df_mod) 看到结果:
C S T L h_func
0 High Med Low True 0.504475
1 Med High High False 0.369767
2 Med High High True 0.630233
3 Med High High True 0.630233
4 Med High High True 0.630233
.. ... ... ... ... ...
95 Low High High True 0.744565
96 High High High False 0.344616
97 Low Low Low False 0.392395
98 High Low Low False 0.495525
99 Low High High True 0.744565
[100 rows x 5 columns]
功能约束表明 C 和 h(L, C, T) 应该是独立的,因此我们可以查看 h_func 列和 C 列之间的独立性证据。由于我们对 C 进行了离散化,我们计算 h(L, C, T) 的结果在技术上也是离散的,因此我们可以使用卡方检验。但是,从理论上讲,h(L, C, T) 是连续的,所以我们将使用箱线图来可视化两个变量之间的依赖性。功能约束表明 C 和 h(L, C, T) 应该是独立的,因此我们将使用一个箱线图,该图将 h(L, C, T) 的值与 C 的值进行对比,以直观地检查 C 和 h(L, C, T) 是否看起来是独立的。
列表 4.17 可视化 C 和 h(L, C, T) 之间的独立性
df_mod.boxplot("h_func", "C")
这生成了图 4.21。

图 4.21 在 x-轴上显示成本 (C) 的箱线图可视化,以及在 y-轴上显示函数 h(L, C, T)(标记为“乘积和”)。对于每个 C 值的乘积和分布的重叠支持了功能约束断言,即这两个量是独立的。
图 4.21 中的x-轴表示不同的成本水平(低、中、高)。y-轴表示总和的值。图 4.21 是一个箱线图;每个箱子代表给定C值的总和乘积的分布。箱子的顶部和底部是分布的四分位数,箱子中间的线是中位数,较短的横向线是最大值和最小值(对于低成本,中位数、上四分位数和最大值非常接近)。总的来说,看起来总和乘积的分布在不同成本水平上变化不大;这正是独立性应有的样子。
我们还可以使用方差分析(ANOVA)方法推导出p-值,这次使用 F 检验而不是卡方检验。以下代码使用 statsmodels 库运行 ANOVA 测试。
注意: “PR(>F)”表示对于一个给定变量(在我们的例子中是C)看到 F 统计量的概率至少与从数据中计算出的 F 统计量一样大,假设该变量与sum_product(即p-值)独立。
列表 4.18 使用 ANOVA 评估独立性
from statsmodels.formula.api import ols
import statsmodels.api as sm
model = ols('h_func ~ C', data=df_mod).fit() #1
aov_table = sm.stats.anova_lm(model, typ=2) #1
print(aov_table["PR(>F)"]["C"]) #1
model = ols('h_func ~ T', data=df_mod).fit() #2
aov_table = sm.stats.anova_lm(model, typ=2) #2
print(aov_table["PR(>F)"]["T"]) #2
model = ols('h_func ~ L', data=df_mod).fit() #3
aov_table = sm.stats.anova_lm(model, typ=2) #3
print(aov_table["PR(>F)"]["L"]) #3
1 使用 statmodels 库进行 ANOVA 的食谱
2 返回高p-值,支持(未能证伪)h(L, C, T)和 C 独立的断言
3 仅作为一个合理性检查,我们运行相同的测试来查看 h(L, C, T)是否看起来独立于 T 和 L。与C不同,T 和 L 不应该独立于 h(L, C, T),正如预期的那样,这些测试返回的p-值要小得多,表明存在依赖性。
我们使用print(aov_table["PR(>F)"]["C"])打印出C的p-值,得到约 0.1876。这个p-值表明我们不能拒绝独立性的零假设,所以看起来数据支持约束。我们也对T和L进行了相同的测试,正如预期的那样,这些值要小得多,表明存在依赖性。它们都低于常见的 0.1 阈值,在这个阈值下,标准假设检验将拒绝h(L, C, T)独立于T和L的假设。
4.5.4 关于可测试含义的最终思考
一个有向无环图(DAG)的 d 分离和功能约束意味着,如果 DAG 是 DGP 的良好因果模型,那么在联合概率分布中应该存在某些条件独立性。我们可以通过运行条件独立性统计测试来证伪 DAG。
更普遍地说,因果模型可以对潜在的联合概率分布有不同的数学含义,其中一些可以进行测试。例如,如果你的模型假设原因和效果之间的关系是线性的,你可以在数据中寻找非线性证据(我们将在第六章中了解更多关于功能因果假设的内容)。当然,你也可以通过实验来证伪你的模型含义(我们将在第七章中看到)。
我们在因果建模方面越擅长,我们在测试和证伪我们的因果模型方面就越擅长。但请记住,不要让测试中的统计和数学细微差别分散你的注意力,你的目标是获得一个足够好的模型,并继续进行你的目标因果推断。
4.6 因果发现(及其风险)简介
在之前的流程中,我们提出了一个因果有向图(DAG),考虑了 DAG 对观测联合分布的潜在影响(如条件独立性),然后使用数据对这些影响进行测试。如果我们反过来操作会怎样?如果我们分析数据以寻找因果约束的统计证据,然后构建一个与这些约束一致的因果 DAG 呢?
这描述了因果发现的任务:从数据中统计学习因果 DAG。在本节中,我将提供因果发现的简要介绍,并涵盖你需要知道的内容,以便使用这类算法。
警惕因果发现的虚假承诺
因果发现算法通常被宣传为神奇的工具,可以将任何数据集(无论质量如何有限)转换为因果 DAG。这种虚假承诺阻碍了建模 DGP(而不是数据)和证伪候选模型的思维模式。这也是为什么在实践中很难找到一致的发现用例。本节将采用一种方法,即阐述发现算法的工作原理和失败的地方,而不是列出一系列算法。我将通过关于如何有效地将这些算法融入你的分析工作流程的建议来结束。
我们将从支撑发现算法的关键思想概述开始。
4.6.1 因果发现方法
因果发现有几种方法。一些算法(通常称为基于约束算法)做的是我刚才建议的事情——从数据中条件独立性的证据中逆向工程一个图。其他算法(通常称为基于评分算法)将因果 DAG 转化为数据的解释模型,并找到具有高拟合度评分的因果 DAG。还有一种方法是假设因果 DAG 中父节点和子节点之间功能关系的额外约束,正如我们在第六章中将要看到的结构因果模型。
可能的 DAG 空间是一个离散空间。一类方法试图将这个空间软化成一个连续空间,并使用连续优化技术。深度学习自动微分库的流行加速了这一趋势。
由于 DAG 的空间可能相当大,因此引入先验知识以约束该空间的大小是有用的。这通常表现为指定必须存在的边或必须不存在的边,或者使用图结构的贝叶斯先验。
一些因果发现算法可以处理实验数据。这需要告诉算法哪些变量是由实验者设置的(或者从第七章开始,我们将说这些变量是“干预”的)。
要开始使用 Python 进行因果发现,我推荐使用 PyWhy 库进行因果发现,如 causal-learn 和 DoDiscover。
4.6.2 因果发现、因果忠实性和潜在变量假设
因果马尔可夫性质假设如果我们的 DAG 是真实的,那么该 DAG 中的 d 分离意味着变量联合概率中的条件独立性陈述:

因果忠实性(或简称“忠实性”)是相反的陈述——联合分布中的条件独立性意味着图中 d 分离:

许多因果发现算法依赖于忠实性成立的假设。这可能并不成立。
发现与忠实性违反
在 4.4 节中,我们使用了马尔可夫性质来测试一个候选 DAG;给定一个对于 DAG 成立的 d 分离语句,我们运行一个统计测试来检查该 d 分离所隐含的条件独立性的经验证据。
想象一下,你想要通过逆向构建你的图。你在数据中检测到条件独立性的实例证据,然后你将候选 DAG 的空间限制为与隐含的 d 分离一致的 DAG。你这样迭代进行,直到你缩小了候选 DAG 的空间。一些发现算法执行某种版本的此过程,并且那些执行此过程的算法依赖于忠实性假设。
注意:将条件独立性的证据与 d 分离相匹配的算法通常被称为“基于约束”的发现算法。一个著名的例子是 PC 算法。基于约束的算法找到与因果性经验证据一致的 DAG。
麻烦来自于“忠实性违反”——在联合概率分布中条件独立性不映射到真实 DAG 中的 d 分离语句的特殊情况。一个简单的忠实性违反的例子是一个可以如下分解的三变量系统:P(x, y, z) = P(x, y)P(y, z)P(x, z)。也就是说,对于任何一个变量的任何值,其他两个变量之间的关联总是相同的。你可以在数据中检测到这种奇特的独立性形式,但你不能在 DAG 中使用 d 分离来表示它。(如果你不相信我,可以试试。)
研究人员担心这些特殊情况,因为这意味着一个依赖于忠实性的发现算法并不能推广到所有分布。当你使用这些算法时,你是在假设忠实性在你所面临的问题域中成立,而这并不是你可以测试的事情。然而,因果忠实性的违反通常并不是实际因果发现中最大的头痛来源。这个荣誉留给了潜在变量。
潜在变量的挑战
更大的问题是,大多数因果发现算法再次面临潜在变量问题。为了说明这一点,假设真实的因果图是图 4.22 中的图。

图 4.22 假设这是真实的因果图。在这里,B、C和D在给定A的条件下是条件独立的。
在这个图中,变量B、C和D在给定A的条件下相互独立。现在假设数据中没有观察到A。以A作为潜在变量,发现算法无法运行B ⊥ C | A这样的测试。算法会检测到B、C和D之间的依赖性,但不会在给定A的情况下找到三个变量之间的条件独立性,并且它可能会返回如图 4.23 所示的图,反映了这些结果。

图 4.23 如果A是潜在的,那么基于A的条件独立性测试无法运行。算法会检测到B、C和D之间的依赖性,但不会发现给定A的条件独立性,并且它可能会返回如图所示的图。
解决这个问题的方法是在发现算法中提供关于潜在变量结构的强领域特定假设。一些通用的发现算法为潜在变量假设提供了一些适应(例如,因果-学习库有几个)。但这很少见,因为很难在跨领域泛化的同时让用户容易地指定领域特定假设。
4.6.3 等价类和 PDAGs
假设我们的算法能够从数据中正确地恢复所有真实的条件独立性陈述,并将它们映射回一个真实的 d 分离陈述集(因果忠实性成立)。我们现在面临的问题是,多个因果图可能具有相同的 d 分离陈述集。这个候选因果图的集合被称为马尔可夫等价类。真实的因果图将是这个可能很大的集合中的一个成员。
例如,假设图 4.24 左边的图是真实因果图。图右边的图在A和T之间的边与正确的图不同。这两个图有相同的 d 分离集。实际上,我们还可以改变{L,S}和{B,S}之间的边的方向,仍然处于相同的等价类,除了引入一个碰撞器{L → S ← B},因为新的碰撞器会改变 d 分离集。

图 4.24 假设左边的 DAG 是真实 DAG,右边的(错误)DAG 与马尔可夫等价类相同。中间的 PDAG 代表等价类,其中无向边缘表示成员对方向存在分歧的边缘。
一些发现算法会返回部分定向无环图(PDAG),如图 4.24 中心的 DAG 所示。在 PDAG 中,无向边缘对应于马尔可夫等价类成员之间对边缘方向存在分歧的边缘。这很好,因为我们得到了等价类的图形表示,并且算法可以潜在地在 PDAG 的空间而不是更大的 DAG 空间中搜索。
对撞机和发现
对撞机在因果发现中占有重要地位,因为它们使我们能够仅根据统计依赖的证据来定向 DAG 中的边缘。假设我们正在使用数据尝试构建图 4.24 中的真实 DAG。我们在数据中发现了A和T之间边缘的依赖性证据。马尔可夫等价性的想法意味着证据不足以确定该边缘的方向。通常,数据中的依赖性和独立性证据可以暗示存在边缘,但无法确定其方向。
对撞机是个例外。仅从独立性和依赖性的证据中,我们可以检测到像{T → E ← L}这样的对撞机;如果数据表明T和L是独立的,但在条件化E时变得依赖,那么你就有了具有定向边缘{T → E ← L}的对撞机的证据。
对撞机还可以强制对撞机外的边缘进行定向。例如,考虑图 4.23 中真实 DAG 中E和X之间的边缘。我们可能从以下数据中的证据推断出该边缘的存在:
-
E和X是相关的。
-
T和X是相关的。
-
在给定E的情况下,T和X是独立的。
E和X之间的边缘与该证据一致,但我们应该选择E → X还是E ← X?在这里,对撞机{T → E ← L}有帮助;它已经定向了边缘T → E,因此添加E ← X将诱导另一个对撞机{T → E ← X}。这个对撞机会表明T和X是独立的,但在条件化E时变得依赖,这违反了观察到的第二和第三项证据。因此,我们通过排除法得出结论,该边缘的方向是E → X。
一些因果发现算法本质上算法化了这种逻辑。但记住,当潜在变量在观测变量之间诱导依赖时,这种逻辑就会失效。
话虽如此,PDAG 和马尔可夫等价类仅捕获编码相同一组条件独立性约束的 DAG 之间的等价性。如果你想找到所有满足额外约束假设的图,例如给定某个先验概率具有相同后验概率的所有图,那么 PDAG 可能就不够了。
如果我们只依赖于条件独立性,数据无法区分马尔可夫等价类中的成员,因为拥有相同的 d 分离集合意味着在数据中拥有相同的条件独立性证据。这是一个缺乏因果识别的例子——当我们的数据和一组因果假设不足以区分因果问题的可能答案时(在这种情况下,“正确的因果 DAG 是什么?”)。我们将在第十章深入探讨因果识别。
4.6.4 如何思考因果发现
在 4.3 节中,我论证了使用现成的假设测试库来测试因果诱导的约束,如条件独立性,应被视为一种反驳你的因果 DAG 的启发式方法,而不是验证 DAG 的严格统计程序。同样,我论证了对于实际用户来说,现成的因果发现算法应被视为在人为驱动的因果 DAG 构建过程中进行探索性数据分析的工具。你能够将这些算法输入更多类型的领域知识和潜在变量的知识,就越好。但即便如此,它们仍会产生明显的错误。就像假设测试案例一样,避免陷入试图“修复”发现算法以避免这些错误的兔子洞。将因果发现作为构建一个好的因果 DAG 和进行后续因果推断分析更广泛项目中的一个不完美的工具。
摘要
-
因果建模在联合概率分布上引入了条件独立性约束。D 分离提供了条件独立性约束的图形表示。
-
建立对 d 分离的直觉对于推理因果效应推断和其他查询非常重要。
-
碰撞器可能会使 d 分离变得复杂,但你可以通过在 NetworkX 和 pgmpy 中使用 d 分离函数来建立直觉。
-
使用传统的条件独立性测试库来测试 d 分离有其挑战。这些测试对样本大小敏感,在许多机器学习环境中表现不佳,并且它们的假设不匹配。
-
由于这些挑战,最好将使用现成的条件独立性测试库来尝试否定 DAG 视为更多是一种启发式方法。专注于构建一个好的(即难以反驳的)因果 DAG 和继续进行下游因果推断任务的整体目标。避免在统计假设检验中过分关注理论严谨性。
-
当存在潜在变量时,因果 DAG 对于观察变量函数的测试性含义仍然可能存在。
-
因果发现是指使用统计算法从数据中恢复因果 DAG。
-
因果忠实性属性假设在联合概率分布中条件独立性映射到在真实因果 DAG 中保持的真正的 d 分离集合。
-
DAGs 的马尔可夫等价类是一组具有相同 d 分离集的 DAGs。假设你拥有真实的 d 分离集,那么真实的因果 DAG 通常与其他(错误的)DAGs 共享这个集合。
-
因果发现特别容易受到潜在变量的影响。
-
你能通过先验假设(如潜在结构和哪些边不可能存在以及哪些必须存在)来约束因果推理越多,效果越好。
-
因果发现算法在构建因果 DAG 的过程中是很有用的探索性数据分析工具,但它们并不是该过程的可靠替代品。再次强调,关注构建一个好的因果 DAG 和进行下游因果推理分析的整体目标。避免试图“修复”因果发现算法,以免在你的领域产生明显的错误。
第五章:连接因果性和深度学习
本章涵盖
-
将深度学习融入因果图模型
-
使用变分自动编码器训练因果图模型
-
使用因果方法来增强机器学习
这本书的标题是《因果 AI》,但因果性究竟是如何与 AI 联系起来的?更具体地说,因果性是如何与深度学习联系起来的,深度学习是 AI 的主导范式?在本章中,我从两个角度来探讨这个问题:
-
如何将深度学习融入因果模型——我们将研究一个计算机视觉问题的因果模型(第 5.1 节),然后训练深度因果图像模型(第 5.2 节)。
-
如何利用因果推理进行更好的深度学习——我们将研究关于机制独立性和半监督学习的案例研究(第 5.3.1 节和 5.3.2 节),并使用因果性来揭示深度学习的神秘面纱(第 5.3.3 节)。
“深度学习”一词广泛指代深度神经网络的运用。这是一种机器学习方法,通过在序列层中堆叠许多非线性模型,模拟大脑中神经元的连接。“深度”指的是堆叠许多层以实现更强的建模能力,尤其是在建模高维和非线性数据方面,如视觉媒体和自然语言文本。“深度”一词指的是堆叠许多层以达到更强的建模能力,尤其是在建模高维和非线性数据方面,如视觉媒体和自然语言文本。神经网络已经存在了一段时间,但相对较近的硬件和自动微分技术的进步使得深度神经网络可以扩展到极其大的规模。正是这种扩展使得近年来,深度学习在许多高级推理和决策任务上超越了人类,例如图像识别、自然语言处理、游戏、医疗诊断、自动驾驶以及生成逼真的文本、图像和视频。
但询问深度学习如何与因果性联系起来可能会得到令人沮丧的答案。AI 公司的首席执行官和大型科技公司的领导者们炒作深度学习模型的力量,甚至声称它们可以学习世界的因果结构。另一方面,一些领先的研究人员声称这些模型仅仅是“随机鹦鹉”,它们可以回声相关模式,尽管这些模式细微而复杂,但仍然不足以达到真正的因果理解。
本章的目标是调和这些观点。但跳过前面的内容,主要的收获是深度学习架构可以集成到因果模型中,我们可以使用深度学习训练技术来训练模型。但不仅如此,我们还可以使用因果推理来构建更好的深度学习模型,并改进我们的训练方法。
我们将通过两个案例研究来巩固这一想法:
-
使用变分自动编码器为计算机视觉构建因果 DAG
-
利用机制独立性实现更好的半监督学习
在本书的其余部分,你将看到的因果与 AI 相互作用的其他示例将建立在我们从这些案例研究中获得的直觉之上。例如,第九章将使用本章我们将构建的变分自动编码器来展示反事实推理。在第十一章中,我们将探讨因果效应推断的机器学习和概率深度学习方法。第十三章将展示如何结合大型语言模型和因果推理。
我们将首先考虑如何将深度学习融入因果模型中。
5.1 计算机视觉问题的因果模型
让我们看看我们可以用因果 DAG 来处理的一个计算机视觉问题。回忆一下第一章中的 MNIST 数据,由数字图像及其标签组成,如图 5.1 所示。

图 5.1 MNIST 数据,包括手写数字图像及其数字标签
有一个相关的数据集叫做 Typeface MNIST(TMNIST),它也包含数字图像及其数字标签。然而,与手写数字不同,图像是渲染在 2,990 种不同字体中的数字,如图 5.2 所示。对于每个图像,除了数字标签外,还有一个字体标签。字体标签的例子包括“GrandHotel-Regular”、“KulimPark-Regular”和“Gorditas-Bold”。

图 5.2 Typeface MNIST 的示例,它由不同字体的数字组成。除了每个数字的数字标签外,还有一个标签表示 2,990 种不同字体(字体)中的一种。
在这次分析中,我们将将这些数据集合并为一个,并在该数据上构建一个简单的深度因果生成模型。我们将“字体”标签简化为一个样本二进制标签,表示 MNIST 图像为“手写”,TMNIST 图像为“打字”。
我们已经看到了如何在 DAG 之上构建因果生成模型。我们将联合分布分解为表示每个节点条件概率分布的因果马尔可夫核的乘积,这些核在 DAG 中基于其父节点。在我们之前的 pgmpy 示例中,我们为这些核中的每一个都拟合了一个条件概率表。
你可以想象使用条件概率表来表示图像中像素的条件概率分布会有多困难。但是,没有任何阻止我们用深度神经网络来建模因果马尔可夫核,我们知道它足够灵活,可以处理像像素这样的高维特征。在本节中,我将演示如何使用深度神经网络来建模由因果 DAG 定义的因果马尔可夫核。
5.1.1 利用通用函数逼近器
深度学习是一种高度有效的通用函数逼近器。让我们想象有一个函数将一组输入映射到一组输出,但我们要么不知道这个函数,要么很难用数学或代码表达它。给定足够的输入和输出的例子,深度学习可以以高精度近似该函数。即使该函数是非线性和高维的,只要有足够的数据,深度学习将学会一个好的近似。
我们经常在因果建模和推理中使用函数,有时对它们进行近似是有意义的,只要这些近似保留了我们关心的因果信息。例如,因果马尔可夫性质使我们感兴趣的是将因果有向图中节点父节点的值映射到该节点值(或概率值)的函数。
在本节中,我们将使用变分自编码器(VAE)框架来执行节点与其父节点之间的映射。在 VAE 中,我们将训练两个深度神经网络,其中一个将父原因变量映射到结果变量的分布,另一个将结果变量映射到原因变量的分布。这个例子将展示在因果是非线性和高维时深度学习的应用;效应变量将是一个表示为高维数组的图像,原因变量将代表图像的内容。
5.1.2 因果抽象和板模型
但构建图像的因果模型意味着什么呢?图像由排列成网格的像素组成。作为数据,我们可以将像素网格表示为数值矩阵,这些数值对应于颜色。在 MNIST 和 TMNIST 的情况下,图像是一个 28 × 28 的灰度值矩阵,如图 5.3 所示。

图 5.3 展示了“6”的 MNIST 图像(左侧)和“7”的 TMNIST 图像。在它们的原始形式中,这些是 28 × 28 的数值矩阵,对应于灰度值。
一个典型的机器学习模型将这个 28 × 28 的像素矩阵视为 784 个特征。机器学习算法学习将像素及其标签连接起来的统计模式。基于这个事实,人们可能会倾向于将每个单独的像素视为朴素因果有向图中的一个节点,如图 5.4 所示,为了视觉上的简单,我画了 16 个像素(一个任意数)而不是所有的 784 个。

图 5.4 一个由 4 × 4 矩阵表示的图像的朴素因果有向图可能看起来是什么样子
在图 5.4 中,存在从数字和手写变量到每个像素的边缘。此外,还有一些表示像素之间可能因果关系的边缘示例。像素之间的因果边缘意味着一个像素的颜色是另一个像素的原因。也许这些关系大多数都在节点之间,只有少数是远距离的。但我们如何知道一个像素是否导致另一个像素?如果两个像素相连,我们如何知道因果关系的方向?
在正确的抽象层次上工作
仅用 16 个像素之间的这些连接,图 5.4 中的朴素 DAG 就已经相当难以处理了。如果用 784 个像素,情况会更糟。除了 DAG 的难以处理之外,像素级模型的问题在于我们的因果问题通常不是在像素级别——我们可能永远不会问“这个像素对这个像素的因果效应是什么?”换句话说,像素的抽象层次太低,这就是为什么考虑单个像素之间的因果关系感觉有点荒谬。
在应用统计学领域,如计量经济学、社会科学、公共卫生和商业,我们的数据有诸如人均收入、收入、位置、年龄等变量。这些变量通常已经是我们获取数据时想要思考的抽象层次。但现代机器学习专注于从原始媒体(如图像、视频、文本和传感器数据)中提取的许多感知问题。我们通常不希望在低级特征上进行因果推理。我们的因果问题通常关于这些低级特征背后的高级抽象。我们需要在这些更高抽象层次上建模。
我们不会考虑单个像素,而是会考虑整个图像。我们将定义一个变量X来表示图像的外观;即,X是一个表示像素的矩阵随机变量。图 5.5 说明了 TMNIST 案例的因果 DAG。简单来说,数字(0-9)和字体(2,990 种可能值)是原因,图像是结果。

图 5.5 表示 Typeface MNIST 背后隐含 DGP 的简单因果 DAG
在这种情况下,我们使用因果 DAG 来做出一个断言,即标签导致图像。这并不总是情况,正如我们将在 5.3 节讨论的半监督学习案例研究中讨论的那样。与所有因果模型一样,它取决于一个领域内的数据生成过程(DGP)。
为什么说数字导致了图像?
柏拉图的洞穴寓言描述了一群人,他们一生都在洞穴中生活,从未见过世界。他们面对一个空白的洞穴墙壁,并观看从他们身后火堆前经过的物体在墙壁上投射的影子。这些影子是真实物体经过火堆前的简化且有时扭曲的表示。在这种情况下,我们可以认为物体的形式是影子的原因。
类似地,数字标签的真实形式导致图像中的表示。MNIST 图像是由人们书写的,他们心中有一个想要渲染到纸上的数字的柏拉图理想。在这个过程中,这个理想受到手的运动变化、纸张的角度、笔在纸上的摩擦以及其他因素的影响——渲染的图像是那个“理想”的“影子”。
这个想法与计算机视觉中称为“视觉作为逆图形”的概念相关(有关更多信息,请参阅www.altdeep.ai/p/causalaibook)。在因果的术语中,要点是当我们分析从环境原始信号渲染的图像时,如果任务是推断导致这些信号的实际物体或事件,因果性从这些物体或事件流向信号。推断任务是使用观察到的信号(洞穴墙壁上的影子)来推断原因的性质(火堆前的物体)。
话虽如此,图像也可以是原因。例如,如果你正在模拟人们在移动应用中看到图像后的行为(例如,他们是否“点击”、“点赞”或“向左滑动”),你可以将图像建模为导致该行为的原因。
板模型
在我们的 TMNIST 数据中建模 2,990 种字体对于我们的目的来说过于冗余。相反,我将这些数据集合并为一个——一半来自 MNIST,一半来自 Typeface MNIST。除了“数字”标签外,我还要有一个简单的二进制标签,称为“is-handwritten”,对于 MNIST 的手写数字图像为 1(真实),对于 TMNIST 的“打字”数字图像为 0(假)。我们可以修改我们的因果 DAG 以获得图 5.6。

图 5.6 表示 MNIST 和 TMNIST 数据组合的因果 DAG,其中“is-handwritten”为 1(MNIST 图像)或 0(TMNIST 图像)
板模型是一种在概率机器学习中使用的可视化技术,它提供了一种在保留低级维度细节的同时,可视化高级抽象的绝佳方法。板符号是一种在 DAG(例如,图 5.4 中的X[1]到X[16])中视觉表示重复变量的方法——在我们的情况下,我们有像素的重复。
我们不是将每个 784 个像素作为单独的节点来绘制,而是使用矩形或“板”将重复变量分组到子图中。然后我们在板上写一个数字来表示板上实体的重复次数。板可以嵌套在另一个板中,以表示嵌套在重复实体中的重复实体。每个板都有一个字母下标,用于索引该板上的元素。图 5.7 中的因果 DAG 代表一个图像。

图 5.7 展示了因果 DAG 的板模型表示。板代表重复变量,在这种情况下是 28 × 28 = 784 像素。X**[j]是第j个像素。
在训练过程中,我们将有一大批训练图像。接下来,我们将修改 DAG 以捕获训练数据中的所有图像。
5.2 训练神经因果模型
为了训练我们的神经因果模型,我们需要加载数据并准备,创建我们模型的架构,编写训练过程,并实现一些评估训练进展的工具。我们将首先加载数据并准备。
5.2.1 设置训练数据
我们的训练数据包含N个示例图像,因此我们需要我们的板模型来表示训练数据中的所有N个图像,一半是手写的,一半是打印的。我们将在图 5.8 中添加另一个板,对应于重复N组图像和标签。

图 5.8 展示了包含数据中N个图像的因果模型,并添加了一个额外的板。
现在我们有一个因果 DAG,它展示了我们希望达到的因果抽象水平以及我们开始训练模型中的神经网络所需的维度信息。
让我们先加载 Pyro 和其他一些库,并设置一些超参数。
设置你的环境
这段代码使用 Python 3.10.12 版本编写,并在 Google Colab 中进行测试。主要库的版本包括 Pyro (pyro-ppl) 1.8.4、torch 2.2.1、torchvision 0.18.0+cu121 和 pandas 2.0.3。我们还将使用 matplotlib 进行绘图。
访问www.altdeep.ai/p/causalaibook获取一个笔记本的链接,该笔记本将在 Google Colab 中加载。
如果您的设备上有 GPU,使用 CUDA(一个在 GPU 上并行计算的平台)训练神经网络将更快。我们将运行一些代码来切换它。如果您没有 GPU 或者不确定是否有,请将USE_CUDA设置为False。
列表 5.1 设置 GPU 训练
import torch
USE_CUDA = False #1
DEVICE_TYPE = torch.device("cuda" if USE_CUDA else "cpu")
1 如果可用,使用 CUDA。
首先,我们将创建一个Dataset类的子类(用于加载数据和预处理数据的类),这将使我们能够结合 MNIST 和 TMNIST 数据集。
列表 5.2 合并数据
from torch.utils.data import Dataset
import numpy as np
import pandas as pd
from torchvision import transforms
class CombinedDataset(Dataset): #1
def __init__(self, csv_file):
self.dataset = pd.read_csv(csv_file)
def __len__(self):
return len(self.dataset)
def __getitem__(self, idx):
images = self.dataset.iloc[idx, 3:] #2
images = np.array(images, dtype='float32')/255\. #2
images = images.reshape(28, 28) #2
transform = transforms.ToTensor() #2
images = transform(images) #2
digits = self.dataset.iloc[idx, 2] #3
digits = np.array([digits], dtype='int') #3
is_handwritten = self.dataset.iloc[idx, 1] #4
is_handwritten = np.array([is_handwritten], dtype='float32') #4
return images, digits, is_handwritten #5
1 此类加载并处理结合 MNIST 和 Typeface MNIST 的数据集。输出是一个 torch.utils.data.Dataset 对象。
2 加载、归一化和重塑图像为 28 × 28 像素。
3 获取和处理数字标签,0–9。
4 1 对于手写数字(MNIST),以及 0 对于“输入”数字(TMNIST)
5 返回一个包含图像、数字标签和 is_handwritten 标签的元组。
接下来,我们将使用 DataLoader 类(它允许在训练期间高效地进行数据迭代和批处理)从 GitHub 中的 CSV 文件加载数据,并将其分为训练集和测试集。
列表 5.3 下载、分割和加载数据
from torch.utils.data import DataLoader
from torch.utils.data import random_split
def setup_dataloaders(batch_size=64, use_cuda=USE_CUDA): #1
combined_dataset = CombinedDataset(
"https://raw.githubusercontent.com/altdeep/causalML/master/datasets
↪/combined_mnist_tmnist_data.csv"
)
n = len(combined_dataset) #2
train_size = int(0.8 * n) #2
test_size = n - train_size #2
train_dataset, test_dataset = random_split( #2
combined_dataset, #2
[train_size, test_size], #2
generator=torch.Generator().manual_seed(42) #2
) #2
kwargs = {'num_workers': 1, 'pin_memory': use_cuda} #2
train_loader = DataLoader( #3
train_dataset, #3
batch_size=batch_size, #3
shuffle=True, #3
**kwargs #3
) #3
test_loader = DataLoader( #3
test_dataset, #3
batch_size=batch_size, #3
shuffle=True, #3
**kwargs #3
) #3
return train_loader, test_loader
1 设置数据加载器,用于加载数据并将其分为训练集和测试集。
2 将 80% 的数据分配给训练数据,剩余的 20% 分配给测试数据。
3 创建训练和测试加载器。
接下来,我们将设置完整的变分自编码器。
5.2.2 设置变分自编码器
变分自编码器(VAE)可能是最简单的深度概率机器学习建模方法。在将 VAE 应用于图像的典型设置中,我们引入一个比图像数据维度小的潜在连续变量 Z。在这里,维度指的是数据向量表示中的元素数量。例如,我们的图像是一个 28 × 28 的像素矩阵,或者也可以是一个维度为 28 × 28 = 784 的向量。由于比图像维度小得多,潜在变量 Z 代表了图像信息的压缩编码。对于数据集中的每个图像,都有一个相应的潜在 Z 值,它代表了该图像的编码。这种设置如图 5.9 所示。

图 5.9 因果 DAG 盘模型,扩展以包括一个“编码”变量 Z。在训练期间,该变量是潜在的,由虚线表示。(在模型部署后,digit 和 is-handwritten 也都是潜在的)。
Z 在因果 DAG 中表现为一个新的父节点,但需要注意的是,经典的 VAE 框架并没有将 Z 定义为因果的。现在我们正在进行因果思考,我们将 Z 给予因果解释。具体来说,作为 DAG 中图像节点的父节点,我们将 digit 和 is-handwritten 视为图像中我们所看到的因果驱动因素。然而,图像中还有其他元素(例如,手写字符的笔画粗细或输入字符的字体)也是我们所看到的图像的因果因素。我们将 Z 视为所有这些我们未明确建模的图像因果因素的连续潜在“替身”,例如 digit 和 is-handwritten。这些因果因素的例子包括 TMNIST 标签中各种字体的细微差别以及由于不同作者和书写时的运动方式而导致的书写数字的所有变化。考虑到这一点,我们可以将 P(X| digit, is-handwritten, Z) 视为 X 的因果马尔可夫核。话虽如此,重要的是要记住,我们为 Z 学习到的表示是潜在因果的替身,并不等同于学习实际的潜在因果。
VAE 设置将训练两个深度神经网络:一个称为“编码器”,它将图像编码为 Z 的一个值。另一个神经网络,称为“解码器”,将与我们的 DAG 对齐。解码器将从 digit 标签、is-handwritten 标签和 Z 值生成图像,如图 5.10 所示。
解码器的作用像一个渲染引擎;给定一个 Z 编码值和 digit 以及 is-handwritten 的值,它将渲染一个图像。

图 5.10 解码器神经网络从输入 Z 和标签 is-handwritten 以及 digit 生成输出图像 X。与任何神经网络一样,输入通过一个或多个“隐藏层”进行处理。
到目前为止的关键 VAE 概念
变分自编码器 (VAE)——深度生成建模中的一种流行框架。我们正在使用它来模拟因果马尔可夫核中的因果模型。
解码器——我们使用解码器作为因果马尔可夫核的模型。它将观察到的原因 is-handwritten 和 digit 以及潜在变量 Z 映射到我们的图像结果变量 X。
这种 VAE 方法允许我们使用神经网络,类似于解码器,来捕捉建模图像作为由 digit 和 is-handwritten 造成的效应所需的复杂和非线性关系。使用我们之前讨论过的条件概率表和其他简单的因果马尔可夫核参数化来建模图像将是困难的。
首先,让我们实现解码器。我们将传递 z_dim 作为 Z 的维度和 hidden_dim 作为隐藏层(宽度)的维度。当我们实例化完整的 VAE 时将指定这些变量。解码器将潜在向量 Z 与额外的输入相结合——代表 digit 的变量和 is-handwritten(一个指示数字是否手写的二进制指示符)。它将生成一个 784 维的输出向量,代表大小为 28 × 28 像素的图像。这个输出向量包含每个像素的伯努利分布的参数,本质上是对每个像素“开启”的可能性的建模。该类使用两个全连接层(fc1 和 fc2),并采用 Softplus 和 Sigmoid “激活函数”,这是神经网络模拟神经元的特点。
列表 5.4 实现解码器
from torch import nn
class Decoder(nn.Module): #1
def __init__(self, z_dim, hidden_dim):
super().__init__()
img_dim = 28 * 28 #2
digit_dim = 10 #3
is_handwritten_dim = 1 #4
self.softplus = nn.Softplus() #5
self.sigmoid = nn.Sigmoid() #5
encoding_dim = z_dim + digit_dim + is_handwritten_dim #6
self.fc1 = nn.Linear(encoding_dim, hidden_dim) #6
self.fc2 = nn.Linear(hidden_dim, img_dim) #7
def forward(self, z, digit, is_handwritten): #8
input = torch.cat([z, digit, is_handwritten], dim=1) #9
hidden = self.softplus(self.fc1(input)) #10
img_param = self.sigmoid(self.fc2(hidden)) #11
return img_param
1 用于 VAE 中的解码器的类
2 图像大小为 28 × 28 像素。
3 数字是 0-9 的一热编码数字,即长度为 10 的向量。
4 一个指示数字是否手写的指示符,大小为 1
5 Softplus 和 sigmoid 是在层之间映射时使用的非线性变换(激活函数)。
6 fc1 是一个线性函数,它将 Z 向量、数字和 is_handwritten 映射到一个线性输出,该输出通过 softplus 激活函数传递,创建一个隐藏层——其长度由 hidden_layer 给出。
7 fc2 线性地将隐藏层映射到传递给 sigmoid 函数的输出。得到的值介于 0 和 1 之间。
8 定义从潜在变量 Z 的值到生成变量 X 的值的正向计算。
9 结合 Z 和标签。
10 计算隐藏层。
11 将隐藏层传递给线性变换,然后传递给 sigmoid 变换以输出长度为 784 的参数向量。向量的每个元素对应于图像像素的伯努利参数值。
我们在因果模型中使用解码器。我们的因果 DAG 作为因果概率机器学习模型的支架,该模型在解码器的帮助下定义了关于{is-handwritten,digit,X,Z}的联合概率分布,其中Z是潜在的。我们可以使用该模型来计算给定Z值的训练数据的似然。
潜在变量z,以一位热向量digit表示的数字身份,以及二进制指示符is_handwritten被建模为来自标准分布的样本。然后这些变量被输入到解码器中,以产生表示图像单个像素概率的伯努利分布的参数(img_param)。
注意,使用伯努利分布来模拟像素是一种折衷的方法。像素不是二进制的黑白结果——它们有灰度值。dist.enable_validation(False)这一行让我们通过解码器的img_param输出为图像获得伯努利对数似然而作弊。
以下模型代码是 PyTorch 神经网络模块的一个类方法。我们稍后会看到整个类。
列表 5.5 因果模型
import pyro
import pyro.distributions as dist
dist.enable_validation(False) #1
def model(self, data_size=1): #2
pyro.module("decoder", self.decoder) #2
options = dict(dtype=torch.float32, device=DEVICE_TYPE)
z_loc = torch.zeros(data_size, self.z_dim, **options) #3
z_scale = torch.ones(data_size, self.z_dim, **options) #3
z = pyro.sample("Z", dist.Normal(z_loc, z_scale).to_event(1)) #3
p_digit = torch.ones(data_size, 10, **options)/10 #4
digit = pyro.sample( #4
"digit", #4
dist.OneHotCategorical(p_digit) #4
) #4
p_is_handwritten = torch.ones(data_size, 1, **options)/2 #5
is_handwritten = pyro.sample( #5
"is_handwritten", #5
dist.Bernoulli(p_is_handwritten).to_event(1) #5
) #5
img_param = self.decoder(z, digit, is_handwritten) #6
img = pyro.sample("img", dist.Bernoulli(img_param).to_event(1)) #7
return img, digit, is_handwritten
1 禁用分布验证使 Pyro 能够在像素不是二进制值的情况下计算对数似然。
2 单个图像的模型。在方法内部,我们将解码器,一个 PyTorch 模块,注册到 Pyro。这使 Pyro 了解解码器网络内部的参数。
3 我们对 Z、数字和 is_handwritten 的联合概率进行建模,每个都从标准分布中进行采样。我们从具有位置参数 z_loc(所有为零)和尺度参数 z_scale(所有为一)的多变量正态分布中采样 Z。
4 我们还从一位热分类分布中采样数字。每个数字被分配相同的概率。
5 我们同样从伯努利分布中采样 is_handwritten 变量。
6 解码器将数字、is_handwritten 和 Z 映射到一个概率参数向量。
7 参数向量被传递到伯努利分布,该分布模拟数据中的像素值。像素在技术上不是伯努利二元变量,但我们将放宽这个假设。
前面的model方法表示一个图像的 DGP。以下列表中的training_model方法将model方法应用于训练数据中的N个图像。
列表 5.6 应用model到数据中N个图像的方法
def training_model(self, img, digit, is_handwritten, batch_size): #1
conditioned_on_data = pyro.condition( #2
self.model,
data={
"digit": digit,
"is_handwritten": is_handwritten,
"img": img
}
)
with pyro.plate("data", batch_size): #3
img, digit, is_handwritten = conditioned_on_data(batch_size)
return img, digit, is_handwritten
1 模型表示一个图像的 DGP。training_model方法将此模型应用于训练数据中的 N 个图像。
2 现在我们将模型条件化在训练数据中的证据上。
3 此上下文管理器代表图 5.9 中数据中重复的独立同分布(IID)示例的 N 大小板。在这种情况下,N 是批大小。它像一个 for 循环,遍历批处理中的每个数据单元。
我们的概率机器学习模型建模了联合分布{Z, X, digit, is-handwritten}。但由于Z是潜在的,该模型将需要学习P(Z|X, digit, is-handwritten)。鉴于我们使用解码器神经网络从Z和标签到X,给定X和标签的Z的分布将是复杂的。我们将使用变分推断,这是一种技术,我们首先定义一个近似分布Q(Z|X, digit, is-handwritten),并尝试使该分布尽可能接近P(Z|X, digit, is-handwritten)。
近似分布的主要成分是 VAE 框架中的第二个神经网络,即编码器,如图 5.11 所示。编码器将训练数据中的观察图像及其标签映射到潜在Z变量。

图 5.11 编码器将实际图像作为输入映射到潜在Z变量作为输出。
编码器负责将图像中的信息压缩到低维编码。
到目前为止的关键 VAE 概念
变分自动编码器(VAE)—深度生成建模中流行的框架。我们正在使用它来模拟因果马尔可夫核。
解码器—我们将解码器用作因果马尔可夫核的模型。它将观察到的原因is-handwritten和digit,以及潜在变量Z,映射到我们的图像结果变量X。
编码器—编码器将图像、digit和is-handwritten指示符映射到分布的参数,我们可以从中抽取Z的样本。
在以下代码中,编码器将图像、数字标签和is-handwritten指示符作为输入。这些输入被连接并通过一系列具有 Softplus 激活函数的全连接层传递。编码器的最终输出由两个向量组成,分别代表潜在空间分布Z上的位置(z_loc)和尺度(z_scale)参数,给定观察到的image(img)、digit(digit)和is-handwritten(is_handwritten)值。
列表 5.7 实现编码器
class Encoder(nn.Module): #1
def __init__(self, z_dim, hidden_dim):
super().__init__()
img_dim = 28 * 28 #2
digit_dim = 10 #3
is_handwritten_dim = 1
self.softplus = nn.Softplus() #4
input_dim = img_dim + digit_dim + is_handwritten_dim #5
self.fc1 = nn.Linear(input_dim, hidden_dim) #5
self.fc21 = nn.Linear(hidden_dim, z_dim) #6
self.fc22 = nn.Linear(hidden_dim, z_dim) #6
def forward(self, img, digit, is_handwritten): #7
input = torch.cat([img, digit, is_handwritten], dim=1) #8
hidden = self.softplus(self.fc1(input)) #9
z_loc = self.fc21(hidden) #10
z_scale = torch.exp(self.fc22(hidden)) #10
return z_loc, z_scale
1 编码器是 PyTorch 模块的一个实例。
2 输入图像是 28 × 28 = 784 像素。
3 数字维度为 10。
4 在编码器中,我们只使用 softplus 变换(激活函数)。
5 线性变换 fc1 与 softplus 结合,将 784 维像素向量、10 维数字标签向量和 2 维 is_handwritten 向量映射到隐藏层。
6 线性变换 fc21 和 fc22 将与 softplus 结合,将隐藏向量映射到 Z 的向量空间。
7 定义从观察到的 X 变量值到潜在 Z 变量值的反向计算。
8 将图像向量、数字标签和 is_handwritten 标签合并为一个输入。
9 将输入映射到隐藏层。
10 VAE 框架将从近似 P(Z|img, digit, is_handwritten)的正态分布中采样 Z。最终的转换将隐藏层映射到该正态分布的位置和尺度参数。
编码器的输出产生Z上的分布的参数。在训练过程中,给定一个图像及其标签(is-handwritten和digit),我们希望得到一个良好的Z值,因此我们编写了一个指导函数,该函数将使用编码器来采样Z的值。
列表 5.8 指导函数
def training_guide(self, img, digit, is_handwritten, batch_size): #1
pyro.module("encoder", self.encoder) #2
options = dict(dtype=torch.float32, device=DEVICE_TYPE)
with pyro.plate("data", batch_size): #3
z_loc, z_scale = self.encoder(img, digit, is_handwritten) #4
normal_dist = dist.Normal(z_loc, z_scale).to_event(1) #4
z = pyro.sample("Z", normal_dist) #5
1 training_guide 是 VAE 的一个方法,它将使用编码器。
2 注册编码器,使 Pyro 了解其权重参数。
3 这是迭代批处理数据的相同 plate 上下文管理器,我们在 training_model 中看到。
4 使用编码器将图像及其标签映射到正态分布的参数。
5 从该正态分布中采样 Z。
我们将这些元素组合成一个表示 VAE 的 PyTorch 神经网络模块。我们将Z的潜在维度初始化为 50。我们将编码器和解码器的隐藏层维度都设置为 400。这意味着对于图像的维度 28 × 28,二进制is-handwritten为 1,以及 one-hot 编码的digit变量为 10,我们将一个 28 × 28 + 1 + 10 = 795 维的特征向量压缩到 400 维的隐藏层,然后将其压缩到Z的多变量正态(高斯)分布的 50 维位置和尺度参数。解码器将digit、is-handwritten和Z的值作为输入,并将这些映射到 400 维的隐藏层和 28 × 28 维的图像。这些关于潜在变量维度、层数、激活函数和隐藏层维度的架构选择取决于问题,通常是通过惯例或通过尝试不同的值来选择的。
现在,我们将这些部分组合成完整的 VAE 类。
列表 5.9 完整的 VAE 类
class VAE(nn.Module):
def __init__(
self,
z_dim=50, #1
hidden_dim=400, #2
use_cuda=USE_CUDA,
):
super().__init__()
self.use_cuda = use_cuda
self.z_dim = z_dim
self.hidden_dim = hidden_dim
self.setup_networks()
def setup_networks(self): #3
self.encoder = Encoder(self.z_dim, self.hidden_dim)
self.decoder = Decoder(self.z_dim, self.hidden_dim)
if self.use_cuda:
self.cuda()
model = model #4
training_model = training_model #4
training_guide = training_guide #4
1 将潜在维度设置为 50。
2 将隐藏层设置为 400 维。
3 设置编码器和解码器。
4 添加模型、训练模型和训练指南的方法。
在指定了 VAE 之后,我们现在可以继续进行训练。
5.2.3 训练过程
当编码器可以将图像编码为潜在值Z,然后解码成图像的重构版本时,我们知道我们有一个好的生成模型。我们可以在训练数据中最小化重构误差——原始图像和重构图像之间的差异。
对“变分推断”训练算法的一些看法
在本节中,您将看到许多与变分推断相关的术语,这是我们用于训练的算法。了解我们为什么使用这个算法是有帮助的。对于拟合神经网络权重和其他参数以及因果推断,有许多统计估计量和算法。其中之一就是变分推断。
为了明确起见,变分推断不是一个“因果”概念。它只是另一种概率推断算法。在这本书中,我更倾向于这种推断算法,因为它即使在 DAG 中的变量在训练数据中是潜在的情况下也能很好地扩展,并且可以与深度神经网络一起工作,利用像 PyTorch 这样的深度学习框架。这为对更丰富的模态(如文本、图像、视频等)进行因果推理打开了大门,而传统的因果推断估计器是为数值数据开发的。此外,我们可以根据不同的问题调整这种方法(参见第一章中关于“推断的商品化”的讨论),并在推断过程中利用领域知识(例如,通过在指南中使用条件独立性的知识)。最后,变分推断的核心概念出现在许多深度生成建模方法中(如潜在扩散模型)。
在实践中,仅仅最小化重建误差会导致过拟合和其他问题,因此我们将选择一种概率方法:给定一个图像,我们将使用我们的指南函数从P(Z|image, is-handwritten, digi**t)中采样一个Z的值。然后我们将该值插入到我们模型的解码器中,输出参数化P(image|is-handwritten, digit, Z)。我们最小化重建误差的概率方法优化了编码器和解码器,以便我们最大化Z相对于P(Z|image, is-handwritten, digit)的似然性,以及原始图像相对于P(image|is-handwritten, digit, Z)的似然性。
但通常我们无法直接从分布P(Z|image, is-handwritten, digit)中采样或获取似然性。因此,我们的指南函数试图近似它。指南代表一个变分分布,表示为Q(Z|X, is-handwritten, digit)。编码器权重的变化代表变分分布的移动。训练将优化编码器的权重,使变分分布向P(Z|image, is-handwritten, digit)移动。这种训练方法被称为变分推断,它通过最小化两个分布之间的Kullback–Leibler 散度(KL 散度)来实现;KL 散度是一种量化两个分布差异的方法。
我们的可变推断过程优化了一个称为 ELBO 的量,它表示 数据的对数似然期望下界。间接最小化负 ELBO 损失可以最小化重建误差和 Q(Z|…) 与 P(Z|…) 之间的 KL 散度。Pyro 在一个名为 Trace_ELBO 的实用工具中实现了 ELBO。
我们的程序将使用 随机 可变推断 (SVI),这意味着使用与随机选择的数据子集(或“批次”)一起工作的训练程序进行可变推断,而不是整个数据集,这可以减少内存使用并有助于扩展到更大的数据。
到目前为止的关键 VAE 概念
变分自动编码器 (VAE)—深度生成模型中的一种流行框架。我们正在使用它来模拟因果模型中的因果马尔可夫核。
解码器—我们将解码器作为因果马尔可夫核的模型。它将观察到的原因 is-handwritten 和 digit,以及潜在变量 Z,映射到我们的图像结果变量 X。
编码器—编码器将 图像、digit 和 is-handwritten 映射到我们可以从中抽取 Z 样本的分布的参数。
引导函数—在训练过程中,我们希望 Z 的值代表一个图像,给定 is-handwritten 和 digit;即,我们希望从 P(Z|image,is-handwritten,digit) 中生成 Z。但我们不能直接从这个分布中采样。因此,我们编写了一个 引导函数,该函数使用编码器和方便的正态分布等标准分布来采样 Z 的值。
变分分布—引导函数表示一个称为 变分分布 的分布,表示为 Q(Z|image,is-handwritten,digit)。在推理过程中,我们希望以代表 P(Z|image,is-handwritten,digit) 的方式从 Q(Z|…) 中采样。
变分推断—这是寻求最大化 Q(Z|…) 和 P(Z|…) 之间接近度的训练过程,以便从 Q(Z|…) 中采样的样本代表 P(Z|…)(例如,通过最小化 KL 散度)。
随机变分推断 (SVI)—变分推断,其中训练依赖于随机选择的数据子集,而不是整个数据,以便使训练更快、更可扩展。
在我们开始之前,我们将创建一个用于绘制图像的辅助函数,这样我们就可以在训练过程中看到我们的进展情况。
列表 5.10 绘制图像的辅助函数
def plot_image(img, title=None): #1
fig = plt.figure()
plt.imshow(img.cpu(), cmap='Greys_r', interpolation='nearest')
if title is not None:
plt.title(title)
plt.show()
1 用于绘制图像的辅助函数
接下来,我们将创建一个 reconstruct_img 辅助函数,它将根据其标签 重建 一个图像,其中“重建”意味着将图像编码到潜在表示中,然后解码潜在表示回到图像。然后我们可以比较原始图像及其重建,以查看编码器和解码器训练得有多好。我们将创建一个 compare_images 函数来进行该比较。
列表 5.11 定义一个用于重建和查看图像的辅助函数
import matplotlib.pyplot as plt
def reconstruct_img(vae, img, digit, is_hw, use_cuda=USE_CUDA): #1
img = img.reshape(-1, 28 * 28)
digit = F.one_hot(torch.tensor(digit), 10)
is_hw = torch.tensor(is_hw).unsqueeze(0)
if use_cuda:
img = img.cuda()
digit = digit.cuda()
is_hw = is_hw.cuda()
z_loc, z_scale = vae.encoder(img, digit, is_hw)
z = dist.Normal(z_loc, z_scale).sample()
img_expectation = vae.decoder(z, digit, is_hw)
return img_expectation.squeeze().view(28, 28).detach()
def compare_images(img1, img2): #2
fig = plt.figure()
ax0 = fig.add_subplot(121)
plt.imshow(img1.cpu(), cmap='Greys_r', interpolation='nearest')
plt.axis('off')
plt.title('original')
ax1 = fig.add_subplot(122)
plt.imshow(img2.cpu(), cmap='Greys_r', interpolation='nearest')
plt.axis('off')
plt.title('reconstruction')
plt.show()
1 给定一个输入图像,此函数通过编码器然后通过解码器传递它来重建图像。
2 将两个图像并排绘制以进行比较。
接下来,我们将创建一些处理数据的辅助函数。我们将使用get_random_example从数据集中获取随机图像。reshape_data函数将图像及其标签转换为编码器的输入。我们将使用generate_data和generate_coded_data从模型中模拟图像。
列表 5.12 训练数据处理的辅助函数
import torch.nn.functional as F
def get_random_example(loader): #1
random_idx = np.random.randint(0, len(loader.dataset)) #1
img, digit, is_handwritten = loader.dataset[random_idx] #1
return img.squeeze(), digit, is_handwritten #1
def reshape_data(img, digit, is_handwritten): #2
digit = F.one_hot(digit, 10).squeeze() #2
img = img.reshape(-1, 28*28) #2
return img, digit, is_handwritten #2
def generate_coded_data(vae, use_cuda=USE_CUDA): #3
z_loc = torch.zeros(1, vae.z_dim) #3
z_scale = torch.ones(1, vae.z_dim) #3
z = dist.Normal(z_loc, z_scale).to_event(1).sample() #3
p_digit = torch.ones(1, 10)/10 #3
digit = dist.OneHotCategorical(p_digit).sample() #3
p_is_handwritten = torch.ones(1, 1)/2 #3
is_handwritten = dist.Bernoulli(p_is_handwritten).sample() #3
if use_cuda: #3
z = z.cuda() #3
digit = digit.cuda() #3
is_handwritten = is_handwritten.cuda() #3
img = vae.decoder(z, digit, is_handwritten) #3
return img, digit, is_handwritten #3
def generate_data(vae, use_cuda=USE_CUDA): #4
img, digit, is_handwritten = generate_coded_data(vae, use_cuda) #4
img = img.squeeze().view(28, 28).detach() #4
digit = torch.argmax(digit, 1) #4
is_handwritten = torch.argmax(is_handwritten, 1) #4
return img, digit, is_handwritten #4
1 从数据集中选择一个随机示例。
2 重新塑形数据。
3 生成已编码的数据。
4 生成(未编码)数据。
最后,我们可以运行训练过程。首先,我们将设置随机变分推理。我们首先设置一个 Adam 优化器的实例,该实例将处理training_guide中的参数优化。然后我们将training_model、training_guide、优化器和 ELBO 损失函数传递给 SVI 构造函数以获取 SVI 实例。
列表 5.13 设置训练过程
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam
pyro.clear_param_store() #1
vae = VAE() #2
train_loader, test_loader = setup_dataloaders(batch_size=256) #3
svi_adam = Adam({"lr": 1.0e-3}) #4
model = vae.training_model #5
guide = vae.training_guide #5
svi = SVI(model, guide, svi_adam, loss=Trace_ELBO()) #5
1 清除指南内存中参数的任何值。
2 初始化 VAE。
3 加载数据。
4 初始化优化器。
5 初始化 SVI 损失计算器。损失负“期望下界”(ELBO)。
在训练生成模型时,设置一个使用测试数据来评估训练进展的流程是有用的。你可以包括你认为在训练期间有用的任何内容。在这里,我计算并打印测试数据上的损失函数,只是为了确保测试损失随着训练损失的逐渐降低而逐渐降低(测试损失平坦化而训练损失继续降低将表明过拟合)。
确定我们的模型训练得有多好的一种更直接的方法是生成并查看图像。在我的测试评估过程中,我生成了两个可视化。首先,我检查它从测试数据中重建随机图像的能力。我将图像通过编码器然后通过解码器传递,创建了一个“重建”的图像。然后我将原始图像和重建图像并排绘制并比较它们,看它们是否非常相似。
接下来,我通过从头生成并绘制图像来可视化它作为一个整体生成模型的性能。每次运行一定数量的 epoch 时,我都会运行此代码一次。
列表 5.14 设置测试评估过程
def test_epoch(vae, test_loader):
epoch_loss_test = 0 #1
for img, digit, is_hw in test_loader: #1
batch_size = img.shape[0] #1
if USE_CUDA: #1
img = img.cuda() #1
digit = digit.cuda() #1
is_hw = is_hw.cuda() #1
img, digit, is_hw = reshape_data( #1
img, digit, is_hw #1
) #1
epoch_loss_test += svi.evaluate_loss( #1
img, digit, is_hw, batch_size #1
) #1
test_size = len(test_loader.dataset) #1
avg_loss = epoch_loss_test/test_size #1
print("Epoch: {} avg. test loss: {}".format(epoch, avg_loss)) #1
print("Comparing a random test image to its reconstruction:") #2
random_example = get_random_example(test_loader) #2
img_r, digit_r, is_hw_r = random_example #2
img_recon = reconstruct_img(vae, img_r, digit_r, is_hw_r) #2
compare_images(img_r, img_recon) #2
print("Generate a random image from the model:") #3
img_gen, digit_gen, is_hw_gen = generate_data(vae) #3
plot_image(img_gen, "Generated Image") #3
print("Intended digit: ", int(digit_gen)) #3
print("Intended as handwritten: ", bool(is_hw_gen == 1)) #3
1 计算并打印测试损失。
2 将随机测试图像与其重建图像进行比较。
3 从模型中生成一个随机图像。
现在我们将运行训练。对于单个 epoch,我们将从训练数据加载器中迭代地获取一批数据并将其传递到 step 方法中,运行一个训练步骤。在一定的 epoch 数(由TEST_FREQUENCY设置)之后,我们将使用辅助函数将随机图像与其重建图像进行比较,以及从头模拟图像并绘制它。
列表 5.15 运行训练和绘制进度
NUM_EPOCHS = 2500
TEST_FREQUENCY = 10
train_loss = []
train_size = len(train_loader.dataset)
for epoch in range(0, NUM_EPOCHS+1): #1
loss = 0
for img, digit, is_handwritten in train_loader:
batch_size = img.shape[0]
if USE_CUDA:
img = img.cuda()
digit = digit.cuda()
is_handwritten = is_handwritten.cuda()
img, digit, is_handwritten = reshape_data(
img, digit, is_handwritten
)
loss += svi.step( #2
img, digit, is_handwritten, batch_size #2
) #2
avg_loss = loss / train_size
print("Epoch: {} avgs training loss: {}".format(epoch, loss))
train_loss.append(avg_loss)
if epoch % TEST_FREQUENCY == 0: #3
test_epoch(vae, test_loader) #3
1 运行一定数量的 epoch 的训练过程。
2 在一个 epoch 中对一个批次运行一个训练步骤。
3 测试数据评估过程每 10 个 epoch 运行一次。
再次,参见www.altdeep.ai/p/causalaibook获取一个包含完整 VAE、编码器/解码器和训练代码的 Jupyter 笔记本链接,包括在 Google Colab 中运行的链接。
5.2.4 评估训练
在训练的某些点上,我们随机选择一个图像,并通过将图像通过编码器来“重构”它以获得Z的潜在值,然后将该值通过解码器返回。在一次运行中,我看到的第一个图像是一个非手写的数字 6。图 5.12 显示了这张图像及其重构。
在训练过程中,我们还从生成模型中模拟随机图像并绘制出来。图 5.13 显示了在一次运行中生成的第一张模拟图像——在这种情况下,数字 3。

图 5.12 训练过程中第一次尝试重构图像显示了模型已经学到了一些东西,但仍有很大的进步空间。

图 5.13 训练过程中从生成模型生成的第一个图像实例
但模型学习得很快。到 130 个 epoch 时,我们在图 5.14 中得到了结果。
训练完成后,我们可以在图 5.15 中看到训练过程中的损失可视化(负 ELBO)。
代码将训练将图像和标签映射到潜在变量的编码器参数。它还将训练将潜在变量和标签映射到图像的解码器。这个潜在变量是 VAE 的基本特征,但我们应该更仔细地看看如何从因果角度解释潜在变量。

图 5.14 经过 130 个 epoch 的训练后,模型重构和随机生成的图像看起来要好得多。

图 5.15 随着训练的进行,测试损失。x轴是 epoch。
5.2.5 我们应该如何因果解释 Z?
我说过我们可以将Z视为图像中所有独立潜在原因的“替身”。Z是从图像像素中学习到的表示。将其视为那些潜在原因的高级因果抽象的诱惑很大,但它可能并不是一个好的因果抽象。自动编码器范式训练一个编码器,可以将图像嵌入到低维表示Z中。它试图以能够尽可能好地重构原始图像的方式做到这一点。为了以最小的损失重构图像,框架试图在低维表示中尽可能多地编码原始图像的信息。
然而,一个好的因果表示不应该试图捕捉尽可能多的信息。相反,它应该努力只捕捉图像中的因果信息,忽略其他所有信息。实际上,当Z是无监督的(意味着我们缺乏Z的标签)时,通常无法在Z中“分离”因果和非因果因素。然而,领域知识、干预和半监督可以帮助。有关因果表示学习和因果因素的分离的参考文献,请参阅www.altdeep.ai/p/causalaibook。随着我们继续阅读本书,我们将对这种表示中的“因果信息”应该是什么样子形成直觉。
5.2.6 这种因果解释的优势
我们 VAE 的设置和训练过程并没有固有的因果性;这在许多机器学习场景中常见的普通监督 VAE 中很典型。我们方法中唯一的因果元素是我们的解释。我们说数字和是否手写是原因,Z是潜在原因的替代品,图像是结果。应用因果马尔可夫性质,我们的因果模型将联合分布分解为P(Z)、P(is-handwritten)、P(digit)和P(image|Z, is-handwritten, digit),其中后者是图像的因果马尔可夫核。
我们可以用这种因果解释做什么?首先,我们可以用它来改进深度学习和通用机器学习的工作流程和任务。我们将在下一节中看到一个关于半监督学习的例子。
在因果模型中融入生成式 AI 不仅限于 VAE
我演示了如何使用 VAE 框架来拟合由因果有向无环图(DAG)蕴含的因果马尔可夫核,但 VAE 只是实现这一目标的一种方法。我们还可以使用其他深度概率机器学习框架,例如生成对抗网络(GAN)或扩散模型。
在本节中,我们将深度学习融入了因果图模型。接下来,我们将探讨如何利用因果思想来增强深度学习。
5.3 利用因果推理增强深度学习
我们可以利用因果洞察来改进我们设置和训练深度学习模型的方式。这些洞察往往导致诸如提高样本效率(即用更少的数据做更多的事情)、进行迁移学习(使用模型在解决一个任务中学到的知识来提高另一个任务的性能)、数据融合(结合不同的数据集)以及使预测更加鲁棒等好处。
深度学习的大部分工作都是试错。例如,在训练 VAE 或其他深度学习模型时,你通常会在得到好结果之前尝试不同的方法(VAE 与其他框架的比较)、架构选择(潜在变量和隐藏层维度、激活函数、层数等)以及训练方法(损失函数的选择、学习率、优化器等)。这些实验耗费时间、精力和资源。在某些情况下,因果建模可以帮助你更好地选择可能有效和可能无效的方法,从而节省成本。在本节中,我们将探讨半监督学习背景下这种特定情况的例子。
5.3.1 机制的独立性作为归纳偏差
假设我们有一个包含两个变量的 DAG(有向无环图):“原因” C 和“结果” O。该 DAG 简单地是 C → O。我们的因果马尔可夫核是 P(C) 和 P(O|C)。回忆一下第三章中关于 机制独立性 的概念——因果马尔可夫核 P(O|C) 代表了原因 C 导致结果 O 的机制。这种机制与系统中的其他机制不同,即对这些机制的改变不会影响 P(O|C)。因此,了解 P(O|C) 并不会告诉你关于原因分布 P(C) 的任何信息,反之亦然。然而,了解关于结果分布 P(O) 的某些信息可能会告诉你关于给定结果的原因分布 P(C|O) 的某些信息,反之亦然。
为了说明这一点,考虑一个场景,其中 C 代表防晒霜的使用,而 O 表示某人是否晒伤。你理解防晒霜防止晒伤的机制(紫外线、SPF 水平、定期涂抹、汗水和游泳的潜在危险等),并且通过扩展,根据一个人如何使用防晒霜,由 P(O|C) 捕获的晒伤的可能性。然而,这种对机制的这种理解并不提供任何关于防晒霜使用有多普遍的信息,这由 P(C) 表示。
现在,假设你正在尝试猜测一个晒伤的人是否使用了防晒霜,即你正在心理上建模 P(C|O)。在这种情况下,了解晒伤的普遍性,P(O),可能会有所帮助。考虑晒伤的个体是否是那种使用了防晒霜但仍然晒伤的情况。如果晒伤是一个常见问题,那么这种情况更有可能发生,如果晒伤很少见,那么人们就会对预防措施不那么谨慎。
同样,假设 C 代表学习努力程度,O 代表考试成绩。你知道学习更多导致更高考试成绩的因果机制,这由 P(O|C) 捕获。但这并不告诉你学生努力学习有多普遍,这由 P(C) 捕获。假设一个学生考试成绩低,你正在尝试推断他们是否努力学习——你正在心理上建模 P(C|O)。再次,知道考试成绩的典型分布 P(O) 可以有所帮助。如果低分很少见,学生可能会自满,因此不太可能努力学习。你可以利用这个洞察作为 归纳偏差——一种约束你心理模型 P(C|O) 的方法。
因果归纳偏差
“归纳偏差”指的是导致推理算法偏好某些推理或预测而不是其他推理或预测的假设(显式或隐式)。归纳偏差的例子包括奥卡姆剃刀和预测中过去趋势将继续到未来的假设。
现代深度学习依赖于使用神经网络架构和训练目标来编码归纳偏差。例如,“卷积”和“最大池化”是计算机视觉卷积神经网络中的架构元素,它们编码了一个称为“平移不变性”的归纳偏差;即,无论一只小猫出现在图像的左边还是右边,它仍然是一只小猫。
因果模型以因果假设的形式提供归纳偏差,这些假设关于 DGP(例如因果图)。深度学习可以利用这些因果归纳偏差,就像它利用其他类型的归纳偏差一样,以获得更好的结果。例如,机制的独立性表明,知道 P(O) 可以在学习 P(C|O) 时提供有用的归纳偏差。
现在考虑两个变量 X 和 Y(可以是向量)的联合分布 P(X, Y)。我们希望设计一个算法,通过从观察到的 P(X, Y) 数据中学习来解决问题。概率的链式法则告诉我们 P(X=x, Y=y) = P(X=x|Y=y)P(Y=y) = P(Y=y|X=x)P(X=x)。因此,从基本概率的角度来看,对集合 {P(X|Y), P(Y)} 的建模等同于对集合 {P(Y|X), P(X)} 的建模。但是考虑以下情况:要么 X 是 Y 的原因,要么 Y 是 X 的原因。在这些情况下,机制的独立性在集合 {P(X|Y), P(Y)} 和 {P(Y|X), P(X)} 之间产生了不对称性(具体来说,集合 {P(Y|X), P(X)} 代表了 X 对 Y 的因果影响背后的独立机制,而集合 {P(X|Y), P(Y)} 则没有),我们可以在这些算法中利用这种不对称性作为归纳偏差。半监督学习是一个很好的例子。
5.3.2 案例研究:半监督学习
回到我们的基于 TMNIST-MNIST VAE 的因果模型,假设除了我们的原始数据外,我们还有一个大量未标记的数字图像集(即,digit和is-handwritten没有被观察到)。我们模型因果解释的暗示表明,我们可以在训练过程中利用这些数据,使用半监督学习。
机制独立性可以帮助你确定何时半监督学习将有效。在监督学习中,训练数据由N个X,Y对的样本组成;(x[1],y[1]),(x[2],y[2]),…,(x[N],y[N])。X是用于预测标签 Y 的特征数据。数据是“监督的”,因为每个x都与一个y配对。我们可以使用这些对来学习P(Y|X)。在无监督学习中,数据X是无监督的,这意味着我们没有标签,没有观察到y的值。我们的数据看起来像(x[1]),(x[2]),…,(x[N])。仅凭这些数据,我们无法直接学习有关P(Y|X)的任何内容;我们只能学习有关P(X)的内容。半监督学习提出的问题是,假设我们有一个监督和无监督数据的组合。这两组数据能否以某种方式结合,使得我们预测y的能力比仅使用监督数据时更好?换句话说,从无监督数据中更多地了解P(X)是否能够以某种方式增强我们从监督数据中学习P(Y|X)的能力?
半监督问题非常实用。如果标记这些示例的成本很高,通常会有大量的无监督示例。例如,假设你在一家社交媒体网站上工作,并被分配构建一个算法来分类上传的图像是否描绘了无节制的暴力。第一步是通过让人类手动标记图像为无节制的暴力或不暴力来创建监督数据。这不仅需要很多人工时,而且对标记者来说精神压力很大。一个成功的半监督方法意味着你可以最小化你需要做的标记工作。
我们的任务是学习 P(X,Y) 的表示,并使用它来预测 P(Y|X)。为了使半监督学习起作用,X 的未标记值必须以提供关于 P(Y|X) 的信息的方式更新 P(X,Y) 的表示。然而,机制独立性意味着学习 P(X,Y) 的任务分解为学习因果马尔可夫核的独立表示,其中每个表示的参数向量与其他参数向量正交。这种参数模块化(见第 3.2 节)可以阻止参数更新信息从 X 的未标记观察结果流向学习到的 P(Y|X) 表示。为了说明,让我们考虑两种可能性,一种情况是 Y 是 X 的原因,另一种情况是 X 是 Y 的原因。如果 Y 是 X 的原因,例如在我们的 MNIST-TMNIST 例子中(Y 是手写和数字变量,X 是图像),那么我们的学习任务分解为学习 P(X|Y) 和 P(Y) 的独立表示。未标记的 X 观察结果可以给我们一个更好的 P(X) 表示,我们可以通过贝叶斯规则将其转换为 P(Y|X)。然而,当 X 是 Y 的原因时,我们的学习任务分解为学习 P(X) 和 P(Y|X) 的独立表示。这种参数模块化意味着那些未标记的 X 值将帮助我们更新 P(X) 的表示,但不会更新 P(Y|X) 的表示。

图 5.16 在因果学习中,特征导致标签。在反因果学习中,标签导致特征。
当特征导致标签的情况有时被称为 因果学习,因为预测的方向是从原因到效果。反因果学习指的是标签导致特征的情况。这两种情况在图 5.16 中得到了说明。
机制独立性的假设表明,半监督学习只能在反因果情况下实现性能提升(相对于仅对标记数据进行监督学习的基线)。有关更详细解释和参考文献,请参阅 www.altdeep.ai/causalAIbook 中的章节注释。但直观上,我们可以看到这反映了防晒霜和晒伤的例子——知道晒伤的普遍性 P(O) 有助于在知道某人是否晒伤 P(C|O) 的情况下猜测防晒霜的使用情况。在这个相同的反因果学习案例中,仅从 P(X) 的观察结果中仍然可以帮助学习一个良好的 P(Y|X) 模型。但在因果学习案例中,这将是一种浪费努力和资源。
在实践中,X 和 Y 之间的因果结构可能比这些简单的 X→Y 和 X←Y 案例更为复杂和微妙。例如,可能存在 X 和 Y 未观察到的共同原因。这里的启示是,当你了解机器学习问题中变量之间的因果关系时,你可以利用这些知识来更有效地建模,即使任务不是因果推理任务(例如,仅根据 X 预测 Y)。这可以帮助你避免在不太可能有效的方法上浪费时间和资源,就像半监督学习的情况一样。或者它可能使推理更加高效、鲁棒或表现更好。
5.3.3 通过因果性揭开深度学习的神秘面纱
我们的半监督学习示例突出了因果视角如何解释我们何时期望半监督学习能够成功,何时会失败。换句话说,它在某种程度上揭开了半监督学习的神秘面纱。
深度学习方法有效性的神秘性导致 AI 研究人员阿里·拉希米将现代机器学习与炼金术进行比较。
炼金术成功了。炼金术士发明了冶金术、染料纺织的方法、现代玻璃制造工艺和药物。然而,炼金术士也相信他们可以用蜥蜴治愈疾病,将普通金属转化为黄金。
换句话说,炼金术是有效的,但炼金术士缺乏理解其工作背后的科学原理,这使其在有效时有效。这种神秘性使得很难知道它何时会失败。因此,炼金术士在死胡同(哲学家石,长生不老药等)上浪费了大量的努力。
章节检查点
将深度学习融入因果模型:
✓ 计算机视觉问题的因果模型
✓ 训练深度因果图像模型
使用因果推理来增强机器学习:
✓ 机制独立与半监督学习案例研究
👉 通过因果性揭开深度学习的神秘面纱
类似地,深度学习“有效”是因为它在广泛的预测和推理任务上取得了良好的性能。但我们通常对为什么以及何时有效缺乏完整理解。这种神秘性导致了可重复性、鲁棒性和安全性的问题。它还导致了 AI 的不负责任应用,例如试图从个人资料照片预测行为(例如犯罪性)的发表工作。这种努力是机器学习中的炼金术长生不老药类似物,其中含有水银等毒素;它们不仅不工作,而且还会造成伤害。
我们经常听到深度学习的“超人类”性能。说到超人类能力,想象一下超人起源故事的另一种说法。想象一下,当超人第一次公开露面时,他的超人类能力是不可靠的?假设他展示了惊人的超人类壮举,如飞行、超级力量和激光视力,但有时他的飞行能力会失败,他的超级力量会减弱。有时他的激光视力会极度不聚焦,造成严重的附带损害。公众会感到印象深刻,并希望他能做一些好事,但不确定在高风险的情况下是否可以依赖他。
现在想象一下,他的养父母,来自中西部的因果推断专家,用因果分析来模拟他能力的如何和为什么。在揭开了他超能力背后的机制之后,他们能够制造出一种稳定这些能力的药片。这种药片并不会赋予超人新的能力;它只是让他的现有能力更加可靠。开发这种药片的工作可能不会像飞行和激光视力那样成为头条新闻,但它却是仅仅拥有超能力与成为超人之间的区别。
这个类比帮助我们理解使用因果方法来揭示深度学习和其他机器学习方法的影响。减少神秘感导致更稳健的方法,并帮助我们避免浪费或有害的应用。
摘要
-
深度学习可以用来增强因果建模和推理。因果推理可以增强深度学习模型的设置、训练和性能。
-
因果模型可以利用深度学习扩展和操作高维非线性关系的能力。
-
你可以使用变分自动编码器等生成式 AI 框架,在 DAG 上构建因果生成模型,就像我们使用 pgmpy 一样。
-
解码器将直接父代(图像的标签)的结果映射到子代(图像)的结果。
-
换句话说,解码器为我们提供了因果马尔可夫核的非线性高维表示,用于图像。
-
编码器将图像变量和原因(标签)映射回潜在变量 Z。
-
我们可以将潜在变量的学习表示视为未建模原因的替代品,但它仍然缺乏我们期望的理想因果表示所具有的品质。学习潜在因果表示是一个活跃的研究领域。
-
因果性通常通过帮助阐明使其工作的基本原理来增强深度学习和其他机器学习方法。例如,因果分析表明,在反因果学习的情况下(当特征由标签引起时),半监督学习应该有效,但在因果学习的情况下(当特征引起标签时)则不适用。
-
这样的因果洞察可以帮助模型构建者避免在某个算法不太可能在给定问题设置中工作的情况下,浪费时间、计算资源、人力小时和其他资源。
-
因果洞察可以揭开构建和训练深度学习模型元素的面纱,从而使它们变得更加稳健、高效和安全。
第三部分 因果层次结构
第三部分深入探讨了因果推断的核心概念,以代码优先的方式。读者将探索结构化因果模型、干预措施、多世界反事实推理以及因果识别——在这里我们确定你可以用你的模型和你的数据回答哪些类型的因果问题。这一部分将为你准备接受因果推断更具挑战性但更有回报的方面,提供基于代码的实用工具,用于推理“如果”情景。到结束时,你将准备好在现实世界的决策场景中使用因果推断技术,利用生成建模框架和深度学习工具。
第六章:结构因果模型
本章涵盖
-
将一般因果图形模型转换为结构因果模型
-
掌握 SCMs 的关键要素
-
为基于规则的系统实现 SCMs
-
使用加性模型从头开始构建 SCM
-
将 SCMs 与深度学习相结合
在本章中,我将介绍一种基本的因果建模方法,称为结构因果模型(SCM)。SCM 是因果生成模型的一个特例,它可以编码超出我们用 DAG 所能捕捉的因果假设。如果一个 DAG 告诉我们“什么”导致“什么”,那么 SCM 就会告诉我们“什么”导致“什么”以及“原因”如何影响“效果”。我们可以利用这额外的“如何”信息来做出更好的因果推断。
在本章中,我们将通过代码示例来关注定义和构建 SCMs(结构因果模型)的直觉。在后续章节中,我们将看到一些因果推断的例子,这些推断我们仅使用 DAG(有向无环图)是无法做出的,但我们可以通过 SCM 来完成。
6.1 从一般因果图形模型到 SCM
在我们迄今为止构建的因果生成模型中,我们对每个节点定义了一个条件概率分布,该分布是在给定节点的直接父节点的情况下给出的,我们称之为“因果马尔可夫核”。然后我们使用数据来“拟合”这些核。具体来说,我们做出了一个实际的选择,即使用某些参数函数类来拟合这些核。例如,我们使用 pgmpy 的TabularCPD来拟合概率表的参数,因为它让我们能够使用 pgmpy 方便的 d 分离和推理工具。我们还使用 VAE 架构中的神经解码器,因为它解决了对高维变量(如图像)建模的问题。这些实际原因与因果性无关;我们的因果假设仅停留在因果 DAG 上。
现在,有了 SCMs,我们将使用参数函数类来捕捉超出因果 DAG 的额外因果假设。正如我所说,SCM 让我们能够表示关于“如何”原因影响其效果的额外假设;例如,原因的变化总是导致效果成比例的变化。确实,概率表或神经网络可能过于灵活,无法捕捉关于因果“如何”的假设;有了足够的数据,它们可以拟合任何东西,因此不暗示强烈的假设。更多的因果假设允许更多的因果推断,但代价是额外的建模错误风险。
SCM 是因果图形模型(CGM)的一个特例——比我们迄今为止构建的 CGM 具有更多的约束。为了清晰起见,我将使用 CGM 来指代更广泛的因果图形模型集合,这些模型不是 SCM。为了使区别清晰,让我们首先看看我们如何修改一个 CGM,使其满足 SCM 的约束。
6.1.1 法医案例研究
想象你是一名为警察工作的法医科学家。警察发现了一具由颅骨、骨盆、几根肋骨和股骨组成的腐烂的人类遗骸。颅骨上的明显钝力创伤导致警察展开谋杀调查。首先,他们需要你帮助他们识别受害者。
当遗骸到达你的实验室时,你会对其进行测量和编目。从骨盆的形状,你可以迅速判断遗骸很可能是成年男性的。你注意到股骨长 45 厘米。正如你可能怀疑的那样,股骨长度和一个人的整体身高之间存在强烈的预测关系。此外,这种关系是因果的。股骨长度是身高的原因。简单来说,拥有长股骨会使你更高,而拥有短股骨会使你更矮。
事实上,当你查阅你的法医文本时,它说身高是股骨长度的线性函数。它提供了以下基于股骨长度(男性)的概率模型:
n[y] ~ N(0, 3.3)
y = 25 + 3x + n[y]
在这里,x是厘米单位的股骨长度,y是厘米单位的身高。当然,精确的身高会因其他因果因素而变化,而n[y]代表这些因素引起的身高变化。N[y]具有均值为 0 和尺度参数 3.3 厘米的正态分布。
这是一个 SCM 的例子。我们将随着讨论的深入扩展这个例子,但这里要关注的关键要素是我们的模型假设支撑身高的因果机制(Y)是线性的。身高(Y)是其原因的线性函数,即股骨长度(X)和n[y],它代表其他影响身高的因果决定因素。
线性建模是一个吸引人的选择,因为它简单,建立在几个世纪的理论之上,并且得到了无数统计和线性代数软件库的支持。但从因果的角度来看,这并不是重点。我们的 SCM 不是使用这个线性函数是因为它方便。相反,我们有意断言因果关系是线性的——即对于股骨长度的变化,身高也会成比例地变化。
让我们深入探讨这个例子,以突出 CGM 和 SCM 之间的差异。
6.1.2 通过重新参数化转换为 SCM
在本节中,我们将首先将我们熟悉的 CGM 类型转换为 SCM。我们的转换练习将突出这些属性,并清楚地说明 SCM 的技术结构和它与迄今为止我们所见的 CGM 之间的差异。然而,请注意,这种“转换”旨在建立直觉;一般来说,你应该从头开始构建你的 SCM,而不是试图将非 SCM 强行纳入 SCM,原因我们将在第 6.2 节中看到。
假设我们的法医供应链管理(SCM)是一个连续广义模型(CGM)。我们可能像图 6.1 中那样实现它。

图 6.1 一个简单的双节点 CGM。股骨长度 (X) 是身高 (Y) 的原因。X 具有均值为 47 厘米和标准差为 2.3 厘米的正态分布。Y 具有均值为 25 + 3x 厘米和标准差为 3.3 厘米的分布。
从第二章回忆起,x ~ P(X) 和 y ~ P(Y|X=x) 表示我们从 X 的概率分布和给定 X 的条件概率分布 Y 中生成。在这种情况下,P(X),股骨长度的分布,表示为均值为 47 厘米和标准差为 2.3 厘米的正态分布。P(Y|X=x) 是给定股骨长度的身高分布,表示为均值为 25 + 3x 厘米和标准差为 3.3 厘米的正态分布。我们将在列表 6.1 中按如下方式在 Pyro 中实现此模型。
设置你的环境
本章中的代码使用 Python 版本 3.10,Pyro 版本 1.9.0,pgmpy 版本 0.1.25 和 torch 2.3.0 编写。有关运行代码的笔记本链接,请参阅 www.altdeep.ai/p/causalaibook。我们还使用 MATLAB 进行一些绘图;此代码已在 3.7 版本上进行了测试。
列表 6.1 图 6.1 中 CGM 的 Pyro 伪代码
from pyro.distributions import Normal
from pyro import sample
def cgm_model(): #1
x = sample("x", Normal(47., 2.3)) #1
y = sample("y", Normal(25\. + 3*x, 3.3)) #1
return x, y #2
1 x 和 y 从它们的因果马尔可夫核中采样,在这种情况下是正态分布。
2 重复调用 cgm_model 将返回 P(X, Y) 的样本。
我们将使用以下算法将此模型转换为 SCM:
-
为 X 引入一个新的潜在因果父节点,称为 N[x],为 Y 引入一个新的潜在因果父节点,称为 N[y],它们的分布为 P(N[x]) 和 P(N[y])。
-
将 X 和 Y 作为 N[x] 和 N[y] 的确定性函数,使得在新模型中 P(X, Y) 与旧模型相同。
按照这些说明并添加两个新变量,我们得到图 6.2。

图 6.2 为了将 CGM 转换为 SCM,我们引入了潜在的“外生”父节点 N**[x] 和 N**[y],以及这些潜在的概率分布 P(N**[x]) 和 P(N**[y])。然后,通过函数 f**[x] 和 f**[y],根据它们的父节点确定性地设置 X 和 Y。
我们有两个新的潜在变量 N[x] 和 N[y],它们的分布为 P(N[x]) 和 P(N[y])。X 和 Y 各自有它们自己的函数 f[x] 和 f[y],这些函数根据图中父母的值确定性设置 X 和 Y。这种差异是关键的;X 和 Y 在图 6.1 所描述的模型中生成,但在新模型中是确定性设置的。为了强调这一点,我使用赋值运算符“:=”而不是等号“=”来强调 f[x] 和 f[y] 赋予 X 和 Y 的值。
为了达到将我们的 CGM 转换为 SCM 的目标,我们希望P(X)和P(Y|X=x)在两个模型中保持相同。为了实现这一点,我们必须选择P(N[x])、P(N[y])、f[x]和f[y],使得P(X)仍然是 Normal(47, 2.3)且P(Y|X=x)仍然是 Normal(25 + 3.3x, 3.3)。一个选择是进行简单的重新参数化。正态分布随机变量的线性函数也是正态分布的。我们可以在图 6.3 中实现该模型。

图 6.3 对原始 CGM 进行简单重新参数化后,产生了一个新的 SCM 模型,其P(X)和P(Y|X)与原始模型相同。
在代码中,我们将其重写如下。
列表 6.2 将 CGM 重写为 SCM
from pyro.distributions import Normal
from pyro import sample
def scm_model():
n_x = sample("n_x", Normal(0., 2.3)) #1
n_y = sample("n_y", Normal(0., 3.3)) #1
x = 47\. + n_x #2
y = 25\. + 3.*x + n_y #2
return x, y #3
1 我们从标准正态分布中采样这些新的潜在变量。
2 X 和 Y 作为 n_x 和 n_y 的线性变换确定性地计算。
3 返回的 P(X, Y)样本与第一个模型匹配。
通过引入新的外生变量N[x]和N[y]、一些线性函数f[x]和f[y]以及重新参数化,我们将 CGM 转换为编码相同分布P(X, Y)的 SCM。接下来,让我们更仔细地看看我们引入的元素。
6.1.3 形式化新模型
要构建一个 SCM,我们假设我们已经构建了一个因果 DAG,如图 6.1 所示。在图 6.2 和 6.3 中,我们看到两种类型的变量:外生变量和内生变量。内生变量是原始变量X和Y——我们将它们定义为我们要明确建模的变量。这些是我们包含在因果 DAG 中的变量。
外生变量(也称为噪声变量)是我们新的节点N[x]和N[y]。这些变量代表我们内生变量的所有未建模原因。在我们的公式中,我们将每个内生变量与其自己的外生变量父节点配对;X获得新的外生因果父节点N[x],而Y获得外生父节点N[y]。我们将这些添加到我们的 DAG 中,如图 6.2 和 6.3 所示,以保持完整性。
在我们的公式中,我们将假设外生变量没有父节点,并且它们之间没有边。换句话说,它们是图中的根节点,并且相对于其他外生变量是独立的。此外,我们将外生变量视为潜在变量。
每个内生变量也获得其自己的赋值函数(也称为结构赋值)f[x]和f[y]。赋值函数确定性地根据因果 DAG 中父节点的值设置内生变量X和Y的值。
赋值函数是我们捕捉关于因果关系“如何”的假设的方式。例如,要说明身高(Y)和股骨长度(X)之间的因果关系是线性的,我们指定f[x]是一个线性函数。
虽然内生变量是确定性设置的,但 SCM 从概率分布中生成外生变量的值。在我们的股骨例子中,我们从分布 P(N[x]) 和 P(N[y]) 中生成外生变量 N[x] 和 N[y] 的值 n[x] 和 n[y],这些分布是 N(0, 2.3) 和 N(0, 3.3),如图 6.3 所示。
生成式供应链管理模型(SCM)的元素
-
一组内生变量(例如,X,Y)——这是我们想要显式建模的变量。它们是我们构建到我们的因果有向图(DAG)中的模型。
-
一组外生变量(例如,N**[x] 和 N[y])*——这些变量代表内生变量的未建模原因。在我们的公式中,每个内生变量都有一个相应的潜在外生变量。
-
一组分配函数(例如,f**[x] 和 f[y])*——每个内生变量都有一个分配函数,它根据其父节点(其对应的外生变量和其他内生变量)确定性地设置其值。
-
*一组外生变量概率分布(例如,P(N[x]) 和 P(N[y]))——SCM 成为一个具有外生变量上的一组分布的生成模型。给定从这些分布生成的值,内生变量被确定性设置。
让我们看看另一个供应链管理模型(SCM)的例子,这次使用离散变量。
6.1.4 供应链管理模型(SCM)的一个离散、命令式示例
我们关于股骨的例子处理了连续变量,如身高和长度。现在让我们回到第二章中的投掷石头示例,并考虑一个离散的供应链管理模型(SCM)的例子。在这个例子中,如果珍妮或布莱恩或两者都愿意,他们会在窗户上投掷石头。窗户是否破碎取决于珍妮和布莱恩是否投掷以及窗户玻璃的强度。
我们如何将这个模型转换为供应链管理模型(SCM)?实际上,这个模型已经是供应链管理模型了。我们用以下代码捕捉了这一点。
列表 6.3 第二章中的投掷石头示例是一个供应链管理模型(SCM)。
import pandas as pd
import random
def true_dgp(
jenny_inclination, #1
brian_inclination, #1
window_strength): #1
jenny_throws_rock = jenny_inclination > 0.5 #2
brian_throws_rock = brian_inclination > 0.5 #2
if jenny_throws_rock and brian_throws_rock: #3
strength_of_impact = 0.8 #3
elif jenny_throws_rock or brian_throws_rock: #3
strength_of_impact = 0.6 #3
else: #3
strength_of_impact = 0.0 #3
window_breaks = window_strength < strength_of_impact #4
return jenny_throws_rock, brian_throws_rock, window_breaks
generated_outcome = true_dgp(
jenny_inclination=random.uniform(0, 1), #5
brian_inclination=random.uniform(0, 1), #5
window_strength=random.uniform(0, 1) #5
)
1 输入值是外生变量的实例。
2 如果珍妮和布莱恩愿意的话,他们会投掷石头。jenny_throws_rock 和 brian_throws_rock 是内生变量。
3 strength_of_impact 是一个内生变量。这个整个 if-then 表达式是影响强度的分配函数。
4 window_breaks 是一个内生变量。分配函数是 lambda strength_of_impact, window_strength: strength_of_impact > window_strength。
5 每个外生变量具有均匀分布(Uniform(0, 1))。
你会看到它满足供应链管理模型的要求。true_dgp 函数的参数(即 jenny_inclination、brian_inclination、window_strength)是外生变量。函数内部命名的变量是内生变量,它们由外生变量确定性设置。
在论文和教科书中遇到的大多数 SCM(软件配置管理)都是以数学形式写下的。然而,这个投掷石头的例子向我们展示了使用像 Python 这样的命令式脚本语言进行因果推理的力量。一些因果过程用代码写比用数学写更容易。直到最近,像 Pyro 这样的工具才使我们能够制作复杂的基于代码的 SCM。
6.1.5 为什么使用 SCM?
更多的因果假设意味着更强的因果推断能力。是否使用 SCM 而不是常规 CGM 的问题等同于询问编码在功能赋值中的额外因果假设是否能够服务于你的因果推断目标。
在我们的股骨例子中,我们的 DAG(有向无环图)表明股骨长度导致身高。我们的 SCM 更进一步,表明股骨长度每增加一个单位,身高也会相应增加。问题是这些额外信息是否有助于我们回答一个因果问题。一个这样的线性假设有助于做出因果推断的例子是使用工具变量估计来估计因果效应,这将在第十一章中讨论。这种方法依赖于线性假设来推断在 DAG 的假设本身不足以做出推断的情况下因果效应。另一个例子是 SCM 可以让我们使用第九章中讨论的算法来回答反事实查询。
当然,如果你的因果推断依赖于一个假设,而这个假设是错误的,那么你的推断可能也是错误的。DAG 中的“什么”假设比 SCM 中的额外“如何”假设简单。DAG 中的一条边是一个关于X导致Y的真是或假命题。SCM 模型中的赋值函数是关于X如何导致Y的陈述。后者的假设更加微妙,而且很难验证,因此更容易出错。考虑这样一个事实:市场上有一些长期存在的药物我们知道它们有效,但我们并不完全了解它们的药理作用机制——它们是如何工作的。
6.1.6 与相关方法的差异
SCM 在不同领域有着丰富的历史。你可能见过一些与我们所阐述的类似但又不尽相同的公式。在这里,我们将突出这个公式的区分性元素以及为什么它们对我们很重要。
带有潜在外生变量的生成 SCM
我们希望将我们的 SCM 用作生成模型。为此,我们将外生变量(我们不希望明确建模的变量)视为内生变量未建模原因的潜在代理。我们只需要指定外生变量的概率分布,我们就能得到一个生成潜在变量模型。
灵活选择赋值函数
你会发现 SCM 最常见的应用使用线性函数作为分配函数,就像我们在股骨示例中所做的那样。然而,在生成式 AI 环境中,我们当然不想将自己限制在线性模型上。我们希望与丰富的函数类一起工作,我们可以将其编写为代码,使用自动微分进行优化,并将其应用于高维非线性问题,如图像。这些函数类在表示因果的“如何”方面可以做得同样好。
与有向无环图(DAG)的联系
我们在基于 DAG 的因果观中定位 SCM。首先,我们像第三章和第四章中那样构建一个因果 DAG。DAG 中的每个变量在 SCM 中成为内生变量(我们想要明确建模的变量)。对于每个内生变量,我们在 DAG 中添加一个单一的潜在外生父节点。接下来,我们将“分配函数”定义为这样一个函数,它根据 DAG 中父节点的值给定的内生变量分配一个值。我们基于 DAG 的所有理论仍然适用,例如因果马尔可夫性质和机制独立性。
注意,并非所有 SCM 的公式都如此紧密地遵循 DAG。一些不采用因果图形视图的从业者仍然使用类似 SCM 的模型(例如,计量经济学中的结构方程模型)。一些图形 SCM 的变体允许我们放宽无环性,并处理循环和反馈回路。
独立的外生变量
为每个内生变量引入一个外生变量可能是一个麻烦;有时将原始 DAG 中没有父节点的节点视为外生变量,或者为两个内生节点有相同的外生父节点可能更容易。但这种方法使我们能够以保持原始 DAG 所蕴含的 d 分离的方式添加外生变量。它还允许我们区分内生变量,我们关心并希望明确建模,以及所有我们不希望明确建模的外生原因。这在例如,当你正在构建因果图像模型,如第五章中所述,并且你不想明确表示图像出现的所有许多原因时,非常有用。
6.1.7 因果决定论及其对建模的影响
供应链管理(SCM)的定义要素是,内生变量是由分配函数确定性地设置,而不是通过从基于因果父代的分布中随机抽取来概率性地设置。这种确定性分配反映了因果决定论的哲学观点,该观点认为,如果你知道一个结果的所有因果因素,你就会以完全的确定性知道这个结果。
SCM 建立在这样的哲学基础上。再次考虑我们的股骨高度示例,如图 6.4 所示。

图 6.4 原始的 CGM 从因果马尔可夫核中采样内生变量。新模型确定性地设置内生变量。
在图 6.4 左侧的原始 CGM 中,我们从其因果马尔可夫核的模型中生成X和Y的值。在相应的 SCM 右侧,内生变量是确定性设定的,不再从分布中抽取。SCM 表示,给定股骨长度以及由N[y]表示的所有其他未建模的高度原因,身高是确定的。
注意,尽管这种决定性观点,SCM 仍然是对内生变量联合概率分布的随机模型P(X, Y)。但与图 6.4 左侧的 CGM 相比,右侧的 SCM 将模型的全部随机性转移到外生变量分布上。X和Y在 SCM 中仍然是随机变量,因为它们是N[x]和N[y]的函数,而随机变量的函数仍然是随机变量。但在给定外生变量的条件下,内生变量是完全确定的(退化的)。
因果决定论为我们这些因果模型师带来了令人耳目一新的结论。首先,当我们将基于 DAG 的因果观应用于给定问题时,我们隐含地假设真实数据生成过程(DGP)是一个结构因果模型(SCM)。我们已经假设真实 DGP 有一个潜在的、真实的 DAG。更进一步,假设该 DAG 中的每个变量都是基于其所有原因(包括 DAG 内外的原因)确定性设定的,这相当于假设真实 DGP 是一个 SCM。SCM 可能是一个黑盒,或者我们可能无法轻易用数学或代码将其写下来,但它仍然是一个 SCM。这意味着,无论我们使用传统的 CGM 还是 SCM,我们都是在模拟一个真实 SCM。
其次,它暗示了如果我们从真实 SCM 生成样本,这些样本中的所有随机变化都将完全归因于外生原因。它不会是由于不可还原的随机性来源,例如,海森堡的不确定性原理或蝴蝶效应。如果这些概念驱动你的建模领域的输出,CGM 可能不是最佳选择。
现在我们知道我们想要模拟一个真实 SCM,让我们探索为什么我们不能简单地从数据中学习它。
6.2 SCMs 之间的等价性
关于 SCM 的一个关键理解是,我们不能完全从数据中学习它们。为了了解原因,让我们回顾一下我们将 CGM 转换为 SCM 的情况。让我们看看为什么在一般情况下,这不能给我们提供真实 SCM。
6.2.1 重参数化是不够的
当我们将通用的 CGM 转换为 SCM 时,我们使用了这样一个事实:正态分布随机变量的线性变换会产生一个正态分布随机变量。这确保了内生变量联合概率分布保持不变。
我们可以使用这种“重新参数化技巧”(在生成式 AI 中被称为此技术)来处理其他分布。当我们应用重新参数化技巧时,我们将所有条件概率分布中的不确定性转移到新引入的外生变量分布上。问题是不同的“重新参数化技巧”可能导致具有不同因果假设的不同 SCM(结构因果模型),从而导致不同的因果推断。
伯努利分布的重新参数化技巧
例如,让X代表选择一个加权硬币,Y代表选择硬币抛掷的结果。Y为 1 表示抛出正面,为 0 表示抛出反面。X有两个值,“硬币 A”或“硬币 B”。硬币 A 抛出正面的概率为.8,硬币 B 抛出正面的概率为.4,如图 6.5 所示。

图 6.5 一个简单的 CGM(因果图模型)。X是选择两个不同重量正反面概率的硬币中的一个。Y是抛硬币的结果(正面或反面)。
我们可以用变量Y从参数为p[x]的伯努利分布中采样来模拟抛硬币的结果,其中p[x]是.8 或.4,这取决于x的值。
y ~ 伯努利(p[x])
我们如何在这里应用重新参数化技巧,使得结果 Y 成为确定性过程的产物?
想象我们有一根一米的棍子(图 6.6)。

图 6.6 要将抛硬币模型转换为 SCM(结构因果模型),首先想象一根一米长的棍子。
想象使用一把小刀在棍子上刻一个标记,将棍子分成两个区域:一个对应“反面”,一个对应“正面”。我们在这个点切标记,使得每个区域的长度与对应结果的概率成比例;正面的区域长度为p[x]米,反面的区域长度为 1 – p[x]米。对于硬币 A,正面的区域将是 0.8 米(80 厘米),反面的区域将是 0.2 米(图 6.7)。

图 6.7 将棍子分成两个区域,分别对应每个结果。区域的长度与结果的概率成比例。
在标记分区后,我们现在将在棍子长度上随机选择一个点来折断棍子。折断发生在给定区域的概率等于该区域相关结果的概率(图 6.8)。这种等式来自于区域的长度与结果的概率相对应。如果折断点在我们用口袋刀切分的分区左侧,则y被分配为 0(“正面”),如果折断点在右侧,则y被分配为 1(“反面”)。
要随机选择一个点来折断木棍,我们可以从均匀分布中生成。假设我们从均匀分布(0, 1)中采样得到 0.15,因此将木棍折断在长度为 0.15 米的位置,如图 6.8 所示。0.15 落在“正面”区域,因此我们返回正面。如果我们重复进行多次折断木棍的程序,我们将从我们的目标伯努利分布中获得样本。
在数学上,我们可以将这个新模型写成以下形式:
n[y] ~ Uniform(0, 1)
y := I(n[y] ≤ p[x])
其中 p[x] 如果 X 是硬币 A,则为 0.8,如果 X 是硬币 B,则为 0.4。在这里,I(.) 是指示函数,当 n[y] < p[x] 时返回 1,否则返回 0。

图 6.8 从 0 到 1 米的均匀分布中生成,在该点折断木棍,并返回与折断发生区域相关的结果。重复生成均匀变量将导致 80%的时间在“正面”区域折断,因为其长度是整个木棍长度的 80%。
这个新模型在技术上是一个 SCM,因为不是 Y 从伯努利分布生成,而是由一个指示“分配”函数确定。我们进行了重新参数化,将所有随机性转移到具有均匀分布的外生变量上,并将该变量传递给分配函数。
不同的“重新参数化技巧”会导致不同的 SCM。
使用 SCM 建模的主要原因是要让功能分配表示超出因果 DAG 所捕获的因果假设。重新参数化技巧的问题是,应用于相同 CGM 的不同重新参数化技巧将创建具有不同分配函数的 SCM,这意味着不同的因果假设。
为了说明,假设不是抛硬币,而是 Y 是一个三面骰子,就像我们在第二章中看到的(图 6.9)。X 决定了我们将掷哪个骰子;骰子 A 或骰子 B(图 6.10)。每个骰子的重量不同,因此它们掷出 1、2 或 3 的概率也不同。

图 6.9 三面骰子

图 6.10 假设我们将模型从选择硬币(两个结果)切换到选择三面骰子(三个结果)。
我们可以将原始模型从伯努利分布(等同于具有两个结果的分类分布)扩展到具有三个结果的分类分布:
y ~ Categorical([p[x][1], p[x][2], p[x][3]])
其中 p[x][1]、p[x][2] 和 p[x][3] 分别是掷出 1、2 和 3 的概率(注意其中一个是冗余的,因为 p[x][1] = 1 – p[x][2] – p[x][3])。
我们也可以在这里使用基于棍子的重新参数化技巧;我们只需要将棍子扩展到一个额外的区域。假设对于骰子A,掷出 1 的概率是p[x][1]=.1,掷出 2 的概率是p[x][2]=.3,掷出 3 的概率是p[x][3]=.6。我们将按照图 6.11 所示标记我们的棍子。

图 6.11 将棍子分为三个区域,对应于三面骰子的结果。
我们将再次使用之前使用生成的均匀变量选择远程区域的方法(图 6.12)。

图 6.12 当Y有三个结果时,转换为棍子打破 SCM
在数学中,我们将这样写:

但如果我们用不同的方式标记这根棍子,从而改变棍子上区域排列的顺序呢?在第二根棍子上,区域的顺序是 3,1,然后是 2(图 6.13)。

图 6.13 重新参数化因果生成模型的不同方式产生两个不同的 SCM。它们编码相同的联合概率分布,但给定相同的外生值,具有不同的内生值。
从每个结果(1、2 或 3)的概率来看,两根棍子是等价的——分配给每个骰子掷出结果的棍子区域大小在两根棍子上是相同的。但我们的因果机制已经改变!这两根棍子对于给定的n[y]值可以返回不同的结果。如果我们随机抽取.15 并因此将棍子折断在.15 米处,第一根棍子将在区域 2 折断,返回 2,而第二根棍子将在区域 3 折断,返回 3。
在数学上,第二个棍子打破的 SCM 具有以下形式:

比喻地说,想象一下在你的建模领域中,棍子总是以某种方式标记,区域以某种方式排列。那么,简单的重新参数化技巧并不能保证给出正确的标记。
为了使这个观点更加明确,让我们回顾一下我们将股骨高度模型转换为 SCM 所执行的重新参数化技巧(图 6.14)。

图 6.14 回顾股骨高度 SCM
假设我们创建一个新的 SCM,它与原来的相同,只是y的分配函数现在看起来像这样:
y := 25 + 3x – n[y]
现在我们有一个第二个 SCM,它减去n[y]而不是加上n[y]。正态分布是对称的,所以n[y]具有均值为 0 的正态分布,因此n[y]和- n[y]的概率值相同,所以Y的概率分布在两个模型中是相同的。但对于相同的n[y]和x值,y的实际分配值将是不同的。接下来,我们将对这个想法进行正式的详细探讨。
6.2.2 SCM 的唯一性和等价性
给定一个因果有向无环图(DAG)和内生变量的联合概率分布,通常可以存在多个与该 DAG 和联合概率分布一致的统计因果模型(SCM)。这意味着我们不能仅仅依靠数据来学习真实的 SCM。我们将在第十章深入探讨这个因果可识别性问题。现在,让我们使用我们迄今为止看到的概念来分解这个想法。
许多 SCM 与一个 DAG 和相应的分布一致
回想一下我们在图 2.24 中概述的多对一关系,这里再次在图 6.15 中展示。

图 6.15 当我们从数据生成过程(DGP)移动到观测数据时,存在多对一的关系。
如果我们可以将潜在的 DGP 表示为一个真实的 SCM,图 6.15 就会变成图 6.16 所示的样子。

图 6.16 不同的 SCM 可以蕴含相同的 DAG 结构和分布。SCM 可以在分配函数(和/或外生分布)上有所不同。
换句话说,给定一组变量的联合分布,可以存在多个与该分布一致的因果 DAG——在第四章中我们称这些 DAG 为马尔可夫等价类。进一步,我们可以有SCM 的等价类——给定一个因果 DAG 和联合分布,可以存在多个与该 DAG 和分布一致的 SCM。我们通过 stick-breaking 骰子滚动 SCM 的两个变体都是与 DAG X(骰子选择)→ Y(骰子滚动)以及分布P(X)(骰子选择的概率分布)和P(Y|X)(骰子滚动的概率)一致的例子看到了这一点。
真实的 SCM 不能从数据中学习(没有因果假设)
当我们在前几章中努力构建因果 DAG 时,我们的隐含目标是重现真实的因果 DAG。现在我们寻求重现图 6.16 中的真实 SCM。
在第四章中,我们看到了数据无法区分等价类中的因果 DAG。同样,仅凭数据本身不足以恢复真实的 SCM。再次考虑我们推导出的 stick-breaking SCM。我们推导出两个标记的棍子,区域排序不同。当然,有三个结果排序的方式:({1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 2}, {3, 1, 2}, {3, 2, 1})。这就是六种标记棍子的方式,因此有六个与分布P(X)和P(Y|X)(骰子滚动的概率)一致的可能的 SCM。
假设这些标记的棍子中的一个就是真实的 SCM,并且它被隐藏在一个黑盒子里,如图 6.17 所示。假设我们反复运行 SCM 来生成一些骰子滚动结果。基于这些骰子滚动结果,我们能否找出真实棍子的标记?换句话说,六个排序中的哪一个就是黑盒子的排序?

图 6.17 假设我们不知道哪个“标记棒”产生了观察到的骰子滚动。仅从骰子滚动中无法推断出正确的标记棒。更普遍地说,SCM 不能仅从数据中的统计信息中学习。
答案是否定的。更普遍地说,由于 SCM 和数据之间存在多对一的关系,你无法仅从数据中的统计信息中学习到真实的 SCM。
让这个想法在脑海中停留一会儿。我在告诉你,即使拥有无限的数据,最前沿的深度学习架构,以及无限的计算预算,你甚至在这个简单的三个结果的棒子断裂例子中都无法找出真正的 SCM。从统计概率的角度来看,每个 SCM 在给定数据的情况下都是等可能的。要在等价类中偏好一个 SCM 而不是另一个,你需要额外的假设,例如,{1, 2, 3} 是最可能的标记,因为标记棒子的人可能会按顺序标记区域。这是一个很好的假设,只要你意识到你正在做出这个假设。
在机器学习的实践中,我们常常没有意识到我们正在做出这样的假设。为了说明这一点,假设你进行了以下实验。你创建了一组棒子断裂 SCM,并从这些 SCM 中模拟数据。然后你将 SCM 向量化,并将它们用作标签,将模拟数据作为特征,在图 6.18 中所示的深度监督学习训练过程中,专注于从模拟数据中预测“真实”的 SCM。

图 6.18 你创建了多个 SCM,并从每个 SCM 中模拟数据。然后你可以对从模拟数据中预测真实 SCM 的深度神经网络进行监督学习。对于同一等价类中的两个 SCM,这种方法将倾向于在训练数据中出现频率更高的属性的那个 SCM。
假设然后你将训练好的模型数据实际的三面骰子滚动的样本输入,目标是预测真实的 SCM。那个预测模型的预测可能会倾向于具有{1, 2, 3}排序的棒子,而不是等效的{2, 3, 1}排序。但只有当{1, 2, 3}排序在训练数据中更常见时,它才会这样做。
类比于程序归纳
从数据中学习 SCM 的问题与计算机科学中程序归纳的挑战相关。假设一个程序以“foo”和“bar”作为输入,并返回“foobar”作为输出。这个程序是什么?你可能认为程序只是简单地连接输入。但它可以是任何东西,包括一个将输入与单词“aardvark”连接起来,然后删除“aardvark”字符,并返回结果的程序。数据(许多程序的输入和输出示例)不足以区分所有可能的程序中哪一个是正确的。为此,你需要额外的假设或约束,例如奥卡姆剃刀类型的归纳偏见,它更喜欢最简单的程序(例如,具有最小描述长度的程序)。
尝试从数据中学习 SCM 是此类问题的特殊情况。程序的输入是外生变量值,输出是内生变量值。假设你有了因果 DAG,但没有分配函数。问题是,给定输入,有无限多的分配函数可以产生那些输出。从数据中学习 SCM 需要额外的假设来约束分配函数,例如约束函数类和使用奥卡姆剃刀(例如,模型选择标准)。
接下来,我们将深入探讨在离散的基于规则的设置中实现 SCM。
6.3 实现基于规则的系统的 SCM
SCM 的一个特别有用的应用是建模基于规则的系统。通过“基于规则”,我指的是已知规则,通常由人类设定,决定了因果关系的“如何”。游戏是一个很好的例子。
为了说明,考虑一下蒙提霍尔问题——一个以 1960 年代一个类似设置的电视游戏节目主持人命名的基于概率的智力题。
6.3.1 案例研究:蒙提霍尔问题
在一个游戏节目中,参赛者被要求在三个关闭的门中选择。其中一扇门后面有一辆车;其他门后面是山羊。玩家选择了第一个门。然后,知道门后面是什么的主持人打开另一扇门,例如第三扇门,那扇门后面有山羊。主持人然后问参赛者,“你想换到第二扇门,还是想坚持你的原始选择?”问题是哪种策略更好,换门还是坚持?
正确答案是换门。这个问题于 1990 年出现在《Parade》杂志的一篇文章中,并给出了正确答案。成千上万的读者邮寄了回复,包括许多受过研究生水平数学训练的人,他们试图反驳这个答案,并说换门没有优势,坚持或换门赢得比赛的概率相同。
图 6.19 说明了为什么换门是更好的直觉。换门是正确答案,因为在标准假设下,“换门”策略赢得汽车的概率是三分之二,而“留下”策略只有三分之一的概率。这看起来似乎不合逻辑,因为当游戏开始时,每个门都有相等的机会藏有汽车。似乎一旦主持人排除了一个门,剩下的每个门都应该有 50-50 的机会。这种逻辑是错误的,因为主持人不是随机排除一个门。他只排除一个既不是玩家的初始选择,又没有汽车的门。三分之一的时候,这些是同一个门,三分之二的时候是不同的门;这种三分之一到三分之二的非对称性就是为什么剩下的门不各自有 50-50 的机会藏有汽车。

图 6.19 蒙提霍尔问题。每个门都有相等的机会藏有奖品。玩家最初选择一个门,主持人揭示一个输掉的门,玩家有选择是否换门的选项。与直觉相反,玩家应该换门;如果他们换门,他们将赢得三分之二的时间。这个插图假设选择了门 1,但无论初始选择哪个门,结果都是相同的。
6.3.2 蒙提霍尔问题的因果 DAG
因果建模使蒙提霍尔问题变得更加直观。我们可以用图 6.20 中的因果 DAG 来表示这个游戏。

图 6.20 蒙提霍尔问题的因果 DAG
每个变量的可能结果如下:
-
藏有汽车的门—表示后面有汽车的门。第 1 个门用 1^(st)表示,第 2 个门用 2^(nd)表示,或者第 3 个门用 3^(rd)表示。
-
玩家第一次选择—表示玩家最初选择的门。第 1 个门用 1^(st)表示,第 2 个门用 2^(nd)表示,或者第 3 个门用 3^(rd)表示。
-
主持人倾向—假设主持人面对着门,从左到右它们是按 1(st)、2(nd)、3^(rd)的顺序排列的。这个主持人倾向变量有两个结果,左和右。当结果是左时,主持人倾向于选择最左边的可用门;否则,主持人将倾向于选择最右边的可用门。
-
主持人门选择—结果仍然是第 1 个、第 2 个和第 3 个。
-
策略—结果是换门,如果策略是从第一个选择换门,或者留下,如果策略是保留第一个选择。
-
玩家第二次选择—表示在主持人询问玩家是否想要换门后,玩家选择的门。结果仍然是第 1 个、第 2 个和第 3 个。
-
赢或输—表示玩家是否赢;结果是赢或输。当玩家第二次选择等于藏有汽车的门时,即为赢。
接下来,我们将看到如何将其作为 SCM 在 pgmpy 中实现。
6.3.3 使用 pgmpy 实现蒙提霍尔问题作为 SCM
游戏规则为我们提供了明确的分配函数逻辑。例如,我们可以用表 6.1 表示主持人门选择的分配函数。
表 6.1:给定玩家第一个选择、带车的门和主持人倾向时,主持人门选择的查找表。它显示了主持人根据玩家的第一个选择、有车的门以及主持人倾向(指主持人是否会在有两个选择时选择最左边的或最右边的门)选择的门。
| 主持人倾向 | 左侧 | 右侧 |
|---|---|---|
| 带车的门 | 1^(st) | 2^(nd) |
| 玩家第一个选择 | 1^(st) | 2^(nd) |
| 主持人门选择 | 2^(nd) | 3^(rd) |
当带车的门和玩家的第一个选择不同时,主持人只能选择剩下的门。但如果带车的门和玩家的第一个选择是同一扇门,主持人有两个门可以选择。如果主持人倾向是左侧,他将选择最左边的门。例如,如果带车的门和玩家第一个选择都是 1^(st),主持人必须在 2^(nd)和 3^(rd)门之间选择。如果主持人倾向 == 左侧,他将选择 2^(nd)门;如果主持人倾向 == 右侧,他将选择 3^(rd)门。
使用像 Pyro 这样的库编写 if-then 逻辑来表示这种逻辑非常直接。但由于规则很简单,我们可以使用更加受限的 pgmpy 库来将此函数编写为一个条件概率表(表 6.2)。
表 6.2:我们可以将主持人门选择的查找表(表 6.1)转换为条件概率表,我们可以在 pgmpy 中将它实现为一个TabularCPD对象,其中给定结果的概率为 0 或 1,因此是确定性的。
| 主持人倾向 | 左侧 | 右侧 |
|---|---|---|
| 带车的门 | 1^(st) | 2^(nd) |
| 玩家第一个选择 | 1^(st) | 2^(nd) |
| 主持人门选择 | 1^(st) | 0 |
| 2^(nd) | 1 | 0 |
| 3^(rd) | 0 | 1 |
表中的条目对应于在原因值给定的情况下主持人门选择结果发生的概率。每个概率结果要么是 0 要么是 1,给定因果父节点,因此给定父节点,结果完全确定。因此,我们可以将其用作我们的分配函数,并且由于它是一个条件概率表,我们可以使用 pgmpy 中的TabularCPD类来实现它。
列表 6.4 在 pgmpy 中实现主持人门选择分配函数
from pgmpy.factors.discrete.CPD import TabularCPD
f_host_door_selection = TabularCPD(
variable='Host Door Selection', #1
variable_card=3, #2
values=[ #3
[0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,0], #3
[1,0,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1], #3
[0,1,0,1,0,0,0,0,0,1,1,0,1,1,0,0,0,0] #3
], #3
evidence=[ #4
'Host Inclination', #4
'Door with Car', #4
'Player First Choice' #4
], #4
evidence_card=[2, 3, 3], #5
state_names={ #6
'Host Door Selection':['1st', '2nd', '3rd'], #6
'Host Inclination': ['left', 'right'], #6
'Door with Car': ['1st', '2nd', '3rd'], #6
'Player First Choice': ['1st', '2nd', '3rd'] #6
} #6
) #6
1 变量的名称
2 卡迪纳尔(结果数量)
3 概率表。值与表 6.2 中的值匹配,只要证据参数中因果变量的顺序与表中因果变量名称的从上到下顺序一致。
4 条件(因果)变量
5 每个条件(因果)变量的卡迪纳尔(结果数量)
6 每个变量的状态名称
此代码生成f_host_door_selection,一个我们可以添加到BayesianNetwork类模型的TabularCPD对象。然后我们可以像使用更典型的TabularCPD对象一样使用它。
同样,我们可以创建一个玩家第二选择的查找表,如表 6.3 所示。
表 6.3 根据玩家第一选择、主持人门选择和策略条件下的玩家第二选择查找表。“玩家第二选择”单元格在玩家第一选择和主持人门选择相同的不可能情况下为空。
| 策略 | 保持 | 切换 |
|---|---|---|
| 主持人门选择 | 1^(st) | 2^(nd) |
| 玩家第一选择 | 1^(st) | 2^(nd) |
| 玩家第二选择 | 2^(nd) |
主持人永远不会选择与玩家第一选择相同的门,因此主持人门选择和玩家第一选择永远不会具有相同的值。在这些情况下,玩家第二选择的条目未定义。
将此扩展为条件概率表,我们得到表 6.4。同样,不可能结果单元格留空。
表 6.4 将玩家第二选择的查找表(表 6.3)转换为我们可以实现为TabularCPD对象的条件概率表的结果
| 策略 | 保持 | 切换 |
|---|---|---|
| 主持人门选择 | 1^(st) | 2^(nd) |
| 玩家第一选择 | 1^(st) | 2^(nd) |
| 玩家第二次选择 | 1^(st) | |
| 2^(nd) | 1 | |
| 3^(rd) | 0 |
不幸的是,当我们指定Tabular-CPD时,我们不能将不可能的值留空,因此,在下面的代码中,我们需要为这些元素分配任意值。
列表 6.5 在 pgmpy 中实现玩家第二次选择分配函数
from pgmpy.factors.discrete.CPD import TabularCPD
f_second_choice = TabularCPD(
variable='Player Second Choice',
variable_card=3,
values=[
[1,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,1,0], #1
[0,1,0,0,1,0,0,1,0,1,0,1,0,1,0,1,0,1], #1
[0,0,1,0,0,1,0,0,1,0,1,0,1,0,0,0,0,0] #1
],
evidence=[
'Strategy',
'Host Door Selection',
'Player First Choice'
],
evidence_card=[2, 3, 3],
state_names={
'Player Second Choice': ['1st', '2nd', '3rd'],
'Strategy': ['stay', 'switch'],
'Host Door Selection': ['1st', '2nd', '3rd'],
'Player First Choice': ['1st', '2nd', '3rd']
}
)
1 概率值是 0 或 1,所以分配函数是确定性的。在父组合不可能的情况下,我们仍然需要分配一个值。
这给我们第二个TabularCPD对象。我们将为每个节点创建一个。
首先,让我们设置因果 DAG。
列表 6.6 实现完整的蒙提霍尔 SCM
from pgmpy.models import BayesianNetwork
from pgmpy.factors.discrete.CPD import TabularCPD
monty_hall_model = BayesianNetwork([ #1
('Host Inclination', 'Host Door Selection'), #1
('Door with Car', 'Host Door Selection'), #1
('Player First Choice', 'Host Door Selection'), #1
('Player First Choice', 'Player Second Choice'), #1
('Host Door Selection', 'Player Second Choice'), #1
('Strategy', 'Player Second Choice'), #1
('Player Second Choice', 'Win or Lose'), #1
('Door with Car', 'Win or Lose') #1
]) #1
1 构建因果 DAG。
monty_hall_model现在是一个因果 DAG。在添加外生变量分布和分配函数后,它将成为 SCM。
以下列表添加了外生变量分布。
列表 6.7 创建外生变量分布
p_host_inclination = TabularCPD( #1
variable='Host Inclination', #1
variable_card=2, #1
values=[[.5], [.5]], #1
state_names={'Host Inclination': ['left', 'right']} #1
) #1
p_door_with_car = TabularCPD( #2
variable='Door with Car', #2
variable_card=3, #2
values=[[1/3], [1/3], [1/3]], #2
state_names={'Door with Car': ['1st', '2nd', '3rd']} #2
) #2
p_player_first_choice = TabularCPD( #3
variable='Player First Choice', #3
variable_card=3, #3
values=[[1/3], [1/3], [1/3]], #3
state_names={'Player First Choice': ['1st', '2nd', '3rd']} #3
) #3
p_host_strategy = TabularCPD( #4
variable='Strategy', #4
variable_card=2, #4
values=[[.5], [.5]], #4
state_names={'Strategy': ['stay', 'switch']} #4
) #4
1 主观倾向变量的 CPD。当玩家选择有车的门时,主持人有两个其他门可以选择。当主持人倾向于选择最左边的门时,这个变量是“left”,如果主持人倾向于选择最右边的门,则变量是“right”。
2 表示哪个门有奖品车的变量的 CPD。假设每个门有相等的机会有车。
3 表示玩家第一次选择门的变量的 CPD。每个门被选择的概率相等。
4 为表示玩家策略的变量创建一个 CPD。“停留”策略是指保持最初的选择,而“切换”策略是指改变门的选择。
在创建了外生分布后,我们现在将创建分配函数。我们已经创建了f_host_door_selection和f_second_choice,所以我们将添加f_win_or_lose——确定玩家是赢还是输的分配函数。
列表 6.8 创建分配函数
f_win_or_lose = TabularCPD(
variable='Win or Lose',
variable_card=2,
values=[
[1,0,0,0,1,0,0,0,1],
[0,1,1,1,0,1,1,1,0],
],
evidence=['Player Second Choice', 'Door with Car'],
evidence_card=[3, 3],
state_names={
'Win or Lose': ['win', 'lose'],
'Player Second Choice': ['1st', '2nd', '3rd'],
'Door with Car': ['1st', '2nd', '3rd']
}
)
最后,我们将添加外生分布和分配函数到monty_hall_model并创建 SCM。
列表 6.9 为蒙提霍尔问题创建 SCM
monty_hall_model.add_cpds(
p_host_inclination,
p_door_with_car,
p_player_first_choice,
p_host_strategy,
f_host_door_selection,
f_second_choice,
f_win_or_lose
)
我们可以运行变量消除推理算法来验证算法的结果。让我们查询在玩家采取“停留”策略的情况下获胜的概率。
列表 6.10 推断获胜策略
from pgmpy.inference import VariableElimination #1
infer = VariableElimination(monty_hall_model)
q1 = infer.query(['Win or Lose'], evidence={'Strategy': 'stay'}) #2
print(q1) #2
q2 = infer.query(['Win or Lose'], evidence={'Strategy': 'switch'}) #3
print(q2) #3
q3 = infer.query(['Strategy'], evidence={'Win or Lose': 'win'}) #4
print(q3) #4
1 我们将使用名为“变量消除”的推理算法。
2 打印当玩家使用“停留”策略时赢得和输掉的概率。
3 打印当玩家使用“切换”策略时赢得和输掉的概率。
4 打印当玩家获胜时,玩家使用“停留”策略与“切换”策略的概率。
这个推理产生了以下输出:
+-------------------+--------------------+
| Win or Lose | phi(Win or Lose) |
+===================+====================+
| Win or Lose(win) | 0.3333 |
+-------------------+--------------------+
| Win or Lose(lose) | 0.6667 |
+-------------------+--------------------+
在“留在原门”策略下,获胜和失败的概率分别是 1/3 和 2/3。相比之下,以下是“换门”策略的输出:
+-------------------+--------------------+
| Win or Lose | phi(Win or Lose) |
+===================+====================+
| Win or Lose(win) | 0.6667 |
+-------------------+--------------------+
| Win or Lose(lose) | 0.3333 |
+-------------------+--------------------+
在“换门”策略下,获胜和失败的概率分别是 2/3 和 1/3。我们还可以根据获胜结果进行条件化,并推断出每种策略导致该结果的概率。
+------------------+-----------------+
| Strategy | phi(Strategy) |
+==================+=================+
| Strategy(stay) | 0.3333 |
+------------------+-----------------+
| Strategy(switch) | 0.6667 |
+------------------+-----------------+
这些是普通的非因果概率推理——我们只是验证我们的 SCM 能够产生这些推理。在第九章中,我们将展示这个 SCM 如何使因果反事实推理成为可能,而简单的模型无法回答,例如“如果输掉游戏的玩家使用了不同的策略,会发生什么?”
6.3.4 规则系统中的外生变量
在这个蒙提霍尔 SCM 中,因果 DAG 中的根节点(没有入边节点的节点)充当外生变量。这与我们关于 SCM 的正式定义略有不同,该定义指出外生变量代表系统之外的因素。主持人倾向符合这个定义,因为这不是原始描述的一部分。有车的门、玩家第一次选择和策略则是另一回事。为了解决这个问题,我们可以向这些变量引入外生父节点,并给定这些父节点,将这些变量确定性地设置,就像我们在本章的其他地方所做的那样。但在使用 pgmpy 建模时,这有点多余。
6.3.5 基于规则的系统 SCM 建模的应用
虽然蒙提霍尔游戏很简单,但不要低估将规则纳入赋值函数的表达能力。在过去的几十年中,人工智能的一些最大成就就是通过简单的规则在棋类游戏中击败专家人类。基于如何从一个状态转换到另一个状态的简单规则,模拟软件可以模拟高度复杂的行为。我们通常希望对人类(他们知道并可以重写这些规则)设计的基于规则的系统进行因果分析,例如自动化制造系统。
6.4 在数据上训练 SCM
给定一个有向无环图(DAG),我们选择使用 CGM 还是 SCM。假设我们想选择 SCM,并希望“拟合”或“训练”这个 SCM 来处理数据。为此,我们为每个赋值函数选择一些参数化的函数类(例如,线性函数、逻辑函数等)。一旦我们在数据上拟合了其参数,这个函数类就变成了一个特定的函数。同样,对于每个外生变量,我们希望指定一个标准概率分布,可能包括我们可以拟合到数据中的参数。
在我们关于股骨高度示例中,所有的赋值函数都是线性函数,外生变量是正态分布。但使用像 Pyro 这样的工具,你可以逐个指定每个赋值函数和外生分布。然后你可以像使用 CGM 一样训练参数。例如,你不必从法医学教科书中获取这个股骨高度模型:
n[y] ~ N(0, 3.3)
y = 25 + 3x + n[y]
你可以直接在真实的法医学数据上拟合线性模型的参数 α、β 和 δ:
n[y] ~ N(0, δ)
y = α + β**x + n[y]
在这个法医学示例中,我们使用线性赋值函数,因为身高与股骨长度成正比。让我们考虑其他捕捉原因如何影响其效果的方法。
6.4.1 我应该选择什么样的赋值函数?
在结构因果模型(SCM)模型中,最重要的选择是赋值函数的 函数类,因为这些选择代表了你对因果“如何”的假设。你可以使用数学中常见的函数类,如线性模型。你还可以使用代码(包括 if-then 语句、循环、递归等),就像我们在抛掷岩石的例子中所做的那样。
记住,你正在建模一个真实的结构因果模型(SCM)。你可能会指定与真实 SCM 中的不同的赋值函数,但这没关系。你不需要你的 SCM 与真实情况完全匹配;你只需要你的模型关于它所依赖的“如何”假设是正确的,以便进行因果推断。
没有关于“如何”假设的结构因果模型(SCM)只是因果图模型(CGM)
假设你构建了一个每个赋值函数都是线性函数的结构因果模型(SCM)。你使用线性高斯假设是因为你选择的库需要它(例如,在 pgmpy 中,LinearGaussianCPD 几乎是你建模连续变量的唯一选择)。然而,你并不打算依赖这个线性假设来进行因果推断。在这种情况下,尽管你的模型符合 SCM 的要求,但实际上它是一个具有线性因果马尔可夫核模型的因果图模型(CGM)。
假设,例如,X 和 Y 之间不是线性关系,而是遵循非线性 S 曲线,并且你的因果推断对这条 S 曲线很敏感。想象一下,真实的结构因果模型(SCM)通过一个形如希尔方程的赋值函数(在生物化学中出现的函数,可以捕捉 S 曲线)来捕捉这一点。但你的 SCM 使用的是基于数据的逻辑函数拟合。尽管你的模型是错误的,但如果你的逻辑赋值函数捕捉到了关于 S 曲线所需的所有信息,以便你的推断能够工作,那么你的模型将足以做出良好的因果推断。
6.4.2 我应该如何建模外生变量分布?
在 6.1.3 节中,我们以特定方式表述了我们的生成式 SCM,其中每个节点都得到一个代表其未建模原因的外部变量。根据这种表述,外部变量分布的作用仅仅是提供足够的变异,以便 SCM 可以模拟联合分布。这意味着,假设你已经选择了你的赋值函数类,你可以根据它们在参数估计后如何拟合数据来选择外部变量的规范分布。某些规范分布可能比其他分布拟合得更好。你可以使用标准的技术来对比不同的选择,这些技术用于模型比较和交叉验证。
这些规范分布可以参数化,例如在
n[y] ~ N(0, δ)
y = α + β**x + n[y]
在生成式 AI 中,更常见的方法是使用规范分布中的常数,并且只训练赋值函数的参数:
n[y] ~ N(0, 1)
y = α + β**x + δ n[y]
两种都行,只要你的选择捕捉到了你的“如何”假设。
6.4.3 加性模型:SCM 建模的流行选择
加性模型是 SCM 模板,它们使用流行的可训练函数类作为赋值函数。它们在 SCM 建模中是一个很好的起点。我们将探讨三种常见的加性模型:线性高斯加性模型(LiGAM)、线性非高斯加性模型(LiNGAM)和非线性加性噪声模型(ANM)。这些模型每个都封装了一对约束:一个关于赋值函数的结构,另一个关于加性外部变量的分布。
加法性使得这种方法更容易,因为通常这些加性模型从数据中学习参数的算法有唯一解。在某些情况下,这些参数有直接的因果解释。还有许多软件库用于在数据上训练加性模型。
让我们用一个例子来展示加性模型的有用性。假设你是一位生物化学家,正在研究生物样本中某种蛋白质的合成。样本中含有一部分酶,它与样本中的某些前体反应并合成你感兴趣的蛋白质。你测量了你感兴趣的蛋白质的量。让 X 表示酶的量,让 Y 表示感兴趣蛋白质的测量量。我们将使用 SCM 来模拟这个系统,其 DAG 如图 6.21 所示。

图 6.21 酶(X)的量是蛋白质(Y)测量量的原因。
我们对因果关系如何影响效果有定性知识,但我们必须将这种知识转化为对赋值函数和外部变量分布的函数类进行明确选择。加性模型是一个很好的起点。
为了说明,我们将关注 Y 的分配函数和外生变量分布,Y 是我们示例中目标蛋白的量。从外生变量生成,并通过分配函数设置 Y,具有以下表示法:
n[y] ~ P(N[y])
y := f**y
f**y 表示 y 的分配函数,它以内源父代 X 和外生父代 N[y] 的值作为输入。
在一个 additive 分配函数中,外生变量始终添加到内源父代的某个函数中。在我们的例子中,这意味着 Y 的分配函数具有以下形式:
y := fy = g(x) + n[y]
在这里,g(.) 是内源父代(s)的一些可训练函数,并且 n[y] 被添加到该函数的结果中。
对于我们的蛋白 Y,这些模型表明测量的蛋白量 Y 等于酶量 g(X) 的某个函数加上一些外生因素,例如测量设备中的噪声。这种假设很有吸引力,因为它让我们将未建模的外生原因视为加性的“噪声”。从统计信号处理的角度来看,从加性噪声中分离出一些核心信号(例如,g(x))相对容易。
通常,让 V 代表模型中的内源变量,V[PA] 代表 V 的内源父代,N[v] 代表外生变量。
v := f**v = g(V[PA]) + n[v]
加性结构因果模型(SCM)具有几个优点,但在这里我们将关注它们作为构建 SCM 模板的益处。我们将从最简单的加性模型,即线性高斯加性模型开始。
6.4.4 线性高斯加性模型
在一个线性高斯加性模型中,分配函数是父代的线性函数,外生变量具有正态分布。
在我们的酶示例中,N[y] 和 Y 给定如下:
n[y] ~ N(0, σ[y])
y := β[0] + β[x]x + n[y]
在这里,β[0] 是截距项,而 β[x] 是 X 的系数。我们假设对于酶量 X 的每单位增加,预期的测量蛋白量会增加 β[x]。N[y] 考虑了由于外生因果因素导致的预期量周围的变异,并且我们假设它具有均值为 0 和尺度参数 σ[y] 的正态分布。例如,我们可能假设 N[y] 主要由测量设备的技术噪声组成,例如干扰传感器的灰尘颗粒。我们可能从使用该设备的经验中知道这种噪声具有正态分布。
通常,对于具有 K 个父代的变量 V,V[PA] = {V[pa][,1], …, V[pa][,][K]}:

该模型定义了参数:β[0] 是截距项,β[j] 是附加到 j^(th) 父亲的系数,而 σ[v] 是 N[v] 的正态分布的尺度参数。
让我们看看 Pyro 中的 LiNGAM 模型示例。
列表 6.11 Pyro 的线性高斯模型示例
from pyro import sample
from pyro.distributions import Normal
def linear_gaussian():
n_x = sample("N_x", Normal(9., 3.))
n_y = sample("N_y", Normal(9., 3.))
x = 10\. + n_x #1
y = 2\. * x + n_y #2
return x, y
1 外生变量的分布是正态(高斯)的。
2 功能赋值是线性的。
线性高斯 SCMs 在社会科学中使用的计量经济学方法中特别受欢迎,因为模型假设具有许多吸引人的统计特性。此外,在线性模型中,我们可以将父因果回归变量系数解释为该父变量对效应(响应)变量的因果效应(平均处理效应)。
6.4.5 线性非高斯加性模型
线性非高斯加性模型(LiNGAM)在对于外生变量高斯假设不适用时很有用。在我们的例子中,蛋白质 Y 的数量不能为负,但在线性模型中,如果 β[0]、x 或 n[x] 的值较低,这种情况很容易发生。LiNGAM 模型通过允许外生变量具有非正态分布来解决这个问题。
列表 6.12 Pyro 的 LiNGAM 模型示例
from pyro import sample
from pyro.distributions import Gamma
def LiNGAM():
n_x = sample("N_x", Gamma(9., 1.)) #1
n_y = sample("N_y", Gamma(9., 1.)) #1
x = 10\. + n_x #2
y = 2\. * x + n_y #2
return x, y
1 与正态(高斯)分布不同,外生变量具有相同均值和方差的伽马分布。
2 这些是线性高斯模型中的相同赋值函数。
在先前的模型中,我们使用伽马分布。伽马分布中的最低可能值是 0,因此 y 不能为负。
6.4.6 非线性加性噪声模型
正如我提到的,SCM 的力量在于选择能够反映如何原因影响其直接效应的功能赋值。在我们的假设例子中,你是一位生物化学家。你能从生物化学中导入知识来设计赋值函数吗?以下就是这个推理可能的样子。(在这个例子中,你不需要理解生物学或数学,只需逻辑即可)。
在酶建模中有一个常见的数学假设,称为质量作用动力学。在这个模型中,T 是目标蛋白质的最大可能量。生化反应在实时发生,在这段时间内,目标蛋白质的数量在稳定在某个平衡值 Y 之前会波动。设 Y(t) 和 X(t) 为给定时间点的目标蛋白质和酶的量。质量作用动力学给我们以下常微分方程:

在这里,v 和 α 是速率参数,它们表征了不同生化反应在时间上发生的速率。这个微分方程有以下的平衡解,

其中 Y 和 X 是 Y(t) 和 X(t) 的平衡值,且 β = v/α。
作为一名酶生物学家,你知道这个方程捕捉了该系统生物化学实际机制的某些方面,就像物理学方程如欧姆定律和流行病学中的 SIR 模型。你选择使用这个方程作为Y的分配函数:

这是一个非线性加性噪声模型(ANM)。一般来说,ANMs 具有以下结构:
V = g(V[pa]) + N[v]
在我们的例子中 g(X) = T × β X / (1 + β X)。N[y]可以是正态(高斯)或非高斯。
将动态建模和仿真与 SCMs(供应链管理)连接
动态模型描述了系统行为随时间演变的方式。正如你在酶建模示例中看到的,动态建模的使用是解决 SCMs 知识获取问题的方法之一。
在本节中,我说明了酶生物学家如何使用特定领域的动态模型,特别是常微分方程(ODE),来构建 SCM。常微分方程只是动态模型的一种类型。另一个例子是计算机仿真模型,如用于气候建模、电网建模和制造业的仿真器。仿真器还可以模拟复杂的社会过程,如金融市场和流行病。仿真软件是一个不断增长的数十亿美元的市场。
在仿真器和其他动态模型中,指定因果关系的“如何”可能比在 SCMs 中更容易。SCMs 需要分配函数来明确捕捉系统的全局行为。动态模型只需要你指定事物从瞬间到瞬间如何变化的规则。然后,通过运行仿真,你可以看到全局行为。权衡的是,动态模型可能运行起来计算成本高昂,并且通常难以在数据上训练动态模型的参数或根据数据作为证据进行推理。这促使了将动态模型的启发式便利性与 SCMs 的统计和计算便利性相结合的有趣研究。
接下来,我们将探讨如何使用回归工具来训练这些加性模型。
6.4.7 使用回归工具训练加性模型 SCMs
在统计学中,回归建模找到参数值,以最小化一组预测变量参数化函数与响应变量之间的差异。回归建模库无处不在,加性 SCM 模型的一个优点是它们可以使用这些库来拟合数据上的 SCM 参数。例如,加性模型的参数可以使用标准的线性和非线性回归参数拟合技术(例如,广义最小二乘法)进行拟合。我们还可以利用这些工具的回归拟合优度统计来评估模型解释数据的好坏。
注意,在一般回归模型中的预测者可以是任何你喜欢的。大多数回归建模教学法鼓励你继续添加预测者,以增加拟合优度(例如,调整 R 平方)或减少预测误差。但在 SCM 中,你的预测者仅限于直接的内因原因。
我可以使用广义线性模型作为 SCM 吗?
在统计建模中,广义线性模型(GLM)是线性回归的灵活推广。在 GLM 中,响应变量与预测者的线性函数相关联,并有一个 链接函数。此外,响应变量的方差可以是预测者的函数。例如,包括逻辑回归、泊松回归和伽马回归。GLMs 是数据科学家的基本统计工具集。
在 CGM(非 SCM)中,GLM(广义线性模型)是因果马尔可夫核的不错选择。但一个常见的问题是 GLM 是否可以用作 SCM 中的分配函数。
几个 GLM 与加性 SCM 的结构相匹配,但通常最好不将 GLM 视为 SCM 的模板。SCM 中分配函数的功能形式旨在反映变量与其因果父变量之间的因果关系本质。GLM 的功能形式将一个(在某些情况下非线性的)链接函数应用于预测者的线性函数。链接函数的设计是为了将预测者的线性函数映射到标准分布(例如,正态分布、泊松分布、伽马分布)的均值。它并不是为了反映因果假设。
6.4.8 超越加性模型
如果分配函数的“如何”需要比加性模型能捕捉到的更细微的差别,不要将自己限制在加性模型上。以生物化学为例,不难想出内因和外因之间的相互作用会促使采用乘性模型的场景。
对于这些更复杂的场景,开始使用概率深度学习工具来实现 SCM(结构化因果模型)是有意义的。
6.5 将 SCM 与深度学习结合
让我们回顾一下酶动力学模型,其中酶 X 的量是目标蛋白 Y 量的原因,如图 6.22 所示。

图 6.22 酶 (X) 的量是蛋白质 (Y) 测量量的原因。
我之前提到,基于在酶生物学研究中流行的动态数学模型,Y 的加性分配函数的一个良好候选者是

此外,假设我们从实验中得知 T 为 100,β 为.08。
理想情况下,我们希望能够从数据中重现这些参数值。更好的是,我们希望利用现代深度学习背后的基于自动微分框架。
6.5.1 使用基本 PyTorch 实现和训练 SCM
首先,让我们创建一个 PyTorch 版本的酶模型。
列表 6.13 实现 PyTorch 酶模型
from torch import nn
class EnzymeModel(nn.Module): #1
def __init__(self):
super().__init__()
self.*β* = nn.Parameter(torch.randn(1, 1)) #2
def forward(self, x):
x = torch.mul(x, self.*β*) #3
x = x.log().sigmoid() #4
x = torch.mul(x, 100.) #5
return x
1 创建酶模型。
2 初始化参数 D�。
3 计算酶量 X 和 D� 的乘积。
4 实现函数 u / (u + 1) 作为 sigmoid(log(u)),因为 sigmoid 和 log 函数是 PyTorch 的本地转换。
5 乘以 T = 100。
假设我们观察到了来自该系统的数据,如图 6.23 所示。

图 6.23 示例酶数据。X 是酶的量,Y 是目标蛋白的量。
让我们尝试使用基本的 PyTorch 工作流程从这个数据中学习 β。
列表 6.14 使用 PyTorch 拟合酶数据
import pandas as pd
from torch import tensor
import torch
df = pd.read_csv("https://raw.githubusercontent.com/altdeep
/causalML/master/datasets/enzyme-data.csv") #1
X = torch.tensor(df['x'].values).unsqueeze(1).float() #2
Y = torch.tensor(df['y'].values).unsqueeze(1).float() #2
def train(X, Y, model, loss_function, optim, num_epochs): #3
loss_history = [] #3
for epoch in range(num_epochs): #3
Y_pred = model(X) #3
loss = loss_function(Y_pred, Y) #3
loss.backward() #3
optim.step() #3
optim.zero_grad() #3
if epoch % 1000 == 0: #4
print(round(loss.data.item(), 6)) #4
torch.manual_seed(1) #5
enzyme_model = EnzymeModel()
optim = torch.optim.Adam(enzyme_model.parameters(), lr=0.00001) #6
loss_function = nn.MSELoss() #7
train(X, Y, enzyme_model, loss_function, optim, num_epochs=60000)
1 从 GitHub 加载酶数据。*#2 将数据转换为张量。
3 创建训练算法。
4 打印训练过程中的损失。
5 设置随机种子以实现可重复性。
6 初始化 Adam 优化器的一个实例。由于损失对 D� 的小变化非常敏感,因此使用低学习率。
7 使用均方损失误差相当于假设 Ny 是可加的和对称的。当我用给定的随机种子运行此代码时,它产生了一个 0.1079 的值(你可以通过打印 enzyme_model.β*.data 来访问这个值),这个值仅略低于真实值 0.08。此实现没有明确表示外生变量 N[y],但统计学理论告诉我们,使用均方误差损失函数相当于假设 N[y] 是可加的并且具有正态分布。然而,它还假设正态分布具有恒定的方差,而散点图中的漏斗形状表明 N[y] 的方差可能随着 X 的值而增加。
6.5.2 使用概率 PyTorch 训练 SCM
这种基本参数优化方法的问题在于 SCM 应该编码一个分布 P(X, Y)。因此,我们可以转向概率建模方法来拟合此模型。
列表 6.15 在概率酶模型中进行贝叶斯估计 β
import pyro
from pyro.distributions import Beta, Normal, Uniform
from pyro.infer.mcmc import NUTS, MCMC
def g(u): #1
return u / (1 + u) #1
def model(N): #2
*β* = pyro.sample("*β*", Beta(0.5, 5.0)) #3
with pyro.plate("data", N): #4
x = pyro.sample("X", Uniform(0.0, 101.0)) #5
y = pyro.sample("Y", Normal(100.0 * g(*β* * x), x**.5)) #6
return x, y
conditioned_model = pyro.condition( #7
model, #7
data={"X": X.squeeze(1), "Y": Y.squeeze(1)} #7
) #7
N = X.shape[0] #8
pyro.set_rng_seed(526) #9
nuts_kernel = NUTS(conditioned_model, adapt_step_size=True) #10
mcmc = MCMC(nuts_kernel, num_samples=1500, warmup_steps=500) #10
mcmc.run(N) #10
1 用于 Y(目标蛋白的量)的分配函数中使用的简单转换
2 概率模型
3 我们打算用此模型拟合的参数 β 的先验。
4 为 X 和 Y 的 N=100 个相同且独立分布的值提供一个“板”。
5 酶的边缘概率 P(X) 是 0 到 101 之间的均匀分布。
6 P(Y|X) 是给定 X(和 β)的 Y(蛋白质浓度)的条件分布。我用一个具有均值和方差的正态分布来模拟 P(Y|X),这两个值都取决于 Y。
7 在观察到的证据上对模型进行条件化。
8 获取数据中的示例数量(100)。
9 设置随机种子以实现可重复性。
10 为了学习 β,我使用了一个基于梯度的 MCMC 算法,称为 No-U-Turn Sampler(NUTS)。这是许多概率参数学习方法之一,这个选择与你的模型因果元素无关。
这种方法的缺点是它没有显式地表示外生变量。如果我们想使用概率机器学习框架来构建 SCM,我们需要使外生变量显式化。这在前面的方法中具有挑战性,原因之一非常微妙:当我用 Pyro 代码写下以下语句时,y = pyro.sample("Y", Normal(…, …)),Pyro 知道使用那个正态分布来计算训练数据中每个 Y 值的概率值(更精确地说,是 似然值)。这些值用于概率推理算法,如 MCMC。但如果我们写一个表示分配函数的语句,如 y = f(x, ny),Pyro 不会自动知道如何计算 Y 的概率值,特别是由于 Pyro 认为 f(.)可以是任何东西。
但还有一个比这个问题更重要的问题。到目前为止,我们一直假设我们方便地知道一个基于域的数学函数形式,用于 Y 的分配函数。使用深度学习来拟合分配函数会很理想,但这存在问题。
6.5.3 神经网络 SCM 和正态化流
假设我们使用神经网络来对 y = f(x, ny) 进行建模。实际上,对于给定的 SCM,我们可以使用多层神经网络来对每个变量进行建模,前提是已知其父变量——我们可以称之为“神经网络 SCM”。问题是,我们希望用于我们的分配函数的可训练函数类能够代表我们对因果“如何”的假设。神经网络作为通用的函数逼近器,按照定义,与曲线拟合函数一样,几乎没有假设。因此,为了使用神经网络 SCM,我们需要方法来约束神经网络分配函数,使其忠实于我们的“如何”假设。这可以通过对训练特征、损失函数和神经网络架构元素施加约束来实现。正态化流是后者的一个例子。
回到酶建模的例子,让我们首先列举一些关于酶和它们帮助合成的蛋白质之间关系的基本生物学假设:
-
蛋白质离开系统的过程与酶的量无关。因此,我们预计在酶的量一定的情况下,目标蛋白的量会 单调递增。
-
然而,系统往往会饱和,这意味着添加更多酶的回报递减。
我们需要一个只允许单调函数且有递减回报的神经网络方法。为此,我们将使用一种称为 正态化流 的深度生成建模方法。
正态化流将复杂概率密度建模为简单基密度可逆变换。我将使用流来将内生变量的分布建模为外生变量分布的可逆变换。有许多不同的变换,但我将使用 神经样条。¹ 样条是使用分段多项式进行曲线拟合的几十年老方法;神经样条是样条的网络版本。
列表 6.16 为分配函数初始化样条
from pyro.distributions.transforms import conditional_spline
print(conditional_spline(input_dim=1, context_dim=1)) #1
1 神经样条变换是一种可逆的 PyTorch 神经网络模块。
我们得到一个具有 ReLU 激活函数的三层神经网络:
ConditionalSpline(
(nn): DenseNN(
(layers): ModuleList(
(0): Linear(in_features=1, out_features=10, bias=True)
(1): Linear(in_features=10, out_features=10, bias=True)
(2): Linear(in_features=10, out_features=31, bias=True)
)
(f): ReLU()
)
)
正态化流解决了我们没有 y = f(x, ny) 的似然值的问题。与其他概率机器学习模型一样,它们允许我们使用变换层将输入随机变量(如外生变量)连接到输出变量(如内生变量)。关键区别在于,正态化流模型自动计算数据中输出变量的实例的概率值(使用概率论中的 变量变换公式)。这种自动计算依赖于单调性;我们的因果“如何”假设是酶浓度与蛋白质丰度之间的关系是单调的,而正态化流为我们提供了单调性。
例如,在以下代码中,NxDist 是外生变量 N[x] 的分布。我们将分布设置为均匀分布(0, 1)。f_x 是 X 的分配函数,实现为一个将此分布映射到均匀分布(1, 101)的 AffineTransformation。
列表 6.17 将 Nx 的分布转换为 X 的分布
from pyro.distributions import TransformedDistribution
from pyro.distributions.transforms import AffineTransform
NxDist = Uniform(torch.zeros(1), torch.ones(1)) #1
f_x = AffineTransform(loc=1., scale=100.0) #2
XDist = TransformedDistribution(NxDist, [f_x]) #3
1 X 的外生分布是均匀分布(0, 1)。
2 f_x 的分配函数。AffineTransform 将 Nx 乘以 100 并加 1。
3 XDist 是 P(X) 的显式表示。乘以 100 并加 1 得到均匀分布(1, 101)。
因此,XDist 允许我们在其值由分配函数确定的情况下计算 X 的概率值。您可以使用 XDist.log_prob(torch.tensor([50.0])) 计算值为 50 的对数概率值,在均匀分布(1, 101)下将是 log(1/100)。
首先,让我们明确模型。
列表 6.18 指定基于流的 SCM
import pyro
from pyro.distributions import (
ConditionalTransformedDistribution,
Normal, Uniform,
TransformedDistribution
)
from pyro.distributions.transforms import (
conditional_spline, spline
)
import torch
from torch.distributions.transforms import AffineTransform
pyro.set_rng_seed(348)
NxDist = Uniform(torch.zeros(1), torch.ones(1)) #1
f_x = AffineTransform(loc=1., scale=100.0) #2
XDist = TransformedDistribution(NxDist, [f_x]) #3
NyDist = Normal(torch.zeros(1), torch.ones(1)) #4
f_y = conditional_spline(input_dim=1, context_dim=1) #5
YDist = ConditionalTransformedDistribution(NyDist, [f_y]) #6
1 X 的外生分布是均匀分布(0, 1)。
2 f_x 的分配函数。AffineTransform 将 Nx 乘以 100 并加 1。
3 XDist 是 P(X) 的显式表示。乘以 100 并加 1 得到均匀分布(1, 101)。
4 Y 的外生分布是正态分布(0, 1)。
5 我们使用神经样条实现 f_y 的分配函数。优化将优化此样条的参数。
6 YDist 是 P(Y|X) 的显式表示。
现在我们开始训练。
列表 6.19 训练 SCM
import matplotlib.pyplot as plt
modules = torch.nn.ModuleList([f_y]) #1
optimizer = torch.optim.Adam(modules.parameters(), lr=3e-3) #2
losses = []
maxY = max(Y) #3
Ynorm = Y / maxY #3
for step in range(800):
optimizer.zero_grad() #4
log_prob_x = XDist.log_prob(X) #5
log_prob_y = YDist.condition(X).log_prob(Ynorm) #6
loss = -(log_prob_x + log_prob_y).mean() #7
loss.backward() #7
optimizer.step() #7
XDist.clear_cache()
YDist.clear_cache()
losses.append(loss.item())
plt.plot(losses[1:]) #8
plt.title("Loss") #8
plt.xlabel("step") #8
plt.ylabel("loss") #8
1 为 Y 注册神经样条功能分配函数。
2 初始化优化器。
3 对 Y 进行归一化,因为分配函数正在与神经网络一起工作。
4 将所有梯度设置为 0。
5 使用 P(X)计算每个 X 值的对数似然值。
6 使用 P(Y|X)计算给定 X 的每个 Y 值的对数似然值。
7 使用最大似然作为目标,拟合神经网络模块的参数。
8 在训练期间可视化损失。
图 6.24 显示了训练过程中的损失。

图 6.24 基于流的 SCM 训练过程的训练损失
现在我们可以从模型中生成样本,并将它们与训练数据进行比较。
列表 6.20 从训练模型生成
x_flow = XDist.sample(torch.Size([100,])) #1
y_flow = YDist.condition(x_flow).sample(torch.Size([100,])) * maxY #1
plt.title("""
Observed values of enzyme concentration X\n
and protein concentration Y""") #2
plt.xlabel('X') #2
plt.ylabel('Y') #2
plt.xlim(0, 105) #2
plt.ylim(0, 120) #2
plt.scatter( #2
X.squeeze(1), Y.squeeze(1), color='firebrick', #2
label='Actual Data', #2
alpha=0.5 #2
) #2
plt.scatter( #2
x_flow.squeeze(1), y_flow.squeeze(), #2
label='Generated values from trained model', #2
alpha=0.5 #2
) #2
plt.legend() #2
plt.show() #2
1 从训练模型生成合成示例。
2 将合成的示例可视化在训练数据中的示例上,以验证模型拟合。
图 6.25 将生成的样本叠加在训练数据中的实际示例上。

图 6.25 从训练模型生成的示例叠加在训练数据中的实际示例上
能够像其他神经网络框架一样具有多层流动,这使得它成为一个极其灵活的建模类别。但这不仅仅是一个曲线拟合练习。在第五章的变分自动编码器示例中,您看到可以使用神经网络将因果父节点映射到 CGM 泛类中的子效果。但对于 SCM 来说,即使您确定性设置内生变量,这也不够。再次强调,SCM 以分配函数的形式反映了关于因果“如何”的因果假设。在这个酶示例中,我们断言酶和蛋白质丰度之间的单调关系对我们想要做的因果推断很重要,因此我们在分配函数中限制了神经网络(和其他转换)以保持单调性。
摘要
-
结构因果模型(SCMs)是一种因果图模型(CGM),它编码的因果假设超越了因果有向无环图(DAG)中编码的假设。因果 DAG 假设捕捉了什么导致什么。SCM 还捕捉了如何原因影响效果。
-
SCM 由外生变量、这些外生变量的概率分布、内生变量和功能分配组成。
-
外生变量代表未建模的原因。
-
内生变量是显式包含在模型中的变量,对应于我们在之前的因果 DAG 中看到的节点。
-
功能分配在给定其因果父变量的情况下,确定性地为每个内生变量设置。
-
SCM 的附加假设以功能分配的形式代表了因果的“如何”。
-
SCM 代表因果的确定性观点,如果所有原因都已知,则结果可以确定。
-
你可以从一个更一般的(非 SCM)CGM 中推导出 SCM。但给定一个一般的 CGM,可能存在多个 SCM,它们包含与该 CGM 相同的 DAG 和联合概率分布。
-
你不能仅从数据中的统计信息中学习 SCM 的功能分配。
-
SCM 是表示具有简单、确定性规则(如游戏)的明确系统的理想选择。
-
加性噪声模型为从头开始构建 SCMs 提供了一个有用的模板。
-
正态化流是当你的因果“如何”假设是单调性时,用于建模 SCMs 的有用概率机器学习框架。
[1] 关于神经网络样条的更多信息,请参阅 C. Durkan, A. Bekasov, I. Murray, 和 G. Papamakarios 在Advances in neural information processing systems, 32 (NeurIPS 2019)中的论文“Neural spline flows”。
第七章:干预和因果关系
本章涵盖
-
机器学习工程背景下的干预案例研究
-
干预与 A/B 测试和随机实验的关系
-
使用干预操作符在因果模型上实施干预
-
使用因果模型来表示许多干预分布
-
因果效应作为干预分布的自然扩展
干预是代理机构为了引起其他事物发生而采取的行动。干预会改变数据生成过程(DGP)。
干预是我们定义因果关系中最基本的概念。例如,干预的概念,用“操纵”和“变化”一个因素来表述,是 1979 年一本有影响力的实验设计教科书中定义的核心:
因果关系中的典范断言是:对原因的操纵将导致结果的操纵……因果关系意味着通过改变一个因素,我可以使另一个因素发生变化。¹
干预是我们从相关性到因果性的途径。相关性是对称的;关于“亚马逊的笔记本电脑销量与亚马逊的笔记本电脑包销量相关”和“亚马逊的笔记本电脑包销量与亚马逊的笔记本电脑销量相关”的陈述是等价的。但干预使因果关系成为单向的:如果亚马逊推荐笔记本电脑的销售,笔记本电脑包的销量将会增加,但如果亚马逊推广笔记本电脑包的销售,我们不会期望人们通过购买新的笔记本电脑来填充它们。
一个模型必须有一种推理干预的方法才能进入因果模型俱乐部。任何允许你推理干预如何改变 DGP 的模型,按定义,就是一个因果模型。
你可能已经熟悉以实验形式进行的干预,例如 A/B 测试或随机临床试验。这类实验专注于推断因果关系。简单来说,因果关系就是比较不同干预措施(例如,治疗和控制,或 A/B 测试中的“A”和“B”)的预期结果。
在本章中,你将学习如何建模干预和因果关系,即使实际上我们无法或不能在现实生活中进行干预。我们将从激发建模干预的案例研究开始本章。所有用于执行这些案例研究的数据库和笔记本都可在www.altdeep.ai/p/causalaibook找到。
7.1 干预案例研究
机器学习模型可以驱动决策以进行“干预”。这些干预反过来又可能创造出与模型训练期间不同的条件。这种训练条件和部署条件的不匹配可能导致问题。
7.1.1 案例研究:预测天气与业务表现
每天当你醒来,望向窗外,猜测今天是否会下雨。基于这个猜测,你决定是否在早上上班的路上带上伞。有好几次你猜错了;要么你带了伞但没下雨,看起来像个傻瓜,要么你没带伞,结果下雨了,看起来很湿。你决定训练一个机器学习模型,该模型将在早上获取详细的气象数据,并预测是否会下雨。通过利用机器学习来获得更准确的预测,你期望在决定是否带伞时犯的错误更少。
你首先收集每日气象数据作为特征,并记录是否下雨作为标签。经过足够多的日子,你就有了第一块训练数据。接下来,你在这些训练数据上训练模型,并在保留数据上验证其准确性。最后,你部署了训练好的模型,这意味着你每天使用它来决定是否带伞。当你使用部署的模型时,你继续每天记录特征和标签。最终,你有了足够多的额外数据来形成第二个训练块,你重新训练模型以从这两块数据中受益,从而比仅训练第一块数据后获得更高的准确性。随着你收集更多数据块,你继续迭代地训练模型。图 7.1 说明了工作流程。

图 7.1 机器学习训练工作流程的示例,其中传感器数据是特征,天气是标签,带伞是决策。在每个训练块之后,新数据用于更新旧模型,并部署新模型。在这种情况下,决策不会影响未来的数据。
现在让我们考虑一个并行于商业的例子。你是一家公司的数据科学家。你有的不是气象数据,而是经济和行业数据。你不是预测天气是否会下雨,而是预测季度是否会以低收入结束。你不是决定是否带伞,而是决定是否做广告。图 7.2 说明了工作流程,这与图 7.1 中的天气例子完全相同;图 7.1 中的晴朗和雨天映射到图 7.2 中的好季度和坏季度,而带伞或不带伞的决定映射到做广告或不做广告的决定。

图 7.2 这是图 7.1 中工作流程的镜像示例。业务指标是特征,季度业绩是标签,广告是决策。在这种情况下,决策会影响未来的数据。
尽管这两个例子中的标签和决策相互对应,但商业示例的因果关系在本质上不同;带伞的行为不会影响未来几天的天气,但广告行为会影响未来几个季度的商业。因此,训练块 2 代表了一个与训练块 1 不同的 DGP,因为在训练块 2 中的收入受到了广告的影响。在训练过程中,一个简单的预测模型可能会将一个糟糕季度的迹象与高收入联系起来,因为在过去,糟糕季度的迹象导致你的公司进行广告宣传,从而提高了收入。
我们部署机器学习模型来驱动或自动化决策。这些决策不会影响气象学、地质学和天文学等领域的数据。但在我们想要使用机器学习的许多(如果不是大多数)领域,这些由模型驱动的决策是干预——改变 DGP 的行动。这可能导致模型训练和部署条件之间的不匹配,导致模型可靠性的问题。
这个问题的另一个现实世界例子发生在异常检测中。
7.1.2 案例研究:信用欺诈检测
异常检测旨在预测何时发生异常事件。一个例子是在信用卡上检测欺诈交易。信用卡公司使用交易数据对欺诈预测模型进行监督训练,其中信用卡交易属性(购买模式、位置、商品成本等)是特征,而客户是否后来报告交易为欺诈是标签。
正如天气和商业示例中一样,你在一个初始训练块上训练模型。训练完成后,你可以部署算法以实时预测欺诈。当交易启动时,算法运行,并生成预测。如果算法预测欺诈,则拒绝交易。
当这个系统处于部署状态时,第二个训练集正在被编译。一些欺诈仍然得以通过,并被客户后来报告为欺诈。这些交易在这个新数据块中被标记为欺诈,但 DGP(数据生成过程)已经从初始训练集改变。部署的 1.0 版本预测模型正在拒绝它预测为欺诈的交易,但由于它们被拒绝,你不知道它们是否真的是欺诈案例。这些被拒绝的交易因为缺乏标签而被排除在下一个训练集之外。
如果模型在第二个块上重新训练,它可能会发展出对欺诈的偏见,这种偏见在欺诈拒绝系统中被遗漏,并对被拒绝的欺诈案例产生偏见。这种偏见可能会在多次迭代中变得更加严重。这个过程类似于一个随着时间的推移在解决涉及不常见武器的案件方面做得很好但在涉及枪支的案件方面做得不好的谋杀案侦探。
在部署中过滤欺诈交易是一种干预措施。在实践中,异常检测算法通过某种方式考虑干预来解决这个问题。
7.1.3 案例研究:在线角色扮演游戏的统计分析
假设你是一家在线角色扮演游戏公司的数据科学家。你的领导想知道支线任务参与度(与游戏主要目标无关的迷你目标)是否是虚拟物品游戏内购买的驱动因素。如果答案是肯定的,公司将在游戏动态中干预,使玩家参与更多支线任务。
你进行了一项分析。你查询数据库并抽取了一千名玩家的记录,其中前五名显示在表 7.1 中。这是观察数据(与实验数据相对),因为数据是玩家登录和玩游戏时的自然行为记录。(完整数据集可在本章的笔记本中找到:www.altdeep.ai/p/causalaibook。)
表 7.1 支线任务参与度 和 游戏内购买 观察数据的示例行
| 用户 ID | 支线任务参与度 | 游戏内购买 |
| --- | --- | --- | --- |
| 71d44ad5 | 高 | 156.77 |
| e6397485 | 低 | 34.89 |
| 87a5eaf7 | 高 | 172.86 |
| c5d78ca4 | 低 | 215.74 |
| d3b2a8ed | 高 | 201.07 |
| dc85d847 | 低 | 12.93 |
标准的数据科学分析将涉及对假设进行统计检验,即高度参与支线任务的玩家的 游戏内购买 与低 支线任务参与度 的玩家之间存在差异。该测试计算两组 游戏内购买 样本均值之间的数学差异。在统计学上,这个差异估计了一个 效应量。该测试将检验这个估计的效应量是否与零有显著差异。
设置你的环境
本章的代码是用 Pyro 版本 1.9.0、pandas 版本 2.2.1 和 pgmpy 版本 0.1.25 编写的。使用 Pyro 的 render 函数将 Pyro 模型可视化为 DAG 将需要 Graphviz。访问 www.altdeep.ai/p/causalaibook 获取包含代码的笔记本链接。
我们将使用 pandas 库执行这个假设检验。首先,我们将获取数据并获取每个组内的样本均值和标准差。
列表 7.1 加载 支线任务参与度 与 游戏内购买 数据并总结
import pandas as pd
data_url = ( #1
"https://raw.githubusercontent.com/altdeep/causalML/master/" #1
"datasets/sidequests_and_purchases_obs.csv" #1
) #1
df = pd.read_csv(data_url) #1
summary = df.drop('User ID', axis=1).groupby( #2
["Side-quest Engagement"] #2
).agg( #2
['count', 'mean', 'std'] #2
) #2
summary
1 从数据库查询中加载数据到 pandas DataFrame。
2 对于每个 支线任务参与度 的级别(“低”,“高”),计算样本计数(玩家数量)、游戏内购买的平均金额和标准差。
这生成了表 7.2 的总结。
表 7.2 在线游戏数据的汇总统计
| 支线任务参与度 | 平均购买 | 标准差 | n |
|---|---|---|---|
| 低 | 73.10 | 75.95 | 518 |
| 高 | 111.61 | 55.56 | 482 |
这个数据库查询检索了 1,000 名玩家,其中 482 名高度参与支线任务,518 名没有参与。高度参与玩家的平均游戏内购买金额约为 112 美元,对于高支线任务参与度,而低支线任务参与度为 73 美元。将数据推广到这个范围之外,我们得出结论,高度参与支线任务的玩家平均比不参与的玩家多花费 112 - 73 = 39 美元。我们可以运行双样本Z测试来确保这种差异是显著的。
列表 7.2 测试参与对游戏内购买的影响是否具有统计学意义
n1, n2 = summary['In-game Purchases']['count'] #1
m1, m2 = summary['In-game Purchases']['mean'] #2
s1, s2 = summary['In-game Purchases']['std'] #3
pooled_std = (s1**2 / n1 + s2**2 / n2) **.5 #4
z_score = (m1 - m2) / pooled_std #5
abs(z_score) > 2\. #6
1 n1 和 n2 是每个组(高参与度与低参与度)中的玩家数量。
2 m1 和 m2 是组样本均值。
3 s1 和 s2 是组标准差。
4 通过合并(组合)组标准差来估计平均支出的差异标准误。
5 将数据转换为 z 分数,该分数在(零假设)不同参与级别之间支出无差异的情况下具有标准正态分布。
6 测试 z 分数是否超过 0 的 2 个标准差,这超过了 5%的显著性阈值。
运行此代码显示均值差异是显著的。太好了,你进行了一些数据科学,表明你有一个具有统计学意义的效果量:与不参与的玩家相比,高度参与支线任务的玩家的游戏内购买显著更高。基于你的发现,管理层决定修改游戏动态以吸引玩家参与更多支线任务。结果,游戏内购买量下降。这怎么可能发生呢?
7.1.4 从随机实验到干预
到现在为止,你可能已经认识到列表 7.2 的结果是相关关系不意味着因果关系的教科书式例子。如果管理层想知道干预游戏动态是否会增加游戏内购买,他们应该依赖于来自随机实验的分析,而不是简单的观察数据。我们将使用随机实验来增强对干预正式模型的直觉,并看看这个干预模型如何模拟随机实验。
7.1.5 从观察到实验
假设你运行的不是观察研究,而是实验。而不是从 SQL 查询中提取数据,你随机选择一组 1,000 名玩家,并将他们随机分配到两个 500 人的组中。在一个组中,游戏动态被修改,使得支线任务参与度被人为地固定在“低”,而在另一个组中则固定在“高”。然后我们将观察他们的游戏内购买水平。
这将创建与第 7.1.3 节中观察数据大小相同,并且参与和不参与玩家大致分割相同的实验数据。同样,我们将运行相同的前向分析。这将使我们能够对使用观察数据与实验数据进行苹果对苹果的比较。
表 7.3 显示了实验数据的示例。您可以在 www.altdeep.ai/p/causalaibook 找到数据链接。
表 7.3 评估 支线任务参与度 对 游戏内购买 影响的实验数据示例行
| 用户 ID | 支线任务参与度 | 游戏内购买 |
|---|---|---|
| 2828924d | 低 | 224.39 |
| 7e7c2452 | 低 | 19.89 |
| 3ddf2915 | 低 | 221.26 |
| 10c3d883 | 高 | 93.21 |
| c5080957 | 高 | 61.82 |
| 241c8fcf | 高 | 188.76 |
再次,我们使用以下代码总结数据。
列表 7.3 加载实验数据并总结
import pandas as pd
exp_data_url = ( #1
"https://raw.githubusercontent.com/altdeep/causalML/master/" #1
"datasets/sidequests_and_purchases_exp.csv" #1
) #1
df = pd.read_csv(exp_data_url) #1
summary = df.drop('User ID', axis=1).groupby( #2
["Side-quest Engagement"] #2
).agg( #2
['count', 'mean', 'std'] #2
)
print(summary)
1 从数据库查询中加载实验数据到 pandas DataFrame。
2 对于每个支线任务参与度级别(“低”,“高”),计算样本计数(玩家数量)、样本平均游戏内购买金额和标准差。
表 7.4 显示了与表 7.2 对观察数据所做的相同汇总统计,对于实验数据。
表 7.4 在线游戏实验数据的汇总统计
| 支线任务参与度 | 平均购买 | 标准差 | n |
|---|---|---|---|
| 低 | 92.99 | 51.67 | 500 |
| 高 | 131.38 | 94.84 | 500 |
实验反映了公司干预以增加支线任务参与度时发生的情况。与我们的第一次分析相比,效应量的符号是负的;我们得到了 –38.39,这意味着平均购买量下降了 $38.39。当我们重新运行列表 7.4 中的显著性测试时,我们看到实验数据与观察数据一样,差异是显著的。
列表 7.4 对(实验)平均购买差异进行显著性测试
n1, n2 = summary['In-game Purchases']['count'] #1
m1, m2 = summary['In-game Purchases']['mean'] #2
s1, s2 = summary['In-game Purchases']['std'] #3
pooled_std = (s1**2 / n1 + s2**2 / n2) **.5 #4
z_score = (m1 - m2) / pooled_std #5
abs(z_score) > 2\. #6
1 n1 和 n2 是每个小组(高参与度 vs 低参与度)中的玩家数量。
2 m1 和 m2 是组样本均值。
3 s1 和 s2 是组标准差。
4 通过合并(组合)组标准差来估计平均花费差异的标准误差。
5 转换为 z 分数,在(零假设)花费水平无差异的假设下具有标准正态分布。
6 测试 z 分数是否超过 0 的 2 个标准差,这超过了 5% 的显著性阈值。
结果显示,组均值差异再次显著。如果你报告的是实验结果而不是观察研究的成果,你就会正确地得出结论:鼓励更高的支线任务参与度的政策会导致平均游戏内购买(以及你不会推荐这样做)的下降。
这个实验是有成本的。实验中包含的 1000 名玩家中,许多人如果没有被包括在实验中,他们会在游戏内购买上花费更多,尤其是对于被分配到高支线任务组的 500 名玩家。这相当于没有进行实验就会实现的收入损失。此外,实验为那些被分配到与其偏好水平不同的支线任务参与度水平的玩家创造了次优的游戏体验。这些玩家为公司支付了一定体验的费用,而实验降低了这种体验。
最不理想的结果是基于我们对观测数据的简单双样本分析进行报告;这没有成本,但给出了错误的答案。更好的结果是运行实验并得到正确的答案,尽管这需要付出代价。理想的结果是在观测数据上免费得到正确的答案。要做到这一点,我们需要一个因果模型。
7.1.6 从实验到干预
让我们看看如何使用因果模型来模拟从观测数据中得到的实验结果。首先,让我们假设图 7.3 中的因果 DAG。

图 7.3 一个简单的 DAG,展示了支线任务参与度和游戏内购买之间的因果关系。公会成员资格是两者的共同原因。
在我们的在线游戏中,许多玩家是公会成员。公会是一群玩家,他们聚集资源并协调他们的游戏玩法,例如一起完成支线任务。我们的模型假设玩家进行的游戏内购买数量也取决于他们是否在公会中;同一公会的成员聚集资源,许多资源是他们必须购买的虚拟物品。
假设你运行了一个修改后的初始数据库查询版本。查询产生了与表 7.1 中看到的相同的观测数据,但这次它包括了一个额外的列,表示公会成员资格。同样,我们在表 7.5 中看到了六名玩家(与表 7.1 中显示的玩家相同)。
表 7.5 与表 7.1 相同的观测数据,但增加了公会成员资格列
| 用户 ID | 支线任务参与度 | 公会成员资格 | 游戏内购买 |
|---|---|---|---|
| 71d44ad5 | 高 | 成员 | 156.77 |
| e6397485 | 低 | 非成员 | 34.89 |
| 87a5eaf7 | 高 | 非成员 | 12.93 |
| c5d78ca4 | 低 | 成员 | 215.74 |
| d3b2a8ed | 高 | 成员 | 201.07 |
| dc85d847 | 低 | 非成员 | 34.89 |
我们将使用 Pyro 在这个观测数据上构建一个因果图模型。为此,我们需要对因果马尔可夫核进行建模:给定公会成员资格的公会成员、支线任务参与度以及给定公会成员资格和支线任务参与度的游戏内购买的概率分布。在我们的 Pyro 模型中,我们需要为这些变量指定一些典型分布并估计它们的参数。
估计参数和构建模型
Pyro 可以联合估计我们每个因果马尔可夫核的参数,就像它可以估计复杂神经网络架构中的参数一样。但使用日常数据科学分析,通过利用第二章中讨论的参数模块化概念,一次估计每个核的参数会使我们的工作更简单。让我们从公会成员资格开始。
列表 7.5 估计公会成员资格的概率分布
import pandas as pd
full_obs_url = ( #1
"https://raw.githubusercontent.com/altdeep/causalML/master/" #1
"datasets/sidequests_and_purchases_full_obs.csv" #1
) #1
df = pd.read_csv(full_obs_url) #1
membership_counts = df['Guild Membership'].value_counts() #2
dist_guild_membership = membership_counts / sum(membership_counts) #2
print(dist_guild_membership) #2
1 从数据库查询中加载数据到 pandas DataFrame 中。
2 计算成员与非成员的比例。
这将打印出以下结果:
nonmember 0.515
member 0.485
Name: Guild Membership, dtype: float64
这些是数据中公会成员与非成员的比例。我们可以将这些作为估计玩家是成员或非成员的概率。如果我们直接使用这些比例,它们将是概率的最大似然估计,但为了简单起见,我们将将其设置为 50/50(成为成员的概率为.5)。
接下来,我们将对给定公会成员资格的支线任务参与度条件概率分布(CPD)做同样的处理。
列表 7.6 估计支线任务参与度给定公会成员资格的 CPD
member_subset = df[(df['Guild Membership'] == 'member')] #1
member_engagement_counts = ( #1
member_subset['Side-quest Engagement'].value_counts() #1
) #1
dist_engagement_member = ( #1
member_engagement_counts / sum(member_engagement_counts) #1
) #1
print(dist_engagement_member) #1
nonmember_subset = df[(df['Guild Membership'] == 'nonmember')] #2
nonmember_engagement_counts = ( #2
nonmember_subset['Side-quest Engagement'].value_counts() #2
) #2
dist_engagement_nonmember = ( #2
nonmember_engagement_counts / #2
sum(nonmember_engagement_counts) #2
) #2
print(dist_engagement_nonmember) #2
1 计算当玩家是公会成员时,支线任务参与度水平(“高”与“低”)的概率分布。
2 计算当玩家不是公会成员时,支线任务参与度水平(“高”与“低”)的概率分布。
列表 7.6 打印了以下输出,显示了公会成员的支线任务参与度水平比例:
high 0.797938
low 0.202062
以下比例适用于非公会成员:
high 0.184466
low 0.815534
同样,我们将对这些结果进行四舍五入。公会成员有 80%的可能性在支线任务中高度参与,而非成员只有 20%的可能性高度参与。
最后,对于公会成员资格和支线任务参与度的每个组合,我们将计算游戏内购买的样本均值和标准差。当我们编码因果模型中游戏内购买的因果马尔可夫核时,我们将使用这些样本统计量作为典型分布中均值和位置参数的估计。
列表 7.7 按参与度和公会成员资格级别计算购买统计数据*
purchase_dist_nonmember_low_engagement = df[ #1
(df['Guild Membership'] == 'nonmember') & #1
(df['Side-quest Engagement'] == 'low') #1
].drop( #1
['User ID', 'Side-quest Engagement', 'Guild Membership'], axis=1 #1
).agg(['mean', 'std']) #1
print(round(purchase_dist_nonmember_low_engagement, 2)) #1
purchase_dist_nonmember_high_engagement = df[ #2
(df['Guild Membership'] == 'nonmember') & #2
(df['Side-quest Engagement'] == 'high') #2
].drop( #2
['User ID', 'Side-quest Engagement', 'Guild Membership'], axis=1 #2
).agg(['mean', 'std']) #2
print(round(purchase_dist_nonmember_high_engagement, 2)) #2
purchase_dist_member_low_engagement = df[ #3
(df['Guild Membership'] == 'member') & #3
(df['Side-quest Engagement'] == 'low') #3
].drop( #3
['User ID', 'Side-quest Engagement', 'Guild Membership'], axis=1 #3
).agg(['mean', 'std']) #3
print(round(purchase_dist_member_low_engagement, 2)) #3
purchase_dist_member_high_engagement = df[ #4
(df['Guild Membership'] == 'member') & #4
(df['Side-quest Engagement'] == 'high') #4
].drop( #4
['User ID', 'Side-quest Engagement', 'Guild Membership'], axis=1 #4
).agg(['mean', 'std']) #4
print(round(purchase_dist_member_high_engagement, 2)) #4
1 估计具有低支线任务参与度的非公会成员的游戏内购买样本均值和标准差。
2 估计具有高支线任务参与度的非公会成员的游戏内购买样本均值和标准差。
3 估计低支线任务参与度的公会成员的游戏内购买的样本均值和标准差。
4 估计高支线任务参与度的公会成员的游戏内购买的样本均值和标准差。
对于低支线任务参与度的非公会成员,我们有这些结果:
In-game Purchases
mean 37.95
std 23.80
对于高支线任务参与度的非公会成员,我们有
In-game Purchases
mean 54.92
std 4.92
对于低支线任务参与度的公会成员,我们有
In-game Purchases
mean 223.71
std 5.30
对于高支线任务参与度的公会成员,我们有
In-game Purchases
mean 125.53
std 53.44
最后,在第 7.8 列表中,我们使用这些各种统计量作为在 Pyro 中构建的因果图模型中的参数估计。
列表 7.8 在 Pyro 中构建游戏内购买的因果模型
import pyro
from torch import tensor
from pyro.distributions import Bernoulli, Normal
def model():
p_member = tensor(0.5) #1
is_guild_member = pyro.sample( #1
"Guild Membership", #1
Bernoulli(p_member) #1
) #1
p_engaged = (tensor(0.8)*is_guild_member + #2
tensor(.2)*(1-is_guild_member)) #2
is_highly_engaged = pyro.sample( #2
"Side-quest Engagement", #2
Bernoulli(p_engaged) #2
) #2
get_purchase_param = lambda param1, param2, param3, param4: ( #3
param1 * (1-is_guild_member) * (1-is_highly_engaged) + #3
param2 * (1-is_guild_member) * (is_highly_engaged) + #3
param3 * (is_guild_member) * (1-is_highly_engaged) + #3
param4 * (is_guild_member) * (is_highly_engaged) #3
) #3
μ = get_purchase_param(37.95, 54.92, 223.71, 125.50) #4
σ = get_purchase_param(23.80, 4.92, 5.30, 53.49) #4
in_game_purchases = pyro.sample( #4
"In-game Purchases", #4
Normal(μ, σ) #4
) #4
guild_membership = "member" if is_guild_member else "nonmember" #5
engagement = "high" if is_highly_engaged else "low" #5
in_game_purchases = float(in_game_purchases) #5
return guild_membership, engagement, in_game_purchases #5
1 公会成员与非成员的概率为.5。使用这个概率,我们从伯努利分布中生成了一个公会成员资格值(成员为 1,非成员为 0)。
2 我们从伯努利分布(高为 1,低为 0)中生成了一个支线任务参与度的值,其参数取决于公会成员资格。
3 用于计算游戏内购买参数的辅助函数
4 我们使用在观测数据中找到的样本均值来指定游戏内购买的正态分布的位置参数。
5 与均值参数一样,我们使用在数据中找到的标准差来指定游戏内购买的正态分布的尺度参数。
为了确认 Pyro 模型编码了一个因果 DAG,我们可以运行pyro.render_model(model),这将产生图 7.4。

图 7.4 调用pyro.render_model与因果模型的结果
利用概率编程的参数灵活性
注意我们在 Pyro 模型中建模变量的选择灵活性。例如,在建模游戏内购买的分布时,我们使用了正态分布,但我们可以使用其他分布。例如,游戏内购买不能是负数,因此我们可以选择一个仅对正数定义的规范分布,而不是对负数和正数都定义的正态分布。这对于低支线任务参与度的非公会成员特别有用,因为从均值为 37.95 和尺度参数为 23.80 的正态分布生成将大约有 5.5%的概率生成负值。然而,我们选择在这个情况下稍微偷懒,使用正态分布,因为几个负数的游戏内购买对我们的分析结果影响不大。
重点是,概率编程工具如 Pyro 为我们提供了参数灵活性,与 pgmpy 等工具不同。利用这种灵活性来反映你对 DGP 的假设是一种好的做法。
Pyro 的干预抽象
Pyro 在 pyro.do 中提供了一个表示干预的抽象。它接受一个模型并返回一个新的模型,该模型反映了干预。列表 7.9 展示了我们可以如何使用 pyro.do 将之前的模型转换为反映将 侧任务参与度 设置为“高”和“低”的干预的模型。
列表 7.9 使用 pyro.do 表示干预
int_engaged_model = pyro.do( #1
model, #1
{"Side-quest Engagement": tensor(1.)} #1
) #1
int_unengaged_model = pyro.do( #2
model, #2
{"Side-quest Engagement": tensor(0.)} #2
) #2
1 将 侧任务参与度 设置为 1.0(即,“高”)。这将返回一个新的模型。
2 将 侧任务参与度 设置为 0.0(即,“低”)。这将返回一个新的模型。
现在我们有两个新的模型:一个将 侧任务参与度 设置为“高”,另一个将其设置为“低”。如果我们的原始模型是正确的,从每个新干预模型中生成 500 个示例,并将它们合并成 1000 个示例,实际上 模拟 了实验。记住,我们仅使用表 7.4 中展示的观察数据来估计这个因果模型的参数。如果我们能在观察数据上训练一个模型,并使用它来准确模拟实验结果,那么我们就无需实际运行实验。
列表 7.10 使用 int_engaged_model 和 int_unengaged_model 来模拟实验数据。我们可以通过比较此模拟数据的汇总统计与实际实验数据的汇总统计来确认模拟的有效性。
列表 7.10 使用 pyro.do 干预模拟实验数据
pyro.util.set_rng_seed(123) #1
simulated_experimental_data = [ #2
int_engaged_model() for _ in range(500) #2
] + [ #2
int_unengaged_model() for _ in range(500) #2
] #2
simulated_experimental_data = pd.DataFrame( #2
simulated_experimental_data, #2
columns=[ #2
"Guild Membership", #2
"Side-quest Engagement", #2
"In-Game Purchases" #2
] #2
) #2
sim_exp_df = simulated_experimental_data.drop( #3
"Guild Membership", axis=1) #3
summary = sim_exp_df.groupby( #4
["Side-quest Engagement"] #4
).agg( #4
['count', 'mean', 'std'] #4
) #4
print(summary) #4
1 为可重复性设置随机种子。
2 从每个干预模型中模拟 500 行,并将它们合并以创建模拟的实验数据。
3 模拟数据将包括一个公会会员资格列。我们可以删除它以获得看起来像原始实验的模拟数据。
4 重新创建每个参与度级别的游戏内购买统计摘要。
此代码模拟了实验,提供了表 7.6 中的总结。同样,这些是从我们创建的模拟实验中获得的样本统计,我们首先在观察数据上估计了一些参数,其次,使用这些参数构建了一个因果生成模型,第三,使用 pyro.do 来模拟干预的结果。将这些与表 7.7 中的统计数据进行对比,后者是我们从 实际 实验数据中获得的。
表 7.6 模拟实验的汇总统计
| 侧任务参与度 | 数量 | 平均值 | 标准差 |
|---|---|---|---|
| 高 | 500 | 89.897309 | 52.696709 |
| 低 | 500 | 130.674021 | 93.921543 |
表 7.7 实际实验的汇总统计
| 侧任务参与度 | 数量 | 平均值 | 标准差 |
|---|---|---|---|
| 高 | 500 | 92.99054 | 51.673631 |
| 低 | 500 | 131.38228 | 94.840705 |
这两组汇总数据足够相似,我们可以断言我们已经成功地从观察数据中复制了实验结果。
7.1.7 概述
让我们回顾一下。因果 DAG 与 Pyro 干预抽象的结合使我们能够在观察数据集上进行分析,其结果与在实验数据集上进行分析的结果相同。如果您在初始观察数据上运行此分析而不是简单的双样本统计检验,您就会为领导提供正确的答案,他们就不会改变动态来增加 Side-Quest Engagement。
注意,这并不是免费的午餐。这项分析需要以因果 DAG 形式表示的因果假设。在指定因果 DAG 时出现的错误可能导致分析输出的错误。但假设您的因果 DAG 是正确的(或足够接近),这将为您节省实际成本和机会成本。
那么 pyro.do 究竟是如何工作的?它是如何修改模型来表示干预的?我们将通过理想干预和干预算子的概念来回答这些问题。
7.2 理想干预和干预算子
为了理解我们的模拟实验是如何工作的,我们需要干预的明确定义。我们将使用一个特定的定义,称为 理想干预,也称为 原子干预、结构干预、外科干预 和 独立干预。
理想干预的定义可以分为三个部分:
-
理想干预针对 DGP 中的特定变量或一组变量。
-
该操作将那些变量设置为固定值。
-
通过将变量设置为固定值,干预阻止了来自目标原因的影响,使得目标现在在统计上与其原因独立。
我们将使用符号 do(X=x) 来表示一个理想干预,即将 X 设置为 x。请注意,我们可以在一组变量上执行干预,例如 do(X=x, Y=y, Z=z)。
7.2.1 干预算子
因果模型表示 DGP 中的关系。理想干预的前述定义描述了它是如何 改变 DGP 的。现在,剩下的任务是我们如何定义我们的因果模型将如何反映这种变化。
干预算子 是一种改变我们的因果模型以反映干预的方式。创建任何新颖的因果计算表示的第一个任务之一就是为理想干预定义干预算子。
干预算子可以实施理想干预、随机干预(在第 7.5 节中讨论)和其他类型的干预。除非我另有说明,否则您可以假设“干预算子”指的是“理想干预的干预算子”。
幸运的是,结构因果模型和一般因果图模型都有明确定义的干预算子。我们将探讨这些,以及查看为因果程序如 pyro.do 设计的干预算子。
7.2.2 结构因果模型中的理想干预
我们将从结构因果模型开始。让M代表在线游戏的结构因果模型。我们会将M写成如下形式:

f[G],f[E],和f[I]分别是G(公会会员资格),E(支线任务参与),和I(游戏内购买)的分配函数。
理想干预 do(E=“高”)将模型转换如下:

SCM 的干预算子用干预值“高”替换干预目标E的分配函数。
假设你有一个包含变量(或一组变量)X的 SCM。你想要应用干预 do(X=x)。干预算子用干预值替换干预目标的分配函数。
考虑这一点如何满足我们理想干预定义的三个要素:
-
干预 do(X=x)只直接影响X的分配函数。其他分配函数不受影响。
-
干预明确地将X设置为特定的值。
-
由于X的值被设定为常数,它不再依赖于其直接的因果父节点。
7.2.3 图形手术:因果 DAG 和因果图形模型中的理想干预
现在,我们将考虑如何图形化地思考理想的干预。首先,让我们重新审视图 7.5 中在线游戏的因果 DAG。

图 7.5 在线游戏的因果 DAG
根据我们的图表,公会会员资格是支线任务参与的因果父节点。这种父子关系决定了因果马尔可夫核——在公会会员资格给定的情况下,支线任务参与的条件概率分布。回想一下我们展示在表 7.8 中的这个因果马尔可夫核模型。
表 7.8 支线任务参与的因果马尔可夫核的条件概率表
*| | 公会会员资格 |
| --- | --- |
|---|---|
| 非成员 | 成员 |
| --- | --- |
| 支线任务参与 | 低 |
| 高 | .2 | .8 |* *想象一下我们实验的机制。玩家登录,数字实验平台选择一些玩家参与实验。其中一些玩家是公会成员,而另一些则不是。
考虑一个名为 Jojo 的玩家,他不是公会成员,正在登录。根据我们的模型,仅凭这些信息,他在这次游戏会话中高度参与支线任务的概率将是 20%。
但实验平台选择他进行实验。它随机将他分配到高支线任务参与度组。一旦他进入这个组,Jojo 在支线任务中高度参与的概率是多少?答案是 100%。在实验术语中,被分配到处理组的人接触到治疗的可能性是多少?100%。对于熟悉 A/B 测试术语的数据科学家来说,被分配到 A 组的人接触到变体 A 的可能性是多少?100%。
事实上,假设实验对象不是 Jojo,而是 Ngozi,她是一名公会成员。虽然最初 Ngozi 有 80%的可能性在支线任务中高度参与,但在实验中被分配到高支线任务参与度组后,她改变为有 100%的可能性高度参与。
我们需要重写我们的支线任务参与度的条件概率分布,以反映这些新的概率,如表 7.9 所示。
表 7.9 重写支线任务参与度的条件概率表,以反映在实验中被分配到高参与度组后参与度的确定性
| 公会成员 | |
|---|---|
| 非成员 | 成员 |
| --- | --- |
| 支线任务参与度 | 低 |
| 高 | 1.0 |
现在我们看到,无论是否为公会成员,这种修改后的支线任务参与度分布都是相同的。这就是概率独立的定义,因此我们应该简化这个条件概率表以反映这一点;我们可以将表 7.9 简化为表 7.10。
表 7.10 重写支线任务参与度的条件概率表,以反映参与度不再依赖于公会成员
| 支线任务参与度 | 低 | 0.0 |
|---|---|---|
| 高 | 1.0 |
当我们以这种方式简化分布时,我们必须记住这是一个因果马尔可夫核的模型,它由图定义。我们的初始图表明支线任务参与度是由公会成员引起的。但似乎在实验随机将玩家分配到高参与度或低参与度组之后,这种因果依赖性被打破了;玩家的参与度完全由他们被分配到的组决定。
我们需要一个干预操作符,以改变我们的因果图来反映这种断裂的因果依赖性。这个干预操作符被称为图手术(也称为图突变),如图 7.6 所示。

图 7.6 图手术移除了干预目标支线任务参与度的入边。
虽然公会成员资格在正常设置中是支线任务参与的原因,但实验对支线任务参与的干预打破了该变量对公会成员资格的因果依赖。由于这种因果依赖已经消失,图手术改变了图,使得从公会成员资格到支线任务参与的边被剪断。
通常,假设你有一个带有节点X的因果图。你想应用一个干预 do(X=x)。然后你通过“手术”移除所有指向X的入边来在因果 DAG 上表示该干预。图手术在如 pgmpy 之类的库中可用。例如,以下是使用 pgmpy 对在线游戏 DAG 应用图手术的方法。
列表 7.11 pgmpy 中对 DAG 的图手术
from pgmpy.base import DAG
G = DAG([ #1
('Guild Membership', 'Side-quest Engagement'), #1
('Side-quest Engagement', 'In-game Purchases'), #1
('Guild Membership', 'In-game Purchases') #1
]) #1
G_int = G.do('Side-quest Engagement') #2
1 构建因果 DAG。
2 DAG 类中的 do 方法应用图手术。
我们现在可以绘制原始 DAG 和转换后的 DAG,并比较它们。
列表 7.12 绘制转换后的 DAG
import pylab as plt
import networkx as nx
pos = { #1
'Guild Membership': [0.0, 1.0], #1
'Side-quest Engagement': [-1.0, 0.0], #1
'In-game Purchases': [1.0, -0.5] #1
} #1
ax = plt.subplot() #2
ax.margins(0.3) #2
nx.draw(G, ax=ax, pos=pos, node_size=3000, #2
node_color='w', with_labels=True) #2
plt.show() #2
ax = plt.subplot() #3
ax.margins(0.3) #3
nx.draw(G_int, ax=ax, pos=pos, #3
node_size=3000, node_color='w', with_labels=True) #3
plt.show() #3
1 创建一个节点位置的字典,我们可以用它来可视化两个图。
2 可视化原始图。
3 可视化转换后的图。
这些可视化产生了与图 7.6 相同的 DAG。
接下来,我们将研究图手术对 d 分离及其对条件独立性的影响。
7.2.4 图手术和 d 分离
考虑图手术如何影响 d 分离推理,如图 7.7 所示。最初,我们有支线任务参与和游戏内购买之间的两个 d 连通路径:一条是直接因果路径,另一条是通过公会成员资格的共同原因。图手术之后,只剩下直接因果路径。

图 7.7 在原始 DAG 的左侧,支线任务参与和游戏内购买之间存在两个 d 连通路径。这些路径等于两个变量之间的统计依赖来源。图手术之后,只剩下因果路径,反映了因果依赖。
回想一下,两个变量之间的每个 d 连通路径都是这些变量之间统计依赖的来源。当我们用一个通过图手术移除干预目标(的)入边来表示干预时,我们移除了通过该变量原因的任何路径。只剩下到其他节点的出路径。因此,从该变量剩余的路径反映了该变量对其他变量的因果影响所导致的依赖。理想干预消除了目标变量从其直接父变量接收的因果影响。因此,它消除了通过这些父变量流动到其他变量的任何依赖。
7.2.5 理想干预和因果马尔可夫核
在因果 DAG 上进行的图手术移除了目标节点(的)入边。然而,对于因果图模型,我们需要一个干预算子,该算子不仅改变图,而且进一步重写干预目标的因果马尔可夫核,就像我们将表 7.9 折叠成表 7.10 时所做的那样。
最初,我们的在线游戏模型具有因果马尔可夫核{P(G), P(E|G), 和 P(I|E, G)}。在表 7.9 中,我们看到了P(E|G)的条件概率表表示以及如何通过干预将其减少到表 7.10,其中 100%的概率被放置在结果“高”上。
通常,因果图模型的干预算子用退化分布替换干预目标的因果马尔可夫核(们),这意味着一个将 100%的概率放在一个值上的分布,即干预值。
当我们将图手术与用退化分布替换目标节点因果马尔可夫核结合起来时,我们得到了一个因果图模型的干预算子,该算子符合理想干预定义的三个要素:
-
你只为干预目标节点移除入边。
-
将 100%的概率分配给一个固定值。
-
移除干预目标节点的入边意味着该变量不再因果依赖于其父节点。
在列表 7.11 中,图手术在DAG类的do方法中实现。我们的默认构建因果图模型的BayesianNetwork类也有一个do方法。与DAG方法类似,它接受一个干预目标。在撰写本文时,该方法不接受干预值,因此不满足理想干预定义的第二要素。
pgmpy 使用BaseFactor类(例如,TabularCPD类)的子类对象来表示因果马尔可夫核。BayesianNetwork类的do方法首先进行图手术,然后替换表示干预目标因果马尔可夫核的因子对象。然而,那个替换因子对象不是退化的;它不会将所有概率值分配给一个结果。相反,它返回一个对象,表示在原始未修改图中对其父节点进行边缘化后的目标变量的概率分布。技术上,这是一个随机干预的干预算子,我将在第 7.5 节中讨论。要构建理想干预的干预算子,你需要编写额外的代码来修改因子,使其将所有概率分配给干预值。
7.2.6 因果程序中的理想干预
回想一下,在列表 7.10 中,我们使用pyro.do运算符模拟了一个实验,其中玩家被分配到高参与度和低参与度组。具体来说,我们像以下列表那样调用pyro.do。
列表 7.13 回顾pyro.do
int_engaged_model = pyro.do( #1
model, #1
{"Side-quest Engagement": tensor(1.)} #1
) #1
int_unengaged_model = pyro.do( #2
model, #2
{"Side-quest Engagement": tensor(0.)} #2
) #2
1 一种将 Side-Quest Engagement 设置为 1.0(即,“高”)的干预。这将返回一个新的模型。
2 一种将 Side-Quest Engagement 设置为 0.0(即,“低”)的干预。这将返回一个新的模型。
pyro.do 究竟做了什么?pyro.do 是 Pyro 的干预操作符。我们通过使用 pyro.render_model 生成图 7.4,看到 Pyro 中的在线游戏模型有一个潜在的因果有向图(DAG),因此它是一个因果图模型。
但是,像 Pyro 这样的深度概率机器学习框架允许你做一些我们难以用因果有向图(DAG)表示的事情,例如递归、条件控制流,或者有随机数量的变量直到运行时才实现。作为一个干预操作符,pyro.do 也必须在这些情况下工作。
Pyro 中的干预操作符通过查找对 pyro.sample 的调用,并用对干预值的赋值来替换这些调用来实现。例如,在线游戏模型有以下一行:
is_highly_engaged = pyro.sample("Side-quest Engagement", Bernoulli(p_engaged))
pyro.sample 调用为 Side-Quest Engagement 生成一个值。pyro.do(model, {"Side-quest Engagement": tensor(1.)}) 在本质上用以下内容替换了那一行:
is_highly_engaged = tensor(1.)
(我说“本质上”是因为 pyro.do 还做了几件其他事情,我将在第十章中讨论)。
这种替换类似于在系统动力学(SCM)中替换赋值函数,或者在因果图模型中使用退化核。作为一个干预操作符,它满足了理想干预的标准。它针对一个特定的变量,并给它赋予一个特定的值。通过消除其依赖性(通过移除程序中先前语句执行的结果的依赖性),它消除了其因果关系。
使用像 Pyro 这样的灵活深度概率机器学习工具构建因果模型,允许你构建超越 DAG 和简单理想干预的因果表示。这样做将你置于理论基础尚不发达的领域,但这可能导致有趣的新应用。
在下一节中,我们将考虑干预如何影响概率分布。
7.3 干预变量和分布
理想干预固定了它所针对的随机变量,本质上将其转变为一个常数。但是,干预间接影响了目标变量下游的所有随机变量。因此,它们的概率分布(联合、条件或边缘)从它们原本的状态发生了变化。
7.3.1 “Do” 和反事实符号
因果建模使用特殊的符号来帮助推理干预如何影响随机变量及其分布。一种常见的方法是使用“do”-notation。以我们的在线游戏为例,P(I)是所有玩家中In-Game Purchases的概率分布,P(I|E=“high”)是在具有“高”参与度的玩家中In-Game Purchases的概率分布,而P(I|do(E= “high”))是在将玩家的参与度设置为“高”的干预下In-Game Purchases的概率分布。表 7.11 的第二列说明了这种符号扩展到联合分布、多个干预以及将干预与观察混合。
表 7.11 Do-notation 和 Counterfactual notation 的示例
| 文字描述 | Do-notation | Counterfactual notation |
|---|---|---|
| 所有玩家中In-Game Purchases的概率分布 | P(I) | P(I) |
| 具有高参与度的玩家的In-Game Purchases的概率分布 | P(I|E=“high”) | P(I|E=“high”) |
| 当玩家的参与度(通过干预)设置为“高”时,In-Game Purchases的概率分布 | P(I|do(E= “high”)) | P(I**[E][= “high”]) |
| 当参与度设置为“高”时,In-Game Purchases和Guild Membership的联合概率分布 | P(I, G|do(E=“high”)) | P(I[E=“high”], G[E=“high”]) |
| 当参与度设置为“高”且会员设置为“非会员”时,In-Game Purchases的概率分布 | P(I|do(E=“high”, G=“nonmember”)) | P(I**[E][= “high”,] [G][=“nonmember”]) |
| 当参与度设置为“高”时,公会成员的In-Game Purchases的概率分布 | P(I|do(E=“high”), G=“member”) | P(I**[E][=][“high”]|G=“member”) |
另一种方法是使用反事实符号,它使用下标来表示系统暴露于干预后变量的新版本。例如,如果I是一个表示In-Game Purchases的变量,I[E][=“high”]表示在将Side-Quest Engagement设置为“高”的干预下In-Game Purchases。如果P(I)是In-Game Purchases的概率分布,那么P(I[E][=“high”])也是概率分布。同样,表 7.11 在第三列中对比了 do-notation 与 counterfactual notation。接下来,我将主要使用 counterfactual notation。
从因果语言到符号
在统计学和机器学习的许多情况下,符号仅用于给可以用普通语言轻松解释的东西增加形式化和严谨性。然而,在因果关系中,符号很重要,因为它清楚地区分了我们是在谈论因果关系还是不是。这很重要,因为在普通英语中区分这一点更困难。例如,考虑以下两个问题:
“对于在支线任务中高度参与的玩家,游戏内购买会是什么样子?”
“如果玩家在支线任务中高度参与,游戏内购买会是什么样子?”
你是否明显地认为第一个问题对应于P(I|E=“high”),第二个对应于P(I[E][=“high”])?第一个问题对应于高度参与的玩家子集。当我们想聚焦于分布或人群的子集时,传统的条件概率符号是合适的。第二个问题询问“如果”有人高度参与。在下一章中,我们将看到“如果”假设性问题意味着干预。但是,由于语言的模糊性,有人可能问一个问题,而实际上意味着另一个。符号给我们提供了一个无歧义的方式来构建我们的因果查询。
再次,在第八章中,我们将探讨更多将语言映射到反事实符号的例子。
7.3.2 当因果符号简化为传统符号时
认识到变量及其干预下的同一变量是否相同至关重要。考虑图 7.8 中的干预参与度。

图 7.8 在原始 DAG(左)中,支线任务参与度和游戏内购买之间存在两条 d-连接路径。这些路径等同于两个变量之间的统计依赖来源。经过图手术,只有因果路径保留,反映了因果依赖。
P(G|E=“high”)(在高 支线任务参与度下公会成员资格的概率分布)是否与P(G)相同?不。在图形术语中,G和E是 d-连接的。在概率术语中,我们可以推理出,知道玩家的支线任务参与度水平可以预测他们是否在公会中。
但是,P(G[E][=“high”])是否与P(G)相同?是的。公会成员资格不受支线任务参与度干预的影响,因为它只能影响支线任务参与度下游的变量。因此,P(G[E][=“high”])等价于P(G).
在一般意义上,从经验上学习变量 Y[X][=][x] 的分布需要在实际生活中进行干预。然而,这种实际生活中的干预,至多是有成本的,在最坏的情况下是不可行的或不可能的。所以如果我们能将 Y[X][=][x] 等同于一些包含 Y 的分布,我们可以从观察数据中学习,那将是一个胜利。这是从相关性到因果性的转变。在简单的情况下,我们可以通过查看图表来完成这项工作,就像我们对 G 和 G[E][=“high”] 所做的那样。但通常,我们需要进行一些数学推导,无论是手动还是使用算法。
这个任务被称为 识别,它是因果推断理论的核心。我们将在第十章详细探讨识别问题。
7.3.3 因果模型表示所有干预分布
作为生成模型,我们处理过的因果模型编码了 DGP 组件的联合概率分布。推理算法使这些模型能够表示(例如,通过蒙特卡洛抽样)给定其他组件状态的某些组件子集的条件分布。
我们现在已经介绍了理想的干预以及它是如何改变数据生成过程(DGP)和变量的联合概率分布的。图 7.9 说明了生成因果模型如何捕捉原始 DGP(及其对应的概率分布)以及通过干预原始过程创建的任何新的 DGP(及其对应的概率分布)。

图 7.9 假设我们的数据生成过程(DGP)包含变量 X、Y 和 Z。一个传统的生成模型(左侧)使用 X、Y 和 Z 的观测值来统计学习 P(X, Y, Z) 的表示。一个生成因果模型(右侧)编码 P(X, Y, Z) 的表示以及通过对 X、Y 和 Z 进行干预得到的分布。通过这种方式,生成因果模型表示了一组广泛的分布。
考虑这个想法的统计含义。给定数据,一个普通生成模型学习联合概率分布的表示。但一个生成因果模型不仅学习那个分布,还学习通过应用一些理想干预所得到的新分布。这就是我们的在线游戏因果模型能够仅通过观察数据重现实验结果的原因。
7.4 干预和因果效应
我们干预的正式模型最常见的用途将是建模 因果效应。现在我们已经定义和形式化了干预,因果效应就很容易考虑了;它们仅仅是干预结果的比较。
7.4.1 二元原因的平均处理效应
最常见的因果效应查询是平均处理效应 (ATE)。在这里,我们将关注我们感兴趣于 X 对 Y 的因果效应的情况,且 X 是二元的,意味着它有两个结果:1 和 0。二元原因涉及实验,其中原因有一个“治疗”值和一个“控制”值,例如“A/B 测试”。使用 do-notation,ATE 是 E(Y|do(X=1)) – E(Y|do(X=0))(回忆 E(…) 表示“…的期望”)。使用反事实符号,ATE 是 E(Y[X][=][1]) – E(Y[X][=][0])。反事实符号的优点是我们可以将它简化为一个期望项,E(Y[X][=1] – Y[X][=0])。
7.4.2 有类别原因的平均处理效应
当原因是有类别的,ATE 需要选择你想要比较的原因的哪些水平。例如,如果 X 有可能的取值 {a, b, c},你可能会选择“a”作为基线,并使用两个 ATE,E(Y[X][=][b] – Y[X][=][a]) 和 E(Y[X][=][c] – Y[X][=][a])。或者,你也可以选择处理 X 的所有成对比较的水平,或者仅仅将 X 转换为一个二元变量,其结果为“a”和“非 a”。选择取决于哪个 ATE 对你最有意义。
7.4.3 连续原因的平均处理效应
如果我们要将 E(Y[X][=][1] – Y[X][=][0]) 推广到 X 连续的情况,我们就会进入微分学的领域。对于一些基线 do(X=x),想象一下通过改变干预值 x 的某个小量 Δ,即 do(X=x+Δ)。取两个结果之间的差值,我们得到 E(Y[X][=][x][+][Δ] – Y[X][=][x])。然后我们可以问,当我们使 Δ 无限小的时候,E(Y[X]) 的变化率是多少。这带我们来到了导数的定义:

注意,这是一个函数,而不是一个点值;当你插入一个 x 的值时,你得到的是 X 与 Y[x] 曲线的 Y[X][=][x] 的变化率。
作为实际例子,考虑药理学的情况,我们想要建立药物剂量对健康结果的平均处理效应。药物剂量是连续的,通常遵循非线性 S 形曲线;随着剂量的增加,我们得到更多的效果,但最终在较高剂量时效果会递减。导数给出了给定剂量在剂量-反应曲线上的平均响应变化率。
7.4.4 条件平均处理效应
条件平均处理效应 (CATE) 是基于其他协变量的 ATE。例如,在我们的在线游戏示例中,E(I[E][=“high”] – I[E][=“low”]) 是 Side-Quest Engagement 的 In-Game Purchases 上的 ATE。如果我们想了解公会成员的 ATE,我们想要 E(I[E][=“high”] – I[E][=“low”]| G=“member”)。
在实际设置中,通常重要的是使用 CATEs 而不是 ATEs,因为 CATEs 可能与 ATEs 和其他在不同条件下有不同的 CATEs 有很大的差异。换句话说,CATEs 更好地反映了人群中处理效应的异质性。例如,可能 ATE 在整体人群中对于一个健康结果的影响是正的,但基于特定亚群体(例如,有某种过敏的人)的条件 CATE 可能是负的。同样,在广告中,某些广告文案可能会使你的客户平均购买更多,但可能使某些客户群体购买更少。通过理解每个细分市场的 CATEs,你可以优化广告活动的投资回报率,或者使用基于 CATE 的推理来进行客户细分。
专家们经常强调使用 CATEs 来衡量异质处理效应的重要性,以免人们认为 ATE 的点值估计就能说明全部情况。但在我们的概率建模方法中,异质性是核心。如果我们有一个因果图模型和一个理想干预模型,那么我们可以建模P(Y[X][=][x])。如果我们能建模P(Y[X][=][x]),那么我们就可以建模P(Y[X][=][1] – Y[X][=][0])。然后我们可以使用这个模型来检查P(Y[X][=][1] – Y[X][=][0])中的所有变异,包括目标群体中谁高于或低于 0 或某个其他阈值。
7.4.5 关联性和因果性的统计度量
在统计学中,效应量是一个衡量两个变量或群体之间关系强度或强度的值。例如,在我们对在线游戏数据的观察性分析中,我们量化了Side-Quest Engagement E和In-Game Purchases I之间的关系,表示为E(I|E=“high”) – E(I|E=“low”)。我们的统计程序通过两组样本平均数之间的差异来估计这个真实效应量。然后我们进行了一个假设检验。我们指定了一个零假设E(I|E=“high”) – E(I|E=“low”) = 0,然后通过在某个零假设分布(通常是正态分布或 t 分布)下计算出的p-值来测试这个效应量估计是否与 0 有统计学上的差异。
因果效应只是一个干预性效应量;在我们的例子中,它是E(I|do(E=“high”)) – E(I|do(E=“low”)) = E(I[E][=][“high”] – I[E][=][“low”]),这就是 ATE。统计假设检验程序与之前相同。实际上,我们仍然需要测试基于样本的 ATEs 和 CATEs 的估计是否具有统计学上的显著性。当你对一个有处理组和对照组的实验数据进行统计显著性测试时,你按照定义是在测试 ATE 的估计。
7.4.6 因果性与回归模型
假设 X 是连续的,但与 Y[X] 的关系是线性的。那么平均处理效应(ATE)d E(Y[X][=][x])/dx 是一个点值,因为线性函数的导数是一个常数。因此,如果你使用 E(Y[X]) 的线性模型,那么该模型中 X 的系数对应于 X 的 ATE。

因此,线性回归建模是建模因果效应的一种流行方法(即使人们并不真正相信因果关系的线性)。
这种便利性也扩展到其他广义线性模型。假设泊松回归或逻辑回归比线性回归更好地模拟了 E(Y[X])。这些模型将两个变量之间的关联度量不是作为均值差异,而是作为比率。例如,我们可以直接从泊松回归模型中读取相对风险(RR),并直接从逻辑回归模型中读取优势比(OR)。一般来说,这些关联度量没有因果解释,但一旦我们使用它们与干预变量一起使用,我们就给予它们因果解释。例如,如果我们正在模拟 E(Y[X]),并且 Y[X] 是二元的,那么相对风险和优势比如下:

因此,一旦我们在干预背景下使用它们,传统的非因果统计关联量化方法就变成了因果关联的度量。并且当我们将这些回归模型拟合到数据中时,我们仍然可以使用所有传统的回归方法进行显著性检验(Wald 检验、F 检验、似然比检验等)。
7.5 随机干预
随机干预是理想干预的一种重要推广。理想干预的第二条规则是干预被设定为固定值。在随机干预中,这个值是随机过程的输出;即,它本身是一个随机变量。大多数文本将随机干预视为因果建模导论范围之外的先进主题,但我特别提到它们,因为它们在机器学习中非常重要,在那里我们经常寻求数据驱动的自动化。随机干预对于自动选择干预措施非常重要。
7.5.1 实验中的随机分配是一种随机干预
例如,我们在线游戏实验中的数字实验平台自动将玩家分配到高参与度和低参与度组。它是随机这样做的。随机分配是一种随机干预;它针对 Side-Quest Engagement 变量,并通过数字抛硬币来设定其值。
注意,随机化不仅仅是我们需要达到正确答案的方法。事实上,在我们的实验模拟中,没有随机化,只有理想化的干预。这些理想化的干预足以 d 分离路径支线任务参与 ← 公会会员 → 游戏内购买,消除了由此路径带来的统计依赖。如果随机化不是量化因果关系所必需的,为什么它被称为“因果推断的黄金标准?”答案在于,当你的因果 DAG 错误时,随机化是有效的。
例如,假设当我们进行实验时,并不是将玩家随机分配到高支线任务参与组或低支线任务参与组,而是数字实验平台自动将前 500 名登录的玩家分配到高支线任务参与组,接下来的 500 名玩家分配到低支线任务参与组。这种干预足以 d 分离路径支线任务参与 ← 公会会员 → 游戏内购买。但如果我们 DAG 错误,并且存在其他路径通过未知的共同原因连接支线任务参与和游戏内购买,会怎样呢?
图 7.10 考虑了当我们的 DAG(有向无环图)错误时会发生什么——我们的模型是右侧的 DAG。考虑如果真正的 DAG 是左侧的 DAG,会发生什么。对于左侧的 DAG,玩家登录的时间驱动了支线任务参与和游戏内购买。

图 7.10 左:真实的因果关系。右:你(错误)的因果 DAG。
例如,假设登录较早的人往往不太可能与朋友一起登录。他们更倾向于参与支线任务,因为支线任务适合单人游戏。那些与朋友计划任务的人往往登录较晚,因为一些朋友在白天有现实世界的约会。一起玩的朋友更专注于游戏的主要叙事,并避免支线任务。此外,玩家往往在一天晚些时候在游戏内购买上花费更多,这与电子商务中晚些时候的广泛消费趋势相呼应。
当我们根据玩家的登录信息将他们分配到某一组或另一组时,这种干预的价值现在取决于一天中的时间,如图 7.11 所示。
图 7.11 的左侧展示了基于一天中某个时间点的 Side-Quest Engagement 介入的结果。正如我们所预期的那样,介入执行了图手术,移除了指向 Side-Quest Engagement E 的入边:T→E 和 G→E。然而,现在介入设置的价值由时间 T 决定,通过一个 time_select 函数。time_select 函数将“高”参与度分配给那些登录时间早于第 501 位登录玩家的玩家,而对于那些登录时间晚于第 501 位玩家的玩家则分配“低”参与度。在图手术之后,我们添加了一个新的因果边 T→E,其机制是 time_select。因此,仍然存在一个非因果的统计关联,通过 d 连通路径 I←T→E 偏斜了实验。

图 7.11 一个基于登录时间变化设置 Side-Quest Engagement 水平的介入,但没有消除 T→E 的因果关系。相比之下,随机化消除了指向 E 的入边。
与之相反,图 7.11 右侧的随机化实现了我们希望的效果,移除了所有指向 E 的入边。它甚至移除了来自 T→E 的边,尽管我们假设的 DAG 并不知道 T→E 存在。实际上,如果 E 和 I 之间存在其他未知的共同原因,随机化将移除指向 E 的这些入边,如图 7.12 所示。

图 7.12 随机化消除了来自未知共同原因的入边。
随机化能够消除我们假设中未能考虑到的共同原因的统计偏差,这就是为什么它被认为是“因果推断的黄金标准”。但为了理解随机介入,请注意,两种分配机制:一种基于登录时间,另一种使用随机化,都是随机介入。两者都使用随机过程设置玩家的 Side-Quest Engagement 水平;一个取决于登录时间,另一个取决于抛硬币的结果。
7.5.2 介入策略
随机介入与自动化决策领域的策略密切相关,例如赌博算法和强化学习。在这些领域,一个代理(例如,机器人、推荐算法)在某个环境中操作。一个 策略 是一个算法,它接受环境中的某些变量的状态作为输入,并为代理返回一个执行的动作。如果该动作的选择中存在随机元素,则它是一个随机介入。
在我们之前的例子中,随机化是一种选择干预措施的政策。但在自动化决策中,大多数政策都是基于系统中其他变量的状态来选择干预措施,就像基于一天中的时间变量进行干预的有偏实验一样。当然,自动化决策中的政策通常试图优化某个效用函数,而不是使实验有偏见。我们将在第十二章中关注自动化决策中的因果关系。
7.6 建模干预措施的实际考虑
我将以一些关于建模干预措施的实际考虑来结束这一章。我们将考虑理想(和随机的)干预措施如何使我们能够模拟不可能发生的事情。然后,我们将确保将这种建模建立在实用主义的基础上。
7.6.1 关于现实中无法实施的干预措施的推理
在我们的在线游戏示例中,我们使用因果模型上的干预操作来复制实验结果。我提出了实际运行实验和模拟实验之间的选择。模拟避免了运行实验的成本,但运行实验对因果假设中的错误更稳健,特别是使用随机化等工具时。
然而,有许多时候我们无法进行实验,因为这样做要么不可行,要么不道德,或者不可能。
-
不可行实验的例子——一项随机实验,测试利率对代际财富的影响。
-
不道德实验的例子——一项随机实验,测试咖啡因对流产的影响。
-
不可能实验的例子——一项随机实验,测试黑洞大小对光谱红移的影响。
在这些情况下,使用因果模型进行模拟是我们的唯一选择。
7.6.2 反驳和现实世界干预
假设你的因果模型预测了干预措施的结果。然后你在现实世界中实施该干预措施,例如通过一个受控实验。如果你的预测干预结果与实际干预结果相冲突,那么你的因果模型就是错误的。
在第四章中,我们讨论了通过检查数据以寻找违反模型 DAG 条件独立性含义的依赖性证据来验证(或更确切地说,反驳)因果模型的概念。在第十一章中,我们将反驳从因果 DAG 扩展到感兴趣的因果推断(例如,估计因果效应)。然而,比较预测和实际干预结果给我们提供了一个比第四章和第十一章中的方法更强的反驳标准。当然,问题是进行这些现实世界干预必须是可行的。
假设它们是,比较预测和现实世界干预的结果提供了一个很好的迭代框架来构建因果模型。首先,列出你可以在现实世界中应用的干预措施。选择其中之一,使用你的模型来预测其结果,然后在现实世界中实施干预。如果结果不匹配,更新你的模型,使其匹配。重复此过程,直到你耗尽进行现实世界干预的能力。
在现实世界中进行干预通常需要资源和时间。为了节省成本,使用你的因果模型来预测你可以进行的所有干预,根据哪个更有趣或更令人惊讶来排序预测的结果,然后根据这个排序优先进行现实世界的干预。有趣或令人惊讶的干预预测可能是你的模型错误的迹象,所以优先考虑它们意味着你将更快、成本更低地对你的模型进行重大更新。如果你的模型最终证明是正确的,你将花费更少的代价来获得对 DGP 的一些重要见解。
7.6.3 “没有操纵就没有因果关系”
“没有操纵就没有因果关系”背后的想法是,应该在因果模型中定义变量,使得如何干预的机制是清晰的。这里的清晰意味着你可以运行一个实施干预的现实世界实验,或者如果实验不可行、不道德或不可能,你至少可以清楚地阐述假设实验将如何进行。“没有操纵就没有因果关系”本质上是在尝试将因果模型的抽象与实验语义联系起来。
例如,这个想法的支持者可能会反对在因果模型中将“种族”作为一个原因,因为从实验中应用干预的角度来看,种族的概念是模糊的——你如何在保持一个人除种族之外的所有其他因素不变的情况下改变他们的种族呢?他们更愿意用足够精确的术语来定义变量,以便在理论上可以进行干预,例如“贷款官员的种族偏见”或“申请表上的种族指标”。当然,我们对像“种族”这样的模糊抽象有很多重要问题要问,所以我们不希望增加太多的精确度,以至于我们无法以有助于回答这些问题的方式推广我们分析的结果。
建立这种与实验的联系的一种策略是在我们的模型中包含我们可以在假设实验中操纵的变量。例如,如果我们对财富→焦虑的因果关系感兴趣,我们可以添加一个“现金补贴”变量和现金补贴→财富边。现金补贴代表对个人的直接支付,这在实验中比直接操纵个人的财富更容易实现。
7.6.4 模拟“非理想”干预
通常,我们在实际设置中使用的干预类型可能难以映射到理想干预。例如,一位生物学家可能正在研究细胞中不同基因表达的因果关系,如基因 A → 基因 B → 基因 C。生物学家可能想知道细胞环境中的应激源(例如,毒素或缺氧)如何影响基因表达。应激源是一个干预;它改变了数据生成过程(DGP)。然而,将其建模为理想干预是具有挑战性的,因为这可能不清楚哪些基因会受到应激源的直接影响的基因,或者应激源设置的特定基因表达量是多少。对这些干预的实际解决方案是将它们明确地建模为因果 DAG 中的根节点,如图 7.13 中的缺氧节点。

图 7.13“缺氧”是一个没有特定目标的干预。将其作为根节点包含,并连接到所有可能受影响的变量。
将干预明确表示为 DGP 的一部分不如理想(或随机)干预表达性强,后者捕捉了任意干预如何改变DGP。
摘要
-
干预是改变数据生成过程(DGP)的行为。干预对于定义因果关系和因果模型是基本的。
-
许多,如果不是大多数,由机器学习驱动的决策都是可以改变模型部署环境与训练环境不同的干预。
-
能够建模干预使得人们可以模拟实验的结果。
-
使用干预模型模拟实验可以节省成本或在实际实验不可行、不道德或不可能时进行模拟实验。
-
理想干预针对特定变量,将其固定到特定值,并使目标变量独立于其因果父变量。
-
因果效应是干预分布的简单扩展。例如,X 对 Y 的平均处理效应(ATE)是 E(Y[X=1]) – E(Y[X=0]),即两个干预分布对 Y 的均值差异。条件平均处理效应(CATEs)是干预分布对 Y 的条件期望的差异。
-
随机干预类似于理想干预,但它们将干预目标固定在由某个随机过程确定的值。这个值可能取决于系统中其他变量的状态。因此,它们与自动决策领域的策略相关,如多臂老丨虎丨机算法和强化学习。
-
干预算子描述了如何改变因果模型以反映理想(或随机)干预。
-
结构因果模型的干预算子用干预值替换目标变量赋值函数。
-
图手术是因果有向无环图(DAG)的干预算子。
-
因果图模型的干预算子应用图手术,并用一个将所有概率都放在干预值上的退化分布替换目标上的因果马尔可夫核。
-
因果模型可以使用观察数据来统计地学习观察分布以及可以通过干预算子推导出的任何干预分布。
-
随机化是一种随机干预,它消除了未知原因对干预目标的因果影响。
-
“无干预则无因果”建议定义你的因果模型,使得干预与假设实验相关联。
-
你可以将不符合理想干预标准的干预建模为具有指向它们可能影响的变量的出度根节点。
[1] D.T. Campbell 和 T.D. Cook,准实验:实地设置的设计与分析问题(Rand McNally,1979),第 36 页。
第八章:反事实与平行世界
本章涵盖了
-
反事实推理的激励示例
-
将反事实问题转化为符号形式
-
为反事实推理构建平行世界图
-
实现反事实推理算法
-
构建图像的反事实深度生成模型
Marjani,我的一个好朋友,曾经不得不在两个约会对象之间做出选择。她有一个理想的长期伴侣的心理成绩单。她和一个男人有很好的化学反应,但在成绩单上排名不高。相比之下,第二个男人满足了所有的条件,所以她选择了第二个。但过了一段时间,尽管他满足了她的所有标准,她对他没有任何感觉。这就像一个失败的召唤仪式;星星完美对齐,但被召唤的灵魂从未出现。因此,就像我们中的任何人在那种情况下会做的那样,她提出了反事实问题:
我根据自己的标准选择伴侣,但结果并不好。如果我是根据化学反应来选择,结果会怎样?
类似这样的反事实查询描述了没有发生但可能发生的事件。反事实是我们定义因果关系的基础;如果 Marjani 的问题答案是肯定的,这意味着根据她的成绩单选择导致了她的爱情生活不成功。
反事实是因果效应推断的基本问题,其中我们比较观察到的结果与“潜在结果”,例如 Marjani 的爱情生活如果她根据化学反应选择伴侣的结果。更广泛地说,回答反事实问题对于学习更好的决策策略是有用的。当某个行动导致某个结果时,如果你问不同的行动可能会导致不同的结果,一个好的答案可以帮助你在未来选择更好的行动。例如,在这段经历之后,Marjani 修改了她的成绩单,在考虑后来的浪漫前景时考虑了化学反应。
本章我们将探讨实际示例,但我首先以爱情和浪漫的例子开始,因为它具有普遍的关联性。它说明了反事实推理对人类认知的基本性——我们对世界的判断是由我们对可能发生的事情的想象所驱动的。
注意,Marjani 的反事实推理涉及一种预测。就像漫威超级英雄电影一样,她想象了一个平行世界,在这个世界里她根据化学反应做出选择,并且她预测在这个世界里她爱情生活的结果。但统计机器学习算法在预测方面比人类更擅长。这个洞察力引导我们展望构建使用统计机器学习工具自动进行类似人类反事实推理的 AI 的前景。
在本章中,我们将通过学习使用概率形式化反事实问题来追求这一目标。在下一章中,我们将实现一个概率反事实推理算法,可以回答这些问题。让我们先探索一些激发算法反事实推理动机的实际案例研究。
8.1 激发反事实推理的动机
在这里,我将介绍一些案例研究,展示回答反事实问题在商业上的价值。然后,我将论证它们如何有助于提高决策能力。
8.1.1 在线游戏
回想第七章中的在线游戏例子,其中玩家的游戏内购买量是由他们在支线任务中的参与程度以及他们是否在公会中驱动的。假设我们观察到一个高度参与支线任务并且有很多游戏内购买的玩家。一个有趣的反事实问题可能是,“如果他们的参与度低,他们的游戏内购买量会是多少?”
8.1.2 流媒体战争
订阅流媒体公司之间在有限的市场中争夺订阅者,这种激烈的竞争被称为“流媒体战争”。Netflix 是这个领域的领头羊,拥有丰富的经验。它学会了通过从头开始建立大片系列来吸引新订阅者,例如 纸牌屋、怪奇物语 和 鱿鱼游戏。
然而,Netflix 与拥有极其雄厚资金实力的亚马逊、苹果和迪士尼竞争。他们可以通过简单地购买现有的成功系列(例如,星球大战)并在该系列内制作新颖的内容(例如,曼达洛人)来与 Netflix 从零开始建立系列的能力竞争。
假设迪士尼正在与购买有史以来最有价值的间谍惊悚片系列 詹姆斯·邦德 进行谈判,而 Netflix 认为一个成功的邦德交易可能会导致它失去迪士尼的订阅者。Netflix 希望通过与一位著名剧集制作人达成协议,创建一个名为 Dead Drop 的新间谍惊悚片系列来防止这种情况。这个新系列将结合经过考验的间谍惊悚片元素(例如, gadgets、异国情调的背景、汽车追逐、夸张的动作场面)以及 Netflix 制作的剧集所特有的复杂角色、多样化的表现和情感上引人入胜的故事情节。Netflix 高管能否与候选剧集制作人达成协议存在不确定性,因为双方必须就创意控制、预算、版税等达成一致。
假设邦德交易成功,迪士尼+ 现在运行了以“邦德宇宙”为背景的新系列和电影。然而,Dead Drop 交易失败了。Netflix 随后获得了数据,这些数据确定了某些随后离开 Netflix、订阅了迪士尼+ 并观看新邦德内容的订阅者。
一个 Netflix 高管可能会提出以下反事实问题:“如果“Dead Drop”交易成功,那些流失的订户会留下吗?” 假设答案是“不会”,因为邦德内容吸引力如此之强,以至于“Dead Drop”交易的结果并不重要。在这种情况下,未能关闭交易的工作人员不应该因为失去订户而受到责备。
或者,假设“Dead Drop”交易成功,Netflix 订户现在可以观看新的“Dead Drop”系列。 “如果交易失败,那些观看“Dead Drop”的订户会转向迪士尼的新邦德系列吗?” 再次,如果答案是“不会”,那么成功关闭交易的工作人员不应该因为保留所有这些订户而获得赞誉。
在这两种情况下,回答这些问题将有助于指导未来的交易决策。
8.1.3 机器学习模型的反事实分析
在本章中,我们专注于使用因果模型来对某些数据生成过程进行反事实推理。在机器学习中,通常的目标是对机器学习模型本身进行反事实分析;即,给定一些输入特征和一些输出预测,如果输入不同,预测会有何不同?这种反事实分析支持可解释人工智能(XAI)、人工智能公平性以及其他任务。
分类中的反事实分析
考虑分类任务——一个训练好的算法接受一些给定示例的特征集合,并为该示例生成一个预测类别。例如,给定一个贷款申请的详细信息,一个算法将申请分类为“拒绝”或“批准”。
给定一个被拒绝的申请,一个自然出现的反事实问题是:“如果申请中的某些元素不同,申请会被批准吗?” 通常,反事实分析的目标是找到对应于分类变化的最小特征向量变化。图 8.1 说明了这个想法。

图 8.1 什么是最小的输入特征变化,会导致批准?在这种情况下,如果收入高出 20,000 美元,贷款就会被批准。
找到导致批准的最小变化需要定义特征空间中的距离度量,然后找到类别边界另一侧的特征值。在这个例子中,假设条件“如果申请人有每年 20,000 美元更多的收入……”对应于特征向量(在决策表面的距离方面)的最小变化,这将导致批准。这种分析对 XAI 很有用;即,理解特征如何逐个案例驱动分类。
反事实算法回溯
对于大多数贷款申请人来说,增加$20,000 的薪水是不现实的。这就是基于反事实的算法回溯可以发挥作用的地方。算法回溯寻找最近的假设条件,该条件可能导致不同的分类。它是在假设条件对申请人来说是可实现的或可操作的的约束下运行的。图 8.2 展示了这是如何工作的。
在这个例子中,假设是根据某些标准(而增加$20,000 的收入则不是),增加$5,000 的收入并提高信用评分是可实现的。

图 8.2 在算法回溯中,我们通常对决策边界另一侧最近的可操作特征向量感兴趣。
算法回溯旨在为受到基于机器学习决策的个人提供他们可以操作的信息。如果一个人考试不及格并询问原因,解释“因为你不是天才”不如“因为你没有复习练习考试”有用,尽管两者可能都是真的。
反事实公平性
反事实公平性分析是一种类似的分析,适用于某些输入特征对应于个人属性的情况。其理念是,个人的某些属性在道德上不应影响分类。例如,使用一个人的种族或性别来决定是否提供贷款是不道德的。即使这样的“受保护属性”没有明确编码到输入特征中,分类算法也可能已经学会了受保护属性的代理,例如一个人居住的社区、社交网络、购物习惯等。在模型中包含这样的特征是有意义的,而且当这些特征作为受保护属性的代理时,可能并不明显。
图 8.3 使用贷款算法示例来说明反事实公平性分析将如何提出反事实问题。在这种情况下,反事实问题是“如果这个人属于不同的种族,他会被批准吗?”分析师会找到代表种族的特征,然后看看这些代理特征的变化是否会导致“批准”的分类。一些技术试图在训练期间使用这种分析来产生更公平的算法。

图 8.3 对于反事实公平性,假设我们想测试算法是否对某些种族存在偏见。对于给定的特征向量、种族元素和相应的“拒绝”结果,我们测试如果种族元素改变,结果是否会改变。
虽然反事实公平性分析不足以解决人工智能公平性的广泛问题,但它是在人工智能公平性工具包中的一个基本要素。
8.1.4 悔恨与为什么我们关心“本会发生什么?”
传统机器学习通常是面向未来的。给定数据,你做出一个预测,这个预测将驱动现在做出一些决策,而这个决策将带来一些未来的成本或收益。我们希望得到好的预测,以便获得更多的未来收益。想象一下,例如,一个能够准确预测股票投资组合表现的机器学习算法——这显然是非常有价值的。
现在,想象一个不同的算法,它能够准确地告诉你,如果你买了不同的股票,你的投资组合今天会表现如何;这肯定比预测未来要少有价值。这种对比突出了对建模反事实的常见批评,即它是面向过去的。例如,我们动机案例研究中的反事实问题集中在过去发生的决策和结果上。事情已经发生;得到这些问题的答案不会改变过去。
但是,首先,并非所有反事实都是回顾性的。在第 8.3 节中,我们将建模类似“如果你不发促销信息,订阅者会有多大几率流失?如果你发了促销信息,他们就不会流失?”的问题(“流失”意味着在特定时间段内停止使用产品或服务。)这个问题没有过去时态,具有商业价值,并且是我们能够建模的。
其次,回顾性反事实有助于你了解如何在未来做出更好的决策。确实,分析在给定不同分配的情况下你的投资组合会如何表现——投资者称之为“回测”——是对比各种投资策略的理想方式。同样,从失败的Dead Drop交易中得到的反事实洞察可能有助于 Netflix 高管与另一位著名剧集制作人达成交易。
当我们考虑关于“本会”或“本可能”发生的事情的回顾性推理时,我们就会得到遗憾的概念。遗憾是关于回顾性反事实对比的;在给定选择的情况下,遗憾是所选择选项的结果与所拒绝选项的想象中的反事实结果之间的比较。用通俗的话说,当你拒绝的选项的反事实结果比你所选择的选项更好时,你会感到一种不良的感觉。但认知科学称之为负面遗憾;也存在正面遗憾,这是当你与想象中的反事实结果比较时,意识到你选择了更好的选项时所获得的良好感觉(例如,“哎呀,我真是躲过了一劫”)。
遗憾对于学习做出更好的决策是有用的。假设你做出一个选择,你付出了一定的代价(时间、精力、资源等),并导致了一个结果。这为你提供了一个基线单点数据用于学习。现在,假设你从后见之明中可以想象,如果你做出了不同的选择,会发生什么样的结果,并且你可以以 100%的准确性想象出来。现在你有了两个可比较的学习数据点,而你只需为其中一个付出代价。
通常,你想象被拒绝的选项的反事实结果的能力并不是 100%准确的。即使有后见之明,对反事实结果仍然存在一些不确定性。但这不是问题——我们可以用概率来模拟这种不确定性。只要后见之明知识为你提供了有关反事实结果的一些信息,你就可以比从单一数据点学习做得更好。
在强化学习和其他自动化决策中,我们通常将我们的决策标准称为“策略”。我们可以在评估和更新策略时纳入反事实分析和遗憾。
8.1.5 强化学习和自动化决策
在自动化决策中,“策略”是一个函数,它接受有关决策问题的信息,并自动选择某些行动方案。强化学习算法旨在找到随着时间的推移优化良好结果的策略。
自动反事实推理可以将好的结果归因于适当的行为。在投资示例中,我们可以想象一个算法,随着更近期的价格进入数据,定期回测不同的投资组合分配策略。同样,想象我们正在编写一个强化学习(RL)算法来学习玩游戏。我们可以让算法使用保存的游戏实例来模拟如果它使用了不同的策略,那个游戏实例会有怎样的不同结果。算法可以通过比较这些模拟结果和实际结果来量化遗憾的概念,并使用这些结果来学习更好的策略。这将减少 AI 学习良好策略所需的游戏数量,并使其能够从游戏中通常不会出现的情况下学习。在第十二章中,我们将更多地关注自动化决策、投币机和强化学习。
8.1.6 回答反事实查询的步骤
在这些应用的每个方面,我们可以使用以下工作流程来回答这些反事实推理问题:
-
提出反事实问题—用最简单的术语清晰地阐述我们想要提出的反事实问题(们)。
-
转换为数学查询—将查询转换为数学符号,使其形式足够正式,可以应用数学或算法分析。
-
进行推理—运行一个推理算法,生成问题的答案。
在接下来的章节中,我们将专注于步骤 1 和 2。在第九章,我们将使用基于 SCM 的算法来处理步骤 3,该算法用于推断我们在步骤 2 中创建的查询。在第十章,我们将看到在没有 SCM 但只有数据和 DAG 的情况下如何进行步骤 3。
8.2 反事实的符号表示
在第七章,我们看到了“反事实符号”,它使用下标来表示干预。现在我们将使用这个符号来表示反事实表达式。关键是记住,正如我们将看到的,反事实查询只是干预性查询的一种特殊类型。我们将通过回顾我们的在线游戏案例来了解干预性查询如何流入反事实查询。
8.2.1 假设性陈述和问题
考虑我们的在线游戏案例研究。在考虑玩家赚取多少时,我们可能会这样说:
对于一个典型玩家,游戏内购买的金额将会超过 50 美元。
我们将这称为假设性陈述。在语法上,我使用情态动词(例如,“would”,“could”,“should”在“would be more”中)来有意标记假设性语言,而不是使用陈述性语言(例如,“is more”或“will be more”),后者我们用来陈述客观事实。
我们想用概率符号形式化这个陈述。对于这个陈述,我们将写P(I>50)—回想一下,我们使用了随机变量I来表示游戏内购买,E表示支线任务参与度,G表示公会会员资格。
我们在我们的开放问题中也会使用假设性语言,如下所示:
对于一个典型玩家,游戏内购买的金额会是多少?
我在询问变量I可能取的值的范围,我用P(I)来表示。
陈述性语言与假设性语言及概率
陈述性语言表达确定性,例如“游戏内购买金额超过 50 美元。”相比之下,假设性语言用于传达推测、想象和假设的陈述,例如“游戏内购买金额将会是超过 50 美元。”
我们中的许多人将概率符号与陈述性语言联系起来,因为概率理论与命题逻辑的联系:P(I>50)量化了陈述性陈述“游戏内购买金额超过 50 美元”为真的概率。但我们将深入使用假设性语言。
假设性语言隐含着不确定性的缺乏——我们谈论的是可能发生的事情,而不是已经发生的事情。不确定性与不确定性相当,而本书中我们采用的贝叶斯哲学引导我们使用概率来模拟不确定性,因此使用假设性语言将使我们更容易用概率符号形式化问题。我们会发现这有助于我们形式化因果陈述和问题。
注意,当我们将其映射到概率符号时,问题或陈述的时态并不重要。例如,我们可以使用这种表述:
典型玩家的游戏内购买金额会是多少?
无论时态如何,我们使用符号 P(I) 来表示我们对问题中感兴趣变量的不确定性。
8.2.2 事实过滤假设到子群体
假设我的陈述如下:
具有高支线任务参与度的玩家的游戏内购买水平将超过 50 美元。P( I>50| E=“high”)
在这里,我是在对玩家子集(那些具有高支线任务参与度的玩家)做出陈述,而不是对所有玩家。当我提出这个问题时,我在做同样的事情:
具有高支线任务参与度的玩家的游戏内购买水平会是多少?P( I| E=“high”)
事实“支线任务参与度”很高有助于将玩家群体过滤到那些事实为真的玩家。如第二章所述,我们使用条件概率来聚焦于一个子群体。在这个例子中,我们使用 P(I>50|E=“high”)作为陈述,P(I|E=“high”)作为问题。
我将使用“事实条件”来指代事实、事件和证据,如 E=“high”,这些条件可以缩小目标人群。这些事实条件出现在条件概率符号 P(.|.) 中“|”的右侧。我们通常可能会称它们为“条件”,但我想避免与“条件假设”混淆,我将在下文中介绍。
8.2.3 条件假设、干预和模拟
现在,假设我做出了以下陈述:
如果玩家的支线任务参与度很高,他们会在游戏内购买上花费超过 50 美元。P(I [E] [=“high”] > 50)
我们将称这为条件假设陈述。我们将称“如果支线任务参与度很高”的部分为假设条件,而“他们会在游戏内购买上花费超过 50 美元”是假设结果。
条件假设问题中的假设条件通常遵循类似的“如果……会怎样”的表述风格:
如果玩家的支线任务参与度很高,他们的游戏内购买金额会是多少?P(I [E] [=“high”])
我们将使用干预符号(即反事实符号中的下标)来表示这些条件。对于陈述,我们将使用 P(I[E][=“high”]>50),而对于问题,P(I[E][=“high”])。
想象力、条件和干预
使用理想干预来模拟条件假设陈述中的假设条件是我们因果建模方法的一个哲学基石。这个想法是,当我们提出假设条件时,我们只关注假设条件的因果后果。
复习:理想干预
理想干预是对数据生成过程的改变,它做以下事情:
-
针对固定变量(例如,X)
-
将该变量设置为特定的值(例如,x)
-
这样做就切断了该变量父代变量的因果影响
这个定义可以推广到一组变量。
我们有时用 do-notation 来表示干预,例如 do(X=x)。在反事实符号中,对于一个变量 Y,我们写 Y[X][=][x] 来表示变量 Y 受到对 X 的干预的影响。在 DAG 中,我们通过图手术来表示理想的干预,这意味着我们切断指向目标变量的入边。在 SCM 中,我们通过用干预值替换目标变量的赋值函数来表示理想的干预。因果库通常为我们实现这些操作,通常是通过一个名为“do”的函数或方法。
让我通过反例来说明。假设我们提出这样的问题:
如果玩家的 side-quest engagement 水平很高,他们进行游戏内购买的金额会是多少?
假设我们用 P(I|E=“high”) 来建模这个情况。那么对这个查询的推理将不仅使用高参与度对 In-Game Purchases 的影响,还会通过路径 E ← G → I 的非因果关联;你可以从玩家的 Side-Quest Engagement 水平推断出玩家是否在公会中,而 Guild Membership 也会推动 In-Game Purchases。但这个问题不是关于 Guild Membership 的;我们只是对 Side-Quest Engagement 如何推动 In-Game Purchases 感兴趣。
“如果...会怎样”的假设性问题使用理想的干预,因为它们只关注条件的因果后果。为了说明这一点,让我们重新表述前面的问题,使隐含的理想干预变得明确:
如果玩家的 side-quest engagement 被设置为高,他们进行 In-Game Purchases 的数量会是多少? P ( I [E] [=“high”])
动词“set”暗示了干预的行为。用理想的干预建模假设条件论证说,原始表述和这种表述意味着相同的事情(从现在开始,我将使用原始表述)。
作为人类,我们通过想象一个假设条件为真的世界,然后想象假设情景如何作为后果展开来回答“如果...会怎样”的问题,就像前面 P(I[E][=“high”]) 问题(无论是原始版本还是重新表述的版本)那样。我们的假设条件中的变量可能在数据生成过程中有自己的因果驱动因素(例如,Guild Membership 是 Side-Quest Engagement 的原因),但我们忽略这些驱动因素,因为我们只对假设条件的后果感兴趣。我们在想象中隔离假设条件中的变量,就像我们在实验中做的那样。理想的干预是设置变量独立于其因果的正确工具。
避免混淆事实条件和假设条件
很容易将“事实条件”与“假设条件”混淆。为了重申,在问题“如果 X 是 x,Y 会是什么?”中,X = x 是一个假设条件,我们使用符号 P(Y[X][=][x])。相比之下,事实条件用于缩小我们询问的群体。例如,在问题“对于 X 是 x 的情况,Y 会是什么?”中,X = x 是一个实际条件,用于筛选出 X = x 的情况。在这里,我们使用符号 P(Y|X=x)。
请记住,我们可以结合事实条件和假设条件,就像以下问题中那样:
如果一个玩家的支线任务参与度很高,那么在公会中的玩家在游戏中的购买量会是多少?P ( I [E] [=“high”]| G= g)
在这里,我们正在询问一个子集玩家的条件假设,这些玩家是公会成员。这个查询与以下不同:
如果一个玩家的支线任务参与度很高,并且他们是公会成员,那么这个玩家在游戏中的购买量会是多少?P ( I [E] [=“high”], [G] [=“member”])
话虽如此,由于自然语言的歧义性,有人可能会问第二个问题,而他们真正想要的是第一个问题的答案。澄清混淆、明确意义并写下正确的符号是模型构建者的责任。
8.2.4 反事实陈述
在自然语言中,反事实陈述是一种条件假设性陈述,其中事实条件与假设条件或结果之间存在某种冲突。换句话说,它是一种“与事实相反”的条件假设性陈述。
在日常语言中,那些冲突的事实条件可以在陈述之前陈述或由上下文暗示。为了我们的目的,我们将要求反事实陈述明确陈述冲突的事实条件:
对于参与度低且游戏内购买金额少于 50 美元的玩家,如果他们的支线任务参与度很高,他们会在游戏内购买超过 50 美元。P* ( I [E] [=“high”]>50| E=“low”, I £50)
作为问题,我们可能会问:
如果一个玩家的支线任务参与度很高,并且游戏内购买金额少于 50 美元,那么这个玩家的游戏内购买量会是多少?P ( I [E] [=“high”] | E=“low”, I £50)
在陈述和问题中,低参与度的事实条件与高参与的假设条件相冲突。在陈述中,假设结果是在游戏中购买超过 50 与实际条件(少于或等于 50)相冲突。同样,问题考虑了所有可能的假设结果,其中大多数与实际条件(少于或等于 50)相冲突。我们使用反事实符号来写这些查询,就像我们写其他条件假设一样。
形式化反事实术语概述
假设性语言—用于表达假设、推测、设想和想象的可能性。在英语中,它通常涉及“would”或“could”,与陈述性语言形成对比。可以说,用假设性语言来形式化因果陈述和问题更为容易。
假设性陈述—关于世界的假设性语言陈述,例如“Y将是y,”,我们将其写成数学形式为 P(Y=y)。
事实条件—指代事实、事件和证据,这些证据缩小了所讨论内容的范围(目标群体)。在条件概率中用作条件。例如,我们会写成“Z是z,Y将是y”为 P(Y=y|Z=z)。
假设性条件—构成假设情景的条件,例如“如果x是x?”或“如果x是x…”。我们用反事实符号中的理想干预和下标[X][=][x]来表示假设条件。
条件假设陈述—具有假设条件的假设陈述,例如“如果x是x,Y将是y,”这成为 P(Y[X][=][x]=y)。我们可以添加事实条件,如“Z是z,如果x是x,Y将是y”变为 P(Y[X][=][x]=y|Z=z)。
反事实陈述—反事实陈述是一种条件假设陈述,其中事实条件中的变量与假设条件或假设结果中的变量重叠。例如,在“X是x,如果X是x',Y将是y” (P(Y[X][=][x][']=y|X=x)) 中,事实条件“X是x”与假设条件“如果X是x'”重叠。在“Y是y,如果X是x',Y将是y'” (P(Y[X][=][x][']=y'|Y=y)) 中,事实条件“Y是y”与假设结果“Y将是y.”重叠。
一致性规则—如果一个事实条件和假设条件重叠但不冲突,你可以省略假设条件。例如,P(Y[X][=][x]|X=x) = P(Y|X=x)。
注意,许多文本将使用“反事实”一词来描述不必然基于事实条件的正式因果查询,例如 Y[X][=][x] 或 P(Y[X][=][x]=y) 或 P(Y[X][=][x]=1, Y[X][=][x][']=0)。我在上面使用“反事实陈述”和其他短语来描述常见的假设和反事实自然语言,并有助于将它们转换为形式化的反事实符号。
注意,我们可以将冲突的事实条件与其他非冲突的事实条件结合起来,例如在这个例子中成为行会成员:
如果一个玩家在支线任务参与度低且游戏内购买少于 50 美元的情况下,他们的支线任务参与度很高,那么他们游戏中的购买量会是多少?P( I [E] [=“high”] | E=“low”, I £50, G=“member”)
图 8.4 展示了形式化反事实查询的元素。

图 8.4 以反事实符号形式形式化的条件反事实假设元素
使用大型语言模型形式化反事实
将反事实问题形式化为反事实符号是一个大型语言模型(LLM)的绝佳任务。最先进的 LLM 在将自然语言查询转换为符号查询(如 SQL 语句)的基准测试中表现相当出色,形式化反事实问题就是这类任务的例子。我们将在第十三章中更详细地探讨 LLM 和因果关系,但现在你可以尝试提示你最喜欢的 LLM 将问题转换为反事实符号。
8.2.5 一致性规则
考虑分布 P(I[E][=“high”]|E=“low”)。假设我们不是用下标 [E][=“high”],而是用 [E][=“low”],那么分布就是 P(I[E][=“low”] |E=“low”)。一致性规则指出,这个分布等同于更简单的 P(I|E=“low”)。更一般地,P(Y[X][=][x]|X=x, Z=z) = P(Y |X=x, Z=z) 对于任何 z。
直观上,P(I[E][=“low”]|E=“low”) 对应的是一个相当奇怪的问题,“如果一个玩家的支线任务参与度低,那么他们的支线任务参与度也会低,那么这个玩家在游戏中的购买量会是多少?”在这个问题中,事实条件和假设条件重叠但不冲突。一致性规则指出,在这种情况下,我们放弃假设条件,认为这相当于询问“如果一个玩家的支线任务参与度低,那么他们在游戏中的购买量会是多少?”
现在考虑一个反事实的版本,其中我们观察到游戏内购买的实际结果。具体来说,考虑 P(I[E][=“high”]|E=“low”, I=75)。这是相应的反事实问题:
如果一个玩家的支线任务参与度低且游戏内购买等于 75 美元,那么他们的支线任务参与度很高,他们游戏中的购买量会是多少?
现在,相反,假设我们将其改为 P(I[E][=“low”]|E=“low”, I=75)。根据一致性规则,这会缩减为 P(I|E=“low”, I=75):
如果一个玩家的支线任务参与度低且游戏内购买等于 75 美元,那么他们游戏中的购买量会是多少?
当然,答案是 75 美元。如果我们询问在 I=75 条件下的 I 的分布,那么我们得到一个所有概率值都集中在 75 的分布。
在反事实推理中,我们经常想知道关于与事实条件中观察到的相同变量的假设结果。一致性规则指出,如果假设条件与实际发生的情况相同,那么假设结果必须与实际发生的情况相同。
回想一下,我们使用干预来模拟假设条件。该规则确保我们,如果一个玩家有低支线任务参与度和一定数量的游戏内购买,如果他们被选入一个随机选择他们进入低支线任务参与度组的实验,他们将会有相同数量的游戏内购买。如果我们期望我们的因果推断能够预测实验的结果,这一点很重要。
8.2.6 更多示例
表 8.1 展示了将反事实问题映射到反事实符号的几个附加示例。
表 8.1 反事实符号示例
| 问题 | 类型 | 反事实符号中的分布 |
|---|---|---|
| 对于一个典型的玩家,他们的游戏内购买金额会是多少? | 假设 | P( I) |
| 对于一个对支线任务有高度参与度的玩家,他们的游戏内购买金额会是多少? | 假设,专注于高度参与的玩家 | P( I| E=“high”) |
| 如果玩家对支线任务有高度参与,他们进行游戏内购买的金额会是多少? | 条件假设 | P( I [E] [=“high”]) |
| 如果玩家是公会成员,他们的参与度和游戏内购买金额会是多少? | 对两个感兴趣的结果的条件假设 | P( E [G] [=“member”], I [G] [=“member”]) |
| 如果玩家有高度支线任务参与度,并且他们不是公会成员,他们的游戏内购买水平会是多少? | 具有两个假设条件的条件假设 | P( I [E] [=“high”,] [G] [=“nonmember”]) |
| 如果玩家在公会中有高度支线任务参与度,他们的游戏内购买水平会是多少? | 专注于公会成员的条件假设 | P( I [E] [=“high”]| G=“member”) |
| 对于参与度低的玩家,如果他们的参与度很高,他们的游戏内购买水平会怎样? | 反事实。事实条件与假设条件冲突。 | P( I [E] [=“high”]| E=“low”) |
| 对于最多花费 50 英镑进行游戏内购买的玩家,如果他们的参与度很高,他们的游戏内购买水平会怎样? | 反事实。事实条件(游戏内购买 50 英镑)与可能的假设结果(游戏内购买可能超过 50 英镑)冲突。 | P( I [E] [=“high”]| I £50) |
| 对于一个参与度低且游戏内消费不超过 50 美元的玩家,如果他们的参与度很高,他们的游戏内消费水平会是多少? | 反事实。事实条件与假设条件以及可能的假设结果相冲突。 | P( I [E] [=“high”]| E=“low”, I £50) |
| 对于一个参与度低的公会玩家,如果他们的参与度很高且不是公会成员,他们的游戏内消费水平会是多少? | 反事实。事实条件与假设条件相冲突。 | P( I [E] [=“high”,] [G] [=“nonmember”]| E=“low”, G=“member”) |
| 如果玩家是公会成员,他们的参与度会是多少?此外,如果他们不是公会成员,他们的游戏内消费水平会是多少? | 反事实。涉及两个不同结果上的两个相互冲突的假设条件。 | P( E [G] [=“member”], I [G] [=“nonmember”]) |
表 8.1 中的最后一个案例是一个特殊情况,在理论中比实践中更常见,它不涉及事实条件,而是具有相互冲突的假设条件。
接下来,我们将探讨一类特定的反事实,这些反事实涉及二元原因和结果。
8.3 二元反事实
反事实查询的一个重要子类是我们称之为二元反事实的。这些反事实涉及二元假设条件和结果变量。当我们从观察性和实验性研究的角度思考时,二元变量,尤其是二元原因,就会出现,例如我们有“暴露”组和“未暴露”组,或者“治疗”组和“控制”组。但二元变量在推理事件发生时也非常有用;一个事件要么发生,要么不发生。
二元反事实查询值得特别提及,因为它们通常更容易思考,具有简化数学属性,这是非二元变量查询所缺乏的,并且在本节中我们将介绍它们的一些实际应用。此外,你通常可以用二元术语表述你想要回答的问题,这样你就可以在正式化查询时将非二元变量转换为二元变量。为了说明,在我们的在线游戏示例中,假设一个玩家在在线购物中花费了 152.34 美元,我们问“为什么这个玩家支付了这么多?”我们并不关心他们为什么支付了确切的那笔金额,而是为什么支付了如此高的金额,其中“如此高的金额”定义为,例如,超过 120 美元。因此,我们的二元指标变量是 X = {1 if I ≥ 120 else 0}。
8.3.1 因果关系的概率
因果关系的概率是二元反事实的一个特别有用的类别。它们的效用在于帮助我们回答“为什么”的问题。它们是实际应用中的基础概念,包括市场营销中的归因、强化学习中的信用分配、工程中的根本原因分析以及个性化医疗。
让我们在流失归因问题的背景下展示因果概率的有用性。在订阅业务模式中,流失是服务失去订阅者的速度,这对企业或业务单元的价值有重大影响。通常,公司部署一个预测算法,将订阅者评估为具有某种程度的流失风险。公司希望阻止那些有高流失风险的订阅者真正流失。在我们的例子中,公司会发送一个促销活动,以吸引订阅者留下(不流失)。因果概率可以帮助我们了解用户为什么会流失或留下。
给定二元(真/假)原因 X 和结果 Y,我们将定义以下因果概率:必要性概率、充分性概率、必要性和充分性概率、启用概率和禁用概率。
必要性概率
对于二元原因 X 和二元结果 Y,必要性概率(PN)是查询 P(Y[X][=][0]=0|X=1, Y=1)。用简单的话来说,PN 背后的问题是“对于 X 和 Y 都发生的情况,如果 X 没有发生,Y 会不会也没有发生?”换句话说,X 是否必须发生才能导致 Y 发生?
让我们考虑我们的流失问题。让 X 代表我们是否发送了促销,让 Y 代表用户是否留下(没有流失)。在这个例子中,P(Y[X][=][0]=0|X=1, Y=1) 代表查询“对于一个收到促销并留下的订阅者,如果没有收到促销,他们流失的可能性有多大?”换句话说,促销优惠是否是维持订阅者的必要条件?
充分性概率
充分性概率(PS)是 P(Y[X][=][1]=1|X=0, Y=0)。PS 的常见简单表述是“对于既没有 X 也没有 Y 发生的情况,如果 X 发生了,Y 会发生吗?”换句话说,X 的发生是否足以导致 Y 发生?例如,“对于没有获得晋升且没有留下(流失)的用户,如果他们获得了晋升,他们会留下吗?”换句话说,晋升是否足够(充分)让他们留下?
充分性的通俗解释可能会令人困惑。反事实查询的事实条件聚焦于 X=0 和 Y=0 的情况(即 X 和 Y 都没有发生的情况)。然而,我们通常对查看 X=1 和 Y=1 的情况感兴趣,并询问 X 是否足以单独导致 Y=1。换句话说,鉴于 X 和 Y 都发生了,即使影响 Y 的各种其他事件的结果不同,Y 仍然会发生吗?但 P(Y[X][=1]=1|X=0, Y=0) 包含这种解释,而无需我们列举查询中所有“影响 Y 的各种其他事件”。有关对充分性进行更深入研究的讨论,请参阅章节注释www.altdeep.ai/p/causalaibook。
因果概率和法律
因果概率与法律概念密切相关。了解这种关系是有帮助的,因为实际应用往往与法律交叉,我们在实际环境中与之合作的许多利益相关者都接受过法律培训。
-
但非因果关系和必要性概率——但非测试是确定侵权法和刑法中因果关系的测试之一。我们表述必要性概率的方式是但非测试的概率等价物,将“如果 X 没有发生,Y 会不会没有发生?”重新表述为“如果没有 X 的发生,Y 会发生吗?”
-
近因因果关系和充分性概率——在法律中,近因因果关系指的是原因 X 在导致结果的直接事件链中的首要地位(例如,伤害或损害)。确实存在与充分性的联系,尽管不是等价关系。近因因果关系确实考虑了因果事件是否足以导致结果,但近因因果关系的法律理论通常超越了充分性,还涉及道德判断。
必要性和充分性概率
必要性和充分性概率(PNS)是 P(Y[X][=1]=1, Y[X][=0]=0)。用通俗的话说,P(Y[X][=1]=1, Y[X][=0]=0) 可以理解为,“如果 X 为 0,则 Y 为 0;如果 X 为 1,则 Y 为 1。”例如,“如果一个用户没有收到晋升,他们有多大可能流失,而如果他们收到了晋升,他们有多大可能保留?”PNS 可以分解为以下几部分:

禁用和启用概率
禁用概率(PD)和启用概率(PE)与 PN 和 PS 类似,但它们不基于原因 X 进行条件化。
PD 是查询 P(Y[X][=][0]=0|Y=1),意味着“对于 Y 发生的案例,如果 X 没有发生,Y 会不会没有发生?”对于流失问题,PD 提出的问题是“如果我们不发送晋升,整体流失概率是多少?仅针对没有流失的用户子群体(无论他们是否收到晋升)。
PE 是查询 P(Y[X][=1]=1|Y=0),或“对于没有发生 Y 的情况,如果 X 发生了,Y 会发生吗?”在我们的流失问题中,PE 询问,“如果我们发送促销,保留的整体概率是多少?”仅针对流失的用户子群体(无论他们是否收到了促销)。
因果概率可以作为反事实分析高级应用中的基本反事实原语。接下来,我将在归因的背景下给出一个例子。
8.3.2 因果概率和归因
因果概率是量化给定结果发生原因的方法的核心成分。例如,假设一家公司的网络有一个有故障的服务器,访问该服务器可能导致网络崩溃。假设网络崩溃了,你被要求分析日志以找到根本原因。你发现你的同事 Lazlo 访问了那个有故障的服务器。Lazlo 应该承担责任吗?
为了回答这个问题,你可能需要量化 Lazlo 成为崩溃的充分原因的可能性;即,Lazlo 访问服务器的可能性足以触发最终导致网络崩溃的多米诺效应。其次,Lazlo 成为必要原因的可能性有多大?例如,也许 Lazlo 不是必要原因,因为即使他没有访问服务器,其他人最终也会这么做。
因果概率需要与其他元素结合,以提供一个完整的归因视图。一个例子是异常的概念。因果事件的异常性描述了该事件在某种意义上是否违反了预期。例如,如果访问那个服务器对员工来说非常不寻常,Lazlo 可能会因为网络崩溃而受到更多的责备。我们可以用概率来量化因果事件的异常性;如果事件 X=1 是异常的,那么它不太可能发生,因此我们给 P(X=1) 分配一个低值。一个称为实际因果强度(ACS)的归因度量,如下结合了异常性和因果概率:

换句话说,这种方法将归因视为在成为一个异常的必要原因和一个正常的充分原因之间的权衡。
此外,还有越来越多的方法将可解释人工智能领域的归因方法(例如,Shapley 和 SHAP 值)与异常和因果关系概念相结合,如因果概率。有关参考文献列表,包括实际的因果强度和可解释人工智能方法,请参阅www.altdeep.ai/p/causalaibook。
8.3.3 二元反事实和提升建模
在商业、政治和研究中,对影响人类行为的活动的统计分析很常见。例如,在我们的流失率示例中,提供促销的目标是说服人们不要流失。同样,企业做广告是为了说服人们购买他们的产品,政治家们接触选民是为了让他们投票或为竞选活动捐款。
影响行为活动的挑战之一是确定谁可能对你的影响尝试做出积极反应,这样你就可以只将有限的资源用于影响这些人。营销领域的先驱约翰·华纳马克(John Wanamaker)说得最好:
我花在广告上的钱有一半是浪费的;麻烦的是我不知道哪一半。
提升建模指的是一类统计技术,它试图通过数据来回答这个问题。然而,第一次接触这个领域的数据科学家会发现各种统计方法,这些方法在术语、假设数据类型、建模假设和建模方法上有所不同,导致混淆。二元反事实在理解问题的高层次以及各种解决方案如何成功或失败地解决它非常有用。
将用户分为可说服者、确定无疑的事情、无望的人和睡狗
在我们的流失率示例中,我们可以假设有两种类型的订阅者。对于一些订阅者,促销会影响他们决定流失。其他人是不响应者,意味着促销对他们没有任何影响。我们可以将后者的不响应者分为两组:
-
无望的人——无论他们是否收到促销,都会流失的人
-
确定无疑的事情——无论他们是否收到促销,都会留下来的人
对于对促销做出反应的人,我们有两种群体:
-
可说服者——可以通过促销说服他们不离开服务的人
-
睡狗——如果你不发促销,就不会流失的订阅者,以及如果你发了促销就会流失的人
睡狗的名字来源于“让睡狗躺着”(最后它们醒来咬你)这个表达。如果你让他们独处,这些人会做你想做的事,但如果你不让他们独处,他们就会违背你的意愿。你有没有收到过订阅服务的营销邮件,心想,“这些人给我发太多垃圾邮件了!我要取消订阅。”你就是一个“睡狗”——公司的邮件就是唤醒你的那一脚,你因此咬了它们。图 8.5 展示了我们的订阅者如何分为这四个部分。

图 8.5 在试图影响行为的过程中,我们将目标人群分为这四个部分。鉴于资源有限,我们希望将我们的影响努力集中在可说服的人群上,避免其他人,尤其是睡狗。
促销在给订阅者提供额外价值的同时也带来成本。你希望避免将这笔成本花在那些无论如何都会流失(确定无疑)和那些无论如何都会流失(无望的事业)的订阅者身上。而且你肯定不希望只为了导致某人流失而花费这笔成本(沉睡的狗)。因此,在这四组人中,你只想向那些可能被说服的人发送促销。统计分析的任务是将我们的用户分成这四个组。
这就是反事实可以帮助我们的地方;我们可以用概率反事实术语来定义每个组:
-
无望的事业—这类人无论我们是否发送促销,都可能流失;即,P(Y[X][=1]=0, Y[X][=0]=0) 的概率很高。
-
确定无疑—如果我们发送促销,他们可能会留下,如果我们不发送促销,他们也会留下;即,P(Y[X][=1]=1, Y[X][=0]=1) 的概率很高。
-
可能被说服的人—如果我们发送促销,他们可能会留下,如果我们不发送促销,他们可能会流失;即,P(Y[X][=1]=1, Y[X][=0]=0) 的概率很高。换句话说,PNS(流失可能性)很高。
-
沉睡的狗—如果我们发送促销,他们可能会流失,如果我们不发送促销,他们可能会留下;即,P(Y[X][=1]=0, Y[X][=0]=1) 的概率很高。
你可以在图 8.6 中看到人口是如何被细分的。

图 8.6 我们可以用反事实术语对人口进行细分。
使用反事实进行细分
每个订阅者都有一些属性集合(人口统计信息、使用习惯、内容偏好等)。我们的目标是把这些属性转换成预测订阅者是否可能被说服、沉睡的狗、无望的事业或确定无疑。
用 C 表示一组订阅者属性。给定一个具有属性 C=c 的订阅者,我们感兴趣的因果查询是 P(Y[X][=1], Y[X][=0]|C=c)。各种统计细分方法试图定义 C,使得用户落入具有高概率的 P(Y[X][=1], Y[X][=0]|C=c) 四种结果之一的组。但在我们应用统计方法之前,我们的首要任务是确保我们能够使用足够的假设和数据来估计这个查询。我们将在第九章中介绍如何使用 SCM 估计反事实,在第十章中介绍如何使用更广泛的估计技术进行识别。
现在我们已经学会了如何提出因果查询并将其形式化为数学,让我们回顾一下图 8.7 中反事实推理的步骤。

图 8.7 反事实推理工作流程
在下一节中,我们将研究可能世界和并行世界图的概念。这些概念对于识别(确定我们是否能够回答问题)和推理算法都至关重要。
8.4 可能世界和并行世界图
在本节中,我将介绍可能世界和并行世界图的概念,这是对支持跨可能世界进行反事实推理的 SCM 中因果 DAG 的扩展。
8.4.1 可能世界中的潜在结果
反事实推理涉及对可能世界的推理。可能世界是世界的一种存在或可能存在的方式。实际世界是我们观察到的具有事件结果的那个可能世界。所有其他可能世界都是假设世界。
在数据生成过程(DGP)方面,实际世界是 DGP 展开产生我们的数据的方式。其他可能世界是由 DGP 可能产生不同数据的所有方式定义的。
潜在结果是因果效应推断中的基本概念。“潜在结果”指的是在不同可能世界中同一变量的结果。如果你头疼并服用阿司匹林,你可能会说在两个可能世界中存在两种潜在结果:一个是你头疼好转,另一个是你头疼没有好转。
可能世界术语的回顾
可能世界—世界的一种存在或可能存在的方式
实际世界—具有观察到的事件结果的可能世界
假设世界—没有观察到的事件结果的可能世界
潜在结果—在不同可能世界中同一变量的结果
并行世界—正在推理的一组可能世界,共享共同和不同的属性
并行世界图—用于识别反事实查询和在反事实推断算法中使用的并行世界的图形表示
8.4.2 并行世界图
并行世界图是因果 DAG 的一个简单扩展,它捕捉了可能世界之间的因果关系。继续使用在线游戏示例,假设我们感兴趣的问题是:“对于一个参与度低且游戏内消费少于 50 美元的玩家,如果他们的参与度高,他们的游戏内消费水平会是什么?”即P(I[E][=“high”]|E=“low”,I<50)。对于这个反事实查询,我们可以在图 8.8 中可视化实际和假设世界。

图 8.8 为了回答在线游戏示例的反事实查询,我们首先在可能世界中复制因果 DAG。
我们将在线游戏示例的因果 DAG 在两个可能世界中复制。每个世界都有一个 DAG 反映了 DGP 在每个世界中的因果结构相同。但我们需要以某种方式连接这些 DAG 来跨世界进行推理。
我们将使用因果 DAG 上定义的 SCM 连接两个世界。我们假设 DAG 的原始节点是 SCM 的内生变量,并通过添加外生变量来扩展 DAG 的可视化。此外,两个因果 DAG 将使用相同的外生节点。我们将结果图称为并行世界图(或,对于这种有两个可能世界的典型情况,称为“双世界图”)。图 8.9 可视化了并行世界图。

图 8.9 在并行世界图中,我们使用 SCM 中的外生变量来统一不同世界中的重复因果 DAG。结果是具有重复内生变量的单个 SCM。
复习:结构因果模型(SCM)
SCM 是一个具有以下组件的因果模型:
-
内生变量—内生变量是我们特别想要建模的变量。
-
外生变量—一组外生变量。外生变量是我们不希望显式建模的、我们内生变量的所有原因的代理。在我们的公式中,我们将每个内生变量 X 与单个外生变量 N**[X] 配对(存在更通用的公式)。
-
外生分布—为了将 SCM 用作生成模型,我们需要每个外生变量的边缘概率分布集,例如 P(N**[X]),它代表模型器对 N**[X] 值的不确定性。
-
功能分配—每个内生变量都有一个功能分配,它根据其父节点确定性地设置其值。
例如,以生成模型的形式,我们在线游戏模型的 SCM 可能如下所示。

分配函数诱导因果 DAG;每个变量是一个节点,外生变量是根节点,一个变量的分配函数的输入对应于 DAG 中的父节点。SCM 是因果图形模型的一个特例,其中内生变量由确定性函数设置,而不是从因果马尔可夫核中采样。
结果是一个具有一组共享的外生变量和重复的内生变量集的单一 SCM——每个可能世界一组。请注意,在 SCM 中,内生变量在外生变量的给定下是确定性地设置的。因此,当我们观察到实际世界中的 E=“低”和 I<50 时,我们知道 E 和 I 的假设结果必须相同。事实上,尽管 Guild Membership (G) 是实际世界中的潜在变量,但我们知道 G 在实际世界中的任何值都必须与假设世界中的相同。换句话说,我们的 SCM 维护了一致性规则,如图 8.10 所示。在图 8.10 中,实际世界中的 E 和 I 是观察变量,因为我们根据它们在查询 P(I[E][=“high”]|E=“low”, I < 50) 中进行了条件化。

图 8.10 在供应链管理(SCM)中,内生变量在给定外生变量的情况下被确定性地设置。在这个模型中,内生变量在各个世界中是复制的。因此,在观察实际世界中低参与度和低于 50 美元的游戏内购买时,我们知道这些值在假设世界中必须相同,除非我们在假设世界中改变某些东西。
8.4.3 通过图手术应用假设条件
假设世界通常与实际世界不同,因为假设条件不同。例如,在 P(I[E][=“高”]|E=“低”, I < 50)中,“如果参与度是高的”([E][=“高”])与实际条件“参与度是低的”(E=“低”)不同。正如我们讨论的那样,我们使用理想的干预措施来模拟假设条件——我们在假设世界中干预 E,将其设置为“高”。我们通过图手术来模拟理想的干预措施——我们将在假设世界中移除指向 E 变量的入边,如图 8.11 所示。

图 8.11 理想干预和图手术表示假设世界中的假设条件。在这种情况下,假设世界中 I 的结果现在可以采取与实际世界中不同的结果,因为其父节点 E 的结果与实际世界中不同。
现在,假设世界中“游戏内购买”(I)的结果可以不同于实际世界中 I=50 的结果,因为其因果父节点 E 在每个世界中都有不同的结果。
8.4.4 跨越两个以上可能世界的推理
反事实符号和并行世界图形式支持跨越两个以上可能世界的反事实推理。为了说明,让我们回顾本章开头提到的 Netflix 示例。总结这个故事,该叙述中的关键变量如下:
-
迪士尼正在尝试达成一项购买邦德系列的交易。如果交易达成,则 B = “成功”,否则 B = “失败”。
-
Netflix 正在尝试达成一项新间谍系列电影“Dead Drop”的交易。如果“Dead Drop”交易达成,则 D = “成功”,否则为“失败”。如果邦德交易达成,它将影响这项交易的条款。因此,B 导致 D。
-
如果“Dead Drop”交易达成,它将影响 Netflix 上与间谍惊悚片相关内容的参与度。如果订阅者在 Netflix 的间谍惊悚片内容中的参与度是“高”,则令 E = “高”,否则为“低”。
-
邦德交易和“Dead Drop”交易的结果都将影响间谍惊悚片粉丝向迪士尼的流失。令 A 为向迪士尼的流失率。
通过这个案例研究,以下的多世界反事实是合理的。假设邦德交易成功(B = “success”),但 Netflix 的Dead Drop 交易失败,结果参与度低(E = “low”),Netflix 向迪士尼的流失率为 10%。图 8.12 展示了这个实际世界的成果。
作为 Netflix 的执行高管,你开始思考归因问题。你假设如果 Dead Drop 交易成功,参与度将会很高。你提出了以下反事实问题:
迪士尼的邦德交易成功,Dead Drop 交易失败,因此,Netflix 的间谍惊悚片参与度低,向迪士尼的流失率为 10%。我假设如果 Dead Drop 交易成功,参与度将会很高。在这种情况下,会有多少归因?
我们可以在图 8.13 的并行世界图中使用世界 2 来实现这个假设。

图 8.12 展示了 Netflix 案例研究的因果 DAG 图。浅灰色节点是实际世界中的观测结果。深色节点是潜在变量。

图 8.13 第二个可能的世界代表了这样的假设:如果Dead Drop 交易成功(通过干预 D=“success”),参与度将会很高(E**[D][=“success”]=“high”)。
最后,你可能会想知道,如果邦德交易失败,Netflix 的流失率会是多少。但你基于第二个世界的假设,即如果Dead Drop 交易成功,参与度将会很高。由于邦德交易失败是一个与第二个世界中的邦德交易成功条件相冲突的假设条件,你需要一个第三世界,如图 8.14 所示。

图 8.14 在世界 1 的实际结果基础上,你提出了世界 2 的假设条件和结果,并在世界 3 中设定条件,并对世界 3 的流失率进行推理。
总结来说,这是一个反事实问题:
迪士尼的邦德交易成功,Dead Drop 交易失败,因此,Netflix 的间谍惊悚片参与度低,向迪士尼的流失率为 10%。我假设如果Dead Drop 交易成功,参与度将会很高。在这种情况下,如果邦德交易失败,会有多少归因?
注意,前面的推理与以下不同:
迪士尼的邦德交易成功,Dead Drop 交易失败,因此,Netflix 的间谍惊悚片参与度低,向迪士尼的流失率为 10%。我假设如果Dead Drop 交易成功而邦德交易失败,参与度将会很高。在这种情况下,会有多少归因?
图 8.15 说明了后一个问题。

在假设 E**[B][=“fail”,][D][=“success”] 的情况下,只需要两个世界。
后一个问题假设如果邦德交易失败并且“死信”交易成功(E[B][=“fail”,] [D][=“success”][=][“][high”]),参与度会很高。相比之下,前一个“三个世界”问题假设如果两个交易都成功,参与度会很高。然后,在第三个世界中,它允许在邦德交易失败的情况下假设情景中有不同的可能参与度水平。例如,可能由于 Netflix 拥有其间谍惊悚片系列而迪士尼没有,因此参与度会很高。或者,如果没有邦德重启,对间谍惊悚片的整体兴趣可能会减少,导致“死信”的参与度低。
8.4.5 经验法则:假设世界应该更简单
再次考虑图 8.16 中的在线游戏示例中的内生节点。注意,在这个例子中,两个世界有相同的一组内生节点,假设世界中的边是实际世界中边的子集。换句话说,我们干预的可能世界比基于证据的条件可能世界简单。

图 8.16 表示具有假设条件的可能世界的图比表示实际世界的图简单。
类似地,在 Netflix 案例研究的三个世界图中,世界 3 是世界 2 的子图,而世界 2 又是世界 1 的子图。作为一个算法规则,对可能世界有这种递减顺序是有用的。这个经验法则可以降低算法不稳定的可能性。
话虽如此,存在更复杂假设世界的用例。例如,模型师可以在假设世界中引入新的节点作为条件。或者他们可以使用随机干预,在假设世界中随机引入新的边。确实,人类的反事实推理可以非常富有想象力。探索这些方法可能导致因果 AI 的新颖算法。
在下一章中,我们将深入探讨在通用反事实推理算法中使用并行世界图。
摘要
-
反事实陈述描述了可能与实际事件冲突的假设事件。它们是定义因果关系的根本。
-
反事实推理支持学习策略以做出更好的决策。
-
反事实推理涉及对可能世界的推理。一个可能世界是世界存在或可能存在的方式。实际世界是我们观察到的结果事件的可能世界。其他可能世界是假设世界。
-
在机器学习中,通常的目标是对机器学习模型本身进行反事实分析。在这里,我们推理如果输入特征向量的元素不同,预测会有何不同。
-
在分类中的反事实分析可以帮助找到导致不同分类的最小特征变化。
-
反事实分析通过帮助识别会改变每个案例预测结果的特征变化,从而支持可解释人工智能。
-
反事实分析通过识别会改变预测结果的特征的可操作变化,从而支持算法回溯。
-
反事实分析通过识别与受保护属性相对应的特征,这些特征的变化会改变预测结果,从而支持人工智能的公平性。
-
“潜在结果”是一个常用术语,指的是从可能世界中给定变量的结果。
-
我们可以使用理想干预和并行世界图来模拟自然语言反事实陈述和问题中的假设条件。
-
反事实符号有助于用概率语言表示假设性陈述和问题。概率可以用来量化关于假设性陈述和问题(包括反事实)的真实性的不确定性。
-
使用假设性语言而不是陈述性语言有助于将反事实陈述或问题形式化为反事实符号。使用假设性语言意味着想象的可能性,因此存在不确定性,这促使我们思考假设性陈述为真的概率。
-
二进制反事实查询是指对二元变量(假设条件和结果)的查询。
-
因果概率,例如必要性概率(PN)、充分性概率(PS)和必要性和充分性概率(PNS),是作为因果归因方法和其他类型高级因果查询原语的有用二进制反事实查询。
-
二进制反事实查询在提升建模问题中区分“可说服者”、“确定无疑”、“无望的案例”和“沉睡的狗”也是很有用的。
-
并行世界图是因果 DAG 的一个简单扩展,它捕捉了可能世界中因果关系的跨世界。它表示一个在共享一组外生变量和重复的内生变量集合的可能世界上的 SCM。
第九章:一般反事实推理算法
本章涵盖
-
实现一般反事实推理算法
-
直接将并行世界 DAG 作为因果图模型实现
-
使用变分推理实现算法
-
构建图像的反事实深度生成模型
上一章教了你如何形式化反事实并使用并行世界图在可能的世界中进行推理。在本章中,我将介绍一个用于推断反事实查询的算法。然后,我将展示三个案例研究,展示使用不同的概率机器学习方法实现该算法。
我将本章中我们将讨论的算法称为“一般”概率反事实推理算法,因为你可以使用这个算法推断任何反事实查询。问题是你需要一个 SCM。此外,你的 SCM 与真实 SCM 之间的差异可能导致你的反事实推理不准确。我们将在第十章讨论识别问题时更仔细地研究这个问题,你也将学习到在不了解真实 SCM 的情况下推断反事实的方法。在本章中,你将看到基于 SCM 的方法的强大之处,尤其是在机器学习中。
9.1 算法概述
在本节中,我们将对概率反事实推理的一般算法进行高级概述。该算法有三个步骤,通常被称为归因、行动和预测:
-
归因—在给定事实条件的情况下推断外生变量的分布。
-
行动—在假设世界中将假设条件作为理想干预(图手术)实现。
-
预测—使用步骤 1 中的外生变量的条件分布来推导假设结果的分布。
我将使用我们的在线游戏示例的并行世界图来说明如何执行这些步骤,如图 9.1 所示。

图 9.1 在线游戏示例的并行世界图
回想一下,在这个例子中,公会成员G是侧任务参与度E和游戏内购买I的原因。侧任务参与度也是游戏内购买的原因。
备注:这个例子将第八章中使用的条件I < $50 更改为I = $50,以便使解释更加简洁。这两种条件都可以与我们所讨论的算法一起使用。
假设我们的反事实问题是“对于一个低侧任务参与度和 50 美元游戏内购买的玩家,如果他们的侧任务参与度很高,他们的游戏内购买水平会是什么?”相应的查询是P(IE =“high”|E=“low”,I=50)。让我们看看如何将算法应用于这个查询。
9.1.1 归因:在给定的观察到的内生变量下推断外生变量
“推理”这个术语指的是进行推理推断,意味着我们从观察到的结果中推断原因。在我们的在线游戏 SCM 中,我们想要从事实条件(E=“低”和I=50)中推断潜在的变量(N**[G], N**[E], 和N**[I])。
在我们的概率建模方法中,我们将外生变量视为潜在变量,并使用概率推理针对它们进行目标定位。在我们的例子中,我们从观察E=“低”推断N**[E]。图 9.2 和图 9.3 分别说明了到N**[G]和N**[I]的 d 连通路径。
正如你在图 9.2 中可以看到的,我们有一条从E到N**[G]的路径,通过路径E←G←N**[G]。此外,观察E和I通过路径E→I←G←N**[G]开启了到N**[G]的碰撞路径。同样,在图 9.3 中,观察E和I也通过路径E→I←N**[I]开启了到N**[I]的碰撞路径。

图 9.2 为了推断反事实结果,我们在实际世界中观察到的结果条件下推断外生变量。从E到N**[G]有一条路径,通过路径E←G←N**[G]。此外,观察E和I通过路径E→I←G←N**[G]开启了碰撞路径。

图 9.3 观察E和I通过E→I←N**[I]开启了通往N**[I]的碰撞路径。
最后,观察E有一个直接连接到N**[E]的路径,如图 9.4 所示。

图 9.4 E是N**[E]的直接子节点,因此观察E会直接提供关于N**[E]的信息。
我们的结构化因果模型(SCM)是一个概率模型。在推理步骤中,我们使用此模型来推断P(N**[G], N**[E], N**[I]| E=“低”,I=50)。这个推断将遵循这些依赖路径。
9.1.2 行动:实施假设原因
回想第八章,我们使用理想干预来实施假设条件。我们的假设条件是“如果他们的支线任务参与度很高”,我们通过将假设世界中的E设置为“高”的理想干预来实施这个条件。由于我们使用的是图,所以我们通过如图 9.5 所示的图手术来实施干预。

图 9.5 在假设世界中,将假设条件作为理想干预(通过图手术)实施。
现在平行世界不同了。请注意,在推理步骤中,外生变量的概率分布已经根据实际世界的信息进行了更新。在最后一步,我们将通过这个修改后的假设世界传播这些信息。
9.1.3 预测:推断假设结果
我们正在处理一个结构化因果模型(SCM),因此假设世界中变量的值是由外生变量确定性设置的。在更新了实际世界中观察到的外生变量分布之后,我们现在将实际世界信息从外生变量传播到假设世界中的内生变量。如果我们没有在假设世界中应用干预,根据一致性法则,假设世界将反映我们在实际世界中观察到的所有内容(参见第八章的定义)。然而,由于我们在假设世界中应用了干预,干预之后的假设变量分布可能与实际世界中的不同。

图 9.6 从外生变量条件分布 P(N**[G], N**[E], N**[I]| E=“low”, I=50) 推断假设分布 I 的路径,给定观察到的实际世界结果
在我们的游戏示例中,我们的查询 P(I[E][=“high”]|E=“low”,I = 50) 旨在获取 I[E][=][“high”] 的假设值。图 9.6 说明了从外生变量到 I[E][=][“high”] 假设值的推理路径。请注意,在这个例子中,影响路径仅来自 N[G] 和 N[I],因为对 E 的干预切断了 N[E] 与假设世界之间的桥梁。
在平行世界图上小心处理 d 连接和 d 分离
回想一下,使用因果有向图(DAG),我们可以使用一个称为 d 分离/d 连接的图形标准来使用因果 DAG 在数据生成过程中推理条件独立性。实际上,这就是我在通过 N**[I] 和 N**[G] 高亮 E 和 I 给定 I**[E][=“high”] 的推理路径时所做的事情。我这样做是为了解释算法的归纳和预测步骤。然而,在一般情况下,人们不能依赖于 d 分离和 d 连接来推理不同世界之间内生变量之间的依赖关系。这是因为一致性法则要求不同世界中的相同内生变量必须具有相同的值(除非一对受到干预的影响)。两个变量总是具有相同的值是一种完美的依赖;d 分离的规则没有捕捉到平行世界图上的这种依赖。
在下一章中,我将介绍反事实图,这是一种从平行世界图中导出的因果 DAG,其中 d 分离和独立性之间的联系在各个世界之间保持。
我们可以从实际世界中事实条件 E=“low” 和 I=50 的信息流,通过外生变量,到假设世界中我们的目标变量 I[E][=“high”],来观察推理过程中的信息流动。我们如何实现推理取决于我们对推理算法的偏好。例如,假设 f[G] 和 f[I] 代表 SCM 对 G 和 I 的分配函数。我们可以使用一个简单的正向采样算法:
-
从 P(N[G], N[E], N[I]| E=“low”, I=50) 中抽取外生值 n[G], n[E],和 n[I] 的样本。
-
推导出公会成员假设值 g^* = f**G 的样本。
-
推导出游戏内购买假设值 i^* = f**I 的样本。
-
重复多次以从分布 P(I[E][=“high”]|E=“low”, I = 50) 中获取样本。
-
这将给我们提供目标 P(I[E][=“high”]|E=“low”, I = 50) 的样本。
9.1.4 反事实蒙特卡洛
通用概率反事实推理算法的输出产生分布的样本。回想第二章,一旦你可以从分布中采样,你就可以应用蒙特卡洛技术来根据该分布进行推理。反事实分布也是如此。
例如,在第八章中,我介绍了遗憾的概念,其中我们比较反事实结果。对于参与度低且仅花费 50 美元的玩家,我们可能会问,如果参与度高,他们的游戏内购买会多出多少。鉴于玩家花费了 50 美元,我们可以定义一个遗憾变量为 R[E][=][e] = I[E][=][e] – 50。通过从 P(I[E][=“high”] |E=“low”, I=50) 中采样并减去 50,我们得到 P(R[E][=“high”] |E=“low”, I = 50) 的样本。我们还可以取这些差异的平均值来估计期望遗憾 E(R[E][=“high”] |E=“low”, I = 50)。请注意,这里的 E(…) 指的是期望算子,而不是支线任务参与度。
当我们想在自动化决策算法中使用这些反事实蒙特卡洛技术时,我们通常是在提出关于 policies 的反事实问题。例如,假设一个推荐算法根据玩家的个人资料向玩家推荐某些内容。我们可以对比他们在一种推荐策略下做出的游戏内购买量与在另一种策略下会做出的购买量。然后我们可以调整推荐算法,以最小化玩家之间的累积遗憾。我们将在第十二章更详细地探讨自动化决策。
接下来,我们将探讨几种在代码中实现此算法的案例研究。
9.1.5 案例研究简介
我们可以通过多种方式使用现代概率机器学习工具来实现此算法。在第 9.2-9.4 节中,我们将探讨三个案例研究。
蒙提霍尔问题
第一个案例研究将专注于第 6.3 节中讨论过的蒙提霍尔问题。我们将使用 pgmpy 库来实现一个完整的并行世界图形 SCM。我们将使用 pgmpy 的TabularCPD来实现 SCM 分配函数,这是它没有设计来做的。作为这种尴尬的交换,我们将能够利用 pgmpy 基于图的推理算法(VariableElimination)将推理和预测步骤合并为一个推理步骤。使用基于图的推理将使我们免于实现推理算法;我们只需要构建模型,应用动作步骤,并运行推理。
股骨长度和身高
接下来,我们将回顾第 6.1 节中的法医示例,其中我们有一个 SCM,其中股骨长度是身高的原因。这个例子将向我们展示如何使用变分推理来完成推理步骤,这是一种现代且流行的概率推理技术,与最前沿的深度学习框架配合得很好。
在这个例子中,我们将使用 Pyro 实现 SCM,Pyro 是一个基于 PyTorch 的概率机器学习库。使用 Pyro 会比 pgmpy 感觉不那么尴尬,因为 Pyro 建模抽象更灵活。权衡是,我们必须为推理步骤编写显式的推理代码。
这个例子很简单:数据很小,每个变量只有一个维度,关系是线性的。然而,我们可以使用相同的基于变分推理的推理技术,在大型、高维和非线性数据设置中使用,在这些设置中变分推理表现出色。
基于反事实的语义图像编辑
在最后的案例研究中,我们将研究如何使用预训练的生成图像模型在 PyTorch 中应用反事实推理算法。虽然蒙提霍尔和股骨长度问题是简单的问题,简单的数学,但这个案例研究展示了算法在现代问题上的应用,即深度生成人工智能中的图像生成。
9.2 案例研究 1:蒙提霍尔问题
我们将首先回顾蒙提霍尔问题的 SCM。再次总结,这是一个游戏节目,玩家从三个门的选择开始。其中一扇门后面有一辆车。玩家选择一扇门,比如说第一扇门,主持人,他知道门后面是什么,会打开另一扇门,比如说第三扇门,这扇门后面没有车。主持人给玩家一个换门的机会。在这种情况下,因为玩家选择了第一扇门,而主持人揭示车不在第三扇门后面,玩家可以切换到第二扇门。问题是坚持原来的选择还是换门哪种策略更好。
答案是,对许多人来说反直觉,换门策略更好——三分之二的情况下,换门策略会导致胜利。图 9.7 说明了换门的可能结果。

图 9.7 摩纳哥大厅问题。假设玩家最初选择了第一个门,三分之二的情况下,换门策略将导致获胜。这个插图假设选择了第一个门,但无论初始选择哪个门,结果都是相同的。
我们将探讨两个反事实问题:
-
对于一个坚持最初选择的大门并输掉游戏的玩家,如果他们换门,他们获胜的概率是多少?
-
对于一个输掉游戏的玩家,如果他们换门,他们获胜的概率是多少?
我们将通过以下步骤回答这些问题:
-
在 pgmpy 中将并行世界模型构建为生成性图形模型。
-
在一个世界中基于证据进行推理,以推断另一个世界中的结果。
在我们开始之前,我们将下载一些工具来帮助我们进行分析。列表 9.1 下载了一些用于与 pgmpy 一起工作的辅助函数:用于实现理想干预的 do 函数和用于复制 TabularCPD 对象的 clone。此外,为了生成可视化,你需要安装 Graphviz 可视化库。
设置你的环境
本章中的代码使用 pgmpy 版本 0.1.25 和 Pyro 版本 1.9.1 进行了测试。我使用 Matplotlib 3.7 进行绘图。DAG 的绘图依赖于 Graphviz。
Graphviz 的安装取决于你的环境。使用 Ubuntu 22.04,我通过 libgraphviz-dev 安装了 graphvizl,然后我安装了 Python 库 Graphviz 版本 0.20.3、PyGraphviz 版本 1.13 和 NetworkX 版本 3.3。
根据你的环境,你可能需要安装 pydot 版本 3.0。Graphviz 和 pydot 仅用于绘图,所以如果你遇到困难,你可以放弃在其余代码中的绘图。
列表 9.1 安装 Graphviz 和辅助函数
import graphviz #1
import networkx as nx #1
from networkx.drawing.nx_agraph import write_dot #1
def plot_graph(G): #1
dot_format = nx.nx_pydot.to_pydot(G).to_string() #1
return graphviz.Source(dot_format) #1
import requests #2
def download_code(url): #2
response = requests.get(url) #2
if response.status_code == 200: #2
code_content = response.text #2
print("Code fetched successfully.") #2
return code_content #2
else: #2
print("Failed to fetch code.") #2
return None #2
url_do = ( #3
"https://raw.githubusercontent.com/altdeep/" #3
"causalML/master/book/pgmpy_do.py" #3
) #3
code_do = download_code(url_do) #3
url_clone = ( #4
"https://raw.githubusercontent.com/altdeep/" #4
"causalML/master/book/chapter%209/hyp_function.py" #4
) #4
code_clone = download_code(url_clone) #4
print(code_do) #5
print(code_clone) #5
#exec(code_do) #5
#exec(code_clone) #5
1 安装用于可视化的 Graphviz 库,并创建一个用于绘图的帮助函数。这在 Ubuntu 22.04.3 上进行了测试,但可能取决于你的环境。如果你遇到麻烦,你可以放弃图形绘图并运行其余的代码。
2 用于从 GitHub 下载一些实用工具的辅助函数
3 下载用于应用理想干预的“do”函数的代码。
4 下载用于跨世界复制分配函数的“clone”辅助函数的代码。
5 在执行之前检查下载的代码是良好的安全实践。取消注释“exec”调用以执行下载的代码。
接下来,我们将构建完整的并行世界模型作为图形模型。我们的第一步是指定外生变量分布。
9.2.1 指定外生变量
我们希望将模型实现为 SCM,因此我们将创建具有分布的外生变量,这些分布包含游戏中的所有随机元素。换句话说,给定这些随机元素的结果以及主持人和玩家的选择,游戏的结果将是确定的。
具体来说,我们将介绍两个三面骰子和一次抛硬币。我们将第一个骰子滚动称为车门骰子滚动;它选择放置汽车的车门。玩家滚动第二个骰子,我们将称之为第一次选择骰子滚动,以选择玩家的第一个门选择。这两个骰子滚动将每个结果分配 1/3 的概率。接下来,我们有一个抛硬币,我们将其称为硬币抛掷,我将稍后解释。
列表 9.2 建模:指定外生变量的分布
from pgmpy.factors.discrete.CPD import TabularCPD
p_door_with_car = TabularCPD( #1
variable='Car Door Die Roll', #1
variable_card=3, #1
values=[[1/3], [1/3], [1/3]], #1
state_names={'Car Door Die Roll': ['1st', '2nd', '3rd']} #1
) #1
p_player_first_choice = TabularCPD( #2
variable='1st Choice Die Roll', #2
variable_card=3, #2
values=[[1/3], [1/3], [1/3]], #2
state_names={'1st Choice Die Roll': ['1st', '2nd', '3rd']} #2
) #2
p_coin_flip = TabularCPD( #3
variable='Coin Flip', #3
variable_card=2, #3
values=[[.5], [.5]], #3
state_names={'Coin Flip': ['tails', 'heads']} #3
) #3
1 为选择哪个门得到汽车的三面骰子滚动的外生变量的先验分布
2 为选择玩家第一个选择门的三个面骰子滚动的外生变量的先验分布
3 对硬币抛掷的外生变量的先验分布。主持人抛硬币以确定主持人选择揭示为无车的门以及玩家选择保持或换门策略。
接下来,我们将为我们的内生变量构建分配函数。
9.2.2 指定内生变量的分配函数
我们的内生变量将是主持人门选择、策略(是否采取换门或保持策略)、第二次选择(根据策略选择门 1、2、3)和赢或输(游戏的结局)。
我们在第六章中定义的 SCM 假设内生变量和外生变量之间是一对一的配对——我们通常假设外生变量是独立的,因为如果我们知道一个共同的原因,我们通常会明确地建模它。在这里,我们将放宽这个假设,将每个外生变量匹配到两个内生变量:
-
第一次选择骰子滚动将驱动主持人门选择和第二次选择
-
硬币抛掷将驱动主持人门选择和策略
-
车门骰子滚动将驱动主持人门选择和赢或输
我们将使用这种简化的方法,将一个外生变量匹配到两个内生变量,因为它将需要更少的代码。在这种情况下,这种方法很有效,因为外生变量精确地编码了游戏的所有外生随机元素——这些元素完全决定了游戏的结果。我们也可以使用传统的公式(其中每个内生变量都有一个独特的外生变量)并得到相同的结果。
让我们逐步分析游戏步骤,然后构建 DAG。
策略
玩家将使用硬币抛掷作为他们策略决策的基础——如果主持人抛出正面,玩家将采用换门策略。否则,他们将保持他们的原始选择。
列表 9.3 为策略创建分配函数
f_strategy = TabularCPD(
variable='Strategy',
variable_card=2,
values=[[1, 0], [0, 1]],
evidence=['Coin Flip'],
evidence_card=[2],
state_names={
'Strategy': ['stay', 'switch'],
'Coin Flip': ['tails', 'heads']}
)
主持人门选择
主持人门选择取决于哪个门有车(汽车门骰子滚动)以及玩家的初始门选择(第一次选择骰子滚动)。如果获胜门和第一次选择门相同,主持人将使用硬币翻转从两个可用的门中选择一个门。如果硬币翻转是正面,他们将选择最右边的门,否则是左边的。
列表 9.4 为主持人门选择创建分配函数
f_host_door_selection = TabularCPD(
variable='Host Door Selection',
variable_card=3,
values=[
[0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,0],
[1,0,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1],
[0,1,0,1,0,0,0,0,0,1,1,0,1,1,0,0,0,0]
],
evidence=['Coin Flip',
'Car Door Die Roll',
'1st Choice Die Roll'],
evidence_card=[2, 3, 3],
state_names={
'Host Door Selection':['1st', '2nd', '3rd'],
'Coin Flip': ['tails', 'heads'],
'Car Door Die Roll': ['1st', '2nd', '3rd'],
'1st Choice Die Roll': ['1st', '2nd', '3rd']
}
)
第二选择
第二选择,玩家在第二轮中选择哪个门的决策,取决于策略、主持人门选择(玩家不能切换到主持人打开的门)以及第一次选择骰子滚动(玩家必须坚持或从第一轮选择的门切换)。
列表 9.5 为第二选择创建一个分配函数
f_second_choice = TabularCPD(
variable='2nd Choice',
variable_card=3,
values=[
[1,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,1,0],
[0,1,0,0,1,0,0,1,0,1,0,1,0,1,0,1,0,1],
[0,0,1,0,0,1,0,0,1,0,1,0,1,0,0,0,0,0]
],
evidence=['Strategy', 'Host Door Selection',
'1st Choice Die Roll'],
evidence_card=[2, 3, 3],
state_names={
'2nd Choice': ['1st', '2nd', '3rd'],
'Strategy': ['stay', 'switch'],
'Host Door Selection': ['1st', '2nd', '3rd'],
'1st Choice Die Roll': ['1st', '2nd', '3rd']
}
)
胜或负
胜或负取决于玩家在第二选择中选择的门以及该门是否是获胜门(汽车门骰子滚动)。
列表 9.6 为胜或负创建一个分配函数
f_win_or_lose = TabularCPD(
variable='Win or Lose',
variable_card=2,
values=[
[1,0,0,0,1,0,0,0,1],
[0,1,1,1,0,1,1,1,0],
],
evidence=['2nd Choice', 'Car Door Die Roll'],
evidence_card=[3, 3],
state_names={
'Win or Lose': ['win', 'lose'],
'2nd Choice': ['1st', '2nd', '3rd'],
'Car Door Die Roll': ['1st', '2nd', '3rd']
}
)
在完成外生变量分布和分配函数后,我们可以构建完整的并行世界图形模型。
9.2.3 构建并行世界图形模型
我们现在可以开始构建完整的并行世界模型。首先,我们将添加图中存在的边。
列表 9.7 构建并行世界图形模型
exogenous_vars = ["Car Door Die Roll", #1
"Coin Flip", #1
"1st Choice Die Roll"] #1
endogenous_vars = ["Host Door Selection", #1
"Strategy", #1
"2nd Choice", "Win or Lose"] #1
actual_world_edges = [ #2
('Coin Flip', 'Host Door Selection'), #2
('Coin Flip', 'Strategy'), #2
('Car Door Die Roll', 'Host Door Selection'), #2
('1st Choice Die Roll', 'Host Door Selection'), #2
('1st Choice Die Roll', '2nd Choice'), #2
('Host Door Selection', '2nd Choice'), #2
('Strategy', '2nd Choice'), #2
('2nd Choice', 'Win or Lose'), #2
('Car Door Die Roll', 'Win or Lose') #2
] #2
possible_world_edges = [ #3
(a + " Hyp" if a in endogenous_vars else a, #3
b + " Hyp" if b in endogenous_vars else b) #3
for a, b in actual_world_edges #3
] #3
1 在因果 DAG 中指定外生和内生变量的列表。
2 指定 SCM 的边。
3 复制假设世界的边。
接下来,我们将编译并绘制图形。
列表 9.8 编译和可视化并行世界图
from pgmpy.models import BayesianNetwork
twin_world_graph = BayesianNetwork( #1
actual_world_edges + #1
possible_world_edges #1
) #1
twin_world_graph.add_cpds( #2
p_door_with_car, #3
p_player_first_choice, #3
p_coin_flip, #3
f_strategy, #4
f_host_door_selection, #4
f_second_choice, #4
f_win_or_lose, #4
clone(f_strategy), #5
clone(f_host_door_selection), #5
clone(f_second_choice), #5
clone(f_win_or_lose), #5
)
plot_graph(twin_world_graph)
1 创建并行世界图。
2 绘制并行世界图。
3 在外生变量上添加概率分布。
4 添加来自 SCM 的分配函数。
5 复制分配函数。
上述代码打印出图 9.8 中的并行世界图。

图 9.8 我们反事实问题的完整并行世界图。假设的世界变量有“Hyp.”后缀
在我们回答反事实问题之前,我们将进行快速合理性检查,以确认我们的模型可以生成切换策略导致三分之二的时间获胜的结果。
列表 9.9 确认给定切换策略的正确获胜概率
from pgmpy.inference import VariableElimination #1
infer = VariableElimination(twin_world_graph) #1
strategy_outcome = infer.query( #2
['Win or Lose'], #2
evidence={"Strategy": "switch"} #2
) #2
print(strategy_outcome)
1 使用变量消除实例化推理算法。
2 推断玩家使用切换策略时“胜或负”的概率分布。
这将打印出以下表格。
+-------------------+--------------------+
| Win or Lose | phi(Win or Lose) |
+===================+====================+
| Win or Lose(win) | 0.6667 |
+-------------------+--------------------+
| Win or Lose(lose) | 0.3333 |
+-------------------+--------------------+
如我们所预期,当我们采用换门策略时,我们三分之二的时间会获胜。
9.2.4 运行反事实推理算法
最后,我们将使用推理来回答我们的反事实问题:
-
对于坚持第一门并输掉的玩家,如果他们换门,他们获胜的概率是多少?
-
对于一个输掉游戏的玩家,如果他们换门,他们获胜的概率是多少?
再次,我们使用变量消除作为我们的推理算法选择。我们将使用do函数来执行动作步骤并实现切换的假设条件。然后我们将使用VariableElimination推理算法一次性完成推理和预测步骤。
列表 9.10 推理反事实分布
cf_model = do(twin_world_graph, {'Strategy Hyp': 'switch'}) #1
infer = VariableElimination(cf_model) #2
cf_dist1 = infer.query( #3
['Win or Lose Hyp'], #3
evidence={'Strategy': 'stay', 'Win or Lose': 'lose'} #3
)
print(cf_dist1)
cf_dist2 = infer.query( #4
['Win or Lose Hyp'], #4
evidence={'Win or Lose': 'lose'} #4
)
print(cf_dist2)
1 动作步骤:使用“do”将“策略假设”设置为“切换”,这是一个理想干预的实现。
2 在并行世界图中应用变量消除作为我们的推理算法。
3 这个推理查询回答了“对于一个使用停留策略并输掉的玩家,如果他们使用切换策略,他们会赢吗?”在“策略 == 停留”和“赢或输 == 输”的条件下,我们在并行世界图中推断“赢或输假设”的概率分布。
4 这个推理查询回答了“对于一个输掉的玩家,如果他们使用切换策略,他们会赢吗?”在“赢或输 == 输”的条件下,我们在并行世界图中推断“赢或输假设”的概率分布。
对于问题“对于一个坚持第一个门并输掉的玩家,他们如果换门会赢的概率是多少?”我们有以下概率表:
+-----------------------+------------------------+
| Win or Lose Hyp | phi(Win or Lose Hyp) |
+=======================+========================+
| Win or Lose Hyp(win) | 1.0000 |
+-----------------------+------------------------+
| Win or Lose Hyp(lose) | 0.0000 |
+-----------------------+------------------------+
第一个问题的结果是显而易见的。如果玩家在停留策略中输了,他们的第一个选择没有车。因此,另外两个门中必有一个有车。在这两个门中,主持人必须打开那个没有车的门。剩下的门就会有车。这就是玩家在切换策略中唯一可以切换的门。所以,在停留策略输的条件下,他们通过切换策略获胜的概率是 100%。
对于问题“对于一个输掉的玩家,他们如果换门会赢的概率是多少?”我们有以下概率表:
+-----------------------+------------------------+
| Win or Lose Hyp | phi(Win or Lose Hyp) |
+=======================+========================+
| Win or Lose Hyp(win) | 0.6667 |
+-----------------------+------------------------+
| Win or Lose Hyp(lose) | 0.3333 |
+-----------------------+------------------------+
第二个问题的答案扩展自第一个。我们知道从模型的原有结果来看,如果一个玩家输了,他们有 2/3 的概率使用了停留策略。正如我们从第一个问题中看到的,在这种情况下,切换到切换策略有 100%的获胜机会。有 1/3 的概率是停留策略,在这种情况下,根据一致性规则,有 100%的输掉概率。
使用 pgmpy 的图形模型推理算法可以为离散变量问题(如蒙提霍尔问题)进行反事实推理。在下一个案例研究中,我们将使用变分推理来解决推理步骤,这可以推广到更广泛的问题类别,并利用现代深度学习。
9.3 案例研究 2:反事实变分推理
在下一个案例研究中,我们将使用基于 PyTorch 的概率建模库 Pyro 中的生成模型来实现反事实推理算法。在这里,我们将关注一个法医 SCM 的例子,其中股骨长度是人类身高(在 6.1 节中讨论过)的原因。
在蒙提霍尔例子中,所有变量都是离散的,外生原因完全捕捉了游戏的随机元素。这使得我们能够在 pgmpy 中使用TabularCPD为分配函数实现 SCM(尽管有些笨拙),然后显式创建一个并行世界图形模型。一旦完成,图形建模推理算法VariableElimination就为我们处理了推理和预测步骤。
相比之下,我们的第二个案例研究提出了一种可以推广到更多类型问题的方法。我们将使用基于 PyTorch 的深度概率建模库 Pyro。我们将使用变分推理处理推理步骤,这是深度学习时代的一种流行推理算法。
在这个例子中,我们将使用这种建模方法来对比两个问题:
-
一个条件假设:“如果一个人的股骨长度是 46 厘米,那么他的身高会是多少?” P(H[F][=46])
-
一个并行世界的反事实:“一个人的股骨长度是 44 厘米,身高是 165 厘米。如果股骨长度是 46 厘米,那么他的身高会是多少?” P(H[F][=46]|F=44, H=165)
在这两种情况下,我们推断H[F][=46](其中H是身高,F是股骨长度)上的分布,但在反事实情况下,我们基于观察到的F=44 和H=165 进行条件推断。实现对比这两个分布的H[F][=46]的代码将帮助我们理解反事实查询的独特之处。
9.3.1 构建模型
为了使事情更有趣,我们将通过添加一个生物性别变量来修改模型,该变量驱动股骨长度和身高。图 9.9 说明了新的因果图。请注意,我们的问题没有提到任何关于性别的内容,因此我们预计会在我们的分布P(H[F][=46])和P(H[F][=46]|F=44, H=165)中看到与性别相关的方差。

图 9.9 股骨长度与身高之间关系的原因图。两者都由生物性别驱动。
下面的代码在 Pyro 中实现了模型。注意创建和使用PseudoDelta分布函数。内生变量是外生变量的确定性函数,但为了变分推理能够工作,我们必须使用pyro.sample为内生变量分配一个分布。我们可以使用狄拉克 delta 分布,它将所有概率值分配给变量的分配函数的输出。但在这种情况下,基于梯度的优化将不起作用。相反,我们将使用“伪 delta”分布——一个具有非常小的尺度参数的正态分布。
列表 9.11 在 Pyro 中实现股骨 SCM
from torch import tensor
from pyro.distributions import Bernoulli, Normal
from pyro import sample
from functools import partial #1
PseudoDelta = partial(Normal, scale=.01) #1
def f_sex(N_sex): #2
return sample("sex", Bernoulli(N_sex)) #2
def f_femur(sex, N_femur): #3
if sex == tensor(1.0): #3
μ = 43.7 + 2.3 * N_femur #3
else: #3
μ = 40.238 + 1.9 * N_femur #3
return sample("femur", PseudoDelta(μ)) #3
def f_height(femur, sex, N_height): #4
if sex == tensor(1.0): #4
μ = 61.41 + 2.21 * femur + 7.62 * N_height #4
else: #4
μ = 54.1 + 2.47 * femur + 7 * N_height #4
return sample("height", PseudoDelta(μ)) #4
def model(exogenous):
N_sex = sample("N_sex", exogenous['N_sex']) #5
N_femur = sample("N_femur", exogenous['N_femur']) #5
N_height = sample("N_height", exogenous['N_height']) #5
sex = f_sex(N_sex) #6
femur = f_femur(sex, N_femur) #6
height = f_height(femur, sex, N_height) #6
return sex, femur, height
exogenous = { #7
'N_sex': Bernoulli(.5), #7
'N_femur': Normal(0., 1.), #7
'N_height': Normal(0., 1.), #7
} #7
1 使用“伪 delta”分布启用近似推理以模拟确定性 delta 分布。
2 生物性别的分配函数
3 厘米单位股骨长度的分配函数。分配使用两个线性函数,每个性别一个。
4 身高分配函数。同样,它使用两个线性函数,每个性别一个。
5 从外生变量先验分布中提取样本
6 在给定外生变量的情况下获取内生变量。
7 指定外生变量的先验分布。
再次,我们的反事实推理算法有三个步骤:
-
归纳
-
行动
-
预测
与我们的 pgmpy 模型不同,我们不需要克隆并行世界中的所有变量。我们只需使用干预操作符pyro.do来应用干预并获取干预模型。对于P(H[F][=][46]),我们将根据从P(N[Sex],N[Femur],N[Height])中采样的样本从干预模型中生成。对于反事实分布,我们将使用变分推理算法进行归纳步骤,以学习P(N[Sex],N[Femur],N[Height]|F=44, H=165)。然后我们再次从干预模型中生成,但这次是基于从P(N[Sex],N[Femur],N[Height]|F=44, H=165)中采样的样本。
处理难以处理的似然
我们使用变分推理来进行归纳步骤,根据观察到的内生变量推断外生变量。变分推理是一种基于似然的技术。通常,我们通过从分布中采样并使用分布的概率质量/密度函数来获取该采样值的概率值来获得似然。但对于 SCMs,我们无法这样做,因为内生变量的值是由分配函数设置的,而不是通过采样。在这个法医示例中的代码使用从“伪”狄拉克δ分布中进行采样,这意味着一个具有非常小的尺度参数的正态分布。这种方法,它从正态分布中提供似然值,属于称为近似贝叶斯计算的方法类别,并且与其他该类成员共享一些权衡。
一种替代方法是使用摊销推理。在这种方法中,你采样许多外生变量值,并使用这些值来计算许多内生变量值。最后,你使用这些样本来训练一个模型,该模型根据内生变量值预测外生变量值。然后你在归纳步骤中使用这个训练好的模型。
处理难以处理的似然是概率机器学习中的一个更广泛挑战,这超出了本书的范围。有关附加参考文献和资源的链接,请参阅章节注释www.altdeep.ai/p/causalaibook。
9.3.2 使用 pyro.do 实现干预
现在,让我们提出条件假设,“如果股骨长度为 46 厘米,身高会是什么样子?”图 9.10 说明了表示将股骨长度设置为 46 的理想干预措施的修改后的 DAG。

图 9.10 我们用理想的干预措施表示假设条件,并在因果 DAG 上表示图手术。
在 Pyro 中,我们将对原始模型应用pyro.do以获取干预模型。然后我们将反复调用算法,使用外生变量分布的先验,并返回生成的内生值。我们将重复几次,并使用直方图可视化身高上的干预分布。
列表 9.12 从“如果股骨长度为 46cm”的干预分布中抽取样本
import matplotlib.pyplot as plt
import pyro
int_model = pyro.do(model, data={"femur": tensor(46.0)}) #1
int_samples = [] #2
for _ in range(10000): #2
_, _, int_height = int_model(exogenous) #2
int_samples.append(float(int_height)) #2
plt.hist( #3
int_samples, #3
bins=20, #3
alpha=0.5, #3
label="Intervention Samples", #3
density=True #3
) #3
plt.ylim(0., .35) #3
plt.legend() #3
plt.xlabel("Height") #3
plt.show() #3
使用 pyro.do 实现假设条件“...如果股骨长度为 46 厘米”的模拟,这将返回一个实现干预措施的新模型。
2 从干预分布中抽取样本。
3 使用样本直方图可视化干预分布。
图 9.11 显示了从P(H[F][=46])中抽取的样本的直方图。我们将将其与P(H[F][=46]|F=44, H=165)的直方图进行对比。

图 9.11 这个样本直方图可视化了干预分布——x轴对应不同的身高值范围,y轴是落在每个范围内的抽样身高的比例。
现在我们将进行反事实推理。
9.3.3 使用变分推理实现推理步骤

图 9.12 股骨长度反事实的平行世界图
我们的假设条件问题是,“如果一个人的股骨长度是 46 厘米,那么这个人的身高会是多少?”现在我们想要回答反事实问题:“一个人的股骨长度是 44 厘米,身高是 165 厘米。如果他们的股骨长度是 46 厘米,他们的身高会是多少?”换句话说,我们想要将P(H[F][=][46])扩展到P(H[F][=][46]|F=44, H=165)。图 9.12 说明了相应的平行世界图。
遵循反事实推理算法,我们需要进行推理步骤并推断P(N[性别], N[股骨], N[身高]|F=44, H=165)。我们将使用变分推理,其中我们将指定一个引导函数——一个具有可训练参数的函数,代表一个分布Q(N[性别], N[股骨], N[身高])。训练过程优化引导函数的参数,使得Q(N[性别], N[股骨], N[身高])与P(N[性别], N[股骨], N[身高]|F=44, H=165)紧密近似。
复习:提议分布和 Pyro 的引导函数
Pyro 使用“引导函数”允许开发者编写他们自己的提议分布,为目标分布中的变量“提议”值。基于抽样的推理算法(例如,重要性抽样或 MCMC)使用提议生成样本,然后对样本进行操作,以便它们代表目标分布。变分推理优化提议分布的参数,使其接近(或“近似”)目标分布。与 pgmpy 的自动推理算法相比,引导函数允许开发者根据他们的需求“引导”推理。
列表 9.13 指定变分推理的引导函数
import torch.distributions.constraints as constraints
from pyro.primitives import param
from pyro.distributions import Delta
def guide(exogenous): #1
p = param("p", tensor(.5), #2
constraint=constraints.unit_interval) #2
n_sex = sample("N_sex", Bernoulli(p)) #2
sex = sample("sex", Bernoulli(n_sex)) #3
n_femur_loc = param("n_femur_loc", tensor(0.0)) #4
n_femur_scale = param( #4
"n_femur_scale", #4
tensor(1.0), #4
constraint=constraints.positive #4
) #4
femur_dist = Normal(n_femur_loc, n_femur_scale) #5
n_femur = sample("N_femur", femur_dist) #5
n_height_loc = param("n_height_loc", tensor(0.0)) #5
n_height_scale = param( #5
"n_height_scale", #5
tensor(1.0), #5
constraint=constraints.positive #5
) #5
height_dist = Normal(n_height_loc, n_height_scale) #5
n_height = sample("N_height", height_dist) #5
femur = sample("femur", Delta(n_femur)) #6
height = sample("height", Delta(n_height)) #6
1 将外生先验分布传递给引导函数。该函数不会使用此参数,但引导函数和模型函数的签名必须匹配。
2 引导函数试图从伯努利分布中近似 P(N_sex|femur, height)。优化目标是这个伯努利分布的参数。
3 n_sex 要么是 0,要么是 1。当作为伯努利分布的参数传递时,结果是确定的。
4 引导函数试图从正态分布中近似 P(N_femur|femur, height)。优化目标是这个正态分布的位置和尺度参数。
5 引导函数试图从正态分布中近似 P(N_height|femur, height)。
6 由于我们对股骨和身高进行了条件化,它们在引导函数中不是必需的。但如果我们想在新的分析中对不同的结果进行条件化,它们是有用的。
确定性归因
当以下两个条件都成立时,归因步骤是一个特殊情况:
-
你观察到所有内生变量。
-
SCM 赋值函数是可逆的。
在那种情况下,给定所有内生变量的观测值,你可以使用逆赋值函数计算外生变量的精确点值。因此,你在假设世界中应用赋值函数以获得假设结果的点值。然而,大多数实际例子都落在以下一般情况中:
-
你只对一些内生变量进行条件化。
-
SCM 赋值函数是不可逆的。
在我们的归因步骤中,我们首先根据股骨和身高的观测值对模型进行条件化。
列表 9.14 基于股骨和身高实际值的条件化
conditioned_model = pyro.condition(
model,
data={"femur": tensor(44.0), "height": tensor(165.0)}
)
接下来,我们使用变分推理根据股骨和身高推断外生变量。
列表 9.15 使用变分推理实现归因步骤
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam
pyro.util.set_rng_seed(123) #1
pyro.clear_param_store() #2
svi = SVI( #3
model=conditioned_model,
guide=guide,
optim=Adam({"lr": 0.003}), #4
loss=Trace_ELBO() #5
)
losses = [] #6
num_steps = 5000 #7
for t in range(num_steps): #7
losses.append(svi.step(exogenous)) #7
plt.plot(losses) #8
plt.title("Loss During Training") #8
plt.xlabel("step") #8
plt.ylabel("loss") #8
1 设置一个种子以实现可重复性。
2 清除任何当前的参数值。
3 初始化随机变分推理算法。
4 使用学习率为 .003 优化参数。
5 使用(负)证据下界(ELBO)作为损失函数。
6 初始化一个列表以存储用于绘图的损失值。
7 运行优化 5,000 步。SVI 的步骤对象与模型和引导函数具有相同的签名,因此必须在此处传递任何模型/引导参数。
8 绘制训练过程中的损失图。
图 9.13 显示了训练过程中的损失,表明变分推理已收敛。

图 9.13 显示了近似 P(N**[Sex], N**[Femur], N**[Height]|F=44, H=165) 分布的参数优化过程中的损失
训练完成后,我们提取更新后的外生变量分布的优化参数。
列表 9.16 提取更新后的外生分布的参数
n_sex_p = param("p").item() #1
n_femur_loc = param("n_femur_loc").item() #1
n_femur_scale = param("n_femur_scale").item() #1
n_height_loc = param("n_height_loc").item() #1
n_height_scale = param("n_height_scale").item() #1
exogenous_posterior = { #2
'N_sex': Bernoulli(n_sex_p), #2
'N_femur': Normal(n_femur_loc, n_femur_scale), #2
'N_height': Normal(n_height_loc, n_height_scale), #2
} #2
1 提取参数值。
2 使用优化后的参数进行推理,创建新的“后验”外生变量分布。
需要注意的一点是,虽然我们在结构因果模型(SCM)中通常为外生变量指定独立的先验分布,但外生变量在给定内生变量的情况下通常是条件相关的(因为存在碰撞路径!)。然而,我编写了一个指导函数,独立地采样外生变量,忽略了这种条件相关性。编写将依赖变量视为独立的指导函数既方便又是常见做法,但这样做会在结果中引入一些偏差。你可以通过编写一个维护图中隐含依赖关系的指导函数来避免这种情况。
使用 ChiRho 进行反事实建模
ChiRho 是 Pyro 的因果扩展,旨在更无缝地将 Pyro 的概率建模方法与因果推理相结合。ChiRho 具有并行世界抽象以及使用正态化流和本例中讨论的变分推理方法实现反事实推理的抽象。作为 Pyro 的扩展,本案例研究中讨论的建模技术也将适用于 ChiRho。
9.3.4 实现行动和预测步骤
在蒙提霍尔例子中,我们明确构建了并行世界模型。在这个例子中,我们可以通过使用 pyro.do 来执行行动步骤,从而获取假设的世界模型,并使用更新后的外生变量分布从该模型中采样。
我们将重复从设置股骨长度为 46 厘米的干预模型中生成样本的步骤。回想一下,我们已经在列表 9.11 中创建了该干预模型,如下所示:
int_model = pyro.do(model, data={"femur": tensor(46.0)})
为了从干预分布中采样,我们在原始的外生变量分布上调用 int_model。现在,在预测步骤中,我们再次调用它,这次使用 exogenous_posterior 而不是 exogenous,因为 exogenous_posterior 编码了实际世界中的所有信息。
列表 9.17 从反事实分布中采样
cf_samples = []
for _ in range(10000):
_, _, cf_height = int_model(exogenous_posterior)
cf_samples.append(float(cf_height))
最后,我们在图 9.14 中叠加了来自反事实分布的样本直方图与干预分布直方图,我们可以清楚地看到这些分布之间的差异。
列表 9.18 比较干预和反事实分布
plt.hist(
int_samples,
bins=20,
alpha=0.5,
label="Intervention Samples",
density=True
)
plt.hist(
cf_samples,
bins=20,
alpha=0.5,
label="Counterfactual Samples",
density=True
)
plt.ylim(0., .35)
plt.legend()
plt.xlabel("Height")
plt.show()
结果图,如图 9.14 所示,对比了干预和反事实样本的直方图。

图 9.14 由因果模型编码的干预和反事实分布生成的样本直方图
图 9.14 说明了反事实分布通常比表示相同假设条件的干预分布具有更小的分散。反事实分布本质上将干预分布过滤到实际世界中观察到的条件为真的案例。在这种情况下,我们有两个对应于两种性别的身高钟形曲线。这些钟形曲线在干预分布中具有更强的重叠。
在最后一个例子中,我们将评估如何在生成式 AI 图像模型的背景下运行反事实推理算法。
9.4 案例研究 3:使用深度生成模型进行反事实图像生成
在生成式 AI 中,用户提供输入,算法生成一些输出。例如,假设我想写一个关于哈丽特·塔布曼成为海盗船长的替代历史的脚本。我转向一个生成式图像模型来获取一些概念艺术,提出了文本问题:“哈丽特·塔布曼作为海盗船长的样子会是什么样子?”该模型生成了图 9.15 中的图像。

图 9.15 生成式 AI 图像模型根据自然语言输入提示“哈丽特·塔布曼作为海盗船长的样子会是什么样子?”的输出。
这个问题本身就是一个反事实——哈丽特·塔布曼并不是海盗。在第十三章中,我们将探讨使用大型语言模型进行自然语言反事实。在这里,我们将对图 9.15 中的图像进行反事实推理。
假设我喜欢这幅图像,但我想进行编辑——我想将这幅图像修改为去除眼镜。一种方法是用像“填充”这样的工具,我选择带有眼镜的像素,并指示我希望像素中的内容消失。这将直接编辑图像的形式。
另一种方法可以是语义编辑,在这种方法中,我并不是直接操作图像中的像素,而是操作与“眼镜”相对应的图像的某些潜在表示。实际上,我提出了一个反事实问题:“如果主体没有戴眼镜,这幅图像会是什么样子?”图 9.16 对比了图像的原始版本和“反事实”版本。

图 9.16 给定左侧生成的图像,用户可能会向生成式 AI 提出一个反事实问题:“如果没有眼镜,这幅图像会是什么样子?”他们期望得到类似右侧的图像,其中图像的概念元素在眼镜去除后不应受到影响。
这是一个有吸引力的用例,因为操纵基本概念通常比操纵形式更可取,尤其是在你想要进行的编辑不是所有都位于像素的同一特定区域时。如果我们的概念模型是一个因果模型,那么特别有吸引力,因为改变概念的下流因果后果会反映在图像中,而一致性法则会防止图像中不受概念改变影响的部分发生变化。
考虑到这个用例,本节将使用我们的反事实算法实现一种语义编辑形式。我们将从实际图像开始。在推理步骤中,我们将推断图像的一些潜在表示。在动作步骤中,我们将提出所需的编辑,在预测步骤中,我们将生成新的图像。
在这个例子中,我们将使用在 PyTorch 中构建的变分自动编码器构建的 SCM。我们还将使用一个名为 dSprites 的简单数据集来证明概念。dSprites 数据展示了这个想法,并且足够简单,可以在普通笔记本电脑上快速训练模型。有关更多实际反事实图像建模示例的参考,请参阅章节注释www.altdeep.ai/p/causalaibook。
9.4.1 dSprites 数据
dSprites 数据集由 2D 形状组成,每个形状渲染在 8 个可能的位置、6 个可能的尺度和 40 个可能的旋转中。形状由 5 个独立因素组成:形状、尺度、旋转、x-位置和y-位置。图 9.17 展示了数据集的样本。

图 9.17 dSprites 数据具有由五个独立因果因素(形状、尺度、旋转、x-位置和y-位置)因果决定的图像特征。
我们将把这些因素视为图像变量的原因,如图 9.18 中的因果 DAG 所示。

图 9.18 dSprites 图像的因果 DAG,以板模型的形式显示以突出N**[I]和I的形状。N**[i]是图像的外生变量。该模型使用编码器-解码器框架进行训练,该框架使用 50 × 1 维度的图像编码来表示N**[I]。
在以下代码中,我们加载 dSprites 数据集中的一个特定图像。
列表 9.19 加载 dSprites 图像
import torch
from matplotlib import pyplot as plt
import io #1
import urllib.request #1
import numpy as np #1
url = ('https://github.com/altdeep/causalML/blob/master/' #1
'book/chapter%209/sprites_example.npz?raw=true') #1
with urllib.request.urlopen(url) as response: #1
data = response.read() #1
file = io.BytesIO(data) #1
npzfile = np.load(file) #1
img_dict = dict(npzfile) #1
img = torch.tensor(img_dict['image'].astype(np.float32) ) #2
plt.imshow(img, cmap='Greys_r', interpolation='nearest') #2
plt.axis('off') #2
plt.title('original') #2
plt.show() #2
causal_factor = torch.from_numpy(img_dict['label']).unsqueeze(0) #3
print(causal_factor) #3
1 从 GitHub 下载 dSprites 示例并加载它。
2 绘制 dSprites 图像。
3 示例的因果因素为[0 0 1 13 26 14],第一个元素始终为 0,第二个元素对应于“正方形”并由 0 表示。其余元素对应于尺度、方向和 X 和 Y 位置。
这张图显示了图 9.19 中的图像。

图 9.19 dSprites 数据的一个示例
打印causal_factor产生tensor ([[ 0, 0, 1, 13, 26, 14]])。对于数据中的所有示例,第一个元素都是 0。因果因素向量的第二个元素对应于形状。正方形、椭圆和心形分别用 0、1 和 2 表示。图像包含一个正方形(P=0),比例S=1,方向O=13,位置X=26 和Y=14。
在这个案例研究中,我们将问:“如果形状是心形而不是正方形,这幅图像会是什么样子?”这暗示了图 9.20 中的并行世界网络。

图 9.20 问题“给定图像,如果形状是心形会是什么样子?”所暗示的并行世界图。
首先,我们将加载一个预训练的编码器,将图像映射到因果因素的因果变量。在这个简单模型中,我们将假设因果因素的外生变量的赋值函数是恒等函数,即因果因素及其外生变量将具有相同的值。让我们先初始化编码器。
列表 9.20 加载因果因素的编码器
import requests
import torch.nn as nn
CARDINALITY = [1, 3, 6, 40, 32, 32] #1
class EncoderCausalFactors(nn.Module): #2
def __init__(self, image_dim, factor_dim):
super(EncoderCausalFactors, self).__init__()
self.image_dim = image_dim
self.factor_dim = factor_dim
hidden_dim = 1000 #3
self.fc1 = nn.Linear(image_dim, hidden_dim) #4
self.fc2 = nn.Linear(hidden_dim, hidden_dim) #4
self.fc3 = nn.Linear(hidden_dim, factor_dim) #4
self.softplus = nn.Softplus() #4
self.sigmoid = nn.Sigmoid() #5
def forward(self, img):
img = img.reshape(-1, self.image_dim) #6
hidden1 = self.softplus(self.fc1(img)) #7
hidden2 = self.softplus(self.fc2(hidden1)) #7
p_loc = self.sigmoid(self.fc3(hidden2)) #8
return p_loc #8
encoder_n_causal_factors = EncoderCausalFactors( #9
image_dim=64*64, #9
factor_dim=sum(CARDINALITY) #9
) #9
1 因果因素在每个维度上的基数
2 因果因素的外生父向量编码器
3 隐藏层长度为 1,000。
4 使用通过 Softplus 激活函数传递的线性变换
5 最终激活是一个 sigmoid 函数。
6 展平图像。
7 计算隐藏层。
8 输出层生成一个概率向量,用作 OneHotCategorical 分布的参数。
9 初始化编码器。图像维度是 64 × 64 像素,因果因素向量的六个元素被 one-hot 编码成一个长度为 1 + 3 + 6 + 40 + 32 + 32 = 114 的向量。
接下来,我们将从本书的 GitHub 仓库下载并加载预训练的权重到这个编码器中。
列表 9.21 下载并加载预训练权重到因果因素编码器
url = ('https://github.com/altdeep/causalML/raw/master/'
'book/chapter%209/sprites-model-encoder-causal-factors.pt')
response = requests.get(url)
response.raise_for_status()
with open('temp_weights.pt', 'wb') as f:
f.write(response.content)
state_dict = torch.load(
'temp_weights.pt',
map_location=torch.device('cpu')
)
encoder_n_causal_factors.load_state_dict(state_dict)
首先,我们将测试编码器能否从图像中恢复出因果因素。
列表 9.22 生成因果外生因素的示例
from pyro import distributions as dist
def decode_one_hot(factor_encoded, cardinality=CARDINALITY):
split = [
torch.split(element, cardinality) #1
for element in factor_encoded #1
] #1
labels = [[int(torch.argmax(vec)) for vec in item] #1
for item in split] #1
return torch.tensor(labels) #1
def sample_one_hot(p_encoded, cardinality=CARDINALITY): #2
split = [torch.split(element, cardinality) #2
for element in p_encoded] #2
sample_list = [ #2
[ #2
dist.OneHotCategorical(p_vec).sample() #2
for p_vec in item #2
] for item in split #2
] #2
sample = torch.stack([ #2
torch.cat(samples, -1) #2
for samples in sample_list #2
]) #2
return sample #2
inferred_cause_p = encoder_n_causal_factors.forward(img) #3
sampled_factors = sample_one_hot( #3
inferred_cause_p #3
) #3
print(decode_one_hot(sampled_factors)) #3
1 解码编码器 one-hot 编码输出的辅助函数
2 来自编码器 _causal_factors 输出概率向量的样本
3 使用编码器预测因果因素。
对采样图像进行编码会打印出因果因素:[ 0, 0, 1, 13, 26, 14]。编码器准确地从图像中恢复了因果因素。
接下来,我们将初始化一个编码器,我们将用它来推理N[I],图像的外生变量。这个编码器接受一个图像和一个因果因素向量的实例作为输入。
列表 9.23 用于N**[I]推理的编码器
class EncoderNImage(nn.Module): #1
def __init__(self, image_dim, factor_dim, n_image_dim):
super(EncoderNImage, self).__init__()
self.image_dim = image_dim
self.factor_dim = factor_dim
self.n_image_dim = n_image_dim
hidden_dim = 1000
self.fc1 = nn.Linear(
self.image_dim + self.factor_dim, hidden_dim #2
) #2
self.fc2 = nn.Linear(hidden_dim, hidden_dim) #2
self.fc31 = nn.Linear(hidden_dim, n_image_dim) #2
self.fc32 = nn.Linear(hidden_dim, n_image_dim) #2
self.softplus = nn.Softplus() #2
def forward(self, img, factor):
img = img.reshape(-1, self.image_dim) #3
inputs = torch.cat((img, factor), -1) #4
hidden1 = self.softplus(self.fc1(inputs)) #5
hidden2 = self.softplus(self.fc2(hidden1)) #5
n_image_loc = self.fc31(hidden2) #6
n_image_scale = torch.exp(self.fc32(hidden2)) #6
return n_image_loc, n_image_scale #6
encoder_n_image = EncoderNImage( #7
image_dim=64*64, #7
factor_dim=sum(CARDINALITY), #7
n_image_dim=50 #7
) #7
1 用于N[I]推理的编码器,它既是图像的因果外生变量,也是 VAE 中的图像编码
2 使用传递到 Softplus 激活函数的线性变换
3 展平图像。
4 连接图像和因果因素向量。
5 计算隐藏层。
6 在 N [I]上计算多元正态分布的位置和尺度参数。
7 初始化编码器。
噪声变量的编码器需要因果因素进行 one-hot 编码,因此我们将创建一个辅助函数来完成这项工作。
列表 9.24 创建一个用于 one-hot 编码的函数
def encode_one_hot(factor, cardinality=CARDINALITY):
new_factor = []
for i, factor_length in enumerate(cardinality):
new_factor.append(
torch.nn.functional.one_hot(
factor[:,i].to(torch.int64), int(factor_length)
)
)
new_factor = torch.cat(new_factor, -1)
return new_factor.to(torch.float32)
再次,我们将下载并加载编码器的预训练权重。
列表 9.25 加载用于推理N**[I]的编码器预训练权重
weight_url = ("https://github.com/altdeep/causalML/raw/master/" #1
"book/chapter%209/sprites-model-encoder-n-image.pt") #1
response = requests.get(weight_url) #1
response.raise_for_status() #1
with open('temp_weights.pt', 'wb') as f: #1
f.write(response.content) #1
state_dict = torch.load( #1
'temp_weights.pt', #1
map_location=torch.device('cpu') #1
) #1
encoder_n_image.load_state_dict(state_dict) #1
n_image_loc, n_image_scale = encoder_n_image.forward( #2
img, #2
encode_one_hot(causal_factor) #2
) #2
n_image = torch.normal(n_image_loc, n_image_scale) #3
1 加载预训练的权重。
2 将图像和因果因素传递到编码器中,并获取 N [I]位置和尺度参数。
3 从 N [I]的后验分布中生成。
最后,我们将加载一个将N[I]和因果因素映射回图像的解码器。在因果意义上,解码器是图像的分配函数的一部分。
列表 9.26 加载并初始化将原因和N**[I]映射到图像的解码器
class Decoder(nn.Module): #1
def __init__(self, image_dim, factor_dim, n_image_dim):
super(Decoder, self).__init__()
hidden_dim = 1000
self.fc1 = nn.Linear(n_image_dim + factor_dim, hidden_dim) #2
self.fc2 = nn.Linear(hidden_dim, hidden_dim) #2
self.fc3 = nn.Linear(hidden_dim, hidden_dim) #2
self.fc4 = nn.Linear(hidden_dim, image_dim) #2
self.softplus = nn.Softplus() #2
self.sigmoid = nn.Sigmoid() #2
def forward(self, n_image, factor):
inputs = torch.cat((n_image, factor), -1) #3
hidden1 = self.softplus(self.fc1(inputs)) #4
hidden2 = self.softplus(self.fc2(hidden1)) #4
hidden3 = self.softplus(self.fc3(hidden2)) #4
p_img = self.sigmoid(self.fc4(hidden3)) #5
return p_img #5
decoder = Decoder( #6
image_dim=64*64, #6
factor_dim=sum(CARDINALITY), #6
n_image_dim=50 #6
) #6
1 解码器将因果因素和 N_image 映射以生成图像上多元伯努利分布的参数。
2 模型使用线性变换,隐藏层使用 Softplus 激活,输出层使用 sigmoid 激活。
3 网络在输入层中连接 n_image 和因素。
4 输入通过三个具有 Softplus 激活函数的隐藏层。
5 输出是一个传递到图像像素上多元伯努利分布的概率参数。
6 初始化编码器。
再次,我们将下载并加载解码器的预训练权重。
列表 9.27 下载并加载解码器权重
dcdr_url = ("https://github.com/altdeep/causalML/raw/master/"
"book/chapter%209/sprites-model-decoder.pt")
response = requests.get(dcdr_url)
response.raise_for_status()
with open('temp_weights.pt', 'wb') as f:
f.write(response.content)
state_dict = torch.load(
'temp_weights.pt',
map_location=torch.device('cpu')
)
decoder.load_state_dict(state_dict)
在我们生成反事实图像之前,我们将创建一个辅助函数来绘制它。
列表 9.28 用于绘制反事实图像的辅助函数
def compare_reconstruction(original, generated):
fig = plt.figure()
ax0 = fig.add_subplot(121)
plt.imshow(
original.cpu().reshape(64, 64),
cmap='Greys_r',
interpolation='nearest'
)
plt.axis('off')
plt.title('actual')
ax1 = fig.add_subplot(122)
plt.imshow(
generated.reshape(64, 64),
cmap='Greys_r', interpolation='nearest')
plt.axis('off')
plt.title('counterfactual')
plt.show()
现在,我们将指定 SCM。我们将编写一个p_n_image函数,它从P(N[image])生成,并为图像编写一个f_image分配函数。
列表 9.29 创建图像的外生分布和分配函数
def p_n_image(n_image_params): #1
n_image_loc, n_image_scale, n_unif_upper = n_image_params #2
n_image_norm = dist.Normal( #3
n_image_loc, n_image_scale #3
).to_event(1).sample() #3
n_image_unif = dist.Uniform(0, n_unif_upper).expand( #4
torch.Size([1, 64*64]) #4
).sample() #4
n_image = n_image_norm, n_image_unif #5
return n_image
def f_image(factor, n_image): #6
n_image_norm, n_image_unif = n_image #7
p_output = decoder.forward( #8
n_image_norm, #8
encode_one_hot(factor) #8
)
sim_img = (n_image_unif <= p_output).int() #9
return sim_img
1 一个从 N_image 外生分布生成随机变量的函数
2 N_image 分布的参数包括正态分布的位置和尺度参数以及均匀分布的上限。
3 从正态分布中采样一个正态随机变量。
4 从均匀分布中采样一个均匀随机变量。
5 将这些合并成一个 n_image 对象。
6 图像的分配函数
7 外生噪声变量分解为一个正态随机变量和一个均匀随机变量。
8 正态随机变量通过解码器传递以获得像素的概率向量。
9 每个像素都通过一个指示函数确定性地设置,如果均匀随机变量的一个元素小于概率向量的相应元素,则返回 1,否则返回 0。
最后,我们可以运行反事实推理算法的步骤来回答问题:“如果这张图像是心脏,它会是什么样子?”
列表 9.30 生成反事实图像
def abduct(img, factor, smoother=1e-3): #1
n_image_loc, n_image_scale = encoder_n_image.forward( #2
img, encode_one_hot(factor) #2
) #2
n_unif_upper = decoder.forward( #3
n_image_loc, #3
encode_one_hot(factor) #3
) #3
n_unif_upper = n_unif_upper * (1 - 2 * smoother) + smoother #3
p_image_params = n_image_loc, n_image_scale, n_unif_upper #4
return p_image_params
def do_action(factor, element=1, val=2): #5
intervened_factor = factor.clone() #5
intervened_factor[0][element] = val #5
return intervened_factor #5
def predict(intervened_factor, n_image_params): #6
n_image = p_n_image(n_image_params) #6
sim_img = f_image(intervened_factor, n_image) #6
return sim_img #6
def counterfactual(img, factor): #7
p_image_params = abduct(img, factor) #7
intervened_factor = do_action(factor) #7
pred_recon = predict(intervened_factor, p_image_params) #7
compare_reconstruction(img, pred_recon) #7
counterfactual(img, causal_factor) #8
1 推理步骤:根据图像推断外生变量。
2 推断 N_I 的参数。首先,这包括正态分布的两个参数。
3 其次,我们推断均匀分布的上限,并应用平滑使其不是正好为 1 或 0。
4 将这些合并成一个推断参数集。
5 行动步骤:应用将形状元素设置为“心形”(用整数 2 表示)的干预措施。
6 预测步骤:从 P(N_image)生成 n_image,并通过一个赋值函数生成一个图像。
7 应用所有三个步骤:推断 n_image,应用干预措施,并正向生成反事实图像。
8 绘制结果。
图 9.21 显示了结果。

图 9.21 原始图像(左)和反事实生成的图像(右)
这是一个概念验证——在反事实图像生成中存在一些额外的细微差别。我在这个 dSprites 示例中有点作弊。反事实生成之所以有效,是因为因果因素是独立的,而且数据相当简单。为了使反事实图像生成在一般情况下有效,我们需要理解和满足某些假设。
9.4.2 反事实图像生成所需的假设
在下一章中,我们将解决识别问题。识别是在给定的建模假设和我们可用的数据的基础上确定我们可以回答哪些因果问题。反事实推理算法假设您拥有真实世界的结构化因果模型(SCM)。如果您能做出这个假设,您就可以使用该算法来回答任何反事实(或干预)查询。
在大多数情况下,我们无法实际假设我们拥有真实世界的 SCM。最多,您将有一个作为真实世界近似值的 SCM。例如,生成 dSprites 图像的真实过程肯定没有涉及解码器神经网络——我们使用具有这种解码器架构的深度学习来近似这个过程。您将在下一章中看到,这种学习到的近似并不保证产生忠实于真实数据生成过程的反事实。
但在图像和其他媒体模态(例如,文本、音频、视频)的反事实生成方面,有一些特殊之处。在这些情况下,当我们可以直接查看(阅读、聆听等)生成的反事实媒体并评估其是否符合我们想象中的样子时,数学保证就不那么关键了。图 9.21 中的图像看起来像你想象中将正方形换成心形的样子吗?没有眼镜的哈里特·塔布曼海盗船长的形象是否符合你的预期?如果是这样,这个工具就非常有用,即使没有识别保证。在这里,效用是指与人类的反事实想象相一致,而不是与事实准确性相符。我有了我想要的塔布曼船长的概念图像,然后我可以继续我的下一个创意任务。
摘要
-
反事实推理算法需要一个 SCM,并涉及三个步骤:归纳、行动和预测。
-
在归纳步骤中,我们根据观察到的内生变量推断外生变量。
-
在行动步骤中,我们使用理想干预措施来实现反事实查询中的假设条件。
-
在预测步骤中,我们根据假设条件和在归纳步骤中学习到的外生变量分布,预测假设结果。
-
我们可以使用不同的概率机器学习框架来实现反事实推理算法。
-
我们可以使用像 pgmpy 这样的因果图模型库,在并行世界图上直接实现生成性结构因果模型(SCM),并使用图形模型推理算法和图手术来推断反事实查询。
-
我们可以使用现代概率深度学习技术,如变分推理和归一化流,来完成反事实推理算法的归纳步骤。
-
深度生成模型通常可以被修改以实现媒体的反事实生成(文本、图像、音频、视频等)。虽然可能存在识别问题,但通常可以检查生成的反事实工件并验证其是否符合你的预期。
第十章:识别和因果层次结构
本章涵盖
-
识别的激励示例
-
使用 y0 进行识别和推导估计值
-
如何在 y0 中推导出反事实图
-
推导基于图的反事实识别的 SWIGs
提高机器学习实践通常依赖于一种盲目的信心,即更多的数据和正确的架构可以解决任何任务。对于具有因果因素的任务,因果识别可以使这不再是信仰的问题,而是科学的问题。它可以告诉我们更多数据不会有所帮助,以及算法要正常工作需要哪些类型的归纳偏差。
因果识别是从纯粹观察数据或从观察或实验数据中做出反事实推断的任务。在统计学和数据科学中,它是允许我们从相关性中提炼因果关系并估计存在混杂因素时的因果效应的理论。但因果识别在人工智能领域也有应用。例如,假设一个深度学习算法在特定的因果推理基准上取得了高性能。因果识别背后的思想告诉我们,某些因果归纳偏差必须嵌入到模型架构、训练数据、训练过程、超参数(例如,提示)和/或基准数据中。通过追踪这些因果信息,我们可以确保算法在新场景中能够持续达到该基准性能。
识别是因果推理中理论性很强的部分。幸运的是,我们可以依赖库来为我们做理论上的繁重工作,并专注于使用这些库进行技能培养。在本章中,我们将重点关注一个名为 y0(发音为“why-not”)的库,该库使用图实现识别算法。到本章结束时,我们将揭开因果识别的神秘面纱,你将知道如何应用 y0 的识别算法。
10.1 因果层次结构
因果层次结构,也称为Pearl 的层次结构或因果阶梯,是我们询问的因果问题类型、构建的模型、获取的数据以及做出的因果推断的三层层次结构。
因果层次结构由三个层次组成:
-
关联
-
介入
-
反事实
当我们进行统计或因果分析时,我们正在这些三个层次中的一个进行推理。当我们知道我们在哪个层次进行推理时,我们可以确定我们需要依赖哪些假设和数据来进行正确的推理。
10.1.1 问题和查询在层次结构中的位置
我们对因果模型提出的问题,以及从这些问题中形式化的因果查询,位于层次结构的不同层次。首先,第 1 层(关联层)关注“是什么...?”的问题。让我们以在线游戏示例来说明,如图 10.1 所示。

图 10.1 在线游戏示例的 DAG
一个第 1 级问题及其相关查询的例子是
“对于高度参与支线任务的玩家,他们的游戏内购买金额是多少?” P ( I| E=“high”)
在这个级别上的推理旨在描述、建模或检测变量之间的依赖关系。在这个级别上,我们不是在推理变量之间的任何因果关系。
第 2 级(干预级)的问题涉及非反事实的假设条件,例如
“如果支线任务参与度高,玩家的游戏内购买会是什么?” P ( I [E] [=“high”])
在第 2 级,我们使用理想干预来形式化这些问题。请注意,从第 2 级查询派生出的任何查询也是第 2 级查询,例如 ATEs(例如,E(I[E][=“high”] – I[E][=“low”]))和 CATEs。
最后,反事实问题和查询位于第 3 级(反事实级):
“假设这个玩家有低支线任务参与度和低购买,如果他们更投入,他们的购买水平会是什么?” P( I [E] [=“high”]| E=“low”, I=“low”)
与第 2 级查询一样,我们从第 3 级查询派生出的任何查询也位于第 3 级。例如,一个旨在回答“为什么这个玩家的购买量低”的因果归因查询,如果它是第 3 级查询(如第 8.3 节中描述的因果概率)的函数,则它将是一个第 3 级查询。
在识别中,我们直接与查询打交道。Python 中的 y0 库为我们提供了一个用于表示查询的特定领域语言。以下代码实现了查询 P(I[E][=][e])。
列表 10.1 在 y0 中创建查询
!pip install git+https://github.com/y0-causal-inference/y0.git@v0.2.0
from y0.dsl import P, Variable #1
E = Variable("E") #2
I = Variable("I") #2
query = PE #3
query #4
1 “P”代表概率分布,“Variable”用于定义变量。
2 定义变量 G(公会成员资格)、E(支线任务参与度)和 I(游戏内购买)。
3 定义分布查询 P(I [E])。
4 如果在笔记本环境中运行,这将显示 P(I [E]) 的渲染图像。
设置你的环境
在本章中,我依赖于 y0 库的 0.2.0 版本。由于这是一个相对较新的库,库的 API 正在开发中,最新版本将与此处所示略有不同。查看库的教程以获取最新发展。
再次强调,我们依赖于 Graphviz 和一些自定义工具来绘制 DAG。Graphviz 的安装取决于你的环境。我使用 Ubuntu 22.04,通过 libgraphviz-dev 安装 Graphviz。然后我安装 Python 库 graphviz 版本 0.20.3 和 PyGraphviz 版本 1.13。Graphviz 代码仅用于绘图,所以如果你遇到困难,你可以跳过绘图而继续编写其余的代码。
query 对象是 Probability 类的一个对象。该类的 __repr__ 方法(它告诉 Python 当直接调用它时返回什么)实现为,当我们在一个 Jupyter 笔记本中的上一行代码的最后评估该对象时,它将显示渲染的 LaTeX(一种专注于数学符号的排版/标记语言),如图 10.2 所示。

图 10.2 当你在列表 10.1 中评估 query 对象时返回的渲染数学图像
因果层次结构适用于模型和数据。
10.1.2 模型和假设在层次结构中的位置
“模型”是一组关于数据生成过程(DGP)的假设。这些假设存在于层次结构的各个层级。
Level 1 假设
在关联层面的模型具有统计但非因果的假设。例如,假设我们感兴趣的是 P(I|E=e),对于 e 可能采取的任何值(“低”,“高”)。我们可能拟合一个线性模型来回归游戏内购买 I 对侧任务参与 E。或者我们可能训练一个将 E 映射到 I 的神经网络。这些都是两个参数化不同的统计模型。换句话说,它们在非因果、统计假设上对 P(I|E) 的放置上有所不同。一旦我们添加因果假设,我们就进入了层次结构的高层。
Level 2 假设
我们可以用因果有向图(DAG)表示的假设是第二层(介入)假设。一个第二层模型的例子是因果图模型(也称为因果贝叶斯网络)——一个在因果有向图上训练的概率模型。一个因果有向图本身就是一个第二层假设集;关于什么导致什么的假设。一般来说,允许你推断干预措施后果的假设是第二层假设。
Level 3 假设
Level 3 模型的典型例子是结构因果模型。但更普遍地说,关于机制——变量如何相互影响——的假设是第三层(反事实)假设。
考虑到这一点的一种方式是,任何你无法在 DAG 结构中表示的因果假设,通过排除法,就是一个第三层假设。例如,假设你的 DAG 有边 X→Y。进一步地,你相信 X 和 Y 之间的因果关系是自然线性的。你无法在 DAG 结构上“看到”线性,因此线性是一个第三层假设。
10.1.3 数据在层次结构中的位置
回顾观察数据和介入数据之间的差异。观察数据是被被动观察的;因此,它捕捉了 DGP 中变量之间依赖关系产生的统计关联。
Level 1 数据
在我们的在线游戏示例中,第一层数据是数据库查询拉取的侧任务参与和游戏内购买日志示例。观察数据位于因果层次结构的第一层。
Level 2 数据
介入数据是在应用介入措施的结果下生成的,例如从随机实验中收集的数据。在游戏示例中,这是由于 A/B 测试随机分配玩家到不同的组,他们被强制参与不同固定侧任务参与水平所创建的数据。介入数据位于层次结构的第二层。
Level 3 数据
反事实数据,位于层次结构的第 3 级,是一个特殊情况。反事实数据将包含来自可能世界的跨世界的数据。在大多数领域,我们只有一个世界的数据——数据中每个观察单位的潜在结果。
然而,存在一些特殊情况,其中存在反事实数据。例如,云服务提供商在给定各种约束的情况下,使用复杂但确定性的策略在云中分配资源。以日志中给定的一个分配结果为例,我们可以通过对该示例应用不同的分配策略来生成该示例的反事实结果。同样,给定由仿真软件产生的数据,我们可以通过改变仿真以反映一个假设条件并使用与原始数据相同的初始条件重新运行它来生成反事实数据。
10.1.4 因果层次定理
因果层次为我们提供了来自称为因果层次定理的东西的关键见解。这个见解是这样的:“没有第 k 级别的假设,你不能回答第 k 级别的问题。”例如,如果你想得到因果效应,你需要 DAG 或其他第 2 级(或第 3 级)假设。如果你想回答反事实问题,你需要第 3 级假设。即使是最前沿的深度学习模型,如果不能编码第 k 级别的假设,也无法可靠地回答第 k 级别的问题。
更正式地说,因果层次定理确立了因果层次的三层在数学术语中是“几乎总是分开的”。粗略地说,“分开”意味着来自层次较低级别的数据不足以从较高级别的层次推断查询。而“几乎总是”意味着这个陈述是正确的,除非在极其罕见的情况下,我们可以将其视为实际上并不重要。
除了这个见解之外,因果层次使得理解识别——可能是所有因果推理中最难的主题——变得容易得多,正如我们在本章的其余部分将看到的那样。
10.2 识别和因果推理流程
在本节中,我们将探讨提出和回答因果问题的流程以及识别在该流程中的作用。我们将使用第七章中引入的在线游戏 DAG 作为例子。让我们首先使用 y0 构建 DAG。
列表 10.2 在 y0 中构建在线游戏 DAG
import requests #1
def download_code(url): #1
response = requests.get(url) #1
if response.status_code == 200: #1
code_content = response.text #1
print("Code fetched successfully.") #1
return code_content #1
else: #1
print("Failed to fetch code.") #1
return None #1
url = ( #1
"https://raw.githubusercontent.com/altdeep/" #1
"causalML/master/book/chapter%2010/id_utilities.py" #1
) #1
utilities_code = download_code(url) #1
print(utilities_code) #2
# After checking, uncomment the exec call to load utilities
#exec(utilities_code) #2
from y0.graph import NxMixedGraph as Y0Graph #3
from y0.dsl import P, Variable #4
G = Variable("G") #4
E = Variable("E") #4
I = Variable("I") #4
dag = Y0Graph.from_edges( #4
directed=[ #4
(G, E), #4
(G, I), #4
(E, I) #4
] #4
) #4
gv_draw(dag) #5
1 安装 Graphviz 以进行 DAG 可视化。下载一些识别和可视化的辅助函数,将一些 y0 抽象转换为我们所熟悉的抽象。
2 在执行之前检查下载的代码,这是一项良好的安全实践。然后取消注释最后一行并执行。
3 y0 与一个名为 NxMixedGraph 的自定义图类一起工作。为了避免混淆,我们将称之为 Y0Graph 并用它来实现 DAG。
4 构建图形。
5 使用 Graphviz 辅助函数绘制图形。
这产生了图 10.3 中的图形。

图 10.3 绘制带有 y0 的在线游戏图
在第七章中,我们的目标是使用我们的 P(G, E, I) 模型通过干预操作从 P(I[E][=“high”]) 中进行模拟。在第 7.1.6 和 7.2.6 节中,我们进行了这种模拟,并看到了对于这个在线游戏示例的实证证据,证明它是有效的。识别意味着基于你的模型和假设,证明它在一般情况下是有效的。正式来说,我们想确保第一层分布 P(G, E, I),或者来自该分布的数据,结合我们的 DAG,
就足以从第二层分布 P(I[E][=“high”]) 中进行模拟。y0 的识别确认这确实可能。
列表 10.3 检查 P(I**[E][=“high”]) 从 P(G, E,I) 的识别
e = E #1
check_identifiable( #2
dag, #2
query=P(I @ e), #2
distribution=P(G, E, I) #2
) #2
1 用小写“e”表示干预值。
2 检查给定 DAG、分布和目标查询的可识别性。Y0 代表理想的干预措施,所以我们写作 P(I [E=e] 作为 P(I @ e)。
这将返回True,但如果我们没有任何关于行会成员 G 的观察呢?我们可以使用 y0 来测试我们是否从 P(E, I) 中获得了对 P(I[E][=“high”]) 的识别。换句话说,测试是否可以从对 E 和 I 的观察中推断出 P(I[E][=“high”])。
列表 10.4 检查 P(I**[E][=“high”]) 从 P(E,I) 的识别
check_identifiable(
dag,
query=P(I @ e),
distribution=P(E, I)
)
这将返回False,因为我们没有从 DAG 和基于我们的图形假设的 P(E, I) 中对 P(I[E][=][e]) 进行识别。
识别不足和误导性的概率机器学习
Y0 显示,根据我们的在线游戏 DAG,从 P(E, I) 中无法识别 P(I**[E][=][e])。从概率机器学习 (ML) 的角度来看,这个结果有什么含义。作为概率机器学习的专家,如果 G 是未测量的,我们可能会倾向于在 P(E, I) 上训练一个潜在变量模型,其中 G 是潜在变量。一旦我们学习了该模型,我们就可以通过图手术设置 E=e 来实施干预,然后从转换后的模型中采样 I。
这个算法将运行;它会生成样本。但 y0 的识别不足结果证明,仅基于我们 DAG 中的假设,我们不能将这些样本视为来自 P(I**[E][=][e]) 的有效样本。并且更多的数据训练也不会有帮助。这个算法能够工作的唯一方式是,如果存在额外的因果假设,这些假设限制了推断,超出了 DAG 编码的假设。
在这个介绍的基础上,让我们定义识别。
10.2.1 定义识别
假设我随机选择一对数字,X 和 Y,并将它们相加得到 Z。然后,我告诉你 Z 的值,并要求你推断 X 和 Y 的值。你能做到吗?没有更多信息的话,不能。那么,如果我给你数百万个特征 Z 和标签 {X,Y} 的例子呢?你能训练一个深度学习模型从输入特征 Z 预测标签 {X,Y} 吗?再次,不能,至少在没有对可能值有强假设的情况下不能。如果我只给你十亿个例子呢?不;更多的数据不会有帮助。在统计学中,我们会说预测目标 {X,Y} 是没有识别的。
换句话说,你想要推断某事,并且你有一个算法(例如,深度网络)可以接收数据并生成答案。由于输入数据的统计变化,这个答案通常会与真实值略有不同。如果你的推断目标是确定的,那么你输入到算法中的数据越多,这种差异就会越小,你的算法答案就会收敛到真实答案。如果你的推断目标没有确定,那么更多的数据不会减少你的算法错误。
因果识别只是因果层次结构各层之间的统计识别。当你的因果假设使你能够使用层次结构较低级别的数据来推断该查询时,因果查询就被识别了。
10.2.2 因果推断工作流程
现在我们已经定义了识别,我们可以定义因果推断的完整工作流程。图 10.4 显示了完整的工作流程。

图 10.4 因果推断工作流程。识别步骤是工作流程中的关键步骤。
识别是工作流程中的关键步骤。让我们逐一走过每个步骤。
第一步:提出你的查询
首先,我们将我们的因果问题作为一个查询提出。例如,给定我们的问题“如果副任务参与度很高,玩家在游戏中的购买会是什么?”我们的查询是 P(I[E][=“high”])。

图 10.5 第二步:构建模型以捕捉相对于你的查询的因果假设。对于查询 P(I**[E][=“high”]),这是我们在线游戏的 DAG。
第二步:构建你的模型
接下来,构建一个因果模型,该模型捕捉你的基本因果假设。我们的模型将是在线游戏的因果 DAG,如图 10.5 所示。
你的模型假设至少应该与你在因果层次结构中的查询级别相匹配。例如,查询 P(I[E][=“high”]) 是一个 2 级查询,因此我们需要至少一些 2 级假设。因果 DAG 是一个 2 级因果模型,因此在我们的分析中,DAG 提供了必要的 2 级假设。
第三步:检查识别
评估你是否在你的查询中具有识别,基于你的模型假设和可用数据。如果你没有识别,你必须要么在你的数据中观察额外的变量,要么改变你的假设。例如,我们可以修改我们的在线游戏 DAG(改变二级假设)。或者简单地停止并得出结论,根据你的数据和关于问题的知识,你不能回答这个问题,并将你的注意力转移到其他地方。
第 4 步:估计你的查询
一旦你知道你的查询具有识别,你就可以对你的查询进行统计推断,或“估计”。有各种各样的估计方法和算法,从贝叶斯推断到线性回归、倾向得分到双重机器学习。我们将在下一章中回顾一些估计方法。
第 5 步:反驳你的因果推断
反驳是最终一步,我们在这里进行敏感性分析,以评估第 4 步的结果对假设违反的敏感性,包括使识别成为可能的假设。我们将在第十一章中看到这方面的例子。
10.2.3 分离识别和估计
在许多文本中,识别和估计在一步中结合,通过匹配估计量和那些估计量将有效的工作场景。在这本书中,我们将强调识别和估计的分离,原因有以下几点:
-
分离使我们能够将所有因果考虑因素都转移到识别步骤。这有助于我们明确我们依赖哪些因果假设来进行估计,并为我们的分析可能失败的情况建立直觉。
-
因此,估计步骤简化为纯粹统计问题,其中我们考虑通常的统计权衡(偏差与方差、不确定性量化、扩展性等)。
-
这种分离也使我们能够利用自动微分能力来处理估计,这些能力是前沿深度学习库的核心,而无需担心这些学习过程是否会错误地得出因果关系。
接下来,我们将深入探讨最常见的识别策略:后门调整。
10.3 使用后门调整进行识别
假设我们想要确定参与对游戏内购买的影响,即E(I[E][=][“high”] – I[E][=][“low”])。我们可以从这个查询E(I[E][=][e]=i)中推导出这个期望,所以我们关注P(I[E][=][e]=i)。我们可以使用在线游戏 DAG 来证明以下是真的:
我们将在下一节中看到如何推导出这个方程。该方程的右侧是一个称为估计量的一级量,我们可以从联合分布P(I, E, G)中推导出来。

查询、估计量和估计者
在统计学中,估计量是统计算法(估计器)估计的东西。识别的任务是找到(识别)查询的估计量。从因果层次结构的角度来看,因果识别是关于找到一个较低级别的估计量来满足较高级别的查询。
在在线游戏后门识别示例中,P(I[E=“high”]=i) 是一个二级查询,而 ∑[g]**P(I=i|E=“high”, G=g)P(G=g) 是一级估计量,称为后门调整估计量。后门调整是我们应用于 P(E, I, G) 的一个操作,其中我们消去(或在连续情况下积分出)共同原因 G。在某些情况下,我们会看到我们不需要明确知道估计量,只需要知道它存在。
我们将我们的 DAG 和干预级别的查询 P(I[E][=][“high”]) 传递给 y0,它告诉我们它识别了一个估计量,这是一个应用于 P(E, I, G) 的操作,与 P(I[E][=][“high”]) 等价。让我们让 y0 显示这个估计量。
列表 10.5 从 P(E, I, G) 推导出估计量以获得 P(I**[E][=“high”])
from y0.graph import NxMixedGraph as Y0Graph
from y0.dsl import P, Variable
from y0.algorithm.identify import Identification, identify
query = P(I @ e)
base_distribution = P(I, E, G)
identification_task = Identification.from_expression(
graph=dag,
query=query,
estimand=base_distribution)
identify(identification_task)
这返回了图 10.6 中的表达式。

图 10.6 y0 识别函数的输出
在我们的记号中,这表示为 ∑[g]P(I=i|E=“high”, G=g) ∑[ε][,][i]**P(E=ε, G=g, I=i), 这可以简化为 ∑[g]P(I=i|E=“high”, G=g) P(G=g). 这就是后门调整估计量。我们将从高层次看到 y0 如何推导出这个估计量。但首先,让我们更仔细地看看这个估计量。
10.3.1 后门调整公式
一般而言,假设 X 是 Y 的原因,并且我们对干预级别的查询 P(Y[X][=][x]) 感兴趣。在这种情况下,后门调整估计量是 ∑[g]**P(X=x, Z=z) P(Z=z)。后门调整公式将因果查询 P(X**[X][=][x]) 与其估计量等同起来:

在这里,Z 是一组被称为调整集的变量。求和是求和和积分的简写——你在调整集中的离散变量上求和,在连续变量上积分。调整集被定义为满足后门准则的一组变量——(1)该集共同分离了从 X 到 Y 的所有后门路径,并且(2)它不包含 X 的后代。
要理解为什么我们要分离 X 和 Y 之间的后门路径,再次考虑我们的在线游戏示例图 10.7 中的 DAG。

图 10.7 在线游戏 DAG
P(I|E=“high”)和P(I[E][=“high”])之间的区别是什么?考虑图 10.8 中E和I之间的两条路径。在P(I|E=“high”)的情况下,观察到E=“high”通过其对I的直接因果影响为我们提供了关于I的信息,即通过路径E→I。但观察到E=“high”也为我们提供了关于G的信息,随后通过后门路径E←G→I关于I的信息。两个变量之间的后门路径是共同原因之间的d-连接路径。在P(I**[E][=“high”])的情况下,我们只想通过直接路径E→I对I产生影响。

图 10.8 E←G→I是一个后门路径,其中G是E和I的共同原因的“混杂因素”。我们感兴趣的是沿着从E到I的因果路径流动的统计信号,但这个信号被后门路径E←G→I上的额外统计信息的非因果噪声所“混杂”。
我们称G为混杂因素,因为沿着从E到I的因果路径流动的统计“信号”被来自G的额外统计信息的非因果“噪声”所“混杂”,这些信息通过后门路径E←G→I传递。为了解决这个问题,我们试图通过在G上阻断来 d-分离这条后门路径。
我们想要识别查询P(I[E][=“high”])的后门估计量。因此,我们在后门调整公式中将I替换为Y,将E替换为X。G阻断后门路径E G I,因此集合G成为我们的调整集:
P(I[E][=][e]=i) = ∑[g]P(I=i|E=e, G=g)P(G=g)
后门调整公式通过求和/积分或换句话说,“调整”后门统计信号来 d-分离后门路径,只留下来自直接因果关系的信号。
备注:一些文献中提到的是 G 公式而不是后门调整公式。后门调整公式就是 G 公式,其中调整集是以后门标准来定义的。
虽然调整集可以包括非混杂因素,但在实践中,排除除最小化后门阻断混杂因素之外的所有因素可以减少复杂性和统计变异。我们在第十一章深入探讨了后门调整的统计考虑因素。
10.3.2 揭秘后门
因此,后门调整估计量从何而来?让我们再次考虑我们的在线游戏示例。查询是P(I[E][=][e]),其中e是“高”或“低”。在反事实的术语中,让我们考虑两个可能的世界,一个是我们原始的 DAG,另一个是我们对副任务参与度(E)进行干预。让我们查看图 10.9 中的平行世界图。

图 10.9 我们有两个平行世界:世界 A 中E没有受到干预,世界 B 中E受到了干预。
如果你足够用力地眯着眼睛看这张图,你会注意到它暗示了在给定 G 的条件下,E 是与 I[E][=][e] 条件独立的。我们将使用一些基于 d-separation 的推理来观察这一点。记住,在一般情况下,我们不能在平行世界图上使用 d-separation 来进行跨世界的推理,因为 d-separation 规则没有考虑到在各个世界之间等效的节点(如 G)。但我们将使用一个技巧,通过观察世界 A 中从 E 到 G 的 d-connected 路径,然后扩展这个 d-connected 路径 从 世界 B 中等效的 G 到 I[E][=][e]。
首先,考虑从世界 A 中的 E 到世界 B 的路径必须穿过两个世界之间的桥梁之一,N[G] 和 N[I]。但是,由于 I 上的碰撞器,到 N[I] 的两条路径(E → I ← N[I],E ← G → I ← N[I])都是 d-separated 的。
因此,我们有一条到世界 B 的 d-connected 路径(E ← G ← N[G])。现在假设我们观察世界 B 中的 G;从世界 B 的 G 出发,到 I[E][=][e] 只是一步之遥。但我们知道,根据一致性法则,两个世界中的 G 的值必须相同;两个 G 都是 N[G] 的相同确定性函数,并且两个 G 都不受干预的影响。因此,为了方便起见,我们将两个 G 在平行世界图中折叠成一个节点(图 10.10)。现在观察路径 E ← G → I[E][=][e],我们可以看到这条路径被 G d-separated。因此,我们可以得出结论 E ⊥ I[E][=][e] | G。

图 10.10 在各个世界之间折叠 G 揭示了 G d-separates E 和 I**[E][=][e]。
在因果推理术语中,这种简化被称为 不可知性。不可知性意味着因果变量 E 和像 I[E][=][e] 这样的反事实潜在结果在混杂因素给定的情况下是条件独立的。不可知性是因果推理中常见的假设。我们可以在推导后门估计量时使用这个不可知性假设。
在我们开始之前,让我们回顾一个关于条件独立的关键定义事实:如果两个变量 U 和 V 在给定 Z 的条件下是条件独立的,那么 P(U|Z=z, V=v) = P(U|Z=z)。反过来,P(U|Z=z) = P(U|Z=z, V=v)。换句话说,P(U|Z=z) = P(U|Z=z, V=“apples”) = P(U|Z=z, V=“oranges”);V 的值无关紧要,因为 Z 使其与 U 独立,其值对 U 没有影响。引入 V 并赋予它我们想要的任何值是使推导工作起来的技巧。此外,回顾一下全概率定律说,我们可以通过对该变量求和(或积分)来从联合分布中消去一个变量,例如 P(U=u) = ∑[v]P(U=u, V=v)。当联合分布受到干预时,也是如此,例如 P(U**[W][=][w]=u) = ∑[v]P(U**[W][=][w]=u, V**[W][=][w]=v)。
现在我们从因果查询 P(I[E][=][e]) 开始,看看它是如何与后门估计量 ∑[g] P(I|E=e, G=g)P(G=g) 相等的。
-
对于游戏内购买的某个值 i,根据全概率定律,P(I[E][=][e]=i) = ∑[g] P(I[E][=][e]=i, G[E][=][e]=g)。
-
∑[g] P(I[E][=][e]=i, G[E][=][e]=g) = ∑[g] P(I[E][=][e]=i, G=g),因为我们知道从我们的原始 DAG 中,G 不受对 E 的干预的影响。
-
接下来,我们使用链式法则对 P(I[E][=][e]=i, G=g) 进行因式分解:∑[g] P(I[E][=][e]=i, G=g) = ∑[g] P(I[E][=][e]=i| G=g)P(G=g)。
-
现在我们来到了一个技巧——对于任何 e 的值,P(I[E][=][e]=i|G=g) = P(I[E][=][e]=i|E=e, G=g),因为一旦我们条件化 G=g,E=e 和 I[E][=][e] 就是独立的。因此,在我们的推导中,我们可以用 P(I[E][=][e]=i|E=e, G=g) 替换 P(I[E][=][e]=i|G=g)。
-
一旦我们条件化 E=e,我们就可以使用一致性定律取消下标:∑[g] P(I[E][=][e]=i|E=e, G=g)P(G=g) = ∑[g] P(I=i|E=e, G=g)P(G=g)。
让我们解释步骤 4 和 5。我们的不可知性结果表明,在给定 G 的条件下,I[E][=][e] 和 E 是条件独立的。因此,在第 4 步中,我们应用了独立性技巧,使我们能够引入 E。进一步地,我们将 E 的值设为 e,以便与下标 [E][=][e] 匹配。这使我们能够应用第八章中的一致性定律,并取消下标 [E][=][e]。
哇,我们已经确定了一个后门估计量,这是一个来自因果层次结构第 1 层的估计量,用于第 2 层因果查询 P(I[E][=][e]),它使用在 DAG 中编码的第 2 层假设。因果识别只是提出这样的推导。如果不是大部分,那么传统因果推理研究的大部分都归结为做这种数学运算,或者编写为你做这种运算的算法。
接下来,我们将探讨 do-calculus,它提供了一套基于图的简单规则,用于识别,我们可以在识别算法中使用这些规则。
10.4 使用 do-calculus 进行图形识别
图形识别(有时称为非参数识别)指的是依赖于对 DAG 进行推理的识别技术。图形识别最著名的途径之一是do-calculus,这是一套用于因果图识别的三条规则。这些规则使用图手术和 d-separation 来确定何时可以用非因果术语如I[E][=][e]替换因果术语。从因果层次结构较高级别的查询开始,我们可以按顺序应用这些规则来推导出较低级别的估计量。
10.4.1 揭秘 do-calculus
回想一下高中几何,你看到了这样的 if-then 语句:
如果形状是正方形,那么所有边都相等。
当你试图解决一个几何问题时,你会在你的解决方案步骤中使用这样的事实。
同样,do-calculus 由三个规则(if-then 语句)组成,形式如下:
如果某些变量在应用图手术后的 DAG 中 d-separation,那么概率查询A等于概率查询B。
do-calculus 的规则并不直观
do-calculus 的三个规则在阅读时并不直观,就像你在高中第一次看到像 cos²x + sin²x = 1 这样的几何规则时并不直观一样。但就像那些几何规则一样,我们是从更简单、更熟悉的概念中推导出 do-calculus 的规则,即 d-separation、理想干预和概率规则。而且就像几何规则一样,我们可以使用 do-calculus 的规则来证明从层次结构的一个级别发出的因果查询与另一个级别的查询是等价的。
实际上,我们可以依赖实现图形识别算法中的 do-calculus 的软件库(如 y0)或简单地硬编码已知的识别结果,如后门调整估计量。为了消除一些神秘感,我将介绍这些规则并展示它们如何推导出后门估计量。这里的目的是不是记住这些规则,而是要看看它们在推导后门估计量时的作用,这与上一节的推导形成对比。
在定义这些规则时,我们将关注在X上的干预下的目标分布Y。我们希望推广到所有 DAG,所以我们将命名另外两个节点,Z和W。Z和W将使我们能够涵盖我们还有另一个潜在干预目标Z和任何我们想要条件化的节点W的情况。此外,虽然我经常提到单个变量,但请记住,当X、Y、Z和W是变量集时,这些规则适用。
规则 1:观察值的插入或删除
如果 Y 和 Z 在你的 DAG 中通过 X 和 W 分隔,在移除 X 的进入边之后……
那么 P( Y [X][=][x]= y | Z = z,W = w)= P( Y [X][=][x] = y | W = w)。
这被称为“插入或移除”,因为我们可以从 P( Y [X][=][x]=y|Z=z,W=w)中移除 Z=z 来得到 P( Y [X][=][x]=y | W=w),反之亦然。
规则 2:交换干预和观察
如果 Y 和 Z 在你的 DAG 中通过 X 和 W 分隔,在 X 的进入边和 Z 的出去边被移除之后……
然后 P( Y [X] [=] [x] [,] [Z] [=] [z]= y | W = w)= P( Y [X] [=] [x] = y | Z = z,W = w)。
在这里,我们可以在 P( Y [X][=][x][,] [Z][=][z]=y | W=w)中 交换 干预 [Z][=][z] 以观察 Z=z 为条件,得到 P( Y [X][=][x]=y | Z=z,W=w),反之亦然。
规则 3:插入或移除干预
对于规则 3,我们将定义 Z 为一组节点,Z(W)为不是 W 的祖先的 Z 的子集。
如果你移除了 X 和 Z( W)的所有进入边后,Y 和 Z 在你的 DAG 中通过 X 和 W 分隔……
然后 P( Y [X][=][x] [,] [Z][=][z] = y | W = w)= P( Y [X][=][x] = y | W = w)。
这条规则允许你将 [Z][=][z] 插入到 P( Y [X][=][x]=y | W=w)中,得到 P( Y [X][=][x][,] [Z][=][z]=y | W=w),或者从 P( Y [X][=][x][,] [Z][=][z]=y | W=w)中移除 [Z][=][z],得到 P( Y [X][=][x]=y | W=w)。
10.4.2 使用 do-calculus 进行后门识别
现在我们将使用 do-calculus 来提供对后门估计量的另一种推导,这种推导与基于“不可知性”的定义不同。同样,我包括这个推导是为了阐明 do-calculus 的应用。如果你没有完全理解每一步,请不要担心:
-
P(I[E][=][e]=i)= ∑[g] P(I[E][=][e]=i,G[E][=][e]=g)根据全概率定律。
-
∑[g] P(I[E][=][e]=i,G[E][=][e]=g)= ∑[g] P(I[E][=][e][,] [G][=][g]=i)P(G[E][=][e][,] [I][=][i]=g)通过 c-component 分解。
-
P(I[E][=][e][,] [G][=][g]=i)= P(I=i|E=e,G=g)根据 do-calculus 的第 2 条规则。
-
P(G[E][=][e][,] [I][=][i]=g)= P(G=g)根据 do-calculus 的第 3 条规则。
-
因此,P(I[E][=][e]=i)= ∑[g] P(I=i|E=e,G=g)P(G=g)通过将 3 和 4 代入 2。
do-calculus 规则在步骤 3 和 4 中应用。
注意:步骤 2 使用了一种称为c-component factorization的分解规则。c-component(混淆组件)是 DAG 中一组节点,其中每个可观察节点的成对节点通过一条路径相连,该路径的边始终指向可观察节点,而不是远离它们(这些是第四章中提到的“孤儿表亲”)。观察变量的联合概率可以被分解为 c-components,这一事实使得步骤 2 成为可能。在识别算法中,对 c-components 进行分解是常见的。请参阅章节注释中的参考文献www.altdeep.ai/p/causalaibook。
这种基于 do-calculus 的推导比我们基于“不可知性”的推导要直观得多。我们为此直觉交换了两个优势。首先,do-calculus 是完整的,这意味着如果一个查询仅使用图形假设就有可识别的估计量,那么它可以使用 do-calculus 推导出来。其次,我们有利用 do-calculus 来自动化图形识别的算法。
10.5 图形识别算法
图形识别算法,通常称为ID 算法,自动化了基于图的应用识别系统,如 do-calculus。当我们使用 y0 检查P(I[E][=][e])的识别并推导后门估计量时,它正在使用其图形识别算法的实现。在本节中,我们将看到如何使用这些算法来识别另一个有用的估计量,称为前门估计量。
10.5.1 案例研究:前门估计量
在我们的在线游戏示例中,假设我们无法观察到公会成员资格。那么我们就无法进行P(I[E][=][e])的后门识别。然而,假设我们在侧任务参与(E)和游戏内购买(I)之间有一个中介——一个在E和I之间的图节点。具体来说,我们的中介代表赢得的物品(W),如图 10.11 所示。

图 10.11 侧任务参与导致赢得像这种魔法弓这样的虚拟物品。赢得的物品会推动更多游戏内购买,例如魔法弓的魔法箭,因此我们在侧任务参与和游戏内购买之间的因果路径上引入了一个中介“赢得的物品”。
赢得的物品的想法如下。当玩家成功完成一个侧任务时,他们会赢得一个虚拟物品。他们完成的侧任务越多,他们获得的物品就越多。那些赢得的虚拟物品和购买的虚拟物品可以相互补充。例如,赢得一个魔法弓会促使购买魔法箭。因此,玩家拥有的赢得物品的数量会影响他们购买的虚拟物品的数量。
给定这个图,我们可以使用 y0 的图形识别算法实现来推导前门估计量。
列表 10.6 在 y0 中推导前门估计量
from y0.graph import NxMixedGraph as Y0Graph
from y0.dsl import P, Variable
G = Variable("G") #1
E = Variable("E") #1
I = Variable("I") #1
W = Variable("W") #1
e = E #1
dag = Y0Graph.from_edges( #1
directed=[ #1
(G, E), #1
(G, I), #1
(E, W), #1
(W, I) #1
] #1
) #1
query=P(I @ e) #2
base_distribution = P(I, E, W) #3
identification_task = Identification.from_expression( #4
graph=dag, #4
query=query, #4
estimand=base_distribution) #4
identify(identification_task) #4
1 使用中介变量构建一个新的图。
2 仍然是列表 10.5 中的相同查询,P(I_{E=e})
3 但现在我们观察到 I,E 和 W
4 最后,我们检查在 DAG 和观测分布的条件下查询是否被识别。
这段代码将返回图 10.12 的输出。

图 10.12 Y0 将识别的数学图作为输出。
重新排列输出,按照我们的记号,这是结果:

如前所述简化,我们得到前门估计量:

注意,有一个关于W的外层求和和一个关于所有E值的内层求和(每个E值用ε表示,与干预值e不同)。
10.5.2 揭示前门之谜
与后门估计量类似,前门估计量的 do-calculus 推导涉及到使用规则 2 和 3 的重复替换。前门估计量的粗略直觉是,侧面任务参与和游戏内购买的统计关联来自直接的因果路径以及通过后门混杂因素公会成员资格(G)的路径。前门估计量使用中介变量来确定这种关联中有多少是由于直接的因果路径;中介变量充当通过该直接因果路径的统计信息流的测量器。
估计量的一个关键好处是,它不需要观察一组阻止所有可能后门路径的混杂因素。当你有许多混杂因素、无法调整由于潜在混杂因素,或者担心可能存在一些未知的混杂因素时,避免后门调整是有用的。
接下来,我们将探讨如何识别反事实。
10.6 通用反事实识别
因果 DAG 是一个 2 级建模假设。因果层次定理告诉我们,在一般情况下,图本身不足以识别 3 级反事实查询。要从 1 级或 2 级分布中进行反事实识别,你需要 3 级假设。简单来说,3 级假设是任何你不能用简单的因果 DAG 表示的因果假设。
在第九章中,我介绍了反事实推理的通用算法。该算法需要一个结构因果模型(SCM),这是一个 3 级模型;它封装了 3 级假设。有了 SCM,算法可以推断出在其基础变量上可以定义的所有反事实查询。这种能力的代价是,SCM 必须封装回答所有这些查询所需的所有假设。其中许多假设不能用 1 级或 2 级数据进行验证。
你做出的假设越多,你的推断就越容易受到这些假设违反的影响。因此,我们寻求识别技术,这些技术针对特定的反事实查询(而不是每个反事实查询)使用尽可能少的 3 级假设。
10.6.1 通用反事实推理算法的问题
当我们将通用反事实推理算法应用于两个类似的 SCM 时,我们可以看到问题。假设有一个与您用于运行算法的 SCM 不同的真实 SCM。假设这两个 SCM 具有完全相同的底层 DAG 和相同的观察数据和实验数据的统计拟合;换句话说,SCM 为所有一级和二级查询提供相同的推理。您的 SCM 仍然可能相对于真实 SCM 产生不同的(不准确的)反事实推理。
为了理解原因,回顾第六章中的木棍断裂示例。我提出了两个相似但不同的 SCM。这是第一个:

这是第二个:

图 10.13 可视化了对这些模型进行单值采样。

图 10.13 两个不同的 SCM 编码了完全相同的观察和干预分布,但给定相同的外生变量值,在每个模型中,您可以得到对应内生变量的两个不同值。
图 10.13 展示了当给定n[y] = .15 的值时,木棍在.15 米处断裂,但第一根木棍将在区域 2 断裂,返回值为 2,而第二根木棍将在区域 3 断裂,返回值为 3。由于它们在第三级假设上的不同,即它们如何处理输入,它们在相同的随机输入下产生不同的结果。
因此,当我们朝相反方向进行,并在通用反事实推理算法中应用归纳步骤时,我们可以得到这些模型的不同结果。对于内生变量的给定值,我们可以得到外生变量的不同后验分布。
图 10.14 说明了这两个模型在观察到结果为 3 时,如何对N[y]产生不同的推理。对于第一个 SCM,y=3 的值意味着P(N[y]|Y=3)是在(p[x][1] + p[x][2])到 1 范围内的连续均匀分布,而对于第二个 SCM,它是在 0 到p[x][3]范围内的连续均匀分布。这些不同的P(N[y]|Y=3)分布会导致反事实推理算法产生不同的结果。现在假设 SCM 2 是正确的,而 SCM 1 是错误的。如果我们选择 SCM 1,我们的反事实推理将是不准确的。

图 10.14 尽管两个 SCM 编码了相同的观察和干预分布集,但在归纳步骤中,它们会对Y=3 给定N[y]产生两个不同的后验。因此,它们编码了不同的反事实分布,可以产生不同的反事实推理。
一般情况更难;可能有多个 SCM 包含相同的第 1 级和第 2 级假设,但具有不同的第 3 级假设。例如,您可能通过使用基于深度神经网络的方法从第 1 级和第 2 级数据中学习深度 SCM 来学习这些 SCM 之一。但是,深度 SCM 可能不是您想要进行反事实推断的正确SCM。
如果您对基础真实 SCM 有信心,反事实推断的一般算法是理想的。但在您不确定的情况下,您可以转向反事实识别,在那里您指定一个最小的第三级假设集合,使您能够识别一个目标反事实查询。
10.6.2 示例:单调性和因果概率
单调性是强大第三级假设的一个例子。单调性是一个简单的假设,即原因 X 和结果 Y 之间的关系是单调的:E(Y|X=x) 当 x 增加时要么从不增加要么从不减少。请注意,线性是单调性的一个特例。
单调性和非单调性的直观例子在药物剂量中。在单调剂量反应关系中,服用更多的药物要么有帮助要么没有帮助。在非单调剂量反应关系中,服用药物可能在正常剂量下有帮助,但过量服用可能会使问题变得更糟。单调性通过消除反事实可能性来帮助识别;如果剂量反应关系是单调的,当你想象如果你服用更强的剂量会发生什么时,你可以消除你变得更糟的可能性。
回想第八章中我们看到的因果概率:
-
必要概率(PN):P(Y[X][=][0]=0|X=1, Y=1)
-
足够概率(PS):P(Y[X][=1]=1|X=0, Y=0)
-
必要性和充分性概率(PNS):P(Y[X][=1]=1, Y[X][=0]=0)
在单调性的假设下,我们可以识别以下关于因果概率的第二级估计量。
-
PN = (P(Y=1) – P(Y[X][=0]=1))/P(X=1, Y=1)
-
PS = (P(Y[X][=1]=1) – P(Y=1))/P(X=0, Y=0)
-
PNS = P(Y[X][=1]=1) – P(Y[X][=0]=1)
我们可以从第二级数据中估计这些第二级估计量,例如一个随机实验。当然,如果我们只有观察数据,我们可以使用后门或前门调整或另一种识别策略,从那些数据中推断出 P(Y[X][=0]=1) 和 P(Y[X][=1]=1)。
我们可以再次手动推导这些估计量,但相反,让我们考虑通过消除反事实可能性来使这种识别单调性得以实现的识别。为了看到这一点,考虑第八章中的提升建模问题。在那里,X 是我们是否发送了促销,而 Y 是客户是否继续作为付费订阅者(Y=1)或“流失”(取消订阅;Y=0)。我们按以下方式对订阅者进行细分:
-
可说服者—当你发送促销时,留下可能性增加的订阅者
-
确定事件—无论是否发送促销,都有很大可能性留下的订阅者
-
无望的原因—无论是否发送促销,留下可能性很低的订阅者
-
沉睡的狗:当你发送促销时,留下可能性下降的订阅者
如果你假设单调性,你是在假设发送促销要么不起作用,要么会增加留下的可能性。它假设没有用户会对促销做出不良反应。换句话说,假设单调性意味着你假设没有沉睡的狗。
现在让我们考虑这如何缩小范围。假设你有一个以下问题:
我未能向一位客户发送促销,他们流失了。如果我发送了促销,他们会留下吗?P(Y[X][=1]=1|X = 0, Y = 0)
这个反事实查询是充分性的概率。我们想知道发送促销是否会增加他们留下的可能性。思考这个问题,
-
如果客户是可说服的,发送促销会增加他们留下的可能性。
-
如果客户是无望的原因,发送促销将不会有任何效果。
-
如果客户是沉睡的狗,发送促销会使他们留下可能性更低。
如果这位客户既是可说服的,又是沉睡的狗,那么很难确定我们是否应该发送促销,在一个情况下促销会有所帮助,而在另一个情况下它会使客户流失更加确定。但如果我们假设单调性,我们就排除了他们是沉睡的狗的可能性,并可以得出结论,发送促销会有所帮助,或者至少不会损害他们留下的机会。
贝叶斯建模和反事实识别
尽管图形识别算法可以处理一些反事实查询,但我们没有反事实识别的通用算法。但鉴于我们关注概率机器学习工具,我们可以转向贝叶斯建模作为前进的道路。
识别本质上关于不确定性。例如,在反事实情况下,缺乏识别意味着即使有无限的一级和二级数据,你也不能确定三级查询的真实值。从贝叶斯的角度来看,我们可以使用概率来处理这种不确定性。
假设你有一组因果假设,包括非图形假设,以及一些一级和二级数据。你可以采取以下贝叶斯方法来测试你的假设和数据是否足以识别你的反事实查询:
-
指定一组既多样化又与你的因果假设一致的 SCM(结构因果模型)。
-
在这个集合上放置一个先验分布,使得更合理的模型获得更高的先验概率值。
-
在给定观测(第 1 级)和干预(第 2 级)数据的情况下,获得 SCM 的后验分布。
-
从后验分布中抽取 SCM 样本,并对每个样本 SCM,你应用针对特定反事实查询的通用反事实推理算法。
结果将构成关于这一反事实推理的后验分布。如果你的因果假设和你的数据足以识别反事实查询,随着你数据的增加,反事实推理的后验将收敛到真实值。(成功的收敛假设了贝叶斯估计的典型“规律性”条件。结果将取决于先验的质量。)但即使它没有收敛到真实值,你的假设仍然可能使收敛到一个足够小以有用的真实值附近区域(这被称为部分识别,如第 10.9 节所述)。
Pyro 库及其因果性扩展 ChiRho,使得以这种方式结合贝叶斯和因果思想成为可能。
从二元动作(如发送或不发送促销)到多动作(如决策或强化学习问题)的单调性推广是通用的,有关参考文献请参阅www.altdeep.ai/p/causalaibook。
10.7 图形反事实识别
一个传统的因果 DAG 仅编码第 2 级假设,但存在推理反事实的图形技术。图形反事实推理仅在特殊情况下有效,但这些情况非常实用。此外,使用图形使我们能够通过算法自动化识别。为了说明图形反事实识别,我们将引入一个新的案例研究。
当你打开 Netflix 时,你会看到 Netflix 仪表板,它显示了多种推荐内容形式。其中两种是“为你精选”,这是 Netflix 算法根据你的过去观看行为和评分预测你将喜欢的个性化节目和电影选择,以及“因为你看了”,它根据你最近观看的内容推荐内容。该系统的模型包括以下变量:
-
T—一个用于推荐策略的变量,该策略选择订阅者的“为你精选”内容。为了简单起见,我们将考虑一个当前在生产中的策略,“+t”。我们将使用“–t”,表示“非t”,来表示替代策略。
-
B—一个用于推荐策略的变量,该策略选择订阅者的“因为你看了”内容。同样,我们将简化为具有策略“+b”的二进制变量,代表生产中的策略,以及所有替代策略“–b”,如“非b”。
-
V—订阅者与“因为你看了”推荐内容互动的参与度。
-
W—订阅者与“为你精选”推荐内容互动的参与度。
-
A—流失,意味着订阅者最终是否离开 Netflix。
-
C—订阅者背景,意味着我们正在处理的订阅者类型(位置、人口统计、偏好等)。
推荐算法始终考虑订阅者的概况以及观看历史,因此订阅者概况 C 是推荐政策变量 T 和 B 的原因。
在本节中,我们将使用 y0 在层次结构的各个级别分析这个问题。我们将首先可视化图形。
列表 10.7 绘制推荐 DAG
T = Variable("T") #1
W = Variable("W") #1
B = Variable("B") #1
V = Variable("V") #1
C = Variable("C") #1
A = Variable("A") #1
t, a, w, v, b = T, A, W, V, B #1
dag = Y0Graph.from_edges(directed=[ #2
(T, W), #2
(W, A), #2
(B, V), #2
(V, A), #2
(C, T), #2
(C, A), #2
(C, B) #2
]) #2
gv_draw(dag) #3
1 为模型定义变量。
2 创建图。
3 绘制图。

图 10.15 推荐算法问题的因果 DAG
这生成了图 10.15 中的 DAG。
作为初步调查,您可能需要查看“为您精选”内容对流失率 E(A[T][=+][t] – A[T][=–][t]) 的平均处理效果(ATE,二级查询)。鉴于流失率 A 具有二元结果,我们可以将其表示为 P(A[T][=+][t]=+a) – P(A[T][=–][t]=+a)。专注于 P(A[T][=–][t]=+a),我们立即知道我们可以通过(二级)后门和前门来识别这一点。因此,让我们继续探讨一个有趣的(三级)反事实查询,称为治疗对接受治疗者的效果(ETT)。
10.7.1 治疗对接受治疗者的效果
记住,您可以直接从随机实验中获得 ATE(无需识别和估计一级估计量)。假设您在一组用户上运行了这样的实验,并显示了一个有利的 ATE,例如,+t相对于-t对W和A有积极的影响。因此,您的团队部署了该政策。
假设+t政策对观看大量电影并因此拥有更多观看数据的用户效果最好。因此,当政策部署到生产环境中时,这类用户更有可能被分配该政策。但由于他们高度参与,无论他们被分配+t或-t政策,他们都不太可能离开。我们可能会出现这样的情况:在随机分配政策且不考虑参与程度的实验中,+t政策看起来有效,但在生产中分配是有偏见的,偏向于高度参与但对政策漠不关心的用户。
解决这个问题的 3 级查询是 ATE 的反事实版本,称为处理对处理的效果(ETT,有时称为 ATT,即“对处理者的平均处理效果”)。我们将其写成反事实查询 E(A[T][=+][t] – A[T][=–][t]|T=+t),即“对于看到政策+t的人,他们相对于看到-t的情况,有多少更多的流失?”像我们对 ATE 所做的那样,对于二进制 A 进行分解,我们可以将其写成 P(A[T][=+][t]=+a|T=+t) – P(A[T][=–][t]=+a|T=+t)。根据一致性法则,P(A[T][=+][t]=+a|T=+t)简化为 P(A=+a|T=+t)。因此,我们可以关注第二个项,P(A[T][=–][t]=+a|T=+t)。
在二进制 A 的这个特殊情况下,我们可以通过图形识别(对于非二进制 A,需要更多 3 级假设)来识别 ETT。为了对反事实进行图形识别,我们可以使用带有反事实图的图形识别算法。
10.7.2 在反事实图上的识别
Y0 可以使用名为“IDC*”的图形识别算法(发音为 I-D-C-star)从 ETT 推导出一个估计量。
图 ID 算法,ID,IDC,ID,IDC,在 y0 中
y0 中实现的一些核心图形识别算法是 ID,ID,IDC 和 IDC。ID 从 DAG 中识别干预(2 级)查询和观察(1 级)数据。ID从观察和实验(1 级和 2 级)数据中识别反事实(3 级)查询。IDC 和 IDC扩展 ID 和 ID*以处理条件查询,例如 ETT。
这些算法使用因果图的结构,通过移除无关变量并将图分解成 c 组件子图来递归简化识别问题。它们应用 do-calculus 规则来减少干预项,阻断混杂的后门路径,并将查询分解成更简单的子查询。如果由于图的结构无法进一步简化,算法将返回一个“不可识别”的结果。
本章的代码依赖于 Y0 对这些算法的实现,尽管 Y0 还实现了其他图形识别算法。
列表 10.8 使用图形识别算法识别 ETT
from y0.algorithm.identify.idc_star import idc_star
idc_star(
dag,
outcomes={A @ -t: +a}, #1
conditions={T: +t} #2
)
1 假设结果 A [T=–t] = +a
2 实际条件 T = +t
这将产生一个相当冗长的 2 级估计量。然后我们可以应用 2 级图形识别算法来得到 1 级估计量,这将简化为以下内容:

我将在下一节展示一个简单的推导。
目前,直觉是我们正在应用所谓的反事实图的图形识别算法。到目前为止,我们用于反事实推理的图形选择是并行世界图。确实,我们可以让 y0 为我们制作一个并行世界图。
列表 10.9 使用 y0 绘制并行世界图
from y0.algorithm.identify.cg import make_parallel_worlds_graph
parallel_world_graph = make_parallel_worlds_graph( #1
dag, #1
{frozenset([+t])} #1
) #1
gv_draw(parallel_world_graph) #2
1 make_parallel_worlds_graph 方法接受一个输入 DAG 和干预集。它为每个集构建一个新的世界。
2 辅助函数以熟悉的方式可视化图表。
这个图与我绘制的图略有不同,因为算法将干预的下标应用于发生干预的世界的每个节点;下标索引世界中的所有变量。我们必须推理出从一个世界的C和从另一个世界的C[+][t]必须具有相同的结果,因为C[+][t]不受其世界干预 do(T=+t)的影响。
现在回想一下,并行世界图的问题在于 d 分离无法与之一起工作。例如,在图 10.16 中,d 分离表明在它们的共同外生父节点N[C]的条件下,C和C[+][t]是条件独立的,但我们刚刚阐述C和 C[+][t]必须是相同的;如果 C 有一个值,C[+][t*]必须具有相同的值,因此它们是完全相关的。

图 10.16 由 y0(和 Graphviz)绘制的并行世界图。在这个并行世界图的版本中,下标表示一个世界。例如,+t表示应用干预 do(T=+t)的世界。为了避免混淆,外生变量使用上标而不是下标来表示它们的子内生变量(例如,N**^C是C(和C[+t])的父节点)。
我们可以通过使用反事实图来解决这个问题。反事实图是通过使用并行世界图和反事实查询来理解并行世界图中不同世界的节点哪些是等价的,然后将等价节点合并为一个来创建的。结果图包含与查询中事件相关的并行世界中的节点。与并行世界图不同,你可以使用 d 分离来推理反事实图。我们可以使用 y0 为事件A[T][=–][t]=+a和T=+t创建一个反事实图。
列表 10.10 列表 10.10 反事实图事件 A**[T][=][–][t]=+a 和 T=+t
from y0.algorithm.identify.cg import make_counterfactual_graph
events = {A @ -t: +a, T: +t} #1
cf_graph, _ = make_counterfactual_graph(dag, events)
gv_draw(cf_graph)
1 反事实图与查询中的事件结果一起工作。对于 P(A [T=–t]=+a|T=+t),我们希望事件 A [T=–t] =+a 和 T=+t。
这就创建了图 10.17 中的反事实图。

图 10.17 由 y0(和 Graphviz)生成的事件反事实图。T**[–t]对应于干预 do(T=–t)。
在高层次上,y0 中的图形识别算法通过使用反事实图而不是传统的 DAG 来进行反事实识别。首先,它找到一个 3 级查询的 2 级估计量。从那里,你可以使用实验数据来回答估计量中的 2 级术语,或者你可以尝试从 2 级术语进一步推导出 1 级估计量。
当你对结果进行条件限制时,仅使用图表是不够的!
假设你感兴趣的不是 ETT 项 P(A**[T][=–][t]=+a|T=+t),而是 P(A**[T][=–][t] =+a|T=+t, A=+a),即回答问题:“给定一个接触到策略 +t 并后来取消订阅的订阅者,如果他们没有接触到该策略,他们还会取消订阅吗?”或者你可能对 E(A**[T][=–][t] – A**[T][=+][t]|T=+t, A=+a) 感兴趣,这有时被称为 反事实遗憾,它捕捉了策略 +t 对取消订阅的个人决定取消订阅的贡献量。
P(A**[T][=–][t]=+a|T=+t, A=+a) 是一个假设结果和事实条件冲突的查询示例。在这种情况下,事实条件包含 A 的一个结果,而假设条件包含 A 的干预结果。本节中提到的图形反事实识别技术不适用于此类查询。在这种情况下,识别需要额外的第三级假设。
这很不幸,因为这种反事实正是那种最有趣、最核心的“事情可能如何不同?”反事实问题,这些问题最有趣,也是人类推理和做决策的核心。
我们还可以使用图形识别来进行更高级的查询。例如,假设你想隔离 T 对 A 的影响与 B 对 A 的影响。你想要关注 B 为 –b 的用户。你找到过去实验中的数据,其中“因为你观看了……”策略 B 是随机化的。你取那些数据,并聚焦于实验中被分配 –b 的参与者。该实验中感兴趣的结果是 V,即“因为你观看了”框中推荐内容的参与量。因此,你有了对那些感兴趣订阅者的 V[B][=–][b] 的结果。有了这些新数据,你将查询从 P(A[T][=–][t]=+a|T=+t) 扩展到 P(A[T][=–][t]=+a|T=+t, B=–b, V[B][=–][b]=v),包括 V[B][=–][b]=v,因为它有助于预测流失。现在你有三个并行世界来推理:实际世界、do(T=+t) 的世界和 do(B=–b) 的世界。
列表 10.11 为 do(T=+t) 和 do(B=–b) 创建一个并行世界图
parallel_world_graph = make_parallel_worlds_graph(
dag,
{frozenset([-t]), frozenset([-b])} #1
)
gv_draw(parallel_world_graph)
1 第二个参数列举了假设条件。
此代码创建了图 10.18 中所示的三世界并行世界图。

图 10.18 一个具有实际世界 T=+t 和假设世界 do(T=–t) 以及 do(B=–b) 的并行世界图。虚线是来自外生变量的边(深灰色)。
显然,查询 P(A[T][=–][t]=+a|T=+t, B=–b, V[B][=–][b]=v) 将并行世界图折叠成与 P(A[T][=–][t]=+a|T=+t) 相同的反事实图。
列表 10.12 扩展表达式的反事实图
joint_query = {A @ -t: +a, T: +t, B: -b, V @ -b: +v}
cf_graph, _ = make_counterfactual_graph(dag, joint_query)
gv_draw(cf_graph)
这为我们提供了图 10.19 中的反事实图,它与图 10.17 中的图相同。

图 10.19 对于P(A**[T][=–][t]=+a|T=+t, B=–b, V**[B][=–][b]=v)的反事实图与P(A**[T][=–][t]=+a|T=+t)相同。
接下来,我们将探讨另一种基于图的称为单世界干预图的方法。
10.7.3 使用单世界干预图进行反事实识别
单世界干预图(SWIGs)为使用反事实图进行反事实识别提供了一种替代方案。与反事实图一样,我们使用原始因果 DAG 和因果查询来构建一个 SWIG。我们将使用 Netflix 推荐示例来构建干预 do(T=–t)和 do (B=–b)的 SWIG。让我们从一个因果 DAG 构建一个 SWIG。
节点分割操作
我们有一个针对 do(T=+t)的干预,我们可以通过一种特殊的图手术称为节点分割操作来实现它。我们在干预目标T**上分割出一个新节点,如图 10.20 所示。T仍然代表原始图中的相同变量,但新节点代表一个常数,干预值+t。T保持其父节点(在这种情况下为C),但将其子节点(在这种情况下为W*)丢失到新节点。
下标继承
接下来,每个新节点的下游节点都将继承新节点的值作为下标。例如,在图 10.21 中,W和A是干预的下游,因此下标[T][=][–][t]被附加到这些节点上,因此它们变为W[T][=][-][t]和A[T][=][-][t]。

图 10.20SWIG 的干预操作是节点分割操作。

图 10.21 干预下游的每个节点都获得干预下标。
对每个干预重复此过程
我们对每个干预重复此过程。在图 10.22 中,我们应用 do(B=–b),分割B,并将V转换为V[B][=–][b],将A[T][=-][t]转换为A[T][=–][t][,][B][=–][b]。

图 10.22 一个节点接受所有上游干预的下标。
与反事实图一样,SWIG 包含反事实变量,并允许 d 分离。有了这些属性,我们可以进行识别。
10.7.4 使用 SWIG 进行识别
假设我们感兴趣的是 ETT,并想识别P(A[T][=][–][t]=+a|T=+t)。我们推导出图 10.23 中的 SWIG。

图 10.23 我们可以使用 SWIG 通过不可知性技巧推导出 ETT。
使用这个图,我们可以使用我在第 10.4 节中介绍的不知性技巧来识别P(A[T][=–][t]=+a|T=+t):
-
P(A[T][=–][t]=+a|T=+t) = ∑[c] P(A[T][=–][t]=+a, C[T][=–][t]=c|T=+t),根据全概率定律。
-
∑[c] P(A[T][=][–][t]=+a, C[T][=–][t]=c|T=+t) = ∑[c] P(A[T][=–][t]=+a, C =c|T=+t),因为 C 不受对 T 的干预的影响。
-
根据概率的链式法则,∑[c] P(A[T][=–][t]=+a, C=c|T=+t) 可以分解为 ∑[c] P(A[T][=–][t] =+a|C=c, T=+t) P(C=c | T=+t)。
-
P(A[T][=–][t]=+a|C=c, T=+t) = P(A[T][=–][t]=+a|C=c, T=–t),再次通过不可知性技巧。
-
就像之前一样,P(A[T][=–][t]=+a|C=c, T=–t) = P(A =+a|C=c, T=–t),根据一致性法则。因此,P(A[T][=–][t]=+a|T=+t) = ∑[c] P(A =+a|C=c, T=–t) P(C=c | T=+t)。
第 4 步中的不可知性技巧中发生了魔法,其中 C 与 A[T][=–][t] 和 T 的 d-分离使我们能够将 T=+t 改为 T=–t。请注意,在图 10.17 中我们推导出的反事实图中也存在相同的 d-分离。区别在于推导 SWIG 很容易,而推导反事实图则很微妙,通常使用 y0 中的 make_counterfactual_grap 算法。
10.7.5 单世界假设
节点拆分操作依赖于一个新的 3 级假设。如果你打算对变量 X 进行节点拆分,那么你是在假设可以知道在没有干预的情况下 X 会自然取什么值,并且在你意识到这个值之前,你有能力进行干预。想象一下在我们的 Netflix 例子中,如果一个订阅者的配置文件 C=c,推荐算法正准备为该订阅者分配一个推荐“为你精选”的策略 +t,但在该策略生效之前,你进行了干预并人为地将其改为 –t。可能你强制策略变为 –t 的方式产生了一些副作用,从而以某种基本方式改变了推荐系统,以至于在这个新系统中,T 最初就不会是 +t。在单世界假设下,你假设你可以知道 T 的自然值会是 +t,并且你的干预不会以影响 T 取自然值的方式改变系统。当你用 SWIGs 进行推理时,你隐含地做出了这个假设。
这个假设让你避免了创建额外的世界来进行推理的需要。你可以在结果 T=+t 上进行条件化,并在“单世界”中进行干预 do(T=–t)。你还可以进行实验,应用干预 do(T=–t) 并测试 T(你知道 T 的“自然值”)在给定 C 的情况下是否对 A(T=–t) 条件独立。这减少了你可以回答的反事实查询的数量,但 SWIGs 的支持者认为这是一个优点,因为它限制了你可以通过实验验证的反事实。
对比反事实图和 SWIGs
反事实图和 SWIGs 在功能上相似,但它们是明显不同的工具。
-
反事实图—反事实图通过在可能世界中合并等效的并行世界图节点来工作。它们只包含与特定查询相关的节点。它们定义了二进制事件,如 {T=+t} 和 {T=–t}—即使在连续变量的情况下这也很好用,因为反事实语言通常比较一个假设条件和一个事实条件(例如,“我们投资了 100 万;如果我们投资了{2/more/half/…}会怎样?”)。
-
单世界干预图(SWIGs)**—SWIG 通过应用一种节点分割类型的图手术来工作。与反事实图不同,它们处理的是一般变量(而不仅仅是二进制事件),并且不是针对特定查询的(所有原始节点都存在)。然而,它们依赖于单世界假设——可以确定知道变量在没有干预的情况下会取什么值。
这两种图的主要用途是识别。反事实图和 SWIGs 都不能从 1 级或 2 级数据中识别出反事实查询,例如 P(A**[T][=–][t]=+a|T=+t, A=+a),其中相同的变量出现在假设结果和事实条件中。但你可以仍然推导出这样的查询的反事实图;对于 SWIGs 来说则不是这样。如果你想要在查询如 P(A**[T][=–][t]=+a|T=+t, A=+a)的情况下推理不同世界之间的独立性,这将是很有用的。
10.8 识别和概率推理
我们已经看到,识别任务的核心部分是推导出一个估计量。这个估计量如何与概率机器学习方法相结合呢?
以我们的在线游戏模型为例,其中 ETT = E(I[E][=“high”] – I[E][=“low”]|E=“high”) = E(I[E][=“high”]|E=“high”) – E(I[E][=“low”]|E=“high”)。我们需要识别 P(I[E][=“high”]|E=“high”) 和 P(I[E][=“low”]|E=“high”)。回想一下,P(I[E][=“high”]|E=“high”) 通过一致性法则简化为 1 级查询 P(I|E=“high”),所以挑战在于识别反事实分布 P(I[E][=“low”]|E=“high”)。
使用 Pyro 的概率机器学习方法,我们知道我们可以通过使用pyro.condition在E=“high”上条件化,然后运行推理来推断 P(I|E=“high”)。问题是我们将如何推断反事实分布 P(I[E][=“low”]|E=“high”)。
在上一节中,我们看到我们可以使用 SWIG 来识别这个查询(假设单世界假设成立)。我们使用了 SWIG 来推导出以下关于 P(I[E][=0]=i|E=“high”) 的估计量:

但关于在 Pyro 中构建模型,我们该如何处理这个估计量呢?我们可以构建两个 Pyro 模型,一个用于 P(G|E),另一个用于 P(I|G, E),推断 P(I=i| G=g, E=“low”) 和 P(G=g | E=“high”),然后进行求和。但与我们的常规因果生成模型进行概率推理的方法相比,这并不优雅:
-
实现完整的因果生成模型。
-
在数据上训练其参数。
-
应用干预算子来模拟干预。
-
运行推理算法。
在这种方法中,我们构建一个因果模型——我们不会为估计量的组件 P(G|E) 和 P(I|G, E) 构建单独的模型。尽管如此,如果我们有识别,那么我们的常规因果生成模型进行概率推理的方法仍然有效,前提是我们实现了步骤 1 中的因果假设,并在步骤 2 中训练的数据。我们甚至不需要明确知道估计量;只要知道它存在——换句话说,查询是可识别的(例如,通过使用 Y0 的 check_identifiable 函数)就足够了。有了识别,步骤 2–4 共同成为对该估计量的另一个估计器。
为了说明,让我们考虑如何使用 Pyro 模型从我们的在线游戏示例中采样 P(I[E][=“low”]|E=“high”)。为了简单起见,让我们将 E=“high” 和 E=“low” 分别替换为 E=1 和 E=0。我们知道,给定我们的因果 DAG 和单词假设,P(I[E][=0]|E=1) 是可以确定的。幸运的是,Pyro 的(以及 ChiRho 的)do 干预算子默认实现了 SWIG 的节点分割操作(如果你使用了 pyro.render_model 来可视化干预,但没有得到你期望的结果,这就是原因)。对于因果 DAG 上的普通干预查询,这与普通图手术方法进行干预没有区别。但当我们想要对 E=1 进行条件化并干预设置 E=0 时,Pyro 会满足我们的需求。我们将使用这种方法来从 P(I[E][=0]|E=1) 中采样。作为一个合理性检查,我们还将从普通的干预分布 P(I[E][=0]) 中采样,并将这些样本与从 P(I[E][=0]|E=1) 中采样的样本进行对比。
设置你的环境
为了改变节奏,我将使用 NumPyro 而不是 Pyro 来说明这个例子,尽管代码在 Pyro 上经过少量调整后也能工作。我们将使用 NumPyro 版本 0.15.0。我们还将使用一个名为 Funsor 的推理库,它旨在补充 NumPyro 和 Pyro,版本为 0.4.5。我们还将使用 Matplotlib 进行绘图。
首先,让我们构建模型。
NumPyro 与 Pyro
Pyro 扩展了 PyTorch,而 NumPyro 扩展了 NumPy 和 JAX 的自动微分。用户界面相当相似。如果你对 PyTorch 抽象不太熟悉,或者你更喜欢使用在 Stan 或 PyMC 中使用的贝叶斯编程模式进行基于 MCMC 的推理,那么你可能会更喜欢 NumPyro。
列表 10.13 在 Pyro 中从 P(I**[E][=][0]) 与 P(I**[E][=][0]|E=1) 中生成
import jax.numpy as np
from jax import random
from numpyro import sample
from numpyro.handlers import condition, do
from numpyro.distributions import Bernoulli, Normal
from numpyro.infer import MCMC, NUTS
import matplotlib.pyplot as plt
rng = random.PRNGKey(1)
def model(): #1
p_member = 0.5 #1
is_guild_member = sample( #1
"Guild Membership", #1
Bernoulli(p_member) #1
) #1
p_engaged = (0.8*is_guild_member + 0.2*(1-is_guild_member)) #1
is_highly_engaged = sample( #1
"Side-quest Engagement", #1
Bernoulli(p_engaged) #1
) #1
p_won_engaged = (.9*is_highly_engaged + .1*(1-is_highly_engaged)) #1
high_won_items = sample("Won Items", Bernoulli(p_won_engaged)) #1
mu = ( #1
37.95*(1-is_guild_member)*(1-high_won_items) + #1
54.92*(1-is_guild_member)*high_won_items + #1
223.71*(is_guild_member)*(1-high_won_items) + #1
125.50*(is_guild_member)*high_won_items #1
) #1
sigma = ( #1
23.80*(1-is_guild_member)*(1-high_won_items) + #1
4.92*(1-is_guild_member)*high_won_items + #1
5.30*(is_guild_member)*(1-high_won_items) + #1
53.49*(is_guild_member)*high_won_items #1
) #1
norm_dist = Normal(mu, sigma)#1
in_game_purchases = sample("In-game Purchases", norm_dist) #1
1 一种在线游戏模型的版本。权重是从数据中估计的(学习过程在此未展示)。
接下来,我们将应用干预并运行推断以从 P(I[E][=][0]) 中采样。
列表 10.14 应用干预 do(E=0)并从 P(I**[E][=][0]) 中推断
intervention_model = do( #1
model, #1
{"Side-quest Engagement": np.array(0.)}) #1
intervention_kernel = NUTS(intervention_model) #2
intervention_model_sampler = MCMC( #2
intervention_kernel, #2
num_samples=5000, #2
num_warmup=200 #2
) #2
intervention_model_sampler.run(rng) #2
intervention_samples = intervention_model_sampler.get_samples() #2
int_purchases_samples = intervention_samples["In-game Purchases"] #2
1 对模型应用 do 操作符。
2 对 P(I [E=0])进行推断以从中采样。
我们将对比来自 P(I[E][=][0]) 的样本与我们将从 P(I**[E][=][0]|E=1) 中抽取的样本。为了推断 P(I**[E][=][0]|E=1),我们将 intervention_model 条件化在事实条件 E=1 上。然后,我们将再次对此条件化后的干预模型进行推断。
列表 10.15 条件干预模型并推断 P(I**[E][=][0]|E=1)
cond_and_int_model = condition( #1
intervention_model, #1
{"Side-quest Engagement": np.array(1.)} #1
) #1
int_cond_kernel = NUTS(cond_and_int_model) #2
int_cond_model_sampler = MCMC( #2
int_cond_kernel, #2
num_samples=5000, #2
num_warmup=200 #2
) #2
int_cond_model_sampler.run(rng) #2
int_cond_samples = int_cond_model_sampler.get_samples() #2
int_cond_purchases_samples = int_cond_samples["In-game Purchases"] #2
1 现在应用条件操作符以从 P(I [E=0]|E=1)中采样。
2 对 P(I [E=0]|E=1)进行推断以从中采样。
注意,Pyro 的 do 和 condition 子例程可以相互组合;即,对于一个具有变量 X 的模型,do(condition(model, {"X": 1.}), {"X": 0.}) 等价于 condition(do(model, {"X": 0.}), {"X": 1.})。
最后,我们将绘制 P(I**[E][=0]) 和 P(I**[E][=0]|E=1) 的样本,并评估这些分布之间的差异。
列表 10.16 从 P(I**[E][=][0]) 和 P(I**[E][=][0]|E=1) 中绘制样本
plt.hist( #1
int_purchases_samples, #1
bins=30, #1
alpha=0.5, #1
label='$P(I_{E=0})$' #1
) #1
plt.hist( #2
int_cond_purchases_samples, #2
bins=30, #2
alpha=0.5, #2
label='$P(I_{E=0}|E=1)$' #2
) #2
plt.legend(loc='upper left') #2
plt.show() #2
1 绘制 P(I [E=0])样本的直方图。
2 绘制 P(I [E=0]|E=1)样本的直方图。
此代码生成了图 10.24 中的直方图。

图 10.24 Pyro 生成的 P(I**[E][=0]) 和 P(I**[E][=0]|E=1) 样本的直方图
在这个例子中,参数已被给出。在第十一章,我们将探讨估计方法,我们将无缝地将这种查询推断与从数据中进行的贝叶斯参数推断结合起来。
10.9 部分识别
我们将在本章结束时简要讨论部分识别。有时,在给定假设的情况下,查询可能无法识别,但它可能是 部分可识别的。部分可识别性意味着你可以识别查询的上限和下限的估计值。部分识别对于因果 AI 高度相关,因为机器学习算法通常依赖于寻找和优化目标函数的界限。让我们通过几个例子来了解一下。
MCMC 与 SVI
在这里,我们使用了马尔可夫链蒙特卡洛(MCMC)方法,但 Pyro 和 NumPyro 都提供了对随机变分推断(SVI)的抽象。在这个例子中,模型的参数(p_member,p_engaged等)被指定。我们也可以使用贝叶斯先验将参数设为未知变量,并对这些因果查询 P(I**[E][=0]) 和 P(I**[E][=0]|E=1) 进行推断;在这种情况下,我们将对这些查询进行贝叶斯推断。
但为此我们需要从观测分布中获取 N 个独立同分布(IID)样本,其中我们有了图形识别(P(G, E, W, I), P(G, E, I), 或 P(E, W, I))。在 P(G, E, W, I) 的情况下,其中 DAG 中的所有变量都被观测到,未知变量的数量仅仅是参数的数量。但在后两种情况下,即 P(G, E, I) 或 P(E, W, I),其中存在一个大小为 N 的潜在 G 或 W,未知变量的数量会随着 N 的增长而增长。在这种情况下,SVI 将更好地与大的 N 进行缩放。我们将在第十一章中看到一个例子。
假设在我们在线游戏的例子中,你进行了一个随机分配玩家到处理组或对照组的实验。处理组的玩家会接触到鼓励更多侧面任务参与的政策。你推理说,由于你不能真正强迫玩家参与侧面任务,所以将这个随机处理/控制变量作为我们的侧面任务参与变量(如图 10.25 中的 DAG 所示)的父变量会更好。

图 10.25 由于 G 和 W 未被观测到,我们无法识别 E 对 I 的 ATE。但给定变量 A,即代表在随机实验中玩家的分配,我们有部分识别。
对于这个新变量 A,让 A=1 表示处理组,A=0 表示对照组。我们有了这个新变量 A,政策对游戏内购买的平均处理效应 E(I[A][=1] – I[A][=0]) 是一个有趣的查询。但假设我们最终仍然对知道侧面任务参与的平均处理效应本身对购买的影响感兴趣,即 E(I[E][=“high”] – I[E][=“low”])。
如果观察到公会成员资格 (G),我们将通过后门调整进行识别。如果观察到赢得的物品 (W),我们可以使用前门调整。但假设在这个场景中你既没有观察到 G 也没有观察到 W。在这种情况下,观察侧面任务分组变量将为你提供部分识别。假设游戏内购买变量是一个二进制值 1 表示“高”和 0 表示“低”,而不是一个连续值。那么 E(I[E][=“high”] – I[E][=“low”]) 的界限是

这些界限可能是拥有完全识别的下一件好事,特别是如果界限很紧的话。或者,也许知道 ATE 对于侧面任务参与的下限显著大于 0 就足够了。
类似地,对于常见的反事实查询,如因果概率,也存在一般界限。例如,假设你想知道高侧面任务参与是否是高游戏内购买的一个必要且充分的条件。你可以构建以下必要性及充分性概率(PNS)的界限:

这些界限包括第二层量如 P(I[E][=e]=i),如果可能的话,你可以根据你的假设继续识别第一层估计量。
记住,部分识别界限高度特定于你的因果假设(如 DAG)和变量的参数化;例如,前面的例子是针对二元变量的。参见www.altdeep.ai/p/causalaibook章节中的笔记,以获取推导这些界限以及其他实际假设集界限的论文链接。
摘要
-
因果识别的重要性在人工智能时代得到了提升,因为我们试图理解深度学习架构中的因果归纳偏差。
-
如 y0 之类的库实现了算法识别的策略。
-
因果层次是一个三层结构,它将我们提出的问题、我们开发的模型以及我们做出的因果推断进行分类。这些级别是关联、干预和反事实。
-
关联层面的推理解决“是什么”问题,并使用基本的统计假设来构建回答这些问题的模型。
-
干预或反事实查询落在它们对应的层次级别上。
-
观察性数据落在关联层面上,而实验数据落在层次结构的干预层面上。反事实数据出现在模型者可以控制确定性数据生成过程(DGP)的情况下。
-
因果识别是从实验或观察数据中推断因果推断的步骤。这是通过确定低层次层次的数据是否可以用来推断高层次层次的一个查询来完成的。
-
因果识别的一个例子是后门公式,它将干预水平查询 P(Y[X][=][x]) 等同于关联水平数量 ∑[z]P(Y|X=x, Z=z)P(Z=z),其中 Z 是一组共同原因。
-
因果层次定理说明了低层次数据在没有高层次建模假设的情况下不足以推断高层次分布。
-
do-calculus 有三个规则,可用于基于图的识别。
-
反事实图是一个包含一个图上反事实世界变量的 DAG。与并行世界图不同,它允许 d 分离。我们从并行世界图和目标查询中推导出反事实图。
-
图形识别算法通过使用如 do-calculus 等规则自动使用图进行识别。
-
非参数识别是指在不依赖图形假设的识别,例如关于模型中变量之间功能关系的假设。
-
不可识别假设是指因果变量和潜在结果在给定混杂因素的情况下条件独立。
-
治疗对受治者(ETT)的效果评估了原因对暴露于该原因的群体子集的影响。
-
单世界干预图(SWIGs)提供了使用 do-calculus 和反事实图进行反事实识别的直观替代方案。它们是通过在原始因果 DAG 上应用节点分割操作来构建的。SWIGs 使用“单世界”假设,该假设认为在变量实现其自然值之前,可以知道变量的自然值,同时对其进行干预,而不会产生任何影响该自然值的副作用。
-
SWIGs 在单世界假设下与变量和一组有限的反事实一起工作,而反事实图可以容纳无法图形识别的查询。
-
Pyro 实现了 SWIG 的节点分割干预模型,这使得对 SWIG 识别的量进行概率推理成为可能。
-
使用因果图模型和概率推理算法进行因果查询的推断是可能的,只要查询被识别,并且考虑到模型的假设和训练数据。
-
部分识别意味着你可以至少识别出对目标查询界限的估计量。如果你缺乏完全识别,这可能会非常有用,尤其是在机器学习通常通过优化目标函数的界限来工作的情况下。
第四部分 因果推断的应用
在第四部分,我们将关注因果推断方法在实际问题中的应用。你将获得因果效应估计工作流程、自动化决策以及因果性与大型语言模型和其他基础模型集成的实践经验。到本部分的结尾,你将能够使用基于机器学习的因果效应估计方法,以及利用因果推断方法来增强从强化学习到前沿生成式人工智能的现代机器学习应用。
第十一章:构建因果推断工作流程
本章涵盖
-
构建因果分析工作流程
-
使用 DoWhy 估计因果效应
-
使用机器学习方法估计因果效应
-
使用因果潜在变量模型进行因果推断
在第十章中,我介绍了一个因果推断工作流程,在本章中,我们将专注于完整构建这个工作流程。我们将特别关注一种查询类型——因果效应,但这个工作流程可以推广到所有因果查询。
我们将专注于因果效应推断,即平均处理效应(ATEs)和条件平均处理效应(CATEs)的估计,因为它们是最流行的因果查询。
在第一章中,我提到了“推断的商品化”——现代软件库如何使我们能够抽象出推断算法的统计和计算细节。在本章中,你首先会看到 DoWhy 库如何“商品化”因果推断,使我们能够从高层次关注算法的因果假设以及它们是否适合我们的问题。
我们将在一个使用概率机器学习对具有潜在变量的因果生成模型进行因果效应推断的示例中再次看到这一现象。在这里,我们将看到使用 PyTorch 的深度学习如何提供另一种商品化推断的方法。
11.1 步骤 1:选择查询
回想第十章中提到的因果推断工作流程,如图 11.1 所示。

图 11.1 因果推断分析的流程图
让我们回到我们的在线游戏示例,并使用这个工作流程来回答一个简单的问题:
旁支任务参与对游戏内购买的影响有多大?
我们将感兴趣的因果关系称为“旁支任务参与”(E),即“处理”变量;“游戏内购买”(I)将是“结果”变量。我们感兴趣的查询是平均处理效应(ATE):
E(I[E][=“high”] – I[E][=“low”])
复习:为什么 ATEs 和 CATEs 占主导地位
估计 ATEs 和 CATEs 是最受欢迎的因果效应推断任务,原因如下:
-
当随机实验不可行、不道德或不可能时,我们可以依赖因果效应推断技术。
-
我们可以使用因果效应推断技术来解决现实世界实验中的实际问题(例如,随机化后的混杂、流失、溢出、缺失数据等)。
-
在公司可以在在线应用程序和商店中运行许多不同数字实验的时代,因果效应推断技术可以帮助优先处理实验,降低机会成本。
此外,当我们调查我们的游戏数据时,我们发现了一些来自过去实验的数据,该实验旨在测试鼓励侧任务参与对游戏内购买的影响。在这个实验中,所有玩家都被随机分配到处理组或对照组。在处理组中,游戏机制被修改以诱使玩家参与更多侧任务,而对照组则玩未修改的游戏版本。我们将 侧任务分组分配 (A) 变量定义为玩家是否在这个实验中被分配到处理组或对照组。
为什么不直接采用这个实验产生的 ATE 估计值?这将是对 E(I[A][=“处理”] – I[A][=“控制”]) 的估计。
这是修改游戏机制对游戏内购买因果效应的体现。虽然这推动了侧任务参与,但我们知道侧任务参与也受到其他可能混杂因素的影响。因此,我们将关注 E(I[E][=“高”] – I[E][=“低”])。
11.2 步骤 2:构建模型
接下来,我们将构建我们的因果模型。由于我们针对的是 ATE,我们可以坚持使用 DAG。让我们假设我们构建了一个更详细的在线游戏示例,并在图 11.2 中产生了因果 DAG。

图 11.2 在线游戏 DAG 的扩展版本。关于对游戏内购买中侧任务参与因果效应的考虑,我们增加了两个额外的混杂因素和两个工具变量。
扩展模型添加了一些新的变量:
-
侧任务分组分配 (A)—如果玩家在随机实验中接触到了鼓励更多侧任务参与的游戏机制,则分配值为 1;否则为 0。
-
定制水平 (C)—量化玩家对其角色和游戏环境的定制的得分。
-
游戏时间 (T)—玩家玩游戏所花费的时间。
-
先前经验 (Y)—玩家在开始玩游戏之前拥有的经验量。
-
玩家技能水平 (S)—玩家在游戏任务中表现好坏的得分。
-
总库存 (V)—玩家积累的游戏物品数量。
我们对 侧任务参与 对 游戏内购买 的平均处理效应 (ATE) 感兴趣,因此根据因果充分性(第三章),我们知道我们需要添加这些变量的共同原因。我们已经看到了 公会会员资格 (G),但现在我们添加额外的共同原因:先前经验、游戏时间 和 玩家技能水平。我们还添加 侧任务分组分配 和 定制水平,因为这些可能是有用的 工具变量——这些变量是感兴趣的治疗的因果,并且变量到结果的唯一因果路径是通过治疗。我将在下一节中更多地说到工具变量。
最后,我们将添加总库存。这是游戏内购买和赢得的物品之间的一个碰撞器。也许我们公司的数据科学家普遍将其用作游戏内购买的预测因子。但正如你将看到的,我们想要避免在因果效应估计中添加碰撞器偏差。
设置你的环境
以下代码是用 DoWhy 0.11 和 EconML 0.15 编写的,它期望使用版本 2.0 之前的 NumPy 版本。具体的 pandas 版本是 1.5.3。再次强调,我们使用 Graphviz 进行可视化,使用 python PyGraphviz 库版本 1.12。如果你跳过 PyGraphviz 的安装,代码应该可以工作,除了可视化之外。
首先,让我们构建 DAG 并使用 PyGraphviz 库可视化图形。
列表 11.1 构建因果 DAG
import pygraphviz as pgv #1
from IPython.display import Image #2
causal_graph = """
digraph {
"Prior Experience" -> "Player Skill Level";
"Prior Experience" -> "Time Spent Playing";
"Time Spent Playing" -> "Player Skill Level";
"Guild Membership" -> "Side-quest Engagement";
"Guild Membership" -> "In-game Purchases";
"Player Skill Level" -> "Side-quest Engagement";
"Player Skill Level" -> "In-game Purchases";
"Time Spent Playing" -> "Side-quest Engagement";
"Time Spent Playing" -> "In-game Purchases";
"Side-quest Group Assignment" -> "Side-quest Engagement";
"Customization Level" -> "Side-quest Engagement";
"Side-quest Engagement" -> "Won Items";
"Won Items" -> "In-game Purchases";
"Won Items" -> "Total Inventory";
"In-game Purchases" -> "Total Inventory";
}
""" #3
G = pgv.AGraph(string=causal_graph) #3
G.draw('/tmp/causal_graph.png', prog='dot') #4
Image('/tmp/causal_graph.png') #5
1 下载 PyGraphviz 和相关库。
2 可选导入用于在 Jupyter 笔记本中可视化 DAG
3 将 DAG 指定为 DOT 语言字符串,并从字符串中加载 PyGraphviz AGraph 对象。
4 将图形渲染为 PNG 文件。
5 显示图形。
这将返回图 11.3 中的图形。

图 11.3 使用 PyGraphviz 库可视化我们的模型
在这个阶段,我们可以使用第四章中概述的条件独立性测试技术来验证我们的模型。但请记住,我们还可以专注于因果效应估计在“反驳”(步骤 5)部分工作流程中依赖的假设子集。
11.3 步骤 3:识别估计量
接下来,我们将运行识别。我们的因果查询是
E(I[E][=“high”] – I[E][=“low”])
为了简单起见,让我们将“high”重新编码为 1,“low”为 0。
E(I[E][=1] – I[E][=0])
此查询位于因果层次结构的第 2 级。我们正在进行实验;我们只有观测数据——来自第 1 级分布的样本。我们的识别任务是使用我们的第 2 级查询和因果模型来识别一个第 1 级估计量,这是一个我们可以应用于我们数据中变量分布的操作。
首先,让我们下载我们的数据并查看我们的观测分布中有哪些变量。
列表 11.2 下载并显示数据
import pandas as pd
data = pd.read_csv(
"https://raw.githubusercontent.com/altdeep/causalML/master/datasets
↪/online_game_example_do_why.csv" #1
)
print(data.columns) #2
1 下载在线游戏数据集。
2 打印变量。
这将打印出以下变量集:
Index(['Guild Membership', 'Player Skill Level', 'Time Spent Playing',
'Side-quest Group Assignment', 'Customization Level',
'Side-quest Engagement', 'Won Items', 'In-game Purchases',
'Total Inventory'],
dtype='object')
我们的第 1 级观测分布包括 DAG 中的所有变量,除了先前经验。因此,先前经验是一个潜在变量(图 11.4)。

图 11.4 先前经验在数据中未观察到;它是我们 DAG 中的一个潜在(未观察到的)变量。
我们使用 y0 的特定领域语言为概率表达式指定了估计量的基本分布:
Identification.from_expression(
graph=dag,
query=query,
estimand=observational_distribution
)
在这里,我们将使用 DoWhy。通过 DoWhy,我们只需传递 pandas DataFrame、DAG 和因果查询到CausalModel类的构造函数中,就可以指定观测分布。
列表 11.3 实例化 DoWhy 的CausalModel
from dowhy import CausalModel #1
model = CausalModel(
data=data, #2
treatment='Side-quest Engagement', #3
outcome='In-game Purchases', #3
graph=causal_graph #4
)
1 安装 DoWhy 并加载 CausalModel 类。
2 使用数据实例化 CausalModel 对象,这些数据代表我们从其中推导出估计量的第一级观察分布。
3 指定我们希望估计的目标因果查询,即处理对结果的影响的因果效应。
4 提供因果 DAG。
接下来,identify_effect方法将向我们展示,根据我们的因果模型和观察到的变量,我们可以针对的可能估计量。
列表 11.4 DoWhy 中的运行识别
identified_estimand = model.identify_effect() #1
print(identified_estimand)
1 CausalModel 类的 identify_effect 方法列出可识别的估计量。
identified_estimand对象是IdentifiedEstimand类的一个对象。打印它将列出(如果有)估计量和它们所包含的假设。在我们的情况下,我们有三个可以针对的估计量:
-
通过调整集玩家技能水平、公会会员资格和游戏时间进行后门调整估计量
-
通过中介赢得物品进行前门调整估计量
-
通过侧任务组分配和定制级别进行工具变量估计量
DoWhy 中的图形识别
在撰写本文时,DoWhy 尚未实现 y0 等图形识别算法,但这些是实验性的,不是默认的识别方法。默认方法基于你的图结构寻找常用的估计量(例如,后门、前门、工具变量)。可能存在默认方法遗漏的可识别估计量,但这些估计量不太常用。
让我们更仔细地检查这些估计量。
11.3.1 后门调整估计量
让我们看看第一个估计量,即后门调整估计量的打印摘要:
Estimand type: EstimandType.NONPARAMETRIC_ATE
### Estimand : 1
Estimand name: backdoor
Estimand expression:
d
────────────────────────(E[In-game Purchases|Time Spent Playing,Guild
d[Side-quest Engagement]
Membership, Player Skill Level])
Estimand assumption 1, Unconfoundedness: If U→{Side-quest Engagement} and U→In-game Purchases then P(In-game Purchases|Side-quest Engagement,Time Spent Playing,Guild Membership,Player Skill Level,U) = P(In-game Purchases|Side-quest Engagement,Time Spent Playing,Guild Membership,Player Skill Level)
这个打印输出告诉我们一些事情:
-
EstimandType.NONPARAMETRIC_ATE—这意味着估计量可以通过图形或“非参数”方法,例如 do-calculus 来识别。 -
Estimand name: backdoor—这是后门调整估计量。 -
Estimand expression—估计量的数学表达式。由于我们想要 ATE,我们修改后门估计量以针对 ATE。 -
Estimand assumption 1—估计量背后的因果假设。
最后一条是最重要的。对于每个估计量,DoWhy 都会列出必须成立的因果假设,以便对目标因果查询进行有效估计。在这种情况下,假设没有隐藏的(未测量的)混杂因素,DoWhy 称之为U。对后门调整估计量的估计假设所有混杂因素都已调整。
注意,我们不需要观察先前经验来获得后门调整估计量。我们只需要观察一个调整集,该调整集可以 d-分离或“阻断”所有后门路径。
打印输出中的下一个估计量是一个工具变量估计量。
11.3.2 工具变量估计量
第二个估计量,工具变量估计量的打印摘要如下(注意,我将变量名称缩短为缩写,以便摘要适合这一页):
### Estimand : 2
Estimand name: iv
Estimand expression:
⎡ -1⎤
⎢ d ⎛ d ⎞ `⎥`
`E``⎢``──────────(IGP)``⋅` `───────────([SQE]) ` ``````py ````` `⎥` `⎣``d[SQGA CL]` `⎝` `d[SQGA CL]` `⎠` ```py` ``` `` `⎦` 估计量假设 1,假设为随机:如果 U→→IGP 则 ¬(U →→{SQGA,CL}) 估计量假设 2,排除:如果我们移除 {SQGA,CL}→{SQE} 则 ¬({SQGA,CL}→IGP) `` ```py ```` ```py`` ``````py
py`` ````There are two level 2 definitional requirements for a variable to be a valid instrument: 1. *As-if-random*—Any backdoor paths between the instrument and the outcome can be blocked. 2. *Exclusion*—The instrument is a cause of the outcome only indirectly through the treatment. The variables in our model that satisfy these constraints are *Side-Quest Group Assignment* and *Customization Level*, as shown in figure 11.5.  ##### Figure 11.5 *Side-Quest Group Assignment* and *Customization Level* are valid instrumental variables. The printout of `identified_estimand` shows the two constraints: 1. `Estimand` `assumption` `1,` `As-if-random`—DoWhy assumes that none of the other causes of the outcome (*In-Game Purchases*) are also causes of either instrument. In other words, there are no backdoor paths between the instruments and the outcome. 2. `Estimand assumption` `2,` `Exclusion`—This says that if we remove the causal path from the instruments to the treatment (*Side-quest Engagement*), there would be no causal paths from the instruments to the outcome (*In-Game Purchases*). In other words, there are no causal paths between the instruments and the outcome that are not mediated by the treatment. Note that DoWhy’s constraints are relatively restrictive; DoWhy prohibits the *existence* of backdoor paths and non-treatment-mediated causal paths between the instrument and the outcome. In practice, it would be possible to block these paths with backdoor adjustment. DoWhy is making a trade-off that favors a simpler interface. ##### Parametric assumptions for instrumental variable estimation The level 2 graphical assumptions are not sufficient for instrumental variable identification; additional parametric assumptions are needed. DoWhy, by default, makes a linearity assumption. With a linear assumption, you can derive the ATE as a simple function of the coefficients of linear models of outcome and the treatment given the instrument. DoWhy does this by fitting linear regression models. Next, we’ll look at the third estimand identified by DoWhy—the front-door estimand. ### 11.3.3 The front-door adjustment estimand Let’s move on to the assumptions in the third estimand, the front-door estimand. DoWhy’s printed summary is as follows (again, I shortened the variable names to acronyms in the printout so it fits the page): py ### Estimand : 3 Estimand name: frontdoor Estimand expression: ⎡ d d ⎤ E⎢────────────(IGP)⋅───────([WI])⎥ ⎣d[WI] d[SQE] ⎦ Estimand assumption 1, Full-mediation: WI intercepts (blocks) all directed paths from SQE to IGP. Estimand assumption 2, First-stage-unconfoundedness: If U→{SQE} and U→{WI} then P(WI|SQE,U) = P(WI|SQE) Estimand assumption 3, Second-stage-unconfoundedness: If U→{WI} and U→IGP then P(IGP|WI, SQE, U) = P(IGP|WI, SQE) ``` As we saw in chapter 10, the front-door estimand requires a mediator on the path from the treatment to the outcome—in our DAG, this is Won Items. The printout for identified_estimand lists three key assumptions for the front-door estimand: 1. Full-mediation—The mediator (Won-Items) intercepts all directed paths from the treatment (Side-Quest Engagement) to the outcome (In-Game Purchases). In other words, conditioning on Won-Items would d-separate (block) all the paths of causal influence from the treatment to the outcome. 2. First-stage-unconfoundedness—There are no hidden confounders between the treatment and the mediator. 3. Second-stage-unconfoundedness—There are no hidden confounders between the outcome and the mediator. With our DAG and the variables observed in the data, DoWhy has identified three estimands for the ATE of Side-Quest Engagement on In-Game Purchases. Remember, the estimand is the thing we estimate, so which estimand should we estimate? ### 11.3.4 Choosing estimands and reducing “DAG anxiety” In step 2 of the causal inference workflow, we specified our causal assumptions about the domain as a DAG (or SCM or other causal model). The subsequent steps all rely on the assumptions we make in step 2. Errors in step 2 can lead to errors in the results of the analysis, and while we can empirically test these assumptions to some extent (e.g., using the methods in chapter 4), we cannot verify all our causal assumptions with observational data alone. This dependence on our subjective and unverified causal assumptions leads to what I call “DAG anxiety”—a fear that if one gets any part of the causal assumptions wrong, then the output of the analysis becomes wrong. Fortunately, we don’t need to get all the assumptions right; we only need to rely on the assumptions required to identify our selected estimand. This is what makes DoWhy’s identify_effect method so powerful. By showing us the assumptions required for each estimand it lists, we can compare these assumptions and target the estimand where we
第十二章:因果决策与强化学习
本章涵盖
-
使用因果模型来自动化决策
-
设置因果伯努利算法
-
如何将因果关系融入强化学习
当我们应用来自统计学和机器学习的方法时,通常是为了做出决策或自动化决策。自动化决策算法,如 伯努利 和 强化学习 (RL) 算法,涉及学习如何做出良好决策的代理。在这两种情况下,决策本质上是一个因果问题:采取某些行动的决策会导致后果,目标是选择对决策者有利的后果。这促使我们采用因果框架。
通常,从行动到后果的路径具有一定的随机性。例如,你选择如何玩一副扑克牌可能是最优的,但你仍然可能因为运气而输掉。这促使我们采用概率建模方法。
我们在这本书中迄今为止使用的因果概率建模方法是一块同时击中这两只鸟的石头。本章将提供一个 因果优先 的介绍,介绍统计决策理论、顺序决策、伯努利和 RL 的基本思想。这里的“因果优先”意味着我将使用我们在前几章中建立的基础,以因果的角度介绍这些思想。我还会以与我们的概率机器学习框架兼容的方式呈现这些思想。即使你已经熟悉这些决策和 RL 概念,我也鼓励你继续阅读,并通过因果的视角再次审视它们。一旦我们这样做,我们就会看到因果方法在 RL 中比非因果方法得到更好的结果的情况。
12.1 决策理论的因果入门
决策理论关注的是代理选择某些行动路线背后的推理。这里的“代理”是一个选择行动的实体。
例如,假设你正在决定是否通过购买股权或购买债务(即向公司贷款并收取利息)来投资一家公司。我们将这个变量称为 X。公司是否成功 (Y) 取决于它所获得的投资类型。

图 12.1 一个简单的因果 DAG,其中行动 X 导致某些结果 Y。决策理论是一个因果问题,因为如果决定采取的行动没有因果后果,那么做出决策的意义何在?
由于 X 因果地驱动 Y,我们可以立即引入一个因果 DAG,如图 12.1 所示。
我们将使用这个例子来从因果的角度说明决策理论的基本概念。
12.1.1 效用、奖励、损失和成本
代理通常选择那些能让他们获得一些效用(或最小化一些损失)的行动。在决策建模中,你可以定义一个效用函数(也称为奖励函数),它量化了决策各种结果的吸引力。假设你投资了$1,000:
-
如果公司变得成功,你将得到$100,000。你的效用是 100,000 – 1,000 = $99,000。
-
如果公司失败,你将得到$0 并损失你的投资。你的效用是 –1,000。
我们可以在图上添加这个效用节点,如图 12.2 所示。

图 12.2 效用节点可以代表效用/奖励,损失/成本。
注意,在这个模型中,效用是 Y 的确定性函数,我们将用 U(Y) 表示。

而不是效用/奖励函数,我们可以定义一个损失函数(也称为成本函数),它只是效用/奖励函数的-1 倍。例如,在第二个情景中,你购买股票而公司失败的情况下,你的效用是 –$1,000,你的损失是$1,000。
虽然代理的目标是决定一条将最大化效用的行动路线,但这样做是有挑战性的,因为通常在某个行动是否会导致期望的结果方面存在一些不确定性。在我们的例子中,投资股票可能看起来很明显,因为股票将导致商业成功,而商业成功无疑将导致更多的效用。但是,股票投资是否会导致商业成功存在一些不确定性。换句话说,我们不假设 P(Y=success|X=equity) = 1。在 P(Y|X=equity) 中,成功和失败都有非零的概率。
12.1.2 不确定性来自其他原因
在因果术语中,给定行动 X,结果 Y 仍然存在一些不确定性,因为还有其他因果因素在推动这个结果。例如,假设商业的成功取决于经济条件,如图 12.3 所示。

图 12.3 我们在决策中通常存在不确定性。从因果的角度来看,不确定性是由于我们无法控制的其他因果因素影响我们行动下游的变量。
或者,那些其他因果因素可能直接影响效用。例如,而不是我为我们商业投资概述的两个离散情景——盈利或亏损,效用(或损失)可能取决于经济表现的好坏,如图 12.4 所示。我们可以利用统计和概率建模来应对这种不确定性。

图 12.4 中,我们无法控制的因素可能直接影响效用(或损失)。
假设你在考虑是否要投资这个业务。你希望你的决策是基于数据的,因此你研究了这个市场中其他投资者之前都做了什么。你考虑了图 12.5 中的因果 DAG。

图 12.5 在这个 DAG 中,经济条件驱动投资者选择如何投资。
根据你的研究,你得出结论,过去投资者的股票与债务选择也取决于经济条件。P(X|C)代表一个行动分布——你正在研究的投资者群体采取的行动分布。
然而,你分析的目标是围绕你自己,而不是其他投资者。你想要回答像“如果我买股票会怎样?”这样的问题。这个问题让我们进入了因果领域。我们不是在推理观察到的投资趋势;我们是在推理条件假设。这是一个我们需要引入基于干预的推理和反事实记号的指标。
12.2 因果决策理论
在本节中,我们将强调决策作为因果查询,并探讨这对建模决策意味着什么。
12.2.1 决策作为第二层查询
因果决策建模的混淆来源之一是行动和干预之间的区别。在许多决策情境中,特别是在强化学习(RL)中,行动是代理人做的事情,它改变了他们的环境。然而,行动也是由环境驱动的变量。当我们查看投资例子时,我们会看到这一点,如图 12.6 所示。

图 12.6 在这个投资 DAG 版本中,行动的选择是由外部因素引起的。
选择股票或债券的行为是由经济因素因果驱动的变量。这意味着什么?这个行为是一个有原因的变量,还是一个干预措施?
答案是两者都是,这取决于上下文。当我们谈论通常会发生什么行为,例如当我们观察其他代理人的行为(甚至当我们反思我们自己的过去行为)以及这些行为导致了什么结果时,我们正在反思人口趋势,我们处于因果层次的第一层。在我们的投资例子中,我们正在推理P(C, X, Y, U)。但如果我们问的问题是“如果我进行股票投资会发生什么?”那么我们就是在问一个第二层的问题,我们需要将提议的行为作为干预措施。
接下来,我们将使用我们的因果符号来描述常见的决策规则。
12.2.2 决策规则和策略的因果特征
决策规则是基于效用分布P(U(Y[X][=][x]))选择行动的规则。代理人根据决策规则选择最优行动。例如,一个常见的决策规则是选择最小化损失或成本或最大化效用或奖励的行动。
在自动决策中,决策规则通常被称为“策略”。在公共卫生环境中,决策规则有时被称为“治疗方案”。
最大化预期效用
最直观且最常见的决策规则是选择最大化预期效用的行动。首先,我们可以查看效用分布的期望值。由于效用是 Y[X][=][x] 的确定性函数,这仅仅是 U(Y[X][=][x]) 在 Y 的干预分布上的期望。

我们然后选择最大化预期效用的行动(x 的值):

在我们的投资示例中,这意味着选择预期能让你赚最多钱的投资方法。
最小-最大决策规则
要理解最小-最大决策规则,请记住,“效用”和“损失”是同一枚硬币的两面;效用 == 负损失。设 L(y) = –U(y)。那么最小-最大决策规则是

用简单的话说,这意味着“选择一个行动,以最小化可能的最大损失。”在我们的投资示例中,这意味着选择一个投资方法,以最小化在最坏情况下的损失。存在许多最小-最大规则变体,但它们有相同的味道——在困难时期最小化损失或最大化效用。
Softmax 规则
softmax 决策规则随机选择一个行动,其概率与结果的效用成比例。
让我们定义 C(x) 为选择行动 x 的概率。那么 C(x) 被定义为与以下成比例的概率值

噪声参数 α 在两种极端之间调节。当 α=0 时,我们在所有选择上都有均匀分布。随着 α 的增大,我们接近最大化预期效用。
有时我们的目标是模拟其他智能体的决策,例如在逆强化学习(inverse RL)中。当智能体不总是做出效用优化的选择时,softmax 决策规则很有用。softmax 决策规则提供了一个简单、可分析的、经验验证的次优选择模型。
另一个我们可能想要使用 softmax 规则的原因是在 探索 和 利用 之间存在权衡,例如在老丨虎丨机问题中。假设智能体对分布 P(Y[X][=][x]) 的形状不确定。根据 P(Y[X][=][x]) 的错误模型所采取的最优行动可能与根据正确模型 P(Y[X][=][x]) 所做的最优选择不同。softmax 决策规则允许我们选择各种行动,获取一些关于结果的数据,并使用这些数据来更新我们对 P(Y[X][=][x]) 的模型。当这些操作按顺序进行时,通常被称为 汤普森抽样。
在我们的投资类比中,假设我们要投资几个企业。也许,根据我们的当前模型,股权投资最大化预期效用,但我们对我们当前模型并不完全自信,所以我们选择选择债务投资,尽管当前模型表示它不太优。目标是增加我们的数据集的多样性,这样我们就可以学习更好的模型。
其他类型的决策规则
还有其他类型的决策规则,并且它们可能会变得复杂,尤其是在涉及统计估计时。例如,在统计假设检验中使用p值涉及一个微妙的效用函数,它平衡了假阳性(错误地选择备择假设)和假阴性(错误地选择零假设)的机会。
幸运的是,当我们与概率因果模型一起工作时,数学通常更容易,我们得到一个称为可接受性的保证。
12.2.3 因果概率决策建模和可接受性
在本节中,我将简要说明选择因果概率建模方法进行决策的理由。当你在一个生产环境中实施自动化决策算法时,你可能需要解释为什么你的实现比另一个更好。在这种情况下,如果你知道你的算法是可接受的,那将是有用的。
如果没有其他规则支配它,则决策规则是可接受的。一个决策规则支配另一个规则,如果前者的性能在某些情况下比后者好,并且永远不会比后者差,相对于效用函数而言。例如,softmax 决策规则被最大化预期效用所支配(假设你知道P(Y**[X][=][x])的真实形状),因为它有时会选择次优行动,因此是不可接受的。确定可接受性是决策理论中的一个关键任务。
对于我们来说,挑战出现在我们使用数据和统计来处理未知情况时,例如参数或潜在变量。如果我们想使用数据来估计一个参数或处理潜在变量,通常有多种统计方法可供选择。如果我们的决策算法依赖于一个统计过程,过程的选择可能会影响被认为最优的行动。我们如何知道我们的统计决策过程是可接受的?
概率建模库如 Pyro 利用贝叶斯推理来估计参数或推断潜在变量。贝叶斯决策理论告诉我们,贝叶斯规则(不要与贝叶斯定理混淆),即优化后验期望效用的决策规则,在温和的正规性条件下具有可接受性保证。这意味着如果我们使用 Pyro 或类似库中的贝叶斯推理来计算和优化后验期望损失,我们就有可接受性保证(如果这些温和的条件成立,通常都是成立的)。这意味着你不必担心别人的决策模型(做出相同的建模假设,具有相同的效用函数,并使用相同的数据)会打败你。
12.2.4 因果和非因果期望的 argmax 值的欺骗性对齐
大多数传统的决策方法,包括在强化学习(RL)中,都侧重于最大化 E(U(Y)|X=x) 而不是 E(U(Y[X][=][x])). 让我们使用 pgmpy 在图 12.6 中实现模型,并比较两种方法。
首先,我们将构建模型中的 DAG。
设置你的环境
这段代码是用 pgmpy 版本 0.1.24 编写的。有关运行此代码的笔记本链接,请参阅章节注释 www.altdeep.ai/p/causalaibook。
列表 12.1 投资决策模型的 DAG
from pgmpy.models import BayesianNetwork
from pgmpy.factors.discrete import TabularCPD
from pgmpy.inference import VariableElimination
import numpy as np
model = BayesianNetwork([ #1
('C', 'X'), #1
('C', 'Y'), #1
('X', 'Y'), #1
('Y', 'U') #1
]) #1
1 设置 DAG
接下来,我们将为 Economy (C)、Debt vs. Equity (X) 和 Business Success (Y) 构建因果马尔可夫核。Economy (C) 的因果马尔可夫核将取两个值:“熊”代表不良经济状况,“牛”代表良好。Debt vs. Equity (X) 的因果马尔可夫核将取决于 C,反映投资者在牛市倾向于偏好股权,在熊市倾向于偏好债务的事实。Success (Y) 取决于经济状况和债务或股权投资的选择。
列表 12.2 创建 C、X 和 Y 的因果马尔可夫核
cpd_c = TabularCPD( #1
variable='C', #1
variable_card=2, #1
values=[[0.5], [0.5]], #1
state_names={'C': ['bear', 'bull']} #1
) #1
cpd_x = TabularCPD( #2
variable='X', #2
variable_card=2, #2
values=[[0.8, 0.2], [0.2, 0.8]], #2
evidence=['C'], #2
evidence_card=[2], #2
state_names={'X': ['debt', 'equity'], 'C': ['bear', 'bull']} #2
) #2
cpd_y = TabularCPD( #3
variable='Y', #3
variable_card=2, #3
values= [[0.3, 0.9, 0.7, 0.6], [0.7, 0.1, 0.3, 0.4]], #3
evidence=['X', 'C'], #3
evidence_card=[2, 2], #3
state_names={ #3
'Y': ['failure', 'success'], #3
'X': ['debt', 'equity'], #3
'C': ['bear', 'bull'] #3
} #3
) #3
1 为 C(经济)设置因果马尔可夫核。它有两个值:“牛”代表良好经济状况,“熊”代表不良。
2 为行动 X 设置因果马尔可夫核,根据经济状况,要么是债务投资,要么是股权投资。
3 为业务结果 Y 设置因果马尔可夫核,成功或失败取决于提供的投资类型(X)和经济状况(C)。
最后,我们将添加 Utility 节点 (U)。我们使用概率 1 和 0 来表示 Y 的确定性函数。我们通过添加所有核到模型中结束。
列表 12.3 实现效用节点并初始化模型
cpd_u = TabularCPD( #1
variable='U', #1
variable_card=2, #1
values=[[1., 0.], [0., 1.]], #1
evidence=['Y'], #1
evidence_card=[2], #1
state_names={'U': [-1000, 99000], 'Y': ['failure', 'success']} #1
) #1
print(cpd_u) #1
model.add_cpds(cpd_c, cpd_x, cpd_y, cpd_u) #1
1 设置效用节点。
2 设置效用节点。
此代码打印出以下因果马尔可夫核的条件概率表。这是针对 Utility 变量的:
+----------+------------+------------+
| Y | Y(failure) | Y(success) |
+----------+------------+------------+
| U(-1000) | 1.0 | 0.0 |
+----------+------------+------------+
| U(99000) | 0.0 | 1.0 |
+----------+------------+------------+
这反映了投资者在牛市倾向于偏好股权投资,在熊市倾向于偏好债务投资的趋势。
下面的概率表是针对 Business Success 变量 Y 的:
+------------+---------+---------+-----------+-----------+
| X | X(debt) | X(debt) | X(equity) | X(equity) |
+------------+---------+---------+-----------+-----------+
| C | C(bear) | C(bull) | C(bear) | C(bull) |
+------------+---------+---------+-----------+-----------+
| Y(failure) | 0.3 | 0.9 | 0.7 | 0.6 |
+------------+---------+---------+-----------+-----------+
| Y(success) | 0.7 | 0.1 | 0.3 | 0.4 |
+------------+---------+---------+-----------+-----------+
这反映了在熊市中,由于利率支付较高,债务融资不如债券融资受欢迎,而在牛市中,由于债券更便宜,债券融资更受欢迎。
最后,效用节点是一个简单的确定性函数,将 Y 映射到效用值:
+----------+------------+------------+
| Y | Y(failure) | Y(success) |
+----------+------------+------------+
| U(-1000) | 1.0 | 0.0 |
+----------+------------+------------+
| U(99000) | 0.0 | 1.0 |
+----------+------------+------------+
接下来,我们将计算 E(U(Y[X][=][x])) 和 E(U(Y)|X=x). 在继续之前,下载并加载一个实现理想干预的辅助函数。为了缓解直接执行下载代码可能带来的安全担忧,代码会打印下载的脚本并提示您在执行脚本之前确认。*
列表 12.4 下载实现理想干预的辅助函数
import requests
url = "https://raw.githubusercontent.com/altdeep/causalML/master/book/pgmpy_do.py" #1
response = requests.get(url) #1
content = response.text #1
print("Downloaded script content:\n") #2
print(content) #2
confirm = input("\nDo you want to execute this script? (yes/no): ") #2
if confirm.lower() == 'yes': #2
exec(content) #2
else: #2
print("Script execution cancelled.") #2
1 加载理想干预的实现。
2 为了缓解安全担忧,您可以检查下载的脚本并在运行之前确认。
到现在为止,在这本书中,您不应该对 E(U(Y[X][=][x])) 与 E(U(Y)|X=x) 不同感到惊讶。让我们看看这些值。
列表 12.5 计算 E(U(Y)|X=x) 和 E(U(Y**[X][=][x]))
def get_expectation(marginal): #1
u_values = marginal.state_names["U"] #1
probs = marginal.values #1
expectation = sum([x * p for x, p in zip(u_values, probs)]) #1
return expectation #1
infer = VariableElimination(model) #2
marginal_u_given_debt = infer.query( #2
variables=['U'], evidence={'X': 'debt'}) #2
marginal_u_given_equity = infer.query( #2
variables=['U'], evidence={'X': 'equity'}) #2
e_u_given_x_debt = get_expectation(marginal_u_given_debt) #2
e_u_given_x_equity = get_expectation(marginal_u_given_equity) #2
print("E(U(Y)|X=debt)=", e_u_given_x_debt) #2
print("E(U(Y)|X=equity)=", e_u_given_x_equity) #2
int_model_x_debt = do(model, {"X": "debt"}) #3
infer_debt = VariableElimination(int_model_x_debt) #3
marginal_u_given_debt = infer_debt.query(variables=['U']) #3
expectation_u_given_debt = get_expectation(marginal_u_given_debt) #3
print("E(U(Y_{X=debt}))=", expectation_u_given_debt) #3
int_model_x_equity = do(model, {"X": "equity"}) #3
infer_equity = VariableElimination(int_model_x_equity) #3
marginal_u_given_equity = infer_equity.query(variables=['U']) #3
expectation_u_given_equity = get_expectation(marginal_u_given_equity) #3
print("E(U(Y_{X=equity}))=", expectation_u_given_equity) #3
1 用于计算预期效用的辅助函数
2 通过干预将 X 设置为债务和债券,并计算每个干预下的 U 的期望值。
3 在 X = debt 和 X = equity 的条件下,计算 U 的期望值。
这给我们以下条件预期效用(我已经用 * 标记了最高的):
-
E(U(Y)|X=debt) = 57000 *
-
E(U(Y)|X=equity) = 37000
它还给我们以下干预预期效用:
-
E(U(Y[X][=debt])) = 39000 *
-
E(U(Y[X][=equity])) = 34000
因此,E(U(Y)|X=debt) 与 E(U(Y[X][=debt])) 不同,E(U(Y)|X=equity) 与 E(U(Y[X][=][equity])) 不同。然而,我们的目标是优化预期效用,在这种情况下,债务最大化了 E(U(Y)|X=x) 和 E(U(Y[X][=][x]))。

如果“债务”最大化了两个查询,因果决策理论有什么用?如果最优行动对于两者都是相同的,E(U(Y)|X=x) 和 E(U(Y[X][=][x])) 是否不同又有什么关系?
在决策问题中,通常情况下,问题的因果表述与更传统的非因果表述提供相同的答案。这在 RL 中常见的更高维问题中尤其如此。您可能会观察到这一点,并想知道为什么因果表述是必需的。
为了回答这个问题,观察当我们对模型中 Y 的参数进行轻微修改时会发生什么。具体来说,我们将把 P(Y=success|X=equity, C=bull) 的参数从 .4 改为 .6。首先,我们将使用参数更改重建模型。
列表 12.6 修改 Y 的因果马尔可夫核中的参数
model2 = BayesianNetwork([ #1
('C', 'X'), #1
('C', 'Y'), #1
('X', 'Y'), #1
('Y', 'U') #1
])
cpd_y2 = TabularCPD( #2
variable='Y',
variable_card=2,
values=[[0.3, 0.9, 0.7, 0.4], [0.7, 0.1, 0.3, 0.6]], #3
evidence=['X', 'C'],
evidence_card=[2, 2],
state_names={
'Y': ['failure', 'success'],
'X': ['debt', 'equity'],
'C': ['bear', 'bull']
}
)
model2.add_cpds(cpd_c, cpd_x, cpd_y2, cpd_u) #4
1 初始化一个新的模型。
2 为 Y 创建一个新的条件概率分布。
3 将第一个列表中的最后一个参数 P(Y=success|X=equity, C=bull) = 0.4 改为 0.6。
4 将因果马尔可夫核添加到模型中。
接下来,我们重新运行推理。
列表 12.7 比较改变参数后的结果
infer = VariableElimination(model2) #1
marginal_u_given_debt = infer.query(variables=['U'], #1
↪evidence={'X': 'debt'}) #1
marginal_u_given_equity = infer.query(variables=['U'], #1
↪evidence={'X': 'equity'}) #1
e_u_given_x_debt = get_expectation(marginal_u_given_debt) #1
e_u_given_x_equity = get_expectation(marginal_u_given_equity) #1
print("E(U(Y)|X=debt)=", e_u_given_x_debt) #1
print("E(U(Y)|X=equity)=", e_u_given_x_equity) #1
int_model_x_debt = do(model2, {"X": "debt"}) #2
infer_debt = VariableElimination(int_model_x_debt) #2
marginal_u_given_debt = infer_debt.query(variables=['U']) #2
expectation_u_given_debt = get_expectation(marginal_u_given_debt) #2
print("E(U(Y_{X=debt}))=", expectation_u_given_debt) #2
int_model_x_equity = do(model2, {"X": "equity"}) #2
infer_equity = VariableElimination(int_model_x_equity) #2
marginal_u_given_equity = infer_equity.query(variables=['U']) #2
expectation_u_given_equity = get_expectation(marginal_u_given_equity) #2
print("E(U(Y_{X=equity}))=", expectation_u_given_equity) #2
1 通过干预将 X 设置为 debt 和 equity,并计算每种干预下的 U 的期望。
2 在 X = debt 和 X = equity 的条件下进行条件化,并计算 U 的期望。
这给我们以下条件期望(*表示最优选择):
-
E(U(Y)|X=debt) = 57000 *
-
E(U(Y)|X=equity) = 53000
它还给我们以下干预期望:
-
E(U(Y[X][=debt])) = 39000
-
E(U(Y[X][=equity])) = 44000 *
在单个参数的微小变化下,“债务”仍然是 E(U(Y)|X=x) 中 x 的最优值,但现在“股权”是 E(U(Y[X][=][x])) 中 x 的最优值。这是一个因果答案与基于证据的条件答案不同的情况。由于我们正在尝试回答一个第二级查询,因果方法是正确的方法。
这意味着虽然简单地优化条件期望通常可以得到正确答案,但在某些情况下,你可能会得到错误答案。这与我们在第四章中关于半监督学习的讨论类似——通常未标记数据可以帮助学习,但在特定情况下,未标记数据不会增加任何价值。因果分析帮助我们以精确的术语描述了这些情况。同样,在这种情况下,有特定的场景,因果问题的因果表述相对于传统的非因果表述将导致不同的、更正确的结果。即使是包括在深度强化学习中使用的基于深度学习的最流行的决策优化算法,也可以通过利用决策问题的因果结构来提高性能。
接下来,我们将看到另一个 Newcomb 悖论的例子。
12.2.5 Newcomb 悖论
一个著名的思想实验,称为 Newcomb 悖论,对比了决策理论中的因果方法,即在干预下最大化效用,与传统的在某种行动条件下最大化效用的方法。在本节中,我们将探讨这个思想实验的 AI 版本,下一节将展示如何使用形式化的因果模型来处理它。
如图 12.7 所示,有两个标记为 A 和 B 的盒子。盒子 A 总是包含$1,000。盒子 B 包含$1,000,000 或$0。决策代理必须选择只拿盒子 B 或两个盒子。代理在做出决定之前不知道盒子 B 里有什么。根据这个信息,很明显代理应该拿两个盒子——选择两个盒子可以得到$1,000 或$1,001,000,而只选择 B 可以得到$0 或$1,000,000。

图 12.7 Newcomb 悖论中盒子的示意图
现在,假设有一个 AI 可以高精度地预测代理打算做出的选择。如果 AI 预测代理打算选择两个盒子,它将不会在盒子 B 中放钱。如果 AI 预测正确,代理选择两个盒子,代理只能得到 $1,000。然而,如果 AI 预测代理打算只选择盒子 B,它将在盒子 B 中放入 $1,000,000。如果 AI 预测正确,代理将在盒子 B 中得到 $1,000,000,但不会在盒子 A 中得到 $1,000。代理在做出选择之前不知道 AI 的预测或盒子 B 中有什么。
传统悖论的产生如下。一个因果论者认为,AI 的行为超出了他们的控制。他们只关注他们能控制的部分——他们选择因果后果。他们不能 造成 盒子 B 的内容,所以他们选择两个盒子,以备万一盒子 B 有钱,就像 AI 不存在时一样。但如果代理知道 AI 的工作原理,选择只选择盒子 B 并确定性地获得一百万不是更有意义吗?
通过列举可能的结果及其概率来进一步深入探讨。假设 AI 的预测准确率为 95%。如果代理选择两个盒子,那么 AI 有 95% 的可能性猜对了代理的选择,将没有钱放入 B,在这种情况下,代理只能得到 $1,000。有 5% 的可能性算法会猜错,在这种情况下,它会将 1,000,000 放入盒子 B,代理赢得 $1,001,000。如果代理只选择盒子 B,那么 AI 有 95% 的可能性预测了选择,并将 1,000,000 放入盒子 B,给代理 $1,000,000 的奖金。有 5% 的可能性它不会这样做,代理将一无所获。我们在表 12.1 中看到这些结果。预期效用计算显示在表 12.2 中。
表 12.1 新科姆问题的结果及其概率
| 策略 | AI 行动 | 奖金 | 概率 |
|---|---|---|---|
| 选择两者 | 将 $0 放入盒子 B | $1,000 | .95 | |
| 选择两者 | 将 $1,000,000 放入盒子 B | $1,001,000 | .05 | |
| 只选择盒子 B | 将 $1,000,000 放入盒子 B | $1,000,000 | .95 | |
| 只选择盒子 B | 将 $0 放入盒子 B | $0 | .05 |
表 12.2 新科姆问题中每个选择的预期效用
| 策略 ( x) | E( U| X= x) |
|---|---|
| 选择两者 | 1,000 × .95 + 1,001,000 × .05 = $51,000 |
| 只选择盒子 B | 1,000,000 × .05 + 0 × .05 = $950,000 |
传统方法建议只选择盒子 B。
当悖论被创造出来时,对问题采取因果方法意味着只关注自己行为的因果后果。记住,AI 在代理行动之前做出预测。由于效果不能在时间上先于原因,AI 的行为不是代理行为的后果,因此具有因果观点的代理忽略 AI,并坚持选择两个盒子的原始策略。
看起来,具有因果观点的代理在未能考虑人工智能的行动时犯了错误。但我们可以通过让代理使用正式的因果模型来解决这个问题。
12.2.6 带有因果模型的 Newcomb 悖论
在 Newcomb 悖论的传统表述中,假设是使用因果决策理论的代理只关注他们行动的后果——他们在图 12.8 中的因果有向图中进行推理。但真实的数据生成过程(DGP)被图 12.9 更好地捕捉。

图 12.8 Newcomb 悖论假设了一种因果决策理论版本,其中天真代理使用这个错误的因果有向图。

图 12.9 代表 Newcomb 悖论框架的更好的因果有向图
代理的选择不能 cause 人工智能的预测,因为预测先发生。因此,我们假设人工智能代理正在推断代理的 intent,因此代理的意图是人工智能预测的原因。
因果决策代理会更喜欢图 12.9 中的图,因为它更好地代表了 DGP。聪明的代理不会专注于最大化 E(U[choice][=][x])。聪明的代理意识到自己的意图,并知道这种意图是盒子 B 内容的原因,它专注于优化 E(U[choice][=][x]|intent=i),其中 i 是他们最初选择哪个盒子的原始意图。

我们将假设代理的初始意图是一种它无法控制的冲动。但尽管他们无法控制他们的初始意图,他们可以进行一些内省并意识到这种意图。进一步,我们将假设,在这样做之后,他们有能力在人工智能做出预测并设置盒子 B 的内容之后,将他们的选择改变为与最初意图不同的东西。让我们在 pgmpy 中模拟这个系统并评估最大化 E(U[choice][=][x]|intent=i)。
首先,让我们构建 DAG。
列表 12.8 创建 DAG
model = BayesianNetwork(
[
('intent', 'AI prediction'),
('intent', 'choice'),
('AI prediction', 'box B'),
('choice', 'U'),
('box B', 'U'),
]
)
接下来,我们将为意图和选择创建因果马尔可夫内核。
列表 12.9 创建意图和选择的因果马尔可夫内核
cpd_intent = TabularCPD( #1
'intent', 2, [[0.5], [0.5]], #1
state_names={'intent': ['B', 'both']} #1
) #1
print(cpd_intent)
cpd_choice = TabularCPD( #2
'choice', 2, [[1, 0], [0, 1]], #2
evidence=['intent'], #2
evidence_card=[2], #2
state_names={ #2
'choice': ['B', 'both'], #2
'intent': ['B', 'both'] #2
} #2
) #2
print(cpd_choice)
1 我们假设代理将更喜欢两个盒子而不是盒子 B 的概率为 50-50。
2 我们假设代理的选择是由他们的意图决定性地驱动的。
同样,我们将为人工智能的决策和盒子 B 的内容创建因果马尔可夫内核。
列表 12.10 创建人工智能预测和盒子 B 内容的因果马尔可夫内核
cpd_AI = TabularCPD( #1
'AI prediction', 2, [[.95, 0.05], [.05, .95]], #1
evidence=['intent'], #1
evidence_card=[2], #1
state_names={ #1
'AI prediction': ['B', 'both'], #1
'intent': ['B', 'both'] #1
} #1
) #1
print(cpd_AI)
cpd_box_b_content = TabularCPD( #2
'box B', 2, [[0, 1], [1, 0]], #2
evidence=['AI prediction'], #2
evidence_card=[2], #2
state_names={ #2
'box B': [0, 1000000], #2
'AI prediction': ['B', 'both'] #2
} #2
) #2
print(cpd_box_b_content)
1 人工智能的预测准确率为 95%。
2 盒子 B 的内容是由人工智能的预测决定性地设置的。
最后,我们将为效用创建一个因果马尔可夫内核,并将所有内核添加到模型中。
列表 12.11 创建效用内核并构建模型
cpd_u = TabularCPD( #1
'U', 4, #1
[ #1
[1, 0, 0, 0], #1
[0, 1, 0, 0], #1
[0, 0, 1, 0], #1
[0, 0, 0, 1], #1
], #1
evidence=['box B', 'choice'], #1
evidence_card=[2, 2], #1
state_names={ #1
'U': [0, 1000, 1000000, 1001000], #1
'box B': [0, 1000000], #1
'choice': ['B', 'both'] #1
} #1
) #1
print(cpd_u)
model.add_cpds(cpd_intent, cpd_choice, cpd_AI, cpd_box_b_content, cpd_u) #2
1 设置效用节点。
2 构建模型。
现在,我们将评估最大化 E(U[choice][=][x]|intent=i)。
列表 12.12 使用干预和基于意图的条件推断最佳选择
int_model_x_both = do(model, {"choice": "both"}) #1
infer_both = VariableElimination(int_model_x_both) #1
marginal_u_given_both = infer_both.query( #1
variables=['U'], evidence={'intent': 'both'}) #1
expectation_u_given_both = get_expectation(marginal_u_given_both) #1
print("E(U(Y_{choice=both}|intent=both))=", expectation_u_given_both) #1
int_model_x_box_B = do(model, {"choice": "B"}) #2
infer_box_B = VariableElimination(int_model_x_box_B) #2
marginal_u_given_box_B = infer_box_B.query( #2
variables=['U'], evidence={'intent': 'both'}) #2
expectation_u_given_box_B = get_expectation(marginal_u_given_box_B) #2
print("E(U(Y_{choice=box B}|intent=both))=", expectation_u_given_box_B) #2
int_model_x_both = do(model, {"choice": "both"}) #3
infer_both = VariableElimination(int_model_x_both) #3
marginal_u_given_both = infer_both.query( #3
variables=['U'], evidence={'intent': 'B'}) #3
expectation_u_given_both = get_expectation(marginal_u_given_both) #3
print("E(U(Y_{choice=both}|intent=B))=", expectation_u_given_both) #3
int_model_x_box_B = do(model, {"choice": "B"}) #4
infer_box_B = VariableElimination(int_model_x_box_B) #4
marginal_u_given_box_B = infer_box_B.query( #4
variables=['U'], evidence={'intent': 'B'}) #4
expectation_u_given_box_B = get_expectation(marginal_u_given_box_B) #4
print("E(U(Y_{choice=box B}|intent=B))=", expectation_u_given_box_B) #4
1 推断 E(U(Y [选择=两个]|意图=两个))。
2 推断 E(U(Y [选择=盒子 B]|意图=两个))。
3 推断 E(U(Y [选择=两个]|意图=B))。
4 推断 E(U(Y [选择=盒子 B]|意图=B))。
这段代码产生了以下结果(*表示给定意图的最优选择):
-
E(U(Y[选择][=两个]|意图=两个)) = 51000 *
-
E(U(Y[选择][=盒子 B]|意图=两个)) = 50000
-
E(U(Y[选择][=两个]|意图=B)) = 951000 *
-
E(U(Y[选择][=盒子 B]|意图=B)) = 950000
当代理的初始意图是选择两个时,最佳选择是选择两个。当代理意图只选择盒子 B 时,最佳选择是忽略那些意图并选择两个。无论如何,代理都应该选择两个。请注意,当代理最初意图只选择盒子 B 时,切换到两个盒子给了他们$951,000 的预期效用,这比非因果方法中最佳选择效用$950,000 要高。
然而,代理无法控制他们的初始意图;如果他们能,他们会故意‘意图’选择盒子 B,然后在最后时刻切换到选择两个盒子,在 AI 将一百万放入盒子 B 之后。然而,他们可以进行一种内省,将他们的初始意图纳入他们的决策中,并在此过程中,考虑到 AI 的行为而不是忽略它。
12.2.7 因果决策理论中的内省
新问题说明了因果决策理论的关键能力——我们能够将内省包括在 DGP 中。

图 12.10 我们的行为通常是环境反应,而不是故意决策的结果。
为了说明,考虑我们的行为通常是环境反应,就像图 12.10 所示。
例如,你可能会因为饥饿而购买一盒巧克力,因为它在你等待在杂货店的结账通道时被放置在诱惑你的位置。你不会经历一些深思熟虑的决策过程,而是对你的渴望有一个简单、甚至可能是无意识的反应,并有一种简单的方法来满足它。
然而,人类能够进行内省——观察和思考他们的内部状态。一个人可能会将他们的正常反应行为视为 DGP 的一部分。这种内省在图 12.11 中得到了说明。

图 12.11 人类和一些其他代理可以思考一个包括他们作为该过程组成部分的 DGP。
通过这种内省,代理可以对以下情况进行 2 级分层推理:如果他们不按常规反应,而是故意行动(例如,坚持饮食并不要买巧克力棒),如图 12.12 所示。

图 12.12 代理对包括他们作为组成部分的 DGP 进行推理。然后他们使用这种推理来询问关于该过程的 2 级“如果...会发生什么”问题。
在许多情况下,代理可能不知道他们环境的完整状态。然而,如果代理能够将他们想要以某种方式反应的冲动与他们的行为分开,他们可以使用这种“冲动”作为深思熟虑的决策的证据,如图 12.13 所示。

图 12.13 代理可能不知道环境中其他变量的状态,但通过内省,他们可能对这些变量有一种直觉。这种直觉可以用作条件因果推断的证据。
我们在 Newcomb 例子中看到了这种模式;代理不知道 AI 的预测是什么,但通过内省,他们可以使用他们的初始意图选择两个盒子作为 AI 选择的证据。
你有没有注意到你在工作中开始犯一些笨拙的错误,并以此作为你疲劳的证据,尽管你并不觉得如此,你心想,“如果我休息一下会怎样?” 你有没有一种直觉,觉得某件事不对劲,尽管你不知道是什么,基于这种感觉开始做出不同的决策?因果建模,尤其是因果生成模型,使得编写能够捕捉这种自我内省的决策算法变得容易。
接下来,我们将探讨顺序决策的因果建模。
12.3 因果图和顺序决策
顺序决策过程是连续决策的过程。这些过程可能涉及人类或算法和工程代理做出的顺序决策。
当我按顺序建模决策过程时,我使用下标来表示序列中的离散步骤,例如 Y[1],Y[2],Y[3]。当我想要表示干预下标时,我将它放在时间步下标的右侧,例如 Y[1,][X][=][x],Y[2,][X][=][x],Y[3,][X][=][x]。
在本节中,我将展示几个典型顺序决策过程的因果图,但你应该将这些视为模板,而不是固定结构。你可以根据给定问题适当添加或删除边。
让我们看看最简单的情况,即投币机反馈。
12.3.1 投币机反馈
投币机反馈指的是在序列中的每个步骤,都有一个导致结果 Y 的行为 X,并伴随一些效用 U(Y)。投币机序列有两个关键特征。第一个特征是,在每个步骤,行为发生后立即有反馈。第二个特征是独立试验,意味着在 t^(th) 时间步的变量与其他时间步的变量是独立的。术语“投币机”来自对“单臂老丨虎丨机”的类比,这是一个用于赌场的老丨虎丨机的俚语,传统上有一个玩家拉动以启动游戏的手臂。老丨虎丨机游戏提供投币机反馈——你投入代币,拉动手臂,并立即知道你是否赢了或输了。这个结果与之前的游戏无关。
我们可以用图 12.14 中的因果 DAG 捕捉多臂老丨虎丨机反馈。

图 12.14 一个因果 DAG,说明了简单的多臂老丨虎丨机反馈
图 12.14 中的因果 DAG 通过每个时间步的效用节点捕捉即时反馈,并且由于缺乏边,反映了变量在时间步之间的独立性。
12.3.2 上下文多臂老丨虎丨机反馈
在上下文多臂老丨虎丨机反馈中,一个或多个变量是动作和结果的共同原因。在图 12.15 中,上下文变量 C 是序列中每个 {X, Y} 元组的共同因素。在这种情况下,上下文变量 C 可以代表某个特定个体的档案,而动作变量 X 是该用户的行为。

图 12.15 一个因果 DAG,说明了上下文多臂老丨虎丨机反馈
或者,上下文变量可以在每一步改变,如图 12.16 所示。

图 12.16 一个因果 DAG,说明了上下文多臂老丨虎丨机反馈,其中上下文在每个时间步改变
我们可以以不同的方式改变这个模板。例如,我们可以在下一个时间步中让动作驱动上下文变量,如图 12.17 所示。选择取决于你的具体问题。

图 12.17 一个因果 DAG,其中一个时间步的动作影响下一个时间步的上下文
12.3.3 延迟反馈
在延迟反馈设置中,结果变量和相应的效用不再是即时反馈。相反,它们在序列的末尾出现。让我们考虑一个上下文变量驱动动作的例子。动作影响上下文变量的下一个实例。

图 12.18 带有延迟反馈的顺序决策因果 DAG 示例
再次,图 12.18 展示了基于先前模型此方法的一个示例。在这里,时间 k 的动作影响时间 k + 1 的上下文变量 (C),这反过来又影响时间 k + 1 的动作。
考虑一个慢性疼痛的案例。在这里,上下文变量代表受试者是否正在经历疼痛 (C)。疼痛的存在驱动了服用止痛药的动作 (X)。服用止痛药(或不服用)影响下一个步骤中是否有疼痛。图 12.19 说明了这个 DAG。

图 12.19 表示慢性疼痛治疗的因果 DAG
Y 这里是受试者的最终健康结果,它既由随时间推移的总疼痛量驱动,也由受试者所服用的药物量驱动(因为可能过度使用止痛药会有害健康)。
12.3.4 对顺序模型进行因果查询
我们可能想要计算我们的顺序决策问题的一些因果查询。例如,给定图 12.19 中的 DAG,我们可能想要计算 X[0] 对 U(Y) 的因果效应:

或者,我们可能对一系列行为对 U(Y) 的因果效应感兴趣:

无论哪种方式,现在我们已经将顺序问题作为因果模型来构建,我们进入了熟悉的领域;我们可以简单地使用我们在前几章中学到的因果推断工具来使用这个模型回答因果查询。
如同往常,我们必须注意潜在原因的可能性,这些原因可能会混淆我们的因果推断。在因果效应的情况下,我们的关注点是行为 (X) 和结果 (Y) 之间,或者行为 (X) 和效用 (U) 之间的潜在共同混淆原因。图 12.20 与图 12.15 相同,除了它引入了一个潜在的 Z 混淆因子。

图 12.20 具有潜在混淆因子的上下文 Bandit
同样,我们可以在每个时间步长有一个独特的混淆因子,如图 12.21 所示。

图 12.21 每个时间步长都有不同上下文和潜在混淆因子的 Bandit 反馈
同样,图 12.22 显示了慢性疼痛图的第二个版本,其中混淆因子相互影响,并且影响上下文变量。这个混淆因子可能是影响疼痛和福祉的受试者环境中的某些外部因素。
当我们想要推断一系列行为对 U(Y) 的因果效应时,这些混淆因子成为一个问题。

图 12.22 一个慢性疼痛 DAG 的版本,其中混淆因子相互影响,并且影响上下文变量
接下来,我们将探讨如何将顺序决策过程中的自动决策策略视为随机干预。
12.4 政策作为随机干预
在自动顺序决策中,术语“策略”比“决策规则”更受欢迎。我将介绍一个用于策略的特殊符号:π(.). 它将是一个函数,它接受其他变量的观察结果并返回一个动作。
为了考虑策略如何影响模型,我们将比较实施策略前后的 DAG。图 12.23 展示了带有上下文变量 C 和潜在变量 Z 的简单示例。策略使用上下文 C 来选择 X 的值。

图 12.23 虚线表示由策略调节的边。策略像理想的干预一样打破了混淆因子 Z 的影响,但通过策略对 C 的依赖仍然存在。
策略是一种随机干预;它从依赖于 C 的某个过程中为 X 选择一个干预值。像理想的干预一样,它改变了图。图 12.23 的左侧显示了策略部署前的 DAG。右侧是策略部署后的 DAG。我在图中添加了一个特殊的策略节点来展示策略如何调节图。虚线边强调了由策略调节的边。就像理想的干预一样,策略生成的干预移除了 X 原始的输入边 C→X 和 Z→X。然而,因为策略依赖于 C,虚线边说明了从 C 到 X 的新影响流。
假设我们感兴趣的是策略选择的动作 X=Π对于值 Y 会有什么影响。在反事实记法中,我们写成

在序列设置中,策略在序列的多个步骤中应用随机干预。从可能世界视角来看,每个干预都会诱导一个新的假设世界。这可能会使反事实记法变得有些复杂,所以从现在开始,我将简化反事实记法,使其看起来像这样:

这意味着 Y[3](第 3 个时间步的 Y)受到策略在时间 0、1 和 2 的效用结果的影响。
12.4.1 序列决策中的例子
在奖励机反馈的情况下,动作由一个奖励机算法产生,这是一种将整个动作和效用结果的历史纳入决定当前最优动作的策略。尽管在每一个时间步中,奖励机反馈过程中的动作和结果都是独立的,但策略引入了对过去动作和结果的依赖,如图 12.24 所示。

图 12.24 奖励机反馈,其中奖励机策略算法根据过去的行为和奖励结果选择下一个动作
回想我们之前关于一个代理对疼痛发作做出药物反应的例子。图 12.25 显示了策略如何接受疼痛程度的历史以及提供的药物剂量。

图 12.25 在疼痛示例中,策略考虑了记录的疼痛程度和相应的药物剂量历史。
策略就像在医院病房里巡视的医生。他们来到病人的床边,病人报告了某种程度的疼痛。医生查看该病人的疼痛报告历史和随后的药物剂量,并使用这些信息来决定这次提供多少剂量。医生的效用函数是以疼痛、超量风险和成瘾风险为标准的。他们需要考虑历史数据,而不仅仅是当前的疼痛水平,以优化这个效用函数。
12.4.2 策略如何引入混杂因素
作为随机干预,策略在图中的其他节点上引入条件干预。正因为如此,策略可能会引入新的后门路径,这可能会混淆因果推断。例如,再次考虑图 12.26 中的 DAG。

图 12.26 政策消除了通过Z的后门路径,但没有消除通过C的后门路径。
政策打破了从X到Y通过Z的后门路径,但仍然存在从X到Y通过C的路径。因此,涉及X和Y的典型因果查询将不得不对C进行条件化或调整。
在下一节中,我们将用因果关系来描述因果强化学习。
12.5 因果强化学习
强化学习(RL)是机器学习的一个分支,通常涉及代理学习最大化累积奖励(效用)的策略。代理从其行动的后果中学习,而不是从被明确教导中学习,并根据它收到的奖励或损失(强化)调整其行为。许多序列决策问题都可以表示为 RL 问题。
12.5.1 因果关系与马尔可夫决策过程的连接
RL 通常将决策过程表示为马尔可夫决策过程(MDP)。一个典型的 MDP 玩具示例是网格世界,如图 12.27 所示。
图 12.27 展示了一个 3 × 4 的网格世界。代理可以在这个网格世界中执行一组固定的动作,向上、向下、向左和向右移动。代理想要执行一组动作,将其带到右上角{0, 3},在那里它获得 100 的奖励。代理想要避开中间右边的方块{1, 3},在那里它有-100 的奖励(损失 100)。位置{1, 1}包含代理无法穿越的障碍物。

图 12.27 简单的网格世界
我们可以将其视为一个游戏。当游戏开始时,代理“随机”出现在一个方块中,除了{0, 3}、{1, 3}和{1, 1}。当代理移动到目标方块时,游戏结束。为了获胜,代理必须绕过{1, 1}中的障碍物,避开{1, 3},并到达{0, 3}。
马尔可夫决策过程使用状态、动作、转移函数和奖励的抽象来模拟这个和更多复杂的“世界”(即领域、问题等)。
状态
状态集表示代理在其环境中当前所处的情况或上下文。在网格世界示例中,一个状态表示代理位于特定的单元格。在这个网格中,有 12 种不同的状态({1, 1}单元格是不可达状态)。我们假设代理有某种方式知道他们处于哪个状态。
我们将用变量S表示状态。在网格世界中,S是一个离散变量,但在其他问题中,S可能是连续的。
动作
动作是智能体可以执行的事情,并且会导致状态的变化。某些动作在特定状态下可能不可用。例如,在网格世界中,网格的边缘是智能体运动的约束。如果智能体处于左下角方格 {2, 0},并且尝试向左或向下移动,它们将保持在原地。同样,{1, 1} 中的单元格是智能体必须绕过的障碍。我们用变量 A 表示动作,它有四种可能的结果 {上,下,右,左}。
转移函数
转移函数是一个概率分布函数。它告诉我们,在给定当前状态和采取的动作的情况下,移动到特定下一个状态的概率。
如果状态是离散的,转移函数看起来像这样:

在这里,S**[t]=s 表示智能体当前处于状态 s。A**[t]=a 表示智能体执行动作 a。P(S[t][+1]=s'|S**[t]=s, A**[t]=a) 是智能体在处于状态 s 并执行动作 a 的情况下过渡到新状态 s' 的概率。当动作以完全确定的方式导致新状态时,这个概率分布函数变得退化(所有概率都集中在单个值上)。
奖励
在强化学习(RL)中,“奖励”一词比“效用”更受欢迎。在 MDP 的上下文中,奖励函数将始终以状态 s 作为参数。我们将它写成 U(s)。
在网格世界示例中,U({0, 3}) = 100,U({1, 3}) = –100。所有其他状态的奖励为 0。请注意,有时在 MDP/RL 文献中,U()是状态和动作的函数,如U(s, a)。仅仅让动作成为状态的函数并不会失去任何东西,因为你可以总是将动作折叠到状态的定义中。
12.5.2 MDP 作为因果 DAG
图 12.28 展示了 MDP 作为一个因果 DAG。

图 12.28 以 DAG 表示的马尔可夫决策过程
作为因果 DAG,MDP 看起来像我们概述的其他顺序决策过程,只不过我们限制了自己只关注状态、动作和奖励。在图 12.28 中,过程一直持续到我们达到终端状态 (S[k]),例如在网格世界示例中到达终端单元格。
因果马尔可夫性质和 MDP
“马尔可夫决策过程”中的“马尔可夫”一词来源于当前状态在给定最后一个状态的情况下与状态的完整历史无关的事实。这与因果 DAG 的因果马尔可夫性质形成对比:在 DAG 中,一个节点在其直接因果父节点给定的情况下,与间接“祖先”原因无关。我们可以看到,当我们把 MDP 看作因果 DAG 时,这个马尔可夫假设等价于因果马尔可夫性质。这意味着我们可以在 MDP 图形设置中使用基于 d-separation 的因果推理,包括 do-calculus。
转移函数和因果马尔可夫核
注意,基于这个 DAG,状态 S[(][t][+1)] 的父节点是先前的状态 S**[t] 和在先前状态下采取的动作 A**[t]。因此,因果马尔可夫核是 P(S[t][+1]=s'|S**[t]=s, A**[t]=a),即转移函数。因此,转移函数是给定状态的因果马尔可夫核。
12.5.3 部分可观察 MDP
MDP 的扩展是 部分可观察 MDP(POMDP)。在 POMDP 中,代理不能确定他们处于什么状态,他们必须根据从环境中的不完整证据对该状态进行推断。这适用于许多实际问题,其中代理无法观察到环境的完整状态。
一个 POMDP 可以根据我们对未观察状态和观察状态之间因果关系的假设具有不同的因果结构。例如,假设一个潜在状态 S 是观察状态 X 的原因。现在观察状态 X 驱动行为 A 而不是 S。图 12.29 展示了将 POMDP 作为因果有向无环图(DAG)的这种表述。

图 12.29 展示了潜在状态 S 导致观察状态 X 的 POMDP。X 驱动动作 A。
相比之下,图 12.30 展示了一个例子,其中潜在状态是观察状态(通过代理的动作中介)和效用(注意从 U(S[i]) 到 U[i] 的符号略有变化)的潜在共同原因(表示为 Z)。在这里,未观察因素影响代理的行为以及该行为的效用。
再次强调,基本的 MDP 和 POMDP DAG 应被视为我们分析的模板。一旦我们理解了我们感兴趣回答的因果查询,我们就可以将观察状态和未观察状态的各个组成部分明确地表示为图中的特定节点,然后使用识别和调整技术来回答我们的因果查询。

图 12.30 展示了未观察状态是潜在共同原因的 POMDP 表述,这些原因可能作为因果推断中的混杂因素。
12.5.4 MDP 中的策略
与之前一样,MDP 中的策略作为随机干预。图 12.31 展示了在当前状态下选择最优动作的策略,这种方式会破坏任何来自混杂因素的影响。

图 12.31 展示了 MDP DAG 的修改
图 12.31 在简单性上,它仅基于当前状态选择一个动作。挑战在于实现,因为在大多数 RL 设置中,状态可以是高维对象。
12.5.5 因果 Bellman 方程
强化学习(RL)是关于寻找最优策略,该策略由 Bellman 方程表征,通常如下所示:

用简单的话说,我们正在寻找一个策略Π^,它最大化了随时间累积的奖励。在这里,γ*是一个折现率,其值在 0 和 1 之间,确保智能体更重视近期的奖励而不是远期的奖励。
由于我们在推理如果我们部署了策略会发生什么,因此因果公式如下:

注意,我们可以对贝尔曼方程的其他变体执行相同的因果重写,例如 Q 学习中使用的 Q 函数。
贝尔曼方程的非因果和因果公式的区别与第 12.2.4 节中优化E(U(Y)|X=x)和E(U(Y[X][=][x]))的区别相同。解决贝尔曼方程因果天真版本的进程可能会引入来自潜在混杂因素或来自对碰撞者和中介条件的条件的偏差。我们调整因果的方法可以帮助避免这些偏差。在许多情况下,天真方法的解决方案将与因果方法一致,因为那些偏差可能不会影响相对于其他策略的顶级策略的排名。然而,正如E(U(Y)|X=x)与E(U(Y[X][=][x]))的例子一样,将会有一些情况下非因果和因果公式的解决方案不同,你的强化学习问题可能是那些情况之一。
12.6 决策理论中的反事实推理
到目前为止,我们已经讨论了在因果层次结构中作为第二级查询的,关于根据效用函数选择最优动作的问题。在决策理论中,第三级反事实推理有什么用?在本节中,我们将简要回顾一些第三级推理的应用。
12.6.1 反事实策略评估
反事实策略评估涉及从生产中的策略中提取日志数据,并问自己,“既然我们使用了这个策略并获得了这个累积奖励,如果我们使用了不同的策略,我们会获得多少累积奖励?”参见章节注释www.altdeep.ai/p/causalaibook,了解诸如反事实引导策略搜索和反事实风险最小化等技术。
12.6.2 反事实遗憾最小化
在第八章和第九章中,我介绍了遗憾作为一个反事实概念。现在我们已经引入了决策语言,我们可以进一步阐明这个想法;遗憾是在给定特定动作或一组动作时实现的效用/奖励与如果采取了另一个动作或一组动作本应实现的效用/奖励之间的差异。
反事实遗憾最小化是一种优化策略的方法,旨在最小化遗憾。为了说明,假设我们有一个策略变量Π,它可以返回几个可用的策略之一。这些策略接受上下文并返回一个动作。这个动作导致一些奖励U。
假设,在我们的日志数据中,对于单个实例,策略是 Π=π,上下文是 C=c。我们得到一个特定的动作 A=π(c) 和奖励 U=u。对于某个策略 π**'),遗憾是对反事实问题的回答,“如果策略是 π=π**',我们会得到多少更多的奖励?”从期望的角度来看,

再次强调,这是在日志数据中单个实例的遗憾,其中上下文是 C=c,效用是 u。有许多变化,但总体思想是找到能够最小化所有 C=c 情况的累积遗憾的策略,目标是优先考虑在未来的 C=c 情况下该策略。
12.6.3 在决策问题中做出第 3 级假设
当然,问题是如何做出第 3 级假设,以实现反事实推理。一种方法可能是指定一个 SCM 并使用反事实推理的通用算法(在第九章中讨论)。例如,在强化学习(RL)中,转移函数 P(S[t][+1]=s'|S**[t]=s, A**[t]=a) 捕获环境状态变化的规则。正如我提到的,P(S[t][+][1]|S**[t]=s, A**[t]=a) 是给定状态 S[t][+1] 的因果马尔可夫核。我们可以指定一个具有赋值函数的 SCM,该赋值函数包含因果马尔可夫核,并将该赋值函数写成

在这里,n[s]['] 是外生变量 S**[t] 的值。
挑战在于指定编码正确反事实分布的赋值函数。在我们对底层因果机制了解更多的领域里,这更容易做到。一个关键例子是在基于规则的博弈中;游戏规则可以提供第 3 级约束,从而实现反事实的模拟。回想一下,在第九章中,蒙特霍尔问题的简单规则使我们能够模拟保持和切换策略的反事实结果。或者考虑像扑克这样的多人游戏,在每一轮游戏中,每位玩家都会被发一手牌,并可以采取某些行动(检查、下注、跟注、加注或弃牌),这些行动基于简单的规则,进而决定该轮赢得或输掉筹码的数量。玩家的反事实遗憾是他们实际获得的筹码与如果他们决定采取不同的行动可能获得的最多筹码之间的差异。这是在考虑决策时可用信息的同时完成的,而不是使用关于对手牌的后见之明。
该领域的反事实后悔最小化算法试图找到游戏策略,以最小化多个玩家之间的反事实后悔。游戏的规则具体,使得可以模拟反事实的游戏轨迹。挑战在于在由于多轮游戏中的多个玩家交互而相当大的可能反事实轨迹空间中寻找最优策略。有关多智能体游戏中反事实后悔最小化的章节注释,请参阅www.altdeep.ai/p/causalaibook。
摘要
-
决策天然是一个因果问题,因为决策导致后果,我们的目标是做出导致有利后果的决策。
-
选择最优决策是一个二级查询,因为我们是在问“如果我做出这个决策会发生什么?”
-
E(U(Y|X=x)) 和 E(U(Y[X][=][x])) 是不同的量。通常,人们想知道优化 E(U(Y[X][=][x])) 的 X 的值,但优化 E(U(Y|X=x)) 通常会在不指定因果模型的情况下得到相同的答案。
-
这在强化学习(RL)中尤其如此,其中 E(U(Y|X=x)) 和 E(U(Y[X][=][x])) 分别是 Bellman 方程的传统和因果形式。在解决 Bellman 方程的传统方法中可能存在混杂因子、中介因子和碰撞因子偏差。但那些偏差通常不会影响相对于其他策略的顶级策略的排名。
-
尽管如此,有时优化 E(U(Y|X=x)) 的 X 的值与优化 E(U(Y[X][=][x])) 的值不同。同样,在解决 Bellman 方程时处理因果细微差别可能会导致与忽略它们时不同的策略。如果你的决策问题属于此类,因果方法是一个更好的选择。
-
新 comb 悖论是一个旨在对比决策理论中因果和非因果方法的思维实验。“悖论”一旦我们使用正式的因果模型,就会显得不那么神秘。
-
结合概率建模工具如 Pyro 和 pgmpy 的因果决策理论非常适合于建模内省,其中智能体反思其内部状态(感觉、直觉、冲动、意图),并使用这些信息来预测决策的“如果”结果。
-
当我们用一个因果有向图(DAG)来表示一个顺序决策过程时,我们可以在这个决策问题中运用所有图形因果推理的工具。
-
策略像随机干预一样运作。它们改变图,但仍然依赖于过去观察到的节点,这种依赖可能会引入后门混杂。
-
在因果 RL 中,我们可以将 MDPs 和 POMDPs 表示为因果 DAG,并再次利用图形因果推理理论。
-
我们可以使用模板有向无环图(DAG)来表示顺序决策过程,但你应该根据你的问题对这些模板进行定制。
-
决策理论中关于反事实推理的常见用例包括反事实政策评估和反事实后悔最小化。
-
如果你能够访问你的马尔可夫决策过程(MDP)中状态转换的底层规则,例如在物理系统或游戏中,你可以构建一个与这些规则反事实忠实对应的 SCM(结构化因果模型),并利用它来处理决策中的反事实用例。
第十三章:因果关系与大型语言模型
本章涵盖
-
在 LLMs 中使用因果信息来增强因果分析
-
将 LLM 的组件与因果理念相连接
-
构建因果 LLM
大型语言模型(LLMs)在人工智能领域取得了重大进步。这些模型是设计用来生成和理解人类可读文本的大型神经网络。它们之所以被称为“大型”,是因为它们的规模确实令人印象深刻——最前沿的 LLMs 拥有数十亿甚至数万亿的参数。作为生成模型,它们的主要功能是生成连贯且与上下文相关的自然语言。它们还可以生成结构化文本,如编程代码、标记语言、数学符号、数据库查询以及许多其他以文本形式的有用事物。
LLMs(大型语言模型)只是广泛类别的生成式 AI 的一个例子。例如,我们可以使用最前沿 LLM 的神经网络架构来模拟其他序列,如时间序列或 DNA。LLMs 是一种基础模型,意味着这些大规模模型作为基础或基石,在此基础上可以构建更专业的模型或应用。一些 LLMs 是多模态的,这意味着它们不仅与文本一起工作,还与其他内容模态一起工作,如图像。在本章中,我们将特别关注 LLMs,但我们讨论的大部分内容可以推广到这些相关概念。
首先,让我们探讨一些使用 LLMs 来增强因果分析用例。
13.1 LLMs 作为因果知识库
正如我在本书中多次提到的,在因果建模中,我们不建模数据,我们建模数据生成过程(DGP)。换句话说,因果建模需要关于 DGP 的因果信息,我们可以将其作为假设,这些假设能够使我们识别因果查询。最前沿的 LLMs 是获取和验证这些因果信息的优秀资源。在本节中,我将展示一些使用 LLMs 查询因果信息以增强因果分析的已验证用例。
以下结果来自 OpenAI 的 LLM GPT-4 通过 ChatGPT 界面。由于模型的随机生成,以及 OpenAI 对模型和 ChatGPT 软件的定期更新,你的结果可能会在一定程度上与我有所不同。
13.1.1 构建因果 DAG
让我们从使用 LLM 来增强本书中讨论的第一个工作流程开始:构建因果 DAG。以因果 DAG 的形式指定假设可能会令人望而却步。如果你的 DAG 是错误的怎么办?现代 LLMs 作为利用公共专家知识构建 DAG 的一种方式,是无价的。
您是一位有用的因果分析助手。我对建模吸烟与肺癌之间的关系感兴趣。请提供一个捕捉这种因果关系的因果 DAG。
当然!有向无环图(DAG)是表示因果关系的有力工具。以下是一个基本的因果 DAG,用于表示吸烟和肺癌之间的关系:
-
节点:
-
S: 吸烟行为(例如,吸烟者、非吸烟者或吸烟频率)
-
LC: 肺癌(存在或不存在)
-
-
有向边:
- 从S到LC的有向边表示吸烟行为可以导致或影响肺癌的发生。
-
可能的混淆因素:
-
可以将其他节点引入 DAG,以考虑潜在的混淆因素。一些可能的混淆因素可能包括:
-
G: 遗传学(有肺癌家族史)
-
A: 年龄(年龄较大可能意味着更高的风险)
-
E: 环境暴露(如石棉、污染等)
-
O: 其他健康行为(如饮食、锻炼)
-
-
-
可能的中介变量:
-
中介变量是原因通过其影响结果的中介变量。
-
例如,可以假设吸烟可能引起肺损伤,进而导致肺癌。在这种情况下:
- LD: 肺损伤
-
DAG 可能看起来是这样的,包括混淆因素和中介变量:
-
S <- G –> LC
-
S <- A –> LC
-
S <- E –> LC
-
S <- O –> LC
-
S –> LC
-
S –> LD –> LC
在建模过程中,重要的是:
-
识别所有可能的混淆因素以避免结果偏差。记住,相关性不等于因果关系。仅仅因为两个节点相关并不意味着一个导致另一个。
-
与领域专家验证你的 DAG。他们可以提供你可能没有考虑到的见解。
-
使用数据来测试和验证 DAG 中的因果关系,例如,通过控制实验或当实验不可行时,使用工具变量或倾向得分匹配等统计方法。
-
请记住,这是一个简化的吸烟和肺癌之间关系的 DAG。根据你研究的粒度和深度,你可能需要调整和扩展这个框架。
继续下去,我可以提示 LLM 提供潜在的工具变量。
建议两个合适的工具变量的候选者。请简要回答。
当然。对于吸烟和肺癌之间的关系,两个可能的工具变量(IVs)可以是:
-
烟草税或价格:如果某些地区或时期烟草价格或税收较高,这可以影响吸烟行为,而不直接影响肺癌率,除非通过减少吸烟的影响。这里的假设是,烟草价格影响肺癌的唯一方式是通过影响吸烟行为。
-
反吸烟运动或立法:某些地区或时间反吸烟运动或立法的介绍或强度可以用作工具变量。如果这些运动或法律是外生决定的(不是基于当前的肺癌率),它们可以影响吸烟率,而不会对肺癌产生直接影响,除非通过其对吸烟行为的影响。
对于两个工具变量,验证两个关键假设的工具有效性至关重要:
-
工具变量与暴露相关(例如,香烟税影响吸烟行为)。
-
工具变量只通过其对暴露的影响影响结果(例如,肺癌),而不是通过其他途径。
提示最佳实践
提示前缀“你是一个有用的因果分析助手”是所谓的“系统提示”的一个例子。系统提示提供了关于从 LLM 期望得到的内容的一般背景。系统提示和其他提示工程技术(例如,思维链提示、情境学习)是获得 LLM 良好输出的最佳实践。为了简洁起见,我将使用系统提示,但在此部分省略了一些其他提示工程最佳实践——在实际环境中包含它们是明智的。
以这种方式继续与 LLM 互动,可能通过多个会话,你可以提出一个对你和你的利益相关者有意义的候选 DAG。接下来,你可以提示 LLM 为你提供运行分析的代码。
13.1.2 生成 DAG、模型和因果分析的代码
现代 LLM 是在广泛的各种编程语言的代码语料库上训练的。这大大加快了因果分析的速度,尤其是如果你使用的库在训练数据中有代表。例如,我们可以提示 ChatGPT 使用 DoWhy 实现其建议的肺癌 DAG 并估计因果效应。
请提供使用 Python 中的 DoWhy 库实现你上面建议的因果 DAG 的代码。包括香烟价格作为工具变量,并提供估计因果效应的代码。
当然!以下是一个使用 Python 中的 DoWhy 库实现吸烟与肺癌之间关系因果 DAG 的基本示例,使用香烟价格作为工具变量。
设置你的环境
本章中的代码是用 DoWhy 版本 0.11、Transformers 版本 4.38.2、Accelerate 版本 0.28.0、pandas 2.0.3、NumPy 1.25.2 和 pyro-ppl (Pyro)版本 1.9.0 编写的。访问章节注释www.altdeep.ai/p/causalaibook以获取包含本章代码的笔记本链接。
Chat GTP 接着生成了以下代码。
表 13.1 ChatGPT 生成的 DoWhy 代码(原文)
import numpy as np
import pandas as pd
import dowhy
from dowhy import CausalModel
from dowhy.datasets import linear_dataset
from dowhy.do_why import CausalModel #1
# Let's create a sample dataset for illustration purposes
# Normally, you'd import your own dataset: data = pd.read_csv('your_dataset.csv')
n_points = 1000
data = pd.DataFrame({
"S": np.random.binomial(n=1, p=0.5, size=n_points), #2
"LC": np.random.binomial(n=1, p=0.5, size=n_points), #3
"Price": np.random.normal(loc=5, scale=1, size=n_points), #4
})
# Create a causal DAG
model=CausalModel(
data = data,
treatment='S',
outcome='LC',
common_causes=['G', 'A', 'E', 'O'], # Potential confounders
instruments=['Price'] # Instrumental Variable
)
# Identify the causal effect
identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
# Estimate the causal effect using Instrumental Variable method
estimate = model.estimate_effect(identified_estimand,
method_name="iv.instrumental_variable",
method_params={'iv_instrument_name': 'Price'})
# Print the causal effect estimate
print(estimate)
1 这一行是错误的,但如果你删除它,代码就可以运行。
2 模拟吸烟状态:0 表示非吸烟者,1 表示吸烟者。
3 模拟肺癌状态:0 表示无肺癌,1 表示有肺癌。
4 以 5 美元的平均价格模拟香烟价格。
这是一个简化的例子。在实际应用中:
-
你的数据将导入而不是像上面那样模拟。
-
确保你的 DAG 正确地根据领域知识建模关系。
-
工具变量背后的假设需要严格检查。
-
进一步的分析步骤(例如,统计测试,检查稳健性)将是必要的。
当你生成代码时,可能会出现错误。例如,DoWhy 已经经过几次迭代,没有任何东西迫使 LLM 跟上库的最新版本。在这种情况下,from dowhy.do_why import CausalModel这一行有错误。没有名为do_why的模块,并且CausalModel已经在之前的行中导入。我们将在第 13.1.7 节中解决 LLM 产生的错误。现在,如果你删除那一行,代码就可以运行。
ChatGPT 将与我们在书中广泛使用的 pgmpy 和 Pyro 库很好地工作。作为一个练习,我建议你将你在 pgmpy 中构建的一个模型提示 LLM 将其转换为 Pyro 中的模型。
接下来,我们将看看 LLM 能告诉我们关于 DAG 结构之外的内容。
13.1.3 解释和机制
DAG 的优势在于,图是一种直观的方式来表示什么导致什么。但它们并没有告诉我们关于导致效应的机制的性质。
结构性因果模型提供了一种数学方法来描述因果关系的潜在机制。在自然科学中,我们有大量的方程描述自然法则,用数学术语描述机制是有意义的。例如,在商业领域,我们缺乏方程,除了近似(广告支出和参与度之间有什么方程?)。
LLM 利用常见和专业知识提供超越 DAG 的机制解释,但形式比正式数学更易于理解。
你是一个有用的因果解释生成器。提供广告支出与品牌知名度之间因果关系的描述。要简洁到足以适应幻灯片中的单张幻灯片。
广告支出与品牌知名度之间的因果关系
-
直接因果关系:随着广告支出的增加,品牌知名度通常会上升。
- 机制:更大的广告支出意味着品牌对目标受众的曝光更加频繁和广泛。
-
收益递减:在某个点上,额外的广告支出可能不会导致品牌知名度的成比例增加。
- 原因:市场饱和、广告疲劳或受众覆盖的冗余。
-
外部因素:广告效率并不仅仅基于支出。
- 影响因素:广告质量、位置、目标受众的相关性、竞争和市场条件都可能影响结果。
考虑到可以识别出一些反事实查询,它们对原因和效果之间功能关系的简单分类假设。例如,从干预数据中可以识别出必要性的概率,如果 X 和 Y 之间的关系是单调的,那么从观察数据中部分可识别:
这是单调关系吗?请将您的答案限制在一句话内。
是的,广告支出与品牌知名度之间的关系通常是单调的,因为增加广告支出不会降低品牌知名度,尽管增加的速度可能会减缓。
我们不必局限于一个解释;我们可以将 LLM 用作解释假设的生成器。例如,考虑推论推理任务,在这些任务中,一个人推断出特定观察到的效应集的可能原因(例如,在科学、刑事调查和医学诊断中)。
为了说明,让我们提出一个源自美国医学执照考试的医疗诊断问题的修改版,用于 ChatGPT。我将要求提出关于观察到的症状原因的多个假设,这就是医生所说的鉴别诊断:
您是一位因果解释助手。您的任务是生成 3 个一句话长的假设,用以解释观察到的现象。这是一个医学诊断示例。
观察结果:一位 48 岁的女性出现复视、眼部疼痛、闭经、潮热、体重减轻、心动过速、高血压、湿润的手掌、甲状腺肿大、突出眼睛、眼睑退缩、结膜充血和无法聚视。视力为双眼 20/20。
-
患者可能患有格雷夫斯病,这是一种影响甲状腺的自身免疫性疾病,导致甲状腺功能亢进及其相关症状,包括眼部表现。 -
- 女性的症状,包括闭经和潮热,表明可能存在早期更年期或影响其生殖激素的另一种内分泌疾病。
-
- 眼部症状,包括复视、眼睑退缩和无法聚视,暗示可能存在甲状腺眼病或眶周肌病。
因果建模的一个挑战是与领域专家合作,将领域假设编码到模型的框架中。这类生成的解释和假设是宝贵的资产,可用于与可能同意或更好地迭代这些生成解释的领域专家沟通。
13.1.4 因果框架问题与 AI 对齐
对于一个感兴趣的查询,我们必须决定哪些变量与问题相关。哲学家和人工智能研究员 Clark Glymour 很好地描述了这项任务背后的挑战:“给定大量的东西,以及使用其中一些东西来完成某个任务,对于这个任务来说,什么是相关的?”¹
LLMs 在建立因果关系框架方面很有用——选择与因果查询相关的因果变量的问题。在第三章关于构建 DAG 的讨论中,我提到了因果充分性作为基于 d-separation 的标准,用于选择包含在 DAG 中的变量。但当我们提示 LLM 提供一个包含与吸烟对肺癌影响相关的变量的 DAG 时,我们高度怀疑 LLM 是否在使用因果充分性来选择这些变量。更有可能的是,它正在使用其训练数据中关于这种关系的常识。这种知识足以提供一组合理的最小相关变量集。
为了说明这种能力的实用性,考虑以下 ChatGPT 交互:
您是一个有用的因果推理助手。考虑以下故事。
Jake 在一个办公室网络中。服务器上有一个开放的共享文件夹。Jake 正在通过网络传输文件,并意外点击了文件夹,启动了一个消耗大量带宽的过程。这导致网络变慢。Jake 对网络减速有多大的责任?
基于提供的故事,让我们使用因果推理来分析这种情况:
-
直接原因:Jake 通过意外点击一个文件启动了一个消耗大量带宽的过程。
-
影响:由于消耗了带宽,网络变慢了。
基于这些事实,我们可以确定 Jake 的行为是网络减速的直接原因。
然而,可以从不同的维度来考虑责任:
-
意图:Jake 的行为是意外的。他并没有意图减慢网络。
-
预防机制:故事没有说明在启动这种消耗大量带宽的过程之前是否有任何警告或用户提示。如果没有警告,有人可能会认为系统没有设计来防止这种意外行为。
-
知识与培训:Jake 是否接受了关于传输大文件或关于消耗大量带宽的特定文件的培训或通知?如果没有,他的知识差距导致了这次事故。
-
系统设计:服务器上的开放共享文件夹对 Jake 可访问。如果文件已知是带宽消耗者,那么值得问的是为什么这样的文件会被放在一个没有任何警告或限制的开放共享文件夹中。
直接回答问题:杰克对网络速度减慢负有责任,因为他的行为是直接原因。然而,如果存在系统设计缺陷、缺乏培训或缺乏预防措施,他的责任程度可能会减轻。如果上述提到的其他因素在事件中发挥了作用,那么责任将在杰克和其他因素之间分担。
在这里,响应包括系统设计缺陷、缺乏培训或缺乏预防措施作为与因果关系框架相关的因素,尽管我在提示中并未提及这些因素。
更重要的是,考虑 LLM 可能包括但未包括的许多因素:或许杰克的鼠标出现了物理故障,导致他点击了文件,或许黑客移除了该文件的安全功能,或者任何其他可能的减轻因素。人类可能会排除这些因素,除非明确提及,LLM 也是如此。
如果人类已经擅长建立因果关系框架,我们为什么还要关心 LLM 能否做到这一点呢?考虑从网络日志中进行根本原因分析的问题。假设关于杰克的相同故事被改写成网络日志的形式,如下所示:
Aug 22 14:02:34 User "Jake" logged in from 192.168.0.105.
Aug 22 14:03:12 Shared folder accessed by user "Jake" at //SERVER/shared-folder.
Aug 22 14:03:20 File transfer initiated by "Jake" from local: /home/jake/documents to remote: //SERVER/shared-folder/documents.
Aug 22 14:03:25 WARNING: Unusual network activity detected. User "Jake" launched file "heavy_process.exe" on //SERVER/shared-folder.
Aug 22 14:03:30 Network performance alert: Bandwidth usage spikes at 95%.
Aug 22 14:04:00 Network slowdown detected: Packet loss rate 35%.
这对于人类来说比自然语言故事更难解析,即使是经验丰富的网络管理员也是如此。此外,人类可能还需要阅读并忽略与这个叙述无关的许多其他网络事件。但 LLM 在其训练数据中拥有大量的网络日志,从较长的文本中提取相关事件是 LLM 的一项既定能力。
在本书的大部分内容中,我们专注于使用因果关系模型来推断关于世界的客观因果关系,例如吸烟是否导致肺癌。在这个例子中,客观事实,即杰克导致了网络速度减慢,是微不足道的。关于杰克对速度减慢的责任程度是一个高度主观的问题。但回答这个问题仍然很有价值,尤其是如果你试图在一个大型网络日志文件中找到根本原因。
人工智能对齐是确保人工智能系统行为与人类目标和偏好一致的任务。在这里,我们希望实现一种特定类型的人工智能对齐。我们希望 LLM 不仅确认明显的因果关系事实,还与人类倾向于做出的因果关系判断相一致。在这种情况下,我向 LLM 提出了关于杰克责任的一个开放式问题,它给出了许多人类都会同意的回应。我也可以指示 LLM 在生成判断时遵循某些标准。例如,律师可以提示 LLM 在生成法律意见时应用某些法律理论和案例法。
接下来,我们将探讨如何使用 LLM 来理解和情境化更细微的因果关系概念。
13.1.5 理解和情境化因果关系概念
理解正式的因果概念并在你的领域内应用它们可能会具有挑战性。LLM 可以作为解释和情境化这些概念的工具。让我们以必要因果关系的反事实概念为例进行说明。
你是一位有益的因果推理主题解释者。请提供一段关于必要因果关系的概念解释。
当然!必要因果关系指的是一种因果关系,其中原因的存在是结果发生的必要条件。换句话说,如果没有原因的存在,结果就不会发生。然而,原因的存在并不保证结果会发生,因为可能还有其他条件需要满足。把它想象成一把钥匙开一把锁:钥匙(原因)是打开锁(结果)的必要条件,但仅仅拥有钥匙并不意味着锁会自动打开;你仍然需要插入并转动它。如果锁没有钥匙仍然锁着,那么钥匙就是解锁的必要原因。
一个前沿的 LLM 会在跨领域的各种理论和研究中进行训练。将一个想法置于各种领域中进行情境化可能是有用的。
在一句话中,解释在法律理论中如何概念化必要因果关系。
在法律理论中,必要因果关系,通常被称为“若无因果关系”,是一个用于建立被告行为与原告所受损害之间直接联系的基础性概念。一个行为要成为导致结果的“若无”原因,如果没有这个行为,结果就不会发生。换句话说,如果可以说“若非被告的行为,损害就不会发生”,那么必要因果关系就成立了。然而,值得注意的是,尽管“若无”因果关系在许多法律情境中是一个关键组成部分,但它本身并不总是足以确立责任。其他因素,如可预见性和近因,通常也会发挥作用,以确定法律责任的全部范围。
从认知科学的角度来看,用一句话简要说明必要因果关系在人类判断中的作用。
从认知科学的角度来看,必然因果关系在人类的判断和决策中起着至关重要的作用。我们的大脑被设计成在环境中寻找模式和因果关系,以预测和导航未来的事件。当我们把一个事件视为另一个事件的必然原因时,它通过在两个事件之间建立决定性的联系,简化了我们的认知处理。这意味着当我们遇到原因时,我们可以预测结果,从而允许更有效的规划和反应。然而,这种倾向也可能导致认知偏差,我们可能会错误地将一个效果归因于一个感知到的必然原因,即使其他影响因素也在发挥作用。
从哲学的角度来看,用一句话解释康德和休谟在关于必然因果关系的信仰上的不同。
康德相信先验综合知识的存在,并认为我们的心灵在我们的经验上强加因果秩序,而休谟则认为因果关系不是世界本身的特征,而是基于事件规律性连续性的思维习惯。
现在我们已经解构了必然因果关系的概念,让我们将其应用到我们感兴趣领域的一个实际问题中。假设你是 Netflix 的一名数据科学家:
给我一个涉及 Netflix 用户取消订阅的必然因果关系的例子。
一个 Netflix 用户取消订阅的可能必然原因是该平台上仅有的独家内容。如果用户深感兴趣的一部特定剧集或电影从 Netflix 上被移除,而该内容在其他地方仍然可用,那么该内容的移除就成为了该用户决定取消订阅的必然原因。如果没有该内容的移除,用户可能不会取消订阅。
现在 LLM 已经帮助我们理解了必然因果关系,我们可以请它帮助我们形式化这个概念。
13.1.6 因果查询的形式化
对于人类来说,自然语言是我们因果推理的媒介。但我们需要将这种自然语言转换为形式术语,以便我们可以应用数学和算法分析。这可能是一个挑战。幸运的是,LLM 在这方面可以提供极大的帮助。
例如,在第八章中,我介绍了几个平行世界反事实语言的例子以及如何将它们转换为反事实符号。必然因果关系,更具体地说,是必然性的概率,是这些反事实之一。让我们将这个任务放在我们的 Netflix 环境中交给 LLM:
您是一位有用的因果推断助手。我在 Netflix 的背景下考虑一个反事实推断问题。我有一个以下查询:“有一组用户在平台上的内容被删除后取消了订阅。给定这个用户组中的某个成员,如果内容未被删除,他们不会取消订阅的概率是多少?”请使用 Pearl 的反事实符号将此查询写成概率表达式。
当然!Pearl 的反事实符号提供了一种简洁的方式来表达此类查询。
让我们定义一些变量:
-
X = 内容的删除(二元变量,删除为 1,未删除为 0)。
-
Y = 用户是否取消订阅(二元变量,用户取消订阅为 1,未取消订阅为 0)。
您的查询实际上是在询问,在内容被删除且用户确实取消订阅的情况下,如果内容未被删除,用户不会订阅的概率。
使用 Pearl 的反事实符号,这可以写成:

其中:
-
Y[x][=0] 是在 X 被设置为 0(即内容未被删除)时 Y 的反事实结果。
-
X=1 表示内容已被删除。
-
Y=1 表示用户已取消订阅。
因此,P(Y**[x=][0] = 0| X = 1, Y = 1) 是在内容被删除且用户确实取消订阅的情况下,如果内容未被删除,用户不会取消订阅的概率。
大型语言模型可以将我们从概念化带到形式化。但请注意,它也可能“产生幻觉”。
13.1.7 小心:LLM 可能产生幻觉
我们已经看到,大型语言模型(LLM)可以帮助生成代码。让我们请求实现必要性概率的代码。
提供 Python 代码,对于给定的处理变量和结果变量对,计算反事实查询“必要性概率”。
必要性概率(PN)是指在处理存在的情况下,并且只有在处理存在时才实现结果的情况的比例。简单来说,它表示结果发生是因为处理。
根据这个定义,你可以使用以下公式计算 PN:

其中:
-
P(Treatment∩Outcome) 是处理和结果同时发生的概率。
-
P(¬Treatment∩Outcome) 是结果发生但处理未发生的概率。
响应继续提供格式良好、注释清晰且可编译的 Python 代码。但这个答案是错误的。这通常被称为“幻觉”,一个令人信服的谎言。这个例子尤其令人信服——该公式仅在治疗与结果之间的关系是单调的、没有混杂因素,以及P(治疗) = P(结果)时才成立。这些假设如此强烈,以至于我们无法原谅 ChatGPT 没有提及它们。模型的其他生成结果甚至更加偏离正确方向。
我们可以使用 LLM 来增强我们的因果分析,特别是利用常见和专家因果知识以及生成的代码。但幻觉意味着这并不是万能的。我们需要依赖我们自己的因果专业知识来识别何时发生幻觉,并理解何时它威胁到我们分析的质量。
要理解这种幻觉发生的原因,让我们首先考察 LLMs 是如何工作的。
13.2 因果主题的 LLM 入门
要了解如何部署 LLMs 进行因果应用,了解它们的工作原理以及它们的局限性非常重要。本节提供了一个关于核心思想的因果主题快速高级概述。
13.2.1 LLMs 的概率机器学习视角
在 LLMs(大型语言模型)的背景下,“token”指的是模型读取的字符序列,它可以短至一个字符,也可以长至一个单词。Token 是输入文本被分割成模型可以管理的片段的单位。
Hugging Face 的 Transformers 库有一个公开可用的 GPT-2 版本,它远不如最前沿的模型,但具有类似的Transformer架构。Transformer 架构是一种深度学习模型,旨在通过关注句子中词语之间的关系(而不考虑它们的位置)来处理和理解文本以及其他序列数据。让我们分词表达式“LLMs 能否进行反事实推理?”
列表 13.2 查看 LLM 操作的示例 token
from transformers import GPT2Tokenizer
tokenizer = GPT2Tokenizer.from_pretrained('gpt2') #1
tokens = tokenizer.tokenize("Can LLMs reason counterfactually?") #2
print(tokens) #3
1 初始化 GPT-2 分词器。
2 分词序列。
3 打印出 token。
这将打印出以下 token:
['Can', 'ĠLL', 'Ms', 'Ġreason', 'Ġcounter', 'fact', 'ually', '?']
“Ġ”对应于一个空格。请注意,标点符号也是 token,像“counterfactual”这样的单词被分割成多个 token。每个 token 对应于一个整数,该整数索引了在大型“词汇表”中的 token。GPT-2 的词汇表大小为 50,257。
列表 13.3 将 token 转换为整数
input_ids = tokenizer.encode( #1
"Can LLMs reason counterfactually?", #1
return_tensors='pt' #1
) #1
print(input_ids)
1 “编码”token 为整数,这些整数在称为“词汇表”的 token 列表中索引 token。
这将 token编码为一个整数序列:
tensor([[ 6090, 27140, 10128, 1738, 3753, 22584, 935, 30]])
Transformer 架构与这些数值值一起工作。
LLMs 在 token 序列上定义了一个联合概率分布。对于短语“LLMs 能否进行反事实推理?”模型定义了一个概率分布:

模型还将考虑这种序列以问号结束而不是继续下去的可能性。为此,LLM 的词汇表中包含一个特殊标记来标记序列的结束。对于 GPT-2,这个标记是 <|endoftext|>:

此外,自回归语言模型(LLM),如 GPT 和 Llama 系列的 Transformer 模型,按照文本序列的顺序对文本进行建模,因此它们将这个联合概率分解如下:

我们可以使用 Transformers 库在对数尺度上计算这些概率中的每一个。在生成对数概率时,我们首先计算词汇表中的每个术语的 logits。对于概率值 p,相应的 logits 是 log(p / (1–p))。
列表 13.4 计算序列中每个标记的对数概率
import torch
from transformers import GPT2LMHeadModel
model = GPT2LMHeadModel.from_pretrained('gpt2-medium') #1
model.eval() #1
input_text = "Can LLMs reason counterfactually?<|endoftext|>" #2
input_ids = tokenizer.encode(input_text, return_tensors='pt') #2
with torch.no_grad(): #3
outputs = model(input_ids) #3
logits = outputs.logits #3
log_probs = torch.nn.functional.log_softmax(logits, dim=-1) #4
for idx, token in enumerate(input_ids[0]): #4
token_log_prob = log_probs[0][idx][token].item() #4
print(f"Token: {tokenizer.decode(token)}" + #4
" | Log Probability: {token_log_prob}") #4
1 初始化 GPT-2 模型并将其设置为评估模式。
2 将短语分词并编码,包括序列结束标记。
3 给定短语,模型为词汇表中的每个元素生成 logits。
4 对于序列中的每个位置,获取对应于实际出现在该位置的标记的对数概率。
这将打印以下输出:
Token: Can | Log Probability: -10.451835632324219
Token: LL | Log Probability: -9.275650978088379
Token: Ms | Log Probability: -14.926365852355957
Token: reason | Log Probability: -10.416162490844727
Token: counter | Log Probability: -8.359155654907227
Token: fact | Log Probability: -22.62082290649414
Token: ually | Log Probability: -11.302435874938965
Token: ? | Log Probability: -10.131906509399414
Token: <|endoftext|> | Log Probability: -11.475025177001953
将这些相加提供了模型下序列的联合概率。
当然,作为生成模型,GPT-2 可以根据前面的标记生成下一个标记。用户提供的 prompt 是序列的开始,模型的响应扩展了序列。
列表 13.5 从 LLM 生成
prompt = "Counterfactual reasoning would enable AI to" #1
input_ids = tokenizer.encode(prompt, return_tensors='pt') #1
output = model.generate( #2
input_ids, #2
max_length=25, #2
do_sample=True, #2
pad_token_id=tokenizer.eos_token_id #2
)
generated_text = tokenizer.decode(output[0], skip_special_tokens=True) #3
print(generated_text) #3
1 指定并编码提示。
2 从模型生成。参数“do_sample=True”表示我们在给定所有前面的标记的情况下,从下一个标记的概率分布中进行随机选择。
3 解码并打印输出。
这将打印出以下内容:
Counterfactual reasoning would enable AI to figure out what people want before they ask them. It would also enable self-awareness
再次注意,ChatGPT 的生成具有随机性,因此这可能会产生不同的结果。
关于混淆术语的说明
类似于 GPT 模型的模型通常被称为“因果语言模型”,但它们并不是本书中讨论的那种因果模型。它们不是 DGP 的因果模型。“因果”在这里指的是模型的自回归性质——模型仅根据前面的标记来评估序列中标记的概率。
所有这些都意味着 LLM 在基本层面上是一个标记序列的联合概率模型。这些模型的典型训练程序试图拟合标记的联合概率分布。像 GPT 这样的模型优化了在训练文档中根据前面的标记预测给定标记的能力。理解 LLM 是一个标记的概率模型并不能解释为什么 LLM 可以生成 连贯 的文本(即具有逻辑和一致思想相互关系、形成可理解整体的文本)。为此,我们需要理解 注意力。
13.2.2 注意力机制
LLMs(大型语言模型)成功的主要驱动力之一是使用Transformer 架构和其他依赖称为注意力机制的神经网络架构。注意力机制允许模型对输入序列的不同部分赋予不同的重要性。这使得模型能够学习“聚焦”于与特定任务更相关的序列的特定部分,同时“忽略”或赋予不太相关的部分较小的权重。
以以下关于树叶是火灾必要原因的条件反事实陈述为例:
劳动节周末的森林大火始于森林,由于地面上干燥的树叶而迅速蔓延。如果进行了可控燃烧,火势就不会如此迅速地蔓延。
注意力机制帮助模型认识到“leaves”指的是树叶,而不是离开,通过权衡周围词语如“ground”、“dry”和“forest”的相关性。
现代 LLMs 在许多神经网络层上堆叠了注意力机制。这使得 LLM 能够关注不同粒度级别上的概念。例如,注意力的第一层关注的是直接的词与词之间的关系,如“leaves”与“ground”,而接下来的几层则连接更广泛的短语,将“劳动节周末的森林大火”视为一个与短语“迅速蔓延”相连的单一实体。
后续层可以表示句子的主题或主题以及更广泛文本的更广泛主题,将“劳动节周末的森林大火”与有关其蔓延的信息联系起来。
13.2.3 从标记符到因果表示
从因果关系的角度来看,关于注意力如何使 LLM 学习高级抽象的能力对我们来说变得有趣。回想一下图 13.1,它首次出现在第五章(如图 5.4)。

图 13.1 来自第五章的示例,其中digit和is-handwritten是低级X**[i]像素的高级因果驱动因素
小方块代表图像中的像素,而digit和is-handwritten方块分别代表图像中描绘的数字以及它是否是手写的。在那个例子(第 5.1.2 节)中,我建议个体像素之间存在的任何因果关系对我们来说并不重要;我们感兴趣的是在图像中描绘的物体层面上进行推理。
与此类似的情况也发生在标记符上,如图 13.2 所示。

图 13.2 X[1]至X[12]是一系列标记符。标记符之间存在的任何结构(因果或其他)对我们来说都是短暂的兴趣。我们感兴趣的是由标记符描述的概念之间的因果关系。
在图 13.2 中,与像素一样,在标记级别存在一些结构。但这个结构与问题无关——我们感兴趣的是构成标记背后意义的概念之间的因果关系。
问题变成了,在什么情况下,注意力,只要它能学习高级表示,能够学习一个因果表示。例如,一个基于注意力的模型,可能在某些架构或学习约束下,或者在训练数据中使用干预措施,能够学习劳动节反事实陈述中的并行世界结构和抽象?
为了考虑这一点,我们可以回顾 LLM 的幻觉问题,并将其与因果识别联系起来。
13.2.4 幻觉、注意力和因果识别
关于必要性的概率的幻觉是由 GPT-4 生成的。同样的模型在 Netflix 关于必要性的概率问题上也给出了正确的答案。事实上,如果幻觉的答案仅仅陈述了正确的识别假设,那么这个答案就是正确的。我相信 GPT 和类似模型未来的版本可能会在第一次尝试时就正确回答这个问题。
但对于不熟悉必要性概率定义的人来说,他们如何知道模型是正确的还是产生了幻觉?首先,因果层次告诉我们,为了能够生成一个超出随机猜测的正确答案,查询需要与第 3 级信息相对应。也许这些信息是由用户在提示中提供的。也许 LLM 以某种方式学习了第 3 级表示(这样的主张需要硬性证据)。
如果用户在提示中提供了那个识别信息,用户如何知道模型是否成功使用了这些信息来响应提示?假设识别的要求存在并且被隐藏在学习的表示或数据中,并且模型在回答因果查询时成功地利用了这些信息,用户如何确信这种情况发生了?
我们需要设计解决方案来回答这些问题和其他期望,以构建向因果 AI 未来的发展。在下一节中,我们将从这个简单的因果 LLM 开始。
13.3 打造你自己的因果 LLM
在本节中,我们将绕过“最前沿的 LLM 能否进行因果推理?”这个问题,转而构建一个能够进行因果推理的因果 LLM。我们将从零开始构建因果性,而不是作为事后考虑。
13.3.1 用于脚本编写的 LLM
我们的数据通常具有一些隐含的因果结构。当我们将这种结构在训练中明确化时,基础模型可以学习更好的因果表示。
为了说明,假设一个历史悠久的电影制作工作室一直坚持要求他们的编剧使用需要遵循三幕叙事原型的剧本编写软件,这在浪漫喜剧中很常见:“男孩遇见女孩,男孩失去女孩,男孩找回女孩。”对于这个原型,他们有一系列许多剧本。在因果术语中,第一幕的事件导致第二幕的事件,第一幕和第二幕的事件导致第三幕的事件。我们可以在图 13.3 中绘制 DAG。

图 13.3 三幕原型的因果有向图
工作室与许多这样的原型一起工作,公司有许多遵循特定原型模板的剧本。假设一组原型涉及国王在第一幕中以某种方式行事,王子在第二幕中以某种方式行事,这两者在第三幕中对王国产生影响。例如,一个可能的原型是“国王宣战,王子领导军队,王国繁荣”。但每个幕都有多种结果:
-
第一幕中的国王:
-
第二幕中的王子:
-
第三幕中的王国:

图 13.4 表示各种国王-王子-王国原型的因果有向图
图 13.4 以因果有向图的形式显示了这种原型空间。
这只描述了 3 × 3 × 3 = 27 种可能的原型,但正如你所预期的,一些原型更常见,而一些原型则不太常见。我们可以很容易地通过在 pgmpy 或 Pyro 中显式编码因果马尔可夫核来对这些原型和联合概率分布进行建模。但这将只是一种基于原型的因果生成模型。如果我们想要一个剧本生成器,我们想要一个基于剧本的因果生成模型。
为了展示这个想法的证明概念,我们将使用短故事片段的训练数据集,而不是完整的剧本。让我们加载并检查训练数据。
列表 13.6 加载因果叙事数据
import pandas as pd
url = ("https://raw.githubusercontent.com/altdeep/"
"causalML/master/book/chapter%2013/"
"king-prince-kingdom-updated.csv")
df = pd.read_csv(url)
print(df.shape[0]) #1
print(df["King"][0] + "\n") #2
print(df["King"][1] + "\n") #2
print(df["King"][2]) #2
print("----")
print(df["Prince"][0] + "\n") #3
print(df["Prince"][1] + "\n") #3
print(df["Prince"][2]) #3
print("----")
print(df["Kingdom"][0] + "\n") #4
print(df["Kingdom"][1] + "\n") #4
print(df["Kingdom"][2]) #4
1 数据包含 21,000 个故事,分为三个简短的故事片段。
2 首先,国王采取行动。
3 然后王子采取行动。
4 最后,王国经历了皇室行动的后果。
此代码打印以下内容:
21000
----
King brokers a peace treaty with a rival kingdom, putting an end to years of bloody conflict
A wise king successfully negotiates peace with a rival nation
A wise king successfully negotiates peace between his kingdom and a long-time enemy
----
however, his son, the Prince, falls in love and marries a foreigner, causing political unrest
Prince falls in love with and marries a foreign princess, forging a strong alliance
but when a new threat emerges, the Prince leads the army to defend their realm
----
despite efforts, the ongoing war results in both kingdoms falling into poverty."
the alliance strengthens their forces, leading the kingdom to a victorious battle."
however, a series of misfortunes and disastrous decisions plunge their once prosperous kingdom into poverty."
有 21,000 组三个故事片段。前面的输出显示了数据集中的前三个组。
13.3.2 使用预训练模型进行因果马尔可夫核
为了训练我们 DAG 中每个节点的因果马尔可夫核,我们将从 Hugging Face Transformers 库中获取预训练模型,然后进一步使用我们的故事片段进行训练(也称为“微调”)。预训练处理了学习生成连贯自然语言文本的重活。微调将使模型朝着表示我们的因果马尔可夫核的方向调整。
首先,我们将使用 GPT-2 的一个变体来模拟国王的行动片段。作为一个文本补全模型,它通常接受一个提示作为输入,但我们将训练它使用空提示生成片段,并根据训练数据中国王行动文本的边缘概率生成片段,如图 13.5 所示。

图 13.5 GPT-2 经过微调,以表示国王行动片段的分布。
接下来,我们将使用 BART 模型来模拟王子行动的因果马尔可夫核。BART 是 2019 年发布的一个 Transformer 模型,专门设计用来接受一个输入序列并生成相应的输出序列,例如翻译或摘要。像 GPT-4 这样的大型模型可以很好地处理序列到序列的任务,但我们将使用一个参数数量大约比 GPT-4 少 4000 倍的 BART 版本,这使得它在您的笔记本电脑或基本的 Python 开发环境中加载和训练变得更加容易。给定国王的行动片段作为输入,它将生成一个王子的行动片段,如图 13.6 所示。
我们还将使用 BART 模型来模拟王国命运的因果马尔可夫核,如图 13.7 所示。该模型将国王和王子行动映射到王国的命运。

图 13.6 一个 BART 序列到序列模型经过微调,以表示在国王行动的情况下王子的行动。

图 13.7 一个 BART 序列到序列模型也用于模拟在国王行动和王子行动的情况下王国的命运。
提前跳过,我们感兴趣的是在王子采取特定行动的情况下,王国命运的条件概率分布。由于这需要根据王子的行动推断国王的行动,因此我们还将额外训练一个 BART 模型,该模型根据王子的行动片段生成国王的行动片段,如图 13.8 所示。

图 13.8 一个 BART 序列到序列模型也经过微调,以模拟国王和王子行动情况下的王国命运。
让我们运行训练过程。首先,我们将设置我们的导入和分词器。我们将使用 Bart 作为我们所有模型的分词器。
列表 13.7 训练因果 LLM
import torch
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset
from transformers import (
AutoModelForCausalLM, AutoModelForSeq2SeqLM,
AutoTokenizer, DataCollatorForLanguageModeling,
Seq2SeqTrainer, Seq2SeqTrainingArguments,
Trainer, TrainingArguments)
url = ("https://raw.githubusercontent.com/altdeep/"
"causalML/master/book/chapter%2013/"
"king-prince-kingdom-updated.csv")
df = pd.read_csv(url)
tokenizer = AutoTokenizer.from_pretrained("facebook/bart-base") #1
tokenizer.pad_token = tokenizer.eos_token #2
def tokenize_phrases(phrases, max_length=40): #3
return tokenizer(
phrases,
truncation=True,
padding='max_length',
max_length=max_length
)
1 设置分词器。
2 使用填充标记(pad token)使所有序列具有相同的长度,以便进行矩阵运算。通常将其设置为“序列结束(EOS)”标记。
3 将标记的最大长度设置为 40,因为所有的片段都少于 40 个标记。
接下来,我们将创建一个类和一个函数来分词国王数据集。我们将创建一个名为ModelDataset的 PyTorch Dataset类的自定义子类,该子类将存储标记编码及其对应的标签。当通过索引访问时,它返回一个包含该索引的标记编码及其相关标签的字典,并通过其__len__方法提供示例总数。
列表 13.8 对 King 片段进行分词
class ModelDataset(Dataset): #1
def __init__(self, encodings, labels): #1
self.encodings = encodings #1
self.labels = labels #1
#1
def __getitem__(self, idx): #1
item = { #A #1
key: torch.tensor(val[idx]) #1
for key, val in self.encodings.items() #1
} #A #1
item['labels'] = torch.tensor(self.labels[idx]) #1
return item #1
#1
def __len__(self): #1
return len(self.encodings.input_ids) #1
def create_king_dataset(input_phrases): #2
king_phrases = input_phrases.tolist() #2
king_encodings = tokenize_phrases(king_phrases) #2
king_dataset = ModelDataset( #2
king_encodings, #2
king_encodings['input_ids']) #2
return king_dataset #2
1 当通过索引访问时,ModelDataset 返回一个包含该索引的标记编码及其相关标签的字典。
2 为 King 片段创建一个 ModelDataset 实例。
接下来,我们将对 Prince 和 Kingdom 的片段进行分词。此代码还将生成用于训练序列到序列模型的验证数据集。
列表 13.9 对 Prince 和 Kingdom 片段进行分词
def create_seq2seq_datasets(input_phrases, target_phrases):
input_phrases_list = input_phrases.tolist()
target_phrases_list = target_phrases.tolist()
spit = train_test_split( #1
input_phrases_list, #1
target_phrases_list, #1
test_size=0.1 #1
) #1
train_inputs, val_inputs, train_targets, val_targets = spit #1
train_input_encodings = tokenize_phrases(train_inputs) #2
val_input_encodings = tokenize_phrases(val_inputs) #2
train_target_encodings = tokenize_phrases(train_targets) #2
val_target_encodings = tokenize_phrases(val_targets) #2
train_dataset = ModelDataset(
train_input_encodings, train_target_encodings['input_ids']
)
val_dataset = ModelDataset(
val_input_encodings, val_target_encodings['input_ids']
)
return train_dataset, val_dataset
1 将输入和目标短语分割成训练集和验证集。
2 对训练集和验证集进行编码。
接下来,我们将编写 King 模型的训练算法。此函数使用指定的参数初始化 GPT-2 模型,设置训练参数,并在提供的数据集上训练模型,最后将训练好的模型保存到指定的目录。
列表 13.10 训练 King 模型
def train_king_model(output_dir, train_dataset,
model_name="gpt2-medium", epochs=4):
king_model = AutoModelForCausalLM.from_pretrained(model_name) #1
training_args_king = TrainingArguments( #1
output_dir=output_dir, #1
per_device_train_batch_size=32, #1
overwrite_output_dir=True, #1
num_train_epochs=epochs, #1
save_total_limit=1, #1
save_steps=len(train_dataset) // 16, #1
max_grad_norm=1.0 #1
) #1
data_collator = DataCollatorForLanguageModeling( #1
tokenizer=tokenizer, #1
mlm=False) #1
trainer_king = Trainer( #2
model=king_model, #2
args=training_args_king, #2
data_collator=data_collator, #2
train_dataset=train_dataset, #2
) #2
trainer_king.train() #3
king_model.save_pretrained(output_dir)
return king_model
1 使用指定的参数初始化和配置模型。
2 配置训练设置。
3 训练模型。
接下来,我们将编写序列到序列模型的训练算法。该函数将提供的输入和目标短语分割成训练集和验证集,对它们进行分词,然后使用ModelDataset类创建并返回两个集合的 PyTorch Dataset对象。train_seq2seq_model函数使用指定的参数初始化序列到序列模型,配置其训练设置,然后使用训练集和验证数据集训练模型,最后返回训练好的模型。
列表 13.11 训练序列到序列模型的函数
def train_seq2seq_model(output_dir, train_dataset, val_dataset,
model_name="facebook/bart-base",
epochs=4):
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
training_args = Seq2SeqTrainingArguments( #1
output_dir=output_dir, #1
per_device_train_batch_size=16, #1
predict_with_generate=True, #1
logging_dir=f"{output_dir}/logs", #1
save_total_limit=1, #1
save_steps=len(train_dataset) // 16, #1
learning_rate=3e-5, #1
num_train_epochs=epochs, #1
warmup_steps=500, #1
weight_decay=0.01, #1
) #1
trainer = Seq2SeqTrainer( #2
model=model, #2
args=training_args, #2
train_dataset=train_dataset, #2
eval_dataset=val_dataset, #2
) #2
trainer.train() #3
model.save_pretrained(output_dir)
return model
1 使用指定的参数初始化和配置序列到序列模型。
2 配置训练设置。
3 使用训练集和验证数据集训练模型,最后返回训练好的模型。
现在我们将使用此函数来训练模型。我们将指定一些用于保存检查点的目录。
注意:在列表 13.14 中,我将提供从 Hugging Face 下载预训练模型的代码,所以如果你不想训练模型,可以跳过此步骤。
列表 13.12 训练 King、Prince 和 Kingdom 模型
import os
king_model_path = os.path.join(os.getcwd(), 'king_model') #1
prince_model_path = os.path.join(os.getcwd(), 'prince_model') #1
kingdom_model_path = os.path.join(os.getcwd(), 'kingdom_model') #1
prince2king_model_path = os.path.join( #1
os.getcwd(), 'prince2king_model') #1
king_dataset = create_king_dataset(df["King"]) #2
king_model = train_king_model(king_model_path, king_dataset) #2
datasets = create_seq2seq_datasets(df["King"], df["Prince"]) #3
train_dataset_prince, val_dataset_prince = datasets #3
prince_model = train_seq2seq_model( #3
prince_model_path, #3
train_dataset_prince, #3
val_dataset_prince, #3
epochs=6 #3
) #3
king_and_prince = [f"{k} {p}" for k, p in zip(df["King"], df["Prince"])] #4
df["King and Prince"] = king_and_prince #4
train_dataset_kingdom, val_dataset_kingdom = create_seq2seq_datasets( #4
df["King and Prince"], df["Kingdom"] #4
) #4
kingdom_model = train_seq2seq_model( #4
kingdom_model_path, #4
train_dataset_kingdom, #4
val_dataset_kingdom, #4
epochs=6 #4
) #4
1 提供你想要保存模型的输出目录。
2 使用 Seq2Seq 训练 King 模型。
3 使用 Seq2Seq 训练 Prince 模型。King 片段用于预测 Prince 片段。
4 使用 Seq2Seq 训练 Prince 模型。King 片段用于预测 Prince 片段。
5 使用 Seq2Seq 训练 Kingdom 模型。结合 King 和 Prince 的片段用于预测 Kingdom 片段。
最后,我们将训练另一个模型,用于根据 Prince 片段推断 King 片段。我们将在推理阶段使用这个模型。
列表 13.13 用于训练 Prince 到 King 模型的函数
p2k_data = create_seq2seq_datasets(
df["Prince"], df["King"])
train_dataset_prince2king, val_dataset_prince2king = p2k_data
prince2king_model = train_seq2seq_model(
prince2king_model_path,
train_dataset_prince2king,
val_dataset_prince2king,
epochs=6
)
运行前面的训练过程将需要一些时间,尤其是如果你没有使用 GPU。幸运的是,Hugging Face Hub 中有训练好的模型的保存版本。以下代码从 Hugging Face Hub 获取 Transformer 模型并从中生成。它还提供了一个计算每个生成序列对数概率的函数。
列表 13.14 从 Hugging Face Hub 获取 Transformer 模型并生成
import matplotlib.pyplot as plt
import pandas as pd
import torch
from sklearn.feature_extraction.text import TfidfVectorizer
from transformers import (
AutoModelForCausalLM, AutoModelForSeq2SeqLM,
AutoTokenizer, GPT2LMHeadModel,
PreTrainedModel, BartForConditionalGeneration)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
king_model = AutoModelForCausalLM.from_pretrained( #1
"osazuwa/causalLLM-king").to(DEVICE) #1
prince_model = AutoModelForSeq2SeqLM.from_pretrained( #1
"osazuwa/causalLLM-prince").to(DEVICE) #1
kingdom_model = AutoModelForSeq2SeqLM.from_pretrained( #1
"osazuwa/causalLLM-kingdom").to(DEVICE) #1
prince2king_model = AutoModelForSeq2SeqLM.from_pretrained( #1
"osazuwa/causalLLM-prince2king").to(DEVICE) #1
tokenizer = AutoTokenizer.from_pretrained("facebook/bart-base") #2
tokenizer.pad_token = tokenizer.eos_token #2
1 加载我们模型的组件。
2 加载我们模型的组件。
3 加载 Bart-base 分词器,并将填充标记设置为序列结束标记。
接下来,我们将编写一些函数来编码文本为标记,解码标记为文本,以及根据输入文本从模型生成文本。
列表 13.15 编码、解码和生成辅助函数
def encode(text:str, device=DEVICE) -> torch.tensor: #1
input_ids = tokenizer.encode(text, return_tensors="pt") #1
input_ids = input_ids.to(device) #1
return input_ids #1
def decode(text_ids: torch.tensor) -> str: #2
output = tokenizer.decode(text_ids, skip_special_tokens=True) #2
return output #2
EMPTY_TEXT = torch.tensor(tokenizer.encode("")).unsqueeze(0).to(DEVICE) #3
def generate_from_model(model: PreTrainedModel, #4
input_sequence: torch.tensor = EMPTY_TEXT, #4
max_length: int = 25, #4
temperature=1.0): #4
output = model.generate( #4
input_sequence, #4
max_length=max_length, #4
do_sample=True, #4
pad_token_id=tokenizer.pad_token_id, #4
eos_token_id=tokenizer.pad_token_id, #4
temperature=temperature, #4
top_p=0.9, #4
) #4
return output #4
def convert_to_text(output):
return decode(output[0]).strip().capitalize()
1 将文本编码成张量。
2 将张量解码为文本。
3 获取空文本的编码,以便于使用。
4 一个从模型生成内容的函数。这些参数对于 GPT-2 和 BART 模型来说执行不同的功能,但它们大致上是重叠的。
我们希望使用我们的概率机器学习方法,因此我们需要一种计算生成序列对数概率的方法,这样我们就可以在推理中使用这些值。以下函数基于 GPT-2 和 BART 模型产生的相关值计算生成序列的对数概率。
列表 13.16 计算生成序列的对数概率
def compute_log_probs(model, output_sequence):
if isinstance(model, GPT2LMHeadModel): #1
outputs = model( #1
input_ids=output_sequence, #1
labels=output_sequence #1
) #1
log_softmax = torch.nn.functional.log_softmax( #1
outputs.logits, dim=-1) #1
log_probs = log_softmax.gather(2, output_sequence.unsqueeze(-1)) #1
log_probs = log_probs.squeeze(-1).sum(dim=-1) #1
elif isinstance(model, BartForConditionalGeneration):
outputs = model( #2
input_ids=output_sequence, #2
labels=output_sequence) #2
loss = outputs.loss #2
log_probs = -loss * output_sequence.size(1) #2
else:
raise ValueError("Unsupported model type")
return torch.tensor(log_probs.item())
1 将 GPT-2 的 logits 转换为 logprobs。
2 将 logits 从 BART 交叉熵转换为 logprobs。
最后,我们将把这些部分组合起来,从我们的三个模型生成一个完整的故事。
列表 13.17 生成完整故事
king_output = generate_from_model(king_model) #1
king_statement = convert_to_text(king_output) #1
print("Generated from king_nodel:", king_statement) #1
log_prob_king = compute_log_probs(king_model, king_output) #1
print("Log prob of generated king text:", log_prob_king) #1
prince_output = generate_from_model(prince_model, king_output) #2
prince_statement = convert_to_text(prince_output) #2
print("Generated from prince_model:", prince_statement) #2
log_prob_prince = compute_log_probs(prince_model, prince_output) #2
print("Log prob of generated prince text:", log_prob_prince) #2
king_prince_statement = king_statement + ". " + prince_statement #3
king_prince_output = encode(king_prince_statement) #3
kingdom_output = generate_from_model(kingdom_model, king_prince_output) #3
kingdom_statement = convert_to_text(kingdom_output) #3
print("Generated from kingdom model:", kingdom_statement) #3
log_prob_kingdom = compute_log_probs(kingdom_model, kingdom_output) #3
print("Log prob of generated kingdom text:", log_prob_kingdom) #3
king_output_infer = generate_from_model(prince2king_model, prince_output) #4
king_statement_infer = convert_to_text(king_output_infer) #4
print("Generated statement from prince2king:", king_statement_infer) #4
log_prob_prince2king = compute_log_probs(prince2king_model, prince_output) #4
print("Log prob of generated inference text:", log_prob_prince2king) #4
1 从基于 GPT 的模型生成关于国王的短篇故事,并计算生成序列的对数概率。
2 从基于 BART 的序列到序列模型生成关于王子的短篇故事,给定关于国王的短篇故事,然后计算生成序列的对数概率。
3 从基于 BART 的序列到序列模型生成关于王国的短篇故事,给定关于国王和王子的短篇故事,然后计算生成序列的对数概率。
4 另一个基于 BART 的序列到序列模型,将关于王子的短篇故事映射到关于国王的短篇故事。我们将使用这个模型从关于王子的短篇故事中推断出关于国王的短篇故事。
输出是非确定性的,但你会得到的一个输出示例如下:
Generated statement from king_model: The king, driven by ambition, declares war on a neighboring nation to expand his kingdom's territories, declares war on.
Log probability of generated king_model: tensor(-325.8379)
Generated statement from prince_model: The prince, disillusioned by his father's actions, abdicates the throne in protest.
Log probability of generated prince text: tensor(-18.2486)
Generated statement from kingdom model: As the war drags on, resources are depleted, and the once-prosperous kingdom falls.
Log probability of generated kingdom text: tensor(-38.3716)
Generated statement from prince2king: A king, driven by greed, declares war on a neighboring kingdom.
Log probability of generated inference text: tensor(-297.3446)
注意,生成的输出并不完美——例如,第一个生成的陈述理想情况下应该在“…王国的领土”之后停止。我们可以尝试进一步训练它或切换到一个更强大的模型,但作为开始来说这已经相当不错了。
接下来,我们将使用这些 Transformers 库模型在 Pyro 中定义分布,然后使用 Pyro 构建一个因果生成模型。首先,我们将使用 Pyro 的 TorchDistributionMixin 来使用语言模型对因果马尔可夫核进行建模。我们将使用国王短篇故事的 GPT-2 模型来创建 King 变量的因果马尔可夫核。
接下来,我们将使用 BART 模型为 Prince 变量创建因果马尔可夫核。King 变量导致这个变量,因此 seq2seq 模型使用 King 变量的值为此模型生成一个值。
最后,我们将为 Kingdom 变量创建因果马尔可夫核。King 和 Prince 变量是因果父变量,因此我们将它们的生成输出连接成一个字符串,并使用该字符串生成 Kingdom 输出,再次使用 BART seq2seq 模型。我们依赖于一个名为 TorchDistributionMixin 的混合器,它对于将 PyTorch 分布包装用于 Pyro 非常有用。
列表 13.18 从 Transformer 模型构建 Torch 分布
import pyro
from pyro.distributions.torch_distribution \
import TorchDistributionMixin
class TransformerModelDistribution(TorchDistributionMixin):
def __init__(self, model: PreTrainedModel,
input_encoding: torch.tensor = EMPTY_TEXT,
):
super().__init__()
self.model = model
self.input_encoding = input_encoding
def sample(self, sample_shape=torch.Size()): #1
output = generate_from_model( #1
self.model, self.input_encoding #1
) #1
return output #1
def log_prob(self, value): #2
return compute_log_probs(self.model, value) #2
1 使用 TorchDistributionMixin 将 Transformers 模型转换为 Pyro 分布。TorchDistributionMixin 用于使 PyTorch 分布与 Pyro 的实用工具兼容。
2 log_prob 方法返回用于推理算法的对数概率。
现在,我们将使用该分布于 Pyro 中。
列表 13.19 使用 Pyro 将 Transformer 模型集成到因果模型中
def causalLLM(): #1
king = pyro.sample( #2
"King", TransformerModelDistribution(king_model) #2
) #2
prince = pyro.sample( #3
"Prince", TransformerModelDistribution( #3
prince_model, king) #3
) #3
king_and_prince = torch.cat([king, prince], dim=1) #4
kingdom = pyro.sample( #4
"Kingdom", TransformerModelDistribution( #4
kingdom_model, king_and_prince) #4
) #4
king_text = convert_to_text(king) #5
prince_text = convert_to_text(prince) #5
kingdom_text = convert_to_text(kingdom) #5
return king_text, prince_text, kingdom_text #5
for _ in range(2): #6
king, prince, kingdom = causalLLM() #6
vignette = " ".join([king, prince, kingdom]) #6
print(vignette) #6
1 构建因果 LLM。
2 为国王变量创建因果马尔可夫核。
3 为王子变量创建因果马尔可夫核。
4 为王国变量创建因果马尔可夫核。
5 将所有生成的短篇故事连接成一个整体故事并返回结果。
6 确认我们的因果模型生成了完整的短篇故事。
之前的代码生成并打印了两个短篇故事,如下所示:
And beloved king falls gravely ill, leaving the kingdom in despair in uncertainty over the inexperienced prince to lead the kingdom. The young prince, eager to prove himself, leads the army into a costly and ill-advised war. As a result, the kingdom's resources are depleted, plunging the once-prosperous land into.
King, fueled by ambition, declares war on a neighboring realm, leaving his subjects anxious and. The prince, disillusioned by his father's actions, abdicates the throne in search of a new life. Without strong leadership, the kingdom spirals into poverty and despair.
我们看到生成的文本相当不错,尽管它们似乎提前截断了。通过调整生成参数,可以解决这些问题以及其他生成问题。
就这样,我们已经构建了一个因果 LLM,这是一个基于因果 DAG 框架构建的 LLM。让我们通过比较 DAG 所蕴含的观察分布和干预分布来证明我们有一个因果模型。
13.3.3 从干预和观察分布中进行采样
到现在为止,你知道分布 P(Kingdom|Prince=x) 将与 P(Kingdom[Prince][=][x]) 不同,但让我们通过这个因果 LLM 来证明这个事实。首先,我们将建模 P(Kingdom|Prince=x),其中 x 是
他的勇敢的王子指挥军队,在一场又一场的战斗中取得胜利
要推断 P(Kingdom|Prince=x),我们必须推断潜在混杂因子 King 的分布。我们将使用我们训练的 prince2king_model 来完成这项工作。我们将使用一种名为“重要性重采样”的概率推理算法。我们将首先创建一个提议函数(Pyro 称之为“引导函数”),该函数将根据王子生成 King 和 Kingdom 的样本。
列表 13.20 P(Kingdom|Prince=x) 的提议分布
import pyro.poutine as poutine
from pyro.distributions import Categorical
PRINCE_STORY = ( #1
"His courageous Prince takes command, leading " #1
"the kingdom's army to victory in battle after battle") #1
cond_model = pyro.condition( #1
causalLLM, {"Prince": encode(PRINCE_STORY)}) #1
def proposal_given_prince(): #2
prince = encode(PRINCE_STORY) #2
king = pyro.sample( #3
"King", #3
TransformerModelDistribution(prince2king_model, prince) #3
) #3
king_and_prince = torch.cat([king, prince], dim=1) #3
kingdom = pyro.sample( #4
"Kingdom", #4
TransformerModelDistribution(kingdom_model, king_and_prince) #4
) #4
vignette = (convert_to_text(king) + #5
PRINCE_STORY + #5
convert_to_text(kingdom)) #5
return vignette
1 我们将模型的条件设置为王子变量的这个值。
2 我们将使用提议函数从我们的目标分布 P(King, Kingdom|Prince=PRINCE_STORY) 中生成样本。
3 提议使用 prince2king_model 来推断王子=PRINCE_STORY 时的国王值。
4 给定王子值和推断出的国王值,使用 king_and_prince 模型来抽取王国。
5 将生成的国王标记和提供的王子标记连接起来,以返回一个生成的短篇故事,这样我们就可以检查所抽取的内容。
现在我们将根据在条件模型下样本的概率与在提议下样本的概率的比率来权衡每个样本。使用这些权重重采样样本将生成来自目标分布的样本。Pyro 提供了重要性采样的实用工具,但由于生成的序列长度不一,直接实现重要性采样将更容易。
首先,我们将编写一个函数来处理样本并获取其重要性权重。
列表 13.21 用于抽取重采样样本的函数
def process_sample(model, proposal):
sample_trace = poutine.trace(proposal).get_trace() #1
king_text = convert_to_text(sample_trace.nodes['King']['value']) #1
kingdom_text = convert_to_text( #1
sample_trace.nodes['Kingdom']['value']) #1
proposal_log_prob = sample_trace.log_prob_sum() #2
replay = poutine.replay(model, trace=sample_trace) #3
model_trace = poutine.trace(replay).get_trace() #3
model_log_prob = model_trace.log_prob_sum() #3
log_importance_weight = model_log_prob - proposal_log_prob #4
sample = (king_text, kingdom_text, log_importance_weight)
return sample
1 从提议中提取一个样本。
2 计算国王和王国样本值的总对数概率。
3 计算在原始模型下国王和王国样本值的总对数概率。
4 计算对数重要性权重。
现在我们将运行重要性重采样。
列表 13.22 列表 13.22 P(Kingdom|Prince=x) 的重要性重采样
def do_importance_resampling(model, proposal, num_samples): #1
original_samples = []
for _ in range(num_samples):
sample = process_sample(model, proposal)
original_samples.append(sample)
unique_samples = list(set(original_samples)) #2
log_importance_weights = torch.tensor( #2
[sample[2] for sample in original_samples]) #2
resampling_dist = Categorical(logits=log_importance_weights) #2
resampled_indices = resampling_dist.sample_n(num_samples) #2
samples = pd.DataFrame( #2
[unique_samples[i] for i in resampled_indices], #2
columns=["King", "Kingdom", "log_importance_weight"] #2
) #2
samples["Prince"] = PRINCE_STORY
samples["Distribution"] = "observational"
return samples[['King', 'Prince', 'Kingdom', 'Distribution']]
num_samples = 1000
posterior_samples = do_importance_resampling(
cond_model, proposal_given_prince, num_samples)
1 使用重要性重采样作为我们的推理过程。
2 使用重要性权重进行重采样。将对数权重传递给“logits”参数。
接下来,我们将推断 P(Kingdom[Prince][=][x])。鉴于我们在 Pyro 中的因果模型,我们可以使用 Pyro 的 do-operator 来应用干预。我们知道,在王子干预下,国王到王子的边被移除,因此我们不需要使用 prince2king_model。我们可以简单地从我们的干预模型中进行普通的前向生成。
列表 13.23 使用纯前向蒙特卡洛采样推断 P(KingdomPrince=x)
intervention_model = pyro.do( #1
causalLLM, {"Prince": encode(PRINCE_STORY)}) #1
intervention_samples = pd.DataFrame( #2
[intervention_model() for _ in range(num_samples)], #2
columns=["King", "Prince", "Kingdom"] #2
) #2
intervention_samples["Distribution"] = "interventional" #2
all_samples = pd.concat( #2
[posterior_samples, intervention_samples], #2
ignore_index=True #2
) #2
1 从干预分布中进行前向样本抽取。
2 标记样本,并将它们与观察样本结合起来。
生成样本将需要一些时间。由于我们直接在 Pyro 中使用编码序列张量工作,我们可以利用潜在的更快基于梯度的推理算法。为了方便,您可以在 GitHub 仓库的书目录中访问预先保存的样本:github.com/altdeep/causalml。
接下来,让我们可视化分布之间的差异。我们需要一种方法来可视化来自干预和观察分布的样本文本。我们可以使用 TF-IDF(词频-逆文档频率),这是一种数值统计量,反映了单词在样本集合中对样本的重要性,强调了特定样本中独特的单词。
列表 13.24 获取P(Kingdom**[Prince=x])和P(Kingdom|Prince=x)的生成 TF-IDF。
import pandas as pd
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
kingdom_samples_url = (
"https://raw.githubusercontent.com/altdeep/causalML/"
"master/book/chapter%2013/kingdom_samples.csv")
all_samples = pd.read_csv(kingdom_samples_url)
observational_texts = all_samples[ #1
all_samples["Distribution"] == "observational"]["Kingdom"] #1
interventional_texts = all_samples[all_samples[ #1
"Distribution"] == "interventional"]["Kingdom"] #1
vectorizer = TfidfVectorizer(stop_words='english') #2
X_obs = vectorizer.fit_transform(observational_texts) #2
X_int = vectorizer.transform(interventional_texts) #2
k = 10 #3
feature_names = vectorizer.get_feature_names_out() #3
obs_indices = X_obs.sum(axis=0).argsort()[0, -k:][::-1] #3
int_indices = X_int.sum(axis=0).argsort()[0, -k:][::-1] #3
combined_indices = np.concatenate((obs_indices, int_indices)) #3
combined_indices = np.unique(combined_indices) #3
1 从观察和干预分布中提取生成的王国场景。
2 计算每个组中生成的王国场景的 TF-IDF 值。
3 获取每个集合中 TF-IDF 评分最高的 k=7 个单词。
最后,我们将可视化这两个分布。
列表 13.25 可视化对比P(Kingdom**[Prince=x])和P(Kingdom|Prince=x)。
import matplotlib.pyplot as plt
labels = [feature_names[i] for i in combined_indices] #1
labels, indices = np.unique(labels, return_index=True) #1
obs_values = np.array(X_obs.sum(axis=0))[0, combined_indices] #1
int_values = np.array(X_int.sum(axis=0))[0, combined_indices] #1
obs_values = [obs_values[0][i] for i in indices] #1
int_values = [int_values[0][i] for i in indices] #1
combined = list(zip(labels, obs_values, int_values)) #1
sorted_combined = sorted(combined, key=lambda x: (-x[1], x[2])) #1
labels, obs_values, int_values = zip(*sorted_combined) #1
width = 0.35 #2
x = np.arange(len(labels)) #2
fig, ax = plt.subplots() #2
rects1 = ax.bar(x - width/2, obs_values, width, #2
label='Observational', alpha=0.7) #2
rects2 = ax.bar(x + width/2, int_values, width, #2
label='Interventional', alpha=0.7) #2
ax.set_xlabel('Words') #2
ax.set_ylabel('TF-IDF Values') #2
ax.set_title( #2
'Top Words in Generated Kingdom Vignettes by TF-IDF Value') #2
ax.set_xticks(x) #2
ax.set_xticklabels(labels) #2
ax.legend() #2
fig.tight_layout() #2
plt.xticks(rotation=45) #2
plt.show() #2
1 准备条形图的数据。
2 生成图表。
这产生了图 13.9。
图 13.9 显示了观察案例中单词的相似 TF-IDF 评分。这是由于观察案例中缺乏变化,因为观察王子限制了国王的可能值。当我们对王子进行干预时,国王可以变化更多,导致结果有更多的变化。

图 13.9 使用 TF-IDF 可视化来自P(Kingdom**[Prince][=][x])和P(Kingdom|Prince=x)的样本之间的差异,其中x是王子带领军队去战斗。由于推断的国王场景变化很小,观察值是平的。干预使国王场景有更多的变化,从而王国场景也有更多的变化。
13.3.4 结束语
这是一个简单的玩具问题,使用简单的 LLM 在简单数据上训练了一个简单的 DAG。但我们可以将其扩展到更复杂的 DAG 和微调更先进的模型。在基础模型中结合因果假设也可能有其他方法。我们只是探索这个令人兴奋空间的开始。
摘要
-
大型语言模型(LLM)是强大的 AI 模型,可以生成文本和其他模态,并在各种基准测试中实现高性能。
-
LLM 在支持因果分析方面已经证明了其用途。
-
LLM 可以帮助构建因果 DAG。此外,它们可以利用关于因果关系和机制的共同和专业知识。
-
因果框架问题是在给定问题中选择相关因果变量并排除无关变量的挑战。最前沿的 LLM 模拟人类设置因果框架的方式,这对于构建 DAG 和根本原因分析等应用很有用。
-
LLMs 可以帮助我们理解细微的因果关系概念以及如何在我们的兴趣领域内对其进行语境化。
-
LLMs 可以帮助我们将因果关系查询转化为形式化术语。
-
LLMs 容易产生幻觉——对我们查询的令人信服但错误的响应。
-
在其核心,LLMs 是概率机器学习模型,它们对标记序列的联合概率分布进行建模。
-
注意力机制使得 LLMs 能够学习高级表示,这使得最前沿的 LLMs 如此强大。
-
即使 LLMs 学习到了高级表示,并不意味着它学习到了因果关系表示。即使在某些特殊情况下这确实有效,用户也很难验证它是否在起作用。
-
我们可以通过在因果 DAG 框架上组合微调的 LLMs 来构建自己的因果 LLMs。这允许我们在承认因果操作,如 do 运算符的同时,使用最前沿的 LLMs。
-
将因果层次理论作为你在探索如何将因果关系与 LLMs 和多模态模型相结合的指南星,同时探索这些模型在自身学习因果关系表示方面的能力。
[1] C. Glymour, “Android epistemology and the frame problem,” in Z.W. Pylyshyn, ed., 《机器人的困境:人工智能中的框架问题》 (Praeger, 1987), pp. 63–75。







浙公网安备 33010602011771号