利用standford-nlp库实现Naive Bayes文本分类系统

什么是文本分类

自然语言处理中,经常需要处理一类问题——文本分类。例如给定一段新闻内容,将新闻自动分类为体育,财经,娱乐中的一个分类;又或者对于接收到的邮件,自动识别邮件是否为广告邮件;又比如对于一段影评,自动判断其为差评还是好评;……. 总之应用非常广泛,此处不再累赘。我们知道,这样的任务,对于我们人来说,其实不是非常费力的,那么如何使计算机也拥有类似我们这样的智能呢?接下来,我将介绍如何利用斯坦福大学NLP组开源的几个软件包,搭建一个文本分类系统。

Naive Bayes算法简介

Naive Bayes是如何工作的

读者先别着急如何搭建系统,先给我几分钟介绍一下一个非常有效但却十分简单的一个分类算法——Naive Bayes。
假设现在我们有一个这样的任务:任意给定一段新闻文稿 X={x1,x2,x3,....,xn} ,我们希望系统能够自动归类到 C={,,,,} 这样五个分类中的其中一个。
从概率角度上,我们希望能够计算得出 p(c|x) 这样的条件概率,也就是给定输入文本:

x=

我们能够计算出:
p(|)=0.11p(|)=0.42p(|)=0.10p(|)=0.08p(|)=0.29

那么我们将可以选取概率最大的那个条件概率,并选取其分类作为文本的分类。上面例子我们将选取“娱乐“作为文本的分类预测。
因此,我们将预测分类定义为:
predict=argmaxcC p(c|x)

那么问题来了,如何计算 p(c|x) 呢?下面我们来做一点推导:

argmaxcC p(c|x)=argmaxcC p(c|x1,x2,...,xn)(x)=argmaxcC p(x1,x2,...,xn|c)p(c)p(x1,x2,....,xn)(Bayes)=argmaxcC p(x1|c)p(x2|c)...p(xn|c)p(c)p(x1,x2,....,xn)(NaiveBayes)=argmaxcCp(c)nip(xi|c)p(x1,x2,....,xn)()=argmaxcC p(c)nip(xi|c)(cc)

上面一堆数学中,除了要理清Bayes公式,最重要地部分就是

p(x1,x2,...,xn|c)=p(x1|c)p(x2|c)...p(xn|c)

这一步严格意义上是错误的,但是Naive Bayes做了一个假设,也就是当给定一个分类的时候,通过该分类产生某一个词的时候是互相独立的。举一个例子:
p(,,|)p(|)p(|)p(|)

也就是说通过娱乐这个分类,产出“范冰冰“或者“新片“之间是相互独立的,没有相互依赖的,一般情况下这样的假设是错误的,这也是Naive Bayes之所以Naive的地方,然而也因为它的Naive使得算法变得简单而高效。

如何训练Naive Bayes

好了,现在问题集中在如何求取:

argmaxcC p(c)inp(xi|c)

Naive Bayes也是一种Supervised Learning的算法,既然需要supervised,那么一开始必须有很多标注好的训练文本,比如:

娱乐:范冰冰方回应新片马震戏份被剪
体育:足协对郜林停5场和批评辽宁赛区的依据是什么
财经:证监会力除股市“毒瘤” 券商看好后市
…..

等等训练数据。那么p(c)如何估计呢?一个非常直接的方式是利用似然法估计:

p(c)=count(c)ccount(c)

也就是等于c分类出现的次数除以所有分类出现的次数。同理,我们可以估计 p(xi|c) :

p(xi|c)=count(xi,c)count(c)

举一个例子:

p(|)count(,)count()

也就是统计在娱乐分类中,新片出现的次数除以娱乐该分类中词语的总数。
然而这样的估计有一个问题,对于那些在训练文本中从来没出现过的词语,我们如何估计它们的概率呢?譬如如何估计 p(|) ,假设“姚晨“在娱乐分类的训练样本中没有出现过。那么count(姚晨,娱乐)为0,那么直接导致 p(|)0 ,然而我们知道,出现姚晨的文本,说明该文本很有可能是关于娱乐的,但是现在 p(|)0 ,导致 p()p(x1|)p(|)...p(xn|)0 ,那么我们很有就不会将该文本归类到娱乐分类中,这明显很不符合常识。怎么解决这样的问题呢?

Add-One Smoothing

一个简单的方法是,在估计 p(xi|c) 的时候,做一点手脚:

p(xi|c)=count(xi,c)+1count(c)+V

其中V是所有训练样本中,词汇的总数。所以对于在训练样本中没见过的词汇,我们也会给他一点点机会,而不是简单粗暴地说他概率为0。

在对数空间中操作

在计算 p(c)nip(xi|c) 的时候,因为概率是0到1的范围,那么直接导致计算结果非常非常小,比如:0.0000000000000000012,那么问题来了,计算机可不是理想的,我们知道浮点数表示的范围是有限的,于是在计算的时候,简单的连乘会出现underflow问题,一个解决办法是将计算转化到对数空间中:

argmaxcC p(c)inp(xi|c)=argmaxcC log p(c)inp(xi|c)=argmaxcC log p(c)+inlog p(xi|c)

因为log函数是单调递增函数,所以并不影响计算结果。

预处理之中文分词

在讲述Naive Bayes的时候,我刻意避开分词不谈,也就是说对于“范冰冰方回应新片马震戏份被剪“这样一句话,我们第一步要做的是将该话进行分词为:“范冰冰,方,回应,新片,马震戏,份,被,剪“这样的形式,再进行喂给Naive Bayes。为什么呢?假如不分词,那么Naive Bayes会认为整句话都是一个词,而对其进行概率预估的时候,我们采取的是统计其出现的个数,那么很明显,几乎所有的训练样本在不分词的情况下都只会出现1次,那么Naive Bayes将学习到一个均匀的概率分布,也就是Naive Bayes变成一个只会瞎猜的算法,这可不是我们想要的。 那么首先第一步是进行文本分词。中文分词本身就是一个研究的难点热点,讲中文分词可不比讲Naive Bayes那么简单了,然而本文的目的是利用standford-nlp库实现Naive Bayes分类系统,中文分词的原理这一块留到别的时间再讲。

利用stanford-classifier进行中文分词

原理不讲,并不代表我们无法实现中文分词,“拿来主义“指导我们前进。斯坦福大学提供了自然语言处理很多开源软件包,都是基于Java实现,具体可以到猛戳官网。我们在官网搜寻一下,不难找到关于中文分词的软件包standford-segmenter.jar,下载的时候请注意要求的Java版本,最新的要求是Java8的运行环境,所以注意了。下载下来之后,有一个Demo,稍微玩玩就知道怎么用:

  private static final String basedir = System.getProperty("SegDemo", "data");
  private static final Set<String> addr = Preprocess.loadAddr(); 

  public static void main(String[] args) throws Exception {
    System.setOut(new PrintStream(System.out, true, "utf-8"));

    Properties props = new Properties();
    props.setProperty("sighanCorporaDict", basedir);
    props.setProperty("serDictionary", basedir + "/dict-chris6.ser.gz");
    props.setProperty("inputEncoding", "UTF-8");
    props.setProperty("sighanPostProcessing", "true");

    CRFClassifier<CoreLabel> segmenter = new CRFClassifier<CoreLabel>(props);
    segmenter.loadClassifierNoExceptions(basedir + "/ctb.gz", props);
    System.out.println(segmenter.segmentString("范冰冰方回应新片马震戏份被剪"));
  }
  输出:[范冰冰, 方, 回应, 新片, 马震, 戏, 份, 被, 剪]

那么现在我们可以将所有训练样本

娱乐:范冰冰方回应新片马震戏份被剪
体育:足协对郜林停5场和批评辽宁赛区的依据是什么
财经:证监会力除股市“毒瘤” 券商看好后市
…..

转化为:

娱乐:[范冰冰, 方, 回应, 新片, 马震, 戏, 份, 被, 剪]
体育:[足协, 对, 郜林, 停, 5, 场, 和, 批评, 辽宁, 赛区, 的, 依据, 是, 什么]
财经:[证监会, 力除, 股市, “, 毒瘤, ”, 券商, 看好, 后市]
…..

也许我们还可以进一步删除一些无用的词,比如“的“无利于分类的词。
至此,我们为接下来利用NaiveBayes分类做好了准备。

利用standford-classifier进行分类

对于Naive Bayes,实现算法本身不是什么难事,但是程序猿,我们必须遵循”DRY”原则,既然斯坦福NLP组实现了那么多软件包,找一个实现Naive Bayes算法的jar包应该不会费力。果然稍微搜索一下,我们可以获得standford-classifier.jar。因为本文重点是讲Naive Bayes进行分类,那我们稍微看点源码(结果证明我们必须看源码,因为缺乏文档,有些实现不是很完善)。
其中有一个类ColumnDataClassifier,它的目的是提供可以利用命令行或者指定一个prop文件,便可以实现各种算法组合。
它要求的训练文件可以有很多种,我只用了最普通的txt文件,data.txt中内容如下:

娱乐   范冰冰   方   回应   新片   马震   戏   份   被   剪
体育  足协  对  郜林  停  5  场  和  批评  辽宁  赛区  的  依据  是  什么
财经  证监会  力除  股市  , 毒瘤,   券商  看好  后市
...

每一个训练样本都是一行,第一列为所属分类,接下来是以tab隔开的词。

指定的train.prop文件内容:

goldAnswerColumn=0 #指第一列为标准答案
featureFormat=true #每一列都是feature
intern=true #String 的intern方法,节省内存

那么通过构造ColumnDataClassifier,便可以指定其中一些属性。

ColumnDataClassifier cdc = new ColumnDataClassifier("train.prop");

然而,我发现ColumnDataClassifier提供的可指定的算法中,并没有NaiveBayes,其中有一个useNB属性,一开始我以为是,但是后来细看源码,发现不是本文所讲的NaiveBayes,而是基于Bernolli的NaiveBayes。后来发现有一个useClassifierFactory熟悉,并且jar包中有实现好的NaiveBayesClassifierFactory,然而发现它的分割做得不是很好,比较难用,我试了好几个方法,都不行,然后果断放弃利用ColumnDataClassifier进行指定算法,自己手动调用。不过利用ColumnDataClassifier做一些辅助工作。好了,看代码的时候到了:

ColumnDataClassifier cdc = new ColumnDataClassifier("train.prop");
        GeneralDataset<String,String> dataInfo = cdc.readTrainingExamples("data.txt");
        dataInfo.randomize(System.currentTimeMillis());
        Pair<GeneralDataset<String,String>,GeneralDataset<String,String>> pair = dataInfo.split(0.75);
        GeneralDataset<String,String> trainData = pair.second;
        GeneralDataset<String,String> testData = pair.first;

        List<RVFDatum<String, String>> rvfData = new ArrayList<RVFDatum<String,String>>();
        Set<String> featset = new HashSet<String>();
        featset.addAll(trainData.featureIndex.objectsList());

        for(RVFDatum<String, String> data : trainData) {
            rvfData.add(data);
        }

        NaiveBayesClassifierFactory<String, String> fact = new NaiveBayesClassifierFactory<String, String>(1,1,-1,
                -1,NaiveBayesClassifierFactory.JL);
        NaiveBayesClassifier<String, String> classifier = fact.trainClassifier(rvfData, featset);
        System.out.println("accuracy "+classifier.accuracy(testData.iterator()));

读者可以细看NaiveBayesClassifierFactory和NaiveBayesClassifier的方法,其中NaiveBayesClassifierFactory的trainWeightsJL方法就是本文所提到的训练方式。不过它提供了对词进行双向索引的类Index,也就是可以很快根据词查询它的索引,或者根据索引查到词的方法。训练的时候都是在对索引进行操作,所以粗略看会觉得和本文提到的算法很不同,但是细看便可知是一致的。

结合这些开源软件,我们可以很轻松地搭建一个文本分类系统,别人都那么热心开源做贡献,我们写写小文广告广告他们的软件也是应该的。

感谢阅读。

参考引用

Andrew Ng cs229 lecture note:http://cs229.stanford.edu/notes/cs229-notes2.pdf
standford nlp 课程:https://www.coursera.org/course/nlp

你可能感兴趣的:(java,NLP,朴素贝叶斯,文本分类,NaiveBayes)