无监督构建词库:更快更好的新词发现算法
作者丨蘇劍林
單位丨追一科技
研究方向丨NLP,神經網絡
個人主頁丨kexue.fm
新詞發現是 NLP 的基礎任務之一,主要是希望通過無監督發掘一些語言特征(主要是統計特征),來判斷一批語料中哪些字符片段可能是一個新詞。
“新詞發現”是一個比較通俗的叫法,更準確的叫法應該是“無監督構建詞庫”,因為原則上它能完整地構建一個詞庫出來,而不僅僅是“新詞”。當然,你可以將它跟常用詞庫進行對比,刪掉常見詞,就可以得到新詞了。
分詞的目的
分詞一般作為文本挖掘的第一步,仿佛是很自然的,但事實上也應該問個為什么:為什么要分詞?人本來就是按照字來書寫和理解的呀?
當模型的記憶和擬合能力足夠強(或者簡單點,足夠智能)的時候,我們完全可以不用分詞的,直接基于字的模型就可以做,比如基于字的文本分類、問答系統等,早已有人在研究。但是,即便這些模型能夠成功,也會因為模型復雜而導致效率下降,因此,很多時候(尤其是生產環境中),我們會尋求更簡單、更高效的方案。
什么方案最高效?以文本分類為例,估計最簡單高效的方案就是“樸素貝葉斯分類器”了,類似的,比較現代的是 FastText,它可以看作是“樸素貝葉斯”的“神經網絡版”。要注意,樸素貝葉斯基于一個樸素的假設:特征之間相互獨立。這個假設越成立,樸素貝葉斯的效果就越好。然而,對于文本來說,顯然上下文緊密聯系,這個假設還成立嗎?
注意到,當特征之間明顯不獨立的時候,可以考慮將特征組合之后,使得特征之間的相關性減弱,再用樸素貝葉斯。比如,對于文本,如果以字為特征,則樸素假設顯然不成立,如“我喜歡數學”中的“喜”和“歡”、“數”和“學”都明顯相關,這時候我們可以考慮將特征進行組合,得到“我/喜歡/數學”,這樣三個片段之間的相關性就沒有那么強了,因此可以考慮用上述結果。
可以發現,這個過程很像分詞,或者反過來說,分詞的主要目的之一,就是將句子分為若干個相關性比較弱的部分,便于進一步處理。從這個角度來看,分的可能不一定是“詞”,也可能是短語、常用搭配等。
說白了,分詞就是為了削弱相關性,降低對詞序的依賴,這一點,哪怕在深度學習模型中,都是相當重要的。有些模型,不分詞但是用 CNN,也就是把若干個字組合作為特征來看,這也是通過字的組合來減弱特征間的相關性的體現。
算法大意
既然分詞是為了削弱相關性,那么我們分詞,就是在相關性弱的地方切斷了。文章《【中文分詞系列】 2. 基于切分的新詞發現》[1] 其實就是這個意思,只是那里認為,文本的相關性僅由相鄰兩字(2grams)來決定,這在很多時候都是不合理的,比如“林心如”中的“心如”、“共和國”中的“和國”,凝固度(相關性)都不是很強,容易錯切。
因此,本文就是在前文的基礎上改進,那里只考慮了相鄰字的凝固度,這里同時考慮多字的內部的凝固度(ngrams),比如,定義三字的字符串內部凝固度為:
這個定義其實也就是說,要枚舉所有可能的切法,因為一個詞應該是處處都很“結實”的,4 字或以上的字符串凝固度類似定義。一般地,我們只需要考慮到 4 字(4grams)就好(但是注意,我們依舊是可以切出 4 字以上的詞來的)。
考慮了多字后,我們可以設置比較高的凝固度閾值,同時防止諸如“共和國”之類的詞不會被切錯,因為考慮三字凝固度,“共和國”就顯得相當結實了,所以,這一步就是“寧放過,勿切錯”的原則。
但是,“各項”和“項目”這兩個詞,它們的內部凝固度都很大,因為前面一步是“寧放過,勿切錯”,因此這樣會導致“各項目”也成詞,類似的例子還有“支撐著”、“球隊員”、“珠海港”等很多例子。但這些案例在 3grams 中來看,凝固度是很低的,所以,我們要有一個“回溯”的過程,在前述步驟得到詞表后,再過濾一遍詞表,過濾的規則就是,如果里邊的 n 字詞,不在原來的高凝固度的 ngrams 中,那么就得“出局”。
所以,考慮 ngrams 的好處就是,可以較大的互信息閾值情況下,不錯切詞,同時又排除模凌兩可的詞。就比如“共和國”,三字互信息很強,兩字就很弱了(主要還是因為“和國”不夠結實),但是又能保證像“的情況”這種不會被切出來,因為閾值大一點,“的情”和“的情況”都不結實了。
詳細的算法
完整的算法步驟如下:
第一步,統計:選取某個固定的 n,統計 2grams、3grams、…、ngrams,計算它們的內部凝固度,只保留高于某個閾值的片段,構成一個集合 G;這一步,可以為 2grams、3grams、…、ngrams 設置不同的閾值,不一定要相同,因為字數越大,一般來說統計就越不充分,越有可能偏高,所以字數越大,閾值要越高;
第二步,切分:用上述 grams 對語料進行切分(粗糙的分詞),并統計頻率。切分的規則是,只有一個片段出現在前一步得到的集合 G 中,這個片段就不切分,比如“各項目”,只要“各項”和“項目”都在 G 中,這時候就算“各項目”不在 G 中,那么“各項目”還是不切分,保留下來;
第三步,回溯:經過第二步,“各項目”會被切出來(因為第二步保證寧放過,不切錯)。回溯就是檢查,如果它是一個小于等于 n 字的詞,那么檢測它在不在 G 中,不在就出局;如果它是一個大于 n 字的詞,那個檢測它每個 n 字片段是不是在 G 中,只要有一個片段不在,就出局。還是以“各項目”為例,回溯就是看看,“各項目”在不在 3gram中,不在的話,就得出局。
每一步的補充說明:
1. 較高的凝固度,但綜合考慮多字,是為了更準,比如兩字的“共和”不會出現在高凝固度集合中,所以會切開(比如“我一共和三個人去玩”,“共和”就切開了),但三字“共和國”出現在高凝固度集合中,所以“中華人民共和國”的“共和”不會切開;
2. 第二步就是根據第一步篩選出來的集合,對句子進行切分(你可以理解為粗糙的分詞),然后把“粗糙的分詞結果”做統計,注意現在是統計分詞結果,跟第一步的凝固度集合篩選沒有交集,我們認為雖然這樣的分詞比較粗糙,但高頻的部分還是靠譜的,所以篩選出高頻部分;
3. 第三步,例如因為“各項”和“項目”都出現高凝固度的片段中,所以第二步我們也不會把“各項目”切開,但我們不希望“各項目”成詞,因為“各”跟“項目”的凝固度不高(“各”跟“項”的凝固度高,不代表“各”跟“項目”的凝固度高),所以通過回溯,把“各項目”移除(只需要看一下“各項目”在不在原來統計的高凝固度集合中即可,所以這步計算量是很小的)。
代碼實現
本次開源地址位于:https://github.com/bojone/word-discovery
注意這個腳本應該只能在 Linux 系統下使用。如果你想要在 Windows 下使用,應該需要做些修改,具體做哪些修改,我也不知道,請自行解決。注意算法本身理論上能適用于任意語言,但本文的實現原則上只適用于以“字”為基本單位的語言。
Github 中核心的腳本是 word_discovery.py [2],它包含了完整的實現和使用例子。下面我們簡單梳理一下這個例子。
首先,寫一個語料的生成器,逐句返回語料:
import?pymongo import?redb?=?pymongo.MongoClient().baike.items#?語料生成器,并且初步預處理語料 #?這個生成器例子的具體含義不重要,只需要知道它就是逐句地把文本yield出來就行了 def?text_generator():for?d?in?db.find().limit(5000000):yield?re.sub(u'[^\u4e00-\u9fa50-9a-zA-Z?]+',?'\n',?d['text']) 讀者不需要看懂我這個生成器究竟在做什么,只需要知道這個生成器就是在逐句地把原始語料給 yield 出來就行了。如果你還不懂生成器怎么寫,請自己去學。請不要在此文章內討論“語料格式應該是怎樣的”、“我要怎么改才能適用我的語料”這樣的問題,謝謝。
順便提一下,因為是無監督訓練,語料一般都是越大越好,幾百 M 到幾個 G 都可以,但其實如果你只要幾 M 的語料(比如一部小說),也可以直接測試,也能看到基本的效果(但可能要修改下面的參數)。
有了生成器之后,配置一些參數,然后就可以逐個步驟執行了:
min_count?=?32 order?=?4 corpus_file?=?'wx.corpus'?#?語料保存的文件名 vocab_file?=?'wx.chars'?#?字符集 ngram_file?=?'wx.ngrams'?#?ngram集 output_file?=?'wx.vocab'?#?最后導出的詞表write_corpus(text_generator(),?corpus_file)?#?將語料轉存為文本 count_ngrams(corpus_file,?order,?vocab_file,?ngram_file)?#?用Kenlm統計ngram ngrams?=?KenlmNgrams(vocab_file,?ngram_file,?order,?min_count)?#?加載ngram ngrams?=?filter_ngrams(ngrams.ngrams,?ngrams.total,?[0,?1,?3,?5])?#?過濾ngram 注意,kenlm 需要一個以空格分詞的、純文本格式的語料作為輸入,而?write_corpus?函數就是幫我們做這件事情的,然后?count_ngrams?就是調用 kenlm 的 count_ngrams 程序來統計 ngram。所以,你需要自行編譯好 kenlm,并把它的 count_ngrams 放到跟 word_discovery.py 同一目錄下。如果有 Linux 環境,那 kenlm 的編譯相當簡單,筆者之前在這里 [3]也討論過 kenlm,可以參考。
count_ngrams?執行完畢后,結果會保存在一個二進制文件中,而?KenlmNgrams?就是讀取這個文件的,如果你輸入的語料比較大,那么這一步會需要比較大的內存。最后?filter_ngrams?就是過濾 ngram 了,[0, 1, 3, 5] 是互信息的閾值,其中第一個 0 無意義,僅填充用,而 1、3、5 分別是 2gram、3gram、4gram 的互信息閾值,基本上單調遞增比較好。?
至此,我們完成了所有的“準備工作”,現在可以著手構建詞庫了。首先構建一個 ngram 的 Trie 樹,然后用這個 Trie 樹就可以做一個基本的“預分詞”:
ngtrie?=?SimpleTrie()?#?構建ngram的Trie樹for?w?in?Progress(ngrams,?100000,?desc=u'build?ngram?trie'):_?=?ngtrie.add_word(w)candidates?=?{}?#?得到候選詞 for?t?in?Progress(text_generator(),?1000,?desc='discovering?words'):for?w?in?ngtrie.tokenize(t):?#?預分詞candidates[w]?=?candidates.get(w,?0)?+?1 這個預分詞的過程在上文中已經介紹過了,總之有點像最大匹配,由 ngram 片段連接成盡可能長的候選詞。最后,再把候選詞過濾一下,就可以把詞庫保存下來了:
#?頻數過濾 candidates?=?{i:?j?for?i,?j?in?candidates.items()?if?j?>=?min_count} #?互信息過濾(回溯) candidates?=?filter_vocab(candidates,?ngrams,?order)#?輸出結果文件 with?open(output_file,?'w')?as?f:for?i,?j?in?sorted(candidates.items(),?key=lambda?s:?-s[1]):s?=?'%s\t%s\n'?%?(i.encode('utf-8'),?j)f.write(s)
評測
這是我從 500 萬篇微信公眾號文章(保存為文本之后是 20 多 G)提取出來的詞庫,供讀者有需使用。https://kexue.fm/usr/uploads/2019/09/1023754363.zip
訓練時間好像是五六個小時吧,我記不是很清楚了,總之比原始的實現會快,資源消耗也低一些。?
讀者之前老說我寫的這些算法沒有標準評測,這次我就做了一個簡單的評測,評測腳本是 evaluate.py。
https://github.com/bojone/word-discovery/blob/master/evaluate.py
具體來說,提取剛才的詞典 wx.vocab.zip 的前 10 萬個詞作為詞庫,用結巴分詞加載這個10萬詞的詞庫(不用它自帶的詞庫,并且關閉新詞發現功能),這就構成了一個基于無監督詞庫的分詞工具,然后用這個分詞工具去分 bakeoff 2005 [4]提供的測試集,并且還是用它的測試腳本評測,最終在 PKU 這個測試集上得分是:
也就是說能做到 0.746 的 F1。這是個什么水平呢?ICLR 2019 有一篇文章叫做 Unsupervised Word Discovery with Segmental Neural Language Models [5],里邊提到了它在同一個測試集上的結果為 F1=0.731,照這樣看這個算法的結果還不差于頂會的最優結果呢。
注:這里是為了給效果提供一個直觀感知,比較可能是不公平的,因為我不確定這篇論文中的訓練集用了哪些語料。但我感覺在相同時間內本文算法會優于論文的算法,因為直覺論文的算法訓練起來會很慢。作者也沒有開源,所以有不少不確定之處,如有錯謬,請讀者指正。
總結
本文復現了筆者之前提出了新詞發現(詞庫構建)算法,主要是做了速度上的優化,然后做了做簡單的效果評測。但具體效果讀者還是得在使用中慢慢調試了。?
祝大家使用愉快,Enjoy it!
相關鏈接
[1]?https://kexue.fm/archives/3913[2]?https://github.com/bojone/word-discovery/blob/master/word_discovery.py[3] https://kexue.fm/archives/3956#實踐:訓練[4]?http://sighan.cs.uchicago.edu/bakeoff2005/[5]?https://arxiv.org/abs/1811.09353
點擊以下標題查看作者其他文章:?
當Bert遇上Keras:這可能是Bert最簡單的打開姿勢
玩轉Keras之Seq2Seq自動生成標題 | 附開源代碼
一文讀懂「Attention is All You Need」| 附代碼實現
基于CNN的閱讀理解式問答模型:DGCNN
基于DGCNN和概率圖的輕量級信息抽取模型
ICLR 2019最佳論文 | 用有序神經元表達層次結構
#投 稿 通 道#
?讓你的論文被更多人看到?
如何才能讓更多的優質內容以更短路徑到達讀者群體,縮短讀者尋找優質內容的成本呢?答案就是:你不認識的人。
總有一些你不認識的人,知道你想知道的東西。PaperWeekly 或許可以成為一座橋梁,促使不同背景、不同方向的學者和學術靈感相互碰撞,迸發出更多的可能性。
PaperWeekly 鼓勵高校實驗室或個人,在我們的平臺上分享各類優質內容,可以是最新論文解讀,也可以是學習心得或技術干貨。我們的目的只有一個,讓知識真正流動起來。
??來稿標準:
? 稿件確系個人原創作品,來稿需注明作者個人信息(姓名+學校/工作單位+學歷/職位+研究方向)?
? 如果文章并非首發,請在投稿時提醒并附上所有已發布鏈接?
? PaperWeekly 默認每篇文章都是首發,均會添加“原創”標志
? 投稿郵箱:
? 投稿郵箱:hr@paperweekly.site?
? 所有文章配圖,請單獨在附件中發送?
? 請留下即時聯系方式(微信或手機),以便我們在編輯發布時和作者溝通
?
現在,在「知乎」也能找到我們了
進入知乎首頁搜索「PaperWeekly」
點擊「關注」訂閱我們的專欄吧
關于PaperWeekly
PaperWeekly 是一個推薦、解讀、討論、報道人工智能前沿論文成果的學術平臺。如果你研究或從事 AI 領域,歡迎在公眾號后臺點擊「交流群」,小助手將把你帶入 PaperWeekly 的交流群里。
▽ 點擊 |?閱讀原文?| 查看作者博客
總結
以上是生活随笔為你收集整理的无监督构建词库:更快更好的新词发现算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 蒙特卡洛梯度估计方法(MCGE)简述
- 下一篇: SIGIR 2019 开源论文 | 用户