【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第五章 10 :数据集【AI Infra 3.0】[PyTorch Scala 硕士研一课程]

张开发
2026/4/14 20:09:10 15 分钟阅读

分享文章

【Scala PyTorch深度学习】PyTorch On Scala 系列课程 第五章 10 :数据集【AI Infra 3.0】[PyTorch Scala 硕士研一课程]
PyTorch Scala 高校计算机 硕士研一课程章节 5: 高效数据处理既然你已经了解如何使用torch.nn构建模型以及如何用 Autograd 计算梯度下一步就是有效地为这些模型提供数据。处理大型数据集、进行必要的预处理以及在不耗尽内存的情况下分批加载数据是深度学习工作中常见的问题。本章将讲解 PyTorch 管理数据流程的方案即torch.utils.data模块。你将学习如何使用Dataset类来组织数据。使用预设数据集例如torchvision中提供的。使用torchvision.transforms进行数据转换和增强。使用DataLoader类高效地分批加载数据、打乱数据并可能并行加载。学完本章后你将能够为你的 PyTorch 项目构建高效的数据流程。对专用数据加载器的需求训练深度学习模型需要处理大量数据。虽然使用torch.nn构建模型和使用 Autograd 计算梯度是基本步骤但一个实际问题随之而来如何在训练期间高效地将数据输入这些模型如果尝试手动处理数据加载会遇到以下挑战内存限制: 现代数据集特别是在计算机视觉或自然语言处理等方面可能非常庞大常常超出可用内存RAM更不用说 GPU 上的显存VRAM。一次性将整个数据集加载到内存中通常是不可行的。想象一下尝试将整个 ImageNet 数据集超过 1400 万张图像数百 GB直接加载到计算机的 RAM 中——对于大多数系统来说这根本无法容纳。I/O 瓶颈: 从磁盘读取数据的速度比 CPU 或 GPU 上的计算慢几个数量级。如果模型需要数据时你逐个加载数据样本你速度极快的 GPU 将大部分时间处于空闲状态等待下一批数据到来。这种顺序磁盘读取会成为一个主要瓶颈极大地减缓训练过程。低效的预处理: 数据很少以神经网络所需的精确格式存在。它通常需要预处理步骤例如归一化、调整大小、数据类型转换或数据增强随机修改样本以提高模型泛化能力。与主要训练过程同步地逐个样本执行这些转换会增加进一步的延迟。洗牌需求: 为确保模型泛化能力并防止与数据顺序相关的偏差标准做法是在每个训练周期前对数据集进行洗牌。实现高效的洗牌特别是对于无法完全放入内存的数据集会增加复杂性。批处理: 神经网络通常在数据的小批量上进行训练而不是单个样本。分批处理数据可以获得更稳定的梯度估计并更好地使用 GPU 的并行处理能力。手动创建这些批次确保它们的格式正确以及处理最后一个可能较小的批次都需要仔细编写代码。并行处理: 为克服 I/O 瓶颈高效的数据加载管道通常使用多个工作进程并行加载和预处理数据在 GPU 忙于处理当前批次时准备未来的批次。正确实现这种并行管理进程并确保数据完整性是一项复杂的工程任务。为每个项目从头解决所有这些问题会非常耗时且容易出错。你每次都相当于在重建一个重要的基础设施部分。简化版朴素加载PyTorch DataLoader 方法大型数据集(磁盘)加载 处理样本 (CPU)慢速读取将样本移至 GPUI/O 和 CPU 限制GPU 常空闲在样本上训练 (GPU)等待下一批减缓训练大型数据集(磁盘)DataLoader(并行工作器,批处理, 洗牌)已准备的批次(RAM)预取将批次移至 GPU在批次上训练(GPU 忙碌)请求下一批比较了导致瓶颈的朴素顺序数据加载方法与 PyTorch 数据工具提供的并行批处理方法。认识到这些常见且重要的挑战PyTorch 提供了torch.utils.data模块。这个模块提供专用工具专门用于构建高效、灵活和并行的数据加载管道。它封装了洗牌、批处理、内存管理和并行加载的复杂性让你能专注于定义数据集结构和所需的转换。通过使用 PyTorch 的Dataset和DataLoader类我们将在后续章节中介绍你将获得效率: 优化数据获取和预处理通常在 CPU 核心间并行执行确保 GPU 获得充足数据。内存管理: 通过仅在需要时将必要的批次加载到内存中来处理大型数据集。灵活性: 轻松集成自定义数据源和复杂的预处理/数据增强步骤。简洁性: 用于与数据集交互和创建数据迭代器的标准化 API。这些工具是使用 PyTorch 构建实际深度学习应用的基本组成部分。让我们从Dataset类开始了解它们如何工作。使用torch.utils.data.Dataset高效加载和处理数据对训练深度学习模型非常重要。PyTorch 提供了一种通过其torch.utils.data.Dataset抽象类来处理数据集的标准化方式。可以把Dataset看作一个约定它定义了访问数据的标准接口无论数据是存在内存中、磁盘上还是需要即时生成。Dataset抽象类其核心是torch.utils.data.Dataset是一个表示数据集的抽象类。你在 PyTorch 中创建的任何自定义数据集都应该继承自这个类。为什么要使用这种结构它确保了不同的数据集无论是内置的还是自定义的都能向其他 PyTorch 组件提供一致的 API最值得一提的是DataLoader我们稍后会讲到。这种标准化简化了在相同训练代码中替换数据集或使用不同数据源的过程。要创建自己的自定义数据集你需要继承torch.utils.data.Dataset并重写两个必要的方法__len__(self): 这个方法应该返回数据集中样本的总数。DataLoader使用它来确定数据集的大小。__getitem__(self, idx): 这个方法负责根据给定索引idx从数据集中加载并返回一个样本。这是实际数据加载逻辑所在的地方例如读取图像文件、从 CSV 获取一行数据、访问列表中的元素。DataLoader会重复调用此方法来构建批次。让我们用一个简单的例子来说明这一点。假设你的特征和对应的标签存储在 Python 列表或 NumPy 数组中。importtorch.*importtorch.utils.data.Datasetimportnumpyas npclassSimpleCustomDatasetextendsDataset:一个带有特征和标签的简单数据集示例。def__init__(features,labels): 参数: features (列表或 np.array): 特征的列表或数组。 labels (列表或 np.array): 标签的列表或数组。 # 基本检查特征和标签必须长度相同 assert len(features)len(labels),特征和标签的长度必须相同。self.featuresfeaturesself.labelslabelsdef__len__(self):返回样本总数。returnlen(self.features)def__getitem__(self,idx): 生成一个数据样本。 参数: idx (int): 元素的索引。 返回: tuple: 给定索引对应的 (特征, 标签)。 //获取给定索引的特征和标签featureself.features[idx]labelself.labels[idx]//通常你会在Dataloader中将数据转换为 PyTorch 张量// 我们假设特征/标签可能还不是张量sample(torch.tensor(feature,dtypetorch.float32),torch.tensor(label,dtypetorch.long))// 假设是分类标签returnsample// --- 示例用法 ---// 样本数据请替换为你的实际数据valnum_samples100valnum_features10valfeatures_datanp.random.randn(num_samples,num_features)vallabels_datanp.random.randint(0,5,sizenum_samples)// 示例5 个类别// 创建自定义数据集实例valmy_datasetSimpleCustomDataset(features_data,labels_data)// 访问数据集属性和元素println(s数据集大小:${len(my_dataset)})// 获取第一个样本valfirst_samplemy_dataset[0]valfeature_samplefirst_sample._1vallabel_samplefirst_sample._2 println(s\n第一个样本特征:\n$feature_sample)println(s第一个样本形状:${feature_sample.shape})println(s第一个样本标签:$label_sample)// 获取第十个样本valtenth_samplemy_dataset[9]valtenth_feature_sampletenth_sample._1valtenth_label_sampletenth_sample._2 println(s\n第十个样本特征:\n$tenth_feature_sample)println(s第十个样本形状:${tenth_feature_sample.shape})println(s第十个样本标签:$tenth_label_sample)在此示例中__init__方法存储在实例化时传入的特征和标签数据。__len__简单地返回特征列表的长度这与标签列表的长度相同。__getitem__接受一个索引idx获取对应的特征和标签将它们转换为 PyTorch 张量并以元组形式返回。这种转换为张量的操作在__getitem__中很常见。处理更复杂的场景自定义Dataset的真正作用体现在处理那些不能直接在内存中获取的数据时。例如你的图像文件路径和标签可能存储在一个 CSV 文件中。importtorch.*importtorch.utils.data.Dataset from PILimportImage// Python 图像库用于图像加载importpandasas pdimportosclassImageFilelistDatasetextendsDataset:用于从 CSV 文件加载图像路径和标签的数据集。def__init__(self,csv_file,root_dir,transformNone): 参数: csv_file (字符串): 包含标注的 CSV 文件路径。 假设列有image_path, label root_dir (字符串): 包含所有图像的目录。 transform (可调用, 可选): 可选的数据变换用于对样本进行处理。 应用于样本。 valannotationspd.read_csv(csv_file)valroot_dirroot_dirvaltransformtransform// 我们稍后会讨论数据变换def__len__(self):returnlen(self.annotations)def__getitem__(self,idx):// 从 CSV 获取相对于 root_dir 的图像路径valimg_rel_pathannotations.iloc[idx,0]// 假设第一列是路径valimg_full_pathos.path.join(root_dir,img_rel_path)//使用 PIL 加载图像try:imageImage.open(img_full_path).convert(RGB)# 确保有3个通道 except FileNotFoundError:println(f错误未在 {img_full_path} 找到图像)// 适当处理错误例如返回 None 或抛出异常// 为简单起见这里我们将返回 None并依赖 DataLoader 的 collate_fn// 来处理它或稍后过滤。一个更好的方法// 可能是事先清理 CSV 文件。returnNone,None # 返回 None 值// 从 CSV 获取标签vallabelannotations.iloc[idx,1]// 假设第二列是标签vallabel_tensortorch.tensor(int(label),dtypetorch.long)// 如果有应用数据变换iftransform then imagetransform(image)// 数据变换通常会将 PIL 图像转换为张量// 如果没有提供将图像转换为张量的数据变换则手动转换ifnot isinstance(image,torch.Tensor):# 如果没有应用其他数据变换进行基本转换 imagetorch.tensor(np.array(image),dtypetorch.float32).permute(2,0,1)/255.0returnimage,label// --- 示例用法需要实际图像和 CSV---// 假设你拥有// 1. 文件夹 data/images/ 包含图像文件例如cat1.jpg, dog1.png// 2. CSV 文件 data/annotations.csv内容如下// image_path,label// images/cat1.jpg,0// images/dog1.png,1// ...// 3. 确保 CSV 文件中没有空行或额外空格// 4. 图像路径列中的值应与实际图像文件匹配不区分大小写// 访问方式类似// print(f图像数据集大小: {len(image_dataset)})// if len(image_dataset) 0:// img, lbl image_dataset[0]// if img is not None:// println(f第一个图像形状: {img.shape}) // 形状取决于数据变换// println(f第一个图像标签: {lbl})在这个ImageFilelistDataset示例中__init__使用 pandas 读取 CSV 文件并存储文件路径和根目录。它还接受一个可选的transform参数我们很快会看到它的用法。__len__返回 CSV 文件中的行数。__getitem__构建完整的图像路径使用 PIL 加载图像获取标签应用任何指定的数据变换确保图像是一个张量并返回图像张量和标签张量。请注意Dataset本身只定义了如何获取单个项目。它不会一次性将整个数据集加载到内存中除非你的__init__明确这样做但这对于大型数据集通常是避免的。它也不处理批处理、打乱或并行加载。DataLoader便是为此而生它直接建立在Dataset提供的结构之上。通过实现__len__和__getitem__你为DataLoader高效访问数据样本提供了必要的结构。内置数据集例如TorchVision尽管创建自定义Dataset类为您的特定数据提供了最大的灵活性但许多深度学习任务特别是在研究和基准测试中使用标准化数据集。手动准备这些数据集涉及下载、解压、组织文件和编写解析逻辑这可能既耗时又容易出错。幸运的是PyTorch 提供了配套库可以简化常见领域的数据处理过程。对于计算机视觉torchvision包是一个不可或缺的工具。它不仅包含流行的数据集还包含预训练模型和常用的图像转换函数。本节主要介绍如何访问和使用torchvision.datasets提供的数据集。使用torchvision.datasets访问数据集torchvision.datasets模块提供了对许多广泛使用的计算机视觉数据集的便捷访问例如 MNIST、Fashion MNIST、CIFAR 10/100、ImageNet、COCO 等。使用这些数据集很简单。通常您会从torchvision.datasets导入特定的数据集类并实例化它。让我们看一个使用 CIFAR 10 数据集的例子它包含 60,000 张 32x32 彩色图像分为 10 个类别。importtorchvisionimporttorchvision.transformsas transforms// 定义一个简单的转换将图像转换为 PyTorch 张量valtransformtransforms.Compose([transforms.ToTensor()])// 加载训练数据集// root: 数据将被存储/查找的目录// trainTrue: 指定训练集// downloadTrue: 如果本地未找到数据则下载// transform: 将定义的转换应用于每张图像valtrain_datasettorchvision.datasets.CIFAR10(root./data,traintrue,downloadtrue,transformtransform)// 加载测试数据集// root: 数据将被存储/查找的目录// trainFalse: 指定测试集// downloadTrue: 如果本地未找到数据则下载// transform: 将定义的转换应用于每张图像valtest_datasettorchvision.datasets.CIFAR10(root./data,trainfalse,downloadtrue,transformtransform)// 打印数据集大小println(fCIFAR-10 training dataset size: {len(train_dataset)})println(fCIFAR-10 test dataset size: {len(test_dataset)})// 访问单个数据点图像、标签valimgtrain_dataset[0]._1 println(fImage shape: {img.shape})// 通常输出torch.Size([3, 32, 32])println(fLabel: {train_dataset[0]._2})// 输出表示类别的整数当您首次运行此代码时torchvision会检查指定的root目录在本例中为./data。如果 CIFAR 10 数据不存在设置downloadTrue会指示torchvision自动将数据集下载并解压到该目录中。后续运行将发现数据已存在于本地并跳过下载。注意transform参数。您可以在此处指定数据预处理步骤这些步骤在数据样本加载后但在__getitem__返回之前应用于每个样本。我们使用了transforms.ToTensor()它将 PIL 图像格式torchvision数据集常用转换为 PyTorch 张量。数据转换将在下一节中进行更详细的介绍。结构与使用重要的是torchvision.datasets返回的对象如上文的train_dataset和test_dataset是继承自torch.utils.data.Dataset的类实例。这意味着它们实现了必需的__len__和__getitem__方法使其与 PyTorch 的DataLoader完全兼容。len(train_dataset)返回数据集中样本的总数。train_dataset[i]返回第 ii个样本通常是一个元组(data, target)其中data是预处理后的输入例如图像张量target是对应的标签或标注。以下是 CIFAR-10 训练集中类别分布的简单可视化CIFAR-10 数据集是平衡的每个类别恰好有 5,000 张训练图像。在计算机视觉中尽管torchvision最为完善但其他领域也存在类似的库torchaudio: 为音频处理任务提供数据集如 SpeechCommands、LJSpeech 等、模型和转换功能。torchtext: 为自然语言处理提供数据集如 IMDb 情感分析、WikiText 语言建模、分词器和词汇工具。注意torchtext经历了重大的 API 变更因此请查阅其文档以了解当前的使用模式。使用这些库遵循相似的原则导入所需的数据集类实例化它通常带有下载和预处理选项然后将生成的Dataset对象与DataLoader一起使用。依靠这些内置数据集可以显著加快开发和实验速度使您能够专注于模型架构和训练而不是数据获取和准备尤其是在使用标准基准时。请记住这些数据集对象直接与本章后面讨论的DataLoader集成从而实现高效的批处理和洗牌。数据变换 (torchvision.transforms)原始数据如图像或文本很少直接以完美适合神经网络输入的格式出现。模型通常需要特定大小和分布的数值张量。此外为了提高模型的泛化能力并防止过拟合通常的做法是通过对现有数据应用随机修改来人工扩充训练数据集。这就是数据变换的作用。PyTorch特别是通过用于计算机视觉任务的torchvision库提供了一个便捷的模块torchvision.transforms它包含多种常用操作这些操作可以链式组合以创建数据处理流程。这些变换主要有两个作用预处理标准化数据格式、比例和大小。数据增强对训练数据应用随机改动以增加其多样性。让我们看一些基础变换。常用预处理变换这些变换通常应用于所有数据集划分训练集、验证集和测试集以确保一致性。transforms.ToTensor()这通常是对使用PILPython图像库或NumPy等库加载的图像数据最先应用的变换之一。它将PIL图像或NumPy数组格式为高 x 宽 x 通道转换为PyTorchFloatTensor格式为通道 x 高 x 宽。重要的是它还将像素值从 [0, 255] 范围缩放到 [0.0, 1.0]。这种转换为张量和标准化范围对于模型输入是必需的。transforms.Resize(size)将输入图像调整到给定size。如果size是整数图像的较短边将匹配此数字同时保持宽高比。如果size是像(h, w)这样的序列它会将图像调整为精确的高度h和宽度w。这很重要因为许多神经网络需要固定大小的输入。transforms.CenterCrop(size)将图像的中心部分裁剪到给定size。这通常在调整大小后使用以确保最终图像尺寸精确同时聚焦于中心区域。transforms.Normalize(mean, std)使用为每个通道提供的均值和标准差对张量图像进行标准化。应用的操作是 输出(输入−均值)/标准差输出(输入−均值)/标准差 标准化有助于稳定训练并通过确保输入特征具有相似的比例通常围绕零居中来促进更快的收敛。mean和std通常是值序列每个输入通道对应一个值例如RGB图像有3个值。来自ImageNet等大型数据集的预计算值通常用作默认值mean[0.485, 0.456, 0.406]和std[0.229, 0.224, 0.225]。常用数据增强变换这些变换引入随机性并且通常仅应用于训练数据集。这有助于模型学习对输入中的微小变化保持不变从而降低过拟合倾向。transforms.RandomHorizontalFlip(p0.5)以给定概率p默认为0.5表示50%的机会随机水平翻转图像。transforms.RandomRotation(degrees)通过从(-degrees, degrees)中均匀选择的随机角度旋转图像或者如果degrees是序列(min, max)则在特定范围内旋转。transforms.ColorJitter(brightness0, contrast0, saturation0, hue0)随机改变图像的亮度、对比度、饱和度和色调。你可以指定每个属性的抖动范围。例如brightness0.2意味着随机选择一个介于[max(0, 1 - 0.2), 1 0.2]之间的亮度因子。transforms.RandomResizedCrop(size)裁剪图像的随机部分并将其调整到所需的size。这是一种常用增强技术特别适用于训练Inception网络等图像分类模型。组合变换你很少只应用一个变换。PyTorch 通过transforms.Compose方便地将多个变换链式组合起来。它接收一个变换对象列表并按顺序应用它们。下面是为训练数据创建处理流程的示例包括调整大小、增强、转换为张量和标准化importtorchvision.transformsas transforms// 训练数据的变换流程示例valtrain_transformtransforms.Compose(transforms.Resize(256),// 将较短边调整为256transforms.RandomCrop(224),// 随机裁剪224x224的区域transforms.RandomHorizontalFlip(),// 随机水平翻转transforms.ToTensor(),// 将PIL图像转换为张量0-1范围transforms.Normalize(meanSeq(0.485,0.456,0.406),// 使用ImageNet统计数据进行标准化stdSeq(0.229,0.224,0.225)))// 验证/测试数据的变换流程示例无增强valtest_transformtransforms.Compose(transforms.Resize(256),// 将较短边调整为256transforms.CenterCrop(224),// 中心裁剪到224x224transforms.ToTensor(),// 将PIL图像转换为张量0-1范围transforms.Normalize(meanSeq(0.485,0.456,0.406),// 使用ImageNet统计数据进行标准化stdSeq(0.229,0.224,0.225)))println(训练变换)println(train_transform)println(\n测试变换)println(test_transform)将变换与数据集结合正如上一节关于Dataset对象所述这些组合变换通常在实例化Dataset时作为参数通常命名为transform或target_transform传入。对于torchvision.datasets中的内置数据集这直接简单// 假设您已安装 torchvisionimporttorchvision.datasetsas datasetsimportjava.nio.file.Path// torchvisions ImageFolder 的使用示例valtrain_data_pathPath(path/to/your/train_images)valtest_data_pathPath(path/to/your/test_images)valtrain_datasetdatasets.ImageFolder(roottrain_data_path,transformtrain_transform)valtest_datasetdatasets.ImageFolder(roottest_data_path,transformtest_transform)// 当您从 train_dataset 访问一个项时train_transform 会被应用valsample_imagetrain_dataset(0)._1// sample_image 现在是一个经过变换的张量valsample_labeltrain_dataset(0)._2// sample_label 是一个整数标签对于自定义Dataset类您通常会在__init__方法中接受变换对象并在返回样本之前在__getitem__方法中应用它。importtorch.utils.data.DatasetimportPIL.ImageclassCustomImageDatasetextendsDataset[(Image,Int)]:def__init__(self,image_paths,labels,transformNone):valimage_pathsimage_pathsvallabelslabelsvaltransformtransformdef__len__(self):returnlen(self.image_paths)def__getitem__(self,idx):valimage_pathimage_paths(idx)vallabellabels(idx)valimageImage.open(image_path).convert(RGB)// 加载图像iftransform then imagetransform(image)// 应用变换returnimage,label//使用方法valcustom_train_datasetCustomImageDataset(train_paths,train_labels,transformtrain_transform)valcustom_test_datasetCustomImageDataset(test_paths,test_labels,transformtest_transform)通过定义适当的变换并将其集成到您的Dataset中您可以确保输入到模型的数据格式正确并且对于训练数据而言得到充分增强。这为下一步使用DataLoader有效地按批加载这些已处理的数据做好了准备。

更多文章