ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

PyTorch图像预处理三件套:Resize、RandomCrop、Normalize实战详解

PyTorch图像预处理三件套:Resize、RandomCrop、Normalize实战详解 我最早带零基础学员做Pytorch项目的时候发现十个人里有七八个会卡在同一个地方——不是模型结构看不懂也不是训练循环不会写而是数据预处理这一层“黑盒”。图片明明看着好好的一跑就报错或者loss死活不降。今天这期零基础入门四就把torchvision.transforms里出现频率最高的三个操作一次性讲透Resize、RandomCrop、Normalize再配一个能直接跑的示例保证你看完就能在自己的项目里用起来。这期内容适合正在学Pytorch、准备做图像分类/目标检测、或者刚接触深度学习却对transforms一知半解的朋友。我会尽量把“每个操作到底在干什么”“为什么这么写”“有哪些坑”都讲清楚而不是只丢给你一段会跑的代码。看完之后你再看到网上形形色色的预处理代码就不会只是复制粘贴了。1. 先把三个操作在Pytorch里的大致位置弄清楚1.1 数据预处理在训练管线中的角色一张图片从硬盘里被读进来到真正喂进模型中间一般要经过这样几步图片读取 - 尺寸调整 - 裁剪 - 转Tensor - 归一化 - 对齐batch。这三者都属于“图片到张量”这一过程中的环节。如果你用的是公开数据集比如CIFAR10、ImageNet这些操作已经在库内部帮你接好了但如果自己收集图片、自己做数据集里的__getitem__就一定要亲手写预处理。用一个生活类比模型就像一台对原料有严格要求的加工机器有的机器只收50x50x3的口径有的机器要求数值在01之间有的则要求在均值0、方差1的分布下才好处理。你从菜市场买回来的土豆大小各异、还带泥预处理就是“洗土豆、切土豆、按标准分量装袋”的环节。Resize负责把不同尺寸的图统一成模型需要的尺寸RandomCrop负责随机切出一块作为训练样本Normalize负责把像素数值调整到合适的范围。很多零基础朋友容易忽略的位置是这些操作并不是模型训练过程中“自动完成的”而是发生在Dataset.__getitem__里、或者用DataLoader批量取数的时候。换句话说它们是CPU上处理的虽然简单但每一步的耗时都会被放大——数据集越大预处理代码的质量影响越明显。1.2 为什么用torchvision的transforms而不是自己手写当然可以手写。你完全可以用PIL自己写一个resize函数再用numpy手算归一化。但torchvision的transforms最大的价值不是“帮你省几行代码”而是组合起来方便、有统一接口、默认实现经过大量项目验证。from torchvision import transforms transform transforms.Compose([ transforms.Resize(128), transforms.RandomCrop(112), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这段代码你看一眼就知道它做了四件事顺序也清楚。而Compose内部会把上面每个变换依次作用到输入图片上像一条流水线。自己手写当然也可以但组合、调试、随机性控制都要自己维护尤其当你需要“训练集和验证集用不同变换”的时候用Compose分别定义两条流水线会清晰很多。不过也正因为是“黑盒”很多人用了一段时间还是搞不清里面到底发生了什么。比如RandomCrop在Resize之后和Resize在RandomCrop之后结果完全不同这个顺序问题很多人第一次都会踩。我后文会单独展开。1.3 跑示例前先把环境准备好如果你是第一次跑代码建议用Pytorch官方推荐的组合Python 3.8及以上搭配较新的Pytorch稳定版torchvision版本尽量和Pytorch大版本对齐否则可能遇到torchvision.transforms里某些接口不存在、或者导入失败的问题。pip install torch torchvision如果是用Anacondaconda install pytorch torchvision -c pytorch理论上这期内容只需要CPU就能跑不需要先配好CUDA。你只要能把import torch、import torchvision执行成功就行。代码里我会用一张本地图片做演示如果没有图片用PIL随机生成一张纯色图也完全可以验证流程。2. Resize统一图片尺寸是模型输入的第一道关卡2.1 Resize到底做了什么transforms.Resize做的事情很简单把输入的PIL图像或Tensor缩放到指定大小。默认使用双线性插值。它的接口长这样transforms.Resize(size, interpolationtransforms.InterpolationMode.BILINEAR)size可以是一个整数也可以是一个(height, width)元组。如果传的是整数比如Resize(128)Pytorch会按短边缩放保持原图宽高比不变让短边长度等于128长边按比例缩放。如果传的是元组比如Resize((128, 128))那就直接把图像拉伸到128x128不保证宽高比。很多模型输入需要固定尺寸例如224x224、256x256所以Resize((224, 224))是最常见的写法。但如果你希望保留比例再配合裁剪来补足尺寸那就得把这两个操作组合起来用。这里有个细节经常被忽视Resize本身是有信息损失的。缩小图片会丢失高频细节纹理、边缘放大图片会引入插值产生的模糊。所以“一次到位”直接Resize((224, 224))并不是所有场景的最佳选择我后面会讲一个更稳妥的套路。2.2 Resize参数怎么选先看图片的原始尺寸和模型输入尺寸差异。如果原图是2000x1500模型输入是224x224直接一次性缩到224x224会让很多小目标细节直接消失。比较好的做法是分两段先缩放到宽边约256512再在后续操作进一步调整。如果原图和目标尺寸差距不大一次性Resize(224)就够了。再看插值方式。torchvision默认的InterpolationMode.BILINEAR在大多数情况下都够用。如果缩小的比例很大可以考虑InterpolationMode.AREA它在缩小图片时按区域平均像素能保留更多信息如果是放大图片InterpolationMode.LANCZOS质量更高但速度慢。实际项目里我见过不少老代码直接用PIL.Image.BILINEAR也能跑只是API在不同版本间有点混乱。关于目标尺寸还有一个常见选择“不让长边超过上限”。比如模型输入可以是任意尺寸但显存有限就把长边限制在512或640短边等比缩放。这时你的Resize只是预处理的第一步后面模型内部可能还有自适应池化之类的机制。这种情况下只传一个整数会更合适。2.3 实际项目里Resize常用的组合套路我自己在图像分类项目里最常用的一套组合是先Resize(256)再RandomCrop(224)。这里的思路是先用双线性插值把图片缩放到一个比最终输入稍微大一点的尺寸然后通过随机裁剪切出224x224区域。这样既能保证输入尺寸统一又相当于做了一次随机缩放给训练过程增加了一点尺度变化。如果图片很小比如原始图只有100x100目标尺寸224x224那直接Resize(224)会把图片放得很模糊没有足够多的细节。这时候需要优先考虑数据本身的清晰度而不是盲目追求模型的输入尺寸。遇到这种情况我更建议先检查一下数据分布了解图片的真实尺寸范围再决定处理策略。还有一个很容易被忽略的点Resize默认接受PIL图像不接受numpy数组。如果你从cv2.imread读进来的图片是numpy格式要先用Image.fromarray转成PIL或者单独使用其他支持Tensor的变换方式。这里不处理好后面一进transforms.Compose就会报类型错误。3. RandomCrop数据增强的“性价比”之王3.1 RandomCrop做什么transforms.RandomCrop(size)的效果是在输入图片上随机选取一个位置裁剪出一块指定大小的区域。它和CenterCrop的区别就是“随机”二字——每次调用裁出来的位置可能都不一样。transforms.RandomCrop(224) # 或 transforms.RandomCrop((224, 224)) # 还可以在裁剪的同时填充边缘 transforms.RandomCrop(224, pad4)随机裁剪为什么有用因为它相当于在训练时给模型提供了“同一张图的不同视图”同一只猫这次裁到的是猫脸下次裁到的是猫身子再下次裁到的是猫爪子。模型看到的是同一个对象在不同位置、不同上下文下的形态这会迫使模型学会“对象本身是什么”而不是死记“某个像素位置出现什么”。这是提升模型平移不变性最直接的手段之一。另一个层面RandomCrop也会间接带来一些尺度变化。因为你先放大图片再随机裁剪等于每次裁到的区域对应的原始范围不一样模型就会适应目标在不同尺度下出现的情况。这和“Multi-Scale Training”的思想是一致的。3.2 RandomCrop和CenterCrop怎么选训练集一般用RandomCrop验证集和测试集用CenterCrop。原因很简单训练时我们希望数据越多样越好但评估时又希望结果可复现、稳定。如果验证集也用RandomCrop同一张图在两次评估中可能得到不同的裁剪结果模型输出的指标就会有波动你很难判断到底是谁在进步。“验证集用CenterCrop”这个习惯要尽早养成。很多第一次做项目的朋友把训练和验证用同一个transform结果模型在训练集上acc很高、验证集上却忽高忽低还以为过拟合了其实是验证时的随机性在捣乱。另外还有个很常见的场景如果你的模型输入是224x224但图片本身非常不规则比如全景图、长截图RandomCrop容易裁到没有内容的背景区域导致训练样本质量下降。这种情况下可以考虑先把长边缩放到固定值再结合RandomResizedCrop去做尺度随机的裁剪而不是单纯用RandomCrop。3.3 ResizeRandomCrop的经典组合前面提过我常用Resize(256) RandomCrop(224)。这里展开说下为什么这个组合比直接Resize(224)更靠谱。如果你直接把图缩到224x224再做随机裁剪就无从下手了——因为图已经和目标尺寸一样大裁剪只会得到完全相同的图。所以必须保证RandomCrop的输入尺寸严格大于输出尺寸随机性才有意义。Resize(256)之后再裁224裁出来的窗口可以在16x16种位置变化训练样本的多样性马上就上去了。组合时要注意顺序。如果先RandomCrop(224)再Resize((224, 224))那么裁出来的区域大概率是低分辨率的局部放大模糊程度可能会影响训练。一般建议先放大到较大尺寸、再裁剪到最终尺寸。当然也有例外如果原图很大先裁剪再缩放反而能减少内存占用这时候顺序就要反过来。没有绝对正确的顺序需要根据你的数据尺寸、模型输入、性能需求权衡。顺便提一嘴torchvision里还有RandomResizedCrop它把“缩放”和“裁剪”合成了一个操作能随机选取原图上一个区域再把该区域缩放到指定尺寸。从效果上比Resize RandomCrop更接近尺度增强但可控性稍弱一点。两者都可以用看你习惯哪种。4. Normalize用均值和方差把数据“校准”到模型友好区间4.1 Normalize的数学原理transforms.Normalize(mean, std)在实际执行时对每个通道做这样的计算x_norm (x - mean) / std它期望的输入是范围[0, 1]的浮点Tensor通常来自transforms.ToTensor()。如果直接对PIL图像做NormalizePIL图像范围是[0, 255]mean、std定义完全对不上结果会非常离谱。ToTensor做的事有两件一是把PIL图像或numpy数组转成torch.FloatTensor二是把像素值从[0, 255]缩放到[0, 1]。也就是说在Normalize之前你的图像数据已经被转换了一次“计量单位”。那为什么要再做一次Normalize因为[0, 1]范围虽然比[0, 255]好用但不同图片的亮度、对比度差异还是很大。有的图平均亮度0.9有的图平均亮度0.3喂给模型后模型看到的输入特征分布不稳定梯度更新的方向就容易波动。Normalize的目的就是让每个通道的像素分布接近“均值0、方差1”的标准正态分布也就是给数据做一次“标准化”让不同样本的数值尺度保持一致。4.2 mean和std从哪里来很多人第一次看示例代码都会困惑mean[0.485, 0.456, 0.406]这几个数哪来的这是ImageNet数据集的统计结果。ImageNet作为目前最常用的图像预训练数据集之一它的RGB三通道均值和标准差被广泛复用。你在很多预训练模型的代码里都会看到这三个数字。如果自己的数据集和ImageNet差异不大比如自然图像分类任务直接用ImageNet的mean/std是常规操作。这样做还有个额外的好处加载ImageNet预训练权重时输入数据的分布和预训练时的分布保持一致模型微调更容易收敛。如果你的数据集很特殊比如医学影像、红外图像那最好自己统计。统计方法很简单把训练集所有图片转成Tensor在所有样本上计算每个通道的均值和标准差。这里有个细节统计时像素范围一定是[0, 1]不是[0, 255]否则算出来的mean和std会大得离谱。另一个细节是通道顺序PIL加载的RGB图片在ToTensor后是(C, H, W)你要算的是每个通道自己的均值和方差不要搞混了。4.3 Normalize的常见坑第一个坑直接用numpy手算RGB均值时忘了把像素范围除以255。你如果统计出来的均值是[123, 116, 103]这种说明你算的是[0, 255]范围而Normalize期望的是[0, 1]范围的mean。正确的统计应该在ToTensor之后进行或者手动把统计结果除以255。第二个坑把Normalize放在ToTensor之前。我看到很多新手代码写成Compose([Normalize(...), ToTensor()])结果不是报错就是数值完全不对。Normalize不认识PIL图像也不会自动帮你转格式必须先ToTensor再Normalize顺序不要记反。第三个坑可视化时忘记“反归一化”。训练途中你想把输入图片打印出来看看如果直接打印Normalize之后的图会发现颜色很奇怪像是加了滤镜一样。这是因为数据分布被改变了。要想看原始图需要先乘std再加mean再乘255或者把像素值截断到[0, 1]再显示。第四坑有时候你只对RGB三个通道中的某些通道做Normalize比如灰度图只有一个通道。这时候应该传一维的mean/std不要照抄三通道的值。5. 跑一个完整示例三操作串联的实操演示5.1 第一个示例单张图片的变化过程我先演示一个基础示例用一张本地的cat.jpg图片依次经过Resize RandomCrop ToTensor Normalize观察输出结果。import torch from PIL import Image from torchvision import transforms # 定义预处理流水线 transform transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 读取图片 img Image.open(cat.jpg).convert(RGB) print(原始尺寸:, img.size) # 应用预处理 out transform(img) print(变换后shape:, tuple(out.shape)) # 应该是 (3, 224, 224) # 统计每个通道的均值和方差肉眼观察是否接近0和1 for i, name in enumerate([R, G, B]): print(f{name}通道均值: {out[i].mean():.4f})运行结果大概长这样原始尺寸: (640, 480) 变换后shape: (3, 224, 224) R通道均值: 0.4821 G通道均值: 0.4503 B通道均值: 0.4082可以看到输出shape固定在了(3, 224, 224)。每次运行这段代码由于RandomCrop是随机的out的内容都会不同但你打印出来的三个通道均值会比较稳定。如果哪一次输出的均值偏差特别大通常是因为裁剪到了一大片空背景导致颜色分布变化这属于正常现象也是随机裁剪的“副作用”。这里还可以做一个操作把RandomCrop(224)换成CenterCrop(224)再跑一遍对比两次输出你就会直观感受到“随机裁剪”带来的变化到底有多大。5.2 第二个示例在Dataset里应用并使用DataLoader加载实际项目里transforms.Compose很少单独作用于一张图片而是放在自定义Dataset里。这里写一个简化版例子展示训练集和验证集如何使用不同的预处理流水线import os import torch from PIL import Image from torch.utils.data import Dataset, DataLoader from torchvision import transforms class ImageFolderDataset(Dataset): def __init__(self, file_list, transformNone): self.file_list file_list self.transform transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): img Image.open(self.file_list[idx]).convert(RGB) if self.transform: img self.transform(img) return img train_files [cat.jpg, dog.jpg, bird.jpg] # 实际项目里换成你的图片路径 val_files [cat2.jpg, dog2.jpg] train_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]) ]) 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]) ]) train_dataset ImageFolderDataset(train_files, transformtrain_transform) val_dataset ImageFolderDataset(val_files, transformval_transform) train_loader DataLoader(train_dataset, batch_size2, shuffleTrue) val_loader DataLoader(val_dataset, batch_size2, shuffleFalse) for batch in train_loader: print(训练batch shape:, batch.shape) break注意这里我在训练里加了RandomHorizontalFlip它和RandomCrop一样都是不改变内容语义的数据增强方式。验证集里没有加目的是保证评估结果稳定。如果是一开始就想快速跑通最简单的做法是先不写Dataset只对一个图片调用transform确认输出shape符合预期再接进DataLoader。这样做的好处是如果出了bug你能快速判断问题出在预处理还是数据加载环节。5.3 示例运行效果和验证方法跑完上面的代码你应该能看到类似下面的输出训练batch shape: torch.Size([2, 3, 224, 224])如果batch_size2每个样本shape是(3, 224, 224)所以整个batch是(2, 3, 224, 224)。这说明你的数据流水线已经通了每一张图都经过了Resize、RandomCrop、ToTensor和Normalize并且被正确堆叠成一个batch。我还会习惯检查一下normalize是否真的生效。最简单的方法是直接比较Normalize前后的输出transform_no_norm transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.ToTensor() ]) transform_with_norm transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(cat.jpg).convert(RGB) a transform_no_norm(img) b transform_with_norm(img) print(归一化前像素范围:, a.min().item(), a.max().item()) print(归一化后像素范围:, b.min().item(), b.max().item())归一化前a的像素范围应该在[0, 1]之间归一化后b的范围通常会超出这个区间可能出现负值这才是Normalize生效的正常表现。6. 常见问题速查与我的几条实操心得6.1 高频报错和排查建议很多报错其实就集中在几个固定点上。我把平时带人和自己踩过的高频问题整理出来报错信息原因解决方案size of the cropped area must be smaller than the imageRandomCrop的尺寸大于图片当前尺寸在RandomCrop之前先Resize到更大的尺寸Input type (PIL.Image.Image) is not supported对一个PIL图像直接调用了Normalize把ToTensor放在Normalize前面Expected tensor with shape (C, H, W)输入了(H, W, C)格式的numpy数组先用ToTensor()转换或调整数据维度图像显示偏色/惨白Normalize之后未做反归一化直接可视化用std * x mean还原再乘255训练集acc高、验证集acc不稳定验证集也用了RandomCrop验证集改用CenterCrop或不裁剪mean和std填错了直接用了[0, 255]范围统计结果ensure统计基于0-1范围前三个报错是零基础同学最常遇到的。尤其是第二个几乎每周都有人来问。记住一句话RandomCrop和Resize可以处理PIL图像ToTensor负责把PIL图像变成TensorNormalize只能在Tensor上操作。顺序别搞反。6.2 我建议的“预处理三板斧”配置公开数据集、自然图像分类、常规CNN模型我推荐的基础配置是train_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]) ]) 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]) ])这套配置在大多数任务上都能直接跑出不错的效果而且不容易出错。简单解释一下为什么Resize(256)RandomCrop(224)给训练集提供随机位置裁剪RandomHorizontalFlip增加了水平翻转的数据增强CenterCrop(224)保证验证集稳定可复现Normalize用ImageNet统计值把数据分布统一。四个操作互相配合覆盖了尺寸控制、数据增强、数值标准化三个层面。如果你的数据集本身图片很大或者训练样本很多我偶尔会把Resize(256)改成训练时RandomResizedCrop(224)验证时Resize(256) CenterCrop(224)。但这是进阶操作新手先用最大化兼容性的方案即可。6.3 一些容易被忽略的细节第一归一化的均值方差不是随便填的。如果你用的是Pytorch自带的预训练模型模型在ImageNet上训练时的预处理就是Normalize(mean[0.485,...])所以加载预训练权重时也要用同一套参数这个坑不排查清楚微调效果会明显变差。第二代码里尽量用一个get_transform(trainTrue/False)返回不同流水线不要训练和验证各自写一遍容易抄错。用函数封装的好处是以后想加ColorJitter、RandomRotation这些增强只需要改一处验证集的稳定性不会被动到。第三检查流水线时打印shape只是第一步打印像素分布也很重要。我通常会在第一次跑通代码后加一行print(batch.min(), batch.max(), batch.mean())。如果mean离0特别远说明normalize的参数和你的数据分布不匹配要重新考虑是否应该自己统计均值方差。第四不要把Resize和RandomCrop的目标尺寸搞混。RandomCrop的size必须是最终输入sizeResize的size只是中间过渡没必要也一样。网上有些代码喜欢Resize(224)RandomCrop(224)这样看起来没报错但实际上随机裁剪没有任何意义——输入尺寸已经等于输出尺寸裁出来的永远是同一张图。我自己用的时候尺寸差个32或64训练效果会有可感知的提升。最后再分享一个个人经验调试预处理时尽量先用1-2张图片单独跑确认尺寸、数值范围都符合预期后再丢进DataLoader。因为DataLoader的多进程加载会把报错信息夹杂在训练日志里新手很容易被带偏。先在小样本上确认流水线正确再全量跑训练这个习惯能帮你省下大量排查时间。整个预处理流程说白了就是“按顺序、看形状、看分布”把这三件事做到位Resize、RandomCrop、Normalize这关就算真正过了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表