构建一个完整的数据处理流程涉及从原始数据(为简化起见,将合成数据)开始,最终得到准备好输入模型的数据批次。这个过程需要创建一个自定义 Dataset,定义数据转换,并将所有内容封装在一个 DataLoader 中。构建合成数据集假设我们有一个数据集,包含特征向量和对应的二元分类标签(0 或 1)。在本次练习中,我们将使用 PyTorch 张量直接生成这些数据。这避免了文件输入/输出的复杂性,让我们能够专注于数据处理机制。import torch import torch.utils.data as data from torchvision import transforms # 生成合成数据 num_samples = 100 num_features = 10 # 创建随机特征向量(例如,传感器读数) features = torch.randn(num_samples, num_features) # 创建随机二元标签(0 或 1) labels = torch.randint(0, 2, (num_samples,)) print(f"Shape of features: {features.shape}") # 输出: torch.Size([100, 10]) print(f"Shape of labels: {labels.shape}") # 输出: torch.Size([100]) print(f"First 5 features:\n{features[:5]}") print(f"First 5 labels:\n{labels[:5]}")这使我们得到了两个张量:features 包含 100 个样本,每个样本有 10 个特征;labels 包含对应的 100 个标签。创建自定义 Dataset现在,我们需要使用 PyTorch 的 Dataset 类来组织这些数据。我们将创建一个自定义类,它继承自 torch.utils.data.Dataset 并实现两个重要方法:__len__(self): 返回数据集中样本的总数。__getitem__(self, idx): 返回指定索引 idx 处的样本(特征和标签)。我们还将添加一个 __init__ 方法来存储数据并可选地接受转换。class SyntheticDataset(data.Dataset): """一个用于合成特征和标签的自定义数据集。""" def __init__(self, features, labels, transform=None): """ 参数: features (Tensor): 包含特征数据的张量。 labels (Tensor): 包含标签的张量。 transform (callable, optional): 可选的样本转换。 """ # 基本检查,确保特征和标签具有相同的样本数量 assert features.shape[0] == labels.shape[0], \ "特征和标签的样本数量必须一致" self.features = features self.labels = labels self.transform = transform def __len__(self): """返回样本总数。""" return self.features.shape[0] def __getitem__(self, idx): """ 根据给定索引获取特征向量和标签。 参数: idx (int): 要获取的样本索引。 返回: tuple: (特征, 标签),其中 feature 是特征向量,label 是对应的标签。 """ # 获取原始特征和标签 feature_sample = self.features[idx] label_sample = self.labels[idx] # 创建一个样本字典(或元组) sample = {'feature': feature_sample, 'label': label_sample} # 如果存在转换,则应用转换 if self.transform: sample = self.transform(sample) # 返回可能已转换的样本 # 常见做法是分别返回特征和标签 return sample['feature'], sample['label'] # 暂时实例化不带转换的数据集 raw_dataset = SyntheticDataset(features, labels) # 测试获取一个样本 sample_idx = 0 feature_sample, label_sample = raw_dataset[sample_idx] print(f"\nSample {sample_idx} - Feature: {feature_sample}") print(f"Sample {sample_idx} - Label: {label_sample}") print(f"Dataset length: {len(raw_dataset)}") # 输出: 100此时,raw_dataset 包含我们的数据并知道如何提供单个样本。定义数据转换通常,原始数据不适合直接输入神经网络。我们可能需要规范化特征、转换数据类型或应用数据增强(特别是对于图像)。torchvision.transforms 提供了方便的工具。即使我们的数据不是图像,我们也可以定义自定义转换或使用对张量进行操作的现有转换。让我们定义一个简单转换流程:将特征张量转换为 torch.float32(模型输入的良好做法)。将标签张量转换为 torch.long(损失函数如 CrossEntropyLoss 常需要的)。对特征应用规范化(减去均值,除以标准差)。我们将在此示例中从合成数据集中计算这些统计量。由于 torchvision.transforms 主要为图像(PIL 图像或张量)设计,将其直接应用于像我们 sample 这样的字典需要一些封装。我们将为此创建自定义可调用类或 lambda 函数。# 计算用于规范化的均值和标准差(跨数据集) feature_mean = features.mean(dim=0) feature_std = features.std(dim=0) # 如果任何特征的标准差为零,则避免除以零 feature_std[feature_std == 0] = 1.0 # 为字典样本格式定义自定义转换类/函数 class ToTensorAndType(object): """将特征转换为 FloatTensor,将标签转换为 LongTensor。""" def __call__(self, sample): feature, label = sample['feature'], sample['label'] return {'feature': feature.float(), 'label': label.long()} class NormalizeFeatures(object): """规范化特征张量。""" def __init__(self, mean, std): self.mean = mean self.std = std def __call__(self, sample): feature, label = sample['feature'], sample['label'] # 应用规范化: (张量 - 均值) / 标准差 normalized_feature = (feature - self.mean) / self.std return {'feature': normalized_feature, 'label': label} # 组合转换 data_transforms = transforms.Compose([ ToTensorAndType(), NormalizeFeatures(mean=feature_mean, std=feature_std) ]) # 实例化带转换的数据集 transformed_dataset = SyntheticDataset(features, labels, transform=data_transforms) # 测试获取一个转换后的样本 sample_idx = 0 transformed_feature, transformed_label = transformed_dataset[sample_idx] print(f"\n--- 转换后的样本 {sample_idx} ---") print(f"原始特征:\n{features[sample_idx]}") print(f"转换后特征:\n{transformed_feature}") print(f"原始标签: {labels[sample_idx]} (dtype={labels.dtype})") print(f"转换后标签: {transformed_label} (dtype={transformed_label.dtype})") # 验证规范化(对于第一个样本的特征,均值应接近 0,标准差应接近 1) print(f"转换后特征均值: {transformed_feature.mean():.4f}") # 应用数据集范围的规范化后应接近 0注意到由于规范化,特征值已发生改变,并且特征和标签的数据类型现在分别是 torch.float32 和 torch.int64 (LongTensor)。使用 DataLoader最后一步是使用 DataLoader。它接收我们的 Dataset 实例,并处理批处理、数据混洗以及可能的并行数据加载。# 创建 DataLoader batch_size = 16 # 以 16 个样本的批次处理数据 shuffle_data = True # 在每个 epoch 开始时打乱数据 num_workers = 0 # 用于数据加载的子进程数量。0 表示数据加载在主进程中进行。 # 在非 Windows 平台上,通常可以将 num_workers 设置为 > 0 进行并行加载 # import os # if os.name != 'nt': # 检查是否非 Windows # num_workers = 2 data_loader = data.DataLoader( transformed_dataset, batch_size=batch_size, shuffle=shuffle_data, num_workers=num_workers ) # 迭代 DataLoader 以获取批次数据 print(f"\n--- 迭代 DataLoader (batch_size={batch_size}) ---") # 获取一个批次 feature_batch, label_batch = next(iter(data_loader)) print(f"Type of feature_batch: {type(feature_batch)}") print(f"Shape of feature_batch: {feature_batch.shape}") # 输出: torch.Size([16, 10]) print(f"Shape of label_batch: {label_batch.shape}") # 输出: torch.Size([16]) print(f"Data type of feature_batch: {feature_batch.dtype}") # 输出: torch.float32 print(f"Data type of label_batch: {label_batch.dtype}") # 输出: torch.int64 # 你可以像这样遍历所有批次(例如,在一个训练 epoch 中) # print("\n遍历几个批次:") # for i, (batch_features, batch_labels) in enumerate(data_loader): # if i >= 3: # 显示前 3 个批次 # break # print(f"Batch {i+1}: Features shape={batch_features.shape}, Labels shape={batch_labels.shape}") # # 在真实的训练循环中,你会将 batch_features 输入到模型DataLoader 产生批次,其中第一个维度对应于 batch_size。我们的特征批次形状为 [16, 10],标签批次形状为 [16]。数据类型反映了我们应用的转换。数据处理流程可视化我们可以可视化刚刚创建的流程:digraph G { rankdir=LR; node [shape=box, style=filled, fillcolor="#e9ecef", fontname="sans-serif"]; edge [fontname="sans-serif"]; 原始数据 [label="原始数据\n(特征, 标签张量)", fillcolor="#a5d8ff"]; 自定义数据集 [label="自定义数据集\n(SyntheticDataset)", fillcolor="#96f2d7"]; 转换 [label="数据转换\n(ToTensor, 规范化)", fillcolor="#ffec99"]; 数据加载器 [label="DataLoader\n(批处理, 混洗)", fillcolor="#fcc2d7"]; 模型输入 [label="模型输入\n(批处理和已处理数据)", fillcolor="#bac8ff"]; 原始数据 -> 自定义数据集 [label=" __init__ "]; 自定义数据集 -> 转换 [label=" __getitem__ 应用 "]; 转换 -> 自定义数据集 [style=dashed]; // 转换在数据集中配置 自定义数据集 -> 数据加载器 [label=" 输入数据集 "]; 数据加载器 -> 模型输入 [label=" 迭代获取批次 "]; }此图表展示了从原始张量到自定义 Dataset 的演进过程,在数据获取时应用转换(__getitem__),最后使用 DataLoader 生成适合模型训练的混洗批次。你现在已成功使用 PyTorch 的核心数据工具构建了数据处理流程。你创建了一个 Dataset 来封装你的数据,应用了必要的 transforms,并使用 DataLoader 高效地生成批次。这种结构化方法是 PyTorch 项目中处理数据的基础,确保你的模型接收正确格式的数据并促进高效训练。这个流程现在已准备好集成到我们将在下一章构建的训练循环中。