用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

PaddlePaddle發表於2019-09-23

命名實體識別(Named Entity Recognition,NER)是 NLP 幾個經典任務之一,通俗易懂的來說,就是從一段文字中抽取出需求的關鍵詞,如地名,人名等。

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF


如上圖所示,Google、IBM、Baidu 這些都是企業名、Chinese、U.S. 都是地名。就科學研究來說,命名實體是非常通用的技術,類似任務型對話中的槽位識別(Slot Filling)、基礎語言學中的語義角色標註(Semantic RoleLabelling)都變相地使用了命名實體識別的技術;而就工業應用而言,命名實體其實就是序列標註(SequentialTagging),是除分類外最值得信賴和應用最廣的技術,例如智慧客服、網路文字分析,關鍵詞提取等。

 

下面我們先帶您瞭解一些 Gated RNN 和 CRF 的背景知識,然後再教您一步一步用 飛槳(PaddlePaddle)實現一個命名實體任務。另外,我們採用經典的 CoNLL 資料集。


Part-1:RNN 基礎知識


迴圈神經網路(Recurrent Neural Networks,RNN)是有效建模有時序特徵輸入的方式。它的原理實際上非常簡單,可以被以下簡單的張量公式建模:

                                                

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

 

其中函式 f, g 是自定的,可以非線性,也可以就是簡單的線性變換,比較常用的是:

 

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

                                                       

雖然理論上 RNN 能建模無限長的序列,但因為很多數值計算(如梯度彌散、過擬合等)的原因致使RNN 實際能收容的長度很小。等等類似的原因催生了門機制。

 

大量實驗證明,基於門機制(Gate Mechanism)可以一定程度上緩解RNN 的梯度彌散、過擬合等問題。LSTM 是最廣為應用的 Gated RNN,它的結構如下:

 

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

 

如上圖所示,運算 (取值 -1 ~ 1)和 (Sigmoid,取值 0 – 1)表示控制濾過資訊的 “門”。網上關於這些門有很多解釋,可以參考這篇博文[1]。

 

除了 LSTM 外,GRU(Gated Recurrent Unit) 也是一種常用的 Gated RNN:

 

  • 由於結構相對簡單,相比起LSTM,GRU 的計算速度更快;

  • 由於引數較少,在小樣本資料及上,GRU 的泛化效果更好;

 

事實上,一些類似機器閱讀的任務要求高效計算,大家都會採用 GRU。甚至現在有很多工作開始為了效率而採用Transformer 的結構。可以參考這篇論文[2]。


Part-2:CRF 基礎知識


給定輸入 ,一般 RNN 模型輸出標註序列 的辦法就是簡單的貪心,在每個詞上做 argmax,忽略了類別之間的時序依存關係。

  

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF


線性鏈條件隨機場(Linear Chain Conditional Random Field),是基於馬爾科夫性建模時序序列的有效方法。演算法上可以利用損失  的函式特點做前向計算;用維特比演算法(實際上是動態規劃,因此比貪心解碼肯定好)做逆向解碼。


形式上,給定發射特徵(由 RNN 編碼器獲得)矩陣  和轉移(CRF 引數矩陣,需要在計算圖中被損失函式反向優化)矩陣T,可計算給定輸入輸出的匹配得分:


 

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF


其中  是輸入詞序列, 是預測的 label 序列。然後使以下目標最大化:


用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

 

以上就是 CRF 的核心原理。當然要實現一個 CRF,尤其是支援 batch 的 CRF,難度非常高,非常容易出BUG 或低效的問題。之前筆者用 Pytorch 時就非常不便,一方面手動實現不是特別方便,另一方面用擷取開原始碼介面不好用。然而飛槳就很棒,它原生的提供了CRF 的介面,同時支援損失函式計算和反向解碼等功能。


Part-3:建模思路


我們資料簡單來說就是一句話。目前比較流行建模序列標註的方法是 BIO 標註,其中B 表示 Begin,即標籤的起始;I 表示 In,即標籤的內部;O 表示other,即非標籤詞。如下面圖所示,低端的  表示輸入,頂端的輸出表示 BIO 標註。

 

用飛槳做命名實體識別,手把手教你實現經典模型 BiGRU + CRF

 

模型的結構也如上圖所示,我們首先用 Bi-GRU(忽略圖中的 LSTM) 迴圈編碼以獲取輸入序列的特徵,然後再用 CRF 優化解碼序列,從而達到比單用RNNs 更好的效果。


Part-4:飛槳實現


終於到了動手的部分。本節將會一步一步教您如何用飛槳實現 BiGRU + CRF 做序列標註。由於是demo,我們力求簡單,讓您能夠將精力放到最核心的地方!

 

# 匯入 PaddlePaddle 函式庫.
import paddle

from paddle importfluid
 

# 匯入內建的 CoNLL 資料集.
from paddle.datasetimport conll05


# 獲取資料集的內建字典資訊.
word_dict, _,label_dict = conll05.get_dict()


WORD_DIM = 32           # 超引數: 詞向量維度.

BATCH_SIZE = 10         # 訓練時 BATCH 大小.

EPOCH_NUM = 20          # 迭代輪數數目.

HIDDEN_DIM = 512        # 模型隱層大小.

LEARNING_RATE =1e-1    # 模型學習率大小.

 

# 設定輸入 word 和目標 label 的變數.
word =fluid.layers.data(name='word_data', shape=[1], dtype='int64', lod_level=1)

target =fluid.layers.data(name='target', shape=[1], dtype='int64', lod_level=1)

 

# 將詞用 embedding 表示並通過線性層.
embedding =fluid.layers.embedding(size=[len(word_dict), WORD_DIM], input=word,

                                 param_attr=fluid.ParamAttr(name="emb", trainable=False))

hidden_0 =fluid.layers.fc(input=embedding, size=HIDDEN_DIM, act="tanh")

rhidden_0 =fluid.layers.fc(input=embedding, size=HIDDEN_DIM, act="tanh")

 

# 用 RNNs 得到輸入的提取特徵並做變換.
hidden_1 =fluid.layers.dynamic_lstm(

    input=hidden_0, size=HIDDEN_DIM,

    gate_activation='sigmoid',

    candidate_activation='relu',

    cell_activation='sigmoid')

 

rhidden_1 =fluid.layers.dynamic_lstm(

    input=rhidden_0, size=HIDDEN_DIM,

    gate_activation='sigmoid',

    candidate_activation='relu',

    cell_activation='sigmoid')

 

feature_out =fluid.layers.concat([hidden_1, rhidden_1], axis=-1)

 

feature_out =fluid.layers.fc(input=hidden_1, size=len(label_dict), act='tanh')

 

# 呼叫內建 CRF 函式並做狀態轉換解碼.
crf_cost =fluid.layers.linear_chain_crf(

    input=feature_out, label=target,

    param_attr=fluid.ParamAttr(name='crfw',learning_rate=LEARNING_RATE))

avg_cost =fluid.layers.mean(crf_cost)

 

# 呼叫 SGD 優化函式並優化平均損失函式.
fluid.optimizer.SGD(learning_rate=LEARNING_RATE).minimize(avg_cost)

 

# 宣告 PaddlePaddle 的計算引擎.
place =fluid.CPUPlace()

exe =fluid.Executor(place)

main_program =fluid.default_main_program()

exe.run(fluid.default_startup_program())

 

# 由於是 DEMO 因此用測試集訓練模型.
feeder =fluid.DataFeeder(feed_list=[word, target], place=place)

shuffle_loader =paddle.reader.shuffle(paddle.dataset.conll05.test(), buf_size=8192)

train_data =paddle.batch(shuffle_loader, batch_size=BATCH_SIZE)

 

# 按 FOR 迴圈迭代訓練模型並列印損失.
batch_id = 0

for pass_id inrange(EPOCH_NUM):

    for data in train_data():

        data = [[d[0], d[-1]] for d in data]

        cost = exe.run(main_program,feed=feeder.feed(data), fetch_list=[avg_cost])

 

        if batch_id % 10 == 0:

            print("avg_cost:\t" +str(cost[0][0]))

        batch_id = batch_id + 1
 

相信經過本節您已經掌握了用飛槳實現一個經典序列標註模型的技術,我們們下期再會,謝謝您的關注,我們會持續更新~

 

想與更多的深度學習開發者交流,請加入飛槳官方QQ群:796771754。


如果您想詳細瞭解更多相關內容,請參閱以下文件。


官網地址:https://www.paddlepaddle.org.cn


更多詞法分析相關內容,可以參考專案地址:

https://github.com/PaddlePaddle/models/tree/v1.5.1/PaddleNLP/lexical_analysis



【Reference】

[1] https://colah.github.io/posts/2015-08-Understanding-LSTMs/

[2] https://arxiv.org/pdf/1804.09541.pdf


相關文章