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,好晚了,今天就先到這裏吧。

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