前言
之前把《机器学习实战》这本书的分类部分学完了,想自己动手实践一下,所以从前面的章节开始,慢慢熟悉代码。
今天在学习决策树的时候,发现书中并没有直接给出预测隐形眼镜类型的代码,于是想借着这个机会自己实践一下。
在这过程中我使用原来的一些函数,比如创建决策树的函数,用来对官方给的文件进行分类,会出现
bestFeatLabel = labels[bestFeat]
IndexError: list index out of range
等错误,于是我就开始从头熟悉代码,print单步调试代码,最终得出了结果。
在原始代码上首先需要对文本数据进行编码
编码操作
编码操作是第一步,大家可以直接复制下面的代码看看结果。
我就直接上代码了,这也是我从博客中看到抄过来的
with open('lenses.txt', 'r') as fr: # 加载文件
lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 处理文件
lenses_target = [] # 提取每组数据的类别,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
print(lenses)
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate','class'] # 特征标签
lenses_list = [] # 保存lenses数据的临时列表
lenses_dict = {} # 保存lenses数据的字典,用于生成pandas
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) #打印字典信息
lenses_pd = pd.DataFrame(lenses_dict) # 生成pandas.DataFrame
print(lenses_pd) # 打印pandas.DataFrame
le = LabelEncoder() # 创建LabelEncoder()对象,用于序列化
for col in lenses_pd.columns: # 为每一列序列化
lenses_pd[col] = le.fit_transform(lenses_pd[col])
print(lenses_pd)
然后就可以使用创建决策树函数了。
代码
过程我也不多提了,直接上代码,想懂原理的请看我之前写的博客就好了。在这里,会出现我前言里面说过的错误,但通过单步调试,找出了问题所在,并解决了它。原因和解决方法我都写在了相应位置的注释,可以参考一下
# -*- coding: UTF-8 -*-
from math import log
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import operator
import pickle
import pandas as pd
from sklearn.preprocessing import LabelEncoder
import pydotplus
import six
"""
函数说明:计算给定数据集的经验熵(香农熵)
Parameters:
dataSet - 数据集
Returns:
shannonEnt - 经验熵(香农熵)
"""
def calcShannonEnt(dataSet):
numEntires = len(dataSet) #返回数据集的行数,样本容量
labelCounts = {} #保存每个标签(Label)出现次数的字典
for featVec in dataSet: #对每组特征向量进行统计
currentLabel = featVec[-1] #提取标签(Label)信息
if currentLabel not in labelCounts.keys(): #如果标签(Label)没有放入统计次数的字典,添加进去
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1 #Label计数
shannonEnt = 0.0 #经验熵(香农熵)
#print(labelCounts)
for key in labelCounts: #计算香农熵
#print(key)
#print(labelCounts[key])
prob = float(labelCounts[key]) / numEntires #选择该标签(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式计算
return shannonEnt #返回经验熵(香农熵)
"""
函数说明:创建测试数据集
Returns:
dataSet - 数据集
labels - 分类属性
"""
def createDataSet():
dataSet = [[0, 0, 0, 0, 'no'], #数据集
[0, 0, 0, 1, 'no'],
[0, 1, 0, 1, 'yes'],
[0, 1, 1, 0, 'yes'],
[0, 0, 0, 0, 'no'],
[1, 0, 0, 0, 'no'],
[1, 0, 0, 1, 'no'],
[1, 1, 1, 1, 'yes'],
[1, 0, 1, 2, 'yes'],
[1, 0, 1, 2, 'yes'],
[2, 0, 1, 2, 'yes'],
[2, 0, 1, 1, 'yes'],
[2, 1, 0, 1, 'yes'],
[2, 1, 0, 2, 'yes'],
[2, 0, 0, 0, 'no']]
labels = ['年龄', '有工作', '有自己的房子', '信贷情况'] #分类属性
return dataSet, labels #返回数据集和分类属性
"""
函数说明:按照给定特征划分数据集
Parameters:
dataSet - 待划分的数据集
axis - 划分数据集的特征
value - 需要返回的特征的值
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #创建返回的数据集列表
for featVec in dataSet: #遍历数据集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #去掉axis特征
reducedFeatVec.extend(featVec[axis+1:]) #将符合条件的添加到返回的数据集
retDataSet.append(reducedFeatVec)
print("划分后的数据集:", retDataSet)
return retDataSet #返回划分后的数据集
"""
函数说明:选择最优特征
Parameters:
dataSet - 数据集
Returns:
bestFeature - 信息增益最大的(最优)特征的索引值
"""
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量,-1是因为最后一列是类别标签
#print("特征数量为:%d" % numFeatures)
baseEntropy = calcShannonEnt(dataSet) #计算数据集的香农熵
bestInfoGain = 0.0 #信息增益
bestFeature = -1 #最优特征的索引值
for i in range(numFeatures): #遍历所有特征
#获取dataSet的第i个所有特征存到featList中
featList = [example[i] for example in dataSet]#已用for验证,把dataSet中的每一行的第i个数据放到featList中
#print(featList)#每个特征的15项特征值列表
uniqueVals = set(featList) #创建set集合{},元素不可重复
#print(uniqueVals)#去除重复项
newEntropy = 0.0 #经验条件熵
#把特征项的数据集分开,去除重复项的原因是将第i个特征的数据分离,对这个特征的进行经验熵的计算
for value in uniqueVals: #计算信息增益
subDataSet = splitDataSet(dataSet, i, value) #subDataSet划分后的子集
#print(subDataSet)
prob = len(subDataSet) / float(len(dataSet)) #计算子集的概率=子集个数除以整个训练集样本个数
newEntropy += prob * calcShannonEnt(subDataSet) #根据公式计算经验条件熵
infoGain = baseEntropy - newEntropy #信息增益
#print("第%d个特征的增益为%.3f" % (i, infoGain)) #打印每个特征的信息增益
if (infoGain > bestInfoGain): #计算信息增益
bestInfoGain = infoGain #更新信息增益,找到最大的信息增益
bestFeature = i #记录信息增益最大的特征的索引值
return bestFeature #返回信息增益最大的特征的索引值
"""
函数说明:创建决策树
Parameters:
dataSet - 训练数据集
labels - 分类属性标签
featLabels - 存储选择的最优特征标签
Returns:
myTree - 决策树
"""
def createTree2(dataSet, labels, featLabels):
classList = [example[-1] for example in dataSet] #取分类标签(是否放贷:yes or no)
#print(len(dataSet[0]))
if classList.count(classList[0]) == len(classList): #如果类别完全相同则停止继续划分
return classList[0]
if len(dataSet[0]) == 1: #遍历完所有特征时返回出现次数最多的类标签
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: #遍历特征,创建决策树。
myTree[bestFeatLabel][value] = createTree2(splitDataSet(dataSet, bestFeat, value), labels, featLabels)
return myTree
"""
函数说明:统计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):
print("****************************************************")
print("数据集为:", dataSet)
classList = [example[-1] for example in dataSet] #取分类标签(是否放贷:yes or no)
"""
特征可能存在多个属性,所以在此判断一下,如果类别完全相同则停止继续划分
"""
if classList.count(0) == len(classList): #如果类别完全相同则停止继续划分
print("停止划分0")
return 0
elif classList.count(1) == len(classList):
print("停止划分1")
return 1
elif classList.count(2) == len(classList):
print("停止划分2")
return 2
if len(dataSet[0]) == 1: #遍历完所有特征时返回出现次数最多的类标签
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #选择最优特征
print("最优特征为:%d" % bestFeat)
bestFeatLabel = labels[bestFeat] #最优特征的标签
print("最优特征的标签:", bestFeatLabel)
featLabels.append(bestFeatLabel)
myTree = {bestFeatLabel:{}} #根据最优特征的标签生成树
print("当前根据最优特征生成的树:", myTree)
print("准备删除的特征标签为:", labels[bestFeat])
#如果是到了三个属性的特征值里,那么不能让他就这么把前面没有创建完的特征标签删除了,因为他是从一个属性递归到底再回到另一个属性进行递归
del (labels[bestFeat]) # 删除已经使用特征标签'age', 'prescript', 'astigmatic', 'tearRate'
print("labels标签里还有:", labels)
featValues = [example[bestFeat] for example in dataSet] #得到训练集中所有最优特征的属性值
print("得到训练集中最优特征的属性值:", featValues)
uniqueVals = set(featValues) #去掉重复的属性值
print("去掉重复属性:", uniqueVals)
for value in uniqueVals: #0/1/2 遍历特征,创建决策树。
if len(uniqueVals) == 3:
print("***************************************************************************************三个属性的第", value)
subLabels = labels[:]
"""
我在没有加subLabels = labels[:]的时候,会报如下错误:
bestFeatLabel = labels[bestFeat] #最优特征的标签
IndexError: list index out of range
经过单步调试与print结果,才发现:
当我输入数据中的特征存在不止2个属性的时候,在递归内每次都会删除已经使用的标签,这就可能存在删除一个还没有全部迭代完的属性
(比如只迭代了0属性到树底,回过头来迭代1属性时,发现已经在迭代到树底过程中就被删除了)
所以为了防止误删除,我们需要在迭代一个属性时复制所有标签,这样树就不会弄乱现有的标签
"""
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, featLabels)
print("当前树:", myTree)
return myTree
"""
函数说明:获取决策树叶子结点的数目
Parameters:
myTree - 决策树
Returns:
numLeafs - 决策树的叶子结点的数目
"""
def getNumLeafs(myTree):
numLeafs = 0 #初始化叶子
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0]
secondDict = myTree[firstStr] #获取下一组字典
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict': #测试该结点是否为字典,如果不是字典,代表此结点为叶子结点
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
"""
函数说明:获取决策树的层数
Parameters:
myTree - 决策树
Returns:
maxDepth - 决策树的层数
"""
def getTreeDepth(myTree):
maxDepth = 0 #初始化决策树深度
firstStr = next(iter(myTree)) #python3中myTree.keys()返回的是dict_keys,不在是list,所以不能使用myTree.keys()[0]的方法获取结点属性,可以使用list(myTree.keys())[0]
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 - 结点格式
"""
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 - 标注的内容
"""
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 - 结点名
"""
def plotTree(myTree, parentPt, nodeTxt):
decisionNode = dict(boxstyle="sawtooth", fc="0.8") #设置结点格式
leafNode = dict(boxstyle="round4", fc="0.8") #设置叶结点格式
numLeafs = getNumLeafs(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 - 决策树(字典)
"""
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(getNumLeafs(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() #显示绘制结果
"""
函数说明:使用决策树分类
Parameters:
inputTree - 已经生成的决策树
featLabels - 存储选择的最优特征标签
testVec - 测试数据列表,顺序对应最优特征标签
Returns:
classLabel - 分类结果
"""
def classify(inputTree, featLabels, testVec):
firstStr = next(iter(inputTree)) #获取决策树结点
print("获取决策树结点:",firstStr)
secondDict = inputTree[firstStr] #下一个字典
print("下一个字典:",secondDict)
featIndex = featLabels.index(firstStr)#获取存储选择的最优特征标签的索引
print("获取存储选择的最优特征标签的索引:",featIndex)
for key in secondDict.keys():#遍历字典的键
print("字典的键:",key)
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else: classLabel = secondDict[key]
return classLabel
"""
函数说明:存储决策树
Parameters:
inputTree - 已经生成的决策树
filename - 决策树的存储文件名
"""
def storeTree(inputTree, filename):
with open(filename, 'wb') as fw:
pickle.dump(inputTree, fw)
"""
函数说明:读取决策树
Parameters:
filename - 决策树的存储文件名
Returns:
pickle.load(fr) - 决策树字典
"""
def grabTree(filename):
fr = open(filename, 'rb')
return pickle.load(fr)
if __name__ == '__main__':
"""计算香农熵、条件经验熵、信息增益、选择最优特征
dataSet, labels = createDataSet()
print("最优特征索引值:" + str(chooseBestFeatureToSplit(dataSet)))
"""
"""
dataSet, labels = createDataSet()
print(dataSet)
featLabels = []
myTree = createTree2(dataSet, labels, featLabels)
print(myTree)
"""
"""可视化决策树
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
createPlot(myTree)
"""
"""决策树进行分类
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
testVec = [0, 1] # 测试数据
result = classify(myTree, featLabels, testVec)
if result == 'yes':
print('放贷')
if result == 'no':
print('不放贷')
"""
"""
进行编码
"""
with open('lenses.txt', 'r') as fr: # 加载文件
lenses = [inst.strip().split('\t') for inst in fr.readlines()] # 处理文件
lenses_target = [] # 提取每组数据的类别,保存在列表里
for each in lenses:
lenses_target.append(each[-1])
print(lenses)
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate','class'] # 特征标签
lenses_list = [] # 保存lenses数据的临时列表
lenses_dict = {} # 保存lenses数据的字典,用于生成pandas
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) #打印字典信息
lenses_pd = pd.DataFrame(lenses_dict) # 生成pandas.DataFrame
print(lenses_pd) # 打印pandas.DataFrame
le = LabelEncoder() # 创建LabelEncoder()对象,用于序列化
for col in lenses_pd.columns: # 为每一列序列化
lenses_pd[col] = le.fit_transform(lenses_pd[col])
print(lenses_pd)
featLabels = []
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 特征标签
myTree = createTree(lenses_pd.values.tolist(), lensesLabels, featLabels)
print("树结构:",myTree)
print("存储选择的最优特征标签:",featLabels)
testVec = [1,1,1,0] # 测试数据
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate'] # 特征标签
"""
这里我给的参数是lensesLabels
所以测试数据他是按照你的特征标签给的标准来进行分类的
如果参数给的是featLabels
那么测试数据他是按照你的树结构来给值才会成功分类
"""
result = classify(myTree, lensesLabels, testVec)
if result == 0 :
print("类型为:hard ")
elif result == 1:
print("类型为:no lenses ")
elif result == 2:
print("类型为:soft ")
createPlot(myTree)
这就是全部的代码,中间有许多打印的东西还有许多注释我没有删,主要是懒,看结果就好了,可以通过修改测试数据来得到不同的分类结果。