LLM 大模型微调完全教程

教程简介

零基础LLM大模型微调完全教程,涵盖监督微调SFT、LoRA/QLoRA低秩适配、数据工程、LLaMA-Factory/Axolotl/Unsloth框架对比、RLHF/DPO强化学习、评估与基准测试、模型合并导出、分布式微调DeepSpeed/FSDP等核心技能,配有从零微调行业专属大模型实战项目,适合AI开发者系统学习。

LLM 大模型微调完全教程

面向零基础用户,从原理到实战,手把手教你微调大语言模型

版本:v1.0 | 最后更新:2026年5月


目录


第一章:大模型微调概述

1.1 什么是大模型微调

大模型微调(Fine-tuning)是指在一个已经经过大规模预训练的语言模型基础上,使用特定领域或特定任务的数据集进行进一步训练,使模型能够更好地适应目标应用场景的过程。简单来说,预训练让模型"学会了语言",而微调让模型"学会了做你的工作"。

想象一下,一个医学院的学生在大学里学习了广泛的医学知识(这相当于预训练阶段),然后他选择成为一名心脏外科医生,在心脏外科领域进行深入的专科训练(这就相当于微调阶段)。虽然他已经具备了扎实的医学基础,但通过专科训练,他在心脏手术方面变得更加专业和精准。

在大模型的世界里,预训练通常需要消耗数百万美元的计算资源和数万亿个文本标记(Token),这不是普通开发者能够承担的。但微调则不同,通过合理的技术手段,我们甚至可以在一张消费级显卡上完成微调工作,这大大降低了大模型应用的门槛。

微调的核心价值在于以下几点:

领域适配:通用大模型虽然知识广泛,但在特定领域(如医疗、法律、金融)的专业深度可能不足。通过微调,可以将领域知识注入模型,使其在特定场景下表现更佳。

风格定制:不同的应用场景需要不同的回答风格。客服场景需要温和礼貌,技术文档需要严谨准确,创意写作需要灵活多变。微调可以让模型学会你期望的表达方式。

格式控制:在很多实际应用中,我们要求模型输出特定格式的内容,比如JSON结构化数据、表格、代码片段等。微调可以让模型更好地遵循输出格式要求。

成本优化:通过微调一个小模型(如7B参数),使其在特定任务上达到甚至超越大模型(如70B参数)的效果,可以大幅降低推理成本。

隐私保护:微调可以在本地进行,敏感数据不需要发送到外部API,这对金融、医疗等对数据隐私要求严格的行业尤为重要。

1.2 为什么需要微调

在实际应用中,我们常常会遇到这样的情况:使用通用大模型(如ChatGPT、Claude等)虽然能够完成大部分任务,但在某些特定场景下总是差强人意。这时候,微调就是一个非常有效的解决方案。

场景一:企业客服系统

假设你正在为一家电商平台搭建智能客服系统。通用大模型虽然能够回答大多数问题,但它不了解你公司的退换货政策、会员积分规则、促销活动细节等。通过微调,你可以让模型成为"最懂你公司业务的客服专家"。

场景二:代码生成助手

你希望拥有一个专门生成你公司内部框架代码的助手。通用大模型可能不了解你们的框架API、编码规范和项目结构。通过微调,可以让模型按照你们的规范生成高质量的内部代码。

场景三:医疗问诊辅助

通用大模型在医学问答中可能会产生幻觉(Hallucination),给出不准确的医学建议。通过使用经过验证的医学问答数据进行微调,可以提高模型在医学领域的准确性和可靠性。

场景四:多语言翻译

虽然通用大模型支持多种语言,但在小语种或专业术语翻译方面可能表现不佳。通过使用高质量的平行语料进行微调,可以显著提升特定语言对的翻译质量。

1.3 微调 vs 提示工程

很多初学者会问:既然可以通过精心设计提示词(Prompt Engineering)来引导模型输出,为什么还需要微调呢?这是一个非常好的问题,让我们来详细对比一下这两种方法。

提示工程(Prompt Engineering) 是指通过设计巧妙的输入提示来引导大模型产生期望的输出,而不修改模型本身的参数。常见技术包括:

  • 零样本提示(Zero-shot Prompting):直接给出任务描述,不提供示例
  • 少样本提示(Few-shot Prompting):在提示中提供几个示例
  • 思维链(Chain-of-Thought):引导模型逐步推理
  • 系统提示(System Prompt):设定模型的角色和行为规范

微调(Fine-tuning) 则是通过训练数据来修改模型的权重参数,使模型的行为发生持久性变化。

两者的核心区别如下:

维度 提示工程 微调
是否修改模型参数
实施成本 低(只需设计提示词) 中高(需要数据、算力、训练)
灵活性 高(随时修改提示词) 中(需要重新训练)
推理成本 高(提示词占用上下文窗口) 低(无需长提示词)
效果上限 受限于模型原始能力 可以超越模型原始能力
适用场景 快速原型、通用任务 专业领域、大规模部署
持久性 不持久(每次都需要提示) 持久(模型权重已改变)
数据需求 不需要训练数据 需要高质量训练数据

什么时候用提示工程?

  • 任务简单,通用模型已经能够胜任
  • 需要快速验证想法,没有时间准备训练数据
  • 任务需求经常变化
  • 预算有限,无法承担训练成本

什么时候用微调?

  • 通用模型在特定任务上表现不佳
  • 需要模型学习特定的输出格式或风格
  • 需要降低推理成本(减少提示词长度)
  • 需要在本地部署,保证数据隐私
  • 需要让小模型达到大模型的效果

两者结合的策略

在实际项目中,提示工程和微调并不是互斥的,而是可以结合使用的。一个常见的最佳实践是:

  1. 首先尝试提示工程,评估通用模型的上限
  2. 如果提示工程无法满足需求,收集训练数据进行微调
  3. 微调完成后,仍然可以使用提示工程来进一步优化输出

1.4 微调的类型

根据微调的方式和目标,可以将微调分为以下几种主要类型:

1.4.1 全参数微调(Full Fine-tuning)

全参数微调是指更新模型的所有参数。这是最直接的微调方式,效果通常最好,但需要的计算资源也最多。

模型参数量与显存需求对照表(全参数微调,FP16):
- 7B 模型  → 约 60-80 GB 显存
- 13B 模型 → 约 120-160 GB 显存
- 70B 模型 → 约 600-800 GB 显存

全参数微调的优缺点:

优点

  • 效果最好,模型可以充分适应目标任务
  • 不需要设计额外的适配器结构
  • 实现简单,直接使用标准训练流程

缺点

  • 显存需求巨大,需要高端GPU或GPU集群
  • 容易过拟合,特别是小数据集场景
  • 训练时间长
  • 每个任务需要保存完整的模型副本

1.4.2 参数高效微调(Parameter-Efficient Fine-tuning, PEFT)

参数高效微调只更新模型的一小部分参数,或者在模型中添加少量新参数进行训练。这是目前最流行的微调方式。

主要的PEFT方法包括:

LoRA(Low-Rank Adaptation):在原始权重矩阵旁边添加低秩矩阵,只训练这些低秩矩阵。这是目前最流行的PEFT方法。

QLoRA(Quantized LoRA):在LoRA的基础上,将基础模型量化为4-bit,进一步减少显存需求。

Adapter:在Transformer层之间插入小型适配器模块,只训练适配器参数。

Prefix Tuning:在输入序列前添加可训练的前缀向量。

Prompt Tuning:在输入嵌入中添加可训练的软提示向量。

PEFT方法显存需求对比(以7B模型为例):
- 全参数微调(FP16):约 60-80 GB
- LoRA(FP16基础模型):约 16-20 GB
- QLoRA(4-bit基础模型):约 6-10 GB
- Prefix Tuning:约 14-18 GB

1.4.3 指令微调(Instruction Tuning)

指令微调是一种特殊的微调方式,其训练数据格式为"指令-输入-输出"三元组。通过指令微调,模型可以学会遵循各种自然语言指令来完成任务。

典型的指令微调数据格式如下:

{
  "instruction": "请将以下英文翻译成中文",
  "input": "The weather is beautiful today.",
  "output": "今天天气很好。"
}
{
  "instruction": "请总结以下文章的主要内容",
  "input": "人工智能(AI)是计算机科学的一个分支...",
  "output": "本文介绍了人工智能的定义、发展历史和主要应用领域。"
}

指令微调的意义在于:预训练模型虽然具备强大的语言能力,但它本质上是一个"续写机器"——给定前文,预测下一个词。通过指令微调,我们教会模型理解并遵循人类的指令,使其从一个"续写机器"变成一个"助手"。

1.4.4 RLHF 微调

RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)是一种更高级的微调方式,通过人类对模型输出的偏好反馈来优化模型行为。

RLHF通常包含三个阶段:

  1. SFT阶段:使用监督数据进行指令微调
  2. 奖励模型训练:训练一个奖励模型来评估输出质量
  3. PPO/DPO优化:使用强化学习算法优化模型

RLHF的目标是让模型的输出更加符合人类的偏好,比如更加安全、有帮助、诚实。ChatGPT的成功很大程度上就归功于RLHF技术的应用。

1.4.5 领域自适应预训练(Domain-Adaptive Pre-training, DAPT)

在某些情况下,我们有大量的领域文本数据(如医学论文、法律文书),但这些数据没有标注。此时可以先用这些无标注数据对模型进行继续预训练,然后再用标注数据进行微调。

DAPT的流程是:

  1. 收集大量领域无标注文本
  2. 使用类似预训练的方式(下一个词预测)继续训练模型
  3. 使用标注数据进行指令微调

这种方法特别适合那些与通用文本差异较大的专业领域,如生物医学、法律、金融等。

1.5 微调的基本流程概览

无论使用哪种微调方法,整体流程大致相同:

1. 明确目标任务和评估标准
        ↓
2. 选择基础模型(Base Model)
        ↓
3. 收集和准备训练数据
        ↓
4. 选择微调方法和框架
        ↓
5. 配置超参数并开始训练
        ↓
6. 评估模型效果
        ↓
7. 迭代优化(调整数据/超参数)
        ↓
8. 模型导出和部署

在后续章节中,我们将对每个步骤进行详细的讲解和实战演示。

1.6 硬件需求概览

在开始微调之前,了解硬件需求是非常重要的。以下是不同场景下的硬件需求参考:

场景 最低GPU要求 推荐GPU 显存需求
QLoRA微调7B模型 RTX 3060 12GB RTX 4090 24GB 6-10 GB
LoRA微调7B模型 RTX 3090 24GB A100 40GB 16-20 GB
全参数微调7B模型 A100 40GB A100 80GB x2 60-80 GB
QLoRA微调13B模型 RTX 4090 24GB A100 40GB 12-16 GB
LoRA微调70B模型 A100 80GB x4 H100 80GB x4 140-200 GB

如果没有本地GPU资源,可以考虑以下云平台:

  • AutoDL:国内平台,价格实惠,适合入门
  • Google Colab Pro:提供免费T4 GPU,Pro版提供A100
  • AWS SageMaker:企业级平台,功能完善
  • Lambda Cloud:专注GPU云服务,性价比高

第二章:监督微调 SFT 原理与实践

2.1 SFT 的基本原理

监督微调(Supervised Fine-Tuning,SFT)是最基础也是最重要的微调方式。它的核心思想非常简单:给模型展示大量的"问题-答案"对,让模型学会在给定问题时生成正确的答案。

从技术角度来说,SFT的训练过程与预训练非常相似,都是通过最小化交叉熵损失函数来更新模型参数。区别在于:

  • 预训练:使用海量无标注文本,让模型学习"预测下一个词"
  • SFT:使用有标注的指令-回答数据,让模型学习"遵循指令生成回答"

SFT的损失函数可以表示为:

L = -Σ log P(y_t | y_<t, x)

其中:
- x 是输入的指令/问题
- y 是目标输出/回答
- y_t 是输出序列中第 t 个 token
- y_<t 是第 t 个 token 之前的所有 token
- P 是模型预测的概率分布

在实际训练中,我们通常只计算输出部分的损失,而不计算输入部分的损失。这是因为我们希望模型学会"生成"回答,而不是"记住"输入。

2.2 SFT 数据准备

SFT数据的质量直接决定了微调效果。俗话说"Garbage in, garbage out",在微调领域尤其如此。

2.2.1 数据格式

SFT数据通常有以下几种常见格式:

Alpaca格式

[
  {
    "instruction": "请解释什么是机器学习",
    "input": "",
    "output": "机器学习是人工智能的一个分支,它使计算机系统能够从数据中学习并改进,而无需被明确编程。通过使用算法来分析数据、识别模式并做出决策,机器学习已经在图像识别、自然语言处理、推荐系统等领域取得了广泛应用。"
  },
  {
    "instruction": "将以下句子翻译成英文",
    "input": "今天天气真好,我们去公园散步吧。",
    "output": "The weather is really nice today. Let's go for a walk in the park."
  }
]

ShareGPT格式

[
  {
    "conversations": [
      {"from": "human", "value": "什么是深度学习?"},
      {"from": "gpt", "value": "深度学习是机器学习的一个子领域,它使用多层神经网络来学习数据的层次化表示。"},
      {"from": "human", "value": "它和传统机器学习有什么区别?"},
      {"from": "gpt", "value": "深度学习的主要区别在于它能够自动学习特征,而传统机器学习通常需要人工设计特征。"}
    ]
  }
]

对话格式(Chat Format)

[
  {
    "messages": [
      {"role": "system", "content": "你是一个专业的法律顾问"},
      {"role": "user", "content": "什么是知识产权?"},
      {"role": "assistant", "content": "知识产权是指人们对其智力劳动成果所享有的专有权利,主要包括专利权、商标权、著作权等。"}
    ]
  }
]

2.2.2 数据规模建议

数据规模是一个经常被问到的问题。以下是基于实践经验的一些参考建议:

任务类型 建议数据量 说明
简单分类任务 500-2000条 如情感分析、意图识别
特定风格生成 1000-5000条 如客服回复、文案生成
领域知识问答 5000-20000条 如医疗问答、法律咨询
复杂推理任务 10000-50000条 如数学推理、代码生成
通用能力增强 50000条以上 如综合助手能力提升

需要注意的是,数据质量远比数据数量重要。1000条高质量数据的微调效果可能远好于10000条低质量数据。

2.2.3 数据质量检查清单

在开始训练之前,务必对数据进行质量检查:

import json
import re
from collections import Counter

def quality_check(data_path):
    """SFT数据质量检查脚本"""
    with open(data_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    print(f"数据总量: {len(data)} 条")

    # 1. 检查空值
    empty_count = 0
    for item in data:
        if not item.get('instruction', '').strip():
            empty_count += 1
        if not item.get('output', '').strip():
            empty_count += 1
    print(f"空值数量: {empty_count}")

    # 2. 检查输出长度分布
    lengths = [len(item.get('output', '')) for item in data]
    print(f"输出长度 - 最小: {min(lengths)}, 最大: {max(lengths)}, "
          f"平均: {sum(lengths)/len(lengths):.0f}")

    # 3. 检查重复数据
    outputs = [item.get('output', '') for item in data]
    duplicate_count = len(outputs) - len(set(outputs))
    print(f"重复输出数量: {duplicate_count}")

    # 4. 检查语言一致性
    chinese_ratio = sum(1 for item in data
                       if re.search(r'[\u4e00-\u9fff]', item.get('output', ''))) / len(data)
    print(f"中文输出比例: {chinese_ratio:.2%}")

    # 5. 检查特殊字符
    special_char_count = sum(1 for item in data
                           if re.search(r'[\x00-\x08\x0b\x0c\x0e-\x1f]',
                                       item.get('output', '')))
    print(f"含特殊控制字符的条目: {special_char_count}")

# 使用示例
quality_check('train_data.json')

2.3 SFT 训练流程

下面我们将完整演示一次SFT训练流程,使用Transformers库和一个7B参数的模型。

2.3.1 环境准备

首先安装必要的依赖:

# 创建虚拟环境
conda create -n sft python=3.10 -y
conda activate sft

# 安装PyTorch(根据CUDA版本选择)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

# 安装Transformers和相关库
pip install transformers>=4.40.0
pip install datasets accelerate peft
pip install bitsandbytes  # 用于量化
pip install wandb  # 用于训练监控(可选)
pip install flash-attn --no-build-isolation  # Flash Attention加速(可选)

2.3.2 数据处理

import json
from datasets import Dataset
from transformers import AutoTokenizer

# 加载分词器
model_name = "Qwen/Qwen2-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# 加载数据
with open('train_data.json', 'r', encoding='utf-8') as f:
    raw_data = json.load(f)

# 格式化为训练格式
def format_data(example):
    """将数据格式化为模型输入格式"""
    if example.get('input', ''):
        prompt = f"### 指令:\n{example['instruction']}\n\n### 输入:\n{example['input']}\n\n### 回答:\n"
    else:
        prompt = f"### 指令:\n{example['instruction']}\n\n### 回答:\n"

    return {
        'prompt': prompt,
        'response': example['output'],
        'text': prompt + example['output'] + tokenizer.eos_token
    }

# 应用格式化
formatted_data = [format_data(item) for item in raw_data]
dataset = Dataset.from_list(formatted_data)

# 分割训练集和验证集
split_dataset = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = split_dataset['train']
eval_dataset = split_dataset['test']

print(f"训练集大小: {len(train_dataset)}")
print(f"验证集大小: {len(eval_dataset)}")

2.3.3 Tokenization处理

def tokenize_function(examples):
    """对文本进行tokenization"""
    result = tokenizer(
        examples['text'],
        truncation=True,
        max_length=2048,
        padding=False,
        return_tensors=None,
    )
    result['labels'] = result['input_ids'].copy()
    return result

# 应用tokenization
train_tokenized = train_dataset.map(
    tokenize_function,
    batched=True,
    remove_columns=train_dataset.column_names,
    desc="Tokenizing训练集",
)

eval_tokenized = eval_dataset.map(
    tokenize_function,
    batched=True,
    remove_columns=eval_dataset.column_names,
    desc="Tokenizing验证集",
)

2.3.4 模型加载与训练配置

import torch
from transformers import (
    AutoModelForCausalLM,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq,
    BitsAndBytesConfig,
)

# 量化配置(可选,用于减少显存占用)
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

# 加载模型
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
    torch_dtype=torch.bfloat16,
)

# 训练参数配置
training_args = TrainingArguments(
    output_dir="./output/sft_model",
    num_train_epochs=3,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=2e-5,
    weight_decay=0.01,
    warmup_ratio=0.1,
    lr_scheduler_type="cosine",
    logging_steps=10,
    save_strategy="steps",
    save_steps=200,
    evaluation_strategy="steps",
    eval_steps=200,
    save_total_limit=3,
    bf16=True,
    gradient_checkpointing=True,
    optim="paged_adamw_8bit",
    report_to="wandb",  # 设为"none"禁用wandb
    dataloader_num_workers=4,
    remove_unused_columns=False,
)

# 数据整理器
data_collator = DataCollatorForSeq2Seq(
    tokenizer=tokenizer,
    padding=True,
    max_length=2048,
    return_tensors="pt",
)

# 创建Trainer
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_tokenized,
    eval_dataset=eval_tokenized,
    data_collator=data_collator,
    tokenizer=tokenizer,
)

# 开始训练
trainer.train()

# 保存模型
trainer.save_model("./output/sft_model_final")
tokenizer.save_pretrained("./output/sft_model_final")

2.4 超参数调优

超参数的选择对微调效果有着至关重要的影响。以下是关键超参数的详细说明和调优建议:

2.4.1 学习率(Learning Rate)

学习率是最重要的超参数之一。太大会导致训练不稳定,太小会导致收敛缓慢。

推荐学习率范围:
- 全参数微调:1e-5 到 5e-5
- LoRA微调:1e-4 到 3e-4
- QLoRA微调:1e-4 到 2e-4

2.4.2 批次大小(Batch Size)

批次大小影响训练的稳定性和速度。由于GPU显存限制,我们通常使用梯度累积来实现等效的大批次。

有效批次大小 = per_device_batch_size × gradient_accumulation_steps × num_gpus

推荐有效批次大小:16 到 128

示例配置:
- 单卡,per_device_batch_size=2, gradient_accumulation=8 → 有效批次=16
- 双卡,per_device_batch_size=4, gradient_accumulation=4 → 有效批次=32

2.4.3 训练轮数(Epochs)

训练轮数取决于数据量和任务复杂度。

推荐训练轮数:
- 数据量 < 1000条:3-5个epoch
- 数据量 1000-10000条:2-3个epoch
- 数据量 > 10000条:1-2个epoch

2.4.4 序列长度(Max Sequence Length)

序列长度决定了模型能处理的最长输入+输出。

常见序列长度设置:
- 短文本任务(分类、摘要):512-1024
- 中等长度任务(问答、翻译):1024-2048
- 长文本任务(文档分析):2048-4096
- 超长文本任务:4096-8192+

2.4.5 学习率调度器(Learning Rate Scheduler)

常用的学习率调度器包括:

# 余弦退火(推荐)
lr_scheduler_type = "cosine"

# 线性衰减
lr_scheduler_type = "linear"

# 带重启的余弦退火
lr_scheduler_type = "cosine_with_restarts"

2.4.6 预热比例(Warmup Ratio)

预热阶段让学习率从一个很小的值逐渐增长到设定值,有助于训练稳定。

# 推荐预热比例
warmup_ratio = 0.05  # 总步数的5%
warmup_ratio = 0.1   # 总步数的10%

2.5 训练监控与调试

在训练过程中,监控关键指标是非常重要的。

2.5.1 使用Weights & Biases监控

import wandb

# 初始化wandb
wandb.init(
    project="llm-sft",
    name="qwen2-7b-sft-experiment-1",
    config={
        "model": "Qwen2-7B-Instruct",
        "learning_rate": 2e-5,
        "epochs": 3,
        "batch_size": 16,
    }
)

# 在TrainingArguments中启用
training_args = TrainingArguments(
    # ... 其他参数
    report_to="wandb",
)

2.5.2 关键监控指标

训练过程中需要关注以下指标:

  • Training Loss:训练损失,应该持续下降
  • Evaluation Loss:验证损失,如果开始上升说明过拟合
  • Learning Rate:学习率变化曲线
  • GPU Memory Usage:显存使用情况
  • Tokens per Second:训练吞吐量

2.5.3 过拟合检测与处理

过拟合是微调中最常见的问题。以下是检测和处理方法:

# 过拟合检测指标
overfitting_score = eval_loss - train_loss

if overfitting_score > 0.5:
    print("警告:可能存在严重过拟合!")
    print("建议:")
    print("1. 增加训练数据量")
    print("2. 增加weight_decay(如0.1)")
    print("3. 减少训练轮数")
    print("4. 使用dropout")
    print("5. 数据增强")

2.6 完整SFT训练脚本

以下是一个完整的、可直接运行的SFT训练脚本:

#!/usr/bin/env python3
"""
完整的SFT训练脚本
支持QLoRA微调,适用于单卡GPU
"""

import os
import json
import torch
from datasets import Dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq,
    BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model, TaskType

# ============ 配置区域 ============
CONFIG = {
    "model_name": "Qwen/Qwen2-7B-Instruct",
    "data_path": "train_data.json",
    "output_dir": "./output/sft",
    "max_seq_length": 2048,
    "num_epochs": 3,
    "batch_size": 2,
    "gradient_accumulation": 8,
    "learning_rate": 2e-5,
    "lora_rank": 16,
    "lora_alpha": 32,
    "lora_dropout": 0.05,
    "warmup_ratio": 0.1,
    "use_qlora": True,
}

# ============ 数据处理 ============
def load_and_format_data(data_path, tokenizer):
    """加载并格式化训练数据"""
    with open(data_path, 'r', encoding='utf-8') as f:
        raw_data = json.load(f)

    formatted = []
    for item in raw_data:
        instruction = item.get('instruction', '')
        inp = item.get('input', '')
        output = item.get('output', '')

        if inp:
            text = f"### 指令:\n{instruction}\n\n### 输入:\n{inp}\n\n### 回答:\n{output}{tokenizer.eos_token}"
        else:
            text = f"### 指令:\n{instruction}\n\n### 回答:\n{output}{tokenizer.eos_token}"

        formatted.append({"text": text})

    return Dataset.from_list(formatted)

def tokenize_dataset(dataset, tokenizer, max_length):
    """对数据集进行tokenization"""
    def tokenize_fn(examples):
        return tokenizer(
            examples['text'],
            truncation=True,
            max_length=max_length,
            padding=False,
        )

    return dataset.map(
        tokenize_fn,
        batched=True,
        remove_columns=dataset.column_names,
        desc="Tokenizing",
    )

# ============ 模型加载 ============
def load_model_and_tokenizer(model_name, use_qlora=True):
    """加载模型和分词器"""
    tokenizer = AutoTokenizer.from_pretrained(
        model_name,
        trust_remote_code=True,
        padding_side="right",
    )
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    load_kwargs = {
        "trust_remote_code": True,
        "torch_dtype": torch.bfloat16,
        "device_map": "auto",
    }

    if use_qlora:
        bnb_config = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_quant_type="nf4",
            bnb_4bit_compute_dtype=torch.bfloat16,
            bnb_4bit_use_double_quant=True,
        )
        load_kwargs["quantization_config"] = bnb_config

    model = AutoModelForCausalLM.from_pretrained(model_name, **load_kwargs)
    model.config.use_cache = False

    return model, tokenizer

# ============ LoRA配置 ============
def setup_lora(model, config):
    """配置LoRA适配器"""
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=config["lora_rank"],
        lora_alpha=config["lora_alpha"],
        lora_dropout=config["lora_dropout"],
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                        "gate_proj", "up_proj", "down_proj"],
        bias="none",
    )
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()
    return model

# ============ 主函数 ============
def main():
    # 加载模型和分词器
    print("加载模型和分词器...")
    model, tokenizer = load_model_and_tokenizer(
        CONFIG["model_name"],
        CONFIG["use_qlora"]
    )

    # 配置LoRA
    print("配置LoRA适配器...")
    model = setup_lora(model, CONFIG)

    # 加载数据
    print("加载训练数据...")
    dataset = load_and_format_data(CONFIG["data_path"], tokenizer)
    split = dataset.train_test_split(test_size=0.1, seed=42)

    train_dataset = tokenize_dataset(split['train'], tokenizer, CONFIG["max_seq_length"])
    eval_dataset = tokenize_dataset(split['test'], tokenizer, CONFIG["max_seq_length"])

    print(f"训练集: {len(train_dataset)} 条, 验证集: {len(eval_dataset)} 条")

    # 训练配置
    training_args = TrainingArguments(
        output_dir=CONFIG["output_dir"],
        num_train_epochs=CONFIG["num_epochs"],
        per_device_train_batch_size=CONFIG["batch_size"],
        gradient_accumulation_steps=CONFIG["gradient_accumulation"],
        learning_rate=CONFIG["learning_rate"],
        warmup_ratio=CONFIG["warmup_ratio"],
        lr_scheduler_type="cosine",
        weight_decay=0.01,
        logging_steps=10,
        save_strategy="steps",
        save_steps=200,
        evaluation_strategy="steps",
        eval_steps=200,
        save_total_limit=3,
        bf16=True,
        gradient_checkpointing=True,
        optim="paged_adamw_8bit",
        report_to="none",
        dataloader_num_workers=4,
    )

    # 创建Trainer并开始训练
    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True),
        tokenizer=tokenizer,
    )

    print("开始训练...")
    trainer.train()

    # 保存模型
    final_path = os.path.join(CONFIG["output_dir"], "final")
    trainer.save_model(final_path)
    tokenizer.save_pretrained(final_path)
    print(f"模型已保存到: {final_path}")

if __name__ == "__main__":
    main()

第三章:LoRA/QLoRA 低秩适配详解

3.1 LoRA 的原理

LoRA(Low-Rank Adaptation,低秩适配)是微软在2021年提出的一种参数高效微调方法。它的核心思想非常优雅:在模型的权重矩阵旁边添加一个低秩的"增量矩阵",只训练这个增量矩阵,而保持原始权重不变。

3.1.1 数学原理

假设原始模型的某个权重矩阵为 W ∈ R^(d×k),在传统微调中,我们会直接更新这个矩阵:W' = W + ΔW。

LoRA的关键洞察是:在微调过程中,权重的变化量 ΔW 实际上具有很低的"内在秩"(intrinsic rank)。因此,我们可以将 ΔW 分解为两个低秩矩阵的乘积:

ΔW = B × A

其中:
- A ∈ R^(r×k),r << min(d, k)
- B ∈ R^(d×r)
- r 是LoRA的秩(rank),通常取 4、8、16、32、64

这样,原本需要训练 d×k 个参数,现在只需要训练 (d×r + r×k) = r×(d+k) 个参数。当 r 远小于 d 和 k 时,参数量大幅减少。

举例说明

以一个 4096×4096 的权重矩阵为例:

  • 原始参数量:4096 × 4096 = 16,777,216(约1600万)
  • LoRA参数量(r=16):16 × (4096 + 4096) = 131,072(约13万)
  • 参数减少比例:约 128 倍

3.1.2 LoRA的前向传播

在前向传播时,LoRA的计算过程如下:

# 原始前向传播
# output = W @ x

# 添加LoRA后的前向传播
# output = W @ x + (B @ A) @ x * (alpha / r)

# 实际实现中,为了效率,通常这样计算:
# h = W @ x          # 原始输出
# h_lora = B @ (A @ x)  # LoRA增量
# output = h + h_lora * scaling  # 最终输出

其中 scaling = alpha / r 是一个缩放因子,用于控制LoRA的学习强度。

3.1.3 LoRA的初始化

LoRA矩阵的初始化方式对训练效果有重要影响:

  • A矩阵:使用随机高斯初始化
  • B矩阵:使用零初始化

这样在训练开始时,LoRA的输出为零,不会影响原始模型的行为。随着训练的进行,LoRA逐渐学习到有用的增量。

# LoRA初始化示例
import torch
import torch.nn as nn

class LoRALayer(nn.Module):
    def __init__(self, in_features, out_features, rank=16, alpha=32):
        super().__init__()
        self.rank = rank
        self.alpha = alpha
        self.scaling = alpha / rank

        # A矩阵:随机高斯初始化
        self.lora_A = nn.Parameter(torch.randn(rank, in_features) * 0.01)
        # B矩阵:零初始化
        self.lora_B = nn.Parameter(torch.zeros(out_features, rank))

    def forward(self, x):
        # 计算LoRA增量
        lora_output = (x @ self.lora_A.T @ self.lora_B.T) * self.scaling
        return lora_output

3.1.4 哪些层适合添加LoRA

在Transformer模型中,我们通常在注意力层和前馈网络层添加LoRA:

# 常见的LoRA目标模块
target_modules = [
    # 注意力层
    "q_proj",   # Query投影
    "k_proj",   # Key投影
    "v_proj",   # Value投影
    "o_proj",   # Output投影

    # 前馈网络层(可选)
    "gate_proj",  # 门控投影
    "up_proj",    # 上投影
    "down_proj",  # 下投影
]

# 只对注意力层添加LoRA(参数更少,训练更快)
target_modules_attention_only = ["q_proj", "v_proj"]

# 对所有线性层添加LoRA(效果通常最好)
target_modules_all = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"]

3.2 QLoRA:4-bit量化 + LoRA

QLoRA(Quantized LoRA)是LoRA的进一步优化版本,由华盛顿大学在2023年提出。它的核心创新是将基础模型量化为4-bit精度,然后在量化后的模型上应用LoRA。

3.2.1 QLoRA的关键技术

QLoRA引入了三项关键技术:

4-bit NormalFloat(NF4)量化:一种针对正态分布数据优化的4-bit数据类型。由于预训练模型的权重通常近似服从正态分布,NF4比传统的4-bit整数量化效果更好。

双重量化(Double Quantization):对量化常数本身也进行量化,进一步减少内存占用。具体来说,将FP32的量化常数量化为FP8,每个参数可以额外节省约0.37bit。

分页优化器(Paged Optimizers):使用NVIDIA的统一内存管理,在GPU显存不足时自动将优化器状态转移到CPU内存,避免OOM(Out of Memory)错误。

3.2.2 QLoRA的显存优势

显存对比(以7B模型为例):

全参数微调(FP16):
  模型参数:14 GB
  优化器状态:28 GB(Adam需要2倍参数量的额外空间)
  梯度:14 GB
  激活值:约20 GB
  总计:约76 GB

LoRA微调(FP16基础模型 + LoRA参数):
  模型参数:14 GB
  LoRA参数:约50 MB
  优化器状态:约100 MB
  激活值:约5 GB
  总计:约20 GB

QLoRA微调(4-bit基础模型 + LoRA参数):
  模型参数:约3.5 GB(4-bit量化)
  LoRA参数:约50 MB
  优化器状态:约100 MB
  激活值:约5 GB
  总计:约9 GB

从上面的对比可以看出,QLoRA将显存需求从76GB降低到了9GB,这意味着一张RTX 3060 12GB就可以微调7B模型!

3.3 LoRA 超参数详解

3.3.1 秩(Rank)

秩r是LoRA最重要的超参数,它决定了LoRA的表达能力。

# 不同rank的参数量对比(以4096维的权重矩阵为例)
for rank in [4, 8, 16, 32, 64, 128]:
    params = rank * (4096 + 4096)
    ratio = params / (4096 * 4096)
    print(f"rank={rank}: {params:,} 参数 ({ratio:.2%})")

输出:

rank=4:   32,768 参数 (0.20%)
rank=8:   65,536 参数 (0.39%)
rank=16:  131,072 参数 (0.78%)
rank=32:  262,144 参数 (1.56%)
rank=64:  524,288 参数 (3.12%)
rank=128: 1,048,576 参数 (6.25%)

rank选择建议

  • 简单任务(分类、情感分析):rank=4或8
  • 中等任务(问答、摘要):rank=16或32
  • 复杂任务(代码生成、数学推理):rank=32或64
  • 不确定时:从rank=16开始尝试

3.3.2 Alpha(缩放因子)

Alpha控制LoRA的学习强度。scaling = alpha / rank。

# 不同alpha对学习强度的影响
for alpha in [8, 16, 32, 64]:
    for rank in [8, 16, 32]:
        scaling = alpha / rank
        print(f"alpha={alpha}, rank={rank}: scaling={scaling:.2f}")

alpha选择建议

  • 通常设置为rank的2倍:alpha = 2 * rank
  • 如果训练不稳定,可以降低alpha
  • 如果学习太慢,可以增加alpha

3.3.3 目标模块(Target Modules)

# 配置1:只对Q和V添加LoRA(最少参数,最快训练)
lora_config_1 = LoraConfig(
    target_modules=["q_proj", "v_proj"],
    r=16,
    lora_alpha=32,
)

# 配置2:对所有注意力层添加LoRA(推荐起点)
lora_config_2 = LoraConfig(
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    r=16,
    lora_alpha=32,
)

# 配置3:对所有线性层添加LoRA(效果最好,参数最多)
lora_config_3 = LoraConfig(
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    r=16,
    lora_alpha=32,
)

3.4 LoRA 实战代码

3.4.1 完整的QLoRA训练代码

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

# 1. 加载分词器
model_name = "Qwen/Qwen2-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# 2. 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)

# 3. 加载量化模型
model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",
    trust_remote_code=True,
)

# 4. 准备模型用于k-bit训练
model = prepare_model_for_kbit_training(model)

# 5. LoRA配置
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

# 6. 应用LoRA
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出示例:
# trainable params: 41,943,040 || all params: 7,615,616,000 || trainable%: 0.5508

3.4.2 LoRA模型的保存与加载

# 保存LoRA权重(只保存增量部分,通常只有几十MB)
model.save_pretrained("./lora_weights")

# 加载LoRA权重
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(model_name, ...)
model = PeftModel.from_pretrained(base_model, "./lora_weights")

# 合并LoRA权重到基础模型(用于部署)
merged_model = model.merge_and_unload()
merged_model.save_pretrained("./merged_model")
tokenizer.save_pretrained("./merged_model")

3.4.3 多LoRA切换

PEFT支持在一个基础模型上加载多个LoRA适配器,并快速切换:

from peft import PeftModel

# 加载基础模型
base_model = AutoModelForCausalLM.from_pretrained(model_name, ...)

# 加载多个LoRA适配器
model = PeftModel.from_pretrained(base_model, "./lora_medical", adapter_name="medical")
model.load_adapter("./lora_legal", adapter_name="legal")
model.load_adapter("./lora_code", adapter_name="code")

# 切换到医疗领域适配器
model.set_adapter("medical")
output = model.generate(...)

# 切换到法律领域适配器
model.set_adapter("legal")
output = model.generate(...)

# 合并所有适配器
model.set_adapter(["medical", "legal", "code"])

3.5 LoRA 最佳实践

3.5.1 选择合适的rank

# 通过实验确定最佳rank
ranks_to_try = [4, 8, 16, 32, 64]
results = {}

for rank in ranks_to_try:
    lora_config = LoraConfig(r=rank, lora_alpha=2*rank, ...)
    # 训练并评估...
    results[rank] = eval_score

# 绘制rank vs 效果曲线
import matplotlib.pyplot as plt
plt.plot(list(results.keys()), list(results.values()), marker='o')
plt.xlabel('LoRA Rank')
plt.ylabel('Evaluation Score')
plt.title('LoRA Rank vs Model Performance')
plt.savefig('rank_comparison.png')

3.5.2 学习率设置

LoRA的学习率通常比全参数微调高一个数量级:

# 全参数微调的学习率
full_ft_lr = 2e-5

# LoRA微调的学习率(推荐范围)
lora_lr_range = (1e-4, 3e-4)

# QLoRA微调的学习率
qlora_lr_range = (1e-4, 2e-4)

3.5.3 避免常见错误

# ❌ 错误:rank设置过大
lora_config = LoraConfig(r=256, ...)  # 参数量接近全参数微调

# ❌ 错误:alpha设置过小
lora_config = LoraConfig(r=16, lora_alpha=4, ...)  # scaling=0.25,学习太慢

# ❌ 错误:target_modules选择不当
lora_config = LoraConfig(target_modules=["embed_tokens"], ...)  # 嵌入层通常不适合

# ✅ 正确:合理的LoRA配置
lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

3.5.4 LoRA的合并与推理优化

训练完成后,可以将LoRA权重合并到基础模型中,这样推理时不会有额外的计算开销:

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

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

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

# 合并权重
merged_model = model.merge_and_unload()

# 保存合并后的模型
merged_model.save_pretrained("./merged_model")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2-7B-Instruct")
tokenizer.save_pretrained("./merged_model")

# 使用合并后的模型进行推理(与原始模型速度相同)
from transformers import pipeline
pipe = pipeline("text-generation", model="./merged_model", tokenizer="./merged_model")
result = pipe("什么是机器学习?", max_new_tokens=200)
print(result[0]['generated_text'])

第四章:数据工程

4.1 数据收集

数据是微调的基础,高质量的训练数据是成功微调的关键。本章将详细介绍如何收集、清洗、格式化和评估训练数据。

4.1.1 数据来源

公开数据集

# 使用Hugging Face Datasets加载公开数据集
from datasets import load_dataset

# Alpaca数据集(英文指令数据)
alpaca = load_dataset("tatsu-lab/alpaca")

# BELLE中文指令数据
belle = load_dataset("BelleGroup/train_1M_CN")

# OpenAssistant对话数据
oasst = load_dataset("OpenAssistant/oasst2")

# 中文对话数据集
# Firefly中文数据集
firefly = load_dataset("YeungNLP/firefly-train-1.1M")

自建数据

# 使用GPT-4等强模型生成数据(Self-Instruct方法)
import openai

def generate_training_data(seed_examples, num_samples=100):
    """使用GPT-4生成训练数据"""
    generated_data = []

    for i in range(num_samples):
        # 随机选择种子示例作为参考
        seed = random.choice(seed_examples)

        prompt = f"""请根据以下示例,生成一个新的指令-输入-输出三元组。
要求:指令应该多样化,输出应该准确、详细。

示例:
指令:{seed['instruction']}
输入:{seed.get('input', '无')}
输出:{seed['output']}

请生成一个新的三元组,以JSON格式输出:"""

        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            temperature=0.8,
        )

        # 解析生成的数据
        new_data = json.loads(response.choices[0].message.content)
        generated_data.append(new_data)

    return generated_data

人工标注

对于高要求的场景,人工标注仍然是最可靠的方式。建议:

  1. 制定详细的标注指南
  2. 使用多人标注并计算一致性
  3. 定期进行质量审核
  4. 使用标注工具(如Label Studio)提高效率

4.1.2 数据收集最佳实践

# 数据收集清单
data_collection_checklist = {
    "多样性": "确保指令类型、难度、领域分布均衡",
    "质量": "输出应该准确、完整、格式规范",
    "一致性": "相同类型的指令应该有一致的回答风格",
    "覆盖度": "覆盖目标任务的主要场景和边界情况",
    "时效性": "确保数据中的信息是最新和准确的",
    "合规性": "确保数据不包含敏感信息或违规内容",
}

4.2 数据清洗

数据清洗是数据工程中最耗时但最重要的环节。

4.2.1 基础清洗

import re
import json
from typing import List, Dict

def basic_clean(text: str) -> str:
    """基础文本清洗"""
    # 1. 去除首尾空白
    text = text.strip()

    # 2. 规范化空白字符
    text = re.sub(r'\s+', ' ', text)

    # 3. 去除特殊控制字符
    text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)

    # 4. 规范化标点符号(中英文标点转换可根据需要调整)
    text = text.replace(',', ',').replace('。', '。')

    # 5. 去除多余换行
    text = re.sub(r'\n{3,}', '\n\n', text)

    return text

def clean_dataset(data: List[Dict]) -> List[Dict]:
    """清洗整个数据集"""
    cleaned = []
    for item in data:
        # 清洗各字段
        cleaned_item = {
            'instruction': basic_clean(item.get('instruction', '')),
            'input': basic_clean(item.get('input', '')),
            'output': basic_clean(item.get('output', '')),
        }

        # 跳过空数据
        if not cleaned_item['instruction'] or not cleaned_item['output']:
            continue

        # 跳过过短的数据
        if len(cleaned_item['output']) < 10:
            continue

        # 跳过过长的数据(可能是垃圾数据)
        if len(cleaned_item['output']) > 10000:
            continue

        cleaned.append(cleaned_item)

    return cleaned

4.2.2 去重

from hashlib import md5

def deduplicate(data: List[Dict]) -> List[Dict]:
    """数据去重"""
    seen = set()
    unique_data = []

    for item in data:
        # 基于指令和输出计算哈希
        key = md5(
            (item['instruction'] + item['output']).encode()
        ).hexdigest()

        if key not in seen:
            seen.add(key)
            unique_data.append(item)

    print(f"去重前: {len(data)} 条, 去重后: {len(unique_data)} 条")
    return unique_data

def semantic_deduplicate(data: List[Dict], threshold=0.95) -> List[Dict]:
    """基于语义相似度去重(需要sentence-transformers)"""
    from sentence_transformers import SentenceTransformer
    from sklearn.metrics.pairwise import cosine_similarity
    import numpy as np

    model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')

    # 计算所有输出的嵌入向量
    outputs = [item['output'] for item in data]
    embeddings = model.encode(outputs, show_progress_bar=True)

    # 计算相似度矩阵
    sim_matrix = cosine_similarity(embeddings)

    # 标记需要保留的索引
    keep = [True] * len(data)
    for i in range(len(data)):
        if not keep[i]:
            continue
        for j in range(i + 1, len(data)):
            if sim_matrix[i][j] > threshold:
                keep[j] = False

    unique_data = [item for item, k in zip(data, keep) if k]
    print(f"语义去重前: {len(data)} 条, 去重后: {len(unique_data)} 条")
    return unique_data

4.2.3 质量过滤

def quality_filter(data: List[Dict]) -> List[Dict]:
    """基于规则的质量过滤"""
    filtered = []

    for item in data:
        instruction = item['instruction']
        output = item['output']

        # 1. 长度过滤
        if len(output) < 20 or len(output) > 5000:
            continue

        # 2. 语言一致性检查(中文任务应该主要是中文)
        chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', output))
        total_chars = len(output)
        if total_chars > 0 and chinese_chars / total_chars < 0.3:
            # 如果是中文任务,跳过中文比例过低的数据
            continue

        # 3. 重复内容检查
        sentences = output.split('。')
        if len(sentences) > 3:
            unique_sentences = set(sentences)
            if len(unique_sentences) / len(sentences) < 0.5:
                continue  # 大量重复句子

        # 4. 特殊模式过滤
        # 过滤包含"作为一个AI语言模型"等模板化回答
        bad_patterns = [
            r'作为(?:一个)?(?:AI|人工智能)',
            r'我(?:是|作为)(?:一个)?(?:AI|语言模型|助手)',
            r'我没有(?:个人|自己的)(?:观点|意见|想法)',
        ]
        if any(re.search(p, output) for p in bad_patterns):
            continue

        filtered.append(item)

    print(f"质量过滤前: {len(data)} 条, 过滤后: {len(filtered)} 条")
    return filtered

4.3 数据格式化

不同的微调框架需要不同格式的数据。以下是常见的数据格式转换:

def convert_to_alpaca_format(data: List[Dict]) -> List[Dict]:
    """转换为Alpaca格式"""
    return [
        {
            "instruction": item['instruction'],
            "input": item.get('input', ''),
            "output": item['output'],
        }
        for item in data
    ]

def convert_to_sharegpt_format(data: List[Dict]) -> List[Dict]:
    """转换为ShareGPT格式"""
    result = []
    for item in data:
        conversation = {
            "conversations": [
                {"from": "human", "value": item['instruction'] + (('\n' + item['input']) if item.get('input') else '')},
                {"from": "gpt", "value": item['output']},
            ]
        }
        result.append(conversation)
    return result

def convert_to_chatml_format(data: List[Dict], system_prompt: str = "") -> List[Dict]:
    """转换为ChatML格式(适用于Qwen等模型)"""
    result = []
    for item in data:
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})

        user_content = item['instruction']
        if item.get('input'):
            user_content += '\n' + item['input']

        messages.append({"role": "user", "content": user_content})
        messages.append({"role": "assistant", "content": item['output']})

        result.append({"messages": messages})
    return result

4.4 数据质量评估

4.4.1 自动评估指标

def compute_data_statistics(data: List[Dict]) -> Dict:
    """计算数据集统计信息"""
    stats = {
        "total_samples": len(data),
        "instruction_lengths": [],
        "output_lengths": [],
        "instruction_types": {},
    }

    for item in data:
        stats["instruction_lengths"].append(len(item['instruction']))
        stats["output_lengths"].append(len(item['output']))

        # 简单的指令类型分类
        instruction = item['instruction'].lower()
        if any(w in instruction for w in ['翻译', 'translate']):
            stats["instruction_types"]["翻译"] = stats["instruction_types"].get("翻译", 0) + 1
        elif any(w in instruction for w in ['总结', '摘要', 'summarize']):
            stats["instruction_types"]["总结"] = stats["instruction_types"].get("总结", 0) + 1
        elif any(w in instruction for w in ['解释', '什么是', '什么是']):
            stats["instruction_types"]["知识问答"] = stats["instruction_types"].get("知识问答", 0) + 1
        else:
            stats["instruction_types"]["其他"] = stats["instruction_types"].get("其他", 0) + 1

    # 打印统计信息
    print(f"数据总量: {stats['total_samples']}")
    print(f"指令长度 - 平均: {sum(stats['instruction_lengths'])/len(stats['instruction_lengths']):.0f}, "
          f"最大: {max(stats['instruction_lengths'])}, 最小: {min(stats['instruction_lengths'])}")
    print(f"输出长度 - 平均: {sum(stats['output_lengths'])/len(stats['output_lengths']):.0f}, "
          f"最大: {max(stats['output_lengths'])}, 最小: {min(stats['output_lengths'])}")
    print(f"指令类型分布: {stats['instruction_types']}")

    return stats

4.4.2 使用LLM进行质量评估

def evaluate_with_llm(data: List[Dict], sample_size: int = 100) -> Dict:
    """使用LLM评估数据质量"""
    import random
    import openai

    samples = random.sample(data, min(sample_size, len(data)))

    scores = {"accuracy": [], "completeness": [], "clarity": [], "relevance": []}

    for item in samples:
        prompt = f"""请评估以下指令-回答对的质量,从1-5分打分(5分最高)。

指令:{item['instruction']}
回答:{item['output']}

请以JSON格式输出评分:
{{"accuracy": 分数, "completeness": 分数, "clarity": 分数, "relevance": 分数}}"""

        response = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
        )

        try:
            rating = json.loads(response.choices[0].message.content)
            for key in scores:
                scores[key].append(rating.get(key, 3))
        except:
            continue

    # 计算平均分
    avg_scores = {key: sum(vals)/len(vals) if vals else 0
                  for key, vals in scores.items()}

    print("数据质量评估结果:")
    for key, score in avg_scores.items():
        print(f"  {key}: {score:.2f}/5")

    return avg_scores

4.5 数据增强

当训练数据不足时,数据增强是一种有效的扩充方法。

4.5.1 基于LLM的数据增强

def augment_with_llm(item: Dict, num_augments: int = 3) -> List[Dict]:
    """使用LLM增强单条数据"""
    import openai

    prompt = f"""基于以下指令-回答对,生成{num_augments}个语义相似但表达不同的新数据对。
保持回答的质量和准确性,但使用不同的措辞和角度。

原始数据:
指令:{item['instruction']}
回答:{item['output']}

请以JSON数组格式输出,每个元素包含instruction和output字段:"""

    response = openai.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0.8,
    )

    try:
        augmented = json.loads(response.choices[0].message.content)
        return augmented
    except:
        return []

4.5.2 回译增强

def back_translation_augment(text: str, mid_lang: str = "en") -> str:
    """通过回译进行数据增强"""
    from transformers import MarianMTModel, MarianTokenizer

    # 中文 -> 英文
    zh_en_model = MarianTokenizer.from_pretrained('Helsinki-NLP/opus-mt-zh-en')
    zh_en_tokenizer = MarianTokenizer.from_pretrained('Helsinki-NLP/opus-mt-zh-en')

    # 英文 -> 中文
    en_zh_model = MarianMTModel.from_pretrained('Helsinki-NLP/opus-mt-en-zh')
    en_zh_tokenizer = MarianTokenizer.from_pretrained('Helsinki-NLP/opus-mt-en-zh')

    # 中文 -> 英文
    inputs = zh_en_tokenizer(text, return_tensors="pt", padding=True)
    translated = zh_en_model.generate(**inputs)
    english_text = zh_en_tokenizer.decode(translated[0], skip_special_tokens=True)

    # 英文 -> 中文
    inputs = en_zh_tokenizer(english_text, return_tensors="pt", padding=True)
    translated = en_zh_model.generate(**inputs)
    back_translated = en_zh_tokenizer.decode(translated[0], skip_special_tokens=True)

    return back_translated

4.5.3 同义词替换增强

import random
import jieba
import jieba.posseg as pseg

# 同义词词典(示例,实际使用时需要更完整的词典)
synonyms = {
    "优秀": ["出色", "卓越", "杰出", "优良"],
    "研究": ["探究", "研讨", "分析", "探讨"],
    "方法": ["方式", "途径", "手段", "办法"],
    "重要": ["关键", "核心", "主要", "关键性"],
    "发展": ["进步", "演进", "进化", "推进"],
}

def synonym_replace_augment(text: str, replace_ratio: float = 0.2) -> str:
    """基于同义词替换的数据增强"""
    words = list(pseg.cut(text))
    num_to_replace = max(1, int(len(words) * replace_ratio))

    # 找到可以替换的词
    replaceable = [(i, word.word) for i, word in enumerate(words)
                   if word.word in synonyms]

    if not replaceable:
        return text

    # 随机选择要替换的词
    to_replace = random.sample(replaceable, min(num_to_replace, len(replaceable)))

    for idx, original in to_replace:
        replacement = random.choice(synonyms[original])
        words[idx] = type(words[idx])(replacement, words[idx].flag)

    return ''.join([word.word for word in words])

4.6 数据配比策略

当使用多种类型的数据进行微调时,数据配比非常重要。

def create_balanced_dataset(datasets: Dict[str, List], strategy: str = "proportional") -> List[Dict]:
    """创建均衡的混合数据集"""
    if strategy == "proportional":
        # 按比例混合
        total = sum(len(data) for data in datasets.values())
        mixed = []
        for name, data in datasets.items():
            ratio = len(data) / total
            sample_size = int(len(data) * ratio * total / len(data))
            mixed.extend(random.sample(data, min(sample_size, len(data))))

    elif strategy == "equal":
        # 等量混合(每种类型取相同数量)
        min_size = min(len(data) for data in datasets.values())
        mixed = []
        for name, data in datasets.items():
            mixed.extend(random.sample(data, min_size))

    elif strategy == "upsample":
        # 上采样少数类别
        max_size = max(len(data) for data in datasets.values())
        mixed = []
        for name, data in datasets.items():
            if len(data) < max_size:
                # 上采样到最大类别数量
                upsampled = data * (max_size // len(data)) + random.sample(data, max_size % len(data))
                mixed.extend(upsampled)
            else:
                mixed.extend(random.sample(data, max_size))

    random.shuffle(mixed)
    return mixed

# 使用示例
datasets = {
    "问答": qa_data,         # 5000条
    "翻译": translation_data, # 2000条
    "摘要": summary_data,     # 1000条
    "代码": code_data,        # 500条
}

# 等量混合
balanced_data = create_balanced_dataset(datasets, strategy="equal")

第五章:主流微调框架对比

5.1 框架概览

目前主流的大模型微调框架主要有以下几个:

框架 特点 适用场景 GitHub Stars
LLaMA-Factory 功能全面,界面友好 入门用户、快速实验 30k+
Unsloth 极致速度优化 追求训练速度 20k+
Axolotl 配置灵活,功能丰富 高级用户、研究 7k+
OpenRLHF 专注RLHF训练 RLHF/DPO研究 5k+
Swift (ModelScope) 阿里出品,中文支持好 中文场景 4k+

5.2 LLaMA-Factory

LLaMA-Factory是目前最流行的一站式大模型微调框架,由国内开发者维护。

核心优势

  • Web UI界面,零代码微调
  • 支持100+种模型
  • 支持多种微调方法(全参、LoRA、QLoRA等)
  • 内置数据集管理
  • 支持分布式训练
  • 中文文档完善

支持的模型

  • LLaMA / LLaMA-2 / LLaMA-3
  • Qwen / Qwen-2
  • ChatGLM
  • Baichuan
  • Yi
  • DeepSeek
  • Mistral / Mixtral
  • Phi-3
  • 等等

安装方法

# 克隆仓库
git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory

# 安装依赖
pip install -e ".[torch,metrics]"

基本使用

# 使用Web UI
llamafactory-cli webui

# 使用命令行训练
llamafactory-cli train \
    --model_name_or_path Qwen/Qwen2-7B-Instruct \
    --dataset alpaca_zh \
    --template qwen \
    --finetuning_type lora \
    --lora_rank 16 \
    --output_dir ./output

5.3 Unsloth

Unsloth专注于训练速度优化,号称可以实现2-5倍的训练加速。

核心优势

  • 训练速度提升2-5倍
  • 显存减少60-80%
  • 支持2倍长的上下文
  • 手写CUDA内核优化
  • 免费开源版本可用

安装方法

pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"

基本使用

from unsloth import FastLanguageModel

# 加载模型(自动优化)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2-7B-Instruct",
    max_seq_length=2048,
    load_in_4bit=True,
)

# 添加LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    use_gradient_checkpointing="unsloth",
)

5.4 Axolotl

Axolotl是一个高度可配置的微调框架,适合高级用户和研究人员。

核心优势

  • YAML配置文件驱动
  • 支持多种微调方法
  • 灵活的数据处理管道
  • 支持多种模型架构
  • 活跃的社区

安装方法

git clone https://github.com/OpenAccess-AI-Collective/axolotl
cd axolotl
pip install -e ".[flash-attn]"

配置文件示例

# config.yml
base_model: Qwen/Qwen2-7B-Instruct
model_type: AutoModelForCausalLM
tokenizer_type: AutoTokenizer

# 训练参数
micro_batch_size: 2
gradient_accumulation_steps: 8
num_epochs: 3
learning_rate: 2e-5
lr_scheduler: cosine
warmup_ratio: 0.1

# LoRA配置
adapter: lora
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules:
  - q_proj
  - k_proj
  - v_proj
  - o_proj

# 数据配置
datasets:
  - path: data/train.json
    type: alpaca

# 输出配置
output_dir: ./output

运行训练

accelerate launch -m axolotl.cli.train config.yml

5.5 OpenRLHF

OpenRLHF是一个专注于RLHF训练的框架。

核心优势

  • 完整的RLHF训练流程
  • 支持PPO、DPO、ORPO等算法
  • 分布式训练支持
  • 奖励模型训练

安装方法

pip install openrlhf

基本使用

# DPO训练示例
from openrlhf.trainer import DPOTrainer

trainer = DPOTrainer(
    model=model,
    ref_model=ref_model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=training_args,
)
trainer.train()

5.6 框架选择建议

def recommend_framework(user_profile: str) -> str:
    """根据用户情况推荐框架"""
    recommendations = {
        "初学者": "LLaMA-Factory - Web UI友好,文档完善,上手最快",
        "追求速度": "Unsloth - 训练速度最快,显存优化最好",
        "研究人员": "Axolotl - 配置灵活,支持最新技术",
        "RLHF研究": "OpenRLHF - 专注RLHF,功能完整",
        "中文场景": "LLaMA-Factory 或 Swift - 中文支持最好",
        "企业部署": "LLaMA-Factory - 功能全面,社区活跃",
    }
    return recommendations.get(user_profile, "LLaMA-Factory - 最全面的选择")

第六章:LLaMA-Factory 实战

6.1 安装与环境配置

6.1.1 环境准备

# 创建conda环境
conda create -n llama-factory python=3.10 -y
conda activate llama-factory

# 安装PyTorch(CUDA 12.1版本)
conda install pytorch==2.1.2 torchvision==0.16.2 torchaudio==2.1.2 pytorch-cuda=12.1 -c pytorch -c nvidia

# 克隆并安装LLaMA-Factory
git clone --depth 1 https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -e ".[torch,metrics]"

# 验证安装
llamafactory-cli version

6.1.2 模型下载

# 方法1:使用Hugging Face Hub(需要网络)
huggingface-cli download Qwen/Qwen2-7B-Instruct --local-dir ./models/Qwen2-7B-Instruct

# 方法2:使用ModelScope(国内推荐)
pip install modelscope
modelscope download --model Qwen/Qwen2-7B-Instruct --local_dir ./models/Qwen2-7B-Instruct

# 方法3:使用Git LFS
git lfs install
git clone https://huggingface.co/Qwen/Qwen2-7B-Instruct ./models/Qwen2-7B-Instruct

6.2 数据准备

6.2.1 LLaMA-Factory数据格式

LLaMA-Factory支持多种数据格式,最常用的是Alpaca和ShareGPT格式。

Alpaca格式示例data/custom_train.json):

[
  {
    "instruction": "请解释什么是深度学习",
    "input": "",
    "output": "深度学习是机器学习的一个分支,它使用多层神经网络来学习数据的层次化表示。通过多层非线性变换,深度学习模型能够自动学习数据中的复杂模式和特征。"
  },
  {
    "instruction": "将以下英文翻译成中文",
    "input": "Machine learning is a subset of artificial intelligence.",
    "output": "机器学习是人工智能的一个子集。"
  }
]

ShareGPT格式示例data/custom_chat.json):

[
  {
    "conversations": [
      {"from": "human", "value": "你好,请介绍一下自己"},
      {"from": "gpt", "value": "你好!我是一个AI助手,可以帮助你解答问题、进行对话交流、协助处理各种任务。有什么我可以帮助你的吗?"}
    ]
  }
]

6.2.2 注册自定义数据集

data/dataset_info.json 中注册你的数据集:

{
  "custom_train": {
    "file_name": "custom_train.json",
    "columns": {
      "prompt": "instruction",
      "query": "input",
      "response": "output"
    }
  },
  "custom_chat": {
    "file_name": "custom_chat.json",
    "formatting": "sharegpt",
    "columns": {
      "messages": "conversations"
    }
  }
}

6.3 Web UI 使用

6.3.1 启动Web UI

# 启动Web界面
llamafactory-cli webui

# 如果需要指定端口和主机
GRADIO_SERVER_NAME=0.0.0.0 GRADIO_SERVER_PORT=7860 llamafactory-cli webui

启动后在浏览器中访问 http://localhost:7860 即可看到Web界面。

6.3.2 Web UI 操作步骤

步骤1:选择模型

在"模型名称"下拉框中选择预训练模型,或者输入本地模型路径。支持的模型会自动显示在列表中。

步骤2:选择微调方法

  • full:全参数微调
  • freeze:冻结部分参数
  • lora:LoRA微调(推荐)

步骤3:配置LoRA参数

  • LoRA秩(Rank):推荐16
  • LoRA缩放因子(Alpha):推荐32
  • LoRA随机失活(Dropout):推荐0.05

步骤4:选择数据集

在数据集列表中选择要使用的训练数据。可以多选。

步骤5:配置训练参数

  • 训练轮数(Epochs):3
  • 批次大小(Batch Size):根据显存调整
  • 学习率(Learning Rate):2e-5
  • 最大序列长度:2048

步骤6:开始训练

点击"开始"按钮开始训练。训练过程中可以查看损失曲线和训练进度。

6.4 命令行训练

6.4.1 LoRA训练命令

llamafactory-cli train \
    --model_name_or_path ./models/Qwen2-7B-Instruct \
    --stage sft \
    --do_train \
    --dataset custom_train \
    --template qwen \
    --finetuning_type lora \
    --lora_rank 16 \
    --lora_alpha 32 \
    --lora_dropout 0.05 \
    --lora_target all \
    --output_dir ./output/qwen2-7b-lora \
    --per_device_train_batch_size 2 \
    --gradient_accumulation_steps 8 \
    --num_train_epochs 3 \
    --learning_rate 2e-5 \
    --lr_scheduler_type cosine \
    --warmup_ratio 0.1 \
    --max_seq_length 2048 \
    --logging_steps 10 \
    --save_steps 200 \
    --eval_steps 200 \
    --evaluation_strategy steps \
    --load_best_model_at_end \
    --bf16 \
    --gradient_checkpointing \
    --seed 42

6.4.2 QLoRA训练命令

llamafactory-cli train \
    --model_name_or_path ./models/Qwen2-7B-Instruct \
    --stage sft \
    --do_train \
    --dataset custom_train \
    --template qwen \
    --finetuning_type lora \
    --quantization_bit 4 \
    --quantization_method bitsandbytes \
    --lora_rank 16 \
    --lora_alpha 32 \
    --lora_dropout 0.05 \
    --lora_target all \
    --output_dir ./output/qwen2-7b-qlora \
    --per_device_train_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --num_train_epochs 3 \
    --learning_rate 1e-4 \
    --lr_scheduler_type cosine \
    --warmup_ratio 0.1 \
    --max_seq_length 2048 \
    --logging_steps 10 \
    --save_steps 200 \
    --bf16 \
    --gradient_checkpointing

6.4.3 全参数微调命令

llamafactory-cli train \
    --model_name_or_path ./models/Qwen2-7B-Instruct \
    --stage sft \
    --do_train \
    --dataset custom_train \
    --template qwen \
    --finetuning_type full \
    --output_dir ./output/qwen2-7b-full \
    --per_device_train_batch_size 1 \
    --gradient_accumulation_steps 16 \
    --num_train_epochs 3 \
    --learning_rate 2e-5 \
    --lr_scheduler_type cosine \
    --warmup_ratio 0.1 \
    --max_seq_length 2048 \
    --logging_steps 10 \
    --save_steps 200 \
    --bf16 \
    --gradient_checkpointing

6.5 模型推理与评估

6.5.1 使用CLI进行推理

# 交互式推理
llamafactory-cli chat \
    --model_name_or_path ./models/Qwen2-7B-Instruct \
    --adapter_name_or_path ./output/qwen2-7b-lora \
    --template qwen \
    --finetuning_type lora

# 批量推理
llamafactory-cli export \
    --model_name_or_path ./models/Qwen2-7B-Instruct \
    --adapter_name_or_path ./output/qwen2-7b-lora \
    --template qwen \
    --finetuning_type lora \
    --export_dir ./merged_model

6.5.2 模型评估

llamafactory-cli eval \
    --model_name_or_path ./output/qwen2-7b-lora \
    --template qwen \
    --task mmlu \
    --split test \
    --lang en \
    --n_shot 5

6.6 常用配置模板

6.6.1 Qwen2-7B QLoRA微调模板

# qwen2_7b_qlora.yaml
model_name_or_path: ./models/Qwen2-7B-Instruct
stage: sft
do_train: true
finetuning_type: lora
quantization_bit: 4
quantization_method: bitsandbytes
lora_rank: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target: all
template: qwen
dataset: custom_train
max_seq_length: 2048
per_device_train_batch_size: 4
gradient_accumulation_steps: 4
num_train_epochs: 3
learning_rate: 1e-4
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
gradient_checkpointing: true
output_dir: ./output/qwen2-7b-qlora
logging_steps: 10
save_steps: 200

6.6.2 使用配置文件训练

llamafactory-cli train qwen2_7b_qlora.yaml

第七章:Unsloth 极速微调教程

7.1 Unsloth 加速原理

Unsloth是一个专注于训练速度优化的微调框架。它通过以下技术实现2-5倍的训练加速:

7.1.1 手写CUDA内核

Unsloth的核心优化来自于手写的CUDA内核,针对Transformer架构的关键计算进行了深度优化:

  • RoPE位置编码优化:将RoPE计算融合到注意力计算中,减少内存访问
  • 注意力计算优化:优化Flash Attention的实现,减少计算冗余
  • 交叉熵损失优化:融合损失计算,减少中间结果的内存占用
  • RMSNorm优化:优化归一化层的计算

7.1.2 梯度检查点优化

Unsloth实现了智能的梯度检查点策略,在显存和计算之间取得更好的平衡:

# Unsloth的梯度检查点优化
# 传统方法:保存所有激活值,显存占用大
# Unsloth:只保存关键层的激活值,其他层在反向传播时重新计算

from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2-7B-Instruct",
    max_seq_length=2048,
    load_in_4bit=True,
)

# 使用Unsloth优化的梯度检查点
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    use_gradient_checkpointing="unsloth",  # 关键参数
)

7.1.3 内存优化

Unsloth通过以下方式减少显存占用:

  • 激活值重计算:在反向传播时重新计算部分激活值,而不是保存
  • 优化器状态优化:减少优化器状态的显存占用
  • 临时缓冲区复用:复用计算过程中的临时缓冲区

7.2 Unsloth 安装

# 方法1:pip安装(推荐)
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"

# 方法2:conda安装
conda install -c conda-forge unsloth

# 方法3:源码安装
git clone https://github.com/unslothai/unsloth.git
cd unsloth
pip install -e .

7.3 Unsloth 基础使用

7.3.1 加载模型

from unsloth import FastLanguageModel
import torch

# 模型配置
max_seq_length = 2048
dtype = None  # 自动检测
load_in_4bit = True  # 使用4-bit量化

# 加载模型和分词器
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2-7B-Instruct",
    max_seq_length=max_seq_length,
    dtype=dtype,
    load_in_4bit=load_in_4bit,
    # token="hf_...",  # 如果需要访问gated模型
)

print(f"模型加载完成,显存占用: {torch.cuda.memory_allocated()/1024**3:.2f} GB")

7.3.2 配置LoRA

# 添加LoRA适配器
model = FastLanguageModel.get_peft_model(
    model,
    r=16,  # LoRA秩
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_alpha=16,
    lora_dropout=0,  # Unsloth建议设为0
    bias="none",
    use_gradient_checkpointing="unsloth",
    random_state=3407,
)

# 打印可训练参数信息
model.print_trainable_parameters()
# 输出示例:
# trainable params: 41,943,040 || all params: 7,615,616,000 || trainable%: 0.5508

7.3.3 准备数据集

from datasets import load_dataset

# 加载数据集
dataset = load_dataset("json", data_files="train_data.json", split="train")

# 定义格式化函数
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs = examples["input"]
    outputs = examples["output"]
    texts = []
    for instruction, input_text, output in zip(instructions, inputs, outputs):
        text = alpaca_prompt.format(instruction, input_text, output) + tokenizer.eos_token
        texts.append(text)
    return {"text": texts}

# 应用格式化
dataset = dataset.map(formatting_prompts_func, batched=True)

7.3.4 训练配置与执行

from trl import SFTTrainer
from transformers import TrainingArguments

# 配置训练参数
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=max_seq_length,
    dataset_num_proc=2,
    packing=False,
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        warmup_steps=5,
        num_train_epochs=3,
        learning_rate=2e-4,
        fp16=not torch.cuda.is_bf16_supported(),
        bf16=torch.cuda.is_bf16_supported(),
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.01,
        lr_scheduler_type="linear",
        seed=3407,
        output_dir="outputs",
    ),
)

# 开始训练
trainer_stats = trainer.train()

# 打印训练统计
print(f"训练时间: {trainer_stats.metrics['train_runtime']:.2f} 秒")
print(f"每秒处理样本数: {trainer_stats.metrics['train_samples_per_second']:.2f}")

7.4 Unsloth 高级功能

7.4.1 推理加速

# 启用推理模式(2倍推理加速)
FastLanguageModel.for_inference(model)

# 推理示例
inputs = tokenizer(
    [alpaca_prompt.format("请解释什么是量子计算", "", "")],
    return_tensors="pt"
).to("cuda")

outputs = model.generate(
    **inputs,
    max_new_tokens=256,
    use_cache=True,
)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

7.4.2 保存模型

# 方法1:保存LoRA权重
model.save_pretrained("lora_model")

# 方法2:保存为合并后的16-bit模型
model.save_pretrained_merged(
    "merged_model_16bit",
    tokenizer,
    save_method="merged_16bit",
)

# 方法3:保存为4-bit量化模型
model.save_pretrained_merged(
    "merged_model_4bit",
    tokenizer,
    save_method="merged_4bit_forced",
)

# 方法4:保存为GGUF格式(用于llama.cpp)
model.save_pretrained_gguf(
    "model_gguf",
    tokenizer,
    quantization_method="q4_k_m",  # 量化方法
)

7.4.3 量化方法对比

# GGUF量化方法选项
quantization_methods = {
    "q2_k": "2-bit量化,最小体积,质量损失较大",
    "q3_k_m": "3-bit量化,体积较小,质量中等",
    "q4_0": "4-bit量化,经典方法",
    "q4_k_m": "4-bit量化,推荐方法,平衡体积和质量",
    "q5_k_m": "5-bit量化,质量较好",
    "q6_k": "6-bit量化,高质量",
    "q8_0": "8-bit量化,最高质量,体积较大",
    "f16": "16-bit浮点,无损,体积最大",
}

# 推荐:q4_k_m 是最佳平衡点
model.save_pretrained_gguf("model_gguf", tokenizer, quantization_method="q4_k_m")

7.5 Unsloth vs 其他框架性能对比

性能对比(Qwen2-7B,单卡RTX 4090,QLoRA):

框架           | 训练速度(tokens/s) | 显存占用(GB) | 最大序列长度
---------------|-------------------|-------------|------------
Transformers   | 2,500             | 18          | 2048
LLaMA-Factory  | 2,800             | 16          | 2048
Unsloth        | 5,200             | 10          | 4096

Unsloth加速比:约 2x
Unsloth显存节省:约 40%

7.6 Unsloth 常见问题

Q1:Unsloth支持哪些模型?

# Unsloth官方支持的模型
supported_models = [
    "Llama-3", "Llama-2",
    "Mistral", "Mixtral",
    "Qwen2", "Qwen2.5",
    "Gemma", "Gemma2",
    "Phi-3",
    "Yi",
    "DeepSeek",
    "CodeLlama",
    # 其他基于上述架构的模型也通常支持
]

Q2:训练时出现OOM怎么办?

# 减少批次大小
per_device_train_batch_size = 1

# 增加梯度累积
gradient_accumulation_steps = 8

# 减少序列长度
max_seq_length = 1024

# 使用更激进的量化
load_in_4bit = True

# 启用packing(将多个短样本打包到一个序列中)
packing = True

第八章:RLHF 人类反馈强化学习

8.1 RLHF 概述

RLHF(Reinforcement Learning from Human Feedback)是一种通过人类反馈来优化大模型行为的技术。它解决了纯监督学习的一个核心问题:如何定义"好"的回答?

在SFT中,我们通过"问题-标准答案"对来训练模型。但在实际场景中,很多问题没有唯一的"标准答案",不同回答之间的优劣是主观的、相对的。RLHF通过让人类比较不同回答的好坏,训练一个奖励模型,然后用强化学习来优化模型。

8.1.1 RLHF的三个阶段

阶段一:监督微调(SFT)

使用高质量的指令-回答数据对预训练模型进行微调,使模型具备基本的指令遵循能力。这一步我们在前面的章节已经详细介绍过。

阶段二:奖励模型训练(Reward Model Training)

收集人类对模型输出的偏好数据(如"A回答比B回答好"),训练一个奖励模型来预测人类的偏好分数。

阶段三:强化学习优化(RL Optimization)

使用奖励模型作为信号,通过PPO(Proximal Policy Optimization)等强化学习算法来优化模型的策略。

RLHF训练流程:

预训练模型 → SFT微调 → SFT模型
                              ↓
                     收集人类偏好数据
                              ↓
                     训练奖励模型
                              ↓
                  PPO/DPO优化 → 最终模型

8.2 奖励模型

8.2.1 奖励模型的原理

奖励模型(Reward Model, RM)是一个能够预测人类对模型输出偏好的模型。它接收一个"问题+回答"作为输入,输出一个标量分数,表示这个回答的质量。

训练数据格式:

{
  "question": "请解释什么是量子计算",
  "chosen": "量子计算是一种利用量子力学原理进行计算的技术。与经典计算机使用比特(0或1)不同,量子计算机使用量子比特,它可以同时处于0和1的叠加态...",
  "rejected": "量子计算就是很快的计算机。"
}

8.2.2 奖励模型训练代码

from transformers import AutoModelForSequenceClassification, AutoTokenizer
from trl import RewardTrainer, RewardConfig

# 加载基础模型作为奖励模型
model_name = "Qwen/Qwen2-7B-Instruct"
model = AutoModelForSequenceClassification.from_pretrained(
    model_name,
    num_labels=1,  # 输出一个标量分数
    trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# 准备偏好数据
def format_preference(example):
    """格式化偏好数据"""
    chosen = example["question"] + "\n" + example["chosen"]
    rejected = example["question"] + "\n" + example["rejected"]
    return {
        "input_ids_chosen": tokenizer(chosen, truncation=True, max_length=2048)["input_ids"],
        "input_ids_rejected": tokenizer(rejected, truncation=True, max_length=2048)["input_ids"],
    }

# 训练配置
training_args = RewardConfig(
    output_dir="./reward_model",
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=1e-5,
    lr_scheduler_type="cosine",
    warmup_ratio=0.1,
    bf16=True,
    logging_steps=10,
    save_strategy="steps",
    save_steps=500,
)

# 创建训练器
trainer = RewardTrainer(
    model=model,
    args=training_args,
    tokenizer=tokenizer,
    train_dataset=preference_dataset,
)

# 开始训练
trainer.train()

# 保存奖励模型
trainer.save_model("./reward_model_final")

8.3 PPO 训练

PPO(Proximal Policy Optimization)是RLHF中最常用的强化学习算法。

8.3.1 PPO训练代码

from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead
from transformers import pipeline

# 加载SFT模型
model = AutoModelForCausalLMWithValueHead.from_pretrained("./sft_model")
tokenizer = AutoTokenizer.from_pretrained("./sft_model")

# 加载奖励模型
reward_model = pipeline(
    "text-classification",
    model="./reward_model_final",
    device_map="auto",
)

# PPO配置
ppo_config = PPOConfig(
    learning_rate=1.41e-5,
    batch_size=16,
    mini_batch_size=4,
    ppo_epochs=4,
    gradient_accumulation_steps=4,
    init_kl_coef=0.2,
    target_kl=6.0,
)

# 创建PPO训练器
ppo_trainer = PPOTrainer(
    config=ppo_config,
    model=model,
    tokenizer=tokenizer,
)

# 训练循环
for epoch in range(num_epochs):
    for batch in dataloader:
        # 1. 生成回答
        response_tensors = ppo_trainer.generate(
            batch["input_ids"],
            max_new_tokens=256,
        )

        # 2. 计算奖励
        texts = [tokenizer.decode(r, skip_special_tokens=True) for r in response_tensors]
        rewards = [reward_model(t)[0]['score'] for t in texts]

        # 3. PPO更新
        stats = ppo_trainer.step(
            batch["input_ids"],
            response_tensors,
            rewards,
        )

        # 4. 记录统计信息
        ppo_trainer.log_stats(stats, batch, rewards)

8.4 DPO:直接偏好优化

DPO(Direct Preference Optimization)是一种更简洁的偏好优化方法,它不需要训练单独的奖励模型,直接使用偏好数据来优化模型。

8.4.1 DPO的原理

DPO的核心思想是:将RLHF中的奖励模型训练和PPO优化合并为一步,直接优化模型使其生成人类偏好的回答。

DPO的损失函数:

L_DPO = -E[log σ(β(log π(y_w|x)/π_ref(y_w|x)) - log π(y_l|x)/π_ref(y_l|x)))]

其中:
- π 是正在训练的模型
- π_ref 是参考模型(通常是SFT后的模型)
- y_w 是被偏好的回答(chosen)
- y_l 是不被偏好的回答(rejected)
- β 是温度参数,控制偏离参考模型的程度

8.4.2 DPO训练代码

from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import DPOTrainer, DPOConfig

# 加载模型
model_name = "Qwen/Qwen2-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# DPO配置
dpo_config = DPOConfig(
    output_dir="./dpo_model",
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=5e-7,
    lr_scheduler_type="cosine",
    warmup_ratio=0.1,
    beta=0.1,  # DPO温度参数
    bf16=True,
    gradient_checkpointing=True,
    logging_steps=10,
    save_strategy="steps",
    save_steps=500,
)

# 准备偏好数据
def format_dpo_data(example):
    """格式化DPO训练数据"""
    return {
        "prompt": example["question"],
        "chosen": example["chosen"],
        "rejected": example["rejected"],
    }

# 创建DPO训练器
trainer = DPOTrainer(
    model=model,
    ref_model=None,  # 自动创建参考模型
    args=dpo_config,
    tokenizer=tokenizer,
    train_dataset=preference_dataset,
    max_length=2048,
    max_prompt_length=1024,
)

# 开始训练
trainer.train()

# 保存模型
trainer.save_model("./dpo_model_final")

8.5 ORPO:无需参考模型的偏好优化

ORPO(Odds Ratio Preference Optimization)是2024年提出的一种新的偏好优化方法,它不需要参考模型,简化了训练流程。

8.5.1 ORPO的原理

ORPO的关键创新是将SFT损失和偏好优化损失合并为一个损失函数:

L_ORPO = L_SFT + λ · L_OR

其中:
- L_SFT 是标准的SFT交叉熵损失
- L_OR 是基于赔率比(Odds Ratio)的偏好损失
- λ 是平衡系数

8.5.2 ORPO训练代码

from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import ORPOTrainer, ORPOConfig

# 加载模型
model_name = "Qwen/Qwen2-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(model_name, trust_remote_code=True)
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)

# ORPO配置
orpo_config = ORPOConfig(
    output_dir="./orpo_model",
    num_train_epochs=1,
    per_device_train_batch_size=2,
    gradient_accumulation_steps=8,
    learning_rate=1e-6,
    lr_scheduler_type="cosine",
    warmup_ratio=0.1,
    beta=0.1,  # ORPO温度参数
    bf16=True,
    gradient_checkpointing=True,
    logging_steps=10,
)

# 创建ORPO训练器
trainer = ORPOTrainer(
    model=model,
    args=orpo_config,
    tokenizer=tokenizer,
    train_dataset=preference_dataset,
    max_length=2048,
)

# 开始训练(无需参考模型!)
trainer.train()

8.6 DPO vs PPO vs ORPO 对比

特性 PPO DPO ORPO
需要奖励模型
需要参考模型
训练稳定性 较差 最好
实现复杂度
显存需求
效果 最好
推荐场景 大规模RLHF 快速偏好优化 资源受限场景

选择建议

  • 如果你有大量计算资源和偏好数据 → 使用PPO
  • 如果你想要快速实验偏好优化 → 使用DPO
  • 如果你资源有限或想要最简单的流程 → 使用ORPO

8.7 偏好数据收集

8.7.1 数据收集流程

# 偏好数据收集脚本
import json

def collect_preference_data(questions, model, tokenizer, num_responses=4):
    """收集偏好数据"""
    preference_data = []

    for question in questions:
        # 对每个问题生成多个回答
        responses = []
        for _ in range(num_responses):
            input_text = f"### 指令:\n{question}\n\n### 回答:\n"
            inputs = tokenizer(input_text, return_tensors="pt").to("cuda")

            outputs = model.generate(
                **inputs,
                max_new_tokens=512,
                temperature=0.8,
                do_sample=True,
            )
            response = tokenizer.decode(outputs[0], skip_special_tokens=True)
            response = response.split("### 回答:\n")[-1].strip()
            responses.append(response)

        # 让人类标注偏好(实际使用时需要人工标注界面)
        print(f"\n问题: {question}")
        for i, resp in enumerate(responses):
            print(f"\n回答 {i+1}: {resp}")

        # 模拟标注(实际使用时需要人工选择)
        chosen_idx = int(input("选择最佳回答 (1-4): ")) - 1
        rejected_idx = int(input("选择最差回答 (1-4): ")) - 1

        preference_data.append({
            "question": question,
            "chosen": responses[chosen_idx],
            "rejected": responses[rejected_idx],
        })

    return preference_data

8.7.2 AI辅助标注

def ai_assisted_annotation(data, judge_model="gpt-4"):
    """使用AI辅助进行偏好标注"""
    import openai

    annotated = []
    for item in data:
        prompt = f"""请比较以下两个回答的质量,选择更好的一个。

问题:{item['question']}

回答A:{item['response_a']}

回答B:{item['response_b']}

评判标准:
1. 准确性:信息是否正确
2. 完整性:是否全面回答了问题
3. 清晰度:表达是否清晰易懂
4. 有用性:对用户是否有帮助

请输出"A"或"B",并简要说明理由:"""

        response = openai.chat.completions.create(
            model=judge_model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
        )

        judgment = response.choices[0].message.content
        if "A" in judgment[:5]:
            annotated.append({
                "question": item['question'],
                "chosen": item['response_a'],
                "rejected": item['response_b'],
            })
        else:
            annotated.append({
                "question": item['question'],
                "chosen": item['response_b'],
                "rejected": item['response_a'],
            })

    return annotated

第九章:评估与基准测试

9.1 评估的重要性

模型评估是微调流程中至关重要的一环。没有评估,我们无法知道微调是否真的提升了模型能力,也无法比较不同微调策略的效果。

评估通常分为两大类:

  • 自动评估:使用自动化指标和基准测试
  • 人工评估:让人类评估模型输出的质量

9.2 自动评估

9.2.1 常用评估指标

困惑度(Perplexity)

困惑度是语言模型最基本的评估指标,表示模型对文本的"惊讶程度"。困惑度越低,说明模型对文本的预测越准确。

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

def compute_perplexity(model, tokenizer, text, max_length=512):
    """计算困惑度"""
    inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length)
    inputs = {k: v.to(model.device) for k, v in inputs.items()}

    with torch.no_grad():
        outputs = model(**inputs, labels=inputs["input_ids"])
        loss = outputs.loss
        perplexity = torch.exp(loss).item()

    return perplexity

# 使用示例
model = AutoModelForCausalLM.from_pretrained("./fine_tuned_model")
tokenizer = AutoTokenizer.from_pretrained("./fine_tuned_model")

text = "深度学习是人工智能的一个重要分支"
ppl = compute_perplexity(model, tokenizer, text)
print(f"困惑度: {ppl:.2f}")

BLEU分数(主要用于翻译任务):

from nltk.translate.bleu_score import corpus_bleu

def compute_bleu(references, hypotheses):
    """计算BLEU分数"""
    ref_tokens = [[ref.split()] for ref in references]
    hyp_tokens = [hyp.split() for hyp in hypotheses]

    bleu1 = corpus_bleu(ref_tokens, hyp_tokens, weights=(1, 0, 0, 0))
    bleu2 = corpus_bleu(ref_tokens, hyp_tokens, weights=(0.5, 0.5, 0, 0))
    bleu3 = corpus_bleu(ref_tokens, hyp_tokens, weights=(0.33, 0.33, 0.33, 0))
    bleu4 = corpus_bleu(ref_tokens, hyp_tokens, weights=(0.25, 0.25, 0.25, 0.25))

    return {"bleu-1": bleu1, "bleu-2": bleu2, "bleu-3": bleu3, "bleu-4": bleu4}

ROUGE分数(主要用于摘要任务):

from rouge_score import rouge_scorer

def compute_rouge(references, hypotheses):
    """计算ROUGE分数"""
    scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)

    scores = {"rouge1": [], "rouge2": [], "rougeL": []}
    for ref, hyp in zip(references, hypotheses):
        score = scorer.score(ref, hyp)
        for key in scores:
            scores[key].append(score[key].fmeasure)

    return {key: sum(vals)/len(vals) for key, vals in scores.items()}

9.2.2 基准测试

MMLU(Massive Multitask Language Understanding)

MMLU是一个广泛使用的多任务语言理解基准,涵盖57个不同学科的选择题。

# 使用LLaMA-Factory进行MMLU评估
llamafactory-cli eval \
    --model_name_or_path ./fine_tuned_model \
    --task mmlu \
    --split test \
    --lang en \
    --n_shot 5

# 或者使用lm-eval-harness
pip install lm-eval

lm_eval --model hf \
    --model_args pretrained=./fine_tuned_model \
    --tasks mmlu \
    --num_fewshot 5 \
    --batch_size 8 \
    --output_path ./eval_results

HumanEval(代码生成)

HumanEval是一个代码生成基准,评估模型生成正确代码的能力。

# 使用lm-eval-harness进行HumanEval评估
lm_eval --model hf \
    --model_args pretrained=./fine_tuned_model \
    --tasks humaneval \
    --batch_size 8 \
    --output_path ./eval_results

C-Eval(中文评估)

C-Eval是一个中文综合评估基准,涵盖52个不同学科。

# 使用LLaMA-Factory进行C-Eval评估
llamafactory-cli eval \
    --model_name_or_path ./fine_tuned_model \
    --task ceval \
    --split val \
    --lang zh \
    --n_shot 5

9.2.3 自定义评估脚本

import json
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from tqdm import tqdm

class ModelEvaluator:
    def __init__(self, model_path, device="cuda"):
        self.model = AutoModelForCausalLM.from_pretrained(
            model_path,
            torch_dtype=torch.bfloat16,
            device_map="auto",
            trust_remote_code=True,
        )
        self.tokenizer = AutoTokenizer.from_pretrained(
            model_path,
            trust_remote_code=True,
        )
        self.model.eval()

    def generate_response(self, instruction, max_new_tokens=512):
        """生成回答"""
        prompt = f"### 指令:\n{instruction}\n\n### 回答:\n"
        inputs = self.tokenizer(prompt, return_tensors="pt").to(self.model.device)

        with torch.no_grad():
            outputs = self.model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=0.7,
                do_sample=True,
                top_p=0.9,
            )

        response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
        response = response.split("### 回答:\n")[-1].strip()
        return response

    def evaluate_accuracy(self, test_data):
        """评估准确率"""
        correct = 0
        total = len(test_data)

        for item in tqdm(test_data, desc="评估中"):
            response = self.generate_response(item['instruction'])

            # 简单的精确匹配评估
            if item['expected_output'].strip() in response:
                correct += 1

        accuracy = correct / total
        return accuracy

    def evaluate_generation_quality(self, test_data):
        """评估生成质量"""
        scores = []

        for item in tqdm(test_data, desc="评估中"):
            response = self.generate_response(item['instruction'])

            # 多维度评分
            score = {
                "length": len(response),
                "has_content": len(response) > 50,
                "no_repetition": not self._has_repetition(response),
                "coherent": self._check_coherence(response),
            }
            scores.append(score)

        # 汇总统计
        avg_scores = {
            key: sum(s[key] for s in scores) / len(scores)
            for key in scores[0]
        }
        return avg_scores

    def _has_repetition(self, text, threshold=0.5):
        """检查文本是否有重复"""
        sentences = text.split('。')
        if len(sentences) < 3:
            return False
        unique = set(sentences)
        return len(unique) / len(sentences) < threshold

    def _check_coherence(self, text):
        """检查文本连贯性"""
        # 简单检查:是否有完整的句子
        return text.endswith(('。', '!', '?', '.', '!', '?'))

# 使用示例
evaluator = ModelEvaluator("./fine_tuned_model")

test_data = [
    {"instruction": "什么是机器学习?", "expected_output": "机器学习"},
    {"instruction": "解释量子计算的基本原理", "expected_output": "量子"},
]

accuracy = evaluator.evaluate_accuracy(test_data)
print(f"准确率: {accuracy:.2%}")

quality = evaluator.evaluate_generation_quality(test_data)
print(f"生成质量: {quality}")

9.3 人工评估

9.3.1 人工评估框架

import json
from datetime import datetime

class HumanEvaluationFramework:
    def __init__(self, eval_file="human_eval_results.json"):
        self.eval_file = eval_file
        self.results = []

    def create_eval_task(self, question, responses, models):
        """创建评估任务"""
        task = {
            "id": len(self.results),
            "question": question,
            "responses": [
                {"model": model, "response": response}
                for model, response in zip(models, responses)
            ],
            "created_at": datetime.now().isoformat(),
            "ratings": None,
        }
        return task

    def submit_rating(self, task_id, ratings):
        """提交评分"""
        # ratings格式: {"model_name": {"accuracy": 1-5, "helpfulness": 1-5, "fluency": 1-5}}
        self.results[task_id]["ratings"] = ratings
        self.results[task_id]["rated_at"] = datetime.now().isoformat()

    def calculate_agreement(self):
        """计算评估者间一致性(如果有多个评估者)"""
        # 使用Cohen's Kappa或Krippendorff's Alpha
        pass

    def generate_report(self):
        """生成评估报告"""
        report = {
            "total_tasks": len(self.results),
            "completed_tasks": sum(1 for r in self.results if r["ratings"]),
            "model_scores": {},
        }

        # 计算每个模型的平均分
        for result in self.results:
            if result["ratings"]:
                for model, scores in result["ratings"].items():
                    if model not in report["model_scores"]:
                        report["model_scores"][model] = []
                    report["model_scores"][model].append(scores)

        # 计算平均
        for model in report["model_scores"]:
            scores_list = report["model_scores"][model]
            avg = {}
            for key in scores_list[0]:
                avg[key] = sum(s[key] for s in scores_list) / len(scores_list)
            report["model_scores"][model] = avg

        return report

    def save(self):
        """保存评估结果"""
        with open(self.eval_file, 'w', encoding='utf-8') as f:
            json.dump(self.results, f, ensure_ascii=False, indent=2)

9.3.2 A/B测试

def ab_test(model_a_path, model_b_path, test_questions, num_evaluations=100):
    """A/B测试两个模型"""
    evaluator_a = ModelEvaluator(model_a_path)
    evaluator_b = ModelEvaluator(model_b_path)

    results = {"a_wins": 0, "b_wins": 0, "tie": 0}

    for question in test_questions[:num_evaluations]:
        response_a = evaluator_a.generate_response(question)
        response_b = evaluator_b.generate_response(question)

        # 使用GPT-4作为评判
        judgment = judge_response(question, response_a, response_b)

        if judgment == "A":
            results["a_wins"] += 1
        elif judgment == "B":
            results["b_wins"] += 1
        else:
            results["tie"] += 1

    # 输出结果
    total = sum(results.values())
    print(f"A胜率: {results['a_wins']/total:.2%}")
    print(f"B胜率: {results['b_wins']/total:.2%}")
    print(f"平局率: {results['tie']/total:.2%}")

    return results

9.4 评估最佳实践

evaluation_best_practices = {
    "多维度评估": "不要只看一个指标,综合考虑准确率、流畅度、相关性等",
    "多样本测试": "测试样本应该覆盖各种场景和难度级别",
    "基线对比": "始终与基线模型(如微调前的模型)进行对比",
    "统计显著性": "确保测试样本量足够,结果具有统计意义",
    "领域专家参与": "对于专业领域,邀请领域专家参与评估",
    "定期评估": "定期评估模型性能,监控效果衰减",
    "错误分析": "深入分析模型犯错的案例,找出改进方向",
}

第十章:模型合并与导出

10.1 模型合并技术

模型合并(Model Merging)是将多个模型的权重合并为一个模型的技术。这在以下场景中非常有用:

  • 将LoRA权重合并到基础模型中
  • 合并多个不同任务的微调模型
  • 创建模型的"混合"版本

10.1.1 LoRA权重合并

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

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",
        trust_remote_code=True,
    )

    # 加载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, trust_remote_code=True)
    tokenizer.save_pretrained(output_path)

    print(f"模型已保存到: {output_path}")
    return merged_model

# 使用示例
merge_lora_weights(
    base_model_path="Qwen/Qwen2-7B-Instruct",
    lora_path="./lora_weights",
    output_path="./merged_model",
)

10.1.2 多LoRA合并

def merge_multiple_loras(base_model_path, lora_paths, output_path, weights=None):
    """合并多个LoRA权重"""
    from peft import PeftModel

    # 加载基础模型
    base_model = AutoModelForCausalLM.from_pretrained(
        base_model_path,
        torch_dtype=torch.bfloat16,
        device_map="auto",
    )

    # 加载第一个LoRA
    model = PeftModel.from_pretrained(base_model, lora_paths[0])

    # 加载并合并其他LoRA
    for i, lora_path in enumerate(lora_paths[1:], 1):
        model.load_adapter(lora_path, adapter_name=f"adapter_{i}")

    # 设置权重(如果提供)
    if weights:
        adapter_weights = {f"adapter_{i}": w for i, w in enumerate(weights)}
        model.add_weighted_adapter(
            adapters=list(adapter_weights.keys()),
            weights=list(adapter_weights.values()),
            adapter_name="merged",
        )
        model.set_adapter("merged")
    else:
        # 等权重合并
        model.add_weighted_adapter(
            adapters=[f"adapter_{i}" for i in range(len(lora_paths))],
            weights=[1.0 / len(lora_paths)] * len(lora_paths),
            adapter_name="merged",
        )
        model.set_adapter("merged")

    # 合并并保存
    merged_model = model.merge_and_unload()
    merged_model.save_pretrained(output_path)

10.1.3 SLERP合并

SLERP(Spherical Linear Interpolation)是一种在球面上进行插值的方法,常用于合并两个模型。

import torch

def slerp_merge(model_a_path, model_b_path, alpha=0.5, output_path=None):
    """使用SLERP方法合并两个模型"""
    # 加载两个模型的权重
    state_dict_a = torch.load(f"{model_a_path}/model.safetensors")
    state_dict_b = torch.load(f"{model_b_path}/model.safetensors")

    merged_state_dict = {}

    for key in state_dict_a:
        if key in state_dict_b:
            weight_a = state_dict_a[key].float()
            weight_b = state_dict_b[key].float()

            # SLERP插值
            # 归一化
            norm_a = torch.norm(weight_a)
            norm_b = torch.norm(weight_b)

            if norm_a == 0 or norm_b == 0:
                # 如果有一个是零向量,直接线性插值
                merged = (1 - alpha) * weight_a + alpha * weight_b
            else:
                # 计算夹角
                cos_angle = torch.sum(weight_a * weight_b) / (norm_a * norm_b)
                cos_angle = torch.clamp(cos_angle, -1, 1)
                angle = torch.acos(cos_angle)

                if angle < 1e-6:
                    # 角度太小,直接线性插值
                    merged = (1 - alpha) * weight_a + alpha * weight_b
                else:
                    # SLERP
                    merged = (torch.sin((1 - alpha) * angle) / torch.sin(angle) * weight_a +
                             torch.sin(alpha * angle) / torch.sin(angle) * weight_b)

            merged_state_dict[key] = merged.to(state_dict_a[key].dtype)
        else:
            merged_state_dict[key] = state_dict_a[key]

    # 保存合并后的模型
    if output_path:
        torch.save(merged_state_dict, f"{output_path}/model.safetensors")

    return merged_state_dict

10.2 GGUF 转换

GGUF(GGML Unified Format)是llama.cpp使用的模型格式,支持多种量化方法,适合在CPU和边缘设备上运行。

10.2.1 使用llama.cpp转换

# 1. 克隆llama.cpp
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# 2. 编译
make

# 3. 转换为GGUF格式
python convert_hf_to_gguf.py \
    ../merged_model \
    --outfile ../model-f16.gguf \
    --outtype f16

# 4. 量化
./llama-quantize \
    ../model-f16.gguf \
    ../model-q4_k_m.gguf \
    q4_k_m

10.2.2 使用Unsloth转换GGUF

from unsloth import FastLanguageModel

# 加载模型
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="./merged_model",
    max_seq_length=2048,
    load_in_4bit=True,
)

# 保存为GGUF格式
model.save_pretrained_gguf(
    "model_gguf",
    tokenizer,
    quantization_method="q4_k_m",  # 量化方法
)

10.2.3 量化方法选择

quantization_guide = {
    "q2_k": {
        "大小": "最小",
        "质量": "较差",
        "适用": "极度受限的设备",
        "每个参数": "约2.5 bit",
    },
    "q3_k_m": {
        "大小": "较小",
        "质量": "中等偏下",
        "适用": "低内存设备",
        "每个参数": "约3.5 bit",
    },
    "q4_k_m": {
        "大小": "中等",
        "质量": "良好",
        "适用": "推荐的平衡选择",
        "每个参数": "约4.5 bit",
    },
    "q5_k_m": {
        "大小": "较大",
        "质量": "很好",
        "适用": "追求质量的场景",
        "每个参数": "约5.5 bit",
    },
    "q6_k": {
        "大小": "大",
        "质量": "优秀",
        "适用": "高质量需求",
        "每个参数": "约6.5 bit",
    },
    "q8_0": {
        "大小": "很大",
        "质量": "接近原始",
        "适用": "质量优先",
        "每个参数": "约8 bit",
    },
}

10.3 Ollama 部署

Ollama是一个轻量级的本地大模型运行工具,支持在本地快速部署和运行大模型。

10.3.1 安装Ollama

# Linux/macOS
curl -fsSL https://ollama.ai/install.sh | sh

# 或者使用brew(macOS)
brew install ollama

10.3.2 创建Ollama模型

首先创建一个Modelfile:

# Modelfle
FROM ./model-q4_k_m.gguf

# 模板
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录