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

  • 2019 年 10 月 7 日
  • 筆記

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

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

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

Part-1:RNN 基礎知識

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

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

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

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

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

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

  • 由於結構相對簡單,相比起LSTM,GRU 的計算速度更快;
  • 由於參數較少,在小樣本數據及上,GRU 的泛化效果更好;

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

Part-2:CRF 基礎知識

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

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

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

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

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

Part-3:建模思路

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

模型的結構也如上圖所示,我們首先用 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