适配器微调 Adapter:插入式轻量模块的迁移学习
认识适配器微调:轻量级迁移学习的革新
在自然语言处理领域,大规模预训练模型(如BERT、GPT)已经展现出惊人的泛化能力。然而,为每个下游任务对数十亿参数进行全量微调(Full Fine-tuning),不仅需要巨大的计算资源和存储开销,还会导致“灾难性遗忘”和多任务扩展困难。适配器微调(Adapter Tuning) 正是为了解决这些痛点而诞生的一种参数高效迁移学习方法。它在不改变原始预训练模型权重的前提下,通过向模型中插入轻量级的可训练模块,实现任务特定的知识注入。
适配器的核心思想:插入而非重写
传统迁移学习会把整个预训练模型视为可训练参数,并在下游数据上更新所有层。这相当于用新知识“重写”原有的通用表示,极易破坏预训练获得的宝贵特征。
而适配器则采用完全不同的策略:冻结预训练模型的所有参数,只在Transformer的每一层中插入少量新增参数(即适配器模块),仅训练这些新增的部分。每个适配器模块通常是一个简单的瓶颈结构,参数量仅占原模型的0.5%~5%,却能逼近全量微调的性能。这就好比给一个已经精雕细琢的引擎添加可插拔的定制化零件,而无需改动引擎本身。
适配器模块的微观结构
一个标准的适配器模块由两个线性层和一个激活函数组成,形成一个下采样→非线性变换→上采样的瓶颈结构:
- 下投影(Down-Project):将输入维度
d压缩到一个较小的维度m(m << d),减少参数量并提取关键信息。 - 非线性激活(Activation):常使用ReLU或GeLU,为变换注入非线性表达能力。
- 上投影(Up-Project):将维度从
m恢复为d,以便与原始网络中的残差路径相加。
最后还会加上一个 残差连接(Skip Connection),确保在适配器参数初始化趋近于零时,模块退化为恒等映射,不破坏预训练模型的初始行为。整体公式可表示为:
Adapter(x) = x + W_up · f( W_down · x )
其中 W_down 的形状为 d × m,W_up 的形状为 m × d,参数总量约为 2dm,远小于原层中的 d² 量级参数。
适配器在Transformer中的插入位置
在Transformer架构中,适配器通常被放置在两个关键位置:
- 多头自注意力(MHA)之后:在自注意力输出的残差路径上,先经过LayerNorm,再进入适配器模块。
- 前馈网络(FFN)之后:在FFN输出的残差路径上放置第二个适配器。
标准配置下,每个Transformer层包含两个适配器模块,一个接在注意力子层后,一个接在FFN子层后。这种布局被证明既能稳定训练,又能充分捕捉任务相关特征。实验显示,仅使用其中一个位置也能工作,但两个位置同时使用通常效果更优。
适配器微调的工作流程
使用适配器进行下游任务适配,通常遵循以下步骤:
- 加载预训练模型:实例化一个大规模预训练Transformer(如BERT-base)。
- 注入适配器模块:在模型每一层的指定位置插入刚刚定义的适配器结构,并随机初始化其权重(通常用接近零的初始化)。
- 冻结基础模型:将预训练模型的所有参数设置为不可训练(
requires_grad=False)。 - 定义任务头部:根据具体任务添加一个分类层(如情感分类)、序列标注层等,这部分参数也是可训练的。
- 训练:仅针对适配器参数和任务头部进行梯度更新。由于可训练参数极少,训练速度极快,内存占用显著降低。
- 推理:训练完成后,模型的前向传播会经过冻结的基础网络和已训练的适配器,输出任务结果。
为何适配器如此高效?
1. 参数效率
以BERT-base为例(约110M参数),若使用瓶颈维度 m=64,每个适配器增加 2×768×64 ≈ 98,304 参数,12层24个适配器总计带来约2.4M额外参数,仅为原模型的2%。训练时仅需保存和更新这2%的参数,极大降低存储和通信成本。
2. 多任务可复用
由于基础模型保持不变,不同的下游任务可以共享同一个骨干网络,只需为每个任务保存一组适配器参数。切换任务时,仅需更换几十MB的适配器权重,无需反复加载庞大的预训练模型。这为动态多任务推理铺平了道路。
3. 避免灾难性遗忘
冻结预训练参数意味着通用的语言知识被完全保留,适配器只是学习一种“偏移”,将模型表示向特定任务微调,而不会抹去原始能力。在面对新任务时,原有的零样本能力依然能得到保持。
4. 训练稳定性与速度
更少的可训练参数使得优化更稳定,可以使用较大的学习率,收敛速度极快。在小样本场景下,适配器的性能往往优于全量微调,因为它不易过拟合。
实践中的适配器:代码示例片段
以下为使用Hugging Face的adapter-transformers库(一个适配器扩展库)快速实现适配器微调的伪代码示意,便于初学者建立感性认识。
from transformers import AutoModelForSequenceClassification, AdapterConfig
# 加载预训练模型
model = AutoModelForSequenceClassification.from_pretrained("bert-base-uncased")
# 将基础模型参数冻结
model.train_adapter("sentiment")
# 添加适配器模块(指定瓶颈维度)
adapter_config = AdapterConfig(mh_adapter=True, output_adapter=True, reduction_factor=16)
model.add_adapter("sentiment", config=adapter_config)
# 只训练适配器和分类头
model.train_adapter(["sentiment"])
# 正常训练循环...
在上述代码中,reduction_factor=16 表示瓶颈维度被压缩为原模型的1/16,对BERT-base而言约为48维。mh_adapter和output_adapter控制是否在注意力层和FFN后添加适配器。
适配器与前缀微调、LoRA的对比
为了更全面理解适配器的定位,我们将其与其他主流参数高效微调方法进行简要对比:
- 前缀微调(Prefix Tuning):在输入序列前添加可训练的连续向量(“前缀”),这些向量间接影响后续层的注意力计算。参数量更少,但可能对长序列表现敏感。
- LoRA(Low-Rank Adaptation):通过低秩矩阵分解更新注意力层的权重矩阵,训练时只需学习低秩矩阵,并与冻结权重相加。LoRA在推理时没有额外延迟,因为训练后的参数可以合并回原矩阵。
- 适配器(Adapter):插入全新模块,结构更为直接,尤其在多任务存储和热插拔方面具有天然优势。
三种方法都能在保持0.1%-5%参数更新的前提下达到接近全量微调的效果,选择取决于具体工程需求和部署约束。适配器因其易于实现、插入式本质,成为多任务和持续学习场景的常客。
适配器的进阶变体
随着研究深入,适配器家族也演化出更高效的变体:
- Pfeiffer Adapter:将Layernorm从适配器外部移动到内部,仅保留残差连接,进一步简化结构。
- Compacter(Compact Adapter):结合了克罗内克积分解和参数共享,将适配器的参数压缩到极致,有时仅需模型总参数的0.1%。
- AdapterFusion:不是简单的任务间切换,而是通过学习如何融合多个任务的适配器参数,实现多任务知识组合。
这些变体都遵循相同的核心理念:保持预训练知识完整,仅用极少的任务专属参数进行适配。
何时应该使用适配器微调?
适配器并非万能,你可以在以下场景优先考虑它:
- 计算资源有限,无法承担全量微调的训练和存储成本。
- 需要同时服务大量下游任务,且必须频繁切换任务模型。
- 进行终身学习或持续学习,要求模型持续吸收新任务而不遗忘旧知识。
- 训练数据极少,全量微调极易过拟合,需要强有力的正则化形式。
如果你追求单任务的极致性能,且拥有充足的资源和时间,全量微调或许仍有微弱优势;但在绝大多数实际生产和多任务环境中,适配器以极小的性能牺牲换来了巨大的工程便利。
总结
适配器微调通过向冻结的预训练模型中插入轻量级瓶颈模块,实现了参数高效的迁移学习。它巧妙地在“知识保留”与“任务适配”之间找到了平衡,让大规模预训练模型能够被灵活、经济地应用于无数下游任务。作为参数高效微调领域的基石之一,适配器至今仍在各种最新的语言模型和多模态模型中被广泛使用和改进。掌握适配器,你将拥有一种强大而节俭的利器,从容应对当代NLP的挑战。
深入学习推荐:尝试使用
adapter-transformers库对BERT进行多任务适配器实验,或阅读论文《Parameter-Efficient Transfer Learning for NLP》了解适配器的完整设计细节。