Pytorch可复现性

Pytorch结果可复现性问题

​ 在我比较不同的采样过程对评价指标的影响时,发现由于计算机的随机性对评价指标有着很大的影响,这种影响带来的不确定性在下游任务中大到根本分辨不出来哪种采样过程效果更好。这篇文章专门用于解决控制 Pytorch 中的随机性问题

随机性的产生:

  1. 非确定性操作: 有些操作在 GPU 上可能是非确定性的,即使设置了随机种子也无法完全确保结果的一致性。一些涉及CUDA加速的操作,比如某些优化算法,可能会引入不确定性
  2. 数据加载过程: 如果在数据加载过程中存在随机性,即使设置了随机种子,每次加载的数据顺序或样本可能仍然不同。确保你的数据加载过程是确定性的,或者在设置种子的同时使用相同的数据加载方式
  3. 多线程操作: 如果你的代码中使用了多线程或多进程,这可能导致并发操作的非确定性。确保在设置种子的同时,对多线程或多进程进行适当的同步和控制,以避免不一致的结果
  4. PyTorch版本差异: PyTorch 的不同版本可能会有微小的实现差异,可能会影响结果的一致性。确保在相同的PyTorch版本下运行代码

我在之前简单任务中使用 CPU 实验的时候,只需要设置:

1
2
3
4
def set_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)

就可以保证结果的可复现性了,但是如果在 GPU 上进行实验,GPU 会引入新的随机性,这就产生了新的问题,即在 GPU 上如何保证结果可复现性

在GPU上保证可复现性:

方法一:将线程数设置为0

​ GPU 引入随机性的原因来源于它的并行计算和 cuda 的随机优化算法,如果我们将它设置为 num_workers = 0 和禁止使用 cuda 随机优化,就可以保证结果可复现,但是这样运行速度就太慢了,失去了 GPU 的意义

方法二:设置cuda的种子

​ 事实上,GPU 也可以设置随机种子,我们需要把随机种子设置中加上一些 cuda 的种子和禁止 cuda 进行不可复现的算法:

1
2
3
4
5
6
7
8
9
10
def setup_seed(seed):
torch.manual_seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
np.random.seed(seed)
random.seed(seed)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.enabled = True

​ 这样也会导致运行速度很慢

方法三:使用Pytorch-lightning

​ Pytorch-lightning 是对 Pytorch 的封装,省去很多繁琐的中间过程,使用起来非常方便。为了确保可复现性,pl 提供了设置接口,非常简洁方便

1
2
3
4
from lightning.pytorch import Trainer, seed_everything
seed_everything(42, workers=True)
# sets seeds for numpy, torch and python.random.
trainer = Trainer(deterministic=True)

​ 设置 workers=True 会确保在dataloader中的多线程的可复现性, 并且确保各线程得到各自独特的不会重复的种子,避免因为相同的种子导致相同的数据增强

​ deterministic决定pytorch是否使用确定性算法,默认False, 设置True会使pytorch使用确定性算法

通过上述pl设置,主要有以下优点(重点!):

  • 保证了可复现的同时,没有牺牲任何训练速度
  • 设置非常简单方便

方法四:Pytorch检验可复现性

​ Pytorch 2.1 版本以后都支持 torch.use_deterministic_algorithms 这一检验可复现性的函数,它如果检验到在算法中存在不确定性就会弹出 RuntimeError 的错误,下面是一个例子:

1
2
3
4
5
6
7
8
9
10
import torch
# Set the seed for generating random numbers
torch.manual_seed(0)
# Enable deterministic algorithms
torch.use_deterministic_algorithms(True)
# Create a tensor with random values
x = torch.randn(3, 3)
# Perform an operation that is non-deterministic
y = x.view(-1)
# This will raise a RuntimeError because `view()` is non-deterministic

​ 如果需要使用 view() 函数,可以使用 torch.reshape() 函数来代替,因为 torch.reshape() 是确定性的,这是因为 view() 函数只适用于连续的张量,而reshape()函数可以用于连续和不连续的张量。这是因为 view() 函数的实现依赖于张量的步幅(stride),而步幅是在张量的存储中定义的。如果张量不是连续的,则步幅可能会发生变化,从而导致 view() 函数的结果不确定。相反,reshape() 函数不依赖于步幅,因此可以用于连续和不连续的张量,具有更好的鲁棒性