蒙特卡罗方法 — 完整知识点与代码案例


一、采样和蒙特卡罗方法

1.1 基本概念

蒙特卡罗方法(Monte Carlo Methods) 是一类基于随机采样来近似计算数学问题数值解的方法。其核心思想是:通过从目标概率分布中抽取大量样本,利用样本的统计特性来估计期望、积分、概率等量。

关键公式:

Ex∼P[f(x)]=∫f(x)P(x)dx≈1N∑i=1Nf(x(i)),x(i)∼P(x)E_{x \sim P}[f(x)] = \int f(x) P(x) dx \approx \frac{1}{N} \sum_{i=1}^{N} f(x^{(i)}), \quad x^{(i)} \sim P(x)Ex∼P​[f(x)]=∫f(x)P(x)dx≈N1​i=1∑N​f(x(i)),x(i)∼P(x)

当样本数 N→∞N \to \inftyN→∞ 时,根据大数定律,样本均值收敛于真实期望。

1.2 基本采样方法

(1)逆变换采样(Inverse Transform Sampling)

原理:若已知累积分布函数(CDF)F(x)F(x)F(x) 及其逆函数 F−1(u)F^{-1}(u)F−1(u),则 X=F−1(U)X = F^{-1}(U)X=F−1(U),其中 U∼Uniform(0,1)U \sim \text{Uniform}(0,1)U∼Uniform(0,1)。

步骤:

  1. 生成均匀随机数 u∼U(0,1)u \sim U(0,1)u∼U(0,1)
  2. 计算 x=F−1(u)x = F^{-1}(u)x=F−1(u)
  3. xxx 即为目标分布的一个样本
(2)拒绝采样(Rejection Sampling)

原理:当目标分布 p(x)p(x)p(x) 难以直接采样时,选择一个容易采样的提议分布 q(x)q(x)q(x),使得存在常数 MMM 满足 Mq(x)≥p(x)Mq(x) \geq p(x)Mq(x)≥p(x) 对所有 xxx 成立。

步骤:

  1. 从 q(x)q(x)q(x) 中采样 x∗x^*x∗
  2. 从 U(0,1)U(0,1)U(0,1) 中采样 uuu
  3. 若 u≤p(x∗)Mq(x∗)u \leq \frac{p(x^*)}{Mq(x^*)}u≤Mq(x∗)p(x∗)​,接受 x∗x^*x∗;否则拒绝,回到步骤1

1.3 代码案例

import numpy as np
import matplotlib.pyplot as plt

# ============================================================
# 案例1:逆变换采样 —— 从指数分布中采样
# ============================================================

# 目标分布:指数分布 Exponential(lambda=1.5)
# PDF: p(x) = lambda * exp(-lambda * x), x >= 0
# CDF: F(x) = 1 - exp(-lambda * x)
# 逆CDF: F^{-1}(u) = -ln(1 - u) / lambda

lambda_param = 1.5                # 指数分布的参数 lambda
n_samples = 10000                 # 需要生成的样本数量

# 步骤1:从均匀分布 U(0,1) 生成随机数
u = np.random.uniform(0, 1, n_samples)   # 生成 n_samples 个 [0,1) 之间的均匀随机数

# 步骤2:通过逆CDF变换,将均匀随机数转换为指数分布样本
samples_exp = -np.log(1 - u) / lambda_param  # 应用逆变换公式 x = -ln(1-u)/lambda

# 可视化:对比采样结果与真实PDF
x_range = np.linspace(0, 6, 500)                # 定义x轴的绘图范围
true_pdf = lambda_param * np.exp(-lambda_param * x_range)  # 计算真实概率密度函数值

plt.figure(figsize=(12, 5))

# 子图1:直方图 vs 真实PDF
plt.subplot(1, 2, 1)
plt.hist(samples_exp, bins=80, density=True, alpha=0.7, color='steelblue',
         label='采样直方图')                        # 绘制采样结果的归一化直方图
plt.plot(x_range, true_pdf, 'r-', linewidth=2,
         label=f'真实PDF (λ={lambda_param})')      # 绘制真实概率密度曲线
plt.xlabel('x')                                    # 设置x轴标签
plt.ylabel('概率密度')                              # 设置y轴标签
plt.title('逆变换采样:指数分布')                     # 设置图标题
plt.legend()                                       # 显示图例

# 子图2:均匀分布输入 vs 指数分布输出
plt.subplot(1, 2, 2)
plt.scatter(u[:1000], samples_exp[:1000], s=1, alpha=0.3, color='coral')
plt.xlabel('u ~ Uniform(0,1)')                     # x轴为原始均匀分布
plt.ylabel('x = F⁻¹(u)')                          # y轴为变换后的指数分布样本
plt.title('逆变换函数映射关系')                       # 展示映射过程
plt.tight_layout()                                  # 自动调整子图间距
plt.show()                                          # 显示图形


# ============================================================
# 案例2:拒绝采样 —— 从复杂分布中采样
# ============================================================

# 目标分布 p(x):混合高斯分布(双峰)
# p(x) = 0.3 * N(-2, 0.5^2) + 0.7 * N(3, 1.0^2)
# 这个分布不容易直接采样,需要用拒绝采样

def target_pdf(x):
    """
    计算目标分布(混合高斯)的概率密度值
    参数:
        x: 输入点(标量或数组)
    返回:
        对应的概率密度值
    """
    # 第一个高斯分量:均值=-2, 标准差=0.5, 权重=0.3
    comp1 = 0.3 * np.exp(-0.5 * ((x + 2) / 0.5) ** 2) / (0.5 * np.sqrt(2 * np.pi))
    # 第二个高斯分量:均值=3, 标准差=1.0, 权重=0.7
    comp2 = 0.7 * np.exp(-0.5 * ((x - 3) / 1.0) ** 2) / (1.0 * np.sqrt(2 * np.pi))
    return comp1 + comp2                                # 返回两个分量之和


# 提议分布 q(x):均匀分布 U(-6, 8)
# q(x) = 1 / (8 - (-6)) = 1/14, 当 x ∈ [-6, 8]
a, b = -6, 8                                            # 均匀分布的范围
M = 15.0                                                 # 包络常数 M,使得 M*q(x) >= p(x) 对所有x成立

n_samples_rejection = 50000                              # 尝试采样的最大次数
accepted_samples = []                                    # 存储接受的样本
n_trials = 0                                             # 记录总尝试次数

while len(accepted_samples) < 10000:                     # 直到接受10000个样本
    # 步骤1:从提议分布 q(x) = Uniform(-6, 8) 中采样候选点
    x_star = np.random.uniform(a, b)                     # 生成候选样本
    # 步骤2:从 Uniform(0, 1) 中采样用于接受/拒绝判断的随机数
    u_accept = np.random.uniform(0, 1)                   # 生成 [0,1) 均匀随机数

    # 步骤3:计算接受概率 = p(x*) / (M * q(x*))
    # 其中 q(x*) = 1/(b-a) = 1/14 为常数
    acceptance_ratio = target_pdf(x_star) / (M * (1.0 / (b - a)))  # 计算接受比率

    # 步骤4:接受/拒绝判断
    if u_accept <= acceptance_ratio:                     # 若 u <= 接受概率,则接受
        accepted_samples.append(x_star)                 # 将候选样本加入接受列表

    n_trials += 1                                        # 累加尝试次数

accepted_samples = np.array(accepted_samples)            # 转换为numpy数组

# 计算接受率(有效采样效率)
acceptance_rate = len(accepted_samples) / n_trials       # 接受率 = 接受样本数 / 总尝试次数
print(f"拒绝采样接受率: {acceptance_rate:.4f}")          # 输出接受率
print(f"总尝试次数: {n_trials}")                         # 输出总尝试次数

# 可视化拒绝采样结果
x_plot = np.linspace(-6, 8, 1000)                        # 定义绘图的x范围
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.hist(accepted_samples, bins=100, density=True, alpha=0.7, color='mediumseagreen',
         label='拒绝采样结果')                            # 绘制接受样本的直方图
plt.plot(x_plot, target_pdf(x_plot), 'r-', linewidth=2,
         label='目标分布 p(x)')                           # 绘制真实目标分布曲线
plt.plot(x_plot, M * np.ones_like(x_plot) / (b - a), 'b--', linewidth=1.5,
         label=f'包络 M·q(x) = {M/(b-a):.2f}')          # 绘制包络线
plt.xlabel('x')
plt.ylabel('概率密度')
plt.title('拒绝采样:混合高斯分布')
plt.legend()

# 子图2:展示接受与拒绝的过程(仅前500次尝试)
plt.subplot(1, 2, 2)
# 重新进行少量采样用于可视化过程
np.random.seed(42)                                       # 固定随机种子以保证可复现
x_candidates = np.random.uniform(a, b, 500)              # 生成500个候选样本
u_vals = np.random.uniform(0, 1, 500)                    # 生成500个判断用随机数
ratios = target_pdf(x_candidates) / (M / (b - a))       # 计算每个候选的接受比率
accepted_mask = u_vals <= ratios                         # 布尔掩码:True表示接受

plt.scatter(x_candidates[accepted_mask], u_vals[accepted_mask],
            s=5, c='green', alpha=0.5, label='接受')      # 绘制接受的点(绿色)
plt.scatter(x_candidates[~accepted_mask], u_vals[~accepted_mask],
            s=5, c='red', alpha=0.3, label='拒绝')        # 绘制拒绝的点(红色)
plt.plot(x_plot, ratios[np.searchsorted(x_candidates, x_plot).clip(0, 499)],
         'b-', alpha=0.3)                                 # 绘制接受概率曲线的近似
plt.xlabel('候选样本 x*')
plt.ylabel('u (用于判断的随机数)')
plt.title('拒绝采样过程可视化')
plt.legend()
plt.tight_layout()
plt.show()

二、重要采样(Importance Sampling)

2.1 核心概念

重要采样是蒙特卡罗方法中最常用的技术之一。其核心思想是:当无法从目标分布 p(x)p(x)p(x) 直接采样时,可以从一个提议分布 q(x)q(x)q(x) 中采样,并通过重要性权重来修正偏差。

公式推导:

Ex∼p[f(x)]=∫f(x)p(x)dx=∫f(x)p(x)q(x)q(x)dx=Ex∼q[f(x)p(x)q(x)]E_{x \sim p}[f(x)] = \int f(x) p(x) dx = \int f(x) \frac{p(x)}{q(x)} q(x) dx = E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]Ex∼p​[f(x)]=∫f(x)p(x)dx=∫f(x)q(x)p(x)​q(x)dx=Ex∼q​[f(x)q(x)p(x)​]

估计量:

μ^=1N∑i=1Nf(x(i))p(x(i))q(x(i)),x(i)∼q(x)\hat{\mu} = \frac{1}{N} \sum_{i=1}^{N} f(x^{(i)}) \frac{p(x^{(i)})}{q(x^{(i)})}, \quad x^{(i)} \sim q(x)μ^​=N1​i=1∑N​f(x(i))q(x(i))p(x(i))​,x(i)∼q(x)

其中 w(x(i))=p(x(i))q(x(i))w(x^{(i)}) = \frac{p(x^{(i)})}{q(x^{(i)})}w(x(i))=q(x(i))p(x(i))​ 称为重要性权重(importance weight)。

2.2 提议分布的选择

最优提议分布(最小化方差):

q∗(x)=∣f(x)∣p(x)∫∣f(x)∣p(x)dxq^*(x) = \frac{|f(x)| p(x)}{\int |f(x)| p(x) dx}q∗(x)=∫∣f(x)∣p(x)dx∣f(x)∣p(x)​

实践中,q(x)q(x)q(x) 应满足:

  • q(x)>0q(x) > 0q(x)>0 当 f(x)p(x)≠0f(x)p(x) \neq 0f(x)p(x)=0
  • q(x)q(x)q(x) 的尾部应比 p(x)p(x)p(x) 更重(heavy-tailed),避免权重爆炸
  • q(x)q(x)q(x) 应易于采样

2.3 标准化重要采样(Self-Normalized Importance Sampling)

当 p(x)p(x)p(x) 仅已知到一个归一化常数时(即只知道 p~(x)\tilde{p}(x)p~​(x)),使用标准化重要采样:

μ^=∑i=1Nf(x(i))w~(x(i))∑i=1Nw~(x(i)),w~(x(i))=p~(x(i))q(x(i))\hat{\mu} = \frac{\sum_{i=1}^{N} f(x^{(i)}) \tilde{w}(x^{(i)})}{\sum_{i=1}^{N} \tilde{w}(x^{(i)})}, \quad \tilde{w}(x^{(i)}) = \frac{\tilde{p}(x^{(i)})}{q(x^{(i)})}μ^​=∑i=1N​w~(x(i))∑i=1N​f(x(i))w~(x(i))​,w~(x(i))=q(x(i))p~​(x(i))​

2.4 代码案例

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# ============================================================
# 案例1:基础重要采样 —— 估计高斯分布尾部概率
# ============================================================

# 问题:计算 P(X > 4), X ~ N(0, 1)
# 这是一个极小概率事件,直接蒙特卡罗估计效率很低

np.random.seed(42)                                        # 固定随机种子

n_samples = 10000                                         # 样本数量

# --- 方法1:直接蒙特卡罗采样 ---
samples_direct = np.random.normal(0, 1, n_samples)        # 从标准正态分布直接采样
# 计算有多少样本落在 x > 4 区域
estimate_direct = np.mean(samples_direct > 4)             # 直接估计 P(X > 4)
print(f"直接蒙特卡罗估计 P(X>4): {estimate_direct}")      # 大部分时候为0,估计极不准确

# --- 方法2:重要采样 ---
# 提议分布 q(x) = N(4, 1),将采样重心移到感兴趣区域
mu_q = 4.0                                                # 提议分布的均值
sigma_q = 1.0                                             # 提议分布的标准差

# 步骤1:从提议分布 q(x) 中采样
samples_is = np.random.normal(mu_q, sigma_q, n_samples)   # 从 N(4,1) 采样

# 步骤2:计算重要性权重 w(x) = p(x) / q(x)
# p(x) = N(0,1) 的PDF值
p_values = stats.norm.pdf(samples_is, loc=0, scale=1)     # 目标分布 p(x) = N(0,1)
# q(x) = N(4,1) 的PDF值
q_values = stats.norm.pdf(samples_is, loc=mu_q, scale=sigma_q)  # 提议分布 q(x) = N(4,1)
# 重要性权重
weights = p_values / q_values                             # w_i = p(x_i) / q(x_i)

# 步骤3:计算指示函数 f(x) = I(x > 4)
indicator = (samples_is > 4).astype(float)                # f(x) = 1 if x > 4, else 0

# 步骤4:加权估计
estimate_is = np.mean(indicator * weights)                # E_p[I(x>4)] ≈ mean(f(x)*w(x))
print(f"重要采样估计 P(X>4): {estimate_is:.6f}")           # 输出重要采样估计值

# 真实值(通过scipy精确计算)
true_value = 1 - stats.norm.cdf(4)                        # 真实的 P(X > 4)
print(f"真实值 P(X>4): {true_value:.6f}")                  # 输出精确值作为对比

# 方差分析
# 有效样本数(Effective Sample Size, ESS)
ESS = (np.sum(weights) ** 2) / np.sum(weights ** 2)       # ESS = (sum w)^2 / sum(w^2)
print(f"有效样本数 ESS: {ESS:.1f} / {n_samples}")         # ESS越接近N越好


# ============================================================
# 案例2:标准化重要采样 —— 估计未归一化分布的期望
# ============================================================

# 场景:目标分布 p(x) 只知道未归一化的形式 tilde_p(x)
# 例如:tilde_p(x) = exp(-x^2/2) * (1 + sin(2x))^2
# 我们想计算 E_p[x] 和 E_p[x^2]

def unnormalized_log_pdf(x):
    """
    计算未归一化对数概率密度
    参数:
        x: 输入值
    返回:
        log(tilde_p(x))
    """
    return -x**2 / 2 + 2 * np.log(np.abs(1 + np.sin(2 * x)) + 1e-10)  # 加小常数避免log(0)


def unnormalized_pdf(x):
    """
    计算未归一化概率密度(直接形式)
    参数:
        x: 输入值
    返回:
        tilde_p(x)
    """
    return np.exp(-x**2 / 2) * (1 + np.sin(2 * x))**2    # 未归一化的密度函数


np.random.seed(42)                                        # 固定随机种子
n_samples_snis = 50000                                    # 采样数量

# 提议分布 q(x) = N(0, 2),选择较宽的高斯以覆盖目标分布
mu_proposal = 0.0                                         # 提议分布均值
sigma_proposal = 2.0                                      # 提议分布标准差(较宽)

# 步骤1:从提议分布中采样
x_samples = np.random.normal(mu_proposal, sigma_proposal, n_samples_snis)  # 从q(x)采样

# 步骤2:计算未归一化重要性权重
p_tilde = unnormalized_pdf(x_samples)                     # 计算 tilde_p(x_i)
q_vals = stats.norm.pdf(x_samples, mu_proposal, sigma_proposal)  # 计算 q(x_i)
unnormalized_weights = p_tilde / q_vals                   # w_tilde_i = tilde_p(x_i) / q(x_i)

# 步骤3:标准化重要采样估计
# E_p[x] 的估计
x_expectation = np.sum(x_samples * unnormalized_weights) / np.sum(unnormalized_weights)
print(f"标准化重要采样估计 E_p[x]: {x_expectation:.6f}")   # 期望的估计值

# E_p[x^2] 的估计
x2_expectation = np.sum(x_samples**2 * unnormalized_weights) / np.sum(unnormalized_weights)
print(f"标准化重要采样估计 E_p[x^2]: {x2_expectation:.6f}") # 二阶矩的估计值

# 可视化:目标分布 vs 提议分布 vs 采样权重
x_plot = np.linspace(-6, 6, 1000)                         # 绘图范围
p_tilde_plot = unnormalized_pdf(x_plot)                   # 未归一化目标分布
q_plot = stats.norm.pdf(x_plot, mu_proposal, sigma_proposal)  # 提议分布

# 归一化目标分布用于对比(数值积分)
from scipy.integrate import quad
normalization_const, _ = quad(unnormalized_pdf, -20, 20)   # 数值计算归一化常数
p_normalized = p_tilde_plot / normalization_const          # 归一化后的目标分布

plt.figure(figsize=(14, 5))

plt.subplot(1, 2, 1)
plt.plot(x_plot, p_normalized, 'r-', linewidth=2, label='目标分布 p(x)(归一化后)')
plt.plot(x_plot, q_plot, 'b--', linewidth=2, label='提议分布 q(x) = N(0, 2)')
plt.fill_between(x_plot, 0, p_normalized, alpha=0.1, color='red')
plt.fill_between(x_plot, 0, q_plot, alpha=0.1, color='blue')
plt.xlabel('x')
plt.ylabel('概率密度')
plt.title('目标分布 vs 提议分布')
plt.legend()
plt.grid(True, alpha=0.3)

# 子图2:样本点的权重分布
plt.subplot(1, 2, 2)
# 按权重大小着色
norm_weights = unnormalized_weights / np.max(unnormalized_weights)  # 归一化权重用于颜色映射
plt.scatter(x_samples[:2000], unnormalized_weights[:2000],
            c=norm_weights[:2000], cmap='hot', s=3, alpha=0.5)  # 权重散点图
plt.xlabel('样本值 x')
plt.ylabel('未归一化权重 w̃')
plt.title('重要性权重分布')
plt.colorbar(label='归一化权重')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# ============================================================
# 案例3:重要采样在强化学习中的应用 —— 策略评估
# ============================================================

# 用重要采样从行为策略(behavior policy)的轨迹评估目标策略(target policy)
# 这是off-policy方法的基础

def behavior_policy(state):
    """
    行为策略:给定状态,返回各动作的概率(较随机的策略)
    参数:
        state: 状态索引
    返回:
        各动作的概率分布
    """
    n_actions = 4                                         # 动作数量
    probs = np.ones(n_actions) / n_actions                # 均匀策略:各动作概率相等
    return probs


def target_policy(state):
    """
    目标策略:给定状态,返回各动作的概率(较确定性的策略)
    参数:
        state: 状态索引
    返回:
        各动作的概率分布
    """
    n_actions = 4                                         # 动作数量
    probs = np.zeros(n_actions)                           # 初始化概率为0
    probs[state % n_actions] = 0.7                        # 偏好某个动作,概率0.7
    probs += 0.3 / n_actions                              # 其余动作分配剩余概率
    return probs


# 模拟收集一条轨迹
np.random.seed(42)
n_steps = 20                                              # 轨迹长度
states = np.random.randint(0, 4, n_steps)                 # 随机生成状态序列
rewards = np.random.randn(n_steps) * 0.5 + 1             # 随机生成奖励(均值为1)

# 使用行为策略收集的动作
actions = []                                              # 存储每步的动作
for t in range(n_steps):                                  # 遍历每个时间步
    action_probs = behavior_policy(states[t])             # 获取行为策略的动作概率
    action = np.random.choice(4, p=action_probs)          # 按行为策略采样动作
    actions.append(action)                                # 记录动作

actions = np.array(actions)                               # 转为numpy数组

# 计算每个时间步的重要性权重
# w_t = pi_target(a_t | s_t) / pi_behavior(a_t | s_t)
importance_weights = np.ones(n_steps)                     # 初始化权重为1
for t in range(n_steps):                                  # 遍历每个时间步
    pi_target = target_policy(states[t])[actions[t]]      # 目标策略选择该动作的概率
    pi_behavior = behavior_policy(states[t])[actions[t]]  # 行为策略选择该动作的概率
    importance_weights[t] = pi_target / pi_behavior       # 计算重要性权重

# 普通重要采样估计每个状态的期望回报
# 在这个简单例子中,我们估计 E[R] under target policy
# 乘积权重(从轨迹开始到当前步)
cumulative_weights = np.cumprod(importance_weights)       # 累积乘积权重

# 普通重要采样的回报估计
V_estimate = np.zeros(n_steps)                            # 存储每个时间步的价值估计
for t in range(n_steps):                                  # 遍历每个时间步
    # 从时间步t开始的折扣回报
    gamma = 0.9                                           # 折扣因子
    returns_t = sum(gamma**(k-t) * rewards[k] for k in range(t, n_steps))  # 计算折扣回报
    V_estimate[t] = cumulative_weights[t] * returns_t     # 加权回报

print("各时间步的重要性权重:", np.round(importance_weights, 4))
print("各时间步的累积权重:", np.round(cumulative_weights, 4))
print(f"普通IS估计的平均价值: {np.mean(V_estimate):.4f}")

# 对比:如果使用截断权重(减少方差)
truncation_threshold = 5.0                                # 权重截断阈值
cumulative_weights_clipped = np.minimum(cumulative_weights, truncation_threshold)  # 截断权重

V_estimate_clipped = np.zeros(n_steps)                    # 存储截断版本的价值估计
for t in range(n_steps):
    gamma = 0.9
    returns_t = sum(gamma**(k-t) * rewards[k] for k in range(t, n_steps))
    V_estimate_clipped[t] = cumulative_weights_clipped[t] * returns_t  # 使用截断权重

print(f"截断IS估计的平均价值: {np.mean(V_estimate_clipped):.4f}")

三、马尔可夫链蒙特卡罗方法(MCMC)

3.1 核心概念

MCMC(Markov Chain Monte Carlo) 是一类构造马尔可夫链、使其平稳分布等于目标分布 p(x)p(x)p(x) 的采样方法。

关键定理——细致平衡条件(Detailed Balance):

p(x)T(x′∣x)=p(x′)T(x∣x′)p(x) T(x'|x) = p(x') T(x|x')p(x)T(x′∣x)=p(x′)T(x∣x′)

其中 T(x′∣x)T(x'|x)T(x′∣x) 是从状态 xxx 转移到 x′x'x′ 的转移概率。满足细致平衡条件的转移核 TTT 的平稳分布即为 p(x)p(x)p(x)。

3.2 Metropolis-Hastings 算法

MH算法是最经典的MCMC方法。

算法流程:

  1. 初始化 x(0)x^{(0)}x(0)
  2. 对于 t=0,1,2,…,T−1t = 0, 1, 2, \ldots, T-1t=0,1,2,…,T−1:
    • 从提议分布 q(x′∣x(t))q(x'|x^{(t)})q(x′∣x(t)) 中采样候选点 x′x'x′
    • 计算接受概率:
      α=min⁡(1,p(x′)q(x(t)∣x′)p(x(t))q(x′∣x(t)))\alpha = \min\left(1, \frac{p(x') q(x^{(t)}|x')}{p(x^{(t)}) q(x'|x^{(t)})}\right)α=min(1,p(x(t))q(x′∣x(t))p(x′)q(x(t)∣x′)​)
    • 以概率 α\alphaα 接受:x(t+1)=x′x^{(t+1)} = x'x(t+1)=x′,否则 x(t+1)=x(t)x^{(t+1)} = x^{(t)}x(t+1)=x(t)

3.3 代码案例

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# ============================================================
# 案例1:Metropolis-Hastings 采样 —— 从二维混合高斯中采样
# ============================================================

def target_distribution_2d(x, y):
    """
    计算目标分布(二维混合高斯)的概率密度值
    p(x,y) = 0.4 * N(mu1, Sigma1) + 0.6 * N(mu2, Sigma2)
    
    参数:
        x, y: 二维坐标
    返回:
        概率密度值
    """
    # 第一个高斯分量:均值=[-2, -2], 标准差=[1, 1]
    comp1 = 0.4 * np.exp(-0.5 * ((x+2)**2 + (y+2)**2))   # 各向同性高斯
    # 第二个高斯分量:均值=[3, 3], 标准差=[1.5, 1.5]
    comp2 = 0.6 * np.exp(-0.5 * ((x-3)**2/1.5**2 + (y-3)**2/1.5**2))
    return comp1 + comp2                                    # 返回混合密度


def metropolis_hastings_2d(n_samples, proposal_std=1.0, initial_point=None):
    """
    二维Metropolis-Hastings采样器
    
    参数:
        n_samples:     需要生成的样本数量
        proposal_std:  提议分布(各向同性高斯)的标准差
        initial_point: 初始点,默认为原点
    返回:
        samples: 采样结果数组, shape=(n_samples, 2)
        accept_rate: 接受率
    """
    if initial_point is None:                               # 若未指定初始点
        initial_point = np.array([0.0, 0.0])                # 默认从原点开始
    
    samples = np.zeros((n_samples, 2))                      # 初始化样本数组
    samples[0] = initial_point                              # 设置初始样本
    n_accepted = 0                                          # 接受次数计数器
    
    for i in range(1, n_samples):                           # 从第2个样本开始迭代
        current = samples[i-1]                              # 当前状态 x_t
        
        # 步骤1:从对称的高斯提议分布 q(x'|x) = N(x, sigma^2 * I) 中采样候选点
        proposal = current + np.random.normal(0, proposal_std, size=2)  # 随机游走提议
        
        # 步骤2:计算接受概率 alpha
        # 由于提议分布是对称的(高斯),q(x'|x) = q(x|x')
        # 因此 MH 比率简化为 p(x') / p(x)
        p_current = target_distribution_2d(current[0], current[1])   # p(x_t)
        p_proposal = target_distribution_2d(proposal[0], proposal[1]) # p(x')
        
        if p_current > 0:                                   # 避免除以零
            acceptance_ratio = p_proposal / p_current       # MH比率(对称提议简化)
        else:
            acceptance_ratio = 1.0                          # 若当前密度为0,直接接受
        
        # 步骤3:接受/拒绝
        alpha = min(1.0, acceptance_ratio)                  # 接受概率,截断在1
        u = np.random.uniform(0, 1)                         # 生成均匀随机数
        
        if u < alpha:                                       # 若 u < alpha,接受候选
            samples[i] = proposal                           # 新状态为候选点
            n_accepted += 1                                 # 接受计数+1
        else:                                               # 否则拒绝
            samples[i] = current                            # 新状态保持不变(拒绝移动)
    
    accept_rate = n_accepted / (n_samples - 1)              # 计算接受率
    return samples, accept_rate                             # 返回样本和接受率


np.random.seed(42)                                          # 固定随机种子

# 运行MH采样器
n_mcmc_samples = 30000                                      # 总采样数
samples_mh, acc_rate = metropolis_hastings_2d(
    n_samples=n_mcmc_samples,                               # 样本数量
    proposal_std=1.5,                                        # 提议分布标准差
    initial_point=np.array([-2.0, -2.0])                    # 从第一个模态附近开始
)
print(f"MH采样接受率: {acc_rate:.4f}")                       # 输出接受率

# 去除burn-in期的样本(前20%的样本作为预热)
burn_in = int(0.2 * n_mcmc_samples)                         # burn-in长度
samples_after_burnin = samples_mh[burn_in:]                  # 丢弃burn-in样本

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 子图1:采样轨迹
axes[0].plot(samples_mh[:5000, 0], samples_mh[:5000, 1],
             linewidth=0.3, alpha=0.5, color='blue')        # 绘制前5000步的轨迹
axes[0].scatter(samples_mh[0, 0], samples_mh[0, 1],
                c='green', s=100, zorder=5, label='起始点')  # 标记起始点
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('MH采样轨迹(前5000步)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 子图2:采样散点图(去burn-in后)
axes[1].scatter(samples_after_burnin[:, 0], samples_after_burnin[:, 1],
                s=1, alpha=0.1, c='coral')                  # 绘制所有样本点
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title(f'MH采样结果(去burn-in后, n={len(samples_after_burnin)})')
axes[1].set_aspect('equal')
axes[1].grid(True, alpha=0.3)

# 子图3:核密度估计的等高线图
from scipy.stats import gaussian_kde
kde = gaussian_kde(samples_after_burnin.T)                  # 对采样结果做KDE
x_grid = np.linspace(-6, 7, 100)                            # x轴网格
y_grid = np.linspace(-6, 7, 100)                            # y轴网格
X, Y = np.meshgrid(x_grid, y_grid)                          # 生成二维网格
positions = np.vstack([X.ravel(), Y.ravel()])                # 展平为2行矩阵
Z = kde(positions).reshape(X.shape)                          # 计算KDE并reshape

axes[2].contourf(X, Y, Z, levels=20, cmap='YlOrRd')        # 填充等高线图
axes[2].set_xlabel('x')
axes[2].set_ylabel('y')
axes[2].set_title('核密度估计等高线')
axes[2].set_aspect('equal')

plt.tight_layout()
plt.show()


# ============================================================
# 案例2:MH算法的诊断 —— 迹图和自相关函数
# ============================================================

def compute_autocorrelation(chain, max_lag=100):
    """
    计算马尔可夫链的自相关函数(ACF)
    
    参数:
        chain:   一维采样链
        max_lag: 最大滞后值
    返回:
        acf: 自相关系数数组
    """
    n = len(chain)                                          # 链的长度
    mean = np.mean(chain)                                   # 链的均值
    var = np.var(chain)                                     # 链的方差
    acf = np.zeros(max_lag)                                 # 初始化ACF数组
    
    for lag in range(max_lag):                              # 遍历每个滞后值
        if var > 0:                                         # 避免除以零
            # 计算滞后lag的自相关系数
            cov = np.mean((chain[:n-lag] - mean) * (chain[lag:] - mean))  # 自协方差
            acf[lag] = cov / var                            # 归一化为自相关系数
        else:
            acf[lag] = 0                                    # 方差为零时ACF为0
    return acf                                              # 返回ACF数组


# 使用不同proposal标准差运行多次MH
proposal_stds = [0.1, 1.5, 10.0]                           # 三种不同的提议标准差
results = {}                                                # 存储各次结果

for pstd in proposal_stds:                                  # 遍历每种标准差
    samples_diag, ar = metropolis_hastings_2d(
        n_samples=20000,                                    # 样本数
        proposal_std=pstd,                                  # 提议标准差
        initial_point=np.array([0.0, 0.0])                 # 初始点
    )
    results[pstd] = {                                       # 存储结果
        'samples': samples_diag,                            # 采样链
        'accept_rate': ar,                                  # 接受率
        'acf_x': compute_autocorrelation(samples_diag[:, 0], 100)  # x分量的ACF
    }
    print(f"proposal_std={pstd:.1f}, 接受率={ar:.4f}")      # 输出诊断信息

fig, axes = plt.subplots(len(proposal_stds), 3, figsize=(18, 4*len(proposal_stds)))

for idx, pstd in enumerate(proposal_stds):                  # 遍历每种提议标准差
    s = results[pstd]['samples']                            # 获取采样结果
    
    # 迹图(trace plot)
    axes[idx, 0].plot(s[:5000, 0], linewidth=0.5)           # 绘制x分量的迹图
    axes[idx, 0].set_title(f'σ_proposal={pstd}, AR={results[pstd]["accept_rate"]:.2f}')
    axes[idx, 0].set_ylabel('x值')                          # y轴标签
    
    # 自相关函数
    axes[idx, 1].bar(range(50), results[pstd]['acf_x'][:50])  # 绘制ACF柱状图
    axes[idx, 1].set_title(f'自相关函数 (lag=0~49)')
    axes[idx, 1].axhline(y=0, color='red', linestyle='--')  # 零线
    
    # 采样直方图
    axes[idx, 2].hist(s[2000:, 0], bins=80, density=True, alpha=0.7, color='steelblue')
    axes[idx, 2].set_title('x分量的边际分布')
    axes[idx, 2].set_xlabel('x')

plt.tight_layout()
plt.show()
# 理解:
# - proposal_std太小(0.1):接受率高但移动慢,自相关高,混合差
# - proposal_std适中(1.5):接受率适中,自相关衰减快,混合好
# - proposal_std太大(10.0):大部分被拒绝,接受率极低,混合差

四、Gibbs采样(Gibbs Sampling)

4.1 核心概念

Gibbs采样是MCMC的一种特殊情况,它通过从条件分布中轮流采样每个变量来生成样本,不需要手动设定提议分布,且接受率为100%。

算法流程:

对于 kkk 维变量 x=(x1,x2,…,xk)\mathbf{x} = (x_1, x_2, \ldots, x_k)x=(x1​,x2​,…,xk​):

  1. 初始化 x(0)=(x1(0),x2(0),…,xk(0))\mathbf{x}^{(0)} = (x_1^{(0)}, x_2^{(0)}, \ldots, x_k^{(0)})x(0)=(x1(0)​,x2(0)​,…,xk(0)​)
  2. 对于 t=0,1,2,…t = 0, 1, 2, \ldotst=0,1,2,…:
    • x1(t+1)∼p(x1∣x2(t),x3(t),…,xk(t))x_1^{(t+1)} \sim p(x_1 | x_2^{(t)}, x_3^{(t)}, \ldots, x_k^{(t)})x1(t+1)​∼p(x1​∣x2(t)​,x3(t)​,…,xk(t)​)
    • x2(t+1)∼p(x2∣x1(t+1),x3(t),…,xk(t))x_2^{(t+1)} \sim p(x_2 | x_1^{(t+1)}, x_3^{(t)}, \ldots, x_k^{(t)})x2(t+1)​∼p(x2​∣x1(t+1)​,x3(t)​,…,xk(t)​)
    • ⋮\vdots⋮
    • xk(t+1)∼p(xk∣x1(t+1),x2(t+1),…,xk−1(t+1))x_k^{(t+1)} \sim p(x_k | x_1^{(t+1)}, x_2^{(t+1)}, \ldots, x_{k-1}^{(t+1)})xk(t+1)​∼p(xk​∣x1(t+1)​,x2(t+1)​,…,xk−1(t+1)​)

关键特点:

  • 每一步只改变一个变量,其他变量固定
  • 提议分布就是条件分布本身,因此接受率恒为1
  • 适合变量之间存在强依赖关系的情况

4.2 代码案例

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# ============================================================
# 案例1:二元高斯分布的Gibbs采样
# ============================================================

# 目标分布:二元正态分布 N(mu, Sigma)
# mu = [mu_x, mu_y], Sigma = [[sigma_x^2, rho*sigma_x*sigma_y],
#                               [rho*sigma_x*sigma_y, sigma_y^2]]
#
# 条件分布(已知推导结果):
# p(x | y) = N(mu_x + rho*(sigma_x/sigma_y)*(y - mu_y), sigma_x^2*(1-rho^2))
# p(y | x) = N(mu_y + rho*(sigma_y/sigma_x)*(x - mu_x), sigma_y^2*(1-rho^2))

# 设置参数
mu_x, mu_y = 0.0, 0.0              # 两个变量的均值
sigma_x, sigma_y = 1.0, 1.5        # 两个变量的标准差
rho = 0.8                           # 相关系数(强正相关)

# 计算条件分布参数(预先计算,避免重复计算)
cond_var_x = sigma_x**2 * (1 - rho**2)                   # p(x|y) 的条件方差
cond_var_y = sigma_y**2 * (1 - rho**2)                    # p(y|x) 的条件方差
cond_std_x = np.sqrt(cond_var_x)                          # p(x|y) 的条件标准差
cond_std_y = np.sqrt(cond_var_y)                          # p(y|x) 的条件标准差


def conditional_x_given_y(y):
    """
    从条件分布 p(x | y) 采样
    x | y ~ N(mu_x + rho*(sigma_x/sigma_y)*(y - mu_y), sigma_x^2*(1-rho^2))
    
    参数:
        y: 固定的y值
    返回:
        从条件分布中采样的x值
    """
    cond_mean = mu_x + rho * (sigma_x / sigma_y) * (y - mu_y)  # 条件均值
    return np.random.normal(cond_mean, cond_std_x)               # 从条件高斯中采样


def conditional_y_given_x(x):
    """
    从条件分布 p(y | x) 采样
    y | x ~ N(mu_y + rho*(sigma_y/sigma_x)*(x - mu_x), sigma_y^2*(1-rho^2))
    
    参数:
        x: 固定的x值
    返回:
        从条件分布中采样的y值
    """
    cond_mean = mu_y + rho * (sigma_y / sigma_x) * (x - mu_x)  # 条件均值
    return np.random.normal(cond_mean, cond_std_y)               # 从条件高斯中采样


def gibbs_sampler_bivariate(n_samples, burn_in=500, init_x=3.0, init_y=-2.0):
    """
    二元高斯分布的Gibbs采样器
    
    参数:
        n_samples: 需要保留的样本数
        burn_in:   预热期迭代数(丢弃前面的样本)
        init_x:    x的初始值
        init_y:    y的初始值
    返回:
        samples: 采样结果数组, shape=(n_samples, 2)
    """
    total_iterations = n_samples + burn_in                 # 总迭代次数
    samples = np.zeros((total_iterations, 2))              # 存储所有采样点
    
    x, y = init_x, init_y                                 # 初始化当前状态
    samples[0] = [x, y]                                    # 存储初始点
    
    for i in range(1, total_iterations):                   # 迭代
        # 步骤1:固定y,从条件分布 p(x|y) 采样新的x
        x = conditional_x_given_y(y)                      # 从 p(x|y) 采样
        
        # 步骤2:固定x(使用刚刚更新的x),从条件分布 p(y|x) 采样新的y
        y = conditional_y_given_x(x)                      # 从 p(y|x) 采样
        
        samples[i] = [x, y]                                # 存储新的采样点
    
    # 丢弃burn-in期的样本
    return samples[burn_in:]                                # 返回去除预热期的样本


np.random.seed(42)                                         # 固定随机种子

# 运行Gibbs采样器
n_gibbs = 10000                                            # 需要保留的样本数
samples_gibbs = gibbs_sampler_bivariate(n_gibbs, burn_in=1000)  # 运行采样器
print(f"Gibbs采样完成,样本数: {len(samples_gibbs)}")       # 输出样本数

# 可视化
fig, axes = plt.subplots(2, 3, figsize=(18, 10))

# 子图1:采样散点图
axes[0, 0].scatter(samples_gibbs[:, 0], samples_gibbs[:, 1],
                    s=2, alpha=0.2, c='navy')              # 绘制采样点
axes[0, 0].set_xlabel('x')
axes[0, 0].set_ylabel('y')
axes[0, 0].set_title('Gibbs采样散点图')
axes[0, 0].set_aspect('equal')                             # 等比例坐标轴
axes[0, 0].grid(True, alpha=0.3)

# 子图2:x分量的迹图
axes[0, 1].plot(samples_gibbs[:3000, 0], linewidth=0.3, color='blue')
axes[0, 1].set_xlabel('迭代次数')
axes[0, 1].set_ylabel('x值')
axes[0, 1].set_title('x分量的迹图(Trace Plot)')
axes[0, 1].axhline(y=mu_x, color='red', linestyle='--', label=f'E[x]={mu_x}')
axes[0, 1].legend()

# 子图3:y分量的迹图
axes[0, 2].plot(samples_gibbs[:3000, 1], linewidth=0.3, color='green')
axes[0, 2].set_xlabel('迭代次数')
axes[0, 2].set_ylabel('y值')
axes[0, 2].set_title('y分量的迹图(Trace Plot)')
axes[0, 2].axhline(y=mu_y, color='red', linestyle='--', label=f'E[y]={mu_y}')
axes[0, 2].legend()

# 子图4:x的边际分布
axes[1, 0].hist(samples_gibbs[:, 0], bins=80, density=True, alpha=0.7, color='steelblue',
                label='Gibbs采样')                         # 采样直方图
x_range = np.linspace(-4, 4, 200)
axes[1, 0].plot(x_range, stats.norm.pdf(x_range, mu_x, sigma_x),
                'r-', linewidth=2, label='真实边际分布')    # 真实边际分布
axes[1, 0].set_title('x的边际分布')
axes[1, 0].legend()

# 子图5:y的边际分布
axes[1, 1].hist(samples_gibbs[:, 1], bins=80, density=True, alpha=0.7, color='coral',
                label='Gibbs采样')
y_range = np.linspace(-5, 5, 200)
axes[1, 1].plot(y_range, stats.norm.pdf(y_range, mu_y, sigma_y),
                'r-', linewidth=2, label='真实边际分布')
axes[1, 1].set_title('y的边际分布')
axes[1, 1].legend()

# 子图6:Gibbs采样路径图(前100步,展示锯齿形路径)
path = gibbs_sampler_bivariate(100, burn_in=0, init_x=3.0, init_y=-2.0)
axes[1, 2].plot(path[:, 0], path[:, 1], 'o-', markersize=3,
                linewidth=0.5, alpha=0.7, color='purple')  # 绘制采样路径
axes[1, 2].set_xlabel('x')
axes[1, 2].set_ylabel('y')
axes[1, 2].set_title('Gibbs采样路径(前100步)')
axes[1, 2].set_aspect('equal')
axes[1, 2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()


# ============================================================
# 案例2:Gibbs采样用于贝叶斯线性回归
# ============================================================

# 模型:y = w0 + w1*x + noise, noise ~ N(0, sigma^2)
# 先验:w0, w1 ~ N(0, tau^2)
# 使用Gibbs采样对后验 p(w0, w1, sigma^2 | data) 进行采样

np.random.seed(42)

# 生成模拟数据
true_w0 = 2.0                                              # 真实截距
true_w1 = 0.5                                              # 真实斜率
true_sigma = 1.0                                           # 真实噪声标准差
n_data = 50                                                # 数据点数

x_data = np.random.uniform(-3, 3, n_data)                  # 生成自变量数据
y_data = true_w0 + true_w1 * x_data + np.random.normal(0, true_sigma, n_data)  # 生成因变量

# 超参数
tau2 = 10.0                                                # 权重先验的方差(弱先验)

def gibbs_linear_regression(x_data, y_data, n_iter=5000, burn_in=1000):
    """
    贝叶斯线性回归的Gibbs采样
    
    条件分布推导:
    - p(w | sigma^2, data) 是多元高斯
    - p(sigma^2 | w, data) 是逆伽马分布
    
    参数:
        x_data:   自变量数组
        y_data:   因变量数组
        n_iter:   总迭代次数
        burn_in:  预热期
    返回:
        w_samples:     权重样本 (n_iter - burn_in, 2)
        sigma_samples: 噪声标准差样本 (n_iter - burn_in,)
    """
    n = len(x_data)                                         # 数据点数
    
    # 构造设计矩阵 X = [1, x]
    X = np.column_stack([np.ones(n), x_data])               # 设计矩阵,第一列全1
    
    # 初始化参数
    w = np.array([0.0, 0.0])                                # 权重初始值 [w0, w1]
    sigma2 = 1.0                                            # 噪声方差初始值
    
    w_all = []                                              # 存储权重采样
    sigma2_all = []                                         # 存储方差采样
    
    for iteration in range(n_iter):                         # 迭代采样
        # === 步骤1:采样 w | sigma^2, data ===
        # 后验 w | sigma^2, data ~ N(mu_w, Sigma_w)
        # Sigma_w = (X^T X / sigma^2 + I/tau^2)^{-1}
        # mu_w = Sigma_w * X^T y / sigma^2
        
        XtX = X.T @ X                                       # X^T * X 矩阵乘法
        Xty = X.T @ y_data                                  # X^T * y 向量乘法
        
        # 后验协方差矩阵
        Sigma_w = np.linalg.inv(XtX / sigma2 + np.eye(2) / tau2)  # 2x2协方差矩阵
        # 后验均值
        mu_w = Sigma_w @ (Xty / sigma2)                     # 2维均值向量
        
        # 从多元高斯分布采样权重
        w = np.random.multivariate_normal(mu_w, Sigma_w)    # 采样 w ~ N(mu_w, Sigma_w)
        
        # === 步骤2:采样 sigma^2 | w, data ===
        # 后验 sigma^2 | w, data ~ InvGamma(a_n, b_n)
        # a_n = a_0 + n/2
        # b_n = b_0 + 0.5 * sum((y - Xw)^2)
        
        residuals = y_data - X @ w                          # 计算残差 y - Xw
        ssr = np.sum(residuals**2)                          # 残差平方和
        
        a_n = 1.0 + n / 2                                   # 逆伽马分布的形状参数
        b_n = 1.0 + ssr / 2                                 # 逆伽马分布的尺度参数
        
        # 从逆伽马分布采样(通过Gamma分布的倒数实现)
        sigma2 = 1.0 / np.random.gamma(a_n, 1.0 / b_n)     # InvGamma = 1/Gamma
        
        # 存储采样结果
        if iteration >= burn_in:                             # 超过burn-in期后开始记录
            w_all.append(w.copy())                           # 存储权重样本
            sigma2_all.append(sigma2)                        # 存储方差样本
    
    return np.array(w_all), np.sqrt(np.array(sigma2_all))  # 返回权重和标准差样本


# 运行Gibbs采样
w_samples, sigma_samples = gibbs_linear_regression(x_data, y_data)

print(f"后验估计 w0: {np.mean(w_samples[:, 0]):.3f} ± {np.std(w_samples[:, 0]):.3f}")
print(f"后验估计 w1: {np.mean(w_samples[:, 1]):.3f} ± {np.std(w_samples[:, 1]):.3f}")
print(f"后验估计 sigma: {np.mean(sigma_samples):.3f} ± {np.std(sigma_samples):.3f}")
print(f"真实值: w0={true_w0}, w1={true_w1}, sigma={true_sigma}")

# 可视化贝叶斯线性回归结果
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 子图1:后验采样的回归线
x_plot = np.linspace(-3, 3, 100)                           # 绘图x范围
for i in range(0, len(w_samples), 50):                     # 每隔50条画一条回归线
    axes[0].plot(x_plot, w_samples[i, 0] + w_samples[i, 1] * x_plot,
                 'b-', alpha=0.05, linewidth=1)             # 后验采样的回归线
# 数据点
axes[0].scatter(x_data, y_data, c='red', s=20, zorder=5, label='数据点')
# 真实回归线
axes[0].plot(x_plot, true_w0 + true_w1 * x_plot, 'g--',
             linewidth=2, label='真实回归线')
# 后验均值回归线
axes[0].plot(x_plot, np.mean(w_samples[:, 0]) + np.mean(w_samples[:, 1]) * x_plot,
             'r-', linewidth=2, label='后验均值回归线')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title('贝叶斯线性回归(Gibbs采样)')
axes[0].legend()

# 子图2:w0和w1的后验分布
axes[1].scatter(w_samples[:, 0], w_samples[:, 1], s=1, alpha=0.2, c='navy')
axes[1].axvline(x=true_w0, color='red', linestyle='--', label=f'真实w0={true_w0}')
axes[1].axhline(y=true_w1, color='green', linestyle='--', label=f'真实w1={true_w1}')
axes[1].set_xlabel('w0 (截距)')
axes[1].set_ylabel('w1 (斜率)')
axes[1].set_title('权重后验分布')
axes[1].legend()

# 子图3:sigma的后验分布
axes[2].hist(sigma_samples, bins=60, density=True, alpha=0.7, color='orange')
axes[2].axvline(x=true_sigma, color='red', linestyle='--',
                linewidth=2, label=f'真实σ={true_sigma}')
axes[2].set_xlabel('σ')
axes[2].set_ylabel('概率密度')
axes[2].set_title('噪声标准差后验分布')
axes[2].legend()

plt.tight_layout()
plt.show()


# ============================================================
# 案例3:Gibbs采样用于图像去噪(Ising模型)
# ============================================================

def gibbs_image_denoise(noisy_image, n_iter=20, beta=2.0, eta=1.0):
    """
    基于Ising模型的Gibbs采样图像去噪
    
    模型:
    - 隐变量 x_{i,j} ∈ {-1, +1}(干净图像像素)
    - 观测 y_{i,j} ∈ {-1, +1}(含噪图像像素)
    - 能量函数: E(x,y) = -beta * sum(x_i * x_j) - eta * sum(x_i * y_i)
      - 第一项:相邻像素的平滑先验(鼓励相邻像素取相同值)
      - 第二项:观测似然(鼓励与观测一致)
    
    参数:
        noisy_image: 含噪二值图像(值为-1或+1)
        n_iter:      每个像素的Gibbs迭代次数
        beta:        先验强度参数(越大越平滑)
        eta:         似然强度参数(越大约束越强)
    返回:
        denoised: 去噪后的图像
    """
    rows, cols = noisy_image.shape                          # 获取图像尺寸
    x = noisy_image.copy()                                  # 初始化隐变量为含噪图像
    
    for iteration in range(n_iter):                         # 迭代去噪
        for i in range(rows):                               # 遍历每一行
            for j in range(cols):                           # 遍历每一列
                # 计算四邻域的和
                neighbor_sum = 0                            # 初始化邻域和
                if i > 0:                                   # 上方邻居
                    neighbor_sum += x[i-1, j]
                if i < rows - 1:                            # 下方邻居
                    neighbor_sum += x[i+1, j]
                if j > 0:                                   # 左方邻居
                    neighbor_sum += x[i, j-1]
                if j < cols - 1:                            # 右方邻居
                    neighbor_sum += x[i, j+1]
                
                # 计算 p(x_{i,j} = +1 | rest) 的未归一化对数概率
                # log p(x=+1) = beta * neighbor_sum + eta * y_{i,j}
                # log p(x=-1) = -beta * neighbor_sum - eta * y_{i,j}
                log_p_pos = beta * neighbor_sum + eta * noisy_image[i, j]   # x=+1的对数概率
                log_p_neg = -beta * neighbor_sum - eta * noisy_image[i, j]  # x=-1的对数概率
                
                # 使用softmax计算概率(数值稳定版本)
                max_log_p = max(log_p_pos, log_p_neg)      # 取最大值防止溢出
                p_pos = np.exp(log_p_pos - max_log_p)       # 计算 x=+1 的(未归一化)概率
                p_neg = np.exp(log_p_neg - max_log_p)       # 计算 x=-1 的(未归一化)概率
                
                prob_pos = p_pos / (p_pos + p_neg)          # 归一化得到概率
                
                # 从伯努利分布采样
                if np.random.random() < prob_pos:           # 以 prob_pos 的概率
                    x[i, j] = 1                             # 设置为 +1
                else:                                       # 以 1 - prob_pos 的概率
                    x[i, j] = -1                            # 设置为 -1
        
        if (iteration + 1) % 5 == 0:                        # 每5次迭代打印进度
            print(f"  Gibbs去噪: 迭代 {iteration+1}/{n_iter}")
    
    return x                                                # 返回去噪后的图像


# 生成测试图像
np.random.seed(42)
img_size = 30                                               # 图像尺寸 30x30

# 创建简单的二值测试图像(条纹模式)
clean_image = np.ones((img_size, img_size))                 # 全白背景
clean_image[:, 10:15] = -1                                  # 中间竖条为黑色
clean_image[10:15, :] = -1                                  # 中间横条为黑色
# 形成十字形图案

# 添加噪声:翻转约20%的像素
noise_prob = 0.2                                            # 噪声翻转概率
noise_mask = np.random.random((img_size, img_size)) < noise_prob  # 生成噪声掩码
noisy_image = clean_image.copy()                            # 复制干净图像
noisy_image[noise_mask] *= -1                               # 翻转被选中的像素

# 运行Gibbs采样去噪
print("开始Gibbs采样去噪...")
denoised_image = gibbs_image_denoise(
    noisy_image,                                            # 输入含噪图像
    n_iter=10,                                              # 迭代次数
    beta=1.5,                                               # 先验强度
    eta=1.0                                                 # 似然强度
)

# 计算去噪准确率
noise_rate_before = np.mean(noisy_image != clean_image)     # 去噪前的错误率
noise_rate_after = np.mean(denoised_image != clean_image)   # 去噪后的错误率
print(f"去噪前错误率: {noise_rate_before:.4f}")
print(f"去噪后错误率: {noise_rate_after:.4f}")

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
axes[0].imshow(clean_image, cmap='gray', vmin=-1, vmax=1)  # 显示干净图像
axes[0].set_title('原始干净图像')
axes[0].axis('off')

axes[1].imshow(noisy_image, cmap='gray', vmin=-1, vmax=1)  # 显示含噪图像
axes[1].set_title(f'含噪图像 (错误率={noise_rate_before:.2%})')
axes[1].axis('off')

axes[2].imshow(denoised_image, cmap='gray', vmin=-1, vmax=1)  # 显示去噪结果
axes[2].set_title(f'Gibbs去噪结果 (错误率={noise_rate_after:.2%})')
axes[2].axis('off')

plt.tight_layout()
plt.show()

五、不同峰值之间的混合挑战(Mixing Challenge)

5.1 核心概念

混合(Mixing)问题 是MCMC方法面临的最核心挑战之一。当目标分布具有多个分离的峰值(模式)时,马尔可夫链可能会长时间被困在某一个峰值附近,难以在不同峰值之间转移,导致:

  1. 采样偏差:样本主要集中在初始峰值附近,不能正确反映完整的目标分布
  2. 自相关性高:连续样本高度相关,有效样本量远低于实际样本量
  3. 收敛缓慢:需要极长的链才能近似正确的分布

根本原因:

  • Metropolis类算法使用局部提议(如高斯随机游走),从一个峰值到另一个峰值的路径可能经过概率极低的区域
  • 提议点大概率落在低概率区域而被拒绝
  • 即使峰值之间的距离不远,若中间有低概率的"谷底",链也难以穿越

5.2 常见解决方案

方法核心思想
退火重要采样(AIS)通过一系列逐渐变化的分布平滑过渡
并行回火(Parallel Tempering)同时运行不同"温度"的链,高温链混合好,通过交换促进混合
汉密尔顿蒙特卡罗(HMC)利用梯度信息引导采样,更有效地穿越低概率区域
混合提议结合局部和全局提议

5.3 代码案例

import numpy as np
import matplotlib.pyplot as plt
from scipy import stats

# ============================================================
# 案例1:展示混合问题 —— 双峰分布的标准MH采样
# ============================================================

def bimodal_pdf(x):
    """
    计算双峰分布的概率密度
    p(x) = 0.4 * N(-4, 1) + 0.6 * N(4, 1.2)
    
    参数:
        x: 输入值
    返回:
        概率密度值
    """
    comp1 = 0.4 * np.exp(-0.5 * (x + 4)**2) / np.sqrt(2 * np.pi)       # 左侧峰值
    comp2 = 0.6 * np.exp(-0.5 * (x - 4)**2 / 1.2**2) / (1.2 * np.sqrt(2 * np.pi))  # 右侧峰值
    return comp1 + comp2                                                  # 返回混合密度


def standard_mh_sampler(n_samples, proposal_std=0.5, init_x=-4.0):
    """
    标准Metropolis-Hastings采样器(对称高斯提议)
    
    参数:
        n_samples:    采样数
        proposal_std: 提议分布标准差
        init_x:       初始点
    返回:
        samples: 采样链
    """
    samples = np.zeros(n_samples)                          # 初始化样本数组
    samples[0] = init_x                                    # 设置初始点
    n_accepted = 0                                         # 接受计数
    
    for i in range(1, n_samples):                          # 迭代
        current = samples[i-1]                             # 当前状态
        # 对称高斯提议
        proposal = current + np.random.normal(0, proposal_std)  # 候选点
        # MH比率
        p_curr = bimodal_pdf(current)                      # 当前点的概率密度
        p_prop = bimodal_pdf(proposal)                     # 候选点的概率密度
        
        alpha = min(1.0, p_prop / (p_curr + 1e-300))       # 接受概率(避免除零)
        
        if np.random.random() < alpha:                     # 接受判断
            samples[i] = proposal                          # 接受候选
            n_accepted += 1
        else:
            samples[i] = current                           # 保持当前状态
    
    return samples, n_accepted / (n_samples - 1)           # 返回样本和接受率


np.random.seed(42)                                         # 固定随机种子

# 标准MH:从左侧峰值开始,proposal_std较小
samples_mh, acc_mh = standard_mh_sampler(
    n_samples=50000,                                       # 样本数
    proposal_std=0.8,                                      # 提议标准差
    init_x=-4.0                                            # 从左侧峰值开始
)
print(f"标准MH 接受率: {acc_mh:.4f}")                       # 输出接受率

# 可视化混合问题
fig, axes = plt.subplots(2, 2, figsize=(16, 12))

x_range = np.linspace(-10, 10, 1000)                       # 绘图范围
true_pdf_vals = bimodal_pdf(x_range)                       # 真实PDF值

# 子图1:迹图 —— 链被困在左侧峰值
axes[0, 0].plot(samples_mh[:10000], linewidth=0.3, color='blue')
axes[0, 0].axhline(y=-4, color='red', linestyle='--', alpha=0.5, label='峰值1位置')
axes[0, 0].axhline(y=4, color='green', linestyle='--', alpha=0.5, label='峰值2位置')
axes[0, 0].set_xlabel('迭代次数')
axes[0, 0].set_ylabel('x值')
axes[0, 0].set_title('标准MH迹图 —— 链被困在单一峰值')
axes[0, 0].legend()

# 子图2:采样直方图 vs 真实分布 —— 严重偏差
axes[0, 1].hist(samples_mh, bins=200, density=True, alpha=0.7, color='steelblue',
                label='标准MH采样结果')
axes[0, 1].plot(x_range, true_pdf_vals, 'r-', linewidth=2, label='真实目标分布')
axes[0, 1].set_xlabel('x')
axes[0, 1].set_ylabel('概率密度')
axes[0, 1].set_title('标准MH采样偏差 —— 几乎未探索第二个峰值')
axes[0, 1].legend()

# ============================================================
# 案例2:解决方案 —— 并行回火 (Parallel Tempering)
# ============================================================

def parallel_tempering_sampler(n_samples, n_temps=10, beta_max=1.0, proposal_std=1.0):
    """
    并行回火采样器
    
    核心思想:
    - 同时运行多条链,每条链使用不同的"温度"
    - 高温链(beta小)的目标分布更平坦,更容易在峰值间移动
    - 低温链(beta大,特别是beta=1)精确采样目标分布
    - 通过相邻温度链之间的交换操作促进信息传递
    
    目标:在温度T下,采样 p(x)^{beta} ∝ exp(beta * log p(x))
    其中 beta = 1/T 是逆温度
    
    参数:
        n_samples:  每条链的采样数
        n_temps:    温度层数
        beta_max:   最大逆温度(=1对应目标分布)
        proposal_std: 提议标准差
    返回:
        all_chains: 所有温度链的采样结果
    """
    # 设置温度序列(几何序列)
    betas = np.linspace(0.1, beta_max, n_temps)             # 逆温度序列 [0.1, ..., 1.0]
    
    # 初始化所有链
    chains = np.full(n_temps, -4.0)                         # 所有链都从左侧峰值开始
    all_chains = np.zeros((n_samples, n_temps))              # 存储所有链的所有采样
    all_chains[0] = chains.copy()                            # 存储初始状态
    
    n_swaps = 0                                              # 交换计数
    n_swap_attempts = 0                                      # 交换尝试计数
    
    for i in range(1, n_samples):                            # 迭代采样
        # === 步骤1:每条链独立进行一步MH采样 ===
        for t in range(n_temps):                             # 遍历每个温度
            current = chains[t]                              # 当前链的状态
            proposal = current + np.random.normal(0, proposal_std)  # 提议候选点
            
            # 在温度beta_t下的接受概率
            # alpha = min(1, [p(x')^beta_t] / [p(x)^beta_t])
            #       = min(1, p(x')/p(x))^beta_t
            p_curr = bimodal_pdf(current) + 1e-300          # 当前点的密度
            p_prop = bimodal_pdf(proposal) + 1e-300         # 候选点的密度
            
            log_ratio = np.log(p_prop) - np.log(p_curr)     # 对数比率
            alpha = min(1.0, np.exp(betas[t] * log_ratio))  # 温度调制的接受概率
            
            if np.random.random() < alpha:                   # 接受判断
                chains[t] = proposal                         # 更新链状态
        
        # === 步骤2:相邻温度链之间的交换操作 ===
        # 随机选择一对相邻温度(奇偶交替以保证各态历经性)
        if i % 2 == 0:                                       # 偶数步:交换 (0,1), (2,3), ...
            pairs = [(k, k+1) for k in range(0, n_temps-1, 2)]
        else:                                                # 奇数步:交换 (1,2), (3,4), ...
            pairs = [(k, k+1) for k in range(1, n_temps-1, 2)]
        
        for t1, t2 in pairs:                                 # 遍历每对相邻温度
            n_swap_attempts += 1                             # 交换尝试计数
            
            # 计算交换的接受概率
            # alpha_swap = min(1, [p(x1)^beta2 * p(x2)^beta1] / [p(x1)^beta1 * p(x2)^beta2])
            #            = min(1, p(x1)^(beta2-beta1) * p(x2)^(beta1-beta2))
            x1, x2 = chains[t1], chains[t2]                 # 获取两条链的状态
            
            log_swap_ratio = (betas[t2] - betas[t1]) * (
                np.log(bimodal_pdf(x1) + 1e-300) - np.log(bimodal_pdf(x2) + 1e-300)
            )                                                # 计算交换的对数接受比
            
            alpha_swap = min(1.0, np.exp(log_swap_ratio))   # 交换接受概率
            
            if np.random.random() < alpha_swap:             # 若接受交换
                chains[t1], chains[t2] = x2, x1             # 交换两条链的状态
                n_swaps += 1                                 # 交换计数+1
        
        all_chains[i] = chains.copy()                        # 存储当前所有链的状态
    
    swap_rate = n_swaps / n_swap_attempts if n_swap_attempts > 0 else 0  # 交换率
    print(f"并行回火 - 温度层数: {n_temps}, 交换率: {swap_rate:.4f}")
    
    return all_chains, betas                                 # 返回所有链和温度序列


# 运行并行回火
np.random.seed(42)
n_pt_samples = 50000
all_chains_pt, betas_pt = parallel_tempering_sampler(
    n_samples=n_pt_samples,
    n_temps=10,                                             # 10个温度层
    proposal_std=1.0                                        # 提议标准差
)

# 只取最低温(beta=1.0,即目标温度)的链
samples_pt = all_chains_pt[:, -1]                            # 最后一列对应beta=1的链

# 子图3:并行回火的迹图
axes[1, 0].plot(samples_pt[:10000], linewidth=0.3, color='purple')
axes[1, 0].axhline(y=-4, color='red', linestyle='--', alpha=0.5, label='峰值1')
axes[1, 0].axhline(y=4, color='green', linestyle='--', alpha=0.5, label='峰值2')
axes[1, 0].set_xlabel('迭代次数')
axes[1, 0].set_ylabel('x值')
axes[1, 0].set_title('并行回火迹图 —— 能在峰值间自由移动')
axes[1, 0].legend()

# 子图4:并行回火采样直方图 vs 真实分布
axes[1, 1].hist(samples_pt, bins=200, density=True, alpha=0.7, color='mediumpurple',
                label='并行回火采样结果')
axes[1, 1].plot(x_range, true_pdf_vals, 'r-', linewidth=2, label='真实目标分布')
axes[1, 1].set_xlabel('x')
axes[1, 1].set_ylabel('概率密度')
axes[1, 1].set_title('并行回火采样 —— 正确覆盖两个峰值')
axes[1, 1].legend()

plt.tight_layout()
plt.show()


# ============================================================
# 案例3:不同温度链的可视化
# ============================================================

fig, axes = plt.subplots(3, 3, figsize=(18, 12))
temp_indices = [0, 4, 9]                                    # 选择3个代表性温度
sample_range = slice(0, 5000)                               # 显示前5000个样本

for row, t_idx in enumerate(temp_indices):                  # 遍历选定的温度
    chain = all_chains_pt[sample_range, t_idx]              # 获取该温度的链
    beta_val = betas_pt[t_idx]                              # 获取该温度的beta值
    
    # 迹图
    axes[row, 0].plot(chain, linewidth=0.3, color=['orange', 'blue', 'red'][row])
    axes[row, 0].set_ylabel(f'β={beta_val:.2f}')
    axes[row, 0].axhline(y=-4, color='gray', linestyle='--', alpha=0.3)
    axes[row, 0].axhline(y=4, color='gray', linestyle='--', alpha=0.3)
    if row == 0:
        axes[row, 0].set_title('迹图')
    
    # 直方图
    axes[row, 1].hist(chain, bins=100, density=True, alpha=0.7,
                      color=['orange', 'blue', 'red'][row])
    axes[row, 1].plot(x_range, true_pdf_vals * betas_pt[t_idx], 'r--',
                      linewidth=1, alpha=0.5)
    if row == 0:
        axes[row, 1].set_title('采样直方图')
    
    # 自相关函数
    acf_vals = compute_autocorrelation(chain, 200) if len(chain) > 200 else np.zeros(200)
    axes[row, 2].bar(range(100), acf_vals[:100], color=['orange', 'blue', 'red'][row], alpha=0.7)
    axes[row, 2].axhline(y=0, color='black', linestyle='-', linewidth=0.5)
    if row == 0:
        axes[row, 2].set_title('自相关函数')
    
    axes[row, 0].set_xlabel('迭代')
    axes[row, 1].set_xlabel('x')
    axes[row, 2].set_xlabel('滞后')

plt.suptitle('并行回火中不同温度链的对比分析', fontsize=16, y=1.02)
plt.tight_layout()
plt.show()


# ============================================================
# 案例4:混合挑战的定量分析 —— 有效样本数比较
# ============================================================

def effective_sample_size(chain):
    """
    计算有效样本数(ESS)
    ESS = N / (1 + 2 * sum_{k=1}^{K} rho_k)
    其中 rho_k 是滞后k的自相关系数,K取到自相关首次变为负值
    
    参数:
        chain: 一维采样链
    返回:
        ess: 有效样本数
    """
    n = len(chain)                                           # 链长度
    mean = np.mean(chain)                                    # 均值
    var = np.var(chain)                                      # 方差
    
    if var < 1e-10:                                          # 链几乎没有变化
        return 1.0                                           # ESS极小
    
    # 计算自相关函数直到第一次变为负
    autocorr_sum = 0                                         # 自相关累积和
    for lag in range(1, min(n // 2, 5000)):                  # 遍历滞后值
        cov = np.mean((chain[:n-lag] - mean) * (chain[lag:] - mean))  # 自协方差
        rho = cov / var                                      # 自相关系数
        if rho < 0:                                          # 若自相关变为负
            break                                            # 停止累加
        autocorr_sum += rho                                  # 累加正的自相关
    
    ess = n / (1 + 2 * autocorr_sum)                         # 计算ESS
    return ess


# 比较不同方法的ESS
ess_mh = effective_sample_size(samples_mh)                   # 标准MH的ESS
ess_pt = effective_sample_size(samples_pt)                   # 并行回火的ESS

print("\n" + "=" * 60)
print("混合效果定量比较")
print("=" * 60)
print(f"标准MH:      总样本={len(samples_mh)}, ESS={ess_mh:.0f}, "
      f"ESS/N={ess_mh/len(samples_mh):.4f}")
print(f"并行回火:    总样本={len(samples_pt)}, ESS={ess_pt:.0f}, "
      f"ESS/N={ess_pt/len(samples_pt):.4f}")

# 比较在两个峰值附近的采样比例
left_peak_ratio_mh = np.mean(samples_mh < 0)                # 标准MH中左侧峰值的采样比例
left_peak_ratio_pt = np.mean(samples_pt < 0)                # 并行回火中左侧峰值的采样比例

# 真实比例
# p(x<0) = 0.4 * Phi(0; -4, 1) 积分...近似计算
true_left_ratio, _ = quad(lambda x: bimodal_pdf(x), -20, 0)  # 数值积分计算左侧真实概率
true_left_ratio = true_left_ratio / quad(bimodal_pdf, -20, 20)[0]  # 归一化

print(f"\n左侧峰值 (x<0) 的采样比例:")
print(f"  标准MH:     {left_peak_ratio_mh:.4f}")
print(f"  并行回火:   {left_peak_ratio_pt:.4f}")
print(f"  真实比例:   {true_left_ratio:.4f}")


# ============================================================
# 案例5:三维多峰分布的混合挑战可视化
# ============================================================

# 使用简单的MH + 大跳跃提议来处理多峰问题

def multimodal_3d_pdf(x):
    """
    三维三峰分布
    三个峰值分别位于 (0,0,0), (5,5,5), (-5,5,0)
    
    参数:
        x: 形状为(3,)的数组
    返回:
        概率密度值
    """
    peaks = [np.array([0, 0, 0]), np.array([5, 5, 5]), np.array([-5, 5, 0])]  # 峰值位置
    weights = [0.3, 0.4, 0.3]                               # 各峰权重
    stds = [1.0, 1.2, 0.8]                                  # 各峰标准差
    
    density = 0                                              # 初始化密度
    for mu, w, s in zip(peaks, weights, stds):               # 遍历每个峰
        dist = np.linalg.norm(x - mu)                        # 计算到峰值的距离
        density += w * np.exp(-0.5 * (dist / s)**2) / (s**3 * (2*np.pi)**1.5)
    
    return density                                           # 返回混合密度


def mh_with_jump_proposals(n_samples, local_std=0.5, jump_prob=0.05, jump_std=5.0):
    """
    带有大跳跃提议的MH采样器
    以 (1-jump_prob) 的概率使用局部提议,以 jump_prob 的概率使用全局跳跃提议
    
    参数:
        n_samples:  采样数
        local_std:  局部提议标准差
        jump_prob:  大跳跃概率
        jump_std:   大跳跃提议标准差
    返回:
        samples: 采样结果
        accept_rate: 接受率
    """
    samples = np.zeros((n_samples, 3))                       # 初始化样本数组
    samples[0] = np.array([0.0, 0.0, 0.0])                  # 从原点开始
    n_accepted = 0                                           # 接受计数
    
    for i in range(1, n_samples):                            # 迭代
        current = samples[i-1]                               # 当前状态
        p_curr = multimodal_3d_pdf(current) + 1e-300        # 当前点密度
        
        if np.random.random() < jump_prob:                   # 以小概率进行大跳跃
            proposal = current + np.random.normal(0, jump_std, 3)  # 大跳跃提议
        else:                                                # 大部分时间使用局部提议
            proposal = current + np.random.normal(0, local_std, 3)  # 局部随机游走
        
        p_prop = multimodal_3d_pdf(proposal) + 1e-300       # 候选点密度
        
        # 考虑非对称提议的修正(混合提议的MH比率需要特殊处理)
        # 这里简化处理,因为局部和跳跃都是对称的
        alpha = min(1.0, p_prop / p_curr)                    # 接受概率
        
        if np.random.random() < alpha:                       # 接受判断
            samples[i] = proposal                            # 接受
            n_accepted += 1
        else:
            samples[i] = current                             # 拒绝
    
    return samples, n_accepted / (n_samples - 1)             # 返回样本和接受率


np.random.seed(42)

# 标准局部MH
samples_local, ar_local = mh_with_jump_proposals(
    n_samples=30000, jump_prob=0.0                           # 不使用跳跃
)

# 混合提议MH
samples_mixed, ar_mixed = mh_with_jump_proposals(
    n_samples=30000, jump_prob=0.05                          # 5%概率大跳跃
)

print(f"\n三维多峰采样:")
print(f"  纯局部MH 接受率: {ar_local:.4f}")
print(f"  混合提议MH 接受率: {ar_mixed:.4f}")

# 可视化(3D散点图投影到2D)
fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# 纯局部MH
axes[0].scatter(samples_local[:, 0], samples_local[:, 1], s=1, alpha=0.1, c='blue')
axes[0].scatter([0, 5, -5], [0, 5, 5], c='red', s=100, marker='*', zorder=5,
                label='真实峰值位置')
axes[0].set_xlabel('x')
axes[0].set_ylabel('y')
axes[0].set_title(f'纯局部MH (接受率={ar_local:.2f}) —— 仅探索初始峰值附近')
axes[0].legend()
axes[0].set_xlim(-10, 10)
axes[0].set_ylim(-5, 12)

# 混合提议MH
axes[1].scatter(samples_mixed[:, 0], samples_mixed[:, 1], s=1, alpha=0.1, c='purple')
axes[1].scatter([0, 5, -5], [0, 5, 5], c='red', s=100, marker='*', zorder=5,
                label='真实峰值位置')
axes[1].set_xlabel('x')
axes[1].set_ylabel('y')
axes[1].set_title(f'混合提议MH (接受率={ar_mixed:.2f}) —— 成功探索所有峰值')
axes[1].legend()
axes[1].set_xlim(-10, 10)
axes[1].set_ylim(-5, 12)

plt.tight_layout()
plt.show()

六、总结对比表

+------------------+-------------------+-------------------+-------------------+-------------------+
|      方法         |   需要采样分布     |    接受率          |   适用场景         |   混合能力         |
+------------------+-------------------+-------------------+-------------------+-------------------+
| 直接采样          | 需要知道CDF逆     | 100%              | 简单分布           | N/A               |
| 拒绝采样          | 需要包络函数      | < 100%(依赖M)    | 中等复杂分布       | 一般               |
| 重要采样          | 需要提议分布      | 100%(估计而非采样)| 期望估计           | N/A(非MCMC)      |
| Metropolis-Hast.  | 需要提议分布      | < 100%            | 通用               | 取决于提议         |
| Gibbs采样         | 需要条件分布      | 100%              | 条件分布已知       | 一般(局部更新)    |
| 并行回火          | 需要提议分布      | < 100% + 交换率   | 多峰分布           | 强                 |
+------------------+-------------------+-------------------+-------------------+-------------------+

更多推荐