Skip to content

模型微调(SFT)

大模型监督微调(Supervised Fine-Tuning, SFT)是将预训练模型适配到特定任务的关键技术。本文详解SFT的应用场景、PEFT技术、LoRA系列方法。

SFT的六大应用场景

  1. 风格化交互输出:针对特定语料风格进行适配训练,典型应用如将模型输出调整为《甄嬛传》古风对话体,满足特定场景的文体一致性需求。

  2. 限定域问答优化:适用于查询空间高度受限的业务场景(如5-6千条标准问题库),通过精准问答对训练提升查询匹配准确率,需注意当业务复杂度超出阈值时需进行全参数后训练。

  3. 轻量级领域知识注入:适用于专业不太复杂的垂直领域知识迁移,对于医学、法律等高复杂度专业领域,建议采用领域预训练(Post pre-train)而非单纯微调方案。

  4. 数学代码等专项能力强化训练:主流技术路径包括代码生成优化与数理逻辑提升,比如如DeepSeek、Kimi等发新模型之前,都会基于特定数据集的定向微调刷个榜。

  5. Function calling支持优化:通过结构化输出训练增强API调用、工具使用等函数执行能力,构建模型与外部系统的标准化交互接口。

  6. Agent协作能力强化:针对多Agent协同、任务拆解与状态维护等复杂场景,通过特定训练模式提升模型的流程控制与策略规划能力。

业界共识

  1. Prompt的质量和多样性远比数量重要,全量微调一个30B量级的base model只需要10w条数据即可,LoRA微调一个30B的大约需要2w条。(经验值)

  2. 合成数据很重要! 数据需要尽可能覆盖所有类别的业务问题,要通过不同方式进行多路合成,减少合成数据的bias。

  3. 可以加点预训练的数据进去,减少灾难性遗忘(Catastrophic Forgetting)

  4. 一般训练1~3个epoch。

  5. 可以全量微调,就不要PEFT。(比如上述六大应用场景中,只有1、2、6适合做PEFT)

  6. SFT阶段不能做太多知识注入,过多的知识注入,或者超出模型能力本身的回答过多会导致对齐税(Alignment Tax)

PEFT技术概述

PEFT(Parameter-Efficient Fine-Tuning) 技术旨在通过最小化微调参数的数量和计算复杂度,来提高预训练模型在新任务上的性能,从而缓解大型预训练模型的训练成本。通过特定领域数据对预训练模型进行针对性优化,以提升其在特定任务上的性能。

LoRA

通过在模型的关键层(多头注意力和Feed-Forward)中添加低秩矩阵,并将其加到原始权重矩阵上。该方法不用改变整个模型的结构,在推理时不需要额外的计算量,并且能保持原有的性能。与之相比,Adapter引入了额外的层,该层必须按顺序处理,影响并行训练;Prefix-tuning在用户输入前加可训练的前缀,减少了可用于处理下游任务的sequence长度,性能较差。

LoRA原理

LoRA固定原始参数 W0,只微调 ABAB 的矩阵维度远低于 W0 维度。A 采用高斯初始化,B 初始化为全0矩阵(第一轮训练时,权重矩阵和不加 AB 时一样,减少刚开始训练时的波动)。当切换下游任务时,只需要更换 AB

W=W0+ΔW=W0+BA

其中 W0Rd×kBRd×rARr×k,且 rmin(d,k)

LoRA可以看作是全量微调的一个泛化形式,随着模型可训练参数的增加(也就是增大 r),LoRA就收敛到了原始模型,而基于Adapter的方法收敛到MLP,基于Prefix Tuning的方法收敛到不能接受长输入的模型。

LoRA超参数

在原论文中,还有一个超参数 αα 控制低秩矩阵对预训练权重的影响。r 决定低秩矩阵能捕捉的特征维度数量。α 一般固定为实验开始时的 r 值,后续不需要再调整。r 一开始设置为比较大的值,后续 α/r 逐渐变大。r 越小表示低秩矩阵的信息更精炼,此时梯度下降的方向更确信,增快梯度下降的速度,相当于调整学习率。

ΔW=αrBA

LoRA应用位置

给定参数上限时,应该尽可能的在更多参数矩阵应用LoRA,即使 r 很小,而不是弄一个大的 r 在一个参数矩阵上。

LoRA的最佳r值

实验证明LoRA在 r 很小时准确率依然很高(如果下游任务和预训练任务相差过大,则应适当增大 r)。

ΔWW 之间的关系

ΔW 放大了 W 中未强调的方向,且放大系数相当大。并且 ΔW 放大了下游任务的重要特征,这些特征尽管 W 学习到了,但并未强调。

AdaLoRA

目的:根据需求调整 r 的值,即在 AB 中选择比较重要的向量,而把其他的设置为0。

正交矩阵

AAT=I,即 A 中的每个列向量之间的乘积都是0,互相没有关系。而在LoRA中无法保证这一点,删除某一列会对其他造成影响。

SVD分解

采用SVD分解,把 Λ 中的某个值变为0就能实现消除 B 的列向量和 A 的横向量(调整了 r),后续轮次恢复这个向量只用再调整 Λ

ΔW=PΛQ=PΣQ

其中 Σ 为奇异值矩阵,通过保留Top-r个奇异值实现秩控制。

top_b策略

也就是逐渐加秩,让模型尽可能多探索。到后期再慢慢把top_b降下来,直到最后以稳定的top_b进行训练,达到AdaLoRA的总目的:把训练资源留给最重要的参数。这个过程就和warm-up非常相似。

QLoRA

背景知识

Scaling Law

计算量FLOP(假设算 a×bb×c 矩阵的乘积,FLOP为 2abc)与数据集大小和模型参数量成正比。Scaling Law指出可以通过在小计算量时得到损失值,预测大计算量的损失。

量化基础

  • 非对称量化:需要记录缩放因子 qx 和零点值 zpx
  • 对称量化:记录缩放因子 qx,使用更普遍,精度较低(当数据非对称分布时,会导致部分区间浪费)。
  • 非均匀量化:针对原始数值分布不均匀的情况,需要记录每一个桶的位置,参数更多。

对比以上三种量化,最常用的是对称量化。

量化的粒度:粒度大时损失的值会更多,且异常值影响到的参数更多。粒度小需要存储更多的scale factor。

训练时计算资源消耗分布

  • 模型参数和梯度:完整的模型参数和需要更新的参数的梯度。
  • 优化器:Adam需要维护一阶和二阶动量
  • 激活函数:在反向传播时,需要前向传播的activation的值。

LoRA能大量减少梯度和激活值需要的计算资源,但参数仍然占据很大空间。

QLoRA原理

QLoRA通过减少每个参数对应的bit数,实现计算量的减少。在训练过程中,QLoRA首先将模型用4-bit存储,然后在训练时把数值反量化到bf16后进行训练。这样的设计使得训练所需的显存大大减少。

QLoRA使用4-bit NormalFloat (NF4),也就是Quantile Quantization,一种在信息理论上最优的数据类型,确保输入向量落入到每个量化区间的值的数量相同。QLoRA论文证明预训练神经网络权重通常遵循以0为中心的正态分布,因此可以通过缩放 σ 使分布适应我们的数据范围(本文采用 [1,1])。

NF4量化与反量化

针对输入向量 X=[0.32,1.76,0.025,1.22],采用对称量化,找到 absmax(X)=1.76,将 X 归一化成 [0.1818,1,0.0142,0.6932]。依据NF4=[1.0000,0.6962,0.5251,0.3949,0.2844,0.1848,0.0911,0.0,0.0796,0.1609,0.2461,0.3379,0.4407,0.5626,0.7230,1.0000]进行舍入。

注意尽管0.0142距离0.0更近,但是在神经网络中通常用0进行padding,为了区分padding和较小的数,不能将较小的数舍入为0。舍入后保存在NF4中对应的下标,例如0.1609对应9。

反量化则是乘以 absmax(X)=1.76,反量化存在一定误差,如 0.1609×1.76=0.283184

总结一下,分为3个步骤:

  1. 计算出量化常数 N(在非对称量化中 N=absmax(X)),将输入向量 X 映射到目标量化区间 D(例如 [1,1])中。
  2. 对于 X/N 中的每个元素,找到分位函数 QX 中计算到的最相近的值 qi
  3. qi 对应的索引 i 存到输出向量 TQ 中。

分位数量化

最理想的情况是,分位函数中的每一个数都以相同概率被用到。例如采用NF4做为分位函数,输入 X 长度为1600,其中的16个值每个被选中100次。

为了实现这一特性,可以将概率分布函数 fX 分为 2k 个区间(k 表示量化后的一个数字占 k bit),每个区间都有相同的面积,这些区间的中点即为 q

NF4实现

python
delta = 1/2*(1/32+1/30)
print(torch.linspace(delta, 0.5, 8))
print(norm.ppf(torch.linspace(delta, 0.5, 8)))
# 输出:
# tensor([0.0323, 0.0991, 0.1659, 0.2327, 0.2996, 0.3664, 0.4332, 0.5000])
# [-1.84813142 -1.28665578 -0.97040379 -0.72985929 -0.52568489 -0.34148556
#  -0.16827238  0.        ]

print(torch.linspace(0.5, 1-delta, 9))
print(norm.ppf(torch.linspace(0.5, 1-delta, 9)))
# 结果:
# tensor([0.5000, 0.5585, 0.6169, 0.6754, 0.7339, 0.7923, 0.8508, 0.9092, 0.9677])
# [0.         0.14707497 0.29742005 0.45484772 0.6245116  0.81448966
#  1.03979003 1.33611882 1.84813166]

将两部分 q 合并,去掉重复的0,就得到了 N(0,1) 分布的NF4。然后将 q 的值归一化到 [1,1] 范围内,就得到了 [1,1] 区间内的NF4。

二次量化

二次量化的目的是为了节省量化常数占用的空间。为了降低异常值对量化的影响,量化的粒度一般比较小,这需要存储大量的量化常数。例如当粒度为64,且采用32位的量化常数时,对于每个参数相当于增加 32/64=0.5 个bit,用于存储量化常数。

Paged optimizers

使用NVIDIA统一内存特性,在CPU和GPU之间进行页传输。当GPU内存不足时,将部分状态转移到CPU RAM中,并在优化器更新步骤需要内存时分页回到GPU内存中。

QLoRA代码实现

python
qlora_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)

LoRA其他变种

  • X-LoRA:采取MOE的思路,对每个token经过多个expert,额外训练一个scaling network,通过输出的scaling对各个expert的输出进行加权。
  • LoHa:对于秩 2R 的LoRA,将 XY 分别拆分为两个矩阵,分别做矩阵相乘再做点积,秩变成了 R2,表达能力更强。

Adapter

在模型层之间插入小的可训练的Adapter模块,原始模型参数训练时保持不变。Adapter一般采用bottleneck结构。

Adapter Fusion

针对多任务训练,每个任务训练出一个Adapter,对所有Adapter进行fuse,采用cross attention实现知识融合。

Prefix Tuning

为LLM添加可训练的、任务特定的前缀,这种Prefix实际就是连续可微的Virtual Token(Soft Prompt/Continuous Prompt),相比离散的Token,更好优化,效果更好。在输入序列前边加上任务相关的前缀,前缀可以是固定的(即手动设计的静态提示)或可训练的(即模型在训练过程中学习的动态提示)。不对prefix做位置编码。

Prompt Tuning

同样在输入中添加可学习的向量。不同的是,prompt模仿自然语言中的提示,引导模型生成特定的输出。prefix提供输入数据的上下文信息,作为模型内部表示的一部分,可以影响整个模型的行为。

P-Tuning

类似Prompt tuning。

  • Prompt Tuning:使用静态的、可训练的虚拟标记嵌入。这些嵌入在初始化后保持固定,除非在训练过程中被更新,相对简单,因为它只涉及调整一组固定的嵌入参数。在处理多种任务时表现良好,但可能在处理特别复杂或需要细粒度控制的任务时受限。只在输入前边插入。

  • P-Tuning:使用一个可训练的LSTM模型(称为prompt_encoder)来动态生成虚拟标记嵌入,允许根据输入数据的不同生成不同的嵌入,提供了更高的灵活性和适应性,适合需要精细控制和理解复杂上下文的任务,相对复杂,因为它涉及一个额外的LSTM模型来生成虚拟标记嵌入。可以在任意位置插入prompt。

P-Tuning v2

P-Tuning只在输入的embedding中添加提示,其他层的prompt embedding都来自上一层。这样由于模型层数增多,prompt对后面的影响难以预估,影响模型稳定性。并且prompt不能过长(跟sequence长度匹配)。

P-Tuning v2在许多层都插入prompt,prompt之间相互独立,增加了可训练的参数。