ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

【Bug已解决】Impact of using data shuffling in Pytorch dataloader 解决方案

【Bug已解决】Impact of using data shuffling in Pytorch dataloader 解决方案 【Bug已解决】Impact of using data shuffling in Pytorch dataloader 解决方案问题描述在 PyTorch 的DataLoader中shuffle参数控制是否在每个 epoch 开始时打乱数据顺序。很多开发者对 shuffle 的影响、何时使用、以及不使用会导致什么问题缺乏清晰的理解。常见的问题场景from torch.utils.data import DataLoader, TensorDataset import torch # 创建数据集 features torch.arange(20).reshape(10, 2).float() labels torch.tensor([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) # 前5个类别0后5个类别1 dataset TensorDataset(features, labels) # 不使用 shuffle dataloader_no_shuffle DataLoader(dataset, batch_size3, shuffleFalse) print(Without shuffle:) for batch_features, batch_labels in dataloader_no_shuffle: print(f Labels: {batch_labels.tolist()}) # Labels: [0, 0, 0] # Labels: [0, 0, 1] # Labels: [1, 1, 1] # 问题前几个batch全是类别0模型可能只学到输出0 # 使用 shuffle dataloader_shuffle DataLoader(dataset, batch_size3, shuffleTrue) print(\nWith shuffle:) for batch_features, batch_labels in dataloader_shuffle: print(f Labels: {batch_labels.tolist()}) # Labels: [1, 0, 1] -- 每个batch有混合的类别 # Labels: [0, 1, 0] # Labels: [1, 0, 1]常见困惑shuffleTrue和shuffleFalse对训练有什么影响训练集和测试集应该分别用什么设置不 shuffle 会导致什么问题shuffle 如何影响 SGD 优化器的性能如何实现自定义的 shuffle 策略错误复现import torch from torch.utils.data import DataLoader, TensorDataset # 复现1不 shuffle 导致类别分布不均 print( * 50) print(复现1不 shuffle 导致 batch 内类别不均) print( * 50) # 模拟有序数据前50个是类别0后50个是类别1 features torch.randn(100, 10) labels torch.cat([torch.zeros(50, dtypetorch.long), torch.ones(50, dtypetorch.long)]) dataset TensorDataset(features, labels) # 不 shuffle dataloader DataLoader(dataset, batch_size10, shuffleFalse) print(Without shuffle - batch label distributions:) for i, (_, batch_labels) in enumerate(dataloader): class_0 (batch_labels 0).sum().item() class_1 (batch_labels 1).sum().item() print(f Batch {i}: class_0{class_0}, class_1{class_1}) if i 2: break # Batch 0: class_010, class_10 -- 全是类别0 # Batch 1: class_010, class_10 -- 全是类别0 # Batch 2: class_010, class_10 -- 全是类别0# 复现2不 shuffle 导致训练不稳定 print(\n * 50) print(复现2不 shuffle 导致训练不稳定) print( * 50) import torch.nn as nn # 创建简单模型 model nn.Sequential( nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 2), ) criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lr0.01) # 不 shuffle 的训练 dataloader_no_shuffle DataLoader(dataset, batch_size10, shuffleFalse) losses_no_shuffle [] for epoch in range(3): epoch_loss 0 for batch_features, batch_labels in dataloader_no_shuffle: optimizer.zero_grad() output model(batch_features) loss criterion(output, batch_labels) loss.backward() optimizer.step() epoch_loss loss.item() losses_no_shuffle.append(epoch_loss / len(dataloader_no_shuffle)) print(fLosses without shuffle: {losses_no_shuffle}) # 可能出现先下降后上升的情况因为先学类别0再学类别1# 复现3shuffle 对 SGD 的影响 print(\n * 50) print(复现3shuffle 对 SGD 的影响) print( * 50) # 重新创建模型 model_shuffled nn.Sequential( nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 2), ) optimizer_shuffled torch.optim.SGD(model_shuffled.parameters(), lr0.01) # shuffle 的训练 dataloader_shuffled DataLoader(dataset, batch_size10, shuffleTrue) losses_shuffled [] for epoch in range(3): epoch_loss 0 for batch_features, batch_labels in dataloader_shuffled: optimizer_shuffled.zero_grad() output model_shuffled(batch_features) loss criterion(output, batch_labels) loss.backward() optimizer_shuffled.step() epoch_loss loss.item() losses_shuffled.append(epoch_loss / len(dataloader_shuffled)) print(fLosses with shuffle: {losses_shuffled}) # 通常更稳定持续下降根因分析1. shuffle 对 SGD 的影响随机梯度下降SGD及其变体Adam、RMSprop 等依赖于每个 batch 的梯度来更新参数。如果数据是有序的连续的 batch 可能包含高度相似的样本导致梯度方向偏差连续多个 batch 的梯度方向相似导致参数更新在某个方向上过度移动训练不稳定模型在一段时间内只看到某一类样本参数向该类优化然后突然切换到另一类陷入局部最优有序数据可能导致优化路径陷入较差的局部最优不 shuffle 的训练过程有序数据 Epoch 1: Batch 1: [类别0 x10] - 梯度方向A - 参数向A方向更新 Batch 2: [类别0 x10] - 梯度方向A - 参数继续向A方向更新 ... Batch 6: [类别1 x10] - 梯度方向B - 参数突然向B方向更新 Batch 7: [类别1 x10] - 梯度方向B - 参数继续向B方向更新 问题参数在A和B之间来回震荡难以收敛 shuffle 的训练过程 Epoch 1: Batch 1: [类别0 x5, 类别1 x5] - 梯度方向C混合 Batch 2: [类别0 x6, 类别1 x4] - 梯度方向D混合 ... 优势每个 batch 都包含混合类别梯度方向更稳定2. shuffle 的数学解释从优化理论的角度SGD 的收敛性依赖于梯度的无偏估计E[∇L_batch] ≈ ∇L_full 其中 L_batch 是 batch 上的损失L_full 是整个数据集上的损失 如果数据有序 - 连续 batch 的梯度高度相关 - E[∇L_batch] 在一段时间内偏离 ∇L_full - 收敛速度变慢甚至不收敛 如果数据 shuffle - 每个 batch 是随机抽样 - E[∇L_batch] 更接近 ∇L_full - 收敛更稳定3. shuffle 的适用场景场景shuffle 设置原因训练集shuffleTrue打破数据顺序提高训练稳定性验证集shuffleFalse验证结果需要可复现测试集shuffleFalse测试结果需要可复现时序数据shuffleFalse保持时间顺序在线学习shuffleTrue随机化数据流4. shuffle 的实现机制# DataLoader 的 shuffle 实现 # 每个 epoch 开始时生成一个随机排列索引 # 然后按这个索引顺序提取数据 import torch # 模拟 shuffle 的过程 data torch.arange(10) print(fOriginal: {data}) # Epoch 1 indices1 torch.randperm(10) print(fEpoch 1 indices: {indices1}) print(fEpoch 1 data: {data[indices1]}) # Epoch 2不同的排列 indices2 torch.randperm(10) print(fEpoch 2 indices: {indices2}) print(fEpoch 2 data: {data[indices2]})解决方案方案一正确设置 shuffle 参数import torch from torch.utils.data import DataLoader, TensorDataset # 创建数据集 features torch.randn(200, 10) labels torch.randint(0, 5, (200,)) dataset TensorDataset(features, labels) # 训练集shuffleTrue train_loader DataLoader( dataset, batch_size32, shuffleTrue, # 训练时打乱 num_workers4, pin_memoryTrue, ) # 验证/测试集shuffleFalse val_loader DataLoader( dataset, batch_size32, shuffleFalse, # 验证时不打乱 ) # 验证 shuffle 效果 print(Training batches (shuffled):) for i, (_, batch_labels) in enumerate(train_loader): if i 3: print(f Batch {i}: {batch_labels[:10].tolist()}) else: break print(\nValidation batches (not shuffled):) for i, (_, batch_labels) in enumerate(val_loader): if i 3: print(f Batch {i}: {batch_labels[:10].tolist()}) else: break方案二可复现的 shuffleimport torch from torch.utils.data import DataLoader, TensorDataset def set_seed(seed: int): 设置随机种子以确保可复现 torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 使用 generator 控制 shuffle 的随机性 g torch.Generator() g.manual_seed(42) dataset TensorDataset(torch.randn(100, 10), torch.randint(0, 5, (100,))) # 使用相同的 generator确保每次运行结果一致 dataloader DataLoader(dataset, batch_size10, shuffleTrue, generatorg) print(Reproducible shuffle:) for epoch in range(2): g.manual_seed(42) # 每个 epoch 重置种子 print(f\nEpoch {epoch}:) for i, (_, labels) in enumerate(dataloader): if i 2: print(f Batch {i}: {labels.tolist()})方案三自定义 Sampler 实现分层 shuffleimport torch from torch.utils.data import Sampler, DataLoader, TensorDataset from typing import Iterator class StratifiedSampler(Sampler): 分层采样器保持每个 batch 中类别比例 def __init__(self, labels, batch_size, seed42): self.labels labels self.batch_size batch_size self.seed seed self.epoch 0 # 获取每个类别的索引 self.class_indices {} for cls in labels.unique(): self.class_indices[cls.item()] (labels cls).nonzero(as_tupleTrue)[0] self.n_samples len(labels) def __iter__(self) - Iterator[int]: # 设置随机种子 g torch.Generator() g.manual_seed(self.seed self.epoch) # 计算每个类别在 batch 中的比例 indices [] for cls, cls_indices in self.class_indices.items(): # 随机打乱该类别的索引 perm cls_indices[torch.randperm(len(cls_indices), generatorg)] indices.append(perm) # 交错排列各类别的索引 all_indices [] max_len max(len(idx) for idx in indices) for i in range(max_len): for cls_indices in indices: if i len(cls_indices): all_indices.append(cls_indices[i].item()) self.epoch 1 return iter(all_indices) def __len__(self) - int: return self.n_samples # 使用示例 features torch.randn(100, 10) labels torch.cat([torch.zeros(60, dtypetorch.long), torch.ones(40, dtypetorch.long)]) dataset TensorDataset(features, labels) # 使用分层采样 sampler StratifiedSampler(labels, batch_size10) dataloader DataLoader(dataset, batch_size10, samplersampler) print(Stratified sampling:) for i, (_, batch_labels) in enumerate(dataloader): class_0 (batch_labels 0).sum().item() class_1 (batch_labels 1).sum().item() print(f Batch {i}: class_0{class_0}, class_1{class_1}) if i 4: break方案四对比 shuffle 和不 shuffle 的训练效果import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset def create_model(): return nn.Sequential( nn.Linear(10, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 5), ) def train_model(dataloader, num_epochs20, lr0.01): model create_model() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lrlr) losses [] for epoch in range(num_epochs): epoch_loss 0 for batch_features, batch_labels in dataloader: optimizer.zero_grad() output model(batch_features) loss criterion(output, batch_labels) loss.backward() optimizer.step() epoch_loss loss.item() losses.append(epoch_loss / len(dataloader)) return losses # 创建有序数据集 features torch.randn(200, 10) labels torch.cat([torch.zeros(40, dtypetorch.long), torch.ones(40, dtypetorch.long), torch.full((40,), 2, dtypetorch.long), torch.full((40,), 3, dtypetorch.long), torch.full((40,), 4, dtypetorch.long)]) dataset TensorDataset(features, labels) # 不 shuffle loader_no_shuffle DataLoader(dataset, batch_size20, shuffleFalse) losses_no_shuffle train_model(loader_no_shuffle) # shuffle loader_shuffle DataLoader(dataset, batch_size20, shuffleTrue) losses_shuffle train_model(loader_shuffle) # 对比 print(Epoch | No Shuffle | Shuffle) print(- * 35) for i in range(0, 20, 4): print(f {i1:3d} | {losses_no_shuffle[i]:.4f} | {losses_shuffle[i]:.4f}) print(f\nFinal loss - No shuffle: {losses_no_shuffle[-1]:.4f}) print(fFinal loss - Shuffle: {losses_shuffle[-1]:.4f})完整修复代码 完整修复代码DataLoader shuffle 的影响和正确使用 实现对比实验和自定义采样策略 import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset, Sampler from typing import Iterator, List import copy class ExperimentRunner: 实验运行器对比 shuffle 和不 shuffle 的效果 staticmethod def create_ordered_dataset(n_samples200, n_features10, n_classes5): 创建有序数据集类别按顺序排列 features torch.randn(n_samples, n_features) samples_per_class n_samples // n_classes labels torch.cat([ torch.full((samples_per_class,), i, dtypetorch.long) for i in range(n_classes) ]) return TensorDataset(features, labels) staticmethod def create_model(input_dim10, hidden_dim64, num_classes5): return nn.Sequential( nn.Linear(input_dim, hidden_dim), nn.ReLU(), nn.Linear(hidden_dim, hidden_dim // 2), nn.ReLU(), nn.Linear(hidden_dim // 2, num_classes), ) staticmethod def train_model(dataloader, num_epochs30, lr0.01, seed42): 训练模型并返回损失历史 torch.manual_seed(seed) model ExperimentRunner.create_model() criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lrlr) losses [] accuracies [] for epoch in range(num_epochs): model.train() epoch_loss 0 correct 0 total 0 for batch_features, batch_labels in dataloader: optimizer.zero_grad() output model(batch_features) loss criterion(output, batch_labels) loss.backward() optimizer.step() epoch_loss loss.item() _, predicted output.max(1) correct predicted.eq(batch_labels).sum().item() total batch_labels.size(0) losses.append(epoch_loss / len(dataloader)) accuracies.append(100. * correct / total) return losses, accuracies staticmethod def run_comparison(n_samples200, batch_size20, num_epochs30): 运行对比实验 print( * 60) print(Shuffle vs No-Shuffle Comparison) print( * 60) dataset ExperimentRunner.create_ordered_dataset(n_samples) # 不 shuffle loader_no_shuffle DataLoader(dataset, batch_sizebatch_size, shuffleFalse) losses_no, accs_no ExperimentRunner.train_model(loader_no_shuffle, num_epochs) # shuffle loader_shuffle DataLoader(dataset, batch_sizebatch_size, shuffleTrue) losses_yes, accs_yes ExperimentRunner.train_model(loader_shuffle, num_epochs) # 打印结果 print(f\n{Epoch:5} | {No Shuffle Loss:15} | {Shuffle Loss:12} | {No Shuffle Acc:14} | {Shuffle Acc:11}) print(- * 70) for i in range(0, num_epochs, 5): print(f{i1:5d} | {losses_no[i]:15.4f} | {losses_yes[i]:12.4f} | {accs_no[i]:13.1f}% | {accs_yes[i]:10.1f}%) print(f\n{Final:5} | {losses_no[-1]:15.4f} | {losses_yes[-1]:12.4f} | {accs_no[-1]:13.1f}% | {accs_yes[-1]:10.1f}%) return losses_no, losses_yes, accs_no, accs_yes class BatchDistributionAnalyzer: 分析 batch 内类别分布 staticmethod def analyze(dataloader, name): 分析 dataloader 中每个 batch 的类别分布 print(f\n--- {name} Batch Distribution ---) for i, (_, labels) in enumerate(dataloader): unique, counts labels.unique(return_countsTrue) dist {u.item(): c.item() for u, c in zip(unique, counts)} print(f Batch {i}: {dist}) if i 4: print(f ... ({len(dataloader)} total batches)) break def test_shuffle_impact(): 测试 shuffle 的影响 print( * 60) print(Testing Shuffle Impact) print( * 60) # 创建有序数据集 dataset ExperimentRunner.create_ordered_dataset(200, n_classes5) # 分析 batch 分布 loader_no DataLoader(dataset, batch_size20, shuffleFalse) loader_yes DataLoader(dataset, batch_size20, shuffleTrue) BatchDistributionAnalyzer.analyze(loader_no, No Shuffle) BatchDistributionAnalyzer.analyze(loader_yes, Shuffle) # 运行对比实验 losses_no, losses_yes, accs_no, accs_yes ExperimentRunner.run_comparison( n_samples200, batch_size20, num_epochs30 ) # 分析结果 print(\n * 60) print(Analysis) print( * 60) # 损失变化的标准差稳定性指标 loss_std_no torch.tensor(losses_no).std().item() loss_std_yes torch.tensor(losses_yes).std().item() print(f\nLoss std (stability):) print(f No shuffle: {loss_std_no:.4f}) print(f Shuffle: {loss_std_yes:.4f}) # 最终准确率 print(f\nFinal accuracy:) print(f No shuffle: {accs_no[-1]:.1f}%) print(f Shuffle: {accs_yes[-1]:.1f}%) # 收敛速度达到80%准确率的epoch def find_convergence_epoch(accuracies, threshold80): for i, acc in enumerate(accuracies): if acc threshold: return i 1 return len(accuracies) conv_no find_convergence_epoch(accs_no) conv_yes find_convergence_epoch(accs_yes) print(f\nEpochs to reach 80% accuracy:) print(f No shuffle: {conv_no if conv_no 30 else 30}) print(f Shuffle: {conv_yes if conv_yes 30 else 30}) print(\n * 60) print(Conclusion: Shuffle generally leads to:) print( 1. More stable training (lower loss std)) print( 2. Faster convergence) print( 3. Better final accuracy) print( * 60) if __name__ __main__: test_shuffle_impact()常见陷阱与注意事项1. 训练集和测试集的 shuffle 设置# 正确训练集 shuffleTrue测试集 shuffleFalse train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse) # 错误测试集也 shuffle test_loader DataLoader(test_dataset, batch_size32, shuffleTrue) # 问题测试结果不可复现难以分析哪些样本预测错误2. shuffle 与 Sampler 的冲突# 不能同时使用 shuffleTrue 和自定义 sampler try: DataLoader(dataset, batch_size32, shuffleTrue, samplercustom_sampler) except ValueError as e: print(fError: {e}) # sampler is incompatible with shuffle # 正确使用 sampler 时设置 shuffleFalse DataLoader(dataset, batch_size32, shuffleFalse, samplercustom_sampler)3. 时序数据不应 shuffle# 时序数据如时间序列预测不应 shuffle # 因为需要保持时间顺序 timeseries_dataset TimeSeriesDataset(data, seq_len10) # 正确shuffleFalse dataloader DataLoader(timeseries_dataset, batch_size32, shuffleFalse)4. shuffle 的随机种子# 默认情况下每次运行 shuffle 结果不同 dataloader DataLoader(dataset, batch_size32, shuffleTrue) # 要确保可复现需要设置随机种子 torch.manual_seed(42) dataloader DataLoader(dataset, batch_size32, shuffleTrue) # 或使用 generator g torch.Generator() g.manual_seed(42) dataloader DataLoader(dataset, batch_size32, shuffleTrue, generatorg)5. num_workers 与 shuffle# 使用多 worker 时每个 worker 有不同的随机种子 dataloader DataLoader(dataset, batch_size32, shuffleTrue, num_workers4) # 确保 worker 间的随机性不同 def worker_init_fn(worker_id): torch.manual_seed(42 worker_id) dataloader DataLoader( dataset, batch_size32, shuffleTrue, num_workers4, worker_init_fnworker_init_fn )总结DataLoader的shuffle参数对模型训练有重要影响。理解其作用机制和正确使用方法对于构建高效的训练流程至关重要。关键要点训练集使用shuffleTrue打破数据顺序使每个 batch 包含混合类别提高训练稳定性和收敛速度。验证/测试集使用shuffleFalse确保评估结果可复现便于分析模型性能。不 shuffle 的问题有序数据导致连续 batch 高度相似梯度方向偏差训练不稳定。SGD 依赖 shuffleSGD 及其变体假设 batch 是随机抽样shuffle 使梯度估计更无偏。可复现性使用generator或设置随机种子确保 shuffle 的可复现性。自定义采样使用Sampler实现分层采样等自定义策略。时序数据例外时间序列等需要保持顺序的数据不应 shuffle。最佳实践训练集始终使用shuffleTrue测试/验证集使用shuffleFalse需要可复现时设置随机种子类别不平衡时考虑分层采样时序数据不 shuffle使用generator控制随机性通过本文的详细分析和完整代码示例你应该能够彻底理解 DataLoader shuffle 的影响在实际项目中正确配置数据加载策略提高模型训练的效率和稳定性。
返回列表