《ML in Action》笔记(2) —— ID3决策树
算法概述
-
决策树是一种分类器,通过一系列“精心设计”的检验记录属性的问题路径,将记录归入某一个类别。构造决策树需要使用训练数据集,找到最优的问题路径设置。建立决策树后,既可用于对新纪录的分类判决。
-
决策树的问题路径并不是随意设置的,而是要遵循最优/次最优的规则。理论上对于给定的属性集,可构造的决策树数量为指数级,而在实际构造中,一般采用贪心策略的递归算法。其核心思路是在构建每层分类问题是都遵循信息增益最大原则(即熵或基尼不纯性最小)。
-
直观地理解,最优划分策略就是能够将不同类别的记录尽可能的区分开来。如果经过一个检验问题后,不同类别的记录还是混杂在每个叶结点,这个问题肯定不是一个好的分类问题。
-
最优划分策略的度量有几种:熵、基尼不纯性、分类误差。这三种度量是一致的,而熵的区分度较好,所以一般就使用熵。(关于熵的直观理解比较困难,但熵是一个超经典的度量变量,可参见信息论有关内容)
-
建立决策树的另一个关键问题是决策树的停止条件,也就是决策树的大小,每个叶结点终止下分的条件。
构建决策树(ID3算法)
- 主要步骤:
- 按照某一个属性进行数据集划分
- 计算每一种划分的香农熵(信息增益)
- 选出最优的(信息增益最大)的划分属性
- 按该属性划分后,对子结点进行递归
- 对于达到停止条件的子结点,根据记录数选举分类结果
【trees.py: part1 —— 决策树核心函数calcShannonEnt()】
def calcShannonEnt(dataSet):
# 数据集记录个数
numEntries = len(dataSet)
# 初始化,保存各分类的记录个数
labelCounts = {}
# 遍历数据集,统计各分类标签的记录个数
for featVec in dataSet:
# 当前记录的分类标签
currentLabel = featVec[-1]
# 分类标签计数+1
if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
# 初始化,保存香农熵计算结果
shannonEnt = 0.0
# 遍历标签dict
for key in labelCounts:
# 计算频率
prob = float(labelCounts[key])/numEntries
# 累加熵
shannonEnt -= prob * log(prob,2) #log base 2
#返回熵
return shannonEnt
【trees.py: part2 —— 决策树核心函数splitDataSet()】
# 按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):
# 初始化,划分后的数据集
retDataSet = []
# 遍历数据集
for featVec in dataSet:
# 指定属性=输入参数时:
if featVec[axis] == value:
# 提取该记录其他属性值,拼接
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
# 将该新纪录加入划分后数据集
retDataSet.append(reducedFeatVec)
# 返回划分后数据集
return retDataSet
【trees.py: part3 —— 决策树核心函数chooseBestFeatureToSplit()】
# 选出划分后信息增益最大的属性
def chooseBestFeatureToSplit(dataSet):
# 取数据集的属性数
numFeatures = len(dataSet[0]) - 1
# 计算数据集的初始香农熵
baseEntropy = calcShannonEnt(dataSet)
# 初始化,最大增益、最佳属性ID
bestInfoGain = 0.0; bestFeature = -1
#遍历所有属性值,进行拆分、计算熵
for i in range(numFeatures):
# 列表推导式,取出所有记录的当前属性值
featList = [example[i] for example in dataSet]
# 数值剔重
uniqueVals = set(featList)
newEntropy = 0.0
# 遍历该属性所有数值,取得其划分后数据集
for value in uniqueVals:
# 调用函数,提取划分后数据集
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
# 计算按该属性划分的信息增益
infoGain = baseEntropy - newEntropy
# 判断是否信息增益最大
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
# 输出信息增益最大的属性ID
return bestFeature
【trees.py: part4 —— 决策树核心函数majorityCnt()】
# 叶结点最终归属类别的表决
def majorityCnt(classList):
# 初始化归属类别的投票计数器
classCount={}
# 遍历该叶节点内所有数据点的类别
for vote in classList:
# 类别计数
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
# 排序选出记录数最多的类别
sortedClassCount = sorted(classCount.items(), key=lambda item:item[1], reverse=True)
# 返回类别标签
return sortedClassCount[0][0]
【trees.py: part5 —— 决策树核心函数createTree()】
# 创建递归决策树
def createTree(dataSet,lb):
# 由于传址参数的问题。故增加一行代码,将lb变量另外复制一份。使用copy.copy()
labels = copy.copy(lb)
# 取出训练数据集的所有类别信息
classList = [example[-1] for example in dataSet]
# 巧用count(),统计分类列表中与第一个结果相同的个数,若该个数等于数组总长度,则说明所有记录归属同一类别,停止划分
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:{}}
# 在labels中删去已用于划分的属性
# 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改,所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
del(labels[bestFeat])
# 取出该属性的所有值
featValues = [example[bestFeat] for example in dataSet]
# 剔重数值
uniqueVals = set(featValues)
# 遍历数值,对所有数值划分后的数据子集进行递归
for value in uniqueVals:
# 复制当前labels,以便原变量不被改变
subLabels = labels[:]
# splitDataSet对最佳属性的当前数值进行划分,然后递归调用createTree,子进程构建的决策树逐层代入myTree树结构
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
【trees.py: part6 —— 决策树核心函数classify()】
def classify(inputTree,featLabels,testVec):
# 获得首层属性名称
# 注:原代码为firstStr = myTree.keys()[0]。在python3中报错'dict_keys' object does not support indexing。因为python3的dict.keys返回dict_keys对象,支持iterable但不支持indexable,所以要将类型转换为list。
firstStr = list(inputTree.keys())[0]
# 获得首层树DICT
secondDict = inputTree[firstStr]
# 反查首层树所用属性的ID
featIndex = featLabels.index(firstStr)
# 获得测试向量的该属性数值
key = testVec[featIndex]
# 获得该数值对应的子结点
valueOfFeat = secondDict[key]
# 如果子结点是字典,则递归继续分类;否则就得到分类结果
if isinstance(valueOfFeat, dict):
# valueOfFeat是下层树,可直接用于递归
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
# 返回分类结果
return classLabel
【trees.py: part7 —— 存储和调用决策树】
# 将决策树存储
def storeTree(inputTree,filename):
import pickle
# pickle(除了最早的版本外)是二进制格式的,打开文件需带 'b' 标志。
fw = open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
# 读取决策树
def grabTree(filename):
import pickle
fr = open(filename,'rb')
return pickle.load(fr)
【IPYTHON —— 运行案例lenses】
fr = open('lenses.txt')
#读取行、拆分、去除回车符
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate']
lensesTree = trees.createTree(lenses,lensesLabels)