去年,筆者寫過一篇文章利用關係抽取構建知識圖譜的一次嘗試,試圖用如今的深度學習辦法去作開放領域的關係抽取,可是遺憾的是,目前在開放領域的關係抽取,尚未成熟的解決方案和模型。當時的文章僅做爲筆者的一次嘗試,在實際使用過程當中,效果有限。
本文將講述如何利用深度學習模型來進行人物關係抽取。人物關係抽取能夠理解爲是關係抽取,這是咱們構建知識圖譜的重要一步。本文人物關係抽取的主要思想是關係抽取的pipeline(管道)模式,由於人名可使用現成的NER模型提取,所以本文僅解決從文章中抽取出人名後,如何進行人物關係抽取。
本文采用的深度學習模型是文本分類模型,結合BERT預訓練模型,取得了較爲不錯的效果。
本項目已經開源,Github地址爲:https://github.com/percent4/people_relation_extract 。
本項目的項目結構圖以下:
html
在進行這方面的嘗試以前,咱們還不得不面對這樣一個難題,那就是中文人物關係抽取語料的缺失。數據是模型的前提,沒有數據,一切模型無從談起。所以,筆者不得不花費大量的時間收集數據。
筆者利用大量本身業餘的時間,收集了大約1800條人物關係樣本,整理成Excel(文件名稱爲人物關係表.xlsx
),前幾行以下:
人物關係一共有14類,分別爲unknown
,夫妻
,父母
,兄弟姐妹
,上下級
,師生
,好友
,同窗
,合做
,同人
,情侶
,祖孫
,同門
,親戚
,其中unknown
類別表示該人物關係不在其他的13類中(人物之間沒有關係或者爲其餘關係),同人
關係指的是兩我的物實際上是同一我的,好比下面的例子:python
邵逸夫(1907年10月4日—2014年1月7日),原名邵仁楞,生於浙江省寧波市鎮海鎮,祖籍浙江寧波。
上面的例子中,邵逸夫和邵仁楞就是同一我的。親戚
關係指的是除了夫妻
,父母
,兄弟姐妹
,祖孫
以外的親戚關係,好比叔侄,舅甥關係等。
爲了對該數據集的每一個關係類別的數量進行統計,咱們可使用腳本data/relation_bar_chart.py
,完整的Python代碼以下:git
# -*- 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()
運行後的結果以下:
unknown
類別最多,有466條,其他的如祖孫
, 親戚
, 情侶
, 同門
等較多,只有60多條,這是由於這類人物關係的數據缺失很差收集。所以,語料的收集費時費力,須要消耗大量的精力。github
收集好數據後,咱們須要對數據進行預處理,預處理主要分兩步,一步是將人物關係和原文本整合在一塊兒,第二步簡單,將數據集劃分爲訓練集和測試集,比例爲8:2。
咱們對第一步進行詳細說明,將人物關係和原文本整合在一塊兒。通常咱們給定原文本和該文本中的兩我的物,好比:web
邵逸夫(1907年10月4日—2014年1月7日),原名邵仁楞,生於浙江省寧波市鎮海鎮,祖籍浙江寧波。
這句話中有兩我的物:邵逸夫,邵仁楞, 這個容易在語料中找到。而後咱們將原文本的這兩我的物中的每一個字符分別用'#'號代碼,並經過'$'符號拼接在一塊兒,造成的整合文本以下:json
邵逸夫$邵仁楞$###(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文件中保存的信息以下:app
{ "unknown": 0, "夫妻": 1, "父母": 2, "兄弟姐妹": 3, "上下級": 4, "師生": 5, "好友": 6, "同窗": 7, "合做": 8, "同人": 9, "情侶": 10, "祖孫": 11, "同門": 12, "親戚": 13 }
簡單來講,是給每種關係一個id,轉化成類別型變量。
以train.txt爲例,其前5行的內容以下:dom
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導出):
模型訓練的腳本爲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預訓練模型,在小標註數據量下對人物關係抽取這個任務取得了還不錯的效果。同時模型的識別準確率和使用範圍還有待於提高,提高點筆者認爲以下:
感謝你們閱讀~
本人的微信公衆號: Python之悟(微信號爲:easy_web_scrape),歡迎你們關注~