LORA微调Qwen2.5-0.5B-Instruct大模型
LoRA 微调 Qwen2.5-0.5B-Instruct 大模型
一、前言:为什么要微调大模型
如今的大语言模型(LLM)在通用任务上已经表现得相当出色,无论是写代码、做翻译,还是回答百科问题,几乎都能给出令人满意的答案。然而当我们把它们放到真实的业务场景中时,会很快发现一个问题:通用能力强,不代表领域适配好。
举几个常见的例子:
-
我们希望模型用统一的客服话术风格回复用户,而不是天马行空地自由发挥;
-
我们希望模型熟悉公司内部的术语、产品名、流程规范;
-
我们希望模型在某个垂直领域(医疗、法律、金融)的回答更专业、更准确。
要让模型"听话",最直接的办法就是微调(Fine-tuning)。但传统的全参数微调,对于一个动辄数十亿参数的模型来说,显存和算力开销都非常大,普通开发者很难在自己的机器上跑起来。
这时候就轮到 LoRA(Low-Rank Adaptation) 登场了。它能让我们用极低的成本完成大模型微调,在一张消费级显卡甚至免费的 Colab 上就能跑通整个流程。
本文将以阿里巴巴开源的 Qwen2.5-0.5B-Instruct 作为基础模型,带你从零到一,用 LoRA 完成一次完整的微调实验:把模型的回答风格调整为"专业、礼貌的客服话术"。选择 0.5B 这个尺寸是因为它足够轻量,方便在任何环境下复现,但所有方法在更大模型上同样适用。
二、什么是 LoRA:用"旁路矩阵"代替全参数更新
要理解 LoRA,我们先回顾一下传统的微调方式。
假设原始模型中有一个权重矩阵 W,全参数微调会直接更新 W 中的每一个数值。对于一个 70 亿参数的模型,这意味着每一步训练都要计算和保存 70 亿个梯度,对显存的压力极大。
LoRA 的核心思想非常优雅:冻结原始权重 W,在它旁边并联一个"旁路",只训练这个旁路。
数学上的表达是这样的:
W_new = W + ΔW = W + B × A
其中:
-
W是原模型的权重(被冻结,不参与训练); -
A和B是两个新增的低秩矩阵,维度远小于W; -
B × A的结果与W同形,相当于对W的"补丁"。
举个具体的例子:如果 W 是一个 4096 × 4096 的矩阵(约 1678 万参数),我们让 A 的形状是 8 × 4096,B 的形状是 4096 × 8,那么 A 和 B 加起来只有约 6.5 万参数,仅占原矩阵的 0.4%。但因为 B × A 仍是 4096 × 4096,它对原模型的表达能力影响是完整的。
| 对比项 | 全参数微调 | LoRA 微调 |
|---|---|---|
| 可训练参数量 | 全部(数十亿) | 极少(百万级) |
| 显存占用 | 极高 | 低 |
| 训练速度 | 慢 | 快 |
| 存储开销 | 每次保存完整模型 | 只保存几 MB 的 LoRA 权重 |
| 效果 | 上限略高 | 接近全参数微调 |
在大多数实际场景中,LoRA 已经能取得与全参数微调相当的效果,性价比极高。
三、环境准备与依赖安装
本次实验依赖以下四个核心库:
-
transformers:Hugging Face 出品的模型加载与训练框架,是整个生态的基石;
-
peft:Parameter-Efficient Fine-Tuning 的缩写,LoRA 的官方实现就在这个库里;
-
accelerate:负责设备调度(CPU / GPU / 多卡)和混合精度训练;
-
datasets:高效处理训练数据的工具库。
为了避免不同版本之间的兼容性问题,建议锁定版本安装:
pip install -q transformers==4.46.3 peft==0.13.2 accelerate==1.1.1 datasets==3.1.0
-q 参数表示静默安装,可以让输出更干净。如果你在 Colab 中运行,需要在每条命令前加 !;如果在本地终端运行,直接执行即可。
四、构造微调数据集
微调数据集决定了模型最终的"性格"和"知识"。本次我们的目标是让模型用专业的客服话术回复用户,所以我们需要准备一批"日常表达 → 客服式回复"的样本对。
from datasets import Dataset
data = [
{"input": "你们家东西怎么这么贵", "output": "您好,非常理解您对价格的关注。我们的产品在选材与工艺上严格把控,致力于为您提供更优质的使用体验,如有需要,可为您介绍当前可享的优惠方案。"},
{"input": "我要退货", "output": "您好,很抱歉给您带来了不便。请您提供订单编号,我将立即为您核实并安排退货流程,确保问题尽快得到妥善解决。"},
{"input": "什么时候发货", "output": "您好,感谢您的耐心等待。订单通常会在付款后的 24 小时内安排发货,请您留意物流信息,若有异常我们会第一时间与您联系。"},
{"input": "东西坏了", "output": "您好,得知您遇到产品问题,我深表歉意。请您提供订单号及问题照片,我们将尽快为您安排售后处理,并确保您的权益得到保障。"},
{"input": "怎么联系你们", "output": "您好,您可以通过在线客服、客服热线或官方邮箱与我们联系,我们的工作时间为每日 9:00 至 21:00,随时为您提供服务。"},
{"input": "可以便宜点吗", "output": "您好,感谢您对我们产品的青睐。当前价格已是优惠后的标准售价,如您是会员,还可享受专属折扣,欢迎了解详情。"},
{"input": "我不满意你们的服务", "output": "您好,非常抱歉给您带来了不愉快的体验。请您告知具体情况,我们会认真核实并改进,同时为您提供相应的补偿方案。"},
{"input": "发票怎么开", "output": "您好,您可在订单详情页申请电子发票,提交后通常 1-3 个工作日内开具完成,若需纸质发票请告知地址,我们将为您邮寄。"},
{"input": "我想换货", "output": "您好,感谢您选择我们的产品。请您提供订单号及希望更换的商品信息,我们将为您核实库存并尽快安排换货流程。"},
{"input": "你们有实体店吗", "output": "您好,感谢您的关注。我们在主要城市均设有线下体验店,具体地址可在官网门店查询页面获取,欢迎您到店体验。"},
{"input": "快递太慢了", "output": "您好,非常抱歉物流速度未达预期。请您提供订单编号,我会立即与快递方核实情况,并加急为您处理后续配送。"},
{"input": "能加急吗", "output": "您好,感谢您的耐心。我已将您的订单备注加急,会优先安排发货,如有进展我会第一时间通知您。"},
{"input": "我忘记密码了", "output": "您好,请您点击登录页的'忘记密码',通过绑定的手机号或邮箱进行重置,若仍有疑问可随时联系我,我将协助您完成操作。"},
{"input": "这个怎么用", "output": "您好,感谢您的咨询。该产品的使用说明可在包装内的指南或官网帮助中心查看,如需进一步指导,我可以为您详细讲解。"},
{"input": "我要投诉", "output": "您好,非常重视您的反馈。请您详细描述遇到的问题,我会立即上报并跟进处理结果,确保给您一个满意的答复。"},
{"input": "有优惠券吗", "output": "您好,感谢您的关注。当前新用户注册即可领取专属优惠券,会员日还会有更多福利活动,详情可在活动页面查询。"},
]
dataset = Dataset.from_list(data)
print("客服话术数据准备完成,共", len(data), "条")
这里要特别说明一点:数据量虽然只有 16 条,但通过多轮训练(后面会设置 40 个 epoch)仍然能让模型明显学到风格变化。在真实生产场景中,建议至少准备数百到数千条高质量样本。
五、加载基础模型与分词器
接下来加载 Qwen2.5-0.5B-Instruct。Hugging Face 的 transformers 库提供了非常简洁的 API:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch model_name = "Qwen/Qwen2.5-0.5B-Instruct" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map="auto" )
这里有两个关键参数值得展开说明:
torch_dtype=torch.float16:以半精度(FP16)加载模型权重,相比默认的 FP32 可以节省一半显存。对于推理和 LoRA 微调来说,FP16 的精度损失基本可以忽略。
device_map="auto":让 accelerate 自动决定把模型的不同层放到哪个设备上。如果你只有一张 GPU,整个模型会自动放到 GPU 上;如果显存不够,部分层会被卸载到 CPU 甚至磁盘。
第一次运行时,模型会从 Hugging Face Hub 下载(约 1GB),需要稍等片刻。
六、配置 LoRA:核心超参逐项解读
现在到了 LoRA 微调最关键的一步:用 peft 库给模型"装上"可训练的旁路矩阵。
from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "v_proj"], lora_dropout=0.05, task_type="CAUSAL_LM", ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()
我们逐项来看这些参数的含义:
r=8:低秩矩阵的秩(rank),即前面公式中 A 和 B 矩阵的"瘦"那一维。r 越大,LoRA 的表达能力越强,但参数量也越多。常见取值有 4、8、16、32,对于多数任务,8 是一个性价比很高的默认值。
lora_alpha=16:缩放系数。LoRA 输出在加到原权重之前会乘以 lora_alpha / r,这里就是 16 / 8 = 2。这个值控制 LoRA 对原模型的影响强度,通常设置为 r 的 1~2 倍。
target_modules=["q_proj", "v_proj"]:指定把 LoRA 注入到哪些模块。Transformer 的注意力机制中有四个投影矩阵(Q、K、V、O),原始 LoRA 论文的实验表明,只在 Query 和 Value 投影上加 LoRA,性价比最高。如果想追求更好的效果,可以扩展到所有线性层。
lora_dropout=0.05:在 LoRA 旁路上加一个轻微的 dropout,防止过拟合。
task_type="CAUSAL_LM":告诉 peft 这是一个因果语言模型任务(即 GPT 类自回归生成模型),它会据此自动配置一些细节。
执行 print_trainable_parameters() 后,你会看到类似下面的输出:
trainable params: 540,672 || all params: 494,573,440 || trainable%: 0.1093
整个模型有约 4.94 亿参数,但只有 54 万是可训练的,占比仅 0.11%。这正是 LoRA 的魅力所在。
七、数据预处理:构造符合 Qwen 对话模板的输入
每个对话模型都有自己的"对话模板"。Qwen 系列使用的是 ChatML 格式,用 <|im_start|> 和 <|im_end|> 作为角色边界标记。把训练数据按照模板拼接,模型才能正确学习"用户问 → 助手答"的对话结构。
def format_data(example):
prompt = f"<|im_start|>user\n{example['input']}<|im_end|>\n<|im_start|>assistant\n{example['output']}<|im_end|>"
tokens = tokenizer(prompt, truncation=True, max_length=256, padding="max_length")
tokens["labels"] = tokens["input_ids"].copy()
return tokens
dataset = dataset.map(format_data, remove_columns=["input", "output"])
print("dataset 当前的列:", dataset.column_names)
这段代码有几个值得关注的细节:
模板拼接:把 input 包在 user 角色里,把 output 包在 assistant 角色里,得到完整的一轮对话文本。
truncation=True, max_length=256, padding="max_length":把所有样本统一截断或填充到 256 个 token 的长度,方便后续批量训练。
tokens["labels"] = tokens["input_ids"].copy():这是因果语言建模的标准做法。模型的训练目标是"预测下一个 token",所以 labels 直接复制 input_ids 即可,Trainer 内部会自动做位置偏移。
remove_columns=["input", "output"]:这一步非常关键。如果不删除原始的 input 和 output 列,Trainer 在拼装 batch 时会因为遇到字符串类型而报错。删除后只保留 input_ids、attention_mask、labels 三列纯数值数据,Trainer 就能顺利处理。
八、配置训练参数并启动训练
数据和模型都准备好了,现在来配置训练参数并启动训练。
from transformers import TrainingArguments, Trainer args = TrainingArguments( output_dir="./lora-customer-service", per_device_train_batch_size=2, num_train_epochs=40, learning_rate=2e-4, logging_steps=5, save_strategy="epoch", fp16=True, ) trainer = Trainer(model=model, args=args, train_dataset=dataset) trainer.train()
逐项解读这些训练参数:
output_dir:训练过程中的 checkpoint 和最终权重保存目录。
per_device_train_batch_size=2:每张 GPU 上的 batch size。Qwen2.5-0.5B 模型较小,2 已经够用;如果显存充足可以适当调大。
num_train_epochs=40:训练轮数。我们只有 16 条数据,轮数必须设得足够大,模型才能反复学习这些样本,最终在风格上发生明显变化。如果数据量较大(比如几千条),通常 3-5 个 epoch 就够了。
learning_rate=2e-4:学习率。LoRA 微调的学习率通常比全参数微调高一个数量级,常见范围是 1e-4 到 3e-4。
logging_steps=5:每 5 个 step 打印一次 loss,方便观察训练进度。
save_strategy="epoch":每个 epoch 结束保存一次检查点。
fp16=True:启用半精度训练,进一步节省显存并加速。
训练启动后,你会看到 loss 从最初的 3 左右逐步下降到 1 以下。当 loss 持续下降并趋于平稳时,就说明模型已经"学会"了我们想要的风格。
九、推理验证:对比微调前后的输出差异
训练完成后,把模型切换到推理模式,写一个简单的 chat 函数来测试效果:
model.eval()
def chat(text):
prompt = f"<|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
output = model.generate(
**inputs,
max_new_tokens=80,
do_sample=True,
temperature=0.7
)
return tokenizer.decode(output[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
print(chat("多少钱啊"))
print(chat("我要退款"))
几个关键点说明:
model.eval():切换到评估模式,关闭 dropout 等只在训练时启用的机制。
max_new_tokens=80:最多生成 80 个新 token,避免无限生成。
do_sample=True, temperature=0.7:启用采样生成。temperature 越高生成越多样,越低越确定。0.7 是一个比较自然的取值。
output[0][inputs.input_ids.shape[1]:]:只截取新生成的部分(不包含输入的 prompt),让输出更干净。
微调前,模型对"多少钱啊"这类口语化提问,可能会给出"价格因产品而异,请提供更多信息"这类生硬的回复。微调后,模型会输出类似"您好,感谢您的咨询。请您告知具体的商品名称,我将立即为您查询并提供准确的价格信息"这样的客服式回复——风格的迁移效果立竿见影。
十、常见问题与进阶方向
在实际操作中,新手常会遇到以下几类问题:
版本冲突:transformers、peft、accelerate 三者的版本必须互相兼容。本文锁定的版本组合是经过验证的,如果你想升级,建议查阅 peft 的官方 release notes 确认依赖关系。
显存不足(OOM):可以减小 batch_size、缩短 max_length、启用梯度累积(gradient_accumulation_steps),或改用 QLoRA(4-bit 量化加载)。
Loss 不下降:检查数据格式是否正确(特别是对话模板的拼接),确认 labels 已经设置,检查学习率是否过小。
生成结果不符合预期:尝试增加 epoch、增加数据量、调高 lora_alpha,或扩大 target_modules 的范围。
完成本文的入门实验后,你可以继续探索以下进阶方向:
-
QLoRA:在 LoRA 基础上加 4-bit 量化,能在更小显存下微调更大的模型;
-
合并 LoRA 权重:将 LoRA 权重合并回原模型,导出一个完整的微调后模型,方便部署;
-
多任务/多数据集混合训练:让模型同时具备多种能力;
-
部署推理服务:用 vLLM、TGI、Ollama 等工具把微调后的模型部署为可调用的 API。
十一、完整代码汇总
最后,整理一份可直接复制运行的完整代码:
# === 1. 安装依赖 ===
# pip install -q transformers==4.46.3 peft==0.13.2 accelerate==1.1.1 datasets==3.1.0
# === 2. 构造数据集 ===
from datasets import Dataset
data = [
{"input": "你们家东西怎么这么贵", "output": "您好,非常理解您对价格的关注。我们的产品在选材与工艺上严格把控,致力于为您提供更优质的使用体验,如有需要,可为您介绍当前可享的优惠方案。"},
{"input": "我要退货", "output": "您好,很抱歉给您带来了不便。请您提供订单编号,我将立即为您核实并安排退货流程,确保问题尽快得到妥善解决。"},
{"input": "什么时候发货", "output": "您好,感谢您的耐心等待。订单通常会在付款后的 24 小时内安排发货,请您留意物流信息,若有异常我们会第一时间与您联系。"},
{"input": "东西坏了", "output": "您好,得知您遇到产品问题,我深表歉意。请您提供订单号及问题照片,我们将尽快为您安排售后处理,并确保您的权益得到保障。"},
{"input": "怎么联系你们", "output": "您好,您可以通过在线客服、客服热线或官方邮箱与我们联系,我们的工作时间为每日 9:00 至 21:00,随时为您提供服务。"},
{"input": "可以便宜点吗", "output": "您好,感谢您对我们产品的青睐。当前价格已是优惠后的标准售价,如您是会员,还可享受专属折扣,欢迎了解详情。"},
{"input": "我不满意你们的服务", "output": "您好,非常抱歉给您带来了不愉快的体验。请您告知具体情况,我们会认真核实并改进,同时为您提供相应的补偿方案。"},
{"input": "发票怎么开", "output": "您好,您可在订单详情页申请电子发票,提交后通常 1-3 个工作日内开具完成,若需纸质发票请告知地址,我们将为您邮寄。"},
{"input": "我想换货", "output": "您好,感谢您选择我们的产品。请您提供订单号及希望更换的商品信息,我们将为您核实库存并尽快安排换货流程。"},
{"input": "你们有实体店吗", "output": "您好,感谢您的关注。我们在主要城市均设有线下体验店,具体地址可在官网门店查询页面获取,欢迎您到店体验。"},
{"input": "快递太慢了", "output": "您好,非常抱歉物流速度未达预期。请您提供订单编号,我会立即与快递方核实情况,并加急为您处理后续配送。"},
{"input": "能加急吗", "output": "您好,感谢您的耐心。我已将您的订单备注加急,会优先安排发货,如有进展我会第一时间通知您。"},
{"input": "我忘记密码了", "output": "您好,请您点击登录页的'忘记密码',通过绑定的手机号或邮箱进行重置,若仍有疑问可随时联系我,我将协助您完成操作。"},
{"input": "这个怎么用", "output": "您好,感谢您的咨询。该产品的使用说明可在包装内的指南或官网帮助中心查看,如需进一步指导,我可以为您详细讲解。"},
{"input": "我要投诉", "output": "您好,非常重视您的反馈。请您详细描述遇到的问题,我会立即上报并跟进处理结果,确保给您一个满意的答复。"},
{"input": "有优惠券吗", "output": "您好,感谢您的关注。当前新用户注册即可领取专属优惠券,会员日还会有更多福利活动,详情可在活动页面查询。"},
]
dataset = Dataset.from_list(data)
# === 3. 加载模型与分词器 ===
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model_name = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
# === 4. 配置 LoRA ===
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# === 5. 数据预处理 ===
def format_data(example):
prompt = f"<|im_start|>user\n{example['input']}<|im_end|>\n<|im_start|>assistant\n{example['output']}<|im_end|>"
tokens = tokenizer(prompt, truncation=True, max_length=256, padding="max_length")
tokens["labels"] = tokens["input_ids"].copy()
return tokens
dataset = dataset.map(format_data, remove_columns=["input", "output"])
# === 6. 训练 ===
from transformers import TrainingArguments, Trainer
args = TrainingArguments(
output_dir="./lora-customer-service",
per_device_train_batch_size=2,
num_train_epochs=40,
learning_rate=2e-4,
logging_steps=5,
save_strategy="epoch",
fp16=True,
)
trainer = Trainer(model=model, args=args, train_dataset=dataset)
trainer.train()
# === 7. 推理验证 ===
model.eval()
def chat(text):
prompt = f"<|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
output = model.generate(**inputs, max_new_tokens=80, do_sample=True, temperature=0.7)
return tokenizer.decode(output[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
print(chat("多少钱啊"))
print(chat("我要退款"))
到这里,整个 LoRA 微调 Qwen2.5-0.5B-Instruct 的完整流程就跑通了。希望这篇文章能帮你建立起对 LoRA 微调的整体认知。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)