摘要:構(gòu)建知識圖譜包含四個主要的步驟:數(shù)據(jù)獲取、知識抽取、知識融合和知識加工。其中最主要的步驟是知識抽取。知識抽取包括三個要素:命名實體識別(NER)、實體關(guān)系抽?。≧E) 和 屬性抽取。其中屬性抽取可以使用python爬蟲爬取百度百科、維基百科等網(wǎng)站,操作較為簡單,因此命名實體識別(NER)和實體關(guān)系抽取(RE)是知識抽取中非常重要的部分,同時其作為自然語言處理(NLP)中最遇到的問題一直以來是科研的研究方向之一。
本文將以深度學(xué)習(xí)的角度,對命名實體識別和關(guān)系抽取進(jìn)行分析,在閱讀本文之前,讀者需要了解深度神經(jīng)網(wǎng)絡(luò)的基本原理、知識圖譜的基本內(nèi)容以及關(guān)于循環(huán)神經(jīng)網(wǎng)絡(luò)的模型。
本文的主要結(jié)構(gòu)如下,首先引入知識抽取的相關(guān)概念;其次對詞向量(word2vec)做分析;然后詳細(xì)講解循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)、長短期記憶神經(jīng)網(wǎng)絡(luò)(LSTM)、門控神經(jīng)單元模型(GRU);了解基于文本的卷積神經(jīng)網(wǎng)絡(luò)模型(Text-CNN);講解隱馬爾可夫模型(HMM)與條件隨機場等圖概率模型(CRF);詳細(xì)分析如何使用這些模型實現(xiàn)命名實體識別與關(guān)系抽取,詳細(xì)分析端到端模型(End-to-end/Joint);介紹注意力機制(Attention)及其NLP的應(yīng)用;隨后介紹知識抽取的應(yīng)用與挑戰(zhàn),最后給出TensorFlow源碼、推薦閱讀以及總結(jié)。本文基本總結(jié)了整個基于深度學(xué)習(xí)的NER與RC的實現(xiàn)過程以及相關(guān)技術(shù),篇幅會很長,請耐心閱讀:
卷積神經(jīng)網(wǎng)絡(luò)是神經(jīng)網(wǎng)絡(luò)的另一個演化體,其通常用于圖像處理、視頻處理中,在自然語言處理范圍內(nèi),常被用來進(jìn)行文本挖掘、情感分類中。因此基于文本類的卷積神經(jīng)網(wǎng)絡(luò)也廣泛的應(yīng)用在關(guān)系抽取任務(wù)中。
卷積神經(jīng)網(wǎng)絡(luò)主要通過卷積運算來實現(xiàn)對多維數(shù)據(jù)的處理,例如對一副圖像數(shù)據(jù),其像素為6x6,通過設(shè)計一個卷積核(或稱過濾器)filter來對該圖形數(shù)據(jù)進(jìn)行掃描,卷積核可以實現(xiàn)對數(shù)據(jù)的過濾,例如下面例子中的卷積核可以過濾出圖像中的垂直邊緣,也稱為垂直邊緣檢測器。
例如假設(shè)矩陣:
表示一個原始圖像類數(shù)據(jù),選擇卷積核(垂直邊緣檢測器):
然后從原始圖像左上方開始,一次向右、向下進(jìn)行掃描,掃描的窗口為該卷積核,每次掃描時,被掃描的9個數(shù)字分別與卷積核對應(yīng)的數(shù)字做乘積(element-wise),并求這9個數(shù)字的和。例如掃描的第一個窗口應(yīng)該為:
則最后生成新的矩陣:
以上的事例稱為矩陣的卷積運算。在卷積神經(jīng)網(wǎng)絡(luò)中,卷積運算包括如下幾個參數(shù):
(1)數(shù)據(jù)維度:即對當(dāng)前需要做卷積運算的矩陣的維度,通常為三維矩陣,維度為 $mnd4。
(2)卷積核維度:即卷積核的維度,記為 ,當(dāng)d為1時,為二維卷積核,通常對二維矩陣進(jìn)行卷積運算,當(dāng) 時為三維卷積核,對圖像類型數(shù)據(jù)進(jìn)行卷積運算。
(3)卷積步長stride:即卷積核所在窗口在輸入數(shù)據(jù)上每次滑動的步數(shù)。上面的事例中明顯步長為1。
(4)數(shù)據(jù)填充padding:在卷積操作中可以發(fā)現(xiàn)新生產(chǎn)的矩陣維度比原始矩陣維度變小,在一些卷積神經(jīng)網(wǎng)絡(luò)運算中,為了保證數(shù)據(jù)維度不變,設(shè)置padding=1,對原始矩陣擴充0。這種方式可以使得邊緣和角落的元素可以被多次卷積運算,也可以保證新生成的矩陣維度不變??梢杂嬎愠雒總€維度應(yīng)該向外擴充 或 。
卷積神經(jīng)網(wǎng)絡(luò)的基本結(jié)構(gòu)如圖所示:
其主要有輸入層、卷積層、池化層、全相連接層和輸出層組成,其中卷積層與池化層通常組合在一起,并在一個模型中循環(huán)多次出現(xiàn)。
(1)輸入層:輸入層主要是將數(shù)據(jù)輸入到模型中,數(shù)據(jù)通常可以是圖像數(shù)據(jù)(像素寬像素高三原色(3)),也可以是經(jīng)過處理后的多維數(shù)組矩陣。
(2)卷積層:卷積層主要是對當(dāng)前的輸入數(shù)據(jù)(矩陣)進(jìn)行卷積運算,9.1節(jié)簡單介紹了卷積運算,下面對卷積運算進(jìn)行符號化表示:
設(shè)當(dāng)前的輸入數(shù)據(jù)為圖像 ,其中 表示 的寬, 表示 的高, 表示 的層數(shù)。
設(shè)卷積神經(jīng)網(wǎng)絡(luò)有2次卷積和池化操作(如上圖),兩次卷積操作分別有 個卷積核 (每個卷積核各不相同,不同的卷積核可以對原始圖像數(shù)據(jù)進(jìn)行不同方面的特征提取),每個卷積核的維度設(shè)為 (卷積核維度一般取奇數(shù)個,使得卷積核可以以正中間的元素位置中心對稱,卷積核的層數(shù)需要與上一輪輸出的數(shù)據(jù)層數(shù)一致),因此對于第 次中的第 個卷積核的卷積操作即為:
其中 表示卷積后的矩陣的第p行第q列, 表示第 次卷積池化操作后的池化層值, 即為原始圖像數(shù)據(jù) 。
卷積運算后,將生成 個維度為 的新矩陣
Ps:公式是三層循環(huán)求和,在程序設(shè)計中可以使用矩陣的對應(yīng)位置求積。
**(3)池化層(polling)**:池化層作用是為了降低卷積運算后產(chǎn)生的數(shù)據(jù)維度,池化操作包括最大池化(max-polling)和平均池化(avg-polling)。
池化操作是一種特殊的卷積,其并不像卷積操作一樣逐個相乘,對于最大池化,是取當(dāng)前所在窗口所在的數(shù)據(jù)中最大的數(shù)據(jù);對于平均池化則是取當(dāng)前窗口所有值的平均值。池化層的窗口維度一般為2*2,窗口的滑動步長stride=2。例如對于9.1節(jié)中經(jīng)過卷積操作的矩陣的池化操作后應(yīng)為:
池化層可以通過提取出相對重要的特征來減少數(shù)據(jù)的維度(即減少數(shù)據(jù)量),即減少了后期的運算,同時可以防止過擬合。第 次卷積池化操作中,對第 個卷積核過濾生成的矩陣進(jìn)行最大或平均池化操作的符號表示如下:
池化操作后矩陣的數(shù)量不變,僅僅是維度變小了。
(4)全相連層:在多次卷積和池化運算后,將生成若干個小矩陣,通過concatenate操作,將其轉(zhuǎn)換為一維度的向量。例如有16個10*10的矩陣,其可以拼接成一個長度為400的向量。全相連層是通過構(gòu)建一個多層的BP神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu),通過每一層的權(quán)重矩陣和偏向的前向傳播,將其不斷的降維。全相連層的輸入神經(jīng)元個數(shù)即為拼接形成向量的個數(shù),隱含層個數(shù)及每層的神經(jīng)元數(shù)量可自定義,輸出層的神經(jīng)元個數(shù)通常為待分類的個數(shù)。
(5)softmax層:與傳統(tǒng)的神經(jīng)網(wǎng)絡(luò)和循環(huán)神經(jīng)網(wǎng)絡(luò)類似,最終的全相連層的輸出是每個類別的概率值,因此需要對其進(jìn)行softmax操作,則最大值所對應(yīng)的的類記為卷積神經(jīng)網(wǎng)絡(luò)的預(yù)測結(jié)果。
卷積神經(jīng)網(wǎng)絡(luò)的訓(xùn)練與BP神經(jīng)網(wǎng)絡(luò)一樣,選擇損失函數(shù) 刻畫模型的誤差程度,選擇最優(yōu)化模型(梯度下降法)進(jìn)行最小化損失函數(shù)。
卷積神經(jīng)網(wǎng)絡(luò)的超參數(shù)包括卷積池化的次數(shù) 、每次卷積池化操作中卷積核的個數(shù) 和其維度 、卷積操作中是否填充(padding取值0或1)、卷積操作中的步長stride、池化層的窗口大?。ㄒ话闳?)及池化類型(一般取max-polling)、全相連的層數(shù)及隱含層神經(jīng)元個數(shù)、激活函數(shù)和學(xué)習(xí)率 等。參數(shù)包括卷積層的每個卷積核的值、全相連層中權(quán)重矩陣和偏向。卷積神經(jīng)網(wǎng)絡(luò)即通過梯度下降法不斷對參數(shù)進(jìn)行調(diào)整,以最小化損失函數(shù)。
卷積神經(jīng)網(wǎng)絡(luò)的訓(xùn)練過程分為前向傳播和反向傳播,前向傳播時需要初始化相關(guān)的參數(shù)和超參數(shù),反向傳播則使用梯度下降法調(diào)參。梯度下降的優(yōu)化中也采用mini-batch法、采用Adam梯度下降策略試圖加速訓(xùn)練,添加正則項防止過擬合。
卷積神經(jīng)網(wǎng)絡(luò)已經(jīng)成功地廣泛應(yīng)用與圖像識別領(lǐng)域,而對于文本類數(shù)據(jù)近期通過論文形式被提出。在2014年,紐約大學(xué)的Yoon Kim發(fā)表的《Convolutional Neural Networks for Sentence Classification》一文開辟了CNN的另一個可應(yīng)用的領(lǐng)域——自然語言處理。
??Yoon提出的Text-CNN與第九節(jié)的卷積神經(jīng)網(wǎng)絡(luò)有一定區(qū)別,但設(shè)計思想是一樣的,都是通過設(shè)計一個窗口并與數(shù)據(jù)進(jìn)行計算。
基于文本的Text-CNN的輸入數(shù)據(jù)是預(yù)訓(xùn)練的詞向量word embeddings,詞向量可參考本文的第2節(jié)內(nèi)容。對于一句話中每個單詞均為 維的詞向量,因此對于長度為 的一句話則可用維度為 的矩陣 表示,如圖所示:
(1)卷積層: 對某一句話對應(yīng)的預(yù)訓(xùn)練的詞向量矩陣維度為 ,設(shè)計一個過濾器窗口 ,其維度為 ,其中 即為詞向量的長度, 表示窗口所含的單詞個數(shù)(Yoon 在實驗中設(shè)置了 的取值為2、3、4)。其次不斷地滑動該窗口,每次滑動一個位置時,完成如下的計算:
其中 為非線性激活函數(shù), 表示該句子中第 到 的單詞組成的詞向量矩陣, 表示兩個矩陣的對應(yīng)位乘積, 表示當(dāng)前窗口位置的取值。
??因此對于長度為 的句子,維度為 的過濾器窗口將可以產(chǎn)生 個值組成的集合:
(2)最大池化層:為了能夠提取出其中最大的特征,Yoon對其進(jìn)行max-over-time操作,即取出集合 中的最大值 。另外可以分析得到max-over-time操作還可以解決每句話長度不一致的問題。
Text-CNN的結(jié)構(gòu)如圖所示:
(3)全相連層:對于 個過濾器窗口,將產(chǎn)生 個值組成的向量 ,Text-CNN通過設(shè)置一個全相連層,將該向量映射為長度為 的向量, 即為待預(yù)測的類的個數(shù),設(shè)置softmax激活函數(shù)即可轉(zhuǎn)換為各個類的概率值。
Text-CNN的前向傳播即為上圖所示流程。反向傳播采用梯度下降法
(1)正則化防止過擬合:
對于 個過濾器窗口產(chǎn)生的向量 ,則輸出值為 ,為了防止過擬合,采用 dropout權(quán)重衰減法,表達(dá)式為:
其中 表示對應(yīng)位置相乘, 表示以概率 產(chǎn)生只含有0或1元素的矩陣, 則表示可能矩陣 中有一半的元素為1 。
在測試環(huán)境,則為了限制 范式的權(quán)重矩陣,設(shè)置 ,當(dāng) 時進(jìn)行梯度下降的調(diào)整。
Ps: 正則化權(quán)重衰減可參考:正則化方法:L1和L2 regularization、數(shù)據(jù)集擴增、dropout[1]
(2)超參數(shù)設(shè)置:Yoon設(shè)置了一系列超參數(shù)如下:
超參數(shù) | 取值 |
---|---|
window( h h h值) | 3/4/5 |
dropout( p p p值) | 0.5 |
l 2 l_2 l2( s s s值) | 3 |
mini-batch | 50 |
Text-CNN在句子分類方面有不錯的效果,而對于關(guān)系抽取問題,可以將其視為句子分類任務(wù)。
假設(shè)長度為 具有 個實體的句子 ,其中實體分別為 ,其中 , 表示第 個實體的長度。該句子內(nèi)所有實體將組成一個集合 。任意取集合 中的兩個實體作為一個組合 ,其中 ,并考察其是否具有關(guān)系,關(guān)系表示為 ,其取值為 , 表示關(guān)系類標(biāo)個數(shù),即預(yù)設(shè)的試題關(guān)系的種類,每一個關(guān)系對應(yīng)一個整數(shù),0則表示沒有關(guān)系。因此對于一個包含 個實體 的句子 將產(chǎn)生 個關(guān)系組合 。
是將一句話 中不同組合的實體及其之間的所有單詞組成一個新子句,并將其對應(yīng)的word embeddings作為輸入數(shù)據(jù)。子句 ,其對應(yīng)的關(guān)系類標(biāo)為 ,因此對于 將有 個子句組成的樣本集 。
對于該樣本,由于每個子句的長度不一致,因此需要進(jìn)行填充0方式使各個子句長度一致,然后將其喂給Text-CNN。Text-CNN采用mini-batch法進(jìn)行梯度下降,其訓(xùn)練過程可參考10.3節(jié)內(nèi)容。
Ps:在諸多論文中都有表示,兩個實體是否具有關(guān)系與其相對距x離有關(guān)系,而一般來說兩者的距離超過25(即兩個實體之間有超過25個單詞),則兩個實體具有關(guān)系的概率將會趨近于0,因此認(rèn)為實體在一個句子內(nèi)會產(chǎn)生關(guān)聯(lián),而不在同一個句子內(nèi)認(rèn)為不具有關(guān)聯(lián)性,當(dāng)然這種假設(shè)也存在一定的問題,但在實驗中影響不大。
卷積神經(jīng)網(wǎng)絡(luò)可以很好的對實體與實體關(guān)系進(jìn)行分類,而在自然語言處理中,通常會對句子進(jìn)行句法分析,通過不同語言的語法規(guī)則建立起模型——依存關(guān)系。基于依存關(guān)系的關(guān)系抽取是該模型的一個應(yīng)用方向,其主要是通過句法分析實現(xiàn),而不是通過深度模型自動挖掘。本文雖然主要是以深度學(xué)習(xí)角度分析,但傳統(tǒng)的“淺層態(tài)”模型也需要了解,以方便將其與深度模型進(jìn)行整合。
基于依存關(guān)系模型的關(guān)系抽取也叫做開放式實體關(guān)系抽取,解決關(guān)系抽取的思路是對一個句子的詞性進(jìn)行預(yù)處理,例如對于一句話“馬云在杭州創(chuàng)辦了阿里巴巴”。不同于之前所講的深度模型,詞性分析則是對該句話中每一個詞進(jìn)行預(yù)先標(biāo)注,例如“馬云”、“杭州”和“阿里巴巴”被標(biāo)記為名詞,“在”和“了”被標(biāo)記為介詞,“創(chuàng)辦”被標(biāo)記為動詞。所謂的依存關(guān)系則體現(xiàn)在不同詞性的詞之間存在著依存關(guān)系路徑?!霸凇蓖ǔ:竺娓孛簿褪敲~,“創(chuàng)辦”動詞前通常為名詞,而“在…創(chuàng)辦了”便是一個依存關(guān)系。因此依存關(guān)系即為不同詞性的詞之間的關(guān)系結(jié)構(gòu),下標(biāo)列出了關(guān)于中文的依存標(biāo)注:
例如對于下面一句話,句子開頭設(shè)置一個“ROOT”作為開始,句子結(jié)束則為句號“?!保来骊P(guān)系可以表示為下圖:
在傳統(tǒng)的實體識別中,是通過基于規(guī)則的詞性分析實現(xiàn)的,最簡單的是正則表達(dá)式匹配,其次是使用NLP詞性標(biāo)注工具。通常認(rèn)為名詞是實體,因此實體可以通過詞性標(biāo)注實現(xiàn)抽取。因為詞性對每一個詞進(jìn)行了標(biāo)注,自然根據(jù)語法規(guī)則可以構(gòu)建起上圖所示的依存關(guān)系。每一個詞根據(jù)語法規(guī)則構(gòu)建起一條關(guān)系路徑。所有路徑的最終起始點即為句子的核心(HED)。
對于關(guān)系抽取問題,基于依存關(guān)系的關(guān)系抽取模型中,關(guān)系詞并非是預(yù)先設(shè)置的類別,而是存在于當(dāng)前的句子中。例如“馬云在杭州創(chuàng)辦了阿里巴巴”,預(yù)定義的關(guān)系可能是“創(chuàng)始人”,而“創(chuàng)始人”一詞在句子中不存在,但是句中存在一個與其相似的詞“創(chuàng)辦”。因此在句法分析中,能夠提取出核心詞“創(chuàng)辦”,該詞前面有一個名詞“杭州”,而“杭州”前面有一個介詞“在”,因此“在杭州”是一個介賓短語,依存路徑被標(biāo)記為POB,所以“杭州”不是“創(chuàng)辦”的主語,自然是“馬云”?!皠?chuàng)辦”一詞后面是助詞“了”可以省略,再往后則是名稱“阿里巴巴”,因此“創(chuàng)辦阿里巴巴”為動賓關(guān)系VOB,與上面的“探索機制”一樣。因此可分析得到語義為“馬云創(chuàng)辦阿里巴巴”,核心詞“創(chuàng)辦”即為關(guān)系,“馬云”和阿里巴巴則是兩個實體。
??因此基于依存關(guān)系的關(guān)系抽取算法步驟如下:
??(1)獲取句子 x x x;
??(2)對句子 x x x 進(jìn)行詞性標(biāo)注;
??(3)構(gòu)建依存關(guān)系路徑,并依據(jù)依存標(biāo)注表對路徑進(jìn)行標(biāo)注;
??(4)提取核心詞;
??(5)構(gòu)建起動賓結(jié)構(gòu),以核心詞為關(guān)系尋找主語和賓語作為兩個實體。
可以發(fā)現(xiàn),基于依存關(guān)系不僅可以抽取關(guān)系,也可以提取出其對應(yīng)的實體。因此包括基于深度學(xué)習(xí)模型在內(nèi),一種端到端的聯(lián)合實體識別與關(guān)系抽取被提出。
參考博主寫的博文:基于監(jiān)督學(xué)習(xí)和遠(yuǎn)程監(jiān)督的神經(jīng)關(guān)系抽取[2]
基于注意力機制的模型目前有許多,本文暫提供兩種論文解讀,詳情博主兩篇論文解讀:
1.論文解讀:Attention Is All You Need[3]
2.論文解讀:Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification[4]
應(yīng)用注意力機制實現(xiàn)命名實體識別和關(guān)系抽?。ㄒ舶ㄆ渌匀徽Z言處理任務(wù))成為常用之計,由于篇幅過大,本章節(jié)決定另開辟博文講述。
在通過一系列算法實現(xiàn)對非結(jié)構(gòu)化數(shù)據(jù)進(jìn)行命名實體識別和關(guān)系抽取之后,按照知識圖譜中知識抽取的步驟將其存儲在數(shù)據(jù)庫中。通常使用的數(shù)據(jù)庫為關(guān)系數(shù)據(jù)庫,即滿足第一和第二范式的“表格型”存儲模型,但關(guān)系型數(shù)據(jù)庫不能夠很直接的處理具有關(guān)系類別的數(shù)據(jù),即若表達(dá)不同數(shù)據(jù)之間的關(guān)系需要構(gòu)建外鍵,并通過外連接方式進(jìn)行關(guān)聯(lián),這對于百萬級別數(shù)據(jù)來說,對查詢速度以及后期的數(shù)據(jù)更新與維護(hù)都是不利的,因此需要一個能夠體現(xiàn)不同實體關(guān)系的數(shù)據(jù)庫。因此引入圖數(shù)據(jù)庫概念。
圖數(shù)據(jù)庫包含的元素主要由結(jié)點、屬性、關(guān)系,如圖:
“馬云”和“阿里巴巴”為實體,對于圖數(shù)據(jù)庫中的結(jié)點,“馬云” 與“阿里巴巴”二者實體之間有一個定向關(guān)系“創(chuàng)辦”,對于于數(shù)據(jù)庫中的關(guān)系,每個實體結(jié)點都有各自的屬性,即圖數(shù)據(jù)庫中的屬性,因此構(gòu)建圖數(shù)據(jù)庫的關(guān)鍵三要素即為實體1,實體2和關(guān)系,即為三元組。
Ps:常用的圖數(shù)據(jù)庫有 Neo4j,NoSQL,Titan,OrientDB和 ArangoDB。關(guān)于圖數(shù)據(jù)庫的概念可參考:圖數(shù)據(jù)庫,數(shù)據(jù)庫中的“黑科技”[5],初識圖數(shù)據(jù)與圖數(shù)據(jù)庫[6]。
Neo4j是由java實現(xiàn)的開源NOSQL圖數(shù)據(jù)庫,數(shù)據(jù)庫分為關(guān)系型和非關(guān)系型兩種類型。其中非關(guān)系型又分為Graph(圖形),Document(文檔),Cloumn Family(列式),以及Key-Value Store(KV),這四種類型數(shù)據(jù)庫分別使用不同的數(shù)據(jù)結(jié)構(gòu)進(jìn)行存儲。因此它們所適用的場景也不盡相同。(引自Neo4j簡介[7])
在手動編輯Neo4j數(shù)據(jù)庫時,需要編寫CQL語句實現(xiàn)對數(shù)據(jù)庫的操作。Java在實現(xiàn)對Neo4j時也需要編寫CQL語句,因此在知識抽取后的保存工作,需要對CQL語句有所了解,下面是簡單的CQL語句:
1、創(chuàng)建
(1)創(chuàng)建單個標(biāo)簽到結(jié)點
CREATE(結(jié)點名稱:標(biāo)簽名)
(2)創(chuàng)建多個標(biāo)簽到結(jié)點:
CREATE(結(jié)點名稱:標(biāo)簽名1:標(biāo)簽名2:....)
(3)創(chuàng)建結(jié)點和屬性:
CREATE(結(jié)點名稱:標(biāo)簽名{屬性1名稱:屬性值1,屬性2名稱:屬性值2,....})
(4)MERGE創(chuàng)建結(jié)點和屬性(檢測是否存在結(jié)點和屬性一模一樣的,若存在則不創(chuàng)建,不存在則創(chuàng)建)
MERGE(結(jié)點名稱:標(biāo)簽名{屬性1名稱:屬性值1,屬性2名稱:屬性值2,....})
2、檢索:
(1)檢索一個結(jié)點
MATCH(結(jié)點名稱:標(biāo)簽名) RETURN 結(jié)點名稱
(2)檢索一個結(jié)點的某個或多個屬性
MATCH(結(jié)點名稱:標(biāo)簽名) RETURN 結(jié)點名稱.屬性名稱1,結(jié)點名稱.屬性名稱2,...
(3)使用WHERE關(guān)鍵字搜索
MATCH(結(jié)點名稱:標(biāo)簽名) WHERE 標(biāo)簽名.屬性名 關(guān)系運算符(=>OR等) 數(shù)值 RETURN 結(jié)點名稱.屬性名稱1,結(jié)點名稱.屬性名稱2,...
Ps:其中WHERE 標(biāo)簽名.屬性名 IS NOT NULL可以過濾掉不存在該屬性值的結(jié)點
Ps:WHERE 標(biāo)簽名.屬性名 IN [100,102] 表示篩選出該屬性值介于100和102之間
(4)使用ORDER BY進(jìn)行排序
MATCH(結(jié)點名稱:標(biāo)簽名) RETURN 結(jié)點名稱.屬性名1,... ORDER BY 結(jié)點名稱.屬性名1
(DESC表示逆序,ASC表示正序)
(5)使用UNION進(jìn)行聯(lián)合查詢(要求具有相同的列名稱和數(shù)據(jù)類型,過濾掉重復(fù)數(shù)據(jù))
MATCH(結(jié)點名稱1.標(biāo)簽名)
RETURN 結(jié)點名稱1.屬性名1 as 屬性名s1,結(jié)點名稱1.屬性名2 as 屬性名s2,..
UNION
MATCH(結(jié)點名稱2.標(biāo)簽名)
RETURN 結(jié)點名稱2.屬性名1 as 屬性名s1,結(jié)點名稱2.屬性名2 as 屬性名s2,..
Ps:若使用UNION ALL,則不過濾重復(fù)行,其他與UNION一致
(6)LIMIT限制返回記錄個數(shù)
MATCH(結(jié)點名稱:標(biāo)簽名稱)
RETURN 結(jié)點名稱.屬性名1,...
LIMIT 5(限制最多返回5條數(shù)據(jù))
(7)SKIP跳過前面的記錄顯示后面的
MATCH(結(jié)點名稱:標(biāo)簽名稱)
RETURN 結(jié)點名稱.屬性名1,...
SKIP 5(跳過前5條數(shù)據(jù)顯示第6個及以后)
3、接點與關(guān)系
(1)添加新結(jié)點并為關(guān)系創(chuàng)建標(biāo)簽:
CREATE(結(jié)點1名稱:標(biāo)簽名1)-[關(guān)系名稱:關(guān)系標(biāo)簽名]->(結(jié)點2名稱:標(biāo)簽名2)
(2)為現(xiàn)有的結(jié)點添加關(guān)系:
MATCH(結(jié)點1名稱:標(biāo)簽名1),(結(jié)點2名稱:標(biāo)簽名2)
WHERE 條件
CREATE(結(jié)點1名稱)-[關(guān)系名稱:關(guān)系標(biāo)簽名]->(結(jié)點2名稱)
(3)為現(xiàn)有結(jié)點添加/修改屬性
MATCH(結(jié)點名稱:標(biāo)簽名)
SET 結(jié)點名稱.新屬性名 = 值
RETURN 結(jié)點名稱
4、刪除
(1)刪除結(jié)點:
MATCH(結(jié)點名稱:標(biāo)簽名) DELETE 結(jié)點名稱
(2)刪除結(jié)點及關(guān)系
MATCH(結(jié)點1名稱:標(biāo)簽名1)-[關(guān)系名稱]->(結(jié)點2名稱:標(biāo)簽名2)
DELETE 結(jié)點1名稱,結(jié)點2名稱,關(guān)系名稱
(3)刪除結(jié)點的屬性
MATCH(結(jié)點名稱:標(biāo)簽名)
REMOVE 結(jié)點名稱.屬性名,...
RETURN 結(jié)點名稱
(4)刪除結(jié)點的某個標(biāo)簽
MATCH(結(jié)點名稱:標(biāo)簽名1)
REMOVE 結(jié)點名稱:標(biāo)簽名2
本文以及詳細(xì)的分析了實現(xiàn)命名實體識別與關(guān)系抽取的各種模型,本節(jié)將提供兩個項目程序,一個是基于Bi-LSTM和CRF的命名實體識別,另一個是基于Bi-LSTM-LSTM和Text-CNN的端到端模型的實體與關(guān)系的聯(lián)合抽取。
(1)main函數(shù),主要解決控制臺參數(shù)獲取、數(shù)據(jù)獲取以及調(diào)用模型:
import tensorflow as tf
import numpy as np
import os, argparse, time, random
from model import BiLSTM_CRF
from utils import str2bool, get_logger, get_entity
from data import read_corpus, read_dictionary, tag2label, random_embedding
## Tensorflow的Session運行環(huán)境
os.environ['CUDA_VISIBLE_DEVICES'] = '0'
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' # default: 0
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.gpu_options.per_process_gpu_memory_fraction = 0.2 # need ~700MB GPU memory
## 控制臺輸入的超參數(shù)
parser = argparse.ArgumentParser(description='BiLSTM-CRF for Chinese NER task')
parser.add_argument('--train_data', type=str, default='data_path', help='train data source')
parser.add_argument('--test_data', type=str, default='data_path', help='test data source')
parser.add_argument('--batch_size', type=int, default=64, help='#sample of each minibatch')
parser.add_argument('--epoch', type=int, default=10, help='#epoch of training')
parser.add_argument('--hidden_dim', type=int, default=300, help='#dim of hidden state')
parser.add_argument('--optimizer', type=str, default='Adam', help='Adam/Adadelta/Adagrad/RMSProp/Momentum/SGD')
parser.add_argument('--CRF', type=str2bool, default=True, help='use CRF at the top layer. if False, use Softmax')
parser.add_argument('--lr', type=float, default=0.001, help='learning rate')
parser.add_argument('--clip', type=float, default=5.0, help='gradient clipping')
parser.add_argument('--dropout', type=float, default=0.5, help='dropout keep_prob')
parser.add_argument('--update_embedding', type=str2bool, default=True, help='update embedding during training')
parser.add_argument('--pretrain_embedding', type=str, default='random', help='use pretrained char embedding or init it randomly')
parser.add_argument('--embedding_dim', type=int, default=300, help='random init char embedding_dim')
parser.add_argument('--shuffle', type=str2bool, default=True, help='shuffle training data before each epoch')
parser.add_argument('--mode', type=str, default='demo', help='train/test/demo')
parser.add_argument('--demo_model', type=str, default='1521112368', help='model for test and demo')
args = parser.parse_args()
## 獲取數(shù)據(jù)(word embeddings)
#word2id:為每一個不重復(fù)的字進(jìn)行編號,其中UNK為最后一位
word2id = read_dictionary(os.path.join('.', args.train_data, 'word2id.pkl'))
print('\n========word2id=========\n',word2id)
if args.pretrain_embedding == 'random':
#隨機生成詞嵌入矩陣(一共3905個字,默認(rèn)取300個特征,維度為3905*300)
embeddings = random_embedding(word2id, args.embedding_dim)
else:
embedding_path = 'pretrain_embedding.npy'
embeddings = np.array(np.load(embedding_path), dtype='float32')
print('\n=========embeddings==========\n',embeddings,'\ndim(embeddings)=',embeddings.shape)
## read corpus and get training data獲取
if args.mode != 'demo':
train_path = os.path.join('.', args.train_data, 'train_data')
test_path = os.path.join('.', args.test_data, 'test_data')
train_data = read_corpus(train_path)#讀取訓(xùn)練集
test_data = read_corpus(test_path); test_size = len(test_data)#讀取測試集
#print('\n==========train_data================\n',train_data)
#print('\n==========test_data================\n',test_data)
## paths setting創(chuàng)建相應(yīng)文件夾目錄
paths = {}
# 時間戳就是一個時間點,一般就是為了在同步更新的情況下提高效率之用。
#就比如一個文件,如果他沒有被更改,那么他的時間戳就不會改變,那么就沒有必要寫回,以提高效率,
#如果不論有沒有被更改都重新寫回的話,很顯然效率會有所下降。
timestamp = str(int(time.time())) if args.mode == 'train' else args.demo_model
#輸出路徑output_path路徑設(shè)置為data_path_save下的具體時間名字為文件名
output_path = os.path.join('.', args.train_data+'_save', timestamp)
if not os.path.exists(output_path): os.makedirs(output_path)
summary_path = os.path.join(output_path, 'summaries')
paths['summary_path'] = summary_path
if not os.path.exists(summary_path): os.makedirs(summary_path)
model_path = os.path.join(output_path, 'checkpoints/')
if not os.path.exists(model_path): os.makedirs(model_path)
ckpt_prefix = os.path.join(model_path, 'model')
paths['model_path'] = ckpt_prefix
result_path = os.path.join(output_path, 'results')
paths['result_path'] = result_path
if not os.path.exists(result_path): os.makedirs(result_path)
log_path = os.path.join(result_path, 'log.txt')
paths['log_path'] = log_path
get_logger(log_path).info(str(args))
## 調(diào)用模型進(jìn)行訓(xùn)練
if args.mode == 'train':
#創(chuàng)建對象model
model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
#創(chuàng)建結(jié)點,
model.build_graph()
## hyperparameters-tuning, split train/dev
# dev_data = train_data[:5000]; dev_size = len(dev_data)
# train_data = train_data[5000:]; train_size = len(train_data)
# print('train data: {0}\ndev data: {1}'.format(train_size, dev_size))
# model.train(train=train_data, dev=dev_data)
## train model on the whole training data
print('train data: {}'.format(len(train_data)))
model.train(train=train_data, dev=test_data) # use test_data as the dev_data to see overfitting phenomena
## 調(diào)用模型進(jìn)行測試
elif args.mode == 'test':
ckpt_file = tf.train.latest_checkpoint(model_path)
print(ckpt_file)
paths['model_path'] = ckpt_file
model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
model.build_graph()
print('test data: {}'.format(test_size))
model.test(test_data)
## 根據(jù)訓(xùn)練并測試好的模型進(jìn)行應(yīng)用
elif args.mode == 'demo':
ckpt_file = tf.train.latest_checkpoint(model_path)
print(ckpt_file)
paths['model_path'] = ckpt_file
model = BiLSTM_CRF(args, embeddings, tag2label, word2id, paths, config=config)
model.build_graph()
saver = tf.train.Saver()
with tf.Session(config=config) as sess:
saver.restore(sess, ckpt_file)
while(1):
print('Please input your sentence:')
demo_sent = input()
if demo_sent == '' or demo_sent.isspace():
print('bye!')
break
else:
demo_sent = list(demo_sent.strip())
demo_data = [(demo_sent, ['O'] * len(demo_sent))]
tag = model.demo_one(sess, demo_data)
PER, LOC, ORG = get_entity(tag, demo_sent)
print('PER: {}\nLOC: {}\nORG: {}'.format(PER, LOC, ORG))
(2)數(shù)據(jù)處理module,處理數(shù)據(jù),包括對數(shù)據(jù)進(jìn)行編號,詞向量獲取以及標(biāo)注信息的處理等:
import sys, pickle, os, random
import numpy as np
## tags, BIO
tag2label = {'O': 0,
'B-PER': 1, 'I-PER': 2,
'B-LOC': 3, 'I-LOC': 4,
'B-ORG': 5, 'I-ORG': 6
}
#輸入train_data文件的路徑,讀取訓(xùn)練集的語料,輸出train_data
def read_corpus(corpus_path):
'''
read corpus and return the list of samples
:param corpus_path:
:return: data
'''
data = []
with open(corpus_path, encoding='utf-8') as fr:
lines = fr.readlines()
sent_, tag_ = [], []
for line in lines:
if line != '\n':
[char, label] = line.strip().split()
sent_.append(char)
tag_.append(label)
else:
data.append((sent_, tag_))
sent_, tag_ = [], []
return data
#生成word2id序列化文件
def vocab_build(vocab_path, corpus_path, min_count):
'''
#建立詞匯表
:param vocab_path:
:param corpus_path:
:param min_count:
:return:
'''
#讀取數(shù)據(jù)(訓(xùn)練集或測試集)
#data格式:[(字,標(biāo)簽),...]
data = read_corpus(corpus_path)
word2id = {}
for sent_, tag_ in data:
for word in sent_:
if word.isdigit():
word = '<NUM>'
elif ('\u0041' <= word <='\u005a') or ('\u0061' <= word <='\u007a'):
word = '<ENG>'
if word not in word2id:
word2id[word] = [len(word2id)+1, 1]
else:
word2id[word][1] += 1
low_freq_words = []
for word, [word_id, word_freq] in word2id.items():
if word_freq < min_count and word != '<NUM>' and word != '<ENG>':
low_freq_words.append(word)
for word in low_freq_words:
del word2id[word]
new_id = 1
for word in word2id.keys():
word2id[word] = new_id
new_id += 1
word2id['<UNK>'] = new_id
word2id['<PAD>'] = 0
print(len(word2id))
#將任意對象進(jìn)行序列化保存
with open(vocab_path, 'wb') as fw:
pickle.dump(word2id, fw)
#將句子中每一個字轉(zhuǎn)換為id編號,例如['我','愛','中','國'] ==> ['453','7','3204','550']
def sentence2id(sent, word2id):
'''
:param sent:源句子
:param word2id:對應(yīng)的轉(zhuǎn)換表
:return:
'''
sentence_id = []
for word in sent:
if word.isdigit():
word = '<NUM>'
elif ('\u0041' <= word <= '\u005a') or ('\u0061' <= word <= '\u007a'):
word = '<ENG>'
if word not in word2id:
word = '<UNK>'
sentence_id.append(word2id[word])
return sentence_id
#讀取word2id文件
def read_dictionary(vocab_path):
'''
:param vocab_path:
:return:
'''
vocab_path = os.path.join(vocab_path)
#反序列化
with open(vocab_path, 'rb') as fr:
word2id = pickle.load(fr)
print('vocab_size:', len(word2id))
return word2id
#隨機嵌入
def random_embedding(vocab, embedding_dim):
'''
:param vocab:
:param embedding_dim:
:return:
'''
embedding_mat = np.random.uniform(-0.25, 0.25, (len(vocab), embedding_dim))
embedding_mat = np.float32(embedding_mat)
return embedding_mat
def pad_sequences(sequences, pad_mark=0):
'''
:param sequences:
:param pad_mark:
:return:
'''
max_len = max(map(lambda x : len(x), sequences))
seq_list, seq_len_list = [], []
for seq in sequences:
seq = list(seq)
seq_ = seq[:max_len] + [pad_mark] * max(max_len - len(seq), 0)
seq_list.append(seq_)
seq_len_list.append(min(len(seq), max_len))
return seq_list, seq_len_list
def batch_yield(data, batch_size, vocab, tag2label, shuffle=False):
'''
:param data:
:param batch_size:
:param vocab:
:param tag2label:
:param shuffle:隨機對列表data進(jìn)行排序
:return:
'''
#如果參數(shù)shuffle為true,則對data列表進(jìn)行隨機排序
if shuffle:
random.shuffle(data)
seqs, labels = [], []
for (sent_, tag_) in data:
#將句子轉(zhuǎn)換為編號組成的數(shù)字序列
sent_ = sentence2id(sent_, vocab)
#將標(biāo)簽序列轉(zhuǎn)換為數(shù)字序列
label_ = [tag2label[tag] for tag in tag_]
#一個句子就是一個樣本,當(dāng)句子數(shù)量等于預(yù)設(shè)的一批訓(xùn)練集數(shù)量,便輸出該樣本
if len(seqs) == batch_size:
yield seqs, labels
seqs, labels = [], []
seqs.append(sent_)
labels.append(label_)
if len(seqs) != 0:
yield seqs, labels
(3)二進(jìn)制讀寫
import os
def conlleval(label_predict, label_path, metric_path):
'''
:param label_predict:
:param label_path:
:param metric_path:
:return:
'''
eval_perl = './conlleval_rev.pl'
with open(label_path, 'w') as fw:
line = []
for sent_result in label_predict:
for char, tag, tag_ in sent_result:
tag = '0' if tag == 'O' else tag
char = char.encode('utf-8')
line.append('{} {} {}\n'.format(char, tag, tag_))
line.append('\n')
fw.writelines(line)
os.system('perl {} < {} > {}'.format(eval_perl, label_path, metric_path))
with open(metric_path) as fr:
metrics = [line.strip() for line in fr]
return metrics
(4)字符串處理,對數(shù)據(jù)類標(biāo)轉(zhuǎn)換,對已生成的標(biāo)注序列進(jìn)行實體提?。?/p>
import logging, sys, argparse
#將字符串轉(zhuǎn)換為布爾型
def str2bool(v):
# copy from StackOverflow
if v.lower() in ('yes', 'true', 't', 'y', '1'):
return True
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
return False
else:
raise argparse.ArgumentTypeError('Boolean value expected.')
#獲得實體(將標(biāo)簽序列找出相應(yīng)的標(biāo)簽組合并返回對應(yīng)的文字)
def get_entity(tag_seq, char_seq):
PER = get_PER_entity(tag_seq, char_seq)
LOC = get_LOC_entity(tag_seq, char_seq)
ORG = get_ORG_entity(tag_seq, char_seq)
return PER, LOC, ORG
def get_PER_entity(tag_seq, char_seq):
length = len(char_seq)
PER = []
for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
if tag == 'B-PER':
if 'per' in locals().keys():
PER.append(per)
del per
per = char
if i+1 == length:
PER.append(per)
if tag == 'I-PER':
per += char
if i+1 == length:
PER.append(per)
if tag not in ['I-PER', 'B-PER']:
if 'per' in locals().keys():
PER.append(per)
del per
continue
return PER
def get_LOC_entity(tag_seq, char_seq):
length = len(char_seq)
LOC = []
for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
if tag == 'B-LOC':
if 'loc' in locals().keys():
LOC.append(loc)
del loc
loc = char
if i+1 == length:
LOC.append(loc)
if tag == 'I-LOC':
loc += char
if i+1 == length:
LOC.append(loc)
if tag not in ['I-LOC', 'B-LOC']:
if 'loc' in locals().keys():
LOC.append(loc)
del loc
continue
return LOC
def get_ORG_entity(tag_seq, char_seq):
length = len(char_seq)
ORG = []
for i, (char, tag) in enumerate(zip(char_seq, tag_seq)):
if tag == 'B-ORG':
if 'org' in locals().keys():
ORG.append(org)
del org
org = char
if i+1 == length:
ORG.append(org)
if tag == 'I-ORG':
org += char
if i+1 == length:
ORG.append(org)
if tag not in ['I-ORG', 'B-ORG']:
if 'org' in locals().keys():
ORG.append(org)
del org
continue
return ORG
def get_logger(filename):
logger = logging.getLogger('logger')
logger.setLevel(logging.DEBUG)
logging.basicConfig(format='%(message)s', level=logging.DEBUG)
handler = logging.FileHandler(filename)
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s: %(message)s'))
logging.getLogger().addHandler(handler)
return logger
(5)Bi-LSTM+CRF模型:
import numpy as np
import os, time, sys
import tensorflow as tf
from tensorflow.contrib.rnn import LSTMCell
from tensorflow.contrib.crf import crf_log_likelihood
from tensorflow.contrib.crf import viterbi_decode
from data import pad_sequences, batch_yield
from utils import get_logger
from eval import conlleval
#batch_size:批大小,一次訓(xùn)練的樣本數(shù)量
#epoch:使用全部訓(xùn)練樣本訓(xùn)練的次數(shù)
#hidden_dim:隱藏維度
#embeddinds:詞嵌入矩陣(字?jǐn)?shù)量*特征數(shù)量)
class BiLSTM_CRF(object):
def __init__(self, args, embeddings, tag2label, vocab, paths, config):
self.batch_size = args.batch_size
self.epoch_num = args.epoch
self.hidden_dim = args.hidden_dim
self.embeddings = embeddings
self.CRF = args.CRF
self.update_embedding = args.update_embedding
self.dropout_keep_prob = args.dropout
self.optimizer = args.optimizer
self.lr = args.lr
self.clip_grad = args.clip
self.tag2label = tag2label
self.num_tags = len(tag2label)
self.vocab = vocab
self.shuffle = args.shuffle
self.model_path = paths['model_path']
self.summary_path = paths['summary_path']
self.logger = get_logger(paths['log_path'])
self.result_path = paths['result_path']
self.config = config
#創(chuàng)建結(jié)點
def build_graph(self):
self.add_placeholders()
self.lookup_layer_op()
self.biLSTM_layer_op()
self.softmax_pred_op()
self.loss_op()
self.trainstep_op()
self.init_op()
#placeholder相當(dāng)于定義了一個位置,這個位置中的數(shù)據(jù)在程序運行時再指定
def add_placeholders(self):
self.word_ids = tf.placeholder(tf.int32, shape=[None, None], name='word_ids')
self.labels = tf.placeholder(tf.int32, shape=[None, None], name='labels')
self.sequence_lengths = tf.placeholder(tf.int32, shape=[None], name='sequence_lengths')
self.dropout_pl = tf.placeholder(dtype=tf.float32, shape=[], name='dropout')
self.lr_pl = tf.placeholder(dtype=tf.float32, shape=[], name='lr')
def lookup_layer_op(self):
#新建變量
with tf.variable_scope('words'):
_word_embeddings = tf.Variable(self.embeddings,
dtype=tf.float32,
trainable=self.update_embedding,
name='_word_embeddings')
#尋找_word_embeddings矩陣中分別為words_ids中元素作為下標(biāo)的值
#提取出該句子每個字對應(yīng)的向量并組合起來
word_embeddings = tf.nn.embedding_lookup(params=_word_embeddings,
ids=self.word_ids,
name='word_embeddings')
#dropout函數(shù)是為了防止在訓(xùn)練中過擬合的操作,將訓(xùn)練輸出按一定規(guī)則進(jìn)行變換
self.word_embeddings = tf.nn.dropout(word_embeddings, self.dropout_pl)
print('========word_embeddings=============\n',word_embeddings)
#雙向LSTM網(wǎng)絡(luò)層輸出
def biLSTM_layer_op(self):
with tf.variable_scope('bi-lstm'):
cell_fw = LSTMCell(self.hidden_dim)#前向cell
cell_bw = LSTMCell(self.hidden_dim)#反向cell
#(output_fw_seq, output_bw_seq)是一個包含前向cell輸出tensor和后向cell輸出tensor組成的元組,_為包含了前向和后向最后的隱藏狀態(tài)的組成的元組
(output_fw_seq, output_bw_seq), _ = tf.nn.bidirectional_dynamic_rnn(
cell_fw=cell_fw,
cell_bw=cell_bw,
inputs=self.word_embeddings,
sequence_length=self.sequence_lengths,
dtype=tf.float32)
output = tf.concat([output_fw_seq, output_bw_seq], axis=-1)
output = tf.nn.dropout(output, self.dropout_pl)
with tf.variable_scope('proj'):
#權(quán)值矩陣
W = tf.get_variable(name='W',
shape=[2 * self.hidden_dim, self.num_tags],
initializer=tf.contrib.layers.xavier_initializer(),
dtype=tf.float32)
#閾值向量
b = tf.get_variable(name='b',
shape=[self.num_tags],
initializer=tf.zeros_initializer(),
dtype=tf.float32)
s = tf.shape(output)
output = tf.reshape(output, [-1, 2*self.hidden_dim])
#tf.matmul矩陣乘法
pred = tf.matmul(output, W) + b
self.logits = tf.reshape(pred, [-1, s[1], self.num_tags])
#損失函數(shù)
def loss_op(self):
#使用CRF的最大似然估計
if self.CRF:
#crf_log_likelihood作為損失函數(shù)
#inputs:unary potentials,就是每個標(biāo)簽的預(yù)測概率值
#tag_indices,這個就是真實的標(biāo)簽序列了
#sequence_lengths,一個樣本真實的序列長度,為了對齊長度會做些padding,但是可以把真實的長度放到這個參數(shù)里
#transition_params,轉(zhuǎn)移概率,可以沒有,沒有的話這個函數(shù)也會算出來
#輸出:log_likelihood:標(biāo)量;transition_params,轉(zhuǎn)移概率,如果輸入沒輸,它就自己算個給返回
#self.logits為雙向LSTM的輸出
log_likelihood, self.transition_params = crf_log_likelihood(inputs=self.logits,
tag_indices=self.labels,
sequence_lengths=self.sequence_lengths)
#tf.reduce_mean默認(rèn)對log_likelihood所有元素求平均
self.loss = -tf.reduce_mean(log_likelihood)
else:
#交差信息熵
losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=self.logits,
labels=self.labels)
mask = tf.sequence_mask(self.sequence_lengths)
losses = tf.boolean_mask(losses, mask)
self.loss = tf.reduce_mean(losses)
tf.summary.scalar('loss', self.loss)
def softmax_pred_op(self):
if not self.CRF:
self.labels_softmax_ = tf.argmax(self.logits, axis=-1)
self.labels_softmax_ = tf.cast(self.labels_softmax_, tf.int32)
def trainstep_op(self):
with tf.variable_scope('train_step'):
#global_step:Optional Variable to increment by one after the variables have been updated
self.global_step = tf.Variable(0, name='global_step', trainable=False)
#選擇優(yōu)化算法
if self.optimizer == 'Adam':
optim = tf.train.AdamOptimizer(learning_rate=self.lr_pl)
elif self.optimizer == 'Adadelta':
optim = tf.train.AdadeltaOptimizer(learning_rate=self.lr_pl)
elif self.optimizer == 'Adagrad':
optim = tf.train.AdagradOptimizer(learning_rate=self.lr_pl)
elif self.optimizer == 'RMSProp':
optim = tf.train.RMSPropOptimizer(learning_rate=self.lr_pl)
elif self.optimizer == 'Momentum':
optim = tf.train.MomentumOptimizer(learning_rate=self.lr_pl, momentum=0.9)
elif self.optimizer == 'SGD':
optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
else:
optim = tf.train.GradientDescentOptimizer(learning_rate=self.lr_pl)
#根據(jù)優(yōu)化算法模型計算損失函數(shù)梯度,返回梯度和變量列表
grads_and_vars = optim.compute_gradients(self.loss)
#tf.clip_by_value(A, min, max)指將列表A中元素壓縮在min和max之間,大于max或小于min的值改成max和min
#梯度修剪
grads_and_vars_clip = [[tf.clip_by_value(g, -self.clip_grad, self.clip_grad), v] for g, v in grads_and_vars]
#grads_and_vars_clip: compute_gradients()函數(shù)返回的(gradient, variable)對的列表并修剪后的
#global_step:Optional Variable to increment by one after the variables have been updated
self.train_op = optim.apply_gradients(grads_and_vars_clip, global_step=self.global_step)
def init_op(self):
self.init_op = tf.global_variables_initializer()
#顯示訓(xùn)練過程中的信息
def add_summary(self, sess):
'''
:param sess:
:return:
'''
self.merged = tf.summary.merge_all()
#指定一個文件用來保存圖。
self.file_writer = tf.summary.FileWriter(self.summary_path, sess.graph)
def train(self, train, dev):
'''
:param train:
:param dev:
:return:
'''
#創(chuàng)建保存模型對象
saver = tf.train.Saver(tf.global_variables())
with tf.Session(config=self.config) as sess:
sess.run(self.init_op)
self.add_summary(sess)
#循環(huán)訓(xùn)練epoch_num次
for epoch in range(self.epoch_num):
self.run_one_epoch(sess, train, dev, self.tag2label, epoch, saver)
def test(self, test):
saver = tf.train.Saver()
with tf.Session(config=self.config) as sess:
self.logger.info('=========== testing ===========')
saver.restore(sess, self.model_path)
label_list, seq_len_list = self.dev_one_epoch(sess, test)
self.evaluate(label_list, seq_len_list, test)
#demo
def demo_one(self, sess, sent):
'''
:param sess:
:param sent:
:return:
'''
label_list = []
#隨機將句子分批次,并遍歷這些批次,對每一批數(shù)據(jù)進(jìn)行預(yù)測
for seqs, labels in batch_yield(sent, self.batch_size, self.vocab, self.tag2label, shuffle=False):
#預(yù)測該批樣本,并返回相應(yīng)的標(biāo)簽數(shù)字序列
label_list_, _ = self.predict_one_batch(sess, seqs)
label_list.extend(label_list_)
label2tag = {}
for tag, label in self.tag2label.items():
label2tag[label] = tag if label != 0 else label
#根據(jù)標(biāo)簽對照表將數(shù)字序列轉(zhuǎn)換為文字標(biāo)簽序列
tag = [label2tag[label] for label in label_list[0]]
print('===mode.demo_one:','label_list=',label_list,',label2tag=',label2tag,',tag=',tag)
return tag
#訓(xùn)練一次
def run_one_epoch(self, sess, train, dev, tag2label, epoch, saver):
'''
:param sess:
:param train:訓(xùn)練集
:param dev:驗證集
:param tag2label:標(biāo)簽轉(zhuǎn)換字典
:param epoch:當(dāng)前訓(xùn)練的輪數(shù)
:param saver:保存的模型
:return:
'''
#訓(xùn)練批次數(shù)
num_batches = (len(train) + self.batch_size - 1) // self.batch_size
start_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
#隨機為每一批分配數(shù)據(jù)
batches = batch_yield(train, self.batch_size, self.vocab, self.tag2label, shuffle=self.shuffle)
#訓(xùn)練每一批訓(xùn)練集
for step, (seqs, labels) in enumerate(batches):
sys.stdout.write(' processing: {} batch / {} batches.'.format(step + 1, num_batches) + '\r')
step_num = epoch * num_batches + step + 1
feed_dict, _ = self.get_feed_dict(seqs, labels, self.lr, self.dropout_keep_prob)
_, loss_train, summary, step_num_ = sess.run([self.train_op, self.loss, self.merged, self.global_step],
feed_dict=feed_dict)
if step + 1 == 1 or (step + 1) % 300 == 0 or step + 1 == num_batches:
self.logger.info(
'{} epoch {}, step {}, loss: {:.4}, global_step: {}'.format(start_time, epoch + 1, step + 1,
loss_train, step_num))
self.file_writer.add_summary(summary, step_num)
#保存模型
if step + 1 == num_batches:
#保存模型數(shù)據(jù)
#第一個參數(shù)sess,這個就不用說了。第二個參數(shù)設(shè)定保存的路徑和名字,第三個參數(shù)將訓(xùn)練的次數(shù)作為后綴加入到模型名字中。
saver.save(sess, self.model_path, global_step=step_num)
self.logger.info('===========validation / test===========')
label_list_dev, seq_len_list_dev = self.dev_one_epoch(sess, dev)
#模型評估
self.evaluate(label_list_dev, seq_len_list_dev, dev, epoch)
def get_feed_dict(self, seqs, labels=None, lr=None, dropout=None):
'''
:param seqs:句子數(shù)字序列
:param labels:對應(yīng)句子的標(biāo)簽數(shù)字序列
:param lr:
:param dropout:
:return: feed_dict
'''
#獲取填充0后的句子序列樣本(使得每個句子填充0后長度一致),同時記錄每個句子實際長度
#例如:s = [['3','1','6','34','66','8'],['3','1','34','66','8'],['3','1','6','34']]
#返回為[['3', '1', '6', '34', '66', '8'], ['3', '1', '34', '66', '8', 0], ['3', '1', '6', '34', 0, 0]],長度為 [6, 5, 4]
word_ids, seq_len_list = pad_sequences(seqs, pad_mark=0)
feed_dict = {self.word_ids: word_ids,
self.sequence_lengths: seq_len_list}
if labels is not None:
#為標(biāo)簽序列填充0并返回每個句子標(biāo)簽的實際長度
labels_, _ = pad_sequences(labels, pad_mark=0)
feed_dict[self.labels] = labels_
if lr is not None:
feed_dict[self.lr_pl] = lr
if dropout is not None:
feed_dict[self.dropout_pl] = dropout
return feed_dict, seq_len_list
def dev_one_epoch(self, sess, dev):
'''
:param sess:
:param dev:
:return:
'''
label_list, seq_len_list = [], []
for seqs, labels in batch_yield(dev, self.batch_size, self.vocab, self.tag2label, shuffle=False):
label_list_, seq_len_list_ = self.predict_one_batch(sess, seqs)
label_list.extend(label_list_)
seq_len_list.extend(seq_len_list_)
return label_list, seq_len_list
#預(yù)測一批數(shù)據(jù)集
def predict_one_batch(self, sess, seqs):
'''
:param sess:
:param seqs:
:return: label_list
seq_len_list
'''
#將樣本進(jìn)行整理(填充0方式使得每句話長度一樣,并返回每句話實際長度)
feed_dict, seq_len_list = self.get_feed_dict(seqs, dropout=1.0)
#若使用CRF
if self.CRF:
logits, transition_params = sess.run([self.logits, self.transition_params],
feed_dict=feed_dict)
label_list = []
for logit, seq_len in zip(logits, seq_len_list):
viterbi_seq, _ = viterbi_decode(logit[:seq_len], transition_params)
label_list.append(viterbi_seq)
return label_list, seq_len_list
else:
label_list = sess.run(self.labels_softmax_, feed_dict=feed_dict)
return label_list, seq_len_list
def evaluate(self, label_list, seq_len_list, data, epoch=None):
'''
:param label_list:
:param seq_len_list:
:param data:
:param epoch:
:return:
'''
label2tag = {}
for tag, label in self.tag2label.items():
label2tag[label] = tag if label != 0 else label
model_predict = []
for label_, (sent, tag) in zip(label_list, data):
tag_ = [label2tag[label__] for label__ in label_]
sent_res = []
if len(label_) != len(sent):
print(sent)
print(len(label_))
print(tag)
for i in range(len(sent)):
sent_res.append([sent[i], tag[i], tag_[i]])
model_predict.append(sent_res)
epoch_num = str(epoch+1) if epoch != None else 'test'
label_path = os.path.join(self.result_path, 'label_' + epoch_num)
metric_path = os.path.join(self.result_path, 'result_metric_' + epoch_num)
for _ in conlleval(model_predict, label_path, metric_path):
self.logger.info(_)
非常能夠感謝你能夠通讀整片文章,本人將針對基于深度學(xué)習(xí)的命名實體識別與關(guān)系抽取為主題,推薦學(xué)習(xí)文獻(xiàn)或書籍。
(1)《統(tǒng)計學(xué)習(xí)方法》(李航)
??這本書是主要針對自然語言處理領(lǐng)域編寫的統(tǒng)計學(xué)習(xí)方法。統(tǒng)計學(xué)習(xí)方法又叫做統(tǒng)計機器學(xué)習(xí)。這本書很細(xì)致的講解了幾大典型的機器學(xué)習(xí)算法,同時針對每一種算法的模型進(jìn)行了相關(guān)推導(dǎo)。另外書中還細(xì)講了圖概率模型、隱馬爾可夫模型和條件隨機場,這對于理解命名實體識別非常有利。
(2)《機器學(xué)習(xí)》(周志華)
??南京大學(xué)人工智能實驗室的主任周志華在機器學(xué)習(xí)和大數(shù)據(jù)挖掘領(lǐng)域內(nèi)是國內(nèi)外知名的“大佬”,他出版的《機器學(xué)習(xí)》西瓜書已經(jīng)成為IT領(lǐng)域內(nèi)的暢銷書。本人閱讀了該書,認(rèn)為該書對數(shù)學(xué)理論要求非常高,因此這本書需要在對基本的機器學(xué)習(xí)知識有所了解之后才適合讀。因此較為合理的應(yīng)先閱讀《統(tǒng)計學(xué)習(xí)方法》后再閱讀《機器學(xué)習(xí)》。
(3)《深度學(xué)習(xí)》([美] 伊恩·古德費洛)
??《深度學(xué)習(xí)》是美國數(shù)學(xué)家伊恩·古德費洛所編寫,由人民郵電出版社出版。該書主要以深度學(xué)習(xí)為主,再簡單介紹了機器學(xué)習(xí)的知識后,對深度學(xué)習(xí)進(jìn)行了多角度的分析,并介紹了多層感知機BP神經(jīng)網(wǎng)絡(luò)、循環(huán)神經(jīng)網(wǎng)絡(luò)、卷積神經(jīng)網(wǎng)絡(luò)、計算圖等內(nèi)容,是深度學(xué)習(xí)領(lǐng)域內(nèi)非常權(quán)威的書籍。因此在學(xué)習(xí)完基本的機器學(xué)習(xí)內(nèi)容之后,學(xué)習(xí)深度學(xué)習(xí)是必要的。
(4)另外推薦幾本數(shù)學(xué)類書籍,對數(shù)學(xué)方面薄弱的或者沒有學(xué)習(xí)的讀者提供學(xué)習(xí)的方向:
《微積分》、《線性代數(shù)·同濟(jì)版》、《矩陣論·華科版》、《最優(yōu)化方法及其應(yīng)用》、《最優(yōu)控制》等。
本文章(專欄)講述了基于深度學(xué)習(xí)的命名實體識別與關(guān)系抽取,細(xì)致的介紹了相關(guān)概念、涉及的模型以及通過公式推導(dǎo)和例子來對模型進(jìn)行分析,最后給出源碼和項目實例。
本人在研究知識圖譜過程中,對知識圖譜的關(guān)鍵步驟——知識抽取花了大量的時間,研究了深度神經(jīng)網(wǎng)絡(luò)模型、循環(huán)神經(jīng)網(wǎng)絡(luò)模型,長期記憶模型,在了解相關(guān)模型后認(rèn)識到了編碼解碼問題,而神經(jīng)網(wǎng)絡(luò)充當(dāng)著編碼,因此解決解碼問題就用到了概率圖模型(當(dāng)然LSTM也可以解碼)。
命名實體識別問題可以被認(rèn)為是序列標(biāo)注問題,關(guān)系抽求可以被認(rèn)為是監(jiān)督分類問題。在2017年幾篇端到端模型的文章紛紛呈現(xiàn),打破了傳統(tǒng)的pipeline模型,在實驗效果上非常出色。幾種端到端模型包括Bi-LSTM+LSTM+Text-CNN,Bi-LSTM+LSTM,Bi-LSTM+Independency-Tree等,這些模型目的是在命名實體抽取的過程中一并將關(guān)系挖掘出來,因此在模型的運行效率上有很大的改善。
除了端到端模型,一種基于注意力機制的模型映入眼簾,在幾篇學(xué)術(shù)論文中聲稱其模型比端到端模型更優(yōu),在實驗中也得到了體現(xiàn)?;谧⒁饬C制模型還需要進(jìn)行深入的研究。
關(guān)于基于深度學(xué)習(xí)的知識抽取,本文便到此結(jié)束,今后會不斷根據(jù)學(xué)術(shù)論文的情況進(jìn)行更新或添加。
正則化方法:L1和L2 regularization、數(shù)據(jù)集擴增、dropout: https://blog.csdn.net/u012162613/article/details/44261657
[2]基于監(jiān)督學(xué)習(xí)和遠(yuǎn)程監(jiān)督的神經(jīng)關(guān)系抽取: https://blog.csdn.net/qq_36426650/article/details/103219167
[3]論文解讀:Attention Is All You Need: https://blog.csdn.net/qq_36426650/article/details/96449575
[4]論文解讀:Attention-Based Bidirectional Long Short-Term Memory Networks for Relation Classification: https://blog.csdn.net/qq_36426650/article/details/88207917
[5]圖數(shù)據(jù)庫,數(shù)據(jù)庫中的“黑科技”: http://www.sohu.com/a/220383268_784849
[6]初識圖數(shù)據(jù)與圖數(shù)據(jù)庫: https://blog.csdn.net/m0_38068229/article/details/80629702
[7]Neo4j簡介: https://www.cnblogs.com/atomicbomb/p/9897484.html
聯(lián)系客服