摘要:
合集:AI案例-NLP-泛金融
赛题:互联网舆情企业风险事件的识别和预警
主办方:世界人工智能创新大赛/AIWIN
主页:http://ailab.aiwin.org.cn/competitions/48
AI问题:文本分类
数据集:已标注的企业舆情信息数据约1万余条,内容包含新闻标题、正文、及对应标签等。
数据集发布方:海通证券
赛题任务:参赛选手从给定的互联网信息中提取、识别出企业主体名称,以及标记风险标签(内容包含新闻标题、正文、及对应标签等)。
数据集价值:基于企业舆情信息来挖掘潜在风险事件
解决方案:基于bert-base-chinese模型进行文本分类。
一、赛题描述
赛题背景
近些年来,资本市场违约事件频发,财务造假、董事长被抓、股权质押爆仓、城投非标违约等负面事件屡屡出现。而在大数据和人工智能技术加持下,各种新兴的金融风险控制手段也正在高速发展,其中通过采集互联网上的企业舆情信息来挖掘潜在风险事件是一种较为有效的方式。但这些风险信息散落在互联网上的海量资讯中,若能从中及时识别出涉及企业的风险事件,并挖掘出潜在的风险特征,将使得银行、证券等金融机构在风险监控领域中更及时、全面和直观地掌握客户风险情况,大幅提升识别和揭示风险的能力。而风险事件以文本的形式存在,需要采用人工智能方法进行自然语言理解,实现风险事件的高精度智能识别。
赛题任务
从给定的互联网信息中提取、识别出企业主体名称,以及标记风险标签。选手预测标签对应格式为(新闻ID,主体全称,对应风险标签)。
1)每篇互联网信息可能会涉及零到多个主体(公司),每篇互联网信息中对每个主体只预测一个风险标签;
2)赛事会提供一份主体(公司)的全称清单(其范围大于待预测名单),新闻中提及的主体可能为其简称或别名或主体相关的自然人(如其董事长、总经理等),选手提交答案时需要统一识别并将他们映射至主体全称输出在最终的结果文件中。主体全称的映射关系需选手自行处理。
3)请注意在训练集中存在一类「无」标签,其指的是对应的新闻内容中不包含需识别的金融风险事件。对于测试集中此类情况,选手模型在输出时只需准确打上「无」的标签,对应主体标记为「/」即可。即输出的为:”新闻 ID,/ ,无”。
4)测试集(需选手利用模型进行预测)的数据中会包含一些噪音数据,比如在主体(公司)的全称清单之外的舆情等,选手同样需要对其预测,不计入自动评分。
评价方式
将选手预测结果和答案进行对比计算出F1值,F1越大约好。
F1计算公式为:
P = 预测对的标签总数 / 预测出的标签数
R = 预测对的标签总数 / 需要预测的总标签数
F1 = 2 * P * R /(P+R)
二、数据集内容
以 CSV 格式提供已标注的企业舆情信息数据约1万余条,内容包含新闻标题、正文、及对应标签等。
元数据
列名 | 数据类型 | 备注 |
---|---|---|
NEWS_BASICINFO_SID | NUMBER(22) | 编号 |
NEWS_TITLE | VARCHAR2(3000) | 新闻标题 |
ABSTRACT | VARCHAR2(4000) | 摘要 |
CONTENT | CLOB | 正文 |
AUTHOR | VARCHAR2(1000) | 作者 |
SRC_URL | VARCHAR2(1000) | 源URL |
SOURCE_TYPE | VARCHAR2(100) | 01-新闻;02-论坛;03-博客;04-微博;05-平媒;06-微信;07-视频;08-长微博;09-APP;10-评论;99-其他 |
PUBLISH_SITE | VARCHAR2(100) | 发布网站 |
FIRST_WEB | VARCHAR2(100) | |
CHANNEL | VARCHAR2(100) | |
NOTICE_DT | DATE | |
COMPANY_NM | VARCHAR2(300) | 公司名称 |
LABEL | VARCHAR2(60) | 标注信息,包括:主板/创业板/中小板/债券退市 债务逾期 实控人变更 破产重整 股票质押率过高 被政府职能部门处罚 被监管机构罚款或查处 被采取监管措施 重大诉讼仲裁 信息披露违规 等具体参见训练数据 |
训练数据
部分列数据样例:3_Train_20210414_A1.xlsx
NEWS_BASICINFO_SID | NEWS_TITLE | CONTENT | COMPANY_NM | LABEL |
---|---|---|---|---|
ID | 新闻标题 | 正文 | 公司名称 | 风险标签 |
70000001 | 联化科技(德州)有限公司发生爆炸 两人死亡 | 联化科技(德州)有限公司发生爆炸 两人死亡 | 联化科技(德州)有限公司 | 安全事故 |
70000002 | 2005.02.06青岛啤酒股份有限公司火灾 | 2005.02.06青岛啤酒股份有限公司火灾 | 青岛啤酒股份有限公司 | 安全事故 |
70000003 | 2004.02.14河南链鑫科技有限公司火灾 | 2004.02.14河南链鑫科技有限公司火灾 | 河南链鑫科技有限公司 | 安全事故 |
公司列表
文件:2_Company_20210414_A1.xlsx
公司名
武汉卡伊娜化妆品有限公司
北京泰禾锦绣置业有限公司
长城新盛信托有限责任公司
招商银行股份有限公司
中国长城资产管理股份有限公司
航天通信控股集团股份有限公司
福建省海新食品有限公司龙海分公司
郑州煤矿机械集团股份有限公司
信保环球控股有限公司
...
三、解决方案样例
源码:baseline.ipynb。这段代码实现了一个中文新闻分类系统,结合了传统机器学习方法和深度学习方法。
安装
参考《安装深度学习框架PyTorch》和《安装Huggingface-Transformers》,同时安装以下两个开发库:
conda install jieba
conda install openpyxl
部署模型bert-base-chinese在当前工程目录下./bert-base-chinese。
导入开发库
import pandas as pd
import jieba
import numpy as np
import seaborn as sns
TF-IDF + Ridge分类器
这是传统机器学习方法。Ridge分类器(Ridge Classifier)是一种基于线性回归的分类算法,它是Ridge回归(L2正则化线性回归)在分类问题上的应用。它通过将目标变量编码为{-1, 1}(二分类)或使用one-hot编码(多分类),然后使用回归预测连续值。最终通过预测值的符号(二分类)或最大值(多分类)来确定类别。
- 使用jieba进行中文分词。进行风险标签LABEL字段的识别。
- 使用TF-IDF将文本转换为特征向量
- 使用RidgeClassifier进行分类
- 按照8:2的比例分割训练集和验证集
from sklearn.preprocessing import LabelEncoder
train_data = train_data.sample(frac = 1.0)
lbl = LabelEncoder().fit(train_data['LABEL'])
train_data['LABEL'] = lbl.transform(train_data['LABEL'])
import jieba
def strcut(s):
seg_list = jieba.cut(s)
return ' '.join(list(seg_list))
train_title = train_data['NEWS_TITLE'].apply(strcut)
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(ngram_range=(1,1))
train_title_ttidf = tfidf.fit_transform(train_title)
from sklearn.model_selection import cross_val_score
from sklearn.linear_model import RidgeClassifier
clf = RidgeClassifier()
from sklearn.model_selection import train_test_split
tr_x, val_x, tr_tfidf, val_tfidf, tr_y, val_y = train_test_split(
train_data['NEWS_TITLE'], train_title_ttidf, train_data['LABEL'],
stratify = train_data['LABEL'],
test_size=0.2
)
clf.fit(tr_tfidf, tr_y)
clf.score(val_tfidf, val_y)
输出预测准确率:0.8694908001711597
分割训练集和验证集
解析 train_test_split
返回的参数
这段代码中的 train_test_split
函数返回了6个参数,它们分别是:
tr_x, val_x, tr_tfidf, val_tfidf, tr_y, val_y = train_test_split(
train_data['NEWS_TITLE'], train_title_ttidf, train_data['LABEL'],
stratify=train_data['LABEL'],
test_size=0.2
)
a、参数详解
tr_x
(训练集原始文本)- 类型: Pandas Series
- 内容: 原始新闻标题文本(未分词)
- 用途: 主要用于BERT模型的输入(后续代码中用于生成BERT的tokenizer输入)
val_x
(验证集原始文本)- 类型: Pandas Series
- 内容: 原始新闻标题文本(未分词)
- 用途: 用于BERT模型的验证
tr_tfidf
(训练集TF-IDF特征)- 类型: Scipy稀疏矩阵(通常是CSR格式)
- 内容: 经过分词和TF-IDF向量化后的训练集特征矩阵
- 用途: 用于传统机器学习模型(RidgeClassifier)的训练
val_tfidf
(验证集TF-IDF特征)- 类型: Scipy稀疏矩阵
- 内容: 经过分词和TF-IDF向量化后的验证集特征矩阵
- 用途: 用于传统机器学习模型的验证
tr_y
(训练集分类标签)- 类型: Pandas Series 或 numpy数组
- 内容: 编码后的分类标签(0-12)
- 用途: 作为训练集的真实分类标签
val_y
(验证集分类标签)- 类型: Pandas Series 或 numpy数组
- 内容: 编码后的分类标签(0-12)
- 用途: 作为验证集的真实分类标签
b、为什么这样拆分?
这段代码同时进行了两种模型的训练准备:
- 传统机器学习模型(RidgeClassifier)
- 使用TF-IDF特征(
tr_tfidf/val_tfidf
)和标签(tr_y/val_y
)
- 使用TF-IDF特征(
- BERT模型
- 使用原始文本(
tr_x/val_x
)和标签(tr_y/val_y
)
- 使用原始文本(
c、数据流示意图
原始数据:
[NEWS_TITLE(text)] → 分词 → TF-IDF → [TF-IDF矩阵]
[COMPANY_NM] (未直接使用)
[LABEL] → LabelEncoder → [编码标签]
拆分后:
训练集:
tr_x: 原始文本(用于BERT)
tr_tfidf: TF-IDF矩阵(用于RidgeClassifier)
tr_y: 编码标签
验证集:
val_x: 原始文本(用于BERT)
val_tfidf: TF-IDF矩阵(用于RidgeClassifier)
val_y: 编码标签
d、注意事项
stratify=train_data['LABEL']
确保了训练集和验证集中各类别的比例与原始数据集一致test_size=0.2
表示验证集占20%,训练集占80%- 虽然输入了两个不同的特征集(原始文本和TF-IDF矩阵),但由于它们来自相同的数据,拆分时保持了样本对应关系
Bert建模分类
1、定义中文BERT模型
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
train_encoding = tokenizer(list(tr_x), truncation=True, padding=True, max_length=128)
val_encoding = tokenizer(list(val_x), truncation=True, padding=True, max_length=128)
- 使用中文BERT模型进行文本编码
- 设置最大长度为128,并进行截断和填充
2、自定义数据集类
class TextDataset(Dataset):
def __init__(self, encodings, labels):
self.encodings = encodings
self.labels = labels
def __getitem__(self, idx):
item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
item['labels'] = torch.tensor(self.labels.iloc[idx], dtype=torch.long)
return item
def __len__(self):
return len(self.labels)
- 自定义PyTorch数据集类,处理BERT的输入格式
3、模型初始化
from transformers import BertForSequenceClassification
model = BertForSequenceClassification.from_pretrained('bert-base-chinese', num_labels=13)
model.to(device)
- 加载预训练的中文BERT模型
- 设置为13分类任务(根据数据中的标签数量)
4、定义训练函数
训练函数的定义,包括前向传播、损失计算、反向传播和优化。
def train():
model.train()
total_train_loss = 0
iter_num = 0
total_iter = len(train_loader)
for batch in train_loader:
optim.zero_grad()
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
# loss = outputs[0]
# loss = loss_function(outputs[1], labels)
loss = loss_function(outputs[1], labels.long())
total_train_loss += loss.item()
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optim.step()
scheduler.step()
iter_num += 1
if(iter_num % 100==0):
print("epoth: %d, iter_num: %d, loss: %.4f, %.2f%%" % (epoch, iter_num, loss.item(), iter_num/total_iter*100))
print("Epoch: %d, Average training loss: %.4f"%(epoch, total_train_loss/len(train_loader)))
- 实现了完整的训练和验证流程
- 使用AdamW优化器和学习率调度器
5、定义验证函数
定义验证过程函数,基于训练的模型计算验证集上的准确率和损失。
def flat_accuracy(preds, labels):
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
return np.sum(pred_flat == labels_flat) / len(labels_flat)
def validation():
model.eval()
total_eval_accuracy = 0
total_eval_loss = 0
for batch in test_dataloader:
with torch.no_grad():
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
outputs = model(input_ids, attention_mask=attention_mask, labels=labels)
loss = outputs[0]
logits = outputs[1]
total_eval_loss += loss.item()
logits = logits.detach().cpu().numpy()
label_ids = labels.to('cpu').numpy()
total_eval_accuracy += flat_accuracy(logits, label_ids)
avg_val_accuracy = total_eval_accuracy / len(test_dataloader)
print("Accuracy: %.4f" % (avg_val_accuracy))
print("Average testing loss: %.4f"%(total_eval_loss/len(test_dataloader)))
print("-------------------------------")
验证过程的数据流图如下
原始文本
↓ (通过TextDataset和DataLoader)
batch = {
'input_ids': [batch_size, 128],
'attention_mask': [batch_size, 128],
'labels': [batch_size]
}
↓ (输入模型)
outputs = (loss, logits)
↓
loss.backward() (训练时) / 仅计算指标(验证时)
↓
logits → argmax → 预测类别
labels → 真实类别
↓
比较预测与真实 → 准确率
输入分析
- 模型输入 (直接输入)
input_ids
: 经过tokenizer编码后的文本ID序列- 形状: [batch_size, max_seq_length] (代码中max_length=128)
- 示例:
[101, 2345, 3456, ..., 102, 0, 0, ..., 0]
(含[CLS],[SEP]和padding)
attention_mask
: 注意力掩码,标识真实token与padding部分- 形状: 同input_ids
- 示例:
[1, 1, 1, ..., 1, 0, 0, ..., 0]
(1=真实token,0=padding)
- 监督信号 (间接输入)
labels
: 真实的分类标签- 形状: [batch_size]
- 类型: torch.LongTensor
- 示例:
[3, 7, 0, ..., 11]
(范围0-12,共13类)
输出分析
- 模型原始输出
outputs
: 包含两个部分的元组outputs[0]
: 损失值(loss)- 类型: torch.Tensor (标量)
- 计算方式: 交叉熵损失
outputs[1]
: 未归一化的分类logits- 形状: [batch_size, num_labels] (num_labels=13)
- 示例:
[[1.2, -0.3, ..., 2.1], ..., [0.5, -1.2, ..., 1.8]]
- 验证指标计算
logits
: 模型预测的未归一化分数- 通过
detach().cpu().numpy()
转换为numpy数组
- 通过
label_ids
: 真实标签的numpy数组flat_accuracy()
计算:def flat_accuracy(preds, labels):
pred_flat = np.argmax(preds, axis=1).flatten()
labels_flat = labels.flatten()
return np.sum(pred_flat == labels_flat) / len(labels_flat)- 输入: logits和真实标签
- 输出: 本batch的准确率(0-1之间)
6、执行训练和验证循环
for epoch in range(4):
print("------------Epoch: %d ----------------" % epoch)
train()
validation()
- 进行4个epoch的训练
训练过程输出展示:
------------Epoch: 0 ----------------
epoth: 0, iter_num: 100, loss: 0.5992, 34.13%
epoth: 0, iter_num: 200, loss: 0.6271, 68.26%
...