编译:AI100,本文已获授权,转载请联系AI100.
原文链接:https://hackernoon.com/how-to-build-a-simple-spam-detecting-machine-learning-classifier-4471fe6b816e
在本篇教程中,我们会先提出要解决的问题,然后再利用名为朴素贝叶斯分类器(NaiveBayes Classifier)的机器学习技术解决相应的问题,非常简单。本篇教程需要读者具备编程和数据方面的相关经验,但不必具备机器学习方面的经验。
垃圾邮件的检测
在公司中,你是一位为数以百万计用户提供邮件服务的软件工程师。
最近,垃圾邮件问题十分棘手并且已经导致部分客户流失。然而,当前的垃圾邮件过滤器只能筛选出那些之前被用户被标记过的垃圾邮件,垃圾邮件发送者也变得越来越狡猾。为了防止客户流失,你的任务就是提前预测出当前正在发送的邮件是否为垃圾邮件。
训练并测试数据
为了创建出预测垃圾邮件的算法,你必须让程序知道什么样的邮件是垃圾邮件(以及什么样的邮件是正常的邮件)。幸运的是,你有用户先前标记的所有垃圾邮件。你还需要找到测试垃圾邮件过滤器准确性的方法。其中一个想法就是利用训练垃圾软件过滤器的数据对其进行测试。但是,这种做法会带来机器学习过程中的过度拟合问题,也就意味着你的模型太过偏向于数据的训练。因此,当其处于此训练集以外的数据中时,运行结果不会太理想。避免这种结果的普遍做法是从训练数据和测试数据中分别抽取70%和30%的标记数据。这么做能够保证其是在测试不同的数据而非训练数据。你一定要注意,数据集中不能全是垃圾邮件,既要有垃圾邮件也要有正常的邮件。如果的确想要与真正的邮件数据极度相似的训练数据的话,我在帖子底端给出了一个很好的数据集链接,你可以参考一下。
贝叶斯定理(Bayes’ Theorem)
贝叶斯定理的数学表达式,如下所示:
从本质上来说,贝叶斯定理的数学表达式为那些无法直接计算的条件概率提供了便捷的途径。例如,如果你想计算某些人在某个年龄段罹患癌症的概率,但是你只有关于癌症年龄分部的数据,那么就可以把这些数据放进贝叶斯定理的数学表达式中,而不必进行全国性的研究。如果数据理论把你弄得晕头转向,完全不必担心,当它转化为代码时会更有意义的。不过,我衷心地建议你,如果无法理解许多逻辑谬误的基础贝叶斯定理的话,稍后请重新阅读一下这一部分,然后试着理解该定理。
朴素贝叶斯分类器
对于我们的问题,我们可以把A设定为垃圾邮件的概率,B设定为邮件内容。如果P(A|B)>P(¬A|B),那么我们就可以把邮件归类为垃圾邮件,反之就可以把相应的邮件归类为正常邮件。请注意,贝叶斯定理的结果是两边都以P(B)为除数,为了方便比较,我们把P(B)从方程式中去掉。现在方程式是这样的:P(A)*P(B|A) > P(¬A)*P(B|¬A)。计算P(A)和P(¬A)并不难,它们只是训练集中垃圾邮件和正常邮件的百分比:
#runs once on training data
def train:
total = 0
numSpam = 0
for email in trainData:
if email.label == SPAM:
numSpam += 1
total += 1
pA = numSpam/(float)total
pNotA = (total — numSpam)/(float)total
更难计算的部分是P(B|A)和 P(B|¬A)。为了计算这些数据,我们准备利用词袋模型。词袋模型是一个非常简单的模型,它把一段文本当作单个词的包(a bag of individual words),顺序并不重要。对于每个单词,我们都分别计算它出现在垃圾邮件和正常邮件中次数的百分比。我们称这种概率为P(B_i|A_x)。我们需要用一个具体的实例来计算P(free | spam),我们计算free这个词在所有垃圾邮件中出现次数的总和,然后除以垃圾文件中所有词的总数。虽然这些是静态值,但是我们可以在训练过程中计算出这些值。
#runs once on training data
def train:
total = 0
numSpam = 0
for email in trainData:
if email.label == SPAM:
numSpam += 1
total += 1
processEmail(email.body, email.label)
pA = numSpam/(float)total
pNotA = (total — numSpam)/(float)total#counts the words in a specific email
def processEmail(body, label):
for word in body:
if label == SPAM:
trainPositive[word] = trainPositive.get(word, 0) + 1
positiveTotal += 1
else:
trainNegative[word] = trainNegative.get(word, 0) + 1
negativeTotal += 1#gives the conditional probability p(B_i | A_x)
def conditionalWord(word, spam):
if spam:
return trainPositive[word]/(float)positiveTotal
return trainNegative[word]/(float)negativeTotal
只要知道每个单词i的P(B_i|A_x)值的结果,我们就能得到全部邮件的P(B|A_x)。请注意,在初始训练的时候,我们无法获得P(B|A_x)值的结果,只能在分类的时候获取该数值。
#gives the conditional probability p(B | A_x)
def conditionalEmail(body, spam):
result = 1.0
for word in body:
result *= conditionalWord(word, spam)
return result
最终,我们获得了需要进行整合的所有元件。我们所需的最后一部分就是调用每封邮件并且利用我们之前的功能对邮件进行分类。
#classifies a new email as spam or not spam
def classify(email):
isSpam = pA * conditionalEmail(email, True) # P (A | B)
notSpam = pNotA * conditionalEmail(email, False) # P(¬A | B)
return isSpam > notSpam
祝贺你!你已经成功地从头开始编码了一个朴素贝叶斯分类器!
可是,你仍需要做一些改进以使分类器达到最佳运行状态而且没有错误:
拉普拉斯平滑方法(Laplace Smoothing):
我们未曾提及的一件事就是:如果分类邮件中出现了一个从未在训练集中出现过的单词,接下来会发生什么。我们需要添加一个平滑因子来处理这种情况。最好的例证就是在修改过的代码下面添加平滑因子alpha,如图所示:
#gives the conditional probability p(B_i | A_x) with smoothing
def conditionalWord(word, spam):
if spam:
return (trainPositive.get(word,0)+alpha)/(float)(positiveTotal+alpha*numWords)
return (trainNegative.get(word,0)+alpha)/(float)(negativeTotal+alpha*numWords)
对数空间函数(Log-Space)
当前的实现方法非常依赖于浮点乘法。为了规避所有可能的问题,我们通常会乘以非常小的数,函数通常在方程式中执行对数运算进而将所有的乘法运算转换成加法运算。我未曾在示例代码中执行这种函数,但是强烈建议你在实践中运用一下这种函数。
TF-IDF算法
总体来说,文本分类器的词包模型是相当朴素的并且可以通过TF-IDF这样的算法对其进行优化处理。
N-Grams算法
我们能进行的另一个优化处理,不仅仅只是计算单个词的概率。在N-Grams技术中,设想其是一个拥有N个连续单词的集并且利用他们计算概率。因为在英语中1-gram 中的‘good’传达的意思并不是the 2-gram 中的‘not good’。
Tokenization(符号化)
其中一件非常有意思的事情就是,你是如何分类不同的单词的。例如,Free、free和FREE这是三个相同的单词吗?对于标点又如何处理呢?
请注意,编写示例代码是为了最优化教学,而不是为了运行这些代码。这些清晰、简单地改进方法能够大幅地提升代码的运行速度。
示例数据集:
https://spamassassin.apache.org/publiccorpus/