基于C++语言的决策树实现

   感觉好久都没有写过程序了,一直上课没有时间。最近有点空,然后就写了下西瓜书中的决策树的实现。由于本人才疏学浅,采用的实现方式和数据结构可能不合理,没有考虑代码的复杂度和时间复杂度等等,仅写下自己的实现想法(大神们就打扰了)。该程序是基于C++语言来实现的,算法就是西瓜书上面的实现算法,采用最简单的ID3算法,用信息增益来选择最优划分,进而进行决策树的实现(没有对决策树进行剪枝操作,以后有时间再改进)。

1、基本概念

决策树的详细算法我就不介绍了,具体参看西瓜书。在这里,我想写一下算法的大体。首先说明一下属性、属性值、属性集、类别、训练集的概念。

1、属性:属性就是用来描述物体特征的量。比如:描述一个西瓜可以用色泽、根蒂、敲声、纹理、脐部、触感等等属性来说明一个西瓜是说明样子的。

2、属性值:属性值是属性的取值。比如:一个西瓜的色泽可以用青绿、乌黑、浅白来描述,这些就是西瓜的色泽属性的属性值。

3、属性集:属性集是物体所有属性的集合。顾名思义,就是上述列出的属性打包在一起组成的集合。

4、类别:物体的种类,决策树的目的就是根据属性来对物体进行分类。比如:西瓜有好瓜和坏瓜。

5、训练集:训练集就是样本集,所有的学习都需要通过数据来形成,决策树也是一样的。需要通过样本来形成一棵决策树。训练集必须包括物体各个属性的属性值和类别。

然后说明下算法的三种情况:

设训练集为D,属性集为A,需要生成树的节点。

1、若训练集D中的全部样本的类别都是一样的,比如都是好瓜或者都是坏瓜。这时候将节点的值标记为样本的类别。

2、若属性集A中的属性为空,即树的分支已经到底了,样本的所有属性已经用完,这时候需要将节点标记为叶节点。或者有训练集D中的所有样本,在属性集A上的取值是一样的。比如属性集A={色泽、根蒂、敲声},而训练集D={{青绿、蜷缩、浊响、清晰、凹陷、硬滑、好瓜}、{青绿、蜷缩、浊响、稍糊、稍凹、软粘、好瓜}、{青绿、蜷缩、浊响、模糊、凹陷、软粘、坏瓜}},这时可以看出,训练集D在属性集A上的取值都为:青绿、蜷缩、浊响。这两种情况需要将节点标记为叶节点,叶节点的属性值为训练集中类别最多的类。如上述例子,就需要将叶节点标记为好瓜。

3、在第三步之前需要先从属性集A中选择一个最优划分属性a,如何选择最优属性a,下面再说明。

假设选择最优属性a的值的集合为{b1、b2、b3}。例如,假设选择的属性是a=色泽,而其对应的属性集为b={青绿、乌黑、浅白}。

这时候,我们需要对每个属性值从训练集D中划分出属性a的取值为b1(b2、b3)的子集c。例如,我们的训练集D={{青绿、蜷缩、浊响、清晰、凹陷、硬滑、好瓜}、{青绿、蜷缩、浊响、稍糊、稍凹、软粘、好瓜}、{青绿、蜷缩、浊响、模糊、凹陷、软粘、坏瓜}},然后假设我们根据属性“纹理”=“凹陷”进行划分,则子集c={{青绿、蜷缩、浊响、清晰、凹陷、硬滑、好瓜}、{青绿、蜷缩、浊响、模糊、凹陷、软粘、坏瓜}}。

然后产生一个节点,(1)若子集c为空,则将该节点标记为叶节点,标记的类别为训练集D中类别最多的类。(2)若c不为空,则将属性a从属性集A中剔除,将剩下的属性集合子集c,从第一步开始继续划分(即递归)。

下面说明最优属性a是如何划分的。首先是信息熵的概念,信息熵的定义如下:

                                            Ent(D)=-\sum_{k=1}^{n}p_{k}log_{2}p_{k} 

其中,n为训练集D的类别数(如子集中的类别为好瓜、坏瓜,则n=2)。pk为第k个类别的样本在训练集中的比例。并规定若p_{k}=0,则p_{k}log_{2}p_{k}=0

有了信息熵就可以写出信息增益了,信息增益定义为:

                                            Gain(D,a)=Ent(D)-\sum_{v=1}^{V}\frac{\mathrm{D_{v}} }{\mathrm{D} }Ent(D_{v})

其中,V为属性a的可能取值个数,Dv为属性a对应的属性值划分出来的训练子集(比如上面提到的c子集),D为训练集。

该算法划分最优属性a是根据信息增益来划分的,即信息增益越大,说明以a作为下一个属性来生成决策树最佳。

 举个例子以便理解。

若训练集D={{青绿 蜷缩 浊响 清晰 凹陷 硬滑 好瓜} {乌黑 蜷缩 沉闷 清晰 凹陷 硬滑 好瓜 } {青绿 蜷缩 沉闷 稍糊 稍凹 硬滑 坏瓜}},则n=2,信息熵为

                                             Ent(D)=-\left ( \frac{2}{3}*log_{2} \frac{2}{3}+\frac{1}{3}*log_{2}\frac{1}{3}\right )

以计算"色泽"信息增益为例(D中色泽属性值有:青绿*2,乌黑*1):

                      Gain(D,a)=Ent(D)+(\frac{2}{3}*\left (\frac{1}{2} *log_{2}\frac{1}{2}+\frac{1}{2} *log_{2}\frac{1}{2}\right )+\frac{1}{3}\left (1*log_{2}1+0\right ))

2、C++算法实现

首先我把西瓜书上的训练集列出来,对理解程序有帮助。

                        

(1)输入函数

输入函数我没有在类内定义,而是直接在主函数中定义,因为我感觉这样比较好,输入与类分开。我们可以看到,数据是非常多的,有属性以及各个样本的各属性对应的属性值。我这里是主要采用map这个结构来对这张表进行存储。废话不多说,直接上程序:

//全局变量
//定义属性数组,存放可能的属性,包括类别
vector<string> data_Attributes;//对于本数据集来说就是:色泽 根蒂 敲声 纹理 脐部 触感 类别
//定义各属性对应的属性值
map<string, vector<string>> data_AttValues;//比如:色泽={青绿 乌黑 浅白}
//定义剩余属性,不包括类别(这个主要用于后面算法的递归)
vector<string> remain_Attributes;//色泽 根蒂 敲声 纹理 脐部 触感
//定义数据表,属性-属性值(全部数据的属性值放在同一个数组)
map<string, vector<string>>data_Table;//整张表


//输入数据生成数据集
void data_Input()
{
	//输入属性(色泽 根蒂 敲声 纹理 脐部 触感 好瓜)
	string input_Line,temp_Attributes;
	cout << "请输入属性:" << endl;
	//获取一行数据,然后绑定到数据流istringstream
	getline(cin, input_Line);
	istringstream input_Attributes(input_Line);
	//将数据流内容(空格不输出)输入数据属性数组中
	while (input_Attributes >> temp_Attributes)
	{
		data_Attributes.push_back(temp_Attributes);
	}
	//剔除类别这个属性
	remain_Attributes = data_Attributes;
	remain_Attributes.pop_back();
	//定义样本数量
	int N = 0;
	cout << "请输入样本数量:" << endl;
	cin >> N;
	cin.ignore();//清空cin缓冲区中的留下的换行符
	//输入数据(属性值)
	cout << "请输入样本:" << endl;
	//一共N个训练样本
	for (int j = 0; j < N; j++)
	{
		string temp_AttValues;
		//获取一行属性值输入
		getline(cin, input_Line);
		istringstream input_AttValues(input_Line);
		//将各属性值输入到数据表data_table中
		for (int i = 0; i < data_Attributes.size(); i++)
		{
			input_AttValues >> temp_AttValues;
			data_Table[data_Attributes[i]].push_back(temp_AttValues);
		}
	}

	//生成各属性对应的属性值集的映射data_AttValues
	for (int i = 0; i < data_Attributes.size(); i++)
	{
        //通过set结构来统计所有样本中各属性对应的属性值的所有可能的取值
        //如:“色泽”的可能取值为:青绿 乌黑 浅白
		set<string> attValues;
		for (int j = 0; j < N; j++)
		{
            //注意:data_Attributes[i]代表某个属性
            //而data_Table[data_Attributes[i]]是一个数组
			string temp = data_Table[data_Attributes[i]][j];
			//若有重复属性值,set是不会插入的
			attValues.insert(temp);
		}
		for (set<string>::iterator it = attValues.begin(); it != attValues.end(); it++)
		{
            //将所有可能的属性值存入data_AttValues[data_Attributes[i]]
			data_AttValues[data_Attributes[i]].push_back(*it);
		}
		
	}
}

(2)决策树节点类的设计

决策树类需要包含的东西挺多的,成员变量主要有

样本数据集的属性个数:attribute_Num

本节点的属性:node_Attribute

本节点属性对应的所有可取的属性值:node_AttValues

数据集的属性:data_Attribute

从根节点到本节点未被用于最优划分属性的属性集:remain_Attributes

本节点属性对应的属性值与子节点的地址的映射集,即本节点属性取属性值后下一个节点的地址的集合(有多个属性值,所以有多个不同的地址):childNode(为空说明该节点是叶节点)

该节点对应的样本的数据表:MyDateTable

各属性对应的属性值(外部传进来的,对后面操作有作用):data_AttValues

成员函数主要有

计算信息熵函数:calc_Entropy()

计算信息增益并寻找最优划分属性a:findBestAttribute()

生成本节点的子节点:generate_ChildNode()

设置节点的属性:set_NodeAttribute()

训练成完整的决策树后,可根据所给样本的属性,预测出该样本的类别:findClass()

class Tree_Node
{
public:
	//构造函数,参数依次为:数据集表(西瓜数据表)、西瓜所有的属性包括类别、每个属性可能的取值构成的表、剩余的未被划分的属性
	Tree_Node(map<string, vector<string>> temp_Table, vector<string> temp_Attribute,map<string, vector<string>> data_AttValues, vector<string> temp_remain);
	//生成子节点
	void generate_ChildNode();
	//计算信息增益 寻找最优划分属性
	string findBestAttribute();
	//计算信息熵
	double calc_Entropy(map<string, vector<string>> temp_Table);
	//设置节点的属性
	void set_NodeAttribute(string atttribute);
	//根据所给属性,对数据进行分类
	string findClass(vector<string> attributes);
	virtual ~Tree_Node();
private:
	//属性个数,不包括类别
	int attribute_Num;
	//本节点的属性
	string node_Attribute;
	//数据集属性
	vector<string> data_Attribute;
	//本节点的所有属性值
	vector<string> node_AttValues;
	//剩余属性集
	vector<string>remain_Attributes;
	//子节点,本节点属性对应的属性值与子节点地址进行一一映射
	//为空说明该节点为叶节点
	map<string, Tree_Node *> childNode;
	//样本集合表
	map<string, vector<string>> MyDateTable;
	//定义各属性对应的属性值
	map<string, vector<string>> data_AttValues;
};

(3)类的实现

首先是类的构造函数,构造函数主要是对类的成员变量进行初始化:

Tree_Node::Tree_Node(map<string, vector<string>> temp_Table,vector<string> temp_Attribute, map<string, vector<string>> data_AttValues, vector<string> temp_remain)
{
	//全部属性,包括类别
	data_Attribute = temp_Attribute;
	//属性个数,不包括类别
	attribute_Num = (int)temp_Attribute.size() - 1;
	//各属性对应的属性值
	this->data_AttValues = data_AttValues;
	//属性表
	MyDateTable = temp_Table;
	//剩余属性集
	remain_Attributes = temp_remain;
}

然后是计算信息熵的成员函数的实现:

//计算信息熵
double Tree_Node::calc_Entropy(map<string, vector<string>> temp_Table)
{
	map<string, vector<string>> table = temp_Table;
	//数据集中样本的数量
	int sample_Num = (int)temp_Table[data_Attribute[0]].size();
	//计算数据集中的类别数量
	map<string, int> class_Map;
	for (int i = 0; i < sample_Num; i++)
	{
		//data_Attribute[attribute_Num]对应的就是数据集的类别
		string class_String = table[data_Attribute[attribute_Num]][i];
		class_Map[class_String]++;
	}

	map<string, int>::iterator it = class_Map.begin();
	//存放类别及其对应的数量
	//vector<string> m_Class;
	vector<int> n_Class;
	
	for (; it != class_Map.end(); it++)
	{
		//m_Class.push_back(it->first);
		n_Class.push_back(it->second);
	}
	//计算信息熵
	double Ent = 0;
	for (int k = 0; k < class_Map.size(); k++)
	{
		//比例
		double p = (double) n_Class[k] / sample_Num;
		if (p == 0)
		{
			//规定了p=0时,plogp=0
			continue;
		}
		//c++中只有log和ln,因此需要应用换底公式
		Ent -= p * (log(p) / log(2));//信息熵
	}
	
	return Ent;
}

接下来实现信息增益的计算以及寻找出最优的划分属性:

//寻找最优划分
string Tree_Node::findBestAttribute()
{
	//样本个数
	int N = (int)MyDateTable[data_Attribute[0]].size();
	//定义用于存放最优属性
	string best_Attribute;
	//信息增益
	double gain = 0;
	//对每个剩余属性
	for (int i = 0; i < remain_Attributes.size(); i++)
	{
		//定义信息增益,选取增益最大的属性来划分即为最优划分
		double temp_Gain = calc_Entropy(MyDateTable);//根据公式先将本节点的信息熵初始化给增益
		//对该属性的数据集进行分类(获取各属性值的数据子集)
		string temp_Att = remain_Attributes[i];//假设选取的属性
		vector<string> remain_AttValues;//属性可能的取值
		for (int j = 0; j < data_AttValues[temp_Att].size(); j++)
		{
			remain_AttValues.push_back(data_AttValues[temp_Att][j]);
		}
		
		//对每个属性值求信息熵
		for (int k = 0; k < remain_AttValues.size(); k++)
		{
			//属性值
			string temp_AttValues = remain_AttValues[k];
			int sample_Num = 0;//该属性值对应样本数量
			//定义map用来存放该属性值下的数据子集
			map<string, vector<string>>sub_DataTable;
			for (int l = 0; l < MyDateTable[temp_Att].size(); l++)
			{
				if (temp_AttValues == MyDateTable[temp_Att][l])
				{
					sample_Num++;
					//将符合条件的训练集存入sub_DataTable
					for (int m = 0; m < data_Attribute.size(); m++)
					{
						sub_DataTable[data_Attribute[m]].push_back(MyDateTable[data_Attribute[m]][l]);
					}
				}
			}
			//累加每个属值的信息熵
			temp_Gain -= (double)sample_Num / N * calc_Entropy(sub_DataTable);
		}
		//比较寻找最优划分属性
		if (temp_Gain > gain)
		{
			gain = temp_Gain;
			best_Attribute = temp_Att;
		}		
	}

	return best_Attribute;
}

然后是实现如何生成子节点的成员函数,在这之前先实现一个设置节点的属性的函数,该函数主要用于设置子节点的节点属性:

void Tree_Node::set_NodeAttribute(string attribute)
{
	//设置节点的属性
	this->node_Attribute = attribute;
}

生成子节点,生成方式就是按照西瓜书上面的算法一步一步实现即可,代码如下:

void Tree_Node::generate_ChildNode()
{
	//样本个数
	int N = (int)MyDateTable[data_Attribute[0]].size();
	
	//将数据集中类别种类和数量放入map里面,只需判断最后一列即可
	map<string,int> category;
	for (int i = 0; i < N; i++)
	{	
		vector<string> temp_Class;
		temp_Class = MyDateTable[data_Attribute[attribute_Num]];
		category[temp_Class[i]]++;
	}

	//第一种情况
	//只有一个类别,标记为叶节点
	if (1 == category.size())
	{
		map<string, int>::iterator it = category.begin();
		node_Attribute = it->first;
		return;
	}
	//第二种情况
	//先判断所有属性是否取相同值
	bool isAllSame = false;
	for (int i = 0; i < remain_Attributes.size(); i++)
	{
		isAllSame = true;
		vector<string> temp;
		temp = MyDateTable[remain_Attributes[i]];
		for (int j = 1; j < temp.size(); j++)
		{
			//只要有一个不同,即可退出
			if (temp[0] != temp[j])
			{
				isAllSame = false;
				break;
			}
		}
		if (isAllSame == false)
		{
			break;
		}
	}
	//若属性集为空或者样本中的全部属性取值相同
	if (remain_Attributes.empty()||isAllSame)
	{
		//找出数量最多的类别及其出现的个数,并将该节点标记为该类
		map<string, int>::iterator it = category.begin();
		node_Attribute = it->first;
		int max = it->second;
		it++;
		for (; it != category.end(); it++)
		{
			int num = it->second;
			if (num > max)
			{
				node_Attribute = it->first;
				max = num;
			}
		}
		return;
	}
	//第三种情况
	//从remian_attributes中划分最优属性
	string best_Attribute = findBestAttribute();
	//将本节点设置为最优属性
	node_Attribute = best_Attribute;
	//对最优属性的每个属性值
	for (int i = 0; i < data_AttValues[best_Attribute].size(); i++)
	{
		string best_AttValues = data_AttValues[best_Attribute][i];
		//计算属性对应的数据集D
		//定义map用来存放该属性值下的数据子集
		map<string, vector<string>> sub_DataTable;
		for (int j = 0; j < MyDateTable[best_Attribute].size(); j++)
		{
			//寻找最优属性在数据集中属性值相同的数据样本
			if (best_AttValues == MyDateTable[best_Attribute][j])
			{
				//找到对应的数据集,存入子集中sub_DataTable(该样本的全部属性都要存入)
				for (int k = 0; k < data_Attribute.size(); k++)
				{
					sub_DataTable[data_Attribute[k]].push_back(MyDateTable[data_Attribute[k]][j]);
				}
			}
		}
		//若子集为空,将分支节点(子节点)标记为叶节点,类别为MyDateTable样本最多的类
		if (sub_DataTable.empty())
		{
			//生成子节点
			Tree_Node * p = new Tree_Node(sub_DataTable, data_Attribute, data_AttValues, remain_Attributes);
			//找出样本最多的类,作为子节点的属性
			map<string, int>::iterator it = category.begin();
			string childNode_Attribute = it->first;
			int max_Num = it->second;
			it++;
			for (; it != category.end(); it++)
			{
				if (it->second > max_Num)
				{
					max_Num = it->second;
					childNode_Attribute = it->first;
				}
			}
			//设置子叶节点属性
			p->set_NodeAttribute(childNode_Attribute);
			//将子节点存入childNode,预测样本的时候会用到
			childNode[best_AttValues] = p;
		}
		else//若不为空,则从剩余属性值剔除该属性,调用generate_ChildNode继续往下细分
		{
			vector<string> child_RemainAtt;
			child_RemainAtt = remain_Attributes;
			//找出child_RemainAtt中的与该最佳属性相等的属性
			vector<string>::iterator it = child_RemainAtt.begin();
			for (; it != child_RemainAtt.end(); it++)
			{
				if (*it == best_Attribute)
				{
					break;
				}
			}
			//删除
			child_RemainAtt.erase(it);

			//生成子节点
			Tree_Node * pt = new Tree_Node(sub_DataTable, data_Attribute, data_AttValues, child_RemainAtt);
			//将子节点存入childNode
			childNode[best_AttValues] = pt;
			//子节点再调用generate_ChildNode函数
			pt->generate_ChildNode();
		}
	}

}

在最后,我们必须有个预测函数,即如果输入一个样本,需要给出该样本的是什么类别的。比如:给出西瓜数据:青绿 蜷缩 浊响 清晰 凹陷 硬滑,则调用该函数应该输出:“好瓜”。该函数实现如下:

//输入为待预测样本的所有属性集合
string Tree_Node::findClass(vector<string> attributes)
{
	//若存在子节点
	if (childNode.size() != 0)
	{
		//找出输入的样例中与本节点属性对应的属性值,以便寻找下个节点,直到找到叶节点
		string attribute_Value;
		for (int i = 0; i < data_AttValues[node_Attribute].size(); i++)
		{
			for (int j = 0; j < attributes.size(); j++)
			{
				//data_AttValues[node_Attribute]为属性node_Attribute对应的所有可能的取值集合
				if (attributes[j] == data_AttValues[node_Attribute][i])
				{
					//找到了样例对应的属性值
					attribute_Value = attributes[j];
					break;
				}
			}
			//找到后就没必要继续循环了
			if (!attribute_Value.empty())
			{
				break;
			}
		}
		//找出该属性值对应的子节点的地址,以便进行访问
		Tree_Node *p = childNode[attribute_Value];
		return p->findClass(attributes);//递归寻找,直到找到叶节点为止
	}
	else//不存在子节点说明已经找到分类,类别为本节点的node_Attribute
	{
		return node_Attribute;
	}
}

3、预测结果

先放上我的主函数,主函数主要是调用函数,然后格式化输出。

int main()
{
	//输入
	data_Input();
	Tree_Node myTree(data_Table, data_Attributes, data_AttValues, remain_Attributes);
	//进行训练
	myTree.generate_ChildNode();
	//输入预测样例,进行预测
	vector<string> predict_Sample;
	string input_Line, temp;
	cout << "请输入属性进行预测:" << endl;
	getline(cin, input_Line);
	istringstream input_Sample(input_Line);
	while (input_Sample >> temp)
	{
		//将输入预测样例的属性都存入predict_Sample,以便传参
		predict_Sample.push_back(temp);
	}
	cout << endl;
	//预测
	cout << "分类结果为:" << myTree.findClass(predict_Sample) << endl;
	system("pause");
	return 0;
}

运行结果:

可以看出,预测结果是正确的。但真正的预测应该不能取样本中已有的样例,我这样做是为了验证程序的正确性。

好了,文章到此就结束了。关于程序的相关优化和剪枝操作以后有时间再来完善。下面附上源程序代码供下载:https://download.csdn.net/download/m0_37543178/10793382。(ps:没有积分的可以直接找我给你源代码:))

猜你喜欢

转载自blog.csdn.net/m0_37543178/article/details/84202045