摘要:
合集:AI案例-自然语言处理-传媒/金融
赛题:AIWIN2023—研报类型识别挑战赛
主办方:国泰君安
主页:http://www.aiwin.org.cn/competitions/87
AI问题:文本分类
数据集:10种类型的市场研究分析报告,报告总数为3122。
数据集价值:市场研究分析报告类型识别
解决方案:chinese-roberta-wwm-ext模型、transformers开发框架
一、赛题描述
随着经济发展,中国居民财富持续增长。资产管理需求日益增多,资产管理业务迎来新的发展机遇。市场对机构的投资管理能力提出了更高的要求。近年来,国泰君安积极建设数字化投资研究平台以助力投资管理业务。公司希望借鉴国内外投研的先进经验,通过数字化与智能化技术为投研业务赋能,实现多源异构研究数据融合、产业投资逻辑的知识沉淀和投研过程的提质增效,从而提升研究效率,增强公司在资产管理领域的核心竞争力。在投资管理业务的研究过程中,研究人员需要阅读和分析各个券商机构制作的研究分析报告。他们需要从这些报告中提取出有价值的关键信息,包括研报分析的个股、当前评级、目标价和盈利预测数据等。这部分研究工作较为繁琐,会耗费研究人员大量的时间和精力。
随着人工智能技术的发展,许多金融机构开始将自然语言处理技术引入到金融文本分析领域,如情感分析、舆情预警和实体识别等。这些工作通常是针对金融纯文本任务,实际上金融领域还有大量的富文本语料有待挖掘和分析,例如上市公司公告、研究机构研究分析报告等。这些报告大多都是PDF格式,其中包含文本、图表和表格等元素,这些元素语义丰富,具有很高的研究价值。基于上述分析,我们希望利用人工智能技术从研报PDF中自动抽取出关键信息并组织成结构化的数据进行分析。具体地,我们结合自然语言处理与计算机视觉相关技术,设计了一套研究报告(以下简称研报)关键信息要素抽取解决方案。该方案包含研报文件解析、研报类型分析和研报要素抽取等功能。
本赛题任务是利用机器学习、深度学习等方法训练一个预测模型,该模型主要针对各种各样的研报进行类型分析。
二、数据集描述
数据集内容
本赛题将10种类型的研报数据会划分为训练集、测试集。训练集用于模型架构设计、模型训练,在测试集上验证效果。以macro precision/recall/f1三个模型评估指标为验证标准。
- 训练集文件:train_dataset.npy
- 测试集文件:eval_dataset.npy
类型标签
labels = ['晨会早报', '宏观研报','策略研报','行业研报','公司研报','基金研报','债券研报','金融工程','其他研报','个股研报']
label2id = {
'晨会早报': 0,
'宏观研报': 1,
'策略研报': 2,
'行业研报': 3,
'公司研报': 4,
'基金研报': 5,
'债券研报': 6,
'金融工程': 7,
'其他研报': 8,
'个股研报': 9
}
训练数据集
训练数据从 ./data/train_dataset.npy
加载,转换为 Pandas DataFrame,包含以下 5列:
train_data = pd.DataFrame(list(np.load(config.train_data, allow_pickle=True)))
train_data.columns = ['label', 'header', 'title', 'paragraph', 'footer']
列说明
列名 | 数据类型 | 描述 |
---|---|---|
label | int | 研报类型标签(对应 config.label_dict 中的数值,如 0=晨会早报 ) |
header | str | 研报的页眉部分(通常包含机构名称、保密声明等,如 "仅供内部参考,请勿外传" ) |
title | str | 研报标题(包含报告类型、日期等,如 "证券研究报告 | 浙商早知道" ) |
paragraph | str | 正文内容(核心文本,可能包含摘要、分析等) |
footer | str | 页脚部分(通常包含免责声明、页码等) |
数据预处理
在训练前,代码将多列文本拼接为单列 text
,作为模型的输入:
train_data['text'] = train_data['header'] + '[SEP]' + train_data['title'] + '[SEP]' + train_data['paragraph'] + '[SEP]' + train_data['footer']
拼接方式:用 [SEP]
(分隔符)连接四部分文本,模拟BERT的输入格式。
最终输入样例:
"仅供内部参考,请勿外传[SEP]证券研究报告 | 浙商早知道[SEP]党的二十大报告提出...[SEP]请务必阅读正文之后的免责条款部分"
提交文件
提交文件为:submission.csv。字段定义为:columns = [‘uid’, ‘label’]。
数据集版权许可协议
BY-NC-SA 4.0
数据集发布方:国泰君安
https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans
三、解决方案样例
工作流程
样例代码实现了完整的机器学习流程,包括:
- 配置管理(Config类)
- 数据集处理(MyDataset类)
- 数据预处理
- 模型加载与配置
- 训练流程(train函数)
- 验证流程(val函数)
- 预测流程(predict函数)
模型简介
chinese-roberta-wwm-ext 是由哈工大(HIT)和科大讯飞联合发布的中文预训练语言模型,基于 RoBERTa 架构改进,专门针对中文文本优化。它在多项中文 NLP 任务中表现出色,是中文自然语言处理领域的重要基线模型之一。以下是其核心特点的介绍:
1. 全词掩码(Whole Word Masking, WWM)
- 传统掩码:BERT 对单个字符进行随机掩码(如
机 -> [MASK]
)。 - 全词掩码:对完整词语(如
机器学习 -> [MASK][MASK]
)进行掩码,更适合中文分词特性。 - 改进效果:增强模型对词语整体语义的理解,尤其在实体识别、文本分类等任务中表现更好。
2. 动态掩码(Dynamic Masking)
- 在训练过程中动态生成掩码模式(而非静态预生成),避免模型过拟合固定掩码模式。
- 优点:提升模型泛化能力。
3. 扩展训练数据与步数
- 训练数据:使用更大规模的 中文维基百科、新闻语料、书籍文本 和 网页数据(总计约 5.4B 字符)。
- 训练步数:比原始 BERT 更长(如 1M 步),充分学习语言模式。
4. 移除 NSP 任务
遵循 RoBERTa 的设计,去除了下一句预测(NSP)任务,专注于单句或跨句的掩码语言建模(MLM)。
运行环境
参考文章:《安装深度学习框架PyTorch》和《安装Huggingface-Transformers》
库名称 | 版本号 |
---|---|
python | 3.12.3 |
transformers | 4.49.0 |
torch | 2.5.1 |
sklearn-compat | 0.1.3 |
四、工作流程
源码:Identification_Of_Research_Report_Types.ipynb
导入开发包:
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, f1_score
import torch
import torch.nn.functional as F
from torch import optim
from torch.utils.data import DataLoader, Dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
1. 配置管理 (Config
类)
- 功能:集中管理所有配置参数,包括数据路径、模型路径、超参数等。
- 关键参数:
device
: 自动检测并设置设备(优先使用GPU)。model_path
: 预训练模型路径(如chinese-roberta-wwm-ext
)。max_seq_len
: 输入文本的最大长度(128)。batch_size
: 训练和验证的批次大小。
- 作用:全局配置,简化参数传递。
class Config():
train_data = './data/train_dataset.npy' # 训练集
predict_data = './data/eval_dataset.npy' # 测试集
result_data_save = './result/submission.csv' # 预测结果
device = 'cpu' # 训练驱动
model_path = './chinese-roberta-wwm-ext' # 预训练模型
model_save_path = './result/model' # 保存模型
tokenizer = None # 预训练模型的tokenizer
# 数据标签
label_dict = {'晨会早报': 0, '宏观研报': 1, '策略研报': 2, '行业研报': 3, '公司研报': 4, '基金研报': 5, '债券研报': 6, '金融工程': 7, '其他研报': 8, '个股研报': 9}
num_labels = len(label_dict) # 标签数量
max_seq_len = 128 # 最大句子长度
test_size = 0.15 # 校验集大小
random_seed = 42 # 随机种子
batch_size = 64 # 训练数据批大小
val_batch_size = 8 # 校验/预测批大小
epochs = 2 # 训练次数
learning_rate = 1e-5 # 学习率
l2_weight_decay = 0.05
print_log = 20 # 日志打印步骤
2. 数据集处理 (MyDataset
类)
- 功能:自定义数据集类,处理文本编码和数据加载。
- 步骤:
- 初始化:接收数据列表和标签,加载配置中的
tokenizer
。 - 文本编码:使用
tokenizer.encode_plus
对文本进行编码,生成input_ids
,token_type_ids
,attention_mask
。 - 设备转换:将编码后的张量移动到指定设备(CPU/GPU)。
- 分类标签处理:若存在分类标签,将其转换为张量。
- 初始化:接收数据列表和标签,加载配置中的
- 输出:每个样本的编码结果和分类标签。
{
'input_ids': torch.Tensor, # 分词后的ID序列
'token_type_ids': torch.Tensor, # 区分不同文本段(如[SEP]前后)
'attention_mask': torch.Tensor, # 标记有效token位置
'labels': torch.Tensor # 分类标签(训练时存在)
}
源码:
class MyDataset(Dataset):
def __init__(self, config: Config, data: list, label: list = None):
self.data = data
self.tokenizer = config.tokenizer
self.max_seq_len = config.max_seq_len
self.len = len(data)
self.label = label
def __getitem__(self, idx):
text = self.data[idx]
# tokenizer
inputs = self.tokenizer.encode_plus(text, return_token_type_ids=True, return_attention_mask=True,
max_length=self.max_seq_len, padding='max_length', truncation=True)
# 打包预处理结果
result = {'input_ids': torch.tensor(inputs['input_ids'], dtype=torch.long),
'token_type_ids': torch.tensor(inputs['token_type_ids'], dtype=torch.long),
'attention_mask': torch.tensor(inputs['attention_mask'], dtype=torch.long)}
if self.label is not None:
result['labels'] = torch.tensor([self.label[idx]], dtype=torch.long)
# 返回
return result
def __len__(self):
return self.len
3. 数据预处理
- 加载数据:从
.npy
文件加载训练数据,转换为 DataFrame。 - 文本拼接:将
header
,title
,paragraph
,footer
用[SEP]
拼接为text
字段。 - 数据划分:使用
train_test_split
划分训练集和验证集(15% 测试集)。 - DataLoader 构建:为训练和验证集创建数据加载器,支持批次处理和打乱顺序。
train_data = pd.DataFrame(list(np.load(config.train_data, allow_pickle=True)))
train_data.head(5)
4. 模型加载与配置
- 加载预训练模型:使用
AutoModelForSequenceClassification
加载中文 RoBERTa 模型,设置分类标签数 (num_labels=10
)。 - 配置 Tokenizer:将加载的
tokenizer
存入config
供后续使用。 - 设备迁移:模型被移动到
config.device
(GPU 或 CPU)。
tokenizer = AutoTokenizer.from_pretrained(config.model_path)
model = AutoModelForSequenceClassification.from_pretrained(config.model_path, num_labels=config.num_labels)
config.tokenizer = tokenizer
5. 验证流程 (val
函数)
- 模型评估模式:
model.eval()
禁用 dropout 和 BatchNorm。 - 指标计算:遍历验证集,计算平均损失、准确率、加权 F1。
- 结果返回:返回验证集指标供训练过程监控。
def val(model, val_dataloader: DataLoader):
model.eval()
total_acc, total_f1, total_loss, test_num_batch = 0., 0., 0., 0
for iter_id, batch in enumerate(val_dataloader):
# 转GPU
batch_cuda = {item: value.to(config.device) for item, value in batch.items()}
# 模型计算
output = model(**batch_cuda)
# 获取结果
loss = output[0]
logits = torch.argmax(output[1], dim=1)
y_pred = [[i] for i in logits.cpu().detach().numpy()]
y_true = batch_cuda['labels'].cpu().detach().numpy()
# 计算指标
acc = accuracy_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred, average='weighted')
total_loss += loss.item()
total_acc += acc
total_f1 += f1
test_num_batch += 1
return total_loss/test_num_batch, total_acc/test_num_batch, total_f1/test_num_batch
6. 训练流程 (train
函数)
- 优化器配置:
- 参数分组:对非
bias
和LayerNorm
参数应用 L2 正则化。 - 使用
AdamW
优化器和余弦退火学习率调度器 (CosineAnnealingLR
)。
- 参数分组:对非
- 训练循环:
- 前向传播:输入数据到模型,计算损失 (
loss
) 和 logits。 - 反向传播:梯度清零 → 计算梯度 → 参数更新 → 学习率调整。
- 日志记录:每隔
print_log
步打印训练指标(损失、准确率、F1)。
- 前向传播:输入数据到模型,计算损失 (
- 验证与保存:
- 每个 epoch 结束后验证模型,保存最佳模型(基于验证集 F1)。
- 保存最终模型和 tokenizer 到指定路径。
def train(model, config: Config, train_dataloader: DataLoader, val_dataloader: DataLoader):
# 模型写入GPU
model.to(config.device)
# 获取BERT模型的所有可训练参数
params = list(model.named_parameters())
# 对除了bias和LayerNorm层的所有参数应用L2正则化
no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in params if not any(nd in n for nd in no_decay)],
'weight_decay': config.l2_weight_decay},
{'params': [p for n, p in params if any(nd in n for nd in no_decay)],
'weight_decay': 0.0}
]
# 创建优化器并使用正则化更新模型参数
opt = torch.optim.AdamW(optimizer_grouped_parameters, lr=config.learning_rate)
# 梯度衰减
scheduler = optim.lr_scheduler.CosineAnnealingLR(opt, len(train_dataloader) * config.epochs)
# 遍历训练
best_f1 = 0
for epoch in range(config.epochs):
total_acc, total_f1, total_loss, train_num_batch = 0., 0., 0., 0
model.train()
for iter_id, batch in enumerate(train_dataloader):
# 数据写入GPU
batch_cuda = {item: value.to(config.device) for item, value in batch.items()}
# 模型计算
output = model(**batch_cuda)
# 获取结果
loss = output[0]
logits = torch.argmax(output[1], dim=1)
y_pred = [[i] for i in logits.cpu().detach().numpy()]
y_true = batch_cuda['labels'].cpu().detach().numpy()
# 计算指标
acc = accuracy_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred, average='weighted')
total_loss += loss.item()
total_acc += acc
total_f1 += f1
# 反向传播,更新参数
opt.zero_grad()
loss.backward()
opt.step()
scheduler.step()
# 打印
if iter_id % config.print_log == 0:
logging.info('epoch:{}, iter_id:{}, loss:{}, acc:{}, f1:{}'.format(epoch, iter_id, loss.item(), acc, f1))
train_num_batch += 1
# 校验操作
val_loss, val_acc, val_f1 = val(model, val_dataloader)
if val_f1 > best_f1:
best_f1 = val_f1
# 保存best模型
config.tokenizer.save_pretrained(config.model_save_path + "/best")
model.save_pretrained(config.model_save_path + "/best")
logging.info('-' * 15 + str(epoch) + '-' * 15)
logging.info('avg_train_loss:{}, avg_train_acc:{}, avg_train_f1:{}'.format(total_loss/train_num_batch, total_acc/train_num_batch, total_f1/train_num_batch))
logging.info('val_loss:{}, val_acc:{}, val_acc:{}, best_f1:{}'.format(val_loss, val_acc, val_f1, best_f1))
logging.info('-' * 30)
# 保存最终模型
config.tokenizer.save_pretrained(config.model_save_path)
model.save_pretrained(config.model_save_path)
训练过程:
INFO:root:epoch:0, iter_id:0, loss:2.4005253314971924, acc:0.0625, f1:0.016010710304188564
INFO:root:epoch:0, iter_id:20, loss:1.6295055150985718, acc:0.546875, f1:0.4424894957983193
...
7. 预测流程 (predict
函数)
- 加载模型:从保存路径加载最佳模型和 tokenizer。
- 数据预处理:处理测试数据,拼接文本字段。
- 推理:使用
DataLoader
批量处理数据,生成预测结果和 softmax 概率。 - 结果保存:将预测的
label
写入 CSV 文件(包含uid
和label
)。
def predict(config:Config):
# 加载模型
config.tokenizer = AutoTokenizer.from_pretrained(config.model_save_path)
model = AutoModelForSequenceClassification.from_pretrained(config.model_save_path)
model.to(config.device)
model.eval()
# 加载数据
test_data = pd.DataFrame(list(np.load(config.predict_data, allow_pickle=True)))
test_data['text'] = test_data['header'] + '[SEP]' + test_data['title'] + '[SEP]' + test_data['paragraph'] + '[SEP]' + test_data['footer']
# 加载dataloader
predict_dataloader = DataLoader(MyDataset(config, test_data['text'].tolist()), batch_size=config.val_batch_size, shuffle=False)
predict_result = []
predict_softmax = []
softmax = None
# 遍历预测
for iter_id, batch in enumerate(predict_dataloader):
batch_cuda = {item: value.to(config.device) for item, value in batch.items()}
# 模型计算
output = model(**batch_cuda)
# 获取结果
logits = torch.argmax(output[0], dim=1)
y_pred = [[i] for i in logits.cpu().detach().numpy()]
# 获取softmax
y_softmax = [i for i in F.softmax(output.logits, dim=1).cpu().detach().numpy()]
# 统计结果
predict_result += y_pred
predict_softmax += y_softmax
# 输出结果
test_data['label'] = [i[0] for i in predict_result]
# 保存文件
test_data[['uid', 'label']].to_csv(config.result_data_save, index=False, encoding='utf-8')
本样例输出的 label 示例如下:其中label为预测后的研报类型。
task_id | label |
---|---|
9c6fa283058e35d6b3d8b4feeb30639b | 9 |
6ba8b3e959643c428012990292d8a675 | 4 |
9dd0e681dc1c305fb3166e5b0b15a324 | 9 |
dcbb390e9539398ab74db13ae4f51a85 | 9 |
fd5d8e2790f23c70a2318cb0acab303d | 9 |
源码开源协议
GPL-v3