预测的程序就很简单了:1. 计算输入样本的特征向量与模型的特征向量的向量内积;2. 在多个类别中,选择概率最大的类别。不过,贝努利模型和多项式模型的区别也就在此,在1——对那些在输入样本中没有出现的特征类型,那些取值为0的特征类型,如何处理。从前说过,我写的是贝努利模型,所以,这些特征类型的条件概率也计算进去了。
计算向量内积的代码如下:
double NaiveBayes::CalcProbLogByFeaVec (vector<int> & FeaIdVec, vector<FeaProbNode> & FeaVec) { double dProbLog = 0.0; vector<int>::iterator pFeaId = FeaIdVec.begin(); vector<FeaProbNode>::iterator pFea = FeaVec.begin(); while (pFeaId != FeaIdVec.end() && pFea != FeaVec.end()) { if (*pFeaId == pFea->iFeadId) { dProbLog += log (pFea->dProb + DBL_MIN); pFeaId++; pFea++; } else if (*pFeaId < pFea->iFeadId) pFeaId++; // it's not a selected feature in Bayes model, skip it else { // strictly speaking, we should do this in naive bayes: // there are two value for a feature type: 1 and 0 // It is 1 when the feature occurs in input sample; otherwise, 0. #if 1 dProbLog += log (1.0 - pFea->dProb + DBL_MIN); #endif pFea++; } } return dProbLog; }
关键的那个地方我用宏夹住了,并在代码里写了相对多的注释。想要变换算法的时候,随时禁止这个宏就行了。当然,更标准的做法是,起一个名字,在某个头文件里面定义这个宏的值是0还是1。不过,我就不搞那么“严肃”的了,毕竟这个东西代码量也不大,不至于那么难维护。
接下来,确定类别的代码:
bool NaiveBayes::PredictByInputFeas (vector<int> & FeaIdVec, int & iClassId) { if (FeaIdVec.empty()) return false; int iClassNum = ClassFeaVec.size(); if (iClassNum <= 0) return false; vector<double> PredProbLogVec (iClassNum, 0.0); // the predicting probability for each class for (int i=0; i<iClassNum; i++) { PredProbLogVec.at(i) = CalcProbLogByFeaVec (FeaIdVec, ClassFeaVec.at(i).FeaVec); } // select the class with max probability double dMaxProbLog = -DBL_MAX; for (int i=0; i<iClassNum; i++) { if (dMaxProbLog < PredProbLogVec.at(i)) { dMaxProbLog = PredProbLogVec.at(i); iClassId = ClassFeaVec.at(i).iClassId; } } return true; }
没啥可说的了。就朴素贝叶斯模型本身,还有一些代码,如:load模型,计算错误率等,都很简单,不罗列了。其实还有很大一部分代码,代码量可能更多,就是数据准备。尤其是分词,分词算法很多,不是这里的主题,以后有时间慢慢细说。还有停用词过滤,等等。呵呵。
【分类效果】
最后说一下我实际使用过程中的分类效果吧。前面的软文说过了,我要做的是文本过滤,也就是二值分类问题。正例样本比较多,反例样本比较少。整体上的错误率(误分率)大概9%;正例样本的误分率是7%,反例样本的误分率是15%。同时用了一下svm,结果svm效果远超过朴素贝叶斯。哎,这时候才体会到,知识就是生产力啊。不过朴素贝叶斯也不是没用了,svm只能处理小量的数据,而且并行化训练很困难。朴素贝叶斯在这些方面都是强项。
【后记】
其实还有些工作是可以做的,或者说有些问题是可以继续思考的。以上这些程序,其中一个缺点是就是模型训练算法与特征选择算法耦合的非常紧。这样做有好处,因为模型训练和特征选择都要对一些统计量进行计数,这种紧耦合的设计,能够在只扫描一遍样本集合的情况下完成任务,这对于大样本集合的问题来讲是比较有利的。但是缺点也很明显,就是耦合的太紧,不够灵活,如果想要换一个特征选择算法,就要直接改变Train函数的代码。如何解耦合?其实也简单,就是将训练过程和特征选择过程分开,分别放到两个类当中,再用聚合的方法将他们放在一起工作。特征选择类可以用工厂模式,来动态选择用哪个特征选择算法。选择的特征,可以用文本文件的形式暂时存储,供训练类读取使用。不过相应地,特征选择过程和训练过程也就分开了,也就是说要分别统计各自的统计量,也就是说至少扫描两遍样本集合。各有各自的优缺点,大家视自己的情况而定。
不说了。完。