一、算法简介
1.1 基本模型介绍
决策树是一类常见的机器学习方法,可以帮助我们解决分类与回归两类问题。模型可解释性强,模型符合人类思维方式,是经典的树形结构。分类决策数模型是一种描述对实例进行分类的树形结构。决策树由结点 (node) 和有向边 (directed edge) 组成。结点包含了一个根结点 (root node)、若干个内部结点 (internal node) 和若干个叶结点 (leaf node)。内部结点表示一个特征或属性,叶结点表示一个类别。
简单而言,决策树是一个多层if-else函数,对对象属性进行多层if-else判断,获取目标属性的类别。由于只使用if-else对特征属性进行判断,所以一般特征属性为离散值,即使为连续值也会先进行区间离散化,如可以采用二分法(bi-partition)。
思考:选哪些特征属性参与决策树建模、哪些属性排在决策树的顶部,哪些排在底部,对属性的值该进行什么样的判断、样本属性的值缺失怎么办、如果输出不是分类而是数值能用么、对决策没有用或没有多大帮助的属性怎么办、什么时候使用决策树?
1.2 决策树特点
· 决策树优点
1)决策树易于理解和实现,人们在在学习过程中不需要使用者了解很多的背景知识,这同时是它的能够直接体现数据的特点,只要通过解释后都有能力去理解决策树所表达的意义。
2)对于决策树,数据的准备往往是简单或者是不必要的,而且能够同时处理数据型和常规型属性,在相对短的时间内能够对大型数据源做出可行且效果良好的结果。
3)易于通过静态测试来对模型进行评测,可以测定模型可信度;如果给定一个观察的模型,那么根据所产生的决策树很容易推出相应的逻辑表达式。
· 决策树缺点
1)对连续性的字段比较难预测。
2)对有时间顺序的数据,需要很多预处理的工作。
3)当类别太多时,错误可能就会增加的比较快。
4)一般的算法分类的时候,只是根据一个字段来分类。
二、算法分类和流程
2.1 算法分类
现有的关于决策树学习的主要思想主要包含以下 3 个研究成果:
由 Quinlan 在 1986 年提出的 ID3 算法
由 Quinlan 在 1993 年提出的 C4.5 算法
由 Breiman 等人在 1984 年提出的 CART 算法
算法比较
2.2 算法流程- 划分选择
决策树学习通常包括 3 个步骤:特征选择、决策树的生成、决策树的修剪。最为关键的就是如何选择最优划分属性。一般而言,随着划分过程不断进行,我们希望决策树的分支结点所包含的样本尽可能属于同一类别,即结点的 “纯度” (purity) 越来越高。
2.2.1 信息增益(information gain)
信息增益表示得知特征Xj的信息而使所属分类的不确定性减少的程度。
特征A对训练数据集D的信息增益g(D,A),定义为集合D的经验熵H(D)与特征A给定的情况下D的经验条件熵H(D|A)之差。
假设数据集D有K种分类,特征A有n种取值可能。其中数据集D的经验熵H(D)为
其中Pk为集合D中的任一样本数据分类k的概率,或者说属于分类k的样本所占的比例。
经验条件熵H(D|A)为
也可记作
其中Pi为特征取值为第i个可取值的概率。Di为特征A为第i个可取值的样本集合。
一般而言,信息增益越大,则意味着使用属性A来进行划分所获得的 "纯度提升" 越大。因此,我们可以用信息增益来进行决策树的划分属性选择。著名的 ID3 决策树学习算法就是以信息增益为准则来选择划分属性。
结果:选择信息增益大的作为最优划分属性,对属性取值较多的越偏向
2.2.2 信息增益比(information gain ratio)
特征A对训练数据集D的信息增益比gR(D,A),定义为其信息增益g(D,A)与训练集D的经验熵H(D)之比。
其中a的“固有值”
结果:选择信息增益率大的作为最优划分属性,对属性取值较少的越偏向
是为了矫正在训练数据集的经验熵大时,信息增益值会偏大,反之,信息增益值会偏小的问题。即特征选择使用了一个启发式策略:先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最高的。C4.5算法是使用该方式来划分属性。
2.2.3 基尼指数(Gini index)
ID3还是C4.5都是基于信息论的熵模型的,这里面会涉及大量的对数运算,为了简化模型同时也完全丢失熵模型的优点,在CART算法中使用基尼系数来代替信息增益比,基尼系数代表了模型的不纯度,基尼系数越小,则不纯度越低,特征越好。这和信息增益(比)是相反的。通过子集计算基尼不纯度,即随机放置的数据项出现于错误分类中的概率。以此来评判属性对分类的重要程度。
其中pk为任一样本点属于第k类的概率,也可以说成样本数据集中属于k类的样本的比例。
集合D的基尼指数为Gini(D),在特征A条件下的集合D的基尼指数为
其中 |Di|为特征A取第i个值时对应的样本个数。|D|为总样本个数
CART算法中对于分类树采用的是上述的基尼指数最小化准则。对于回归树,CART采用的是平方误差最小化准则。
2.3 剪枝
剪枝 (pruning) 是决策树学习算法对付 “过拟合” 的主要手段。在决策树学习中,为了尽可能正确分类训练样本,结点划分过程将不断重复,有时会造成决策树分支过多,这时就可能因针对训练样本学得 “太好” 了,以至于把训练集自身的一些特点当作所有数据都具有的一般性质而导致过拟合。因此,可通过主动去掉一些分支来降低过拟合的风险。
决策树剪枝的基本策略有 “预剪枝” (prepruning) 和 “后剪枝” (postpruning)。
· 预剪枝 是指在决策树生成过程中,对每个结点在划分前先进行估计,若当前节点的划分不能带来决策树泛化性能的提升,则停止划分并将当前结点标记为叶结点;
· 后剪枝 则是先从训练集生成一颗完整的决策树,然后自底向上地对非叶结点进行考察,若将该结点的子树替换为叶结点能带来决策树泛化性能的提升,则将该子树替换为叶结点。
三、决策树代码
- ID3代码
import operator import numpy as np def createDataSet(): """ outlook-> 0:sunny | 1:overcast |2:rain temperature-> 0:hot |1:mild | 2:cool humidity-> 0:high |1:normal windy-> 0:false | 1:true :return: """ dataSet = np.array([[0, 0, 0, 0, 'N'], [0, 0, 0, 1, 'N'], [1, 0, 0, 0, 'Y'], [2, 2, 1, 0, 'Y'], [2, 2, 1, 1, 'N'], [1, 2, 1, 1, 'Y']]) labels = np.array(['outlook', 'temperature', 'humidity', 'windy']) return dataSet, labels def createTestSet(): testSet = np.array([[0, 1, 0, 0], [0, 2, 1, 0], [2, 1, 1, 0], [0, 1, 1, 1], [1, 1, 0, 1], [1, 0, 1, 0], [2, 1, 0, 1]]) return testSet """ 根据样例创建树模型 """ def createTree(dataset, labels): calssListData = dataset[:, -1] if len(set(calssListData)) == 1: return dataset[:, -1][0] if labels.size == 0: return mayorClass(calssListData) bestFeatureIndex = chooseBestFeature(dataset, labels) bestFeature = labels[bestFeatureIndex] dtree = {bestFeature: {}} featureList = dataset[:, bestFeatureIndex] featureValue = set(featureList) for value in featureValue: subdataset = splitDataSet(dataset, bestFeatureIndex, value) sublabels = np.delete(labels, bestFeatureIndex) dtree[bestFeature][value] = createTree(subdataset, sublabels) return dtree def mayorClass(classList): labelCount = {} for i in range(classList.size): label = classList[i] labelCount[label] = labelCount.get(label, 0) + 1 sortedLabel = sorted(labelCount.items(), key=operator.itemgetter(1), reverse=True) return sortedLabel[0][0] """ 计算信息增益确定树 """ def chooseBestFeature(dataset, labels): # 特征的个数 featureNum = labels.size # 最小熵值 minEntropy, bestFeatureIndex = 1, None # 样本的总数 n = dataset.shape[0] for i in range(featureNum): # 指定特征的条件熵 featureEntorpy = 0 # 返回所有子集 featureList = dataset[:, i] featureValues = set(featureList) for value in featureValues: # 拆分子集 subDataSet = splitDataSet(dataset, i, value) # 信息熵值 ent = dataset_entropy(subDataSet) # 同一特征的相同属性个数 count = subDataSet.shape[0] # 当信息权重越小信息增益越大,和机器学习公式相同,取消的结果信息熵的相减 featureEntorpy += ((count / n) * ent) if minEntropy > featureEntorpy: minEntropy = featureEntorpy bestFeatureIndex = i print(minEntropy) return bestFeatureIndex """ 计算信息熵值 """ def dataset_entropy(dataset): classLabel = dataset[:, -1] labelCount = {} for i in range(classLabel.size): label = classLabel[i] labelCount[label] = labelCount.get(label, 0) + 1 # 熵值 ent = 0 for k, v in labelCount.items(): ent += -v / classLabel.size * np.log2(v / classLabel.size) return ent """ 拆分子集 """ def splitDataSet(dataset, featureIndex, value): # 划分后的子集 subdataset = [] for example in dataset: if example[featureIndex] == value: subdataset.append(example) return np.delete(subdataset, featureIndex, axis=1) """ 预测单个测试数据 """ def predict(tree, labels, testData): rootName = list(tree.keys())[0] rootValue = tree[rootName] featureIndex = list(labels).index(rootName) classLabel = None for key in rootValue.keys(): if testData[featureIndex] == int(key): # 判断value是否为字典类型的数据,若是则继续单个下钻 if type(rootValue[key]).__name__ == "dict": classLabel = predict(rootValue[key], labels, testData) else: classLabel = rootValue[key] return classLabel """ 预测所有测试集 """ def predictAll(tree, labels, testSet): classLabels = [] for i in testSet: classLabels.append(predict(tree, labels, i)) return classLabels if __name__ == "__main__": dataset, labels = createDataSet() # print(dataset) # print(dataset_entropy(dataset)) # s = splitDataSet(dataset, 0) # for item in s: # print(item) # 创建树模型 tree = createTree(dataset, labels) # 获取测试集 testSet = createTestSet() # 预测测试数据分类 print(predictAll(tree, labels, testSet))
- C4.5代码
import operator import numpy as np def createDataSet(): """ outlook-> 0:sunny | 1:overcast |2:rain temperature-> 0:hot |1:mild | 2:cool humidity-> 0:high |1:normal windy-> 0:false | 1:true :return: """ dataSet = np.array([[0, 0, 0, 0, 'N'], [0, 0, 0, 1, 'N'], [1, 0, 0, 0, 'Y'], [2, 2, 1, 0, 'Y'], [2, 2, 1, 1, 'N'], [1, 2, 1, 1, 'Y']]) labels = np.array(['outlook', 'temperature', 'humidity', 'windy']) return dataSet, labels def createTestSet(): testSet = np.array([[0, 1, 0, 0], [0, 2, 1, 0], [2, 1, 1, 0], [0, 1, 1, 1], [1, 1, 0, 1], [1, 0, 1, 0], [2, 1, 0, 1]]) return testSet """ 根据样例创建树模型 """ def createTree(dataset, labels): calssListData = dataset[:, -1] if len(set(calssListData)) == 1: return dataset[:, -1][0] if labels.size == 0: return mayorClass(calssListData) bestFeatureIndex = chooseBestFeature(dataset, labels) bestFeature = labels[bestFeatureIndex] dtree = {bestFeature: {}} featureList = dataset[:, bestFeatureIndex] featureValue = set(featureList) for value in featureValue: subdataset = splitDataSet(dataset, bestFeatureIndex, value) sublabels = np.delete(labels, bestFeatureIndex) dtree[bestFeature][value] = createTree(subdataset, sublabels) return dtree def mayorClass(classList): labelCount = {} for i in range(classList.size): label = classList[i] labelCount[label] = labelCount.get(label, 0) + 1 sortedLabel = sorted(labelCount.items(), key=operator.itemgetter(1), reverse=True) return sortedLabel[0][0] """ 计算信息增益确定树 """ def chooseBestFeature(dataset, labels): # 特征的个数 featureNum = labels.size # 结果的信息熵值 baseEntropy = dataset_entropy(dataset) # 最小熵值 maxRatio, bestFeatureIndex = 0, None # 样本的总数 n = dataset.shape[0] for i in range(featureNum): # 指定特征的条件熵 featureEntorpy = 0 splitInfo = 0 # 返回所有子集 featureList = dataset[:, i] featureValues = set(featureList) for value in featureValues: # 拆分子集 subDataSet = splitDataSet(dataset, i, value) # 信息熵值 ent = dataset_entropy(subDataSet) # 同一特征的相同属性个数 count = subDataSet.shape[0] # 当信息权重越小信息增益越大,和机器学习公式相同,取消的结果信息熵的相减 featureEntorpy += ((count / n) * ent) splitInfo += -subDataSet.shape[0] / n * np.log2(subDataSet.shape[0] / n) gainRatio = (baseEntropy - featureEntorpy) / splitInfo if gainRatio > maxRatio: maxRatio = gainRatio bestFeatureIndex = i return bestFeatureIndex """ 计算信息熵值 """ def dataset_entropy(dataset): classLabel = dataset[:, -1] labelCount = {} for i in range(classLabel.size): label = classLabel[i] labelCount[label] = labelCount.get(label, 0) + 1 # 熵值 ent = 0 for k, v in labelCount.items(): ent += -v / classLabel.size * np.log2(v / classLabel.size) return ent """ 拆分子集 """ def splitDataSet(dataset, featureIndex, value): # 划分后的子集 subdataset = [] for example in dataset: if example[featureIndex] == value: subdataset.append(example) return np.delete(subdataset, featureIndex, axis=1) """ 预测单个测试数据 """ def predict(tree, labels, testData): rootName = list(tree.keys())[0] rootValue = tree[rootName] featureIndex = list(labels).index(rootName) classLabel = None for key in rootValue.keys(): if testData[featureIndex] == int(key): # 判断value是否为字典类型的数据,若是则继续单个下钻 if type(rootValue[key]).__name__ == "dict": classLabel = predict(rootValue[key], labels, testData) else: classLabel = rootValue[key] return classLabel """ 预测所有测试集 """ def predictAll(tree, labels, testSet): classLabels = [] for i in testSet: classLabels.append(predict(tree, labels, i)) return classLabels if __name__ == "__main__": dataset, labels = createDataSet() # print(dataset) # print(dataset_entropy(dataset)) # s = splitDataSet(dataset, 0) # for item in s: # print(item) # 创建树模型 tree = createTree(dataset, labels) # 获取测试集 testSet = createTestSet() # 预测测试数据分类 print(predictAll(tree, labels, testSet))
- cart代码
import operator import numpy as np def createDataSet(): """ outlook-> 0:sunny | 1:overcast |2:rain temperature-> 0:hot |1:mild | 2:cool humidity-> 0:high |1:normal windy-> 0:false | 1:true :return: """ dataSet = np.array([[0, 0, 0, 0, 'N'], [0, 0, 0, 1, 'N'], [1, 0, 0, 0, 'Y'], [2, 2, 1, 0, 'Y'], [2, 2, 1, 1, 'N'], [1, 2, 1, 1, 'Y']]) labels = np.array(['outlook', 'temperature', 'humidity', 'windy']) return dataSet, labels def createTestSet(): testSet = np.array([[0, 1, 0, 0], [0, 2, 1, 0], [2, 1, 1, 0], [0, 1, 1, 1], [1, 1, 0, 1], [1, 0, 1, 0], [2, 1, 0, 1]]) return testSet """ 根据样例创建树模型 """ def createTree(dataset, labels): calssListData = dataset[:, -1] if len(set(calssListData)) == 1: return dataset[:, -1][0] if labels.size == 0: return mayorClass(calssListData) bestFeatureIndex = chooseBestFeature(dataset, labels) bestFeature = labels[bestFeatureIndex] dtree = {bestFeature: {}} featureList = dataset[:, bestFeatureIndex] featureValue = set(featureList) for value in featureValue: subdataset = splitDataSet(dataset, bestFeatureIndex, value) sublabels = np.delete(labels, bestFeatureIndex) dtree[bestFeature][value] = createTree(subdataset, sublabels) return dtree def mayorClass(classList): labelCount = {} for i in range(classList.size): label = classList[i] labelCount[label] = labelCount.get(label, 0) + 1 sortedLabel = sorted(labelCount.items(), key=operator.itemgetter(1), reverse=True) return sortedLabel[0][0] """ 计算信息增益确定树 """ def chooseBestFeature(dataset, labels): # 特征的个数 featureNum = labels.size # 结果的信息熵值 # baseEntropy = dataset_entropy(dataset) # 最小熵值 maxEntropy, bestFeatureIndex = 0, None # 样本的总数 n = dataset.shape[0] minGini = 1 for i in range(featureNum): # 指定特征的条件熵 # featureEntorpy = 0 gini = 0 # 返回所有子集 featureList = dataset[:, i] featureValues = set(featureList) for value in featureValues: # 拆分子集 subDataSet = splitDataSet(dataset, i, value) pi = subDataSet.shape[0] / n gini += pi * (1 - classLablePi(subDataSet)) print(gini) if minGini > gini: minGini = gini bestFeatureIndex = i return bestFeatureIndex def classLablePi(dataset): classLabel = dataset[:, -1] labelCount = {} for i in range(classLabel.size): label = classLabel[i] labelCount[label] = labelCount.get(label, 0) + 1 valueList = list(labelCount.values()) sum = np.sum(valueList) pi = 0 for i in valueList: pi += (i / sum) ** 2 return pi """ 计算信息熵值 """ def dataset_entropy(dataset): classLabel = dataset[:, -1] labelCount = {} for i in range(classLabel.size): label = classLabel[i] labelCount[label] = labelCount.get(label, 0) + 1 # 熵值 ent = 0 for k, v in labelCount.items(): ent += -v / classLabel.size * np.log2(v / classLabel.size) return ent """ 拆分子集 """ def splitDataSet(dataset, featureIndex, value): # 划分后的子集 subdataset = [] for example in dataset: if example[featureIndex] == value: subdataset.append(example) return np.delete(subdataset, featureIndex, axis=1) """ 预测单个测试数据 """ def predict(tree, labels, testData): rootName = list(tree.keys())[0] rootValue = tree[rootName] featureIndex = list(labels).index(rootName) classLabel = None for key in rootValue.keys(): if testData[featureIndex] == int(key): # 判断value是否为字典类型的数据,若是则继续单个下钻 if type(rootValue[key]).__name__ == "dict": classLabel = predict(rootValue[key], labels, testData) else: classLabel = rootValue[key] return classLabel """ 预测所有测试集 """ def predictAll(tree, labels, testSet): classLabels = [] for i in testSet: classLabels.append(predict(tree, labels, i)) return classLabels if __name__ == "__main__": dataset, labels = createDataSet() # print(dataset) # print(dataset_entropy(dataset)) # s = splitDataSet(dataset, 0) # for item in s: # print(item) # 创建树模型 tree = createTree(dataset, labels) # 获取测试集 testSet = createTestSet() # 预测测试数据分类 print(predictAll(tree, labels, testSet))