PyMC 架构解析:从概率图模型定义到后验推断的模块化设计
【免费下载链接】pymcBayesian Modeling and Probabilistic Programming in Python项目地址: https://gitcode.com/GitHub_Trending/py/pymc
导读:本文基于 PyMC 仓库中的 ARCHITECTURE.md,系统梳理 PyMC 的高层架构设计:概率图模型的定义、对数概率计算与随机采样如何被组织为相互独立的模块,PyTensor 与 ArviZ 如何在外围承担张量运算与诊断绘图职责,以及
pymc.model、distributions/、sampling.py、step_methods/、tests/等核心代码单元的职责边界。读完本文,你将掌握 PyMC 的整体分层模型,能够快速定位"模型定义—对数概率—采样器—结果诊断"这条主链路在源码中的具体落点,并理解为什么这类架构设计有利于推理算法与分布实现的独立演进。
PyMC 的定位是"贝叶斯建模与概率编程框架":它允许用户定义概率图(probabilistic graph)或模型,并基于该图便捷地完成两类核心计算——计算对数概率(用于后验推断)以及抽取随机样本(用于先验/后验预测)。围绕这两条主线,PyMC 提供了多种推断技术,其中最主要的是:
- 马尔可夫链蒙特卡洛(MCMC):包括 Hamiltonian Monte Carlo 与 NUTS 等步进方法;
- 变分推断(Variational Inference):如 ADVI、FullRankADVI 与 Stein 变分梯度下降;
- 序列蒙特卡洛(Sequential Monte Carlo,SMC):适用于多模态与不可微模型的采样方法。
此外,PyMC 还内置了 Graphviz 模型可视化工具(见 Model.to_graphviz 与 model_graph.py)以及各类数学辅助函数。官方架构图清晰展示了这些模块与外部包的协作关系(注意:图中并未列出全部模块,一些小而自明的模块以及处于弃用流程中的模块被省略了)。
职责边界:什么"不在" PyMC 中
理解 PyMC 架构的第一步,恰恰是先划清它的边界。PyMC 刻意将大量底层与外围工作委托给专门库,避免重复造轮子:
PyTensor:张量计算引擎
PyTensor 承担了 PyMC 中几乎所有与张量操作相关的底层工作,包括:
- 梯度计算:NUTS 采样所需的
dlogp即由 Model.dlogp 构造对数概率图后交给 PyTensor 的gradient求导完成; - 随机数生成:所有分布变量在底层都是 PyTensor 的
RandomVariable(见 pymc/distributions/distribution.py 中Distribution.rv_op的定义); - 底层张量运算定义与**底层操作图(operation graph)**的构建、重写与编译。
因此 PyMC 中出现的TensorVariable、FunctionGraph等类型均来自 PyTensor,PyMC 更多扮演"概率语义"层。
ArviZ:推断结果的后处理
ArviZ 负责推断完成之后的全部"下游"工作,包括:
- 绘图:迹线图(trace plots)、排名图(rank plots)、后验图(posterior plots)等;
- MCMC 采样诊断:R-hat、有效样本量(Effective Sample Size)等收敛指标(对应 pymc/backends/arviz.py 中把采样结果转换为 ArviZ 数据结构的逻辑);
- 模型比较,尤其是高效的留一法交叉验证近似(LOO);
- 面向贝叶斯推断数据存储与操作的数据结构(
InferenceData)。
PyMC 的pm.sample()返回的正是 ArviZ 的InferenceData对象,其内部由 backends/ 目录中的多个后端(arviz、ndarray、zarr、mcbackend)负责落盘与传输。
核心模块逐层拆解
PyMC 的代码库由根目录下的单文件 Python 模块与按功能逻辑分组的子目录共同构成。作者在架构文档中坦诚指出:单文件与目录的划分并非由严格标准决定,通常只是当单个.py文件"过大"时才拆分为目录。下面以文档给出的"简单 MCMC 模型"为例,逐一深入各模块。
import pymc as pm with pm.Model() as model: theta = pm.Beta("theta", alpha=1, beta=2) p = pm.Beta("n", p=theta, n=2, observed=[1, 2]) inf_data = pm.sample()注:原文示例中的
pm.Beta("n", p=theta, n=2, ...)系笔误(Beta的第二参数是beta而非n);更常见的可运行写法是pm.Binomial("n", p=theta, n=2, observed=[1, 2])。此处保留原文形态以对照架构文档,实际使用时请按正确的分布参数编写。
pymc.model:模型定义与求值的核心
pymc/model/ 承载了模型定义与模型求值方法所需的一切原语,是理解 PyMC 架构的起点。架构文档点名的四个关键组件全部位于 pymc/model/core.py:
| 组件 | 源码位置 | 职责 |
|---|---|---|
ContextMeta | core.py#L308-L317 | 元类(metaclass),在Model.__init__期间自动激活模型上下文,从而支持with pm.Model() as model:语法 |
Model/BaseModel | core.py#L334 | 模型容器,维护free_RVs、observed_RVs、deterministics、potentials等变量注册表 |
ValueGradFunction | core.py#L142 | 构建"同时计算数值与梯度"的 PyTensor 函数,是 PyMC 与 PyTensor 的主要连接点 |
Deterministic/Potential | core.py#L2467 / core.py#L2554 | 模型定义中的两类特殊节点 |
上下文管理机制。ContextMeta是Model的元类,其__call__方法在实例化时先__new__出实例、随即with instance:进入上下文、再执行__init__。BaseModel.__enter__/__exit__维护线程安全的MODEL_MANAGER.active_contexts栈(core.py#L101-L127),这也是modelcontext()与Model.get_context()能够"找到当前模型"的根本原因。
Factor的历史角色与现状。架构文档提到Factor"为模型定义各类对数概率方法"。需要说明的是,在本文所对应的仓库版本中,这一角色已由独立的 pymc/logprob/ 包(包含abstract.py、basic.py、transforms.py等)与Model上的logp/dlogp/d2logp方法承担,pymc.Factor类已不在当前代码库中——这正是架构文档强调"并非所有模块都被列出,有些处于弃用流程中"的典型例证。当前求值链路的实际入口是:
- Model.logp:汇总
free_RVs + observed_RVs + potentials的逐项对数概率,经transformed_conditional_logp得到含变换 Jacobian 的联合对数概率图; - Model.dlogp:对
logp图调用 PyTensorgradient求梯度,供 HMC/NUTS 使用; - Model.compile_logp 等编译方法:把图编译为可被采样器反复调用的
PointFunc。
Deterministic与Potential。这两个函数分别解决两类模型定义需求:
Deterministic(name, var)注册一个"确定性节点",用于在采样迹线中记录中间计算结果(如逻辑回归中的p = pm.Deterministic("p", pm.math.invlogit(alpha * x + intercept)))。文档与源码 core.py#L2523-L2532 特别强调:它不会带来性能损失,因为确定性节点在主计算图之外单独求值,主图可被优化得如同不存在该节点一样,而 NUTS 每步可能求值数千次的只是主图,确定性量在每步末尾用最终变量值计算一次即可;Potential(name, var)向模型联合对数概率中追加任意项,典型用途是施加软/硬约束。源码 core.py#L2554-L2600 同时给出重要警告:Potential 项只影响概率类采样(如pm.sample),不影响前向采样(如sample_prior_predictive/sample_posterior_predictive),对含 Potential 的模型做前向采样时会抛出警告。
distributions/:分布家族与对数概率
pymc/distributions/ 包含多个子模块,既定义各类分布,也提供服务于分布使用的辅助逻辑。架构文档点名的三个重点文件如下(注意个别文件已随版本演进改名或迁移):
distribution.py:所有 PyMC 分布的父类Distribution所在(distribution.py#L465)。其__new__方法(distribution.py#L475-L524)是整个 PyMC 变量创建的枢纽:在上下文中取得当前Model,解析dims、observed、initval、total_size、transform等参数后调用Model.register_rv完成注册。文档特别强调的observed参数正是在此发挥作用——observed的有无区分了"随机变量分布"与"似然分布":未观测的变量进入free_RVs(作为采样目标),带观测数据的变量进入observed_RVs(其logp项会保留在联合概率中)。- 对数概率逻辑:架构文档将其描述为
distributions/logprob.py,而在当前版本中,分布与图节点的对数概率推导已被系统化地重写为独立的 pymc/logprob/ 包(含basic.py、abstract.py、transforms.py、mixture.py、scan.py、censoring.py等十余个模块),对数概率的计算最终委托给 PyTensor,PyMC 只负责把概率语义(如变换的 Jacobian 修正)翻译成可求导的符号图。 dist_math.py:分布专用的便捷算子集合(pymc/distributions/dist_math.py),包括logpow等数学算子、一系列对数正态方法与变换方法。与之并列的还有moments/(矩的符号计算,供initval与support_point使用)以及shape_utils.py等形状推导工具。
sampling.py:采样入口与步进方法初始化
架构文档中的sampling.py在当前仓库中对应 pymc/sampling/ 目录,其职责是:
- 后验/先验/后验预测采样接口:
sample定义于 mcmc.py#L554-L620,先验/后验预测采样则位于 forward.py(sample_prior_predictive、sample_posterior_predictive); - 步进方法的识别与初始化:根据模型变量类型选择合适的 stepper;
- "全连续"检查与 NUTS 初始化:
all_continuous(vars)(mcmc.py#L364)判断模型是否全部为连续变量,init_nuts(mcmc.py#L1759)则负责质量矩阵(mass matrix)的初始化。
init_nuts的实现细节很能体现架构设计:NUTS 的收敛与采样速度高度依赖质量/缩放矩阵的选择,因此该函数提供了init参数,支持auto(默认解析为jitter+adapt_diag)、adapt_diag、jitter+adapt_diag_grad、advi+adapt_diag、advi、advi_map、map、adapt_full、jitter+adapt_full等多种初始化策略;同时通过model.logp_dlogp_function(ravel_inputs=True)(内部正是ValueGradFunction)构造值-梯度函数,并用_init_jitter对初始点加均匀抖动、以jitter_max_retries控制重试次数,最终返回(initial_points, NUTS)元组供采样循环使用。
step_methods/:步进方法家族
pymc/step_methods/ 汇集了各种采样算法对应的步进方法,支撑 MCMC 与 SMC。其中:
- step_methods/hmc/ 包含 Hamiltonian Monte Carlo 采样方法族及其辅助函数——
hmc.py、nuts.py(NUTS 实现)、integration.py(蛙跳/辛积分器)、quadpotential.py(二次型势能,即质量矩阵的抽象)与base_hmc.py(HMC 基类); - step_methods/metropolis.py 与 step_methods/slicer.py 分别提供 Metropolis 类方法与切片采样;
- step_methods/arraystep.py、compound.py(复合步进)与 state.py 提供了步进方法的基类、组合与状态持久化。
由于离散变量无法使用 HMC/NUTS,sample会在内部为离散变量装配 Metropolis 等步进器,并与连续变量的 NUTS 组成复合步进器——这正是sampling模块执行"识别并初始化 stepper"职责的具体体现。
tests/:测试的组织方式
tests/ 存放全部功能测试。其组织规则与代码库的模块划分保持镜像:所有以test_前缀命名的模块都是测试本体,其余模块则是支撑代码(fixtures、配置、共享模型等)。例如 tests/models.py 提供可在多个测试间复用的模型定义,tests/helpers.py 与 tests/sampler_fixtures.py 提供断言与采样夹具。分布测试(如 tests/distributions/test_continuous.py)、对数概率测试(tests/logprob/)、步进方法测试(tests/step_methods/)与采样测试(tests/sampling/)分别验证对应模块的行为,其中test_random_alternative_backends.py还验证了 JAX 等替代后端上的随机行为一致性。
模型可视化:Graphviz 工具链
除了推断与求值,PyMC 还提供模型结构可视化能力。架构文档提及的 Graphviz 工具由 Model.to_graphviz 与 pymc/model_graph.py 实现,可将模型变量及其依赖关系渲染为有向图,便于在建模阶段检查变量连接与观测/未观测状态。
从架构回望:一条完整的主调用链
把上述模块串起来,pm.sample()背后的主干链路可以概括为:
- 模型定义:
with pm.Model()经由ContextMeta激活上下文,pm.Beta(...)通过Distribution.__new__→Model.register_rv把随机变量注册到模型; - 图构建:
Model.logp汇总自由变量、观测变量与 potential 的logp项,经 logprob/ 与变换系统得到含 Jacobian 的联合对数概率符号图; - 编译求值:
ValueGradFunction把logp与dlogp图编译为 PyTensor 函数,为 NUTS 的每一步提供数值与梯度; - 步进与初始化:
init_nuts(含all_continuous检查)确定质量矩阵初始化策略,step_methods/hmc/中的积分器与 NUTS 算法驱动采样; - 结果交付:采样迹线经 backends/ 转成 ArviZ
InferenceData,供用户做 R-hat 诊断、绘制后验图或进一步模型比较。
这条链路的每一环都能在本文对应的模块划分中找到精确的落点——这也是 PyMC 将"概率语义"(PyMC)、"张量计算"(PyTensor)与"诊断分析"(ArviZ)分层解耦的架构价值所在。对于希望深入源码的读者,建议从 pymc/model/core.py 出发,沿register_rv → logp → compile_logp的路径逐段阅读,再结合 tests/model/test_core.py 中的断言理解各 API 的实际行为约束。
【免费下载链接】pymcBayesian Modeling and Probabilistic Programming in Python项目地址: https://gitcode.com/GitHub_Trending/py/pymc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考