目录
一、决策树的介绍:
决策树是一个预测模型,它代表的是对象属性与对象值之间的一种映射关系。树中每个节点表示某个对象,而每个分叉路径则代表某个可能的属性值,而每个叶节点则对应从根节点到该叶节点所经历的路径所表示的对象的值。
从数据产生决策树的机器学习技术叫做决策树学习,通俗说就是决策树。
决策树是一树状结构,它的每一个叶节点对应着一个分类,非叶节点对应着在某个属性上的划分,根据样本在该属性上的不同取值将其划分成若干个子集。对于非纯的叶节点,多数类的标号给出到达这个节点的样本所属的类。构造决策树的核心问题是在每一步如何选择适当的属性对样本做拆分。对一个分类问题,从已知类标记的训练样本中学习并构造出决策树是一个自上而下,分而治之的过程。
二、决策树算法:
![](https://img-blog.csdnimg.cn/direct/11885191b97b40d4bef90c8a82775e31.png)
ID3算法(信息增益):
从信息论的知识中我们知道:信息熵越大,样本的纯度越低。ID3 算法的核心思想就是以信息增益来度量特征选择,选择信息增益最大的特征进行分裂。
信息增益 = 信息熵 - 条件熵:
也可以表示为H0 - H1,比如上面实例中我选择纹理作为根节点,将根节点一分为三,则:
意思是,没有选择纹理特征前,是否是好瓜的信息熵为0.998,在我选择了纹理这一特征之后,信息熵下降为0.764,信息熵下降了0.234,也就是信息增益为0.234。
C4.5算法(信息增益率):
C4.5算法最大的特点是克服了ID3对特征数目的偏重这一缺点,引入信息增益率来作为分类标准。
信息增益率=信息增益/特征本身的熵:
信息增益率对可取值较少的特征有所偏好(分母越小,整体越大),因此C4.5并不是直接用增益率最大的特征进行划分,而是使用一个启发式方法:先从候选划分特征中找到信息增益高于平均值的特征,再从中选择增益率最高的。
例如上述的例子,我们考虑纹理本身的熵,也就是是否是好瓜的熵。
纹理本身有三种可能,每种概率都已知,则纹理的熵为:
那么选择纹理作为分类依据时,信息增益率为:
CART算法(基尼指数):
基尼指数(基尼不纯度):表示在样本集合中一个随机选中的样本被分错的概率。
基尼系数越小,不纯度越低,特征越好。这和信息增益(率)正好相反。基尼指数可以用来度量任何不均匀分布,是介于0-1之间的数,0是完全相等,1是完全不相等。
三种算法的对比
适用范围:
ID3算法只能处理离散特征的分类问题,C4.5能够处理离散特征和连续特征的分类问题,CART算法可以处理离散和连续特征的分类与回归问题。
假设空间:
ID3和C4.5算法使用的决策树可以是多分叉的,而CART算法的决策树必须是二叉树。
优化算法:
ID3算法没有剪枝策略,当叶子节点上的样本都属于同一个类别或者所有特征都使用过了的情况下决策树停止生长。
C4.5算法使用预剪枝策略,当分裂后的增益小于给定阈值或者叶子上的样本数量小于某个阈值或者叶子节点数量达到限定值或者树的深度达到限定值,决策树停止生长。
CART决策树主要使用后剪枝策略。
剪枝处理:
决策树算法很容易过拟合,剪枝算法就是用来防止决策树过拟合,提高泛华性能的方法。剪枝分为预剪枝与后剪枝。
预剪枝
预剪枝是指在决策树的生成过程中,对每个节点在划分前先进行评估,若当前的划分不能带来泛化性能的提升,则停止划分,并将当前节点标记为叶节点。
预剪枝方法有:
(1)当叶节点的实例个数小于某个阈值时停止生长;
(2)当决策树达到预定高度时停止生长;
(3)当每次拓展对系统性能的增益小于某个阈值时停止生长;
预剪枝不足就是剪枝后决策树可能会不满足需求就被过早停止决策树的生长。
后剪枝
后剪枝是指先从训练集生成一颗完整的决策树,然后自底向上对非叶节点进行考察,若将该节点对应的子树替换为叶节点,能带来泛化性能的提升,则将该子树替换为叶节点。
后剪枝决策树通常比预剪枝决策树保留了更多的分枝,一般情形下,后剪枝决策树的欠拟合风险很小,泛化能力往往优于预剪枝决策树。但后剪枝决策树是在生产完全决策树之后进行的,并且要自底向上地对所有非叶子节点进行逐一考察,因此其训练时间开销比未剪枝的决策树和预剪枝的决策树都要大很多。
决策树特点
优点:
容易理解,可解释性较好
可以用于小数据集
时间复杂度较小
可以处理多输入问题,可以处理不相关特征数据
对缺失值不敏感
缺点:
在处理特征关联性比较强的数据时,表现得不太好
当样本中各类别不均匀时,信息增益会偏向于那些具有更多数值的特征
对连续性的字段比较难预测
容易出现过拟合
当类别太多时,错误可能会增加得比较快
三、决策树的实现:
ID3算法实现:
import math
import operator
def calcShannonEnt(dataset):
numEntries = len(dataset)
labelCounts = {}
for featVec in dataset:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * math.log(prob, 2)
return shannonEnt
def CreateDataSet():
dataset = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['outlook', 'temperature','humidity','false']
return dataset, labels
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis + 1:])
retDataSet.append(reducedFeatVec)
return retDataSet
def chooseBestFeatureToSplit(dataSet):
numberFeatures = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0;
bestFeature = -1;
for i in range(numberFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] = 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
del (labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
myDat, labels = CreateDataSet()
myTree = createTree(myDat, labels)
print(myTree)
运行结果如下:
CART算法实现:
# 构造数据集
def create_dataset():
dataset = [['youth', 'no', 'no', 'just so-so', 'no'],
['youth', 'no', 'no', 'good', 'no'],
['youth', 'yes', 'no', 'good', 'yes'],
['youth', 'yes', 'yes', 'just so-so', 'yes'],
['youth', 'no', 'no', 'just so-so', 'no'],
['midlife', 'no', 'no', 'just so-so', 'no'],
['midlife', 'no', 'no', 'good', 'no'],
['midlife', 'yes', 'yes', 'good', 'yes'],
['midlife', 'no', 'yes', 'great', 'yes'],
['midlife', 'no', 'yes', 'great', 'yes'],
['geriatric', 'no', 'yes', 'great', 'yes'],
['geriatric', 'no', 'yes', 'good', 'yes'],
['geriatric', 'yes', 'no', 'good', 'yes'],
['geriatric', 'yes', 'no', 'great', 'yes'],
['geriatric', 'no', 'no', 'just so-so', 'no']]
features = ['age', 'work', 'house', 'credit']
return dataset, features
# 计算当前集合的Gini系数
def calcGini(dataset):
# 求总样本数
num_of_examples = len(dataset)
labelCnt = {}
# 遍历整个样本集合
for example in dataset:
# 当前样本的标签值是该列表的最后一个元素
currentLabel = example[-1]
# 统计每个标签各出现了几次
if currentLabel not in labelCnt.keys():
labelCnt[currentLabel] = 0
labelCnt[currentLabel] += 1
# 得到了当前集合中每个标签的样本个数后,计算它们的p值
for key in labelCnt:
labelCnt[key] /= num_of_examples
labelCnt[key] = labelCnt[key] * labelCnt[key]
# 计算Gini系数
Gini = 1 - sum(labelCnt.values())
return Gini
# 提取子集合
# 功能:从dataSet中先找到所有第axis个标签值 = value的样本
# 然后将这些样本删去第axis个标签值,再全部提取出来成为一个新的样本集
def create_sub_dataset(dataset, index, value):
sub_dataset = []
for example in dataset:
current_list = []
if example[index] == value:
current_list = example[:index]
current_list.extend(example[index + 1 :])
sub_dataset.append(current_list)
return sub_dataset
# 将当前样本集分割成特征i取值为value的一部分和取值不为value的一部分(二分)
def split_dataset(dataset, index, value):
sub_dataset1 = []
sub_dataset2 = []
for example in dataset:
current_list = []
if example[index] == value:
current_list = example[:index]
current_list.extend(example[index + 1 :])
sub_dataset1.append(current_list)
else:
current_list = example[:index]
current_list.extend(example[index + 1 :])
sub_dataset2.append(current_list)
return sub_dataset1, sub_dataset2
def choose_best_feature(dataset):
# 特征总数
numFeatures = len(dataset[0]) - 1
# 当只有一个特征时
if numFeatures == 1:
return 0
# 初始化最佳基尼系数
bestGini = 1
# 初始化最优特征
index_of_best_feature = -1
# 遍历所有特征,寻找最优特征和该特征下的最优切分点
for i in range(numFeatures):
# 去重,每个属性值唯一
uniqueVals = set(example[i] for example in dataset)
# Gini字典中的每个值代表以该值对应的键作为切分点对当前集合进行划分后的Gini系数
Gini = {}
# 对于当前特征的每个取值
for value in uniqueVals:
# 先求由该值进行划分得到的两个子集
sub_dataset1, sub_dataset2 = split_dataset(dataset,i,value)
# 求两个子集占原集合的比例系数prob1 prob2
prob1 = len(sub_dataset1) / float(len(dataset))
prob2 = len(sub_dataset2) / float(len(dataset))
# 计算子集1的Gini系数
Gini_of_sub_dataset1 = calcGini(sub_dataset1)
# 计算子集2的Gini系数
Gini_of_sub_dataset2 = calcGini(sub_dataset2)
# 计算由当前最优切分点划分后的最终Gini系数
Gini[value] = prob1 * Gini_of_sub_dataset1 + prob2 * Gini_of_sub_dataset2
# 更新最优特征和最优切分点
if Gini[value] < bestGini:
bestGini = Gini[value]
index_of_best_feature = i
best_split_point = value
return index_of_best_feature, best_split_point
# 返回具有最多样本数的那个标签的值('yes' or 'no')
def find_label(classList):
# 初始化统计各标签次数的字典
# 键为各标签,对应的值为标签出现的次数
labelCnt = {}
for key in classList:
if key not in labelCnt.keys():
labelCnt[key] = 0
labelCnt[key] += 1
# 将classCount按值降序排列
# 例如:sorted_labelCnt = {'yes': 9, 'no': 6}
sorted_labelCnt = sorted(labelCnt.items(), key = lambda a:a[1], reverse = True)
# 下面这种写法有问题
# sortedClassCount = sorted(labelCnt.iteritems(), key=operator.itemgetter(1), reverse=True)
# 取sorted_labelCnt中第一个元素中的第一个值,即为所求
return sorted_labelCnt[0][0]
def create_decision_tree(dataset, features):
# 求出训练集所有样本的标签
# 对于初始数据集,其label_list = ['no', 'no', 'yes', 'yes', 'no', 'no', 'no', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'yes', 'no']
label_list = [example[-1] for example in dataset]
# 先写两个递归结束的情况:
# 若当前集合的所有样本标签相等(即样本已被分“纯”)
# 则直接返回该标签值作为一个叶子节点
if label_list.count(label_list[0]) == len(label_list):
return label_list[0]
# 若训练集的所有特征都被使用完毕,当前无可用特征,但样本仍未被分“纯”
# 则返回所含样本最多的标签作为结果
if len(dataset[0]) == 1:
return find_label(label_list)
# 下面是正式建树的过程
# 选取进行分支的最佳特征的下标和最佳切分点
index_of_best_feature, best_split_point = choose_best_feature(dataset)
# 得到最佳特征
best_feature = features[index_of_best_feature]
# 初始化决策树
decision_tree = {best_feature: {}}
# 使用过当前最佳特征后将其删去
del(features[index_of_best_feature])
# 子特征 = 当前特征(因为刚才已经删去了用过的特征)
sub_labels = features[:]
# 递归调用create_decision_tree去生成新节点
# 生成由最优切分点划分出来的二分子集
sub_dataset1, sub_dataset2 = split_dataset(dataset,index_of_best_feature,best_split_point)
# 构造左子树
decision_tree[best_feature][best_split_point] = create_decision_tree(sub_dataset1, sub_labels)
# 构造右子树
decision_tree[best_feature]['others'] = create_decision_tree(sub_dataset2, sub_labels)
return decision_tree
# 用上面训练好的决策树对新样本分类
def classify(decision_tree, features, test_example):
# 根节点代表的属性
first_feature = list(decision_tree.keys())[0]
# second_dict是第一个分类属性的值(也是字典)
second_dict = decision_tree[first_feature]
# 树根代表的属性,所在属性标签中的位置,即第几个属性
index_of_first_feature = features.index(first_feature)
# 对于second_dict中的每一个key
for key in second_dict.keys():
# 不等于'others'的key
if key != 'others':
if test_example[index_of_first_feature] == key:
# 若当前second_dict的key的value是一个字典
if type(second_dict[key]).__name__ == 'dict':
# 则需要递归查询
classLabel = classify(second_dict[key], features, test_example)
# 若当前second_dict的key的value是一个单独的值
else:
# 则就是要找的标签值
classLabel = second_dict[key]
# 如果测试样本在当前特征的取值不等于key,就说明它在当前特征的取值属于'others'
else:
# 如果second_dict['others']的值是个字符串,则直接输出
if isinstance(second_dict['others'],str):
classLabel = second_dict['others']
# 如果second_dict['others']的值是个字典,则递归查询
else:
classLabel = classify(second_dict['others'], features, test_example)
return classLabel
if __name__ == '__main__':
dataset, features = create_dataset()
decision_tree = create_decision_tree(dataset, features)
# 打印生成的决策树
print(decision_tree)
# 对新样本进行分类测试
features = ['age', 'work', 'house', 'credit']
test_example = ['midlife', 'yes', 'no', 'great']
print(classify(decision_tree, features, test_example))
运行结果如下:
四、算法评估与分析:
ID3 只能处理离散数据且缺失值敏感,C4.5 和 CART 可以处理连续性数据且有多种方式处理缺失值;从样本量考虑的话,小样本建议 C4.5、大样本建议 CART。C4.5 处理过程中需对数据集进行多次扫描排序,处理成本耗时较高,而 CART 本身是一种大样本的统计方法,小样本处理下泛化误差较大 。