
模型什么时候该“长大”这个标题是我从一个朋友那儿看来的。他在用 ResNet 预训练模型做一个会不断来新数据的图像分类项目每次来一批新类目他就得把所有参数重新微调一遍或者手工往模型里塞几个新的 Adapter 模块。来回折腾几次之后他忍不住问了一句能不能让模型自己判断啥时候该加 Adapter啥时候不用加CVPR 2025 上有一篇叫 SEMA 的工作正好就是干这个的——让预训练模型在运行过程中按需长出新 Adapter。先给搜到这篇文章的朋友泼盆冷水这里说的 Adapter不是你在电商平台上搜到的蓝牙适配器也不是设备管理器里那些 generic bluetooth adapter、VMware 虚拟网卡驱动。在深度学习里Adapter 是一种插在预训练网络层之间的小型可学习模块。这篇解读我会尽量用直白的语言把 SEMA 的动机、核心机制、实操策略和常见坑讲清楚尤其适合正在做模型微调、持续学习或动态部署的朋友。如果你只是拿现成模型做单任务分类这篇文章可能跟你关系不大但如果你要处理的是“任务会持续增加”“边缘端模型要原地升级”“不想因为新数据把旧知识冲掉”这类问题那 SEMA 的思路会给你一个非常不一样的视角。1. 为什么模型需要“按需长大”1.1 预训练模型微调的两个老方向预训练模型比如 ResNet、ViT、RoBERTa的微调现在基本分成两个派别。第一派是全量微调把整个骨干网络的参数全部更新一遍。效果通常不错但计算量和存储量都很吓人几十层网络全量算梯度小团队和边缘设备根本扛不住。第二派是参数高效微调典型代表就是 Adapter 和 LoRA。这些方法会把主干网络冻住只训练额外插入的小模块。Adam 优化器只维护被训练参数的动量和方差参数量少了显存占用和训练时间能降一个数量级。Adapter 的结构通常是个瓶颈形状的小网络先把特征压到一个低维空间经过非线性激活再映射回原来的维度。它就像在主路旁边修了一条小路学到的残差叠加回主干特征上整体参数量一般只占模型的 1% 到 5%。你可能会问这么小的模块真的够用吗够不够用恰恰就是 SEMA 这篇工作想讨论的核心问题。1.2 固定结构在持续任务里的具体麻烦传统 PEFT 的方法Adapter 的数量和位置是人事先设计好的。比如给 10 个任务就准备 10 套 Adapter再训练一个路由器去挑选或者一上来就在每一层都插两个 Adapter谁爱用谁用。这种“预先定死”的思路有两个天然问题。第一任务复杂度不确定。有的任务很轻一个 shallow adapter 就够了有的任务很难需要好几个 Adapter 在不同层协作才能解决。你在训练之前根本不可能知道未来任务长什么样。第二任务会不断到来。持续学习场景下昨天的模型可能还没为新任务做好准备今天新任务就来了。你只能不停人工干预加个 Adapter、调一下路由、再做一次校准。这就像你装修房子时把所有柜子都买齐但住进来之后发现有些房间根本不需要那么多柜子客厅反而缺一个合适的书架。真正可持续的做法是先住进去等发现哪里不够用了再按需添置。SEMA 的出发点就是把“模型该多复杂”这件事从“人工提前决定”变成“模型自己动态决定”。1.3 SEMA 这个名字背后的视角转换SEMA 这类工作之所以在 CVPR 2025 上能引起关注是因为它换了一个问题视角。过去我们习惯把模型容量当成一个固定超参数SEMA 则把容量当成一种可增长资源。模型可以在看到足够多新数据、发现当前容量不再能降低损失的时候主动申请“长大”。这其实跟人类的学习过程很像一个小孩遇到一道完全解不出来的题他不会带着现有知识硬扛而是会去学一个新的解题策略。学习策略的增加就是认知结构的“按需生长”。我把这类方法拆开之后发现无论论文写得再复杂都绕不开三个问题什么时候长、长在哪里、长成什么样。下面我就按这三个问题来拆解 SEMA 的核心机制。2. SEMA 的核心机制拆解什么时候长、长在哪、长成什么样2.1 什么时候“长大”靠什么信号来判定让模型自己决定成长最关键的是找到可靠的“成长信号”。如果信号选错了模型要么在不需要的地方疯狂长要么该长的时候一动不动。我在实际项目里见过几种可用的信号SEMA 这类方法大概率会结合它们来设计判定条件。第一种是性能高原判据。看新任务在验证集上的 loss 或者准确率连续多个 epoch 都看不到改善这时候可以认为当前 Adapter 容量已经到了瓶颈。这个信号最直观也最容易被风吹草动干扰比如学习率没调好也会让 loss 不动所以一般会配合早停机制使用。第二种是不确定性判据。用 MC-Dropout 或者多个子模型做集成估计模型对新样本的置信度和预测方差。当模型在某一类样本上反复给出不稳定的预测说明现有特征空间还没覆盖到这类数据这就是一个比较强的新增容量信号。第三种是特征变化判据。把新任务的数据过一遍当前模型计算某一层输出的 CKA 相似度或者 Gram 矩阵。如果新数据的特征分布和旧数据已经有明显偏移说明你需要在特征偏移比较严重的层做增强处理。SEMA 的“生长判定”大概率不是只用某一个信号而是做组合式判断。比如先看验证集 loss 是否进入平台期再结合不确定性是否偏高两条件同时满足才触发成长。这样做的好处是降低误判率坏处是会让训练流程多出几个需要调试的超参数。但这是动态结构绕不开的成本。2.2 长在哪里路由与插槽的设计决定在哪个位置长 Adapter本质是一个结构搜索的问题。老老实实在每一层都做候选成本太高也不优雅。比较常见的做法是先在骨干网络里预留一些“插槽”。插槽不是一开始就挂 Adapter而是一个可以随时接入新模块的位置。比如每个 Transformer block 的 FFN 后面留一个 slot或者每个 ResNet stage 的残差连接旁留一个 slot。训练初期所有 slot 都是空的模型就是一个标准预训练网络。当成长信号被触发模型要从候选 slot 里选一个最适合的位置接入新 Adapter。怎么选可以看梯度信息。把新任务的数据在候选 slot 处做一个探针实验插入一个小 Adapter 后观察损失下降了多少。收益最大的那个 slot 就作为新 Adapter 的落脚点。这个选择过程也常常由一个轻量级路由器来完成。路由器的作用是在推理阶段为每个输入样本选择路径是直接走主干还是走已有的某个 Adapter还是走新长出来的 Adapter。为了让路由是可微的、能训练的一般会用 Gumbel-Softmax 或者 Sparsemax 这样的离散选择技巧。这里我想提醒一句路由器并不是越复杂越好。它本身也是个网络如果比 Adapter 还大那参数高效微调就名存实亡了。所以路由器的设计目标是用尽量少的参数做到足够稳的选择。2.3 长成什么样初始化与旧知识隔离新 Adapter 长出来之后最怕的一件事就是“一出生就把模型带歪”。如果随机初始化权重新 Adapter 的输出会直接污染主干的特征分布原本好好工作的模型瞬间变傻。所以新 Adapter 的初始化至关重要。两种比较稳妥的做法。第一种是 Zero-init把新 Adapter 的最后一层权重初始化为零让它的初始输出是零那么模型行为完全不变。之后随着训练新 Adapter 逐渐从零开始学习一个残差补充就像给旧知识加了一个微小的修正项。第二种是复制近邻 Adapter 的权重再加一个很小的噪声。这样新 Adapter 一开始的行为接近已有 Adapter可以在类似任务上快速迁移。新 Adapter 与旧知识的关系也需要仔细设计。我的建议是已有 Adapter 的参数在生长之后应该冻结。如果旧 Adapter 还能被继续更新新任务的数据很可能会把旧任务专属的特征表示带偏造成灾难性遗忘。新 Adapter 只负责在特定位置输出一个残差叠加并且跟主干、旧 Adapter 保持隔离这样旧知识存在的物理通路就不会被破坏。除了以上三点SEMA 还要考虑“别长太大”的问题。动态生长如果没有约束模型最终可能长出几百个 Adapter推理时内存直接爆炸。所以通常会设置一个总参数预算或者限制每个样本只允许经过一个 Adapter。这种“预算感”是动态结构工程落地时必须有的设计。3. 实操过程与核心环节实现3.1 训练流程的参考伪代码我整理了一份参考训练流程不保证跟 SEMA 原论文完全一致但这类“增长式 Adapter”方法的骨架基本如此。for task_t in task_sequence: # 当前已存在若干 adapter 和 router while not converged: features backbone(x, route_choicerouter(features)) logits classifier(features) loss ce_loss(logits, y) \ lambda_r * router_penalty(route_choice) \ lambda_l * complexity_penalty(active_adapters) loss.backward() if step % eval_interval 0: plateau is_loss_plateau(task_t) uncertain is_uncertainty_high(features) if plateau and uncertain and cooldown_ok(): slot choose_best_slot(task_t, backbone) new_adapter Adapter(hidden_dim, rank8, zero_initTrue) backbone.insert_adapter(slot, new_adapter) # 冻结旧 adapter只训练 router 新 adapter enable_grad(new_adapter) enable_grad(router) disable_grad(backbone_except_router) trigger_rollback_checkpoint()伪代码里的每一步都有实际意义。先看router_penalty这是为了让路由的选择尽量稀疏不要出现“一个样本同时激活三个 Adapter”的情况。complexity_penalty则是用于控制 Adapter 总量比如常用的 L0 正则或者参数范数惩罚避免模型无限膨胀。cooldown_ok()是冷却期检查刚长完一个 Adapter 后至少再训练若干个 epoch 才能触发下一次生长。我会把is_loss_plateau实现成在最近 5 个评估点里loss 相对最佳值的下降不超过 0.5%。这个阈值很保守适合小型任务。如果遇到难任务可以放宽到 1% 左右。3.2 训练顺序与分阶段冻结策略实现动态生长最常见的一个误区是想一次性把所有参数都训练起来。真实效果会很差因为新 Adapter 刚长出时很弱它给出的信号根本竞争不过其他路径。我实际踩过这个坑之后总结了一套分阶段冻结策略。刚插入新 Adapter 时主干网络、旧 Adapter 全部冻结只训练新 Adapter 和路由。这个阶段通常持续几百到几千步目的是让新 Adapter 稳定地学会针对新任务的特征变换。之后可以解开主干网络最后 1 到 2 层的梯度让深层特征能稍微适应新任务。但浅层特征尽量不动否则低级视觉特征会被新任务带偏。路由器的训练可以贯穿始终但它的学习率要相对低一些不然会出现“路由频繁改道”的混乱局面。3.3 推理部署与 Adapter 融合动态结构在训练时很有价值但部署时如果不管很容易变成灾难。想象一下推理时每个样本都要把所有 Adapter 测一遍再决定走哪条路这开销没几个人扛得住。因此部署时要分成两步走。第一步路由离线固化。用一小批验证集统计路由器的选择规律把一个样本映射到某个 Adapter 路径的决策离线固化成一个查表规则或者轻量分类器。线上推理时不需要再跑复杂的 Gumbel-Softmax。第二步Adapter 融合。对于线性路径为主的 Adapter比如 LoRA 风格的低秩分解可以直接把增量矩阵合并到主干权重里。对于有非线性的 Adapter则可以把路划分为“独立小网络”按路由结果动态挂载但不要同时载入全部 Adapter。这样内存占用可以被压到一个比较低的上限。4. 常见问题与排查技巧实录4.1 新 Adapter 一直不生效我见过最多的情况是新 Adapter 长出来了但训练了整整一个 epoch验证集上一点变化都没有。排查思路从三个方向走先看新 Adapter 的参数范数有没有变化如果范数一直趋近于零说明梯度根本流不进去再看路由是否真的给新样本分配了这条路径如果路由器总是选旧 Adapter那新分支等于白长最后检查插入位置如果插在一个信息流已经很晚的层前面提取的特征已经定型新 Adapter 能做修正的空间就非常有限。一个很实用的调试技巧是临时禁用路由强制让新任务的数据只走新 Adapter看 loss 能不能下降。如果强制走都不降那就是新 Adapter 本身的学习率或者初始化出了问题。如果强制走能降但开了路由就不降那问题在路由器和旧 Adapter 之间的竞争关系上。4.2 模型疯狂生长Adapter 数量爆炸动态生长最让人头痛的副作用之一就是模型总想“多长几个”。原因多半是成长信号太敏感或者冷却期不够。我遇到过某个项目里每隔几十步就触发一次生长最后长出来 40 多个 Adapter性能没有变好推理倒是越来越慢。我的建议是提高成长阈值的标准。把“训练 loss 平台期”改成“验证集上连续 N 次没有真正收益”同时引入严格的收益评估新 Adapter 加入后如果验证集提升小于某个阈值比如 0.3%就把它回滚掉。这就是“先长出来试试不好用就剪掉”的思路。达到参数预算上限后强制让模型进入“重组模式”不再允许新增 Adapter只能对已有 Adapter 做合并或剪枝。4.3 路由震荡与任务纠缠路由震荡的表现是同一个训练样本在不同 epoch 被分配到不同的 Adapter有时甚至一个样本同时激活两条路。这会让两个 Adapter 都在学同一个样本的特征互相干扰最后没有一个学得好。这个问题在持续学习场景特别明显因为任务之间特征有大量重叠。解决手段有几个。第一个是温度退火Gumbel-Softmax 的温度系数从大往小降让路由决策逐渐从随机变得确定第二个是给路由决策加一个指数滑动平均让历史决策参与统计避免单次波动影响太大第三个是引入“任务置信度”机制只有当路由器给出的最高概率超过阈值时才走对应 Adapter否则走主干。这个机制有点类似大家熟悉的“困惑度过滤”能有效抑制低质量决策。4.4 预训练知识被新 Adapter 冲掉“学了新任务就忘了旧任务”是持续学习的千年难题。SEMA 这类动态结构本身已经把风险压低了因为旧 Adapter 的参数被冻结主干也被冻结。但我仍然遇到过一种特殊情况新 Adapter 插在太靠近输入的层它输出的残差叠加之后后续所有层都会受到影响间接导致旧 Adapter 的输入分布漂移。面对这种情况我会选择把新 Adapter 的插入位置往高层移动。低层特征是通用的应该保持不变高层特征才更接近任务语义在那里做适配更安全。另外新 Adapter 的输出可以加一个对齐损失让它初始阶段输出的残差趋近于零只在新任务数据上逐步放开。这个“对齐 零初始化”的组合拳在大多数场景下都比单纯用低学习率更稳。下面是一个快速排查表我把典型问题、现象、排查方法和修复思路放在一起问题典型现象排查方向修复方法新 Adapter 无效验证集 loss 不变参数范数、路由选择、插入位置禁用路由强制测试 / 提高新模块学习率 / 换高层 slotAdapter 数量爆炸训练过程不断触发生长成长阈敏感、冷却期短、阈值过低提高收益阈值 / 加入冷却期 / 设置参数预算路由震荡同一样本被分配到不同路径温度、历史决策、任务置信度温度退火 / EMA / 置信度阈值旧知识遗忘旧任务准确率明显下降插入位置太浅、新模块对齐不足高层插入 / 输出对齐损失 / 旧模块冻结内存溢出推理时模型占用超限Adapter 加载方式、并发路径数离线路由固化 / 线性路径融合 / 限制激活路径数5. 这套思路对实际项目的启发5.1 哪些场景值得用 SEMA 这类方法我并不会推荐所有人都上动态生长。如果你的任务是固定数据集、固定类别数、一次性训练那老老实实做 PEFT 就好动态机制只会增加调试复杂性。真正适合 SEMA 的场景有三类。第一类是持续学习新的类别或新任务会不断到来你的系统需要不停吸收新知识但不能推倒重来。第二类是边缘端模型原地升级设备网络不稳定不可能每次更新都重新拉一个大模型下来倒不如只推一个小 Adapter 就地在设备上挂载。第三类是超大规模基础模型的个性化服务底座模型共享不同租户挂不同的 Adapter在线需要开辟新租户时“长”一个新的个性化模块比完整复制模型要划算得多。5.2 和 LoRA、Prompt 的互补关系很多人会把 SEMA 和 LoRA、Prompt Tuning 放在一起比较。我的看法是它们不完全是同一维度的东西。LoRA 和 Adapter 是“长好之后再插入”的静态方案Prompt Tuning 是“在输入侧加额外标记”的表示方案而 SEMA 是“随着任务需要动态建立新模块”的结构自适应方案。SEMA 完全可以和 LoRA 结合新长出来的模块内部就用低秩形式来表达进一步压缩新增参数量。这里有个经验可以分享。在视觉任务上如果你发现 LoRA 已经把表现提升到了瓶颈继续增大 rank 收益很小那么大概率不是“参数不够多”而是“结构不够适配”。这时候动态新增 Adapter 可能是更合理的选择。反之如果模型在低 rank 下表现还不错就没必要引入动态生长简单方案往往更稳。5.3 一个轻量实验提前判断你的模型是否需要“长大”最后一个建议来自我自己的项目经验。如果你不确定当前任务是否值得引入 SEMA 这种复杂度先做一个容量敏感度测试。拿三份不同容量的 Adapter 配置比如低容量 rank4、中容量 rank16、高容量 rank64在同样的数据上各跑一轮。然后画一条“容量 vs 验证集准确率”的曲线。如果高容量明显优于低容量并且低容量的 loss 曲线出现平台期说明任务本身需要更多容量动态生长值得考虑。如果三种容量结果差不多甚至高容量过拟合严重那说明当前模型已经足够别折腾动态结构静下心来把数据清理好比什么都强。这个测试成本很低但能帮你避免在错误的方向上疯狂调参。关于动态生长机制我最深的体会是它试图让模型复杂度和任务复杂度真正对齐而不是一上来就把模型撑到最大。SEMA 这类方法的难点从来不只是算法本身而是你愿不愿意把“是否长大”这个决定权交给模型。我个人会建议你先用一个小规模模拟实验观察验证集 loss、不确定性信号和新 Adapter 的边际收益等你在自己的数据上看到这几个信号真的和表征能力相关时再决定要不要引入完整的动态方案。至少这会比盲目堆参数靠谱得多。