AI-For-Beginners 第 8 课深度解析:预训练网络与迁移学习(Transfer Learning)

原创2026-10-08 21:27:531,458 阅读
文章标签:教程人工智能机器学习深度学习计算机视觉NLP

AI-For-Beginners 第 8 课深度解析:预训练网络与迁移学习(Transfer Learning)

本篇文章基于 AI-For-Beginners 课程体系中 4-ComputerVision/08-TransferLearning 一课的核心内容展开。你将理解"迁移学习"的本质——把在大规模数据集(如 ImageNet)上训练好的 CNN 特征提取能力"搬运"到自己的分类任务上,并掌握 Keras(TensorFlow)与 PyTorch 两条技术路线下的完整实战流程:从加载预训练权重、提取特征、冻结骨干网络训练分类头,到微调(Fine-tuning)、可视化"理想图像"与对抗样本。读完本文,你可以在几乎不训练卷积层的情况下,快速搭建高精度的猫狗分类器等自定义图像分类模型。

为什么需要迁移学习?

从头训练一个 CNN 需要大量时间和海量数据。但深入思考会发现:训练过程中,网络绝大部分计算量都花在**学习低层滤波器(filters)**上——这些滤波器负责从图像中提取水平线、垂直线、笔画等基础纹理模式。CNN 是一个层次化特征提取器:前面的卷积层提取像素组合级别的低层特征,后面的层则将这些组合成"眼睛""火焰"之类的高层特征。

由此引出一个自然的问题:能否复用一份已经在某数据集上训练好的网络,在不进行完整训练的前提下,去分类完全不同的图像?

答案就是本课的主角——迁移学习(Transfer Learning)。它把知识从一个神经网络模型"迁移"到另一个模型:通常我们先取一个在 ImageNet 这样的大型通用图像数据集上预训练好的模型,这些模型已经能够很好地从通用图片中提取各类特征,此时我们往往只需要在提取出的特征之上再搭建一个简单的分类器,就能获得不错的结果。

拓展阅读提示:本课文档中提到,此前一课的 CNN 架构总览 对 VGG、ResNet、MobileNet 等经典 CNN 架构有更系统的讲解,建议先回顾该页再继续本文。

预训练模型:充当"特征提取器"的现成零件

Keras 和 PyTorch 都内置了加载常见 CNN 架构预训练权重的函数,绝大多数这类权重都是在 ImageNet 上训练得到的。课程文档特别推荐了三个常用选择:

架构 特点 适用场景
VGG-16 / VGG-19 结构相对简单,仍能给出不错精度 第一次尝试迁移学习时首选,便于理解流程
ResNet 微软研究院 2015 年提出,层数更多、更"深" 追求更高精度且算力充足时
MobileNet 模型体积大幅压缩 移动端、资源受限、可牺牲少量精度

例如,用 VGG-16 对一张猫的图片进行特征提取,得到的正是下图这种逐层激活的特征图——低层响应边缘纹理,高层响应更抽象的结构语义:

VGG-16 提取的猫图像特征

TensorFlow/Keras:一行代码加载 VGG-16

在课程的 TransferLearningTF.ipynb 中,加载并直接使用完整 VGG-16 非常简单:

import tensorflow as tf
from tensorflow import keras

vgg = keras.applications.VGG16(weights=_weights_path)  # 或直接 keras.applications.VGG16()
inp = keras.applications.vgg16.preprocess_input(x_sample[:1])
res = vgg(inp)
print(f"Most probable class = {tf.argmax(res,1)}")
keras.applications.vgg16.decode_predictions(res.numpy())

这里有几个关键细节,值得展开:

  • 输入必须预处理:调用预训练网络前,必须使用对应的 preprocess_input 函数。以 VGG-16 为例,它会完成图像归一化,并对每个通道减去预定义的平均值——因为 VGG-16 当初就是这样被训练的,输入分布不对会导致效果骤降。
  • 输出是 1000 类概率:网络返回一个形状为 (batch, 1000) 的张量,每个维度对应一个 ImageNet 类别概率;用 argmax 取出最大概率的类别编号,再用 decode_predictions 转成可读的类别名称(如 [('n02099712', 'Labrador_retriever', 0.53), ...])。
  • 用 vgg.summary() 可以查看完整结构:VGG-16 全模型约 138,357,544 个参数,从 224×224×3 的输入逐层经过 5 个卷积块,最后展平(Flatten)为 25088 维向量送入全连接层。

PyTorch:同样的能力,不同的 API

在 TransferLearningPyTorch.ipynb 中,加载逻辑对应为:

import torchvision
vgg = torchvision.models.vgg16(pretrained=True)
res = vgg(sample_image)
print(res[0].argmax())  # 例如 282,对应 ImageNet 类别索引

加载类别索引表后即可把编号翻译成名字(如 ['n02123159', 'tiger_cat'])。PyTorch 版本里还顺带介绍了 Dropout 层:它作为一种**正则化(regularization)**手段,在训练时按比例(VGG 中约为 30%~50%)随机丢弃上一层的部分神经元,帮助优化过程跳出局部极小值,并在不同神经通路间分散"决策权",提升网络稳定性。

PyTorch 加载数据也有讲究:使用 transforms.Compose 组合 Resize(256) → CenterCrop(224) → ToTensor() → 按 ImageNet 统计值 Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) 做标准化,然后交给 torchvision.datasets.ImageFolder 读取目录式数据集。

实战数据集:猫狗大战(Cats vs. Dogs)

课程示例选用的是经典的 Cats vs. Dogs(猫狗大战) 数据集(可自 Microsoft 官网下载),非常接近真实世界的图像分类场景。

数据下载与清理

!wget -P data https://download.microsoft.com/download/3/E/1/3E1C3F21-ECDB-4869-8368-6DEBA77B919F/kagglecatsanddogs_5340.zip
import zipfile
if not os.path.exists('data/PetImages'):
    with zipfile.ZipFile('data/kagglecatsanddogs_5340.zip', 'r') as zip_ref:
        zip_ref.extractall('data')

注意:该数据集内混有损坏图片文件,必须先行清理。课程的 tfcv.py 提供了 check_image_dir 工具,用 PIL 验证每个文件是否为合法 JPEG,并删除损坏文件——实际运行会输出上百条 Corrupt image or wrong format: ... 日志,这正是数据清洗的必经环节。

TensorFlow 的 Dataset API:从目录加载图像

由于真实数据集常常无法整体装入内存,课程演示了 Keras 的 image_dataset_from_directory 生成器式数据流:

data_dir = 'data/PetImages'
batch_size = 64
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)
  • 它自动按子目录名识别类别(ds_train.class_names 返回 ['Cat', 'Dog']),并完成缩放。
  • 两次调用必须使用相同的 seed=13,以保证训练/验证划分完全一致。
  • 数据集可直接传给 model.fit,也可用 for x, y in ds_train 遍历,如输出 Training batch shape: features=(64, 224, 224, 3), labels=(64,)。
  • 遍历到的像素值范围为 0~255,送入网络前需要缩放到 0~1,绘图时则要转回 int 类型。

PyTorch 的 DataLoader

PyTorch 一侧使用 DataLoader 与 random_split:

dataset = torchvision.datasets.ImageFolder('data/PetImages', transform=trans)
trainset, testset = torch.utils.data.random_split(dataset, [20000, len(dataset)-20000])
train_loader = torch.utils.data.DataLoader(trainset, batch_size=16)
test_loader = torch.utils.data.DataLoader(testset, batch_size=16)

两条迁移学习实战路线

路线一:把预训练网络当"免费特征提取器"

最直接的做法是手动预计算特征:去掉 VGG-16 顶部分类层(TensorFlow 用 include_top=False;PyTorch 直接用 vgg.features),把每张图片跑一遍得到形状为 (7, 7, 512) 的特征张量(PyTorch 中是 [1, 512, 7, 7]),再在这些特征上训练一个简单分类器。

TensorFlow 侧的核心代码:

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'])
model.fit(ds_features_train, validation_data=ds_features_test)

由于是二分类,这里用 sigmoid 激活 + binary_crossentropy 损失。课程运行结果显示:训练集精度约 0.9144、验证精度约 0.9516——仅用一个全连接层,就拿到了约 95% 的分类精度。

PyTorch 侧用 torch.utils.data.TensorDataset 打包特征张量与标签,再 random_split([700, 100]) 划分训练/验证,同样以单层 Linear(512*7*7, 2) + LogSoftmax 训练,验证精度可达约 98%。课程也提醒:这种手动提取方式耗时较长,示例中仅使用了全部图片的子集。

路线二:端到端地"借用"整个 VGG 网络

更优雅的作法是把 VGG-16 当作自己网络的第一层直接参与训练。Keras 的架构之美在于:预训练模型本身可以作为一个 layer 被嵌入任意新网络。

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.summary()

为什么要冻结(freeze)卷积特征提取器的权重? 如果不冻结,尚未训练好的分类层产生的巨大误差会通过反向传播"摧毁"卷积层中宝贵的预训练权重。模型结构打印显示:整个网络总参数约 14,739,777,但可训练参数只有 25,089——其余 1470 万参数全部来自冻结的 VGG-16。这正是迁移学习的精髓:用更少的可训练参数、更少的样本完成微调。

训练结果验证精度约 0.9671(对比手动提取路线的 0.9516,端到端训练更进一步)。训练完成后可以用 model.save('data/cats_dogs.tf') 保存,随时用 keras.models.load_model 重新加载,避免下次实验把模型搞坏后从头再来。

PyTorch 的对应操作是:把 vgg.classifier 替换为 torch.nn.Linear(25088, 2),并将 vgg.features 中所有参数的 requires_grad 置为 False;模型约 1500 万参数中同样只有约 5 万可训练。训练时建议使用课程提供的 train_long 函数实时观察中间指标,且强烈建议在 GPU 上运行。

GPU 加速检查

VGG-16 等深度模型对算力要求不低,课程特别演示了如何确认 GPU 可用:

  • TensorFlow:tf.config.list_physical_devices('GPU'),若返回 [PhysicalDevice(...)] 即说明 GPU 可用,Keras 会自动加速。
  • PyTorch:需要显式指定计算设备——device = 'cuda' if torch.cuda.is_available() else 'cpu',再把模型和输入张量 .to(device)。

微调(Fine-tuning):让特征更贴合你的数据

如果直接复用 ImageNet 特征就能有 95% 以上的精度,那为什么要微调?因为当你的目标图像与 ImageNet 图片视觉差异较大时(例如植物叶片、工业产线零件),预训练特征组合可能并非最优。此时就应该开始训练部分卷积层。

课程给出了明确的微调纪律:

  1. 先冻结,先训分类层:一定要先冻结特征提取器、训练若干轮次让分类层权重稳定下来,再解冻。如果一开始就用未冻结参数端到端训练,巨大的误差很可能毁掉预训练权重。
  2. 不要一次性全部解冻:可以先只解冻最后几层卷积——它们包含的高层模式与你的图像最相关。课程示例用循环只保留最后 4 层可训练:
for i in range(len(model.layers[0].layers) - 4):
    model.layers[0].layers[i].trainable = False

解冻后可训练参数从 2.5 万跃升至约 7,104,513(约占总参数一半)。3. 使用更小的学习率(PyTorch 示例用 lr=0.0001),把对预训练权重的冲击降到最低。即便用低学习率,也可以预期训练初期精度会先下跌,最终才回升到比冻结权重更高的水平。

注意:微调阶段需要把梯度穿过网络很多层回传,训练会明显变慢。

课程还顺带介绍了现代网络(如 ResNet)中的 Batch Normalization(批归一化) 层:它计算当前 mini-batch 所有值的均值与标准差,在送入激活函数前把信号归一化,防止深层网络训练中数值大幅越界,显著提升深度网络的稳定性。

进阶实验:可视化"理想猫"与对抗样本

预训练网络的"大脑"里其实存着各种模式的概念——包括理想猫(ideal cat)、理想狗、理想斑马。课程用一个非常有趣的实验把这些概念可视化出来。

用梯度下降"反向调图"

思路是:从一个随机噪声图像出发,用梯度下降优化不断调整图像本身,直到网络认为它是猫。优化目标是交叉熵损失(课程用 sparse categorical cross-entropy,目标类别为 284,即 Siamese cat):

x = tf.Variable(tf.random.normal((1,224,224,3)))
target = [284]  # Siamese cat

def cross_entropy_loss(target, res):
    return tf.reduce_mean(keras.metrics.sparse_categorical_crossentropy(target, res))

with tf.GradientTape() as t:
    res = model(x)
    loss = loss_fn(target, res)
    grads = t.gradient(loss, x)
    x.assign_sub(eta * grads)   # x^{(i+1)} = x^{(i)} - η·∂L/∂x

图像优化循环示意

然而,直接这样优化的结果非常接近随机噪声——因为"让网络把输入当成猫"的方式有无数种,其中很多在视觉上毫无意义。虽然这些图像确实包含大量猫的典型模式,但没有任何约束保证它们看起来像一只猫。

引入 Variation Loss:让图像"变得平滑"

解决办法是在损失函数中加入变分损失(variation loss)——衡量图像中相邻像素的相似程度。最小化它会让图像更平滑、去除噪声,从而暴露出更吸引人的视觉模式。课程的实际实现是两者加权组合:

def total_loss(target, res):
    return 10 * tf.reduce_mean(keras.metrics.sparse_categorical_crossentropy(target, res)) + \
           0.005 * tf.image.total_variation(x, res)

系数 10 和 0.005 可以自行调节,观察图像如何变化。加入变分损失后,优化 900 个 epoch 得到的图像已能看出可辨识的轮廓:

理想猫与理想斑马 理想斑马
理想猫 理想斑马

完整复现代码见 AdversarialCat_TF.ipynb。

从"理想化"到"对抗攻击"

同样的技术稍作改造,就是著名的对抗攻击(adversarial attack):取一张被网络正确识别为"狗"的图像,用梯度下降优化做微小扰动,直到网络开始把它分类成"猫"——尽管它看起来仍然是一只狗:

原始狗图像 被分类为猫的狗图像
原始狗图像 被网络分类为"猫"的狗图像

这揭示了深度模型脆弱性的一面:识别依据并非人类可理解的视觉特征,这一点对安全相关的应用尤为重要。

深入训练技巧与作业

课程建议课后通读 TrainingTricks.md 系统学习深度网络训练技巧,要点包括:

  • 数值区间控制:尽量让网络内所有值处于 [-1..1] 或 [0..1],避免浮点计算精度损失。
  • 权重初始化:相比标准正态分布 N(0,1),优先使用 uniform、N(0,1/n)、N(0,1/√n_in),以及著名的 Xavier 初始化(glorot) N(0,√(2/(n_in+n_out))),后者在前后向传播中都能保持信号在合理区间。
  • Batch Normalization:按 mini-batch 计算均值/标准差做归一化,提升最终精度并加速训练。
  • Dropout:随机丢弃一定比例神经元(通常 10%~50%),可视为"隐式模型平均",参考 Dropout.ipynb 的 MNIST 实验。
  • 防止过拟合:早停(early stopping)、显式权重衰减/正则化、模型平均、Dropout 等手段。
  • 优化器选择:动量 SGD、Adagrad、RMSProp、Adam(不确定选哪个就用 Adam);梯度裁剪(θ 通常取 1 或 10)应对梯度爆炸;学习率衰减。
  • 架构选择:参考 CNN 架构总览,避免模型过强于数据量导致过拟合。

课后作业:Oxford Pets 品种分类

本课的 lab/README.md 要求使用真实世界的 Oxford-IIIT Pets 数据集(含 35 种猫狗品种)构建迁移学习分类器,可先打开 OxfordPets.ipynb 开始。下载数据集:

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

作业的核心启示是:迁移学习与预训练网络能让我们相对轻松地解决真实世界的图像分类问题,但预训练网络只对"相似类型"的图像效果好——如果开始分类差异极大的图像(例如医学影像),效果大概率会明显变差。这也是课程在多个 notebook 结尾反复强调的"Takeaway"。

小结

通过迁移学习,你能够快速组装一个针对自定义对象分类任务的高精度分类器——绝大多数工作由 ImageNet 预训练权重完成,你只需要训练一个极小的分类头,必要时再微调少量高层卷积层。同时,当前这些更复杂的任务通常需要较高的算力,CPU 上难以轻松完成。课程预告:下一单元将尝试用更轻量级的实现,在更低算力下训练同样的模型——精度仅略有下降。

动手实验建议:优先跑通 TransferLearningTF.ipynb 或 TransferLearningPyTorch.ipynb;再尝试作业中的挑战——换用与训练数据差异较大的全新图像类型,观察你的迁移模型表现是好是坏。

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