前言
翻看贝叶斯的案例,我们就会发现90%以上的案例都是文本分类,如果想以后转做文本数据数据挖掘,那么贝叶斯应该是必须点亮的技能灯。“真的是任重道远啊,希望三天时间能skip到下一个算法。”
正文
我们将文本数据的标签用c表示,c包含多个变量,c_i,特征用w表示,w_i,也就是下面说的词条,同时假设,特征之间是相互独立的。
另外在实际做的过程中,也会发现,相比于之前做的贝叶斯分类器,特征是二维变量,而在文本分类器中,实际上特征只是一维变量,这在计算类条件概率上会简单一些。当然,复杂的贝叶斯分类器应该也会涉及到多特征维度的情况。当特征太多的时候,也就到了贝叶斯的极限了,除非我们依然可以保证相互独立性,负责贝叶斯的预测误差就会出现。#仅为个人理解,希望指正,以后有新的认识,我会回来修正这个理解。
下面是伪代码:
#计算每个类别中的文档数目 #也就是$P(C_i)$,各类标签的文档数目
#对每篇训练文档:
# 对每个类别
# 如果词条出现在文档中:增加该词条计数值
# 增加该词条计数值
# 对每个类别:
# 对每个词条:
# 将该词条的数目除以总词条数目得到条件概率 #求$P(w|c_i)$
核心思想:利用文本构建词库向量
有关词向量的概念解释,参考:https://blog.csdn.net/michael_liuyu09/article/details/78029062
其实这里的只是简单的向量化,方便我们统计词频,但是深入到NLP的研究中,词库向量就会被利用计算相关性,这在NLP中应该比较重要,现在没有涉及,以后希望有机会
import numpy as np
#数据导入模块
def loadDataSet():
postingList=[['my','dog','has','flea','problems','help','please'],
['maybe','not','take','him','to','dog','park','stupid'],
['my','dalmation','id','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): #dataSet是指loadDataSet的反馈文本
vocabSet = set([]) #python中的set是一个无序,去重的集合
for document in dataSet:
vocabSet = vocabSet | set(document) #set(document)对每一个句子进行去重唯一,然后与vocabSet进行合并,扩充vocabSet
return(list(vocabSet))
#构建词库向量
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0]*len(vocabList) #构建0向量,[0,1]分布
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 trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #计算我们的文本容量,文件数
numwords = len(trainMatrix[0]) #计算样本库词汇数
pAbusive = sum(trainCategory)/float(numTrainDocs) #计算$P_c_i$
p0Num=np.zeros(numwords)
p1Num=np.zeros(numwords)
p0Denom = 0.0; p1Denom = 0.0
for i in range(numTrainDocs): #遍历每一篇文本
if trainCategory[i]==1: #条件概率分类1的情况
p1Num += trainMatrix[i] #累计每个词汇出现的次数
p1Denom += sum(trainMatrix[i]) #累计分类1中的所有词汇的出现次数
else: #条件概率分类0的情况
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect=p1Num/p1Denom #计算每个词汇在分类1中出现的概率 P(w_i|c_1)
p0Vect=p0Num/p0Denom #计算每个词汇在分类1中出现的概率 P(w_i|c_0)
return(p0Vect, p1Vect, pAbusive)
#main函数
if __name__ == "__main__":
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
trainMat = []
for postinDoc in listOPosts: #对文本内容逐行遍历,进行向量化
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V,p1V,pAb = trainNB0(trainMat,listClasses) #
print(p0V, p1V, pAb)
其实现在,我们正常情况下就可以计算了
套用我们的公式,P(c_i|w) = \frac{P(w|c_i)P(c_i)}{P(w)}
如果单纯考虑后验概率最大化,我们只需要计算分子部分,上面的P(w|c_i)我们可以通过p0V,p1V连乘得到。然后分开比较大小就可以帮助我们做出判断。
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1): #vec2Classify是我们将目标文本向量化的产物
p1 = sum(vec2Classify * p1Vec) * pClass1
p0 = sum(vec2Classify * p0Vec) * (1 - pClass1)
if (p1 > p0):
return(1)
if (p1 < p0):
return(0)
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as', classifyNB(thisDoc, p0V, p1V, pAb))
但是现实情况并非如此完美,如果我们已有的样本中某分类下并没有该特征词,会导致p(w_i|c_i)=0,这会进而导致我们在计算似然函数时结果为0的情况,因此为了避免这个问题,我们引入“拉普拉斯修正”。在条件概率式子中分子分母分别加入一个正数,\lambda>0。当\lambda=0的时候,就是我们平时说的极大似然函数,当\lambda=1的时候,我们称为拉普拉斯平滑。
公式为:p(w|c_i) = \frac{\sum_{i=1}^N(I(x_j|c_i))+\lambda}{\sum_{i=1}^N(I(c_i))+S_j\lambda}, p(ci)=\frac{\sum_{i=1}^NI(c_i)+\lambda}{N+K\lambda} ,其中,S_j为每一种X的种类数,K为属性个数
在这里我们用拉普拉斯平滑,令\lambda=1,因为我们样本标签分为2类,一类是好的语言,一类是有侮辱性的语言,那么,在我们令K=2,同时我们的样本每个单词同属一类特征,那么,S=1
在代码里,我们初始化p0Num=1, p1Num=1, p0Denom=2.0, p1Denom=2.0
在这里我们会遇到一个新的问题,那就是数据溢出,在计算中,当数字非常小的时候,而我们还在做连乘的时候,会出现数字下溢出的问题,为了避免这个问题实际中,我们用转换函数,换种方式计算,避免数字过小的问题。我们引入ln(a*b)=ln(a)+ln(b)
我们先比较f(x)和ln(f(x))的区别
import matplotlib.pyplot as plt
x=np.linspace(0.01,0.9*np.pi,30)
f=np.sin(x) #我们假设原函数f(x)为sin函数
g=np.log(f) #我们假设实际函数为log(f(x))
plt.plot(x,f)
plt.plot(x,g)
plt.legend(['f(x)','log(f(x))'])
plt.show()
在上图中,我们发现虽然两个函数不完全相同,但是,两个函数的极值点很接近,这对于我们贝叶斯在使用极大似然定理里,影响不大,因此,我们引入ln函数替换连乘问题。
接下来,我们对前面的部分函数进行优化
#统计频数,计算后验概率
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) #计算我们的文本容量,文件数
numwords = len(trainMatrix[0]) #计算样本库词汇数
pAbusive = sum(trainCategory)/float(numTrainDocs) #计算$P_c_i$
p0Num=np.ones(numwords)
p1Num=np.ones(numwords)
p0Denom = 2.0; p1Denom = 2.0
for i in range(numTrainDocs): #遍历每一篇文本
if trainCategory[i]==1: #条件概率分类1的情况
p1Num += trainMatrix[i] #累计每个词汇出现的次数
p1Denom += sum(trainMatrix[i]) #累计分类1中的所有词汇的出现次数
else: #条件概率分类0的情况
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i])
p1Vect=np.log(p1Num/p1Denom) #计算每个词汇在分类1中出现的概率 P(w_i|c_1)
p0Vect=np.log(p0Num/p0Denom) #计算每个词汇在分类1中出现的概率 P(w_i|c_0)
return(p0Vect, p1Vect, pAbusive)
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1): #vec2Classify是我们将目标文本向量化的产物
p1 = sum(vec2Classify * p1Vec) + np.log(pClass1)
p0 = sum(vec2Classify * p0Vec) + np.log(1 - pClass1)
if (p1 > p0):
return(1)
if (p1 < p0):
return(0)
if __name__ == "__main__":
listOPosts, listClasses = loadDataSet()
myVocabList = createVocabList(listOPosts)
trainMat = []
for postinDoc in listOPosts: #对文本内容逐行遍历,进行向量化
trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
p0V,p1V,pAb = trainNB0(trainMat,listClasses) #
print(p0V, p1V, pAb)
在这里,我们可以发现,文本向量矩阵的值已经变了,但是不影响我们的结果。
testEntry = ['love', 'my', 'dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as', classifyNB(thisDoc, p0V, p1V, pAb))
testEntry = ['stupid','dalmation']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry, 'classified as', classifyNB(thisDoc, p0V, p1V, pAb))
到这里,贝叶斯方法的实践应该是比较深入了,算法的使用也好,还是具体一些细节问题的思考也好,接下来就是抽时间看下邮件分类有没有时间做