【光照】UnityURP[泛光Bloom]原理与实现

蛋萌际秸实上,上手学习大模型、人工智能相关的开发并没有什么太过高深的门槛,真的很简单,真的就是【有手就行】。

二、大模型微调概述

微调(Fine-tuning)有很多种不同的方法,但是使用的场景以及代价也都是不一样的。作为一个没什么资源(数据缺缺,GPU缺缺)的普通人来说,考虑的肯定是低成本方案。

方法类型 参数更新范围 计算成本 适用场景 典型工具框架

全参数微调 全部参数 极高 大数据集、高资源场景 Hugging Face Transformers

Adapter Tuning 适配器参数 低 多任务、资源受限 AdapterHub、PEFT

LoRA/QLoRA 低秩矩阵参数 极低 大模型单卡微调、小样本 LoRA、QLoRA(PEFT 库集成)

指令微调 全量 / 部分参数 中 - 高 通用对话模型、多任务泛化 Alpaca-LoRA、FastChat

领域适配微调 全量 / 部分参数 中 垂直领域任务 自定义领域数据集 + Transformers

三、LoRA微调全流程

前阵子在将小落同学项目的智能体代码摘成独立的OddAgent项目时,实践的是一个会议相关的语音助手功能,该功能有针对Qwen2.5-0.5B-Instruct模型和Qwen3-4B-Instruct-2507这两个模型重点做了一些测试和验证,用的就是其中成本最低的LoRA微调。

最后跑下来Qwen3-4B-Instruct-2507的效果要显著好于Qwen2.5-0.5B-Instruct(有同时针对这两个模型用同一套数据集去做了LoRA微调)。

因此,本文的重点就放在了Qwen2.5-0.5B-Instruct的LoRA微调上,因为后面我还准备再继续针对这个模型再补充一些训练集来做一下微调,目标是在这个模型上也能做到100%的意图/槽位准确率。

跟之前训练大模型一样,还是在我家里的这个10年前的老笔记本上进行的。

硬件配置:CPU: i7-8850H CPU @ 2.60GHz+16G内存

微调训练时长:约50分钟(由于训练时,还在用这个笔记本上网课,所以时长仅供参考)

  1. 创建虚拟环境

为了不影响现有的python环境,建议为这个LoRA训练单独创建一个环境。个人习惯用conda(环境可复用,节省硬盘空间),venv或者vu也都一样,全看大家的个人喜好。

conda create -n lora python=3.12 -y

conda activate lora

  1. 模型下载

下载模型前请注意一下你的硬盘空间,整个模型需要955M的硬盘空间,如果你跟我一样常年硬盘空间都是严重不足的,请视情况清理一下空间,避免下载失败。

pip install modelscope --index https://pypi.mirrors.ustc.edu.cn/simple

modelscope download --model Qwen/Qwen2.5-0.5B-Instruct

下载下来后,会保存到指定的.cache这个目录下。如果是Windows,默认是在 C:\Users\Administrator\.cache 目录下,如果是Linux/Mac则是在 .cache 目录下。

其中:

config.json 定义模型结构

tokenizer.json 定义文本输入方式

safetensors 文件则存储模型的参数权重。

  1. 微调环境安装

call pip install torch --index-url https://download.pytorch.org/whl/cu121

call pip install transformers accelerate bitsandbytes peft datasets trl streamlit sentencepiece tensorboard

  1. 数据库集准备

训练需要你预先准备好你用于训练的数据集,但是。。。凡事总会有一个"但是",今天的"但是"是:

要数据?对不起,没有!

换成几年前,哥必须对这个"但是"暴跳如雷,然而,今天有了大模型,腰不酸了,背不疼了,雷没有了,直接用其他大模型自动为每个意图生成50条数据,提示词略,格式如下:

{

"instruct": "启动工作会议",

"input": "",

"output": {

"tool_name": "INSTANT_MEETING",

"parameters": {

"meeting_name": "工作会议"

}

}

},

{

"instruct": "创建汇报会会议",

"input": "",

"output": {

"tool_name": "INSTANT_MEETING",

"parameters": {

"meeting_name": "汇报会"

}

}

},

intruct为用户指令,ouput字段为期望输出

  1. LoRA微调代码

1)微调训练代码

我的完整代码,代码文件名:train_lora.py

"""

Qwen2.5-0.5B-Instruct LoRA/QLoRA 微调脚本

该脚本用于对 Qwen2.5-0.5B-Instruct 模型进行低秩适应(LoRA)或量化低秩适应(QLoRA)微调

"""

import torch

from peft import TaskType, LoraConfig, get_peft_model, prepare_model_for_kbit_training

from datasets import Dataset

import pandas as pd

from transformers import (

AutoTokenizer, AutoModelForCausalLM,

DataCollatorForSeq2Seq, TrainingArguments,

Trainer, GenerationConfig

)

定义要微调的基础模型名称

TRAINING_MODEL = "Qwen/Qwen2.5-0.5B-Instruct"

def load_dataset_json(path):

"""

加载JSON格式的数据集

:param path: 数据集文件的完整路径

:return: 转换为Hugging Face Dataset格式的数据集

"""

使用pandas读取JSON文件

df = pd.read_json(path)

将pandas DataFrame转换为Hugging Face Dataset

ds = Dataset.from_pandas(df)

return ds

def dataset_preprocess(ds):

"""

对数据集进行预处理,包括加载分词器和处理数据样本

:param ds: Hugging Face Dataset格式的原始数据集

:return: 预处理后的数据集和使用的分词器

"""

加载预训练分词器

use_fast=False: 使用慢速分词器,支持更复杂的文本处理

trust_remote_code=True: 信任模型提供的自定义代码

tokenizer = AutoTokenizer.from_pretrained(TRAINING_MODEL, use_fast=False, trust_remote_code=True)

def process_func(example):

"""

处理单个数据样本的内部函数

:param example: 单个数据样本,包含instruct、input和output字段

:return: 处理后的样本,包含input_ids、attention_mask和labels

"""

MAX_LENGTH = 384 # 最大序列长度限制

input_ids, attention_mask, labels = [], [], []

构建指令部分的输入,使用模型要求的对话格式

instruction = tokenizer(

f"<|im_start|>system\n现在你要扮演会议语音助手<|im_end|>\n<|im_start|>user\n{example['instruct'] + example['input']}<|im_end|>\n<|im_start|>assistant\n",

add_special_tokens=False # 不自动添加特殊标记,因为我们已经手动添加

)

构建响应部分的输入

response = tokenizer(f"{example['output']}", add_special_tokens=False)

合并指令和响应的token ids,并添加pad_token作为结束

input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]

合并注意力掩码,pad_token位置设置为1表示需要关注

attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]

构建标签:指令部分用-100屏蔽(不参与损失计算),响应部分保留实际token id

labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]

截断超过最大长度的序列

if len(input_ids) > MAX_LENGTH:

input_ids = input_ids[:MAX_LENGTH]

attention_mask = attention_mask[:MAX_LENGTH]

labels = labels[:MAX_LENGTH]

return {

"input_ids": input_ids,

"attention_mask": attention_mask,

"labels": labels

}

应用处理函数到整个数据集,并移除原始列名

tokenized_id = ds.map(process_func, remove_columns=ds.column_names)

解码并打印第一个样本的输入,用于调试

tokenizer.decode(tokenized_id[0]['input_ids'])

解码并打印第二个样本的标签(过滤掉-100),用于调试

tokenizer.decode(list(filter(lambda x: x != -100, tokenized_id[1]["labels"])))

return tokenized_id, tokenizer

def train(tokenized_id, tokenizer, cpu=True):

"""

执行模型微调的主函数

:param tokenized_id: 预处理后的数据集

:param tokenizer: 分词器

:param cpu: 是否使用CPU进行训练,默认为True(CPU训练)

"""

根据是否使用CPU选择数据类型

CPU训练使用bfloat16,GPU训练使用float16

dtype = torch.bfloat16 if cpu else torch.float16

加载预训练模型

device_map="auto": 自动分配模型到可用设备(CPU或GPU)

torch_dtype=dtype: 设置模型的数据类型

model = AutoModelForCausalLM.from_pretrained(TRAINING_MODEL, device_map="auto", torch_dtype=dtype)

启用输入梯度检查点,减少内存使用

model.enable_input_require_grads()

配置LoRA参数

config = LoraConfig(

task_type=TaskType.CAUSAL_LM, # 任务类型为因果语言模型

指定要微调的模型模块

target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],

inference_mode=False, # 训练模式(True为推理模式)

r=8, # LoRA秩,控制适配器的维度

lora_alpha=32, # LoRA缩放因子,通常为r的4倍

lora_dropout=0.1 # Dropout比例,防止过拟合

)

应用LoRA配置到模型

model = get_peft_model(model, config)

配置训练参数

args = TrainingArguments(

output_dir=f"./output/{TRAINING_MODEL}_lora", # 模型输出目录

per_device_train_batch_size=4, # 每个设备的训练批量大小

gradient_accumulation_steps=4, # 梯度累积步数,实际批量大小=4*4=16

logging_steps=10, # 每10步记录一次日志

num_train_epochs=8, # 训练轮数

save_steps=100, # 每100步保存一次模型

learning_rate=1e-4, # 学习率

save_on_each_node=True, # 在每个节点上保存模型

gradient_checkpointing=True, # 启用梯度检查点,减少内存使用

)

创建训练器

trainer = Trainer(

model=model, # 要训练的模型

args=args, # 训练参数

train_dataset=tokenized_id, # 训练数据集

数据整理器,用于处理批量数据

data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),

)

开始训练

trainer.train()

def combine_and_save_models(cpu=True):

"""

合并LoRA适配器和基础模型,并保存合并后的模型

:param cpu: 是否使用CPU进行模型合并,默认为True

"""

导入必要的库(函数内部导入,避免不必要的依赖加载)

from transformers import AutoModelForCausalLM, AutoTokenizer

import torch

from peft import PeftModel

根据是否使用CPU选择数据类型

dtype = torch.bfloat16 if cpu else torch.float16

model_path = TRAINING_MODEL

LoRA检查点路径(假设是最后一个保存的检查点)

lora_path = f'./output/{TRAINING_MODEL}_lora/checkpoint-100'

加载分词器

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

加载预训练模型(评估模式)

model = AutoModelForCausalLM.from_pretrained(

model_path,

device_map="auto", # 自动分配设备

torch_dtype=dtype, # 设置数据类型

trust_remote_code=True # 信任自定义代码

).eval() # 设置为评估模式

加载LoRA适配器并合并到基础模型

model = PeftModel.from_pretrained(model, model_id=lora_path)

model = model.merge_and_unload() # 合并权重并卸载peft封装

保存合并后的模型

merged_model_path = f'./merged_{TRAINING_MODEL}_lora'

model.save_pretrained(merged_model_path)

tokenizer.save_pretrained(merged_model_path)

print(f"Merged model saved to {merged_model_path}")

主程序入口

if name == "main":

print("Start to train...")

训练数据集路径

training_dataset = "./data/train.json"

加载数据集

ds = load_dataset_json(training_dataset)

print(f'dataset loaded, train size: {len(ds)}, {ds[0:3]}')

预处理数据集

tokenized_id, tokenizer = dataset_preprocess(ds)

print(f'dataset preprocessed, start to run training...')

执行训练

train(tokenized_id, tokenizer, cpu=True)

合并并保存模型

print("Start to combine and save models...")

combine_and_save_models(cpu=True)

print("Done")

2)微调训练

python train_lora.py

整个微调训练在我这个10年前的老笔记本电脑上用CPU跑,总共花了2936.4626秒(约48.94分钟)。所以如果你也是在一个老电脑上跑的话,执行了训练后可以自己去玩一会儿了,回来可能就训练完成了。

3)微调输出文件结构

微调完成后,输出目录结构如下图所示:

其中:

微调后的参数权重:位于output\Qwen\Qwen2.5-0.5B-Instruct_lora目录下。

合并后的模型:位于merged_Qwen\Qwen2.5-0.5B-Instruct_lora目录下。

4)推理验证

微调完成后,可以用如下代码进行推理验证。

我的测试程序完整代码。文件名:test_lora.py。

from transformers import AutoModelForCausalLM, AutoTokenizer

import torch

from peft import PeftModel

import argparse # 添加argparse模块用于命令行参数解析

model_path = 'Qwen/Qwen2.5-0.5B-Instruct'

lora_path = './output/Qwen/Qwen2.5-0.5B-Instruct_lora/checkpoint-100'

def test(instruct, cpu=True):

"""

测试微调后的LoRA模型

:param instruct: 用户指令内容,通过命令行传入

:param cpu: 是否使用CPU进行推理,默认为True

"""

加载tokenizer

tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)

根据设备选择数据类型和设备映射

if cpu:

dtype = torch.float16

device_map = "cpu" # 强制使用CPU

device = "cpu"

else:

dtype = torch.bfloat16

device_map = "auto" # 自动分配设备

device = "cuda" if torch.cuda.is_available() else "cpu"

加载基础模型

model = AutoModelForCausalLM.from_pretrained(

model_path,

device_map=device_map, # 使用明确的设备映射

dtype=dtype,

trust_remote_code=True

).eval() # 设置为评估模式

加载LoRA权重

model = PeftModel.from_pretrained(model, model_id=lora_path)

测试对话

inputs = tokenizer.apply_chat_template(

{"role": "user", "content": "你是一个智能会议语音助手,请根据用户指令输出正确的指令和参数"}, {"role": "user", "content": instruct} \], add_generation_prompt=True, tokenize=True, return_tensors="pt", return_dict=True ) # 将输入移动到与模型相同的设备 inputs = inputs.to(device) # 生成配置 gen_kwargs = {"max_length": 2500, "do_sample": True, "top_k": 1} # 生成回复 with torch.no_grad(): outputs = model.generate(\*\*inputs, \*\*gen_kwargs) # 提取生成的部分 outputs = outputs\[:, inputs\['input_ids'\].shape\[1\]:

解码并打印结果

print(tokenizer.decode(outputs[0], skip_special_tokens=True))

def parse_args():

"""

解析命令行参数

:return: 解析后的参数对象

"""

parser = argparse.ArgumentParser(description="测试LoRA微调后的Qwen2.5-0.5B-Instruct模型")

必选参数:用户指令

parser.add_argument("--instruct", type=str, required=True, help="用户指令内容,例如:'打开麦克风'")

可选参数:是否使用CPU,默认使用CPU

parser.add_argument("--cpu", action="store_true", help="是否使用CPU进行推理(默认使用CPU)")

可选参数:是否使用GPU

parser.add_argument("--gpu", action="store_true", help="是否使用GPU进行推理(优先级高于--cpu)")

return parser.parse_args()

if name == "main":

解析命令行参数

args = parse_args()

确定是否使用CPU

如果同时指定了--cpu和--gpu,优先使用GPU

use_cpu = not args.gpu and args.cpu

调用测试函数

test(instruct=args.instruct, cpu=use_cpu)

测试命令:

python test_lora.py

经实测,用这些训练数据集训练后的模型,对指令识别的准确率提升还是非常的明显的,不过前面经过几轮次的微调训练、测试验证、再微调训练、测试验证,在我当前的需求规格下识别的准确率仍然未能达到100%(最后一版是96.47%),所以后面还需要再把意图/配位(参数)识别错误的一些命令词再补充整理一下,然后再继续来做微调训练。

四、补充说明