本文將會介紹筆者在2019語言與智能技術競賽的三元組抽取比賽方面的一次嘗試。因爲該比賽早已結束,筆者當時也沒有參加這個比賽,所以沒有測評成績,咱們也只能拿到訓練集和驗證集。可是,這並不耽誤咱們在這方面作實驗。python
該比賽的網址爲:http://lic2019.ccf.org.cn/kg ,該比賽主要是從給定的句子中提取三元組,給定schema約束集合及句子sent,其中schema定義了關係P以及其對應的主體S和客體O的類別,例如(S_TYPE:人物,P:妻子,O_TYPE:人物)、(S_TYPE:公司,P:創始人,O_TYPE:人物)等。好比下面的例子:git
{ "text": "九玄珠是在縱橫中文網連載的一部小說,做者是龍馬", "spo_list": [ ["九玄珠", "連載網站", "縱橫中文網"], ["九玄珠", "做者", "龍馬"] ] }
該比賽一共提供了20多萬標註質量很高的三元組,其中17萬訓練集,2萬驗證集和2萬測試集,實體關係(schema)50個。
在具體介紹筆者的思路和實戰前,先介紹下本次任務的處理思路:
首先是對拿到的數據進行數據分析,包括統計每一個句子的長度及三元組數量,每種關係的數量分佈狀況。接着,對數據單獨走序列標註模型和關係分析模型。最後在提取三元組的時候,用Pipeline模型,先用序列標註模型預測句子中的實體,再對實體(加上句子)走關係分類模型,預測實體的關係,最後造成有效的三元組。
接下來筆者將逐一介紹,項目結構圖以下:github
咱們能拿到的只有訓練集和驗證集,沒有測試集。咱們對訓練集作數據分析,訓練集數據文件爲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')
輸出結果以下:json
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。
每句話中的三元組數量的分佈圖以下:
關係數量的分佈圖以下:app
咱們將句子中的主體和客體做爲實體,分別標註爲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)
咱們在網上找幾條樣本進行測試,測試的結果以下:
原文: 真人版的《花木蘭》由新西蘭導演妮基·卡羅執導,由劉亦菲、甄子丹、鄭佩佩、鞏俐、李連杰等加盟,幾乎是全亞洲陣容。
實體: {'OBJ': ['妮基·卡羅', '劉亦菲', '甄子丹', '鄭佩佩', '鞏俐', '李連杰'], 'SUBJ': ['花木蘭']}
三元組: [['花木蘭', '主演', '劉亦菲'],
['花木蘭', '導演', '妮基·卡羅'],
['花木蘭', '主演', '甄子丹'],
['花木蘭', '主演', '李連杰'],
['花木蘭', '主演', '鄭佩佩'],
['花木蘭', '主演', '鞏俐']]原文: 《冒險小王子》做者周藝文先生,教育、文學領域的專家學者以及來自全國各地的出版業從業者參加了這次沙龍,並圍繞兒童文學創做這一話題作了精彩的分享與交流。
實體: {'OBJ': ['周藝文'], 'SUBJ': ['冒險小王子']}
三元組: [['冒險小王子', '做者', '周藝文']]原文: 宋應星是江西奉新人,公元1587年生,經歷過明朝腐敗至滅亡的最後時期。
實體: {'OBJ': ['江西奉新', '1587年'], 'SUBJ': ['宋應星']}
三元組: [['宋應星', '出生地', '江西奉新'], ['宋應星', '出生日期', '1587年']]原文: 韓愈,字退之,河陽(今河南孟縣)人。
實體: {'OBJ': ['退之', '河陽'], 'SUBJ': ['韓愈']}
三元組: [['韓愈', '出生地', '河陽'], ['韓愈', '字', '退之']]原文: 公開資料顯示,李強,男,漢族,出生於1971年12月,北京市人,北京市委黨校在職研究生學歷,教育學學士學位,1996年11月入黨,1993年7月參加工做。
實體: {'OBJ': ['漢族', '1971年12月', '北京市', '北京市委黨校'], 'SUBJ': ['李強']}
三元組: [['李強', '民族', '漢族'],
['李強', '出生地', '北京市'],
['李強', '畢業院校', '北京市委黨校'],
['李強', '出生日期', '1971年12月']]原文: 楊牧,本名王靖獻,早期筆名葉珊,1940年生於臺灣花蓮,著名詩人、做家。
實體: {'OBJ': ['1940年', '臺灣花蓮'], 'SUBJ': ['楊牧']}
三元組: [['楊牧', '出生地', '臺灣花蓮'], ['楊牧', '出生日期', '1940年']]原文: 楊廣是隋文帝楊堅的第二個兒子。
實體: {'OBJ': ['楊堅'], 'SUBJ': ['楊廣']}
三元組: [['楊廣', '父親', '楊堅']]原文: 這次權益變更後,何金明與妻子宋琦、其子何浩再也不擁有對上市公司的控制權。
實體: {'OBJ': ['何金明'], 'SUBJ': ['宋琦', '何浩']}
三元組: [['何浩', '父親', '何金明'], ['宋琦', '丈夫', '何金明']]原文: 線上直播發佈會中,譚維維首次演繹了新歌《章存仙》,這首歌由錢雷做曲、尹約做詞,尹約也在直播現場透過手機鏡頭跟網友互動聊天。
實體: {'OBJ': ['譚維維', '錢雷', '尹約', '尹約'], 'SUBJ': ['章存仙']}
三元組: [['章存仙', '做曲', '錢雷'], ['章存仙', '做詞', '尹約'], ['章存仙', '歌手', '譚維維']]原文: 「土木之變」後,造就了明代傑出的民族英雄于謙。
實體: {'OBJ': ['明代'], 'SUBJ': ['于謙']}
三元組: [['于謙', '朝代', '明代']]原文: 另外,哈爾濱歷史博物館也是全國面積最小的國有博物館,該場館面積只有50平方米,可稱之「微縮博物館」。
實體: {'OBJ': ['50平方米'], 'SUBJ': ['哈爾濱歷史博物館']}
三元組: [['哈爾濱歷史博物館', '佔地面積', '50平方米']]原文: 孫楊的媽媽叫楊明,孫楊的名字後面一個字也是來源於她的名字。
實體: {'OBJ': ['楊明', '孫楊'], 'SUBJ': ['孫楊']}
三元組: [['孫楊', '母親', '楊明']]原文: 企查查顯示,達鑫電子成立於1998年6月,法定表明人張高圳,註冊資本772.33萬美圓,股東僅新加坡達鑫控股有限公司一名。
實體: {'OBJ': ['1998年6月'], 'SUBJ': ['達鑫電子']}
三元組: [['達鑫電子', '成立日期', '1998年6月']]
本文標題爲限定領域的三元組抽取的一次嘗試,之因此取名爲限定領域,是由於該任務的實體關係是肯定,一共爲50種關係。
固然,上述方法還存在着諸多不足,參考蘇建林的文章基於DGCNN和機率圖的輕量級信息抽取模型,咱們發現不足之處以下:
從比賽的角度將,本文的辦法效果未知,應該會比聯合模型的效果差一些。可是,這是做爲筆者本身的模型,算法是一種嘗試,之因此採用這種方法,是由於筆者一開始是從開放領域的三元組抽取入手的,而這種方法方便擴展至開放領域。關於開放領域的三元組抽取,筆者稍後就會寫文章介紹,敬請期待。
本文的源代碼已經公開至Github,網址爲:
https://github.com/percent4/ccks_triple_extract 。