学习数据挖掘决策树ID3算法

一个月前的C语言程序设计课上学习了决策树ID3算法
然后自己用了两个多星期的时间开始用C语言实现,结果由于过程太过于复杂,写出来的东西就跟屎一样。
可能是自己对于这个算法理解的不够深刻,或者是在设计的时候没有构思好。
所以决定在这里写一写大概的构思然后再去用C实现。
这样可能会更加有效率一点。



决策树之ID3算法:


ID3算法的实质是检索哪个属性的分类能力更强,然后用拿个分类能力强的属性将数据分类,然后继续检索继续分类。

这样最后分完之后就会是一个树的结构。

分到最后的时候数据会到达一定的纯度。

之后你拿一个样本过来,顺着这个树往下找,找到它所属于的那一堆数据里,也就是叶节点上。

就能根据这一堆数据生和死的比例来对样本进行判断。


如何判断一个属性的分类能力?


根据香农的信息论。香农定义了一个名叫信息熵的东西,来说明一个系统的信息稳定程度。

一个系统的信息熵越小,那么用它分类出的系统就越稳定,它的分类能力就比较强。


信息熵的计算公式:(n1 / m) * log2(n1 / m) + (n2 / m) * log2(n2 / m) + ........... +  (nn / m) * log2(nn / m)



具体的构建步骤:(以泰坦尼克号的数据为训练模型)


训练数据的结构:(第一列是生死标签)

学习数据挖掘决策树ID3算法_第1张图片

//上一节课老师跟我说可以不用树的结构去构建数据结构,但是我实在不知道该怎么写,在他的口中用所谓字符数组的结构去做要比我这个简单很多。

//所以在这里还是采用我以前想的那个树的结构去做。


第一步,我们需要有一个函数来读取文件数据,我们选择一个二维数组去储存。
我觉得这个函数还是不错的。

int getData()//获取数据
{
	FILE *p;
	if (auto err = fopen_s(&p, "C:\\Users\\XueChuanyu\\Desktop\\test.txt", "r") == NULL)
	{
		printf("\nNo sucn a file");
	}

	int i = 0, k = 0;
	for (i = 0; i < DATASIZE; i++)
	{
		for (k = 0; k < 8; k++)
		{
			fscanf_s(p, "%lf", &data[i][k]);
			if (k == 3)
			{
				disperseAge(data[i][3], 15);
			}
			if (k == 6)
			{
				disperseAge(data[i][6], 70);
			}
		}
	}

	fclose(p);

	return 0;

}


但是我们有 很多数据都是连续的数据,所以要先进行离散化
这个函数也还算能用的。

double disperseAge(double x, int different)// 根据离散间隔值离散化
{

	if (x < different)
	{
		x = 0;
		return x;
	}
	else
	{
		if (different <= x && x <= 2 * different)
		{
			x = 1.0;
			return x;
		}
		else
		{
			if (2 * different < x && x <= 3 * different)
			{
				x = 2.0;
				return x;
			}
			else
			{
				x = 3.0;
				return x;
			}
		}
	}
}

下一步我们需要有计算熵的函数,之前写的东西写之前还觉得写的挺好的,但是写完了了以后一个星期看根本看不懂了。
事实上我觉得是因为数据结构没想好就写,然后写了之后发现不合适,所以代码都看不懂了。
所以现在先来模拟一下该如何去做。
首先这个计算熵的函数必须具有通用性,也就是在每一次进行分类的时候都能用,所以这个检索也就变成了在某一个数据集里检索某个属性的熵。

所以这就要求我们必须在每个节点能够知道目前此节点的数据集是什么,所以我们的节点至少是这样的。


struct node
{
	int includeData[1000];
};


2017.04.10

今天上午上课的时候测试了一下自己之前写的那个在某个节点判断熵值的函数,发现还是可以用的,用那个训练集可以大概算出来在第一个节点的时候(也就是包含所有数据的节点)性别是最好的分类依据,虽然实现的效率和方法不是太好,但是毕竟也已经写出来了。自己也懒得改了,这样查找熵值的工作就完成了,下一步就只剩下根据训练集建树了。


(老师上课讲,当初没考虑到,现在自己试试用C去实现,确实很复杂,可能没个1000行写不出来,如果你不用各种库函数会相当痛苦)

然而我已经痛苦好几个星期了,已经麻木。

      自己的队友已经放弃C,去用C++这种高级魔法去做了。

     但我觉得我的代码还能再抢救一下。


下面贴出实现数据检索和熵值计算的函数。


检索在某一个节点(数据集)中某一属性下满足某个元素时另一属性中某元素的数量

这个函数是直接从之前的朴素贝叶斯算法中拉过来的,感觉用在这个模型上有点牵强了,感觉就是这个检索拖累了我。
这个函数的优势是可以检索单一属性中的元素,也可以同时检索两个属性,但是可能感觉有点难理解。

int checker(node *p, int typeSon, int typeDad, int sampleSon, int sampleDad)//在某节点中某属性下某属性的数量
{
	int result = 0, i = 0;
	for (i = 0; i < DATASIZE; i++)
	{
		if (p->typeNumber[i] != 0 && data[i][typeSon] == sampleSon && data[i][typeDad] == sampleDad)
		{
			result++;
		}
	}

	return result;
}


计算在某个节点(数据集)中某个特定属性的熵值

计算在某个节点的熵值,前面的这个检索某属性的种类,为的是能够提高程序的通用性。

类似于建立一个元素列表吧,告诉你这个属性下有什么元素。

但是实际上针对于这个数据其实可以完全用个一维数组。。。前面那部分弄麻烦了,但是也能用。

double entropy(node *p, int type)//计算某节点中某类别的熵值
{
	int property[DATASIZE][3] = {0};//property[某种属性][0:总数,1:对应存活,2:对应死亡]
							 //用于记录各属性及其对应值
	int dataSize = 0;
	int flag = 1;
	double entropy = 0;

	for (int i = 0; i < DATASIZE - 1; i++)//知道类型下属性的种类
	{
		if (flag && p->typeNumber[i] != 0)
		{
			int temp = data[i][type];
			property[temp][0]++;
			flag = 0; // 获得首个数据
		}
		else
		{
			continue;
		}

		if (p->typeNumber[i] != 0 && data[i][type] != data[i + 1][type])
		{
			int temp = data[i + 1][type];
			property[temp][0]++;//如果和前面的数据不同 再添加一个类型
		}
	}

	for (int i = 0; i < 8; i++)// 这个i有属性值的作用
	{
		if (property[i][0] != 0)
		{
			property[i][0] = checker(p, type, type, i, i);//每种属性的数量
			dataSize += property[i][0];
			property[i][1] = checker(p, type, 0, i, 1);//在此属性下生存的数量
			property[i][2] = checker(p, type, 0, i, 0);//在此属性下死亡的数量
		}
	}
	
	for (int i = 0; i < 8; i++)//计算熵
	{
		if (property[i][0] != 0)
		{
			entropy -= ((double)property[i][0] / dataSize) * (((double)property[i][1] / property[i][0]) * (log2((double)property[i][1] / property[i][0])));
		}
	}

	return entropy;

}

比较在某节点下所有属性的分类能力

一个小函数,没啥可说的。

int compare(node *p)//在某个节点下寻找最优划分类型
{
	int i = 0, result = 0;
	double lastType = 1, nowType = 1;

	for (i = 1; i < 8; i++)
	{
		nowType = entropy(p, i);
		
		if (nowType < lastType)
		{
			lastType = nowType;
			result = i;
		}
	}

	return result;
}

考虑了一下,作为一个树的结构,没个节点至少提供三个信息


1.该节点包含的数据集
2.记录该节点是以什么分类的
3.该节点上挂载的子节点


所以做了这样的节点

struct node
{
	int typeNumber[1000];
	double entropy;
	node *pNode[MAXTYPE];
};

如何建一棵树?



首先需要一个建立节点的函数:



函数需要得到的信息:


1. 当前所属的节点以及是否为空,以便知道下一步该如何建树。

2. 若当前节点的纯度到达一定程度结束函数。

2. 在当前节点下最优的分类属性(调用compare函数)。

3. 根据分类属性所拥有元素的数量调用建立节点的函数并创建节点(递归实现,该函数要返回一个指针,给上一个节点的指针数组)。


本来是打算用熵值作为结束树的条件的

但是如果用熵找不到好的衡量标准

所以写了这个计算纯度的函数

double getPureValue(node *p)
{
	int dead = 0, alive = 0;

	for (int i = 0; i != DATASIZE; i++)
	{
		if (p->typeNumber[i] == 1)
		{
			if (data[i][0] == 0)
			{
				dead++;
			}
			else
			{
				alive++;
			}
		}
	}
	return (dead > alive) ? (dead / static_cast(dead + alive)) : (alive / static_cast(dead + alive));

}






这里是前天写好的建树。。但是有些小问题。。

学习数据挖掘决策树ID3算法_第2张图片

出现了这样的状况。。。跟平时野指针内存泄漏栈溢出的情况都不一样。。。暂时还没找到解决方法,感觉逻辑还是没什么问题的

如果哪位大神找到错误可以在下面评论。。感觉应该不是爆栈。。



贴下建树码

node* bulidTree(node *pSender, int bestType, int devideCondition)
{
	node *dadP = NULL;

	if (pSender == NULL)
	{
		dadP = new node;

		for (auto &i : dadP->typeNumber)
		{
			i = 1;
		}

		int bestType = compare(dadP);
		isUsed[bestType] = true;
		dadP->purity = getPureValue(dadP);

		int nodeAmount = typeAmount(dadP, bestType);

		for (int i = 0; i != nodeAmount; i++)
		{
			dadP->pNode[i] = bulidTree(dadP, bestType, i);
		}
		
	}
	else
	{
		dadP = pSender;
		
		if (dadP->purity > 0.75)
		{
			return NULL;
		}
		else
		{
			node *sonP = new node;
			
			for (int i = 0; i != DATASIZE; i++)
			{
				if (data[i][bestType] == devideCondition && dadP->typeNumber[i] != 0)
				{
					sonP->typeNumber[i] = 1;
				}
				else
				{
					sonP->typeNumber[i] = 0;
				}
			}

			int bestestType = compare(sonP);
			isUsed[bestestType] = true;
			sonP->purity = getPureValue(sonP);

			int nodeAmount = typeAmount(sonP, bestestType);

			for (int i = 0; i != nodeAmount; i++)
			{
				sonP->pNode[i] = bulidTree(sonP, bestestType, i);
			}

			delete dadP;
			return sonP;

		}

	}
}

之前写的那个东西,在调试的时候出现了不停在两个属性循环分类的情况

后来发现ID3算法是不能重复用某个属性的,原理如下

学习数据挖掘决策树ID3算法_第3张图片



所以这个还是未完成吧

不过暂时这个就到这一段落了

下面要学习OpenCV和python

这个以后有时间再慢慢研究了


2017/4/24

建树的问题解决了

主要
之前出现了两个问题

1.之前在函数开始的时候创建的一个指针,把那个指针删掉改为用后面直接用new创建node对象,就没有了已触发了一个断点的东西

   分析可能是以前的那种创建指针的方式,在后来用到了没有开辟空间的指针,也就是用到了一个空指针的情况

2.出现的在建树过程中,纯度不断提高,到最后突然变成66。6%的情况。

   原因是这个ID3选用不同分类属性的时候是不能重复使用分类属性的,所以在后来分类的属性肯定越来越差(好的都被用完了)

   所以用一个超差的分类属性对较少的数据进行分类,结果就是出现这种纯度下降的情况,解决办法就是设置一个根据树的深度停止的判断

   新的代码贴到如下:

node* bulidTree(node *pSender, int bestType, int devideCondition)
{
	if (pSender == NULL)
	{
		node *dadP = new node;

		for (auto &i : dadP->typeNumber)
		{
			i = 1;
		}

		int bestType = compare(dadP);
		isUsed[bestType] = true;
		dadP->purity = getPureValue(dadP);
		dadP->deep = 0;
		int nodeAmount = typeAmount(dadP, bestType);

		for (int i = 0; i != nodeAmount; i++)
		{
			dadP->pNode[i] = bulidTree(dadP, bestType, i);
		}
		
	}
	else
	{
		node *dadP = pSender;
		
		if (dadP->purity > 0.8 || dadP->deep >= 3)
		{
			return NULL;
		}
		else
		{
			node *sonP = new node;
			sonP->deep = dadP->deep + 1;
			
			for (int i = 0; i != DATASIZE; i++)
			{
				if (data[i][bestType] == devideCondition && dadP->typeNumber[i] != 0)
				{
					sonP->typeNumber[i] = 1;
				}
				else
				{
					sonP->typeNumber[i] = 0;
				}
			}

			int bestestType = compare(sonP);
			isUsed[bestestType] = true;
			sonP->purity = getPureValue(sonP);

			int nodeAmount = typeAmount(sonP, bestestType);

			for (int i = 0; i != nodeAmount; i++)
			{
				sonP->pNode[i] = bulidTree(sonP, bestestType, i);
			}

			return sonP;

		}

	}
}


你可能感兴趣的:(DataMining学习笔记)