自然语言处理中,经常需要处理一类问题——文本分类。例如给定一段新闻内容,将新闻自动分类为体育,财经,娱乐中的一个分类;又或者对于接收到的邮件,自动识别邮件是否为广告邮件;又比如对于一段影评,自动判断其为差评还是好评;……. 总之应用非常广泛,此处不再累赘。我们知道,这样的任务,对于我们人来说,其实不是非常费力的,那么如何使计算机也拥有类似我们这样的智能呢?接下来,我将介绍如何利用斯坦福大学NLP组开源的几个软件包,搭建一个文本分类系统。
读者先别着急如何搭建系统,先给我几分钟介绍一下一个非常有效但却十分简单的一个分类算法——Naive Bayes。
假设现在我们有一个这样的任务:任意给定一段新闻文稿 X={x1,x2,x3,....,xn} ,我们希望系统能够自动归类到 C={体育,娱乐,教育,财经,其他} 这样五个分类中的其中一个。
从概率角度上,我们希望能够计算得出 p(c|x) 这样的条件概率,也就是给定输入文本:
argmaxc∈C p(c|x)=argmaxc∈C p(c|x1,x2,...,xn)(只是简单扩展,将x还原为一个一个词组)=argmaxc∈C p(x1,x2,...,xn|c)∗p(c)p(x1,x2,....,xn)(利用Bayes公式进行转换)=argmaxc∈C p(x1|c)∗p(x2|c)∗...∗p(xn|c)∗p(c)p(x1,x2,....,xn)(利用NaiveBayes假设)=argmaxc∈Cp(c)∗∏nip(xi|c)p(x1,x2,....,xn)(简化公式)=argmaxc∈C p(c)∗∏nip(xi|c)(因为我们只关心c,而分母不含有c可以直接忽略)
上面一堆数学中,除了要理清Bayes公式,最重要地部分就是
好了,现在问题集中在如何求取:
娱乐:范冰冰方回应新片马震戏份被剪
体育:足协对郜林停5场和批评辽宁赛区的依据是什么
财经:证监会力除股市“毒瘤” 券商看好后市
…..
等等训练数据。那么p(c)如何估计呢?一个非常直接的方式是利用似然法估计:
也就是等于c分类出现的次数除以所有分类出现的次数。同理,我们可以估计 p(xi|c) :
举一个例子:
一个简单的方法是,在估计 p(xi|c) 的时候,做一点手脚:
其中V是所有训练样本中,词汇的总数。所以对于在训练样本中没见过的词汇,我们也会给他一点点机会,而不是简单粗暴地说他概率为0。
在计算 p(c)∗∏nip(xi|c) 的时候,因为概率是0到1的范围,那么直接导致计算结果非常非常小,比如:0.0000000000000000012,那么问题来了,计算机可不是理想的,我们知道浮点数表示的范围是有限的,于是在计算的时候,简单的连乘会出现underflow问题,一个解决办法是将计算转化到对数空间中:
在讲述Naive Bayes的时候,我刻意避开分词不谈,也就是说对于“范冰冰方回应新片马震戏份被剪“这样一句话,我们第一步要做的是将该话进行分词为:“范冰冰,方,回应,新片,马震戏,份,被,剪“这样的形式,再进行喂给Naive Bayes。为什么呢?假如不分词,那么Naive Bayes会认为整句话都是一个词,而对其进行概率预估的时候,我们采取的是统计其出现的个数,那么很明显,几乎所有的训练样本在不分词的情况下都只会出现1次,那么Naive Bayes将学习到一个均匀的概率分布,也就是Naive Bayes变成一个只会瞎猜的算法,这可不是我们想要的。 那么首先第一步是进行文本分词。中文分词本身就是一个研究的难点热点,讲中文分词可不比讲Naive Bayes那么简单了,然而本文的目的是利用standford-nlp库实现Naive Bayes分类系统,中文分词的原理这一块留到别的时间再讲。
原理不讲,并不代表我们无法实现中文分词,“拿来主义“指导我们前进。斯坦福大学提供了自然语言处理很多开源软件包,都是基于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分类做好了准备。
对于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