不要忘记设置log,以记录操作历史


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

从String到向量

这次,我们从string类型表示的文档开始:


In [2]:
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$个文档的语料库,每个文档包含了一句话。 首先,我们先对文档进行解析,移除掉通用词(使用一个简短的停词表):


In [11]:
# 移除通用词并解析
stoplist = set('for a of the and to in'.split())
# print stoplist
texts = [[word for word in document.lower().split() if word not in stoplist] for document in documents]
# print texts
# 下面的调用方式只能是二维数组
# texts[1] = texts[1].append(['sdf']) 
# 移除只出现一次的词,注意sum函数的使用方式,相当于MapReduce中的一些类似的操作
all_tokens = sum(texts, [])
# print all_tokens
tokens_once = set(word for word in set(all_tokens) if all_tokens.count(word) == 1)
# print tokens_once
texts = [[word for word in text if word not in tokens_once] for text in texts]
print(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']]

当然不同场景下处理文档的方式会有很大的不同,这里,我们仅仅使用空格来进行解析,然后将每个词转换成小写。实际上,我们模仿了Deerweater等人的LSA文章中的方式(简化并且是低效的一种方式)。 这类处理文档的方式差异很大并且和应用与语言相关,所以我没有使用任何接口对这些进行限制。相反,文档由从其中抽取出来的特征表示,而不是最初的那些字符串表示:如何获得这些特征取决于你自己。下面我会给出一个通用的观点来对文档进行处理,但是需要记住在不同的应用场景,需要有不同的特征,garbage in,garbage out。 为了将文档转换为向量,我们将使用词袋模型。其中ing每个文档都被表示成一个向量,其中每个项都是这个项在文档中出现的次数。

将这些词都使用他们的整型的id进行表示比较方便。而词和id之间的对应关系存放在字典之中:


In [15]:
dictionary = corpora.Dictionary(texts)
dictionary.save('/tmp/deerwester.dict')
print(dictionary)


Dictionary(12 unique tokens: [u'minors', u'graph', u'system', u'trees', u'eps']...)

我们这里讲一个独一无二的id赋值给出现在语料库中的词,对应的类就是gensim.corpora.dictionary.Dictionary。这里会对所有的文档进行扫描,搜集词出现的个数和相关的统计。最终,我们看到这堆文档中有$12$个不同的词,也即是说每个文档就可以使用$12$个数字构成的(12维的向量)。我们可以看看词和对应id的映射关系:


In [16]:
print(dictionary.token2id)


{u'minors': 11, u'graph': 10, u'system': 5, u'trees': 9, u'eps': 8, u'computer': 0, u'survey': 4, u'user': 7, u'human': 1, u'time': 6, u'interface': 2, u'response': 3}

实际上来讲解析后的文档转换为向量:


In [18]:
new_doc = "Human computer interaction"
new_vec = dictionary.doc2bow(new_doc.lower().split())
print(new_vec) # interaction 未出现在语料中,所以被忽略了


[(0, 1), (1, 1)]

doc2bow简单滴统计了每个不同词出现的次数,将这个词转换为其对应的id,并返回一个稀疏向量。这个例子中,[(0, 1), (1, 1)]就是指computerhuman出现了一次,而其他的词都是出现0次。


In [19]:
corpus = [dictionary.doc2bow(text) for text in texts]
corpora.MmCorpus.serialize('/tmp/deerwester.mm', corpus) # store to disk, for later use
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)]]

但是现在应该知道向量的特征id=10表示“对应id为10的那个词在文档中出现了多少次”,对于前6个文档,这个值都是0,其余3个都是1。所以现在我们就得到上一章节的语料向量。

流式处理语料库

注意上面例子中语料都存放在内存中,作为python的一个列表。在这个例子中,内存并不是问题,但是为了让事情变得清晰一些,我们假设在语料库中存在上百万篇文档。将这些文档都存放到内存中很不合适的。所以我们假设文档存放在硬盘上的一个文件中。Gensim只需要语料库可以每次调用一个文档并返回一个文档向量:


In [20]:
class MyCorpus(object):
    def __iter__(self):
        for line in open('mycorpus.txt'):
            # 假设每行一篇文档,词使用空白进行分隔
            yield dictionary.doc2bow(line.lower().split())

下载文档mycorpus.txt,我们创建了一个文档的语料库,形式就是向量流。下面我们就开始使用gensim来处理语料:


In [21]:
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)

在这个教程中,我将告诉你如何将文档从一个向量形式转换为另一个形式。主要分为两目标:

  1. 为了将语料中的隐含结构找出来,发现词之间的关系,使用这些信息来从一个新的和更加语义角度地来描述这些文档。
  2. 让文档的表示更加紧致一些。有助于提高性能(新的表示方法消耗较少的资源)和准确度(噪声被减少很多) ## 创建一个转换 转换是标准的python对象,使用一个训练语料库进行初始化:

In [24]:
tfidf = models.TfidfModel(corpus)

我们使用教程1中的旧的语料,初始化转换模型。不同的转换可能需要不同的初始化参数。在Tfidf的例子中,训练过程包含扫描一遍语料然后计算所有特征的文档的频率。训练其他的模型,例如LSI或者LDA,需要消耗更多的空间和更多的时间。

转换通常是在两个特定的向量空间之间进行转换。同样的向量空间(同样的特征id的集合)肯定也在子向量转换中使用到。当然如果使用不同的字符串预处理产生的结果,使用不同的特征id或者使用词袋模型而需要Tfidf向量时都会导致特征的混乱,所以也就导致最终的垃圾结果产生或者运行时出现异常。

转换向量

从现在开始,tfidf被当做是只读的对象,可以用来把从旧的表示方式(词袋模型整数值)到新的表示方式(tfidf模型的实数值):


In [26]:
doc_bow = [(0,1),(1,1)]
print(tfidf[doc_bow])


[(0, 0.7071067811865476), (1, 0.7071067811865476)]

或者对整个语料库进行转换:


In [27]:
corpus_tfidf = tfidf[corpus]
for doc in corpus_tfidf:
    print(doc)


[(0, 0.5773502691896257), (1, 0.5773502691896257), (2, 0.5773502691896257)]
[(0, 0.44424552527467476), (3, 0.44424552527467476), (4, 0.44424552527467476), (5, 0.3244870206138555), (6, 0.44424552527467476), (7, 0.3244870206138555)]
[(2, 0.5710059809418182), (5, 0.4170757362022777), (7, 0.4170757362022777), (8, 0.5710059809418182)]
[(1, 0.49182558987264147), (5, 0.7184811607083769), (8, 0.49182558987264147)]
[(3, 0.6282580468670046), (6, 0.6282580468670046), (7, 0.45889394536615247)]
[(9, 1.0)]
[(9, 0.7071067811865475), (10, 0.7071067811865475)]
[(9, 0.5080429008916749), (10, 0.5080429008916749), (11, 0.695546419520037)]
[(4, 0.6282580468670046), (10, 0.45889394536615247), (11, 0.6282580468670046)]

在这个例子中,我们在转换用于训练的同样的预料,但是这只是一个巧合。一旦转换模型已经初始化了,它就可以应用在任何向量上(当然这些向量来自同样的向量空间)。即使他们没在训练预料中使用过。这可以通过一个LSA中的称为folding-in的过程得到(在LDA中叫做话题推断)。

调用model[corpus]仅仅创建了一个wrapper在旧的语料文档流之外。实际上转换就在文档迭代过程中完成。我们不能在调用corpus_transformed = model[corpus]的时候转换整个语料,因为这要求将结果存储在主存中,与gensim的内存独立的目标冲突。如果你需要读取corpus_transformed多次,代价很大,你可以先将结果语料序列化存进磁盘中,然后使用它。

转换可以以链式的方式进行被序列化:


In [28]:
lsi = models.LsiModel(corpus_tfidf, id2word=dictionary, num_topics=2)
corpus_lsi = lsi[corpus_tfidf]

这里我们将Tfidf语料使用Latent semantic indexing转换为一个潜在的2D空间(因为我们这里将topic的数目设置为2 num_topics=2)。现在你可能在想:这两个隐含的维度表示什么?让我们好好看看models.LsiModel.print_topics()


In [34]:
lsi.print_topics(2)


Out[34]:
[u'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"',
 u'-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,trees、graph和minors是相关的词(并对第一个话题贡献了最大的力度,而第二个话题看上去所有的 。前5个文档强相关于第二个话题,而后4个就关联于第一个话题:


In [35]:
for doc in corpus_lsi:
    print(doc)


[(0, 0.06600783396090347), (1, -0.52007033063618469)]
[(0, 0.19667592859142535), (1, -0.76095631677000442)]
[(0, 0.089926399724464146), (1, -0.72418606267525076)]
[(0, 0.075858476521781557), (1, -0.63205515860034289)]
[(0, 0.10150299184980199), (1, -0.57373084830029519)]
[(0, 0.70321089393783109), (1, 0.16115180214025829)]
[(0, 0.87747876731198304), (1, 0.16758906864659448)]
[(0, 0.90986246868185772), (1, 0.14086553628719053)]
[(0, 0.61658253505692795), (1, -0.0539290756638936)]

模型的持久性可以通过save()load()达成:


In [37]:
lsi.save('/tmp/model.lsi')
lsi = models.LsiModel.load('/tmp/model.lsi')

下面的问题就是:这些文档之间相似程度怎么样?有一种方式来形式化这样的相似性么,给定一个输入文档,找到与之相似的一系列文档?相似度量在下个教程中讲述。

可用的转换

gensim实现了几个著名的向量空间模型算法:

  1. TFidf

In [38]:
model = tfidfmodel.TfidfModel(bow_corpus, normalize=True)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-38-141f95f69899> in <module>()
----> 1 model = tfidfmodel.TfidfModel(bow_corpus, normalize=True)

NameError: name 'tfidfmodel' is not defined
  1. LSI 将文档从词袋模型或者Tfidf权值模型转换到一个低维度的潜在空间。在我们的例子中,潜在空间维度就是设置成了2,实际情况中,这个值要设置为200~500差不多,这是标准的做法:

In [ ]:
>>> model = lsimodel.LsiModel(tfidf_corpus, id2word=dictionary, num_topics=300)
>>> model.add_documents(another_tfidf_corpus) # now LSI has been trained on tfidf_corpus + another_tfidf_corpus
>>> lsi_vec = model[tfidf_vec] # convert some new document into the LSI space, without affecting the model
>>> ...
>>> model.add_documents(more_documents) # tfidf_corpus + another_tfidf_corpus + more_documents
>>> lsi_vec = model[tfidf_vec]
>>> ...

LSI训练独特之处,在于我们可以在任何时刻继续训练,就是单纯地通过增加额外的训练文档。对原本的模型进行增量更新,也有个专业术语叫做在线训练(online training)。因为这个特征,输入文档流甚至可以是无穷多的,在有新的文档输入时使用之前已经计算出来的模型(让其只读) 可以参见文档gensim.models.lsimodels来看如何让LSI逐渐地忘记旧的观测值的。如果你希望看到更加多的信息,还有不少的参数可以调整来获得速度、内存痕迹和LSI的准确性之间的关系。 gensim使用了一个新式的在线增量流分布式训练算法,参见5。gensim同样执行了一个由Halko提出的随机多趟算法来加速核心计算。使用分布式计算集群来计算参见这里

  1. LDA 同样是一种从词袋模型转换到更低的维度空间的方式。LDA是LSI的一种概率扩展(也被称为多项分布PCA),所以LDA的话题可以被解释成词上的概率分布。这些分布如同LSA中一样,是由训练语料中自动的推断出来的。文档被解释成这些话题的一种混合(mixture),就像LSA那样

In [39]:
model = ldamodel.LdaModel(bow_corpus, id2word=dictionary, num_topics=100)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-39-39884ab8a083> in <module>()
----> 1 model = ldamodel.LdaModel(bow_corpus, id2word=dictionary, num_topics=100)

NameError: name 'ldamodel' is not defined

gensim使用了在线LDA参数估计快速实现,可以以分布方式在计算机集群上执行。