前言
本篇不是Deepdive入門教程,而是對其一些源碼細節進行了解讀,換句話說要深入到內部去看看其具體是怎麼做的,所以看本篇的前提是假設讀者已經大概清楚了deepdive的使用流程,如果不是很熟悉,或是第一次使用建議先去看一下入門教程。
本篇先是分析特徵方面的源碼,接着是實踐部分,即使用ltp替換默認的斯坦福NLP信息抽取部分進而可優化該部分到數秒內,最後簡單說一下其模型方面的問題以及其它補充
其實關於入門教程總結來說就是使用了postgresql數據庫來做存儲部分,NLP部分使用的是standford nlp。然後過程基本都遵循以下套路:
---------------------------------------------------------------------------------------------------------------------------------------
一 先在app.ddlog中定義我們想要輸出的結構化內容
二 再在app.ddlog中使用function定義相關的處理函數,其中over後面就是函數的形參,implementation 是指的真真的處理函數
可 以 是python 腳本,.sh等,但必須寫成是一個迭代器,這個腳本就可以根據需求自行寫啦
三 最後再在app.ddlog中調用上述函數,傳入參數
上面三步都是在app.ddlog中定義的,其實也很清楚就是定義了一個函數,包括了輸入輸出以及怎麼處理等等
四:上面只是定義好了,最後我們就是在命令窗口中運行上面過程了,很簡單:就是編譯和得到sentences
---------------------------------------------------------------------------------------------------------------------------------------
基本就是這麼個套路,因爲該部分不是本文重點,所以就不展開說了,更多的可以看:
Deepdive官網:http://deepdive.stanford.edu/
Deepdive 源碼:https://github.com/HazyResearch/deepdive
中文博客:這是一箇中文翻譯的博客和github,特別詳細
https://blog.csdn.net/cx943024256/article/details/79056726
https://github.com/theDoctor2013/DeepDive-tutorial/blob/master/Deepdive_new.md
關於ltp 編譯好的安裝包:
鏈接:https://pan.baidu.com/s/12uqQmz3x0QeaLKeZFQwT2Q
提取碼:bw6i
特徵
在熟悉了DeepDive 流程後,不知道有沒有注意到就是在特徵提取部分即extract_transaction_features.py腳本,其中最重要的是調用了ddlib庫的get_generic_features_relation產生了特徵,特徵的樣子大概是這樣:
我們這裏選取的這句話是文章id號 1201734457的第三句話,這一對命名實體集合index分別是【46,51】和【22,27】
最後一共爲這對命名實體結合提取了79個特徵
爲了下面便於分析結果,我們這裏把這句話找了出來:
同時通過transaction_candidate表我們找到其對應的兩個命名實體集合名即上面黃色的部分
可是上述得到的79個特徵具體含義到底是什麼呢?換句話說這些特徵是怎麼產生的呢?,這是本文想要解讀的,所以本部分就其展開說明。
先看一下其整體的輸入輸出:
該部分代碼主要就是調用了ddlib包,其有一些列屬性和方法
其中屬性定義在dd.py中
https://github.com/HazyResearch/deepdive/blob/master/ddlib/ddlib/dd.py
其中下面最常用的就是Word,Span,DepEdge
除此之外還會看到一個字典dictionaries,其裏面可以看成就是保存了關鍵字
方法的話也有很多,其中就用本部分最重要的get_generic_features_relation
https://github.com/HazyResearch/deepdive/blob/master/ddlib/ddlib/gen_feats.py
可以看到有很多,彆着急,大部分方法都是作爲一個子模塊被get_generic_features_relation調用的,現在我們就從get_generic_features_relation入手各個擊破吧,當然load_dictionary就是一個從文件中加載預知關鍵字的函數沒什麼可說的。
下面正式開始吧:每一部分的關鍵代碼會以紅框圈出
----------------------------------------------------------------------------------------------------------------------------------------------------------
經過一些前面的步驟,我們大概要提取這麼一對(圖中兩個橙色部分)的特徵,然後get_generic_features_relation首先將一句話分爲上述結構:span1和span2就是上述的一對命名實體集合(具體到本例子中就是一對公司),爲了方面我們下文統一稱這一對爲mentions
其中betw_span對應的文本就是紅色部分
Convering_sapn對應的就是黃色+紅色的部分
span1和span2分別對應的是“甘薯大有農業科技有限公司”和“甘肅天潤薯業有限責任公司”
然後其依次進行了如下九方面的特徵提取
一 是否是反轉的
當前兩個命名實體集合是否是前後順序,如果不是,會返回一個特徵IS_INVERTED字段
由於當前例子中第一個命名實體集合是從46開始的,而後面一個命名實體集合是從22開始的,所以應該返回一個倒序的標誌
對應的結果就是,否則就什麼也不用返回了,所以當兩個命名實體集合正好是前後順序的時候,結果是沒有返回的對應特徵顯示的
這也很好理解,文本特徵一個重要方面就是上下文,所以這種前後關係至關重要!
二 _get_seq_features
分別將betw_span這個窗口內的詞內容,詞根,命名實體,詞性輸出並分別加上其對應的SEQ前綴
這裏很簡單,我們來對應的看一下其提取的特徵結果:(就是文本紅色的部分)
三 _get_window_features
這個是以converving_span爲大小,分別取了begin左邊的大小爲3的窗口的詞根和命名實體,以及end右面的大小爲3的窗口的詞根和命名實體即下面黑色區域
然後分別在詞根和命名實體上對其左右進行many VS many組合即多對多組合
好了看一下對應的輸出吧
這裏所說的左面右面就是上述畫圖中的左右黑框,只不過這裏"有限責任公司"是後一個圖中後一個黃色框,“甘薯大有農業”是前一個黃色框,這裏需要注意一下,同理下面的輸出都應該注意該問題
這裏應該是2(詞根+命名實體)*3*3(多對多組合)個特徵
加上第一方面的特徵,目前爲止已經是4+2*3*3=22個特徵啦
四 _get_ngram_features
提取Betw_span內的N-grams
關於N-grams (https://www.cnblogs.com/jielongAI/p/10189907.html)
簡單來說這裏就是以窗口小於等於3爲一個整體 的單詞組合提取出來,比如
【我,是,中國,大家庭,中,的一員】
那麼輸出就是
我是
我是中國
是中國
是中國大家庭
,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
對應的輸出(文本中的紅色部分)
五 _get_dictionary_indicator_features
提取一對公司中的關鍵字(dictionaries中存在的詞根)
但是目前有兩個疑問:
在本文例子中dictionaries中是空,沒看到有什麼?難道該部分沒有提取嗎
另外一個問題就是待從匹配的部分是從哪裏開始的,看代碼是從sentences一開始匹配,難道不應該是匹配兩個命名實體集合嗎?
由於沒有加載關鍵字,所以沒有對應的輸出
六 _get_min_dep_path_features
提取一對公司的最小路徑的一些特徵,首先將這一對mentions(兩個命名實體集合)中的實體兩兩組合求出當前兩個實體的最短路徑,然後迭代比較直到找到全局最小路徑,最後就是輸出一些該路徑方面的信息
_get_min_dep_path方法就是對應的迭代過程
其中最關鍵的就是dd.dep_path_between_words,即找當前一對實體的最短路徑,我們來看看其是怎麼做的:
首先使用_path_to_root找到當前命名實體到最根節點上這一路上的詞語集合,這裏的根節點(dep_par)指的是我們通過句法結構挖缺得到的結果,一般句法結構結果包含兩部分即句法和父節點,這裏的dep_par就是結果中的父節點。
注意:從這裏可以看到ddlib裏面是將-1看爲根節點的,而我們使用一些NLP信息抽取包在做句法抽取的時候對應的根節點一般是以0爲根節點,所以爲了使用這裏ddlib包就得給我們抽取的出來的父節點減1,這也就是extract_transaction_features.py這裏減一的原因:
然後就是求兩者到根節點這一路上不相同的路徑,然後記錄下這些不相同的路徑的一些詳細信息,注意前後邊結構中word1和word2設的不一樣,而且最後進行了反轉
其實感覺這裏有點像LCA的影子,爲了更清晰的理解上面額邏輯,我們這裏還是畫一下圖簡單說一下:
黃色就是代碼中的common
然後反轉後相當於變爲3-》4,所以最後path的效果大概就是1-2-3-4
這樣就得到了一對命名實體的路徑,然後遍歷一對公司下所有命名實體的組合,找到最短的那條路徑對應的一路上邊的信息即main_path.
之後就好說了,就是返回這條邊的一些信息,主要從三方面來返回的
返回 label(句法結構,比如主謂什麼的)和詞根
只返回 label
也返回 label和詞根,只不過是對應的詞根要是在字典中出現的(即是關鍵字),那麼就給其加一個表示符"DICT_"
輸出結果:
由於這裏沒有預加載的關鍵字所以這裏沒有DICT_標示,即第三行和第一行的both一樣!
七 _get_substring_indices
這裏其實和五做了相同的事情,都是得到一些和最短路徑有關的特徵,五求的是一對mentions之間的最短路徑,而這裏求得是兩個命名實體之外的關鍵字部分分別和這一對mentions的最短路徑,其中_get_substring_indices是遍歷整個sentence,以小於等於3的滑動窗口得到一些列小短語,然後通過過濾掉落在兩個命名實體之間的部分,那麼最後剩下的就是兩個命名實體之外的部分了,同時判斷其中這段小短語中是否包含關鍵字,只有包含了關鍵字纔會進行:
其實說的再直白一點就是:五是求一對mentions之間的最短路徑特徵,六是求關鍵字和mentions之間的最短路徑特徵
所以最後特徵中帶有前綴KW_IND應該就是一對mentions之外的那些滑動窗口系列中含有的關鍵字。
最後得到的 kw_span就是一個個窗口,之後呢就沒什麼了,就是用kw_span去分別和那一對mentions即span1,span2做和五一樣的事情了對應的是三種返回(也具體可以回看五)
由於沒有預加載關鍵字,所以沒有對應的輸出。
八 提取大寫字母
這個很簡單啦,就是判斷這一對mentions的開頭詞是否大寫
對應的輸出是:
九 這對mentions的長度
這裏所謂的長度值的是以5爲單位來算,看看其有多少個長度爲5的單位,當然了5是get_generic_features_relation可調的一個參數
對應的輸出是:
----------------------------------------------------------------------------------------------------------------------------------------------------------
到這裏特徵的部分基本就講解完成了,更多的內容可以看(當然其中的大部分方法都以講解了)
https://github.com/HazyResearch/deepdive/blob/master/ddlib/ddlib/gen_feats.py
https://addons.mozilla.org/zh-CN/firefox/addon/pdf-saver-for-csdn-blog/
實踐
原demo中信息抽取部分是使用斯坦福包進行的,該過程很慢,需要幾個小時,爲此這裏考慮替換使用ltp包,兩種解決思路:
一種是先在外部使用ltp處理好數據即提取好信息,後續就是導入到數據庫中
上述方法實際上將文本抽取信息這部分脫離了deepdive框架,這樣就沒有了完整性,所以我們也可以將ltp信息抽取整合到deepdive框架下進行,這就是第二種方法。
下面介紹的是第一種,由於目前在linux中安裝ltp,只有在python3環境下安裝成功了,而deepdive中是基於python2的,所以如果採用第二種方法整合在一起的話,會出現一些問題,即在deepdive框架下使用python3腳本需要修改部分deepdive源碼,考慮到篇幅,第二種方法由另一篇博客介紹:
https://blog.csdn.net/weixin_42001089/article/details/91388707
開始第一種方法的解決
該部分代碼:https://github.com/Mryangkaitong/python-Machine-learning/tree/master/deepdive/demo_version_1
如果之前實驗過別的數據,這裏一定要初始化數據庫,即清空數據庫,重新開始
deepdive initdb
更多deepdive 可以使用help查看
deepdive help
一使用pyltp提取信息:
由於系統問題,articles.csv文件在linux和win 上面會出現一些符號上面的錯誤,這個需要注意,下面用到的articlesc.csv就出現了一部分符號錯誤,但不影響大局,實際使用的時候要注意
可以看到加載數據和ltp模型大概用了25秒
之後就是提取這50篇 文章的信息啦
僅僅用到大概15秒,注意這裏將一句話中提取的字段拼接成了一個字符串以&&&&隔開,另外具體到某一個字段比如句法父節點也是個列表,那麼這裏是以@@分隔,所以在解析的時候,相當於&&&&是一級分隔,分隔出各個字段的信息,@@是二級分隔,分隔出每個字段下面的具體信息。同時這裏去掉了兩個字段,如下:
第一個是詞根,對於這個例子來說,中文的詞根和tokens時一樣的,所以後續只要將tokens複製給lemmas即可,另外從上面特徵的分析部分也可以看到,根本是不需要doc_offsets(單詞偏移量)這個字段的,所以這裏也沒有提取該字段
注意:一最後的csv名字是sentences_nlp(這裏要和後面統一)
二csv不要保存列名,即header=0
二 新建sentences_nlp數據表,並導入
在app.ddlog中添加:
注意,表的列名即sentences_list可以順便起
但表名必須和csv保持一致
導入很簡單啦:
deepdive compile && deepdive do sentences_nlp
看一下是否導入成功:
deepdive query '?- sentences_nlp(sentences_list).'
三 轉化成sentences多個字段的信息
這部分就是將sentences_nlp表的數據結構轉化成如下數據結構,注意這裏去掉了doc_offects字段
需要修改的部分,首先是app.ddlog
可以看到這裏我們使用nlp_markup.py來處理,其實該python很簡單啦,就是split以一級分隔符&&&&和二級分隔符@@進行分隔即可,具體的可以看相關腳本文件。
好啦,開始運行:
deepdive compile && deepdive do sentences
看一下運行結果:
deepdive query '
doc_id, index, tokens, ner_tags | 5
?- sentences(doc_id, index, text, tokens, lemmas, pos_tags, ner_tags, _, _).'
注意因爲我們上面少了一個字段,所以對比deepdive官網給的demo,這裏應該少一個字段哈:
注意:至此,上面的部分其實可以歸結爲使用nlp包進行最原始的信息抽取,可以使用不同的nlp包,進行不同的
而後面的步驟流程基本都一樣啦,只不過有的地方需要小改,比如原先使用斯坦福nlp包的時候,公式對應的是ORG,而使用ltp包的時候,其對應的是-Ni,所以後面不再細說,可直接參看deepdive官網本demo的說明,下面只說哪裏需要改動
爲了清楚區分兩部分,這裏暫且畫一條分割線吧
------------------------------------------------------------------------------------------------------------------------------------------------------------------
四 抽取候選關係
需要改的地方首先是app.ddlog中給map.comanpy_mention.py傳參的時候,用到的是sentences表,但是注意,因爲我們在上面給表sentences取掉了一個字段,所以下面第二個紅框記得刪除一個字段
再者需要修改的就是map.comanpy_mention.py函數,原先公司對應的命名實體是ORG,使用ltp包後應該是Ni,當然啦,其還進一步對Ni進行了細化,這裏不管,只要包含Ni即可
好啦,修改完畢,運行:
deepdive compile && deepdive do company_mention
看一下結果:
deepdive query 'mention_id,mention_text,doc_id,sentence_index,begin_index,end_index | 5 ?- company_mention(mention_id,mention_text,doc_id,sentence_index,begin_index,end_index).'
五 抽取候選關係
這裏不需要什麼修改,直接運行即可
deepdive compile && deepdive do transaction_candidate
查看一下結果
deepdive sql "select * from transaction_candidate"
六 特徵提取
這裏僅僅需要修改extract_transaction_features.py輸入參數時用到sentences表,還是將其參數減少一個
即app.ddlog:
修改完畢運行:
deepdive compile && deepdive do transaction_feature
看一下運行結果
deepdive sql "select * from transaction_feature"
七 樣本打標
先導入先驗label 數據,即已知的交易數據,很簡單,直接導入就可以啦:
deepdive compile && deepdive do transaction_dbdata
首先需要修改supervise_transaction.py函數輸入參數時用到sentences表,還是將其參數減少一個
即app.ddlog:
其次修改supervise_transaction.py腳本:
修改完畢,運行
deepdive compile && deepdive do transaction_label_resolved
看一下結果:
deepdive sql "select * from transaction_label_resolved"
八 模型構建
沒什麼變化,直接運行即可
deepdive compile && deepdive do has_transaction
看一下結果:
九 因子圖構建
沒什麼變化直接運行即可
deepdive compile && deepdive do probabilities
查看一下結果:
deepdive sql "SELECT p1_id, p2_id, expectation FROM has_transaction_label_inference ORDER BY random() LIMIT 20"
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
劃分數據集看其在測試集上面的效果:
首先導入測試集數據
deepdive compile && deepdive do transaction_dbdata_test
然後相關聯查詢
deepdive sql "select t1.p1_id,company_name_1,t1.p2_id,company_name_2,expectation from transaction_dbdata_test inner join (select transaction_candidate.p1_id as p1_id,p1_name,transaction_candidate.p2_id as p2_id,p2_name,expectation from has_transaction_label_inference left join transaction_candidate on has_transaction_label_inference.p1_id=transaction_candidate.p1_id and has_transaction_label_inference.p2_id=transaction_candidate.p2_id ) AS t1 on t1.p1_name=transaction_dbdata_test.company_name_1 and t1.p2_name=transaction_dbdata_test.company_name_2"
可以看到大部分還是好的
即使是測試集因爲上述的打標過程使得其也是有label的,爲了查看模型在沒有打標那部分上面的效果,這裏應該查一開始label爲NULL的這部分數據:
deepdive sql "select t2.p1_id,t2.p1_name,t2.p2_id,t2.p2_name,t2.expectation from transaction_dbdata_test inner join (select t1.p1_id,transaction_candidate.p1_name,t1.p2_id,transaction_candidate.p2_name, expectation from transaction_candidate inner join (select has_transaction.p1_id,has_transaction.p2_id,expectation from has_transaction_label_inference inner join has_transaction on has_transaction_label_inference.p1_id=has_transaction.p1_id and has_transaction_label_inference.p2_id=has_transaction.p2_id where has_transaction.label is NULL) as t1 on t1.p1_id=transaction_candidate.p1_id and t1.p2_id=transaction_candidate.p2_id) as t2 on t2.p1_name=transaction_dbdata_test.company_name_1 and t2.p2_name=transaction_dbdata_test.company_name_2"
最後這裏再提一句,由於ltp提取的命名實體中,使用Ni 表示機構名,且其進一步進行了細化即使用B 表示實體開始詞,I表示實體中間詞,E表示實體結束詞,S表示單獨成實體,所以如果想完全和斯坦福對應,這裏可以考慮將所有細化都改爲Ni
模型
關於模型方面是採用的因子圖,可以分爲兩大塊來看那就是:權重學習和推理
推理部分就是利用權重進行一個邊緣概率的計算,很簡單,最後計算得到一種關係的概率。
https://blog.csdn.net/unreliable/article/details/79982361
兒難點在於權重學習部分,這裏又有兩大部分,一種是人爲指定的某些依賴關係的權重,就是demo中比如p1和p2有交易,那麼就認爲p2和p1有交易,這裏的權值是3,不用學習,還有一部分weight是特徵,需要靠特徵學習,得到這些特徵的權值,可以使用如下命令查看訓練後特徵的權值
首先要運行
deepdive do data/model/weights
該命令會創建一個權重的綜合的視圖,叫做: dd_inference_result_weights_mapping
. 有了這個視圖,就可以很容易得到每個推理規則和它們的參數值,如:
deepdive sql "SELECT description,weight FROM dd_inference_result_weights_mapping order by weight desc"
正如上面所看到的,有了權值,通過計算一個邊緣概率(所謂的推理部分)便可得到我們想要的最終關係的預測概率,所以重點在於權重要合理,就這要求我們指定一些依賴關係爲常數的時候,要充分考慮當前預測領域的一些專業背景,這裏可以定義各種依賴關係的常數權重。
最後總結一下,最終的預測概率是一個邊緣概率,其計算用到兩方面一個是和依賴節點的這個關係的權值,一個是依賴節點的概率,其中後者中部分節點的概率是已知的即label(當然label 的得來其實也可以看做是另外一個權值的過程,因爲從打標的過程可以看出,其也是定義了多種規則,對於一個樣本最後得到一系列權值,隨後將其相加即綜合考慮後才決定了最終的label是1還是0,) ,其餘是待預測的
注意:要區分開打標過程中定義的那些規則和模型這裏的定義的依賴關係的定義,前者是針對一個節點的label說的,其最終要實現的目的是經過定義的多種規則後確定label 的正負,而後者是因子圖裏面的因子,即依賴關係+權值,重點是要說明哪些依賴關係的權值是多少,這兩部分要分開看。
補充
下面幾個應該是常用的幾個數據庫層面的deepdive命令
#查看當前數據庫中的表
deepdive relation list
#查看某個表的字段
deepdive relation columns articles
#導出數據庫
deepdive unload bar bar-1.tsv /data/bar-2.csv.bz2
下面三個文件,是DeepDive中編譯的三個相關文件:
app.ddlog
deepdive.conf
schema.json
即當任何一個文件改變時,要先編譯,編譯後產生的文件是位於run/文件夾下的,即run/文件夾是編譯產生的
run下的比較常用的可以看一下dataflow.svg,其就是這個數據流圖,可以用瀏覽器直接打開。