机器学习实战(3)-朴素贝叶斯

朴素贝叶斯

这是一种有监督的机器学习算法,以自变量之间的独立(条件特征独立)性和连续变量的正态性假设为前提。

贝叶斯决策理论

核心思想是计算待测数据在各个类别的概率,选择高概率的决策。

条件概率公式:
其中$P(A|B)$称为后验概率,$P(A)$称为先验概率,$\frac{P(B|A)}{P(B)}$称为调整因子。

朴素贝叶斯推断

朴素贝叶斯不能忽略‘朴素’这两个字,它对条件概率分布做了条件独立性的假设。

文本分类

这里我们利用朴素贝叶斯来进行文本分类。
针对社区留言,过滤不恰当的侮辱性的留言,使用1表示侮辱类留言,0表示非侮辱性留言。

准备数据:从文本中构建词向量

首先要将句子转换成向量。考虑出现所有文档中的单词,再决定将哪些单词纳入词汇表或是词汇集合,然后将每一篇文档转换成词汇表上的向量。这里为了方便起见,假设已经将文本分切完毕,并存放到列表中,并对词汇向量进行分类标注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
函数说明:创建实验样本

Parameters:

Returns:
postingList - 实验样本切分的词条
classVec - 类别标签向量
"""
def loadDataSet():
postingList=[['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'], #切分的词条
['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
['my', 'dalmation', 'is', 'so', 'cute', 'I', 'love', 'him'],
['stop', 'posting', 'stupid', 'worthless', 'garbage'],
['mr', 'licks', 'ate', 'my', 'steak', 'how', 'to', 'stop', 'him'],
['quit', 'buying', 'worthless', 'dog', 'food', 'stupid']]
classVec = [0,1,0,1,0,1] # 1表示侮辱,0表示非侮辱 #类别标签向量,1代表侮辱性词汇,0代表不是
return postingList,classVec

上面只是分词列表,我们的重点在于将这些切分好的词条转换为词条向量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def creatVocabList(dataSet):
myVocabList = []
for eachList in dataSet:
for each in eachList:
myVocabList.append(each)

return list(set(myVocabList))
def setOfWords2Vec(vocabList,inputSet):
retVec = [0]*len(vocabList)
for each in inputSet:
if each not in vocabList:
print('{0}不在本字典里'.format(each))
else:
retVec[vocabList.index(each)] = 1
return retVec

首先要利用词条建立单词表,然后将词条进行向量化,一个单词如果出现在单词表中,则在相应位置设置为1,未出现的位置设置为0。
设置一个空列表,存放所有的词条转换的向量:

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
postingList,classVec = loadDataSet()
myVocabList =creatVocabList(postingList)
print(myVocabList)
trainMat = []
for inputSet in postingList:
retVec = setOfWords2Vec(myVocabList,inputSet)
trainMat.append(retVec)
print(trainMat)

训练算法:从词向量计算概率

现在我们就要用朴素贝叶斯来计算一条留言属于侮辱或是非侮辱类的概率。
假设将类别记为$C_i$,表示侮辱类或是非侮辱;每个文档记为$w$,由$w_0、w_1…w_N$这些词构成。这里假设每个词都是独立的。
我们要计算的就是$P(C_i|w)$的概率。
同时由于每个单词都是独立的,所以$P(W|C_i)=P(W_0|C_i)P(W_1|C_i)…P(W_N|C_i)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) # 计算用于训练的文档数量
numWords = len(trainMatrix[0]) # 计算每条文档的词的数量
pAbuse = trainCategory.count(1)/float(numTrainDocs) # 文档属于侮辱类的概率 即P(C1)
w1Num = np.zeros(numWords)
w0Num = np.zeros(numWords)
for i in range(numTrainDocs):
if trainCategory[i] == 1:
# for each in trainMatrix[i]:
# if each == 1:
# w1Num[i] += 1
w1Num += trainMatrix[i] # 统计侮辱类文档中,每个词出现的次数
else:
w0Num += trainMatrix[i]
p1 = w1Num / float(sum(w1Num)) # 计算条件概率p(w|C1)
p0 = w0Num / float(sum(w0Num)) # 计算条件概率p(w|C0)
return p0,p1,pAbuse

这里我们计算出了每个$P(w|C_1)$和$P(W|C_0)$以及$P(C_1)$,方便用贝叶斯公式计算文档属于侮辱类的概率。

测试算法

编写一个分类函数以及测试函数,测试函数中testEntry是待分类的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def classifyNB(VecInput,p0,p1,pAbuse):
classfy_p1 = reduce(lambda x,y:x*y,VecInput*p1) # 计算待分类词向量的条件概率p(w|C1),注意这里假设每个词是条件独立的
classfy_p0 = reduce(lambda x,y:x*y,VecInput*p0) # 计算待分类词向量的条件概率p(w|C0),注意这里假设每个词是条件独立的
result1 = classfy_p1 * pAbuse # 计算该词向量是侮辱类的概率 这里省略了贝叶斯公式的分母,因为对分类结果没有影响
result0 = classfy_p0 * (1-pAbuse) # 计算该词向量是非侮辱的概率
if result1 > result0:
return 1
if result1 < result0:
return 0

def testingNB(testEntry):
postingList, classVec = loadDataSet()
myVocabList = creatVocabList(postingList)
trainMat = []
for inputSet in postingList:
retVec = setOfWords2Vec(myVocabList, inputSet)
trainMat.append(retVec)
p0, p1, pAbuse = trainNB0(trainMat, classVec)


testRetVec = setOfWords2Vec(myVocabList,testEntry)
result = classifyNB(testRetVec,p0,p1,pAbuse)
return result

经过测试:

1
2
3
4
if __name__ == '__main__':
testEntry = ['love', 'my', 'dalmation']
result = testingNB(testEntry)
print(result)

可以发现结果返回None,因为不管是分类到侮辱类还是非侮辱类的概率都是0。

这是因为$P(w|C_1)$和$P(W|C_0)$这一步,由于分词独立,累乘后结果太小了,为了解决这一缺陷,我们对乘积取自然对数:$ln(a*b)=ln(a) + ln(b)$。

另外,由于有的词的概率值如果为0,那么会造成整个结果也为0,所以我们需要将所有词的出现次数初始化为1,分母初始化为2。

观察f(x)与lnf(x)这两条曲线:

可以发现,这两条曲线在相同区域内同时增加或者减少,并且在相同点上取到极值。函数的结果虽然不同,但是对最终的分类结果是没有影响的。

接下来修改trainNB0()和classifyNB()

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
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) # 计算用于训练的文档数量
numWords = len(trainMatrix[0]) # 计算每条文档的词的数量
pAbuse = trainCategory.count(1)/float(numTrainDocs) # 文档属于侮辱类的概率 即P(C1)
w1Num = np.ones(numWords) # 所有词的出现次数初始化为1,拉普拉斯平滑
w0Num = np.ones(numWords)
for i in range(numTrainDocs):
if trainCategory[i] == 1:
# for each in trainMatrix[i]:
# if each == 1:
# w1Num[i] += 1
w1Num += trainMatrix[i] # 统计侮辱类文档中,每个词出现的次数
else:
w0Num += trainMatrix[i]
# 分母初始化为2,拉普拉斯平滑
p1 = np.log(w1Num / (2+float(sum(w1Num)))) # 计算条件概率p(w|C1)
p0 = np.log(w0Num / (2+float(sum(w0Num)))) # 计算条件概率p(w|C0)
return p0,p1,pAbuse

def classifyNB(VecInput,p0,p1,pAbuse):
classfy_p1 = sum(VecInput*p1)
classfy_p0 = sum(VecInput*p0)
result1 = classfy_p1 + np.log(pAbuse) # ln(a*b) = ln(a) +ln(b)
result0 = classfy_p0 +np.log(pAbuse)
if result1 > result0:
return "侮辱类"
if result1 < result0:
return "非侮辱"

经过测试:

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
testEntry = ['love', 'my', 'dalmation']
result = testingNB(testEntry)
print(result)
testEntry = ['stupid','garbage']
result = testingNB(testEntry)
print(result)
# 非侮辱
# 侮辱类

可以看到朴素贝叶斯分类器可以正确分类了。

一点说明

完整代码见bayes.py