机器学习的一个重要应用就是文档的自动分类。

在文档分类中,整个文档(如一封电子邮件)是实例,而文档中的某些元素则构成特征。我们可以观察文档中出现的词,把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。

朴素贝叶斯是贝叶斯分类器的一个扩展,是用于文档分类的常用算法。

朴素贝叶斯算法大致步骤
  1. 收集数据:可以使用任何方法。如RSS源。
  2. 准备数据:数值型或布尔型数据。
  3. 分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好。
  4. 训练算法:计算不同的独立特征的条件概率。
  5. 测试算法:计算错误率。
  6. 使用算法:常用于文档分类。可以在任意分类问题中使用朴素贝叶斯分类器,不一定是文本。

要得到好的概率分布,就需要足够的数据样本,假定样本数为 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法

由统计学知,如果每个特征需要 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法 个样本,那么对于 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_03 个特征将需要 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_04个样本,对于 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_05 个特征将需要 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_06个样本。可以看到,所需要的样本数会随着特征数目增大而迅速增长。

如果特征之间相互独立,那么样本数就可以从 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_06减少到 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_08

所谓独立( 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就会返回一个不重复词表。

首先,创建一个空集,然后将每篇文档返回的新词集合添加到该集合中。操作符 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_09 用于求
两个集合的并集,这也是一个按位或 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_python_10

在数学符号表示上,按位或操作与集合求并操作使用相同记号。


# 文档词集模型
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。


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

知道了如何将一组单词转换为一组数字后,我们看看如何使用这些数字计算概率。
现在已知一个词是否出现在一篇文档中,也知道该文档所属的类别。

重写贝叶斯准则,将之前的 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_11 替换为 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_12。粗体 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_12

朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_14

使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。如何计算呢?

首先可以通过类别 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_python_15(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_16。接下来计算 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_17,这里就要用到朴素贝叶斯假设。如果将 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_12 展开为一个个独立特征,那么就可
以将上述概率写作 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_19

这里假设所有词都互相独立,该假设也称作条件独立性假设,它意味着可以使用 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_python_20 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_21 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_22 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_23 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_24

该函数的伪代码如下:
计算每个类别中的文档数目
对每篇训练文档:
	对每个类别:
		如果词条出现文档中 ——> 增加该词条的计数值
		增加所有词条的计数值
	对每个类别:
		对每个词条:
			将该词条的数目除以总词条数目得到条件概率
	返回每个类别的条件概率
三、测试算法:根据现实情况修改分类器

利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_25 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_26 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_27。如果其中一个概率值为0,那么最后的乘积也为0。

为降低这种影响,可以将所有词的出现数初始化为1,并将分母初始化为2。


另一个遇到的问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_机器学习_28 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_21 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯分类器对连续属性可用_22朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_24 时,由于大部分因子都非常小,所以程序会下溢出。
一种解决办法是对乘积取自然对数。在代数中有 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_32,于是通过求对数可以
避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。

函数 朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_算法_33朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_34


朴素贝叶斯分类器对连续属性可用 朴素贝叶斯分类器实现_朴素贝叶斯算法_35

这两条曲线在相同区域内同时增加或者减少,并且极值点相同。它们的取值虽然不同,但不影响最终结果。修改完如下:

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