基于朴素贝叶斯的垃圾邮件分类器Java实现和讲解

朴素贝叶斯算法最典型的应用就是垃圾邮件的识别,在数据量非常大的情况下,识别的正确率可以达到接近100%,同时实现起来思路并不复杂。本文介绍的就是基于朴素贝叶斯算法的垃圾邮件识别的实现。如果之前对贝叶斯算法不了解的同学可以先阅读这篇文章,非常好懂!https://blog.csdn.net/fisherming/article/details/79509025

这篇文章最后得到一个非常通俗的公式:

          

第一个“=”代表的就是贝叶斯公式,第二个“=”是在此基础上使用全概率公式进行展开。通过这个公式,使对未知概率的预测转换成了对已知概率的运算。贝叶斯算法的实现其实就是在统计已知数据的概率,最后套入公式计算。

1 数据集

贝叶斯算法实现并不复杂,主要难点在于对数据集的处理,使算法的正确率提高。因此编码之前需要先选择好合适的数据集。

1.1 数据集的下载

 2006 TREC Public Spam Corpora

这是我在网上找到的非常不错的一个垃圾邮件数据集,里面有中英文两种垃圾邮件数据集下载。我下载的是中文垃圾邮件,里面包含了几万封正常邮件和垃圾邮件。

1.2 数据集的索引

这份数据集里面包含了3个文件夹,data里面包含的就是邮件文件夹,包含了60000+封邮件,正常、垃圾邮件都有,没有进行分类和标识。

delay和full里面包含的都是索引文件,索引文件里面每个索引对应一封邮件。如

spam ../data/000/000

ham ../data/000/001

代表的是data中000文件下的000文件为一封垃圾(spam为垃圾)邮件;001文件为一封正常(ham)邮件。

1.3 邮件信息的处理

每一封邮件的格式都是邮件传输信息+邮件主体。

Received: from hp-5e1fe6310264 ([218.79.188.136])
    by spam-gw.ccert.edu.cn (MIMEDefang) with ESMTP id j7CAoGvt023247
    for <lu@ccert.edu.cn>; Sun, 14 Aug 2005 09:59:04 +0800 (CST)
Message-ID: <200508121850.j7CAoGvt023247@spam-gw.ccert.edu.cn>
From: "yan"<(8月27-28,上海)培训课程>
Reply-To: yan@vip.163.com"<b4a7r0h0@vip.163.com>
To: lu@ccert.edu.cn
Subject: =?gb2312?B?t8eyxs7xvq3A7bXEssbO8bncwO0to6jJs8XMxKPE4qOp?=
Date: Tue, 30 Aug 2005 10:08:15 +0800
MIME-Version: 1.0
Content-type: multipart/related;
    type="multipart/alternative";
    boundary="----=_NextPart_000_004A_2531AAAC.6F950005"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2800.1158
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2800.1441


                      非财务纠淼牟莆窆芾-(沙盘模拟)

                     ------如何运用财务岳硖岣吖芾砑ㄐ

                                      

                               [课 程 背 景]

    
   每一位管理和技术人员都清楚地懂得,单纯从技术角度衡量为合算的方案,也许
   却是一个财务陷阱,表面赢利而暗地里亏损,使经
   营者无法接受。如何将技术手段与财务运作相结合,使每位管理和技术人员都从
   老板的角度进行思考,有效地规避财务陷阱,实现管理决策与居勘甑囊恢滦裕
   本课程通过沙盘模拟和案例分析,使企业各级管理和技术人员掌握财务管理知识
   ,利用财务信息改进管理决策,实现管理效益最大化。通过学习本课程,您将:
   ★ 对会计与财务管理有基本了解,提高日常管理活动的财务可行性;
   ★ 掌握业绩评价的依据和方法,评估居导,实施科学的业绩考核;
   ★ 掌握合乎财务栽虻墓芾砭霾叻椒,与老板的思维同步;
   ★ 通过分析关键业绩指标,形成战略规划与全面预算;
   ★ 突出企业管理的重心,形成管理的系统性。

而邮件的传输信息会影响到我们对于邮件的分词,导致正确率下降。因此在实现时需要跳过传输信息这部分。每封邮件的传输信息和主体信息之间会有一行空行,我是通过这个进行分离,只读取空行之后的邮件内容。

2 训练数据集

了解数据集结构后,我们便可以对数据集进行处理。首先需要对一部分数据集进行训练。由于数据集中有60000+邮件,于是我们采用50000封作为训练集,10000封作为测试集。

public static final int TRAIN_MAX_NUM = 50000;//训练邮件最大数
public static final int TEST_MAX_NUM = 10000;//测试邮件最大数

2.1 索引文件读取

训练数直接作用于读取索引的索引数。

    while((indexLine = bufferedReader.readLine())!=null && train_num < TRAIN_MAX_NUM){
            train_num++;
            System.out.println(indexLine);
            String[] typeAndIndex = indexLine.split(" ");
            String type = typeAndIndex[0];
            String index = typeAndIndex[1].replace("..","mail/trec06c");
            String str = readFile(index);

读取一个索引,就需要分离出它所代表的的邮件类型(垃圾或正常)、文件位置。再根据文件位置读取邮件。

2.2 邮件读取、分词

我们需要将邮件读取成一个String字符串,并且跳过传输信息。只有当读取到空行之后才进行字符串拼接。

            String tmp = "";
            int flag = 0;
            while ((tmp = br.readLine()) != null){
                if (flag == 1){
                    str.append(tmp);
                }
                if (tmp.equals("") || tmp.length() == 0){
                    flag = 1;
                }
            }

读取之后得到的字符串需要进行分词,得到一个个独立语义的词语,并保存在集合中。

这里使用的“结巴分词Java版” 

JiebaSegmenter jb = new JiebaSegmenter();
List<String> tempList = jb.sentenceProcess(str);

得到的分词并不是十分完美,里面包含了标点符号这类无意义的字符,因此需要对集合进行清理。这里我只对单个字符或字进行清理,想要得到更完美的集合还需要清理类似多个符号组成的分词。

            List<String> rightList = new ArrayList<>();
            for (String s : tempList){
                if(s.length() > 1)
                    rightList.add(s);
            }

那么,在得到一封邮件的所有分词之后,就可以根据这封邮件的类别(垃圾或正常),对这些分词进行归类。单纯的归入集合中并不能体现出这个词与“垃圾邮件”之间的关联度,为此我们需要使用Map进行存储,存储这个分词在所有“垃圾邮件”中出现的次数。同时,我们也需要对我们所读取的所有分词量进行记录。有了这些才能对上文中的概率公式进行计算。

    public static Map<String, Integer> spamMailMap = new HashMap<>();//垃圾邮件分词表
    public static Map<String, Integer> hamMailMap = new HashMap<>();//正常邮件分词表
    public static Integer spamMailSegNum = 0;//垃圾邮件分词总数量
    public static Integer hamMailSegNum = 0;//正常邮件分词总数量
          if (type.equals("spam")) {
                spamMailSegNum += rightList.size();
                for (String s : rightList) {
                    spamMailMap.put(s, spamMailMap.containsKey(s) ? spamMailMap.get(s) + 1 : 1);
                }
            }else {
                hamMailSegNum += rightList.size();
                for (String s : rightList) {
                    hamMailMap.put(s, hamMailMap.containsKey(s) ? hamMailMap.get(s) + 1 : 1);
                }
            }

2.3 分词概率表的统计

这一步我们计算的是对于每个分词,“邮件中出现分词时,该邮件为垃圾邮件的概率”,公式:

P(Spam|ti) =P1(ti)/((P1(ti) +P2(ti));

 P1(ti )表示ti出现在垃圾邮件中的次数 / 垃圾邮件分词总个数

 P2(ti )表示ti出现在正常邮件中的次数 / 正常邮件分词总个数

这儿如果直接用“ti在垃圾邮件中出现的次数/ti出现的总次数”得到结果的话,在垃圾邮件占多数的情况下,会影响结果。 因此需要用“ti出现的概率代替ti出现的次数”进行计算

    for (Iterator iter = spamMailMap.keySet().iterator(); iter.hasNext();) {
            String key = (String) iter.next();
            double rate = (double) spamMailMap.get(key) / (double) spamMailSegNum;
            double allRate = rate;
            if (hamMailMap.containsKey(key)) {
                allRate += (double) hamMailMap.get(key) / (double) hamMailSegNum;
            }else{ //如果正常邮件中未出现该词,则默认在正常邮件中出现过1次,防止判断过于绝对
                allRate += 1d / (double) hamMailSegNum;
            }
            spamMailRateMap.put(key, rate / allRate);
        }

特殊情况:当该分词未出现在正常邮件集中,则rate = 1,在之后的计算中会导致垃圾邮件的概率直接为100%,否定了其它分词的作用。这是不合理的,因此引入Laplace校准,默认在正常邮件中出现1次。

3 测试数据集

测试的目标是得到每一封邮件为“垃圾邮件”的概率。在此之前,我们已经得到了训练完成的已知概率集合。只要测试邮件的分词出现在我们的已知概率集合中,我们就能通过概率计算(联合概率公式、贝叶斯公式、全概率公式)计算出它为垃圾邮件的概率。

3.1 读取测试集

和训练数据集类似,测试集的读取仍然依靠着full文件夹下的索引文件进行读取。

因为与训练集读取的是同一份文件,我们需要跳过训练集所对应的索引。start_count为训练集的数量。

            if (nowIndex < start_count) {
                nowIndex++;
                continue;
            }

3.2 计算概率

在垃圾邮件这类二分类问题的概率计算上,可以直接使用贝叶斯公式进行代入计算,也可使用联合概率简化计算。联合概率法是在先验概率=1/2(P(垃圾邮件) = P(正常邮件))的基础上,对贝叶斯算法的进一步推导。虽然在实际上,垃圾邮件的概率占多数,但这种方法的正确率仍保持非常高。具体推导过程https://blog.csdn.net/wangyao_bupt/article/details/55002483

3.2.1 联合概率法

P(S|W1 W2) = P(W1 W2|S)P(S) / (P(W1 W2|S)P(S) + P(W1 W2|~S)P(~S))

W1,W2独立:P(W1 W2) = P(W1)P(W2), P(W1 W2|S) = P(W1|S)P(W2|S) (?)
上式 = P(W1|S)P(W2|S)P(S) / (P(W1|S)P(W2|S)P(S) + P(W1|~S)P(W2|~S)P(~S))

应用Bayesian 原理,将 P(Wi|S) 用 P(S|Wi) 表示:
上式 = (P(S|W1)P(S|W2)P(S) * P(W1)P(W2) / P(S)^2) / ((P(S|W1)P(S|W2)P(S) * P(W1)P(W2) / P(S)^2) + (P(~S|W1)P(~S|W2)P(~S) * P(W1)P(W2) / P(~S)^2))

在 P(S) = P(~S) = 50% 的条件下:
上式 = P(S|W1)P(S|W2) / (P(S|W1)P(S|W2) + P(~S|W1)P(~S|W2))
= P1P2 / (P1P2 + (1-P1)(1-P2));

这是我在网上找到的简单推导方法,由此可以得到一个公式

P(Spam|t1,t2,t3……tn)
=[P(Spam|t1)*P(Spam|t2)*……P( Spam|t1)]
/(P(Spam|t1)*P(Spam|t2)*……P( Spam|tn)+[1-P(Spam|t1)]*(1-P(Spam|t2))*……(1-P(Spam|tn)))

其中, P(Spam|ti)我们已经在2.3 分词概率表中求得。

所以使用联合概率法,我们只需要找到测试邮件的所有分词在分词概率表中的概率,代入公式就能得到结果,非常方便。

具体代码:

                //该邮件是否手动设置概率的标志
                boolean flag = false;
                double rate = 1d;
                double tempRate = 1d;
                for (String s : wordList) {
                    if (spamMailRateMap.containsKey(s)) {
                        double tmp = spamMailRateMap.get(s);
                        tempRate *= 1d - tmp;
                        rate *= tmp;
                        //当有一个概率非常趋近于0时,需要进行判断
                        // 判断该邮件更接近垃圾邮件还是正常邮件
                        if (tempRate == 0d && rate == 0d){//同时趋于0,则概率设为0.5
                            probability[testNum] = 0.5d;
                        }else if (tempRate == 0d){//正常邮件概率趋于0
                            probability[testNum] = 1d;
                        }else{//垃圾邮件趋于0
                            probability[testNum] = 0d;
                        }
                        if (tempRate == 0d || rate == 0d){//有趋近,则后续不用进行设置概率
                            flag = true;
                            break;
                        }
                    }
                }
                if (!flag) //flag=true代表已经手动设置过概率
                    probability[testNum] = rate / (rate + tempRate);

 其中rate的结果对应着公式的分子部分,(rate + tempRate)的结果对应着公式的分母部分。probability为邮件的垃圾概率。这一步先存在数组中,最后再与阈值比较。在公式的计算中,rate和tempRate都又可能趋于0。而double型最多只能存储10^-325的数值,小于此数值时会直接即为0。如果不进行处理,后续的probability计算将得到NaN,因此需要进行判断更接近垃圾邮件还是正常邮件。

3.2.2 贝叶斯公式法

贝叶斯公式法相对联合概率法,更容易理解,但需要求更多参数。

  
  注:P1(ti)为ti出现在垃圾邮件中的次数/垃圾邮件分词总数
      P2(ti)为ti出现在正常邮件中的次数/正常邮件分词总数
      P(ti)为ti出现在所有邮件中的次数/邮件分词总数

  1 计算邮件中出现ti时,该邮件为垃圾邮件的概率
  P(Spam|ti)=P1(ti)/((P1(ti)+P2(ti))
  P(ham|ti)=P2(ti)/((P1(ti)+P2(ti))

  2 贝叶斯公式计算垃圾邮件中ti的概率
  P(ti|Spam)=P(Spam|ti)*P(ti)/P(Spam)
  P(ti|ham)=P(ham|ti)*P(ti)/P(ham)=[1-P(Spam|ti)]*P(ti)/[1-P(Spam)]
 
  3 贝叶斯公式计算最后概率
  P(Spam|t1,t2,...,tn)
=P(Spam)*P(t1|Spam)P(t2|Spam)*...*P(tn|Spam)
/[P(Spam)*P(t1|Spam)P(t2|Spam)*...*P(tn|Spam)+ P(ham)*P(t1|ham)P(t2|ham)*...*P(tn|ham)]
 
  3.1 分子展开
  P(Spam)*P(t1|Spam)*P(t2|Spam)*...*P(tn|Spam)
=P(Spam)*[P(Spam|t1)*P(t1)/P(Spam)]*...*[P(Spam|tn)*P(tn)/P(Spam)]
 
  3.2 分母后一项展开
  P(ham)*P(t1|ham)*P(t2|ham)*...*P(tn|ham)
=[1-P(Spam)]*[1-P(Spam|t1)]*P(t1)/[1-P(Spam)]*...*[1-P(Spam|tn)]*P(tn)/[1-P(Spam)]

 

P(Spam)就是所有邮件中垃圾邮件的概率(先验概率),P(ham) = 1 - P(Spam)

我们在训练后就可以得到:

        //垃圾邮件率
        spamTrainRate = (double) spamTrainMailNum / (double) train_num;

同时,P(Spam|ti)我们已经在2.3 分词概率表中求得。

P(ti)为ti出现在所有邮件中的次数/邮件分词总数,我们也可以通过计算求得:

((double)(spamMailMap.get(s) + (hamMailMap.containsKey(s)? hamMailMap.get(s): 0)))
 / (double) (spamMailSegNum + hamMailSegNum)

至此,分子和分母中所有参数均可以计算。贴上代码:

//该邮件是否手动设置概率的标志
                boolean flag = false;
                //P(Spam)*  乘积P(Spam|ti)*P(ti)/P(Spam) (i从1-n)
                //spamTrainRate = P(Spam)
                double rate = spamTrainRate;
                //[1-P(Spam)]*  乘积[1-P(Spam|ti)]*P(ti)/[1-P(Spam)] (i从1-n)
                // 1 - spamTrainRate = [1-P(Spam)]
                double tempRate = 1 - spamTrainRate;
                for (String s : wordList) {
                    if (spamMailRateMap.containsKey(s)) {
                        double spamTmp = spamMailRateMap.get(s);
                        rate *= spamTmp * (((double)(spamMailMap.get(s) + (hamMailMap.containsKey(s)? hamMailMap.get(s): 0))) / (double) (spamMailSegNum + hamMailSegNum)) / spamTrainRate;
                        tempRate *= (1d - spamTmp) * (((double)spamMailMap.get(s)+(hamMailMap.containsKey(s)? hamMailMap.get(s): 0)) / (double) (spamMailSegNum + hamMailSegNum)) / spamTrainRate;;
                        //当有一个概率非常趋近于0时,需要进行判断
                        // 判断该邮件更接近垃圾邮件还是正常邮件
                        if (tempRate == 0d && rate == 0d){//同时趋于0,则概率设为0.5
                            probability[test_num] = 0.5d;
                        }else if (tempRate == 0d){//正常邮件概率趋于0
                            probability[test_num] = 1d;
                        }else{//垃圾邮件趋于0
                            probability[test_num] = 0d;
                        }
                        if (tempRate == 0d || rate == 0d){//有趋近,则后续不用进行设置概率
                            flag = true;
                            break;
                        }
                    }

                }
                if (!flag) { //flag=true代表已经手动设置过概率
                    probability[test_num] = rate / (rate + tempRate);
                }

与联合概率法的明显区别在于rate,tempRate初值的设定(贝叶斯法需要将先验概率作为初值)以及复杂计算公式。

后续

1.优化了联合概率法手动判定邮件类型语句,减少了误判率,消除了0.5概率处样本聚集情况。

                //该邮件是否手动设置概率的标志
                int flag = 0;
                double rate = 1d;
                double tempRate = 1d;
                for (String s : wordList) {
                    if (spamMailRateMap.containsKey(s)) {
                        double tmp = spamMailRateMap.get(s);
                        tempRate *= 1d - tmp;
                        rate *= tmp;
                        //当有一个概率非常趋近于0时,需要进行判断
                        // 判断该邮件更接近垃圾邮件还是正常邮件
                        if (tempRate == 0d && rate == 0d){//同时趋于0,则概率设为0.5
                            flag = 1;//同时趋于0
                        }else if (tempRate == 0d){//正常邮件概率趋于0
                            probability[testNum] = 1d;
                            flag = 2;//正常趋于0
                            if (rate > Math.pow(10, -200)){//差125量级,直接判定
                                break;
                            }
                        }else if(rate == 0d){//垃圾邮件趋于0
                            probability[testNum] = 0d;
                            flag = 3;//垃圾趋于0
                            if (tempRate > Math.pow(10, -200)){//差125量级,直接判定
                                break;
                            }
                        }
                        if (flag == 1){//已经同时趋于0,不需要继续遍历
                            probability[testNum] = 0.5d;
                            break;
                        }
                    }
                }
                if (flag == 0) { //flag!=0代表已经手动设置过概率
                    probability[testNum] = rate / (rate + tempRate);
                }

2.贝叶斯公式在处理大量分词时,会导致正常邮件概率(tempRate)和垃圾邮件概率(rate)都趋于0, 导致程序判定为概率为0.5

于是优化了贝叶斯公式法,当正常邮件概率(tempRate)和垃圾邮件概率(rate)有一个快趋于0时,将二者都进行放大,不会影响结果,且保证了概率不会趋于0。

if (rate < Math.pow(10, -300) || tempRate < Math.pow(10, -300)){
     if (rate > 1){
        probability[testNum] = 1d;
        flag = 2;
        break;
     }else if (tempRate > 1){
        probability[testNum] = 0d;
        flag = 3;
        break;
     }else {
        rate *= Math.pow(10, 7);
        tempRate *= Math.pow(10, 7);
     }
}

 分布恢复正常:

 

结语

得到probability数组后,我们就可以通过它与阈值的比较,判断出所有邮件的类别(垃圾或正常),在50000条训练集下,两种计算公式均能达到95%以上的正确率。而垃圾邮件分类器,不仅对正确率有要求,还得尽量不能“错杀”任何一封正常邮件。我将在下一篇文章对这个垃圾邮件分类器进行指标评价,欢迎阅读。后续也会将项目源码放在GitHub上。

GitHub链接:https://github.com/aGreySky/bayesSpamMail

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值