你有垃圾邮件






4.77/5 (10投票s)
设计并实现一个简单的人工智能代理,该代理能够学习和对抗无休止的垃圾邮件瘟疫。
- 下载 Training_Datasets.zip - 1.1 MB
- 下载 Test_Dataset.zip - 72.1 KB
- 下载 Knowledge_Base.zip - 6.2 KB
- 下载 Programs.zip - 3.2 KB
垃圾邮件瘟疫
自互联网问世以来,垃圾邮件就一直充斥着网络空间,并且难以捉摸。根据《牛津词典》的说法,“Spam”一词源于一个玩笑,它是
引用一种罐装肉制品,主要由火腿制成。
如图 1 所示
图 1:无辜的垃圾邮件罐头
然而,在网络空间中,它却成了所有人的恼人干扰。
引用发送到互联网上的不相关或未经请求的消息,通常发送给大量用户,用于广告、网络钓鱼、传播恶意软件等目的。
感谢《牛津词典》,在本文中,我将比喻性地将所有合法消息称为“ham”(火腿)。
除非您是垃圾邮件发送者,否则普遍的看法是,人们更喜欢“ham”(火腿)而不是“spam”(垃圾邮件)。
即使有垃圾邮件过滤软件的帮助,仍然有一些垃圾邮件设法逃脱过滤,偶尔会到达我们的机器。筛选和清除垃圾邮件仍然是当今许多数字时代凡人的一个苦差事。既然无法阻止垃圾邮件发送者,我们只能寄希望于更好、更智能的垃圾邮件过滤软件来抵挡无休止的垃圾邮件。应对这样的挑战需要为传统的垃圾邮件过滤软件配备更好的算法以及自主学习的能力。
在本文中,我们将着手一个项目,探索一个简单的人工智能代理的设计和实现,该代理能够根据消息中包含的词语来学习和更新知识,以判断一条消息是否为垃圾邮件。我们称这个代理为“Spam Doctor”(垃圾邮件医生)。
问题陈述
我们将从大声朗读以下问题陈述开始我们的学习之旅:
引用您如何判断一条消息可能是垃圾邮件?
您多半是通过扫描消息中的词语来得出结论的,对吗?基于经验,垃圾邮件虽然主题和类型各不相同,但在词语的选择上似乎有一些共同的模式。根据 Symantec 的一项研究,观察到在垃圾邮件中:
引用常用词相当通用,但似乎都旨在鼓励立即做出反应,试图制造某种紧迫感。
因此,我们可以合理地假设“某些词语在垃圾邮件中比在 ham 中出现的频率更高”。似乎我们可以利用这一假设来区分垃圾邮件和 ham。我们现在可以将问题陈述重新表述为:
引用给定一条消息包含某些词语,它很可能是垃圾邮件的概率是多少?
这听起来更清晰,也更容易管理。我们现在有了一个方向,但仍然不确定前方是什么,我们需要一盏灯来为我们指引方向。我们将在这里使用的灯是“贝叶斯定理”。
快速了解贝叶斯定理
生活充满不确定性。不确定性会带来焦虑,因为它们阻止人们自信地做出决定。无论是抛硬币和掷骰子这样的琐事,还是国家安全或医疗诊断等生死攸关的情况,人们都希望确信“某个事件发生的概率是多少”。为了减轻不确定性,人们可能会根据从相关经验证据、统计数据或个人观点、经验和评估的组合中获得的先验知识进行推断。
例如,根据英国癌症研究中心提供的肺癌统计数据:
引用13 名男性和 17 名女性一生中将被诊断出患有肺癌。
在概率论中,上述陈述可以表示为:
这种仅基于样本分布先验知识的概率有时被称为“先验概率”。在没有其他支持证据的情况下,先验概率本身只是非常笼统地表达了一种信念,即每个男性的肺癌发病几率都相等,每个女性也是如此,而我们知道情况并非如此。
英国癌症研究中心提供的肺癌统计数据还发现:
引用一个人患癌症的风险取决于许多因素,包括年龄、遗传以及暴露于风险因素(包括一些可能可以避免的生活方式因素)。
因此,除了性别之外,这个陈述清楚地表明,患肺癌的几率因人而异,取决于其他证据,例如每个人身上的年龄、遗传、吸烟、饮食等。如果我们能够将这些证据纳入概率计算,我们将对结果更加确定。简单地说,我们正在寻找一种方法来计算一个人患肺癌的概率,同时考虑到任何相关风险因素的存在,例如:
其中 \(|\) 表示“给定”。
这就引出了贝叶斯定理……
贝叶斯定理描述了一种方法,通过考虑支持和反对某个假设的证据来推导和更新某个假设(某个事件将发生的信念)的概率。这种类型的概率有时被称为“后验概率”,因为它的值是通过考虑支持和反对该假设的证据来更新先验概率得出的。下面的公式显示了贝叶斯规则在处理多个假设 \(H_{k}\) 和多个证据 \(E_{n}\) 的情况:
其中
- \(P(H_{i})\) 是假设 \(i\) 的先验概率。
- \(P(E_{n}|H_{i})\) 是在假设为真时证据 \(n\) 的条件概率。
- \(P(H_{i} |E_{1}E_{2}\cdot \cdot \cdot E_{n})\) 是在考虑了支持和反对假设 \(i\) 的证据后得到的后验概率。
- 假设必须是互斥且完备的,即 \(H_{j}\cap H_{j}= \oslash\; for\; i\neq j\),并且 \(\sum_{k=1}^{m}P( H_{k})\) \(= 1\)。
- 证据是独立的,即 \( P(E_{i}|E_{j})\) \(= P(E_{i})\) \(for\; i \neq j\)。
贝叶斯规则将假设在获得证据之前的先验概率 \(P( H_{i})\) 与获得证据之后的假设后验概率 \(P( H_{i} |E_{1}E_{2}\cdot \cdot \cdot E_{n})\) 联系起来。这个连接两者的因子有时被称为“似然比”,即 \(\frac{P(E_{1}|H_{i}) \times P(E_{2}|H_{i})\cdot \cdot \cdot \times P(E_{n}|H_{i})}{\sum_{k=1}^{m}P(E_{1}|H_{k}) \times P(E_{2}|H_{k})\cdot \cdot \cdot \times P(E_{n}|H_{k})\times P( H_{k})}\)。
我们将使用贝叶斯规则作为算法,根据消息中包含的词语作为证据,计算“消息是垃圾邮件”这一假设的后验概率。
算法
我们已经找到了 Spam Doctor 的算法,即贝叶斯规则。让我们将这个算法植入 Spam Doctor。
假设
一条消息要么是垃圾邮件,要么是 ham。这就给我们提供了两个互斥且完备的假设,如下所示:
证据
一条消息中包含的词语集合(去除停用词、标点符号和 HTML 标签等噪声)就是证据。证据被定义为一个词语集合,这意味着集合中的每个词语只出现一次,即使它在消息中出现多次。例如:
一个更聪明的垃圾邮件医生
我们的 Spam Doctor 在植入了贝叶斯规则后变得更加智能,形式如下:
使用这个公式,Spam Doctor 将能够计算出“一条消息是垃圾邮件的后验概率,给定它包含的词语集合”,即 (P( H_{spam} |E_{1}E_{2}\cdot \cdot \cdot E_{n}))。
话虽如此,我们的 Spam Doctor 仍然缺乏正常运行所需的一些基本知识。这些知识包括:
- 一条消息是垃圾邮件的先验概率,即 \(P( H_{spam} )\)。
- 一条消息是 ham 的先验概率,即 \(P( H_{ham} )\)。
- 一个词语出现在垃圾邮件中的条件概率,即 \(P(E_{n}|H_{spam})\)。
- 一个词语出现在 ham 中的条件概率,即 \(P(E_{n}|H_{ham})\)。
机器学习
为了获取知识,我们学习。我们的 Spam Doctor 也是如此。它将通过机器学习从两个训练数据集中获取知识,每个数据集包含 1000 条垃圾邮件和 1000 条 ham 消息。用于实现 Spam Doctor 机器学习的代码包含在这个名为 `spam_trainer.py` 的 Python 程序中。
让我们一步步了解机器学习的各个阶段,并附带相关的代码片段和/或截图(如果适用)。
预处理训练数据
通过以下步骤预处理训练数据:
- 从两个训练数据集中的每条消息中删除停用词、标点符号和 HTML 标签等噪声;然后
- 将每条消息分词成一个词语集合,其中每个词语只出现一次。
噪声的去除有助于提高处理速度,而每个词语只计算一次则可以最大限度地减少对特定假设的偏见。
用于预处理训练数据的函数如下所示:
def tokenize(dataset): # Convert all letters to lowercase temp1 = [ message.lower() for message in dataset ] # print(temp1[-1], end='\n\n') # Relegate each unwanted word to a whitespace temp2 = [ message.replace('<p>', ' ').replace('</p>', ' ').replace('<a href="https://', ' ').replace('">', ' ').replace('</a>', ' ') for message in temp1 ] # Break each message into tokens of word temp3 = [ word_tokenize(message) for message in temp2 ] # Remove duplicate tokens in each message temp4 = [ set(element) for element in temp3 ] # print(temp4[-1], end='\n\n') # Remove tokens of stop words and punctuation stopWords = set(stopwords.words('english')) stopWords.update(string.punctuation) finalDataset = [] for tokenList in temp4: temp5 = [] for token in tokenList: if token not in stopWords: temp5.append(token) finalDataset.append(temp5) return finalDataset
以下是垃圾邮件训练数据集中包含的消息之一:
<p>But could then once pomp to nor that glee glorious of deigned. The vexed times childe none native. To he vast now in to sore nor flow and most fabled. The few tis to loved vexed and all yet yea childe. Fulness consecrate of it before his a a a that.</p><p>Mirthful and and pangs wrong. Objects isle with partings ancient made was are. Childe and gild of all had to and ofttimes made soon from to long youth way condole sore.</p>
预处理后,上述消息被分词成一个词语集合,如下所示:
'partings', 'sore', 'childe', 'none', 'soon', 'isle', 'native', 'ofttimes', 'consecrate', 'mirthful', 'objects', 'glee', 'gild', 'condole', 'ancient', 'deigned', 'fulness', 'times', 'glorious', 'way', 'wrong', 'made', 'flow', 'vexed', 'pomp', 'loved', 'tis', 'could', 'yea', 'yet', 'long', 'youth', 'fabled', 'vast', 'pangs'
计算垃圾邮件和 ham 中分词后词语的条件概率
分别计算每个分词后词语在垃圾邮件训练数据集和 ham 训练数据集中出现的条件概率。
用于计算训练数据集中包含的分词后词语的条件概率的函数如下所示:
def tokenConditionalProbability(dataset): # Number of samples in dataset sampleSize = len(dataset) # Dictionary of token-probability pairs conditionalProbabilities = {} # Count probability of occurence of each token flatten = [] flatten[len(flatten):] = [ token for sample in dataset for token in sample ] tokenCount = Counter(flatten) conditionalProbabilities = { key : value / sampleSize for key, value in tokenCount.items()} return conditionalProbabilities
以下显示了源自垃圾邮件训练数据集的一些分词后词语及其相关的条件概率:
'objects': 0.26, 'made': 0.471, 'ancient': 0.263, 'sore': 0.466, 'fulness': 0.29, 'glorious': 0.265, 'native': 0.478, 'could': 0.481, 'partings': 0.287, 'consecrate': 0.287, 'loved': 0.616, 'tis': 0.471, 'deigned': 0.262, 'pangs': 0.27, 'vast': 0.276, 'times': 0.292, 'long': 0.621, 'mirthful': 0.297, 'youth': 0.286, 'wrong': 0.295, 'fabled': 0.289, 'condole': 0.286, 'soon': 0.311, 'isle': 0.293, 'pomp': 0.281, 'gild': 0.293, 'vexed': 0.253, 'ofttimes': 0.275, 'glee': 0.316, 'childe': 0.865, 'none': 0.69, 'flow': 0.252, 'way': 0.27, 'yea': 0.277, 'yet': 0.714, 'saw': 0.263, 'change': 0.274, 'would': 0.731, 'deem': 0.468, 'talethis': 0.285, 'old': 0.274, 'dome': 0.249, 'atonement': 0.267, 'spoiled': 0.276, 'things': 0.283, 'holy': 0.279, 'cell': 0.278, 'suffice': 0.284, 'mote': 0.497, 'vaunted': 0.309, 'noontide': 0.279, 'break': 0.28, 'days': 0.261, 'basked': 0.279, 'breast': 0.503, 'found': 0.282, 'adieu': 0.289, 'adversity': 0.282, 'love': 0.464, 'men': 0.301, 'prose': 0.274, 'strange': 0.269, 'said': 0.283
计算假设的先验概率
在样本空间总共 2000 条消息的情况下,当从该样本空间中随机选择一条消息时,选择一条垃圾邮件的先验概率与选择一条 ham 的先验概率相同,如下所示:
设置区分垃圾邮件和 ham 的阈值
如果垃圾邮件假设的后验概率超过某个阈值,例如 0.8,则该消息被视为垃圾邮件,如下所示:
建议将阈值设置得更高,以尽量减少“误报”,即 ham 被错误地识别为垃圾邮件的情况。最佳阈值因人而异,可以通过反复试验得出。
保存学习到的知识
训练结束后,我们的 Spam Doctor 将获得实现贝叶斯规则算法所需的知识。您应该将学习到的知识,即词语在垃圾邮件中的条件概率、词语在 ham 中的条件概率、假设的先验概率以及区分垃圾邮件和 ham 的阈值,保存到某个计算机存储中,以供将来测试和生产时重用。它们构成了 Spam Doctor 的知识库。
测试 1、2、3
Spam Doctor 已经获得了算法,并学会了区分垃圾邮件和 ham 的基本知识。然而,我们不知道它做得怎么样?让我们来测试一下。
测试是通过向 Spam Doctor 输入来自测试数据集的 43 条垃圾邮件和 57 条 ham 消息的混合数据来完成的。测试的步骤如下:
- 从测试数据集中每条消息中删除停用词、标点符号和 HTML 标签等噪声。
- 将测试数据集中的每条消息分词成一个词语集合,其中每个词语只出现一次。
- 使用知识库中这些词语的条件概率和假设的先验概率,计算测试数据集中每条消息是垃圾邮件假设的后验概率。在知识库中找不到的分词后词语,其条件概率为 0.01,而不是 0。这是为了最大限度地减少对某个假设的偏见。
- 如果一条消息是垃圾邮件假设的后验概率超过某个阈值,例如 0.8,则将其诊断为垃圾邮件,否则为 ham。
- 测试结束后,将 Spam Doctor 测试的每条消息的结果与其在测试数据集中的预期结果进行比较。例如,垃圾邮件可能在测试中被错误地诊断为 ham,而不是获得预期的垃圾邮件结果。
用于实现测试的代码包含在 `spam_trainer.py` 中。
具体来说,用于计算测试数据集中每条消息是垃圾邮件假设的后验概率的函数如下所示:
def spamPosteriorProbability(tokenList): spamTokenConditionalProbability = 1 hamTokenConditionalProbability = 1 for token in tokenList: if token not in spamTokensConditionalProbabilities: spamTokenConditionalProbability *= 0.01 # To minimize false positive else: spamTokenConditionalProbability *= spamTokensConditionalProbabilities[token] if token not in hamTokensConditionalProbabilities: hamTokenConditionalProbability *= 0.01 # To mininize false negative else: hamTokenConditionalProbability *= hamTokensConditionalProbabilities[token] return spamTokenConditionalProbability * spamPriorProbability / (spamTokenConditionalProbability * spamPriorProbability + hamTokenConditionalProbability * hamPriorProbability)
我们可以使用混淆矩阵来评估 Spam Doctor 在测试中的表现,如下图 2 所示:
图 2:混淆矩阵
- 真阳性 (True Positive) 是 Spam Doctor 在测试中成功识别为垃圾邮件的预期垃圾邮件数量。它是图 2 中“预期垃圾邮件”和“测试为垃圾邮件”的交集。测试结果为 43。
- 假阴性 (False Negative) 是 Spam Doctor 在测试中错误地将预期垃圾邮件识别为 ham 的数量。它是图 2 中“预期垃圾邮件”和“测试为 ham”的交集。测试结果为 0。
- 假阳性 (False Positive) 是 Spam Doctor 在测试中错误地将预期 ham 识别为垃圾邮件的数量。它是图 2 中“预期 ham”和“测试为垃圾邮件”的交集。测试结果为 0。
- 真阴性 (True Negative) 是 Spam Doctor 在测试中成功识别为 ham 的预期 ham 数量。它是图 2 中“预期 ham”和“测试为 ham”的交集。测试结果为 57。
- 准确率 (Accuracy) 指的是测试的整体正确性,表示为:$(真阳性 + 真阴性) \div 总数 = (43 + 57) \div 100=1$
- 精确率 (Precision) 指的是测试结果的正确性,表示为:$真阳性 \div (真阳性 + 假阳性) = 43 \div (43 + 0) = 1$
用于计算混淆矩阵各种度量的函数如下所示:
for data, spamPosteriorProbability in zip(testSet, spamPosteriorProbability): expected = data.split(',')[0] if expected == 'Spam': if spamPosteriorProbability > threshold: truePositive += 1 else: falseNegative += 1 elif expected == 'Ham': if spamPosteriorProbability > threshold: falsePositive += 1 else: trueNegative += 1
混淆矩阵显示,我们的 Spam Doctor 以优异的成绩通过了测试!:thumbsup
投入使用
既然我们有了一个称职的人工智能代理 Spam Doctor,为什么不把它投入使用呢?例如,我用 Python 编写了一个简单的 GUI 应用程序 `spam_doctor.py`,该应用程序融合了 Spam Doctor 的智能和知识。启动该应用程序后,它会提供一个文本框,供用户粘贴消息,以及一个提交消息到 Spam Doctor 引擎进行诊断的按钮。然后,诊断结果会显示在一个弹出框中。您可以自己使用测试数据集中的消息进行检查,或者通过图 3 中的动画一睹 Spam Doctor 的工作状态。
图 3:Spam Doctor 工作中
结论
本文以垃圾邮件过滤项目为借口,带您了解了设计和实现一个简单的人工智能代理和机器学习的基础知识。
与人类一样,人工智能代理通过在历史数据上应用适当的算法来学习解决问题的知识。一个优秀的人工智能代理应该能够从学到的知识中进行泛化,以解决它在训练阶段没有遇到过的新问题。
由于泛化的权衡,人工智能代理,就像人类一样,不太可能像我们的 Spam Doctor 那样获得完美的准确率和精确率分数。要真正让 Spam Doctor 准备好面对现实世界,您必须用大量的真实垃圾邮件和 ham 消息来训练它,然后用大量的真实但它在训练期间未遇到过的垃圾邮件和 ham 消息来测试它。在每个测试阶段之后,使用混淆矩阵或其他合适的工具根据测试结果衡量其性能。调整一些参数,例如阈值,然后重新训练,反复测试,直到达到满意的性能。为此,我将把这项任务交给您。
最后但同样重要的是,即使人工智能代理犯了错误,它也应该从错误中学习,将失败的经历纳入其知识库,这样它就不会在将来重复同样的错误。这难道不也适用于我们人类吗?