Python-贝叶斯分析第三版-全-
Python 贝叶斯分析第三版(全)
原文:
annas-archive.org/md5/23c902bea72dd81eede00e0b57b70813译者:飞龙
第一章
概率思维
概率论无非是将常识简化为计算。——皮埃尔·西蒙·拉普拉斯
在这一章中,我们将学习贝叶斯统计的核心概念以及贝叶斯工具箱中的一些工具。我们会使用一些 Python 代码,但这一章大部分内容将是理论性的;我们在这里看到的大部分概念将在本书的许多部分中反复出现。这一章理论性较强,可能会让你这个编码者有些焦虑,但我认为它会为有效地将贝叶斯统计应用于你的问题铺平道路。
在这一章中,我们将涵盖以下主题:
-
统计建模
-
概率与不确定性
-
贝叶斯定理与统计推断
-
单参数推断和经典的抛硬币问题
-
选择先验分布及为何人们通常不喜欢它们,但其实应该喜欢
-
传达贝叶斯分析结果
1.1 统计学、模型和本书的方法
统计学是关于收集、整理、分析和解释数据的,因此统计知识对数据分析至关重要。在数据分析中使用了两种主要的统计方法:
-
探索性数据分析(EDA):这涉及到数值汇总,例如均值、众数、标准差和四分位数范围。EDA 还涉及通过可视化检查数据,使用你可能已经熟悉的工具,比如直方图和散点图。
-
推断统计:这是关于超越当前数据进行推理。我们可能想要了解某种特定现象,或者我们想要对未来(尚未观察到的)数据点进行预测,或者我们需要在多个竞争性解释之间做出选择,针对同一组观察数据。总之,推断统计让我们能够从有限的数据中提取有意义的见解,并基于分析结果做出明智的决策。
天作之合
本书的重点是如何进行贝叶斯推断统计,但我们也会借用探索性数据分析(EDA)中的一些思想来总结、解释、检查并传达贝叶斯推断的结果。
大多数入门级统计课程,至少对于非统计学专业的人来说,通常被教授为一系列的“配方”,大致如下:走进统计学的储藏室,挑选一罐罐头打开,加入数据,按个人口味调味,搅拌直到得到一个一致的 p 值,最好小于 0.05。 这些课程的主要目标是教你如何选择合适的罐头。我从不喜欢这种方法,主要是因为最常见的结果是一群困惑的人,甚至在概念层面也无法理解不同学习方法的统一性。我们将采取不同的方法:我们将学习一些配方,但它们是自制的,而非罐头食品;我们将学习如何混合新鲜的原料,适用于不同的统计场合,更重要的是,这将让你能将这些概念应用到本书中的例子之外的场景中。
采用这种方法是因为两个原因:
-
本体论:统计学是一种建模方法,统一于概率论的数学框架下。采用概率方法能够提供一个统一的视角来看待那些看似截然不同的方法;统计方法和机器学习方法在概率视角下显得更加相似。
-
技术性:现代软件,如 PyMC,使得从业者——就像你我一样——能够相对轻松地定义和解决模型。几年前,许多这样的模型是无法求解的,或者需要高水平的数学和技术精湛。
1.2 处理数据
数据是统计学和数据科学的核心要素。数据来源于多个渠道,例如实验、计算机模拟、调查和实地观察。如果我们负责生成或收集数据,首先仔细思考我们想要回答的问题以及我们将使用哪些方法是非常重要的,只有在此之后,我们才应该开始收集数据。统计学中有一个专门研究数据收集的分支,称为实验设计。在数据泛滥的时代,我们有时会忘记,收集数据并不总是便宜的。例如,虽然大型强子对撞机(LHC)每天能产生数百 TB 的数据,但它的建造过程花费了数年的人工和智力劳动。
一般来说,我们可以认为数据生成的过程是随机的,因为存在本体论、技术性和/或认识论的不确定性,也就是说,系统本质上是随机的,技术性问题会增加噪声或限制我们以任意精度进行测量,和/或存在概念性局限遮蔽了我们无法看到的细节。基于这些原因,我们总是需要在模型的框架下解读数据,包括心智模型和形式化模型。数据不直接发声,只有通过模型才有意义。
在本书中,我们假设我们已经收集好了数据。我们的数据也将是干净且整洁的,而这在现实世界中是极少见的。我们作出这些假设是为了集中讨论本书的主题。我特别想强调,尤其是对于数据分析的新人来说,即使本书没有涉及,仍然有一些重要的技能需要你去学习和实践,以便能够成功地处理数据。
分析数据时,一个非常有用的技能是知道如何在编程语言中编写代码,例如 Python。由于我们生活在一个杂乱的世界中,数据更加杂乱,因此操控数据通常是必要的,编程有助于完成任务。即使你很幸运,数据非常干净整洁,编程仍然非常有用,因为现代贝叶斯统计主要通过像 Python 或 R 这样的编程语言进行。如果你想学习如何使用 Python 来清理和操控数据,可以参考 McKinney 的 Python for Data Analysis 一书 [2022]。
1.3 贝叶斯建模
模型是对某个系统或过程的简化描述,出于某些原因,我们对此系统或过程感兴趣。这些描述是刻意设计的,只捕捉系统中最相关的方面,而不解释每一个微小的细节。这也是为什么更复杂的模型不一定是更好的模型的原因之一。有许多不同种类的模型;在本书中,我们将只讨论贝叶斯模型。我们可以用三个步骤总结贝叶斯建模过程:
-
给定一些数据和关于这些数据如何生成的假设,我们通过结合称为概率分布的构建模块来设计一个模型。大多数时候,这些模型是粗略的近似,但大多数情况下,这正是我们所需要的。
-
我们使用贝叶斯定理将数据添加到我们的模型中,并推导出结合数据和假设的逻辑后果。我们说我们正在对模型进行条件化。
-
我们根据不同的标准评估模型及其预测,包括数据、我们对该主题的专业知识,有时还会通过与其他模型进行比较。
通常,我们会发现自己在一个迭代的非线性方式中执行这三个步骤。我们会在任何时候重新追溯我们的步骤:也许我们犯了一个愚蠢的编码错误,或者我们找到了一种方法来改变模型并改进它,或者我们意识到需要添加更多的数据或收集不同种类的数据。
贝叶斯模型也被称为概率模型,因为它们是通过概率构建的。为什么是概率?因为概率是建模不确定性的非常有用的工具;我们甚至有充分的理由认为它们是正确的数学概念。所以让我们一起走进 叉路花园 [Borges, 1944]。
1.4 贝叶斯实践者的概率入门
在本节中,我们将讨论一些对于更好理解贝叶斯方法至关重要的通用概念和重要概念。未来的章节将根据需要介绍或详细说明其他与概率相关的概念。然而,对于概率论的详细学习,我强烈推荐 Blitzstein 的《Introduction to Probability》[2019]一书。已经熟悉概率论基本要素的读者可以跳过本节或快速浏览。
1.4.1 样本空间与事件
假设我们正在调查人们对自己所在地区天气的看法。我们问了三个人是否喜欢晴天,可能的回答是“是”或“否”。所有可能结果的样本空间可以用S表示,并包含八种可能的组合:
S = {(是, 是, 是), (是, 是, 否), (是, 否, 是), (否, 是, 是), (是, 否, 否), (否, 是, 否), (否, 否, 是), (否, 否, 否)}
在这里,样本空间中的每个元素代表三个人根据被问到的顺序所做出的回答。例如,(是, 否, 是)表示第一和第三个人回答了“是”,而第二个人回答了“否”。
我们可以将事件定义为样本空间的子集。例如,事件A就是所有三个人都回答“是”时发生的事件:
A = {(是, 是, 是)}
类似地,我们可以定义事件B为至少有一个人回答“否”的情况,然后我们将得到:
B = {(是, 是, 否), (是, 否, 是), (否, 是, 是), (是, 否, 否), (否, 是, 否), (否, 否, 是), (否, 否, 否)}
我们可以使用概率来衡量这些事件发生的可能性。假设所有事件发生的概率相等,那么事件A的概率,即所有三个人都回答“是”的事件概率为:

在这种情况下,A中只有一个结果,而S中有八个结果。因此,A的概率为:

同样,我们可以计算事件B的概率,这个事件表示至少有一个人回答“否”。由于B中有七个结果,而S中有八个结果,事件B的概率为:

将所有事件视为同样可能的事件只是一个特殊情况,它使得计算概率更为简便。这被称为朴素的概率定义,因为它具有局限性并依赖于强假设。然而,如果我们谨慎使用,它仍然是有用的。例如,并不是所有的“是-否”问题都有 50-50 的概率。再举个例子,看到一匹紫色的马的概率是多少?正确答案可以有很大的不同,具体取决于我们是在谈论一匹真实马的自然颜色、卡通中的马、一匹穿着游行服装的马,等等。无论事件是否等可能,整个样本空间的概率总是等于 1。我们可以通过计算来验证这一点:

1 是概率能达到的最高值。说P(S) = 1 就意味着S不仅非常可能,它是确定的。如果S定义了所有可能发生的事情,那么S一定会发生。
如果一个事件是不可能的,那么它的概率就是 0。我们定义事件C为三个人都说“香蕉”的事件:
C = {(香蕉, 香蕉, 香蕉)}
由于C不是S的一部分,根据定义,它是无法发生的。可以把它看作是我们的调查问卷只有两个选项,yes和no。根据设计,我们的调查限制了所有其他可能的选项。
我们可以利用 Python 包含集合的事实,并定义一个 Python 函数来按照它们的朴素定义计算概率:
代码 1.1
def P(S, A):
if set(A).issubset(set(S)):
return len(A)/len(S)
else:
return 0
我把玩这个函数的乐趣留给读者了。
一种有用的理解概率的方式是将概率视为分布在样本空间中的守恒量。这意味着如果一个事件的概率增加,其他一些事件的概率必须减少,以便总概率保持为 1。可以通过一个简单的例子来说明这一点。
假设我们问某人明天是否会下雨,可能的回答是“是”或“否”。可能的回答的样本空间是S = {是, 否}。表示明天会下雨的事件是A = {是}。如果P(A)是 0.5,那么事件A的补集事件的概率,即P
,也必须是 0.5。如果由于某种原因P(A)增加到 0.8,那么P
必须减少到 0.2。这个特性适用于互斥事件,即不能同时发生的事件。例如,明天不可能同时“下雨”和“不下雨”。你可能会反驳,早上可能下雨,下午不下雨。没错,但那是不同的样本空间!
到目前为止,我们避免了直接定义概率,而是展示了一些概率的性质以及计算方法。适用于非等可能事件的概率的一个一般定义如下。给定一个样本空间S,以及事件A,它是S的一个子集,概率是一个函数P,它以A为输入,返回一个介于 0 和 1 之间的实数作为输出。函数P有一些限制,这些限制由以下三个公理定义。请记住,公理是被认为为真的陈述,我们用它作为推理的起点:
-
事件的概率是一个非负实数。
-
P(S) = 1
-
如果A1, A2, …是互斥事件,意味着它们不能同时发生,那么P(A1, A2, …) = P(A1) + P(A2) + …
如果这是一本关于概率论的书,我们可能会专门用几页来展示这些公理的后果,并提供一些练习来操作概率。这将帮助我们熟练地操作概率。然而,我们的主要关注点不在这些话题上。我展示这些公理的动机仅仅是为了说明概率是一个定义明确的数学概念,并且有规则来支配它们的运算。它们是特定类型的函数,并且并不神秘。
1.4.2 随机变量
随机变量是一个将样本空间映射到实数ℝ的函数(见图 1.1)。假设我们关注的事件是骰子的点数,映射非常简单,我们将
与数字 1 关联,
与 2,依此类推。另一个简单的例子是回答问题“明天会下雨吗?”,我们可以将“是”映射为 1,将“否”映射为 0。通常,随机变量使用大写字母表示,如X,而其结果使用小写字母表示,如x。例如,如果X表示一次骰子投掷,那么x表示某个特定的整数{1,2,3,4,5,6}。因此,我们可以写P(X = 3)来表示投掷骰子得到 3 的概率。我们也可以不指定x,例如,我们可以写P(X = x)来表示得到某个值x的概率,或者写P(X ≤ x),表示得到小于或等于x的概率。
能够将符号如
或字符串如“yes”映射到数字上,使得分析变得更加简单,因为我们已经知道如何用数字进行数学运算。随机变量也很有用,因为我们可以在不直接考虑样本空间的情况下对它们进行操作。随着样本空间变得越来越复杂,这一特点变得愈加重要。例如,在模拟分子系统时,我们需要指定每个原子的位置信息和速度;对于像蛋白质这样复杂的分子,这意味着我们需要追踪成千上万甚至更多的数字。相反,我们可以使用随机变量来总结系统的某些属性,比如总能量或系统中某些原子之间的相对角度。
如果你仍然感到困惑,那也没关系。随机变量的概念刚开始可能显得过于抽象,但我们将在全书中看到许多例子,帮助你巩固这些概念。在继续之前,我想举一个类比,希望对你有帮助。随机变量的作用类似于 Python 函数的作用。我们通常将代码封装在函数中,这样就可以将复杂的数据操作存储、重用,并通过一次调用来隐藏。更进一步,当我们拥有多个函数时,有时可以通过多种方式组合它们,比如将两个函数的输出相加,或将一个函数的输出作为另一个函数的输入。我们也可以在没有函数的情况下完成这些操作,但将内部工作抽象化不仅让代码更简洁,还帮助理解和激发新的创意。随机变量在统计学中起着类似的作用。

图 1.1:在一个包含 5 个元素的样本空间上定义的随机变量X,其中元素包括{S[1],
S[5]},其可能的值为-1、2 和 π。
样本空间与ℝ之间的映射是确定性的。这里没有涉及随机性。那么,为什么我们称其为随机变量呢?因为我们可以请求该变量的值,每次请求时,得到的数字都会不同。随机性来源于与事件相关的概率。在图 1.1中,我们通过圆圈的大小表示了P。
两种最常见的随机变量类型是离散型和连续型。虽然不做正式定义,但我们可以说离散型变量只取离散值,通常使用整数表示,例如 1、5、42。连续型变量则取实数值,因此我们使用浮点数来表示它们,例如 3.1415、1.01、23.4214 等等。我们使用哪种类型取决于具体问题。如果我们询问人们最喜欢的颜色,答案可能是“红色”、“蓝色”和“绿色”。这是一个离散随机变量的例子。答案是类别间的——“红色”和“绿色”之间没有中间值。但如果我们研究光的吸收特性,那么像“红色”和“绿色”这样的离散值可能不够准确,转而使用波长可能更为合适。在这种情况下,我们会得到类似 650 纳米和 510 纳米的值,并且任何中间值也都可能出现,包括 579.1 纳米。
1.4.3 离散随机变量及其分布
我们可能不仅仅想计算所有三个人都回答“是”的概率,或者掷骰子得到 3 的概率,我们可能更感兴趣的是找到所有可能答案或骰子上所有可能数字的概率列表。一旦这个列表计算出来,我们可以通过可视化查看它,或者利用它来计算其他量,比如至少得到一个“否”的概率、得到奇数的概率,或者得到大于或等于 5 的数字的概率。这个列表的正式名称是概率 分布。
我们可以通过掷骰子几次并记录每个数字出现的次数来获得骰子的经验概率分布。为了将每个值转化为概率,并使整个列表成为有效的概率分布,我们需要归一化这些计数。我们可以通过将每个数字出现的次数除以掷骰子的次数来实现这一点。
经验分布非常有用,我们将广泛使用它们。但我们不会再手动掷骰子,而是将使用先进的计算方法来为我们完成这项繁重的工作;这不仅能节省我们的时间和避免无聊,还能让我们轻松地从非常复杂的分布中获取样本。不过我们现在有点急于求成。我们的优先任务是集中精力研究理论分布,因为它们在统计学中占据核心地位,原因之一是它们能够构建概率模型。
正如我们所看到的,随机变量并没有什么随机或神秘之处;它们只是数学函数的一种类型。理论概率分布也是如此。我喜欢将概率分布与圆形进行比较。因为我们在上学之前就已经熟悉圆形了,所以我们对它们不感到害怕,它们也不会让我们觉得神秘。我们可以将圆定义为平面上与另一个点(称为圆心)等距的所有点的几何空间。我们可以进一步给出这个定义的数学表达式。如果我们假设圆心的位置无关紧要,那么半径为r的圆可以简单地描述为所有满足以下条件的点集(x,y):

从这个表达式中,我们可以看到,给定参数 r,圆就被完全定义了。这就是我们绘制它所需的所有信息,也是我们计算如周长(2πr)等性质所需的所有信息。
现在请注意,所有的圆看起来都非常相似,并且任何两个半径相同的圆基本上是相同的对象。因此,我们可以把圆的家族看作是其中每个成员都恰好通过半径 r 的值与其他成员区分开来的。
到目前为止,一切顺利,但我们为什么要谈论圆呢?因为这一切都可以直接应用于概率分布。圆和概率分布都有定义它们的数学表达式,这些表达式有我们可以改变的参数,用以定义概率分布家族中的所有成员。图 1.2 显示了一个名为 BetaBinomial 的概率分布的四个成员。在图 1.2中,条形的高度表示每个 x 值的概率。低于 1 或高于 6 的 x 值的概率为 0,因为它们超出了分布的支持范围。

图 1.2:具有参数 α 和 β 的 BetaBinomial 分布的四个成员
这是 BetaBinomial 分布的数学表达式:

pmf 代表概率质量函数。对于离散随机变量,pmf 是返回概率的函数。在数学符号中,如果我们有一个随机变量 X,那么 pmf(x) = P(X = x)。
理解或记住 BetaBinomial 的 pmf 对我们来说并不重要。我只是展示它,以便你能看到这仅仅是另一个函数;你输入一个数字,输出另一个数字。没什么奇怪的,至少原则上是这样。我必须承认,要完全理解 BetaBinomial 分布的细节,我们需要知道什么是
,即二项系数,以及B是什么,即 Beta 函数。但这与展示 x² + y² = r²并没有根本区别。
数学表达式非常有用,因为它们简洁,我们可以利用它们推导出性质。但有时这可能会太复杂,即使我们擅长数学。可视化可能是一个很好的替代(或补充),帮助我们理解概率分布。我无法在纸面上完全展示,但如果你运行以下代码,你将获得一个交互式图,每次调整alpha、beta和n的滑块时都会更新:
代码 1.2
pz.BetaBinomial(alpha=10, beta=10, n=6).plot_interactive()
图 1.3展示了该交互式图的静态版本。黑色的点代表每个随机变量值的概率,而虚线黑色线条仅作为视觉辅助线。

图 1.3:pz.BetaBinomial(alpha=10, beta=10, n=6).plot_interactive()的输出
在 x 轴上,我们有 BetaBinomial 分布的支持,即它可以取的值,x ∈{0,1,2,3,4,5}。在 y 轴上,与这些值相关的概率。完整列表请参见表格 1.1。
| x 值 | 概率 |
|---|---|
| 0 | 0.047 |
| 1 | 0.168 |
| 2 | 0.285 |
| 3 | 0.285 |
| 4 | 0.168 |
| 5 | 0.047 |
表格 1.1:pz.BetaBinomial(alpha=10, beta=10, n=6)的概率
请注意,对于BetaBinomial(alpha=10, beta=10, n=6)分布,{0,1,2,3,4,5}之外的值(例如 −1,0.5,π,42)的概率为 0。
我们之前提到过,我们可以询问随机变量获取值,每次询问时,我们都会得到不同的数字。我们可以用 PreliZ [Icazatti et al., 2023]来模拟这一过程,PreliZ 是一个用于先验引导的 Python 库。例如,考虑以下代码片段:
代码 1.3
pz.BetaBinomial(alpha=10, beta=10, n=6).rvs()
这将给我们一个 0 到 5 之间的整数。是哪一个?我们不知道!但让我们运行以下代码:
代码 1.4
plt.hist(pz.BetaBinomial(alpha=2, beta=5, n=5).rvs(1000))
pz.BetaBinomial(alpha=2, beta=5, n=5).plot_pdf();
我们将得到类似于图 1.4的结果。即使我们无法从随机变量中预测下一个值,我们也可以预测获得任何特定值的概率,反过来,如果我们获取了许多值,我们也可以预测它们的整体分布。

图 1.4:灰色的点表示 BetaBinomial 样本的概率质量函数(pmf)。浅灰色为从该分布中抽取的 1,000 次样本的直方图。
在本书中,我们有时会知道给定分布的参数,并希望从中获取随机样本。其他时候,我们将遇到相反的情况:我们会有一组样本,并希望估计该分布的参数。在这两种情境之间的来回切换将随着我们深入书中的内容而变得得心应手。
1.4.4 连续随机变量及其分布
可能最广为人知的连续概率分布是正态分布,也称为高斯分布。其概率密度函数为:

再次强调,我们仅展示这个表达式是为了揭开其中的神秘面纱。无需过多关注其细节,除了该分布有两个参数 μ,它控制曲线的峰值位置,和 σ,它控制曲线的扩展度以外。Figure 1.5 展示了来自高斯家族的 3 个示例。如果你想深入了解这个分布,我建议你观看这个视频:www.youtube.com/watch?v=cy8r7WSuT1I。

Figure 1.5:高斯家族的三个成员
如果你一直在关注,你可能会注意到我们使用了概率密度函数(pdf)而不是概率质量函数(pmf)。这不是笔误——它们实际上是两个不同的概念。我们先退后一步,思考一下:离散概率分布的输出是概率。Figure 1.2 中的条形高度或 Figure 1.3 中的点的高度就是概率。每个条形或点的高度永远不会超过 1,并且如果你将所有的条形或点相加,你总会得到 1。让我们做同样的事情,但换成 Figure 1.5 中的曲线。首先需要注意的是,我们没有条形或点;我们有一个连续、平滑的曲线。所以,也许我们可以认为这个曲线是由非常细的条形组成的,这些条形细得我们为分布支持中的每一个实数值分配一条条形,测量每条条形的高度,然后进行无限求和。这是合理的吗?
是的,但我们从中获得的结果并不立刻显现出来。这个求和会得到精确的 1 吗?还是会得到一个较大的数字呢?这个求和是有限的吗?结果是否依赖于分布的参数?
要正确回答这些问题需要测度理论,而这只是一个非常非正式的概率学入门,因此我们不会深入探讨这个问题。但本质上,答案是,对于连续随机变量,我们只能为它可能取的每个单独值分配 0 的概率;相反,我们可以为它们分配密度值,然后我们可以计算一个值范围内的概率。因此,对于高斯分布,得到精确数值 -2 的概率,即 -2 后面跟着无限多个零的小数部分,概率为 0。但是得到介于 -2 和 0 之间的数字的概率是一个大于 0 且小于 1 的数。为了找出准确答案,我们需要计算以下内容:

为了计算这个,我们需要用具体的数量来替代符号。如果我们将 pdf 替换为 Normal(0,1),并且 a = −2,b = 0,我们将得到 P(−2 < X < 0)≈ 0.477,这就是 Figure 1.6 中的阴影区域。

图 1.6:黑线表示参数为 mu=0 和 sigma=1 的高斯分布的 pdf,灰色区域表示一个值大于 -2 且小于 0 的概率
你可能还记得,我们可以通过求矩形面积的和来近似一个积分,随着矩形底边长度的缩小,这种近似会变得越来越准确(请参阅维基百科上的 Riemann 积分 条目)。基于这个思路,并使用 PreliZ,我们可以估算 P(−2 < X < 0) 为:
代码 1.5
dist = pz.Normal(0, 1)
a = -2
b = 0
num = 10
x_s = np.linspace(a, b, num)
base = (b-a)/num
np.sum(dist.pdf(x_s) * base)
如果我们增加 num 的值,结果会得到更精确的近似值。
1.4.5 累积分布函数
我们已经看过了概率质量函数(pmf)和概率密度函数(pdf),但这些并不是描述分布的唯一方式。另一种选择是累积分布函数(cdf)。随机变量 X 的 cdf 是函数 F[X],由 F**X = P(X ≤ x) 给出。换句话说,cdf 是对这个问题的回答:得到小于或等于 x 的数值的概率是多少?在图 1.7 的第一列中,我们可以看到 BetaBinomial 分布的 pmf 和 cdf,在第二列中,则展示了高斯分布的 pdf 和 cdf。请注意,离散变量的 cdf 会“跳跃”,而连续变量的 cdf 则是平滑的。每次跳跃的高度代表一个概率——只需将其与点的高度进行比较。我们可以通过绘制连续变量的 cdf 来作为概率为零的视觉证明,任何连续变量的数值都没有“跳跃”,这等同于说这些跳跃的高度正好为零。

图 1.7:BetaBinomial 分布的 pmf 及其对应的 cdf 和正态分布的 pdf 及其对应的 cdf
仅通过查看 cdf,就更容易找到某个数字小于,比如 1 的概率。我们只需要在 x 轴上找到 1 的值,向上移动直到穿过黑线,然后查看 y 轴的值。例如,在图 1.7 中,对于正态分布,我们可以看到该值位于 0.75 和 1 之间。假设它大约是 ≈ 0.85。这比使用 pdf 要难得多,因为我们需要将 1 以下的整个区域与总区域进行比较才能得到答案。人类在判断面积方面不如在判断高度或长度时准确。
1.4.6 条件概率
给定两个事件 A 和 B,且 P(B) > 0,条件概率 P(A|B) 定义为:

P(A,B) 是事件 A 和事件 B 同时发生的概率。P(A|B) 被称为条件概率,它表示在已知(或假设、想象、假定等)B 发生的前提下,事件 A 发生的概率。例如,路面湿了的概率与已知下雨时路面湿了的概率是不同的。
条件概率可以大于、小于或等于无条件概率。如果知道 B 对我们理解 A 没有提供任何信息,那么 P(A|B) = P(A)。只有当 A 和 B 互相独立时,这个关系才成立。相反,如果知道 B 给我们提供了关于 A 的有用信息,那么条件概率可能大于或小于无条件概率,具体取决于知道 B 是否让 A 更加可能或更不可能。让我们通过一个简单的例子来看看,假设我们掷一个公平的六面骰子。掷到数字 3 的概率是多少?P(骰子 = 3) =
,因为对于一个公平的六面骰子,每个数字的机会是一样的。那么,假如我们已经知道掷到的是奇数,掷到数字 3 的概率是多少?P(骰子 = 3 | 骰子 = {1,3,5}) =
,因为如果我们知道结果是奇数,那么可能的数字只有 {1,3,5},而且它们的机会是相等的。最后,如果我们已经知道掷到的是偶数,掷到 3 的概率是多少?这是 P(骰子 = 3 | 骰子 = {2,4,6}) = 0,因为如果我们知道结果是偶数,那么唯一可能的数字是 {2,4,6},所以掷到 3 的概率为 0。
如我们从这些简单的例子中可以看到,通过对观测数据进行条件化,我们正在改变样本空间。当我们询问 P(骰子 = 3) 时,我们需要评估样本空间 S = {1,2,3,4,5,6},但当我们在已知掷到的是偶数的情况下进行条件化时,新的样本空间变为 T = {2,4,6}。
条件概率是统计学的核心,无论你面对的问题是掷骰子还是构建自动驾驶汽车。
图 1.8 的中央面板使用灰度显示了联合分布p(A,B),其中较深的颜色表示较高的概率密度。我们可以看到联合分布呈现拉长的形态,表明A的值越高,B的值也越高,反之亦然。知道了A的值,就能推测出B的值,反之亦然。在图 1.8 的顶部和右侧边缘分别展示了边际分布 p(A)和p(B)。要计算A的边际分布,我们需要对p(A,B)进行对所有B值的平均,直观地说,这就像把二维对象(联合分布)投影到一维。B的边际分布也以类似的方式计算。虚线表示 3 个不同B值下的条件概率 p(A|B)。我们通过在给定B值时切割联合分布p(A,B)来得到它们。我们可以把这看作是在已观察到特定的B值时,A的分布。

图 1.8:联合概率p(A,B)、边缘概率p(A)和p(B),以及条件概率p(A|B)之间关系的表示
1.4.7 期望值
如果X是一个离散的随机变量,我们可以计算其期望值,公式如下:

这只是均值或平均值。
你可能已经习惯于计算样本或一组数字的均值或平均值,无论是手动、用计算器,还是使用 Python。但请注意,在这里我们讨论的不是一堆数字的均值,而是分布的均值。一旦我们定义了分布的参数,原则上可以计算其期望值。它们是分布的特性,就像圆的周长是圆的一个特性,定义圆的半径后就可以确定。
另一个期望值是方差,我们可以用它来描述分布的离散程度。方差在许多统计计算中自然出现,但在实践中,通常使用标准差,它是方差的平方根。原因是标准差的单位与随机变量相同。
均值和方差通常被称为分布的矩。其他的矩包括偏度,它告诉我们分布的偏斜程度,以及峰度,它告诉我们分布尾部或极值的行为[Westfall, 2014]。图 1.9 展示了不同分布及其均值μ、标准差σ、偏度γ和峰度的例子
。请注意,对于某些分布,某些矩可能没有定义,或者它们可能是无穷大。

图 1.9:四个分布及其前四个矩
现在我们已经了解了一些概率论的基本概念和术语,我们可以继续进入大家期待的时刻了。
1.4.8 贝叶斯定理
毫不拖延,让我们以它的威严,思考贝叶斯定理:

好吧,这并不是什么令人印象深刻的东西,对吧?它看起来像是小学的公式,但引用理查德·费曼的话,这就是你需要了解的贝叶斯统计的全部内容。了解贝叶斯定理的来源将帮助我们理解它的意义。根据乘积规则,我们有:

这也可以写成:

由于左侧的项对于两个方程是相等的,我们可以将它们合并并写出:

重新排列后,我们得到贝叶斯定理:

为什么贝叶斯定理如此重要?让我们看看。
首先,它说p(θ|Y)不一定等于p(Y|θ)。这是一个非常重要的事实——一个即使是受过统计学和概率学训练的人也容易在日常情况下忽视的事实。我们通过一个简单的例子来澄清为什么这些量不一定相同。一个人是教皇的概率,给定这个人是阿根廷人,并不等同于,给定这个人是教皇,成为阿根廷人的概率。由于大约有 4700 万阿根廷人,而其中只有一个是现任教皇,我们有p(教皇 | 阿根廷人) ≈
,同时我们也有p(阿根廷人 | 教皇) = 1。
如果我们将θ替换为“假设”,将Y替换为“数据”,贝叶斯定理告诉我们如何计算在给定数据Y的情况下,假设θ的概率,这也是你会在很多地方看到的贝叶斯定理的解释。但是,如何将一个假设转化为可以放入贝叶斯定理中的东西呢?嗯,我们通过使用概率分布来做到这一点。所以,一般来说,我们的假设是一个非常非常非常狭义的假设;如果我们谈论的是找到适合我们模型的参数的值,即概率分布的参数,那么我们会更精确。顺便说一句,不要试图将θ设定为“独角兽存在”的陈述,除非你愿意构建一个现实的独角兽存在的概率模型!
贝叶斯定理是贝叶斯统计的核心。正如我们在第二章中看到的,使用像 PyMC 这样的工具解放了我们每次构建贝叶斯模型时都必须显式书写贝叶斯定理的需要。然而,了解其各个部分的名称是很重要的,因为我们将不断引用它们,而且理解每个部分的含义也很重要,因为这有助于我们构建模型的概念。所以,让我现在带着标签重写贝叶斯定理:

先验分布应当反映我们在看到数据之前对参数θ值的了解,Y。如果我们什么都不知道,就像乔恩·雪诺那样,我们可以使用平坦的先验,这不会传达太多信息。通常来说,我们可以比平坦的先验做得更好,正如我们在本书中将要学到的那样。先验的使用是为什么一些人仍然认为贝叶斯统计是主观的,尽管先验只是我们在建模时所做的另一种假设,因此它和其他假设(如似然)一样主观(或客观)。
似然是我们在分析中引入数据的方式。它是给定参数下数据的合理性表达。在某些文本中,你会发现有人称这个术语为采样模型、统计模型或仅仅是模型。我们将坚持使用“似然”这个名称,并将建模先验和似然的组合。
后验分布是贝叶斯分析的结果,反映了我们关于一个问题的所有已知信息(基于我们的数据和模型)。后验分布是模型参数的概率分布,而不是单一的值。这个分布是先验和似然之间的平衡。有一个著名的笑话:贝叶斯学家是那种模糊地期望看到一匹马,却只看到了一只驴,并坚信自己看到了骡子的人。听到这个笑话后,解释说如果似然和先验都很模糊,那么得到的后验反映的就是对看到骡子的模糊信念,而不是强烈的信念,这会大大破坏气氛。无论如何,我喜欢这个笑话,也喜欢它传达了后验作为先验和似然之间某种妥协的想法。从概念上讲,我们可以将后验视为根据(新)数据更新后的先验。理论上,一个分析的后验可以作为新分析的先验(但实际上,生活可能会更复杂)。这使得贝叶斯分析特别适用于分析按顺序提供的数据。一个例子可能是自然灾害的早期预警系统,它处理来自气象站和卫星的在线数据。有关更多细节,请阅读关于在线机器学习方法的资料。
最后一项是边际似然性,有时也称为证据。严格来说,边际似然性是观察数据的概率,取决于所有参数可能取值的平均值(按照先验分布规定)。我们可以将其表示为 ∫ [Θ]^(p(Y |θ)p(θ)dθ。我们直到第五章5 才会真正关心边际似然性。但目前,我们可以将其视为一个归一化因子,确保后验分布是一个合适的 pmf 或 pdf。如果忽略边际似然性,我们可以将贝叶斯定理写为一个比例关系,这也是一种常见的贝叶斯定理表示方式。

理解贝叶斯定理中每个项的确切作用需要一些时间和实践,并且需要通过几个例子来帮助理解,但这就是本书其余部分的目的所在。
1.5 概率解释
概率可以通过各种有用的方式来解释。例如,我们可以认为 P(A) = 0.125 意味着如果我们多次重复调查,我们期望这三个人大约 12.5%的时间会回答“是”。我们正在将概率解释为长期实验的结果。这是一种非常常见且有用的解释。它不仅可以帮助我们思考概率,还可以提供一种经验方法来估计概率。我们想知道,如果汽车轮胎充气超过制造商推荐的标准,爆胎的概率是多少吗?只需充气大约 120 个轮胎,你就能得到一个不错的近似值。这通常被称为频率主义解释。
概率的另一种解释,通常称为主观或贝叶斯解释,认为概率可以被解释为个体对事件不确定性的度量。在这种解释下,概率与我们对世界的知识状态相关,并不一定基于重复试验。在这种概率定义下,提出关于火星上是否有生命的概率、电子质量为 9.1 × 10^(−31) kg 的概率,或者 1816 年 7 月 9 日布宜诺斯艾利斯是否是晴天的概率,都是有效且自然的。这些都是一次性事件。我们不能重新创造 1 百万个宇宙,每个宇宙都有一个火星,并检查其中有多少个发展出了生命。当然,我们可以做这个心理实验,只要长期频率仍然是一个有效的心理框架。
有时贝叶斯对概率的解释被描述为个人信念;我不喜欢这样。我认为这可能会导致不必要的混淆,因为信念通常与信仰或没有依据的主张有关。这种关联很容易让人认为贝叶斯概率,进而贝叶斯统计学,比其他方法不那么客观或不那么科学。我还认为,这种说法有助于产生对统计学中先验知识角色的混淆,让人们误以为客观或理性就意味着不使用先验信息。
贝叶斯方法与我们拥有的任何其他成熟的科学方法一样主观(或客观)。让我用一个例子来解释:火星上是否存在生命,答案是二元的,类似是或不是的问题。但考虑到我们无法确认这一事实,合理的做法是尝试找出火星上生命存在的可能性。为了回答这个问题,任何诚实且具有科学思维的人都会使用所有相关的火星地球物理数据、所有关于生命所需条件的生物化学知识等等。这个回答必然是关于我们知识状态的,不同的人可能会有不同的看法,甚至得出不同的概率。但至少,从原则上讲,他们都会能够为自己的数据、方法、建模决策等提供支持论据。关于火星生命的科学理性辩论不容许像“天使告诉我有小绿人”这样的论据。然而,贝叶斯统计学只是一种使用概率作为构建模块来做科学陈述的程序。
1.6 概率、不确定性和逻辑
概率可以帮助我们量化不确定性。如果我们对一个问题没有信息,那么可以合理地说每一个可能的事件发生的概率是相等的。这相当于对每一个可能的事件赋予相同的概率。在没有信息的情况下,我们的不确定性是最大的,我并不是随便这么说;这是我们可以通过概率来计算的。如果我们知道某些事件更可能发生,那么可以通过给这些事件赋予更高的概率,而其他事件赋予较低的概率来正式表示这一点。请注意,当我们在统计学中谈论事件时,并不仅仅局限于可能发生的事情,比如小行星撞击地球或我姨妈的 60 岁生日派对。事件只是一个变量可以取的任何可能值(或值的子集),比如你年龄超过 30 岁、萨赫托尔特的价格,或明年全球将售出的自行车数量。
概率的概念也与逻辑学有关。在经典逻辑下,我们只能有真或假的命题。在贝叶斯概率定义下,确定性只是一个特殊的情况:一个真实的命题的概率是 1,而一个虚假的命题的概率是 0。只有在拥有决定性数据表明有东西在生长、繁殖以及进行其他我们认为与生物体相关的活动时,我们才会给“火星上有生命”这一命题分配概率为 1。
然而,请注意,分配一个概率为 0 更难,因为我们总是可以认为火星上还有一些未探索的区域,或者我们在某些实验中犯了错误,或者有其他几个原因可能导致我们错误地认为火星上没有生命,即使实际上是有的。这与克劳梅尔法则有关,该法则指出我们应该将概率为 0 或 1 的命题保留给逻辑上真实或虚假的命题。有趣的是,可以证明,如果我们想要将逻辑扩展以包括不确定性,我们必须使用概率和概率理论。
如我们很快就会看到的,贝叶斯定理只是概率规则的逻辑结果。因此,我们可以将贝叶斯统计看作是逻辑的扩展,它在我们处理不确定性时非常有用。因此,采用贝叶斯方法的一个理由是承认不确定性是普遍存在的。我们通常不得不处理不完整或嘈杂的数据,我们本质上受到进化塑造的灵长类大脑的局限,等等。
贝叶斯精神
概率用于衡量我们对参数的不确定性,而贝叶斯定理是一个在有新数据的情况下正确更新这些概率的机制,希望能够减少我们的不确定性。
1.7 单一参数推断
现在我们知道了贝叶斯统计是什么,接下来让我们通过一个简单的例子来学习如何进行贝叶斯统计。我们将从推断一个单一的未知参数开始。
1.7.1 投硬币问题
投硬币问题,或者如果你想在聚会上显得更专业的话可以称其为 BetaBinomial 模型,是统计学中的一个经典问题,问题是这样的:我们多次掷硬币并记录正反面朝上的次数。基于这些数据,我们试图回答这样的问题:这枚硬币公平吗?或者,更一般地说,这枚硬币有多偏?虽然这个问题可能听起来很无聊,但我们不应该低估它。
投硬币问题是学习贝叶斯统计基础的一个很好的例子,因为它是一个简单的模型,我们可以轻松地解决和计算。此外,许多实际问题由二元的、互斥的结果组成,例如 0 或 1、正或负、奇数或偶数、垃圾邮件或非垃圾邮件、热狗或不是热狗、猫或狗、安全或不安全、健康或不健康。因此,即使我们在谈论硬币时,这个模型也适用于任何这些问题。为了估计硬币的偏向性,并且通常来说,回答贝叶斯环境中的任何问题,我们将需要数据和一个概率模型。对于这个例子,我们假设我们已经抛掷了几次硬币,并且我们有观察到的正面朝上的次数记录,所以数据收集部分已经完成。获得模型将需要更多的努力。由于这是我们的第一个模型,我们将明确地写出贝叶斯定理并完成所有必要的数学运算(不用担心,我保证这不会痛苦),并且我们将非常慢地进行。从 2 开始,我们将使用 PyMC 和我们的计算机来为我们做数学运算。
我们将做的第一件事是推广偏向的概念。我们将说,偏向为 1 的硬币总是会正面朝上,偏向为 0 的硬币总是会反面朝上,而偏向为 0.5 的硬币在一半的时间里会正面朝上,另一半时间会反面朝上。为了表示偏向,我们将使用参数θ,而为了表示多次投掷中正面朝上的总次数,我们将使用变量Y。根据贝叶斯定理,我们必须指定先验分布p(θ)和似然函数p(Y | θ),我们将使用这些。让我们从似然函数开始。
1.7.2 选择似然函数
假设只有两种可能的结果——正面或反面——同时假设一次硬币投掷不会影响其他投掷,也就是说,我们假设硬币投掷是相互独立的。我们进一步假设所有硬币投掷来自同一分布。因此,随机变量硬币投掷是独立同分布(iid)变量的一个例子。我希望你同意这些假设对于我们的问题来说是非常合理的。基于这些假设,似然函数的一个合适候选是二项分布:

这是一种离散分布,返回在N次硬币投掷(或一般的试验或实验)中得到y次正面(或一般的成功)的概率,前提是固定的θ值。
图 1.10 显示了来自二项分布族的九个分布;每个子图都有一个图例,表示参数的值。请注意,对于这个图,我没有省略 y 轴上的值。我这样做是为了让你自己检查,如果你将所有柱子的高度相加,你将得到 1,也就是说,对于离散分布,柱子的高度代表的是实际概率。

Figure 1.10: 二项家族的九位成员
二项分布是似然性的合理选择。我们可以看到θ表示抛硬币时得到头的可能性有多大。当N=1 时更容易看到,但对于任何N的值,只需比较y=1(头)时θ的值与柱状图的高度即可。
1.7.3 选择先验分布
作为先验,我们将使用贝塔分布,这在贝叶斯统计中非常常见,外观如下:

如果我们仔细观察,会发现贝塔分布与二项分布看起来很相似,除了第一项。 Γ是希腊大写 gamma 字母,代表伽玛函数,但这并不是真正重要的。对我们而言重要的是,第一项是一个归一化常数,确保分布积分为 1。从前述公式中可以看出,贝塔分布有两个参数,α和β。Figure 1.11 展示了贝塔家族的九位成员。

Figure 1.11: 贝塔家族的九位成员
我喜欢贝塔分布及其所有可能的形状,但为什么我们要在我们的模型中使用它呢?使用贝塔分布来处理此类问题有许多理由。其中之一是贝塔分布限制在 0 到 1 之间,与我们的θ参数相同。一般来说,我们在想要建模二项变量的比例时使用贝塔分布。另一个原因是其多功能性。正如我们在Figure 1.11 中看到的,该分布采用多种形状(均限制在[0,1]区间内),包括均匀分布、类似正态分布和 U 形分布。
作为第三个原因,Beta 分布是二项分布(我们作为似然使用的分布)的共轭先验。共轭先验是指,当与给定的似然结合使用时,返回的后验具有与先验相同的函数形式。简单来说,每次我们使用 Beta 分布作为先验,二项分布作为似然时,我们将得到 Beta 作为后验分布。还有其他共轭先验的配对;例如,正态分布是其自身的共轭先验。多年来,贝叶斯分析一直局限于使用共轭先验。共轭性确保了后验的数学可解性,这一点非常重要,因为贝叶斯统计中一个常见的问题是我们最终得到一个无法解析求解的后验。在开发适合的计算方法解决概率方法之前,这曾是一个关键的障碍。从第 2 章开始,我们将学习如何使用现代计算方法解决贝叶斯问题,无论我们选择是否使用共轭先验。
1.7.4 获取后验
让我们记住,贝叶斯定理表明后验与似然和先验的乘积成正比。因此,对于我们的问题,我们需要将二项分布和 Beta 分布相乘:

我们可以通过去掉所有与 θ 无关的项来简化这个表达式,结果仍然有效。因此,我们可以写成:

重新排列它,并注意到这具有 Beta 分布的形式,我们得到:

基于这个解析表达式,我们可以计算后验。图 1.12 显示了 3 个先验和不同试验次数下的结果。以下代码块展示了生成 图 1.12 的要点(省略了绘图所需的代码)。
代码 1.6
n_trials = [0, 1, 2, 3, 4, 8, 16, 32, 50, 150]
n_heads = [0, 1, 1, 1, 1, 4, 6, 9, 13, 48]
beta_params = [(1, 1), (20, 20), (1, 4)]
x = np.linspace(0, 1, 2000)
for idx, N in enumerate(n_trials):
y = n_heads[idx]
for (*α*_prior, *β*_prior) in beta_params:
posterior = pz.Beta(*α*_prior + y, *β*_prior + N - y).pdf(x)

图 1.12:第一个子图显示了 3 个先验。其余的显示了随着新数据的到来,更新后的结果。
在图 1.12 的第一个子图中,我们有零次试验,因此三条曲线表示我们的先验:
-
均匀先验(黑色):这表示在先验中所有偏差的可能值都是等概率的。
-
高斯型先验(深灰色):这围绕 0.5 进行集中,表示该先验与信息兼容,表明硬币正反面的概率大致相同。我们也可以说,这个先验与硬币公平的知识是兼容的。
-
倾斜的先验(浅灰色):这将大部分权重放在尾部偏向的结果上。
其余的子图展示了后续试验的后验分布。每个子图的图例中标明了试验次数(或掷硬币次数)和正面朝上的次数。还有一个黑点在 0.35 处,表示θ的真实值。当然,在实际问题中,我们并不知道这个值,它在这里仅用于教学目的。图 1.12,可以帮助我们深入理解贝叶斯分析,因此拿起你的咖啡、茶或最喜欢的饮品,我们来花点时间理解它:
-
贝叶斯分析的结果是一个后验分布——它不是一个单一值,而是给定数据和模型下的一个可能值的分布。
-
最可能的值由后验分布的众数(分布的峰值)给出。
-
后验分布的扩展与参数值的不确定性成正比;分布越广泛,我们的确定性就越低。
-
直观上,当我们观察到更多支持某一结果的数据时,我们对结果的信心就更强。因此,即使数值上
=
= 0.5,在八次试验中看到四次正面朝上比在两次试验中看到一次正面朝上能更有信心地认为偏差是 0.5。这个直觉在后验分布中有所体现,您可以自己检查,如果注意观察第三和第六个子图中的(黑色)后验分布;虽然众数相同,但第三个子图的扩展(不确定性)比第六个子图更大。 -
给定足够多的数据,两个或多个具有不同先验的贝叶斯模型将趋向于收敛到相同的结果。在无限数据的极限下,无论我们使用什么先验,所有的模型都将提供相同的后验分布。
-
记住,无限是一个极限,而不是一个数字,因此从实际角度来看,对于有限且相对较小的数据点,我们可能得到几乎等同的后验分布。
-
后验收敛到相同分布的速度取决于数据和模型。我们可以看到,从黑色先验(均匀分布)和灰色先验(偏向尾部)得到的后验分布收敛得更快,几乎相同,而从深灰色先验(偏向集中分布)得到的后验分布则收敛得较慢。即使经过 150 次试验,仍然很容易识别出深灰色后验分布与另外两个分布的区别。
-
从图中不容易看出来的一点是,如果我们按顺序更新后验,就像一次性计算后验一样,最终会得到相同的结果。我们可以计算 150 次后验,每次加入一个新观察值,并将获得的后验作为新的先验,或者我们可以一次性计算 150 次掷硬币的后验。结果将完全相同。这个特性不仅非常合理,而且为我们提供了一种在获取新数据时更新估计值的自然方法,这种情况在许多数据分析问题中都很常见。
1.7.5 先验的影响
从前面的例子可以清楚地看出,先验可以影响推断。这是正常的——先验本来就应该这样做。也许最好根本不设置先验,那样建模不就更简单了吗?嗯,不一定。如果你不设置先验,别人会为你设置。有时这没问题——默认先验是有用的,也有它的作用——但有时最好能有更多的控制权。让我来解释一下。
我们可以认为每一个(统计)模型,不论是否是贝叶斯模型,都有某种类型的先验,即使先验没有明确设置。例如,许多在频率派统计中常用的程序可以看作是在某些条件下(如平坦先验)贝叶斯模型的特例。一种常见的参数估计方法被称为最大似然估计;这种方法避免设置先验,只通过找到最大化似然的单一值来工作。这个值通常通过在我们估计的参数名称上方加一个小帽子来表示,例如
。与后验估计不同,后验估计是一个分布,而
是一个点估计,是一个数值。对于抛硬币问题,我们可以通过解析方法计算出来:

如果你回到图 1.12,你将能自己检查出黑色后验的模态(即与均匀/平坦先验对应的那个模态)与每个子图中计算得到的
值一致。这不是巧合;这是因为设置均匀先验后,再取后验的模态相当于最大似然估计。
我们无法避免先验,但如果将其纳入分析中,我们可以获得一些潜在的好处。最直接的好处是我们得到一个后验分布,它是一个合理值的分布,而不仅仅是最可能的值。拥有一个分布比单一的点估计更具信息性,正如我们所看到的,分布的宽度与我们对估计的不确定性有关。另一个好处是,计算后验意味着对先验进行平均。这可以导致更难以过拟合的模型和更稳健的预测[Wilson 和 Izmailov, 2022]。
先验分布能为我们带来其他好处。从下一章开始,我们将使用数值方法来获得后验分布。这些方法看起来像魔法,直到它们不再有效。统计计算的民间定理指出:“当你遇到计算问题时,通常是模型出了问题”[Gelman,2008]。有时候,明智的先验选择可以使推断变得更容易或更快速。需要指出的是,我们并不提倡为了加速推断而特意设置先验,但通常情况下,通过考虑先验,我们可以得到更快速的模型。
先验的一个优点,有时被忽视了,就是必须考虑先验可能迫使我们更深入地思考我们要解决的问题以及我们所拥有的数据。有时候,建模过程本身就能带来更好的理解,不管我们最终如何拟合数据或做出预测。通过明确先验,我们能够得到更透明的模型,这意味着这些模型更容易被批评、调试(广义上讲)、向他人解释,并且有可能得到改善。
1.8 如何选择先验
初学贝叶斯分析的人(以及该范式的反对者)通常对如何选择先验感到有些紧张。通常,他们担心先验分布会使数据无法自主表达!没关系,但我们必须记住,数据并不会“说话”;充其量,数据只是低声细语。我们只能在模型的上下文中理解数据,包括数学模型和心理模型。科学史上有很多例子表明,相同的数据曾让人们对相同的话题产生不同的看法,即便你基于正式模型来形成观点,也会发生这种情况。
有些人喜欢使用非信息性先验(也称为平坦的、模糊的或扩散的先验)这一想法。这些先验对分析的影响最小。虽然在某些问题中使用它们是可行的,但真正推导出非信息性先验是很困难的,甚至是不可能的。此外,通常我们能够做得更好,因为我们通常拥有一些先验信息。
在本书中,我们将遵循 Gelman、McElreath、Kruschke 等人的建议,并偏好弱信息先验。对于许多问题,我们通常对一个参数可能取的值有所了解。我们可能知道某个参数只能取正值,或者知道它可能的范围,或者预期它接近零或在某个值的上下方。在这种情况下,我们可以使用先验来在模型中引入一些弱信息,而不必担心过于强势。因为这些先验有助于保持后验分布在合理的范围内,所以它们也被称为正则化先验。
信息性先验是非常强的先验,能传递大量信息。使用它们也是一个有效的选项。根据你的问题,从领域知识中找到优质的信息并将其转化为先验可能容易,也可能不容易。我曾经从事结构生物信息学工作。在这个领域,大家一直在使用贝叶斯和非贝叶斯方法,利用所有能够获取的先验信息来研究和预测蛋白质的结构。这是合理的,因为几十年来,我们通过数千个精心设计的实验收集了大量数据,因此我们手头有大量可信的先验信息。不使用这些信息简直荒谬!抛弃有价值的信息,绝对没有什么“客观”或“科学”可言。如果你有可靠的先验信息,就应该使用它。试想一下,如果每次汽车工程师需要设计一辆新车时,都必须从头开始,重新发明内燃机、车轮,甚至是汽车的基本概念,那该多么浪费时间!
PreliZ 是一个全新的 Python 库,用于先验知识的引出 [Mikkola et al., 2023, Icazatti et al., 2023]。它的使命是帮助你引出、表示和可视化你的先验知识。例如,我们可以让 PreliZ 计算一个满足一组约束条件的分布的参数。假设我们想找到一个 Beta 分布,其中 90% 的质量位于 0.1 和 0.7 之间,那么我们可以写:
代码 1.7
dist = pz.Beta()
pz.maxent(dist, 0.1, 0.7, 0.9)
结果是一个具有参数α = 2.5 和 β = 3.6(四舍五入到小数点后一位)的 Beta 分布。pz.maxent 函数计算了在我们指定约束条件下的最大 熵分布。为什么是最大熵分布?因为这相当于在这些约束条件下计算最不具信息量的分布。默认情况下,PreliZ 会绘制如下所示的分布:

图 1.13:最大熵 Beta 分布,90% 的质量位于 0.1 和 0.7 之间
由于先验引出有很多方面,PreliZ 提供了许多其他方法来引出先验。如果你有兴趣了解更多关于 PreliZ 的信息,可以查看 preliz.readthedocs.io 上的文档。
构建模型是一个迭代过程;有时迭代只需几分钟,有时则可能需要几年。可重复性很重要,模型中的透明假设有助于提高其可重复性。如果我们对某个特定的先验(或似然)没有把握,我们可以自由地为给定的分析使用多个先验(或似然);探索不同先验的效果也能带来有价值的信息。建模过程的一部分是质疑假设,先验(和似然)正是其中的一部分。不同的假设将导致不同的模型,可能还会得出不同的结果。通过使用数据和我们对问题的领域知识,我们能够比较不同的模型,并在必要时决定一个优胜者。第五章将专门讨论这个问题。由于先验在贝叶斯统计中的核心作用,我们将在面对新问题时继续讨论它们。因此,如果你对这个讨论有疑问并感到有些困惑,不用担心,保持冷静,别着急,很多人也困惑了几十年,这个讨论仍在继续。
1.9 贝叶斯分析的沟通
创建报告和传达结果是统计学和数据科学实践的核心内容。在本节中,我们将简要讨论在使用贝叶斯模型时,进行这项任务的一些特殊之处。在未来的章节中,我们将继续讨论这个重要问题的例子。
1.9.1 模型符号与可视化
如果你想传达分析结果,也应该传达你所使用的模型。表示概率模型的一种常见符号是:
| θ ∼ Beta(α,β) | ||
|---|---|---|
| y ∼ Bin(n = 1,p = θ) |
这就是我们用于抛硬币示例的模型。如你所记得,∼ 符号表示它左边的变量是一个随机变量,按照右边的分布进行分布。在许多上下文中,这个符号用来表示某个变量大致取某个值,但在谈论概率模型时,我们会把这个符号读作 按……分布。因此,我们可以说 θ 按 Beta 分布,参数为 α 和 β,而 y 按 Binomial 分布,参数为 n = 1 和 p = θ。同样的模型可以通过克鲁什克图形象地表示,如 图 1.14 所示。

图 1.14:BetaBinomial 模型的克鲁什克图
在第一层,我们有生成 θ 值的先验,然后是似然,最后一行是数据 y。箭头表示变量之间的关系,符号 ∼ 表示变量的随机性质。书中的所有克鲁什克图都是使用 Rasmus Bååth 提供的模板制作的(www.sumsar.net/blog/2013/10/diy-kruschke-style-diagrams/)。
1.9.2 总结后验
贝叶斯分析的结果是后验分布,所有关于参数的信息(在给定模型和数据集的情况下)都包含在后验分布中。因此,通过总结后验,我们实际上是在总结模型和数据的逻辑结果。一种常见做法是报告每个参数的均值(或众数或中位数),以了解分布的位置,同时报告一些离散度量(如标准差),以了解我们估计的不确定性。标准差对于类似正态分布的分布效果良好,但对于其他类型的分布(如偏斜分布)可能会产生误导。
一种常用的设备来总结后验分布的广度是使用最高密度区间(HDI)。HDI 是包含给定概率密度部分的最短区间。如果我们说某个分析的 95% HDI 是 [2,5],我们意味着根据我们的数据和模型,相关参数的值介于 2 到 5 之间,且其概率为 0.95。选择 95%、50% 或其他任何值并没有什么特别之处。我们可以自由选择 82% 的 HDI 区间。如果愿意,理想情况下,选择的依据应根据上下文而定,而不是自动的,但选择一个常见值(如 95%)也没问题。为了提醒这种选择的任意性,ArviZ 的默认值是 94%。
ArviZ 是一个用于贝叶斯模型探索性分析的 Python 包,提供了许多帮助我们总结后验的功能。其中一个功能是 az.plot_posterior,我们可以使用它生成一个包含θ的均值和 HDI 的图表。分布不必是后验分布,任何分布都可以使用。图 1.15 显示了来自 Beta 分布的随机样本的结果:
代码 1.8
np.random.seed(1)
az.plot_posterior({'*θ*':pz.Beta(4, 12).rvs(1000)})

图 1.15:来自 Beta 分布的样本的 KDE 图,包含其均值和 94% HDI
不是置信区间
如果你熟悉频率学派的范式,请注意,HDI 与置信区间不同。在频率学派框架下,参数是由设计固定的;频率学派的置信区间要么包含真实参数值,要么不包含。而在贝叶斯框架中,参数是随机变量,因此我们可以谈论某个参数具有特定值或位于某个区间内的概率。置信区间的直觉性较差,容易被误解,人们常常把频率学派的置信区间当作贝叶斯可信区间来讨论。
1.10 总结
我们以简短的统计建模讨论开始了贝叶斯之旅,内容包括概率、条件概率、随机变量、概率分布和贝叶斯定理。然后我们用抛硬币问题作为借口,引入了贝叶斯建模和数据分析的基本概念。我们利用这个经典的玩具示例传达了贝叶斯统计学中一些最重要的思想,比如使用概率分布来构建模型并表示不确定性。我们试图揭开先验的神秘面纱,并将其与模型过程中的其他元素(如似然性)平等对待,甚至涉及到更多的元问题,比如我们为何要解决特定问题。
我们在本章最后讨论了贝叶斯分析结果的解释和沟通。我们假设存在一个真实的分布,这个分布通常是未知的(原则上也无法知道),我们从中获取一个有限的样本,可能是通过实验、调查、观察或模拟获得的。为了从真实分布中学习一些东西,鉴于我们只能观察到一个样本,我们构建了一个概率模型。概率模型有两个基本组成部分:先验和似然。使用模型和样本,我们进行贝叶斯推断并得到后验分布;这个分布封装了关于问题的所有信息,基于我们的模型和数据。从贝叶斯的角度看,后验分布是最重要的对象,一切其他内容都从它中推导出来,包括以后验预测分布形式呈现的预测。由于后验分布(以及从中推导的任何其他量)是模型和数据的结果,因此贝叶斯推断的有用性受到模型和数据质量的限制。最后,我们简要总结了贝叶斯数据分析的主要方面。在本书的其余部分,我们将再次回顾这些思想,将其吸收并作为更高级概念的框架。
在下一章,我们将介绍 PyMC,它是一个用于贝叶斯建模和概率机器学习的 Python 库,还会使用更多来自 ArviZ 的特性,这是一个用于贝叶斯模型探索性分析的 Python 库,以及 PreliZ,这是一个用于先验引导的 Python 库。
1.11 练习
我们不知道大脑是否以贝叶斯方式工作,或以大致贝叶斯方式工作,亦或是某种进化(或多或少)优化的启发式方法。尽管如此,我们知道通过接触数据、示例和练习来学习……你可能会说,人类从未真正学习,看看我们作为物种在战争或经济体系等方面的表现,这些体系优先考虑利润而非人民的福祉……不管怎样,我建议你在每章结束时做一下所提的练习:
-
假设你有一个罐子,里面有 4 颗果冻豆:2 颗是草莓味的,1 颗是蓝莓味的,1 颗是肉桂味的。你从罐子里随机抽取一颗果冻豆。
-
这个实验的样本空间是什么?
-
我们将事件A定义为抽到的果冻豆是草莓味的,将事件B定义为抽到的果冻豆不是肉桂味的。事件A和B的概率分别是多少?
-
事件A和B是互斥事件吗?为什么或者为什么不?
-
-
之前,我们定义了一个 Python 函数
P来使用概率的简单定义计算事件的概率。将该函数推广,以计算当事件的概率不完全相等时的事件概率。使用这个新函数计算前面练习中事件A和B的概率。提示:你可以传递一个第三个参数来表示每个事件的概率。 -
使用 PreliZ 探索 BetaBinomial 和高斯分布的不同参数。使用
plot_pdf、plot_cdf和plot_interactive方法。 -
我们讨论了概率质量/密度函数和累积分布函数。但也有其他方式表示函数,比如百分位点函数 ppf。使用 PreliZ 的
plot_ppf方法,绘制 BetaBinomial 和高斯分布的百分位点函数。你能解释 ppf 是如何与 cdf 和 pmf/pdf 相关的吗? -
以下表达式中,哪一个对应于:在 1816 年 7 月 9 日已知的情况下,晴天的概率?
-
p(sunny)
-
p(sunny|July)
-
p(sunny|9 of July of 1816)
-
p(9^(th) of July of 1816|sunny)
-
![p(sunny,9th of July-of 1816) p(9th of July of 1816)]()
-
-
我们展示了随机选择一个人并选出教皇的概率与教皇是人类的概率是不同的。在动画系列《未来都市》中,(太空)教皇是爬行动物。这会如何改变你之前的计算结果?
-
按照图 1.9中的示例,使用 PreliZ 计算 SkewNormal 分布的矩,参数组合不同。生成不同大小的随机样本,例如 10、100 和 1,000,看看是否能从样本中恢复出前两个矩的值(均值和方差)。你观察到什么?
-
对学生的 T 分布重复之前的练习。尝试ν的值,比如 2、3、500。你观察到什么?
-
在以下的概率模型定义中,识别先验和似然:
![Y ∼ Normal (μ,σ) μ ∼ Normal (0,2) σ ∼ HalfNormal (0.75 )]()
-
在前面的模型中,后验将有多少个参数?与掷硬币问题的模型进行比较。
-
为第 9 题中的模型写出贝叶斯定理。
-
假设我们有两枚硬币;当我们掷第一枚硬币时,正面朝上的概率为一半,反面朝上的概率也为一半。另一枚硬币是一枚加重硬币,总是朝上正面。如果我们随机选择其中一枚硬币并且掷出正面,那么这枚硬币是不公平的概率是多少?
-
尝试使用其他先验(
beta_params)和其他数据(trials和data)重新绘制图 1.12。 -
阅读关于克伦威尔法则的 Wikipedia 文章:
en.wikipedia.org/wiki/Cromwell%27s_rule。 -
阅读关于概率和荷兰书的 Wikipedia 文章:
en.wikipedia.org/wiki/Dutch_book。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,与志同道合的人一起学习,和超过 5000 名成员共同进步: packt.link/bayesian

第二章
概率编程
我们的魔像很少有实体形式,但它们通常也由硅中生长的黏土做成,作为计算机代码存在。—— 理查德·麦克埃尔里斯
现在我们对概率理论和贝叶斯统计有了非常基础的理解,接下来我们将学习如何使用计算工具构建概率模型。具体来说,我们将学习如何使用 PyMC 进行概率编程 [Abril-Pla 等人, 2023]。基本的思想是,我们通过代码来指定统计模型,然后 PyMC 将为我们求解这些模型。我们不需要显式地写出贝叶斯定理。这样做有两个原因。首先,许多模型无法得到解析解,因此我们只能使用数值方法来求解这些模型。其次,现代贝叶斯统计主要通过编写代码来完成。我们将看到,概率编程提供了一种有效的方法来构建和求解复杂模型,它让我们能够更多地关注模型设计、评估和解释,而少关注数学或计算的细节。
本章将涵盖以下主题:
-
概率编程
-
PyMC 入门
-
硬币抛掷问题重访
-
总结后验分布
-
高斯模型和学生 t 分布模型
-
比较不同组别和效应大小
2.1 概率编程
贝叶斯统计从概念上讲非常简单。我们有已知和未知,然后使用贝叶斯定理将后者条件化于前者。如果我们足够幸运,这个过程将减少对未知的 uncertainty。通常,我们把已知称为数据并将其视为常数,未知则称为参数并将其视为随机变量。
尽管从概念上讲很简单,完全概率模型常常导致解析上不可处理的表达式。多年来,这一直是一个真正的问题,也是贝叶斯方法未能广泛应用于一些利基领域的主要原因之一。计算时代的到来和数值方法的发展,至少在原理上可以用来解决任何推断问题,已经极大地改变了贝叶斯数据分析的实践。我们可以将这些数值方法视为通用推断引擎。自动化推断过程的可能性催生了概率编程语言(PPLs),它使得模型创建和推断之间有了清晰的分离。在 PPL 框架中,用户通过编写几行代码来指定完整的概率模型,然后推断过程会自动进行。
预计概率编程将在数据科学和其他学科中产生重大影响,能够使从业人员以更省时且更少出错的方式构建复杂的概率模型。我认为,编程语言对科学计算的影响可以通过六十多年前 Fortran 编程语言的引入来类比。虽然现在 Fortran 已经不再流行,但曾几何时,它被认为是革命性的。科学家们第一次摆脱了计算细节,开始更自然地专注于构建数值方法、模型和仿真。很有趣的是,有些人正在努力让 Fortran 再度焕发光彩,如果你有兴趣,可以查看他们的工作:fortran-lang.org/en。
2.1.1 使用 PyMC 抛硬币
让我们回顾一下第一章中的抛硬币问题,这次使用 PyMC。我们将使用与那一章中相同的合成数据。因为我们生成了数据,所以我们知道以下代码块中 θ 的真实值,称为 theta_real。当然,对于实际数据集,我们没有这些知识:
代码 2.1
np.random.seed(123)
trials = 4
theta_real = 0.35 # unknown value in a real experiment
data = pz.Binomial(n=1, p=theta_real).rvs(trials)
现在我们有了数据,我们需要指定模型。记住,这是通过指定似然性和先验来完成的。对于似然性,我们将使用参数 n = 1,p = θ 的二项分布;对于先验,我们将使用参数 α = β = 1 的 Beta 分布。具有此类参数的 Beta 分布等同于区间 [0, 1] 上的均匀分布。使用数学符号我们可以将模型写为:

这个统计模型几乎可以直接翻译为 PyMC:
代码 2.2
with pm.Model() as our_first_model:
*θ* = pm.Beta('*θ*', alpha=1., beta=1.)
y = pm.Bernoulli('y', p=*θ*, observed=data)
idata = pm.sample(1000)
代码的第一行创建了我们模型的容器。with 块中的所有内容将自动添加到 our_first_model 中。你可以把这看作是一种语法糖,它简化了模型的指定,因为我们不需要手动将变量分配给模型。第二行指定了先验。如你所见,语法紧密遵循了数学符号。第三行指定了似然性;语法与先验几乎相同,唯一不同的是我们使用 observed 参数传递数据。观测值可以作为 Python 列表、元组、NumPy 数组或 pandas DataFrame 传递。至此,我们完成了模型的指定!是不是很简洁?
我们还有一行代码需要解释。最后一行就是魔法发生的地方。背后这个看似无害的代码行,PyMC 正在自动化许多任务,简直就像是有成百上千的 oompa loompas 在为您唱歌并烤制美味的贝叶斯推断!嗯,虽然并非如此,但 PyMC 确实在自动化很多任务。暂时,我们将把这一行视作一个黑箱,它将为我们提供正确的结果。重要的是要理解,在底层,我们将使用数值方法来计算后验分布。原则上,这些数值方法能够解决我们可以编写的任何模型。我们为这种通用性所付出的代价是,结果将以来自后验的样本形式呈现。稍后,我们将能够证实这些样本来自一个 Beta 分布,正如我们在上一章学到的那样。由于数值方法是随机的,每次运行时样本都会有所不同。然而,如果推断过程按预期进行,这些样本将代表后验分布,因此我们将从这些样本中得出相同的结论。有关底层发生的详细情况以及如何检查样本是否值得信任,将在 第十章 中解释。
还有一件事:idata 变量是一个 InferenceData 对象,它是一个容器,包含 PyMC 生成的所有数据。我们将在本章稍后学习更多有关此的内容。
好的,在最后一行中,我们请求从后验中获得 1,000 个样本。如果您运行代码,您将看到类似这样的消息:
Auto-assigning NUTS sampler... Initializing NUTS using jitter+adapt_diag... Multiprocess sampling (4 chains in 4 jobs)
NUTS: [*θ*]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 1 second.
第一行和第二行告诉我们,PyMC 已经自动分配了 NUTS 采样器(一个非常适合连续变量的推断引擎),并使用了一种方法来初始化该采样器(这些方法需要对开始采样的位置进行一些初步猜测)。第三行说 PyMC 将并行运行四个链,因此我们将从后验中获得四个独立的样本。由于 PyMC 尝试将这些链并行化到机器上可用的处理器中,我们将以一个价格获得四个样本。链的确切数量是根据机器中的处理器数量计算的;您可以使用 chains 参数来更改 sample 函数的链数。接下来的这一行告诉我们每个采样器正在采样哪些变量。对于这个特定的例子,这一行并没有添加新信息,因为 NUTS 被用来采样我们唯一的变量 θ。然而,这并不总是这样,因为 PyMC 可以为不同的变量分配不同的采样器。PyMC 有规则来确保每个变量都与最佳的采样器关联。用户可以使用 sample 函数的 step 参数手动分配采样器,但您几乎不需要这么做。
最后,最后一行是一个进度条,显示了与采样器工作速度相关的几个指标,包括每秒的迭代次数。如果你运行代码,你会看到进度条更新得非常快。在这里,我们看到的是最后阶段,即采样器已经完成了它的工作。你会注意到,我们要求了 1,000 个样本,但 PyMC 实际上计算了 8,000 个样本。每条链有 1,000 个抽样用于调节采样算法(在本例中是 NUTS)。这些抽样默认会被丢弃;PyMC 使用它们来提高采样方法的效率和可靠性,这对于获得有用的后验近似是非常重要的。每条链还有 1,000 个有效抽样,共计 4,000 个。这些是我们将用作后验分布的样本。我们可以通过 tune 参数调整调节步骤的数量,通过 draw 参数调整抽样的数量。
更快的采样
在幕后,PyMC 使用 PyTensor,这是一个可以定义、优化和高效评估涉及多维数组的数学表达式的库。PyTensor 大大提高了 PyMC 的速度和性能。尽管有这些优势,但值得注意的是,PyMC 中的采样器是用 Python 实现的,这有时可能导致执行速度较慢。为了应对这个限制,PyMC 允许使用外部采样器。我推荐使用 nutpie,一个用 Rust 编写的采样器。有关如何从 PyMC 安装和调用 nutpie 的更多信息,请查阅 第 10 章。
2.2 总结后验分布
通常,在从后验分布中采样后,我们执行的第一项任务是检查结果的表现。ArviZ 的 plot_trace 函数非常适合这项任务:
代码 2.3
az.plot_trace(idata)

图 2.1:our_first_model 后验的追踪图
图 2.1 显示了调用 az.plot_trace 时的默认结果;我们为每个未观察到的变量获得两个子图。我们模型中唯一未观察到的变量是 θ。注意,y 是一个表示数据的观察变量;我们不需要对其进行采样,因为我们已经知道这些值。因此,我们只得到两个子图。左边是一个 核密度估计(KDE)图;这类似于直方图的平滑版本。理想情况下,我们希望所有链条的 KDE 非常相似,就像 图 2.1 中所示。右边,我们得到每个采样步骤的个体值;每条链条对应一条线。理想情况下,我们希望它看起来杂乱无章,没有明显的模式,我们应该很难区分不同的链条。在 第 10 章中,我们提供了更多关于如何解读这些图表的细节。关键是,如果我们运行了许多链条,我们期望它们几乎无法区分。采样器表现得很好,我们可以信任这些样本。
与其他 ArviZ 函数一样,az.plot_trace 也有许多选项。例如,我们可以通过设置 combined=True 参数来生成所有链的单个 KDE 图,并通过设置 kind=rank_bars 来生成一个 排名 图。
代码 2.4:
az.plot_trace(idata, kind="rank_bars", combined=True)

图 2.2:our_first_model 后验的痕迹图,使用了选项 kind="rank_bars" 和 combined=True。
排名图是另一种检查我们是否可以信任样本的方法;对于这个图,我们为每条链生成一个直方图,并希望它们尽可能均匀,像在 图 2.2 中那样。由于随机抽样,一些小的均匀性偏差是可以接受的,但大的均匀性偏差则表明链正在探索后验的不同区域。理想情况下,我们希望所有的链都能探索整个后验。在 第十章 中,我们提供了如何解释排名图及其构建方法的更多细节。
ArviZ 提供了几种其他的图表来帮助解释后验,我们将在接下来的页面中看到它们。我们也可能希望获得后验的数值总结。我们可以使用 az.summary 来实现,它将返回一个 pandas DataFrame,如 表 2.1 所示。
代码 2.5
az.summary(idata, kind="stats").round(2)
| 均值 | 标准差 | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| θ | 0.34 | 0.18 | 0.03 | 0.66 |
表 2.1:总结统计数据
在第一列,我们有变量的名称,第二列是后验的均值,第三列是后验的标准差,最后两列是 94% 最高密度区间的下限和上限。因此,根据我们的模型和数据,我们认为 θ 的值很可能是 0.34,并且以 94% 的概率,它的真实值在 0.03 和 0.66 之间。我们也可以使用标准差报告一个类似的总结。标准差相比 HDI 的优点是它是一种更常见的统计量。但缺点是我们需要更小心地解释它,否则可能会得出毫无意义的结果。例如,如果我们计算均值 ± 2 个标准差,我们将得到区间 (-0.02, 0.7);上限值与我们从 HDI 得到的 0.66 相差不远,但下限实际上超出了 θ 的可能值(它应该在 0 和 1 之间)。
另一种直观总结后验的方法是使用 ArviZ 中的 az.plot_posterior 函数(见 图 2.3)。我们在前一章中已经用它处理了一个虚假的后验。现在,我们将使用它来处理一个真实的后验。
代码 2.6
az.plot_posterior(idata)

图 2.3:该图显示了 θ 的后验分布及 94% HDI。
默认情况下,plot_posterior 为离散变量显示直方图,为连续变量显示核密度估计(KDE)。我们还可以得到分布的均值(可以通过point_estimate参数请求中位数或众数)和 94%的 HDI,该黑线位于图表底部。可以使用hdi_prob参数为 HDI 设置不同的区间值。这种类型的图表是由 John K. Kruschke 在他的伟大著作《Doing Bayesian Data Analysis》中首次提出的[Kruschke, 2014]。
2.3 基于后验的决策
有时候,仅仅描述后验是不够的。我们可能需要基于推断做出决策,并将连续估计转化为二分法:是-否,健康-生病,污染-安全,等等。例如,硬币是否公平?一个公平的硬币是θ值恰好为 0.5 的硬币。我们可以将 0.5 的值与 HDI 区间进行比较。从图 2.3中,我们可以看到 HDI 区间从 0.03 到 0.7,因此 0.5 包含在 HDI 内。我们可以将此解读为硬币可能有尾部偏向,但我们不能完全排除硬币实际上是公平的可能性。如果我们希望做出更明确的决策,我们需要收集更多的数据来减少后验的分布范围,或者可能需要重新定义一个更具信息性的先验。
2.3.1 Savage-Dickey 密度比
评估后验提供给某个特定值的支持力度的一种方法是比较该值下后验密度和先验密度的比值。这称为 Savage-Dickey 密度比,我们可以使用 ArviZ 通过az.plot_bf函数来计算:
代码 2.7
az.plot_bf(idata, var_name="*θ*",
prior=np.random.uniform(0, 1, 10000), ref_val=0.5);

图 2.4:该图显示了our_first_model的先验和后验;黑色点表示在参考值 0.5 处评估的值
从图 2.4中我们可以看到,BF_01的值为 1.3,这意味着在后验分布下,θ = 0.5 的值比在先验分布下更有可能,概率是 1.3 倍。为了计算这个值,我们将后验在θ = 0.5 处的高度除以前验在θ = 0.5 处的高度。BF_10的值则是其倒数!-1- 1.3 ≈ 0.8。我们可以理解为在后验分布下,θ≠0.5的可能性是先验分布下的 0.76 倍。我们如何解读这些数字?要带有一丝怀疑……以下表格展示了 Kass 和 Raftery 在 1995 年提出的其中一种可能的解释:
| BF_01 | 解释 |
|---|---|
| 1 到 3.2 | 不值一提 |
| 3.2 到 10 | 实质性 |
| 10 到 100 | 强 |
| > 100 | 决定性 |
表 2.2:贝叶斯因子(BF_01)的解释
Savage-Dickey 密度比率是一种特定的计算方法,用来求解所谓的贝叶斯因子。我们将在 第五章 中学习更多关于贝叶斯因子及其注意事项。
2.3.2 实际等价区域
严格来说,观察到精确的 0.5(即带有无限多个零)的概率为零。此外,在实践中,我们通常不关心精确结果,而是关心某个范围内的结果。因此,在实践中,我们可以放宽对公平性的定义,认为一个公平的硬币的值应该是 接近 0.5。例如,我们可以说,区间 [0.45, 0.55] 内的任何值对于我们的目的来说,实际上都等同于 0.5。我们称这个区间为实际等价区域(ROPE)。一旦定义了 ROPE,我们就可以将其与 HDI 进行比较。我们可以得到至少三种情况:
-
ROPE 不与 HDI 重叠;我们可以说硬币是不公平的
-
ROPE 包含了整个 HDI;我们可以说硬币是公平的
-
ROPE 部分与 HDI 重叠;我们不能说硬币是公平的还是不公平的
如果我们选择将 ROPE 与参数的支持区域匹配,例如在硬币投掷示例中使用 [0, 1],我们将总是认为硬币是公平的。请注意,我们不需要收集数据来进行任何推断。
ROPE 的选择完全是任意的:我们可以选择任何我们想要的值。有些选择并没有什么实际意义。例如,对于硬币投掷示例,如果我们选择 ROPE 为 [0, 1],那么我们将总是认为硬币是公平的。更重要的是,我们不需要收集数据或进行任何分析来得出这个结论,这是一个微不足道的例子。更值得担心的是,在进行分析之后选择 ROPE。这是有问题的,因为我们可以调整结果来使它符合我们想要的结论。但如果我们要调整结果以符合我们的预期,那我们为什么还要做分析呢?ROPE 应该由领域知识来决定。
我们可以使用 plot_posterior 函数来绘制包含 HDI 区间和 ROPE 的后验分布。ROPE 显示为半透明的粗(灰色)线:
代码 2.8
az.plot_posterior(idata, rope=[0.45, .55])

图 2.5:该图展示了 θ 的后验分布和 94% HDI。ROPE 以一条粗的浅灰色线表示
另一个我们可以用来帮助决策的工具是将后验分布与参考值进行比较。我们可以使用 plot_posterior 来实现。如 图 2.6 所示,我们得到一条垂直的(灰色)线,并且可以看到后验分布在参考值之上和之下的比例:
代码 2.9
az.plot_posterior(idata, ref_val=0.5)

图 2.6:该图展示了 θ 的后验分布和 94% HDI。参考值显示为灰色垂直线
关于 ROPE 使用的更详细讨论,可以阅读 Kruschke 的《Doing Bayesian Data Analysis》第十二章[2014]。该章节还讨论了如何在贝叶斯框架中进行假设检验及其注意事项,无论是在贝叶斯还是非贝叶斯设置下。
2.3.3 损失函数
如果你觉得这些 ROPE 规则听起来有些繁琐,并且想要更正式的东西,损失函数就是你所寻找的!为了做出一个好的决策,重要的是对相关参数的估计值具有尽可能高的精度,但同样重要的是要考虑犯错的代价。成本/收益的权衡可以通过数学化的方式使用损失函数来形式化。损失函数或其逆的名称在不同领域中各不相同,我们可能会遇到诸如成本函数、目标函数、适应度函数、效用函数等名称。无论名称如何,关键思想是使用一个函数来捕捉参数的真实值和估计值之间的差异。损失函数的值越大,估计结果越差(根据损失函数的定义)。一些常见的损失函数示例如下:
-
绝对损失函数,|θ −
| -
二次损失函数,(θ −
)² -
0-1 损失函数,1(**θ* ≠*
),其中 是指示函数
实际上,我们并不知道真实参数的值。相反,我们有一个后验分布形式的估计。因此,我们可以做的是找出一个 θ,使其最小化期望损失函数。所谓的期望损失函数是指在整个后验分布上平均的损失函数。
在以下代码块中,我们有两个损失函数:绝对损失(lossf_a)和二次损失(lossf_b)。我们将探索一个包含 200 个点的网格上的值。然后,我们会绘制这些曲线,并且还会包括最小化每个损失函数的 θ 值。以下代码块展示了没有绘图部分的 Python 代码:
代码 2.10
grid = np.linspace(0, 1, 200)
*θ*_pos = idata.posterior['*θ*']
lossf_a = [np.mean(abs(i - *θ*_pos)) for i in grid]
lossf_b = [np.mean((i - *θ*_pos)**2) for i in grid]
for lossf, c in zip([lossf_a, lossf_b], ['C0', 'C1']):
...

图 2.7:应用于 our_first_model 后验的绝对(黑色)和二次(灰色)损失函数
更有趣的是,从图 2.7可以看到,我们从绝对损失得到的值等于后验分布的中位数,而从二次损失得到的值等于后验分布的均值。你可以通过计算np.mean(θ_pos),np.median(θ_pos)来验证这一点。这并非巧合:不同的损失函数与不同的点估计有关。均值是最小化二次损失的点估计,中位数是最小化绝对损失的点估计,而众数是最小化 1-0 损失的点估计。
如果我们想正式一点,并且想要计算一个单点估计值,我们必须决定选择哪个损失函数。相反地,如果我们选择一个点估计值,我们实际上(可能是无意识地)选择了一个损失函数。显式选择损失函数的优点是我们可以根据自己的问题量身定制函数,而不是使用预定义的规则。我们常常会发现,做决策的成本是非对称的;例如,疫苗可能会引起免疫系统的过度反应,但接种疫苗的人,甚至未接种疫苗的人,所获得的益处通常远远超过风险,差距通常很大。因此,如果我们的任务要求,我们可以构建一个非对称的损失函数。还需要注意的是,由于后验分布是数值样本的形式,我们可以计算复杂的损失函数,这些函数不必受限于数学便利性或单纯的简化。下面的代码,以及由它生成的图 2.8,就是这个概念的一个简单示例:
代码 2.11
lossf = []
for i in grid:
if i < 0.5:
f = 1/np.median(*θ*_pos / np.abs(i**2 - *θ*_pos))
else:
f = np.mean((i - *θ*_pos)**2 + np.exp(-i)) - 0.25
lossf.append(f)

图 2.8:应用于our_first_model后验分布的一个奇怪损失函数
直到现在,我们一直在讨论贝叶斯统计和概率编程的主要概念,主要通过 BetaBinomial 模型,因为它的简洁性。在构建更复杂模型的过程中,我们现在将焦点转向高斯推理的领域。
2.4 高斯分布的深入探索
从数学角度来看,高斯分布非常吸引人。与高斯分布进行工作相对容易,许多应用于高斯分布的操作会返回另一个高斯分布。此外,许多自然现象可以使用高斯分布进行很好的近似;实际上,几乎每次我们测量某物的平均值时,只要样本量足够大,该平均值就会呈高斯分布。这种现象何时成立、何时不成立以及何时大致成立的细节在中心极限定理(CLT)中有详细阐述;你可能现在就想停下来,去查找一下这个非常核心的统计学概念(有意的恶搞)。
好吧,我们刚才说过,许多现象确实是平均值的结果。举个老套的例子,身高(几乎所有人的其他特征也是如此)是许多环境因素和许多遗传因素的综合结果,因此我们可以得到成年人的身高符合高斯分布。事实上,我们得到的是两个高斯分布的混合,这是女性和男性身高分布重叠的结果,但你大概明白了。总之,高斯分布是易于使用且在自然现象中普遍存在的;因此,许多你可能已经了解的统计方法假设数据是符合正态分布的。因此,学习如何建立这些模型很重要,而同样重要的是学会如何放宽正态性假设,这在贝叶斯框架和现代计算工具如 PyMC 中是非常容易的。
2.4.1 高斯推断
核磁共振(NMR)是一种强大的技术,用于研究分子以及诸如人类、向日葵和酵母等生物(毕竟,我们不过是一堆分子)。NMR 使得我们能够测量与有趣的不可观察分子性质相关的不同种类的可观察量 Arroyuelo 等人 [2021]。其中一种可观察量被称为化学位移,这个值仅能通过某些类型原子的核来获取。具体的细节属于量子化学的范畴,跟本讨论无关。就我们目前关心的来说,我们也可以测量一群人的身高、回家所需的平均时间或一袋袋橙子的重量。在这些例子中,变量是连续的,因此把它们看作是一个平均值加上一个离散度是有意义的。有时如果可能的值足够多,我们可以用高斯模型来处理离散变量;例如,倭黑猩猩非常好色,所以也许我们可以用高斯分布来建模我们表亲的性伴侣数量。
回到我们的例子,我们在图 2.9 中通过箱型图表示了 48 个化学位移值。我们可以看到,中位数(箱内的线)大约是 53,而四分位距(箱子的范围)大约是 52 和 55。我们还可以看到,有两个值远离其余数据(空心圆)。

图 2.9:48 个化学位移值的箱型图。我们观察到有两个值超过 60,远离其余数据。
让我们暂时忘记这两个点,假设高斯分布是描述数据的一个合理模型。由于我们不知道均值或标准差,因此必须为它们设置先验分布。因此,一个合理的模型可以是:

(l,h)是l和h之间的均匀分布,
(σ[σ])是带有尺度σ[σ]的 HalfNormal 分布,
(μ,σ)是均值为μ,标准差为σ的高斯分布。HalfNormal 分布考虑的是以零为中心的正态分布的绝对值。图 2.10展示了该模型的图形表示。

图 2.10: model_g的图形表示
如果我们不知道μ和σ的可能值,可以设置反映我们无知的先验分布。一种选择是将均匀分布的边界设置为l = 40,h = 75,这是一个比数据范围更大的范围。另一种选择是根据我们之前的知识选择一个范围。例如,我们可能知道这种类型的测量值不可能低于 0 或高于 100,因此可以将这些值作为均匀分布的边界。对于 HalfNormal 分布,在没有更多信息的情况下,我们可以选择一个相对于数据规模较大的值。表示在图 2.10中模型的 PyMC 代码是:
代码 2.12
with pm.Model() as model_g:
μ = pm.Uniform('μ', lower=40, upper=70)
σ = pm.HalfNormal('σ', sigma=5)
Y = pm.Normal('Y', mu=μ, sigma=σ, observed=data)
idata_g = pm.sample()
让我们看看后验分布的形态。图 2.11是通过 ArviZ 的plot_trace函数生成的。它每一行对应一个参数。对于该模型,后验分布是二维的,因此每一行展示一个边际分布。

图 2.11: 使用az.plot_trace(idata_g)绘制的model_g后验分布
我们可以使用 ArviZ 的plot_pair函数查看二维后验分布的形态,并同时查看μ和σ的边际分布。请参见图 2.12:

图 2.12: 使用az.plot_pair(idata_g, kind=’kde’, marginals=True)绘制的model_g后验分布
我们将打印汇总以供以后使用(参见表 2.3)。我们使用以下代码:
代码 2.13
az.summary(idata_g, kind="stats").round(2)
| 均值 | 标准差 | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| μ | 53.50 | 0.52 | 52.51 | 54.44 |
| σ | 3.52 | 0.38 | 2.86 | 4.25 |
表 2.3: μ和σ的汇总统计
2.5 后验预测检查
贝叶斯工具包的一个优点是,一旦我们得到了后验p(θ|Y ),就可以用它来生成预测p(Ỹ)。在数学上,这可以通过计算来完成:

这种分布被称为后验预测分布。它是预测性的,因为它用于进行预测,并且是后验的,因为它是使用后验分布计算的。因此,我们可以将其视为给定模型和观察数据后未来数据的分布。
使用 PyMC 获取后验预测样本非常简单;我们无需计算任何积分。只需要调用sample_posterior_predictive函数,并将InferenceData对象作为第一个参数传入。我们还需要传入model对象,并可以使用extend_inferencedata参数将后验预测样本添加到InferenceData对象中。代码如下:
代码 2.14
pm.sample_posterior_predictive(idata_g, model=model_g,
extend_inferencedata=True)
后验预测分布的一个常见用途是进行后验预测检验。这是一组可以用来检查模型是否适合数据的测试。我们可以使用 ArviZ 中的plot_ppc函数来可视化后验预测分布和观测数据。代码如下:
代码 2.15
az.plot_ppc(idata_g, num_pp_samples=100)

图 2.13:使用az.plot_ppc绘制的model_g后验预测检验
在图 2.13中,黑色线条是数据的 KDE,灰色线条是从每一个 100 个后验预测样本中计算出的 KDE。灰色线条反映了我们对预测数据分布的不确定性。当数据点很少时,图像看起来会显得杂乱或奇怪;这是常见的情况。默认情况下,ArviZ 中的 KDE 是在实际数据范围内估算的,并假设在数据范围之外为零。虽然有人可能认为这是一个 bug,但我认为它是一个特性,因为它反映了数据的一个特性,而不是过度平滑处理。
从图 2.13中,我们可以看到模拟数据的均值略微向右偏移,并且模拟数据的方差似乎比实际数据大。这个差异的来源可以归因于我们选择的似然函数和两个偏离数据主体的观测值(在图 2.9中为空心点)。我们如何解释这个图?模型是错误的还是正确的?我们可以使用它吗,还是需要换一个模型?其实,这取决于情况。模型的解释、评估和批评始终是依赖于具体情境的。根据我在这种测量中的经验,我认为这个模型是对数据的一个合理且足够的表示,并且对于我大多数分析来说非常有用。不过,重要的是要记住,我们可能会找到其他更好地适应整个数据集的模型,包括那两个远离数据主体的观测值。让我们看看如何做到这一点。
2.6 鲁棒推断
我们对model_g可能有一个异议,那就是我们假设了正态分布,但数据中有两个点远离数据的主体。通过使用正态分布作为似然性,我们间接假设我们不期望看到大量远离主体的数据点。图 2.13 显示了将这些假设与数据结合的结果。由于正态分布的尾部随着离均值越远而迅速下降,正态分布(至少是人性化的正态分布)对这两个点的出现感到惊讶,并以两种方式做出反应,将其均值向这两个点移动,并增加其标准差。另一种直观的解释方式是,认为这些点在决定正态分布参数时有过大的权重。
那么,我们该怎么做呢?一个选项是检查数据中的错误。如果我们回溯步骤,可能会发现清理或预处理数据时代码出错,或者可以将这些假定的异常值与测量设备故障关联起来。不幸的是,这并不总是一个选项。很多时候,数据是由别人收集的,我们没有很好的记录它是如何收集、测量或处理的。无论如何,在建模之前检查数据总是一个好主意,这在一般情况下都是一种良好的做法。
另一个选项是将这些点声明为离群值并将其从数据中移除。识别数据集离群值的两条常见经验法则是:
-
使用四分位间距(IQR):任何低于下四分位数 1.5 倍 IQR,或高于上四分位数 1.5 倍 IQR 的数据点,都被认为是离群值。
-
使用标准差:任何低于或高于数据标准差N倍的数据点都被认为是离群值。通常N的值是 2 或 3。
然而,需要注意的是,像任何自动化方法一样,这些经验法则并不完美,可能会导致丢弃有效的数据点。
从建模的角度来看,我们可以将问题归咎于模型,而不是数据,并进行修改,正如下一节所解释的那样。一般来说,贝叶斯方法更倾向于通过使用不同的先验和似然性,将假设直接编码到模型中,而不是通过诸如去除离群值规则等临时启发式方法。
2.6.1 正常性的程度
有一个分布看起来非常类似于正态分布。它有三个参数:位置参数μ,尺度参数σ,以及正态性参数ν。这个分布被称为学生 t 分布。图 2.14展示了这一家族的成员。当ν = ∞时,分布就是正态分布,μ是均值,σ是标准差。当ν = 1 时,我们得到的是柯西分布或洛伦兹分布。ν的取值范围是从 0 到∞。这个值越小,分布的尾部越重。我们还可以说,ν值越小,峰度越高。峰度是第四阶矩,你可能还记得前一章中提到的。所谓“重尾”,是指在这种分布中,偏离均值的值比在正态分布中更为常见,换句话说,值的分布不像正态分布那样集中在均值附近。例如,95%的学生 t 分布值(μ = 0,σ = 1,ν = 1)会落在-12.7 到 12.7 之间。而对于正态分布(μ = 0,σ = 1,ν = ∞),这一范围是-1.96 到 1.96。

图 2.14:学生 t 分布
学生 t 分布的一个非常有趣的特点是,当ν ≤ 1 时,它没有定义的均值。虽然从学生 t 分布中得到的任何有限样本都可以计算出经验均值,但理论上的分布本身没有定义均值的值。直观地说,可以理解为:分布的尾部非常重,以至于在任何时候我们可能从实数线上几乎任何位置抽取一个样本值,因此如果我们不断地得到数值,我们永远不会接近一个固定的值。相反,估计值将不断地游走。
什么自由度?
在大多数教科书中,学生 t 分布的参数ν被称为自由度参数。然而,我更倾向于遵循 Kruschke 的建议,将其称为正态性参数。这个名称更能描述该参数在分布中的作用,特别是在用于鲁棒回归时。
同样,这个分布的方差只有在ν > 2 时才被定义。因此,重要的是要注意,学生 t 分布的尺度与其标准差不同。随着ν趋近于无穷大,尺度和标准差会越来越接近。
2.6.2 正态模型的鲁棒版本
我们将通过将高斯分布替换为学生 t 分布来重写前面的模型(model_g)。由于学生 t 分布比高斯分布多了一个参数ν,因此我们需要指定一个额外的先验,对于这个模型我们决定使用指数分布,但其他限制在正区间的分布也可以适用。

图 2.15 显示了该模型的图形表示

图 2.15:model_t 的图形表示
让我们用 PyMC 编写这个模型;像往常一样,我们可以通过指定几行代码来(重新)编写模型。唯一需要注意的是,PyMC 中的指数分布默认使用均值的倒数作为参数化方式。我们将设置 ν 为均值为 30 的指数分布。从 图 2.14 中可以看到,ν = 30 的学生 t 分布与高斯分布相似(即使它并非真正的高斯分布)。事实上,从相同的图中我们可以看到,大部分的变化发生在较小的 ν 值上。因此,我们可以说,均值为 30 的指数先验是一种弱信息先验,告知模型我们大致认为 ν 应该在 30 附近,但也能容易地向较小或较大的值偏移。在许多问题中,估计 ν 并非直接关注的重点。
代码 2.16
with pm.Model() as model_t:
μ = pm.Uniform('μ', 40, 75)
σ = pm.HalfNormal('σ', sigma=10)
ν = pm.Exponential('ν', 1/30)
y = pm.StudentT('y', nu=ν, mu=μ, sigma=σ, observed=data)
idata_t = pm.sample()
比较 model_g 的轨迹(图 2.11) 与 model_t 的轨迹(图 2.16):

图 2.16:使用 az.plot_trace(idata_t) 绘制的 model_t 后验
现在,打印出 model_t 的总结。你应该得到类似于 表 2.4 的内容。将结果与 model_g 的结果进行比较。
| 均值 | 标准差 | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| μ | 53.02 | 0.39 | 52.27 | 53.71 |
| σ | 2.21 | 0.42 | 1.46 | 3.01 |
| ν | 4.94 | 5.45 | 1.07 | 10.10 |
表 2.4:μ、σ 和 ν 的总结统计
在继续阅读之前,花点时间将前面的结果与 model_g 的结果进行比较,并找出两者之间的差异。你发现了什么有趣的地方吗?
两个模型之间的 μ 估计值相似,差异约为 ≈ 0.5. σ 的估计值对于 model_g 为 ≈ 3.5, 对于 model_t 为 ≈ 2.2。这是因为学生 t 分布将较远离均值的值分配较小的权重。简单来说,学生 t 分布对远离均值的值不那么惊讶。我们还可以看到 ν 的均值为 ≈ 5,这意味着我们有一个重尾分布,而非类似高斯分布的分布。
图 2.17 显示了 model_t 的后验预测检验。让我们将其与 model_g 的结果(*图 2.13)进行比较。使用学生 t 分布的模型产生的预测样本似乎在分布的峰值位置以及扩展范围上更好地拟合数据。请注意,样本如何从数据的主要部分延伸得很远,且一些预测样本看起来非常平坦。这是学生 t 分布期望看到远离均值或数据主体的数据点的直接结果。如果你查看用于生成 图 2.17 的代码,你会发现我们使用了 ax.set_xlim(40, 70)。

图 2.17:model_t 的后验预测检查
学生 t 分布使我们能够更 稳健地估计均值和标准差,因为异常值对 ν 的影响是减少,而不是拉动均值或增大标准差。因此,均值和尺度是通过加权靠近主体的数据点来估计的,而远离主体的数据点则权重较小。作为经验法则,对于 ν > 2 且 不太小 的值,我们可以将学生 t 分布的尺度视为去除异常值后的数据标准差的合理实用代理。这是一个经验法则,因为我们知道尺度并不是标准差。
2.7 InferenceData
InferenceData 是一个包含贝叶斯推断结果的丰富容器。现代的贝叶斯分析可能会生成多组数据,包括后验样本和后验预测样本。但是我们也有观察数据、先验样本,甚至是由采样器生成的统计量。所有这些数据以及更多内容都可以存储在一个 InferenceData 对象中。为了帮助组织这些信息,每一组数据都有其自己的组。例如,后验样本存储在 posterior 组中,观察数据存储在 observed_data 组中。
图 2.18 显示了 model_g 的 InferenceData 对象的 HTML 表示。我们可以看到 4 个组:posterior、posterior_predictive、sample_stats 和 observed_data。除了 posterior 组外,其他组都已折叠。我们可以看到有两个坐标 chain 和 draw,其维度分别为 4 和 1000。我们还有 2 个变量 μ 和 σ。

图 2.18:model_g 的 InferenceData 对象
到目前为止,PyMC 已经生成了一个 InferenceData 对象,而 ArviZ 已使用该对象生成图表或数值摘要。但是,我们也可以操作 InferenceData 对象。一些常见的操作是访问特定的组。例如,要访问后验组,我们可以这样写:
代码 2.17
posterior = idata_g.posterior
这将返回一个 xarray 数据集。如果你不熟悉 xarray [Hoyer 和 Hamman, 2017]( docs.xarray.dev/en/stable/),可以把它想象成带标签的 NumPy 多维数组!这使得许多操作变得更简单,因为你不需要记住维度的顺序。例如,以下代码将返回链条 0 和链条 2 的第一次抽样:
代码 2.18
posterior.sel(draw=0, chain=[0, 2])
我们使用 sel 方法选择一系列值,例如从所有链条中选择前 100 次抽样:
代码 2.19
posterior.sel(draw=slice(0, 100))
此外,以下代码返回了在所有抽样和链条上计算的 μ 和 σ 的均值:
代码 2.20
posterior.mean()
同时,以下代码返回抽样的均值,即,它会返回 μ 和 σ 的四个值,每个链条一个:
代码 2.21
posterior.mean("draw")
我们通常不关心链和绘图,只想获得后验样本。在这种情况下,我们可以使用az.extract函数:
代码 2.22
stacked = az.extract(idata_g)
这将chain和draw合并为一个sample坐标,这可以使后续操作更为简便。默认情况下,az.extract作用于后验分布,但你也可以通过group参数指定其他组。你还可以使用az.extract来获取后验的随机样本:
代码 2.23
az.extract(idata_g, num_samples=100)
本书中我们将一直使用 InferenceData 对象,因此你将有时间熟悉它并在接下来的页面中了解更多。
2.8 组间比较
一种常见的统计分析是组间比较。我们可能会对患者对某种药物的反应、交通法规引入后车祸减少、不同教学方法下的学生表现等感兴趣。有时,这类问题会以假设检验的形式呈现,目标是声明某个结果具有统计学意义。仅仅依赖统计学显著性可能会带来很多问题:一方面,统计学显著性不等于实际意义;另一方面,单纯通过收集足够的数据,就可能宣布一个非常小的效应为显著。
假设检验的概念与 p 值的概念相关。这种联系并非根本性的,而是一种文化上的联系;人们习惯于这样思考,主要是因为这是大多数入门统计课程中教授的内容。长期以来有很多研究和论文表明,p 值经常被错误使用和解读,甚至是那些每天都在使用它们的人。我们不会进行假设检验,而是采取不同的途径,专注于估计效应量,即量化两组之间的差异。思考效应量的一个好处是,我们可以摆脱“是否有效?”或“是否有影响?”这样的简单是非问题,转而提出更细致的问题,如“效果如何?”或“效应有多大?”。
有时,在比较不同组时,人们会提到对照组和实验组。例如,当我们想要测试一种新药时,我们希望将新药(实验组)与安慰剂(对照组)进行比较。安慰剂效应是一种心理现象,患者在接受无效物质或治疗后,可能会感觉症状或状况有所改善。通过在临床试验中将药物与安慰剂组的效果进行比较,研究人员可以辨别药物是否真的有效。安慰剂效应是实验设计和统计分析中的一个广泛挑战的例子,表明在实验中考虑所有因素是困难的。
这种设计的一个有趣替代方案是,将新药与市面上最受欢迎或最有效的药物进行比较,用以治疗该疾病。在这种情况下,控制组不能是安慰剂;它应该是另一种药物。虚假的控制组是用统计学撒谎的一个绝佳方法。
例如,假设你为一家乳制品公司工作,这家公司想通过告诉父母这种酸奶可以增强免疫系统或帮助孩子更强壮来向孩子们推销过多含糖的酸奶。一种通过数据作假的方式是使用牛奶或甚至水作为对照组,而不是另一种更便宜、含糖量更少、市场推广较少的酸奶。把事情说成这样时,可能会显得很荒谬,但我正在描述的是在实际科学期刊上发表的真实实验。当有人说某物更难、更好、更快或更强时,记得问一下用于比较的基准是什么。
2.8.1 小费数据集
为了探讨本节的主题,我们将使用提示数据集[Bryant 和 Smith,1995]。我们想研究星期几对餐厅小费的影响。在这个例子中,不同的组别是不同的星期几。请注意,这里没有控制组或处理组。如果我们愿意,可以随意将某一天(例如星期四)设定为参考组或控制组。现在,让我们通过仅用一行代码将数据集加载为 pandas DataFrame 来开始分析。如果你不熟悉 pandas,tail 命令用于显示 DataFrame 的最后几行(见 表 2.5),你也可以尝试使用 head:
代码 2.24
tips = pd.read_csv("data/tips.csv")
tips.tail()
| 总账单 | 小费 | 性别 | 吸烟者 | 星期几 | 餐次 | 人数 | |
|---|---|---|---|---|---|---|---|
| 239 | 29.03 | 5.92 | 男性 | 否 | 星期六 | 晚餐 | 3 |
| 240 | 27.18 | 2.00 | 女性 | 是 | 星期六 | 晚餐 | 2 |
| 241 | 22.67 | 2.00 | 男性 | 是 | 星期六 | 晚餐 | 2 |
| 242 | 17.82 | 1.75 | 男性 | 否 | 星期六 | 晚餐 | 2 |
| 243 | 18.78 | 3.00 | 女性 | 否 | 星期四 | 晚餐 | 2 |
表 2.5:餐厅样本数据
在这个 DataFrame 中,我们只使用 day 和 tip 两列。图 2.19 显示了使用 ridge 图展示的数据分布。这个图是用 ArviZ 绘制的。尽管 ArviZ 主要用于贝叶斯模型分析,但它的一些功能在数据分析中也很有用。

图 2.19:按星期几分布的小费
我们将对数据进行一些小的预处理。首先,我们将创建一个 tip 变量,表示以美元为单位的小费。然后我们创建 idx 变量,一个类别虚拟变量,用数字编码星期几,也就是说,[0, 1, 2, 3] 代替 [’Thur’, ’Fri’, ’Sat’, ’Sun’]。
代码 2.25
categories = np.array(["Thur", "Fri", "Sat", "Sun"])
tip = tips["tip"].values
idx = pd.Categorical(tips["day"], categories=categories).codes
这个问题的模型几乎与model_g相同,唯一的区别是现在μ和σ将变成向量而非标量。PyMC 的语法在这种情况下非常有帮助:我们可以以矢量化的方式编写模型,而不是使用循环。
代码 2.26
with pm.Model() as comparing_groups:
μ = pm.Normal("μ", mu=0, sigma=10, shape=4)
σ = pm.HalfNormal("σ", sigma=10, shape=4)
y = pm.Normal("y", mu=μ[idx], sigma=σ[idx], observed=tip)
请注意我们如何为先验分布传递了一个shape参数。对于μ,这意味着我们指定了四个独立的(0,10),而对于σ,则是四个独立的
(10)。另外,请注意我们如何使用
idx变量来正确索引传递给似然函数的μ和σ的值。
PyMC 提供了一种替代语法,要求指定坐标和维度。这种替代方法的优势在于它能更好地与 ArviZ 集成。
在这个例子中,我们有 4 个均值和 4 个标准差,因此我们使用shape=4。InferenceData 将具有 4 个索引0, 1, 2, 3,分别对应每个 4 天。然而,将这些数值索引与具体的日期关联起来是用户的任务。通过使用坐标和维度,我们以及 ArviZ 可以使用标签"Thur", "Fri", "Sat", "Sun"轻松地将参数映射到相应的日期。
我们将指定两个坐标;"days",其维度为"Thur", "Fri", "Sat", "Sun";和"days_flat",它将包含相同的标签,但根据每个观察值的顺序和长度重复。"days_flat"将在后续的后验预测测试中发挥作用。
代码 2.27
coords = {"days": categories, "days_flat":categories[idx]}
with pm.Model(coords=coords) as comparing_groups:
μ = pm.HalfNormal("μ", sigma=5, dims="days")
σ = pm.HalfNormal("σ", sigma=1, dims="days")
y = pm.Gamma("y", mu=μ[idx], sigma=σ[idx], observed=tip, dims="days_flat")
idata_cg = pm.sample()
idata_cg.extend(pm.sample_posterior_predictive(idata_cg))
一旦后验分布被计算出来,我们就可以进行所有我们认为相关的分析。例如,我们可以进行后验预测测试。在 ArviZ 的帮助下,我们可以通过调用az.plot_ppc来实现。我们使用coords和flatten参数来为每一天获取一个子图。
代码 2.28
_, axes = plt.subplots(2, 2)
az.plot_ppc(idata_cg, num_pp_samples=100,
coords={"days_flat":[categories]}, flatten=[], ax=axes)
从下图中可以看出,模型能够捕捉到分布的一般形状,但仍有一些细节难以捉摸。这可能是由于样本量相对较小,除了日期外,其他因素对小费的影响,或者两者的结合。

图 2.20:Tips 数据集的后验预测检查
目前,我们认为该模型足够好,可以继续探索后验分布。我们可以从平均值的角度解释结果,然后找出哪些日期的平均值较高。但也有其他方法;例如,我们可能希望用后验均值差异来表达结果。此外,我们可能想要使用一些对我们受众来说更常见的效应大小度量,如优势概率或 Cohen's d。在接下来的部分中,我们将解释这些替代方法。
2.8.2 Cohen's d
测量效应大小的常见方法是 Cohen's d,定义如下:

由于我们有后验分布,因此可以计算 Cohen’s d 的分布。如果我们需要一个单一值,可以计算该分布的均值或中位数。
这个公式告诉我们,效应大小是均值差异与两组合并标准差的比值。通过取合并标准差,我们对均值差异进行了标准化。这一点非常重要,因为当差异为 1 且标准差为 0.1 时,效应大小大于标准差为 10 时相同差异的效应大小。Cohen’s d 可以被解释为 Z 得分(标准分数)。Z 得分是一个有符号的标准差数,表示一个值与被观测或测量的均值的差异。因此,0.5 的 Cohen’s d 可以被解释为两组之间差异为 0.5 个标准差。
即使均值差异已被标准化,我们仍然可能需要根据特定问题的上下文进行校准,才能判断某个给定值是大是小是中等。例如,如果我们习惯于对相同或相似的问题进行多次分析,我们可以习惯于 Cohen’s d 为 1。于是,当我们得到一个 Cohen’s d 为 2 时,我们就知道我们得到了重要的结果(或者是某处出错了!)。如果您还没有这种经验,您可以请教领域专家,获取他们宝贵的意见。
一个非常好的网页,可以探索 Cohen’s d 的不同值是什么样子的,网址是rpsychologist.com/d3/cohend。在该页面上,您还可以找到表示效应大小的其他方法;其中一些可能更为直观,例如超越概率,我们将在下一部分讨论。
2.8.3 超越概率
这是报告效应大小的另一种方式,它定义为从一个组中随机抽取的数据点大于从另一个组中随机抽取的一个数据点的概率。如果我们假设所使用的数据是正态分布的,那么我们可以使用以下公式通过 Cohen’s d 计算超越概率:

Φ是累积正态分布,而δ是 Cohen’s d。
如果我们接受正态性假设,那么可以使用此公式从 Cohen’s d 的值计算超越概率。否则,我们可以直接通过从两个组中随机抽取样本并计算有多少次一个值大于另一个值来计算超越概率。这样,我们不需要 Cohen’s d 或假设正态性(请参见练习部分)。这是使用马尔可夫链蒙特卡洛(MCMC)方法的一个优点;一旦我们从后验分布中获得样本,就可以通过这种方式计算许多量,通常比其他方法更为简单。
2.8.4 平均差异的后验分析
为了总结我们之前的讨论,让我们计算均值差异、Cohen's d 和优势概率的后验分布,并将它们整合到一个图表中。图 2.21包含了大量信息。根据受众的不同,图表可能会过载,或者太拥挤。或许它适合你团队内部的讨论,但对于大众而言,去除某些元素或将信息分布在一个图表和一个表格,或者两个图表之间,可能会更方便。无论如何,在这里我们精确地展示它,以便你可以比较展示相同信息的不同方式,所以请花些时间思考这个图表。

图 2.21:小费数据集的均值差异、Cohen’s d 和优势概率的后验分布
阅读图 2.21的一种方法是将零差异的参考值与 HDI 区间进行比较。只有一个案例,94%的 HDI 排除了参考值,也就是星期四和星期天的小费差异。在所有其他比较中,根据 HDI-参考值重叠标准,我们不能排除零差异。但即使是那个案例,平均差异也约为 0.5 美元。这个差异足够大吗?这个差异足够支持接受在星期天工作,并错过和家人或朋友共度时光的机会吗?这个差异足够让我们接受将小费平均分配到四天,并给每个女服务员和男服务员相同的小费金额吗?
简短的回答是,这类问题不能通过统计学来回答;它们只能通过统计学来提供信息。我希望你不会因为这个答案而感到失望,但除非我们在分析中包含所有对相关方重要的值,否则我们无法得到自动的答案。正式来说,这要求定义一个损失函数,或至少定义一个效应大小的阈值,这个阈值应该由这些值来决定。
2.9 总结
尽管贝叶斯统计在概念上很简单,但完全概率模型常常导致解析上不可处理的表达式。多年来,这一直是一个巨大的障碍,阻碍了贝叶斯方法的广泛应用。幸运的是,数学、统计学、物理学和计算机科学通过数值方法来拯救这一局面,这些方法在原则上能够解决任何推断问题。自动化推断过程的可能性促使了概率编程语言的发展,这些语言清晰地将模型定义和推断分开。PyMC 是一个用于概率编程的 Python 库,具有非常简单、直观且易读的语法,而且非常接近描述概率模型的统计语法。
我们通过重新回顾第一章中的抛硬币模型来介绍 PyMC 库,这次我们没有通过解析方法推导后验分布。PyMC 模型定义在上下文管理器中。为了将概率分布添加到模型中,我们只需写一行代码。分布可以组合使用,可以作为先验(未观察变量)或似然(已观察变量)。如果我们将数据传递给分布,它将成为似然。采样也可以通过一行代码实现。PyMC 允许我们从后验分布中获取样本。如果一切顺利,这些样本将代表正确的后验分布,因此它们将成为我们模型和数据的逻辑后果的表示。我们可以使用与 PyMC 配合使用的 Python 库 ArviZ 来探索 PyMC 生成的后验分布,ArviZ 可以帮助我们解释和可视化后验分布。在使用后验帮助我们做推理驱动的决策时,一种方法是将 ROPE 与 HDI 区间进行比较。我们还简要提到过损失函数的概念,这是量化在不确定性下做决策时的权衡和成本的一种正式方式。我们学到,损失函数和点估计是紧密相关的。
到目前为止,讨论仅限于一个简单的单参数模型。使用 PyMC 将其推广到任意数量的参数是非常简单的;我们通过高斯分布和学生 t 分布模型来展示如何做到这一点。高斯分布是学生 t 分布的特例,我们向你展示了如何使用后者在存在离群值的情况下进行稳健推断。在接下来的章节中,我们将探讨这些模型如何作为线性回归模型的一部分进行使用。我们使用高斯模型来比较不同组之间的差异。虽然这有时会以假设检验的方式进行框架化,但我们采取了另一种方法,将这一任务框架化为推断效应量的问题,我们通常认为这种方法更丰富、更有成效。我们还探讨了不同的效应量解释和报告方式。
通过本章及上一章的学习,我们已经准备好学习本书中最重要的概念之一——层级模型。那将是下一章的主题。
2.10 练习
-
使用 PyMC,将
our_first_model中 Beta 分布的先验参数更改为与上一章相匹配的参数。将结果与上一章进行比较。 -
将模型
our_first_model与先前的θ ∼ Beta(1,1)的模型进行比较,再与具有先验θ ∼(0,1)的模型进行比较。后验分布相似还是不同?采样是更慢、更快还是相同?如果使用不同区间(如[-1, 2])的均匀分布会怎样?模型能运行吗?会遇到什么错误?
-
PyMC 有一个函数
pm.model_to_graphviz,可以用来可视化模型。使用它来可视化our_first_model模型。将结果与 Kruschke 图进行比较。使用pm.model_to_graphviz来可视化模型comparing_groups。 -
阅读关于 PyMC 文档中煤矿灾难模型的内容(
shorturl.at/hyCX2)。尝试自己实现并运行这个模型。 -
修改
model_g,将均值的先验改为以经验均值为中心的高斯分布,并尝试一些合理的标准差值来调整这个先验。推断对这些变化的稳健性/敏感度如何?你认为使用高斯分布(一种无界分布,范围从-∞到∞)来建模像这样的有界数据(数据在 0 到 100 之间)怎么样?记住我们曾说过,不可能观察到低于 0 或高于 100 的值。 -
使用
chemical_shifts.csv文件中的数据,计算有无异常值的经验均值和标准差。将这些结果与使用高斯分布和 Student’s t 分布的贝叶斯估计结果进行比较。你观察到了什么? -
通过向
chemical_shifts.csv中添加更多异常值来重复上一个练习,并使用这些新数据为model_g和model_t计算新的后验分布。你观察到了什么? -
探索
idata_cg的 InferenceData 对象。-
它包含多少个组?
-
使用
sel方法检查特定日期参数μ的后验分布。 -
计算星期四和星期日之间均值差异的分布。结果的 DataArray 的坐标和维度是什么?
-
-
对于提示示例,直接从后验分布计算优势概率(无需先计算 Cohen’s d)。你可以使用
pm.sample_posterior_predictive()函数从每个组中采样。这与假设正态分布的计算结果有何不同?你能解释结果吗?
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,并与 5000 多名成员一起学习:packt.link/bayesian

第三章
分层模型
分层模型是一个非常棒的想法——让我们做更多这样的模型吧!- 贝叶斯建模的禅意
在第二章中,我们看到了一个小费的例子,其中数据中有多个组,每个组对应星期四、星期五、星期六和星期日。我们决定分别建模每个组。这样做有时是可以的,但我们应当注意我们的假设。通过独立建模每个组,我们假设这些组是互不相关的。换句话说,我们假设知道一天的小费信息并不能给我们提供其他一天的小费信息。这可能是一个过于强的假设。那么,是否有可能构建一个模型,允许我们在组之间共享信息呢?这不仅可能,而且正是本章的主要内容。幸运的是,你来了!
在本章中,我们将讨论以下主题:
-
分层模型
-
部分合并
-
收缩
3.1 信息共享,共享先验
分层模型也被称为多级模型、混合效应模型、随机效应模型或嵌套模型。当数据可以被描述为分组或具有不同层级时,分层模型尤其有用,比如数据嵌套在地理区域中(例如,属于一个省的城市和属于一个国家的省),或者具有分层结构(如学生嵌套在学校内,或者病人嵌套在医院中),或者是对同一个体的重复测量。

图 3.1:合并模型、非合并模型和分层模型之间的差异
分层模型是一个自然的方式,用于在不同组之间共享信息。在分层模型中,先验分布的参数本身也会赋予一个先验分布。这些更高层次的先验通常被称为超先验;“超”在希腊语中意味着“在……之上”。拥有超先验使得模型可以在组之间共享信息,同时仍允许组之间存在差异。换句话说,我们可以认为先验分布的参数属于一个共同的参数群体。图 3.1 显示了一个图示,展示了合并模型(单一组)、非合并模型(所有分开的组)和分层模型(也称为部分合并模型)之间的高层次差异。
分层模型的概念看起来可能过于简单,几乎是微不足道的,但它有着深远的影响。因此,在本章的其余部分,我们将通过不同的例子来理解它们的含义。我相信这些例子不仅能帮助你更好地理解这一概念,还能说服你,它是一个非常有用的工具,能够应用于你自己的问题。
3.2 分层转变
蛋白质是由 20 种叫做氨基酸的单位组成的分子。每种氨基酸可以在蛋白质中出现 0 次或更多次。就像旋律是由一系列音符定义的,蛋白质则是由一系列氨基酸定义的。一些音符的变化可能导致旋律的微小变化,而其他音符的变化则可能导致完全不同的旋律。蛋白质中也有类似的情况。研究蛋白质的一种方法是使用核磁共振(与医学成像使用的技术相同)。这项技术使我们能够测量多种量,其中之一叫做化学位移。你可能还记得我们在第 2章中看过一个使用化学位移的例子。
假设我们想将一种理论的化学位移计算方法与实验观察结果进行比较,以评估该理论方法是否能再现实验值。幸运的是,有人已经做了实验并进行了理论计算,我们只需要将它们进行比较。以下数据集包含了一组蛋白质的化学位移值。如果你查看cs_data数据框,你会发现它有四列:
-
第一列是一个代码,用来识别蛋白质(你可以通过在
www.rcsb.org/输入该代码来获取有关该蛋白质的许多信息) -
第二列是氨基酸的名称(你可能会注意到这里只有 19 个独特的名称;该数据集中缺少一种氨基酸)
-
第三列包含了化学位移的理论值(使用量子方法计算得出)
-
第四列包含了实验值
现在我们有了数据,接下来应该怎么做?一个选项是采用经验差异并拟合一个高斯分布,或者也许是学生 t 分布。因为氨基酸是一类化学化合物,假设它们都是相同的,并为所有差异估算一个单一的高斯分布是有道理的。但你可能会认为,有 20 种不同类型的氨基酸,每种都有不同的化学性质,因此一个更好的选择是拟合 20 个独立的高斯分布。我们应该怎么做?
让我们花点时间思考一下,哪个选项是最好的。如果我们将所有数据合并,估算结果会更准确,但我们无法从单个组(氨基酸)中获取信息。相反,如果我们将它们作为独立的组处理,我们将获得更详细的分析,但准确性较差。我们应该怎么做?
当有疑问时,什么都行!(不确定这是否是你生活中好的普遍建议,但我喜欢这首歌 www.youtube.com/watch?v=1di09XZUlIw)。我们可以建立一个层级模型;通过这种方式,我们允许在组级别进行估算,但有一个限制,即它们都属于一个更大的组或人群。为了更好地理解这一点,我们来为化学位移数据建立一个层级模型。
为了查看非层次模型(非汇聚模型)和层次模型之间的区别,我们将构建两个模型。第一个模型本质上与第 2 章中的comparing_groups模型相同:
代码 3.1
with pm.Model(coords=coords) as cs_nh:
μ = pm.Normal('μ', mu=0, sigma=10, dims="aa")
σ = pm.HalfNormal('σ', sigma=10, dims="aa")
y = pm.Normal('y', mu=μ[idx], sigma=σ[idx], observed=diff)
idata_cs_nh = pm.sample()
现在,我们将构建模型的层次版本。我们添加了两个超先验,一个用于μ的均值,另一个用于μ的标准差。我们将σ留空,不添加超先验;换句话说,我们假设观察值与理论值之间的方差对于所有组都是相同的。这是一个建模选择,你可能会遇到一个问题,觉得这种假设不可接受,并认为有必要为σ添加一个超先验;你可以自由这样做:
代码 3.2
with pm.Model(coords=coords) as cs_h:
# hyper_priors
μ_mu = pm.Normal('μ_mu', mu=0, sigma=10)
μ_sd = pm.HalfNormal('μ_sd', 10)
# priors
μ = pm.Normal('μ', mu=μ_mu, sigma=μ_sd, dims="aa")
σ = pm.HalfNormal('σ', sigma=10, dims="aa")
# likelihood
y = pm.Normal('y', mu=μ[idx], sigma=σ[idx], observed=diff)
idata_cs_h = pm.sample()
图 3.2 显示了cs_h和cs_nh模型的图形表示。我们可以看到,cs_h多了一个层级,用于表示μ的超先验。

图 3.2:化学位移数据的非层次(左)和层次(右)模型的图形表示。每个子图都是通过pm.model_to_graphviz(.)函数生成的
我们将使用 ArviZ 的plot_forest函数来比较结果。我们可以将多个模型传递给此函数。当我们希望比较不同模型中参数的值时,这非常有用,比如当前示例中所展示的。在图 3.3中,我们有一个包含 40 个估计均值的图,每个氨基酸(共 20 个)对应两个模型中的一个。我们还展示了它们的 94% HDI 和四分位距(分布的中央 50%)。垂直虚线表示根据层次模型得到的全局均值。该值接近零,正如预期的那样,理论值与实验值相符。

图 3.3:层次和非层次模型的化学位移差异
该图的最相关部分是层次模型的估计值被拉向部分汇聚的均值,或者说它们相比于非汇聚的估计值被“收缩”。你还会注意到,这种效应在那些离均值较远的组(如PRO)中更加明显,而且不确定性与非层次模型的相当,甚至更小。估计值是部分汇聚的,因为我们为每个组都有一个估计,但各个组的估计值通过超先验相互约束。因此,我们得到了一种介于将所有化学位移放在一个组中和将每个氨基酸分为 20 个独立组之间的中间情况。各位女士、先生以及非二元性别流动者,这就是层次模型的美妙之处。
3.3 水质
假设我们想分析一个城市的水质,那么我们通过将城市划分为多个社区来进行采样。我们可能认为有两种选择来分析这些数据:
-
将每个社区作为一个独立实体进行研究
-
将所有数据汇总在一起,并将城市的水质估算为一个大的整体。
你可能已经注意到这里的模式。我们可以通过说我们获得了问题的更详细视图来证明第一个选项的合理性,否则如果我们对数据进行平均处理,问题可能会变得不可见或不那么明显。第二个选项的合理性可以通过说如果我们将数据汇总,我们就能获得更大的样本量,从而得到更准确的估计来证明。但是我们已经知道我们有第三个选项:我们可以做一个层次模型!
对于这个例子,我们将使用合成数据。我喜欢使用合成数据;它是理解事物的一个好方法。如果你不理解某个东西,就模拟它!合成数据有很多用途。在这里,我们将假设我们已经从同一城市的三个不同区域收集了水样,并测量了水中的铅含量;铅含量超过世界卫生组织建议值的样本标记为零,低于建议值的样本标记为一。这是一个非常简单的场景。在更现实的例子中,我们会有铅浓度的连续测量,并且可能会有更多的组。然而,针对我们当前的目的,这个例子足够揭示层次模型的细节。我们可以用以下代码生成合成数据:
代码 3.3
N_samples = [30, 30, 30]
G_samples = [18, 18, 18]
group_idx = np.repeat(np.arange(len(N_samples)), N_samples)
data = []
for i in range(0, len(N_samples)):
data.extend(np.repeat([1, 0], [G_samples[i], N_samples[i]-G_samples[i]]))
我们正在模拟一个实验,我们测量了三个组,每个组包含一定数量的样本;我们将每组的样本总数存储在N_samples列表中。使用G_samples列表,我们记录每组中良好水质样本的数量。其余的代码仅用于生成一个包含零和一的数据列表。
这个问题的模型与我们用于硬币问题的模型类似,除了两个重要特征:
-
我们定义了两个超先验,这些将影响 Beta 先验。
-
我们不是对参数α和β设置超先验,而是通过μ(均值)和ν(Beta 分布的浓度或精度)来定义 Beta 分布。精度类似于标准差的倒数;ν的值越大,Beta 分布就越集中。在统计符号中,我们的模型如下所示:

请注意,我们使用下标i来表示模型中某些参数在不同组之间的取值。通过 Kruschke 图(见图 3.4),我们可以看到新模型相比于图 1.14,多了一个额外的层级。还要注意,在这个模型中,我们是用μ和ν来参数化 Beta 先验分布,而不是使用α和β。这是贝叶斯统计中的常见做法,因为μ和ν比α和β更直观。

图 3.4:层次模型
让我们在 PyMC 中编写这个模型:
代码 3.4
with pm.Model() as model_h:
# hypyerpriors
μ = pm.Beta('μ', 1, 1)
ν = pm.HalfNormal('ν', 10)
# prior
*θ* = pm.Beta('*θ*', mu=μ, nu=ν, shape=len(N_samples))
# likelihood
y = pm.Bernoulli('y', p=*θ*[group_idx], observed=data)
idata_h = pm.sample()
3.4 收缩效应
为了展示层次模型的主要后果,我需要你的帮助,请参与一个简短的实验。我需要你打印并保存通过az.summary(idata_h)计算出的摘要。然后,我希望你在对合成数据做些小调整后,再运行模型两次。记得每次运行后保存摘要。总的来说,我们会进行三次运行:
-
一轮设置
G_samples的所有元素为 18 -
一轮设置
G_samples的所有元素为 3 -
最后一轮设置一个元素为 18,另外两个为 3
在继续之前,请花点时间思考一下这个实验的结果。重点关注每个实验中θ的估计均值。根据前两次运行的模型,你能预测第三种情况的结果吗?
如果我们把结果放在表格中,会得到大致如下的内容;记住,由于采样过程的随机性,可能会有小的变化:
| G_samples | Mean |
|---|---|
| 18, 18, 18 | 0.6, 0.6, 0.6 |
| 3, 3, 3 | 0.11, 0.11, 0.11 |
| 18, 3, 3 | 0.55, 0.13, 0.13 |
表格 3.1:样本数据和对应的均值
在第一行中,我们看到对于一个由 30 个样本中挑选出的 18 个好样本组成的数据集,我们得到的θ均值是 0.6;请记住,现在θ的均值是一个包含 3 个元素的向量,每个组对应一个元素。然后,在第二行中,只有 30 个样本中的 3 个是好样本,θ的均值为 0.11。这些结果并不令人惊讶;我们的估计值与经验均值几乎相同。有趣的部分出现在第三行。我们没有得到来自前两行的θ均值的混合结果,如 0.6、0.11 和 0.11,而是得到了不同的值,即 0.55、0.13 和 0.13。
到底发生了什么?我们是不是哪里出错了?并不是那样。我们看到的现象是估计值已经向共同均值收缩了。这是完全正常的;实际上,这只是我们模型的一个结果。通过使用超先验,我们是从数据中估计 Beta 先验分布的参数。每个组都在向其他组提供信息,同时每个组也通过其他组的估计结果来获得信息。
图 3.5 显示了将μ和ν的后验估计值代入 Beta 分布。换句话说,这是推断 Beta 先验分布的后验分布。

图 3.5: 推断 Beta 先验分布的后验分布
为什么收缩是可取的?因为它有助于更稳定的推断。从多个方面来看,这类似于我们在学生 t 分布和异常值中看到的情况;使用重尾分布能够使模型对偏离均值的数据点更具鲁棒性。引入超先验会导致模型更为保守,使其对单个组中极端值的反应更为迟缓。想象一下,不同邻里之间的样本量不同,有的较小,有的较大;样本量越小,出现虚假结果的可能性越大。在极端情况下,如果你在某个邻里只取一个样本,可能会碰到整个邻里唯一的老旧铅管,或者相反,唯一的 PVC 管。在一种情况下,你会高估差的质量,而在另一种情况下,你会低估它。在层级模型下,一个组的错误估计会通过其他组提供的信息得到缓解。更大的样本量也能起到类似的效果,但往往这种方法不可行。
收缩的程度取决于数据;数据量更多的组会比数据量较少的组更强烈地拉动其他组的估计值。如果几个组相似,而某一个组不同,那么相似的组会将它们的相似性传递给其他组,并强化共同的估计,同时将不同组的估计值向它们拉拢;这正是我们在之前的例子中看到的情况。超先验在调节收缩量方面也起着作用。如果我们对组级分布有可信的信息,我们可以有效地使用信息性先验分布将估计值收缩到一个合理的值。
收缩
在层级模型中,共享相同超先验的组实际上通过超先验共享信息。这导致了收缩现象,也就是,单独的估计值会向共同的均值收缩。通过部分汇总数据,我们将组视为一种在独立组和单一大组之间的中间状态。
没有什么能阻止我们只使用两个组来构建层级模型,但我们更倾向于使用多个组。直观来看,原因是收缩的过程就像假设每个组都是一个数据点,我们在组层面上估计标准差。一般而言,除非我们有强有力的先验信息来指导我们的估计,否则我们不太信任数据点过少的估计。层级模型也是如此。
3.5 层级模型逐层构建
各种数据结构有助于层次化描述,可以涵盖多个层级。例如,考虑职业足球(足球)运动员。与许多其他运动一样,运动员有不同的职位。我们可能有兴趣估算每个运动员、每个职位以及所有职业足球运动员的技能指标。这种层次化结构在许多其他领域也能找到:
-
医学研究:假设我们有兴趣估计不同药物治疗某种疾病的效果。我们可以根据患者的个人信息、疾病严重程度和其他相关因素对患者进行分类,并构建一个层次模型来估算每个子组的治愈或治疗成功的概率。然后,我们可以使用子组分布的参数来估算整个患者群体的治愈或治疗成功的总体概率。
-
环境科学:假设我们有兴趣估计某种污染物对特定生态系统的影响。我们可以对生态系统内的不同栖息地(例如河流、湖泊、森林、湿地)进行分类,并构建一个层次模型来估算每个栖息地内的污染物水平分布。然后,我们可以使用栖息地分布的参数来估算整个生态系统中污染物水平的总体分布。
-
市场研究:假设我们有兴趣了解不同地区消费者购买某一产品的行为。我们可以根据消费者的个人信息(例如年龄、性别、收入、教育)对其进行分类,并构建一个层次模型来估算每个子组的购买行为分布。然后,我们可以使用子组分布的参数来估算整个消费者群体的购买行为分布。
回到我们的足球运动员数据,我们收集了来自英超联赛、法甲联赛、德甲联赛、意甲联赛和西甲联赛的数据,数据覆盖了四年(2017 至 2020 年)。假设我们有兴趣了解每次射门的进球数。这通常是统计学家所说的成功率,我们可以通过二项模型来估算,模型中参数n是射门次数,观察值y是进球数。这样就剩下了一个未知的参数p。在之前的例子中,我们将此参数称为θ,并使用 Beta 分布对其建模。我们现在也会这样做,但采用层次化的方法。请参见图 3.6,它提供了整个模型的图形表示。

图 3.6:足球运动员示例的层次模型。注意,与之前的层次模型相比,我们增加了一个层级。
在我们的模型中,θ表示每个球员的成功率,因此它是一个大小为n_players的向量。我们使用 Beta 分布来建模θ。Beta 分布的超参数将是μ[p]和ν[p]向量,这些向量的大小为 4,代表我们数据集中四个位置(后卫DF、中场MF、前锋FW和门将GK)。我们需要正确地索引向量μ[p]和ν[p],以匹配球员的总数。最后,我们将有两个全局参数,μ和ν,表示职业足球运动员。
PyMC 模型在下面的代码块中定义。pm.Beta(’μ’, 1.7, 5.8)是在 PreliZ 的帮助下选择的先验,95%的质量分布在 0 到 0.5 之间。这是一个弱信息先验的示例,因为几乎没有疑问,0.5 的成功率是一个较高的值。体育统计数据研究得非常透彻,存在大量可用于定义更强先验的信息。对于这个示例,我们将使用这个先验。类似的理由也适用于先验pm.Gamma(’’, mu=125, sigma=50),我们定义其为最大熵 Gamma 先验,90%的质量分布在 50 到 200 之间:
代码 3.5
coords = {"pos": pos_codes}
with pm.Model(coords=coords) as model_football:
# Hyper parameters
μ = pm.Beta('μ', 1.7, 5.8)
ν = pm.Gamma('ν', mu=125, sigma=50)
# Parameters for positions
μ_p = pm.Beta('μ_p',
mu=μ,
nu=ν,
dims = "pos")
ν_p = pm.Gamma('ν_p', mu=125, sigma=50, dims="pos")
# Parameter for players
*θ* = pm.Beta('*θ*',
mu=μ_p[pos_idx],
nu=ν_p[pos_idx])
_ = pm.Binomial('gs', n=football.shots.values, p=*θ*,
observed=football.goals.values)
idata_football = pm.sample()
在图 3.7的顶部面板中,我们展示了全局参数μ的后验分布。后验分布接近于 0.1。这意味着,对于一名职业足球运动员(来自顶级联赛),进球的概率平均为 10%。这是一个合理的值,因为进球并非易事,并且我们没有区分位置,即我们考虑的是那些主要角色不是进球的球员。在中间面板中,我们展示了前锋位置的估计μ[p]值;如预期的那样,它高于全局参数μ。在底部面板中,我们展示了梅西的估计θ值,值为 0.17,高于全局参数μ和前锋位置μ[p]值。这也是可以预期的,因为梅西是世界上最优秀的足球运动员,他的主要角色是进球。

图 3.7:全局参数的后验分布(顶部)、前向位置的均值(中部)以及梅西的θ参数(底部)
图 3.8 展示了参数 μ[p] 的后验分布的森林图。正如我们已经看到的,前锋位置的后验分布集中在 0.13 附近,并且是四个位置中最高的。这是有道理的,因为前锋的角色是进球以及助攻。μ[p] 的最低值出现在守门员位置。这是预期的,因为守门员的主要角色是阻止对方进球,而不是进球。值得注意的是,不确定性非常高;这是因为我们数据集中进球的守门员数量非常少,准确来说只有三名。后卫和中场的位置的后验分布大致处于中间,且中场略高于后卫。我们可以解释为,中场的主要角色是既要防守也要进攻,因此进球的概率比后卫高,但低于前锋。

图 3.8:参数 μ 的后验分布
p,均值位置
你需要知道何时停止
我们可以根据需要创建具有任意层级的层级模型。但除非问题需要额外的结构,否则添加超过必要层级的层次不会提升模型或推断的质量。相反,我们会陷入超先验和超参数的迷网中,而无法为它们赋予任何有意义的解释。构建模型的目标是理解数据,因此,实用的模型通常是那些能够反映并利用数据结构的模型。
3.6 小结
本章介绍了本书中最重要的概念之一:层级模型。每当我们能在数据中识别出子群时,就可以构建层级模型。在这种情况下,我们不是将子群当作独立的实体,或忽略子群将它们视为单一群体,而是构建一个模型,在群体之间部分合并信息。部分合并的主要效果是,每个子群的估计会受到其他子群估计的影响。这种效应被称为收缩效应,一般来说,这是一个非常有用的技巧,通过让推断更加保守(因为每个子群通过将估计拉向自己来影响其他子群)和更具信息量,来帮助改进推断。我们在子群层面和群体层面都能获得估计。
借用 Python 禅意的表达方式,我们可以肯定地说,层次模型是一个 非常棒的主意,让我们做更多这样的事情! 在接下来的章节中,我们将继续构建层次模型,并学习如何使用它们来构建更好的模型。我们还将讨论层次模型与统计学和机器学习中普遍存在的过拟合/欠拟合问题的关系,内容将在第五章中进行探讨。在第十章中,我们将讨论在从层次模型中采样时可能遇到的一些技术问题,并探讨如何诊断和解决这些问题。
3.7 练习
-
用你自己的话解释以下概念,用两到三句话:
-
完全汇总
-
无汇总
-
部分汇总
-
-
重复我们在
model_h中进行的练习。这一次,不使用层次结构,使用一个简单的先验,比如 Beta(α = 1,β* = 1)。比较两种模型的结果。 -
创建一个层次化版本的 第二章 中的 tips 示例,通过在一周的天数之间进行部分汇总。将结果与没有层次结构时获得的结果进行比较。
-
对图 3.7中的每个子面板,添加一条表示每个层级的经验均值的参考线,即全局均值、前向均值和梅西的均值。比较经验值和后验均值。你观察到什么?
-
氨基酸通常被分为
极性、非极性、带电和特殊等类别。构建一个类似于cs_h的层次模型,但包括一个氨基酸类别的组效应。将结果与本章中获得的结果进行比较。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,与志同道合的人交流,并和超过 5000 名成员一起学习,链接地址:packt.link/bayesian

第四章
使用线性模型
在三百多年的科学历史中,唯一没有改变的或许只有一点:对简单的热爱。– 乔治·瓦根斯贝格
音乐——从古典作品到 The Ramones 的Sheena is a Punk Rocker,再到车库乐队的未被认可的热门歌曲以及皮亚佐拉的《自由探戈》——由重复出现的模式构成。相同的音阶、和弦组合、吉他即兴、旋律等反复出现,创造出一个美妙的音景,能够激发并调节人类可能体验到的所有情感。同样,统计学的世界也是建立在不断出现的模式上的,这些小的动机时不时地出现。在本章中,我们将关注其中一个最流行和有用的模式,即线性 模型(或者如果你愿意,可以叫做动机)。这是一个非常有用的模型,独立使用时效果显著,同时也是许多其他模型的基础。如果你曾经学过统计学课程,你可能听说过简单和多元线性回归、逻辑回归、方差分析(ANOVA)、协方差分析(ANCOVA)等方法。所有这些方法都是同一个基本模式——线性回归模型——的不同变体。
本章将涉及以下主题:
-
简单线性回归
-
负二项回归
-
稳健回归
-
逻辑回归
-
变量的方差
-
层次线性回归
-
多元线性回归
4.1 简单线性回归
我们在科学、工程和商业中遇到的许多问题都具有以下形式:我们有一个变量X,我们想要建模或预测一个变量Y。重要的是,这些变量是成对的,如{(x[1],y[1]),(x[2],y[2]),
,(x[n],y[n])}。在最简单的情况下,即简单线性回归中,X和Y都是一维连续随机变量。所谓连续,意味着该变量用实数表示。使用 NumPy,你将把这些变量表示为一维浮点数组。通常,人们称Y为因变量、预测变量或结果变量,X为自变量、预测因子或输入变量。
一些典型的线性回归模型应用场景如下:
-
模拟土壤盐分与作物生产力之间的关系。然后,回答一些问题,比如:这种关系是线性的吗?这种关系有多强?
-
找出各国平均巧克力消费量与该国诺贝尔奖得主数量之间的关系,并理解为何这种关系可能是虚假的。
-
通过使用本地天气报告中的太阳辐射,预测你家的燃气账单(用于取暖和烹饪)。这个预测有多准确?
在第二章中,我们看到了正态模型,我们定义它为:

线性回归的主要思想是通过添加一个预测变量X来扩展此模型,以估计均值μ:

该模型表示变量 X 与变量 Y 之间存在线性关系。但由于噪声项 σ 的存在,这种关系并不是确定性的。此外,该模型表示 Y 的均值是 X 的线性函数,具有截距 α 和斜率 β。截距告诉我们当 X = 0 时 Y 的值,斜率则告诉我们 X 每变化一个单位,Y 会发生多少变化。由于我们不知道 α、β 或 σ 的具体值,因此我们为它们设定了先验分布。
在为线性模型设定先验时,我们通常假设它们是独立的。这个假设极大简化了先验设置,因为这样我们只需要设置三个先验,而不是一个联合先验。至少原则上,α 和 β 可以取实数线上的任何值,因此通常使用正态分布作为它们的先验。而因为 σ 是一个正数,通常为其使用半正态分布或指数分布作为先验。
截距的值可以根据不同的问题和领域知识有很大的变化。对于我曾处理过的许多问题,α 通常围绕 0 中心,且标准差不超过 1,但这只是我在一小部分问题上的经验(几乎是轶事性质的),并不是可以轻易迁移到其他问题的经验。通常,预测斜率(β)的先验值可能更为容易。例如,我们可能知道斜率的符号:例如,我们期望变量体重与身高变量平均呈正相关。对于 σ,我们可以将其设置为与变量 Y 的值同量级的大值,例如是其标准差的两倍。我们应该小心不要使用观察数据来推测先验;通常,数据用来避免使用过于严格的先验是可以接受的。如果我们对参数知之甚少,那么确保我们的先验具有模糊性是合乎逻辑的。如果我们希望使用更具信息量的先验,那么不应从观察数据中获取这些信息,而应从我们的领域知识中获得。
扩展常规模型
线性回归模型是常规模型的扩展,其中均值是作为预测变量的线性函数计算的。
4.2 线性单车模型
我们现在对贝叶斯线性模型有了一个大致的了解。让我们通过一个例子来巩固这个概念。我们将从一个非常简单的例子开始:我们有一座城市的温度记录和租赁自行车的数量。我们想要建立温度和租赁自行车数量之间的关系模型。图 4.1 显示了来自 UCI 机器学习库的共享单车数据集这两个变量的散点图(archive.ics.uci.edu/ml/index.php)。

图 4.1:自行车共享数据集。摄氏温度与租用自行车数量的散点图
原始数据集包含 17,379 条记录,每条记录有 17 个变量。我们将只使用 359 条记录和两个变量,temperature(摄氏温度)和 rented(租用的自行车数量)。我们将使用 temperature 作为自变量(我们的 X),租用的自行车数量作为因变量(我们的 Y)。我们将使用以下模型:
代码 4.1
with pm.Model() as model_lb:
*α* = pm.Normal("*α*", mu=0, sigma=100)
*β* = pm.Normal("*β*", mu=0, sigma=10)
σ = pm.HalfCauchy("σ", 10)
μ = pm.Deterministic("μ", *α* + *β* * bikes.temperature)
y_pred = pm.Normal("y_pred", mu=μ, sigma=σ, observed=bikes.rented)
idata_lb = pm.sample()
花点时间逐行阅读代码,确保理解代码的含义。同时查看 图 4.2,以获得该模型的可视化表示。

图 4.2:自行车共享数据集的贝叶斯线性模型
正如我们之前所说,这类似于一个正态模型,但现在均值被建模为温度的线性函数。截距是 α,斜率是 β。噪声项是
,均值是 μ。这里唯一的新东西是 Deterministic 变量 μ。这个变量不是随机变量,而是一个确定性变量,它由截距、斜率和温度计算得出。我们需要指定这个变量,因为我们希望将其保存在 InferenceData 中以备后续使用。我们本可以写 μ = α + β * bikes.temperature,甚至可以写成 _ = pm.Normal('y_pred', mu=α + β * bikes.temperature, ...,模型会保持不变,但我们将无法将 μ 保存在 InferenceData 中。注意,μ 是一个与 bikes.temperature 长度相同的向量,它与数据集中的记录数相同。
4.2.1 解释后验均值
为了探索推断结果,我们将生成一个后验图,但会省略确定性变量 μ。我们这样做是因为如果不省略该变量,我们将得到很多图表,每个 temperature 值对应一个图表。我们可以通过将我们想要包含在图表中的变量名作为列表传递给 var_names 参数,或者像以下代码块中那样否定我们想要排除的变量:
代码 4.2
az.plot_posterior(idata_lb, var_names=['∼μ'])

图 4.3:自行车线性模型的后验图
从图 4.3中,我们可以看到α、β和σ的边际后验分布。如果我们只读取每个分布的均值,比如μ = 69 + 7.9X,通过这些信息我们可以得出,温度为 0 时租赁自行车的预期数量为 69 辆,每升高 1 度温度,租赁的自行车数量增加 7.9 辆。因此,当温度为 28 度时,我们预期租赁 278 辆自行车,即 69 + 7.9 ∗ 28 ≈ 278 辆。这是我们的预期值,但后验分布也告诉我们这个估计值的周围不确定性。例如,β的 94% HDI 为(6.1, 9.7),所以每升高 1 度温度,租赁的自行车数量可能增加 6 辆至约 10 辆。此外,即使我们忽略后验不确定性,只关注均值,我们仍然对租赁自行车数量有不确定性,因为我们有一个σ值为 170。因此,如果我们说温度为 28 度时我们预期租赁 278 辆自行车,我们也不应该感到惊讶,实际数量可能在 100 到 500 辆之间。
现在,让我们创建一些图表,帮助我们可视化这些参数的综合不确定性。我们从两个图开始,展示均值(见图 4.4)。这两个图都是温度作为自变量时租赁自行车数量的均值图。不同之处在于我们如何表示不确定性。我们展示了两种常见的表示方法。在左侧子面板中,我们从后验分布中抽取 50 个样本,并将它们作为单独的线条绘制。在右侧子面板中,我们则采用所有可用的后验样本来计算 94%的 HDI。

图 4.4:自行车线性模型的后验图
图 4.4中的图表传达的基本相同的信息,只是其中一个通过一组线条表示不确定性,另一个则通过阴影区域表示。请注意,如果你重复代码生成图表,你会得到不同的线条,因为我们正在从后验分布中采样。然而,阴影区域将保持不变,因为我们使用了所有可用的后验样本。如果我们进一步拟合模型,我们不仅会得到不同的线条,阴影区域也可能发生变化,并且不同运行之间的差异可能非常小;如果差异很大,可能需要增加抽样次数,或者模型和采样存在问题(有关指导,请参见第十章)。
不管怎样,为什么我们要展示两个略有不同的图,它们传达的是相同的信息呢?嗯,这是为了突出不同的方式来表示不确定性。哪种更好?像往常一样,这取决于具体的上下文。阴影区域是一个不错的选择;它非常常见,且计算和解释都很简单。除非有特定的原因需要展示单个后验样本,否则阴影区域可能是你首选的方式。但我们也许希望展示单个后验样本。例如,大多数线条可能覆盖某个区域,但我们得到一些斜率非常大的线条。阴影区域可能会掩盖这些信息。如果你在展示单个后验样本时,可能可以考虑将其做成动画,特别是当你在演示或视频中展示时(详见 Kale 等人 [2019]了解更多)。
向你展示图 4.4中的两个图的另一个原因是,你可以学习从后验中提取信息的不同方式。请注意接下来的代码块。为了清晰起见,我们省略了绘图代码,只展示核心计算:
代码 4.3
posterior = az.extract(idata_lb, num_samples=50)
x_plot = xr.DataArray(
np.linspace(bikes.temperature.min(), bikes.temperature.max(), 50),
dims="plot_id"
)
mean_line = posterior["*α*"].mean() + posterior["*β*"].mean() * x_plot
lines = posterior["*α*"] + posterior["*β*"] * x_plot
hdi_lines = az.hdi(idata_lb.posterior["μ"])
...
你可以看到在第一行中,我们使用了az.extract。这个函数将chain和draw维度堆叠到一个单一的sample维度中,这在后续处理时可能会很有用。此外,我们使用num_samples参数从后验中请求一个子样本。默认情况下,az.extract会作用于后验组。如果你想从另一个组提取信息,可以使用group参数。在第二行中,我们定义了一个叫做x_plot的 DataArray,包含从最小到最大观测温度的等间距值。创建 DataArray 的原因是能够在接下来的两行中使用 Xarray 的自动对齐功能。如果我们使用 NumPy 数组,则需要添加额外的维度,这通常会令人困惑。为了更好地理解我的意思,最好的方式是定义x_plot = np.linspace(bikes.temperature.min(), bikes.temperature.max())并尝试重新绘制图形。在代码的第三行,我们计算了后验中μ的均值,针对每个x_plot的值;在第四行,我们计算了μ的个别值。在这两行中,我们本可以使用posterior[’μ’],但我们显式地重写了线性模型。我们这样做的目的是希望能帮助你更好地理解线性模型。
4.2.2 解释后验预测
如果我们不仅仅对期望值(均值)感兴趣,而是想从预测的角度来思考,也就是说,从租用自行车的角度来看怎么办?嗯,为此,我们可以进行后验预测采样。在执行下一行代码后,idata_lb将被填充一个新的组,posterior_predictive,其中包含一个变量y_pred,表示租用自行车数量的后验预测分布。
代码 4.4
pm.sample_posterior_predictive(idata_lb, model=model_lb, extend_inferencedata=True)
图 4.5 中的黑线代表租赁自行车的均值。这与 图 4.4 中的情况相同。新增元素包括代表租赁自行车中心 50% 的深灰色带(分位数 0.25 和 0.75),以及代表中心 94% 的浅灰色带(分位数 0.03 和 0.97)。您可能注意到我们的模型预测了一个负数自行车数量,这是没有意义的。但仔细思考后,我们会发现这是预期的,因为在 model_lb 中我们使用了正态分布来描述似然。一个非常简陋的 修正 可以是将预测值剪切为低于 0 的值,但那样很丑陋。在接下来的部分,我们将看到我们可以轻松改进这个模型,以避免不合理的预测。

图 4.5:自行车线性模型的后验预测图
4.3 泛化线性模型
我们一直在使用的线性模型是更一般模型的特例,即广义线性模型(GLM)。GLM 是线性模型的泛化,允许我们使用不同的分布来描述似然。在高层次上,我们可以将贝叶斯 GLM 写成:

是任意分布;一些常见情况包括正态分布、学生 t 分布、伽马分布和负二项分布。θ 表示分布可能具有的任何 辅助 参数,例如正态分布中的 σ。我们还有 f,通常称为反向链接函数。当
是正态分布时,f 是恒等函数。对于伽马分布和负二项分布等分布,f 通常是指数函数。为什么我们需要 f?因为线性模型通常位于实数线上,但 μ 参数(或其等价物)可能在不同的定义域上。例如,负二项分布的 μ 定义为正值,因此我们需要对 μ 进行变换。指数函数是这种变换的一个好选择。我们将在本书中探讨几种 GLM。在阅读本书时,一个很好的练习是创建一个表格,每次看到新的 GLM 时,添加一行说明 phi、theta 和 f 是什么,以及关于何时使用该 GLM 的一些注释。好的,让我们从我们第一个具体的 GLM 示例开始。
4.4 计数自行车
如何改进 model_lb 以更好地适应自行车数据?需要注意两点:租赁自行车数量是离散的,并且其下界为 0。这通常被称为计数数据,指的是通过计数某物得出的数据。计数数据有时使用连续分布(如正态分布)来建模,特别是当计数数量较大时。但通常使用离散分布更为合适。两种常见的选择是泊松分布和 NegativeBinomial 分布。主要的区别是,对于泊松分布,均值和方差是相同的,但如果这不成立或甚至大致不成立,那么 NegativeBinomial 可能是一个更好的选择,因为它允许均值和方差不同。如果不确定,可以同时拟合泊松分布和 NegativeBinomial 分布,看看哪个模型更好。我们将在 第五章 中进行这一操作。但目前,我们将使用 NegativeBinomial 模型。
代码 4.5
with pm.Model() as model_neg:
*α* = pm.Normal("*α*", mu=0, sigma=1)
*β* = pm.Normal("*β*", mu=0, sigma=10)
σ = pm.HalfNormal("σ", 10)
μ = pm.Deterministic("μ", pm.math.exp(*α* + *β* * bikes.temperature))
y_pred = pm.NegativeBinomial("y_pred", mu=μ, alpha=σ, observed=bikes.rented)
idata_neg = pm.sample()
idata_neg.extend(pm.sample_posterior_predictive(idata_neg))
PyMC 模型与之前的模型非常相似,但有两个主要区别。首先,我们使用 pm.NegativeBinomial 代替 pm.Normal 作为似然函数。NegativeBinomial 分布有两个参数:均值 μ 和离散参数 α。NegativeBinomial 的方差为 μ +
,因此 α 的值越大,方差越大。第二个区别是,μ 现在是 pm.math.exp(α + β * bikes.temperature),而不是简单的 α + β * bikes.temperature,正如我们之前解释的那样,这需要将实数线转换为正的区间。
model_neg 的后验预测分布显示在 图 4.6 中。后验预测分布与我们在使用线性模型时获得的分布非常相似(图 4.5)。主要的区别是,现在我们不再预测负数的租赁自行车数量!我们还可以看到,预测的方差随着均值的增加而增大。这是预期之中的,因为 NegativeBinomial 的方差为 μ +
。

图 4.6:自行车 NegativeBinomial 线性模型的后验预测图
图 4.7 显示了 model_lb 的后验预测检验(左侧)和 model_neg 的后验预测检验(右侧)。我们可以看到,当使用正态分布时,最大的偏差是模型预测出租赁自行车数量为负数,但即使在正值范围内,我们也能看到拟合效果不太好。另一方面,NegativeBinomial 模型似乎更适合,尽管它并不完美。看右尾:预测值的尾部比观测值重。但也注意到,这种非常高的需求的概率较低。因此,总的来说,我们可以重新表述为,NegativeBinomial 模型比正态分布模型更好。

图 4.7:自行车线性模型的后验预测检验
4.5 稳健回归
我曾经运行过一个复杂的分子系统模拟。在每一步模拟中,我都需要进行线性回归拟合作为中间步骤。我有理论和经验上的理由认为,在给定 X 值的情况下,我的 Y 值是条件正态分布的,所以我决定使用简单的线性回归来解决。但有时,模拟会生成一些远高于或低于数据主群的 Y 值。这完全破坏了我的模拟,我不得不重新启动它。
通常,这些与数据主群非常不同的值被称为异常值。我的模拟失败的原因是这些异常值拉扯回归线远离数据主群,而当我将这个估计传递到模拟的下一步时,事情就停止了。我通过我们亲爱的朋友——学生 t 分布解决了这个问题,正如我们在第二章中看到的,学生 t 分布比正态分布有更重的尾部。这意味着异常值对回归线的影响较小。这就是稳健回归的一个例子。
为了举例说明学生 t 分布为线性回归带来的稳健性,我们将使用一个非常简单且有趣的数据集:Anscombe 四重奏中的第三组数据。如果你不知道 Anscombe 四重奏是什么,可以在维基百科查看( en.wikipedia.org/wiki/Anscombe%27s_quartet )。
在接下来的模型model_t中,我们使用了一个移位的指数分布来避免接近 0 的值。未移位的指数分布对接近 0 的值赋予过多权重。根据我的经验,这对没有异常值或异常值适中的数据来说是可以的,但对于有极端异常值(或包含少量集群点)的数据,如 Anscombe 的第三组数据,最好避免这种低值。请对这一点,以及其他先前的建议持谨慎态度。默认值是一个不错的起点,但没有必要死守它们。其他常见的先验包括 Gamma(2, 0.1)和 Gamma(mu=20, sigma=15),它们与 Exponential(1/30)相似,但更少有接近 0 的值:
代码 4.6
with pm.Model() as model_t:
*α* = pm.Normal("*α*", mu=ans.y.mean(), sigma=1)
*β* = pm.Normal("*β*", mu=0, sigma=1)
σ = pm.HalfNormal("σ", 5)
ν_ = pm.Exponential("ν_", 1 / 29)
ν = pm.Deterministic("ν", ν_ + 1)
μ = pm.Deterministic("μ", *α* + *β* * ans.x)
_ = pm.StudentT("y_pred", mu=μ, sigma=σ, nu=ν, observed=ans.y)
idata_t = pm.sample(2000)
在图 4.8中,我们可以看到根据model_t的稳健拟合和根据 SciPy 的linregress(此函数执行最小二乘回归)的非稳健拟合。

图 4.8:根据model_t的稳健回归
虽然非稳健拟合试图妥协并包含所有点,但稳健贝叶斯模型model_t自动丢弃一个点,并拟合一条通过所有剩余点更接近的直线。我知道这是一个非常特殊的数据集,但其信息与其他数据集的结论相同;由于学生 t 分布的尾部更重,它对远离数据主群的点赋予较小的权重。
从图 4.9中,我们可以看到对于大部分数据,我们得到了一个非常好的匹配。同时,注意到我们的模型预测了远离大多数数据的值,向两侧扩展,而不仅仅是像观察到的数据那样在大部分数据上方。就我们目前的目的而言,这个模型表现得相当不错,不需要进一步修改。然而,注意到对于某些问题,我们可能希望避免这种情况。在这种情况下,我们可能需要回过头来修改模型,使用截断的学生 t 分布将y_pred的可能值限制为正值。这部分留给读者作为练习。

图 4.9:model_t的后验预测检验
4.6 逻辑回归
逻辑回归模型是线性回归模型的推广,我们可以在响应变量为二元时使用该模型。该模型使用逻辑函数作为逆链接函数。在我们继续讨论模型之前,让我们先熟悉一下这个函数:

对我们而言,逻辑函数的关键特性是,无论其自变量z的值如何,结果总是一个位于[0-1]区间的数字。因此,我们可以将这个函数看作是将通过线性模型计算得出的值压缩成可以输入伯努利分布的值的一种便捷方式。由于其特有的 S 形状,这个逻辑函数也被称为 sigmoid 函数,正如我们从图 4.10中看到的那样。

图 4.10:逻辑函数
4.6.1 逻辑模型
我们几乎具备了将一个简单的线性回归转换为一个简单的逻辑回归所需的所有元素。我们从只有两个类别的情况开始,例如,垃圾邮件/非垃圾邮件、安全/不安全、多云/晴天、健康/生病,或热狗/非热狗。首先,我们通过声明预测变量y只能取两个值,即 0 或 1 来对这些类别进行编码,即y ∈{0,*1}。
从这个角度描述,问题听起来非常像我们在前几章使用的掷硬币问题。我们可能记得我们使用了伯努利分布作为似然函数。与掷硬币问题的不同之处在于,现在θ不会从 beta 分布生成,而是通过一个线性模型来定义,使用逻辑函数作为逆链接函数。省略先验分布后,我们有:

我们将对经典的鸢尾花数据集应用逻辑回归,该数据集包含来自三种密切相关物种的花卉测量数据:setosa、virginica 和 versicolor。这些测量数据包括花瓣长度、花瓣宽度、萼片长度和萼片宽度。如果你想知道,萼片是经过改良的叶子,通常与保护花朵在花蕾中的功能有关。
我们将从一个简单的案例开始。假设我们只有两个类别,setosa 和 versicolor,并且只有一个独立变量或特征,sepal_length。我们希望根据花萼长度预测一朵花是 setosa 的概率。
如同常见的做法,我们将使用数字0和1对setosa和versicolor类别进行编码。使用 pandas,我们可以执行以下操作:
代码 4.7
df = iris.query("species == ('setosa', 'versicolor')")
y_0 = pd.Categorical(df["species"]).codes
x_n = "sepal_length"
x_0 = df[x_n].values
x_c = x_0 - x_0.mean()
与其他线性模型一样,中心化数据有助于采样。现在我们已经将数据转换为正确的格式,接下来我们可以使用 PyMC 构建模型:
代码 4.8
with pm.Model() as model_lrs:
*α* = pm.Normal("*α*", mu=0, sigma=1)
*β* = pm.Normal("*β*", mu=0, sigma=5)
μ = *α* + x_c * *β*
*θ* = pm.Deterministic("*θ*", pm.math.sigmoid(μ))
bd = pm.Deterministic("bd", -*α* / *β*)
yl = pm.Bernoulli("yl", p=*θ*, observed=y_0)
idata_lrs = pm.sample()
model_lrs有两个确定性变量:θ和bd。θ是将逻辑函数应用于变量μ的结果。bd是边界决策值,我们使用这个值来区分类别。我们稍后会详细讨论这一点。另一个值得注意的地方是,我们并没有自己编写逻辑函数,而是使用了 PyMC 提供的pm.math.sigmoid函数。
图 4.11 显示了model_lrs的结果:

图 4.11:逻辑回归,model_lrs的结果
图 4.11 显示了花萼长度与为 versicolor 的概率θ(如果需要,也可以是为 setosa 的概率,1 − θ)的关系。我们对二元响应添加了一些抖动(噪音),以避免数据点重叠。黑色的 S 形线表示θ的平均值。这条线可以解释为在已知花萼长度的情况下,一朵花为 versicolor 的概率。半透明的 S 形带表示 94%的 HDI。垂直线又代表什么呢?这将是下一节的主题。
4.6.2 使用逻辑回归进行分类
我的母亲做了一道美味的菜叫做 sopa seca,基本上是一道以意大利面为主的菜肴,字面意思是“干汤”。虽然听起来可能像是个误称,甚至是个矛盾修饰法,但当你了解它的做法时,这道菜的名字就完全合理了(你可以在本书的 GitHub 仓库中查看这个食谱:github.com/aloctavodia/BAP3)。类似的事情也发生在逻辑回归中,尽管它的名字如此,但通常被当作一种解决分类问题的方法。让我们看看这种二元性的来源。
回归问题是关于根据一个或多个输入变量的值来预测输出变量的连续值。我们已经见过许多回归的例子,包括逻辑回归。然而,逻辑回归通常是以分类的形式讨论的。分类涉及根据一些输入变量为输出变量分配离散值(代表一个类别,比如 versicolor),例如,根据花萼长度判断一朵花是 versicolor 还是 setosa。
那么,逻辑回归是回归方法还是分类方法呢?答案是,它是一种回归方法;我们回归的是属于某个类别的概率,但它也可以用于分类。我们需要的只是一个决策规则:例如,如果θ ≥ 0.5,则将样本归为versicolor类,否则归为setosa类。图 4.11中的垂直线是边界决策,它被定义为使得 versicolor 的概率等于 0.5 时独立变量的值。我们可以通过分析计算出这个值,它等于−
。这个计算基于模型的定义:

根据逻辑函数的定义,当α + β**x = 0 时,θ = 0.5。

通过重新排列,我们发现使得θ = 0.5 的x值是−
。
因为我们对α和β的值存在不确定性,所以我们对于边界决策的值也存在不确定性。这种不确定性在图 4.11中以垂直(灰色)带的形式表示,范围从≈5.3 到≈5.6。如果我们根据花萼长度进行花卉的自动分类(或任何可以在此模型框架下描述的类似问题),我们可以将花萼长度小于 5.3 的花归为 setosa 类,将花萼长度大于 5.6 的花归为 versicolor 类。对于花萼长度在 5.3 到 5.6 之间的花,我们将对其类别感到不确定,因此可以随机分配它们的类别,或者使用其他信息做出决策,包括让人类检查这些花卉。
总结本节内容:
-
θ的值通常来说是P(Y = 1|X)。从这个角度来看,逻辑回归是真正的回归方法;关键细节是,我们正在回归一个数据点属于类别 1 的概率,前提是给定特征的线性组合。
-
我们正在建模一个二元变量的均值,它是[0-1]区间中的一个数字。因此,如果我们想将逻辑回归用于分类,我们需要引入一个规则,将这个概率转换为二分类赋值。例如,如果P(Y = 1) > 0.5,我们将该观测值分配给类别 1,否则分配给类别 0。
-
值 0.5 并没有什么特别之处,除了它是 0 和 1 之间的中间值。当我们可以接受将数据点错误分类到任一方向时,这个边界是可以解释的。但这并不总是如此,因为错误分类的成本不一定是对称的。例如,如果我们尝试预测一个病人是否患有某种疾病,我们可能希望使用一个边界来最小化假阴性(患病但我们预测他们没有)或假阳性(未患病但我们预测他们患病)的数量。我们将在下一节中更详细地讨论这个问题。
4.6.3 解释逻辑回归的系数
在解释逻辑回归的系数时,我们必须小心。与简单线性模型不同,解释并不是那么直接。使用逻辑逆链接函数引入了一个非线性因素,我们必须考虑到这一点。如果β为正,则增加x会使p(y = 1)增加一定量,但这个量不是x的线性函数。相反,依赖关系是x值的非线性函数,这意味着x对p(y = 1)的影响取决于x的值。我们可以在图 4.11中直观地展示这一点。我们不再得到一条常数斜率的直线,而是得到一条 S 形的曲线,斜率随x的变化而变化。
一些代数可以帮助我们进一步理解p(y = 1)随着x的变化有多少变化。基本的逻辑斯蒂模型是:

逻辑回归的逆函数是 logit 函数,公式为:

结合这两个表达式,我们得到:

记住,在我们的模型中,θ是p(y = 1),所以我们可以将之前的表达式改写为:

这一量被称为p = 1 的赔率。如果我们把p = 1 看作是成功,那么成功的赔率就是成功的概率与失败的概率之比。例如,掷一个公平的骰子得到 2 的概率是
,而得到 2 的赔率是
=
= 0.2. 换句话说,每五次不成功的事件中就有一次成功事件。赔率通常被赌博者使用,因为它们比原始概率提供了更直观的投注思考工具。图 4.12展示了概率、赔率和 log-odds 之间的关系。

图 4.12:概率、赔率和 log-odds 的关系
解释逻辑回归
在逻辑回归中,β系数(斜率)表示x变量增加一个单位时,log-odds 单位的增加。
从概率到赔率的转换是单调变化,意味着随着概率的增加,赔率也会增加,反之亦然。虽然概率限制在[0,1]区间内,但赔率位于 0,∞)区间内。对数是另一种单调变化,而对数赔率则位于(−∞,∞)区间内。
4.7 变量方差
我们一直使用线性模式来建模分布的均值,并且在上一节中,我们用它来建模交互效应。在统计学中,当误差的方差在所有观察值中不恒定时,我们称线性回归模型呈现异方差性。在这种情况下,我们可能需要考虑将方差(或标准差)作为因变量的(线性)函数。
世界卫生组织及其他全球卫生机构收集新生儿和幼儿的数据,并设计生长曲线标准。这些图表是儿科工具包的重要组成部分,也是衡量人群整体健康状况的标准,能够帮助制定健康相关政策、规划干预措施并监测其效果。这样的数据示例包括新生儿/幼儿女孩的身高(体长)与年龄(月龄)之间的关系:
代码 4.9
data = pd.read_csv("data/babies.csv")
data.plot.scatter("month", "length")
为了对这些数据进行建模,我们将引入三个我们之前未见过的元素:
-
σ现在是预测变量的线性函数。因此,我们新增了两个参数,γ和δ。这两个参数是均值线性模型中α和β的直接类比。
-
均值的线性模型是一个函数
图 4.13:左面板为model_vv的后验拟合。右面板是随着身长变化的均值估计方差。
现在我们已经拟合了模型,可能想使用模型来了解某个特定女孩的身高与分布的比较。回答这个问题的一种方法是询问模型关于 0.5 个月大婴儿的length变量的分布。我们可以通过从后验预测分布中进行采样,条件是身高为 0.5 来回答这个问题。使用 PyMC,我们可以通过采样pm.sample_posterior_predictive得到答案;唯一的问题是,默认情况下,这个函数会返回ỹ值,这些值是已经观察到的x值,即用于拟合模型的值。获取未观察值的预测值的最简单方法是定义一个MutableData变量(在这个例子中是x_shared),然后在采样后验预测分布之前更新这个变量的值,如下代码块所示:
代码 4.11
with model_vv:
pm.set_data({"x_shared": [0.5]})
ppc = pm.sample_posterior_predictive(idata_vv)
y_ppc = ppc.posterior_predictive["y_pred"].stack(sample=("chain", "draw"))
现在,我们可以绘制出 2 周大女孩的预期身长分布,并计算其他量,如该身长女孩的百分位(见图 4.14)。

图 4.14:0.5 个月时的身高预期分布。阴影区域表示 32%的累计质量
4.8 层次线性回归
在第三章中,我们学习了层次模型的基本概念,这是一个非常强大的概念,它允许我们建模复杂的数据结构。层次模型使我们能够处理组层次的推断以及组层次以上的估计。如我们所见,这可以通过包含超先验来实现。我们还展示了组之间可以通过使用共同的超先验来共享信息,这提供了收缩效应,有助于正则化估计。
我们可以将这些相同的概念应用到线性回归中,得到层次线性回归模型。在本节中,我们将通过两个例子来阐明这些概念在实际场景中的应用,第一个使用的是合成数据集,第二个使用的是pigs数据集。
对于第一个例子,我创建了八个相关的组,其中有一个组只有一个数据点。我们可以从图 4.15 中看到数据的表现。如果你想了解更多关于这些数据是如何生成的,请访问 GitHub 仓库 github.com/aloctavodia/BAP3。

图 4.15:层次线性回归示例的合成数据
首先,我们将拟合一个非层次模型:
代码 4.12
coords = {"group": ["A", "B", "C", "D", "E", "F", "G", "H"]}
with pm.Model(coords=coords) as unpooled_model:
*α* = pm.Normal("*α*", mu=0, sigma=10, dims="group")
*β* = pm.Normal("*β*", mu=0, sigma=10, dims="group")
σ = pm.HalfNormal("σ", 5)
_ = pm.Normal("y_pred", mu=*α*[idx] + *β*[idx] * x_m, sigma=σ, observed=y_m)
idata_up = pm.sample()
图 4.16 显示了参数α和β的后验估计值。

图 4.16:unpooled_model的α和β的后验分布
正如图 4.16所示,组H的估计值与其他组的估计值非常不同。这是预期中的情况,因为对于组H,我们只有一个数据点,也就是说我们没有足够的信息来拟合一条直线。我们至少需要两个数据点;否则,模型会过度参数化,这意味着我们有比数据所能确定的更多的参数。
为了克服这种情况,我们可以提供更多的信息;我们可以通过使用先验或向模型中添加更多结构来实现这一点。让我们通过构建一个分层模型来添加更多结构。
这是分层模型的 PyMC 模型:
代码 4.13
with pm.Model(coords=coords) as hierarchical_centered:
# hyperpriors
*α*_μ = pm.Normal("*α*_μ", mu=y_m.mean(), sigma=1)
*α*_σ = pm.HalfNormal("*α*_σ", 5)
*β*_μ = pm.Normal("*β*_μ", mu=0, sigma=1)
*β*_σ = pm.HalfNormal("*β*_σ", sigma=5)
# priors
*α* = pm.Normal("*α*", mu=*α*_μ, sigma=*α*_σ, dims="group")
*β* = pm.Normal("*β*", mu=*β*_μ, sigma=*β*_σ, dims="group")
σ = pm.HalfNormal("σ", 5)
_ = pm.Normal("y_pred", mu=*α*[idx] + *β*[idx] * x_m, sigma=σ, observed=y_m)
idata_cen = pm.sample()
如果你运行hierarchical_centered,你会看到 PyMC 显示一条信息,类似于调整后有 149 次发散。增加 target_accept 或重新参数化。 这条信息意味着 PyMC 生成的样本可能不可信。到目前为止,我们假设 PyMC 总是返回我们可以无问题使用的样本,但事实并非总是如此。在第十章中,我们将进一步讨论为什么会这样,并提供一些诊断方法,帮助你识别这些情况以及解决潜在问题的建议。在这一节中,我们也会解释什么是发散现象。现在,我们只想说,当使用分层线性模型时,我们通常会遇到很多发散现象。
解决这个问题的简单方法是增加target_accept,正如 PyMC 亲切地建议的那样。它是pm.sample()的一个参数,默认值为 0.8,最大值为 1。如果你看到有发散现象,可以将这个参数设置为 0.85、0.9,甚至更高值,这有助于解决问题。但如果你已经设置到 0.99,仍然有发散现象,那么这种简单的方法可能就不奏效了,你需要采取其他措施。这就是重新参数化。那么,什么是重新参数化呢?重新参数化就是以一种不同的方式编写模型,但这在数学上与原始模型是等价的:你并没有改变模型,只是以另一种方式表达它。许多模型,如果不是所有模型,都可以用其他方式来编写。有时,重新参数化可以提高采样器的效率或模型的可解释性。例如,通过重新参数化,你可以消除发散现象。接下来我们将在下一节中讲解如何做到这一点。
4.8.1 分层模型:集中式与非集中式
层次线性模型有两种常见的参数化方法,分别是中心化和非中心化。hierarchical_centered模型使用的是中心化方法。该参数化方法的特点是,我们直接为各个小组估计参数;例如,我们明确地估计每个小组的斜率。相反,对于非中心化参数化方法,我们为所有小组估计一个共同的斜率,然后为每个小组估计一个偏移量。需要注意的是,我们仍然是在为每个小组建模斜率,但相对于共同的斜率,所获得的信息是相同的,只是表达方式不同。因为模型胜过千言万语,我们来看看hierarchical_non_centered:
代码 4.14
with pm.Model(coords=coords) as hierarchical_non_centered:
# hyperpriors
*α*_μ = pm.Normal("*α*_μ", mu=y_m.mean(), sigma=1)
*α*_σ = pm.HalfNormal("*α*_σ", 5)
*β*_μ = pm.Normal("*β*_μ", mu=0, sigma=1)
*β*_σ = pm.HalfNormal("*β*_σ", sigma=5)
# priors
*α* = pm.Normal("*α*", mu=*α*_μ, sigma=*α*_σ, dims="group")
*β*_offset = pm.Normal("*β*_offset", mu=0, sigma=1, dims="group")
*β* = pm.Deterministic("*β*", *β*_μ + *β*_offset * *β*_σ, dims="group")
σ = pm.HalfNormal("σ", 5)
_ = pm.Normal("y_pred", mu=*α*[idx] + *β*[idx] * x_m, sigma=σ, observed=y_m)
idata_ncen = pm.sample(target_accept=0.85)
区别在于,对于模型hierarchical_centered,我们定义了β ∼ (β[μ],β**[σ]),而对于
hierarchical_non_centered,我们则定义了β = β[μ] + β[offset] * β[σ]。非中心化参数化更高效:当我运行模型时,只有 2 个发散点,而不是之前的 148 个。为了去除这些剩余的发散点,我们可能仍然需要增加target_accept。对于这个特定的案例,将其从 0.8 改为 0.85 效果非常好。要完全理解为什么这种重新参数化有效,你需要理解后验分布的几何结构,但这超出了本节的范围。别担心,我们会在第十章讨论这个问题。
现在我们的样本没有发散点了,我们可以回去分析后验分布。图 4.17展示了hierarchical_model中α和β的估计值。

图 4.17:hierarchical_non_centered模型中α和β的后验分布
小组H的估计值仍然具有较高的不确定性。但结果看起来比图 4.16中的要更合理;原因在于小组之间共享了信息。因此,即使我们没有足够的信息将一条线拟合到一个单一的点上,小组H也受到了其他小组的影响。实际上,所有小组都在相互影响。这就是层次模型的强大之处。
图 4.18展示了八个小组的拟合线。我们可以看到,尽管我们设法将一条直线拟合到一个单一的点上,乍一看这可能显得奇怪甚至可疑,但这只是层次模型结构的结果。每一条线都受到其他小组的影响,因此我们并非真正地将一条线拟合到一个单一的点,而是将一条已被其他小组数据所影响的线拟合到该点。

图 4.18:hierarchical_non_centered的拟合线
4.9 多元线性回归
到目前为止,我们一直在处理一个因变量和一个自变量。然而,拥有多个自变量并将它们纳入模型是很常见的。一些例子可能是:
-
葡萄酒的感知质量(因变量)和酸度、密度、酒精含量、残糖量、硫酸盐含量(自变量)
-
学生的平均成绩(因变量)和家庭收入、从家到学校的距离、母亲的教育水平(类别变量)
我们可以很容易地将简单线性回归模型扩展到处理多个自变量。我们称这种模型为多元线性回归,或者较少使用的名称是多变量线性回归(不要与多元回归线性回归混淆,后者是指我们有多个因变量的情况)。
在多元线性回归模型中,我们将因变量的均值建模如下:

使用线性代数符号,我们可以写出更简短的版本:

X是一个n × k大小的矩阵,包含自变量的值,β是一个k大小的向量,包含自变量的系数,n是观测值的数量。
如果你对线性代数有些生疏,可能需要查看维基百科关于两个向量的点积及其矩阵乘法推广的文章:en.wikipedia.org/wiki/Dot_product。基本上,你需要知道的是,我们只是在使用一种更简洁、方便的方式来书写我们的模型:

使用简单线性回归模型,我们找到了一条直线,能够(希望)解释我们的数据。而在多元线性回归模型下,我们得到的是一个维度为k的超平面。因此,多元线性回归模型本质上与简单线性回归模型相同,唯一的区别在于,现在β是一个向量,X是一个矩阵。
看一下下面的例子
对于多元线性回归模型,让我们回到自行车数据集。我们将使用当天的温度和湿度来预测租赁的自行车数量:
代码 4.15
with pm.Model() as model_mlb:
*α* = pm.Normal("*α*", mu=0, sigma=1)
*β*0 = pm.Normal("*β*0", mu=0, sigma=10)
*β*1 = pm.Normal("*β*1", mu=0, sigma=10)
σ = pm.HalfNormal("σ", 10)
μ = pm.Deterministic("μ", pm.math.exp(*α* + *β*0 * bikes.temperature +
*β*1 * bikes.hour))
_ = pm.NegativeBinomial("y_pred", mu=μ, alpha=σ, observed=bikes.rented)
idata_mlb = pm.sample()
请花一点时间对比model_mlb(有两个自变量:temperature和hour)和model_neg(只有一个自变量:temperature)。唯一的区别是现在我们有了两个β系数,每个自变量对应一个系数。模型的其余部分相同。注意,我们本来可以写作β= pm.Normal("β1", mu=0, sigma=10, shape=2),然后在定义μ时使用β1[0]和β1[1]。我通常会这么做。
如你所见,编写多元回归模型与编写简单回归模型并没有太大不同。不过,解释结果可能更加具有挑战性。例如,temperature的系数现在是β[0],而hour的系数是β[1]。我们仍然可以将系数解释为因变量在自变量变化一个单位时的变化量。但现在我们必须小心地指定我们正在讨论的是哪个自变量。例如,我们可以说,温度增加一个单位时,租赁自行车的数量增加β[0]个单位,同时保持hour的值不变。或者我们可以说,小时数增加一个单位时,租赁自行车的数量增加β[1]个单位,同时保持temperature的值不变。此外,某一变量的系数值取决于我们在模型中包含了哪些其他变量。例如,temperature的系数将根据是否将hour变量纳入模型而有所变化。
图 4.19 显示了模型model_neg(仅有temperature)和模型model_mld(temperature和hour)的β系数。

图 4.19:model_neg和model_mlb的缩放β系数
我们可以看到,temperature的系数在两个模型中是不同的。这是因为temperature对租赁自行车数量的影响取决于一天中的小时数。更进一步,β系数的值已经通过其对应的独立变量的标准差进行了缩放,因此我们可以使它们具有可比性。我们可以看到,一旦在模型中加入了hour,temperature对租赁自行车数量的影响变得更小。这是因为hour的影响已经解释了之前由temperature解释的部分租赁自行车数量的变化。在极端情况下,新增一个变量可能会使系数变为 0,甚至改变符号。我们将在下一章中进一步讨论这个问题。
4.10 总结
在本章中,我们学习了线性回归,旨在建立因变量与自变量之间的关系模型。我们了解了如何使用 PyMC 拟合线性回归模型,并如何解释结果和制作可以与不同受众分享的图表。
我们的第一个例子是一个具有高斯响应的模型。但随后我们看到这只是一个假设,我们可以轻松地将其更改为处理非高斯响应,例如使用负二项回归模型处理计数数据,或使用逻辑回归模型处理二元数据。我们还看到,当这样做时,我们还需要设置一个逆链接函数,将线性预测器映射到响应变量。使用 Student’s t 分布作为似然函数对于处理异常值很有用。我们花费了大部分章节将均值建模为自变量的线性函数,但我们了解到我们也可以建模其他参数,如方差。当我们有异方差数据时,这非常有用。我们学会了如何应用部分汇聚的概念来创建层次线性回归模型。最后,我们简要讨论了多元线性回归模型。
PyMC 通过修改一两行代码,使得实现各种贝叶斯线性回归变得非常简单。在下一章中,我们将学习更多关于线性回归的内容,并且我们将学习 Bambi,这是一个构建在 PyMC 之上的工具,它使得构建和分析线性回归模型变得更加容易。
4.11 练习
-
使用 howell 数据集(可在
github.com/aloctavodia/BAP3获取),创建一个体重(x)与身高(y)之间的线性模型。排除 18 岁以下的受试者。解释结果。 -
对于四个受试者,我们得到了体重(45.73, 65.8, 54.2, 32.59),但没有身高。使用前面的模型,预测每个受试者的身高,并给出他们的 50%和 94% HDI。提示:使用
pm.MutableData。 -
重复练习 1,这次包括 18 岁以下的受试者。解释结果。
-
已知许多物种的体重与身高不成比例,而是与体重的对数成比例。使用这一信息来拟合 howell 数据(包括所有年龄段的受试者)。
-
查看附带的代码
model_t2(以及与之相关的数据)。尝试不同的ν先验,例如未移位的指数分布和伽玛分布先验(它们在代码中有注释)。绘制先验分布,以确保你理解它们。一种简单的方法是调用pm.sample_prior_predictive()函数,而不是pm.sample()。你也可以使用 PreliZ。 -
使用
petal_length变量重新运行model_lrs,然后使用petal_width变量。结果有哪些主要区别?在每种情况下,94% HDI 的宽度是多大? -
重复前面的练习,这次使用 Student’s t 分布作为一个弱信息先验。尝试不同的ν值。
-
选择一个你感兴趣的数据集,并使用简单线性回归模型进行分析。一定要利用 ArviZ 函数探索结果。如果你没有找到感兴趣的数据集,可以尝试在线搜索,例如,在
data.worldbank.org或www.stat.ufl.edu/~winner/datasets.html查找。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,与志同道合的人们一起学习,并与超过 5000 名成员共同成长,链接地址:packt.link/bayesian

第五章
比较模型
地图不是它所代表的领土,但如果它是正确的,它具有与领土相似的结构。 – 阿尔弗雷德·科尔齐布斯基
模型应当作为近似值来设计,帮助我们理解一个特定问题或一类相关问题。模型并不是为了完美复制真实世界而设计的。因此,所有模型都在某种意义上是错误的,就像地图不是领土一样。但并非所有模型都是同样错误的;有些模型在描述某个特定问题时会比其他模型更好。
在前几章中,我们将注意力集中在推断问题上,也就是如何从数据中学习参数值。在本章中,我们将重点关注一个互补的问题:如何比较针对同一数据的两个或多个模型。正如我们将要学习的,这既是数据分析中的一个核心问题,也是一个棘手的问题。在本章中,我们将保持示例简单,以便专注于模型比较的技术细节。在接下来的章节中,我们将把在这里学到的知识应用于更复杂的例子。
在本章中,我们将探讨以下主题:
-
过拟合与欠拟合
-
信息准则
-
交叉验证
-
贝叶斯因子
5.1 后验预测检查
我们之前介绍并讨论了后验预测检查,作为评估模型如何解释用于拟合模型的数据的一种方法。这种测试的目的是不是确定模型是否错误;我们早就知道这一点了!这个过程的目标是理解我们如何捕捉数据。通过进行后验预测检查,我们旨在更好地理解模型的局限性。一旦我们理解了局限性,我们可以简单地承认它们,或者通过改进模型来尝试去除它们。预计模型无法重现问题的所有方面,这通常不是问题,因为模型是为了特定目的构建的。由于不同的模型通常捕捉数据的不同方面,我们可以通过后验预测检查来比较模型。
让我们看一个简单的例子。我们有一个包含两个变量x和y的数据集。我们将使用线性模型来拟合这些数据:

我们还将使用二次模型来拟合数据,即一个比线性模型多一个项的模型。对于这个额外的项,我们只需将x的平方加上一个β系数:

我们可以像往常一样在 PyMC 中编写这些模型;参考以下代码块。与我们之前见过的所有模型唯一的不同之处在于,我们向pm.sample传递了idata_kwargs="log_likelihood": True这个参数。这个额外的步骤将把对数似然值存储在InferenceData对象中,我们稍后将使用这个信息:
代码 5.1
with pm.Model() as model_l:
*α* = pm.Normal("*α*", mu=0, sigma=1)
*β* = pm.Normal("*β*", mu=0, sigma=10)
σ = pm.HalfNormal("σ", 5)
μ = *α* + *β* * x_c[0]
y_pred = pm.Normal("y_pred", mu=μ, sigma=σ, observed=y_c)
idata_l = pm.sample(2000, idata_kwargs={"log_likelihood": True})
idata_l.extend(pm.sample_posterior_predictive(idata_l))
with pm.Model() as model_q:
*α* = pm.Normal("*α*", mu=0, sigma=1)
*β* = pm.Normal("*β*", mu=0, sigma=10, shape=order)
σ = pm.HalfNormal("σ", 5)
μ = *α* + pm.math.dot(*β*, x_c)
y_pred = pm.Normal("y_pred", mu=μ, sigma=σ, observed=y_c)
idata_q = pm.sample(2000, idata_kwargs={"log_likelihood": True})
idata_q.extend(pm.sample_posterior_predictive(idata_q))
图 5.1 显示了两个模型的均值拟合。从视觉上看,两个模型似乎都对数据提供了合理的拟合。至少对我来说,要看出哪个模型最好并不那么容易。你怎么看?

图 5.1:model_l(线性模型)和model_q(二次模型)的均值拟合
为了获得更多的见解,我们可以进行后验预测检查。图 5.2 显示了观察值和预测值的数据的 KDE(核密度估计)。在这里,很容易看出model_q,即二次模型,更好地拟合了数据。我们还可以看到,特别是在分布的尾部,有很多不确定性。这是因为我们数据点的数量很少。

图 5.2:使用az.plot_ppc函数创建的model_l和model_q的后验预测检查
后验预测检查是一个非常灵活的概念。我们可以通过许多方式比较观察值和预测值。例如,我们可以比较分布的密度,而不是直接比较密度,我们还可以比较总结统计量。在图 5.3的顶部面板中,我们展示了两个模型的均值分布。x 轴上的点表示观察值。我们可以看到,两个模型都很好地捕捉了均值,二次模型的方差更小。两个模型都能够很好地捕捉均值并不令人惊讶,因为我们显式地建模了均值。在底部面板中,我们展示了四分位间距的分布。这一比较则偏向于线性模型。

图 5.3:使用az.plot_bpv函数创建的model_l和model_q的后验预测检查
通常,与模型明确建模的内容正交的统计量对于评估模型来说会更具信息量。如果有疑问,评估多个统计量可能会更方便。一个有用的问题是,问问自己你对数据的哪些方面感兴趣。
为了生成图 5.3,我们使用了az.plot_bpv ArviZ 函数。生成该图的完整代码片段如下:
代码 5.2
idatas = [idata_l, idata_q]
def iqr(x, a=-1):
"""interquartile range"""
return np.subtract(*np.percentile(x, [75, 25], axis=a))
for idata in idatas:
az.plot_bpv(idata, kind="t_stat", t_stat="mean", ax=axes[0])
for idata in idatas:
az.plot_bpv(idata, kind="t_stat", t_stat=iqr, ax=axes[1])
请注意,我们使用了kind="t_stat"参数来指示我们将使用总结统计量。我们可以传递一个字符串,比如t_stat="mean",表示我们想要使用均值作为总结统计量。或者,我们也可以使用用户定义的函数,比如t_stat=iqr。
你可能已经注意到,图 5.3 也包含了带有 bpv 值的图例。bpv 代表贝叶斯 p 值。这是一种以数值方式总结模拟数据和观察数据之间比较的方法。为了获得它们,选择一个汇总统计量 T,比如均值、中位数、标准差,或者你认为值得比较的任何东西。然后,计算观察数据 T[obs] 和模拟数据 T[sim] 的 T。最后,我们问自己这个问题:“T[sim] 小于或等于 T[obs] 的概率是多少?”如果观察值与预测值一致,那么期望值将为 0.5。换句话说,一半的预测值将低于观察值,一半将高于观察值。这个量被称为 贝叶斯 p 值:

还有另一种计算贝叶斯 p 值的方法。我们可以使用整个分布,而不是使用汇总统计量。在这种情况下,我们可以问自己这个问题:“对于每个观察值,预测一个更小或相等的值的概率是多少?”。如果模型被很好地校准,那么这些概率应该对于所有观察值是相同的。因为模型能够同样好地捕捉所有观察值,所以我们应该期待一个均匀分布。ArviZ 可以帮助我们进行计算;这次我们需要使用带有 kind="p_value" 参数(这是默认值)的 az.plot_bpv 函数。图 5.4 显示了此计算的结果。白色线条表示期望的均匀分布,灰色带状区域显示了由于样本的有限大小而预期的偏差。可以看出,这些模型非常相似。

图 5.4:通过 az.plot_bpv 函数创建的 model_l 和 model_q 的后验预测检验
那些不是 p 值
对于那些熟悉 p 值及其在频率统计中的使用的人来说,有一些需要澄清的地方。关于这些 p 值的“贝叶斯”之处在于,我们并没有使用抽样分布,而是使用了后验预测分布。此外,我们并没有进行原假设检验,也没有试图声明一个差异是“显著的”。我们只是试图量化模型如何解释数据。
后验预测检验提供了一个非常灵活的框架,用于评估和比较模型,无论是使用图形,还是使用诸如贝叶斯 p 值之类的数值摘要,或两者的组合。这个概念足够通用,允许分析人员发挥想象力,寻找不同的方式来探索模型的预测,并使用最适合其建模目标的方式。
在接下来的章节中,我们将探索其他比较模型的方法。这些新方法可以与后验预测检验结合使用。
5.2 简单性与准确性之间的平衡
在选择不同解释时,有一个原则被称为奥卡姆剃刀。一般来说,这个原则规定,当有两个或更多等效的解释时,最简单的解释是首选解释。简洁性的一种常见标准是模型中的参数数量。
这一启发式方法有很多理由支持。我们不会讨论它们的具体内容,我们只会将它们视为一种合理的指导原则。
我们在比较模型时通常需要考虑的另一个因素是它们的准确性,也就是模型对数据的拟合程度。根据这一标准,如果我们有两个(或更多)模型,其中一个比另一个更好地解释了数据,那么这个模型就是首选模型。
直观上,似乎在比较模型时,我们倾向于偏好那些最能拟合数据且简单的模型。但如果这两个原则导致我们选择不同的模型,我们该怎么做呢?或者更一般地说,是否有一种量化的方法来平衡这两者的贡献?简短的回答是有,而且实际上有不止一种方法可以做到。但首先,让我们看一个例子,以便获得直觉。
5.2.1 多个参数(可能)导致过拟合
图 5.5展示了三个参数数量逐渐增加的模型。第一个(零次)只是一个常数值:无论 X 的值是多少,模型总是预测相同的 Y 值。第二个模型(一阶)是一个线性模型,就像我们在第四章中看到的那样。最后一个模型(五次)是一个五次多项式模型。我们将在第六章中更深入地讨论多项式回归,但目前我们只需要知道,该模型的核心形式是 α + β[0]x + β[0]x² + β[0]x³ + β[0]x⁴ + β[0]x⁵。

图 5.5:简单数据集的三种模型
在图 5.5中,我们可以看到,随着模型复杂度(参数数量)的增加,模型的准确度也得到了提升,这一变化反映在决定系数 R² 中。这是一种衡量模型拟合度的方式(欲了解更多信息,请阅读en.wikipedia.org/wiki/Coefficient_of_determination)。事实上,我们可以看到五次多项式完美地拟合了数据,得到了 R² = 1。
为什么五次多项式可以在没有误差的情况下拟合数据呢?原因在于我们有与数据相同数量的参数,也就是六个。因此,模型实际上只是作为一种表达数据的替代方式。模型并没有学习数据的模式,而是在记忆数据!这可能是个问题。察觉这一点的更简单方式是,思考当面对新的、未见过的数据时,记住数据的模型会发生什么。你认为会发生什么?
好吧,性能预期会很差,就像某人只是背了考试问题,却发现问题在最后一刻被更改了!这种情况在图 5.6中有所展示;这里,我们添加了两个新数据点。也许我们有资金进行新的实验,或者我们的老板刚刚给我们发送了新的数据。我们可以看到,原本能够完美拟合数据的 5 阶模型,现在在R²的度量下比线性模型表现得更差。从这个简单的例子中,我们可以看出,最适合的模型不一定是理想的模型。

图 5.6:三个简单数据集的模型,外加两个新点
大致来说,当一个模型非常好地拟合了用于学习该模型参数的数据集,但在拟合新数据集时表现很差,我们就遇到了过拟合问题。这是分析数据时非常常见的一个问题。思考过拟合的一个有用方法是将数据集视为包含两个部分:信号和噪声。信号是我们希望从数据中捕捉到的(或学习到的)内容。如果我们使用某个数据集,那是因为我们认为其中有信号,否则这将是徒劳的。另一方面,噪声是没有用的,它是测量误差、数据生成或捕获方式的局限性、数据损坏等因素的产物。当模型过于灵活(对于一个数据集)到能够学习噪声时,就会发生过拟合。这导致信号被隐藏起来。
这是奥卡姆剃刀的一个实际论据,同时也是一个警告:至少在原则上,总是可以创建一个复杂到足以解释数据集中所有细节的模型,甚至是最不相关的细节——就像博尔赫斯故事中的制图师,他们制作了一张与帝国一样庞大的地图,完美复制了每一个细节。
5.2.2 参数过少导致欠拟合
继续使用相同的例子,但从复杂度的另一极来看,我们得到了 0 阶模型。这个模型仅仅是一个伪装成线性模型的高斯分布。这个模型只能捕捉到Y的均值,因此完全不关心X的值。我们说这个模型对数据进行了欠拟合。欠拟合的模型也可能具有误导性,尤其是在我们没有意识到这一点时。
5.3 预测准确性的度量
“一切应当尽可能简化,但不能简化得过度”是一句常被归因于爱因斯坦的话。就像健康饮食一样,建模时我们也需要保持平衡。理想情况下,我们希望有一个既不过度拟合也不欠拟合数据的模型。我们希望在简洁性和拟合优度之间找到某种平衡。
在前面的例子中,相对容易看出,0 阶模型过于简单,而 5 阶模型过于复杂。为了得到一个通用的方法,使我们能够对模型进行排序,我们需要将这种简单性和准确性之间的平衡形式化。
让我们来看几个对我们有用的术语:
-
样本内准确度:使用与拟合模型相同的数据来衡量的准确度。
-
样本外准确度:使用未用于拟合模型的数据来衡量的准确度。
样本内准确度的平均值通常会大于样本外准确度。这就是为什么一般来说,使用样本内准确度来评估模型会让我们误以为模型比实际更好。因此,使用样本外准确度是一个不错的选择,可以避免我们自我欺骗。然而,留下数据意味着我们将拥有更少的数据来训练模型,而这通常是我们不能奢侈的。因此,这个问题在数据分析中是一个核心问题,已有多个提案来解决它。两种非常流行的方法是:
-
信息准则:这是一个总称,用来指代各种表达式,这些表达式将样本外准确度近似为样本内准确度加上一个惩罚模型复杂性的项。
-
交叉验证:这是一种基于将可用数据分为不同子集的方法,这些子集交替用于拟合和评估模型。
让我们在接下来的部分更详细地讨论这两种方法。
5.3.1 信息准则
信息准则是一组密切相关的工具,用于从拟合优度和模型复杂度的角度比较模型。换句话说,信息准则形式化了我们在本章开始时发展起来的直觉。这些量的具体推导方式与一个叫做信息理论的领域有关([MacKay, 2003]),这是一个有趣的领域,但我们将追求一个更直观的解释。
衡量模型拟合数据好坏的一种方法是计算数据与模型预测值之间的均方根误差:

E(y[i]|θ)是给定估计参数后的预测值。值得注意的是,这本质上是观察值和预测数据之间平方差的平均值。将误差平方可以确保差异不会相互抵消,并且相对于其他方法(例如计算绝对值),它能更强调较大的误差。
均方根误差可能你已经很熟悉了。它是一个非常流行的度量——流行到我们可能从未花时间思考它。但如果我们仔细想想,就会发现,原则上它并没有什么特别之处,我们完全可以设计出其他类似的表达式。当我们采用概率方法时,正如本书中所做的那样,一个更一般(和自然的)的表达式如下:

也就是说,我们为每个n个观测值计算似然。我们使用和而不是乘积,因为我们在使用对数。为什么我们说这是自然的呢?因为我们可以认为,在为模型选择似然时,我们实际上在选择如何惩罚数据与预测之间的偏差。事实上,当 p(y[i]|θ) 是高斯分布时,上述表达式将与均方根误差成比例。
现在,让我们将注意力转向对几个特定信息准则的详细探索。
赤池信息量准则
赤池信息量准则(AIC)是一个著名且广泛使用的信息准则,尤其在贝叶斯领域外,定义为:

k 是模型参数的数量,
[mle] 是 θ 的最大似然估计。
最大似然估计是非贝叶斯方法中的常见做法,并且通常,当使用平坦的先验时,它等同于贝叶斯最大后验估计(MAP)。需要注意的是,
[mle] 是一个点估计,而不是一个分布。
因子 −2 只是一个常数,我们可以省略它,但通常不这么做。从实际的角度来看,重要的是,第一个项考虑了模型与数据的拟合程度,而第二个项则惩罚了模型的复杂度。因此,如果两个模型对数据的拟合程度相同,AIC 表示我们应该选择参数最少的模型。
AIC 在非贝叶斯方法中表现良好,但在其他情况下存在问题。一个原因是它没有使用θ的后验分布,因此丢失了信息。此外,从贝叶斯的角度来看,AIC 假设先验是平坦的,因此 AIC 与像本书中使用的那些信息丰富或稍微信息丰富的先验不兼容。另外,当使用信息丰富的先验或层次结构等结构时,模型中的参数数量并不是衡量模型复杂度的好方法,因为这些方法会减少有效参数的数量,也称为正则化。我们将在后面回到正则化的这个概念。
广泛适用的信息准则
广泛应用的信息准则 (WAIC) 类似于 AIC 的贝叶斯版本。它也有两个项,一个衡量拟合的好坏,另一个对复杂模型进行惩罚。但 WAIC 使用完整的后验分布来估计这两个项。以下表达式假设后验分布作为大小为S的样本(通过 MCMC 方法获得)来表示:

第一个项与赤池准则类似,只不过它是在所有观测值和所有后验样本上进行评估的。第二个项有点难以解释,除非涉及到一些技术细节。但它可以被理解为有效参数的数量。从实践角度来看,重要的是 WAIC 使用整个后验分布(而非点估计)来计算这两个项,因此 WAIC 可以应用于几乎任何贝叶斯模型。
其他信息准则
另一个广泛使用的信息准则是偏差信息准则 (DIC)。如果我们使用bayes-o-meter^(TM),DIC 比 AIC 更具贝叶斯特性,但不如 WAIC。虽然仍然很流行,但已通过理论和实证研究表明,WAIC 和主要的 LOO(见下一节)比 DIC 更有用。因此,我们不推荐使用它。
另一个广泛使用的准则是贝叶斯信息准则 (BIC)。就像逻辑回归和我母亲的干汤一样,这个名字可能会让人误解。BIC 作为一种修正 AIC 问题的方法提出,作者也为其提出了贝叶斯的理论依据。但 BIC 并不完全是贝叶斯的,因为像 AIC 一样,它假设使用平坦先验,并使用最大似然估计。
但更重要的是,BIC 与 AIC 和 WAIC 在目标上有所不同。AIC、WAIC 和 LOO(见下一节)试图反映哪个模型能更好地推广到其他数据(预测精度),而 BIC 试图识别哪个是正确的模型,因此更与贝叶斯因子相关。
5.3.2 交叉验证
交叉验证是一种简单且在大多数情况下有效的模型比较方法。我们将数据划分为 K 个切片,尽量使这些切片在大小上保持一致(有时也会在其他特征上保持一致,如类别数量)。然后,我们使用 K-1 个切片来训练模型,使用剩下的一个切片来进行测试。这个过程是通过系统地反复省略每次训练集中的一个不同切片,并使用该切片作为评估集来完成的。直到完成 K 轮拟合与评估,可以在图 5.7中看到。模型的准确度将是 K 轮中每一轮准确度的平均值。这被称为 K 折交叉验证。最后,在执行完交叉验证后,我们使用所有数据进行最后一次拟合,这个模型就可以用来进行预测或其他目的。

图 5.7:K 折交叉验证
当 K 等于数据点的数量时,我们得到的就是留一法交叉验证(LOOCV),即每次都将模型拟合到除了一个数据点之外的所有数据点上。
交叉验证是机器学习中的常规实践,我们所描述的仅仅是这种实践的最基本方面。这里展示的模式还有很多变体。有关更多信息,您可以阅读 James 等人 [2023]或 Raschka 等人 [2022]的研究。
交叉验证是一个非常简单且有用的概念,但对于某些模型或大规模数据集,交叉验证的计算成本可能超出了我们的能力。许多人尝试找到更简单的计算量,比如信息准则。在接下来的章节中,我们将讨论一种通过对所有数据进行单次拟合来近似交叉验证的方法。
近似交叉验证
交叉验证是一个不错的想法,但它可能代价高昂,特别是像留一法交叉验证这样的变体。幸运的是,利用单次拟合的数据可以近似交叉验证!这一方法称为“帕累托平滑重要性采样留一法交叉验证”。这个名字非常长,因此在实践中我们通常简称它为 LOO。从概念上来说,我们尝试计算的是:

这是期望的对数逐点预测密度(ELPD)。我们加上下标LOO-CV,以明确表示我们使用留一法交叉验证来计算 ELPD。[−i]表示我们省略了观测值i。
这个表达式与后验预测分布的表达式非常相似。不同之处在于,现在我们要计算的是从没有包含观察值 y[i] 的后验分布中计算的后验预测分布。我们采取的第一个逼近方法是通过从后验分布中抽样来避免显式计算积分。因此,我们可以写成:

这里,求和是对 S 后验样本进行的。在本书中,我们经常使用 MCMC 样本。因此,这种逼近方法对你来说应该不陌生。接下来是比较棘手的部分。
可以使用重要性抽样来逼近 。我们不会详细讨论这种统计方法,但我们将看到重要性抽样是一种通过重新加权从另一个分布中获得的值来逼近目标分布的方法。当我们不知道如何从目标分布中抽样,但知道如何从另一个分布中抽样时,这种方法很有用。重要性抽样在已知分布比目标分布更宽泛时效果最好。
在我们的例子中,一旦模型拟合完成,已知分布就是所有观察值的对数似然。我们希望逼近的是如果我们去掉一个观察值后的对数似然。为此,我们需要估计每个观察值在确定后验分布中的“重要性”(或权重)。一个观察值的“重要性”与如果该观察值被移除时该变量对后验分布的影响成正比。直观地说,一个相对不太可能的观察值比一个预期中的观察值更为重要(或权重更大)。幸运的是,一旦我们计算了后验分布,这些权重就容易计算。实际上,观察值 i 对于 s 后验样本的权重是:

这个 w[s] 可能不可靠。主要的问题是,有时一些 w[s] 可能会非常大,以至于它们主导了我们的计算,使得计算不稳定。为了控制这些极端的权重,我们可以使用 Pareto 平滑方法。这个解决方案包括将这些权重中的一部分替换为通过拟合 Pareto 分布获得的权重。为什么使用 Pareto 分布?因为理论表明,权重应该遵循这种分布。
因此,对于每个观察值 y[i],使用最大的权重来估计 Pareto 分布,并且使用该分布来替换这些权重为“平滑”后的权重。这个过程为 ELPD 的估计提供了稳健性,并且提供了一种诊断逼近的方法,即,能够发出警告,提示 LOO 方法可能出现问题。为此,我们需要关注 k 的值,k 是 Pareto 分布的一个参数。k 值大于 0.7 表明我们可能有非常有影响力的观察值。
5.4 使用 ArviZ 计算预测准确性
幸运的是,使用 ArviZ 计算 WAIC 和 LOO 非常简单。我们只需要确保推理数据包含对数似然组。使用 PyMC 计算后验时,可以通过执行pm.sample(idata_kwargs="log_likelihood": True)来实现这一点。现在,让我们看看如何计算 LOO:
代码 5.3
az.loo(idata_l)
Computed from 8000 posterior samples and 33 observations log-likelihood matrix.
Estimate SE elpd_loo -14.31 2.67
p_loo 2.40 -
------
Pareto k diagnostic values:
Count Pct. (-Inf, 0.5] (good) 33 100.0%
(0.5, 0.7] (ok) 0 0.0%
(0.7, 1] (bad) 0 0.0%
(1, Inf) (very bad) 0 0.0%
az.loo的输出分为两部分。在第一部分,我们得到一个包含两行的表格。第一行是 ELPD(elpd_loo),第二行是有效参数数(p_loo)。在第二部分,我们有 Pareto k 诊断。这是 LOO 近似可靠性的一种度量。k 值大于 0.7 表示我们可能有非常有影响力的观测值。在这种情况下,我们有 33 个观测值,且它们都是好的,因此我们可以信任这个近似。
要计算 WAIC,你可以使用az.waic;输出将类似,只是我们不会得到 Pareto k 诊断或任何类似的诊断信息。这是 WAIC 的一个缺点:我们无法获得任何关于近似可靠性的信息。
如果我们为二次模型计算 LOO,将会得到类似的输出,但 ELPD 值会更高(大约-4),这表明二次模型更好。
ELPD 的数值本身并不太有用,必须与其他 ELPD 值进行比较来解读。这就是为什么 ArviZ 提供了两个辅助函数来方便这种比较。我们先来看az.compare:
代码 5.4
cmp_df = az.compare({"model_l": idata_l, "model_q": idata_q})
| rank | elpd_loo | p_loo | elpd_diff | weight | se | dse | warning | scale | |
|---|---|---|---|---|---|---|---|---|---|
| model_q | 0 | -4.6 | 2.68 | 0 | 1 | 2.36 | 0 | False | log |
| model_l | 1 | -14.3 | 2.42 | 9.74 | 3.0e-14 | 2.67 | 2.65 | False | log |
在行中,我们列出了比较的模型,在列中,我们有:
-
rank: 模型的顺序(从最好到最差)。 -
elpd_loo: ELPD 的点估计。 -
p_loo: 有效的参数数。 -
elpd_diff: 最佳模型与其他模型之间的 ELPD 差异。 -
weight: 每个模型的相对权重。如果我们想通过结合不同的模型来进行预测,而不是仅选择一个模型,这将是我们应该赋予每个模型的权重。在这种情况下,我们看到多项式模型占用了所有的权重。 -
se: ELPD 的标准误差。 -
dse: 差异的标准误差。 -
warning: 关于高 k 值的警告。 -
scale: 计算 ELPD 时所用的尺度。
ArviZ 提供的另一个辅助函数是az.compareplot。这个函数提供了与az.compare相似的信息,但以图形方式呈现。图 5.8展示了该函数的输出。请注意:
-
空心圆代表 ELPD 值,黑线是标准误差。
-
最高的 ELPD 值通过一条垂直的虚线灰线标示,以便与其他值进行比较。
-
对于所有模型,除了最优模型,我们还会得到一个三角形,表示每个模型与最优模型之间 ELPD 差值的大小。灰色误差条表示点估计之间差异的标准误差。

图 5.8:az.compareplot(cmp_df)的输出
使用 LOO(或 WAIC)最简单的方法是选择一个单一模型。只需选择 ELPD 值最高的模型。如果我们遵循这个规则,我们将不得不接受二次模型是最优的。即使考虑到标准误差,我们也可以看到它们并没有重叠。这给了我们一些确定性,表明这些模型确实足够不同。如果标准误差重叠,我们应该给出更细致的答案。
5.5 模型平均
模型选择因其简便性而具有吸引力,但我们可能忽视了模型中关于不确定性的信息。这有点类似于计算完整的后验分布,然后仅保留后验均值;这可能导致我们对自己所知的内容过于自信。
另一种方法是选择一个单一模型,但报告并分析不同的模型,以及计算出的信息标准、它们的标准误差,或许还包括后验预测检查。将所有这些数字和测试放在我们的实际问题背景下非常重要,这样我们和我们的观众可以更好地了解模型可能的局限性和缺陷。对于学术界的人来说,这些元素可以用来在论文、报告、论文等的讨论部分中增加内容。在行业中,这对向利益相关者提供有关模型、预测和结论的优缺点非常有用。
另一种可能性是对模型进行平均。通过这种方式,我们保留了每个模型拟合优度的不确定性。然后我们使用每个模型的加权平均值来获得一个元模型(以及元预测)。ArviZ 提供了一个用于此任务的函数 az.weight_predictions,它接受推断数据对象列表和权重列表作为参数。权重可以使用 az.compare 函数计算。例如,如果我们想要对我们一直在使用的两个模型进行平均,可以按照以下方式操作:
代码 5.5
idata_w = az.weight_predictions(idatas, weights=[0.35, 0.65])
图 5.9 显示了该计算的结果。浅灰色虚线是两个模型的加权平均,黑色实线是线性模型,灰色实线是二次模型。

图 5.9:线性和二次模型的加权平均
还有其他方式来对模型进行平均,例如显式地构建一个元模型,将所有感兴趣的模型作为特定情况包含在内。例如,二阶多项式包含了线性模型作为特定情况,或者分层模型是两种极端模型之间的连续版本,一个是分组模型,另一个是非分组模型。
5.6 贝叶斯因子
LOO、交叉验证和信息准则的替代方法是贝叶斯因子。贝叶斯因子通常作为频率派假设检验的贝叶斯替代方案出现在文献中。
比较k个模型的贝叶斯方法是计算每个模型的边际 似然 p(y|M[k]),即给定模型M[k]时观察数据Y的概率。边际似然是贝叶斯定理的归一化常数。我们可以通过写出贝叶斯定理并明确表明所有推断都依赖于模型来看到这一点。

其中,y是数据,θ是参数,M[k]是从k个竞争模型中选择的模型。
如果我们的主要目标是从一组模型中选择一个模型,即选择最佳模型,那么我们可以选择具有最大p(y|M[k])值的那个模型。如果我们假设所有模型的先验概率相同,这样做是可以的。否则,我们必须计算:

如果我们的主要目标是比较模型,以确定哪些模型更可能且可能的程度,那么可以使用贝叶斯因子来实现:

这是两个模型的边际似然比。BF[01]的值越高,分子中的模型(本例中的M[0])就越好。为了方便解读贝叶斯因子,并将数字转化为文字,Harold Jeffreys 提出了一个解读贝叶斯因子的尺度,其中包括支持或强度的不同级别(见表 5.1)。
| 贝叶斯因子 | 支持度 |
|---|---|
| 1–3 | 轶事 |
| 3–10 | 中等 |
| 10–30 | 强烈 |
| 30–100 | 非常强烈 |
| >100 | 极端 |
表 5.1:分子中的模型M[0]的支持度
请记住,如果你得到的数值低于 1,那么支持的是M[1],即分母中的模型。对于这些情况,也有表格可供参考,但请注意,你可以简单地取获得值的倒数。
记住,这些规则只是约定——充其量只是简单的指南。结果应始终放在我们的具体问题背景下,并附上足够的细节,以便他人可以自行评估他们是否同意我们的结论。在粒子物理学中确保某些东西的证明,或者在法庭上,或者在面对即将发生的自然灾难时决定是否进行撤离的证明是不一样的。
5.6.1 一些观察结果
现在我们简要讨论一些关于边际似然的关键事实:
-
优点:包括奥卡姆剃刀。参数较多的模型比参数较少的模型有更大的惩罚。直观的理由是,参数越多,先验相对于似然的扩展就越大。一个容易看出这种情况的例子是嵌套模型:例如,二次多项式“包含”了一次多项式和零次多项式模型。
-
坏处:对于许多问题,边际似然无法进行解析计算。此外,数值近似通常是一项艰巨的任务,在最好的情况下需要专门的方法,而在最坏的情况下,估算结果要么不切实际,要么不可靠。事实上,MCMC 方法的流行在于它们可以在不计算边际似然的情况下获得后验分布。
-
缺点:边际似然对每个模型中的参数先验分布非常敏感(p(θ[k]|M[k]))。
需要注意的是,优点和缺点是相关的。使用边际似然来比较模型是一个好主意,因为它已经对复杂模型进行了惩罚(这有助于我们防止过拟合),同时,先验的变化将影响边际似然的计算。起初,这听起来有些傻;我们已经知道先验会影响计算(否则我们可以直接避免使用它们)。但我们这里谈论的是先验的变化,这些变化在后验中可能只有小的影响,但对边际似然值有很大影响。
贝叶斯因子的使用常常是贝叶斯学派的分水岭。其计算的难度以及对先验的敏感性是反对其使用的一些论点。另一个原因是,像 p 值和假设检验一样,贝叶斯因子更倾向于二分法思维,而不是“效应大小”的估计。换句话说,我们不是在问自己像这样的问题:癌症治疗可以延长多少年的生命?而是最终问,治疗与不治疗之间的差异是否是“统计显著的”。请注意,这个最后的问题在某些背景下是有用的。关键是,在许多其他情况下,这种问题并不是我们感兴趣的问题;我们只对我们被教导去回答的问题感兴趣。
5.6.2 贝叶斯因子的计算
正如我们已经提到的,边际似然(以及由此得出的贝叶斯因子)通常无法以闭式形式提供,除非是某些特定模型。因此,已经设计了许多数值方法来计算它。其中一些方法非常简单且天真(radfordneal.wordpress.com/2008/08/17/the-harmonic-mean-of-the-likelihood-worst-monte-carlo-method-ever),以至于在实践中效果非常差。
分析上
对于某些模型,如 BetaBinomial 模型,我们可以解析计算边际似然。如果我们将该模型表示为:
| θ ∼ Beta(α,β) | ||
|---|---|---|
| y ∼ Bin(n = 1,p = θ) |
然后,边际似然将是:

B 是贝塔函数(不要与 Beta 分布混淆),n 是实验次数,h 是成功次数。
由于我们只关心在两种不同模型下的边际似然相对值(对于相同的数据),我们可以省略二项系数
,因此我们可以写成:

这个表达式已在下一个代码块中编写,但有些变化。我们将使用 betaln 函数,它返回 beta 函数的自然对数,通常在统计学中,计算使用对数尺度。这可以减少在处理概率时的数值问题。
对数尺度。这减少了处理概率时的数值问题。
代码 5.6
from scipy.special import betaln
def beta_binom(prior, y):
"""
Calculate the marginal probability, analytically, for a BetaBinomial model.
prior : tuple
alpha and beta parameters for the beta prior
y : array
array with "1" and "0" corresponding to success and failure respectively
"""
alpha, beta = prior
h = np.sum(y)
n = len(y)
p_y = np.exp(betaln(alpha + h, beta + n - h) - betaln(alpha, beta))
return p_y
本示例的数据由 100 次抛硬币实验组成,正反面数量相同。我们将比较两种模型,一种具有均匀先验,另一种具有围绕 θ = 0.5 的 更集中 先验:
代码 5.7
y = np.repeat([1, 0], [50, 50]) # 50 heads, 50 tails
priors = ((1, 1), (30, 30)) # uniform prior, peaked prior
图 5.10 显示了两个先验分布。均匀先验是黑线,尖峰先验是灰线。

图 5.10:均匀先验和尖峰先验
现在,我们可以为每个模型计算边际似然和贝叶斯因子,结果为 5:
代码 5.8
BF = beta_binom(priors[1], y) / beta_binom(priors[0], y)
print(round(BF))
5
我们看到,具有先验 beta(30,30) 的模型,其浓度更高,支持度大约是 beta(1,1) 模型的 5 倍。这是可以预期的,因为第一个模型的先验集中在 θ = 0.5 附近,而数据 Y 中正反面数量相同,即它们与 θ 约为 0.5 的值一致。
序列蒙特卡洛
序列蒙特卡洛(SMC)方法是一种通过一系列连续阶段的采样方法,逐步连接一个易于采样的分布和感兴趣的后验分布。在实践中,起始分布通常是先验分布。SMC 采样器的副产品是边际似然的估计。
代码 5.9
models = []
idatas = []
for alpha, beta in priors:
with pm.Model() as model:
a = pm.Beta("a", alpha, beta)
yl = pm.Bernoulli("yl", a, observed=y)
idata = pm.sample_smc(random_seed=42)
models.append(model)
idatas.append(idata)
BF_smc = np.exp(
idatas[1].sample_stats["log_marginal_likelihood"].mean()
- idatas[0].sample_stats["log_marginal_likelihood"].mean()
)
print(np.round(BF_smc).item())
5.0
从前面的代码块中可以看出,SMC 也给出了贝叶斯因子 5,结果与解析计算一致!使用 SMC 计算边际似然的优点是我们可以将其应用于更广泛的模型,因为我们不再需要知道一个封闭形式的表达式。我们为这种灵活性付出的代价是更高的计算成本。此外,值得注意的是,SMC(使用独立的 Metropolis-Hastings 核心,如 PyMC 中实现的)并不像 NUTS 那样高效。随着问题的维度增加,更精确的后验估计和边际似然需要更多的后验样本。
对数空间
在计算统计学中,我们通常在对数空间中进行计算。这有助于提供数值稳定性和计算效率等。举个例子,参考前面的代码块;你可以看到我们计算了差异(而不是除法),然后在返回结果之前取了指数。
Savage–Dickey 比率
对于上述例子,我们比较了两个 BetaBinomial 模型。我们本可以比较两个完全不同的模型,但有时我们希望比较一个零假设H_0(或零模型)与一个备择假设H_1。例如,为了回答“这个硬币是否有偏?”的问题,我们可以将值θ = 0.5(表示没有偏差)与允许θ变化的模型的输出进行比较。对于这种比较,零模型嵌套在备择模型中,这意味着零假设是我们正在构建的模型中的一个特定值。在这些情况下,计算贝叶斯因子非常简单,不需要任何特殊的方法。我们只需要比较在备择模型下评估零值(例如θ = 0.5)时的先验和后验。我们可以从以下表达式中看到这一点:

仅当H[0]是H[1]的特定情况时,这才成立(statproofbook.github.io/P/bf-sddr)。接下来,让我们用 PyMC 和 ArviZ 来实现。我们只需要为一个模型进行先验和后验的采样。让我们尝试使用 Uniform 先验的 BetaBinomial 模型:
代码 5.10
with pm.Model() as model_uni:
a = pm.Beta("a", 1, 1)
yl = pm.Bernoulli("yl", a, observed=y)
idata_uni = pm.sample(2000, random_seed=42)
idata_uni.extend(pm.sample_prior_predictive(8000))
az.plot_bf(idata_uni, var_name="a", ref_val=0.5)
结果显示在图 5.11中。我们可以看到一个先验的 KDE(黑色)和一个后验的 KDE(灰色)。两个黑点显示我们在值 0.5 处评估了这两个分布。我们可以看到支持零假设的贝叶斯因子BF_01约为 8,我们可以将其解释为适度证据支持零假设(见表 5.1)。

图 5.11:具有 Uniform 先验的 BetaBinomial 模型的贝叶斯因子
正如我们已经讨论过的,贝叶斯因子衡量的是哪个模型更好地解释了数据。这包括先验,即使先验对后验计算的影响相对较小。我们也可以通过将第二个模型与零模型进行比较,看到这种先验效应。
如果我们的模型是一个带有 Beta 先验(30, 30)的 BetaBinomial 模型,那么BF_01会更低(在 Jeffrey 尺度上为轶事证据)。这是因为根据这个模型,θ = 0.5 的值在先验中比 Uniform 先验更有可能,因此先验和后验将更加相似。也就是说,在收集数据后,看到后验集中在 0.5 附近并不令人惊讶。不要只相信我,我们来计算一下:
代码 5.11
with pm.Model() as model_conc:
a = pm.Beta("a", 30, 30)
yl = pm.Bernoulli("yl", a, observed=y)
idata_conc = pm.sample(2000, random_seed=42)
idata_conc.extend(pm.sample_prior_predictive(8000))
az.plot_bf(idata_conc, var_name="a", ref_val=0.5)
图 5.12 显示了结果。我们可以看到BF_01大约是 1.6,这可以解释为支持零假设的轶事证据(参见之前讨论的 Jeffreys 尺度)。

图 5.12:具有尖峰先验的 BetaBinomial 模型的贝叶斯因子
5.7 贝叶斯因子与推断
到目前为止,我们已经使用贝叶斯因子来判断哪个模型在解释数据方面似乎更好,我们发现其中一个模型的表现大约是另一个的 5 倍更好。
那么,来自这些模型的后验怎么样呢?它们有多不同?表 5.2 总结了这两种后验:
| 均值 | 标准差 | hdi_3% | hdi_97% | |
|---|---|---|---|---|
| 均匀 | 0.5 | 0.05 | 0.4 | 0.59 |
| 尖峰 | 0.5 | 0.04 | 0.42 | 0.57 |
表 5.2:使用 ArviZ 汇总函数计算的具有均匀和尖峰先验的模型统计数据
我们可以说结果非常相似;我们得到了相同的θ均值,而model_0的后验略宽,这是预期的,因为该模型具有较宽的先验。我们还可以检查后验预测分布,看看它们有多相似(见图 5.13)。

图 5.13:具有均匀和尖峰先验的模型的后验预测分布
在这个例子中,观察到的数据更符合model_1,因为其先验集中在正确的θ值附近,而model_0则对所有可能的θ值赋予相同的概率。这种模型之间的差异由贝叶斯因子捕捉到。我们可以说,贝叶斯因子衡量的是哪个模型作为整体更适合解释数据。这包括了先验的细节,无论模型预测多么相似。在许多情况下,比较模型时,我们并不关心这些细节,反而更倾向于评估模型预测的相似度。对于这些情况,我们可以使用 LOO。
5.8 正则化先验
使用信息丰富和弱信息先验是一种在模型中引入偏差的方法,如果做得恰当,这实际上是非常有益的,因为偏差可以防止过拟合,从而帮助模型进行具有良好泛化能力的预测。这种向模型中加入偏差元素以减少泛化误差而不影响模型充分建模问题能力的想法被称为正则化。这种正则化通常表现为对模型中某些参数值进行惩罚的项,例如在回归模型中对过大的系数进行惩罚。限制参数值是减少模型能够表示的数据量的一种方式,从而减少模型捕捉噪声而非信号的可能性。
这个正则化思想非常强大且有用,以至于它在贝叶斯框架之外也被多次发现。对于回归模型,且不局限于贝叶斯统计,两个流行的正则化方法是岭回归和 lasso 回归。从贝叶斯角度来看,岭回归可以解释为对线性模型的β系数使用正态分布作为先验,其中的标准差较小,使得系数趋向于零。从这个意义上讲,我们在本书中的每个线性模型(除了本章中使用 SciPy 的例子)实际上都在做类似岭回归的事情!
另一方面,lasso 回归可以从贝叶斯角度解释为通过从具有拉普拉斯先验的模型中计算的后验的最大后验估计(MAP),其中β系数使用拉普拉斯分布作为先验。拉普拉斯分布看起来类似于高斯分布,但在零点处有一个尖锐的峰值。你也可以将其解释为两个背对背的指数分布(试试pz.Laplace(0, 1).plot_pdf())。与高斯分布相比,拉普拉斯分布的概率质量更集中在零附近。使用这种先验的思想是提供正则化和变量选择。具体来说,由于我们在零点处有一个峰值,我们期望先验分布能够引入稀疏性,也就是说,我们创建一个具有大量参数的模型,而先验会自动使大多数参数为零,仅保留对模型输出有贡献的相关变量。
不幸的是,与岭回归不同,这一思想并不能直接从频率学派转化到贝叶斯学派。然而,确实存在一些贝叶斯先验可以用来引入稀疏性并执行变量选择,比如马鞍先验。如果你想了解更多关于马鞍先验和其他收缩型先验的信息,你可以参考 Piironen 和 Vehtari 的文章[2017],它可以在arxiv.org/abs/1707.01694找到。在下一章中,我们将进一步讨论变量选择。最后补充一句:值得注意的是,岭回归和 lasso 回归的经典版本对应于单点估计,而贝叶斯版本则产生完整的后验分布。
5.9 总结
在本章中,我们已经看到如何使用后验预测检查、信息准则、近似交叉验证和贝叶斯因子来比较模型。
后验预测检查是一个通用概念和实践,它帮助我们理解模型在捕捉数据的不同方面方面的表现。我们可以仅用一个模型或多个模型进行后验预测检查,因此我们可以将其用作模型比较的方法。后验预测检查通常通过可视化方式进行,但像贝叶斯值这样的数值摘要也能提供帮助。
好的模型在复杂度和预测准确性之间有一个良好的平衡。我们通过使用经典的多项式回归例子来展示这一特点。我们讨论了两种方法来估计外样本准确性,而不需要将数据排除在外:交叉验证和信息准则。从实际角度来看,信息准则是一类理论方法,旨在平衡两个方面的贡献:衡量模型如何拟合数据的度量和对复杂模型的惩罚项。我们简要讨论了 AIC,因其历史重要性,然后讨论了 WAIC,它是贝叶斯模型的更好方法,因为它考虑了整个后验分布,并使用更复杂的方法来计算有效参数数目。
我们还讨论了交叉验证,看到我们可以使用 LOO 来近似留一法交叉验证。WAIC 和 LOO 通常会产生非常相似的结果,但 LOO 可能更为可靠。所以我们推荐使用它。WAIC 和 LOO 都可以用于模型选择和模型平均。与其选择一个最佳模型,模型平均是通过加权平均所有可用的模型来实现的。
模型选择、比较和模型平均的另一种方法是贝叶斯因子,它是两个模型的边际似然比。贝叶斯因子的计算可能非常具有挑战性。在本章中,我们展示了使用 PyMC 和 ArviZ 计算贝叶斯因子的两种方法:一种是使用被称为顺序蒙特卡洛的方法,另一种是使用萨维奇–迪基比率。第一种方法可以用于任何模型,只要顺序蒙特卡洛提供了良好的后验分布。由于 PyMC 中 SMC 的当前实现,对于高维模型或层次模型来说,这可能会具有挑战性。第二种方法仅在原假设模型是备择模型的特定情况时才可使用。除了计算上具有挑战性外,贝叶斯因子的使用也存在问题,因为它们对先验设定非常(过度)敏感。
我们已经展示了贝叶斯因子和 LOO/WAIC 是回答两个相关但不同问题的工具。前者侧重于识别正确的模型,而后者则侧重于识别具有较低泛化损失的模型,即做出最佳预测的模型。这些方法都不是没有问题的,但 WAIC,特别是 LOO,在实践中比其他方法更为稳健。
5.10 练习
-
本练习涉及正则化先验。在生成
x_c, y_c数据的代码中(见github.com/aloctavodia/BAP3),将order=2更改为其他值,例如order=5。然后,拟合model_q并绘制结果曲线。重复此步骤,但这次使用sd=100的β先验,而不是sd=1,并绘制结果曲线。这些曲线有何不同?也试试使用sd=np.array([10, 0.1, 0.1, 0.1, 0.1])。 -
重复之前的练习,但将数据量增加到 500 个数据点。
-
拟合一个三次模型(阶数 3),计算 WAIC 和 LOO,绘制结果,并与线性和二次模型进行比较。
-
使用
pm.sample_posterior_predictive()重新运行 PPC 示例,但这次绘制y的值,而不是均值的值。 -
阅读并运行 PyMC 文档中的后验预测示例,链接:
www.pymc.io/projects/docs/en/stable/learn/core_notebooks/posterior_predictive.html。特别注意共享变量和pm.MutableData的使用。 -
返回生成图 5.5 和 图 5.6 的代码,并修改它以获取新的六个数据点集。直观评估不同的多项式如何拟合这些新数据集。将结果与本书中的讨论联系起来。
-
阅读并运行 PyMC 文档中的模型平均示例,链接:
www.pymc.io/projects/examples/en/latest/diagnostics_and_criticism/model_averaging.html。 -
使用均匀先验 Beta(1, 1) 和如 Beta(0.5, 0.5) 等先验计算硬币问题的贝叶斯因子。设定 15 次正面和 30 枚硬币。将此结果与本书第一章的推断结果进行比较。
-
重复上一个示例,我们比较贝叶斯因子和信息准则,但这次减少样本量。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,并与超过 5000 名成员一起学习,链接:packt.link/bayesian

第六章
使用 Bambi 建模
一件好工具能改善你的工作方式,一件伟大的工具能改善你的思维方式。——Jeff Duntemann
在第四章中,我们描述了线性回归模型的基本成分以及如何将其推广以更好地适应我们的需求。在这一章中,我们将继续学习线性模型,但这次,我们将使用 Bambi [Capretto et al., 2022],一个基于 PyMC 构建的高层贝叶斯模型构建接口。Bambi 旨在使拟合线性模型,包括分层模型,变得极其简单。我们将看到 Bambi 的领域不仅限于线性模型。
我们将学习以下内容:
-
使用 Bambi 构建并拟合模型
-
使用 Bambi 分析结果
-
多项式回归与样条函数
-
分布模型
-
分类预测变量
-
交互作用
-
使用 Kulprit 进行变量选择
6.1 一种语法解决所有问题
PyMC 有一个非常简单而富有表现力的语法,它允许我们构建任意模型。这通常是一个福音,但有时也可能成为负担。Bambi 则专注于回归模型,这种限制使得语法和功能更加专注,正如我们将看到的那样。
Bambi 使用了类似许多 R 包(如 nlme、lme4 和 brms)所用的 Wilkinson 公式语法。假设data是一个像表格 6.1中展示的那样的 pandas DataFrame。
| y | x | z | g | |
|---|---|---|---|---|
| 0 | -0.633494 | -0.196436 | -0.355148 | A 组 |
| 1 | 2.32684 | 0.0163941 | -1.22847 | B 组 |
| 2 | 0.999604 | 0.107602 | -0.391528 | C 组 |
| 3 | -0.119111 | 0.804268 | 0.967253 | A 组 |
| 4 | 2.07504 | 0.991417 | 0.590832 | B 组 |
| 5 | -0.412135 | 0.691132 | -2.13044 | C 组 |
表格 6.1:一个示例 pandas DataFrame
使用这些数据,我们想要构建一个从x预测y的线性模型。使用 PyMC,我们会做类似以下代码块中的模型:
代码 6.1
with pm.Model() as lm:
Intercept = pm.Normal("Intercept", 0, 1)
x = pm.Normal("x", 0, 1)
y_sigma = pm.HalfNormal("sigma", 1)
y_mean = Intercept + x * data["x"]
y = pm.Normal("y", y_mean, y_sigma, observed=data["y"])
Bambi 使用的公式语法让我们可以更紧凑地定义一个等效的模型:
代码 6.2
a_model = bmb.Model("y ∼ x", data)
在波浪符号(∼)的左侧是因变量,右侧是自变量(们)。通过这种语法,我们只是指定了均值(在 PyMC 模型lm中的μ)。默认情况下,Bambi 假设似然函数是高斯分布;你可以通过family参数来更改这一点。公式语法并没有指定先验分布,只是描述了因变量和自变量之间的关系。Bambi 会自动为我们定义(非常)弱的信息先验。我们可以通过打印 Bambi 模型来获取更多信息。如果你打印a_model,你应该会看到类似如下的内容:
Formula: y ~ x Family: gaussian Link: mu = identity Observations: 117 Priors: target = mu Common-level effects Intercept ~ Normal(mu: 0.02, sigma: 2.8414) x ~ Normal(mu: 0.0, sigma: 3.1104) Auxiliary parameters sigma ~ HalfStudentT(nu: 4.0, sigma: 1.1348)
第一行显示了我们用来定义模型的公式,第二行是似然函数。第三行是链接函数。然后我们有用于拟合模型的观测数量,接下来告诉我们我们正在线性建模高斯的参数mu。输出的后半部分显示了模型结构:共同水平效应,在本例中是截距(Intercept)和斜率(x),以及辅助参数,即所有非线性建模的参数,本例中是高斯标准差。
您可以通过将字典传递给bmb.Model的priors参数来覆盖默认先验分布。例如,如果我们想为变量x的系数和辅助参数sigma定义自定义先验分布,我们可以这样做:
代码 6.3
priors = {"x": bmb.Prior("HalfNormal", sigma=3),
"sigma": bmb.Prior("Gamma", mu=1, sigma=2),
}
a_model_wcp = bmb.Model("y ∼ x", data, priors=priors)
结果,我们将得到以下模型规范:
Formula: y ~ x Family: gaussian Link: mu = identity Observations: 117 Priors: target = mu Common-level effects Intercept ~ Normal(mu: 0.02, sigma: 2.837) x ~ HalfNormal(sigma: 3.0) Auxiliary parameters sigma ~ Gamma(mu: 1.0, sigma: 2.0)
如果您想从模型中省略截距,可以这样做:
代码 6.4:
no_intercept_model = bmb.Model("y ∼ 0 + x", data)
或者甚至这样做:
代码 6.5:
no_intercept_model = bmb.Model("y ∼ -1 + x", data)
打印模型no_intercept_model,您会看到截距不再存在。
如果我们想要包含更多变量怎么办?我们可以这样做:
代码 6.6
model_2 = bmb.Model("y ∼ x + z", data)
我们还可以包含组级效应(层次结构);例如,如果我们想要使用变量g来部分汇总x的估计值,我们可以这样做:
代码 6.7
model_h = bmb.Model("y ∼ x + z + (x | g)", data)
我们可以在图 6.1中看到这个模型的视觉表示。注意变量1|g_offset和x|g_offset。默认情况下,Bambi 拟合一个非居中的分层模型;您可以通过参数noncentered来更改这一点。

图 6.1:model_h的视觉表示
公式语法非常简单,但也非常强大。我们只是初步展示了可以使用它做什么。如果您想深入了解,可以查看公式文档 bambinos.github.io/formulae/。 formulae 是负责解析威尔金森公式用于 Bambi 的 Python 包。
6.2 自行车模型,Bambi 版本
我们将首先使用自行车模型来说明如何使用 Bambi,该模型来自第 4章。我们可以通过以下方式加载数据:
代码 6.8
bikes = pd.read_csv("data/bikes.csv")
现在我们可以构建和拟合模型:
代码 6.9
model_t = bmb.Model("rented ∼ temperature", bikes, family="negativebinomial")
idata_t = model_t.fit()
图 6.2展示了模型的视觉表示。如果您想要直观地检查先验分布,可以使用model.plot_priors():

图 6.2:用命令model.graph()计算的自行车模型的视觉表示
现在让我们绘制后验均值和后验预测分布(预测)。为了使图表看起来漂亮,省略了一些细节,做这些操作的代码是:
代码 6.10
_, axes = plt.subplots(1, 2, sharey=True, figsize=(12, 4))
bmb.interpret.plot_predictions(model_t, idata_t,
"temperature", ax=axes[0])
bmb.interpret.plot_predictions(model_t, idata_t,
"temperature", pps=True, ax=axes[1])
plot_predictions 是 Bambi 子模块 interpret 中的一个函数。这个函数通过绘制条件调整预测,帮助分析回归模型,展示(条件)响应分布中的某个参数如何随(某些)插值的解释变量变化。我们可以在图 6.3中看到这段代码的结果。左侧面板显示后验均值和 94% HDI,而右侧面板显示后验预测分布(租用自行车的预测分布)。请注意,预测的不确定性远大于均值的不确定性(pps=False)。这是因为后验预测分布考虑了模型参数的不确定性以及数据的不确定性,而均值的后验分布仅考虑了截距和斜率参数的不确定性。

图 6.3:自行车模型的后验均值和后验预测分布
当我们有多个解释变量时,plot_cap 的实用性变得更加明显。例如,假设我们拟合一个模型,使用温度和湿度来预测租赁的自行车数量:
代码 6.11
model_th = bmb.Model("rented ∼ temperature + humidity", bikes,
family="negativebinomial")
idata_th = model_th.fit()
bmb.interpret.plot_predictions(model_th, idata_th, ["temperature", "humidity"],
subplot_kwargs={"group":None, "panel":"humidity"})
在图 6.4中,我们可以看到五个面板,每个面板展示了在不同湿度值下,租赁自行车数量随温度变化的情况。正如你所看到的,租赁的自行车数量随温度上升而增加,但在湿度较低时,斜率更大。

图 6.4:带有温度和湿度的自行车模型的后验均值
6.3 多项式回归
使用线性回归模型拟合曲线的一种方法是构建一个多项式,像这样:

我们称m为多项式的次数。
有两点需要注意。首先,多项式回归仍然是线性回归;线性是指系数(β),而不是变量(x)。第二点需要注意的是,我们正在凭空创建新变量。唯一被观测到的变量是x,其余的只是x的幂。通过观察到的变量创造新变量是回归中完全有效的“技巧”;有时这种变换可以通过理论来解释或说明(比如取婴儿身长的平方根),但有时它仅仅是为了拟合曲线。多项式的直观理解是,对于给定的x值,阶数越高的多项式,曲线的灵活性越强。1 阶多项式是直线,2 阶多项式是可以向上或向下的曲线,3 阶多项式是可以先上升再下降的曲线(或反之),以此类推。注意我说的是“可以”,因为如果我们有一个 3 阶多项式,比如 β[0] + β[1]x + β[2]x² + β[3]x³,但系数β[2]和β[3]是 0(或几乎为 0),那么曲线就会是一条直线。
使用 Bambi 定义多项式回归有两种方式。我们可以写出原始多项式:
代码 6.12
"y ∼ x + I(x ** 2) + I(x ** 3) + I(x ** 4)"
在这里,我们使用恒等函数I()来明确表示我们希望将x提升到某个幂次。我们需要这样做,因为**操作符在 Bambi 中有特殊的含义。如果我们使用这种语法,实际上是在告诉 Bambi 将y的均值建模为 α + β[0]x + β[0]x² + β[0]x³ + β[0]x⁴。
或者,我们可以写:
代码 6.13
"y ∼ poly(x, 4)"
这也会生成一个 4 阶的多项式,但多项式项之间是正交的,这意味着项之间的相关性被降低了。不深入数学细节,这至少有两个重要的结果与标准多项式相比。首先,估计可能在数值上更稳定;其次,系数的解释方式不同。在标准多项式回归中,系数可能很难解释,因为改变一个系数的值会影响整个多项式。相比之下,正交多项式可以让你更清晰地解释每个项的影响,因为它们彼此独立。尽管系数的解释不同,但其他结果保持不变。例如,使用这两种方法你应该得到相同的预测结果。
让我们构建一个 4 阶的正交多项式来拟合自行车数据。在这个例子中,我们将使用hour变量:
代码 6.14
model_poly4 = bmb.Model("rented ∼ poly(temperature, degree=4)", bikes,
family="negativebinomial")
idata_poly4 = model_poly4.fit()
图 6.5 显示了后验均值和后验预测分布。在第一行,你将看到一个 1 阶的多项式,它相当于一个线性模型。在第二行,你将看到一个 4 阶的多项式。

图 6.5:温度和湿度下的自行车模型的后验均值和后验预测分布
多项式的一个问题是它们作用全局。当我们应用一个阶数为m的多项式时,我们实际上是在说自变量与因变量之间的关系在整个数据集中都是阶数m。当数据的不同区域需要不同灵活性时,这可能会造成问题。例如,这可能导致曲线过于灵活。随着阶数的增加,拟合变得更加敏感于数据点的移除,或者等同于对未来数据的加入。换句话说,随着阶数的增加,模型更容易发生过拟合。贝叶斯多项式回归通常较少遭遇这种“过度”灵活性,因为我们通常不使用平坦的先验,并且我们不会计算单一的系数集,而是计算整个后验分布。不过,我们还是可以做得更好。
6.4 样条曲线
一般来说,写出非常灵活的模型的方法是将函数B[m]应用于X[m],然后将它们与系数β[m]相乘:

我们可以自由选择B[m],例如,我们可以选择多项式。但我们也可以选择其他函数。一种常见的选择是使用 B 样条;我们不打算讨论它们的定义,但可以将它们视为一种创建平滑曲线的方式,使得我们在获得像多项式一样的灵活性时,过拟合的可能性较小。我们通过使用分段多项式来实现这一点,即将多项式限制在仅影响数据的一部分的区域内。图 6.6 显示了逐步增加阶数的分段多项式的三个示例。虚线垂直线表示“节点”,它们是用于限制区域的点,虚灰线表示我们要逼近的函数,黑线表示分段多项式。

图 6.6:逐步增加阶数的分段多项式
图 6.7 显示了 1 阶和 3 阶样条的示例;底部的点表示“节点”,虚线表示 B 样条。在顶部,我们展示了所有具有相等权重的 B 样条;我们使用灰度来突出显示我们有多个 B 样条。在底部面板中,每个 B 样条的权重不同(我们通过β[m]系数对它们进行加权);如果我们对加权后的 B 样条进行求和,最终得到的黑线就是结果。这条黑线通常被称为“样条曲线”。我们可以使用贝叶斯统计来找到 B 样条的适当权重。

图 6.7:1 阶(分段线性)或 3 阶(立方样条)的 B 样条及其结果样条曲线。
我们可以通过使用bs函数,在 Bambi 中使用 B 样条。例如,让我们对自行车数据拟合一个 3 阶的样条曲线:
代码 6.15
num_knots = 6
knots = np.linspace(0, 23, num_knots+2)[1:-1]
model_spline = bmb.Model("rented ∼ bs(hour, degree=3, knots=knots)", bikes,
family="negativebinomial")
idata_spline = model_spline.fit()
图 6.8 显示了租赁自行车数量在深夜时最低。然后有一个增加,可能是因为人们醒来去上班、上学或做其他活动。我们在大约第 8 小时左右有一个第一个高峰,然后略微下降,接着在大约第 18 小时出现第二个高峰,可能是因为人们下班回家,之后出现稳定的下降。注意,曲线并不是非常平滑;这并不是因为样条模型的原因,而是因为数据的原因。我们的测量是在离散的时间点(每小时)进行的。

图 6.8:样条模型的后验均值
在处理样条时,我们必须做出一个重要的决策,那就是确定结点的数量和位置。这可能是一个相当令人头疼的任务,因为最佳的结点数量及其间距并不立刻显现出来。一个有用的建议是考虑基于分位数而非均匀分布来确定结点的位置——像这样 knots = np.quantile(bikes.hour, np.linspace(0, 1, num_knots))。通过这样做,我们会在数据量较大的区域放置更多的结点,而在数据量较少的区域放置较少的结点。这样可以得到一个更具适应性的近似,从而有效地捕捉数据点密度较高区域的变化。此外,我们可能希望尝试使用不同数量和位置的结点来拟合样条,然后评估结果,使用诸如 LOO 等工具,就像我们在第五章中看到的那样。
6.5 分布模型
我们之前看到,可以使用线性模型来表示均值(或位置参数)以外的其他参数。例如,我们可以为高斯分布的均值和标准差分别使用线性模型。这些模型通常被称为分布模型。分布模型的语法非常相似;我们只需要为我们想要建模的辅助参数添加一行。例如,高斯分布的σ,或负二项分布的α。
现在让我们重现第四章中的一个例子——婴儿示例:
代码 6.16
formula = bmb.Formula(
"length ∼ np.sqrt(month)",
"sigma ∼ month"
)
model_dis = bmb.Model(formula, babies)
idata_dis = model_dis.fit()
图 6.9 显示了 model_dis(变化的 sigma)和常数 sigma 模型的后验分布值。我们可以看到,当 sigma 允许变化时,得到的值既低于也高于常数 sigma 的估计值,这意味着当我们不允许 sigma 改变时,我们会低估或高估这个参数。

图 6.9:婴儿数据的常数与变化的 sigma
图 6.10 显示了 model_dis 的后验拟合。注意,模型能够捕捉到婴儿成长过程中变化的变异性。这个图与图 4.13非常相似。

图 6.10:model_dis的后验拟合
在使用 PyMC 时,我们看到从后验预测分布中进行采样时,需要定义“Xs”作为可变数据,然后在计算后验预测分布之前更新该变量。而在 Bambi 中,这不需要。我们可以使用predict方法,通过将新值传递给data参数来预测新值。例如,让我们预测一只小企鹅在 0.5 个月(15 天)时的体长:
代码 6.17
model_dis.predict(idata_dis, kind="pps", data=pd.DataFrame({"month":[0.5]}))
6.6 类别预测变量
类别变量表示不同的组或类别,这些类别只能从有限的集合中取值。通常,这些值是标签或名称,本身不具备数值意义。以下是一些例子:
-
政治立场:保守派、自由派或进步派。
-
性别:女性或男性。
-
客户满意度:非常不满意、不满意、中立、满意或非常满意。
线性回归模型可以轻松地处理类别变量;我们只需要将类别编码为数字。可以通过几种方式做到这一点,Bambi 可以轻松地为我们处理这些细节。真正的难点在于结果的解释,我们将在接下来的两节中深入探讨。
6.6.1 类别企鹅
对于当前的例子,我们将使用 palmerpenguins 数据集,Horst 等人[2020],该数据集包含 344 个观测值和 8 个变量。目前,我们关心的是将企鹅的体重建模为喙长的函数。预计随着喙长的增加,企鹅的体重也会增加。本例的新颖之处在于,我们将考虑类别变量species。在这个数据集中,物种变量有 3 个类别或级别,分别是 Adelie、Chinstrap 和 Gentoo。图 6.11 显示了我们要建模的变量的散点图。

图 6.11:3 种企鹅的喙长与体重的关系
让我们加载数据并拟合模型:
代码 6.18
penguins = pd.read_csv("data/penguins.csv").dropna()
model_p = bmb.Model("body_mass ∼ bill_length + species", data=penguins)
idata_p = model_p.fit()
请注意,定义 Bambi 模型中的类别变量时没有特殊的语法。Bambi 可以自动检测并处理它们。
图 6.12 显示了model_p的森林图。注意到什么意外情况了吗?Adelie 没有后验值。这不是错误。默认情况下,Bambi 将具有 N 个级别(3 个物种)的类别变量编码为 N-1 个虚拟变量(2 个物种)。因此,物种-Chinstrap 和物种-Gentoo 的系数被建模为与基线模型的偏差:

为了更清楚地说明这一点,我们来查看几个图表。我们可以通过图 6.12 理解为,Chinstrap 的体重平均比 Adelie 轻 0.89。Gentoo 也是如此,不过这次我们需要在基线模型的均值上加上 0.66。

图 6.12: 来自 model_p 的森林图
你可以通过查看图 6.13 来验证这两个陈述的真实性。注意中间是阿德利(Adelie),下方是拟企鹅(Chinstrap,-0.89),上方是根趾企鹅(Gentoo,0.58),它们的三条线基本平行。

图 6.13: 来自 model_p 的平均样本预测
6.6.2 关于分层模型的关系
在第 3 章中,我们讨论了并对比了汇集模型和分层(或部分汇集)模型。我们在那里展示了通常情况下,我们利用数据的结构或层次。遵循该章节的逻辑,你可以认为阿德利、根趾企鹅和拟企鹅虽然是不同的物种,但都是企鹅。因此,分层建模它们的体重可能是个好主意。你这样想是正确的。那么这种模型与我们在本节中使用的模型有什么区别呢?
区分因素在于斜率和截距的微妙组成部分。在后者的情况下,斜率在三种企鹅物种中保持不变,而截距可以变化:阿德利的Intercept + 0,拟企鹅的Intercept + species[Chinstrap],根趾企鹅的Intercept + species[Gentoo]。因此,该模型突出了不同的截距,同时保持了统一的斜率。
如果我们建立了层次模型 body_mass ~(bill_length|species),我们将请求部分汇集的斜率和截距。如果我们建立了模型 body_mass ~(0 + bill_length | species),我们将请求部分汇集的斜率和公共截距。
除了这些特定的模型外,在考虑将预测变量作为分组变量或分类预测变量使用时,通常有必要问问该变量是否包括所有可能的类别(例如所有星期的所有天,所有物种等),还是仅包括子组(某些学校或少数音乐流派)。如果我们有所有可能的类别,那么我们可能更喜欢将其建模为分类预测变量,否则建模为分组变量。
正如我们之前讨论过的,通常在决定哪一个模型最佳之前,我们会创建多个模型。最佳模型是与你分析目标一致、提供有意义的见解,并准确代表数据潜在模式的模型。探索多个模型,使用适当的标准(例如第 5 章中讨论的标准)比较它们的性能,并考虑每个模型对研究或决策过程的实际影响通常是个好主意。
6.7 交互作用
交互效应或统计交互效应发生在独立变量对响应的影响因另一个独立变量的值而变化时。交互作用可以发生在两个或多个变量之间。一些例子包括:
-
教育水平和收入的影响:较高的教育水平可能对某一性别的收入产生更强的正向影响,从而形成教育和性别之间的交互作用。
-
药物疗效与年龄:一种对年长者效果更好的药物,而对年轻人效果较差。
-
运动和饮食对减重的影响:对于做很少或不做运动的人来说,饮食对减重的影响可能较小;而对于做中等强度运动的人来说,饮食对减重的影响可能较大。
-
作物生长的温度和湿度:一些作物可能在高温高湿的环境中茁壮成长,而其他作物则可能在较凉爽、湿度较低的环境中表现更好。
当两个或更多变量的联合效应不等于它们单独效应的总和时,我们就有了交互作用。因此,如果我们有如下模型,我们就无法建模交互作用:

建模交互作用效应的最常见方法是将两个(或更多)变量相乘。例如,考虑以下这样的模型:

在建模交互作用效应时,通常还会包括主效应/项。
新的预测因子
将两个变量相乘可以看作是一种技巧,类似于我们在多项式回归(或对给定变量的任何变换)中使用的方法。我们不是将一个预测因子与自身相乘,而是将两个不同的预测因子相乘,得到一个新的预测因子。
在 PyMC 模型中定义两个变量之间的交互作用非常简单;我们只需要将这两个预测因子相乘,并添加一个系数。对于 Bambi 模型来说,更加简单;我们使用 : 运算符。为了让区别更加清晰,让我们看看一个有交互作用和没有交互作用的模型示例:
代码 6.19
# No interaction
model_noint = bmb.Model("body_mass ∼ bill_depth + bill_length",
data=penguins)
#Interaction
model_int = bmb.Model("body_mass ∼ bill_depth + bill_length +
bill_depth:bill_length",
data=penguins)
idata_noint = model_noint.fit()
idata_int = model_int.fit()
我们现在使用 Bambi 的 plot_prediction 来比较不同 bill_length 值如何作为 bill_depth 的函数影响 body_mass。图 6.14 显示了结果。我们为 bill_depth 在 5 个固定的 bill_length 值下计算了平均回归拟合。左侧是 model_noint(无交互作用)结果,右侧是 model_int(有交互作用)结果。我们可以看到,当没有交互作用时,bill_depth 的拟合曲线在不同的 bill_length 水平上是平行的。相反,当存在交互作用时,这些曲线不再平行,正因为 bill_depth 的变化对 body_mass 变化的影响不再是常数,而是受到 bill_length 值的调节。

图 6.14:model_noint(左)和 model_int(右)的样本内平均预测
如果你生成了像图 6.14 这样的图,但不是固定 bill_length,而是决定固定 bill_depth,你将观察到类似的行为。
在本书的 GitHub 仓库中(github.com/aloctavodia/BAP3),你将找到文件 interactions.ipynb。这个脚本生成一个 3D 图形,我希望它能帮助你建立关于添加交互项时我们所做工作的直觉。如果你运行它,你会看到,当没有交互项时,我们拟合的是一个 2D 平面,一张像纸一样的平坦表面。但是,当添加交互项时,你会拟合一个弯曲的表面。将 interactions.ipynb 的结果与 图 6.14 进行比较。
我们刚刚通过可视化看到,解释包含交互项的线性模型并不像解释没有交互项的线性模型那样简单。让我们从数学上来看一下。
假设我们有一个包含 2 个变量 X[0] 和 X[1] 以及它们之间的交互作用的模型:

我们可以将这个模型重写为:

或者甚至像这样:

从这个表达式中,我们可以看到:
-
交互项可以理解为线性模型中的线性模型。
-
交互作用是对称的;我们可以将其理解为 X[0] 作为 X[1] 的函数的斜率,同时也是 X[1] 作为 X[0] 的函数的斜率。这也可以从交互式图形中看到。
-
我们之前知道,β[0] 系数可以解释为 μ 对 X[0] 单位变化的响应量(这就是我们称之为斜率的原因)。如果我们添加一个交互项,那么只有当 X[1] = 0 时,这个关系才成立。试着使用交互式图形自己看看。数学上,这是真的,因为当 X[1] = 0 时,β[2]X[1] = 0,因此 X[0] 的斜率简化为 β[0]X[0]。通过对称性,同样的推理可以应用于 β[1]。
6.8 使用 Bambi 解释模型
在本章中,我们已经多次使用了 bmb.interpret_plot_predictions。但这并不是 Bambi 提供的唯一帮助我们理解模型的工具。另一个工具是 bmb.interpret_plot_comparisons。这个工具帮助我们回答这样的问题:“当我们比较某个变量的两个值时,保持其他所有值不变,预测差异是什么?”
我们使用上一节的 model_int,因此无需重新拟合模型。我们使用以下代码块生成 图 6.15:
代码 6.20
bmb.interpret.plot_comparisons(model_int, idata_int,
contrast={"bill_depth":[1.4, 1.8]},
conditional={"bill_length":[3.5, 4.5, 5.5]})
图 6.15 显示了当将假设的企鹅的 bill_depth 从 1.8 比较到 1.4 时,期望的差异是:
-
大约 0.8 千克,当喙长为 3.5 厘米时
-
-0.6 千克,当喙长为 4.5 厘米时
-
大约 -2 千克,当喙长为 5.5 厘米时

图 6.15:在 3 个固定的 bill_length 值下,将 bill_depth 从 1.8 厘米对比到 1.4 厘米
如果你希望以表格形式查看信息,可以使用函数bmb.interpret.comparisons,这样你将得到一个 DataFrame 而不是图表。
另一个有用的函数是bmb.interpret_plot_slopes,它可用于计算给定值下的“瞬时变化率”或斜率。我们使用以下代码块生成图 6.16:
代码 6.21
bmb.interpret.plot_slopes(model_int, idata_int,
wrt={"bill_depth":1.8},
conditional={"bill_length":[3.5, 4.5, 5.5]},
图 6.16 显示了bill_depth为 1.8 时的斜率:
-
≈ 2 kg/cm,适用于鸟嘴长度为 3.5 cm 时
-
-1.4 kg/cm,适用于鸟嘴长度为 4.5 cm 时
-
≈ -5 kg/cm,适用于鸟嘴长度为 5.5 cm 时

图 6.16:对于bill_length的三个固定值,bill_depth在 1.8 cm 时的斜率
如果你希望以表格形式查看信息,可以使用函数bmb.interpret.slopes,这样你将得到一个 DataFrame 而不是图表。
在本节中,我们仅仅触及了bmb.interpret模块工具的表面。这个模块是 Bambi 的一个非常有用的功能,尤其适用于含有交互作用和/或具有除恒等函数之外链接函数的模型。我强烈建议你阅读 Bambi 文档,获取更多未在此介绍的示例和细节。
6.9 变量选择
变量选择是指从一组潜在预测变量中识别出对模型结果最相关的变量的过程。我们进行变量选择时假设,只有一部分变量对感兴趣的结果有显著影响,而其他变量几乎没有或没有额外的价值。
构建模型时,最“贝叶斯”的做法可能是将我们可能想到的所有变量都包含在一个模型中,然后利用该模型的后验分布进行预测或了解变量之间的关系。这是最“贝叶斯”的方法,因为我们尽可能地使用数据,并在后验中纳入变量重要性的未知性。然而,比贝叶斯更“贝叶斯”并不总是最佳选择。我们在第五章中已经看到,即使贝叶斯因子是贝叶斯定理的直接结果,它们也可能存在问题。
当以下情况出现时,执行变量选择是一个好主意:
-
我们需要降低测量成本。例如,在医学中,我们可能有资金和资源进行一项试点研究,测量 200 名患者的 30 个变量。但是我们无法对数千人做同样的事情。或者,我们可能能在开阔地带放置大量传感器来更好地建模作物收益,但我们无法将其扩展到一个国家的规模。降低成本并不总是意味着金钱或时间;在与人类或其他动物工作时,减少疼痛和不适也很重要。例如,我们可能想预测患者发生心脏病发作的风险。我们可以通过测量很多变量来做到这一点,但我们也可以通过测量少量更不具侵入性的变量来实现。
-
我们希望减少计算成本。这对于小型和简单模型来说不是问题,但当我们有大量变量、大量数据或两者兼有时,计算成本可能会变得无法承受。
-
我们寻求更好地理解重要的关联结构。也就是说,我们有兴趣理解哪些变量提供更好的预测。需要明确的是,我们不是在讨论因果关系。虽然统计模型,尤其是广义线性模型(GLMS),可以用来推断因果关系,但这需要额外的步骤和假设。在本书中,我们不讨论如何进行因果推断。如果你想要一个非常基础的因果推断介绍,请参见这个视频:
www.youtube.com/watch?v=gV6wzTk3o1U。如果你更加严肃,您可以查阅 Scott Cunningham 的在线书籍《因果推断:混音带》[Cunningham,2021]mixtape.scunning.com/。 -
当我们希望获得一个对数据生成分布变化更具韧性的模型时,我们可以将变量选择视为一种使模型对不具代表性数据更加稳健的方法。
6.9.1 投影预测推断
有许多方法可以执行变量选择。在本节中,我们将重点介绍其中一种叫做投影预测推断的方法[Piironen 等人,2020,McLatchie 等人,2023]。我们专注于这种方法的主要原因是,它在广泛的领域中表现出了非常好的性能。
投影预测推断的主要步骤如下:
-
生成一个参考模型,即包含所有你认为可能相关和/或你能够测量的变量的模型。
-
生成一组子模型,即仅包含参考模型中某些子集变量的模型。
-
将参考模型的后验分布投影到子模型中。
-
选择一个预测结果与参考模型足够接近的最小模型。
在进行投影预测推断时,我们只需要执行一次贝叶斯推断,仅针对参考模型。对于子模型,后验分布是通过投影得到的。不深入讨论技术细节,投影过程是通过某种方式找到子模型的参数,使得子模型的预测尽可能接近参考模型的预测。这个投影过程可以以计算效率高的方式进行,因此估计后验分布的成本比使用 MCMC 方法低几个数量级。这一点很重要,因为当我们增加参考模型中的变量数时,可能的子模型总数会呈指数增长。考虑一下我们需要评估所有可能的组合,不重复变量。例如,假设我们有四个变量(A,B,C 和 D),需要评估 7 个模型,分别是 A,B,C,AB,BC,AC 和参考模型 ABC。七个模型听起来不多,但当我们增加到 8 个变量时,我们将需要评估 92 个不同的模型。看到没有,我们将变量数翻倍,模型的数量却增加了 10 倍以上!
当然,减少需要探索的子模型总数是有办法的。例如,我们可以使用某些廉价方法筛选出最有前景的变量,然后只对这些变量进行投影预测推断。另一个替代方案被称为前向搜索;也就是说,我们首先拟合与我们所拥有的变量数量相等的模型。然后选择一个模型/变量,即生成与参考模型的预测最接近的那个模型。接着,我们生成所有包含上一阶段选择变量的两个变量子模型,依此类推。如果我们对一个包含 8 个变量的参考模型进行这种前向程序,而不是 92 个不同的模型,我们只需要评估 36 个模型。
另一个在进行投影预测推断时需要考虑的因素是,我们只为参考模型提供了先验分布。子模型没有明确的先验分布;它们只是通过投影过程继承了参考模型的先验分布。
投影预测在实践中有效的原因之一,正是得益于参考模型的使用。通过将子模型拟合到参考模型在样本中的预测,而不是拟合观察到的数据,我们能够过滤掉数据中的噪声。这有助于将更相关的变量与不太相关的变量区分开。另一个因素是,在选择子模型时使用了交叉验证,如在第五章中所讨论的那样。
6.9.2 使用 Kulprit 进行投影预测
Kulprit 是一个用于投影预测推断的 Python 包。它与 Bambi 配合使用,我们可以传递一个用 Bambi 构建的参考模型,Kulprit 会为我们完成所有复杂的工作。为了说明如何使用 Kulprit,我们将使用体脂数据集 [Penrose et al., 1985]。这个数据集包含了 251 个个体的测量数据,包括他们的年龄、体重、身高、腹围等。我们的目的是预测体脂百分比(通过 siri 变量估算)。由于获得准确的体脂测量既昂贵又可能对患者造成困扰,我们希望在减少测量数量的同时保持 siri 的良好预测准确性。原始数据集包含 13 个变量;为了简化示例,我已经预先选择了 6 个变量。
我们首先需要像往常一样定义并拟合一个 Bambi 模型。我们必须确保包含参数 idata_kwargs=’log_likelihood’:True。Kulprit 内部会计算 ELPD,正如我们在 第五章 中讨论的那样,我们需要在 InferenceData 对象中包含对数似然值,以便能够估算 ELPD:
代码 6.22
model = bmb.Model("siri ∼ age + weight + height + abdomen + thigh + wrist",
data=body)
idata = model.fit(idata_kwargs={'log_likelihood': True})
完成这些后,我们就可以开始使用 Kulprit 了。首先,我们需要调用 ProjectionPredictive 类,并传入 Bambi 模型和从该模型拟合得到的 idata。然后我们让 Kulprit 执行搜索;默认情况下,它会进行前向搜索:
代码 6.23:
ppi = kpt.ProjectionPredictive(model, idata)
ppi.search()
搜索完成后,我们可以让 Kulprit 根据 ELPD 比较各个子模型。子模型会按 ELPD 从低到高排序,如 图 6.17 所示。在 x 轴上,我们有子模型的大小,即变量的数量;我们从零开始,因为我们包含了仅有截距的模型。虚线灰色线表示参考模型的 ELPD。

图 6.17:使用 Kulprit 得到的子模型比较。由 ppi.plot_compare 生成。
然后我们可以看到,大小为 3 的子模型几乎与参考模型等效。但具体包含了哪些变量呢?如果我们在执行搜索后打印 ppi 对象,我们将得到一个子模型公式的有序列表,列表顺序与通过命令 ppi.plot_compare 得到的图中的顺序相匹配:
代码 6.24
print(ppi)
0 siri ~ 1
1 siri ~ abdomen
2 siri ~ abdomen + wrist
3 siri ~ abdomen + wrist + height
4 siri ~ abdomen + wrist + height + age
5 siri ~ abdomen + wrist + height + age + weight
6 siri ~ abdomen + wrist + height + age + weight + thigh
然后我们可以看到,大小为 3 的模型是包含变量 abdomen(腹部)、wrist(手腕)和 height(身高)的模型。这个结果告诉我们,如果我们想选择一个比参考模型更少变量的模型,但预测精度相似,那么这是一个不错的选择。根据不同的情境,其他子模型也可能是一个好选择。例如,我们可能会认为大小为 2 和 3 的子模型之间的差异非常小。因此,我们可能愿意牺牲一些精度来选择一个更小的模型。对于这个例子来说,测量患者身高可能不会带来太大问题,但在其他场景下,添加第三个变量可能会很昂贵、麻烦、危险等等。
另一种解释 图 6.17 的方式是注意到大小为 3 或更大的模型的 ELPD 值是非常接近的。可能的情况是,如果我们使用稍微不同的数据集,甚至是相同的数据集,但增加更多的后验样本,我们可能会得到一个略有不同的顺序。因此,如果我们有许多大小为 3 的模型,它们可能具有相同的实际预测精度,我们可以通过外部因素来为选择第三个变量提供理由,比如它的测量是否容易或便宜,或者哪个对患者来说更不痛苦等等。总之,和其他统计工具一样,结果不应盲目接受,而是要结合上下文来解读;你应该有最终的决定权,工具应帮助你做出决策。
好的,假设我们确实对 Kulprit 计算的大小为 3 的子模型感兴趣;我们可以通过以下方式获取它:
代码 6.25
submodel = ppi.project(3)
从 submodel 对象中,我们可以检索一些有用的信息,比如 Bambi 的模型 submodel.model 或 InferenceData 对象 submodel.idata。
对于解释这两个对象,有一点需要注意——submodel.model 是一个由公式生成的 Bambi 模型。因此,它的先验将是 Bambi 自动计算的那些。但 Kulprit 计算的后验,存储在 submodel.idata.posterior 中,并非直接来自该模型。相反,它是使用投影预测推理(而非 MCMC)计算的,先验是在投影步骤中隐式继承的(而非显式先验)。图 6.18 展示了这种投影后的后验。

图 6.18:大小为 3 的子模型的投影后验
我们可以信任预测的后验分布吗?在非常一般的条件下,这应该是一个有效的后验分布,因此我们可以信任它。它应该足够提供参数值的大致概念,当然,这对于变量选择也是足够的。缺乏显式的先验可能会使模型的解释更加困难,但如果你只关心预测,这应该不是问题。当然,你始终可以使用 Bambi(或 PyMC)像往常一样显式地计算完整的后验,并在需要时自己指定先验。图 6.19 显示了通过 Bambi(真实)计算的子模型后验的森林图和通过 Kulprit(预测)近似的子模型后验。请注意,这里有两个可能的差异来源:MCMC 方法与投影预测方法之间的内在差异,以及两个模型的不同先验。

图 6.19:通过 Kulprit 计算的子模型(siri ~abdomen + wrist + height)后验与通过 Bambi 计算的参考模型后验的比较;未在两个模型中共享的变量已被省略
Kulprit 是一个非常新的库,将不断发展,用户可以期待不久后会有大量的增强和改进。如果 Kulprit 引起了你的兴趣,你可以通过报告问题、提出建议、改进文档或参与其代码库的开发来帮助其发展,网址是github.com/bambinos/kulprit。
6.10 小结
在本章中,我们已经看到如何使用 Bambi 拟合贝叶斯模型,作为纯 PyMC 模型的替代方案。我们从最简单的情况开始,一个包含单一预测变量的模型,然后过渡到更复杂的模型,包括多项式、样条、分布模型、包含分类预测变量的模型以及交互作用模型。
Bambi 的主要优势在于它非常易于使用;它与 R 的formula语法非常相似。内部,Bambi 定义了弱信息量的先验,并处理了复杂模型中可能繁琐的细节。主要的缺点是它不像 PyMC 那样灵活。Bambi 能够处理的模型范围是 PyMC 模型范围的一个小子集。不过,这个子集包含了工业界和学术界中最常用的许多统计模型。Bambi 的优势不仅仅在于轻松构建模型,还在于更容易的模型解释。在本章中,我们已经看到如何使用 Bambi 的interpret模块更好地理解我们拟合的模型。最后,我们还看到了如何使用 Kulprit 进行投影预测推断并进行变量选择。投影预测推断为变量选择提供了一种有前景的方法,而 Kulprit 则是一种有前景的 Python 化方式。
6.11 练习
-
阅读 Bambi 文档(
bambinos.github.io/bambi/)并学习如何指定自定义先验。 -
应用前一点学到的内容,为
model_t的斜率指定一个 HalfNormal 先验分布。 -
定义一个类似
model_poly4的模型,但使用raw多项式,比较两个模型的系数和均值拟合效果。 -
用你自己的话解释什么是分布模型。
-
将
model_spline扩展为一个分布模型。使用另一个样条来建模 NegativeBinomial 家族的 α 参数。 -
创建一个名为
model_p2的模型,用于预测body_mass,其预测变量为bill_length、bill_depth、flipper_length和species。 -
使用 LOO 方法比较前一点中的模型和
model_p。 -
使用
interpret模块中的函数来解释model_p2,并同时使用图表和表格。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,与志同道合的人一起学习,并与超过 5000 名成员共同成长,访问链接:packt.link/bayesian

第七章
混合模型
…父亲有狮子的形态,母亲有蚂蚁的形态;父亲吃肉,母亲吃草。于是他们孕育了蚂蚁狮子…——《虚构生物志》
拉普拉塔河(也称为拉普拉塔河或拉普拉塔江)是地球上最宽的河流,也是阿根廷和乌拉圭之间的自然边界。在 19 世纪末,这条河沿岸的港口区是土著人、非洲人(大多数是奴隶)和欧洲移民的混居地。这种文化交汇的一个结果是,欧洲音乐如华尔兹和马祖卡与非洲坎东贝舞曲和阿根廷米隆加(后者又是非洲裔美国节奏的混合)相融合,创造了我们现在所称的探戈舞和音乐。
将先前存在的元素混合在一起是创造新事物的好方法,这不仅仅是在音乐领域。在统计学中,混合模型是建模的常见方法之一。这些模型通过混合更简单的分布来获得更复杂的分布。例如,我们可以结合两个高斯分布来描述双峰分布,或者结合多个高斯分布来描述任意分布。尽管使用高斯分布非常普遍,但原则上我们可以混合任何我们想要的分布族。混合模型用于不同的目的,例如直接建模子人群,或者作为处理无法用简单分布描述的复杂分布的有用技巧。
本章将涵盖以下主题:
-
有限混合模型
-
零膨胀模型和障碍模型
-
无限混合模型
-
连续混合模型
7.1 理解混合模型
当总体人群是不同子人群的组合时,自然会出现混合模型。一个常见的例子是给定成人群体中的身高分布,它可以被描述为女性和男性子人群的混合。另一个经典的例子是手写数字的聚类。在这种情况下,期望有 10 个子人群是非常合理的,至少在十进制系统中是这样!如果我们知道每个观察值属于哪个子人群,通常来说,利用这些信息将每个子人群建模为一个独立的群体是一个好主意。然而,当我们无法直接访问这些信息时,混合模型就显得特别有用。
分布的混合
许多数据集无法用单一的概率分布准确描述,但可以通过将其描述为多种分布的混合来进行建模。假设数据来自混合分布的模型被称为混合模型。
在构建混合模型时,并不一定需要相信我们正在描述数据中的真实子群体。混合模型也可以作为一种统计技巧,为我们的工具箱增添灵活性。以高斯分布为例,我们可以将其作为许多单峰且近似对称分布的合理近似。那么,对于多峰或偏斜分布呢?我们能用高斯分布来建模吗?当然可以,如果我们使用高斯混合。
在高斯混合模型中,每个成分都是一个均值不同的高斯分布,并且通常(但不一定)具有不同的标准差。通过组合高斯分布,我们可以为我们的模型增添灵活性,并拟合复杂的数据分布。事实上,我们可以通过适当组合高斯分布来逼近几乎任何我们想要的分布。分布的具体数量将取决于近似的准确度和数据的细节。实际上,我们在本书中的许多图表中已经使用了这个思想。核密度估计(KDE)技术就是这一思想的非贝叶斯实现。从概念上讲,当我们调用 az.plot_kde 时,函数会在每个数据点上方放置一个固定方差的高斯分布,然后将所有单独的高斯分布求和,以逼近数据的经验分布。图 7.1 显示了如何将 8 个高斯分布混合以表示复杂分布的示例,就像一条蟒蛇消化一只大象,或者一个帽子,取决于你的视角。
在 图 7.1 中,所有高斯分布具有相同的方差,并且它们都集中在灰色的点上,这些点代表可能的未知人群中的样本点。如果你仔细观察,可能会发现两个高斯分布重叠在一起。

图 7.1:作为高斯混合模型的 KDE 示例
无论我们是相信子群体的存在,还是将其作为数学上的便利(甚至是介于两者之间的某种观点),混合模型通过使用分布的混合来描述数据,是为我们的模型增添灵活性的一种有效方式。
7.2 有限混合模型
构建混合模型的一种方法是考虑两个或更多分布的有限加权混合。然后,观察数据的概率密度是 K 个子群体的概率密度加权求和:

我们可以将 w[i] 解释为成分 i 的概率,因此其值被限制在区间 [0, 1] 内,并且它们的和需要为 1。成分 p(y|θ[i]) 通常是简单的分布,比如高斯分布或泊松分布。如果 K 是有限的,那么我们就得到了一个有限混合模型。为了拟合这样的模型,我们需要提供一个 K 值,无论是因为我们事先知道正确的值,还是因为我们能做出有根据的猜测。
从概念上讲,解决混合模型的问题,我们只需要正确地将每个数据点分配到一个成分。在一个概率模型中,我们可以通过引入一个随机变量来实现这一点,随机变量的功能是指定某个观察值被分配到哪个成分。这个变量通常被称为潜变量,因为我们不能直接观察到它。
对于只有两个成分的混合模型(K = 2),我们可以使用抛硬币问题模型作为构建模块。在该模型中,我们有两种可能的结果,并且使用伯努利分布来描述它们。由于我们不知道正面或反面的概率,我们使用 Beta 分布作为先验分布。如果我们把硬币的正面和反面换成任何两个组(或成分或类别),我们可以在得到 0 时将观察值分配到一个组,得到 1 时分配到另一个组。这一切都很好,但在混合模型中,我们可能有两个或更多组,因此我们需要使这个想法更加通用。我们可以通过注意到伯努利分布到 K 个结果的推广是类别分布,而 Beta 分布到更高维度的推广是狄利克雷分布来实现这一点。如果我们分配的成分是高斯分布,就像在图 7.1 中一样,那么图 7.2 显示了一个类似 Kruschke 风格的模型图。圆角框表示我们有 K 个成分,类别变量决定了我们用哪个成分来描述给定的数据点。请注意,只有 μ[k] 依赖于不同的成分,而 σ[μ] 和 σ[σ] 是所有成分共享的。这只是一个建模选择;如果需要,我们可以改变它,并允许其他参数依赖于每个成分。

图 7.2:带有高斯成分的混合模型的 Kruschke 风格图
我们将在 PyMC 中实现这个模型,但在此之前,让我介绍一下类别分布和狄利克雷分布。如果你已经熟悉这些分布,可以跳过接下来的两节,直接跳到示例部分。
7.2.1 类别分布
类别分布是最一般的离散分布,它由一个向量进行参数化,其中每个元素指定每个可能结果的概率。图 7.3 表示了类别分布的两个可能实例。点表示类别分布的值,而连续的虚线是一个视觉辅助,帮助我们轻松理解分布的形状:

图 7.3:类别分布族的两个成员。虚线只是视觉辅助。
7.2.2 狄利克雷分布
Dirichlet 分布存在于单纯形中,你可以将其视为一个 n 维三角形;一个 1-单纯形是一个线段,2-单纯形是一个三角形,3-单纯形是一个四面体,依此类推。为什么是单纯形?直观上,因为这个分布的输出是一个K维向量,其元素被限制在区间 [0,1] 内,并且它们的和为 1。正如我们所说,Dirichlet 分布是 Beta 分布的推广。因此,理解 Dirichlet 分布的一个好方法是将其与 Beta 分布进行比较。Beta 分布用于处理两种结果的问题:一种的概率是p,另一种的概率是 1 − p。正如我们所见,p + (1 − p) = 1。Beta 分布返回一个两元素向量(p, q = 1 − p),但在实际应用中,我们省略了q,因为一旦知道p的值,结果就完全确定了。如果我们要将 Beta 分布扩展到三种结果,我们需要一个三元素向量(p, q, r),其中每个元素都在区间 [0,1] 内,且它们的和为 1。类似于 Beta 分布,我们可以使用三个标量来对这种分布进行参数化,我们可以称它们为α, β 和 γ;然而,由于希腊字母只有 24 个,我们很容易就会用完它们。相反,我们可以使用一个名为α的K维向量。请注意,我们可以将 Beta 和 Dirichlet 看作是比例分布。图 7.4展示了当k = 3 时,Dirichlet 分布的 4 个成员。在顶部是概率密度函数(pdf),在底部是来自该分布的样本。

图 7.4:Dirichlet 分布族的四个成员
好的,现在我们已经具备了实现我们第一个混合模型的所有组件。
7.2.3 化学混合物
为了让事情更具体,我们来做一个例子。我们将使用我们在第 2 章中看到的化学位移数据。图 7.5展示了这组数据的直方图。

图 7.5:化学位移数据的直方图
我们可以看到,这些数据无法用像高斯这样的单一分布来准确描述,但如果我们使用多个分布,就能做到这一点,就像我们在图 7.1中展示的那样;也许三个或四个分布就能解决问题。虽然有充分的理论依据(我们在这里不讨论),表明这些化学位移数据来自于 40 个子群体的混合分布,但仅通过观察数据,我们似乎无法恢复出真正的群体,因为它们之间有大量的重叠。
以下代码块展示了在 PyMC 中实现两个分量的高斯混合模型:
代码 7.1
K = 2
with pm.Model() as model_kg:
p = pm.Dirichlet('p', a=np.ones(K))
z = pm.Categorical('z', p=p, shape=len(cs_exp))
means = pm.Normal('means', mu=cs_exp.mean(), sigma=10, shape=K)
sd = pm.HalfNormal('sd', sigma=10)
y = pm.Normal('y', mu=means[z], sigma=sd, observed=cs_exp)
idata_kg = pm.sample()
如果你运行这段代码,你会发现它非常慢,且轨迹看起来非常糟糕(参考第十章了解更多诊断信息)。我们能让这个模型跑得更快吗?可以,让我们看看如何操作。
在model_kg中,我们已经明确将潜在变量z包含在模型中。对这个离散变量进行采样通常会导致后验采样不良。解决这个问题的一种方法是对模型进行重新参数化,使得z不再是模型的显式组成部分。这种重新参数化方法叫做边缘化(marginalization)。将离散变量边缘化通常能够提高速度并改进采样。不幸的是,这需要一些数学技能,而不是每个人都具备。不过幸运的是,我们不需要自己动手,因为 PyMC 已经包含了一个NormalMixture分布。所以,我们可以将混合模型写成如下形式:
代码 7.2
with pm.Model() as model_mg:
p = pm.Dirichlet('p', a=np.ones(K))
means = pm.Normal('means', mu=cs_exp.mean(), sigma=10, shape=K)
sd = pm.HalfNormal('sd', sigma=5)
y = pm.NormalMixture('y', w=p, mu=means, sigma=sd, observed=cs_exp)
idata_mg = pm.sample()
让我们通过森林图来检查结果。图 7.6显示了一些有趣的现象。在进入下一个章节之前,花些时间思考一下。你能发现问题吗?我们将在下一节讨论它。

图 7.6:model_mg的均值森林图
7.3 混合模型的非可识别性
means参数的形状为 2,从图 7.6我们可以看到,其中一个值大约是 47,另一个接近 57.5。搞笑的是,我们有一条链说means[0]是 47,其他三条链说它是 57.5,means[1]则正好相反。因此,如果我们计算means[0]的均值,我们会得到一个接近 55 的值(57.5 × 3 + 47 × 1),这显然不是正确的值。我们看到的正是一个名为参数非可识别性(parameter non-identifiability)现象的例子。这是因为,从模型的角度来看,如果组件 1 的均值为 47 而组件 2 的均值为 57.5,或者反之,两种情况是等价的。在混合模型的背景下,这也被称为标签交换问题(label-switching problem)。
非可识别性
如果模型的一个或多个参数无法被唯一确定,则该统计模型是不可识别的。如果对于模型参数的多个选择,得到相同的似然函数,则该模型的参数没有被识别。可能出现的情况是数据中没有足够的信息来估计这些参数。在其他情况下,参数可能无法识别,因为模型在结构上不可识别,这意味着即使所有必要的数据都可用,参数仍然无法唯一确定。
对于混合模型,有至少两种方式可以对模型进行参数化,从而消除非可识别性问题。我们可以强制使组件按顺序排列;例如,将组件的均值按严格递增的顺序排列和/或使用信息量大的先验分布。
使用 PyMC,我们可以通过下一个代码块中的转换实现第一个选项。请注意,我们还提供了均值的初始值;任何能确保第一个均值小于第二个均值的方式都可以。
代码 7.3
with pm.Model() as model_mgo:
p = pm.Dirichlet('p', a=np.ones(K))
means = pm.Normal('means', mu=cs_exp.mean(), sigma=10, shape=K,
transform=pm.distributions.transforms.ordered,
initval=np.array([cs_exp.mean()-1, cs_exp.mean()+1]))
sd = pm.HalfNormal('sd', sigma=10)
y = pm.NormalMixture('y', w=p, mu=means, sigma=sd, observed=cs_exp)
idata_mgo = pm.sample()
让我们通过森林图检查新的结果。图 7.7 确认我们已解决了不可识别性问题:

图 7.7:model_mgo 的均值森林图
7.4 如何选择 K
有关有限混合模型的一个主要问题是如何确定成分的数量。一个经验法则是从相对较小的成分数开始,然后增加成分数来改善模型拟合度。如我们在 第五章 中已经知道的,模型拟合可以通过后验预测检查、ELPD 等指标以及模型者的专业知识来评估。
让我们比较 K = {2,3,4,5*} 的模型。为此,我们将拟合该模型四次,然后保存数据和模型对象以备后用:
代码 7.4
Ks = [2, 3, 4, 5]
models = []
idatas = []
for k in Ks:
with pm.Model() as model:
p = pm.Dirichlet('p', a=np.ones(k))
means = pm.Normal('means',
mu=np.linspace(cs_exp.min(), cs_exp.max(), k),
sigma=cs_exp.var() / k, shape=k,
transform=pm.distributions.transforms.ordered,
)
sd = pm.HalfNormal('sd', sigma=5)
y = pm.NormalMixture('y', w=p, mu=means, sigma=sd, observed=cs_exp)
idata = pm.sample(random_seed=123,
idata_kwargs={"log_likelihood":True}
)
idatas.append(idata)
models.append(model)
图 7.8 展示了 K 个高斯分布的混合模型。黑色实线表示后验均值,灰色线表示后验样本。均值高斯分量使用黑色虚线表示。

图 7.8:不同数量高斯分布的高斯混合模型 (K)
从视觉效果来看,K = 2 太低了,但我们如何选择一个更好的值呢?正如我们在 第五章 中讨论过的,我们可以使用后验预测检查来评估感兴趣的测试量,并计算贝叶斯 p 值。图 7.9 展示了此类计算和可视化的示例。K = 5 是最佳解,K = 4 接近最佳解。

图 7.9:后验预测检查以选择 K
为了补充后验预测检查,我们可以计算使用 LOO 方法近似的 ELPD。这在 图 7.10 中展示。我们比较的是相同的模型,但 K 值不同。我们可以看到 K = 5 是最佳解,而 K = 4 也接近最佳解。这与 图 7.9 中显示的贝叶斯 p 值一致。

图 7.10:使用 LOO 进行模型选择以选择 K
化学位移的例子虽然简单,但展示了有限混合模型的主要思想。在这个例子中,我们使用了高斯分布,因为它们提供了一个良好的数据拟合近似。然而,如果需要,我们也可以使用非高斯分量。例如,我们可以使用:
-
泊松混合模型:假设你在监控每小时进入商店的顾客数量。泊松混合模型可以帮助识别顾客流量的不同模式,例如高峰时段或高峰日,通过假设数据遵循泊松分布的混合。
-
指数混合模型:假设你在研究某种类型的灯泡寿命。指数混合模型可以帮助识别不同寿命的灯泡群体,提示制造质量或环境因素的潜在差异。
在接下来的部分,我们将探索一种非常特殊的混合模型,这种模型涉及两个过程:一个生成零,另一个生成零或非零。
7.5 零膨胀和障碍模型
在计数事物时,比如道路上的汽车、天空中的星星、皮肤上的痣,或几乎任何其他事物,一个选项是不计数某个事物,也就是说得到零。零这个数字通常会由于很多原因出现;比如我们在数红色的车,但是一辆红色的车没有经过街道,或者我们错过了它。如果我们使用泊松分布或负二项分布来建模这种数据,我们会注意到模型生成的零比实际数据少。我们该如何解决这个问题呢?我们可以尝试解决模型预测零比实际观察到的少的具体原因,并将这一因素纳入模型中。但通常情况下,假设我们有两个过程的混合模型可能就足够了,而且更简单:
-
一个过程由一个离散分布建模,概率为
![]()
-
另一个过程以概率 1 −
生成额外的零
在某些文本中,你会发现
代表额外的零,而不是 1 −
。这不是什么大问题;只需注意哪个是哪个,尤其是具体示例时。
允许生成“额外”零的分布族被称为零膨胀分布。该家族中最常见的成员包括:
-
零膨胀泊松分布
-
零膨胀负二项分布
-
零膨胀二项分布
在下一节中,我们将使用零膨胀泊松分布来解决回归问题。一旦你了解如何使用这个分布,使用零膨胀负二项分布或零膨胀二项分布将变得非常简单。
7.5.1 零膨胀泊松回归
为了举例说明零膨胀泊松回归模型,我们将使用来自数字研究与教育研究所的数据集(www.ats.ucla.edu/stat/data)。我们有 250 组游客数据,数据包括:每组捕捉到的鱼的数量(count)、每组中的儿童数量(child)以及他们是否带了露营车到公园(camper)。使用这些数据,我们将建立一个模型,通过儿童和露营车变量来预测捕捉到的鱼的数量。
使用 PyMC,我们可以为这个数据编写一个模型,如下所示:
代码 7.5
with pm.Model() as ZIP_reg:
 = pm.Beta('', 1, 1)
*α* = pm.Normal('*α*', 0, 1)
*β* = pm.Normal('*β*', 0, 1, shape=2)
*θ* = pm.math.exp(*α* + *β*[0] * fish_data['child'] + *β*[1] * fish_data['camper'])
yl = pm.ZeroInflatedPoisson('yl', , *θ*, observed=fish_data['count'])
trace_ZIP_reg = pm.sample()
camper 是一个二元变量,值为 0 表示不是露营车,1 表示是露营车。表示某个属性是否存在的变量通常被称为虚拟变量或指示变量。请注意,当 camper 的值为 0 时,涉及 β[1] 的项也会变为 0,模型就会简化为一个只有一个自变量的回归模型。我们在第六章 6 中讨论了分类预测变量时已经提到过这一点。
结果如图 7.11所示。我们可以看到,孩子数量越多,捕获的鱼的数量越少。而且,带着露营车旅行的人通常会捕到更多的鱼。如果你检查 child 和 camper 的系数,你会发现我们可以得出以下结论:
-
每增加一个孩子,捕获的鱼的期望数量会减少约 ≈ 0.4
-
使用露营车露营会使捕获的鱼的期望数量增加约 2

图 7.11:捕获的鱼数作为孩子数量和是否使用露营车的函数
零膨胀模型与障碍模型密切相关,因此在零膨胀模型的概念仍然清晰的情况下学习障碍模型是非常有益的。
7.5.2 障碍模型
在障碍模型中,伯努利概率决定一个计数变量是否为零或大于零。如果大于零,我们认为已跨越 障碍,这些正值的分布是通过截断在零的分布来确定的。
在混合模型的框架下,我们可以将零膨胀模型视为零和其他某些值(可能是零或非零)的混合。而障碍模型则是零和非零的混合。因此,零膨胀模型只能增加 P(x = 0) 的概率,而对于障碍模型,概率可以变得更小或更大。
在 PyMC 和 Bambi 中可用于障碍模型的分布包括:
-
障碍 Poisson 分布
-
障碍负二项分布
-
障碍伽玛分布
-
障碍对数正态分布
为了说明障碍模型,我们将使用螯虾数据集 [Brockmann, 1996]。螯虾成对地来到海滩进行产卵仪式。此外,孤独的雄性也会来到海岸,聚集在已成对的雌雄螯虾周围,争夺受精卵的机会。这些被称为卫星雄性个体,通常会在特定的巢对附近聚集,忽略其他巢对。我们想要建立模型来预测雄性 卫星 的数量。我们怀疑这个数量与雌性螯虾的特征有关。作为预测变量,我们将使用螯甲的 宽度 和 颜色。螯甲是螯虾的坚硬上壳。颜色则使用从 1 到 4 的整数编码,从浅色到深色。
我们将使用 Bambi 来编码并拟合四个模型。这四个模型的主要区别在于我们将使用四种不同的似然函数或分布族,分别是 Poisson 分布、障碍 Poisson 分布、负二项分布和障碍负二项分布。模型展示在下一个代码块中:
代码 7.6
model_crab_p = bmb.Model("satellite ∼ width + C(color)",
family="poisson", data=crab)
model_crab_hp = bmb.Model("satellite ∼ width + C(color)",
family="hurdle_poisson", data=crab)
model_crab_nb = bmb.Model("satellite ∼ width + C(color)",
family="negativebinomial", data=crab)
model_crab_hnb = bmb.Model("satellite ∼ width + C(color)",
family="hurdle_negativebinomial", data=crab)
请注意,我们已经将颜色编码为 C(color),以向 Bambi 指示应将其视为分类变量,而不是数值型变量。
图 7.12 显示了我们为马蹄铁数据拟合的四个模型的后验预测检查。灰色条形图代表观察数据的频率。点是根据模型计算的预期值。虚线只是一个视觉辅助工具。我们可以看到,NegativeBinomial 比 Poisson 模型有了改善,而 Hurdle 模型比非膨胀模型有所提升。

图 7.12:四个模型在马蹄铁数据上的后验预测检查
图 7.13 显示了通过 LOO 计算的 ELPD 模型比较。Hurdle NegativeBinomial 是最佳模型,而 Poisson 模型是最差的。

图 7.13:使用 LOO 进行的四个模型在马蹄铁数据上的模型比较
图 7.12 很好,但还有一种替代的表示方式,叫做悬挂根图 [Kleiber and Zeileis, 2016],它在诊断和处理计数数据模型中的问题(如过度离散和/或零值过多)时特别有用。请参见图 7.14,这是针对马蹄铁数据和我们的四个模型的一个示例。

图 7.14:四个模型在马蹄铁数据上的后验预测检查,附带根图
在悬挂的根图中,我们绘制了观察值和预测值的平方根。这是一种快速的方式,用于大致调整不同计数之间的尺度差异。换句话说,它使得即使是低频率的观察值和预期频率也能更容易进行比较。其次,观察数据的条形图是从预期值“悬挂”下来的,而不是像在图 7.12 中那样从零开始“生长”。由于条形图是悬挂的,如果条形图没有达到零(虚线灰线),则表示模型对该计数的预测过高;如果条形图低于零,则表示模型对该计数的预测过低。
让我们总结一下图 7.12 中每个子图的内容:
-
Poisson:零值被低估,1 到 4 的计数被高估。6 及以后的大部分计数也被低估。这个模式表明数据中存在过度离散,而 0 的巨大差异表明零值过多。
-
NegativeBinomial:我们可以看到,与 Poisson 模型相比,过度离散得到了更好的处理。我们仍然看到零值被低估,计数 1 和 2 被高估,这可能表明零值过多。
-
Hurdle Poisson:正如预期的那样,对于一个 Hurdle 模型,我们对零值的拟合非常完美。对于正值,我们仍然会看到一些偏差。
-
Hurdle NegativeBinomial:我们可以看到,该模型能够很好地拟合数据,对于大多数计数,偏差非常小。
7.6 混合模型和聚类
聚类或聚类分析是将对象分组的任务,目的是使得同一组中的对象彼此之间比与其他组的对象更接近。这些组被称为簇,接近程度可以通过多种不同的方式计算,例如使用度量,如欧几里得距离。如果我们选择概率方法,那么混合模型作为解决聚类任务的自然候选者出现。
使用概率模型进行聚类通常被称为基于模型的聚类。使用概率模型可以计算每个数据点属于每个簇的概率。这被称为软聚类,而不是硬聚类,在硬聚类中,每个数据点要么属于一个簇,概率为 0 或 1。我们可以通过引入一些规则或边界将软聚类转化为硬聚类。事实上,你可能还记得,这正是我们将逻辑回归转化为分类方法时所做的操作,其中我们使用 0.5 作为默认边界值。对于聚类,合理的选择是将数据点分配给概率最高的簇。
总结来说,当人们谈论聚类时,通常是在谈论将对象分组,而当人们谈论混合模型时,他们是在谈论使用简单分布的混合来建模更复杂的分布,目的是识别子群体或仅仅为了拥有一个更灵活的模型来描述数据。
7.7 非有限混合模型
对于一些问题,例如尝试对手写数字进行聚类,容易确定我们期望在数据中找到的组数。对于其他问题,我们可以有很好的猜测;例如,我们可能知道我们的鸢尾花样本来自一个只有三种鸢尾花生长的区域,因此使用三个成分是一个合理的起点。当我们不确定成分的数量时,我们可以使用模型选择来帮助我们选择组数。然而,对于其他问题,事先选择组数可能是一个缺点,或者我们可能更感兴趣的是直接从数据中估计这个数量。对于这种类型的问题,贝叶斯解决方案与狄利克雷过程相关。
7.7.1 狄利克雷过程
到目前为止我们所看到的所有模型都是参数模型,意味着它们具有固定数量的参数,我们感兴趣的是估计这些参数,如固定数量的聚类。我们也可以有非参数模型。这些模型的更好名称可能是非固定参数模型或具有可变数量参数的模型,但已经有人为我们决定了名称。我们可以将非参数模型看作是具有理论上无限数量参数的模型。在实践中,我们在某种程度上让数据将理论上无限的参数数量减少到有限的数量。由于数据决定了实际的参数数量,非参数模型非常灵活,并且有可能对欠拟合和过拟合具有鲁棒性。在本书中,我们将看到三种此类模型的例子:高斯过程(GP)、贝叶斯加法回归树(BART)和 Dirichlet 过程(DP)。虽然接下来的章节将分别集中讨论 GP 和 BART,但我们当前的重点将是探索 DPs。
由于 Dirichlet 分布是 Beta 分布的 n 维推广,Dirichlet 过程是 Dirichlet 分布的无限维推广。我知道这一开始可能让人感到困惑,所以在继续之前,花点时间重新阅读上一句。
Dirichlet 分布是在概率空间上的一种概率分布,而 DP 是在分布空间上的概率分布。这意味着从 DP 中单次抽样实际上是一个分布。对于有限混合模型,我们使用 Dirichlet 分布来为固定数量的聚类或组分配先验分布。DP 是一种为非固定数量的聚类分配先验分布的方法。我们可以将 DP 看作是从分布的先验分布中进行抽样的一种方法。
在我们进入实际的非参数混合模型之前,让我们花点时间讨论一些 DP 的细节。DP 的正式定义相对晦涩,除非你非常了解概率论,否则不容易理解,因此让我描述一些 DP 的属性,这些属性对于理解其在非有限混合模型中的作用是非常重要的:
-
DP 是一种其实现为概率分布的分布。例如,从高斯分布中,你会抽样数值,而从 DP 中,你会抽样分布。
-
DP 由基础分布
和 α(一个正实数,称为浓度参数)来指定。α 类似于 Dirichlet 分布中的浓度参数。
-
是 DP 的期望值。这意味着 DP 会围绕基础分布生成分布。这在某种程度上等同于高斯分布的均值。
-
随着 α 的增加,实现在分布上的集中度越来越低。
-
在实践中,DP 总是生成离散分布。
-
在极限情况下,α → ∞,DP 的实现将等于基础分布,因此如果基础分布是连续的,DP 将生成一个连续分布。因为这个原因,数学家们说从 DP 生成的分布几乎总是离散的。实际上,alpha 是一个有限数值,因此我们总是使用离散分布。
分布的先验
我们可以将 DP 看作是随机分布 f 的先验,其中基础分布 是我们预期 f 应该是什么,而浓度参数 α 表示我们对先验猜测的信心程度。
为了使这些特性更具体,我们再来看一下 图 7.3 中的分类分布。我们可以通过指明 x 轴上的位置和 y 轴上的高度来完全指定这个分布。对于分类分布,x 轴上的位置被限制为整数,并且高度之和必须等于 1。我们保持最后一个限制,但放宽前一个限制。为了生成 x 轴上的位置,我们将从基础分布 中采样。原则上,它可以是我们想要的任何分布;因此,如果我们选择高斯分布,位置将是实数线上的任意值。相反,如果我们选择 Beta 分布,位置将限制在区间 [0, 1] 内,如果我们选择泊松分布作为基础分布,位置将限制为非负整数 0, 1, 2, ….
到目前为止都很好,但我们如何选择 y 轴上的值呢?我们遵循一个被称为“想象实验”(Gedanken experiment)的过程,称为“断棒过程”。假设我们有一根长度为 1 的棒子,然后我们把它分成两部分(不一定相等)。我们把一部分放一边,接着把另一部分再分成两部分,然后我们就这样不断地做下去,永远做下去。实际上,由于我们不能真正无限地重复这个过程,我们在某个预定义的值 K 处截断它,但这个基本思路至少在实践中是成立的。为了控制断棒过程,我们使用一个参数 α。当我们增大 α 的值时,我们会把棒子分成越来越小的部分。因此,当 α = 0 时,我们不分棒子,而当 α = ∞ 时,我们会把它分成无限多的部分。图 7.15 展示了从 DP 中抽取的四个样本,分别对应四个不同的 α 值。

图 7.15:以高斯分布为基础的断棒过程
从图 7.15中我们可以看到,DP 是一个离散分布。当α增大时,我们从初始单位长度的棒中获得较小的块;注意 y 轴刻度的变化。基础分布(在此图中为 Normal(0, 1))控制位置。随着α的增加,棒逐渐更多地呈现出基础分布的特征。在本章的配套笔记本中,你将找到生成图 7.15的代码。我强烈建议你玩一下这段代码,以便更好地理解 DP 的直觉。
图 7.1 显示了,如果你在每个数据点上放置一个高斯分布,然后将所有高斯分布相加,你可以近似数据的分布。我们可以使用 DP 做类似的事情,但不是在每个数据点上放置一个高斯分布,而是在每个原始单位长度的棒的的位置上放置一个高斯分布。然后我们通过每块棒的长度来加权每个高斯分布。这个过程为非有限高斯混合模型提供了一般的方法。
或者,我们可以用任何其他分布替代高斯分布,这样我们就得到了一个非有限混合模型的一般方法。图 7.16 展示了这种模型的一个示例。我使用了拉普拉斯分布的混合模型,这只是为了强调你绝不局限于仅使用高斯混合模型:

图 7.16:使用 DP 的拉普拉斯混合模型
现在我们已经准备好尝试在 PyMC 中实现 DP。让我们首先定义一个与 PyMC 兼容的stick_breaking函数:
代码 7.7
K = 10
def stick_breaking(*α*, K):
*β* = pm.Beta('*β*', 1., *α*, shape=K)
w = *β* * pt.concatenate([[1.], pt.extra_ops.cumprod(1\. - *β*)[:-1]]) + 1E-6
return w/w.sum()
我们不是固定α(浓度参数)的值,而是为其定义一个先验。一个常见的选择是 Gamma 分布,如下代码块所示:
代码 7.8
with pm.Model() as model_DP:
*α* = pm.Gamma('*α*', 2, 1)
w = pm.Deterministic('w', stick_breaking(*α*, K))
means = pm.Normal('means',
mu=np.linspace(cs_exp.min(), cs_exp.max(), K),
sigma=5, shape=K,
transform=pm.distributions.transforms.ordered,
)
sd = pm.HalfNormal('sd', sigma=10, shape=K)
obs = pm.NormalMixture('obs', w, means, sigma=sd, observed=cs_exp.values)
idata = pm.sample()
因为我们是通过截断的 stick-breaking 过程来近似无限 DP,因此重要的是检查截断值(在此例中为K = 10)是否引入了任何偏差。一种简单的方法是计算每个组件的平均权重,对其进行排序,然后绘制其累积和。为了保险起见,我们应该确保至少有一些组件的权重可以忽略不计;否则,我们必须增加截断值。此类图的示例见图 7.17。

图 7.17:DP 组件平均权重的有序累积分布
我们可以看到,只有前 7 个组件在某种程度上是重要的。前 7 个组件占总权重的 99.9%以上(在图 7.17中的灰色虚线),因此我们可以确信选择的上限值(K = 10)对于这组数据来说足够大。
图 7.18 显示了使用 DP 模型估计的平均密度(黑线),以及从后验样本中获得的样本(灰线),以反映估计的不确定性。

图 7.18:用于化学位移数据的 DP 混合模型
7.8 连续混合
本章的重点是离散混合模型,但我们也可以有连续混合模型。事实上,我们已经知道其中一些。例如,层次模型也可以解释为连续混合模型,其中每个组的参数来自上层的连续分布。为了更具体一点,想象一下对多个组进行线性回归。我们可以假设每个组都有自己的斜率,或者所有组共享相同的斜率。或者,与其将问题框架设置为两个极端的离散选项,层次模型让我们能够有效地建模这两种选项的连续混合。
7.8.1 一些常见分布是混合分布
BetaBinomial 是一种离散分布,通常用于描述在每次试验成功的概率p未知并假设服从参数α和β的 Beta 分布的情况下,进行n次伯努利试验时成功的次数y:

也就是说,为了找到观察到结果y的概率,我们需要对所有可能的(且连续的)p值进行平均。因此,BetaBinomial 可以被视为一个连续混合模型。如果 BetaBinomial 模型对你来说听起来很熟悉,那是因为你在书的前两章时已经注意到了!这是我们用于掷硬币问题的模型,尽管我们明确使用了 Beta 和 Binomial 分布,而不是使用已经混合的 Beta-Binomial 分布。
类似地,我们有负二项分布,它可以被理解为伽马-泊松混合模型。也就是说,这是泊松分布的混合,其中速率参数服从伽马分布。负二项分布通常用于解决处理计数数据时常遇到的一个问题。这个问题被称为过度离散。假设你使用泊松分布来建模计数数据,然后你意识到数据中的方差超过了模型的方差;使用泊松分布的一个问题是,均值和方差由同一个参数描述。解决过度离散的一种方法是将数据建模为泊松分布的(连续)混合。通过考虑分布的混合,我们的模型具有更大的灵活性,能够更好地适应数据的均值和方差。
另一种分布混合的例子是 Student's t 分布。我们将该分布介绍为高斯分布的稳健替代。在这种情况下,t 分布是由均值μ且方差未知的高斯分布的混合所产生的,该方差服从 InverseGamma 分布。
7.9 总结
许多问题可以描述为由不同子群体组成的整体群体。当我们知道每个观察值属于哪个子群体时,就可以将每个子群体特定地建模为一个独立的群体。然而,许多时候我们无法直接获得这些信息,因此使用混合模型来建模这些数据可能是合适的。我们可以利用混合模型来尝试捕捉数据中的真实子群体,或者作为一种通用的统计技巧,通过将简单分布组合来建模复杂的分布。
在本章中,我们将混合模型分为三类:有限混合模型、非有限混合模型和连续混合模型。有限混合模型是由两个或多个分布的有限加权混合组成,每个分布或组分代表数据的一个子群体。原则上,组分可以是我们认为有用的任何东西,从简单的分布(如高斯分布或泊松分布)到更复杂的对象(如层次模型或神经网络)。从概念上讲,为了解决混合模型,我们所需要做的就是将每个数据点正确地分配给一个组分。我们可以通过引入潜在变量z来实现这一点。我们为z使用类别分布,这是一种最通用的离散分布,具有狄利克雷先验,它是 Beta 分布的 n 维推广。对离散变量z进行采样可能会出现问题,因此将其边缘化可能更为方便。PyMC 包括一个正态混合分布和一个执行此边缘化的混合分布,这使得使用 PyMC 构建混合模型变得更加简单。
在本章中,我们研究混合模型时遇到的一个常见问题是,这种模型可能会导致标签交换问题,这是一种非可识别性问题。解决非可识别性的一种方法是强制将各个组分进行排序。有限混合模型的一个挑战是如何确定组分的数量。一种解决方案是对一组模型进行模型比较,基于估计的组分数量来选择最合适的模型。这一估计应当尽可能地借助我们对当前问题的理解。另一个选择是尝试从数据中自动估计组分的数量。为此,我们引入了狄利克雷过程的概念,作为狄利克雷分布的无限维版本,借此我们可以构建一个非参数的混合模型。
最后,为了结束本章,我们简要讨论了许多模型(例如 Beta 二项分布(用于硬币抛掷问题)、负二项分布、学生 t 分布,甚至层次模型)如何被解释为连续混合模型。
7.10 习题
-
从 3 个高斯分布的混合中生成合成数据。请查阅本章附带的 Jupyter 笔记本,了解如何执行此操作。拟合一个具有 2、3 或 4 个组分的有限高斯混合模型。
-
使用 LOO 比较练习 1 的结果。
-
阅读并运行以下关于混合模型的 PyMC 文档示例:
-
使用负二项分布和障碍负二项分布模型重新拟合
fish_data。使用根图比较这两个模型与本章展示的零膨胀泊松模型。 -
使用狄利克雷过程重复练习 1。
-
假设你暂时不知道鸢尾花数据集的正确物种/标签,使用混合模型将三个鸢尾花物种进行聚类,选择一个特征(例如花萼的长度)。
-
重复练习 6,但这次使用两个特征。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,与超过 5000 名成员一起学习,网址:packt.link/bayesian

第八章
高斯过程
孤单吗?你有你自己。你无限的自己。- Rick Sanchez(至少是 C-137 维度中的那个)
在上一章中,我们学习了狄利克雷过程,这是一种狄利克雷分布的无限维推广,可以用来对未知的连续分布设定先验。在本章中,我们将学习高斯过程,这是一种高斯分布的无限维推广,可以用来对未知的函数设定先验。狄利克雷过程和高斯过程都用于贝叶斯统计中,构建灵活的模型,在这些模型中,参数的数量可以随着数据量的增加而增加。
我们将讨论以下主题:
-
作为概率对象的函数
-
核函数
-
具有高斯似然的高斯过程
-
非高斯似然下的高斯过程
-
希尔伯特空间中的高斯过程
8.1 线性模型与非线性数据
在第四章和第六章中,我们学习了如何构建以下形式的模型:

这里,θ是某个概率分布的参数,例如,高斯分布的均值,二项分布的p参数,泊松分布的速率,等等。我们称
为逆链接函数,
是我们用来潜在地变换数据的其他函数,比如平方根、多项式函数,或者其他形式。
拟合或学习一个贝叶斯模型可以看作是寻找权重β的后验分布,因此这被称为近似函数的权重视角。正如我们在多项式回归和样条回归中看到的那样,通过让
成为一个非线性函数,我们可以将输入映射到特征空间中。我们还看到,通过使用适当阶数的多项式,我们可以完美地拟合任何函数。但除非我们应用某种形式的正则化,例如,使用先验分布,否则这将导致记忆数据的模型,换句话说,模型的泛化能力非常差。我们还提到,样条回归可以像多项式一样灵活,但具有更好的统计属性。现在,我们将讨论高斯过程,它为通过有效地让数据决定函数复杂度来建模任意函数提供了一个有原则的解决方案,同时避免或至少最小化过拟合的可能性。
以下章节从一个非常实际的角度讨论高斯过程;我们避免了涉及几乎所有相关的数学内容。对于更正式的解释,你可以阅读 Rasmussen 和 Williams 的《Gaussian Processes for Machine Learning》[2005]。
8.2 函数建模
我们将通过首先描述一种将函数表示为概率对象的方法来开始讨论高斯过程。我们可以将一个函数 f 视为从输入集合 X 到输出集合 Y 的映射。因此,我们可以写成:

表示函数的一种非常粗略的方式是列出每个 x[i] 值对应的 y[i] 值,如 表 8.1 所示。你可能还记得这种表示函数的方式,它来自小学阶段。
| x | y |
|---|---|
| 0.00 | 0.46 |
| 0.33 | 2.60 |
| 0.67 | 5.90 |
| 1.00 | 7.91 |
表 8.1:一种函数的表格表示(某种程度上)
作为一般情况,X 和 Y 的值将位于实数线上;因此,我们可以将函数视为一个(可能)无限且有序的值对列表 (x[i],y[i])。顺序很重要,因为如果我们打乱这些值的顺序,得到的将是不同的函数。
根据这个描述,我们可以数值地表示我们想要的任何特定函数。但是,如果我们想要概率性地表示函数怎么办呢?那么,我们就需要编码一个概率映射。让我解释一下;我们可以让每个值都是一个具有某种关联分布的随机变量。由于使用高斯分布通常很方便,我们可以假设它们服从具有给定均值和方差的高斯分布。这样,我们就不再描述单一的特定函数,而是描述一个分布族。
为了使讨论更加具体,我们将使用一些 Python 代码来构建并绘制两个这样的函数示例:
代码 8.1
np.random.seed(42)
x = np.linspace(0, 1, 10)
y = np.random.normal(0, 1, len(x))
plt.plot(x, y, 'o-', label='the first one')
y = np.zeros_like(x)
for i in range(len(x)):
y[i] = np.random.normal(y[i-1], 1)
plt.plot(x, y, 'o-', label='the second one')
plt.legend()

图 8.1:从高斯分布中采样的两个虚拟函数
图 8.1 显示了使用来自高斯分布的样本来编码函数并非那么疯狂或愚蠢,所以我们可能走在正确的道路上。然而,用于生成 图 8.1 的方法是有限的,并且不够灵活。
虽然我们期望实际的函数具有某种结构或模式,但我们表示 第一个 函数的方式并没有让我们编码数据点之间的任何关系。实际上,每个点都是完全独立的,因为我们只是从一个共同的一维高斯分布中随机抽取了 10 个独立的样本。对于 第二个 函数,我们引入了一些依赖关系。点 y[i+1] 的均值是 y[i],因此我们这里有一定的结构。然而,我们将在接下来的内容中看到,捕捉依赖关系有一种更为通用的方法,而不仅仅是连续点之间的依赖。
在继续之前,让我停下来思考一下,为什么我们使用高斯分布而不是其他概率分布。首先,通过将自己限制为只使用高斯分布,我们在指定不同形状的函数时不会失去任何灵活性,因为每个点可能有自己的均值和方差。其次,从数学的角度来看,使用高斯分布是非常方便的。
8.3 多元高斯和函数
在图 8.1中,我们将一个函数表示为从一维高斯分布中采样得到的集合。另一种选择是使用 n 维多元高斯分布,得到一个长度为n的样本向量。实际上,你可以尝试重现图 8.1,但将np.random.normal(0, 1, len(x))替换为np.random.multivariate_normal,均值为np.zeros_like(x),标准差为np.eye(len(x))。使用多元正态分布的优势在于,我们可以利用协方差矩阵来编码有关函数的信息。例如,通过将协方差矩阵设置为np.eye(len(x)),我们表明我们在评估函数的 10 个点中,每个点的方差为 1。我们还表明它们之间的方差,即协方差为 0。换句话说,它们是独立的。如果我们将这些零替换为其他数值,我们可以得到描述不同故事的协方差。
我希望你已经开始相信使用多元高斯来表示函数是可能的。如果是这样,那么我们只需要找到一个合适的协方差矩阵。这也是下一节的主题。
8.3.1 协方差函数和核函数
在实践中,协方差矩阵是通过称为核函数的函数来指定的。不幸的是,术语“核”是一个多义词,甚至在统计学文献中也是如此。定义核函数的一种简单方法是,任何返回有效协方差矩阵的函数都可以称为核。但这个定义是自我重复的,且不太直观。一个更具概念性和实用性的定义是,核函数定义了输入空间中数据点之间的相似度度量,而这种相似度决定了一个数据点对预测另一个数据点值的影响程度。
有许多有用的核函数,一个流行的核函数是指数二次核:

在这里,∥X − X′∥²是平方欧几里得距离:

对于这个核函数,我们可以看到它是一个对称函数,接受两个输入,如果输入相同则返回 0,否则返回正值。因此,我们可以将指数二次核的输出解释为两个输入之间的相似度度量。
乍一看可能不太明显,但指数二次核的公式与高斯分布非常相似。正因为如此,这个核函数也被称为高斯核函数。术语ℓ被称为长度尺度(或带宽或方差),它控制着核函数的宽度。换句话说,它控制着X值在什么尺度下被认为是相似的。
为了更好地理解核函数的作用,我建议你动手尝试它们。例如,定义一个 Python 函数来计算指数二次核函数:
代码 8.2
def exp_quad_kernel(x, knots, ℓ=1):
"""exponentiated quadratic kernel"""
return np.array([np.exp(-(x-k)**2 / (2*ℓ**2)) for k in knots])
图 8.2 展示了在不同输入下 4 × 4 协方差矩阵的样子。我选择的输入非常简单,包含了值 [−1,0,1,2]。

图 8.2:输入值(左),协方差矩阵(右)
在 图 8.2 的左面板中,我们展示了输入值。这些是 x 轴上的值,我们将点从 0 标记到 3。因此,点 0 取值 -1,点 1 取值 0,以此类推。在右面板中,我们展示了使用指数二次核函数计算得到的协方差矩阵的热力图。颜色越浅,协方差值越大。如你所见,热力图是对称的,且对角线上的值最大。当我们意识到协方差矩阵中每个元素的值与点之间的距离成反比时,这一点是有意义的,对角线是通过将每个数据点与自身进行比较得到的。最小的值出现在点 0 和点 3,因为它们是最远的两个点。
一旦你理解了这个例子,你应该尝试使用其他输入。请参考本章末的练习 1 以及附带的笔记本(github.com/aloctavodia/BAP3)进行进一步练习。
现在我们更好地理解了如何使用核函数生成协方差矩阵,接下来让我们更进一步,使用协方差矩阵来采样函数。如你在 图 8.3 中所见,高斯核函数意味着一系列不同的函数,而参数 ℓ 控制函数的平滑度。ℓ 的值越大,函数越平滑。

图 8.3:四个 ℓ 值的高斯核函数的实现(每个 ℓ 值对应两个实现)
你告诉我你的朋友,我就能告诉你你的未来
核函数将数据点沿 x 轴的距离转化为期望函数值(y 轴)的协方差值。因此,数据点在 x 轴上越接近,我们期望它们在 y 轴上的值越相似。
8.4 高斯过程
现在我们已经准备好理解什么是高斯过程(GPs)以及它们在实践中的应用。根据维基百科,高斯过程的一个正式定义如下:
“由时间或空间索引的随机变量的集合,使得这些随机变量的每个有限集合都具有多元正态分布,即它们的每个有限线性组合服从正态分布。”
这个定义可能并不是很有用,至少在你当前的学习阶段不太有用。理解高斯过程的诀窍在于意识到,高斯过程的概念是一个心理(和数学)框架,因为在实际应用中,我们并不需要直接处理这个无限维的数学对象。相反,我们只在有数据的点上评估高斯过程。通过这样做,我们将无限维的高斯过程压缩成一个有限维的多元高斯分布,维度数与数据点的数量相等。从数学上讲,这种压缩是通过对无限未观察到的维度进行边际化来实现的。理论告诉我们,除了我们观察到的点,忽略(实际上是边际化)所有其他点是可以的。它还保证我们总是会得到一个多元高斯分布。因此,我们可以严格地解释图 8.3 为高斯过程的实际样本!
到目前为止,我们专注于多元正态分布的协方差矩阵,并没有讨论均值。在使用高斯过程时,将多元高斯的均值设置为 0 是常见做法,因为高斯过程足够灵活,可以将均值建模得非常好。但请注意,这样做并没有限制。实际上,对于某些问题,你可能希望参数化地建模均值,而将高斯过程用来建模残差。
高斯过程是对函数的先验
高斯过程是对函数的先验分布,使得在你评估函数的每个点时,它会在该点放置一个具有给定均值和方差的高斯分布。在实践中,高斯过程通常是通过使用核函数来构建的,核函数将 x 轴上的距离转化为 y 轴上的相似度。
8.5 高斯过程回归
假设我们可以将一个变量Y建模为X的函数f加上一些高斯噪声:

如果f是X的线性函数,那么这个假设本质上与我们在第四章讨论简单线性回归时使用的假设相同。在本章中,我们将通过对f设置先验来使用一个更一般的表达式。通过这种方式,我们将能够得到比线性更复杂的函数。如果我们决定使用高斯过程作为这个先验,那么我们可以写成:

这里,表示一个均值函数μ[X]和协方差函数K(X,X′)的高斯过程。尽管在实践中,我们总是处理有限对象,但我们使用函数这个词来表示从数学上讲,均值和协方差是无限对象。
我之前提到过,使用高斯分布非常方便。例如,如果先验分布是一个高斯过程,而似然是高斯分布,那么后验分布也是高斯过程,并且我们可以解析地计算它。此外,使用高斯似然的好处是我们可以边际化高斯过程,这大大减少了我们需要从中抽样的参数空间的大小。PyMC 中的高斯过程模块利用了这一点,并为高斯和非高斯似然提供了不同的实现方式。在接下来的部分中,我们将探索这两者。
8.6 使用 PyMC 进行高斯过程回归
图 8.4中的灰色线是一个正弦函数。我们假设我们不知道这个函数,而是只有一组数据点(点)。然后我们使用高斯过程来逼近生成这些数据点的函数。

图 8.4:从已知函数(线)生成的合成数据(点)
高斯过程在 PyMC 中实现为一系列 Python 类,这些类与我们之前见过的模型稍有不同;然而,代码依然非常PyMConic。我在接下来的代码中添加了一些注释,帮助你理解在 PyMC 中定义高斯过程的关键步骤。
代码 8.3
# A one-dimensional column vector of inputs.
X = x[:, None]
with pm.Model() as model_reg:
# hyperprior for lengthscale kernel parameter
ℓ = pm.InverseGamma("ℓ", 7, 17)
# instanciate a covariance function
cov = pm.gp.cov.ExpQuad(1, ls=ℓ)
# instanciate a GP prior
gp = pm.gp.Marginal(cov_func=cov)
σ = pm.HalfNormal('σ', 25)
# Class representing that the observed data is a GP plus Gaussian noise
y_pred = gp.marginal_likelihood('y_pred', X=X, y=y, sigma=σ)
idata_reg = pm.sample()
请注意,我们没有使用高斯似然,而是使用了gp.marginal_likelihood方法。这个方法利用了后验分布有封闭形式这一事实,正如前面部分所解释的那样。
好的,现在我们已经计算了后验分布,接下来让我们看看如何得到均值拟合函数的预测。我们可以通过使用gp.conditional计算在新输入位置上评估的条件分布来实现这一点。
代码 8.4
X_new = np.linspace(np.floor(x.min()), np.ceil(x.max()), 100)[:,None]
with model_reg:
f_pred = gp.conditional('f_pred', X_new)
结果,我们得到了一个新的 PyMC 随机变量f_pred,我们可以用它来从后验预测分布中获取样本:
代码 8.5
with model_reg:
idata_subset = idata_reg.sel(draw=slice(0, None, 100))
pred_samples = pm.sample_posterior_predictive(idata_subset,
var_names=["f_pred"])
f_pred = (pred_samples.
posterior_predictive.stack(samples=("chain", "draw"))['f_pred'].
values)
现在我们可以在原始数据上绘制拟合函数,以直观检查它们与数据的拟合程度以及我们预测的相关不确定性。正如我们在第四章中对线性模型所做的那样,我们将展示几种不同的方式来绘制相同的结果。图 8.5展示了拟合函数的线条。

图 8.5:线条表示从model_reg的后验均值中抽取的样本
或者,我们可以使用辅助函数pm.gp.util.plot_gp_dist来获得如图 8.6所示的漂亮图表。在这个图表中,每条带状线表示不同的百分位数,从 99 百分位(浅灰色)到 51 百分位(深灰色)。

图 8.6:使用plot_gp_dist函数绘制的从model_reg的后验样本
另一种选择是计算在参数空间中给定点处的条件分布的均值向量和标准差。在图 8.7中,我们使用均值(基于轨迹中的样本)来表示ℓ和
。我们可以使用gp.predict方法来计算均值和方差。

图 8.7:model_reg的后验均值,带有 1 和 2 标准差的带宽
正如我们在第 4章中看到的,我们可以使用一个具有非高斯似然度和适当逆链接函数的线性模型来扩展有用线性模型的范围。对于高斯过程(GP),我们也可以做同样的事情。例如,我们可以使用一个带有指数逆链接函数的泊松似然度。对于这样的模型,后验分布不再是解析可解的,但我们仍然可以使用数值方法来近似计算。在接下来的部分中,我们将讨论这些类型的模型。
8.6.1 设置长度尺度的先验
对于长度尺度参数,避免为零的先验通常效果更好。正如我们之前看到的,ℓ控制着函数的平滑度,因此,ℓ为 0 意味着函数不平滑;我们会得到类似于图 8.1中的“第一个”函数。但更重要的原因是,对于大于 0 但仍低于协变量最小间距的ℓ值,可能会出现一些不良效应。本质上,在该点以下,似然度无法区分不同的长度尺度,因此它们都同样好。这是一个不可识别性问题。因此,我们将得到一个倾向于过拟合并且准确插值输入数据的高斯过程(GP)。此外,MCMC 采样器会遇到更多困难,可能会出现更长的采样时间或简单的不可靠样本。类似的情况发生在数据范围之外。如果数据范围为 10 且ℓ> = 10,这意味着函数是平坦的。再往后,您(以及似然度)无法区分不同的参数值。因此,即使您不确定函数是多么平滑或起伏,您仍然可以设置一个先验,避免极低或极高的ℓ值。例如,为了获得先验pm.InverseGamma("ℓ", 7, 17),我们请求 PreliZ 提供一个最大熵先验,其中 0.95 的质量分布在 1 到 5 之间:
代码 8.6
pz.maxent(pz.InverseGamma(), 1, 5, 0.95)
逆伽马分布(InverseGamma)是一个常见的选择。与伽马分布(Gamma)类似,它允许我们设置一个避免为 0 的先验,但与伽马分布不同的是,逆伽马分布在接近 0 时有一个较轻的尾部,换句话说,它为小值分配的质量较少。
本章剩余部分,我们将使用函数get_ig_params从协变量的尺度中获取弱信息先验。你可以在附带的代码中找到详细信息(github.com/aloctavodia/BAP3),但本质上,我们使用 PreliZ 中的maxent函数,将大部分先验质量设置在一个与协变量范围兼容的区间内。
8.7 高斯过程分类
在第四章中,我们看到了如何使用线性模型来分类数据。我们使用了伯努利似然和逻辑逆链接函数。然后,我们应用了边界决策规则。在这一节中,我们将做同样的事情,但这次使用 GP 而不是线性模型。就像我们在第四章中的model_lrs一样,我们将使用包含两类setosa和versicolor、一个预测变量sepal length的鸢尾花数据集。
对于这个模型,我们不能使用pm.gp.Marginal类,因为该类仅限于高斯似然,因为它利用了 GP 先验与高斯似然组合的数学可处理性。相反,我们需要使用更通用的pm.gp.Latent类。
代码 8.7
with pm.Model() as model_iris:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(x_1))
cov = pm.gp.cov.ExpQuad(1, ℓ)
gp = pm.gp.Latent(cov_func=cov)
f = gp.prior("f", X=X_1)
# logistic inverse link function and Bernoulli likelihood
y_ = pm.Bernoulli("y", p=pm.math.sigmoid(f), observed=y)
idata_iris = pm.sample()
如我们所见,图 8.8看起来与图 4.11非常相似。请花些时间对比这两张图。

图 8.8:逻辑回归,model_lrs的结果
你可能已经注意到,推断出的函数看起来类似于 sigmoid 曲线,除了在较低的sepal_length值时尾部向上,较高的sepal_length值时尾部向下。为什么会这样呢?因为当数据很少或没有数据时,GP 后验倾向于恢复到 GP 先验。如果我们考虑到在没有数据的情况下,后验基本上变成了先验,这就很有意义。
如果我们唯一关注的是决策边界,那么尾部的行为可能是无关紧要的。但如果我们想要建模在不同sepal_length值下属于 setosa 或 versicolor 的概率,我们应该做一些事情来改善尾部的模型。实现这一目标的一种方法是为高斯过程添加更多结构。高斯过程的一个非常好的特点是,我们可以组合协方差函数。因此,对于下一个模型,我们将组合三个核函数:指数二次核、线性核和白噪声核。
线性核的效果是在数据边界处使尾部趋近于 0 或 1。此外,我们使用白噪声核作为稳定协方差矩阵计算的一种技巧。高斯过程的核函数受到限制,以保证生成的协方差矩阵是正定的。然而,数值误差可能导致违反这一条件。这种问题的表现之一是,在计算拟合函数的后验预测样本时会得到 NaN。缓解此错误的一种方法是通过添加噪声来稳定计算。事实上,PyMC 在后台已经做了类似的处理,但有时需要更多的噪声,如下面的代码所示:
代码 8.8
with pm.Model() as model_iris2:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(x_1))
c = pm.Normal('c', x_1.min())
τ = pm.HalfNormal('τ', 5)
cov = (pm.gp.cov.ExpQuad(1, ℓ) +
τ * pm.gp.cov.Linear(1, c) +
pm.gp.cov.WhiteNoise(1E-5))
gp = pm.gp.Latent(cov_func=cov)
f = gp.prior("f", X=X_1)
# logistic inverse link function and Bernoulli likelihood
y_ = pm.Bernoulli("y", p=pm.math.sigmoid(f), observed=y)
idata_iris2 = pm.sample()
我们可以在图 8.9中看到这个模型的结果。注意,这个图看起来现在与图 4.11相比,更加相似,而不是图 8.8。

图 8.9:逻辑回归,model_lrs的结果
本节讨论的示例有两个主要目的:
-
显示如何轻松地将核函数组合在一起,以获得更具表现力的模型
-
显示如何使用高斯过程恢复逻辑回归
关于第二点,逻辑回归确实是高斯过程的一个特例,因为简单的线性回归只是高斯过程的一个特例。事实上,许多已知的模型可以看作是高斯过程的特例,或者至少它们与高斯过程有某种联系。如果你想了解更多,可以阅读 Kevin Murphy 的《机器学习:一种概率视角》第一版第十五章[Murphy,2012],以及第二版的第十八章[Murphy,2023]。
8.7.1 高斯过程用于空间流感
实际上,使用高斯过程来建模我们可以通过逻辑回归解决的问题并没有太大意义。相反,我们希望使用高斯过程来建模那些无法通过不太灵活的模型很好捕捉的更复杂的数据。例如,假设我们想要将患病的概率作为年龄的函数进行建模。结果表明,年轻人和年老的人比中年人有更高的风险。数据集space_flu.csv是一个受上述描述启发的合成数据集。图 8.10展示了它的图形。
让我们拟合以下模型并绘制结果:
代码 8.9
with pm.Model() as model_space_flu:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(age))
cov = pm.gp.cov.ExpQuad(1, ℓ) + pm.gp.cov.WhiteNoise(1E-5)
gp = pm.gp.Latent(cov_func=cov)
f = gp.prior('f', X=age)
y_ = pm.Bernoulli('y', p=pm.math.sigmoid(f), observed=space_flu)
idata_space_flu = pm.sample()
请注意,如图 8.10所示,高斯过程可以很好地拟合这个空间流感数据集,即使数据要求函数比逻辑回归更复杂。对于简单的逻辑回归来说,拟合这些数据几乎是不可能的,除非我们做一些特殊的修改来帮助它(有关这种修改的讨论请参见本章末的练习 6)。

图 8.10:逻辑回归,model_space_flu的结果
8.8 Cox 过程
现在我们要建模计数数据。我们将看到两个例子;一个是具有时间变化率的,另一个是具有二维空间变化率的。为此,我们将使用泊松似然,并通过高斯过程来建模该比率。由于泊松分布的比率限制为正值,我们将使用指数作为逆链接函数,正如我们在第四章中的负二项回归中所做的那样。
我们可以将泊松过程看作是在给定空间中点集的分布,其中每个有限的点集都是泊松分布。当泊松过程的比率本身是一个随机过程时,例如一个高斯过程,那么我们就有了 Cox 过程。
8.8.1 煤矿灾难
第一个例子被称为煤矿灾难。这个例子记录了英国从 1851 年到 1962 年的煤矿灾难数据。灾难的数量被认为受到了这一时期安全法规变化的影响。我们希望将灾难的发生率建模为时间的函数。我们的数据集包含一列数据,每条数据对应一次灾难发生的时间。我们将用于拟合数据的模型形式如下:
如您所见,这是一个泊松回归问题。此时,您可能会想,我们如何仅凭一个只有灾难日期的单列数据来进行回归分析。答案是将数据离散化,就像我们构建直方图一样。我们将使用箱子的中心作为X变量,箱子内的计数作为Y变量:
代码 8.10
# discretize data
years = int((coal_df.max() - coal_df.min()).iloc[0])
bins = years // 4
hist, x_edges = np.histogram(coal_df, bins=bins)
# Compute the location of the centers of the discretized data
x_centers = x_edges[:-1] + (x_edges[1] - x_edges[0]) / 2
# xdata needs to be 2D for BART
x_data = x_centers[:, None]
# express data as the rate number of disasters per year
y_data = hist
现在我们使用 PyMC 来定义并求解模型:
代码 8.11
with pm.Model() as model_coal:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(x_edges))
cov = pm.gp.cov.ExpQuad(1, ls=ℓ) + pm.gp.cov.WhiteNoise(1E-5)
gp = pm.gp.Latent(cov_func=cov)
f = gp.prior('f', X=x_data)
y_pred = pm.Poisson('y_pred', mu=pm.math.exp(f), observed=y_data)
idata_coal = pm.sample()
图 8.11 显示了灾难率随时间变化的中位数(白线)。带状区域描述了 50%的 HDI(较暗)和 94%的 HDI(较亮)。底部的黑色标记表示每次灾难发生的时刻。正如我们所看到的,事故率随着时间的推移而下降,除了最初的短暂上升。PyMC 文档包括了煤矿灾难的案例,但从不同的角度进行建模。我强烈建议您查看这个例子,因为它本身非常有用,并且与我们刚刚实现的方法做对比也很有价值。

图 8.11:逻辑回归,model_coal的结果
请注意,即使我们对数据进行了分箱,结果仍然是一个平滑的曲线。从这个角度来看,我们可以将 model_coal(以及一般而言,这种类型的模型)视为构建直方图后进行平滑处理。
8.8.2 红木
让我们将刚才使用的方法应用于一个二维空间问题。我们将使用如图 8.12所示的红木数据集。该数据集(采用 GPL 许可证分发)来自 GPstuff 包。数据集包含了某一地区红木树的位置。推断的动机是获得一个速率图,表示某一地区内树木的数量。
与煤矿灾难示例类似,我们需要对数据进行离散化处理:
代码 8.12
# discretize spatial data
bins = 20
hist, x1_edges, x2_edges = np.histogram2d(
rw_df[1].values, rw_df[0].values, bins=bins)
# compute the location of the centers of the discretized data
x1_centers = x1_edges[:-1] + (x1_edges[1] - x1_edges[0]) / 2
x2_centers = x2_edges[:-1] + (x2_edges[1] - x2_edges[0]) / 2
# arrange xdata into proper shape for GP
x_data = [x1_centers[:, None], x2_centers[:, None]]
# arrange ydata into proper shape for GP
y_data = hist.flatten().astype(int)

图 8.12:红木数据
请注意,与其做网格化处理,我们将x1和x2数据视为独立的数组。这使我们能够为每个坐标独立地构建协方差矩阵,从而有效地减少计算高斯过程所需的矩阵大小。然后我们使用LatentKron类将两个矩阵结合起来。需要强调的是,这不是一种数值技巧,而是此类矩阵结构的数学特性,因此我们并没有在模型中引入任何近似或误差。我们只是以一种方式表达它,从而实现更快的计算:
代码 8.13
with pm.Model() as model_rw:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(x_data), shape=2)
cov_func1 = pm.gp.cov.ExpQuad(1, ls=ℓ[0])
cov_func2 = pm.gp.cov.ExpQuad(1, ls=ℓ[1])
gp = pm.gp.LatentKron(cov_funcs=[cov_func1, cov_func2])
f = gp.prior('f', Xs=x_data)
y = pm.Poisson('y', mu=pm.math.exp(f), observed=y_data)
idata_rw = pm.sample()
在图 8.13中,灰色的阴影越深,树木的生长速率越高。我们可以想象,我们的目标是找到高生长速率的区域,因为我们可能对木材从火灾中恢复的情况感兴趣,或者我们可能对土壤的一些特性感兴趣,并用树木作为代理。

图 8.13:逻辑回归,model_rw的结果
8.9 带有空间自相关的回归分析
以下示例摘自Statistical Rethinking: A Bayesian Course with Examples in R and STAN, Second Edition by Richard McElreath, Copyright (2020) by Chapman and Hall/CRC. 经 Taylor & Francis Group 授权复制。我强烈推荐阅读这本书,因为你会发现很多像这样的好例子以及非常好的解释。唯一的警告是,这些书中的例子是用 R/Stan 实现的,但别担心并继续采样;你可以在github.com/pymc-devs/pymc-resources资源中找到这些例子的 Python/PyMC 版本。
对于这个示例,我们有 10 个不同的岛屿社会;对于每一个社会,我们都有它们使用的工具数量。一些理论预测,较大的人口比小人口能发展和维持更多的工具。因此,我们有一个回归问题,其中因变量是工具的数量,独立变量是人口。因为工具数量是计数变量,所以我们可以使用泊松分布。此外,我们有充分的理论依据认为,人口的对数比绝对人口大小更合适,因为真正重要的(根据理论)是人口的数量级。
到目前为止,我们所考虑的模型是泊松回归,但这里有个有趣的部分。另一个影响工具数量的重要因素是岛屿社会之间的接触率。将接触率纳入我们模型的一种方法是收集这些社会在历史上接触的频率信息,并创建一个分类变量,如低/高接触率。另一种方法是使用社会之间的距离作为接触率的代理,因为合理的假设是,地理上较近的社会比远离的社会更频繁接触。
工具数量、人口规模和坐标存储在本书 GitHub 仓库中的islands.csv文件里(github.com/aloctavodia/BAP3)。
忽略先验分布,我们将要构建的模型是:
这个模型是一个线性模型加上一个高斯过程(GP)项。我们用线性部分来建模人口对数的影响,用高斯过程项来建模距离/接触率的影响。通过这种方式,我们实际上在有效地纳入技术暴露的相似性度量(从距离矩阵估算)。因此,我们不会假设总数量仅仅是人口的结果,且各社会间相互独立,而是将每个社会的工具数量建模为其空间分布的函数。
关于空间分布的信息是以纬度和经度的形式存在的,但 PyMC 中的核函数假设所有距离都是欧几里得距离。这可能会带来问题。绕过这个问题的最简洁方法可能是使用考虑岛屿处于大致球形地球上的距离。例如,我们可以使用哈弗辛距离,它基于经纬度计算两个点之间的大圆距离。大圆距离是球面上两点之间的最短距离,是沿着球面测量的。为了使用这种距离,我们需要创建一个新的核函数,如下一个代码块所示。如果你对 Python 中的类不太熟悉,你只需要知道我所做的是复制了 PyMC 代码库中的ExpQuad代码,并稍微修改它,创建了一个新的类ExpQuadHaversine。最大变化是添加了函数/方法haversine_distance。
代码 8.14
class ExpQuadHaversine(pm.gp.cov.Stationary):
def __init__(self, input_dims, ls, ls_inv=None, r=6371, active_dims=None):
super().__init__(input_dims, ls=ls, ls_inv=ls_inv, active_dims=active_dims)
self.r = r # earth radius in km
def haversine_distance(self, X):
lat = np.radians(X[:, 0])
lon = np.radians(X[:, 1])
latd = lat[:,None] - lat
lond = lon[:,None] - lon
d = pt.cos(lat[:,None]) * pt.cos(lat)
a = pt.sin(latd / 2)** 2 + d * pt.sin(lond / 2)** 2
c = 2 * pt.arctan2(pt.sqrt(a), pt.sqrt(1 - a))
return self.r * c
def full(self, X, _):
return pt.exp(-0.5 * self.haversine_distance(X)**2)
现在我们已经定义了类ExpQuadHaversine,可以像使用之前的内置核函数那样,使用它来定义协方差矩阵。对于这个模型,我们将引入另一个变化。我们将定义一个参数η。这个参数的作用是对高斯过程在 y 轴方向进行缩放。通常,我们会为高斯过程定义ℓ和η这两个参数。
代码 8.15
with pm.Model() as model_islands:
η = pm.Exponential('η', 2)
ℓ = pm.InverseGamma('ℓ', *get_ig_params(islands_dist))
cov = η * ExpQuadHaversine(2, ls=ℓ)
gp = pm.gp.Latent(cov_func=cov)
f = gp.prior('f', X=X)
*α* = pm.Normal('*α*', 0, 5)
*β* = pm.Normal('*β*', 0, 1)
μ = pm.math.exp(*α* + *β* * log_pop + f)
_ = pm.Poisson('tt_pred', μ, observed=total_tools)
idata_islands = pm.sample()
为了理解协方差函数相对于距离的后验分布,我们可以绘制一些来自后验分布的样本,如 图 8.14 所示。黑色曲线表示每个距离的后验中位数协方差,灰色曲线表示从 ℓ 和 η 的联合后验分布中采样的函数。

图 8.14:空间协方差的后验分布
图 8.14 中的粗黑线表示社会对之间的协方差的后验中位数与距离的关系。我们使用中位数是因为 ℓ 和 η 的分布非常偏斜。我们可以看到,协方差的平均值并不高,而且在大约 2,000 公里时几乎降到 0。细线表示不确定性,我们可以看到存在很大的不确定性。
现在让我们看看根据模型和数据,岛屿社会之间的相关性有多强。为此,我们必须将协方差矩阵转换为相关矩阵。详情请参见附带的代码。图 8.15 显示了均值相关矩阵的热图。

图 8.15:后验均值相关矩阵
从其他观察中脱颖而出的两个观点是,首先,夏威夷非常孤独。这是有道理的,因为夏威夷距离其他岛屿社会非常遥远。其次,我们可以看到马莱库拉(Ml)、提科皮亚(Ti)和圣克鲁兹(SC)之间高度相关。这也是合理的,因为这些社会彼此非常接近,并且它们也有相似数量的工具。
图 8.16 的左面板本质上是一个地图。岛屿社会被表示为它们相对的位置。线条表示社会之间的后验中位数相关性。线条的透明度与相关性的值成正比。

图 8.16:空间协方差的后验分布
在图 8.16的右侧面板中,我们再次展示了后验中位数相关性,但这次是根据对数人口与工具总数的关系来绘制的。虚线表示工具的中位数和 94%的高密度区间(HDI)作为对数人口的函数。在图 8.16的两个面板中,点的大小与每个岛屿社会的人口成正比。注意,马列库拉、提科皮亚和圣克鲁斯之间的相关性描述了它们工具数量相对较少,接近中位数或低于根据其人口预期的工具数量。类似的情况发生在特罗布里安群岛和马努斯岛;它们地理位置接近,且工具数量低于预期。汤加的工具数量远高于其人口预期,并且与斐济有较高的相关性。从某种程度上讲,模型告诉我们,汤加对斐济的影响是积极的,增加了工具的总数,同时抵消了对其邻近地区马列库拉、提科皮亚和圣克鲁斯的影响。
8.10 希尔伯特空间高斯过程(HSGP)
高斯过程可能会比较慢。主要原因是它们的计算需要我们对一个矩阵进行求逆,而该矩阵的大小会随着观察数据的增多而增长。这个操作计算成本较高,而且扩展性不好。因此,围绕高斯过程的研究大部分集中在寻找近似方法,以便更快地计算它们并使其能够扩展到大规模数据。
我们将只讨论其中一种近似方法,即希尔伯特空间高斯过程(HSGP),而不深入探讨这种近似是如何实现的。从概念上讲,我们可以将其视为一种基函数展开,类似于样条函数的构建方式(参见第六章)。这种近似的结果是,它将矩阵求逆操作转化为矩阵乘法,这是一种速度更快的操作。
但什么时候它能起作用?
我们只能在低维度(大约 1 到 3 或 4 维)中使用 HSGP,并且仅限于某些核函数,如指数二次核或马特恩核。原因是,为了使 HSGP 近似法生效,核函数必须以一种特殊的形式表示,即功率谱密度形式,而并非所有核函数都可以用这种形式表示。
在 PyMC 中使用 HSGP 近似方法是非常直接的,我们将在自行车数据集上演示这一点。我们希望将出租自行车的数量建模为一天中小时数的函数。以下代码块展示了此模型的 PyMC 实现。
代码 8.16
with pm.Model() as model_hsgp:
ℓ = pm.InverseGamma('ℓ', *get_ig_params(X))
cov = pm.gp.cov.ExpQuad(1, ls=ℓ)
gp = pm.gp.HSGP(m=[10], c=1.5, cov_func=cov)
f = gp.prior('f', X=X)
*α* = pm.HalfNormal('*α*', 1)
_ = pm.NegativeBinomial("obs", np.exp(f), *α*, observed=y)
idata_hsgp = pm.sample()
与之前的高斯过程模型的主要区别在于,使用了pm.gp.HSGP(.)类,而不是用于非高斯似然和标准高斯过程的pm.gp.Latent(.)类。pm.gp.HSGP(.)类有两个参数:
-
m是我们用来逼近 GP 的基本函数数量。m值越大,逼近效果越好,但计算成本也越高。 -
c是一个边界因子。对于一个固定且足够大的m值,c主要影响均值函数在边界附近的逼近。它不应小于 1.2(如果使用较小的值,PyMC 会给出警告),通常 1.5 是一个不错的选择。改变此参数不会影响计算速度。
我们设置 m=10,部分原因是我们喜欢十进制系统,部分原因是基于 Riutort-Mayol et al. 发表的论文《Practical Hilbert space approximate Bayesian Gaussian processes for probabilistic programming》中的建议 [2022]。实际上,只要 m 和 c 的值在某个范围内,与长度尺度的先验一致,结果对它们的精确值是稳健的。关于 HSGP 如何工作以及如何在实践中使用它的一些建议,可以参考 Riutort-Mayol et al. [2022]。
现在让我们看看结果。图 8.17 显示了黑色的均值后验 GP 和来自 GP 后验的 100 个样本(灰色线)。你可以将这些结果与使用样条得到的结果进行比较(见图 6.8)。

图 8.17:HSGP 模型对租赁自行车的后验均值,作为一天中时间的函数
HSGP 近似也已在 Bambi 中实现。让我们看看如何使用它。
8.10.1 HSGP 与 Bambi
要使用 Bambi 拟合之前的模型,我们需要编写以下代码:
代码 8.17
bmb.Model("rented ∼ 0 + hsgp(hour, m=10, c=1.5)", bikes,
family="negativebinomial")
这样是可行的,但我们将为 Bambi 提供先验,就像我们在 PyMC 中做的那样。这将导致更快的采样和更可靠的样本。
正如我们在第 6 章中看到的那样,要在 Bambi 中定义先验,我们只需将字典传递给 bmb.Model 的 priors 参数。但我们必须注意,HSGP 项不会接收先验。相反,我们需要为 ℓ(在 Bambi 中称为 ell)和 η(在 Bambi 中称为 sigma)定义先验,并将这些先验传递给 HSGP 项。还有一件事:和之前的模型一样,我们没有使用 η,但由于 Bambi 需要它,我们使用了一个小技巧来定义一个基本上是 1 的先验。
代码 8.18
prior_gp = {
"sigma": bmb.Prior("Gamma", mu=1, sigma=0.01),
"ell": bmb.Prior("InverseGamma", **get_ig_params(X))
}
priors = {
"hsgp(hour, m=10, c=1.5)": prior_gp,
"alpha": bmb.Prior("HalfNormal", sigma=1)
}
model_hsb = bmb.Model("rented ∼ 0 + hsgp(hour, m=10, c=1.5)", bikes,
family="negativebinomial",
priors=priors)
idata_hsb = model_hsb.fit()
我邀请你检查 Bambi 计算的参数,它们与我们通过 PyMC 得到的非常相似。图 8.18 显示了黑色的均值后验 GP 和 94% HDI 的带状区域。该图是通过 bmb.interpret.plot_predictions 生成的。

图 8.18:HSGP 模型对租赁自行车的后验均值,作为一天中时间的函数,使用 Bambi
在本节中,我们探讨了 HSGP 的概念,作为一种强大的近似方法,可以将高斯过程扩展到大数据集。通过将 PyMC 和 Bambi 的灵活性与 HSGP 提供的可扩展性相结合,研究人员和实践者可以更有效地解决复杂的建模任务,为在日益庞大和复杂的数据集上应用高斯过程铺平道路。
8.11 总结
高斯过程是多元高斯分布的一种推广,适用于无限多个维度,完全由均值函数和协方差函数指定。由于我们可以概念上将函数视为无限长的向量,因此可以将高斯过程作为函数的先验。在实践中,我们并不处理无限对象,而是处理与数据点数量相等维度的多元高斯分布。为了定义其相应的协方差函数,我们使用了适当参数化的核;通过学习这些超参数,我们最终学会了关于任意复杂函数的知识。
在本章中,我们简要介绍了高斯过程(GP)。我们涵盖了回归、半参数模型(岛屿示例)、将两个或多个核组合以更好地描述未知函数,以及高斯过程如何用于分类任务。还有许多其他主题我们可以讨论。然而,我希望这段高斯过程的介绍足以激励你继续使用、阅读和学习高斯过程及贝叶斯非参数模型。
8.12 练习
-
对于协方差函数和核部分中的示例,确保理解输入数据与生成的协方差矩阵之间的关系。尝试使用其他输入,如
data = np.random.normal(size=4)。 -
重新运行生成图 8.3的代码,并将从高斯过程(GP)先验中获得的样本数量增加到约 200。原始图中的样本数量为 2。生成值的范围是什么?
-
对于前一个练习中生成的图表,计算每个点的标准差。按照以下形式进行操作:
-
从视觉上看,仅仅观察图表
-
直接来自于
pz.MVNormal(.).rvs生成的值 -
通过检查协方差矩阵(如果有疑问,请回到练习 1)
从这三种方法中得到的值是否匹配?
-
-
使用测试点
np.linspace(np.floor(x.min()), 20, 100)[:,None]并重新运行model_reg。绘制结果。你观察到了什么?这与 GP 先验的规范有何关系? -
重复练习 1,但这次使用线性核(参见附带的线性核代码)。
-
请查看 PyMC 文档中的
www.pymc.io/projects/examples/en/latest/gaussian_processes/GP-MeansAndCovs.html。 -
对
space_flu数据运行逻辑回归模型。你看到了什么?你能解释结果吗? -
修改逻辑回归模型以适应数据。提示:使用二阶多项式。
-
将煤矿灾难的模型与 PyMC 文档中的模型进行比较(
www.pymc.io/projects/docs/en/stable/learn/core_notebooks/pymc_overview.html#case-study-2-coal-mining-disasters)。描述这两个模型在模型规范和结果方面的差异。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,并与 5000 多名成员一起学习:packt.link/bayesian

第九章
贝叶斯加法回归树
个人而言,我们是一个水滴。一起时,我们是海洋。—— 佐藤隆之
在上一章中,我们讨论了高斯过程(GPs),一种用于回归的非参数模型。在本章中,我们将学习另一种非参数回归模型,称为贝叶斯加法回归树,或者亲切地称为 BART。我们可以从多个不同的角度来看待 BART。它可以看作是决策树的一个集成,每棵树在整体数据理解中扮演着独特的角色和贡献。这些树在贝叶斯先验的指导下和谐工作,以捕捉数据的细微差别,避免个体过拟合的陷阱。通常,BART 作为一个独立的模型进行讨论,实施该模型的软件通常仅限于一个或少数几个模型。在本章中,我们将采用不同的方式,使用 PyMC-BART,这是一个允许在 PyMC 中使用 BART 模型的 Python 库。
在本章中,我们将讨论以下主题:
-
决策树
-
BART 模型
-
使用 BART 的灵活回归
-
部分依赖图
-
单个条件期望图
-
变量选择
9.1 决策树
在深入讨论 BART 模型之前,先花点时间了解一下什么是决策树。决策树就像一个流程图,指导你通过不同的问题,直到你做出最终选择。例如,假设你每天早上需要决定穿什么鞋子。为了做出决定,你可能会问自己一系列问题:“今天暖和吗?”如果是的话,你可能会问更具体的问题,比如“我需要去办公室吗?”最终,你会停止提问并得到一个输出值,比如拖鞋、运动鞋、靴子、莫卡辛鞋等。
该流程图可以方便地编码为树状结构,在树的根部放置更一般性的问题,然后沿着树结构向更具体的问题推进,最终到达树的叶子节点,输出不同类型鞋子的结果。树是计算机科学和数据分析中非常常见的数据结构。
更正式地说,树是节点和连接这些节点的顶点的集合。带有问题的节点称为决策节点,带有树的输出(如鞋子)的节点称为叶子节点。当答案是“是”或“否”时,我们就有了一个二叉树,因为每个节点最多可以有两个子节点。图 9.1 显示了一个决策树。圆角方块是叶子节点,普通方块是决策节点。

图 9.1:选择鞋类的决策树。
我们可以使用决策树进行分类,即返回离散类别,如运动鞋、拖鞋、凉鞋等。但我们也可以用它们进行回归,即返回连续结果,如 4.24 或 20.9(以及介于两者之间的任何值)。通常,这些树被称为回归树。图 9.2展示了左侧的回归树。我们还可以将回归树视为分段阶梯函数的表示,如图 9.2右侧所示。这与表示平滑函数(至少在某种程度上)的三次样条或高斯过程(GPs)不同。树可以足够灵活,以提供良好的平滑函数的实用近似。

图 9.2:左侧为回归树,右侧为对应的分段阶梯函数
树可以非常灵活;在极端情况下,我们可以有一棵树,其中的叶节点数量与观测值一样多,这棵树将完美拟合数据。正如我们在第五章中看到的,除非我们添加一些正则化,否则这可能不是一个好主意。在贝叶斯术语中,我们可以通过先验来实现这种正则化。例如,我们可以设置一个先验,使树较浅。通过这种方式,我们使得树的节点数与数据点数相等的情况非常不太可能发生。
9.2 BART 模型
贝叶斯加性回归树(BART)模型是m棵树的和,我们用它来逼近一个函数[Chipman et al.,2010]。为了完成模型,我们需要设置树的先验。这些先验的主要作用是防止过拟合,同时保留树所提供的灵活性。先验设计旨在使得每棵树相对较浅,并且叶节点的值相对较小。
PyMC 不直接支持 BART 模型,但我们可以使用 PyMC-BART,一个扩展 PyMC 功能以支持 BART 模型的 Python 模块。PyMC-BART 提供:
-
一个与 PyMC 中其他分布(如
pm.Normal、pm.Poisson等)非常相似的 BART 随机变量。 -
一个名为 PGBART 的采样器,因为树不能使用 PyMC 的默认步进方法(如 NUTS 或 Metropolis)进行采样。
-
以下是一些帮助处理 BART 模型结果的实用函数:
-
pmb.plot_pdp:用于生成部分依赖图的函数[Friedman,2001]。 -
pmb.plot_ice:用于生成个体条件期望图的函数[Goldstein et al.,2013]。 -
pmb.plot_variable_importance:用于估算变量重要性的函数。 -
pmb.plot_convergence:一个绘制 BART 随机变量的有效样本大小的经验累积分布以及
值的函数。
-
BART 是步进函数的先验
我们可以将 BART 看作是分段常数函数的先验。此外,当树的数量m → ∞时,BART 会收敛为一个处处不可微的高斯过程。
在接下来的部分,我们将重点讨论 BART 的应用部分,特别是如何使用 PyMC-BART。如果您有兴趣了解更多有关 BART 模型工作原理的细节,PyMC-BART 的实现细节,以及更改 PyMC-BART 的超参数如何影响结果,建议阅读 Quiroga 等人 [2022]。
9.2.1 巴特企鹅
假设出于某种原因,我们有兴趣将企鹅的体重作为其他身体指标的函数进行建模。下面的代码块展示了一个针对这种问题的 BART 模型。在这个示例中,X = "flipper_length", "bill_depth", "bill_length"],而Y是body_mass:
代码 9.1
with pm.Model() as model_pen:
σ = pm.HalfNormal("σ", 1)
μ = pmb.BART("μ", X, Y)
y = pm.Normal("y", mu=μ, sigma=σ, observed=Y)
idata_pen = pm.sample()
pm.sample_posterior_predictive(idata_pen, extend_inferencedata=True)
我们可以看到,使用 PyMC-BART 通过 PyMC 定义 BART 模型是很简单的。基本上,我们需要定义一个带有参数X(协变量)和Y(响应变量)的 BART 随机变量。除此之外,模型的其余部分应该与其他回归模型非常相似。像其他回归模型一样,μ的长度将与观察值的数量相同。
从理论上讲,这些树仅仅是X的函数,但 PyMC-BART 需要Y来获取树叶节点上方差初始值的估计。
一旦我们使用 BART 变量拟合了模型,剩余的工作流程就和通常一样。例如,我们可以通过调用az.plot_ppc(.)来计算后验预测检验,结果将类似于图 9.3。

图 9.3:model_pen的后验预测检验
图 9.3显示了一个合理的拟合。值得注意的是,即使我们使用正态似然,也没有得到负的质量。然而,使用 PyMC 和 PyMC-BART 时,尝试其他似然分布非常简单;只需将正态分布替换为其他分布(如 Gamma 分布或截断正态分布),就像在常规的 PyMC 模型中那样,就能顺利进行。然后,您可以使用后验预测检验和 LOO,如第五章中讨论的那样。
在接下来的几节中,我们将讨论如何使用和解释 PyMC-BART 提供的工具函数(pmb.plot_convergence除外,该函数将在第十章中与其他诊断方法一起讨论)。
9.2.2 部分依赖图
部分依赖图(PDP)是一种在 BART 文献中广泛使用的图形工具,但并非仅限于 BART。原则上,它可以与任何方法或模型一起使用。它的基本原理是绘制给定协变量 X[i] 下的预测响应,同时对其余协变量 X[−i] 进行平均。因此,实质上,我们是在绘制每个协变量对响应变量的贡献,同时保持其他变量不变。对于 BART 和其他基于树的方法,有一个特点是 PDP 的计算可以在不重新拟合模型到合成数据的情况下进行;相反,它可以通过已拟合的树高效地计算出来。这使得 BART 成为模型可解释性和理解单个特征对预测影响的一个有吸引力的选择。
一旦像 model_pen 这样的模型已经拟合,我们可以通过以下方式计算部分依赖图(PDP):
代码 9.2
pmb.plot_pdp(μ, X, Y)
请注意,我们传递了 BART 随机变量、协变量和响应变量。响应变量其实并非必需,但如果传递了且它是 pandas Series 类型,它将使用其名称作为 y 轴标签。
图 9.4 显示了来自 model_pen 的部分依赖图示例。我们可以看到,flipper_length 显示出最大的效应,呈线性关系,而另外两个变量则基本保持平坦的响应,表明它们对响应变量的部分贡献不大。对于对响应变量贡献为零的变量,其预期的 PDP 将是一个平坦的常数线,值为响应变量的平均值。

图 9.4:model_pen 的部分依赖图
在 图 9.4 中,我们可以看到,最大的贡献来自 flipper_length,但这并不意味着其他两个变量与 body_mass 没有关系。我们只能说,考虑到模型中已经包含了 flipper_length,其他两个变量的影响是最小的。
9.2.3 个体条件图
在计算部分依赖图时,我们假设变量 X[i] 和 X[−i] 之间没有相关性。在许多实际问题中,情况往往并非如此,部分依赖图可能会掩盖数据中的关系。然而,如果所选变量子集之间的依赖关系不太强,则部分依赖图仍然可以作为有用的总结工具 Friedman [2001]。
个体条件期望(ICE)图与部分依赖图(PDP)密切相关。不同之处在于,我们不是绘制目标协变量对预测响应的平均部分效应,而是在给定固定值(默认为 10)下,绘制 n 条条件期望曲线。也就是说,ICE 图中的每条曲线反映了在固定的 X[i] 值下,协变量 X[i] 对预测响应的部分影响。
一旦像 model_pen 这样的模型已经拟合,我们可以通过以下命令计算 ICE 图:
代码 9.3
pmb.plot_ice(μ, X, Y)
这个签名与 PDP 相同。结果显示在图 9.5中。灰色曲线是不同值下的条件期望曲线。如果我们对它们取平均值,我们应该得到 PDP 曲线(黑色)。如果 ICE 图中的曲线大多平行,这意味着协变量对响应变量的贡献大多是独立的。这对于flipper_length和bill_length来说是成立的。在这种情况下,ICE 图和 PDP 图传达的是相同的信息。然而,如果曲线交叉,则表示贡献是非独立的。在这种情况下,PDP 会隐藏这些效应。我们可以在下图中看到bill_depth的例子:

图 9.5:model_pen的个体条件期望图
默认情况下,ICE 图是居中的,这意味着灰色曲线围绕在 x 轴最低值处计算的部分响应进行居中。这有助于解释图形:例如,可以更容易地看出线条是否交叉。这也解释了为什么图 9.5中的 y 轴刻度与图 9.4中的刻度不同。你可以通过参数centered=False来修改这一点。
9.2.4 使用 BART 进行变量选择
在第 6章中,我们已经讨论过变量选择,并解释了在某些场景下我们可能有兴趣选择一个变量子集。PyMC-BART 提供了一种非常简单、几乎不需要计算的启发式方法来估计变量重要性。它跟踪每个协变量作为分裂变量被使用的次数。对于 BART 模型,变量重要性是通过在m棵树和所有后验样本上取平均值来计算的。为了进一步简化解释,我们可以报告经过归一化的值,使每个值都位于区间[0, 1]内,且总重要性为 1。
在某些 BART 实现中,变量重要性的估计对树的数量m非常敏感。这些实现的作者建议,在进行变量选择时使用相对较少的树,而在进行模型拟合/预测时使用更多的树。PyMC-BART 不是这种情况,PyMC-BART 的变量重要性估计对树的数量具有鲁棒性。
一旦我们像model_pen这样拟合了一个模型来使用 PyMC-BART 进行变量选择,我们需要做如下操作:
代码 9.4
pmb.plot_variable_importance(idata_pen, μ, X)
请注意,我们传递了推断数据、BART 随机变量和协变量。结果显示在图 9.6中:

图 9.6:model_pen的变量重要性图
从顶部面板,我们可以看到flipper_length具有最大的变量重要性值,其次是bill_depth和fill_length。请注意,这与部分依赖图和个体条件期望图在定性上是一致的。
计算一个变量在树中出现的次数这一简单启发式方法存在一些问题。其中一个问题涉及可解释性,因为缺乏一个明确的阈值来区分重要变量和不重要变量,这是一个问题。PyMC-BART 提供了一些帮助。图 9.6的底部面板显示了参考模型生成的预测与使用子模型生成的预测之间的皮尔逊相关系数的平方,参考模型是包含所有协变量的模型,子模型是包含较少协变量的模型。我们可以使用这个图来找到能够做出与参考模型尽可能接近的预测的最小模型。图 9.6告诉我们,只有flipper_length的模型,其预测性能几乎与包含所有三个变量的模型相同。请注意,通过加入bill_depth可能会稍微提高一点性能,但这可能是微不足道的。
现在,让我简要解释一下pmb.plot_variable_importance在背后做了什么。主要有两个近似过程:
-
它不会评估所有可能的协变量组合。相反,它一次添加一个变量,按照变量的重要性顺序进行(图 9.6中的上面子图)。
-
它并不会重新拟合从 1 到 n 个协变量的所有模型。相反,它通过遍历参考模型的后验分布中的树,近似估计去除一个变量的影响,并且剪枝去除没有兴趣变量的分支。这与计算部分依赖图的过程类似,不同之处在于,对于部分依赖图,我们排除了除了一个变量之外的所有变量,而对于变量重要性,我们首先排除除了最重要的一个变量之外的所有变量,然后排除除了最重要的两个变量之外的所有变量,依此类推,直到包括所有变量。
如果这个变量选择过程对你来说听起来很熟悉,那么很可能你一直在关注这一章,并且也看过第六章。这个过程在概念上类似于 Kulprit 所做的工作。在这里,我们也利用了参考模型的概念,并根据其预测分布来评估模型。但相似之处到此为止。PyMC-BART 并没有使用 ELPD,而是使用皮尔逊相关系数的平方,并通过修剪用参考模型拟合的树来估计子模型,而不是通过 Kullback-Liebler 散度投影。
在进入下一个话题之前,让我再补充几点提醒。正如我们在第六章中讨论的那样,使用 Kulprit 的输出时,我们不应过度解读变量的顺序。样本适用于通过pmb.plot_variable_importance生成的图形,如图 9.6。如果两个变量的重要性非常相似,当我们用不同的随机种子重新拟合模型,或数据发生轻微变化(如添加或删除一个数据点)时,顺序可能会发生变化。变量重要性的误差条可以提供帮助,但它们可能低估了真实的变异性。因此,要谨慎对待顺序,将其作为解决问题时的参考。
9.3 分布式 BART 模型
正如我们在第六章中看到的,对于广义线性模型,我们并不限于为均值或位置参数创建线性模型;我们还可以建模其他参数,例如高斯分布的标准差,甚至同时建模均值和标准差。BART 模型也适用同样的原则。
为了举例说明,让我们以自行车数据集为模型。我们将使用rented作为响应变量,hour、temperature、humidity和workday作为预测变量。正如我们之前所做的,我们将使用负二项分布作为似然函数。该分布有两个参数,μ和alpha。我们将为这两个参数使用树的和。以下代码块展示了模型:
代码 9.5
with pm.Model() as model_bb:
μ = pmb.BART("μ", X, np.log(Y), shape=(2, 348), separate_trees=True)
pm.NegativeBinomial('yl', np.exp(μ[0]), np.exp(μ[1]), observed=Y)
idata_bb = pm.sample(2000,
random_seed=123,
pgbart={"batch":(0.05, 0.15)})
让我们花点时间确保理解这个模型。首先,注意我们传递了一个shape参数给pmb.BART()。当separate_trees = True时,这指示 PyMC-BART 拟合两组独立的树和。然后我们索引μ,以便使用第一维度作为负二项分布的μ参数,第二维度作为α参数。如果separate_trees = False,则告诉 PyMC-BART 拟合一组树和,但每棵树在每个叶节点返回 2 个值,而不是 1 个。这样做的好处是算法运行更快,内存使用更少,因为我们只拟合一组树。缺点是模型的灵活性较差。实际上,这两种选项都可以有用,因此你应该选择哪个,取决于你的建模决策。
model_bb的另一个重要方面是我们对μ取指数。这样做是为了确保 NegativeBinomial 分布的μ和α值都为正值。这与我们在广义线性模型中讨论的变换类型相同。PyMC-BART 的特殊之处在于,我们将其逆变换应用于传递给pmb.BART()的Y值。根据我的经验,这有助于 PyMC-BART 找到更好的解。对于具有二项分布或类别分布的模型,无需分别对逻辑回归或 softmax 应用逆变换。PyMC-BART 将二项分布作为特例处理,对于类别分布,我们通过经验发现即使没有逆变换,也能获得良好的结果。需要强调的是,我们传递给pmb.BART()的Y值仅用于初始化 BART 变量的采样。初始化似乎对我们传递的值具有鲁棒性,传递Y或其某种变换在大多数情况下都能很好地工作。
我希望你注意的第三个方面是,我们向pm.sample传递了一个新的参数,即pgbart。这个参数的值是字典"batch":(0.05, 0.15)。为什么要这么做呢?有时,为了获得高质量的样本,必须调整采样器的超参数。在之前的示例中,我们选择省略这一部分,以保持简单和专注。然而,正如我们在第 10章中更深入地讨论的那样,关注这些调整可能变得非常重要。对于 PGBART 采样器的特殊情况,我们可以改变两个超参数。其中一个是num_particles(默认为 10),粒子数量越大,BART 的采样越准确,但代价也越高。另一个是batch;默认值为元组(0.1, 0.1),意味着在每一步中,采样器在调节阶段拟合 10%的m棵树,在采样阶段也是如此。对于model_bb模型,我们使用了(0.05, 0.15),即在调节阶段使用 5%(2 棵树),在实际采样阶段使用 15%(7 棵树)。

图 9.7:model_bb的部分依赖图
我们可以像图 9.7中一样,探索协变量与响应之间的关系。请注意,变量出现了两次:第一列对应参数μ,第二列对应参数α。我们可以看到,hour对 NegativeBinomial 的两个参数的响应变量影响最大。
9.4 常数和线性响应
默认情况下,PyMC-BART 将拟合在每个叶节点返回单一值的树。这是一种通常非常有效的简单方法。然而,理解其影响是很重要的。例如,这意味着对于任何超出用于拟合模型的观察数据范围的值,预测将是常数。为了验证这一点,回去查看图 9.2。这棵树对于c1以下的任何值都将返回 1.9。请注意,即使我们将多棵树相加,这仍然是事实,因为加总一堆常数值仍然会得到另一个常数值。
这是否是一个问题,取决于你和你应用 BART 模型的上下文。尽管如此,PyMC-BART 提供了一个response参数,可以传递给 BART 随机变量。它的默认值是"constant"。你可以将其更改为"linear",在这种情况下,PyMC-BART 将在每个叶节点返回一个线性拟合,或者更改为"mix",这将在采样过程中提出具有常数或线性值的树。
为了举例说明差异,我们来拟合一个非常简单的例子:租赁自行车的数量与温度的关系。观察到的温度值从≈−5 到≈35。拟合此模型后,我们将要求范围为[-20, 45]的样本外后验预测值。因此,我们将设置一个具有可变变量的模型,如第四章所介绍的。
代码 9.6
with pm.Model() as model_tmp1:
X_mut1 = pm.MutableData("X_mut1", X)
*α* = pm.HalfNormal('*α*', 1)
μ = pmb.BART("μ", X_mut1, np.log(Y), m=100, response="linear")
_ = pm.NegativeBinomial('yl', np.exp(μ), *α*, observed=Y, shape=μ.shape)
idata_tmp1 = pm.sample(random_seed=123)
注意我们将shape=μ.shape传递给了似然函数。这是我们需要做的,以便能够改变X_mut1的形状,这也是 PyMC 的要求,所以你在使用非 BART 模型(如线性回归)时也应该做这件事。
好的,继续这个例子,在随附的代码中,你将找到model_tmp0模型的代码,它与model_tmp1完全相同,只是它具有默认的常数响应。两个模型的结果显示在图 9.8中。

图 9.8:常数和线性响应的平均预测
请注意,在数据范围之外(虚线灰色线),具有常数响应的模型的预测确实是常数。哪个模型提供的预测更好?我不确定。我认为,在较低温度范围内,线性响应更好,因为它预测租赁自行车的数量将继续减少,直到最终降至 0。但在较高温度范围内,平台效应或甚至下降应该比上升更可能。我是说,我尝试过在 40 度甚至 42 度时骑自行车,那可不是一个超级愉快的体验。你怎么看?
9.5 选择树的数量
树的数量(m)控制 BART 函数的灵活性。一般来说,默认值 50 应该足以得到一个不错的近似。较大的值,比如 100 或 200,应当提供更精细的结果。通常,增加树的数量不会导致过拟合,因为树的数量越大,叶节点的值越小。
在实际应用中,你可能会担心 m 的值过大,因为 BART 的计算成本(无论是时间还是内存)会随之增加。调节 m 的一种方式是执行 K 折交叉验证,正如 Chipman et al. [2010] 所推荐的那样。另一种选择是通过使用 LOO 来近似交叉验证,正如在第五章中讨论的那样。我们已经观察到 LOO 确实能帮助提供合理的 m 值 [Quiroga et al., 2022]。
9.6 总结
BART 是一种灵活的非参数模型,通过树的总和来近似从数据中获得的未知函数。先验用于规范推理,主要是通过限制树的学习能力,使得没有单一的树能够解释数据,而是树的总和。PyMC-BART 是一个 Python 库,它扩展了 PyMC 以支持 BART 模型。
本章中我们构建了一些 BART 模型,并学习了如何执行变量选择,利用部分依赖图和个体条件图来解释 BART 模型的输出。
9.7 练习
-
解释以下内容:
-
BART 和线性回归、样条有什么不同?
-
在什么情况下你可能会选择线性回归而非 BART?
-
在什么情况下你可能会选择高斯过程而非 BART?
-
-
用你自己的话解释为什么多个小树比一棵大树更能拟合模式。两者方法有什么区别?各自有什么权衡?
-
以下是两个简单的合成数据集。对每个数据集拟合一个 m=50 的 BART 模型。绘制数据和均值拟合函数。描述拟合情况。
-
x = np.linspace(-1, 1., 200) 和 y = np.random.normal(2*x, 0.25)
-
x = np.linspace(-1, 1., 200) 和 y = np.random.normal(x**2, 0.25)
-
创建你自己的合成数据集。
-
-
创建以下数据集 Y = 10sin(πX[0]X[1])+20(X[2] −0.5)² +10X[3] +5X[4] +
,其中
∼(0,1),且 X[0:9] ∼
(0,1)。这就是所谓的 Friedman 的五维函数。注意,我们实际上有 10 个维度,但最后 5 个是纯噪声。
-
将 BART 模型拟合到这个数据中。
-
计算 PDP 和变量重要性(VI)。
-
PDP 和 VI 是否在定性上相符?如何相符?
-
-
使用 BART 模型和企鹅数据集。将
bill_length、flipper_length、bill_depth、bill_length和body_mass作为协变量,物种Adelie和Chistrap作为响应变量。尝试不同的m值——10、20、50 和 100。使用 LOO 选择合适的值。 -
查看前一个问题中模型的变量重要性。将结果与使用 Kulprit 构建的同一协变量和响应变量的广义线性模型的结果进行比较,该模型使用 Bambi 构建。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,并与超过 5000 名成员一起学习: packt.link/bayesian

第十章
推断引擎
第一个原则是,你绝不能欺骗自己——而你最容易被自己欺骗。——理查德·费曼
到目前为止,我们主要关注模型构建、结果解释和模型批评。我们已经依赖 pm.sample 函数的魔力为我们计算后验分布。现在我们将集中学习该函数背后推断引擎的一些细节。
概率编程工具的整个目的,比如 PyMC,就是用户不必关心采样是如何进行的,但理解我们如何从后验分布中获取样本对完全理解推断过程非常重要,也有助于我们了解这些方法在何时何种情况下失败,以及如何应对。如果你不关心这些方法是如何工作的,你可以跳过本章的大部分内容,但我强烈建议你至少阅读诊断样本这一节,因为这一节提供了一些指导方针,帮助你检查后验样本是否可靠。计算后验分布的方法有很多。在本章中,我们将讨论一些通用思想,并重点介绍 PyMC 中实现的最重要的方法。我们将学习:
-
推断引擎
-
美特罗波利斯-哈斯廷斯算法
-
哈密尔顿蒙特卡洛
-
序贯蒙特卡洛
-
诊断样本
10.1 推断引擎
虽然贝叶斯方法在概念上简单,但在数学和数值计算上具有挑战性。主要原因是边际似然性,贝叶斯定理中的分母,通常表现为一个难以求解或计算上开销较大的积分。因此,后验通常是通过数值方法使用马尔可夫链****蒙特卡洛(MCMC)家族的算法来估计的。这些方法有时被称为推断引擎,因为至少在理论上,它们能够近似任何概率模型的后验分布。尽管在实际应用中推断并不总是那么理想,但这些方法的存在促使了概率编程语言(如 PyMC)的发展。
概率编程语言的目标是将模型构建过程与推理过程分离,以便于模型构建、评估以及模型修改/扩展的迭代步骤。通过将推理过程(而非模型构建过程)视为黑箱,像 PyMC 这样的概率编程语言用户可以专注于他们的具体问题,而将计算细节交给 PyMC 来处理。这正是我们迄今为止所做的事情。因此,你可能会偏向于认为这是显而易见或自然的方法。但需要注意的是,在概率编程语言出现之前,使用概率模型的人员也习惯于编写自己的采样方法,这些方法通常是根据他们的模型量身定制的,或者他们习惯于简化模型,使其适应某些数学近似。事实上,在某些学术圈子里,这种情况仍然存在。这种量身定制的方法可能更优雅,甚至可以为非常特定的模型提供更高效的后验计算方法,但它也容易出错且耗时,即使对于专家来说也是如此。此外,这种量身定制的方法并不适用于大多数希望利用概率模型解决问题的实践者。像 PyMC 这样的软件邀请来自不同背景的人们使用概率模型,降低了数学和计算的入门门槛。我个人认为这非常棒,也是在邀请我们更多地了解统计建模中的最佳实践,以避免自欺欺人。
之前的章节主要介绍了贝叶斯建模的基础知识;现在我们将从概念层面学习,自动推理是如何实现的,何时以及为什么会失败,失败时该怎么办。
然而,在讨论 MCMC 方法之前,让我先解释另外两种方法,这些方法有时也很有用,并且提供一种直观的理解,说明为什么我们通常使用 MCMC 作为通用方法。
10.2 网格法
网格法是一种简单的暴力法。即使你无法计算整个后验分布,你也许能够逐点计算先验和似然性;这是一个相当常见的情况,甚至可以说是最常见的情形。
假设我们想计算一个单参数模型的后验分布。网格逼近方法如下:
-
为参数定义一个合理的区间(先验应该会给你一些提示)。
-
在该区间上放置一个点的网格(通常是等距的)。
-
对于网格中的每个点,计算似然性与先验的乘积。
可选地,我们可以对计算值进行归一化,即将posterior数组中的每个值除以曲线下的总面积,确保总面积为 1。
以下代码块实现了硬币翻转模型的网格法:
代码 10.1
def posterior_grid(grid_points=50, heads=6, tails=9):
"""
A grid implementation for the coin-flipping problem
"""
grid = np.linspace(0, 1, grid_points)
prior = np.repeat(1/grid_points, grid_points) # uniform prior
likelihood = pz.Binomial(n=heads+tails, p=grid).pdf(heads)
posterior = likelihood * prior
posterior /= posterior.sum() * (1/grid_points)
return grid, posterior
图 10.1展示了我们在均匀先验下,抛硬币 13 次并观察到 3 次正面的后验。由于我们仅使用了 10 个点的网格,曲线显得非常崎岖。如果增加点的数量,曲线会变得更加平滑,计算结果也会更准确,但成本也会更高。

图 10.1:使用网格方法计算的后验
网格方法的最大弊端是,当参数的数量(也称为维度)增多时,这种方法的扩展性较差。我们可以通过一个简单的例子来说明这一点。假设我们想要采样一个单位区间(参见图 10.2),就像抛硬币问题一样,我们使用四个等距的点;这意味着分辨率为 0.25 单位。现在,假设我们有一个二维问题(图 10.2中的正方形),并且我们想用相同分辨率的网格;我们需要 16 个点。最后,对于三维问题,我们需要 64 个点(参见图 10.2中的立方体)。在这个例子中,我们需要 16 倍的资源来从一个边长为 1 的立方体中采样,相比于从一个边长为 1、分辨率为 0.25 的线段中采样。如果我们决定改为分辨率为 0.1 单位,我们将需要为线段采样 10 个点,为立方体采样 1,000 个点。

图 10.2:1 维、2 维和 3 维中具有相同分辨率的网格
除了点的数量增长速度外,还有一个现象,并非网格方法或任何其他方法的特性,而是高维空间的特性。随着参数数量的增加,后验概率大部分集中在参数空间的某个区域,而这个区域相对于采样体积来说变得越来越小。这是一个普遍存在的现象,通常被称为“维度灾难”,或者数学家们更喜欢称之为“测度集中”。
维度灾难是指一些相关现象,这些现象在低维空间中不存在,但在高维空间中存在。以下是一些这些现象的例子:
-
随着维度的增加,任何两个样本之间的欧几里得距离趋向于类似其他样本对之间的距离。也就是说,在高维空间中,大多数点之间的距离基本上是相同的。
-
对于超立方体,大部分的体积位于其角落,而非中心。对于超球体,大部分的体积位于其表面,而非中心。
-
在高维空间中,多元高斯分布的大部分质量并不集中在均值(或众数)附近,而是位于一个壳层中,这个壳层从均值向尾部扩展,随着维度的增加而远离均值。这个壳层被称为典型集。
有关说明这些概念的代码示例,请访问本书的仓库:github.com/aloctavodia/BAP3。
对于我们当前的讨论,所有这些事实意味着,如果我们不明智地选择评估后验分布的位置,我们将大部分时间都花在计算对后验贡献几乎为零的值上,从而浪费宝贵的资源。因此,网格方法并不是一个明智的选择来评估后验分布,因此作为高维问题的一般方法并不十分有用。
10.3 二次方法
二次近似,也称为 Laplace 方法或正态近似,包含将后验分布近似为高斯分布。为此,我们首先找到后验分布的模型;在数值上,我们可以使用优化方法来实现。然后我们计算 Hessian 矩阵,从中可以估算标准差。如果你在想,Hessian 矩阵是一个二阶偏导数的方阵。就我们关心的内容来说,我们可以用它来获得标准差,通常是协方差矩阵的标准差。
Bambi 可以使用二次方法为我们解决贝叶斯模型。在以下代码块中,我们首先定义一个硬币投掷问题的模型,这是我们之前为网格方法定义的相同模型,然后使用 Bambi 中称为 laplace 的二次方法进行拟合:
代码 10.2
data = pd.DataFrame(data, columns=["w"])
priors = {"Intercept": bmb.Prior("Uniform", lower=0, upper=1)}
model = bmb.Model("w ∼ 1", data=data, family="bernoulli", priors=priors,
link="identity")
results = model.fit(draws=4000, inference_method="laplace")
图 10.3 显示了计算得到的后验分布和精确的后验分布。请注意,Bambi 在使用此方法时还返回样本。它首先将后验分布近似为高斯分布(或多元高斯分布),然后从中采样。

图 10.3:后验的二次近似
二次/Laplace 方法在 Bambi 中主要是出于教学目的。尽管如此,一个不错的特点是 Bambi 会考虑边界。例如,在硬币投掷问题中,我们知道解必须位于区间 [0, 1] 内。即使在背后使用高斯分布时,Bambi 也能确保这一点。Bambi 通过在一个无界的参数空间中拟合高斯分布,然后将其转换到适当的有界空间,从而实现这一点。
二次/Laplace 方法虽然本身非常有限,但可以作为更高级方法的构建块。例如,集成嵌套 拉普拉斯近似(INLA)可以非常高效地拟合多种模型。
10.4 马尔可夫方法
有一类相关的方法,统称为马尔可夫链 蒙特卡洛(MCMC)方法。这些是随机方法,只要我们能逐点计算似然函数和先验分布,就可以从真实的后验分布中获取样本。你可能记得,这正是我们在网格方法中需要的条件,但与网格方法不同,MCMC 方法能够高效地从高维空间中的高概率区域采样。
MCMC 方法根据各个区域的相对概率访问参数空间的每个区域。如果区域 A 的概率是区域 B 的两倍,那么我们从 A 中获得的样本将是从 B 中获得样本的两倍。因此,即使我们无法通过解析方式计算整个后验分布,我们仍然可以使用 MCMC 方法从中抽取样本。理论上,MCMC 将给我们来自正确分布的样本——关键在于,这一理论保证只在渐进意义上成立,也就是说,在无限数量的样本下!实际上,我们总是有有限数量的样本,因此我们需要检查这些样本是否可信。我们将要学习这一点,但不要急于求成;首先,让我们对 MCMC 方法的工作原理有些直观的了解。这将有助于我们稍后理解诊断方法。为了理解 MCMC 方法,我们将把这个方法拆分为“两个 MC 部分”:蒙特卡洛部分和马尔科夫链部分。
10.4.1 蒙特卡洛
随机数的使用解释了“蒙特卡洛”这个名字的一部分。蒙特卡洛方法是一类非常广泛的算法,通过随机抽样来计算或模拟给定的过程。蒙特卡洛是位于摩纳哥公国的一家非常著名的赌场。蒙特卡洛方法的开发者之一,Stanislaw Ulam,有一个叔叔曾在那儿赌博。Stan 的关键思想是,尽管许多问题难以精确求解或甚至无法精确表达,但它们可以通过从中抽取样本有效地进行研究。事实上,故事是这样传的:最初的动机是为了回答关于在纸牌接龙游戏中获得特定手牌的概率问题。
解决这个问题的一种方法是遵循分析组合问题。另一种方法,Stanislaw 认为,是玩几局纸牌接龙,并计算我们玩过的手牌中有多少和我们感兴趣的特定手牌相匹配!也许这对你来说听起来很显而易见,或者至少相当合理;你甚至可能用过重采样方法来解决统计问题。但请记住,这个心理实验是在大约 70 年前进行的,那时第一台实用计算机才开始被开发出来!
蒙特卡洛方法的首次应用是解决一个核物理问题,这是当时工具无法轻松应对的难题。如今,甚至个人计算机也足够强大,能够使用蒙特卡洛方法解决许多有趣的问题;因此,这些方法被广泛应用于科学、工程、工业和艺术等多个领域。使用蒙特卡洛方法计算感兴趣量的经典教学示例是对π值的数值估计。实际上,对于这个特定的计算,存在更好的方法,但它在教学中的价值依然存在。
我们可以通过以下过程估算π的值:
-
随机地将N个点投掷到边长为 2R的正方形中。
-
在方形内部画一个半径为R的圆,并计算圆内的点数M。
-
计算
作为比例 4
。
这里有几点说明:
-
圆的面积与圆内点数(M)成正比,方形的面积与总点数(N)成正比。
-
我们知道,如果满足以下关系式,则一个点在圆内:
。 -
方形的面积是(2R)²,圆的面积是πR²。因此,我们知道方形面积与圆面积的比值是π。
使用几行 Python 代码,我们可以运行这个简单的蒙特卡罗模拟并计算π,同时也能计算我们估算值相对于真实π值的相对误差。一次运行的结果如图 10.4所示。

图 10.4:π的蒙特卡罗近似
10.4.2 马尔可夫链
马尔可夫链是一个数学对象,由一系列状态和描述如何在状态之间转换的转移概率组成。你可以自己创建一个马尔可夫链;比如,抛硬币,如果得到正面,就向右走一步,否则向左走一步。这是一个简单的一维马尔可夫链。如果一个链是马尔可夫链,那么从任何状态转移到其他状态的概率只依赖于当前状态。
作为从业者,你只需要知道马尔可夫链提供了一个框架来研究 MCMC 采样器的性质(以及其他一些有用的应用)。它们并不难理解,至少是最基本的性质并不难理解。但对于你作为模型构建者来说,深入细节并没有太大意义,因此我们不会进一步讨论。如果你有兴趣,可以查看 Blitzstein [2019],那是一个很好的入门介绍。
最流行的 MCMC 方法可能是美特罗波利斯-哈斯廷斯算法,接下来我们将讨论它。
10.4.3 美特罗波利斯-哈斯廷斯算法
对于某些分布,例如高斯分布,我们有非常高效的算法可以从中获得样本,但对于其他分布则不一定是这样。美特罗波利斯-哈斯廷斯算法使我们能够从任何概率分布中获得样本,只要我们能够计算出与之成比例的值,因此可以忽略归一化因子。这非常有用,因为很多时候更困难的部分恰恰是计算归一化因子。这在贝叶斯统计中尤为明显,在贝叶斯统计中,计算边际似然可能是一个决定性的难点。
为了概念性地理解这种方法,我们将使用以下类比。假设我们有兴趣找到湖泊中的水量,并且找出湖泊中最深的部分。湖水非常浑浊,因此我们无法通过看水底来估算深度,湖泊又非常大,所以使用网格近似方法似乎不是一个好主意。为了制定抽样策略,我们寻求了两位好朋友的帮助:Markovia 和 Monty。经过一次富有成效的讨论,他们提出了以下算法,它需要一艘船——不用太奢华,我们甚至可以使用一只木筏和一根非常长的木棍。这比声呐便宜,而且我们已经把所有的钱都花在船上了!看看这些步骤:
-
在湖中选择一个随机位置,并将船移到那里。
-
使用木棍测量湖泊的深度。
-
将船移动到另一个点并进行新的测量。
-
通过以下方式比较这两种方法:
-
如果新位置比第一个位置更深,记下新位置的深度并从第 3 步重复。
-
如果这个位置比第一个位置更浅,我们有两个选择:接受或拒绝。接受意味着我们记录下新位置的深度并从第 3 步重复。拒绝意味着我们返回第一个位置,并再次(是的,重新!)记录第一个位置的深度值。
-
决定是否接受或拒绝新位置的规则称为 Metropolis-Hastings 标准,基本上说的是,我们必须按与新旧位置深度比值成正比的概率接受新位置。
如果我们按照这个迭代过程进行,不仅能得到湖泊的总体水量和最深的点,还能得到湖底整个曲率的近似值。正如你可能已经猜到的那样,在这个类比中,湖底的曲率就是后验分布,最深的点是众数。根据我们的朋友 Markovia 说,迭代次数越多,近似值就越好。事实上,理论保证在某些一般条件下,如果我们获取无限多个样本,我们最终会得到精确的答案。幸运的是,在实践中,对于许多问题,我们可以通过有限且相对较少的样本获得非常准确的近似值。
上述解释足以帮助我们概念性地理解 Metropolis-Hastings 算法。接下来的几页将提供更详细和正式的解释,以防你想深入了解。
Metropolis-Hastings 算法有以下步骤:
-
为参数 x[i] 选择一个初始值。这可以通过随机选择或根据经验猜测来完成。
-
通过从 Q(x[i+1]|x[i]) 中抽样,选择一个新的参数值 x[i+1]。我们可以把这一步看作是对状态 x[i] 的某种扰动。
-
使用 Metropolis-Hastings 标准计算接受新参数值的概率:
![( ) pa(xi+1 | xi) = min 1, p(xi+1) q(xi-| xi+1) p(xi) q(xi+1 | xi)]()
-
如果在步骤 3 中计算的概率大于从 [0, 1] 区间的均匀分布中取出的值,我们就接受新的状态;否则,我们保持在旧的状态中。
-
我们从步骤 2 开始迭代,直到我们有足够的样本。
这里有几个需要注意的事项:
-
Q 被称为提议分布。它可以是我们想要的任何分布,但我们通常选择一个容易采样的分布,比如高斯分布或均匀分布。
-
注意,Q 不是先验、似然或模型的任何部分。它是 MCMC 方法的一个组成部分,而不是模型的一部分。
-
如果 Q 是对称的,则 q(x[i]|x[i+1]) 和 q(x[i+1]|x[i]) 会相互抵消。因此,我们只需要评估比值
。 -
步骤 3 和步骤 4 表明我们总是会接受转向一个更可能的状态。较不可能的参数值会按概率接受,给定新参数值 x[i+1] 和旧参数值 x[i] 之间的比率。接受提议步骤的这一标准,相较于网格方法,给我们提供了一种更高效的采样方法,同时保证了正确的采样。
-
目标分布(贝叶斯统计中的后验分布)是通过一组采样的参数值来近似的。如果我们接受,就将 x[i+1] 添加到新采样值的列表中。如果我们拒绝,就将 x[i] 添加到列表中,即使这个值是重复的。
在这个过程的最后,我们将得到一组数值。如果一切都按正确的方式完成,这些样本将是后验分布的近似。我们追踪中出现频率最高的数值将是根据后验分布最可能的值。此过程的一个优点是,分析后验分布就像操作一组数值数组一样简单,正如你在之前的章节中所做的那样。
以下代码演示了 metropolis 算法的一个非常基础的实现。它并不是为了求解一个实际问题,而是为了展示如果我们知道如何逐点计算概率密度,我们可以从概率分布中采样。请注意,以下实现没有涉及任何贝叶斯的内容;它没有先验,甚至没有数据!请记住,MCMC 方法是非常通用的算法,可以应用于广泛的问题。
metropolis 函数的第一个参数是一个 PreliZ 分布;我们假设我们不知道如何直接从这个分布中获取样本:
代码 10.3
def metropolis(dist, draws=1000):
"""A very simple Metropolis implementation"""
trace = np.zeros(draws)
old_x = 0.5
old_prob = dist.pdf(old_x)
delta = np.random.normal(0, 0.5, draws)
for i in range(draws):
new_x = old_x + delta[i]
new_prob = dist.pdf(new_x)
acceptance = new_prob / old_prob
if acceptance >= np.random.random():
trace[i] = new_x
old_x = new_x
old_prob = new_prob
else:
trace[i] = old_x
return trace
我们的简单 metropolis 算法的结果如 图 10.5 所示。黑线表示真实分布,而条形表示我们计算的样本。

图 10.5:来自简单的 metropolis 算法的样本
算法的效率在很大程度上依赖于提议分布;如果提议的状态与当前状态相差很远,拒绝的概率就会非常高;如果提议的状态非常接近当前状态,我们就会非常缓慢地探索参数空间。在这两种情况下,我们需要的样本数量都远远多于较不极端的情况。通常,提议分布是一个多变量高斯分布,其协方差矩阵在调节阶段确定。PyMC 通过遵循一个经验法则来自适应地调节协方差,即一维高斯分布的理想接受率大约是 50%,而 n 维高斯目标分布的理想接受率大约是 23%。
MCMC 方法通常需要一些时间才能开始从目标分布中获取样本。所以,在实践中,人们会进行烧入步骤,即消除前一部分样本。进行烧入是一个实际的技巧,而不是马尔可夫理论的一部分;实际上,对于无限样本来说,这并不是必需的。因此,去除前一部分样本只是一个临时的技巧,用于在只能计算有限样本的情况下获得更好的结果。拥有理论保证或指导总是比没有它们要好,但对于任何实际问题,理解理论与实践之间的区别非常重要。记住,我们不应当因为将数学对象与这些对象的近似混淆而感到困惑。球体、高斯分布、马尔可夫链以及所有数学对象只存在于理念的柏拉图世界中,而不在我们不完美的现实世界中。
到这一点,我希望你已经对 Metropolis-Hastings 方法有了一个清晰的概念理解。你可能需要回头再读这一部分几遍;这完全没问题。主要思想简单但也很微妙。
10.4.4 哈密顿蒙特卡洛
MCMC 方法,包括 Metropolis-Hastings,提供了理论上的保证:如果我们获取足够多的样本,就能准确地近似正确的分布。然而,在实践中,可能需要比我们拥有的时间更多的时间来获取足够的样本。因此,已经提出了 Metropolis-Hastings 算法的替代方案。
许多替代方法,比如 Metropolis-Hastings 算法本身,最初是为了解决统计力学中的问题而开发的。统计力学是物理学的一个分支,研究原子和分子系统的性质,因此可以通过物理系统的类比以非常自然的方式进行解释。一个这样的修改被称为哈密顿蒙特卡洛,或者混合蒙特卡洛(HMC)。简单来说,哈密顿量是描述物理系统总能量的量。术语混合也被使用,因为它最初被构想为 Metropolis-Hastings 与分子力学的混合,分子力学是用于分子系统的广泛应用的模拟技术。
从概念上讲,我们可以将 HMC 方法视为一种 Metropolis-Hastings 方法,只不过其提议分布不是随机的。为了在不涉及数学细节的情况下对 HMC 有一个概念性的理解,我们可以再次使用湖泊和船只的类比。我们不再随机地移动船只,而是沿着湖底的曲率来移动船只。为了决定船只的移动方向,我们让一个球从当前位置开始滚到湖底。我们的球是一个非常特殊的球:它不仅是完美的球形,而且没有摩擦力,因此不会被水或泥土减速。我们扔下这个球,让它滚动一小段时间,直到我们突然停止它。然后,我们使用 Metropolis 准则接受或拒绝这个提议的步骤,就像我们在传统的 Metropolis-Hastings 方法中所做的那样。接着,整个过程会被重复多次。幸运的是,这种修改后的程序会增加接受新位置的机会,即使这些新位置相对于之前的位置远得多。
根据参数空间的曲率进行移动被证明是一种更聪明的移动方式,因为它避免了 Metropolis-Hastings 方法的主要缺点之一:高效探索样本空间需要拒绝大多数提出的步骤。相反,使用 HMC 方法,即使是参数空间中较远的点,也能获得较高的接受率,从而实现非常高效的采样方法。
让我们走出这个假想实验,回到现实世界。我们必须为这种非常巧妙的基于哈密顿量的提议付出代价。我们需要计算我们函数的梯度。梯度是导数概念在多维空间中的推广;计算函数在某一点的导数告诉我们函数在哪个方向上增大,在哪个方向上减小。我们可以使用梯度信息来模拟球体在弯曲空间中的运动;事实上,我们使用与经典物理学中模拟经典力学系统(如滚动的球体、行星系统中的轨道、分子震动)相同的运动定律和数学工具。
计算梯度使我们面临一个权衡:每一步 HMC 的计算成本比 Metropolis-Hastings 步骤高,但 HMC 的接受概率远高于 Metropolis。为了平衡这个权衡,使其有利于 HMC,我们需要调整 HMC 模型的一些参数(类似于调整 Metropolis-Hastings 采样器的提议分布宽度)。当这种调整是手动进行时,需要通过反复试验,并且还需要经验丰富的用户,这使得这个过程比我们希望的更加依赖于用户的经验,缺乏普遍适用性。幸运的是,现代的概率编程语言配备了高效的自适应哈密顿蒙特卡洛方法,比如 PyMC 中的 NUTS 采样器。这个方法已经证明在求解贝叶斯模型时非常有用和高效,且无需人工干预(或者至少可以最小化人工干预)。
哈密顿蒙特卡洛方法的一个局限是,它们仅适用于连续分布;原因是我们无法对离散分布计算梯度。PyMC 通过将 NUTS 分配给连续参数,将其他采样器分配给其他参数(例如,将 PGBART 分配给 BART 随机变量,将 Metropolis 分配给离散变量)来解决这个问题。
基于 JAX 的采样
JAX 是一个旨在提供高性能数值计算和自动微分的库,用于复杂的数学运算。PyMC 使用的是 NUTS 的 Python 版本。但你也可以使用基于 JAX 的采样器实现。根据你的模型,这些采样器可能比 PyMC 默认的 NUTS 采样器快得多。为了使用它们,我们需要为pm.sample()指定nuts_sampler参数。当前支持的选项有"nutpie"、"blackjax"和"numpyro"。这三种采样器默认不与 PyMC 一起安装,所以你需要手动安装它们。对于 CPU,nutpie 可能是最快的选择:github.com/pymc-devs/nutpie。在本书中,我们使用了 nutpie 从高斯过程(GPs)中采样——请参见第八章中的 Jupyter 笔记本。
我强烈推荐你配合这个非常酷的应用程序来阅读本节内容,作者是 Chi Feng:chi-feng.github.io/mcmc-demo。
10.5 序列蒙特卡洛
Metropolis-Hastings 和 NUTS(以及其他哈密顿蒙特卡洛变体)的一大局限是,如果后验分布具有多个峰值且这些峰值之间被非常低概率的区域分隔,这些方法可能会陷入单一模式,错过其他峰值!
许多为克服这个多重极小值问题而开发的方法基于温度化的思想。这个思想再次借鉴了统计力学的概念。一个物理系统可以占据的状态数取决于系统的温度;在 0 开尔文(最低可能温度)下,所有系统都陷入一个单一状态。而在另一极端,对于无限温度,所有可能的状态都是等概率的。通常,我们关注的是处于某个中间温度的系统。对于贝叶斯模型,通过对贝叶斯定理进行某种变化,可以直观地应用这个温度化思想。

参数β被称为逆温度或温度化参数。注意,当β = 0 时,我们得到p(y|θ)^(β) = 1,因此温度化后验p(θ|y)[β]仅仅是先验p(θ),而当β = 1 时,温度化后验就是实际的完整后验。由于从先验分布进行采样通常比从后验分布采样更容易(通过增加β的值),我们从一个较容易的分布开始采样,并逐渐将其转变为我们真正关心的更复杂的分布。
有许多方法利用了这个思想,其中之一被称为顺序蒙特卡罗(SMC)。PyMC 中实现的 SMC 方法可以总结如下(也见图 10.6):
-
将β初始化为 0。
-
从温度化后验中生成N个样本S[β]。
-
将β稍微增加一点。
-
计算一组N的权重W。这些权重是根据新的温度化后验计算得出的。
-
通过根据W对S[b]进行重采样,获得S[w]。
-
运行N个 Metropolis 链,每个链都从S[w]中的不同样本开始。
-
从步骤 3 开始重复,直到β ≥ 1。

图 10.6:SMC 的示意图
重采样步骤通过移除概率较低的样本并用概率较高的样本替换它们来工作。Metropolis 步骤扰动这些样本,帮助探索参数空间。
温度化方法的效率很大程度上取决于β的中间值,这通常被称为降温计划。β的两个连续值之间的差异越小,两个连续的温度化后验就会越接近,因此从一个阶段到下一个阶段的过渡就会更容易。但如果步长太小,我们将需要许多中间阶段,超过某个点,这将导致浪费大量计算资源,而并未真正提高结果的准确性。
幸运的是,SMC 可以自动计算β的中间值。具体的降温计划将根据问题的难度进行调整;较难采样的分布将需要比简单分布更多的阶段。
在图 10.6 的顶部,我们有九个样本或粒子(灰色点),它们来自先验,表示为覆盖所有内容的非常宽的分布(阶段 0)。对于后续阶段,我们根据其加权后验密度重新加权前一个阶段的样本。然后,我们根据这些权重进行重采样。因此,一些粒子会被丢失并被其他样本替代,所以总数保持不变。接着,我们对样本进行突变,也就是说,我们对粒子应用一个或多个 MCMC 步骤。然后我们增加 β 并重复这一过程。当我们达到 β = 1 时,粒子(或样本)将按照后验分布进行分布。
除了β的中间值外,还有两个参数是根据前一个阶段的接受率动态计算的:每个马尔可夫链的步数和提议分布的宽度。
10.6 诊断样本
在本书中,我们使用数值方法计算几乎所有模型的后验。你在使用贝叶斯方法处理自己问题时,很可能也会遇到这种情况。由于我们使用有限数量的样本来近似后验,因此检查我们是否有有效样本是非常重要的;否则,从中得出的任何分析都会完全失真。我们可以执行几个测试,其中一些是视觉测试,其他的是定量测试。这些测试旨在发现样本的问题,但无法证明我们拥有正确的分布;它们只能提供样本似乎合理的证据。如果我们发现样本有问题,还有很多解决方案可以尝试。我们将在诊断过程中讨论这些解决方法。
为了使解释更具体,我们将使用最简模型,包含两个参数:一个全局参数 a 和一个组参数 b。仅此而已,这些模型甚至没有似然/数据!
代码 10.4
with pm.Model() as model_c:
a = pm.HalfNormal('a', 10)
b = pm.Normal('b', 0, a, shape=10)
idata_cm = pm.sample(tune=2000)
with pm.Model() as model_nc:
a = pm.HalfNormal('a', 10)
b_offset = pm.Normal('b_offset', mu=0, sigma=1, shape=10)
b = pm.Deterministic('b', 0 + b_offset * a)
idata_ncm = pm.sample(tune=2000)
model_c和model_nc模型之间的区别在于,对于前者,我们直接拟合组级参数,而对于后者,我们将组级参数建模为一个平移和缩放的高斯分布。
这两个模型可能看起来对你来说过于人工化,或者只是奇怪。然而,需要注意的是,这两个模型的结构本质上与我们在第四章 4 中已经讨论过的居中和非居中参数化是相同的。
根据该章节的讨论,我们应该期待从model_nc获得比model_c更好的样本。让我们检查我们的预期是否成立。
10.7 收敛性
理论上,MCMC 方法在我们采集无限样本时是可以保证收敛的。实际上,我们需要检查所采样本是否合理且有限。通常,当我们收集到证据表明样本在某种意义上是稳定的时,我们就认为采样器已收敛。一个简单的测试方法是多次运行相同的 MCMC 模拟,并检查每次是否得到相同的结果。这也是为什么 PyMC 默认运行更多链条的原因。对于现代计算机来说,这是几乎没有成本的,因为我们有多个核心。而且,它们不会浪费资源,因为我们可以将不同链条的样本合并来计算摘要、绘图等。
有许多方法可以检查不同链条在实际应用中的等效性,无论是通过视觉方式还是正式测试。我们在这里不会深入探讨技术细节;我们只会展示一些例子,希望它们足以让你培养出解读诊断结果的直觉。
10.7.1 轨迹图
检查收敛性的一种方法是通过视觉检查链条是否相似。例如,我们可以使用 ArviZ 的 plot_trace 函数。为了更好地理解在检查这些图表时应该注意什么,我们将比较之前定义的两个模型的结果。
变量 b 是 10 维的。为了清晰和简洁,我们只展示其中一个维度。你可以随时在自己的计算机上将所有维度可视化。图 10.7 显示了许多问题。在左列中,我们有四个 KDE,每个链条一个。我们可以看到它们看起来不同。这表明每条链条采样的后验区域略有不同。在右列中,我们有轨迹本身。我们也有四条线,每条链条一条,虽然它们有些凌乱,但我们仍然可以看到,有一条链条从第一步就卡在 0 附近,直到几乎第 400 步才有所变化。我们在第 800 步左右也看到类似的情况。
当我们将图 10.7与图 10.8进行比较时,问题变得更加明显。对于后者,我们看到四条链的 KDE 看起来相互之间更为相似,而轨迹图看起来模糊得多,更像是噪声,很难看到明显的模式。我们希望曲线能够自由地漫游。当这种情况发生时,我们说我们有良好的 混合。我们这么表达是因为很难区分一条链与另一条链;它们是混合的。这是好事,因为这意味着即使我们从不同的起点启动四条(或更多)独立链条,它们也都描述了相同的分布。这并不是收敛的证明,但至少我们没有看到非收敛或糟糕混合的证据。

图 10.7:model_c 的轨迹图
图 10.7 还有一些黑色垂直线条出现在顶部,而图 10.8 中没有这些线条。这些是发散点;本章稍后有专门的部分讨论这些问题。

图 10.8:model_nc 的轨迹图
10.7.2 排名图
轨迹图可能很难解读,特别是当我们有多个链时,因为容易忽略一些细节。一个替代方案是使用排名图 [Vehtari et al., 2021]。为了构建某一参数的排名图,我们首先将所有链中的所有样本进行排序,并分配一个整数:这是第 0 个样本,这是第 1 个样本,这是第 2 个样本,依此类推。然后,我们将所有排名根据原始链进行分组。最后,我们绘制与链数相等的直方图。如果所有链都来自相同的分布,我们可以预期所有链都有相同数量的低排名、高排名、中等排名等。换句话说,排名的直方图应该是均匀的。
要获取排名图,我们可以调用 ArviZ 的 plot_trace 函数,并使用 kind="rank_bars" 参数。图 10.9 和图 10.10 是这类图的示例。

图 10.9:model_c 的排名图

图 10.10:model_nc 的排名图
左边是我们之前展示过的相同的 KDE 图。右边是排名图。再次强调,model_nc 的结果看起来好多了;与均匀分布的偏差非常小。另一方面,我们可以从 图 10.9 中看到一些问题;例如,参数 a 的排名在 500 或更低的地方看起来非常差,参数 b 在排名大约为 2000 处也很糟糕。其他区域也存在问题。
10.7.3
(R hat)
比较独立链的一种定量方法是使用
统计量。此测试的思路是计算链之间的方差与链内部的方差之比。理想情况下,我们应该期望值为 1。作为经验法则,值小于 1.01 也是可以接受的;较高的值则表示缺乏收敛性。我们可以使用 az.r_hat 函数计算该值(参见 表 10.1)。
诊断量也可以通过默认的 az.summary 函数计算,或者通过 az.plot_forest(使用 r_hat=True 参数)来选择性计算。
| a | b[0] | b[1] | b[2] | b[3] | b[4] | b[5] | b[6] | b[7] | b[8] | b[9] | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| model_c | 1.2 | 1.17 | 1.05 | 1.17 | 1.17 | 1.15 | 1.11 | 1.09 | 1.17 | 1.18 | 1.17 |
| model_nc | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 | 1.0 |
表 10.1:model_c 和 model_ncm 模型的
值
大约 1.1 的值在建模初期可能是可以接受的,特别是当你只是检查一个似然是否合理,或仅仅是尝试找出你真正想要构建的模型时。此外,对于参数较多的模型来说,1.01 的阈值可能过于严格。原因是即使你确实已经达到了收敛,你仍然可能偶然得到一些超过这个阈值的
值。例如,PyMC-BART 包括 plot_convergence 函数。该函数旨在检查 BART 随机变量的收敛性。使用 BART 模型时,每个观察值都会得到一个
,而且这个数量可能很多。因此,plot_convergence 显示了
值的累积分布以及一个阈值,这个阈值会自动计算并考虑到观察值的数量,从而进行多重比较的校正。
图 10.11 显示了此类图的示例。在右侧,我们展示了
的累积分布,并有一条灰色虚线表示调整后的阈值。理想情况下,整个累积曲线应位于虚线的左侧。在 图 10.11 的左侧子图中,我们展示了 有效样本量(ESS)。我们将在下一节解释 ESS。

图 10.11:使用 pmb.plot_convergence(.) 计算的诊断图
10.8 有效样本量(ESS)
MCMC 样本可能是相关的。原因是我们使用当前的位置生成一个新位置,并在考虑旧位置的基础上接受或拒绝下一个位置。这种依赖性对于调优良好的现代方法(如哈密顿蒙特卡洛)通常较低,但有时也可能较高。我们可以使用 az.plot_autocorrelation 计算并绘制自相关图。但通常,更有用的度量是计算 有效样本量(ESS)。我们可以将这个数字看作是样本中有用的抽样次数。由于自相关,这个数字通常会低于实际的样本数。我们可以使用 az.ess 函数来计算它(见 表 10.2)。ESS 诊断也可以通过 az.summary 函数默认计算,或者通过 az.plot_forest 函数(使用 ess=True 参数)来选择性计算。
| a | b[0] | b[1] | b[2] | b[3] | b[4] | b[5] | b[6] | b[7] | b[8] | b[9] | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| model_cm | 14 | 339 | 3893 | 5187 | 4025 | 5588 | 4448 | 4576 | 4025 | 4249 | 4973 |
| model_ncm | 2918 | 4100 | 4089 | 3942 | 3806 | 4171 | 3632 | 4653 | 3975 | 4092 | 3647 |
表 10.2:model_c 和 model_ncm 模型的 ESS 值
一般规则是我们至少需要一个有效样本量为 400(每条链 100 ESS)。如果得到的值低于这个数值,我们的估计不仅可能过于嘈杂,甚至像
这样的诊断图也可能变得不可靠。
MCMC 样本的质量可能会因后验分布的不同区域而异。例如,至少对于某些问题,采样分布的中心部分可能比尾部部分更容易。因此,我们可能希望计算后验分布不同区域的 ESS 值。az.ess()返回的默认值是中心区域的 ESS,它估计分布中心的解析精度。如果你对某个参数的均值或中位数感兴趣,应该检查这个 ESS 值。如果你想报告后验区间或关注稀有事件,应检查尾部 ESS 值,它是在 5%和 95%分位数处计算的最小 ESS 值。如果你对特定分位数感兴趣,可以使用az.ess(., method='quantile')来请求这些具体的值。我们甚至可以使用az.plot_ess(., kind="quantiles")函数同时绘制多个分位数的 ESS,如图 10.12所示,针对参数a。

图 10.12:参数a的 ESS 分位数
最后,当我们运行模型并发现 ESS 值非常低时,第一反应可能是增加样本数。有时候这就足够了。但有时即使增加 10 倍样本也不够。我们可以使用az.plot_ess(., kind="evolution"),而不是通过反复试验。这将为我们提供一个样本与 ESS 的图,如图 10.13所示。我们可以利用这些信息来估计需要多少样本才能达到给定的 ESS 值。例如,在图 10.13中,我们可以看到仅通过增加样本数,model_c中参数a的 ESS 值没有太大希望变得理想。与此相比,model_nc的 ESS 值对于大部分样本接近实际样本数。

图 10.13:参数a的 ESS 演变
10.9 蒙特卡罗标准误差
即使我们有一个非常低的
和一个非常高的 ESS 值,MCMC 的样本仍然是有限的,因此我们在估计后验参数时会引入误差。幸运的是,我们可以估算这个误差,它被称为蒙特卡罗标准误差(MCSE)。MCSE 的估算考虑到样本之间并非真正独立。我们希望得到的结果精度受限于这个值。如果某个参数的 MCSE 为 0.2,那么报告该参数为 2.54 是没有意义的。相反,如果我们重复模拟(使用不同的随机种子),我们应该预期 68%的结果会落在 2.54±0.2 的范围内。同样,对于 95%的结果,它们应该落在 2.54±0.4 的范围内。在这里,我假设 MCSE 服从正态分布,并利用高斯分布的性质:大约 68%的值位于一个标准差以内,约 95%的值位于两个标准差以内。
、ESS 和 MCSE 是相关的。实际上,我们应该使用 ESS 作为无量纲诊断,以确保我们拥有足够的有效样本。它是无量纲的,因为无论一个参数从 0 到 1,另一个从 0 到 100,都不影响 ESS 的比较。我们可以比较它们的 ESS 值。对于 ESS,值越大越好,最低值至少为 400。如果达到最低值,我们检查是否有足够低的
。我们还可以通过可视化的排名图或轨迹图来检查(我们还应检查发散,因为稍后我们将进行解释)。如果一切正常,我们再检查 MCSE 是否足够低,适用于我们希望报告的参数和精度。希望对于大多数问题,MCSE 都会远低于我们所需的精度。
过多的数字可能会有害
在报告文本、表格或图表时,重要的是要意识到过多的数字会让数字难以读取和理解。数字如 0.9 比 0.909297 更容易阅读,也更容易记住。此外,当报告的数字位数超过实际需要时,技术受众可能会认为你暗示着比实际存在的更高的显著性。因此,你会误导这些受众,让他们试图从本无意义的差异中寻找含义。最后,过多的数字会让你的图表、表格和图形看起来凌乱且视觉上令人不堪重负。所以,请时刻记住要考虑你的受众的数据兴趣和上下文。
10.10 发散
我们现在将探讨发散(divergences),这是一种 NUTS 特有的诊断方法,因为它基于方法的内部机制,而不是生成样本的属性。发散是一种强大且敏感的诊断方法,它表明采样器很可能已经发现了后验分布中的一个高曲率区域,该区域无法被正确探索。发散的一个优点是,它们通常出现在问题参数空间区域附近,因此我们可以利用它们来识别问题可能所在的位置。
让我们通过视觉辅助工具讨论发散:

图 10.14:model_c 和 model_nc 模型中选定参数的配对图
如你所见,图 10.14 显示了以下三个子图:
-
左侧子图:我们展示的是
model_c模型的两个参数的散点图;即,b参数的一个维度(我们随便挑了一个 – 你可以选择不同的维度),以及a参数的对数值。我们取对数是因为a被限制为正值(它是一个尺度参数)。在采样之前,PyMC 会将所有有界的参数转换为无界的参数。对于像a这样的参数,转换方法是取对数。我们在这里也做同样的处理,因为我们想理解采样器究竟在“看到”什么。好了,我们有一个散点图,其中灰色点表示样本。看看这个参数的形状,这种形状被称为 Neal 的漏斗形状,在层次模型中很常见。黑色点表示发散,它们散布在各处,但我们可以看到,许多黑点集中在漏斗的尖端附近。这个几何形状对大多数 MCMC 方法来说是有问题的,因为很难调整采样器,使其既能在漏斗尖端和顶部获取良好的样本,又能适应这种几何形状。一种是更“球形”的区域,采样器可以上下左右自由移动,另一种是“更窄”的区域,采样器必须主要沿上下方向移动,左右方向的移动则非常有限。 -
中间的子图:我们基本上与之前的情况相同,但对于
model_nc模型,现在漏斗形状更加明显。但我们没有出现发散。而且我们从前面的章节已经知道,这个模型的样本实际上更好。到底发生了什么?理解这一点的关键在于模型的定义。你会注意到,对于这个模型,b并没有被采样:b是一个确定性变量,是b_offset和a的组合,这两个参数在最后一个子图中被绘制出来。 -
右侧子图:我们有
b_offset与a的关系,可以看到几何形状更“球形”。正是这一点,而不是中间的子图,才是采样器“看到”的内容。因为这种几何形状更容易采样,所以我们没有出现发散,整体的诊断结果也更好。
改变模型的参数化是去除发散的一种方法,但除非你已经知道模型的另一种参数化方法,否则找到合适的参数化可能非常耗时。一个常见且容易尝试的替代方案是改变target_accept的值,这是pm.sample的一个参数。有时候,你可能需要同时更换参数化方法和target_accept的值。那么,什么是target_accept呢?它是控制 PyMC 中 NUTS 采样器调优的一个参数。它控制提议样本的接受率,默认值为 0.8。也就是说,接受 80%的提议样本。NUTS 采样器通过自适应调整哈密顿动力学仿真中的步长来实现目标接受率。80%是一个不错的默认值,但对于某些模型,你可能想尝试更大的值,如 0.90、0.95、0.99,甚至是 0.999,如果你不想完全失去希望的话。
10.11 保持冷静,继续尝试
当诊断显示问题时,我们应该怎么做?我们应该尝试修复它们。有时候,PyMC 会提供一些关于如何修改的建议。注意这些建议,你可以节省大量的调试时间。在这里,我列出了几个常见的操作,你可以采取:
-
检查拼写错误或其他低级错误。即使是专家,也很常犯这种“低级”错误。如果你拼错了一个变量名,很可能模型根本无法运行。但有时候,错误更加微妙,你仍然得到一个语法上有效的模型,它能运行,但语义却是错误的。
-
增加样本数量。这可能对一些轻微的问题有所帮助,比如当你接近目标 ESS(或 MCSE)时,或者当^R略高于 1.01 但差距不大时。
-
从轨迹的开始部分移除一些样本。当检查轨迹图时,你可能会观察到,前几步的部分样本相比于其余部分具有明显较高或较低的值,而其他部分看起来正常。如果是这种情况,简单地移除这些前几步的样本可能就足够了。这被称为预热,这在过去是非常常见的做法。现代的采样器已经减少了对它的需求。此外,PyMC 已经会丢弃调优阶段的样本,所以这个建议现在不再像以前那样有用。
-
修改采样器参数,例如增加调优阶段的长度,或者增加 NUTS 采样器的
target_accept参数。 -
转换数据。例如,对于线性回归模型,居中协变量(减去其均值)通常能加速采样器,并且减少采样问题。
-
花些时间思考你的先验分布。你不应该为了加速采样器或者去除不良诊断而调整先验。你应该用先验来编码已有的知识。但通常情况下,当你这么做时,也会让采样器的工作变得更加容易。使用像 PreliZ 和先验预测检查这样的工具,帮助你编码更好的先验分布。
-
重新参数化模型,即用一种不同但等效的方式表达模型。这并不总是容易的,但对于一些常见模型,如层级模型,你已经知道一些替代的参数化方法。
10.12 总结
在本章中,我们对一些最常用的计算后验分布的方法进行了概念性的讲解。我们特别强调了 MCMC 方法,这些方法旨在适用于任何给定的模型(或者至少广泛的模型范围),因此有时被称为通用推断引擎。这些方法是任何概率编程语言的核心,因为它们允许自动推断,从而让用户专注于迭代的模型设计和结果的解释。
我们还讨论了用于诊断样本的数值和视觉测试。没有良好的后验分布近似,贝叶斯框架的所有优势和灵活性都会消失。因此,在进行任何其他类型的分析之前,评估样本的质量是一个至关重要的步骤。
10.13 练习
-
使用网格方法与其他先验;例如,尝试
prior = (grid <= 0.5).astype(int)或prior = abs(grid - 0.5),或者尝试定义你自己的奇特先验。还可以尝试使用其他数据,例如增加数据的总量,或使数据在正反面次数上更加均匀或不均匀。 -
在估计 π 的代码中,保持
N固定并重新运行几次。注意到结果有所不同,因为我们使用的是随机数,但也要检查误差大致相同的顺序。尝试改变N点的数量,并重新运行代码。你能估算出N点的数量和误差之间的关系吗?为了更好的估算,你可能想修改代码,将误差作为N的函数进行计算。你还可以用相同的N运行几次代码,计算平均误差和误差的标准差。你可以使用 Matplotlib 的plt.errorbar()函数绘制这些结果。尝试使用一组N值,如 100、1000 和 10000;也就是一个数量级的差异。 -
修改你传递给 metropolis 函数的
dist参数;尝试使用 第一章 中的先验值。将此代码与网格方法进行比较;哪一部分需要修改,才能使用它来解决贝叶斯推断问题? -
将你在之前练习中的答案与 Thomas Wiecki 的代码进行比较:
twiecki.github.io/blog/2015/11/10/mcmc-sampling/ -
重温至少几个前面章节中的模型,并运行我们在本章中看到的所有诊断工具。
-
重温所有前几章的代码,找出那些存在偏差的地方,并尽量减少这些偏差。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,与志同道合的人一起学习,超过 5000 名成员在此一起成长: packt.link/bayesian

第十一章
接下来该做什么
统计学家是对一个愤世嫉俗的数据科学家的技术称谓。——吉姆·萨维奇
我写这本书是为了向那些已经熟悉 Python 及其数据处理库,但不太熟悉统计分析的读者介绍贝叶斯统计的主要概念和实践。通过阅读前十章,你应该对贝叶斯统计的许多主要主题有了合理的实际理解。虽然你不会成为一个专家级的贝叶斯“忍者黑客”(不管那是什么),但你应该能够创建自己的概率模型来解决自己的数据分析问题。如果你真心喜欢贝叶斯统计,这本书远远不够——没有一本书能够做到足够的深度。
要在贝叶斯统计中变得流利,你需要实践、时间、耐心、更多的实践、热情、问题,甚至更多的实践。你还将从不同的角度重新审视这些思想和概念中受益。为了收集更多材料,你应该查看 PyMC 文档www.pymc.io、Bambi 文档bambinos.github.io/bambi/、ArviZ 文档python.arviz.org以及 PreliZ 文档preliz.readthedocs.io。一定要查看示例部分,里面有许多本书涵盖的模型的示例,还有许多其他没有涵盖的模型示例。与 ArviZ 团队一起,我们正在编写一份名为《贝叶斯模型的探索性分析》的教育资源。我们希望这将是一个有用的参考,尤其是对于贝叶斯建模的初学者:arviz-devs.github.io/Exploratory-Analysis-of-Bayesian-Models/。
如果你想提供任何反馈,无论是关于文本还是代码的,你可以在github.com/aloctavodia/BAP3上提交问题。如果你有关于贝叶斯统计的问题,尤其是与 PyMC、ArviZ 或 Bambi 相关的问题,你可以在discourse.pymc.io上提问。
在以下列表中,我汇总了一些我认为你可能会觉得有用的资源,帮助你继续学习贝叶斯统计:
-
在
github.com/pymc-devs/pymc-resources的仓库中,你可以找到用 Python/PyMC 编写的代码,这些代码最初是为其他编程语言写的书籍中的内容。 -
《Python 中的贝叶斯建模与计算》是我合著的另一本书:
bayesiancomputationbook.com。虽然其中一些主题与本书中呈现的内容有重叠,但你可能仍然会发现它有用,因为许多内容是从不同的角度或使用不同的例子来呈现的。此外,一些主题也补充或扩展了我们在这里讨论的内容。 -
有很多关于贝叶斯统计的好书,但我最喜欢的入门/实用书籍包括统计学的重新思考、贝叶斯数据分析、回归与其他故事,以及贝叶斯规则! 如果你更偏向机器学习方面且希望更多数学内容,机器学习:一种概率视角是一个很好的资源。
-
学习贝叶斯统计 是一个每两周更新的播客,许多不同领域的研究者、开发者和实践者讨论贝叶斯统计:
learnbayesstats.com/。 -
PyMCon 是为贝叶斯社区设计的优先考虑异步的虚拟会议:
pymcon.com/。这里有很多精彩的讲座,同时也是你展示自己讲座的好机会! -
直观贝叶斯 是一系列付费课程和社区设计的项目,旨在帮助你从对贝叶斯感兴趣到快速成为实践者:
www.intuitivebayes.com/。
作为一个孩子,我曾梦想有飞行汽车、清洁无限的能源、在火星或月球上的假期,还有一个追求全人类福祉的全球政府……是的,我知道……我曾是一个梦想家!但由于种种原因,我们没有这些。相反,我们得到了完全无法想象的东西,至少在几十年前对我来说是不可思议的:相对容易访问到非常强大的计算方法(至少对某些人而言)。
计算机革命的副作用之一是,任何具备基本编程语言知识(如 Python)的人现在都可以使用大量用于数据分析、模拟和其他复杂任务的计算方法。我认为这是好事,但也意味着我们需要特别小心这些方法。我在本科时学习统计学的方式,以及必须记住各种固定方法的过程,既令人沮丧,又没有用,而且与这些进展完全无关。从个人角度来说,这本书或许是对那段令人沮丧经历的回应。我尝试写一本以建模方法为重点,并强调依据具体情境进行分析的统计学书籍。我不确定是否真正成功了,但如果没有成功,可能的原因之一是我仍然需要在这方面学习更多(也许,甚至,我们作为一个社区都需要更多地学习这方面的知识)。另一个原因是,恰当的统计分析应由领域知识和情境引导,而在一本面向广泛读者的入门书籍中,提供这种情境通常是困难的。不过,我希望我已经提供了一个理智、批判性的统计模型视角,一些有用的例子,以及足够的动力让你继续学习。
加入我们的社区 Discord 空间
加入我们的 Discord 社区,结识志同道合的人,并与 5000 多名成员一起学习: packt.link/bayesian

第十二章:参考书目
Oriol Abril-Pla, Virgile Andreani, Colin Carroll, Larry Dong, Christopher J. Fonnesbeck, Maxim Kochurov, Ravin Kumar, Jupeng Lao, Christian C. Luhmann, Osvaldo A. Martin, Michael Osthege, Ricardo Vieira, Thomas Wiecki, 和 Robert Zinkov。Pymc:一个现代化且全面的概率编程框架,基于 Python。PeerJ 计算机科学,9:e1516,2023。doi: 10.7717/peerj-cs.1516。
Agustina Arroyuelo,Jorge A. Vila,和 Osvaldo A. Martin。从贝叶斯视角探索蛋白质结构模型的质量。计算化学期刊,42(21):1466-1474,2021 年。doi: https://doi.org/10.1002/jcc.26556。网址 onlinelibrary.wiley.com/doi/abs/10.1002/jcc.26556。
Joseph K. Blitzstein。概率论入门,第 2 版。查普曼与霍尔/CRC,博卡拉顿,第 2 版,2019 年 2 月。ISBN 978-1-138-36991-7。
Luis Jorge Borges。虚构集。Sur,1944 年。
H. Jane Brockmann。马蹄蟹中的卫星雄性群体,Limulus polyphemus。动物行为学,102(1):1-21,1996 年。doi: https://doi.org/10.1111/j.1439-0310.1996.tb01099.x。网址 onlinelibrary.wiley.com/doi/abs/10.1111/j.1439-0310.1996.tb01099.x。
Peter G. Bryant 和 Marlene A. Smith。实用数据分析:商业统计案例研究。Irwin,芝加哥,1995 年。ISBN 978-0-256-15829-8。OCLC:726362789。
Tomás Capretto,Camen Piho,Ravin Kumar,Jacob Westfall,Tal Yarkoni,和 Osvaldo A Martin。Bambi:一个简单的 Python 贝叶斯线性模型拟合接口。统计软件期刊,103(15):1-29,2022 年。doi: 10.18637/jss.v103.i15。网址 www.jstatsoft.org/index.php/jss/article/view/v103i15。
Hugh A. Chipman,Edward I. George,和 Robert E. McCulloch。BART:贝叶斯加性回归树。应用统计年鉴,4(1):266-298,2010 年 3 月。ISSN 1932-6157。doi: 10.1214/09-AOAS285。
Aubrey Clayton。伯努利的谬误:统计学非逻辑与现代科学危机。哥伦比亚大学出版社,纽约,2021 年 8 月。ISBN 978-0-231-19994-0。
Scott Cunningham。因果推断:混音带。耶鲁大学出版社,纽黑文;伦敦,2021 年 1 月。ISBN 978-0-300-25168-5。
Jerome H. Friedman。贪心函数近似:一种梯度提升机器。统计年鉴,29(5):1189 - 1232,2001 年。doi: 10.1214/aos/1013203451。网址 doi.org/10.1214/aos/1013203451。
Andrew Gelman。统计计算的民间定理,2008 年 5 月。网址 statmodeling.stat.columbia.edu/2008/05/13/the_folk_theore/。
A. Goldstein, Adam Kapelner, Justin Bleich, 和 Emily Pitkin. 透视黑盒:通过个体条件期望的图表可视化统计学习. 计算与图形统计学杂志, 24:44 – 65, 2013 年。
Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, Robert Kern, Matti Picus, Stephan Hoyer, Marten H. van Kerkwijk, Matthew Brett, Allan Haldane, Jaime Fernández del Río, Mark Wiebe, Pearu Peterson, Pierre Gérard-Marchant, Kevin Sheppard, Tyler Reddy, Warren Weckesser, Hameer Abbasi, Christoph Gohlke, 和 Travis E. Oliphant. 使用 NumPy 进行数组编程. 自然, 585(7825):357–362, 2020 年 9 月. doi: 10.1038/s41586-020-2649-2. URL doi.org/10.1038/s41586-020-2649-2。
Allison Marie Horst, Alison Presmanes Hill, 和 Kristen B Gorman. palmerpenguins: Palmer 群岛(南极洲)企鹅数据, 2020 年. URL allisonhorst.github.io/palmerpenguins/. R 包版本 0.1.0。
Stephan Hoyer 和 Joe Hamman. Xarray:Python 中的 N 维标签数组和数据集. 开放研究软件杂志, 5(1), 2017 年 4 月. ISSN 2049-9647. doi: 10.5334/jors.148。
J. D. Hunter. Matplotlib:一个二维图形环境. 科学与工程计算, 9(3):90–95, 2007 年. doi: 10.1109/MCSE.2007.55。
Alejandro A. Icazatti, Oriol Abril-Pla, Arto Klami, 和 Osvaldo A. Martin. Preliz:一个用于先验引导的工具箱. 开放源代码软件杂志, 2023 年。
Gareth James, Daniela Witten, Trevor Hastie, Robert Tibshirani, 和 Jonathan Taylor. 统计学习导论: Python 应用. Springer, 2023 年 9 月. ISBN 978-3-031-39189-7。
Alex Kale, Francis Nguyen, Matthew Kay, 和 Jessica Hullman. 假设结果图帮助未经训练的观察者判断模糊数据中的趋势. IEEE 可视化与计算机图形学学报, 25(1):892–902, 2019 年. doi: 10.1109/TVCG.2018.2864909。
Robert E. Kass 和 Adrian E. Raftery. 贝叶斯因子. 美国统计学会杂志, 90(430):773–795, 1995 年 6 月. ISSN 0162-1459. doi: 10.1080/01621459.1995.10476572. URL www.tandfonline.com/doi/abs/10.1080/01621459.1995.10476572. 出版商: Taylor & Francis_eprint: https://www.tandfonline.com/doi/pdf/10.1080/01621459.1995.10476572。
Christian Kleiber 和 Achim Zeileis. 使用根图可视化计数数据回归. 美国统计学家, 70(3):296–303, 2016 年 7 月. ISSN 0003-1305, 1537-2731. doi: 10.1080/00031305.2016.1173590. URL arxiv.org/abs/1605.01311. arXiv:1605.01311 [stat]。
John Kruschke. 进行贝叶斯数据分析,第二版:使用 R、JAGS 和 Stan 的 教程。学术出版社,波士顿,第二版,2014 年 11 月。ISBN 978-0-12-405888-0。
Ravin Kumar, Colin Carroll, Ari Hartikainen, 和 Osvaldo Martin. Arviz:一个用于贝叶斯模型探索分析的统一库,适用于 Python。开放源代码软件杂志,4(33):1143,2019 年。
David J. C. MacKay. 信息理论、推理与学习 算法。剑桥大学出版社,剑桥,英国;纽约,2003 年 10 月。ISBN 978-0-521-64298-9。
Wes McKinney. Python 数据分析:使用 pandas、NumPy 和 Jupyter 进行数据清洗。O'Reilly 媒体,北京、波士顿、法纳姆、塞巴斯托波尔、东京,2022 年 9 月。ISBN 978-1-09-810403-0。
Yann McLatchie, Sölvi Rögnvaldsson, Frank Weber, 和 Aki Vehtari. 稳健且高效的投影预测推断,2023 年 6 月。网址 arxiv.org/abs/2306.15581。arXiv:2306.15581 [stat]。
Petrus Mikkola, Osvaldo A. Martin, Suyog Chandramouli, Marcelo Hartmann, Oriol Abril Pla, Owen Thomas, Henri Pesonen, Jukka Corander, Aki Vehtari, Samuel Kaski, Paul-Christian Bürkner, 和 Arto Klami. 先验知识引导:过去、现在与未来。贝叶斯分析,第 1 – 33 页,2023 年。doi: 10.1214/23-BA1381。网址 doi.org/10.1214/23-BA1381。
Kevin P. Murphy. 机器学习:概率视角。MIT 出版社,剑桥,MA,第一版,2012 年 8 月。ISBN 978-0-262-01802-9。
Kevin P. Murphy. 概率机器学习:高级主题。MIT 出版社,2023 年。网址 probml.github.io/book2。
K. W. Penrose, A. G. Nelson, 和 A. G. Fisher. 男性使用简单测量技术的广义体成分预测方程。医学与运动科学 在体育与运动中的应用,17(2):189,1985 年 4 月。ISSN 0195-9131。网址 journals.lww.com/acsm-msse/citation/1985/04000/generalized_body_composition_prediction_equation.37.aspx。
Juho Piironen 和 Aki Vehtari. 马鞍分布和其他收缩先验中的稀疏性信息与正则化。电子统计学杂志,11(2):5018 – 5051,2017 年。doi: 10.1214/17-EJS1337SI。网址 doi.org/10.1214/17-EJS1337SI。
Juho Piironen, Markus Paasiniemi, 和 Aki Vehtari. 高维问题中的投影推断:预测与特征选择。电子统计学杂志,14(1):2155 – 2197,2020 年。doi: 10.1214/20-EJS1711。网址 doi.org/10.1214/20-EJS1711。
Miriana Quiroga, Pablo G Garay, Juan M. Alonso, Juan Martin Loyola, 和 Osvaldo A Martin. 贝叶斯加性回归树在概率编程中的应用,2022 年。
Sebastian Raschka, Yuxi Liu, Vahid Mirjalili, 和 Dmytro Dzhulgakov. 使用 PyTorch 和 Scikit-Learn 的机器学习:用 Python 开发机器学习和深度学习模型。Packt 出版公司,伯明翰 孟买,2022 年 2 月。ISBN 978-1-80181-931-2。
Carl Edward Rasmussen 和 Christopher K. I. Williams. 机器学习中的高斯过程。麻省理工学院出版社,剑桥,马萨诸塞州,2005 年 11 月。ISBN 978-0-262-18253-9。
Riemann 积分。Riemann 积分,2023 年 5 月。网址 en.wikipedia.org/w/index.php?title=Riemann_integral&oldid=1152570392。页面版本 ID:1152570392。
Gabriel Riutort-Mayol, Paul-Christian Bürkner, Michael R. Andersen, Arno Solin, 和 Aki Vehtari. 用于概率编程的实用希尔伯特空间近似贝叶斯高斯过程,2022 年。
Aki Vehtari, Andrew Gelman, Daniel Simpson, Bob Carpenter, 和 Paul-Christian Bürkner. 排名归一化、折叠和局部化:一种改进的R 用于评估 MCMC 收敛性(附讨论)。贝叶斯分析,16(2):667 - 718,2021 年。doi: 10.1214/20-BA1221。网址 doi.org/10.1214/20-BA1221。
Wes McKinney. Python 中的统计计算数据结构。在 Stéfan van der Walt 和 Jarrod Millman 编辑的第九届 Python 科学会议论文集中,第 56 - 61 页,2010 年。doi: 10.25080/Majora-92bf1922-00a。
Peter H Westfall. 峰度作为尖峭度,1905-2014。rip。美国统计学家,68(3):191-195,2014 年。
Andrew Gordon Wilson 和 Pavel Izmailov. 贝叶斯深度学习与泛化的概率视角,2022 年 3 月。网址 arxiv.org/abs/2002.08791。arXiv:2002.08791 [cs, stat]。

订阅我们的在线数字图书馆,您将能全面访问超过 7,000 本书籍和视频,并获得行业领先的工具来帮助您规划个人发展和职业进步。有关更多信息,请访问我们的网站。
第十三章:为什么订阅?
[nosep]通过来自超过 4,000 位行业专家的实用电子书和视频,减少学习时间,增加编码时间。通过专为您设计的技能计划改善学习,每月获得免费的电子书或视频,完全可搜索,轻松访问重要信息,可以复制粘贴、打印和书签内容。
您知道 Packt 为每本出版的书籍提供电子书版本,包括 PDF 和 ePub 格式吗?您可以在 packt.com 升级到电子书版本,作为纸质书客户,您有权获得电子书版本的折扣。如需更多信息,请通过 customercare@packtpub.com 与我们联系。
在www.packt.com,您还可以阅读一系列免费的技术文章,注册各种免费
其他您可能喜欢的书籍
如果您喜欢这本书,您可能会对 Packt 出版的其他书籍感兴趣:
使用 PyTorch 和 Scikit-Learn 进行机器学习
Sebastian Raschka、Yuxi(Hayden)Liu、Vahid Mirjalili
ISBN: 978-1-80181-931-2
-
探索机器从数据中学习的框架、模型和技术
-
使用 scikit-learn 进行机器学习,使用 PyTorch 进行深度学习
-
在图像、文本等数据上训练机器学习分类器
-
构建和训练神经网络、变换器和提升算法
-
探索评估和调整模型的最佳实践
-
使用回归分析预测连续目标结果
-
深入探索文本和社交媒体数据,使用情感分析
《使用 R 进行机器学习(第四版)》
Brett Lantz
ISBN: 978-1-80107-132-1
-
学习从原始数据到实现的端到端机器学习过程
-
使用最近邻和贝叶斯方法分类重要结果
-
使用决策树、规则和支持向量机预测未来事件
-
使用回归方法预测数值数据和估算财务值
-
使用人工神经网络建模复杂过程,使用 tidyverse 准备、转换和清理数据
-
评估您的模型并提高其性能
-
将 R 与 SQL 数据库及新兴的大数据技术(如 Spark、Hadoop、H2O 和 TensorFlow)连接
使用 Python 进行时间序列机器学习
Ben Auffarth
ISBN: 978-1-80181-962-6
-
理解时间序列的主要类别,并学习如何检测异常值和模式
-
选择合适的方法来解决时间序列问题
-
通过自相关和统计技术描述季节性和相关性模式
-
掌握时间序列数据可视化
-
理解经典的时间序列模型,如 ARMA 和 ARIMA
-
实现深度学习模型,如高斯过程、变压器和最先进的机器学习模型
-
熟悉像 Prophet、XGboost 和 TensorFlow 等多个库
Packt 正在寻找像你这样的作者
如果你有兴趣成为 Packt 的作者,请访问 authors.packtpub.com 并立即申请。我们已经与成千上万的开发者和技术专家合作,帮助他们将自己的见解分享给全球技术社区。你可以进行一般申请,申请我们正在招聘的特定热门话题,或提交你自己的创意。
分享你的想法
阅读完 Python Bayesian 分析(第三版) 后,我们很想听听你的想法!扫描下面的二维码,直接进入该书的亚马逊评论页面,分享你的反馈。

你的评论对我们和技术社区非常重要,将帮助我们确保提供高质量的内容。


=
= 0.5,在八次试验中看到四次正面朝上比在两次试验中看到一次正面朝上能更有信心地认为偏差是 0.5。这个直觉在后验分布中有所体现,您可以自己检查,如果注意观察第三和第六个子图中的(黑色)后验分布;虽然众数相同,但第三个子图的扩展(不确定性)比第六个子图更大。

作为比例 4
。
。
。
浙公网安备 33010602011771号