决策树原理和Python实现
一个实现了ID3, C4.5, CART三种算法的完整样例的github源码地址:https://github.com/HenryLiu0/decision-tree-sample
1.算法原理
决策树算法是机器学习中常用的分类和回归算法,而决策树学习属于有监督学习。经典的决策树模型有 ID3,C4.5 和 CART 三种,它们的区别在于选择最佳特征的原则不同。
决策树如同它的名字,是一个树形的结构,它既可以是二叉树,也可以是非二叉树。决策树学习通过学习大量带有标签的样本,构建一个树模型。根据这个树模型,可以预测某条不含标签样本的结果。
构建决策树的过程可以分为几个步骤:
- 创建根结点;
- 判断当前的结点是否可以成为叶节点,这需要特定的边界条件。若是叶节点,根据多数原则判定此叶结点类别,然后此递归的分支结束,若不是,去往下一步骤;
- 遍历当前结点特征,根据特定原则(ID3,C4.5,CART)选择一个最佳的特征,根据这个特征将数据集划分成若干个子数据集,为每个子数据集创建一个子结点,每个子结点的特征删去选出的最佳特征;
- 递归建树,对每个子结点,去往步骤 2。
假设当前结点的数据集为D,特征集为A,那么构建决策树过程中递归的边界条件如下:
- D 中的所有样本标签属于同一类别C,则当前结点标记为C类叶结点;
- A 为空集,此时没有可用于选择的特征,这种情况出现在决策树的最后一层;D 中所有样本在 A 中所有特征上取值相同,此时所有特征都可称为最佳特征。以上两种情况都无法划分D,将当前结点标记为叶结点,类别为D中出现最多的类。
- D 为空集,发生这种情况是在当前结点的父结点选出的最佳特征的取值缺少某个类别的时候,比如下图中,attr1 结点中缺少 attr1 取值为 low 的样本,导致没有 low 路径。训练集样本不会有这条路径不代表测试集样本没有这条路径,不增加这条路径会引发测试错误。解决方案是每个结点需要依据所选特征的所有取值创建子结点。
选择最佳特征的经典方法有三种:
- 使用信息增益的方法(ID3),以 A 中信息增益最大的特征为最佳特征。首先计算数据集 D 的经验熵 H ( D ) = − ∑ d ∈ D p ( d ) log p ( d ) H(D)=-\sum_{d\in D}p(d)\log p(d) H(D)=−∑d∈Dp(d)logp(d),再计算某个特征 a 对数据集 D 的条件熵 H ( D ∣ a ) = ∑ a ∈ A p ( a ) H ( D ∣ A = a ) H(D|a)=\sum_{a\in A}p(a)H(D|A=a) H(D∣a)=∑a∈Ap(a)H(D∣A=a),最后计算信息增益 g ( D , a ) = H ( D ) = H ( D ∣ a ) g(D,a)=H(D)=H(D|a) g(D,a)=H(D)=H(D∣a)。
- 用信息增益率的方法(C4.5),以 A 中信息增益率最大的特征为最佳特征,改善了 ID3 偏向多数样本的缺点。首先计算 a 对数据集 D 的信息增益 g ( D , a ) = H ( D ) − H ( D │ a ) g(D,a)=H(D)-H(D│a) g(D,a)=H(D)−H(D│a),然后计算数据集 D 关于特征 a 的值的熵 S p l i t I n f o ( D , a ) = − ∑ j = 1 v ∣ D j ∣ ∣ D ∣ × log ∣ D j ∣ ∣ D ∣ SplitInfo(D,a)=-\sum_{j=1}^v \frac{|D_j |}{|D|} ×\log{\frac{|D_j |}{|D|}} SplitInfo(D,a)=−∑j=1v∣D∣∣Dj∣×log∣D∣∣Dj∣,最后计算信息增益率 g R a t i o ( D , a ) = g ( D , a ) S p l i t I n f o ( D , a ) gRatio(D,a)=\frac{g(D,a)}{SplitInfo(D,a)} gRatio(D,a)=SplitInfo(D,a)g(D,a) 。
- 使用 Gini 系数的方法(CART),值越小表示不确定性越小,所以 A 中 Gini 系数最小的特征为最佳特征。数据集 D 的 Gini 系数就是 g i n i ( D , a ) = ∑ j = 1 v p ( a j ) × g i n i ( D j │ A = a j ) gini(D,a)=\sum_{j=1}^v p(a_j )\times gini(D_j│A=a_j ) gini(D,a)=∑j=1vp(aj)×gini(Dj│A=aj),其中 g i n i ( D j │ A = a j ) = ∑ ∑ i = 1 n p i ( 1 − p i ) = 1 − ∑ i = 1 n p i 2 gini(D_j│A=a_j )=∑\sum_{i=1}^n p_i (1-p_i ) =1-\sum_{i=1}^n p_i^2 gini(Dj│A=aj)=∑∑i=1npi(1−pi)=1−∑i=1npi2 。
2.伪代码
以下代码为递归创建决策树的伪代码。
def createTree(dataSet, Attribute, AttrUniValSet, algorithm = 'ID3'):
"""
输入:当前结点数据集D,特征集A,特征集A每个特征a能取到的值的集合,最佳收益算法
输出:当前结点后部分的决策树
描述:递归创建决策树函数
"""
labelList = [entry[-1] for entry in dataSet]
# 1. 所有样本标签相同,那么该结点为记为该类的叶子结点
if len(labelList) == labelList.count(labelList[0]):
return labelList[0]
# 2 数据集D没有可以分类的属性,那么,该结点标记为出现最多类的叶子结点
if len(attr) == 0:
# 返回出现次数最多的标签
return Counter(labelList).most_common(1)[0][0]
# bestAttrIndex是收益最大的属性的下标
bestAttrIndex = selectBestAttrIndex(dataSet, algorithm)
# bestAttr是收益最大属性
bestAttr = attr[bestAttrIndex]
# 构建字典树
resTree = {bestAttr : {}}
# 从A中删除收益最大属性,与split后的dataSet相同长度
del(attr[bestAttrIndex])
# valueSet 是 bestAttr 所有可能的取值
valueSet = attrUniValSet[bestAttrIndex]
# 删除
del(attrUniValSet[bestAttrIndex])
# 为每个value创建分支
for value in valueSet:
subDataSet = splitDataSet(dataSet, bestAttrIndex, value)
# 3. 数据集为空,预测标签为父节点出现最多的标签
if len(subDataSet) == 0:
resTree[bestAttr][value] = Counter(labelList).most_common(1)[0][0]
else:
# 递归创建子树
resTree[bestAttr][value] = createTree(subDataSet, cpyAttr, attrUniValSet, algorithm)
return resTree
根据以上伪代码,可以画出如下流程图
3.关键代码
1)伪代码中出现过的选择最佳特征函数 selectBestAttrIndex,根据评价最佳(区分度最大)特征的标准不同而选择不同的函数。
def selectBestAttrIndex(dataSet, algorithm):
"""
输入:二维数据集D
输出:特定标准下A中最佳特征
"""
if algorithm == 'ID3':
return selectBestAttrIndex_ID3(dataSet)
elif algorithm == 'C4.5':
return selectBestAttrIndex_C45(dataSet)
elif algorithm == 'CART':
return selectBestAttrIndex_CART(dataSet)
2)ID3 方法函数,输入数据集D,返回最佳特征下标
def selectBestAttrIndex_ID3(dataSet):
# label是特征attribute数目
labelNum = len(dataSet[0])-1
# 计算数据集D的经验熵
oldEntropy = calEntropy(dataSet)
# bestIndex是最佳特征在A中的下标
bestIndex = -1
# maxInfoGain用于保存最大信息增益
maxInfoGain = 0.0
for index in range(labelNum):
newEntropy = 0.0
# 获得dataSet中每个特征的所有value的列表
attrValueList = [entry[index] for entry in dataSet]
# 获得value列表的不重复set,遍历计算每个value的熵
attrValueSet = set(attrValueList)
for uniqueValue in attrValueSet:
# 分离出col=index, value = uniqueValue 的数据集
subDataSet = splitDataSet(dataSet, index, uniqueValue)
# 计算子数据集占总数据比例
p = float(len(subDataSet)) / len(dataSet)
newEntropy += p * calEntropy(subDataSet)
infoGain = oldEntropy - newEntropy
if infoGain > maxInfoGain:
maxInfoGain = infoGain
bestIndex = index
return bestIndex
3)C4.5方法函数,仅仅在 ID3 方法增加计算特征熵的语句
def selectBestAttrIndex_C45(dataSet):
labelNum = len(dataSet[0])-1
oldEntropy = calEntropy(dataSet)
bestIndex = -1
maxInfoGainRotio = 0.0
for index in range(labelNum):
newEntropy = 0.0
splitInfo = 0.0
attrValueList = [entry[index] for entry in dataSet]
attrValueSet = set(attrValueList)
for uniqueValue in attrValueSet:
subDataSet = splitDataSet(dataSet, index, uniqueValue)
p = float(len(subDataSet)) / len(dataSet)
newEntropy += p * calEntropy(subDataSet)
# 计算特征的熵
splitInfo -= p * log(p, 2)
infoGain = oldEntropy - newEntropy
if splitInfo == 0.0:
continue
# 计算信息增益率
infoGainRatio = infoGain / splitInfo
if infoGainRatio > maxInfoGainRotio:
maxInfoGainRotio = infoGainRatio
bestIndex = index
return bestIndex
4)CART 方法函数
def selectBestAttrIndex_CART(dataSet):
labelNum = len(dataSet[0])-1
# 保存最佳特征下标
bestIndex = -1
# 保存最小Gini系数
minGini = float("inf")
for index in range(labelNum):
attrValueList = [entry[index] for entry in dataSet]
attrValueSet = set(attrValueList)
newGini = 0.0
for uniqueValue in attrValueSet:
subDataSet = splitDataSet(dataSet, index, uniqueValue)
p = float(len(subDataSet)) / len(dataSet)
# 对每个子数据集计算Gini系数
newGini += p * calGini(subDataSet)
if newGini < minGini:
minGini = newGini
bestIndex = index
return bestIndex
最后
您的点赞是对我最大的激励!