65.9K
CodeProject 正在变化。 阅读更多。
Home

朴素贝叶斯垃圾邮件分类器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.40/5 (2投票s)

2018年3月3日

CPOL

10分钟阅读

viewsIcon

14045

downloadIcon

435

通过构建使用 Python 和 scikit-learn 的朴素贝叶斯垃圾邮件分类器,来演练常见的机器学习任务。

引言

在本文中,我们将逐步介绍如何使用 Python 和 scikit-learn 构建一个用于朴素贝叶斯垃圾邮件分类器的机器学习模型。由于垃圾邮件是一个已被充分理解的问题,并且我们选择了一个流行的算法——朴素贝叶斯,我将不再深入探讨其数学原理和理论。相反,我将专注于如何将其作为机器学习问题来解决。我将讨论常见的机器学习步骤以及我如何将它们应用于该问题。

安装

本文使用 sklearn 完成机器学习任务。它是一个流行的 Python 机器学习工具。所有相关代码都在 spam-detection.ipynb Jupyter Notebook 中运行,它包含在源下载和 GitHub 仓库中。我还已经在 environment.yml 中定义了所有依赖项,以便更轻松地设置相同的环境。

如果您想跟着操作,可以:

  1. 安装 Python3Anaconda
  2. 从源文件中包含的 environment.yml 创建并激活环境。
    conda env create -f environment.y
    
    # Activate the new environment
    # on windows
    activate spam-detection
    
    # on macOS and Linux 
    source activate spam-detection
  3. 运行 notebook。
    jupyter notebook spam-detection.ipynb
    

定义机器学习问题

首先,让我们将该问题定义为一个机器学习问题。我们有一个电子邮件语料库,每封电子邮件都标记为垃圾邮件或非垃圾邮件(正常邮件)。这是我们的训练数据。目标是使用训练数据来训练我们的机器,以便当它看到一封以前从未见过的新电子邮件时,能够告诉我们它是否是垃圾邮件。

我们如何做到这一点?通常,训练机器学习模型有几个步骤。我们将逐一介绍每个步骤,并看看如何将它们应用于我们的问题。

  • 数据准备
  • 模型训练
  • 审查模型准确性
  • 使用训练好的模型进行预测

数据准备

在数据准备步骤中,我们将探索数据以寻找潜在问题。我们将对数据进行预处理,使其适合我们的机器学习算法。在这个问题中,我们有电子邮件文本数据和相应的标签,用于分类电子邮件是否为垃圾邮件,我们需要将它们转换为数字向量,然后才能将它们输入到我们的训练模型中。

数据准备的重要性

数据准备是一个非常重要的步骤。在许多问题中,您可能最终将大部分精力都花在这个步骤上。

引用

你吃什么,你就变成什么。

您的机器学习模型的好坏取决于您用来训练它的数据。举个例子来说明这一点。假设我们正在训练一个机器学习模型来识别图像中的苹果和橙子。我们已经收集了许多不同形状和大小的苹果和橙子的图像作为训练数据。在训练过程中,我们会将这些图像展示给机器,并告诉它这是苹果还是橙子。机器学习模型应该能识别出橙子和苹果的模式。当我们之后向它展示苹果或橙子的图像时,它应该能正确分类。现在,如果我们在标记过程中犯了一个错误,导致所有图像标签都完全相反,即橙子图像被标记为苹果,苹果图像被标记为橙子。如果我们用这些图像训练我们的机器会发生什么?模型预测也将完全相反!

前面的例子有点牵强,但它简单明了地说明了坏数据对机器学习模型可能造成的危害。实际上,坏数据存在的方式多种多样。有些像前面的例子一样容易发现和解决,而另一些则更难处理。我们至少应该理解拥有好数据的重要性。我们应该尽力消除任何偏差,并用良好、干净的训练数据来训练模型。

在文章后面,我们将在垃圾邮件分类器问题的背景下,再看几个检测潜在数据问题的例子。现在让我们回到垃圾邮件分类器的数据准备步骤。

加载并解析原始数据

我们的训练数据以文本文件形式提供。该文件包含许多带标签的样本。每个样本都在一行上,格式为:{spam_or_ham},{email_text}。第一部分是标签,用于标识电子邮件是垃圾邮件还是正常邮件(非垃圾邮件),后面是电子邮件文本。

我们将使用以下代码从文件中读取数据,并将其加载到两个列表:features 和 labels 中。features 列表中的每个项都是原始电子邮件文本。labels 列表中的每个项是 1(垃圾邮件)或 0(正常邮件),并对 features 列表中的相应项进行分类。

import random

def read_file(path):
    """
    read and return all data in a file
    """
    with open(path, 'r') as f:
        return f.read()

def load_data():
    """
    load and return the data in features and labels lists
    each item in features contains the raw email text
    each item in labels is either 1(spam) or 0(ham) and identifies corresponding item in features
    """
    # load all data from file
    data_path = "data/SpamDetectionData.txt"
    all_data = read_file(data_path)
    
    # split the data into lines, each line is a single sample
    all_lines = all_data.split('\n')

    # each line in the file is a sample and has the following format
    # it begins with either "Spam," or "Ham,", and follows by the actual text of the email
    # e.g. Spam,<p>His honeyed and land....
    
    # extract the feature (email text) and label (spam or ham) from each line
    features = []
    labels = []
    for line in all_lines:
        if line[0:4] == 'Spam':
            labels.append(1)
            features.append(line[5:])
            pass
        elif line[0:3] == 'Ham':
            labels.append(0)
            features.append(line[4:])
            pass
        else:
            # ignore markers, empty lines and other lines that aren't valid sample
            # print('ignore: "{}"'.format(line));
            pass
    
    return features, labels

数据探索

在数据探索阶段,我们将分析数据以发现并解决数据中存在的任何潜在问题。这里的实际实现将根据您尝试解决的机器学习问题和您拥有的训练数据类型(例如图像、音频、文本)而有所不同。

在垃圾邮件分类问题中,首先我想看看数据是什么样的。我使用以下代码打印一个随机样本。

print("\nPrint a random sample for inspection:")
random_idx = random.randint(0, len(labels))
print("example feature: {}".format(features[random_idx][0:]))
print("example label: {} ({})".format(labels[random_idx], \
      'spam' if labels[random_idx] else 'ham'))

这会产生以下输出:

Print a random sample for inspection:
example feature: <p>Fantastic and this there than rapping we that store all me 
that leave meaninglittle stronger grew from. Was floating front nodded shrieked 
said only stately press bust one that this oer i i discourse get obeisance. 
Of peering much. This door theeby melancholy i something peering this this sat the it the. 
Made a one came. The in and said that on saintly pondered. Perched ah 
<a href="https://www.bust.com">till</a> above door leave ...
example label: 0 (ham)

从打印的样本中,我们可以看到原始电子邮件文本包含 <p><a> 等 HTML 标签。我们还看到文本中包含许多停用词,如“we”、“this”和“and”等。这些词出现频率很高,但预测能力不强。我们将在后面的步骤中看到如何处理它们。

其次,我想看看我们有多少垃圾邮件样本和正常邮件样本。为什么?因为我们希望拥有大致平衡的样本数量。不平衡的样本会导致模型偏差。想象一下,如果我们的训练样本中 99% 是垃圾邮件,只有 1% 是非垃圾邮件。如果我们的模型只是每次都预测训练样本是垃圾邮件,它将达到 99% 的准确率。

使用以下代码打印出每个类别中的样本数量

features, labels = load_data()

print("total no. of samples: {}".format(len(labels)))
print("total no. of spam samples: {}".format(labels.count(1)))
print("total no. of ham samples: {}".format(labels.count(0)))

输出如下

total no. of samples: 2100
total no. of spam samples: 1043
total no. of ham samples: 1057

垃圾邮件样本的数量与正常邮件样本的数量大致相同。样本已经平衡,我们无需进一步处理。

将数据随机分割成训练集和测试集

在这一步中,我们将把 2100 个样本分成 2 个子集:90% 的训练子集和 10% 的测试子集。我们将使用 90% 的子集来训练我们的模型。我们将使用 10% 的子集来测试训练模型在未见过数据上的性能。请注意,Code Project 提供的原始训练数据已经将样本分为训练和测试部分。我展示这一步是为了说明当您的数据尚未分离时,通常需要做什么。

为什么我们将一部分数据作为测试数据而不是全部用于训练?毕竟,更多的训练数据对模型准确性更有利。我们这样做是因为我们需要一种方法来验证我们的模型是否真正学习了数据中的模式并具有泛化到未见过数据的能力。

请看下面的例子。假设我们正在训练一个机器学习模型来识别图像中的字母“A”,并且我们有以下训练图像:

如果我们的模型只是简单地记住这四张训练数据图像中的每一个像素,它将总能识别出这四张图像中的字母“A”。它将对训练数据获得 100% 的准确率。现在,如果您部署这个完美的模型,您会发现它在任何与它训练过的图像即使略有不同的图像上表现也非常糟糕。如果我们在测试时有未见过的数据,我们本可以更早地发现这个问题。

我使用 sklearn 的 train_test_split 函数来帮助我将数据分成 90% 的训练子集和 10% 的测试子集。该函数还会打乱数据,这样您就不仅仅是简单地将前 90% 作为训练数据,将最后 10% 作为测试数据。这非常有用,因为根据数据的收集和存储方式,它可能具有某种固有的排序顺序,而我们希望我们的测试子集能代表我们所有的数据。

以下是执行 train_test 拆分的代码。

from sklearn.model_selection import train_test_split

# load features and labels
features, labels = load_data()

# split data into training / test sets
features_train, features_test, labels_train, labels_test = train_test_split(
    features, 
    labels, 
    test_size=0.2,   # use 10% for testing
    random_state=42)

预处理数据

在这一步中,我们将把数据预处理成可以输入到机器学习训练过程中的格式。

我们稍后将训练的 MultinomialNB 分类器要求输入数据为词向量计数或 tf-idf 向量。我们需要一种方法将电子邮件文本转换为数字向量,以表示文本文档中每个词的频率。我们可以手动编写代码来完成,但这是一种常见的任务,而 sklearn 拥有使这些任务变得非常容易的模块。我们可以使用 CountVectorizer 创建词向量计数,或者使用 TfidfVectorizer 创建 tf-idf 向量。

Tf-idf 代表词频-逆文档频率。主要思想是它会给予出现频率较低的词更高的权重。

引用

在一个大型文本语料库中,某些词(例如英语中的“the”、“a”、“is”)将非常常见,因此它们对于文档的实际内容携带的有效信息很少。如果我们将直接计数数据直接输入分类器,那些非常频繁的词会掩盖那些更罕见但更有趣的词的频率。

这对我来说很有意义,而且使用起来同样简单,所以我使用了 TfidfVectorizer。下面是向量化代码

from sklearn.feature_extraction.text import TfidfVectorizer

# vectorize email text into tfidf matrix
# TfidfVectorizer converts collection of raw documents to a matrix of TF-IDF features.
# It's equivalent to CountVectorizer followed by TfidfTransformer.
vectorizer = TfidfVectorizer(
    input='content',     # input is actual text
    lowercase=True,      # convert to lower case before tokenizing
    stop_words='english' # remove stop words
)
features_train_transformed = vectorizer.fit_transform(features_train)
features_test_transformed  = vectorizer.transform(features_test

模型训练

在这一步中,我们将训练我们的模型。Sklearn 提供了许多分类算法可供选择。在这个问题中,我们使用 朴素贝叶斯 算法。由于其相对简单性,它在文本分类中很受欢迎。在 sklearn 中,朴素贝叶斯分类器在 MultinomialNB 中实现。MultinomialNB 需要输入数据为词向量计数或 tf-idf 向量,这些我们已在数据准备步骤中准备好。下面是模型训练步骤中所需的代码。您可以看到在 sklearn 中训练朴素贝叶斯分类器是多么容易。

from sklearn.naive_bayes import MultinomialNB

# train a classifier
classifier = MultinomialNB()
classifier.fit(features_train_transformed, labels_train)

就是这样!我们现在已经训练好了分类器。

审查模型准确性

在这一步中,我们将使用测试数据来评估训练模型的准确性。同样,sklearn 使其变得极其简单。我们所需要做的就是调用分类器上的 score 方法。以下是代码:

from sklearn.naive_bayes import MultinomialNB
classifier = MultinomialNB()
...
# score the classifier accuracy
print("classifier accuracy {:.2f}%".format(classifier.score
(features_test_transformed, labels_test) * 100))

这是输出

classifier accuracy 100.00%

我们的模型获得了 100% 的准确率。

使用训练好的模型进行预测

现在我们有了一个训练好的机器学习模型,我们如何使用它呢?

首先,我们需要一种在模型训练完成后将模型保存到磁盘的方法。我们将使用 Python pickle 来序列化模型并将其保存到磁盘。以下是代码:

import pickle

def save(vectorizer, classifier):
    '''
    save classifier to disk
    '''
    with open('model.pkl', 'wb') as file:
        pickle.dump((vectorizer, classifier), file)

然后,当我们需要使用模型进行预测时,我们需要一种加载模型的方法。我们将从磁盘读取保存的模型,并再次使用 Python pickle 进行反序列化。以下是代码:

def load():
    '''
    load classifier from disk
    '''
    with open('model.pkl', 'rb') as file:
      vectorizer, clf = pickle.load(file)
    return vectorizer, clf

请注意,在我们的代码中,除了分类器之外,我们还保存和加载了向量化器。当我们将模型用于预测时,我们始终需要以与训练模型时相同的方式预处理数据。我们保存和加载向量化器,以便在预测期间将其用于数据预处理。

最后,我们可以使用我们保存的分类器对新数据进行预测。以下是我们的预测代码示例:

# load previously saved classifier and vectorizer
vectorizer, classifer = load()

print('\nPerform a test')                    
email_input = ['<p>Sick sea he uses might where each sooth would by he and dear friend then. 
Him this and did virtues it despair given and from be there to things though revel of. 
Felt charms waste said below breast. Nor haply scorching scorching in sighed vile me he 
maidens maddest. Alas of deeds monks. Dote my and was sight though. Seemed her feels he 
childe which care hill.</p><p>Of her was of deigned for vexed given. A along plain. 
Pile that could can stalked made talethis to of his suffice had. Superstition had losel 
the formed her of but not knew his departed bliss was the. Riot spent only tear childe. 
Ere in a disporting more. Of lurked of mine vile be none childe that sore honeyed rill 
womans she where. She time all upon loathed to known. Seek atonement hall sore where ear. 
Ofttimes rake domestic dear the monks one thence come friends. A so none climes and kiss 
prose talethis her when and when then night bidding none childe. Will fame deemed relief 
delphis he whateer. Soon love scorching low of lone mine ee haply. Than oft lurked worse 
perchance and gild earth. Are did the losel of none would ofttimes his and. His in this 
basked such one at so was himnot native. Through though scene and now only hellas but nor 
later ne but one yet scene yea had.</p>']
email_input_transformed = vectorizer.transform(email_input)
prediction = classifer.predict(email_input_transformed)

print('The email is', 'SPAM' if prediction else 'HAM')

此代码产生了正确的输出

The email is SPAM

结论

在本文中,我们创建了一个朴素贝叶斯垃圾邮件分类器。我们研究了每个常见机器学习步骤中发生的事情,并将其应用于我们的问题。

如果您是机器学习新手,我希望本文能让这个过程不那么神秘。

历史

  • 2018年3月3日:初始版本
© . All rights reserved.