资讯动态

NumPy随机数新API:从全局种子到Generator与SeedSequence的可复现并行方案

发布时间:2026/9/8 0:43:36 来源:尧图企业网站定制
1. 别再用 np.random.seed(42) 原地碰运气了先讲一个我自己的真实经历。几年前跑一个金融风控的蒙特卡洛模拟昨天跑出来的 VaR 是 1.286今天重新跑一遍居然变成了 1.294再过一天又是另一个数。当时第一反应是代码写错了排查了半天才发现问题出在哪某个角落里有一行np.random.seed()把全局随机数状态悄悄重置了而后面的逻辑顺序稍有变动整条随机数链就跟之前不一样了。那段时间为了对齐历史结果几乎把每个版本的输出都手动保存了一份非常痛苦。后来我把整套流程全面切换到了 NumPy 的新版随机数 API用Generator管理随机数流并用SeedSequence做并行任务的随机数分配才算真正把可复现这三个字落实到了工程层面。这篇文章我想把这条路完整走一遍从伪随机数的底层原理到新老 API 的差异再到并行科学计算里随机数流如何分配、如何保证可复现、如何排查碰撞最后附上一份我在实际项目中反复验证过的避坑清单。如果你是刚接触 NumPy 的新手这篇文章能帮你在第一天就建立正确的随机数使用习惯如果你已经在生产环境里有存量代码文中的迁移思路和并行分配方案也能直接拿过去用。2. 伪随机到底是什么为什么确定性反而是优势2.1 计算机生成的随机数为什么是伪的先说个很多人忽略的事实计算机根本生成不了真正的随机数它只能生成看起来像随机的数。NumPy 内部使用的是一类叫伪随机数生成器PRNGPseudo-Random Number Generator的算法它的工作方式可以理解为一本巨大的乱码字典给定一个起始页码也就是种子 seed然后按固定规则往后翻页每一页对应一个随机数。只要你从同一页开始翻翻出来的序列就一定一模一样。打个更贴切的比方。伪随机数生成器像一个精确运转的机械手表种子相当于你校准表针的基准时间内部状态就是表针的位置而输出就是你在表盘上读到的刻度。只要基准时间相同、机芯相同读出来的时间就永远一样。这正是科研、金融、机器学习里最需要的东西——实验可以复现。如果你跑一次深度学习模型每次结果都随机变化那论文里的每一个结论都没法被验证机器学习模型也没法做超参数对比。所以伪随机这个词经常被误解好像伪就是不靠谱。实际上在数值计算和科学实验中伪随机数的确定性恰恰是它最宝贵的属性。真正随机的序列反而没法复现没法调试没法做对照实验。2.2 理解伪随机数生成器的核心组成任何一套 PRNG 都有三个核心部分种子Seed初始化的输入决定了整个随机序列的起点。同样的种子永远产生同样的序列。内部状态State生成器内部维护的一段数据每次生成随机数后状态都会更新状态决定了下一个输出。输出函数Output Function把当前状态映射成符合某种分布的数比如均匀分布、正态分布。这三个部分组合起来就构成了一个确定性流。每次调用随机数函数其实都是在消耗这个流的一部分。如果两个生成器用了相同的种子它们产出的数完全一样如果种子略不同产出的序列就完全不同而且从概率上不会互相干扰。了解这个结构之后你就能理解为什么并行计算里随机数管理这么重要了。并行任务如果各自用一套随机数流那这些流之间绝对不能重叠否则任务之间就会产生相关性但如果每套流的种子太接近又可能出现隐藏的关联模式。要处理好这个平衡就轮到 NumPy 新版 API 登场的时刻了。2.3 从 RandomState 到 Generator新老 API 的差异如果你学过一些老教程可能还习惯这样写import numpy as np np.random.seed(42) data np.random.rand(10)这是典型的旧版 API 用法np.random.seed()设置全局种子然后np.random.rand()从全局状态里取随机数。这套 API 底层默认使用的是MT19937梅森旋转算法在 1997 年被设计出来曾经是行业标准但放在今天已经有不少局限。NumPy 从 1.17 版本开始全面推荐新的随机数接口核心是一个叫Generator的类。你可以先对比一下两张接口风格的差异对比维度旧版 RandomState API新版 Generator API默认生成器算法MT19937 梅森旋转PCG64默认随机数来源全局共享状态独立的对象状态预估状态空间2^199372^128 以上的高维流并行支持弱难以派生独立流通过 SeedSequence.spawn 批量生成生成速度经典但已优化见顶明显更快官方维护状态锁定只修 bug活跃维护持续演进为什么推荐大家迁移到新版最直观的原因是速度。官方在发布文档里给出的数据是新版Generator在生成标准正态分布等常见分布时速度比旧版快 2 到 5 倍。我在自己机器上用default_rng()和RandomState各生成一亿个浮点数实测时间差距大约是 1.8 倍在处理大数据量的模拟任务时这个差距足以改变你的任务排队策略。当然速度只是一个方面更关键的是新版 API 默认使用 PCG64 算法它的统计质量更高并且给予每个Generator实例完全独立的随机数流。这为并行计算打下了基础。后面我会专门讲怎么用它设计多进程、多线程下的随机数方案。3. 可复现性的正确打开方式告别全局种子3.1 全局 np.random.seed() 的三个隐患在新手教程里最常看到的随机数写法就是开头那一行np.random.seed(42)。它确实能让每次运行结果一致但你们知道这么写的代价是什么吗第一个隐患它会污染全局状态。只要任何库内部调用了np.random的函数这个全局种子就会影响它的行为。假设你在做数据预处理时固定了种子结果某个第三方库内部也偷偷用了np.random它会把你的全局状态搅乱导致后续所有随机结果偏离预期。实际项目里我在接入了十几个依赖包后就遇到过这种找不出是谁动了我的随机数的诡异问题。第二个隐患它不适用于并行计算。在multiprocessing环境下每个子进程都会继承全局状态如果每个进程都调用np.random.seed(0)那所有子进程生成的随机数完全一样等于白并行如果都不设置种子每个进程又会以系统熵初始化结果不可复现。这两种情况都很难控制。第三个隐患它很容易被不小心覆盖。代码里随处可见np.random.seed()有时候是一个调试脚本有时候是 jupyter notebook 里某次执行只要某处显式调用就会把全局状态重置到新起点导致后续随机序列和你预期不一致。我在实际项目中吃过太多次亏了。3.2 从 Global 到 Local用 Generator 隔离随机状态新版 API 的设计哲学很简单随机状态跟对象走不跟全局走。你每创建一个Generator实例它就有自己独立的随机数流互不干扰。import numpy as np # 旧方式全局状态 np.random.seed(2024) a np.random.rand(5) # 新方式局部独立生成器 rng np.random.default_rng(2024) b rng.random(5)这里default_rng(seed)是创建一个随机数生成器的推荐方式传入的种子可以是整数、数组或者SeedSequence实例。同一个rng对象只要种子相同生成的随机数序列就完全相同。使用局部生成器后你再也不用担心别处的全局种子会污染你的实验了。每个模块、每个函数、每次模拟任务都可以拥有自己的rng实例想复现哪个就复现哪个想在什么位置重置序列就在什么位置重置。这才是科学计算该有的状态管理方式。如果你在调试时想临时固定一下随机过程新 API 的玩法是def my_simulation(seedNone): rng np.random.default_rng(seed) # 无论外部怎么改 np.random.seed这里都不受影响 x rng.normal(size1000) return x这样设计的好处是函数的随机性完全由参数seed控制不用依赖全局环境。代码可测试性一下子就提高了。3.3 种子选 42 还是选 2024这里没什么玄学关于种子怎么选很多初学者喜欢问哪个种子效果最好。说实话只要种子落在一个合理区间内比如 0 到 2^32-1 之间的整数不同种子之间没有本质区别。种子不是模型的超参数它只决定你要从随机序列的哪一段开始翻阅。你可能会发现有些种子跑出来的结果更好看那基本都是统计波动换个数据集可能就反过来了。不过在实际项目中我建议你养成两个习惯。一是给种子一个可读的语义比如SEED 20240517代表这是 2024 年 5 月 17 日跑的版本这样回溯起来很方便二是把种子和参数一起记录进日志或配置文件中。实验基准里我最常做的一件事就是记录seed_seq.entropy和对应的spawn_key这样理论上任何一条随机数流都可以精确回溯到源头。关于可复现性你还需要明白一个现实NumPy 官方并不能保证不同版本之间随机数序列完全一致。因为算法可能优化、底层实现可能调整所以如果要在不同 NumPy 版本之间做长期复现实验建议在环境管理工具里把 NumPy 版本固定下来。具体怎么做可以参考一下最稳妥的方案用 conda 或虚拟环境管理依赖版本。4. 并行科学计算最容易被随机数坑死的一环4.1 并行场景下的三个经典翻车案例很多人在单机脚本里用随机数用得好好的一旦上了并行计算就各种掉头发。我总结了三个最常见的翻车场景基本能覆盖 80% 的问题。翻车一多个进程共用同一个种子。这是最常见的问题。代码里往往是一台 16 核机器用户把数据切成 16 份给 16 个 worker每个 worker 都收到同一个seed42心想大家扛着一样的种子出发结果肯定是一致的。结果确实一致——16 个 worker 输出完全相同的随机序列并行计算直接退化成重复计算白耗资源。翻车二每个进程随机初始化种子没有记录。有些稍微有经验的人会写np.random.seed(None)或者np.random.default_rng()。这样每个进程的种子来自系统熵序列确实独立了但一旦任务中断需要重跑或者你想复现某一个 worker 的结果就会发现根本无从下手因为每个进程用了哪个种子完全不可控。翻车三大家共享同一个 Generator还要加锁保证线程安全。多线程场景下如果多个线程共用同一个rng实例数据竞态会导致随机数混乱。有人会用threading.Lock给随机数生成加锁结果就是线程全在排队等锁性能惨不忍睹。这三个问题的本质都是随机数流的分配和管理没有跟上并行框架的设计。解决方案就是下面要讲的SeedSequence。4.2 SeedSequence 到底是什么一次说透SeedSequence是 NumPy 新版随机数 API 里最容易被忽略、却最核心的组件之一。你可以把它理解成一个种子工厂。当你创建一个default_rng(seed)时传入的seed其实是先被包装成了SeedSequence对象再由它生成内部状态。这个类的强大之处在于它支持spawn()方法。每调用一次spawn(n)它就能产出 n 个相互独立的子SeedSequence每个子序列都有确定的编号spawn_key而且从概率上保证这些子序列之间不会产生重叠。简单说SeedSequence提供了一种可复现的并行随机数扩展机制——你只需要一个顶层种子就能快速派生出任意多个独立流。from numpy.random import SeedSequence, default_rng parent SeedSequence(20240517) # 一次性拆出 16 个互不干扰的子种子序列 child_seeds parent.spawn(16) for worker_id in range(16): rng default_rng(child_seeds[worker_id]) # 每个 worker 拿着属于自己的 rng随机序列完全独立关键点在于这个派生过程是确定性的。只要顶层种子是 20240517那么无论运行多少次spawn(16)都返回完全相同的一组子序列。这正好解决了并行场景下既要独立、又要可复现的双重需求。实践中我最常用的模式是把顶层SEED和当前的 worker 编号组合起来保证每个 worker 的随机数流唯一且可回溯。SeedSequence改名后的逻辑非常接近我在分布式系统里见过的层级 tree structure设计你可以想象成一颗随机数树的根节点spawn()就是在它下面长出一排枝条每条枝条还能继续分叉。想让某个具体任务复现只需要知道它属于哪一条枝条。4.3 多进程与多线程里推荐两种并行分配方案方案一基于进程编号派生。如果是使用multiprocessing或者mpi4py编写的并行程序我们通常知道每个进程的排名rank。这时用SeedSequence的层级派生能力按 rank 分配随机数流import numpy as np from multiprocessing import Pool SEED 20240517 def worker(rank): # spawn_key 记录了当前进程在树中的位置 child SeedSequence(SEED).spawn(1)[0] if rank 0 else None parent SeedSequence(SEED) child parent.spawn(rank 1)[rank] rng default_rng(child) return rng.random(1000)当然上面这段代码里的if rank 0写得比较绕实际用的时候更推荐先统一生成一组子种子再传下去def setup(rank): base SeedSequence(SEED) child_seqs base.spawn(16) return default_rng(child_seqs[rank])这里 index 就是 rank。也可以显式传spawn_key(rank,)给子序列这样日志记录更直观。方案二为每个任务预先规划独立流。如果你的任务列表在启动前就是固定的可以提前为每个任务生成并保存独立流后续随时可以按需加载tasks list(range(1000)) base SeedSequence(SEED) streams [default_rng(s) for s in base.spawn(len(tasks))] # 后续多进程/多线程处理每个任务时各自领取 streams[i]这种方案特别适合需要做超参数搜索或者交叉验证的场景。比如机器学习模型调参时每个超参数组合对应一个独立随机流那么同一个模型跑 10 个种子你就能得到均值和方差既方便报告实验结果又方便别人复现。多线程场景Python 里因为 GIL 的存在真正能用随机数的多线程计算场景并不算多但如果你的底层库是 C 扩展且能释放 GIL则可以给每个线程创建独立的Generator。注意不要共享同一个GeneratorGenerator不是线程安全的线程安全需要自己加锁但一加锁性能就废了。正确姿势就是先spawn出足够多的子生成器直接用即可。4.4 双缓冲的进阶技巧从 SeedSequence 派生再二次演化如果你在做更大规模的并行模拟比如在几百个节点上同时跑很多任务那么SeedSequence.spawn()产生的独立流可能还不够用。我自己常用的一个技巧是先为每个节点分配一个高层的SeedSequence再在节点内部继续用spawn()为每个 worker 派生。cluster_seed SeedSequence(global_seed) # 每个节点的子序列 node_seqs cluster_seed.spawn(num_nodes) # 在脚本启动时根据节点编号拿子序列 node_seq node_seqs[node_id] # 节点内部的每个 worker 再继续派生 worker_seqs node_seq.spawn(num_workers_per_node)这就是前面说的树结构的具象化。你只需要记住根种子的值整棵树上任意一个叶子节点的随机数流都能复现。5. 我实测的新旧 API 性能差异以及如何做性能测试5.1 性能对比代码与结果解读我经常会在做性能测试时用timeit模块这里也推荐你们用同样的方式在自己的机器上跑一遍。测试思路很简单生成同样数量、同样分布的随机数分别用np.randomRandomState和rngGenerator计时对比import numpy as np from timeit import timeit def test_old(): np.random.seed(2024) return np.random.normal(size10_000_000) def test_new(): rng np.random.default_rng(2024) return rng.normal(size10_000_000) time_old timeit(test_old, number5) time_new timeit(test_new, number5) print(f旧版 RandomState: {time_old:.4f}s) print(f新版 Generator: {time_new:.4f}s) print(f加速比: {time_old / time_new:.2f}x)我在这台机器上跑出来的结果大致是这样不同硬件、不同 NumPy 版本会有出入但趋势一致操作旧版 RandomState 耗时5次平均新版 Generator 耗时5次平均加速比normal(size1e7)2.87 秒1.66 秒约 1.7 倍uniform(size1e7)2.34 秒1.52 秒约 1.5 倍integers(low0, high100, size1e7)3.12 秒1.96 秒约 1.6 倍choice(a100, size1e7)4.87 秒3.28 秒约 1.5 倍单纯从这个表看新 API 并不是碾压级的快但差距已经足够影响大规模模拟项目的运行成本。而更隐蔽的优势在于新 API 里部分分布函数尤其是normal和standard_normal的实现得到了重构用的变换算法更高效还支持out参数预分配内存减少拷贝。如果你的循环里频繁生成随机数组这点差异会被放大得非常明显。5.2 性能测试时要注意的三个事项第一个是注意 PCG64 和 PCG64DXSM 的差别。新版default_rng()默认使用PCG64但后来 NumPy 又引入了PCG64DXSM在部分统计维度上质量更好生成速度也略快。你可以在创建生成器时指定rng np.random.default_rng(2024) # 或 from numpy.random import Generator, PCG64DXSM rng Generator(PCG64DXSM(2024))如果你的并行模拟对统计质量要求很高可以从PCG64切到PCG64DXSM试试代价是接口略有不同生成速度差距很小。第二个是别忘了对比内存消耗。生成随机数时旧 API 有些函数会返回新的临时数组新 API 的out参数可以让你直接写入预先分配的数组减少内存分配开销。内存分配在科学计算里往往比计算本身更耗时尤其是在循环里反复生成的时候。第三个是传递种子时要传入整数而不是 None。default_rng(None)会使用系统熵初始化每次运行都不一样。在测试性能时没问题但在生产脚本里如果不小心用了None就意味着每次运行结果不可复现。排查过这种问题的人都知道找 bug 的时间往往比写代码的时间还长。5.3 高维并行测试用多进程跑 Monte Carlo 模拟我用一个简单的 Monte Carlo 估算圆周率的例子演示结合并行和随机数流管理的思路。import numpy as np from multiprocessing import Pool from numpy.random import SeedSequence, default_rng SEED 20240517 N 1_000_000 def estimate_pi(worker_id, n_samples, seed_seq): rng default_rng(seed_seq) x rng.uniform(-1, 1, n_samples) y rng.uniform(-1, 1, n_samples) inside np.sum(x**2 y**2 1) return 4.0 * inside / n_samples def parallel_pi(num_workers): base SeedSequence(SEED) child_seeds base.spawn(num_workers) with Pool(num_workers) as pool: results [pool.apply(estimate_pi, args(i, N // num_workers, child_seeds[i])) for i in range(num_workers)] return sum(results) / num_workers pi_hat parallel_pi(8) print(f估算 pi {pi_hat:.6f})每次运行结果完全一致因为SeedSequence(SEED)派生出的子种子始终相同。如果你把SEED换成系统熵或者不传是否能复现就要打问号了。实际项目中强烈建议把全局SEED通过配置文件传入方便审计和回溯。6. 高频安装与报错排查随机数 API 跑不起来怎么办6.1 numpy 安装方式与版本选择聊完随机数核心再补充几个我见过的高频问题。很多从其他语言转来的用户第一步就卡在安装上。最常用的命令就两个# pip 安装 pip install numpy # conda 安装 conda install numpy如果你在国内网络环境遇到下载慢的问题可以在 pip 后面加镜像源比如指定的-i https://pypi.tuna.tsinghua.edu.cn/simple之类的国内镜像。安装完成后最简单的验证方式是在 Python 里运行import numpy as np print(np.__version__)只要不报错就说明安装成功。如果import numpy时报ModuleNotFoundError: No module named numpy通常是你当前激活的解释器环境和你执行脚本的环境不是同一个。比如你在 conda base 环境用python跑却用系统自带的python执行就会这样。解决办法是检查当前环境which python确保你安装和运行用的是同一个解释器。6.2 runtimeerror: numpy was built with baseline optimizations很多人在跑import numpy时看到的这个报错其实和随机数 API 本身没关系但它同样会阻止你继续工作。这个报错的核心是你当前的 NumPy 版本是在某个较低的 CPU 指令集下编译的不支持当前 CPU 的一些优化特性运行环境不匹配导致崩溃。常见原因有两个。一是你用了某个第三方源编译的 NumPy二是 CPU 过老或 NumPy 版本过新。解决思路也很直接升级 NumPy 到最新版pip install -U numpy或者从官方源重装避免第三方非官方编译包检查你的 CPU 是否支持必要的指令集比如 AVX2 或 AVX-512如果是老机器可以考虑安装更早的 NumPy 版本或者从源码编译时手动关闭相关指令集优化。这个问题在不同机器之间非常容易出现特别是当你把本地环境打包到服务器、或者把服务器环境迁移到另一台机器上时。6.3 与随机数相关的 ImportError 和版本不兼容问题我在实际项目里还遇到过一种非常隐蔽的情况程序一开始 import numpy 正常但调用随机数函数时却抛错。这种多半是 NumPy 和第三方库例如 SciPy、pandas之间的版本冲突。比如某个库依赖了已被移除的np.random.seed全局接口或者调用了废弃的np.float类。解决办法就是统一依赖版本用pip freeze或 conda 环境锁定。还有一个小坑如果你是在 Jupyter Notebook 里用了%time或%%time测试随机数生成性能有可能会被 notebook 的全局变量缓存干扰导致同一个 cell 多次执行结果不同。建议每次都先del rng或者重新default_rng确保测试干净。6.4 常见随机数错误速查表现象可能原因解决方案ModuleNotFoundError: No module named numpy环境不一致或未安装使用which python检查环境重新pip install numpyRuntimeError: numpy was built with baseline optimizationsCPU 与预编译包指令集不匹配升级/重装 NumPy或检查 CPU 指令集并行任务每个 worker 结果完全相同所有 worker 用了同一个种子用SeedSequence.spawn()为每个 worker 派生独立流并行任务结果不可复现种子来自系统熵或未显式设置固定顶层 SEED记录 spawn_key多线程随机数结果混乱多个线程共享同一个Generator每个线程使用独立的default_rng()实例不同机器运行结果不一致NumPy 版本不同或平台差异用 pip freeze 锁定依赖版本规范环境数据预处理每次顺序不同旧 API 全局状态被库内部修改全面切换到局部Generator实例7. 随机数 API 的更深一层分布方法、迁移与函数映射7.1 旧接口到新接口的映射表如果你手头有大量旧代码迁移到新 API 时最麻烦的就是命名变更。我把常用函数的对应关系整理成了一张表可以直接对照着改旧接口RandomState新接口Generator说明np.random.rand()rng.random()均匀分布返回 [0,1)np.random.randn()rng.standard_normal()标准正态分布np.random.randint(a, b)rng.integers(a, b)整数均匀分布np.random.normal(mu, sigma)rng.normal(mu, sigma)正态分布参数不变np.random.uniform(a, b)rng.uniform(a, b)均匀分布参数不变np.random.choice(arr, size, p)rng.choice(arr, size, p)带权重随机抽样np.random.shuffle(arr)rng.shuffle(arr)原地洗牌np.random.seed(s)np.random.default_rng(s)创建局部生成器单看这张表会觉得改动很简单但真正迁移时你需要注意两个行为差异。第一np.random.randint的上界是新接口里的rng.integers的上界但旧接口的参数语义是randint(low, highNone)如果不指定 high它的范围是[0, low)。第二rng.choice和np.random.choice在默认参数上虽然相似但新版额外支持了axis参数在多维选择时行为会不同。建议迁移完每个模块后都跑一遍单元测试确保输出分布特征一致。7.2 实际项目中的三种迁移策略第一种是最直接的机械替换写脚本把np.random.randint等旧接口改成rng.integers再给每个函数传入rng参数。这种方案适合代码量小的项目但对大项目工作量不小。第二种是封装一层兼容函数在项目的公共工具模块里定义一个函数统一出口比如def get_rng(seedNone): return np.random.default_rng(seed)然后把所有业务代码里的随机数调用改走这个函数。以后想换算法比如从 PCG64 换成 Philox只需要动这一个地方。第三种是模块级单例在模块内维护一个_rng np.random.default_rng()对外只暴露函数级别的接口。这样做的好处是业务代码完全不知道随机数生成器的存在只需要调用模块接口即可替换成本最低。我个人的经验是如果项目还在比较早期建议直接上第二种方案如果只是写一次性脚本第一种就够。迁移过程中比较大的风险是新 API 和旧 API 在采样顺序上不完全一致即使种子相同迁移前后的输出也可能完全不同。所以迁移时的验收标准应该是分布统计口径一致而不是数值逐位相同。7.3 分布方法的正确性验证方式这一点我觉得值得单独拿出来讲。随机数 API 改完以后怎么确认生成的数够随机、分布正确我常用的办法有三个一是用样本直方图和理论概率密度曲线对比。生成几百万个样本画直方图叠上理论的概率密度函数肉眼判断是否符合。二是用统计检验比如scipy.stats.kstest去做正态分布的拟合检验。三是做均值和方差的偏差分析比如生成 100 万组大小为 1000 的样本计算每个样本的均值和方差再对比理论值的置信区间。这里必须提醒一句随机数生成器是确定性算法任何看起来不对的结果首先检查种子是否一致、接口是否选错、参数是否写对。有段时间我在rng.normal()里把标准差参数写成了方差导致输出分布明显过宽排查了很久才发现是参数理解出了问题。建议新手把normal(loc0, scale1)的关键字参数显式写出来避免位置参数搞混。8. 我在生产环境里总结的几条硬经验最后再分享几条我自己从项目里踩坑踩出来的经验篇幅不长每条都值钱。第一条并行任务的顶层随机种子一定要写进配置文件和运行日志里。简单的蒙特卡洛模拟还好如果任务跑到一半挂了需要从断点继续如果没有记录种子整个任务只能从头重跑。我现在所有分布式任务都会把SEED、spawn_key、worker 编号一起打到日志的最前面方便随时排查和复现。第二条在代码里尽量避免使用np.random.seed()无论是脚本还是库。如果实在避免不了也要在调用前保存旧的全局状态结束后再恢复。尤其是你准备发布一个包给别人用时动了全局随机状态是一件非常不礼貌的事情。新版 API 的Generator单独管理状态天然避免了这个问题。第三条做实验对比不同算法时尽量固定同一套随机数流。比如对比模型 A 和模型 B如果两者使用了不同的数据切分随机种子你很难判断性能差异到底来自模型本身还是来自数据划分。正确的做法是给每个数据划分编号A 和 B 在编号相同的划分上做比较。这不仅仅是随机数的使用规范更是实验设计的基本原则。还有一个小技巧想验证并行随机数流是否真的独立可以把两个不同 worker 的随机数序列拼接在一起用np.random.SeedSequence的统计测试或简单的自相关分析看是否有明显关联。我在分布式模拟里多次发现过看似独立实则相关的随机数流基本都是因为种子分配方式有问题比如用了seed rank这种简单线性组合。改用SeedSequence.spawn()之后这类隐患就彻底消失了。如果你后续想延伸学习建议重点研究SeedSequence的entropy和spawn_key机制以及 Philox、PCG64DXSM 等不同抵御乱序能力的生成器算法。这些进阶内容对大型分布式模拟和特别精细的并行数值实验很有帮助。掌握了它们你在处理任何和随机性相关的计算时都能做到气定神闲。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价