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 是原模型的权重(被冻结,不参与训练);

  • AB 是两个新增的低秩矩阵,维度远小于 W

  • B × A 的结果与 W 同形,相当于对 W 的"补丁"。

举个具体的例子:如果 W 是一个 4096 × 4096 的矩阵(约 1678 万参数),我们让 A 的形状是 8 × 4096B 的形状是 4096 × 8,那么 AB 加起来只有约 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),即前面公式中 AB 矩阵的"瘦"那一维。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"]:这一步非常关键。如果不删除原始的 inputoutput 列,Trainer 在拼装 batch 时会因为遇到字符串类型而报错。删除后只保留 input_idsattention_masklabels 三列纯数值数据,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-43e-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 微调的整体认知。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐