第六章:微调 — 02 LoRA 低秩适配微调#

LoRA(Low-Rank Adaptation) 只训练少量参数(< 1%),大幅降低微调成本。

传统全参数微调需要更新模型所有参数(如 7B 模型 = 70亿个参数),对显存和计算的要求极高。LoRA 的核心思想是:预训练模型的权重矩阵在任务适配时,其变化量是低秩的

本章目标:

  • 理解 LoRA 的数学原理

  • 计算实际参数节省量

  • 准备微调数据集格式

  • 编写完整训练代码框架

  • 了解 QLoRA(量化 + LoRA)进一步节省显存

# 依赖说明
# 本章完整运行需要安装:
#   pip install transformers datasets peft torch trl bitsandbytes
# 部分章节(原理、计算)无需 GPU 即可运行

import json
import os

# 尝试导入深度学习库,给出友好提示
try:
    import torch
    TORCH_AVAILABLE = True
    print(f"PyTorch 版本: {torch.__version__}")
    print(f"CUDA 可用: {torch.cuda.is_available()}")
    if torch.cuda.is_available():
        print(f"GPU: {torch.cuda.get_device_name(0)}")
        print(f"显存: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
except ImportError:
    TORCH_AVAILABLE = False
    print("PyTorch 未安装。Section 1-3 仍可运行,Section 4-6 需要 PyTorch。")
    print("安装命令:pip install torch transformers peft trl bitsandbytes datasets")

try:
    from peft import LoraConfig, get_peft_model, TaskType
    from peft import PeftModel
    PEFT_AVAILABLE = True
    print("PEFT 已安装")
except ImportError:
    PEFT_AVAILABLE = False
    print("PEFT 未安装,LoRA 训练代码需要:pip install peft")

try:
    from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, BitsAndBytesConfig
    TRANSFORMERS_AVAILABLE = True
    print("Transformers 已安装")
except ImportError:
    TRANSFORMERS_AVAILABLE = False
    print("Transformers 未安装:pip install transformers")
PyTorch 未安装。Section 1-3 仍可运行,Section 4-6 需要 PyTorch。
安装命令:pip install torch transformers peft trl bitsandbytes datasets
PEFT 未安装,LoRA 训练代码需要:pip install peft
Transformers 未安装:pip install transformers

Section 1:LoRA 原理图解#

核心思想#

原始模型权重 W₀(冻结,不更新)+ 低秩增量 ΔW = B × A(可训练)

传统微调:
┌─────────────────────────────────┐
│  W  (d × k,全部更新)           │   参数量 = d × k
└─────────────────────────────────┘

LoRA 微调:
┌─────────────────────────────────┐
│  W₀ (d × k,冻结,不更新)      │
└─────────────────────────────────┘
              +
┌──────┐   ┌──────────────────────┐
│  B   │ × │         A            │
│(d×r) │   │       (r×k)          │
└──────┘   └──────────────────────┘
  可训练       可训练
  参数量 = r×d + r×k = r×(d+k)
  其中 r << min(d, k)

推理时的前向传播#

输入 x
  │
  ├──→ W₀ × x  ──────────────────┐
  │                               │
  └──→ A × x → B × (Ax) × (α/r) ─┤
                                   ↓
                              输出 h = W₀x + BAx·(α/r)

关键超参数#

参数

含义

典型值

r (rank)

低秩矩阵的秩,越大参数越多效果越好

4, 8, 16, 32, 64

lora_alpha

缩放因子,影响学习率的有效值

通常为 r 或 2r

lora_dropout

防过拟合的 dropout

0.0 - 0.1

target_modules

对哪些层应用 LoRA

q_proj, v_proj, …

参数节省公式#

$$\text{原始参数} = d \times k$$ $$\text{LoRA 参数} = r \times (d + k)$$ $$\text{节省率} = 1 - \frac{r \times (d + k)}{d \times k}$$

# Section 2:参数量计算(纯 Python,无需 GPU)

def calc_lora_params(model_total_params, target_layers_config, rank, alpha):
    """
    计算 LoRA 微调的参数量
    
    Args:
        model_total_params: 模型总参数量
        target_layers_config: list of (layer_name, d, k) 元组
        rank: LoRA rank r
        alpha: lora_alpha
    
    Returns:
        dict with detailed breakdown
    """
    lora_params = 0
    layer_breakdown = []
    
    for layer_name, d, k in target_layers_config:
        original_params = d * k
        lora_layer_params = rank * (d + k)
        saving_ratio = 1 - lora_layer_params / original_params
        
        lora_params += lora_layer_params
        layer_breakdown.append({
            "layer": layer_name,
            "shape": f"{d}×{k}",
            "original_params": original_params,
            "lora_params": lora_layer_params,
            "saving": f"{saving_ratio:.1%}"
        })
    
    trainable_ratio = lora_params / model_total_params
    effective_alpha = alpha / rank  # 缩放因子
    
    return {
        "model_total_params": model_total_params,
        "lora_trainable_params": lora_params,
        "trainable_ratio": trainable_ratio,
        "rank": rank,
        "alpha": alpha,
        "effective_scaling": effective_alpha,
        "layer_breakdown": layer_breakdown
    }


# Llama 3 7B 模型示例
# 隐藏维度 d_model = 4096
# 注意力头:32 个头,每个头 128 维
# Transformer 层数:32 层

D_MODEL = 4096
NUM_LAYERS = 32
NUM_HEADS = 32
HEAD_DIM = D_MODEL // NUM_HEADS  # 128

# 典型的 LoRA 目标层(每个 Transformer 层)
target_layers = []
for layer_idx in range(NUM_LAYERS):
    # Q, K, V, O 投影矩阵
    target_layers.extend([
        (f"layer{layer_idx}.self_attn.q_proj", D_MODEL, D_MODEL),
        (f"layer{layer_idx}.self_attn.v_proj", D_MODEL, D_MODEL),
    ])

LLAMA_7B_TOTAL_PARAMS = 7_000_000_000

for rank in [8, 16, 64]:
    result = calc_lora_params(
        model_total_params=LLAMA_7B_TOTAL_PARAMS,
        target_layers_config=target_layers,
        rank=rank,
        alpha=rank * 2
    )
    
    print(f"\nRank = {rank}:")
    print(f"  模型总参数:   {result['model_total_params'] / 1e9:.1f}B")
    print(f"  LoRA 可训练:  {result['lora_trainable_params'] / 1e6:.1f}M ({result['trainable_ratio']:.3%})")
    print(f"  缩放系数:     {result['effective_scaling']:.1f}")
    
    # 显存估算
    # 全参数微调:模型(16GB fp16) + 梯度(16GB) + 优化器状态(32GB) ≈ 64GB
    # LoRA:模型(14GB 量化) + LoRA参数梯度(很小) + 优化器(很小)
    lora_params_gb = result['lora_trainable_params'] * 4 / 1e9  # fp32
    optimizer_gb = lora_params_gb * 2  # Adam 需要 2x
    base_model_gb = 14  # 4-bit 量化后
    total_vram = base_model_gb + lora_params_gb + optimizer_gb
    print(f"  预估显存:     {total_vram:.1f} GB (vs 全参微调 ~64GB)")
Rank = 8:
  模型总参数:   7.0B
  LoRA 可训练:  4.2M (0.060%)
  缩放系数:     2.0
  预估显存:     14.1 GB (vs 全参微调 ~64GB)

Rank = 16:
  模型总参数:   7.0B
  LoRA 可训练:  8.4M (0.120%)
  缩放系数:     2.0
  预估显存:     14.1 GB (vs 全参微调 ~64GB)

Rank = 64:
  模型总参数:   7.0B
  LoRA 可训练:  33.6M (0.479%)
  缩放系数:     2.0
  预估显存:     14.4 GB (vs 全参微调 ~64GB)
# 详细展示 rank=16 的层级分解
result = calc_lora_params(
    model_total_params=LLAMA_7B_TOTAL_PARAMS,
    target_layers_config=target_layers[:4],  # 只显示前2层
    rank=16,
    alpha=32
)

print("前 2 层的 LoRA 参数分解(rank=16):")
print(f"{'层名':<40} {'形状':>12} {'原始参数':>12} {'LoRA参数':>10} {'节省'}")
print("-" * 85)
for layer in result["layer_breakdown"]:
    print(f"{layer['layer']:<40} {layer['shape']:>12} {layer['original_params']:>12,} {layer['lora_params']:>10,} {layer['saving']}")
前 2 层的 LoRA 参数分解(rank=16):
层名                                                 形状         原始参数     LoRA参数 节省
-------------------------------------------------------------------------------------
layer0.self_attn.q_proj                     4096×4096   16,777,216    131,072 99.2%
layer0.self_attn.v_proj                     4096×4096   16,777,216    131,072 99.2%
layer1.self_attn.q_proj                     4096×4096   16,777,216    131,072 99.2%
layer1.self_attn.v_proj                     4096×4096   16,777,216    131,072 99.2%

Section 3:数据集准备#

微调数据通常使用 Alpaca 格式(instruction + input + output)或 ChatML 格式

JSONL 格式(Alpaca Style)#

{"instruction": "任务描述", "input": "输入内容(可选)", "output": "期望输出"}

ChatML 格式(对话格式)#

{"messages": [{"role": "system", ...}, {"role": "user", ...}, {"role": "assistant", ...}]}
import json

# 为客服场景生成 5 个合成训练样本

alpaca_examples = [
    {
        "instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
        "input": '我昨天下单了,为什么还没发货?',
        "output": "您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快!"
    },
    {
        "instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
        "input": '收到的商品有破损,怎么办?',
        "output": "非常抱歉给您带来不便!请您按以下步骤操作:\n1. 拍摄破损商品照片(包括包装和商品本身)\n2. 在订单页点击'申请售后'\n3. 上传照片,选择'商品破损'\n\n我们会在 24 小时内审核并为您安排补发或退款。如需紧急处理,可直接联系在线客服。"
    },
    {
        "instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
        "input": '可以修改收货地址吗?订单已经提交了。',
        "output": "可以修改!但需要在发货前操作:\n- **未发货**:进入订单详情→'修改地址'即可\n- **已发货**:需要联系快递公司改签,或等待退回后重新发货\n\n建议您尽快确认订单状态。如订单状态不允许自行修改,请告诉我您的订单号,我来帮您处理!"
    },
    {
        "instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
        "input": '退款多久到账?',
        "output": "退款到账时间取决于支付方式:\n- **支付宝/微信**:1-3 个工作日\n- **银行卡**:3-7 个工作日\n- **信用卡**:7-15 个工作日(取决于发卡行)\n\n退款申请审核通过后会自动退回原支付账户。如超时未收到,请联系我们提供退款凭证,我们协助您催款。"
    },
    {
        "instruction": "你是一位专业的电商客服,用友好、简洁的语气回复客户问题。",
        "input": '商品和图片不一样,能退货吗?',
        "output": "当然可以!商品与描述不符属于质量问题,您享有以下权益:\n1. **7天无理由退换货**(自收货之日起)\n2. **运费由我们承担**(质量/描述问题)\n\n操作方法:订单页→申请退货→选择'与描述不符'→上传对比照片。审核通过后,我们寄出退货标签,您无需自费。非常抱歉给您带来不便!"
    },
]

print("Alpaca 格式训练数据示例:")
print("=" * 60)
for i, ex in enumerate(alpaca_examples, 1):
    print(f"\n样本 {i}:")
    print(f"  instruction: {ex['instruction'][:40]}...")
    print(f"  input: {ex['input']}")
    print(f"  output: {ex['output'][:80]}...")

# 保存到 JSONL 文件
output_path = "/tmp/customer_service_train.jsonl"
with open(output_path, "w", encoding="utf-8") as f:
    for ex in alpaca_examples:
        f.write(json.dumps(ex, ensure_ascii=False) + "\n")

print(f"\n已保存 {len(alpaca_examples)} 个样本到: {output_path}")
Alpaca 格式训练数据示例:
============================================================

样本 1:
  instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
  input: 我昨天下单了,为什么还没发货?
  output: 您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,...

样本 2:
  instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
  input: 收到的商品有破损,怎么办?
  output: 非常抱歉给您带来不便!请您按以下步骤操作:
1. 拍摄破损商品照片(包括包装和商品本身)
2. 在订单页点击'申请售后'
3. 上传照片,选择'商品破损'

我...

样本 3:
  instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
  input: 可以修改收货地址吗?订单已经提交了。
  output: 可以修改!但需要在发货前操作:
- **未发货**:进入订单详情→'修改地址'即可
- **已发货**:需要联系快递公司改签,或等待退回后重新发货

建议您尽快...

样本 4:
  instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
  input: 退款多久到账?
  output: 退款到账时间取决于支付方式:
- **支付宝/微信**:1-3 个工作日
- **银行卡**:3-7 个工作日
- **信用卡**:7-15 个工作日(取决于发...

样本 5:
  instruction: 你是一位专业的电商客服,用友好、简洁的语气回复客户问题。...
  input: 商品和图片不一样,能退货吗?
  output: 当然可以!商品与描述不符属于质量问题,您享有以下权益:
1. **7天无理由退换货**(自收货之日起)
2. **运费由我们承担**(质量/描述问题)

操作方...

已保存 5 个样本到: /tmp/customer_service_train.jsonl
# 将 Alpaca 格式转换为 ChatML 格式(训练对话模型)

def alpaca_to_chatml(examples):
    """将 Alpaca 格式转换为 ChatML 对话格式"""
    chatml_examples = []
    for ex in examples:
        messages = [
            {"role": "system", "content": ex["instruction"]},
            {"role": "user", "content": ex["input"]},
            {"role": "assistant", "content": ex["output"]}
        ]
        chatml_examples.append({"messages": messages})
    return chatml_examples


def format_chatml_string(messages):
    """将 messages 格式化为 ChatML token 字符串(用于 tokenizer)"""
    result = ""
    for msg in messages:
        result += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
    result += "<|im_start|>assistant\n"  # 最后一个 assistant 开始
    return result


chatml_examples = alpaca_to_chatml(alpaca_examples)

print("ChatML 格式示例(样本 1):")
print("-" * 60)
for msg in chatml_examples[0]["messages"]:
    print(f"[{msg['role']}]")
    print(f"{msg['content'][:100]}..." if len(msg['content']) > 100 else msg['content'])
    print()

print("\nChatML Token 字符串格式:")
print("-" * 60)
chatml_str = format_chatml_string(chatml_examples[0]["messages"])
print(chatml_str[:300] + "...")
ChatML 格式示例(样本 1):
------------------------------------------------------------
[system]
你是一位专业的电商客服,用友好、简洁的语气回复客户问题。

[user]
我昨天下单了,为什么还没发货?

[assistant]
您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快...


ChatML Token 字符串格式:
------------------------------------------------------------
<|im_start|>system
你是一位专业的电商客服,用友好、简洁的语气回复客户问题。<|im_end|>
<|im_start|>user
我昨天下单了,为什么还没发货?<|im_end|>
<|im_start|>assistant
您好!感谢您的耐心等待。订单通常在下单后 1-2 个工作日内发货,节假日可能稍有延迟。您可以在'我的订单'页面查看实时物流状态。如果超过 3 个工作日仍未发货,请联系我们,我们会优先处理。祝您购物愉快!<|im_end|>
<|im_start|>assistant
...

Section 4:LoRA 配置与训练(代码框架)#

以下是完整的 LoRA 训练代码。使用 facebook/opt-125m(最小可用模型)演示,实际使用时换成目标模型。

注意:实际训练需要 GPU,以下代码在有 PyTorch 环境时可运行 10 步演示。

# LoRA 训练配置展示

if PEFT_AVAILABLE and TRANSFORMERS_AVAILABLE and TORCH_AVAILABLE:
    from datasets import Dataset
    try:
        from trl import SFTTrainer, SFTConfig
        TRL_AVAILABLE = True
    except ImportError:
        TRL_AVAILABLE = False
        print("trl 未安装:pip install trl")

    # === Step 1:LoRA 配置 ===
    lora_config = LoraConfig(
        r=16,                        # 秩:参数量与效果的权衡
        lora_alpha=32,               # 缩放:通常为 r 的 2 倍
        target_modules=["q_proj", "v_proj"],  # 对注意力层的 Q、V 应用 LoRA
        lora_dropout=0.05,           # 轻微正则化
        bias="none",                 # 不训练 bias
        task_type=TaskType.CAUSAL_LM # 因果语言模型(自回归生成)
    )
    
    print("LoRA 配置:")
    print(f"  rank (r):          {lora_config.r}")
    print(f"  lora_alpha:        {lora_config.lora_alpha}")
    print(f"  缩放因子 (α/r):    {lora_config.lora_alpha / lora_config.r}")
    print(f"  目标模块:          {lora_config.target_modules}")
    print(f"  dropout:           {lora_config.lora_dropout}")
    print(f"  任务类型:          {lora_config.task_type}")

    # === Step 2:加载模型(小模型演示)===
    print("\n正在加载 facebook/opt-125m(125M 参数,CPU 可运行)...")
    
    model_name = "facebook/opt-125m"
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    tokenizer.pad_token = tokenizer.eos_token
    
    base_model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.float32,  # CPU 用 float32
    )
    
    print(f"基础模型总参数: {sum(p.numel() for p in base_model.parameters()) / 1e6:.1f}M")
    
    # 应用 LoRA
    # opt 模型的注意力层名称不同,需要调整
    lora_config_opt = LoraConfig(
        r=16,
        lora_alpha=32,
        target_modules=["q_proj", "v_proj"],
        lora_dropout=0.05,
        bias="none",
        task_type=TaskType.CAUSAL_LM
    )
    
    peft_model = get_peft_model(base_model, lora_config_opt)
    
    trainable, total = 0, 0
    for param in peft_model.parameters():
        total += param.numel()
        if param.requires_grad:
            trainable += param.numel()
    
    print(f"LoRA 后可训练参数: {trainable / 1e6:.2f}M ({trainable/total:.2%})")
    print(f"参数节省: {1 - trainable/total:.2%}")

else:
    print("跳过实际加载(PyTorch/PEFT 未安装)")
    print("\n以下是等价的代码逻辑展示:")
    print("""
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

# 对于 Llama 3 7B:
# 可训练参数 ≈ 8M(0.11%),节省 99.9% 的参数更新
    """)
跳过实际加载(PyTorch/PEFT 未安装)

以下是等价的代码逻辑展示:

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

# 对于 Llama 3 7B:
# 可训练参数 ≈ 8M(0.11%),节省 99.9% 的参数更新
    
# 训练配置和 SFTTrainer 设置

training_args_config = {
    "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,              # LoRA 通常比全参微调用更大的 lr
    "fp16": True,                        # GPU 半精度训练
    "logging_steps": 10,
    "save_steps": 100,
    "warmup_ratio": 0.03,               # 3% 的步骤做 warmup
    "lr_scheduler_type": "cosine",      # 余弦退火
    "max_steps": 10,                    # 演示只跑 10 步
}

print("TrainingArguments 配置说明:")
print("=" * 60)
explanations = {
    "per_device_train_batch_size": "每块 GPU 的 batch 大小,受显存限制",
    "gradient_accumulation_steps": "梯度累积步数,变相增大 batch size",
    "learning_rate": "LoRA 微调推荐 1e-4 到 3e-4",
    "fp16": '半精度训练节省显存(GPU 支持时开启)',
    "warmup_ratio": '先小 lr 稳定训练,再升到目标 lr',
    "lr_scheduler_type": "cosine:训练后期 lr 缓慢降低",
}
for key, val in training_args_config.items():
    exp = explanations.get(key, "")
    print(f"  {key:<35} = {str(val):<10}  # {exp}")

print("\n完整训练代码框架(Llama 3 + LoRA):")
print("-" * 60)
training_code = '''
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_dataset

# 1. 加载模型和 tokenizer
model_name = "meta-llama/Meta-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")

# 2. LoRA 配置
lora_config = LoraConfig(
    r=16, lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05, task_type=TaskType.CAUSAL_LM
)

# 3. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# 4. 训练配置
training_args = TrainingArguments(
    output_dir="./lora_output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    save_steps=100,
    logging_steps=10,
)

# 5. 开始训练
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    tokenizer=tokenizer,
)
trainer.train()
'''
print(training_code)
TrainingArguments 配置说明:
============================================================
  output_dir                          = ./lora_output  # 
  num_train_epochs                    = 3           # 
  per_device_train_batch_size         = 4           # 每块 GPU 的 batch 大小,受显存限制
  gradient_accumulation_steps         = 4           # 梯度累积步数,变相增大 batch size
  learning_rate                       = 0.0002      # LoRA 微调推荐 1e-4 到 3e-4
  fp16                                = True        # 半精度训练节省显存(GPU 支持时开启)
  logging_steps                       = 10          # 
  save_steps                          = 100         # 
  warmup_ratio                        = 0.03        # 先小 lr 稳定训练,再升到目标 lr
  lr_scheduler_type                   = cosine      # cosine:训练后期 lr 缓慢降低
  max_steps                           = 10          # 

完整训练代码框架(Llama 3 + LoRA):
------------------------------------------------------------

from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_dataset

# 1. 加载模型和 tokenizer
model_name = "meta-llama/Meta-Llama-3-8B"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, torch_dtype="auto")

# 2. LoRA 配置
lora_config = LoraConfig(
    r=16, lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0.05, task_type=TaskType.CAUSAL_LM
)

# 3. 应用 LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

# 4. 训练配置
training_args = TrainingArguments(
    output_dir="./lora_output",
    num_train_epochs=3,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    fp16=True,
    save_steps=100,
    logging_steps=10,
)

# 5. 开始训练
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    tokenizer=tokenizer,
)
trainer.train()

Section 5:加载与使用 LoRA 模型#

# 加载和使用 LoRA adapter 的完整流程

if PEFT_AVAILABLE and TRANSFORMERS_AVAILABLE and TORCH_AVAILABLE:
    # 假设训练完成,展示如何加载和使用
    
    # 方法 1:加载基础模型 + LoRA adapter(灵活,可切换不同 adapter)
    print("方法 1:加载基础模型 + LoRA Adapter")
    print("-" * 50)
    
    # 用前面训练的 peft_model 演示保存
    save_path = "/tmp/lora_adapter_demo"
    peft_model.save_pretrained(save_path)
    print(f"LoRA Adapter 已保存到: {save_path}")
    
    import os
    saved_files = os.listdir(save_path)
    print(f'保存的文件: {saved_files}')
    
    # 加载:先加载基础模型,再加载 adapter
    loaded_model = AutoModelForCausalLM.from_pretrained(
        "facebook/opt-125m",
        torch_dtype=torch.float32,
    )
    loaded_with_adapter = PeftModel.from_pretrained(loaded_model, save_path)
    print("\nLoRA Adapter 加载成功!")
    
    # 方法 2:合并 LoRA 权重到基础模型(推理更快,不可恢复)
    print("\n方法 2:合并权重(merge_and_unload)")
    print("-" * 50)
    merged_model = loaded_with_adapter.merge_and_unload()
    print("权重已合并,现在是普通 HuggingFace 模型")
    print(f"合并后模型类型: {type(merged_model).__name__}")
    
    # 保存合并后的完整模型
    merged_save_path = "/tmp/merged_model_demo"
    merged_model.save_pretrained(merged_save_path)
    tokenizer.save_pretrained(merged_save_path)
    print(f"合并模型已保存到: {merged_save_path}")
    
    # 推理演示
    print("\n推理演示:")
    print("-" * 50)
    test_input = "Customer: I haven't received my order yet."
    inputs = tokenizer(test_input, return_tensors="pt")
    
    with torch.no_grad():
        outputs = merged_model.generate(
            **inputs,
            max_new_tokens=50,
            temperature=0.7,
            do_sample=True,
            pad_token_id=tokenizer.eos_token_id
        )
    
    generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(f'输入: {test_input}')
    print(f"输出: {generated[len(test_input):]}")
    print("(注意:125M 模型很小,效果仅供演示,实际用 7B+ 模型)")

else:
    print("PyTorch/PEFT 未安装,展示等价代码:")
    code = '''
# 加载 LoRA adapter
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model_with_lora = PeftModel.from_pretrained(base_model, "./lora_output")

# 合并权重(推理加速)
merged_model = model_with_lora.merge_and_unload()
merged_model.save_pretrained("./merged_model")
'''
    print(code)
PyTorch/PEFT 未安装,展示等价代码:

# 加载 LoRA adapter
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B")
model_with_lora = PeftModel.from_pretrained(base_model, "./lora_output")

# 合并权重(推理加速)
merged_model = model_with_lora.merge_and_unload()
merged_model.save_pretrained("./merged_model")

Section 6:QLoRA — 量化 + LoRA#

QLoRA(Quantized LoRA)将基础模型压缩到 4-bit 精度,再应用 LoRA 微调。

显存对比#

方法

Llama 3 7B 显存需求

GPU 要求

全参微调 (fp16)

~56GB

至少 2× A100 80GB

LoRA (fp16)

~16GB

A100 40GB

QLoRA (4-bit + LoRA)

~6GB

RTX 3090/4090 即可

4-bit 量化原理:将 16-bit 浮点数压缩到 4-bit 整数存储,推理时动态反量化。精度损失极小(< 1%),显存节省 4x。

# QLoRA 配置示例(需要 bitsandbytes 库)

print("QLoRA 配置代码(需要 GPU + bitsandbytes):")
print("=" * 60)

qlora_code = '''
from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
import torch

# Step 1:4-bit 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,              # 4-bit 加载
    bnb_4bit_quant_type="nf4",      # NF4 量化(QLoRA 论文推荐)
    bnb_4bit_compute_dtype=torch.bfloat16,  # 计算时用 bf16
    bnb_4bit_use_double_quant=True,  # 嵌套量化,进一步省显存
)

# Step 2:加载量化后的基础模型(7B 模型只需 ~6GB 显存)
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=bnb_config,
    device_map="auto",  # 自动分配 GPU/CPU
)

# Step 3:为量化模型准备 LoRA 微调
model = prepare_model_for_kbit_training(model)  # 重要!

# Step 4:应用 LoRA(与普通 LoRA 相同)
lora_config = LoraConfig(
    r=64,           # QLoRA 通常用更大 rank 补偿量化误差
    lora_alpha=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],  # 覆盖更多层
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

model = get_peft_model(model, lora_config)
# 显存使用:~6-8GB(原本需要 56GB)
'''

print(qlora_code)

# 显存节省计算
print("\n量化后的显存节省:")
print("-" * 50)

model_params_b = 7  # 7B 模型
bytes_per_param = {
    "fp32 (全参微调)": 4,
    "fp16/bf16": 2,
    "int8 (8-bit)": 1,
    "nf4 (4-bit QLoRA)": 0.5,
}

print(f"{'精度':<20} {'模型显存':>10} {'+ 训练开销':>12} {'总计'}")
print("-" * 55)
for precision, bpp in bytes_per_param.items():
    model_gb = model_params_b * 1e9 * bpp / 1e9
    if '全参' in precision:
        train_overhead_gb = model_gb * 3  # 梯度 + Adam 状态
    else:
        train_overhead_gb = 2  # LoRA 参数很少
    total = model_gb + train_overhead_gb
    print(f"{precision:<20} {model_gb:>8.1f}GB {train_overhead_gb:>10.1f}GB {total:>8.1f}GB")
QLoRA 配置代码(需要 GPU + bitsandbytes):
============================================================

from transformers import BitsAndBytesConfig, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
import torch

# Step 1:4-bit 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,              # 4-bit 加载
    bnb_4bit_quant_type="nf4",      # NF4 量化(QLoRA 论文推荐)
    bnb_4bit_compute_dtype=torch.bfloat16,  # 计算时用 bf16
    bnb_4bit_use_double_quant=True,  # 嵌套量化,进一步省显存
)

# Step 2:加载量化后的基础模型(7B 模型只需 ~6GB 显存)
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=bnb_config,
    device_map="auto",  # 自动分配 GPU/CPU
)

# Step 3:为量化模型准备 LoRA 微调
model = prepare_model_for_kbit_training(model)  # 重要!

# Step 4:应用 LoRA(与普通 LoRA 相同)
lora_config = LoraConfig(
    r=64,           # QLoRA 通常用更大 rank 补偿量化误差
    lora_alpha=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],  # 覆盖更多层
    lora_dropout=0.05,
    bias="none",
    task_type=TaskType.CAUSAL_LM
)

model = get_peft_model(model, lora_config)
# 显存使用:~6-8GB(原本需要 56GB)


量化后的显存节省:
--------------------------------------------------
精度                         模型显存       + 训练开销 总计
-------------------------------------------------------
fp32 (全参微调)              28.0GB       84.0GB    112.0GB
fp16/bf16                14.0GB        2.0GB     16.0GB
int8 (8-bit)              7.0GB        2.0GB      9.0GB
nf4 (4-bit QLoRA)         3.5GB        2.0GB      5.5GB

Section 7:LoRA vs 全参微调 — 何时选择哪个?#

# LoRA vs 全参微调 对比

comparison = {
    '维度': ['训练参数量', '显存需求', '训练时间', '效果(任务适配)', '灾难性遗忘', '可逆性', '多任务', '推荐场景'],
    "LoRA / QLoRA": [
        "< 1%(几百万参数)",
        '低(6-16GB)',
        '快(小时级)',
        "95%(接近全参)",
        '几乎无',
        '可(卸载 adapter)',
        '多个 adapter 切换',
        '资源有限、任务适配、快速迭代'
    ],
    '全参微调': [
        "100%(数十亿参数)",
        '高(64GB+)',
        '慢(天级)',
        "100%(最佳)",
        '可能发生',
        '不可逆',
        '需要多个模型副本',
        '数据充足、追求极致效果'
    ]
}

print(f"{'维度':<18} {'LoRA / QLoRA':<35} {'全参微调'}")
print("-" * 85)
for i in range(len(comparison['维度'])):
    dim = comparison['维度'][i]
    lora = comparison["LoRA / QLoRA"][i]
    full = comparison['全参微调'][i]
    print(f"{dim:<18} {lora:<35} {full}")
维度                 LoRA / QLoRA                        全参微调
-------------------------------------------------------------------------------------
训练参数量              < 1%(几百万参数)                         100%(数十亿参数)
显存需求               低(6-16GB)                           高(64GB+)
训练时间               快(小时级)                              慢(天级)
效果(任务适配)           95%(接近全参)                           100%(最佳)
灾难性遗忘              几乎无                                 可能发生
可逆性                可(卸载 adapter)                       不可逆
多任务                多个 adapter 切换                       需要多个模型副本
推荐场景               资源有限、任务适配、快速迭代                      数据充足、追求极致效果

总结:LoRA 超参数调优指南#

超参数选择表#

超参数

低资源场景

标准场景

追求最佳效果

r (rank)

4-8

16

32-64

lora_alpha

r 的 2 倍

r 的 2 倍

r 或 r 的 2 倍

lora_dropout

0.05

0.05

0.1

target_modules

q_proj, v_proj

q_proj, k_proj, v_proj, o_proj

所有线性层

learning_rate

3e-4

2e-4

1e-4

batch_size

4

16

32+

关键经验#

  1. rank 越大效果越好,但参数量线性增长,通常 r=16 是最佳平衡点

  2. alpha 通常设为 r 的 2 倍,相当于有效 learning rate = lr × (alpha/r) = lr × 2

  3. target_modules 越多覆盖越全,但训练更慢;从 q_proj+v_proj 开始实验

  4. 数据质量 > 数据量 > 超参数调优,先确保数据质量

  5. QLoRA 是资源受限时的首选,效果损失 < 1%,显存节省 4x

下一步#

  • 下一章(07_production/01_evaluation.ipynb):如何评估微调后的模型效果

  • 推荐实践:使用 LLaMA-Factory 一键完成 LoRA 微调