利用朴素贝叶斯算法进行文档分类

本文利用 scikit-learn 里的朴素贝叶斯算法对文档进行分类,以便更深入地理解贝叶斯算法。本文的侧重点不是自然语言处理,所以语料库直接使用英文,以避免介绍中文分词技术。为了读者更好的理解原理,本文介绍了 TF-IDF ,这是一个表达词语权重信息的模型。

在 scikit-learn 里,朴素贝叶斯算法在 sklearn.naive_bayes 包里实现,包含了本章介绍的几种典型的概率分布算法。其中 GaussianNB 实现了高斯分布的朴素贝叶斯算法,MultinomialNB 实现了多项式分布的朴素贝叶斯算法,BernoulliNB 实现了伯努利分布的朴素贝叶斯算法。本文我们用 MultinomialNB 来实现文档自动分类。如果你不熟悉朴素贝叶斯算法,可参阅笔者的另外一篇博客零基础学习朴素贝叶斯算法

1 获取数据集

本节使用的数据集来自 mlcomp.org 上的 20news-18828,免费注册后即可下载。下载完数据集后,可以解压解压到 ~/code/datasets/mlcomp/ 目录下,解压后会在 ~/code/datasets/mlcomp 下生成一个名为 379 的目录,其目录下包含三个子目录和一个名为 metadata 的介绍文件:

$ cd ~/code/datasets/mlcomp
$ ls 379
metadata  raw  test  train

我们将使用 train 子目录下的文档进行模型训练,然后使用 test 子目录下的文档进行模型测试。train 子目录下包含 20 个子目录,每个子目录代表一种文档的类型,子目录下的所有文档都是属于目录名称所标识的文档类型。读者朋友可以随意浏览数据集,以便对数据集有一个感性的认识。比如 datasets/mlcomp/379/train/rec.autos/6652-103421 ,这是一个纯文本文件,可以使用任何文本编辑器打开。这是一个讨论汽车主题的帖子:

Hahahahahaha. gasp pant Hm, I'm not sure whether the above
was just a silly remark or a serious remark. But in case there are
some misconceptions, I think Henry Robertson hasn't updated his data
file on Korea since...mid 1970s. Owning a car in Korea is no longer
a luxury. Most middle class people in Korea can afford a car and do
have at least one car. The problem in Korea, especially in Seoul, is
that there are just so many privately-owned cars, as well as taxis and
buses, the rush-hour has become a 24 hour phenomenon and that there is
no place to park. Last time I heard, back in January, the Kim Administration
wanted to legislate a law requireing a potential car owner to provide
his or her own parking area, just like they do in Japan.

Also, Henry would be glad to know that Hyundai isn't the only
car manufacturer in Korea. Daewoo has always manufactured cars and
I believe Kia is back in business as well. Imported cars, such as
Mercury Sable are becoming quite popular as well, though they are still
quite expensive.

Finally, please ignore Henry's posting about Korean politics
and bureaucracy. He's quite uninformed.

2 文档的数学表达

怎么样把一个文档表达为计算机可以理解并处理的信息?这是自然语言处理里的一个重要课题,完整的内容可以写成鸿篇巨著。本节简单介绍 TF-IDF 的原理,以便读者更好地理解本文介绍的实例。

TF-IDF 是一种统计方法,用以评估一个词语对于一份文档的重要程度。TF 表示词频 (Term Frequency),对一份文档而言,词频为特定词语在这篇文档里出现的次数除以文档的词语总数。比如一篇文档总共有 1000 个词,其中 “朴素贝叶斯” 出现了 5 次,“的” 出现了 25 次,“应用” 出现了 12 次,那么它们的词频分别是 0.005, 0.025, 0.012。

IDF 表示一个词的逆向文档频率指数 (Inverse Document Frequency) ,可以由总文档数目除以包含该词语的文档的数目,再将得到的商取对数得到,它表达的是词语的权重指数。比如,我们的数据集总共有 10000 篇文档,其中 “朴素贝叶斯” 只出现在 10 篇文档中,则其权重指数 IDF = log(10000 / 10) = 3 。“的” 在所有的文档中都出现,则其权重指数 IDF = log(1) = 0 。“应用” 在 1000 篇文档中出现,则其权重指数 IDF = log(10000 / 1000) = 1 。

计算出每个词的词频和权重指数后,两者相乘,即可得到这个词在文档中的重要程度。词语的重要性随着它在文档中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。关于 TF-IDF 在搜索引擎上的应用,可参阅吴军老师的《数学之美》里的《如何确定网页和查询的相关性》一文。

有了 TF-IDF 这个工具,我们就可以把一篇文档转换为一个向量。首先,可以从我们的数据集(在自然语言处理领域,也称为 corpus ,即语料库)里提取出所有出现的词语,我们称为词典。假设词典里总共有 10000 个词语,则每个文档都可转化为一个 10000 维的向量。其次,针对我们要转换的文档里出现的每个词语,都去计算其 TF-IDF 的值,并把这个值填入文档向量里,这个词所对应的元素上。这样就完成了把一篇文档转换为一个向量的过程。一个文档往往只会由词典里的一小部分词语构成,这就意味着这个这个向量里大部分元素都是零。

所幸,上述过程我们不需要自己写代码完成,scikit-learn 软件包里实现了把文档转换为向量的过程。首先,我们把训练用的语料库读入内存:

from time import time
from sklearn.datasets import load_files

print("loading train dataset ...")
t = time()
news_train = load_files('datasets/mlcomp/379/train')
print("summary: {0} documents in {1} categories.".format(
    len(news_train.data), len(news_train.target_names)))
print("done in {0} seconds".format(time() - t))

我们的代码保存在 ~/code/ 目录下,其相对路径 datasets/mlcomp/379/train 目录下放的就是我们的语料库,其中包含 20 个子目录,每个子目录的名字表示的是文档的类别,子目录下包含这种类别的所有文档。load_files() 函数会从这个目录里把所有的文档都读入内存,并且自动根据所在的子目录名称打上标签。其中,news_train.data 是一个数组,里面包含了所有文档的文本信息。news_train.target 也是一个数组,包含了所有文档所属的类别,而 news_train.target_names 则是的类别的名称,因此,如果我们想知道第一篇文档所属的类别名称,只需要通过代码 news_train.target_names[news_train.target[0]] 即可得到。

上述代码在笔者电脑上的输出是:

loading train dataset ...
summary: 13180 documents in 20 categories.
done in 0.212177991867 seconds

不难看到,我们的语料库里,总共有 13180 个文档,其中分成 20 个类别。接着,我们需要把这些文档全部转换为由 TF-IDF 表达的权重信息构成的向量:

from sklearn.feature_extraction.text import TfidfVectorizer

print("vectorizing train dataset ...")
t = time()
vectorizer = TfidfVectorizer(encoding='latin-1')
X_train = vectorizer.fit_transform((d for d in news_train.data))
print("n_samples: %d, n_features: %d" % X_train.shape)
print("number of non-zero features in sample [{0}]: {1}".format(
    news_train.filenames[0], X_train[0].getnnz()))
print("done in {0} seconds".format(time() - t))

其中,TfidfVectorizer 类是用来把所有的文档转换为矩阵,该矩阵每行都代表一个文档,一行中的每个元素代表一个对应的词语的重要性,词语的重要性由 TF-IDF 来表示。熟悉 scikit-learn API 的读者应该清楚,其 fit_transform() 方法是 fit()transform() 合并起来。其中,fit() 会先完成语料库分析,提取词典等操作,transform() 会把对每篇文档转换为向量,最终构成一个矩阵,保存在 X_train 变量里。这段代码在笔者电脑上的输出为:

vectorizing train dataset ...
n_samples: 13180, n_features: 130274
number of non-zero features in sample
  [datasets/mlcomp/379/train/talk.politics.misc/17860-178992]: 108
done in 4.15024495125 seconds

由程序的输出,我们可以知道,我们的词典总共有 130274 个词语,即每篇文档都可转换为一个 130274 维的向量(这是一个巨大的向量)。第一篇文档中,只有 108 个非零元素,即这篇文章由 108 个单词组成,其单词数大于等于 108 个(因为有些词可出现多次)。在这篇文档中出现的这 108 个单词的 TF-IDF 值会被计算出来,并保存在向量中的指定位置上。X_train 是一个维度为 13180 x 130274 的稀疏矩阵(这是一个巨稀疏的矩阵)。

3 模型训练

费了好些功夫,终于把文档数据转换为 scikit-learn 里典型的训练数据集矩阵:矩阵的每一行表示一个数据样本,矩阵的每一列表示一个特征。接着,我们可以直接使用 MultinomialNB 来对数据集进行训练:

from sklearn.naive_bayes import MultinomialNB

print("traning models ...".format(time() - t))
t = time()
y_train = news_train.target
clf = MultinomialNB(alpha=0.0001)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
print("train score: {0}".format(train_score))
print("done in {0} seconds".format(time() - t))

其中 alpha 表示平滑参数,其值越小,越容易造成过拟合,值太大,容易造成欠拟合。这段代码在笔者电脑上的输出为:

traning models ...
train score: 0.997875569044
done in 0.274363040924 seconds

接着,我们加载测试数据集,并拿一篇文档来预测看看是否准确。测试数据集在 ~/code/datasets/mlcomp/379/test 目录下,我们用上文介绍的相同的方法,先加载数据集:

print("loading test dataset ...")
t = time()
news_test = load_files('datasets/mlcomp/379/test')
print("summary: {0} documents in {1} categories.".format(
    len(news_test.data), len(news_test.target_names)))
print("done in {0} seconds".format(time() - t))

在笔者的电脑上的输出为:

loading test dataset ...
summary: 5648 documents in 20 categories.
done in 0.117918014526 seconds

可见,我们的测试数据集总共有 5648 篇文档。接着,我们把文档向量化:

print("vectorizing test dataset ...")
t = time()
X_test = vectorizer.transform((d for d in news_test.data))
y_test = news_test.target
print("n_samples: %d, n_features: %d" % X_test.shape)
print("number of non-zero features in sample [{0}]: {1}".format(
    news_test.filenames[0], X_test[0].getnnz()))
print("done in %fs" % (time() - t))

这里需要注意,vectorizer 变量是我们处理训练数据集时用到的广本向量化的类 TfidfVectorizer 的实例,此处我们只需要调用 transform() 进行 TF-IDF 数值计算即可,不需要再调用 fit() 进行语料库分析了。这段代码在笔者电脑上的输出为:

vectorizing test dataset ...
n_samples: 5648, n_features: 130274
number of non-zero features in sample
    [datasets/mlcomp/379/test/rec.autos/7429-103268]: 61
done in 2.915759s

这样,我们的测试数据集也转换为一个维度为 5648 x 130274 的稀疏矩阵。我们可以取测试数据集里第一篇文档初步验证一下,看看我们训练出来的模型能否正确地预测这个文档所属的类别:

pred = clf.predict(X_test[0])
print("predict: {0} is in category {1}".format(
    news_test.filenames[0], news_test.target_names[pred[0]]))
print("actually: {0} is in category {1}".format(
    news_test.filenames[0], news_test.target_names[news_test.target[0]]))

这段代码在笔者电脑上的输出为:

predict: datasets/mlcomp/379/test/rec.autos/7429-103268 is in category rec.autos
actually: datasets/mlcomp/379/test/rec.autos/7429-103268 is in category rec.autos

看来预测的和实际的是相符的。

4 模型评价

显然,我们不能通过一个样本的预测来评价模型的准确性。我们需要对模型有个全方位的评价,所幸 scikit-learn 软件包提供了全方位的模型评价工具。

首先,我们需要对测试数据集进行预测:

print("predicting test dataset ...")
t0 = time()
pred = clf.predict(X_test)
print("done in %fs" % (time() - t0))

在笔者的电脑上输出:

predicting test dataset ...
done in 0.090978s

接着,我们使用 classification_report() 函数来查看一下针对每个类别的预测准确性:

from sklearn.metrics import classification_report

print("classification report on test set for classifier:")
print(clf)
print(classification_report(y_test, pred,
                            target_names=news_test.target_names))

在笔者电脑上的输出为:

classification report on test set for classifier:
MultinomialNB(alpha=0.0001, class_prior=None, fit_prior=True)
                          precision    recall  f1-score   support

             alt.atheism       0.90      0.91      0.91       245
           comp.graphics       0.80      0.90      0.85       298
 comp.os.ms-windows.misc       0.82      0.79      0.80       292
comp.sys.ibm.pc.hardware       0.81      0.80      0.81       301
   comp.sys.mac.hardware       0.90      0.91      0.91       256
          comp.windows.x       0.88      0.88      0.88       297
            misc.forsale       0.87      0.81      0.84       290
               rec.autos       0.92      0.93      0.92       324
         rec.motorcycles       0.96      0.96      0.96       294
      rec.sport.baseball       0.97      0.94      0.96       315
        rec.sport.hockey       0.96      0.99      0.98       302
               sci.crypt       0.95      0.96      0.95       297
         sci.electronics       0.91      0.85      0.88       313
                 sci.med       0.96      0.96      0.96       277
               sci.space       0.94      0.97      0.96       305
  soc.religion.christian       0.93      0.96      0.94       293
      talk.politics.guns       0.91      0.96      0.93       246
   talk.politics.mideast       0.96      0.98      0.97       296
      talk.politics.misc       0.90      0.90      0.90       236
      talk.religion.misc       0.89      0.78      0.83       171

             avg / total       0.91      0.91      0.91      5648

从输出可以看出来,针对每种类别,都统计了查准率,召回率和 F1-Score。对这些概念不熟悉的读者可参阅笔者的另外一篇博客机器学习系统的设计与调优。此外,我们还可以通过 confusion_matrix() 函数,来生成混淆矩阵,观察每种类别被错误分类的情况,比如,这些被错误分类的文档是被错误分类到哪些类别里的:

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_test, pred)
print("confusion matrix:")
print(cm)

在笔者电脑上的输出为:

confusion matrix:
[224   0   0   0   0   0   0   0   0   0   0   0   0   0   2   5   0   0   1  13]
[ 1 267   5   5   2   8   1   1   0   0   0   2   3   2   1   0   0   0   0   0]
[ 1  13 230  24   4  10   5   0   0   0   0   1   2   1   0   0   0   0   1   0]
[ 0   9  21 242   7   2  10   1   0   0   1   1   7   0   0   0   0   0   0   0]
[ 0   1   5   5 233   2   2   2   1   0   0   3   1   0   1   0   0   0   0   0]
[ 0  20   6   3   1 260   0   0   0   2   0   1   0   0   2   0   2   0   0   0]
[ 0   2   5  12   3   1 235  10   2   3   1   0   7   0   2   0   2   1   4   0]
[ 0   1   0   0   1   0   8 300   4   1   0   0   1   2   3   0   2   0   1   0]
[ 0   1   0   0   0   2   2   3 283   0   0   0   1   0   0   0   0   0   1   1]
[ 0   1   1   0   1   2   1   2   0 297   8   1   0   1   0   0   0   0   0   0]
[ 0   0   0   0   0   0   0   0   2   2 298   0   0   0   0   0   0   0   0   0]
[ 0   1   2   0   0   1   1   0   0   0   0 284   2   1   0   0   2   1   2   0]
[ 0  11   3   5   4   2   4   5   1   1   0   4 266   1   4   0   1   0   1   0]
[ 1   1   0   1   0   2   1   0   0   0   0   0   1 266   2   1   0   0   1   0]
[ 0   3   0   0   1   1   0   0   0   0   0   1   0   1 296   0   1   0   1   0]
[ 3   1   0   1   0   0   0   0   0   0   1   0   0   2   1 280   0   1   1   2]
[ 1   0   2   0   0   0   0   0   1   0   0   0   0   0   0   0 236   1   4   1]
[ 1   0   0   0   0   1   0   0   0   0   0   0   0   0   0   3   0 290   1   0]
[ 2   1   0   0   1   1   0   1   0   0   0   0   0   0   0   1  10   7 212   0]
[16   0   0   0   0   0   0   0   0   0   0   0   0   0   0  12   4   1   4 134]]

从第一行数据可以看出来,类别 0 (alt.atheism) 的文档,有 13 个文档被错误地分类到类别 19 (talk.religion.misc) 里。当然,我们还可以把混淆矩阵进行数据可视化处理:

# Show confusion matrix
import matplotlib.pyplot as plt

plt.figure(figsize=(8, 8), dpi=144)
plt.title('Confusion matrix of the classifier')
ax = plt.gca()
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.spines['bottom'].set_color('none')
ax.spines['left'].set_color('none')
ax.xaxis.set_ticks_position('none')
ax.yaxis.set_ticks_position('none')
ax.set_xticklabels([])
ax.set_yticklabels([])
plt.matshow(cm, fignum=1, cmap='gray')
plt.colorbar()
plt.show()

在笔者电脑上的输出如下图所示:

混淆矩阵

除对角线外,其他地方颜色越浅,说明此处错误越多。通过这些数据,我们可以详细分析样本数据,找出为什么某种类别会被错误地分类到另外一种类别里,从而进一步优化模型。

5 参数选择

MultinomialNB 有一个重要的参数是 alpha ,用来控制模型拟合时的平滑度。我们选择了 0.0001 这个值。实际上,有一个更科学的方法,是利用 scikit-learn 的 sklearn.model_selection.GridSearchCV 来进行自动选择。即我们可以设置一个 alpha 参数的范围,然后让用代码选择出一个在这个范围内最优的参数值。感兴趣的朋友可以阅读 GridSearchCV 的文档偿试一下。

(完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容