這個學期要學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!
這是給完大棒給蘿蔔的節奏?先不論本算法的代碼本身比較短,代碼組織的方式也是比隔壁幾個算法的實現版本不知道高到哪裏去。
這個樸素貝葉斯分類器本身不難,主要只是做一些簡單的數據統計,然後算算條件概率,最後生成分類器來預測,指導分類。加上良心的代碼風格和足量的註釋,我第一次感覺不到我的註釋有多少存在的意義。
既然如此我就着重講講對一些理論知識的理解吧:
樸素貝葉斯分類器:
- 貝葉斯公式是整個算法的核心,也是統計學必修公式,是前置技能。
- 貝葉斯決策:在X的條件下,對於所有的事件Ci與Cj(i≠j),都有P(Ci|X)>P(Cj|X),則認爲X爲類別Ci。換成大白話,事件X發生後,有一個事件Ci發生的概率比其他所有事件都要高,那我們可以理解爲Ci和X之間肯定有py交易,很可能Ci是X的小號,所以P(Ci|X)比較高。用這樣的指標來衡量分類。
極大後驗假設:這裏主要是通過一些公式和一個重要的假設:假設每一個類別都有相同的先驗概率,來化簡我們對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)的情況缺點,書上自己也寫得很清楚了,樸素貝葉斯分類器的最大前提是分類屬性間相互獨立這個設定。現實世界中大部分因素都是相互聯繫的,so。。。。。【攤手】
貝葉斯信念網(BBN)
- 這個貝葉斯信念網很有意思,他的大前提是建立在主觀的經驗之上,來構建一個有向無環圖。這裏加入了主觀的因素,雖然說一般主觀因素都會有偏差,報道出錯是要負責任的,但是一些專家的主觀經驗,或者一些知識,都可以加入到BBN當中。打個比方,目前學到的其他算法是給你一堆原生數據,只知道尋找規律的方法,不知道一些局部的結論,通過不斷的“學習”和“挖掘”,你可以得到整體的一些結論;而BBN給我感覺就是,一開始就有老師“教給你”一些知識了,但都是很零散的,你需要自己構建知識體系去完善整體的結論。
- 算法的大抵過程:給所有因素排個序,對與每一個因素Vi,進行P(Vi|V0~Vi-1)的化簡,化簡的依據是BBN的性質【P107】:BBN中的一個結點,如果它的父母節點已知,則它條件獨立於它所有的非後代節點。
注意是非後代節點,很嚴謹的 非(後代節點),祖先也不行。然後就會得到一些條件概率表達式,根據將表達式左邊和右邊的因素連接起來(參照書本P109的過程和P108的圖),就可以建立起一個BBN了
最後說幾句:
如果後面的代碼都能保持這一份的質量,我也沒啥好吐槽的了,但是,稍微翻了一下,,,,,預計一波黑線正在路上趕來。。。
幸虧這樸素貝葉斯很短,也不愧我一天內擠點時間就看完並且在睡覺前寫完BLOG,好晚了,今天就先到這裏吧。