深度学习实验可复现问题

深度学习在训练过程中,由于随机初始化,样本读取的随机性,导致重复的实验结果会有差别,个别情况甚至波动较大。一般论文为了严谨,实验结论能够复现/可重复,通常采取固定随机种子使得结果确定。先给出我的完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 设置环境变量—— CUDA_LAUNCH_BLOCKING 、CUBLAS_WORKSPACE_CONFIG
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8"
# 从 config.json 配置文件中读取到 json 对象 config ,再从其中获取手动设置的随机数种子以及手动指定的运行显卡
seed = config["seed"]
device = config["cuda_device"]
# 设置 python 、numpy 、torch 的随机数种子
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)
# 如果显卡可以使用,就设置运行显卡、cudnn、cuda_seed 以及强制检测算法可复现性
if torch.cuda.is_available():
torch.cuda.set_device(device)
torch.backends.cudnn.benchmark = False
torch.backends.cudnn.deterministic = True
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.use_deterministic_algorithms(True)
# 读取数据集时设置 num_work = 0 ,可以实现可复现性,如果想多线程读取,那就要设置 seed_worker
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
np.random.seed(worker_seed)
random.seed(worker_seed)
g = torch.Generator()
g.manual_seed(seed)
test_loader = DataLoader(
test_set,
collate_fn=test_set.collate,
batch_size=config["batch_size"]["test"],
shuffle=config["shuffle"]["test"],
num_workers=config["num_workers"]["test"],
worker_init_fn=seed_worker,
generator=g,
)

接下来我们再来详细解释这段代码的意义。

随机种子设置

随机函数是最大的不确定性来源,影响了模型参数的随机初始化,样本的shuffle等等操作。

  • PyTorch 随机种子
  • python 随机种子
  • numpy 随机种子
1
2
3
4
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
os.environ["PYTHONHASHSEED"] = str(seed)

CPU版本下,上述随机种子设置完成之后,基本就可实现实验的可复现了。但是对于GPU版本,存在大量算法实现为不确定结果的算法,这种算法实现效率很高,但是每次返回的值会不完全一样。主要是由于浮点精度舍弃,不同浮点数以不同顺序相加,值可能会有很小的差异(小数点最末位)。此外,官方的文档提到,对于 RNN 类模型会因为 cuDNN 和 CUDA 的原因导致结果无法复现,可以通过设置环境变量来解决。

  • CUDA 10.1:设置环境变量 CUDA_LAUNCH_BLOCKING=1
  • CUDA 10.2 或者更高版本:设置环境变量 (注意两个冒号)CUBLAS_WORKSPACE_CONFIG=:16:8 或者 CUBLAS_WORKSPACE_CONFIG=:4096:2.
1
2
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8"

GPU算法确定性实现

GPU算法的不确定来源有两个

  • CUDA convolution benchmarking
  • Nondeterministic algorithms

CUDA convolution benchmarking:这个设置是为了提升运行效率,对模型参数试运行后,选取最优实现。不同硬件以及benchmarking本身存在噪音,导致不确定性。

Nondeterministic algorithms:GPU最大优势就是并行计算,如果能够忽略顺序,就避免了同步要求,能够大大提升运行效率,所以很多算法都有非确定性结果的算法实现。通过设置use_deterministic_algorithms,就可以使得pytorch选择确定性算法。

1
2
3
4
# 不需要benchmarking
torch.backends.cudnn.benchmark=False
# 选择确定性算法
torch.use_deterministic_algorithms(True)

RUNTIME ERROR

对于一个PyTorch 的函数接口,没有确定性算法实现,只有非确定性算法实现,同时设置了use_deterministic_algorithms(),那么会导致运行时错误。比如:

1
2
3
4
5
6
7
>>> import torch
>>> torch.use_deterministic_algorithms(True)
>>> torch.randn(2, 2).cuda().index_add_(0, torch.tensor([0, 1]), torch.randn(2, 2))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: index_add_cuda_ does not have a deterministic implementation, but you set
'torch.use_deterministic_algorithms(True)'. ...

错误原因:

index_add没有确定性的实现,出现这种错误,一般都是因为调用了torch.index_select 这个api接口,或者直接调用tensor.index_add_。

解决方案:

自己定义一个确定性的实现,替换调用的接口。对于torch.index_select 这个接口,可以有如下的实现。

1
2
3
4
5
6
7
8
def deterministic_index_select(input_tensor, dim, indices):
"""
input_tensor: Tensor
dim: dim
indices: 1D tensor
"""
tensor_transpose = torch.transpose(x, 0, dim)
return tensor_transpose[indices].transpose(dim, 0)

样本读取随机

  1. 多线程情况下,设置每个线程读取的随机种子
  2. 设置样本generator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 设置每个读取线程的随机种子
def seed_worker(worker_id):
worker_seed = torch.initial_seed() % 2**32
numpy.random.seed(worker_seed)
random.seed(worker_seed)

g = torch.Generator()
# 设置样本shuffle随机种子,作为DataLoader的参数
g.manual_seed(0)

DataLoader(
train_dataset,
batch_size=batch_size,
num_workers=num_workers,
worker_init_fn=seed_worker,
generator=g,
)

nn.Upsample

在我自己的实验中,上述设置全部配置完成后依然出现了无法复现的问题。幸运的是 torch.use_deterministic_algorithms(True) 这个设置进行了报错,经实验发现 nn.Upsample 中调用了 F.interpolate,其 bilinear 插值模式是 nondeterministic 的,只有默认的 nearest 模式是 deterministic 的,但是效果比前者差了很多很多。在训练中可以使用转置卷积进行上采样,但是在深度监督中使用这个方法太过奇怪,放到 CPU 中跑完再送回 GPU 又太过耗时,目前仍没有很好地解决方法,因此暂时选择通过多次重复实验选取中位数或者均值作为实验结果。