Gensim官方教程翻译

转载 2019-05-16 21:46  阅读 115 次 评论 0 条

为了方便自己学习,翻译了官方的教程,原文:http://radimrehurek.com/gensim/tutorial.html。

本教程按照一系列的实例组织,用以突出gensim的各种特征。本教程的受众是熟悉Python,已经安装了gensim,而且阅读过介绍的读者。

本教程包括为以下几个部分:

  • 语料库与向量空间
    • 从字符串到向量
    • 语料库流-一次一个文档
    • 语料库格式
    • 与NumPy和SciPy的兼容性
  • 主题与转换
    • 转换接口
    • 可用的转换
  • 相似性查询
    • 相似性接口
    • 接下来做什么?
  • 对于英文维基百科的实验
    • 准备语料库
    • 潜在语义分析
    • 隐含狄利克雷分配
  • 分布式计算
    • 为什么需要分布式计算?
    • 先决条件
    • 核心概念
    • 可用的分布式算法

预备

所有的例子都可以复制到你的Python解释器窗口。IPython的cpaste命令对于复制-粘贴代码片段十分方便,包括开头无意义的“>>>”字符。

Gensim使用Python标准的日志类来记录不同优先级的各种事件,想要激活日志(可选的),运行如下代码:

import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

快速入门

首先,让我们导入gensim并创建一个小小的语料库,其中有9篇文档和12个属性[1]:

from gensim import corpora, models, similarities
 
corpus = [[(0, 1.0), (1, 1.0), (2, 1.0)],
[(2, 1.0), (3, 1.0), (4, 1.0), (5, 1.0), (6, 1.0), (8, 1.0)],
[(1, 1.0), (3, 1.0), (4, 1.0), (7, 1.0)],
[(0, 1.0), (4, 2.0), (7, 1.0)],
[(3, 1.0), (5, 1.0), (6, 1.0)],
[(9, 1.0)],
[(9, 1.0), (10, 1.0)],
[(9, 1.0), (10, 1.0), (11, 1.0)],
[(8, 1.0), (10, 1.0), (11, 1.0)]]

语料库是一个简单的对象,我们能够通过不断迭代从中取出用稀疏向量代表的文档。如果你不熟悉向量空间模型,我们将会在下一个教程《语料库与向量空间》填平原始字符串、语料库、稀疏向量之间的鸿沟。

如果你熟悉向量空间模型,你可能也知道如何将你的文档解析、转换为向量,并且也知道这些转换对接下来的应用的会产生什么影响。

作者注:在这个例子中我们的整个语料作为一个Python List被存在内存中。但是,语料库接口仅仅规定一个语料库必须支持迭代取出其文档。对于特别大的语料库,最好将整个语料库存在硬盘上,并且按序一次取出一篇文档。所有的操作和转换通过一种语料库大小无关的方式实现,内存依赖较低。

接下来,让我们初始化一个转换

tfidf = models.TfidfModel(corpus)

转换就是将文档的一种向量表示方法转换为另一种(以便我们从特定的角度更好地分析数据):

vec = [(0, 1), (4, 1)]
print(tfidf[vec])
#[(0, 0.8075244), (4, 0.5898342)]

在此,我们使用了Tf-Idf,这是一种简单的转换。它要求输入的文档用带有词频的词袋的方法表示,可以用来降低常用词的权重(相对地提高了罕见词的权重)。它还会把结果向量的长度调整为单位长度(指欧几里得范数)。

转化方法详情,请看教程《主题与转换》

为了将整个语料库通过Tf-idf转化并索引,以便相似度查询,需要做如下准备:

index = similarities.SparseMatrixSimilarity(tfidf[corpus], num_features=12)

为了查询我们需要的向量vec相对于其他所有文档的相似度,需要:

sims = index[tfidf[vec]]
print(list(enumerate(sims)))
#[(0, 0.4662244), (1, 0.19139354), (2, 0.24600551), (3, 0.82094586), (4, 0.0), (5, 0.0), (6, 0.0), (7, 0.0), (8, 0.0)]

如何解释这些输出呢?文档0(第1个文档)与vec的相似度为0.466=46.6%,第二个文档与vec的相似度为19.1,依次类推。 因此,根据Tfidf文档表示方法和余弦相似度方法,与我们的查询文档vec最相似的文档为3号文档,相似度达到82.1%。注意:4-8号文档与vec没有任何公共的属性,因此相似度为0.0。

如果你想记录日志,请不要忘记设置:

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)
其他格式还有Joachim’s SVMlight、Blei’s LDA-C、GibbsLDA++等:
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)

转换接口

在之前的教程《语料库与向量空间》中,我们创建了一个用向量流表示文档的语料库。为了继续征程,让我们启动gensim并使用该语料库。

from gensim import corpora, models, similarities
dictionary = corpora.Dictionary.load('/tmp/deerwester.dict')
corpus = corpora.MmCorpus('/tmp/deerwester.mm')
print(corpus)
MmCorpus(9 documents, 12 features, 28 non-zero entries)

在本次教程中,我将会向你展示如何将文档从一种向量表示方式转换到另一种。这个处理是为了两个目的:

将语料库中隐藏的结构发掘出来,发现词语之间的关系,并且利用这些结构、关系使用一种新的、更有语义价值的(这是我们最希望的)方式描述其中的文档。

使得表示方式更加简洁。这样不仅能提高效率(新的表示方法一般消耗较少的资源)还能提高效果(忽略了边际数据趋势、降低了噪音)。

创建一个转换

转换(transformations)是标准的Python类,通常通过训练语料库的方式初始化

tfidf = models.TfidfModel(corpus) # 第一步 -- 初始化一个模型

我们使用了前一个教程中用过的语料库来初始化(训练)这个转换模型。不同的转换可能需要不同的初始化参数;在Tfidf案例中,“训练”仅仅是遍历提供的语料库然后计算所有属性的文档频率(译者注:在多少文档中过)。训练其他模型,例如潜在语义分析或隐含狄利克雷分配,更加复杂,因此耗时也多。

作者注:转换常常是在两个特定的向量空间之间进行。训练与后续的转换必须使用相同的向量空间(=有相同的属性,且编号相同)。如若不然,例如使用不同的预处理方法处理字符串、使用不同的属性编号、需要Tfidf向量的时候却输入了词袋向量,将会导致转换过程中属性匹配错误,进而使输出结果无意义并可能引发异常。

转换向量

从现在开始,tfidf将被视为只读的对象,可以用它来转换将任何采用旧表示方法的向量(词袋整数计数)转换为新的表示方法(Tfidf 实数权重):

doc_bow = [(0, 1), (1, 1)]
print(tfidf[doc_bow]) # 第二步 -- 使用模型转换向量
[(0, 0.70710678), (1, 0.70710678)]

或者对整个语料库实施转换:

corpus_tfidf = tfidf[corpus]
for doc in corpus_tfidf:
print(doc)
'''
[(0, 0.57735026918962573), (1, 0.57735026918962573), (2, 0.57735026918962573)]
[(0, 0.44424552527467476), (3, 0.44424552527467476), (4, 0.44424552527467476), (5, 0.32448702061385548), (6, 0.44424552527467476), (7, 0.32448702061385548)]
[(2, 0.5710059809418182), (5, 0.41707573620227772), (7, 0.41707573620227772), (8, 0.5710059809418182)]
[(1, 0.49182558987264147), (5, 0.71848116070837686), (8, 0.49182558987264147)]
[(3, 0.62825804686700459), (6, 0.62825804686700459), (7, 0.45889394536615247)]
[(9, 1.0)]
[(9, 0.70710678118654746), (10, 0.70710678118654746)]
[(9, 0.50804290089167492), (10, 0.50804290089167492), (11, 0.69554641952003704)]
[(4, 0.62825804686700459), (10, 0.45889394536615247), (11, 0.62825804686700459)]
'''

在这个特殊的情况中,被转换的语料库与用来训练的语料库相同,但是这仅仅是偶然。一旦转换模型被初始化了,它可以用来转换任何向量(当然最好使用与训练语料库相同的向量空间 | 注:即语言环境),即使它们并没有在训练语料库中出现。这是通过潜在语义分析的调入(folding in)、隐含狄利克雷分配的主题推断(topic inference)等得到的。

调用model[corpus]只能在旧的corpus文档流的基础上创建一个包装-真正的转化是在迭代文档时即时计算的。我们可以通过调用corpus_transformed = model[corpus]一次转化整个语料库,因为这样意味着我们要将结果存入内存中,这与gensim内存无关的设计理念不太符合。如果你还要多次迭代转化后的corpus_transformed,负担将会十分巨大。你可以先将结果序列化并存储到硬盘上再做需要的操作。

转换也可以被序列化,还可以一个(转换)叠另一个,像一串链条一样:

lsi = models.LsiModel(corpus_tfidf, id2word=dictionary, num_topics=2) # 初始化一个LSI转换
corpus_lsi = lsi[corpus_tfidf] # 在原始语料库上加上双重包装: bow->tfidf->fold-in-lsi

这里我们利用潜在语义索引(LSI) 将Tf-Idf语料转化为一个潜在2-D空间(2-D是因为我们设置了num_topics=2)。现在你可能想知道:2潜在维度意味着什么?让我们利用models.LsiModel.print_topics()来检查一下这个过程到底产生了什么变化吧:

lsi.print_topics(2)
'''
topic #0(1.594): -0.703*"trees" + -0.538*"graph" + -0.402*"minors" + -0.187*"survey" + -0.061*"system" + -0.060*"response" + -0.060*"time" + -0.058*"user" + -0.049*"computer" + -0.035*"interface"
topic #1(1.476): -0.460*"system" + -0.373*"user" + -0.332*"eps" + -0.328*"interface" + -0.320*"response" + -0.320*"time" + -0.293*"computer" + -0.280*"human" + -0.171*"survey" + 0.161*"trees"
'''

(这些主题将会记录在日志中,想要了解如何激活日志,请看开头的注解)

根据LSI来看,“tree”、“graph”、“minors”都是相关的词语(而且在第一主题的方向上贡献最多),而第二主题实际上与所有的词语都有关系。如我们所料,前五个文档与第二个主题的关联更强,而其他四个文档与第一个主题关联最强:

for doc in corpus_lsi: # both bow->tfidf and tfidf->lsi transformations are actually executed here, on the fly
print(doc)
'''
[(0, -0.066), (1, 0.520)] # "Human machine interface for lab abc computer applications"
[(0, -0.197), (1, 0.761)] # "A survey of user opinion of computer system response time"
[(0, -0.090), (1, 0.724)] # "The EPS user interface management system"
[(0, -0.076), (1, 0.632)] # "System and human system engineering testing of EPS"
[(0, -0.102), (1, 0.574)] # "Relation of user perceived response time to error measurement"
[(0, -0.703), (1, -0.161)] # "The generation of random binary unordered trees"
[(0, -0.877), (1, -0.168)] # "The intersection graph of paths in trees"
[(0, -0.910), (1, -0.141)] # "Graph minors IV Widths of trees and well quasi ordering"
[(0, -0.617), (1, 0.054)] # "Graph minors A survey"
'''

模型的持久可以借助save()和load()函数完成:

lsi.save('/tmp/model.lsi') # same for tfidf, lda, ...
lsi = models.LsiModel.load('/tmp/model.lsi')

下一个问题可能就该是:这些文档之间确切的相似度是多少呢?能否将相似性形式化,以便给定一个文档,我们能够根据其他文档与该文档的相似度排序呢?敬请阅读下个教程——《相似度查询》。

可用的转换

Gensim实现了几种常见的向量空间模型算法:

词频-逆文档频(Term Frequency * Inverse Document Frequency, Tf-Idf)

需要一个词袋形式(整数值)的训练语料库来实现初始化。转换过程中,他将会接收一个向量同时返回一个相同维度的向量,在语料库中非常稀有的属性的权重将会提高。因此,他会将整数型的向量转化为实数型的向量,同时让维度不变。而且。你可以选择是否将返回结果标准化至单位长度(欧几里得范数)。

 model = tfidfmodel.TfidfModel(bow_corpus, normalize=True)

潜在语义索引(Latent Semantic Indexing,LSI,or sometimes LSA)

将文档从词袋或TfIdf权重空间(更好)转化为一个低维的潜在空间。对于我们上面用到的玩具级的语料库,我们使用了2潜在维度,但是在真正的语料库上,推荐200-500的目标维度为“金标准”。[1]

model = lsimodel.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=300)

LSI训练的独特之处是我们能在任何继续“训练”,仅需提供更多的训练文本。这是通过对底层模型进行增量更新,这个过程被称为“在线训练”。正因为它的这个特性,输入文档流可以是无限大——我们能在以只读的方式使用计算好的模型的同时,还能在新文档到达时一直“喂食”给LSI“消化”!

model.add_documents(another_tfidf_corpus) # 现在LSI已经使用tfidf_corpus + another_tfidf_corpus进行过训练了
lsi_vec = model[tfidf_vec] # 将新文档转化到LSI空间不会影响该模型
...
model.add_documents(more_documents) # tfidf_corpus + another_tfidf_corpus + more_documents
lsi_vec = model[tfidf_vec]
...

有关在无限大的流中,如何让LSI逐渐“忘记”旧的观测结果,详情请看gensim.models.lsimodel的帮助文档。如果你不怕麻烦,有几个参数可以控制LSI算法的影响速度、内存占用和数值精度等。

gensim使用一个新颖的在线增量流分布式训练算法(还挺拗口的…),我曾将该方法发表在[5]中。gensim内部执行了一个来自Halko等[4]的随机多通道算法(stochastic multi-pass algorithm)来加速核心(in-core)部分的计算。参考《在英文维基百科上的实验》教程了解如何通过计算机集群分布式计算来提高速度。

随机映射(Random Projections,RP)

目的在于减小空维度。这是一个非常高效(对CPU和内存都很友好)方法,通过抛出一点点随机性,来近似得到两个文档之间的Tfidf距离。推荐目标维度也是成百上千,具体数值要视你的数据集大小而定。

model = rpmodel.RpModel(tfidf_corpus, num_topics=500)

隐含狄利克雷分配(Latent Dirichlet Allocation, LDA)

也是将词袋计数转化为一个低维主题空间的转换。LDA是LSA(也叫多项式PCA)的概率扩展,因此LDA的主题可以被解释为词语的概率分布。这些分布式从训练语料库中自动推断的,就像LSA一样。相应地,文档可以被解释为这些主题的一个(软)混合(又是就像LSA一样)。

model = ldamodel.LdaModel(bow_corpus, id2word=dictionary, num_topics=100)

gensim使用一个基于[2]的快速的在线LDA参数估计实现,修改并使其可以在计算机集群上以分布式模式运行。

分层狄利克雷过程(Hierarchical Dirichlet Process,HDP)

是一个无参数贝叶斯方法(注意:这里没有num_topics参数):

model = hdpmodel.HdpModel(bow_corpus, id2word=dictionary)

gensim使用一种基于[3]的快速在线来实现。该算法是新加入gensim的,并且还是一种粗糙的学术边缘产品——小心使用。

增加新的VSM转化(例如新的权重方案)相当平常;参见API参考或者直接参考我们的源代码以获取信息与帮助。

值得一提的是,这些模型增量模型,无需一次将所有的训练语料库全部放到内存中。在关心内存的同时,我还在不断改进分布式计算,来提高CPU效率。如果你自觉能够贡献一份力量(测试、提供用例或代码),请让我知道。

相似度接口

在之前的教程《语料库与向量空间》和《主题与转换》中,我们了解了创建在向量空间创建一个语料库意味着什么,如何在不同的向量空间之间转换。我们所做的一切都是为了一个共同目标:决定文档对之间的相似度或者一篇特定文档和其他文档之间的相似度(例如用户输入与已索引文档)。

为了展示如何在gensim中做这项工作,让我们使用与之前相同的语料库(这个语料库真的来自于1990年的Deerwester等的《 Indexing by Latent Semantic Analysis》):

from gensim import corpora, models, similarities
dictionary = corpora.Dictionary.load('/tmp/deerwester.dict')
corpus = corpora.MmCorpus('/tmp/deerwester.mm') # comes from the first tutorial, "From strings to vectors"
print(corpus)
#MmCorpus(9 documents, 12 features, 28 non-zero entries)

为了模仿Deerwester的例子,我们首先用这个微型语料库来定义一个2维LSI空间:

lsi = models.LsiModel(corpus, id2word=dictionary, num_topics=2)

现在假设一个用户键入查询“Human computer interaction”,我们应该对我们的九个语料库文档按照与该输入的关联性逆向排序。与现代搜索引擎不同,我们仅集中关注一个单一方面的可能的相似性——文本(单词)的语义关联性。没有超链接,没有随机行走静态排列,只是在关键词布尔匹配的基础上进行了语义扩展。

doc = "Human computer interaction"
vec_bow = dictionary.doc2bow(doc.lower().split())
vec_lsi = lsi[vec_bow] # convert the query to LSI space
print(vec_lsi)
#[(0, -0.461821), (1, 0.070028)]

此外,我们将会考虑余弦相似性来决定两个向量的相似性。余弦相似性是一种向量空间模型的标准方法,但是对于表示概率分布的向量,其他相似度方法可能更好。

初始化查询结构

为了准备相似度查询,我们需要输入所有我们我们需要比较的文档。在本例中,他们是LSI训练中用到的被转换到2-D的LSA空间的9个文档。但是这只是偶然,我们也可能索引完全不同的语料库。

index = similarities.MatrixSimilarity(lsi[corpus]) # transform corpus to LSI space and index it

警告:similarities.MatrixSimilarity类仅仅适合能将所有的向量都在内存中的情况。例如,如果一个百万文档级的语料库使用该类,可能需要2G内存与256维LSI空间。
如果没有足够的内存,你可以使用similarities.Similarity类。该类的操作只需要固定大小的内存,因为他将索引切分为多个文件(称为碎片)存储到硬盘上了。它实际上使用了similarities.MatrixSimilarity和similarities.SparseMatrixSimilarity两个类,因此它也是比较快的,虽然看起来更加复杂了。

索引也可以通过标准的save()和load()函数来存储到硬盘上。

index.save('/tmp/deerwester.index')
index = similarities.MatrixSimilarity.load('/tmp/deerwester.index')

所有的索引类都可以用这种方法(similarities.Similarity,similarities.MatrixSimilarity和similarities.SparseMatrixSimilarity)。下面也是,索引可以是任何一个索引类。如果有疑问,可以使用similarities.Similarity,因为它是最具扩展性的版本,它也支持之后增加更多的文档索引。

实施查询

为了获得我们的查询文档相对于其他9个经过索引的文档的相似度:

sims = index[vec_lsi] # perform a similarity query against the corpus
print(list(enumerate(sims))) # print (document_number, document_similarity) 2-tuples
[(0, 0.99809301), (1, 0.93748635), (2, 0.99844527), (3, 0.9865886), (4, 0.90755945),
(5, -0.12416792), (6, -0.1063926), (7, -0.098794639), (8, 0.05004178)]

余弦方法返回的相似度在-1~1之间(越大越相似),所以第一个文档分数为0.99809301等。

使用一些标准Python函数,我们将这些相似性倒序排列,并且获得了查询“Human computer interaction”的结果。

sims = sorted(enumerate(sims), key=lambda item: -item[1])
print(sims) # print sorted (document number, similarity score) 2-tuples
'''
[(2, 0.99844527), # The EPS user interface management system
(0, 0.99809301), # Human machine interface for lab abc computer applications
(3, 0.9865886), # System and human system engineering testing of EPS
(1, 0.93748635), # A survey of user opinion of computer system response time
(4, 0.90755945), # Relation of user perceived response time to error measurement
(8, 0.050041795), # Graph minors A survey
(7, -0.098794639), # Graph minors IV Widths of trees and well quasi ordering
(6, -0.1063926), # The intersection graph of paths in trees
(5, -0.12416792)] # The generation of random binary unordered trees
'''

(出处结果的注释是我加的,为了方便观察。)

需要注意,使用标准的布尔全文搜索将不会返回编号分别为2和4的文档(The EPS user interface management system”,”Relation of user perceived response time to error measurement”),因为他们与“Human computer interaction”没有任何相同的单词。可是,在使用了LSI后,我们可以看到他们都得到了比较高的分数(2号文档是最相似的),直观感觉上他们都更加符合我们查询的“computer-human”(“人机”)相关主题。事实上,将语义概括化是我们首先使用转换和主题模型的原因。

接下来做什么

恭喜,你已经完成了教程-现在你知道gensim如何工作了。^_^为了钻研详细内容,你可以通篇浏览API文档、阅读《维基百科实验》或者可能试一试gensim的分布式计算。

Gensim是一个比较成熟的工具包,很多公司、个人已经成功将该工具应用于快速原型和生产。但是也并不意味着他是完美的。

还有一些部分可以用更加高效地方式实现(例如用c实现),或者更好地利用并行(多台机器内核)。

随时都有可能发布新的算法;你可以通过讨论与贡献代码帮助gensim跟上时代的步伐。

欢迎并且感激你的反馈(不仅仅是代码):创意贡献、Bug反馈或者仅仅是贡献你的使用经理及问题。

Gensim并没有野心成为一个能包含自然语言处理(NLP)(或者仅仅是机器学习)领域的一切的框架。他的任务只是帮助NLP实践者能轻松地尝试流行的主题模型算法应用于大型数据集,并且帮助研究人员设计新算法原型。

英文维基百科举例

准备语料库

首先,从 http://download.wikimedia.org/enwiki/下载维基百科文档的存档(你可能需要一个像这样的文件enwiki-latest-pages-articles.xml.bz2)。这个文件大约8GB包含了所有的英文维基百科的文章(译者注:实际上应该更大)。

将文章转换为普通文本(处理wiki标记)并且将结果以稀疏TF-IDF向量的形式存储。在Python中,非常轻松就能完成这个任务,我们甚至不需要将整个压缩文件解压到硬盘上。Gensim中有一个脚本可以完成这个任务,仅需:

已经废弃 | Depressed

$ python -m gensim.scripts.make_wiki

当前可用 | Recommend(2017年7月19日)

import genism.corpora.WikiCorpus  
wiki = WikiCorpus('enwiki-20100622-pages-articles.xml.bz2') # 创建word到word_id的映射,需要很长时间,请耐心    
MmCorpus.serialize('wiki_en_tfidf.mm', wiki) # 将语料库存到硬盘中,需要很长时间,请耐心    
wiki.dictionary.save_as_text('wiki_en_wordids.txt')

注:预处理步骤对8.2G压缩的维基存档实施了2个操作(提取字典、创建于存储稀疏向量),在我的笔记版上消耗了大约9小时,因此你可能想要来1、2杯咖啡。

你需要大概35GB空闲硬盘空间来存储输出的稀疏向量。我建议直接压缩这些文件,例如bzip2(压缩至约13GB)。Gensim可以直接使用压缩文件,可以帮助你节约硬盘空间。

潜在语义分析

首先让我们加载在上述步骤创建的语料库迭代器与字典:

import logging, gensim, bz2
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
# 加载id->word映射(字典),上述步骤的结果之一
id2word = gensim.corpora.Dictionary.load_from_text('wiki_en_wordids.txt')
# 加载语料库迭代器
mm = gensim.corpora.MmCorpus('wiki_en_tfidf.mm')
# mm = gensim.corpora.MmCorpus(bz2.BZ2File('wiki_en_tfidf.mm.bz2')) # 如果你压缩了tf-idf输出,请使用这个
print(mm)
#MmCorpus(3931787 documents, 100000 features, 756379027 non-zero entries)

我们看到我们的语料库用稀疏TF-IDF矩阵表示时,包含了3.9M的文档、100K属性(不同的记号——这里就是单词)、0.76G非零输入(译者注:代表总词数)。英文维基百科语料库包括大约22.4亿记号。

现在,我们已经准备好计算英文维基百科的LSA结果了:

# 提取400LSI主题,使用默认的单程算法

lsi = gensim.models.lsimodel.LsiModel(corpus=mm, id2word=id2word, num_topics=400)
# 打印对前10个主题贡献最多的单词(无论积极与消极)
lsi.print_topics(10)
'''
topic #0(332.762): 0.425*"utc" + 0.299*"talk" + 0.293*"page" + 0.226*"article" + 0.224*"delete" + 0.216*"discussion" + 0.205*"deletion" + 0.198*"should" + 0.146*"debate" + 0.132*"be"
topic #1(201.852): 0.282*"link" + 0.209*"he" + 0.145*"com" + 0.139*"his" + -0.137*"page" + -0.118*"delete" + 0.114*"blacklist" + -0.108*"deletion" + -0.105*"discussion" + 0.100*"diff"
topic #2(191.991): -0.565*"link" + -0.241*"com" + -0.238*"blacklist" + -0.202*"diff" + -0.193*"additions" + -0.182*"users" + -0.158*"coibot" + -0.136*"user" + 0.133*"he" + -0.130*"resolves"
topic #3(141.284): -0.476*"image" + -0.255*"copyright" + -0.245*"fair" + -0.225*"use" + -0.173*"album" + -0.163*"cover" + -0.155*"resolution" + -0.141*"licensing" + 0.137*"he" + -0.121*"copies"
topic #4(130.909): 0.264*"population" + 0.246*"age" + 0.243*"median" + 0.213*"income" + 0.195*"census" + -0.189*"he" + 0.184*"households" + 0.175*"were" + 0.167*"females" + 0.166*"males"
topic #5(120.397): 0.304*"diff" + 0.278*"utc" + 0.213*"you" + -0.171*"additions" + 0.165*"talk" + -0.159*"image" + 0.159*"undo" + 0.155*"www" + -0.152*"page" + 0.148*"contribs"
topic #6(115.414): -0.362*"diff" + -0.203*"www" + 0.197*"you" + -0.180*"undo" + -0.180*"kategori" + 0.164*"users" + 0.157*"additions" + -0.150*"contribs" + -0.139*"he" + -0.136*"image"
topic #7(111.440): 0.429*"kategori" + 0.276*"categoria" + 0.251*"category" + 0.207*"kategorija" + 0.198*"kategorie" + -0.188*"diff" + 0.163*"категория" + 0.153*"categoría" + 0.139*"kategoria" + 0.133*"categorie"
topic #8(109.907): 0.385*"album" + 0.224*"song" + 0.209*"chart" + 0.204*"band" + 0.169*"released" + 0.151*"music" + 0.142*"diff" + 0.141*"vocals" + 0.138*"she" + 0.132*"guitar"
topic #9(102.599): -0.237*"league" + -0.214*"he" + -0.180*"season" + -0.174*"football" + -0.166*"team" + 0.159*"station" + -0.137*"played" + -0.131*"cup" + 0.131*"she" + -0.128*"utc"
'''

创建这个LSI模型在我的笔记本上消耗了4小时9分钟[1]。大约16,000个文档每分钟,包括所有的I/O。

注:如果你需要更快得到结果,参见《分布式计算》教程。Gensim中的BLAS库显然利用了多核,以便相同的数据可以在一台多核机器上免费地更快处理,而且不用任何分布式安装。

我们看到总处理时间主要是用于预处理阶段总原始维基百科XML中提取TF-IDF语料库,消耗了9小时。

gensim中使用的算法对于每个输入只需浏览一遍,因此适合用于文档以不重复流的形式输入及存储/迭代多次语料库消费太大的情形。

隐含狄利克雷分配

正如上面的潜在语义分析,首先载入迭代器与字典。

import logging, gensim, bz2
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
 
# load id->word mapping (the dictionary), one of the results of step 2 above
id2word = gensim.corpora.Dictionary.load_from_text('wiki_en_wordids.txt')
# load corpus iterator
mm = gensim.corpora.MmCorpus('wiki_en_tfidf.mm')
# mm = gensim.corpora.MmCorpus(bz2.BZ2File('wiki_en_tfidf.mm.bz2')) # use this if you compressed the TFIDF output
 
print(mm)
#MmCorpus(3931787 documents, 100000 features, 756379027 non-zero entries)

我们将会运行在线LDA(Hoffman等[3]),这个算法可以不断利用新到的文档更新LDA模型。在线LDA可以与批量LDA进行对比,后者是一次处理整个语料库,若要更新只能再次处理整个语料库。不同点是,给定一个相对稳定的文档流(没有太多主题偏移),在线更新只需遍历较小的文档块(子语料库)本身是相当不错的,因此模型收敛更快( The difference is that given a reasonably stationary document stream (not much topic drift), the online updates over the smaller chunks (subcorpora) are pretty good in themselves, so that the model estimation converges faster)。结果是,我们可能只需要处理一遍全部语料库:如果语料库有3M文章,我们在读入10K文章后更新一次,这意味着在一次遍历中我们需要更新300次,很可能得到一个比较准确的主题估计:

# 提取100维LDA主题,使用一次遍历,每1万文档更新一次
lda = gensim.models.ldamodel.LdaModel(corpus=mm, id2word=id2word, num_topics=100, update_every=1, chunksize=10000, passes=1)
using serial LDA version on this node
running online LDA training, 100 topics, 1 passes over the supplied corpus of 3931787 documents, updating model once every 10000 documents
...

不像LSA,来自LDA的主题更容易解释:

# print the most contributing words for 20 randomly selected topics
lda.print_topics(20)
'''
topic #0: 0.009*river + 0.008*lake + 0.006*island + 0.005*mountain + 0.004*area + 0.004*park + 0.004*antarctic + 0.004*south + 0.004*mountains + 0.004*dam
topic #1: 0.026*relay + 0.026*athletics + 0.025*metres + 0.023*freestyle + 0.022*hurdles + 0.020*ret + 0.017*divisão + 0.017*athletes + 0.016*bundesliga + 0.014*medals
topic #2: 0.002*were + 0.002*he + 0.002*court + 0.002*his + 0.002*had + 0.002*law + 0.002*government + 0.002*police + 0.002*patrolling + 0.002*their
topic #3: 0.040*courcelles + 0.035*centimeters + 0.023*mattythewhite + 0.021*wine + 0.019*stamps + 0.018*oko + 0.017*perennial + 0.014*stubs + 0.012*ovate + 0.011*greyish
topic #4: 0.039*al + 0.029*sysop + 0.019*iran + 0.015*pakistan + 0.014*ali + 0.013*arab + 0.010*islamic + 0.010*arabic + 0.010*saudi + 0.010*muhammad
topic #5: 0.020*copyrighted + 0.020*northamerica + 0.014*uncopyrighted + 0.007*rihanna + 0.005*cloudz + 0.005*knowles + 0.004*gaga + 0.004*zombie + 0.004*wigan + 0.003*maccabi
topic #6: 0.061*israel + 0.056*israeli + 0.030*sockpuppet + 0.025*jerusalem + 0.025*tel + 0.023*aviv + 0.022*palestinian + 0.019*ifk + 0.016*palestine + 0.014*hebrew
topic #7: 0.015*melbourne + 0.014*rovers + 0.013*vfl + 0.012*australian + 0.012*wanderers + 0.011*afl + 0.008*dinamo + 0.008*queensland + 0.008*tracklist + 0.008*brisbane
topic #8: 0.011*film + 0.007*her + 0.007*she + 0.004*he + 0.004*series + 0.004*his + 0.004*episode + 0.003*films + 0.003*television + 0.003*best
topic #9: 0.019*wrestling + 0.013*château + 0.013*ligue + 0.012*discus + 0.012*estonian + 0.009*uci + 0.008*hockeyarchives + 0.008*wwe + 0.008*estonia + 0.007*reign
topic #10: 0.078*edits + 0.059*notability + 0.035*archived + 0.025*clearer + 0.022*speedy + 0.021*deleted + 0.016*hook + 0.015*checkuser + 0.014*ron + 0.011*nominator
topic #11: 0.013*admins + 0.009*acid + 0.009*molniya + 0.009*chemical + 0.007*ch + 0.007*chemistry + 0.007*compound + 0.007*anemone + 0.006*mg + 0.006*reaction
topic #12: 0.018*india + 0.013*indian + 0.010*tamil + 0.009*singh + 0.008*film + 0.008*temple + 0.006*kumar + 0.006*hindi + 0.006*delhi + 0.005*bengal
topic #13: 0.047*bwebs + 0.024*malta + 0.020*hobart + 0.019*basa + 0.019*columella + 0.019*huon + 0.018*tasmania + 0.016*popups + 0.014*tasmanian + 0.014*modèle
topic #14: 0.014*jewish + 0.011*rabbi + 0.008*bgwhite + 0.008*lebanese + 0.007*lebanon + 0.006*homs + 0.005*beirut + 0.004*jews + 0.004*hebrew + 0.004*caligari
topic #15: 0.025*german + 0.020*der + 0.017*von + 0.015*und + 0.014*berlin + 0.012*germany + 0.012*die + 0.010*des + 0.008*kategorie + 0.007*cross
topic #16: 0.003*can + 0.003*system + 0.003*power + 0.003*are + 0.003*energy + 0.002*data + 0.002*be + 0.002*used + 0.002*or + 0.002*using
topic #17: 0.049*indonesia + 0.042*indonesian + 0.031*malaysia + 0.024*singapore + 0.022*greek + 0.021*jakarta + 0.016*greece + 0.015*dord + 0.014*athens + 0.011*malaysian
topic #18: 0.031*stakes + 0.029*webs + 0.018*futsal + 0.014*whitish + 0.013*hyun + 0.012*thoroughbred + 0.012*dnf + 0.012*jockey + 0.011*medalists + 0.011*racehorse
topic #19: 0.119*oblast + 0.034*uploaded + 0.034*uploads + 0.033*nordland + 0.025*selsoviet + 0.023*raion + 0.022*krai + 0.018*okrug + 0.015*hålogaland + 0.015*russiae + 0.020*manga + 0.017*dragon + 0.012*theme + 0.011*dvd + 0.011*super + 0.011*hunter + 0.009*ash + 0.009*dream + 0.009*angel
'''
创建该LDA模型在我的笔记本上消耗了6小时20分钟。如果你需要更快些,考虑在计算机集群上使用分布式LDA。 

请注意LDA和LSA运行中的两个不同点:我要求LSA提取400主题,LDA只要100主题(因此速度的差异实际上可能更大)。第二,gensim中的LSA实现是真正的在线:如果输入流的性质(nature)及时改变,LSA将会重定向以反映这些变化,只用一个相对较小的更新。相反地,LDA不是真正在线(虽然文章[3]的标题如此),因为后来更新对模型的影响逐渐变小。如果输入的文档流有主题偏移,LDA将会感到困惑,并且调整自己以适应新状况的速度会越来越慢。
 
一句话,多次使用LDA的更新功能时请小心。当事先知道整个语料库并且没有表现出主题偏移时,批量LDA的用法是可以的,并没有影响。 
要运行批量LDA(非在线),训练LdaModel:
# extract 100 LDA topics, using 20 full passes, no online updates
lda = gensim.models.ldamodel.LdaModel(corpus=mm, id2word=id2word, num_topics=100, update_every=0, passes=20)

像往常一样,一个训练好的模型可以用来将新的文档、没见过的文档(普通词袋计数向量)转化为LDA主题分布:

doc_lda = lda[doc_bow]

为何要分布式计算?

需要构建一个百万文档级语料库的语义代表,却耗时太~~长?手上有几个可用的闲置计算机?分布式计算力争通过将给定的任务切分为几个小型任务,并将这些任务指派给几台平行的计算机完成来实现加速计算。

在这里提到的计算节点是指通过其IP地址/端口识别的计算机,并通过TCP/IP协议完成通讯。所有可用的计算机作为一个整体,称为集群(cluster)。分布式是非常粗粒度的(没有太多实时通讯),因此允许网络有相对较高的延迟。

警告:使用分布式的最重要的原因是加快速度。在gensim中,大多数时间消耗在其内部的底层线性代数计算上,主要是NumPy中,与任何gensim代码无关。为NumPy安装一个快速的基本线性代数(BLAS)库能将提高至15倍!因此在你开始购买额外的计算机前,先考虑安装一个快速的、多线程的BLAS来使你的机器发挥其更大的能力(而不是一个通用的、二进制分布式库)。可选的BLAS库包括你的供应商的BLAS库(Inter MKL,AMD ACML,OS X vecLib,Sun Sunperf)或者一些开源的 (GotoBLAS, ALTAS)。

想查看你正在使用哪个BLAS和LAPACK,你可以在命令行输入:

python -c 'import scipy; scipy.show_config()'

预备知识

Gensim使用Python远程对象(Remote Object,Pyro)实现节点间的通讯,版本号不低于4.27。这是一个底层套接字通讯和远程程序调用(RPC)库。Pyro是一个纯Python库,因此其安装十分简单,仅需将其*.py文件复制到你的Python的import路径。

sudo easy_install Pyro4

你不需要为了运行gensim安装Pyro,但是如果你不安装,就不能使用其分布式计算(即所有的过程都将是连续模式运行,本页面的例子无法使用)。

核心概念

就像往常一样,gensim努力做到干净、简单的API(参见介绍部分的“属性”)。你不需要为了能在计算机集群上运行而对你的代码进行任何修改!
你需要做的是开始计算前在每一个集群节点上运行一个工作者脚本(见下方)。运行该脚本告知gensim它可以使用这些节点工作。在初始化时,gensim内部算法将会尝试寻找和使用所有可用的工作者节点。

节点

一个逻辑工作单元。可以是一台物理机器,也可以在一台机器上运行多个工作者脚本得到多个逻辑节点。

集群

几个可以通过TCP/IP通讯的节点。现在,网络广播被用来发现和连接所有通讯节点,因此节点必须在同一个广播域。

工作者

在每个节点上创建的进程。从集群中移除节点,仅需结束他的工作者进程。

调度

调度器将会负责协调所有计算任务、队列、分发(“分派”)各个工作者的工作。计算指令从不与工作者节点直接交流,仅仅通过调度器。一个集群中同一时间仅能有一个活动的调度器,不像工作者可以有多个。

其他分布式算法

分布式潜在语义分析案例(Distributed Latent Semantic Analysis)

设置一个集群

我们将会通过一个案例展示如何运行分布式潜在语义分析。让我们假设我们有5个计算机,所有的电脑都在一个网段(网络广播可达)。为了开始,首先安装gensim并在每台电脑上设置Pyro():

$ sudo easy_install gensim[distributed]

$ export PYRO_SERIALIZERS_ACCEPTED=pickle

$ export PYRO_SERIALIZER=pickle

接下来,在某一个计算机上运行Pyro的名称服务(无论是哪一台):

$ python -m Pyro4.naming -n 0.0.0.0 &

假设我们的集群中的电脑都是有带有内存负载的双核电脑,我们可以在其中4台上运行2个工作者脚本,共创建8个逻辑工作节点:

$ python -m gensim.models.lsi_worker &

这将会运行gensim的lsi_worker.py脚本(在4台电脑上运行2次)。这让gensim知道它可以在这四台电脑上每台并行运行两个工作,以便计算可以更快,当然也会消耗双倍的内存。
再下一步,选择一台计算机将其作为一个作业调度程序,负责工作者同步,并在其上运行LSA调度器。
在我们的例子中,我们将会使用第5台电脑来作为调度器,并在那里运行:

$ python -m gensim.models.lsi_dispatcher &

一般来说,调度器可以运行在与其中一个工作者节点上,或者也可以是另一个不同的电脑(在相同的广播域)。调度器大多数时间不会占用太多CPU,但是请选择一个有足够内存的计算机。
就是这样!集群已经被建立起来了,并且可以用来接受工作了。后来需要移除工作者节点时,只要结束其lsi_worker进程即可(不会影响正在运行的计算,节点的添加和删除都是动态的)。如果结束了lsi_dispatcher,在你重启它之前将不能运行计算(虽然已经存在的工作者进程能重新启用)。

运行LSA

让我们测试一下我们的设置,运行一个分布式LSA计算。在五台计算机中的任意一台打开一个Python shell(再说一遍,可以是任意一台在同一广播域的计算机,我们的选择都是偶然的)并尝试运行:

from gensim import corpora, models, utils
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
 
corpus = corpora.MmCorpus('/tmp/deerwester.mm') # 载入一个在教程中用到的9个文档的语料库
id2word = corpora.Dictionary.load('/tmp/deerwester.dict')
 
lsi = models.LsiModel(corpus, id2word=id2word, num_topics=200, chunksize=1, distributed=True) # 运行分布式LSA

这里使用到了《语料库与向量空间》教程中创建的语料库及属性记号映射。如果你查找Python会话的日志,你应该会找到类似这样的一行:

2010-08-09 23:44:25,746 : INFO : using distributed version with 8 workers

这意味着一切都在顺利进行。你也可以检查来自工作者和调度器进程的日志文件——这对于防止问题特别有用。检查一下LSA的结果,打印前两个潜在主题:

topic #0(3.341): 0.644*"system" + 0.404*"user" + 0.301*"eps" + 0.265*"time" + 0.265*"response"
topic #1(2.542): 0.623*"graph" + 0.490*"trees" + 0.451*"minors" + 0.274*"survey" + -0.167*"system"

成功了!但是这种规模的语料库对于我们强大的集群来说没有什么挑战性……实际上,我们故意降低了单个文档一次工作的大小(chunksize参数),否则所有的文档将会被一个工作者一次处理完。

所以,让我们在百万文档上试验一下LSA:

# 不断重复corpus将语料填充至1M
corpus1m = utils.RepeatCorpus(corpus, 1000000)
# 运行分布式LSA
lsi1m = models.LsiModel(corpus1m, id2word=id2word, num_topics=200, chunksize=10000, distributed=True)
 
lsi1m.print_topics(num_topics=2, num_words=5)
topic #0(1113.628): 0.644*"system" + 0.404*"user" + 0.301*"eps" + 0.265*"time" + 0.265*"response"
topic #1(847.233): 0.623*"graph" + 0.490*"trees" + 0.451*"minors" + 0.274*"survey" + -0.167*"system"

其日志文件应该类似于:

2010-08-10 02:46:35,087 : INFO : using distributed version with 8 workers 
2010-08-10 02:46:35,087 : INFO : updating SVD with new documents 
2010-08-10 02:46:35,202 : INFO : dispatched documents up to #10000 
2010-08-10 02:46:35,296 : INFO : dispatched documents up to #20000 2010-08-10 02:46:46,524 : INFO : dispatched documents up to #990000 
2010-08-10 02:46:46,694 : INFO : dispatched documents up to #1000000 
2010-08-10 02:46:46,694 : INFO : reached the end of input; now waiting for all remaining jobs to finish 
2010-08-10 02:46:47,195 : INFO : all jobs finished, downloading final projection 
2010-08-10 02:46:47,200 : INFO : decomposition complete

因为我们的“一百万语料库”词汇量太小、结构太一般,LSA的计算啊仅仅消耗了12秒。-_-!为了真实的压力测试,让我们在英文维基百科语料库上做一个LSA。

维基百科上的分布式LSA

首先,像之前的《英文维基百科的实验》一样下载并准备维基百科语料库,然后加载语料库迭代器:

import logging, gensim, bz2
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)
 
# 加载id->word mapping (the dictionary)
id2word = gensim.corpora.Dictionary.load_from_text('wiki_en_wordids.txt')
# load corpus iterator
mm = gensim.corpora.MmCorpus('wiki_en_tfidf.mm')
# mm = gensim.corpora.MmCorpus(bz2.BZ2File('wiki_en_tfidf.mm.bz2')) # use this if you compressed the TFIDF output
 
print(mm)
#MmCorpus(3199665 documents, 100000 features, 495547400 non-zero entries)

现在我们已经准备好在英文维基百科上运行分布式LSA了:

# 使用集群提取400个LSI主题
lsi = gensim.models.lsimodel.LsiModel(corpus=mm, id2word=id2word, num_topics=400, chunksize=20000, distributed=True)
 
# 打印前10个主题的贡献最高的单词(消极或积极)
lsi.print_topics(10)
'''
2010-11-03 16:08:27,602 : INFO : topic #0(200.990): -0.475*"delete" + -0.383*"deletion" + -0.275*"debate" + -0.223*"comments" + -0.220*"edits" + -0.213*"modify" + -0.208*"appropriate" + -0.194*"subsequent" + -0.155*"wp" + -0.117*"notability"
2010-11-03 16:08:27,626 : INFO : topic #1(143.129): -0.320*"diff" + -0.305*"link" + -0.199*"image" + -0.171*"www" + -0.162*"user" + 0.149*"delete" + -0.147*"undo" + -0.144*"contribs" + -0.122*"album" + 0.113*"deletion"
2010-11-03 16:08:27,651 : INFO : topic #2(135.665): -0.437*"diff" + -0.400*"link" + -0.202*"undo" + -0.192*"user" + -0.182*"www" + -0.176*"contribs" + 0.168*"image" + -0.109*"added" + 0.106*"album" + 0.097*"copyright"
2010-11-03 16:08:27,677 : INFO : topic #3(125.027): -0.354*"image" + 0.239*"age" + 0.218*"median" + -0.213*"copyright" + 0.204*"population" + -0.195*"fair" + 0.195*"income" + 0.167*"census" + 0.165*"km" + 0.162*"households"
2010-11-03 16:08:27,701 : INFO : topic #4(116.927): -0.307*"image" + 0.195*"players" + -0.184*"median" + -0.184*"copyright" + -0.181*"age" + -0.167*"fair" + -0.162*"income" + -0.151*"population" + -0.136*"households" + -0.134*"census"
2010-11-03 16:08:27,728 : INFO : topic #5(100.326): 0.501*"players" + 0.318*"football" + 0.284*"league" + 0.193*"footballers" + 0.142*"image" + 0.133*"season" + 0.119*"cup" + 0.113*"club" + 0.110*"baseball" + 0.103*"f"
2010-11-03 16:08:27,754 : INFO : topic #6(92.298): -0.411*"album" + -0.275*"albums" + -0.217*"band" + -0.214*"song" + -0.184*"chart" + -0.163*"songs" + -0.160*"singles" + -0.149*"vocals" + -0.139*"guitar" + -0.129*"track"
2010-11-03 16:08:27,780 : INFO : topic #7(83.811): -0.248*"wikipedia" + -0.182*"keep" + 0.180*"delete" + -0.167*"articles" + -0.152*"your" + -0.150*"my" + 0.144*"film" + -0.130*"we" + -0.123*"think" + -0.120*"user"
2010-11-03 16:08:27,807 : INFO : topic #8(78.981): 0.588*"film" + 0.460*"films" + -0.130*"album" + -0.127*"station" + 0.121*"television" + 0.115*"poster" + 0.112*"directed" + 0.110*"actors" + -0.096*"railway" + 0.086*"movie"
2010-11-03 16:08:27,834 : INFO : topic #9(78.620): 0.502*"kategori" + 0.282*"categoria" + 0.248*"kategorija" + 0.234*"kategorie" + 0.172*"категория" + 0.165*"categoría" + 0.161*"kategoria" + 0.148*"categorie" + 0.126*"kategória" + 0.121*"catégorie"
'''

在串行模式,使用单程算法(one-pass algorithm)创建维基百科的LSI模型,在我的笔记本上消耗了5.25小时(OS X, C2D 2.53GHz, 4GB RAM with libVec)。使用了有4个工作者(Linux, dual-core Xeons of 2Ghz, 4GB RAM with ATLAS)的分布式模式,消耗时间下降至1小时41分。你可以在我的研究论文中阅读到更多的关于内部设定和实验的内容。

本文地址:http://51blog.com/?p=1883
关注我们:请关注一下我们的微信公众号:扫描二维码广东高校数据家园_51博客的公众号,公众号:数博联盟
温馨提示:文章内容系作者个人观点,不代表广东高校数据家园_51博客对观点赞同或支持。
版权声明:本文为转载文章,来源于  在路上吗 ,版权归原作者所有,欢迎分享本文,转载请保留出处!

发表评论


表情