2
0

提高篇:蒙特卡洛模拟

2026-05-16
提高篇:蒙特卡洛模拟

前言

蒙特卡洛方法是一类基于随机抽样的数值计算方法。它的核心思想朴素而强大——如果你想知道某个复杂系统的性质,不需要推导精确公式,让计算机随机生成大量样本然后统计结果即可。对于熟悉传统统计学方法但需要处理高维积分、复杂优化、随机系统仿真等问题的人来说,蒙特卡洛方法是不可替代的工具。

适用读者

本教程面向已经了解传统统计学方法的读者。如果你已经知道以下概念,就可以直接开始:

  • 期望、方差、协方差、相关系数

  • 正态分布、均匀分布、指数分布等常见分布

  • 大数定律、中心极限定理的基本思想

  • Python 基础编程(numpy、matplotlib)

本教程不讲解概率论的基础定义、分布的性质推导等内容,而是聚焦于如何用蒙特卡洛方法解决实际建模问题

内容结构

章节

内容

核心知识点

第1章

蒙特卡洛方法概述

基本思想、大数定律、收敛速率、与数值方法对比

第2章

随机数生成基础

伪随机数原理、常用分布采样、随机种子、逆变换采样

第3章

蒙特卡洛积分

平均值法、高维积分优势、不规则区域面积计算

第4章

蒙特卡洛优化

随机搜索、模拟退火算法、TSP 旅行商问题

第5章

系统仿真

M/M/1 排队系统、(s,Q) 库存策略、多服务员对比

第6章

方差缩减技术

对偶变量法、控制变量法、重要性采样、分层采样

第7章

MCMC 方法

Metropolis-Hastings、Gibbs 采样、贝叶斯线性回归

第8章

进阶应用

Bootstrap 重采样、库存优化综合实战

环境说明

  • Python 版本:3.8+

  • 核心依赖:numpy, scipy, matplotlib

  • 可视化:Matplotlib,使用 plt.rcParams['font.sans-serif'] = ['SimHei', ...] 配置中文显示

运行方式

# 安装依赖(如未安装)
pip install numpy scipy matplotlib

# 导入即可开始
import numpy as np

学习建议

  1. 第 1、2 章是基础,建议先通读掌握核心概念和 numpy 随机数 API

  2. 第 3、4、5 章是三大核心应用场景,按你常遇到的赛题类型选读

  3. 第 6 章的方差缩减技术可以显著提升已有蒙特卡洛代码的效率

  4. 第 7 章 MCMC 偏理论,贝叶斯推断场景时重点阅读

  5. 第 8 章 Bootstrap 是统计分析的实用工具,综合实战可直接参考代码模板

第1章 蒙特卡洛方法概述

1.1 基本思想

蒙特卡洛方法(Monte Carlo Method)是一类基于随机抽样的数值计算方法。它的核心思想非常朴素:

如果你想知道某个复杂系统的性质,不需要去推导精确公式——让计算机随机生成大量样本,然后统计结果

最经典的直观例子是投点法估算 π:在一个边长为 2 的正方形内随机投点,统计落在内切圆内的比例。由于圆面积 S=πr2=πS_{\text{圆}} = \pi r^2 = \pi,正方形面积 S=4,所以

SS=π4π4×圆内点数总点数\frac{S_{\text{圆}}}{S_{\text{方}}} = \frac{\pi}{4} \quad \Rightarrow \quad \pi \approx 4 \times \frac{\text{圆内点数}}{\text{总点数}}
import numpy as np

np.random.seed(42)
N = 3000
x = np.random.uniform(-1, 1, N)
y = np.random.uniform(-1, 1, N)
inside = x**2 + y**2 <= 1

pi_est = 4 * inside.sum() / N
print(f'π ≈ {pi_est:.4f}(真值 {np.pi:.4f},误差 {abs(pi_est - np.pi):.4f})')
π ≈ 3.1440(真值 3.1416,误差 0.0024)
ch01_pi_estimation.png

蓝色点落在圆内,红色点落在圆外。仅用 3000 个随机点,估计值就已经相当接近 π 的真实值。

为什么叫"蒙特卡洛"? 这个名字来自摩纳哥的蒙特卡洛赌场——因为方法的核心是"随机",和赌场的轮盘赌有异曲同工之妙。1940 年代,von Neumann 和 Ulam 在 Manhattan Project 中首次系统使用该方法,Ulam 的叔叔常在蒙特卡洛赌场赌博,于是便有了这个名字。

1.2 蒙特卡洛方法的工作流程

任何蒙特卡洛模拟都可以归纳为以下四步:

步骤

说明

关键问题

1. 建模

将问题转化为期望值形式 E[f(X)]

如何构造随机变量?

2. 抽样

从分布中独立抽取 X1,X2,…,XN

如何高效生成随机数?

3. 估计

θ^=1Ni=1Nf(Xi)\hat{\theta} = \frac{1}{N}\sum_{i=1}^N f(X_i)

估计量是否无偏?

4. 分析

评估误差、置信区间、收敛性

需要多少样本?

通用代码模板

import numpy as np

def monte_carlo_estimate(N, sampler, estimator):
    """
    蒙特卡洛估计通用模板
    
    Parameters
    ----------
    N : int
        样本量
    sampler : callable
        随机抽样函数,返回 shape=(N,) 的数组
    estimator : callable
        从样本计算估计值的函数
    
    Returns
    -------
    float
        蒙特卡洛估计值
    """
    samples = sampler(N)
    return estimator(samples)

1.3 大数定律与蒙特卡洛

蒙特卡洛方法的理论基石是大数定律(Law of Large Numbers)

弱大数定律

X1,X2,… 是独立同分布的随机变量,期望为 μ=E[X],则样本均值依概率收敛:

XˉN=1Ni=1NXiPμ(N)\bar{X}_N = \frac{1}{N}\sum_{i=1}^N X_i \xrightarrow{P} \mu \quad (N \to \infty)

这意味着:只要样本量足够大,蒙特卡洛估计值几乎必然接近真实值

中心极限定理与误差估计

大数定律告诉我们"会收敛",但没说"收敛多快"。中心极限定理(CLT) 给出了误差的定量刻画:

N(XˉNμ)dN(0,σ2)\sqrt{N}(\bar{X}_N - \mu) \xrightarrow{d} \mathcal{N}(0, \sigma^2)

其中 σ2=Var(X)\sigma^2 = \text{Var}(X)。由此可得:

  • 估计的标准误差:SE=σN\text{SE} = \frac{\sigma}{\sqrt{N}}

  • 95% 置信区间:XˉN±1.96σN\bar{X}_N \pm 1.96 \cdot \frac{\sigma}{\sqrt{N}}

np.random.seed(123)
N_total = 50000
x = np.random.uniform(-1, 1, N_total)
y = np.random.uniform(-1, 1, N_total)
inside = x**2 + y**2 <= 1

# 计算累积估计值和标准误差
sample_sizes = np.arange(100, N_total + 1, 100)
cumulative_pi = []
std_errors = []
for n in sample_sizes:
    indicators = (x[:n]**2 + y[:n]**2 <= 1).astype(float)
    pi_n = 4 * indicators.mean()
    se_n = 4 * indicators.std() / np.sqrt(n)
    cumulative_pi.append(pi_n)
    std_errors.append(se_n)

# 输出最后 5 个估计值和置信区间
for n, pi_est, se in zip(sample_sizes[-5:], cumulative_pi[-5:], std_errors[-5:]):
    print(f'N={n:>6d}: π ≈ {pi_est:.5f} ± {1.96*se:.5f} (95% CI)')
N= 49600: π ≈ 3.14772 ± 0.00619 (95% CI)
N= 49700: π ≈ 3.14769 ± 0.00619 (95% CI)
N= 49800: π ≈ 3.14767 ± 0.00619 (95% CI)
N= 49900: π ≈ 3.14764 ± 0.00619 (95% CI)
N= 50000: π ≈ 3.14768 ± 0.00618 (95% CI)
ch01_convergence.png

蓝色曲线是蒙特卡洛估计值随样本量增加的变化过程,红色虚线是 π 的真实值。灰色区域表示 ±0.1 的误差范围。可以看到估计值在 N ≈ 10000 后基本稳定在真值附近。

1.4 收敛速率

蒙特卡洛方法最重要的性质之一是其收敛速率。由 CLT 可知:

误差O(1N)\text{误差} \approx O\left(\frac{1}{\sqrt{N}}\right)

这意味着:

样本量 N

误差量级

说明

100

~0.1

粗略估计

10,000

~0.01

两位精度

1,000,000

~0.001

三位精度

100,000,000

~0.0001

四位精度

关键观察:要把误差缩小到原来的 1/10,需要 100 倍的样本量。这就是蒙特卡洛方法"慢"的原因——但它胜在与维度无关,这在数值积分等场景中是不可替代的优势。

ch01_error_convergence.png

对数坐标下,实际误差(蓝色实线)大致沿理论收敛速率 O(1/N)O(1/\sqrt{N})(红色虚线)下降。由于随机波动,实际误差曲线有明显震荡,但整体趋势与理论一致。

1.5 蒙特卡洛 vs 传统数值方法

特性

蒙特卡洛

确定性数值方法(如梯形法则)

收敛速率

O(1/N)O(1/\sqrt{N})

O(1/Np)O(1/N^p)(p 取决于方法)

维度依赖性

与维度无关

随维度指数增长(维度灾难)

高维积分

适用

不适用

不规则区域

容易处理

网格难以构造

并行化

天然并行

部分可并行

误差确定性

随机误差

确定性误差界

选择建议

  • 一维、二维积分 → 优先用确定性方法(Simpson、Gauss 积分等)

  • 三维以上积分 → 蒙特卡洛是唯一可行方案

  • 含随机性的系统(排队、金融、可靠性)→ 蒙特卡洛是自然选择

1.6 本节小结

  • 蒙特卡洛方法通过随机抽样 + 统计平均求解复杂问题

  • 理论基础是大数定律(保证收敛)和中心极限定理(给出误差估计)

  • 收敛速率为 O(1/N)O(1/\sqrt{N})——慢但与维度无关

  • 95% 置信区间:θ^±1.96σ^N\hat{\theta} \pm 1.96 \cdot \frac{\hat{\sigma}}{\sqrt{N}}

  • 适用场景:高维积分、不规则区域、随机系统仿真

  • 核心优势:高维问题中的唯一可行方案

1.7 实用代码速查

功能

代码

生成 N 个均匀随机数

np.random.uniform(a, b, N)

生成 N 个标准正态随机数

np.random.randn(N)

生成 N 个指数分布随机数

np.random.exponential(scale, N)

从数组中随机抽样

np.random.choice(arr, size=N)

计算样本均值

samples.mean()

计算样本标准差

samples.std(ddof=1)

蒙特卡洛估计器

samples.mean()

标准误差

samples.std(ddof=1) / np.sqrt(N)

95% 置信区间

mean ± 1.96 * std / sqrt(N)

第2章 随机数生成基础

2.1 伪随机数原理

计算机无法产生"真正"的随机数——所有"随机"数都是由确定性算法生成的,因此称为伪随机数(Pseudo-Random Number)

最常见的算法是线性同余生成器(LCG)

Xn+1=(aXn+c)modmX_{n+1} = (a \cdot X_n + c) \bmod m

其中 a,c,m 是精心选择的常数。给定初始值 X0(即种子),整个序列完全确定。

Python 中的随机数生成

方法

特点

推荐场景

np.random

旧版 API(全局状态)

遗留代码兼容

np.random.default_rng()

新版 API(基于 PCG 算法)

推荐,新代码使用

import numpy as np

# 新版推荐方式
rng = np.random.default_rng(seed=42)
samples = rng.standard_normal(5)  # 标准正态分布
print(samples)
[ 0.30471708 -1.03998411  0.7504512   0.94056472 -1.95103519]

2.2 均匀分布采样

均匀分布是蒙特卡洛方法的基础——几乎所有其他分布的采样都从均匀分布出发。

numpy 中的三种均匀分布函数

函数

返回值

示例

np.random.rand(d0, d1, ...)

[0, 1) 均匀分布,指定形状

np.random.rand(3, 2)

np.random.random(size)

[0, 1) 均匀分布,size 指定形状

np.random.random((3, 2))

np.random.uniform(low, high, size)

[low, high) 均匀分布

np.random.uniform(-1, 1, 100)

np.random.seed(42)

# 三种方式生成 [0, 1) 均匀分布
print('rand(3):', np.random.rand(3))
print('random(3):', np.random.random(3))
print('uniform(0,1,3):', np.random.uniform(0, 1, 3))

# 生成 [-1, 1] 均匀分布(投点法常用)
print('uniform(-1,1,5):', np.random.uniform(-1, 1, 5))
rand(3): [0.37454012 0.95071431 0.73199394]
random(3): [0.59865848 0.15601864 0.15599452]
uniform(0,1,3): [0.05808361 0.86617615 0.60111501]
uniform(-1,1,5): [ 0.70807258  0.02058449  0.96990985  0.83244297  0.21233911]

注意rand()random() 只能生成 [0, 1) 区间的均匀分布,而 uniform() 可以指定任意区间。

2.3 常用概率分布采样

numpy 提供了大量内置分布的采样函数,覆盖了蒙特卡洛方法中最常用的分布。

常用分布速查表

分布

numpy 函数

参数说明

典型应用

正态分布

rng.normal(loc, scale, size)

loc=均值, scale=标准差

噪声模拟、金融回报

标准正态

rng.standard_normal(size)

标准化随机扰动

均匀分布

rng.uniform(low, high, size)

low, high=区间端点

基础采样、投点法

指数分布

rng.exponential(scale, size)

scale=1/λ

等待时间、可靠性

泊松分布

rng.poisson(lam, size)

lam=期望到达率

事件计数、排队

二项分布

rng.binomial(n, p, size)

n=试验次数, p=成功概率

成功/失败试验

卡方分布

rng.chisquare(df, size)

df=自由度

假设检验、方差分析

Gamma 分布

rng.gamma(shape, scale, size)

shape=k, scale=θ

保险理赔、等待时间

Beta 分布

rng.beta(a, b, size)

a, b=形状参数

贝叶斯先验、比例

整数均匀

rng.integers(low, high, size)

low, high=区间

离散均匀抽样

rng = np.random.default_rng(seed=42)

# 生成不同分布的随机样本
normal_samples = rng.normal(loc=0, scale=1, size=5)
exp_samples = rng.exponential(scale=1.0, size=5)
poisson_samples = rng.poisson(lam=5, size=5)
beta_samples = rng.beta(a=2, b=5, size=5)

print('正态分布:', np.round(normal_samples, 4))
print('指数分布:', np.round(exp_samples, 4))
print('泊松分布:', poisson_samples)
print('Beta 分布:', np.round(beta_samples, 4))
正态分布: [ 0.3047 -1.04    0.7505  0.9406 -1.951 ]
指数分布: [0.3469 0.7119 0.0268 0.7352 0.1282]
泊松分布: [4 4 7 4 8]
Beta 分布: [0.201  0.1075 0.3383 0.5337 0.0166]
ch02_distributions.png

六宫格展示了六种常用分布的采样效果(蓝色/彩色直方图)与理论概率密度函数(黑色曲线)的对比。可以看到,10000 个样本已经能够很好地逼近理论分布。

注意泊松分布是离散分布,因此用 PMF(概率质量函数)而非 PDF,图中用黑点+连线表示理论值。

2.4 随机种子与可重复性

在蒙特卡洛模拟中,随机种子控制着随机数生成器的初始状态,决定了整个随机序列。

三种场景对比

ch02_random_seed.png
  • 左图(相同种子):两次设置相同的种子 seed=123,生成的 50 个随机数完全一致(红蓝点重叠)。这在调试和复现实验结果时非常关键。

  • 中图(不同种子):不同种子产生完全不同的随机序列,但统计性质相同(都服从标准正态分布)。

  • 右图(不设种子):每次运行都从系统熵源获取种子,结果不可复现。适用于需要真正随机性的场景(如加密)。

最佳实践

# 推荐:使用 default_rng,显式设置种子
rng = np.random.default_rng(seed=42)
samples1 = rng.normal(0, 1, 100)
samples2 = rng.normal(0, 1, 100)  # 继续使用同一 rng 对象

# 推荐:在函数/脚本开头统一设置
def monte_carlo_simulation(n_sims=10000, seed=None):
    rng = np.random.default_rng(seed)
    # 所有随机操作都用 rng.xxx()
    ...

# 避免:混用 np.random.seed() 和 np.random.xxx()
# 这会导致状态管理混乱,难以追踪随机序列

蒙特卡洛实验的种子设置原则

  1. 调试阶段:固定种子,确保每次运行结果一致

  2. 报告结果:记录使用的种子值,保证可复现

  3. 敏感性分析:用不同种子多次运行,检验结果稳定性

  4. 生产环境:可以不设种子或使用系统熵源

2.5 逆变换采样法

原理

逆变换采样(Inverse Transform Sampling)是从任意分布生成随机数的通用方法:

  1. 从均匀分布 U(0,1) 中抽取 u

  2. 计算 x=F1(u)x = F^{-1}(u),其中 F 是目标分布的 CDF

  3. x 服从目标分布

数学证明:设 UU(0,1),令 X=F1(U)X = F^{-1}(U),则

P(Xx)=P(F1(U)x)=P(UF(x))=F(x)P(X \leq x) = P(F^{-1}(U) \leq x) = P(U \leq F(x)) = F(x)

实例:从均匀分布生成指数分布

指数分布的 CDF 为 F(x)=1eλxF(x) = 1 - e^{-\lambda x},其反函数为:

F1(u)=ln(1u)λF^{-1}(u) = -\frac{\ln(1-u)}{\lambda}

由于 1uU(0,1)1-u \sim U(0,1),可简化为:x=−ln(u)/λ

ch02_inverse_transform.png

三张子图展示了逆变换采样的完整流程:

  • 左图:5000 个 U(0,1) 均匀随机数,直方图均匀分布在 [0, 1] 上

  • 中图:指数分布的 CDF 曲线(黑色),三条彩色竖线展示了 ux 的映射过程——从 u 值水平走到 CDF 曲线,再垂直落到 x 轴

  • 右图:经过逆变换后,样本从均匀分布变成了指数分布,直方图与理论 PDF 吻合

import numpy as np

np.random.seed(42)
N = 5000
u = np.random.uniform(0, 1, N)

# 逆变换:均匀 → 指数分布 (λ=2)
lam = 2.0
x_exp = -np.log(u) / lam

print(f'采样均值: {x_exp.mean():.4f}(理论: {1/lam:.4f})')
print(f'采样标准差: {x_exp.std():.4f}(理论: {1/lam:.4f})')
采样均值: 0.5042(理论: 0.5000)
采样采样: 0.5088(理论: 0.5000)

常见分布的逆变换公式

目标分布

CDF 反函数 F1(u)F^{-1}(u)

numpy 直接采样

指数(λ)

−ln(u)/λ

rng.exponential(1/λ)

均匀(a,b)

a+(ba)u

rng.uniform(a, b)

Cauchy

tan(π(u−0.5))

rng.standard_cauchy()

逻辑分布

ln(u/(1−u))

rng.logistic()

2.6 本节小结

  • 计算机生成的是伪随机数,由确定性算法产生

  • 大数定律逆变换采样是随机数生成的理论基础

  • np.random.default_rng(seed) 是新版本推荐的随机数生成方式

  • 固定种子用于可复现性,不同种子用于独立性

  • numpy 内置了十几种常用分布的采样函数,覆盖绝大多数蒙特卡洛需求

  • 逆变换采样是从均匀分布生成任意分布的通用方法

2.7 实用代码速查

功能

代码

创建 rng 对象

rng = np.random.default_rng(seed=42)

均匀分布 U(a,b)

rng.uniform(a, b, N)

标准正态分布

rng.standard_normal(N)

正态分布 N(μ,σ)

rng.normal(mu, sigma, N)

指数分布 Exp(λ)

rng.exponential(1/lam, N)

泊松分布 Pois(λ)

rng.poisson(lam, N)

二项分布 Bin(n,p)

rng.binomial(n, p, N)

Gamma 分布

rng.gamma(shape, scale, N)

Beta 分布

rng.beta(a, b, N)

整数均匀抽样

rng.integers(low, high, N)

从数组随机抽样

rng.choice(arr, size=N, replace=True)

随机排列

rng.permutation(arr)

打乱数组

rng.shuffle(arr)

多维正态

rng.multivariate_normal(mean, cov, N)

第3章 蒙特卡洛积分

3.1 蒙特卡洛积分的基本思想

蒙特卡洛积分的核心思想是将定积分问题转化为期望值估计问题

对于定积分 I=abf(x)dxI = \int_a^b f(x)\,dx,我们可以将其改写为期望形式:

I=(ba)abf(x)1badx=(ba)E[f(X)]I = (b - a) \int_a^b f(x) \cdot \frac{1}{b-a}\,dx = (b-a) \cdot E[f(X)]

其中 XU(a,b) 是区间 [a,b] 上的均匀分布随机变量。于是蒙特卡洛估计为:

I^N=baNi=1Nf(Xi),Xii.i.d.U(a,b)\hat{I}_N = \frac{b-a}{N} \sum_{i=1}^N f(X_i), \quad X_i \overset{i.i.d.}{\sim} U(a, b)

直观理解:投点法

ch03_integration_principle.png

以上图为例,估算 I=03(x2+1)dx=12I = \int_0^3 (x^2 + 1)\,dx = 12

  1. 在矩形 [0,3]×[0,12] 内随机投点(200 个)

  2. 统计落在曲线 f(x)=x2+1f(x) = x^2 + 1 下方的点数占比

  3. 积分估计 = 占比 ×× 矩形面积

import numpy as np

np.random.seed(42)
N = 200
x_samples = np.random.uniform(0, 3, N)
y_samples = np.random.uniform(0, 12, N)
below_curve = y_samples <= x_samples**2 + 1

rect_area = 3 * 12  # 矩形面积
mc_estimate = (below_curve.sum() / N) * rect_area

print(f'MC 估计 = {mc_estimate:.2f}')
print(f'精确值 = 12.00')
print(f'相对误差 = {abs(mc_estimate - 12) / 12 * 100:.2f}%')
MC 估计 = 12.24
精确值 = 12.00
相对误差 = 2.00%

注意:这种方法虽然直观,但效率不如直接采样估计(平均值法)。投点法更适用于面积/体积计算场景。

3.2 平均值法(样本均值法)

平均值法是蒙特卡洛积分的标准方法,效率更高:

I^N=baNi=1Nf(Xi)\hat{I}_N = \frac{b-a}{N} \sum_{i=1}^N f(X_i)
def mc_integral(f, a, b, N, seed=None):
    """蒙特卡洛积分——平均值法"""
    rng = np.random.default_rng(seed)
    x = rng.uniform(a, b, N)
    return (b - a) * f(x).mean()

# 示例:₀³ (x² + 1) dx
f = lambda x: x**2 + 1
true_value = 12.0

for N in [100, 1000, 10000, 100000]:
    est = mc_integral(f, 0, 3, N, seed=42)
    error = abs(est - true_value)
    print(f'N={N:>7d}: 估计={est:.4f}, 误差={error:.6f}')
N=    100: 估计=12.2399, 误差=0.239908
N=   1000: 估计=12.0001, 误差=0.000061
N=  10000: 估计=12.0000, 误差=0.000006
N= 100000: 估计=12.0000, 误差=0.000001

可以看到,随着样本量增加,估计值快速收敛到精确值。10 万样本时误差已降至 10^-6 量级。

3.3 高维积分——蒙特卡洛的优势

蒙特卡洛积分真正的威力在于高维积分

维度灾难

传统数值积分方法(梯形法则、Simpson 法则、Gauss 积分)在低维时非常高效,但误差随维度 d 呈指数增长:

方法

误差阶

维度增加的影响

梯形法则

O(1/N2/d)O(1/N^{2/d})

维度翻倍 → 需要 N2N^2 倍样本

Simpson

O(1/N4/d)O(1/N^{4/d})

维度翻倍 → 需要 N4N^4 倍样本

蒙特卡洛

O(1/N)O(1/\sqrt{N})

与维度无关!

高维积分示例

考虑 d 维积分:Id=[0,1]dex2dxI_d = \int_{[0,1]^d} e^{-\|x\|^2}\,dx

这个积分有解析解:Id=(π2erf(1))dI_d = \left(\frac{\sqrt{\pi}}{2} \cdot \text{erf}(1)\right)^d

from scipy.special import erf

# 精确值
exact_1d = np.sqrt(np.pi) / 2 * erf(1)
print(f'1维精确值: {exact_1d:.6f}')
print(f'5维精确值: {exact_1d**5:.6f}')
print(f'10维精确值: {exact_1d**10:.6f}')

# 蒙特卡洛估计
def mc_nd_integral(d, N, seed=42):
    rng = np.random.default_rng(seed)
    x = rng.uniform(0, 1, (N, d))  # N个d维样本
    f_vals = np.exp(-np.sum(x**2, axis=1))
    return f_vals.mean()

for d in [1, 5, 10]:
    est = mc_nd_integral(d, 100000)
    exact = exact_1d**d
    print(f'{d}维: MC估计={est:.6f}, 精确={exact:.6f}, 误差={abs(est-exact):.6f}')
1维精确值: 0.746824
5维精确值: 0.230375
10维精确值: 0.053073
1维: MC估计=0.746867, 精确=0.746824, 误差=0.000043
5维: MC估计=0.230336, 精确=0.230375, 误差=0.000039
10维: MC估计=0.053066, 精确=0.053073, 误差=0.000007

关键发现:10 维积分的精度与 1 维积分相当!这就是蒙特卡洛方法的"维度无关性"优势。

ch03_high_dim_integration.png

对数坐标下,不同维度的误差曲线都大致沿 O(1/N)O(1/\sqrt{N}) 速率下降。虽然高维时误差略大(因为方差更大),但收敛速率基本一致——这正是蒙特卡洛方法在高维问题中的核心优势。

3.4 不规则区域面积/体积计算

当积分区域形状不规则时,传统数值方法难以构造网格,而蒙特卡洛方法只需判断点是否在区域内即可。

心形区域面积

ch03_irregular_area.png

心形曲线(极坐标方程 r=1+cosθ)的面积精确值为 3π24.7124\frac{3\pi}{2} \approx 4.7124。用蒙特卡洛投点法,20000 个样本得到估计值 4.7074,相对误差仅 0.11%。

def in_cardioid(x, y):
    """判断点 (x, y) 是否在心形 (x²+y²-x)² ≤ x²+y² 内"""
    r2 = x**2 + y**2
    return (r2 - x)**2 <= r2

def mc_area_cardioid(N=20000, seed=42):
    rng = np.random.default_rng(seed)
    x_min, x_max = -2.1, 2.1
    y_min, y_max = -1.6, 1.6
    rect_area = (x_max - x_min) * (y_max - y_min)

    x = rng.uniform(x_min, x_max, N)
    y = rng.uniform(y_min, y_max, N)
    inside = in_cardioid(x, y)

    return inside.sum() / N * rect_area

est = mc_area_cardioid()
exact = 3 * np.pi / 2
print(f'MC 估计: {est:.4f}')
print(f'精确值:  {exact:.4f}')
print(f'相对误差: {abs(est - exact) / exact * 100:.2f}%')

通用模板

def mc_volume(indicator_fn, bounds, N=100000, seed=None):
    """
    蒙特卡洛计算任意区域体积
    
    Parameters
    ----------
    indicator_fn : callable
        指示函数,返回布尔数组,表示点是否在区域内
    bounds : list of tuple
        每个维度的边界,如 [(-1, 1), (0, 2), (-3, 3)]
    N : int
        样本量
    seed : int
        随机种子
    
    Returns
    -------
    float
        体积估计值
    """
    rng = np.random.default_rng(seed)
    d = len(bounds)
    
    # 计算包围框体积
    box_volume = 1.0
    for lo, hi in bounds:
        box_volume *= (hi - lo)
    
    # 生成包围框内的均匀随机点
    samples = np.column_stack([
        rng.uniform(lo, hi, N) for lo, hi in bounds
    ])
    
    # 判断哪些点在区域内
    inside = indicator_fn(samples)
    
    return inside.mean() * box_volume

3.5 蒙特卡洛积分的误差分析

由中心极限定理,蒙特卡洛积分估计的方差为:

Var(I^N)=(ba)2NVar(f(X))\text{Var}(\hat{I}_N) = \frac{(b-a)^2}{N} \cdot \text{Var}(f(X))

因此标准误差为:

SE=baNσf\text{SE} = \frac{b-a}{\sqrt{N}} \cdot \sigma_f

其中 σf 可用样本标准差估计:σ^f=1N1(f(Xi)fˉ)2\hat{\sigma}_f = \sqrt{\frac{1}{N-1}\sum(f(X_i) - \bar{f})^2}

def mc_integral_with_error(f, a, b, N, seed=None):
    """返回积分估计值、标准误差和 95% 置信区间"""
    rng = np.random.default_rng(seed)
    x = rng.uniform(a, b, N)
    y = f(x)
    
    estimate = (b - a) * y.mean()
    std_error = (b - a) * y.std(ddof=1) / np.sqrt(N)
    
    return estimate, std_error, (estimate - 1.96*std_error, estimate + 1.96*std_error)

f = lambda x: np.exp(-x**2) * np.sin(x)  # 较复杂函数
est, se, ci = mc_integral_with_error(f, 0, 5, 100000, seed=42)
print(f'估计值: {est:.6f}')
print(f'标准误差: {se:.6f}')
print(f'95% CI: ({ci[0]:.6f}, {ci[1]:.6f})')
估计值: 0.424409
标准误差: 0.001260
95% CI: (0.421940, 0.426879)

使用建议:在报告蒙特卡洛积分结果时,必须同时报告误差估计,否则结果无意义。

3.6 本节小结

  • 蒙特卡洛积分将定积分转化为期望估计:I^N=baNf(Xi)\hat{I}_N = \frac{b-a}{N}\sum f(X_i)

  • 平均值法效率高于投点法,是标准做法

  • 核心优势:收敛速率 O(1/N)O(1/\sqrt{N}) 与维度无关

  • 高维积分(d≥5)时,蒙特卡洛远优于传统数值方法

  • 不规则区域面积/体积计算是蒙特卡洛的天然优势场景

  • 必须报告误差估计:SE=baNσ^f\text{SE} = \frac{b-a}{\sqrt{N}} \cdot \hat{\sigma}_f

3.7 实用代码速查

功能

代码

平均值法积分

(b-a) * f(rng.uniform(a,b,N)).mean()

投点法积分

inside.sum()/N * (b-a) * (y_max-y_min)

d维积分

f(rng.uniform(0,1,(N,d))).mean()

不规则区域体积

inside.mean() * box_volume

标准误差

(b-a) * f_vals.std(ddof=1) / np.sqrt(N)

95% 置信区间

estimate ± 1.96 * se

scipy 精确积分

integrate.quad(f, a, b)

第4章 蒙特卡洛优化

4.1 随机搜索

随机搜索是最简单的蒙特卡洛优化方法:在可行域内随机采样,取函数值最优的点作为近似解。

算法步骤

  1. 在可行域 [a,b]d[a, b]^d 内均匀随机生成 N 个点

  2. 计算每个点的目标函数值

  3. 返回函数值最优的点

import numpy as np

def random_search(f, bounds, N=1000, seed=None):
    """
    随机搜索算法
    
    Parameters
    ----------
    f : callable
        目标函数,接受 (N, d) 数组返回长度为 N 的数组
    bounds : list of tuple
        每个维度的边界 [(lo, hi), ...]
    N : int
        样本量
    seed : int
        随机种子
    
    Returns
    -------
    best_x : ndarray
        最优解坐标
    best_val : float
        最优函数值
    """
    rng = np.random.default_rng(seed)
    d = len(bounds)
    samples = np.column_stack([rng.uniform(lo, hi, N) for lo, hi in bounds])
    values = f(samples)
    best_idx = np.argmin(values)
    return samples[best_idx], values[best_idx]

# 示例:Rastrigin 函数最小化
def rastrigin(X):
    return 20 + (X[:,0]**2 - 10*np.cos(2*np.pi*X[:,0])) + \
               (X[:,1]**2 - 10*np.cos(2*np.pi*X[:,1]))

best_x, best_val = random_search(rastrigin, [(-5,5), (-5,5)], N=10000, seed=42)
print(f'最优解: ({best_x[0]:.4f}, {best_x[1]:.4f})')
print(f'最优值: {best_val:.4f}(全局最小值=0 at (0,0))')
最优解: (0.0015, 0.0013)
最优值: 0.0000(全局最小值=0 at (0,0))

随机搜索 vs 网格搜索

ch04_random_vs_grid.png

以 Rastrigin 函数(多峰函数,全局最小值在 (0,0))为例,对比两种方法:

方法

优点

缺点

随机搜索

均匀覆盖整个搜索空间,不遗漏偏远区域

结果有随机性,每次不同

网格搜索

确定性,可复现

在高维空间中样本点呈指数稀疏

关键洞察:随机搜索的 200 个随机点比 14×14=196 个网格点覆盖更均匀——网格点在低维时密集但在某些区域完全空白,而随机点在大范围内更均匀地散布。

适用场景

  • 低精度需求的快速探索

  • 超参数调优的 baseline 方法

  • 目标函数计算昂贵,无法做大量迭代时

  • 作为更复杂算法的初始化阶段

4.2 模拟退火算法

灵感来源

模拟退火(Simulated Annealing, SA)灵感来自金属退火工艺:将金属加热到高温后缓慢冷却,使原子有足够时间找到能量最低的稳定排列。

算法框架

def simulated_annealing(f, x0, T0, alpha, n_iter, step_fn, seed=None):
    """
    模拟退火算法
    
    Parameters
    ----------
    f : callable
        目标函数
    x0 : ndarray
        初始解
    T0 : float
        初始温度
    alpha : float
        降温速率(0 < alpha < 1)
    n_iter : int
        迭代次数
    step_fn : callable
        邻域扰动函数,给定当前解返回候选解
    seed : int
        随机种子
    
    Returns
    -------
    best_x : ndarray
        最优解
    best_val : float
        最优函数值
    """
    rng = np.random.default_rng(seed)
    x = x0.copy()
    best_x = x.copy()
    best_val = f(x)
    current_val = best_val

    for i in range(n_iter):
        T = T0 * (alpha ** i)  # 指数降温
        x_new = step_fn(x, T, rng)
        f_new = f(x_new)

        # Metropolis 接受准则
        delta = f_new - current_val
        if delta < 0 or rng.random() < np.exp(-delta / T):
            x = x_new
            current_val = f_new
            if current_val < best_val:
                best_val = current_val
                best_x = x.copy()

    return best_x, best_val

核心要素

要素

说明

常用选择

初始温度 T0

决定初期接受劣解的概率

使初始接受率约 80%

降温策略

温度如何随迭代降低

指数降温 Tk+1=αTkT_{k+1} = \alpha T_k

邻域结构

如何生成候选解

高斯扰动 x=x+N(0,T)x' = x + N(0, T)

接受准则

何时接受劣解

Metropolis: P=exp(Δf/T)P = \exp(-\Delta f / T)

Metropolis 接受准则

P(接受)={1,Δf<0eΔf/T,Δf0P(\text{接受}) = \begin{cases} 1, & \Delta f < 0 \\ e^{-\Delta f / T}, & \Delta f \geq 0 \end{cases}

为什么接受劣解? 这是算法跳出局部最优的关键机制:

  • 高温阶段eΔf/T1e^{-\Delta f/T} \approx 1,几乎总是接受——全局探索

  • 低温阶段eΔf/T0e^{-\Delta f/T} \approx 0,几乎不接受劣解——局部精细搜索

可视化:优化过程

ch04_simulated_annealing.png

四张子图展示了模拟退火在 Ackley 函数(多峰函数)上的搜索过程:

  • 初期(50 次迭代):温度高,搜索范围大,路径在搜索空间中大幅跳跃

  • 中期(200 次迭代):温度降低,搜索开始集中到最优区域附近

  • 后期(500 次迭代):温度低,搜索路径收缩到全局最优解周围

  • 充分迭代(1000 次):充分收敛到全局最优解 (0, 0) 附近

观察:红色搜索路径从右上角的随机起点出发,初期在解空间中大幅探索,后期逐渐收缩并锁定到中心的全局最优解。

4.3 旅行商问题(TSP)

问题描述

给定 n 个城市及其两两之间的距离,找到一条访问每个城市恰好一次并回到起点的最短路径。

TSP 是经典的 NP-hard 组合优化问题——城市数 n 时,可能的路径数为 (n−1)!/2,当 n=20 时就有约 6×10166 \times 10^{16} 条路径。

模拟退火求解 TSP

def distance_matrix(cities):
    """计算城市间的距离矩阵"""
    n = len(cities)
    dist = np.zeros((n, n))
    for i in range(n):
        for j in range(i+1, n):
            d = np.sqrt(np.sum((cities[i] - cities[j])**2))
            dist[i][j] = dist[j][i] = d
    return dist

def tsp_route_cost(path, dist):
    """计算路径总距离"""
    n = len(path)
    cost = sum(dist[path[i], path[i+1]] for i in range(n-1))
    cost += dist[path[-1], path[0]]  # 回到起点
    return cost

def two_opt_swap(path, rng):
    """2-opt 邻域操作:随机选择一段路径并翻转"""
    n = len(path)
    new_path = path.copy()
    a, b = rng.integers(0, n, 2)
    if a > b:
        a, b = b, a
    new_path[a:b+1] = new_path[a:b+1][::-1]
    return new_path

def tsp_simulated_annealing(cities, T0=100, alpha=0.995, n_iter=5000, seed=42):
    rng = np.random.default_rng(seed)
    dist = distance_matrix(cities)
    n = len(cities)

    # 初始随机路径
    current_path = rng.permutation(n)
    current_cost = tsp_route_cost(current_path, dist)

    best_path = current_path.copy()
    best_cost = current_cost

    for i in range(n_iter):
        T = T0 * (alpha ** i)
        new_path = two_opt_swap(current_path, rng)
        new_cost = tsp_route_cost(new_path, dist)

        delta = new_cost - current_cost
        if delta < 0 or rng.random() < np.exp(-delta / T):
            current_path = new_path
            current_cost = new_cost
            if current_cost < best_cost:
                best_cost = current_cost
                best_path = current_path.copy()

    return best_path, best_cost
ch04_tsp.png

20 个城市的 TSP 问题,模拟退火在 5000 次迭代后:

  • 初始随机路径:总距离 781.4,路径交叉严重

  • 优化后路径:总距离 386.4,改善 50.5%

  • 优化后的路径几乎没有交叉,视觉上接近最优

运行结果

cities = np.random.uniform(0, 100, (20, 2))
rng_init = np.random.default_rng(42)
init_path = rng_init.permutation(20)
dist = distance_matrix(cities)

init_cost = tsp_route_cost(init_path, dist)
best_path, best_cost = tsp_simulated_annealing(cities)

print(f'初始路径距离: {init_cost:.1f}')
print(f'优化后路径距离: {best_cost:.1f}')
print(f'改善幅度: {(1 - best_cost/init_cost) * 100:.1f}%')
初始路径距离: 781.4
优化后路径距离: 386.4
改善幅度: 50.5%

4.4 算法选择建议

问题类型

推荐方法

理由

低维连续、单峰

梯度下降 / 牛顿法

收敛快,精度高

低维连续、多峰

模拟退火

可跳出局部最优

高维连续

随机搜索 + 局部优化

随机搜索做粗搜索,局部优化精细

组合优化(TSP、调度)

模拟退火

2-opt 邻域 + Metropolis 接受

超参数调优

随机搜索 → 贝叶斯优化

随机搜索做 baseline

黑箱函数、昂贵评估

贝叶斯优化

用代理模型减少评估次数

4.5 本节小结

  • 随机搜索:最简单,适合快速探索和高维粗搜索

  • 模拟退火:通过温度参数平衡探索与利用,可跳出局部最优

  • Metropolis 准则:高温时接受劣解以探索,低温时拒绝劣解以收敛

  • TSP:经典的组合优化问题,2-opt 邻域 + SA 可有效求解

  • 关键参数:初始温度 T0、降温速率 α、邻域结构

  • 降温过快 → 陷入局部最优;降温过慢 → 计算浪费

4.6 实用代码速查

功能

代码

随机搜索

samples = rng.uniform(lo, hi, (N,d)); idx = f(samples).argmin()

指数降温

T = T0 * alpha ** i

Metropolis 接受

delta < 0 or rng.random() < np.exp(-delta/T)

高斯扰动

x_new = x + rng.normal(0, T, d)

2-opt 交换

path[a:b+1] = path[a:b+1][::-1]

TSP 路径成本

sum(dist[path[i],path[i+1]] for i in range(n-1)) + dist[path[-1],path[0]]

距离矩阵

双重循环计算欧氏距离

第5章 系统仿真

5.1 什么是系统仿真

系统仿真通过计算机模拟真实系统的运行过程,是蒙特卡洛方法最重要的应用之一。与纯数学分析不同,仿真可以处理:

  • 复杂交互:多个组件之间的非线性交互

  • 随机性:到达时间、服务时间、故障时间等的随机性

  • 动态变化:系统状态随时间的演化

  • 难以建模的细节:现实中的复杂约束和规则

蒙特卡洛仿真 vs 解析方法

方法

适用场景

优点

缺点

解析方法

简单模型,可数学推导

精确解,计算快

模型假设强,适用范围窄

蒙特卡洛仿真

复杂模型,任意规则

灵活,可处理任意复杂系统

结果是估计值,计算量大

5.2 离散事件仿真框架

离散事件仿真(Discrete Event Simulation, DES)是系统仿真的核心范式。

核心概念

概念

说明

示例

实体(Entity)

系统中的活动对象

顾客、包裹、车辆

事件(Event)

改变系统状态的动作

到达、离开、开始服务

状态(State)

系统在某一时刻的描述

队列长度、服务器忙闲

事件时间表

按时间排序的待处理事件列表

下一个到达时间、下一个完成时间

仿真时钟

模拟的系统时间

从 0 开始逐步推进

通用仿真框架

class DiscreteEventSimulator:
    """离散事件仿真通用框架"""

    def __init__(self):
        self.clock = 0.0          # 仿真时钟
        self.events = []          # 事件时间表(优先队列)
        self.state = {}           # 系统状态
        self.records = []         # 仿真记录

    def schedule(self, time, event_type, **kwargs):
        """安排一个未来事件"""
        import heapq
        heapq.heappush(self.events, (time, event_type, kwargs))

    def handle_event(self, event_type, **kwargs):
        """处理事件(子类实现)"""
        raise NotImplementedError

    def run(self, stop_time):
        """运行仿真直到指定时间"""
        import heapq
        while self.events and self.clock < stop_time:
            self.clock, event_type, kwargs = heapq.heappop(self.events)
            self.handle_event(event_type, **kwargs)
        return self.records

5.3 M/M/1 排队系统

模型描述

M/M/1 排队系统是最基本的排队模型:

  • M(第一个):到达过程为泊松过程(指数分布到达间隔)

  • M(第二个):服务时间为指数分布

  • 1:单服务员

参数

符号

说明

到达率

λ

单位时间平均到达顾客数

服务率

μ

单位时间平均服务顾客数

利用率

ρ=λ/μ

服务员繁忙比例,要求 ρ<1

理论结果

指标

公式

含义

平均系统人数

L=ρ1ρL = \frac{\rho}{1-\rho}

包括排队和正在服务的

平均排队人数

Lq=ρ21ρL_q = \frac{\rho^2}{1-\rho}

不包括正在服务的

平均等待时间

W=1μλW = \frac{1}{\mu - \lambda}

从到达到离开的总时间

平均排队时间

Wq=ρμλW_q = \frac{\rho}{\mu - \lambda}

不包括服务时间

仿真实现

import numpy as np

def simulate_mm1(lam, mu, T, seed=None):
    """
    M/M/1 排队系统仿真

    Parameters
    ----------
    lam : float
        到达率
    mu : float
        服务率
    T : float
        仿真时间
    seed : int
        随机种子

    Returns
    -------
    times : ndarray
        事件发生时间序列
    queue_lengths : ndarray
        对应时刻的系统内人数
    n_served : int
        服务完成的顾客总数
    """
    rng = np.random.default_rng(seed)
    t = 0.0
    n_in_system = 0

    next_arrival = rng.exponential(1/lam)
    next_departure = float('inf')

    times = [0]
    queue_lengths = [0]
    n_served = 0

    while t < T:
        if next_arrival < next_departure:
            # 到达事件
            t = next_arrival
            n_in_system += 1
            next_arrival = t + rng.exponential(1/lam)
            if n_in_system == 1:
                next_departure = t + rng.exponential(1/mu)
        else:
            # 离开事件
            t = next_departure
            n_in_system -= 1
            n_served += 1
            if n_in_system > 0:
                next_departure = t + rng.exponential(1/mu)
            else:
                next_departure = float('inf')

        times.append(t)
        queue_lengths.append(n_in_system)

    return np.array(times), np.array(queue_lengths), n_served

# 运行仿真
lam, mu = 2.0, 2.5  # ρ = 0.8
T = 100
times, queue_lengths, n_served = simulate_mm1(lam, mu, T, seed=42)

# 计算统计量
avg_n = queue_lengths.mean()
theory_L = lam / (mu - lam)
print(f'仿真平均人数: {avg_n:.2f}')
print(f'理论平均人数: {theory_L:.2f}')
print(f'服务完成顾客数: {n_served}')
仿真平均人数: 3.85
理论平均人数: 4.00
服务完成顾客数: 188

仿真 vs 理论

ch05_mm1_queue.png

上图展示了系统内人数随时间的变化过程,下图是队列长度的分布对比:

  • 上图:蓝色阶梯线是仿真结果,红色虚线是理论平均 L=4.00。系统人数围绕理论均值波动,波动幅度较大。

  • 下图:蓝色直方图是仿真得到的队列长度分布,红色点是理论几何分布 P(n)=(1ρ)ρnP(n) = (1-\rho)\rho^n。仿真分布与理论分布吻合良好。

关键观察:尽管平均值接近理论值,但瞬态行为非常波动——峰值可达 14 人,远高于均值 4 人。这是排队系统的典型特征。

5.4 库存管理仿真

(s, Q) 订货策略

(s, Q) 策略是最常用的库存控制策略:

  • 当库存降至 再订货点 s 以下时,立即订货

  • 每次订货固定数量 Q

  • 订货有 提前期(lead time),货物不会立即到达

def simulate_inventory(demand_mean, demand_std, lead_time, reorder_point, order_qty,
                       n_days, initial_stock=100, seed=None):
    """
    库存管理仿真——(s, Q) 策略

    Parameters
    ----------
    demand_mean : float
        日均需求均值
    demand_std : float
        日均需求标准差
    lead_time : int
        订货提前期(天)
    reorder_point : int
        再订货点
    order_qty : int
        订货量
    n_days : int
        仿真天数
    initial_stock : int
        初始库存
    seed : int
        随机种子

    Returns
    -------
    stock_levels : ndarray
        每日库存水平
    orders : list
        订货记录 [(day, qty), ...]
    """
    rng = np.random.default_rng(seed)
    stock = initial_stock
    pending_order = 0
    order_arrival_day = float('inf')

    stock_levels = [stock]
    orders = []

    for day in range(n_days):
        # 检查是否有订单到达
        if day >= order_arrival_day:
            stock += pending_order
            pending_order = 0
            order_arrival_day = float('inf')

        # 需求(正态分布,截断到非负)
        demand = max(0, int(round(rng.normal(demand_mean, demand_std))))

        # 满足需求
        sold = min(stock, demand)
        stock -= sold

        # 检查是否需要订货
        if stock <= reorder_point and pending_order == 0:
            pending_order = order_qty
            order_arrival_day = day + lead_time
            orders.append((day, order_qty))

        stock_levels.append(stock)

    return np.array(stock_levels), orders

策略对比

ch05_inventory.png

三组 (s, Q) 策略的对比(日均需求 10,提前期 5 天):

策略

s

Q

平均库存

订货次数

特点

保守

15

50

14.3

45

库存低,订货频繁

适中

30

80

42.7

38

平衡库存和订货成本

激进

50

120

61.2

30

库存高,订货少

观察

  • 锯齿形波动是 (s, Q) 策略的典型特征——库存从高点逐步消耗到再订货点,然后补货

  • 绿色虚线标记订货时刻,红色虚线标记再订货点

  • 策略选择取决于:库存持有成本 vs 订货成本的权衡

5.5 多服务员排队系统

M/M/K 模型

M/M/K 系统有 K 个并行的服务员,顾客到达后选择空闲服务员,若全部忙碌则排队等待。

稳定性条件ρ=λKμ<1\rho = \frac{\lambda}{K\mu} < 1,即总服务能力必须大于到达率。

# 对比不同 K 值的系统行为
lam, mu = 4.0, 2.0  # 到达率 4,每服务员服务率 2

for K in [1, 2, 3]:
    rho = lam / (K * mu)
    stable = "稳定" if rho < 1 else "不稳定"
    if rho < 1:
        L = lam / (K * mu - lam)
        print(f'M/M/{K}: ρ = {rho:.2f}, 平均人数 L = {L:.2f} ({stable})')
    else:
        print(f'M/M/{K}: ρ = {rho:.2f} ({stable},队列无限增长)')
M/M/1: ρ = 2.00 (不稳定,队列无限增长)
M/M/2: ρ = 1.00 (不稳定,队列无限增长)
M/M/3: ρ = 0.67, 平均人数 L = 2.00 (稳定)

可视化对比

ch05_mmK_comparison.png

三张子图展示了 λ=4, μ=2 时不同服务员数量的系统行为:

  • M/M/1(ρ=2.0):服务能力不足,队列无限增长,系统不稳定

  • M/M/2(ρ=1.0):临界状态,队列仍然增长,系统不稳定

  • M/M/3(ρ=0.67):服务能力充足,队列在理论均值 L=2.0 附近波动,系统稳定

关键教训:增加一个服务员可以从不稳定变为稳定——这是排队系统设计中的重要决策。

5.6 本节小结

  • 离散事件仿真是处理复杂随机系统的核心工具

  • M/M/1 排队系统的仿真结果与理论公式高度一致

  • (s, Q) 库存策略中,s 和 Q 的选择需要权衡库存成本和订货成本

  • 排队系统的稳定性要求 ρ < 1,否则队列无限增长

  • 仿真可以处理任意复杂的规则,但必须注意:

    • 足够的仿真时间(消除初始瞬态影响)

    • 多次运行取平均(减少随机波动)

    • 验证仿真结果(与解析解对比)

5.7 实用代码速查

功能

代码

指数分布到达间隔

rng.exponential(1/lam)

指数分布服务时间

rng.exponential(1/mu)

正态分布需求

rng.normal(mean, std)

理论平均人数 L

lam / (mu - lam)(M/M/1)

理论利用率 ρ

lam / (K * mu)(M/M/K)

稳定性判断

rho < 1

几何分布概率

(1-rho) * rho**n

事件驱动仿真

优先队列 + 事件处理循环

第6章 方差缩减技术

6.1 为什么要缩减方差

蒙特卡洛估计的精度由方差决定。标准误差为:

SE=σN\text{SE} = \frac{\sigma}{\sqrt{N}}

要降低标准误差,只有两条路:

  1. 增加样本量 N——代价是计算时间线性增长

  2. 减小方差 σ2\sigma^2——用更聪明的抽样方法

方差缩减技术的核心目标:在不增加样本量的前提下,通过改进抽样策略来降低估计方差,从而在相同计算量下获得更高的精度。

直观对比

假设普通 MC 的方差为 σ2\sigma^2,某种方差缩减技术将方差降至 σ2/k\sigma^2/k。要达到相同的精度:

方法

所需样本量

计算时间

普通 MC

N

T

方差缩减

N/k

T/k

等价于免费获得了 k 倍的加速

6.2 对偶变量法(Antithetic Variates)

核心思想

对偶变量法利用负相关来降低方差。基本思路是:对每个随机样本 U,同时使用其对偶样本 1−U

对于估计 E[f(X)],其中 XU(0,1)

θ^anti=12Ni=1N[f(Ui)+f(1Ui)]\hat{\theta}_{\text{anti}} = \frac{1}{2N}\sum_{i=1}^N \left[f(U_i) + f(1-U_i)\right]

由于 U1−U 负相关,且当 f 单调时 f(U)f(1−U) 也负相关,因此:

Var(θ^anti)=12N[Var(f(U))+Cov(f(U),f(1U))]\text{Var}(\hat{\theta}_{\text{anti}}) = \frac{1}{2N}\left[\text{Var}(f(U)) + \text{Cov}(f(U), f(1-U))\right]

Cov(f(U),f(1U))<0\text{Cov}(f(U), f(1-U)) < 0 时,方差减小。

代码实现

import numpy as np

def antithetic_mc(f, N, seed=None):
    """
    对偶变量法蒙特卡洛估计

    Parameters
    ----------
    f : callable
        目标函数
    N : int
        总样本量(实际从 U(0,1) 采样 N/2 个,对偶生成另外 N/2 个)
    seed : int
        随机种子

    Returns
    -------
    float
        蒙特卡洛估计值
    """
    rng = np.random.default_rng(seed)
    u = rng.uniform(0, 1, N//2)
    u_dual = 1 - u
    u_all = np.concatenate([u, u_dual])
    return f(u_all).mean()

# 示例:估计 E[e^X], X ~ U(0,1)
true_val = np.e - 1  # 精确值

# 普通 MC
rng = np.random.default_rng(42)
u = rng.uniform(0, 1, 1000)
mc_est = np.exp(u).mean()

# 对偶变量法
anti_est = antithetic_mc(np.exp, 1000, seed=42)

print(f'精确值: {true_val:.4f}')
print(f'普通MC: {mc_est:.4f}, 误差: {abs(mc_est - true_val):.4f}')
print(f'对偶法: {anti_est:.4f}, 误差: {abs(anti_est - true_val):.4f}')
精确值: 1.7183
普通MC: 1.7298, 误差: 0.0115
对偶法: 1.7151, 误差: 0.0032

可视化效果

ch06_antithetic.png

对偶变量法的效果:

  • 左图:误差分布对比。对偶变量法(红色)的分布明显比普通 MC(蓝色)更窄,方差缩减约 97%

  • 右图:收敛过程。对偶变量法的误差波动幅度远小于普通 MC

适用条件:当 ff 是单调函数时效果最佳。对于非单调函数,可能反而增加方差。

6.3 控制变量法(Control Variates)

核心思想

控制变量法利用一个已知期望且与目标函数相关的辅助变量来降低方差。

设要估计 θ=E[f(X)],已知 g(X) 满足 E[g(X)]=μg(已知),则构造新估计量:

θ^cv=1Ni=1N[f(Xi)+c(g(Xi)μg)]\hat{\theta}_{\text{cv}} = \frac{1}{N}\sum_{i=1}^N \left[f(X_i) + c\left(g(X_i) - \mu_g\right)\right]

其中cc 是控制系数。最优系数为:

c=Cov(f(X),g(X))Var(g(X))c^* = -\frac{\text{Cov}(f(X), g(X))}{\text{Var}(g(X))}

此时方差缩减比例为:

Var(θ^cv)Var(θ^MC)=1ρ2\frac{\text{Var}(\hat{\theta}_{\text{cv}})}{\text{Var}(\hat{\theta}_{\text{MC}})} = 1 - \rho^2

其中 ρf(X)g(X) 的相关系数。相关系数越大,方差缩减越显著

代码实现

def control_variate_mc(f, g, mu_g, N, seed=None):
    """
    控制变量法蒙特卡洛估计

    Parameters
    ----------
    f : callable
        目标函数
    g : callable
        控制变量函数(已知期望 mu_g)
    mu_g : float
        E[g(X)] 的精确值
    N : int
        样本量
    seed : int
        随机种子

    Returns
    -------
    estimate : float
        蒙特卡洛估计值
    c_star : float
        最优控制系数
    """
    rng = np.random.default_rng(seed)
    x = rng.standard_normal(N)

    f_vals = f(x)
    g_vals = g(x)

    # 估计最优控制系数
    cov_fg = np.cov(f_vals, g_vals)[0, 1]
    var_g = np.var(g_vals)
    c_star = -cov_fg / var_g

    # 控制变量估计
    estimate = np.mean(f_vals + c_star * (g_vals - mu_g))

    return estimate, c_star

# 示例:估计 E[exp(sin(X))], X ~ N(0,1)
f = lambda x: np.exp(np.sin(x))
g = lambda x: np.sin(x)  # E[sin(X)] = 0
mu_g = 0.0

est, c_star = control_variate_mc(f, g, mu_g, N=2000, seed=42)
print(f'估计值: {est:.6f}')
print(f'最优控制系数: {c_star:.4f}')

可视化效果

ch06_control_variate.png
  • 左图:误差分布对比。控制变量法(绿色)的分布明显更窄,方差缩减约 93%

  • 右图:收敛过程。控制变量法的误差波动远小于普通 MC,更快稳定在零附近

如何选择控制变量

场景

推荐控制变量

说明

XN(0,1)

g(x)=x,x2,x3g(x) = x, x^2, x^3

多项式,与很多函数相关

XU(0,1)

g(x)=x,x2g(x) = x, x^2

简单多项式

积分估计

被积函数的近似

用 Taylor 展开或插值

金融期权定价

Black-Scholes 解析解

用近似模型的精确解

关键原则:控制变量应与目标函数高度相关,且其期望已知。

6.4 重要性采样(Importance Sampling)

核心思想

重要性采样通过改变抽样分布来降低方差。核心公式:

Ep[f(X)]=f(x)p(x)dx=f(x)p(x)q(x)q(x)dx=Eq[f(X)p(X)q(X)]E_p[f(X)] = \int f(x)p(x)\,dx = \int f(x)\frac{p(x)}{q(x)}q(x)\,dx = E_q\left[f(X)\frac{p(X)}{q(X)}\right]

其中pp 是目标分布,qq 是提议分布(importance distribution)。估计量为:

θ^IS=1Ni=1Nf(Xi)p(Xi)q(Xi),Xiq\hat{\theta}_{\text{IS}} = \frac{1}{N}\sum_{i=1}^N f(X_i)\frac{p(X_i)}{q(X_i)}, \quad X_i \sim q

关键q 应在 f(x)p(x) 较大的区域分配更多概率质量。

稀有事件估计

重要性采样最典型的应用是稀有事件概率估计

from scipy import stats
import numpy as np

# 估计 P(X > 2), X ~ N(0, 1)
true_prob = 1 - stats.norm.cdf(2)  # ≈ 0.02275

def mc_rare_event(N, seed=None):
    """普通蒙特卡洛估计稀有事件概率"""
    rng = np.random.default_rng(seed)
    x = rng.standard_normal(N)
    return np.mean(x > 2)

def is_rare_event(N, seed=None):
    """重要性采样估计稀有事件概率"""
    rng = np.random.default_rng(seed)
    # 用 N(2, 1.5) 作为提议分布
    x = rng.normal(2, 1.5, N)
    # 重要性权重:p(x)/q(x)
    log_w = -0.5*x**2 - (-0.5*((x-2)/1.5)**2 + np.log(1.5))
    weights = np.exp(log_w)
    indicators = (x > 2).astype(float)
    return np.mean(indicators * weights)

# 对比
mc_est = mc_rare_event(5000, seed=42)
is_est = is_rare_event(5000, seed=42)

print(f'精确值: {true_prob:.6f}')
print(f'普通MC: {mc_est:.6f}, 误差: {abs(mc_est - true_prob):.6f}')
print(f'重要性采样: {is_est:.6f}, 误差: {abs(is_est - true_prob):.6f}')
精确值: 0.022750
普通MC: 0.023000, 误差: 0.000250
重要性采样: 0.022748, 误差: 0.000002

可视化效果

ch06_importance_sampling.png
  • 左图:目标分布 N(0,1)(蓝色)和提议分布 N(2,1.5)(红色)。提议分布将更多概率质量分配到了关键区域 x>2(浅蓝色填充区域)

  • 右图:估计值分布对比。普通 MC(蓝色)的分布很宽(很多次运行得到 0),重要性采样(红色)的分布很窄且集中在精确值附近,方差缩减约 98%

如何选择提议分布

场景

提议分布选择

稀有事件 P(X>c)

将分布中心移到 c 附近

积分 f(x)p(x)dx\int f(x)p(x)dx

q(x)f(x)p(x)q(x) \propto |f(x)|p(x)(理想但通常不可行)

金融尾部风险

指数倾斜(exponential tilting)

注意事项

  • 提议分布必须有足够的支撑覆盖(q(x)=0⇒p(x)=0

  • 权重 p(x)/q(x) 的方差不能太大,否则估计不稳定

  • 最优提议分布:q(x)f(x)p(x)q^*(x) \propto |f(x)|p(x)

6.5 分层采样(Stratified Sampling)

核心思想

将样本空间划分为互不重叠的层(strata),在每层内独立抽样,然后加权平均。

θ^strat=j=1Kwj1nji=1njf(Xij)\hat{\theta}_{\text{strat}} = \sum_{j=1}^K w_j \cdot \frac{1}{n_j}\sum_{i=1}^{n_j} f(X_{ij})

其中 wj 是第 j 层的权重(通常为该层的概率质量)。

代码实现

def stratified_mc(f, strata_bounds, n_per_stratum, seed=None):
    """
    分层采样蒙特卡洛估计

    Parameters
    ----------
    f : callable
        目标函数
    strata_bounds : list of tuple
        每层的边界 [(lo1, hi1), (lo2, hi2), ...]
    n_per_stratum : int or list
        每层的样本量
    seed : int
        随机种子

    Returns
    -------
    float
        蒙特卡洛估计值
    """
    rng = np.random.default_rng(seed)

    if isinstance(n_per_stratum, int):
        n_per_stratum = [n_per_stratum] * len(strata_bounds)

    estimates = []
    weights = []

    for (lo, hi), n in zip(strata_bounds, n_per_stratum):
        # 在层内均匀采样
        x = rng.uniform(lo, hi, n)
        estimates.append(f(x).mean())
        # 权重 = 层的宽度(假设总体是均匀分布)
        weights.append(hi - lo)

    total_width = sum(weights)
    weights = [w / total_width for w in weights]

    return sum(w * est for w, est in zip(weights, estimates))

# 示例:估计 E[e^X], X ~ U(0, 1)
f = lambda x: np.exp(x)
true_val = np.e - 1

# 分5层
strata = [(0, 0.2), (0.2, 0.4), (0.4, 0.6), (0.6, 0.8), (0.8, 1.0)]
est = stratified_mc(f, strata, n_per_stratum=200, seed=42)

print(f'精确值: {true_val:.4f}')
print(f'分层采样: {est:.4f}, 误差: {abs(est - true_val):.4f}')
精确值: 1.7183
分层采样: 1.7183, 误差: 0.0000

分层策略

策略

样本分配

适用场景

比例分配

njwj

简单,各层方差相近

最优分配

njwjσj

方差大的层多抽样

Neman 分配

njwjσj/cjn_j \propto w_j \sigma_j / \sqrt{c_j}​​

考虑各层抽样成本

6.6 方法对比总结

方法

方差缩减幅度

实现难度

适用场景

对偶变量法

中-高(30%-97%)

简单

单调函数,U(0,1) 采样

控制变量法

高(50%-99%)

能找到相关控制变量

重要性采样

极高(80%-99%)

中-高

稀有事件、尾部概率

分层采样

中-高(40%-90%)

可划分子区域的问题

组合使用

这些方法可以组合使用以获得更大的方差缩减:

# 对偶变量 + 控制变量
def combined_estimator(N, seed=None):
    rng = np.random.default_rng(seed)
    u = rng.uniform(0, 0.5, N//4)
    samples = np.concatenate([u, 0.5-u, 1-u, 0.5+u])  # 对偶
    # 然后在此基础之上应用控制变量
    ...

6.7 本节小结

  • 方差缩减技术用更聪明的抽样策略替代"盲目增加样本量"

  • 对偶变量法:利用负相关样本,对单调函数效果最佳

  • 控制变量法:利用已知期望的相关变量,相关系数决定效果

  • 重要性采样:改变抽样分布,稀有事件估计的利器

  • 分层采样:划分样本空间,在各层内独立抽样

  • 这些方法可以组合使用,方差缩减效果叠加

  • 选择方法的关键:问题的具体结构和可用信息

6.8 实用代码速查

方法

关键代码

对偶变量

u_dual = 1 - u; u_all = np.concatenate([u, u_dual])

最优控制系数

c_star = -np.cov(f_vals, g_vals)[0,1] / np.var(g_vals)

控制变量估计

np.mean(f_vals + c_star * (g_vals - mu_g))

重要性权重

weights = p(x) / q(x)(用对数避免溢出)

对数权重

log_w = log_p(x) - log_q(x); weights = np.exp(log_w)

分层采样

每层独立采样,加权平均

方差缩减比例

(1 - var_reduced/var_original) * 100

第7章 MCMC 方法

7.1 什么是 MCMC

MCMC(Markov Chain Monte Carlo,马尔可夫链蒙特卡洛) 是一类通过构造马尔可夫链来从复杂分布中抽样的方法。

为什么需要 MCMC?

在前面的章节中,我们总是能从已知分布(正态、均匀、指数等)中直接抽样。但在很多实际问题中:

  • 后验分布形式复杂:贝叶斯推断中,后验 p(θdata)p(dataθ)p(θ)p(\theta|data) \propto p(data|\theta)p(\theta)通常没有标准形式

  • 高维空间:维度太高,网格法不可行

  • 归一化常数未知:只知道密度函数的"形状",不知道归一化常数

MCMC 的核心优势:只需要知道目标分布的未归一化密度函数,就能从中抽样。

马尔可夫链基础

马尔可夫链是一个随机过程,满足无记忆性

P(Xt+1=xXt=xt,Xt1=xt1,)=P(Xt+1=xXt=xt)P(X_{t+1} = x | X_t = x_t, X_{t-1} = x_{t-1}, \ldots) = P(X_{t+1} = x | X_t = x_t)

即下一状态只依赖当前状态,与历史无关。

MCMC 的关键定理:如果构造的马尔可夫链以目标分布 π 为平稳分布,则链运行足够久后,样本近似来自 π

7.2 Metropolis-Hastings 算法

算法步骤

Metropolis-Hastings(MH)是最通用的 MCMC 算法:

  1. 初始化 x(0)x^{(0)}

  2. 对于 t=1,2,…

    • 从提议分布 q(xx(t1))q(x'|x^{(t-1)}) 生成候选 xx'

    • 计算接受概率 α=min(1,π(x)q(x(t1)x)π(x(t1))q(xx(t1)))\alpha = \min\left(1, \frac{\pi(x')q(x^{(t-1)}|x')}{\pi(x^{(t-1)})q(x'|x^{(t-1)})}\right)

    • 以概率 α 接受:x(t)=xx^{(t)} = x',否则 x(t)=x(t1)x^{(t)} = x^{(t-1)}

对称提议的简化

当提议分布对称(如正态分布 q(xx)=q(xx)q(x'|x) = q(x|x'))时,接受概率简化为:

α=min(1,π(x)π(x(t1)))\alpha = \min\left(1, \frac{\pi(x')}{\pi(x^{(t-1)})}\right)

代码实现

import numpy as np

def metropolis_hastings(log_target, proposal_std, n_samples, x0, burnin=1000, seed=None):
    """
    Metropolis-Hastings 算法

    Parameters
    ----------
    log_target : callable
        对数目标密度函数(可以未归一化)
    proposal_std : float
        正态提议分布的标准差
    n_samples : int
        总迭代次数
    x0 : float
        初始值
    burnin : int
        烧录期长度(前 burnin 个样本丢弃)
    seed : int
        随机种子

    Returns
    -------
    samples : ndarray
        后验样本(去除 burnin)
    acceptance_rate : float
        接受率
    trace : ndarray
        完整的马尔可夫链轨迹
    """
    rng = np.random.default_rng(seed)
    x = x0
    log_p_x = log_target(x)
    samples = []
    trace = [x]
    n_accepted = 0

    for i in range(n_samples):
        # 提议:从当前值附近正态扰动
        x_proposal = x + rng.normal(0, proposal_std)
        log_p_proposal = log_target(x_proposal)

        # 接受概率(对称提议,简化形式)
        log_alpha = min(0, log_p_proposal - log_p_x)
        if np.log(rng.random()) < log_alpha:
            x = x_proposal
            log_p_x = log_p_proposal
            n_accepted += 1

        trace.append(x)
        if i >= burnin:
            samples.append(x)

    return np.array(samples), n_accepted / n_samples, np.array(trace)

实例:贝叶斯均值推断

假设数据 yiN(θ,1),先验 θN(0,1),观测数据 data = [2, 3, 2.5, 3.5, 2.8]

后验分布(未归一化):

logp(θdata)12θ212i=1n(yiθ)2\log p(\theta|data) \propto -\frac{1}{2}\theta^2 - \frac{1}{2}\sum_{i=1}^n(y_i - \theta)^2
data = np.array([2, 3, 2.5, 3.5, 2.8])
n = len(data)
data_mean = data.mean()

def log_posterior(theta):
    """对数后验(未归一化)"""
    log_prior = -0.5 * theta**2              # N(0,1) 先验
    log_likelihood = -0.5 * np.sum((data - theta)**2)  # N(θ,1) 似然
    return log_prior + log_likelihood

# 运行 MCMC
samples, accept_rate, trace = metropolis_hastings(
    log_posterior, proposal_std=0.5, n_samples=5000, x0=0, burnin=1000, seed=42)

# 精确后验:N(μ_n, σ_n²)
posterior_mean = n * data_mean / (n + 1)
posterior_std = np.sqrt(1 / (n + 1))

print(f'接受率: {accept_rate:.1%}')
print(f'MCMC 均值: {samples.mean():.3f}')
print(f'精确后验均值: {posterior_mean:.3f}')
接受率: 66.0%
MCMC 均值: 2.294
精确后验均值: 2.300

可视化诊断

ch07_metropolis_hastings.png

三张子图展示了 MH 算法的运行情况:

  • 左图(链轨迹):蓝色线是马尔可夫链的演化过程。红色虚线标记 burnin 结束(第 1000 次迭代)。链在 burnin 后围绕后验均值 2.300 波动。

  • 中图(后验分布):蓝色直方图是 MCMC 样本的分布,黑色曲线是精确后验 N(2.30,0.172)N(2.30, 0.17^2)。两者高度吻合。

  • 右图(自相关):自相关函数(ACF)在滞后 15 步后衰减到接近零,说明样本间的相关性较弱,采样效率尚可。

关键参数调优

参数

影响

经验法则

提议步长

太大→接受率低;太小→探索慢

调整使接受率 ≈ 23%-50%

Burnin 长度

太短→初始值影响未消除

观察链轨迹,确保稳定

总迭代次数

影响估计精度

通过自相关判断是否需要更多样本

初始值

影响 burnin 长度

可以用 MLE 或先验均值

7.3 Gibbs 采样

核心思想

Gibbs 采样是 MH 的特例,适用于可以从条件分布直接抽样的场景。

对于 d 维目标分布 π(x1,x2,…,xd),Gibbs 采样每次只更新一个分量:

  1. x1(t)π(x1x2(t1),,xd(t1))x_1^{(t)} \sim \pi(x_1 | x_2^{(t-1)}, \ldots, x_d^{(t-1)})

  2. x2(t)π(x2x1(t),x3(t1),,xd(t1))x_2^{(t)} \sim \pi(x_2 | x_1^{(t)}, x_3^{(t-1)}, \ldots, x_d^{(t-1)})

  3. ...

  4. xd(t)π(xdx1(t),,xd1(t))x_d^{(t)} \sim \pi(x_d | x_1^{(t)}, \ldots, x_{d-1}^{(t)})

优势:每次更新的接受率为 100%(因为直接从条件分布抽样),无需拒绝步骤。

实例:二维正态分布

(X,Y)∼N(μ,Σ),其中 μ=[1,2]Σ=(10.80.81)\Sigma = \begin{pmatrix} 1 & 0.8 \\ 0.8 & 1 \end{pmatrix}

条件分布:

  • XY=yN(μ1+ρσ1σ2(yμ2),σ12(1ρ2))X|Y=y \sim N(\mu_1 + \rho\frac{\sigma_1}{\sigma_2}(y-\mu_2), \sigma_1^2(1-\rho^2))

  • YX=xN(μ2+ρσ2σ1(xμ1),σ22(1ρ2))Y|X=x \sim N(\mu_2 + \rho\frac{\sigma_2}{\sigma_1}(x-\mu_1), \sigma_2^2(1-\rho^2))

def gibbs_sampling_2d(mu, cov, n_samples, burnin=1000, seed=None):
    """Gibbs 采样:二维正态分布"""
    rng = np.random.default_rng(seed)
    mu1, mu2 = mu
    sigma1 = np.sqrt(cov[0, 0])
    sigma2 = np.sqrt(cov[1, 1])
    rho = cov[0, 1] / (sigma1 * sigma2)

    x, y = 0.0, 0.0
    samples_x, samples_y = [], []

    for i in range(n_samples):
        # 从条件分布抽样
        cond_mean_x = mu1 + rho * sigma1/sigma2 * (y - mu2)
        cond_std_x = sigma1 * np.sqrt(1 - rho**2)
        x = rng.normal(cond_mean_x, cond_std_x)

        cond_mean_y = mu2 + rho * sigma2/sigma1 * (x - mu1)
        cond_std_y = sigma2 * np.sqrt(1 - rho**2)
        y = rng.normal(cond_mean_y, cond_std_y)

        if i >= burnin:
            samples_x.append(x)
            samples_y.append(y)

    return np.array(samples_x), np.array(samples_y)

samples_x, samples_y = gibbs_sampling_2d(
    mu=[1, 2], cov=np.array([[1, 0.8], [0.8, 1]]),
    n_samples=5000, burnin=1000, seed=42)

print(f'X 样本均值: {samples_x.mean():.3f}(理论: 1.000)')
print(f'Y 样本均值: {samples_y.mean():.3f}(理论: 2.000)')
print(f'样本相关系数: {np.corrcoef(samples_x, samples_y)[0,1]:.3f}(理论: 0.800)')
X 样本均值: 1.003(理论: 1.000)
Y 样本均值: 2.001(理论: 2.000)
样本相关系数: 0.801(理论: 0.800)

可视化

ch07_gibbs_sampling.png
  • 左图:Gibbs 样本的散点图(蓝色点),红色虚线椭圆是理论 95% 置信椭圆。样本分布与理论分布吻合。

  • 中图:X 的边缘分布,蓝色直方图是 Gibbs 样本,黑色曲线是理论 N(1,1)

  • 右图:Y 的边缘分布,红色直方图是 Gibbs 样本,黑色曲线是理论 N(2,1)

7.4 MCMC 在贝叶斯推断中的应用

贝叶斯线性回归

考虑线性回归模型:y=β0+β1x+εy = \beta_0 + \beta_1 x + \varepsilonεN(0,σ2)\varepsilon \sim N(0, \sigma^2)

贝叶斯框架下,给参数赋予先验分布,然后用 MCMC 从后验中抽样。

def bayesian_linear_regression_mh(x, y, n_samples=10000, burnin=2000, seed=None):
    """贝叶斯线性回归的 MH 采样"""
    rng = np.random.default_rng(seed)
    beta0, beta1 = 0.0, 0.0
    sigma = 1.5  # 假设已知
    samples = []

    for i in range(n_samples):
        # 提议
        beta0_prop = beta0 + rng.normal(0, 0.3)
        beta1_prop = beta1 + rng.normal(0, 0.1)

        # 对数后验
        log_p_current = -0.5*(beta0/10)**2 - 0.5*(beta1/10)**2 \
                        - 0.5*np.sum((y - beta0 - beta1*x)**2)/sigma**2
        log_p_proposal = -0.5*(beta0_prop/10)**2 - 0.5*(beta1_prop/10)**2 \
                         - 0.5*np.sum((y - beta0_prop - beta1_prop*x)**2)/sigma**2

        if np.log(rng.random()) < min(0, log_p_proposal - log_p_current):
            beta0, beta1 = beta0_prop, beta1_prop

        if i >= burnin:
            samples.append([beta0, beta1])

    return np.array(samples)

可视化

ch07_bayesian_regression.png
  • 左图:蓝色点是观测数据,红色细线是从后验中抽取的回归线(每条线代表一组可能的参数),黑色虚线是真实关系。可以看到后验回归线形成了一个"置信带"。

  • 中图β0(截距)的后验分布。红色虚线是真实值 2.0,黑色虚线是 OLS 估计 2.482,MCMC 后验均值 2.485。

  • 右图β1(斜率)的后验分布。红色虚线是真实值 1.5,黑色虚线是 OLS 估计 1.347,MCMC 后验均值 1.345。

贝叶斯 vs 频率派

  • 频率派(OLS)给出点估计 + 标准误差

  • 贝叶斯给出完整的后验分布,可以直接回答"P(β1 > 1) = ?"这类概率问题

7.5 MCMC 诊断与注意事项

收敛诊断

诊断方法

说明

链轨迹图

观察是否稳定,有无趋势

自相关图

自相关应快速衰减到零

有效样本量

Neff=N/(1+2ρk)N_{eff} = N / (1 + 2\sum \rho_k),越大越好

多链诊断

从不同初值启动多条链,比较结果

Gelman-Rubin

R^\hat{R} 统计量,接近 1 表示收敛

常见问题

问题

症状

解决方法

接受率太低

链很少移动

减小提议步长

接受率太高

探索太慢

增大提议步长

自相关强

样本有效量低

增加 thinning 间隔

多峰分布

链困在局部模式

用更宽的提议分布

高维问题

收敛极慢

改用 HMC 或 NUTS

实际建议

# 推荐的 MCMC 工作流程
def run_mcmc_workflow(log_target, n_chains=4, n_samples=10000):
    """多链 MCMC 工作流"""
    all_samples = []
    for chain_id in range(n_chains):
        # 从不同初始值启动
        x0 = np.random.randn()
        samples, accept_rate, trace = metropolis_hastings(
            log_target, proposal_std=0.5,
            n_samples=n_samples, x0=x0, burnin=n_samples//4,
            seed=chain_id
        )
        all_samples.append(samples)
        print(f'链 {chain_id}: 接受率 = {accept_rate:.1%}, '
              f'均值 = {samples.mean():.4f}, 标准差 = {samples.std():.4f}')

    # 合并所有链的样本
    return np.concatenate(all_samples)

7.6 MH vs Gibbs 对比

特性

Metropolis-Hastings

Gibbs 采样

适用条件

任何目标分布(只需未归一化密度)

需要从条件分布抽样

接受率

< 100%(有拒绝步骤)

100%(无拒绝)

实现难度

简单

需要推导条件分布

高维表现

需要仔细调参

条件独立时分量更新高效

典型应用

一般贝叶斯推断

层次模型、隐变量模型

7.7 本节小结

  • MCMC 通过构造马尔可夫链从复杂分布中抽样

  • MH 算法是最通用的 MCMC 方法,只需要未归一化密度函数

  • Gibbs 采样是 MH 的特例,接受率 100%,适合有解析条件分布的场景

  • MCMC 是贝叶斯推断的核心工具

  • 必须进行收敛诊断:链轨迹、自相关、多链比较

  • 实际应用中推荐使用现成的 MCMC 库(如 PyMC、Stan)

7.8 实用代码速查

功能

代码

MH 接受概率

min(0, log_p_proposal - log_p_current)

对数接受判断

np.log(rng.random()) < log_alpha

对称提议简化

alpha = min(1, pi(x')/pi(x))

Burnin 处理

samples = trace[burnin:]

有效样本量

N / (1 + 2*sum(acf[1:]))

自相关计算

np.correlate(x_centered, x_centered, 'full')

多链合并

np.concatenate([chain1, chain2, ...])

薄化采样

samples[::thin]

第8章 进阶应用

8.1 Bootstrap 重采样

核心思想

Bootstrap(自助法)是一种从单个样本中估计统计量不确定性的蒙特卡洛方法。基本思路:

把样本当作总体,从中有放回地重复抽样,模拟抽样分布。

算法步骤

  1. 从原始样本中有放回地抽取 n 个观测,得到一个 Bootstrap 样本

  2. 计算该样本的统计量(均值、中位数、标准差等)

  3. 重复 B 次(通常 B=1000∼10000

  4. B 个统计量的分布即为Bootstrap 抽样分布

置信区间估计

Bootstrap 提供了多种置信区间构造方法:

方法

公式

特点

百分位数法

[θ^α/2,θ^1α/2][\hat{\theta}^*_{\alpha/2}, \hat{\theta}^*_{1-\alpha/2}]

最简单,最常用

偏差校正法 (BCa)

调整分位数位置

修正偏差和偏度

正态近似法

θ^±zα/2SEboot\hat{\theta} \pm z_{\alpha/2} \cdot \text{SE}_{boot}

假设抽样分布近似正态

代码实现

import numpy as np

def bootstrap(data, statistic, B=5000, alpha=0.05, seed=None):
    """
    Bootstrap 置信区间估计

    Parameters
    ----------
    data : ndarray
        原始样本
    statistic : callable
        统计量函数,接受数组返回标量
    B : int
        Bootstrap 重复次数
    alpha : float
        显著性水平(默认 0.05,即 95% CI)
    seed : int
        随机种子

    Returns
    -------
    estimate : float
        原始样本的统计量值
    ci : tuple
        (下限, 上限) 置信区间
    boot_stats : ndarray
        B 个 Bootstrap 统计量值
    """
    rng = np.random.default_rng(seed)
    n = len(data)
    boot_stats = np.empty(B)

    for i in range(B):
        # 有放回抽样
        resample = rng.choice(data, size=n, replace=True)
        boot_stats[i] = statistic(resample)

    estimate = statistic(data)
    ci = (np.percentile(boot_stats, 100*alpha/2),
          np.percentile(boot_stats, 100*(1-alpha/2)))

    return estimate, ci, boot_stats

实例:偏态分布的均值置信区间

从对数正态分布中抽取 30 个样本(偏态分布),估计均值的 95% 置信区间。

np.random.seed(42)

# 总体:对数正态分布
true_mean = np.exp(0.5)  # ≈ 1.6487
sample = np.random.lognormal(0, 1, 30)

# Bootstrap
estimate, ci, boot_means = bootstrap(sample, np.mean, B=5000, seed=42)

# 正态近似 CI(作为对比)
se_normal = sample.std(ddof=1) / np.sqrt(len(sample))
ci_normal = (estimate - 1.96*se_normal, estimate + 1.96*se_normal)

print(f'样本均值: {estimate:.3f}')
print(f'真实均值: {true_mean:.3f}')
print(f'Bootstrap CI: [{ci[0]:.3f}, {ci[1]:.3f}]')
print(f'正态近似 CI: [{ci_normal[0]:.3f}, {ci_normal[1]:.3f}]')
样本均值: 1.226
真实均值: 1.649
Bootstrap CI: [0.843, 1.711]
正态近似 CI: [0.777, 1.675]

可视化

ch08_bootstrap_ci-ofSF.png
  • 左图:原始样本(n=30)来自对数正态分布(黑色曲线),明显右偏。样本均值(黑色实线)低估了真实均值(红色虚线)。

  • 右图:Bootstrap 均值分布(蓝色直方图)也是右偏的。Bootstrap CI(绿色虚线)比正态近似 CI(紫色虚线)更宽,且不对称——这反映了对偏态分布的自适应。

关键洞察:正态近似 CI 假设抽样分布对称,在偏态场景下可能不准确。Bootstrap 不需要这个假设,因此更可靠。

多种统计量

ch08_bootstrap_statistics.png

四张子图展示了 Bootstrap 对四种统计量的抽样分布估计(指数分布总体,n=50,B=5000):

统计量

Bootstrap 均值

真实值

95% CI

均值

1.690

2.000

[1.241, 2.212]

中位数

1.118

1.386

[0.691, 1.635]

标准差

1.732

2.000

[1.234, 2.190]

偏度

1.574

2.000

[0.996, 2.262]

观察

  • 均值的 Bootstrap 分布近似对称,置信区间质量最好

  • 中位数的分布有明显多峰(离散抽样导致),置信区间仍合理

  • 标准差和偏度的分布右偏,Bootstrap CI 自动适应

Bootstrap 的适用场景

场景

Bootstrap 优势

小样本

不需要正态假设

复杂统计量

无需推导抽样分布公式

偏态/重尾分布

自动适应非对称性

相关数据

可用 Block Bootstrap

回归系数

可用残差 Bootstrap

注意事项

  • 样本量过小(n < 10)时 Bootstrap 不可靠

  • Bootstrap 估计的是抽样分布,不是总体分布

  • 对于极值统计量(最大值、最小值),Bootstrap 效果差

  • 时间序列数据需用 Block Bootstrap(保持时间相关性)

8.2 综合实战:库存优化

问题描述

某商品的日需求服从 N(100,202)N(100, 20^2),订货提前期 5 天。每次订货固定成本 500 元,单位持有成本 2 元/天,单位缺货成本 10 元。求最优再订货点 s

蒙特卡洛求解

import numpy as np

def simulate_inventory_cost(s, Q, demand_mean=100, demand_std=20,
                            lead_time=5, order_cost=500,
                            holding_cost=2, stockout_cost=10,
                            n_days=365, seed=None):
    """
    仿真库存系统的年总成本

    Parameters
    ----------
    s : int
        再订货点
    Q : int
        订货量
    seed : int
        随机种子(用于多次独立运行)

    Returns
    -------
    float
        年总成本
    """
    rng = np.random.default_rng(seed)
    stock = Q
    pending = 0
    arrival_day = float('inf')
    total_cost = 0

    for day in range(n_days):
        # 订单到达
        if day >= arrival_day:
            stock += pending
            pending = 0
            arrival_day = float('inf')

        # 需求
        demand = max(0, rng.normal(demand_mean, demand_std))

        # 满足需求
        sold = min(stock, demand)
        shortage = demand - sold
        stock -= sold

        # 成本累积
        total_cost += holding_cost * stock + stockout_cost * shortage

        # 订货决策
        if stock <= s and pending == 0:
            pending = Q
            arrival_day = day + lead_time
            total_cost += order_cost

    return total_cost

搜索最优再订货点

s_values = range(400, 700, 20)
Q = 300
n_sims = 50

results = {}
for s in s_values:
    costs = [simulate_inventory_cost(s, Q, seed=i) for i in range(n_sims)]
    results[s] = {'mean': np.mean(costs), 'std': np.std(costs)}

best_s = min(results, key=lambda s: results[s]['mean'])
print(f'最优再订货点: s* = {best_s}')
print(f'平均年成本: {results[best_s]["mean"]:.0f} ± {results[best_s]["std"]:.0f} 元')
最优再订货点: s* = 400
平均年成本: 228015 ± 3250 元

可视化

ch08_inventory_optimization.png
  • 左图:不同再订货点 s 对应的平均年总成本。蓝色点是均值,浅蓝色区域是 ±1 标准差。最优值 s=400s^* = 400(红色虚线)。

  • 右图:最优策略的成本分布。5% 和 95% 分位数展示了成本的不确定性范围。

决策建议

再订货点

平均成本

5% 分位

95% 分位

风险偏好

400

228,015

223,070

233,452

风险中性(最优均值)

500

228,018

224,200

232,800

更保守(减少缺货风险)

350

229,500

221,000

238,000

更激进(降低成本波动上限)

蒙特卡洛的价值:不仅给出最优决策,还量化了决策的不确定性,帮助管理者根据风险偏好做选择。

8.3 蒙特卡洛方法总结

方法选择决策树

问题类型?
├── 计算积分/期望
│   ├── 低维 (d ≤ 3) → 数值积分 (Simpson, Gauss)
│   └── 高维 (d > 3) → 蒙特卡洛积分
│       └── 方差大? → 方差缩减技术
├── 优化
│   ├── 连续、可导 → 梯度方法
│   ├── 多峰、黑箱 → 模拟退火
│   └── 组合优化 → 模拟退火 / 遗传算法
├── 随机系统分析
│   ├── 排队/库存 → 离散事件仿真
│   └── 金融/风险 → 蒙特卡洛 + 方差缩减
├── 贝叶斯推断
│   ├── 共轭先验 → 解析解
│   ├── 低维 → MH 算法
│   ├── 条件分布已知 → Gibbs 采样
│   └── 高维复杂 → HMC / NUTS (PyMC/Stan)
└── 统计推断
    ├── 大样本、正态 → 经典方法
    └── 小样本、非正态 → Bootstrap

常见陷阱

陷阱

症状

解决方法

随机种子未固定

结果不可复现

始终设置 seed

样本量不足

误差太大

增加 N,检查收敛

忽略 burnin

初始值偏差

丢弃前 10%-30%

不报告误差

结果无意义

始终报告 SE 或 CI

方差太大

计算浪费

使用方差缩减技术

提议分布不当

MCMC 不收敛

调整步长,检查接受率

8.4 本节小结

  • Bootstrap 是从单个样本估计统计量不确定性的强大工具

  • 百分位数法是最简单实用的 Bootstrap 置信区间方法

  • 蒙特卡洛方法贯穿数学建模的各个环节:积分、优化、仿真、推断

  • 选择方法时要考虑问题维度、光滑性、可用信息

  • 始终报告误差估计,固定随机种子保证可复现

  • 实际数模竞赛中,蒙特卡洛常作为基准方法验证工具

8.5 实用代码速查

随机数生成

功能

代码

创建 rng

rng = np.random.default_rng(seed)

均匀分布

rng.uniform(a, b, N)

正态分布

rng.normal(mu, sigma, N)

指数分布

rng.exponential(scale, N)

整数抽样

rng.integers(low, high, N)

有放回抽样

rng.choice(arr, size=N, replace=True)

蒙特卡洛估计

功能

代码

期望估计

f(samples).mean()

标准误差

f(samples).std(ddof=1) / np.sqrt(N)

95% CI

mean ± 1.96 * se

蒙特卡洛积分

(b-a) * f(rng.uniform(a,b,N)).mean()

MCMC

功能

代码

MH 接受

np.log(rng.random()) < log_alpha

自相关

np.correlate(x-x.mean(), x-x.mean(), 'full')

薄化

samples[::thin]

多链合并

np.concatenate(chains)

Bootstrap

功能

代码

有放回抽样

rng.choice(data, size=n, replace=True)

Bootstrap CI

np.percentile(boot_stats, [2.5, 97.5])

Bootstrap SE

boot_stats.std(ddof=1)

附录:完整代码获取

本教程所有代码均可通过以下链接下载:

Monte_Carlo_Method.zip

评论