欺诈文本分类检测(十七):支持分类原因训练

1. 引言

前文数据校正与增强进行了数据增强,本文将使用增强后的数据对模型进行进一步训练,以便得到能同时预测出分类标签、欺诈者、分类原因多个信息的模型。

为此,我们需要对整个训练过程进行调整,包括:

  1. 交叉训练逻辑封装
  2. 数据序列化的改造
  3. 评测方法改造

2. 交叉训练封装

首先,我们将前文 交叉训练验证的代码封装为一个脚本trainer_cross.py,方便复用。内容如下:

python 复制代码
import glob
import gc
import numpy as np
from datasets import Dataset, concatenate_datasets
from sklearn.model_selection import KFold
from trainer import *

def find_last_checkpoint(output_dir):
    checkpoint_dirs = glob.glob(os.path.join(output_dir, 'checkpoint-*'))
    last_checkpoint_dir = max(checkpoint_dirs, key=os.path.getctime)
    return last_checkpoint_dir

def load_model_v2(model_path, checkpoint_path='', device='cuda'):
    # 加载模型
    model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16, trust_remote_code=True).to(device)
    # 加载lora权重
    if checkpoint_path: 
        model = PeftModel.from_pretrained(model, model_id=checkpoint_path).to(device)
    # 将基础模型的参数设置为不可训练
    for param in model.base_model.parameters():
        param.requires_grad = False
    
    # 将 LoRA 插入模块的参数设置为可训练
    for name, param in model.named_parameters():
        if 'lora' in name:
            param.requires_grad = True
    return model

def build_trainer_v2(model, tokenizer, train_args, train_dataset, eval_dataset):
    # 开启梯度检查点时,要执行该方法
    if train_args.gradient_checkpointing:
        model.enable_input_require_grads()
    return Trainer(
        model=model,
        args=train_args,
        train_dataset=train_dataset,
        eval_dataset=eval_dataset,
        data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
        callbacks=[EarlyStoppingCallback(early_stopping_patience=5)],  # 早停回调
    )

def train_kfold(model_path, output_base_path, datasets, build_args_func, fold_num=5, device='cuda', last_checkpoint_path=''):
    tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
    kf = KFold(n_splits=fold_num, shuffle=True)
    results = []
    
    for fold, (train_index, val_index) in enumerate(kf.split(np.arange(len(datasets)))):
        print(f"fold={fold} start, train_index={train_index}, val_index={val_index}")
        train_dataset = datasets.select(train_index)
        eval_dataset = datasets.select(val_index)
        print(f"train data: {len(train_dataset)}, eval: {len(eval_dataset)}")
    
        output_path = f'{output_base_path}_{fold}'
        train_args, lora_config = build_args_func(output_path)
        if last_checkpoint_path:
            model = load_model_v2(model_path, last_checkpoint_path, device)
            print(f"fold={fold}, load model from checkpoint: {last_checkpoint_path}")
        else:
            model = AutoModelForCausalLM.from_pretrained(model_path, torch_dtype=torch.bfloat16).to(device)
            model = get_peft_model(model, lora_config)
    
        model.print_trainable_parameters()
        trainer = build_trainer_v2(model, tokenizer, train_args, train_dataset, eval_dataset)
        train_result = trainer.train()
        print(f"fold={fold}, result = {train_result}")
        results.append(train_result)
        
        last_checkpoint_path = find_last_checkpoint(output_path)
        
    return results

其中,各个方法的作用释义如下:

  • find_last_checkpoint:用于从一个目录下查找最新的checkpoint。
  • load_model_v2:加载模型和微调的checkpoint,并将lora权重设置为可训练,非lora权重设置为不可训练。
  • build_trainer_v2:构造训练器
  • train_kfold:封装K折交叉训练验证的主循环逻辑,循环的每个批次为不同的数据集

train_kfold是此脚本最终对外公开的方法,它开放了如下参数以便灵活调整训练过程:

  • model_path:基座模型路径;
  • output_base_path:输出模型的基础路径,K折交叉训练会以此路径为基础,来构造每一折数据的输出路径;
  • datasets:经过预处理后的数据集;
  • build_args_func:构造训练参数的方法,根据output_path来构造训练参数和Lora参数;
  • fold_num: 数据集要分割的折数;
  • device: 训练的GPU设备;
  • last_checkpoint_path: 最近一次训练的checkpoint路径,当接着上一次的训练结果继续训练时传此参数。

3. 数据加载改造

当输出数据改变后,模型的预期输出不再仅仅是一个分类标签,还需要包括欺诈者和分类原因。因此,我们加载数据和数据序列化的方式需要作相应调整。

改造数据预处理函数,扩展with_reason参数,参数值定义:

  • true:表示预期结果除了is_fraud字段外,还包含fraud_speaker和reason字段。
  • false:表示预期结果不包含fraud_speaker和reason字段。

代码如下(有变化的仅仅是if with_reason的判断分支)。

python 复制代码
def preprocess(item, tokenizer, with_reason=False, max_length=2048):
    system_message = "You are a helpful assistant."
    user_message = item['instruction'] + '\n' + item['input']
    if with_reason: 
        output = {"is_fraud":item["label"], "fraud_speaker":item["fraud_speaker"], "reason":item["reason"]}
    else:
        output = {"is_fraud":item["label"]}
        
    assistant_message = json.dumps(output, ensure_ascii=False)
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer(f"<|im_start|>system\n{system_message}<|im_end|>\n<|im_start|>user\n{user_message}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False)  
    response = tokenizer(assistant_message, 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]  
    # -100是一个特殊的标记,用于指示指令部分的token不应参与损失计算
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]  
    
    # 对输入长度做一个限制保护,超出截断
    return {
        "input_ids": input_ids[:max_length],
        "attention_mask": attention_mask[:max_length],
        "labels": labels[:max_length]
    }

相应对外的load_dataset方法也扩展with_reason参数,目的兼容之前的单独分类标签训练,支持带原因和不带原因两种加载数据的模式。

python 复制代码
def load_one_dataset(data_path, tokenizer, with_reason:bool):
    df = load_jsonl(data_path)
    ds = Dataset.from_pandas(df)
    return ds.map(
        lambda x: preprocess(x, tokenizer, with_reason=with_reason),
        remove_columns=ds.column_names)

def load_dataset(train_path, eval_path, tokenizer, with_reason=False):
    train_dataset = load_one_dataset(train_path, tokenizer, with_reason)
    eval_dataset = load_one_dataset(eval_path, tokenizer, with_reason)
    return train_dataset, eval_dataset

4. 开始训练

4.1 初始化

初始化改为引入新封装的脚本trainer_cross.py

python 复制代码
%run trainer_cross.py

数据路径、模型路径、设备定义基本和之前保持一致。

python 复制代码
traindata_path = '/data2/anti_fraud/dataset/train0902.jsonl'
evaldata_path = '/data2/anti_fraud/dataset/eval0902.jsonl'
model_path = '/data2/anti_fraud/models/modelscope/hub/Qwen/Qwen2-1___5B-Instruct'
output_base_path = '/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0913'
python 复制代码
os.environ["CUDA_VISIBLE_DEVICES"] = "1"
device = 'cuda'

加载数据集,使用concatenate_datasets方法将训练集和验证集合并为一个数据集。

python 复制代码
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
train_dataset, eval_dataset = load_dataset(traindata_path, evaldata_path, tokenizer, with_reason=True, lazy=False)
datasets = concatenate_datasets([train_dataset, eval_dataset])
4.2 训练

定义参数构造的方法,用于构造训练参数和Lora参数,具体参数值保持与之前相同。

python 复制代码
def build_arguments(output_path):
    train_args = build_train_arguments(output_path)
    train_args.eval_strategy='epoch'
    train_args.save_strategy='epoch'
    train_args.num_train_epochs = 2
    train_args.per_device_train_batch_size = 8
    
    lora_config = build_loraconfig()
    lora_config.lora_dropout = 0.2  
    lora_config.r = 16
    lora_config.lora_alpha = 32
    return train_args, lora_config

调用train_kfold方法开始训练:

python 复制代码
results = train_kfold(model_path, output_base_path, datasets, build_args_func=build_arguments, fold_num=5, last_checkpoint_path=last_checkpoint_path)

总共进行了5折数据10轮训练,每折数据进行了两轮训练,相应的训练损失和验证损失数据如下:

Epoch Training Loss Validation Loss
1 0.780100 0.825167
2 0.696700 0.813522
3 0.785400 0.738886
4 0.666200 0.731676
5 0.679400 0.619393
6 0.558900 0.610776
7 0.582100 0.503672
8 0.429700 0.490893
9 0.483300 0.394778
10 0.308000 0.372799
4.3 评测

根据验证损失数据,基于前文支持分类原因评测改造的脚本,采用微调效果最好的最后一轮checkpoint进行评测。

python 复制代码
%run evaluate_v2.py
testdata_path = '/data2/anti_fraud/dataset/test0902.jsonl'
checkpoint_path = '/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0913_4/checkpoint-5454'
evaluate_v2(model_path, checkpoint_path, testdata_path, device, debug=True)

三个字段的评测指标分别如下:

字段 指标
is_fraud precision: 0.9422, recall: 0.9434, accuracy: 0.9419
fraud_speaker accuracy: 0.9175
reason precision: 0.3596, recall: 0.3708, f1-score: 0.3571

经过训练后,三个字段的指标都有不同程度的提高,分别为:

  • is_fraud: precision从0.6232提升到0.9422,表明模型在欺诈文本分类任务上的精确率有明显提高,这能减少欺诈文本误报的次数;
  • fraud_speaker: accuracy从0.6327提升到0.9175,表明模型能有效的识别哪些说话者可能涉及欺诈;
  • reason: 召回率从0.2324提升到0.3708,f1-score从0.2638提升到0.3571

可以看到,is_fraud和fraud_speaker两个字段的准确率提升是比较明显的,而reason字段的召回率也有一定程度的提升,但分数没有那么高。

猜想原因可能在于训练不充分,因为从上面训练的损失数据中能看到一个现象:整个10轮训练下来,不论是训练损失还是验证损失都还在持续下降,这说明训练还未完成。

5. 再次训练

调整输出目录,并定义最近一次训练结构的checkpoint路径:

python 复制代码
output_base_path = '/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0924'
last_checkpoint_path = '/data2/anti_fraud/models/Qwen2-1___5B-Instruct_ft_0913_4/checkpoint-5454'

将折子数调整为10, 其它都和上面相同,基于指定的checkpoint继续训练:

python 复制代码
results = train_kfold(model_path, output_base_path, datasets, build_args_func=build_arguments, fold_num=10, last_checkpoint_path=last_checkpoint_path)

总共进行了10折数据20轮训练,每折数据进行了两轮训练,相应的训练损失和验证损失数据如下:

Epoch Training Loss Validation Loss
1 0.439300 0.365142
2 0.204100 0.351331
3 0.327500 0.268683
4 0.202400 0.246474
5 0.253900 0.192165
6 0.133200 0.165728
7 0.1677 0.1145
8 0.1555 0.1024
9 0.1431 0.08527
10 0.1329 0.07053
11 0.1231 0.06223
12 0.1139 0.0571
13 0.1066 0.05086
14 0.1008 0.04274
15 0.09484 0.04154
16 0.070900 0.038615
17 0.069700 0.033552
18 0.068800 0.029461
19 0.059300 0.026729
20 0.068200 0.023861

从训练结果来看,损失数据一直在不断的下降,这里用最后第20轮的checkpoint进行评测(代码省略),三个字段的评测指标分别如下:

字段 指标
is_fraud 0.9372, recall: 0.9414, accuracy: 0.9383
fraud_speaker accuracy: 0.9152
reason precision: 0.3664, recall: 0.3705, f1-score: 0.3601

结果是有些失望的,虽然损失在不断下降,但各项评测指标基本都没有什么改善。

原因猜测可能有以下几个:

  1. 数据量不足,2-3万条训练数据相对于文本生成任务来说还是太少,可能还不足以明显改善生成文本的相似度。
  2. Lora参数矩阵(r=16)较小,不足以储存足够的信息特征。

补充:后续尝试过将Lora矩阵的秩r调到64,并训练了10折20轮,但测评结果依然与上面的结果相似,没有明显改善。

由于分类原因的相似度指标并不是我们必需的,这里受精力和数据量的限制,暂时没继续往下训练,但欺诈者字段的准确率已经达到预期,带欺诈者和分类原因的response格式已经能够正常生成,如下所示:

json 复制代码
{"is_fraud": true, "fraud_speaker": "小灿", "reason": "小灿要求吴某某登录一个网站,并根据其指示进行国际黄金的操作,这种行为很可能是典型的投资诈骗手段。"}
{"is_fraud": true, "fraud_speaker": "李小龙", "reason": "李小龙要求支付一笔费用以帮助其亲戚获得释放,但没有提供任何具体的细节或证明其关系。使用 relative/convicted等模糊词汇来联系律师,并要求立即支付费用,这种行为具有明显的诈骗特征。"}
{"is_fraud": true, "fraud_speaker": "朱立", "reason": "朱立提供的投资方案中,明显存在通过吸引投资者大量投入资金来获取高额回报的行为。这种高回报承诺通常是不现实且具有欺骗性的,极有可能构成经济诈骗。具体表现为:- 98元、598元和998元的投资计划承诺回报远远超过正常市场回报率,这违反了普遍的投资原则和常识。\- 投资98元的方案回报5万到10万元,这样的回报率过高,容易让人怀疑其真实性。\- 同样,其他投资方案也承诺极高的回报,如598元投资回报50万元,998元投资回报200万元,这些回报率远超正常市场水平,极易诱导投资者上当。\n"}

小结:本文通过对数据加载的改造和交叉训练过程的封装,完成了一次针对带分类原因的欺诈文本分类任务的训练,并通过评测方法的改造实现了对不同类型字段的结果评测。从损失数据和评测结果来看,要改善生成文本的精确率和召回率,可能还需要更多更丰富的数据,后续腾出时间再研究。

参考阅读

相关推荐
陈苏同学4 分钟前
机器翻译 & 数据集 (NLP基础 - 预处理 → tokenize → 词表 → 截断/填充 → 迭代器) + 代码实现 —— 笔记3.9《动手学深度学习》
人工智能·pytorch·笔记·python·深度学习·自然语言处理·机器翻译
狂放不羁霸4 分钟前
组会 | 大语言模型 + LoRA
人工智能·语言模型·自然语言处理
sp_fyf_20246 分钟前
【大语言模型】ACL2024论文-20 SCIMON:面向新颖性的科学启示机器优化
人工智能·深度学习·机器学习·语言模型·自然语言处理·数据挖掘
宋138102797208 分钟前
SouVR Feedback force7 力反馈设备
人工智能·机器人·vr
我叫白小猿43 分钟前
【大模型-智能体】AutoGen Studio测试和导出工作流程
人工智能·python·workflow·工作流·智能体·autogen
CopyLower1 小时前
AI赋能电商:智能购物推荐、会员分类与商品定价的创新探索
人工智能·分类·数据挖掘
界面开发小八哥1 小时前
界面控件DevExpress WinForms v24.2新功能预览 - 人工智能(AI)
人工智能·.net·界面控件·devexpress·ui开发
2zcode1 小时前
基于YOLOv8深度学习的独居老人情感状态监护系统(PyQt5界面+数据集+训练代码)
人工智能·深度学习·yolo
藓类少女1 小时前
【深度学习】模型训练时减少GPU显存占用
人工智能·深度学习
苏涵.1 小时前
深度学习实验十二 卷积神经网络(3)——基于残差网络实现手写体数字识别实验
人工智能·深度学习·神经网络·cnn