跳转至

四:数据处理与加载

自定义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) 或字典。

常见格式:

  1. 监督学习(图像分类):返回 (image_tensor, label),其中 image_tensor(C, H, W) 的张量,label 是整数标量。

  2. 自然语言处理(文本分类):返回 (text, label) 或预先 tokenize 的 (input_ids, attention_mask, label)

  3. 多任务/复杂场景:返回字典 {'image': img, 'mask': mask, 'label': label},这时必须配合自定义 collate_fn 来正确堆叠字典中的每个字段。

  4. 生成式任务:对于指令微调,常返回 (prompt, response) 或已经编码的 input_idslabels

关键: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_sizeshuffle 参数的作用是什么?

  • 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_fnDataLoader 的关键组件,它接收一个样本列表(来自 getitem 的多个返回),并负责将它们组合成一个批次张量。

默认行为(collate_fn=None):

  • 将样本按第一维(batch 维度)堆叠为张量,要求所有样本形状一致。

  • 如果样本是 (input, target) 元组,会分别堆叠为 (batched_input, batched_target)

需要自定义的情况:

  1. 变长序列:NLP 中的文本长度不一,需要填充(padding)到相同长度,并生成 attention_mask

  2. 数据类型复杂:返回的是字典,包含图像、字符串、不同维度的标签等,需要分别处理。

  3. 特殊预处理:需要执行批次级别的归一化、数据增强,或需要生成 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,可通过 DataLoadersampler 参数指定。


如何使用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)

image.png

可以通过 print(layer.weight.shape) 验证。


给定输入尺寸、卷积核等参数,如何计算Conv2d的输出尺寸?

公式为:

image.png

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

image.png

示例:输入 (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_meanrunning_var(指数移动平均)。 评估模式:直接使用训练阶段积累的 running_meanrunning_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

class Swish(nn.Module):
    def forward(self, x):
        return x * torch.sigmoid(x)

使用时像其他层一样实例化并调用。


交叉熵损失nn.CrossEntropyLoss内部包含了什么操作?输入有什么要求?

内部组合了 LogSoftmaxNLLLoss

输入要求:

  • 预测值:原始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以保证数值稳定性。