XGBoost和LightGBM已经成为Kaggle,天池等比赛和数据研究,必用的算法。这篇文章,从决策树出发,一步步引出GBDT、XGBoost和LightGBM,讲解原理,比较不同,介绍应用。
决策树就像是我们初中学过的流程图,一步步去判断条件,是或否依次进行不同的选择,只不过决策树不只是“是”和“否”这两个判断条件。
决策树不只可以用与分类问题,回归问题同样适用。
if:
是这个域名发送的邮件:无聊时阅读
else:
转下一步
if:
包含曲棍球:需要及时处理
else:
垃圾邮件
决策树的构建是我们的核心内容。决策树构建一般分为为3个步骤:特征选择、决策树的生成和决策树的修剪。
数据收集是否充足,特征选择是否恰当,决策树的修剪,都将影响到我们的准确率和训练速度。接下来主要便是介绍特征选择。
通过所给的训练数据学习一个贷款申请的决策树,用于对未来的贷款申请进行分类,即当新的客户提出贷款申请时,根据申请人的特征利用决策树决定是否批准贷款申请。
回想流程图,一组数据含有多个特征,而我们到底选择哪个特征,首先选择哪个特征,然后选择哪个,会影响到我们训练的速度和准确率。也就是如何选择最优划分的属性的问题。
主要有三个判断指标:信息增益(ID3算法)、增益率(C4.5算法)、基尼指数(CART算法)。括号中分别是不同的三种决策树的构建方法,与这三个指标一一对应,。本文主要介绍信息增益这个指标。
在划分数据集前后,信息发生的变化称为信息增益,获得信息增益最高的特征是最好的选择。
集合信息的度量方式成为香农熵或者简称为熵(entropy)。熵是表示随机变量不确定性的度量。是度量样本信息和纯度最常用的一种指标。
多个分类中,某一分类样本信息:
l ( x i ) = log 2 p ( x i ) l\left(x_{i}\right)=\log _{2} p\left(x_{i}\right) l(xi)=log2p(xi)
p(xi):是该分类的概率。
香农熵:算所有类别所有可能值包含的信息期望值(数学期望)
H = − ∑ i = 1 n p ( x i ) log 2 p ( x i ) H=-\sum_{i=1}^{n} p\left(x_{i}\right) \log _{2} p\left(x_{i}\right) H=−i=1∑np(xi)log2p(xi)
n是分类的数目
from math import log
"""
函数说明:创建测试数据集
Parameters:
无
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 - 数据集
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 #经验熵(香农熵)
for key in labelCounts: #计算香农熵
prob = float(labelCounts[key]) / numEntires #选择该标签(Label)的概率
shannonEnt -= prob * log(prob, 2) #利用公式计算
return shannonEnt #返回经验熵(香农熵)
if __name__ == '__main__':
dataSet, features = createDataSet()
print(dataSet)
print(calcShannonEnt(dataSet))
信息增益无非就是,我们对原始数据根据不同的特征划分后,出现的新数据的香农熵,与原数据的香农熵的差。不要考虑什么条件概率,条件熵,只要抓住这一点,前后数据熵的变化即可,由于被划分后,数据的个数不同,所以不同的部分占有不同的权重,所以考虑进去权重即可,即使高中概率知识也可理解。
def splitDataSet(dataSet,axis, value):
"""
函数说明:
对数据集根据某个特征进行划分
:param dataSet: 被划分的数据集
:param axis: 划分根据的特征
:param value: 需要返回的特征的值
:return: 无
"""
retDataSet = [] #建立空列表,存储返回的数据集
for featVec in dataSet: #遍历数据,一个对象一个对象的进行操作
if featVec[axis] == value: #找到axis特征,等于value值得数据
reducedFeatVec = featVec[:axis] #选出去除axis特征,只保留其他特征的数据
reducedFeatVec.extend(featVec[axis+1:]) #保留后面的
retDataSet.append(reducedFeatVec) #对数据进行连接,按照列表形式
return retDataSet #返回划分后被取出的数据集,即满足条件的数据
#---------------------------------------------------------------------------
def chooseBestFeaturesToSplit(dataSet):
"""
函数说明:
选择最佳的分类属性
:param :dataSet-数据集
:return: bestFeature-最佳划分属性
"""
numFeatures = len(dataSet[0]) - 1 #特征数量,去除最后一个标签值
baseEntropy = calShannoEnt(dataSet) #计算原始数据的香农熵
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: #计算信息增益
#根据第i特征,value值对数据进行划分
subDataSet = splitDataSet(dataSet, i, value)
#求划分后数据的经验条件熵
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calShannoEnt(subDataSet)
infoGain = baseEntropy - newEntropy #求划分前后的信息增益
print("第%d个特征的增益是%.3f" %(i,infoGain))
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
上篇博客已经讲到,构建决策树,我们有三种算法:ID3,C4.5和CART。这一篇,我们只介绍,ID3算法,其他两种后面介绍。
也就是使用使用信息增益作为判断指标,用于选择最优的划分特征。递归调用上述算法,不断产生分支,直到所有特征的信息增益均很小或没有特征可以选择为止。
首先看我们要分析的数据:
通过上篇写到的信息增益的计算方法,计算各个特征的信息增益,我们发现是否有自己的房子,有最大的信息增益,所以我们把有自己的房子,作为根节点的划分标准。
将训练集D划分为两个子集D1(有自己的房子为"是")和D2(有自己的房子为"否")。由于D1只有同一类的样本点,所以它成为一个叶结点,结点的类标记为“是”。
然后,我们对D2的A1(年龄),A2(有工作)和A4(信贷情况)特征中选择最优划分特征,计算各个特征的信息增益:
可以看出A2信息增益最大,所以选择A2(有工作)作为划分特征。然后根据是否有工作,再次进行划分。一个对应"是"(有工作)的子结点,包含3个样本,它们属于同一类,所以这是一个叶结点,类标记为"是";另一个是对应"否"(无工作)的子结点,包含6个样本,它们也属于同一类,所以这也是一个叶结点,类标记为"否"。
生成决策树:
使用字典存储决策树的结构,上小节的决策树,用字典可以表示为:
{'有自己的房子': {0: {'有工作': {0: 'no', 1: 'yes'}}, 1: 'yes'}}
创建函数majorityCnt统计classList中出现此处最多的元素(类标签),创建函数createTree用来递归构建决策树。
递归创建决策树时,递归有两个终止条件:
一:所有的类标签完全相同,则直接返回该类标签;
二:使用完了所有特征,仍然不能将数据划分仅包含唯一类别的分组,即决策树构建失败,特征不够用。此时说明数据纬度不够,由于第二个停止条件无法简单地返回唯一的类标签,这里挑选出现数量最多的类别作为返回值。
from math import log
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
import operator
#--------------------------------------------------------------
def createDataSet():
"""
函数说明:创建测试数据集
参数:无
return: dataSet-数据集
labels-分类属性
"""
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,1,'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
def calShannoEnt(dataSet):
"""
函数说明:
计算数据集的香农熵
param :dataSet-数据集
return: shannonEnt-香农熵
"""
numEntires = len(dataSet) #数据集行数
labelCounts = {} #该字典用于保存每个标签出现的次数
for featVec in dataSet:
currentLabel = featVec[-1] #for循环,来处理每一个数据的标签
if currentLabel not in labelCounts.keys(): #对字典进行初始化
labelCounts[currentLabel] = 0 #令两类初始值分别为0
labelCounts[currentLabel] += 1 #统计数据
shannonEnt = 0.0
for key in labelCounts: #根据公式,利用循环求香农熵
prob = float(labelCounts[key])/numEntires
shannonEnt -= prob*log(prob,2)
return shannonEnt
#-------------------------------------------------------------
def splitDataSet(dataSet,axis, value):
"""
函数说明:
对数据集根据某个特征进行划分
:param dataSet: 被划分的数据集
:param axis: 划分根据的特征
:param value: 需要返回的特征的值
:return: 无
"""
retDataSet = [] #建立空列表,存储返回的数据集
for featVec in dataSet: #遍历数据,一个对象一个对象的进行操作
if featVec[axis] == value: #找到axis特征,等于value值得数据
reducedFeatVec = featVec[:axis] #选出去除axis特征,只保留其他特征的数据
reducedFeatVec.extend(featVec[axis+1:]) #保留后面的
retDataSet.append(reducedFeatVec) #对数据进行连接,按照列表形式
return retDataSet #返回划分后被取出的数据集,即满足条件的数据
#---------------------------------------------------------------------------
def chooseBestFeaturesToSplit(dataSet):
"""
函数说明:
选择最佳的分类属性
:param :dataSet-数据集
:return: bestFeature-最佳划分属性
"""
numFeatures = len(dataSet[0]) - 1 #特征数量,去除最后一个标签值
baseEntropy = calShannoEnt(dataSet) #计算原始数据的香农熵
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: #计算信息增益
#根据第i特征,value值对数据进行划分
subDataSet = splitDataSet(dataSet, i, value)
#求划分后数据的经验条件熵
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calShannoEnt(subDataSet)
infoGain = baseEntropy - newEntropy #求划分前后的信息增益
print("第%d个特征的增益是%.3f" %(i,infoGain))
if(infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
#--------------------------------------------------------------
在决策树构建过程中,我们通过不断地划分,可能一直到遍历所有特征,依然不能彻底划分数据,也就是说,划分到最后,包含的数据类别存在不一样。此时我们选择,这多个类别中,出现次数最多的作为该部分数据的类别。
def majorityCnt(classList):
"""
函数说明:
当划分结束,遍历所有特征,但是依然不能彻底划分数据时
将这多个类别中,出现次数最多的作为该类别
:param classList-类别列表
:return: sortedClassCount[0][0]-类别
"""
classCount = {}
for vote in 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]
#--------------------------------------------------------------
def createTree(dataSet, labels, featLabels):
"""
函数说明:
创建决策树
:param dataSet-训练集
:param labels-训练集数据标签
:param featLabels:
:return:
"""
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: #遍历完所有特征时返回出现次数最多的类标签
return majorityCnt(classList)
bestFeat = chooseBestFeaturesToSplit(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] = createTree(splitDataSet(dataSet, bestFeat, value),labels,featLabels)
return myTree
#-------------------------------------------------------------
if __name__ == '__main__':
dataSet, labels = createDataSet()
featLabels = []
myTree = createTree(dataSet, labels, featLabels)
print(myTree)
运行后我们可以得到,刚开始,字典类型的决策树。
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)
testVec = [0,1] #测试数据
result = classify(myTree, featLabels, testVec)
if result == 'yes':
print('放贷')
if result == 'no':
print('不放贷')
以上简短的介绍了,决策树的生成和实现。有句俗话,三个臭皮匠顶个诸葛亮,机器学习上也存在同样类似的理论。我们对决策树,进行集成,得到多个基学习器的组合,效果会比使用单个学习器要好。
下一篇我们介绍集成学习。
ID3 | C4.5 | CART | |
---|---|---|---|
缺失值 | 对缺失值敏感 | 一定处理 | 一定的处理 |
分之数 | 多分支 | 多分支 | 二叉树 |
特征复用 | 不会复用 | 不会复用 | 可以被重复利用 |