25

NLP(二十一)人物关系抽取的一次实战

 4 years ago
source link: http://www.cnblogs.com/jclian91/p/12328570.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

去年,笔者写过一篇文章 利用关系抽取构建知识图谱的一次尝试 ,试图用现在的深度学习办法去做开放领域的关系抽取,但是遗憾的是,目前在开放领域的关系抽取,还没有成熟的解决方案和模型。当时的文章仅作为笔者的一次尝试,在实际使用过程中,效果有限。

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

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

本项目已经开源,Github地址为: https://github.com/percent4/people_relation_extract

本项目的项目结构图如下:

NBFbAvz.png!web

数据集介绍

在进行这方面的尝试之前,我们还不得不面对这样一个难题,那就是中文人物关系抽取语料的缺失。数据是模型的前提,没有数据,一切模型无从谈起。因此,笔者不得不花费大量的时间收集数据。

笔者利用大量自己业余的时间,收集了大约1800条人物关系样本,整理成Excel(文件名称为 人物关系表.xlsx ),前几行如下:

jiaABnj.png!web

人物关系一共有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()

# Mac系统设置中文字体支持
plt.rcParams["font.family"] = 'Arial Unicode MS'

# 利用Matplotlib绘制条形图
x = range(len(num_list))
rects = plt.bar(left=x, height=num_list, width=0.6, color='blue', label="频数")
plt.ylim(0, 500) # 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()

运行后的结果如下:

VNRV3eJ.png!web

unknown 类别最多,有466条,其余的如 祖孙 , 亲戚 , 情侣 , 同门 等较多,只有60多条,这是因为这类人物关系的数据缺失不好收集。因此,语料的收集费时费力,需要消耗大量的精力。

数据预处理

收集好数据后,我们需要对数据进行预处理,预处理主要分两步,一步是将人物关系和原文本整合在一起,第二步简单,将数据集划分为训练集和测试集,比例为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))

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文件中保存的信息如下:

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

简单来说,是给每种关系一个id,转化成类别型变量。

以train.txt为例,其前5行的内容如下:

4 方琳$李伟康$在生活中,###则把##看作小辈,常常替她解决难题。
3 佳子$久仁$12月,##和弟弟##参加了在东京举行的全国初中生演讲比赛。
2 钱慧安$钱禄新$###,生卒年不详,海上画家###之子。
0 吴继坤$邓新生$###还曾对媒体说:“我这个小小的投资商,经常得到###等领导的亲自关注和关照,我觉到受宠若惊。”
2 洪博培$乔恩·M·亨茨曼$###的父亲########是著名企业家、美国最大化学公司亨茨曼公司创始人。
10 夏乐$陈飞$两小无猜剧情简介:##和##是一对从小一起长大的青梅竹马。

在每一行中,空格之前的数字所对应的人物关系可以在 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用来提取文本的特征,关于这一部分的介绍,已经在文章 NLP(二十)利用BERT实现文本二分类 中给出;Attention为注意力机制层,FC为全连接层,模型的结构图如下(利用Keras导出):

YJvM3uA.png!web

模型训练的脚本为 model_train.py ,完整的Python代码如下:

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

import numpy as np
from load_data import get_train_test_pd
from keras.utils import to_categorical
from keras.models import Model
from keras.optimizers import Adam
from keras.layers import Input, Dense
from bert.extract_feature import BertVector

from att import Attention
from 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']])
# 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(14, activation='softmax')(attention)
model = Model(inputs, output)

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

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))

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

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,在小数据量下的效果还是不错的。

模型预测

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

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

import json
import numpy as np
from bert.extract_feature import BertVector
from keras.models import load_model
from 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提取句子特征
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])

该人物关系输出的结果为 夫妻

接着,我们对更好的数据进行预测,输出的结果如下:

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

总结

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

  • 标注的数据量需要加大,现在的数据才1800条左右,如果数据量上去了,那么模型的准确率还有使用范围也会提升;
  • 其他更多的模型有待于尝试;
  • 在预测时,模型的预测时间较长,原因在于用BERT提取特征时耗时较长,可以考虑缩短模型预测的时间;
  • 其他问题欢迎补充。

感谢大家阅读~

本人的微信公众号: Python之悟(微信号为:easy_web_scrape),欢迎大家关注~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK