文章目录
I-比赛简介
CCF大数据与计算智能大赛(CCF Big Data & Computing Intelligence Contest,简称CCF BDCI)由中国计算机学会于2013年创办,是大数据与人工智能领域的算法、应用和系统大型挑战赛事。2022年,是第十届CCF BDCI。
本赛题为大赛赛题之一,由智慧芽公司联合举办。
II-赛题简介
本赛题为专利文本分类比赛。智慧芽作为国际领先的知识产权SaaS平台,根据用户的搜索习惯等因素,制定了一套新的专利分类体系。
比赛方公开958条有监督专利数据,包括专利权人、专利标题、专利摘要和分类标签,其中分类标签经过脱敏处理,共36类。要求选手设计一套算法,完成测试数据的分类任务。
参赛队伍1426支。
A榜测试集20839条。
B榜测试集20890条。
III-数据分析
-
专利摘要文本长度
# 摘要文本长度分布直方图 df['abstract_len'] = df['abstract'].apply(lambda x : len(x)) df['abstract_len'].hist()
# 摘要文本长度统计值,最大长度、最小长度、平均长度等 df['abstract'].describe()
-
标签分布情况
# 标签分布直方图 import matplotlib.pyplot as plt df['label_id_value'] = df['label_id'].apply(int) plt.hist(df['label_id_value'], 36)
# 标签类别数
from collections import Counter
Counter(df['label_id'])
IV-Baseline
以fine-tuning 的方式进行训练,采用 Pytorch 框架,加载 bert-base-chinese 模型。
训练集、验证集划分比例为9:1.
-
加载需要的库
import torch from transformers import Trainer, TrainingArguments, DefaultFlowCallback, AdamW, get_constant_schedule,BertTokenizerFast, BertForSequenceClassification from sklearn.model_selection import train_test_split from sklearn.metrics import f1_score import pandas as pd from tqdm import tqdm import json from torch.utils.data import Dataset
-
加载模型
# BertForSequenceClassification # BertTokenizerFast model = BertForSequenceClassification.from_pretrained('/root/bert-base-chinese', num_labels=36) tokenizer = BertTokenizerFast.from_pretrained('/root/bert-base-chinese')
-
加载训练数据
file = open("../Datas/train.json", 'r', encoding='utf-8') trains = [] for line in file.readlines(): dic = json.loads(line) trains.append(dic) df=pd.DataFrame(trains)
-
构建输入文本
df['input_string'] = "这份专利的标题为:《"+df['title']+"》,由“"+df['assignee']+"”公司申请,详细说明如下:"+df['abstract']
-
划分数据集
df_train, df_dev = train_test_split(df, test_size=0.1, random_state=50) df_train = df_train.reset_index(drop=True) # 有shuffle和划分 df_dev = df_dev.reset_index(drop=True)
-
构建数据集
class BertDataset(Dataset): def __init__(self, data, tokenizer): self.data = data self.input_encodings = tokenizer([data.loc[i,'input_string'] for i in range(len(data))], max_length=512, truncation=True, padding=True) self.targets = data['label'].astype(int) def __len__(self): return len(self.targets) def __getitem__(self, index): return { 'input_ids': torch.tensor(self.input_encodings['input_ids'][index]), 'token_type_ids': torch.tensor(self.input_encodings['token_type_ids'][index]), 'attention_mask': torch.tensor(self.input_encodings['attention_mask'][index]), 'labels': torch.tensor(self.targets[index]) } train_dataset = BertDataset(df_train, tokenizer) dev_dataset = BertDataset(df_dev, tokenizer)
-
模型训练
def compute_metrics(pred): labels = pred.label_ids preds = pred.predictions.argmax(-1) f1 = f1_score(preds, labels, average='macro') print('f1: ', f1) return {'f1': f1} # Total optimization steps=768*num_train_epochs/per_device_train_batch_size/gradient_accumulation_steps training_args = TrainingArguments( output_dir='../ckpts', num_train_epochs=30, per_device_train_batch_size=8, gradient_accumulation_steps=2, # 显存不够,需要减小batch size per_device_eval_batch_size=32, # 2*per_device_train_batch_size dataloader_num_workers=8, logging_dir='../logs', logging_steps=50, evaluation_strategy="steps", eval_steps=50, save_steps=50, fp16=False, save_total_limit=10, load_best_model_at_end=True, metric_for_best_model='f1', report_to=[], ) optimizer = AdamW(model.parameters(), lr=1e-5, correct_bias=True) trainer = Trainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=dev_dataset, optimizers= (optimizer, get_constant_schedule(optimizer, last_epoch=-1)), compute_metrics=compute_metrics ) trainer.train() trainer.save_model('../best_ckpt/')
-
使用模型预测结果
model = BertForSequenceClassification.from_pretrained('../best_ckpt/', num_labels=36) model = model.to('cuda') tokenizer = BertTokenizerFast.from_pretrained('anferico/bert-for-patents') df_test = pd.read_csv('../Datas/testA_en.csv', delimiter=',') df_test['input_string'] = "The title of the patent is《" + df_test['title'] + "》,is applied by"+df_test['ass'] + "and the detailed description is:" + df_test['abs'] df_test['input_string'] = df_test['input_string'].apply(str) BATCH_SIZE = 8 total_batch_num = int(len(df_test)/BATCH_SIZE) + 1 outputs_list = [] for batch_num in tqdm(range(total_batch_num)): batch_input = df_test[batch_num*BATCH_SIZE:(batch_num+1)*BATCH_SIZE].reset_index(drop=True) if len(batch_input)==0: break tokens = tokenizer([batch_input.loc[i,'input_string'] for i in range(len(batch_input))], return_tensors="pt", max_length=512, truncation=True, padding=True).to('cuda') outputs = model(input_ids=tokens.input_ids, attention_mask=tokens.attention_mask, token_type_ids=tokens.token_type_ids) res_tmp = outputs['logits'].argmax(1).tolist() outputs_list.extend(res_tmp) del tokens, res_tmp assert len(df_test) == len(outputs_list) df_test['label'] = outputs_list df_test['label'] = df_test['label'].apply(int) df_test[['id','label']].to_csv('../submit.csv',index=False)
V-技术方案
通过数据分析阶段可以发现,36个类别的专利数据分布十分不均衡,因此可以从两方面(损失函数、数据增强)消除数据不均衡带来的影响。
1. 损失函数
Cross Entropy(标准交叉熵)
C E ( p , y ) = { − l o g ( p ) , i f ( y = 1 ) − l o g ( 1 − p ) , o t h e r w i s e CE(p,y)=\begin{cases} -log(p),if(y=1)\\ -log(1-p), otherwise \end{cases} CE(p,y)={−log(p),if(y=1)−log(1−p),otherwise
公式中,p代表样本在该类别的预测概率,y代表样本标签。可以看出,当标签为1时,p越接近1,则损失越小;标签为0时,p越接近0,则损失越小,符合优化的方向。
为了方便表示,将p标记为
p
t
=
{
p
,
i
f
(
y
=
1
)
1
−
p
,
o
t
h
e
r
w
i
s
e
p_t=\begin{cases} p,if(y=1)\\ 1-p, otherwise \end{cases}
pt={p,if(y=1)1−p,otherwise
则标准交叉熵损失函数可表示为
C
E
(
p
,
y
)
=
C
E
(
p
t
)
=
−
l
o
g
(
p
t
)
CE(p,y)=CE(p_t)=-log(p_t)
CE(p,y)=CE(pt)=−log(pt)
标准的交叉熵中所有样本的权重都是相同的,因此如果正、负样本不均衡,大量简单的负样本会占据主导地位,少量的难样本与正样本会起不到作用,导致精度变差。
使用Cross Entropy代码如下
# Cross Entropy
loss_func = torch.nn.CrossEntropyLoss()
Balanced Cross-Entropy(平衡交叉熵)
为了改善样本的不平衡问题,平衡交叉熵在标准的基础上增加了一个系数αt来平衡正、负样本的权重,α取值在[0,1]区间内。
α
t
=
{
α
,
i
f
(
y
=
1
)
1
−
α
,
o
t
h
e
r
w
i
s
e
α_t=\begin{cases} α,if(y=1)\\ 1-α, otherwise \end{cases}
αt={α,if(y=1)1−α,otherwise
有了权重后,平衡交叉熵损失函数为
C
E
(
p
t
)
=
α
t
C
E
(
p
t
)
=
−
α
t
l
o
g
(
p
t
)
CE(p_t)=α_tCE(p_t)=-α_tlog(p_t)
CE(pt)=αtCE(pt)=−αtlog(pt)
使用Balanced Cross-Entropy代码如下
# 根据训练集的不同类别样本数量设置平衡交叉熵函数的权重
dt = pd.read_csv('/data/zhihuiya/train_en.csv', header=None, names=['label','title','abstract','assignee'])
class_weights = torch.FloatTensor(dt.label.value_counts(sort=False).values.tolist()).cuda()
loss_func = torch.nn.CrossEntropyLoss(weight=torch.div(torch.max(class_weights),class_weights))
Focal loss
Focal loss提出了调节难易样本。若在平衡正、负样本的基础上则公式如下,否则没有参数αt。
F
L
(
p
t
)
=
α
t
(
1
−
p
t
)
γ
C
E
(
p
t
)
=
−
α
t
(
1
−
p
t
)
γ
l
o
g
(
p
t
)
FL(p_t)=α_t(1-p_t)^γCE(p_t)=-α_t(1-p_t)^γlog(p_t)
FL(pt)=αt(1−pt)γCE(pt)=−αt(1−pt)γlog(pt)
i. αt是平衡正、负样本的权重;
ii. (1-pt)γ是为了调节难易样本的权重。当一个样本不易分类即总被分类错误时,pt较小,则(1-pt)γ接近于1,则损失几乎不受影响;当一个样本易分类时,pt较大,则(1-pt)γ接近于0,则损失影响被降低了;
iii. γ是一个调制因子,γ越大,简单样本损失的贡献会越低。
Focal loss针对二分类和多分类在网上有不同的实现方式,本赛题基于多分类,因此给出多分类的实现。
# Multi CE Focal Loss实现
class MultiCEFocalLoss(torch.nn.Module):
def __init__(self, class_num, gamma=2, alpha=None, reduction='mean'):
super(MultiCEFocalLoss, self).__init__()
if alpha is None:
self.alpha = Variable(torch.ones(class_num, 1))
else:
self.alpha = Variable(torch.full((class_num, 1), alpha))
self.gamma = gamma
self.reduction = reduction
self.class_num = class_num
def forward(self, predict, target):
pt = F.softmax(predict, dim=1) # softmmax获取预测概率
class_mask = F.one_hot(target, self.class_num) # 获取target的one hot编码
ids = target.view(-1, 1)
alpha = self.alpha[ids.data.view(-1)].view(-1, 1).cuda() # 注意,这里的alpha是给定的一个list(tensor),里面的元素分别是每一个类的权重因子
probs = (pt * class_mask).sum(1).view(-1, 1) # 利用onehot作为mask,提取对应的pt
log_p = probs.log()
# 同样,原始ce上增加一个动态权重衰减因子
loss = -alpha * (torch.pow((1 - probs), self.gamma)) * log_p
if self.reduction == 'mean':
loss = loss.mean()
elif self.reduction == 'sum':
loss = loss.sum()
return loss
# 使用focal loss, alpha、gamma是可调节参数
loss_func = MultiCEFocalLoss(class_num=36, alpha=0.25, gamma=2)
在本赛题中,对上述三种方式的损失函数均进行了应用,经过测试发现Balanced Cross-Entropy的效果最好。
通过查阅资料显示,Focal loss对二分类任务能够起到良好的作用,但对多分类任务效果不佳。
2. 数据增强
EDA
EDA(easy data augmentation)是一种应用于文本分类的简单的数据增强技术,由4种方法组成,分别是:同义词替换、随机插入、随机替换与随机删除,论文证明使用EDA的数据增强技术可以在分类任务中显著提升模型性能。
增强策略为:记录原始训练数据中类别数量最多的样本数max_nums, 将原始训练数据中类别数量少于max_nums的类别的数量通过数据增强增加到max_nums。
CBert
原理简介
Bert的输入为以下3种Embedding叠加:
- Token Embedding(词编码);
- Position Embedding(位置编码);
- Segment Embedding(区分句子对中两个句子的编码)
CBert认为传统的数据增强一般是基于替换的方法,而被替换词的同义词只能从原始文本中产生,十分受限。故提出基于Bert的MLM任务的数据增强方法,通过随机mask一定比例的token,根据上下文进行预测填空,但Bert的MLM任务是以无监督的方式执行的,不考虑标签的变化,极有可能出现如下情况:
“The flim is good.” -> “The flim is [MASK].” -> “The flim is bad.”
这将导致标签与增强语句的矛盾,成为噪声数据,干扰模型的学习。因此论文提出将Bert输入中的Segment Embedding替换成对应数据的标签,则输入如下图所示。 该模型收敛后,可以同时考虑上下文和标签,预测[MASK]位置的单词。
- Paper Conditional BERT Contextual Augmentation
- Code https://github.com/1024er/cbert_aug/tree/master
3. Prompt(提示学习)
除了上述两种数据层面的技术方案,接下来将从一种新的最近较为流行的为下游任务设计的一种模板或者范式——prompt展开讨论。
Fine-tuning:使用预训练模型去迁就我们的下游任务,也就是说根据具体的下游任务添加辅助loss然后反向梯度更新预训练模型中的参数,这样的话就不能很好的激发预训练模型的潜能。
Prompt:让我们的下游任务去迁就预训练模型,其实是尽量让下游任务和预训练相似,充分发挥预训练模型的潜能。简单的说就是将下游任务和预训练任务的统一(近似),比如说MLM。
简单来说Prompt由两部分构成:
-
Template:通过人工定义等方法生成与给定句子相关的含有[MASK]标记的模板,与原句拼接后输入到预训练模型。
-
Verbalizer:由于[MASK]位置预测的结果不一定是标签值,而是与标签相似的词,因此需要建立一个预测词到标签的映射关系。
PET
原理简介
PET: Pattern Exploiting Training ,是一种半监督学习方法,应用于few-shot learning。
PET训练的流程为:
-
训练 PVP(Pattern-Verbalizer Pair)模型
Single PVP
记预训练模型为M,其字典为V,标签集合为L,输入序列x=(s1,s2,…,sk)
-
首先定义一个pattern,将输入x转化为含有[MASK]的pattern序列,P(x)∈V*,表示序列中的所有元素来自于V;
-
同时定义一个verbalizer映射函数v:L->V,将每个标签l映射为字典中一个token v(l);
-
然后,输入P(x),模型做MLM任务,预测[MASK]位置的原始字符v(l),然后根据verbalizer反推到标签l∈L。
集成PVP
通过设置不同的pattern、verbalizer可以有多种PVP,对于每个p∈P,微调得到对应的PVP微调模型Mp。将多个PVP微调模型通过集成学习的思想组合为PVP集成模型M={Mp|p∈P}。
-
-
样本扩充
记有标签数据集为T,无标签数据集为D
使用得到的PVP集成模型M给无标签数据x∈D打标签,得到原本无标签数据的伪标签(soft label),记伪标签样本集合为Tc。
-
得到最终模型
使用新的PLM在Tc上训练,得到最终模型C。
注意在论文中这里没有加上有标签的数据T,此谓蒸馏。
上述PET过程中,不同的PVP微调模型是相互独立的,容易由于某些子模型精度不佳造成集成模型精度不佳的情况,最终导致伪标签准确度不理想的情况。
为解决这种问题,作者提出了iPET,用于迭代式增加数据集、训练模型。每次迭代分为两个步骤:
-
训练模型
在数据集上训练多个PVP微调模型Mp。
-
标注数据集
随机选择其他N个Mp组成PVP集成模型进行数据标注,选择置信度高的样本填充到标注集合T中,为模型Mij(第i模型第j次迭代)生成标注数据集Tij。
在工业界应用中,可以选择一些简单的模型(FastText、TextCNN)。
- Paper Exploiting Cloze Questions for Few Shot Text Classification and Natural Language Inference
- Code https://github.com/timoschick/pet
KPT
原理简介
KPT是针对Verbalizer通常是人工制作或基于梯度下降搜索的缺乏覆盖范围这一问题提出的。主要包括3个步骤。
-
构建Verbalizer
在做标签映射时融入外部知识,利用word embeddings,ConceptNet,WordNet等对映射词进行扩充*(不同粒度、客观、覆盖面广)*。
-
优化Verbalizer
通过4种方式对扩充后的词语进行细化,冗余词去除,得到最终的标签词空间
-
Frequency Refinement
通过上下文中的先验概率删除外部知识库得到的但在PLM中罕见的词。
-
Relevance Refinement
对每类标签假设一个中心词,得到类中所有词与中心词词向量的余弦相似度值,删除值低于1的词。
-
Contextualized Calibration
由于标签词的先验分布具有很大差异,无论输入句子的标签如何,但有一些标签词不可能被预测到。利用标签词的上下文先验分布来校准预测的分布。对预测概率与上下文分布做了一个对齐操作。
-
Learnable Refinement(在few-shot场景下)
为每个标签词分配一个可学习的权重。
-
-
使用Verbalizer
- zero-shot:简单地认为扩展词中每个词对于预测标签的贡献相同,因此对其进行简单平均,并用预测分数的均值作为该标签的预测分数,最后取出预测分数最大的类别,作为最后的结果。
- few-shot:根据LR中学到的权重,将其视作扩展词中每个词对于预测标签的贡献度,因此将其进行加权平均。
- Paper Knowledgeable Prompt-tuning: Incorporating Knowledge into Prompt Verbalizer for Text Classification
- Code https://github.com/thunlp/KnowledgeablePromptTuning
P-Tuning
原理简介
针对prompt模板的细微变化会对最终结果产生巨大变化等问题,作者提出了P-Tuning。其实现过程可简单描述为:
-
初始化template
对输入的句子设计一个离散的template,如The Disney film is good! It was [MASK].;
-
生成pseudo token
对输入的template挑选一个或多个token作为pseudo token:The Disney film is good! [pseudo] was [MASK].,其初始化向量可使用原本的token embedding,将所有的pseudo token的token embedding Pi输入一层LSTM,并获得输出的隐状态向量hi;
-
预测结果
将整个句子输入Bert,对pseudo token部分的token embedding使用hi进行替换,再通过MLM任务预测[MASK]位置的结果。
- Paper GPT Understands, Too
- Code https://github.com/THUDM/P-tuning
prompt相关实现均基于OpenPrompt框架,多种类型模板和标签映射方式均在该框架中有所集成,详见API文档
4. 其他trick
除了上述通用技术层面的方案,接下来将从一些需要适应任务的方面提出几点小trick。
中英翻译
专利文本数据具有一定的领域特性,通用的预训练模型不能很好地提取相关特征。英文有专门在专利领域训练的模型bert-for-patent。因此可以将中文文本进行翻译,再在bert-for-patent上进行分类任务。
翻译工具:有道智云api
文本分段
通过对专利文本摘要的分析,发现其所属类别与摘要中某一部分如功效,更具有相关性。因此将摘要文本作分段处理,将其划分为3个部分:概述、主要内容和功效。概述通常为第一句话,功效为最后一句话,其余部分作为主要内容。
-
加载需要的包
import json import pandas as pd import csv import numpy as np from sklearn.model_selection import train_test_split import re
-
读取数据
df = pd.read_csv('train_en.csv',names=["label","title","abs","ass"])
-
数据分割
# 定义分割后的文本列 df["introduction"]="." df["content"]="." df["effect"]="." # 文本分割 t=re.split('\,|\.|\;',df["abs"][0],1) for i in range(len(df["abs"])): # re.split(pattern, string, maxsplit=0, flags=0) # maxsplit=1 表示只分割1次,即只在string中第一次匹配到pattern时分割一次,返回结果是包含两个字符串的list t=re.split('\,|\.|\;',df["abs"][i],1) df.loc[i,"introduction"] =t[0] if t[1]!="": t[1]=t[1].rstrip(".") # 从字符串最右边删除参数指定字符 if len(t[1].rsplit(".",1))!=2: if len(t[1].rsplit(";",1))!=2: if len(t[1].rsplit(":",1))!=2: if len(t[1].rsplit(",",3))==4: st=t[1].rsplit(",",3) df.loc[i,"content"] =st[0].strip() df.loc[i,"effect"]=st[1]+","+st[2]+","+st[3] df.loc[i,"effect"]=df["effect"][i].strip() else: df.loc[i,"effect"]=t[1].strip() else: st=t[1].rsplit(":",1) df.loc[i,"content"]=st[0].strip() df.loc[i,"effect"]=st[1].strip() else: st=t[1].rsplit(";",1) df.loc[i,"content"]=st[0].strip() df.loc[i,"effect"]=st[1].strip() else: st=t[1].rsplit(".",1) df.loc[i,"content"]=st[0].strip() df.loc[i,"effect"]=st[1].strip()
传统技巧
- 模型选择:Bert、Roberta、ERNIE、bert-for-patent
- 交叉验证
- 调参
比赛群里提到的
简单交流一下吧。前期,我们使用两种数据增强(后向翻译和随机调换邻近词的位置)创造新的数据,再用类似这样(‘这篇专利的类别是[MASK]。’ + line[‘title’] + ‘。’ + line[‘assignee’] + ‘。’ + line[‘abstract’])文本组合方式, 选用Roberta large 4fold,分数早早就到了0.62x。又换了其它的预训练模型训练,简单融合分数就上了0.63. 到了比赛中段,我们主要靠伪标签来提升单模,通过不同的预训练模型融合来提升总分。分数也很快上了0.65 。
伪标签策略:
1)不同分类取不同概率阈值,对数量多的分类随机采样,最后组合的时候尽量和train的分类比例靠近;
2)不同分类取不同概率阈值,通过4fold的精准率和召回率来选择最佳阈值,对数量多的分类随机采样;
3)伪标签数量大概13k左右这比赛work的trick:
1)后向翻译和随机调换邻近词的位置;
2)R-DROP,MULTI-DROPOUT,对抗训练,分层学习率;
3)伪标签;
4)简单的MASK学习。类似:这篇专利的类别是[MASK]
5)百度的ERNIE 3.0预训练模型很好用。效果比各种large都好。不work的:
1)UDA以及某些半监督的论文
2)MLM预训练
3)换不同的种子。我们用42的种子和lb比较配合,换了别的种子,分数就跌了很多。
4)英文bert for patent。不管是base还是large,lb 都很差,都不如ERNIE 3.0
5)同义词和近义词数据增强
5. 比赛结果
Baseline
PLM | test_f1 |
---|---|
bert-base-chinese | 0.51~0.54 |
bert-for-patent | 0.57299310796 |
提交结果
PLM | test_f1 | 排名 | |
---|---|---|---|
A榜 | bert-for-patent | 0.60101792 | 89 / 474 |
B榜 | bert-for-patent | 0.58529877 | 26 / 118 |
采用方法
- 中译英
- P-tuning(Verbalizer按effect总结且舍弃困难样本)
- Balanced Cross-Entropy
- 数据分段