AFAC2024: 基于保险条款的问答挑战——我的实战日记
概述
在最近的AFAC2024竞赛中,我参与了基于保险条款的问答赛道。这是一次深度学习与自然语言处理的实战演练,旨在提升模型在复杂保险文本理解与问答生成方面的能力。本文将分享我的参赛过程,包括数据处理、模型选择、微调策略、实验观察及最终成果。
比赛报名链接 https://tianchi.aliyun.com/competition/entrance/532194/introduction
数据与挑战
竞赛提供的数据集包含约6000条基于保险条款的问答对。这些数据覆盖了多种保险类型,如人寿保险、财产保险和健康保险,涉及保险条款的解释、索赔流程、覆盖范围等问题。数据集的多样性和专业性构成了此次竞赛的主要挑战。
llama factory 数据预处理
import jsontrain = json.load(open("round1_training_data/train.json",'r'))
dev = json.load(open("round1_training_data/dev.json",'r'))
a = []
for train_one in train:a.append({"input":"""目前有产品名称、相关条款。如果问题与产品名称、相关条款有关系,那么就依照产品名称、相关条款回答问题,如果没有关系直接回答问题。根据"""+train_one['产品名']+""",相关条款"""+train_one['条款']+",问题:"+train_one['问题'],"output":train_one['答案']})
for train_one in dev:a.append({"input":"""目前有产品名称、相关条款。如果问题与产品名称、相关条款有关系,那么就依照产品名称、相关条款回答问题,如果没有关系直接回答问题。根据"""+train_one['产品名']+""",相关条款"""+train_one['条款']+",问题:"+train_one['问题'],"output":train_one['答案']})
json.dump(a,open('data/a.json','w'),ensure_ascii=False)
任务总共数据6000条。
最大长度超过10000。
修改llama factory data下的data_info.json 加入我们的数据集
"a": {"file_name": "a.json","columns": {"prompt": "input","response": "output"}},
启动云脑任务的时候可以预先选择上我们要进行作业的模型。
配置好这个以后我们就可以启动任务了。在项目启动后我们需要把模型文件拉到本地。
from c2net.context import prepare#初始化导入数据集和预训练模型到容器内
c2net_context = prepare()#获取数据集路径
chat_json_path = c2net_context.dataset_path+"/"+"chat.json"#获取预训练模型路径
qwen1_5_14b_chat_path = c2net_context.pretrain_model_path+"/"+"Qwen1.5-14B-Chat"#输出结果必须保存在该目录
you_should_save_here = c2net_context.output_path
如果不选择会浪费更多的时间在下载数据集上。平台不支持访问transformers只能访问国内的modelscope。
模型选择与微调
为了应对挑战,我选择了Qwen的多个版本作为基础模型。具体来说,我尝试了两种策略:
- LoRA微调:首先,我使用了qwen2-7b-instruct和qwen1.5-14B-chat模型,通过LoRA(低秩适配)进行微调。LoRA允许在不修改原模型权重的情况下,仅优化少量新增参数,从而有效减少了计算资源需求。
lora 微调 qwen2-7b-instruct 在比赛中拿到了489分
lora 微调 qwen1.5-14B-chat
超参数 qwen1.5-14B-chat
### model
model_name_or_path: pretrainmodel/Qwen1.5-14B-Chat### method
stage: sft
do_train: true
finetuning_type: lora
lora_target: all
deepspeed: examples/deepspeed/ds_z2_config.json### dataset
dataset: a
template: qwen
cutoff_len: 2048
overwrite_cache: true
preprocessing_num_workers: 16### output
output_dir: saves/qwen1.5-14b/full/sft
logging_steps: 10
save_steps: 500
plot_loss: true
overwrite_output_dir: true### train
per_device_train_batch_size: 1
gradient_accumulation_steps: 2
learning_rate: 5.0e-5
num_train_epochs: 3.0
lr_scheduler_type: cosine
warmup_ratio: 0.1
bf16: true
ddp_timeout: 180000000### eval
val_size: 0.1
per_device_eval_batch_size: 1
eval_strategy: steps
eval_steps: 500
这里我们选择了使用deepspeed zero2的方式进行微调工作。在910B1 npu上可以使用bf16精度进行模型训练。验证集设置的大小是百分之十的数据作为验证数据集规模。
第一个500步验证精度
{
'eval_loss': 0.2957316040992737,
'eval_accuracy': 0.910640437309614,
'eval_runtime': 156.6478,
'eval_samples_per_second': 3.83,
'eval_steps_per_second': 3.83,
'epoch': 0.19
}
第二个500步验证精度
{'eval_loss': 0.27959996461868286, 'eval_accuracy': 0.9174682889249636, 'eval_runtime': 158.7283, 'eval_samples_per_second': 3.78, 'eval_steps_per_second': 3.78, 'epoch': 0.37}
第三个500步验证
{
'eval_loss': 0.25857865810394287,
'eval_accuracy': 0.9208686929382228,
'eval_runtime': 158.2571,
'eval_samples_per_second': 3.791,
'eval_steps_per_second': 3.791,
'epoch': 0.56
}
llama factory只有在最后训练结束的时候才会把图生成出来,但是我们在openi平台上只有四个小时。所以我们可以在模型输出目录下找到train log文件自己绘图。
import pandas as pd
import matplotlib.pyplot as plt# 将日志数据转换为Pandas DataFrame
import json
log_data = []
trainer_log = open("sft/trainer_log.jsonl").readlines()
for trainer_log_one in trainer_log:trainer_log_data = json.loads(trainer_log_one)if "loss" in trainer_log_data:log_data.append(trainer_log_data)
df = pd.DataFrame(log_data)# 设置图表样式
plt.style.use('ggplot')# 绘制损失随训练步骤变化的图表
plt.figure(figsize=(12, 6))
plt.plot(df['current_steps'], df['loss'])
plt.title('Training Loss Over Steps')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.grid(True)
plt.savefig('Training Loss Over Steps')# 绘制学习率随训练步骤变化的图表
plt.figure(figsize=(12, 6))
plt.plot(df['current_steps'], df['learning_rate'])
plt.title('Learning Rate Over Steps')
plt.xlabel('Steps')
plt.ylabel('Learning Rate')
plt.grid(True)
plt.savefig('Learning Rate Over Steps')
验证集相关图表
import pandas as pd
import matplotlib.pyplot as plt# 将日志数据转换为Pandas DataFrame
import json
log_data = []
trainer_log = open("sft/trainer_log.jsonl").readlines()
for trainer_log_one in trainer_log:trainer_log_data = json.loads(trainer_log_one)if "eval_loss" in trainer_log_data:log_data.append(trainer_log_data)
df = pd.DataFrame(log_data)# 设置图表样式
plt.style.use('ggplot')# 绘制损失随训练步骤变化的图表
plt.figure(figsize=(12, 6))
plt.plot(df['current_steps'], df['eval_loss'])
plt.title('eval Loss Over Steps')
plt.xlabel('Steps')
plt.ylabel('Loss')
plt.grid(True)
plt.savefig('eval Loss Over Steps')
针对所出现的拟合过快的问题,我们提出以下的优化策略。
- 降低学习率(
learning_rate
):
学习率是影响模型训练速度和稳定性的重要因素。降低学习率可以让模型在训练过程中更加谨慎地更新权重,从而减慢拟合速度。learning_rate: 2.0e-5 # 从5.0e-5降低到2.0e-5
- 增加warmup步数或比例(
warmup_ratio
):
增加warmup的步数或比例可以让模型在前期以更慢的速度学习,这有助于模型在后期训练中更稳定。warmup_ratio: 0.2 # 从0.1增加到0.2
- 减少梯度累积步数(
gradient_accumulation_steps
):
减少梯度累积步数会增加每次更新权重的间隔,从而使模型的学习速度减慢。gradient_accumulation_steps: 1 # 从2减少到1
- 增加训练批次大小(
per_device_train_batch_size
):
增加批次大小可以提高训练的稳定性,但同时需要相应地调整学习率。per_device_train_batch_size: 2 # 从1增加到2,同时可能需要再次调整学习率
- 调整优化器的动量或重量衰减(如果使用的话):
对于使用动量或重量衰减的优化器,调整这些参数可以影响模型的收敛速度。# 假设使用的是AdamW优化器 optimizer:type: AdamWbetas: [0.8, 0.999] # 降低动量参数weight_decay: 0.01 # 增加重力衰减
- 使用更保守的lr调度器(
lr_scheduler_type
):
选择一个更保守的调度器,如linear
或cosine
的缓慢下降版本。lr_scheduler_type: linear # 从cosine改为linear
- 增加正则化:
增加L1或L2正则化可以防止模型过拟合,从而减慢拟合速度。# 增加L2正则化 optimizer:weight_decay: 0.01 # 增加这个值可以增加正则化
请记住,调整超参数是一个试验和错误的过程,可能需要多次尝试才能找到最佳的配置。每次调整后,都应该监控模型的性能,以确保它仍然在正确的方向上前进。
- 全参数量微调:其次,我利用qwen2-7B-instruct模型进行了全参数量微调,以探索模型在充分学习数据集方面的潜力。
最开始我也想全参数量微调qwen1.5 14B chat模型。目前观察的情况是会爆显存。所以暂时搁浅。
合并模型部分
通过对训练过程的观察,发现在2.5k步的时候验证损失是最低的。所以采用2.5k步的模型作为此次验证最优模型。这里我在第一个4小时结束训练后启动第二次四小时训练的开始选择模型合并操作。模型合并操作执行了将近半个小时。
### Note: DO NOT use quantized model or quantization_bit when merging lora adapters### model
model_name_or_path: pretrainmodel/Qwen1.5-14B-Chat
adapter_name_or_path: saves/qwen1.5-14b/full/sft/checkpoint-2500
template: qwen
finetuning_type: lora### export
##export_dir: /home/songzhijun/work/Langchain-Chatchat/longbao1
export_dir: models/Qwen1.5-14B-match
export_size: 2
export_device: cpu
export_legacy_format: false
生成答案部分
因为在openi平台中启动api接口后无法本地调用。所以这里我选择了使用huggingface transformers原生的办法进行生成提交数据的操作。
from transformers import AutoModelForCausalLM, AutoTokenizermodel_name = "models/Qwen1.5-14B-match"
device = "npu" # the device to load the model ontomodel = AutoModelForCausalLM.from_pretrained(model_name,torch_dtype="auto",device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)data_test = json.load(open("round1_training_data/test.json",'r'))outfile = open("tianyan_result.jsonl",'w',encoding="utf-8")
for data_test_one in data_test:messages = [{"role": "user", "content": "根据条款"+data_test_one['条款']+"回答问题"+data_test_one['问题']}]text = tokenizer.apply_chat_template(messages,tokenize=False,add_generation_prompt=True)model_inputs = tokenizer([text], return_tensors="pt").to(device)generated_ids = model.generate(**model_inputs,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]# data = response.json()outfile.write(json.dumps({"ID": str(data_test_one['ID']), "question": data_test_one['问题'], "answer": response}, ensure_ascii=False) + "\n")
计算资源
实验是在华为910B1 GPU上进行的,配备了64GB显存。这一配置足以支持大型语言模型的高效训练和微调。
我做采用的资源来自openi平台。每天启动云脑任务会给10积分。相当于两个半小时的910B1 64GB版本计算资源。
您的好友正在邀请您加入OpenI启智AI协作平台,畅享充沛的普惠算力资源(GPU/NPU/GCU/GPGPU/DCU/MLU)。
注册地址:https://openi.pcl.ac.cn/user/sign_up?sharedUser=nlp_future_01
推荐人:nlp_future_01
实验观察
在初步实验中,我发现模型在较短时间内便达到了较高的训练集准确率,显示出了快速拟合的趋势。这可能是由于数据集的大小相对于模型容量而言较小,导致过拟合现象。
7b lora 微调后
score:489.9103
14B lora 微调后
score:592.4397
参数调节与时间限制
为解决过拟合问题,我开始调整学习率、批次大小和正则化参数。此外,我还增加了Dropout比例,以增强模型的泛化能力。然而,openi平台的时间限制(每轮实验仅限4小时)为模型训练和验证带来了额外的挑战。我不得不精心设计实验计划,确保在有限时间内完成尽可能多的有效迭代。
结论
尽管面临时间限制和技术难题,这次竞赛经历极大地丰富了我的知识库,特别是在处理特定领域文本和优化模型训练流程方面。未来,我计划继续探索更高级的微调技术和模型架构,以提高模型在保险条款问答任务上的表现。
通过本次竞赛,我深刻体会到理论与实践结合的重要性,以及在有限资源下优化模型性能的挑战。希望我的经验能为同样热衷于自然语言处理领域的研究者和工程师们提供有价值的参考。