美食圖譜復現指南之依存句法分析
本文程式碼開源在: DesertsX/gulius-projects
哈工大語言云的官網有一篇名為 《使用語言云分析微博使用者飲食習慣》 的文章,裡面講到了藉助分詞、詞性標註和依存句法分析等NLP技術,可以從微博文字內容中提取出使用者飲食習慣等資料。
進而可以結合使用者性別、地區、發微博時間等不同維度資訊,展現出許多有趣的結果,比如下圖分別是上海、重慶、以及廣東(男性)的特色飲食習慣:

那麼如何抽取出上述食物呢?原文給出了由三個條件組成的規則: 一條微博裡含有詞語“吃”+與“吃”相關的句法關係為VOB(動賓關係)+“吃”的賓語為名詞 ,就可以判斷髮生飲食行為,進而提取出“吃”的賓語就是相關的食物。
作為解釋,給出了三個例句:“我剛吃了一塊巧克力”、“今天我去電影院看了濃情巧克力”、“我吃了個巧克力味冰淇淋”。

句子經過分詞,並在下方標註了詞性,依存弧表明每個詞語之間的關係,比如主謂關係(SBV)、動賓關係(VOB)等等。
由上述規則可以判斷出第二句沒有飲食行為,於是進行過濾;而從另外兩句中可以分別抽取出“巧克力”和“冰淇淋”(當然第三句更細粒度、更準確地應該是“巧克力味冰淇淋”,如何改進上面的規則,後面再提)。
經過上面的介紹,看起來這條規則還蠻符合邏輯,應該能行的吧?但不知怎的腦海中突然浮現出張學友這張表情包,呼之欲出就是這句......

於是用語言云官方的線上演示試了下:出現“吃”這個字+與“吃”相關的有VOB動賓關係+賓語是名詞“n”......過於完美地符合所有條件。

扯回來,總得來看,給出的判斷邏輯還是靠譜的,那麼該如何實現呢?“章口就萊”甩下一句:Talk is cheap. Show me the code. 然而翻遍原文也沒找到實現程式碼。

很早以前就看過這篇文章,一直不會,重新試了下,發現非常簡單,果然是“難者不會,會者不難”,核心程式碼也就兩行。

以下是程式碼部分(本文程式碼開源在: DesertsX/gulius-projects ),原本不必講 pip install pyltp
這種基礎安裝第三方庫的事,但因為windows下可能會出現 Microsoft Visual C++
等相關錯誤,所以建議參考:《 哈工大自然語言處理ltp在windows10下的安裝使用 》一文的方案二,親測可行。

再是pyltp的入門介紹此處略過,看官方文件 使用 pyltp 一文就夠了。
pyltp 是 LTP 的 Python 封裝,提供了分詞,詞性標註,命名實體識別,依存句法分析,語義角色標註的功能。
pyltp 安裝成功,並下載好相應的 LTP 模型檔案後,分別載入分詞、詞性標註和依存句法分析的模型。
import os from pyltp import Segmentor LTP_DATA_DIR = '/path/to/your/ltp_data' # ltp模型目錄的路徑 # 載入分詞模型 cws_model_path = os.path.join(LTP_DATA_DIR, 'cws.model') segmentor = Segmentor() segmentor.load(cws_model_path) # 載入詞性標註模型 from pyltp import Postagger pos_model_path = os.path.join(LTP_DATA_DIR, 'pos.model') postagger = Postagger() postagger.load(pos_model_path) # 載入依存句法分析模型 from pyltp import Parser par_model_path = os.path.join(LTP_DATA_DIR, 'parser.model') parser = Parser() parser.load(par_model_path)
對每個句子分別進行分詞、詞性標註和依存句法分析,並對分詞後的每個詞語依次提取依存弧的父節點id(Root 的 id 為0,其他按分詞結果依次遞增)、依存關係以及依存父節點對應的詞語。最後寫出進行飲食行為判斷的核心程式碼即可。
def extract_food(sentence): words = segmentor.segment(sentence) print(" ".join(words)) postags = postagger.postag(words) for word, postag in zip(words, postags): print(word + '/'+ postag, end=' ') netags = recognizer.recognize(words, postags) arcs = parser.parse(words, postags) # 例句:我 剛 吃 了 一 塊 巧克力 。 # 提取依存父節點id # 3, 3, 0, 3, 6, 7, 3, 3 rely_id = [arc.head for arc in arcs] # 提取依存關係 # ['SBV', 'ADV', 'HED', 'RAD', 'ATT', 'ATT', 'VOB', 'WP'] relation = [arc.relation for arc in arcs] # 匹配依存父節點詞語 # ['吃', '吃', 'Root', '吃', '塊', '巧克力', '吃', '吃'] heads = ['Root' if id==0 else words[id-1] for id in rely_id] print("\n") for i in range(len(words)): if postags[i] == 'n' and heads[i] == '吃' and relation[i] == 'VOB': print("找到了一種食物:" + words[i]) print("=" * 30)
對依存關係這部分還不太理解的可以看下此文: 依存句法分析結果的輸出怎麼看

接著三個例句進行測試,結果和原文相符。
sentences = ['我剛吃了一塊巧克力。', '今天我去電影院看了濃情巧克力。', '我吃了個巧克力味冰淇淋。'] for sent in sentences: extract_food(sent)

以上算是簡單復現了下這篇部落格的思路。還有個問題就是,現實中大家討論飲食的方式可能並不像例句中那麼規整簡單。
以以前爬取的知乎想法裡與“#好吃的杭州#”相關的言論為例( 該話題知乎想法API ),各種表述方式應有盡有,理想與現實的差距可見一斑。

再者是復現的程式碼,對於食物賓語的提取邏輯過於簡單,以致諸如“巧克力味冰淇淋”、“西湖醋魚”等帶有字首修飾的詞語都無法提取。當然賓語補全也能實現,此處暫且不表。

本文程式碼開源在: DesertsX/gulius-projects