微调
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) 以确保实验结果可以复现。
- 记录好所有的超参数配置和实验环境。
全参数微调虽然强大,但也需要精心设计和细致调整。理解这些细节和注意事项,将帮助你更有效地利用预训练模型的强大能力,并避免常见的陷阱。