去年,笔者写过一篇文章利用关系抽取构建知识图谱的一次尝试 ,试图用现在的深度学习办法去做开放领域的关系抽取,但是遗憾的是,目前在开放领域的关系抽取,还没有成熟的解决方案和模型。当时的文章仅作为笔者的一次尝试,在实际使用过程中,效果有限。
本文将讲述如何利用深度学习模型来进行人物关系抽取。人物关系抽取可以理解为是关系抽取,这是我们构建知识图谱的重要一步。本文人物关系抽取的主要思想是关系抽取的pipeline(管道)模式,因为人名可以使用现成的NER模型提取,因此本文仅解决从文章中抽取出人名后,如何进行人物关系抽取。
本文采用的深度学习模型是文本分类模型,结合BERT预训练模型,取得了较为不错的效果。
本项目已经开源,Github地址为:https://github.com/percent4/people_relation_extract
。
本项目的项目结构图如下:
人物关系抽取项目结构
数据集介绍
在进行这方面的尝试之前,我们还不得不面对这样一个难题,那就是中文人物关系抽取语料的缺失。数据是模型的前提,没有数据,一切模型无从谈起。因此,笔者不得不花费大量的时间收集数据。
笔者利用大量自己业余的时间,收集了大约2900条人物关系样本,整理成Excel(文件名称为人物关系表.xlsx
),其中几行如下:
笔者自己收集的数据集
人物关系一共有14类,分别为unknown
,夫妻
,父母
,兄弟姐妹
,上下级
,师生
,好友
,同学
,合作
,同人
,情侣
,祖孙
,同门
,亲戚
,其中unknown
类别表示该人物关系不在其余的13类中(人物之间没有关系或者为其他关系),同人
关系指的是两个人物其实是同一个人,比如下面的例子:
1 邵逸夫(1907年10月4 日—2014年1月7 日),原名邵仁楞,生于浙江省宁波市镇海镇,祖籍浙江宁波。
上面的例子中,邵逸夫和邵仁楞就是同一个人。亲戚
关系指的是除了夫妻
,父母
,兄弟姐妹
,祖孙
之外的亲戚关系,比如叔侄,舅甥关系等。
为了对该数据集的每个关系类别的数量进行统计,我们可以使用脚本data/relation_bar_chart.py
,完整的Python代码如下:
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 import pandas as pdimport matplotlib.pyplot as plt df = pd.read_excel('人物关系表.xlsx' ) label_list = list (df['关系' ].value_counts().index) num_list= df['关系' ].value_counts().tolist() plt.rcParams["font.family" ] = 'Arial Unicode MS' x = range (len (num_list)) rects = plt.bar(left=x, height=num_list, width=0.6 , color='blue' , label="频数" ) plt.ylim(0 , 500 ) plt.ylabel("数量" ) plt.xticks([index + 0.1 for index in x], label_list) plt.xticks(rotation=45 ) plt.xlabel("人物关系" ) plt.title("人物关系频数统计" ) plt.legend()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()
运行后的结果如下:
人物关系条形图
unknown
类别最多,有791条,其余的如祖孙
,
亲戚
,
情侣
等较少,只有90多条,这是因为这类人物关系的数据缺失不好收集。因此,语料的收集费时费力,需要消耗大量的精力。
数据预处理
收集好数据后,我们需要对数据进行预处理,预处理主要分两步,一步是将人物关系和原文本整合在一起,第二步简单,将数据集划分为训练集和测试集,比例为8:2。
我们对第一步进行详细说明,将人物关系和原文本整合在一起。一般我们给定原文本和该文本中的两个人物,比如:
1 邵逸夫(1907年10月4 日—2014年1月7 日),原名邵仁楞,生于浙江省宁波市镇海镇,祖籍浙江宁波。
这句话中有两个人物:邵逸夫,邵仁楞,
这个容易在语料中找到。然后我们将原文本的这两个人物中的每个字符分别用'#'号代码,并通过'$'符号拼接在一起,形成的整合文本如下:
1 邵逸夫$邵仁楞$###(1907 年10 月4 日—2014 年1 月7 日),原名###,生于浙江省宁波市镇海镇,祖籍浙江宁波。
处理成这种格式是为了方便文本分类模型进行调用。
数据预处理的脚本为data/data_into_train_test.py
,完整的Python代码如下:
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 import jsonimport pandas as pdfrom pprint import pprint df = pd.read_excel('人物关系表.xlsx' ) relations = list (df['关系' ].unique()) relations.remove('unknown' ) relation_dict = {'unknown' : 0 } relation_dict.update(dict (zip (relations, range (1 , len (relations)+1 ))))with open ('rel_dict.json' , 'w' , encoding='utf-8' ) as h: h.write(json.dumps(relation_dict, ensure_ascii=False , indent=2 )) pprint(df['关系' ].value_counts()) df['rel' ] = df['关系' ].apply(lambda x: relation_dict[x]) texts = []for per1, per2, text in zip (df['人物1' ].tolist(), df['人物2' ].tolist(), df['文本' ].tolist()): text = '$' .join([per1, per2, text.replace(per1, len (per1)*'#' ).replace(per2, len (per2)*'#' )]) texts.append(text) df['text' ] = texts train_df = df.sample(frac=0.8 , random_state=1024 ) test_df = df.drop(train_df.index)with open ('train.txt' , 'w' , encoding='utf-8' ) as f: for text, rel in zip (train_df['text' ].tolist(), train_df['rel' ].tolist()): f.write(str (rel)+' ' +text+'\n' )with open ('test.txt' , 'w' , encoding='utf-8' ) as g: for text, rel in zip (test_df['text' ].tolist(), test_df['rel' ].tolist()): g.write(str (rel)+' ' +text+'\n' )
运行完该脚本后,会在data
目录下生成train.txt,
test.txt和rel_dict.json,该json文件中保存的信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "unknown" : 0 , "夫妻" : 1 , "父母" : 2 , "兄弟姐妹" : 3 , "上下级" : 4 , "师生" : 5 , "好友" : 6 , "同学" : 7 , "合作" : 8 , "同人" : 9 , "情侣" : 10 , "祖孙" : 11 , "同门" : 12 , "亲戚" : 13 }
简单来说,是给每种关系一个id,转化成类别型变量。
以train.txt为例,其前5行的内容如下:
1 2 3 4 5 6 4 方琳$李伟康$在生活中,###则把##看作小辈,常常替她解决难题。3 佳子$久仁$12 月,##和弟弟##参加了在东京举行的全国初中生演讲比赛。2 钱慧安$钱禄新$###,生卒年不详,海上画家###之子。0 吴继坤$邓新生$###还曾对媒体说:“我这个小小的投资商,经常得到###等领导的亲自关注和关照,我觉到受宠若惊。”2 洪博培$乔恩·M·亨茨曼$###的父亲########是著名企业家、美国最大化学公司亨茨曼公司创始人。10 夏乐$陈飞$两小无猜剧情简介:##和##是一对从小一起长大的青梅竹马。
在每一行中,空格之前的数字所对应的人物关系可以在rel_dict.json
中找到。
模型训练
在模型训练前,为了将数据的格式更好地适应模型,需要再对trian.txt和test.txt进行处理。处理脚本为load_data.py
,完整的Python代码如下:
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 import pandas as pddef read_txt_file (file_path ): with open (file_path, 'r' , encoding='utf-8' ) as f: content = [_.strip() for _ in f.readlines()] labels, texts = [], [] for line in content: parts = line.split() label, text = parts[0 ], '' .join(parts[1 :]) labels.append(label) texts.append(text) return labels, textsdef get_train_test_pd (): file_path = 'data/train.txt' labels, texts = read_txt_file(file_path) train_df = pd.DataFrame({'label' : labels, 'text' : texts}) file_path = 'data/test.txt' labels, texts = read_txt_file(file_path) test_df = pd.DataFrame({'label' : labels, 'text' : texts}) return train_df, test_dfif __name__ == '__main__' : train_df, test_df = get_train_test_pd() print (train_df.head()) print (test_df.head()) train_df['text_len' ] = train_df['text' ].apply(lambda x: len (x)) print (train_df.describe())
本项目所采用的模型为:BERT + 双向GRU + Attention +
FC,其中BERT用来提取文本的特征,关于这一部分的介绍,已经在文章NLP(二十)利用BERT实现文本二分类 中给出;Attention为注意力机制层,FC为全连接层,模型的结构图如下(利用Keras导出):
模型结构示例图
模型训练的脚本为model_train.py
,完整的Python代码如下:
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 import numpy as npfrom load_data import get_train_test_pdfrom keras.utils import to_categoricalfrom keras.models import Modelfrom keras.optimizers import Adamfrom keras.layers import Input, Densefrom bert.extract_feature import BertVectorfrom att import Attentionfrom keras.layers import GRU, Bidirectional train_df, test_df = get_train_test_pd() bert_model = BertVector(pooling_strategy="NONE" , max_seq_len=80 )print ('begin encoding' ) f = lambda text: bert_model.encode([text])["encodes" ][0 ] train_df['x' ] = train_df['text' ].apply(f) test_df['x' ] = test_df['text' ].apply(f)print ('end encoding' ) x_train = np.array([vec for vec in train_df['x' ]]) x_test = np.array([vec for vec in test_df['x' ]]) y_train = np.array([vec for vec in train_df['label' ]]) y_test = np.array([vec for vec in test_df['label' ]]) num_classes = 14 y_train = to_categorical(y_train, num_classes) y_test = to_categorical(y_test, num_classes) inputs = Input(shape=(80 , 768 ,)) gru = Bidirectional(GRU(128 , dropout=0.2 , return_sequences=True ))(inputs) attention = Attention(32 )(gru) output = Dense(14 , activation='softmax' )(attention) model = Model(inputs, output) model.compile (loss='categorical_crossentropy' , optimizer=Adam(), metrics=['accuracy' ]) model.fit(x_train, y_train, batch_size=8 , epochs=30 ) model.save('people_relation.h5' )print (model.evaluate(x_test, y_test))
利用该模型对数据集进行训练,输出的结果如下:
1 2 3 4 5 6 7 8 begin encoding end encoding Epoch 1/30 1433/1433 [==============================] - 15s 10ms/step - loss: 1.5558 - acc: 0.4962**** **** **(中间部分省略输出)** **** **** **** Epoch 30/30 1433/1433 [==============================] - 12s 8ms/step - loss: 0.0210 - acc: 0.9951 [1.1099, 0.7709]
整个训练过程持续十来分钟,经过30个epoch的训练,最终在测试集上的loss为1.1099,acc为0.7709,在小数据量下的效果还是不错的。训练过程(加入了early
stopping机制)生成的loss和acc图形如下:
加入early
stopping后的训练结果
模型预测
上述模型训练完后,利用保存好的模型文件,对新的数据进行预测。模型预测的脚本为model_predict.py
,完整的Python代码如下:
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 import jsonimport numpy as npfrom bert.extract_feature import BertVectorfrom keras.models import load_modelfrom att import Attention model = load_model('people_relation.h5' , custom_objects={"Attention" : Attention}) text = '赵金闪#罗玉兄#在这里,赵金闪和罗玉兄夫妇已经生活了大半辈子。他们夫妇都是哈密市伊州区林业和草原局的护林员,扎根东天山脚下,守护着这片绿。' per1, per2, doc = text.split('#' ) text = '$' .join([per1, per2, doc.replace(per1, len (per1)*'#' ).replace(per2, len (per2)*'#' )])print (text) bert_model = BertVector(pooling_strategy="NONE" , max_seq_len=80 ) vec = bert_model.encode([text])["encodes" ][0 ] x_train = np.array([vec]) predicted = model.predict(x_train) y = np.argmax(predicted[0 ])with open ('data/rel_dict.json' , 'r' , encoding='utf-8' ) as f: rel_dict = json.load(f) id_rel_dict = {v:k for k,v in rel_dict.items()}print (id_rel_dict[y])
该人物关系输出的结果为夫妻
。
接着,我们对更好的数据进行预测,输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 原文: 润生#润叶#不过,他对润生的姐姐润叶倒怀有一种亲切的感情。 预测人物关系: 兄弟姐妹 原文: 孙玉厚#兰花#脑子里把前后村庄未嫁的女子一个个想过去,最后选定了双水村孙玉厚的大女子兰花。 预测人物关系: 父母 原文: 金波#田福堂#每天来回二十里路,与他一块上学的金波和大队书记田福堂的儿子润生都有自行车,只有他是两条腿走路。 预测人物关系: unknown 原文: 润生#田福堂#每天来回二十里路,与他一块上学的金波和大队书记田福堂的儿子润生都有自行车,只有他是两条腿走路。 预测人物关系: 父母 原文: 周山#李自成#周山原是李自成亲手提拔的将领,闯王对他十分信任,叫他担任中军。 预测人物关系: 上下级 原文: 高桂英#李自成#高桂英是李自成的结发妻子,今年才三十岁。 预测人物关系: 夫妻 原文: 罗斯福#特德#果然,此后罗斯福的政治旅程与长他24 岁的特德叔叔如出一辙——纽约州议员、助理海军部长、纽约州州长以至美国总统。 预测人物关系: 亲戚 原文: 詹姆斯#克利夫兰#詹姆斯担任了该公司的经理,作为一名民主党人,他曾资助过克利夫兰的再度竞选,两人私交不错。 预测人物关系: 上下级(预测出错,应该是好友关系) 原文: 高剑父#关山月#高剑父是关山月在艺术道路上非常重要的导师,同时关山月也是最能够贯彻高剑父“折中中西”理念的得意门生。 预测人物关系: 师生 原文: 唐怡莹#唐石霞#唐怡莹,姓他他拉氏,名为他他拉·怡莹,又名唐石霞,隶属于满洲镶红旗。 预测人物关系: 同人
总结
本文采用的深度学习模型是文本分类模型,结合BERT预训练模型,在小标注数据量下对人物关系抽取这个任务取得了还不错的效果。同时模型的识别准确率和使用范围还有待于提升,提升点笔者认为如下:
欢迎关注我的公众号
NLP奇幻之旅 ,原创技术文章第一时间推送。
欢迎关注我的知识星球“自然语言处理奇幻之旅 ”,笔者正在努力构建自己的技术社区。