这个学期要学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,好晚了,今天就先到这里吧。