今年上半年,我在人人網實習了一段時間,期間獲得了不少寶貴的數據,並作了一些還算有意義的事情,在這裏和你們一起分享。感謝人人網提供的數據與工做環境,感謝趙繼承博士、詹衛東老師的支持和建議。在這項工做中,我獲得了不少與衆人交流的機會,特別感謝 OpenParty 、 TEDxBeijing 提供的平臺。本文已發表在了《程序員》雜誌,分上下兩部分刊於 2012 年 7 月刊和 8 月刊,在此感謝盧鶇翔編輯的辛勤工做。因爲衆所周知的緣由,《程序員》刊出的文章被和諧過(看到後面你們就自動地知道被和諧的內容是什麼了),於是我決定把完整版發在 Blog 上,同時與更多的人一同分享。對此感興趣的朋友能夠給我發郵件繼續交流。好了,開始說正文吧。程序員
做爲中文系應用語言學專業的學生以及一名數學 Geek ,我很是熱衷於用計算的方法去分析漢語資料。漢語是一種獨特而神奇的語言。對漢語資料進行天然語言處理時,咱們會遇到不少其餘語言不會有的困難,好比分詞——漢語的詞與詞之間沒有空格,那計算機怎麼才知道,「已結婚的和還沒有結婚的青年都要實行計劃生育」究竟說的是「已/結婚/的/和/還沒有/結婚/的/青年」,仍是「已/結婚/的/和尚/未/結婚/的/青年」呢?這就是所謂的分詞歧義難題。不過,如今不少語言模型已經能比較漂亮地解決這一問題了。但在中文分詞領域裏,還有一個比分詞歧義更使人頭疼的東西——未登陸詞。中文沒有首字母大寫,專名號也被取消了,這叫計算機如何辨認人名地名之類的東西?更慘的則是機構名、品牌名、專業名詞、縮略語、網絡新詞等等,它們的產生機制彷佛徹底無規律可尋。最近十年來,中文分詞領域都在集中攻克這一難關。自動發現新詞成爲了關鍵的環節。github
挖掘新詞的傳統方法是,先對文本進行分詞,而後猜想未能成功匹配的剩餘片斷就是新詞。這彷佛陷入了一個怪圈:分詞的準確性自己就依賴於詞庫的完整性,若是詞庫中根本沒有新詞,咱們又怎麼能信任分詞結果呢?此時,一種大膽的想法是,首先不依賴於任何已有的詞庫,僅僅根據詞的共同特徵,將一段大規模語料中可能成詞的文本片斷所有提取出來,無論它是新詞仍是舊詞。而後,再把全部抽出來的詞和已有詞庫進行比較,不就能找出新詞了嗎?有了抽詞算法後,咱們還能以詞爲單位作更多有趣的數據挖掘工做。這裏,我所選用的語料是人人網 2011 年 12 月前半個月部分用戶的狀態。很是感謝人人網提供這份極具價值的網絡語料。算法
要想從一段文本中抽出詞來,咱們的第一個問題就是,怎樣的文本片斷纔算一個詞?你們想到的第一個標準或許是,看這個文本片斷出現的次數是否足夠多。咱們能夠把全部出現頻數超過某個閾值的片斷提取出來,做爲該語料中的詞彙輸出。不過,光是出現頻數高還不夠,一個常常出現的文本片斷有可能不是一個詞,而是多個詞構成的詞組。在人人網用戶狀態中,「的電影」出現了 389 次,「電影院」只出現了 175 次,然而咱們卻更傾向於把「電影院」看成一個詞,由於直覺上看,「電影」和「院」凝固得更緊一些。網絡
爲了證實「電影院」一詞的內部凝固程度確實很高,咱們能夠計算一下,若是「電影」和「院」真的是各自獨立地在文本中隨機出現,它倆正好拼到一塊兒的機率會有多小。在整個 2400 萬字的數據中,「電影」一共出現了 2774 次,出現的機率約爲 0.000113 。「院」字則出現了 4797 次,出現的機率約爲 0.0001969 。若是二者之間真的毫無關係,它們剛好拼在了一塊兒的機率就應該是 0.000113 × 0.0001969 ,約爲 2.223 × 10-8 次方。但事實上,「電影院」在語料中一共出現了 175 次,出現機率約爲 7.183 × 10-6 次方,是預測值的 300 多倍。相似地,統計可得「的」字的出現機率約爲 0.0166 ,於是「的」和「電影」隨機組合到了一塊兒的理論機率值爲 0.0166 × 0.000113 ,約爲 1.875 × 10-6 ,這與「的電影」出現的真實機率很接近——真實機率約爲 1.6 × 10-5 次方,是預測值的 8.5 倍。計算結果代表,「電影院」更多是一個有意義的搭配,而「的電影」則更像是「的」和「電影」這兩個成分偶然拼到一塊兒的。測試
固然,做爲一個無知識庫的抽詞程序,咱們並不知道「電影院」是「電影」加「院」得來的,也並不知道「的電影」是「的」加上「電影」得來的。錯誤的切分方法會太高地估計該片斷的凝合程度。若是咱們把「電影院」看做是「電」加「影院」所得,由此獲得的凝合程度會更高一些。所以,爲了算出一個文本片斷的凝合程度,咱們須要枚舉它的凝合方式——這個文本片斷是由哪兩部分組合而來的。令 p(x) 爲文本片斷 x 在整個語料中出現的機率,那麼咱們定義「電影院」的凝合程度就是 p(電影院) 與 p(電) · p(影院) 比值和 p(電影院) 與 p(電影) · p(院) 的比值中的較小值,「的電影」的凝合程度則是 p(的電影) 分別除以 p(的) · p(電影) 和 p(的電) · p(影) 所得的熵的較小值。優化
能夠想到,凝合程度最高的文本片斷就是諸如「蝙蝠」、「蜘蛛」、「彷徨」、「忐忑」、「玫瑰」之類的詞了,這些詞裏的每個字幾乎老是會和另外一個字同時出現,從不在其餘場合中使用。
光看文本片斷內部的凝合程度還不夠,咱們還須要從總體來看它在外部的表現。考慮「被子」和「輩子」這兩個片斷。咱們能夠說「買被子」、「蓋被子」、「進被子」、「好被子」、「這被子」等等,在「被子」前面加各類字;但「輩子」的用法卻很是固定,除了「一生」、「這輩子」、「上輩子」、「下輩子」,基本上「輩子」前面不能加別的字了。「輩子」這個文本片斷左邊能夠出現的字太有限,以致於直覺上咱們可能會認爲,「輩子」並不單獨成詞,真正成詞的實際上是「一生」、「這輩子」之類的總體。可見,文本片斷的自由運用程度也是判斷它是否成詞的重要標準。若是一個文本片斷可以算做一個詞的話,它應該可以靈活地出如今各類不一樣的環境中,具備很是豐富的左鄰字集合和右鄰字集合。
「信息熵」是一個很是神奇的概念,它可以反映知道一個事件的結果後平均會給你帶來多大的信息量。若是某個結果的發生機率爲 p ,當你知道它確實發生了,你獲得的信息量就被定義爲 – log(p) 。 p 越小,你獲得的信息量就越大。若是一顆骰子的六個面分別是 1 、 1 、 1 、 2 、 2 、 3 ,那麼你知道了投擲的結果是 1 時可能並不會那麼吃驚,它給你帶來的信息量是 – log(1/2) ,約爲 0.693 。知道投擲結果是 2 ,給你帶來的信息量則是 – log(1/3) ≈ 1.0986 。知道投擲結果是 3 ,給你帶來的信息量則有 – log(1/6) ≈ 1.79 。可是,你只有 1/2 的機會獲得 0.693 的信息量,只有 1/3 的機會獲得 1.0986 的信息量,只有 1/6 的機會獲得 1.79 的信息量,於是平均狀況下你會獲得 0.693/2 + 1.0986/3 + 1.79/6 ≈ 1.0114 的信息量。這個 1.0114 就是那顆骰子的信息熵。如今,假如某顆骰子有 100 個面,其中 99 個面都是 1 ,只有一個面上寫的 2 。知道骰子的拋擲結果是 2 會給你帶來一個巨大無比的信息量,它等於 – log(1/100) ,約爲 4.605 ;但你只有百分之一的機率獲取到這麼大的信息量,其餘狀況下你只能獲得 – log(99/100) ≈ 0.01005 的信息量。平均狀況下,你只能得到 0.056 的信息量,這就是這顆骰子的信息熵。再考慮一個最極端的狀況:若是一顆骰子的六個面都是 1 ,投擲它不會給你帶來任何信息,它的信息熵爲 – log(1) = 0 。何時信息熵會更大呢?換句話說,發生了怎樣的事件以後,你最想問一下它的結果如何?直覺上看,固然就是那些結果最不肯定的事件。沒錯,信息熵直觀地反映了一個事件的結果有多麼的隨機。
咱們用信息熵來衡量一個文本片斷的左鄰字集合和右鄰字集合有多隨機。考慮這麼一句話「吃葡萄不吐葡萄皮不吃葡萄倒吐葡萄皮」,「葡萄」一詞出現了四次,其中左鄰字分別爲 {吃, 吐, 吃, 吐} ,右鄰字分別爲 {不, 皮, 倒, 皮} 。根據公式,「葡萄」一詞的左鄰字的信息熵爲 – (1/2) · log(1/2) – (1/2) · log(1/2) ≈ 0.693 ,它的右鄰字的信息熵則爲 – (1/2) · log(1/2) – (1/4) · log(1/4) – (1/4) · log(1/4) ≈ 1.04 。可見,在這個句子中,「葡萄」一詞的右鄰字更加豐富一些。
在人人網用戶狀態中,「被子」一詞一共出現了 956 次,「輩子」一詞一共出現了 2330 次,二者的右鄰字集合的信息熵分別爲 3.87404 和 4.11644 ,數值上很是接近。但「被子」的左鄰字用例很是豐富:用得最多的是「曬被子」,它一共出現了 162 次;其次是「的被子」,出現了 85 次;接下來分別是「條被子」、「在被子」、「牀被子」,分別出現了 69 次、 64 次和 52 次;固然,還有「疊被子」、「蓋被子」、「加被子」、「新被子」、「掀被子」、「收被子」、「薄被子」、「踢被子」、「搶被子」等 100 多種不一樣的用法構成的長尾⋯⋯全部左鄰字的信息熵爲 3.67453 。但「輩子」的左鄰字就很可憐了, 2330 個「輩子」中有 1276 個是「一生」,有 596 個「這輩子」,有 235 個「下輩子」,有 149 個「上輩子」,有 32 個「半輩子」,有 10 個「八輩子」,有 7 個「幾輩子」,有 6 個「哪輩子」,以及「n 輩子」、「兩輩子」等 13 種更罕見的用法。全部左鄰字的信息熵僅爲 1.25963 。於是,「輩子」可否成詞,明顯就有爭議了。「下子」則是更典型的例子, 310 個「下子」的用例中有 294 個出自「一會兒」, 5 個出自「兩下子」, 5 個出自「這下子」,其他的都是隻出現過一次的罕見用法。事實上,「下子」的左鄰字信息熵僅爲 0.294421 ,咱們不該該把它看做一個能靈活運用的詞。固然,一些文本片斷的左鄰字沒啥問題,右鄰字用例卻很是貧乏,例如「交響」、「後遺」、「鵝卵」等,把它們看做單獨的詞彷佛也不太合適。咱們不妨就把一個文本片斷的自由運用程度定義爲它的左鄰字信息熵和右鄰字信息熵中的較小值。
在實際運用中你會發現,文本片斷的凝固程度和自由程度,兩種判斷標準缺一不可。只看凝固程度的話,程序會找出「巧克」、「俄羅」、「顏六色」、「柴可夫」等其實是「半個詞」的片斷;只看自由程度的話,程序則會把「吃了一頓」、「看了一遍」、「睡了一晚」、「去了一趟」中的「了一」提取出來,由於它的左右鄰字都太豐富了。
咱們把文本中出現過的全部長度不超過 d 的子串都看成潛在的詞(即候選詞,其中 d 爲本身設定的候選詞長度上限,我設定的值爲 5 ),再爲出現頻數、凝固程度和自由程度各設定一個閾值,而後只須要提取出全部知足閾值要求的候選詞便可。爲了提升效率,咱們能夠把語料全文視做一整個字符串,並對該字符串的全部後綴按字典序排序。下表就是對「四是四十是十十四是十四四十是四十」的全部後綴進行排序後的結果。實際上咱們只須要在內存中存儲這些後綴的前 d + 1 個字,或者更好地,只儲存它們在語料中的起始位置。
更多見原文 http://www.matrix67.com/blog/archives/5044
更多相關實現:
我把樓主的算法用python實現,並進行一些必要的優化,並加入到個人分詞庫裏。開源的分詞庫地址:https://github.com/jannson/yaha
實如今 yaha/wordmaker.py裏
實用示例在 tests/test_cuttor.py裏
歡迎你們進行測試。6M如下的文本問題不大,如若要分析更大的文本,後續會添加一個c++實現的版本,測試發現比python快 10倍
https://github.com/sing1ee/dict_build 這個簡單用java實現了,看效果還不錯。挺好玩的。
C++11/14實現了一個,寫得渣勿噴。。 https://github.com/zouyxdut/new-words-discoverer
成詞條件
互信息
左右熵
位置成詞機率
ngram 頻率
自動構建中文詞庫:http://www.matrix67.com/blog/archives/5044
python 3 實現:
https://github.com/yanghanxy/New-Word-Detection
Python 3 實現了一個,(新)
Neologism
https://github.com/jtyoui/Jtyoui/tree/master/jtyoui/word