随机数生成基础:伪随机数生成器(PRNG)原理、种子(Seed)的重要性、Python中的random模块

做蒙特卡洛模拟,说白了就是跟随机数打交道。你想想看,没有随机数,哪来的模拟?但这里有个坑——计算机其实不会生成真正的随机数。嗯,这听起来有点反直觉,但确实是事实。

我刚开始做量化的时候,也以为计算机能像掷骰子一样产生随机数。后来踩了坑才明白,计算机生成的其实是「伪随机数」。今天我们就来聊聊这个。

伪随机数生成器(PRNG)原理

伪随机数生成器,英文叫 Pseudo Random Number Generator,简称 PRNG。它的核心思想很简单:用一个确定的算法,生成看起来随机的数列

为什么叫「伪」?因为只要你知道算法和初始状态,就能完全预测出后续所有数字。真正的随机数应该是不可预测的,比如用物理噪声源生成的。

最常见的 PRNG 算法是线性同余生成器(Linear Congruential Generator, LCG)。它的公式长这样:

X_{n+1} = (a * X_n + c) mod m

其中:

  • X_n 是当前值
  • a 是乘数
  • c 是增量
  • m 是模数

选好 a、c、m 这三个参数,就能生成一串看起来乱糟糟的数字。但注意,这串数字最终会循环——因为模运算的结果范围是有限的。循环周期越长,PRNG 的质量就越好。

核心要点:PRNG 不是真随机,而是「看起来像随机」的确定性序列。对于蒙特卡洛模拟来说,只要周期够长、统计性质够好,就够用了。

我个人习惯用 Mersenne Twister 算法,它是 Python 的 random 模块默认使用的 PRNG。周期长达 2^19937-1,基本不用担心循环问题。

种子(Seed)的重要性

种子是什么?它是 PRNG 的初始状态。同一个种子,生成的随机数序列完全一样。

这听起来好像是个缺点?其实恰恰相反。可复现性在金融模拟中是命根子

我在项目中遇到过这样的情况:跑了一个压力测试,结果看起来不错。但第二天重新跑一遍,结果变了。领导问:「昨天的结果呢?怎么不一样?」——这就是没设种子的后果。

避坑指南:我曾经因为没设种子,导致两个同事跑同一个模型得到不同结果,争论了半天才发现是种子问题。从那以后,我所有模拟代码的第一行都是 random.seed(42)

设置种子的好处:

  • 可复现:别人能重现你的结果
  • 可调试:出 bug 了可以反复跑同一组随机数
  • 可对比:不同策略在相同随机序列下比较才公平

你可能会问:那设了种子,随机性不就没了?其实不会。种子只决定序列的起点,序列本身依然是随机的。就像一本书,你从第 100 页开始读,和从第 200 页开始读,内容完全不同,但书本身没变。

Python中的random模块

Python 的 random 模块是我们做蒙特卡洛模拟最常用的工具。它封装了 Mersenne Twister 算法,用起来非常方便。

先看基本用法:

import random

# 设置种子
random.seed(42)

# 生成 [0, 1) 之间的浮点数
print(random.random())      # 0.6394267984578837

# 生成 [a, b] 之间的整数
print(random.randint(1, 100))  # 82

# 从列表中随机选一个
print(random.choice(['A', 'B', 'C']))  # 'C'

# 打乱列表
deck = [1, 2, 3, 4, 5]
random.shuffle(deck)
print(deck)  # [3, 1, 5, 2, 4]

常用的函数我整理了一下:

函数 说明 示例
random() [0, 1) 均匀分布浮点数 random.random()
uniform(a, b) [a, b] 均匀分布浮点数 random.uniform(0, 10)
randint(a, b) [a, b] 均匀分布整数 random.randint(1, 6)
gauss(mu, sigma) 正态分布 random.gauss(0, 1)
choice(seq) 从序列中随机选一个 random.choice(['涨', '跌'])
sample(seq, k) 从序列中随机选 k 个(不重复) random.sample(range(100), 10)

小技巧:做蒙特卡洛模拟时,我建议用 random.seed(None) 来基于系统时间生成种子,这样每次运行结果不同。但调试阶段一定要固定种子,比如 random.seed(42)

还有一个容易被忽略的点:random 模块不是线程安全的。如果你用多线程做模拟,每个线程应该有自己的 random 实例,或者用 random.Random 类创建独立实例。

import random

# 创建独立的随机数生成器
rng1 = random.Random(42)
rng2 = random.Random(123)

# 两个生成器互不影响
print(rng1.random())  # 0.6394267984578837
print(rng2.random())  # 0.052363598850944326

说到这,我想起一个项目经历。当时做期权定价的蒙特卡洛模拟,用了多线程加速。结果发现每次跑出来的结果都不一样,排查了半天——原来是多个线程共享了同一个 random 实例。改成每个线程独立实例后,问题就解决了。

知识体系总览

下面这张图总结了本章的核心逻辑:

随机数生成知识体系 伪随机数生成器 (PRNG) 算法原理 (LCG等) 种子 (Seed) Python random模块 确定性序列 · 周期 · 统计性质 可复现性 · 调试 · 对比 random() · randint() · gauss() 蒙特卡洛模拟 · 压力测试

从这张图可以看出,PRNG 是基础,种子决定可复现性,Python 的 random 模块则是我们实战中的工具。三者环环相扣,缺一不可。

总结一下:

  • PRNG 用确定算法生成「看起来随机」的序列
  • 种子让随机序列可复现,这是金融模拟的硬要求
  • Python random 模块封装了 Mersenne Twister,够快够好用
  • 多线程场景下,记得用独立 Random 实例

嗯,随机数这块就聊到这。记住一句话:没有种子的随机数,就像没有锚的船——漂哪算哪。做金融模拟,千万别省这一行代码。


公众号:蓝海资料掘金营,微信deep3321