<audio id="audio" title="21丨朴素贝叶斯分类（下）：如何对文档进行分类？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ee/e3/eec0c33f4d835287d62cc89d4c1e58e3.mp3"></audio>

我们上一节讲了朴素贝叶斯的工作原理，今天我们来讲下这些原理是如何指导实际业务的。

朴素贝叶斯分类最适合的场景就是文本分类、情感分析和垃圾邮件识别。其中情感分析和垃圾邮件识别都是通过文本来进行判断。从这里你能看出来，这三个场景本质上都是文本分类，这也是朴素贝叶斯最擅长的地方。所以朴素贝叶斯也常用于自然语言处理NLP的工具。

今天我带你一起使用朴素贝叶斯做下文档分类的项目，最重要的工具就是sklearn这个机器学习神器。

## sklearn机器学习包

sklearn的全称叫Scikit-learn，它给我们提供了3个朴素贝叶斯分类算法，分别是高斯朴素贝叶斯（GaussianNB）、多项式朴素贝叶斯（MultinomialNB）和伯努利朴素贝叶斯（BernoulliNB）。

这三种算法适合应用在不同的场景下，我们应该根据特征变量的不同选择不同的算法：

**高斯朴素贝叶斯**：特征变量是连续变量，符合高斯分布，比如说人的身高，物体的长度。

**多项式朴素贝叶斯**：特征变量是离散变量，符合多项分布，在文档分类中特征变量体现在一个单词出现的次数，或者是单词的TF-IDF值等。

**伯努利朴素贝叶斯**：特征变量是布尔变量，符合0/1分布，在文档分类中特征是单词是否出现。

伯努利朴素贝叶斯是以文件为粒度，如果该单词在某文件中出现了即为1，否则为0。而多项式朴素贝叶斯是以单词为粒度，会计算在某个文件中的具体次数。而高斯朴素贝叶斯适合处理特征变量是连续变量，且符合正态分布（高斯分布）的情况。比如身高、体重这种自然界的现象就比较适合用高斯朴素贝叶斯来处理。而文本分类是使用多项式朴素贝叶斯或者伯努利朴素贝叶斯。

## 什么是TF-IDF值呢？

我在多项式朴素贝叶斯中提到了“词的TF-IDF值”，如何理解这个概念呢？

TF-IDF是一个统计方法，用来评估某个词语对于一个文件集或文档库中的其中一份文件的重要程度。

TF-IDF实际上是两个词组Term Frequency和Inverse Document Frequency的总称，两者缩写为TF和IDF，分别代表了词频和逆向文档频率。

**词频TF**计算了一个单词在文档中出现的次数，它认为一个单词的重要性和它在文档中出现的次数呈正比。

**逆向文档频率IDF**，是指一个单词在文档中的区分度。它认为一个单词出现在的文档数越少，就越能通过这个单词把该文档和其他文档区分开。IDF越大就代表该单词的区分度越大。

**所以TF-IDF实际上是词频TF和逆向文档频率IDF的乘积**。这样我们倾向于找到TF和IDF取值都高的单词作为区分，即这个单词在一个文档中出现的次数多，同时又很少出现在其他文档中。这样的单词适合用于分类。

## TF-IDF如何计算

首先我们看下词频TF和逆向文档概率IDF的公式。

<img src="https://static001.geekbang.org/resource/image/bc/4d/bc31ff1f31f9cd26144404221f705d4d.png" alt="">

<img src="https://static001.geekbang.org/resource/image/b7/65/b7ad53560f61407e6964e7436da14365.png" alt="">

为什么IDF的分母中，单词出现的文档数要加1呢？因为有些单词可能不会存在文档中，为了避免分母为0，统一给单词出现的文档数都加1。

**TF-IDF=TF*IDF。**

你可以看到，TF-IDF值就是TF与IDF的乘积,这样可以更准确地对文档进行分类。比如“我”这样的高频单词，虽然TF词频高，但是IDF值很低，整体的TF-IDF也不高。

我在这里举个例子。假设一个文件夹里一共有10篇文档，其中一篇文档有1000个单词，“this”这个单词出现20次，“bayes”出现了5次。“this”在所有文档中均出现过，而“bayes”只在2篇文档中出现过。我们来计算一下这两个词语的TF-IDF值。

针对“this”，计算TF-IDF值：

<img src="https://static001.geekbang.org/resource/image/63/12/63abe3ce8aa0ea4a78ba537b5504df12.png" alt="">

<img src="https://static001.geekbang.org/resource/image/b5/7e/b5ac88c4e2a71cc2d4ceef4c01e0ba7e.png" alt="">

所以TF-IDF=0.02*(-0.0414)=-8.28e-4。

针对“bayes”，计算TF-IDF值：

<img src="https://static001.geekbang.org/resource/image/3b/8d/3bbe56a7b76513604bfe6b39b890dd8d.png" alt="">

<img src="https://static001.geekbang.org/resource/image/1e/2e/1e8b7465b9949fe071e95aede172a52e.png" alt="">

TF-IDF=0.005*0.5229=2.61e-3。

很明显“bayes”的TF-IDF值要大于“this”的TF-IDF值。这就说明用“bayes”这个单词做区分比单词“this”要好。

**如何求TF-IDF**

在sklearn中我们直接使用TfidfVectorizer类，它可以帮我们计算单词TF-IDF向量的值。在这个类中，取sklearn计算的对数log时，底数是e，不是10。

下面我来讲下如何创建TfidfVectorizer类。

## TfidfVectorizer类的创建：

创建TfidfVectorizer的方法是：

```
TfidfVectorizer(stop_words=stop_words, token_pattern=token_pattern)

```

我们在创建的时候，有两个构造参数，可以自定义停用词stop_words和规律规则token_pattern。需要注意的是传递的数据结构，停用词stop_words是一个列表List类型，而过滤规则token_pattern是正则表达式。

什么是停用词？停用词就是在分类中没有用的词，这些词一般词频TF高，但是IDF很低，起不到分类的作用。为了节省空间和计算时间，我们把这些词作为停用词stop words，告诉机器这些词不需要帮我计算。

<img src="https://static001.geekbang.org/resource/image/04/e9/040723cc99b36e8ad7e45aa31e0690e9.png" alt=""><br>
当我们创建好TF-IDF向量类型时，可以用fit_transform帮我们计算，返回给我们文本矩阵，该矩阵表示了每个单词在每个文档中的TF-IDF值。

<img src="https://static001.geekbang.org/resource/image/0d/43/0d2263fbc97beb520680382f08656b43.png" alt=""><br>
在我们进行fit_transform拟合模型后，我们可以得到更多的TF-IDF向量属性，比如，我们可以得到词汇的对应关系（字典类型）和向量的IDF值，当然也可以获取设置的停用词stop_words。

<img src="https://static001.geekbang.org/resource/image/a4/6b/a42780a5bca0531e75a294b4e2fe356b.png" alt=""><br>
举个例子，假设我们有4个文档：

文档1：this is the bayes document；

文档2：this is the second second document；

文档3：and the third one；

文档4：is this the document。

现在想要计算文档里都有哪些单词，这些单词在不同文档中的TF-IDF值是多少呢？

首先我们创建TfidfVectorizer类：

```
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vec = TfidfVectorizer()

```

然后我们创建4个文档的列表documents，并让创建好的tfidf_vec对documents进行拟合，得到TF-IDF矩阵：

```
documents = [
    'this is the bayes document',
    'this is the second second document',
    'and the third one',
    'is this the document'
]
tfidf_matrix = tfidf_vec.fit_transform(documents)

```

输出文档中所有不重复的词：

```
print('不重复的词:', tfidf_vec.get_feature_names())

```

运行结果

```
不重复的词: ['and', 'bayes', 'document', 'is', 'one', 'second', 'the', 'third', 'this']

```

输出每个单词对应的id值：

```
print('每个单词的ID:', tfidf_vec.vocabulary_)

```

运行结果

```
每个单词的ID: {'this': 8, 'is': 3, 'the': 6, 'bayes': 1, 'document': 2, 'second': 5, 'and': 0, 'third': 7, 'one': 4}

```

输出每个单词在每个文档中的TF-IDF值，向量里的顺序是按照词语的id顺序来的：

```
print('每个单词的tfidf值:', tfidf_matrix.toarray())

```

运行结果：

```
每个单词的tfidf值: [[0.         0.63314609 0.40412895 0.40412895 0.         0.
  0.33040189 0.         0.40412895]
 [0.         0.         0.27230147 0.27230147 0.         0.85322574
  0.22262429 0.         0.27230147]
 [0.55280532 0.         0.         0.         0.55280532 0.
  0.28847675 0.55280532 0.        ]
 [0.         0.         0.52210862 0.52210862 0.         0.
  0.42685801 0.         0.52210862]]

```

## 如何对文档进行分类

如果我们要对文档进行分类，有两个重要的阶段：

<img src="https://static001.geekbang.org/resource/image/25/c3/257e01f173e8bc78b37b71b2358ff7c3.jpg" alt="">

<li>
**基于分词的数据准备**，包括分词、单词权重计算、去掉停用词；
</li>
<li>
**应用朴素贝叶斯分类进行分类**，首先通过训练集得到朴素贝叶斯分类器，然后将分类器应用于测试集，并与实际结果做对比，最终得到测试集的分类准确率。
</li>

下面，我分别对这些模块进行介绍。

**模块1：对文档进行分词**

在准备阶段里，最重要的就是分词。那么如果给文档进行分词呢？英文文档和中文文档所使用的分词工具不同。

在英文文档中，最常用的是NTLK包。NTLK包中包含了英文的停用词stop words、分词和标注方法。

```
import nltk
word_list = nltk.word_tokenize(text) #分词
nltk.pos_tag(word_list) #标注单词的词性

```

在中文文档中，最常用的是jieba包。jieba包中包含了中文的停用词stop words和分词方法。

```
import jieba
word_list = jieba.cut (text) #中文分词

```

**模块2：加载停用词表**

我们需要自己读取停用词表文件，从网上可以找到中文常用的停用词保存在stop_words.txt，然后利用Python的文件读取函数读取文件，保存在stop_words数组中。

```
stop_words = [line.strip().decode('utf-8') for line in io.open('stop_words.txt').readlines()]

```

**模块3：计算单词的权重**

这里我们用到sklearn里的TfidfVectorizer类，上面我们介绍过它使用的方法。

直接创建TfidfVectorizer类，然后使用fit_transform方法进行拟合，得到TF-IDF特征空间features，你可以理解为选出来的分词就是特征。我们计算这些特征在文档上的特征向量，得到特征空间features。

```
tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5)
features = tf.fit_transform(train_contents)

```

这里max_df参数用来描述单词在文档中的最高出现率。假设max_df=0.5，代表一个单词在50%的文档中都出现过了，那么它只携带了非常少的信息，因此就不作为分词统计。

一般很少设置min_df，因为min_df通常都会很小。

**模块4：生成朴素贝叶斯分类器**

我们将特征训练集的特征空间train_features，以及训练集对应的分类train_labels传递给贝叶斯分类器clf，它会自动生成一个符合特征空间和对应分类的分类器。

这里我们采用的是多项式贝叶斯分类器，其中alpha为平滑参数。为什么要使用平滑呢？因为如果一个单词在训练样本中没有出现，这个单词的概率就会被计算为0。但训练集样本只是整体的抽样情况，我们不能因为一个事件没有观察到，就认为整个事件的概率为0。为了解决这个问题，我们需要做平滑处理。

当alpha=1时，使用的是Laplace平滑。Laplace平滑就是采用加1的方式，来统计没有出现过的单词的概率。这样当训练样本很大的时候，加1得到的概率变化可以忽略不计，也同时避免了零概率的问题。

当0&lt;alpha&lt;1时，使用的是Lidstone平滑。对于Lidstone平滑来说，alpha 越小，迭代次数越多，精度越高。我们可以设置alpha为0.001。

```
# 多项式贝叶斯分类器
from sklearn.naive_bayes import MultinomialNB  
clf = MultinomialNB(alpha=0.001).fit(train_features, train_labels)

```

**模块5：使用生成的分类器做预测**

首先我们需要得到测试集的特征矩阵。

方法是用训练集的分词创建一个TfidfVectorizer类，使用同样的stop_words和max_df，然后用这个TfidfVectorizer类对测试集的内容进行fit_transform拟合，得到测试集的特征矩阵test_features。

```
test_tf = TfidfVectorizer(stop_words=stop_words, max_df=0.5, vocabulary=train_vocabulary)
test_features=test_tf.fit_transform(test_contents)

```

然后我们用训练好的分类器对新数据做预测。

方法是使用predict函数，传入测试集的特征矩阵test_features，得到分类结果predicted_labels。predict函数做的工作就是求解所有后验概率并找出最大的那个。

```
predicted_labels=clf.predict(test_features)

```

**模块6：计算准确率**

计算准确率实际上是对分类模型的评估。我们可以调用sklearn中的metrics包，在metrics中提供了accuracy_score函数，方便我们对实际结果和预测的结果做对比，给出模型的准确率。

使用方法如下：

```
from sklearn import metrics
print metrics.accuracy_score(test_labels, predicted_labels)

```

## 数据挖掘神器sklearn

从数据挖掘的流程来看，一般包括了获取数据、数据清洗、模型训练、模型评估和模型部署这几个过程。

sklearn中包含了大量的数据挖掘算法，比如三种朴素贝叶斯算法，我们只需要了解不同算法的适用条件，以及创建时所需的参数，就可以用模型帮我们进行训练。在模型评估中，sklearn提供了metrics包，帮我们对预测结果与实际结果进行评估。

在文档分类的项目中，我们针对文档的特点，给出了基于分词的准备流程。一般来说NTLK包适用于英文文档，而jieba适用于中文文档。我们可以根据文档选择不同的包，对文档提取分词。这些分词就是贝叶斯分类中最重要的特征属性。基于这些分词，我们得到分词的权重，即特征矩阵。

通过特征矩阵与分类结果，我们就可以创建出朴素贝叶斯分类器，然后用分类器进行预测，最后预测结果与实际结果做对比即可以得到分类器在测试集上的准确率。

<img src="https://static001.geekbang.org/resource/image/2e/6e/2e2962ddb7e85a71e0cecb9c6d13306e.png" alt="">

## 练习题

我已经讲了中文文档分类中的6个关键的模块，最后，我给你留一道对中文文档分类的练习题吧。

我将中文文档数据集上传到了GitHub上，[点击这里下载](https://github.com/cystanford/text_classification)。

数据说明：

1. 文档共有4种类型：女性、体育、文学、校园；

<img src="https://static001.geekbang.org/resource/image/67/28/67abc1783f7c4e7cd69194fafc514328.png" alt="">

1. 训练集放到train文件夹里，测试集放到test文件夹里，停用词放到stop文件夹里。

<img src="https://static001.geekbang.org/resource/image/0c/0f/0c374e3501cc28a24687bc030733050f.png" alt=""><br>
请使用朴素贝叶斯分类对训练集进行训练，并对测试集进行验证，并给出测试集的准确率。

最后你不妨思考一下，假设我们要判断一个人的性别，是通过身高、体重、鞋码、外貌等属性进行判断的，如果我们用朴素贝叶斯做分类，适合使用哪种朴素贝叶斯分类器？停用词的作用又是什么？

欢迎你在评论区进行留言，与我分享你的答案。也欢迎点击“请朋友读”，把这篇文章分享给你的朋友或者同事。


