Gensim官方教程翻譯(二)——語料庫與向量空間(Corpora and Vector Spaces)
====================正==========文====================
如果你想記錄日誌,請不要忘記設定:
>>> import logging
>>> logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
從字串到向量
這次,讓我們從用字串表示的文件開始:
>>> from gensim import corpora, models, similarities >>> >>> documents = ["Human machine interface for lab abc computer applications", >>> "A survey of user opinion of computer system response time", >>> "The EPS user interface management system", >>> "System and human system engineering testing of EPS", >>> "Relation of user perceived response time to error measurement", >>> "The generation of random binary unordered trees", >>> "The intersection graph of paths in trees", >>> "Graph minors IV Widths of trees and well quasi ordering", >>> "Graph minors A survey"]
這是一個由9篇文件組成的微型語料庫,每個文件僅有一個句子組成。
(記號化 or tokenize)
首先,讓我們對這些文件進行記號化(tokenize,或稱標記化等)處理,遮蔽常用詞(利用停用詞表)和整個語料庫中僅僅出現一次的詞:
>>> # 去除停用詞並分詞 >>> # 譯者注:這裡只是例子,實際上還有其他停用詞 >>> # 處理中文時,請藉助 Py結巴分詞 https://github.com/fxsjy/jieba >>> stoplist = set('for a of the and to in'.split()) >>> texts = [[word for word in document.lower().split() if word not in stoplist] >>> for document in documents] >>> >>> # 去除僅出現一次的單詞 >>> from collections import defaultdict >>> frequency = defaultdict(int) >>> for text in texts: >>> for token in text: >>> frequency[token] += 1 >>> >>> texts = [[token for token in text if frequency[token] > 1] >>> for text in texts] >>> >>> from pprint import pprint # pretty-printer >>> pprint(texts) [['human', 'interface', 'computer'], ['survey', 'user', 'computer', 'system', 'response', 'time'], ['eps', 'user', 'interface', 'system'], ['system', 'human', 'system', 'eps'], ['user', 'response', 'time'], ['trees'], ['graph', 'trees'], ['graph', 'minors', 'trees'], ['graph', 'minors', 'survey']]
在這裡我僅利用空格切分字串來記號化,並將它們都轉成小寫;而你處理文件的方式很可能會有所不同。實際上,我是在用這個特殊(簡單且效率低)的設定來模仿Deerwester等人的原始LSA文章中的實驗。[1]
(總結屬性字典)
處理文件的方法應該視應用情形、語言而定,因此我決定不對處理方法做任何的限制。取而代之的是,一個文件必須由其中提取出來的屬性表示,而不僅僅是其字面形式;如何提取這些屬性由你來決定(可以是單詞、文件長度數量等)。接下來,我將描述一個通用的、常規目的的方法(稱為詞袋),但是請記住不同應用領域應使用不同的屬性。如若不然,渣進滓出(garbage
in, garbage out)。
“‘系統’這個單詞出現了多少次?1次。”
我們最好用這些問題的(整數)編號來代替這些問題。問題與編號之間的對映,我們稱其為字典(Dictionary)。
>>> dictionary = corpora.Dictionary(texts)
>>> dictionary.save('/tmp/deerwester.dict') # 把字典儲存起來,方便以後使用
>>> print(dictionary)
Dictionary(12 unique tokens)
上面這些步驟,我們利用gensim.corpora.dictionary.Dictionary類為每個出現在語料庫中的單詞分配了一個獨一無二的整數編號。這個操作收集了單詞計數及其他相關的統計資訊。在結尾,我們看到語料庫中有12個不同的單詞,這表明每個文件將會用12個數字表示(即12維向量)。如果想要檢視單詞與編號之間的對映關係:
>>> print(dictionary.token2id)
{'minors': 11, 'graph': 10, 'system': 5, 'trees': 9, 'eps': 8, 'computer': 0,
'survey': 4, 'user': 7, 'human': 1, 'time': 6, 'interface': 2, 'response': 3}
(產生稀疏文件向量)
為了真正將記號化的文件轉換為向量,需要:
>>> new_doc = "Human computer interaction"
>>> new_vec = dictionary.doc2bow(new_doc.lower().split())
>>> print(new_vec) # "interaction"沒有在dictionary中出現,因此忽略
[(0, 1), (1, 1)]
函式doc2bow()簡單地對每個不同單詞的出現次數進行了計數,並將單詞轉換為其編號,然後以稀疏向量的形式返回結果。因此,稀疏向量[(0, 1), (1, 1)]表示:在“Human computer interaction”中“computer”(id 0) 和“human”(id 1)各出現一次;其他10個dictionary中的單詞沒有出現過(隱含的)。
>>> corpus = [dictionary.doc2bow(text) for text in texts]
>>> corpora.MmCorpus.serialize('/tmp/deerwester.mm', corpus) # 存入硬碟,以備後需
>>> print(corpus)
[(0, 1), (1, 1), (2, 1)]
[(0, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)]
[(2, 1), (5, 1), (7, 1), (8, 1)]
[(1, 1), (5, 2), (8, 1)]
[(3, 1), (6, 1), (7, 1)]
[(9, 1)]
[(9, 1), (10, 1)]
[(9, 1), (10, 1), (11, 1)]
[(4, 1), (10, 1), (11, 1)]
(通過上面的操作,我們看到了這次我們得到的語料庫。)到現在為止,我們應該明確,上面的輸出表明:對於前六個文件來說,編號為10的屬性值為0表示問題“文件中‘graph’出現了幾次”的答案是“0”;而其他文件的答案是1。事實上,我們得到了《快速入門》中的示例語料庫。
語料庫流——一次一個文件
需要注意的是,上面的語料庫整個作為一個Python List存在了記憶體中。在這個簡單的例子中,這當然無關緊要。但是我們因該清楚,假設我們有一個百萬數量級文件的語料庫,我們不可能將整個語料庫全部存入記憶體。假設這些文件存在一個硬碟上的檔案中,每行一篇文件。Gemsim僅要求一個語料庫可以每次返回一個文件向量:>>> class MyCorpus(object):
>>> def __iter__(self):
>>> for line in open('mycorpus.txt'):
>>> # assume there's one document per line, tokens separated by whitespace
>>> yield dictionary.doc2bow(line.lower().split())
請在這裡下載示例檔案mycorpus.txt。
這裡假設的在一個單獨的檔案中每個文件佔一行不是十分重要;你可以改造 __iter__ 函式來適應你的輸入格式,無論你的輸入格式是什麼樣的,例如遍歷資料夾、解析XML、訪問網路等等。你僅需在每個文件中解析出一個由記號(tokens)組成的乾淨列表,然後利用dictionary將這些符號轉換為其id,最後在__iter__函式中產生一個稀疏向量即可。>>> corpus_memory_friendly = MyCorpus() # 沒有將整個語料庫載入記憶體
>>> print(corpus_memory_friendly)
<__main__.MyCorpus object at 0x10d5690>
現在的語料庫是一個物件。我們沒有定義任何列印它的方法,所以僅能列印該物件在記憶體中的地址,對我們沒什麼幫助。為了查看向量的組成,讓我們通過迭代的方式取出語料庫中的每個文件向量(一次一個)並列印:>>> for vector in corpus_memory_friendly: # 一次讀入記憶體一個向量
... print(vector)
[(0, 1), (1, 1), (2, 1)]
[(0, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1)]
[(2, 1), (5, 1), (7, 1), (8, 1)]
[(1, 1), (5, 2), (8, 1)]
[(3, 1), (6, 1), (7, 1)]
[(9, 1)]
[(9, 1), (10, 1)]
[(9, 1), (10, 1), (11, 1)]
[(4, 1), (10, 1), (11, 1)]
雖然輸出與普通的Python List一樣,但是現在的語料庫對記憶體更加友好,因為一次最多隻有一個向量寄存於記憶體中。你的語料庫現在可以想多大就多大啦!相似的,為了構造dictionary我們也不必將全部文件讀入記憶體:
>>> # 收集所有符號的統計資訊
>>> dictionary = corpora.Dictionary(line.lower().split() for line in open('mycorpus.txt'))
>>> # 收集停用詞和僅出現一次的詞的id
>>> stop_ids = [dictionary.token2id[stopword] for stopword in stoplist
>>> if stopword in dictionary.token2id]
>>> once_ids = [tokenid for tokenid, docfreq in dictionary.dfs.iteritems() if docfreq == 1]
>>> dictionary.filter_tokens(stop_ids + once_ids) # 刪除停用詞和僅出現一次的詞
>>> dictionary.compactify() # 消除id序列在刪除詞後產生的不連續的缺口
>>> print(dictionary)
Dictionary(12 unique tokens)
這就是你需要為他準備的所有,至少從詞袋模型的角度考慮是這樣的。當然,我們用該語料庫做什麼事另外一個問題,我們並不清楚計算不同單詞的詞頻是否真的有用。事實證明,它確實也沒有什麼用,我們將需要首先對這種簡單的表示方法進行一個轉換,才能計算出一些有意義的文件及文件相似性。轉換的內容將會在下個教程講解,在這之前,讓我們暫時將注意力集中到語料庫持久上來。
各種語料庫格式
(儲存語料庫)
我們有幾種檔案格式來序列化一個向量空間語料庫(~向量序列),並存到硬碟上。Gemsim通過之前提到的語料庫流介面實現了這些方法,用一個惰性方式來將文件從硬碟中讀出(或寫入)。一次一個文件,不會將整個語料庫讀入主記憶體。
所有的語料庫格式中,一種非常出名的檔案格式就是
Market Matrix格式。想要將語料庫儲存為這種格式:
>>> from gensim import corpora
>>> # 建立一個玩具級的語料庫
>>> corpus = [[(1, 0.5)], []] # 讓一個文件為空,作為它的heck
>>>
>>> corpora.MmCorpus.serialize('/tmp/corpus.mm', corpus)
>>> corpora.SvmLightCorpus.serialize('/tmp/corpus.svmlight', corpus)
>>> corpora.BleiCorpus.serialize('/tmp/corpus.lda-c', corpus)
>>> corpora.LowCorpus.serialize('/tmp/corpus.low', corpus)
(載入語料庫)
相反地,從一個Matrix Market檔案載入語料庫:
>>> corpus = corpora.MmCorpus('/tmp/corpus.mm')
語料庫物件是流式的,因此你不能直接將其打印出來
>>> print(corpus)
MmCorpus(2 documents, 2 features, 1 non-zero entries)
如果你真的特別想看看語料庫的內容,也不是沒有辦法:
>>> # 將語料庫全部匯入記憶體的方法
>>> print(list(corpus)) # 呼叫list()將會把所有的序列轉換為普通Python List
[[(1, 0.5)], []]
或者
>>> # 另一種利用流介面,一次只打印一個文件
>>> for doc in corpus:
... print(doc)
[(1, 0.5)]
[]
第二種方法顯然更加記憶體友好,但是如果只是為了測試與開發,沒有什麼比呼叫list()更簡單了。(*^_^*)
(轉存語料庫)
想將這個 Matrix Market格式的語料庫存為Blei’s LDA-C格式,你只需:
>>> corpora.BleiCorpus.serialize('/tmp/corpus.lda-c', corpus)
這種方式,gensim可以被用作一個記憶體節約型的I/O格式轉換器:你只要用一種檔案格式流載入語料庫,然後直接儲存成其他格式就好了。增加一種新的格式簡直是太容易了,請參照我們為SVMlight語料庫設計的程式碼。
與NumPy和SciPy的相容性
Gensim包含了許多高效的工具函式來幫你實現語料庫與numpy矩陣之間互相轉換:
>>> corpus = gensim.matutils.Dense2Corpus(numpy_matrix)
>>> numpy_matrix = gensim.matutils.corpus2dense(corpus, num_terms=number_of_corpus_features)
以及語料庫與scipy稀疏矩陣之間的轉換:
>>> corpus = gensim.matutils.Sparse2Corpus(scipy_sparse_matrix)
>>> scipy_csc_matrix = gensim.matutils.corpus2csc(corpus)
想要更全面的參考(例如,壓縮詞典的大小、優化語料庫與NumPy/SciPy陣列的轉換),參見API文件,或者繼續閱讀下一篇《主題與轉換》教程。
==================================================