NLP-入門實體命名識別(NER)+Bilstm-CRF模型原理Pytorch程式碼詳解——最全攻略
最近在系統地接觸學習NER,但是發現這方面的小帖子還比較零散。所以我把學習的記錄放出來給大家作參考,其中匯聚了很多其他博主的知識,在本文中也放出了他們的原鏈。希望能夠以這篇文章為載體,幫助其他跟我一樣的學習者梳理、串起NER的各個小知識點,最後上手NER的主流模型(Bilstm+CRF)(文中講的是pytorch,但是懂了pytorch去看keras十分容易相信我哈)
全文結構:
一、NER資料(主要介紹NER)
二、主流模型Bilstm-CRF實現詳解(Pytorch篇)
三、實現程式碼的拓展(在第二點的基礎上進行拓展)
程式碼執行環境
電腦:聯想小新Air 13 pro
CPU:i5 ,4G執行記憶體
顯示卡:NVIDIA GeForce 940MX,2G視訊記憶體
系統:windows10 64位系統
軟體:Anaconda 5。3。0 python 3。6。6 Pytorch1。0
一、NER資料
參考:
NLP之CRF應用篇(序列標註任務)(CRF++的詳細解析、Bi-LSTM+CRF中CRF層的詳細解析、Bi-LSTM後加CRF的原因、CRF和Bi-LSTM+CRF最佳化目標的區別)
CRF++完成的是學習和解碼的過程:訓練即為學習的過程,預測即為解碼的過程。
參考:
Bilstm+crf中的crf詳解(這份資料對後面程式碼的理解是有幫助的)
參考:
BiLSTM-CRF中CRF層解析-2
在上一篇的參考中提到,會在每一句話的開始加上“START”,在句尾加上“END”,這點我們可能會有疑惑。
這篇參考給予瞭解答:
這是為了使轉移得分矩陣的魯棒性更好,才額外加兩個標籤:START和END,START表示一句話的開始,注意這不是指該句話的第一個單詞,START後才是第一個單詞,同樣的,END代表著這句話的結束。
下表就是一個轉移得分矩陣的示例,該示例包含了START和END標籤。
每一個格里的值表示的意思是:這個格的行值轉成列值的機率大小。打個比方:上圖中紅框(B-Person,I-person)的值為0。9,表示的意思就是B-person轉移至I-person的機率為0。9,這是合乎BIO標註的規定的(B是實體的開始,I是實體的內部)。類推一下,藍框的意思代表的就是B-Organization轉移至I-Organization的機率為0。8。
參考:
BiLSTM-CRF中CRF層解析-3(看完前面的參考來看這份,簡直不要太良心了,易懂很多)
但是前面很多概念有提到,就不贅述了,只是加深一下印象,順帶推一下這個博主對CRF的一系類解析
其中
Pi,yi
為
第 i 個位置 softmax 輸出為 yi 的機率
,
Ayi,yi+1
為
從 yi 到 yi+1 的轉移機率
,當tag(B-person,B-location……)個數為n的時候,轉移機率矩陣為(n+2)*(n+2),因為額外增加了一個開始位置和結束位置。這個得分函式S就很好地彌補了傳統BiLSTM的不足,因為我們當一個預測序列得分很高時,並不是各個位置都是softmax輸出最大機率值對應的label,還要考慮前面
轉移機率相加最大
,
即還要符合輸出規則(B後面不能再跟B)
,比如假設BiLSTM輸出的最有可能序列為BBIBIOOO,那麼因為我們的轉移機率矩陣中B->B的機率很小甚至為負,那麼根據s得分,這種序列不會得到最高的分數,即就不是我們想要的序列。
整個過程中需要訓練的引數為:
BiLSTM中的引數
轉移機率矩陣A
BiLSTM+CRF的預測:
作為預測結果輸出。
參考:
BiLSTM+crf的一些理解(也是很有幫助的資料,記錄如下)
model中由於CRF中有
轉移特徵
,即它會考慮輸出label之間的順序性,所以考慮用CRF去做BiLSTM的輸出層。
二、NER主流模型——Bilstm-CRF程式碼詳解部分(pytorch篇)
參考1:
ADVANCED: MAKING DYNAMIC DECISIONS AND THE BI-LSTM CRF(PyTorch關於BILSTM+CRF的tutorial)
從參考1中 找到 pytorch 關於 Bilstm-CRF 模型的tutorial,然後執行它,我這裡講一下幾個主體部分的作用(我是用jupyter notebook跑的,大家最好也跑完帶著疑惑往下看):
(定義函式)log_sum_exp:先做減法的原因在於,減去最大值可以避免e的指數次導致計算機上溢
訓練資料集的格式:list內為tuple,然後分字以及bio欄位
建立text欄位以及bio標籤對映成文字的索引,這一步是可替換的,因為是抽象對映為數字
建立BiLSTM_CRF model,及最佳化器
在該demo中建立model的四個引數
訓練300epoch,畫紅框的是核心。將text欄位及bio label轉換為對映的數字,輸入模型即可訓練
現在的很多NLP的網紅模型,無非是將文字到數字的對映建立的更合理。是可拓展的。
另外,這裡的模型訓練是適用 model。neg_log_likelihood() 。這是程式碼中建立好的 BiLSTM_CRF 類的一部分,弄明白需繼續看 model(參考:pytorch版的bilstm+crf實現sequence label,
有模型註解
)
torch。nn。Parameter():首先可以把這個函數理解為
型別轉換函式
,將一個不可訓練的型別Tensor轉換成可以訓練的型別parameter並將這個parameter繫結到這個module裡面(net。parameter()中就有這個繫結的parameter,所以在引數最佳化的時候可以進行最佳化的),所以經過型別轉換這個self。v變成了模型的一部分,成為了模型中根據訓練可以改動的引數了。使用這個函式的
目的
也是想讓某些變數在學習的過程中不斷的修改其值以達到最最佳化。(參考)【一句話解釋:就是希望它能夠梯度下降,學習最佳化】
(建立轉移矩陣A,並加了兩個我們不會變動的約束條件:1是我們不會從其他tag轉向start。2是不會從stop開始轉向其他。所以這些位置設為-1e4)
注意:轉移矩陣是隨機的,而且放入了網路中,是會更新的)(如果轉移矩陣A的概念不懂可以理解了轉移矩陣再回來看
即類似於將矩陣中start那一行及stop那一列添加了約束——self。transitions。data
forward_var
lstm層:經過了embedding,lstm,linear層,output為發射矩陣——emission matrix
核心部分,註解如圖
_forward_alg
feats。size() = torch。Size([7, 5])
參考2:
pytorch實現BiLSTM+CRF用於NER(命名實體識別)(提到了viterbi編碼,很有啟發!記錄如下)【統籌CRF演算法code,以及forward_score - gold_score 作為loss的根本原因】
CRF是判別模型, 判別公式如下 y 是標記序列,x 是單詞序列,即已知單詞序列,求最有可能的標記序列
Score(x, y) 即單詞序列 x 產生標記序列 y 的得分,得分越高,說明其產生的機率越大。
在pytorch的tutorial中,其用於實體識別定義的 Score(x,y) 包含兩個特徵函式,一個是
轉移特徵函式
,一個是
狀態特徵函式
程式碼中用到了
前向演算法
和
維特比演算法(viterbi)
log_sum_exp函式
就是計算
,
前向演算法(_forward_alg)
需要用到這個函式
前向演算法,求出α(alpha),即Z(x),也就是
,如果不懂可以看一下
李航的書
關於CRF的前向演算法
但是不同於李航書的是,程式碼中α都取了對數,一個是為了
運算方便
,一個為了後面的
最大似然估計
。
這個程式碼裡面沒有進行最佳化,作者也指出來了,
其實對feats的迭代完全沒有必要用兩次迴圈,其實矩陣相乘就夠了
,作者是為了方便我們理解,所以細化了步驟
維特比演算法(viterbi)
中規中矩,可以參考
李航書上條件隨機場的預測演算法
neg_log_likelihood函式
的作用:
我們知道forward_score是log Z(x),即
gold_score是
我們的目標是極大化
兩邊取對數即
所以我們需要極大化 gold_score - forward_score,也就是極小化 forward_score - gold_score。
這就是為什麼 forward_score - gold_score 可以作為loss的根本原因。
參考3:
Bi-LSTM-CRF for Sequence Labeling(記錄如下)
這篇跟參考2講的是一個意思。得分
score
表示為
也很清晰地提到了
CRF
的作用以及
score
中
P
和
A
矩陣分別代表的含義:P為Bi-LSTM的輸出矩陣;A為tag之間的轉移矩陣
根據畫紅線的去看上方score的定義
在許多參考文章中都有提到score的成分包含了兩部分,一個是Bilstm的輸出結果,另一個就是CRF的轉移矩陣,而轉移矩陣的作用就是去給標註結果一些約束。例如標註B的後面不能接B這種約束。這種約束是根據轉移矩陣A提供的。而轉移矩陣A是根據你提供的訓練集,訓練學習、梯度下降得到的。根據畫紅線的去看上方score的定義,就明白定義了每一種標註情況為一條路徑,使用score去計算該路徑的得分的意思了。再囉嗦一下:Ayi, yi+1是表達這個tagyi(標註yi)轉移至下一個tagyi+1(標註yi+1)的分數(機率)。而Pi,yi就是Bilstm的輸出矩陣,可以看到每個字對應到不同tag(標註)的分數。【不懂也沒關係,有很多文章都提到了。反覆看就會有感覺了】
CRF的機率函式
表示為
S(X,y)的計算很簡單,而
(下面記作
logsumexp
)的計算稍微複雜一些,因為
需要計算每一條可能路徑的分數
。這裡用一種簡便的方法,對於到詞的路徑,可以先把到詞的logsumexp計算出來,因為
因此先
計算每一步的路徑分數和直接計算全域性分數相同
,但這樣可以大大
減少計算的時間
。
參考4:
BiLSTM-CRF中CRF層解析-4(用程式的思想去理解怎麼計算所有路徑的得分和,巨良心)
這篇文章提到了動態規劃的程式設計思想,雖然跟pytorch的tutorial有些許偏差。但已經很到位了。卡在
_foward_alg函式
的同學多看幾遍這篇文章,先理解一下動態規劃的思路吧。會有幫助的。
參考5:
BiLSTM-CRF中CRF層解析-5(還是這個系列,講預測)
上一篇在講loss的一部分:所有路徑的得分和。現在講怎麼去解碼預測。大概的思路就是根據最高的得分去反哺這條路徑,使用較多的就是Viterbi解碼了。這篇文章就很詳細很詳細地提到了怎麼去解碼這個路徑,具體就直接進到博主的解析上看吧!致敬一下參考4和參考5的作者:勤勞的凌菲
參考6:
pytorch lstm crf 程式碼理解(走心的解讀,統籌程式碼塊的作用,其心得部分十分到位)
這裡就羅列一下作者的心得體會:
反向傳播不需要一定使用forward(),而且不需要定義loss=nn。MSError()等,直接score1 - score2 (
neg_log_likelihood函式
),就可以反向傳播了。
使用self。transitions = nn。Parameter(torch。randn(self。tagset_size, self。tagset_size)) 將想要更新的矩陣,放入到module的引數中,然後兩個矩陣無論怎麼操作,只要滿足 y = f(x, w),就能夠反向傳播
從程式碼看出每個迴圈裡只是去了轉移矩陣A的一行,或者就是一個值,進行操作,轉移矩陣就能夠更新。至於為什麼能夠更新,作者也不知道,這涉及到pytorch的機制。
發射矩陣(emit score)是 BiLSTM算出來的。轉移矩陣是單獨定義的,要學習的。初始矩陣是 [-1000,-1000,-1000,0,-1000],固定的。因為當加了開始符號後,第一個位置是開始符號的機率是100%。
顯式的加入了start標記,隱式的使用了end標記(總是最後多一步轉移到end)的分數
參考7:
PyTorch高階實戰教程: 基於BI-LSTM CRF實現命名實體識別和中文分詞
對這份pytorch NER tutorial,只需要將中文分詞的資料集預處理成作者提到的格式,即可很快的就遷移了這個程式碼到中文分詞中。但這種方式並不適合處理很多的資料(資料格式遷移問題),但是對於 demo 來說非常友好,把英文改成中文,標籤改成分詞問題中的 “BEMS” 就可以跑起來了。
參考資料:
pytorch中bilstm-crf部分code解析(也很良心了,作者畫了草圖幫助理解)
pytorch版的bilstm+crf實現sequence label(比較粗的註解)
三、模型程式碼拓展部分(pytorch篇)
前面我們介紹了很久pytorch實現NER任務的主流model——Bilstm+CRF,為了便於新手入門,所以還是稍微簡陋了一些。剛好看到有份資源是移植這個tutorial去實踐的,還是很有必要學習的
資料:
ChineseNER(中文NER、有tf和torch版,市面上Bilstm+CRF的torch code基本都是出自官方tutorial)(py2。7)
因為是py2的程式碼,所以是需要改成py3的。
訓練程式碼:
train_py3。py
資料集地址
但這個“Bosondata。pkl”是需要我們先到路徑“ChineseNER\data\boson”下執行“data_util。py”才生成的
生成“Bosondata。pkl”的位置
當然,原始碼也是存在python版本的問題(原始碼是py2的)例如:
報錯:
AttributeError: ‘str’ object has no attribute ‘decode’
解決方法:把
。decode(“*”)
那部分刪除即可
溯源:
https://www。
cnblogs。com/xiaodai0/p/
10564471。html
報錯:ImportError: No module named ‘compiler。ast’
解決方法:重新寫一個函式來替代
from compiler。ast import flatten
的flatten函式
import collections
def flatten(x):
result = []
for el in x:
if isinstance(x, collections。Iterable) and not isinstance(el, str):
result。extend(flatten(el))
else:
result。append(el)
return result
溯源:
https://
blog。csdn。net/w5688414/
article/details/78489277
當成功執行“data_util。py”生成“Bosondata。pkl”後,把“train_py3。py”裡面第38行的“word2id”修改為“id2word”(應該是作者打錯了),然後在程式碼路徑下創造資料夾“model”,就可以開始訓練了。
最後附上修改後的github原始碼
供參考借鑑,感謝大家。
下一篇:非遺在身邊—英歌舞