第7章 基于朴素贝叶斯的垃圾邮件分类

1. 引言

在正式学习朴素贝叶斯之前,需要明确的是机器学习所要实现的是基于有限的训练样本集尽可能准确地估计出后验概率P(c|x),即根据特征得到所属类别的概率,首先引入两个概念。
判别式模型(discriminative models):给定x,直接建模P(c|x)来预测c,比如决策树、BP神经网络、支持向量机等;
生成式模型(generative models):联合概率分布P(x,c)进行建模,然后由此获得p(c|x),典型代表就是下面要讲的朴素贝叶斯。

2. 问题描述

本文基于朴素贝叶斯构建一个分类垃圾邮件的模型,研究对象是英文的垃圾邮件,一来英文垃圾邮件数据集比较容易找到比较多,二来难度较中文的稍小,并且很多人都在用英文邮件,可比性较强,适合新手入门。

3. 算法流程

A. 数据准备

a. 数据集使用 UCI Machine Learning Repository,已上传至GitHub,可进行下载。
b. 训练集中,positive邮件数量为12500,negative邮件数量为12500;测试集中,positive邮件数量和negative邮件数量同为12500。
c. 数据预处理。去除邮件中的特殊符号以及没有任何意义的词语,如一些html标签或者"the"、"a"等词语。具体方式为使用现成的停用词表进行操作。

B. 理论依据

a. 贝叶斯定理

贝叶斯定理是该算法的核心思想。


贝叶斯定理

其中,P(c)是类先验概率,p(x|c)是条件概率,p(x)是“证据因子”(在同一问题中p(x)全相同);
p(c)=类别c的数目/总数。
因此,求解p(c|x)的问题变成了求解p(x|c)的问题,接下来引出下面两种解决方法。

b1. 极大似然估计(源自频率主义学派)

试错,此路不通。
假设p(x|c)具有确定的形式并且被参数向量θc唯一确定,则要利用训练集估计θc的值,即p(x|c)-->p(x|θc)。
Frequentist:参数虽然未知,但是客观存在的固定值。
Bayesian:参数是未观察到的随机变量,其本身也有分布。
若样本Dc满足独立同分布,则参数θc对于数据集Dc的似然是

极大似然估计

注:使用对数似然的原因是避免连乘造成下溢。
这种参数化的方法的弊端是结果的准确性严重依赖于所假设的概率分布形式是否符合潜在的真实数据分布。下面就是贝叶斯的优越性了。

b2. 朴素贝叶斯分类器

朴素贝叶斯分类器(naive Bayes classifier)采用了属性条件独立性假设(attribute conditional independence assumption),即对于已知类别,假设所有的属性都相互独立(虽然这是不对的,但至少在垃圾邮件分类这里假设所有的属性独立取得了还不赖的结果)。换言之,假设所有属性独立地对分类结果发生影响。

朴素贝叶斯分类器

另外,为了避免进行计算时c类样本的数量和在样本中第i个属性的取值为0导致p(c)或者p(x|c)为0,这里使用拉普拉斯修正(Laplacian correction),即
Laplace

C. 垃圾邮件实际问题

  • a. 建立词汇表(之后可以创建TF-IDF词汇表,现在仅仅简化)
    统计邮件中出现的所有词汇并记作一维向量的形式,即(abandon,ahead,buy,go,...,zero)。
def word_create(ori_data):
    #还没有进行去停用词的操作nltk.stopwoords的包没能下载下来
#    ori_data.data = [word for word in ori_data.data if(word not in stopwords.words('english'))]
    print("Vectorzing dataset ...")
    #建立一个集合列表
    word_dic = set([])
    #词向量的时间
    vectorTime = time()
    #词典的构造
    for doc in ori_data.data:
        #doc是byte,这里将byte转化为string
        doc = str(doc, encoding = "utf-8")
        #使用正则表达式将特殊符号去除
        doc = re.sub("[\s+\.\!\/_,$%^*(+\"\'-]+|[+——!,。?、~@#¥%……&*()<>]+", " ", doc)
        #使用默认的空格方式将email分隔开,然后转化为小写字母,与原集合取并集
        word_dic = word_dic|set(doc.lower().split())
    #向量化的时间和词典中词的数量
    print("Vectorzing time:{0}\nThe number of word_dictionary:{1}".format(vectorTime,len(word_dic)))
    return list(word_dic)
  • b. 词向量表示(之后使用Word2Vec表示词向量,现在仅仅简化)
    将每封邮件依据词汇表以向量的形式表示出来,该词在邮件中出现记为1,反之记为0,即比如(1,1,0,0,...,1)。
    无疑这会造成维数灾难,Word2Vec会好很多,不过现在的计算量不是很大也不涉及其他算法就用one-hot方式表示了。
def doc_represent(wordDic,ori_data):
    #创建一个文档数(行)*词向量(列)长度的二维数组
    doc_re = numpy.zeros((len(ori_data.data),len(wordDic)),dtype= numpy.int)
    #计数器
    count = 0
    #用来记录词向量表示时间
    representTime = time()
    for doc in ori_data.data:
        #同word_create函数,进行同样的操作
        doc = str(doc, encoding = "utf-8")
        doc = re.sub("[\s+\.\!\/_,$%^*(+\"\'-]+|[+——!,。?、~@#¥%……&*()<>]+", " ", doc)
        for word in doc.lower().split():
            if word in wordDic:
                #将对应词向量位置置1
                doc_re[count][wordDic.index(word)] = 1
        count = count+1
    print("Represent doc time:{0}\nThe number of doc:{1}".format(representTime-time(),len(doc_re)))
    #返回表示文档的二维数组
    return doc_re
  • c. 计算先验概率p(c)
    p(正常邮件)=D(正常邮件数)+1/D(总邮件数)+2
    p(垃圾邮件)=D(垃圾邮件数)+1/D(总邮件数)+2
def pre_probabilty(ori_data):
    s_pre_pro = []

    #正常邮件的先验概率
    P_normal = (normal + 1.0)/(len(ori_data.data) + 2.0)
    s_pre_pro.append(P_normal)
    #垃圾邮件的先验概率
    P_spam = (spam + 1.0)/(len(ori_data.data) + 2.0)
    s_pre_pro.append(P_spam)
    #返回先验概率的列表
    return s_pre_pro
  • d. 计算每个词汇的条件概率
    p(abandon=1|正常邮件)=D(正常邮件中有abandon的数目)+1/D(正常邮件数)+2
    p(abandon=1|垃圾邮件)=D(垃圾邮件中有abandon的数目)+1/D(垃圾邮件数)+2
    p(ahead=1|正常邮件)=D(正常邮件中有ahead的数目)+1/D(正常邮件数)+2
    p(ahead=1|垃圾邮件)=D(垃圾邮件中有ahead的数目)+1/D(垃圾邮件数)+2
    ...
    p(zero=1|正常邮件)=D(正常邮件中有zer的数目)+1/D(正常邮件数)+2
    p(zero=1|垃圾邮件)=D(垃圾邮件中有zero的数目)+1/D(垃圾邮件数)+2
#计算每个词在正常邮件垃圾邮件中的数目
def wordNum_email(email_repre,wordDic):
    #用二维向量存储
    num_word = numpy.zeros((2,len(wordDic)),dtype= numpy.int)
    for i in range(len(wordDic)):
        #在正常邮件的数目
        for j in range(normal):
            num_word[0][i] += email_repre[j][i]
        #在垃圾邮件中的数目
        for j in range(normal, spam+normal):
            num_word[1][i] += email_repre[j][i]
    return num_word

#条件概率
def con_probabilty(email_repre,wordDic):
    #得到每个词汇在正常邮件、垃圾邮件中的数目
    word_num = wordNum_email(email_repre,wordDic)
    word_pro = numpy.zeros((2,len(wordDic)),dtype = numpy.double)
    for i in range(len(wordDic)):
        word_pro[0][i] = round((word_num[0][i]+1)/(normal + 2),8)
        word_pro[1][i] = round((word_num[1][i]+1)/(spam + 2 ),8)
    return word_pro
  • e. 测试
    p(email1=正常邮件)=p(正常邮件)p(zero|正常邮件)p(buy|正常邮件)p(achieve|正常邮件)=...
    p(email1=垃圾邮件)=p(垃圾邮件)
    p(zero|垃圾邮件)p(buy|垃圾邮件)p(achieve|垃圾邮件)=...
    若p(email1=正常邮件)>p(email1=垃圾邮件),则为正常邮件;
    若p(email1=正常邮件)<p(email1=垃圾邮件),则为垃圾邮件;
    若p(email1=正常邮件)=p(email1=垃圾邮件),则用一个随机数随机决定。
    将测试结果与实际结果进行比较,并记录下分类正确和分类错误的数目,计算出TP、FP、FN和TN,最后得到准确率、精确率和召回率。如果有必要的话,画出相应的图进行说明。
    看到要计算准确率、精确率和召回率这里我表示很惭愧,只计算了准确率
#测试
def test_spam(test_repre,pre_pro,con_pro):
    email_pro = numpy.zeros((len(test_repre),2),dtype = numpy.double)
    email_judge = []
    normal_num = 0
    spam_num = 0
    for i in range(len(test_repre)):
        email_pro[i][0] = round(pre_pro[0],8)
        email_pro[i][1] = round(pre_pro[1],8)
        for j in range(len(test_repre[0])):
            if test_repre[i][j] != 0:
                email_pro[i][0] *= con_pro[0][j]
                email_pro[i][1] *= con_pro[1][j]
        if email_pro[i][0] > email_pro[i][1] :
            email_judge.append(0)
        elif email_pro[i][0] < email_pro[i][1] :
            email_judge.append(1)
        else :
            if random.random() > 0.5:
                email_judge.append(1)
            else:
                email_judge.append(0)
    for i in range(normal_test):
        if email_judge[i] == 0:
            normal_num +=1
    for i in range(normal_test,len(test_repre)):
        if email_judge[i] == 1:
            spam_num +=1
    print("email_judge\n")
    print(email_judge)
    print("normal_num="+str(normal_num)+"\nspam_num="+str(spam_num))
    return (normal_num + spam_num)/len(test_repre)

4.结论

由于诸多因素(设置阈值去除稀有词汇,使用TF-IDF表示词汇向量,使用log防止乘数下溢)没有考虑,所以训练大量的数据集时会导致内存不足的现象,故只使用了36个训练数据进行训练,使用36个测试数据测试准确率。最终准确率只有55.5%多,只比瞎猜的大了一点,个人觉得还是训练集太少,再放大一点肯定还是会提升很多的。朴素贝叶斯的基本思想在这,这里我就不多改了,其他的问题之后注意,要进入下一个算法的学习了。在这里附上代码地址,(朴素贝叶斯-spamClassification)[https://github.com/RuiDream/MachineLearning.git]
自己手写的Bayes结果展示:

手写Bayes结果

调包的结果展示(人家的很齐全,还有混淆矩阵,服):
库中Bayes结果

库中Bayes混淆矩阵

注:准确率相差这么大很大的一个原因是训练集的大小不同,大的为25000,我自己写的为36.。。不过还是要怪自己的代码不精,革命刚刚起步……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容