1. 数据读取机制:被低估的PyTorch核心模块
很多人学PyTorch,上来就啃模型搭建、损失函数、优化器,或者一头扎进各种经典网络的复现里。等真正跑起训练,才发现半天时间都耗在“等数据”上——GPU空转、CPU飙满、内存爆掉,问题往往不在模型,而在数据读取这一层。
深度之眼Pytorch框架训练营第四期专门讲Dataloader与Dataset,这个安排其实非常懂行。数据读取机制恰恰是PyTorch里最容易被低估、却最影响实际训练效率的部分。它不像模型结构那样“看得见摸得着”,但整个训练流程的起点和瓶颈都在这里。简单说,Dataset负责“怎么拿数据和打标签”,Dataloader负责“怎么把数据高效喂给模型”,两者配合得好,训练过程才谈得上顺畅。
这篇内容就围绕数据读取机制展开,重点拆解Dataset和Dataloader的设计逻辑、协作方式、关键参数、实际踩坑与排查方法。不管你是刚接触PyTorch的新手,还是已经跑过几个项目但总被数据加载问题困扰的开发者,这篇内容都值得认真过一遍。我会尽量把“为什么这么做”讲清楚,而不是停留在“照着写就能跑”的层面。
2. Dataset设计:数据从哪来、怎么取
2.1 从__init__到__getitem__:三个方法的分工逻辑
Dataset是PyTorch中数据读取的“源头”。它的核心职责可以概括为一句话:把“原始数据”组织成“样本+标签”的可索引结构。换句话说,Dataset决定了你如何从一个文件夹、一个CSV文件或一张数据库表里,拿到第N个样本以及它对应的标签。
一个标准的自定义Dataset类需要实现三个方法:__init__、__len__和__getitem__。__init__负责初始化路径列表、读取标签、做预处理准备;__len__返回数据集总长度;__getitem__则根据索引返回对应的样本和标签。
这套设计逻辑其实借鉴了Python的序列协议——你传入一个索引,就能拿到一个元素,底层数据怎么存储、怎么读取,对使用者完全透明。对于PyTorch而言,这套抽象的妙处在于:不管你的数据是图片、文本、音频还是表格,Dataloader都可以用统一的方式去调用它们。
import torch from torch.utils.data import Dataset from PIL import Image import os class ImageDataset(Dataset): def __init__(self, image_dir, label_file, transform=None): self.image_dir = image_dir self.transform = transform self.image_paths = [] self.labels = [] # 读取标签文件,这里以简单的txt为例:每行是"图片名 标签" with open(label_file, 'r') as f: for line in f: img_name, label = line.strip().split() self.image_paths.append(os.path.join(image_dir, img_name)) self.labels.append(int(label)) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image = Image.open(self.image_paths[idx]).convert('RGB') label = self.labels[idx] if self.transform: image = self.transform(image) return image, label这段代码是Dataset最典型的写法。__init__里把图片路径和标签全部加载到内存中的列表里,__getitem__里按索引读取图片、应用变换、返回结果。注意几个细节:图片用PIL读取而不是直接用cv2或matplotlib,因为PIL配合torchvision.transforms最顺手;标签在__init__阶段就全部读入内存,避免训练过程中频繁的磁盘IO;convert('RGB')是为了统一通道数,防止出现灰度图和彩色图混杂导致的维度错误。
2.2 数据预处理到底该放在Dataset还是Dataloader
这是训练营学员问得最多的问题之一。严格来说,预处理操作(比如归一化、随机裁剪、翻转)放在__getitem__里执行,因为它对每个样本单独执行,天然适配“样本级操作”。而Dataloader的collate_fn更适合处理“批级操作”,比如对不同长度的序列做padding、把图片堆叠成batch tensor。
实际项目中常见的做法是:在Dataset里定义transform参数,训练时传入包含随机增强的变换序列,验证/测试时只传入ToTensor和归一化。这样既保证了训练集的增强,又避免了验证集被随机变换污染导致评估不准确。
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])设计Dataset时,transform最好是作为参数传入而不是在Dataset内部写死。这样做的好处是:同一个Dataset类可以在训练和验证阶段复用,只需要切换变换策略即可。我在实际项目中还会额外传入is_train标志位,内部自动选择相应的变换策略,这样调用起来更简洁。
2.3 内存优化:大文件与懒加载策略
一个常见的误区是:Dataset的__init__必须把所有数据一次性读入内存。对于小型数据集这没问题,但如果数据是几万张高清图片或几十GB的文本语料,这种“全量读入”策略会直接拖垮内存。
正确的思路是“懒加载”。__init__只记录路径和元信息,__getitem__在索引发生时才会真正去磁盘读数据。上面的图片Dataset示例已经体现了这个思想——内存中只保存路径列表,图片本身按需读取。
但对于某些特殊场景,懒加载还不够。比如需要预处理耗时极长、或数据频繁随机访问导致IO瓶颈时,可以在__getitem__里加一层缓存机制:
import functools class CachedDataset(Dataset): def __init__(self, data_source, cache_size=128): self.data_source = data_source self.cache = {} self.cache_size = cache_size def __len__(self): return len(self.data_source) def __getitem__(self, idx): if idx in self.cache: return self.cache[idx] data = self.data_source[idx] if len(self.cache) >= self.cache_size: # 简单的LRU清理,实际项目可用collections.OrderedDict实现 self.cache.clear() self.cache[idx] = data return data这种做法在NLP项目里尤其常见——词向量矩阵初始化耗时很长,缓存可以避免重复构建;而对于图片数据集,如果预处理包含人脸对齐、OCR检测这类耗时操作,缓存的效果也非常明显。不过要提醒一句:缓存机制会占用内存,cache_size需要根据数据样本大小和机器配置反复调。一个经验值是单样本占用内存乘上cache_size,不超过总可用内存的10%~20%。
3. Dataloader核心机制:采样、批次与多进程
3.1 DataLoader的基本工作流程
DataLoader的本质是一个迭代器,它接收一个Dataset实例,按照设定的批次大小、采样顺序、进程数量等参数,把Dataset中的样本组织成batch输出给训练循环。这个“组织”的过程并不像看起来那么简单,背后经过了好几个环节。
完整的数据流是这样的:Sampler根据策略生成索引序列,比如SequentialSampler按顺序生成[0,1,2,...],RandomSampler打乱顺序;然后DataLoader按batch_size把索引切成一个个小组;之后每个索引对应的样本通过Dataset的__getitem__取出来;最后collate_fn把这些样本拼接成批。如果设置了num_workers大于0,这些操作会分布在多个子进程并行执行。
用一句话概括Dataloader:它决定了“每次训练迭代时,模型看到的batch长什么样、从哪来、以什么顺序来”。
from torch.utils.data import DataLoader train_loader = DataLoader( dataset=train_dataset, batch_size=32, shuffle=True, sampler=None, batch_sampler=None, num_workers=4, collate_fn=None, pin_memory=True, drop_last=False, timeout=0, worker_init_fn=None )上面列出了DataLoader最常见的参数。shuffle和sampler不能同时指定,因为shuffle=True内部会创建一个RandomSampler;同理,batch_sampler和batch_size、shuffle、drop_last也不能同时存在——批量采样器已经包含了所有批次划分逻辑。这个限制我见过很多人踩过坑,代码写着写着就报出冲突错误。
3.2 sampler与batch_sampler的适用场景
如果需要更精细地控制样本顺序,就需要自定义Sampler了。比如类别不平衡任务中,希望每个batch都包含各类别的样本,就要写一个WeightedRandomSampler;如果你按照视频ID组织数据,希望同一个视频的帧在同一批次内,那就需要自定义BatchSampler。
WeightedRandomSampler的用法是这样的:传入每个样本的权重列表和采样数量,权重越大的样本被抽中的概率越高。使用它替代普通的shuffle=True,可以实现在不复制样本的前提下控制batch内的类别分布。
from torch.utils.data import WeightedRandomSampler # 假设labels是全部样本的标签列表 class_counts = [sum(1 for l in labels if l == c) for c in range(num_classes)] sample_weights = [1.0 / class_counts[label] for label in labels] sampler = WeightedRandomSampler( weights=sample_weights, num_samples=len(sample_weights), replacement=True ) loader = DataLoader(dataset, batch_size=32, sampler=sampler)这里的权重设定是“类别样本数越多,单个样本权重越低”,这样每个类别在采样中出现的总概率趋近,就天然实现了类别均衡。replacement=True表示允许重复采样,这在不平衡数据中恰恰是我们想要的效果——少数类会被额外补充采样。多数类也不会因此被完全丢弃,因为num_samples设置为数据集总长度,每个epoch的迭代次数和原来基本一致。
3.3 num_workers与pin_memory:训练提速的两把钥匙
num_workers控制数据加载的子进程数。它并不是越大越好——设置为0表示在主进程中加载数据,此时数据读取会和模型训练串行,GPU常常因为等数据而空闲;设置为大于0则可以并行读取、预取数据,但进程切换和IPC通信也会引入额外开销。
关于num_workers的设置,经验值是“CPU核心数的1/4到1/2”起步,然后逐步调高观察训练速度的变化。如果在任务管理器里看到CPU没有跑满,可以继续加;但提速不明显且CPU占用极高时,反而需要往回调。对于Windows系统,num_workers>0时必须把训练代码放在if __name__ == '__main__':保护块中,否则会在多进程启动时出现无限递归报错。这个坑简直是Windows下PyTorch常见问题Top 3。
pin_memory=True的底层逻辑是:将数据加载到锁页内存(page-locked memory),这部分内存可以被GPU直接通过DMA方式读取,省去了数据从可分页内存拷贝到锁页内存的中间步骤。简单理解就是给GPU数据传输开了条“直通车”。实际使用中,pin_memory=True加上num_workers合理配置,通常能带来10%~30%的训练提速,几乎零成本。
还有一个容易被忽略的参数是persistent_workers=True。默认情况下,每个epoch结束,num_workers进程会被销毁重建;设置persistent_workers=True后,进程会跨epoch保持存活,省去了反复创建进程的开销。如果你每个epoch的迭代次数不多(比如batch数量小于几十),这个参数的加速效果非常明显。
4. collate_fn:batch组装最后一公里
4.1 默认行为与自定义需求
collate_fn是Dataloader中容易被忽略、却往往决定代码能否跑通的关键环节。它的作用是把一个batch的样本列表组装成模型能直接接收的形态。
默认情况下,PyTorch的default_collate会自动完成以下操作:把每个样本的数据部分堆叠成tensor(比如图片从[32, 3, 224, 224]变成[batch_size, 3, 224, 224])、标签部分堆叠成tensor、返回一个元组。这个过程要求batch内所有样本的shape完全相同。
但很多实际场景并不满足这个“完全相同”的条件。典型例子是NLP中的变长序列。句子有长有短,直接堆叠会失败。这时候就需要在collate_fn里做padding,把同一个batch内的序列补齐到相同长度。
def collate_fn(batch): inputs, labels = zip(*batch) # 获取当前batch的最大长度 max_len = max([len(seq) for seq in inputs]) # padding到相同长度(假设input是list of token ids) padded_inputs = [] attention_masks = [] for seq in inputs: padding_len = max_len - len(seq) padded_seq = seq + [0] * padding_len mask = [1] * len(seq) + [0] * padding_len padded_inputs.append(padded_seq) attention_masks.append(mask) return ( torch.tensor(padded_inputs), torch.tensor(attention_masks), torch.tensor(labels) )这里有一个关键技巧:zip(*batch)把batch中的样本元组拆开,分别聚合所有输入和所有标签。每个样本结构是(input_ids, label),batch是[(input_ids1, label1), (input_ids2, label2), ...],zip(*batch)后inputs就是(input_ids1, input_ids2, ...),labels就是(label1, label2, ...)。这样可以避免写成循环逐个索引取值的丑陋代码。
4.2 多输入模型与目标检测的collate写法
许多模型不止一个输入。比如视觉问答需要同时接收图片和问题文本,推荐系统需要用户特征和物品特征拼接。此时__getitem__返回的是一个字典或更复杂的元组,collate_fn需要把每个键对应的数据分别堆叠。
def multi_input_collate_fn(batch): images = torch.stack([item['image'] for item in batch], dim=0) questions = [item['question'] for item in batch] labels = torch.tensor([item['label'] for item in batch]) return { 'image': images, 'question': questions, 'label': labels }注意questions这里没有堆叠成tensor,因为它是变长文本。如果后续模型需要定长输入,可以在collate里面做tokenize和padding;也可以保持list形式,模型内部再做处理。两种方案没有绝对的对错,但统一放在collate里会让模型代码更干净,也更好地利用了多进程并行的优势。
目标检测任务更特殊一些。每张图片的标注框数量不同,collate_fn不能简单堆叠标注框。常见做法是:图片用torch.stack堆叠,标注保持list形式传入,在模型内部通过batch中每张图的targets来区分。
def detect_collate_fn(batch): images = [] targets = [] for img, target in batch: images.append(img) targets.append(target) return torch.stack(images, dim=0), targets这是一个很多检测代码库中实际使用的写法。target是一个字典,包含boxes和labels字段,每张图的框数量可以不同。由于Dataloader只负责把这些数据传进来,真正按batch进行loss计算时,检测框架内部会自行根据targets来匹配预测和真值,所以batch内目标数量不一致完全没问题。
5. 实战案例:三个典型场景的数据读取实现
5.1 图片分类:从文件夹到批量训练
图片分类是最常见、也最适合用来理解Dataloader全流程的场景。这里用torchvision.datasets.ImageFolder就能快速完成数据集构建,但为了展示自定义Dataset的灵活性,我用一个实际项目中的定制写法来演示。
import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class CustomImageFolder(Dataset): def __init__(self, root_dir, transform=None): self.root_dir = root_dir self.transform = transform self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {cls: idx for idx, cls in enumerate(self.classes)} self.samples = [] for class_name in self.classes: class_path = os.path.join(root_dir, class_name) if not os.path.isdir(class_path): continue for img_name in os.listdir(class_path): if img_name.lower().endswith(('.jpg', '.jpeg', '.png')): self.samples.append(( os.path.join(class_path, img_name), self.class_to_idx[class_name] )) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label = self.samples[idx] image = Image.open(img_path).convert('RGB') if self.transform: image = self.transform(image) return image, label这段代码做的事和ImageFolder基本一致,但有几个好处:完全可控的路径筛选逻辑、方便添加额外的样本过滤规则、便于在初始化时打印数据统计信息。当数据集目录结构不规整、或需要排除损坏图片时,自定义类的价值就体现出来了。
配套使用:
transform = transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) dataset = CustomImageFolder(root_dir='data/train', transform=transform) dataloader = DataLoader( dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True ) for epoch in range(50): for batch_idx, (images, labels) in enumerate(dataloader): # images: [64, 3, 224, 224] # labels: [64] pass一个容易忽略的点:RandomCrop和RandomHorizontalFlip这类随机增强只应该在训练阶段使用。如果只定义一个transform变量,验证阶段也会做随机裁剪和翻转,导致验证指标波动剧烈且不准确。正确的做法是像前面说的,训练和验证分别使用不同的transform序列。
5.2 文本分类:变长序列与padding细节
文本数据与图片最大的区别在于“变长”。一个batch中的句子可能从几个词到上百个词不等,处理策略直接影响了训练效果和效率。这里用TextCNN场景来演示。
import jieba # 仅用于分词演示 class TextDataset(Dataset): def __init__(self, file_path, vocab, max_seq_len=200): self.vocab = vocab self.max_seq_len = max_seq_len self.texts = [] self.labels = [] with open(file_path, 'r', encoding='utf-8') as f: for line in f: parts = line.strip().split('\t') if len(parts) < 2: continue self.texts.append(parts[0]) self.labels.append(int(parts[1])) def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] # 分词并映射到索引 tokens = jieba.lcut(text) token_ids = [self.vocab.get(t, self.vocab['<UNK>']) for t in tokens] # 截断到最大长度 token_ids = token_ids[:self.max_seq_len] return token_ids, label这里__getitem__返回的token_ids是变长的Python list,需要配合自定义collate_fn才能组装成batch。按batch内最大长度做padding是对训练效果最友好的方式——比固定max_seq_len的方式更少冗余计算。
还有一个平时容易忽略的细节:对于Rnn/Lstm这类按时间步展开的网络,同一个batch内计算时padding部分参与运算有时候会产生无效的隐状态更新,因此往往需要attention_mask来屏蔽padding位置的注意力。这也是为什么前面示例中collate_fn会返回attention_masks。对于TextCNN这类纯卷积模型,padding不参与卷积核的有效区域,影响较小,但不传mask直接强行训练的做法还是不推荐——除非你明确知道自己的模型对padding不敏感。
5.3 单样本延迟加载:医学影像与超大数据集场景
最后一类场景是单样本读取极慢或单个文件极大的情况——典型的如医学影像(CT、MRI序列)或高分辨率遥感影像。这类数据可能一个样本就有数百MB到几GB,全量读取到内存完全不可行,连__getitem__里按需读取都可能成为训练瓶颈。
这类场景的做法是:配合预处理脚本,先对原始影像完成必要的切片、降采样或格式转换,生成中间缓存格式。Dataset只读取处理后的缓存文件而不是直接面对原始大文件。更进一步,可以在训练前用多进程把所有样本预先处理成合适尺寸的小块(例如128x128x128的子块),再通过Dataset采样这些子块。
class VolumeDataset(Dataset): def __init__(self, file_list): self.file_list = file_list def __len__(self): return len(self.file_list) def __getitem__(self, idx): # 这里用np.load模拟加载预处理的npy文件 volume_path = self.file_list[idx] # 实际项目中可能对volume做随机子块采样 sub_volume = load_random_subvolume(volume_path, patch_size=64) return torch.from_numpy(sub_volume).float()重点在于:如果预处理步骤包含重计算或高IO,优先考虑离线完成;在线__getitem__中只保留随机裁剪、归一化这些轻量操作。训练启动时间会因此变长——因为数据处理通常在第一个epoch才开始——但对于超大数据集来说,这种预计算节省的总训练时间往往相当可观。
6. 常见问题与排查技巧实录
6.1 第一个epoch卡顿与数据预热
几乎所有用DataLoader的人都会遇到一种现象:训练启动后,第一个epoch特别慢,后面就正常了。这是正常的——初始化进程、建立内存页、读取文件缓存、预热显存,大量工作都在第一个epoch完成。
但如果第一个epoch慢到离谱(例如一个batch要好几秒),就需要排查了。优先检查num_workers是否设置合理,过小会导致读取能力不足。其次检查数据存储介质——机械硬盘的随机读写性能极差,如果数据文件是几万个小图,机械硬盘读取速度会被严重拖慢。解决方案有两个:一是把数据打平成单个大文件(如TFRecord、HDF5或numpy的npz格式),二是换成固态硬盘存储。很多情况下,这两个方案能把数据读取时间缩短几倍。
6.2 死锁与卡死:Windows下多进程的坑
Windows平台的DataLoader多进程方案和Linux有本质区别:Windows使用spawn方式创建子进程,它会重新导入主模块,如果你的训练代码在模块顶层写了一些不可序列化的内容(如lambda函数、局部定义的对象),就会在创建子进程时报错或卡死。
确保以下几点基本能解决大部分问题:训练入口放在if __name__ == '__main__':保护块中;Dataset中的__getitem__里不要返回lambda或不可pickle的对象;全局变量尽量只读共享。如果死锁发生在Windows下且以上都没问题,尝试将num_workers从4降到2或1,确认是进程数过多还是代码本身的问题。
6.3 内存持续增长与数据泄漏
使用DataLoader训练时内存每epoch都增长,最后直接OOM——这在图片数据场景最常见的原因是:Dataset的__getitem__里显式或隐式全局保存了已读取的数据。例如,某些同学会希望缓存读取过的图片,是用一个self.cache字典保存,但忘记限制缓存上限或用完清理,导致每个epoch都往缓存里塞新数据,内存自然持续膨胀。
另一个隐藏原因和整体架构有关:在训练循环中把每batch的inputs、labels留在循环作用域外,或者记录loss.item()时不断append到一个history列表而不限制大小。这些虽然不直接影响DataLoader本身,但会让整体内存占用线性增长。
关键排查手段:把worker_init_fn里可能需要初始化的资源单独管理,并在finally块中释放;对缓存添加容量上限;训练循环中及时del大tensor。我的习惯是每轮epoch结束打印一下内存占用,发现异常立刻检查代码,不要等OOM再处理。
6.4 数据加载慢的终极排查路径
如果训练中GPU利用率长期低于80%,而CPU和内存并没有跑满,问题很可能是IO层面的。排查路径建议如下:
先用一个最简单的实验定位瓶颈:把Dataset的__getitem__直接跑一遍,记录耗时的分布。如果每个样本耗时正常但Dataloader整体速度慢,问题往往在数据读取并发或GPU数据传输。如果单样本读取耗时很高,优先优化__getitem__内部逻辑——比如用albumentations代替torchvision.transforms中的部分慢操作,或引入缓存。
另一个有效方法是用cProfile或torch.profiler对训练循环中的各个阶段计时。如果数据加载的wall time明显高于模型前向计算,就说明读取机制需要调整;反过来如果等待时间不大,那么提升num_workers的意义就有限了。
6.5 分布式训练下的Dataloader差异
如果你的项目要扩展到多卡训练,Dataloader的使用还有额外讲究。最核心的变化是DistributedSampler:它会把数据集按进程数划分成互不重叠的子集,保证多个进程不会重复看到同一批数据。
from torch.utils.data.distributed import DistributedSampler sampler = DistributedSampler(dataset, num_replicas=world_size, rank=rank) dataloader = DataLoader(dataset, batch_size=32, sampler=sampler, shuffle=False)注意两个细节:使用DistributedSampler时shuffle参数必须设为False,因为打乱逻辑已经由Sampler内部管理;每个epoch开始时要调用sampler.set_epoch(epoch)以确保数据顺序在每个epoch不同——否则每个epoch的数据顺序完全一致,直接影响模型收敛效果。
for epoch in range(epochs): sampler.set_epoch(epoch) for batch in dataloader: # 训练逻辑 pass多卡训练下还有一个隐性收益:由于总batch_size会随卡数成倍增加,数据处理吞吐能力也必须匹配。此时num_workers通常按每卡4~8来设置,取决于机器CPU核心数和数据读取的复杂度。
7. 实操心得与进阶建议
前面把原理、代码和坑都铺开了,最后聊点经验层面的东西。
我的建议是:不要只满足于跑通代码,可以自己实现一遍Dataset和Dataloader的完整流程。把__getitem__、collate_fn、Sampler各个击破,你才能对PyTorch的数据流有真正深入的理解。等到你熟悉了这套机制,遇到各种“玄学”性能问题就自然有一套排查思路了。
一个我经常用的小技巧是:在__getitem__里临时加一行耗时打印,快速找到读数据阶段最重的操作,然后针对它做优化——是缓存、预处理还是换存储介质,方向一下子就清楚了。这个方法简单粗暴,比分析任何profiling工具都直观。
还有一点值得强调:数据读取机制不是越花哨越好。batch_size大小、num_workers数量、是否用pin_memory、Sampler怎么选,都取决于你的数据规模、模型复杂度、硬件配置。初学者最容易犯的错就是把网上某个大型项目的配置直接套到自己小数据集上,结果GPU利用率反倒下降了。最适合的配置,是在自己环境里实测出来的——多试几组配置,记录每组的数据加载耗时和训练吞吐量,让数据告诉你答案。
最后再提一个规划思路:当你的项目数据量上了规模、预处理步骤越来越复杂后,可以考虑把数据读取部分从主训练代码中拆分成独立模块。Dataset负责数据解析,Dataloader配置集中在单独的配置字典里,collate策略写在独立的函数文件中。这套结构在后来的多个项目中帮我节省了大量调试时间,也让我能轻松应对各种各样的数据集——图片、文本、表格、多模态,本质上都是同一套机制在灵活运作。
如果你还在被“数据加载慢”“batch尺寸对不上”“多进程卡死”这些问题反复折磨,希望这篇内容能帮你理清思路。数据读取机制说难不难,说简单也藏着不少讲究——但弄明白之后,你会发现整个PyTorch训练流程都变得通透了许多。