[TOC]
本章内容:
朴素贝叶斯:
朴素贝叶斯是贝叶斯决策理论的一部分,所以有必要快速了解一下贝叶斯决策理论。
假设现在又一个数据集,它由两类数据组成,数据分布如图 4-1:
图 4-1
假设找到了描述图中两类数据的统计参数,现在用 p1(x,y) 表示数据点 (x,y) 属于类别 1 (图中用圆点表示的类别)的概率,用 p2(x,y) 表示数据点 (x,y) 属于类别 2 (图中用三角形表示的类别)的概率,那么对于一个新数据点 (x,y) ,可以用下面的规则来判断它的类别:
也就是说,我们会选择高概率对应的类别,这就是贝叶斯决策理论的核心思想,即选择具有最高概率的决策。
类似上图应该选择哪种方法来进行分类?
假设现在有一个装了 7 块石头的罐子,其中 3 块是灰色的,4 块是黑色的。如果从罐子中随机取出一块石头,那么是灰色石头的概率为 3/7,是黑色石头的概率为 4/7,。使用 P(gray) 来表示取到灰色石头的概率,其概率值可以通过灰色石头数目除以总的石头数目来得到。
图 4-3
要计算 P(gray) 或者 P(black),事先知道石头所在桶的信息会不会改变结果?计算从 B 桶中取到灰色石头的概率的办法,这就是所谓的 条件概率(conditional probability)。假定计算的是从 B 桶取到灰色石头的概率,这个概率可以记作 P(gray|bucketB),我们称之为 "在已知石头出自 B 桶取到灰色石头的概率。不难得到,P(gray|bucketA) = 2/4 ,P(gray|bucketB) = 1/3.
条件概率的计算公式如下:
P(gray|bucketB) = P(gray and bucketB) / P(bucketB)
另一种有效计算条件概率的方法称为 贝叶斯准则。贝叶斯准则告诉我们如何交换条件概率中的条件与结果,即如果已知 P(x|c),要求 P(c|x),那么可以使用下面的计算方法:
P(c|x) = P(x|c)P(c) / P(x)
第 1 节提到贝叶斯决策理论要求计算两个概率 p1(x,y) 和 p2(x,y):
但这两个准则并不是贝叶斯决策理论的所有内容。真正需要计算和比较的是 p(c1|x,y) 和 p(c2|x,y),即:给定某个由 x,y 表示的数据点,那么该数据点来自类别 c1 的概率是多少?数据点来自 c2 的概率又是多少?
具体地,应用贝叶斯准则得到:
p(c_i|x,y)=\frac{p(x,y|c_i)p(c_i)}{p(x,y)}
使用这些定义,可以定义贝叶斯分类准则为:
使用贝叶斯准则,可以通过已知的三个概率值来计算未知的概率值。
机器学习的一个重要应用就是文档的自动分类。可以观察文档中出现的此,并把每个此的出现或者不出现作为一个特征,这样得到的特征数目就会跟词汇表中的词目一样多。
使用每个此作为特征并观察它们是否出现,这样得到的特征数目会有多少呢?
朴素贝叶斯的一本过程:
假定样本数为 N,由统计学知,如果每个特征需要 N 的样本,那么对于 1000 个特征将需要 $N^{1000}$ 个样本。可以看到,所需要的样本数会随着特征数目增大而迅速增长。
如果特征之间相互独立,那么样本数就可以从 $N^{1000}$ 减少到 1000*N 。所谓 独立(independence) 指的是统计意义上的独立,即一个特征或者单词出现的可能性与它和其他相邻没有关系。
这个假设正是朴素贝叶斯分类器中 朴素(naive) 一词的含义。朴素贝叶斯分类器中的另一个假设是,每个特征同等重要。
要从文本中获取特征,需要先拆分文本。这里的特征是来自文本的词条(token),一个词条是字符的任意组合。然后将每一个文本片段表示为一个词条向量,其中值为 1 表示词条出现在文档中,
以在线社区的留言板为例。为了不影响社区的发展,我们要屏蔽侮辱性的言论,所以要构建一个快速过滤器,如果某条留言使用了负面或侮辱性的语言,那么久将该留言标识为内容不当。对此问题建立两个类别:侮辱类和非侮辱类,使用 1 和 0 分别表示。
朴素贝叶斯分类器通常有两种实现方式:一种基于贝努利模型实现(只考虑出不出现,不考虑出现的次数),一种基于多项式模型实现(会考虑词在文档中出现的次数)。
我们将把文本看成单词向量或者词条向量,也就是说将橘子转换成向量。考虑出现在所有文档中的所有单词,再决定将哪些词纳入词汇表或者说所要的词汇集合,然后必须将每一篇文档转换成词汇表上的向量。
创建一个叫 bayes.py 的文件,添加 loadDataSet() 函数:
- def loadDataSet():
- """
- 创建样本
- :return: postingList:进行词条切分后的文档集合,classVec:类别标签集合
- """
- 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
添加 createVocabList(dataSet) 函数:
- def createVocabList(dataSet):
- """
- 得到集合的并集
- :param dataSet:
- :return:
- """
- vocabSet = set([])
- for document in dataSet:
- vocabSet = vocabSet | set(document)
- return list(vocabSet)
添加 setOfWords2Vec(vocabList, inputSet) 函数:
- def setOfWords2Vec(vocabList, inputSet):
- """
- 输出文档在词汇表中形成的文档向量:文档中出现某个词,那么文档向量中这个词的对应值就是 1
- :param vocabList: 词汇表
- :param inputSet: 文档
- :return: 文档向量
- """
- 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 vocabularty!" % word)
- return returnVec
查看运行效果:
- listOPosts, listClasses = loadDataSet()
- myVocabList = createVocabList(listOPosts)
- print(myVocabList) # ['my', 'dog', 'flea', 'worthless', 'dalmation', 'how', 'maybe', ' has', 'stop', 'buying', 'love', 'help', 'to', 'not', 'problems', 'please', 'stupid', 'him', 'posting', 'licks', 'so', 'I', 'is', 'quit', 'garbage', 'ate', 'take', 'food', 'steak', 'mr', 'cute', 'park']
- vec1 = setOfWords2Vec(myVocabList,listOPosts[0])
- print(vec1) # [1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
$$ p(c_i\vec{w})=\frac{p(\vec{w}c_i)p(c_i)}{p(\vec{w})}$$
我们将使用上述公式,对每个类计算该值,然后比较这两个概率值的大小。如何计算呢?
该函数的伪代码如下:
- 计算每个类别中的文档数目
- 对每篇训练文档:
- 对每个类别:
- 如果词条出现文档中 >> 增加该词条的计数值
- 增加所有词条的计数值
- 对每个类别:
- 对每个词条:
- 将该词条的数目除以总词条数目得到条件概率
- 返回每个类别的条件概率
按照上述伪代码的逻辑,添加 trainNB0(trainMatrix, trainCategory) 函数来实现:
- def trainNB0(trainMatrix, trainCategory):
- """
- 朴素贝叶斯分类器训练函数
- :param trainMatrix: 文档矩阵
- :param trainCategory: 由每篇文档类别标签所构成的向量
- :return:
- """
- 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:
- p1Num += trainMatrix[i]
- p1Denom += sum(trainMatrix[i])
- else:
- p0Num += trainMatrix[i]
- p0Denom += sum(trainMatrix[i])
- p1Vect = p1Num/p1Denom # change to log()
- p0Vect = p0Num/p0Denom # change to log()
- return p0Vect,p1Vect,pAbusive
运行验证:
- listOPosts, listClasses = loadDataSet()
- myVocabList = createVocabList(listOPosts)
- # print(myVocabList) # ['my', 'dog', 'flea', 'worthless', 'dalmation', 'how', 'maybe', ' has', 'stop', 'buying', 'love', 'help', 'to', 'not', 'problems', 'please', 'stupid', 'him', 'posting', 'licks', 'so', 'I', 'is', 'quit', 'garbage', 'ate', 'take', 'food', 'steak', 'mr', 'cute', 'park']
- # vec1 = setOfWords2Vec(myVocabList,listOPosts[0])
- # print(vec1) # [1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
- trainMat = []
- for postinDoc in listOPosts:
- trainMat.append(setOfWords2Vec(myVocabList, postinDoc))
- print('myVocabList:',myVocabList) # ['my', 'I', 'maybe', 'food', 'buying', 'so', 'to', 'mr', 'park', 'dog', 'is', ' has', 'ate', 'take', 'how', 'stop', 'flea', 'steak', 'worthless', 'love', 'help', 'quit', 'cute', 'dalmation', 'him', 'licks', 'posting', 'not', 'problems', 'please', 'stupid', 'garbage']
- print('trainMat:',trainMat) # [[1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0], [0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0], [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1], [1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]]
- print('listClasses:',listClasses) # [0, 1, 0, 1, 0, 1]
- p0v,p1v,pAb = trainNB0(trainMat, listClasses)
- print('p0v:',p0v)
- # [ 0.125 0.04166667 0. 0. 0. 0.04166667
- # 0.04166667 0.04166667 0. 0.04166667 0.04166667 0.04166667
- # 0.04166667 0. 0.04166667 0.04166667 0.04166667 0.04166667
- # 0. 0.04166667 0.04166667 0. 0.04166667 0.04166667
- # 0.08333333 0.04166667 0. 0. 0.04166667 0.04166667
- # 0. 0. ]
- print('p1v:',p1v)
- # [ 0. 0. 0.05263158 0.05263158 0.05263158 0.
- # 0.05263158 0. 0.05263158 0.10526316 0. 0. 0.
- # 0.05263158 0. 0.05263158 0. 0. 0.10526316
- # 0. 0. 0.05263158 0. 0. 0.05263158
- # 0. 0.05263158 0.05263158 0. 0. 0.15789474
- # 0.05263158]
- print('pAb:',pAb) # 0.5
利用贝叶斯分类器对文档进行分类时,要计算多个概率的乘积以获得文档属于某个类别的概率,即计算 p(W0|1)p(W1|1)p(W2|1)。如果其中一个概率值为 0,那么最后的乘积也为 0。为降低这种影响,可以将所有词的出现数初始化为 1,并将分母初始化为 2。
即 trainNB0() 中修改为:
- # 初始化概率
- p0Num = np.ones(numWords) # 非侮辱性文档向量的加和
- p1Num = np.ones(numWords) # 侮辱性文档向量的加和
- p0Denom = 2.0 # 非侮辱性文档的概率
- p1Denom = 2.0 # 侮辱性文档的概率
另一个问题是下溢出,这是由于太多很小的数相乘造成的。当计算乘积 p(W0|Ci)p(W1|Ci)...p(Wn|Ci) 时,由于大部分因子都非常小,所以程序会下溢出或者得不到正确的答案。一种解决办法是对乘积取自然对数。在代数中有 ln(a*b) = ln(a) + ln(b),于是通过求对数可以避免下溢出或者浮点数舍入导致的错误。同时,采用自然对数进行处理不会有任何损失。
图 404 给出函数 f(x) 与 ln(f(x)) 的曲线。检查这两条曲线,就会发现它们在相同区域内同时增加或减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。通过修改 return 前的两行代码,将上述做法用到分类器中:
- p1Vect = np.log(p1Num/p1Denom) # change to log()
- p0Vect = np.log(p0Num/p0Denom) # change to log()
图 4-4
添加 classifyNB(vec2Classify, p0Vec, p1Vec, pClass1) 函数:
- def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):
- """
- 朴素贝叶斯分类
- :param vec2Classify: 要分类的向量
- :param p0Vec:非侮辱性文档的向量
- :param p1Vec:侮辱性文档的向量
- :param pClass1:
- :return:
- """
- p1 = sum(vec2Classify * p1Vec) + np.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']
- thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
- print(testEntry,'classified as:',classifyNB(thisDoc, p0V,p1V,pAb)) # ['love', 'my', 'dalmation'] classified as: 0
- testEntry = ['stupid', 'garbage']
- thisDoc = np.array(setOfWords2Vec(myVocabList, testEntry))
- print(testEntry,'classified as:',classifyNB(thisDoc, p0V,p1V,pAb)) # ['stupid', 'garbage'] classified as: 1
- testingNB()
我们将每个此的出现与否作为一个特征,这可以被描述为 词集模型(set-of-words model)。如歌一个词再来文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为 词袋模型(bag-of-words mode;)。在词袋中,每个单词可以出现多次,而在词集中,每个此只能出现一次。为适应词袋模型,需要对函数 setOfWords2Vec() 做修改,修改后的函数称为 bagOfWords2Vec():
- def bagOfWords2Vec(vocabList, inputSet):
- """
- 朴素贝叶斯词袋模型
- :param vocabList: 词汇表
- :param inputSet: 文档
- :return: 文档向量
- """
- returnVec = [0] * len(vocabList)
- for word in inputSet:
- if word in inputSet: # 每当遇到一个单词时,就增加向量中的对应值,而不只是将对应的数值设为1
- returnVec[vocabList.index(word)] += 1
- return returnVec
示例:使用朴素贝叶斯对电子邮件进行分类:
切分,去除标点、空格,大写转小写等:
- def textParse(bigString):
- import re
- listOfTokens = re.split(r'\W*',bigString)
- return [tok.lower() for tok in listOfTokens if len(tok)>2] # 去掉少于两个字符的单词,转小写
添加 spamTest() 函数:
- def spamTest():
- """
- 对贝叶斯垃圾邮件分类器进行自动化处理
- :return: 错误率
- """
- docList = []
- classList = []
- fullText = []
- for i in range(1,26): # 取数据
- filetxt = open('email/spam/%d.txt' % i).read() # 读取文本文件
- wordList = textParse(filetxt)
- docList.append(wordList)
- fullText.extend(wordList)
- classList.append(1)
- filetxt = open('email/ham/%d.txt' % i).read()
- wordList = textParse(filetxt)
- docList.append(wordList)
- fullText.extend(wordList)
- classList.append(0)
- vocabList = createVocabList(docList)
- trainingSet = list(range(50))
- testSet = []
- for i in range(10):
- randIndex = int(np.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])
- re_learn = classifyNB(np.array(wordVector), p0V, p1V, pSpam)
- print('re_learn:',re_learn)
- print('classList[docIndex]:',classList[docIndex])
- if re_learn != classList[docIndex]:
- errorCount += 1
- print('the error rate is :', float(errorCount)/len(testSet))
测试代码:
- spamTest()#the error rate is: 0.1
如果发现错误的话,函数会输出错分文档的词表,这样就可以了解到底是哪篇文档发生了错误,如果想要更好地估计错误率,那么久应该讲上述过程重复多次,然后求取平均值。
注意:这里一直出现吗的错误是将垃圾邮件误判为正常邮件。相比之下,将垃圾邮件误判为正常邮件要比将正常邮件归到垃圾邮件好。
在这个例子中,我们将分析中美国的两个城市中选取一些人,通过分析这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。若干结论确实是不同,那么他们各自常用的词是哪些?从人们的用词当中,我们能否对不同城市的人所关心的内容有所了解?
示例:使用朴素贝叶斯来发现地区相关的用词
我们的目的不是使用分类器进行分类,而是通过观察单词和条件概率值来发现与特定城市相关的内容。
构建一个类似于 spamTest() 函数来对测试过程自动化 localWords:
- def localWords(feed1, feed0):
- """
- 数据源来自:RSS,移除高频单词,其他基本与 spamTest() 一致
- :param feed1:
- :param feed0:
- :return:
- """
- import feedparser
- docList = []
- classList = []
- fullText = []
- minlen = min(len(feed1['entries']),len(feed0['entries'])) # 获取条目较少的RSS源的条目数
- for i in range(minlen): # 解析和处理数据
- wordList = textParse(feed1['entries'][i]['summary'])
- docList.append(wordList)
- fullText.extend(wordList)
- classList.append(1)
- wordList = textParse(feed0['entries'][i]['summary'])
- docList.append(wordList)
- fullText.extend(wordList)
- classList.append(0)
- vocabList = createVocabList(docList) # 所有词条列表
- top30Words = calcMostFreq(vocabList, fullText) # 高频的30个单词
- for pairW in top30Words:
- if pairW[0] in vocabList:vocabList.remove(pairW[0])
- trainingSet = list(range(2*minlen))
- testSet = []
- for i in range(20):
- randIndex = int(np.random.uniform(0,len(trainingSet)))
- testSet.append(trainingSet[randIndex])
- del(trainingSet[randIndex])
- trainMat = []
- trainClasses = []
- for docIndex in trainingSet:
- trainMat.append(bagOfWords2Vec(vocabList, docList[docIndex]))
- trainClasses.append(classList[docIndex])
- p0V, p1V, pSpam = trainNB0(np.array(trainMat), np.array(trainClasses))
- errorCount = 0
- for docIndex in testSet:
- wordVector = bagOfWords2Vec(vocabList, docList[docIndex])
- if classifyNB(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
- errorCount += 1
- print('the error rate is: ',float(errorCount)/len((testSet)))
- return vocabList, p0V, p1V
辅助函数 calcMostFreq() 函数:
- def calcMostFreq(vocabList, fullText):
- """
- 遍历词汇表中的每个此并统计它在文本中出现的次数,然后根据出现次数从高到低对词典排序,最后返回排序最高的30个
- :param vocabList:
- :param fullText:
- :return:
- """
- import operator
- freqDict = {}
- for token in vocabList:
- freqDict[token]=fullText.count(token)
- sortedFreq = sorted(freqDict.items(),key=operator.itemgetter(1),reverse=True)
- return sortedFreq[:30]
移除高频单词的原因:高频单词会占据较高的次数,产生这种现象的原因是语言中大部分都是冗余和结构辅助性内容,另一个常用的方法是不仅移除高频单词,同时从某个预定词表中移除结构上的辅助词。该词表称为 停用词表(stop word list) 。
添加 getTopWords() 函数:
- def getTopWords(ny,sf):
- import operator
- vocaList,p0V,p1V=localWords(ny,sf) # 训练并测试朴素贝叶斯分类器
- topNY = []
- topSF = []
- for i in range(len(p0V)): #存储大于某个阈值(0.6)的所有词
- if p0V[i] > -6.0: topSF.append((vocaList[i], p0V[i]))
- if p1V[i] > -6.0: topNY.append((vocaList[i], p1V[i]))
- sortedSF = sorted(topSF, key=lambda pair:pair[1],reverse=True)
- print('------------SF-------------')
- for item in sortedSF:
- print(item[0])
- sortedNF = sorted(topNY, key=lambda pair:pair[1], reverse=True)
- print('-----------NF-----------')
- for item in sortedNF:
- print(item[0])
对于分类而言,使用概率有时要比使用硬规则更为有效。贝叶斯概率即贝叶斯准则提供了一种可以利用已知值来估计未知概率的有效方法。
可以通过特征之间的条件独立性假设,降低对数据量的需求。独立性假设是指一个词的出现概率并不依赖于文档中的其他词。因为这个假设过于简单,所有称为朴素贝叶斯。
来源: http://www.jianshu.com/p/47b7b35ca90f