在科大讯飞疫情新闻中的地理位置识别挑战赛中,使用
w2ner
模型加载uer/roberta-base-finetuned-cluener2020-chinese
预训练权重,在赛题数据集上进行训练,之后对测试集进行推理,最终测试集上的jaccard_score
指标平均0.909
左右,最高0.9216
,排行榜TOP1
。代码开源地址:https://github.com/itlubber/iflytek-geographical-identification-top1
微信公众号推文:https://mp.weixin.qq.com/s/hxv7HwWF3RSm70n5P22fPA
图1: 疫情新闻中的地理位置识别挑战赛排行榜
赛题简介
赛题链接
https://challenge.xfyun.cn/topic/info?type=geographical-identification
赛题背景
疫情期间地理位置信息是快速识别并防范疫情的关键,而地理位置信息不仅包含城市、街道,也包含具体的公司名称和位置。在本次赛题中我们需要选手构建自然语言处理模型,识别出文本中的位置单词。
赛题任务
构建自然语言处理模型,对文本中的位置信息进行识别。案例如下:
输入:密接案例出现在恒盛地产北京合天和信房地产开发有限公司;
输出:恒盛地产北京合天和信房地产开发有限公司。
数据情况
赛题数据由训练集和测试集组成,分别使用 csv
进行读取。训练集包含 6000
条数据,测试集包含 2600+
条数据。
tree -L 2
.
└── 疫情新闻中的地理位置信息识别挑战赛公开数据
├── sample_submit.csv
├── test.csv
└── train.csv
1 directory, 3 files
评审规则
每支参赛队伍每天最多提交 3次
测试集预测结果,采用 jaccard_score
指标来评判提交结果的好坏,评估代码参考:
# sklearn.metrics.jaccard_score(y_true, y_pred, *, labels=None, pos_label=1, average='binary', sample_weight=None)
def jaccard_score(pred, label):
return len(set(pred) & set(label)) / len(set(pred) | set(label))
赛题分析
数据分析
文本分析
- 分析训练集和测试集中数据,发现文本长度最大值为
50
,赛题提供的新闻文本应该是最大长度为 50 截断后的文本,大部分都不是完整的句子 - 赛题提供的句子中的标点符号全角和半角都存在,可能先把半角符号都转为全角符号之后输入模型比较好
- 很多句子的最后一个字符是标点符号,但非中文句子的结尾字符,部分为单个括号,可能对句子语义有一定的破坏,可以考虑去掉句尾的标点符号
标注实体分析
- 查看训练集数据中的标注数据,分析可以发现,实体类型不止包含地理位置信息,还包含公司名称、组织机构名称、游戏名称、人名等类别,故本题采用腾讯
UER-py
预训练模型仓库中的uer/roberta-base-finetuned-cluener2020-chinese
作为预训练模型,在此基础上微调可以很好的匹配当前任务
序号 | 新闻内容 | 实体标注 | 实体类型 |
---|---|---|---|
1 | 会安博物馆等,漫步会安古镇各精致的工艺品店、品尝路边的小吃摊,体验当地的风土民情。 | [‘会安古镇’] | 景点 |
8 | 联达四方经纪公司总经理杨少峰 | [‘联达四方经纪公司’] | 公司名称 |
14 | 银行业协会今年理财产品销量将首破1万亿 | [‘银行业协会’] | 组织机构 |
371 | RTS星战之路首月将举行《魔兽争霸3》项目 | [‘RTS星战之路’] | 游戏 |
… | … | … | … |
模型分析
- 目前
NLP
相关的赛题,如果不是对模型训练和推理有资源限制,基本上都可以无脑用bert
类的模型,效果通常都还可以 - 预训练模型选择方面,可以考虑
Hugging Face
上面的预训练模型,也可以在github
或者paperwithcode
上找相关的预训练模型。考虑到本题是序列标注的问题,根据以往经验来看,预训练模型选roberta large
、nezha large
或者uer
里面的一些预训练模型,效果应该都还行,时间和资源都充裕的情况下也可以尝试下澜舟的mengzi
或者封神榜
里面的开源的预训练模型
解题方案
特别说明
本次比赛的主要代码来自 xiangking
大佬开源的 ark-nlp
库,库中收集和复现了部分常见的 NLP
模型,包括文本分类、文本匹配、序列标注、关系抽取等模型。
ark-nlp 仓库链接:https://github.com/xiangking/ark-nlp
解题思路
本题在 ark-nlp
库中的 w2ner_bert
代码的基础上,进行了部分小改动,使得模型能够直接加载 uer/roberta-base-finetuned-cluener2020-chinese
预训练模型权重进行训练,并在训练过程中,加入对抗训练FGM,对模型中的 word_embedding
层的参数注入扰动,结合EMA策略,提高了模型的鲁棒性。
同时,考虑到本次赛题数据集较少,且模型测试集上 jaccard_score
指标超过0.90,故使用了伪标签的方式扩充训练数据,并且在验证时依旧采用原有训练集中的数据来保证模型的准确性和泛化能力,有效的提高了模型的指标。
前期把 ark-nlp
库中的 ner
方案基本都试了一遍,也实验了 bert4torch
里面的一些模型,比如 UIE
, 也实验了下在文本首尾添加 promot
的方式,最终根据提交效果选择了最优的 word2ner
模型,同时对比了不同的预训练模型,包括 nezha
、roberta
、bert
以及其他在 huggingface
上的其他预训练模型,无论是 base
、 large
还是模型融合, 基本上都不如 uer/roberta-base-finetuned-cluener2020-chinese
预训练模型微调直接预测结果好。
代码结构
.
├── requirements.txt # 相关环境依赖
├── README.md # 说明文档
├── code
│ ├── clear_cache.sh # 清除代码运行过程中生成的 pyc、pyd文件
│ ├── additional.py # 比赛页面显示的测试集前16条答案, 在推理时可以选择是否使用
│ ├── train.sh # 模型训练脚本, cd ./code 后 chmod +x train.sh 再 ./train.sh
│ ├── test.sh # 模型训练脚本, cd ./code 后 chmod +x test.sh 再 ./test.sh, 修改脚本中的 additional_tags 为 True 可使用页面上的标签
│ ├── ark_nlp # ark_nlp 代码, 参考 https://github.com/xiangking/ark-nlp/tree/main/ark_nlp
│ │ ├── __init__.py
│ │ ├── dataset
│ │ ├── factory
│ │ ├── model # 模型代码
│ │ ├── nn
│ │ └── processor
│ ├── test.py # 推理代码
│ ├── test_pseudo.py # 使用伪标签训练模型后的推理代码
│ ├── train_all.py # 全量数据训练模型代码
│ └── train_all_pseudo.py # 全量数据 + 伪标签 训练模型代码
├── prediction_result
├── user_data
│ ├── outputs # 模型训练完保存的文件夹
│ ├── roberta-base-finetuned-cluener2020-chinese # UER.py 提供的预训练模型
│ │ ├── config.json
│ │ ├── pytorch_model.bin
│ │ ├── special_tokens_map.json
│ │ ├── tokenizer_config.json
│ │ └── vocab.txt
│ ├── examples # 比赛过程中的一些尝试,调整代码结构前的
│ │ ├── 0.w2ner_all.ipynb
│ │ ├── 0.w2ner_all_pseudo.ipynb
│ │ ├── 2.w2ner_pseudo.ipynb
│ │ ├── biaffine.ipynb
│ │ ├── gpbaseline.ipynb
│ │ ├── spanbert.ipynb
│ │ ├── w2ner.ipynb
│ │ └── w2ner_cv.ipynb
│ ├── scores.jpg # 比赛过程中提交结果的得分记录
│ └── 页面显示的测试集前16条答案.png
└── xfdata
└── 疫情新闻中的地理位置信息识别挑战赛公开数据 # 比赛提供的数据集
├── sample_submit.csv
├── test.csv # 测试集
└── train.csv # 训练集
数据预处理
原始文本中的半角标点符号转换为全角标点符号
def E_trans_to_C(string):
E_pun = u',.!?[]()<>"\''
C_pun = u',。!?【】()《》“‘'
table= {ord(f):ord(t) for f,t in zip(E_pun,C_pun)}
return string.translate(table)
去除文本末尾无意义的标点符号
train["text"] = train["text"].apply(lambda line: E_trans_to_C(re.sub("[\(《:→;,。、\-”]+$", "", line.strip())))
train["tag"] = train["tag"].apply(lambda x: [E_trans_to_C(i) for i in eval(str(x))])
将输出转换为标准的数据格式
train["entities"] = train.progress_apply(lambda row: [["LOC", *i.span()] for tag in row["tag"] for i in re.finditer(tag, row["text"])], axis=1)
datalist = []
for _, row in train.iterrows():
entity_labels = []
for _type, _start_idx, _end_idx in row["entities"]:
entity_labels.append({
'start_idx': _start_idx,
'end_idx': _end_idx,
'type': _type,
'entity': row["text"][_start_idx: _end_idx]
})
datalist.append({
'text': row["text"],
'entities': entity_labels
})
train_data_df = pd.DataFrame(datalist)
def get_label(x):
entities = []
for entity in x:
entity_ = {}
idx = list(range(entity['start_idx'], entity['end_idx']))
entity_['idx'] = idx
entity_['type'] = entity['type']
entity_['entity'] = entity['entity']
entities.append(entity_)
return entities
train_data_df['label'] = train_data_df['entities'].apply(lambda x: get_label(x))
train_data_df = train_data_df.loc[:, ['text', 'label']]
train_data_df['label'] = train_data_df['label'].apply(lambda x: str(x))
将处理好的数据转换为bert模型的输入
ner_train_dataset = Dataset(train_data_df)
prompt = None
tokenizer = Tokenizer(vocab='../user_data/roberta-base-finetuned-cluener2020-chinese', max_seq_len=52)
ner_train_dataset.convert_to_ids(tokenizer, prompt=prompt)
模型训练
模型加载
torch.cuda.empty_cache()
config = W2NERBertConfig.from_pretrained('../user_data/roberta-base-finetuned-cluener2020-chinese', num_labels=len(ner_train_dataset.cat2id))
dl_module = W2NERBert.from_pretrained('../user_data/roberta-base-finetuned-cluener2020-chinese', config=config)
选择优化器和学习率调整策略
# 设置运行次数
num_epoches, batch_size = 40, 256
optimizer = get_default_w2ner_optimizer(dl_module, lr=1e-2, bert_lr=5e-5, weight_decay=0.01)
# 注意lr衰减轮次的设定
show_step = len(ner_train_dataset) // batch_size + 2
t_total = len(ner_train_dataset) // batch_size * num_epoches
scheduler = get_default_cosine_schedule_with_warmup(optimizer, t_total, warmup_ratio=0.2)
模型训练
model = Task(dl_module, optimizer, 'ce', cude_device=2, scheduler=scheduler, grad_clip=5.0, ema_decay=0.995, fgm_attack=True, save_path="../user_data/outputs/roberta-finetuned", )
model.fit(ner_train_dataset, epochs=num_epoches, batch_size=batch_size, show_step=show_step)
模型推理
训练集数据推理
class IFW2NERPredictor(Predictor):
def E_trans_to_C(self, string):
E_pun = u',.!?[]()<>"\''
C_pun = u',。!?【】()《》“‘'
table= {ord(f):ord(t) for f,t in zip(E_pun,C_pun)}
return string.translate(table)
def predict_one_sample(self, text='', prompt=None, cv=False):
text = text.strip()
features = self._get_input_ids(E_trans_to_C(re.sub("[\(《:;→,。、\-”]+$", "", text)), prompt=prompt)
self.module.eval()
with torch.no_grad():
inputs = self._get_module_one_sample_inputs(features)
logit = self.module(**inputs)
preds = torch.argmax(logit, -1)
instance, l = preds.cpu().numpy()[0], int(inputs['input_lengths'].cpu().numpy()[0])
forward_dict = {}
head_dict = {}
ht_type_dict = {}
for i in range(l):
for j in range(i + 1, l):
if instance[i, j] == 1:
if i not in forward_dict:
forward_dict[i] = [j]
else:
forward_dict[i].append(j)
for i in range(l):
for j in range(i, l):
if instance[j, i] > 1:
ht_type_dict[(i, j)] = instance[j, i]
if i not in head_dict:
head_dict[i] = {j}
else:
head_dict[i].add(j)
predicts = []
def find_entity(key, entity, tails):
entity.append(key)
if key not in forward_dict:
if key in tails:
predicts.append(entity.copy())
entity.pop()
return
else:
if key in tails:
predicts.append(entity.copy())
for k in forward_dict[key]:
find_entity(k, entity, tails)
entity.pop()
for head in head_dict:
find_entity(head, [], head_dict[head])
entities = []
for entity_ in predicts:
entities.append({
"idx": entity_,
"entity": ''.join([text[i] for i in entity_]),
"type": self.id2cat[ht_type_dict[(entity_[0], entity_[-1])]]
})
if cv:
return text, int(inputs['input_lengths'].cpu().numpy()[0]), logit.cpu().numpy()
return entities
test = pd.read_csv("../xfdata/疫情新闻中的地理位置信息识别挑战赛公开数据/test.csv", sep="\t")
tokenizer = Tokenizer(vocab='../user_data/roberta-base-finetuned-cluener2020-chinese', max_seq_len=52)
ner_predictor_instance = IFW2NERPredictor(torch.load("../user_data/outputs/roberta-finetuned-all.pkl"), tokenizer, {'<none>': 0, '<suc>': 1, 'LOC': 2})
predict_results = []
for _line in tqdm(test["text"].tolist()):
label = set()
for _preditc in ner_predictor_instance.predict_one_sample(_line, prompt=prompt):
label.add(_preditc["entity"])
with open('../prediction_result/result.csv', 'w', encoding='utf-8') as f:
f.write("tag\n")
for _result in predict_results:
f.write(f"{str(_result)}\n")
模型trick
伪标签数据增广
# 伪标签数据
test = pd.read_csv("../xfdata/疫情新闻中的地理位置信息识别挑战赛公开数据/test.csv", sep="\t")
test["tag"] = pd.read_csv("../prediction_result/result.csv", sep="\t")["tag"]
test["text"] = test["text"].apply(lambda line: E_trans_to_C(re.sub("[\(《:→;,。、\-”]+$", "", line.strip())))
test["tag"] = test["tag"].apply(lambda x: [E_trans_to_C(i) for i in eval(str(x))])
test = test[test["tag"].apply(len) > 0
train = pd.read_csv("../xfdata/疫情新闻中的地理位置信息识别挑战赛公开数据/train.csv", sep="\t")
train = pd.concat([train[["text", "tag"]], test[["text", "tag"]]]).reset_index(drop=True)
FGM对抗学习
# code/ark_nlp/factory/task/base/_sequence_classification.py line: 108
# fgm attack
self._on_train_attack(inputs, **kwargs)
# code/ark_nlp/factory/task/base/_sequence_classification.py line: 269
def _on_train_attack(self, inputs, gradient_accumulation_steps=1, **kwargs):
if self.fgm_attack:
self.fgm.attack()
_, attck_loss = self._get_train_loss(inputs, self.module(**inputs), **kwargs)
# 如果GPU数量大于1
if self.n_gpu > 1:
attck_loss = attck_loss.mean()
# 如果使用了梯度累积,除以累积的轮数
if gradient_accumulation_steps > 1:
attck_loss = attck_loss / gradient_accumulation_steps
attck_loss.backward()
self.fgm.restore()
优化方向
- 文本数据增广,参考中文
EDA
或者AEDA
数据增广:https://github.com/425776024/nlpcda 或 https://github.com/akkarimi/aeda_nlp ,用了伪标签之后感觉没必要就没搞这个 - 继续预训练,在赛题数据集上进行
继续预训练
之后再fine tune
,可以mask
掉实体后让模型学实体的信息,由于分太高故而没搞这个,参考:https://github.com/zhoujx4/NLP-Series-NewWordsMining-PTMPretraining 或 https://github.com/zhusleep/pytorch_chinese_lm_pretrain - 用
prompt
或者把问题转换为MRC
任务来做,可以考虑用UIE
模型来做,理论上讲数据不多,实体类型不多的时候应该能 work,不过我中途尝试效果不太理想后放弃了,参考:https://github.com/Tongjilibo/bert4torch - 考虑用其他模型,比如
nezha
、lattice bert
、zen2
等模型,参考:https://github.com/alibaba/AliceMind/tree/main/LatticeBERT 、https://github.com/lonePatient/NeZha_Chinese_PyTorch 或 https://github.com/sinovation/ZEN2 - 考虑其他预训练模型,这题选对预训练模型是非常关键的
- 模型多样性和模型融合,最容易上
0.91
的方法是用global pointer bert
生成伪标签数据,再用w2ner
训练模型,在测试集上进行推理,提交结果上去应该就差不多了
评论区