在介绍了低秩适配 (LoRA) 这种高效微调方法之后,我们接下来介绍适配器微调。适配器微调最初由Houlsby等人(2019)提出,是早期的附加型PEFT方法之一。与LoRA修改现有权重不同,适配器微调是在预训练模型架构中引入新的、小的、可训练的模块,同时保持原始模型参数冻结。这种方法大幅减少了微调时需要更新的参数数量。核心思想:注入可训练模块适配器微调的基本原理简洁而巧妙:冻结预训练模型绝大多数参数(通常有数十亿个),并在模型的每一层或选定层中插入紧凑的、任务专用的“适配器”模块。微调时,只有这些新添加的适配器模块的参数会被训练。大语言模型(LLM)的原始权重保持不变。这种模块化提供了显著优点:参数效率: 仅训练总参数中极小一部分(适配器中的参数),大幅降低了优化器状态和梯度的内存需求。任务专用性: 可以使用相同的冻结基础模型为不同的下游任务训练不同的适配器。部署时,只需“插入”对应目标任务的适配器,避免存储大型基础模型的多个副本。适配器模块架构标准适配器模块通常采用瓶颈架构,旨在降低计算成本,同时提供足够的适应表达能力。它通常按顺序放置在Transformer块内,经常处理多头注意力或前馈网络等子层的输出。结构通常包含:下投影: 一个线性层,将输入隐藏状态 $h$(维度为 $d$)投影到一个小得多的中间维度 $m$。这个瓶颈维度 $m$ 是一个重要的超参数,通常远小于 $d$(例如,$m \ll d$)。非线性处理: 一个激活函数 $\sigma$(如ReLU、GeLU或SiLU),应用于下投影的输出。这使得适配器能够学习复杂的非线性变换。上投影: 一个线性层,将激活后的瓶颈表示重新投影回原始隐藏维度 $d$。残差连接: 上投影的输出加回到原始输入隐藏状态 $h$。数学上,如果 $h \in \mathbb{R}^d$ 是适配器模块的输入,该操作可表示为:$$ h_{adapter} = W_{up}(\sigma(W_{down}(h))) $$这里 $W_{down} \in \mathbb{R}^{m \times d}$ 是下投影的权重矩阵,$W_{up} \in \mathbb{R}^{d \times m}$ 是上投影的权重矩阵,$\sigma$ 是非线性激活函数。线性层中可以选择性地包含偏置项。适配器模块处理后的最终输出 $h'$,包括残差连接,为:$$ h' = h + h_{adapter} = h + W_{up}(\sigma(W_{down}(h))) $$初始化: 一种常见做法是随机初始化 $W_{down}$(例如使用标准的Kaiming或Xavier初始化),但将 $W_{up}$ 初始化为接近零。这确保了在训练开始时($t=0$),适配器模块的输出 $h_{adapter}$ 接近零,使 $h' \approx h$。这种策略有助于保留预训练模型的初始能力并有助于稳定训练,因为适配器会逐步学习必要的任务专用变换。Transformer中的插入位置适配器模块需要策略性地放置在Transformer架构内。常见位置包含:在多头自注意力(MHSA)子层之后: 修改注意力机制生成的表示。在前馈网络(FFN)子层之后: 修改逐位置变换后的表示。在每个Transformer块的最终层归一化之后。通常,适配器在每个Transformer块内的注意力子层和FFN子层之后插入。下图展示了这些典型插入位置。digraph G { rankdir=LR; node [shape=box, style="filled,rounded", fontname="sans-serif", color="#495057", fillcolor="#dee2e6"]; edge [color="#495057", fontname="sans-serif", fontsize=10]; subgraph cluster_transformer_block { label = "Transformer块中适配器的典型插入"; labeljust = l; fontsize=14; bgcolor="#f8f9fa"; color="#ced4da"; style="rounded"; Input [label="输入 (h)", shape=rect]; Node1 [label="子层 1\n(例如,MHSA)\n(冻结)", color="#4263eb", fillcolor="#bac8ff"]; AddNorm1 [label="加法与归一化", shape=invhouse]; Adapter1 [label="适配器\n(可训练)", color="#12b886", fillcolor="#96f2d7"]; Node2 [label="子层 2\n(例如,FFN)\n(冻结)", color="#4263eb", fillcolor="#bac8ff"]; AddNorm2 [label="加法与归一化", shape=invhouse]; Adapter2 [label="适配器\n(可训练)", color="#12b886", fillcolor="#96f2d7"]; Output [label="输出 (h')", shape=rect]; // 顺序流,显示适配器位置 Input -> Node1 -> AddNorm1 -> Adapter1 [label=" 处理\n 输出"] ; Adapter1 -> Node2 [label=" 修改后的\n 表示"]; Node2 -> AddNorm2 -> Adapter2 [label=" 处理\n 输出"]; Adapter2 -> Output [label=" 修改后的\n 表示"]; // 注意:为清楚显示适配器位置,Add&Norm内部的残差连接已省略。 } }Transformer块的简化视图,突出显示了适配器模块在主要子层(注意力层和FFN)之后的典型插入位置。基础模型组件保持冻结。参数效率分析适配器微调的参数效率源于适配器模块相对于完整模型层的小尺寸。对于一个输入/输出维度为 $d$、瓶颈维度为 $m$ 的单个适配器模块,可训练参数的数量大约是 $2 \times d \times m$(来自 $W_{down}$ 和 $W_{up}$),再加上可能的少量偏置项(下投影为 $m$ 个,上投影为 $d$ 个)。如果我们考虑一个有 $L$ 层的模型,并在每层插入两个适配器(一个在MHSA之后,一个在FFN之后),可训练参数总数大约是 $L \times 2 \times (2dm + d + m)$。鉴于 $m \ll d$,这个数量远小于原始模型层中的参数,后者通常与 $O(d^2)$ 成比例(例如在前馈网络和注意力投影中)。例如,如果 $d=1024$ 且 $m=16$,每个适配器大约有 $2 \times 1024 \times 16 \approx 32,768$ 个参数(忽略偏置项),而一个单一的FFN层可能拥有数百万个参数。这种减少使得在内存有限的硬件上微调大型模型成为可能。适应原理这些简单的模块如何适应一个大型预训练模型呢?适配器学习任务专用函数,这些函数作用于由冻结的LLM层生成的中间表示。它们充当轻量级的“引导”机制,精细地修改通过网络的信息流,以更好地适应目标任务的要求。通过以特定方式调整注意力层和前馈层的输出,适配器可以使模型的行为专用化,而无需改变其在预训练期间捕获的核心知识。残差连接确保适配器最初提供近似恒等映射(如果 $W_{up}$ 接近零),这使得模型能够从其预训练状态开始,并逐步融入所学习的适配器变换。虽然这里描述的基本架构是常见的,但存在一些变体,例如在适配器内放置层归一化或研究并行适配器配置。然而,插入带有残差连接的小型、可训练瓶颈模块的主要原理仍是适配器微调方法的核心。在下一节中,我们将介绍实际实现细节。