信息抽取实战:人物关系抽取【BERT模型】(附代码)

实战:人物关系抽取

  人物关系抽取可看作是实体关系抽取的的一个重要的分支,只是将关系抽取中的命名实体限定为人名。人物由于其特定的存在而与他人之间产生的关系称为人物关系。人物关系抽取有其特定的模式和特征。在之前的研究中,人物关系抽取的方法主要有三种:

  1)采用关系描述模式该方法一般事先定义好需要抽取的人物关系类型,统计或自动生成关系描述词,接着收集人物关系描述模式或者有序列的关系特征词模式,利用这些模式匹配出更多的人物实例。
  2)利用机器学习算法训练分类器。这类方法摒弃关系模式方法的强制匹配,而是选择有效特征,在标记关系数据的基础上,选择合适的机器学习算法(常用算法是SVM、改进SVM等)进行训练,得到关系分类器用以关系识别
  3)自动生成关系描述短语这类方法一般采用聚类算法,无需定义人物关系类型,而是将两个人名实体的共现句中能表达关系的短语作为关系类型。

  从当前人物关系抽取技术的研究现状来看,人物关系抽取的方法研究中仍然存在着以下几点所面临的挑战: 1)关于是否预先定义人物关系类型。2)关于人物关系在识别时被遗漏。3)关于无效的人名实体共现句在存在关系的人名实体共现句中,会有部分共现句对该人物关系的描述是没有明显作用,是无效的。或者共现句中存在两个以上的人名实体,多种关系类型,那么这些共现句对其中任意一个关系类型都没有太多有效性。

  本项目将讲述如何利用深度学习模型来进行人物关系抽取人物关系抽取可以理解为是关系抽取,这是我们构建知识图谱的重要一步。本项目人物关系抽取的主要思想是关系抽取的pipeline(管道)模式,因为人名可以使用现成的NER模型提取,因此本项目仅解决从文章中抽取出人名后,如何进行人物关系抽取。

  本项目采用的深度学习模型是文本分类模型,结合BERT预训练模型,取得了较为不错的效果

  本项目已经开源,Github地址为:https://github.com/chenlian-zhou/people_relation_extract/tree/master/people_relation_extract

  本项目的项目结构图如下
在这里插入图片描述

数据集介绍

   在进行这方面的尝试之前,我们还不得不面对这样一个难题,那就是中文人物关系抽取语料的缺失。数据是模型的前提,没有数据,一切模型无从谈起。因此,笔者不得不花费大量的时间收集数据。
  利用大量自己业余的时间,收集了大约2900条人物关系样本,整理成Excel(文件名称为人物关系表.xlsx),其中几行如下:
在这里插入图片描述
  人物关系一共有14类,分别为unknown、夫妻、父母、兄弟姐妹、上下级、师生、好友、同学、合作、同人、情侣、祖孙、同门、亲戚,其中unknown类别表示该人物关系不在其余的13类中(人物之间没有关系或者为其他关系),同人关系指的是两个人物其实是同一个人,比如下面的例子:

邵逸夫(1907年10月4日—2014年1月7日),原名邵仁楞,生于浙江省宁波市镇海镇,祖籍浙江宁波。

  上面的例子中,邵逸夫和邵仁楞就是同一个人。亲戚关系指的是除了夫妻,父母,兄弟姐妹,祖孙之外的亲戚关系,比如叔侄,舅甥关系等。
  为了对该数据集的每个关系类别的数量进行统计,我们可以使用脚本data/relation_bar_chart.py,完整的Python代码如下:

# -*- coding: utf-8 -*-
# 绘制人物关系频数统计条形图
import pandas as pd
import matplotlib.pyplot as plt

# 读取EXCEL数据
df = pd.read_excel('人物关系表.xlsx')
label_list = list(df['关系'].value_counts().index)
num_list = df['关系'].value_counts().tolist()

# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei'] # 指定默认字体
plt.rcParams['axes.unicode_minus'] = False # 解决保存图像是负号'-'显示为方块的问题

# 利用Matplotlib模块绘制条形图
x = range(len(num_list))
rects = plt.bar(x=x, height=num_list, width=0.6, color='blue', label="频数")
# plt.ylim(0, 800) # y轴范围
plt.ylabel("数量")
plt.xticks([index + 0.1 for index in x], label_list)
plt.xticks(rotation=45) # x轴的标签旋转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()
plt.savefig('./bar_chart.png')

运行后的结果如下:
在这里插入图片描述
  unknown类别最多,有791条,其余的如祖孙、亲戚、情侣等较少,只有90多条,这是因为这类人物关系的数据缺失不好收集。因此,语料的收集费时费力,需要消耗大量的精力。

数据预处理

  收集好数据后,我们需要对数据进行预处理,预处理主要分两步,一步是将人物关系和原文本整合在一起;第二步,将数据集划分为训练集和测试集,比例为8:2。
  我们对第一步进行详细说明,将人物关系和原文本整合在一起。一般我们给定原文本和该文本中的两个人物,比如:

邵逸夫(1907年10月4日—2014年1月7日),原名邵仁楞,生于浙江省宁波市镇海镇,祖籍浙江宁波。

  这句话中有两个人物:邵逸夫、邵仁楞, 这个容易在语料中找到。然后我们将原文本的这两个人物中的每个字符分别用’#‘号代码,并通过’$'符号拼接在一起,形成的整合文本如下:

邵逸夫 邵仁楞 ###(1907年10月4日—2014年1月7日),原名###,生于浙江省宁波市镇海镇,祖籍浙江宁波。

  处理成这种格式是为了方便文本分类模型进行调用。
  数据预处理的脚本为data/data_into_train_test.py,完整的Python代码如下:

# -*- coding: utf-8 -*-
import json
import pandas as pd
from 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))

print('总数: %s' % len(df))
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

# df = df.iloc[:100, :] # 取前n条数据进行模型方面的测试

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.txtrel_dict.json,该json文件中保存的信息如下:

{
“unknown”: 0,
“夫妻”: 1,
“父母”: 2,
“兄弟姐妹”: 3,
“上下级”: 4,
“师生”: 5,
“好友”: 6,
“同学”: 7,
“合作”: 8,
“同人”: 9,
“情侣”: 10,
“祖孙”: 11,
“同门”: 12,
“亲戚”: 13
}

  简单来说,是给每种关系一个id,转化成类别型变量。
以train.txt为例,其前5行的内容如下:

在这里插入图片描述

  在每一行中,空格之前的数字所对应的人物关系可以在rel_dict.json中找到。

模型训练

  在模型训练前,为了将数据的格式更好地适应模型,需要再对trian.txt和test.txt进行处理。处理脚本为load_data.py,完整的Python代码如下:

# -*- coding: utf-8 -*-
import pandas as pd


# 读取txt文件
def 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, texts

# 获取训练数据和测试数据,格式为pandas的DataFrame
def 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_df


if __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用来提取文本的特征;Attention为注意力机制层,FC为全连接层,模型的结构图如下(利用Keras导出):
在这里插入图片描述
  模型训练的脚本为model_train.py,完整的Python代码如下:

# -*- coding: utf-8 -*-
# 模型训练

import os, json
import numpy as np
from keras.utils import to_categorical
from keras.models import Model
from keras.optimizers import Adam
from keras.layers import Input, Dense
from keras.callbacks import EarlyStopping
from att import Attention
from keras.layers import GRU, LSTM, Bidirectional
from keras.callbacks import ModelCheckpoint
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report
from operator import itemgetter

from load_data import get_train_test_pd
from bert.extract_feature import BertVector


# 读取文件并进行转换
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']])
# print('x_train: ', x_train.shape)

# 将类型y值转化为ont-hot向量
num_classes = 14
y_train = to_categorical(y_train, num_classes)
y_test = to_categorical(y_test, num_classes)

# 模型结构:BERT + 双向GRU + Attention + FC
inputs = Input(shape=(80, 768, ))
gru = Bidirectional(GRU(128, dropout=0.2, return_sequences=True))(inputs)
attention = Attention(32)(gru)
output = Dense(num_classes, activation='softmax')(attention)
model = Model(inputs, output)

# 模型可视化
# from keras.utils import plot_model
# plot_model(model, to_file='model.png', show_shapes=True)

model.compile(loss='categorical_crossentropy',
              optimizer=Adam(),
              metrics=['accuracy'])

# early stopping
early_stopping = EarlyStopping(monitor='val_accuracy', patience=5, mode='max')

# 如果原来models文件夹下存在.h5文件,则全部删除
model_dir = './models'
if os.makedirs(model_dir):
    for file in os.listdir(model_dir):
        os.remove(os.path.join(model_dir, file))

# 保存最新的val_acc最好的模型文件
filepath="models/per-rel-{epoch:02d}-{val_accuracy:.4f}.h5"
checkpoint = ModelCheckpoint(filepath, monitor='val_accuracy', verbose=1, save_best_only=True,mode='max')

# 模型训练以及评估
history = model.fit(x_train, y_train, validation_data=(x_test, y_test), batch_size=16, epochs=30, callbacks=[early_stopping, checkpoint])
# model.save('people_relation.h5')

print('在测试集上的效果:', model.evaluate(x_test, y_test))

# 读取关系对应表
with open('./data/rel_dict.json', 'r', encoding='utf-8') as f:
    label_id_dict = json.loads(f.read())

sorted_label_id_dict = sorted(label_id_dict.items(), key=itemgetter(1))
values = [_[0] for _ in sorted_label_id_dict]

# 输出每一类的classification report
y_pred = model.predict(x_test, batch_size=32)
print(classification_report(y_test.argmax(axis=1), y_pred.argmax(axis=1), target_names=values))

# 绘制loss和acc图像
plt.subplot(2, 1, 1)
epochs = len(history.history['loss'])
plt.plot(range(epochs), history.history['loss'], label='loss')
plt.plot(range(epochs), history.history['val_loss'], label='val_loss')
plt.legend()

plt.subplot(2, 1, 2)
epochs = len(history.history['accuracy'])
plt.plot(range(epochs), history.history['accuracy'], label='acc')
plt.plot(range(epochs), history.history['val_accuracy'], label='val_acc')
plt.legend()
plt.show()
plt.savefig("loss_acc.png")

  利用该模型对数据集进行训练,输出的结果如下:

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图形如下:
在这里插入图片描述

模型预测

  上述模型训练完后,利用保存好的模型文件,对新的数据进行预测。模型预测的脚本为model_predict.py,完整的Python代码如下:

# -*- coding: utf-8 -*-
# 模型预测

import os, json
import numpy as np
from bert.extract_feature import BertVector
from keras.models import load_model
from att import Attention

# 加载训练效果最好的模型
model_dir = './models'
files = os.listdir(model_dir)
models_path = [os.path.join(model_dir, _) for _ in files]
best_model_path = sorted(models_path, key=lambda x: float(x.split('-')[-1].replace('.h5', '')), reverse=True)[0]
print(best_model_path)
model = load_model(best_model_path, custom_objects={"Attention": Attention})

# 示例语句及预处理
# text1 = '唐怡莹#唐石霞#唐怡莹,姓他他拉氏,名为他他拉·怡莹,又名唐石霞,隶属于满洲镶红旗。'
text1 = '谢才萍#文斌#谢才萍的丈夫文斌利用其兄文强的影响,公开找渝中区公安分局原局长彭长健,要他关照老婆的“经'
# text1 = '文强#文斌#谢才萍的丈夫文斌利用其兄文强的影响,公开找渝中区公安分局原局长彭长健,要他关照老婆的“经'
# text1 = '文强#谢才萍#谢才萍的丈夫文斌利用其兄文强的影响,公开找渝中区公安分局原局长彭长健,要他关照老婆的“经'
# text1 = '崔新琴#黄晓明#那时,老师崔新琴如此评价黄晓明:“没有灵性,就是一块漂亮的木头。”'
# text1 = '秦天#秦先生#这个看来并不高深的游戏却让秦天和的爸爸秦先生大伤脑筋。'
# text1 = '马清伟#马桂烽#说到早前传出与家族拥百亿财产的富豪马清伟和薛芷伦大儿子马桂烽(Justin)分手,她说:“有这个传闻的时候我经纪人已经代为回答,这是两个人的事不会回应,今天也是一样,多谢大家关心。'
# text1 = '李克农#李伦#6月25日,澎湃新闻(www.thepaper.cn)从李伦将军亲友处获悉,开国上将李克农之子、解放军原总后勤部副部长李伦中将于2019年6月25日凌晨在北京逝世,享年92岁。'
# text1 = '利孝和#陆雁群#利孝和的妻子是陆雁群,尊称「利孝和夫人」,是现任无线非执行董事,香港著名慈善家及利希慎家族成员'
# text1 = '张少怀#费贞绫#家庭出生演艺世家的张菲,父亲为台湾综艺大哥张少怀,叔叔是费玉清,姑姑是费贞绫。'
# text1 = '查济民#刘璧如	#刘璧如女士在香港妇女界是位出类拔萃的人物,与其夫婿查济民先生一道经营中国染厂 。'
# text1 = '申军良#申聪#3月7日,申军良夫妇与申聪认亲。'
# text1 = '林志玲#黑泽良平#而最受关注的要数林志玲会携新婚丈夫黑泽良平首次合体亮相晚会。'
# text1 = '陈发科#陈小旺#陈发科的爷爷是陈式太极拳一代宗师陈小旺,父亲是陈照旭。'
# text1 = '陈发科#陈照旭#陈发科的爷爷是陈式太极拳一代宗师陈小旺,父亲是陈照旭。'
# text1 = '何鸿燊#叶德利#叶德利的妻子是何鸿燊胞妹何婉婉'
# text1 = '诸葛瑾#诸葛恪#诸葛瑾,(174-240)诸葛亮之兄,诸葛恪之父,经鲁肃推荐,为东吴效力。'
per1, per2, doc = text1.split('#')
text = '$'.join([per1, per2, doc.replace(per1, len(per1)*'#').replace(per2, len(per2)*'#')])
print(text)


# 利用BERT提取句子特征
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('原文: %s' % text1)
print('预测人物关系: %s' % id_rel_dict[y])

# 预测分类结果为:夫妻

  该人物关系输出的结果为夫妻
  接着,我们对更好的数据进行预测,输出的结果如下:

原文: 润生#润叶#不过,他对润生的姐姐润叶倒怀有一种亲切的感情。
预测人物关系: 兄弟姐妹 原文:
孙玉厚#兰花#脑子里把前后村庄未嫁的女子一个个想过去,最后选定了双水村孙玉厚的大女子兰花。
预测人物关系: 父母
原文: 金波#田福堂#每天来回二十里路,与他一块上学的金波和大队书记田福堂的儿子润生都有自行车,只有他是两条腿走路。
预测人物关系: unknown
原文: 润生#田福堂#每天来回二十里路,与他一块上学的金波和大队书记田福堂的儿子润生都有自行车,只有他是两条腿走路。
预测人物关系: 父母
原文: 周山#李自成#周山原是李自成亲手提拔的将领,闯王对他十分信任,叫他担任中军。
预测人物关系: 上下级 原文:
高桂英#李自成#高桂英是李自成的结发妻子,今年才三十岁。
预测人物关系: 夫妻
原文: 罗斯福#特德#果然,此后罗斯福的政治旅程与长他24岁的特德叔叔如出一辙——纽约州议员、助理海军部长、纽约州州长以至美国总统。
预测人物关系:亲戚
原文: 詹姆斯#克利夫兰#詹姆斯担任了该公司的经理,作为一名民主党人,他曾资助过克利夫兰的再度竞选,两人私交不错。
预测人物关系:上下级(预测出错,应该是好友关系)
原文: 高剑父#关山月#高剑父是关山月在艺术道路上非常重要的导师,同时关山月也是最能够贯彻高剑父“折中中西”理念的得意门生。
预测人物关系: 师生
原文: 唐怡莹#唐石霞#唐怡莹,姓他他拉氏,名为他他拉·怡莹,又名唐石霞,隶属于满洲镶红旗。
预测人物关系: 同人

总结

  本项目采用的深度学习模型是文本分类模型结合BERT预训练模型,在小标注数据量下对人物关系抽取这个任务取得了还不错的效果。同时模型的识别准确率和使用范围还有待提升点如下:

  • 标注的数据量需要加大,现在的数据才2900条左右,如果数据量上去了,那么模型的准确率还有使用范围也会提升;
  • 尝试其他的预训练模型
  • 在预测时,模型的预测时间较长,原因在于用BERT提取特征时耗时较长,可以考虑缩短模型预测的时间(比如使用ALBERT就能大大缩短预测时间);

【参考】

https://www.cnblogs.com/jclian91/p/12328570.html

猜你喜欢

转载自blog.csdn.net/weixin_42691585/article/details/107271784