最近正在嘗試幾種文本分類的算法,卻一直苦於沒有結構化的中文語料,原本是打算先爬下大把大把的部落格文章,再依 tag 將它們分門別類,可惜試了一陣子後,我見識到了理想和現實間的鴻溝。
所以就找上了基於非監督學習的 word2vec,為了銜接後續的資料處理,這邊採用的是基於 python 的主題模型函式庫 gensim。這篇教學並不會談太多 word2vec 的數學原理,而是考慮如何輕鬆又直覺地訓練中文詞向量,文章裡所有的程式碼都會傳上 github,現在,就讓我們進入正題吧。
取得語料
要訓練詞向量,第一步當然是取得資料集。由於 word2vec 是基於非監督式學習,訓練集一定一定要越大越好,語料涵蓋的越全面,訓練出來的結果也會越漂亮。我所採用的是維基百科於2016/08/20的備份,文章篇數共有 2822639 篇。因為維基百科會定期更新備份資料,如果 8 月 20 號的備份不幸地被刪除了,也可以前往維基百科:資料庫下載挑選更近期的資料,不過請特別注意一點,我們要挑選的是以 pages-articles.xml.bz2
結尾的備份,而不是以 pages-articles-multistream.xml.bz2
結尾的備份唷,否則會在清理上出現一些異常,無法正常解析文章。
在等待下載的這段時間,我們可以先把這次的主角gensim
配置好:
1 | pip3 install --upgrade gensim |
維基百科下載好後,先別急著解壓縮,因為這是一份 xml 文件,裏頭佈滿了各式各樣的標籤,我們得先想辦法送走這群不速之客,不過也別太擔心,gensim 早已看穿了一切,藉由調用 wikiCorpus,我們能很輕鬆的只取出文章的標題和內容。
初始化WikiCorpus
後,能藉由get_texts()
可迭代每一篇文章,它所回傳的是一個tokens list,我以空白符將這些 tokens 串接起來,統一輸出到同一份文字檔裡。這邊要注意一件事,get_texts()
受wikicorpus.py
中的變數ARTICLE_MIN_WORDS
限制,只會回傳內容長度大於 50 的文章。
1 | # -*- coding: utf-8 -*- |
在shell 裡輸入:
1 | python3 wiki_to_txt.py zhwiki-20160820-pages-articles.xml.bz2 |
如果你的資料不是 8 月 20 號的備份,記得把zhwiki-20160820-pages-articles.xml.bz2
換成你的備份的檔名唷。這約需花費 20 分鐘來處理,就讓我們先看一下接下來還要做些什麼吧~
開始斷詞
我們有清完標籤的語料了,第二件事就是要把語料中每個句子,進一步拆解成一個一個詞,這個步驟稱為「斷詞」。中文斷詞的工具比比皆是,這裏我採用的是 jieba,儘管它在繁體中文的斷詞上還是有些不如CKIP
,但他實在太簡單、太方便、太好調用了,足以彌補這一點小缺憾:
1 | 快速安裝結巴 |
1 | # 斷詞示例 |
現在,我們上一階段的檔案也差不多出爐了,以vi打開看起來會是這個樣子:
1 | 歐幾里得 西元前三世紀的希臘數學家 現在被認為是幾何之父 此畫為拉斐爾的作品 雅典學院 数学 是利用符号语言研究數量 结构 变化以及空间等概念的一門学科 从某种角度看屬於形式科學的一種 數學透過抽象化和邏輯推理的使用 由計數 計算 數學家們拓展這些概念...... |
Opps!出了一點狀況,我們發現簡體跟繁體混在一起了,比如「数学」與「數學」會被 word2vec 當成兩個不同的詞,所以我們在斷詞前,還需加上一道繁簡轉換的手續。然而我們的語料集相當龐大,一般的繁簡轉換會有些力不從心,建議採用OpenCC,轉換的方式很簡單:
1 | opencc -i wiki_texts.txt -o wiki_zh_tw.txt -c s2tw.json |
如果是要將繁體轉為簡體,只要將config的參數從s2tw.json
改成t2s.json
即可。現在再檢查一次wiki_zh_tw.txt
,的確只剩下繁體字了,終於能進入斷詞,輸入python3 segment.py
。
1 |
|
Stopwords and Window
好啦,這個東西大概要跑個 80 分鐘,先讓我講些幹話,先讓我們看看上頭做了什麼。除了之前演示的斷詞外,這邊還多做了兩件事,一是調整jieba的辭典,讓他對繁體斷詞比較友善,二是引入了停用詞,停用詞就是像英文中的 the,a,this,中文的你我他,與其他詞相比顯得不怎麼重要,對文章主題也無關緊要的,就可以將它視為停用詞。而要排除停用詞的理由,其實與word2vec的實作概念大大相關,由於在開頭講明了不深究概念,就讓我舉個例子替代長篇大論。
首先,在word2vec有一個概念叫 windows,我習慣叫他窗口,因為它給我的感覺跟TCP 那個會滑來滑去的東東很像。
Word2Vec
很顯然,一個詞的意涵跟他的左右鄰居很有關係,比如「雨越下越大,茶越充越淡」,什麼會「下」?「雨」會下,什麼會「淡」?茶會「淡」,這樣的類比舉不勝舉,那麼,若把思維逆轉過來呢?
顯然,我們或多或少能從左右鄰居是誰,猜出中間的是什麼,這很像我們國高中時天天在練的英文克漏字。那麼問題來了,左右鄰居有誰?能更精確地說,你要往左往右看幾個?假設我們以「孔乙己 一到 店 所有 喝酒 的 人 便都 看著 他 笑」為例,如果往左往右各看一個:
1 | [孔乙己 一到] 店 所有 喝酒 的 人 便 都 看著 他 笑 |
這樣就構成了一個 size=1 的 windows,這個 1 是極端的例子,為了讓我們看看有停用詞跟沒停用詞差在哪,這句話去除了停用詞應該會變成:
1 | 孔乙己 一到 店 所有 喝酒 人 看著 笑 |
我們看看「人」的窗口變化,原本是「的 人 便」,後來是「喝酒 人 看著」,相比原本的情形,去除停用詞後,我們對「人」這個詞有更多認識,比如人會喝酒,人會看東西,當然啦,這是我以口語的表達,機器並不會這麼想,機器知道的是人跟喝酒會有某種關聯,跟看會有某種關聯,但儘管如此,也遠比本來的「的 人 便」好太多太多了。
就在剛剛,我的斷詞已經跑完了,現在,讓我們進入收尾的階段吧
1 | 2016-08-26 22:27:59,480 : INFO : 已處理 260000 個 token |
訓練詞向量
這是最簡單的部分,同時也是最困難的部分,簡單的是程式碼,困難的是詞向量效能上的微調與後訓練。對了,如果你已經對詞向量和語言模型有些研究,在輸入python3 train.py
之前,建議先看一下之後的內文,相信我,你會需要的。
1 | # -*- coding: utf-8 -*- |
扣掉logging
與註釋就剩下三行,真是精簡的漂亮。上頭通篇的學問在model = word2vec.Word2Vec(sentences, size=250)
,我們先讓它現出原型:
1 | class gensim.models.word2vec.Word2Vec(sentences=None, size=100, alpha=0.025, window=5, min_count=5, max_vocab_size=None, sample=0.001, seed=1, workers=3, min_alpha=0.0001, sg=0, hs=0, negative=5, cbow_mean=1, hashfxn=<built-in function hash>, iter=5, null_word=0, trim_rule=None, sorted_vocab=1, batch_words=10000) |
這抵得上 train.py 的所有程式碼了。不過也別太擔心,裏頭多是無關緊要的參數,從初學的角度來看,我們會去動到的大概是:
- sentences:當然了,這是要訓練的句子集,沒有他就不用跑了
- size:這表示的是訓練出的詞向量會有幾維
- alpha:機器學習中的學習率,這東西會逐漸收斂到
min_alpha
- sg:這個不是三言兩語能說完的,sg=1表示採用skip-gram,sg=0 表示採用cbow
- window:還記得孔乙己的例子嗎?能往左往右看幾個字的意思
- workers:執行緒數目,除非電腦不錯,不然建議別超過 4
- min_count:若這個詞出現的次數小於
min_count
,那他就不會被視為訓練對象
等摸清 Word2Vec 背後的原理後,也可以試著調調hs
、negative
,看看對效能會有什麼影響。
詞向量實驗
訓練完成後,讓我們來測試一下模型的效能,運行python3 demo.py
。由於 gensim 將整個模型讀了進來,所以記憶體會消耗相當多,如果出現了MemoryError
,可能得調整一下min_count
或對常用詞作一層快取,這點要注意一下。
先來試試相似詞排序吧!
1 | 飲料 |
前100太多了,所以只把前幾個結果貼上來,我們也能調用model.similarity(word2,word1)
來直接取得兩個詞的相似度:
1 | 冰沙 刨冰 |
能稍微區隔出詞與詞之間的主題,整體來說算是可以接受的了。
更上一層樓
如何優化詞向量的表現?這其實有蠻多方法的,大方向是從應用的角度出發,我們能針對應用特化的語料進行再訓練,除此之外,斷詞器的選擇也很重要,它很大程度的決定什麼詞該在什麼地方出現,如果發現 jieba
有些力不能及的,不妨試著採用別的斷詞器,或是試著在 jieba
自訂辭典,調一下每個詞的權重。
應用考慮好了,接著看看模型,我們可以調整 model()
的參數,比方窗口大小、維度、學習率,進一步還能比較 skip-gram 與 cbow 的效能差異,什麼,你說不知道 skip-gram 跟 cbow 是什麼?且看下回分解。