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),即
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结果展示:
调包的结果展示(人家的很齐全,还有混淆矩阵,服):
注:准确率相差这么大很大的一个原因是训练集的大小不同,大的为25000,我自己写的为36.。。不过还是要怪自己的代码不精,革命刚刚起步……