CLUE命名实体识别

使用GitHub项目:https://github.com/dbiir/UER-py

项目完整文档:https://github.com/dbiir/UER-py/wiki/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B

预训练模型仓库:https://github.com/dbiir/UER-py/wiki/%E9%A2%84%E8%AE%AD%E7%BB%83%E6%A8%A1%E5%9E%8B%E4%BB%93%E5%BA%93

上篇文章是使用huggingface transformer中预训练好的命名实体识别模型来进行实体抽取,但是在实际任务中,一般需要根据自己的数据集再进行微调,所以就继续研究如何进行微调,最终定位到UER-py这个工具包,所以这里先研究下这个工具怎么用。

CLUE命名实体识别

在文档中,我们看到其中针对竞赛列了几个典型的例子,其中之一是CLUE命名实体识别

竞赛网站地址为:https://www.cluebenchmarks.com/introduce.html

我们通过网站上给出的链接可以下载数据集,数据集也是简单易懂,数据量也不多,其格式对于我们自己准备数据集有参考意义,而且我们也可以先使用此数据集,先将代码跑通。

训练集:10748 验证集:1343

数据分为10个标签类别,分别为:

地址(address),书名(book),公司(company),游戏(game),政府(government),

电影(movie),姓名(name),组织机构(organization),职位(position),景点(scene)

cluener下载链接:数据下载

例子:

1
2
{"text": "浙商银行企业信贷部叶老桂博士则从另一个角度对五道门槛进行了解读。叶老桂认为,对目前国内商业银行而言,", "label": {"name": {"叶老桂": [[9, 11]]}, "company": {"浙商银行": [[0, 3]]}}}
{"text": "生生不息CSOL生化狂潮让你填弹狂扫", "label": {"game": {"CSOL": [[4, 7]]}}}

上篇文章,我们下载使用了uer/roberta-base-finetuned-cluener2020-chinese模型,我使用该模型针对此数据集进行了实体抽取测试,发现其实已经实现了针对该数据集的实体抽取。

如果说,我们想体验一下,“将一个模型从针对该数据集不能抽取,训练到可以抽取”这一过程的话,该怎么做呢?

我们可以根据UER-py的文档,下载一个比较原始的模型,google_zh_model.bin,然后针对该模型在此数据集上进行微调。

我们需要先确认该模型确实不能实现该抽取任务,所以可以先直接进行预测,使用inference/run_ner_infer.py,不过在执行该脚本之前需要先按照相应的格式准备好数据集。

明确UER-py这个框架所需的数据集格式

经过对run_ner_infer这个脚本的观察,其需要的数据集是一种tsv文件,文件内容如下

train.tsv

1
2
3
4
5
6
7
text_a	label
海 钓 比 赛 地 点 在 厦 门 与 金 门 之 间 的 海 域 。 O O O O O O O B-LOC I-LOC O B-LOC I-LOC O O O O O O
这 座 依 山 傍 水 的 博 物 馆 由 国 内 一 流 的 设 计 师 主 持 设 计 , 整 个 建 筑 群 精 美 而 恢 宏 。 O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O
但 作 为 一 个 共 产 党 员 、 人 民 公 仆 , 应 当 胸 怀 宽 阔 , 真 正 做 到 “ 先 天 下 之 忧 而 忧 , 后 天 下 之 乐 而 乐 ” , 淡 化 个 人 的 名 利 得 失 和 宠 辱 悲 喜 , 把 改 革 大 业 摆 在 首 位 , 这 样 才 能 超 越 自 我 , 摆 脱 世 俗 , 有 所 作 为 。 O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O
在 发 达 国 家 , 急 救 保 险 十 分 普 及 , 已 成 为 社 会 保 障 体 系 的 重 要 组 成 部 分 。 O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O O
日 俄 两 国 国 内 政 局 都 充 满 变 数 , 尽 管 日 俄 关 系 目 前 是 历 史 最 佳 时 期 , 但 其 脆 弱 性 不 言 自 明 。 B-LOC B-LOC O O O O O O O O O O O O O O B-LOC B-LOC O O O O O O O O O O O O O O O O O O O O O O
克 马 尔 的 女 儿 让 娜 今 年 读 五 年 级 , 她 所 在 的 班 上 有 3 0 多 名 同 学 , 该 班 的 “ 家 委 会 ” 由 1 0 名 家 长 组 成 。 B-PER I-PER I-PER O O O B-PER I-PER O O O O O O O O O O O O O O O O O O O O O O O O O B-ORG I-ORG I-ORG O O O O O O O O O O

每行一个句子和标签(之间用\t分隔),每个字之间用空格分开,后面的标签也是空格分隔,O(英文字母)表示不是实体,采用BIO标注规则,比如厦门是一个地点,“夏”的位置是B-LOC,代表location(一种实体,地点)的Beginning(开始位置),“门”的位置是I-LOC,代表location的inside(内部,可以理解为这个位置是实体的一个组成部分)。

另外还有test.tsv,dev.tsv,prediction.tsv其文件格式都是一致的。

dev.tsv文件按照我的理解是在训练的时候作为验证集使用;test.tsv文件则是测试集;
prediction.tsv文件是预测的时候,保存预测结果的文件,是往里面写的。

还有两个数据集文件比较特殊,一个是test_nolabel.tsv,顾名思义,他与上面文件的区别是,不含有标签;另一个是label2id.json,这个文件中存储的是所有的标签分类,并且对应到一个id(就是从0开始按顺序递增)。
例如:

1
{"O": 0, "B-LOC": 1, "I-LOC": 2, "B-PER": 3, "I-PER": 4, "B-ORG": 5, "I-ORG": 6}

也就是这里只有三类实体,分别是LOC、PER、ORG,然后再分别对应到B 和 I,再加上一个O(非实体),如果增加实体种类,就要在这里增加相应的内容。

数据集格式转化

我们解压下载的cluener数据集,得到的是三个主要的文件,train.json、test.json和dev.json,具体格式上面已经介绍过了,不过区别的地方就是test.json中没有给标签,这三个文件对应的可以转化为:train.tsv、dev.tsv和test_nolabel.tsv文件。

下面开发一个可以直接将数据集进行转换的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""
将cluener的数据集转为tsv格式

{"text": "浙商银行企业信贷部叶老桂博士则从另一个角度对五道门槛进行了解读。叶老桂认为,对目前国内商业银行而言,",
"label": {"name": {"叶老桂": [[9, 11]]}, "company": {"浙商银行": [[0, 3]]}}}


"""
import json


# 转换带有标签的数据
def convert_train_data(input_file, output_file):
fr = open(fr'../../datasets/cluener_public/{input_file}', 'r', encoding='utf-8')
fw = open(fr'../../datasets/cluener_public/{output_file}', 'w', encoding='utf-8')
fw.write("text_a\tlabel\n")
data_list = fr.readlines()
for each in data_list:
each = json.loads(each)
text = each.get("text")
for i, word in enumerate(text):
if i == 0:
fw.write(word)
else:
fw.write(" " + word)
fw.write("\t")
label = each.get("label")
label_list = ['O'] * len(text)
for entity_name, info in label.items():
for entity_text, index in info.items():
if len(entity_text) == 1:
label_list[index[0][0]] = f"B-{entity_name}"
else:
for i, idx in enumerate(range(index[0][0], index[0][1]+1)):
if i == 0:
label_list[idx] = f"B-{entity_name}"
else:
label_list[idx] = f"I-{entity_name}"
for i, t in enumerate(label_list):
if i == 0:
fw.write(t)
else:
fw.write(f" {t}")
fw.write("\n")

fr.close()
fw.close()


# 转换不带标签的数据
def convert_no_label_data(input_file, output_file):
fr = open(fr'../../datasets/cluener_public/{input_file}', 'r', encoding='utf-8')
fw = open(fr'../../datasets/cluener_public/{output_file}', 'w', encoding='utf-8')
fw.write("text_a\n")
data_list = fr.readlines()
for each in data_list:
each = json.loads(each)
text = each.get("text")
for i, word in enumerate(text):
if i == 0:
fw.write(word)
else:
fw.write(" " + word)
fw.write("\n")

fr.close()
fw.close()


convert_train_data("train.json", "train.tsv")
# convert_train_data("dev.json", "dev.tsv")
# convert_no_label_data("test.json", "test_nolabel.tsv")

别忘了还有一个label2id.json需要根据自己的标签准备

1
{"O": 0, "B-address": 1, "I-address": 2, "B-book": 3, "I-book": 4, "B-company": 5, "I-company": 6, "B-game": 7, "I-game": 8, "B-government": 9, "I-government": 10, "B-movie": 11, "I-movie": 12, "B-name": 13, "I-name": 14, "B-organization": 15, "I-organization": 16, "B-position": 17, "I-position": 18, "B-scene": 19, "I-scene": 20}

针对CLUE数据集进行训练(finetune)

训练之前先使用原始的模型来预测看一下结果,按照上面的步骤把数据集都准备好之后,就可以按照下面的命令来运行了

1
2
3
4
5
6
python3 inference/run_ner_infer.py --load_model_path models/google_zh_model.bin \
--vocab_path models/google_zh_vocab.txt \
--config_path models/bert/base_config.json \
--test_path datasets/cluener2020/test_nolabel.tsv \
--prediction_path datasets/cluener2020/prediction.tsv \
--label2id_path datasets/cluener2020/label2id.json

这里我们先用下载的原始模型google_zh_model来进行预测,等后面我们微调完成生成新的模型文件之后,再用训练得到的模型文件来进行预测。

我这里随机选取了前五个句子的预测结果,一团糟

1
2
3
4
5
pred_label
B-company B-company B-company B-company B-company B-company B-company B-organization I-scene I-scene B-company B-company B-company B-company B-company B-company B-company B-company B-company B-book B-company B-company B-company B-company B-company I-company B-company B-company B-company B-company B-company B-company B-company B-company B-company B-company I-company B-company B-company O B-company B-company B-company B-company B-company B-company B-company B-company
B-organization I-organization B-position B-organization B-company B-position B-position B-position B-position B-position B-position B-organization B-organization B-organization B-organization B-position B-position [PAD] B-position I-name B-position B-position B-position B-position B-position B-position B-position B-position B-position B-position B-organization
B-position I-company B-position B-position B-position B-position I-company B-position B-position B-position B-position B-position B-position B-position B-position B-position B-position
B-organization B-organization B-organization B-organization B-organization B-organization B-book B-book B-book B-book B-book B-organization B-position B-book B-book B-book B-book B-organization I-position B-book I-position

这样就表示该模型目前不具备这个数据集的预测能力,我们开始微调训练。

1
2
3
4
5
6
7
8
python3 finetune/run_ner.py --pretrained_model_path models/google_zh_model.bin \
--vocab_path models/google_zh_vocab.txt \
--config_path models/bert/base_config.json \
--train_path datasets/cluener2020/train.tsv \
--dev_path datasets/cluener2020/dev.tsv \
--label2id_path datasets/cluener2020/label2id.json \
--output_model_path models/ner_model.bin \
--epochs_num 5 --batch_size 16

实际上关于这里的词表,google_zh_vocab是BERT论文使用的词表,如果有必要的话,我们可以基于这个词表,把我们自己数据集中的词(BERT里实际上就是单个的字)扩充进去,甚至微调词向量,不过这需要针对BERT模型finetune(我们常说的BERT finetune实际上是在预训练词向量,并非具体任务),我前面写过一篇BERT finetune的文章,大致流程都是一致的,但是UER中有自己的BERT finetune方法,后面我也会介绍。

训练结束之后,模型最终的一个评估值如下

1
2
[2022-07-03 22:29:18,640 INFO] Report precision, recall, and f1:
[2022-07-03 22:29:18,640 INFO] 0.758, 0.773, 0.765

在CLUE命名实体识别竞赛官网,我们可以找到UER的得分为77分左右,不是很确定这个分值是怎么计算的,但是与这里的F1值*100,总体相差不大,UER在当时取得了一个最高的分数(2020-04-15),现在这个榜单上最高分已经达到了84分。

使用训练后的模型再来针对测试集数据做个预测,看一下实际效果,选取五个句子

原始文本

1
2
3
4
5
四 川 敦 煌 学 ” 。 近 年 来 , 丹 棱 县 等 地 一 些 不 知 名 的 石 窟 迎 来 了 海 内 外 的 游 客 , 他 们 随 身 携 带 着 胡 文 和 的 著 作 。
尼 日 利 亚 海 军 发 言 人 当 天 在 阿 布 贾 向 尼 日 利 亚 通 讯 社 证 实 了 这 一 消 息 。
销 售 冠 军 : 辐 射 3 - B e t h e s d a
所 以 大 多 数 人 都 是 从 巴 厘 岛 南 部 开 始 环 岛 之 旅 。
备 受 瞩 目 的 动 作 及 冒 险 类 大 作 《 迷 失 》 在 其 英 文 版 上 市 之 初 就 受 到 了 全 球 玩 家 的 大 力 追 捧 。

预测结果

1
2
3
4
5
B-organization I-organization I-organization I-organization I-organization I-organization O O O O O B-address I-address I-address O O O O O O O O O O O O O O O O O O O O O O O O O O O B-name I-name I-name O O O O 
B-government I-government I-government I-government I-government I-government O O O O O O B-address I-address I-address O B-organization I-organization I-organization I-organization I-organization I-organization I-organization O O O O O O O O
O O O O O B-game I-game I-game O O I-game I-game O O O O O
O O O O O O O O O B-scene I-scene I-scene O O O O O O O O O
O O O O O O O O O O O O O B-game I-game I-game I-game O O O O O O O O O O O O O O O O O O O O O O O

我们再进一步整理下预测得到的数据,方便理解

organization:四 川 敦 煌 学 ”
address:丹 棱 县
name:胡 文 和
government:尼 日 利 亚 海 军
address:阿 布 贾
organization:尼 日 利 亚 通 讯 社
game:辐 射 3
game:e t
scene:巴 厘 岛
game:《 迷 失 》

上面这个抽取结果,除了四 川 敦 煌 学 ”多了个引号,game:e t抽取不完整,其他的都没有问题,证明我们的训练是有效地,只不过还有一定的改进空间,因为我们本篇文章的目的在于了解使用UER进行NER任务微调的方法和过程,就不再深入研究,后面我会针对我自己的数据集,做相应的实体抽取任务。