微调
LLM 微调 (Fine-tuning): 概述
在预训练阶段,大语言模型(LLM)通过在海量通用文本数据上进行训练,学习到了广泛的语言知识、语法结构、世界常识以及一定的推理能力。然而,这些预训练模型是“通才”,它们并不针对任何特定的下游任务或特定的行为模式进行优化。
微调 (Fine-tuning) 的核心目标是:在预训练模型的基础上,使用特定任务或领域的数据集进行进一步训练,从而使模型能够更好地适应这些特定需求,表现出期望的行为或在特定任务上达到更高的性能。
可以把预训练模型想象成一个已经完成了大学通识教育的学生,而微调则像是针对特定专业方向进行的深造或职业培训。
微调的类型与目标
根据在微调过程中更新模型参数的范围和方式,主要可以将微调分为两大类:
- 全参数微调 (Full Fine-tuning): 更新模型的所有参数。
- 参数高效微调 (Parameter-Efficient Fine-Tuning, PEFT): 只更新模型中一小部分参数,或者增加少量新的可训练模块,而保持大部分预训练参数冻结。
让我们详细探讨这两种类型:
1. 全参数微调 (Full Fine-tuning)
-
核心思想: 在微调过程中,预训练模型的所有权重(包括词嵌入层、Transformer 层中的注意力权重和前馈网络权重等)都会参与梯度计算和参数更新。
-
如何操作:
- 加载预训练模型: 获取一个已经预训练好的 LLM (例如,从 Hugging Face Hub 加载一个 BERT, GPT, Llama 模型)。
- 准备特定任务的数据集: 针对你想要解决的下游任务(如情感分析、文本摘要、问答、代码生成等),准备一个标注好的数据集。
- (可选) 修改模型头部: 对于某些任务(尤其是分类任务或序列标注任务),可能需要在预训练模型的顶部添加一个新的、特定于任务的层(称为“头部”,Head)。例如,在 BERT 模型之上添加一个线性分类层来进行情感分析。对于生成式任务,通常不需要修改头部,直接用模型的语言模型头部进行生成。
- 训练: 使用特定任务的数据集,以较小的学习率继续训练整个模型(包括预训练部分和新添加的头部)。损失函数根据具体任务定义(如交叉熵损失)。
-
优点:
- 潜力巨大: 由于所有参数都参与训练,模型有最大的自由度去适应新的数据和任务,理论上可以达到最佳的性能。
- 概念简单: 比较直观,就是继续训练。
-
缺点:
- 计算成本高:
- 训练时间长: 更新所有参数需要大量的计算资源 (GPU/TPU 时间)。
- 显存占用大: 需要存储所有参数的梯度和优化器状态(例如 Adam 优化器会为每个参数存储其一阶和二阶矩估计,这会使得显存占用约为模型参数量的 3-4 倍,如果使用混合精度可能更多)。对于大型 LLM (数十亿甚至上百亿参数),即使是微调,单张 GPU 的显存也可能无法承受。
- 存储成本高: 如果你需要为多个不同的任务微调同一个基础模型,每个任务都需要存储一份完整的、经过微调的模型副本。这对于拥有大量参数的 LLM 来说,存储开销非常巨大。
- 灾难性遗忘 (Catastrophic Forgetting): 当模型在一个新任务上进行全参数微调时,它可能会“忘记”在预训练阶段学到的一些通用知识,或者在之前微调过的其他任务上的能力。这是因为所有参数都在向新任务的目标优化。
- 部署复杂性: 为每个任务维护和部署一个完整的模型副本,管理和更新会比较麻烦。
- 计算成本高:
-
适用场景:
- 当计算资源充足,且追求在特定任务上达到极致性能时。
- 当目标任务与预训练任务差异较大,需要模型进行大幅度调整时。
- 当模型规模相对较小,全参数微调的成本尚可接受时。
2. 参数高效微调 (Parameter-Efficient Fine-Tuning, PEFT)
-
核心思想: 保持预训练 LLM 的绝大部分(甚至全部)参数冻结不变,只引入和训练一小部分新的参数,或者只微调模型中已有的一小部分参数。
-
目标/动机:
- 降低计算成本: 由于只训练少量参数,训练所需的计算资源和时间显著减少。梯度计算和优化器状态的存储需求也大大降低,使得在单张消费级 GPU 上微调大型 LLM 成为可能。
- 减少存储成本: 对于每个特定任务,只需要存储那一小部分被修改或新增的参数,而不是整个模型的副本。基础的预训练模型可以被多个任务共享。例如,一个 7B 参数的模型,其 PEFT 模块可能只有几 MB 到几十 MB。
- 缓解灾难性遗忘: 由于大部分预训练参数被冻结,模型在预训练阶段学到的通用知识和能力能够得到更好的保留,从而减少在适应新任务时对原有能力的损害。
- 更容易管理多个任务的模型: 可以为每个任务训练一个轻量级的 PEFT 模块,在推理时根据任务加载对应的模块到共享的基础模型上。
- 提高数据效率: 有研究表明,在数据量较少的情况下,PEFT 方法有时能比全参数微调取得更好的效果,因为它们限制了模型的自由度,减少了过拟合的风险。
-
常见的 PEFT 方法 (我们后续会逐个详细学习):
- Adapter Tuning: 在 Transformer 层的固定预训练参数之间插入小型可训练的 "Adapter" 模块。
- Prefix Tuning: 在输入序列的嵌入层前添加可学习的连续向量前缀。
- Prompt Tuning (Soft Prompts): 类似 Prefix Tuning,但通常更轻量,只在输入嵌入前添加可学习的提示向量。
- LoRA (Low-Rank Adaptation): 通过学习低秩矩阵来近似预训练模型权重在微调时的变化。这是目前非常流行且效果显著的一种 PEFT 方法。
- (IA)^3 (Infused Adapter by Inhibiting and Amplifying Inner Activations): 通过学习缩放因子来调整预训练模型的内部激活。
- BitFit: 只微调模型中的偏置项 (bias terms)。
-
如何操作 (以 LoRA 为例的简化概念):
- 加载预训练模型并冻结其参数。
- 为模型中的某些层 (通常是注意力层或前馈网络层) 引入 LoRA 模块。 这些 LoRA 模块包含少量可训练的参数 (低秩分解矩阵 A 和 B)。
- 准备特定任务的数据集。
- 训练: 只训练这些 LoRA 模块的参数,而预训练模型的原始权重保持不变。
- 推理/部署: 可以将训练好的 LoRA 权重与原始预训练权重合并(对于某些方法如 LoRA),或者在推理时动态加载 LoRA 模块。
-
优点: (即 PEFT 的主要目标)
- 显著降低计算和存储成本。
- 有效缓解灾难性遗忘。
- 便于多任务模型的管理和部署。
- 在某些情况下性能接近甚至超过全参数微调,尤其是在低资源场景下。
-
缺点:
- 性能上限可能略低于全参数微调: 由于可训练参数的限制,在某些非常复杂的任务或需要模型进行根本性转变的情况下,PEFT 的性能上限可能略低于精心调整的全参数微调。
- 引入新的超参数: 不同的 PEFT 方法会引入各自的超参数 (例如 LoRA 中的秩
r
、alpha
值,Adapter 的瓶颈维度等),需要进行调整。 - 并非所有 PEFT 方法效果都一样好: 不同 PEFT 方法在不同模型、不同任务上的表现可能存在差异。
-
适用场景:
- 计算资源有限,尤其是显存受限的场景。
- 需要为大量不同任务微调同一个基础模型。
- 希望快速迭代和实验不同任务。
- 关注模型的便携性和部署效率。
- 希望尽可能保留预训练模型的通用能力。
总结与对比:
特性 | 全参数微调 (Full Fine-tuning) | 参数高效微调 (PEFT) |
---|---|---|
可训练参数 | 模型所有参数 | 少量新增参数或模型中一小部分已有参数 (通常 <1% of total) |
计算成本 | 高 (训练时间长,显存占用大) | 低 (训练时间短,显存占用小) |
存储成本 | 高 (每个任务一个完整模型副本) | 低 (每个任务一个小型 PEFT 模块) |
灾难性遗忘 | 风险较高 | 风险较低 |
性能潜力 | 理论上最高 | 通常接近全参数微调,有时甚至更好 (尤其低资源时),但上限可能略低 |
部署管理 | 复杂 (管理多个大模型) | 简单 (共享基础模型,加载不同 PEFT 模块) |
适用场景举例 | 追求极致性能且资源充足;任务与预训练差异大 | 资源有限;多任务场景;快速迭代;希望保留通用能力 |
理解这两种微调范式是进行 LLM 应用开发的基础。在实践中,PEFT 由于其巨大的优势,在当前的 LLM 领域变得越来越流行和重要。
全参数微调的操作细节
-
选择合适的预训练模型 (Choosing the Right Pre-trained Model):
- 任务相关性: 选择一个在其预训练任务或架构上与你的下游任务尽可能相似的模型。例如:
- 对于NLU任务(如文本分类、NER),BERT、RoBERTa、ELECTRA 等 Encoder-only 模型通常是好的起点。
- 对于文本生成任务(如摘要、翻译、对话),GPT系列、LLaMA、T5、BART 等 Decoder-only 或 Encoder-Decoder 模型更合适。
- 对于序列到序列的理解与生成任务,T5、BART 是不错的选择。
- 模型大小: 根据你的计算资源(GPU显存、训练时间预算)和对性能的要求来选择。更大的模型通常性能更好,但微调成本也更高。可以先从 base 版本开始尝试,如果资源允许再考虑 large 或更大版本。
- 预训练数据的领域: 如果有与你目标领域相关的预训练模型(例如,在医学文献上预训练的 BioBERT),可能会比通用领域预训练模型有更好的起点。
- 模型是否经过指令微调: 如果你的任务是遵循指令进行输出,选择一个已经经过指令微调的基础模型(如 Llama 2-Chat 的基础模型 Llama 2,而不是直接用 Llama 2-Chat)可能会更容易控制,因为你将进行特定任务的指令微调。如果直接用已经很“对话化”的模型进行非对话任务的微调,可能需要更小心。
- 任务相关性: 选择一个在其预训练任务或架构上与你的下游任务尽可能相似的模型。例如:
-
准备高质量的微调数据集 (Preparing High-Quality Fine-tuning Data):
- 数据质量至上: 数据的质量比数量更重要。确保标注准确、一致,数据清洗干净。低质量的数据会导致模型学到错误的信息。
- 数据格式: 将数据转换成模型输入所需的格式。这通常包括:
- 分词 (Tokenization): 使用与预训练模型相同的 Tokenizer。确保特殊 token(如
[CLS]
,[SEP]
,[PAD]
)的正确使用。 - 输入序列构建: 根据任务类型构建输入序列。例如,对于句子对分类任务,输入是
[CLS] 句子A [SEP] 句子B [SEP]
。 - 标签处理: 将标签转换为模型可以理解的格式 (例如,对于分类任务,将类别标签转换为整数索引或 one-hot 向量)。
- 分词 (Tokenization): 使用与预训练模型相同的 Tokenizer。确保特殊 token(如
- 数据量: 微调所需的数据量远少于预训练,但仍然需要足够的数据来覆盖任务的多样性。数据量不足容易导致过拟合。具体数量因任务复杂度而异,从几百到几万甚至更多不等。
- 数据划分: 将数据集划分为训练集 (Training Set)、验证集 (Validation Set) 和测试集 (Test Set)。
- 训练集: 用于训练模型参数。
- 验证集: 用于在训练过程中监控模型性能、调整超参数(如学习率、训练轮数)、进行早停 (Early Stopping)。
- 测试集: 用于在模型训练完成后评估其最终性能,测试集的数据不应在训练或验证阶段被模型看到。
- 处理类别不平衡 (Handling Class Imbalance): (如果适用) 对于分类任务,如果不同类别的样本数量差异很大,需要考虑处理方法,如过采样少数类、欠采样多数类、使用加权损失函数等。
-
模型架构调整 (Adapting Model Architecture - Task-specific Head):
- 对于大多数NLU任务 (如分类、序列标注),通常需要在预训练模型的顶部添加一个或多个新的、特定于任务的层 (head)。
- 分类任务: 通常在
[CLS]
token 对应的最终隐藏状态之上添加一个线性层 (全连接层) + Softmax (或 Sigmoid,取决于单标签/多标签分类)。 - 序列标注任务 (NER, POS): 通常在每个 token 对应的最终隐藏状态之上添加一个线性层 + Softmax,来预测每个 token 的标签。
- 问答任务 (如 SQuAD): 可能需要两个线性层来分别预测答案在原文中的起始位置和结束位置。
- 生成任务: 通常不需要修改模型头部,直接使用预训练模型的语言模型头部进行解码生成。
- 初始化新添加的层: 新添加的层通常使用随机初始化,或者使用一些启发式的初始化方法。
-
设置优化器和学习率 (Setting up Optimizer and Learning Rate):
- 优化器 (Optimizer):
- AdamW: 通常是首选优化器,它是 Adam 优化器的一个改进版本,能更好地处理权重衰减 (Weight Decay)。
- 学习率 (Learning Rate):
- 非常关键的超参数!
- 比预训练小得多: 全参数微调的学习率通常设置得比预训练时小一个或几个数量级 (例如,预训练可能是
1e-4
到5e-4
,微调可能是1e-5
到5e-5
,甚至更小如2e-5
,3e-5
)。 - 原因: 预训练模型已经学到了很多有用的知识,过大的学习率可能会破坏这些知识,导致训练不稳定或性能下降。我们希望在预训练的基础上进行“精细调整”。
- 实验确定: 最佳学习率通常需要通过实验在验证集上找到。
- 学习率调度器 (Learning Rate Scheduler):
- Warmup: 在训练初期,学习率从一个很小的值逐渐增加到设定的初始学习率。这有助于在训练早期稳定模型。
- 衰减 (Decay): Warmup 之后,学习率随着训练的进行逐渐降低 (如线性衰减、余弦衰减)。这有助于模型在训练后期更好地收敛到最优解。
- 常见的组合是 "linear warmup with linear decay" 或 "linear warmup with cosine decay"。
- 优化器 (Optimizer):
-
训练过程与超参数调整 (Training Process and Hyperparameter Tuning):
- 批处理大小 (Batch Size):
- 受限于GPU显存。在显存允许的范围内,较大的批处理大小通常能提供更稳定的梯度,但过大也可能导致泛化能力下降。
- 如果显存不足以支持期望的批处理大小,可以使用梯度累积 (Gradient Accumulation):将多个小批次的梯度累加起来,然后进行一次参数更新,等效于使用了一个更大的批处理大小。
- 训练轮数 (Number of Epochs):
- 通常不需要很多轮 (例如,2-5 轮可能就足够了,具体取决于数据集大小和任务复杂度)。训练过多的轮数容易导致过拟合。
- 早停 (Early Stopping):
- 在训练过程中,定期在验证集上评估模型性能。如果验证集上的性能在一定轮数内不再提升(甚至开始下降),就提前停止训练,以防止过拟合,并保存性能最好的那个模型。
- 权重衰减 (Weight Decay):
- 一种正则化技术,用于防止过拟合,通过在损失函数中添加一个与权重大小相关的惩罚项。AdamW 优化器内置了对权重衰减的正确处理。
- Dropout:
- 预训练模型通常已经包含了 Dropout 层。在微调时,这些 Dropout 仍然起作用,帮助防止过拟合。Dropout 概率通常保持预训练时的设置。
- 混合精度训练 (Mixed Precision Training):
- 使用 FP16 (半精度浮点数) 或 BF16 (BFloat16) 进行训练,可以显著减少显存占用并加速训练,同时尽可能保持 FP32 (单精度浮点数) 的训练精度。需要硬件和软件支持 (如 NVIDIA Apex 或 PyTorch AMP - Automatic Mixed Precision)。
- 批处理大小 (Batch Size):
-
评估与监控 (Evaluation and Monitoring):
- 选择合适的评估指标: 根据任务类型选择评估指标 (如准确率、F1 分数、精确率、召回率、BLEU、ROUGE、困惑度等)。
- 频繁验证: 在每个 epoch 结束时(或者更频繁地,例如每 N 个 steps)在验证集上进行评估,监控模型性能变化。
- 日志记录: 记录训练过程中的关键信息,如训练损失、验证损失、学习率、评估指标等,方便后续分析和调试 (可以使用 TensorBoard, Weights & Biases 等工具)。
-
保存与加载模型 (Saving and Loading Models):
- 保存最佳模型: 根据验证集上的表现,保存性能最好的模型权重 (checkpoint)。
- 保存 Tokenizer 配置: 除了模型权重,还需要保存 Tokenizer 的配置,以便在推理时能够以相同的方式处理输入。Hugging Face Transformers 库通常会将模型和 Tokenizer 一起保存。
注意事项 (Important Considerations)
-
灾难性遗忘 (Catastrophic Forgetting):
- 核心问题: 全参数微调时,模型为了适应新任务,可能会丢失在预训练阶段或之前微调任务中学到的通用知识。
- 缓解方法:
- 使用较小的学习率。
- 训练更少的轮数。
- 逐步解冻 (Gradual Unfreezing): 先冻结预训练模型的底层,只微调顶层或新添加的层,然后逐渐解冻更多层进行微调(这种方法在 CV 领域更常见,NLP 中也有尝试)。
- 多任务学习 (Multi-task Learning): 如果有多个相关任务,可以尝试同时微调。
- 弹性权重巩固 (Elastic Weight Consolidation, EWC) 等持续学习方法: 更复杂,但在特定场景下有效。
- PEFT 方法 是一个更直接有效的避免灾难性遗忘的策略。
-
过拟合 (Overfitting):
- 核心问题: 模型在训练数据上表现很好,但在未见过的验证集或测试集上表现较差。微调数据集通常较小,更容易发生过拟合。
- 缓解方法:
- 使用正则化技术: Dropout, Weight Decay。
- 早停 (Early Stopping)。
- 数据增强 (Data Augmentation): 如果适用,可以增加训练数据的多样性 (例如,文本替换、回译等,但要小心引入噪声)。
- 使用更小的模型 (如果可能)。
- 减少训练轮数。
- 交叉验证 (Cross-Validation): 在数据量非常少的情况下,可以使用交叉验证来更鲁棒地评估模型性能和选择超参数。
-
超参数搜索 (Hyperparameter Search):
- 学习率、批处理大小、训练轮数、权重衰减等超参数对微调结果影响很大。
- 通常需要进行一定的实验来找到最佳组合。可以使用网格搜索 (Grid Search)、随机搜索 (Random Search) 或更高级的贝叶斯优化等方法。
- 从合理的默认值或文献中的推荐值开始。
-
计算资源限制 (Computational Resource Constraints):
- 全参数微调大型 LLM 对 GPU 显存要求非常高。
- 梯度累积 可以帮助在显存不足时模拟大批次训练。
- 混合精度训练 可以减少显存占用和加速训练。
- 分布式训练 (Data Parallelism): 如果有多张 GPU,可以使用数据并行来分担计算负载(但每个 GPU 仍需加载完整模型,显存瓶颈仍在)。
- DeepSpeed ZeRO 或 PyTorch FSDP: 更高级的分布式训练技术,可以将模型参数、梯度和优化器状态分片到多个 GPU 上,从而支持微调远超单 GPU 显存容量的模型。
- 考虑 PEFT: 如果资源实在有限,PEFT 是更实际的选择。
-
Tokenizer 的一致性 (Tokenizer Consistency):
- 必须使用与预训练模型完全相同的 Tokenizer 进行微调和后续的推理。
- 如果 Tokenizer 不匹配,会导致输入表示错误,模型性能会急剧下降。
-
实验的可复现性 (Reproducibility):
- 设置随机种子 (Python, NumPy, PyTorch/TensorFlow) 以确保实验结果可以复现。
- 记录好所有的超参数配置和实验环境。
全参数微调虽然强大,但也需要精心设计和细致调整。理解这些细节和注意事项,将帮助你更有效地利用预训练模型的强大能力,并避免常见的陷阱。
Adapter Tuning
-
核心思想: 在预训练 Transformer 模型的每一层(或某些选定的层)中,插入一些小型的、可训练的神经网络模块,称为“适配器 (Adapters)”。在微调时,只训练这些新插入的 Adapter 模块的参数,而预训练模型的主体参数保持冻结。
-
提出动机:
- 解决全参数微调带来的高计算和存储成本问题。
- 减少灾难性遗忘,因为大部分预训练知识被保留在冻结的参数中。
- 实现一个基础模型服务于多个下游任务,每个任务只需要一个轻量级的 Adapter 模块。
Adapter 模块的结构:
一个典型的 Adapter 模块通常包含以下组件,按顺序排列:
-
下投影 (Down-projection) / 瓶颈层 (Bottleneck Layer):
- 一个全连接层(线性层),将 Transformer 层的高维输出(例如,
d_model
,如 768)投影到一个低维空间(例如,d_adapter
,如 64 或更小)。 - 这个低维空间的大小(瓶颈维度)是一个重要的超参数,它控制了 Adapter 模块的参数量。
- 作用: 压缩信息,减少后续计算量和参数量。
- 一个全连接层(线性层),将 Transformer 层的高维输出(例如,
-
非线性激活函数 (Non-linearity):
- 在下投影之后,通常会接一个非线性激活函数,如 ReLU, GeLU, SiLU 等。
- 作用: 增加模型的表达能力,使得 Adapter 能够学习更复杂的函数。
-
上投影 (Up-projection):
- 另一个全连接层,将低维空间的表示再投影回原始的 Transformer 层输出维度 (
d_model
)。 - 作用: 将 Adapter 处理后的信息恢复到与原始 Transformer 流一致的维度,以便后续的残差连接。
- 另一个全连接层,将低维空间的表示再投影回原始的 Transformer 层输出维度 (
-
残差连接 (Residual Connection):
- Adapter 模块的输出会通过一个残差连接与原始 Transformer 层的输出(即 Adapter 模块的输入)相加。
- 公式:
Output_Layer = Input_Layer + Adapter_Output
- 作用:
- 保证了即使 Adapter 初始化为零(或其输出很小),原始的预训练模型信息流仍然可以顺畅通过,使得训练更稳定,并保留预训练知识。
- 允许 Adapter 模块只学习对原始表示的“修正”或“补充”。
Adapter 模块在 Transformer 层中的位置:
Adapter 模块通常被插入到 Transformer Encoder 或 Decoder 层的两个关键位置:
-
在多头注意力 (Multi-Head Attention, MHA) 子层之后,但在第一个残差连接和层归一化 (LayerNorm) 之前。
- 更准确地说,是 MHA 的输出经过一个 Dropout 和残差连接后,再输入到 Adapter,Adapter 的输出再与 MHA 的输入进行残差连接,然后进行 LayerNorm。
- 有些实现是 MHA -> Dropout -> Add(MHA_input) -> LayerNorm -> Adapter。
- 还有一种常见的做法是:MHA -> Dropout -> Adapter -> Add(MHA_input) -> LayerNorm。
- (原始论文中是
h + Adapter(LN(h))
的形式,即先 LayerNorm 再 Adapter,但后续实现略有不同)
-
在前馈神经网络 (Feed-Forward Network, FFN) 子层之后,但在第二个残差连接和层归一化之前。
- 与 MHA 后的插入方式类似。
- FFN -> Dropout -> Adapter -> Add(FFN_input) -> LayerNorm。
示意图 (简化版,以 FFN 后的 Adapter 为例):
Input_to_FFN (来自前面的层或MHA的输出)
|
Feed-Forward Network (FFN) <-- 冻结 (Frozen)
|
Output_from_FFN
|
---------------------
| Adapter Module | <-- 只训练这里 (Trainable)
| | |
| Down-projection |
| | |
| Non-linearity |
| | |
| Up-projection |
---------------------
| (Adapter_Output)
|
Residual Connection: Output_from_FFN + Adapter_Output
|
Layer Normalization
|
Output_of_Transformer_Layer (送往下一层)
Adapter Tuning 的操作流程:
- 加载预训练模型: 获取一个预训练好的 LLM。
- 冻结预训练参数: 将 LLM 的所有原始参数设置为不可训练 (requires_grad = False)。
- 插入 Adapter 模块:
- 遍历模型中的 Transformer 层(或选定的层)。
- 在每个选定层中的指定位置(如 MHA 后和 FFN 后)实例化并插入 Adapter 模块。
- Adapter 模块的参数是可训练的 (requires_grad = True)。
- 准备特定任务的数据集。
- 训练:
- 只训练 Adapter 模块的参数以及可能的模型头部 (Task-specific Head,如果需要的话)。
- 使用较小的学习率。
- 损失函数根据具体任务定义。
- 推理/部署:
- 加载预训练模型和训练好的特定任务的 Adapter 权重。
- 或者,如果需要,可以将 Adapter 的权重“融合”回模型中(但不常见,因为 Adapter 的设计初衷就是模块化)。
优点 (回顾 PEFT 的通用优点,并针对 Adapter):
- 参数效率极高: 只需训练和存储非常少量的 Adapter 参数 (通常只占原始模型参数的 0.5% - 5%)。
- 计算成本低: 训练速度快,显存占用小。
- 模块化: 可以为每个任务训练一个独立的 Adapter,基础模型保持不变。这使得添加新任务非常方便,并且可以轻松切换不同任务的 Adapter。
- 有效缓解灾难性遗忘: 预训练模型的主体被冻结,其通用知识得以保留。
- 性能接近全参数微调: 在许多任务上,精心设计的 Adapter 能够达到与全参数微调相近甚至相当的性能,尤其是在中等规模的数据集上。
缺点与挑战:
- 引入额外的推理延迟: 虽然 Adapter 模块很小,但在每一层都进行额外的计算(两次线性投影和一次激活)会不可避免地增加推理时的总计算量和延迟。对于延迟敏感的应用,这可能是一个问题。
- 最佳插入位置和结构: Adapter 应该插入到哪些层?是所有层还是部分层?Adapter 内部的结构(瓶颈维度、激活函数)如何选择?这些都需要实验和调整。
- 瓶颈维度的选择:
- 瓶颈维度太小,Adapter 的表达能力可能不足,无法充分适应新任务。
- 瓶颈维度太大,参数量增加,失去了 PEFT 的部分优势,也可能导致过拟合。
- 与某些模型架构的兼容性: 虽然 Adapter 的思想很通用,但在特定模型架构上实现和优化可能需要额外的工作。
变体与发展:
Adapter Tuning 的思想催生了很多后续的研究和改进:
- AdapterFusion (Pfeiffer et al., 2020): 提出了一种在推理时组合多个已训练好的任务 Adapter 的方法,以期在目标任务上获得更好的性能,或者实现零样本跨任务泛化。它通过学习一个注意力机制来动态地加权不同 Adapter 的输出。
- AdapterDrop (Rücklé et al., 2020): 提出在训练和推理时随机丢弃一些 Adapter 层,以提高效率和鲁棒性,减少推理延迟。
- LoRA (Low-Rank Adaptation): 虽然机制不同(不是插入新模块,而是修改现有权重的更新方式),但其目标和很多优点与 Adapter 类似,并且在实践中表现非常出色,可以看作是 PEFT 发展的一个重要方向。
- (IA)^3 (Infused Adapter by Inhibiting and Amplifying Inner Activations): 也是一种轻量级的调整方式,通过学习三个缩放向量来调整 FFN 和注意力机制中的激活值。
总结:
Adapter Tuning 是一种开创性的 PEFT 方法,它通过在预训练模型中插入小型可训练模块,实现了在保持大部分预训练参数不变的情况下,高效地将模型适应到下游任务。它显著降低了微调的成本,并为后续更先进的 PEFT 技术提供了宝贵的思路。
理解 Adapter 的核心思想——“冻结主干,插件式学习”——对于掌握其他 PEFT 方法也非常有帮助。
Adapter Tuning 的一些具体实现细节
1. Adapter 模块的参数初始化
- 目标: Adapter 模块在训练开始时不应该对预训练模型的原始行为产生大的干扰。理想情况下,如果 Adapter 的输出接近于零,那么由于残差连接的存在,原始 Transformer 层的输出将几乎不受影响。
- 常见做法:
- 下投影层 (Down-projection) 的权重: 通常使用接近于零的初始化,例如从一个均值为0、标准差较小(如0.01或更小)的正态分布中采样。或者使用 Xavier/Kaiming 初始化,然后乘以一个很小的常数。
- 下投影层的偏置 (Bias): 通常初始化为零。
- 上投影层 (Up-projection) 的权重: 关键在于确保其初始输出接近于零。 因此,上投影层的权重通常也初始化为零,或者从一个非常小的分布中采样。
- 上投影层的偏置 (Bias): 通常初始化为零。
- 效果: 这样的初始化策略使得在训练初期,
Adapter_Output
非常小,Output_Layer ≈ Input_Layer
(这里的Input_Layer
指的是 Adapter 模块的输入,即原始 Transformer 子层的输出)。这有助于训练的稳定性,并让模型从一个更接近预训练模型行为的状态开始学习。
2. 瓶颈维度 (Bottleneck Dimension / d_adapter
) 的选择
- 这是 Adapter 模块最重要的超参数之一。它直接决定了 Adapter 的参数量和潜在的表达能力。
- 参数量计算: 如果原始 Transformer 层的维度是
d_model
,瓶颈维度是d_adapter
,那么一个 Adapter 模块(包含下投影和上投影)的参数量大约是2 * d_model * d_adapter + d_adapter + d_model
(权重 + 偏置)。可以看到,d_adapter
越小,参数量越少。 - 选择范围:
d_adapter
通常远小于d_model
。常见的值可能在 16, 32, 64, 128, 256 等,具体取决于d_model
的大小和任务的复杂度。- 例如,对于
d_model = 768
(如 BERT-base),d_adapter
可能选择 32 或 64。 - 对于
d_model = 1024
(如 BERT-large),可能会选择 64 或 128。
- 例如,对于
- 权衡:
- 较小的
d_adapter
: 参数更少,训练更快,存储更小,更不容易过拟合,但可能表达能力不足,无法充分学习任务特征。 - 较大的
d_adapter
: 参数更多,表达能力更强,但训练和存储成本增加,也更容易过拟合,尤其是在数据量较少的情况下。
- 较小的
- 实践: 通常需要通过实验在验证集上调整
d_adapter
来找到最佳值。可以从一个较小的值开始尝试,然后逐渐增加。
3. 非线性激活函数的选择
- 常见的选择包括 ReLU, GeLU, SiLU (Swish), Tanh 等。
- GeLU (Gaussian Error Linear Unit) 和 SiLU 是现代 Transformer 模型中常用的激活函数,因为它们在实践中表现良好。
- 选择哪个激活函数通常不是 Adapter 性能的关键瓶颈,但与预训练模型主体使用的激活函数保持一致性或者选择一个被广泛证明有效的激活函数是比较稳妥的做法。
4. Adapter 的插入位置与策略
- 标准位置: 如前所述,通常在每个 Transformer 层的 MHA 子层之后和 FFN 子层之后各插入一个 Adapter 模块。
- 变体:
- 只在 FFN 后插入: 有些研究发现,只在 FFN 子层后插入 Adapter 也能达到不错的效果,并且可以进一步减少参数量和推理延迟。
- Pfeiffer
style Adapters (AdapterDrop 论文中提到的一种更轻量级的配置):
- 将 Adapter 模块设计为
LN -> Down-proj -> ReLU -> Up-proj -> Dropout -> Add
,然后这个整体替换掉原始的 FFN 层,或者在 FFN 之前/之后加入。 - 或者只在 FFN 内部的两个线性层之间插入一个更小的 Adapter 结构。
- 将 Adapter 模块设计为
- 只在顶层或部分层插入: 对于某些任务,可能不需要在所有 Transformer 层都插入 Adapter,只在模型的顶几层或对任务贡献较大的层插入可能就足够了。这需要具体实验。
- 串行 vs. 并行 Adapter (AdapterFusion 的概念):
- 串行 (Sequential): 标准的 Adapter 插入方式,Adapter 模块按顺序处理信息。
- 并行 (Parallel): AdapterFusion 提出可以将多个任务的 Adapter 并行放置,并通过一个学习到的门控机制来融合它们的输出。这主要用于组合多个已训练的 Adapter。
5. Layer Normalization (层归一化) 的处理
- 原始 Transformer 层中的 LayerNorm 保持冻结: 通常,预训练模型中原有的 LayerNorm 层的参数 (gamma 和 beta) 是保持冻结的,因为它们也包含了预训练学到的重要统计信息。
- Adapter 内部是否需要 LayerNorm:
- 大多数经典的 Adapter 设计中,Adapter 模块内部不包含额外的 LayerNorm 层。
- 但也有一些变体或更复杂的 Adapter 设计可能会在 Adapter 内部的特定位置加入 LayerNorm,但这会增加参数量和复杂性。
- 主要的 LayerNorm 操作还是依赖于 Transformer 主干网络中的 LayerNorm 层。
6. 训练细节
- 学习率: 通常比全参数微调还要小一些,或者在相似的范围内 (例如
1e-4
到5e-4
,或者5e-5
到3e-4
)。因为 Adapter 参数量少,可能对学习率更敏感。也需要实验调整。 - 优化器: AdamW 仍然是常用的选择。
- 批处理大小和训练轮数: 与全参数微调类似,根据任务和数据量调整。由于参数少,可能收敛更快,需要的轮数也可能更少。
- 只训练 Adapter 参数: 务必确保只有 Adapter 模块的参数
requires_grad = True
,而预训练模型的主体参数requires_grad = False
。可以通过遍历模型参数并检查其名称或类型来设置。 - 模型头部 (Task-specific Head): 如果任务需要一个分类头或其他特定任务的头部,这个头部的参数通常也是可训练的,并且与 Adapter 参数一起训练。
7. 实现库和框架
adapter-transformers
库: 这是一个基于 Hugging Facetransformers
库的扩展,专门用于方便地使用各种 Adapter 方法(包括原始 Adapter, AdapterFusion, AdapterDrop 等)。它极大地简化了 Adapter 的加载、插入和训练过程。如果你想实际使用 Adapter,强烈推荐了解这个库。- 它允许你通过简单的配置字符串来加载带有特定 Adapter 配置的模型。
- 支持 Adapter Hub,可以共享和下载预训练的 Adapter 模块。
- Hugging Face
peft
库: 虽然peft
库更侧重于 LoRA, Prefix Tuning, Prompt Tuning 等,但 Adapter 的思想是这些方法的基础。理解 Adapter 有助于理解peft
库中其他方法的动机。一些更现代的、与 Adapter 思想类似的轻量级调整方法也可能被peft
支持。
示例:如何检查哪些参数是可训练的 (PyTorch)
# 假设 model 是你加载并插入了 Adapter 的模型
total_params = 0
trainable_params = 0
for name, param in model.named_parameters():
total_params += param.numel()
if param.requires_grad:
trainable_params += param.numel()
print(f"Trainable parameter: {name}, Shape: {param.shape}")
print(f"Total parameters: {total_params}")
print(f"Trainable parameters: {trainable_params}")
print(f"Percentage of trainable parameters: {100 * trainable_params / total_params:.2f}%")
通过运行类似上面的代码,你可以验证是否只有 Adapter 模块(以及可能的分类头)的参数是可训练的。
总结一下实现细节的关键点:
- 精心初始化 Adapter 权重,使其初始输出接近零。
- 合理选择瓶颈维度,平衡参数量和表达能力。
- 通常在 MHA 和 FFN 之后插入 Adapter。
- 冻结预训练模型主体,只训练 Adapter 和任务头。
- 使用较小的学习率。
- 利用现有库 (如
adapter-transformers
) 可以大大简化实现。
理解这些细节能帮助你在实际项目中更有效地应用和调试 Adapter Tuning。这是一个非常优雅且实用的 PEFT 方法。
核心概念:软提示 (Soft Prompts) vs. 硬提示 (Hard Prompts)
在我们之前讨论 Prompt 工程时,主要指的是“硬提示”,即人工设计的、离散的文本指令。
-
硬提示 (Hard Prompts):
- 由人类设计的自然语言文本。
- 例如:"将以下英文翻译成中文:"
- 优点:直观,易于理解和创建。
- 缺点:设计优秀硬提示需要技巧和大量实验;离散的 token 可能不是最优的引导方式;难以通过梯度下降进行端到端的优化。
-
软提示 (Soft Prompts) / 连续提示 (Continuous Prompts):
- 不是离散的文本 token,而是一系列可学习的连续向量 (embeddings)。
- 这些向量与模型的词嵌入向量具有相同的维度。
- 它们作为模型输入的一部分,但其值是通过梯度下降在训练过程中学习得到的,而不是人工指定的。
- 优点:
- 可以通过梯度直接优化,可能比人类设计的硬提示更有效。
- 参数量非常小。
- 为模型提供了更灵活的引导方式。
Prefix Tuning 和 Prompt Tuning 都属于使用软提示的方法。
Prefix Tuning
-
核心思想: 在 Transformer 模型的每一层的 Key (K) 和 Value (V) 向量前,都添加一段可学习的、特定于任务的向量序列(称为 "prefix")。这些 prefix 向量作为上下文,引导模型在该层注意力机制的行为。
-
提出者: Li and Liang, 2021 ("Prefix-Tuning: Optimizing Continuous Prompts for Generation")
-
针对的模型架构: 最初主要针对自回归模型 (如 GPT) 和 Encoder-Decoder 模型 (如 BART, T5) 的文本生成任务进行优化。
-
如何工作 (以 Decoder-only 模型如 GPT 为例):
- 预训练模型参数冻结: 原始 Transformer 模型的权重保持不变。
- Prefix 向量:
- 为 Transformer 的每一层都创建一组可学习的 prefix 向量。
- 这些 prefix 向量的长度
L_p
是一个超参数。 - 每个 prefix 向量的维度与该层 Key 和 Value 向量的维度相同 (
d_k
或d_model / num_heads
)。 - 所以,对于一个有
N
层的 Transformer 模型,总共需要学习N * 2 * L_p * d_k
个参数(2 代表 Key 和 Value 各有一组 prefix)。
- 修改注意力计算:
- 在原始的注意力计算
Attention(Q, K, V)
中:- 原始的 Key 序列
K
变为[P_k; K]
(其中P_k
是该层的可学习 Key prefix 向量序列)。 - 原始的 Value 序列
V
变为[P_v; V]
(其中P_v
是该层的可学习 Value prefix 向量序列)。 - Query (Q) 仍然由输入序列的隐藏状态产生。
- 原始的 Key 序列
- 这样,当计算注意力时,Query 会与这些可学习的 prefix Key 进行交互,并从 prefix Value 中提取信息。
- 在原始的注意力计算
- Prefix 向量的参数化:
- 直接为每一层学习独立的 prefix 向量参数量可能仍然较大。
- 论文中提出使用一个较小的重参数化网络 (reparameterization network),通常是一个小型的 MLP(例如,输入是 prefix 的索引,输出是 prefix 向量),来生成实际的 prefix 向量。这可以减少直接存储和学习的参数数量,并可能提高稳定性。但更简单的实现是直接学习这些 prefix 向量。
- 训练: 只训练这些 prefix 向量(或者重参数化网络的参数)。
-
图示理解 (简化版,单层注意力):
原始输入序列: X = [x_1, x_2, ..., x_n] 经过Embedding和前面层处理得到: H = [h_1, h_2, ..., h_n] (作为Q, K, V的来源) 可学习的Prefix (长度 L_p): P_k = [pk_1, pk_2, ..., pk_Lp] (Key Prefix) P_v = [pv_1, pv_2, ..., pv_Lp] (Value Prefix) 注意力计算时: Query 依然来自 H (例如 q_i 来自 h_i) Key' = Concat(P_k, K_from_H) Value' = Concat(P_v, V_from_H) Attention_Output = Attention(Query, Key', Value')
这个过程在 Transformer 的每一层都会发生,每一层都有自己独立的、可学习的
P_k
和P_v
。 -
优点:
- 参数高效: 只需学习 prefix 向量,参数量远小于全参数微调 (例如,对于 GPT-2 Medium,参数量可以减少1000倍以上)。
- 性能接近全参数微调: 在文本生成任务上,尤其是在低数据场景下,Prefix Tuning 能够达到与全参数微调相当甚至更好的性能。
- 模块化: 每个任务对应一组 prefix 向量,易于管理。
- 不修改模型主干: 预训练模型完全冻结。
-
缺点与挑战:
- 在每一层都添加前缀: 尽管每个前缀向量维度不高,但层数多了,总的 prefix 参数量相对于更轻量的 Prompt Tuning 还是会多一些。
- 训练稳定性: 学习这些连续向量有时可能不稳定,重参数化技巧有助于缓解。
- 理解其工作机制: 连续向量如何精确地引导模型行为,其可解释性不如硬提示。
Prompt Tuning (Soft Prompts)
-
核心思想: 只在模型的输入嵌入层 (input embedding layer) 前添加一段可学习的、特定于任务的连续向量序列 (soft prompt)。这些 soft prompt 向量会与原始的输入 token 嵌入拼接在一起,共同作为 Transformer 第一层的输入。与 Prefix Tuning 不同,Prompt Tuning 不在模型的每一层内部添加可学习参数。
-
提出者: Lester et al., 2021 ("The Power of Scale for Parameter-Efficient Prompt Tuning")
-
针对的模型架构: 可以应用于各种 Transformer 架构 (Encoder-only, Decoder-only, Encoder-Decoder)。
-
如何工作:
- 预训练模型参数冻结: 原始 Transformer 模型的权重保持不变。
- Soft Prompt 向量:
- 创建一组可学习的 prompt 向量
P_emb = [p_1, p_2, ..., p_L_prompt]
。 - 这些 prompt 向量的长度
L_prompt
是一个超参数 (例如,5, 20, 100 个 prompt token)。 - 每个 prompt 向量
p_i
的维度与模型的词嵌入向量维度相同 (d_model
)。
- 创建一组可学习的 prompt 向量
- 修改模型输入:
- 原始输入 token 序列
X = [x_1, x_2, ..., x_n]
经过词嵌入层得到E_X = [e_x1, e_x2, ..., e_xn]
。 - 将可学习的 soft prompt 向量
P_emb
与E_X
拼接起来,形成新的输入嵌入序列:Input_Embeddings' = Concat(P_emb, E_X) = [p_1, ..., p_L_prompt, e_x1, ..., e_xn]
- 这个拼接后的嵌入序列将作为 Transformer 第一层的输入。
- 原始输入 token 序列
- 训练: 只训练这些 soft prompt 向量
P_emb
的参数。
-
图示理解:
可学习的Soft Prompt (长度 L_prompt): P_emb = [p_1, p_2, ..., p_L_prompt] (每个 p_i 是一个 d_model 维向量) 原始输入Token序列: X = [x_1, x_2, ..., x_n] 经过词嵌入层: E_X = [e_x1, e_x2, ..., e_xn] 拼接后的输入 (送入Transformer第一层): Input_Embeddings' = [p_1, ..., p_L_prompt, e_x1, ..., e_xn]
-
优点:
- 极其参数高效: 参数量是所有 PEFT 方法中最少的之一。只需要学习
L_prompt * d_model
个参数。例如,对于一个 100B 参数的模型,如果L_prompt=20
且d_model=4096
(典型值),那么可训练参数只有约 8 万个,几乎可以忽略不计。 - 实现简单: 概念和实现都非常直接。
- 易于存储和共享: 每个任务只需要存储非常小的 prompt 向量。
- 随着模型规模增大效果越好: 论文 "The Power of Scale for Parameter-Efficient Prompt Tuning" 表明,当预训练模型规模足够大时(例如超过 10B 参数),Prompt Tuning 的性能可以追平甚至超过全参数微调。对于较小的模型,Prompt Tuning 的效果可能不如全参数微调或其他 PEFT 方法(如 Adapter 或 LoRA)。
- 不修改模型主干,推理时无额外计算层: 与 Adapter 不同,Prompt Tuning 不在模型内部增加计算层,只是增加了输入序列的有效长度,因此推理时的计算开销增加非常小 (主要来自序列长度增加)。
- 极其参数高效: 参数量是所有 PEFT 方法中最少的之一。只需要学习
-
缺点与挑战:
- 对小模型效果可能不佳: 对于规模较小的预训练模型,Prompt Tuning 可能难以达到与全参数微调或其他更“重”的 PEFT 方法相当的性能。模型的表达能力可能不足以仅通过输入端的少量提示向量就被充分引导。
- 初始化和优化敏感性: Prompt 向量的初始化方法和优化过程可能对最终性能有较大影响。
- Prompt 长度的选择:
L_prompt
的长度是一个需要调整的超参数。太短可能不足以引导模型,太长则增加少量参数并可能引入不必要的复杂性。 - 可解释性依然是挑战。
Prefix Tuning vs. Prompt Tuning (Soft Prompts) - 关键区别总结:
特性 | Prefix Tuning | Prompt Tuning (Soft Prompts) |
---|---|---|
可学习参数位置 | 在 Transformer 每一层的 Key 和 Value 前添加前缀 | 只在输入嵌入层前添加提示向量 |
影响范围 | 影响每一层的注意力计算 | 只直接影响第一层的输入,间接影响后续层 |
参数量 | 相对较少 (但比 Prompt Tuning 多) | 极少 |
对模型结构的修改 | 需要修改每一层的注意力输入 | 只需要修改模型的最终输入嵌入序列 |
对小模型性能 | 通常比 Prompt Tuning 在小模型上表现更好 | 在小模型上性能可能不如 Prefix Tuning 或其他 PEFT 方法 |
对大模型性能 | 表现良好 | 在超大模型上可以媲美全参数微调 |
推理开销增加 | 略微增加 (因每一层都有额外 K, V) | 极小增加 (因输入序列变长) |
哪个更好?
- 如果模型规模巨大 (例如 >10B 参数),且追求极致的参数效率和简单性,Prompt Tuning 是一个非常有吸引力的选择。
- 如果模型规模适中,或者任务比较复杂,Prefix Tuning 或其他 PEFT 方法 (如 Adapter, LoRA) 可能提供更强的性能,尽管参数量比 Prompt Tuning 多一些。
- 实际选择通常需要根据具体模型、任务、数据量和计算资源进行实验评估。
P-Tuning (v1 & v2):
值得一提的是,与 Prompt Tuning 和 Prefix Tuning 相关的还有 P-Tuning (Liu et al., 2021) 及其后续的 P-Tuning v2 (Liu et al., 2022)。
- P-Tuning (v1): 类似于 Prompt Tuning,但在输入端使用一个小的 LSTM 或 MLP 来生成可学习的 prompt 嵌入,并发现其在 NLU 任务上效果较好,但对于复杂的序列到序列任务效果不如 Prefix Tuning。
- P-Tuning v2: 更接近于 Prefix Tuning 的思想,将可学习的 prompt 向量也应用到模型的每一层 (或者说,让每一层都能直接访问和利用这些 prompt token),而不仅仅是输入层。它旨在结合 Prefix Tuning 的性能优势和 Prompt Tuning 的简单性与稳定性,并声称在各种模型规模和任务上都能取得良好效果,且实现更简单。可以看作是深度版本的 Prompt Tuning,或者一种更精简的 Prefix Tuning 实现。
这些方法都体现了通过操纵模型的输入表示或内部激活来高效引导大型预训练模型的共同趋势。
LoRA (Low-Rank Adaptation)
LoRA (Low-Rank Adaptation of Large Language Models)
-
核心思想: 微软的研究者发现,大型语言模型在进行全参数微调时,其权重的变化量 (ΔW) 往往具有低的“内在秩” (low intrinsic rank)。也就是说,尽管模型参数很多,但为了适应新任务,真正需要改变的“信息方向”其实是有限的,可以用一个远小于原始参数矩阵秩的低秩矩阵来近似这个变化。 LoRA 的做法是,冻结预训练模型的原始权重 (W),并在模型的特定层(通常是 Transformer 中的权重矩阵,如注意力机制中的查询 Q、键 K、值 V、输出 O 的线性投影层,以及前馈网络 FFN 中的线性层)旁边,注入两个小的、可训练的低秩“分解矩阵”A 和 B。这两个矩阵的乘积 (BA) 用来近似权重在微调时的变化量 (ΔW)。
-
数学表示: 对于一个预训练的权重矩阵
W_0 ∈ R^(d×k)
,全参数微调会更新它得到W_0 + ΔW
。 LoRA 假设ΔW
可以被低秩分解:ΔW ≈ B A
其中:A ∈ R^(r×k)
是一个随机高斯初始化后固定的矩阵 (或者有些实现中 A 也是可训练的,但更常见的是 A 随机高斯,B 初始化为零,然后训练 A 和 B)。更准确地说,论文中 A 是随机高斯初始化,B 初始化为零,然后只训练 A 和 B。B ∈ R^(d×r)
是一个初始化为零的矩阵。r
是 LoRA 的秩 (rank),这是一个非常重要的超参数,且r << min(d, k)
(远小于原始矩阵的维度)。例如,r
可能取 4, 8, 16, 32, 64 等。- 所以,在微调过程中,模型的输出
h
变为:h = W_0 x + ΔW x = W_0 x + B A x
这里x
是输入。 实际上,计算时是h = W_0 x + (B (A x))
,即先用A
对x
进行降维,再用B
升维。
-
如何工作 (以单个线性层为例):
- 冻结原始权重
W_0
: 预训练模型的权重保持不变。 - 注入 LoRA 模块:
- 为选定的线性层(例如,注意力中的
W_q
,W_k
,W_v
投影层)添加两个新的可训练矩阵A
和B
。 A
的作用是将输入x
从k
维投影到低秩r
维。B
的作用是将r
维表示投影回d
维。
- 为选定的线性层(例如,注意力中的
- 计算:
- 原始路径:
h_0 = W_0 x
- LoRA 路径:
h_lora = B (A x)
- 最终输出:
h = h_0 + h_lora = W_0 x + B A x
- 原始路径:
- 训练: 只训练矩阵
A
和B
的参数。由于r
很小,A
和B
的参数量远小于原始W_0
的参数量。A
的参数量是r * k
B
的参数量是d * r
- 总共可训练参数是
r * (d + k)
,这远小于d * k
。
- 冻结原始权重
-
图示理解 (单个线性层):
Input x (k-dimensional) | |--------------------------| | | Pre-trained (Frozen) LoRA Path (Trainable) Weight W_0 | | | h_0 = W_0 x Matrix A (r x k) | (A x, r-dimensional) Matrix B (d x r) | (B (A x), d-dimensional) h_lora = B (A x) | | |--------ADD-------------| (Element-wise sum) | Output h (d-dimensional)
LoRA 的关键优势和特点:
-
极高的参数效率:
- 可训练参数数量大大减少。例如,对于一个拥有数十亿参数的 GPT-3 模型,使用 LoRA (如
r=8
) 可能只需要训练几百万个参数,减少了几个数量级。 - 这使得在单张消费级 GPU 上微调非常大的模型成为可能。
- 可训练参数数量大大减少。例如,对于一个拥有数十亿参数的 GPT-3 模型,使用 LoRA (如
-
显著降低存储成本:
- 对于每个下游任务,只需要存储很小的 LoRA 权重 (
A
和B
),而不是整个模型的副本。基础的预训练模型可以被所有任务共享。 - 例如,一个 7B 参数的模型,其 LoRA 权重可能只有几 MB 到几十 MB。
- 对于每个下游任务,只需要存储很小的 LoRA 权重 (
-
可与全参数微调媲美的性能:
- 大量实验表明,在许多任务上,精心调整的 LoRA 能够达到与全参数微调非常接近甚至相当的性能,有时甚至更好(尤其是在数据量较少或需要避免过拟合的情况下)。
-
无额外的推理延迟 (通过权重合并):
- 这是 LoRA 相对于 Adapter 等方法的一个显著优点。
- 在训练完成后,可以将学习到的 LoRA 权重
BA
与原始的预训练权重W_0
直接合并成一个新的权重矩阵W' = W_0 + BA
。 - 在推理时,只需要使用这个合并后的权重矩阵
W'
进行计算,其结构与原始模型完全相同,因此不会引入任何额外的计算层或推理延迟。 - 这意味着你可以享受 PEFT 带来的训练效率,同时在部署时获得与原始模型相当的推理速度。
-
易于实现和集成:
- LoRA 的概念相对简单,可以很容易地集成到现有的 Transformer 模型代码中。Hugging Face 的
peft
库提供了非常方便的 LoRA 实现。
- LoRA 的概念相对简单,可以很容易地集成到现有的 Transformer 模型代码中。Hugging Face 的
-
有效缓解灾难性遗忘:
- 由于预训练权重
W_0
被冻结,模型在预训练阶段学到的通用知识得到了很好的保留。
- 由于预训练权重
-
任务切换方便:
- 可以为不同的任务训练不同的 LoRA 权重。在推理时,根据当前任务加载对应的 LoRA 权重(或者直接加载合并后的权重)。
LoRA 的超参数:
-
r
(Rank): LoRA 的秩。这是最重要的超参数。r
越小,可训练参数越少,训练越快,但可能欠拟合,无法充分捕捉任务特定的变化。r
越大,可训练参数越多,表达能力越强,但训练成本增加,也可能过拟合。- 常见的值有 4, 8, 16, 32, 64。通常
r=8
或r=16
在很多情况下能取得不错的平衡。 - 需要根据具体任务和模型进行实验调整。
-
lora_alpha
: 一个缩放因子。在 LoRA 的计算中,ΔWx
通常被缩放为(lora_alpha / r) * BAx
。lora_alpha
可以看作是一个调整 LoRA 路径贡献大小的超参数。- 通常,将
lora_alpha
设置为与r
相同的值(例如,如果r=16
,则lora_alpha=16
)是一个常见的做法,这样缩放因子就是 1。但也可以将其视为一个独立的超参数进行调整。 - 有些实现中,这个缩放因子是固定的,或者不显式暴露。
-
lora_dropout
: 在 LoRA 模块的A
矩阵之后(或B
矩阵的输出)可以添加一个 Dropout 层,以防止过拟合。其值通常设置在 0.05 到 0.1 之间,或者根据验证集调整。 -
target_modules
: 指定在模型的哪些模块(哪些类型的线性层)上应用 LoRA。- 通常会选择 Transformer 中的关键线性层,如:
- 注意力机制中的
query
,key
,value
投影层 (常表示为q_proj
,k_proj
,v_proj
或Wq
,Wk
,Wv
)。 - 注意力机制的输出投影层 (
o_proj
或Wo
)。 - 前馈网络 (FFN) 中的线性层 (
gate_proj
,up_proj
,down_proj
或fc1
,fc2
)。
- 注意力机制中的
- 选择哪些模块应用 LoRA 也会影响性能和参数量。通常,对更多的线性层应用 LoRA (尤其是 QKV) 会带来更好的性能,但参数量也会相应增加。
- 通常会选择 Transformer 中的关键线性层,如:
应用 LoRA 的步骤 (使用 Hugging Face peft
库为例):
- 加载预训练模型:
model = AutoModelForCausalLM.from_pretrained(...)
- 定义 LoRA 配置:
from peft import LoraConfig, get_peft_model config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], # 根据你的模型具体层名调整 lora_dropout=0.05, bias="none", # 通常不训练偏置项,或只训练 LoRA 模块的偏置 task_type="CAUSAL_LM" # 或 "SEQ_2_SEQ_LM", "TOKEN_CLS" 等 )
- 获取 PEFT 模型:
model = get_peft_model(model, config)
这一步会自动将 LoRA 模块注入到target_modules
指定的层,并冻结其他参数。 - 打印可训练参数:
model.print_trainable_parameters()
(验证可训练参数数量是否符合预期) - 准备数据并进行训练: 正常进行模型训练,但只有 LoRA 参数会被更新。
- (可选) 合并权重进行部署:
- 训练完成后,可以调用
model = model.merge_and_unload()
将 LoRA 权重合并回基础模型,以便获得无额外延迟的推理模型。
- 训练完成后,可以调用
总结:
LoRA 是一种非常强大且实用的 PEFT 技术。它通过低秩分解的思想,在保持预训练模型大部分参数不变的情况下,高效地微调大型语言模型。其核心优势在于参数效率高、存储成本低、性能接近全参数微调,并且可以通过权重合并实现无额外推理延迟。这使得 LoRA 成为当前 LLM 微调领域的主流选择之一。
理解 LoRA 的原理和关键超参数,将对你进行 LLM 的高效微调非常有帮助!
LoRA 变体和相关技术:
1. QLoRA (Quantized LoRA)
- 提出者: Dettmers et al., 2023 ("QLoRA: Efficient Finetuning of Quantized LLMs")
- 核心思想: 在冻结的、4-bit 量化的预训练大语言模型 (LLM) 的基础上,再插入和训练 LoRA 适配器。 目标是进一步降低微调大型 LLM 的显存消耗,使得在单张消费级 GPU (如 24GB 或 48GB 显存) 上也能微调非常巨大的模型 (例如 65B 参数的模型)。
- 关键技术点:
- 4-bit NormalFloat (NF4): 一种新的理论上信息最优的量化数据类型,专为正态分布的权重设计,比传统的 4-bit 整数或浮点量化能更好地保留信息。
- 双量化 (Double Quantization): 为了进一步节省显存,对量化常数 (quantization constants) 本身再次进行量化。
- 分页优化器 (Paged Optimizers): 利用 NVIDIA 统一内存 (Unified Memory) 的特性,在 GPU 显存不足以容纳所有优化器状态时,自动将一部分优化器状态分页到 CPU 内存,从而避免 OOM (Out-of-Memory) 错误,使得训练过程更加稳定。
- LoRA 适配器仍然使用 BF16 (或 FP16) 精度进行训练: 尽管基础模型是 4-bit 量化的,但 LoRA 模块的权重及其梯度计算仍然使用较高的精度 (如 BF16) 来保持微调的性能。在反向传播时,4-bit 量化的权重会被动态地反量化回 BF16,计算梯度,然后 LoRA 权重被更新。
- 优点:
- 极大地降低了微调超大模型的显存门槛。 例如,论文声称可以在单个 48GB GPU 上微调 65B 模型,或在单个 24GB GPU 上微调 33B 模型。
- 性能接近甚至优于 16-bit 全参数微调: 尽管基础模型被量化到 4-bit,但通过精心设计的量化方法和 LoRA 的结合,QLoRA 能够在许多基准测试上达到与 16-bit 全参数微调相当的性能。
- 实现: Hugging Face
bitsandbytes
库提供了 QLoRA 的核心量化功能,并与peft
库紧密集成。 - 重要性: QLoRA 是 LLM 民主化和平民化的一个重要里程碑,使得更多研究者和开发者能够接触和微调超大规模模型。
2. LoRA-FA (LoRA with Fast Attention)
- 背景: 虽然 LoRA 本身在推理时可以通过权重合并消除额外延迟,但在训练时,LoRA 路径的计算仍然会略微增加前向和后向传播的时间。
- 核心思想: 探索如何在训练阶段也减少 LoRA 引入的计算开销,特别是与注意力机制结合时。
- 具体方法可能包括:
- 优化 LoRA 矩阵乘法的计算方式。
- 与 FlashAttention 等高效注意力算子结合,确保 LoRA 的引入不会破坏这些优化。
- 探索是否可以对 LoRA 路径的计算进行进一步的近似或简化,以加速训练。
- 注意: "LoRA-FA" 这个术语可能不是一个广泛公认的、有单一明确定义的变体,而是指将 LoRA 与各种快速注意力技术结合的思路或实践。具体实现细节可能因研究或库而异。
- 目标: 在保持 LoRA 参数效率的同时,进一步提升训练时的计算效率。
3. AdaLoRA (Adaptive LoRA / Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning)
- 提出者: Zhang et al., 2023 ("AdaLoRA: Adaptive Budget Allocation for Parameter-Efficient Fine-Tuning")
- 核心思想: 动态地、自适应地为不同权重矩阵分配 LoRA 的秩 (rank) 或重要性。 传统 LoRA 对所有应用了 LoRA 的层使用固定的秩
r
。AdaLoRA 认为不同层或不同权重矩阵对下游任务的重要性是不同的,因此应该给更重要的权重分配更多的“参数预算”(即更大的有效秩),而给不那么重要的权重分配更少的预算。 - 关键技术点:
- 重要性评分: 在训练过程中,估计不同权重矩阵中 LoRA 模块的重要性。这可以通过分析梯度、激活值或参数的敏感性等方式进行。
- 参数预算分配: 根据重要性评分,动态地调整每个 LoRA 模块的秩或者其奇异值分解 (SVD) 中奇异值的数量。例如,通过对
ΔW = BA
中的B
和A
进行 SVD 分解B = U_B Σ_B V_B^T
和A = U_A Σ_A V_A^T
,然后根据重要性来裁剪(置零)那些不重要的奇异值,从而动态地减少有效秩。 - 迭代更新: 重要性评分和预算分配可以在训练过程中周期性地进行更新。
- 优点:
- 更精细的参数分配: 相比固定秩的 LoRA,AdaLoRA 能够更智能地利用有限的参数预算,将更多的可训练参数分配给对任务性能贡献更大的权重。
- 可能在相同参数预算下达到更好性能,或在达到相同性能时使用更少参数。
- 挑战:
- 实现相对更复杂,需要设计和计算重要性评分,并进行动态预算分配。
- 引入了新的超参数(如重要性度量方法、预算分配策略等)。
4. VeRA (Vector-based Random Matrix Adaptation)
- 提出者: Kopiczko et al., 2023 ("VeRA: Vector-based Random Matrix Adaptation")
- 核心思想: 在 LoRA 的基础上进一步减少可训练参数。LoRA 中的矩阵
A
和B
是可训练的。VeRA 提出,可以将其中一个矩阵(例如B
)固定为随机初始化的矩阵,而只训练另一个矩阵(例如A
),或者更进一步,将A
和B
都设置为由一对共享的可学习向量d
和b
(以及固定的随机矩阵)生成。 - 具体做法 (一种可能的简化形式):
ΔW ≈ b (d^T P)
或ΔW ≈ (P d) b^T
,其中P
是一个固定的随机投影矩阵,b
和d
是可学习的低维向量。- 这意味着
A
和B
矩阵的很多参数是共享的,或者由更少的自由参数生成。
- 优点:
- 比标准 LoRA 更少的训练参数: 如果
b
和d
的维度远小于r * d_model
,那么参数量会进一步减少。 - 声称在极低的参数预算下仍能保持较好的性能。
- 比标准 LoRA 更少的训练参数: 如果
- 挑战:
- 性能是否能稳定超过精心调整的 LoRA 仍有待更多验证。
- 随机矩阵的选择和共享向量的设计可能对结果有影响。
5. LongLoRA
- 提出者: Chen et al., 2023 ("LongLoRA: Efficient Fine-tuning of Long-Context Large Language Models")
- 核心思想: 专门为微调具有长上下文能力的 LLM (如需要处理几万甚至几十万 token 输入的模型) 而设计的 LoRA 变体。 直接在长上下文模型上使用标准 LoRA 进行全注意力微调的计算成本仍然很高。
- 关键技术点:
- Shifted Short Attention (S2-Attn): 一种近似全注意力的方法。在训练时,不是计算所有 token 之间的全注意力,而是在分组的、偏移的局部窗口内计算注意力。这可以显著减少长序列注意力计算的复杂度和显存占用。
- LoRA 仍然应用于模型的权重矩阵: 但由于使用了 S2-Attn,即使是全参数微调(如果这样做的话)也变得更可行,因此 LoRA 在此基础上进一步降低了参数量。
- 在短上下文预训练模型上进行长上下文微调: LongLoRA 使得可以用较少的资源将一个在短上下文上预训练的模型高效地微调到支持更长的上下文。
- 优点:
- 显著降低了微调长上下文 LLM 的成本。
- 使得在有限资源下扩展 LLM 的上下文处理能力成为可能。
- 重要性: 随着对 LLM 处理长文档、长对话等需求日益增加,高效微调长上下文模型的技术变得越来越重要。
学习这些变体的建议:
- 先深入理解标准 LoRA: 这是所有变体的基础。
- 阅读原始论文: 了解每个变体的提出动机、核心技术和实验结果。
- 关注Hugging Face
peft
和bitsandbytes
等库的更新: 这些库通常会率先支持和实现这些新的 PEFT 技术。 - 动手实践: 如果条件允许,尝试使用这些变体进行微调实验,感受它们的效果和特点。
- 理解权衡: 每种变体都有其优缺点和适用场景。例如,QLoRA 侧重于极致的显存优化,AdaLoRA 侧重于智能的参数分配,LongLoRA 侧重于长上下文。
LoRA 及其变体的快速发展表明,PEFT 领域仍然充满活力,未来可能会有更多创新性的方法出现。掌握这些技术将使你在应用和优化大型语言模型时拥有更多的选择和更强的能力。
选择合适的参数高效微调 (PEFT) 方法是一个非常实际且重要的问题。没有一种 PEFT 方法是万能的,最佳选择取决于多种因素的权衡。
以下是一些关键的考虑因素和选择策略:
1. 任务类型 (Task Type):
- 自然语言理解 (NLU) 任务 (如文本分类, 命名实体识别, 问答匹配):
- LoRA: 通常表现非常出色且稳健。
- Adapter Tuning: 也是一个不错的选择,尤其是在需要清晰模块化或希望利用 Adapter Hub 资源时。
- Prompt Tuning / P-Tuning v2: 对于大型模型,也可能有效,但对于小模型可能不如 LoRA 或 Adapter。
- (IA)^3: 可能有效,但对于需要模型进行较大语义理解转变的任务,可能不如 LoRA 灵活。
- 自然语言生成 (NLG) 任务 (如文本摘要, 翻译, 对话生成, 代码生成):
- LoRA: 仍然是强有力的竞争者,尤其对于 Decoder-only 和 Encoder-Decoder 模型。
- Prefix Tuning: 最初就是为生成任务设计的,通过在每一层注入前缀来引导生成过程,效果较好。
- Prompt Tuning (Soft Prompts): 对于大型生成模型,如果追求极致的参数效率,值得尝试。
- Adapter Tuning: 也可以用于生成任务,但可能需要仔细调整 Adapter 的设计。
- 序列标注任务 (Sequence Tagging):
- LoRA, Adapter 通常都适用。
- 长上下文任务:
- LongLoRA: 专门为此设计,结合了高效注意力和 LoRA。
- 其他方法如果能结合长上下文处理技术(如 FlashAttention)也可能适用。
2. 模型大小 (Model Scale):
- 超大模型 (例如 >10B-70B+ 参数):
- QLoRA: 如果显存是主要瓶颈,QLoRA 是首选,它使得在消费级硬件上微调这些巨型模型成为可能。
- Prompt Tuning (Soft Prompts): 在这种规模下,Prompt Tuning 的性能往往能追平甚至超过全参数微调,且参数效率极高。
- LoRA: 仍然是一个非常好的选择,参数效率高,性能有保障。
- (IA)^3: 参数效率最高,值得尝试,但性能上限可能需要评估。
- 中大型模型 (例如 1B-10B 参数):
- LoRA: 通常是性能和效率的最佳平衡点。
- Adapter Tuning: 表现良好,模块化清晰。
- Prefix Tuning: 尤其对于生成任务,是一个不错的选择。
- Prompt Tuning: 效果可能开始显现,但可能不如 LoRA 稳定。
- 较小模型 (例如 <1B 参数):
- LoRA / Adapter Tuning: 仍然是不错的选择,可以提供比全参数微调更好的泛化性(尤其在数据较少时)和更低的成本。
- Prompt Tuning / (IA)^3: 性能可能不如前两者,因为小模型的表达能力有限,可能需要更“强力”的参数调整来适应任务。
- 全参数微调: 对于非常小的模型,如果资源允许且数据充足,全参数微调的成本可能尚可接受,且可能达到最佳性能。
3. 计算资源限制 (Computational Resource Constraints):
- GPU 显存 (VRAM):
- 极度受限: QLoRA 是不二之选。其次是 (IA)^3 和 Prompt Tuning (因其极小的可训练参数量)。
- 中度受限: LoRA 是一个很好的平衡。
- 相对充足: 可以考虑 Adapter Tuning (参数量略多于 LoRA),甚至 全参数微调 (如果模型不是特别巨大)。
- 训练时间预算:
- 参数量越少的 PEFT 方法(如 Prompt Tuning, (IA)^3, LoRA)通常训练得越快。
- Adapter Tuning 由于在每层都引入模块,训练时间可能略长于 LoRA。
- 推理延迟要求:
- LoRA (带权重合并): 推理时无额外延迟,与原始模型一致。
- Prompt Tuning / (IA)^3: 推理时增加的计算量极小,几乎可以忽略。
- Adapter Tuning: 会在每一层引入额外的计算模块,导致推理延迟增加。如果对延迟非常敏感,需要谨慎考虑。
- Prefix Tuning: 也会在每层注意力计算时增加少量计算。
4. 数据量 (Amount of Fine-tuning Data):
- 数据量非常少 (Low-resource):
- PEFT 方法通常比全参数微调表现更好,因为它们限制了模型的自由度,减少了过拟合的风险。
- LoRA, Adapter Tuning, Prefix Tuning 通常在这种情况下表现稳健。
- Prompt Tuning 和 (IA)^3 可能需要更多数据才能充分学习到有效的引导信号,但在极低资源下也值得一试。
- 数据量中等或充足:
- 大多数 PEFT 方法都能取得不错的效果。
- LoRA 仍然是一个强有力的基线。
5. 对灾难性遗忘的关注程度 (Concern about Catastrophic Forgetting):
- 所有 PEFT 方法都比全参数微调更能缓解灾难性遗忘,因为它们冻结了大部分预训练参数。
- 如果这一点至关重要,PEFT 方法是明确的选择。
6. 对模型模块化和任务管理的需求 (Need for Modularity and Task Management):
- Adapter Tuning: 提供了非常清晰的模块化,每个任务一个独立的 Adapter 模块。Adapter Hub 使得共享和重用 Adapter 变得容易。
- LoRA, Prompt Tuning, Prefix Tuning, (IA)^3: 也都支持为每个任务训练独立的、轻量级的参数集,方便管理和切换。LoRA 的权重合并特性在部署单个任务时很方便。
7. 实现复杂度和易用性 (Implementation Complexity and Ease of Use):
- Hugging Face
peft
库大大降低了使用主流 PEFT 方法 (如 LoRA, Prompt Tuning, Prefix Tuning, AdaLoRA) 的门槛。 adapter-transformers
库专注于 Adapter 及其变体。(IA)^3
和一些更新的变体可能需要开发者自己实现或等待库的支持。- Prompt Tuning 和 (IA)^3 在概念和实现上相对更简单。LoRA 的实现也比较直接。Adapter 和 Prefix Tuning 稍微复杂一些,因为需要在模型内部插入模块或修改注意力计算。
选择策略的总结性建议:
- 默认尝试 LoRA: LoRA 因其在性能、效率和易用性上的出色平衡,通常可以作为首选的 PEFT 基线方法进行尝试。
- 显存是最大瓶颈? -> QLoRA: 如果你需要微调远超单 GPU 显存容量的模型,QLoRA 是目前最有效的解决方案。
- 追求极致的参数效率和简单性,且模型规模巨大? -> Prompt Tuning 或 (IA)^3: 这两种方法参数量极小,值得在大模型上尝试。
- 生成任务,且模型规模适中? -> Prefix Tuning 或 LoRA: Prefix Tuning 在生成任务上表现良好,LoRA 也同样适用。
- 需要清晰的模块化和生态支持? -> Adapter Tuning: Adapter Hub 提供了丰富的资源。
- 长上下文任务? -> LongLoRA: 专门优化。
- 希望更智能地分配参数预算? -> AdaLoRA: 如果愿意接受更高的实现复杂度。
- 实验是王道: 阅读文献和理解原理后,最好的方法是在你的具体任务、模型和数据上进行实验,对比不同 PEFT 方法的效果。从小规模实验开始,快速迭代。
- 关注超参数调整: 每种 PEFT 方法都有其关键超参数(如 LoRA 的
r
和lora_alpha
,Adapter 的瓶颈维度,Prompt Tuning 的提示长度等),这些超参数对最终性能影响很大,需要仔细调整。 - 考虑组合使用: 虽然不常见,但在某些高级场景下,可能会考虑组合不同的 PEFT 思想(但这会增加复杂性)。
一个简化的决策流程示例:
graph TD
A[开始: 选择PEFT方法] --> B{GPU显存是否极度紧张?};
B -- 是 --> C[尝试 QLoRA];
B -- 否 --> D{模型规模是否超大 (>10B)?};
D -- 是 --> E{追求极致参数效率?};
E -- 是 --> F[尝试 Prompt Tuning / (IA)^3];
E -- 否 --> G[尝试 LoRA];
D -- 否 (中小型模型) --> H{任务类型是?};
H -- 生成任务 --> I[尝试 Prefix Tuning / LoRA];
H -- 理解任务 --> J[尝试 LoRA / Adapter Tuning];
H -- 长上下文 --> K[尝试 LongLoRA];
C --> Z[评估效果];
F --> Z;
G --> Z;
I --> Z;
J --> Z;
K --> Z;
Z --> X{满足要求?};
X -- 是 --> Y[完成];
X -- 否 --> W[调整超参数或尝试其他方法];
W --> A;