3.朴素贝叶斯和KNN算法的推导和python实现

  前面一个博客我们用Scikit-Learn实现了中文文本分类的全过程,这篇博客,着重分析项目最核心的部分分类算法:朴素贝叶斯算法以及KNN算法的基本原理和简单python实现。

3.1 贝叶斯公式的推导

  简单介绍一下什么是贝叶斯:

  

  让我们从一个故事开始。

  1 看着后视镜往前开车

  想象这么一个场景,我开着车,经过笔直的大道,快速地往下一个路口驶去。我知道,到了下一个路口就要右转了。
  这件事情很简单,我坐在驾驶室内,看到下一个路口,往右边打方向盘就好了:
  马同学高等数学
  突然,不管什么原因(这故事是我写的,可以安排一百种原因,干脆就不解释),反正前挡风玻璃碎了:
  马同学高等数学
  不要纠结我这幅图,反正你已经无法看清前面的路了,那怎么知道什么时候该右转?
  还好,开车的是一位数学家,智商及时上线。
  数学家根据自己的经验,估计这条笔直的道路上:
  这也就意味着如果随意的右转,有95\%的概率是错误的。
  数学家从后视镜看出去,发现后面有一辆车在打右转弯灯,他意识到:
  
  新的信息出现了,此时如果右转,错误的概率就比之前小很多。
  这种思考方法,就是贝叶斯定理所阐述的思考方法。

  2 结合开车来理解贝叶斯公式

  我们来看看贝叶斯公式是怎么写的:

  P(A|B)=P(A)\frac{P(B|A)}{P(B)}

  先把刚才的开车给符号化:
  
  我们再来理解下贝叶斯公式:
  
  因此贝叶斯公式实际上阐述了这么一个事情:
  
  我们再通过韦恩图来理解一下这个事情(为了观看方便,下面的A,B的圆形面积是示意):
  
  
  新的信息的出现,比如之前看到了亮着右转弯灯的车,就好比知道点已经落入了B
  至于为什么B事件发生后导致的调整为:

  \frac{P(B|A)}{P(B)}

  这就需要代数了,推导也不复杂,这里就不演算了。
  2.1 小结
  我们可以看到有形的十字路口,却看不到明天是否下雨,我们可以看到前方是否有路障,却不清楚下一次飞机是否会出事。甚至有时候,眼睛还会欺骗我们。
  很多时候,我们不得不看着后视镜开车,这个时候概率论、贝叶斯定理就是我们的指路明灯。
  看着后视镜开车,肯定常常会撞车,没关系,我们可以不断的去修正我们的假设。
  比如,撞了几次车之后,就发现可能之前估计的在十字路口打右转弯灯的数据明显偏大了,我们修正之后再继续开车。我们人类的学习,本身也是一个试错的过程。

3 贝叶斯定理与人脑

  贝叶斯定理现在很多人在研究,就是因为不少人相信贝叶斯定理和人脑的工作机制很像(此处颇多争论,望自行判断),因此成为机器学习的基础。
  比如,你和对方聊天的时候,如果对方说出“虽然”两个字,你大概就会猜测,对方后继九成的可能性会说出“但是”。我们的大脑看起来就好像是天生在用贝叶斯定理。
  吴军博士在他的著作《数学之美》里面就提到了,最早的自然语言处理,比如翻译、语音识别,都是通过语法分析来进行的,就是把“主谓宾”分析清楚,然后处理,正确率惨不忍睹。
  后来,google由自然语言处理专家贾里尼克领导的部门,通过统计、概率方法进行上述研究,正确率提高了很多。在书中也列举了贝叶斯定理是如何参与自然语言处理的。
  书中还有一句业界广为流传的名言:
  语法是人类后来总结出来的,我们天生是不需要语法就可以开口说话的,或许,人脑真的是贝叶斯大脑。
  最后,有一个小小的问题,根据我们的经验,硬币正反两面出现的概率都是50\%,如果我扔了一千次都是正面,那说明了什么?
  介绍完贝叶斯的基本概念,再来看一下贝叶斯在文本分类上该如何应用。就文本分类来说,我们可以认为词袋中的两两词之间的关系是相互独立的,也就是说对象的特征向量中每个维度都是相互独立的。例如,虽然苹果和梨子有共同的属性,但是苹果和梨是相互独立的,这也是朴素贝叶斯理论的思想基础。
  朴素贝叶斯分类的正式定义如下:

第(3)步的各个条件概率,我们可以按以下步骤计算:

 

 3.2 贝叶斯算法的实现

   为了将主要的精力集中在算法本身,我们使用简单的英文语料作为数据集。

import sys
import os
from numpy import *
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','my'],
                 ['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 is abusive, 0 not
    return postingList,classVec

#编写贝叶斯算法类,创建默认的构造方法
class NBayes(object):
    def __init__(self):
        self.vocabulary = [] #词典
        self.idf = 0 #词典的IDF权值向量
        self.tf = 0 #训练集的权值矩阵
        self.tdm=0  #P(x|yi)
        self.Pcates = {}    #p(yi)是一个类别字典,训练样本中0占多少比例,1占多少比例
        self.labels= [] #对应每个文本的分类,是一个外部导入的列表
        self.doclength = 0 #训练集文本数
        self.vocablen = 0 #词典词长
        self.testset = 0 #测试集

    #导入和训练数据集,生成算法所必需的参数和数据结构
    def train_set(self,trainset,classVec):
        self.cate_prob(classVec)#计算每个分类在数据集中的概率P(Yi)
        self.doclength = len(trainset)#训练集合的个数
        print(self.doclength)
        tempset = set()
        #生成词典(去重后的词典,即训练样本中的所有词汇)
        [tempset.add(word) for doc in trainset for word in doc]
        print(tempset)
        #训练样本词典
        self.vocabulary = list(tempset)
        #词典长度
        self.vocablen = len(self.vocabulary)
        self.calc_wordfreq(trainset)#计算词频数据集
        self.build_tdm() #按分类累计向量空间的每维值P(X|Yi)

    #计算在数据集中每个分类的概率P(Yi)
    def cate_prob(self,classVec):
        self.labels = classVec
        #输出各个分类
        print(self.labels)
        #转成集合,去重,只有0,1
        labeltemps = set(self.labels)#获取全部分类
        print(labeltemps)
        for labeltemp in labeltemps:
            # 统计列表中重复的分类:self.labels.count(labeltemp)
            #0的比例占多少,1的比例占多少
            self.Pcates[labeltemp] = float(self.labels.count(labeltemp))/float(len(self.labels))
            print(self.Pcates[labeltemp])

    #生成普通的词频向量
    def calc_wordfreq(self,trainset):
        self.idf = np.zeros([1,self.vocablen])#1*词典单词数,统计的是所有的样本单词的次数
        print(self.idf)
        self.tf = np.zeros([self.doclength,self.vocablen])#训练集文件数*词典数,统计的是每一个训练样本单词的次数
        print(self.tf)
        #遍历训练集
        for indx in range(self.doclength):
            # 遍历每一个训练样本中的每个词
            for word in trainset[indx]:
                #相应位置上的单词+1,第indx行,self.vocabulary.index(word)列
                #self.vocabulary.index(word)返回的是word在词典中的下标
                self.tf[indx,self.vocabulary.index(word)] += 1
            #整个词典的计数加1
            for signleword in set(trainset[indx]):
                self.idf[0,self.vocabulary.index(signleword)] += 1
        print(self.idf)
        print(self.tf)


    #按分类累计计算向量空间的每维值P(X|Yi)
    def build_tdm(self):
        self.tdm = np.zeros([len(self.Pcates),self.vocablen])#类别个数*词典长度
        print(self.tdm)
        sumlist = np.zeros([len(self.Pcates),1])#类别个数*1
        print(sumlist)
        #遍历所有的训练样本
        for indx in range(self.doclength):
            #求同一类别的词向量空间值加总,加的是每一个训练样本对应的每个词的个数
            self.tdm[self.labels[indx]] += self.tf[indx]
            #统计每个分类的总的词数
            sumlist[self.labels[indx]] = np.sum(self.tdm[self.labels[indx]])
        print(self.tdm)
        print(sumlist)
        self.tdm = self.tdm/sumlist#生成P(X|Yi),即求每个分类的单词,占该分类的所有单词总数的比例
        print(self.tdm)

    #将测试集映射到当前词典
    def map2vocab(self,testdata):
        #计算测试集的特征向量(非线性代数的特征向量,而是一行中对词的统计信息)
        self.testset = np.zeros([1,self.vocablen])
        for word in testdata:
            self.testset[0,self.vocabulary.index(word)] += 1

    #将预测分类,输出预测的分类类别
    def predict(self,testset):
        #np.shape(testset)[1]列值
        if np.shape(testset)[1] != self.vocablen:#如果测试集长度与字典不相同则退出程序
            print("输入错误")
            exit(0)
        predvalue = 0 #初始化后类别概率
        predclass = "" #初始化类别名称
        #tdm是每一类某个单词,占该类的总的单词的比例,后面一个参数是所有类别
        for tdm_vect,keyclass in zip(self.tdm,self.Pcates):
            #P(X|Yi) P(Yi)
            #变量tdm,计算最大分类值
            print("---"*30)
            print(testset*tdm_vect*self.Pcates[keyclass])
            temp = np.sum(testset*tdm_vect*self.Pcates[keyclass])
            print(temp)
            #选择概率最大的一个
            if temp > predvalue:
                predvalue = temp
                predclass = keyclass
        return  predclass

#dataset:句子的词向量
#listClasses是句子所属于的类别[0,1,0,1,0,1]
dataSet,listClasses = loadDataSet()#导入外部数据集
nb = NBayes()#实例化
nb.train_set(dataSet,listClasses)#训练数据集
nb.map2vocab(dataSet[2])#随机选择一个测试句
print(nb.predict(nb.testset))#输出分类结果

 3.3 kNN分类算法

  前面使用了朴素贝叶斯算法实现了文本分类。下面我们可以考虑通过计算向量间的距离衡量相似度来进行文本分类。这就是kNN算法,一种基于向量间相似度的分类算法。

3.3.1 kNN算法原理

  k最近邻(k-Nearest Neighbor)算法是比较简单的机器学习算法。采用测量不同特征值之间的距离方法进行分类。它的思想很简单:如果一个样本在特征空间中的k个最近邻(最相似)的样本中的大多数都属于某一个类别,则该样本也属于这个类别。第一个字母k可以小写,表示外部定义的近邻数量。举个栗子:

  有一个数据集,这个数据集很简单,由二维空间上的4个点构成一个矩阵:

  其中前 两个点构成一个类别A,后两个点构成一个类别B。

代码:

import sys
import os
from numpy import *
import numpy as np
import matplotlib.pyplot as plt
import operator

def createDataSet():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group,labels

dataSet,labels = createDataSet()
#绘图
fig = plt.figure()
ax = fig.add_subplot(111)
indx = 0
for point in dataSet:
    if labels[indx] == 'A':
        ax.scatter(point[0],point[1],c='blue',marker='o',linewidths=0,s=300)
        plt.annotate("("+str(point[0])+","+str(point[1])+")",xy = (point[0],point[1]))
    else:
        ax.scatter(point[0], point[1], c='red', marker='^', linewidths=0, s=300)
        plt.annotate("(" + str(point[0]) + "," + str(point[1]) + ")", xy=(point[0], point[1]))
    indx += 1
plt.show()

 

如下图 

  如图所示,可以清晰得看到由4个点构成的训练集。该训练集呗分成两个类别:A类----蓝色圆圈,B类----红色三角形。可以看出红色区域的点距比它们到蓝色区域内的点距要小得多,这种分类也是自然而然的。

  下面再给出测试集,并把它加入到刚才的矩阵中去。

  对测试集进行绘图:

import sys
import os
from numpy import *
import numpy as np
import matplotlib.pyplot as plt
import operator

def createDataSet():
    group = array([[1.0,1.1],[1.0,1.0],[0,0],[0,0.1]])
    labels = ['A','A','B','B']
    return group,labels

dataSet,labels = createDataSet()
#绘图
fig = plt.figure()
ax = fig.add_subplot(111)
indx = 0
for point in dataSet:
    if labels[indx] == 'A':
        ax.scatter(point[0],point[1],c='blue',marker='o',linewidths=0,s=300)
        plt.annotate("("+str(point[0])+","+str(point[1])+")",xy = (point[0],point[1]))
    else:
        ax.scatter(point[0], point[1], c='red', marker='^', linewidths=0, s=300)
        plt.annotate("(" + str(point[0]) + "," + str(point[1]) + ")", xy=(point[0], point[1]))
    indx += 1

testdata=[0.2,0.2]
ax.scatter(testdata[0], testdata[1], c='green', marker='^', linewidths=0, s=300)
plt.annotate("(" + str(testdata[0]) + "," + str(testdata[1]) + ")", xy=(testdata[0], testdata[1]))

plt.show()

  很清晰,从距离上看,它更接近红色的三角形范围,应该归入B类,这就是kNN算法的基本原理。

  由此可见,kNN算法应该由如下的步骤构成:

  第三阶段:统计这k个样本点钟各个类别的数量。如上图所示,如果我们选定k值为3,则B类样本(三角形)有两个,A类样本(圆形)有1个,那么我们就把这个数据点定为B类,根据k个样本中数量最多的样本是什么类别,我们就把这个数据点定为什么类别。

3.3.2 kNN算法的Python实现

import sys
import os
from numpy import *
import numpy as np
import operator
from Nbayes_lib import *

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','my'],
                 ['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 is abusive, 0 not
    return postingList,classVec

#编写贝叶斯算法类,创建默认的构造方法
class NBayes(object):
    def __init__(self):
        self.vocabulary = [] #词典
        self.idf = 0 #词典的IDF权值向量
        self.tf = 0 #训练集的权值矩阵
        self.tdm=0  #P(x|yi)
        self.Pcates = {}    #p(yi)是一个类别字典,训练样本中0占多少比例,1占多少比例
        self.labels= [] #对应每个文本的分类,是一个外部导入的列表
        self.doclength = 0 #训练集文本数
        self.vocablen = 0 #词典词长
        self.testset = 0 #测试集

    #导入和训练数据集,生成算法所必需的参数和数据结构
    def train_set(self,trainset,classVec):
        self.cate_prob(classVec)#计算每个分类在数据集中的概率P(Yi)
        self.doclength = len(trainset)#训练集合的个数
        print(self.doclength)
        tempset = set()
        #生成词典(去重后的词典,即训练样本中的所有词汇)
        [tempset.add(word) for doc in trainset for word in doc]
        print(tempset)
        #训练样本词典
        self.vocabulary = list(tempset)
        #词典长度
        self.vocablen = len(self.vocabulary)
        self.calc_wordfreq(trainset)#计算词频数据集
        self.build_tdm() #按分类累计向量空间的每维值P(X|Yi)

    #计算在数据集中每个分类的概率P(Yi)
    def cate_prob(self,classVec):
        self.labels = classVec
        #输出各个分类
        print(self.labels)
        #转成集合,去重,只有0,1
        labeltemps = set(self.labels)#获取全部分类
        print(labeltemps)
        for labeltemp in labeltemps:
            # 统计列表中重复的分类:self.labels.count(labeltemp)
            #0的比例占多少,1的比例占多少
            self.Pcates[labeltemp] = float(self.labels.count(labeltemp))/float(len(self.labels))
            print(self.Pcates[labeltemp])

    #生成普通的词频向量
    def calc_wordfreq(self,trainset):
        self.idf = np.zeros([1,self.vocablen])#1*词典单词数,统计的是所有的样本单词的次数
        print(self.idf)
        self.tf = np.zeros([self.doclength,self.vocablen])#训练集文件数*词典数,统计的是每一个训练样本单词的次数
        print(self.tf)
        #遍历训练集
        for indx in range(self.doclength):
            # 遍历每一个训练样本中的每个词
            for word in trainset[indx]:
                #相应位置上的单词+1,第indx行,self.vocabulary.index(word)列
                #self.vocabulary.index(word)返回的是word在词典中的下标
                self.tf[indx,self.vocabulary.index(word)] += 1
            #整个词典的计数加1
            for signleword in set(trainset[indx]):
                self.idf[0,self.vocabulary.index(signleword)] += 1
        print(self.idf)
        print(self.tf)


    #按分类累计计算向量空间的每维值P(X|Yi)
    def build_tdm(self):
        self.tdm = np.zeros([len(self.Pcates),self.vocablen])#类别个数*词典长度
        print(self.tdm)
        sumlist = np.zeros([len(self.Pcates),1])#类别个数*1
        print(sumlist)
        #遍历所有的训练样本
        for indx in range(self.doclength):
            #求同一类别的词向量空间值加总,加的是每一个训练样本对应的每个词的个数
            self.tdm[self.labels[indx]] += self.tf[indx]
            #统计每个分类的总的词数
            sumlist[self.labels[indx]] = np.sum(self.tdm[self.labels[indx]])
        print(self.tdm)
        print(sumlist)
        self.tdm = self.tdm/sumlist#生成P(X|Yi),即求每个分类的单词,占该分类的所有单词总数的比例
        print(self.tdm)

    #将测试集映射到当前词典
    def map2vocab(self,testdata):
        #计算测试集的特征向量(非线性代数的特征向量,而是一行中对词的统计信息)
        self.testset = np.zeros([1,self.vocablen])
        for word in testdata:
            self.testset[0,self.vocabulary.index(word)] += 1

    #将预测分类,输出预测的分类类别
    def predict(self,testset):
        #np.shape(testset)[1]列值
        if np.shape(testset)[1] != self.vocablen:#如果测试集长度与字典不相同则退出程序
            print("输入错误")
            exit(0)
        predvalue = 0 #初始化后类别概率
        predclass = "" #初始化类别名称
        #tdm是每一类某个单词,占该类的总的单词的比例,后面一个参数是所有类别
        for tdm_vect,keyclass in zip(self.tdm,self.Pcates):
            #P(X|Yi) P(Yi)
            #变量tdm,计算最大分类值
            print("---"*30)
            print(testset*tdm_vect*self.Pcates[keyclass])
            temp = np.sum(testset*tdm_vect*self.Pcates[keyclass])
            print(temp)
            #选择概率最大的一个
            if temp > predvalue:
                predvalue = temp
                predclass = keyclass
        return  predclass


k=3
#夹角余弦距离公式
def cosdist(vector1,vector2):
    return dot(vector1,vector2)/(linalg.norm(vector1)*linalg.norm(vector2))

#kNN实现分类器
#测试集:testdata 训练集:trainSet 类别标签:listClasses k:k个邻居数
def classify(testdata,trainSet,listClasses,k):
    dataSetSize = trainSet.shape[0]#放回样本集的行数
    print(dataSetSize)
    distances = array(zeros(dataSetSize))
    print(distances)
    #计算测试集与训练集直接的距离:夹角余弦
    for indx in range(dataSetSize):
        distances[indx] = cosdist(testdata,trainSet[indx])
    #根据夹角余弦从大到小排序,结果为索引号
    sortedDistIndicies = argsort(-distances)
    print(sortedDistIndicies)
    classCount = {}
    for i in range(k):#获取角度最小的前k项作为参考项
        #按排序顺序返回样本集对应的类别标签
        voteIlabel = listClasses[sortedDistIndicies[i]]
        try:
            classCount[voteIlabel] += 1
        except:
            classCount[voteIlabel] = 1
    print(listClasses)
    print(classCount)

    #对分类字典classCount按value重新排序
    #sorted(data.iteritems(),key=operator.itemgetter(1),reversee=True)
    #该句是按字典值排序的固定用法
    #classCount.iteritems():字典迭代器函数
    #key:排序参数 operator.itemgetter(1):多级排序
    sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
    print(sortedClassCount)
    return sortedClassCount[0][0]#返回序最高的一项

dataSet,listClasses = loadDataSet()
nb = NBayes()
nb.train_set(dataSet,listClasses)
print("------"*20)
#nb.tf:统计的是每一个训练样本单词的次数
print(classify(nb.tf[3],nb.tf,listClasses,k))

 

 

 
posted @ 2019-06-03 08:36  喵小喵~  阅读(1797)  评论(0编辑  收藏  举报