C4.5决策树生成算法完整版(Python)
转载请注明出处:©️ Sylvan Ding
ID3算法实验
决策树从一组无次序、无规则的事例中推理出决策树表示的分类规则,采用自顶向下的递归方式,在决策树的内部节点进行属性值的比较并根据不同的属性值判断从该结点向下的分支,在决策树叶结点得到结论。
实验目的
- 理解ID3算法原理
- 理解C4.5算法原理
- 编程实现C4.5算法
决策树生成算法
决策树的生成过程是使用满足划分准则的特征不断地将数据集划分为纯度更高、不确定性更小的子集的过程。
Generate_decision_tree
输入: 训练样本samples,由离散值属性表示;候选属性的集合attribute_list。
输出: 一颗决策树
创建结点N
IF samples都在同一个类C THEN
返回N作为叶结点,以类C标记;
IF attribute_list为空 THEN
返回N作为叶结点,标记为samples中最普通的类;
选择attribute_list中具有最高信息增益(或增益率)的属性test_attribute;
标记结点N为test_attribute;
FOR each test_attribute中的已知值ai //划分samples
由结点N长出一个条件为test_attribute=ai的分支;
设si是samples中test_attribute=ai的样本集合 //一个划分
IF si为空 THEN
加上一个树叶,标记为samples中最普通的类;
ELSE 加上一个由Generate_decision_tree(si, attribute_list - test_attribute)返回的结点;
ID3算法原理
- 决策树中每一个非叶结点对应着一个非类别属性,树枝代表这个属性的值。一个叶结点代表从树根到叶结点之间的路径对应的记录所属的类别属性值。
- 采用信息增益来选择最佳划分属性。
信息增益计算
ID3总是选择具有最高信息增益(最大熵)的属性作为当前结点的测试属性(test_attribute)。
设S是s个数据样本的集合,假定类标号属性有m个不同值,定义m个不同类 C i ( i = 1 , 2 , … , m ) C_i(i=1,2,\dots,m) Ci(i=1,2,…,m)。设 s i s_i si是类 C i C_i Ci中的样本数。对一个给定的样本分类所需的期望信息为:
I ( s 1 , s 2 , … , s m ) = − ∑ i = 1 m p i log ( p i ) I(s_1,s2,\dots,s_m) = -\sum_{i=1}^{m}p_i\log(p_i) I(s1,s2,…,sm)=−i=1∑mpilog(pi)
其中, p i p_i pi是任意样本属于 C i C_i Ci的概率, p i = s i s p_i=\frac{s_i}{s} pi=ssi。
设属性A具有v个不同值,则可用属性A将S划分为v个子集,设 s i j s_{ij} sij是子集 S j S_j Sj中类 C i C_i Ci的样本数,则根据A划分子集的熵为:
E ( A ) = − ∑ j = 1 v ∑ i = 1 m s i j s I ( s 1 j , s 2 j , … , s m j ) E(A) = -\sum_{j=1}^{v}\frac{\sum_{i=1}^{m}s_{ij}}{s}I(s_{1j}, s_{2j}, \dots, s_{mj}) E(A)=−j=1∑vs∑i=1msijI(s1j,s2j,…,smj)
由期望信息和熵值可以得到对应的信息增益值:
G a i n ( A ) = I ( s 1 j , s 2 j , … , s m j ) − E ( A ) Gain(A)=I(s_{1j}, s_{2j}, \dots, s_{mj})-E(A) Gain(A)=I(s1j,s2j,…,smj)−E(A)
ID3算法分析
优点
ID3算法避免了搜索不包含目标函数的不完整假设空间的主要风险,因为有限个离散值函数可以表示某个决策树。
缺点
无法回溯爬山搜索中常见的风险,如收敛到局部最优,而不是全局最优。ID3算法只能处理离散值的属性。
当特征的取值较多时,根据此特征划分更容易得到纯度高的子集,因此划分之后的熵更低,由于划分前的熵是一定的,所以信息增益更大,ID3偏袒较多值的属性。
C4.5算法
- 用信息增益率来代替信息增益
- 合并具有连续属性的值
- 处理缺少属性值的训练样本
信息增益率
G a i n R a t i o ( A ) = G a i n ( A ) S p l i t I ( A ) GainRatio(A)=\frac{Gain(A)}{SplitI(A)} GainRatio(A)=SplitI(A)Gain(A)
其中,
S p l i t I ( A ) = − ∑ j = 1 v p j log ( p j ) SplitI(A)=-\sum_{j=1}^{v}p_j\log(p_j) SplitI(A)=−j=1∑vpjlog(pj)
连续属性的离散化
- 根据属性的值,对数据集排序;
- 用不同的阈值将数据集动态划分;
- 取两个实际值中点作为一个阈值;
- 取两个划分,所有样本都在这两个划分中;
- 得到所有可能的阈值和增益率;
对连续属性A进行排序,按阈值将A划分为两部分,一部分落入 v j v_j vj对范围内,而另一部分则大于 v j v_{j} vj,选择增益率最大的划分所对应的阈值为划分阈值进行属性离散化。注意,当前划分属性为连续属性,则该属性还可以作为其后代的划分属性。新增labelProperties表示属性为连续还是离散。
缺失值处理
选取最优划分属性
有缺失值属性的信息增益为该属性(设该属性为A)下的无缺失值样本占比 × \times ×无缺失值样本子集的信息增益。
G a i n ( A ) = p × G a i n ( A ~ ) Gain(A) = p\times Gain(\widetilde{A}) Gain(A)=p×Gain(A )
其中,p为A属性下无缺失值样本的占比,即 p = ∑ x ∈ D ~ w x ∑ x ∈ D w x p=\frac{\sum_{x\in \widetilde{D}}w_x}{\sum_{x\in D}w_x} p=∑x∈Dwx∑x∈D wx, G a i n ( A ~ ) Gain(\widetilde{A}) Gain(A )为A属性下无缺失值样本的信息增益, w x w_x wx是样本x的权重,D是所有样本, D ~ \widetilde{D} D 是无缺失样本。
缺失值样本的划分
增加样本权重概念。样本权重的初始值为1,对于无缺失值样本,将其划分到子结点时,权重保持不变。而对于有缺失值样本,在划分时按无缺失值样本在每个分支中所占比重(即对于分支中无缺失值样本数/该属性下无缺失值样本总数)划分到分支中。此时,ID3中所得公式需改写:
G a i n ( A ~ ) = I ( S ) − E ( A ) Gain(\widetilde{A})=I(S)-E(A) Gain(A )=I(S)−E(A)
I ( S ) = − ∑ i = 1 m p i ~ log ( p i ~ ) I(S)= -\sum_{i=1}^{m}\widetilde{p_i}\log(\widetilde{p_i}) I(S)=−i=1∑mpi log(pi )
E ( A ) = − ∑ j = 1 v r j ~ I ( S j ) E(A)=-\sum_{j=1}^v \widetilde{r_j}I(S^j) E(A)=−j=1∑vrj I(Sj)
S j = ( s 1 j , s 2 j , … , s m j ) S^j=(s_{1j},s_{2j},\dots,s_{mj}) Sj=(s1j,s2j,…,smj)
S = ( s 1 , s 2 , … , s m ) S=(s_{1},s_{2},\dots,s_{m}) S=(s1,s2,…,sm)
p i ~ = ∑ x ∈ D i ~ w x ∑ x ∈ D ~ w x \widetilde{p_i}=\frac{\sum_{x\in \widetilde{D_i}}w_x}{\sum_{x\in \widetilde{D}}w_x} pi =∑x∈D wx∑x∈Di wx
r j ~ = ∑ x ∈ D j ~ w x ∑ x ∈ D ~ w x \widetilde{r_j}=\frac{\sum_{x\in \widetilde{D^j}}w_x}{\sum_{x\in \widetilde{D}}w_x} rj =∑x∈D wx∑x∈Dj wx
p i ~ \widetilde{p_i} pi 是无缺失值样本中第i类所占比例,样本个数按权重计算; r j ~ \widetilde{r_j} rj 是无缺失值样本中属性A上取值为 a j a_j aj的样本所占比例,样本个数按权重计算。i是样本索引,j是属性索引。
*缺失测试样本的分类
C4.5算法的决策树构造完成后,需要对含缺失值的测试样本进行分类。在为测试样本某属性下的未知值选择分支时,考虑该缺失属性在该分支下的每个叶子结点中属于不同分类的概率,最大概率对于的分类即为所属分类。概率的计算使用该分支下每个叶子结点中不同分类的权值的加权平均。
本实验仅涉及决策树生成算法,故不考虑测试集的分类和决策树剪枝算法。
*C4.5算法的缺陷和修正
当离散属性和连续属性并存时,C4.5算法倾向于选择连续特征作为最佳树分裂点。因此要对最佳分裂点的信息增益进行修正:
G a i n ( A ) = G a i n ( A ) − log ( N − 1 ) ∣ D ∣ Gain(A) = Gain(A)-\frac{\log{(N-1)}}{\left| D\right|} Gain(A)=Gain(A)−∣D∣log(N−1)
其中,N为连续特征可能的分裂点个数,D是样本数目。
此外,C4.5算法的信息增益率偏向取值较少的特征。因此,并不直接选择信息增益率最大的特征,而是在候选特征中找出信息增益高于平均水平的特征,然后在这些特征中再选择信息增益率最高的特征作为最佳划分特征。
本实验编写的决策树生成算法,不考虑上述两种问题的修正。
代码
import copy
import operator
from math import log
from numpy import inf
NAN = 'Nan' # 缺失值定义
def calcShannonEnt(dataSet: list, labelIndex: int):
"""
计算对应属性索引下样本的香农熵
:param dataSet: 样本
:param labelIndex: 属性索引
:return: shannonEnt 香农熵
"""
numEntries = 0 # 样本数(按权重计算)
labelCounts = {}
# 遍历样本,计算每类的权重
for featVec in dataSet:
# 样本的属性不为空
if featVec[labelIndex] != NAN:
weight = featVec[-2]
numEntries += weight
currentLabel = featVec[-1] # 当前样本的类别
# 如果样本类别不在labelCounts
if currentLabel not in labelCounts.keys():
# 添加该类别,令该类别权重为0
labelCounts[currentLabel] = .0
# 添加该类别的权重
labelCounts[currentLabel] += weight
shannonEnt = .0
for key in labelCounts: # 计算信息熵
prob = labelCounts[key] / numEntries
shannonEnt -= prob * log(prob, 2)
return shannonEnt
def splitDataSet(dataSet: list, axis: int, value, AttrType='N'):
"""
划分数据集
:param dataSet: 数据集
:param axis: 按第几个特征划分
:param value: 划分特征的值
:param AttrType: N-离散属性; L-小于等于value值; R-大于value值
:return: 对应axis为value(连续情况下则为大于或小于value)的数据集dataSet的子集
"""
subDataSet = []
# N-离散属性
if AttrType == 'N':
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
subDataSet.append(reducedFeatVec)
# L-小于等于value值
elif AttrType == 'L':
for featVec in dataSet:
# 样本axis对应属性非空
if featVec[axis] != NAN:
if featVec[axis] <= value:
# 无需减少该特征
subDataSet.append(featVec)
# R-大于value值
elif AttrType == 'R':
for featVec in dataSet:
if featVec[axis] != NAN:
if featVec[axis] > value:
# 无需减少该特征
subDataSet.append(featVec)
else:
exit(0)
return subDataSet
def calcTotalWeight(dataSet: list, labelIndex: int, isContainNull: bool):
"""
计算样本对某个特征值的总样本数(按权重计算)
:param dataSet: 数据集
:param labelIndex: 属性索引
:param isContainNull: 是否包含空值
:return: 样本的总权重
"""
totalWeight = .0
# 遍历样本
for featVec in dataSet:
# 样本权重
weight = featVec[-2]
# 不包含空值并且该属性非空
if isContainNull is False and featVec[labelIndex] != NAN:
# 非空样本树,按权重计算
totalWeight += weight
# 包含空值
if isContainNull is True:
# 总样本数
totalWeight += weight
return totalWeight
def splitDataSetWithNull(dataSet: list, axis: int, value, AttrType='N'):
"""
划分含有缺失值的数据集
:param dataSet: 数据集
:param axis: 按第几个特征划分
:param value: 划分特征的值
:param AttrType: N-离散属性; L-小于等于value值; R-大于value值
:return: 按value划分的数据集dataSet的子集
"""
# 属性值未缺失样本子集
subDataSet = []
# 属性值缺失样本子集
nullDataSet = []
# 计算非空样本总权重
totalWeightV = calcTotalWeight(dataSet, axis, False)
# N-离散属性
if AttrType == 'N':
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
subDataSet.append(reducedFeatVec)
# 样本该属性值缺失
elif featVec[axis] == NAN:
reducedNullVec = featVec[:axis]
reducedNullVec.extend(featVec[axis + 1:])
nullDataSet.append(reducedNullVec)
# L-小于等于value值
elif AttrType == 'L':
for featVec in dataSet:
# 样本该属性值未缺失
if featVec[axis] != NAN:
if value is None or featVec[axis] < value:
subDataSet.append(featVec)
# 样本该属性值缺失
elif featVec[axis] == NAN:
nullDataSet.append(featVec)
# R-大于value值
elif AttrType == 'R':
for featVec in dataSet:
# 样本该属性值未缺失
if featVec[axis] != NAN:
if featVec[axis] > value:
subDataSet.append(featVec)
# 样本该属性值缺失
elif featVec[axis] == NAN:
nullDataSet.append(featVec)
# 计算此分支中非空样本的总权重
totalWeightSub = calcTotalWeight(subDataSet, -1, True)
# 缺失值样本按权值比例划分到分支中
for nullVec in nullDataSet:
nullVec[-2] = nullVec[-2] * totalWeightSub / totalWeightV
subDataSet.append(nullVec)
return subDataSet
def calcGainRatio(dataSet: list, labelIndex: int, labelType: bool):
"""
计算信息增益率,返回信息增益率和连续属性的划分点
:param dataSet: 数据集
:param labelIndex: 属性索引
:param labelType: 属性类型,0为离散,1为连续
:return: 信息增益率和连续属性的划分点
"""
# 计算根节点的信息熵
baseE = calcShannonEnt(dataSet, labelIndex)
# 对应labelIndex的特征值向量
featVec = [row[labelIndex] for row in dataSet]
# featVec值的种类
uniqueVals = set(featVec)
newE = .0 # 新信息熵
bestPivotValue = None # 最佳划分属性
IV = .0 # 该变量取自西瓜书
# 总样本权重
totalWeight = calcTotalWeight(dataSet, labelIndex, True)
# 非空样本权重
totalWeightV = calcTotalWeight(dataSet, labelIndex, False)
# 对离散的特征
if labelType == 0:
# 按属性值划分数据集,计算各子集的信息熵
for value in uniqueVals:
# 划分数据集
subDataSet = splitDataSet(dataSet, labelIndex, value)
# 计算子集总权重
totalWeightSub = calcTotalWeight(subDataSet, labelIndex, True)
# 过滤空属性
if value != NAN:
prob = totalWeightSub / totalWeightV
newE += prob * calcShannonEnt(subDataSet, labelIndex)
prob1 = totalWeightSub / totalWeight
IV -= prob1 * log(prob1, 2)
# 对连续的特征
else:
uniqueValsList = list(uniqueVals)
# 过滤空属性
if NAN in uniqueValsList:
uniqueValsList.remove(NAN)
# 计算空值样本的总权重,用于计算IV
dataSetNull = splitDataSet(dataSet, labelIndex, NAN)
totalWeightN = calcTotalWeight(dataSetNull, labelIndex, True)
probNull = totalWeightN / totalWeight
if probNull > 0:
IV += -1 * probNull * log(probNull, 2)
# 属性值排序
sortedUniqueVals = sorted(uniqueValsList)
minEntropy = inf # 定义最小熵
# 如果UniqueVals只有一个值,则说明只有左子集,没有右子集
if len(sortedUniqueVals) == 1:
totalWeightL = calcTotalWeight(dataSet, labelIndex, True)
probL = totalWeightL / totalWeightV
minEntropy = probL * calcShannonEnt(dataSet, labelIndex)
IV = -1 * probL * log(probL, 2)
# 如果UniqueVals只有多个值,则计算划分点
else:
for j in range(len(sortedUniqueVals) - 1):
pivotValue = (sortedUniqueVals[j] + sortedUniqueVals[j + 1]) / 2
# 对每个划分点,划分得左右两子集
dataSetL = splitDataSet(dataSet, labelIndex, pivotValue, 'L')
dataSetR = splitDataSet(dataSet, labelIndex, pivotValue, 'R')
# 对每个划分点,计算左右两侧总权重
totalWeightL = calcTotalWeight(dataSetL, labelIndex, True)
totalWeightR = calcTotalWeight(dataSetR, labelIndex, True)
probL = totalWeightL / totalWeightV
probR = totalWeightR / totalWeightV
Ent = probL * calcShannonEnt(dataSetL, labelIndex) + probR * calcShannonEnt(dataSetR, labelIndex)
# 取最小的信息熵
if Ent < minEntropy:
minEntropy = Ent
bestPivotValue = pivotValue
probL1 = totalWeightL / totalWeight
probR1 = totalWeightR / totalWeight
IV += -1 * (probL1 * log(probL1, 2) + probR1 * log(probR1, 2))
newE = minEntropy
gain = totalWeightV / totalWeight * (baseE - newE)
# 避免IV为0(属性只有一个值的情况下)
if IV == 0.0:
IV = 0.0000000001
gainRatio = gain / IV
return gainRatio, bestPivotValue
def chooseBestFeatureToSplit(dataSet: list, labelProps: list):
"""
选择最佳数据集划分方式
:param dataSet: 数据集
:param labelProps: 属性类型,0离散,1连续
:return: 最佳划分属性的索引和连续属性的最佳划分值
"""
numFeatures = len(labelProps) # 属性数
bestGainRatio = -inf # 最大信息增益
bestFeature = -1 # 最优划分属性索引
bestPivotValue = None # 连续属性的最佳划分值
for featureI in range(numFeatures): # 对每个特征循环
gainRatio, bestPivotValuei = calcGainRatio(dataSet, featureI, labelProps[featureI])
# 取信息益率最大的特征
if gainRatio > bestGainRatio:
bestGainRatio = gainRatio
bestFeature = featureI
bestPivotValue = bestPivotValuei
return bestFeature, bestPivotValue
def majorityCnt(classList: list, weightList: list):
"""
返回出现次数最多的类别(按权重计)
:param classList: 类别
:param weightList: 权重
:return: 出现次数最多的类别
"""
classCount = {}
# 计算classCount
for cls, wei in zip(classList, weightList):
if cls not in classCount.keys():
classCount[cls] = .0
classCount[cls] += wei
# 排序
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
# 仅剩一个类别
if len(sortedClassCount) == 1:
return sortedClassCount[0][0], sortedClassCount[0][1]
# 剩余多个类别,返回出现次数最多的类别
return sortedClassCount[0][0], sortedClassCount[0][1]
def isSame(dataSet: list):
"""
比较样本特征是否相同
:param dataSet: 数据集
:return: 相同True,否则False
"""
for j in range(len(dataSet[0])-2):
for i in range(1, len(dataSet)):
if not dataSet[i][j] == dataSet[0][j]:
return False
return True
def createTree(dataSet: list, labels: list, labelProps: list):
"""
创建决策树(Decision Tree)
:param dataSet: 数据集
:param labels: 属性集
:param labelProps: 属性类型,0离散,1连续
:return: 决策树
"""
classList = [sample[-1] for sample in dataSet] # 类别向量
weightList = [sample[-2] for sample in dataSet] # 权重向量
# 如果只剩一个类别,返回并退出
if classList.count(classList[0]) == len(classList):
totalWeight = calcTotalWeight(dataSet, 0, True)
return classList[0], totalWeight
# 如果所有特征都遍历完了,返回出现次数最多的类别,并退出
if len(dataSet[0]) == 1:
return majorityCnt(classList, weightList)
# 如果剩余样本特征相同,返回出现次数最多的类别,并退出
if isSame(copy.copy(dataSet)):
return majorityCnt(classList, weightList)
# 计算最优分类特征的索引,若为连续属性,则还返回连续属性的最优划分点
bestFeat, bestPivotValue = chooseBestFeatureToSplit(dataSet, labelProps)
# 对离散的特征
if labelProps[bestFeat] == 0:
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
labelsNew = copy.copy(labels)
labelPropertyNew = copy.copy(labelProps)
# 已经选择的离散特征不再参与分类
del (labelsNew[bestFeat])
del (labelPropertyNew[bestFeat])
featValues = [sample[bestFeat] for sample in dataSet]
# 最佳花划分属性包含的所有值
uniqueValue = set(featValues)
# 删去缺失值
uniqueValue.discard(NAN)
# 遍历每个属性值,递归构建树
for value in uniqueValue:
subLabels = labelsNew[:]
subLabelProperty = labelPropertyNew[:]
myTree[bestFeatLabel][value] = createTree(splitDataSetWithNull(dataSet, bestFeat, value),
subLabels, subLabelProperty)
# 对连续特征,不删除该特征,分别构建左子树和右子树
else:
bestFeatLabel = labels[bestFeat] + '<' + str(bestPivotValue)
myTree = {bestFeatLabel: {}}
subLabels = labels[:]
subLabelProperty = labelProps[:]
# 构建左子树
valueLeft = 'Y'
myTree[bestFeatLabel][valueLeft] = createTree(splitDataSetWithNull(dataSet, bestFeat, bestPivotValue, 'L'),
subLabels, subLabelProperty)
# 构建右子树
valueRight = 'N'
myTree[bestFeatLabel][valueRight] = createTree(splitDataSetWithNull(dataSet, bestFeat, bestPivotValue, 'R'),
subLabels, subLabelProperty)
return myTree
if __name__ == '__main__':
# 读取数据文件
fr = open(r'data.csv')
data = [row.strip().split(',') for row in fr.readlines()]
labels = data[0][0:-1] # labels:属性
dataset = data[1:] # dataset:数据集(初始样本)
labelProperties = [0, 1, 0] # labelProperties:属性标识,0为离散,1为连续
# 样本权重初始化
for row in dataset:
row.insert(-1, 1.0)
# 按labelProperties连续化离散属性
for row in dataset:
for i, lp in enumerate(labelProperties):
# 若标识为连续属性,则转化为float型
if lp:
row[i] = float(row[i])
# C4.5算法生成决策树
trees = createTree(copy.copy(dataset), copy.copy(labels), copy.copy(labelProperties))
print(trees)
Python3.6
结果验证
在data.csv数据集上运行上述代码,得到结果如下:
{'天气': {'多云': ('玩', 3.230769230769231), '晴': {'湿度<77.5': {'Y': ('玩', 2.0), 'N': {'有雨?': {'有': ('不玩', 1.0), '无': ('不玩', 2.0)}}}}, '雨': {'有雨?': {'有': {'湿度<85.0': {'Y': ('不玩', 2.0), 'N': ('玩', 0.38461538461538464)}}, '无': ('玩', 3.0)}}}} // (结果, 权重)
附录(data.csv)
天气 | 湿度 | 有雨? | 去玩? |
---|---|---|---|
晴 | 70 | 有 | 玩 |
晴 | 90 | 有 | 不玩 |
晴 | 85 | 无 | 不玩 |
晴 | 95 | 无 | 不玩 |
晴 | 70 | 无 | 玩 |
Nan | 90 | 有 | 玩 |
多云 | 78 | 无 | 玩 |
多云 | 65 | 有 | 玩 |
多云 | 75 | 无 | 玩 |
雨 | 80 | 有 | 不玩 |
雨 | 70 | 有 | 不玩 |
雨 | 80 | 无 | 玩 |
雨 | 80 | 无 | 玩 |
雨 | 96 | 无 | 玩 |
参考
- 数据挖掘原理与算法(第3版)
- 《机器学习》周志华
- 决策树–信息增益,信息增益比,Geni指数的理解
- 机器学习笔记(5)——C4.5决策树中的连续值处理和Python实现
- 机器学习笔记(7)——C4.5决策树中的缺失值处理