深度学习网络模型中初始的权值参数通常都是初始化成随机数,而使用梯度下降法最终得到的局部最优解对于初始位置点的选择很敏感,下面介绍Pytorch中随机种子的设置及其原理。
1. Pytorch随机种子设置
在同一开发环境中,随机数种子seed确定时,模型的训练结果将始终保持一致。
def setup_seed(seed):
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.deterministic = True
# 设置随机数种子
setup_seed(20)
# 预处理数据以及训练模型
-
统一随机种子仍然无法复现实验结果的原因
-
如果设置了nn.LSTM的dropout参数,是无法通过随机种子的固定来复现实验结果的;
-
在数据预处理阶段使用了随机初始化的nn.Embedding,并将其通过持久化方式pickle、cpickle保存了下来。再次使用时,通过pickle.load()读取,即使固定了随机数种子,此时读取到的nn.Embedding()中的weight与当初保存下来的weight是不同的。
-
numpy随机数与Dataloader多进程共用时可能会导致随机结果是不同的
-
其他配置具体操作解读
-
CuDnn
-
cuDnn中对卷积操作进行了优化,牺牲了精度来换取计算效率。如果需要保证可重复性,可以使用如下设置:
from torch.backends import cudnn
cudnn.benchmark = False # if benchmark=True, deterministic will be False
cudnn.deterministic = True
不过实际上这个设置对精度影响不大,仅仅是小数点后几位的差别。所以如果不是对精度要求极高,其实不太建议修改,因为会使计算效率降低。
- Pytorch
seed = 0
torch.manual_seed(seed) # 为CPU设置随机种子
torch.cuda.manual_seed(seed) # 为当前GPU设置随机种子
torch.cuda.manual_seed_all(seed) # 为所有GPU设置随机种子
- Python & Numpy
如果读取数据的过程采用了随机预处理(如RandomCrop、RandomHorizontalFlip等),那么对Python、Numpy的随机数生成器也需要设置种子。
import os
import random
import numpy as np
seed = 0
random.seed(seed)
np.random.seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed) # 为了禁止hash随机化,使得实验可复现。
- Dataloader
如果Dataloader采用了多线程(num_workers > 1), 那么由于读取数据的顺序不同,最终运行结果也会有差异。也就是说,改变num_workers参数,也会对实验结果产生影响。
目前暂时没有发现解决这个问题的方法,但是只要固定num_workers数目(线程数)不变,基本上也能够重复实验结果。
对于不同线程的随机数种子设置,主要通过DataLoader的worker_init_fn参数来实现。默认情况下使用线程ID作为随机数种子。如果需要自己设定,可以参考以下代码:
GLOBAL_SEED = 1
def set_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
GLOBAL_WORKER_ID = None
def worker_init_fn(worker_id):
global GLOBAL_WORKER_ID
GLOBAL_WORKER_ID = worker_id
set_seed(GLOBAL_SEED + worker_id)
dataloader = DataLoader(dataset, batch_size=16, shuffle=True, num_workers=2, worker_init_fn=worker_init_fn)
2. Pytorch随机种子原理
Pytorch的随机种子生成器位于/torch/include/Aten/core/Generator.h文件中,其中包含完整的生成方法说明
/**
* Note [Generator]
* ~~~~~~~~~~~~~~~~
* A Pseudo Random Number Generator (PRNG) is an engine that uses an algorithm to
* generate a seemingly random sequence of numbers, that may be later be used in creating
* a random distribution. Such an engine almost always maintains a state and requires a
* seed to start off the creation of random numbers. Often times, users have
* found it beneficial to be able to explicitly create, retain, and destroy
* PRNG states and also be able to have control over the seed value.
*
* A Generator in ATen gives users the ability to read, write and modify a PRNG engine.
* For instance, it does so by letting users seed a PRNG engine, fork the state of the
* engine, etc.
*
* By default, there is one generator per device, and a device's generator is
* lazily created. A user can use the torch.Generator() api to create their own generator.
*/
Pytorch采用线性同余方法生成随机种子,线性同余方法(LCG)是一种产生伪随机数的方法,下面将具体介绍该方法:
\[LCG是根据递归公式:RandSeed = (A * RandSeed + B) % M \]- 线性同余法最重要的是定义了三个整数,乘数 A、增量 B和模数 M,其中A, B, M是产生器设定的常数。 LCG的周期最大为 M,但大部分情况都会少于M。要令LCG达到最大周期,应符合以下条件:
- 这个算法的缺点是,在参数确定后,伪随机序列只与\(RandSeed[0]\)相关,容易被破解。有一种改进的办法就是每隔N个数就以时钟值对m取模作为新的种子来产生新的序列。还有一种方法是直接将随机数加上时钟值再对m取模。