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_config = { "r": 8, "lora_alpha": 16, "lora_dropout": 0.05, "target_modules": [ "q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj" ], "bias": "none", "task_type": "CAUSAL_LM" }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from peft import get_peft_model, LoraConfig
base_model = "meta-llama/Llama-2-7b"
lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" )
model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()
|
3. QLoRA(Quantized LoRA)
LoRA + 量化,进一步减少显存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", 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" )
model = get_peft_model(model, lora_config)
|
4. prefix-tuning / Prompt-tuning
只训练前缀向量:
1 2 3 4 5 6 7 8
| from peft import PrefixTuningConfig
prefix_config = PrefixTuningConfig( num_virtual_tokens=20, 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]
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+ 条 |
对话质量很重要 |
训练实战
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
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" )
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)
training_args = TrainingArguments( output_dir="./output", num_train_epochs=3, per_device_train_batch_size=4, gradient_accumulation_steps=4, 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" )
data_collator = DataCollatorForLanguageModeling( tokenizer=tokenizer, mlm=False )
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
| base_model: meta-llama/Llama-2-7b model_type: LlamaForCausalLM
dataset: path: ./data/training_data.json type: alpaca
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
| from unsloth import FastLanguageModel import torch
model, tokenizer = FastLanguageModel.from_pretrained( model_name="unsloth/llama-2-7b-bnb-4bit", max_seq_length=2048, dtype=None, load_in_4bit=True, )
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
| 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)
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" )
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
| python llama.cpp/convert.py \ ./merged_model \ --outtype q8_0 \ --outfile ./model-q8.gguf
|
微调 vs RAG
| 维度 |
微调 |
RAG |
| 知识更新 |
需要重新训练 |
实时更新知识库 |
| 知识类型 |
泛化知识 |
精确事实 |
| 成本 |
高(需要训练) |
低(只需索引) |
| 适用场景 |
风格、格式、领域适应 |
私有知识、实时信息 |
| 幻觉风险 |
可能继承模型幻觉 |
可追溯,减少幻觉 |
选择建议:
- 知识库问答 → RAG
- 风格/格式定制 → 微调
- 两者结合 → 最佳效果
总结
微调是将通用 LLM 适配到特定任务的有效方法:
- 选择合适方法:数据少用 LoRA/QLoRA,数据多用全量微调
- 重视数据质量:数据量不是越多越好,质量更重要
- 控制训练参数:学习率、epoch、batch_size 需要调优
- 充分评估验证:自动化指标 + 人工评估
- 注意资源消耗:量化和梯度检查点是救命稻草
掌握微调技术,就能让通用模型变成你的专属智能助手。
有问题欢迎留言讨论!