LLM 微调实战:从理论到落地

什么是模型微调

模型微调(Fine-tuning)是在预训练模型的基础上,用特定领域或任务的数据进行额外训练,使模型适应特定需求。与从头训练相比,微调需要的资源和数据量都少得多。

简单类比:预训练模型像是一个学完所有基础课程的学生,微调则是针对某个专业方向的强化训练。

什么时候需要微调

vs Prompt Engineering

场景 推荐方案
简单任务 Prompt Engineering
风格定制 Prompt Engineering / 微调
领域术语 微调
特定格式输出 微调
私有知识问答 RAG
复杂推理能力 Prompt Engineering / 微调

需要微调的信号

1
2
3
4
5
6
7
signals_to_finetune = [
"模型在特定领域反复出错",
"需要固定的输出格式",
"需要模型学会某种特定语言风格",
"Prompt 工程已经无法满足效果要求",
"需要在边缘设备上高效运行",
]

微调的主要方法

1. 全量微调(Full Fine-tuning)

更新模型所有参数:

1
2
3
4
5
6
7
8
9
10
11
12
# 适用场景:数据量充足,有充足算力
# 优点:效果最好
# 缺点:成本高,容易过拟合,灾难性遗忘

training_config = {
"learning_rate": 1e-5,
"batch_size": 4,
"epochs": 3,
"warmup_steps": 100,
"optimizer": "adamw",
"weight_decay": 0.01,
}

2. LoRA(Low-Rank Adaptation)

只训练少量低秩矩阵:

1
2
3
4
5
6
预训练权重: W₀ (d × d)
LoRA 策略:
- 冻结 W₀
- 添加两个低秩矩阵 A (d × r) 和 B (r × d)
- 新权重: W = W₀ + BA
- 其中 r << d,大大减少参数量
1
2
3
4
5
6
7
8
9
10
11
12
13
# LoRA 配置示例
lora_config = {
"r": 8, # 秩的大小
"lora_alpha": 16, # 缩放因子
"lora_dropout": 0.05, # Dropout 概率
"target_modules": [ # 应用 LoRA 的模块
"q_proj", "v_proj",
"k_proj", "o_proj",
"gate_proj", "up_proj"
],
"bias": "none", # 不训练 bias
"task_type": "CAUSAL_LM" # 任务类型
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用 PEFT 库应用 LoRA
from peft import get_peft_model, LoraConfig

base_model = "meta-llama/Llama-2-7b"

# LoRA 配置
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)

# 创建 LoRA 模型
model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 4M || all params: 6.7B || trainable%: 0.06%

3. QLoRA(Quantized LoRA)

LoRA + 量化,进一步减少显存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# QLoRA 配置
from transformers import BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4 位量化
bnb_4bit_quant_type="nf4", # Normal Float 4
bnb_4bit_compute_dtype="float16",
bnb_4bit_use_double_quant=True # 双重量化
)

# 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
quantization_config=bnb_config,
device_map="auto"
)

# 应用 LoRA
model = get_peft_model(model, lora_config)

4. prefix-tuning / Prompt-tuning

只训练前缀向量:

1
2
3
4
5
6
7
8
# prefix-tuning 示例
from peft import PrefixTuningConfig

prefix_config = PrefixTuningConfig(
num_virtual_tokens=20, # 虚拟 token 数量
prefix_projection=True, # 是否投影
hidden_size=128,
)

训练数据准备

数据格式(Alpaca 格式)

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"instruction": "将以下中文翻译成英文",
"input": "今天天气真好",
"output": "The weather is nice today."
},
{
"instruction": "判断情感是正面还是负面",
"input": "这个产品太棒了",
"output": "正面"
}
]

聊天格式(ChatML)

1
2
3
4
5
6
7
8
9
[
{
"messages": [
{"role": "system", "content": "你是一个有帮助的助手"},
{"role": "user", "content": "什么是大语言模型?"},
{"role": "assistant", "content": "大语言模型是..."}
]
}
]

数据清洗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def clean_training_data(data):
cleaned = []

for item in data:
# 去除空白
item = item.strip()

# 过滤太短的样本
if len(item) < 10:
continue

# 过滤太长导致截断
if len(item) > 2048:
item = item[:2048]

# 去除特殊字符(可选)
# item = re.sub(r'[^\w\s\u4e00-\u9fff]', '', item)

cleaned.append(item)

return cleaned

def deduplicate(data):
"""去除重复数据"""
seen = set()
unique_data = []

for item in data:
if item not in seen:
seen.add(item)
unique_data.append(item)

return unique_data

数据量建议

任务类型 建议数据量 说明
风格迁移 100-500 条 主要是格式和风格
领域适应 500-2000 条 注入领域知识
指令遵循 1000-10000 条 需要多样化覆盖
对话系统 5000+ 条 对话质量很重要

训练实战

使用 Hugging Face Transformers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from datasets import load_dataset

# 1. 加载模型和分词器
model_name = "meta-llama/Llama-2-7b"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
model_name,
device_map="auto"
)

# 2. 准备数据
def tokenize_function(examples):
result = tokenizer(
examples["text"],
truncation=True,
max_length=512,
padding="max_length"
)
result["labels"] = result["input_ids"].copy()
return result

dataset = load_dataset("json", data_files="training_data.json")
tokenized_dataset = dataset.map(tokenize_function, batched=True)

# 3. 训练配置
training_args = TrainingArguments(
output_dir="./output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 有效 batch_size = 4*4=16
learning_rate=1e-4,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_steps=500,
save_total_limit=3,
fp16=True,
optim="paged_adamw_8bit",
report_to="tensorboard"
)

# 4. 数据整理器
data_collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False # Causal LM 不是 MLM
)

# 5. 开始训练
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset["train"],
data_collator=data_collator,
)

trainer.train()

使用 Axolotl(简化训练配置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# axolotl 配置文件
base_model: meta-llama/Llama-2-7b
model_type: LlamaForCausalLM

# 数据
dataset:
path: ./data/training_data.json
type: alpaca

# LoRA 配置
lora_config:
r: 8
lora_alpha: 16
dropout: 0.05
modules_to_save: null

# 训练参数
sequence_len: 2048
batch_size: 4
gradient_accumulation: 4
num_epochs: 3
learning_rate: 0.0002
warmup_ratio: 0.03
optimizer: adamw_torch

# 量化配置
load_in_4bit: true
bf16: false
fp16: true
1
2
# 一行命令启动训练
axolotl train configs/llama2-7b-lora.yaml

使用 Unsloth(加速训练)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Unsloth 可以实现 2-5x 加速
from unsloth import FastLanguageModel
import torch

# 加载模型(自动应用 LoRA)
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-2-7b-bnb-4bit",
max_seq_length=2048,
dtype=None,
load_in_4bit=True,
)

# 添加 LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing="unsloth"
)

# 训练
FastLanguageModel.for_training(model)

# 准备数据并训练...

评估与验证

常用指标

指标 说明 适用场景
Perplexity 语言模型的困惑度,越低越好 通用语言建模
Rouge-L 生成文本与参考文本的相似度 摘要、翻译
BLEU N-gram 精确率 机器翻译
Accuracy 准确率 分类、问答
人工评估 人工打分 所有场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 计算 Perplexity
def calculate_perplexity(model, tokenizer, dataset):
encodings = tokenizer("\n\n".join(dataset), return_tensors="pt")

max_length = model.config.max_position_embeddings
stride = 512

nlls = []
for i in range(0, encodings.input_ids.size(1), stride):
begin_loc = max(i + stride - max_length, 0)
end_loc = min(i + stride, encodings.input_ids.size(1))
trg_len = end_loc - i

input_ids = encodings.input_ids[:, begin_loc:end_loc].to(model.device)
target_ids = input_ids.clone()
target_ids[:, :-trg_len] = -100

with torch.no_grad():
outputs = model(input_ids, labels=target_ids)
neg_log_likelihood = outputs.loss * trg_len

nlls.append(neg_log_likelihood)

ppl = torch.exp(torch.stack(nlls).sum() / end_loc)
return ppl.item()

人工评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
evaluation_prompt = """
请对以下模型输出进行评估:

任务:情感分类
输入:等了半小时还没上菜,太失望了
模型输出:负面

请从以下维度评分(1-5分):
1. 准确性:输出是否正确?
2. 相关性:输出是否相关?
3. 自然度:表达是否自然?
4. 完整性:是否完整回答?

评分:
"""

常见问题与解决

1. 灾难性遗忘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 问题:微调后模型忘记了原有能力
# 解决:混合预训练数据

def create_mixed_dataset(task_data, pretrain_data, ratio=0.1):
"""将预训练数据以一定比例混入任务数据"""
mixed = []

for item in task_data:
mixed.append(item)

# 每N条任务数据,混入1条预训练数据
if len(mixed) % (1/ratio) == 0:
mixed.append(pretrain_data.pop(0))

return mixed

2. 过拟合

1
2
3
4
5
6
7
8
# 解决策略
solutions = {
"增加数据": "数据量不足导致",
"减小学习率": "过大的学习率导致",
"增加 dropout": "模型过于复杂",
"减少训练轮数": "训练过多轮",
"使用 LoRA": "减少可训练参数",
}

3. 训练不稳定

1
2
3
4
5
6
# 解决:梯度裁剪 + 权重衰减
training_args = TrainingArguments(
max_grad_norm=1.0, # 梯度裁剪
weight_decay=0.01, # 权重衰减
warmup_ratio=0.03, # 预热
)

4. 显存不足

1
2
3
4
5
6
7
8
# 解决策略
memory_solutions = [
"使用 4bit/8bit 量化",
"启用梯度检查点(gradient checkpointing)",
"减小 batch_size,增加 gradient_accumulation",
"使用更小的模型(如 7B -> 3B)",
"使用 DeepSpeed ZeRO",
]

模型合并与导出

合并 LoRA 权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from peft import PeftModel

# 加载基座模型
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
torch_dtype=torch.float16,
device_map="auto"
)

# 合并 LoRA 权重
model = PeftModel.from_pretrained(base_model, "./lora_output")
model = model.merge_and_unload()

# 保存
model.save_pretrained("./merged_model")
tokenizer.save_pretrained("./merged_model")

导出为 GGUF(用于 llama.cpp)

1
2
3
4
5
# 使用 llama.cpp 的 convert.py 转换
python llama.cpp/convert.py \
./merged_model \
--outtype q8_0 \
--outfile ./model-q8.gguf

微调 vs RAG

维度 微调 RAG
知识更新 需要重新训练 实时更新知识库
知识类型 泛化知识 精确事实
成本 高(需要训练) 低(只需索引)
适用场景 风格、格式、领域适应 私有知识、实时信息
幻觉风险 可能继承模型幻觉 可追溯,减少幻觉

选择建议:

  • 知识库问答 → RAG
  • 风格/格式定制 → 微调
  • 两者结合 → 最佳效果

总结

微调是将通用 LLM 适配到特定任务的有效方法:

  1. 选择合适方法:数据少用 LoRA/QLoRA,数据多用全量微调
  2. 重视数据质量:数据量不是越多越好,质量更重要
  3. 控制训练参数:学习率、epoch、batch_size 需要调优
  4. 充分评估验证:自动化指标 + 人工评估
  5. 注意资源消耗:量化和梯度检查点是救命稻草

掌握微调技术,就能让通用模型变成你的专属智能助手。


有问题欢迎留言讨论!