决策树构造的输入是一组带有类别标记的例子,构造的结果是一棵二叉树或多叉树。二叉树的内部节点(非叶子节点)一般表示为一个逻辑判断,如形式为a=a_j的逻辑判断,其中a是属性,a_j是该属性的所有取值:树的边是逻辑判断的分支结果。多叉树(ID3)的内部结点是属性,边是该属性的所有取值,有几个属性值就有几条边。树的叶子节点都是类别标记。
由于数据表示不当、有噪声或者由于决策树生成时产生重复的子树等原因,都会造成产生的决策树过大。因此,简化决策树是一个不可缺少的环节。寻找一棵最优决策树,主要应解决以下3个最优化问题:①生成最少数目的叶子节点;②生成的每个叶子节点的深度最小;③生成的决策树叶子节点最少且每个叶子节点的深度最小。
ID3算法是一种经典的决策树算法,它从根节点开始,根节点被赋予一个最好的属性。随后对该属性的每个取值都生成相应的分支,在每个分支上又生成新的节点。对于最好的属性的选择标准,ID3采用基于信息熵定义的信息增益来选择内节点的测试属性,熵(Entropy)刻画了任意样本集的纯度。
ID3算法存在的缺点:(1)ID3算法在选择根节点和内部节点中的分支属性时,采用信息增益作为评价标准。信息增益的缺点是倾向于选择取值较多是属性,在有些情况下这类属性可能不会提供太多有价值的信息。(2)ID3算法只能对描述属性为离散型属性的数据集构造决策树。
ID3算法的目的在于减少树的深度。但是忽略了叶子数目的研究。C4.5算法在ID3算法的基础上进行了改进,对于预测变量的缺值处理、剪枝技术、派生规则等方面作了较大的改进,既适合于分类问题,又适合于回归问题。
C4.5算法主要做出了以下方面的改进:
(1) 用信息增益率来选择属性
克服了用信息增益来选择属性时偏向选择值多的属性的不足。信息增益率定义为:
GainRatio(S,A) = Gain(S,A)/ SplitInfo(S,A)
其中,Grain(S,A)与ID3算法中的信息增益相同,而分裂信息SplitInfo(S, A)代表了按照属性A分裂样本集S的广度和均匀性。
其中,S1到Sc是c个不同值的属性A分割S而形成的c个样本子集。如按照属性A把S集(含30个用例)分成了10个用例和20个用例两个集合,则SplitInfo(S,A)=-1/3*log(1/3)-2/3*log(2/3)。
(2) 可以处理连续数值型属性
C4.5算法既可以处理离散型描述属性,也可以处理连续性描述属性。在选择某节点上的分枝属性时,对于离散型描述属性,C4.5算法的处理方法与ID3相同,按照该属性本身的取值个数进行计算;对于某个连续性描述属性Ac,假设在某个节点上的数据集的样本数量为total,C4.5算法将作以下处理:
l 将该节点上的所有数据样本按照连续型描述的属性的具体数值,由小到大进行排序,得到属性值的取值序列{A1c,A2c,……Atotalc}。
l 在取值序列生成total-1个分割点。第i(0<i<total)个分割点的取值设置为Vi=(Aic+A(i+1)c)/2,它可以将该节点上的数据集划分为两个子集。
l 从total-1个分割点中选择最佳分割点。对于每一个分割点划分数据集的方式,C4.5算法计算它的信息增益比,并且从中选择信息增益比最大的分割点来划分数据集。
C4.5采用的是信息增益率(Gain Ratio)作为分裂准则,并且能处理连续属性,在C4.5中,对连续属性的处理如下:
1. 对属性的取值进行排序
2. 两个属性取值之间的中点作为可能的分裂点,将数据集分成两部分,计算每个可能的分裂点的信息增益(InforGain)
3. 对每个分裂点的信息增益(InforGain)就行修正:减去log2(N-1)/|D|
4. 选择修正后信息增益(InforGain)最大的,分裂点作为该属性的最佳分裂点
5. 计算最佳分裂点的信息增益率(Gain Ratio)作为属性的Gain Ratio
6. 选择Gain Ratio最大的属性作为分裂属性
其中,3,4两点在93年Quinlan的C4.5算法中并没有体现,后来Quinlan在96年发表文章,对C4.5进行了改进,这两点是主要的修改。Quinlan的主要理由是数据集中同时出现连续属性和离散属性时,原始的C4.5算法倾向于选择连续的属性作为分裂属性,因此连续属性的信息增益需要减去log2(N-1)/|D|作为修正,其中N为可能的分裂点个数,|D|是数据集大小。第二个修改是,选择最佳分裂点不用信息增益率(Gain Ratio),而用信息增益(Information Gain),然后用最大的信息增益对应的Gain Ratio作为属性的Gain Ratio.
(3) 采用了一种后剪枝方法
避免树的高度无节制的增长,避免过度拟合数据,该方法是用训练样本本身来估计剪枝前后的误差,从而决定是否真正剪枝。方法中使用的公式如下:
其中N是实例的数量,f=E/N为观察到的误差率(其中E为N个实例中分类错误的个数),q为真实的误差率,c为置信度(C4.5算法的一个熟人参数,默认值为0.25),z为对应于置信度c的标准差,其值可根据c的设定值通过查正态分布表得到。通过该公式即可计算出真实误差率q的一个置信区间上限,用此上限为该节点误差率e做一个悲观的估计:
通过判断剪枝前后e的大小,从而决定是否需要剪枝。
(4) 对于缺失值的处理
在某些情况下,可供使用的数据可能缺少某些属性的值。假如<x,c(x)>是样本集S中的一个训练实例,但是其属性A的值A(x)未知。处理缺少属性值的一种策略是赋给它节点n所对应的训练实例中该属性的最常见值;另外一种更复杂的策略是为A的每个可能值赋予一个概率。例如,给定一个布尔属性A,如果结点n包含6个已知A=1和4个A=0的实例,那么A(x)=1的概率是0.6,而A(x)=0的概率是0.4。于是,实例x的60%被分配到A=1的分支,40%被分配到另一个分支。这些片断样例(fractional examples)的目的是计算信息增益,另外,如果有第二个缺失值的属性必须被测试,这些样例可以在后继的树分支中被进一步细分。C4.5就是使用这种方法处理缺少的属性值
C4.5算法的优点是产生的分类规则易于理解,准确率较高。缺点就是在构造树的过程中,需要对数据集进行多次的顺序扫描和排序,因而导致算法的低效。此外,C4.5算法只适合于能够驻留于内存的数据集,当训练集大得无法在内存容纳时,程序无法运行。
1、C4.5算法的实现
假设用S代表当前样本集,当前候选属性集用A表示,则C4.5算法C4.5formtree(S, A)的伪代码如下。
算法:Generate_decision_tree由给定的训练数据产生一棵决策树
输入:训练样本samples;候选属性的集合attributelist
输出:一棵决策树
(1) 创建根节点N;
(2) IF S都属于同一类C,则返回N为叶节点,标记为类C;
(3) IF attributelist为空 OR S中所剩的样本数少于某给定值
则返回N为叶节点,标记N为S中出现最多的类;
(4) FOR each attributelist中的属性
计算信息增益率information gain ratio;
(5) N的测试属性test.attribute = attributelist具有最高信息增益率的属性;
(6) IF测试属性为连续型
则找到该属性的分割阈值;
(7) For each由节点N一个新的叶子节点{
If该叶子节点对应的样本子集S’为空
则分裂此叶子节点生成新叶节点,将其标记为S中出现最多的类
Else
在该叶子节点上执行C4.5formtree(S’, S’.attributelist),继续对它分裂;
}
(8) 计算每个节点的分类错误,进行剪枝。
<span style="font-size:18px;">// C4.5_test.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <stdio.h>
#include <math.h>
#include "malloc.h"
#include <stdlib.h>
const int MAX = 10;
int** iInput;
int i = 0;//列数
int j = 0;//行数
void build_tree(FILE *fp, int* iSamples, int* iAttribute, int ilevel);//输出规则
int choose_attribute(int* iSamples, int* iAttribute);//通过计算信息增益率选出test_attribute
double info(double dTrue, double dFalse);//计算期望信息
double entropy(double dTrue, double dFalse, double dAll);//求熵
double splitinfo(int* list, double dAll);
int check_samples(int *iSamples);//检查samples是否都在同一个类里
int check_ordinary(int *iSamples);//检查最普通的类
int check_attribute_null(int *iAttribute);//检查attribute是否为空
void get_attributes(int *iSamples, int *iAttributeValue, int iAttribute);
int _tmain(int argc, _TCHAR* argv[])
{
FILE *fp;
FILE *fp1;
char iGet;
int a = 0;
int b = 0;//a,b是循环变量
int* iSamples;
int* iAttribute;
fp = fopen("c:\\input.txt", "r");
if (NULL == fp)
{
printf("error\n");
return 0;
}
iGet = getc(fp);
while (('\n' != iGet) && (EOF != iGet))
{
if (',' == iGet)
{
i++;
}
iGet = getc(fp);
}
i++;
iAttribute = (int *)malloc(sizeof(int)*i);
for (int k = 0; k < i; k++)
{
iAttribute[k] = (int)malloc(sizeof(int));
iAttribute[k] = 1;
}
while (EOF != iGet)
{
if ('\n' == iGet)
{
j++;
}
iGet = getc(fp);
}
j++;
iInput = (int **)malloc(sizeof(int*)*j);
iSamples = (int *)malloc(sizeof(int)*j);
for (a = 0; a < j; a++)
{
iInput[a] = (int *)malloc(sizeof(int)*i);
iSamples[a] = (int)malloc(sizeof(int));
iSamples[a] = a;
}
a = 0;
fclose(fp);
fp = fopen("c:\\input.txt", "r");
iGet = getc(fp);
while (EOF != iGet)
{
if ((',' != iGet) && ('\n' != iGet))
{
iInput[a][b] = iGet - 48;
b++;
}
if (b == i)
{
a++;
b = 0;
}
iGet = getc(fp);
}
fp1 = fopen("d:\\output.txt", "w");
build_tree(fp1, iSamples, iAttribute, 0);
fclose(fp);
return 0;
}
void build_tree(FILE * fp, int* iSamples, int* iAttribute, int level)//
{
int iTest_Attribute = 0;
int iAttributeValue[MAX];
int k = 0;
int l = 0;
int m = 0;
int *iSamples1;
for (k = 0; k < MAX; k++)
{
iAttributeValue[k] = -1;
}
if (0 == check_samples(iSamples))
{
fprintf(fp, "result: %d\n", iInput[iSamples[0]][i - 1]);
return;
}
if (1 == check_attribute_null(iAttribute))
{
fprintf(fp, "result: %d\n", check_ordinary(iSamples));
return;
}
iTest_Attribute = choose_attribute(iSamples, iAttribute);
iAttribute[iTest_Attribute] = -1;
get_attributes(iSamples, iAttributeValue, iTest_Attribute);
k = 0;
while ((-1 != iAttributeValue[k]) && (k < MAX))
{
l = 0;
m = 0;
while ((-1 != iSamples[l]) && (l < j))
{
if (iInput[iSamples[l]][iTest_Attribute] == iAttributeValue[k])
{
m++;
}
l++;
}
iSamples1 = (int *)malloc(sizeof(int)*(m + 1));
l = 0;
m = 0;
while ((-1 != iSamples[l]) && (l < j))
{
if (iInput[iSamples[l]][iTest_Attribute] == iAttributeValue[k])
{
iSamples1[m] = iSamples[l];
m++;
}
l++;
}
iSamples1[m] = -1;
if (-1 == iSamples1[0])
{
fprintf(fp, "result: %d\n", check_ordinary(iSamples));
return;
}
fprintf(fp, "level%d: %d = %d\n", level, iTest_Attribute, iAttributeValue[k]);
build_tree(fp, iSamples1, iAttribute, level + 1);
k++;
}
}
int choose_attribute(int* iSamples, int* iAttribute)
{
int iTestAttribute = -1;
int k = 0;
int l = 0;
int m = 0;
int n = 0;
int iTrue = 0;
int iFalse = 0;
int iTrue1 = 0;
int iFalse1 = 0;
int iDepart[MAX];
int iRecord[MAX];
double dEntropy = 0.0;
double dGainratio = 0.0;
double test = 0.0;
for (k = 0; k < MAX; k++)
{
iDepart[k] = -1;
iRecord[k] = 0;
}
k = 0;
while ((l != 2) && (k < (i - 1)))
{
if (iAttribute[k] == -1)
{
l++;
}
k++;
}
if (l == 1)
{
for (k = 0; k < (k - 1); k++)
{
if (iAttribute[k] == -1)
{
return iAttribute[k];
}
}
}
for (k = 0; k < (i - 1); k++)
{
l = 0;
iTrue = 0;
iFalse = 0;
if (iAttribute[k] != -1)
{
while ((-1 != iSamples[l]) && (l < j))
{
if (0 == iInput[iSamples[l]][i - 1])
{
iFalse++;
}
if (1 == iInput[iSamples[l]][i - 1])
{
iTrue++;
}
l++;
}
for (n = 0; n < l; n++)//计算该属性有多少不同的值并记录
{
m = 0;
while ((iDepart[m] != -1) && (m != MAX))
{
if (iInput[iSamples[n]][iAttribute[k]] == iDepart[m])
{
break;
}
m++;
}
if (-1 == iDepart[m])
{
iDepart[m] = iInput[iSamples[n]][iAttribute[k]];
}
}
while ((iDepart[m] != -1) && (m != MAX))
{
for (n = 0; n < l; n++)
{
if (iInput[iSamples[n]][iAttribute[k]] == iDepart[m])
{
if (1 == iInput[iSamples[n]][i - 1])
{
iTrue1++;
}
if (0 == iInput[iSamples[n]][i - 1])
{
iFalse1++;
}
iRecord[m]++;
}
}
dEntropy += entropy((double)iTrue1, (double)iFalse1, (double)l);
iTrue1 = 0;
iFalse1 = 0;
m++;
}
double dSplitinfo = splitinfo(iRecord, (double)l);
if (-1 == iTestAttribute)
{
iTestAttribute = k;
dGainratio = (info((double)iTrue, (double)iFalse) - dEntropy) / dSplitinfo;
}
else
{
test = (info((double)iTrue, (double)iFalse) - dEntropy) / dSplitinfo;
if (dGainratio < test)
{
iTestAttribute = k;
dGainratio = test;
}
}
}
}
return iTestAttribute;
}
double info(double dTrue, double dFalse)
{
double dInfo = 0.0;
dInfo = ((dTrue / (dTrue + dFalse))*(log(dTrue / (dTrue + dFalse)) / log(2.0)) + (dFalse / (dTrue + dFalse))*(log(dFalse / (dTrue + dFalse)) / log(2.0)))*(-1);
return dInfo;
}
double entropy(double dTrue, double dFalse, double dAll)
{
double dEntropy = 0.0;
dEntropy = (dTrue + dFalse)*info(dTrue, dFalse) / dAll;
return dEntropy;
}
double splitinfo(int* list, double dAll)
{
int k = 0;
double dSplitinfo = 0.0;
while (0 != list[k])
{
dSplitinfo -= ((double)list[k] / (double)dAll)*(log((double)list[k] / (double)dAll));
k++;
}
return dSplitinfo;
}
int check_samples(int *iSamples)
{
int k = 0;
int b = 0;
while ((-1 != iSamples[k]) && (k < j - 1))
{
if (iInput[k][i - 1] != iInput[k + 1][i - 1])
{
b = 1;
break;
}
k++;
}
return b;
}
int check_ordinary(int *iSamples)
{
int k = 0;
int iTrue = 0;
int iFalse = 0;
while ((-1 != iSamples[k]) && (k < i))
{
if (0 == iInput[iSamples[k]][i - 1])
{
iFalse++;
}
else
{
iTrue++;
}
k++;
}
if (iTrue >= iFalse)
{
return 1;
}
else
{
return 0;
}
}
int check_attribute_null(int *iAttribute)
{
int k = 0;
while (k < (i - 1))
{
if (-1 != iAttribute[k])
{
return 0;
}
k++;
}
return 1;
}
void get_attributes(int *iSamples, int *iAttributeValue, int iAttribute)
{
int k = 0;
int l = 0;
while ((-1 != iSamples[k]) && (k < j))
{
l = 0;
while (-1 != iAttributeValue[l])
{
if (iInput[iSamples[k]][iAttribute] == iAttributeValue[l])
{
break;
}
l++;
}
if (-1 == iAttributeValue[l])
{
iAttributeValue[l] = iInput[iSamples[k]][iAttribute];
}
k++;
}
}
</span>
大家可以去看看C4.5决策树提出者J. R. Quinlan的原论文