跳到主要内容

[LLM 2/10] SFT + LoRA:只训练 1.69% 的参数,教模型学会当助手

· 阅读需 7 分钟
Kobkrit Viriyayudhakorn
CEO, iApp Technology

上一章我们用继续预训练把知识灌进了模型权重。 但"知道"的模型不等于"会回答"的模型——base 模型只有一个职业:把文字续写下去。 这一章要讲的是用 LoRA 来做 SFT(Supervised Fine-Tuning):只训练约 1.7% 的参数, 却能改变整个模型的行为——在免费 Colab 上约 15 分钟跑完。 而你拿到手的,是一个约 40 MB 的 adapter 文件,它将成为本系列余下所有章节的脊梁。

Open in Colab02_sft_lora.ipynb

1. 问题(Problem statement)

拿上一章那个纯 base 模型 Qwen3-0.6B-Base,输入一句泰语 "帮我推荐几道泰国菜吧"。你得到的往往不是回答, 而是续写——它可能再帮你编三个问题、把话头接成一篇旅游文章, 或者写到一半切换成英语。因为它唯一被训练过的事情是:"互联网上这样的文字,后面通常跟着什么"。

回答的能力——接住指令、答在点上、然后停下来——不是 pretraining 自带的。 它来自 SFT:用几千到几百万对(指令,好的回答)继续训练。 你用过的每一个 instruct 模型,无一例外都走过这一步。

但真要自己动手,会撞上叠在一起的两层问题:

第一层——full fine-tuning 的成本。 如果训练全部参数,每个任务你都会得到一整个新模型(0.6B 模型约 1.2 GB)。 一个有十个任务的组织——文档摘要、公文起草、客服回复、投诉分类——就得存十份副本。 而且用偏高的学习率去动每一个权重,正是把第 1 章刚灌进去的知识抹掉的配方(还记得那个学习率警告框吗)。

第二层——泰语。 即便是已经 post-train 过的 Qwen3-0.6B, 也有一个我们全系列反复看到的症状:用泰语提问,回答却在句子中途滑回英语 (这正是本系列 th_ratio 指标的由来),因为它见过的 SFT 数据以英语为主。

这一章同时解决两层问题:用泰语指令数据做 SFT,并用 LoRA 代替 full fine-tuning。

2. 我们要做什么(Solution)

我们会拿 Qwen3-0.6B,用 4,000 条泰语指令-回答对来训练, loss 和第 1 章一模一样,外加两件新东西:

  1. 补全掩码(completion mask)——只在回答一侧的 token 上计 loss,不计提问一侧(3.1 节会解释跳过这一步会怎样以一种滑稽的方式翻车)
  2. LoRA(Low-Rank Adaptation)——把原有权重全部冻结,改为在每层之上叠加两个小矩阵来训练
本章的核心观点

并不是在训练模型权重——你是在训练一个叠加在原权重之上的低秩"修正项"(correction)

这就是 adapter 只有约 40 MB 而不是 1.2 GB 的原因, 是你可以在同一个 base 上存二十个 adapter 换着用的原因(二十个任务 = 0.8 GB,而不是 24 GB), 也是第 4 章(DPO)的 reference model 额外显存开销为零字节的原因—— 把 adapter 一关,就原封不动拿回了初始模型。

3. 公式(Equation)

3.1 SFT loss 与补全掩码

LSFT(θ)=E(x,y)[t=1ymtlogpθ(ytx,y<t)]\mathcal{L}_{\text{SFT}}(\theta) = -\mathbb{E}_{(x,y)}\left[\sum_{t=1}^{|y|} m_t \log p_\theta(y_t \mid x, y_{<t})\right]
  • (x,y)(x, y) = 一条训练样本——xx 是指令部分(含 chat template),yy 是模型训练时真正看到的 token 序列
  • yty_t = 第 tt 个位置的 token,y<ty_{<t} = 它前面的全部 token
  • pθp_\theta = 参数为 θ\theta 的模型预测出的概率
  • mt{0,1}m_t \in \{0,1\} = 补全掩码——只在回答一侧的 token 上为 1,在 prompt 的 token 上为 0

试着把 mtm_t 拿掉(即令 mt1m_t \equiv 1 处处成立),这个式子立刻变回第 1 章 CPT 的 objective。 SFT 就是把文本布置成对话场景的 CPT,再加上一个掩码——没有更多了。

但这一个掩码就是成功的一半,因为 mt1m_t \equiv 1最容易踩中的默认值 (很多 pipeline,包括 SFTTrainer,如果 collator 没配对,就会悄悄按这种方式训练)。 而在典型的泰语指令数据里,prompt 一侧的 token 占到样本的约 60%—— 也就是说你的大部分梯度正在教模型学写用户的提问,而不是学回答。 它的后遗症会在第 9 节现出原形。

3.2 LoRA:训练修正项,而不是权重

LoRA 不直接更新权重矩阵 W0W_0,而是把 W0W_0 钉死,去学习一个由两个小矩阵相乘构成的差量:

W=W0+ΔW=W0+αrBAW' = W_0 + \Delta W = W_0 + \frac{\alpha}{r}\,B A
  • W0Rd×kW_0 \in \mathbb{R}^{d\times k} = 该层的原始权重,被冻结,完全不接收梯度
  • BRd×rB \in \mathbb{R}^{d\times r}ARr×kA \in \mathbb{R}^{r\times k} = 我们真正训练的两个 adapter 矩阵
  • rmin(d,k)r \ll \min(d, k) = 修正项的秩(rank)——LoRA 的主旋钮(本章用 r=16r = 16
  • α\alpha = 缩放系数——乘积 BABA 永远会被乘上 α/r\alpha/r(本章 α=32\alpha = 32,所以 α/r=2\alpha/r = 2

这个定义里有两个细节,重要程度远超它们的长相:

BB 整个矩阵被初始化为零,所以第一步时 ΔW=0\Delta W = 0—— 训练从 base 模型分毫不差地出发,不存在被随机权重扰动的阶段。 (AA 则是随机 Gaussian——如果两个都设成零,双方的梯度会永远为零,因为各自都在和零相乘。)

α/r\alpha/r 里的除数 rr 让 update 的量级与秩无关—— 把 rr 翻倍,BABA 的求和多出一倍的项,却刚好被除回去。 所以你可以扫一遍 rr 的取值,而不必每次重新调学习率。

训练结束后你有两条路:mergeW=W0+αrBAW' = W_0 + \frac{\alpha}{r}BA,得到一个没有额外 latency 的单一模型), 或分开保存——本系列选的是第二条,因为可插拔的 adapter 正是第 4 章免费变出 reference model 的道具。

3.3 可训练参数占比——一个要亲手验算的数字,不是背下来的数字

对一个 d×kd \times k 的矩阵,adapter 有 rd+rkrd + rk 个参数,占比为

r(d+k)dk\frac{r(d+k)}{dk}
  • d,kd, k = 原权重矩阵的维度
  • rr = adapter 的秩

代入 Qwen3-0.6B 的真实数值(hidden 1024、intermediate 3072、28 层, 在 q、k、v、o、gate、up、down 全部 7 个矩阵上挂 adapter),可训练参数是 10,092,544 个, 基座是 596,049,920 个 = 1.69%

1.69% 不是"低于 1%"——一堂关于验算数字的课

几乎每篇 LoRA 文章都说"只训练不到 1% 的参数"。这个数字在 7B 以上的尺度是真的, 但对小模型不成立:adapter 参数按 r(d+k)r(d+k) 增长——随 hidden size 线性, 而基座参数按 dkdk 增长——二次方。模型越小,adapter 的占比反而越大。

再注意 1.69% 低于逐矩阵的占比(约 2.1–2.3%)——因为分母里还包含了 约 1.56 亿个我们没挂 adapter 的 embedding 参数。这些数字用纯算术就能验证, notebook 会在第 7 节让 peft 把真实值打印给你亲眼看——信 print,不要信博客(包括这一篇)

完整内容在课程中

这篇文章大约是本章的前 30%。其余部分——环境准备、数据准备、核心代码、实测结果与总结——都在免费的 LLM Finetuning 课程中,使用 Google 登录即可阅读。

在课程中阅读完整章节 →