模型什么时候该“长大”?这个标题是我从一个朋友那儿看来的。他在用 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_choice=router(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, rank=8, zero_init=True) 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 不变 | 参数范数、路由选择、插入位置 | 禁用路由强制测试 / 提高新模块学习率 / 换高层 slot |
| Adapter 数量爆炸 | 训练过程不断触发生长 | 成长阈敏感、冷却期短、阈值过低 | 提高收益阈值 / 加入冷却期 / 设置参数预算 |
| 路由震荡 | 同一样本被分配到不同路径 | 温度、历史决策、任务置信度 | 温度退火 / 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 配置,比如低容量 rank=4、中容量 rank=16、高容量 rank=64,在同样的数据上各跑一轮。然后画一条“容量 vs 验证集准确率”的曲线。
如果高容量明显优于低容量,并且低容量的 loss 曲线出现平台期,说明任务本身需要更多容量,动态生长值得考虑。如果三种容量结果差不多,甚至高容量过拟合严重,那说明当前模型已经足够,别折腾动态结构,静下心来把数据清理好比什么都强。这个测试成本很低,但能帮你避免在错误的方向上疯狂调参。
关于动态生长机制,我最深的体会是:它试图让模型复杂度和任务复杂度真正对齐,而不是一上来就把模型撑到最大。SEMA 这类方法的难点从来不只是算法本身,而是你愿不愿意把“是否长大”这个决定权交给模型。我个人会建议你先用一个小规模模拟实验,观察验证集 loss、不确定性信号和新 Adapter 的边际收益,等你在自己的数据上看到这几个信号真的和表征能力相关时,再决定要不要引入完整的动态方案。至少,这会比盲目堆参数靠谱得多。