用Python给文本创立向量空间模型的教程


Posted in Python onApril 23, 2015

我们需要开始思考如何将文本集合转化为可量化的东西。最简单的方法是考虑词频。

我将尽量尝试不使用NLTK和Scikits-Learn包。我们首先使用Python讲解一些基本概念。

基本词频

首先,我们回顾一下如何得到每篇文档中的词的个数:一个词频向量。
 

#examples taken from here: http://stackoverflow.com/a/1750187
 
mydoclist = ['Julie loves me more than Linda loves me',
'Jane likes me more than Julie loves me',
'He likes basketball more than baseball']
 
#mydoclist = ['sun sky bright', 'sun sun bright']
 
from collections import Counter
 
for doc in mydoclist:
  tf = Counter()
  for word in doc.split():
    tf[word] +=1
  print tf.items()

[('me', 2), ('Julie', 1), ('loves', 2), ('Linda', 1), ('than', 1), ('more', 1)]
[('me', 2), ('Julie', 1), ('likes', 1), ('loves', 1), ('Jane', 1), ('than', 1), ('more', 1)]
[('basketball', 1), ('baseball', 1), ('likes', 1), ('He', 1), ('than', 1), ('more', 1)]

这里我们引入了一个新的Python对象,被称作为Counter。该对象只在Python2.7及更高的版本中有效。Counters非常的灵活,利用它们你可以完成这样的功能:在一个循环中进行计数。

根据每篇文档中词的个数,我们进行了文档量化的第一个尝试。但对于那些已经学过向量空间模型中“向量”概念的人来说,第一次尝试量化的结果不能进行比较。这是因为它们不在同一词汇空间中。

我们真正想要的是,每一篇文件的量化结果都有相同的长度,而这里的长度是由我们语料库的词汇总量决定的。
 

import string #allows for format()
   
def build_lexicon(corpus):
  lexicon = set()
  for doc in corpus:
    lexicon.update([word for word in doc.split()])
  return lexicon
 
def tf(term, document):
 return freq(term, document)
 
def freq(term, document):
 return document.split().count(term)
 
vocabulary = build_lexicon(mydoclist)
 
doc_term_matrix = []
print 'Our vocabulary vector is [' + ', '.join(list(vocabulary)) + ']'
for doc in mydoclist:
  print 'The doc is "' + doc + '"'
  tf_vector = [tf(word, doc) for word in vocabulary]
  tf_vector_string = ', '.join(format(freq, 'd') for freq in tf_vector)
  print 'The tf vector for Document %d is [%s]' % ((mydoclist.index(doc)+1), tf_vector_string)
  doc_term_matrix.append(tf_vector)
   
  # here's a test: why did I wrap mydoclist.index(doc)+1 in parens? it returns an int...
  # try it! type(mydoclist.index(doc) + 1)
 
print 'All combined, here is our master document term matrix: '
print doc_term_matrix

我们的词向量为[me, basketball, Julie, baseball, likes, loves, Jane, Linda, He, than, more]

文档”Julie loves me more than Linda loves me”的词频向量为:[2, 0, 1, 0, 0, 2, 0, 1, 0, 1, 1]

文档”Jane likes me more than Julie loves me”的词频向量为:[2, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1]

文档”He likes basketball more than baseball”的词频向量为:[0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1]

合在一起,就是我们主文档的词矩阵:

[[2, 0, 1, 0, 0, 2, 0, 1, 0, 1, 1], [2, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1], [0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1]]

好吧,这看起来似乎很合理。如果你有任何机器学习的经验,你刚刚看到的是建立一个特征空间。现在每篇文档都在相同的特征空间中,这意味着我们可以在同样维数的空间中表示整个语料库,而不会丢失太多信息。

标准化向量,使其L2范数为1

一旦你在同一个特征空间中得到了数据,你就可以开始应用一些机器学习方法:分类、聚类等等。但实际上,我们同样遇到一些问题。单词并不都包含相同的信息。

如果有些单词在一个单一的文件中过于频繁地出现,它们将扰乱我们的分析。我们想要对每一个词频向量进行比例缩放,使其变得更具有代表性。换句话说,我们需要进行向量标准化。

我们真的没有时间过多地讨论关于这方面的数学知识。现在仅仅接受这样一个事实:我们需要确保每个向量的L2范数等于1。这里有一些代码,展示这是如何实现的。
 

import math
 
def l2_normalizer(vec):
  denom = np.sum([el**2 for el in vec])
  return [(el / math.sqrt(denom)) for el in vec]
 
doc_term_matrix_l2 = []
for vec in doc_term_matrix:
  doc_term_matrix_l2.append(l2_normalizer(vec))
 
print 'A regular old document term matrix: '
print np.matrix(doc_term_matrix)
print '\nA document term matrix with row-wise L2 norms of 1:'
print np.matrix(doc_term_matrix_l2)
 
# if you want to check this math, perform the following:
# from numpy import linalg as la
# la.norm(doc_term_matrix[0])
# la.norm(doc_term_matrix_l2[0])

格式化后的旧的文档词矩阵:

[[2 0 1 0 0 2 0 1 0 1 1]
[2 0 1 0 1 1 1 0 0 1 1]
[0 1 0 1 1 0 0 0 1 1 1]]

按行计算的L2范数为1的文档词矩阵:

[[ 0.57735027 0. 0.28867513 0. 0. 0.57735027
0. 0.28867513 0. 0.28867513 0.28867513]
[ 0.63245553 0. 0.31622777 0. 0.31622777 0.31622777
0.31622777 0. 0. 0.31622777 0.31622777]
[ 0. 0.40824829 0. 0.40824829 0.40824829 0. 0.
0. 0.40824829 0.40824829 0.40824829]]

还不错,没有太深究线性代数的知识,你就可以马上看到我们按比例缩小了各个向量,使它们的每一个元素都在0到1之间,并且不会丢失太多有价值的信息。你看到了,一个计数为1的词在一个向量中的值和其在另一个向量中的值不再相同。

为什么我们关心这种标准化吗?考虑这种情况,如果你想让一个文档看起来比它实际上和一个特定主题更相关,你可能会通过不断重复同一个词,来增加它包含到一个主题的可能性。坦率地说,在某种程度上,我们得到了一个在该词的信息价值上衰减的结果。所以我们需要按比例缩小那些在一篇文档中频繁出现的单词的值。

IDF频率加权

我们现在还没有得到想要的结果。就像一篇文档中的所有单词不具有相同的价值一样,也不是全部文档中的所有单词都有价值。我们尝试利用反文档词频(IDF)调整每一个单词权重。我们看看这包含了些什么:
 

def numDocsContaining(word, doclist):
  doccount = 0
  for doc in doclist:
    if freq(word, doc) > 0:
      doccount +=1
  return doccount 
 
def idf(word, doclist):
  n_samples = len(doclist)
  df = numDocsContaining(word, doclist)
  return np.log(n_samples / 1+df)
 
my_idf_vector = [idf(word, mydoclist) for word in vocabulary]
 
print 'Our vocabulary vector is [' + ', '.join(list(vocabulary)) + ']'
print 'The inverse document frequency vector is [' + ', '.join(format(freq, 'f') for freq in my_idf_vector) + ']'

我们的词向量为[me, basketball, Julie, baseball, likes, loves, Jane, Linda, He, than, more]

反文档词频向量为[1.609438, 1.386294, 1.609438, 1.386294, 1.609438, 1.609438, 1.386294, 1.386294, 1.386294, 1.791759, 1.791759]

现在,对于词汇中的每一个词,我们都有一个常规意义上的信息值,用于解释他们在整个语料库中的相对频率。回想一下,这个信息值是一个“逆”!即信息值越小的词,它在语料库中出现的越频繁。

我们快得到想要的结果了。为了得到TF-IDF加权词向量,你必须做一个简单的计算:tf * idf。

现在让我们退一步想想。回想下线性代数:如果你用一个AxB的向量乘以另一个AxB的向量,你将得到一个大小为AxA的向量,或者一个标量。我们不会那么做,因为我们想要的是一个具有相同维度(1 x词数量)的词向量,向量中的每个元素都已经被自己的idf权重加权了。我们如何在Python中实现这样的计算呢?

在这里我们可以编写完整的函数,但我们不那么做,我们将要对numpy做一个简介。
 

import numpy as np
 
def build_idf_matrix(idf_vector):
  idf_mat = np.zeros((len(idf_vector), len(idf_vector)))
  np.fill_diagonal(idf_mat, idf_vector)
  return idf_mat
 
my_idf_matrix = build_idf_matrix(my_idf_vector)
 
#print my_idf_matrix

太棒了!现在我们已经将IDF向量转化为BxB的矩阵了,矩阵的对角线就是IDF向量。这意味着我们现在可以用反文档词频矩阵乘以每一个词频向量了。接着,为了确保我们也考虑那些过于频繁地出现在文档中的词,我们将对每篇文档的向量进行标准化,使其L2范数等于1。
 

doc_term_matrix_tfidf = []
 
#performing tf-idf matrix multiplication
for tf_vector in doc_term_matrix:
  doc_term_matrix_tfidf.append(np.dot(tf_vector, my_idf_matrix))
 
#normalizing
doc_term_matrix_tfidf_l2 = []
for tf_vector in doc_term_matrix_tfidf:
  doc_term_matrix_tfidf_l2.append(l2_normalizer(tf_vector))
                   
print vocabulary
print np.matrix(doc_term_matrix_tfidf_l2) # np.matrix() just to make it easier to look at

set(['me', 'basketball', 'Julie', 'baseball', 'likes', 'loves', 'Jane', 'Linda', 'He', 'than', 'more'])

[[ 0.57211257 0. 0.28605628 0. 0. 0.57211257
0. 0.24639547 0. 0.31846153 0.31846153]
[ 0.62558902 0. 0.31279451 0. 0.31279451 0.31279451
0.26942653 0. 0. 0.34822873 0.34822873]
[ 0. 0.36063612 0. 0.36063612 0.41868557 0. 0.
0. 0.36063612 0.46611542 0.46611542]]

太棒了!你刚看到了一个展示如何繁琐地建立一个TF-IDF加权的文档词矩阵的例子。

最好的部分来了:你甚至不需要手动计算上述变量,使用scikit-learn即可。

记住,Python中的一切都是一个对象,对象本身占用内存,同时对象执行操作占用时间。使用scikit-learn包,以确保你不必担心前面所有步骤的效率问题。

注意:你从TfidfVectorizer/TfidfTransformer得到的值将和我们手动计算的值不同。这是因为scikit-learn使用一个Tfidf的改进版本处理除零的错误。这里有一个更深入的讨论。
 

from sklearn.feature_extraction.text import CountVectorizer
 
count_vectorizer = CountVectorizer(min_df=1)
term_freq_matrix = count_vectorizer.fit_transform(mydoclist)
print "Vocabulary:", count_vectorizer.vocabulary_
 
from sklearn.feature_extraction.text import TfidfTransformer
 
tfidf = TfidfTransformer(norm="l2")
tfidf.fit(term_freq_matrix)
 
tf_idf_matrix = tfidf.transform(term_freq_matrix)
print tf_idf_matrix.todense()

Vocabulary: {u'me': 8, u'basketball': 1, u'julie': 4, u'baseball': 0, u'likes': 5, u'loves': 7, u'jane': 3, u'linda': 6, u'more': 9, u'than': 10, u'he': 2}
[[ 0. 0. 0. 0. 0.28945906 0.
0.38060387 0.57891811 0.57891811 0.22479078 0.22479078]
[ 0. 0. 0. 0.41715759 0.3172591 0.3172591
0. 0.3172591 0.6345182 0.24637999 0.24637999]
[ 0.48359121 0.48359121 0.48359121 0. 0. 0.36778358
0. 0. 0. 0.28561676 0.28561676]]

实际上,你可以用一个函数完成所有的步骤:TfidfVectorizer
 

from sklearn.feature_extraction.text import TfidfVectorizer
 
tfidf_vectorizer = TfidfVectorizer(min_df = 1)
tfidf_matrix = tfidf_vectorizer.fit_transform(mydoclist)
 
print tfidf_matrix.todense()
[[ 0. 0. 0. 0. 0.28945906 0.
0.38060387 0.57891811 0.57891811 0.22479078 0.22479078]
[ 0. 0. 0. 0.41715759 0.3172591 0.3172591
0. 0.3172591 0.6345182 0.24637999 0.24637999]
[ 0.48359121 0.48359121 0.48359121 0. 0. 0.36778358
0. 0. 0. 0.28561676 0.28561676]]

并且我们可以利用这个词汇空间处理新的观测文档,就像这样:
 

new_docs = ['He watches basketball and baseball', 'Julie likes to play basketball', 'Jane loves to play baseball']
new_term_freq_matrix = tfidf_vectorizer.transform(new_docs)
print tfidf_vectorizer.vocabulary_
print new_term_freq_matrix.todense()
{u'me': 8, u'basketball': 1, u'julie': 4, u'baseball': 0, u'likes': 5, u'loves': 7, u'jane': 3, u'linda': 6, u'more': 9, u'than': 10, u'he': 2}
[[ 0.57735027 0.57735027 0.57735027 0. 0. 0. 0.
0. 0. 0. 0. ]
[ 0. 0.68091856 0. 0. 0.51785612 0.51785612
0. 0. 0. 0. 0. ]
[ 0.62276601 0. 0. 0.62276601 0. 0. 0.
0.4736296 0. 0. 0. ]]

请注意,在new_term_freq_matrix中并没有“watches”这样的单词。这是因为我们用于训练的文档是mydoclist中的文档,这个词并不会出现在那个语料库的词汇中。换句话说,它在我们的词汇词典之外。

回到Amazon评论文本

练习2

现在是时候尝试使用你学过的东西了。利用TfidfVectorizer,你可以在Amazon评论文本的字符串列表上尝试建立一个TF-IDF加权文档词矩。
 

import os
import csv
 
#os.chdir('/Users/rweiss/Dropbox/presentations/IRiSS2013/text1/fileformats/')
 
with open('amazon/sociology_2010.csv', 'rb') as csvfile:
  amazon_reader = csv.DictReader(csvfile, delimiter=',')
  amazon_reviews = [row['review_text'] for row in amazon_reader]
 
  #your code here!!!
Python 相关文章推荐
Python使用minidom读写xml的方法
Jun 03 Python
django开发post接口简单案例,获取参数值的方法
Dec 11 Python
python 动态生成变量名以及动态获取变量的变量名方法
Jan 20 Python
pandas分区间,算频率的实例
Jul 04 Python
Python数据类型之列表和元组的方法实例详解
Jul 08 Python
Python项目 基于Scapy实现SYN泛洪攻击的方法
Jul 23 Python
Python 离线工作环境搭建的方法步骤
Jul 29 Python
Python warning警告出现的原因及忽略方法
Jan 31 Python
python中有帮助函数吗
Jun 19 Python
matplotlib基础绘图命令之errorbar的使用
Aug 13 Python
Python 将代码转换为可执行文件脱离python环境运行(步骤详解)
Jan 25 Python
5个pandas调用函数的方法让数据处理更加灵活自如
Apr 24 Python
用Python进行行为驱动开发的入门教程
Apr 23 #Python
python正常时间和unix时间戳相互转换的方法
Apr 23 #Python
python执行等待程序直到第二天零点的方法
Apr 23 #Python
在Python中测试访问同一数据的竞争条件的方法
Apr 23 #Python
python实现在每个独立进程中运行一个函数的方法
Apr 23 #Python
python输出指定月份日历的方法
Apr 23 #Python
python打开文件并获取文件相关属性的方法
Apr 23 #Python
You might like
解析PHP处理换行符的问题 \r\n
2013/06/13 PHP
原生JS实现Ajax通过GET方式与PHP进行交互操作示例
2018/05/12 PHP
asp javascript 实现关闭窗口时保存数据的办法
2007/11/24 Javascript
javascript写的日历类(基于pj)
2010/12/28 Javascript
JavaScript设置IFrame高度自适应(兼容各主流浏览器)
2013/06/05 Javascript
浅析js设置控件的readonly与enabled属性问题
2013/12/25 Javascript
JavaScript实现的圆形浮动标签云效果实例
2015/08/06 Javascript
jQuery插件实现表格隔行变色及鼠标滑过高亮显示效果代码
2016/02/25 Javascript
浅谈JavaScript函数的四种存在形态
2016/06/08 Javascript
JS作用域深度解析
2016/12/29 Javascript
浅谈vue中使用图片懒加载vue-lazyload插件详细指南
2017/10/23 Javascript
Array数组对象中的forEach、map、filter及reduce详析
2018/08/02 Javascript
vue2 设置router-view默认路径的实例
2018/09/20 Javascript
微信小程序在线客服自动回复功能(基于node)
2019/07/03 Javascript
JS实现导航栏楼层特效
2020/01/01 Javascript
JS简单表单验证功能完整示例
2020/01/26 Javascript
基于Web Audio API实现音频可视化效果
2020/06/12 Javascript
React实现评论的添加和删除
2020/10/20 Javascript
Vue中使用wangeditor富文本编辑的问题
2021/02/07 Vue.js
[35:27]完美世界DOTA2联赛循环赛 GXR vs FTD BO2第二场 10.29
2020/10/29 DOTA
跟老齐学Python之字典,你还记得吗?
2014/09/20 Python
Python学习笔记之open()函数打开文件路径报错问题
2018/04/28 Python
PyTorch快速搭建神经网络及其保存提取方法详解
2018/04/28 Python
简单了解python元组tuple相关原理
2019/12/02 Python
一些常用的HTML5模式(pattern) 总结
2015/07/14 HTML / CSS
HTML5拖拽功能实现的拼图游戏
2018/07/31 HTML / CSS
法国综合购物网站:RueDuCommerce
2016/09/12 全球购物
欧舒丹比利时官网:L’OCCITANE比利时
2017/04/25 全球购物
一份比较全的PHP面试题
2016/07/29 面试题
Java面试题:请说出如下代码的输出结果
2013/04/22 面试题
车间安全生产标语
2014/06/06 职场文书
大学毕业生管理学求职信
2014/09/01 职场文书
会计试用期自我评价
2014/09/19 职场文书
党员群众路线自我剖析材料
2014/10/06 职场文书
2014年副班长工作总结
2014/12/10 职场文书
2016年秋季新学期致辞
2015/07/30 职场文书