你好,游客 登录
背景:
阅读新闻

使用朴素贝叶斯进行问卷分析(C++)

[日期:2018-03-06] 来源:  作者: [字体: ]

根据贝叶斯定理,对一个分类问题,给定样本特征x,样本属于类别y的概率是 

在这里,x是一个特征向量,将设x维度为M。因为朴素的假设,即特征条件独立,根据全概率公式展开,上述公式可以表达为 

 

这里,只要分别估计出,特征xi在每一类的条件概率就可以了。类别y的先验概率可以通过训练集算出,同样通过训练集上的统计,可以得出对应每一类上的,条件独立的特征对应的条件概率向量。

 

以下结合代码谈谈具体的实现

  1. struct questionnaire{ 
  2.     char x[4]; 
  3.     int y[24]; 
  4.     char x3; 
  5.     char x5; 
  6.     int y6[6]; 
  7.     char x7; 
  8. }; 

 

首先定义一个questionnaire的结构体

x[0]~x[3]分别表示基本信息中的性别、年龄、学业背景和职业

 

需要说明的是,我们把多选题看做n道单选题,这每道单选题的答案集合只有yes和no

y[0]~y[4]位主题问卷第一题的abcde

y[5]~y[10]位主题问卷第二题的abcdef

y[11]为第四题

y[12]~y[17]为第八题的abcdef

y[18]~y[23]为第八题的abcdeg

x3为第三题

x5为第五题

y6[0]~y6[5]为第六题的abcdef

x7为第七题

 

 

 

关于我们类的设置,先来看看我们的总体思路:

 

第一步先是学习的过程,也就是结合问卷统计相关的数量来计算我们所需要的概率为后面一步利用贝叶斯公式来计算(分类)打下基础。

如果说我们的训练集是TrainingSet={(x1,y1),(x2,y2),...,(xN,yN)} 包含N条训练数据,其中 xi=(x(1)i,x(2)i,...,x(M)i)T是M维向量,yi∈{c1,c2,...cK}属于K类中的一类。

 

学习 1.首先,我们来计算上面公式中的p(y=ck)

其中I(x)为指示函数,若括号内成立,则计1,否则为0。

 

学习 2.接下来计算分子中的条件概率,设M维特征的第j维有L个取值,则某维特征的某个取值ajl,在给定某分类ck下的条件概率为: 

 

为了避免出现统计量为零时概率算出也为零的不符合实际的情况,根据拉普拉斯平滑改进这两个式子,改进完上述式子变为:

 

K是类的个数 

Lj是第j维特征的最大取值,λ是一个常数,我们将其设置为1就好。

 

这就是我们第一步的学习步骤,所以结合代码看看我们的类是怎么设计的。

  1. class allQuestionnaires{ 
  2. public
  3.     questionnaire sample[N]; 
  4.     double p_yck[24][2]; 
  5.     double p_when_yck_then_x[24][2][22]; 
  6.     double p_yck3[4]; 
  7.     double p_when_yck3_then_x[4][22]; 
  8.     double p_yck6[6][2]; 
  9.     double p_when_yck6_then_x[6][2][22]; 
  10.     double p_yck5[3]; 
  11.     double p_when_yck5_then_x[3][22]; 
  12.     double p_yck7[5]; 
  13.     double p_when_yck7_then_x[5][22]; 
  14. }; 

 

这是我们的allQuestionnaires类的成员变量,为了方便将所有的变量都设为public的访问权限。首先定义一个questionnaire的数组,其中N是一个全局变量,代表问卷的总数。之后的p_yck就是p(y=ck),第一维的数字代表不同的问题,第二维的数字代表本题不同的选项,比如p_yck[24][2]的24代表的是第1289题的24个问题,2代表yes和no,所以这些变量所代表的含义为:

 

p_yck[0~4][0]为第一题五道单选题选yes的概率

p_yck[5~10][0]为第二题六道单选题选yes的概率

p_yck[11][0]为第四题选题选“体验过”的概率

p_yck[12~17][0]为第八题六道单选题选yes的概率

p_yck[18~23][0]为第九题六道单选题选yes的概率

 

选no的同理,将第二维改为1即可。所以p_yck3[4]、p_yck6[6][2]、p_yck5[3]、p_yck7[5]也就是第3657道题对应的解释,在此不做赘述。

 

而p_when_yck_then_x就是p(xj=ajl|y=ck),以p_when_yck_then_x[24][2][22]举例,第一维还是对应的1289的那24道题,第二维还是yes和no,而第三维表示的是基本信息里对应的22个选项,即0和1是“男”和“女”,2~7分别是年龄对应的六个选项,8~12为学历对应的五个选项,13~21为职业对应的九个选项。所以p_yck[0][0][0]的含义为第一题第一道小题选yes的情况下答题者性别为男的概率,其他的与此类似,不做赘述。

  1. allQuestionnaires(); 
  2.  
  3. ~allQuestionnaires(); 
  4.  
  5. void learn(); 
  6.  
  7. void classify(char,char,char,char); 
  8.  
  9. void learn_classify_67(char,char,char,char); 
  10.  
  11. void learn_classify_5(char,char,char,char); 

 

再来看看几个函数,第一个是构造函数,里面将包含初始化变量和读取文件的任务;第二个析构函数负责程序结束后的扫尾工作,第三个学习函数就是上文所说的两步学习的具体实现,第四个分类函数则会在下文给出原理。需要说明的是由于第5和67题和第四题的选择有关,所以把他们单独分离出来。

 

我们把问卷存在txt文档里存成下列形式,以其中两行做一个例子说明,一行代表一份问卷,它们分别对应的就是对应各个问题的选择,abcd的是单选题的选择,12的是多选题对应的小题的选择,1代表yes,2代表no,其中最后8个选择为5、6、7的选择,空是代表这个人第四题选了“去过“或”没去过”所以对应可以跳过相应的问题没有回答。

a b d g 2 1 1 2 2 2 1 2 2 1 2 b 2 1 2 2 2 22 1 2 1 1 2 1 a             

a b d i 1 1 1 1 2 2 2 1 1 1 2 a 1 2 2 2 1 21 1 1 2 1 1 2  2 2 1 2 2 2 b

 

所以构造函数就不多说了,实现的效果就是把变量中p(y=ck)和p(xj=ajl|y=ck)先初始化为0,然后把txt里对应题目的对应答案读入到我们设置的结构体中去,即把所有sample的x[4]、y[24]、x3、x5、y6[6]、x7全都填满。

 

析构函数无需赘述,我们来看学习函数的具体实现。还是以其中的p_yck[24][2]和p_when_yck_then_x[24][2][22]为例子:

  1. int help_p_yck[24]; 
  2.     for(int i=0;i<24;i++) 
  3.         help_p_yck[i]=0; 
  4.     for(int i=0;i<N;i++) 
  5.         for(int j=0;j<24;j++) 
  6.             if(sample[i].y[j]==1)help_p_yck[j]++; 
  7.     for(int i=0;i<24;i++){ 
  8.         p_yck[i][0]=double(help_p_yck[i]+1)/double(N+2); 
  9.         p_yck[i][1]=double(N-help_p_yck[i]+1)/double(N+2); 
  10.     } 

 

第一步学习,通过统计每道题中某个选项出现的次数,再将其除以总数来求得每道题的每个选项的p(y=ck),记住应用拉普拉斯平滑。

  1. int help_p_when_yck_then_x[24][2][22]; 
  2.  
  3.     for(intx=0;x<24;x++) 
  4.  
  5.         for(inty=0;y<2;y++) 
  6.  
  7.             for(int z=0;z<22;z++) 
  8.  
  9.                 help_p_when_yck_then_x[x][y][z]=0; 
  10.  
  11.     for(intx=0;x<24;x++){ 
  12.  
  13.         for(inti=0;i<N;i++){ 
  14.  
  15.             for(intj=0;j<22;j++){ 
  16.  
  17.                 intm2; 
  18.  
  19.                 if(sample[i].y[x]==1)m2=0; 
  20.  
  21.                 elsem2=1; 
  22.  
  23.                 switch(sample[i].x[0]){ 
  24.  
  25.                 case'a'
  26.  
  27.                     help_p_when_yck_then_x[x][m2][0]++; 
  28.  
  29.                     break
  30.  
  31.                 case'b'
  32.  
  33.                     help_p_when_yck_then_x[x][m2][1]++; 
  34.  
  35.                     break
  36.  
  37.                 }//第一维 
  38.  
  39.                  
  40.  
  41.             } 
  42.  
  43.         } 

 

第二步学习,首先统计当某道题选择某个选项的情况下,基本信息四题的选择情况

  1. for(int m=0;m<2;m++){ 
  2.  
  3.     p_when_yck_then_x[x][0][m]=double(help_p_when_yck_then_x[x][0][m]+1)/double(help_p_yck[x]+2); 
  4.  
  5.             p_when_yck_then_x[x][1][m]=double(help_p_when_yck_then_x[x][1][m]+1)/double(N-help_p_yck[x]+2); 
  6.  
  7.    
  8.  
  9.  
  10.    

 

接着再将取得的值除以第一步学习中已经求得相应的各题各选项的统计总数,来求得p(xj=ajl|y=ck),即当此题选此选项时,四个基本信息的选择频率计算得到。

 

 

由此,我们完成了学习部分,对于分类部分,先来看一下思路。

 

通过学到的概率,给定未分类新实例X,就可以通过上述概率进行计算,得到该实例属于各类的后验概率p(y=ck|X),因为对所有的类来说,第二个公式中分母的值都相同,所以只计算分子部分即可,具体步骤如下:

 

分类 1.计算该实例属于y=ck类的概率 

 

分类 2.确定该实例所属的分类y 

 

这个式子表示使某道题使分类1中那个式子得到的概率最大的那个选项作为我们的预测结果。于是我们得到了新实例的分类结果。

 

代码实现也十分清晰明了,以第一大题为例:

  1. ofstream out("predict.txt"); 
  2.  
  3.     double P[4]; 
  4.  
  5.     for(intx=0;x<5;x++){ 
  6.  
  7.         out<<"第一题第"<<x+1<<"小题的预测结果是"
  8.  
  9.         P[0]=p_yck[x][0]*p_when_yck_then_x[x][0][sex-'a']*p_when_yck_then_x[x][0][2+age-'a'
  10.  
  11.         *p_when_yck_then_x[x][0][8+educatedbackground-'a']*p_when_yck_then_x[x][0][13+job-'a']; 
  12.  
  13.         P[1]=p_yck[x][1]*p_when_yck_then_x[x][1][sex-'a']*p_when_yck_then_x[x][1][2+age-'a'
  14.  
  15.         *p_when_yck_then_x[x][1][8+educatedbackground-'a']*p_when_yck_then_x[x][1][13+job-'a']; 
  16.  
  17.         if(P[0]>P[1]){ 
  18.  
  19.             out<<"yes"<<endl; 
  20.  
  21.         } 
  22.  
  23.         else 
  24.  
  25.             if(P[0]<P[1]){ 
  26.  
  27.                 out<<"no"<<endl; 
  28.  
  29.             } 
  30.  
  31.             else
  32.  
  33.                 cout<<"第一题第"<<x+1<<"小题无法预测,yes no皆可以"<<endl; 
  34.  
  35.             } 
  36.  
  37.     } 

 

打开要写入的txt文本,按照分类1的公式和学习步骤已经计算出的相关结果计算出p(y=ck|X),再比较相同一体内各个题目概率大小,输出预测结果。

 

至此,我们完成了所有的内容。运行代码,输入参数a b d g(代表 男 21~30 大学本科 学生),得到整份问卷的预测结果:

第一题第1小题的预测结果是yes

第一题第2小题的预测结果是yes

第一题第3小题的预测结果是yes

第一题第4小题的预测结果是yes

第一题第5小题的预测结果是no

第二题第1小题的预测结果是no

第二题第2小题的预测结果是no

第二题第3小题的预测结果是yes

第二题第4小题的预测结果是no

第二题第5小题的预测结果是no

第二题第6小题的预测结果是yes

第三题的预测结果是a

第四题的预测结果是没去过

第五题的预测结果是a

跳过第六七题

第八题第1小题的预测结果是no

第八题第2小题的预测结果是no

第八题第3小题的预测结果是no

第八题第4小题的预测结果是yes

第八题第5小题的预测结果是yes

第八题第6小题的预测结果是yes

第九题第1小题的预测结果是yes

第九题第2小题的预测结果是yes

第九题第3小题的预测结果是yes

第九题第4小题的预测结果是yes

第九题第5小题的预测结果是no

第九题第6小题的预测结果是no

 

可以看到,在这份问卷上的与用spss的统计分析完得到的结果还是比较吻合的,比如机器预测第三题这个人会选择“很有必要”,第四题会选“没去过”,第五题会是“根本没有时间”。

 

然而经过与随机抽出的20份问卷进行真实性对比,发现正确率并不是特别高,经过反思,我觉得原因如下:

1. 样本因素,学习算法最重要的还是靠丰富的样本量,对于我们的问卷情况,有一些实际情况导致预测结果正确率低,它们是:

(1)  问卷总数不够多

(2)  问卷上基本信息的各个选项不够丰满,比如选择小学、五六十岁、退休的人特别少,这造成当正好预测到这些人时预测效果十分离谱。

(3)  预测表现不好的那些题往往是几个选项概率比较接近的那些题,这个很好理解。所以表现出像第二题,这种选择比较分散的问题,预测结果就不好。

2. 模型因素,我们可以发现,对于单项题的预测正确率还可以,但是对于多选题就表现的不太好了,原因自然是我们把多选变单选这个做法上的不足,很容易造成的后果是某道多选题全yes或者全no(比如第二题),这自然没考虑到多选与同样数量的几道yes no的单选上的差异。

 

总体来说,作为用比较简单的机器学习模型——朴素贝叶斯公式作为对问卷最终数据分析预测的一种尝试,效果虽然不是特别好,但是如果在样本更加理想的情况下,预测效果还是可期的。虽然朴素贝叶斯要求的前提是“朴素”,即各道题间相互独立,但是经过千万人的经验,它在非独立但假设它独立的“朴素”前提下在分类领域依旧有很好的表现。而且因为其有对应的概率学上的公式可以推导,所以在对待问卷分析上还有便于预测后依据概率学公式进行分析这一优点。所以这一尝试还是十分成功的。

推荐 打印 | 录入:Cstor | 阅读:
相关新闻      
本文评论   
评论声明
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款