☰
fastbook 第七章实战:用 fastai 从零训练 SOTA 图像分类模型——归一化、渐进式调整大小、TTA、Mixup 与标签平滑
2026/10/1 2:08:14 网站建设 项目流程
  • 教程
  • 深度学习
  • 机器学习

【免费下载链接】fastbook

The fastai book, published as Jupyter Notebooks

项目地址:https://gitcode.com/gh_mirrors/fa/fastbook
点击查看免费下载

本篇技术指南以 fastbook 仓库第七章 07_sizing_and_tta.ipynb(中文译稿见 translations/cn/07_sizing_and_tta.md)为骨架,系统讲解在不使用迁移学习的前提下,借助 Imagenette 数据集将图像分类模型从 82% 提升至 87%+ 准确率的五项关键技术:数据归一化(Normalization)、渐进式调整大小(Progressive Resizing)、测试时间增强(TTA)、Mixup 数据混合与标签平滑(Label Smoothing)。读完本文,你将掌握这些技巧在 fastai 中的完整可运行代码、各方法的适用场景与调参要点,并理解其背后的训练动力学原理。

为什么需要一个"小 ImageNet":Imagenette

在 fast.ai 起步阶段,社区主要用三大数据集构建与测试视觉模型:ImageNet(约 130 万张、约 500 像素宽、1000 类、需数天训练)、MNIST(5 万张 28×28 灰度手写数字)、CIFAR10(6 万张 32×32 彩色图像、10 类)。问题在于,小数据集无法有效泛化到大 ImageNet 场景——在 ImageNet 上有效的方法往往必须在 ImageNet 上开发和训练,这曾让许多人误以为只有拥有巨型算力的研究者才能参与图像分类算法的研发。

fast.ai 团队的做法是:自建一个能在几分钟内完成训练、又能提供对完整 ImageNet 有价值的见解的数据集。于是他们从完整 ImageNet 中挑选了 10 个差异很大的类别,用了大约三个小时构建出Imagenette——训练快、成本低,且在此数据集上验证有效的算法调整在 ImageNet 上同样有效。

核心方法论:你拿到的数据集未必是你想要的。开发与原型设计时应追求"几分钟内跑完一个实验"的迭代速度;若实验过慢,应设法缩小数据集或简化模型。实验做得越多,效果越好。

获取 Imagenette 并构建基线模型:

from fastai.vision.all import * path = untar_data(URLs.IMAGENETTE) dblock = DataBlock(blocks=(ImageBlock(), CategoryBlock()), get_items=get_image_files, get_y=parent_label, item_tfms=Resize(460), batch_tfms=aug_transforms(size=224, min_scale=0.75)) dls = dblock.dataloaders(path, bs=64)

这里沿用了第五章的"预调整"(presizing)技巧:先用Resize(460)做 item 级处理,再在 batch 级用aug_transforms完成随机裁剪与缩放(min_scale=0.75表示最小缩放比例为 75%)。随后做一次不加载预训练权重的基线训练:

model = xresnet50(n_out=dls.c) learn = Learner(dls, model, loss_func=CrossEntropyLossFlat(), metrics=accuracy) learn.fit_one_cycle(5, 3e-3)
epochtrain_lossvalid_lossaccuracytime
01.5834032.0643170.40179201:03
11.2088771.2601060.60156801:02
20.9252651.0361540.66430201:03
30.7301900.7009060.77781901:03
40.5857070.5418100.82524301:03

5 个 epoch 后验证准确率约82.5%。这是一个不错的起点——没有使用任何预训练模型。接下来要做的,就是让这个数字继续上涨。

归一化:让输入分布匹配模型的"预期"

模型训练时,若输入数据是归一化的(均值为 0、标准差为 1)会有帮助。但大多数图像与视觉库使用 0~255 或 0~1 的像素值,都不满足该条件。先看看实际批次数据长什么样(对除通道轴外的所有轴求均值,通道轴是轴 1):

x,y = dls.one_batch() x.mean(dim=[0,2,3]),x.std(dim=[0,2,3])

输出:

(TensorImage([0.4842, 0.4711, 0.4511], device='cuda:5'), TensorImage([0.2873, 0.2893, 0.3110], device='cuda:5'))

均值明显偏离 0、标准差也远非 1。在 fastai 中只需添加Normalize转换即可:它一次性作用于整个小批量,因此应放入数据块的batch_tfms。你需要传入目标均值与标准差,fastai 已内置 ImageNet 标准统计量(imagenet_stats);若不给Normalize传任何统计量,fastai 会自动从数据的一个批次中计算出来。

把get_dls封装成接受bs与size的函数,便于后续渐进式调整大小复用:

def get_dls(bs, size): dblock = DataBlock(blocks=(ImageBlock, CategoryBlock), get_items=get_image_files, get_y=parent_label, item_tfms=Resize(460), batch_tfms=[*aug_transforms(size=size, min_scale=0.75), Normalize.from_stats(*imagenet_stats)]) return dblock.dataloaders(path, bs=bs) dls = get_dls(64, 224) x,y = dls.one_batch() x.mean(dim=[0,2,3]),x.std(dim=[0,2,3])

输出:

(TensorImage([-0.0787, 0.0525, 0.2136], device='cuda:5'), TensorImage([1.2330, 1.2112, 1.3031], device='cuda:5'))

均值已接近 0。重新训练同一模型:

model = xresnet50(n_out=dls.c) learn = Learner(dls, model, loss_func=CrossEntropyLossFlat(), metrics=accuracy) learn.fit_one_cycle(5, 3e-3)
epochtrain_lossvalid_lossaccuracytime
01.6328652.2500240.39133701:02
11.2940411.5799320.51717701:02
20.9605351.0691640.65720701:04
30.7302200.7674330.77184501:05
40.5778890.5506730.82449601:06

归一化在从头训练时收益有限(准确率仍约 82.4%),但在使用预训练模型时至关重要:预训练模型只认识它见过的数据类型——若预训练数据平均像素值为 0,而你的数据像素最小值为 0,模型看到的将完全不是它预期的输入。

由此引出一条工程准则:

  • 分发模型时必须同时分发归一化统计量,任何用它做推断或迁移学习的人都要使用相同统计量;
  • 同理,使用他人训练的模型时,务必弄清并匹配其归一化统计量。

此前章节之所以无需手动处理,是因为通过vision_learner(原书cnn_learner)使用预训练模型时,fastai 会自动加入合适的Normalize转换——模型已用特定统计量(通常来自 ImageNet)预训练,库可以自动填充。这一机制仅适用于预训练模型,这正是从头训练时需要手动添加的原因。

渐进式调整大小:小图起步,大图收尾

2018 年 fast.ai 团队赢得 DAWNBench 比赛时,最重要的创新之一非常朴素:用较小的图像开始训练,用较大的图像结束训练。大部分 epoch 用小数图像训练能让训练显著加快,最后用大图收尾则能显著提升最终准确率——这就是渐进式调整大小(Progressive Resizing)。

术语:渐进式调整大小—— 在训练过程中逐渐使用越来越大的图像。

其可行性来自卷积网络的一个特性:CNN 学到的特征类型与图像尺寸无关——浅层找边缘、梯度,深层找鼻子、日落等。因此训练中途改图像大小,不必为模型寻找完全不同的参数。但小图与大图之间终究存在差异,不应指望模型毫无变化地继续完美工作——这听起来很像迁移学习!是的,调整图像大小后应使用fine_tune方法。渐进式调整大小还有一个额外收益:它本身是另一种数据增强形式,因此可以期待更好的泛化能力。

利用上文的get_dls函数,先用小尺寸训练较少的 epoch:

dls = get_dls(128, 128) learn = Learner(dls, xresnet50(n_out=dls.c), loss_func=CrossEntropyLossFlat(), metrics=accuracy) learn.fit_one_cycle(4, 3e-3)
epochtrain_lossvalid_lossaccuracytime
01.9029432.4470060.40141900:30
11.3152031.5729920.52576500:30
21.0011990.7678860.75914900:30
30.7658640.6655620.79798400:30

注意每个 epoch 只耗时 30 秒。然后直接在Learner内部替换DataLoaders并微调:

learn.dls = get_dls(64, 224) learn.fine_tune(5, 1e-3)
epochtrain_lossvalid_lossaccuracytime
00.9852131.6540630.56572101:06
epochtrain_lossvalid_lossaccuracytime
00.7068690.6896220.78454101:07
10.7392170.9285410.71247201:07
20.6294620.7889060.76400301:07
30.4919120.5026220.83644501:06
40.4148800.4313320.86333101:06

最终验证准确率达到86.3%,明显超过直接 224 尺寸训练的基线,且小图阶段每 epoch 更快。这一"增大尺寸→继续训练"的过程可按需重复,直到期望的图像大小——当然,使用比磁盘上原图更大的尺寸不会带来任何额外收益。

需要警惕的适用边界:对于迁移学习,渐进式调整大小可能反而损害性能。如果预训练模型与你的任务、数据集高度相似,且训练图像尺寸相近,那么权重几乎无需改动,此时用较小图像训练可能损伤预训练权重;反之,若迁移任务使用的图像在尺寸、形状或风格上与预训练差异较大,渐进式调整大小大概率有帮助。结论依然是那句"试试看!"。

测试时间增强(TTA):验证阶段也做增强

此前我们只用随机裁剪做训练增强,验证集始终看到同一张图。fastai 在训练用随机裁剪时,验证集自动使用中心裁剪(选择图像中心、不超过边缘的最大正方形区域)。这在多标签场景中常出问题:图像边缘的小物体可能被整体裁掉;即使在宠物品种分类中,鼻子的颜色等关键特征也可能被裁掉。

两个候选方案:

  1. 完全放弃随机裁剪,直接压缩或拉伸矩形图像填充正方形——但这会丢掉一项非常有效的数据增强,且迫使模型学会识别被压扁/拉长的图像,识别更难;
  2. 验证时不做单一中心裁剪,而是从原始矩形图像中选取多个区域分别过模型,再对预测取最大值或平均值。事实上不仅可以对不同裁剪做,还可以对所有测试时增强参数的不同取值做。这就是测试时间增强(TTA)。

术语:测试时间增强(TTA)—— 在推断或验证期间,使用数据增强创建每个图像的多个版本,然后取每个增强版本预测的平均值或最大值。

TTA 视数据集不同可显著提升准确率;它不改变训练时间,但验证/推断时间会按请求的增强图像数量成倍增加。fastai 默认使用未增强的中心裁剪图 + 四张随机增强图像。任何DataLoader都可以传给tta方法,默认使用验证集:

preds,targs = learn.tta() accuracy(preds, targs).item()
0.8737863898277283

在渐进式调整大小得到的 86.3% 基础上,TTA 把准确率推到87.4%,无需额外训练。代价是推断变慢:若平均五张图像,推断约慢 5 倍。

Mixup:对图像与标签同时做线性组合

Mixup 由张宏毅等人在 2017 年论文《mixup: Beyond Empirical Risk Minimization》中提出,是一种非常强大的数据增强技术,能带来显著更高的准确率,尤其适合数据不多、又没有在相似数据上预训练过的模型的场景。论文指出:"虽然数据增强总能带来泛化改进,但该过程依赖数据集,因此需要专业知识的运用。"例如翻转图像很常见,但该水平翻转还是垂直翻转?答案取决于数据集;而且翻转这种增强无法"多翻"。Mixup 的价值在于:它可以像旋钮一样调节变化程度。

对每张图像,Mixup 的工作流程:

  1. 随机从数据集中选择另一张图像;
  2. 随机选择一个权重;
  3. 用该权重对所选图像与当前图像做加权平均——这是自变量;
  4. 用同一权重对两幅图像的标签做加权平均——这是因变量。

伪代码如下(t是加权平均的权重):

image2,target2 = dataset[randint(0,len(dataset)] t = random_float(0.5,1.0) new_image = t * image1 + (1-t) * image2 new_target = t * target1 + (1-t) * target2

要让这种加权标签成立,目标需要独热编码。下图是论文中的公式摘录(论文中的 λ 即伪代码中的t):

图 7-1. Mixup 论文摘录

下图展示了 Mixup 中图像线性组合的样子:

图 7-2. 混合教堂与加油站

第三张图由 0.3×第一张 + 0.7×第二张构成。模型该预测"教堂"还是"加油站"?正确答案是30% 教堂、70% 加油站。例如 10 个类别中"教堂"用索引 2、"加油站"用索引 7 表示,独热编码分别为:

[0, 0, 1, 0, 0, 0, 0, 0, 0, 0] and [0, 0, 0, 0, 0, 0, 0, 1, 0, 0]

最终目标:

[0, 0, 0.3, 0, 0, 0, 0, 0.7, 0, 0]

在 fastai 中,这一切由一个callback完成。Callback 是 fastai 在训练循环中注入自定义行为的机制(如学习率调度、混合精度训练);第十六章会详细讲解如何自建 callback,眼下只需知道用Learner的cbs参数传入:

model = xresnet50(n_out=dls.c) learn = Learner(dls, model, loss_func=CrossEntropyLossFlat(), metrics=accuracy, cbs=MixUp()) learn.fit_one_cycle(5, 3e-3)

用"混合"数据训练会发生什么?显然训练更难——图像内容难以辨认,模型必须同时预测两个标签并推断各自的权重。但过拟合似乎不再是个问题:每个 epoch 看到的不是同一张图像,而是两张图像的随机组合。

要点与权衡:

  • Mixup 需要更多 epoch才能达到更好的准确率。原书提示可参考 fastai 仓库的examples/train_imagenette.py脚本做对照实验;截至撰写时,Imagenette 排行榜上超过 80 epoch 的领先结果均使用 Mixup,更少 epoch 时则不用——与本章经验一致。
  • Mixup 不止适用于照片:有人通过在模型内部激活上使用 Mixup 也取得了良好结果,这让 Mixup 可用于 NLP 等其他数据类型。
  • Mixup 顺带解决了"损失永远无法为 0"的问题:此前模型标签是 1 和 0,但 softmax/sigmoid 输出永远无法等于 1 或 0,导致训练把激活值越推越极端、epoch 越多越极端;使用 Mixup 后,标签只有恰好与同类图像混合时才为 1 或 0,其余时间都是线性组合(如 0.7 与 0.3),模型不再被推向极端。

一个潜在局限:Mixup 是"意外地"把标签改到大于 0、小于 1——我们并未显式声明要这样修改标签。若想把标签调得离 0/1 更近或更远,只能改变 Mixup 的量,而这会同时改变数据增强的量,可能并非所愿。更直接的处理方式是标签平滑。

标签平滑:让模型别那么自信

在分类问题损失的理论表达中,目标(target)是独热编码的(实践中为省内存通常不真的做独热编码,但计算出的损失等价)。这意味着模型被训练成:除一个类别返回 1 外其余全部返回 0。即使是 0.999 也不够好——模型仍会获得梯度、学到更高置信度的激活。这会鼓励过拟合,并在推理时给出没有意义概率的模型:即便不太确定,它也会对预测类别输出 1,只因它被如此训练。

如果数据标注不完全干净,这会非常有害。本书第二章的熊分类器中就存在错误标注、或一张图含两种熊的情况;人工标注同样可能出错或在难以标注的图像上意见不一。标签平滑的做法是:把所有 1 换成略小于 1 的数、所有 0 换成略大于 0 的数再训练。通过鼓励模型降低自信度,标签平滑让训练对错误标注更健壮,最终得到泛化更好的模型。

具体实现:从独热编码标签出发,用 ε/N 替换所有 0(ε 是希腊字母epsilon,源自提出标签平滑的论文及 fastai 代码),其中 N 是类别数,ε 通常取 0.1(意味着对标签有 10% 的不确定)。由于希望标签总和为 1,把原来的 1 替换为 1−ε+ε/N。于是 Imagenette(10 类)中对应于索引 3 的目标变为:

[0.01, 0.01, 0.01, 0.91, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]

实践中无需真的独热编码标签(独热编码只是用于解释与可视化)。使用它只需在调用Learner时更换损失函数:

model = xresnet50(n_out=dls.c) learn = Learner(dls, model, loss_func=LabelSmoothingCrossEntropy(), metrics=accuracy) learn.fit_one_cycle(5, 3e-3)

与 Mixup 类似,通常训练更多 epoch 后才能看到标签平滑带来的显著改进——不妨自己实验:需要训练多少个 epoch 才能观察到标签平滑的改善?

标签平滑的原理(论文视角)

提出标签平滑的论文(Szegedy 等人)这样解释其动机:让与真实标签对应的 logit 远大于其余 logit 可逼近最大值 1,但会引发两个问题:其一,模型对每个训练样本都学到把全部分配给真实标签,泛化没有保证;其二,它鼓励最大 logit 与其余 logit 的差距拉大,叠加有界的梯度 ∂ℓ/∂z_k,会削弱模型的适应能力——直觉上,这是因为模型对自身预测变得过于自信。交叉熵的梯度本质是output - target,两者都在 0~1 之间,差值有界(不超过 ±1),因此 SGD 每步更新也受限,在迁移学习场景中更难被更新。

结论与实战路线图

至此,你已经掌握从头训练或迁移学习场景下训练 SOTA 计算机视觉模型所需的全部要素。本章给出的完整方案在 Imagenette 上的提升路径清晰可见:

  1. 基线(224 尺寸、无归一化):82.5%;
  2. 加入归一化:82.4%(从头训练收益有限,但迁移学习必备);
  3. 渐进式调整大小(128 → 224):86.3%,且小图阶段每 epoch 更快;
  4. 测试时间增强(中心裁剪 + 4 张增强):87.4%;
  5. 若希望进一步冲榜:Mixup 与标签平滑配合更长的训练周期,可有效避免过拟合并给出更好的结果。

在自己问题上动手时,请记住:

  • 用cbs=MixUp()与loss_func=LabelSmoothingCrossEntropy()尝试更长训练;
  • 试试渐进式调整大小与测试时间增强;
  • 如果数据集很大,不要在全集上做原型——找出一个代表整体的小子集(就像 Imagenette 之于 ImageNet)先做实验。

本文对应的可运行代码、完整实验输出与课后练习位于 07_sizing_and_tta.ipynb,清理版见 clean/07_sizing_and_tta.ipynb,中文译稿见 translations/cn/07_sizing_and_tta.md。仓库运行环境可参考根目录的 environment.yml 与 requirements.txt。

延伸思考(原书问卷精选):

  • ImageNet 与 Imagenette 有何区别?何时该在哪个上做实验?
  • 为什么用预训练模型时无需操心归一化?
  • 在自己的项目中实现渐进式调整大小,它是否有帮助?
  • 推理阶段用 TTA 比常规推理更慢还是更快?为什么?
  • 为什么 Mixup 能防止模型过于自信?为什么 5 个 epoch 的 Mixup 训练反而更差?
  • 标签平滑能解决数据中的哪些问题?用 5 个类别做标签平滑时,与索引 1 关联的目标是什么?
  • 想在新数据集上快速原型实验,第一步应该做什么?
  • 教程
  • 深度学习
  • 机器学习

【免费下载链接】fastbook

The fastai book, published as Jupyter Notebooks

项目地址:https://gitcode.com/gh_mirrors/fa/fastbook
点击查看免费下载

相关推荐

上一篇:基于 Prisma 的 GraphQL 服务器托管指南:Now、Apex Up 与 Serverless 部署方案解析
下一篇:Antmicro Jetson Nano Baseboard:开源硬件如何重塑边缘AI开发边界

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询