BiLSTM-CRF模型理解

適用任務

中文分詞、詞性標註、命名實體識別是天然語言理解中,基礎性的工做,同時也是很是重要的工做。php

在不少NLP的項目中,工做開始以前都要通過這三者中的一到多項工做的處理。python

在深度學習中,有一種模型能夠同時勝任這三種工做,並且效果還很不錯--那就是biLSTM_CRF。git

biLSTM,指的是雙向LSTM;CRF指的是條件隨機場。github

 

一些說明

以命名實體識別爲例,咱們規定在數據集中有兩類實體,人名和組織機構名稱。web

在數據集中總共有5類標籤:算法

B-Person (人名的開始部分)shell

I- Person (人名的中間部分)ruby

B-Organization (組織機構的開始部分)bash

I-Organization (組織機構的中間部分)app

O (非實體信息)

此外,假設x 是包含了5個單詞的一句話(w0,w1,w2,w3,w4)。

在句子x中[w0,w1]是人名,[w3]是組織機構名稱,其餘都是「O」。

 

BiLSTM-CRF 模型

先來簡要的介紹一下該模型。

以下圖所示:

首先,句中的每一個單詞是一條包含詞嵌入和字嵌入的詞向量,詞嵌入一般是事先訓練好的,字嵌入則是隨機初始化的。全部的嵌入都會隨着訓練的迭代過程被調整。

其次,BiLSTM-CRF的輸入是詞嵌入向量,輸出是每一個單詞對應的預測標籤

 

以下圖所示,BiLSTM層的輸入表示該單詞對應各個類別的分數。如W0,BiLSTM節點的輸出是1.5 (B-Person), 0.9 (I-Person), 0.1 (B-Organization), 0.08 (I-Organization) and 0.05 (O)。這些分數將會是CRF層的輸入。 全部的經BiLSTM層輸出的分數將做爲CRF層的輸入,類別序列中分數最高的類別就是咱們預測的最終結果。

 

 

 若是沒有CRF層會是什麼樣

 

即便沒有CRF層,咱們照樣能夠訓練一個基於BiLSTM的命名實體識別模型,以下圖所示。

由於BiLSTM模型的結果是單詞對應各種別的分數,咱們能夠選擇分數最高的類別做爲預測結果。如W0,「B-Person」的分數最高(1.5),那麼咱們能夠選定「B-Person」做爲預測結果。一樣的,w1是「I-Person」, w2是「O」,w3是 「B-Organization」 ,w4是 「O」。

儘管咱們在該例子中獲得了正確的結果,但實際狀況並不老是這樣:

 

顯然,此次的分類結果並不許確。

 

CRF層能夠學習到句子的約束條件

CRF層能夠加入一些約束來保證最終預測結果是有效的。這些約束能夠在訓練數據時被CRF層自動學習獲得。

可能的約束條件有:

  • 句子的開頭應該是「B-」或「O」,而不是「I-」。
  • 「B-label1 I-label2 I-label3…」,在該模式中,類別1,2,3應該是同一種實體類別。好比,「B-Person I-Person」 是正確的,而「B-Person I-Organization」則是錯誤的。
  • 「O I-label」是錯誤的,命名實體的開頭應該是「B-」而不是「I-」。

有了這些有用的約束,錯誤的預測序列將會大大減小。

 

CRF 層

CRF層中的損失函數包括兩種類型的分數,而理解這兩類分數的計算是理解CRF的關鍵。

1 Emission score

第一個類型的分數是發射分數(狀態分數)。這些狀態分數來自BiLSTM層的輸出,在這裏就是word預測爲某個標籤的機率。以下圖所示,w0被預測爲B-Person的分數是1.5.

 

 

爲方便起見,咱們給每一個類別一個索引,以下表所示:

 

Xiyj表明狀態分數,i是單詞的位置索引,yj是類別的索引。根據上表,

表示單詞w1被預測爲B−Organization的分數是0.1。

 

2 轉移分數

用tyiyj來表示轉移分數。例如,tB−Person,I−Person=0.9表示從類別B−Person→I−Person的分數是0.9。所以,有一個全部類別間的轉移分數矩陣。

爲了使轉移分數矩陣更具魯棒性,咱們加上START 和 END兩類標籤。START表明一個句子的開始(不是句子的第一個單詞),END表明一個句子的結束。

下表是加上START和END標籤的轉移分數矩陣。

如上表格所示,轉移矩陣已經學習到一些有用的約束條件:

  • 句子的第一個單詞應該是「B-」 或 「O」,而不是「I」。(從「START」->「I-Person 或 I-Organization」的轉移分數很低)
  • 「B-label1 I-label2 I-label3…」,在該模式中,類別1,2,3應該是同一種實體類別。好比,「B-Person I-Person」 是正確的,而「B-Person I-Organization」則是錯誤的。(「B-Organization」 -> 「I-Person」的分數很低)
  • 「O I-label」是錯誤的,命名實體的開頭應該是「B-」而不是「I-」。

 

要怎樣獲得這個轉移矩陣呢?

實際上,轉移矩陣是BiLSTM-CRF模型的一個參數。在訓練模型以前,你能夠隨機初始化轉移矩陣的分數。這些分數將隨着訓練的迭代過程被更新,換句話說,CRF層能夠本身學到這些約束條件。

 

CRF損失函數

CRF損失函數由兩部分組成,真實路徑的分數 和 全部路徑的總分數。真實路徑的分數應該是全部路徑中分數最高的。

例如,數據集中有以下幾種類別:

 

一個包含5個單詞的句子,可能的類別序列以下:

  • 1. START B-Person B-Person B-Person B-Person B-Person END
  • 2. START B-Person I-Person B-Person B-Person B-Person END
  • …..
  • 10. START B-Person I-Person O B-Organization O END
  • N. O O O O O O O

每種可能的路徑的分數爲Pi,共有N條路徑,則路徑的總分是

,e是常數e。

若是第十條路徑是真實路徑,也就是說第十條是正確預測結果,那麼第十條路徑的分數應該是全部可能路徑裏得分最高的

根據以下損失函數,在訓練過程當中,BiLSTM-CRF模型的參數值將隨着訓練過程的迭代不斷更新,使得真實路徑所佔的比值愈來愈大

如今的問題是:

  1. 怎麼定義路徑的分數?
  2. 怎麼計算全部路徑的總分?
  3. 當計算全部路徑總分時,是否須要列舉出全部可能的路徑?(答案是不須要

真實路徑分數

計算真實路徑分數,eSi,是很是容易的。

咱們先集中注意力來計算Si:

以「START B-Person I-Person O B-Organization O END」這條真實路徑來講:

句子中有5個單詞,w1,w2,w3,w4,w5,加上START和END 在句子的開始位置和結束位置,記爲,w0,w6

Si = EmissionScore + TransitionScore

這些分數來自BiLSTM層的輸出,至於x0,START 和x6,END ,則設爲0

這些分數來自於CRF層,將這兩類分數加和便可獲得Si 和 路徑分數eSi

 

全部路徑的總分

如何計算全部路徑的總分呢?以一個玩具的例子詳細講解。

Step 1

咱們定義的損失函數以下:

如今咱們把它變成對數損失函數:

訓練目標一般是最小化損失函數,加負號:

前面咱們已經很清楚如何計算真實路徑得分,如今咱們須要找到一個方法去計算

 

Step 2:回憶一下狀態分數 和 轉移分數

爲了簡化問題,假定我句子只有3個單詞組成:

X = [w0, w1 ,w2]

只有兩個類別:

LabelSet = {l1, l2}

狀態分數以下:

 

轉移矩陣以下:

 

Step 3:

目標是:

整個過程是一個分數的積聚過程。它的實現思想有點像動態規劃。首先,w0全部路徑的總分先被計算出來,而後,計算w0 -> w1的全部路徑的得分,最後計算w0 -> w1 -> w2的全部路徑的得分,也就是咱們須要的結果。

 

接下來,會看到兩個變量:obs和 previous。Previous存儲了以前步驟的結果,obs表明當前單詞所帶的信息

若是句子只有一個單詞,就沒有以前步驟的結果,因此Previous 是空。只能觀測到狀態分數 obs =【x01,x02】

W0 的全部路徑總分就是:

 

 

 

 

爲啥要擴展previous 和 obs 矩陣呢?由於這樣操做能夠是接下來的計算至關高效。

 

 

 

實際上,第二次迭代過程也就完成了。

 

發現了嗎,這其實就是咱們的目標,

 

 

 

讀到這邊,差很少就大功告成了。這一步,咱們再重複一次以前的步驟。

 

跟上一步驟同樣,用新的previous計算總分:

 

們最終獲得了咱們的目標,

,咱們的句子中共有3個單詞和兩個類別,因此共有8條路徑。

 

biLSTM_CRF模型在tensorflow中的實現。

運行環境

python 3.6
tensorflow 1.2
本文GITHUB 歡迎Star和Fork。
使用一樣方法,構造的中文分詞。中文分詞GITHUB

正文

1.數據預處理
2.模型構建
3.模型訓練與測試
4.模型驗證
5.總結

1.數據預處理

 

 

首先是將預測數據進行處理,轉成模型可以識別的數字。

 
數據原格式

數據是以列形式存儲,截圖翻轉了一下。

我從訓練文本中,抽取頻數在前5000的字,實際只抽取到了4830左右個字。加入'<PAD>','<UNK>','<NUM>',分別表示填充字符,未知字符,數字字符。一塊兒存入字典。

 
字典

標籤一樣也有對應的字典。

# 將tag轉換成數字
tag2label = {"O": 0, "B-PER": 1, "I-PER": 2, "B-LOC": 3, "I-LOC": 4, "B-ORG": 5, "I-ORG": 6}

 

 

依據字典與標籤字典,將文字與標籤分別轉成數字。第一行是文本,第二行是標籤。

 
文本與標籤

下一步是生成batch的操做。
生成batch後,須要對batch內句子padding到統一的長度,並計算每句的真實長度。

2.模型構建

採用雙向LSTM對序列進行處理,將輸出結果進行拼接。輸入shape[batch,seq_Length,hidden_dim],輸出shape[batch,seq_length,2*hidden_dim]。

with tf.name_scope('biLSTM'): cell_fw = tf.nn.rnn_cell.LSTMCell(pm.hidden_dim) cell_bw = tf.nn.rnn_cell.LSTMCell(pm.hidden_dim) outputs, outstates = tf.nn.bidirectional_dynamic_rnn(cell_fw=cell_fw, cell_bw=cell_bw,inputs=self.embedding, sequence_length=self.seq_length, dtype=tf.float32) outputs = tf.concat(outputs, 2)#將雙向RNN的結果進行拼接 #outputs三維張量,[batchsize,seq_length,2*hidden_dim] 

咱們從本文的第一幅圖中,能夠看出,整個biLSTM完整的輸出格式是[batch,seq_length,num_tag]。num_tag是標籤的數量,本實驗中是標籤數量是7。因此咱們須要一個全鏈接層,將輸出格式處理一下。

with tf.name_scope('output'): s = tf.shape(outputs) output = tf.reshape(outputs, [-1, 2*pm.hidden_dim]) output = tf.layers.dense(output, pm.num_tags) output = tf.contrib.layers.dropout(output, pm.keep_pro) self.logits = tf.reshape(output, [-1, s[1], pm.num_tags]) 

self.logits就是須要輸入CRF層中的數據。代碼的第三行,對output的變形,表示將[batch,seq_length,2hidden_dim]變成[batchseq_length,2*hidden_dim],最後處理時再變形爲[batch,seq_length,num_tag]。
下面就是CRF層的處理:

with tf.name_scope('crf'): log_likelihood, self.transition_params = crf_log_likelihood(inputs=self.logits, tag_indices=self.input_y, sequence_lengths=self.seq_length) # log_likelihood是對數似然函數,transition_params是轉移機率矩陣 #crf_log_likelihood{inputs:[batch_size,max_seq_length,num_tags], #tag_indices:[batchsize,max_seq_length], #sequence_lengths:[real_seq_length] #transition_params: A [num_tags, num_tags] transition matrix #log_likelihood: A scalar containing the log-likelihood of the given sequence of tag indices. 

這一步,是調用from tensorflow.contrib.crf import crf_log_likelihood函數,求最大似然函數,以及求轉移矩陣。最大似然函數前加上"-",能夠用梯度降低法求最小值;

with tf.name_scope('loss'): self.loss = tf.reduce_mean(-log_likelihood) #最大似然取負,使用梯度降低 

轉移矩陣能夠幫助維特比算法來求解最優標註序列。

def predict(self, sess, seqs): seq_pad, seq_length = process_seq(seqs) logits, transition_params = sess.run([self.logits, self.transition_params], feed_dict={self.input_x: seq_pad, self.seq_length: seq_length, self.keep_pro: 1.0}) label_ = [] for logit, length in zip(logits, seq_length): #logit 每一個子句的輸出值,length子句的真實長度,logit[:length]的真實輸出值 # 調用維特比算法求最優標註序列 viterbi_seq, _ = viterbi_decode(logit[:length], transition_params) label_.append(viterbi_seq) return label_ 

3.模型訓練與測試

 

 

訓練時,共進行12次迭代,每迭代4次,將訓練獲得的結果,保存到checkpoints;loss的狀況,保留到tensorboard中;每100個batch,輸出此時的訓練結果與測試結果。

 
模型訓練

模型的loss由最初在訓練集54.93降到2.29,在測試集上由47.45降到1.73。咱們看下,保存的模型在驗證集上的效果。

4.模型驗證

 

 

我從1998年的人民網的新聞素材中,隨機抽取了幾條語句。

 
模型驗證

ORG表示組織名詞,LOC表示地理名詞,PER表示人名。從驗證結果上看,模型在命名實體識別上,效果還能夠。

 

 

 

 

對句子的單詞詞性作預測

Step 1:BiLSTM-CRF模型獲得的發射分數和轉移分數

假定咱們的句子共3個單詞組成:

而且,咱們已經從咱們的模型中獲得了發射分數和轉移分數,以下:

 

 

轉移矩陣:

 

 

Step 2:開始預測

若是你熟悉Viterbi算法,理解這一步的知識點將會很是容易。固然,若是你不熟悉也無所謂,整個預測過程和以前求全部路徑總分的過程很是相似。我將逐步解釋清楚,咱們先從左到右的順序來運行預測算法。

 

 

你將會看到兩類變量:obs 和 previous。Previous存儲了上一個步驟的最終結果,obs表明當前單詞包含的信息(發射分數)。

Alpha0 是歷史最佳的分數 ,alpha1 是最佳分數所對應的類別索引。這兩類變量的詳細信息待會會作說明。先來看下面的圖片:你能夠把這兩類變量當作狗狗去森林裏玩耍時在路上作的標記,這些標記能夠幫助狗狗找到回家的路。

 

 

 

 

如今,咱們來觀測第一個單詞W0,很顯然,W0所對應的最佳預測類別是很是容易知道的。好比,若是

,顯然,最佳預測結果是l2。

 

 

 

 

看到這裏,你可能好奇這跟以前求全部路徑分數的算法沒什麼區別,別急,你立刻就會看到不一樣之處啦!

 

在下一次迭代前更改previous的值:max!

 

 

舉個例子,若是咱們的得分以下:

 

 

那麼咱們的previous應該是:

 

這是什麼意思呢?其實也就是說previous存儲的是當前單詞對應各種別的最佳路徑得分。W1被預測爲L1類別的最高分是0.5,路徑是L2->L1,W1被預測爲L2類別的最高分是0.4,路徑是L2->L2。

 

這邊,咱們有兩個變量來儲存歷史信息,alpha0 和 alpha1.

在本次迭代中,咱們將最佳分數存儲到alpha0 :

 

 

同時,最佳分數所對應的類別索引存儲到alpha1:

 

 

類別L1的索引是0,L2的索引是1,因此(1,1)=(L2,L2)。表示當前最佳分數0.5對應的路徑是L2->L1,最佳分數0.4對應的路徑是L2->L2。(1,1)能夠理解爲前一單詞分別對應的類別索引。

 

上面scores有錯誤,應該是0.5+x21+t11 等

 

更改previous的值:

 

 

假如咱們的得分是:

 

 

如今咱們的previous是:

 

 

如今,咱們選取previous[0] 和previous[1]中最大的分數做爲最佳路徑。也就是0.9對應的路徑是咱們的預測結果。

同時,每一個類別對應的最大得分添加到alpha0 和 alpha1中:

 

 

Step 3:根據最大得分找到最佳路徑

這是最後一步,alpha0 和 alpha1將被用來找到最佳路徑。

先看alpha0,alpha0中最後一個單詞對應的類別得分分別是0.8 和 0.9,那麼0.9對應的類別L2就是最佳預測結果。再看alpha1,L2對應的索引是0, 「0」表示以前一個單詞對應的類別是L1,因此W1-W2的最佳路徑是: L1->L2

接着往前推,alpha1=(1,1),咱們已經知道W1的預測結果是L1,對應的索引是0,(1,1)[0] = 1,因此W0對應的類別是L2。

因此咱們預測的最佳路徑是 L2-> L1 -> L2 。

相關文章
相關標籤/搜索