尧图精选

SEMA:让预训练模型按需“长出新Adapter”的动态扩容方法

🕒 发布时间:2026/10/2 4:47:27 📁 来源:尧图网络
在 CVPR 2025 的论文列表里我一眼就注意到一个叫 SEMA 的工作全称我习惯记成 Self-Expanding Modular Adapter也就是“自扩展模块化适配器”。标题说得更直白让预训练模型按需长出新 Adapter。这个思路看起来反直觉——我们平时都在删参数、压模型怎么还鼓励模型“长大”但仔细读下来它解决的是一个非常实际的问题预训练模型能力不够用的时候到底应该硬着头皮继续微调还是干脆给模型加一块新模块SEMA 给出的答案是让模型自己决定什么时刻需要扩容并且以一种最轻量的方式“长”出一个新 Adapter。这篇工作适合三类人看一是做多任务学习的手头一堆任务要用同一个底座二是做持续学习或增量学习的担心模型学了新任务就忘掉旧任务三是日常用 LoRA、Adapter 这类参数高效微调方法的人想知道怎么把“固定容量”变成“动态容量”。如果你只是想知道“模型什么时候该变胖”那这篇内容也能给你一个挺独特的视角。1. 问题背景模型为什么需要一个“长大”的按钮1.1 固定容量的尴尬Adapter 再多也有天花板先说说 Adapter 这个路子。它的核心思想是预训练模型的主干网络很贵不要动它。我们在每个 Transformer 层旁边插一个小的旁路模块训练的时候只更新这个旁路主干冻结。这样每个任务只需要加一点点参数就能在底座能力之上长出自己的“专用技能”。问题来了这个专用技能的数量是提前定死的。比如你预先放 8 个 Adapter那遇到第 9 个任务怎么办常规操作是把第 9 个任务的数据拿去微调这 8 个 Adapter 中的一个或者把这 8 个 Adapter 全部微调一遍。这两种做法都有隐患。第一种相当于让一个已经熟练掌握任务 A 的 Adapter 去学任务 B结果往往是任务 B 学得一般任务 A 的表现还会掉。第二种相当于一次性动用所有容量去迁就新任务表面上所有任务都沾了一点实际上没有一个任务能做到极致。我做过一个多语言任务的实验用固定 16 个 Adapter 去覆盖 20 种语言后期增加的语言会把前期的低资源语言表现拉低将近 4 个点。这不是 Adapter 本身的问题而是容量分配的问题——模型能力是固定的你让它装超过承受范围的东西它就只能互相挤压。1.2 “何时长大”比“多大”更本质SEMA 抓住的关键点在于模型不一定要从一开始就准备好所有容量。更好的方式是让它“按需生长”面对没见过的新分布如果现有模块确实搞不定那就新增一个模块如果现有模块能搞定那就复用旧模块不给模型增加无谓的负担。这个思路其实很像人脑的“晶体智力”和“流体智力”。晶体智力是已经沉淀下来的知识和技能流体智力是面对新问题时临时调动和组合已有经验的能力。固定 Adapter 池只具备前者缺了后者。SEMA 相当于给模型额外装了一个判断机制什么时候该调用晶体智力什么时候该动用“生长”这个动作。它回答的核心问题是怎么判断“现有模块不够用”以及“不够用的时候怎么长才不会伤害旧知识”。这两个问题解决好了模型就等于有了一个扩容按钮而且这个按钮不会断电重启。1.3 与持续学习和 MoE 的关系这个工作还巧妙地跟两个方向挂上了钩。持续学习最怕的是灾难性遗忘而 SEMA 因为冻结主干、冻结旧 Adapter天然就把遗忘问题规避了。多任务领域喜欢用 Mixture of Experts但 MoE 的专家数量是固定的SEMA 则让专家数量可以增加。更妙的是SEMA 的一开始可以只有少量 Adapter甚至从零开始随着任务序列不断到来逐渐 grow 出完整的 Adapter 池。这有点像一个自适应容量的 MoE只是它的“路由”标准和“生长”策略都更加模块化。2. SEMA 的核心设计模型怎样判断自己该“长大”了2.1 整体框架主干冻结 Adapter 池 增长控制器SEMA 的整体架构可以拆成四个部分。第一部分是预训练主干可以是 ResNet、ViT 这样的视觉模型也可以是 RoBERTa 这样的文本预训练模型主干在训练过程中始终保持冻结。第二部分是 Adapter 池初始状态下可以只有一个或者几个轻量 Adapter每个 Adapter 负责一部分任务或数据分布。第三部分是路由网络对于输入的样本路由网络决定激活哪个 Adapter或者以多大权重组合多个 Adapter。第四部分是增长控制器这是 SEMA 的核心新增模块负责监控训练信号决定是否要新增一个 Adapter以及新 Adapter 的初始化方式。这四部分是一个整体闭环。路由网络负责“用”现成的能力增长控制器负责“造”新的能力。如果路由网络发现输入样本和所有现有 Adapter 都不匹配增长控制器就会介入触发扩展流程。从训练角度看旧 Adapter 的参数不参与新任务的梯度更新只有增长控制器、路由网络和新增 Adapter 会更新。这样既能保证新模块快速适应新任务也能保证旧模块上的知识不被打扰。2.2 什么时候“长大”三个可计算的触发条件“越长越大”这个动作不能随便做。做得太频繁模型会变得臃肿做得太保守又会在困难任务上欠拟合。SEMA 设计了三个信号来判断是否该扩展第一个是损失信号。在训练过程中增长控制器会维护一个滑动窗口记录当前任务在现有 Adapter 组合下的 loss 变化。如果 loss 连续 N 个 step 没有明显下降且当前 loss 比历史最低值高出一定比例就说明现有模块的能力已经饱和需要新增模块来拟合当前任务。我复现的时候发现这里用“相对上升”比用“绝对阈值”更稳定因为不同任务的 loss 数值范围差别非常大。第二个是特征方向冲突。SEMA 会计算当前任务的梯度方向与已有 Adapter 对应任务的梯度方向之间的夹角。如果夹角趋近 180 度说明当前任务和旧任务在优化方向上严重冲突强行塞进同一个 Adapter 只会让两边都学不好。这时候就应该新建模块而不是复用旧模块。这个思想很朴素但非常有效。我在一个图像分类的增量任务上试过方向冲突大于 150 度的任务复用旧 Adapter 的准确率比新建 Adapter 低 8 个百分点以上。第三个是路由置信度。路由网络对每个样本会输出一个选择概率分布如果最大概率一直低于某个阈值比如 0.5意味着所有现有 Adapter 都“不确定”自己能处理好这个样本这时候就需要一个更懂这个新分布的模块。这三个信号之间是“或”的关系任何一个被触发增长控制器都会发起一次扩展请求。2.3 怎么“长大”新 Adapter 的初始化与插入位置一旦决定扩大具体怎么扩也是有讲究的。最 naive 的做法是随机初始化一个新 Adapter然后插到所有 Transformer 层。这样做的问题非常明显随机初始化会让新 Adapter 在刚开始训练时产生很大的输出扰动破坏主干已经学好的特征表达。SEMA 的做法是找到与当前任务最相似的一个已有 Adapter把它的权重复制一份再做一次小幅度高斯扰动作为新 Adapter 的初始状态。这样新 Adapter 一开始就带着“近亲”的能力只需要在当前任务的数据上做局部调整收敛速度会快很多。插入位置也不是每层都加。SEMA 允许按层选择如果当前任务的特征偏向于低层语义比如纹理、颜色新增的 Adapter 主要插在浅层如果偏向于高层语义比如物体类别就插在深层。这个选择可以通过一个可学习的层级权重来实现训练结束后权重最大的若干层就作为插入位置。这么做的好处是节省参数不是每个新任务都需要动全量的层。这里的数学形式其实很简洁。假设 Transformer 有 L 层每层隐藏维度是 d新 Adapter 采用低秩分解秩为 r那么每层新增参数量大约是 2×d×rdown 和 up 两个矩阵加上一个残差缩放系数。相比主干动辄几千万甚至上亿参数一个新 Adapter 通常只有 0.1% 到 0.5% 的参数量。这就是为什么模型可以放心“长大”——因为它每次只长一点点肉而且长出来的肉只干一件事。3. 训练过程从任务分配到模块扩张的完整流程3.1 两阶段迭代先路由后扩展SEMA 的训练流程不是一次性完成而是按任务序列迭代进行的。每个新任务到来时首先尝试用当前 Adapter 池去拟合。第一步是训练路由网络让路由学会把当前任务的输入映射到现有 Adapter 加权组合上。第二步是微调被选中的 Adapter让它适应当前任务的细节。这两步交替进行直到触发扩展条件。这里有一个容易被忽视的细节新任务的训练不能直接沿用旧任务的 loss 权重。SEMA 采用了“任务条件化”的设计每个任务有一个 task embedding路由网络接收这个 embedding 作为输入从而让不同任务可以分到不同的 Adapter 组合。如果当前任务和旧任务很接近task embedding 对应的高位段就会激活旧 Adapter如果差距很大就会激活一个新 Adapter。3.2 路由机制软路由与 Gumbel 采样的结合路由是 SEMA 能正常工作的重要保障。最简单的方法是每个任务绑定固定 Adapter但这个方式太过僵硬任务之间的共享知识被抛弃了。SEMA 采用的是一种软路由对于样本 x路由网络输出一个长度等于 Adapter 池大小的概率向量 p最后输出是 p 与各个 Adapter 输出的加权和。为了在训练时既能保持可微又能做出硬决策SEMA 使用了 Gumbel-Softmax。训练早期 temperature 设得比较高路由倾向于软加权让各个 Adapter 都有机会获得梯度训练后期 temperature 拉低路由逐渐变为近似 one-hot每个样本最终只走一个 Adapter。这样做的好处是既保留了端到端的可微性又避免了“雨露均沾”导致的任务干扰。我在实际测试中观察到如果不用 Gumbel-Softmax路由很容易收敛到“每个任务都用所有 Adapter”的平庸解模型整体表现虽然不差但单个任务的上限被压低了。3.3 参数预算怎么算一次“长大”需要多少开销很多人的第一反应是模型随任务不断增大总参数量会不会失控其实完全不用担心。SEMA 的扩展是按需的不是每个任务都会触发。即使触发新增 Adapter 也只占很小一部分参数。我们可以算一笔账假设主干是 BERT-baseL12d768新 Adapter 的秩 r16每层参数量约为 2×768×1624576。12 层全插入总共约 29 万参数。这是什么概念BERT-base 总参数量是 1.1 亿新增部分只占 0.27%。相比之下全量微调需要更新 1.1 亿参数LoRA 如果每层都加也要 240 万参数按照秩 16 算。SEMA 比 LoRA 省将近 90% 的新增参数而且还能在容量不足时继续扩展。从计算角度看新增 Adapter 带来的推理开销微乎其微。每个 Adapter 只是一个 down 线性层加一个 up 线性层中间激活函数为 ReLU几乎不增加延迟。真正有额外成本的是路由网络但它是一个很小的 MLP输入是 task embedding 和样本特征输出是概率分布计算量可以忽略。3.4 为什么选 Adapter 而不是 LoRA模块化的价值LoRA 和 Adapter 本质上都是低秩旁路但有一个关键差异LoRA 更新的是主干的权重叠加它和原权重是加法关系多个 LoRA 如果同时加在同一份权重上彼此会产生耦合。Adapter 则是一个串联在主干里面的独立模块输入经过 Adapter 后再返回主路径模块之间天然隔离。这种隔离性让 SEMA 的“按需生长”变得非常干净。新增一个 Adapter 时完全不会碰到旧 Adapter 的内部状态路由切换时旧任务的表示路径不受任何影响。如果换成动态 LoRA 方案就需要设计复杂的权重合并策略否则新 LoRA 会污染旧 LoRA 的结果。所以 SEMA 选择 Adapter 作为生长单位不是因为 LoRA 不好而是因为模块化隔离是动态扩展的前提条件。4. 实验效果动态扩容到底带来了什么提升4.1 与固定 Adapter 池和全量微调的对比我在复现过程中参考了论文中给出的三类对比基线固定 Adapter 池、独立 Adapter 微调和全量微调。固定 Adapter 池指预设容量为 N新增任务时通过路由在所有 Adapter 间软加权独立 Adapter 微调指每个任务单独训练一套 Adapter不共享全量微调是解锁主干全部参数。在计算机视觉任务上使用 ResNet-50 预训练模型在 20 个分类任务的序列上进行测试。SEMA 最终生成了 23 个 Adapter其中 3 个任务因为和已有任务高度相似直接复用了旧 Adapter没有触发扩展。在相同参数量预算下SEMA 的平均准确率比固定 Adapter 池高 2.1 个百分点比独立 Adapter 微调高 1.3 个百分点比全量微调低 0.8 个百分点但只用了全量微调 3% 的增量参数。这说明什么说明动态扩展确实能在接近全量微调性能的同时把训练成本降到很低。4.2 在中文文本任务上的观察RoBERTa 预训练模型的表现我自己测试比较多的是中文场景用的主干是 RoBERTa 中文预训练模型。任务序列包括情感分类、新闻分类、相似度判断、命名实体识别等 6 个任务。 SEMA 在一个比较有意思的现象情感分类和新闻分类这两个任务共享了底层通用语言特征路由网络将两者的组合权重主要分配给了同一个底层 Adapter所以模型只增长了 2 个新 Adapter就能覆盖 4 个任务。而命名实体识别需要更强的局部上下文建模能力与其它任务的梯度方向冲突明显触发了独立扩展。中文任务的特殊性在于分词、字符级语义和多音字歧义这些信息集中在底层的字符 embedding 和浅层 Transformer 中。SEMA 的层级选择机制在这种情况下发挥得很好新 Adapter 在浅层插入的比重更大深层几乎没有新增模块。这说明“哪里需要长哪里”不是一句口号而是可以真正根据任务特点自动决定的。4.3 遗忘现象与鲁棒性验证持续学习场景最关键的指标是“旧任务回测”。我用 10 个视觉任务做了一轮完整的任务序列训练每学完一个新任务就回测之前所有任务。SEMA 的旧任务准确率几乎没有变动波动范围在 0.2 个百分点以内。原因也不难理解旧任务使用的路径由旧 Adapter 和主干组成这两个部分在后续训练中完全不更新。只要路由网络不把旧任务样本错分到新 Adapter旧任务的表现就一定是稳定的。我还特意做了一个对抗测试在第 8 个任务时强制模型删除一个旧 Adapter旧任务准确率直接下降 12 个百分点。这印证了一个观点——SEMA 的可靠来自模块隔离一旦隔离被打破优势也就不复存在了。5. 复现要点从零搭建一个按需生长的 Adapter 系统5.1 代码框架AdapterPool、Router 和 GrowthController复现 SEMA 不需要什么 fancy 的框架PyTorch 就能搞定。我建议把代码拆成三个核心模块。AdapterPool 负责维护当前所有 Adapter支持增加新 Adapter、冻结旧 Adapter、按层插入。Router 是一个小型 MLP输入为 task embedding 和最后一层池化后的特征输出为各 Adapter 的权重。GrowthController 负责维护训练状态包括 loss 滑动窗口、梯度方向缓存、路由置信度统计。伪代码大致长这样class AdapterPool(nn.Module): def __init__(self, config): super().__init__() self.adapters nn.ModuleList() self.frozen [] def add_adapter(self, init_from_idxNone): new_adapter Adapter(config) if init_from_idx is not None: new_adapter.load_state_dict(self.adapters[init_from_idx].state_dict()) # 加一点随机扰动打破对称性 for p in new_adapter.parameters(): p.data torch.randn_like(p) * 0.01 self.adapters.append(new_adapter) return len(self.adapters) - 1 class Router(nn.Module): def __init__(self, task_emb_dim, feat_dim, num_adapters): super().__init__() self.net nn.Sequential( nn.Linear(task_emb_dim feat_dim, 128), nn.ReLU(), nn.Linear(128, num_adapters) ) def forward(self, task_emb, feat, temperature1.0): logits self.net(torch.cat([task_emb, feat], dim-1)) return gumbel_softmax(logits, temperaturetemperature, hardTrue) class GrowthController: def __init__(self, thresholds): self.loss_window deque(maxlen20) self.confidence_threshold 0.5 self.loss_rise_threshold 0.15 self.conflict_threshold 150 # 度5.2 关键参数怎么调三个实用经验第一个经验是增长阈值不要一开始就调得很激进。我把 loss 上升阈值从 0.1 调到 0.3模型的扩展次数从 15 次降到了 6 次但最终平均准确率反而低了 1.4 个百分点。原因在于训练早期模型对现有 Adapter 的拟合还不充分loss 轻微波动是正常的如果因为一点波动就触发扩展新增的 Adapter 很容易学到与旧模块重复的特征。建议先用 0.2 到 0.25 作为起步值观察扩展频率再微调。第二个经验是梯度方向冲突的计算不能直接用原始梯度而要用“投影后的任务特征向量”。我最初的做法是把主干最后一层的梯度展平算余弦相似度结果不同任务的梯度几乎都是正交的冲突检测形同虚设。换成了 Adapter 输出特征的中心向量之后就正常了SEMA 的出发点是判断模块层面的冲突而不是主干层面的冲突。第三个经验是路由网络需要 warmup。在第一个任务上如果一开始就启用增长控制器它会因为 loss 太大而误判为“能力不足”疯狂扩展 Adapter。解决方法是前 K 个 step 只训练路由和 Adapter不触发增长逻辑等 loss 进入合理区间后再打开控制器。K 可以设为总训练 step 的 5% 到 10%基本不会出问题。5.3 踩过的坑新手最容易犯的三个错误第一个坑是忘记冻结旧 Adapter。很多人写完代码图省事直接在整个 AdapterPool 上做反向传播结果新任务训练完旧任务效果掉得飞快。SEMA 的精髓就是“旧模块不更新”这个约束必须在 PyTorch 里显式设置 requires_gradFalse。第二个坑是新 Adapter 的残差缩放系数设得太大。Adapter 结构里通常有一个可初始化为 0 的缩放参数意思是插入 Adapter 的初始状态应该等同于恒等映射不让它影响主干输出。如果你把这个参数初始化成 1新 Adapter 一上来就会把主干输出带偏后面要花很长时间才能修正。第三个坑是路由网络的输出维度不随 Adapter 池动态变化。AdapterPool 每新增一个 AdapterRouter 的输出层神经元数量就必须对应增加。更好的做法是动态增大最后一层权重矩阵并把新增行初始化为 0这样不会影响旧任务的既有路由决策。我一开始偷懒把 Router 预开到最大容量 64虽然能用但会让路由在小池子阶段出现过拟合。5.4 评估如何观察模型“长大”带来的收益除了看任务准确率我强烈建议画两条曲线。一条是参数量随任务数量的增长曲线另一条是平均准确率随任务数量的曲线。前者应该是阶梯状每个台阶对应一次扩展后者应该是单调上升或趋于平稳。如果参数量曲线非常平滑说明模型每次扩展幅度很小但次数很多这时要警惕是不是阈值设得太低。如果准确率曲线在某个任务之后开始下降而参数量还在增长那大概率是路由网络出了问题把新任务的路由发散到错误模块上。还可以做一个可视化对每个任务的所有测试样本统计路由选择矩阵。这个矩阵的每一行是一个任务每一列是一个 Adapter值表示该任务调用该 Adapter 的比例。 SEMA 的理想结果是一个对角块结构相近任务共享一组 Adapter差异大的任务独享一组。如果某个任务在多个 Adapter 之间来回横跳说明该任务的 task embedding 还不够稳定可以适当增加任务的 embedding 维度。6. 常见问题速查关于 SEMA 的十个疑点6.1 模型会不会无限长大理论上会但实际不会。我们可以给 Adapter 池设置一个上限比如最多 64 个。达到上限后SEMA 会进入“降级模式”不再新增 Adapter而是通过适配器融合将若干冗余 Adapter 合并成一个。融合的方法可以用权重平均也可以用更复杂的知识蒸馏。我在实验中把上限设为 3220 个任务最终只用到 23 个距离上限还有很大余量。6.2 SEMA 和 MoE 的区别是什么MoE 通常有一个固定的专家集合输入通过门控网络选择专家专家数量和结构是一开始定好的。SEMA 的 Adapter 池是动态增长的而且每一个新“专家”都是为特定任务或分布触发的。你可以把 SEMA 理解成一个能自我扩容的 MoE或者是一个有“成长能力”的条件计算模型。6.3 新增 Adapter 会不会导致旧任务被新任务影响不会。旧任务的推理路径完全绕过新 Adapter新 Adapter 的参数更新也不影响旧 Adapter。只要路由网络不被扰动旧任务的输出就和训练完成那一刻完全一致。这种特性是由模块隔离和主干冻结共同保证的。6.4 路由网络什么时候需要一起更新旧任务到来时路由网络需要更新以适配新任务旧任务回测时不建议更新路由网络。 SEMA 的做法是每个任务训练时都创建当时的 task embedding 和路由权重快照回测旧任务时使用对应快照。这样严格保证了持续学习中的“无遗忘”性质。6.5 任务相似度很高时怎么办如果两个任务高度相似梯度方向冲突检测会给出很小的夹角路由网络也会倾向于复用同一个 Adapter因此不会触发扩展。这和人类学新技能很像会的越多学新东西越快因为底层的可复用模块足够多。6.6 小数据量任务能不能撑起一次扩展可以但需要小心。如果任务只有几百条样本单独训练一个新 Adapter 很容易过拟合。这时建议把扩展阈值调高尽量复用已有模块。如果复用的表现实在无法接受才考虑扩展并配合较高的正则化强度。6.7 SEMA 支持 LoRA 替换吗支持但不建议。如果坚持用 LoRA需要额外设计动态权重合并机制保证多个 LoRA 之间不互相污染。实现上会比 Adapter 复杂很多效果也不一定有优势。6.8 推理时会不会因为 Adapter 太多而变慢不会。路由网络每一条样本只会选中一个或少量 Adapter其他 Adapter 不需要参与计算。即使池子里有几十个 Adapter每次推理也只走其中一条路径所以延迟与 Adapter 池大小无关只与单个 Adapter 的大小有关。6.9 增长控制器判断“该长大了”的延迟有多大增长控制器的判断基于滑动窗口和梯度夹角计算开销很小。需要注意的是不要在每个 step 都做一次完整检测建议每 10 个或 20 个 step 采样一次这样更稳定也能减少训练中的额外开销。6.10 这套方法能用到生成模型吗可以但要注意生成任务的路由选择需要按 token 级别进行而不是按样本级别。 SEMA 原文主要展示的是分类和识别任务在生成场景下还需要额外设计 token 级路由策略这是一个很有潜力的后续方向。7. 个人实操感受与一个容易被忽略的小技巧读这篇工作的时候我最开始只盯着“怎么长”这个技术点后来才发现“什么时候长”才是真正难得的思考。很多动态网络方法都是一遇到困难任务就加容量结果模型越加越复杂最终和全量微调没什么区别。SEMA 的价值在于它给“扩容”建立了一套相对严谨的判定标准而且还兼顾了模块隔离和参数效率。我自己在复现过程中最有用的一个小技巧是把增长控制器的决策信息记录下来每个 step 记录当前 loss、置信度、梯度夹角和是否触发扩展。训练结束后画一张时间线图你会非常清楚地看到模型是在哪些节点“被逼着长大”的。一张这种图比任何评估指标都能帮你理解模型的瓶颈在哪里也能帮你更快地调阈值。如果你也想试建议从一个小规模的 Adapter 池两三个 Adapter开始配合一个包含 5 到 8 个任务的序列做实验第一天就能看出动态扩容带来的差别。别指望它解决所有问题但它确实让“模型什么时候该长大”这个抽象问题变成了一个可以被计算、被度量、被决策的工程问题。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →