四:数据处理与加载
自定义Dataset需要实现哪两个基本方法?¶
在 PyTorch 中,自定义数据集必须继承 torch.utils.data.Dataset 并实现两个核心方法:
-
len(self):返回数据集的样本总数。这是DataLoader计算批次数量、控制迭代次数的依据。 -
getitem(self, idx):根据给定的索引idx,返回单个样本(及其标签或附加信息)。这是数据加载的核心,所有数据预处理、增强操作通常都在此方法内完成。
from torch.utils.data import Dataset
class MyDataset(Dataset):
def __init__(self, data, targets):
self.data = data
self.targets = targets
def __len__(self):
return len(self.data)
def __getitem__(self, idx):
sample = self.data[idx]
label = self.targets[idx]
# 可在此进行任何预处理、增强
return sample, label
深入理解:
-
len必须返回一个非负整数,否则DataLoader无法正确采样。如果数据集是流式的或长度不确定,可以继承IterableDataset并改为实现iter。 -
getitem返回的数据格式是自由的,但通常由collate_fn统一打包为 batch。对于监督学习,返回(input, target)是最常见的约定。
2. getitem 通常返回什么格式的数据?¶
getitem 返回的格式完全取决于任务和 collate_fn 的约定,但最常见的规范是返回一个元组 (input, target) 或字典。
常见格式:
-
监督学习(图像分类):返回
(image_tensor, label),其中image_tensor是(C, H, W)的张量,label是整数标量。 -
自然语言处理(文本分类):返回
(text, label)或预先 tokenize 的(input_ids, attention_mask, label)。 -
多任务/复杂场景:返回字典
{'image': img, 'mask': mask, 'label': label},这时必须配合自定义collate_fn来正确堆叠字典中的每个字段。 -
生成式任务:对于指令微调,常返回
(prompt, response)或已经编码的input_ids和labels。
关键:getitem 返回的张量不需要具有相同大小,collate_fn 负责在 batch 维度进行填充或拼接。
def __getitem__(self, idx):
img = self.load_image(idx) # (3, 224, 224)
label = self.labels[idx]
return img, label
如何将图像和标签组成一个样本返回?¶
最直接的方式是在 getitem 中读取图像并转换成张量,然后与标签一起打包成元组返回。
from PIL import Image
import torch
class ImageDataset(Dataset):
def __init__(self, image_paths, labels, transform=None):
self.image_paths = image_paths
self.labels = labels
self.transform = transform
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx):
img = Image.open(self.image_paths[idx]).convert('RGB')
if self.transform:
img = self.transform(img) # 返回 Tensor (C, H, W)
label = self.labels[idx]
return img, label # 元组形式
进阶:如果需要返回多个辅助信息(如文件名、边界框),可以返回字典,并在 collate_fn 中处理。
DataLoader的 batch_size 和 shuffle 参数的作用是什么?¶
-
batch_size:指定每个批次包含的样本数量。DataLoader会从数据集中提取多个样本,通过collate_fn堆叠成一个批次,然后返回。它直接决定了每次模型前向传播的输入规模,影响显存占用和训练效率。 -
shuffle:布尔值,决定是否在每个 epoch 开始时随机打乱数据顺序。 - 训练集通常设置为
True,以打破样本间的关联,提升模型泛化能力。 - 验证集和测试集通常设置为
False,以保证评估结果的可复现性和一致性。
深入细节:
-
shuffle=True时,DataLoader内部使用RandomSampler生成随机索引;shuffle=False时使用SequentialSampler顺序索引。 -
打乱操作消耗 CPU 内存,对于极大数据集,可以考虑使用
IterableDataset配合外部混洗。
5. num_workers 设置多少合适?过多或过少会有什么影响?¶
num_workers 控制用于数据加载的子进程数量。设置为 0 表示在主进程中加载(同步),速度最慢,一般仅用于调试。
设置建议:
-
经验值:通常设为 2~4倍CPU核心数 或直接使用
os.cpu_count()。但实际最优值受 I/O 瓶颈、内存带宽和数据集大小影响。 -
GPU 训练:一般 4~8 个 worker 足够。过多的 worker 会导致 CPU 上下文切换和内存开销,反而降低吞吐量。
-
最佳实践:从小值(如 4)开始,逐步增加并监测 GPU 利用率和数据加载时间。当 GPU 等待时间(空闲)趋近于零时,即达到最优。
影响:
-
过少:数据供给速度跟不上 GPU 计算速度,导致 GPU 频繁空闲(利用率低),训练时间延长。
-
过多:子进程间频繁通信和内存复制,可能导致 CPU 占用过高、缓存颠簸,甚至触发 OOM。同时,每个子进程会复制父进程的内存,若数据集较大,会迅速耗尽 RAM。
注意:num_workers 涉及多进程,必须将 DataLoader 的初始化代码包裹在 if name == 'main': 中,以避免递归创建子进程。
多进程数据加载时,为什么要使用 if name == 'main': 保护?¶
这是Windows 和 POSIX 系统的多进程机制要求。Python 在创建子进程时会重新导入主模块脚本。如果没有 if name == 'main': 保护,子进程会再次执行 DataLoader 的创建代码,从而无限递归地创建新进程,最终导致程序崩溃。
原理:
-
当
num_workers > 0时,PyTorch 使用torch.multiprocessing启动子进程。每个子进程都需要导入并执行主脚本,以获得数据集类定义等全局变量。 -
如果不将主执行代码限制在
main中,子进程一启动就会再次执行DataLoader初始化,进而再次启动子进程,形成死循环。
正确写法:
if __name__ == '__main__':
dataset = MyDataset()
loader = DataLoader(dataset, num_workers=4)
for batch in loader:
train(batch)
注意:在 Jupyter Notebook 中,多进程限制更多,可能无法使用 num_workers>0,或需要特别设置。
collate_fn 的作用是什么?什么情况下需要自定义?¶
collate_fn 是 DataLoader 的关键组件,它接收一个样本列表(来自 getitem 的多个返回),并负责将它们组合成一个批次张量。
默认行为(collate_fn=None):
-
将样本按第一维(batch 维度)堆叠为张量,要求所有样本形状一致。
-
如果样本是
(input, target)元组,会分别堆叠为(batched_input, batched_target)。
需要自定义的情况:
-
变长序列:NLP 中的文本长度不一,需要填充(padding)到相同长度,并生成
attention_mask。 -
数据类型复杂:返回的是字典,包含图像、字符串、不同维度的标签等,需要分别处理。
-
特殊预处理:需要执行批次级别的归一化、数据增强,或需要生成
labels掩码(如 SFT 中的 loss masking)。
示例:自定义 collate 处理变长序列
def my_collate(batch):
# batch 是一个列表,每个元素是 (input_ids, label)
inputs, labels = zip(*batch)
# 填充 input_ids
inputs = torch.nn.utils.rnn.pad_sequence(inputs, batch_first=True, padding_value=0)
labels = torch.tensor(labels)
return inputs, labels
loader = DataLoader(dataset, batch_size=16, collate_fn=my_collate)
如何自定义 collate_fn 来处理变长序列并填充?¶
处理变长序列的核心是动态填充,使一个 batch 内的序列长度一致,同时生成 attention_mask 让模型忽略填充部分。
import torch
from torch.nn.utils.rnn import pad_sequence
def collate_fn_for_seq(batch):
# 假设 batch 中每个元素是 (input_ids, label)
input_ids = [item[0] for item in batch]
labels = [item[1] for item in batch]
# 填充到最长序列,batch_first=True 使形状为 (B, L)
padded_inputs = pad_sequence(input_ids, batch_first=True, padding_value=0)
attention_mask = (padded_inputs != 0).long()
labels = torch.tensor(labels)
return padded_inputs, attention_mask, labels
高级技巧:
-
可在
collate_fn中实现 packing(序列打包),将多个短序列拼接成一个长序列,配合分块对角注意力掩码,提高训练效率。 -
对于 SFT 数据,可在
collate_fn中根据input_ids自动生成labels并设置 prompt 部分的掩码为-100。
Sampler的作用是什么?如何实现一个类别平衡采样器?¶
Sampler(采样器) 控制 DataLoader 如何从数据集中抽取样本的顺序和频率。它生成索引序列,传递给 Dataset.getitem。默认情况下,DataLoader 使用 RandomSampler(当 shuffle=True)或 SequentialSampler(当 shuffle=False)。
实现类别平衡采样器:
目标是在每个 batch 中各类别样本数量大致均等,缓解类别不均衡。需要继承 torch.utils.data.Sampler
from torch.utils.data import Sampler
import numpy as np
from collections import Counter
class BalancedBatchSampler(Sampler):
def __init__(self, dataset, labels, batch_size, num_batches):
self.labels = np.array(labels)
self.classes = np.unique(labels)
self.batch_size = batch_size
self.num_batches = num_batches
self.class_indices = {c: np.where(self.labels == c)[0] for c in self.classes}
def __iter__(self):
# 每个 batch 从每个类别中均匀采样
samples_per_class = self.batch_size // len(self.classes)
for _ in range(self.num_batches):
batch = []
for c in self.classes:
batch.extend(np.random.choice(self.class_indices[c], samples_per_class, replace=False))
np.random.shuffle(batch)
yield from batch
def __len__(self):
return self.batch_size * self.num_batches
注意:更好的方式是使用 WeightedRandomSampler,为每个样本赋予与类别频率成反比的权重,然后进行带权随机采样,无需手动平衡每个 batch。
随机采样RandomSampler和顺序采样SequentialSampler的区别。¶
-
RandomSampler:每次迭代时,随机排列数据集中所有样本的顺序,然后依次返回索引。每个 epoch 的顺序都不同,有助于打乱样本关联,提升模型泛化能力。它是训练时的默认选择(当shuffle=True)。 -
SequentialSampler:始终按照数据集的原始顺序,依次返回索引。不打乱,每次 epoch 的顺序完全相同。适用于验证和测试,保证评估的可复现性,以及某些需要顺序处理的场景(如时序预测的验证)。
内部机制:RandomSampler 生成一个包含 [0, len(dataset)-1] 的随机排列列表;SequentialSampler 直接返回 range(len(dataset))。两者都继承自 Sampler,可通过 DataLoader 的 sampler 参数指定。
如何使用WeightedRandomSampler解决样本不平衡问题?¶
WeightedRandomSampler 为每个样本分配一个采样权重,权重越大的样本被抽中的概率越高。解决不平衡时,通常为少数类样本赋予更高的权重。
from torch.utils.data import WeightedRandomSampler
# 假设 targets 是所有样本的标签列表
labels = torch.tensor(targets)
class_counts = torch.bincount(labels)
# 为每个样本计算权重:与所在类别的数量成反比
weights = 1.0 / class_counts[labels]
sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)
loader = DataLoader(dataset, batch_size=32, sampler=sampler)
关键参数:
-
weights:一个与数据集等长的张量,每个元素是该样本的权重。 -
num_samples:总采样数量(通常设为数据集大小)。 -
replacement:是否允许重复采样。对于少数类,通常设为True以增加其出现次数。
原理:WeightedRandomSampler 根据权重进行多项式采样。通过放大少数类的权重,使得每个 batch 中各类别的比例趋于均衡,从而缓解模型偏向多数类的问题。
进阶:可以结合 BatchSampler 进一步控制每个 batch 内的类别分布,或使用更高级的 ImbalancedDatasetSampler(需自定义)。同时,采样仅解决训练数据不平衡,评估时仍应使用原始分布的数据。### 12. 如何利用backward hook实现梯度反转层?
梯度反转层(Gradient Reversal Layer, GRL)在正向传播时是恒等映射,反向传播时将梯度乘以一个负系数(通常为 −λ−λ),从而实现梯度的“反转”。它常用于对抗性训练,如领域自适应(Domain-Adversarial Neural Network, DANN),逼迫特征提取器学习到领域无关的表征。
最稳定和推荐的方式是自定义一个 torch.autograd.Function。
from torch.autograd import Function
class GradientReversal(Function):
@staticmethod
def forward(ctx, x, lambda_=1.0):
ctx.lambda_ = lambda_
return x.view_as(x)
@staticmethod
def backward(ctx, grad_output):
return grad_output.neg() * ctx.lambda_, None
def grad_reverse(x, lambda_=1.0):
return GradientReversal.apply(x, lambda_)
使用 register_backward_hook 也可行,但自定义 Function 更清晰、更可控。
nn.Linear的权重矩阵形状是怎样的?偏置呢?¶
对于 nn.Linear(in_features, out_features, bias=True):

可以通过 print(layer.weight.shape) 验证。
给定输入尺寸、卷积核等参数,如何计算Conv2d的输出尺寸?¶
公式为:

通常无空洞时的简化公式:

示例:输入 (1, 3, 32, 32),Conv2d(3, 16, kernel=3, stride=2, padding=1),输出尺寸为 (1, 16, 16, 16)。
空洞卷积在nn.Conv2d中如何设置?对感受野有何影响?¶
通过 dilation 参数设置,例如 nn.Conv2d(..., dilation=2)。默认为1。
对感受野的影响:有效核大小 = dilation * (kernel_size - 1) + 1。例如 kernel_size=3, dilation=2,有效核大小为5,在不增加参数的情况下成倍扩大感受野,常用于语义分割。
转置卷积nn.ConvTranspose2d主要应用在什么场景?¶
主要用于上采样和生成模型,将低分辨率特征图恢复到高分辨率,如:
-
图像分割(U-Net解码器)
-
GAN的生成器
-
自编码器的解码器
它不是卷积的逆运算,而是通过学习得到核的矩阵转置实现上采样。
自适应池化AdaptiveAvgPool2d的优势是什么?¶
可以指定输出尺寸 (H_out, W_out),内部自动计算所需的池化核和步长,使得无论输入特征图尺寸如何,输出尺寸固定。
优势:简化网络设计,使网络可接受任意尺寸输入;避免繁琐的尺寸计算;常用于全局平均池化(如ResNet最后输出1×1特征)。
BatchNorm在训练和评估模式下的行为有何不同?momentum参数的作用。¶
训练模式:使用当前batch的均值和方差进行归一化,并更新全局 running_mean 和 running_var(指数移动平均)。
评估模式:直接使用训练阶段积累的 running_mean 和 running_var 进行归一化,不更新。
momentum:控制EMA更新的速度,默认0.1。值越小,全局统计量更新越慢,越依赖历史数据。
LayerNorm和BatchNorm的主要区别是什么?Transformer为何用LayerNorm?¶
Transformer使用LayerNorm因为它不依赖batch size,适合序列可变长和自回归模型,且能稳定训练。
Dropout层在训练和测试时的行为差异是什么?原理上为何能正则化?¶
训练时:以概率 p 随机置零神经元,并对幸存元素放大 1/(1−p)倍。
测试时:所有神经元激活,不缩放,等效于集成模型的平均。
正则化原理:训练时相当于随机丢弃部分神经元,训练多个子网络,测试时融合这些子网络,减少过拟合;同时打破神经元间的共适应,提升鲁棒性。
激活函数作为层(如nn.ReLU)和作为函数(F.relu)使用有何区别?¶
作为层(nn.ReLU())是一个 nn.Module,可注册到模块树中,受 model.to() 影响;作为函数(F.relu())是纯函数,直接操作张量,无状态。
两者功能等价,层方式更模块化,函数方式更简洁。对于无参数激活函数,选择主要看代码风格。
使用inplace=True的激活函数需要注意什么?¶
原地操作会直接修改输入张量,可能导致反向传播时无法正确恢复前向激活值,从而报错。因此,如果张量需要梯度,不应使用原地激活,除非确保该张量之后不会再被用到。通常默认不使用 inplace=True。
如何自定义一个激活函数并集成到nn.Module中?¶
继承 nn.Module 并实现 forward 方法。如果需要可学习参数,在 init 中定义 nn.Parameter。
使用时像其他层一样实例化并调用。
交叉熵损失nn.CrossEntropyLoss内部包含了什么操作?输入有什么要求?¶
内部组合了 LogSoftmax 和 NLLLoss。
输入要求:
-
预测值:原始logits,形状
(N, C),无需经过softmax。 -
目标值:类别索引,形状
(N,),每个值在[0, C-1]内。
示例:
loss_fn = nn.CrossEntropyLoss()
logits = torch.randn(10, 5)
targets = torch.randint(0, 5, (10,))
loss = loss_fn(logits, targets)
注意网络最后一层不应加激活,直接输出logits以保证数值稳定性。