朴素贝叶斯是一种基于概率论的分类方法,贝叶斯规则被用于很多分类的理论分析中,首先从实战的代码看一下朴素贝叶斯的操作方法,最后做理论分析,分析一下代码中为什么要这样做。
一、算法原理
贝叶斯规则的公式如下
w的类别可以表示为,在ci类别下w发生的概率乘以ci发生的概率再除以w发生的概率,想要理解书中的代码这一句话就够了。
二、代码实现
先做一个小实验,针对侮辱性和非侮辱性的留言进行分类,代码如下:
1. 文本处理的函数
函数输出:文本集合,文本对应的标签
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]
return postingList,classVec
函数输入:所有的文本集合
函数输出:词汇表
def createVocabList(dataSet):
vocabSet = set([])
for document in dataSet:
vocabSet = vocabSet | set(document) #将完全不重复的dataSet,通过并集赋值到vocabSet
return list(vocabSet)
函数输入:词汇表,测试文本或训练文本
函数输出:词向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0] * len(vocabList)
for word in inputSet:
if word in vocabList: #如果词条存在于词汇表中,则置1
returnVec[vocabList.index(word)] = 1
else: print("the word: %s is not in my Vocabulary!" % word)
return returnVec
这里,我们将vocabSet称为词汇表,inputset称为测试或者训练文本,函数setofwoed2vec()的输出称为词向量。
createVocabList()函数的功能是将全有文本中含有的词汇都提取出来,然后写入一个大的列表中,这个列表中的所有元素都是不重复的。setOfWord2Vec()函数的功能是,对于输入的测试或者训练文本,将文本中的词汇都映射到词向量中,文本中出现的词,在词向量的相应位置就置1。
有了上述两个函数就可以对输入的文本进行处理。
2. 训练函数
训练函数清单:
函数输入:由词向量组成的列表,相应的标签
函数输出:p0vec 侮辱类词条对应的条件概率(p0vec是一个列表,其长度和词向量相同)
p1vec 非侮辱类词条对应的条件概率
pABusive 文本属于侮辱类词条的概率
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)
p0Num = np.zeros(numWords); p1Num = np.zeros(numWords)
p0Denom = 0.0; p1Denom = 0.0
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 = p1Num/p1Denom
p0Vect = p0Num/p0Denom
return p0Vect,p1Vect,pAbusive #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
首先得到文本集合中文本的个数,然后得到词向量的元素个数,因为trainCategory中属于侮辱性文档的的文本对应的值为1,所以由pAbusive = sum(trainCategory)/float(numTrainDocs) 可以得到p(ci)的概率。
进入循环,每当文本文档是侮辱性的时候,就将该文本对应的词向量加到p1num对应的词向量上,然后计算侮辱性文档中的总词数,相除后得到的就是p( w | ci )。这是因为出现几率越高的词汇,在p1num中对应的数字就越大,侮辱性文本中就越有可能含有它。
但实际能够应用的代码稍有修改,因为上述代码中,如果P(w0|1),P(w1|1),P(w2|1)中有一项为0,那么最终的p( w | ci )全部为0,为了解决这个问题,对上述代码做一下修改:
def trainNB0(trainMatrix,trainCategory):
numTrainDocs = len(trainMatrix)
numWords = len(trainMatrix[0])
pAbusive = sum(trainCategory)/float(numTrainDocs)
p0Num = ones(numWords); p1Num = ones(numWords) #初始化为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 #返回属于侮辱类的条件概率数组,属于非侮辱类的条件概率数组,文档属于侮辱类的概率
修改的部分是将分子初始化为1,分母初始化为2,这种做法叫Laplace Smoothing。具体的原理如下
经过修改后,条件概率不会出现为0的情况,同时函数的变化更加平滑。
3. 分类和测试函数
函数输入:待分类的词向量,p0Vec , p1Vec , p(ci)
函数输出:分类结果
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
这个函数很简单,就不解释了,只是需要注意这里用log的加法代替了原来的乘法。
测试函数:
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,'属于非侮辱类')
首先训练分类器,用的数据集就是第一个函数中创建的数据。然后写一个测试的样本,将其向量化后丢到分类器中,输出分类结果。
以上就是整个朴素贝叶斯分类器的工作过程,接下来看它的实际运用的例程。
三、实际运用(用朴素贝叶斯做邮件过滤)
想要对邮件进行分类,首先要获取邮件中的文本信息(即 将邮件中的文本切分成词汇)。所以就需要写一个切分文本的函数:
函数输入:一个大的字符串
函数输出:切分好的词汇文本
def textParse(bigString):
listOfTokens = re.split(r'\W*', bigString) # 把特殊符号作为切分标志,进行字符串切分
return [tok.lower() for tok in listOfTokens if len(tok) > 2] # 除了单个字母,例如大写的I,其它单词变成小写
邮件分类测试函数
函数输入:空
函数输出:分类结果,错误率
def spamTest():
docList = []; classList = []; fullText = []
for i in range(1, 26): #遍历25个txt文件
wordList = textParse(open('email/spam/%d.txt' % i, 'r').read()) #读取每个垃圾邮件,并字符串转换成字符串列表
docList.append(wordList)
fullText.append(wordList)
classList.append(1) #标记垃圾邮件,1表示垃圾文件
wordList = textParse(open('email/ham/%d.txt' % i, 'r').read()) #读取每个非垃圾邮件,并字符串转换成字符串列表
docList.append(wordList)
fullText.append(wordList)
classList.append(0) #标记非垃圾邮件,1表示垃圾文件
vocabList = createVocabList(docList)
trainingSet = list(range(50)); testSet = [] #创建存储训练集的索引值的列表和测试集的索引值的列表
for i in range(10): #从50个邮件中,选10个做测试集
randIndex = int(random.uniform(0, len(trainingSet)))
testSet.append(trainingSet[randIndex])
del(trainingSet[randIndex]) #在训练集列表中删除添加到测试集的索引值
trainMat = []; trainClasses = [] #创建训练集矩阵和训练集类别标签系向量
for docIndex in trainingSet:
trainMat.append(setOfWords2Vec(vocabList, docList[docIndex])) #将生成的词集模型添加到训练矩阵中
trainClasses.append(classList[docIndex]) #将类别添加到训练集类别标签系向量中
p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses)) #训练分类器
errorCount = 0
for docIndex in testSet:
wordVector = setOfWords2Vec(vocabList, docList[docIndex])
if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]: #如果分类错误
errorCount += 1 #错误数加1
print("分类错误的测试集:",docList[docIndex])
print('错误率:%.2f%%' % (float(errorCount) / len(testSet) * 100))
这段程序就是在不停地调用之前的几个函数,并不难读,注释也很清楚,运行后显示的是分类的错误率。
贝叶斯分类器的数学推导,会在之后补充上。