基于 Qwen2.5-7B 预训练大模型,通过微调(LoRA),在 Banking77 数据集的意图识别任务上达到了 92.92% 的测试集准确率
课程任务,基于Qwen2.5-7B,进行 banking77 数据集的 LoRA 微调,识别意图,能在24G显存流畅训练,量化后能在8G显存推理,最终正确率达到92.92%
完成课程任务做的训练,租的阿里云的服务器,规格ecs.gn7i-c8g1.2xlarge,GPU:NVIDIA A10
基于banking77,训练的银行相关的意图识别,模型接受用户的自然语言输入,输出一个意图。
后续考虑的模型升级方向:
1、提高识别准确率
2、接受用户的自然语言输入,用户可能不止一个意图,模型需要做出处理
3、对用户的乱输入现象进行处理(这个可能将乱输入交给调用其他自然语言模型处理,确保给分类模型的是合法输入)
接下来是完整训练代码(未包含merge和评估脚本)
import os
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
import torch
引入cuda内存分配优化器,开启 expandable_segments 后,允许显存段动态扩展,减少碎片问题,尤其适合 LLM 训练中变长序列场景。
注意:该设置仅在使用 cudaMallocAsync 分配器时生效(通常配合 torch.compile() 或某些特定版本)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
启用TF32加速
TF32 是 NVIDIA Ampere 架构引入的一种浮点格式,在保持自动精度转换的同时大幅提升矩阵乘法速度。
allow_tf32=True 允许在 float32 输入下使用 TF32 计算 GEMM 和卷积操作。
它不影响显存占用(仍按 FP32 存储),但能显著提升训练吞吐量(尤其是 A10/A100/V100)。
from typing import Dict, Any #类型注解
from transformers import (
AutoTokenizer, #自动加载分词器
AutoModelForCausalLM, #加载因果语言模型
TrainingArguments,
Trainer, #高层训练接口封装
DataCollatorForSeq2Seq, #自动padding和label处理的collector
BitsAndBytesConfig, #控制量化行为的配置类
)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training #分别是定义Lora参数,包装为PEFT可训练模型,k-bit训练前处理
from modelscope import snapshot_download #从魔塔下载模型
from datasets import load_dataset #加载数据集
#定义各种配置
MODEL_REPO = "qwen/Qwen2.5-7B-Instruct"
MODEL_CACHE_DIR = "/home/ubuntu/models" #缓存路径,防止重复下载
TRAIN_DATA_PATH = "./data_deal/train_lora.json" #训练集
VALID_DATA_PATH = "./data_deal/valid_lora.json" #验证集
OUTPUT_DIR = "./qwen-banking77-final" #输出存储路径
os.makedirs(OUTPUT_DIR, exist_ok=True) #创建输出目录
os.environ["CUDA_VISIBLE_DEVICES"] = "0" #指定用0号GPU
print("开始下载模型...")
model_dir = snapshot_download(MODEL_REPO, cache_dir=MODEL_CACHE_DIR)
接下来是配置量化,进行性能优化,降低模型显存开销
quantization_config = BitsAndBytesConfig(
load_in_4bit=True, #启用4-bit量化加载
bnb_4bit_compute_dtype=torch.bfloat16, #在推理/训练时升回 bfloat16 进行计算(保证精度)
bnb_4bit_quant_type="nf4", #使用 Normal Float 4-bit(NF4),一种针对正态分布权重优化的 4-bit 数据类型,比 int4 更精确
bnb_4bit_use_double_quant=True, #对量化常数也进行一次量化压缩
)
减少开销,7B模型的14G内存减少至6~8G,使其能在单张A10上运行
print("加载 tokenizer...")
tokenizer = AutoTokenizer.from_pretrained(
model_dir,
use_fast=False, #Qwen 当前未提供 Rust 快速 tokenizer,必须关闭 fast tokenizer
trust_remote_code=True, #允许执行模型仓库中的自定义代码(Qwen 使用了特殊 tokenizer 类)
padding_side='right' #因为是 Causal LM(自回归生成),右填充是标准做法(左填充会导致 attention 错误)
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.pad_token_id = tokenizer.eos_token_id
Qwen 等一些模型没有显式的 [PAD] token。
但在批处理(batching)时需要 padding 到相同长度,所以手动将 eos_token(结束符)当作 pad token 使用。
注意: 这要求你在构建 labels 时把 padding 位置 mask 掉(用 -100),否则模型会被迫学习“生成 EOS”作为填充。
print("加载 4-bit 模型...")
model = AutoModelForCausalLM.from_pretrained(
model_dir,
quantization_config=quantization_config, #应用上述的量化配置
device_map="auto", #使用 accelerate 库自动分配模型层到可用设备
trust_remote_code=True, #信任远程代码
torch_dtype=torch.bfloat16, #指定计算 dtype(即使量化了,运算仍在 bfloat16 下进行)
)
加载4-bit量化
model = prepare_model_for_kbit_training(model)
来自 peft 库的方法,为 k-bit 模型做准备,主要做两件事:
启用 Gradient Checkpointing(节省显存)
设置所有 requires_grad 的 module 为 train() 模式
不添加可能导致训练失败
接下来对LoRA进行配置
lora_config = LoraConfig(
r=64, # 秩
lora_alpha=128, # alpha = 2*r
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
#要进行LoRA注入的模块名列表,q/k/v/o_proj: Attention 中的 QKV 和输出投影,gate/up/down_proj: MLP 子层(Qwen 使用 SwiGLU 结构)
lora_dropout=0.05, #加入dropout正则化,防止过拟合
bias="none", #不训练bias项
task_type="CAUSAL_LM", #表明是因果语言建模任务
)
model = get_peft_model(model, lora_config) #将LoRA注入原模型,返回一个可以训练的PEFT包装模型
model.print_trainable_parameters() #输出可训练的参数数量,可以查看LoRA是否生效
print("加载数据集...")
train_dataset = load_dataset('json', data_files=TRAIN_DATA_PATH, split='train')
valid_dataset = load_dataset('json', data_files=VALID_DATA_PATH, split='train')
print(f"训练集大小: {len(train_dataset)}")
print(f"验证集大小: {len(valid_dataset)}") #打印数据量
[ { “instruction”: “请判断用户在银行对话中的意图类别。只能输出一个类别名称,不要解释。”, “input”: “I am still waiting on my card?”, “output”: “card_arrival” }, { “instruction”: “请判断用户在银行对话中的意图类别。只能输出一个类别名称,不要解释。”, “input”: “What can I do if my card still hasn’t arrived after 2 weeks?”, “output”: “card_arrival” },…]
这是我的数据格式
接下来进行数据预处理
import random
def preprocess_function(example: Dict[str, Any]) -> Dict[str, Any]:
"""
数据格式: {"instruction": "...", "input": "...", "output": "..."}
优化策略:
1. 直接映射 Query -> Intent
2. 确保labels正确构建
3. 数据增强:随机使用不同的prompt格式
"""
# 提取数据
query = str(example.get("input", "")).strip()
intent = str(example.get("output", "")).strip()
# 数据增强:随机选择prompt格式,提升模型泛化能力
prompt_formats = [
(f"Query: {query}\nIntent:", f"Query: {query}\nIntent: {intent}"),
(f"User query: {query}\nIntent:", f"User query: {query}\nIntent: {intent}"),
(f"Question: {query}\nCategory:", f"Question: {query}\nCategory: {intent}"),
(f"Classify: {query}\nResult:", f"Classify: {query}\nResult: {intent}"),
]
input_text, full_text = random.choice(prompt_formats)
full_text = full_text + tokenizer.eos_token #添加结束符,告诉模型停止生成
# 对完整文本进行分词编码
tokenized_full = tokenizer(
full_text,
truncation=True,
max_length=256,
padding=False,
return_tensors=None,
add_special_tokens=True,
)
# 对输入部分进行分词编码
tokenized_input = tokenizer(
input_text,
truncation=True,
max_length=256,
padding=False,
return_tensors=None,
add_special_tokens=True,
)
input_ids = tokenized_full["input_ids"] #完整id序列(input_ids 是文本经过分词后转换成的数字编号(整数),是模型能“读懂”文本的第一步)
attention_mask = tokenized_full["attention_mask"] #全部序列mask位置
input_len = len(tokenized_input["input_ids"]) #输入序列长度
# 构建labels:输入部分mask为-100,只训练输出部分,计算生成部分的损失
labels = [-100] * input_len + input_ids[input_len:]
labels = labels[:len(input_ids)] # 确保长度一致
return {
"input_ids": input_ids,
"attention_mask": attention_mask,
"labels": labels,
} #返回标准字典格式,供trainer使用
接下来应用预处理
print("开始预处理数据...")
train_dataset = train_dataset.map(
preprocess_function,
remove_columns=train_dataset.column_names,
num_proc=4,
desc="处理训练集"
)
valid_dataset = valid_dataset.map(
preprocess_function,
remove_columns=valid_dataset.column_names, # 删除原始列(如 input/output),保留新生成的字段
num_proc=4, #多进程加速处理
desc="处理验证集"
)
# 打印样例检查
print("\n📝 数据样例检查:")
sample = train_dataset[0]
print(f"Input_ids长度: {len(sample['input_ids'])}")
print(f"解码文本: {tokenizer.decode(sample['input_ids'])}")
labels_text = [id for id in sample['labels'] if id != -100]
print(f"训练目标(labels): {tokenizer.decode(labels_text)}")
print()
打印第一个数据进行检查,调试用
data_collator = DataCollatorForSeq2Seq(
tokenizer,
padding="longest", #每一个batch都padding到最长样本
pad_to_multiple_of=8, #使序列长度是 8 的倍数,利于 Tensor Core 加速(尤其是使用 BF16/TF32 时)
return_tensors="pt" #返回 PyTorch 张量
)
数据整理器,会自动处理labels字段
接下来进行训练参数设置
training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
# Batch size配置
per_device_train_batch_size=2, # 训练batch
per_device_eval_batch_size=2, #评估batch
gradient_accumulation_steps=4, # 梯度累积步数4,有效batch=8
max_steps=8000, #训练步数
# 学习率调度
learning_rate=1e-4, # 稳定的学习率
warmup_steps=400, # 预热步数warmup 5%
lr_scheduler_type="cosine", # 余弦衰减,有利于模型探索参数空间
# 精度设置
bf16=True,
fp16=False,
# 日志和保存
logging_steps=200,
save_steps=1000,
eval_strategy="steps",
eval_steps=1000, #每一千次保存一个检查点并评估
save_strategy="steps",
save_total_limit=2, #限制保留两个检查点
# 优化器
optim="adamw_bnb_8bit", #使用使用 bitsandbytes 实现的 8-bit AdamW 优化器
# Gradient checkpointing梯度检查点,节省显存
gradient_checkpointing=True,
gradient_checkpointing_kwargs={"use_reentrant": False},
remove_unused_columns=False, #用了自定义字段,关闭自动删列
report_to="none", #不连接 wandb/tensorboard
load_best_model_at_end=True, #训练结束后自动加载验证 loss 最低的模型
metric_for_best_model="eval_loss", #以 eval loss 为准选最优模型
greater_is_better=False,
# 防止过拟合,AdamW 的 L2 正则
weight_decay=0.01,
)
接下来组装所有组件,创建高层训练器
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=valid_dataset,
data_collator=data_collator,
tokenizer=tokenizer,
)
接下来是训练恢复机制,寻找最新的检查点,支持中断后恢复训练
def get_last_checkpoint(folder):
if not os.path.isdir(folder):
return None
checkpoints = [x for x in os.listdir(folder) if x.startswith("checkpoint-")]
if not checkpoints:
return None
return os.path.join(folder, max(checkpoints, key=lambda x: int(x.split("-")[-1])))
last_checkpoint = get_last_checkpoint(OUTPUT_DIR)
print(f"检查点: {last_checkpoint}")
trainer.train(resume_from_checkpoint=last_checkpoint) #启动训练
model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
#保存训练模型
最后用banking77测试集评估完全符合0%,去掉空格,不论大小写后92.92%
更多推荐
所有评论(0)