做深度学习图像方向的朋友,每天打交道最多的除了模型本身,恐怕就是数据怎么喂给模型这件事了。PyTorch里的transforms工具箱,就是专门干这个的——它把图像从"读进来"到"能进网络"之间的所有琐碎步骤,统一封装成一套可组合、可复用的预处理流水线。我在多个图像分类、目标检测项目里都用它,可以说只要接触PyTorch图像任务,transforms就是绕不开的基础设施,而且它的设计思路往往直接影响训练效果的上限。
这篇文章写给两类人:一类是刚入坑PyTorch、被各种ToTensor和Normalize搞晕的初学者,另一类是已经在跑模型但总觉得预处理环节"差点意思"的进阶玩家。我会从transforms的整体设计思路讲起,把每个常用操作背后的原理、参数选择依据、实际踩坑经验都掰开揉碎,最后给出一套可以直接抄作业的完整流程。读完你不仅能熟练使用,还能自己动手写符合业务需求的transform。
1. 整体设计思路:为什么PyTorch要单独做一个transforms工具箱
1.1 图像预处理到底在解决什么问题
很多人一开始觉得预处理就是"把图片缩小、转成张量",其实远不止这么简单。深度神经网络对输入数据有非常明确的要求:数值范围要稳定、尺寸要统一、批次内的数据要能对齐。而原始图像是以像素矩阵的形式存在的,每个像素值是0到255的整数,通道排列可能是HWC(高宽通道)也可能是CHW(通道高宽),这些如果不做处理,直接丢给模型,轻则训练不收敛,重则直接报错。
更关键的是,深度学习模型的泛化能力很大程度上依赖训练数据的多样性。如果每次喂给网络的都是同一批图片、同一个角度、同一种亮度,模型很快就"背"下了训练集,而不是真正学到特征。预处理的另一个核心任务就是做数据增强,在不改变语义的前提下,通过随机翻转、裁剪、色彩扰动等方式,变出更多"看起来不太一样但本质相同"的样本。
transforms这个工具箱的设计初衷就是应对这两类需求:它把确定性变换(比如缩放、归一化)和随机性变换(比如随机翻转、随机裁剪)统一在一个接口下,再用Compose把它们串成一条流水线,让用户能用几行代码就搭好一套完整的数据处理流程。
1.2 从PIL到Tensor:数据流在transforms里怎么走
要理解transforms,必须先懂它的数据流转路径。PyTorch的图像预处理管道通常是这样工作的:数据从磁盘上读进来时,最常见的形式是PIL图像或NumPy数组,它们是HWC格式、数值范围0到255;经过一系列几何和色彩变换后,在进入模型前必须转换成CHW格式、数值范围约0到1的浮点张量,这一步就是ToTensor干的活;如果还要做标准化,再通过Normalize把每个通道的分布拉成均值为0、方差为1的状态。
这条链路里有个特别容易忽略的点:很多transform(比如RandomCrop、RandomHorizontalFlip)要求输入是PIL图像或张量,但两者行为有细微差别。PIL模式下某些随机操作使用的是PIL自带的采样逻辑,而张量模式下用的是PyTorch的实现。我在项目里踩过一次坑:同样的随机裁剪,用PIL输入和用张量输入,产出的图片细节不一样,虽然不影响大局,但复现实验时会让人摸不着头脑。所以我的建议是,在一个项目里固定输入类型,不要混着用。
1.3 transforms的可组合性为什么这么重要
transforms最大的优势不是某个单独的函数写得多好,而是它把每个操作拆成了独立的小积木。Resize只管尺寸,RandomRotation只管角度,ColorJitter只管颜色,它们之间没有依赖,你可以像搭积木一样自由组合。这种设计的价值在实际项目中会体现得非常明显:不同的数据集、不同的模型,需要的数据处理策略完全不同。
比如做迁移学习时,预训练模型对输入尺寸有固定要求(ResNet系列通常要224×224),你只需要在Compose链里加一个Resize(224);但如果做目标检测,图像尺寸往往要按批次动态调整,这时就不能用固定Resize,得用自定义逻辑。可组合的设计让你能根据任务灵活删改,而不是为每种情况都写一套新代码。这也是为什么我强烈建议新人不急着写自定义逻辑,先把官方提供的二十几个transform吃透,因为绝大多数需求官方工具箱已经覆盖了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心transform逐个拆解:从ToTensor到Normalize
2.1 ToTensor:所有预处理的第一步,也是坑最多的一步
transforms.ToTensor()做的事情可以用一句话概括:把PIL图像或NumPy数组从HWC格式、0到255的整数范围,转成CHW格式、0.0到1.0的浮点张量。这个转换是硬性的,它把像素值除以255,同时调整通道顺序。为什么PyTorch要这么设计?因为神经网络内部大量使用的激活函数和损失函数,都是在0到1或-1到1的数值范围内表现最佳,直接喂0到255的整数会让梯度计算变得不稳定。
使用ToTensor时有一个新手特别容易踩的坑:如果输入已经是torch.Tensor类型,ToTensor会直接原样返回,不会帮你重新缩放。什么意思?就是你把一张图片已经用PIL读进来再转成了Tensor,然后再传给ToTensor,它并不会再做一次除以255的操作,数据值还是原来的。这在训练流程里会导致两个批次的数据分布不一致,训练直接出问题。正确的做法是:要么始终从PIL图像开始,让ToTensor统一做转换;要么自己做手动归一化,不要混用。
另外还有一个颜色通道的问题。默认情况下,PyTorch训练时用RGB顺序,但很多图像库(比如OpenCV)默认读出来是BGR顺序。如果用OpenCV读了图再喂给transforms,你会发现模型训练出来的结果颜色怪怪的,准确率还上不去。解决办法就是在预处理链最前面加一步通道反转,或者统一用PIL读图。
2.2 Resize与尺寸策略:如何选择你真正需要的尺寸
python复制transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
])
这段代码是经典的ImageNet预处理风格,很多人直接照抄,但未必理解为什么要在Resize(256)之后再来一个CenterCrop(224)。原因在于:直接Resize到224的话,会把原本长宽比不同的图片强行拉伸成正方形,画面会发生畸变,物体看起来被压扁或拉长,这对模型学习特征是负面干扰。先缩放到短边等于256,让长宽比保持不变,然后再从中间裁出一块224×224的区域,既能保证尺寸统一,又最大程度保留了原始画面的比例。
那256和224这两个数字是怎么定的?它们是经验值。224是ResNet等主流分类网络的标准输入尺寸,256则是为了在裁剪时留出一点余量。如果你用的是其他尺寸的骨干网络(比如ViT用384),对应策略就是Resize(400)加CenterCrop(384)。这里的关键心法是:Resize的目标是保证裁剪后有足够的像素信息,所以它通常比最终输入尺寸大10%到15%。
实际项目中还有一个坑:Resize的插值算法。默认情况用的是双线性插值(InterpolationMode.BILINEAR),但如果是做分割任务或者处理文字类图片,双线性插值会让边缘变糊,这时建议改用InterpolationMode.NEAREST或InterpolationMode.LANCZOS,能保留更多硬边缘信息。我做过一个车牌识别项目,刚开始用默认的双线性插值,小字总是识别错,换成LANCZOS之后准确率立刻提了好几个点。
2.3 Normalize:标准化参数不是随便填的
Normalize是transforms里最容易被误解的操作。很多人从网上抄到一行:
python复制transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
然后就直接用了,但不知道这组数字是ImageNet数据集的统计值,也只适用于在ImageNet上预训练过的模型。Normalize做的事情是把每个通道的数据减去均值再除以标准差,公式是 (x - mean) / std。之所以要这么做,是因为神经网络在面对均值不为0、数值范围波动大的输入时,梯度更新容易抖动,收敛也慢;把数据标准化成均值为0、方差为1的分布后,优化过程会稳定很多。
问题来了:如果你训练一个自己的数据集,或者加载一个在别的数据集上预训练的模型,mean和std该怎么选?三种情况分开说。第一种,用官方在ImageNet上预训练的权重做迁移学习,直接用上面那组标准值就行,因为模型在训练时看到的输入就是这样处理的,你预处理越接近它训练时的样子,迁移效果越好。第二种,从零训练一个新模型,建议在训练集上统计每个通道的均值和标准差,代码很简单,几行就能算出来,不要拍脑袋填。第三种,输入是灰度图或单通道数据,mean和std对应的维度也要改,否则维度不匹配会直接报错。
还有一个细节:Normalize必须在ToTensor之后调用,因为它处理的是张量数据。如果你先Normalize再ToTensor,会先对0到255的整数做标准化,数值范围完全乱掉,模型根本训不动。这个顺序错误我在代码评审里见过好多次,每次都要特别强调。
3. 数据增强核心transforms:让模型见过的世界更丰富
3.1 随机翻转与随机裁剪:最便宜有效的正则化手段
数据增强里性价比最高的两个操作就是随机水平翻转和随机裁剪。RandomHorizontalFlip(p=0.5)的意思是以50%的概率把图片水平翻一下。为什么是0.5?因为这样训练集中原图和镜像图的比例是均衡的,不会让模型对某一侧产生偏好。这个操作对自然图像特别有效——一个物体水平翻转之后语义完全不变,但模型被迫学习更稳健的特征。
RandomCrop则更讲究。它不像CenterCrop那样每次裁的都是中间区域,而是每次随机从图片里取一块。这个操作强迫模型不要依赖物体的固定位置来判断类别,因为同一个物体可能出现在画面的任意位置。但RandomCrop有个风险:随机裁剪可能把物体截掉一半,导致语义丢失。解决方法是配合Pad使用,或者采用RandomResizedCrop。
RandomResizedCrop是RandomCrop的增强版,它会先随机选一个缩放比例(比如0.08到1.0),再按随机长宽比裁剪,最后resize到目标尺寸。这是SimCLR等自监督方法的核心增强手段,也适合分类任务。它的参数里有个scale=(0.08, 1.0),意思是裁剪区域最小可以只有原图的8%,这个下界不能设得太高,否则模型只在"看全图",学不到局部特征。
3.2 ColorJitter:调整亮度、对比度、饱和度和色相
ColorJitter是一个很强大也很危险的操作。它统一管理四个参数:brightness(亮度)、contrast(对比度)、saturation(饱和度)、hue(色相)。用法如下:
python复制transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1)
每个参数的含义范围不一样。brightness、contrast、saturation的取值范围是0到1之间的倍数偏移,比如0.2表示在0.8到1.2倍之间随机调整;hue的取值范围是-0.5到0.5,表示色相偏移的程度,一般建议不超过0.1,超过这个值颜色会变得非常诡异。
这个操作的危险性在于:如果增强力度太大,模型会学到"色彩扭曲"这种假特征,训练集上效果很好,一测真实数据就打回原形。我见过一个反例:有人为了提升鲁棒性,把ColorJitter的每个参数都调到0.5,结果模型在正常图片上的准确率反而掉了一大截,训练集和验证集严重失衡。原因就是训练时看到的颜色分布和真实世界差太远了。合理的做法是保持亮度、对比度、饱和度在0.1到0.3之间,色相在0.05到0.1之间,并且可以根据业务场景做取舍。比如夜间图像任务,亮度扰动可以适当调大;医学影像分割,色相扰动一般要关掉,因为颜色本身可能就是诊断特征。
3.3 自动化增强:RandAugment和TrivialAugment这类新玩法
如果嫌手动调增强参数太麻烦,PyTorch在torchvision.transforms.autoaugment里提供了自动化方案。目前主流的是RandAugment和TrivialAugmentWide。RandAugment的核心思想是:维护一个增强操作池(比如旋转、剪切、颜色变换、对比度等十几种),每次随机从池子里选N个操作,用统一的幅度M来执行。
python复制transforms.RandAugment(num_ops=2, magnitude=9)
这里的num_ops=2表示每次选两个操作叠加,magnitude=9表示增强强度(范围0到30,越大越猛)。这套方案的亮点是少了一个需要人工搜参的维度:你只需要调两个超参数,而且默认值在大多数图像分类任务上都表现不错。TrivialAugment更激进,它每次只随机选一个操作、用完全随机的强度,连超参数都不用调了,实验结果在很多数据集上和RandAugment打平甚至更好。
我个人的建议是:如果你的任务算力充足、训练数据量大,可以试试RandAugment;如果数据量小,想快速提升鲁棒性,TrivialAugment的开箱即用体验更好。但要注意,这类自动增强对某些特定任务不一定友好。我做过一个医学图像项目,RandAugment里的锐化操作把病灶细节弄得特别怪异,后来我改成自定义一个只包含几何变换的增强池,效果反而更好。自动化方案是好工具,但绝对不是万能解。
4. Compose组合调度与自定义transform
4.1 Compose的串联逻辑:顺序决定了结果
transforms.Compose是transforms的编排核心,它接收一个transform列表,然后按顺序依次执行:
python复制transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225]),
])
这个顺序不是随便排的,背后有逻辑。几何类操作(Resize、Crop、Flip、Rotation)放在最前面,因为它们操作的是PIL图像,输入输出格式一致;接着是色彩类操作(ColorJitter),也要在PIL阶段做,因为转成Tensor后色彩调整要涉及复杂的数值计算;然后是ToTensor,做数据格式转换;最后是Normalize,做数值标准化。一旦ToTensor执行完,后面只能接数值型变换,接不了任何图像几何操作。
这个顺序问题我还想多说一句:随机操作在Compose里的位置会影响最终效果。比如先RandomResizedCrop再RandomHorizontalFlip,和反过来,生成的样本分布是不一样的。前者先裁剪再翻转,后者先翻转再裁剪,虽然最终每张图也是又裁又翻,但两个随机过程叠加后覆盖的样本空间会有细微差别。如果做消融实验,这类细节要固定下来,否则实验结果根本没法复现。
4.2 用类的方式自定义transform:完整可复用的做法
虽然官方transform覆盖了大多数场景,但实际项目中总有它满足不了的需求,比如给图片加噪声、做特定的掩码处理、实现mixup风格的样本混合。这时候就需要自己写transform了。官方推荐的自定义方式是实现一个类,类里面要有__call__方法,这样实例才能被Compose调用:
python复制class RandomNoise(object):
def __init__(self, noise_level=0.05):
self.noise_level = noise_level
def __call__(self, img):
if not isinstance(img, torch.Tensor):
raise TypeError(f"Expected torch.Tensor, got {type(img)}")
noise = torch.randn_like(img) * self.noise_level
return img + noise
def __repr__(self):
return self.__class__.__name__ + f'(noise_level={self.noise_level})'
写自定义transform时有几个规范要注意。第一,__call__的输入类型要明确,并在开头做类型检查,否则类型不匹配时错误信息非常难排查。第二,尽量实现__repr__方法,这个在调试时太有用了——你可以直接print整个Compose,能清楚地看到每一步是什么。第三,尽量保持transform是"纯函数",不要在里面修改全局状态或者依赖随机种子之外的不确定性,否则复现会很痛苦。
还有一点:如果transform内部有随机性,要确保和PyTorch的随机状态一致。PyTorch的随机操作基本都是基于全局随机种子生成的,所以你在transform里如果用了np.random,就要额外设置NumPy的随机种子,否则固定主种子之后每次运行的结果可能还是不一样。
4.3 Lambda与partial:快速自定义的小技巧
如果只是做一个简单操作,写一个类有点重,PyTorch提供了transforms.Lambda,可以快速地把匿名函数包装成transform:
python复制transform = transforms.Lambda(lambda x: x / 255.0)
Lambda适合那些"一次性"的简单操作,比如自定义归一化、调整通道顺序、对张量做简单的数值处理。它的缺点是序列化时只能存函数引用,如果用lambda表达式,保存和加载模型时可能找不到函数定义。所以我的经验是:开发调试阶段用Lambda图方便,但正式代码里能写成类的就写成类,能避免很多部署时的麻烦。
functools.partial也可以配合已有transform做参数固定。比如你想固定ColorJitter只用亮度扰动,可以这样:
python复制from functools import partial
brightness_only = partial(transforms.ColorJitter, brightness=0.2, contrast=0, saturation=0, hue=0)
不过partial生成的不是torch.nn.Module的实例,某些场景下直接传进Compose没问题,但如果要用到torch.jit.script或者torchvision.transforms.v2的某些特性,就会有限制。整体来说,自定义transform用类是最稳的,Lambda和partial只适合临时脚本。
5. 实操:完整图像分类任务的transforms配置
5.1 训练集和验证集为什么要用不同的transform策略
这是新手最容易忽略的问题。训练集和验证集的数据处理策略应该是完全不同的——训练集需要丰富的数据增强来扩充样本空间,验证集需要稳定可复现的确定性预处理来公平评估模型性能。
python复制# 训练集:带随机增强
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.08, 1.0)),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(10),
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05),
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]),
])
训练集里用RandomResizedCrop而不是Resize加CenterCrop,因为随机裁剪本身就提供了丰富的几何变化;验证集用Resize(256)加CenterCrop(224)则是为了平衡效率和评估公平性——每张图片都被同一种方式处理,出的指标才有可比性。我自己在实际项目中验证过:训练和验证都用同一套随机变换,验证集损失会震荡得非常厉害,几乎无法判断模型是否真的在收敛;改成确定性预处理之后,验证曲线平滑多了,早停策略也终于能正常工作了。
5.2 数据加载和transform的实际配合
transforms往往和torch.utils.data.DataLoader配合使用。一个典型的流程是:用datasets.ImageFolder指定根目录,每个子文件夹是一个类别,通过transform参数传入预处理流程;然后在DataLoader里设置batch_size、shuffle、num_workers,DataLoader会从数据集里取batch并将transform应用到每张图上。
python复制from torchvision import datasets
from torch.utils.data import DataLoader
train_dataset = datasets.ImageFolder(
root='data/train',
transform=train_transform
)
train_loader = DataLoader(
train_dataset,
batch_size=64,
shuffle=True,
num_workers=4,
pin_memory=True
)
这里的num_workers和transform执行效率有关系。num_workers是加载数据的子进程数,每个子进程都会独立执行transform pipeline。如果transform本身很重(比如大量随机裁剪、色彩扰动),数据加载就会成为训练瓶颈。一个粗略的参考:先在默认num_workers=0下跑一个epoch计时,再逐步加大worker数,观察训练速度变化。如果加了worker反而变慢,大概率是内存带宽成了瓶颈,不是CPU核数不够。
pin_memory=True这个参数也值得提一下。它把数据加载到锁页内存,能加速GPU拷贝。配合non_blocking=True使用效果更好。很多人以为这两行代码无关紧要,但在batch_size大或输入尺寸大的任务里,它能省下不少数据搬运时间。
5.3 参数选择的实际经验:小数据集和大数据集的策略差异
数据集规模不同,预处理策略应该有明显差异。数据集比较小(比如几千张图)时,数据增强要重一些,因为模型需要更多样本来避免过拟合。我建议把RandomResizedCrop的scale下界调低一点、ColorJitter强度适当加大,甚至可以考虑用RandAugment。但也别过头,增强过猛会让模型学到错误特征,我见过一个小数据集项目,增强太强导致训练集准确率都上不去。
数据集比较大(比如几十万张图)时,数据增强可以保守一些。大数据本身已经提供了足够的多样性,强行加太多随机会让训练收敛变慢。这种情况下我一般只保留RandomResizedCrop和RandomHorizontalFlip这两个基础增强,色彩扰动可以非常轻微或者干脆关掉,让模型专注在真实分布上学习。
关于尺寸,还要考虑模型的感受野。224×224的输入适合大多数分类网络,但如果目标物体很小,建议用更大的输入尺寸比如384或512。代价是计算量和显存占用会非线性上涨。我测试过用ResNet50在224和384下训练同一批数据,384的准确率能高1到2个点,但训练时间多了将近一倍。所以尺寸选择本质上是速度和精度的权衡,没有绝对最优解。
6. 常见问题与排查技巧实录
6.1 类型和维度不匹配的经典报错
transforms使用过程中报错最多的就是类型和维度问题,我把几个高频错误列在这里。
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
TypeError: pic should be PIL Image or ndarray |
输入类型不是PIL或ndarray | 检查输入是不是Tensor,如果是就改用手动数值处理 |
ValueError: Expected tensor to be a tensor image of size (C, H, W) |
张量维度不是3维 | 确认有没有增加batch维度,单张图不需要batch维 |
RuntimeError: The size of tensor a (3) must match the size of tensor b (4) |
通道数不一致 | 检查图片是否含有Alpha通道,转成RGB后再处理 |
TypeError: img should be PIL Image |
transform放在ToTensor之后 | 检查Compose顺序,几何变换必须在ToTensor之前 |
这些错误里最隐蔽的是Alpha通道问题。PNG图片可能带4通道(RGBA),直接用默认的transforms处理时会报通道数错误。解决方法是在最前面加transforms.ConvertImageDtype或者用PIL.Image.convert('RGB')转一下,统一去掉Alpha通道。
6.2 为什么训练集和验证集结果差很多
这不是模型过拟合,也可能就是预处理设置的问题。常见情况是:训练时用了随机增强,验证时忘了关掉,导致验证集评估本身就有随机性,每次都跑出不同结果。解决办法是确保验证集用的是纯确定性transform,并且和训练集除了增强部分外其他预处理完全一致(特别是Normalize的mean和std必须相同)。
还有一种容易被忽略的情况:图像增强的随机性导致训练样本分布和验证分布不一致。比如训练时用RandomErasing随机遮挡图像区域,但验证时全图可见,这种训练和测试分布差异在某些任务上会显著影响最终性能。如果业务场景中确实存在遮挡情况,验证集也应该用相应的遮挡增强来做评估,但要用固定的随机种子保证可复现。
6.3 性能排查:transform太慢怎么办
transform如果成为训练瓶颈,最典型的表现是GPU利用率很低,但CPU占用率却快满了。这时候优先做三件事:第一,检查num_workers是不是太小,调大worker数并行处理;第二,检查transform里是不是有特别重的操作,比如超大尺寸的Resize、过多的裁剪尝试;第三,检查数据是不是存在网络磁盘上,I/O等待会让整个管线完全卡住。
torchvision在较新版本里提供了transforms.v2接口,支持在Tensor和PIL之间自动切换,并且对部分数据增强操作做了性能优化。如果你的PyTorch版本允许,建议直接使用torchvision.transforms.v2,我在生产环境实测数据加载速度能提升20%到30%,而且API基本兼容,迁移成本很低。另外,torch.compile在最新的torchvision里也能和部分transform配合使用,但兼容性还不稳定,生产环境不建议直接上。
6.4 实验可复现性的一个小技巧:固定随机种子
深度学习实验的可复现性,很大程度取决于transform的随机性是否可控。最保险的做法是在训练脚本最开头设置好所有随机源:
python复制import random
import numpy as np
import torch
def set_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
这里有个坑:torch.backends.cudnn.benchmark = False会降低某些卷积层的计算速度,因为cuDNN不再自动选择最优算法。我的建议是:复现实验时开启deterministic模式,正式训练时关掉它追求速度。另外,DataLoader在num_workers > 0时,子进程的随机状态是独立的,即使设置了主进程的种子,worker里生成的随机增强也不保证完全复现。要彻底复现,需要给DataLoader传generator参数并固定worker_seed,但这个操作比较繁琐,实际项目中很少有人做到这一步,大家通常只在单卡、worker数较少时追求完全复现。
最后再分享一个我很早养成的习惯:每搭一套新数据集的transform,我都先把处理前和处理后的图像各保存几张出来看看。肉眼确认增强后的图片没有失真、没有丢信息、标签语义还正确,再开始跑训练。这个习惯帮我避开了很多低级错误——比如有一次Normalize的mean真值填反了,图片整体发蓝,模型训了三天准确率纹丝不动,就是靠这一步查出来的。预处理这个环节看起来不起眼,但它决定了整个训练流程的天花板,值得多花一点心思。
