TensorFlow 迁移学习怎么实现?预训练模型怎么选?
迁移学习解决的核心问题
从零训练一个深度学习模型需要大量标注数据和算力,现实中经常遇到数据集只有几百张图的情况。迁移学习的思路很简单:把别人在百万级数据上训练好的模型拿过来,只改造最后一部分,就能在自己的任务上获得不错的表现。
这背后依赖一个关键事实——深度卷积网络的前几层学到的是通用视觉特征(边缘、纹理、色彩模式),这些特征对大多数视觉任务都有效,只有最后几层才负责任务特定的语义判断。所以冻结前面的层、只训练后面的层,既省计算又保效果。
2014 年 Yosinski 等人的实验就验证了这一点:迁移前几层的特征,在新任务上几乎不掉精度;迁移的层越靠后,和原始任务越绑定,迁移效果才逐渐下降。这也是为什么迁移学习在视觉任务上效果特别好的原因——ImageNet 的 1000 个类别已经覆盖了足够多的视觉模式。
两种迁移学习策略的选择
特征提取:冻结全部,只训分类头
当你的数据集很小(几百到几千张),且和 ImageNet 之类的原始数据集差异不大时,直接冻结整个预训练模型,只在顶部加几层全连接层做分类。这种方式训练最快,过拟合风险最低。
pythonfrom tensorflow.keras.applications import MobileNetV2 from tensorflow.keras import layers, models base_model = MobileNetV2(weights='imagenet', include_top=False, input_shape=(224, 224, 3)) base_model.trainable = False # 冻结全部权重 model = models.Sequential([ base_model, layers.GlobalAveragePooling2D(), layers.Dense(256, activation='relu'), layers.Dropout(0.5), layers.Dense(10, activation='softmax') ]) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
关键点在于 include_top=False,这会去掉原始模型的分类层,只保留特征提取部分。GlobalAveragePooling2D 将二维特征图压缩成一维向量,比 Flatten 更不容易过拟合——因为 Flatten 会保留所有空间信息,参数量骤增,小数据集上特别容易过拟合。
特征提取阶段通常 5-10 个 epoch 就够收敛了,因为只训练几千个参数(分类头的全连接层),而预训练模型的上百万参数是锁死的。
微调:解冻部分层联合训练
如果你的数据集稍大,或者和原始数据集有差异,冻结全部层可能欠拟合。这时可以解冻预训练模型的最后几层,让它们在新数据上微调。但要注意:解冻的层数越多,过拟合风险越大,学习率也要相应降低。
python# 先用特征提取方式训练几个 epoch model.fit(train_dataset, epochs=5) # 解冻最后 20 层进行微调 base_model.trainable = True for layer in base_model.layers[:-20]: layer.trainable = False # 学习率降到原来的 1/100 model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) model.fit(train_dataset, epochs=10)
微调的学习率通常设在 1e-5 到 1e-4 之间,太大会破坏预训练权重。一个实用的策略是先冻结训练收敛,再解冻微调,而不是一开始就解冻。先冻结阶段让分类头有个合理的初始化,解冻后才不会产生梯度爆炸把预训练权重冲坏。
预训练模型怎么选
TensorFlow 生态中有两大来源:Keras Applications(内置)和 TensorFlow Hub(社区贡献)。Keras Applications 更稳定,适合大多数场景;TensorFlow Hub 模型种类更多,但需要注意版本兼容性。从 2024 年起,TensorFlow Hub 上的新模型已逐步迁移到 Kaggle Models,使用时建议优先查看 Kaggle 上的版本。
选择预训练模型时,有三个维度要权衡:参数量(决定推理速度和显存占用)、在 ImageNet 上的 Top-1 精度(代表特征提取能力)、以及输入分辨率(影响细节捕捉能力)。下面按场景具体分析。
按场景选模型
移动端和边缘设备,优先选 MobileNetV3 或 EfficientNet-Lite:
pythonfrom tensorflow.keras.applications import MobileNetV3Small base_model = MobileNetV3Small(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
MobileNetV3Small 只有约 250 万参数,推理速度在手机上可以做到实时。它使用了深度可分离卷积和挤压-激励结构,在参数效率和精度之间做了很好的平衡。如果你的硬件稍好一点,EfficientNet-Lite0 在精度和速度之间平衡得更好,而且 Lite 版本去掉了 SiLU 激活函数,对 TFLite 部署更友好。
服务端通用分类,ResNet50 或 EfficientNetB0 是安全的选择:
pythonfrom tensorflow.keras.applications import EfficientNetB0 base_model = EfficientNetB0(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
EfficientNet 系列通过复合缩放策略同时调整深度、宽度和分辨率,同等参数量下精度通常优于 ResNet。但 ResNet50 的社区资源更丰富,遇到问题更容易找到解决方案。如果对精度要求高且算力充足,可以上 EfficientNetB3-B5,Top-1 精度可以从 77% 提升到 82% 以上。
医学影像,DenseNet121 是被验证最多的选择。它的密集连接结构使得每层都能直接访问前面所有层的特征图,这对需要精细纹理信息的医学图像特别有效。CheXNet 等经典工作就是基于 DenseNet121 在 ChestX-ray14 数据集上做迁移学习。不过 DenseNet 的推理速度较慢,如果对延迟敏感,可以考虑用 EfficientNetB3 替代。
目标检测和实例分割的骨干网络,通常选 ResNet50 或 ResNet101。Faster R-CNN、Mask R-CNN、RetinaNet 等检测框架的官方实现都以 ResNet 为默认骨干。Swing Transformer 近年也很流行,但 TensorFlow 生态中 ResNet 的支持更成熟。
文本任务,推荐用 KerasNLP 加载 BERT:
pythonimport keras_nlp classifier = keras_nlp.models.BertClassifier.from_preset("bert_base_en_uncased") classifier.fit(train_dataset, epochs=3)
KerasNLP 是 TensorFlow 官方推荐的高级 API,比直接加载 TensorFlow Hub 上的 BERT 模型更简洁,也更容易微调。对于中文任务,使用 bert_base_zh 预训练模型。
预训练模型对比
| 模型 | 参数量 | 推理速度 | ImageNet Top-1 | 适用场景 |
|---|---|---|---|---|
| MobileNetV3Small | 2.5M | 快 | 67.4% | 移动端、嵌入式 |
| EfficientNetB0 | 5.3M | 中 | 77.1% | 通用分类、服务端 |
| ResNet50 | 25M | 中 | 76.0% | 通用分类、检测骨干 |
| EfficientNetB3 | 12M | 慢 | 81.6% | 高精度分类 |
| DenseNet121 | 8M | 慢 | 75.0% | 医学影像 |
| InceptionV3 | 23M | 中 | 77.9% | 复杂场景分类 |
| BERT-Base | 110M | 慢 | - | 文本分类、NER |
参数量不等于显存占用——推理时的显存还受 batch size 和输入分辨率影响。移动端部署时,除了参数量还要看 FLOPs。EfficientNetB0 的 FLOPs 约为 0.4B,而 ResNet50 约为 4.1B,差了 10 倍,但精度只差 1%。
完整实战:用 ResNet50 做猫狗分类
这是一个可以直接跑起来的端到端示例,从数据加载到微调全流程覆盖。
数据准备
pythonimport tensorflow as tf import tensorflow_datasets as tfds # 加载猫狗数据集 dataset, info = tfds.load('cats_vs_dogs', with_info=True, as_supervised=True) train_data = dataset['train'].take(20000) val_data = dataset['train'].skip(20000).take(5000) IMG_SIZE = 224 BATCH_SIZE = 32 def preprocess(image, label): image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE)) image = tf.keras.applications.resnet50.preprocess_input(image) return image, label # 数据增强 data_augmentation = tf.keras.Sequential([ layers.RandomFlip('horizontal'), layers.RandomRotation(0.1), layers.RandomZoom(0.1), ]) train_ds = train_data.map(preprocess).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE) val_ds = val_data.map(preprocess).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
preprocess_input 不是可选的——每个预训练模型都有自己的归一化方式,ResNet 要求 BGR 格式且减去 ImageNet 均值。如果跳过这一步,精度可能掉 10% 以上。prefetch(tf.data.AUTOTUNE) 让数据加载和模型训练并行执行,避免 GPU 等数据。
构建和训练
pythonfrom tensorflow.keras.applications import ResNet50 from tensorflow.keras import layers, models base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(IMG_SIZE, IMG_SIZE, 3)) base_model.trainable = False # Functional API 比 Sequential 更灵活 inputs = tf.keras.Input(shape=(IMG_SIZE, IMG_SIZE, 3)) x = data_augmentation(inputs) x = base_model(x, training=False) # training=False 保证 BN 层用推理模式 x = layers.GlobalAveragePooling2D()(x) x = layers.Dense(256, activation='relu')(x) x = layers.Dropout(0.5)(x) outputs = layers.Dense(1, activation='sigmoid')(x) model = models.Model(inputs, outputs) model.compile( optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'] ) # 第一阶段:只训练分类头 history = model.fit(train_ds, epochs=5, validation_data=val_ds)
这里有个容易忽略的细节:base_model(x, training=False)。如果传 training=True,BatchNormalization 层会使用当前 batch 的统计量,小 batch 下会导致训练不稳定。冻结阶段务必传 training=False,让 BN 层用预训练时积累的 running mean 和 running variance。
分类头的 256 维全连接层不是随便选的。太大了(比如 1024)容易过拟合,太小了(比如 32)可能瓶颈。一般取特征向量维度的 1/4 到 1/2 比较合适。ResNet50 输出的特征向量是 2048 维,所以 256 是合理选择。
微调
python# 解冻最后 10 层 base_model.trainable = True for layer in base_model.layers[:-10]: layer.trainable = False model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5), loss='binary_crossentropy', metrics=['accuracy'] ) # 第二阶段:微调 history_fine = model.fit(train_ds, epochs=5, validation_data=val_ds)
微调时如果验证损失开始上升,说明解冻层数过多或学习率过高,可以尝试只解冻最后 5 层,或者把学习率降到 1e-6。解冻的层数可以通过查看 base_model.layers 的名字来判断——通常 conv5 开头的层是最后的卷积块,解冻这些就够了。
高级技巧
渐进式解冻
不是一次解冻 N 层,而是分阶段逐步解冻,每阶段降低学习率:
python# 阶段 1:冻结全部,lr=1e-3 base_model.trainable = False model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss='sparse_categorical_crossentropy') model.fit(train_ds, epochs=3) # 阶段 2:解冻最后 5 层,lr=1e-4 base_model.trainable = True for layer in base_model.layers[:-5]: layer.trainable = False model.compile(optimizer=tf.keras.optimizers.Adam(1e-4), loss='sparse_categorical_crossentropy') model.fit(train_ds, epochs=3) # 阶段 3:解冻最后 15 层,lr=1e-5 for layer in base_model.layers[:-15]: layer.trainable = False model.compile(optimizer=tf.keras.optimizers.Adam(1e-5), loss='sparse_categorical_crossentropy') model.fit(train_ds, epochs=5)
这种方式比一次性解冻更稳定,尤其在大模型上效果明显。每个阶段相当于让模型"适应"一次权重变化,避免了突然改变带来的训练震荡。实践中,3 阶段渐进式解冻通常比 1 阶段直接微调高 1-2% 精度。
学习率预热
微调开始时,模型刚从冻结状态解冻,直接用目标学习率可能导致训练震荡。可以先线性预热几个 step:
pythonwarmup_steps = 100 total_steps = 1000 class WarmupSchedule(tf.keras.optimizers.schedules.LearningRateSchedule): def __init__(self, base_lr, warmup_steps): super().__init__() self.base_lr = base_lr self.warmup_steps = warmup_steps def __call__(self, step): step = tf.cast(step, tf.float32) warmup_ratio = step / self.warmup_steps return tf.minimum(self.base_lr * warmup_ratio, self.base_lr) lr_schedule = WarmupSchedule(base_lr=1e-4, warmup_steps=warmup_steps) model.compile(optimizer=tf.keras.optimizers.Adam(lr_schedule), loss='sparse_categorical_crossentropy')
预热步数通常设为总步数的 5%-10%。预热完成后学习率达到目标值,之后可以配合余弦退火继续衰减,这样训练过程更稳定。
混合精度训练加速
如果用 V100 或 A100 等 Tensor Core GPU,开启混合精度可以加速 1.5-2 倍,精度几乎无损:
pythonfrom tensorflow.keras import mixed_precision mixed_precision.set_global_policy('mixed_float16') # 构建模型时注意最后一层用 float32 outputs = layers.Dense(10, activation='softmax', dtype='float32')(x)
最后一层必须保持 float32,因为 float16 的求和精度不够,softmax 之前的 logits 如果数值较大,float16 下容易出现数值溢出,导致 loss 变成 NaN。开启混合精度后,显存占用通常减少 30%-50%,可以用更大的 batch size。
数据增强的正确用法
数据增强层应该放在模型内部而不是预处理阶段,这样在推理时不会执行增强:
pythondata_augmentation = tf.keras.Sequential([ layers.RandomFlip('horizontal'), layers.RandomRotation(0.1), layers.RandomZoom(0.1), layers.RandomContrast(0.1), ]) # 在模型中:训练时增强,推理时不增强(自动处理) inputs = tf.keras.Input(shape=(224, 224, 3)) x = data_augmentation(inputs, training=True) x = base_model(x, training=False)
注意旋转角度不要设太大——0.1 弧度约 6 度,对大多数任务足够了。设到 0.5(约 29 度)可能导致图像中目标被旋转到不可识别的角度,反而降低训练效果。缩放也是同理,0.1-0.2 的范围比较安全。
差异学习率
解冻微调时,可以让靠近输出的层用较大的学习率,靠近输入的层用更小的学习率。这样高层特征适应新任务更快,底层通用特征变化更慢:
python# 给不同层设置不同学习率 base_layers = base_model.layers fine_tune_at = len(base_layers) - 10 optimizer = tf.keras.optimizers.Adam() # 自定义训练步中实现差异学习率 @tf.function def train_step(images, labels): with tf.GradientTape() as tape: predictions = model(images, training=True) loss = loss_object(labels, predictions) gradients = tape.gradient(loss, model.trainable_variables) # 对不同层应用不同的学习率缩放 scaled_gradients = [] for grad, var in zip(gradients, model.trainable_variables): if var in base_model.trainable_variables: scale = 0.1 # 预训练层用 1/10 的学习率 else: scale = 1.0 # 新加的分类头用正常学习率 scaled_gradients.append(grad * scale) optimizer.apply_gradients(zip(scaled_gradients, model.trainable_variables)) return loss
这种做法在自定义训练循环中比较常见,Keras 的 model.fit 没有直接支持,但可以通过自定义优化器或回调实现。
常见问题
迁移学习精度反而比从零训练低?
可能是负迁移——当新任务和原始数据集差异太大时,预训练特征反而是干扰。比如用 ImageNet 预训练模型做卫星图像分类,可能不如从头训练。此时可以尝试只保留前几层(更通用的特征),或者用目标领域的预训练模型(如遥感领域的 RemoteCLIP)。另一个思路是增大解冻层数,让模型有更多参数去适应新域。
微调时 loss 震荡怎么办?
三个排查方向:学习率太大(降到 1e-5 甚至 1e-6)、解冻层数太多(减少到 5 层以下)、batch size 太小(BatchNorm 统计量不稳定,至少保证 batch size >= 16)。如果降低学习率后仍然震荡,试试加梯度裁剪:optimizer = tf.keras.optimizers.Adam(clipnorm=1.0)。
冻结层占用显存吗?
冻结只是不计算梯度,权重本身仍然在显存里。冻结不会减少显存占用,只会减少训练时间和反向传播的计算量。所以冻结 20 层和冻结全部层的显存占用是一样的,只是训练速度不同。
如何判断该用特征提取还是微调?
简单判断:数据量小于原始数据集的 1/10 且分布相似,用特征提取;数据量较大或分布差异明显,用微调。如果不确定,两种都试,看验证集表现。实际项目中,先跑特征提取作为 baseline,再尝试微调看有没有提升,是最稳妥的流程。
TensorFlow Hub 和 Keras Applications 有什么区别?
Keras Applications 是 tf.keras.applications 模块内置的模型,不需要额外下载依赖,API 风格统一。TensorFlow Hub 是社区贡献的模型仓库,种类更多(包括 BERT、YOLO 等),但加载方式不同(用 hub.KerasLayer),且模型质量参差不齐。新项目建议优先用 Keras Applications,找不到的模型再去 Kaggle Models 上搜索。
实际部署注意事项
训练完迁移学习模型后,部署时有两个容易踩坑的地方:
输入预处理必须一致。训练时用了 resnet50.preprocess_input,推理时也必须用。很多线上精度下降的问题都是预处理不一致导致的。最好把预处理层直接包进模型:
python# 把预处理嵌入模型,部署时只做 resize inputs = tf.keras.Input(shape=(None, None, 3)) x = tf.keras.layers.Resizing(224, 224)(inputs) x = tf.keras.applications.resnet50.preprocess_input(x) x = base_model(x, training=False) # ...
这样部署时只需要传原始图像,不需要在服务端维护一套预处理逻辑。
模型导出格式。如果部署环境不是 Python(比如 TensorFlow Serving、TensorRT),建议导出为 SavedModel 格式:
pythonmodel.save('my_transfer_model') # SavedModel 格式
如果需要更小的模型体积,可以用 TensorFlow Lite 量化:
pythonconverter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open('model.tflite', 'wb') as f: f.write(tflite_model)
量化后模型体积减少约 4 倍,精度损失通常在 1% 以内,对移动端部署很实用。如果需要更极致的压缩,可以用全整数量化(需要提供代表性的校准数据集):
pythondef representative_dataset(): for image, _ in val_ds.take(100): yield [image] converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = representative_dataset converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] tflite_model = converter.convert()
全整数量化后模型体积再减一半,推理速度在支持 INT8 的 NPU 上可以快 2-3 倍。
迁移学习的核心不是记住多少个 API,而是理解"通用特征到任务特征"这个思路。选对预训练模型、掌握冻结和解冻的节奏、注意预处理和部署的一致性,就能在大多数任务上用最少的资源拿到最好的效果。