1.分析
1.1 背景和意义:
相信很多人都玩过一个网络上传的游戏,脑海里面想一个名人的名字,然后出若干多道问题,比如男的女的,国外的国内的,你只能答是或不是,最后给出你想的那个名人是谁。只要不是很偏的应该都能想出来,一般人觉得很震惊,其实这只是一种简单机器算法——决策树的应用而已。
决策树简单易懂,符合人的处理问题的方式,用在专家系统中甚至可以匹敌在当前领域具有十几年工作经验的专家。可以帮助我们解决分类与回归两类问题。本文讲解的是分类决策树。
在如今的机器学习领域中,树模型可以说是最为重要的模型,在各种数据挖掘的竞赛中和生产环境中,威力巨大的大杀器XGboost和lightGBM 两个工具都是基于树模型构建的。学习决策树,要掌握两个关键问题:第一,面对一个实际的数据集,应该如何构建出一颗树;第二,在构建树的过程中,树分裂节点时,如何选择出最优的属性作为分裂节点。
1.2 定义:
分类决策树是一种对实例进行分类的树形结构。由结点和有向边组成。结点有两种类型:内结点(表示一个特征),叶节点(表示一个类别)。
- 怎么应用决策树来进行分类呢?
首先,从根结点开始,对实例的当前分类的特征进行测试得到它属于该特征的哪一个取值分支,然后分配到该特征取值子节点(每个子结点对应该特征的一个取值),对子结点做同样的事,递归地对实例进行测试分配,直到到达叶结点,就分配到对应的类别中了。
- 为什么这样可以进行分类?
决策树可以看成是一个if-then规则的集合,又或看成一个条件概率分布。由决策树的根结点到叶结点的每一条路构建起一条分类规则:路径上的内结点的特征对应该规则的一个条件,叶结点的类别对应按照该规则分类的结果。决策树的路径是互斥且完备的,就是每一个实例进入该树最终只能通过一条路径到达唯一的结果。
1.3 决策树分类算法的原理
- 面对一个实际的数据集,应该如何构建出一颗决策分类树;
概述:递归地选择当前最优特征,并根据该特征对训练数据集进行分割,使得对各个子数据数据集都有一个最好的分类的过程,这个过程在划分特征空间的同时也构建了决策树。
具体:开始构建根结点,将所有的数据都先放在根结点,选择一个最优特征来分割训练数据集成子集。如果子集已经基本正确分类(属于同一类),就构建叶结点把子集放入其中;如果有些子集不能基本分类,就对其选择一个最优特征继续分割……如此递归,直到所有训练数据子集都被基本分类、不再有合适的分类特征为止。这样每一个子集都分到叶结点上,分出了对应的类别,同时也构建了决策树。
- 遇到的问题:
过拟合问题:要剪枝,使树变简单有泛化能力:就是去掉过于细分的叶结点,直接归入父结点或者更高结点成为新的叶结点。
特征过多:要选择有足够分类能力的特征。
所以决策树学习算法包含特征选择,决策树的生成,决策树的剪枝
模型通过局部最优生成决策树(在当前结点做决定),但剪枝要考虑全局最优(看哪些才是细分的节点)
常用的有三种算法:ID3、C4.5与CART。CART可以用来分类也可以用来回归,在后面的博客中会继续讲到。
- 那么在构建树的过程中,树分裂节点时,如何选择出最优的属性作为分裂节点?
我们可以用信息增益或者信息增益比来进行决策树的划分属性选择。
信息增益:表示得知特征X的信息而使得类Y的信息的不确定性减少的程度,公式如下:
ID3 决策树学习算法就是以信息增益为准则来选择划分属性。
问题:信息增益准则对可取值数目较多的属性有所偏好,就好比,我们获取的样本集数据中包含了一条属性是每个样本的 ID 号,那么根据信息增益的计算公式可以得到,当使用 ID 号作为划分属性时,每个分支肯定都是一个类别,它的信息增益率极高,但是不具备泛化能力,无法对新样本进行有效预测。
为了解决这个问题,就用到了信息增益比。
信息增益比:信息增益与训练集D关于特征A的值的熵之比,公式如下:
C4.5决策树学习算法就是以信息增益为准则来选择划分属性。
2.实践(《机器学习实战》代码解析)
3-1 计算香农熵
from math import log
import operator
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
3-2 划分数据集
def splitDataSet(dataSet, axis, value):#样本数据,要选取的特征索引(第几个特征),特征的某一取值
retDataSet = [] # 初始化分割后的样本集为空
for featVec in dataSet: # 逐行读取样本
if featVec[axis] == value: # 奇妙:把第axis个特征等于value的除掉该特征项后输出,就是把左边和右边的合并,就等于除掉自己
reducedFeatVec = featVec[:axis] # 取出特征列左侧的数据
reducedFeatVec.extend(featVec[axis+1:]) # 合并特征列右侧的数据
retDataSet.append(reducedFeatVec)
return retDataSet
3-3 选择最好的分割特征来分割数据集:根据最大信息增益
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数量
baseEntropy = calcShannonEnt(dataSet) # 基础熵为样本集本身的香农熵
bestInfoGain = 0.0; bestFeature = -1 # 初始化最大信息增益为0;最佳特征索引为-1
for i in range(numFeatures): # 遍历所有特征
featList = [example[i] for example in dataSet] # 取出第i个特征列数据
uniqueVals = set(featList) # 第i个特征列的取值集合(删除重复元素)
newEntropy = 0.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 # 更新最佳特征索引
return bestFeature
从类别列表中计算出现次数最多的类别(多数表决法)用过很多次了
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)
print(sortedClassCount)
return sortedClassCount[0][0] #返回classList中出现次数最多的元素
3-4 构建决策树
def createTree(dataSet,labels):
# labels为每个特征的名称/说明
classList = [example[-1] for example in dataSet] # 取出类别列
if classList.count(classList[0]) == len(classList): # 只有1个类别时停止分割,返回当前类别
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 = inputTree.keys()[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__=='dict':##判断secondDict[key]是不是字典
classLabel = classify(secondDict[key],featLabels,testVec)
else: classLabel = secondDict[key]
return classLabel
使用算法:决策树的存储
可以把分类器存储在硬盘上不用每次对数据分类都要重新学习一下,这也是决策树的优点,如KNN就无法持久化分类器。
#使用pickle模块存储决策树
def storeTree(inputTree, filename):
import pickle
fw = open(filename,'w')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)