2025-10-20 09:53:02 +08:00
|
|
|
|
import numpy as np
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
2025-11-02 20:47:28 +08:00
|
|
|
|
import scipy.stats as stats
|
|
|
|
|
|
from scipy.special import gammaln # 用于计算 log(Γ(x))
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# --- 1. 定义先验和似然函数 ---
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# 定义先验参数
|
|
|
|
|
|
# π(λ) ~ Gamma(α, β) (注意:scipy.stats.gamma用 a=shape, scale=1/rate)
|
|
|
|
|
|
# 我们使用 α=2, β=1 (rate=1) 作为先验
|
|
|
|
|
|
ALPHA_LAM = 2
|
|
|
|
|
|
BETA_LAM = 1 # 这是 rate (或 1/scale)
|
2025-10-23 10:29:27 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# π(r) ~ InverseGamma(α, β) (注意:scipy.stats.invgamma用 a=shape, scale=scale)
|
|
|
|
|
|
# 我们使用 α=2, β=1 (scale=1) 作为先验
|
|
|
|
|
|
ALPHA_R = 2
|
|
|
|
|
|
BETA_R = 1 # 这是 scale
|
2025-10-23 10:29:27 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# 模型先验
|
|
|
|
|
|
LOG_PRIOR_K1 = np.log(0.5)
|
|
|
|
|
|
LOG_PRIOR_K2 = np.log(0.5)
|
2025-10-23 10:29:27 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# 模型跳跃提议概率 q(k'|k)
|
|
|
|
|
|
# q(1|1)=0.5, q(2|1)=0.5, q(1|2)=0.5, q(2|2)=0.5
|
|
|
|
|
|
LOG_Q_1_GIVEN_1 = np.log(0.5)
|
|
|
|
|
|
LOG_Q_2_GIVEN_1 = np.log(0.5)
|
|
|
|
|
|
LOG_Q_1_GIVEN_2 = np.log(0.5)
|
|
|
|
|
|
LOG_Q_2_GIVEN_2 = np.log(0.5)
|
2025-10-23 10:29:27 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
def get_log_prior(k, params):
|
|
|
|
|
|
"""计算参数的对数先验概率"""
|
|
|
|
|
|
if k == 1:
|
|
|
|
|
|
lam = params[0]
|
|
|
|
|
|
if lam <= 0:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
# π(λ)
|
|
|
|
|
|
return stats.gamma.logpdf(lam, a=ALPHA_LAM, scale=1.0/BETA_LAM)
|
2025-10-23 10:29:27 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
elif k == 2:
|
|
|
|
|
|
lam, r = params
|
|
|
|
|
|
if lam <= 0 or r <= 0:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
# π(λ, r) = π(λ) * π(r) (假设先验独立)
|
|
|
|
|
|
log_p_lam = stats.gamma.logpdf(lam, a=ALPHA_LAM, scale=1.0/BETA_LAM)
|
|
|
|
|
|
log_p_r = stats.invgamma.logpdf(r, a=ALPHA_R, scale=BETA_R)
|
|
|
|
|
|
return log_p_lam + log_p_r
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
def get_log_likelihood(k, params, data):
|
|
|
|
|
|
"""计算数据的对数似然"""
|
|
|
|
|
|
if k == 1:
|
|
|
|
|
|
lam = params[0]
|
|
|
|
|
|
if lam <= 0:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
# Model 1: Poisson(λ)
|
|
|
|
|
|
return stats.poisson.logpmf(data, lam).sum()
|
|
|
|
|
|
|
|
|
|
|
|
elif k == 2:
|
|
|
|
|
|
lam, r = params
|
|
|
|
|
|
if lam <= 0 or r <= 0:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
# Model 2: Negative Binomial
|
|
|
|
|
|
# 使用 p = r / (λ + r) 的参数化
|
|
|
|
|
|
p = r / (lam + r)
|
|
|
|
|
|
# 必须检查 p 是否在 (0, 1] 范围内
|
|
|
|
|
|
if p <= 0 or p > 1:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
return stats.nbinom.logpmf(data, n=r, p=p).sum()
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
def get_log_posterior(k, params, data):
|
|
|
|
|
|
"""计算完整的对数后验(正比于)"""
|
|
|
|
|
|
log_prior = get_log_prior(k, params)
|
|
|
|
|
|
if log_prior == -np.inf:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
|
|
|
|
|
|
log_lik = get_log_likelihood(k, params, data)
|
|
|
|
|
|
if log_lik == -np.inf:
|
|
|
|
|
|
return -np.inf
|
|
|
|
|
|
|
|
|
|
|
|
log_model_prior = LOG_PRIOR_K1 if k == 1 else LOG_PRIOR_K2
|
|
|
|
|
|
|
|
|
|
|
|
return log_lik + log_prior + log_model_prior
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# --- 2. 生成模拟数据 ---
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
# 我们故意从一个过度离散的负二项分布生成数据
|
|
|
|
|
|
# 泊松分布:均值=方差。 负二项:方差 > 均值。
|
|
|
|
|
|
TRUE_LAMBDA = 5.0
|
|
|
|
|
|
TRUE_R = 10.0 # R 值变大,方差接近均值 (方差 = 5 + 25/10 = 7.5)
|
|
|
|
|
|
TRUE_P = TRUE_R / (TRUE_LAMBDA + TRUE_R)
|
|
|
|
|
|
np.random.seed(42)
|
|
|
|
|
|
N_data = 5000 # <--- 之前缺失的行
|
|
|
|
|
|
data = stats.nbinom.rvs(n=TRUE_R, p=TRUE_P, size=N_data)
|
2025-10-20 09:53:02 +08:00
|
|
|
|
|
2025-11-02 20:47:28 +08:00
|
|
|
|
print(f"模拟数据均值: {data.mean():.2f} (真实均值 = {TRUE_LAMBDA})")
|
|
|
|
|
|
print(f"模拟数据方差: {data.var():.2f} (泊松模型的方差应为 {data.mean():.2f})")
|
|
|
|
|
|
|
|
|
|
|
|
# --- 3. RJMCMC 主函数 ---
|
|
|
|
|
|
|
|
|
|
|
|
def run_rjmcmc(data, n_iter=50000, burn_in=10000):
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化
|
|
|
|
|
|
# 从模型1开始,λ 使用数据的均值
|
|
|
|
|
|
current_k = 1
|
|
|
|
|
|
current_lambda = data.mean()
|
|
|
|
|
|
current_params = [current_lambda]
|
|
|
|
|
|
|
|
|
|
|
|
# 存储轨迹
|
|
|
|
|
|
trace_k = np.zeros(n_iter, dtype=int)
|
|
|
|
|
|
trace_lambda = np.zeros(n_iter)
|
|
|
|
|
|
trace_r = np.full(n_iter, np.nan) # 仅当 k=2 时有值
|
|
|
|
|
|
|
|
|
|
|
|
# 接受计数器
|
|
|
|
|
|
acceptance = {
|
|
|
|
|
|
"1_to_1": 0, "2_to_2": 0, "1_to_2": 0, "2_to_1": 0
|
|
|
|
|
|
}
|
|
|
|
|
|
attempts = {
|
|
|
|
|
|
"1_to_1": 0, "2_to_2": 0, "1_to_2": 0, "2_to_1": 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for i in range(n_iter):
|
|
|
|
|
|
# 1. 提议一个目标模型 k_prop
|
|
|
|
|
|
# 无论当前 k 是多少,都以 50/50 的概率提议 k=1 或 k=2
|
|
|
|
|
|
k_prop = np.random.choice([1, 2])
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当前的对数后验
|
|
|
|
|
|
current_log_post = get_log_posterior(current_k, current_params, data)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------
|
|
|
|
|
|
# 情况 A: 模型内移动 (k_prop == current_k)
|
|
|
|
|
|
# ----------------------------------
|
|
|
|
|
|
if k_prop == current_k:
|
|
|
|
|
|
if current_k == 1:
|
|
|
|
|
|
# --- Model 1 -> Model 1 ---
|
|
|
|
|
|
attempts["1_to_1"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 提议一个新的 λ (使用正态分布随机游走)
|
|
|
|
|
|
lambda_prop = current_params[0] + np.random.normal(0, 0.5)
|
|
|
|
|
|
prop_params = [lambda_prop]
|
|
|
|
|
|
|
|
|
|
|
|
# 计算接受率
|
|
|
|
|
|
prop_log_post = get_log_posterior(1, prop_params, data)
|
|
|
|
|
|
log_alpha = prop_log_post - current_log_post
|
|
|
|
|
|
# (提议分布是对称的, q(λ'|λ) = q(λ|λ'))
|
|
|
|
|
|
|
|
|
|
|
|
if np.log(np.random.rand()) < log_alpha:
|
|
|
|
|
|
current_params = prop_params
|
|
|
|
|
|
acceptance["1_to_1"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
elif current_k == 2:
|
|
|
|
|
|
# --- Model 2 -> Model 2 ---
|
|
|
|
|
|
attempts["2_to_2"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 提议新的 (λ, r)
|
|
|
|
|
|
lambda_prop = current_params[0] + np.random.normal(0, 0.5)
|
|
|
|
|
|
r_prop = current_params[1] + np.random.normal(0, 0.5)
|
|
|
|
|
|
prop_params = [lambda_prop, r_prop]
|
|
|
|
|
|
|
|
|
|
|
|
# 计算接受率
|
|
|
|
|
|
prop_log_post = get_log_posterior(2, prop_params, data)
|
|
|
|
|
|
log_alpha = prop_log_post - current_log_post
|
|
|
|
|
|
|
|
|
|
|
|
if np.log(np.random.rand()) < log_alpha:
|
|
|
|
|
|
current_params = prop_params
|
|
|
|
|
|
acceptance["2_to_2"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------
|
|
|
|
|
|
# 情况 B: 跨模型移动 (k_prop != current_k)
|
|
|
|
|
|
# ----------------------------------
|
|
|
|
|
|
else:
|
|
|
|
|
|
if current_k == 1 and k_prop == 2:
|
|
|
|
|
|
# --- Model 1 -> Model 2 (诞生) ---
|
|
|
|
|
|
attempts["1_to_2"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 抽取辅助变量 w
|
|
|
|
|
|
w = np.random.uniform(0, 1)
|
|
|
|
|
|
log_g_w = stats.uniform.logpdf(w, 0, 1) # 这是 log(1) = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 应用映射
|
|
|
|
|
|
lambda_prop = current_params[0]
|
|
|
|
|
|
r_prop = -np.log(w)
|
|
|
|
|
|
prop_params = [lambda_prop, r_prop]
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 计算雅可比项 |J| = 1/w
|
|
|
|
|
|
log_jacobian = np.log(1.0 / w)
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 计算接受率
|
|
|
|
|
|
prop_log_post = get_log_posterior(2, prop_params, data)
|
|
|
|
|
|
|
|
|
|
|
|
# log_alpha = (log_post_prop + log_q_backward) - (log_post_curr + log_q_forward + log_g_w) + log_jacobian
|
|
|
|
|
|
log_alpha = (prop_log_post + LOG_Q_1_GIVEN_2) - \
|
|
|
|
|
|
(current_log_post + LOG_Q_2_GIVEN_1 + log_g_w) + \
|
|
|
|
|
|
log_jacobian
|
|
|
|
|
|
|
|
|
|
|
|
if np.log(np.random.rand()) < log_alpha:
|
|
|
|
|
|
current_k = 2
|
|
|
|
|
|
current_params = prop_params
|
|
|
|
|
|
acceptance["1_to_2"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
elif current_k == 2 and k_prop == 1:
|
|
|
|
|
|
# --- Model 2 -> Model 1 (死亡) ---
|
|
|
|
|
|
attempts["2_to_1"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 这是一个确定性映射(h' 的逆)
|
|
|
|
|
|
current_lambda, current_r = current_params
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 应用逆映射
|
|
|
|
|
|
lambda_prop = current_lambda
|
|
|
|
|
|
w_prime = np.exp(-current_r) # 这就是辅助变量 w'
|
|
|
|
|
|
prop_params = [lambda_prop]
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 计算雅可比项 |J'| = e^(-r)
|
|
|
|
|
|
log_jacobian_prime = np.log(np.exp(-current_r)) # 即 -current_r
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 计算 g(w'),这是 *正向* 移动 (1->2) 中 w 的密度
|
|
|
|
|
|
# 正向移动是 w ~ U(0, 1),所以 g(w') = 1 (只要 0 < w' < 1)
|
|
|
|
|
|
# 因为 r > 0, 所以 w' = e^(-r) 总是在 (0, 1) 区间内
|
|
|
|
|
|
log_g_w_prime = stats.uniform.logpdf(w_prime, 0, 1) # log(1) = 0
|
|
|
|
|
|
|
|
|
|
|
|
# 5. 计算接受率
|
|
|
|
|
|
prop_log_post = get_log_posterior(1, prop_params, data)
|
|
|
|
|
|
|
|
|
|
|
|
# log_alpha = (log_post_prop + log_q_backward + log_g_w_prime) - (log_post_curr + log_q_forward) + log_jacobian
|
|
|
|
|
|
log_alpha = (prop_log_post + LOG_Q_2_GIVEN_1 + log_g_w_prime) - \
|
|
|
|
|
|
(current_log_post + LOG_Q_1_GIVEN_2) + \
|
|
|
|
|
|
log_jacobian_prime
|
|
|
|
|
|
|
|
|
|
|
|
if np.log(np.random.rand()) < log_alpha:
|
|
|
|
|
|
current_k = 1
|
|
|
|
|
|
current_params = prop_params
|
|
|
|
|
|
acceptance["2_to_1"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
# 存储当前状态
|
|
|
|
|
|
trace_k[i] = current_k
|
|
|
|
|
|
trace_lambda[i] = current_params[0]
|
|
|
|
|
|
if current_k == 2:
|
|
|
|
|
|
trace_r[i] = current_params[1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
trace_r[i] = np.nan
|
|
|
|
|
|
|
|
|
|
|
|
# 打印接受率
|
|
|
|
|
|
print("\n--- 接受率 ---")
|
|
|
|
|
|
for move, count in attempts.items():
|
|
|
|
|
|
if count > 0:
|
|
|
|
|
|
rate = acceptance[move] / count
|
|
|
|
|
|
print(f"{move}: {acceptance[move]}/{count} ({rate:.2%})")
|
|
|
|
|
|
|
|
|
|
|
|
# 丢弃 Burn-in
|
|
|
|
|
|
trace_k_burned = trace_k[burn_in:]
|
|
|
|
|
|
trace_lambda_burned = trace_lambda[burn_in:]
|
|
|
|
|
|
trace_r_burned = trace_r[burn_in:]
|
|
|
|
|
|
|
|
|
|
|
|
return trace_k_burned, trace_lambda_burned, trace_r_burned
|
|
|
|
|
|
|
|
|
|
|
|
# --- 4. 运行和绘图 ---
|
|
|
|
|
|
|
|
|
|
|
|
N_ITER = 20000
|
|
|
|
|
|
BURN_IN = 5000
|
|
|
|
|
|
trace_k, trace_lambda, trace_r = run_rjmcmc(data, n_iter=N_ITER, burn_in=BURN_IN)
|
|
|
|
|
|
|
|
|
|
|
|
# --- 绘图 ---
|
|
|
|
|
|
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
|
|
|
|
|
|
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
|
|
|
|
|
|
|
|
|
|
|
|
# 图 1: 模型后验概率
|
|
|
|
|
|
plt.figure(figsize=(12, 10))
|
|
|
|
|
|
|
|
|
|
|
|
prob_k1 = np.mean(trace_k == 1)
|
|
|
|
|
|
prob_k2 = np.mean(trace_k == 2)
|
|
|
|
|
|
|
|
|
|
|
|
ax1 = plt.subplot(3, 1, 1)
|
|
|
|
|
|
bars = plt.bar([1, 2], [prob_k1, prob_k2], color=["#227dbe", "#f97b0df9"], tick_label=['模型 1 (泊松)', '模型 2 (负二项)'])
|
|
|
|
|
|
plt.title('模型后验概率', fontsize=16)
|
|
|
|
|
|
plt.ylabel('P(k | data)', fontsize=12)
|
|
|
|
|
|
ax1.bar_label(bars, fmt='{:.2%}', fontsize=12)
|
|
|
|
|
|
ax1.set_ylim(0, 1)
|
|
|
|
|
|
|
|
|
|
|
|
# 图 2: 模型跳跃轨迹 (仅显示前 2000 步,看得更清楚)
|
|
|
|
|
|
ax2 = plt.subplot(3, 1, 2)
|
|
|
|
|
|
ax2.plot(trace_k[:2000], 'k.', markersize=2, alpha=0.5)
|
|
|
|
|
|
ax2.set_yticks([1, 2])
|
|
|
|
|
|
ax2.set_yticklabels(['模型 1 (泊松)', '模型 2 (负二项)'])
|
|
|
|
|
|
ax2.set_title('模型空间轨迹 (前2000次迭代)', fontsize=16)
|
|
|
|
|
|
ax2.set_xlabel('迭代次数', fontsize=12)
|
|
|
|
|
|
|
|
|
|
|
|
# 图 3: 参数后验分布
|
|
|
|
|
|
# λ (lambda) 的后验
|
|
|
|
|
|
ax3 = plt.subplot(3, 2, 5)
|
|
|
|
|
|
ax3.hist(trace_lambda, bins=50, density=True, color='#1f77b4', alpha=0.7, label='$\lambda$ 的后验分布')
|
|
|
|
|
|
ax3.axvline(data.mean(), color='red', linestyle='--', label=f'数据均值 ({data.mean():.2f})')
|
|
|
|
|
|
ax3.axvline(TRUE_LAMBDA, color='black', linestyle=':', label=f'真实 $\lambda$ ({TRUE_LAMBDA})')
|
|
|
|
|
|
ax3.set_title('参数 $\lambda$ 的后验分布', fontsize=14)
|
|
|
|
|
|
ax3.set_xlabel('$\lambda$ 值', fontsize=12)
|
|
|
|
|
|
ax3.set_ylabel('密度', fontsize=12)
|
|
|
|
|
|
ax3.legend()
|
|
|
|
|
|
|
|
|
|
|
|
# r 的后验
|
|
|
|
|
|
ax4 = plt.subplot(3, 2, 6)
|
|
|
|
|
|
# 仅使用 k=2 时的 r 值
|
|
|
|
|
|
trace_r_k2 = trace_r[~np.isnan(trace_r)]
|
|
|
|
|
|
if len(trace_r_k2) > 0:
|
|
|
|
|
|
ax4.hist(trace_r_k2, bins=50, density=True, color='#ff7f0e', alpha=0.7, label='$r$ 的后验分布 (当 k=2)')
|
|
|
|
|
|
ax4.axvline(TRUE_R, color='black', linestyle=':', label=f'真实 $r$ ({TRUE_R})')
|
|
|
|
|
|
ax4.set_title('参数 $r$ 的后验分布 (仅当k=2)', fontsize=14)
|
|
|
|
|
|
ax4.set_xlabel('$r$ 值', fontsize=12)
|
|
|
|
|
|
ax4.legend()
|
|
|
|
|
|
else:
|
|
|
|
|
|
ax4.set_title('参数 $r$ 的后验分布 (未采样到)', fontsize=14)
|
|
|
|
|
|
ax4.text(0.5, 0.5, '从未接受过模型 2', horizontalalignment='center', verticalalignment='center', transform=ax4.transAxes)
|
|
|
|
|
|
|
|
|
|
|
|
plt.tight_layout()
|
2025-10-23 10:29:27 +08:00
|
|
|
|
plt.show()
|