赞
踩
决策树算法是一种分类算法。 在分类问题中,决策树算法通过样本中某一维属性的值,将样本划分到不同的类别中。可以举一个生活中的例子来简单说明什么是决策树算法。
案例:
一个女孩的母亲给女孩介绍一个男朋友。
女儿:多大年纪了?母亲:28。
女儿:长得帅吗?母亲:帅。
女儿:收入高吗?母亲:中等。
女儿:是公务员吗?母亲:是。
女儿:那就见见。
上述案例就可以用决策树表示为:
结合前文的案例,我们可以知道决策树算法它能从给定的无序的训练样本中,提炼出树形的分类模型。树中的每个非叶子节点(根节点和中间节点)记录了使用哪个特征来进行类别的判断,每个叶子节点则代表了最后判断的类别。根节点到每个叶子节点均形成一条分类的路径规则。
构造决策树的过程:根据给定的数据集,按照数据的各个特征作为节点,构造决策树模型,进行数据的划分。
但是在构造的过程中,需要考虑每个特征作为节点的先后顺序,衡量标准就是熵值。
熵:在机器学习中代表随机变量不确定的度量,越混乱值越高。可以理解为,假如想要买一双鞋,可以去商场里购买,但是商场里的商品各种各样,那么在商场里想要买到一双鞋的难度很大。首先需要一层一层的找,找到楼层后还要去找位置。但是如果直接去一家鞋店就容易很多。所以去不同的地方买鞋所造成的代价是不一样的。在商场那熵值就很大,而去鞋店就会很小。
用数学公式表示为:
式子中的p代表事件发生的概率,这个公式就表示事件发生的概率与概率的对数值相乘的总和。当把样本按照特征A的值α划分为n个子集时,整个数据集的熵可以看做是n个子集的熵的加权和。
例如前文所述的案例,取十条数据来计算:
可以看到在这十条数据中,决定去相亲的占样本总数的五分钟之二,所以熵值计算为:
在节点顺序的选择上,如何使得特征对数据的划分更具有区分性,在决策树算法中,通常有三个标准:信息增益、增益率和基尼指数。
信息增益:对于给定的数据集,划分前后信息熵的减小量。它表示的是数据集中的不纯度,信息熵较小则表明数据集的纯度提高了,在选择数据集划分的标准时,通常选择 能够使得信息增益最大的标准。
信息增益的计算公式为:
之后计算在三个特征中对结果的影响,将影响即信息增益最大的作为根节点,熵值越大,节点越靠上。例如年龄这个特征计算对熵值的影响:
那么整个年龄这一特征的熵值就是分别的熵值与对应概率相乘,最后求得结果:
相减得到最后的信息增益为0.046,也就是年龄这一特征对结果的影响。
接下来计算其他特征对结果的影响,最后得到长相这一特征的信息增益为0.61,工资的信息增益为0.046。最终分析可得,对于年龄工资长相这三个特征中长相对原始熵值的影响最大,则选择这一特征的根节点。
增益率是可以作为选择最优化分属性的方法,其计算方法为:
其中IV(A)被称为特征A的固有值,即:
这种算法简而言之,就是用信息增益除以自身的熵值。
对于前文所提供的数据,若是加入一列ID作为一个特征的话,这样以信息增益计算后会发现ID这一特征的信息增益是最大的,将以它作为根节点,但这并不是我们想要的,这时候就需要用到增益率这一种算法。
基尼指数也可以选择最优的划分属性,对于数据集D,假设有K个分类,则样本属于第k个类的概率为p,则此概率分布得到基尼指数为:
对于数据集D,其基尼指数为:
其中,Ck表示的是数据集D中属于类别k的样本个数。
CART决策树算法就是利用基尼指数作为划分数据集的方法。
用Python实现基尼指数的过程如下:
def cal_gini_index(data):
total_sample = len(data)
if len(data) == 0:
return 0
label_counts = label_uniq_cnt(data)
gini = 0
for label in label_counts:
gini = gini + pow(label_counts[label], 2)
gini = 1 - float(gini) / pow(total_sample, 2)
return gini
在上述代码中,函数用于计算数据集data的基尼指数,在计算基尼指数的过程中,需要判断数据集中类别标签的个数,label_uniq_cnt函数用于计算数据集data中不同类别的标签个数,其函数的具体实现过程如下:
from math import pow
def label_uniq_cnt(data):
label_uniq_cnt = {}
for x in data:
label = x[len(x) - 1]
if label not in label_uniq_cnt:
label_uniq_cnt[label] = 0
label_uniq_cnt[label] = label_uniq_cnt[label] + 1
return label_uniq_cnt
由于决策树的建立完全是依赖于训练样本,因此该决策树对训练样本能够产生完美的拟合效果。但这样的决策树对于测试样本来说过于庞大而复杂,可能产生较高的分类错误。
决策树的剪枝就是将复杂的决策树进行简化,即去掉一些不必要的节点,是为了我们训练出的决策树算法具有很好的泛化能力,也就是解决了决策树的过拟合问题。
剪枝方法分为预剪枝和后剪枝两大类:
预剪枝:在训练模型的过程中进行剪枝,限制深度、叶子节点个数、信息增益等。其实就是在构建决策树的过程中,提前终止决策树的生长,从而避免过多的节点产生。
后剪枝:已经根据数据训练好了决策树模型,对这一模型进行剪枝操作。也就是在决策树构建完成之后,对那些置信度不达标的节点子树用叶子节点代替,该叶子节点的类标号用该节点子树中频率最高的类标记。
上式就是一个后剪枝的衡量标准,C(T)代表了一个特征的样本个数和熵值的乘积,这样计算出了最后的损失值,然后判断节点和细分之后的哪个好来判断是否剪枝。
两种方法相比较而言,预剪枝在训练的过程中,就将模型控制在了我们想要的深度或者想要的具有泛化能力的模型,也能在一定程度上节省计算量,但是其问题在于很难精确的判断何时终止树的生长。
CART分类回归树是一种典型的二叉决策树,可以处理连续型变量和离散型变量。如果待遇测分类是离散型数据,则CART生成分类决策树;如果待遇测分类是连续型数据,则CART生成回归决策树。数据对象的条件属性为离散型或连续型,并不是区别分类树与回归树的标准;数据对象xi的属性A、B为离散型或连续型,也并不是区别分类树与回归树的标准。
对于待遇测分类为离散型数据,选择具有最小Gain_GINI的属性及其属性值,作为最优分裂属性以及最优分裂属性值。Gain_GINI越小,说明二分后的子样本的纯度越高,即说明选择该属性作为分裂属性的效果越好。对于样本集S,Gini计算如下:
其中,在样本集S中,Pk表示分类结果中第k个类别出现的频率。
对于含有N个样本的样本集S,根据属性A的第i个属性值,将数据集S分成两部分,则划分后Gain_GINI计算如下:
对于属性A分别计算任意属性值将数据集划分成两部分之后的Gain_GINI,选取其最小值作为属性A的最优二分方案:
对于样本集S,计算所有属性的最优二分方案,选取其中的最小值,作为样本集S的最优二分方案:
这样所得到的属性A及其第i个属性值,即为样本集S的最优分裂属性以及最优分裂属性值。
区别于分类树,回归树的待遇测分类为连续型数据。 同时,区别于分类树选取Gain_GIN为评价分裂属性的指标,回归树选取Gain_σ为评价分裂属性的指标。选择具有最小Gain_σ的属性及其属性值,作为最优分裂属性以及最优分裂属性值。Gain_σ越小,说明二分之后的子样本的差异性越小,选择该属性值作为分裂属性值的效果越好。
针对含有连续型分类结果的样本集S,总方差计算如下:
其中,μ表示样本集S中分类结果的均值,Ck表示第k个分类结果。
对于含有N个样本的样本集S,根据属性A的第i个属性值,将数据集S划分成两部分,则划分之后Gain_σ的计算如下:
对于属性A,分别计算任意属性值将数据集划分成两部分之后的Gain_σ,选取其中的最小值,作为属性A得到的最优二分方案:
对于样本集S,计算所有属性的最优二分方案,选取其中的最小值,作为样本集S的最优二分方案:
这样所得到的属性A及其第i属性值,即为样本集S的最优分裂属性以及最优分裂属性值。
import numpy as np class node: def __init__(self, fea=-1, val=None, res=None, right=None, left=None): self.fea = fea self.val = val self.res = res self.right = right self.left = left class CART_REG: def __init__(self, epsilon=0.1, min_sample=10): self.epsilon = epsilon self.min_sample = min_sample self.tree = None def err(self, y_data): return y_data.var() * y_data.shape[0] def leaf(self, y_data): return y_data.mean() def split(self, fea, val, X_data): set1_inds = np.where(X_data[:, fea] <= val)[0] set2_inds = list(set(range(X_data.shape[0]))-set(set1_inds)) return set1_inds, set2_inds def getBestSplit(self, X_data, y_data): best_err = self.err(y_data) best_split = None subsets_inds = None for fea in range(X_data.shape[1]): for val in X_data[:, fea]: set1_inds, set2_inds = self.split(fea, val, X_data) if len(set1_inds) < 2 or len(set2_inds) < 2: continue now_err = self.err(y_data[set1_inds]) + self.err(y_data[set2_inds]) if now_err < best_err: best_err = now_err best_split = (fea, val) subsets_inds = (set1_inds, set2_inds) return best_err, best_split, subsets_inds def buildTree(self, X_data, y_data): if y_data.shape[0] < self.min_sample: return node(res=self.leaf(y_data)) best_err, best_split, subsets_inds = self.getBestSplit(X_data, y_data) if subsets_inds is None: return node(res=self.leaf(y_data)) if best_err < self.epsilon: return node(res=self.leaf(y_data)) else: left = self.buildTree(X_data[subsets_inds[0]], y_data[subsets_inds[0]]) right = self.buildTree(X_data[subsets_inds[1]], y_data[subsets_inds[1]]) return node(fea=best_split[0], val=best_split[1], right=right, left=left) def fit(self, X_data, y_data): self.tree = self.buildTree(X_data, y_data) return def predict(self, x): def helper(x, tree): if tree.res is not None: return tree.res else: if x[tree.fea] <= tree.val: branch = tree.left else: branch = tree.right return helper(x, branch) return helper(x, self.tree) if __name__ == '__main__': import matplotlib.pyplot as plt X_data_raw = np.linspace(-3, 3, 50) np.random.shuffle(X_data_raw) y_data = np.sin(X_data_raw) X_data = np.transpose([X_data_raw]) y_data = y_data + 0.1 * np.random.randn(y_data.shape[0]) clf = CART_REG(epsilon=1e-4, min_sample=1) clf.fit(X_data, y_data) res = [] for i in range(X_data.shape[0]): res.append(clf.predict(X_data[i])) p1 = plt.scatter(X_data_raw, y_data) p2 = plt.scatter(X_data_raw, res, marker='*') plt.legend([p1,p2],['real','pred'],loc='upper left') plt.show()
首先构建特征集,空树,然后递归构建二叉树。递归终止条件:
之后以每个特征j及相应的取值s为切分点,将数据集划分成左右两个子数据集,计算两个子数据集的平方误差。 取平方误差最小的(j, s),构建二叉树的节点。最后调用前两个步骤,递归对两个子数据集划分。
如上图所示,CART回归树样本空间细分成若干子空间,子空间内样本的输出y(连续值)的均值即为该子空间内的预测值。故对于输入X为一维时,预测结果可表示为阶梯函数。
下面用Python代码实现CART分类树:
这部分代码用于计算给定数据集的香农熵。
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 * log(prob, 2)
return shannonEnt
输入数据集,按照给定特征划分数据集,去除选择维度中等于选择值的项,输出划分后的数据集。
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reduceFeatVec = featVec[:axis]
reduceFeatVec.extend(featVec[axis+1:])
retDataSet.append(reduceFeatVec)
return retDataSet
对给定的数据集划分维度。
def chooseBestFeatureToSplit(dataSet): numFeatures = len(dataSet[0]) - 1 bestGini = 999999.0 bestFeature = -1 for i in range(numFeatures): featList = [example[i] for example in dataSet] uniqueVals = set(featList) gini = 0.0 for value in uniqueVals: subDataSet = splitDataSet(dataSet, i, value) prob = len(subDataSet)/float(len(dataSet)) subProb = len(splitDataSet(subDataSet, -1, 'N')) / float(len(subDataSet)) gini += prob * (1.0 - pow(subProb, 2) - pow(1 - subProb, 2)) if (gini < bestGini): bestGini = gini 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), reversed=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
输入决策树,分类标签和测试数据查看决策结果。
def classify(inputTree, featLabels, testVec):
firstStr = list(inputTree.keys())[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
classLabel = 'N'
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
这里创建一个新的数据集,如下图所示:
完整实验代码如下:
import numpy as np import pandas as pd import matplotlib.pyplot as plt import matplotlib from collections import Counter matplotlib.rcParams['font.family']='SimHei' plt.rcParams['axes.unicode_minus']=False decisionNode = dict(boxstyle="sawtooth", fc="0.8") leafNode = dict(boxstyle="round4", fc="0.8") arrow_args = dict(arrowstyle="<-") def calcGini(dataSet): y_lables = np.unique(dataSet[: , -1]) y_counts=len(dataSet) y_p={} gini=1.0 for y_lable in y_lables: y_p[y_lable]=len(dataSet[dataSet[:, -1]==y_lable])/y_counts gini-=y_p[y_lable]**2 return gini def splitDataSet(dataSet, i, value,types=1): if types==1: subDataSet=dataSet[list(dataSet[:,i]==value)] subDataSet = np.array(subDataSet) elif types==2: subDataSet=dataSet[list(dataSet[:,i]!=value)] subDataSet = np.array(subDataSet) return subDataSet ,len(subDataSet) def chooseBestFeature(dataSet,types='Gini'): numTotal=dataSet.shape[0] numFeatures = len(dataSet[0]) - 1 bestFeature = -1 columnFeaGini={} for i in range(numFeatures): prob = {} featList = list(dataSet[:,i]) prob=dict(Counter(featList)) for value in prob.keys(): feaGini = 0.0 bestFlag = 1.00001 subDataSet1,sublen1 = splitDataSet(dataSet, i, value, 1) subDataSet2,sublen2 = splitDataSet(dataSet, i, value, 2) if (sublen1/numTotal) * calcGini(subDataSet1)==0: bestFlag = 1 feaGini += (sublen1/numTotal) * calcGini(subDataSet1) + (sublen2/numTotal) * calcGini(subDataSet2) columnFeaGini['%d_%s'%(i,value)]=feaGini*bestFlag bestFeature=min(columnFeaGini,key=columnFeaGini.get) return bestFeature,columnFeaGini def createTree(dataSet,features,types='Gini'): y_lables = np.unique(dataSet[: , -1]) if len(set(y_lables)) == 1: return y_lables[0] if len(dataSet[0]) == 1: labelCount = {} labelCount=dict(Counter(y_lables)) return max(labelCount,key=labelCount.get) bestFeature,columnFeaGini=chooseBestFeature(dataSet,types) bestFeatureLable = features[int(bestFeature.split('_')[0])] decisionTree = {bestFeatureLable:{}} del(features[int(bestFeature.split('_')[0])]) y_lables_split=dataSet[list(dataSet[:,int(bestFeature.split('_')[0])]==bestFeature.split('_')[1])][:,-1] y_lables_grp=dict(Counter(y_lables_split)) y_leaf=max(y_lables_grp,key=y_lables_grp.get) decisionTree[bestFeatureLable][bestFeature.split('_')[1]]= y_leaf dataSetNew= np.delete(dataSet,int(bestFeature.split('_')[0]),axis=1) subFeatures = features[:] y1=y_lables[0] y2=y_lables[1] if y_leaf==y1: decisionTree[bestFeatureLable][y2]= {} decisionTree[bestFeatureLable][y2] = createTree(dataSetNew, subFeatures,types) elif y_leaf==y2: decisionTree[bestFeatureLable][y1]= {} decisionTree[bestFeatureLable][y1] = createTree(dataSetNew, subFeatures,types) return decisionTree def getNumLeafs(myTree): numLeafs = 0 firstStr = list(myTree.keys())[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes numLeafs += getNumLeafs(secondDict[key]) else: numLeafs +=1 return numLeafs def getTreeDepth(myTree): maxDepth = 0 firstStr = list(myTree.keys())[0]#myTree.keys()[0] secondDict = myTree[firstStr] for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes thisDepth = 1 + getTreeDepth(secondDict[key]) else: thisDepth = 1 if thisDepth > maxDepth: maxDepth = thisDepth return maxDepth def plotNode(nodeTxt, centerPt, parentPt, nodeType): createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction', xytext=centerPt, textcoords='axes fraction', va="center", ha="center", bbox=nodeType, arrowprops=arrow_args ) def plotMidText(cntrPt, parentPt, txtString): xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0] yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1] createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30) def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on numLeafs = getNumLeafs(myTree) #this determines the x width of this tree #depth = getTreeDepth(myTree) firstStr = list(myTree.keys())[0]#myTree.keys()[0] #the text label for this node should be this cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff) plotMidText(cntrPt, parentPt, nodeTxt) plotNode(firstStr, cntrPt, parentPt, decisionNode) secondDict = myTree[firstStr] plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD for key in secondDict.keys(): if type(secondDict[key]).__name__=='dict':#test to see if the nodes are dictonaires, if not they are leaf nodes plotTree(secondDict[key],cntrPt,str(key)) #recursion else: #it's a leaf node print the leaf node plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode) plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key)) plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD #if you do get a dictonary you know it's a tree, and the first element will be another dict def createPlot(myTree): fig = plt.figure(1, facecolor='white') fig.clf() axprops = dict(xticks=[], yticks=[]) createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #no ticks #createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses plotTree.totalW = float(getNumLeafs(myTree)) plotTree.totalD = float(getTreeDepth(myTree)) plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0; plotTree(myTree, (0.5,1.0), '') plt.show() if __name__ == "__main__": df_data=pd.read_csv('loan_application.csv') features = list(df_data.columns[1:-1]) dataSet=np.array(df_data.values[:,1:]) bestFeature,columnFeaGini=chooseBestFeature(dataSet,'Gini') print('\nbestFeature:',bestFeature,'\nGini(D,A):',columnFeaGini) dt_Gini = createTree(dataSet, features,'Gini') print('CART',dt_Gini) createPlot(dt_Gini)
最后得到的输出结果和CART树如下所示:
该数据集由个样本组成,数据包括贷款申请人的四个特征。第一个特征是年龄,有三个可能值;第二个特征是是否工作,有两个可能值;第三个特征是是否有自己的房子,有两个可能值;第四个特征是贷款情况,有三个可能值;最后为类别,即是否同意贷款。这一决策树模型用以根据申请人的特征来判断是否贷款。
在上图的输出结果中,计算了各个特征的基尼指数,对比得到尼基指数由小到大的顺序为是否有自己的房子,是否有工作,信贷情况和年龄特征,故以此可以将是否有自己的房子作为一个最优特征,且“是”为最优切分点。于是根节点产生两个子节点,一个是叶节点,对另一个节点继续切分选择最优特征与最优切分点。
分类决策树模型表示基于特征对实例进行分类的树形结构,可以看做是一个定义在特征空间划分上的类的条件概率分布。通过特征选择,树的生成和树的剪枝,旨在构建一个与训练数据拟合很好并且复杂度小的决策树。
在决策树生成时,通常使用信息增益最大,增益率最大或是基尼指数最小的特征选择的准则,从根节点开始递归地产生决策树,就是说不断的选取局部最优特征,或将训练集分割为能够基本正确分类的子集。
由于生成的决策树存在过拟合的问题,需要对其进行剪枝,往往是从已生成的树上减掉一些叶子节点或者子树并将其父节点作为新的叶节点从而化简生成新的决策树。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。