無監督構建詞庫:更快更好的新詞發現算法


無監督構建詞庫:更快更好的新詞發現算法


作者丨蘇劍林

單位丨追一科技

研究方向丨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 re

db = 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

 

 

 

點擊以下標題查看作者其他文章: 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM