当前位置: 首页 > news >正文

Qwen3 LoRA 微调指南:Alpaca 格式 + PEFT + RK3588 部署

本文介绍基于 Qwen3 的 LoRA 微调通用方法,适用于意图识别、文本分类、指令跟随等场景。采用 Alpaca 格式数据、PEFT 框架,支持从数据准备到 RK3588 边缘部署的完整流程。

目录

一、方案概述

1.1 适用场景

1.2 技术栈

二、项目结构

三、数据准备

3.1 Alpaca 格式

3.2 生成训练数据

四、环境搭建

4.1 硬件要求

4.2 依赖安装

五、Lora微调

5.1 快速启动

5.2 手动指定参数

5.3 训练参数说明

5.4 技术要点

5.5 完整代码

六、Lora合并

6.1 完整代码

七、RK3588 部署(可选)

7.1 前置条件

7.2 导出步骤

7.3 部署

7.4 转换说明

八、完整流程总结


一、方案概述

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,包含三个字段:

字段类型说明
instructionstring系统提示(任务说明、规则、输出格式)
inputstring用户输入
outputstring期望输出

示例 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训练数据路径
--modelQwen/Qwen3-0.6B-Base基座模型(HuggingFace 名称或本地路径)
--output./output输出目录
--epochs3训练轮数
--batch-size4每设备 batch 大小
--lr2e-5学习率
--max-length768最大序列长度
--lora-r16LoRA rank
--lora-alpha32LoRA alpha
--lora-dropout0.05LoRA dropout
--val-split0.1验证集比例
--bf16True使用 BF16
--gradient-checkpointingTrue梯度检查点(省显存)

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一致)
--loraLoRA 权重目录(训练输出目录)
--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
原生 Qwen3HuggingFace 原始 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 导出 → 边缘部署


http://www.jsqmd.com/news/466159/

相关文章:

  • 大模型:RAG基础介绍
  • minio社区版本的精简问题
  • 麻省理工研发复杂视觉任务AI规划新方法,成功率提升至70%
  • 2026必备!AI论文网站 千笔 VS 灵感风暴AI,本科生写作神器!
  • 螺钉/螺丝等五金件的自动化排列与研磨抛光:前置整列的技术价值
  • 一个5V电源 1个12V电源 提供不同电压给电路板 2个电源共地 是5V的负极 跟 12V的负极接在一起 接gnd吗?
  • 深入浅出LC滤波器:从原理设计到实战
  • 在内容审核、网络安全、AI对话监管等领域,敏感词和敏感对话的差异
  • 老王-快乐到死的5个顶级思维
  • 2026年武汉房屋检测公司权威排名与选购指南 - 2026年企业推荐榜
  • OSPF考题
  • 2026别错过!AI论文网站千笔AI VS 灵感ai,研究生写作神器!
  • 2026年质量好的门窗品牌推荐:高档门窗/浙江系统门窗/定制系统门窗热门厂家推荐汇总 - 行业平台推荐
  • 真人实录:做完筋膜提升多久恢复、做完筋膜提升注意事项~
  • 2026年 卷发棒品牌推荐排行榜,自动/负离子/便携/直卷两用/智能温控/多功能/快速加热/纳米水离子/陶瓷/不伤发卷发棒,护发造型神器精选指南 - 品牌企业推荐师(官方)
  • 老王-来时一丝不挂
  • 2026 AI产业全景解析:国内外模型争霸,内容生产迎来智能革命
  • 中山豪车维修优质机构推荐榜:豪华汽车维修/24小时市道路救援/新能源汽车维修/汽车维修保养/汽车维修发动机/汽车维修换油保养/选择指南 - 优质品牌商家
  • AI智能体威力巨大,厂商正在开发工具修复它们对基础设施的破坏
  • 2026年药物制剂虚拟仿真软件厂家推荐榜:教学实训系统、模拟药厂仿真平台与高校课程解决方案深度解析 - 品牌企业推荐师(官方)
  • 谷歌编程之夏 2026:如何为时序数据库 Apache IoTDB 撰写优秀提案?
  • 阿里、字节面试必问:MySQL 索引失效的 8 种场景,这次彻底搞懂!
  • 如何让你的龙虾更智能
  • 【高清视频】介绍一个自动化测试辅助小工具 - 上下电测试适用于电脑冷启动的掉电盒
  • 新中地GIS开发特训营2505期正式结业|一份超全GIS开发学习内容清单请查收
  • openclaw配置免费千问模型
  • spring cloud eureka打包教程
  • 机器人设计与应用综合实训——ESP32开发技术分享3.11
  • 第19届CISCN_pwn_typo 小白初探
  • 自建docker镜像仓库