《机器学习实战》基于概率论的分类方法:朴素贝叶斯

朴素贝叶斯是贝叶斯系列算法中应用最广的方法,贝叶斯方法最早由应该统计学家Thomas Bayes提出,其实简单的理解的话就是根据已经发生的某些情况来确定某事件发生的概率,而朴素贝叶斯的“朴素”指的是假设样本的所有特征都独立。核心公式理解简单了就是条件概率公式:
P(A|B)=P(B|A)*PA/P(B)=P(AB)/P(B)
由于前面已经写过朴素贝叶斯(https://blog.csdn.net/cxjoker/article/details/78472376)、半朴素贝叶斯(https://blog.csdn.net/cxjoker/article/details/81878037)和贝叶斯网络(https://blog.csdn.net/cxjoker/article/details/81878188)了,所以就不细讲述,有兴趣的可以去看。所以这一节直接从《机器学习实战》的4.5节开始吧。
使用python进行文本分类
数据准备阶段比较简单,其实就是用了自然语言处理上面的One-Hot方法,就是先建立一个词集(createvocablist)包含所有在现有的文本中出现的单词,如果有新的句子加入,就将该句话中出现的新词加入到词袋中。然后对一句话生成它的词向量(bagofwords2vecmn),即当该句话中出现过某个词时,向量上就取1,为出现就是0,向量和词集一样长。这是自然语言处理常用的方法,但是同时也会出现该句话的向量过于稀疏的情况。
训练函数:
主要用到了朴素贝叶斯公式,根据特征独立假设(即所有词之间是独立的),直接计算p(w0|ci)p(w1|ci)p(w2|ci)…p(wN|ci),然后获得p(ci|w),比较哪个类别的概率大,然后直接将类别概率大的作为分类结果。
代码思想:

  1. 计算每个类别中的文档数目
  2. 对每篇文档:
    对每个类别:
    如果词条出现在文档中:增加该词条的计数值
    增加所有词条的计数值
  3. 列表内容
    对每个类别:
    对每个词条:
    将该词条的数目初一总词条数目得到条件概率
  4. 返回每个类别的条件概率

    具体代码如下:

def loaddataset():
    postinglist=[['my','dog','has','flae','problem','help','plaese'],
                 ['maybe','not','take','him','to','dog','park','stupid'],
                 ['my','dog','is','so','cute','i','love','him'],
                 ['stop','posting','stupid','worthless','garbage',],
                 ['mr','licks','ate','my','steak','how','to','stop','him'],
                 ['qiut','buying','worthless','dog','food','stupid']]
    classvec=[0,1,0,1,0,1]
    return postinglist,classvec

def createvocablist(dataset):#功能是创建一个不重复的词汇表
    vocabset=set([])#创建出来的是一个集合,
    for document in dataset:
        vocabset =vocabset | set(document)#set(document)不重复的提取出所有的词,并且加入到vocabset集里面去
    return list(vocabset)


def bagofwords2vecmn(vocablist,inputset):#生成分类向量,当所输入的文章中的词在vocablist中出现时,就将其对应的向量设置为1,否则维持原来的不变
    returnvec=[0]*len(vocablist)
    for word in inputset:
        if word in vocablist:
            returnvec[vocablist.index(word)] +=1
    return returnvec





def trainnbo(trainmatrix,traincategory):#输入训练样本和属性(分类)标记
    numtraindocs=len(trainmatrix)#计算训练样本的样本数
    numwords=len(trainmatrix[0])#
    pabusive=sum(traincategory)/float(numtraindocs)#计算p(c)的概率(比重)这里面指的是分类为1的比重,因为求和之后为0的不考虑
    p0num=np.ones(numwords);p1num=np.ones(numwords)
    p0denom=2.0;p1denom=2.0
    for i in range(numtraindocs):
        if traincategory[i]==1:
            p1num +=trainmatrix[i]#如果说归为1类,那么把这个训练样本的向量和原先已有的向量加和,保证了同一位置(表示同一单词)的数量的叠加,起到计数的作用
            p1denom +=sum(trainmatrix[i])
        else:
            p0num +=trainmatrix[i]
            p0denom +=sum(trainmatrix[i])
    np.set_printoptions(threshold=np.inf)
    print(p0num)
    print(p1num)
    p1vect=np.log(p1num/p1denom)#p1num是一个向量,而怕denom是一个数,除了之后计算得到的是每一个单词出现在不同类别的概率
    p0vect=np.log(p0num/p0denom)
    return p0vect,p1vect,pabusive

针对算法的部分改进

1)计算概率时,需要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|ci)p(w1|ci)…p(wN|ci),然后当其中任意一项的值为0,那么最后的乘积也为0.为降低这种影响,
采用拉普拉斯平滑,在分子上添加a(一般为1),分母上添加ka(k表示类别总数),即在这里将所有词的出现数初始化为1,并将分母初始化为2*1=2

p0Num=ones(numWords);p1Num=ones(numWords)
p0Denom=2.0;p1Denom=2.0
2)解决下溢出问题

  正如上面所述,由于有太多很小的数相乘。计算概率时,由于大部分因子都非常小,最后相乘的结果四舍五入为0,造成下溢出或者得不到准确的结果,所以,我们可以对成绩取自然对数,
即求解对数似然概率。这样,可以避免下溢出或者浮点数舍入导致的错误。同时采用自然对数处理不会有任何损失。
p0Vect=log(p0Num/p0Denom);p1Vect=log(p1Num/p1Denom)

测试算法

def classifynb(vec2classify,p0vec,p1vec,pclass1):
    p1=sum(vec2classify*p1vec)+np.log(pclass1)
    p0=sum(vec2classify*p0vec)+np.log(1-pclass1)
    if p1>p0:
        return 1
    else:
        return 0

def testingnb():#测试算法
    listoposts,listclasses=loaddataset()
    myvocablist=createvocablist(listoposts)
    trainmat=[]
    for postdoc in listoposts:
        trainmat.append(bagofwords2vecmn(myvocablist,postdoc))#利用词袋模型生成所需的向量
    p0v,p1v,pab=trainnbo(trainmat,listclasses)#计算每一类中,出现的词语属于该类的概率,生成模型
    testentry=['love','my','dalmation']#测试信息
    thisdoc=np.array(bagofwords2vecmn(myvocablist,testentry))
    print(testentry,'classified as',classifynb(thisdoc,p0v,p1v,pab))

但是词集有个不好的地方就是当某个单词多次出现时,向量不能表示这种关系,所以会采用词袋模型来衡量,就是向量中的数值代表单词出现的次数。

垃圾邮件的过滤

def textparse(bigstring):
    listoftokens=re.split(r'\W*',bigstring)#利用除字符和数字之外的字符串进行分割,\w是转义字符的意思,为了保持原有的意思所以需要加上“r”来表示
    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(r'C:\Users\chenxi\Desktop\python.py\朴素贝叶斯\spam\%d.txt'%i).read())#文件夹中ham都是正常文章,spam都是广告
        doclist.append(wordlist)#做单个的词向量
        fulltext.extend(wordlist)#fulltext是在做词库
        classlist.append(1)#生成类别标签
        wordlist=textparse(open(r'C:\Users\chenxi\Desktop\python.py\朴素贝叶斯\ham\%d.txt'%i).read())
        doclist.append(wordlist)
        fulltext.extend(wordlist)
        classlist.append(0)
    vocablist=createvocablist(doclist)    
    print(doclist)
    trainingset=range(50)
    testset=[]
    for i in range(10):#交叉验证
        randindex=int(random.uniform(0,len(trainingset)))#利用uniform函数随机生成一个在0到len范围之间的随机实数,用于随机选取十个数据集
        testset.append(trainingset[randindex])#这里用的是append函数,说明这十个测试集在一个testset中,每个测试样本为一个数据集
        del(trainingset[randindex])
    trainmat=[];trainclasses=[]
    for docindex in trainingset:
        trainmat.append(bagofwords2vecmn(vocablist,doclist[docindex]))#原代码前面已经改成了bagofwords2vecmn,而他还在用setofwords2vec很明显用词袋比词集合理
        trainclasses.append(classlist[docindex])
    p0v,p1v,pspam=trainnbo(np.array(trainmat),np.array(trainclasses))
    errorcount=0
    for docindex in testset:
        wordvector=bagofwords2vecmn(vocablist,doclist[docindex])
        if classifynb(np.array(wordvector),p0v,p1v,pspam) != classlist[docindex]:
            errorcount +=1
    print('the error rate is:%s'%(float(errorcount/len(testset))))#这曾经少打了一个括号,让我声泪俱下

所遇到的坑和一些需要用到的技术
【一】文本的基本处理
read()readline()和readlines()方法
read 读取整个文件
readline 读取下一行
readlines 读取整个文件到一个迭代器以供我们遍历(读取到一个list中,以供使用,比较方便)、

re.split 以列表形式返回分割的字符串
  可以使用re.split来分割字符串,如:re.split(r’\s+’, text);将字符串按空格分割成一个单词列表。

split(string [, maxsplit = 0])

你可以通过设置 maxsplit 值来限制分片数。当 maxsplit 非零时,最多只能有 maxsplit 个分片,字符串的其余部分被做为列表的最后部分返回。在下面的例子中,定界符可以是非数字字母字符的任意序列。

p = re.compile(r’\W+’)
p.split(‘This is a test, short and sweet, of split().’)
[‘This’, ‘is’, ‘a’, ‘test’, ‘short’, ‘and’, ‘sweet’, ‘of’, ‘split’, ”]
p.split(‘This is a test, short and sweet, of split().’, 3)
[‘This’, ‘is’, ‘a’, ‘test, short and sweet, of split().’]
有时,你不仅对定界符之间的文本感兴趣,也需要知道定界符是什么。定界符可以是非数字字母字符的任意序列 ,如果捕获括号在 RE中使用,那么它们(定界符)的值也会当作列表的一部分返回。比较下面的调用:

re.split(“([ab])”,”carbs”) # [‘c’, ‘a’, ‘r’, ‘b’, ‘s’] 定界符是a或b,结果返回界定符a、b。
re.split(“([ab]#)”, “carbs”) # [‘carbs’] 定界符是a#或b#,结果 [‘carbs’]

p = re.compile(r’\W+’)
p2 = re.compile(r’(\W+)’)
p.split(‘This… is a test.’)
[‘This’, ‘is’, ‘a’, ‘test’, ”]
p2.split(‘This… is a test.’)
[‘This’, ‘… ‘, ‘is’, ’ ‘, ‘a’, ’ ‘, ‘test’, ‘.’, ”]

strip方法和split方法
strip() 方法用于移除字符串头尾指定的字符(默认为空格)。
以下实例展示了strip()函数的使用方法:
实例(Python 2.0+)
!/usr/bin/python
-- coding: UTF-8 --

str = “0000000 Runoob 0000000”;
print str.strip( ‘0’ ); # 去除首尾字符 0

str2 = ” Runoob “; # 去除首尾空格
print str2.strip();
以上实例输出结果如下:
Runoob
Runoob

split()方法

从词向量计算概率问题
伪代码为:
计算每个类别中的文档数目
对每篇训练文档:
对每个类别:
如果词条出现在文档中———增加该词条的计数值
增加所有词条的计数值
对每个类别:
对每个词条
将该词条的数目除以总词条数目得到条件概率
返回每个类别的条件概率

理解:他的作用主要是计算p(wi | ci)的值,因为p(wi)和p(ci)是直接可以计算的,所以不用考虑其他的东西就能算出来,主要就是p(wi | ci),先计算测试样本wi出现在ci类的数目,直接计算然后得到n(wi),再计算p(wi | ci=n(wi)/N(ci),最后得到wi的概率,直接将概率相乘就是最后的朴素贝叶斯的概率

re库之正则表达式
参考http://www.runoob.com/python/python-reg-expressions.html

遇到的问题
(1) for postdoc in listoposts:
trainmat.append(setofwords2vec(myvocablist,postdoc))在这段代码中,myvocablist是基于 listoposts中的词语构建出来的,而postdoc也是listposts的一部分,这样的话为什么在setofwords2vec函数中会出现0,1向量,而不直接都是1,因为词汇表中包含了postdoc中的所有元素啊?

for word in inputset:
if word in vocablist:
returnvec[vocablist.index(word)]=1
回答:理解有错,他建立的基于词汇表的文本向量是这样的:先建立一个和词汇表长度一样的横向量,然后现在给定一个新的句子,遍历这个句子如果句子中的单词在词汇表中出现,则将需要转化的向量中对应的索引的地方(即在词汇表中出现这个word的对应的索引数值作为新的returnvec的索引并将其改为对应的1),基于词汇表来建立向量,保证生成的向量和词汇表有相同的长度。

(2)无法输出完整的数组:[ 1. 0. 0. …, 1. 1. 1.]
添加以下语句:np.set_printoptions(threshold=np.nan)或者np.set_printoptions(threshold=np.inf)即可

(3)用re正则表达式时遇到这样的问题:’NoneType’ object has no attribute ‘span’
源码:
a=’Catsaresmartethandogs’
print(re.match(’s’, a).span(0))
主要是因为match匹配的是模式,而不是搜索得到字符串的具体的位置,可以考虑使用search
re.match与re.search的区别
re.match只匹配字符串的开始,如果字符串开始不符合正则表达式,则匹配失败,函数返回None;而re.search匹配整个字符串,直到找到一个匹配。
即match从开始匹配,如果不是的话就返回none值
findall
在字符串中找到正则表达式所匹配的所有子串,并返回一个列表,如果没有找到匹配的,则返回空列表。
注意: match 和 search 是匹配一次 findall 匹配所有。
语法格式为:
findall(string[, pos[, endpos]])

(1)运行时报错
wordlist=textparse(open(‘C:\Users\chenxi\Desktop\python.py\朴素贝叶斯\spam\%d.txt’%i).read())#文件夹中ham都是正常文章,spam都是广告
^
SyntaxError: (unicode error) ‘unicodeescape’ codec can’t decode bytes in position 2-3: truncated \UXXXXXXXX escape

在Python中\是转义符,\u表示其后是UNICODE编码,因此\User在这里会报错,在字符串前面加个r表示就可以了

(2)报错
if name == ‘main‘:
^
SyntaxError: invalid syntax
因为少了括号 MMP
(3)报错
module ‘feedparser’ has no attribute ‘parser’

所有代码

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 15 17:49:29 2018

@author: chenxi
"""
import numpy as np
import re
import feedparser
import random
def fenci(filename):
    fr=open(filename)
    lists=fr.read()

    listword=re.split(lists)

    print(listword)




def loaddataset():
    postinglist=[['my','dog','has','flae','problem','help','plaese'],
                 ['maybe','not','take','him','to','dog','park','stupid'],
                 ['my','dog','is','so','cute','i','love','him'],
                 ['stop','posting','stupid','worthless','garbage',],
                 ['mr','licks','ate','my','steak','how','to','stop','him'],
                 ['qiut','buying','worthless','dog','food','stupid']]
    classvec=[0,1,0,1,0,1]
    return postinglist,classvec

def createvocablist(dataset):#功能是创建一个不重复的词汇表
    vocabset=set([])#创建出来的是一个集合,
    for document in dataset:
        vocabset =vocabset | set(document)#set(document)不重复的提取出所有的词,并且加入到vocabset集里面去
    return list(vocabset)


def bagofwords2vecmn(vocablist,inputset):#生成分类向量,当所输入的文章中的词在vocablist中出现时,就将其对应的向量设置为1,否则维持原来的不变
    returnvec=[0]*len(vocablist)
    for word in inputset:
        if word in vocablist:
            returnvec[vocablist.index(word)] +=1
    return returnvec





def trainnbo(trainmatrix,traincategory):#输入训练样本和属性(分类)标记
    numtraindocs=len(trainmatrix)#计算训练样本的样本数
    numwords=len(trainmatrix[0])#
    pabusive=sum(traincategory)/float(numtraindocs)#计算p(c)的概率(比重)这里面指的是分类为1的比重,因为求和之后为0的不考虑
    p0num=np.ones(numwords);p1num=np.ones(numwords)
    p0denom=2.0;p1denom=2.0
    for i in range(numtraindocs):
        if traincategory[i]==1:
            p1num +=trainmatrix[i]#如果说归为1类,那么把这个训练样本的向量和原先已有的向量加和,保证了同一位置(表示同一单词)的数量的叠加,起到计数的作用
            p1denom +=sum(trainmatrix[i])
        else:
            p0num +=trainmatrix[i]
            p0denom +=sum(trainmatrix[i])
    np.set_printoptions(threshold=np.inf)
    print(p0num)
    print(p1num)
    p1vect=np.log(p1num/p1denom)#p1num是一个向量,而怕denom是一个数,除了之后计算得到的是每一个单词出现在不同类别的概率
    p0vect=np.log(p0num/p0denom)
    return p0vect,p1vect,pabusive

'''
3 针对算法的部分改进

1)计算概率时,需要计算多个概率的乘积以获得文档属于某个类别的概率,即计算p(w0|ci)*p(w1|ci)*...p(wN|ci),然后当其中任意一项的值为0,那么最后的乘积也为0.为降低这种影响,
    采用拉普拉斯平滑,在分子上添加a(一般为1),分母上添加ka(k表示类别总数),即在这里将所有词的出现数初始化为1,并将分母初始化为2*1=2

#p0Num=ones(numWords);p1Num=ones(numWords)
#p0Denom=2.0;p1Denom=2.0
2)解决下溢出问题

  正如上面所述,由于有太多很小的数相乘。计算概率时,由于大部分因子都非常小,最后相乘的结果四舍五入为0,造成下溢出或者得不到准确的结果,所以,我们可以对成绩取自然对数,
  即求解对数似然概率。这样,可以避免下溢出或者浮点数舍入导致的错误。同时采用自然对数处理不会有任何损失。

#p0Vect=log(p0Num/p0Denom);p1Vect=log(p1Num/p1Denom)
'''  

def classifynb(vec2classify,p0vec,p1vec,pclass1):
    p1=sum(vec2classify*p1vec)+np.log(pclass1)
    p0=sum(vec2classify*p0vec)+np.log(1-pclass1)
    if p1>p0:
        return 1
    else:
        return 0

def testingnb():#测试算法
    listoposts,listclasses=loaddataset()
    myvocablist=createvocablist(listoposts)
    trainmat=[]
    for postdoc in listoposts:
        trainmat.append(bagofwords2vecmn(myvocablist,postdoc))#利用词袋模型生成所需的向量
    p0v,p1v,pab=trainnbo(trainmat,listclasses)#计算每一类中,出现的词语属于该类的概率,生成模型
    testentry=['love','my','dalmation']#测试信息
    thisdoc=np.array(bagofwords2vecmn(myvocablist,testentry))
    print(testentry,'classified as',classifynb(thisdoc,p0v,p1v,pab))



def textparse(bigstring):
    listoftokens=re.split(r'\W*',bigstring)#利用除字符和数字之外的字符串进行分割,\w是转义字符的意思,为了保持原有的意思所以需要加上“r”来表示
    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(r'C:\Users\chenxi\Desktop\python.py\朴素贝叶斯\spam\%d.txt'%i).read())#文件夹中ham都是正常文章,spam都是广告
        doclist.append(wordlist)#做单个的词向量
        fulltext.extend(wordlist)#fulltext是在做词库
        classlist.append(1)#生成类别标签
        wordlist=textparse(open(r'C:\Users\chenxi\Desktop\python.py\朴素贝叶斯\ham\%d.txt'%i).read())
        doclist.append(wordlist)
        fulltext.extend(wordlist)
        classlist.append(0)
    vocablist=createvocablist(doclist)    
    print(doclist)
    trainingset=range(50)
    testset=[]
    for i in range(10):#交叉验证
        randindex=int(random.uniform(0,len(trainingset)))#利用uniform函数随机生成一个在0到len范围之间的随机实数,用于随机选取十个数据集
        testset.append(trainingset[randindex])#这里用的是append函数,说明这十个测试集在一个testset中,每个测试样本为一个数据集
        del(trainingset[randindex])
    trainmat=[];trainclasses=[]
    for docindex in trainingset:
        trainmat.append(bagofwords2vecmn(vocablist,doclist[docindex]))#原代码前面已经改成了bagofwords2vecmn,而他还在用setofwords2vec很明显用词袋比词集合理
        trainclasses.append(classlist[docindex])
    p0v,p1v,pspam=trainnbo(np.array(trainmat),np.array(trainclasses))
    errorcount=0
    for docindex in testset:
        wordvector=bagofwords2vecmn(vocablist,doclist[docindex])
        if classifynb(np.array(wordvector),p0v,p1v,pspam) != classlist[docindex]:
            errorcount +=1
    print('the error rate is:%s'%(float(errorcount/len(testset))))#这曾经少打了一个括号,让我声泪俱下


def calmostfreq(vocablist,fulltext):
    import operator
    freqdict={}
    for token in vocablist:
        freqdict[token]=fulltext.count(token)
    sortedfreq=sorted(freqdict.items(),key=operator.itemgetter(1),reverse=True)
    return sortedfreq[0:30]



def localwords(feed1,feed0):
    import feedparser
    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=calmostfreq(vocablist,fulltext)
    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(random.uniform(0,len(trainingset))) 
        testset.append(trainingset[randindex])
        del(trainingset[randindex])
    trainmat=[];trainclasses=[]
    for docindex in trainingset:
        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:
        wordvector=bagofwords2vecmn(vocablist,doclist[docindex])
        if classifynb(np.array(wordvector),p0v,p1v,pspam) != classlist[docindex]:
            errorcount +=1
    print('the error rate is:%s'%(float(errorcount/len(testset))))
    return vocablist,p0v,p1v            



if __name__=='__main__':
    ny=feedparser.parser('http://newyork.craigslist.org/stp/index.rss')
    sf=feedparser.parser('http://sfbay.craigslist.org/stp/index.rss')
    vocablist,psf,pny=localwords(ny,sf)

猜你喜欢

转载自blog.csdn.net/cxjoker/article/details/82596754
今日推荐