☰
权值共享与分层学习率:模型训练提速与稳定的实战指南
2026/10/13 5:43:28 网站建设 项目流程

这两年我陆陆续续跑过图像分类、目标检测、文本分类、自监督预训练好几类项目,有个特别明显的体会:很多人训练网络时习惯一套配置用到底——所有层统一学习率、所有参数统一惩罚。这套配置在跑小模型、短任务时问题不大,但模型一旦变深、数据变大,或者要做“预训练+微调”,收敛速度、最终精度、稳定性都会跑出不小差距。

权值共享和不同层赋予不同学习率,是我反复调整后沉淀下来的两个实用技能。一个管参数量,一个管收敛节奏,看起来是两件事,但在真正训练时经常要配合使用。这篇文章就把我实际训练中的理解、配置方式和踩坑记录一起整理出来,适合正卡在训练不收敛、收敛太慢,或者准备系统学习参数分组、权重绑定的朋友参考。

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 出现和在时间点 t+10 出现,本质上应该被同样处理。比如文本里“苹果”这个词出现在句首还是句尾,词义是一样的。如果每个时间步都有自己的独立权重,模型既无法处理变长序列,也无法把在某个位置学到的模式迁移到别的位置。

用 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, transpose=False): 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, transpose=True)

另一个非常经典的捆绑权重场景是 Seq2Seq 模型:输入侧的词嵌入矩阵和输出侧的预测投影矩阵共用同一份权重。比如在 PyTorch 里你可以直接把 Linear 层的权重赋值成 Embedding 层的权重:

self.wte = nn.Embedding(vocab_size, d_model) self.lm_head = nn.Linear(d_model, vocab_size, bias=False) 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}, ], momentum=0.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(), lr=1e-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_decay=1e-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-4,training 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 训练前我应该检查的七个点

每换一个新任务,我都会按下面的顺序花十分钟检查一遍,省的后面出问题还要重新排查:

  1. 打印模型结构,确认哪些模块是共享参数的,哪些是独立参数;
  2. 检查 requires_grad,确认冻结范围没有误伤;
  3. 用一个 dummy batch 手动 backward,检查每个分组是否有非空梯度;
  4. 打印 optimizer.param_groups,确认每个分组的 lr、weight_decay 是否符合预期;
  5. 确认调度器类型对多分组的兼容性;
  6. 从训练曲线观察每个分组的参数变化:如果某个分组参数范数没变化,大概率学习率太小或梯度传不到;
  7. 记录初始 lr 和各分组比例,方便后续对照实验。

6.3 关于这两个技术,我最后的几点感受

权值共享和不同层赋予不同学习率,单独理解都不难,但真正用好它们,需要对“网络每一层在训练过程中的角色”有个整体判断。权值共享是在结构层面用先验换效率,分层学习率是在优化层面按状态定步长。两者配合时,你其实是在同时回答两个问题:哪些参数该存在,哪些参数该快动。

以我个人实际训练的感受来说,分层学习率的价值在微调任务上体现得最直接:它能让预训练知识保留得很好,同时让新任务头快速适应,本质上是给“旧知识和新任务”分配了不同的学习预算。权值共享的价值则更多体现在模型结构设计阶段:它决定了你能用多少参数换多少表达能力,以及你的模型需要多少数据才能被充分训练。

如果你现在正在做一个迁移学习任务,最值得先试的改动就是:把统一学习率改成骨干低、头部高的两组配置。如果你正在设计自定义网络,优先考虑结构里是否有天然可以共享的对称部分,能用共享的尽量别另开参数。做完这两个改动,你大概率能感受到训练节奏明显更可控,最终效果也更稳定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询