如何利用SFT和QLoRA技术为AI代理定制大型语言模型
在本教程中,我将向您展示如何使用QLoRA进行有监督微调,从而优化大型语言模型,使其能够用于AI代理中。这种方法使我们能够对预训练模型进行定制,使其表现出我们期望的行为。我们将采用一种轻量级的训练流程,仅更新模型中的少量参数。 我们会利用Unsloth以及Hugging Face生态系来下载Qwen 1.5B基础模型,然后应用基于QLoRA的有监督微调方法,并将优化后的LoRA适配器权重保存在本地,以便后续进行推理使用。所有操作都在本地完成,因此您无需支付任何模型API的使用费用。 目录 背景知识 什么是有监督微调? 什么是LoRA? 动机与架构 步骤1:安装Python相关依赖库 步骤2:训
在本教程中,我将向您展示如何使用QLoRA进行有监督微调,从而优化大型语言模型,使其能够用于AI代理中。这种方法使我们能够对预训练模型进行定制,使其表现出我们期望的行为。我们将采用一种轻量级的训练流程,仅更新模型中的少量参数。
我们会利用Unsloth以及Hugging Face生态系来下载Qwen 1.5B基础模型,然后应用基于QLoRA的有监督微调方法,并将优化后的LoRA适配器权重保存在本地,以便后续进行推理使用。所有操作都在本地完成,因此您无需支付任何模型API的使用费用。
目录
背景知识
训练语言模型意味着向它提供大量示例数据,并更新其内部的参数,从而使它能够更准确地预测预期的输出结果。现代的大型语言模型通常拥有数百万甚至数十亿个参数,这也是训练这些模型成本较高的原因之一。模型的参数数量越多,通常所需的计算资源和内存也就越多。
像Claude和ChatGPT这样的基础大型语言模型也被训练成具有通用性,这意味着它们的回答可能会显得较为笼统、缺乏一致性,或者无法很好地适应特定的应用场景。即使使用提示信息来帮助模型学习,有时我们也希望模型能够直接从示例数据中学会更加规律的行为模式。
这时,微调技术就派上了用场。微调是一种将预训练模型调整得更适合特定任务使用的过程。其中一种常见的方法就是有监督微调,即利用带有标签的输入/输出示例来对模型进行训练,从而让模型学会产生符合我们需求的行为结果。
本教程适用于macOS、Windows和Linux操作系统。我使用的是一台配备32GB内存的MacBook Pro,没有安装外部GPU,但通过使用规模较小的预训练模型,这个训练流程也可以在配置较低的硬件上顺利运行。
什么是有监督微调?
有监督微调,简称SFT,是指利用已预训练的模型,在带有标签的输入/输出示例数据的基础上对其进行进一步训练。与从零开始训练模型不同,有监督微调是从一个已经具备一定语言理解能力的模型出发,然后教会它以更符合特定任务需求的方式产生响应。例如,我们可能希望模型能够用某种特定的语气进行回答、遵循某种格式结构,或者在某个具体的任务中表现出更高的稳定性。有监督微调通过向模型展示大量期望的行为示例,帮助它朝着这些目标方向发展。
你所需的数据量取决于具体的任务。对于诸如语气或格式调整这类简单的修改,几百个高质量的示例就已经足够了。而对于那些需要更复杂处理或针对特定领域进行适配的任务,通常就需要大量精心挑选的示例了。
在本教程中,我们会使用五个示例来让训练过程更加快速简便,但同样的代码其实也可以应用于规模更大的数据集,在实际的生产环境中同样适用。
什么是LoRA?
对于大型语言模型来说,进行全面的微调往往成本较高,因为这些模型拥有大量的参数。更新所有这些参数会消耗大量的GPU内存、计算资源和存储空间。
LoRA是一种更为轻量化的微调方法。它属于那些以高效利用参数为特点的微调技术之一,也就是说,它可以在不修改模型原始权重的情况下对预训练模型进行适配。具体来说,基础模型的大部分权重保持不变,而LoRA只会添加一小部分可训练的适配权重。
在本教程中,我们将使用QLoRA这种方法。QLoRA结合了量化技术与LoRA机制:首先以低精度(通常是4位)加载基础模型,然后再对这些适配权重进行训练。这种方式能够进一步降低内存消耗,使得在硬件资源有限的情况下也能轻松进行微调。
我们还会使用一个名为Unsloth的开源库,这个库专门为加快大型语言模型的微调速度并提高其内存利用率而设计。它可以从Hugging Face平台上下载模型权重、分词器及相关配置文件,在使用LoRA进行有监督微调等场景中非常实用,尤其是在硬件资源有限的情况下。
开发动机与架构原理
一旦你构建了一个AI代理,你可能会发现,某些现成的模型在产生你所需要的输出结果时,需要长篇的提示信息、重复的指令以及额外的上下文信息。这样一来,不仅会增加token的使用量、延迟以及成本,而且得到的结果也可能不稳定。在这种情况下,一个自然的解决办法就是训练模型,使其能够以更符合你的需求的方式作出响应。
我们的架构方案是:首先加载一个经过量化的基础模型,然后整理好带有标签的聊天示例数据,接着添加LoRA适配权重,并仅对这些适配权重进行有监督微调,最后将训练得到的适配权重保存下来,以便日后在基础模型之上使用它们来运行AI代理。具体的实现代码将在后面的章节中详细说明。
步骤1:安装Python相关依赖库
创建一个虚拟环境,并安装所需的软件包:
python3 -m venv venv
source venv/bin/activate
pip install unsloth datasets transformers trl torch peft accelerate bitsandbytes
步骤2:训练代码实现
我们会使用Qwen2.5-1.5B-Instruct这个模型,因为它体积较小,且经过专门针对指令输入进行优化,因此非常适合在硬件资源有限的环境中进行QLoRA微调。
从高层次来看,该代码会以4位模式加载预训练的unsloth/Qwen2.5-1.5B-Instruct模型,正是这一设计使得它属于QLoRA风格的工作流程,并且能够有效降低GPU的内存使用量。
随后,它会使用一个包含五个聊天式客户支持示例的小型有监督微调数据集(这些示例在文件的开头部分有详细说明),将这些示例按照Qwen的聊天模板进行格式化处理,并在选定的注意力层和MLP层上添加LoRA适配器。
在训练过程中,SFTTrainer只会更新那些较小的LoRA适配器权重,而不会修改整个基础模型。它采用了诸如梯度累积、8位AdamW优化器以及Unsloth的梯度检查点保存机制等节省内存的技术。
请将这段代码保存为`train.py`文件。我在代码中添加了注释,以便帮助你理解其工作原理:
from unsloth import FastLanguageModel
from datasets import Dataset
from transformers import TrainingArguments
from trl import SFTConfig, SFTTrainer
max_seq_length = 2048
dataset = Dataset.from_list([
{
"messages": [
{"role": "system",
"content": "您是一位乐于提供帮助的电商客服助手。请保持礼貌、简洁,不要编造订单细节。"},
{"role": "user", "content": "我的订单在哪里?"},
{"role": "assistant", "content": "我可以帮您查询。请提供您的订单号码,我就能查看最新的配送状态。"}
]
},
{
"messages": [
{"role": "system",
"content": "您是一位乐于提供帮助的电商客服助手。请保持礼貌、简洁,不要编造订单细节。"},
{"role": "user", "content": "我的包裹损坏了,我该怎么办?"},
{"role": "assistant", "content": "非常抱歉。请您上传损坏物品和包装的照片,我会指导您进行换货或退款申请。"}
]
},
{
"messages": [
{"role": "system",
"content": "您是一位乐于提供帮助的电商客服助手。请保持礼貌、简洁,不要编造订单细节。"},
{"role": "user", "content": "45天后我还能退货吗?"},
{"role": "assistant", "content": "我们的标准退换货期限为30天。如果您的商品有质量问题或存在配送问题,请提供详细情况,我会帮您考虑是否可以特殊处理。"}
]
},
{
"messages": [
{"role": "system",
"content": "您是一位乐于提供帮助的电商客服助手。请保持礼貌、简洁,不要编造订单细节。"},
{"role": "user", "content": "我输入了错误的配送地址。」,
{"role": "assistant", "content": "如果订单尚未发货,我们或许可以更改地址。请您尽快提供订单号码和正确的配送地址。"}
]
},
{
"messages": [
{"role": "system",
"content": "您是一位乐于提供帮助的电商客服助手。请保持礼貌、简洁,不要编造订单细节。"},
{"role": "user", "content": "我的订单被收取了两次费用。」,
{"role": "assistant", "content": "对此我深感抱歉。请您提供您的订单号码以及收费发生的日期,我们会尽快核实重复收费的情况并协助您解决问题。"}
]
},
])
# 加载预训练的Qwen指令模型。Unsloth会自动配置高效加载方式。
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen2.5-1.5B-Instruct", # 从Hugging Face/Unsloth下载的预训练模型
max_seq_length=max_seq_length, # 模型应适应的最大序列长度
# 较长的上下文信息会消耗更多内存
load_in_4bit=True, # 以4位量化格式加载模型权重
# 这种方式能大幅降低训练/推理时的VRAM使用量
# LoRA/QLoRA工作流程中常用此方法
dtype=None, # 让Unsloth/Torch自动选择合适的数值精度
# 通常会选择float16/bfloat16等适合的类型
)
def format_example(example):
text = tokenizer.apply_chat_template(
example["messages"], # 从“messages”字段中读取对话内容
tokenize=False, # 返回格式化后的字符串,而不是token ID
add_generation_prompt=False, # 不添加空的助手提示语
# 因为这个示例已经包含了助手的回复内容
)
return {"text": text} # 返回一个新字段,其中包含格式化后的聊天文本
formatted_dataset = dataset.map(format_example)
# LoRA不会训练数十亿个参数,
# 而是在注意力层中插入一些小型、可训练的矩阵。
model = FastLanguageModel.get_peft_model(
model, # 基础预训练模型;LoRA适配器将添加到这个模型上
r=16, # LoRA的秩:
# 低秩适配器矩阵的大小
# 数值越大,表示容量越大、可训练的参数也越多
# 数值越小,模型的计算速度会更快,但表达能力也会减弱
target_modules=[
"q_proj", # 注意力机制中的查询投影层
"k_proj", # 注意力机制中的键投影层
"vProj", # 注意力机制中的值投影层
"o_proj", # 注意力机制中的输出投影层
"gate_proj", # MLP模块中的门控投影层
"up_proj", # MLP模块中的上升投影层
"downprojection", # MLP模块中的下降投影层
], # LoRA适配器仅添加到这些层中
lora_alpha=16, # LoRA的缩放因子
# 控制适配器更新对基础模型权重的影响程度
# 通常将其设置为与r相同的值
loradropout=0, # 在训练过程中,LoRA路径上的dropout率为0
# 在Unsloth的相关示例中,这个值通常被设置为0
bias="none", # 不训练偏置参数
# 只有LoRA适配器权重是可训练的
use_gradient_checkpointing="unsloth", # 使用Unsloth提供的节省内存的检查点保存机制
# 这种方式可以在反向传播过程中避免重新计算激活值,从而降低VRAM使用量
max_seq_length=max_seq_length, # 在训练过程中预期出现的最大token序列长度
)
trainer = SFTTrainer(
model=model, # 需要微调的模型(基础模型+LoRA适配器)
tokenizer=tokenizer, # 将文本转换为模型能够理解的token ID
train_dataset=formatted_dataset, # 你的训练数据集
dataset_text_field="text", # 数据集中包含训练文本的列
max_seq_length=max_seq_length, # 每个示例中最大的token数量
args=SFTConfig(
output_dir="../outputs", # 存储检查点、日志和结果的文件夹路径
per_device_train_batch_size=2, # 每个GPU一次处理的样本数量
gradient_accumulation_steps=4, # 在更新权重之前,先累积4个小批量的梯度
# 在1个GPU上,有效的批量大小约为8
max_steps=30, # 训练进行10次优化器更新后停止训练
logging_steps=1, # 每隔1步打印训练指标
warmup_steps=5, # 在前5步逐渐增加学习率
learning_rate=2e-4, # 主要的学习率值
optim="adamw_8bit", # 节省内存的AdamW优化器(适合VRAM资源有限的环境)
weight_decay=0.01, # 适当的正则化系数,有助于防止过拟合
lr_scheduler_type="linear", # 温暖期结束后,学习率会呈线性下降
seed=3407, # 随机种子值,以确保训练结果的可重复性
report_to="none", # 不使用WandB等外部日志工具
),
)
trainer.train()
# 只保存LoRA适配器权重,而不是整个基础模型。
model.save_pretrained("qwen2_0_5b_lora")
# 保存tokenizer,以便在推理时使用相同的词汇表。
tokenizer.save_pretrained("qwen2_0_5b_lora")
步骤3:推理代码
从宏观层面来看,推理代码包含`generate_reply()`函数。该函数会加载使用Unsloth框架训练的模型(模型路径可以是预训练基础模型,也可以是本地保存的LoRA适配器目录),启用推理优化功能,将聊天消息转换成Qwen模型所需的格式,对其进行分词处理,然后将其传输到可用的计算设备上,最后通过`model.generate()`函数生成回复。
请将这段代码保存为`inference.py`文件:
from unsloth import FastLanguageModel
import torch
messages = [
{
"role": "system",
"content": "您是一位乐于提供帮助的电商客户支持助理。请保持礼貌、简洁,不要编造订单细节。"
},
{
"role": "user",
"content": "我想取消我的订单。”
}
]
def generate_reply(model_name, messages):
# 加载基础模型,并自动加载保存好的LoRA适配器。
# “qwen2_0_5b_lora”是使用`model.save_pretrained()`创建的目录。
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=model_name, # 您微调后的LoRA模型的路径或名称
max_seq_length=2048, # 模型应支持的最大上下文长度。较长的上下文长度会消耗更多内存
load_in_4bit=True, # 以4位量化格式加载模型权重。这样在推理过程中可以减少VRAM的使用量
)
# 启用推理优化功能,从而加快生成速度并降低内存消耗。
FastLanguageModel.for_inference(model)
# 将聊天消息转换成Qwen模型所需的格式。
inputs = tokenizer.apply_chat_template(
messages, # 聊天消息列表:系统角色、用户角色及助理角色的对话内容
tokenize=True, # 将格式化后的聊天提示转换为token ID
addgeneration_prompt=True, # 添加助理角色的提示信息,以便模型知道需要生成回复
return_tensors="pt", # 返回PyTorch张量形式的结果
)
# 将输入数据传输到与模型相同的计算设备上。
device = "cuda" if torch.cuda.is_available() else "cpu"
inputs = inputs.to(device)
# 生成助理角色的回复。
outputs = model.generate(
input_ids=inputs, # 传入模型的token化提示信息
max_new_tokens=80, // 回复中最多可以生成80个新的token
temperature=0.2, # 温度值越低,生成的回复就越确定、越有条理;
# 温度值越高,生成的回复就会越随机、越富有创造性
)
# 删除原始的聊天提示信息,只保留新生成的回复内容。
generated_tokens = outputs[0][inputs.shape[-1]:]
# 将token ID转换回可读文本形式。
response = tokenizer.decode(generated_tokens, skip_special_tokens=True)
return response
before = generate_reply("unsloth/Qwen2-0.5B-Instruct-bnb-4bit", messages)
after = generate-reply("./qwen2_0_5b_lora", messages)
print("=== 修改前的结果 ===")
print(before)
print()
print("=== 修改后的结果 ===")
print(after)
示例输出
训练过程产生的输出如下:
$ python train.py
...
Unsloth:应用了LoRA技术——共有18,464,768个参数可进行训练(占总数456,701,440的4.04%)
...
Unsloth:正在执行30步训练,批量大小为2,梯度累积次数为4,序列长度为2048
Unsloth:使用的特征计算方法包括CCE、GC;学习率为线性下降,优化算法为adamw
第1步/30步 | 损失值:3.9350 | 梯度值:4.8440 | 学习率:0.00e+00 | 每秒处理令牌数:352 | 最大内存使用量:2.35 GB
第2步/30步 | 损失值:4.0082 | 梯度值:4.9456 | 学习率:4.00e-05 | 每秒处理令牌数:388 | 最大内存使用量:2.50 GB
...
第30步/30步 | 损失值:0.0646 | 梯度值:0.6915 | 学习率:8.00e-06 | 每秒处理令牌数:379 | 最大内存使用量:2.57 GB
Unsloth:训练完成!平均损失值:1.2078 | 总耗时:35.6秒 | 训练步数:30步 | 处理的令牌总数:14,480个
Unsloth:已将LoRA适配器的配置信息保存到输出目录中
Unsloth:最终训练结果已保存到输出目录
从输出内容可以看出,LoRA技术应用成功,而且只有大约4%的模型参数被进行了训练,这使得微调过程所需的计算资源较少。
在30步的训练过程中,Unsloth会记录损失值、学习率、每秒处理令牌数以及最大内存使用量等关键指标。损失值从约3.9降到了0.06,这说明模型正在从这个小型数据集中学习知识;整个训练过程仅耗时约35秒,且最多使用了2.6 GB的内存。
最后,Unsloth会将训练得到的LoRA适配器权重保存到outputs目录中,以便后续进行推理计算。你会看到一个名为qwen2_0_5b_lora的新目录,其中包含以下文件:
adapter_config.json
adapters.safetensors
chat_template.jinja
tokenizer.json
tokenizer_config.json
这些保存下来的文件虽然体积不大,但作用非常重要。adapter_config.json记录了LoRA适配器的配置信息,比如哪些层被进行了调整、适配器的具体配置方式等。adapters.safetensors存放了实际训练得到的LoRA权重。chat_template.jinja定义了系统消息、用户输入以及助手回复应该如何被格式化成模型能够识别的结构。tokenizer.json存储了分词器的词汇表和分词规则,而tokenizer_config.json则包含了特殊标记的设置及格式化相关参数。
有了这些文件,就可以重新加载经过微调的适配器,并使用训练时使用的相同提示格式和分词规则来进行推理计算了。
现在可以运行推理脚本来测试这个经过微调的模型了。它会加载保存在qwen2_0_5b_lora目录中的LoRA适配器权重,并在推理过程中将这些权重应用到基础模型上。
用户输入的提示是“我想取消我的订单”。下面的输出展示了同一个模型在应用SFT技术之前和之后给出的回答。
$ python inference.py
=== 应用SFT技术之前的回答 ===
当然可以,我很乐意帮助您!要取消订单,请按照以下步骤操作:
1. **查看您的订单**:在取消订单之前,您需要先确认订单详情。请确保您掌握了所有必要的信息,比如订单编号、订单日期以及其他相关细节。
2. **仔细核对订单内容**:确认订单信息无误后……
=== 应用SFT技术之后的回答 ===
如果您的订单尚未发货,我们或许可以帮您取消它。请您提供订单编号并说明取消的原因,这样我就可以协助您办理退款或积分抵扣手续了。
在应用SFT之前,基础模型给出的回答虽然听起来有一定帮助,但内容较为冗长,且并未遵循明确的电子商务支持工作流程。
应用SFT之后,该模型产生的回复更加简洁明了,会正确地要求提供订单编号,并将取消操作与货物的配送状态联系起来。这充分说明了即使使用规模相对较小的领域特定数据集,SFT也能有效改善模型的响应方式及其与各角色职责之间的匹配度。
微调、提示工程与蒸馏技术
提示工程、微调和蒸馏技术都会以不同的方式影响模型的行为表现。
提示工程通过在推理阶段改变输入模型的指令来发挥作用。通常来说,这是开始训练时最快且最经济有效的方法。
微调则通过使用具体示例对模型进行训练,使其能够更准确地学习到所需的行为模式。
当需要让一个规模较小的模型模仿规模较大的模型的行为时,就可以采用蒸馏技术。
在实际应用中,通常会先从提示工程开始;当需要进一步提升模型的任务适应性时,再进入微调阶段;而当效率成为更重要的考量因素时,蒸馏技术就显得十分有用。
结论
在本教程中,我们使用了QLoRA对预训练的语言模型进行了有监督的微调。与从零开始训练模型相比,我们直接使用了一个通用的指令模型,然后通过少量示例对话对其进行了训练,最后仅更新了轻量级的LoRA适配器权重。这样的方法在硬件资源有限的情况下依然能够使训练过程更加高效,同时让模型更好地适应特定的客户支持应用场景。
今后,你可以尝试使用更大的数据集、不同的提示/响应格式,或者选择规模更大的基础模型来进行实验,观察模型的行为变化。祝你在探索过程中取得成功!
如果你喜欢这个教程,可以在我的博客中阅读更多我的文章(最近的文章包括一系列系统设计论文);也可以在我的个人网站上了解我的工作进展;同时,你还可以在LinkedIn上关注我的动态。
相关文章
如何利用提示工程与上下文工程来开发人工智能代理
在这个教程中,我将向您展示提示工程和上下文工程如何提升人工智能模型的性能。 我们将构建一个简单的本地模型,从基础输入开始,然后通过使用更合适的提示语和更丰富的上下文信息来改进它,这样您就能看到每一项改变对最终输出结果的影响。 我们将会使用LangChain v1、Ollama、Qwen以及Python。所有操作都在您的个人电脑上完成,因此您无需支付任何API费用。 目录 背景知识 什么是提示工程? 什么是上下文工程? 为什么提示工程和上下文工程对人工智能模型如此重要 动机与架构 步骤1:安装Ollama并下载模型 步骤2:安装Python相关依赖库 步骤3:编写代理代码 示例输出结果 提示语优
阅读全文
Flutter中的低功耗蓝牙技术:开发者手册
大多数Flutter教程都只涉及到网络调用和REST API。但一旦你需要与物理设备进行交互——比如心率监测器、智能灯泡、健身追踪器、工业传感器,或者你自己定制的硬件设备——你就不得不离开HTTP这个“舒适的环境”,转而使用蓝牙低功耗技术。 本指南会教你如何在Flutter中正确且全面地实现这些功能。 移动设备上的蓝牙功能其实相当复杂。Android和iOS之间的权限设置有所不同,即使是同一款Android系统的不同版本,权限要求也会存在差异。蓝牙连接的生命周期包含许多状态,服务与特征的数据模型也会让新手感到困惑,而字节级的数据编码方式几乎会让每个人在初次尝试时遇到麻烦。 flutter_bl
阅读全文
如何构建能够自动切换模型的人工智能应用程序
大型语言模型从根本上改变了我们构建现代软件的方式。 但是,如果对于每个用户请求都依赖同一个人工智能模型,就会带来严重的生产风险。API可能会出现故障;对于一些简单的任务来说,使用专有模型可能会耗费高昂的成本;而那些价格较低的开源模型则可能在处理复杂的逻辑推理时遇到困难。 当我所在的团队为我们的客户支持平台开发一个企业级的人工智能引擎时,我们选择对所有场景都使用同一个顶级模型。 然而仅仅一个月后,我们就遇到了两个严重的问题:一次大规模的API故障导致我们的应用程序完全无法正常运行;同时,由于我们使用了价格较高的模型来回答一些简单的常见问题,我们的每月API使用费用也大幅增加了。 为了解决这些问题
阅读全文
如何使用Node.js和Google Gemini通过函数调用来构建一个人工智能代理
github.com/ziaongit/nodejs-gemini-agent 。 目录 功能调用机制的工作原理 我们正在构建什么 先决条件 项目设置 工具的定义 工具功能的实现 构建智能代理的循环机制 命令行入口点的设置 添加Express HTTP服务器 智能代理的测试 故障排除 接下来要构建什么 功能调用机制的工作原理 这里有一个让人感到惊讶的地方:Gemini并不会直接运行你的代码。它只会返回一个结构化对象,其中包含诸如“调用 get_weather 函数、将 city 设置为柏林”这样的指令。你的代码会接收到这些指令,然后执行相应的功能并将结果反馈回去。Gemini会检查这些结果是否
阅读全文