决策树的构建
ID3算法
构建决策树的算法有很多,这里使用ID3算法构建决策树。
ID3算法的核心是在决策树的各个结点上对应信息增益准则选择特征,递归地构建决策树。
方法如下:
从根节点开始,对结点计算所有可能的特征的信息增益,然后将信息增益大的作为结点的特征,然后根据这个特征的不同取值来建立不同的子节点。然后使用递归方法进行选择新的特征作为新的结点,直到所有特征的信息增益都很小或者没有特征选择结束。
数据如下:
我们在机器学习实战的决策树的基础笔记里面得到特征是“有自己的房子”的信息增益值最大,那么我们就将有自己房子这个特征作为根节点。然后将训练集D根据类别分成两类:D1(有自己的房子的数据集)、D2(没有自己的房子的数据集)。因为有房子的D1最后都给贷款了,所以他们是同一类的样本点,所以D1直接就是一个叶结点。但是D2中没有房子的最后有的贷款了,有的没有贷款。所以还要往下递归。
然后计算D2训练集中的剩余的三个特征(A1:年龄、A2:有工作、A4:信贷情况)的信息增益:
那么选择信息增益最大的A2(有工作)这个特征作为新的子节点。那么新的特征下面还是有两种情况:有工作和没有工作。没有房子但是有工作一共有三个样本,这三个样本最后的分类结果都是拿到贷款了,所以他们是同一类数据,那么有工作下面就是一个叶结点。没有房子没有工作的样本最后都没有给贷款,那么他们这六个样本也是属于一类,也是一个叶结点。
根据上面的特征选择的情况,绘制出的决策树的样子如下:
代码实现构建决策树
"""
函数说明:统计classList中出现此处最多的元素(类标签)
Parameters:
classList - 类标签列表
Returns:
sortedClassCount[0][0] - 出现此处最多的元素(类标签)
"""
def majorityCnt(classList):
classCount = {}
for vote in classList: #统计classList中每个元素出现的次数
if vote not in classCount.keys():classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key = operator.itemgetter(1), reverse = True) #根据字典的值降序排序
return sortedClassCount[0][0] #返回classList中出现次数最多的元素
"""
函数说明:创建决策树
Parameters:
dataSet - 训练数据集
labels - 分类属性标签
featLabels - 存储选择的最优特征标签
Returns:
myTree - 决策树
"""
def createTree(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分类标签(是否放贷:yes or no)
if classList.count(classList[0]) == len(classList): #如果类别完全相同则停止继续划分
return classList[0]
if len(dataSet[0]) == 1 or len(labels) == 0: #遍历完所有特征时返回出现次数最多的类标签
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #选择最优特征
bestFeatLabel = labels[bestFeat] #最优特征的标签
featLabels.append(bestFeatLabel)
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, featLabels)
return myTree
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
函数majorityCnt统计classList中出现此处最多的元素(类标签),函数createTree用来递归构建决策树。下面将决策树可视化。
####决策树的可视化
'''
说明:获取决策树叶子的数目
Parameters:
myTree - 决策树
Returns:
numLeafs - 决策树的叶子结点的数目
'''
def getNumLeaf(myTree):
#初始化叶子
numLeafs=0
firstStr=next(iter(myTree))
secondDict=myTree[firstStr]
#获取下一组字典
for key in secondDict.keys():
# 测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
if type(secondDict[key]).__name__ == 'dict':
numLeafs+=getNumLeaf(secondDict[key])
else:
numLeafs+=1
return numLeafs
'''
说明:获取决策树的层数
Parameters:
myTree - 决策树
Returns:
maxDepth - 决策树的层数
'''
def getTreeDepth(myTree):
#初始化决策树深度
maxDepth=0
firstStr=next(iter(myTree))
#获取下一个字典
secondDict=myTree[firstStr]
for key in secondDict.keys():
# 测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
if type(secondDict[key]).__name__ =='dict':
thisDepth=1+getTreeDepth(secondDict[key])
else:
thisDepth=1
#更新层数
if thisDepth > maxDepth:
maxDepth=thisDepth
return maxDepth
'''
绘制结点
Parameters:
nodeTxt - 结点名
centerPt - 文本位置
parentPt - 标注的箭头位置
nodeType - 结点格式
Returns:
无
'''
def plotNode(nodeTxt,centerPt,parentPt,nodeType):
#设置箭头格式
arrow_args=dict(arrowstyle="<-")
#设置中文字体
font=FontProperties(fname=r"c:\windows\fonts\simsun.ttc",size=14)
#绘制结点
createPlot.ax1.annotate(nodeTxt,xy=parentPt,xycoords='axes fraction',
xytext=centerPt,textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args, FontProperties=font
)
'''
说明:绘制有向边属性值
Parameters:
cntrPt、parentPt - 用于计算标注位置
txtString - 标注的内容
Returns:
无
'''
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)
"""
函数说明:绘制决策树
Parameters:
myTree - 决策树(字典)
parentPt - 标注的内容
nodeTxt - 结点名
Returns:
无
"""
def plotTree(myTree, parentPt, nodeTxt):
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
# 设置结点格式
leafNode = dict(boxstyle="round4", fc="0.8")
# 设置叶结点格式
numLeafs = getNumLeaf(myTree)
# 获取决策树叶结点数目,决定了树的宽度
depth = getTreeDepth(myTree)
# 获取决策树层数
firstStr = next(iter(myTree))
# 下个字典
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
# y偏移
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
# 测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
plotTree(secondDict[key], cntrPt, str(key))
# 不是叶结点,递归调用继续绘制
else: # 如果是叶结点,绘制叶结点,并标注有向边属性值
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
"""
函数说明:创建绘制面板
Parameters:
inTree - 决策树(字典)
Returns:
无
"""
def createPlot(inTree):
fig = plt.figure(1, facecolor='white') # 创建fig
fig.clf() # 清空fig
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) # 去掉x、y轴
plotTree.totalW = float(getNumLeaf(inTree)) # 获取决策树叶结点数目
plotTree.totalD = float(getTreeDepth(inTree)) # 获取决策树层数
plotTree.xOff = -0.5 / plotTree.totalW;
plotTree.yOff = 1.0; # x偏移
plotTree(inTree, (0.5, 1.0), '') # 绘制决策树
plt.show()
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
createPlot(myTree)
决策树图如下:
使用决策树进行分类
在进行数据分类的时候,需要决策树和用于构造树的标签向量,然后程序比较测试数据与决策树上的数值,递归执行然后进入叶子结点。最后将这个测试数据定义为叶子结点所属的类型。
在构建决策树时,featLabels参数用来记录各个分类结点的,在用决策树做预测的时候,我们按照顺序输入需要的分类结点的属性值即可。
比如,对上面已经训练好的模型,只需要提供这个人是否有房子是否有工作这两个信息就可以了,不需要其他的信息。
'''
说明:使用决策树进行分类
Parameters:
inputTree - 已经生成的决策树
featLabels - 存储选择的最优特征标签
testVec - 测试数据列表,顺序对应最优特征标签
Returns:
classLabel - 分类结果
'''
def classify(inputTree,featLabels,testVec):
firstStr = next(iter(inputTree)) # 获取决策树结点
secondDict = inputTree[firstStr] # 下一个字典
featIndex = featLabels.index(firstStr)
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
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
createPlot(myTree)
testVec = [0, 1] # 测试数据
result = classify(myTree, featLabels, testVec)
if result == 'yes':
print('放贷')
if result == 'no':
print('不放贷')
分类结果:
决策树的存储
但是如果我们每次想要分类的时候,难道每次都要训练一次吗?我们使用pickle序列化对象,将决策树进行保存。
import pickle
'''
说明:存储决策树
Parameters:
filename - 决策树的存储文件名
Returns:
pickle.load(fr) - 决策树字典
'''
def storeTree(inputTree,filename):
with open(filename,'wb') as fw:
pickle.dump(inputTree,fw)
if __name__ == '__main__':
myTree = {'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
storeTree(myTree, 'classifierStorage.txt')
上面是保存决策树的代码,将决策树保存在classifierStorage.txt文件里。
如果要使用的话,读取文件,将决策树读入内存即可。
import pickle
'''
说明:读取决策树
Parameters:
filename - 决策树的存储文件名
Returns:
pickle.load(fr) - 决策树字典
'''
def grabTree(filename):
fr=open(filename,'rb')
return pickle.load(fr)
if __name__ == '__main__':
myTree = grabTree('classifierStorage.txt')
print(myTree)
sklearn预测隐形眼镜类型
我们将通过一个例子讲解决策树如何预测患者需要佩戴的隐形眼镜类型。使用小数据集,我们就可以利用决策树学到很多知识:眼科医生是如何判断患者需要佩戴的镜片类型;一旦理解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。
隐形眼镜数据集是非常著名的数据集,它包含很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型。隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。数据来源于UCI数据库。
数据格式:
sklearn中的有sklearn.tree模块提供了决策树的模型,来解决分类和回归问题。
官网链接:官网链接
>> from sklearn.datasets import load_iris
>>> from sklearn.model_selection import cross_val_score
>>> from sklearn.tree import DecisionTreeClassifier
>>> clf = DecisionTreeClassifier(random_state=0)
>>> iris = load_iris()
>>> cross_val_score(clf, iris.data, iris.target, cv=10)
... # doctest: +SKIP
...
array([ 1. , 0.93..., 0.86..., 0.93..., 0.93...,
0.93..., 0.93..., 1. , 0.93..., 1. ])
官网给的实例。详细请看官网链接。
使用DecisionTreeClassifier模块完成决策树的构建,使用export_graphviz完成决策树的可视化。
sklearn.tree.DecisionTreeClassifier(criterion='gini', splitter='best', max_depth=None, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_features=None, random_state=None, max_leaf_nodes=None, min_impurity_decrease=0.0, min_impurity_split=None, class_weight=None, presort='deprecated', ccp_alpha=0.0)
参数如上。解释说明如下:
决策树的方法如下:
- criterion:特征选择标准,可选参数,默认是gini,可以设置为entropy。gini是基尼不纯度,是将来自集合的某种结果随机应用于某一数据项的预期误差率,是一种基于统计的思想。entropy是香农熵,也就是上篇文章讲过的内容,是一种基于信息论的思想。Sklearn把gini设为默认参数,应该也是做了相应的斟酌的,精度也许更高些?ID3算法使用的是entropy,CART算法使用的则是gini。
- splitter:特征划分点选择标准,可选参数,默认是best,可以设置为random。每个结点的选择策略。best参数是根据算法选择最佳的切分特征,例如gini、entropy。random随机的在部分划分点中找局部最优的划分点。默认的"best"适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐"random"。
- max_features:划分时考虑的最大特征数,可选参数,默认是None。寻找最佳切分时考虑的最大特征数(n_features为总共的特征数),有如下6种情况:
1.如果max_features是整型的数,则考虑max_features个特征;
2如果max_features是浮点型的数,则考虑int(max_features * n_features)个特征;
3如果max_features设为auto,那么max_features = sqrt(n_features);
4如果max_features设为sqrt,那么max_featrues = sqrt(n_features),跟auto一样;
5如果max_features设为log2,那么max_features = log2(n_features);
6如果max_features设为None,那么max_features = n_features,也就是所有特征都用。
一般来说,如果样本特征数不多,比如小于50,我们用默认的"None"就可以了,如果特征数非常多,我们可以灵活使用刚才描述的其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。 - max_depth:决策树最大深,可选参数,默认是None。这个参数是这是树的层数的。层数的概念就是,比如在贷款的例子中,决策树的层数是2层。如果这个参数设置为None,那么决策树在建立子树的时候不会限制子树的深度。一般来说,数据少或者特征少的时候可以不管这个值。或者如果设置了min_samples_slipt参数,那么直到少于min_smaples_split个样本为止。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。常用的可以取值10-100之间。
- min_samples_split:内部节点再划分所需最小样本数,可选参数,默认是2。这个值限制了子树继续划分的条件。如果min_samples_split为整数,那么在切分内部结点的时候,min_samples_split作为最小的样本数,也就是说,如果样本已经少于min_samples_split个样本,则停止继续切分。如果min_samples_split为浮点数,那么min_samples_split就是一个百分比,ceil(min_samples_split * n_samples),数是向上取整的。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。
- min_samples_leaf:叶子节点最少样本数,可选参数,默认是1。这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝。叶结点需要最少的样本数,也就是最后到叶结点,需要多少个样本才能算一个叶结点。如果设置为1,哪怕这个类别只有1个样本,决策树也会构建出来。如果min_samples_leaf是整数,那么min_samples_leaf作为最小的样本数。如果是浮点数,那么min_samples_leaf就是一个百分比,同上,celi(min_samples_leaf * n_samples),数是向上取整的。如果样本量不大,不需要管这个值。如果样本量数量级非常大,则推荐增大这个值。
- min_weight_fraction_leaf:叶子节点最小的样本权重和,可选参数,默认是0。这个值限制了叶子节点所有样本权重和的最小值,如果小于这个值,则会和兄弟节点一起被剪枝。一般来说,如果我们有较多样本有缺失值,或者分类树样本的分布类别偏差很大,就会引入样本权重,这时我们就要注意这个值了。
- max_leaf_nodes:最大叶子节点数,可选参数,默认是None。通过限制最大叶子节点数,可以防止过拟合。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。
- class_weight:类别权重,可选参数,默认是None,也可以字典、字典列表、balanced。指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策树过于偏向这些类别。类别的权重可以通过{class_label:weight}这样的格式给出,这里可以自己指定各个样本的权重,或者用balanced,如果使用balanced,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的None。
- random_state:可选参数,默认是None。随机数种子。如果是证书,那么random_state会作为随机数生成器的随机数种子。随机数种子,如果没有设置随机数,随机出来的数与当前系统时间有关,每个时刻都是不同的。如果设置了随机数种子,那么相同随机数种子,不同时刻产生的随机数也是相同的。如果是RandomState instance,那么random_state是随机数生成器。如果为None,则随机数生成器使用np.random。
- min_impurity_split:节点划分最小不纯度,可选参数,默认是1e-7。这是个阈值,这个值限制了决策树的增长,如果某节点的不纯度(基尼系数,信息增益,均方差,绝对差)小于这个阈值,则该节点不再生成子节点。即为叶子节点 。
- presort:数据是否预排序,可选参数,默认为False,这个值是布尔值,默认是False不排序。一般来说,如果样本量少或者限制了一个深度很小的决策树,设置为true可以让划分点选择更加快,决策树建立的更加快。如果样本量太大的话,反而没有什么好处。问题是样本量少的时候,我速度本来就不慢。所以这个值一般懒得理它就可以了。
from sklearn import tree
if __name__ == '__main__':
fr=open('lenses.txt')
lenses=[str.strip().split('\t') for str in fr.readlines()]
print(lenses)
lensesLabels=['age','prescript','astigmatic','tearRate']
clf=tree.DecisionTreeClassifier()
lenses=clf.fit(lenses,lensesLabels)
报错如下:
fit()不能拟合string的数据,需要对数据集编码。常用两种,一种是LabelEncoder:将字符串转换乘增量值。还有一种是onehot.将字符串转换成onehot的编码。
下面生成pandas数据,然后再进行LabelEncoder:编码。
from sklearn import tree
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.externals.six import StringIO
if __name__ == '__main__':
fr=open('lenses.txt')
lenses=[str.strip().split('\t') for str in fr.readlines()]
lenses_target=[]
#提取每组数据的类别,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
lensesLabels=['age','prescript','astigmatic','tearRate']
#保存lenses数据的临时列表
lenses_list=[]
#保存lenses数据的字典,用于生成pandas
lenses_dict={}
#提取信息生成字典
for each_label in lensesLabels:
for each in lenses:
lenses_list.append(each[lensesLabels.index(each_label)])
lenses_dict[each_label]=lenses_list
lenses_list=[]
##print(lenses_dict)
#使用dataframe生成pandas数据
lenses_pd=pd.DataFrame(lenses_dict)
print(lenses_pd)
#创建LabelEncoder()对象,用于序列化。
le=LabelEncoder()
for col in lenses_pd.columns:
lenses_pd[col]=le.fit_transform(lenses_pd[col])
print(lenses_pd)
结果如下:
决策树可视化
raphviz的是AT&T Labs Research开发的图形绘制工具,他可以很方便的用来绘制结构化的图形网络,支持多种格式输出,生成图片的质量和速度都不错。它的输入是一个用dot语言编写的绘图脚本,通过对输入脚本的解析,分析出其中的点,边以及子图,然后根据属性进行绘制。是使用Sklearn生成的决策树就是dot格式的,因此我们可以直接利用Graphviz将决策树可视化。
from sklearn import tree
import pandas as pd
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.externals.six import StringIO
import numpy as np
import pydotplus
import os
os.environ["PATH"] += os.pathsep + 'G:/soft_exe/Graphviz/bin/' #注意修改你的路径
if __name__ == '__main__':
fr=open('lenses.txt')
lenses=[str.strip().split('\t') for str in fr.readlines()]
lenses_target=[]
#提取每组数据的类别,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
lensesLabels=['age','prescript','astigmatic','tearRate']
#保存lenses数据的临时列表
lenses_list=[]
#保存lenses数据的字典,用于生成pandas
lenses_dict={}
#提取信息生成字典
for each_label in lensesLabels:
for each in lenses:
lenses_list.append(each[lensesLabels.index(each_label)])
lenses_dict[each_label]=lenses_list
lenses_list=[]
##print(lenses_dict)
#使用dataframe生成pandas数据
lenses_pd=pd.DataFrame(lenses_dict)
print(lenses_pd)
#创建LabelEncoder()对象,用于序列化。
le=LabelEncoder()
for col in lenses_pd.columns:
lenses_pd[col]=le.fit_transform(lenses_pd[col])
# print(lenses_pd)
#创建DecisionTreeClassifier()类
clf=tree.DecisionTreeClassifier(max_depth=4)
#使用数据构建决策树
clf=clf.fit(lenses_pd.values.tolist(),lenses_target)
dot_data=StringIO()
#绘制决策树
tree.export_graphviz(clf,out_file=dot_data,
feature_names=lenses_pd.keys(),
class_names=clf.classes_,
filled=True,
rounded=True,
special_characters=True
)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
#保存绘制的决策树
graph.write_pdf("tree.pdf")
决策树的图像:
需要注意的是可能你安装的Graphviz不能使用:在代码里面加上如下代码即可:
后面加的是你的安装路径。
import os
os.environ["PATH"] += os.pathsep + 'G:/soft_exe/Graphviz/bin/'
预测的代码如下:
print(clf.predict([[1,1,1,0]]))
学习资料
- Jack Cui的个人博客