大模型微调实战学习笔记

一、 学习背景与目标

1.1 为什么需要微调?

虽然现在的大语言模型(如 Qwen, Llama 3, ChatGPT)能力很强,但直接使用预训练模型(Base Model)往往存在以下问题:

  • 指令遵循能力弱: 不理解“请翻译”、“请总结”等指令。
  • 缺乏领域知识: 对医疗、法律、金融等垂直领域的专业术语或格式不了解。
  • 输出格式不可控: 难以强制输出 JSON、特定表格等格式。

微调(SFT, Supervised Fine-Tuning) 的目的就是在预训练模型的基础上,通过特定的指令数据集,进一步调整模型参数,使其学会“听懂指令”并“按需输出”。

1.2 微调的层级

  • Full Fine-tuning(全量微调): 更新模型所有参数。效果最好,但显存需求巨大,成本极高。
  • PEFT(Parameter-Efficient Fine-Tuning,参数高效微调): 只更新极少部分参数。本笔记核心。

二、 核心技术:PEFT 与 LoRA

2.1 PEFT (Parameter-Efficient Fine-Tuning)

PEFT 是一种旨在解决全量微调算力负担过重的技术框架。它通过冻结大部分模型参数,只训练极少的额外参数(通常少于原模型的 1%),就能达到接近全量微调的效果。

2.2 LoRA (Low-Rank Adaptation)

LoRA 是目前最流行的 PEFT 方法。

  • 核心原理:
    假设预训练的某个权重矩阵为 WWW(维度 d×kd \times kd×k)。微调时的增量更新 ΔW\Delta WΔW 通常是一个低秩矩阵。
    LoRA 将 ΔW\Delta WΔW 分解为两个极小的矩阵 AAABBB 的乘积:ΔW=B×A\Delta W = B \times AΔW=B×A
    • BBB 的维度是 d×rd \times rd×r
    • AAA 的维度是 r×kr \times kr×k
    • rrr 是秩,通常设得很小(如 8, 16, 32),远小于 dddkkk
  • 优势:
    1. 显存占用极低: 只需要存储 AAABBB 的参数。
    2. 训练速度快: 梯度计算量大幅减少。
    3. 无推理延迟: 在推理阶段,可以将 B×AB \times AB×A 的结果合并回原矩阵 WWW,不改变模型结构。

2.3 QLoRA

为了进一步节省显存,QLoRA 在 LoRA 的基础上,将基础大模型量化为 4-bit 整数进行存储和计算,仅在训练特定层时反量化回 BF16。这使得我们在单张消费级显卡(如 RTX 3090/4090)上即可微调 7B 甚至更大的模型。


三、 实战流程详解

本实战基于 Hugging Face 生态系统,主要使用 transformers, peft, trl 库。

3.1 环境准备

pip install transformers datasets peft trl bitsandbytes accelerate torch

3.2 数据集准备

微调数据通常采用 JSON/JSONL 格式,主流的指令微调格式如下(Alpaca 格式变体):

{
  "instruction": "解释一下量子纠缠。",
  "input": "",
  "output": "量子纠缠是..."
}

或者更通用的对话格式:

{
  "system": "你是一个智能助手。",
  "user": "你好",
  "assistant": "你好!有什么我可以帮你的吗?"
}

处理步骤:

  1. 加载数据集(使用 datasets 库)。
  2. 进行 Tokenization(分词)。
  3. 拼接与截断:将 Instruction 和 Input 拼接,确保不超过模型最大长度(如 2048)。
  4. 构造 Labels:通常需要将输入部分的 Label 设为 -100(PyTorch 忽略计算 loss),只计算输出部分的 Loss。

3.3 加载模型与 LoRA 配置

这是最关键的一步,我们需要以 4-bit 加载基础模型,并挂载 LoRA 适配器。

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 1. 配置 4-bit 量化 (QLoRA)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=False,
)

# 2. 加载基础模型 (如 Qwen1.5-7B-Chat)
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen1.5-7B-Chat",
    quantization_config=bnb_config,
    device_map="auto"
)

# 3. 预处理模型以适应 k-bit 训练
model = prepare_model_for_kbit_training(model)

# 4. 配置 LoRA 参数
peft_config = LoraConfig(
    r=16,                 # LoRA 秩,越大参数越多,效果可能越好但越慢
    lora_alpha=32,        # LoRA 缩放因子,通常设为 2*r
    lora_dropout=0.05,    # Dropout 比例
    bias="none",         # bias 是否训练
    task_type="CAUSAL_LM", # 任务类型:因果语言模型
    target_modules=[      # 需要注入 LoRA 的层名 (针对不同模型需调整)
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj"
    ]
)

# 5. 获取 PEFT 模型
model = get_peft_model(model, peft_config)
model.print_trainable_parameters() # 查看可训练参数占比

3.4 训练 (使用 SFTTrainer)

trl 库提供了 SFTTrainer,封装了数据预处理、LoRA 应用和训练循环,大大简化了代码。

from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,      # 之前处理好的数据集
    dataset_text_field="text",  # 数据集中包含 prompt+response 的字段
    max_seq_length=2048,        # 最大序列长度
    tokenizer=tokenizer,
    args=TrainingArguments(
        output_dir="./results",
        num_train_epochs=3,     # 训练轮数
        per_device_train_batch_size=4, # 批次大小 (显存不够则减小)
        gradient_accumulation_steps=1, # 梯度累积
        optim="paged_adamw_32bit",
        save_steps=50,
        logging_steps=10,
        learning_rate=2e-4,     # 学习率 (LoRA 通常推荐 2e-4 到 5e-5)
        weight_decay=0.001,
        fp16=False,             # 如果用 bf16 则设为 False
        bf16=True,              # 推荐 bf16
        gradient_checkpointing=True, # 激活梯度检查点以省显存
    ),
    packing=False,              # 是否打包多个短样本到一个序列
)

trainer.train()

3.5 保存与加载模型

训练完成后,我们只保存 LoRA 的适配器权重(几十 MB),不需要重新保存整个大模型。

# 保存 LoRA 适配器
model.save_pretrained("./lora_adaptor")
tokenizer.save_pretrained("./lora_adaptor")

# 加载模型进行推理
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen1.5-7B-Chat",
    torch_dtype=torch.float16,
    device_map="auto"
)

# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, "./lora_adaptor")

# 此时 model 已经融合了 LoRA 权重,可以直接用于 generate

四、 关键参数与避坑指南

4.1 关键参数解释

  • rank ®: 决定了 LoRA 矩阵的大小。太小(如 4)可能信息量不足,太大(如 128)则显存占用增加,可能导致过拟合。常用值:8, 16, 32, 64。
  • Alpha: 缩放系数 ΔW×αr\Delta W \times \frac{\alpha}{r}ΔW×rα。通常 Alpha = 2 * r。
  • Max Seq Length: 不要设得太大,因为上下文长度是显存杀手。如果不需要处理长文本,512 或 1024 足矣。
  • Learning Rate: LoRA 微调对学习率比较敏感。一般全量微调用 1e-5,LoRA 可以用 2e-4 或 1e-4。

4.2 常见问题

  1. 显存溢出 (OOM):
    • 减小 per_device_train_batch_size(如从 4 减到 1)。
    • 增加 gradient_accumulation_steps(如从 1 增到 4),保持等效 Batch Size 不变。
    • 开启 gradient_checkpointing=True
    • 确保使用了 4-bit 量化加载模型。
  2. Loss 不下降:
    • 检查数据格式是否正确(Prompt 结尾是否有换行符?)。
    • 学习率是否过大或过小。
    • 检查 target_modules 是否写对(例如 Qwen 模型通常使用 q_proj, v_proj 等,不同架构名称不同)。
  3. 模型输出重复/循环:
    • 这是常见现象,通常是因为训练轮数过多导致过拟合,或者是温度参数设置问题。在推理时适当调整 temperaturetop_p

五、 总结

通过本次实战学习,我们掌握了如何利用 PEFT (LoRA)QLoRA 技术在有限的算力资源下对开源大模型(如 Qwen)进行微调。

核心流程回顾:

  1. 数据构建: 构造高质量的指令问答对。
  2. 模型量化: 使用 BitsAndBytesConfig 进行 4-bit 加载。
  3. 配置 LoRA: 只训练低秩适配器矩阵。
  4. SFT 训练: 使用 trl 库快速启动训练。
  5. 模型合并: 加载适配器进行推理。

微调不仅让模型学会了特定任务,更重要的是让模型的行为更符合人类的预期,是构建垂直领域 AI 应用的基石技能。

更多推荐