预训练网络与迁移学习实战:AI-For-Beginners 计算机视觉课程 08 深度解析

原创2026-10-03 16:44:38860 阅读
文章标签:教程人工智能机器学习深度学习

预训练网络与迁移学习实战:AI-For-Beginners 计算机视觉课程 08 深度解析

导读

本篇文章围绕 AI-For-Beginners 课程第 08 课《预训练网络与迁移学习(Transfer Learning)》展开,系统讲解如何利用在 ImageNet 上训练好的 VGG、ResNet 等模型,在数据量有限的情况下快速构建猫狗识别等高精度图像分类器。你将学会"特征提取 + 轻量分类器""端到端冻结训练""微调(Fine-tuning)"三套标准流程的 PyTorch 与 TensorFlow 实现,并深入了解如何用梯度下降优化图像、制造"理想猫"与对抗样本,最终通过牛津宠物数据集(Oxford-IIIT Pets)完成迁移学习实战作业。

为什么需要迁移学习

从头训练一个卷积神经网络(CNN)需要消耗大量时间和数据,这是因为网络的绝大部分算力都花费在学习"低层滤波器"上——这些滤波器用来从图像中提取模式:从水平/垂直的线条、笔画等低层像素组合,逐步组合出对应"眼睛""火焰"之类的高层特征。

由此产生一个自然的疑问:能否直接使用一个在某个大型数据集上训练好的神经网络,加以适配后去分类完全不同的图像,而不必重走完整训练流程?

这个方案正是迁移学习(Transfer Learning):把知识从一个神经网络模型迁移到另一个模型。典型做法是以一个在大型图像数据集(如 ImageNet)上训练好的模型为起点。这些模型已经能很好地从通用图像中提取各种特征,在很多场景下,只需在这些特征之上构建一个分类器,就能得到不错的结果。

✅ 迁移学习也是教育学等学科中的术语,指把某个领域的知识应用到另一个领域的过程。这里则是把模型在 ImageNet 上学到的图像特征提取能力,迁移到自己的分类任务上。

预训练模型作为特征提取器

在前一课(CNN 架构速览)中介绍过的卷积网络,包含一系列负责从图像中提取特征的层:从低层像素组合(水平/垂直线、笔画),到对应于眼睛、火焰等概念的高层特征组合。当 CNN 在足够大、足够多样化的通用图像数据集上训练时,网络就会学会提取这些通用特征。

Keras 与 PyTorch 都内置了加载常见架构预训练权重的函数,绝大多数权重都是在 ImageNet 图像上训练得到的。最常用的架构如下:

模型 特点 适用场景
VGG-16 / VGG-19 结构相对简单,仍能给出不错精度 常作为第一次尝试迁移学习的首选,便于理解原理
ResNet 2015 年由微软研究院提出,层数更多,资源消耗更大 需要更高精度、算力充足时使用
MobileNet 体积显著缩小,适合移动设备 资源紧张、愿意牺牲少量精度时使用

下图是 VGG-16 网络从一张猫的图片中提取出的特征(每个通道按二维可视化后的效果):

VGG-16 提取的图像特征

从图中可以看到,VGG-16 的卷积特征图将原始图像转化为大量"模式响应",这些响应就是对后续分类最有用的中间表示。

实战数据集:Cats vs. Dogs

课程示例使用微软提供的 Cats and Dogs(Kaggle Dogs vs. Cats)数据集,它非常接近真实的图像分类场景:约 2.5 万张猫狗照片,每张尺寸不一、背景各异,且数据集中还存在少量损坏的图片文件需要清洗。

数据准备要点(PyTorch 版)

在 TransferLearningPyTorch.ipynb 中,下载数据并解压到 data 目录后,需要先校验并删除损坏图片。课程把这段校验逻辑封装在 pytorchcv.py 中:check_image() 用 PIL 打开并 verify() 图片,check_image_dir() 遍历目录并删除损坏文件(运行时会看到诸如 Corrupt image: data/PetImages/Cat/666.jpg 的输出)。

接着用 ImageNet 预训练 VGG 期望的输入分布来归一化数据,这一步至关重要:

std_normalize = transforms.Normalize(mean=[0.485, 0.456, 0.406],
                          std=[0.229, 0.224, 0.225])
trans = transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        std_normalize])
dataset = torchvision.datasets.ImageFolder('data/PetImages', transform=trans)
trainset, testset = torch.utils.data.random_split(dataset, [20000, len(dataset)-20000])

要点说明:

  • Resize(256) + CenterCrop(224):把图片统一裁剪到 VGG 要求的 224×224 输入尺寸;
  • Normalize(mean/std) 中的均值和标准差来自 ImageNet 数据集,是预训练权重能够正常工作的前提;
  • random_split([20000, ...]):划分出 2 万张训练图。

数据准备要点(TensorFlow 版)

在 TransferLearningTF.ipynb 中,清洗损坏图片的 check_image() 还会额外要求图片必须是 JPEG 格式。加载数据则使用 Keras 内置的 image_dataset_from_directory,它会从对应类别的子目录自动读取图片、完成缩放,并切分训练/验证集:

ds_train = keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split = 0.2,
    subset = 'training',
    seed = 13,
    image_size = (224,224),
    batch_size = batch_size
)
ds_test = keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split = 0.2,
    subset = 'validation',
    seed = 13,
    image_size = (224,224),
    batch_size = batch_size
)

注意:两次调用必须设置相同的 seed = 13,因为该 seed 决定了训练/验证的切分方式。返回的 ds_train 是生成器式数据集(每个 batch 的 shape 为 (64, 224, 224, 3)),可直接传给 model.fit,类别名通过 ds_train.class_names 获取(本例为 <a href="https://link.gitcode.com/i/506c64dabf73d474c852e65b9992d9e7" target="_blank">'Cat', 'Dog'])。数据集加载逻辑封装在 [tfcv.py 的 load_cats_dogs_dataset() 中。

迁移学习实战:三种标准方案

方案一:特征提取 + 轻量分类器(Feature Extractor + Classifier)

核心思路:截掉预训练模型最后的分类层,把它当作"特征提取器";先为所有训练图片一次性计算特征向量,再用一个很小的网络(甚至单层线性层)在特征之上做分类。

PyTorch 实现:

vgg = torchvision.models.vgg16(pretrained=True)
res = vgg.features(sample_image)          # 输出特征 shape = (1, 512, 7, 7)

特征张量维度是 512×7×7,展平后是 25088 维。然后批量预计算特征向量并构建一个单层分类器:

bs = 8
feature_tensor = torch.zeros(num, 512*7*7).to(device)
label_tensor  = torch.zeros(num).to(device)
for x, l in dl:
    with torch.no_grad():
        f = vgg.features(x.to(device))
        feature_tensor[i:i+bs] = f.view(bs, -1)
        ...

net = torch.nn.Sequential(torch.nn.Linear(512*7*7, 2), torch.nn.LogSoftmax()).to(device)
history = train(net, train_loader, test_loader)

使用课程自带的 pytorchcv.py 训练工具(默认优化器为 torch.optim.Adam),在 800 张图片子集上 10 个 epoch 内即可达到接近 98% 的验证精度。正如 notebook 中所注:因为手工预计算特征比较耗时,示例只在小规模子集上演示,完整数据上完全可以重复。

TensorFlow 实现:先用 VGG16(include_top=False) 实例化去掉分类头的 VGG-16,得到 (7, 7, 512) 的特征输出,再用 Dataset API 的 map 把原始图片映射为特征:

vgg = keras.applications.VGG16(include_top=False)
ds_features_train = ds_train.take(50).map(lambda x, y : (vgg(x), y))
model = keras.models.Sequential([
    keras.layers.Flatten(input_shape=(7,7,512)),
    keras.layers.Dense(1, activation='sigmoid')
])
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])

二分类任务使用 sigmoid 激活 + binary_crossentropy 损失,子集上验证精度约 95%。

方案二:端到端冻结训练(Freeze & Train)

不预先计算特征,而是把预训练网络整体作为网络的第一层参与训练,同时冻结卷积特征提取器的权重,只训练新加入的分类层。

PyTorch 实现(TransferLearningPyTorch.ipynb):

vgg.classifier = torch.nn.Linear(25088, 2).to(device)   # 替换最后一层分类器

for x in vgg.features.parameters():                      # 冻结卷积层
    x.requires_grad = False

summary(vgg, (1, 3, 244, 244))

打印模型摘要可以看到:总参数量约 1500 万,但可训练参数只有 5 万左右——只有新加的线性分类层(Linear(25088, 2),50178 个参数)在训练。用小样本量微调少量参数是迁移学习能防止过拟合的关键。训练时建议启用 GPU:

device = 'cuda' if torch.cuda.is_available() else 'cpu'
train_long(vgg, train_loader, test_loader,
           loss_fn=torch.nn.CrossEntropyLoss(), epochs=1, print_freq=90)

TensorFlow 实现:Keras 的巧妙之处在于 VGG-16 模型本身可以作为另一个网络的一层。构建顺序模型并在第一层冻结卷积基:

model = keras.models.Sequential()
model.add(keras.applications.VGG16(include_top=False, input_shape=(224,224,3)))
model.add(keras.layers.Flatten())
model.add(keras.layers.Dense(1, activation='sigmoid'))

model.layers[0].trainable = False   # 冻结特征提取器
model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['acc'])

model.summary() 显示:总参数约 1474 万,可训练参数仅 25089。冻结之所以必要,是因为未训练的分类层误差过大时,会反向传播破坏卷积层原有的预训练权重。训练完成后可用 model.save('data/cats_dogs.tf') 与 keras.models.load_model(...) 保存/恢复模型。

方案三:微调(Fine-tuning)

前两种方案都依赖 ImageNet 上学到的特征。如果目标图像与 ImageNet 分布差异较大,这套特征组合可能不是最优,此时需要解冻卷积层继续训练——这就是微调。

微调的标准节奏是:先冻结特征提取器训练几个 epoch,让分类层权重稳定后,再解冻卷积层用小学习率继续训练。若一开始就端到端训练未冻结参数,分类层的大误差极可能毁掉预训练权重。

PyTorch 版:

for x in vgg.features.parameters():
    x.requires_grad = True

train_long(vgg, train_loader, test_loader,
           loss_fn=torch.nn.CrossEntropyLoss(), epochs=1, print_freq=90, lr=0.0001)

解冻后建议使用更低的学习率(如 0.0001)以尽量减小对预训练权重的冲击,并预期训练早期精度会有回落,随后超过冻结时的水平。同时反向传播要经过更深层网络,训练速度明显变慢。

TensorFlow 版:先 model.layers[0].trainable = True 解冻,但不推荐一次性全部解冻,而是先只解冻最后几个卷积层(它们编码的是与目标图像更相关的高层模式):

for i in range(len(model.layers[0].layers)-4):
    model.layers[0].layers[i].trainable = False
model.summary()   # 可训练参数约 710 万,占总参数量约一半

解冻后继续 model.fit,训练精度通常会更高(验证精度提升相对有限),可尝试解冻更多层来换取更高精度。

加载预训练模型时要注意什么

无论框架如何,加载预训练模型都有两条经验法则(来自两个 notebook 的原始注释):

  1. 输入必须经过对应的预处理函数。TensorFlow 中调用 keras.applications.vgg16.preprocess_input(归一化并减去各通道预定义均值);PyTorch 中则是上文 transforms.Normalize(mean=[0.485, 0.456, 0.406], ...)。因为 VGG-16 原本就是在这种预处理下训练的。
  2. 预测结果是 ImageNet 类别索引。用 argmax 取最大概率类别编号后,TensorFlow 可用 decode_predictions 直接返回 Top-N 类别名称(例如 Labrador_retriever);PyTorch 需要自己加载 class 映射表来解析。

让网络"画出"理想猫与对抗样本

预训练网络内部存储着各种模式,包括"理想猫"(以及理想狗、理想斑马等)的概念。能否把它"脑中的图像"可视化出来?由于模式分散在层级组织的网络权重中,这并不简单。

课程给出的思路(详见 AdversarialCat_TF.ipynb)是:从一张随机图像出发,用梯度下降优化不断调整图像,直到网络认为它是猫:

图像优化循环示意图

但直接这样做的结果是得到一堆类似随机噪声的图像——因为"让网络把输入当成猫"的方式有无数种,包括很多视觉上毫无意义的模式。它们包含大量猫的典型特征,却没有约束让它们在视觉上可分辨。

解决办法是在损失函数中加入**变分损失(variation loss)**项:它度量图像相邻像素的相似程度,最小化变分损失会让图像更平滑、去除噪声,从而显现出更有视觉美感的模式。课程中展示的"理想猫"与"理想斑马"就是被高概率分类为该类别的优化结果(原文档以并排表格形式给出了这两张图:ideal-cat.203dd4597643d6b0.webp 与 ideal-zebra.7f70e8b54ee15a7a.webp,对应仓库路径为 理想猫 和 理想斑马)。

同样的思路还能用于对神经网络实施对抗攻击(adversarial attack):假设想骗过网络、让一只狗"看起来像猫"。取一张被网络正确识别为狗的照片(原图),用梯度下降对图像做轻微调整,直到网络把它分类为猫——最终得到的对抗图像(被分类为猫的狗)与原始照片仅存在肉眼几乎不可察觉的差异。这类实验既直观展示了"模型在输入空间中的决策边界",也提示了对抗样本在安全场景中的现实风险。

进阶阅读:深度学习训练技巧

课程推荐深入阅读 TrainingTricks.md,其中覆盖了让深层网络训练更稳定的常用技术:

  • 数值范围控制:浮点运算对数量级差异敏感(如 10⁻¹⁰ 与 10¹⁰ 相加会丢失小数的尾数),应尽量让所有值保持在 [-1..1] 或 [0..1];大多数激活函数的非线性区也集中在这个范围。
  • 权重初始化:N(0,1) 会导致输出标准差随输入数量放大,常用替代包括均匀分布 uniform、N(0,1/n)(gaussian)、N(0,1/√n_in),以及保持前向/后向信号范围的 Xavier 初始化(glorot,即 N(0, √(2/(n_in+n_out))))。
  • 批归一化(Batch Normalization):在权重之后、激活之前按当前 minibatch 的均值/方差归一化,通常能提高最终精度并加速训练——ResNet 等现代架构(本课 PyTorch notebook 打印的 resnet18 结构、TF 版 resnet50 结构中都大量包含 BatchNorm2d/BatchNormalization 层)正是依赖它提升深层网络稳定性。
  • Dropout:训练时随机丢弃 10%~50% 的神经元,可被理解为随机扰动跳出局部最优,或"隐式模型平均"。可在 Dropout.ipynb 中观察其对 MNIST 分类训练速度与精度的实际影响。
  • 防止过拟合:早停(验证误差开始上升即停止)、显式权重衰减/正则化、模型平均,以及 Dropout;模型参数量必须与训练样本量匹配(可回顾 过拟合概念)。
  • 优化器选择:批量随机梯度下降(SGD)、带动量的 SGD(γ 控制惯性程度)、Adagrad、RMSProp,以及被认为是很多应用高效默认选择的 Adam;配合梯度裁剪(阈值 θ 常取 1 或 10)和学习率衰减(如每 epoch 乘以 0.98)使用。
  • 架构选择:优先选用在类似任务上已被验证有效的架构;ResNet 与 Inception 在一定程度上能自适应任务复杂度。

挑战与作业:牛津宠物数据集实战

挑战:配套 notebook 末尾的注释指出,迁移知识在训练数据与目标任务较为相似时效果最好(例如识别一种新的动物类别)。请尝试用完全不同类型的图像(如植物叶片、工业产线细节等)做实验,观察迁移模型的表现如何变化。

作业(lab/README.md):设想为宠物保育园开发一款应用,通过照片自动识别宠物品种。作业使用真实世界的 Oxford-IIIT Pets 数据集(含 35 种猫狗品种),构建一个迁移学习分类器。下载数据:

!wget https://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
!tar xfz images.tar.gz

在 OxfordPets.ipynb 中开始你的实现。作业的要点是:预训练网络能让我们相对轻松地解决真实世界图像分类问题,但如果对象与 ImageNet 图像差异很大(例如医学影像),效果往往会明显下降。

小结

通过迁移学习,我们能够在自定义目标分类任务上快速搭建分类器并获得高精度:相比从零训练,只需训练几万个参数(而非上亿个),对数据量、算力的要求都大幅降低。同时,本节实验也说明:当前任务复杂度越高,对算力的需求越大,纯 CPU 环境难以高效支撑。下一单元课程将尝试用更轻量的实现,以更低的算力训练同样的模型,仅牺牲少量精度。

登录后查看全文
AI-For-Beginners