本文档面向把
ceffyl.Sampler输出的(并行退火)MCMC 链转换成 Bayesian evidence(边际似然,$\log Z$)的场景。
适用于:likelihood / prior 与(例如 UltraNest)nested sampling 使用 同一套pta.get_lnlikelihood、pta.get_lnprior(或 prior transform)时,对两种方法得到的 $\log Z$ 进行一致比较。
1. 核心概念与常见误区
1.1 Evidence 的定义
Bayesian evidence(边际似然)定义为:
\[Z \equiv p(y) = \int p(\theta)\, p(y\mid\theta)\, d\theta = \int p(\theta)\, L(\theta)\, d\theta\]常用对数形式:
\[\log Z = \log \int p(\theta)\, L(\theta)\, d\theta\]其中:
- $p(\theta)$ 是 归一化的 prior 密度(否则 $\log Z$ 会差一个常数)
- $L(\theta)$ 是 likelihood
1.2 为什么“只有一条冷链”算不出 TI evidence
热力学积分(Thermodynamic Integration,TI)需要一族 power posterior:
\[p_\beta(\theta\mid y) \propto p(\theta)\,L(\theta)^\beta,\quad \beta\in[0,1]\]其中 $\beta$ 是逆温度($\beta = 1/T$)。TI 的公式是:
\[\log Z = \int_0^1 \left\langle \log L(\theta)\right\rangle_\beta\, d\beta\]这里 $\langle \cdot \rangle_\beta$ 表示在 $p_\beta(\theta\mid y)$ 下取期望。
重要: 若输出里只有一条 $\beta=1$(冷链,$T=1$)的链,那么只有一个点 $\left\langle \log L\right\rangle_{\beta=1}$,无法完成 $[0,1]$ 的积分,因此 TI evidence 无法从单链得到。
常见误区:人为补一个 $\beta=0$ 点并令 $\langle\log L\rangle_{\beta=0}=\langle\log L\rangle_{\beta=1}$,会让积分退化成
\(\int_0^1 \text{常数}\, d\beta = \langle\log L\rangle_{\beta=1}\) 得到的只是 后验下的平均 log-likelihood,不是 evidence。
1.3 用公式解释 “mean(logL) 比 logZ 大”的原因
当 prior 归一化时,有恒等式:
\[\log Z = \mathbb{E}_{p(\theta\mid y)}[\log L(\theta)] - D_{\mathrm{KL}}\!\left(p(\theta\mid y)\,\|\,p(\theta)\right)\]由于 $D_{\mathrm{KL}}\ge 0$,因此:
\[\log Z \le \mathbb{E}_{post}[\log L]\]这解释了:当 TI 被误算成 $\mathbb{E}_{post}[\log L]$ 时,数值往往会比 nested sampling 的 $\log Z$ 更不负(更大),并且二者差值正好对应 KL 信息增益(Occam 因子)。
2. 让 ceffyl.Sampler 产出多温度链(Parallel Tempering)
ceffyl.Sampler 底层使用 PTMCMCSampler。多温度链通常依赖 MPI 并行:启动多少个 MPI 进程,就对应多少条温度链。
2.1 编写采样脚本 run_pt.py
下面脚本展示了最小可用配置:指定 Tmax,并启用 writeHotChains=True 写出热链文件。
# run_pt.py
from ceffyl.Sampler import Sampler
# 需要保证 pta_NG15 已在脚本里正确构造
# 例如:pta_NG15 = ...
outdir = "./mcmc2"
sampler = Sampler.setup_sampler(
pta_NG15,
outdir=outdir,
logL=pta_NG15.ln_likelihood, # 或者 pta_NG15.get_lnlikelihood 的封装
logp=pta_NG15.ln_prior, # MCMC 场景使用 ln_prior(不是 prior transform)
resume=False,
jump=True,
)
x0 = pta_NG15.initial_samples()
sampler.sample(
x0,
int(20_000_000),
Tmin=1,
Tmax=1e3, # 可调:见 2.3 节
Tskip=100,
isave=1000,
thin=10,
writeHotChains=True, # 关键:写出热链
)
2.2 用 MPI 启动(16 核 → 16 条链)
mpirun -np 16 python run_pt.py
运行结束后,outdir 中应出现多条链文件(具体命名取决于 ceffyl/PTMCMCSampler 版本),例如:
chains_1.txt(冷链)chains_2.txt、chains_3.txt…(热链) 或按温度/β 命名的变体。
2.3 Tmax 不是越大越好:如何选择
对固定链数 $N_\mathrm{temp}$,若使用几何温度梯子,相邻温度比大致为:
\[r \approx \left(\frac{T_{\max}}{T_{\min}}\right)^{1/(N_\mathrm{temp}-1)}\]- $T_{\max}$ 太小:最热链不够“热”,$\beta_{\min}=1/T_{\max}$ 不够接近 0,TI/SS 在低 $\beta$ 端误差变大
- $T_{\max}$ 太大:相邻温度差变大,swap 接受率降低,链间交换变差,均值 $\langle\log L\rangle_\beta$ 偏差与方差都可能变大
实践上更好的目标是同时满足:
1) 最热端已经让 $\langle\log L\rangle_\beta$ 对 $\beta$ 的变化 趋于平缓
2) 相邻温度的 swap 接受率 不至于过低(经验上常希望在 $0.1\sim0.3$ 量级)
3. 用 la_forge 读取多温度链并计算 evidence
3.1 读取链(cold + hot)
import la_forge.core as co
outdir = "./mcmc2"
c = co.Core(outdir, burn=0.85, pt_chains=True)
# 参数列名列表
print("params:", c.params)
print("has hot_chains:", bool(c.hot_chains))
if c.hot_chains:
print("hot chain keys:", list(c.hot_chains.keys())[:10])
若 hot_chains 为空,通常是:
- 实际没有写出热链(
writeHotChains=False) - 文件命名不被
la_forge识别(必要时可用软链接/重命名,让热链文件符合la_forge的扫描规则)
4. Evidence 的两种常用估计:TI 与 Stepping-Stone(SS)
4.1 准备:提取 $\beta$ 与 $\langle\log L\rangle_\beta$
import numpy as np
import la_forge.core as co
outdir = "./mcmc2"
c = co.Core(outdir, burn=0.85, pt_chains=True)
lnlike_idx = c.params.index("lnlike")
pairs = []
# 冷链:T=1 -> beta=1
pairs.append((1.0, c.chain[c.burn:, lnlike_idx]))
# 热链:key 常是温度或 beta 的字符串(视版本而定)
for key, ch in (c.hot_chains or {}).items():
# 常见情况:key 是温度 T
T = float(key)
beta = 1.0 / T
pairs.append((beta, ch[c.burn:, lnlike_idx]))
# beta 升序
pairs.sort(key=lambda t: t[0])
betas = np.array([b for b, _ in pairs])
lnL_series = [x for _, x in pairs]
print("beta_min =", betas[0], "T_max ~", 1.0/betas[0])
print("N_temps =", len(betas))
若 key 实际是 $\beta$ 而非温度 $T$,应改为
beta=float(key)(并去掉1.0/T)。
一个简单识别方式:若key里出现1.0、0.3这类小于 1 的数,通常更像 $\beta$;若普遍大于 1,则更像温度 $T$。
4.2 方法 A:热力学积分(TI)
TI 估计:
\[\log Z \approx \int_0^1 \langle \log L\rangle_\beta\, d\beta\]用 Simpson 积分(或 trapezoid):
import numpy as np
from scipy.integrate import simpson
# 计算每个 beta 的均值
ElogL = np.array([x.mean() for x in lnL_series])
# (推荐)只对已覆盖的 beta 做积分;不要在缺少高温端时“硬补 beta=0”
logZ_ti = simpson(ElogL, x=betas)
print("logZ (TI, covered beta-range) =", logZ_ti)
4.2.1 低 $\beta$ 端点的处理($\beta\to 0$)
严格的 TI 需要覆盖 $\beta\in[0,1]$。若最小 $\beta_{\min}$ 仍不够小,有三种可选策略:
1) 增大 $T_{\max}$ 或增加链数:让 $\beta_{\min}$ 更接近 0
2) 加入 prior sampling 估计 $\langle\log L\rangle_{\beta=0}$:
$\beta=0$ 时,$p_\beta(\theta)\equiv p(\theta)$,因此可从 prior 抽样计算 $\log L$ 并取均值
3) 在小 $\beta$ 区间外推:用最小几条链拟合 $\langle\log L\rangle_\beta$ 在 $\beta\to 0$ 的趋势(通常不如 1/2 稳)
注意:策略 2 要求能从 prior 采样。若已有 nested sampling 的 prior transform(把 $u\sim U[0,1]^d$ 映射到 $\theta$),那是最方便的方式。
下面给出“有 prior transform 时”的 prior 采样估计(示例接口名需按工程实际替换):
import numpy as np
# 假设存在 prior_transform(u) -> theta
# u: shape (d,) in [0,1]
# theta: parameter vector compatible with pta.get_lnlikelihood
def estimate_ElogL_beta0(pta, prior_transform, nsamp=20000, seed=0):
rng = np.random.default_rng(seed)
d = len(pta.param_names)
vals = []
for _ in range(nsamp):
u = rng.random(d)
theta = prior_transform(u)
vals.append(pta.get_lnlikelihood(theta))
return float(np.mean(vals)), float(np.std(vals, ddof=1)/np.sqrt(nsamp))
E0, E0_se = estimate_ElogL_beta0(pta_NG15, prior_transform=pta_NG15.prior_transform)
print("E[lnL] at beta=0 ~", E0, "+/-", E0_se)
将 $\beta=0$ 点加入积分(例如把它作为一个额外点插入到 betas 与 ElogL 的开头):
from scipy.integrate import simpson
import numpy as np
ElogL = np.array([x.mean() for x in lnL_series])
if betas[0] > 0:
betas2 = np.r_[0.0, betas]
ElogL2 = np.r_[E0, ElogL] # 用 prior 采样给出的 E0
else:
betas2, ElogL2 = betas, ElogL
logZ_ti_full = simpson(ElogL2, x=betas2)
print("logZ (TI, with beta=0 prior estimate) =", logZ_ti_full)
4.3 方法 B:Stepping-Stone(SS)(常比 TI 更稳)
定义 $\beta_0=0<\beta_1<\cdots<\beta_K=1$。令
\[Z_\beta=\int p(\theta)\,L(\theta)^\beta\, d\theta\]在分布 $p_{\beta_k}$ 下有:
\[\mathbb{E}_{\beta_k}\!\left[L(\theta)^{\beta_{k-1}-\beta_k}\right] = \frac{Z_{\beta_{k-1}}}{Z_{\beta_k}}\]因此(若 prior 归一化,$Z_{\beta_0}=1$):
\[\log Z = -\sum_{k=1}^{K} \log \mathbb{E}_{\beta_k}\!\left[L(\theta)^{\beta_{k-1}-\beta_k}\right]\]用 log-sum-exp 稳定计算:
import numpy as np
from scipy.special import logsumexp
def logZ_stepping_stone(betas, lnL_series, logZ_beta0=0.0):
# betas: ascending array, should include 1.0; can optionally include 0.0
# lnL_series[k]: samples of lnL at beta=betas[k]
betas = np.asarray(betas)
logZ = float(logZ_beta0)
for k in range(1, len(betas)):
db = betas[k-1] - betas[k] # negative number if betas ascending
lnL = np.asarray(lnL_series[k])
# E_{beta_k}[exp(db * lnL)] (db <= 0)
log_mean = logsumexp(db * lnL) - np.log(len(lnL))
logZ += -log_mean
return float(logZ)
# 如果 betas 不含 0,可把 beta=0 的 prior 采样加入(推荐)
# 例如:betas2 = [0] + betas, lnL_series2 = [prior_lnL_samples] + lnL_series
logZ_ss = logZ_stepping_stone(betas, lnL_series, logZ_beta0=0.0)
print("logZ (SS, covered beta-range) =", logZ_ss)
若能提供 $\beta=0$ 的 prior 采样链(或至少 prior 的 lnL 样本),把它作为第 0 级 stepping stone 会更稳。
5. 结果一致性检查:确认 MCMC 链的 lnlike 与 PTA likelihood 完全一致
为排除 “$\log L$ 定义不一致(差常数/模型不同)” 导致的 evidence 偏差,可抽样对比:
import numpy as np
import la_forge.core as co
outdir = "./mcmc2"
c = co.Core(outdir, burn=0.85, pt_chains=True)
lnlike_idx = c.params.index("lnlike")
# 建议使用 pta.param_names 的顺序与链对齐
pta_names = getattr(pta_NG15, "param_names", None)
if pta_names is None:
pta_names = [par.name for par in pta_NG15.params]
col_idx = [c.params.index(n) for n in pta_names]
rng = np.random.default_rng(0)
rows = rng.integers(low=c.burn, high=c.chain.shape[0], size=20)
diffs = []
for r in rows:
theta = c.chain[r, col_idx]
ll_chain = c.chain[r, lnlike_idx]
ll_eval = pta_NG15.get_lnlikelihood(theta)
diffs.append(ll_chain - ll_eval)
diffs = np.array(diffs)
print("lnlike(chain)-lnlike(pta) mean =", diffs.mean(), "std =", diffs.std())
若均值与标准差都在数值误差量级(例如 $10^{-6}$ 以内),说明两边 likelihood 一致;evidence 差异更可能来自温度覆盖/积分误差/链混合等因素。
6. 不确定度评估:Block Bootstrap 给 TI/SS 一个误差条
对于 TI/SS,常见差异 $0.1\sim0.5$ 很可能来自有限样本与积分离散化。下面对 TI 做一个简单 block bootstrap(对 SS 也可类似处理):
import numpy as np
from scipy.integrate import simpson
def block_resample(x, B, rng):
x = np.asarray(x)
n = len(x)
m = n // B
x = x[:m*B].reshape(B, m)
idx = rng.integers(0, B, size=B)
return x[idx, :].reshape(-1)
def bootstrap_ti(betas, lnL_series, B=50, R=300, seed=0):
rng = np.random.default_rng(seed)
betas = np.asarray(betas)
vals = []
for _ in range(R):
E = []
for x in lnL_series:
xr = block_resample(x, B, rng)
E.append(xr.mean())
vals.append(simpson(np.array(E), x=betas))
vals = np.array(vals)
return float(vals.mean()), float(vals.std(ddof=1))
m, s = bootstrap_ti(betas, lnL_series, B=50, R=300)
print("TI logZ ~", m, "+/-", s)
若 bootstrap 标准差接近当前与 nested 的差距(例如 $\sim 0.3$),该差距可能处于 TI 估计本身的不确定度范围内。
7. Troubleshooting 清单
7.1 只看到 chains_1.txt(或仅冷链)
- 检查是否用 MPI 启动:
mpirun -np N python run_pt.py - 确认
writeHotChains=True - 确认 outdir 中确实出现多条链文件
7.2 la_forge 读不到热链
- 检查文件命名是否被
la_forge扫描规则识别 - 必要时创建软链接/重命名,使其符合
la_forge的期望(例如chain_1.0.txt等变体)
7.3 TI/SS 与 nested 差 $\sim 0.1\sim0.5$
- 检查低 $\beta$ 端覆盖:$\beta_{\min}$ 是否足够小;最热端 $\langle\log L\rangle_\beta$ 是否趋于平缓
- 尝试 SS(通常更稳)
- 计算 bootstrap 误差条,判断差异是否在不确定度范围
- 检查热链 ESS(高温链混合不足会直接影响 $\langle\log L\rangle_\beta$)
8. 最小工作流程总结
1) MPI 多链采样:mpirun -np 16 python run_pt.py,并开启 writeHotChains=True
2) 用 la_forge 读取热链:co.Core(outdir, burn=..., pt_chains=True)
3) 提取 betas 与 lnL 序列
4) 先算 SS,再算 TI;必要时加入 prior 采样估计 $\beta=0$ 端点
5) 用 block bootstrap 给误差条,再与 nested 的 $\log Z$ 对比
6) 用样本点回代 likelihood 验证 lnlike 与 pta.get_lnlikelihood 一致
如果这个短文对您有用,请关注赵志超的相关工作,并在有帮助时适当引用: https://inspirehep.net/authors/1608126?ui-citation-summary=true