学习《机器学习实战》(朴素贝叶斯)
第四章原理挺好理解的,但是很多代码让我很不解,感觉和给出的公式并不一样,希望有大佬能给我指明一下
先上概率论学过的贝叶斯公式:
大致理解,设Ai代表一系列事物特征,Bi代表某一结果,若已知B发生的情况下,Ai的概率,现在想求某种A发生时Bi的概率,就用这个公式。
朴素贝叶斯的适用范围,像垃圾邮件分类、分本分类这样二分类或多分类的情况效果较好, 核心思想是选择高概率对应的类别 ,所以不用特别准确的求出各自的概率,能比较出来结果即可
开始确定已知的B和A, 也就是训练集数据,书上用分类言论为辱骂类还是正常类的数据举的例子。
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
postingList中每一行相当于一个向量A, classVec中是其对应的结果B, 有了这些数据,根据上面的公式,我们可以求出结果B下,A中每个特征发生的概率,再乘于结果B发生的概率,就可以把贝叶斯公式中的分子求出来了。
然后考虑怎么求,把分子的公式拿下来
P(Bi)发生的概率,就让它发生的数量除以classVec的长度即可
P(A|Bi)就需要,让结果为Bi的那些行,按列累加,得到每个特征发生的数量,再除以Bi的总行数, 这样可以得到每个特征在Bi下发生的概率,但是这和P(A|Bi)还有点不同,因为它们可能是相关的,所以无法直接概率相乘,为了把这个步骤简化,这个算法就假设每个单词的出现没有半毛钱的关系即独立,那就可以写成P(A|Bi) = P(A1|Bi)*P(A2|Bi)…这样, 这也是朴素贝叶斯中”朴素“的含义。当然这是我理解的,书上讲的我感觉就是这样,但是代码里的计算就看不太懂了。
这时候突然意识到一个问题,刚才按列累加,默认每列是一个特征了, 并且1表示存在0表示不存在,事实上数据是没有规律的字符串数组,所以需要写一个函数转化一下,将字符串数组变成标识特征的向量。
# 从数据集中抽取词汇表 输入:[][] 输出:[]
def createVocabList(dataSet):
vocabSet = set([]) # 集合去重
for document in dataSet:
vocabSet = vocabSet | set(document)
return list(vocabSet)
# 将字符串数组转换成向量 输入:[][] [] 输出:[] 词集模型
def setOfWords2Vec(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] = 1
else:
print("the word: %s is not in my Vocabulary!" % word)
return returnVec
这样就满足了
可以看一下用最开始的数据,得到的向量
因为创建词汇表时,每个单词的位置是随机的,所以只能通过数1的方式大致判断一下
接下来就可以训练分类器了
import numpy as np
# 朴素贝叶斯分类器训练函数 输入:[][] [] 输出:[] [] float
def trainNB0(trainMatrix, trainCategory):
numTrainDocs = len(trainMatrix) # 数据的个数
numWords = len(trainMatrix[0]) # 特征个数
pAbusive = sum(trainCategory)/float(numTrainDocs) # 好巧妙 求辱骂言论的概率
p0Num = np.ones(numWords) # 初始为1
p1Num = np.ones(numWords) # 初始为1
p0Denom = 2.0 # p(0)发生的总词条数 # 初始为2
p1Denom = 2.0 # p(1)发生的总词条数 # 初始为2
for i in range(numTrainDocs):
if trainCategory[i] == 1:
p1Num += trainMatrix[i]
p1Denom += sum(trainMatrix[i]) # ????
else:
p0Num += trainMatrix[i]
p0Denom += sum(trainMatrix[i]) # ????
p1Vect = p1Num/p1Denom
p0Vect = p0Num/p0Denom
return p0Vect, p1Vect, pAbusive
上面代码中的初始为1,初始为2,在公式中相当于分子加1,分母加2是为了防止某一概率为0,导致相乘后结果都为0了。这里面分母的求法让我很费解,难道不应该是直接除以行数吗, 也就是每次循环+1,这里为什么要求和?暂时还想不明白…
这个函数可以求得各个特征在结果是辱骂类或正常类的时候的概率,以及邮件中辱骂类的比例,正常类就等于1减去辱骂类的比例
有了这个结果就可以带入贝叶斯公式,完成分类了
书上又做了一步优化,考虑到了下溢出,因为向量只有0或者1,而词汇表中的总数却很多,所以每个特征的概率都很小,再相乘,结果小的不得了,很可能就下溢出了,所以通过取对数的方式,把小数变成大数,而且这个函数本身递增,不影响最后的概率比较。
接下来就是分类函数了
import math
# 朴素贝叶斯分类函数 vec2Classify:要分类的向量[] p0Vec:0时各个词条的概率[] p1Vec:1时各个词条的概率[] pClass1:
def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
p1 = sum(vec2Classify * p1Vec) + math.log(pClass1) # ???
p0 = sum(vec2Classify * p0Vec) + math.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']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
testEntry = ['stupid', 'garbage']
thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
print(testEntry,'classified as: ',classifyNB(thisDoc,p0V,p1V,pAb))
输出
整段代码最难理解的就算p0、p1的计算,这个我也不明白,就代码而言,公式中的乘变成了加,是因为求对数,为什么前面不先求个log再求和,而是直接求和,之后再与结果概率的对数相加。我暂且理解为,求不求log对结果的比较无影响,这样可以简化计算。
词袋模型
上面的特征向量,是通过0和1来表示有还是没有,这样的描述被称为词集模型, 事实上单词出现的频率也很能说明文本的主题,所以想把0和1改成对应单词出现的次数,这样的描述称为词袋模型。
词袋模型只需要改一下转换向量的函数即可,每次加1而不是取1
# 词袋模型
def bagOfWords2VecMN(vocabList, inputSet):
returnVec = [0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)] += 1 # 每次加1
return returnVec
下面是案例
使用朴素贝叶斯过滤垃圾邮件
这是朴素贝叶斯最著名的应用
准备数据阶段,将文本切分成字符串数组
交叉验证,也是一段神奇的代码
# 文件解析及完整的垃圾邮件测试函数
def textParse(bigString): #input is big string, #output is word list
import re
listOfTokens = re.split(r'\W+', bigString)
return [tok.lower() for tok in listOfTokens if len(tok) > 2]
def spamTest():
docList=[] # 所有文本信息
classList = [] # 所有标签信息
fullText =[] # 所有字符串数组
for i in range(1,26):
wordList = textParse(open('email/spam/%d.txt' % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(1)
wordList = textParse(open('email/ham/%d.txt' % i).read())
docList.append(wordList)
fullText.extend(wordList)
classList.append(0)
vocabList = createVocabList(docList) # 创建词汇表
trainingSet = list(range(50)); testSet=[]
for i in range(10): # 10个测试数据
randIndex = int(np.random.uniform(0,len(trainingSet))) # 0-len 的随机数字
testSet.append(trainingSet[randIndex]) # 保存对应下标
del(trainingSet[randIndex]) # 从训练集中移除
trainMat=[]; trainClasses = []
for docIndex in trainingSet: # train the classifier (get probs) trainNB0
trainMat.append(bagOfWords2VecMN(vocabList, docList[docIndex])) # 转化成向量
trainClasses.append(classList[docIndex])
p0V,p1V,pSpam = trainNBO(np.array(trainMat),np.array(trainClasses))
errorCount = 0
for docIndex in testSet: # classify the remaining items
wordVector = bagOfWords2VecMN(vocabList, docList[docIndex]) # 测试向量
if classifyNB(np.array(wordVector),p0V,p1V,pSpam) != classList[docIndex]: # 分类不正确
errorCount += 1
print("classification error",docList[docIndex])
print('the error rate is: ',float(errorCount)/len(testSet))
return vocabList,fullText
书上说平均为6%
接着后面讲了一些用RSS源的数据,我访问得到的数据都为空就不记录了。
第四章完结