8.2 词袋模型简介
记得在第4章中,我们需要将文本或者单词等分类数据转换为数值格式,以方便在机器学习算法中使用。在本节,我们将介绍词袋模型(bag-of-words model),它将文本以数值特征向量的形式来表示。词袋模型的理念很简单,可描述如下:
1)我们在整个文档集上为每个词汇创建了唯一的标记,例如单词。
2)我们为每个文档构建一个特征向量,其中包含每个单词在此文档中出现的次数。
由于每个文档中出现的单词数量只是整个词袋中单词总量很小的一个子集,因此特征向量中的大多数元素为零,这也是我们称之为稀疏(sparse)的原因。这些内容听起来过于抽象,不过毋庸担心,下面我们将逐步讲解创建简单词袋模型的过程。
8.2.1 将单词转换为特征向量
如需根据每个文档中的单词数量构建词袋模型,我们可以使用scikit-learn中的Count-Vectorizer类。如下述代码所示,CountVectorizer以文本数据数组作为输入,其中文本数据可以是个文档或仅仅是个句子,返回的就是我们所要构建的词袋模型:
通过调用CountVectorizer的fit_tranform方法,我们创建了词袋模型的词汇库,并将下面三个句子转换成为稀疏的特征向量:
我们将相关词汇的内容显示出来,以更好地理解相关概念:
由上述命令的运行结果可见,词汇以Python字典的格式存储,将单个的单词映射为一个整数索引。下面我们来看一下之前创建的特征向量:
特征向量中的每个索引位置与通过CountVectorizer得到的词汇表字典中存储的整数值对应。例如,索引位置为0的第一个特征对应单词and,它只在最后一个文档中出现过;单词is的索引位置是1(文档向量中的第二个特征),它在三个句子中都出现过。出现在特征向量中的值也称作原始词频(raw term frequency):tf(t,d)——词汇t在文档d中出现的次数。
我们刚创建的词袋模型中,各项目的序列也称为1元组(1-gram)或者单元组(unigram)模型——词汇表中的每一项或者每个单元代表一个词汇。更普遍地,自然语言处理(NLP)中各项(包括单词、字母、符号)的序列,也称作n元组(n-gram)。n元组模型中数字n的选择依赖于特定的应用;例如,Kanaris等人通过研究发现,在反垃圾邮件过滤中,n的值为3或者4的n元组即可得到很好的效果(Ioannis Kanaris,Konstantinos Kanaris,Ioannis Houvardas,and Efstathios Stamatatos.Words vs Character N-Grams for Anti-Spam Filtering.International Journal on Artificial Intelligence Tools,16(06):1047–1067,2007)。对n元组表示的概念做个总结,分别使用1元组和2元组来表示文档"the sum is shining"的结果如下:
·1元组:"the","sun","is","shining"
·2元组:"the sun","sun is","is shining"
借助于scikit-learn中的CountVecorizer类,我们可以通过设置其ngram_range参数来使用不同的n元组模型。此类默认为1元组,使用ngram_range=(2,2)初始化一个新的CountVectorizer类,可以得到一个z元组表示。
8.2.2 通过词频-逆文档频率计算单词关联度
当我们分析文本数据时,经常遇到的问题就是:一个单词出现在两种类型的多个文档中。这种频繁出现的单词通常不包含有用或具备辨识度的信息。在本小节中,我们将学习一种称为词频-逆文档频率(term frequency-inverse document frequency,tf-idf)的技术,它可以用于解决特征向量中单词频繁出现的问题。tf-idf可以定义为词频与逆文档频率的乘积:
tf-idf(t,d)=tf(t,d)×idf(t,d)
其中,tf(t,d)是我们上一节中介绍的词频,而逆文档频率idf(t,d)可通过如下公式计算:
这里的nd为文档的总数,df(d,t)为包含词汇t的文档d的数量。请注意,分母中加入常数1是可选的,对于没有出现在任何训练样本中的词汇,它能保证分母不为零;取对数是为了保证文档中出现频率较低的词汇不会被赋予过大的权重。
scikit-learn还实现了另外一个转换器:TfidfTransformer,它以CountVectorizer的原始词频作为输入,并将其转换为tf-idf:
在上一小节中我们看到,is在第三个文档中具有最高的词频,它是文档中出现得最为频繁的单词。但是在将特征向量转换为tf-idf后,单词is在第三个文档中只得到了一个相对较小的tf-idf(0.48),这是由于第一和第二个文档中都包含单词is,因此它不太可能包含有用或是有辨识度的信息。
不过,如果我们人工计算特征向量中单个条目的tf-idf值,就会发现,TfidfTrans-former中对tf-idf的计算方式与我们此前定义的标准计算公式不同。scikit-learn中实现的idf和tf-idf分别为:
在scikit-learn中使用的tf-idf公式为:
tf-idf(t,d)=tf(t,d)×(idf(t,d)+1)
通常在计算tf-idf之前都会对原始词频进行归一化处理,TfidfTransformer就直接对tf-idf做了归一化。默认情况下(norm='l2'),scikit-learn中的TfidfTransformer使用L2归一化,它通过与一个未归一化特征向量L2范数的比值,使得返回向量的长度为1:
为确保读者能够理解TfidfTransformer的工作方式,在此我们给出一个例子,计算第三个文档中单词is的tf-idf。
在文档3中,单词is的词频为2(tf=2),由于is在三个文档中都出现过,因此它的文档频率为3(df=3)。由此,idf的计算方法如下:
为了计算tf-idf,我们需要为逆文档频率的值加1,并且将其与词频相乘:
tf-idf("is",d3)=2×(0+1)=2
如果我们对第三个文档中的所有条目重复此过程,将会得到如下tf-idf向量:[1.69,2.00,1.29,1.29,1.29,2.00,1.29]。大家应该注意到了,这里得到的特征向量的值不同于此前我们使用TfidfTransformer得到的值。最后还缺少一个对tf-idf进行L2归一化的步骤,计算方式如下:
可以看到,现在的结果与使用scikit-learn中TfidfTransformer得到的值相同。既然已经理解了tf-idf的计算方式,我们进入下一节,将这些概念应用于电影评论数据集。
8.2.3 清洗文本数据
在前面的小节中,我们学习了词袋模型、词频以及逆文档频率。不过在构建词袋模型之前,最重要的一步就是——通过去除所有不需要的字符对文本数据进行清洗。为了说明此步骤的重要性,我们先展示一下经过重排后数据集中第一个文档的最后50个字符:
正如我们所见,文本中包含HTML标记、标点符号以及其他非字母字符。HTML标记并未包含很多有用语义,标点符号可以在某些NLP语境中提供有用及附加信息。不过,为了简单起见,我们将去除标点符号,只保留表情符号,如":)",因为它们在情感分析中通常是有用的。为了完成此任务,我们将使用Python的正则表达式(regex)库:re,代码如下:
通过代码中的第一个正则表达式<[^>]*>,我们试图移除电影评论中所有的HTML标记。虽然许多程序员通常建议不要使用正则表达式解析HTML,但这个正则表达式足以清理此特定数据集。在移除了HTML标记后,我们使用稍微复杂的正则表达式寻找表情符号,并将其临时存储在emoticons中。接下来,我们通过正则表达式[\W]+删除文本中所有的非单词字符,将文本转换为小写字母,最后将emoticons中临时存储的表情符号追加在经过处理的文档字符串后。此外,为了保证表情符号的一致,我们还删除了表情符号中代表鼻子的字符(-)。
虽然正则表达式为我们提供了一种在字符串中搜索特定字符的方便且有效的方法,但掌握它会有一个相对陡峭的学习曲线,而对正则表达式的讨论也超出了本书的范围。不过,读者可以通过谷歌开发者门户找到相关的入门教程:https://developers.google.com/edu/python/regular-expressions,或者查看Python中re模块的官方文档:https://docs.python.org/3.4/library/re.html。
尽管将表情符号追加到经过清洗的文档字符串的最后,看上去不是最简洁的方法,但是当词袋模型中所有的符号都由单个单词组成时,单词的顺序并不重要。在对文档划分成条目、单词及符号做进一步讨论之前,先来确认一下预处理操作是否能正常工作:
最后,由于我们将在下一节中反复使用在此经过清洗的文本数据,现在通过preprocessor函数移除DataFrame中所有的电影评论信息:
8.2.4 标记文档
准备好电影评论数据集后,我们需要思考如何将文本语料拆分为单独的元素。标记(tokenize)文档的一种常用方法就是通过文档的空白字符将其拆分为单独的单词。
在对文本进行标记的过程中,另外一种有用的技术就是词干提取(word stemming),这是一个提取单词原形的过程,那样我们就可以将一个单词映射到其对应的词干上。最初的词干提取算法是由Martin F.Porter于1979年提出的,由此也称为Porter Stemmer算法[1]。Python自然语言工具包(NLTK,http://www.nltk.org)实现了Porter Stemming算法,我们将在下一小节中用到它。要安装NLTK,只要执行命令:pip install nltk。
尽管NLTK不是本章的重点,若读者对自然语言处理有更进一步的兴趣,作者强烈建议访问NTLK的网站及其官方资料,它们均可通过链接http://www.nltk.org/book/免费获得。
使用nltk包中的PorterStemmer修改tokenizer函数,使单词都恢复到其原始形式,借用前面的例子,单词running恢复为run。
Porter stamming算法可能是最原始也是最简单的词干提取算法了。其他流行的词干提取算法包括Snowball stemmer(Porter2,也称为"English"stemmer)以及Lancaster stemmer(Paice-Husk stemmer),与Porter stemming算法相比,它们提取速度更高不过提取时也更加野蛮。这些算法也在nltk包中得以实现(http://www.nltk.org/api/nltk.stem.html)。
正如前面例子所示,词干提取也可能生成一些不存在的单词,例如前面例子中提到的thu(通过提取thus得到),词形还原(lemmatization)是一种以获得单个单词标准形式(语法上正确)——也就是所谓的词元(lemma)——为目标的技术。不过,相较于词干提取,词形还原的计算更加复杂和昂贵,通过实际应用中的观察发现,在文本分类中,这两种技术对分类结果的影响不大(Michal Toman,Roman Tesar,and Karel Jezek.Influence of word normalization on text classification.Proceedings of InSciT,pages 354-358,2006)。
下一章将使用词袋模型训练一个机器学习模型,在此之前,我们简要介绍下另一种有用的技术:停用词移除(stop-word removal)。停用词是指在各种文本中太过常见,以致没有(或很少)含有用于区分文本所属类别的有用信息。常见的停用词有is、and、has等。由于tf-idf可以降低频繁出现单词的权重,因此当我们使用原始或归一化的词频而不是tf-idf时,移除停用词是很有用的。
我们可通过调用nltk.download函数得到NLTK库提供的停用词,并使用其中的127个停用词对电影评论数据进行停用词移除处理:
完成之后,可以通过如下方式加载和使用由nltk.download得到的英文停用词集:
[1] Martin F.Porter.An algorithm for suffix stripping.Program:electronic library and information systems,14(3):130-137.1980.