4、随机数生成:伪随机数生成器、种子设置、常见分布抽样方法(逆变换法、Box-Muller等)
蒙特卡洛模拟的本质,是用大量随机样本来逼近复杂系统的真实概率分布。因此,随机数的质量直接决定了模拟结果的可靠性与稳定性。如果随机数生成器存在周期性过短、统计偏差或种子设置不当等问题,即使模型构建得再精巧,输出结果也可能产生系统性偏差。本章将系统讲解伪随机数生成器(PRNG)的工作原理、种子设置的最佳实践,以及从均匀分布出发构造常见概率分布的抽样方法,帮助你在风险管理中建立扎实的随机数基础。
4.1 伪随机数生成器(PRNG)的核心原理
真正的随机数来源于物理过程(如放射性衰变、热噪声),但在计算机模拟中,我们使用的是伪随机数生成器——一种通过确定性算法产生看似随机序列的程序。PRNG 并非真正随机,而是从一个初始状态(种子)出发,通过数学递推公式生成序列。只要种子相同,生成的序列就完全一致,这为实验的可复现性提供了基础。
在风险管理中,最常用的 PRNG 是 梅森旋转算法(MT19937),其周期长达 219937-1,远超过任何实际模拟所需的样本量。Python 的 random 模块和 NumPy 的默认生成器均基于此算法。近年来,PCG(Permuted Congruential Generator) 和 xoshiro256++ 等新一代生成器因其更快的速度和更好的统计特性,也逐渐被广泛采用。
| 生成器 | 周期 | 速度 | 统计质量 | 适用场景 |
|---|---|---|---|---|
| MT19937 | 219937-1 | 中等 | 优秀(通过 TestU01) | 通用模拟、金融建模 |
| PCG32 | 264 | 快 | 优秀 | 大规模并行模拟 |
| xoshiro256++ | 2256-1 | 极快 | 良好 | 高性能计算、GPU模拟 |
| 线性同余(LCG) | 231-1 | 快 | 较差(低维偏差) | 不推荐用于风险管理 |
RAND() 或 C 语言旧版 rand() 进行蒙特卡洛模拟。这些生成器的周期短(通常不超过 231),且在高维抽样时会出现明显的相关性,可能导致 VaR 估计偏差高达 10%-20%。
4.2 种子设置:可复现性与独立性的平衡
种子(Seed)是 PRNG 的初始状态。在风险管理实践中,种子设置需要根据场景灵活选择:
- 固定种子(如 seed=42):用于模型验证、回归测试和教学演示。固定种子确保每次运行结果一致,便于调试和审计。
- 随机种子(基于系统时间或硬件熵):用于生产环境中的独立模拟。每次运行产生不同结果,反映模拟的随机性本质。
- 并行种子(每个进程/线程独立种子):在分布式计算中,为每个工作节点分配不同的种子,避免子序列重叠。
在 Python 中,推荐使用 NumPy 的 Generator 接口(而非旧的 RandomState),因为它支持更现代的算法和更好的并行特性:
import numpy as np
# 固定种子(可复现)
rng = np.random.default_rng(seed=42)
samples = rng.normal(0, 1, 10000)
# 随机种子(基于系统熵)
rng = np.random.default_rng()
samples = rng.normal(0, 1, 10000)
# 并行模拟:为每个任务分配独立种子
seeds = np.random.SeedSequence(12345).spawn(8)
for i, seed in enumerate(seeds):
rng_i = np.random.default_rng(seed)
# 每个 rng_i 生成独立的随机序列
4.3 逆变换法(Inverse Transform Method)
逆变换法是离散和连续分布抽样的基础方法。其核心思想是:如果随机变量 X 的累积分布函数(CDF)为 F(x),且 F 存在逆函数 F-1,则当 U ~ U(0,1) 时,X = F-1(U) 服从 F 所对应的分布。
操作流程:
- 生成均匀随机数 U ~ U(0,1);
- 计算目标分布的逆 CDF:X = F-1(U);
- 输出 X 作为目标分布的一个样本。
以指数分布为例,其 CDF 为 F(x) = 1 - e-λx,求逆得到 F-1(u) = -ln(1-u)/λ。由于 1-U 与 U 同分布,可简化为 X = -ln(U)/λ。在 Python 中:
import numpy as np
def exponential_sample(lambda_, n, rng):
"""逆变换法生成指数分布样本"""
u = rng.random(n)
return -np.log(u) / lambda_
# 生成 λ=0.5 的指数分布样本
rng = np.random.default_rng(42)
samples = exponential_sample(0.5, 100000, rng)
print(f"均值: {samples.mean():.4f} (理论值: 2.0)")
print(f"标准差: {samples.std():.4f} (理论值: 2.0)")
逆变换法的优势在于单调性保持——排序后的样本仍然保持排序,这在生成相关随机变量(如 Copula 模型)时非常有用。但其局限是:并非所有分布的 CDF 都有解析逆函数。例如,正态分布的 CDF 无法用初等函数表示,因此需要其他方法。
4.4 Box-Muller 变换与正态分布抽样
Box-Muller 变换是生成标准正态分布样本的经典方法。其原理是:若 U₁, U₂ 独立且均服从 U(0,1),则:
Z₀ = √(-2·ln U₁) · cos(2πU₂)
Z₁ = √(-2·ln U₁) · sin(2πU₂)
Z₀ 和 Z₁ 相互独立且均服从标准正态分布 N(0,1)。这一变换的几何意义是:将单位圆内的均匀点通过极坐标映射到二维正态分布。
在实际应用中,更推荐使用 Ziggurat 算法(NumPy 默认采用),它比 Box-Muller 快 2-3 倍,且数值稳定性更好。但 Box-Muller 在教学和理解上仍有重要价值:
import numpy as np
def box_muller(n, rng):
"""Box-Muller 生成标准正态样本"""
u1 = rng.random(n)
u2 = rng.random(n)
z0 = np.sqrt(-2 * np.log(u1)) * np.cos(2 * np.pi * u2)
z1 = np.sqrt(-2 * np.log(u1)) * np.sin(2 * np.pi * u2)
return z0, z1
rng = np.random.default_rng(42)
z0, z1 = box_muller(50000, rng)
# 验证统计性质
print(f"Z0 均值: {z0.mean():.4f}, 标准差: {z0.std():.4f}")
print(f"Z1 均值: {z1.mean():.4f}, 标准差: {z1.std():.4f}")
得到标准正态样本后,通过线性变换 X = μ + σZ 即可得到任意正态分布 N(μ, σ²) 的样本。在风险管理中,正态分布常用于资产收益率建模,但需注意金融收益率常具有厚尾特征,此时可考虑 t 分布或混合正态分布。
4.5 常见分布的抽样方法对比
| 分布 | 推荐方法 | 关键公式/步骤 | 注意事项 |
|---|---|---|---|
| 均匀分布 U(a,b) | 直接缩放 | X = a + (b-a)·U | 最基础,注意边界 |
| 指数分布 Exp(λ) | 逆变换法 | X = -ln(U)/λ | λ 为速率参数 |
| 正态分布 N(μ,σ²) | Box-Muller / Ziggurat | Z ~ N(0,1), X = μ+σZ | 避免用逆变换(无解析式) |
| 对数正态分布 | 指数变换 | X = eY, Y ~ N(μ,σ²) | 用于资产价格建模 |
| 泊松分布 Poi(λ) | Knuth 算法 / 逆变换 | 累乘直到 e-λ 被超过 | λ 较大时用正态近似 |
| t 分布 | 正态/卡方比值 | T = Z / √(V/ν) | ν 为自由度,厚尾 |
| 威布尔分布 | 逆变换法 | X = λ·(-ln U)1/k | 用于极端事件建模 |
4.6 案例拆解:用随机数生成验证 VaR 模型
背景:某投资组合包含两只股票,日收益率分别服从 N(0.02%, 1.5%) 和 N(0.01%, 2.0%),相关系数为 0.3。风险团队需要计算 95% 置信水平下的日 VaR,并验证不同随机数种子对结果的影响。
初始现象:使用固定种子 seed=42 时,VaR 估计为 3.82%;改用随机种子后,多次运行结果在 3.65%-4.10% 之间波动,团队担心结果不稳定。
分析过程:
- 首先确认样本量是否充足。使用 10 万条路径时,蒙特卡洛标准误约为 σ/√N ≈ 2.5%/316 ≈ 0.008%,远小于观察到的波动范围。
- 检查相关性是否被正确引入。使用 Cholesky 分解生成相关正态变量:
import numpy as np
# 协方差矩阵
corr = np.array([[1.0, 0.3], [0.3, 1.0]])
sigma = np.array([0.015, 0.02])
cov = np.outer(sigma, sigma) * corr
# Cholesky 分解
L = np.linalg.cholesky(cov)
# 生成相关正态样本
rng = np.random.default_rng(42)
Z = rng.normal(0, 1, (100000, 2))
X = Z @ L.T # 相关收益率
portfolio_returns = 0.6 * X[:, 0] + 0.4 * X[:, 1]
# 计算 VaR
var_95 = np.percentile(portfolio_returns, 5)
print(f"95% VaR: {-var_95*100:.2f}%")
- 对比不同种子下的结果分布:
var_estimates = []
for seed in range(20):
rng = np.random.default_rng(seed)
Z = rng.normal(0, 1, (100000, 2))
X = Z @ L.T
portfolio_returns = 0.6 * X[:, 0] + 0.4 * X[:, 1]
var_estimates.append(-np.percentile(portfolio_returns, 5) * 100)
print(f"VaR 均值: {np.mean(var_estimates):.2f}%")
print(f"VaR 标准差: {np.std(var_estimates):.3f}%")
print(f"VaR 范围: [{np.min(var_estimates):.2f}%, {np.max(var_estimates):.2f}%]")
结果对比:20 个不同种子下,VaR 估计的均值为 3.85%,标准差为 0.012%。这说明当样本量足够大时,种子选择对结果的影响可以忽略(<0.5% 的相对偏差)。之前观察到的 3.65%-4.10% 波动,很可能是因为使用了过小的样本量(如 1000 条路径)。
复盘经验:
- 在蒙特卡洛模拟中,样本量比种子选择更重要。建议至少使用 10 万条路径,并检查收敛性。
- 固定种子用于调试和审计,随机种子用于生产环境。
- 当结果对种子敏感时,应怀疑模型本身存在问题(如相关性矩阵不正定、分布假设不合理)。