“迁移学习还没整理好”——这句话我在好几个朋友的文档收藏夹里都见过。说真的,迁移学习这个方向入门不算难,但想把理论、代码、实验技巧串成一条线,确实容易越看越乱。今天我就把自己整理迁移学习内容的思路和踩过的坑从头到尾捋一遍,既帮自己把这些散落的知识点归位,也希望能给正在从零整理迁移学习资料的你一份可以直接参考的提纲。
这不算教程,更像一次“现场整理”。我会先说清楚迁移学习解决什么问题、有哪些主流方法,然后给出一套可复现的微调实验模板,再把实战里高频出现的坑逐个拆开,最后聊聊怎么把这些碎片沉淀成自己的知识体系。适合准备入门迁移学习、正在做领域自适应项目、或者单纯被各种资料淹没的同学参考。
1. 先把迁移学习到底解决什么问题说清楚
1.1 什么时候才需要迁移学习
很多人会把“迁移学习”和“预训练模型”划等号,其实不完全对。迁移学习的核心场景只有一个:目标域数据不够,但你又想训练一个效果不错的模型。这里的目标域,就是你真正要应用的场景。比如你在做一个工业零件表面缺陷分类项目,每个缺陷类别只有几百张现场图片,这点数据根本撑不起一个深层网络从头训练;但在公开的通用图像数据集上,已经有人训练好了效果很好的模型,这些模型里已经学到了边缘、纹理、形状这类底层视觉特征。
这时候迁移学习就派上用场了。把预先训练好的模型参数作为起点,用你那几百张现场图片做一次“定向调整”,让模型在保留通用特征的同时,学会识别你关心的缺陷类型。整个过程省去了大量数据和GPU时间,这是我在实际项目里选择它的第一理由。
还有一种情况也经常出现:任务相关,但数据分布不同。比如同样的零件,在不同车间、不同光照条件下拍摄,图像的背景和亮度差别很大。你有一个在旧产线数据上训练好的模型,直接拿去过新产线,效果大概率会下滑。这时也可以借助迁移学习的思想做域适应,让模型在不同环境下都能稳定工作。说白了,迁移学习不是花架子,而是专门解决“数据不够、训练太贵、分布不一致”这三个现实问题的。
1.2 几个绕不开的基本概念
整理迁移学习笔记,头一天我就被“领域、任务、源域、目标域”这些词绕晕了。我后来用一句话给自己顺明白了:领域是“模型看到的世界长什么样”,任务是“模型在这个世界里要做什么事”。
领域包含两部分,一个是特征空间,也就是说你用哪些信息描述样本;另一个是这些特征服从的概率分布。比如图像领域,特征可能是像素;文本领域,特征可能是词向量。任务则对应标签空间和预测函数,说白了就是你要预测什么、怎么预测。迁移学习中,我们已经有数据和标签的那个领域叫源域,你想最终解决的领域叫目标域。目标域可能只有少量有标签数据,或者干脆只有无标签数据。
迁移学习的定义也由此而来:利用源域中学到的知识,提升模型在目标域上的学习效果。注意是“提升”,不是随便用个预训练模型就算迁移了,关键是让目标域的任务做得比不迁移时更好。
按迁移场景还能分成几类:归纳式迁移,源域和目标域任务不同,比如在自然图像上学会分类,再去解决风格分类;直推式迁移,任务相同但分布不同,比如同一个缺陷分类任务,把从A产线学到的知识用到B产线;无监督迁移,目标域没有任何标签,典型例子是跨域图片聚类。刚开始整理时不需要把这些分类背得多熟,但脑子里要有这个框架,后面看论文、选方案会清晰很多。
2. 主流的迁移学习方法,按实现思路拆开看
2.1 基于实例的迁移:给源域样本重新打分
这类方法说起来很直观:源域里有些样本跟目标域很接近,有些差别很大,那就在训练时给接近的样本更高权重,给不相关的样本更低权重,让源域和目标域的分布尽量靠近。就好比你想让北方人适应南方口味,先从北方菜里挑出那些少油不辣的来推广,而不是按原比例硬推。
实际用的时候,这种思路大多出现在传统机器学习里,比如通过重要性采样来调整权重。但深度学习高维场景下,精确估计每个样本的权重很困难,我很少单独用它。更多时候,我会把这个思想用在“数据筛选”上:从一个大而杂的源域数据池里,挑出与目标域风格最接近的一部分,再去做预训练或微调。这个操作简单,但对后续效果提升明显。
2.2 基于特征的迁移:把两个域拉到一个空间
基于特征的方法,核心是找到一组公共特征,让源域和目标域在这组特征上的分布差异变小。最经典的做法是MMD距离,也就是最大均值差异。你可以把MMD理解成一把尺子,专门用来量两个分布有多不一样,尺子读数越小,说明两个域越像。训练时,把MMD作为一个正则项写进损失函数,一边做分类任务,一边缩小两个域的特征差异。
后来深度学习流行起来,域对抗方法也火了一把。做法是给特征提取器后面接一个“域判别器”,它只干一件事:判断当前特征来自源域还是目标域。而特征提取器的目标正好相反,它想让判别器认不出来。两者互相博弈,最后得到一个不管来自哪个域都表现稳定的特征提权器。我做一个跨设备手势识别项目时用过这个思路,源域是台式机摄像头数据,目标域是笔记本摄像头数据,加了域对抗后准确率从78%涨到89%。这类方法比实例迁移更适应高维特征,是我在分布差异较大时的首选。
2.3 基于模型的迁移:预训练加微调
这是绝大多数人最常用的一条路,也是性价比最高的。它的假设是:模型底层学习的特征有很强的通用性,比如图像领域的边缘、颜色、角点,文本领域的词法、句法,这些知识可以跨任务复用;而上层则偏向具体任务,比如识别猫脸还是识别缺陷纹理,这部分需要根据目标域重新学习。
具体操作就是,把别人在大规模通用数据上训练好的模型拿过来,替换掉最后的任务层,接着用你的目标域数据以小学习率继续训练。我在很多实验里都发现,预训练模型像是读过很多书的实习生,你只需告诉他你们公司的业务流程,他很快能上手;而从零训练则像让一个实习生先花半年读基础教材,周期完全不一样。
2.4 基于关系的迁移:冷门但值得知道
还有一类方法是基于关系迁移,它关注的是源域和目标域中样本之间的关系,而不是样本本身。比如在社交网络里,朋友关系的结构可以迁移到另一个网络,用于链路预测。这类方法在图像和文本应用里不太常见,更多出现在图数据、知识图谱等领域。整理笔记时知道有这回事就行,不要被“四大类”吓住,先用好模型迁移,再逐步扩展。
3. 实操:用预训练模型微调完成一次迁移学习
3.1 环境与数据准备
我以“模拟项目X:工业零件表面缺陷分类”为例,这个任务有四个缺陷类别,每类只有三百到五百张图片,总共小两千张。这个数据量,从零训练一个深度网络非常吃力,微调预训练模型则很合适。开发环境就是最常见的组合:Python、PyTorch、一块普通GPU。
数据准备阶段有两件事别偷懒。第一,目录结构按“train/类别名”和“val/类别名”组织好,别把所有图片塞一个文件夹再自己写标签映射,后面调bug会痛苦。第二,预处理必须和目标域实际情况匹配。预训练模型在训练时有自己的一套标准化参数,比如均值、方差,迁移时最好沿用这套参数,否则输入分布一变,之前学到的特征也会被干扰。
数据划分我用70%训练、20%验证、10%测试。验证集很重要,微调过程中判断是否过拟合、是否发生灾难性遗忘,都靠它。测试集只用来做最终评估,平时训练别碰它。
3.2 加载预训练模型并改造分类头
核心代码其实很短:
import torch import torch.nn as nn import torchvision.models as models # 加载在大规模通用图像数据集上预训练过的骨干模型 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) # 查看原来的全连接分类头 print(model.fc) # 输出: Linear(in_features=512, out_features=1000, bias=True) # 替换成目标域需要的分类头 num_classes = 4 model.fc = nn.Sequential( nn.Dropout(0.2), nn.Linear(512, num_classes) )为什么要替换最后一层?因为预训练模型的输出是它在源任务上的类别数,比如一千类,而我们的目标域只有四类,输出维度必须改。很多新手会忘记dropout或者直接用一个裸的全连接层,我建议至少留一个Dropout,尤其是目标域数据量少的时候,它能帮你压一压过拟合。
3.3 选择冻结策略和训练参数
加载完之后,面临一个关键选择:哪些层参与训练?三种做法我都试过。
第一种,冻结所有骨干参数,只训练分类头。适合目标域数据特别少,比如每类只有五十张图。这种方式训练最快,但骨架完全是源域的,可能无法适应目标域特有的特征。
第二种,微调全部层。适合目标域数据量中等偏上,比如每类几百上千张,并且目标域和源域有差异。这时需要一个很小的学习率,否则会把预训练知识一把冲垮。
第三种,冻结前面大部分层,只解冻最后几个残差块和分类头。这是我在工业零件检测里最常用的方案,兼顾了特征复用和任务适配。
用代码控制冻结很简单:
# 冻结骨干所有层 for param in model.parameters(): param.requires_grad = False # 解冻最后两个残差块 for name, param in model.named_parameters(): if "layer4" in name or "layer3" in name: param.requires_grad = True优化器里只传需要更新的参数:
trainable_params = [p for p in model.parameters() if p.requires_grad] optimizer = torch.optim.AdamW(trainable_params, lr=1e-4)学习率我建议分类头给1e-3,骨干层给1e-5到1e-6。如果嫌麻烦,可以给所有训练层一个1e-4,再配合warmup和cosine衰减,效果也够用。batch size一般32,学习率太高和batch太大组合容易让模型震荡。
3.4 完整训练与评估
训练循环用一个最小框架足够:
from torch.utils.data import DataLoader from torchvision import datasets, transforms # 简单的预处理和加载 train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_set = datasets.ImageFolder("train", transform=train_transform) train_loader = DataLoader(train_set, batch_size=32, shuffle=True) for epoch in range(30): model.train() for x, y in train_loader: x, y = x.cuda(), y.cuda() pred = model(x) loss = nn.CrossEntropyLoss()(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() # 每轮结束后跑一次验证集,保存最优模型我要强调一下验证的重要性。每轮训练完,都在验证集上计算准确率,如果连续五轮验证指标不再上升就早停。我做过几十次微调实验,最稳定的时间点通常在10到20轮之间,不需要硬跑几十轮。
下面这张对照表,是我用这个工业零件数据跑出来的典型结果,趋势有普遍性,具体数字依赖数据,别当固定标准:
| 训练方式 | 收敛轮数 | 验证准确率 | 说明 |
|---|---|---|---|
| 从零初始化训练 | 约80轮 | 82%左右 | 数据少,过拟合明显 |
| 冻结骨干,只训分类头 | 约10轮 | 90%左右 | 稳定,但上限受限于骨干 |
| 微调最后两个残差块+分类头 | 约15轮 | 94%左右 | 推荐,平衡效果好 |
| 微调全部层 | 约12轮 | 95%左右 | 效果最好,但要盯住过拟合 |
我在实际项目里首选第三种,因为第四种提升没那么大,却更容易踩到灾难性遗忘的坑。
4. 迁移学习实战中的高频坑和排查实录
4.1 灾难性遗忘:新任务学好了,旧知识丢了
灾难性遗忘的现象很典型:模型在目标域验证集上越来越好,但如果你拿回源域的小测试集去测,发现原先学到的通用能力下降得很厉害。对业务来说,模型变得“偏科”了,泛化能力反而差。
我踩过最深的一次坑,是微调全部层时学习率设成1e-3,跑了不到十轮,目标域准确率确实涨了,但图像增强和旋转等扰动下的鲁棒性明显崩了。后来排查才发现,过大的学习率把预训练好的底层特征大幅改写了。
对策有三个。第一,冻结前面大部分层,只微调高层,这是最直接的防遗忘手段。第二,学习率降到1e-5级别,让参数在预训练权重附近小步移动。第三,用联合训练,在训练目标域的同时,偶尔混入一部分源域数据,让模型两边的记忆都保持更新。现在我自己习惯先固定骨干跑十轮,再解冻部分层低学习率微调,这套流程基本能避免灾难性遗忘。
4.2 源域和目标域差异太大,迁移反而起反作用
负迁移是我最想让新手注意的一个概念。有时候不迁移比迁移更好,迁移了反而更差。我遇到过一个人脸关键点检测任务,直接用了通用图像模型,结果在特定角度下误差比从零训练还大。原因很简单:源域里的物体形态和目标域差异太大,或者源域任务的标签空间和目标域不对齐,模型迁移过来的“经验”根本没参考价值。
怎么判断自己是不是负迁移?我在实验中会用三个信号:第一,加入预训练模型后,验证集指标明显低于从零训练;第二,训练过程中损失下降异常缓慢,收敛不到预期区间;第三,用t-SNE把源域和目标域的特征可视化,发现两者分布彼此分离,根本没有对齐的迹象。
遇到这种情况,先别急着换方法。看看有没有更接近目标域数据的源域模型,比如做医学影像就找医学影像预训练模型,做卫星遥感就找遥感数据预训练模型。如果实在找不到,数据量又足够,那放弃迁移、从零训练反而是一条干净的路。
4.3 数据分布不均衡和标签噪声
迁移学习很少在数据干净、类别均衡的理想条件下生效,真实场景里目标域样本少就算了,类别还不均衡。比如缺陷类别中“划痕”占了七成,“气泡”只有百分之五。直接用交叉熵训练,模型大概率把气泡全部忽略掉,因为全预测成划痕也有很高的准确率。
我的解决办法是,在DataLoader里做类别加权采样,保证每个batch都能看到少数类样本。代码上可以这样:
from torch.utils.data.sampler import WeightedRandomSampler labels = [y for _, y in train_set.samples] class_counts = torch.bincount(torch.tensor(labels)) weights = 1.0 / class_counts[labels].float() sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True)如果标签噪声也大,比如人工标注的缺陷边界不统一,那就要在训练中做标签平滑,把硬标签改成软标签,防止模型在错误标注上死记硬背。我的经验是,目标域数据越少,越要把这些工程细节做扎实。
4.4 适合自己项目的迁移路线选择速查表
整理到最后,我把面向不同数据条件的经验压成了一张速查表,每次开新项目直接查:
| 目标域有标签数据量 | 源域与目标域相关性 | 推荐路线 |
|---|---|---|
| 每类50张以下 | 高 | 冻结骨干,只训练分类头,强正则化 |
| 每类100-1000张 | 中高 | 微调最后几个残差块/Transformer后几层 |
| 每类千张以上 | 中 | 微调全部层,低学习率,配合早停 |
| 每类千张以上 | 低 | 评估从零训练,或多任务预训练 |
| 目标域无标签 | 中 | 无监督域适应,建议先用MMD/域对抗做基线 |
这张表不能替代你自己的实验,但能帮你第一版方案不跑偏。我每次开新任务,都先按这张表定一个大方向,再用小规模数据快速验证,比盲目试方法省时间。
5. 从“还没整理好”到系统化:我自己的内容整理法
5.1 用一棵知识树代替收藏夹
“迁移学习还没整理好”,说到底不是缺资料,而是缺整理方法。收藏夹里的链接、未读完的论文、跑了一半的脚本,放着不管永远是一堆杂物。我现在用的是目录树加笔记的方式:
迁移学习/ ├── 01-基础概念/ │ ├── 领域和任务.md │ └── 迁移学习分类.md ├── 02-方法笔记/ │ ├── 实例迁移.md │ ├── 特征迁移-MMD.md │ └── 模型迁移-微调.md ├── 03-代码模板/ │ ├── 微调脚本.py │ └── 域适应模板.py ├── 04-实验记录/ │ └── 工业零件缺陷分类.xlsx └── 05-踩坑笔记/ ├── 灾难性遗忘.md └── 负迁移.md我要求自己每篇方法笔记必须写五件事:核心思路、适用条件、输入输出、代码片段、踩坑记录。只写“这是什么”那一句话的笔记,回头根本不会再看。有了知识树之后,看似零散的内容就各归其位了。
5.2 把实验模板变成自己的工具箱
整理迁移学习内容,除了整理理论,还要整理代码。我最早是把所有实验脚本堆在一个文件夹里,每改一次参数就另存一份,最后光脚本文件就有几十个,根本分不清哪个对应哪个结果。后来改成所有代码共用一个训练模板,把可变参数集中在一个配置文件里,每次实验只改配置,不直接改逻辑。
实验记录表也很关键。我用最简单的一种表格,每行一次实验:
| 实验编号 | 模型 | 预训练源 | 冻结策略 | 学习率 | 数据量 | 验证准确率 | 备注 |
|---|---|---|---|---|---|---|---|
| 01 | ResNet18 | 通用图像 | 冻结骨干 | 1e-3 | 1600 | 90% | 基线 |
| 02 | ResNet18 | 通用图像 | 微调layer4 | 1e-4 | 1600 | 94% | 推荐 |
连失败的实验也记录。看起来是额外工作量,但当你需要复盘“为什么那版效果好”时,这份记录能救你命。我后来很多结论,都是从失败实验里对照出来的。
5.3 我的几个私藏心得
整理过程中有几条个人体会,写在这里供你参考。
第一,先跑通最小实验再啃理论。我最初花了很长时间读各种迁移学习论文,一知半解;后来用现成模型跑通了一个微调任务,再回头看理论,很多概念自己就通了。先有手感,再有体系。
第二,每个方法先在小规模数据上验证,再上正式场景。小规模数据跑一轮只要几分钟,能快速暴露流程上的问题。一定要先验证流程,再花算力。
第三,可视化特征比盯损失曲线有用。我会在训练结束后,把源域和目标域的特征用t-SNE投影画出来,一眼就能看出分布是否对齐、模型是否把两个域混在一起。这个习惯帮我发现过好几次负迁移。
最后再分享一个小技巧:每次开始新实验前,花十分钟把上一轮实验的模型输出和特征图存下来,标记好对应的配置编号。后面不管写总结、调bug还是做技术分享,这些“现场证据”都是最好的素材。迁移学习的整理,本质上就是给每一份知识、每一组参数、每一次失败都找到一个明确的抽屉。抽屉分好了,知识自然就“整理好”了。