Qwen1.5-7B-Chat 高效微调实战:基于 transformers 与 peft 的 Lora 指令微调全流程指南(self-llm 项目)
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
本文是《开源大模型食用指南》(Datawhale/self-llm)中 Qwen1.5 系列教程的核心篇章,完整讲解如何在 Linux + PyTorch 环境下,基于 transformers、peft、datasets 等框架对 Qwen1.5-7B-Chat 进行 Lora 指令微调,并将其训练成具有甄嬛对话风格的人设化聊天模型。读完本文,你将掌握从环境搭建、指令数据集构建、数据格式化、LoraConfig 与 TrainingArguments 参数设计,到 Trainer 训练与 Lora 权重加载推理的端到端实战能力。
概览:Lora 微调的关键要素
本节教程要解决的工程问题十分明确:以最小的可训练参数量,让 Qwen1.5-7B-Chat 具备理解并遵循特定指令(人设对话)的能力。Lora(Low-Rank Adaptation)通过冻结基座模型、仅训练注入的低秩矩阵来实现高效微调。本文涉及的完整链路如下:
- 环境配置与依赖安装(
transformers/peft/datasets/modelscope); - 指令集构建:设计
instruction/input/output三元组数据; - 数据格式化:按 Qwen1.5 的 Chat Template 将文本编码为
input_ids/attention_mask/labels; - 加载半精度模型并定义
LoraConfig; - 配置
TrainingArguments并使用Trainer训练; - 加载训练好的 Lora 权重进行推理。
配套的可运行示例代码位于仓库 models/Qwen1.5/Qwen1.5-7B-Chat Lora.ipynb,建议读者跟随本文阅读时同步打开 notebook 逐 cell 执行验证。
环境配置
本文基础环境如下:
---------------- ubuntu 22.04 python 3.12 cuda 12.1 pytorch 2.3.0 ----------------本文默认学习者已安装好以上 PyTorch(cuda) 环境,如未安装请自行安装。
接下来开始环境配置、模型下载和运行演示。首先对pip换源加速下载并安装依赖包:
pip install --upgrade pip # 更换 pypi 源加速库的安装 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install modelscope==1.16.1 pip install transformers==4.43.2 pip install accelerate==0.32.1 pip install peft==0.11.1 pip install datasets==2.20.0各依赖的职责如下:
modelscope:国内模型下载工具,用于拉取 Qwen1.5-7B-Chat 权重;transformers:模型加载、Tokenizer 与训练器Trainer的底层框架;accelerate:device_map="auto"多卡/显存自动分配与Trainer训练的底层加速库;peft:Lora 适配器(LoraConfig、get_peft_model、PeftModel)所在库;datasets:数据集加载与map批量预处理。
考虑到部分同学配置环境可能会遇到一些问题,本项目在 AutoDL 平台准备了 Qwen1.5 的环境镜像,该镜像适用于本仓库除 Qwen-GPTQ 和 vllm 外的所有部署环境,可直接创建 AutoDL 示例使用。
在本节教程里,我们将微调数据集放置在根目录 dataset/huanhuan.json。
模型下载
微调前需要先获取 Qwen1.5-7B-Chat 的原始权重。仓库中 01-Qwen1.5-7B-Chat FastApi 部署调用.md 给出了使用modelscope的snapshot_download函数下载模型的完整方式:第一个参数为模型名称,参数cache_dir为自定义的模型下载路径,参数revision为模型仓库分支版本,master代表主分支,也是一般模型上传的默认分支:
# model_download.py from modelscope import snapshot_download model_dir = snapshot_download('qwen/Qwen1.5-7B-Chat', cache_dir='/root/autodl-tmp', revision='master')注意:记得修改
cache_dir为你的模型下载路径。模型权重文件比较大,需要耐心等待直到下载完成。下载完成后,在后续代码中可将模型路径(如./qwen/Qwen1.5-7B-Chat/)替换为实际的本地目录。
指令集构建
LLM 的微调一般指指令微调过程。所谓指令微调,是说我们使用的微调数据形如:
{ "instruction":"回答以下用户问题,仅输出答案。", "input":"1+1等于几?", "output":"2" }其中,instruction是用户指令,告知模型其需要完成的任务;input是用户输入,是完成用户指令所必须的输入内容;output是模型应该给出的输出。
即我们的核心训练目标是让模型具有理解并遵循用户指令的能力。因此,在指令集构建时,我们应针对目标任务,针对性构建任务指令集。例如,在本节我们使用合作开源的 Chat-甄嬛(huanhuan-chat)项目作为示例,目标是构建一个能够模拟甄嬛对话风格的个性化 LLM,因此构造的指令形如:
{ "instruction": "你是谁?", "input":"", "output":"家父是大理寺少卿甄远道。" }本项目实际使用的全部指令数据集位于仓库 dataset/huanhuan.json,共3729 条对话样本,字段结构即为上述三元组。数据预览(取自 notebook 中的ds[:3]输出):
{ "instruction": ["小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——", "这个温太医啊,也是古怪,谁不知太医不得皇命不能为皇族以外的人请脉诊病,他倒好,十天半月便往咱们府里跑。", "嬛妹妹,刚刚我去府上请脉,听甄伯母说你来这里进香了。"], "input": ["", "", ""], "output": ["嘘——都说许愿说破是不灵的。", "你们俩话太多了,我该和温太医要一剂药,好好治治你们。", "出来走走,也是散心。"] }数据加载方式同样来自 notebook:
from datasets import Dataset import pandas as pd df = pd.read_json('./huanhuan.json') ds = Dataset.from_pandas(df)数据格式化
Lora训练的数据是需要经过格式化、编码之后再输入给模型进行训练的。熟悉Pytorch模型训练流程的同学会知道,我们一般需要将输入文本编码为 input_ids,将输出文本编码为labels,编码之后的结果都是多维的向量。我们首先定义一个预处理函数,这个函数用于对每一个样本,编码其输入、输出文本并返回一个编码后的字典:
def process_func(example): MAX_LENGTH = 384 # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性 input_ids, attention_mask, labels = [], [], [] instruction = tokenizer(f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False) # add_special_tokens 不在开头加 special_tokens response = tokenizer(f"{example['output']}", add_special_tokens=False) input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id] attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1] # 因为eos token咱们也是要关注的所以 补充为1 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 }这段代码是理解 Lora 微调数据流的关键,逐行解读如下:
MAX_LENGTH = 384:Qwen 分词器会将一个中文字切分为多个 token,需要放开最大长度保证数据完整性;超长样本直接截断到 384。instruction编码:使用add_special_tokens=False手动拼接 Qwen1.5 的 Chat Template,避免 tokenizer 自动在开头追加 special token。input_ids拼接:指令部分 + 回答部分 + pad_token_id,其中末尾的pad_token_id起到句子结束标记(EOS)的作用。attention_mask:末尾补充1,因为 EOS token 也需要被模型关注。labels:指令部分的 label 全部置为-100(表示不参与 loss 计算),只有回答部分(response["input_ids"])参与损失,这正是"只学习回答、不学习提问"的监督微调核心。
随后通过datasets的map方法批量处理全部样本(notebook 中该步骤实际处理了 3729 条样本):
tokenized_id = ds.map(process_func, remove_columns=ds.column_names)处理完成后可用tokenizer.decode验证编码正确性。notebook 中解码第一条样本得到的完整训练文本为:
<|im_start|>system 现在你要扮演皇帝身边的女人--甄嬛<|im_end|> <|im_start|>user 小姐,别的秀女都在求中选,唯有咱们小姐想被撂牌子,菩萨一定记得真真儿的——<|im_end|> <|im_start|>assistant 嘘——都说许愿说破是不灵的。<|endoftext|>Qwen1.5采用的Prompt Template格式如下:
<|im_start|>system You are a helpful assistant.<|im_end|> <|im_start|>user 你是谁?<|im_end|> <|im_start|>assistant 我是一个有用的助手。<|im_end|>从 notebook 加载的 tokenizer 信息可以看到,Qwen1.5-7B-Chat 使用的是Qwen2Tokenizer(vocab_size=151643,model_max_length=32768,use_fast=False),其特殊 token 为:<|endoftext|>(同时作为 eos_token 与 pad_token)、<|im_start|>、<|im_end|>。这解释了为什么在推理时可以使用tokenizer.apply_chat_template自动生成上述模板文本。
加载tokenizer和半精度模型
模型以半精度形式加载,如果你的显卡比较新的话,可以用torch.bfloat16形式加载。对于自定义的模型一定要指定trust_remote_code参数为True:
tokenizer = AutoTokenizer.from_pretrained('./qwen/Qwen1.5-7B-Chat/', use_fast=False, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained('./qwen/Qwen1.5-7B-Chat/', device_map="auto",torch_dtype=torch.bfloat16)use_fast=False:使用慢速(经典)tokenizer 实现,保证与仓库保存的 tokenizer 行为一致;device_map="auto":由accelerate自动将模型各层分配到可用 GPU/CPU 上,显存不足时自动降级到 CPU;torch_dtype=torch.bfloat16:以半精度 bfloat16 加载,显存占用约为 fp32 的一半,同时具备更大的指数范围,更适合 7B 级别模型在单卡上的微调。
notebook 中加载完成的模型结构(model.print输出)清晰展示了 Qwen1.5-7B-Chat 基于 Qwen2 架构的实现:Qwen2ForCausalLM包含 32 层Qwen2DecoderLayer,每层由self_attn(q/k/v_proj 均为 4096 维,o_proj 无 bias)与mlp(gate/up_proj 升维到 11008,down_proj 降回 4096,激活函数 SiLU)构成,lm_head映射到 151936 词表。这一结构也直接决定了下方LoraConfig中target_modules的选择范围。
定义LoraConfig
LoraConfig这个类中可以设置很多参数,但主要的参数没多少,简单讲一讲,感兴趣的同学可以直接看 peft 源码。
task_type:模型类型target_modules:需要训练的模型层的名字,主要就是attention部分的层,不同的模型对应的层的名字不同,可以传入数组,也可以字符串,也可以正则表达式。r:lora的秩,具体可以看Lora原理lora_alpha:Lora alaph,具体作用参见Lora原理
Lora的缩放是啥嘞?当然不是r(秩),这个缩放就是lora_alpha/r,在这个LoraConfig中缩放就是 4 倍。
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, # 训练模式 r=8, # Lora 秩 lora_alpha=32, # Lora alaph,具体作用参见 Lora 原理 lora_dropout=0.1# Dropout 比例 )参数要点结合仓库 notebook 的实际运行结果展开:
target_modules:这里同时覆盖了注意力层的q/k/v/o_proj与 MLP 层的gate/up/down_proj全部 7 个线性层,与前面模型结构中观察到的Qwen2DecoderLayer内部层名一一对应。更激进的微调范围通常能带来更好的效果,但也会增加可训练参数量。- 缩放系数
lora_alpha/r = 32/8 = 4:即注入的低秩分支输出会乘上 4 的缩放因子。该值越大,Lora 分支对最终输出的影响越强,实践中常与r按 2~4 倍比例搭配。 r=8的含义:每个被适配的线性层只训练一个秩为 8 的低秩矩阵对(A、B),而不是全量权重。
将配置应用到模型(notebook 中使用了get_peft_model),随后打印可训练参数统计:
from peft import LoraConfig, TaskType, get_peft_model model = get_peft_model(model, config) model.print_trainable_parameters()notebook 的真实运行结果为:
trainable params: 19,988,480 || all params: 7,741,313,024 || trainable%: 0.2582052933143348可以看到:在 77.4 亿参数的 Qwen1.5-7B-Chat 上,Lora 仅训练约2000 万参数,可训练比例仅0.26%——这正是 Lora"高效微调"的直接体现:显存与训练时间开销大幅下降,同时保留基座模型的通用能力。
自定义 TrainingArguments 参数
TrainingArguments这个类的源码也介绍了每个参数的具体作用,当然大家可以自行探索,这里就简单说几个常用的。
output_dir:模型的输出路径per_device_train_batch_size:顾名思义batch_sizegradient_accumulation_steps: 梯度累加,如果你的显存比较小,那可以把batch_size设置小一点,梯度累加增大一些。logging_steps:多少步,输出一次lognum_train_epochs:顾名思义epochgradient_checkpointing:梯度检查,这个一旦开启,模型就必须执行model.enable_input_require_grads(),这个原理大家可以自行探索,这里就不细说了。
args = TrainingArguments( output_dir="./output/Qwen1.5-7B-Chat", per_device_train_batch_size=4, gradient_accumulation_steps=4, logging_steps=10, num_train_epochs=3, save_steps=100, learning_rate=1e-4, save_on_each_node=True, gradient_checkpointing=True )参数组合的实际含义与显存影响:
- 有效 batch size:
per_device_train_batch_size(4) × gradient_accumulation_steps(4) = 16。如果显存较小,可以调低 batch_size 并相应增大梯度累加步数,保持等效 batch 不变。 gradient_checkpointing=True:以重计算换取显存,训练时需配套执行model.enable_input_require_grads()(notebook 中在加载模型后显式调用了该方法)。同时,开启后use_cache会被自动置为False(notebook 训练日志中有对应提示),即推理缓存与梯度检查点不兼容。save_steps=100:每 100 步保存一次 checkpoint 到output_dir,便于中断恢复与选取最优 checkpoint。learning_rate=1e-4:Lora 微调常用学习率区间为 1e-5 ~ 2e-4,1e-4是较为稳妥的默认选择;全参微调则通常需要更小的学习率。save_on_each_node=True:多节点训练时每个节点均保存 checkpoint。
使用 Trainer 训练
trainer = Trainer( model=model, args=args, train_dataset=tokenized_id, data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True), ) trainer.train()关键点说明:
train_dataset传入的是经过process_func编码后的tokenized_id(仅含input_ids/attention_mask/labels三列,原列已被remove_columns移除);DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)负责在 batch 内对不定长序列做 padding 对齐,并正确处理labels侧的 pad token(默认用-100填充,避免 padding 位置参与 loss 计算)。
notebook 中实际训练了 3 个 epoch,总计约 699 步,其中记录了真实的收敛过程:前 10 步训练损失约 4.15,至 130 步时已降至约 2.94,呈现平稳下降趋势,说明模型正在逐步习得"甄嬛风格回复"这一目标分布。不同显存条件下训练时长会有差异,可依据实际资源调整 batch、梯度累加与 epoch 数。
加载 lora 权重推理
训练好了之后可以使用如下方式加载lora权重进行推理:
from transformers import AutoModelForCausalLM, AutoTokenizer import torch from peft import PeftModel model_path = './qwen/Qwen1.5-7B-Chat/' lora_path = 'lora_path' # 加载tokenizer tokenizer = AutoTokenizer.from_pretrained(model_path) # 加载模型 model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto",torch_dtype=torch.bfloat16) # 加载lora权重 model = PeftModel.from_pretrained(model, model_id=lora_path, config=config) prompt = "你是谁?" messages = [ {"role": "system", "content": "现在你要扮演皇帝身边的女人--甄嬛"}, {"role": "user", "content": prompt} ] text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) model_inputs = tokenizer([text], return_tensors="pt").to('cuda') generated_ids = model.generate( model_inputs.input_ids, max_new_tokens=512 ) generated_ids = [ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids) ] response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0] print(response)推理链路要点:
PeftModel.from_pretrained(model, model_id=lora_path):将训练好的 Lora 适配器权重挂载到原始基座模型上。lora_path应替换为你训练时的 checkpoint 目录(例如./output/Qwen1.5-7B-Chat/checkpoint-xxx/)。这一加载方式在本仓库的多个 Lora 微调教程中保持一致,例如 CharacterGLM Lora 微调 与 ChatGLM3 Lora 微调 均采用同样的PeftModel.from_pretrained加载模式。tokenizer.apply_chat_template:与训练时的数据格式化逻辑对应,按 Qwen1.5 的 Chat Template 自动拼装system + user消息并追加assistant起始标记(add_generation_prompt=True),无需手工拼接模板字符串。skip_special_tokens=True:解码时剔除<|im_start|>、<|im_end|>、<|endoftext|>等特殊 token,只保留干净的回答文本。
推理时 model 与 tokenizer 加载完成后即挂载 Lora 权重;若希望永久合并 Lora 与基座权重,可使用 peft 的save_pretrained/合并功能导出完整模型(本仓库 Atom-7B-Chat Lora 微调 中给出了model.save_pretrained(training_args.output_dir)的保存参考)。
延伸学习
- 本文配套的完整逐 cell 可执行版本见 Qwen1.5-7B-Chat Lora.ipynb,其中包含每一步的真实输出(数据集预览、模型结构、可训练参数统计、训练损失曲线等),非常适合对照复现。
- 想在微调过程中加入实验跟踪与指标可视化,可参考同目录的 08-Qwen1.5-7B-chat LoRA微调接入实验管理.md,在本文训练流程基础上无缝接入 SwanLab。
- 训练完成后,可将微调模型接入 FastApi 服务、WebDemo 或 LangChain 知识库,相关教程见 Qwen1.5 系列文档 中列出的 01/02/03 号部署文档。
- Lora 的底层原理(低秩分解、缩放系数、与全参微调的关系)可进一步阅读 peft 源码或查阅《深入浅出 Lora》等公开技术博客深入理解。
【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/datawhalechina/self-llm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考