經典論文復現 | 基於標註策略的實體和關係聯合抽取

過去幾年發表於各大 AI 頂會論文提出的 400 多種算法中,公開算法代碼的僅佔 6%,其中三分之一的論文做者分享了測試數據,約 54% 的分享包含「僞代碼」。這是今年 AAAI 會議上一個嚴峻的報告。 人工智能這個蓬勃發展的領域正面臨着實驗重現的危機,就像實驗重現問題過去十年來一直困擾着心理學、醫學以及其餘領域同樣。最根本的問題是研究人員一般不共享他們的源代碼。 git

可驗證的知識是科學的基礎,它事關理解。隨着人工智能領域的發展,打破不可復現性將是必要的。爲此,PaperWeekly 聯手百度 PaddlePaddle 共同發起了本次論文有獎復現,咱們但願和來自學界、工業界的研究者一塊兒接力,爲 AI 行業帶來良性循環。算法

做者丨戴一鳴json

學校丨清華海峽研究院網絡

研究方向丨天然語言處理架構

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

引言

筆者本次復現的是中科院自動化所發表於 ACL 2017 的經典文章——Joint Extraction of Entities and Relations Based on a Novel Tagging Schemeapp

640

對於實體和關係的聯合抽取一直是信息抽取中重要的任務。爲了解決這一問題,論文提出了一個新型的標註方式,能夠解決聯合信息抽取中的標註問題。隨後,基於這一標註方法,論文研究了不一樣的端到端模型,在不須要分開識別實體和關係的同時,直接抽取實體和實體之間的關係。框架

論文在使用了遠程監督製做的公開數據集上進行了實驗,結果說明這一標註策略較現有的管道和聯合學習方法。此外,論文所提出的端到端模型在公開數據集上取得了目前最好的效果。ide

論文復現代碼: 函數

http://aistudio.baidu.com/aistudio/#/projectdetail/26338學習

論文方法

論文提出了一個新型的標註方式,並設計了一個帶有偏置(Bias)目標函數的端到端模型,去聯合抽取實體和實體間的關係。

標註方式

圖 1 是一個如何將原始標註數據(實體+實體關係)轉換爲論文中提到的新型標註方式的示例。在數據中,每個詞彙會被賦予一個實體標籤,所以經過抽取和實體有關的詞語,構成實體。

640

 圖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)轉換爲詞嵌入的向量。

640

 公式1. 雙向長短時編碼器

公式 1 中的 i,f 和 o 分別爲 LSTM 模塊在 t 時刻的輸入門,遺忘門和輸出門。c 爲 LSTM 模塊的輸出,W 爲權重。對於當前時刻,其隱層向量640的結果取決於起義時刻的640,上一時刻的640,以及當前時刻的輸入詞語640

對於一句話,表示爲640。其中640是第 d 維度下在第 t 個詞彙的詞向量,n 則是句序列的長度。在通過了詞嵌入後,前向和後向的長短時神經網絡分佈接受數據輸入,前向則句子順序從前向後,後向則從後向前。

對於每個詞語向量(通過詞嵌入後)640, 前向長短時神經網絡層經過考慮語義信息,將640640的信息所有編碼,記爲640。一樣,後向長短時則爲640。編碼器最後將兩個層的輸入相接。

長短時解碼器

論文同時使用了長短時解碼器用於標註給定序列。解碼器在當前時刻的輸入爲來自雙向編碼器的隱層向量640,前一個預測的標籤的嵌入640,前一個時刻的神經元輸入640,以及前一時刻的隱層向量640。解碼器根據雙向長短時編碼器的輸出進行計算。解碼器的內部公式相似於公式 1

640

640

 公式2. 長短時解碼器

Softmax層

在解碼器後加入 softmax 層,預測該詞語的標籤。解碼器的內部結構相似於編碼器。

640

 公式3. softmax層

640爲 softmax 矩陣,640爲總標籤數,640爲預測標籤的向量。

640

 圖2. 網絡總體結構圖

偏置目標函數(Bias Objective Function)

640

 公式4. 訓練中激活函數使用RMSprop

|D| 是訓練集大小,640是句子640的長度,640是詞語 t 在640的標籤,640是歸一化的 tag 的機率。I(O) 是一個條件函數(switching function),用於區分 tag 爲「O」和不爲「O」的時候的損失。

640

 公式5. 條件函數

α 是偏置權重,該項越大,則帶關係的標籤對模型的影響越大。

import paddle.fluid as fluidimport paddle.v2 as paddlefrom paddle.fluid.initializer import NormalInitializerimport reimport math#coding='utf-8'import jsonimport numpy as npfrom paddle.v2.plot import Plotertrain_title = "Train cost"test_title = "Test cost"plot_cost = Ploter(train_title, test_title)step = 0#=============================================global parameters and hyperparameters==================================EMBEDDING = 300DROPOUT = 0.5LSTM_ENCODE = 300LSTM_DECODE = 600BIAS_ALPHA = 10VALIDATION_SIZE = 0.1TRAIN_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 = 1000BATCH_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_testdef 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_datadef 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_labeldef 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_listdef format_control(string):    str1 = re.sub(r'\r','',string)    str2 = re.sub(r'\n','',str1)    str3 = re.sub(r'\s*','',str2)    return str3def 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 lossdef 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_datadef 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)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」。表格前三項爲管道方法,中間三項爲聯合抽取方法。

640

 表1. 實體和實體關係抽取結果

從實驗結果看出,論文提到的方法廣泛優於管道方法和絕大多數聯合抽取方法。本論文另外一個值得注意的地方是,論文提出的方法較好地平衡了精確率和召回率的關係,儘管在精確率指標上略低於 LSTM-CRF。 

表 1 也說明深度學習方法對三元組結果的抽取基本上好於傳統方法。做者認爲,這是由於深度學習方法在信息抽取中廣泛使用雙向長短時編碼器,能夠較好地編碼語義信息。

在不一樣深度學習的表現對比中,做者發現,LSTM-LSTM 方法好於 LSTM-CRF。論文認爲,這多是由於 LSTM 較 CRF 更好地捕捉了文本中實體的較長依賴關係。

分析和討論

錯誤分析

表 2 爲深度學習方法對三元組各個元素的抽取效果對比,E1 表示實體 1 的抽取結果,E2 表示實體 2 的抽取結果,(E1,E2)表示實體的關係的抽取結果。

640

 表2. 深度學習方法對三元組各元素抽取效果

表 2 說明,在對三元組實體的抽取中,對關係的抽取較三元組各個實體的抽取的精確率更好,但召回率更低。論文認爲,這是因爲有大量的實體抽取後未能組成合適的實體關係對。模型僅抽取了第一個實體 1,但未能找到合適的對應實體 2,或者僅有實體 2 被正確抽取出來。

此外,做者發現,表 2 的關係抽取結果比表 1 的結果提升了約 3%。做者認爲,這是因爲 3% 的結果預測錯誤是由於關係預測錯誤,而非實體預測錯誤致使的。

偏置損失分析

做者同時將論文方法和其餘深度學習方法在識別單個實體(實體 1,實體 2)上的表現進行了對比。做者認爲,雖然論文方法在識別單個實體上的表現低於其餘方法,但可以更好地識別關係。

640

 表3. 單個實體識別結果

做者對比發現,當偏置項等於 10 時,F1 數值最高。所以建議偏置項設爲 10。

640

 表4. 偏置項(α)數值和各項表現指標的關係

結論

本文提出一種新型的標註方式,將傳統的命名實體識別和關係抽取任務聯合起來,使用端到端模型進行直接聯合信息抽取。在和傳統方法以及深度學習方法的對比中均取得了滿意的成果。

考慮到目前論文設計的實體關係抽取僅限於單個的關係,沒法對一句話中重合的多個實體關係進行抽取,論文做者考慮使用多分類器替換 softmax 層,以便對詞語進行多分類標註。

關於PaddlePaddle

使用 PaddlePaddle 進行工做大致上感受不錯,優勢主要有:

1. 構建模型的過程較爲順利 

PaddlePaddle 的官方文檔較爲清楚,大量的函數和 TensorFlow 主流框架對應,所以在尋找組件的時候能夠找到。

2. 運行速度快 

據瞭解,PaddlePaddle 底層優化較好,速度比 TensorFlow 快不少。

3. 對 GPU 的支持 

主流框架目前都支持了 GPU,PaddlePaddle 也一樣具備這一特性。

4. 動態圖架構 

在數據更加複雜的狀況下,動態圖的構建優點比靜態圖更爲明顯。PaddlePaddle 框架下的 fluid 版本甚至比 TensorFlow 的動態圖支持更領先。

固然,考慮到 PaddlePaddle 依然年輕,仍有很多問題須要進一步優化:

1. 在筆者使用的時候,仍然不支持 Python 3.x(2018 年 9 月)。聽說在 11 月份會開始支持 Python 3.x,正在期待中。

2. Debug 仍然困難。可能一方面是由於筆者使用了 AI studio 而非傳統的 IDE 進行項目,另外一方面是 PaddlePaddle 內部的優化問題,代碼出錯的時候,很難找到問題緣由。這一點和 TensorFlow 有點像——各類各樣的報錯。

接下來期待 PaddlePaddle 更加支持 TPU 和 NPU,並更好地增長對小型移動設備和物聯網系統的支持,使模型能夠無障礙部署。

下載安裝命令

## CPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/cpu paddlepaddle

## GPU版本安裝命令
pip install -f https://paddlepaddle.org.cn/pip/oschina/gpu paddlepaddle-gpu

640?

 


640?wx_fmt=png


 

本文同步分享在 博客「飛槳PaddlePaddle」(CSDN)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索