[转]《面向程序员的数据挖掘指南》——(七)朴素贝叶斯和文本数据

内容:

  • 自动判别文本中的感情色彩
  • 使用朴素贝叶斯进行分类
  • 去掉常用词和停词
  • 分类新闻组
  • 使用Python实现贝叶斯
  • 情感分析

非结构化文本的分类算法

在前几个章节中,我们学习了如何使用人们对物品的评价(五星、顶和踩)来进行推荐;还使用了他们的隐式评价——买过什么,点击过什么;我们利用特征来进行分类,如身高、体重、对法案的投票等。这些数据有一个共性——能用表格来展现:

因此这类数据我们称为“结构化数据”——数据集中的每条数据(上表中的一行)由多个特征进行描述(上表中的列)。而非结构化的数据指的是诸如电子邮件文本、推特信息、博客、新闻等。这些数据至少第一眼看起来是无法用一张表格来展现的。

举个例子,我们想从推特信息中获取用户对各种电影的评价:

可以看到,Andy Gavin喜欢看地心引力,因为他的消息中有“不寒而栗”、“演的太棒了”之类的文本。而Debra Murphy则不太喜欢这部电影,因为她说“还是省下看这部电影的钱吧”。如果有人说“我太想看这部电影了,都兴奋坏了!”,我们可以看出她是喜欢这部电影的,即使信息中有“坏”这个字。

我在逛超市时看到一种叫Chobani的酸奶,名字挺有趣的,但真的好吃吗?于是我掏出iPhone,谷歌了一把,看到一篇名为“女人不能只吃面包”的博客:

无糖酸奶品评

你喝过Chobani酸奶吗?如果没有,就赶紧拿起钥匙出门去买吧!虽然它是脱脂原味的,但喝起来和酸奶的口感很像,致使我每次喝都有负罪感,因为这分明就是在喝全脂酸奶啊!原味的感觉很酸很够味,你也可以尝试一下蜂蜜口味的。我承认,虽然我在减肥期间不该吃蜂蜜的,但如果我有一天心情很糟想吃甜食,我就会在原味酸奶里舀一勺蜂蜜,太值得了!至于那些水果味的,应该都有糖分在里面,但其实酸奶本身就已经很美味了,水果只是点缀。如果你家附近没有Chobani,也可以试试Fage,同样好吃。

虽然需要花上一美元不到,而且还会增加20卡路里,但还是很值得的,毕竟我已经一下午没吃东西了!

http://womandoesnotliveonbreadalone.blogspot.com/2009/03/sugar-free-yogurt-reviews.html

这是一篇正面评价吗?从第二句就可以看出,作者非常鼓励我去买。她还用了“够味”、“美味”等词汇,这些都是正面的评价。所以,让我先去吃会儿……

自动判别文本中的感情色彩

约翰,这条推文应该是称赞地心引力的!

假设我们要构建一个自动判别文本感情色彩的系统,它有什么作用呢?比如说有家公司是售卖健康检测设备的,他们想要知道人们对这款产品的反响如何。他们投放了很多广告,顾客是喜欢(我好想买一台)还是讨厌(看起来很糟糕)呢?再比如苹果公司召开了一次新闻发布会,讨论iPhone现有的问题,结果是正面的还是负面的呢?一位参议会议员对某个法案做了一次公开演讲,那些政治评论家的反应如何?看来这个系统还是有些作用的。

那要怎样构建一套这样的系统呢?

假设我要从文本中区分顾客对某些食品的喜好,可能就会列出一些表达喜欢的词语,以及表达厌恶的词:

  • 表达喜欢的词:美味、好吃、不错、喜欢、可口
  • 表达厌恶的词:糟糕、难吃、不好、讨厌、恶心

比如我们想知道某篇评论对Chobani酸奶的评价是正面的还是负面的,我们可以去统计评论中表达喜欢和厌恶的词的数量,看哪种类型出现的频率高。这种方法也可以应用到其他分类中,比如判断某个人是否支持堕胎,如果他的言论中经常出现“未出生的小孩”,那他很可能是反堕胎的;如果言论中出现“胎儿”这个词比较多,那有可能是支持堕胎的。其实,用词语出现的数量来进行分类还是很容易想到的。

我们可以使用朴素贝叶斯算法来进行分类,而不是一般的计数。先来回忆一下公式:

argmax表示选取概率最大的分类;h∈H表示计算每个事件的概率;P(D|h)表示在给定h的条件下,D发生的概率(如给定某类文章,这类文章中特定单词出现的概率);P(h)则指事件h发生的概率。

我们的训练集是一组文本,又称为语料库。每个文本(即每条记录)是一则140字左右的推文,并被标记为喜欢和讨厌两类。P(h)表示的就是喜欢和讨厌出现的概率。我们的训练集中有1000条记录,喜欢和讨厌各有500条,因此它们的概率是:

P(喜欢) = 0.5
P(讨厌) = 0.5

当我们使用已经标记好分类的数据集进行训练时,这种类型的机器学习称为“监督式学习”。文本分类就是监督式学习的一种。

如果训练集没有标好分类,那就称为“非监督式学习”,聚类就是一种非监督式学习,我们将在下一章讲解。

还有一些算法结合了监督式和非监督式,通常是在初始化阶段使用分类好的数据,之后再使用未分类的数据进行学习。

让我们回到上面的公式,首先来看P(D|h)要如何计算——在正面评价中,单词D出现的概率。比如说“Puts the Thrill back in Trhiller”这句话,我们可以统计所有表达“喜欢”的文章中第一个单词是“Puts”的概率,第二个单词是“the”的概率,以此类推。接着我们再计算表达“讨厌”的文章中第一个单词是“Puts”的概率,第二个单词是“the”的概率等等。

谷歌曾统计过英语中大约有一百万的词汇,如果一条推文中有14个单词,那我们就需要计算1,000,00014个概率了,显然是不现实的。

的确,这种方法并不可行。我们可以简化一下,不考虑文本中单词的顺序,仅统计表达“喜欢”的文章中某个单词出现的概率。以下是统计方法。

训练阶段

首先,我们统计所有文本中一共出现了多少个不同的单词,记作“|Vocabulary|”(总词汇表)。对于每个单词wk,我们将计算P(wk|hi),每个hi(喜欢和讨厌两种)的计算步骤如下:

  1. 将该分类下的所有文章合并到一起;
  2. 统计每个单词出现的数量,记为n;
  3. 对于总词汇表中的单词wk,统计他们在本类文章中出现的次数nk:
  4. 最后应用下方的公式:

使用朴素贝叶斯进行分类

分类阶段比较简单,直接应用贝叶斯公式就可以了,让我们试试吧!

通过训练,我们得到以下概率结果:

比如下面这句话,要如何判断它是正面还是负面的呢?

I am stunned by the hype over gravity.

我们需要计算的是下面两个概率,并选取较高的结果:

P(like)×P(I|like)×P(am|like)×P(stunned|like)×...

P(dislike)×P(I|dislike)×P(am|dislike)×P(stunned|dislike)×...

因此分类的结果是“讨厌”。

提示 结果中的6.22E-22是科学计数法,等价于6.22×10-22。

哇,这个概率也太小了吧!

是的,如果文本中有100个单词,那乘出来的概率就会更小。

但是Python不能处理那么小的小数,最后都会变成零的。

没错,因此我们要用对数来算——将每个概率的对数相加!

假设一个包含100字的文本中,每个单词的概率是0.0001,那么计算结果是:

>>> 0.0001 ** 100

0.0

如果我们用对数相加来运算的话:

>>> import math

>>> p = 0

>>> for i in range(100):

...     p += math.log(0.0001)

... 

>>> p

-921.034037197617

提示

  • bn = x 可以转换为 logbx = n
  • log10(ab) = log10(a) + log10(b)

新闻组语料库

我们下面要处理的数据集是新闻,这些新闻可以分为不同的新闻组,我们会构造一个分类器来判断某则新闻是属于哪个新闻组的:

比如下面这则新闻是属于rec.motorcycles组的:

注意到这则新闻中还有一些拼写错误(如accesories、ussually等),这对分类器是一个不小的挑战。

这些数据集都来自 http://qwone.com/~jason/20Newsgroups/ (我们使用的是20news-bydate数据集),你也可以从 这里 获得。这个数据集包含18,846个文档,并将训练集(60%)和测试集放在了不同的目录中,每个子目录都是一个新闻组,目录中的文件即新闻文本。

把不要的东西丢掉!

比如我们要对下面这篇新闻做分类:

让我们看看哪些单词是比较重要的:

(helpful – 重要,not helpful – 不重要)

如果我们将英语中最常用的200个单词剔除掉,这篇新闻就成了这样:

去除掉这些单词后,新闻就只剩下一半大小了。而且,这些单词看上去并不会对分类结果产生影响。H.P. Luhn在他的论文中说“这些组成语法结构的单词是没有意义的,反而会产生很多噪音”。也就是说,将这些“噪音”单词去除后是会提升分类正确率的。我们将这些单词称为“停词”,有专门的停词表可供使用。去除这些词的理由是:

  1. 能够减少需要处理的数据量;
  2. 这些词的存在会对分类效果产生负面影响。

常用词和停词

虽然像the、a这种单词的确没有意义,但有些常用词如work、write、school等在某些场合下还是有作用的,如果将他们也列进停词表里可能会有问题。

年轻人,那些常用词是不能随便丢弃的!

因此在定制停词表时还是需要做些考虑的。比如要判别阿拉伯语文档是在哪个地区书写的,可以只看文章中最常出现的词(和上面的方式相反)。如果你有兴趣,可以到我的 个人网站 上看看这篇论文。而在分析聊天记录时,强奸犯会使用更多I、me、you这样的词汇,如果在分析前将这些单词去除了,效果就会变差。

不要盲目地使用停词表!

编写Python代码

首先让我们实现朴素贝叶斯分类器的训练部分。训练集的格式是这样的:

最上层的目录是训练集(20news-bydate-train),其下的子目录代表不同的新闻组(如alt.atheism),子目录中有多个文本文件,即新闻内容。测试集的目录结构也是相同的。因此,分类器的初始化代码要完成以下工作:

  1. 读取停词列表;
  2. 获取训练集中各目录(分类)的名称;
  3. 对于各个分类,调用train方法,统计单词出现的次数;
  4. 计算下面的公式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
from __future__ import print_function

import os, codecs, math



class BayesText:



def __init__(self, trainingdir, stopwordlist):

"""朴素贝叶斯分类器

trainingdir 训练集目录,子目录是分类,子目录中包含若干文本

stopwordlist 停词列表(一行一个)

"""


self.vocabulary = {}

self.prob = {}

self.totals = {}

self.stopwords = {}

f = open(stopwordlist)

for line in f:

self.stopwords[line.strip()] = 1

f.close()

categories = os.listdir(trainingdir)

# 将不是目录的元素过滤掉

self.categories = [filename for filename in categories

if os.path.isdir(trainingdir + filename)]

print("Counting ...")

for category in self.categories:

print(' ' + category)

(self.prob[category],

self.totals[category]) = self.train(trainingdir, category)

# 删除出现次数小于3次的单词

toDelete = []

for word in self.vocabulary:

if self.vocabulary[word] < 3:

# 遍历列表时不能删除元素,因此做一个标记

toDelete.append(word)

# 删除

for word in toDelete:

del self.vocabulary[word]

# 计算概率

vocabLength = len(self.vocabulary)

print("Computing probabilities:")

for category in self.categories:

print(' ' + category)

denominator = self.totals[category] + vocabLength

for word in self.vocabulary:

if word in self.prob[category]:

count = self.prob[category][word]

else:

count = 1

self.prob[category][word] = (float(count + 1)

/ denominator)

print ("DONE TRAINING\n\n")





def train(self, trainingdir, category):

"""计算分类下各单词出现的次数"""

currentdir = trainingdir + category

files = os.listdir(currentdir)

counts = {}

total = 0

for file in files:

#print(currentdir + '/' + file)

f = codecs.open(currentdir + '/' + file, 'r', 'iso8859-1')

for line in f:

tokens = line.split()

for token in tokens:

# 删除标点符号,并将单词转换为小写

token = token.strip('\'".,?:-')

token = token.lower()

if token != '' and not token in self.stopwords:

self.vocabulary.setdefault(token, 0)

self.vocabulary[token] += 1

counts.setdefault(token, 0)

counts[token] += 1

total += 1

f.close()

return(counts, total)

训练结果存储在一个名为prop的字典里,字典的键是分类,值是另一个字典——键是单词,值是概率。

god这个词在rec.motorcycles新闻组中出现的概率是0.00013,而在soc.religion.christian新闻组中出现的概率是0.00424。

训练阶段的另一个产物是分类列表:

训练结束了,下面让我们开始进行文本分类吧。

请尝试编写一个分类器,达成以下效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def classify(self, filename):

results = {}

for category in self.categories:

results[category] = 0

f = codecs.open(filename, 'r', 'iso8859-1')

for line in f:

tokens = line.split()

for token in tokens:

#print(token)

token = token.strip('\'".,?:-').lower()

if token in self.vocabulary:

for category in self.categories:

if self.prob[category][token] == 0:

print("%s %s" % (category, token))

results[category] += math.log(

self.prob[category][token])

f.close()

results = list(results.items())

results.sort(key=lambda tuple: tuple[1], reverse = True)

# 如果要调试,可以打印出整个列表。

return results[0][0]

最后我们编写一个函数对测试集中的所有文档进行分类,并计算准确率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def testCategory(self, directory, category):

files = os.listdir(directory)

total = 0

correct = 0

for file in files:

total += 1

result = self.classify(directory + file)

if result == category:

correct += 1

return (correct, total)



def test(self, testdir):

"""测试集的目录结构和训练集相同"""

categories = os.listdir(testdir)

# 过滤掉不是目录的元素

categories = [filename for filename in categories if

os.path.isdir(testdir + filename)]

correct = 0

total = 0

for category in categories:

print(".", end="")

(catCorrect, catTotal) = self.testCategory(

testdir + category + '/', category)

correct += catCorrect

total += catTotal

print("\n\nAccuracy is %f%% (%i test instances)" %

((float(correct) / total) * 100, total))

在不使用停词列表的情况下,这个分类器的效果是:

准确率77.77%,看起来很不错。如果用了停词列表效果会如何呢?

那让我们来测试一下吧!

请自行到网络上查找一些停词列表,并填写以下表格:

我找到了两个停词列表,分别是包含25个词174个词 的列表,结果如下:

看来第二个停词列表能提升2%的效果,你的结果如何?

朴素贝叶斯与情感分析

情感分析的目的是判断作者的态度或意见:

情感分析的例子之一是判断一篇评论是正面的还是反面的,我们可以用朴素贝叶斯算法来实现。我们可以用Pang&Lee 2004的影评数据来测试,这份数据集包含1000个正面和1000个负面的评价,以下是一些示例:

本月第二部连环杀人犯电影实在太糟糕了!虽然开头的故事情节和场景布置还可以,但后面就……

当我听说罗密欧与朱丽叶又出了一部改编电影后,心想莎士比亚的经典又要被糟蹋了。不过我错了,Baz Luhrman导演的水平还是高的……

你可以从 http://www.cs.cornell.edu/People/pabo/movie-review-data/ 上下载这个数据集,并整理成以下形式:

你也可以从这里下载整理好的数据。

动手实践

你可以为上文的朴素贝叶斯分类器增加十折交叉验证的逻辑吗?它的输出结果应该是如下形式:

另外,请计算Kappa指标。

再次声明:只看不练是不行的,就好比你不可能通过阅读乐谱就学会弹奏钢琴。

解答

这是我得到的结果:

Kappa指标则是:

所以我们的分类算法效果是不错的。

代码链接

英文原文:http://guidetodatamining.com/chapter-7/

文章出处:https://github.com/jizhang/guidetodatamining/blob/master/chapter-7.md

转自:http://dataunion.org/8895.html

其他章节