首页 / 资讯中心 / 文章详情

预训练权重与迁移学习:为何读、怎么用、坑在哪

预训练权重与迁移学习:为何读、怎么用、坑在哪 ★ FEATURED ARTICLE
有段时间经常被问到同一个问题“训练模型的时候代码里为什么要先读一份预训练权重直接随机初始化从头训练不是更干净吗”问这个问题的多半是刚接触深度学习不久的同学也有些做过几个项目的朋友心里依旧把“加载预训练权重”当成某种不自信的手段。我今天想把这件事彻底讲明白包括它到底是什么、为什么有效、以及加载时那些让人挠头的坑。如果你是刚入门正在纠结“自己训练的模型要不要加载预训练权重”或者你已经在跑实验被各种size mismatch、missing key折磨过又或者你只是好奇迁移学习为什么好使——这篇文章都适合你。1. 预训练权重到底是什么1.1 模型 网络结构 一组“背下来的经验”先拆一个基本概念。任何一个深度学习模型本质上由两部分组成网络结构以及网络里的权重。网络结构回答的是“这个函数长什么样”比如有多少层卷积、每层多少通道、激活函数是什么。它定义了模型的容量和表达能力。而权重回答的是“这个函数具体怎么算”也就是成千上万个浮点数它们决定了输入数据经过层层计算之后最终输出的结果偏向什么。随机初始化相当于给这组权重随便填了一批数值模型是一张白纸。它没有见过任何真实世界的样本所有的知识都得靠接下来的训练数据一点一点喂出来。预训练权重则是另一个模型在某个更大的数据集上训练结束后保存下来的那批浮点数。它记录的不是某个任务的“标准答案”而是模型从海量样本里总结出来的规律和特征。你可以把它理解为一个优秀学长的解题笔记你已经知道怎么识别边缘、纹理、形状、甚至更抽象的模式接手新任务时不需要把这些基础能力重新摸索一遍。所以“训练时读取预训练权重”这句话翻译成人话就是我不打算从零开始教这个模型认世界我让它先继承别人已经学好的通用能力再针对我自己的任务做定向调整。1.2 别和“断点续训”的checkpoint搞混这里要马上澄清一个常见混淆点训练脚本里的“读取权重”有两种完全不同的用途。第一种是读取外部预训练权重目的是迁移学习。比如你用别人在千万级图片上训好的模型参数初始化你自己的模型然后在自己小数据集上继续训练。这就是标题里说的“预训练权重”。第二种是读取自己之前训练中途保存的checkpoint目的是断点续训。比如训练到第50个epoch服务器崩了或者你想接着上次的状态继续跑于是加载上次保存的模型继续训练。这种“读取”跟预训练权重没太大关系它只是为了恢复训练现场而不是为了引入外部知识。很多初学者看到代码里有load_state_dict就开始困惑老师不是说训练是从随机初始化开始吗怎么又加载权重其实只要区分清楚这两类“读取”迷雾就散了一半。我们今天重点讨论的是第一类用别人已经训练好的权重作为自己训练的起点。2. 为什么预训练权重能起作用特征的可迁移性2.1 深度网络低层学到的是通用特征要回答“为什么有用”得先搞清楚深度神经网络到底在学什么。拿图像分类举例。你训练一个卷积网络它前面几层卷积核学习到的往往是通用度极高的特征边缘、色块、角点、纹理方向。中间层开始出现更复杂的组合比如弧线、圆圈、局部纹理。再往后才会逐步出现跟任务强相关的语义特征比如人脸轮廓、车轮形状、某种病灶的形态特征。这意味着什么意味着网络越靠前的层越不依赖具体任务。在几千万张自然图片上训练出来的低层特征放到医疗影像、卫星图像、工业瑕疵检测上同样有效。因为无论是CT片、航空照片还是钢材表面都是由边缘、纹理、颜色渐变这些基本元素构成的。这些基础特征不需要在新任务上重新从零学习它们是可以“继承”的。NLP领域也是同一套逻辑。像BERT这类在大规模文本上预训练的模型早期层学习到的是词法、句法、词性等信息这些语言共有的知识对绝大多数下游任务都有用。你让它直接去分析金融文本、法律文书或者客服对话它可以带着“已经会中文”的基础只需要再学一下行业术语和表达习惯。所以预训练权重本质上是一笔“通用知识资产”。它把人类积累下来的、从海量数据里凝练出的通用特征以数值形式固化下来让你在启动训练的那一瞬间就已经站在一个很高的起点上。2.2 迁移学习不是万能但边界比想象中宽有人会问如果我的任务和预训练任务差别很大呢比如预训练模型是在自然图片上训练的我拿去做显微镜图像识别低层特征还有用吗答案是有用但作用会打折扣。显微镜图像虽然和自然图片差异很大但边缘、纹理、局部形态这些底层视觉特征依然是共通的。真正需要重点调整的是中高层特征因为它更接近具体任务。这种时候常见的做法是加载预训练权重但只让高层参与大幅更新把低层冻结住或者用小学习率微调。反过来如果你的任务底层规律就和预训练数据完全不同比如某些抽象的数据分布、或者你把图像像素随意打乱后的任务那预训练权重可能真的帮不上忙甚至带来负迁移。这种情况较少见但确实存在。所以“预训练权重有没有用”最终要落到“你的任务和预训练任务共享多少底层规律”这个问题上。我个人的经验是绝大多数实际问题任务之间共享的底层规律都比想象中多。图像任务共享视觉基础文本任务共享语言基础甚至跨模态任务也能找到不少底层结构。因此优先考虑使用预训练权重遇到效果不好的情况再分析原因通常是比较稳妥的策略。3. 预训练权重给训练带来的具体收益3.1 收敛速度站在半山腰而不是山脚随机初始化的模型它所有层的参数都是噪声前向传播输出几乎没有任何有效信息反向传播的梯度信号也不稳定。这种情况下模型前期基本是在漫无目的地探索损失下降很慢还容易掉进陡峭的局部极小值。加载预训练权重之后情况完全不同。低层特征已经处于“比较好用”的状态模型在一开始就有一定预测能力损失也处在一个比较低的位置。你不再需要花大量时间去学最基础的表示只需要在这个基础上精细化调整。这就像登山别人已经把你送到半山腰你只需要沿着路继续爬而不是从山脚重新开路。实际体感差距有多大我做过一次图像分类实验同样一个小规模数据集同一个模型结构从随机初始化开始训可能需要几十个epoch才能达到可接受的精度加载预训练权重后往往五到十个epoch就已经超过它。时间成本直接省下一大截。这还只是时间上的收益。更关键的是由于起点更靠近好的解最终收敛到的局部最优通常也更平滑、泛化更好。训练深度学习模型本质上是在一个高维非凸的损失面上寻找一个足够好的极小值点好的初始位置对结果的影响很多时候比后面调学习率、调batch size更重要。3.2 数据效率与泛化小样本时的救命稻草预训练权重最大的价值其实体现在数据量不够的时候。深度学习有个朴素规律数据越多模型越强。但现实中你手里往往只有几千张图、几千条文本这点数据要支撑一个深层的网络从零开始学结果几乎必然是过拟合——训练集上表现很好测试集上立刻露馅。模型靠死记硬背把训练样本记住了根本没有学到可泛化的规律。预训练权重提供了额外的信息源。它让模型在开始训练之前就已经掌握了一整套通用特征。你只需要用少量数据去调整高层或者微调一下所有层就能得到一个泛化能力不错的模型。这变相等于把外部海量数据的信息“压缩”进了初始化参数里弥补了你自己数据集规模不足的短板。当然预训练权重不能完全替代数据。如果你的样本量和任务复杂度相差太远即使有预训练兜底也可能过拟合到这个小数据集上。但把它看作一种先验约束模型被限制在一个合理的参数空间附近不会跑到奇怪的地方去泛化能力自然更有保障。4. 实操加载预训练权重的标准动作4.1 主流程结构对齐、参数导入、分类头替换加载预训练权重核心就三步准备模型结构、导入权重、处理不匹配的层。以某个主流深度学习框架为例使用它的模型库可以直接下载预训练权重import torch import torchvision.models as models # 默认加载预训练权重 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 如果不想加载预训练权重就传 None # model models.resnet50(weightsNone)这里的weights参数就是决定要不要读预训练权重的地方。很多框架都内置了常用预训练模型你只需要指定框架会自动下载并加载。这是最省心的方式因为代码内部已经把结构对齐、权重匹配都处理好了。但实际项目中你通常不会直接用原版模型而是会改造结构。最常见的就是替换分类头比如预训练模型是在1000类ImageNet上训练的你的任务是10分类。模型的最后一个全连接层输出维度就从1000变成了10这样一来最后一层的权重和预训练权重就对不上了。处理办法很简单先创建模型替换最后一层再加载权重时跳过不匹配的层。import torch import torch.nn as nn import torchvision.models as models # 1. 改成自己的分类数量 num_classes 10 model models.resnet50(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) # 2. 加载预训练权重跳过 fc 层因为在结构上已经不一致 checkpoint torch.load(resnet50_weights.pt, map_locationcpu) checkpoint.pop(fc.weight, None) checkpoint.pop(fc.bias, None) model.load_state_dict(checkpoint, strictFalse)关键就是strictFalse。它的意思是允许权重不完全匹配只要名字和形状一致的就加载不一致的就忽略。分类头在加载之后又会被随机初始化但实际上我们往往会单独初始化这一层然后从头训练它。严格模式strictTrue是默认值会强制要求权重文件里的所有参数名和模型里的完全对应否则直接报错。对于我们替换了分类头的情况必须用strictFalse。如果你的框架是类似Keras的接口做法也很直观import tensorflow as tf # 加载预训练模型作为基础不包含分类头 base_model tf.keras.applications.EfficientNetB0( include_topFalse, # 去掉顶部分类层 weightsimagenet, # 加载ImageNet预训练权重 input_shape(224, 224, 3) ) # 拼接自己的分类头 x base_model.output x tf.keras.layers.GlobalAveragePooling2D()(x) x tf.keras.layers.Dense(256, activationrelu)(x) output tf.keras.layers.Dense(num_classes, activationsoftmax)(x) model tf.keras.Model(inputsbase_model.input, outputsoutput)include_topFalse是这里的关键参数它让你把顶部的分类层去掉只保留特征提取部分然后自己拼接新的分类头。分类头是新初始化的而底部的卷积层则带着预训练知识进入新任务。4.2 微调策略冻结、解冻、学习率怎么定权重加载进来之后接下来要决定的是“哪些层要更新哪些层不要动”。这就涉及冻结freeze和解冻unfreeze。最常见的策略是分阶段微调第一阶段冻结所有预训练层只训练新加的分类头。原因是新分类头是随机初始化的梯度很大。如果一开始就让所有层一起更新强烈的梯度信号会迅速破坏预训练好的低层特征导致前面几个epoch损失剧烈震荡最后效果反而很差。先只训练分类头让它在预训练特征的基础上快速达到一个合理水平。第二阶段解冻浅层也就是靠近输入的层以小学习率全局微调。这时候分类头已经有一定判别能力梯度信号比较稳定再用很小的学习率去微调预训练层的参数让低层特征向目标数据的方向做细微调整。在框架里冻结参数操作上就是停止某些层的梯度更新for param in model.parameters(): param.requires_grad False # 先冻结所有层 # 只让新的分类头更新 for param in model.module.fc.parameters(): param.requires_grad True # 训练完分类头之后再解冻全部层做微调 for param in model.parameters(): param.requires_grad True学习率怎么选经验法则是加载了预训练权重的层用较小的学习率比如1e-4、5e-5新加的随机初始化层可以用稍大一点的学习率比如1e-3。原因是已经预训练的层只差一点微调而随机初始化的层需要从头学习步子可以大一些。顺序上不要搞反先冻结训分类头再解冻全部层微调。很多人图省事加载完预训练权重直接就全量微调图像领域里如果你的数据量和任务差异都适中也能得到一个不错的结果。但一旦你的分类头随机初始化带来的梯度波动太大前期训练就会很不稳定。所以项目初期我建议老老实实按照“先冻结后解冻”来能少踩很多坑。5. 加载预训练权重时的坑与排查5.1 key mismatch和size mismatch的排查我看到过最多的报错就是这两种。size mismatch说的是参数形状对不上。比如你的模型某一层是[1000, 2048]权重文件里对应的是[10, 2048]。这种情况最常见的原因就是你改了分类头。遇到这个问题直接把对应层从加载清单里剔除就像上面代码里的pop操作。诊断方法也不难把报错信息里列出的层名打印出来跟模型的state_dict做一个交叉对比你就能清楚看到是哪几层对不上。missing key的意思是权重文件里缺少模型需要的某些键。如果你是拿一个预训练权重去初始化一个比原来更大的模型比如在原有网络上新增了一些分支那新分支的参数就不会出现在权重文件里。解决办法同样是strictFalse然后在初始化后给缺的参数单独做初始化。另一种情况是权重名称不一致。同一个模型不同框架、不同版本的命名规则可能不同。比如某些层叫conv1另一些叫features.conv1。这时候需要手动做一个名字映射把权重文件里的键名转换成当前模型的键名。这个操作比较繁琐但只要你把这些键名当成普通的字典key来重命名其实也没什么技术难度。经验而言加载权重前最好先打印一遍两边的键名确认一下对齐情况。这个动作花不了几分钟但能省下大量排查时间。5.2 加载后效果反而更差怎么定位这是一个很打击人的情况明明加载了预训练权重训练完之后精度还不如随机初始化来的高。首先要明确一个基本判断加载预训练权重不等于直接得到一个好模型。它只是提供了一个好起点后面能不能学好还取决于你的数据质量、训练配置、微调策略。最常见的罪魁祸首是学习率太大。微调阶段的模型起点已经比较好梯度更新量如果太大很容易一步跳出原有优势区域。这时候损失可能先涨后跌最终落到一个比较差的结果。解决方式很简单把微调阶段的学习率降下来降到从零训练学习率的十分之一甚至更低。第二个常见原因是不合理的全量微调。分类头和预训练层的训练进度差异巨大一起从头训容易互相干扰。解决办法就是我上面说的分阶段微调。第三个原因则是任务本身跟预训练任务差异太大。预训练权重带来的负迁移压过了它带来的好初值模型在后续训练中很难跳出一个被错误先验影响的位置。这种情况下你可以尝试少加载一些低层权重或者干脆不加载从零训练。还有一个容易忽略的点数据分布和预处理方式。预训练模型对输入有固定的归一化方式比如ImageNet的mean和std如果测试和训练时预处理不一致模型的输入分布和预训练阶段严重偏离效果会大打折扣。务必检查你的数据预处理代码是否严格复用了预训练模型配套的归一化参数。5.3 什么时候不要加载预训练权重虽然预训练权重很香但它不是万能的有些情形下真的不建议用。第一你的数据量足够大且任务特殊。假如你有上千万张图而且任务的照片风格和预训练数据差异很大从零训练可能更自由模型会自己去发现适合这个数据分布的特征。这时候预训练权重反而可能把模型往“自然图片特征”方向拉限制了它学习全新分布的能力。第二你设计的网络结构并没有对应的预训练权重。自己搭了一个全新的网络结构别人没有训过这时只能从零开始。强行用别的结构的权重做部分初始化也不是不行但技术成本和调试成本都很高性价比低。第三某些特殊模态或者特殊任务。比如图神经网络、强化学习里的策略网络、推荐系统里的深度排序模型预训练权重的定义和使用方式跟CV/NLP差别很大。这类任务里迁移学习的通用规律没有那么强更多还是靠任务数据本身来训练结构。即使有初始化的概念它也不是其他领域那种“大模型预训练小模型微调”的路线。判断标准其实很简单你期望模型继承的“底层能力”是否在预训练数据里有充分体现。如果是就加载如果不是或者不确定可以先做一组小实验对比一下加载和不加载的结果用数据说话。6. 场景与选择几个可以抄作业的判断6.1 一张速查表我把自己工作中常用的判断逻辑整理成一张表不敢说绝对正确但对大多数常规任务都适用。场景建议原因图像分类/检测/分割数据量几万以下加载预训练权重数据不够特征复用能大幅提升泛化图像任务数据量百万级、分布较特殊先做对比实验再决定数据够了预训练权重的约束可能变成束缚NLP文本分类小语料库加载预训练模型语言基础能力复用收益极大自定义全新网络结构从零训练没有现成权重可用优先保证结构验证任务域和预训练域差异巨大像素乱序等不大建议加载底层特征不共享可能出现负迁移算力紧张训练时长受限优先加载预训练权重收敛快能省下大量GPU时间这个表的核心逻辑就是回答“底层特征到底共不共享”这个问题。视觉任务之间共享多语言任务之间共享多跨得远就要小心。6.2 一套稳妥的默认方案如果你暂时没有精力做细致的消融实验我推荐一套比较稳妥的默认配置它在我做过的多个项目里都表现良好。第一步加载与任务最接近的预训练权重。图像任务用ImageNet预训练NLP任务用大规模文本预训练模型。不要贪图复杂选择主流模型库默认带权重的版本能省去很多麻烦。第二步替换分类头并随机初始化新层。确保输入输出维度跟你的任务匹配数据预处理严格使用预训练模型的归一化方式。第三步先冻结所有预训练层只训练新分类头若干epoch直到验证损失不再明显下降。这一步通常很快但价值很大。第四步解冻全部层用较小的学习率比如从零训练的十分之一全局微调。微调过程中建议监控每一层的梯度范数如果某一层梯度异常大说明它还没稳定可以考虑降低学习率或者只解冻部分层。第五步最终模型效果如果还是不够理想再从数据增强、难样本挖掘、模型容量这些角度继续优化。预训练权重只是起点后面的路还得靠你自己一步步走。这套方案不是最优解但它是高成功率的稳妥解。项目初期用它快速建立基线后期再根据情况做精细化调整效率会高很多。聊到这儿预训练权重的原理、收益和坑应该都覆盖到了。我自己刚接触深度学习的时候也犯过错总觉得加载别人训练好的权重不够“纯粹”后来在几个小数据任务上用实测对比才真正服气同样一个模型加载预训练权重之后效果从七十几分到九十几分训练时间还少了一大半。从此以后我的默认动作就是先找合适的预训练权重再谈训练策略。不管你是做图像、文本还是其他领域只要任务里存在可以复用的底层特征这笔“继承”总是划算的。最后提醒一句权重文件下载之后记得校验完整加载之前多打印一遍键名这两步能帮你避开大多数莫名其妙的报错。
阅读完成 · 觉得有帮助?
咨询建站