尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

PyTorch数据预处理入门:Normalize、Resize、RandomCrop组合实战

PyTorch数据预处理入门:Normalize、Resize、RandomCrop组合实战 Pytorch|零基础入门四 NormalizeResizeRandomCrop 含运行示例我接触PyTorch这几年教过不少新人跑模型发现一个特别有意思的现象很多人能背下来torchvision.transforms里那一长串API名字但真正写训练代码时数据预处理部分永远是抄过来能用就行。直到模型训练出来效果不对或者loss不收敛才回头怀疑是不是预处理出了问题。其实数据预处理在PyTorch里地位极其特殊——它是模型看到数据的唯一入口。你喂给模型什么样的张量模型就只能从这个张量里学东西。今天这篇零基础入门第四篇我把Normalize、Resize、RandomCrop这三个最常用的transform拆开揉碎讲明白它们各自在干什么、怎么组合、参数怎么定最后给一份可以直接跑通的完整示例。适合刚装好PyTorch、准备认真跑第一个图像分类任务的朋友也适合那些已经写过训练脚本但一直没搞懂预处理细节的人。1. 为什么入门PyTorch要先搞懂这三个操作1.1 数据预处理不是伺候数据而是模型训练的一部分很多人觉得数据预处理只是把图片变成模型能吃的张量这种理解太浅了。我在实际调试中反复验证过一个结论预处理方式直接决定训练能不能收敛以及最终精度上限在哪里。Resize解决的是输入尺寸统一的问题。神经网络的全连接层、卷积层的输出尺寸是固定的模型在定义时输入 shape 就写死了。你喂一张 256x256 和一张 512x512 的图进去前向传播直接报错。所以必须把训练集、验证集、测试集的所有图片都统一到同一个尺寸。Normalize解决的是数据分布标准化的问题。深度学习训练依赖梯度下降如果输入数据的量纲差异太大——比如有的像素值在 0~255有的在 0~1有的特征均值是 50有的均值是 200——梯度更新就会像无头苍蝇一样乱撞。把数据分布拉到均值为 0、标准差为 1 的范围内优化过程会稳定很多。RandomCrop解决的是数据增强问题。深度学习最怕过拟合——模型把训练集背下来了测试集上就不会了。随机裁剪相当于给模型看同一张图的不同局部迫使模型学习更泛化的特征而不是死记某一块固定区域。这三个操作侧重点完全不同Resize管尺寸Normalize管分布RandomCrop管泛化。但实际使用中它们经常出现在同一个transforms.Compose里顺序组合还有讲究后面我会专门讲。1.2 为什么第一个要学的是 torchvision.transformsPyTorch 生态里处理图像最方便的工具就是torchvision.transforms它是 torchvision 这个官方视觉库的子模块。它把图像处理抽象成一系列变换每个变换是一个可调用对象通过transforms.Compose把它们串成流水线。这套设计有一个特别好的点每个 transform 都是可组合的、可复用的单元你可以像搭积木一样自由搭配。而且它原生支持 NumPy 数组、PIL Image 和 Tensor 之间的互相转换训练、验证、测试可以用同一套代码逻辑只是传入的 transform 列表不同。零基础入门阶段一个最大的误区是以为要把 opencv、PIL、numpy 的知识全学完才能开始做预处理。完全不需要。transforms把这层复杂性封装好了你只需要理解每个变换的参数含义和输入输出类型就行。这一篇我就按是什么、为什么、怎么用三层逻辑逐个拆解。2. Resize统一尺寸是入网的前提2.1 Resize 到底在做什么Resize字面意思就是缩放图像。它接收一个 PIL Image 或 Tensor按你指定的目标尺寸输出新的图像。官方参数是torchvision.transforms.Resize(size, interpolationInterpolationMode.BILINEAR)其中size有两种写法一个整数int比如Resize(256)短边缩放到 256长边按比例缩放保持宽高比不变。一个二元组(h, w)比如Resize((224, 224))直接拉伸到指定高度和宽度不保证比例。我第一次用的时候踩过一个坑:以为传一个整数表示直接把两张边都变成这个数实际上它保持宽高比。如果训练集的图全是 3:4 竖图用Resize(256)出来的是 192x256 而不是 256x256后续如果模型要求固定输入还得再补一步中心裁剪。2.2 插值方式为什么值得看interpolation参数默认是双线性插值BILINEAR这个参数控制缩放时像素点的重采样算法。实际使用中我遇到过的情况缩小时区域插值InterpolationMode.AREA比双线性更能保留细节不容易出现摩尔纹。放大时双三次插值InterpolationMode.BICUBIC理论上更平滑但训练场景下没必要双线性已经足够快且够用。目标检测、分割任务里缩放可能影响标注框和掩码的对齐这种情况不推荐直接用Resize要走专门的Resize变体或者自定义 transform。只要不是特殊任务默认双线性就行。但是你要明白这个参数的存在——当你换成更复杂的网络、或者在超分、检测这类对像素敏感的任务里它就可能成为影响精度的因素。2.3 什么时候 Resize 到多大这个问题没有唯一答案取决于你的模型设计。以 ImageNet 预训练模型为例ResNet 系列默认输入是 224x224ViT 通常是 224 或 384。如果你用自己的数据微调Resize((224, 224))是最不容易出错的起点。但有一点我反复跟新手强调Resize不是越大越好。分辨率高意味着计算量大、显存占用高训练速度直线下降。224 是精度和效率的经典平衡点先跑通再调大。另外要注意验证集和测试集也要用相同的Resize策略否则数据分布不一致评估结果没有意义。3. Normalize让模型看懂像素的分布3.1 为什么像素值不能直接用PIL 读进来的图像像素值范围是 0~255这是一个跨度很大的绝对数值。如果直接把这个值放进网络第一层卷积的权重更新会面临很大的梯度波动。Normalize做的事是把每个通道的像素值做标准化output (input - mean) / std这里mean和std是每个通道的均值与标准差。标准化后数据均值约 0、方差约 1分布更符合优化算法的偏好。我用一个生活化的类比解释假设你要比较两个城市的房价一个城市房价是 1 万到 10 万每平另一个是 3 万到 50 万每平如果不做任何归一化直接比较数字大的城市会完全主导分析结果。标准化之后两个城市的房价都映射到相对自己均值的偏差才有可比性。3.2 均值标准差到底填什么这是新手最常问的问题。常见的做法有两种第一种使用 ImageNet 数据集的统计值。这是 torchvision 官方预训练模型默认使用的mean[0.485, 0.456, 0.406] std[0.229, 0.224, 0.225]只要是加载 torchvision 预训练权重必须用这一组值。因为预训练模型是在经过这套标准化处理的数据上训练出来的你输入的数据分布必须和它训练时一致否则模型看到的图像跟它学过的完全不是一回事。第二种自己数据集的统计值。当你的数据分布和 ImageNet 差异很大时——比如医学影像、红外图片、深度图——可以自己统计import numpy as np from PIL import Image means [] stds [] for img_path in all_images: img np.array(Image.open(img_path)).astype(np.float32) / 255.0 means.append(img.mean(axis(0, 1))) stds.append(img.std(axis(0, 1))) mean np.mean(means, axis0) std np.mean(stds, axis0)统计之后把结果填进Normalize。我个人的建议是加载预训练模型就用 ImageNet 值从头训练一个小模型可以先试试 ImageNet 值。自己统计当然更贴合数据但在数据量不大的情况下自己统计出来的均值方差反而可能引入偏差。实测跑 CIFAR-10 这种彩色自然图像ImageNet 值就够用。3.3 Normalize 的输入顺序和通道要求用Normalize前必须搞清楚输入 Tensor 的格式。torchvision.transforms.ToTensor()会把 PIL Image 或 NumPy 数组转换成形状为(C, H, W)的浮点张量像素值从 0~255 缩放到 0~1然后Normalize才能正确按通道处理。如果跳过ToTensor直接对 PIL Image 做Normalize会直接报错——Normalize只接受 Tensor 输入。如果数据已经做了ToTensor但通道顺序不是 RGB 而是 BGR 或者灰度图那么 mean/std 的通道顺序也要相应调整这是个非常隐蔽的坑。灰度图尤其要注意它是单通道mean/std 也要用一个元素比如mean[0.5], std[0.5]填三组值会报 shape mismatch。4. RandomCrop用变着花样看同一张图对抗过拟合4.1 RandomCrop 的工作机制RandomCrop的本质是在图像上随机选取一个位置裁剪出指定大小的区域。每次调用都会得到不同的裁剪位置也就是说同一个 epoch 里同一张图每次进入模型的内容可能都不一样。参数方面torchvision.transforms.RandomCrop(size, paddingNone, pad_if_neededFalse, fill0)size裁剪后的尺寸可以是一个 int 或(h, w)。padding如果原图比裁剪尺寸小可以先用填充把图变大再裁剪。pad_if_needed如果原图不足以裁剪自动填充到足够大。fill填充的像素值默认 0。这个操作的目的很明显——数据增强。模型不再每次都看到同一张图的同一块区域而是看到不同位置的局部内容相当于被动扩大了训练集的多样性。这在数据量不够的时候尤其有效是提升泛化能力最便宜的方法之一。4.2 为什么通常和 Resize 搭配使用RandomCrop的一个问题是如果直接对原图做随机裁剪不同图片裁剪出来的内容尺度差异可能很大。有些图裁到的是近距离特写有些是远景这会让模型的学习目标变得混乱。所以常见做法是先用Resize把图放大到一个较大的尺寸比如Resize((256, 256))再RandomCrop((224, 224))。这样可以保证裁剪后图像的语义尺度基本一致同时保留了随机的空间位置变化数据增强效果比直接对原图裁更好。训练阶段用这个组合而验证/测试阶段一般不使用RandomCrop而是用CenterCrop或直接Resize到目标尺寸。原因是验证集需要确定性的输出随机裁剪会让每次评估的结果不同没法稳定对比模型好坏。4.3 一个约束条件图像不能小于裁剪尺寸RandomCrop((224, 224))的前提是输入图至少是 224x224否则它不知道该从哪里裁。所以必须确保流水线里Resize输出的尺寸比RandomCrop的目标尺寸大。实际写代码时我建议加上pad_if_neededTruetransforms.RandomCrop(224, pad_if_neededTrue)这样即使遇到个别异常小图也不会因为尺寸不够而崩掉而是先填充再裁剪。这个参数新手很容易忽略但数据集里偶尔冒出一张异常尺寸的图整个训练流程就会中断加上它即便不常用也是个保险。5. 组合顺序与完整运行示例5.1 组合顺序为什么关键transforms.Compose里的变换是按书写顺序依次执行的。顺序错了效果天差地别。下面这个例子是训练集最经典的一套组合transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])逻辑是这样Resize先把图统一放大到 256x256保证后续裁剪有足够空间。RandomCrop在 256x256 里随机裁出 224x224。RandomHorizontalFlip再随机水平翻转又增加一个维度的多样性。ToTensor把 PIL Image 转成(C, H, W)的 Tensor像素缩放到 0~1。Normalize按通道做标准化。如果顺序错了——比如先ToTensor再ResizeResize虽然支持 Tensor 输入但插值方式、边缘处理都不一样而且还可能因为尺寸兼容问题报错。更要注意的是Normalize一定要在ToTensor之后。ToTensor把像素从 0~255 压到 0~1Normalize再减均值除标准差这个顺序是配套的。你反过来先减均值再缩放均值就白减了。验证集和测试集不用RandomCrop用CenterCrop或直接Resize到最终尺寸transform_val transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])5.2 完整可运行的训练示例下面给一个完整示例包含数据集加载、预处理、一个简单的 CNN 训练循环直接复制到本地装好 PyTorch 和 torchvision就能跑。我用的是 CIFAR-10不需要额外下载图片数据。import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torch.utils.data import DataLoader # 定义数据预处理流水线 transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) transform_val transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 加载 CIFAR-10 数据集自动下载到 ./data train_dataset torchvision.datasets.CIFAR10( root./data, trainTrue, downloadTrue, transformtransform_train ) val_dataset torchvision.datasets.CIFAR10( root./data, trainFalse, downloadTrue, transformtransform_val ) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers2) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers2) # 定义一个极简 CNN方便跑通全流程 class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), ) self.classifier nn.Linear(128 * 28 * 28, 10) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) x self.classifier(x) return x device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 训练两轮 for epoch in range(2): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() # 验证 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch1} Loss: {running_loss/len(train_loader):.4f} Acc: {100 * correct / total:.2f}%)这段代码跑在 CPU 上也不会太慢CIFAR-10 的 5 万张训练图跑两轮大概几分钟。跑通之后你可以自己改Resize尺寸、改RandomCrop的 size、换一组 mean/std观察对训练的影响比看任何文档都直观。5.3 自己动手验证 Normalize 的效果如果你刚接触这一块建议做一个小实验随便取一张彩色图分别用不预处理和完整预处理两种方式喂给同一个模型观察 loss 的下降速度。你会发现完整预处理那条 loss 收敛明显更稳、更快。还可以直接看张量数值from PIL import Image img Image.open(test.jpg) tensor transforms.ToTensor()(img) print(tensor.min(), tensor.max(), tensor.mean()) normed transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])(tensor) print(normed.min(), normed.max(), normed.mean())第一次操作时你会看到 ToTensor 后数值在 0~1 之间Normalize 后出现负数均值接近 0。这就是标准化的直观效果。6. 常见问题与排查技巧实录我在帮人调代码时遇到最多的问题都集中在数据预处理上整理成速查表如下问题现象原因解决方案运行时报错 Tensor shape 不对Resize和RandomCrop混用导致尺寸不匹配确保Resize输出尺寸大于RandomCrop的裁剪尺寸Normalize报错expected shape (C, H, W)没有先执行ToTensor输入的还是 PIL Image 或 HWC 顺序检查流水线顺序Normalize必须在ToTensor之后加载预训练模型结果乱码一样用了自己的 mean/std 而不是 ImageNet 的统计值加载官方权重时使用官方默认 mean/std灰度图训练报错 channel mismatch灰度图是单通道但 mean/std 写了三组值用单通道的 mean/std或transforms.Grayscale(num_output_channels3)转成三通道训练 loss 下降慢没有 Normalize或者 Resize 过小确认数据分布已经标准化增大分辨率试试验证准确率忽高忽低不稳定验证集错误地用了 RandomCrop验证集用确定性变换Resize CenterCrop这里多说一个容易忽略的点训练和验证的预处理不一致是合理的而且必须不一致。训练时要加随机增强让模型见多识广验证时要确定性输出保证结果可复现。很多人把训练那套直接复制给验证最后评估结果飘忽不定还以为模型出了问题其实只是评估逻辑不对。6.1 一个容易被忽略的坑Resize 后 CenterCrop前面给的验证集 transform 里直接Resize((224, 224))。但如果你是先Resize(256)再CenterCrop(224)效果会更好——因为大部分自然图像的重要目标位于中心区域先保持比例放大到 256再取中心 224比直接暴力拉伸到 224 能保留更多真实细节。验证集推荐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)保持宽高比缩放CenterCrop(224)再取中心正方形。这是 ImageNet 时代最经典的一套验证预处理至今仍然好用。6.2 ToTensor 的位置和它的隐藏作用ToTensor看似简单但它在流水线里的位置直接影响后续所有变换。它的输入是 PIL Image 或 NumPy 数组输出是torch.FloatTensorshape 从(H, W, C)转成(C, H, W)像素值从 0~255 缩放到 0~1。我见过一些新手在Resize之前就先ToTensor然后想着用 tensor 去 resize。虽然 torchvision 的Resize现在支持 Tensor 输入但大多数数据增强操作比如RandomHorizontalFlip设计上更适配 PIL Image。所以最佳实践是所有空间变换放在前面ToTensor放在中间或靠后Normalize放最后。6.3 实测总结三个参数组怎么选以 CIFAR-10 或自定义图像分类为例我实际验证过几组配置配置训练结果表现评价只用 Resize ToTensor收敛缓慢精度偏低缺增强、未标准化不推荐Resize RandomCrop ToTensor收敛改善过拟合减少数据增强有效但分布偏差仍在Resize RandomCrop ToTensor Normalize收敛稳泛化好完整配置推荐作为默认Normalize 这一步在可视化上看不出来很多人觉得可有可无实际上它对训练稳定性的贡献非常实在。7. 其他常用 transform 的简要补充7.1 水平翻转、旋转、色彩抖动RandomHorizontalFlip()是最便宜最有效的数据增强方式之一对自然图像几乎无损我一直默认加在训练集里。RandomRotation和ColorJitter要根据任务决定——数字识别、医学图像这种对方向或颜色敏感的任务要谨慎使用而自然图像分类任务基本都可以放心加。transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.1)这个操作在数据量小、光照变化大的场景里效果很明显但参数别调太大否则会破坏图像语义。7.2 学习路径接下来该学什么这篇文章是 PyTorch 零基础系列的第四篇前几篇通常覆盖了张量基础、自动求导和模型搭建。跑通这个示例之后下一步我建议学这些transforms.Normalize的逆操作用于可视化或图像生成任务。DataLoader的collate_fn自定义应对变长输入。数据增强的进阶库比如torchvision.transforms.v2和新出的增强算子。学会在不同数据集上灵活组合这些 transform形成自己的标准化模板。我自己的习惯是把这套 transform 模板存在一个utils.py里每次开新项目直接改参数复用。你也可以这么做省去重复摸索的成本。8. 写在最后的实操心得这三年来我每带一个新同事跑模型都会让他们先把预处理这一块吃透。数据预处理不像模型结构那么显眼但它带来的影响是全局性的。同一个模型预处理做得合理训练又快又稳预处理做得随意哪怕模型结构再花哨效果也出不来。如果你现在正准备跑自己的第一个 PyTorch 图像项目我建议你按这篇文章的示例先原样跑通然后花一下午时间做一组对照实验改改Resize的尺寸换换RandomCrop的大小去掉Normalize看看 loss 曲线变成什么样。这个过程比看十篇教程都有用因为你会亲眼看到每个参数是怎么影响训练的。最后分享一个小技巧我调试预处理时经常加一段可视化代码把 transform 之后的图片用torchvision.utils.make_grid拼成网格存下来肉眼看一下增强效果。有时候你以为自己在做数据增强实际上因为参数不合理图片已经被裁得看不出原样了。可视化能帮你第一时间发现问题减少无效实验。
返回列表