随机数生成基础:伪随机数生成器(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 是基础,种子决定可复现性,Python 的 random 模块则是我们实战中的工具。三者环环相扣,缺一不可。
总结一下:
- PRNG 用确定算法生成「看起来随机」的序列
- 种子让随机序列可复现,这是金融模拟的硬要求
- Python random 模块封装了 Mersenne Twister,够快够好用
- 多线程场景下,记得用独立 Random 实例
嗯,随机数这块就聊到这。记住一句话:没有种子的随机数,就像没有锚的船——漂哪算哪。做金融模拟,千万别省这一行代码。
公众号:蓝海资料掘金营,微信deep3321