朴素贝叶斯 - 贝叶斯分类器做文本分类(五)

449 阅读9分钟

根据菜菜的课程进行整理,方便记忆理解

代码位置如下:

案例:贝叶斯分类器做文本分类

文本分类是现代机器学习应用中的一大模块,更是自然语言处理的基础之一。我们可以通过将文字数据处理成数字数据,然后使用贝叶斯来帮助我们判断一段话,或者一篇文章中的主题分类,感情倾向,甚至文章体裁。现在,绝大多数社交媒体数据的自动化采集,都是依靠首先将文本编码成数字,然后按分类结果采集需要的信息。虽然现在自然语言处理领域大部分由深度学习所控制,贝叶斯分类器依然是文本分类中的一颗明珠。现在,我们就来学习一下,贝叶斯分类器是怎样实现文本分类的。

文本编码技术简介

单词计数向量

在开始分类之前,我们必须先将文本编码成数字。一种常用的方法是单词计数向量。在这种技术中,一个样本可以包含一段话或一篇文章,这个样本中如果出现了10个单词,就会有10个特征(n=10),每个特征代表一个单词,特征的取值表示这个单词在这个样本中总共出现了几次,是一个离散的,代表次数的,正整数。

在sklearn当中,单词计数向量计数可以通过feature_extraction.text模块中的CountVectorizer类实现,来看一个简单的例子:

sample = ["Machine learning is fascinating, it is wonderful"
          ,"Machine learning is a sensational techonology"
          ,"Elsa is a popular character"]
from sklearn.feature_extraction.text import CountVectorizer
vec = CountVectorizer()
X = vec.fit_transform(sample)
X
"""
<3x11 sparse matrix of type '<class 'numpy.int64'>'
	with 15 stored elements in Compressed Sparse Row format>
"""

#使用接口get_feature_names()调用每个列的名称

vec.get_feature_names() #按照字母的顺序排列
"""
['character',
 'elsa',
 'fascinating',
 'is',
 'it',
 'learning',
 'machine',
 'popular',
 'sensational',
 'techonology',
 'wonderful']
"""

import pandas as pd
#注意稀疏矩阵是无法输入pandas的
CVresult = pd.DataFrame(X.toarray(),columns = vec.get_feature_names())
CVresult

image.png

从这个编码结果,我们可以发现两个问题。

首先,来回忆一下我们多项式朴素贝叶斯的计算公式

image.png

如果我们将每一列加和,除以整个特征矩阵的和,就是每一列对应的概率θi\theta_i。由于是将xjix_{ji}进行加和,对于一个在很多个特征下都有值的样本来说,这个样本在对θci\theta_{ci}的贡献就会比其他的样本更大。对于句子特别长的样本而言,这个样本θi\theta_i对的影响是巨大的。因此补集朴素贝叶斯让每个特征的权重除以自己的L2范式,就是为了避免这种情况发生。

第二个问题,观察我们的矩阵,会发现"is"这个单词出现了四次,那经过计算,这个单词出现的概率就会最大,但其实它对我们的语义并没有什么影响(除非我们希望判断的是,文章描述的是过去的事件还是现在发生的事件)。可以遇见,如果使用单词计数向量,可能会导致一部分常用词(比如中文中的”的“)频繁出现在我们的矩阵中并且占有很高的权重,对分类来说,这明显是对算法的一种误导。为了解决这个问题,比起使用次数,我们使用单词在句子中所占的比例来编码我们的单词,这就是我们著名的TF-IDF方法。

TF-IDF

TF-IDF全称term frequency-inverse document frequency,词频逆文档频率,是通过单词在文档中出现的频率来衡量其权重,也就是说,IDF的大小与一个词的常见程度成反比,这个词越常见,编码后为它设置的权重会倾向于越小,以此来压制频繁出现的一些无意义的词。在sklearn当中,我们使用feature_extraction.text中类TfidfVectorizer来执行这种编码。

from sklearn.feature_extraction.text import TfidfVectorizer as TFIDF
vec = TFIDF()

X = vec.fit_transform(sample)
X #每一个单词作为一个特征,每个单词在这个句子中所占的比例
"""
<3x11 sparse matrix of type '<class 'numpy.float64'>'
	with 15 stored elements in Compressed Sparse Row format>
"""

#同样使用接口get_feature_names()调用每个列的名称
TFIDFresult = pd.DataFrame(X.toarray(),columns=vec.get_feature_names())

TFIDFresult

image.png

#使用TF-IDF编码之后,出现得多的单词的权重被降低了么? theta
CVresult.sum(axis=0)/CVresult.sum(axis=0).sum()
"""
character      0.0625
elsa           0.0625
fascinating    0.0625
is             0.2500
it             0.0625
learning       0.1250
machine        0.1250
popular        0.0625
sensational    0.0625
techonology    0.0625
wonderful      0.0625
dtype: float64
"""

TFIDFresult.sum(axis=0) / TFIDFresult.sum(axis=0).sum()
#将原本出现次数比较多的词压缩我们的权重
"""
character      0.083071
elsa           0.083071
fascinating    0.064516
is             0.173225
it             0.064516
learning       0.110815
machine        0.110815
popular        0.083071
sensational    0.081192
techonology    0.081192
wonderful      0.064516
dtype: float64
"""

探索文本数据

在现实中,文本数据的处理是十分耗时耗力的,尤其是不规则的长文本的处理方式,绝对不是一两句话能够说明白的,因此在这里我们将使用的数据集是sklearn中自带的文本数据集fetch_20newsgroup。这个数据集是20个网络新闻组的语料库,其中包含约2万篇新闻,全部以英文显示,如果大家希望使用中文则处理过程会更加困难,会需要自己加载中文的语料库。在这个例子中,主要目的是为大家展示贝叶斯的用法和效果,因此我们就使用英文的语料库。

from sklearn.datasets import fetch_20newsgroups
#初次使用这个数据集的时候,会在实例化的时候开始下载
data = fetch_20newsgroups()

# 通常我们使用data来查看data里面到底包含了什么内容
# 但由于fetch_20newsgourps这个类加载出的数据巨大,数据结构中混杂很多文字,因此很难去看清
#不同类型的新闻
#标签的分类都有哪些
data.target_names
"""
['alt.atheism',
 'comp.graphics',
 'comp.os.ms-windows.misc',
 'comp.sys.ibm.pc.hardware',
 'comp.sys.mac.hardware',
 'comp.windows.x',
 'misc.forsale',
 'rec.autos',
 'rec.motorcycles',
 'rec.sport.baseball',
 'rec.sport.hockey',
 'sci.crypt',
 'sci.electronics',
 'sci.med',
 'sci.space',
 'soc.religion.christian',
 'talk.politics.guns',
 'talk.politics.mideast',
 'talk.politics.misc',
 'talk.religion.misc']
"""

# 其实fetch_20newsgroups也是一个类,既然是类,应该就有可以调用的参数
# 面对简单数据集,我们往往在实例化的过程中什么都不写,但是现在data中数据量太多,不方便探索
# 因此我们需要来看看我们的类fetch_20newsgroups都有什么样的参数可以帮助我们

sklearn.datasets.fetch_20newsgroups (data_home=None, subset=’train’, categories=None, shuffle=True,random_state=42, remove=(), download_if_missing=True)

在这之中,我们来认识几个比较重要的参数:

参数含义
subset选择类中包含的数据子集
输入"train"表示选择训练集,“test"表示输入测试集,”all"表示加载所有的数据
categories可输入None或者数据所在的目录
选择一个子集下,不同类型或不同内容的数据所在的目录。如果不输入默认None,则会加载全部的目录
download_if_missing可选,默认是True
如果发现本地数据不全,是否自动进行下载
shuffle布尔值,可不填,表示是否打乱样本顺序
对于假设样本之间互相独立并且服从相同分布的算法或模型(比如随机梯度下降)来说可能很重要

现在我们就可以直接通过参数来提取我们希望得到的数据集了

import numpy as np
import pandas as pd

categories = ["sci.space" #科学技术 - 太空
              ,"rec.sport.hockey" #运动 - 曲棍球
              ,"talk.politics.guns" #政治 - 枪支问题
              ,"talk.politics.mideast"] #政治 - 中东问题

train = fetch_20newsgroups(subset="train",categories = categories)
test = fetch_20newsgroups(subset="test",categories = categories)

train
#可以观察到,里面依然是类字典结构,我们可以通过使用键的方式来提取内容

image.png

train.target_names #四个类别,四个目录下的标签的分类
"""
['rec.sport.hockey',
 'sci.space',
 'talk.politics.guns',
 'talk.politics.mideast']
"""

#查看总共有多少篇文章存在
len(train.data)
# 2303

#随意提取一篇文章来看看
print(train.data[0])

image.png

#查看一下我们的标签
np.unique(train.target)
# array([0, 1, 2, 3], dtype=int64)

len(train.target)
# 2303

#是否存在样本不平衡问题?
for i in [1,2,3]:
    print(i,(train.target == i).sum()/len(train.target))
"""
1 0.25749023013460703
2 0.23708206686930092
3 0.24489795918367346
"""

使用TF-IDF将文本数据编码

from sklearn.feature_extraction.text import TfidfVectorizer as TFIDF
Xtrain = train.data
Xtest = test.data
Ytrain = train.target
Ytest = test.target

tfidf = TFIDF().fit(Xtrain)
Xtrain_ = tfidf.transform(Xtrain)
Xtest_ = tfidf.transform(Xtest)

Xtrain_
"""
<2303x40725 sparse matrix of type '<class 'numpy.float64'>'
	with 430306 stored elements in Compressed Sparse Row format>
"""

tosee = pd.DataFrame(Xtrain_.toarray(),columns=tfidf.get_feature_names())
tosee.head()

tosee.shape
# (2303, 40725)

image.png

在贝叶斯上分别建模,查看结果

from sklearn.naive_bayes import MultinomialNB, ComplementNB, BernoulliNB
from sklearn.metrics import brier_score_loss as BS

name = ["Multinomial","Complement","Bournulli"]
#注意高斯朴素贝叶斯不接受稀疏矩阵
models = [MultinomialNB(),ComplementNB(),BernoulliNB()]

for name,clf in zip(name,models):
    clf.fit(Xtrain_,Ytrain)
    y_pred = clf.predict(Xtest_)
    proba = clf.predict_proba(Xtest_)
    score = clf.score(Xtest_,Ytest)
    print(name)
    
    #4个不同的标签取值下的布里尔分数
    Bscore = []
    for i in range(len(np.unique(Ytrain))):
        bs = BS(Ytest,proba[:,i],pos_label=i)
        Bscore.append(bs)
        print("\tBrier under {}:{:.3f}".format(train.target_names[i],bs))
        
    print("\tAverage Brier:{:.3f}".format(np.mean(Bscore)))
    print("\tAccuracy:{:.3f}".format(score))
    print("\n")
    
"""
Multinomial
	Brier under rec.sport.hockey:0.018
	Brier under sci.space:0.033
	Brier under talk.politics.guns:0.030
	Brier under talk.politics.mideast:0.026
	Average Brier:0.027
	Accuracy:0.975


Complement
	Brier under rec.sport.hockey:0.023
	Brier under sci.space:0.039
	Brier under talk.politics.guns:0.039
	Brier under talk.politics.mideast:0.033
	Average Brier:0.033
	Accuracy:0.986


Bournulli
	Brier under rec.sport.hockey:0.068
	Brier under sci.space:0.025
	Brier under talk.politics.guns:0.045
	Brier under talk.politics.mideast:0.053
	Average Brier:0.048
	Accuracy:0.902
"""

从结果上来看,两种贝叶斯的效果都很不错。虽然补集贝叶斯的布里尔分数更高,但它的精确度更高。我们可以使用概率校准来试试看能否让模型进一步突破:

from sklearn.calibration import CalibratedClassifierCV

name = ["Multinomial"
        ,"Multinomial + Isotonic"
        ,"Multinomial + Sigmoid"
        ,"Complement"
        ,"Complement + Isotonic"
        ,"Complement + Sigmoid"
        ,"Bernoulli"
        ,"Bernoulli + Isotonic"
        ,"Bernoulli + Sigmoid"]

models = [MultinomialNB()
          ,CalibratedClassifierCV(MultinomialNB(), cv=2, method='isotonic')
          ,CalibratedClassifierCV(MultinomialNB(), cv=2, method='sigmoid')
          ,ComplementNB()
          ,CalibratedClassifierCV(ComplementNB(), cv=2, method='isotonic')
          ,CalibratedClassifierCV(ComplementNB(), cv=2, method='sigmoid')
          ,BernoulliNB()
          ,CalibratedClassifierCV(BernoulliNB(), cv=2, method='isotonic')
          ,CalibratedClassifierCV(BernoulliNB(), cv=2, method='sigmoid')
         ]

for name,clf in zip(name,models):
    clf.fit(Xtrain_,Ytrain)
    y_pred = clf.predict(Xtest_)
    proba = clf.predict_proba(Xtest_)
    score = clf.score(Xtest_,Ytest)
    print(name)
    Bscore = []
    for i in range(len(np.unique(Ytrain))):
        bs = BS(Ytest,proba[:,i],pos_label=i)
        Bscore.append(bs)
        print("\tBrier under {}:{:.3f}".format(train.target_names[i],bs))
    print("\tAverage Brier:{:.3f}".format(np.mean(Bscore)))
    print("\tAccuracy:{:.3f}".format(score))
    print("\n")
    
"""
Multinomial
	Brier under rec.sport.hockey:0.018
	Brier under sci.space:0.033
	Brier under talk.politics.guns:0.030
	Brier under talk.politics.mideast:0.026
	Average Brier:0.027
	Accuracy:0.975


Multinomial + Isotonic
	Brier under rec.sport.hockey:0.006
	Brier under sci.space:0.012
	Brier under talk.politics.guns:0.013
	Brier under talk.politics.mideast:0.009
	Average Brier:0.010
	Accuracy:0.973


Multinomial + Sigmoid
	Brier under rec.sport.hockey:0.006
	Brier under sci.space:0.012
	Brier under talk.politics.guns:0.013
	Brier under talk.politics.mideast:0.009
	Average Brier:0.010
	Accuracy:0.973


Complement
	Brier under rec.sport.hockey:0.023
	Brier under sci.space:0.039
	Brier under talk.politics.guns:0.039
	Brier under talk.politics.mideast:0.033
	Average Brier:0.033
	Accuracy:0.986


Complement + Isotonic
	Brier under rec.sport.hockey:0.004
	Brier under sci.space:0.007
	Brier under talk.politics.guns:0.009
	Brier under talk.politics.mideast:0.006
	Average Brier:0.006
	Accuracy:0.985


Complement + Sigmoid
	Brier under rec.sport.hockey:0.004
	Brier under sci.space:0.009
	Brier under talk.politics.guns:0.010
	Brier under talk.politics.mideast:0.007
	Average Brier:0.007
	Accuracy:0.986


Bernoulli
	Brier under rec.sport.hockey:0.068
	Brier under sci.space:0.025
	Brier under talk.politics.guns:0.045
	Brier under talk.politics.mideast:0.053
	Average Brier:0.048
	Accuracy:0.902


Bernoulli + Isotonic
	Brier under rec.sport.hockey:0.016
	Brier under sci.space:0.014
	Brier under talk.politics.guns:0.034
	Brier under talk.politics.mideast:0.033
	Average Brier:0.024
	Accuracy:0.952


Bernoulli + Sigmoid
	Brier under rec.sport.hockey:0.066
	Brier under sci.space:0.030
	Brier under talk.politics.guns:0.056
	Brier under talk.politics.mideast:0.059
	Average Brier:0.053
	Accuracy:0.879
"""

可以观察到,多项式分布下无论如何调整,算法的效果都不如补集朴素贝叶斯来得好。因此我们在分类的时候,应该选择补集朴素贝叶斯。对于补集朴素贝叶斯来说,使用Sigmoid进行概率校准的模型综合最优秀:准确率最高,对数损失和布里尔分数都在0.1以下,可以说是非常理想的模型了。

对于机器学习而言,朴素贝叶斯也许不是最常用的分类算法,但作为概率预测算法中唯一一个真正依赖概率来进行计算,并且简单快捷的算法,朴素贝叶斯还是常常被人们提起。并且,朴素贝叶斯在文本分类上的效果的确非常优秀。由此可见,只要我们能够提供足够的数据,合理利用高维数据进行训练,朴素贝叶斯就可以为我们提供意想不到的效果。