LLM 大模型微调完全教程
面向零基础用户,从原理到实战,手把手教你微调大语言模型
版本:v1.0 | 最后更新:2026年5月
目录
- 第一章:大模型微调概述
- 第二章:监督微调 SFT 原理与实践
- 第三章:LoRA/QLoRA 低秩适配详解
- 第四章:数据工程
- 第五章:主流微调框架对比
- 第六章:LLaMA-Factory 实战
- 第七章:Unsloth 极速微调教程
- 第八章:RLHF 人类反馈强化学习
- 第九章:评估与基准测试
- 第十章:模型合并与导出
- 第十一章:分布式微调
- 第十二章:综合实战项目
- 第十三章:常见问题与最佳实践
第一章:大模型微调概述
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.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通常包含三个阶段:
- SFT阶段:使用监督数据进行指令微调
- 奖励模型训练:训练一个奖励模型来评估输出质量
- PPO/DPO优化:使用强化学习算法优化模型
RLHF的目标是让模型的输出更加符合人类的偏好,比如更加安全、有帮助、诚实。ChatGPT的成功很大程度上就归功于RLHF技术的应用。
1.4.5 领域自适应预训练(Domain-Adaptive Pre-training, DAPT)
在某些情况下,我们有大量的领域文本数据(如医学论文、法律文书),但这些数据没有标注。此时可以先用这些无标注数据对模型进行继续预训练,然后再用标注数据进行微调。
DAPT的流程是:
- 收集大量领域无标注文本
- 使用类似预训练的方式(下一个词预测)继续训练模型
- 使用标注数据进行指令微调
这种方法特别适合那些与通用文本差异较大的专业领域,如生物医学、法律、金融等。
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
人工标注:
对于高要求的场景,人工标注仍然是最可靠的方式。建议:
- 制定详细的标注指南
- 使用多人标注并计算一致性
- 定期进行质量审核
- 使用标注工具(如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 }}