大模型微调技术
大约 11 分钟约 3372 字
大模型微调技术
简介
大语言模型(LLM)微调是将预训练模型适配到特定任务的技术。理解 LoRA、QLoRA 等参数高效微调(PEFT)方法、训练数据准备和微调流程,有助于在有限的 GPU 资源下定制专属模型。
特点
LoRA 微调
低秩适配原理
# LoRA(Low-Rank Adaptation)
# 核心思想:冻结原始权重 W,只训练低秩矩阵 A 和 B
# W' = W + BA,其中 B ∈ R^(d×r), A ∈ R^(r×d), r << d
import torch
import torch.nn as nn
from dataclasses import dataclass
@dataclass
class LoRAConfig:
r: int = 8 # LoRA 秩(通常 4-64)
lora_alpha: int = 16 # 缩放因子
lora_dropout: float = 0.05
target_modules: list = None # 目标模块名
class LoRALayer(nn.Module):
"""LoRA 适配层"""
def __init__(self, original_layer: nn.Linear, r: int = 8, lora_alpha: int = 16):
super().__init__()
self.original_layer = original_layer
self.r = r
self.lora_alpha = lora_alpha
self.scaling = lora_alpha / r
d_in = original_layer.in_features
d_out = original_layer.out_features
# 低秩矩阵
self.lora_A = nn.Parameter(torch.randn(r, d_in) * 0.01) # (r, d_in)
self.lora_B = nn.Parameter(torch.zeros(d_out, r)) # (d_out, r)
self.lora_dropout = nn.Dropout(0.05)
# 冻结原始权重
self.original_layer.weight.requires_grad = False
if self.original_layer.bias is not None:
self.original_layer.bias.requires_grad = False
def forward(self, x):
# 原始路径
original_output = self.original_layer(x)
# LoRA 路径
lora_input = self.lora_dropout(x)
lora_output = lora_input @ self.lora_A.T @ self.lora_B.T * self.scaling
return original_output + lora_output
def merge_weights(self):
"""训练完成后合并权重"""
with torch.no_grad():
self.original_layer.weight.data += (
self.lora_B @ self.lora_A * self.scaling
)
# 释放 LoRA 参数
del self.lora_A
del self.lora_B
# 应用 LoRA 到模型
def apply_lora_to_model(model, config: LoRAConfig):
"""将 LoRA 应用到目标模块"""
target_names = set(config.target_modules or ["q_proj", "v_proj", "k_proj", "o_proj"])
lora_modules = []
for name, module in model.named_modules():
if isinstance(module, nn.Linear) and any(t in name for t in target_names):
# 替换为 LoRA 层
lora_layer = LoRALayer(module, r=config.r, lora_alpha=config.lora_alpha)
# 设置父模块的属性
parts = name.split('.')
parent = model
for part in parts[:-1]:
parent = getattr(parent, part)
setattr(parent, parts[-1], lora_layer)
lora_modules.append(name)
# 冻结非 LoRA 参数
for name, param in model.named_parameters():
if 'lora_' not in name:
param.requires_grad = False
# 统计参数量
total_params = sum(p.numel() for p in model.parameters())
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"总参数: {total_params:,}")
print(f"可训练参数: {trainable_params:,} ({100*trainable_params/total_params:.2f}%)")
return model, lora_modules
# 使用 Hugging Face PEFT 库(推荐)
# pip install peft transformers datasets accelerate
from peft import LoraConfig, get_peft_model, TaskType
def create_lora_model(model_name="meta-llama/Llama-2-7b-hf"):
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
model_name,
load_in_4bit=True, # 4-bit 量化加载
device_map="auto",
torch_dtype=torch.bfloat16
)
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # 秩
lora_alpha=32, # 缩放因子
lora_dropout=0.05,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj", # 注意力层
"gate_proj", "up_proj", "down_proj" # FFN 层
],
bias="none"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出示例: trainable params: 13,107,200 || all params: 6,738,415,616 || trainable%: 0.1945
return modelQLoRA 微调
4-bit 量化微调
# QLoRA = 4-bit 量化 + LoRA
# 关键技术:NF4 量化 + 双重量化 + 分页优化器
from transformers import BitsAndBytesConfig
def create_qlora_config():
"""QLoRA 量化配置"""
return BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # Normal Float 4-bit
bnb_4bit_compute_dtype=torch.bfloat16, # 计算精度
bnb_4bit_use_double_quant=True, # 双重量化(量化常数也量化)
bnb_4bit_quant_storage=torch.uint8
)
# QLoRA 训练流程
def qlora_training_pipeline():
from transformers import AutoTokenizer, AutoModelForCausalLM, TrainingArguments
from trl import SFTTrainer
from datasets import load_dataset
model_name = "meta-llama/Llama-2-7b-hf"
# 1. 加载 tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 2. 4-bit 加载模型
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=create_qlora_config(),
device_map="auto",
attn_implementation="flash_attention_2" # Flash Attention 2
)
# 3. LoRA 配置
peft_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]
)
# 4. 加载训练数据
dataset = load_dataset("json", data_files="train_data.jsonl")
def format_instruction(sample):
"""格式化为指令微调格式"""
return f"""### Instruction:
{sample['instruction']}
### Input:
{sample['input']}
### Response:
{sample['output']}"""
# 5. 训练参数
training_args = TrainingArguments(
output_dir="./lora-output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4, # 等效 batch_size = 16
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
bf16=True,
logging_steps=10,
save_strategy="epoch",
evaluation_strategy="epoch",
gradient_checkpointing=True, # 节省显存
optim="paged_adamw_8bit", # 分页优化器(8-bit)
max_grad_norm=1.0,
seed=42
)
# 6. 创建训练器
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=dataset["train"],
peft_config=peft_config,
formatting_func=format_instruction,
max_seq_length=2048
)
# 7. 开始训练
trainer.train()
# 8. 保存 LoRA 权重
trainer.model.save_pretrained("./lora-output/final")
tokenizer.save_pretrained("./lora-output/final")
return trainer训练数据准备
SFT 数据格式
# SFT(Supervised Fine-Tuning)数据格式
# 1. Alpaca 格式
alpaca_format = {
"instruction": "将以下句子翻译成英文",
"input": "今天天气很好",
"output": "The weather is nice today."
}
# 2. ShareGPT 格式(多轮对话)
sharegpt_format = {
"conversations": [
{"role": "user", "content": "什么是机器学习?"},
{"role": "assistant", "content": "机器学习是人工智能的一个分支..."},
{"role": "user", "content": "它和深度学习有什么区别?"},
{"role": "assistant", "content": "深度学习是机器学习的子集..."}
]
}
# 3. 数据质量过滤
class DataQualityFilter:
"""训练数据质量过滤器"""
def __init__(self, tokenizer, max_length=2048):
self.tokenizer = tokenizer
self.max_length = max_length
def filter_sample(self, sample):
"""过滤低质量样本"""
# 长度过滤
text = f"{sample.get('instruction', '')} {sample.get('input', '')} {sample.get('output', '')}"
tokens = self.tokenizer.encode(text)
if len(tokens) > self.max_length:
return False
# 输出不能太短
output = sample.get('output', '')
if len(output) < 10:
return False
# 去重(简单的精确匹配)
return True
def deduplicate(self, samples):
"""基于 MinHash 的近似去重"""
from datasketch import MinHash, MinHashLSH
lsh = MinHashLSH(threshold=0.8, num_perm=128)
unique_samples = []
for i, sample in enumerate(samples):
text = sample.get('output', '')
mh = MinHash(num_perm=128)
for word in text.split():
mh.update(word.encode('utf-8'))
if not lsh.query(mh):
lsh.insert(str(i), mh)
unique_samples.append(sample)
print(f"去重: {len(samples)} → {len(unique_samples)}")
return unique_samples
# 4. 数据增强
class DataAugmenter:
"""训练数据增强"""
@staticmethod
def augment_with_paraphrase(sample, paraphraser):
"""同义改写增强"""
original_output = sample['output']
paraphrased = paraphraser.paraphrase(original_output)
return {**sample, 'output': paraphrased}
@staticmethod
def augment_with_self_instruct(topic, llm):
"""使用 LLM 自我生成指令"""
prompt = f"""请为"{topic}"主题生成 5 个问答对。
格式:
- Q: 问题
- A: 答案"""
response = llm.generate(prompt)
return response合并与推理
LoRA 权重合并与部署
# 1. 合并 LoRA 权重到基础模型
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
def merge_lora_weights(base_model_path, lora_path, output_path):
"""合并 LoRA 权重"""
# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
base_model_path,
torch_dtype=torch.bfloat16,
device_map="auto"
)
# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, lora_path)
# 合并权重
merged_model = model.merge_and_unload()
# 保存合并后的模型
merged_model.save_pretrained(output_path)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)
tokenizer.save_pretrained(output_path)
print(f"合并后的模型已保存到: {output_path}")
return merged_model
# 2. vLLM 高性能推理
# pip install vllm
from vllm import LLM, SamplingParams
def vllm_inference(model_path):
"""使用 vLLM 进行高性能推理"""
llm = LLM(
model=model_path,
tensor_parallel_size=1, # GPU 数量
max_model_len=4096,
dtype="bfloat16",
quantization=None, # 或 "awq", "gptq"
)
sampling_params = SamplingParams(
temperature=0.7,
top_p=0.9,
max_tokens=512,
frequency_penalty=0.1
)
# 批量推理
prompts = [
"什么是机器学习?",
"解释一下量子计算的基本原理。"
]
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(f"Prompt: {output.prompt}")
print(f"Response: {output.outputs[0].text}\n")
# 3. GPTQ 量化部署
from auto_gptq import AutoGPTQForCausalLM
def quantize_with_gptq(model_path, output_path):
"""GPTQ 4-bit 量化"""
model = AutoGPTQForCausalLM.from_pretrained(
model_path,
quantize_config={
"bits": 4,
"group_size": 128,
"desc_act": True,
"damp_percent": 0.01
}
)
# 使用校准数据量化
model.quantize(calibration_data)
model.save_quantized(output_path)评估与测试
def evaluate_model(model, tokenizer, eval_dataset, max_samples=100):
"""评估微调后的模型质量
评估维度:
1. 困惑度(Perplexity)— 模型对文本的预测能力
2. 准确率 — 任务特定的正确率
3. 生成质量 — 人工评估或 GPT-4 评估
4. 延迟和吞吐量 — 推理性能指标
"""
import math
model.eval()
total_loss = 0.0
count = 0
for sample in eval_dataset[:max_samples]:
inputs = tokenizer(sample['output'], return_tensors='pt')
inputs = {k: v.to(model.device) for k, v in inputs.items()}
with torch.no_grad():
outputs = model(**inputs, labels=inputs['input_ids'])
total_loss += outputs.loss.item()
count += 1
avg_loss = total_loss / max(count, 1)
perplexity = math.exp(avg_loss)
print(f"评估样本数: {count}")
print(f"平均 Loss: {avg_loss:.4f}")
print(f"困惑度 (PPL): {perplexity:.2f}")
return {'loss': avg_loss, 'perplexity': perplexity}
def generate_comparison_report(
base_model, tuned_model, tokenizer, test_prompts
):
"""生成对比报告 — 基础模型 vs 微调模型"""
results = []
for prompt in test_prompts:
inputs = tokenizer(prompt, return_tensors='pt')
inputs = {k: v.to(base_model.device) for k, v in inputs.items()}
# 基础模型输出
with torch.no_grad():
base_output = base_model.generate(
**inputs, max_new_tokens=256, temperature=0.7, do_sample=True
)
# 微调模型输出
with torch.no_grad():
tuned_output = tuned_model.generate(
**inputs, max_new_tokens=256, temperature=0.7, do_sample=True
)
results.append({
'prompt': prompt,
'base_response': tokenizer.decode(base_output[0], skip_special_tokens=True),
'tuned_response': tokenizer.decode(tuned_output[0], skip_special_tokens=True),
})
print(f"\n--- Prompt: {prompt[:50]}... ---")
print(f"基础模型: {results[-1]['base_response'][:100]}...")
print(f"微调模型: {results[-1]['tuned_response'][:100]}...")
return results
# 微调超参数参考表
HYPERPARAMETERS_GUIDE = {
"学习率": {
"LoRA": "1e-4 ~ 5e-4",
"QLoRA": "1e-4 ~ 2e-4",
"Full FT": "1e-6 ~ 1e-5",
"说明": "LoRA 学习率通常比全量微调高 10-100 倍"
},
"Batch Size": {
"推荐": "4-16(受 GPU 显存限制)",
"梯度累积": "gradient_accumulation_steps=4-8",
"说明": "等效 batch_size = per_device_batch_size * accumulation_steps"
},
"LoRA Rank (r)": {
"简单任务": "r=4-8",
"中等任务": "r=16-32",
"复杂任务": "r=64-128",
"说明": "r 越大可训练参数越多,但过大会退化为全量微调"
},
"训练轮次": {
"SFT": "1-5 epochs",
"继续预训练": "1-3 epochs",
"说明": "过多轮次会导致过拟合(灾难性遗忘)"
},
"数据量": {
"最低": "100-500 条高质量样本",
"推荐": "1K-10K 条",
"说明": "数据质量 > 数据数量"
},
}优点
缺点
总结
LoRA 通过低秩矩阵分解(W' = W + BA)实现参数高效微调,只训练 0.1%-1% 的参数。QLoRA 在 LoRA 基础上使用 NF4 量化和双重量化,将 7B 模型的微调显存需求降低到 ~16GB。训练数据格式支持 Alpaca(单轮)和 ShareGPT(多轮对话)。数据质量过滤包括长度检查、去重和格式验证。推理部署使用 vLLM 实现高吞吐推理,GPTQ/AWQ 量化减小模型体积。
关键知识点
- 先分清模型能力边界、数据边界和工程边界。
- 任何 AI 主题都不只看效果,还要看延迟、成本、可解释性和安全性。
- 评估方式和失败样例往往比“换哪个模型”更重要。
项目落地视角
- 给数据来源、Prompt 模板、Embedding 版本、评估集和实验结果做版本管理。
- 上线前准备兜底策略,例如拒答、回退、人工审核或缓存降级。
- 观察错误类型时,区分数据问题、召回问题、提示词问题和模型问题。
常见误区
- 只关注 Demo 效果,不考虑线上稳定性和可复现性。
- 没有评估集就频繁调参,最后无法解释为什么变好或变差。
- 忽略权限、审计、隐私和模型输出的安全边界。
进阶路线
- 继续补齐训练、推理、评估、MLOps 和治理链路。
- 把主题放回真实业务流程,思考谁提供数据、谁消费结果、谁负责兜底。
- 把 PoC 逐步升级到可观测、可回滚、可演进的生产方案。
适用场景
- 当你准备把《大模型微调技术》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合企业知识问答、内容生成、分类抽取和智能助手等场景。
- 当需求同时关注效果、时延、成本和安全边界时,这类主题最有价值。
落地建议
- 先定义评估集、成功标准和失败样例,再开始调模型或调提示。
- 把数据来源、分块方式、Embedding 版本和 Prompt 模板纳入版本管理。
- 上线前准备兜底策略,例如拒答、回退、人工审核或检索降级。
排错清单
- 先判断问题出在数据、检索、Prompt、模型还是后处理。
- 检查上下文是否过长、分块是否过碎或召回是否偏题。
- 对错误回答做分类,区分幻觉、事实过时、指令误解和格式错误。
常见微调问题与解决
训练 Loss 不下降
def diagnose_training_loss():
"""训练 Loss 不下降的常见原因和排查步骤"""
checks = {
"1. 学习率过大": "尝试降低学习率(1e-5 ~ 5e-5),观察 Loss 是否震荡",
"2. 学习率过小": "Loss 缓慢下降但太慢,尝试提高到 2e-4",
"3. 数据格式错误": "检查模板是否正确,input/output 是否颠倒",
"4. 数据质量差": "检查是否有大量重复、空输出或格式不一致的样本",
"5. LoRA rank 太小": "从 r=8 提高到 r=16 或 r=32",
"6. 目标模块不足": "尝试增加 FFN 层(gate_proj, up_proj, down_proj)",
"7. tokenizer 不匹配": "确认 tokenizer 与基础模型完全一致",
}
for issue, solution in checks.items():
print(f" {issue}: {solution}")
diagnose_training_loss()灾难性遗忘
def prevent_catastrophic_forgetting():
"""防止灾难性遗忘的策略
微调后模型丧失原有能力的常见原因和解决方法。
"""
strategies = {
"降低学习率": "使用更小的学习率(1e-5 ~ 5e-5)",
"减少训练轮次": "1-3 epochs 即可,不要过度训练",
"混合通用数据": "在训练集中混入 10-20% 的通用指令数据",
"正则化": "使用 weight_decay=0.01 防止权重过度变化",
"减小 LoRA rank": "使用较小的 r(4-16),减少参数变化量",
"早停策略": "监控验证集 Loss,在开始上升时停止训练",
}
print("防止灾难性遗忘策略:")
for name, desc in strategies.items():
print(f" {name}: {desc}")
prevent_catastrophic_forgetting()GPU 显存优化
def gpu_memory_optimization():
"""GPU 显存优化技巧
7B 模型在单卡 24GB GPU 上微调的显存分配:
- 模型权重(4-bit): ~4GB
- LoRA 参数: ~50MB
- 优化器状态: ~200MB
- 梯度: ~100MB
- 激活值: ~8-12GB(取决于 batch_size 和 seq_length)
- 总计: ~14-18GB
"""
tips = {
"gradient_checkpointing": "启用梯度检查点,用计算换显存(减少 60-70% 激活值显存)",
"减小 batch_size": "从 4 降到 2,配合梯度累积保持等效 batch_size",
"减小 max_seq_length": "从 2048 降到 1024 或 512",
"使用 QLoRA": "4-bit 量化加载模型,比 16-bit 节省约 60% 显存",
"使用 Flash Attention": "减少注意力计算的显存占用",
"使用 8-bit 优化器": "paged_adamw_8bit 替代标准 AdamW",
}
for tip, desc in tips.items():
print(f" {tip}: {desc}")
gpu_memory_optimization()复盘问题
- 如果把《大模型微调技术》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《大模型微调技术》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《大模型微调技术》最大的收益和代价分别是什么?
