Qwen3 LoRA 微调指南:Alpaca 格式 + PEFT + RK3588 部署
本文介绍基于 Qwen3 的 LoRA 微调通用方法,适用于意图识别、文本分类、指令跟随等场景。采用 Alpaca 格式数据、PEFT 框架,支持从数据准备到 RK3588 边缘部署的完整流程。
目录
一、方案概述
1.1 适用场景
- 意图识别:用户输入 → 模型输出结构化标签
- 文本分类:多分类、多标签任务
- 指令跟随:按特定格式输出(如 JSON、列表)
- 领域适配:将通用模型适配到特定领域
1.2 技术栈
| 组件 | 说明 |
|---|---|
| 基座模型 | Qwen3-0.6B(可替换为其他 Qwen 系列) |
| 微调方式 | LoRA(低秩适配) |
| 数据格式 | Alpaca(instruction / input / output) |
| 训练框架 | PEFT + TRL SFTTrainer |
| 部署 | HuggingFace 格式 / RKLLM(RK3588) |
二、项目结构
project/
├── dataset/ # 数据准备
│ ├── generate_alpaca_dataset.py # 自定义数据 → Alpaca JSONL
│ └── train_alpaca.jsonl # 训练数据
├── finetune/ # 微调训练
│ ├── train.py # LoRA 微调主脚本
│ ├── train.sh # 训练启动脚本
│ ├── merge_lora.py # LoRA 合并
│ └── requirements_finetune.txt # 依赖
└── merged_model/ # 合并后模型(输出)
三、数据准备
3.1 Alpaca 格式
每行一条 JSON,包含三个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
instruction |
string | 系统提示(任务说明、规则、输出格式) |
input |
string | 用户输入 |
output |
string | 期望输出 |
示例 1:意图识别
{
"instruction": "你是意图识别助手。根据用户输入,从类别列表中选择匹配项,以 JSON 列表格式输出。类别:['查询', '办理', '咨询']。无匹配时输出 []。",
"input": "我想查一下余额",
"output": "[\"查询\"]"
}
示例 2:文本分类
{
"instruction": "对以下文本进行情感分类,输出:正面/负面/中性。",
"input": "这个产品非常好用。",
"output": "正面"
}
示例 3:指令跟随
{
"instruction": "将用户输入转为 JSON 格式,包含 key 和 value。",
"input": "姓名:张三,年龄:25",
"output": "{\"姓名\": \"张三\", \"年龄\": \"25\"}"
}
3.2 生成训练数据
可以选择从 excel 中读取,或者直接编写JSONL。
Excel 格式:至少包含 `input` 列和 `output` 列(列名可配置)。
也可手动编写 JSONL,每行一条 Alpaca 样本:
# 示例
echo '{"instruction":"...","input":"...","output":"..."}' >> train_alpaca.jsonl
四、环境搭建
4.1 硬件要求
- GPU:单卡 24GB+(如 A10、V100、RTX 4090)
- 系统:Linux 推荐,Windows 需 WSL2 或 CUDA 环境
4.2 依赖安装
cd finetune
pip install -r requirements_finetune.txt
requirements_finetune.txt:
# 微调依赖(需在 GPU 服务器上安装,全量训练无需 bitsandbytes)
torch>=2.0.0
transformers>=4.45.0
peft>=0.10.0
trl>=0.8.0
datasets>=2.14.0
accelerate>=0.25.0
sentencepiece
protobuf
五、Lora微调
5.1 快速启动
cd finetune
./train.sh
或使用环境变量覆盖:
DATA=../dataset/train_alpaca.jsonl OUTPUT=./output MODEL=Qwen/Qwen3-0.6B-Base ./train.sh
5.2 手动指定参数
python train.py \
--data ../dataset/train_alpaca.jsonl \
--model Qwen/Qwen3-0.6B-Base \
--output ./output \
--epochs 3 \
--batch-size 4 \
--lr 2e-5 \
--max-length 768 \
--lora-r 16 \
--lora-alpha 32 \
--lora-dropout 0.05 \
--val-split 0.1 \
--bf16 \
--gradient-checkpointing
5.3 训练参数说明
| 参数 | 默认值 | 说明 |
|---|---|---|
--data |
../dataset/train_alpaca.jsonl | 训练数据路径 |
--model |
Qwen/Qwen3-0.6B-Base | 基座模型(HuggingFace 名称或本地路径) |
--output |
./output | 输出目录 |
--epochs |
3 | 训练轮数 |
--batch-size |
4 | 每设备 batch 大小 |
--lr |
2e-5 | 学习率 |
--max-length |
768 | 最大序列长度 |
--lora-r |
16 | LoRA rank |
--lora-alpha |
32 | LoRA alpha |
--lora-dropout |
0.05 | LoRA dropout |
--val-split |
0.1 | 验证集比例 |
--bf16 |
True | 使用 BF16 |
--gradient-checkpointing |
True | 梯度检查点(省显存) |
5.4 技术要点
- LoRA 目标模块:`q_proj`、`k_proj`、`v_proj`、`o_proj`
- 格式转换:Alpaca → ChatML(`<|im_start|>system/user/assistant`)
- Loss 计算:`completion_only_loss=True`,仅对 assistant 回复计算 loss
- 梯度累积:4 步,等效 batch_size=16
5.5 完整代码
train.sh
#!/bin/bash
# LoRA 微调启动脚本
# 需在 GPU 服务器上执行,建议单卡 24GB+
set -e
cd "$(dirname "$0")"
DATA="${DATA:-../dataset/train_alpaca.jsonl}"
OUTPUT="${OUTPUT:-./output}"
MODEL="${MODEL:-Qwen/Qwen3-0.6B-Base}"
echo "数据: $DATA"
echo "模型: $MODEL"
echo "输出: $OUTPUT"
python train.py \
--data "$DATA" \
--model "$MODEL" \
--output "$OUTPUT" \
--epochs 3 \
--batch-size 4 \
--lr 2e-5 \
--max-length 768 \
--lora-r 16 \
--lora-alpha 32 \
--lora-dropout 0.05 \
--val-split 0.1 \
--bf16 \
--gradient-checkpointing
train.py
"""
Qwen3-0.6B 业务意图识别 LoRA 微调脚本
使用 Alpaca 格式数据,输出可用于 RKLLM 导出的模型
兼容 trl 0.29+(使用 prompt-completion 格式,无需 DataCollatorForCompletionOnlyLM)
"""
import argparse
import json
from pathlib import Path
import torch
from datasets import Dataset
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer
from trl import SFTConfig, SFTTrainer
def load_alpaca_dataset(path: str) -> Dataset:
"""加载 Alpaca 格式 JSONL 数据"""
data = []
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
data.append(json.loads(line))
return Dataset.from_list(data)
def alpaca_to_prompt_completion(example: dict) -> dict:
"""将 Alpaca 样本转为 prompt-completion 格式(trl 0.29+ 仅对 completion 计算 loss)"""
instruction = example.get("instruction", "")
user_input = example.get("input", "")
output = example.get("output", "")
prompt = (
f"<|im_start|>system\n{instruction}<|im_end|>\n"
f"<|im_start|>user\n{user_input}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
completion = f"{output}<|im_end|>"
return {"prompt": prompt, "completion": completion}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--data", default="../dataset/train_alpaca.jsonl", help="训练数据 JSONL 路径")
parser.add_argument("--model", default="Qwen/Qwen3-0.6B-Base", help="基座模型路径或 HuggingFace 名称")
parser.add_argument("--output", default="./output", help="输出目录")
parser.add_argument("--epochs", type=int, default=3)
parser.add_argument("--batch-size", type=int, default=4)
parser.add_argument("--lr", type=float, default=2e-5)
parser.add_argument("--max-length", type=int, default=768)
parser.add_argument("--lora-r", type=int, default=16)
parser.add_argument("--lora-alpha", type=int, default=32)
parser.add_argument("--lora-dropout", type=float, default=0.05)
parser.add_argument("--bf16", action="store_true", default=True)
parser.add_argument("--gradient-checkpointing", action="store_true", default=True)
parser.add_argument("--val-split", type=float, default=0.1, help="验证集比例 0~1")
args = parser.parse_args()
data_path = Path(args.data)
if not data_path.exists():
raise FileNotFoundError(f"数据文件不存在: {data_path}")
print("加载 tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(
args.model,
trust_remote_code=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
print("加载数据集...")
dataset = load_alpaca_dataset(str(data_path))
dataset = dataset.map(alpaca_to_prompt_completion, remove_columns=dataset.column_names, desc="转为 prompt-completion")
if args.val_split > 0:
split = dataset.train_test_split(test_size=args.val_split, seed=42)
train_dataset = split["train"]
eval_dataset = split["test"]
print(f"训练集: {len(train_dataset)}, 验证集: {len(eval_dataset)}")
else:
train_dataset = dataset
eval_dataset = None
print("加载模型(全量训练,无量化)...")
torch_dtype = torch.bfloat16 if args.bf16 else torch.float16
model = AutoModelForCausalLM.from_pretrained(
args.model,
torch_dtype=torch_dtype,
device_map="auto",
trust_remote_code=True,
)
if args.gradient_checkpointing:
model.enable_input_require_grads()
lora_config = LoraConfig(
r=args.lora_r,
lora_alpha=args.lora_alpha,
lora_dropout=args.lora_dropout,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
sft_config = SFTConfig(
output_dir=args.output,
num_train_epochs=args.epochs,
per_device_train_batch_size=args.batch_size,
per_device_eval_batch_size=args.batch_size,
gradient_accumulation_steps=4,
learning_rate=args.lr,
warmup_ratio=0.1,
logging_steps=10,
save_strategy="epoch",
eval_strategy="epoch" if eval_dataset else "no",
bf16=args.bf16,
gradient_checkpointing=args.gradient_checkpointing,
max_length=args.max_length,
completion_only_loss=True,
)
trainer = SFTTrainer(
model=model,
args=sft_config,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
processing_class=tokenizer,
)
print("开始训练...")
trainer.train()
trainer.save_model(args.output)
tokenizer.save_pretrained(args.output)
print(f"模型已保存到 {args.output}")
if __name__ == "__main__":
main()
六、Lora合并
训练完成后,将 LoRA 权重合并到基座模型,得到完整模型用于推理或导出:
python merge_lora.py \
--base Qwen/Qwen3-0.6B-Base \
--lora ./output \
--output ./merged_model
| 参数 | 说明 |
|---|---|
--base |
基座模型路径(与训练时 --model 一致) |
--lora |
LoRA 权重目录(训练输出目录) |
--output |
合并后模型保存路径 |
合并后的模型可直接用于 HuggingFace 推理或后续格式转换。
6.1 完整代码
merge_lora.py
"""
合并 LoRA 权重到基座模型
合并后的模型可用于 RKLLM 导出或本地推理
"""
import argparse
from pathlib import Path
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--base", required=True, help="基座模型路径(与训练时 --model 一致)")
parser.add_argument("--lora", required=True, help="LoRA 权重目录(训练输出目录)")
parser.add_argument("--output", required=True, help="合并后模型保存路径")
args = parser.parse_args()
print("加载基座模型...")
model = AutoModelForCausalLM.from_pretrained(
args.base,
device_map="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained(args.base, trust_remote_code=True)
print("加载并合并 LoRA...")
model = PeftModel.from_pretrained(model, args.lora)
model = model.merge_and_unload()
output_path = Path(args.output)
output_path.mkdir(parents=True, exist_ok=True)
print(f"保存合并模型到 {output_path}...")
model.save_pretrained(output_path)
tokenizer.save_pretrained(output_path)
print("完成")
if __name__ == "__main__":
main()
七、RK3588 部署(可选)
若需部署到 RK3588 等边缘设备,需将合并后的模型转为 RKLLM 格式。
可参考我之前的文章:【RK芯片学习笔记】RK3588开发板上大语言模型转换教程
7.1 前置条件
1. 已完成 LoRA 合并
2. 安装 [RKLLM-Toolkit2](https://github.com/rockchip-linux/rknn-toolkit2)
7.2 导出步骤
# 参考 RKLLM 官方文档
python convert.py \
--model_path ./merged_model \
--output_path ./output.rkllm \
--quantization W8A8 \
--target-platform rk3588
具体参数以 RKLLM-Toolkit2 当前版本为准。
7.3 部署
将 `.rkllm` 文件拷贝到 RK3588,配置 RKLLM 服务加载该模型即可。
7.4 转换说明
直接使用官方提供的转换脚本会出现 'list' has no attribute 'keys' 报错,核心区别在于:原生模型和 LoRA 合并后的模型,tokenizer 的保存方式不同。
原生 Qwen3(HuggingFace 直接下载)
- tokenizer_config.json 是 HuggingFace 官方仓库里的原始版本
- 这些字段的格式通常和 RKLLM 预期一致(例如 added_tokens_decoder 为 dict)
- RKLLM 很可能就是针对这种格式开发的,所以解析正常
LoRA 合并后的模型
- 合并流程是:merge_lora.py 里用 tokenizer.save_pretrained(output_path) 重新保存 tokenizer
- 保存时用的是 transformers 的序列化逻辑,而不是 HuggingFace 仓库里的原始格式
不同 transformers 版本在序列化时可能:
- 把 added_tokens_decoder 写成 [{...}, {...}](list)
- 把 chat_template 写成 list 形式
- 把 auto_map 写成 list 等
RKLLM 在解析时假设这些字段是 dict,会调用 .keys(),遇到 list 就会报错 'list' has no attribute 'keys'。
| 场景 | tokenizer 来源 | 是否需要 fix |
|---|---|---|
| 原生 Qwen3 | HuggingFace 原始 tokenizer_config | 一般不需要 |
| LoRA 合并后 | 通过 tokenizer.save_pretrained() 重新保存 | 需要 |
因此,需要在代码中添加fix_config_for_rkllm函数,fix_config_for_rkllm 的作用是:把 transformers 重新保存后的 tokenizer 配置,转成 RKLLM 能正确解析的格式,避免在解析 list 时调用 .keys() 导致报错。
完整代码如下:
from rkllm.api import RKLLM
import os
import json
os.environ['CUDA_VISIBLE_DEVICES']='0'
'''
https://huggingface.co/Qwen/Qwen3-0.6B
'''
modelpath = '/home/gwi/yy_workspace/llm_fine_tuning_260226/finetune/merged_model'
llm = RKLLM()
def fix_config_for_rkllm(model_dir):
"""修复 tokenizer_config 中 list 字段,避免 RKLLM 解析时 'list' has no attribute 'keys'"""
path = os.path.join(model_dir, 'tokenizer_config.json')
if not os.path.exists(path):
return
with open(path, 'r', encoding='utf-8') as f:
cfg = json.load(f)
changed = False
for k in ['chat_template', 'added_tokens_decoder', 'extra_special_tokens', 'auto_map']:
if k in cfg and isinstance(cfg[k], list):
cfg[k] = {} if k != 'chat_template' else ''
changed = True
if changed:
with open(path, 'w', encoding='utf-8') as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
if os.path.exists(modelpath):
fix_config_for_rkllm(modelpath)
# Load model
# Use 'export CUDA_VISIBLE_DEVICES=0' to specify GPU device
# device options ['cpu', 'cuda']
# dtype options ['float32', 'float16', 'bfloat16']
# Using 'bfloat16' or 'float16' can significantly reduce memory consumption but at the cost of lower precision
# compared to 'float32'. Choose the appropriate dtype based on your hardware and model requirements.
ret = llm.load_huggingface(model=modelpath, model_lora = None, device='cuda', dtype="float32", custom_config=None, load_weight=True)
# ret = llm.load_gguf(model = modelpath)
if ret != 0:
print('Load model failed!')
exit(ret)
# Build model
dataset = "./data_quant.json"
# Json file format, please note to add prompt in the input,like this:
# [{"input":"Human: 你好!\nAssistant: ", "target": "你好!我是人工智能助手KK!"},...]
# Different quantization methods are optimized for different algorithms:
# w8a8/w8a8_gx is recommended to use the normal algorithm.
# w4a16/w4a16_gx is recommended to use the grq algorithm.
qparams = None # Use extra_qparams
target_platform = "RK3588"
optimization_level = 1
quantized_dtype = "W8A8"
quantized_algorithm = "normal"
num_npu_core = 3
ret = llm.build(do_quantization=True, optimization_level=optimization_level, quantized_dtype=quantized_dtype,
quantized_algorithm=quantized_algorithm, target_platform=target_platform, num_npu_core=num_npu_core, extra_qparams=qparams, dataset=dataset, hybrid_rate=0, max_context=4096)
if ret != 0:
print('Build model failed!')
exit(ret)
# Export rkllm model
ret = llm.export_rkllm(f"./{os.path.basename(modelpath)}_{quantized_dtype}_{target_platform}.rkllm")
if ret != 0:
print('Export model failed!')
exit(ret)
八、完整流程总结
1. 准备 Alpaca 格式数据(instruction / input / output)
↓
2. train_alpaca.jsonl
↓
3. python train.py → output/(LoRA 权重)
↓
4. python merge_lora.py → merged_model/
↓
5. 本地推理 / RKLLM 导出 → 边缘部署
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)