完成课程任务做的训练,租的阿里云的服务器,规格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%

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐