過去幾年發表於各大 AI 頂會論文提出的 400 多種演算法中,公開演算法程式碼的僅佔 6%,其中三分之一的論文作者分享了測試資料,約 54% 的分享包含“虛擬碼”。這是今年 AAAI 會議上一個嚴峻的報告。 人工智慧這個蓬勃發展的領域正面臨著實驗重現的危機,就像實驗重現問題過去十年來一直困擾著心理學、醫學以及其他領域一樣。最根本的問題是研究人員通常不共享他們的原始碼。
可驗證的知識是科學的基礎,它事關理解。隨著人工智慧領域的發展,打破不可復現性將是必要的。為此,PaperWeekly 聯手百度 PaddlePaddle 共同發起了本次論文有獎復現,我們希望和來自學界、工業界的研究者一起接力,為 AI 行業帶來良性迴圈。
引言
筆者本次復現的是中科院自動化所發表於 ACL 2017 的經典文章——Joint Extraction of Entities and Relations Based on a Novel Tagging Scheme。
對於實體和關係的聯合抽取一直是資訊抽取中重要的任務。為了解決這一問題,論文提出了一個新型的標註方式,可以解決聯合資訊抽取中的標註問題。隨後,基於這一標註方法,論文研究了不同的端到端模型,在不需要分開識別實體和關係的同時,直接抽取實體和實體之間的關係。
論文在使用了遠端監督製作的公開資料集上進行了實驗,結果說明這一標註策略較現有的管道和聯合學習方法。此外,論文所提出的端到端模型在公開資料集上取得了目前最好的效果。
論文復現程式碼:
http://aistudio.baidu.com/aistudio/#/projectdetail/26338
論文方法
論文提出了一個新型的標註方式,並設計了一個帶有偏置(Bias)目標函式的端到端模型,去聯合抽取實體和實體間的關係。
標註方式
圖 1 是一個如何將原始標註資料(實體+實體關係)轉換為論文中提到的新型標註方式的示例。在資料中,每一個詞彙會被賦予一個實體標籤,因此通過抽取和實體有關的詞語,構成實體。
▲ 圖1. 一個構成實體和關係的資料例項
第一個標籤是“O”,表示這個詞屬於“Other”標籤,詞語不在被抽取結果中。除了標籤“O”以外,其他標籤都由三部分組成:1)詞語在實體中的位置,2)實體關係型別,3)關係角色。
論文使用“BIES”規則(B:實體起始,I:實體內部,E:實體結束,S:單一實體)去標註詞語在實體中的位置資訊。對於實體關係型別,則通過預先定義的關係集合確定。對於關係角色,論文使用“1”和“2”確定。一個被抽取的實體關係結果由一個三元組表示(實體 1-關係型別-實體 2)。“1”表示這個詞語屬於第一個實體,“2”則表示這個詞語屬於第二個實體。因此,標籤總數是:Nt = 2*4 *|R|+1。R 是預先定義好的關係型別的數量。
從圖 1 可以看出,輸入的句子包含兩個三元組:
{United States, Country-President, Trump}
{Apple Inc, Company-Founder, Steven Paul Jobs}
預先定義的兩組關係是:
Country-President: CP
Company-Founder:CF
由於“United”,“States”,“ Trump”,“Apple”,“Inc” ,“Steven”, “Paul”, “Jobs”構成了描述實體的詞彙,因此這些詞語都被賦予了特定的標記。
例如,“United”是實體“United States”的第一個詞語,同時也和“Country-President”關聯,因此“United”詞語的標註是“B-CP-1”。“B”表示Begin,“CP”表示Country President,“1”表示“United”詞語所在的實體“United States”是三元組中的第一個物件。
同理,因為“States”是“United States”實體的結尾詞語,但依然屬於“Country President”關係,同時也是三元組的第一個物件,因此“States”的標註是“E-CP-1”。
對於另一個詞語“Trump”,它是構成“Trump”這一實體的唯一詞語,因此使用“S”。同時,Trump 實體屬於“Country President”關係,因此它具有CP標籤,又同時這一實體在三元組中是第二個物件,因此它被標註“2”。綜上,“Trump”這一詞語的標註是:“S-CP-2”。除了這些和實體有關的詞語外,無關詞語被標註“O”。
當然,對於擁有兩個和兩個以上實體的句子,論文將每兩個實體構成一個三元組,並使用最小距離原則(距離最近的兩個實體構成一對關係)。在圖 1 中,“United States”和“Trump”因為最小距離構成了一對實體。此外,論文只探討一對一關係三元組。
端到端模型
雙向長短時編碼層(Bi-LSTM Encoder Layer)
在序列標註問題中,雙向長短時編碼器體現了對單個詞語的語義資訊的良好捕捉。這一編碼器有一個前向和後向的長短時層,並在末尾將兩層合併。詞嵌入層則將詞語的獨熱編碼(1-hot representation)轉換為詞嵌入的向量。
▲ 公式1. 雙向長短時編碼器
公式 1 中的 i,f 和 o 分別為 LSTM 模組在 t 時刻的輸入門,遺忘門和輸出門。c 為 LSTM 模組的輸出,W 為權重。對於當前時刻,其隱層向量的結果取決於起義時刻的,上一時刻的,以及當前時刻的輸入詞語。
對於一句話,表示為。其中是第 d 維度下在第 t 個詞彙的詞向量,n 則是句序列的長度。在經過了詞嵌入後,前向和後向的長短時神經網路分佈接受資料輸入,前向則句子順序從前向後,後向則從後向前。
對於每一個詞語向量(經過詞嵌入後), 前向長短時神經網路層通過考慮語義資訊,將到的資訊全部編碼,記為。同樣,後向長短時則為。編碼器最後將兩個層的輸入相接。
長短時解碼器
論文同時使用了長短時解碼器用於標註給定序列。解碼器在當前時刻的輸入為來自雙向編碼器的隱層向量,前一個預測的標籤的嵌入,前一個時刻的神經元輸入,以及前一時刻的隱層向量。解碼器根據雙向長短時編碼器的輸出進行計算。解碼器的內部公式類似於公式 1。
▲ 公式2. 長短時解碼器
Softmax層
在解碼器後加入 softmax 層,預測該詞語的標籤。解碼器的內部結構類似於編碼器。
▲ 公式3. softmax層
為 softmax 矩陣,為總標籤數,為預測標籤的向量。
▲ 圖2. 網路整體結構圖
偏置目標函式(Bias Objective Function)
▲ 公式4. 訓練中啟用函式使用RMSprop
|D| 是訓練集大小,是句子的長度,是詞語 t 在的標籤,是歸一化的 tag 的概率。I(O) 是一個條件函式(switching function),用於區分 tag 為“O”和不為“O”的時候的損失。
▲ 公式5. 條件函式
α 是偏置權重,該項越大,則帶關係的標籤對模型的影響越大。
import paddle.fluid as fluid
import paddle.v2 as paddle
from paddle.fluid.initializer import NormalInitializer
import re
import math
#coding='utf-8'
import json
import numpy as np
from paddle.v2.plot import Ploter
train_title = "Train cost"
test_title = "Test cost"
plot_cost = Ploter(train_title, test_title)
step = 0
#=============================================global parameters and hyperparameters==================================
EMBEDDING = 300
DROPOUT = 0.5
LSTM_ENCODE = 300
LSTM_DECODE = 600
BIAS_ALPHA = 10
VALIDATION_SIZE = 0.1
TRAIN_PATH = '/home/aistudio/data/data1272/train.json'
TEST_PATH = '/home/aistudio/data/data1272/test.json'
FILE_PATH = '/home/aistudio/data/'
X_TRAIN = '/home/aistudio/data/data1272/sentence_train.txt'
Y_TRAIN = '/home/aistudio/data/data1272/seq_train.txt'
X_TEST = '/home/aistudio/data/data1272/sentence_test.txt'
Y_TEST = '/home/aistudio/data/data1272/seq_test.txt'
WORD_DICT = '/home/aistudio/data/data1272/word_dict.txt'
TAG_DICT = '/home/aistudio/data/data1272/tag_dict.txt'
EPOCH_NUM = 1000
BATCH_SIZE = 128
#=============================================get data from the dataset==============================================
def get_data(train_path, test_path, train_valid_size):
'''
extracting data for json file
'''
train_file = open(train_path).readlines()
x_train = []
y_train = []
for i in train_file:
data = json.loads(i)
x_data, y_data = data_decoding(data)
'''
appending each single data into the x_train/y_train sets
'''
x_train += x_data
y_train += y_data
test_file = open(test_path).readlines()
x_test = []
y_test = []
for j in test_file:
data = json.loads(j)
x_data, y_data = data_decoding(data)
x_test += x_data
y_test += y_data
return x_train, y_train, x_test, y_test
def data_decoding(data):
'''
decode the json file
sentText is the sentence
each sentence may have multiple types of relations
for every single data, it contains: (sentence-splited, labels)
'''
sentence = data["sentText"]
relations = data["relationMentions"]
x_data = []
y_data = []
for i in relations:
entity_1 = i["em1Text"].split(" ")
entity_2 = i["em2Text"].split(" ")
relation = i["label"]
relation_label_1 = entity_label_construction(entity_1)
relation_label_2 = entity_label_construction(entity_2)
output_list = sentence_label_construction(sentence, relation_label_1, relation_label_2, relation)
x_data.append(sentence.split(" "))
y_data.append(output_list)
return x_data, y_data
def entity_label_construction(entity):
'''
give each word in an entity the label
for entity with multiple words, it should follow the BIES rule
'''
relation_label = {}
for i in range(len(entity)):
if i == 0 and len(entity) >= 1:
relation_label[entity[i]] = "B"
if i != 0 and len(entity) >= 1 and i != len(entity) -1:
relation_label[entity[i]] = "I"
if i== len(entity) -1 and len(entity) >= 1:
relation_label[entity[i]] = "E"
if i ==0 and len(entity) == 1:
relation_label[entity[i]] = "S"
return relation_label
def sentence_label_construction(sentence, relation_label_1, relation_label_2, relation):
'''
combine the label for each word in each entity with the relation
and then combine the relation-entity label with the position of the entity in the triplet
'''
element_list = sentence.split(" ")
dlist_1 = list(relation_label_1)
dlist_2 = list(relation_label_2)
output_list = []
for i in element_list:
if i in dlist_1:
output_list.append(relation + '-' + relation_label_1[i] + '-1' )
elif i in dlist_2:
output_list.append(relation + '-' + relation_label_2[i] + '-2')
else:
output_list.append('O')
return output_list
def format_control(string):
str1 = re.sub(r'\r','',string)
str2 = re.sub(r'\n','',str1)
str3 = re.sub(r'\s*','',str2)
return str3
def joint_extraction():
vocab_size = len(open(WORD_DICT,'r').readlines())
tag_num = len(open(TAG_DICT,'r').readlines())
def bilstm_lstm(word, target, vocab_size, tag_num):
x = fluid.layers.embedding(
input = word,
size = [vocab_size, EMBEDDING],
dtype = "float32",
is_sparse = True)
y = fluid.layers.embedding(
input = target,
size = [tag_num, tag_num],
dtype = "float32",
is_sparse = True)
fw, _ = fluid.layers.dynamic_lstm(
input = fluid.layers.fc(size = LSTM_ENCODE*4, input=x),
size = LSTM_ENCODE*4,
candidate_activation = "tanh",
gate_activation = "sigmoid",
cell_activation = "sigmoid",
bias_attr=fluid.ParamAttr(
initializer=NormalInitializer(loc=0.0, scale=1.0)),
is_reverse = False)
bw, _ = fluid.layers.dynamic_lstm(
input = fluid.layers.fc(size = LSTM_ENCODE*4, input=x),
size = LSTM_ENCODE*4,
candidate_activation = "tanh",
gate_activation = "sigmoid",
cell_activation = "sigmoid",
bias_attr=fluid.ParamAttr(
initializer=NormalInitializer(loc=0.0, scale=1.0)),
is_reverse = True)
combine = fluid.layers.concat([fw,bw], axis=1)
decode, _ = fluid.layers.dynamic_lstm(
input = fluid.layers.fc(size = LSTM_DECODE*4, input=combine),
size = LSTM_DECODE*4,
candidate_activation = "tanh",
gate_activation = "sigmoid",
cell_activation = "sigmoid",
bias_attr=fluid.ParamAttr(
initializer=NormalInitializer(loc=0.0, scale=1.0)),
is_reverse = False)
softmax_connect = fluid.layers.fc(input=decode, size=tag_num)
_cost = fluid.layers.softmax_with_cross_entropy(
logits=softmax_connect,
label = y,
soft_label = True)
_loss = fluid.layers.mean(x=_cost)
return _loss, softmax_connect
source = fluid.layers.data(name="source", shape=[1], dtype="int64", lod_level=1)
target = fluid.layers.data(name="target", shape=[1], dtype="int64", lod_level=1)
loss, softmax_connect = bilstm_lstm(source, target, vocab_size, tag_num)
return loss
def get_index(word_dict, tag_dict, x_data, y_data):
x_out = [word_dict[str(k)] for k in x_data]
y_out = [tag_dict[str(l)] for l in y_data]
return [x_out, y_out]
def data2index(WORD_DICT, TAG_DICT, x_train, y_train):
def _out_dict(word_dict_path, tag_dict_path):
word_dict = {}
f = open(word_dict_path,'r').readlines()
for i, j in enumerate(f):
word = re.sub(r'\n','',str(j))
# word = re.sub(r'\r','',str(j))
# word = re.sub(r'\s*','',str(j))
word_dict[word] = i + 1
tag_dict = {}
f = open(tag_dict_path,'r').readlines()
for m,n in enumerate(f):
tag = re.sub(r'\n','',str(n))
tag_dict[tag] = m+1
return word_dict, tag_dict
def _out_data():
word_dict, tag_dict = _out_dict(WORD_DICT, TAG_DICT)
for data in list(zip(x_train, y_train)):
x_out, y_out = get_index(word_dict, tag_dict, data[0], data[1])
yield x_out, y_out
return _out_data
def optimizer_program():
return fluid.optimizer.Adam()
if __name__ == "__main__":
sentence_train, seq_train, sentence_test, seq_test = get_data(TRAIN_PATH,TEST_PATH,VALIDATION_SIZE)
train_reader = paddle.batch(
paddle.reader.shuffle(
data2index(WORD_DICT, TAG_DICT, sentence_train, seq_train),
buf_size=500),
batch_size=128)
test_reader = paddle.batch(
paddle.reader.shuffle(
data2index(WORD_DICT, TAG_DICT, sentence_test, seq_test),
buf_size=500),
batch_size=128)
place = fluid.CPUPlace()
feed_order=['source', 'target']
trainer = fluid.Trainer(
train_func=joint_extraction,
place=place,
optimizer_func = optimizer_program)
trainer.train(
reader=train_reader,
num_epochs=100,
event_handler=event_handler_plot,
feed_order=feed_order)
▲ 模型和執行函式train程式碼展示
實驗
實驗設定
資料集
使用 NYT 公開資料集。大量資料通過遠端監督的方式提取。測試集則使用了人工標註的方式。訓練集總共有 353k 的三元組,測試集有 3880 個。此外,預定義的關係數量為 24 個。
評價方式
採用標準的精確率(Precision)和召回率(Recall)以及 F1 分數對結果進行評價。當三元組中的實體 1,實體 2,以及關係的抽取均正確才可記為 True。10% 的資料用於驗證集,且實驗進行了 10 次,結果取平均值和標準差。
超引數
詞嵌入使用 word2vec,詞嵌入向量是 300 維。論文對嵌入層進行了正則化,其 dropout 概率為 0.5。長短時編碼器的長短時神經元數量為 300,解碼器為 600。偏置函式的權重 α 為 10。
論文和其他三元組抽取方法進行了對比,包括多項管道方法,聯合抽取方法等。
實驗結果
表 1 為實體和實體關係抽取的表現結果,本論文正式方法名稱為“LSTM-LSTM-Bias”。表格前三項為管道方法,中間三項為聯合抽取方法。
▲ 表1. 實體和實體關係抽取結果
從實驗結果看出,論文提到的方法普遍優於管道方法和絕大多數聯合抽取方法。本論文另一個值得注意的地方是,論文提出的方法較好地平衡了精確率和召回率的關係,儘管在精確率指標上略低於 LSTM-CRF。
表 1 也說明深度學習方法對三元組結果的抽取基本上好於傳統方法。作者認為,這是因為深度學習方法在資訊抽取中普遍使用雙向長短時編碼器,可以較好地編碼語義資訊。
在不同深度學習的表現對比中,作者發現,LSTM-LSTM 方法好於 LSTM-CRF。論文認為,這可能是因為 LSTM 較 CRF 更好地捕捉了文字中實體的較長依賴關係。
分析和討論
錯誤分析
表 2 為深度學習方法對三元組各個元素的抽取效果對比,E1 表示實體 1 的抽取結果,E2 表示實體 2 的抽取結果,(E1,E2)表示實體的關係的抽取結果。
▲ 表2. 深度學習方法對三元組各元素抽取效果
表 2 說明,在對三元組實體的抽取中,對關係的抽取較三元組各個實體的抽取的精確率更好,但召回率更低。論文認為,這是由於有大量的實體抽取後未能組成合適的實體關係對。模型僅抽取了第一個實體 1,但未能找到合適的對應實體 2,或者僅有實體 2 被正確抽取出來。
此外,作者發現,表 2 的關係抽取結果比表 1 的結果提高了約 3%。作者認為,這是由於 3% 的結果預測錯誤是因為關係預測錯誤,而非實體預測錯誤導致的。
偏置損失分析
作者同時將論文方法和其他深度學習方法在識別單個實體(實體 1,實體 2)上的表現進行了對比。作者認為,雖然論文方法在識別單個實體上的表現低於其他方法,但能夠更好地識別關係。
▲ 表3. 單個實體識別結果
作者對比發現,當偏置項等於 10 時,F1 數值最高。因此建議偏置項設為 10。
▲ 表4. 偏置項(α)數值和各項表現指標的關係
結論
本文提出一種新型的標註方式,將傳統的命名實體識別和關係抽取任務聯合起來,使用端到端模型進行直接聯合資訊抽取。在和傳統方法以及深度學習方法的對比中均取得了滿意的成果。
考慮到目前論文設計的實體關係抽取僅限於單個的關係,無法對一句話中重合的多個實體關係進行抽取,論文作者考慮使用多分類器替換 softmax 層,以便對詞語進行多分類標註。