迁移学习的核心理念,是将一个已经在大规模数据集上学到的视觉特征表示,迁移到新的图像任务中继续利用。对于样本量有限的项目而言,如果从随机初始化开始训练,模型通常需要更多训练图片和更长训练周期,才能逐步学到边缘、纹理、形状等通用视觉特征;而预训练模型往往已经提前完成了这部分基础能力的学习。

但“加载预训练权重再训练几轮”并不等同于完整的迁移学习方案。真正影响图像分类效果的,通常在于权重与数据预处理是否一致、冻结层范围是否合适、分类头是否正确替换,以及验证流程能否及时识别过拟合。下面以一个多分类图像识别任务为例,整理出一套可执行、可检查、适合工程落地的训练流程。
一、问题背景
假设项目需要识别多种物体,而每个类别只有几百张甚至几十张图片。在数据规模不足的前提下,直接训练完整卷积神经网络通常会遇到三个典型问题:
模型参数量相对于样本规模过大,容易记住训练集细节,而不是学习真正可泛化的类别规律。 训练初期的梯度既要负责“学习通用视觉特征”,又要负责“适应当前分类任务”,整体优化成本更高。 如果类别分布不平衡,只观察总体准确率,可能会掩盖少数类别几乎无法被正确识别的情况。迁移学习通常可以拆分为两个部分来理解:前面的特征提取器负责把输入图像转换为高层语义表示,最后的分类头负责将这些表示映射到目标类别。预训练阶段学到的特征提取器往往可以直接复用,而分类头通常必须根据新任务的类别数量重新设计。
这里所说的“预训练”,仅表示模型参数来自另一个训练任务,并不意味着它一定天然适合当前数据集。如果源数据与目标数据差异较大,预训练特征的可迁移性就会下降,这时往往需要解冻更多网络层进行微调,或者重新评估所选模型结构是否合适。
二、两种训练策略
1. 冻结特征提取器
冻结卷积骨干网络,只训练新建的分类头。这种做法的优势在于需要更新的参数更少、训练速度更快,也更适合小样本学习场景。缺点是骨干网络无法进一步适应目标领域,当目标图像与预训练数据差异较大时,特征表达能力可能不够。
冻结参数的关键操作,是将 requires_grad 设置为 False。同时,优化器只应接收需要更新的参数;否则即使冻结参数不会被更新,也会带来不必要的计算开销和代码理解上的歧义。
2. 部分或全部微调
常见做法是先训练分类头,让输出层先进入相对合理的状态,然后再解冻骨干网络的后几层,以较小学习率继续训练。网络后部通常包含与任务更相关的语义特征,优先解冻这些层,能够在适应新任务和保留通用特征之间取得更好的平衡。
微调阶段通常会使用分层学习率:分类头采用相对更大的学习率,骨干网络采用更小的学习率。学习率并不存在脱离数据和模型的固定标准,下面给出的配置只能作为起点,实际数值仍应结合验证集表现和训练曲线进行调整。
三、准备数据集
推荐使用按类别分目录的数据结构,便于直接配合 ImageFolder 使用:
dataset/train/cat/dog/bird/val/cat/dog/bird/ 训练集可以使用随机裁剪、水平翻转等数据增强方式;验证集则应尽量只保留确定性的缩放与裁剪。训练和验证必须使用相同的归一化参数,但不能把随机增强带入验证流程,否则评估结果会产生额外波动,影响模型效果判断。
from torchvision import datasets, transformsfrom torch.utils.data import DataLoaderimage_size = 224mean = [0.485, 0.456, 0.406]std = [0.229, 0.224, 0.225]train_transform = transforms.Compose([transforms.RandomResizedCrop(image_size, scale=(0.7, 1.0)),transforms.RandomHorizontalFlip(),transforms.ToTensor(),transforms.Normalize(mean, std),])val_transform = transforms.Compose([transforms.Resize(256),transforms.CenterCrop(image_size),transforms.ToTensor(),transforms.Normalize(mean, std),])train_set = datasets.ImageFolder("dataset/train", transform=train_transform)val_set = datasets.ImageFolder("dataset/val", transform=val_transform)train_loader = DataLoader(train_set, batch_size=32, shuffle=True, num_workers=2)val_loader = DataLoader(val_set, batch_size=32, shuffle=False, num_workers=2)num_classes = len(train_set.classes)print(train_set.classes) 如果使用的预训练模型规定了不同的输入尺寸或归一化参数,就应该以该模型公开说明中的配置为准。不能仅仅因为图片可以送入网络,就误以为预处理已经完全正确。
四、替换分类头并训练
下面使用 torchvision 提供的 ResNet 作为示例。模型名称和权重接口可能会随安装版本变化,因此实际运行时应以当前环境中的官方文档和接口说明为准。代码中的 weights 是模型权重配置对象,并不是密钥字段。
import copyimport torchfrom torch import nnfrom torchvision.models import resnet18, ResNet18_Weightsdevice = torch.device("cuda" if torch.cuda.is_a vailable() else "cpu")model = resnet18(weights=ResNet18_Weights.DEFAULT)# 第一阶段:冻结骨干网络for parameter in model.parameters():parameter.requires_grad = Falsein_features = model.fc.in_featuresmodel.fc = nn.Linear(in_features, num_classes)model = model.to(device)criterion = nn.CrossEntropyLoss()optimizer = torch.optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-4) 训练与验证函数必须明确切换 train() 和 eval() 模式。这一步对包含 BatchNorm 或 Dropout 的模型尤其关键:训练模式会更新部分统计量并启用随机失活,而验证模式则用于获得更稳定、更可比较的评估结果。
def run_epoch(model, loader, criterion, optimizer=None):is_train = optimizer is not Nonemodel.train(is_train)total_loss = 0.0total_correct = 0total_count = 0for images, labels in loader:images = images.to(device)labels = labels.to(device)if is_train:optimizer.zero_grad(set_to_none=True)with torch.set_grad_enabled(is_train):logits = model(images)loss = criterion(logits, labels)if is_train:loss.backward()optimizer.step()total_loss = loss.item() * labels.size(0)total_correct = (logits.argmax(dim=1) == labels).sum().item()total_count = labels.size(0)return total_loss / total_count, total_correct / total_countbest_state = Nonebest_val_acc = -1.0for epoch in range(8):train_loss, train_acc = run_epoch(model, train_loader, criterion, optimizer)with torch.no_grad():val_loss, val_acc = run_epoch(model, val_loader, criterion)print(f"epoch={epoch 1} "f"train_loss={train_loss:.4f} train_acc={train_acc:.4f} "f"val_loss={val_loss:.4f} val_acc={val_acc:.4f}")if val_acc > best_val_acc:best_val_acc = val_accbest_state = copy.deepcopy(model.state_dict())if best_state is not None:model.load_state_dict(best_state)torch.sa ve({ "model": model.state_dict(), "classes": train_set.classes},"classifier.pt",) 这里保存的是验证集表现最好的模型权重,而不是无条件保存最后一个 epoch 的结果。需要注意的是,验证集本身并不是完全独立的最终测试集,因此在正式项目评估中,仍应额外保留一份不参与调参的测试数据。
五、从冻结到微调
如果训练完分类头后,训练集和验证集表现都不理想,可能说明模型容量、数据质量或预处理流程存在问题;如果训练集指标持续提升,而验证集停滞甚至下降,则应优先怀疑过拟合。确认数据和标签没有明显异常后,可以进一步解冻骨干网络的后几层进行微调。
# 解冻 layer4,分类头继续保持可训练for parameter in model.layer4.parameters():parameter.requires_grad = Trueoptimizer = torch.optim.AdamW([{ "params": model.layer4.parameters(), "lr": 1e-5},{ "params": model.fc.parameters(), "lr": 1e-4},], weight_decay=1e-4)for epoch in range(5):train_loss, train_acc = run_epoch(model, train_loader, criterion, optimizer)with torch.no_grad():val_loss, val_acc = run_epoch(model, val_loader, criterion)print(epoch 1, train_loss, train_acc, val_loss, val_acc) 进入微调阶段后,有三件事尤其值得注意。第一,完成解冻之后,要再次确认优化器参数列表已经真正包含新解冻的层。第二,这一阶段的学习率通常应低于只训练分类头时的设置。第三,当样本量本来就很少时,BatchNorm 的统计量可能不够稳定;是否需要冻结它,要结合模型结构、批大小和数据分布综合判断,不能简单套用固定经验。
六、推理与类别映射
推理阶段必须复用验证阶段使用的确定性预处理,并按照训练集生成的类别顺序来解释输出结果。如果只保存模型权重而没有保存类别映射,部署后就可能出现数值预测正确、但类别名称对应错位的问题。
from PIL import Imagecheckpoint = torch.load("classifier.pt", map_location=device)classes = checkpoint["classes"]model.load_state_dict(checkpoint["model"])model.eval()image = Image.open("sample.jpg").convert("RGB")input_tensor = val_transform(image).unsqueeze(0).to(device)with torch.no_grad():probability = model(input_tensor).softmax(dim=1)[0]index = probability.argmax().item()print({ "label": classes[index], "score": float(probability[index])}) 输出的概率,本质上是模型在当前类别集合和训练分布下的相对置信度,不能直接理解为真实正确率。对于安全、医疗或其他高风险应用场景,还需要额外加入概率校准、拒识策略以及人工复核机制。
七、常见问题
预训练模型为什么仍然识别错误?
迁移学习复用的是模型参数,而不是目标领域的全部知识。当拍摄角度、光照条件、图像分辨率、背景环境或类别定义发生较大变化时,通用特征可能就不够用了。此时应先检查标签质量和类别边界,再考虑是否需要更合适的数据增强、更大的解冻范围或不同的模型结构。
训练准确率很高,验证准确率很低怎么办?
这通常符合过拟合的典型特征,但也可能意味着训练集与验证集之间存在分布差异,或者出现了数据泄漏。应检查同一对象的相似图片是否被拆分到了不同集合,适当降低不必要的增强强度,增加有效样本量,并结合早停或正则化方法控制过拟合。不要只根据一次验证结果就频繁调整模型。
类别数量不平衡时只改 batch size 可以吗?
通常不够。可以根据训练集中各类别的频率设置交叉熵损失权重,或者使用更合适的采样策略;同时建议报告每一类的召回率、精确率以及混淆矩阵。具体采用哪种方法,仍需结合少数类样本规模和业务评价目标来决定。
什么时候不适合冻结大部分层?
当目标数据与预训练数据差异非常明显,或者新任务依赖于预训练模型未覆盖的细粒度特征时,完全冻结大部分网络层可能会限制最终效果。这时可以逐步解冻后部网络进行微调,但应控制学习率,并使用独立测试集验证收益是否真实存在。
总结
迁移学习从来不只是“把一个现成模型直接拿来用”这么简单,它更像是一整套贯穿数据准备、预训练权重选择、训练策略制定、模型评估到部署落地的完整闭环。想把图像分类迁移学习真正做稳,至少要抓住几个关键点:先确认预训练权重与输入预处理严格匹配;优先训练新的分类头,再根据验证集表现决定是否继续微调;妥善保存最佳权重与类别映射;使用独立数据检验模型泛化能力;最后结合每类指标和混淆矩阵,把问题定位到模型、数据还是训练策略本身。
对于小样本视觉分类任务来说,冻结特征提取器通常是一个合理起点,但并不是最终答案。只有把训练曲线、数据分布和错误样本结合起来分析,才能准确判断问题究竟来自模型结构、数据质量,还是训练流程本身。
