文章目录
随机性的起源
在训练深度神经网络的过程中,常见引发随机性、导致训练和推理不可复现的因素包括:
- 参数初始化:网络权重和偏置的初始值通常是随机生成的。
- 数据加载与打乱:训练数据在每个 epoch 前通常会被随机打乱(shuffle)。
- 小批量采样(Mini-batch Sampling):每次训练迭代时,从数据集中随机采样小批量数据。
- Dropout:训练时随机丢弃部分神经元以防止过拟合。
- 数据增强:如随机裁剪、旋转、翻转等操作会引入随机性。
- 多线程/多进程并行:数据加载和模型训练的并行实现可能导致操作顺序不确定。
- 硬件和库实现差异:不同硬件(如 GPU)或深度学习库的底层实现可能导致数值计算的微小差异。
为实现可复现性,通常需要设置随机种子,并尽量控制上述因素的随机行为。
接下来,我们对几种常见的造成随机性的因素进行描述。
模型权重初始化
模型权重初始化是指在神经网络训练开始前,为每个参数(权重和偏置)赋予初始值。通常,这些初始值是通过随机分布(如均匀分布或正态分布)生成的。由于每次随机生成的权重不同,即使网络结构和训练数据完全相同,模型的训练过程和最终结果也可能不同。这种由初始化带来的不确定性就是“随机性”。
主要影响
- 可能导致模型收敛到不同的局部最优解。
- 影响模型训练的速度和稳定性。
- 在实验复现时,如果不设置随机种子,结果可能无法完全一致。
常见做法
通过设置随机种子(如 torch.manual_seed(42)
或 np.random.seed(42)
)来控制初始化的随机性,提高实验可复现性。
代码示例
- 不使用随机种子
import torch
layer = torch.nn.Linear(2, 3)
print(layer.weight)
Parameter containing:
tensor([[-0.1860, -0.3759],
[ 0.0164, -0.1212],
[-0.2732, 0.1423]], requires_grad=True)
layer = torch.nn.Linear(2, 3)
print(layer.weight)
Parameter containing:
tensor([[-0.6760, 0.3505],
[-0.6525, -0.6014],
[-0.6918, 0.1477]], requires_grad=True)
- 使用随机种子
torch.manual_seed(123)
layer = torch.nn.Linear(2, 3)
print(layer.weight)
Parameter containing:
tensor([[-0.2883, 0.0234],
[-0.3512, 0.2667],
[-0.6025, 0.5183]], requires_grad=True)
torch.manual_seed(123)
layer = torch.nn.Linear(2, 3)
print(layer.weight)
Parameter containing:
tensor([[-0.2883, 0.0234],
[-0.3512, 0.2667],
[-0.6025, 0.5183]], requires_grad=True)
数据采集与重排(加载与打乱)
在训练和评测机器学习模型时,我们通常先将数据集划分为训练集和测试集。这一过程需要随机采样,以决定哪些样本被分配到训练集中,哪些样本被分配到测试集中。
在实践中,我们常常采用如k折交叉验证或留出法等模型评测技术。在留出法中,训练集会进一步划分为训练集、验证集和测试集,采样过程同样会受随机性影响。除非我们固定了随机种子,否则每次划分数据集,或用k折交叉验证进行模型调优及评测时,都会由于训练分区的不同得到有差异的模型。
数据采集与重排过程中的随机性,主要体现在以下几个方面:
-
随机采样(Random Sampling)
在将原始数据集划分为训练集、验证集和测试集时,通常会采用随机采样。这样可以确保每个子集都能代表整体数据的分布,减少因数据顺序或分布不均导致的偏差。例如,常见的做法是用train_test_split
(如在Python的scikit-learn库中)随机选取一定比例的数据作为训练集和测试集。 -
数据加载时的随机打乱(Shuffling)
在训练模型时,通常会在每个epoch开始前对训练数据进行随机打乱。这样可以防止模型记住数据的顺序,提高泛化能力。例如,PyTorch的DataLoader
和TensorFlow的tf.data.Dataset
都支持shuffle操作。 -
批次采样的随机性(Mini-batch Sampling)
在小批量梯度下降(mini-batch SGD)中,每个batch的数据通常是从训练集中随机采样得到的。这种随机性有助于优化过程跳出局部最优,提高训练效果。
例子
假设有一个包含1000条样本的数据集:
- 首先,使用随机采样将数据集划分为800条训练数据和200条测试数据。
- 然后,在每个训练epoch开始前,对800条训练数据进行随机打乱。
- 最后,每次训练时,从打乱后的训练集中随机采样出32条数据组成一个mini-batch。
注意事项
- 随机性可以通过设置随机种子(random seed)来保证实验的可复现性。
- 如果数据划分或打乱不随机,可能导致模型训练和评估结果不准确。
这种随机性是机器学习中提高模型泛化能力和评估公平性的关键步骤。
代码示例
- 不使用随机种子进行数据划分
import numpy as np
x_toydata = np.array([1., 2., 3., 4., 5., 6., 7., 8., 9.])
y_labels = np.array([ 0, 1, 0, 1, 1, 0, 0, 1, 0 ])
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
x_toydata, y_labels, test_size=0.3, shuffle=True, stratify=y_labels
)
print("X_train", X_train)
print("X_test", X_test)
X_train [1. 9. 5. 2. 3. 4.]
X_test [6. 7. 8.]
X_train, X_test, y_train, y_test = train_test_split(
x_toydata, y_labels, test_size=0.3, shuffle=True, stratify=y_labels
)
print("X_train", X_train)
print("X_test", X_test)
X_train [4. 5. 2. 3. 6. 7.]
X_test [8. 1. 9.]
- 使用随机种子进行数据划分
X_train, X_test, y_train, y_test = train_test_split(
x_toydata, y_labels, test_size=0.3, shuffle=True, stratify=y_labels,
random_state=123
)
print("X_train", X_train)
print("X_test", X_test)
X_train [9. 2. 8. 3. 5. 7.]
X_test [4. 1. 6.]
X_train, X_test, y_train, y_test = train_test_split(
x_toydata, y_labels, test_size=0.3, shuffle=True, stratify=y_labels,
random_state=123
)
print("X_train", X_train)
print("X_test", X_test)
X_train [9. 2. 8. 3. 5. 7.]
X_test [4. 1. 6.]
- 不使用随机种子的k折交叉验证
from sklearn.model_selection import StratifiedKFold
cv = StratifiedKFold(n_splits=3, shuffle=True)
for train_idx, valid_idx in cv.split(x_toydata, y_labels):
print("Feature values", x_toydata[train_idx])
Feature values [3. 4. 5. 6. 8. 9.]
Feature values [1. 2. 4. 5. 6. 7.]
Feature values [1. 2. 3. 7. 8. 9.]
for train_idx, valid_idx in cv.split(x_toydata, y_labels):
print("Feature values", x_toydata[train_idx])
Feature values [1. 2. 3. 4. 5. 6.]
Feature values [4. 5. 6. 7. 8. 9.]
Feature values [1. 2. 3. 7. 8. 9.]
- 使用随机种子的k折交叉验证
cv = StratifiedKFold(n_splits=3, random_state=123, shuffle=True)
for train_idx, valid_idx in cv.split(x_toydata, y_labels):
print("Feature values", x_toydata[train_idx])
Feature values [3. 4. 5. 6. 8. 9.]
Feature values [1. 2. 4. 5. 6. 7.]
Feature values [1. 2. 3. 7. 8. 9.]
cv = StratifiedKFold(n_splits=3, random_state=123, shuffle=True)
for train_idx, valid_idx in cv.split(x_toydata, y_labels):
print("Feature values", x_toydata[train_idx])
Feature values [3. 4. 5. 6. 8. 9.]
Feature values [1. 2. 4. 5. 6. 7.]
Feature values [1. 2. 3. 7. 8. 9.]
- 不使用随机种子的数据加载
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
train_dataset = datasets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)
train_loader = DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
for inputs, labels in train_loader:
pass
print(labels)
100.0%
100.0%
100.0%
100.0%
tensor([1, 6, 8, 1, 3, 6, 1, 2])
train_loader = DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
for inputs, labels in train_loader:
pass
print(labels)
tensor([0, 9, 1, 3, 3, 9, 9, 7])
- 使用随机种子的数据加载
import torch
torch.manual_seed(123)
train_loader = DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
for inputs, labels in train_loader:
pass
print(labels)
tensor([1, 8, 8, 7, 2, 5, 4, 1])
torch.manual_seed(123)
train_loader = DataLoader(dataset=train_dataset, batch_size=8, shuffle=True)
for inputs, labels in train_loader:
pass
print(labels)
tensor([1, 8, 8, 7, 2, 5, 4, 1])
非确定性算法
根据所选架构和超参数,我们可能会在模型中融入随机的组件和算法,其中一个广为人知的例子便是Dropout。
Dropout带来的随机性
Dropout 是一种在神经网络训练过程中常用的正则化技术。它的随机性体现在每次前向传播时,会以一定概率 p p p(如 p = 0.5 p=0.5 p=0.5,概率 p p p的取值范围一般是0.2到0.8)随机“丢弃”部分神经元(即将其输出设为 0),而不是每次都使用全部神经元。
这种做法的随机性主要体现在:
- 每次训练迭代时,丢弃的神经元集合都是随机的,不会固定某些神经元被丢弃。
- 同一个输入样本,每次经过 Dropout 层时激活的神经元可能不同,导致网络的输出也会有细微差异。
这种随机丢弃机制可以防止模型对某些神经元的过度依赖,从而提升模型的泛化能力,减少过拟合。训练完成后,推理阶段通常会关闭 Dropout(即全部神经元都参与计算),或者对输出进行缩放以补偿训练时的丢弃概率。
代码示例
import torch
class MLP(torch.nn.Module):
def __init__(self, num_features, num_classes):
super().__init__()
self.all_layers = torch.nn.Sequential(
torch.nn.Linear(num_features, 10),
torch.nn.ReLU(),
torch.nn.Dropout(0.5),
# output layer
torch.nn.Linear(10, num_classes),
)
def forward(self, x):
logits = self.all_layers(x)
return logits
torch.manual_seed(123)
model = MLP(num_features=5, num_classes=2)
- 训练过程中丢弃(Dropout)
x = torch.tensor([1., 0.3, 2.4, -1.1, -0.8])
model(x)
tensor([-0.1564, -0.2977], grad_fn=<ViewBackward0>)
model(x)
tensor([0.1359, 0.0523], grad_fn=<ViewBackward0>)
- 推理过程中禁用丢弃(Dropout)
model.eval();
model(x)
tensor([-0.0458, -0.1777], grad_fn=<ViewBackward0>)
model(x)
tensor([-0.0458, -0.1777], grad_fn=<ViewBackward0>)
注意:在推理(inference)阶段,建议配合使用 torch.no_grad()
或 torch.inference_mode()
上下文,以关闭梯度追踪,提高推理效率。(上面的示例未使用该上下文,是为了演示 .eval()
在推理时会禁用 Dropout。)
with torch.inference_mode():
print(model(x))
tensor([-0.0458, -0.1777])
不同运行时的算法
在深度学习中,即使设置了随机种子,模型在不同硬件(如CPU、GPU)或不同深度学习库(如PyTorch、TensorFlow)上运行时,仍可能出现结果不完全一致的情况。这主要是因为底层数值计算实现的差异,尤其是在卷积神经网络(CNN)中的卷积操作上表现得尤为明显。
卷积操作的实现差异
卷积操作可以有多种实现方式,例如直接法(direct convolution)、基于矩阵乘法的im2col方法、FFT加速、Winograd算法等。不同的硬件和库会根据性能优化选择不同的实现方式:
- GPU与CPU的实现不同:GPU通常采用高度并行的算法和库(如cuDNN),而CPU实现则可能更注重内存访问模式和缓存优化。
- 库版本差异:同一深度学习库的不同版本,或不同厂商的库(如cuDNN、MKL-DNN、OpenBLAS等)可能采用不同的数值精度、并行策略或近似算法。
- 非确定性操作:某些高性能实现(如cuDNN的某些卷积算法)为了加速计算,可能引入非确定性操作(如原子加法、并行归约顺序不固定),导致每次运行结果略有不同。
例子
以PyTorch为例,即使设置了随机种子,若在不同设备或不同cuDNN配置下运行同一段卷积代码,输出结果可能会有微小差异:
import torch
import torch.nn as nn
torch.manual_seed(42)
conv = nn.Conv2d(1, 1, 3, bias=False)
input = torch.randn(1, 1, 5, 5)
# 在CPU上
output_cpu = conv(input)
# 在GPU上
conv_cuda = conv.cuda()
input_cuda = input.cuda()
output_gpu = conv_cuda(input_cuda)
print(output_cpu)
print(output_gpu.cpu())
即使权重和输入完全相同,output_cpu
和 output_gpu
可能会有细微差别。
解决方法
- 可以通过设置库的确定性选项(如
torch.backends.cudnn.deterministic = True
或torch.use_deterministic_algorithms(True)
)来减少这种随机性,但可能会牺牲部分性能。 - 完全消除这种差异通常很难,尤其是在追求高性能的生产环境下。
小结
不同运行时和库的底层实现差异,尤其是在并行和数值精度处理上的不同,会导致即使随机种子一致,模型结果也可能略有不同。这种“随机性”本质上是由硬件和软件实现的非确定性引入的。
硬件与驱动程序
即使采用相同的算法,执行相同的操作,在不同硬件上训练的深度神经网络也可能因为微小的数值差异而产生不同的结果。这些差异有时归因于不同的浮点运算数值精度。但即使在相同的精度下,由于硬件和软件优化的不同,也可能出现细微的数值差异。
例如,不同的硬件平台可能会采用专门的优化方法或代码库,这些都可能对深度学习算法的行为产生细微影响。正如 NVIDIA 官方文档所述:“在不同架构之间,cuDNN 的各个例程不保证逐位复现性。例如,在 NVIDIA Volta™、NVIDIA Turing™ 和 NVIDIA Ampere 架构上运行同一例程时,不能保证结果逐位一致。”
随机性与生成式AI
在生成式AI(如文本生成、图像生成等)中,随机性是模型输出多样性和创造力的关键来源。即使输入和模型参数相同,每次生成的内容也可能不同,这主要归因于采样策略的随机选择。
生成式AI中的随机性
生成式模型(如GPT、Diffusion、GAN等)在推理阶段通常不会总是选择概率最高的下一个输出(如单词或像素),而是根据概率分布进行采样。这样可以避免模型每次都输出相同的内容,提升生成结果的多样性和自然性。常见的采样方法包括贪婪解码、随机采样、top-k采样和核采样(top-p采样)等。
Top-k采样
Top-k采样是一种限制采样空间的策略。在每一步生成时,模型会根据概率分布选出概率最高的k个候选(如k=50),然后只在这k个候选中按归一化概率进行随机采样。这样可以避免概率极低但偶尔被采样到的“奇怪”输出,同时保留一定的多样性。
优点:控制输出的合理性和多样性,减少低概率异常输出。
核采样(Top-p采样)
核采样(top-p sampling)是一种自适应的采样方法。与top-k不同,top-p不是固定数量的候选,而是选取累计概率达到阈值p(如p=0.9)的最小候选集。例如,按概率从高到低排序,依次累加,直到总和超过p为止,然后只在这些候选中采样。
优点:根据实际概率分布动态调整采样空间,既能避免低概率异常输出,又能在高不确定性时保留更多多样性。
小结
生成式AI的随机性主要体现在采样阶段。合理选择采样策略(如top-k、top-p)可以在输出的多样性和合理性之间取得平衡,是生成式模型应用中的重要调节手段。
总结
本文系统梳理了深度学习和生成式AI中随机性的主要来源,包括模型参数初始化、数据加载与打乱、mini-batch采样、Dropout等正则化方法,以及硬件和底层库实现的差异。文中通过代码示例展示了如何通过设置随机种子提升实验的可复现性,但也指出即使如此,不同硬件和库版本仍可能导致结果微小差异。此外,生成式AI中的采样策略(如top-k和top-p采样)也是随机性的重要体现。理解和合理控制这些随机性,有助于提升模型的可复现性、泛化能力和生成多样性,是机器学习实验和应用中的关键环节。