机器学习的一个重要应用就是文档的自动分类。
在文档分类中,整个文档(如一封电子邮件)是实例,而文档中的某些元素则构成特征。我们可以观察文档中出现的词,把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。
朴素贝叶斯是贝叶斯分类器的一个扩展,是用于文档分类的常用算法。
朴素贝叶斯算法大致步骤
- 收集数据:可以使用任何方法。如RSS源。
- 准备数据:数值型或布尔型数据。
- 分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好。
- 训练算法:计算不同的独立特征的条件概率。
- 测试算法:计算错误率。
- 使用算法:常用于文档分类。可以在任意分类问题中使用朴素贝叶斯分类器,不一定是文本。
要得到好的概率分布,就需要足够的数据样本,假定样本数为 。
由统计学知,如果每个特征需要 个样本,那么对于 个特征将需要 个样本,对于 个特征将需要 个样本。可以看到,所需要的样本数会随着特征数目增大而迅速增长。
如果特征之间相互独立,那么样本数就可以从 减少到 。
所谓独立( independence )指的是统计意义上的独立,即一个特征出现的可能性与它和其他特征相邻没有关系。
例如,假设单词 bacon 出现在 unhealthy 后面与出现在 delicious 后面的概率相同。
当然,其实 bacon 常常出现在 delicious 附近,而很少出现在 unhealthy 附近,这个假设正是朴素贝叶斯分类器中朴素(naive)一词的含义。
朴素贝叶斯分类器中的另一个假设是,每个特征同等重要。
尽管上述假设存在一些小的瑕疵,但朴素贝叶斯的实际效果却很好。
如何从文本中获取特征?
要从文本中获取特征,需要先拆分文本。具体如何做呢?
这里的特征是来自文本的词条(token),一个词条是字符的任意组合。可以把词条想象为单词,也可以使用非单词词条,如URL、IP地址或者任意其他字符串。然后将每一个文本片段表示为一个词条向量,其中值为1表示词条出现在文档中,0表示词条未出现。
在网络社区,我们要屏蔽不适当的言论,因此要构建一个快速过滤器,如果某条发言使用了负面或者侮辱性的语言,就将该发言标识为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用1和0分别表示。
一、准备数据:从文本中构建词向量
我们把文本看成单词向量或者词条向量。考虑出现在所有文档中的所有单词,再决定将哪些词汇集合,然后将每一篇文档转换为词汇表上的向量。
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代表不是
return postingList, classVec
loadDataSet函数
创建了一些实验样本。该函数返回的第一个变量是进行词条切分后的文档集合。loadDataset函数
返回的第二个变量是一个类别标签的集合。
这些标注信息用于训练程序以便自动检测不适当发言。
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 取并集
return list(vocabSet)
createVocablist函数
创建一个文档中出现的不重复词的列表,使用了Python的set数据类型。将词条列表输给set构造函数,set就会返回一个不重复词表。
首先,创建一个空集,然后将每篇文档返回的新词集合添加到该集合中。操作符 用于求
两个集合的并集,这也是一个按位或
在数学符号表示上,按位或操作与集合求并操作使用相同记号。
# 文档词集模型
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
# 遍历每个词条
for word in inputSet:
# 如果词条存在于词汇表中,则置1
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return returnVec # 返回文档向量
获得词汇表后,便可以使用 setOfWords2Vec函数
,该函数的输入参数为词汇表及某个文档,输出的是文档向量,向量的每一元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。
首先创建一个和词汇表等长的向量,并将其元素都设置为0。接着,遍历文档中的所有单词,如果出现了词汇表中的单词,则将输出的文档向量中的对应值设为1。
二、训练算法:从词向量计算概率
知道了如何将一组单词转换为一组数字后,我们看看如何使用这些数字计算概率。
现在已知一个词是否出现在一篇文档中,也知道该文档所属的类别。
重写贝叶斯准则,将之前的 替换为 。粗体
使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。如何计算呢?
首先可以通过类别 (侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率 。接下来计算 ,这里就要用到朴素贝叶斯假设。如果将 展开为一个个独立特征,那么就可
以将上述概率写作 。
这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可以使用
该函数的伪代码如下:
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现文档中 ——> 增加该词条的计数值
增加所有词条的计数值
对每个类别:
对每个词条:
将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率
三、测试算法:根据现实情况修改分类器
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 。如果其中一个概率值为0,那么最后的乘积也为0。
为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。
另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 … 时,由于大部分因子都非常小,所以程序会下溢出。
一种解决办法是对乘积取自然对数。在代数中有 ,于是通过求对数可以
避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。
函数 与
这两条曲线在相同区域内同时增加或者减少,并且极值点相同。它们的取值虽然不同,但不影响最终结果。修改完如下:
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) # 计算训练的文档数目
numWords = len(trainMatrix[0]) # 计算每篇文档的词条数
pAbusive = sum(trainCategory)/float(numTrainDocs) # 文档属于侮辱类的概率
p0Num = np.ones(numWords); p1Num = np.ones(numWords) # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
p0Denom = 2.0; p1Denom = 2.0 # 分母初始化为2,拉普拉斯平滑
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num/p1Denom) # 取对数,防止下溢出
p0Vect = np.log(p0Num/p0Denom)
return p0Vect, p1Vect, pAbusive # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
朴素贝叶斯分类函数:
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def testingNB():
listOPosts,listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
trainMat=[]
for postinDoc in listOPosts:
trainMat.append(setOfWords2Vec(myVocabList, postinDoc)) # 将实验样本向量化
p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses)) # 训练朴素贝叶斯分类器
testEntry = ['love', 'my', 'dalmation'] # 测试样本1
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry,'属于非侮辱类') # 执行分类并打印分类结果
testEntry = ['stupid', 'garbage'] # 测试样本2
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry,'属于非侮辱类') # 执行分类并打印分类结果
文档词袋模型:
如果将每个词的出现与否作为一个特征,这可以被描述为词集模型。
如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型。
在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。
为适应词袋模型,需要对 setOfWords2Vec
函数稍加修改,修改后为 bagOfWords2Vec
函数。
它与 setOfWords2Vec
函数唯一不同的是,每当遇到一个单词时,它会增加词向量中的对应值,而不只是将对应的数值设为1。
# 文档词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1 # 更新此处代码
return returnVec
四、使用算法:
import numpy as np
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代表不是
return postingList, classVec
def createVocabList(dataSet):
vocabSet = set([]) # 创建一个空集
for document in dataSet:
vocabSet = vocabSet | set(document) # 取并集
return list(vocabSet)
# 文档词集模型
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList) # 创建一个其中所含元素都为0的向量
# 遍历每个词条
for word in inputSet:
# 如果词条存在于词汇表中,则置1
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return returnVec # 返回文档向量
# 文档词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList) # 创建一个其中所含元素都为0的向量
for word in inputSet: # 遍历每个词条
if word in vocabList: # 如果词条存在于词汇表中,则计数加一
returnVec[vocabList.index(word)] += 1
return returnVec # 返回词袋模型
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix) # 计算训练的文档数目
numWords = len(trainMatrix[0]) # 计算每篇文档的词条数
pAbusive = sum(trainCategory)/float(numTrainDocs) # 文档属于侮辱类的概率
p0Num = np.ones(numWords); p1Num = np.ones(numWords) # 创建numpy.ones数组,词条出现数初始化为1,拉普拉斯平滑
p0Denom = 2.0; p1Denom = 2.0 # 分母初始化为2,拉普拉斯平滑
for i in range(numTrainDocs):
if trainCategory[i] == 1: # 统计属于侮辱类的条件概率所需的数据,即P(w0|1),P(w1|1),P(w2|1)···
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i])
else: # 统计属于非侮辱类的条件概率所需的数据,即P(w0|0),P(w1|0),P(w2|0)···
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect = np.log(p1Num/p1Denom) # 取对数,防止下溢出
p0Vect = np.log(p0Num/p0Denom)
return p0Vect, p1Vect, pAbusive # 返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1) # 对应元素相乘。logA * B = logA + logB,所以这里加上log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1.0 - pClass1)
if p1 > p0:
return 1
else:
return 0
def testingNB():
listOPosts,listClasses = loadDataSet() # 创建实验样本
myVocabList = createVocabList(listOPosts) # 创建词汇表
trainMat=[]
for postinDoc in listOPosts:
trainMat.append(bagOfWords2VecMN(myVocabList, postinDoc)) # 将实验样本向量化
p0V,p1V,pAb = trainNB0(np.array(trainMat),np.array(listClasses)) # 训练朴素贝叶斯分类器
testEntry = ['love', 'my', 'dalmation'] # 测试样本1
thisDoc = np.array(bagOfWords2VecMN(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry,'属于非侮辱类') # 执行分类并打印分类结果
testEntry = ['stupid', 'garbage'] # 测试样本2
thisDoc = np.array(bagOfWords2VecMN(myVocabList, testEntry)) # 测试样本向量化
if classifyNB(thisDoc,p0V,p1V,pAb):
print(testEntry,'属于侮辱类') # 执行分类并打印分类结果
else:
print(testEntry,'属于非侮辱类') # 执行分类并打印分类结果
if __name__ == '__main__':
testingNB()
参考资料:《机器学习实战》、https://cuijiahua.com/blog/2017/11/ml_4_bayes_1.html