以 gensim 訓練中文詞向量

来源:互联网 发布:安装数据库失败日志 编辑:程序博客网 时间:2024/06/06 02:34

转自: http://zake7749.github.io/2016/08/28/word2vec-with-gensim/

最近正在嘗試幾種文本分類的算法,卻一直苦於沒有結構化的中文語料,原本是打算先爬下大把大把的部落格文章,再依 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 的文章。

12345678910111213141516171819202122232425
# -*- coding: utf-8 -*-import loggingimport sysfrom gensim.corpora import WikiCorpusdef main():    if len(sys.argv) != 2:        print("Usage: python3 " + sys.argv[0] + " wiki_data_path")        exit()    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)    wiki_corpus = WikiCorpus(sys.argv[1], dictionary={})        texts_num = 0        with open("wiki_texts.txt",'w',encoding='utf-8') as output:        for text in wiki_corpus.get_texts():            output.write(b' '.join(text).decode('utf-8') + '\n')            texts_num += 1            if texts_num % 10000 == 0:                logging.info("已處理 %d 篇文章" % texts_num)if __name__ == "__main__":    main()

在shell 裡輸入:

1
python3 wiki_to_txt.py zhwiki-20160820-pages-articles.xml.bz2

如果你的資料不是 8 月 20 號的備份,記得把zhwiki-20160820-pages-articles.xml.bz2換成你的備份的檔名唷。這約需花費 20 分鐘來處理,就讓我們先看一下接下來還要做些什麼吧~

開始斷詞

我們有清完標籤的語料了,第二件事就是要把語料中每個句子,進一步拆解成一個一個詞,這個步驟稱為「斷詞」。中文斷詞的工具比比皆是,這裏我採用的是 jieba,儘管它在繁體中文的斷詞上還是有些不如CKIP,但他實在太簡單、太方便、太好調用了,足以彌補這一點小缺憾:

12
快速安裝結巴pip3 install jieba
12345678
# 斷詞示例import jiebaseg_list = jieba.cut("我来到北京清华大学", cut_all=False)print("Default Mode: " + "/ ".join(seg_list))  # 精确模式#輸出 Default Mode: 我/ 来到/ 北京/ 清华大学

現在,我們上一階段的檔案也差不多出爐了,以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

1234567891011121314151617181920212223242526272829303132333435
# -*- coding: utf-8 -*-import jiebaimport loggingdef main():    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)    # jieba custom setting.    jieba.set_dictionary('jieba_dict/dict.txt.big')    # load stopwords set    stopwordset = set()    with open('jieba_dict/stopwords.txt','r',encoding='utf-8') as sw:        for line in sw:            stopwordset.add(line.strip('\n'))    output = open('wiki_seg.txt','w')        texts_num = 0        with open('wiki_zh_tw.txt','r') as content :        for line in content:            words = jieba.cut(line, cut_all=False)            for word in words:                if word not in stopwordset:                    output.write(word +' ')            texts_num += 1            if texts_num % 10000 == 0:                logging.info("已完成前 %d 行的斷詞" % texts_num)    output.close()    if __name__ == '__main__':main()

Stopwords and Window

好啦,這個東西大概要跑個 80 分鐘,先讓我講些幹話,先讓我們看看上頭做了什麼。除了之前演示的斷詞外,這邊還多做了兩件事,一是調整jieba的辭典,讓他對繁體斷詞比較友善,二是引入了停用詞,停用詞就是像英文中的 the,a,this,中文的你我他,與其他詞相比顯得不怎麼重要,對文章主題也無關緊要的,就可以將它視為停用詞。而要排除停用詞的理由,其實與word2vec的實作概念大大相關,由於在開頭講明了不深究概念,就讓我舉個例子替代長篇大論。

首先,在word2vec有一個概念叫 windows,我習慣叫他窗口,因為它給我的感覺跟TCP 那個會滑來滑去的東東很像。

Word2Vec

很顯然,一個詞的意涵跟他的左右鄰居很有關係,比如「雨越下越大,茶越充越淡」,什麼會「下」?「雨」會下,什麼會「淡」?茶會「淡」,這樣的類比舉不勝舉,那麼,若把思維逆轉過來呢?

逆轉

顯然,我們或多或少能從左右鄰居是誰,猜出中間的是什麼,這很像我們國高中時天天在練的英文克漏字。那麼問題來了,左右鄰居有誰?能更精確地說,你要往左往右看幾個?假設我們以「孔乙己 一到 店 所有 喝酒 的 人 便都 看著 他 笑」為例,如果往左往右各看一個:

12345
[孔乙己 一到] 店 所有 喝酒 的 人 便 都 看著 他 笑[孔乙己 一到 店] 所有 喝酒 的 人 便 都 看著 他 笑孔乙己 [一到 店 所有] 喝酒 的 人 便 都 看著 他 笑孔乙己 一到 [店 所有 喝酒] 的 人 便 都 看著 他 笑......

這樣就構成了一個 size=1 的 windows,這個 1 是極端的例子,為了讓我們看看有停用詞跟沒停用詞差在哪,這句話去除了停用詞應該會變成:

1
孔乙己 一到 店 所有 喝酒 人 看著 笑

我們看看「人」的窗口變化,原本是「的 人 便」,後來是「喝酒 人 看著」,相比原本的情形,去除停用詞後,我們對「人」這個詞有更多認識,比如人會喝酒,人會看東西,當然啦,這是我以口語的表達,機器並不會這麼想,機器知道的是人跟喝酒會有某種關聯,跟看會有某種關聯,但儘管如此,也遠比本來的「的 人 便」好太多太多了。

就在剛剛,我的斷詞已經跑完了,現在,讓我們進入收尾的階段吧

1
2016-08-26 22:27:59,480 : INFO : 已處理 260000 個 token

訓練詞向量

這是最簡單的部分,同時也是最困難的部分,簡單的是程式碼,困難的是詞向量效能上的微調與後訓練。對了,如果你已經對詞向量和語言模型有些研究,在輸入python3 train.py之前,建議先看一下之後的內文,相信我,你會需要的。

12345678910111213141516171819
# -*- coding: utf-8 -*-from gensim.models import word2vecimport loggingdef main():    logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)    sentences = word2vec.Text8Corpus("wiki_seg.txt")    model = word2vec.Word2Vec(sentences, size=250)    # Save our model.    model.save("med250.model.bin")    # To load a model.    # model = word2vec.Word2Vec.load("your_model.bin")if __name__ == "__main__":    main()

扣掉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 背後的原理後,也可以試著調調hsnegative,看看對效能會有什麼影響。

詞向量實驗

訓練完成後,讓我們來測試一下模型的效能,運行python3 demo.py。由於 gensim 將整個模型讀了進來,所以記憶體會消耗相當多,如果出現了MemoryError,可能得調整一下min_count或對常用詞作一層快取,這點要注意一下。

先來試試相似詞排序吧!

123456789101112131415161718192021222324252627282930313233343536373839404142
飲料相似詞前 100 排序飲品,0.8439314365386963果汁,0.7858869433403015罐裝,0.7305712699890137冰淇淋,0.702262818813324酸奶,0.7007108926773071口香糖,0.6987590193748474酒類,0.6967358589172363可口可樂,0.6885123252868652酒精類,0.6843742728233337含酒精,0.6825539469718933啤酒,0.6816493272781372薯片,0.6779764294624329紅茶,0.6656282544136047奶茶,0.656740128993988提神,0.6566425561904907牛奶,0.6556192636489868檸檬茶,0.6494661569595337籃球相似詞前 100 排序美式足球,0.6463411450386047橄欖球,0.6382837891578674男子籃球,0.6187020540237427冰球,0.6056296825408936棒球,0.5859025716781616籃球運動,0.5831792950630188籃球員,0.5782726407051086籃球隊,0.576259195804596排球,0.5743488073348999黑子,0.5609416961669922籃球比賽,0.5498511791229248打球,0.5496408939361572中國籃球,0.5471529960632324男籃,0.5460700392723083ncaa,0.543986439704895投投,0.5439497232437134曲棍球,0.5435376167297363nba,0.5415610671043396

前100太多了,所以只把前幾個結果貼上來,我們也能調用model.similarity(word2,word1)來直接取得兩個詞的相似度:

123456789101112131415
冰沙 刨冰計算 Cosine 相似度0.631961417455電腦 飛鏢計算 Cosine 相似度0.154503715708電腦 程式計算 Cosine 相似度0.5021829415衛生紙 漫畫計算 Cosine 相似度0.167776641495

能稍微區隔出詞與詞之間的主題,整體來說算是可以接受的了。

更上一層樓

如何優化詞向量的表現?這其實有蠻多方法的,大方向是從應用的角度出發,我們能針對應用特化的語料進行再訓練,除此之外,斷詞器的選擇也很重要,它很大程度的決定什麼詞該在什麼地方出現,如果發現 jieba 有些力不能及的,不妨試著採用別的斷詞器,或是試著在 jieba 自訂辭典,調一下每個詞的權重。

應用考慮好了,接著看看模型,我們可以調整 model() 的參數,比方窗口大小、維度、學習率,進一步還能比較 skip-gram 與 cbow 的效能差異,什麼,你說不知道 skip-gram 跟 cbow 是什麼?且看下回分解。

參考資料

Training Word2Vec Model on English Wikipedia by Gensim

原创粉丝点击