机器学习算法——朴素贝叶斯(贝努利模型和多项式模型实现分类)

朴素贝叶斯算法

    • 0、朴素贝叶斯是贝叶斯决策理论的一部分。之所以称之为“朴素”,是因为整个形式化过程只做最原始、最简单的假设。
    • 1、文本分类实例
    • 2、朴素贝叶斯过滤垃圾邮件
    • 3、使用朴素贝叶斯分类器从个人广告
    • 4、小结
      • 学习永无止境,后续还会继续更新其他的机器学习算法。

0、朴素贝叶斯是贝叶斯决策理论的一部分。之所以称之为“朴素”,是因为整个形式化过程只做最原始、最简单的假设。

本文所用到的所有数据来源

链接:百度网盘
提取码:01gm

1、算法优缺点比较
优点:在数据较少的情况下仍然有效,可以处理多类别问题
缺点:对于输入数据的准备方式较为敏感
使用数据类型:标称型数据

2、关于贝叶斯决策理论的简单解释:
假设我们有如下的数据集,由两类数据组成,数据分布如下
机器学习算法——朴素贝叶斯(贝努利模型和多项式模型实现分类)_第1张图片
我们用p1(x1,x2)表示数据点(x1,x2)属于类别1 图中红色×的概率,
p2(x1,x2)表示数据点(x1,x2)属于类别2 图中蓝色o的概率。
若p1(x1,x2) > p2(x1,x2),那么该点属于类别1
若p1(x1,x2) < p2(x1,x2),那么该点属于类别2

也就是说,我们利用高概率选择对应的类别。贝叶斯决策理论的核心思想就在于此,即选择具有最高概率的决策。

3、贝叶斯准则:
p( c | x ) = p( x | c ) * p( c ) / p( x )
该准则告诉我们c,x作为条件互换时,相应概率的计算方法。

4、使用条件概率分类
根据贝叶斯决策理论要求实现二分类时,我们需要计算对应的p1(x1,x2) 和 p2(x1,x2)。
对于上面的数据集而言,我们真正需要计算和比较的是p(c1| x,y) , p(c2| x,y)。即给定某个由x,y表示的数据点,那么该数据点来自类别c1,c2的概率分别为多少。
同样的,我们利用贝叶斯准则可以很快的得到
机器学习算法——朴素贝叶斯(贝努利模型和多项式模型实现分类)_第2张图片
使用贝叶斯准则,可以通过已知的三个概率值来计算未知的概率值。
使用这些定义,可以定义贝叶斯分类准则为:

  • 如果p(c1 | x,y) > p(c2| x,y) ,那么属于类别c1
  • 如果p(c1 | x,y) < p(c2| x,y) ,那么属于类别c2

5、朴素贝叶斯
朴素贝叶斯算法的一个重要应用就是文档的分类。在对新闻报道,用户留言,政府公文等其他任意类型的文本进行分类是,我们可以观察文档中出现的词,并把每个词的出现或者不出现作为一个特征,这样得到的特征数目就会和我们建立的词汇表中数目相同。后续我们以英文单词和电子邮件举例。
假设词汇表中有5000个单词,要得到好的概率分布,就需要足够多的样本数据,假定我们的数据样本数目为N。由排列组合可知,每个特征需要N个样本,那么100个特征就需要N100 个样本呢,而对于包含1000个特征的词汇表将需要 N1000个样本,样本数随着特征数目指数膨胀。
若特征之间互相独立,那么样本数就可以从N1000减少到1000*N。这里的独立,指的是统计学意义上的互不相关,即某个特征或者单词出现的可能性不受其他单词相邻的影响。当然,我们知道这种假设并不正确,有些单词之间的相关性非常高。但为简便起见,我们暂不考虑,这也就是朴素一词的重要体现。
朴素贝叶斯分类器的另一个假设是,每个特征同等重要。同样的,这也是为了简便起见,我们知道,想要大致了解一段500单词的文本讲述的内容,只需要看10~50个特征便可以判断,这正是基于不同特征的重要性不尽相同。当然,及时有着各种各样的小瑕疵,朴素贝叶斯仍然能取得很好的效果。

值得注意的是,朴素贝叶斯分类器通常有两种实现方式:
1、基于贝努利模型,贝努利模型只考虑文本词汇是否出现而不考虑出现次数,这在最后的朴素贝叶斯分类器函数的编写上带来了很大的便利,第一个例子先使用贝努利模型简单尝试。
2、基于多项式模型,多项式模型考虑词在文档中出现的次数,使得出现频率不同的词被赋予了不同的权重。将在第一种模型的基础上实现

1、文本分类实例

要从文本中获取特征,需要先拆分文本。我们从文本的词条中获取特征,词条是字符的任意组合,可以是"hello" ,“http”,"www"等等字符(串)。词条类似于单词,但是它不一定需要有单词的排列。

然后,我们将每个文本片段表示为一个词条向量,其中值为1代表某个词条出现在文本中,0表示未出现。这种表示方法和独热码有异曲同工之妙。

现在,我们以某个社区的留言板为例,为了社会的和谐发展,我们需要和谐一些侮辱性的言论,所以如果能够构建出一个快速的过滤器,将有着非常好的作用。如果某条言论使用了过于负面或者侮辱言论,过滤器就会表示其为内容不当。此处我们只考虑其是否属于侮辱性言论或者属于正常言论,使用1和0区分之。

我们将文本看成单词向量或者词条向量,下面的代码将把句子转换为向量。

import numpy as np
import re
import feedparser


def createDataSet():
    """
    该函数创建了一些实验样本,返回的第一个变量是进行词条切分后的文档集合,
    这些文档来自某个斑点狗爱好者论坛的留言板,这些留言文本在这里被切分为一系列
    的词条集合,标点符号从文本中去掉。该函数返回的第二个变量是一个类别标签的集合。
    此处分为两类,侮辱性和非侮辱性,提前进行过人工标注,利用标注好的学习训练程序以便自动检测侮辱性言论。
    :return:留言数据集以及对应的标签
    """
    commentsList = [['my', 'dog', 'has', 'flea', 'problems', 'help', 'please'],
                    ['maybe', 'not', 'take', 'him', 'to', 'dog', 'park', 'stupid'],
                    ['my', 'dalmatian', '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 commentsList, classVec


def createVocabList(dataSet):
    """
    该函数会创建一个包含在所有文档中出现的不重复词的列表,python自带的set给了很大的帮助
    :param dataSet: 文本文档数据集
    :return: 输入文档所用词构成的集合
    """
    vocabSet = set([])  # 预分配
    for document in dataSet:
        vocabSet = vocabSet | set(document)  # 得到两个集合的并集
    return list(vocabSet)


def WordSet2Vec(vocabList, inputSet):
    """
    该函数创建一个和词汇表等长的向量,并将其元素都设置为0,随后遍历文档中的所有单词,
    对于出现在词汇表中的单词,将输出的文档向量对应值置为1
    :param vocabList:词汇表
    :param inputSet: 某个文档
    :return: 文档向量,向量的每一个元素为1或0,分别表示词汇表中的单词在输入文档中是否出现。
    """
    textVec = [0] * len(vocabList)  # 预分配空间
    for word in inputSet:
        if word in vocabList:
            textVec[vocabList.index(word)] = 1  # 对应的值置为1
        else:
            print("the word: %s is not in the vocabulary!" % word)
    return textVec

将一组单词转换为一组数字后,我们将利用数字计算概率,我们利用前面所说的贝叶斯准则,并将x、y替换成ww表示一个向量,其由多个数值组成。此处,数值个数与词汇表中的词个数相同。
我们想要求的是在已知w的前提下,其分类为ci
机器学习算法——朴素贝叶斯(贝努利模型和多项式模型实现分类)_第3张图片
利用上述的公式,对每个类计算概率值,比较大小,得出分类。
计算方法:首先可以通过类别i(侮辱性留言或非侮辱性留言)中文档数除以总的文档数来计算概率p(c i) 。然后我们来计算p(w|ci) , 我们需要用到前面所述的朴素贝叶斯假设。如果将w展开为一个个独立特征,那么就可以将p(w|ci)写作p(w0,w1,w2,…wi,wN | ci)来计算概率,有了这样的条件独立性假设,我们就可以使用p(w0|ci)*p(w1|ci)…*p(wN|ci)大大简化计算。
给出伪代码如下

提取所有文档中的词条并进行去重
获取文档的所有类别
计算每个类别中的文档数目
对每篇训练文档: 
    对每个类别: 
        如果词条出现在文档中-->增加该词条的计数值(for循环或者矩阵相加)
        增加所有词条的计数值(此类别下词条总数)
对每个类别: 
    对每个词条: 
        将该词条的数目除以总词条数目得到的条件概率(P(词条|类别))
返回该文档属于每个类别的条件概率(P(类别|文档的所有词条)

由于在利用前面公式编程时会出现问题:
问题一:p(wn|ci) 中有一个为0,导致整个累乘结果也为0。这是错误的结论。
解决方法:将所有词的出现次数初始化为1,并将分母初始化为2。

问题二:即使 p(wn|ci) 不为0了,可是它的值也许会很小,这样会导致浮点数值类型的下溢出等精度问题错误。
解决方法:用 p(wn|ci) 的对数进行计算。

下图给出了函数 f(x) 与 ln(f(x)) 的曲线。可以看出,它们在相同区域内同时增加或者减少,并且在相同点上取到极值。它们的取值虽然不同,但不影响最终结果。
机器学习算法——朴素贝叶斯(贝努利模型和多项式模型实现分类)_第4张图片
针对上述问题,代码如下

def trainNaiveBayes0(trainMatrix, trainCategory):
    """
    :param trainMatrix: 文件单词矩阵 [[1,0,1,1,1....],[],[]...]
    :param trainCategory: 文件对应的标签类别[0,1,1,0....],列表长度等于单词矩阵数,其中的1代表对应的文件是侮辱性文件,0代表不是侮辱性矩阵
    :return:
            p0Vect:    各单词在分类0的条件下出现的概率
            p1Vect:    各单词在分类1的条件下出现的概率
            pAbusive:    文档属于分类1的概率
    """
    numTrainDocs = len(trainMatrix)
    numWords = len(trainMatrix[0])
    pAbusive = sum(trainCategory) / float(numTrainDocs)
    p0Num = np.ones(numWords)
    p1Num = np.ones(numWords)  
    p0Denom = 2.0
    p1Denom = 2.0  
    """
    for循环中遍历训练集trainMatrix中的所有文档,一旦某个词语(侮辱性或者正常词语)
    在某一文档中出现,则该词对应的个数(p1num 和 p0num )增1,同时,
    在所有文档中,该文档的总次数也相应增加1
    """
    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 = np.log(p1Num / p1Denom) 
    p0Vect = np.log(p0Num / p0Denom) 
    return p0Vect, p1Vect, pAbusive

使用对数的另一个好处是 ln(a*b) = ln(a) * ln(b)。
然后我们来实现朴素贝叶斯分类函数:

def classifyNaiveBayes(vec2Classify, p0Vec, p1Vec, pClass1):
    """
    :param vec2Classify: 需要分类的向量
    
    trainNaiveBayes0()计算得到的三个概率
    :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 testingNaiveBayes():
    """
    简单测试朴素贝叶斯的分类效果
    """
    listOPosts, listClasses = createDataSet()
    myVocabList = createVocabList(listOPosts)
    trainMat = []
    for postinDoc in listOPosts:
        trainMat.append(WordSet2Vec(myVocabList, postinDoc))
    p0V, p1V, pAb = trainNaiveBayes0(np.array(trainMat), np.array(listClasses))
    testEntry = ['love', 'my', 'dalmatian']
    thisText = np.array(WordSet2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNaiveBayes(thisText, p0V, p1V, pAb))
    testEntry = ['stupid', 'garbage']
    thisText = np.array(WordSet2Vec(myVocabList, testEntry))
    print(testEntry, 'classified as: ', classifyNaiveBayes(thisText, p0V, p1V, pAb))

testingNaiveBayes()
"""
运行结果:
['love', 'my', 'dalmatian'] classified as:  0
['stupid', 'garbage'] classified as:  1
"""

2、朴素贝叶斯过滤垃圾邮件

前面的WordsSet2Vec函数将每个词的出现与否作为一个特征,这可以被描述为词集模型(set-of-words model)。如果一个词在文档中出现不止一次,这可能意味着包含该词是否出现在文档中所不能表达的某种信息,这种方法被称为词袋模型(bag-of-words model)。在词袋中,每个单词可以出现多次,而在词集中,每个词只能出现一次。为适应词袋模型,我们对函数稍作修改,在遍历时使用累加符号+=即可。

def Wordsbag2VecMN(vocabList, inputSet):
    returnVec = [0] * len(vocabList)
    for word in inputSet:
        if word in vocabList:
            returnVec[vocabList.index(word)] += 1
    return returnVec

使用朴素贝叶斯对电子邮件进行分类的流程:

  1. 准备数据,网盘中下载
  2. 准备数据,将文本文件解析成词条向量
  3. 分析数据,检查词条以确保解析函数运行正常
  4. 训练算法,利用前述的trainNaiveBayes0()
  5. 测试算法,使用classifyNaiveBayes(),并且构建一个新的测试函数来计算文档集的准确率
  6. 使用算法,构建一个完整的程序对一组文档进行分类,将错分的文档输出到屏幕
def textParse(bigString):
    """
    接受一个长的字符串,将其解析为字符串列表,同时改函数去掉少于两个字符的字符串,并将所有字符串转换为小写
    :param bigString:完整的字符串
    :return:切分好的词条
    """
    listOfTokens = re.split(r'\W+', bigString)
    return [tok.lower() for tok in listOfTokens if len(tok) > 2]


def spamTest():
    """
    对贝叶斯垃圾邮件分类器进行自动化处理,导入文件夹spam,ham下的文本文件,
    将之解析为词列表,接下来构建一个测试集与训练集,两个集合中的邮件都是随机抽取的
    :return:
    """
    docList = []
    classList = []
    fullText = []
    """
    分别读取正常邮件与垃圾邮件
    """
    for i in range(1, 26):
        wordList = textParse(open('email/spam/%d.txt' % i, encoding="ISO-8859-1").read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(1)
        wordList = textParse(open('email/ham/%d.txt' % i, encoding="ISO-8859-1").read())
        docList.append(wordList)
        fullText.extend(wordList)
        classList.append(0)
    vocabList = createVocabList(docList)  # 创建词典
    trainingSet = range(50)
    testSet = []  # create test set
    """
    采用留存交叉验证,随机选择部分文件,选择出的数字对应的文档被添加到测试集,并从训练集中剔除,
    为了更精确的估计分类器的准确率,进行多次迭代后求出平均准确率
    """
    for i in range(10):
        randIndex = int(np.random.uniform(0, len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del (list(trainingSet)[randIndex])
    trainMat = []
    trainClasses = []
    for docIndex in trainingSet:  # 利用trainNaiveBayes0训练
        trainMat.append(Wordsbag2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V, p1V, pSpam = trainNaiveBayes0(np.array(trainMat), np.array(trainClasses))
    errorCount = 0
    for docIndex in testSet:  # 对剩余的进行验证
        wordVector = Wordsbag2VecMN(vocabList, docList[docIndex])
        if classifyNaiveBayes(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
            print("classification error", docList[docIndex])
    print('the accuracy is: ', (1 - float(errorCount) / len(testSet)))
    # return vocabList, fullText


3、使用朴素贝叶斯分类器从个人广告

在本例中,我们将分别从美国的两个城市中选取一些人,通过分析这些人发布的征婚广告信息,来比较这两个城市的人们在广告用词上是否不同。从人们的用词中,我们或许可以看出不同城市的人所关心的内容有何不同。

  1. 收集数据:从RSS源收集内容,这里需要对RSS源构建一个接口
  2. 准备数据:将文本文件解析成词条向量
  3. 分析数据:检查词条确保解析的正确性
  4. 训练算法:使用之前建立的trainNaiveBayes0()函数
  5. 测试算法:观察错误率,确保分类器可用,可以修改切分程序,以降低错误率,提高分类结果
  6. 使用算法:构建一个完整的程序,封装所有内容,给定两个RSS源,该程序会显示最常用的公共词

代码如下:

def calcMostFreq(vocabList, fullText):
    """
    计算出现频率,改函数遍历词汇表中每个词并统计它在文本中出现的次数
    然后根据出现次数从高到低对词典进行排序,最后返回排序最高的30个单词
    :param vocabList: 
    :param fullText: 
    :return: 
    """
    freqDict = {
     }
    for token in vocabList:
        freqDict[token] = fullText.count(token)
    sortedFreq = sorted(freqDict.items(), key=operator.itemgetter(1), reverse=True)
    return sortedFreq[:30]


def localWords(feed1, feed0):
    docList = []
    classList = []
    fullText = []
    minLen = min(len(feed1['entries']), len(feed0['entries']))
    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 = range(2 * minLen)
    testSet = []  # 创建测试集
    for i in range(20):
        randIndex = int(np.random.uniform(0, len(trainingSet)))
        testSet.append(trainingSet[randIndex])
        del (list(trainingSet)[randIndex])
    trainMat = []
    trainClasses = []
    for docIndex in trainingSet:  # 利用trainNaiveBayes0训练
        trainMat.append(Wordsbag2VecMN(vocabList, docList[docIndex]))
        trainClasses.append(classList[docIndex])
    p0V, p1V, pSpam = trainNaiveBayes0(np.array(trainMat), np.array(trainClasses))
    errorCount = 0
    for docIndex in testSet:  # 对测试集进行验证
        wordVector = Wordsbag2VecMN(vocabList, docList[docIndex])
        if classifyNaiveBayes(np.array(wordVector), p0V, p1V, pSpam) != classList[docIndex]:
            errorCount += 1
    print('the accuracy is: ', (1 - float(errorCount) / len(testSet)))
    return vocabList, p0V, p1V


def getTopWords(city_1, city_2):
    """
    使用两个RSS源作为输入,然后训练并测试朴素贝叶斯分类器,
    返回使用的概率值,然后创建两个列表用于元组的存,
    这些元组会按照他们的条件概率进行排序
    :param city_1:第一个城市
    :param city_2:第二个城市
    :return:
    """
    vocabList, p0V, p1V = localWords(city_1, city_2)
    topNY = []
    topSF = []
    for i in range(len(p0V)):
        if p0V[i] > -6.0: topSF.append((vocabList[i], p0V[i]))
        if p1V[i] > -6.0: topNY.append((vocabList[i], p1V[i]))
    sortedSF = sorted(topSF, key=lambda pair: pair[1], reverse=True)
    print("city2*****city2*****city2*****city2*****city2*****city2")
    for item in sortedSF:
        print(item[0])
    sortedNY = sorted(topNY, key=lambda pair: pair[1], reverse=True)
    print("city1*****city1*****city1*****city1*****city1*****city1")
    for item in sortedNY:
        print(item[0])


ny = feedparser.parse('http://newyork.craigslist.org/stp/index.rss')
sf = feedparser.parse('http://sfbay.craigslist.org/stp/index.rss')
localWords(ny,sf)
getTopWords(ny, sf)

4、小结

就分类而言,纯粹的使用概率有时候会比使用硬规则更为有效,贝叶斯概率和贝叶斯准则提供了一种利用已知值来估计未知概率的有效办法。

通过特征之间的条件独立性假设,我们降低了对数据量的需求,独立性假设是指一个词的出现概率并不依赖文档中的其他词。虽然这个假设过于简单,但是这正体现了朴素贝叶斯的朴素之处。尽管条件独立性假设并不正确,但贝叶斯仍然是一种有效的分类器。

利用python等编程语言来实现朴素贝叶斯时需要考虑很多实际因素,下溢出就是其中一个比较大的问题,我们使用了自然对数缓解了这种情况。另外,我们引入了词袋模型,该方法基于多项式模型,在解决文档分类问题山比词集模型有所提高。同时还可以考虑移除停用词,或者考虑对切分器进行人工优化。

学习永无止境,后续还会继续更新其他的机器学习算法。

你可能感兴趣的:(传统机器学习算法,机器学习,概率论,朴素贝叶斯算法,机器学习,算法)