DM&ML_note.3-朴素贝叶斯分类器

这个学期要学DM&ML,用的是《数据挖掘算法原理与实现》王振武 本着造福同学的思想,开一个DM&ML的笔记系列,打算给书上的源代码添加一点注释,方便阅读和理解。


前置知识要求:

离散数学,概率论(主要是关于贝叶斯定理已经相关的知识,这里其实书上有简略的介绍,有一点概率论基础的同学基本就可以看懂书上的一些证明过程了),C++,STL

具体实现:

// bys.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
/*hiro:
stdafx的英文全称为:
Standard Application Framework Extensions(标准应用程序框架的扩展)。
所谓头文件预编译,就是把一个工程(Project)中使用的一些MFC标准头
文件(如Windows.H、Afxwin.H)预先编译,以后该工程编译时,
不再编译这部分头文件,仅仅使用预编译的结果。这样可以加快编译速度,节省时间。
*/

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
using namespace std;
vector<string> split(const string& src,const string& delimiter);    //根据定界符分离字符串
void rejudge();       //重新判断原输入数据的类别
vector<vector<string> > vect;    //二维容器
map<string,int> category_bak; //存放类别
map<string,double> pro_map;   //存放各种概率的map容器
int main()
{
   string strLine;
   ifstream readfile("weather.txt");
   if(!readfile)  //打开文件失败!
   {
      cout<<"Fail to open file weather!"<<endl;
      cout<<getchar();
      return 0;
   }
   else
   {
      cout<<"读取原始数据如下:"<<endl;
      vector<vector<string> >::size_type st_x;    //二维容器x座标
      vector<string>::size_type st_y;   //二维容器y座标
      vector<string> temp_vect;
      while(getline(readfile,strLine))  //一行一行读取数据
      {
         cout<<strLine<<endl;
         temp_vect=split(strLine,",");     //调用分割函数分割一行字符串
         vect.push_back(temp_vect);     //插入二维容器
         temp_vect.clear();        //清空容器
      }
      string temp_string;     //临时字符串
      /*hiro:size_type是用于实现与机器无关的数据类型,方便移植*/
      vector<string>::size_type temp_size1=vect.size()-1;   //总行数
      vector<string>::size_type temp_size2=vect[0].size();   //总列数
      for(st_x=1;st_x<temp_size1+1;st_x++)  //遍历二维容器,统计各种类别、属性|类别的个数,以便后面的概率的计算(跳过第一行的属性标题)
      {
          for(st_y=0;st_y<temp_size2;st_y++)
          {
              if(st_y!=temp_size2-1)  //处理每一行前面的属性,统计属性|类别的个数
              {
                 temp_string=vect[0][st_y]+"="+vect[st_x][st_y]+"|"+vect[0][temp_size2-1]+"="+vect[st_x][temp_size2-1];
                 pro_map[temp_string]++;    //计数加1
              }
              else        //处理每一行的类别,统计类别的个数
              {
                 temp_string=vect[0][temp_size2-1]+"="+vect[st_x][temp_size2-1];
                 pro_map[temp_string]++;   //计数加1 
                 category_bak[vect[st_x][temp_size2-1]]=1; //还没有类别,则加入新的类别
              }
              temp_string.erase();
          }
      }
      string::size_type st;
      cout<<"统计过程如下:"<<endl;
      for(map<string,double>::iterator it=pro_map.begin();it!=pro_map.end();it++)  //计算条件概率(属性|类别)
      {
          cout<<it->first<<":"<<it->second<<endl;
          /*hiro:string::npos是find函数的一种特殊返回值,用于表示查询失败*/
          if((st=it->first.find("|"))!=string::npos)
          {
              /*hiro:↓增加用于中间输出
              当前项的计数,比如:"humidity=high|Play tennis=no"为4    
              */
              cout << it->second << endl;
              /*hiro:st为‘|’字符的下标,substr(str+1)表示这个项的具体分类,
              比如:"humidity=high|Play tennis=no"的substr(str+1)为
              “Play tennis=no”,由于“Play tennis=no”的次数已经被统计,
              所以访问pro_map[it->first.substr(st+1)]即等于pro_map[“Play tennis=no”]
              表示“Play tennis=no”的统计次数*/
              cout << pro_map[it->first.substr(st + 1)]<<endl;
              /*hiro:所以it->second在这一步变成了记录条件概率,比如P(humidity=high|Play tennis=no)*/
             it->second=it->second/pro_map[it->first.substr(st+1)];

          }
      }
      cout<<"计算概率过程如下:"<<endl;
      for(map<string,double>::iterator it2=pro_map.begin();it2!=pro_map.end();it2++)  //计算概率(类别)
      {
          /*hiro:注意这里的条件是==,即计算分类本身的概率
          比如P(play=no)=5/14*/
          if((st=it2->first.find("|"))==string::npos)
          {
             pro_map[it2->first]=pro_map[it2->first]/(double)temp_size1;
          }
          cout<<it2->first<<":"<<it2->second<<endl;
      }
      //cout<<"play=no:"<<(no/(double)temp_size1)<<endl;
     // cout<<"play=yes:"<<(yes/(double)temp_size1)<<endl;
      rejudge();
   }

   cout<<getchar();
   return 0;
}
vector<string> split(const string& src,const string& delimiter)  //根据定界符分离字符串
{
   string::size_type st;
   /*hiro:异常处理*/
   if(src.empty())
   {
      throw "Empty string!";
   }
   if(delimiter.empty())
   {
      throw "Empty delimiter!";
   }
   vector<string> vect;
   string::size_type last_st=0;  
   while((st=src.find_first_of(delimiter,last_st))!=string::npos)
   {
      if(st!=last_st)    //2个标记间的字符串为一个子字符串
      {
         vect.push_back(src.substr(last_st,st-last_st));
      }
      last_st=st+1;
   }
   if(last_st!=src.size())     //标记不为最后一个字符
   {
      vect.push_back(src.substr(last_st,string::npos));
   }
   return vect;

}
void rejudge()    //重新判断原输入数据的类别
{
   string temp_string;
   double temp_pro;
   map<string,double> temp_map;  //存放后验概率的临时容器
   cout<<"经过简单贝叶斯算法重新分类的结果如下:"<<endl;
   for(vector<vector<string> >::size_type st_x=1;st_x<vect.size();st_x++)  //处理每一行数据
   {
     for(map<string,int>::iterator it=category_bak.begin();it!=category_bak.end();it++)  //遍历类别,取出p(x|c1)和p(x|c2)等的概率值
     {
       temp_pro=1.0;
       temp_string=vect[0][vect[0].size()-1]+"="+it->first;
       temp_pro*=pro_map[temp_string];      //乘上p(ci)
       temp_string.erase();
       for(vector<string>::size_type st_y=0;st_y<vect[st_x].size();st_y++)  //处理列
       {
          if(it==category_bak.begin()&&st_y!=vect[st_x].size()-1)   //不输出原始数据已有的类别,使用预测出来的类别(只输出一次)
          {
             cout<<vect[st_x][st_y]<<" ";
          }
          if(st_y!=vect[st_x].size()-1)    //乘上p(xi|cj),跳过最后一列,因为是类别而非属性
          {
             temp_string=vect[0][st_y]+"="+vect[st_x][st_y]+"|"+vect[0][vect[0].size()-1]+"="+it->first;
             temp_pro*=pro_map[temp_string];   //乘上p(xi|cj)
             temp_string.erase();
          }
       }
       temp_map[it->first]=temp_pro;  //存下概率
     }
     //////////根据概率最大判断哪个该条记录应属于哪个类别
     string temp_string2;
     temp_pro=0;   //初始化概率为0
     cout<<"\t后验概率:";
     for(map<string,double>::iterator it2=temp_map.begin();it2!=temp_map.end();it2++)  //遍历容器,找到后验概率最大的类别
     {
       cout<<it2->first<<":"<<it2->second<<"\t";
       if(it2->second>temp_pro)
       {
          temp_string2.erase();
          temp_string2=it2->first;
          temp_pro=it2->second;
       }
     }
     cout<<"\t归类:"<<vect[0][vect[0].size()-1]<<"="<<temp_string2<<endl;  //输出该条记录所属的类别
   }
} 



感想:

Elegance!
这是给完大棒给萝卜的节奏?先不论本算法的代码本身比较短,代码组织的方式也是比隔壁几个算法的实现版本不知道高到哪里去。
这个朴素贝叶斯分类器本身不难,主要只是做一些简单的数据统计,然后算算条件概率,最后生成分类器来预测,指导分类。加上良心的代码风格和足量的注释,我第一次感觉不到我的注释有多少存在的意义。
既然如此我就着重讲讲对一些理论知识的理解吧:

朴素贝叶斯分类器:

  1. 贝叶斯公式是整个算法的核心,也是统计学必修公式,是前置技能。
  2. 贝叶斯决策:在X的条件下,对于所有的事件Ci与Cj(i≠j),都有P(Ci|X)>P(Cj|X),则认为X为类别Ci。换成大白话,事件X发生后,有一个事件Ci发生的概率比其他所有事件都要高,那我们可以理解为Ci和X之间肯定有py交易,很可能Ci是X的小号,所以P(Ci|X)比较高。用这样的指标来衡量分类。
  3. 极大后验假设:这里主要是通过一些公式和一个重要的假设:假设每一个类别都有相同的先验概率,来化简我们对2提到的P(Ci|X)的计算。推导过程大致如下:

    =>P(Ci|X)
    =>P(X)与假设Ci无关所以可以去掉
    =>P(X|Ci)*P(Ci)
    =>假设每一个类别(Ci)都有相同的先验概率
    =>只需计算MAX(P(X|Ci))即可
    由于P(X|Ci)【即后验概率】我们可以通过已有的数据算得,所以反推可得用后验概率来反映P(Ci|X)的情况

  4. 缺点,书上自己也写得很清楚了,朴素贝叶斯分类器的最大前提是分类属性间相互独立这个设定。现实世界中大部分因素都是相互联系的,so。。。。。【摊手】

贝叶斯信念网(BBN)

  1. 这个贝叶斯信念网很有意思,他的大前提是建立在主观的经验之上,来构建一个有向无环图。这里加入了主观的因素,虽然说一般主观因素都会有偏差,报道出错是要负责任的,但是一些专家的主观经验,或者一些知识,都可以加入到BBN当中。打个比方,目前学到的其他算法是给你一堆原生数据,只知道寻找规律的方法,不知道一些局部的结论,通过不断的“学习”和“挖掘”,你可以得到整体的一些结论;而BBN给我感觉就是,一开始就有老师“教给你”一些知识了,但都是很零散的,你需要自己构建知识体系去完善整体的结论。
  2. 算法的大抵过程:给所有因素排个序,对与每一个因素Vi,进行P(Vi|V0~Vi-1)的化简,化简的依据是BBN的性质【P107】:BBN中的一个结点,如果它的父母节点已知,则它条件独立于它所有的非后代节点。
    注意是非后代节点,很严谨的 非(后代节点),祖先也不行。然后就会得到一些条件概率表达式,根据将表达式左边和右边的因素连接起来(参照书本P109的过程和P108的图),就可以建立起一个BBN了

最后说几句:

如果后面的代码都能保持这一份的质量,我也没啥好吐槽的了,但是,稍微翻了一下,,,,,预计一波黑线正在路上赶来。。。
幸亏这朴素贝叶斯很短,也不愧我一天内挤点时间就看完并且在睡觉前写完BLOG,好晚了,今天就先到这里吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章