『深度應用』NLP命名實體識別(NER)開源實戰教程

  • 2019 年 10 月 3 日
  • 筆記

近幾年來,基於神經網路的深度學習方法在電腦視覺、語音識別等領域取得了巨大成功,另外在自然語言處理領域也取得了不少進展。在NLP的關鍵性基礎任務—命名實體識別(Named Entity Recognition,NER)的研究中,深度學習也獲得了不錯的效果。

開源地址:https://github.com/xiaosongshine/NLP_NER_RNN_Keras

目錄

0.概念講解

0.1 NER 簡介

0.2 深度學習方法在NER中的應用

 

2.編程實戰

2.1 概述

2.2數據預處理

2.3 模型搭建

2.4 模型訓練

2.5模型應用

3. 總結&待續

-1.參考


0.概念講解

 

0.1 NER 簡介

NER又稱作專名識別,是自然語言處理中的一項基礎任務,應用範圍非常廣泛。命名實體一般指的是文本中具有特定意義或者指代性強的實體,通常包括人名、地名、組織機構名、日期時間、專有名詞等。NER系統就是從非結構化的輸入文本中抽取出上述實體,並且可以按照業務需求識別出更多類別的實體,比如產品名稱、型號、價格等。因此實體這個概念可以很廣,只要是業務需要的特殊文本片段都可以稱為實體。

學術上NER所涉及的命名實體一般包括3大類(實體類,時間類,數字類)和7小類(人名、地名、組織機構名、時間、日期、貨幣、百分比)。

實際應用中,NER模型通常只要識別出人名、地名、組織機構名、日期時間即可,一些系統還會給出專有名詞結果(比如縮寫、會議名、產品名等)。貨幣、百分比等數字類實體可通過正則搞定。另外,在一些應用場景下會給出特定領域內的實體,如書名、歌曲名、期刊名等。

NER是NLP中一項基礎性關鍵任務。從自然語言處理的流程來看,NER可以看作詞法分析中未登錄詞識別的一種,是未登錄詞中數量最多、識別難度最大、對分詞效果影響最大問題。同時NER也是關係抽取、事件抽取、知識圖譜、機器翻譯、問答系統等諸多NLP任務的基礎。

NER當前並不算是一個大熱的研究課題,因為學術界部分學者認為這是一個已經解決的問題。當然也有學者認為這個問題還沒有得到很好地解決,原因主要有:命名實體識別只是在有限的文本類型(主要是新聞語料中)和實體類別(主要是人名、地名、組織機構名)中取得了不錯的效果;與其他資訊檢索領域相比,實體命名評測預料較小,容易產生過擬合;命名實體識別更側重高召回率,但在資訊檢索領域,高準確率更重要;通用的識別多種類型的命名實體的系統性能很差。

總結一下就是從語句中提取出關鍵名詞

0.2 深度學習方法在NER中的應用

NER一直是NLP領域中的研究熱點,從早期基於詞典和規則的方法,到傳統機器學習的方法,到近年來基於深度學習的方法,NER研究進展的大概趨勢大致如下圖所示。

圖1:NER發展趨勢

在基於機器學習的方法中,NER被當作序列標註問題。利用大規模語料來學習出標註模型,從而對句子的各個位置進行標註。NER 任務中的常用模型包括生成式模型HMM、判別式模型CRF等。條件隨機場(ConditionalRandom Field,CRF)是NER目前的主流模型。它的目標函數不僅考慮輸入的狀態特徵函數,而且還包含了標籤轉移特徵函數。在訓練時可以使用SGD學習模型參數。在已知模型時,給輸入序列求預測輸出序列即求使目標函數最大化的最優序列,是一個動態規劃問題,可以使用Viterbi演算法解碼來得到最優標籤序列。CRF的優點在於其為一個位置進行標註的過程中可以利用豐富的內部及上下文特徵資訊。圖2:一種線性鏈條件隨機場

近年來,隨著硬體計算能力的發展以及詞的分散式表示(word embedding)的提出,神經網路可以有效處理許多NLP任務。這類方法對於序列標註任務(如CWS、POS、NER)的處理方式是類似的:將token從離散one-hot表示映射到低維空間中成為稠密的embedding,隨後將句子的embedding序列輸入到RNN中,用神經網路自動提取特徵,Softmax來預測每個token的標籤。

這種方法使得模型的訓練成為一個端到端的過程,而非傳統的pipeline,不依賴於特徵工程,是一種數據驅動的方法,但網路種類繁多、對參數設置依賴大,模型可解釋性差。此外,這種方法的一個缺點是對每個token打標籤的過程是獨立的進行,不能直接利用上文已經預測的標籤(只能靠隱含狀態傳遞上文資訊),進而導致預測出的標籤序列可能是無效的,例如標籤I-PER後面是不可能緊跟著B-PER的,但Softmax不會利用到這個資訊。

學界提出了DL-CRF模型做序列標註。在神經網路的輸出層接入CRF層(重點是利用標籤轉移概率)來做句子級別的標籤預測,使得標註過程不再是對各個token獨立分類。

0.2.1  BiLSTM-CRF(RNN base)

LongShort Term Memory網路一般叫做LSTM,是RNN的一種特殊類型,可以學習長距離依賴資訊。LSTM由Hochreiter &Schmidhuber (1997)提出,並在近期被Alex Graves進行了改良和推廣。在很多問題上,LSTM 都取得了相當巨大的成功,並得到了廣泛的使用。LSTM 通過巧妙的設計來解決長距離依賴問題。
所有 RNN 都具有一種重複神經網路單元的鏈式形式。在標準的RNN中,這個重複的單元只有一個非常簡單的結構,例如一個tanh層。

圖3:傳統RNN結構

LSTM 同樣是這樣的結構,但是重複的單元擁有一個不同的結構。不同於普通RNN單元,這裡是有四個,以一種非常特殊的方式進行交互。

​圖4:LSTM結構

LSTM通過三個門結構(輸入門,遺忘門,輸出門),選擇性地遺忘部分歷史資訊,加入部分當前輸入資訊,最終整合到當前狀態併產生輸出狀態。

圖5:LSTM各個門控結構

應用於NER中的biLSTM-CRF模型主要由Embedding層(主要有詞向量,字向量以及一些額外特徵),雙向LSTM層,以及最後的CRF層構成。實驗結果表明biLSTM-CRF已經達到或者超過了基於豐富特徵的CRF模型,成為目前基於深度學習的NER方法中的最主流模型。在特徵方面,該模型繼承了深度學習方法的優勢,無需特徵工程,使用詞向量以及字元向量就可以達到很好的效果,如果有高品質的詞典特徵,能夠進一步獲得提高。

圖6:biLSTM-CRF結構示意圖

0.2.2 IDCNN-CRF(CNN base)

對於序列標註來講,普通CNN有一個不足,就是卷積之後,末層神經元可能只是得到了原始輸入數據中一小塊的資訊。而對NER來講,整個輸入句子中每個字都有可能對當前位置的標註產生影響,即所謂的長距離依賴問題。為了覆蓋到全部的輸入資訊就需要加入更多的卷積層,導致層數越來越深,參數越來越多。而為了防止過擬合又要加入更多的Dropout之類的正則化,帶來更多的超參數,整個模型變得龐大且難以訓練。因為CNN這樣的劣勢,對於大部分序列標註問題人們還是選擇biLSTM之類的網路結構,儘可能利用網路的記憶力記住全句的資訊來對當前字做標註。

但這又帶來另外一個問題,biLSTM本質是一個序列模型,在對GPU並行計算的利用上不如CNN那麼強大。如何能夠像CNN那樣給GPU提供一個火力全開的戰場,而又像LSTM這樣用簡單的結構記住儘可能多的輸入資訊呢?

Fisher Yu and Vladlen Koltun 2015 提出了dilated CNN模型,意思是“膨脹的”CNN。其想法並不複雜:正常CNN的filter,都是作用在輸入矩陣一片連續的區域上,不斷sliding做卷積。dilated CNN為這個filter增加了一個dilation width,作用在輸入矩陣的時候,會skip所有dilation width中間的輸入數據;而filter本身的大小保持不變,這樣filter獲取到了更廣闊的輸入矩陣上的數據,看上去就像是“膨脹”了一般。

具體使用時,dilated width會隨著層數的增加而指數增加。這樣隨著層數的增加,參數數量是線性增加的,而receptive field卻是指數增加的,可以很快覆蓋到全部的輸入數據。圖7:idcnn示意圖

圖7中可見感受域是以指數速率擴大的。原始感受域是位於中心點的1×1區域:

(a)圖中經由原始感受域按步長為1向外擴散,得到8個1×1的區域構成新的感受域,大小為3×3;

(b)圖中經過步長為2的擴散,上一步3×3的感受域擴展為為7×7;

(c)圖中經步長為4的擴散,原7×7的感受域擴大為15×15的感受域。每一層的參數數量是相互獨立的。感受域呈指數擴大,但參數數量呈線性增加。

對應在文本上,輸入是一個一維的向量,每個元素是一個character embedding:

圖8:一個最大膨脹步長為4的idcnn塊

IDCNN對輸入句子的每一個字生成一個logits,這裡就和biLSTM模型輸出logits完全一樣,加入CRF層,用Viterbi演算法解碼出標註結果。

CNN base方法利用空洞卷積+多層的方式實現提取整句的功能,同時也能實現並行計算加速(相較於RNN,CNN與RNN速度對比區別可以參考我之間博文,CNN RNN 並行理解)。

在biLSTM或者IDCNN這樣的網路模型末端接上CRF層是序列標註的一個很常見的方法。biLSTM或者IDCNN計算出的是每個詞的各標籤概率,而CRF層引入序列的轉移概率,最終計算出loss回饋回網路。

 

現在就剩一個問題了:什麼是CRF層?為什麼要用?

0.2.3 CRF層講解

接下來,簡明介紹一下該模型。
示意圖如下所示:

  • 首先,句子xxx中的每個單詞表達成一個向量,該向量包含了上述的word embedding和character embedding,其中character embedding隨機初始化,word embedding通常採用預訓練模型初始化。所有的embeddings 將在訓練過程中進行微調。
  • 其次,BiLSTM-CRF模型的的輸入是上述的embeddings,輸出是該句子xxx中每個單詞的預測標籤。

儘管,我們講的是CRF層,不必了解BiLSTM層的細節,但是為了便於了解CRF層,我們必須知道BiLSTM層輸出的意義。

從上圖可以看出,BiLSTM層的輸出是每個標籤的得分,如單詞w0w_0w0​,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針對每個單詞的輸出是標籤得分,對於每個單詞,我們可以選擇最高得分的標籤作為預測結果。
例如,對於w0w_0w0​,“B-Person”得分最高(1.5),因此我們可以選擇“B-Person”最為其預測標籤;同樣的,w1w_1w1​的標籤為”I-Person”,w2w_2w2​的為”O”, w3w_3w3​的標籤為”B-Organization”,w4w_4w4​的標籤為”O”。
按照上述方法,對於xxx雖然我們得到了正確的標籤,但是大多數情況下是不能獲得正確標籤的,例如下圖的例子:

顯然,輸出標籤“I-Organization I-Person” 和 “B-Organization I-Person”是不對的。

CRF能夠從訓練數據中學習到約束條件

CRF層可以對最終的約束標籤添加一些約束條件,從而保證預測標籤的有效性。而這些約束條件是CRF層自動從訓練數據中學到。
約束可能是:

  • 一句話中第一個單詞的標籤應該是“B-“ or “O”,而不能是”I-“;
  • “B-label1 I-label2 I-label3 I-…”中,label1, label2, label3 …應該是相同的命名實體標籤。如“B-Person I-Person”是有效的,而“B-Person I-Organization” 是無效的;
  • “O I-label” 是無效的。一個命名實體的第一個標籤應該以 “B-“ 開頭,而不能以“I-“開頭,換句話說, 應該是“O B-label”這種模式;

有了這些約束條件,無效的預測標籤序列將急劇減少。

CRF層就是加了約束使得輸出更加符合要求,同時也增加演算法成本,有些類似束搜索的功能,下面我們看一看CRF層具體如何工作的。

逐幀softmax #

CRF主要用於序列標註問題,可以簡單理解為是給序列中的每一幀都進行分類,既然是分類,很自然想到將這個序列用CNN或者RNN進行編碼後,接一個全連接層用softmax激活,如下圖所示

逐幀softmax並沒有直接考慮輸出的上下文關聯

逐幀softmax並沒有直接考慮輸出的上下文關聯

 

條件隨機場 #

然而,當我們設計標籤時,比如用s、b、m、e的4個標籤來做字標註法的分詞,目標輸出序列本身會帶有一些上下文關聯,比如s後面就不能接m和e,等等。逐標籤softmax並沒有考慮這種輸出層面的上下文關聯,所以它意味著把這些關聯放到了編碼層面,希望模型能自己學到這些內容,但有時候會“強模型所難”。

而CRF則更直接一點,它將輸出層面的關聯分離了出來,這使得模型在學習上更為“從容”:

CRF在輸出端顯式地考慮了上下文關聯

CRF在輸出端顯式地考慮了上下文關聯

 

數學 #

當然,如果僅僅是引入輸出的關聯,還不僅僅是CRF的全部,CRF的真正精巧的地方,是它以路徑為單位,考慮的是路徑的概率

模型概要 #

假如一個輸入有nn幀,每一幀的標籤有kk種可能性,那麼理論上就有knkn中不同的輸出。我們可以將它用如下的網路圖進行簡單的可視化。在下圖中,每個點代表一個標籤的可能性,點之間的連線表示標籤之間的關聯,而每一種標註結果,都對應著圖上的一條完整的路徑。

4tag分詞模型中輸出網路圖

4tag分詞模型中輸出網路圖

 

而在序列標註任務中,我們的正確答案是一般是唯一的。比如“今天天氣不錯”,如果對應的分詞結果是“今天/天氣/不/錯”,那麼目標輸出序列就是bebess,除此之外別的路徑都不符合要求。換言之,在序列標註任務中,我們的研究的基本單位應該是路徑,我們要做的事情,是從knkn條路徑選出正確的一條,那就意味著,如果將它視為一個分類問題,那麼將是knkn類中選一類的分類問題!

總結一下:CRF作用可以優化輸出實體之間的關聯

2.編程實戰

2.1 概述

  • 該實戰項目參考博文
  • 該項目使用了conll2003_v2數據集,其中標註的命名實體共計九類:
['O', 'B-LOC', 'B-PER', 'B-ORG', 'I-PER', 'I-ORG', 'B-MISC', 'I-LOC', 'I-MISC']

實現了將輸入識別為命名實體的模型,如下所示:

# input  ['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']

# output  ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

2.2數據預處理

數據下載並解壓,以供訓練,地址 http://files.deeppavlov.ai/deeppavlov_data/conll2003_v2.tar.gz

下載解壓後可以看到三個文件:test.txt,train.txt,valid.txt

打開後可以看到,數據格式如下:我們只需要每行開頭和最後一個數據,他們分別是文本資訊和命名實體。

-DOCSTART- -X- -X- O    EU		NNP B-NP B-ORG  rejects VBZ B-VP O  German 	JJ 	B-NP B-MISC  call 	NN 	I-NP O  to 		TO 	B-VP O  boycott VB 	I-VP O  British JJ 	B-NP B-MISC  lamb 	NN 	I-NP O  . 		. 	O 	 O  

數據讀取與預處理

我們需要將數據進行處理,使之成為網路能接收的形式。

讀取數據,並測試輸出

from tqdm import tqdm    class NerDatasetReader:      def read(self, data_path):          data_parts = ['train', 'valid', 'test']          extension = '.txt'          dataset = {}          for data_part in tqdm(data_parts):              file_path = data_path + data_part + extension              dataset[data_part] = self.read_file(str(file_path))          return dataset        def read_file(self, file_path):          fileobj = open(file_path, 'r', encoding='utf-8')          samples = []          tokens = []          tags = []            for content in fileobj:                content = content.strip('n')                if content == '-DOCSTART- -X- -X- O':                  pass              elif content == '':                  if len(tokens) != 0:                      samples.append((tokens, tags))                      tokens = []                      tags = []              else:                  contents = content.split(' ')                  tokens.append(contents[0])                  tags.append(contents[-1])          return samples    if __name__ == "__main__":      ds_rd = NerDatasetReader()      data1 = ds_rd.read("./conll2003_v2/")          for sample in data1['train'][:2]:          print(sample)          for token, tag in zip(*sample):              print('%st%s' % (token, tag))          print()

輸出結果

(['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'], ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O'])  EU      B-ORG  rejects O  German  B-MISC  call    O  to      O  boycott O  British B-MISC  lamb    O  .       O    (['Peter', 'Blackburn'], ['B-PER', 'I-PER'])  Peter   B-PER  Blackburn       I-PER

可以看出數據已經經過整理,每一句保存為了兩個list,一個是單詞list,另一個是標註list

但是這裡還有兩個問題:1.網路無法處理單詞級別數據,我們需要准換成數值表示 2.每個句子長度不同,無法統一訓練,需要歸一化

對於問題1.我們可以通過轉換為字典的方式來數值化。

def get_dicts(datas):      w_all_dict,n_all_dict = {},{}      for sample in datas:          for token, tag in zip(*sample):              if token not in w_all_dict.keys():                  w_all_dict[token] = 1              else:                  w_all_dict[token] += 1                if tag not in n_all_dict.keys():                  n_all_dict[tag] = 1              else:                  n_all_dict[tag] += 1        sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)      sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)      w_keys = [x for x,_ in sort_w_list[:15999]]      w_keys.insert(0,"UNK")        n_keys = [ x for x,_ in sort_n_list]      w_dict = { x:i for i,x in enumerate(w_keys) }      n_dict = { x:i for i,x in enumerate(n_keys) }      return(w_dict,n_dict)                if __name__ == "__main__":      ds_rd = NerDatasetReader()      data1 = ds_rd.read("./conll2003_v2/")        w_dict,n_dict = get_dicts(data1["train"])        print(len(w_dict),n_dict)

測試輸出結果

8000 {'O': 0, 'B-LOC': 1, 'B-PER': 2, 'B-ORG': 3, 'I-PER': 4, 'I-ORG': 5, 'B-MISC': 6, 'I-LOC': 7, 'I-MISC': 8}

我們保留前15999個常用的單詞,新增了一個”UNK”代表未知單詞。

下面我們就要將利用這些字典把單詞給替換為數值

def w2num(datas,w_dict,n_dict):      ret_datas = []      for sample in datas:          num_w_list,num_n_list = [],[]          for token, tag in zip(*sample):              if token not in w_dict.keys():                  token = "UNK"                if tag not in n_dict:                  tag = "O"                num_w_list.append(w_dict[token])              num_n_list.append(n_dict[tag])            ret_datas.append((num_w_list,num_n_list,len(num_n_list)))      return(ret_datas)        if __name__ == "__main__":      ds_rd = NerDatasetReader()      dataset = ds_rd.read("./conll2003_v2/")        w_dict,n_dict = get_dicts(dataset["train"])        data_num = {}      data_num["train"] = w2num(dataset["train"],w_dict,n_dict)      print(data_num["train"][:4])      print(dataset["train"][:4])

測試輸出結果,已經實現了數值化的要求。為了方便統計句子長度,每個元祖最後一位保存為了句子長度。

[([957, 11983, 233, 762, 6, 4147, 209, 6182, 1], [3, 0, 6, 0, 0, 0, 6, 0, 0], 9), ([732, 2068], [2, 4], 2), ([1379, 134], [1, 0], 2), ([18, 226, 455, 13, 12, 66, 35, 8127, 24, 233, 4148, 6, 2476, 6, 11984, 209, 6182, 407, 3542, 2069, 499, 1789, 1920, 651, 287, 39, 8128, 6, 1921, 1], [0, 3, 5, 0, 0, 0, 0, 0, 0, 6, 0,  0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 30)]  [(['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'], ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']), (['Peter', 'Blackburn'], ['B-PER', 'I-PER']), (['BRUSSELS', '1996-08-22'], ['B-LOC', 'O']), (['The', 'European', 'Commission', 'said', 'on', 'Thursday', 'it', 'disagreed', 'with', 'German', 'advice', 'to', 'consumers', 'to', 'shun', 'British', 'lamb', 'until', 'scientists', 'determine', 'whether', 'mad', 'cow', 'disease', 'can', 'be', 'transmitted', 'to', 'sheep', '.'], ['O', 'B-ORG', 'I-ORG', 'O', 'O', 'O', 'O', 'O', 'O', 'B-MISC', 'O', 'O', 'O', 'O', 'O', 'B-MISC', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O'])]

我們輸出句子長度的統計,發現最大值113,最小值為1,為了方便統一訓練,我們歸一化長度為80

data_num["train"] = w2num(dataset["train"],w_dict,n_dict)  w_lens = [data[-1] for data in  data_num["train"]]  print(max(w_lens),min(w_lens))  #out 113 1  

句子長度歸一化操作,這裡採用padding為0,就是當做“UNK”與“O”來用,其實也可以使用Mask方法等

def len_norm(data_num,lens=80):      ret_datas = []      for sample1 in list(data_num):          sample = list(sample1)          ls = sample[-1]          #print(sample)          while(ls<lens):              sample[0].append(0)              ls = len(sample[0])              sample[1].append(0)          else:              sample[0] = sample[0][:lens]              sample[1] = sample[1][:lens]            ret_datas.append(sample[:2])      return(ret_datas)        if __name__ == "__main__":      ds_rd = NerDatasetReader()      dataset = ds_rd.read("./conll2003_v2/")        w_dict,n_dict = get_dicts(dataset["train"])        data_num = {}      data_num["train"] = w2num(dataset["train"],w_dict,n_dict)        data_norm = {}      data_norm["train"] = len_norm(data_num["train"])        print(data_norm["train"][:4])

測試輸出結果為

[[[957, 11983, 233, 762, 6, 4147, 209, 6182, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [3, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[732, 2068, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[1379, 134, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], [[18, 226, 455, 13, 12, 66, 35, 8127, 24, 233, 4148, 6, 2476, 6, 11984, 209, 6182, 407, 3542, 2069, 499, 1789, 1920, 651, 287, 39, 8128, 6, 1921, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 3, 5, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]]

2.3 模型搭建

模型搭建採用了BiRNN方法,具體的說是BiLSTM,為了方便講解,採用的是RNN+Softmax方式,沒有用CRF,後面有時間我會更新一個CRF的版本。網路結構如下:

模型搭建程式碼

from tensorflow.keras.layers import *  from tensorflow.keras.models import *  from tensorflow.keras.optimizers  import *  def build_model(num_classes=9):      model = Sequential()      model.add(Embedding(16000, 256, input_length=80))      model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))      model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))      model.add(Dense(128, activation='relu'))      model.add(Dense(num_classes, activation='softmax'))      return(model)

輸出模型結構

Layer (type)                 Output Shape              Param #  =================================================================  embedding (Embedding)        (None, 80, 256)           4096000  _________________________________________________________________  bidirectional (Bidirectional (None, 80, 256)           394240  _________________________________________________________________  bidirectional_1 (Bidirection (None, 80, 256)           394240  _________________________________________________________________  dense (Dense)                (None, 80, 128)           32896  _________________________________________________________________  dense_1 (Dense)              (None, 80, 9)             1161  =================================================================  Total params: 4,918,537  Trainable params: 4,918,537  Non-trainable params: 0  _________________________________________________________________  None

2.4 模型訓練

train_data = np.array(data_norm["train"])  train_x = train_data[:,0,:]  train_y = train_data[:,1,:]      print(train_x.shape)    model.fit(x=train_x,y=train_y,epochs=5,batch_size=200,verbose=1,validation_split=0.1)  model.save("model.h5")

訓練10個epoch,MX150GPU耗時五分鐘,可以發現train_loss與val_loss都在下降

12636/12636 [==============================] - 68s 5ms/sample - loss: 0.3199 - val_loss: 0.1359  Epoch 2/5  12636/12636 [==============================] - 58s 5ms/sample - loss: 0.1274 - val_loss: 0.1201  Epoch 3/5  12636/12636 [==============================] - 63s 5ms/sample - loss: 0.1099 - val_loss: 0.0957  Epoch 4/5  12636/12636 [==============================] - 58s 5ms/sample - loss: 0.0681 - val_loss: 0.0601  Epoch 5/5  12636/12636 [==============================] - 63s 5ms/sample - loss: 0.0372 - val_loss: 0.0498

2.5模型應用

最終訓練10個epoch

model.load_weights("model.h5")  pre_y = model.predict(train_x[:4])    print(pre_y.shape)    pre_y = np.argmax(pre_y,axis=-1)    for i in range(0,len(train_y[0:4])):      print("label "+str(i),train_y[i])      print("pred  "+str(i),pre_y[i])

測試輸出結果,可以發現,預測前四個訓練集數據達到不錯的效果。

label 0 [3 0 6 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  pred 0 [3 0 6 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  label 1 [2 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  pred 1 [2 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  label 2 [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  pred 2 [1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  label 3 [0 3 5 0 0 0 0 0 0 6 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]  pred 3 [0 3 5 0 0 0 0 0 0 6 0 0 0 0 0 6 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   0 0 0 0 0 0]

3. 總結&待續

為了簡化,本文只用了RNN+Softmax方法進行了訓練集測試,可以改進地方還有很多,例如加入CRF,使用Mask方法,三個數據集都用到等,後面有時間就會進行更新。也歡迎大家一起交流,共同改進。

-1.參考

  1. https://www.jiqizhixin.com/articles/2018-08-31-2
  2. https://blog.csdn.net/suan2014/article/details/89419283
  3. https://spaces.ac.cn/archives/5542
  4. https://blog.csdn.net/chinatelecom08/article/details/82871376