Skip to content

认识模型微调

什么是微调?

微调(Fine-tuning) 是在预训练模型的基础上,用特定数据继续训练,让模型适应你的任务。

预训练模型:看过大量通用文本,什么都知道一点

     ↓ 微调(用你的数据继续训练)
   
微调后模型:更擅长的特定任务

RAG 和 微调

维度RAG微调
改什么改输入(加资料)改模型(调参数)
知识更新实时,换文档就行慢,要重新训练
幻觉控制好(有资料约束)一般(靠记忆)
成本按 Token 付费一次性训练成本
适用场景知识问答、客服格式遵循、风格迁移
需要数据知识库文档问答对/指令数据

什么时候应该用哪个?

一个知识库,用户问的是"公司政策是什么?"
→ RAG (知识经常变,需要查具体文档)

需要模型输出特定格式(JSON、Markdown、特定风格)
→ 微调 (不需要外部知识,需要行为对齐)

既要知识又要格式?
→ RAG + 微调 (最佳组合,RAG 负责知识,微调负责行为)

微调的三种层次

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

更新所有参数
需要 8× 80GB GPU(甚至更多)
效果好,但贵得离谱
普通人基本不用

2. 高效微调(PEFT)

只更新一小部分参数(~1%)
LoRA / QLoRA / Adapter
一张 24GB GPU 就能跑
效果接近全参微调

3. 领域微调

在特定领域数据上继续预训练
比如:法律、医疗、代码
需要大量领域数据
适合做垂直领域模型

主流高效微调方法

方法原理显存需求效果
LoRA低秩矩阵注入24GB⭐⭐⭐⭐
QLoRALoRA + 4bit 量化8-12GB⭐⭐⭐
Prompt Tuning只调 Prompt 模板8GB⭐⭐
Prefix Tuning加前缀向量8GB⭐⭐

先用 QLoRA(门槛最低),再试 LoRA(效果更好)

微调的典型流程

① 准备数据
   └─ 收集/生成 问答对 → 格式化 → 检查质量

② 选择基座模型
   └─ Qwen / LLaMA / DeepSeek / ChatGLM

③ 配置训练参数
   └─ LoRA rank / learning rate / batch size

④ 开始训练
   └─ 观察 loss → 选择最佳 checkpoint

⑤ 评估效果
   └─ 在测试集上对比微调前后的表现

⑥ 部署
   └─ 合并 LoRA 权重 → 量化 → 部署推理

LoRA 与 QLoRA

LoRA 原理

冻结原始权重,在旁边加一条"小回路"训练,训练完合并回去。

原始模型权重(冻结,不更新)     LoRA 旁路(可训练,占 1%)
┌─────────────────┐          ┌──────────────┐
│  W (1000×1000)  │    +     │  A×B (低秩)   │
│   冻结,不动     │          │   只训这个    │
└─────────────────┘          └──────────────┘

环境准备

bash
pip install torch transformers datasets accelerate
pip install peft           # LoRA 核心库
pip install bitsandbytes   # 4bit 量化(QLoRA 需要)
pip install trl            # SFTTrainer(训练器)

完整微调代码

用 QLoRA 微调 Qwen2.5-7B

python
# finetune_qwen.py
import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer


# ===== 1. 量化配置(QLoRA 核心) =====
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                     # 4bit 量化
    bnb_4bit_quant_type="nf4",              # 量化方式
    bnb_4bit_compute_dtype=torch.float16,   # 计算精度
    bnb_4bit_use_double_quant=True,         # 双重量化(省更多显存)
)

# ===== 2. 加载模型和分词器 =====
model_name = "Qwen/Qwen2.5-7B-Instruct"

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,  # 4bit 加载
    device_map="auto",               # 自动分配 GPU
    trust_remote_code=True,
)

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
tokenizer.pad_token = tokenizer.eos_token

# ===== 3. LoRA 配置 =====
lora_config = LoraConfig(
    r=16,                       # LoRA 秩(越大越强,越费显存)
    lora_alpha=32,              # 缩放系数
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],  # 作用的目标模块
    lora_dropout=0.05,          # Dropout 防过拟合
    bias="none",
    task_type="CAUSAL_LM",      # 因果语言模型
)

model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: ~8M / 7B total = 0.1% ← 只训练 0.1% 的参数!

# ===== 4. 准备训练数据 =====
# 数据格式:每条是 {"instruction": "...", "output": "..."}
dataset = load_dataset("json", data_files="train_data.json")

def format_chat(example):
    """将数据格式化为对话"""
    return {
        "text": f"<|im_start|>user\n{example['instruction']}\n<|im_end|>\n"
                f"<|im_start|>assistant\n{example['output']}\n<|im_end|>"
    }

dataset = dataset.map(format_chat)

# ===== 5. 训练参数 =====
training_args = TrainingArguments(
    output_dir="./qwen-lora",       # 输出目录
    per_device_train_batch_size=4,  # 批次大小(根据显存调)
    gradient_accumulation_steps=4,  # 梯度累积(等效 batch_size = 16)
    learning_rate=2e-4,             # 学习率(LoRA 通常比全参大)
    num_train_epochs=3,             # 训练轮数
    logging_steps=10,               # 每隔几步打日志
    save_steps=100,                 # 每隔几步保存
    save_total_limit=2,             # 最多保留 2 个检查点
    fp16=True,                      # 半精度训练
    optim="paged_adamw_8bit",       # 8bit 优化器(省显存)
    lr_scheduler_type="cosine",     # 学习率调度
    warmup_ratio=0.03,              # 预热比例
    report_to="none",               # 不报告到外部
)

# ===== 6. 训练 =====
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    args=training_args,
    train_dataset=dataset["train"],
    max_seq_length=1024,            # 最大序列长度
    dataset_text_field="text",
)

trainer.train()

# ===== 7. 保存 LoRA 权重 =====
model.save_pretrained("./qwen-lora-final")
tokenizer.save_pretrained("./qwen-lora-final")
print("微调完成!权重已保存")

推理时加载 LoRA 权重

python
# inference.py
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

# 1. 加载基座模型
base_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct",
    torch_dtype=torch.float16,
    device_map="auto",
)

# 2. 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, "./qwen-lora-final")

# 3. 合并权重(可选,合并后推理更快)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./qwen-lora-merged")

# 4. 推理
tokenizer = AutoTokenizer.from_pretrained("./qwen-lora-final")

prompt = "<|im_start|>user\n什么是 RAG?\n<|im_end|>\n<|im_start|>assistant\n"
inputs = tokenizer(prompt, return_tensors="pt")
outputs = merged_model.generate(**inputs, max_new_tokens=256)
print(tokenizer.decode(outputs[0]))

LoRA 参数调优

参数推荐值大了会怎样小了会怎样
r(秩)8-32更准但费显存欠拟合
lora_alpha16-64权重影响大影响小
learning_rate1e-4 ~ 5e-4不稳定收敛慢
num_epochs2-5过拟合欠拟合
batch_size尽量大稳定震荡

显存占用参考

方法模型大小显存需求典型 GPU
QLoRA (4bit)7B8-12GBRTX 3060/4060
QLoRA (4bit)14B16-20GBRTX 3090
LoRA (8bit)7B16-20GBRTX 3090
LoRA (16bit)7B28-32GBRTX 4090
全参微调7B56+GBA100 80G

LoRA ≈ 花 1% 的成本,拿到 90% 全参微调的效果。QLoRA 连显卡门槛都给降到消费级了。

微调数据集构建

数据格式

对话格式(最常用)

json
[
    {
        "instruction": "什么是 RAG?",
        "output": "RAG(检索增强生成)是一种...",
        "system": "你是一个 AI 知识库助手"  // 可选
    },
    {
        "instruction": "RAG 和微调有什么区别?",
        "output": "RAG 是外挂知识,微调是内化能力..."
    }
]

ShareGPT 格式

json
{
    "conversations": [
        {"from": "system", "value": "你是一个助手"},
        {"from": "human", "value": "什么是 RAG?"},
        {"from": "gpt", "value": "RAG 是..."}
    ]
}

从知识库生成数据

有了知识库,可以直接从中生成训练数据:

python
from openai import OpenAI

client = OpenAI()


def generate_training_data(
    documents: list[str],
    pairs_per_doc: int = 5,
    model: str = "gpt-4o",
) -> list[dict]:
    """
    从文档生成训练数据
  
    用强模型(GPT-4o)从文档中提取知识点
    生成问答对,用来微调小模型
    """
    dataset = []
  
    for doc in documents:
        prompt = f"""
        根据以下文档,生成 {pairs_per_doc} 个高质量的问答对。
        要求:
        - 问题和答案都基于文档内容
        - 问题多样化(是什么、为什么、怎么做、对比)
        - 答案清晰完整
      
        文档:
        {doc[:2000]}
      
        回复 JSON 格式(列表):
        [{{"instruction": "问题", "output": "答案"}}]
        """
      
        response = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            response_format={"type": "json_object"},
        )
      
        pairs = json.loads(response.content)
        dataset.extend(pairs)
  
    return dataset


# 使用
documents = ["RAG 是检索增强生成...", "向量数据库用于..."]
dataset = generate_training_data(documents)

数据质量控制

python
class DatasetValidator:
    """训练数据集质量检查"""
  
    def __init__(self, dataset: list[dict]):
        self.dataset = dataset
  
    def check_basic(self) -> list[str]:
        """基础检查"""
        issues = []
      
        for i, item in enumerate(self.dataset):
            # 空内容
            if not item.get("instruction", "").strip():
                issues.append(f"#{i}: instruction 为空")
            if not item.get("output", "").strip():
                issues.append(f"#{i}: output 为空")
          
            # 过短
            if len(item.get("output", "")) < 10:
                issues.append(f"#{i}: output 太短 ({len(item['output'])}字符)")
          
            # 包含占位符
            if "{{" in item.get("output", ""):
                issues.append(f"#{i}: 包含未替换的占位符")
      
        return issues
  
    def check_duplicates(self) -> list[str]:
        """查重"""
        from collections import Counter
        questions = [d["instruction"] for d in self.dataset]
        return [q for q, c in Counter(questions).items() if c > 1]
  
    def check_length_distribution(self) -> dict:
        """长度分布"""
        lengths = [len(d["output"]) for d in self.dataset]
        return {
            "total": len(lengths),
            "min": min(lengths),
            "max": max(lengths),
            "avg": sum(lengths) / len(lengths),
            "short (<20)": sum(1 for l in lengths if l < 20),
            "long (>500)": sum(1 for l in lengths if l > 500),
        }
  
    def report(self) -> dict:
        """完整报告"""
        return {
            "total": len(self.dataset),
            "issues": self.check_basic(),
            "duplicates": self.check_duplicates(),
            "lengths": self.check_length_distribution(),
            "pass": len(self.check_basic()) == 0 and len(self.check_duplicates()) == 0,
        }

数据量参考

任务类型最少数据推荐数据说明
格式遵循100 条500+让模型学会输出特定格式
指令遵循500 条2000+让模型听话
领域知识1000 条5000+让模型学会新知识
对话能力2000 条10000+让模型学会聊天

质量远比数量重要。100 条精心标注的数据 > 1000 条自动生成的垃圾数据。

数据增强技巧

python
def augment_dataset(dataset: list[dict]) -> list[dict]:
    """数据增强"""
    augmented = []
  
    for item in dataset:
        # 原样保留
        augmented.append(item)
      
        # 1. 改写问题(同义替换)
        augmented.append({
            "instruction": rewrite_question(item["instruction"]),
            "output": item["output"],
        })
      
        # 2. 反过来问
        if "是什么" in item["instruction"]:
            augmented.append({
                "instruction": item["instruction"].replace("是什么", "有哪些"),
                "output": item["output"],
            })
  
    return augmented

训练数据 = 模型的"参考答案"。答案质量直接决定微调效果。宁缺毋滥。

微调评估与部署

微调后怎么评估?

用更加强大的llm生成问答,来评测微调效果:

python
def compare_before_after(
    base_model_fn,    # 微调前的模型
    ft_model_fn,      # 微调后的模型
    test_questions: list[str],
) -> dict:
    """对比微调前后效果"""
    results = {"before": [], "after": []}
  
    for q in test_questions:
        base_ans = base_model_fn(q)
        ft_ans = ft_model_fn(q)
      
        results["before"].append({
            "question": q,
            "answer": base_ans,
            "length": len(base_ans),
        })
        results["after"].append({
            "question": q,
            "answer": ft_ans,
            "length": len(ft_ans),
        })
  
    return results

合并 LoRA 权重

训练后有两种使用方式:

python
# 方式一:不合并(灵活)
model = PeftModel.from_pretrained(base_model, lora_path)
# 可以随时卸载 LoRA 回到基座模型

# 方式二:合并(效率高)
merged = model.merge_and_unload()
merged.save_pretrained("./merged_model")
# 推理速度和不带 LoRA 一样快

部署到 Ollama(配合本地部署)

bash
# 1. 将合并后的模型转为 GGUF
# 使用 llama.cpp 的 convert.py

# 2. 创建 Modelfile
echo "FROM ./qwen-lora-merged.gguf" > Modelfile
echo 'TEMPLATE """<|im_start|>user\n{{.Prompt}}\n<|im_end|>\n<|im_start|>assistant\n"""' >> Modelfile

# 3. 导入 Ollama
ollama create my-finetuned-model -f Modelfile

# 4. 运行
ollama run my-finetuned-model

常见问题

问题原因解决
Loss 不降学习率太小增大 lr 到 2e-4
Loss 震荡学习率太大/batch 太小减小 lr 或增大 batch
过拟合(训练集好测试集差)数据太少/epoch 太多减少 epoch 或增大 dropout
灾难性遗忘(忘了原来的能力)学习率太大/训练太久用更低 lr 或混合通用数据
输出乱码分词器/模板不对检查 chat template

最后更新:

Built with VitePress