跳转至

01 - 高效微调技术(PEFT)

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

📌 定位说明:本章侧重PEFT方法论的原理与研究进展。 - 📖 微调应用实战请参考 LLM应用/09-大模型微调技术 - 📖 LoRA/QLoRA实战请参考 LLM应用/10-LoRA与QLoRA

学习目标:理解并掌握LoRA等参数高效微调技术,能够在有限显存下微调大模型。


1. 为什么需要高效微调?

1.1 全量微调的问题

假设你要微调一个7B参数的LLaMA模型:

Text Only
模型参数: 7B × 4字节 (FP32) = 28 GB
梯度: 7B × 4字节 = 28 GB
优化器状态 (Adam): 7B × 8字节 = 56 GB
激活值: ~10-20 GB
----------------------------------------
总计: ~120+ GB 显存!

问题: - 需要A100 80GB × 2 或 A100 40GB × 4 - 成本极高 - 训练时间长

1.2 高效微调的核心思想

关键洞察:预训练模型已经学到了丰富的知识,微调时不需要改变所有参数。

Text Only
全量微调: 更新所有参数 (100%)
高效微调: 只更新少量参数 (0.1% - 1%)

效果: 在大多数任务上,高效微调 ≈ 全量微调

2. LoRA:低秩适配

2.1 核心思想

论文: LoRA: Low-Rank Adaptation of Large Language Models

直觉:权重矩阵的更新是低秩的

Text Only
预训练权重: W₀ ∈ ℝ^(d×k)
微调后的权重: W = W₀ + ΔW

LoRA假设: ΔW 是低秩的,即 ΔW = BA
其中: B ∈ ℝ^(d×r), A ∈ ℝ^(r×k), r << min(d, k)

为什么低秩有效?(数学直觉)

核心理论依据来自 Aghajanyan et al. (2020) 的内在维度(Intrinsic Dimensionality)发现:

Text Only
实验发现:
  对于一个 d 维参数空间的预训练模型,存在一个远小于 d 的
  内在维度 d_int,使得在该低维子空间内优化即可达到全量微调
  90% 的性能。

  例如:RoBERTa-Large 有 355M 参数,但微调的内在维度仅约
  几百到几千维,压缩比高达数万倍。

数学直觉:
  1. 权重矩阵的奇异值分解:W = UΣV^T
     预训练后,Σ 中大部分奇异值很小 → 权重矩阵本身就近似低秩

  2. 微调的增量 ΔW 比 W 更加低秩:
     - 微调只是"微调",变化幅度小
     - 变化主要集中在少数关键方向(对应最大奇异值的方向)
     - 理论上 rank(ΔW) << rank(W)

  3. 经验验证(原论文):
     r=4 即可在大多数NLU任务上匹配全量微调
     r=1 在某些简单任务上也足够

  直觉类比:
  想象一个已经训练好的画家(预训练模型),让他学画某种新风格
  (微调)。他不需要重新学所有绘画技巧(全量参数),只需调整
  少数几个风格参数(色调、笔触力度等)—— 这就是"低秩"调整。

2.2 数学原理

标准前向传播

Text Only
h = W₀x

LoRA前向传播

Text Only
h = W₀x + ΔWx = W₀x + BAx

其中:
- W₀: 预训练权重(冻结,不训练)
- B, A: 低秩矩阵(可训练)
- x: 输入

初始化策略

Text Only
- A: Kaiming均匀初始化
- B: 零初始化

这样初始化保证训练开始时 ΔW = BA = 0
模型输出与预训练时相同,训练稳定

2.3 为什么能减少显存?

Text Only
假设: d = 4096, k = 4096, r = 16

全量微调:
  可训练参数: 4096 × 4096 = 16,777,216

LoRA:
  A: 4096 × 16 = 65,536
  B: 4096 × 16 = 65,536
  总计: 131,072

节省: 16,777,216 / 131,072 = 128倍!

2.4 应用到Transformer

LoRA通常应用于以下权重矩阵:

Text Only
Transformer中的注意力权重:
- W_q (Query投影)
- W_k (Key投影)
- W_v (Value投影)
- W_o (输出投影)

以及FFN中的权重(可选)

为什么不应用于所有层? - 注意力层包含了大部分任务相关知识 - 实验表明只适配注意力层已经足够


3. LoRA的实现

3.1 基础实现

Python
import torch
import torch.nn as nn
import torch.nn.functional as F
import math

class LoRALayer(nn.Module):
    """
    LoRA层的基本实现
    """
    def __init__(self, in_features, out_features, rank=16, lora_alpha=1):
        super().__init__()  # super()调用父类方法

        self.rank = rank
        self.lora_alpha = lora_alpha
        self.scaling = lora_alpha / rank

        # 低秩矩阵
        # 注:论文中 ΔW = BA,A ∈ ℝ^(r×k)(降维),B ∈ ℝ^(d×r)(升维)
        # 代码中存储为转置形式以便直接 x @ lora_A @ lora_B 计算
        # lora_A 对应 A^T ∈ ℝ^(k×r),lora_B 对应 B^T ∈ ℝ^(r×d)
        self.lora_A = nn.Parameter(torch.zeros(in_features, rank))   # [k, r]
        self.lora_B = nn.Parameter(torch.zeros(rank, out_features))  # [r, d]

        # 初始化:A随机初始化,B零初始化 → 保证初始ΔW=0
        nn.init.kaiming_uniform_(self.lora_A, a=math.sqrt(5))
        nn.init.zeros_(self.lora_B)

    def forward(self, x):
        """
        x: [batch, seq_len, in_features]
        返回: [batch, seq_len, out_features]
        """
        # 计算 ΔWx = BAx
        # 等价于 x @ A^T @ B^T(使用存储的转置形式)
        return (x @ self.lora_A @ self.lora_B) * self.scaling

class LinearWithLoRA(nn.Module):
    """
    带LoRA的线性层
    """
    def __init__(self, linear_layer, rank=16, lora_alpha=1):
        super().__init__()

        # 原始线性层(冻结)
        self.linear = linear_layer
        self.linear.weight.requires_grad = False
        if self.linear.bias is not None:
            self.linear.bias.requires_grad = False

        # LoRA层
        self.lora = LoRALayer(
            linear_layer.in_features,
            linear_layer.out_features,
            rank=rank,
            lora_alpha=lora_alpha
        )

    def forward(self, x):
        # 原始输出 + LoRA输出
        return self.linear(x) + self.lora(x)

3.2 应用到预训练模型

Python
def inject_lora_to_model(model, target_modules, rank=16, lora_alpha=1):
    """
    将LoRA注入到模型的指定模块

    Args:
        model: 预训练模型
        target_modules: 要替换的模块名列表,如["q_proj", "v_proj"]
        rank: LoRA秩
        lora_alpha: LoRA alpha参数
    """
    for name, module in model.named_modules():
        # 检查是否是目标模块
        if any(target in name for target in target_modules):  # any()任一为True则返回True
            if isinstance(module, nn.Linear):  # isinstance检查类型
                # 获取父模块和属性名
                parent_name = '.'.join(name.split('.')[:-1])
                child_name = name.split('.')[-1]

                if parent_name == '':
                    parent = model
                else:
                    parent = model.get_submodule(parent_name)

                # 替换为带LoRA的线性层
                lora_layer = LinearWithLoRA(module, rank=rank, lora_alpha=lora_alpha)
                setattr(parent, child_name, lora_layer)  # setattr动态设置对象属性
                print(f"注入LoRA到: {name}")

    return model

# 使用示例
from transformers import AutoModelForCausalLM

# 加载预训练模型
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")

# 注入LoRA
model = inject_lora_to_model(
    model,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    rank=16,
    lora_alpha=32
)

# 打印可训练参数
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
total_params = sum(p.numel() for p in model.parameters())
print(f"可训练参数: {trainable_params:,} ({100*trainable_params/total_params:.2f}%)")

3.3 超参数选择

Rank ®

Text Only
r=1:  极简适配,适合简单任务
r=4:  小任务,快速实验
r=8:  中等复杂度任务
r=16: 复杂任务(最常用)
r=32: 非常复杂的任务
r=64: 极少使用

经验法则: 从r=8或16开始,根据效果调整

Alpha (α)

Text Only
alpha控制LoRA的缩放: scaling = alpha / rank

常见设置:
- alpha = 2 * rank
- alpha = rank

alpha越大,LoRA的影响越大

Dropout

Text Only
LoRA也可以使用dropout防止过拟合

lora_dropout: 0.0 - 0.1

4. LoRA的变体

4.1 QLoRA:量化 + LoRA

问题:即使使用LoRA,7B模型加载还是需要28GB显存(FP32)或14GB(FP16)

解决方案:4-bit量化 + LoRA

Text Only
模型加载: 7B × 0.5字节 (4-bit) = 3.5 GB
LoRA参数: 可忽略
激活值: ~2-4 GB
----------------------------------------
总计: ~6-8 GB 显存!

可以在消费级GPU(RTX 3090/4090)上微调7B模型!

关键技术: - 4-bit NormalFloat (NF4):针对正态分布权重的最优4-bit量化 - 双量化:量化量化常数,进一步节省显存 - 分页优化器:使用CPU内存作为显存的页交换

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

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

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

# 准备模型用于训练
model = prepare_model_for_kbit_training(model)

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

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

4.2 DoRA:权重分解低秩适配

问题:LoRA只学习方向变化,忽略了幅度变化

解决方案:将权重分解为幅度和方向,分别适配

权重分解公式

\[W = m \cdot \frac{V}{\|V\|}\]

其中: - \(V = W_0 + \Delta V\):更新后的权重向量 - \(m \in \mathbb{R}^{1 \times k}\):可学习的幅度向量 - \(\|V\|\):列归一化(每列的L2范数)

DoRA的数学分解

\[\Delta W = W - W_0 = \underbrace{m \cdot \frac{W_0 + BA}{\|W_0 + BA\|}}_{\text{DoRA}} - W_0\]

其中: - \(B \in \mathbb{R}^{d \times r}\):LoRA的B矩阵 - \(A \in \mathbb{R}^{r \times k}\):LoRA的A矩阵 - \(r \ll \min(d, k)\):低秩维度

训练过程: 1. 初始化\(m\) 初始化为 \(W_0\) 各列的L2范数,\(B\)\(A\) 随机初始化 2. 前向传播:计算 \(W = m \cdot (W_0 + BA) / \|W_0 + BA\|\) 3. 梯度更新:同时更新 \(m\)\(B\)\(A\)

代码示例

Python
from peft import LoraConfig, get_peft_model
import torch.nn as nn

class DoRALayer(nn.Module):
    """DoRA层的简化实现"""
    def __init__(self, weight, rank=8):
        super().__init__()
        self.weight = nn.Parameter(weight.clone())
        out_features, in_features = weight.shape

        # 幅度向量
        self.magnitude = nn.Parameter(weight.norm(dim=0, keepdim=True))

        # LoRA矩阵
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))
        self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)

    def forward(self, x):
        # 计算更新后的权重
        delta_W = self.lora_B @ self.lora_A
        W_updated = self.weight + delta_W

        # 归一化并应用幅度
        W_normalized = W_updated / W_updated.norm(dim=0, keepdim=True)
        W_dora = self.magnitude * W_normalized

        return x @ W_dora.T

效果:比LoRA更稳定,效果更好,在多个基准测试上提升1-2%

4.3 AdaLoRA:自适应秩分配

问题:不同层需要不同的秩,固定秩不是最优的

解决方案:根据重要性动态分配秩

动态秩分配算法

  1. 重要性评分:对每个奇异值计算重要性分数
\[S_i = |u_i^T \nabla_L(W) v_i|\]

其中: - \(u_i, v_i\):第 \(i\) 个奇异向量 - \(\nabla_L(W)\):损失对权重的梯度 - \(S_i\):第 \(i\) 个奇异值的重要性分数

  1. 奇异值剪枝:根据重要性分数剪枝
\[\text{rank}_t(W) = \sum_{i=1}^{r_{\max}} \mathbb{1}[S_i > \tau_t]\]

其中: - \(\tau_t\):第 \(t\) 步的剪枝阈值 - \(r_{\max}\):初始最大秩

  1. 预算分配:在总参数预算约束下分配秩
\[\min_{\{r_l\}} \sum_{l=1}^{L} \text{rank}_l \cdot d_l \cdot k_l \quad \text{s.t.} \quad \sum_{l=1}^{L} \text{rank}_l \cdot d_l \cdot k_l \leq B\]

算法流程

Text Only
输入: 预训练模型W₀, 目标参数预算B, 初始秩r_max
输出: 适配后的模型

1. 初始化: 为每层设置初始秩r_max
2. For t = 1 to T (训练步数):
   a. 前向传播计算损失L
   b. 计算每层奇异值的重要性分数S
   c. 根据预算B和重要性分数调整每层秩
   d. 剪枝不重要的奇异值
   e. 更新保留的LoRA参数
3. 返回适配后的模型

代码示例

Python
from peft import AdaLoraConfig, get_peft_model

# 配置AdaLoRA
adalora_config = AdaLoraConfig(
    init_r=12,              # 初始秩
    target_r=4,             # 目标秩
    beta1=0.85,             # 奇异值保留比例
    beta2=0.85,             # 奇异值保留比例
    tinit=200,              # 开始剪枝的步数
    tfinal=1000,            # 停止剪枝的步数
    deltaT=10,              # 剪枝间隔
    lora_alpha=32,
    lora_dropout=0.1,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
    task_type="CAUSAL_LM"
)

# 应用AdaLoRA
model = get_peft_model(model, adalora_config)

优势: - 自动为重要层分配更高秩 - 减少不重要层的参数浪费 - 在相同参数预算下效果优于固定秩LoRA


5. 其他高效微调方法

5.1 Prefix Tuning

思想:在输入前添加可训练的前缀向量

Text Only
输入: [x1, x2, x3, ..., xn]
变为: [p1, p2, ..., pk, x1, x2, x3, ..., xn]

其中 p1...pk 是可训练的prefix向量

优点: - 参数量极小(prefix长度 × 维度 × 层数) - 适合生成任务

缺点: - 需要修改模型输入 - 分类任务效果一般

5.2 Prompt Tuning

思想:只在输入层添加可训练的soft prompt

Text Only
输入: [p1, p2, ..., pk, x1, x2, ..., xn]

与Prefix Tuning的区别:
- Prompt Tuning只在输入层
- Prefix Tuning在每一层

优点: - 参数量最小(通常 < 0.01%) - 实现简单

缺点: - 需要较大的prompt长度(50-100) - 小模型上效果差

5.3 Adapter

思想:在Transformer层中插入小型适配器模块

Text Only
Adapter结构:
Input -> Down-project (d->r) -> ReLU -> Up-project (r->d) -> Output
        +----------------- Residual ----------------------+

通常放在:
- 注意力之后
- FFN之后

优点: - 推理时可以移除Adapter,恢复原始模型 - 适合多任务(每个任务一个Adapter)

缺点: - 推理时有额外开销

5.4 方法对比

方法 可训练参数 存储开销 推理开销 适用场景
Full Fine-tuning 100% 100% 充足资源
LoRA 0.1%-1% 可合并 通用
QLoRA 0.1%-1% 极小 可合并 资源受限
Prefix Tuning 0.1% 生成任务
Prompt Tuning <0.01% 极小 大模型分类
Adapter 0.5%-5% 多任务

6. 实践指南

6.1 选择合适的微调方法

Text Only
场景1: 消费级GPU(8-16GB)
  -> QLoRA (4-bit + LoRA)

场景2: 专业级GPU(24-48GB)
  -> LoRA (FP16/BF16)

场景3: 多任务场景
  -> LoRA 或 Adapter

场景4: 极致节省参数
  -> Prompt Tuning (模型>10B)

场景5: 需要推理速度
  -> LoRA (可合并权重)

6.2 训练技巧

学习率

Text Only
LoRA的学习率通常比全量微调大10倍

全量微调: 1e-5 - 1e-4
LoRA: 1e-4 - 1e-3

使用学习率预热(warmup)

批量大小

Text Only
LoRA对batch size不敏感,可以使用梯度累积

batch_size=1 + gradient_accumulation_steps=32
等效于 batch_size=32

训练轮数

Text Only
LoRA通常需要更少的epoch

全量微调: 3-10 epochs
LoRA: 1-3 epochs

早停(Early Stopping)很重要

6.3 评估与调试

检查LoRA是否正确应用

Python
def print_trainable_parameters(model):
    """打印可训练参数"""
    trainable_params = 0
    all_params = 0

    for name, param in model.named_parameters():
        all_params += param.numel()
        if param.requires_grad:
            trainable_params += param.numel()
            print(f"可训练: {name}: {param.numel()}")

    print(f"\n可训练参数: {trainable_params:,} || "
          f"总参数: {all_params:,} || "
          f"比例: {100*trainable_params/all_params:.2f}%")

# 使用
print_trainable_parameters(model)

检查训练是否生效

Python
# 训练前保存LoRA权重
lora_weights_before = {name: param.clone() for name, param in model.named_parameters() if param.requires_grad}

# 训练...

# 训练后比较
lora_weights_after = {name: param.clone() for name, param in model.named_parameters() if param.requires_grad}

for name in lora_weights_before:
    diff = (lora_weights_after[name] - lora_weights_before[name]).abs().mean()
    print(f"{name} 平均变化: {diff:.6f}")

7. 完整实践:使用 PEFT 库微调 LLaMA

📌 从零实现 LoRA(不依赖 peft 库)请参考 06-LoRA从零实现

7.1 使用 HuggingFace PEFT + SFTTrainer 完整流程

Python
"""
QLoRA微调LLaMA的完整可运行代码
需要: pip install transformers peft bitsandbytes trl datasets
硬件: RTX 3090/4090 (24GB VRAM) 即可运行 7B 模型
"""
import torch
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer
from datasets import load_dataset

# ---------- Step 1: 量化加载模型 ----------
model_name = "meta-llama/Llama-3.1-8B"

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
    torch_dtype=torch.bfloat16,
)
model = prepare_model_for_kbit_training(model)

tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# ---------- Step 2: 配置 LoRA ----------
lora_config = LoraConfig(
    r=16,                          # 秩
    lora_alpha=32,                 # alpha = 2 * r 是常见选择
    target_modules=[               # LLaMA 的注意力和 FFN 权重
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 83,886,080 || all params: 8,114,212,864 || trainable%: 1.03%

# ---------- Step 3: 准备数据集 ----------
# 使用 Alpaca 格式的数据集作为示例
dataset = load_dataset("tatsu-lab/alpaca", split="train[:5000]")

def format_instruction(example):
    """将 Alpaca 格式转为对话模板"""
    if example.get("input", ""):
        text = (
            f"### Instruction:\n{example['instruction']}\n\n"
            f"### Input:\n{example['input']}\n\n"
            f"### Response:\n{example['output']}"
        )
    else:
        text = (
            f"### Instruction:\n{example['instruction']}\n\n"
            f"### Response:\n{example['output']}"
        )
    return {"text": text}

dataset = dataset.map(format_instruction)

# ---------- Step 4: 训练配置 ----------
training_args = TrainingArguments(
    output_dir="./lora-llama-output",
    num_train_epochs=1,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,     # 有效batch_size = 4 * 4 = 16
    learning_rate=2e-4,                # LoRA 学习率通常比全量微调高10倍
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    bf16=True,                         # 使用 bfloat16 混合精度
    logging_steps=10,
    save_strategy="steps",
    save_steps=200,
    max_grad_norm=0.3,                 # 梯度裁剪
    optim="paged_adamw_8bit",          # 分页优化器,节省显存
)

# ---------- Step 5: 使用 SFTTrainer 训练 ----------
# 注意:TRL v0.8+ 废弃了 dataset_text_field 参数,
# 需要将数据集预处理为包含 "text" 列或使用 formatting_func
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=training_args,
    processing_class=tokenizer,        # TRL v0.12+: tokenizer 参数更名为 processing_class
    max_seq_length=512,
    packing=True,                      # 将短样本打包,提高GPU利用率
    # 如果数据集列名不是 "text",使用 formatting_func:
    # formatting_func=lambda examples: [ex["text"] for ex in examples],
)

trainer.train()

# ---------- Step 6: 保存 LoRA 权重 ----------
model.save_pretrained("./lora-llama-adapter")
# 只保存 LoRA 权重(~100MB vs 完整模型 ~16GB)

# ---------- Step 7: 加载和推理 ----------
from peft import PeftModel

# 重新加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
)

# 加载 LoRA 权重
model = PeftModel.from_pretrained(base_model, "./lora-llama-adapter")

# 推理
prompt = "### Instruction:\n用Python实现快速排序\n\n### Response:\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

# ---------- Step 8: 合并权重(可选,部署用)----------
merged_model = model.merge_and_unload()  # 将 LoRA 合并回基础模型
merged_model.save_pretrained("./llama-merged")
tokenizer.save_pretrained("./llama-merged")
# 现在 merged 模型可以直接用于推理,无需 peft 库

7.2 多 LoRA 适配器管理

一个基座模型可以加载不同的 LoRA 适配器,适配不同任务:

Python
from peft import PeftModel

# 加载基础模型(一次)
base_model = AutoModelForCausalLM.from_pretrained(model_name, device_map="auto")

# 加载代码助手 LoRA
code_model = PeftModel.from_pretrained(base_model, "./lora-code-assistant")

# 加载翻译器 LoRA(同一个基座)
translation_model = PeftModel.from_pretrained(base_model, "./lora-translator")

# 运行时切换适配器(如果使用同一个 PeftModel)
model = PeftModel.from_pretrained(base_model, "./lora-code-assistant")
model.load_adapter("./lora-translator", adapter_name="translator")

model.set_adapter("default")     # 使用代码助手
model.set_adapter("translator")  # 切换到翻译器

# 禁用所有 LoRA(回到原始模型)
with model.disable_adapter():
    outputs = model.generate(...)  # 使用原始基础模型

7.3 LoRA 效果验证清单

Python
"""微调效果诊断脚本"""

def diagnose_lora_training(model, training_log):
    """训练后诊断清单"""

    # 1. 检查可训练参数比例
    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    total = sum(p.numel() for p in model.parameters())
    ratio = trainable / total * 100
    print(f"[1] 可训练参数: {trainable:,} / {total:,} ({ratio:.2f}%)")
    assert ratio < 5, "警告:可训练参数过多,检查是否正确冻结了基础模型"  # assert断言:条件False时抛出AssertionError

    # 2. 检查 loss 是否下降
    losses = [log["loss"] for log in training_log if "loss" in log]
    if len(losses) > 1:
        improvement = (losses[0] - losses[-1]) / losses[0] * 100  # [-1]负索引取最后一个元素
        print(f"[2] Loss: {losses[0]:.4f}{losses[-1]:.4f} (下降 {improvement:.1f}%)")

    # 3. 检查 LoRA 权重是否有变化
    for name, param in model.named_parameters():
        if "lora_B" in name and param.requires_grad:
            if param.abs().max() < 1e-6:
                print(f"[3] 警告:{name} 仍为零,LoRA可能未生效")
                break
    else:
        print("[3] LoRA B矩阵已更新 ✓")

    # 4. 检查梯度范数
    grad_norms = []
    for name, param in model.named_parameters():
        if param.requires_grad and param.grad is not None:
            grad_norms.append(param.grad.norm().item())
    if grad_norms:
        print(f"[4] 梯度范数: min={min(grad_norms):.6f}, max={max(grad_norms):.6f}")

8. 下一步

完成本节后,你应该: - [x] 理解LoRA的数学原理(低秩分解 \(\Delta W = BA\)) - [x] 能够使用 PEFT 库进行 QLoRA 微调 - [x] 了解 DoRA/AdaLoRA 等变体的改进思路 - [x] 掌握 Prefix Tuning / Prompt Tuning / Adapter 等替代方案 - [x] 能够在消费级GPU上微调7B+模型

深入实践: - 06-LoRA从零实现 — 不依赖 peft 库,从零手写完整 LoRA - LLM应用/09-大模型微调技术 — 微调的完整项目实战 - LLM应用/10-LoRA与QLoRA — 应用层面的 LoRA 使用指南

下一步02-推理优化技术 - 学习KV Cache、量化等推理加速技术


最后更新日期:2025-07-11 适用版本:LLM学习教程 v2025