模式分类与应用-贝叶斯垃圾邮件分类

 

 

 

垃圾邮件分类

 

 

 

任务要求

 

使用文件spambase.data中的数据,训练垃圾邮件分类的贝叶斯分类器,并测试分类性能。

 

数据初步分析

 

spambase.data是一个垃圾邮件的数据库,来自于惠普公司的Hewlett Packard Labs实验室,采集时间是1999年的6~7月份。

该数据库中包含了4601个样本,其中1813条为垃圾邮件(spam);每个样本有58个属性。在数据文件中,每行为一个样本,58个属性按顺序排列,使用","分隔。格式如下:

 

图 21 数据文件的格式

 

各属性的含义和数值范围如下表所列:

表格 21 样本属性说明

属性序号

含义

范围

最大值

1-48 

特定单词的出现频率

[0, 100]

<100

49-54 

特定字符的出现频率

[0, 100]

<100

55 

大写字母游程均长

[1, …]

1102.5

56 

最长大写字母游程

[1, …]

9989

57 

大写字母游程总长

[1, …]

15841

58 

垃圾邮件标识(1代表垃圾邮件)

{0, 1}

1

 

第1-54属性可以看成统计特性相似的一类,因为它们都代表了单词或字符的出现频率,范围都是[0, 100]. 考察第1个属性的分布规律,对全部4601个样本做频数统计,如图2-1(a), 可见大部分样本的值为0,还有少部分分布在1以内,均值在零点几左右。

同样,第55~57个属性的统计特性应当是一致的,都表示游程的长度。对第55个属性的数值作频数统计,如图2-1(b), 虽然数值的分布范围很大,但主要集中分布在数值较小的区域。

 

图 22 (a)属性1的数值分布, (b)属性55的数值分布

 

显然,这样的分布不能用高斯分布或其他分布函数来表示。所以可以建立离散的概率密度函数,用样本的频数估计概率密度。

频数统计首先确定组距,或者称之为量化阶:

\[Quantization\_order = \frac{{Max - Min}}{{Bins}}\] 

其中,Max是可接受数据的最大值,min是最小值,Bins为组数。

然后建立频数表,其长度就是组数Bins。遍历待统计的数据,判断每个数据落入何组中,频数表中对于的数值就加1.

如果要得到概率,只需将频数除以总样本数。

 

贝叶斯分类器

 

 

原理:

 

贝叶斯分类是一种有监督的分类方法。其原理如下:首先统计各类的先验概率,以及类条件概率分布,然后通过贝叶斯公式:

\(P({\omega _i}|{\mathbf{x}}) = \frac{{P({\mathbf{x}}|{\omega _i}) \cdot P({\omega _i})}}{{P({\mathbf{x}})}} = \frac{{p({\mathbf{x}}|{\omega _i})d{\mathbf{x}} \cdot P({\omega _i})}}{{p({\mathbf{x}})d{\mathbf{x}}}} = \frac{{p({\mathbf{x}}|{\omega _i}) \cdot P({\omega _i})}}{{p({\mathbf{x}})}}\)( 1)

可以利用已知量转换得到后验概率  \(P({\omega _i}|{\bf{x}})\),即表示在特征x时属于类\({\omega _i}\) 的概率分布。

从式(1)还可以看出,因为  \(P({\bf{x}}|{\omega _i})\)=\(p({\bf{x}}|{\omega _i})d{\bf{x}}\),分子分母同时消去后,后验概率分布实际上也可以通过先验概率密度来计算。而且实际上,\(p({\bf{x}}|{\omega _i})\) 比\(P({\bf{x}}|{\omega _i})\)更容易表示,所以一般使用\(p({\bf{x}}|{\omega _i})\)计算后验概率。

 

朴素贝叶斯

 

如果样本的特征向量x维数很大,会给条件概率密度函数\(p({\bf{x}}|{\omega _i})\)的求取带来困难。假设各属性的值相互独立,则\(p({\bf{x}}|{\omega _i})\)可以表示为:

$$p({\bf{x}}|{\omega _i}) = \prod p({x_k}|{\omega _i})$$(2)

    将式(2)代入式(1),就得到了朴素贝叶斯的表达式:

$$P({\omega _i}|{\bf{x}}) = \frac{{\prod p({x_k}|{\omega _i}) \cdot P({\omega _i})}}{{p({\bf{x}})}} = $$(3)

而\(\prod p({x_k}|{\omega _i})\)中的每一项,可以使用第2节中所说的频数进行估计。

 

决策规则

 

对于两类分类问题,根据后验概率的大小判决所属分类,即:

基于上式,代入贝叶斯公式,由于分母都一样,可以得到变换形式的决策式:

(4-a)

(4-b)

(4-c)

他们都是等价的,针对不同的后验概率形式,选择最简单的表达式即可。在本题中,使用4-a更方便。

 

程序设计

 

通过上述原理的分析,该题的主要计算工作可以分为4步,用顺序结构即可实现。框图归纳如下:

图 41程序流程图

图中标注的步骤编号与程序源码中的注释相对应。

 

编程实现

 

程序按照框图中的4个主要步骤设计。为了将更清楚地显示运行过程,在相同目录下建立了一个文本文件Tracking.log,每一个Step执行完成后,使用file.write()函数在文件中写入运行时间戳和运行结果。

创建文本文件的代码如下,使用写入模式w,若不存在则创建,若已存在同名文件,会清空内容后重新创建。

file= open('Tracking.log','w',buffering=100)#创建一个文件,用于写入运行报告

Step1读入数据,并统计长度


spambase.data中的数据一行有58个,用","分隔。使用numpy的loadtxt()函数可以直接读入,返回一个array对象属性。然后使用array.shape方法得到数据的维度。


original_data_readin = np.loadtxt("spambase.data", dtype=float, delimiter=",")

size = original_data_readin.shape

sampleNums_of_classALl = size[0];#样本总数

sampleNums_of_class_spam=int(sum(original_data_readin[:,-1]))#统计属于垃圾邮件的样本数量(1813)

sampleNums_of_class_good= sampleNums_of_classALl - sampleNums_of_class_spam #正常邮件的数量

length_of_attributes = size[1];#属性向量的长度(应该是58)

程序执行后,original_data_readin变量中存储了读取的原始数据, sampleNums_of_classALl是所有样本的数量,sampleNums_of_class_spam是垃圾邮件样本数量,sampleNums_of_class_good是正常邮件的样本数量,length_of_attributes是特征属性长度。

 

Step2随机产2/3的数据用于训练,并计算类先验概率

 

这一部分位于源码的line49-70.

所给的数据库中,垃圾邮件和非垃圾邮件已经分类,前面是垃圾邮件,后面是非垃圾邮件。以垃圾邮件中产生2/3训练样本为例,说明随机抽取的方法:

遍历所有垃圾邮件样本,使用random.uniform(0,3)函数产生0-3均匀分布的随机数,与1比较,如果小于1,归类为测试样本;大于等于1,则归类为训练样本。将垃圾邮件的训练样本存入矩阵sampls_spam_train,测试样本存入sampls_spam_test.

同理,对正常邮件也进行2/3随机抽取,训练样本存入矩阵sampls_good_train,测试样本存入sampls_good_test.

随后,根据sampls_spam_train和sampls_good_train的长度,计算了类先验概率和.

文件Tracing.log中写入这一步的执行结果。

 

Step3 根据频数估计类条件概率


如第二节中所述,使用频数估计类条件概率密度。这里定义了一个函数:Hist_Estimate(data,bins, min,max,normal),具体如下:

def Hist_Estimate(data=np.zeros((1,54)),bins=quant_order, min=0.0,max=100,normal=1 ):
    #@对输入的数据进行频数估计,返回bins×data.shape[1]维的向量
    deltaX=float(bins)/(max-min)
    #print deltaX
    pdf=np.zeros((bins,data.shape[1]))
    for i in range(data.shape[1]):#pdf的列遍历
        #print i
        for j in range (data.shape[0]):#统计频数
            #print str(i)+str(j)
            pdf[int(deltaX*data[j,i]),i] += 1
    if normal!=1: #返回不归一化的频数
        return pdf
    else:  #返回归一化的频数,可以看作概率密度函数
        return pdf/data.shape[0]

它实现了第2节中所述的频数统计功能,输入data是N*n的样本(N为样本数,n为特征数),bins量化阶数,min和max分别表示频数表的最小和最大值,normal=1返回归一化的频率,否则返回频数。返回值是一个bins*n的矩阵,每一列就是一个属性的频数(或频率)表。由于样本中前54个属性的值范围为[0,100],而55,56,57三个属性的值从1到几千,所以使用对他们分两种情况求频数。调用Hist_Estimate()函数时,前者取min=0, max=100,后者的min=1, max=20000. 量化阶bins都取同样值,这样返回的频数表长度都是一样的。


pdf54_spam = Hist_Estimate(sampls_spam_train[:,:54],bins=quant_order)

pdf54_good = Hist_Estimate(sampls_good_train[:,:54],bins=quant_order)

pdf57_spam = Hist_Estimate(sampls_spam_train[:,54:57],min=0.0,max=20000,bins=quant_order)

pdf57_good = Hist_Estimate(sampls_good_train[:,54:57],min=0.0,max=20000,bins=quant_order)

pdf54_spam中存储了垃圾邮件类中前54个属性的频数表,pdf57_spam是后3个属性的频数表。非垃圾邮件也类似处理。

    这部分的代码在line 84 – 103.

 

Step4 使用测试集数据对分类器作性能测试

 

通过之前的步骤,已经得到了类先验概率和类条件概率密度.由式(3)可以计算出后验概率,再根据式(4-a)做出决策。

函数Gx_Compute(data,px,bins, min,max)用于计算一个输入样本的各个属性的先验概率乘积,即. 由于有些值的概率密度为0,为了避免乘0后结果恒为0,加上了一个微小的常数0.001,所以实际上计算的是.函数Gx_Compute()的输入参数data是一个样本中某些属性组成的向量(1*nk),px是对于的频率表(bins*nk), bin是量化阶数,min和max是频率表中的最小值和最大值。

首先测试垃圾邮件的分类性能。调用函数Gx_Compute(),对sampls_spam_test中的一个样本,计算式(4-a)左右两项。由于57个属性分成了两种量化阶不同的统计,所以计算需要使用两次Gx_Compute(),计算也需要使用两次Gx_Compute(). 比较和的值,如果前者大,则判为垃圾邮件,即正确分类了,计数值amount_of_test_spam_correct加一。遍历完所有待测试垃圾邮件之后,计算正确识别的比例,即amount_of_test_spam_correct / amount_of_test_spam_total.垃圾邮件分类测试的代码如下:

#对spam测试样本进行检验
amount_of_test_spam_total   = sampls_spam_test.shape[0]
amount_of_test_spam_correct = 0

for i in range(amount_of_test_spam_total):
    if (Gx_Compute(sampls_spam_test[i,0:54],pdf54_spam)*Gx_Compute(sampls_spam_test[i,54:57],pdf57_spam,quant_order,0.0,20000)*Pw1>Gx_Compute(sampls_spam_test[i,0:54],pdf54_good)*Gx_Compute(sampls_spam_test[i,54:57],pdf57_good,quant_order,0.0,20000)*Pw2 ):
        amount_of_test_spam_correct  +=1  
print u'垃圾邮件正确检出率'
print float(amount_of_test_spam_correct) / amount_of_test_spam_total

    检验正常邮件识别性能的方法与之相同,只是改变了输入样本,并反转判决条件。

    这部分的代码位于line 124 – 145, 检测结果会打印在屏幕上,同时写入日志。

 

运行结果

 

使用pyinstaller工具打包编译成了单文件exe,将数据文件和程序放在同一目录下,运行exe。

 

图 61数据文件和编译得到的程序

图 62 运行结果

等待程序将所需的动态链接库解压到系统缓存后,就会执行python程序,本次运行,垃圾邮件检出率在0.83,正常邮件识别率在0.93,运行了大约1.15秒。打开目录下生成的Tracking.log文本文件,如下所示:

 

图 63 log文件

文件中记录了每步完成的时间,也记录了关键步骤的运算结果。从结果来看,程序功能是正确的。

 

总结

 

本次的编程过程比较顺利,写完一个步骤就进行验证,没有出现找很久Bug的情况。由于不涉及复杂矩阵运算,几乎所有的运算操作都是从0开始自己写的,没有使用别的库,也就没有参考程序。但这直接造成了程序冗长繁杂,写的有些随意,有些计算方法也不是最优的。幸好我认为变量定义还是很清楚的,看到名称可以大概想到其含义,否则就真的写完之后我自己也看不懂了。

在一开始写的时候,我感觉训练好分类器,然后检测一下分类结果,输出一个正确率,程序就结束了,似乎不是很直观,而且编写时也正好需要调试,突发奇想就想把关键步骤的执行结果写到文本文件里。后来又加了一个毫秒精度的时间戳,这样每一步执行了多少时间,都会写到日志里,这就很有意思了,看到日志似乎可以回顾程序的执行过程。

这次还使用了pyinstaller工具,将python程序编译成了exe,这样即使在没有安装python的计算机上也可以运行。

此外,还遇到了一些问题。首先是print语句打印中文乱码问题,表现为在IDLE的shell中是正常的,但在控制台中运行就会乱码。最后查找到原因是系统的编码与py2.7不同。这在python3中已经解决,但对于python2,需要对编码进行转换:

import sys

type = sys.getfilesystemencoding()

print '输出中文测试..'.decode('utf-8').encode(type)

或者在字符前加u,表示unicode编码:

print u'中文测试'

对于概率密度的计算,使用了频数进行估计。这就涉及到量化阶的问题,即把多大的范围看作一个频数统计区间。如果把量化阶取得过大,一些原本有差异的数值会落入同一个统计区间中,这样就使得模型的灵敏度降低,影响分类效果;但是如果量化阶取得过小,即频数分组很多,在有限的训练样本下,就会造成估计的失准,最后也影响分类准确率。程序中使用了500个量化值进行统计,效果较好。

取不同的频数量化组数,识别率变化如下,说明量化组数在200到1000内比较理想。

频数量化组数

垃圾邮件识别率

非垃圾邮件正确识别率

100

0.835820895522

0.891832229581

200

0.85641025641 

0.929399367756

500 

0.855687606112

0.949506037322

1000 

0.809825673534 

0.953241232731 

1500

0.786991869919 

0.93237704918 

2000 

0.802931596091 

0.932584269663 

3000 

0.78813559322 

0.924528301887 

此外,垃圾邮件的识别率总是比正常邮件的识别率低,我就想到一句话:正常邮件都是相似的,垃圾邮件各有各的不同。

好了,写到这里正好跨年了,我就回去把第一页的日期也改了,祝大家新年快乐!

  • 5
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值