跳到主要内容

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

· 阅读需 21 分钟
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,不要信博客(包括这一篇)

4. 把公式画出来(Visualize)

一个掩码,扭转整坨梯度的方向

水平柱状图对比 loss 各项的分布:不加掩码时 60% 的 loss 落在 prompt 一侧的 token 上,而加了补全掩码后全部 loss 都落在回答一侧的 token 上水平柱状图对比 loss 各项的分布:不加掩码时 60% 的 loss 落在 prompt 一侧的 token 上,而加了补全掩码后全部 loss 都落在回答一侧的 token 上

Figure 2.1一对典型泰语问答的纯 token 记账(prompt 180 + 回答 120 token):如果不加掩码,loss 中 60% 的项都在学写用户的提问

这张图没有任何比数 token 更深的内容——但被跳过的恰恰就是数 token 这一步: 泰语的 prompt(含 system message 和 chat template)往往比回答更长, 不加掩码地训练,你就把大部分算力花在了教模型一件你根本不想要的事上。 notebook 会把抽样数据集的真实占比打印出来——图中的 60/40 只是一个典型样本的代表值,不是什么神圣常数。

为什么 rank 16 就"够了"——low-rank 假设

全部 1024 个奇异值的 log-log 图,在第 16 个位置之后出现明显的断崖,前 16 个方向持有矩阵约 93% 的能量全部 1024 个奇异值的 log-log 图,在第 16 个位置之后出现明显的断崖,前 16 个方向持有矩阵约 93% 的能量

Figure 2.2一个合成 ΔW(1024×1024)的奇异值谱:rank 16 的信号被埋在满秩噪声之下——这是 low-rank 假设的示意插图,不是对真实 fine-tune 的测量

LoRA 的假设(Hu 等人,2021)是:相对于矩阵的全维度,fine-tune 只在为数不多的几个重要方向上挪动权重。 这张图人为构造了一个正好具有这种结构的假 ΔW\Delta W(rank 16 信号 + 噪声), 再让 SVD 把它找回来——如果真实的 update 长这样,rank 16 的 adapter 就能留住几乎全部内容。 这张图没有证明的是:真实的 update 总是长这样。 那是研究的经验性发现,也是 LoRA "通常"逼近 full fine-tuning、而非"总是"的原因。

验算数字,不要背数字

LoRA rank 从 1 到 256 的可训练参数占比 log-log 图,呈一条直线,标出 r=16 位于 1.69% 的点和 1% 处的红色虚线LoRA rank 从 1 到 256 的可训练参数占比 log-log 图,呈一条直线,标出 r=16 位于 1.69% 的点和 1% 处的红色虚线

Figure 2.3可训练参数占比随 rank 的变化,按 Qwen3-0.6B 的真实维度精确计算——r=16 那个点在 1.69%,高于常被引用的'低于 1%'线

log-log 轴上的直线印证了公式所说的:占比随 rr 精确线性增长。 在这个模型上,"低于 1%"只有当 r9r \le 9 时才成立—— 而那不是任何人在语言任务上真正会用的值。我们用的点(r=16r=16)是 1.69%,以 fp32 保存约 40 MB。

5. 准备环境(Environment)

打开 Colab,选择 Runtime → Change runtime type → T4 GPU(免费额度够用)。

本系列每章都要重读一遍的警告

Colab 的 T4 是 Turing 架构(SM 7.5),它不支持 bfloat16,也不支持 FlashAttention-2

但 Qwen3-0.6B 的 config.json 里写着 torch_dtype: bfloat16。 所以 torch_dtype="auto" 是个陷阱:代码会崩掉或者慢得离谱,而且不会告诉你原因。

torch_dtype=torch.float16      # 不是 bfloat16
attn_implementation="sdpa" # 不是 flash_attention_2
fp16=True # 在 SFTConfig 里(不是 bf16=True)
cap = torch.cuda.get_device_capability(0)
print("compute capability:", cap) # T4 = (7, 5)
print("native bf16:", cap[0] >= 8) # T4 -> False
print("torch says :", torch.cuda.is_bf16_supported()) # T4 -> True(把 emulation 也算上了!)
is_bf16_supported() 在 T4 上会骗你

较新的 torch 在 T4 上返回 True,因为它把**模拟(emulation)**也算作支持——而模拟比 fp16 慢得多。 请改为判断 compute capability ≥ 8.0(Ampere 及以上)。这是真正在 Colab 上跑才发现的 bug。

fp16 + LoRA:只把 adapter 转成 fp32

第 1 章我们训练前必须 model.float() 整个模型,因为训练的是全部参数。 这一章 base 被冻结——没有梯度——所以可以安安稳稳留在 fp16,省下一半显存。 必须是 fp32 的只有可训练的参数,也就是那约 1,000 万个 adapter 参数:

for p in model.parameters():
if p.requires_grad:
p.data = p.data.float() # 只转 adapter —— 不是整个模型

忘了这一步,你会看到 ValueError: Attempting to unscale FP16 gradients.—— 和第 1 章一模一样的报错,只是这次修复的代价便宜得多:转 1,000 万个参数,而不是 5.96 亿个。

用了 LoRA 之后彻底改头换面的显存预算

还记得第 1 章 Adam 的 optimizer state 比模型本身还占地方吗(mmvv 合计约 4.4 GB)。 现在只训练 adapter,Adam 只需为 1,010 万个参数存 state——大约 0.08 GB, 连 adamw_bnb_8bit 都不再需要了。剩下的大头是 base 权重(fp16,约 1.2 GB)和 activations。

在计算器里切换 full fine-tuning 和不同的 LoRA rank,看看预算在哪里发生变化:

596.0M parameters, derived from config.json
Weight dtype
Run mode
Trades about 30% more compute for a large drop in activation memory.
weights: 1.13 GiBgradients: 19.25 MiBoptimizer: 115.50 MiBactivations: 170.00 MiB16 GB — Colab T40481216GiB
  • Weights1.13 GiB
  • Gradients19.25 MiB
  • Optimizer state115.50 MiB
  • Activations170.00 MiB
  • KV cache
Total VRAM1.43 GiB14.57 GiB to spare
Trainable params10.1M1.69%
KV cache per token112 KiB2 x 28 x 8 x 128
Full context KV4.38 GiB41.0K tok

It fits.This run needs 1.43 GiB and leaves 14.57 GiB of headroom on a free Colab T4.

6. 准备数据(Data)

我们使用 airesearch/wangchanx-seed-free-synthetic-instruct-thai-120k—— WangchanX 团队的 120,000 对合成泰语指令-回答数据集(抽样 4,000 条使用)。

from datasets import load_dataset

ds = load_dataset("airesearch/wangchanx-seed-free-synthetic-instruct-thai-120k", split="train")
ds = ds.shuffle(seed=42).select(range(4000))

def to_text(ex):
messages = [
{"role": "user", "content": ex["instruction"]}, # 列名务必对照 dataset card 检查
{"role": "assistant", "content": ex["output"]},
]
return {"text": tok.apply_chat_template(messages, tokenize=False,
enable_thinking=False)}

train_ds = ds.map(to_text, remove_columns=ds.column_names)

有两行值得放慢速度读:

  • apply_chat_template 把消息拼装成 Qwen3 被训练时的格式(<|im_start|>user<|im_end|> …)。 我们只在这一个地方调用一次——原因见第 9 节的陷阱 2
  • enable_thinking=False 关闭 Qwen3 的 thinking 模式,让本章的样本保持简单、掩码逻辑直截了当

然后是一个应该养成习惯的仪式:永远把第一条样本 decode 出来亲眼看一遍

print(train_ds[0]["text"][:400])
# 必须看到 <|im_start|>user ... <|im_end|> ... <|im_start|>assistant ... 每个 turn 各出现一次

notebook 还会打印抽样数据的 token 长度统计:prompt 一侧与回答一侧的中位数, 以及长度超过 768 token 的样本占比——后面这个数字会在陷阱 3 里回来讨债。

7. 核心代码(Main code)

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer, SFTConfig, DataCollatorForCompletionOnlyLM

tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")

model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-0.6B", # post-trained 版 —— 本章修的是行为,不是灌知识
torch_dtype=torch.float16, # T4 没有 bf16
attn_implementation="sdpa", # T4 没有 FlashAttention-2
).cuda()

lora = LoraConfig(
r=16,
lora_alpha=32, # α/r = 2 —— 见公式 3.2
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora)
model.print_trainable_parameters() # 信这一行,别信任何博客里的数字

for p in model.parameters(): # 第 5 节的 fp16 警告框
if p.requires_grad:
p.data = p.data.float()
如果打印出的百分比不是精确的 1.69,先别慌

peft 计算百分比时的分母是已含 adapter 的总参数量,而我们的 1.69% 除的是纯基座参数。 定义差一点点,数字就差一点点——而这恰恰是 3.3 节的重点: 这类数字必须知道分子分母各是什么,而不是把一个孤零零的数字背下来到处引用。

接下来是把公式 3.1 翻译成代码的那一块——充当 mtm_t 的 collator

collator = DataCollatorForCompletionOnlyLM(
response_template="<|im_start|>assistant\n", # 这之前的一切 = prompt
tokenizer=tok,
)

这个 collator 把 <|im_start|>assistant 之前所有 token 的 label 设为 -100—— 这是 PyTorch 的 loss function 会跳过的值,也正是第 4 章 seq_logp 函数里 用来掩掉 prompt 一侧的同一个值。同一个掩码,出现在不同的章节。

cfg = SFTConfig(
output_dir="sft-out",
dataset_text_field="text",
max_seq_length=768, # notebook 会打印被截断的样本数 —— 陷阱 3
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # effective batch = 16
num_train_epochs=1,
learning_rate=2e-4, # LoRA 扛得住 —— full FT 用这个值就是灾难(第 1 章)
lr_scheduler_type="cosine",
warmup_ratio=0.03,
packing=False, # completion mask 无法直接和 packing 一起用
fp16=True, # T4:不是 bf16
logging_steps=10,
)

trainer = SFTTrainer(
model=model,
args=cfg,
train_dataset=train_ds,
data_collator=collator,
processing_class=tok,
)
trainer.train() # 在 T4 上约 15 分钟
为什么 2e-4 在这里安全,在第 1 章却是灾难

同样一个学习率,如果用来训练全部参数,几百个 step 就会把模型的能力抹掉。 但配上 LoRA 它是安全的,因为(1)5.96 亿个原权重被冻结——base 里的知识不可能被覆写; (2)BB 从零出发——第一步时模型就是原封不动的 base,然后才慢慢走出去。 LoRA 能造成的最坏结果是一个糟糕的 adapter,而 adapter 随时可以扔掉。

训练结束,只保存修正项:

model.save_pretrained("qwen3-0.6b-th-sft-lora")   # ~40 MB —— 不是 1.2 GB

这个 adapter 正是第 4 章以 kobkrit/qwen3-0.6b-th-sft-lora 之名加载的那个, 既当初始 policy,又(在关掉 adapter 时)当 reference model——一份价值 40 MB 的跨章礼物。

8. 结果(Results)

notebook 会在训练前后各测三项指标,并写入 results.json

  1. TH-INSTR——通过指令遵循 rubric(答在点上、自己收尾、不出戏)的回答占比, 来自 KobEval-TH 评测集,附 Wilson 95% CI
  2. th_ratio——回答中泰文字符的占比,本系列的常驻指标。 初始的 Qwen3-0.6B 在泰语 prompt 上以滑回英语著称,用 4,000 条纯泰语数据做 SFT 应当让这个数字明显上移——如果没动,那就是先去排查掩码和 template 的信号,别的以后再说
  3. 在 token 预算内以 eos 收尾的回答占比——捕捉陷阱 1 和陷阱 3 造成的"停不下来"症状
再强调一次置信区间

一百来道题的评测集给出的 CI 宽度约 ±10 个点。没有 CI 的数字还不算实验结果。 第 9 节的表格里凡是要等 notebook 跑出真实数字的地方都放了 ?——我们不在博客里预猜结果。

Prompt
Promptอธิบายว่าทำไมท้องฟ้าถึงเป็นสีฟ้า แบบสั้น ๆ

base

Thai 18%41 tokens
The sky appears blue because of Rayleigh scattering. ท้องฟ้า is blue เพราะ light scatter ครับ. Shorter wavelengths scatter more than longer ones.

sft

Thai 99%78 tokens
ท้องฟ้าเป็นสีฟ้าเพราะแสงอาทิตย์กระทบกับโมเลกุลของอากาศแล้วเกิดการกระเจิงแบบเรย์ลี ซึ่งแสงสีน้ำเงินที่มีความยาวคลื่นสั้นกว่าจะกระเจิงได้มากกว่าแสงสีแดง เราจึงมองเห็นท้องฟ้าเป็นสีฟ้าครับ

Showing the built-in sample.

9. 对比(Comparison)

notebook 在同一份数据上训练三种配置,再与初始模型对照:

模型TH-INSTR (95% CI)th_ratioPPL训练参数训练时间
Qwen3-0.6B(base)73.3%(55.6–85.8)0.9321.3
LoRA r=16 + 补全掩码83.3%(66.4–92.7)0.9717.710.1M(1.69%)13.2 分钟
在 Colab T4 上实测 —— Qwen3-0.6B,wangchanx 4,000 条样本,显存峰值 7.42 GB, 250 个优化步。所有数字均来自 notebook 自动写出的 results.json

SFT 在每个维度都有提升:TH-INSTR +10 分(73.3% → 83.3%),th_ratio 上升 (更多回答留在泰语,不再漂移到英语),困惑度从 21.3 降到 17.7 —— 而只训练了 1.69% 的参数,adapter 仅约 40 MB。

置信区间仍然重叠

73.3% 对 83.3% 看着差距明显,但 Wilson 区间是 55.6–85.8 与 66.4–92.7,仍然重叠。 在 n=30 的规模下还达不到统计显著。更扎实的证据是建立在数千 token 上的 th_ratio 与 PPL。 要让 TH-INSTR 具备结论性,题目需增至数百(第 9 篇)。

关于全参数微调与不加掩码那一行: notebook 确实训练了一个不加掩码的 LoRA,用来展示模型 答完之后自问自答续写用户问题的行为(示例见 §8),但没有为它计算完整指标,也没有做 100% 参数的全参数微调 —— 那超出了免费 Colab 的预算。我们只报告实测到的数字,不为没有跑过的 实验填数。

10. 小结(Summary)

  • SFT 就是对话版的 CPT + 补全掩码——loss 还是第 1 章那个,变的是数据被布置成了对话,以及掩码决定哪些 token 计入
  • 掩码是成功的一半——mt1m_t \equiv 1 会让 60% 的梯度去学写提问,训出一个答完还自己编问题的模型
  • LoRA 训练的是"修正项",不是权重W=W0+αrBAW' = W_0 + \frac{\alpha}{r}BABB 从零出发,训练从 base 精确起步;α/r\alpha/r 让换 rank 不用重调 lr
  • 1.69% 不是"低于 1%"——adapter 随 hidden size 线性增长,base 按二次方增长,模型越小 adapter 占比越大。永远用 print_trainable_parameters() 核实
  • 每个任务约 40 MB 的 adapter:一个 base + 多个 adapter,这也是第 4 章 reference model 免费的原因
  • lr 2e-4 之所以安全,是因为 base 被冻结——同一个值在全参数训练下会毁掉模型
  • pad ≠ eos、template 只套一次、别让截断砍在回答中间——三个症状在 inference 现形、根子却在数据里的坑
这个实验的局限

SFT 教的是"格式与风格",远大于"知识"——4,000 条样本不会往模型里添加任何新的事实。 模型不知道的事,SFT 之后它依然不知道,只是错得格式更漂亮、语气更自信—— 这反而更危险。灌知识是第 1 章(CPT)的工作,不是本章的。

和每一章一样:4,000 条样本演示的是机制。生产级的 SFT 用的是数万到数百万对、 经过多层质量筛选的数据。你从本章得到的是"每个旋钮做什么、会怎么坏"的理解, 这部分可以迁移到真实规模——但请不要拿这个结果去宣称你得到了更强的泰语模型。

下一章: RLHF 与 PPO——模型会回答了,但"答得好"写不成 loss function。 我们会让人类比较回答、从这些偏好中训练 reward model,再用 reinforcement learning 把模型推向它—— 而起点,就是本章这个 40 MB 的 adapter。

参考文献(References)

  1. Hu et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models — 第 3 节 W' = W₀ + (α/r)BA 公式的出处
  2. Aghajanyan et al. (2020). Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning — 微调具有低内在维度的证据——LoRA 为何有效
  3. Dettmers et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs — 用 4-bit 扩展 LoRA,使更大的模型能装进单张 GPU
  4. Biderman et al. (2024). LoRA Learns Less and Forgets Less — LoRA 以学得更少换取忘得更少——建议与第 9 节对读
  5. Ouyang et al. (2022). Training language models to follow instructions with human feedback — InstructGPT:整条 SFT -> RM -> PPO 流水线的源头
  6. Wang et al. (2022). Self-Instruct: Aligning Language Models with Self-Generated Instructions — 用模型自身生成指令数据
  7. Zhou et al. (2023). LIMA: Less Is More for Alignment — 几千条高质量样本就够——我们只用 4,000 条的理由

本系列的文章、代码与 notebook 均以 CC BY-NC-SA 4.0 授权 —— 可自由使用与改编,须署名、限非商业用途,并以相同方式共享。文中引用的第三方模型与数据集仍适用各自的许可证。