
简介这是一份面向迁移学习研究者和入门者的 MATLAB 实现代码包围绕 TCA传递成分分析等经典域适配方法展开同时整理了 JDA、GFK、TKL、DAN、JAN 等流行方法的参考实现与论文清单便于系统梳理迁移学习脉络。压缩包共 23 个文件以 8 个 .m 源码文件为主体另有 3 个 Python 脚本、4 个 Markdown 文档、4 张示意图以及少量配置与数据文件整体仅 921KB轻量而完整。源码涵盖 MyTCA、MyJDA、MyGFK、MyTJM、MyJGSA、MyCORAL 等多种代表性实现便于横向对比不同方法的建模思路并配有数据说明与文档可帮助理解各类域适配方法的核心思路、数学推导与代码细节文档中还整理了知识蒸馏、深度网络特征可转移性等经典论文方便延伸阅读。已有 668 人学习下载适合作为课程作业、论文复现或项目起步的参考资料无论初学者还是进阶者都能从中找到可借鉴的代码和思路。1. 别把迁移学习当成黑匣子这个示例到底解决了什么问题一个示例工程敢在标题里放“惊人”两个字通常是因为它解决了一个非常具体的痛点你手上只有几千张打标图片没有大集群就用一台带普通独显的工作站几十分钟内就能把图像分类准确率从随机的 20% 拉到 85% 以上。这个痛点对应的是深度学习里的冷启动问题迁移学习transfer learning就是针对它设计的。标题里的 AmazingTransferLearning 是一个跑在 MATLAB 环境里的迁移学习示例工程核心思路是加载预训练好的卷积网络保留它已经学会的底层特征只替换最后分类层并做微调。这篇文章会带你把这条链路完整复现一遍选网络、准备数据、替换分类头、设置训练选项、评估再讲清楚调参边界和常见坑。适合两类人有标注数据但算力有限的研究者以及需要快速验证图像分类方案可行性的工程人员。2. 迁移学习为什么能省训练时间预训练网络、特征复用与选型依据迁移学习不是玄学它的依据是卷积网络特征的层次性。浅层卷积核学到的是边缘、纹理、颜色渐变这类基础模式这些模式在医学影像、工业缺陷、卫星图、自然图像之间是通用的越往深层卷积核越依赖具体数据集的语义结构。所以把在 ImageNet 上训好的网络拿过来用前中部特征不用重新学只需要在尾部接一个新的分类头去适配你的任务类别。于是同样用几千张图片训练从头初始化要训几百轮预训练模型往往十几轮就收敛训练时间通常能省一个数量级。这也解释了为什么示例工程叫“惊人”却不神奇它没有改变训练的数学原理只是换了一个好得多的起点。你真正要做的是理解这个预训练起点在哪里结束、新任务从哪里开始然后把两者接得够稳。下面先解决选型问题再给一个最小可跑的流程。2.1 预训练网络选哪种ResNet-50、GoogLeNet 与轻量网络的取舍在 MATLAB 深度学习工具箱里可以直接加载使用的预训练网络有好几个先记住这四个就够用了。网络层数参数量近似输入尺寸典型适用场景ResNet-505025.6M224×224数据量中等、显存充足精度与速度均衡GoogLeNet227M224×224数据量不大或机器性能一般MobileNet-v2533.4M224×224移动端部署、CPU 训练SqueezeNet181.2M227×227显存很紧时用来跑通流程我一般按这样的逻辑选择数据量在 5000 张以下直接上 ResNet-50 或 GoogLeNet前者精度潜力更大后者更容易收敛如果只有一张老显卡或根本没有独显就把 SqueezeNet 当作首跑目标流程通了、确认数据没问题再换回 ResNet-50。这里有个容易写错的细节各网络输入尺寸不一样SqueezeNet 是 227×227不是 224后面构造 augmentedImageDatastore 时尺寸填错会直接报输入不匹配。加载方式统一是net resnet50这种第一次运行会联网下载预训练权重耗时长短取决于你的网络环境这是正常现象。% 加载预训练好的 ResNet-50第一次运行会下载权重 net resnet50; % 转成 layerGraph 才能灵活替换层 lgraph layerGraph(net); % 打开可视化窗口检查每层输出尺寸与层名 analyzeNetwork(lgraph);analyzeNetwork弹出的窗口会列出每一层的名称、输出尺寸、参数量这个窗口对定位“分类层在哪”非常关键。ResNet-50 最后三层通常是全局池化、全连接层 fc1000、以及一个分类输出层后续替换操作必须使用准确的层名字符串所以先运行它比直接翻文档更可靠。2.2 最小流程加载网络、确认输入尺寸与分类层位置在替换分类层之前先确认两件事输入尺寸是多少、最后一层叫什么。以 ResNet-50 为例输入尺寸是 224×224分类层叫ClassificationLayer_fc1000它前面紧挨着fc1000全连接层。换到 GoogLeNet这两个名字就变成了loss3-classifier和output完全对不上所以记网络的层名比记网络结构更实用。% 查看最后三层的信息层名必须从这里复制 disp(lgraph.Layers(end-2:end)); % 查看第一层输入尺寸 inputSize net.Layers(1).InputSize; disp(inputSize);disp输出的结构里Name字段就是要用的层名。复制粘贴而不是手敲可以避免很多拼写错误。确认输入尺寸后后续所有图片都要统一缩放到这个尺寸。请不要尝试去改网络第一层的输入大小预训练权重的卷积统计是基于原始输入尺寸学习的硬改第一层会让底层特征的感受野失配精度会明显下降。3. 完整复现 AmazingTransferLearning从数据目录到训练脚本跑通整个流程只需要四个环节数据按目录组织、按比例划分、替换分类层、训练与评估。这一章给的是能直接抄的脚本每一段后面会解释为什么这么写、参数怎么改。3.1 数据集的目录组织与按比例划分做迁移学习项目我第一步不是写训练脚本而是先把数据目录理顺。推荐的结构是平铺的类别文件夹每个类别一个子目录子目录名就是类别标签。这样的好处是imageDatastore可以直接用foldernames自动生成标签不需要写 CSV 映射文件也不会在代码里硬编码路径与标签的对应关系。% data/all 下是类别子文件夹比如 data/all/cat、data/all/dog imdsAll imageDatastore(data/all, ... IncludeSubfolders, true, ... LabelSource, foldernames); % 按每个类别内部取 80% 作为训练集20% 作为验证集 [imdsTrain, imdsVal] splitEachLabel(imdsAll, 0.8, randomized);splitEachLabel的第一个参数是按类别内部比例划分所以哪怕某个类别只有 50 张图片也会保证 40 张进训练集、10 张进验证集不会出现某个类别整体跑进一个集合的情况。参数randomized必须保留否则datastore按内部顺序取连续段同一个场景连续拍摄的高相似图片会被分到同一侧验证集就失去了参考价值。划分之后可以再跑一下countEachLabel(imdsTrain)看看各类数量是否均衡如果不均衡训练阶段要考虑加权重或数据增强这是后话。3.2 替换分类层先查层名再重新接线替换分类层是整个流程里最容易翻车的一步。翻车原因基本只有一个层名写错。不同预训练网络的分类层叫法完全不同ResNet-50 最后是fc1000和ClassificationLayer_fc1000GoogLeNet 是loss3-classifier和output硬套模板必报错报错内容通常是“找不到名为 xx 的层”。% 用训练集的标签类别数确定新分类头尺寸 numClasses numel(categories(imdsTrain.Labels)); % 构造新的全连接层与分类层 newFC fullyConnectedLayer(numClasses, Name, new_fc); newCL classificationLayer(Name, new_classoutput); % 替换 ResNet-50 的旧全连接层和旧分类层层名必须与上面 disp 的一致 lgraph replaceLayer(lgraph, fc1000, newFC); lgraph replaceLayer(lgraph, ClassificationLayer_fc1000, newCL);replaceLayer会检查输入输出连接是否匹配层名写错会当场报错不会静默跳过。fullyConnectedLayer里也可以顺手设置WeightLearnRateFactor这个参数留到第 5 章讲。还有一种情况如果你是从深网设计器导出的网络结构里可能带有辅助分支或额外输出替换前需要先把辅助分支裁掉否则训练选项里还要配置OutputNetwork之类的东西流程会复杂很多。3.3 一个能跑通的训练脚本与各参数含义训练集和验证集都必须经过augmentedImageDatastore统一缩放这一层的名字里带 “augmented”但它可以只做缩放不做任何增强。验证集在这里保持干净数据增强等跑通基线之后再加。% 训练集与验证集统一缩放到 224x224 augimdsTrain augmentedImageDatastore([224 224], imdsTrain, ... OutputSizeMode, resize); augimdsVal augmentedImageDatastore([224 224], imdsVal, ... OutputSizeMode, resize); % 微调场景下学习率是命门见下方参数说明 options trainingOptions(sgdm, ... InitialLearnRate, 1e-4, ... MiniBatchSize, 16, ... MaxEpochs, 10, ... Shuffle, every-epoch, ... ValidationData, augimdsVal, ... ValidationFrequency, 20, ... Plots, training-progress, ... Verbose, true); netTransfer trainNetwork(augimdsTrain, lgraph, options);InitialLearnRate设为 1e-4 是微调的安全起点不是 1e-2更不是 0.1。从头训练用大学习率没问题但迁移学习的预训练权重已经接近一个良好的局部最优学习率太大会发生灾难性遗忘损失可能在第一个 epoch 短暂下降后立刻发散。MiniBatchSize设为 16 是折中显存不足就降到 8迁移学习里小 batch 常常泛化更好不用太担心收敛变慢。ValidationFrequency是每多少次迭代做一次验证20 意味着一个 epoch 内能看到一两次验证曲线太频繁会拖慢训练。Shuffle保持every-epoch防止模型记住 batch 顺序。3.4 训练后的评估准确率、混淆矩阵、单张预测训练完先看验证集整体表现再看哪些类别互相混淆最后抽单张图片确认置信度。这三步能排除绝大多数的“假成功”——比如准确率看着高但某一个类别完全没学会。% 对整个验证集做预测 YPred classify(netTransfer, augimdsVal); YReal imdsVal.Labels; accuracy mean(YPred YReal); fprintf(验证准确率: %.2f%%\n, accuracy * 100); % 混淆矩阵查看类别间互相混淆的情况 figure; confusionchart(confusionmat(YReal, YPred)); % 单张图片预测 img imread(data/val/cat/021.jpg); img imresize(img, [224 224]); lab predict(netTransfer, img); [~, idx] max(lab); disp(categories(imdsVal.Labels(idx)));classify可以直接接受 datastore 或四维数组但如果只给一个H×W×3的单张图片必须先imresize不然会报维度错误。predict输出的是每个类别的概率向量向量顺序与categories(imdsTrain.Labels)的顺序一致所以取max索引后要用categories再映射回类别名直接输出数字索引是没有意义的。4. MATLAB 迁移学习避坑指南五类高频问题的定位与解决跑通基线容易跑出可用模型难。这一章不是泛泛的注意事项而是我复现这类示例时实际遇到的五个问题按“现象 → 原因 → 解决”拆开写。这些问题原理都不深但 MATLAB 的报错信息和实际原因之间往往隔了好几层第一次遇到会卡很久。4.1 验证准确率一直不涨先怀疑学习率再怀疑验证集增强现象训练 loss 正常下降但验证准确率始终在随机水平附近比如五类任务一直卡在 20% 上下。原因最大嫌疑是InitialLearnRate太大预训练权重在一次大步更新里被推离原来的良好区域损失发散其次是验证集用了带增强的 datastore验证分布被污染曲线一路虚高或者完全不涨还有一个小概率原因是replaceLayer之后训练用的却还是旧网络模型输出维度对应原来的 1000 类。解决先把学习率降到 1e-5 重跑一遍这是排错成本最低的一步。如果还不涨打开验证集datastore构造语句检查有没有RandXTranslation这类增强参数有就去掉。最后训练完打印netTransfer.Layers(end).Name如果还是旧分类层的名字检查一下是不是lgraph被重新赋回了原网络。这三个检查做完基本能覆盖全部常见原因。4.2 输入尺寸不匹配改数据不要改网络现象trainNetwork报Input image size错误或者提示期望输入是[224 224 3]。原因imageDatastore读出的图片是原始尺寸而预训练网络第一层的输入尺寸是写死的。ResNet-50 要求 224×224任何其他尺寸都会报错。解决用augmentedImageDatastore统一缩放而不是去改网络第一层。不少资料会教人重写网络输入层这在迁移学习里是下策输入尺寸变了预训练卷积核的感受野覆盖范围与统计分布就错位了特征提取能力会明显退化。另外如果数据是灰度图读进来是H×W单通道还需要在readFcn里通过repmat复制成三通道否则会在输入层维度检查时报错。4.3 显存溢出与 CPU 训练慢先降 batch再去查 datastore现象GPU 训练中途报Out of memoryCPU 训练一个 epoch 跑几十分钟。原因显存溢出主要是MiniBatchSize太大CPU 慢则有两层原因一是网络本身太大二是原始图片分辨率过高augmentedImageDatastore在 CPU 侧做 resize 的开销全部变成了瓶颈GPU 只能空等。解决先把MiniBatchSize降到 8再看有没有救。如果原始图是 4000×3000 级别不要直接在 datastore 里缩放先写个脚本把所有图片imresize到 512×512 存盘再建 datastore这一步能把 CPU 预处理时间降一个量级。最后还是不行就换 SqueezeNet 跑通验证确认数据与流程没问题再换回大网络。关掉Plots的training-progress也能省一点 CPU训练多轮时绘图开销并不小。4.4 训练 loss 变 NaN三个排查方向现象第一个 epoch 正常第二个 epoch 开始 loss 变成NaN准确率直接归零。原因最常见是学习率过大导致权重迭代越界其次输入图片里本身有NaN或Inf像素读进来一算就崩还有可能是分类层输出维度与标签数不一致numClasses写错比如取标签时没有用categories而导致维度对不上。解决三个方向并行排查。先检查数据img readimage(imdsTrain, 1); any(isnan(img(:))) % 再检查标签是否与你预期的类别数一致 disp(unique(imdsTrain.Labels));如果数据和标签都没问题把优化器换成adam学习率降到 1e-5 再跑。adam的默认动量对学习率不敏感比sgdm更适合用来救急等模型能稳定收敛了再换回sgdm精调。4.5 冻结层之后训练时间没缩短正确理解 LearnRateFactor 的边界现象把骨干层的WeightLearnRateFactor设为 0 后训练时间几乎没有变化。原因LearnRateFactor0只是让优化器跳过该层权重的更新自动微分在前向和反向传播阶段仍然会把梯度算完。很多人以为冻结等于省了前向计算实际省的主要是优化器状态更新那一步计算量大头还在。解决想缩短训练时间优先减MaxEpochs、增大ValidationFrequency、减小输入尺寸或换轻量网络。冻结的真正价值不是提速而是防止随机初始化的新分类头在反向传播中把预训练权重冲乱。如果你的目标只是省时间不要去纠结冻结直接换小网络。5. 把“能跑”调成“能用”分层学习率、两阶段微调与数据增强参数基线准确率只能证明流程通了。真实项目里准确率往往会卡在 85% 附近再往上不去了。这时候不需要堆 epoch而是要从学习率结构和数据分布上下功夫。这一章的三个手段是迁移学习里最常用的提精度组合拳。5.1 分层学习率骨架层小步走分类头大步跑把整个网络用同一个学习率微调表面省事实际会陷入尴尬学习率小了新分类头学得慢学习率大了底层预训练特征被扰动。分层学习率的思路很简单靠近输入的特征通用性最强让它们小步更新甚至不动靠近输出的高层语义与新任务最相关让它们大步学习。% 拿到当前所有层对象 layers lgraph.Layers; for i 1:numel(layers) thisLayer layers(i); % 只处理卷积层全连接层保持默认学习率因子 1 if isa(thisLayer, nnet.cnn.layer.Convolution2DLayer) % 越靠近输入的层这个因子可以设得越小比如 0.01 thisLayer.WeightLearnRateFactor 0.1; thisLayer.BiasLearnRateFactor 0.1; lgraph replaceLayer(lgraph, thisLayer.Name, thisLayer); end end这里的关键是将修改后的层对象通过replaceLayer写回lgraph直接改lgraph.Layers的返回值不会生效因为layerGraph内部是独立存储层对象的。如果只想精细控制某一层比如把res5之前设成 0.01、res5设成 0.1、新 FC 设成 1那就把循环里的判断条件换成按Name前缀匹配常见做法是先跑一版全 0.1 的根据验证曲线再决定要不要细分。5.2 两阶段微调先训新分类头再解冻最后一个残差块两阶段微调是我在数据量特别少时固定使用的一套流程第一阶段冻结骨干只让随机初始化的新分类头收敛第二阶段解冻靠近输出的最后几个残差块用很小的学习率精调。顺序不能反过来因为新分类头随机初始化产生的梯度幅度很大如果一开始就带着骨干一起更新预训练权重会被冲乱后面再怎么精调都很难回来。% 阶段一冻结骨干只训练新分类头 layers lgraph.Layers; for i 1:numel(layers) thisLayer layers(i); if ~strcmp(thisLayer.Name, new_fc) ~strcmp(thisLayer.Name, new_classoutput) % 有学习率因子属性的层全部冻结 if isprop(thisLayer, WeightLearnRateFactor) thisLayer.WeightLearnRateFactor 0; thisLayer.BiasLearnRateFactor 0; lgraph replaceLayer(lgraph, thisLayer.Name, thisLayer); end end end options1 trainingOptions(sgdm, ... InitialLearnRate, 3e-4, ... % 新分类头是随机初始化的可以稍大 MiniBatchSize, 16, ... MaxEpochs, 15, ... ValidationData, augimdsVal, ... ValidationFrequency, 30); net1 trainNetwork(augimdsTrain, lgraph, options1);第二阶段只在最后一段卷积上精调% 阶段二解冻 res5 之后的层用极小学习率精调 layers net1.Layers; for i 1:numel(layers) thisLayer layers(i); if isprop(thisLayer, WeightLearnRateFactor) % 以 ResNet-50 为例ps输出前两个字符判断是否属于末尾段 if startsWith(thisLayer.Name, res5) || startsWith(thisLayer.Name, bn5) thisLayer.WeightLearnRateFactor 1; thisLayer.BiasLearnRateFactor 1; lgraph replaceLayer(lgraph, thisLayer.Name, thisLayer); end end end options2 trainingOptions(sgdm, ... InitialLearnRate, 1e-5, ... MiniBatchSize, 16, ... MaxEpochs, 10, ... ValidationData, augimdsVal); netTransfer trainNetwork(augimdsTrain, lgraph, options2);两个阶段的参数差别可以对照看阶段冻结范围InitialLearnRateMaxEpochs目的阶段一除 new_fc 外全部冻结3e-415让随机分类头收敛阶段二仅解冻 res5 之后1e-510精调高层语义特征注意startsWith(thisLayer.Name, res5)是 ResNet-50 的命名规则换网络必须重新查层名GoogLeNet 没有res5所以要对着disp(lgraph.Layers)找尾部卷积层的真实前缀。5.3 数据增强参数四个旋钮怎么定才不破坏语义数据增强不是开得越猛越好它有很强的任务相关性。我通常从最小的一档开始跑一个 epoch 看验证集与训练集准确率差距再决定要不要加码。% 训练集启用轻度增强 augimdsTrain augmentedImageDatastore([224 224], imdsTrain, ... RandXTranslation, [-10 10], ... RandYTranslation, [-10 10], ... RandRotation, [-15 15], ... RandXScale, [0.9 1.1], ... RandXReflection, true); % 验证集永远不增强 augimdsVal augmentedImageDatastore([224 224], imdsVal, ... OutputSizeMode, resize);参数边界这里直接给结论RandRotation15 度对日常物体分类足够但如果你做的是车牌识别、字符识别旋转超过 10 度会改变字符的语义要关掉或收紧到 5 度以内。RandXReflection水平翻转不是所有任务都适用区分左右手、识别朝向的任务开了翻转等于把标签改错。RandXScale0.9 到 1.1 是安全范围缩放太猛会把小目标变得过小。验证集与训练集准确率差距超过 5 个百分点说明增强不够或数据量太少如果训练集被增强拖到涨不上去那就是增强过头了。提示验证集必须使用不带任何增强的 datastore否则验证曲线会“虚高”调参时你会误以为模型已经很好实际一到现场就露馅。6. 用类激活热力图给迁移学习做一次体检训练结束只是完成了一半。我习惯在每次迁移学习训练后做一次“体检”用类激活热力图看看模型到底在看哪里。MATLAB 深度学习工具箱提供了gradCAM功能它计算输入图像各个位置对最终分类决策的贡献强度把贡献度叠加显示在原图上能直观暴露模型是不是在偷懒。% 挑一张验证集图片用真实标签作为目标 img readimage(imdsVal, 5); trueLabel imdsVal.Labels(5); % 在最后一个卷积块的输出上计算热力图 map gradCAM(netTransfer, img, trueLabel, Layer, res5c_relu); % 原图与热力图半透明叠加 figure; imshow(img); hold on; imagesc(map, AlphaData, 0.5); colormap(jet); colorbar;Layer参数指定为最后一个卷积块的输出效果比直接接在分类层上更稳定因为分类层之后是全局平均池化空间信息已经被压缩热力图分辨率会很低。gradCAM返回的热力图尺寸与输入图片一致可以直接叠加显示。期望的形态是热力图集中在这个类别的典型部位上而不是贴在背景或某个固定角落。热力图的分布还能反过来指导数据清洗。如果热力图一直聚焦在背景、边缘或画面的固定位置通常说明数据集里存在系统性偏差比如训练样本里的目标总出现在同一位置、背景高度相似。有次我做一个类似的缺陷分类任务验证准确率到了 96%投入实际场景却持续漏检后来把每张图的热力图翻出来一看模型关注的全都集中在样本右下角的固定反光区根本原因是训练时标注框把缺陷全部裁到了画面正中心。之后我把裁剪范围扩大、加入随机平移问题才真正解决。这件事之后我给自己立了一个规矩准确率达标不算完先看热力图再谈上线。希望帮到你。本文还有配套的精品资源点击获取