核心摘要
微调会改变模型参数,或增加可训练适配器,使基础模型在目标分布上的行为发生变化。它适合稳定的行为、输出结构、工具调用约定或领域任务模式;不适合替代频繁更新的知识库,也不会自动让私有数据安全、事实正确、获得授权或可删除。
应先建立提示词和检索基线。只有当可测量的失败持续存在、训练数据具备授权且有代表性,并且团队能够运行能力、安全、隐私和回归评测时,才进入微调。所有显存、质量、成本和基准数字都应绑定模型修订、分词器、序列长度、优化器、硬件和日期,作为实验结果记录。
微调改变了什么
微调决策有两个相互独立的轴。学习目标描述训练信号:持续预训练预测语料 Token,SFT 模仿选定目标 Token,偏好优化比较候选行为或学习奖励。可训练参数面描述哪些参数变化:全参数更新、LoRA 等 PEFT 适配器,或像 QLoRA 一样在量化存储的冻结基础模型上训练适配器。LoRA + SFT 与 LoRA + DPO 都成立;把 LoRA 说成 SFT 的替代品,是混淆了两个轴。
| 维度 | 可选方案 | 必须声明的内容 |
|---|---|---|
| 学习目标 | 持续预训练、SFT、偏好优化、任务专用目标 | 记录格式、标签语义、Loss、Mask、Reference 或 Reward Model |
| 可训练参数面 | 全参数、选定层、LoRA/其他 PEFT、Prompt/Prefix 参数 | 冻结与训练的 Tensor、目标模块、秩、初始化 |
| 存储与计算 | 全精度、混合精度、量化冻结底座、分布式切分 | 存储 Dtype、计算 Dtype、Kernel、Optimizer、硬件拓扑 |
微调可能改善:
- 稳定的回答格式或风格;
- 分类、抽取、路由或工具调用模式;
- 明确领域分布上的任务表现;
- 数据和评测充分时,小模型的特定任务能力。
它不能可靠提供:
- 当前数据库中的事实;
- 授权或租户隔离;
- 某条记忆样本的删除;
- 对提示注入或不安全输出的免疫;
- 模型必然引用或遵循每条训练样本的保证。
选择干预方式
| 失败或要求 | 首先尝试 | 原因 |
|---|---|---|
| 当前事实或长文档 | 带引用的检索 | 知识可更新且可做访问控制 |
| 稳定输出 Schema 或风格 | 提示词、约束解码,再考虑 SFT | 先测量是否值得写入权重 |
| 重复分类/抽取 | 小型监督模型或 SFT | 有界标签空间更容易评测 |
| 偏好或答案排序 | 先有强 SFT 基线,再做偏好优化 | 目标不同于模仿 |
| 延迟或成本 | 小模型、缓存、批处理、量化推理 | 微调不一定解决服务成本 |
| 授权或安全策略 | 运行时策略和沙箱控制 | 模型权重不是安全边界 |
RAG 与微调可以共存:训练可以教会格式和使用检索的行为,检索则提供当前且经过权限过滤的证据。检索内容仍是不可信数据,不是指令。
全量微调、LoRA 与 QLoRA
全量微调
更新全部权重需要同时考虑权重、梯度、优化器状态、激活、临时缓冲和检查点。单独说“7B 模型需要多少 GB”是不完整的,还必须说明精度、优化器、序列长度、微批量、激活检查点、并行方式和框架。应估算并测量完整训练任务,而不是复制固定数字。
LoRA
LoRA 为选定模块加入低秩更新:
W' = W + scale × B × A
秩、目标模块、缩放、dropout 和初始化都是设计选择。可训练参数比例和质量变化依赖架构与配置,“0.1–1%”不是普遍保证。适配器便于版本管理和切换,但多个适配器可能冲突,合并后也会改变数值行为。
QLoRA
QLoRA 将适配器与量化的冻结基础权重结合。NF4、双重量化、分页优化器、计算 dtype、内核和硬件都会影响结果。量化常能降低部分配置的显存,但不代表固定质量或固定显存;必须用相同基线和任务切片评估量化模型。
数据集工程
来源与授权
每条记录都应保留来源、许可证或许可、采集日期、变换步骤、标注/复核者和删除血缘。移除密钥、不必要的个人数据、凭据及未经授权使用的数据。来源撤回后,模型检查点或适配器并不自动容易清除。
先切分,再变换
在改写、切块、增强或对话模板化之前,先按样本身份创建训练、验证和测试集。跨集合去重近似重复样本,并保留用于检测基准污染的集合。不要在最终测试集上调超参数。
格式与质量
使用基础模型文档规定的聊天模板,保留角色边界。校验 JSONL 记录、分词长度、空轮次、语言、标签、工具参数和拒答样本。审查普通与对抗案例。不要把“100–10,000 条”当目标;样本效率取决于任务熵、基础模型能力、标签噪声和覆盖范围。
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
)
type Record struct {
RecordID string `json:"record_id"`
GroupID string `json:"group_id"`
Split string `json:"split"`
Source string `json:"source"`
License string `json:"license"`
Prompt string `json:"prompt"`
Response string `json:"response"`
}
func validateJSONL(reader io.Reader) (int, error) {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 64*1024), 4*1024*1024)
seenRecords := map[string]bool{}
groupSplits := map[string]string{}
count := 0
for scanner.Scan() {
count++
var record Record
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
return 0, fmt.Errorf("line %d: %w", count, err)
}
if record.RecordID == "" || record.GroupID == "" || record.Source == "" ||
record.License == "" || strings.TrimSpace(record.Prompt) == "" ||
strings.TrimSpace(record.Response) == "" {
return 0, fmt.Errorf("line %d: missing content or provenance", count)
}
if seenRecords[record.RecordID] {
return 0, fmt.Errorf("line %d: duplicate record_id", count)
}
if split, ok := groupSplits[record.GroupID]; ok && split != record.Split {
return 0, fmt.Errorf("line %d: group leaks across splits", count)
}
if record.Split != "train" && record.Split != "validation" && record.Split != "test" {
return 0, fmt.Errorf("line %d: invalid split", count)
}
seenRecords[record.RecordID] = true
groupSplits[record.GroupID] = record.Split
}
if err := scanner.Err(); err != nil {
return 0, err
}
if count == 0 {
return 0, errors.New("empty_dataset")
}
return count, nil
}
func main() {
input := `{"record_id":"r1","group_id":"g1","split":"train","source":"reviewed","license":"approved","prompt":"Classify A","response":"alpha"}
{"record_id":"r2","group_id":"g2","split":"test","source":"reviewed","license":"approved","prompt":"Classify B","response":"beta"}`
count, err := validateJSONL(strings.NewReader(input))
fmt.Printf("records=%d err=%v\n", count, err)
}
// Output: records=2 err=<nil>
这段代码校验 Shape、声明的 Provenance、Record ID 唯一性与 Group-level Split Isolation;它不能证明事实、授权、隐私、标签质量或任务正确性。
可复现的训练运行
框架 API 和模型许可证会变化。应固定基础模型修订、分词器修订、框架与加速栈版本、数据集哈希、聊天模板、随机种子、硬件、精度和训练配置,并写入实验清单。
下面的清单刻意分开学习目标与可训练参数面,不宣称通用模型 ID、目标模块、序列长度或学习率:
run:
objective: sft
base_model_revision: immutable-commit
tokenizer_revision: immutable-commit
chat_template_sha256: sha256-of-template
dataset_revision: sha256-of-split-manifest
framework_lock_sha256: sha256-of-lockfile
seed_policy: [11, 29, 47]
trainable_surface:
method: lora
target_modules: reviewed-list
rank: measured-candidate
precision:
weight_storage: declared
compute: declared
optimizer_state: declared
evaluation:
frozen_suite_revision: immutable-commit
baseline_model_revision: immutable-commit
release_policy_revision: policy-17
实际运行必须确认聊天模板、Padding 方向、EOS 行为、Assistant 或 Completion Label Mask、Packing Boundary、截断策略、量化计算 Dtype、目标模块与基础模型一致。当前 TRL 支持 Conversational 与 Prompt-completion 格式,但默认 Loss Mask 不同;合法 JSONL 仍可能训练了错误 Token。trust_remote_code=True 会执行仓库代码,不应作为默认值,必须使用审查过且固定版本的来源。
应有意识地使用梯度累积:
有效 batch = 设备数 × 每设备微批量 × 累积步数
它会改变优化动态,并不等价于拥有无限显存。应监控每秒 token、峰值显存、损失、验证损失、梯度异常、检查点完整性和中断恢复。
部署前评测
在同一冻结测试集上比较未修改的基础模型、仅提示词基线、相关的 RAG 基线和每个微调候选。至少包括:
- 与任务匹配的准确率或 exact match;
- 结构化输出有效性和 Schema 合规;
- 使用检索时的引用/证据正确性;
- 校准或拒答行为;
- 安全、隐私、越狱、提示注入和工具使用切片;
- 多语言、长上下文、罕见案例和分布外切片;
- 延迟、吞吐、显存、成本和失败率。
BLEU 和 ROUGE 可用于窄领域生成任务,但不是通用事实性、帮助性或安全指标。适当时使用多参考答案或任务评分量表、盲审、评审者一致性和不确定性区间。报告模型修订、提示词、数据、基线、指标定义、硬件、精度、采样策略、日期和样本数。
package main
import (
"fmt"
"strings"
)
func normalize(value string) string {
return strings.ToLower(strings.Join(strings.Fields(value), " "))
}
func exactMatchRate(predictions, references []string) (float64, error) {
if len(predictions) == 0 || len(predictions) != len(references) {
return 0, fmt.Errorf("predictions and references must have equal non-zero length")
}
matches := 0
for index := range predictions {
if normalize(predictions[index]) == normalize(references[index]) {
matches++
}
}
return float64(matches) / float64(len(references)), nil
}
func main() {
rate, err := exactMatchRate(
[]string{"ALPHA", "beta value", "wrong"},
[]string{"alpha", "beta value", "gamma"},
)
fmt.Printf("rate=%.2f err=%v\n", rate, err)
}
// Output: rate=0.67 err=<nil>
不要复用训练样本评测,不要只报告挑选出来的 prompt,也不要仅凭损失推断事实性。验证损失下降可能同时伴随指令遵循、安全性或记忆风险变差。
超参数与消融
学习率、秩、alpha、dropout、轮数、packing、序列长度、批量、warmup、优化器和量化是相互作用的变量。应先做小规模、可记录的搜索,一次改变一个有意义因素,并保存精确配置与基线比较。
有价值的消融包括:
- 基础模型与适配器;
- 不同数据子集和去重策略;
- 目标模块与秩;
- 全精度与量化加载;
- 仅提示词、RAG 与 SFT;
- 去除可能污染或合成记录前后的差异。
不要在模型、数据、目标和评测协议不一致时照搬论文学习率或硬件结果。论文结果只证明其设置下的证据,不是通用配方。
部署与治理
将基础模型、适配器、分词器、聊天模板、提示词、安全策略、依赖锁文件和实验清单一起版本化。加载前验证模型与适配器兼容性,扫描产物和日志中的密钥与个人数据。
服务运行时负责:
- 认证、租户/对象授权和工具允许列表;
- 输入/输出 Schema 校验与内容控制;
- 限流、预算、超时、取消和审计日志;
- 检索权限与删除传播;
- 回滚和流量影子测试。
适配器不是使用训练数据的许可证,私有检查点也不是数据不可访问的证明。训练前应定义保留、删除、访问、导出和事故流程。
常见失败模式
| 失败 | 原因 | 更好的控制 |
|---|---|---|
| 模型记住密钥或基准答案 | 泄漏、重复或污染切分 | 来源、去重、canary、成员推断/隐私检查 |
| 损失下降但任务质量变差 | 模板、标签、分布或指标错误 | 冻结基线和任务切片 |
| 把“私有训练”当作安全保证 | 检查点、日志、托管服务和产物可能泄露 | 威胁模型、访问策略、加密、留存和审查 |
| QLoRA 显存无法复现 | 量化器、序列长度、内核或优化器不同 | 记录完整环境并测量峰值显存 |
| 模型会模仿风格但编造事实 | 风格学习不是事实 grounding | 检索、引用、拒答和事实性评测 |
| 适配器只适配一个基础修订 | 产物未固定或不兼容 | 固定并验证模型/分词器/适配器修订 |
常见问题
微调需要多少数据?
没有通用数量。应从有代表性且有授权的小规模试验和未触碰测试集开始,用学习曲线、错误切片、标注质量和基线差距判断,而不是套用 100/1,000/10,000 规则。
LoRA 的秩控制什么?
秩控制低秩更新的容量,不保证质量。更高的秩可能增加容量、可训练参数和显存,也可能增加过拟合风险。应在受控消融中比较不同秩。
微调能加入可靠的领域知识吗?
它可以改善训练分布上的行为,但不是可查询、持续更新的知识存储。变化中的事实应使用经过权限过滤的检索,并评测事实性,不要假定记忆必然正确。
微调能删除私有数据吗?
不能可靠保证。数据集删除并不证明检查点、适配器、优化器状态、缓存、日志或衍生产物已经删除。训练前就要规划数据血缘和删除测试。
微调与 RAG 可以结合吗?
通常可以。微调教会稳定格式或检索行为,RAG 提供当前证据;检索授权和提示注入防护必须留在运行时。
如何评估微调效果?
使用冻结任务测试、基线比较、安全/隐私和分布外切片、结构化输出检查、人工复核以及成本/延迟测量。公布协议和不确定性,不要只展示一个好看的分数。
一手来源
- Hu 等:LoRA
- Dettmers 等:QLoRA
- Rafailov 等:Direct Preference Optimization
- Hugging Face PEFT 文档
- Hugging Face TRL SFT Trainer
- OpenAI Model Optimization
- NIST AI Risk Management Framework
- OWASP:大语言模型应用十大风险
总结
微调是实验性干预,不是普遍升级。应先建立基线,使用来源清晰的数据,固定完整环境,测量真实资源路径,并在未触碰切片上评估质量、安全、隐私、泛化、成本和不确定性。当 LoRA 或 QLoRA 的运营取舍适合实验时再采用,并把授权和治理留在模型权重之外。