《机器学习》西瓜书课后习题4.3——python实现基于信息熵划分的决策树算法
《机器学习》西瓜书P93
4.3 试编程实现基于信息熵进行划分选择的决策树算法,并为表4.3中数据生成一棵决策树。
以下内容是使用sklearn快捷生成的决策树,由于该函数的局限性,所得到的决策树是二叉树,但是也能做到相对正确的分类,同时在敲代码的过程中参考了大量资料和博客,在此文中一并总结了决策树构造所踩的坑和学到的东西,请大家仔细阅读
前言(都是坑、运行代码前务必阅读、理解以下部分并配置好相关的环境)
初识sklearn构造决策树
python中的sklearn库中含有DecisionTree的构造方法,在本实例中我们主要是采用该方法来构造决策树,一般方法代码量较大且复杂,如果想要详细实现决策树构建的每一步请参见其他博客。下面介绍一下python中的DecisionTree构造方法。
各种划分选择算法理论参见我的另一篇博客:https://blog.csdn.net/qq_45955883/article/details/116481131
python中sklearn库中的DecisionTreeClaasifier使用方法参见官方文档:官方文档
在python使用此类方法是ID3和CART之间存在差别,具体参见博客:
数据编码
注意:在使用sklearn构造决策树的过程中你会发现,该方法只针对数据部分为数值的数据集进行构造,像“色泽:青绿”这种字符串类型是无法进行处理的,因此,我们需要解决的问题是如何将这些字符串转换成数字来标识,该思路简言之就是对字符串进行编码处理。
同样地,在sklearn中也存在一部分函数用于离散型编码,对于不同的函数对应于不同的编码类型,为了降低编码这一过程对决策树的产生所造成的影响最低,同时也要考虑最终输出结果能够清晰明了,那么我们究竟该选择哪一种编码方式对我们的数据进行合适的编码呐?
关于sklearn中的多种离散型编码方式,参见博客:sklearn中多种编码方式——category_encoders(one-hot多种用法)
经过最终的比较和分析,我们选定使用DictVectorizer方法来对数据中的离散型内容进行编码,同时使用preprocessing.LabelBinarizer对标记进行编码,原因如下
-
使用preprocessing.LabelBinarizer对标记进行编码很容易理解,就是将标签进行二值化,即’1’代表’是‘,’0‘代表‘否’。从而将字符转换为0-1表示。
-
DictVectorizer的处理对象是符号化(非数字化)的但是具有一定结构的特征数据,如字典等,将符号转成数字0/1表示。这句话是什么意思呐?下面看一个例子就能明白
#-*- coding:utf-8 -*- #学习目标:使用DictVectorizer对使用字典存储的数据进行特征抽取和向量化 #定义一组字典列表,用来表示多个数据样本(每个字典代表一个数据样本) measurements = [{'city':'Beijing','temperature':33.},{'city':'London','temperature':12.},{'city':'San Fransisco','temperature':18.}] #从sklearn.feature_extraction导入DictVectorizer from sklearn.feature_extraction import DictVectorizer vec = DictVectorizer() #输出转化后的特征矩阵 print vec.fit_transform(measurements).toarray() #输出各个维度的特征含义 print vec.get_feature_names()
输出结果为:
[[ 1. 0. 0. 33.] [ 0. 1. 0. 12.] [ 0. 0. 1. 18.]]
['city=Dubai', 'city=London', 'city=San Fransisco', 'temperature']
我们不难发现,DictVectorizer对非数字化的处理方式是,借助原特征的名称,组合成新的特征,并采用0/1的方式进行量化,而数值型的特征转化比较方便,一般情况维持原值即可。
其中[ 1. 0. 0. 33.]代表的含义是’city=Dubai,temperature=33’,因为数组第一个元素为1,故表示‘city=Dubai’被选中,最后的元素代表‘temperature’的具体的值;该方法只针对离散型的值进行编码,对原本为数值的不修改值。
那么对于我们的西瓜数据集2.0为什么适用于这种方法呐?
首先,我们的数据集中‘密度’和‘含糖率’是连续的,其他属性是离散的,因此使用上述方法可以统一进行一次性编码,无需分开编码后再合并。
第二,该方法中的离散型数据均使用0-1来表示,就好像线性模型中的对率回归中使用的“单位阶跃函数”一样,对于x>0.5,可认为x=1,而x<0.5代表x=0,虽然表面看似是连续性的数值,实际其离散的特性已经隐含其中,并未因编码而对数据原本的特性产生较大的影响。
接下来我们来谈一谈决策树的可视化问题……
决策树的可视化
同样的,python中也提供了决策树可视化的库和函数,即Graphviz,我们只需要在决策树建立完成之后将其导出到graphviz,
可视化软件gvedit.exe 免费下载地址:graphviz-2.38.msi免费下载
运行程序后会产生一个dot文件,使用gvedit.exe打开即可,注意:由于我的程序的问题最终产生的dot文件中文无法正确输出,需要以txt文本打开后在文本中添加多行命令:fontname=“SimSun”,添加到节点内,具体的操作百度搜索一下就知道了……
综上,我们大致可以得到编程的主要思路,读取数据后使用二值化进行编码,将字符串类型的数据编码为0、1,而数字型数据保持float类型不做改变,然后利用sklearn建立决策树,完成之后利用graphviz进行可视化输出即可。
下面给出每一步实现的程序代码,并在个别代码附加详细解释和说明……
-
数据集——西瓜数据集3.0.csv(我们csv文件的编码类型是GBK,在使用文件读取数据的时候注意下!)
编号,色泽,根蒂,敲声,纹理,脐部,触感,密度,含糖率,好瓜 1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,0.697,0.46,是 2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,0.774,0.376,是 3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,0.634,0.264,是 4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,0.608,0.318,是 5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,0.556,0.215,是 6,青绿,稍蜷,浊响,清晰,稍凹,软粘,0.403,0.237,是 7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,0.481,0.149,是 8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,0.437,0.211,是 9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,0.666,0.091,否 10,青绿,硬挺,清脆,清晰,平坦,软粘,0.243,0.267,否 11,浅白,硬挺,清脆,模糊,平坦,硬滑,0.245,0.057,否 12,浅白,蜷缩,浊响,模糊,平坦,软粘,0.343,0.099,否 13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,0.639,0.161,否 14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,0.657,0.198,否 15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,0.36,0.37,否 16,浅白,蜷缩,浊响,模糊,平坦,硬滑,0.593,0.042,否 17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,0.719,0.103,否
-
主要代码块
-
建立一个函数is_number,作用是将数据中的字符型数字转换为float类型。
def is_number(n): is_number = True try: num = float(n) # 检查 "nan" is_number = num == num # 或者使用 `math.isnan(num)` except ValueError: is_number = False return is_number
-
建立一个函数用于读取数据,并将数据进行预处理
def loadData(filename): data=open(filename,'r',encoding='GBK') reader = csv.reader(data) headers = next(reader) featureList = [] labelList = [] for row in reader: labelList.append(row[len(row)-1]) rowDict = {} for i in range(1,len(row)-1): if is_number(row[i]) == True: rowDict[headers[i]] = float(row[i]) else: rowDict[headers[i]]=row[i] featureList.append(rowDict) print(featureList) print(labelList) return featureList,labelList
-
构造决策树并进行可视化输出
def createDTree(featureList,labelList): vec = DictVectorizer() dummyX = vec.fit_transform(featureList).toarray() # print("dummyX:"+str(dummyX)) # print(vec.get_feature_names()) # print("labelList:"+str(labelList)) lb = preprocessing.LabelBinarizer() dummyY = lb.fit_transform(labelList) # print("dummyY: ",str(dummyY)) clf = tree.DecisionTreeClassifier(criterion='gini') clf = clf.fit(dummyX,dummyY) print("clf: ",str(clf)) print(vec.get_feature_names()) print(dummyX) print(dummyY) target_name=['不是好瓜','是好瓜'] with open("DecisionTree_gini.dot",'w') as f: f = tree.export_graphviz(clf,feature_names=vec.get_feature_names(),class_names=target_name,out_file=f)
-
外部调用函数
filename='西瓜数据集3.0.csv' featureList,labelList=loadData(filename) createDTree(featureList,labelList)
-
-
决策树可视化展示
-
基于基尼指数建立的决策树
-
-
基于基尼指数建立的决策树2
-
基于信息增益建立的决策树
针上面生成的决策树图像的几点说明:
- 基于基尼指数生成的决策树有两种,why?参见博客:sklearn 决策树相同数据集及模型参数为什么生成的模型不一样?
- 图的阅读方法,以第一幅图为例现予以解释:第一个节点中的samples=17,说明此时样本个数为17个,gini=0.498代表基尼指数,根据”含糖率<=0.204“分为小于0.204 和大于0.204两个分支;对于第二个节点”触感=硬滑<=0.5"是什么意思呢?我们最初在进行编码的时候设置的自己具有的属性值为1,不具有的为0,即如果该样本”触感=硬滑“则对应的值为1,否则为0,那么”触感=硬滑<=0.5"的意思是”触感=硬滑“这一属性值为0(x小于0.5,x=0),即触感不硬滑;如果为True(代表触感不硬滑)则分到“色泽=乌黑<=0.5”这一节点,如果为False(代表触感硬滑)则分到“不是好瓜”这一节点。
【完整源代码】
'''
4.3 试编程实现基于信息熵进行划分选择的决策树算法,并为表4.3中数据生成一棵决策树
'''
import csv
from sklearn.feature_extraction import DictVectorizer
from sklearn import preprocessing
from sklearn import tree
def is_number(n):
is_number = True
try:
num = float(n)
# 检查 "nan"
is_number = num == num # 或者使用 `math.isnan(num)`
except ValueError:
is_number = False
return is_number
def loadData(filename):
data=open(filename,'r',encoding='GBK')
reader = csv.reader(data)
headers = next(reader)
featureList = []
labelList = []
for row in reader:
labelList.append(row[len(row)-1])
rowDict = {}
for i in range(1,len(row)-1):
if is_number(row[i]) == True:
rowDict[headers[i]] = float(row[i])
else:
rowDict[headers[i]]=row[i]
featureList.append(rowDict)
print(featureList)
print(labelList)
return featureList,labelList
def createDTree(featureList,labelList):
vec = DictVectorizer()
dummyX = vec.fit_transform(featureList).toarray()
# print("dummyX:"+str(dummyX))
# print(vec.get_feature_names())
# print("labelList:"+str(labelList))
lb = preprocessing.LabelBinarizer()
dummyY = lb.fit_transform(labelList)
# print("dummyY: ",str(dummyY))
clf = tree.DecisionTreeClassifier(criterion='gini')
clf = clf.fit(dummyX,dummyY)
print("clf: ",str(clf))
print(vec.get_feature_names())
print(dummyX)
print(dummyY)
target_name=['不是好瓜','是好瓜']
with open("DecisionTree_gini.dot",'w') as f:
f = tree.export_graphviz(clf,feature_names=vec.get_feature_names(),class_names=target_name,out_file=f)
filename='西瓜数据集3.0.csv'
featureList,labelList=loadData(filename)
createDTree(featureList,labelList)
后记
内容较多代码若有错误请及时留言联系,可能上述代码并不符合题目上要求使用的方法,鉴于代码复杂度和任务量故选择此类简单快速实现的方法来构造决策树,如有任何问题欢迎留言讨论交流!!!