本文将会介绍笔者在2019语言与智能技术竞赛的三元组抽取比赛方面的一次尝试。由于该比赛早已结束,笔者当时也没有参加这个比赛,因此没有测评成绩,我们也只能拿到训练集和验证集。但是,这并不耽误我们在这方面做实验。
比赛介绍
该比赛的网址为:http://lic2019.ccf.org.cn/kg ,该比赛主要是从给定的句子中提取三元组,给定schema约束集合及句子sent,其中schema定义了关系P以及其对应的主体S和客体O的类别,例如(S_TYPE:人物,P:妻子,O_TYPE:人物)、(S_TYPE:公司,P:创始人,O_TYPE:人物)等。比如下面的例子:
{
"text": "九玄珠是在纵横中文网连载的一部小说,作者是龙马",
"spo_list": [
["九玄珠", "连载网站", "纵横中文网"],
["九玄珠", "作者", "龙马"]
]
}
该比赛一共提供了20多万标注质量很高的三元组,其中17万训练集,2万验证集和2万测试集,实体关系(schema)50个。
在具体介绍笔者的思路和实战前,先介绍下本次任务的处理思路:
首先是对拿到的数据进行数据分析,包括统计每个句子的长度及三元组数量,每种关系的数量分布情况。接着,对数据单独走序列标注模型和关系分析模型。最后在提取三元组的时候,用Pipeline模型,先用序列标注模型预测句子中的实体,再对实体(加上句子)走关系分类模型,预测实体的关系,最后形成有效的三元组。
接下来笔者将逐一介绍,项目结构图如下:
数据分析
我们能拿到的只有训练集和验证集,没有测试集。我们对训练集做数据分析,训练集数据文件为train_data.json。
数据分析会统计训练集中每个句子的长度及三元组数量,还有关系的分布图,代码如下:
# -*- coding: utf-8 -*-
# author: Jclian91
# place: Pudong Shanghai
# time: 2020-03-12 21:52
import json
from pprint import pprint
import pandas as pd
from collections import defaultdict
import matplotlib.pyplot as plt
plt.figure(figsize=(18, 8), dpi=100) # 输出图片大小为1800*800
# Mac系统设置中文字体支持
plt.rcParams["font.family"] = 'Arial Unicode MS'
# 加载数据集
def load_data(filename):
D = []
with open(filename, 'r', encoding='utf-8') as f:
content = f.readlines()
content = [_.replace(' ', '').replace('\u3000', '').replace('\xa0', '').replace('\u2003', '') for _ in content]
for l in content:
l = json.loads(l)
D.append({
'text': l['text'],
'spo_list': [
(spo['subject'], spo['predicate'], spo['object'])
for spo in l['spo_list']
]
})
return D
filename = '../data/train_data.json'
D = load_data(filename=filename)
pprint(D)
# 创建text, text_length, spo_num的DataFrame
text_list = [_["text"] for _ in D]
spo_num = [len(_["spo_list"])for _ in D]
df = pd.DataFrame({"text": text_list, "spo_num": spo_num} )
df["text_length"] = df["text"].apply(lambda x: len(x))
print(df.head())
print(df.describe())
# 绘制spo_num的条形统计图
pprint(df['spo_num'].value_counts())
label_list = list(df['spo_num'].value_counts().index)
num_list = df['spo_num'].value_counts().tolist()
# 利用Matplotlib模块绘制条形图
x = range(len(num_list))
rects = plt.bar(x=x, height=num_list, width=0.6, color='blue', label="频数")
plt.ylim(0, 80000) # y轴范围
plt.ylabel("数量")
plt.xticks([index + 0.1 for index in x], label_list)
plt.xlabel("三元组数量")
plt.title("三元组频数统计图")
# 条形图的文字说明
for rect in rects:
height = rect.get_height()
plt.text(rect.get_x() + rect.get_width() / 2, height+1, str(height), ha="center", va="bottom")
# plt.show()
plt.savefig('./spo_num_bar_chart.png')
plt.close()
import matplotlib.pyplot as plt
plt.figure(figsize=(18, 8), dpi=100) # 输出图片大小为1800*800
# Mac系统设置中文字体支持
plt.rcParams["font.family"] = 'Arial Unicode MS'
# 关系统计图
relation_dict = defaultdict(int)
for spo_dict in D:
# print(spo_dict["spo_list"])
for spo in spo_dict["spo_list"]:
relation_dict[spo[1]] += 1
label_list = list(relation_dict.keys())
num_list = list(relation_dict.values())
# 利用Matplotlib模块绘制条形图
x = range(len(num_list))
rects = plt.bar(x=x, height=num_list, width=0.6, color='blue', label="频数")
plt.ylim(0, 80000) # y轴范围
plt.ylabel("数量")
plt.xticks([index + 0.1 for index in x], label_list)
plt.xticks(rotation=45) # x轴的标签旋转45度
plt.xlabel("三元组关系")
plt.title("三元组关系频数统计图")
# 条形图的文字说明
for rect in rects:
height = rect.get_height()
plt.text(rect.get_x() + rect.get_width() / 2, height+1, str(height), ha="center", va="bottom")
plt.savefig('./relation_bar_chart.png')
输出结果如下:
spo_num text_length
count 173108.000000 173108.000000
mean 2.103993 54.057190
std 1.569331 31.498245
min 0.000000 5.000000
25% 1.000000 32.000000
50% 2.000000 45.000000
75% 2.000000 68.000000
max 25.000000 300.000000
句子的平均长度为54,最大长度为300;每句话中的三元组数量的平均值为2.1,最大值为25。
每句话中的三元组数量的分布图如下:
关系数量的分布图如下:
序列标注模型
我们将句子中的主体和客体作为实体,分别标注为SUBJ和OBJ,标注体系采用BIO。一个简单的标注例子如下:
如 O
何 O
演 O
好 O
自 O
己 O
的 O
角 O
色 O
, O
请 O
读 O
《 O
演 O
员 O
自 O
我 O
修 O
养 O
》 O
《 O
喜 B-SUBJ
剧 I-SUBJ
之 I-SUBJ
王 I-SUBJ
》 O
周 B-OBJ
星 I-OBJ
驰 I-OBJ
崛 O
起 O
于 O
穷 O
困 O
潦 O
倒 O
之 O
中 O
的 O
独 O
门 O
秘 O
笈 O
序列标注的模型采用ALBERT+Bi-LSTM+CRF,结构图如下:
模型方面的代码不再具体给出,有兴趣的同学可以参考文章NLP(二十五)实现ALBERT+Bi-LSTM+CRF模型,也可以参考文章最后给出的Github项目网址。
模型设置文本最大长度为128,利用ALBERT做特征提取,在自己的电脑上用CPU训练5个epoch,结果如下:
_________________________________________________________________
Train on 173109 samples, validate on 21639 samples
Epoch 1/10
173109/173109 [==============================] - 422s 2ms/step - loss: 0.4460 - crf_viterbi_accuracy: 0.8710 - val_loss: 0.1613 - val_crf_viterbi_accuracy: 0.9235
Epoch 2/10
173109/173109 [==============================] - 417s 2ms/step - loss: 0.1170 - crf_viterbi_accuracy: 0.9496 - val_loss: 0.0885 - val_crf_viterbi_accuracy: 0.9592
Epoch 3/10
173109/173109 [==============================] - 417s 2ms/step - loss: 0.0758 - crf_viterbi_accuracy: 0.9602 - val_loss: 0.0653 - val_crf_viterbi_accuracy: 0.9638
Epoch 4/10
173109/173109 [==============================] - 415s 2ms/step - loss: 0.0586 - crf_viterbi_accuracy: 0.9645 - val_loss: 0.0544 - val_crf_viterbi_accuracy: 0.9651
Epoch 5/10
173109/173109 [==============================] - 422s 2ms/step - loss: 0.0488 - crf_viterbi_accuracy: 0.9663 - val_loss: 0.0464 - val_crf_viterbi_accuracy: 0.9654
Epoch 6/10
173109/173109 [==============================] - 423s 2ms/step - loss: 0.0399 - crf_viterbi_accuracy: 0.9677 - val_loss: 0.0375 - val_crf_viterbi_accuracy: 0.9660
Epoch 7/10
173109/173109 [==============================] - 415s 2ms/step - loss: 0.0293 - crf_viterbi_accuracy: 0.9687 - val_loss: 0.0265 - val_crf_viterbi_accuracy: 0.9664
Epoch 8/10
173109/173109 [==============================] - 414s 2ms/step - loss: 0.0174 - crf_viterbi_accuracy: 0.9695 - val_loss: 0.0149 - val_crf_viterbi_accuracy: 0.9671
Epoch 9/10
173109/173109 [==============================] - 422s 2ms/step - loss: 0.0049 - crf_viterbi_accuracy: 0.9703 - val_loss: 0.0036 - val_crf_viterbi_accuracy: 0.9670
Epoch 10/10
173109/173109 [==============================] - 429s 2ms/step - loss: -0.0072 - crf_viterbi_accuracy: 0.9709 - val_loss: -0.0078 - val_crf_viterbi_accuracy: 0.9674
precision recall f1-score support
OBJ 0.9593 0.9026 0.9301 44598
SUBJ 0.9670 0.9238 0.9449 25521
micro avg 0.9621 0.9104 0.9355 70119
macro avg 0.9621 0.9104 0.9355 70119
利用seqeval模块做评估,在验证集上的F1值约为93.55%。
关系分类模型
需要对关系做一下说明,因为笔者会对句子(sent)中的主体(S)和客体(O)组合起来,加上句子,形成训练数据。举个例子,在句子历史评价李氏朝鲜的创立并非太祖大王李成桂一人之功﹐其五子李芳远功不可没
,三元组为[{"predicate": "父亲", "object_type": "人物", "subject_type": "人物", "object": "李成桂", "subject": "李芳远"}, {"predicate": "国籍", "object_type": "国家", "subject_type": "人物", "object": "朝鲜", "subject": "李成桂"}]}
,在这句话中主体有李成桂,李芳远,客体有李成桂和朝鲜,关系有父亲(关系类型:2)和国籍(关系类型:22)。按照笔者的思路,这句话应组成4个关系分类样本,如下:
2 李芳远$李成桂$历史评价李氏朝鲜的创立并非太祖大王###一人之功﹐其五子###功不可没
0 李芳远$朝鲜$历史评价李氏##的创立并非太祖大王李成桂一人之功﹐其五子###功不可没
0 李成桂$李成桂$历史评价李氏朝鲜的创立并非太祖大王###一人之功﹐其五子李芳远功不可没
22 李成桂$朝鲜$历史评价李氏##的创立并非太祖大王###一人之功﹐其五子李芳远功不可没
因此,就会出现关系0(表示“未知”),这样我们在提取三元组的时候就可以略过这条关系,形成真正有用的三元组。
因此,关系一共为51个(加上未知关系:0)。关系分类模型采用ALBERT+Bi-GRU+ATT,结构图如下:
模型方面的代码不再具体给出,有兴趣的同学可以参考文章NLP(二十一)人物关系抽取的一次实战,也可以参考文章最后给出的Github项目网址。
模型设置文本最大长度为128,利用ALBERT做特征提取,在自己的电脑上用CPU训练30个epoch(实际上,由于有early stopping机制,训练不到30个eopch),在验证集上的评估结果如下:
Epoch 23/30
396766/396766 [==============================] - 776s 2ms/step - loss: 0.1770 - accuracy: 0.9402 - val_loss: 0.2170 - val_accuracy: 0.9308
Epoch 00023: val_accuracy did not improve from 0.93292
49506/49506 [==============================] - 151s 3ms/step
在测试集上的效果: [0.21701653493155634, 0.930776059627533]
precision recall f1-score support
未知 0.87 0.76 0.81 5057
祖籍 0.92 0.73 0.82 181
父亲 0.79 0.88 0.83 609
总部地点 0.95 0.95 0.95 310
出生地 0.94 0.95 0.94 2330
目 1.00 1.00 1.00 1271
面积 0.90 0.92 0.91 79
简称 0.97 0.99 0.98 138
上映时间 0.94 0.98 0.96 463
妻子 0.91 0.83 0.87 680
所属专辑 0.97 0.97 0.97 1282
注册资本 1.00 1.00 1.00 63
首都 0.92 0.96 0.94 47
导演 0.92 0.94 0.93 2603
字 0.96 0.97 0.97 339
身高 0.98 0.98 0.98 393
出品公司 0.96 0.96 0.96 851
修业年限 1.00 1.00 1.00 2
出生日期 0.99 0.99 0.99 2892
制片人 0.69 0.88 0.77 127
母亲 0.75 0.88 0.81 425
编剧 0.82 0.80 0.81 771
国籍 0.92 0.92 0.92 1621
海拔 1.00 1.00 1.00 43
连载网站 0.98 1.00 0.99 1658
丈夫 0.84 0.91 0.87 678
朝代 0.85 0.92 0.88 419
民族 0.98 0.99 0.99 1434
号 0.95 0.99 0.97 197
出版社 0.98 0.99 0.99 2272
主持人 0.82 0.86 0.84 200
专业代码 1.00 1.00 1.00 3
歌手 0.89 0.94 0.91 2857
作词 0.85 0.81 0.83 884
主角 0.86 0.77 0.81 39
董事长 0.81 0.74 0.78 47
毕业院校 0.99 0.99 0.99 1433
占地面积 0.89 0.89 0.89 61
官方语言 1.00 1.00 1.00 15
邮政编码 1.00 1.00 1.00 4
人口数量 1.00 1.00 1.00 45
所在城市 0.90 0.94 0.92 77
作者 0.97 0.97 0.97 4359
成立日期 0.99 0.99 0.99 1608
作曲 0.78 0.77 0.78 849
气候 1.00 1.00 1.00 103
嘉宾 0.76 0.72 0.74 158
主演 0.94 0.97 0.95 7383
改编自 0.95 0.82 0.88 71
创始人 0.86 0.87 0.86 75
accuracy 0.93 49506
macro avg 0.92 0.92 0.92 49506
weighted avg 0.93 0.93 0.93 49506
三元组提取
最后一部分,也是本次比赛的最终目标,就是三元组提取。
三元组提取采用Pipeline模式,先用序列标注模型预测句子中的实体,然后再用关系分类模型判断实体关系的类别,过滤掉关系为未知的情形,就是我们想要提取的三元组了。
三元组提取的代码如下:
# -*- coding: utf-8 -*-
# author: Jclian91
# place: Pudong Shanghai
# time: 2020-03-14 20:41
import os, re, json, traceback
import json
import numpy as np
from keras_contrib.layers import CRF
from keras_contrib.losses import crf_loss
from keras_contrib.metrics import crf_accuracy, crf_viterbi_accuracy
from keras.models import load_model
from collections import defaultdict
from pprint import pprint
from text_classification.att import Attention
from albert_zh.extract_feature import BertVector
# 读取label2id字典
with open("../sequence_labeling/ccks2019_label2id.json", "r", encoding="utf-8") as h:
label_id_dict = json.loads(h.read())
id_label_dict = {v: k for k, v in label_id_dict.items()}
# 利用ALBERT提取文本特征
bert_model = BertVector(pooling_strategy="NONE", max_seq_len=128)
f = lambda text: bert_model.encode([text])["encodes"][0]
# 载入NER模型
custom_objects = {'CRF': CRF, 'crf_loss': crf_loss, 'crf_viterbi_accuracy': crf_viterbi_accuracy}
ner_model = load_model("../sequence_labeling/ccks2019_ner.h5", custom_objects=custom_objects)
# 载入分类模型
best_model_path = '../text_classification/models/per-rel-08-0.9234.h5'
classification_model = load_model(best_model_path, custom_objects={"Attention": Attention})
# 分类与id的对应关系
with open("../data/relation2id.json", "r", encoding="utf-8") as g:
relation_id_dict = json.loads(g.read())
id_relation_dict = {v: k for k, v in relation_id_dict.items()}
# 从预测的标签列表中获取实体
def get_entity(sent, tags_list):
entity_dict = defaultdict(list)
i = 0
for char, tag in zip(sent, tags_list):
if 'B-' in tag:
entity = char
j = i+1
entity_type = tag.split('-')[-1]
while j < min(len(sent), len(tags_list)) and 'I-%s' % entity_type in tags_list[j]:
entity += sent[j]
j += 1
entity_dict[entity_type].append(entity)
i += 1
return dict(entity_dict)
# 三元组提取类
class TripleExtract(object):
def __init__(self, text):
self.text = text.replace(" ", "") # 输入句子
# 获取输入句子中的实体(即:主体和客体)
def get_entity(self):
train_x = np.array([f(self. text)])
y = np.argmax(ner_model.predict(train_x), axis=2)
y = [id_label_dict[_] for _ in y[0] if _]
# 输出预测结果
return get_entity(self.text, y)
# 对实体做关系判定
def relation_classify(self):
entities = self.get_entity()
subjects = list(set(entities.get("SUBJ", [])))
objs = list(set(entities.get("OBJ", [])))
spo_list = []
for subj in subjects:
for obj in objs:
sample = '$'.join([subj, obj, self.text.replace(subj, '#'*len(subj)).replace(obj, "#"*len(obj))])
vec = bert_model.encode([sample])["encodes"][0]
x_train = np.array([vec])
# 模型预测并输出预测结果
predicted = classification_model.predict(x_train)
y = np.argmax(predicted[0])
relation = id_relation_dict[y]
if relation != "未知":
spo_list.append([subj, relation, obj])
return spo_list
# 提取三元组
def extractor(self):
return self.relation_classify()
运行三元组提取脚本,代码如下:
# -*- coding: utf-8 -*-
# author: Jclian91
# place: Pudong Shanghai
# time: 2020-03-14 20:53
import os, re, json, traceback
from pprint import pprint
from triple_extract.triple_extractor import TripleExtract
text = "真人版的《花木兰》由新西兰导演妮基·卡罗执导,由刘亦菲、甄子丹、郑佩佩、巩俐、李连杰等加盟,几乎是全亚洲阵容。"
triple_extract = TripleExtract(text)
print("原文: %s" % text)
entities = triple_extract.get_entity()
print("实体: ", end='')
pprint(entities)
spo_list = triple_extract.extractor()
print("三元组: ", end='')
pprint(spo_list)
我们在网上找几条样本进行测试,测试的结果如下:
总结
本文标题为限定领域的三元组抽取的一次尝试,之所以取名为限定领域,是因为该任务的实体关系是确定,一共为50种关系。
当然,上述方法还存在着诸多不足,参考苏建林的文章基于DGCNN和概率图的轻量级信息抽取模型,我们发现不足之处如下:
- 主体和客体的标注策略有问题,因为句子中有时候主体和客体会重叠在一起;
- 新引入了一类关系:未知,是否有办法避免引入;
- 其他(暂时未想到)
从比赛的角度将,本文的办法效果未知,应该会比联合模型的效果差一些。但是,这是作为笔者自己的模型,算法是一种尝试,之所以采用这种方法,是因为笔者一开始是从开放领域的三元组抽取入手的,而这种方法方便扩展至开放领域。关于开放领域的三元组抽取,笔者稍后就会写文章介绍,敬请期待。
本文的源代码已经公开至Github,网址为:
https://github.com/percent4/ccks_triple_extract 。
参考网址
- NLP(二十五)实现ALBERT+Bi-LSTM+CRF模型:https://blog.csdn.net/jclian91/article/details/104826655
- NLP(二十一)人物关系抽取的一次实战: https://blog.csdn.net/jclian91/article/details/104380371
- 基于DGCNN和概率图的轻量级信息抽取模型:https://spaces.ac.cn/archives/6671