NLP(二十六)限定領域的三元組抽取的一次嘗試

  • 2020 年 3 月 15 日
  • 筆記

  本文將會介紹筆者在2019語言與智能技術競賽的三元組抽取比賽方面的一次嘗試。由於該比賽早已結束,筆者當時也沒有參加這個比賽,因此沒有測評成績,我們也只能拿到訓練集和驗證集。但是,這並不耽誤我們在這方面做實驗。

比賽介紹

  該比賽的網址為:http://lic2019.ccf.org.cn/kg ,該比賽主要是從給定的句子中提取三元組,給定schema約束集合及句子sent,其中schema定義了關係P以及其對應的主體S和客體O的類別,例如(S_TYPE:人物,P:妻子,O_TYPE:人物)、(S_TYPE:公司,P:創始人,O_TYPE:人物)等。比如下面的例子:

{    "text": "九玄珠是在縱橫中文網連載的一部小說,作者是龍馬",    "spo_list": [      ["九玄珠", "連載網站", "縱橫中文網"],      ["九玄珠", "作者", "龍馬"]    ]  }

該比賽一共提供了20多萬標註質量很高的三元組,其中17萬訓練集,2萬驗證集和2萬測試集,實體關係(schema)50個。
  在具體介紹筆者的思路和實戰前,先介紹下本次任務的處理思路:
任務的處理思路
首先是對拿到的數據進行數據分析,包括統計每個句子的長度及三元組數量,每種關係的數量分佈情況。接着,對數據單獨走序列標註模型和關係分析模型。最後在提取三元組的時候,用Pipeline模型,先用序列標註模型預測句子中的實體,再對實體(加上句子)走關係分類模型,預測實體的關係,最後形成有效的三元組。
  接下來筆者將逐一介紹,項目結構圖如下:
項目結構圖

數據分析

  我們能拿到的只有訓練集和驗證集,沒有測試集。我們對訓練集做數據分析,訓練集數據文件為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')

輸出結果如下:

             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。
  每句話中的三元組數量的分佈圖如下:每句話中的三元組數量分佈圖
  關係數量的分佈圖如下:
關係數量分佈圖

序列標註模型

  我們將句子中的主體和客體作為實體,分別標註為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/5  173109/173109 [==============================] - 3275s 19ms/step - loss: 0.1269 - crf_viterbi_accuracy: 0.9417 - val_loss: 0.0251 - val_crf_viterbi_accuracy: 0.9613  Epoch 2/5  173109/173109 [==============================] - 3252s 19ms/step - loss: -0.0192 - crf_viterbi_accuracy: 0.9623 - val_loss: -0.0612 - val_crf_viterbi_accuracy: 0.9638  Epoch 3/5  173109/173109 [==============================] - 3445s 20ms/step - loss: -0.1040 - crf_viterbi_accuracy: 0.9644 - val_loss: -0.1450 - val_crf_viterbi_accuracy: 0.9649  Epoch 4/5  173109/173109 [==============================] - 3363s 19ms/step - loss: -0.1869 - crf_viterbi_accuracy: 0.9655 - val_loss: -0.2269 - val_crf_viterbi_accuracy: 0.9652  Epoch 5/5  173109/173109 [==============================] - 3266s 19ms/step - loss: -0.2694 - crf_viterbi_accuracy: 0.9662 - val_loss: -0.3088 - val_crf_viterbi_accuracy: 0.9651             precision    recall  f1-score   support          OBJ     0.9591    0.8870    0.9216     40844       SUBJ     0.9621    0.9202    0.9407     25252    micro avg     0.9603    0.8997    0.9290     66096  macro avg     0.9602    0.8997    0.9289     66096

利用seqeval模塊做評估,在驗證集上的F1值約為93%。

關係分類模型

  需要對關係做一下說明,因為筆者會對句子(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

參考網址

  1. NLP(二十五)實現ALBERT+Bi-LSTM+CRF模型:https://blog.csdn.net/jclian91/article/details/104826655
  2. NLP(二十一)人物關係抽取的一次實戰: https://blog.csdn.net/jclian91/article/details/104380371
  3. 基於DGCNN和概率圖的輕量級信息抽取模型:https://spaces.ac.cn/archives/6671