目录

PyTorch 随机种子(Seed)全面解释

在深度学习和计算机科学中,随机种子是一个用于初始化伪随机数生成器(PRNG)的数值。通过设置相同的种子,可以确保程序在每次运行时生成相同的随机数序列,从而实现实验的可重复性。在 PyTorch 等框架中,随机种子控制着模型参数初始化、数据打乱、数据增强、Dropout 等几乎所有涉及随机性的操作。


  • 可重复性:科学研究要求实验能够被复现。设置固定种子后,其他人(或未来的你)运行同一份代码应得到完全相同的结果。
  • 调试:当模型出现异常时,固定随机性有助于定位问题,排除随机波动干扰。
  • 公平比较:在对比不同算法或超参数时,需要控制随机因素,确保差异来自方法本身而非随机噪声。

PyTorch 使用自己的 PRNG,同时也可能依赖其他库(如 NumPy、Python random)。要全面固定随机性,需要同时设置多个种子:

import torch
import numpy as np
import random

def set_seed(seed):
    torch.manual_seed(seed)                 # CPU 随机种子
    torch.cuda.manual_seed(seed)            # 当前 GPU 随机种子
    torch.cuda.manual_seed_all(seed)         # 所有 GPU 随机种子(如果有多卡)
    np.random.seed(seed)                     # NumPy 随机种子
    random.seed(seed)                         # Python 内置随机模块
    # 可选:设置 cuDNN 为确定性算法(可能降低性能)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

注意事项

  • torch.manual_seed 同时设置 CPU 和当前 GPU 的种子(若 CUDA 可用)。
  • torch.cuda.manual_seed_all 在多 GPU 环境下为所有 GPU 设置相同种子,确保每张卡的随机性一致。
  • torch.backends.cudnn.deterministic = True 强制 cuDNN 使用确定性算法,但可能使某些操作变慢。
  • torch.backends.cudnn.benchmark = False 禁用 cuDNN 的自动优化选择,避免因算法选择引入的不确定性。

当使用多进程数据加载(num_workers > 0)时,每个 worker 是一个独立的子进程,它们会复制主进程的 PRNG 状态。如果所有 worker 使用相同的初始种子,它们将产生完全相同的数据顺序,导致训练时每个 batch 内的样本高度相似(例如所有 worker 同时打乱,但每个 worker 内的打乱结果相同),这会破坏数据随机性并可能降低模型性能。

PyTorch 的 DataLoader 在创建 worker 进程时,会自动为每个 worker 分配不同的初始种子,具体规则如下:

  • 每个 worker 的种子基于主进程的当前种子worker 的 ID 通过一个固定公式生成。
  • 因此,即使不设置 worker_init_fn,每个 worker 内部的 PyTorch 随机操作(如数据集的 __getitem__ 中如果使用了 torch.rand)也会有独立的行为。

然而,这个自动机制只保证 PyTorch 自己的 PRNG 在 worker 之间不同不会自动设置 NumPy 或 Python random 的种子。如果数据预处理中使用了这些库,就需要在 worker_init_fn 中手动设置。

def worker_init_fn(worker_id):
    # 获取主进程为每个 worker 分配的种子(PyTorch 内部机制)
    seed = torch.initial_seed() % 2**32
    np.random.seed(seed)
    random.seed(seed)

dataloader = DataLoader(dataset, batch_size=32, num_workers=4,
                        worker_init_fn=worker_init_fn)
  • torch.initial_seed() 返回当前 PyTorch PRNG 的种子值,该值在 worker 中已被 PyTorch 自动设为与 worker_id 相关的唯一值。
  • 我们将此值模 2^32 后传递给 NumPy 和 Python random,确保它们的随机序列也与 worker_id 唯一关联,且与 PyTorch 的随机性协调。

重要worker_init_fn 仅在 worker 进程启动时调用一次(即使 persistent_workers=True,也只调用一次),因此 worker 内部的随机状态在整个生命周期中保持一致。


在分布式训练中,每个进程通常负责一部分数据,且每个进程有自己的 PRNG。为了确保全局可重复性,需要:

  • 在所有进程中设置相同的初始种子。
  • 同时确保每个进程(或每个 worker)的随机性彼此独立,避免数据重叠或同步问题。

通常做法是:在初始化每个进程时调用 set_seed(global_seed + rank),其中 rank 是进程的全局编号。这样既保持了全局固定,又保证了进程间的独立性。

import torch.distributed as dist

def setup(rank, world_size, seed):
    torch.manual_seed(seed + rank)
    # 同样设置其他库

对于 DataLoader 的 worker,PyTorch 的自动种子机制已经考虑了 worker 的局部 rank,因此无需额外处理。


设置种子仅影响初始化后的随机操作。如果代码中某些部分在设置种子之前已经使用了随机数,则无法保证可重复性。因此,应在程序的最开始调用设置函数

cuDNN 的一些算法(如卷积)默认是非确定性的,即使设置了相同的种子,多次运行结果也可能有微小差异。通过设置:

torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False

可以强制 cuDNN 使用确定性算法,但会牺牲一定性能。如果允许轻微浮动,可以不设置这两个选项。

Python 中的字典、集合等容器的迭代顺序在 Python 3.6+ 中虽已稳定,但某些操作(如 set 的遍历)仍可能因随机哈希而不同。可以通过设置环境变量 PYTHONHASHSEED 来固定哈希种子:

import os
os.environ['PYTHONHASHSEED'] = str(seed)

这应在导入任何模块之前设置。

如果使用了多线程(例如在数据预处理中),每个线程的随机性也需要独立控制。但通常 DataLoader 的 worker 已经是多进程,线程安全问题较少。

完全的可重复性很难保证,尤其是在以下情况下:

  • 使用了非确定性 GPU 操作(如某些原子操作)。
  • 硬件或驱动版本不同。
  • 浮点运算顺序变化(例如并行归约)。

因此,通常追求的是算法层面的可重复性,而非逐比特一致。


组件种子设置方式注意事项
PyTorch (CPU/GPU)torch.manual_seed(), torch.cuda.manual_seed_all()覆盖所有 PyTorch 操作
NumPynp.random.seed()在 worker 中需单独设置
Python randomrandom.seed()同上
DataLoader worker自动设置 PyTorch 种子;需在 worker_init_fn 中设置 NumPy 和 random确保不同 worker 独立随机
cuDNNtorch.backends.cudnn.deterministic = True保证卷积等操作的确定性
Python 哈希PYTHONHASHSEED 环境变量避免容器顺序随机性

最终建议:编写一个统一的 set_seed 函数,在程序入口处调用,并在 worker_init_fn 中为每个 worker 同步设置其他库的种子,这样可以最大程度保证实验的可重复性。

worker_init_fn 中,我们通常这样写:

seed = torch.initial_seed() % 2**32
np.random.seed(seed)
random.seed(seed)




  • PyTorchtorch.initial_seed() 返回的是一个 Python int,其值可以很大(例如 64 位甚至更大)。PyTorch 内部使用 64 位或更高精度的种子来初始化其 PRNG(基于 Philox 算法等),因此大整数没有问题。
  • NumPynp.random.seed() 接受的参数最终会传递给 C 语言的 mt19937 实现,该实现期望一个 32 位无符号整数。如果传入的整数大于 23212^{32}-1,NumPy 会将其截断为低 32 位(相当于隐式取模 2322^{32})。
  • Python 内置 randomrandom.seed() 接受任意大小的整数,但内部会通过一个哈希函数将其转换为一个用于初始化 Mersenne Twister 的 32 位种子。虽然它允许大整数,但哈希结果仍然是一个 32 位的值;并且不同 Python 版本或平台对哈希的处理可能略有差异,直接传入大整数可能引入不确定性。

因此,直接传递 torch.initial_seed() 的原始值(可能很大)给 NumPy 或 Python random 是可以工作的,因为它们最终都会将其映射到 32 位空间。但这种隐式映射(截断或哈希)的具体行为依赖于实现,可能在不同版本或平台上产生细微差异,从而破坏实验的可重复性。


  • 明确意图:显式取模清楚地表明我们希望将种子限制在 32 位范围内,避免依赖隐式截断。
  • 跨平台一致性:无论在哪个系统或库版本下,取模运算的结果都是确定的,确保了随机数序列的一致性。
  • 兼容性:虽然当前版本可能处理大整数没问题,但未来库的更新可能改变隐式处理方式。显式取模可以防止这种潜在的不兼容。
  • 理论合理性2322^{32} 是 32 位无符号整数的模数,取模后得到的值均匀分布在 [0,2321][0, 2^{32}-1] 区间,这对 PRNG 的初始化来说已经足够(32 位种子空间对于大部分非加密应用完全够用,且能保证不同 worker 之间的随机性独立)。

  • 历史上,Mersenne Twister(NumPy 和 Python random 使用的算法)的初始化通常需要一个 32 位种子。
  • 许多随机数生成器的种子接口都设计为接受 32 位整数。
  • 2322^{32} 操作简单且保留了原始种子的低 32 位信息,不会引入额外的偏差。

假设 torch.initial_seed() 返回 12345678901234567890(远大于 2322^{32}):

  • 在 NumPy 中,np.random.seed(12345678901234567890) 实际上会隐式转换为 12345678901234567890 & 0xFFFFFFFF,结果等于 12345678901234567890 % 2**32。所以最终效果与显式取模一致,但依赖隐式截断。
  • 在 Python random 中,random.seed(12345678901234567890) 会将大整数通过哈希函数转换为状态。虽然它也会得到一个确定的结果,但哈希函数的具体实现可能在不同 Python 微版本中变化,而取模则不受影响。

因此,显式取模是更安全、更可移植的做法。


  • 取模 2322^{32} 是为了将 PyTorch 的种子值(可能很大)规范化为一个 32 位范围内的整数,符合 NumPy 和 Python random 内部 PRNG 的期望输入范围。
  • 它消除了依赖隐式截断或哈希的不确定性,确保在多 worker 环境下每个 worker 的随机数生成器具有可预测且独立的种子,从而保证实验的完全可重复性。
  • 这是一种广泛采用的编程惯例,体现了对跨平台兼容性和代码清晰度的追求。

相关内容