一、决策树的定义
决策树(decision tree) 每个决策或事件(即自然状态)都可能引出两个或多个事件,导致不同的结果,为了便于理解,把这种决策分支画成树的形状,故称决策树。决策树由结点和有向边组成。结点有两种类型:内部结点和叶结点,内部结点表示一个特征或属性,叶结点表示一个类(分类结果)。在分类问题中,它可以看作是if-then规则的集合,从根结点到叶结点的每一条路径都是一条规则,叶结点就是每一条规则所指向的结果。
二、决策树的分类过程
- 从根结点(root)开始,首先对某一属性的取值提问
- 天气
- 与根结点相连的不同分支,对应这个属性的不同取值
- 晴朗
- 多云
- 下雨
- 根据不同的选择,转向相应的分支
- 晴朗
- 在新到达的分支结点处做同样的分支判断
- 湿度? <=75 或 >75
- 这一过程持续,直到到达某个叶结点输出该叶结点的类别标记
- YES (去打球)
三、决策树的学习
通过学习事先收集的训练数据,可生成一棵决策树。决策树学习的目的是找到一个与训练数据矛盾较小的决策树,同时还需要有很好的泛化能力。决策树学习用损失函数作为度量,决策树学习的损失函数通常是正则化的极大似然函数。决策树学习的策略是最小化损失函数。
决策树的生成过程主要分为以下3个部分:
特征选择:特征选择是指从已有的训练数据的众多特征中选择一个特征作为当前结点的分类标准。如何选择特征有着很多不同量化评估标准标准,从而衍生出不同的决策树算法。
递归生成: 根据选择的特征评估标准,从上至下递归地生成子结点,直到数据集不可分则停止决策树停止生长。
剪枝:决策树学习某个数据集后,容易得到一个对该数据集有很好的分类能力的决策树,但对于未知数据的预测却未必好。因为决策树容易过度拟合,所以需要剪枝来缩小树结构规模,通过删去一些过于细分的叶结点,使其退回父结点或者更高的结点,然后将其作为新的叶结点,从而使得决策树模型有较好的泛化能力。
3.1 决策树的特征选择
特征选择在于选取对训练数据具有分类能力的特征,如果一个特征的分类效果不优于随机分类,那么这个这 个特征是没有分类能力的,应当放弃。特征选择的准则有信息增益、信息增益率、Gini系数。
(1)信息熵
熵: 随机变量不确定性的度量,熵即信息的期望值。
设X是一个取有限个值的离散随机变量,其概率分布为:
则随机变量的熵定义为:
熵越大,随机变量的不确定性就越大,数据混合的种类就越高,一个变量可能的变化就越多,携带的信息量就越大。
(2)条件熵
设随机变量(X,Y),其联合概率分布为:
条件熵表示在已知随机变量X的条件下随机变量Y的不确定性。随机变量X给定的条件下随机变量Y的条件熵H(Y|X),定义为X给定条件下Y的条件概率分布对X的数学期望:
(3)信息增益
信息增益表示得知特征X的信息后,而使得Y的不确定性减少的程度。
特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定条件下D的经验条件熵H(D|A)之差。即:
(4)信息增益率
以信息增益作为划分训练数据集的特征,存在偏向于选择特征取值较多的特征的问题,使用信息增益率可以对这一问题进行校正。
特征A对训练数据集D的信息增益率gg(D,A)定义为其信息增益g(D,A)与训练数据集D关于特征A的值的熵HA(D)之比。即:
(5)Gini系数
分类问题中假设有K个类,样本点属于第k类的概率为pk,则概率分布的Gini系数定义为:
对于给定的样本集合D,其Gini系数为:
这里,Ck是D中属于第k类的样本子集,K是类的个数。
Gini系数越大,样本集合的不确定性也就越大。
3.2 决策树的剪枝
对某一个数据集进行学习之后,决策树会产生过拟合现象,即:该决策树对该数据集可以完美分类,但对于未知数据的分类预测效果可能并不精准。
解决过拟合现象的方法是适当地减少树的复杂度。在决策树学习中,将已生成的树进行简化的过程称为剪枝。剪枝又分预剪枝和后剪枝。预剪枝是在树生长到一定条件时,停止继续划分。后剪枝指通过删去一些过于细分的叶结点,使其退回父结点或者更高的结点,然后将其作为新的叶结点,从而使得决策树模型有较好的泛化能力。
剪枝往往通过极小化损失函数来实现,决策树学习的损失函数Cα(T):
∣ T ∣ |T| ∣T∣为树T叶结点个数,t是树T的叶结点,该叶结点有 N t N_t Nt个样本点,其中k类的样本点有 N t k N_tk Ntk个。
其中经验熵 H t ( T ) Ht(T) Ht(T)为:
损失函数第一项**C(T)**可以理解为所有叶结点的经验熵加权和:
C ( T ) C(T) C(T) 表示模型对训练数据的预测误差,即模型与训练数据的拟合程度, ∣ T ∣ |T| ∣T∣表示模型复杂度,参数 α ≥ 0 α≥0 α≥0控制两者之间的影响。较大的 α α α促使选择较简单的决策树模型,较小的 α α α促使选择较复杂的决策树模型。 α = 0 α=0 α=0意味着只考虑模型与训练数据的拟合程度,不考虑模型的复杂度。
算法:树的后剪枝算法
输入:生成算法产生的整棵树T,参数α
输出:修剪后的子树Tα
(1)计算每个结点的经验熵
(2)递归地从树的叶结点向上回缩
设一组叶结点回缩到其父结点之前与之后的整体树分别为TB与TA,其对应的损失函数值分别是Cα(TB)与Cα(TA),如果Cα(TA)≤Cα(TB),则进行剪枝,即:将父结点变为新的叶结点
(3)返回(2),直至不能继续为止,得到损失函数最小的子树Tα.
四、决策树的优缺点
优点:
-
计算复杂度不高
-
分类速度快
- 通过一系列简单查询可判断类别
-
语义可表示性
-
从根结点到叶结点表示为合取式
-
利用合取式和析取式可以获得某个类别的明确描述
-
缺点:
- 难以处理缺失数据
- 过度拟合
- 忽略了数据集中属性之间的相关性
五、基于信息论的三种决策树算法
5.1 ID3算法
ID3算法中,决策树的特征选择以信息增益作为评估,每次选择信息增益最大的特征,递归生成决策树。没有剪枝的过程,通过裁剪合并相邻的无法产生大量信息增益的叶子结点,容易过度拟合。
具体实现:从根结点开始,在当前结点计算所有特征的信息增益,选择信息增益最大的特征作为当前结点的特征,然后根据该特征的不同取值构建子结点;接着对新生成的子结点递归执行以上步骤,直到所有特征的信息增益都很小或者没有特征可选(分类完毕)为止。
5.2 C4.5算法
ID3算法有两个缺点:
1. 不能处理连续分布的数据特征。
2. ID3算法以信息增益来选择属性时,偏向于能取不同值的个数多的某个属性。
C4.5算法是ID3算法的一个改进算法,继承了ID3算法的优点;用信息增益率来选择属性,在树构造的过程中进行剪枝,克服了ID3算法的缺点。C4.5算法产生的分类准则易于理解、准确率高;但效率低下,因为在生成决策树的过程中,需要对数据集进行多次的顺序扫描和排序。
具体实现与ID3算法相类似。处理连续属性时,C4.5算法先把连续属性转换为离散属性再进行处理。虽然本质上属性的取值是连续的,但对于有限的采样数据它是离散的,如果有N条样本,那么我们有N-1种离散化的方法:≤vj的分到左子树,>vj的分到右子树。计算这N-1种情况下最大的信息增益率。
5.3 CART算法
分类与回归树(classification and regression tree,CART)是一棵二叉树,且每个非叶子结点都有两个孩子。CART算法用Gini系数来选择属性,选择Gini系数最小的属性s,同时它包含后剪枝操作。
具体实现:
算法:CART生成算法
输入:训练数据集D,停止计算的条件;
输出 :CART决策树
根据训练数据集,从根结点开始,递归地对每个结点进行以下操作,构建二叉决策树:
(1)设结点的训练数据集为D,计算现有特征对该数据集的Gini系数。此时,对每一个特征A,对其可能取的每个值a,根据样本点对A=a的测试为“是”或“否”将D分割成D1和D2两部分,计算A=a时的Gini系数。
(2) 在所有可能的特征A以及它们所有可能的切分点a中,选择Gini系数最小的特征及其对应的切分点作为最优特征与最优切分点。依最优特征与最优切分点,从现结点生成两个子结点,将训练数据集依特征分配到两个子结点中去。
(3) 对两个子结点递归地调用(1)(2),直至满足停止条件。
(4) 生成CART决策树。
算法停止计算的条件是结点中的样本个数小于预定阈值,或样本集的Gini系数小于预定阈值(样本基本属于同一类),或者没有更多特征。
决策树应用
一、问题描述
**眼镜配制问题。**事先收集的实例如下:
age | prescipt | astigmatic | tear rate | conclusion | |
---|---|---|---|---|---|
1 | young | myope | no | reduced | no lenses |
2 | young | myope | no | normal | soft |
3 | young | myope | yes | reduced | no lenses |
4 | young | myope | yes | normal | hard |
5 | young | hyper | no | reduced | no lenses |
6 | young | hyper | no | normal | soft |
7 | young | hyper | yes | reduced | no lenses |
8 | young | hyper | yes | normal | hard |
9 | pre | myope | no | reduced | no lenses |
10 | pre | myope | no | normal | soft |
11 | pre | myope | yes | reduced | no lenses |
12 | pre | myope | yes | normal | hard |
13 | pre | hyper | no | reduced | no lenses |
14 | pre | hyper | no | normal | soft |
15 | pre | hyper | yes | reduced | no lenses |
16 | pre | hyper | yes | normal | no lenses |
17 | presbyopic | myope | no | reduced | no lenses |
18 | presbyopic | myope | no | normal | no lenses |
19 | presbyopic | myope | yes | reduced | no lenses |
20 | presbyopic | myope | yes | normal | hard |
21 | presbyopic | hyper | no | reduced | no lenses |
22 | presbyopic | hyper | no | normal | soft |
23 | presbyopic | hyper | yes | reduced | no lenses |
24 | presbyopic | hyper | yes | normal | no lenses |
二、问题分析
2.1 算法选择
此处选择了C4.5算法来解决眼镜配置问题。原因为:1)与ID3算法比较,C4.5算法是ID3算法的改进,可以弥补ID3算法的诸多缺点。2)与CART算法比较,C4.5算法易实现,从样本量考虑,小样本选择C4.5,大样本选择CART,本问题给出了24个样本,属于小样本问题,且CART本身是一种大样本的统计方法,小样本处理下泛化误差较大。
2.2 数据处理
该问题中,共有24个样本,4个属性(age、prescipt、tear rate、astigmatic),每个属性的取值情况如下:
-
age:young、pre、presbyopic
-
prescipt:myope、hyper
-
astigmatic:yes、no
-
tear rate:reduced、normal
conclusion为类别,共有3个类:soft、hard、no lenses.
采用枚举类型enum来标记属性,其在计算机内部以int的形式表达
2.3 编写函数
根据熵、信息增益、信息增益率的定义来编写相关的函数compute_entropy()、compute_gain()、compute_gain_ratio().
2.4 确定数据结构
采用树的结构表达:
2.5 打印决策树
因为树生成的时候采用了递归的形式,所以打印也用递归的形式。而之前用了enum编写结构,在打印时需转为string类型:
三、源代码
/*************************************************
Description: C4.5决策树算法
**************************************************/
#include <iostream>
#include <cmath>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
/*************************************************
Description: 数据初始化
**************************************************/
const int att_num = 4; // 属性种类
const int data_num = 24; // 训练数据个数
enum attribute_names { age, prescipt, astigmatic, tear_rate }; // 输入属性名称
enum attribute_values { //输入属性取值
young, pre, presbyopic,
myope, hyper,
yes, no,
reduced, normal,
soft, hard, no_lenses
};
// 导入训练数据
int train_data[data_num][att_num + 1]{
{ young, myope, no, reduced, no_lenses },
{ young, myope, no, normal, no_lenses },
{ young, myope, yes, reduced, no_lenses },
{ young, myope, yes, normal, hard },
{ young, hyper, no, reduced, no_lenses },
{ young, hyper, no, normal, soft },
{ young, hyper, yes, reduced, no_lenses },
{ young, hyper, yes, normal, hard },
{ pre, myope, no, reduced, no_lenses },
{ pre, myope, no, normal, soft },
{ pre, myope, yes, reduced, no_lenses },
{ pre, myope, yes, normal, hard },
{ pre, hyper, no, reduced, no_lenses },
{ pre, hyper, no, normal, soft },
{ pre, hyper, yes, reduced, no_lenses },
{ pre, hyper, yes, normal, no_lenses },
{ presbyopic, myope, no, reduced, no_lenses },
{ presbyopic, myope, no, normal, no_lenses },
{ presbyopic, myope, yes, reduced, no_lenses },
{ presbyopic, myope, yes, normal, hard },
{ presbyopic, hyper, no, reduced, no_lenses },
{ presbyopic, hyper, no, normal, soft },
{ presbyopic, hyper, yes, reduced, no_lenses },
{ presbyopic, hyper, yes, normal, no_lenses }
};
/*************************************************
Function: unique()
Description: 去除vector中的重复元素
Calls: 无
Input: 含有重复元素的vector vals
Output: 不含有重复元素的vector unique_vals
*************************************************/
vector<int> unique(vector<int> vals)
{
vector<int> unique_vals;
vector<int>::iterator itr;
vector<int>::iterator subitr;
int flag = 0;
while (!vals.empty())
{
unique_vals.push_back(vals[0]);
itr = vals.begin();
subitr = unique_vals.begin() + flag;
while (itr != vals.end())
{
if (*subitr == *itr)
itr = vals.erase(itr);
else
++itr;
}
flag++;
}
return unique_vals;
}
/*************************************************
Function: compute_entropy()
Description: 根据属性的取值,计算该属性的熵
Calls: unique()
log2()
count(),其中count()在STL的algorithm库中
Input: vector<int> 含有重复元素的向量
Output: double entropy属性的熵
*************************************************/
double compute_entropy(vector<int> v)
{
vector<int> unique_v;
unique_v = unique(v);
vector<int>::iterator itr;
itr = unique_v.begin();
double entropy = 0.0;
auto total = v.size();
while (itr != unique_v.end())
{
double cnt = count(v.begin(), v.end(), *itr);
entropy -= cnt / total * log2(cnt / total);
++itr;
}
return entropy;
}
/*************************************************
Function: compute_gain()
Description : 计算数据集中所有属性的信息增益
Calls : compute_entropy()
unique()
Input : vector<vector<int> >相当于一个二维数组,存储着训练数据集
Output : vector<double> 存储着所有属性的信息增益
*************************************************/
vector<double> compute_gain(vector<vector<int> > data)
{
vector<double> gain(att_num, 0);
vector<int> attribute_vals;
vector<int> labels;
for (int j = 0; j < data.size(); j++)
labels.push_back(data[j].back());
for (int i = 0; i < att_num; i++)
{
for (int j = 0; j < data.size(); j++)
attribute_vals.push_back(data[j][i]);
vector<int> unique_vals = unique(attribute_vals);
vector<int>::iterator itr = unique_vals.begin();
vector<int> subset;
while (itr != unique_vals.end())
{
for (int k = 0; k < data.size(); k++)
if (*itr == attribute_vals[k])
subset.push_back(data[k].back());
double A = (double)subset.size();
gain[i] += A / data.size() * compute_entropy(subset);
++itr;
subset.clear();
}
gain[i] = compute_entropy(labels) - gain[i];
attribute_vals.clear();
}
return gain;
}
/*************************************************
Function: compute_gain_ratio()
Description: 计算数据集中所有属性的信息增益率
Calls: compute_gain()
compute_entropy()
Input: vector<vector<int> > data 训练数据集
Output: vector<double> gain_ratio 信息增益率
*************************************************/
vector<double> compute_gain_ratio(vector<vector<int> > data)
{
vector<double> gain = compute_gain(data);
vector<int> attribute_vals;
vector<double> entropies;
vector<double> gain_ratio;
for (int i = 0; i < att_num; i++)
{
for (int j = 0; j < data.size(); j++)
attribute_vals.push_back(data[j][i]);
double current_entropy = compute_entropy(attribute_vals);
if (current_entropy)
gain_ratio.push_back(gain[i] / current_entropy);
else
gain_ratio.push_back(0.0);
attribute_vals.clear();
}
return gain_ratio;
}
/*************************************************
Function: find_most_common_label()
Description: 找出数据集中最多的类别标签
Calls: count()
unique()
Input: vector<vector<int> > data 数据集
Output: int label 类别标签
*************************************************/
int find_most_common_label(vector<vector<int> > data)
{
vector<int> labels;
for (int i = 0; i < data.size(); i++)
labels.push_back(data[i].back());
vector<int> unique_labels=unique(labels);
vector<int>::iterator itr = unique_labels.begin();
int most_common_label;
int most_counter = 0;
while (itr != unique_labels.end())
{
int current_counter = count(labels.begin(), labels.end(), *itr);
if (current_counter > most_counter)
{
most_common_label = *itr;
most_counter = current_counter;
}
++itr;
}
return most_common_label;
}
/*************************************************
Function: find_attribute_values()
Description: 根据属性,找出该属性可能的取值
Calls: unique()
Input: int attribute属性,vector<vector<int> > data 训练数据集
Output: vector<int> 属性所有可能的取值(不重复)
*************************************************/
vector<int> find_attribute_values(int attribute, vector<vector<int> > data)
{
vector<int> values;
for (int i = 0; i < data.size(); i++)
values.push_back(data[i][attribute]);
return unique(values);
}
/*************************************************
Function: drop_one_attribute()
Description: 去除已经考虑过的属性,标记为-1
Calls: unique()
Input: int attribute 属性, vector<vector<int> > data 数据集
Output: vector<vector<int> > data 去除已经考虑过的属性的数据集
*************************************************/
vector<vector<int> > drop_one_attribute(int attribute, vector<vector<int> > data)
{
for (int i = 0; i < data.size(); i++)
data[i][attribute] = -1;
return data;
}
struct Tree {
int root;//结点属性值
vector<int> branches;//结点可能取值
vector<Tree> children; //孩子结点
};
/*************************************************
Function: build_decision_tree()
Description: 递归构建决策树
Calls: unique(),count(),
find_most_common_label()
compute_gain_ratio(),
find_attribute_values(),
drop_one_attribute(),
build_decision_tree()(递归调用)
Input: vector<vector<int> > data训练数据集,Tree &tree一个空决策树
Output: /
*************************************************/
void build_decision_tree(vector<vector<int> > data, Tree &tree)
{
// 判断所有实例是否都属于同一类,如果是,则决策树是单结点
vector<int> labels(data.size(), 0);
for (int i = 0; i < data.size(); i++)
labels[i] = data[i].back();
if (unique(labels).size() == 1)
{
tree.root = labels[0];
return;
}
// 判断是否还有剩余的属性没有考虑,如果所有属性都已经考虑过了,
// 那么此时属性数量为0,将训练集中最多的类别标记作为该结点的类别标记
if (count(data[0].begin(), data[0].end(), -1) == 2 )//只剩下一列类别标记
{
tree.root = find_most_common_label(data);
return;
}
// 计算信息增益率
vector<double> gain_ratio = compute_gain_ratio(data);
tree.root = 0;
for (int i = 0; i < gain_ratio.size(); i++)
if (gain_ratio[i] >= gain_ratio[tree.root] && data[0][i] != -1)
tree.root = i;
tree.branches = find_attribute_values(tree.root, data);
// 根据结点的取值,将data分成若干子集
vector<vector<int> > new_data = drop_one_attribute(tree.root, data);
vector<vector<int> > subset;
for (int i = 0; i < tree.branches.size(); i++)
{
for (int j = 0; j < data.size(); j++)
{
for (int k = 0; k < data[0].size(); k++)
if (tree.branches[i] == data[j][k])
subset.push_back(new_data[j]);
}
// 对每一个子集递归调用build_decision_tree()函数
Tree new_tree;
build_decision_tree(subset, new_tree);
tree.children.push_back(new_tree);
subset.clear();
}
}
string print_attribute_names[] = { "age", "prescipt", "astigmatic", "tear_rate" };
string print_attribute_values[] = {
"young", "pre", "presbyopic",
"myope", "hyper",
"yes", "no",
"reduced", "normal",
"soft", "hard", "no_lenses"
};
/*************************************************
Function: print_tree()
Description: 打印决策树
Calls: print_tree();
Input: 决策树,层数
Output: 无
*************************************************/
void print_tree(Tree tree, int depth)
{
for (int d = 0; d < depth; d++) cout << "\t";
if (!tree.branches.empty()) //不是叶子结点
{
cout << print_attribute_names[tree.root] << endl;
for (int i = 0; i < tree.branches.size(); i++)
{
for (int d = 0; d < depth + 1; d++) cout << "\t";
cout << print_attribute_values[tree.branches[i]] << endl;
print_tree(tree.children[i], depth + 2);
}
}
else //是叶子结点
cout << print_attribute_values[tree.root] << endl;
}
int main()
{
vector<vector<int> > data(data_num, vector<int>(att_num + 1, 0));
for (int i = 0; i < data_num; i++)
for (int j = 0; j <= att_num; j++)
data[i][j] = train_data[i][j];
Tree tree;
build_decision_tree(data, tree);
cout << " 眼镜配置问题的决策树:" << endl;
print_tree(tree, 0);
return 0;
}
四、 结果示例
程序运行结果如下:
画成图后结果如下:
因为进行了预剪枝,所以最终得到的是一棵简化了的决策树。
参考资料
- 决策树 - MBA智库百科 http://wiki.mbalib.com/wiki/决策树
- 机器学习经典算法详解及Python实现–决策树(Decision Tree) - 优快云博客 https://blog.youkuaiyun.com/suipingsp/article/details/41927247
- 决策树学习笔记(一) - 优快云博客 https://blog.youkuaiyun.com/sb19931201/article/details/52464743
- 决策树学习笔记(二) - 优快云博客https://blog.youkuaiyun.com/sb19931201/article/details/52491430
- C4.5决策树 - Orisun - 博客园 http://www.cnblogs.com/zhangchaoyang/articles/2709922.html
- 分类回归树CART(上) - Orisun - 博客园 http://www.cnblogs.com/zhangchaoyang/articles/2842490.html
- 一步一步详解ID3和C4.5的C++实现 - 90Zeng - 博客园 https://www.cnblogs.com/90zeng/p/ID3_C4_5.html