2026/10/13 5:43:34

权值共享与分层学习率:模型训练提速与稳定的实战指南

权值共享与分层学习率:模型训练提速与稳定的实战指南 这两年我陆陆续续跑过图像分类、目标检测、文本分类、自监督预训练好几类项目有个特别明显的体会很多人训练网络时习惯一套配置用到底——所有层统一学习率、所有参数统一惩罚。这套配置在跑小模型、短任务时问题不大但模型一旦变深、数据变大或者要做“预训练微调”收敛速度、最终精度、稳定性都会跑出不小差距。权值共享和不同层赋予不同学习率是我反复调整后沉淀下来的两个实用技能。一个管参数量一个管收敛节奏看起来是两件事但在真正训练时经常要配合使用。这篇文章就把我实际训练中的理解、配置方式和踩坑记录一起整理出来适合正卡在训练不收敛、收敛太慢或者准备系统学习参数分组、权重绑定的朋友参考。1. 别再说“一套默认配置走天下”这两个操作到底在解决什么问题先说一个我常被问到的现象为什么自己从零训练一个网络loss 掉得很慢或者验证集精度一直上不去很多人第一反应是调大学习率结果模型直接震荡。这个坑的根源往往不是学习率大小不对而是所有层用了同一个学习率这件事本身就过于粗糙。1.1 全网络同一个学习率的问题出在哪一次梯度更新可以简化成一句话参数往梯度的反方向走一步步长就是学习率。写成大家都熟悉的形式就是θ_new θ_old - lr * gradient如果所有层共用一个 lr那就等于假设所有参数的梯度量级大致相同。但这个假设在实际网络里几乎不成立。靠近输出的层梯度直接从 loss 传过来量级通常偏大靠近输入的层梯度要穿过很多层非线性变换量级常常偏小。于是同一个 lr 对高层来说可能太大对底层来说可能太小。这就是为什么单纯调大 lr 容易出现“高层震荡、底层还是学不动”的怪现象。你能看到 loss 上下跳动但网络真正该学好的底层特征其实一直没动。反过来把 lr 调小让高层稳定了底层更新就更慢了整个训练变成慢动作。不同层赋予不同学习率本质上就是承认每个层梯度分布不一样让每个层按自己的步长走。1.2 权值共享管的是“参数量”不是“收敛速度”权值共享解决的是另一个维度的问题。它和训练速度没有直接关系和你有多大显存、需要多少数据才能训起来有关系。什么叫权值共享就是让不同位置的参数用同一份权重。最典型的例子是卷积操作一个 3x3 的卷积核在整张图上滑动不管 feature map 是 32x32 还是 224x224卷积核的权重始终是同一份。如果不用卷积而是把每个位置的每个像素都连到下一层那参数量会爆炸到根本无法训练。“共享”这个动作实际上是在告诉模型一个重要先验同一个局部模式在图片的任何位置出现都应该用同样的方式去识别。比如边缘、纹理、角点这些底层特征平移到哪里都还是边缘、纹理、角点。这个先验让模型可以用很少的参数学到泛化性很强的特征同时要求的数据量也大大降低。1.3 什么情况下你会意识到“该用它们了”我个人经验里出现下面几类场景时就该把权值共享和分层学习率拿出来用了从零训练一个比较深的网络发现底层梯度很小训练很久底层特征还没成型用预训练模型做迁移学习直接全量微调发现过拟合特别快或者预训练特征被冲掉模型里存在明显的对称结构比如自编码器、Seq2Seq 里的词嵌入和输出层多阶段训练比如先训主干再训任务头不同阶段的网络部分学习节奏完全不同。先说一个最常见的场景——用预训练模型微调到新任务。这时候预训练层已经有相当成熟的表达能力你应该给它一个小学习率让它在新数据上慢慢“微调”而不是大步流星地把学过的特征忘得精光。新加的任务层完全不同它从随机初始化开始需要相对大的学习率去快速学习目标任务里的专属映射。这就是“不同层赋予不同学习率”最典型的使用场景也是后面会重点展开的部分。2. 权值共享不止卷积三种最常见的形态和实现思路权值共享这个词听起来像是一个固定操作其实它在不同网络结构里长得完全不一样。我把平时用到过的三种典型形态整理一下后面遇到哪种都不陌生。2.1 卷积共享空间上的“一套规则走天下”卷积是最早把权值共享发扬光大的结构。一个输入是 3 通道、224x224 的图如果用全连接接到下一层哪怕下一层只有很小的 1000 维向量也需要几亿个参数。卷积把这个数量级直接压到几千。关键就在于卷积核在空间维度上滑动时是不换参数的。来自不同空间位置的梯度在反向传播时会累加到同一份卷积核参数上所以卷积核每更新一次其实是吸收了所有位置反馈的“平均意见”。这里有个容易被忽略的细节卷积共享的是一份卷积核权重但不同位置的梯度差异依然存在。背景区域和前景区域的梯度幅度不一样累加之后会对卷积核产生不一样的影响力。所以实际训练中我经常在卷积层后面配合归一化手段让不同位置的响应先拉回一个可控的尺度否则参数更新容易被某些高响应区域“带偏”。2.2 循环共享时间上的“同一个决策器”RNN、GRU、LSTM 这类循环结构是另一种共享形态。它们共享的是时间维度的参数——每个时间步读取输入时用的都是同一套状态转移矩阵。这个设计的逻辑在于序列数据里某个模式在时间点 t 出现和在时间点 t10 出现本质上应该被同样处理。比如文本里“苹果”这个词出现在句首还是句尾词义是一样的。如果每个时间步都有自己的独立权重模型既无法处理变长序列也无法把在某个位置学到的模式迁移到别的位置。用 PyTorch 实现时你几乎感觉不到这个过程因为 GRU、LSTM 内部已经做了循环共享。但理解和训练相关时要注意RNN 反向传播会沿时间步展开梯度在时间维度上累计如果序列很长长期依赖的梯度和短期依赖的梯度量级差很远。这时候不同层赋予不同学习率也能派上用场——比如对输入变换矩阵和隐藏状态过渡矩阵分别设不同 lr能一定程度上缓解长期依赖学不进去的问题。2.3 捆绑权重自编码器和词嵌入里的“照镜子”式共享第三种是手动指定两个不同位置的权重共用一份参数通常叫 weight tying中文里也叫捆绑权重或权重绑定。自编码器是典型场景。编码器把输入压缩到隐空间解码器再把隐空间还原成输入。编码器和解码器本质上在做相反的线性变换。与其让它们各自学一套参数不如让解码器直接用编码器权重的转置这样参数量几乎减半而且相当于给模型加了一个很强的正则约束减少过拟合。结合 PyTorch 写个最小示例方便你理解怎么实现import torch import torch.nn as nn import torch.nn.functional as F class TiedLinear(nn.Module): # 用同一个可学习矩阵分别承担编码和解码的角色 def __init__(self, in_features, out_features): super().__init__() self.shared nn.Parameter(torch.randn(out_features, in_features)) def forward(self, x, transposeFalse): w self.shared if not transpose else self.shared.t() return F.linear(x, w) class TiedAutoEncoder(nn.Module): def __init__(self, dim, latent_dim): super().__init__() self.enc TiedLinear(dim, latent_dim) self.dec TiedLinear(latent_dim, dim) def forward(self, x): h F.relu(self.enc(x)) return self.dec(h, transposeTrue)另一个非常经典的捆绑权重场景是 Seq2Seq 模型输入侧的词嵌入矩阵和输出侧的预测投影矩阵共用同一份权重。比如在 PyTorch 里你可以直接把 Linear 层的权重赋值成 Embedding 层的权重self.wte nn.Embedding(vocab_size, d_model) self.lm_head nn.Linear(d_model, vocab_size, biasFalse) self.lm_head.weight self.wte.weight # 绑定权重这样模型在预测下一个词时会复用输入词嵌入学到的那份语义表示。好处很明显词嵌入矩阵往往巨大共享后省了一大块显存而且输出层不会学到和输入表示不一致的“分裂语义”。我实测下来这个操作对数据量不大的文本任务尤其友好过拟合明显减轻。3. 分层学习率底层小步走高层大步跑聊完了参数量再回到收敛节奏。不同层赋予不同学习率到底怎么设设多大很多人拿到手里还是不知道怎么下手。我按自己常用的思路拆开讲。3.1 先想清楚哪些层该小步哪些层该大步我一般分两种情况讨论。第一种是从零训练整个网络。这时候高层靠近 loss梯度大步子要小一点防止震荡底层梯度小学习率可以适当给大一点弥补更新幅度不足。但这里有个反直觉的点——底层特征通常比较通用边缘、纹理、颜色它们不需要频繁大规模更新真正决定任务表现的往往是在中高层形成的组合特征。所以从零训练时我更常用的是“底层小一点高层大一点”的配置保护底层通用特征让高层按任务需求快速调整。第二种是迁移学习和微调。预训练模型已经学到了成熟的特征新任务头部是随机初始化的。这时候配置方向就和从零训练反过来了预训练层基本已经在一个不错的位置学习率必须小否则会灾难性遗忘新初始化的任务层需要快速探索学习率可以给大一些。两个场景一对照就明白了分层学习率的设置核心依据是“这个层当前的状态离最优解有多远”而不是简单的“梯度大就调小”。3.2 用 PyTorch 的 param_groups 给不同层安排不同学习率PyTorch 实现分层学习率非常方便核心就是 optimizer 的 param_groups。你可以把不同模块的参数装进不同的分组每个分组有自己的 lr、weight_decay甚至可以有不同的动量。下面是我微调某个图像分类模型时的典型配置import torch import torch.nn as nn # 假设网络包含一个 backbone 和一个分类头 model torch.hub.load(某预训练模型) # 仅示意实际换成你的模型结构 num_classes 10 model.fc nn.Linear(model.fc.in_features, num_classes) # 替换任务头 backbone_params [] head_params [] for name, param in model.named_parameters(): if fc in name: # 分类头 head_params.append(param) else: # 预训练骨干 backbone_params.append(param) optimizer torch.optim.SGD([ {params: backbone_params, lr: 1e-4, weight_decay: 5e-4}, {params: head_params, lr: 1e-3, weight_decay: 1e-4}, ], momentum0.9)这里 backbone 用 1e-4新分类头用 1e-3差一个数量级。为什么选“差一个数量级”而不是“差两倍”因为预训练特征被破坏的门槛是数量级层面的你给预训练层一个 5e-4 可能还只是“痒”给到 1e-3 以上训练几个 epoch 后重新抽特征分布就开始明显漂移了。新层从随机开始如果 lr 太小它需要很久才能匹配上底层特征已经提供的信息量。数量级的差距是经验值但不是拍脑袋拍的我用不同比例跑过对比10 倍左右通常比 2 到 3 倍效果稳定。3.3 学习率调度器会和分层设置打架吗很多人配好了分层学习率又顺手加了个 CosineAnnealingLR结果发现训练曲线和预期对不上。其实调度器本身不会覆盖掉分组配置。大多数内置调度器比如 StepLR、CosineAnnealingLR、LambdaLR在 PyTorch 里的实现逻辑是遍历 optimizer.param_groups对每个组的当前 lr 施加同样的调度规则。也就是说如果你的分组初始 lr 是 1e-4 和 1e-3调度器会给它们分别乘以同一个衰减因子保持相对比例不变。这个行为整体是合理的。真正需要注意的是 ReduceLROnPlateau它内部只监控一个指标而且对多分组支持得不够直观实际触发时会同步降低所有组的 lr幅度一致。如果你希望微调阶段 head 的大学习率保持不变、只衰减 backbone那就不能用 ReduceLROnPlateau得自己写回调或者改用 LambdaLR 手动控制分组衰减。def lr_lambda_backbone(epoch): return 0.1 ** (epoch // 30) # 每 30 个 epoch 衰减一次 scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambda[lr_lambda_backbone, lambda e: 1.0] # 分别作用于两个分组 )这样你就能让 backbone 按计划退火同时 head 一直保持大步长探索。4. 微调场景下的组合拳冻结配合分层学习率前面把两个技术单独讲清楚了现在组合起来看一套完整配方。这是我在某图像分类项目里真正用过的流程虽然任务数据并不大但思路可以通用到很多迁移学习场景。4.1 先想清楚冻结、微调、全量微调怎么选迁移学习里其实有三个梯度完全冻结、部分冻结、全部微调。完全冻结等于把预训练模型当成特征提取器只训练后面接的线性分类层。好处是训练快、占显存小、几乎不会过拟合坏处是如果预训练任务和目标任务差异很大底层特征不太匹配任务效果会受限。全部微调是让预训练所有层都参与新数据训练。好处是模型能整体自适应坏处是数据不够时过拟合风险高而且训练成本高。我的经验是如果目标数据和预训练数据领域差得远但数据量又不大一般用“冻结大部分骨干只解冻最后几层 训练新任务头”。如果领域比较接近比如都是自然图像那可以全部微调但骨干学习率要压到很小。如果目标数据特别多再考虑骨架全部放开。4.2 一个可落地的微调配置流程以某图像分类任务为例我用的是“先冻结主干训头部再解冻浅层联合微调”的两阶段方案具体步骤长这样第一阶段# 1. 载入预训练权重后先冻结所有骨干层 for name, param in model.named_parameters(): if fc not in name: param.requires_grad False # 2. 只把分类头参数交给优化器 optimizer torch.optim.AdamW(model.fc.parameters(), lr1e-3)这个阶段直接训练分类头。因为 backbone 是冻结的相当于把输入映射到特征空间后再训练一个线性分类器通常几十个 epoch 后头就能达到一个不错的 baseline。这个阶段的目的是让分类头先适应预训练特征分布的“语言”后面解冻骨干时就不会出现头和特征之间互相拖拽。第二阶段# 3. 解冻整个骨干但要按层设置学习率 for name, param in model.named_parameters(): param.requires_grad True optimizer torch.optim.AdamW([ {params: model.features[:6].parameters(), lr: 1e-5}, # 底层尽量别动 {params: model.features[6:].parameters(), lr: 1e-4}, # 中高层微调 {params: model.fc.parameters(), lr: 5e-4}, # 分类头继续学 ], weight_decay1e-4)第二阶段的初始 lr 整体比第一阶段低是因为骨干解冻后整个模型的更新幅度会变大如果维持头部的 1e-3很容易在第一个 epoch 就把 backbone 的原始特征“冲坏”。我记得有一次第二阶段直接用了和第一阶段相同的学习率结果验证集损失在头两个 epoch 断崖式上升后来降到这个配置才稳住。4.3 各层学习率参考范围给一个我平时微调预训练模型时的参考范围方便你快速起步网络部分参考学习率原因底层骨干1e-5 到 5e-5底层特征通用破坏后难恢复中高层骨干5e-5 到 2e-4开始出现任务相关特征适度微调全局池化后新任务头1e-4 到 5e-4随机初始化需要相对大步探索这只是初始值实际训练中要配合曲线调。判断标准是骨干学习率如果导致骨干部分的参数范数快速上升说明步子大了头部学习率如果 loss 一直不掉看看是不是低了一两个数量级。5. 踩坑现场分层学习率与权值共享的常见错误这部分是我最想说的。以下全是真实训练中遇到过或者排查过的问题我按排查链路写出来方便你遇到类似现象时知道从哪里下手。5.1 冻结过头loss 完全不动先别急着调学习率有次我做文本分类微调初始化之后怎么训 loss 都不动。第一反应是 lr 太小调大一个数量级还是不动甚至观察不到任何波动。后来逐层打印 requires_grad 才发现分类头里一层全连接的权重在加载预训练模型时名字匹配错了renormalization 时被意外冻结了。排查建议训练前写一个小脚本打印每个模块参数 is leaf 和 requires_grad再用一个 dummy batch 手动跑 forward 和 backward确认 loss 到每个分组的梯度都不为空。很多人要么完全忘记检查冻结状态要么以为冻结只影响正向传播其实冻结的主要作用是让参数不更新如果该更新的参数被冻结了调多少学习率都没用。5.2 深层 Transformer 结构里的梯度量级失衡Transformer 类结构和普通卷积网络不一样它每一层内部经常有多个子层梯度的量级波动比卷积网络更剧烈。如果统一使用相同 lr某些子层更新过快容易出现训练曲线莫名抖动的现象。我建议对这类结构做分组时把注意力层、前馈层、LayerNorm 层分开处理。LayerNorm 的 gamma、beta 参数适合用和主体相同的 lr但正则项可以设小前馈层参数量大梯度稀疏可以给稍大 lr注意力投影矩阵往往受残余连接影响梯度偏小可以适当补偿。当然如果用了 AdamW 系优化器它会按参数的梯度二阶矩自适应缩放分层 lr 的直接效果会被弱化但依然能给不同模块不同的“初始信任度”。我自己做 Transformer 微调时分层初始 lr 的设置思路仍然有效底层小一点顶层大一点只不过差值从 10 倍缩小到 2 到 3 倍就足够。5.3 共享权重叠加上 BatchNorm一个容易忽略的组合权值共享虽然省参数但和 BatchNorm 放在一起时要多留个心眼。卷积层权值共享后同一份权重要在不同空间位置输出 feature map而 BN 计算的是整个 batch 在所有空间位置上的均值和方差。这个归一化过程会改变共享权重梯度的影响范围。我遇到过一次实际情况在某个轻量网络里加了共享卷积核训练曲线初期非常不稳定验证集忽高忽低。排查后发现共享卷积后面接了 BN不同位置的统计特性差异大BN 的 running stats 一直在震荡。后来我把共享卷积核的 lr 调低同时让 BN 的 momentum 调大一些让统计量尽快稳定训练才恢复正常。所以如果你真的要在自定义结构里做权重共享建议先跑一个不共享的 baseline再逐步引入共享否则出问题很难分清是共享导致的还是 BN 归一化导致的。5.4 调度器把分层设置“覆盖”掉的坑前面提到过 ReduceLROnPlateau 的问题再补充一个实际案例。我之前在某个项目里组了三个 param_group骨干 1e-5、中层 1e-4、头部 1e-3。加了个 ReduceLROnPlateau训练中验证集连续三个 epoch 不涨就降 lr。结果降了一次之后三个组的学习率乘以 0.1头部变成 1e-4和骨干初始值一个量级整个训练节奏彻底乱掉。现在我的习惯是使用多分组学习率时调度器优先选 CosineAnnealingLR 或者 LambdaLR它们对分组是逐组施加规则的不会出现“一刀切”。如果确实要看指标动态降 lr我一般只对其中一个分组执行或者用自定义函数去修改特定 param_group 的 lr而不是交给通用调度器统一处理。再补充一个容易忽略的点如果你手动打印 optimizer.param_groups 里的 lr看到的不一定是调度器实际采用的 lr因为调度器内部维护的是初始 lr 乘以倍率的乘积打印当前 lr 时手动去看 param_groups 是最准的。5.5 共享权重和正则化混用时的参数更新“叠加效应”捆绑权重最大的特点是梯度会从两个位置同时汇入同一份参数。比如自编码器里 encoder 和 decoder 共用矩阵反向传播时两个方向的梯度会相加。如果再加上 weight_decay这份共享参数会被拉向零的速度也翻倍。我踩过类似的坑一个带捆绑权重的自编码器weight_decay 设成 5e-4training loss 一直降不下来后来发现共享参数被正则压得太狠重建输出明显“模糊”。降 weight_decay 到 1e-5 之后loss 才正常。所以只要代码里存在共享权重所有正则项的强度都要相对缩小相当于“同一份参数被两个人同时克制”。6. 一个可复用的训练速查表以及我习惯的检查顺序写到这里把零散的经验归拢一下给出一份我常用的配置速查表还有每次训练模型前会过的检查单。6.1 不同任务下的配置参考任务类型权值共享情况分层策略常用优化器从零训练 CNN卷积天然共享底层 1x中层 0.5x高层 0.3x 基准 lrSGD Momentum微调图像分类卷积天然共享骨干 0.05x分类头 1x 基准 lrAdamW / SGD微调文本模型词嵌入可绑定输出层底层 0.3x上层 1x新增头部 2xAdamW自编码器降维编码解码权重可捆绑两端同源通常一致Adam基准 lr 怎么定从零训练用 SGD 时我一般从 0.01 开始用 AdamW 从 3e-4 左右开始。微调场景基准 lr 通常从 3e-5 到 1e-4再按分组倍数换算。6.2 训练前我应该检查的七个点每换一个新任务我都会按下面的顺序花十分钟检查一遍省的后面出问题还要重新排查打印模型结构确认哪些模块是共享参数的哪些是独立参数检查 requires_grad确认冻结范围没有误伤用一个 dummy batch 手动 backward检查每个分组是否有非空梯度打印 optimizer.param_groups确认每个分组的 lr、weight_decay 是否符合预期确认调度器类型对多分组的兼容性从训练曲线观察每个分组的参数变化如果某个分组参数范数没变化大概率学习率太小或梯度传不到记录初始 lr 和各分组比例方便后续对照实验。6.3 关于这两个技术我最后的几点感受权值共享和不同层赋予不同学习率单独理解都不难但真正用好它们需要对“网络每一层在训练过程中的角色”有个整体判断。权值共享是在结构层面用先验换效率分层学习率是在优化层面按状态定步长。两者配合时你其实是在同时回答两个问题哪些参数该存在哪些参数该快动。以我个人实际训练的感受来说分层学习率的价值在微调任务上体现得最直接它能让预训练知识保留得很好同时让新任务头快速适应本质上是给“旧知识和新任务”分配了不同的学习预算。权值共享的价值则更多体现在模型结构设计阶段它决定了你能用多少参数换多少表达能力以及你的模型需要多少数据才能被充分训练。如果你现在正在做一个迁移学习任务最值得先试的改动就是把统一学习率改成骨干低、头部高的两组配置。如果你正在设计自定义网络优先考虑结构里是否有天然可以共享的对称部分能用共享的尽量别另开参数。做完这两个改动你大概率能感受到训练节奏明显更可控最终效果也更稳定。