朴素贝叶斯(Naive Bayers)算法是一种基于概率统计的分类方法。它在条件独立假设的基础上,使用贝叶斯定理构建算法,在文本处理领域有广泛的应用。
1.朴素贝叶斯算法原理
朴素贝叶斯算法,需要从贝叶斯定理说起,它是一个条件概率公式。
1.贝叶斯定理
先来看一个案例。某警察使用一个假冒伪劣的呼吸测试仪来测试司机是否醉驾。假设这个仪器有5%的概率会把一个正常的司机判断为醉驾,但对真正醉驾的司机其测试结果是100%准确的。从过往的统计得知,大概有0.1%的司机为醉驾。假设该警察随机拦下一个司机,让他做呼吸测试,仪器测试结果为醉驾。仅凭这一结果判断,这位司机真的是醉驾的概率有多高?
90%?50%?真实的结果是不到2%。如果我们没有通过其他方法(如闻司机身上的酒味),仅凭这个仪器的测试结果来判断,其实准确性是非常低的。
假设我们的样本里有1000人,根据过往的统计数据,这1000位司机里有0.1%的概率为真正醉驾,即有1位是真正醉驾的司机,999位是正常的。这1000位司机均拿这个劣质呼吸测试仪来测试,则有多少人会被判断为醉驾?对这位真正醉驾的司机,他无法蒙混过关,而对999位正常的司机,有5%的概率会被误判,所以总共有1+999*5%=51个司机会被仪器判断为醉驾。由此可知,所有被判断为醉驾的司机里,真正醉驾的概率是1/(1+51)=1.96%。
实际上,贝叶斯定理是计算这类条件概率问题的绝佳方法。我们记P(A|B)表示观察到的事件B发生时事件A发生的概率,则贝叶斯定理的数学表达式为:
回到醉驾的例子,我们记事件A为司机真正醉驾,事件B为仪器显示司机醉驾。则例子里要求解的问题即为P(A|B),即观察到仪器显示司机醉驾(事件B发生)时,司机真正醉驾(事件A发生)的概率是多少。P(A)表示司机真正醉驾的概率,这是先验概率,这里是0.1%。P(B|A)表示当司机真正醉驾时(事件A发生),仪器显示司机醉驾(事件B发生)的概率是多少,这里是100%。P(B)表示仪器显示司机醉驾的概率,包含两部分数据,针对真正醉驾的司机(0.1%),仪器能100%检测出来,因为这部分的数值为0.1% x 100%;针对正常的司机(1-0.1%),仪器显示醉驾的概率为(1-0.1%) x 5%。代入贝叶斯定理公式得:
2.朴素贝叶斯分类法
假设有一个已经标记的数据集,其中,即数据集总共有b个类别;,即总共有n个输入特征。针对一个新的样本x,我们需要预测y的值,即对x进行分类。这是个典型的机器学习里的分类问题。
对我们要求解的问题,使用统计学的语言可以描述为:当观察到输入样本是x时,其所属于的类别的概率,使用条件概率公式表示为:
其中,,我们只需要分别求出所有b个类别的概率,然后取概率最大的那个即是x所属的类别。直接求解上述公式比较困难,可以应用贝叶斯定理进行一次变换:
对于一个确定的数据集,,都是固定的值。因此:
其中,表示成正比的意思。因此,我们只需要求解,针对不同的的情况下,的最大值即可知道,x属于哪个类别。根据联合概率公式,可得:
因为x是n维向量,即,可得
根据链式法则及条件概率的定义,可进一步推导公式:
上述推导里,只用了贝叶斯定理,我们还要再前面加上“朴素”二字。朴素指的是条件独立假设,即事件之间没有关联关系。例如,掷一个质地均匀的骰子两次,前后之间出现的数字是独立、不相关的,我们称这两个事件时条件独立的。朴素贝叶斯算法的前提是,输入特征需要满足条件假设。即当时,和是不相关的,通俗地说就是事件是否发生和没关系。根据条件独立的原则:
有了这个公式,我们就可以简化为:
这样我们的最终推导结果就是:
其中,是连乘符号。表示每种类别出现的概率,这个值很容易从数据集里统计出来。表示当类别为时,特征出现的概率,这个值也可以从数据集中统计出来。
这就是朴素贝叶斯分类算法的数学原理。
2.一个简单的例子
我们先通过一个简单的例子,来看怎样应用朴素贝叶斯分类算法。假设有以下关于驾龄、平均车速和性别的统计数据:
现在观察到一个驾龄为2年的人,平均车速为80。问:这个人的性别是什么?
假设表示女,表示男,表示驾龄,表示平均车速。我们先来计算这个人为女性的概率相对值。根据统计数据,女性司机的概率。驾龄为2年的女性司机的概率即。平均车速为80的女性司机的概率。根据朴素贝叶斯分类算法的数学公式:
接着计算这个人为男性的概率相对值。根据统计数据,不难得出男性司机的概率。驾龄为2年的男性司机的概率。平均车速为80的男性司机的概率。根据朴素贝叶斯分类算法的数学公式:
从相对概率来看,这个人是男性的概率是女性的概率的6倍,据此判断这个人是男性。我们也可以从相对概率中算出绝对概率,即这个人是男性的绝对概率是0.12/(0.12+0.02)=0.857。
3.概率分布
到目前为止,我们介绍的朴素贝叶斯分类算法是根据数据集里的数据,计算出绝对概率来进行求解。再看一遍朴素贝叶斯分类算法的数学公式:
其中,表示在类别里特征出现的概率。这里有个最大的问题,如果数据集太小,那么从数据集里计算出来的概率偏差将非常严重。例如,观察一个质地均匀的骰子投掷6次的结果是[1,3,1,5,3,3]。质地均匀的骰子每个点出现的概率都是1/6,如果根据观察到的数据集去计算每个点的概率,和真实的概率相差将是非常大的。
怎么解决这个问题呢?答案是使用概率分布来计算概率,而不是从数据集里计算概率。
1.概率统计的基本概念
人的身高是一个连续的随机变量,而投掷一个骰子得到的点数则是一个离散的随机变量。我们闭着眼睛随便找一个人,问这个人的身高是170cm的可能性是多大呢?如果有一个函数,能描述人类身高的可能性,那么直接把170cm代入即可求出这个可能性。这个函数就是概率密度函数,也称为PDF(Probability Density Function)。典型的概率密度函数是高斯分布函数,如人类的身高就满足高斯分布的规律。
再例如,投掷一个质地均匀的骰子,得到6的概率是多少呢?大家都知道答案是1/6。假如有一个函数f(x),能描述骰子出现x点数()的概率,那么把x代入即可得到概率,这个函数称为概率质量函数,即PMF(Probability Mass Function)。那么,为什么还有使用概率质量函数呢?一是在数学上追求统一性,二是并不是所有的离散随机变量的概率分布都像掷一次骰子这个直观。例如,投掷6次质地均匀的骰子,得到4个4的概率是多少?这个时候如果有概率质量函数,就可以轻松求解了。
总结一下,随机变量分成两种,一种是连续随机变量,另外一种是离散随机变量。概率密度函数描述的是连续随机变量在某个特定值的可能性,概率质量函数描述的是离散随机变量在某个特定值的可能性。而概率分布则是描述随机变量取值的概率规律。
2.多项式分布
抛一枚硬币,要么出现正面,要么出现反面(假设硬币不会立起来)。假如出现正面的概率是p,则出现反面的概率就是1-p。符合这个规律的概率分布,称为伯努利分布(Bernoulli Distribution)。其概率质量函数为:
其中,,p是出现1的概率。例如,一枚质地均匀的硬币被抛一次,得到正面的概率为0.5。我们代入上述公式,也可以得到相同的结果,即f(1;0.5)=0.5。
更一般的情况,即不止两种可能性时,假设每种可能性是,则满足条件的概率分布,称为类别分布(Categorical Distribution)。例如,投掷一枚骰子,则会出现6中可能性,所有的可能性加起来的概率为1。类别分布的概率质量函数为:
其中,是连乘符号,是类别的数量,是第种类别的概率,当且仅当类别为类别,其值为1,其他情况其值为0。例如,对于质地均匀的骰子,的值为6,的值都为1/6。问:投掷这个骰子得到3的概率是多少?答案是1/6。我们代入概率质量函数验算一下:,针对所有的情况,,针对的情况,,所以容易算出。
那么,一枚质地均匀的硬币被抛10次,出现3次正面的概率是多少呢?这是个典型的二项式分布问题。二项式分布指的是把符号伯努利分布的实验做了n次,结果1出现0次、1次、2次……n次的概率分别是多少,它的概率质量函数为:
其中,是结果1出现的次数,,是实验的总次数。怎么理解这个公式呢,我们总共进行了次实验,那么出现次结果1的概率为,剩下的必定是结果0的次数,即出现了次,其概率是。公式前面的系数表示的是组合,即次实验中出现次1的情况有多少种。回到最初的问题:一枚质地均匀的硬币被抛10次,出现3次正面的概率是多少?代入二项式分布的概率质量函数,得到:
其中,0的阶乘为1,即0!=1。结果跟我们预期的相符。当实验只做一次时,二项式分布退化为伯努利分布。
多项式分布是指满足类别分布的实验,连续做n次后,每种类别出现的特定次数组合的概率分布情况。假设,表示类别出现的次数,表示类别在单次实验中出现的概率。当满足前提条件时,由随机变量构成的随机向量满足以下分布函数:
其中,P是由各个类别的概率构成的向量,即,k表示类别的总数,n表示实验进行的总次数。理解这个公式也比较简单,可以把 理解为按照特定顺序,所有类别出现的某个特定次数组合的概率,如投掷6次骰子,出现(1,2,3,4,5,6)这样特定顺序组合的概率。前面的系数表示组合的个数,如投掷6次骰子,每个点数都出现一次,可以是(1,2,3,4,5,6),也可以是(1,3,2,4,5,6)。
我们看一个例子,同时投掷6个质地均匀的骰子,出现(1,2,3,4,5,6)这种组合的概率是多少?我们可以把这个问题转换成连续6次投掷质地均匀的骰子。这是个典型的多项式分布问题,其中随机向量,代入多项式分布的概率质量函数可得:
那么,将质地均匀的骰子投掷6次,得到4个4的概率是多少?我们需要把这个问题转换为二项分布问题。投掷1次骰子时,得到4的概率是1/6,得到其他点数(非4)的概率是5/6。现在需要就算投掷6次骰子得到4个4的概率,代入二项式分布的概率质量函数可得:
我们再来算一下同时投掷6个质地均匀的骰子,出现5个1的概率是多少?还是转换为二项式分布问题:
简单总结一下,二项式分布描述的是多次伯努利实验中,某个结果出现次数的概率。多项式分布描述的是多次进行满足类别分布的实验中,所有类别出现的次数组合的分布。
二项式分布和多项式分布结合朴素贝叶斯算法,经常被用来实现文章分类算法。例如,有一个论坛需要对用户的评论进行过滤,屏蔽不文明的评论。首先要有一个经过标记的数据集,我们称为语料库。假设使用人工标记的方法对评论进行标记,1表示不文明的评论,0表示正常评论。
假设我们的词库大小为k,则评论中出现某个词可以看成是一次满足k个类别的类别分布实验。我们知道,一篇评论是由n个词组成的,因此一篇评论可以看出是进行n次类别分布实验后的产物。由此得知,一篇评论服从多项式分布,它是词库里的所有词语出现的次数组合构成的随机向量。一般情况下,词库比较大,评论只是由少量词组成,所以这个随机向量是很稀疏的,即大部分元素为0。通过分析预料库,我们容易统计出每个词出现在不文明评论及正常评论的概率,即的值。同时针对待预测的评论,我们可以统计词库里的所有词在这篇评论里出现的次数即的值,及评论的词语个数。代入多项式分布的概率质量函数:
我们可以求出,待预测评论构成的随机向量,其为不文明评论的相对概率。同理也可以求出其为正常评论的相对概率。通过比较两个相对概率,就可以对这篇评论输出一个预测值。当然,实际应用中,涉及大量的自然语言处理的手段,包括中文分词技术、词的数学表示等,这里不再展开。
3.高斯分布
在前面的车速和性别预测的例子里,对于平均车速,给出的是离散值,实际上它是一个连续值。这个时候怎么使用贝叶斯算法来处理呢?答案是,可以用区间把连续值转换成离散值。例如,我们可以把平均车速[0,40]作为一个级别,[40-80],等等。这样就可以把连续值变成离散值,从而使用贝叶斯算法进行处理。另外一个方法,是使用连续随机变量的概率密度函数,把数值转换为一个相对概率。高斯分布就是这样一种方法。
高斯分布(Gaussian Distribution)也称为正态分布(Normal Distribution),是最常见的一种分布。例如人的身高满足高斯分布,特别高和特别矮的人出现的相对概率都很低,大部分人身高都处在中间水平。还有人的智商也符合高斯分布,特别聪明的天才和特别笨的人出现的相对概率都很低,大部分人的智力都差不多。高斯分布的概率密度函数为:
其中,为随机变量的值,为随机变量的相对概率,为样本的平均值,其决定了高斯分布曲线的位置,为标准差,其决定了高斯分布的幅度,值越大,分布越分散,值越小,分布越集中。典型的高斯分布如下图所示:
这里需要注意的是:高斯分布的概率密度函数和支持向量机里的高斯核函数的区别。二者的核心数学模型是相同的,但是目的不同。
4.连续值的处理
假设,有一组身体特征的统计数据如下:
假设某人身高6英尺,体重130榜,脚掌8英寸,请问此人的性别是什么?
根据朴素贝叶斯公式:
针对待预测的这个人的数据,我们只需要分别求出男性和女性的相对概率:
然后取相对概率较高的性别为预测值即可。这里的困难在于,所有的特征都是连续变量,无法根据统计数据计算概率。当然,这里我们可以使用区间法,把连续变量变为离散变量,然后再计算概率。但由于数据量较小,这显然不是一个好办法。由于人类身高、体重、脚掌尺寸满足高斯分布,因此更好的办法是使用高斯分布的概率密度函数来求相对概率。
首先,针对男性和女性,分别求出特征的平均值和方差:
接着利用高斯分布的概率密度函数,来求解男性身高为6英尺的相对概率:
这里的关键是把连续值(身高)作为输入,通过高斯分布的概率密度函数的处理,直接转换为相对概率。注意这里是相对概率,所以其值大于1并未违反概率论规则。
使用相同的方法,可以算出以下数值:
由于,因此这个人是男性的相对概率为:
使用相同的方法,可以算出这个人为女性的相对概率为。这个人为女性的概率比男性的概率大,因此判定这个人为女性。
5.示例:文档分类
在scikit-learn里,朴素贝叶斯算法在sklearn.naive_bayes包里实现,包含本文介绍的几种典型的概率分布算法。其中GaussianNB实现了高斯分布的朴素贝叶斯算法,MultinomialNB实现了多项式分布的朴素贝叶斯算法,BernoulliNB实现了伯努利分布的朴素贝叶斯算法。朴素贝叶斯算法在自然语言处理领域有着广泛的应用,这里我们使用MultinomialNB来实现文档的自动分类。
1.获取数据集
这里使用的数据集来自mlcomp.org上的20news-18828,可以直接访问http://mlcomp.org/datasets/379下载。载完数据集后,解压到datasets/mlcomp/目录下,解压后会在datasets/mlcomp下生成一个名为379的目录,其目录下包含3个子目录和一个名为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(10000/10000)=0;“应用”在1000篇文档中出现过,则其IDF=log(10000/1000)=1。
计算出每个词的TF和IDF之后,两者相乘,即可得到这个词在文档中的重要程度。词语的重要性与它在该文档中出现的次数成正比,与它在语料库中出现的文档数成反比。
有了TF-IDF这个工具,我们就可以把一篇文档转换为一个向量。首先,可以从数据集(在自然语言处理领域也称corpus,即语料库)里提取出所有出现的词,我们称为词典。假设词典里总共有10000个词语,则每个文档都可以转化为一个10000维的向量。其次,针对我们要转换的文档里出现的每个词语,都去计算其TF-IDF,并把这个值填入文档向量里这个词对应的元素上。这样就完成了把一篇文档转换为一个向量的过程。一个文档往往只会由词典里的一小部分词语构成,这就意味着这个向量里的大部分元素都是0。
所幸,上述过程不需要我们自己写代码去完成,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))
输出如下:
loading train dataset ...
summary: 13180 documents in 20 categories.
done in 0.8563289642333984 seconds
其中,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]]即可得到。
该语料库里总共有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 samples [{0}]:{1}"
.format(news_train.filenames[0],X_train[0].getnnz()))
print("done in {0} seconds".format(time()-t))
输出如下:
vectorizing train dataset ...
n_samples: 13180, n_features: 130274
number of non-zero features in samples [datasets/mlcomp/379/train\talk.politics.misc\17860-178992]:108
done in 2.6174726486206055 seconds
其中,TfidfVectorizer类是用来把所有的文档转换为矩阵,该矩阵每行都代表一个文档,一行中的每个元素代表一个对应的词语的重要性,词语的重要性由TF-IDF来表示。其fit_transform()方法是fit()和transform()合并起来。其中,fit()会先完成语料库分析、提取词典等操作,transform()会把对每篇文档转换为向量,最终构成一个矩阵,保存在X_train变量里。
由输出可以知道,我们的词典总共有130274个词语,即每篇文档都可转换为一个130274维的向量。第一篇文档中,只有108个非零元素,即这篇文档总共由108个不重复的单词组成,在这篇文档中出现的这108个单词的TF-IDF值会被计算出来,并保存在向量中的指定位置上。X_train是一个维度为13180*130274的稀疏矩阵。
X_train稀疏矩阵由一个三元组(row,col,score)构成:
print(X_train[0])
输出如下:
(0, 56813) 0.014332663773643272
(0, 45689) 0.08373343949755
(0, 46084) 0.08109733529789522
(0, 125882) 0.0873157704840211
(0, 50150) 0.020654313721609956
(0, 87702) 0.04643235585055511
(0, 33334) 0.1025405658189532
(0, 111805) 0.014332663773643272
: :
(0, 67768) 0.08982314745972582
(0, 41790) 0.09260592033433869
(0, 105800) 0.08713990737243116
(0, 37075) 0.10018566542781165
(0, 23162) 0.08920437523600384
(0, 124699) 0.06257976758779137
(0, 94119) 0.1159317059788844
(0, 56555) 0.06984885482106491
(0, 62776) 0.10474995568339582
3.模型训练
使用MultinomialNB对数据集进行训练:
from sklearn.naive_bayes import MultinomialNB
print("training models ...")
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))
输出如下:
training models ...
train score: 0.9978755690440061
done in 0.4581763744354248 seconds
其中,alpha表示平滑参数,其值越小,越容易造成过拟合,值太大,容易造成欠拟合。
接着,我们加载测试数据集,并用一篇文档来预测其是否准确。测试数据集在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.3548290729522705 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 {0} seconds".format(time()-t))
输出如下:
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 0.9695498943328857 seconds
注意,vectorizer变量是我们处理训练数据集时用到的向量化的类的实例,此处我们只需要调用transform()进行TF-IDF数值计算即可,不需要再调用fit()进行语料库分析了。
这样,我们的测试数据集也转换为了一个维度为5648*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 ...")
t = time()
pred_test = clf.predict(X_test)
print("done in %fs" % (time()-t))
接着使用classification_report()函数来查看一下针对每个类别的预测准确性:
from sklearn.metrics import classification_report
print("classification report on test set for classifier:")
print(clf)
print(classification_report(y_test,pred_test,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_test)
print("confusion matrix:\n")
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()
输出图形如下:
除对角线外,其他地方颜色越浅,说明此处错误越多。通过这些数据,我们可以详细分析样本数据,找出为什么某种类别会被错误地分类到另一种类别里,从而进一步优化模型。