本章的最后一个例子非常有趣。我们前面介绍了朴素贝叶斯的两个实际应用的例子,第一个例子是过滤网站的恶意留言,第二个是过滤垃圾邮件。分类还有大量的其他应用。我曾经见过有人使用朴素贝叶斯从他喜欢及不喜欢的女性的社交网络档案学习相应的分类器,然后利用该分类器测试他是否会喜欢一个陌生女人。分类的可能应用确实有很多,比如有证据表示,人的年龄越大,他所用的词也越好。那么,可以基于一个人的用词来推测他的年龄吗?除了年龄之外,还能否推测其他方面?广告商往往想知道关于一个人的一些特定人口统计信息,以便能够更好地定向推销广告。从哪里可以获得这些训练数据呢?事实上,互联网上拥有大量的训练数据。几乎任一个能想到的利基市场1都有专业社区,很多人会认为自己属于该社区。4.5.1节中的斑点犬爱好者网站就是一个非常好的例子。
1. 利基(niche)是指针对企业的优势细分出来的市场,这个市场不大,而且没有得到令人满意的服务。产品推进这个市场,有盈利的基础。在这里特指针对性和专业性都很强的产品。也就是说,利基是细分市场没有被服务好的群体。——译者注
在这个最后的例子当中,我们将分别从美国的两个城市中选取一些人,通过分析这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。如果结论确实是不同,那么他们各自常用的词是哪些?从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解?
示例:使用朴素贝叶斯来发现地域相关的用词
- 收集数据:从RSS源收集内容,这里需要对RSS源构建一个接口。
- 准备数据:将文本文件解析成词条向量。
- 分析数据:检查词条确保解析的正确性。
- 训练算法:使用我们之前建立的
trainNB0
函数。- 测试算法:观察错误率,确保分类器可用。可以修改切分程序,以降低错误率,提高分类结果。
- 使用算法:构建一个完整的程序,封装所有内容。给定两个RSS源,该程序会显示最常用的公共词。
下面将使用来自不同城市的广告训练一个分类器,然后观察分类器的效果。我们的目的并不是使用该分类器进行分类,而是通过观察单词和条件概率值来发现与特定城市相关的内容。
4.7.1 收集数据:导入RSS源
接下来要做的第一件事是使用Python下载文本。幸好,利用RSS,这些文本很容易得到。现在所需要的是一个RSS阅读器。Universal Feed Parser是Python中最常用的RSS程序库。
你可以在http://code.google.com/p/feedparser/下浏览相关文档,然后和其他Python包一样来安装feedparse。首先解压下载的包,并将当前目录切换到解压文件所在的文件夹,然后在Python提示符下敲入>>python setup.py install
。
下面使用Craigslist上的个人广告,当然希望是在服务条款允许的条件下。打开Craigslist上的RSS源,在Python提示符下输入:
>>> import feedparser>>>ny=feedparser.parse(/'http://newyork.craigslist.org/stp/index.rss/')
我决定使用Craigslist中比较纯洁的那部分内容,其他内容稍显少儿不宜。你可以查阅feedparser.org中出色的说明文档以及RSS源。要访问所有条目的列表,输入:
>>> ny[/'entries/']>>> len(ny[/'entries/'])100
可以构建一个类似于spamTest
的函数来对测试过程自动化。打开文本编辑器,输入下列程序清单中的代码。
程序清单4-6 RSS源分类器及高频词去除函数
#❶(以下四行)计算出现频率 def calcMostFreq(vocabList,fullText): import operator freqDict = {} for token in vocabList: freqDict[token]=fullText.count(token) sortedFreq = sorted(freqDict.iteritems, key=operator.itemgetter(1), reverse=True) return sortedFreq[:30]def localWords(feed1,feed0): import feedparser docList=; classList = ; fullText = minLen = min(len(feed1[/'entries/']),len(feed0[/'entries/'])) for i in range(minLen): #❷ 每次访问一条RSS源 wordList = textParse(feed1[/'entries/'][i][/'summary/']) docList.append(wordList) fullText.extend(wordList) classList.append(1) wordList = textParse(feed0[/'entries/'][i][/'summary/']) docList.append(wordList) fullText.extend(wordList) classList.append(0) #❸(以下四行)去掉出现次数最高的那些词 vocabList = createVocabList(docList) top30Words = calcMostFreq(vocabList,fullText) for pairW in top30Words: if pairW[0] in vocabList: vocabList.remove(pairW[0]) trainingSet = range(2*minLen); testSet= for i in range(20): randIndex = int(random.uniform(0,len(trainingSet))) testSet.append(trainingSet[randIndex]) del(trainingSet[randIndex]) trainMat=; trainClasses = for docIndex in trainingSet: trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) trainClasses.append(classList[docIndex]) p0V,p1V,pSpam = trainNB0(array(trainMat),array(trainClasses)) errorCount = 0 for docIndex in testSet: wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) if classifyNB(array(wordVector),p0V,p1V,pSpam) != classList[docIndex]: errorCount += 1 print /'the error rate is: /',float(errorCount)/len(testSet) return vocabList,p0V,p1V
上述代码类似程序清单4-5中的函数spamTest
,不过添加了新的功能。代码中引入了一个辅助函数calcMostFreq
❶。该函数遍历词汇表中的每个词并统计它在文本中出现的次数,然后根据出现次数从高到低对词典进行排序,最后返回排序最高的30个单词。你很快就会明白这个函数的重要性。
下一个函数localWords
使用两个RSS源作为参数。RSS源要在函数外导入,这样做的原因是RSS源会随时间而改变。如果想通过改变代码来比较程序执行的差异,就应该使用相同的输入。重新加载RSS源就会得到新的数据,但很难确定是代码原因还是输入原因导致输出结果的改变。函数localWords
与程序清单4-5中的spamTest
函数几乎相同,区别在于这里访问的是RSS源❷而不是文件。然后调用函数calcMostFreq
来获得排序最高的30个单词并随后将它们移除❸。函数的剩余部分与spamTest
基本类似,不同的是最后一行要返回下面要用到的值。
你可以注释掉用于移除高频词的三行代码,然后比较注释前后的分类性能❸。我自己也尝试了一下,去掉这几行代码之后,我发现错误率为54%,而保留这些代码得到的错误率为70%。这里观察到的一个有趣现象是,这些留言中出现次数最多的前30个词涵盖了所有用词的30%。我在进行测试的时候,vocabList
的大小约为3000个词。也就是说,词汇表中的一小部分单词却占据了所有文本用词的一大部分。产生这种现象的原因是因为语言中大部分都是冗余和结构辅助性内容。另一个常用的方法是不仅移除高频词,同时从某个预定词表中移除结构上的辅助词。该词表称为停用词表(stop word list),目前可以找到许多停用词表(在本书写作期间,http://www.ranks.nl/resources/stopwords.html 上有一个很好的多语言停用词列表)。
将程序清单4-6中的代码加入到bayes.py
文件之后,可以通过输入如下命令在Python中进行测试:
>>> reload(bayes)<module /'bayes/' from /'bayes.py/'>>>>ny=feedparser.parse(/'http://newyork.craigslist.org/stp/index.rss/')>>>sf=feedparser.parse(/'http://sfbay.craigslist.org/stp/index.rss/')>>> vocabList,pSF,pNY=bayes.localWords(ny,sf)the error rate is: 0.1>>> vocabList,pSF,pNY=bayes.localWords(ny,sf)the error rate is: 0.35
为了得到错误率的精确估计,应该多次进行上述实验,然后取平均值。这里的错误率要远高于垃圾邮件中的错误率。由于这里关注的是单词概率而不是实际分类,因此这个问题倒不严重。可以通过函数caclMostFreq
改变要移除的单词数目,然后观察错误率的变化情况。
4.7.2 分析数据:显示地域相关的用词
可以先对向量pSF与pNY进行排序,然后按照顺序将词打印出来。下面的最后一段代码会完成这部分工作。再次打开bayes.py文件,将下面的代码添加到文件中。
程序清单4-7 最具表征性的词汇显示函数
def getTopWords(ny,sf): import operator vocabList,p0V,p1V=localWords(ny,sf) topNY=; topSF= for i in range(len(p0V)): if p0V[i] > -6.0 : topSF.append((vocabList[i],p0V[i])) if p1V[i] > -6.0 : topNY.append((vocabList[i],p1V[i])) sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True) print /"SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF** for item in sortedSF: print item[0] sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True) print /"NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY **/" for item in sortedNY: print item[0]
程序清单4-7中的函数getTopWords
使用两个RSS源作为输入,然后训练并测试朴素贝叶斯分类器,返回使用的概率值。然后创建两个列表用于元组的存储。与之前返回排名最高的X个单词不同,这里可以返回大于某个阈值的所有词。这些元组会按照它们的条件概率进行排序。
下面看一下实际的运行效果,保存bayes.py文件,在Python提示符下输入:
>>> reload(bayes)<module /'bayes/' from /'bayes.pyc/'>>>> bayes.getTopWords(ny,sf)the error rate is: 0.2SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**SF**lovetimewilltherehitsendfranciscofemaleNY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**NY**friendpeoplewillsinglesexfemalenight420relationshipplayhope
最后输出的单词很有意思。值得注意的现象是,程序输出了大量的停用词。移除固定的停用词看看结果会如何变化也十分有趣。依我的经验来看,这样做的话,分类错误率也会降低。