决策树
一、什么是决策树
(1)决策树,就是一个类似于流程图的树形结构,树内部的每一个节点代表的是对一个特征的测试,树的分支代表该特征的每一个测试结果,而树的每一个叶子节点代表一个类别。树的最高层是就是根节点。
使用决策树进行决策的过程就是从根节点开始,测试待分类项中相应的特征属性,并按照其值选择输出分支,直到到达叶子节点,将叶子节点存放的类别作为决策结果。
(2)对比kNN kNN可以完成很多分类任务,但是最大的缺点是无法给出数据的内在含义,而决策树的主要优势就在于数据形式非常容易理解。
(3)下图即为一个决策树的示意描述,内部节点用矩形表示,叶子节点用椭圆表示。
(4)决策树可能会出现过拟合的问题,解决的策略是剪枝。 剪枝是决策树停止分支的方法之一,剪枝有分预先剪枝和后剪枝两种。预先剪枝是在树的生长过程中设定一个指标,当达到该指标时就停止生长,这样做容易产生“视界局限”,就是一旦停止分支,使得节点N成为叶节点,就断绝了其后继节点进行“好”的分支操作的任何可能性。不严格的说这些已停止的分支会误导学习算法,导致产生的树不纯度降差最大的地方过分靠近根节点。后剪枝中树首先要充分生长,直到叶节点都有最小的不纯度值为止,因而可以克服“视界局限”。然后对所有相邻的成对叶节点考虑是否消去它们,如果消去能引起令人满意的不纯度增长,那么执行消去,并令它们的公共父节点成为新的叶节点。这种“合并”叶节点的做法和节点分支的过程恰好相反,经过剪枝后叶节点常常会分布在很宽的层次上,树也变得非平衡。后剪枝技术的优点是克服了“视界局限”效应,而且无需保留部分样本用于交叉验证,所以可以充分利用全部训练集的信息。但后剪枝的计算量代价比预剪枝方法大得多,特别是在大样本集中,不过对于小样本的情况,后剪枝方法还是优于预剪枝方法的。
二、决策树剪枝
1.由于噪声等因素的影响,会使得样本某些特征的取值与样本自身的类别不相匹配的情况,基于这些数据生成的决策树的某些枝叶会产生一些错误;尤其是在决策树靠近枝叶的末端,由于样本变少,这种无关因素的干扰就会突显出来;由此产生的决策树可能存在过拟合的现象。树枝修剪就是通过统计学的方法删除不可靠的分支,使得整个决策树的分类速度和分类精度得到提高。
2.为什么要剪枝:决策树过拟合风险很大,理论上可以完全分得开数据。(想象一下,如果树足够庞大,每个叶子节点不就一个数据了嘛)
- 剪枝策略:预剪枝,后剪枝
(1)预剪枝:边建立决策树边进行剪枝的操作(更实用)
在决策树生成分支的过程,除了要进行基础规则的判断外,还需要利用统计学的方法对即将分支的节点进行判断,比如统计χ2或统计信息增益,如果分支后使得子集的样本统计特性不满足规定的阈值,则停止分支;但是阈值如何选取才合理是比较困难的。
(2)后剪枝:当建立完决策树后来进行剪枝操作
在决策树充分生长后,修剪掉多余的分支。根据每个分支的分类错误率及每个分支的权重,计算该节点不修剪时预期分类错误率;对于每个非叶节点,计算该节点被修剪后的分类错误率,如果修剪后分类错误率变大,即放弃修剪;否则将该节点强制为叶节点,并标记类别。产生一系列修剪过的决策树候选之后,利用测试数据(未参与建模的数据)对各候选决策树的分类准确性进行评价,保留分类错误率最小的决策树。
三、决策树的三种常用算法
三种属性划分方法:信息增益:ID3;信息增益率:C4.5;基尼指数:CART。
划分数据集的最大原则是:将无序的数据变得更加有序。
1. ID3
(1)定义
D3算法的核心是在决策树各个节点上应用信息增益准则来选择特征,递归的构建决策树。 具体方法是:从根节点开始,对节点计算所有可能的特征的信息增益,选择信息增益最大的特征作为节点的特征,由该特征的不同取值建立子节点:再对子节点递归的调用以上方法,构建决策树:直到所有的特征信息增益均很小或没有特征可以选择为止。
缺点
从上图中,我们可以看出,选择ID作为特征,信息增益最大,可是从业务的角度的看,这个特征意义不大,每个ID必然只对应一个类别。故信息增益的问题的就从这里引发出来,它的缺点就是偏向选择取值较多的属性,这种算法本身有一种倾向性,所以改进之后的算法在后面会讲到是C4.5。
(2)知识点
信息熵
熵定义为信息的期望值,集合信息的度量方式。首先我们先要明确信息的定义。如果待分类的事务可能划分在多个分类之中,则符号
x
i
x_{i}
xi的信息定义为
l
(
x
i
)
=
−
l
o
g
2
p
(
x
i
)
l(x_{i}) = -log_{2}p(x_{i})
l(xi)=−log2p(xi),
其中
p
(
x
i
)
p(x_{i})
p(xi)是选择该分类的概率。
“信息嫡”是度量样本集合纯度最常用的一种指标,为了计算熵,我们需要计算所有类别所有可能值包含的信息期望值,假定当前样本集合D中第k类样本所占的比例为p:(K=1,2,…yl),则D的信息嫡定义为
E
n
t
(
D
)
=
−
∑
k
=
1
∣
y
∣
p
k
l
o
g
2
p
k
Ent(D) = -\sum_{k=1}^{\left | y \right |}p_{k}log_{2}p_{k}
Ent(D)=−∑k=1∣y∣pklog2pk
Ent(D)的值越小,则D的纯度越高,计算信息嫡时约定:若p=0,则
p
l
o
g
2
p
plog_{2}p
plog2p = 0。当p=1或p=0时,Ent(D)取到最小值为0,完全没有不确定性;当p=0.5时,取到最大值为
l
o
g
2
∣
y
∣
log_{2}{\left | y \right |}
log2∣y∣,此时最为混乱,事件最不确定。
信息增益
在划分数据集之前之后信息发生的变化称为信息增益。获得信息增益最高的特征就是最好的选择。
离散属性a有V个可能的取值{a’ , a2,…, a,用a来进行划分,则会产生V个分支结点,其中第v个分支结点包含了D中所有在属性a上取值为a的样本,记为Dv。则可计算出用属性a对样本集D进行划分所获得的“信息增益”:
G
a
i
n
(
D
,
a
)
=
E
n
t
(
D
)
−
∑
v
=
1
V
∣
D
v
∣
∣
D
∣
E
n
t
(
D
v
)
Gain(D,a) = Ent(D)-\sum_{v=1}^{V}\frac{\left | D^{v} \right |}{\left | D \right |}Ent(D^{v})
Gain(D,a)=Ent(D)−∑v=1V∣D∣∣Dv∣Ent(Dv)
为分支结点权重,样本数越多的分支结点的影响越大
一般而言,信息增益越大,则意味着使用属性a来进行划分所获得的“纯度提升”越大。
(3)步骤
决策树的构造
- 计算给定数据集的香农熵
from math import log
#计算给定数据集的香农熵
def calcShannonEnt(dataSet):
#计算数据集中实例的总数
numEntries = len(dataSet)
#建立一个空字典,准备装原始数据的每种分类的数目,"yes"=是,"no"=否
labelCounts = {}
for featVec in dataSet:
#取每组数据的最后一个值,作为标签
currentLabel = featVec[-1]
#如果标签不在创建的字典中,则添加进去
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
#print(labelCounts)
for key in labelCounts:
#计算标签占总数据的比例
prob = float(labelCounts[key])/numEntries
#求熵公式
shannonEnt -= prob * log(prob,2)
return shannonEnt
创建一个数据集,该数据集有2个特征,分别代表是否可以浮出水面、是否有脚蹼。用1代表是,0代表否。最后一列数据表示当前样本是否为鱼类。
数据要求: 一是数据必须是一种由列表元素组成的列表,而且所有的列表元素都要具有相同的数据长度;二是数据的最后一列或者每个实例的最后一个元素是当前实例的类别标签。
#创建数据
def createDataSet():
dataSet = [[1, 1, 'yes'],#例如这个样本点代表不能浮出水面、有脚蹼,是鱼类
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']#label记录样本的特征名称
return dataSet, labels
测试计算出该数据集的熵
mydata,labels=createDataSet()
print(calcShannonEnt(mydata))
输出结果
当改变数据集第一个列表中的分类yes变为maybe
#创建数据
def createDataSet():
dataSet = [[1, 1, 'maybe'], #例如这个样本点代表不能浮出水面、有脚蹼,是鱼类
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']#label记录样本的特征名称
return dataSet, labels
熵越高,说明数据越混乱,即分类更多了。
- 按照给定特征划分数据集
实现的逻辑是给定特征,并选取该特征下对应的某个值,依据该值将数据划分为子数据集,并将该特征维度给去除掉,用于进一步划分。
#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):#给定数据集、划分数据集的特征 、特征所对应的值
retDataSet = [] #创建一个备份数据集,避免原始数据被修改
for featVec in dataSet: #遍历数据集
if featVec[axis] == value: #该特征维度下和value值相等的样本划分到一起,并将该特征去除掉维度去掉
#将axis维度两边的数据进行拼接,就将该特征维度给去除掉
reducedFeatVec = featVec[:axis] #取得[0,axis)的一个列表
reducedFeatVec.extend(featVec[axis+1:]) #取得[axis+1,结束]的一个列表
retDataSet.append(reducedFeatVec)
return retDataSet
print(splitDataSet(mydata,0,1))
- 选择最好的数据集划分方式
然后遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #numfeature为特征的维度,因为最后一列为标签,所以需要减去1
baseEntropy = calcShannonEnt(dataSet) #用来记录最小信息熵,初始值为原始数据集对应的信息熵
bestInfoGain = 0.0; bestFeature = -1 #信息增益初始化为0,最优的划分特征初始化为-1
for i in range(numFeatures): #遍历所有的特征
featList = [example[i] for example in dataSet] #创建list用来存每个样本在第i维度的特征值
uniqueVals = set(featList) #获取该特征下的所有不同的值,即根据该特征可以划分为几类
newEntropy = 0.0 #初始化熵为0
for value in uniqueVals: #遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) #依据这个值,将样本划分为几个子集,有几个value,就有几个子集
prob = len(subDataSet)/float(len(dataSet)) #计算p值
newEntropy += prob * calcShannonEnt(subDataSet) #计算每个子集对应的信息熵,并全部相加,得到划分后数据的信息熵
infoGain = baseEntropy - newEntropy #将原数据的信息熵-划分后数据的信息熵,得到信息增益
if (infoGain > bestInfoGain): #如果这个信息增益比当前记录的最佳信息增益还大,就将该增益和划分依据的特征记录下来
bestInfoGain = infoGain
bestFeature = i
return bestFeature #returns an integer
运行结果为0,即第0个特征是最好的用于划分数据集的特征。
- 递归构建决策树
这里我们只是对于原始数据集选择出了一个特征进行划分,实际上如果划分后的样本还有多个类别,那么还必须进一步划分,因此需要递归创建出决策树。在给出最终创建决策树的代码前,首先解决一个问题,假设特征都用完标签还不唯一,这时又无法继续选择某特征进行划分,那么怎么办?解决的方法就是多数表决,在这个数据集中,哪个的标签最多,就设这个数据集就就代表着这一类别,这有些类似KNN的思想。
#处理完所有属性,类标签依然不唯一,多数表决
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1 #计算每个标签对应的数目
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)#从大到小进行排列
return sortedClassCount[0][0]#选出标签数量最多的返回
- 创建树
#创建树
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet] #存储所有样本的标签
if classList.count(classList[0]) == len(classList):
return classList[0] #如果所有的标签都是一样,就直接返回该子集的标签,这里用的方法是计算其中某个类别的标签数量,如果该数量就等于标签的总数,那容易知道,该数据集的类别标签是一样的
if len(dataSet[0]) == 1: #如果样本的特征值就剩一个,即样本长度为1,就停止,返回该数据集标签数目最多,作为该数据集的标签
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #选取出该数据集最佳的划分特征
bestFeatLabel = labels[bestFeat] #得出该特征所对应的标签名称
myTree = {bestFeatLabel:{}} #创建mytree字典,这个字典将会一层套一层,这个看后面的结果就明白
del(labels[bestFeat]) #将这个最佳的划分特征从标签名称列表中删除,这是为了下次递归进来不会发生错误的引用,因为下面每次递归的数据集都会删除划分特征所对应的那一列
featValues = [example[bestFeat] for example in dataSet] #获取到该划分特征下对应的所有特征值
uniqueVals = set(featValues) #经过set去重,值代表着该特征能将当前的数据集划分成多少个不同的子集
for value in uniqueVals: #现在对划分的子集进一步进行划分,也就是递归的开始
subLabels = labels[:] #将样本标签复制给sublabels,这样就不会在每次的递归中改变原始labels
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat,value),subLabels)#将样本划分的子集再进行迭代
return myTree
测试结果
print(createTree(mydata,labels))
运行结果
使用决策树执行分类
递归调用
# 使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
firstStr = list(inputTree.keys())[0]
# print(firstStr)
secondDict = inputTree[firstStr]
# print(secondDict)
featIndex = featLabels.index(firstStr)
# print(featIndex)
try: #返回none时跳过
for key in list(secondDict.keys()):
if testVec[featIndex] == key:
if type(secondDict[key]).__name__=='dict':
classLabel = classify(secondDict[key],featLabels,testVec)
else:
classLabel = secondDict[key]
return classLabel
except:
pass
测试课本数据
train = [['1', '1', 'yes'], #例如这个样本点代表不能浮出水面、有脚蹼,是鱼类
['1', '1', 'yes'],
['1', '0', 'no'],
['0', '1', 'no'],
['0', '1', 'no']]
labels = ['no surfacing','flippers']
labels1 = labels[:]
myTree = createTree(train,labels)
print(classify(myTree,labels1,['1','1']))
运行结果:
使用Matplotlib注解绘制树形图
显然字典的表示形式非常不易理解,不够直观。
import matplotlib.pyplot as plt
decisionNode = dict(boxstyle=“sawtooth”, fc=“0.8”)
leafNode = dict(boxstyle=“round4”, fc=“0.8”)
arrow_args = dict(arrowstyle=“<-“)
def getNumLeafs(myTree):
numLeafs = 0
#firstStr = myTree.keys()[0]
firstSides = list(myTree.keys())
firstStr = firstSides[0]#找到输入的第一个元素
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__==’dict’:#test to see if the nodes are dictonaires, if not they are leaf nodes
numLeafs += getNumLeafs(secondDict[key])
else: numLeafs +=1
return numLeafs
def getTreeDepth(myTree):
maxDepth = 1
firstSides = list(myTree.keys())
firstStr = firstSides[0]#找到输入的第一个元素
#firstStr = myTree.keys()[0] #注意这里和机器学习实战中代码不同,这里使用的是Python3,而在Python2中可以写成这种形式
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]) == dict:
thisDepth = 1 + getTreeDepth(secondDict[key])
else: thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords=‘axes fraction’,
xytext=centerPt, textcoords=‘axes fraction’,
va=“center”, ha=“center”, bbox=nodeType, arrowprops=arrow_args )
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)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstSides = list(myTree.keys())
firstStr = firstSides[0]#找到输入的第一个元素
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
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
def createPlot(inTree):
fig = plt.figure(1, facecolor=‘white’)
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #no ticks
#createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5/plotTree.totalW; plotTree.yOff = 1.0;
plotTree(inTree, (0.5,1.0), ”)
plt.show()
测试
createPlot({'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}})
运行结果
(4)使用自己的数据集
训练集和测试集的创建
#创建训练集
def createTrainSet():
f = open('train.txt','r')
trainSet = []
for i in f.readlines():
lst = i.strip('\n').split(',')
trainSet.append(lst)
#可写成dataSet = [i.strip('\n').split(',') for i in f.readlines()]
labels = ['buying','maint','doors','persons','lug_boot','safety']#label记录样本的特征名称
return trainSet, labels
#创建测试集
def createTestSet():
f = open('test.txt','r')
testSet = []
for i in f.readlines():
lst = i.strip('\n').split(',')
l = lst[:6]
testSet.append(l)
#可写成dataSet = [i.strip('\n').split(',') for i in f.readlines()]
labels = ['buying','maint','doors','persons','lug_boot','safety']#label记录样本的特征名称
return testSet, labels
数据集是通过UCI数据集下载的(http://archive.ics.uci.edu/ml/index.php),Car Evaluation数据集,其中信息为:
Attribute Information:
Class Values:
unacc, acc, good, vgood
Attributes:
buying: vhigh, high, med, low.
maint: vhigh, high, med, low.
doors: 2, 3, 4, 5more.
persons: 2, 4, more.
lug_boot: small, med, big.
safety: low, med, high.
分类测试
train,labels = createTrainSet()
labels = ['buying','maint','doors','persons','lug_boot','safety']
labels1 = labels[:]
myTree = createTree(train,labels)
test,labels = createTestSet()
# print(classify(myTree,labels1,['med', 'low', '2','4','small','high']))
for i in test:
print(classify(myTree,labels1,i))
运行结果
这是所生成的决策树,通过字典表达,训练集有1208条,所以字典延伸到很后面。
决策树可视化,数据较多,所以树重叠在一起了。
测试集通过训练集得到的决策树模型获得分类结果。
训练集和测试集创建
#创建训练集
def createTrainSet():
f = open('dataSet.txt','r')
trainSet = []
j = 0
for i in f.readlines():
lst = i.strip('\n').split(',')
if j % 2 == 0:
trainSet.append(lst)
j+=1
# print(trainSet)
#可写成dataSet = [i.strip('\n').split(',') for i in f.readlines()]
labels = ['buying','maint','doors','persons','lug_boot','safety']#label记录样本的特征名称
return trainSet, labels
#创建测试集
def createTestSet():
f = open('dataSet.txt','r')
testSet = []
testend = []
k = 0
for i in f.readlines():
lst = i.strip('\n').split(',')
l = lst[:6]
if k % 2 == 1:
testSet.append(l)
testend.append(lst)
k+=1
#可写成dataSet = [i.strip('\n').split(',') for i in f.readlines()]
labels = ['buying','maint','doors','persons','lug_boot','safety']#label记录样本的特征名称
return testSet, labels,testend
主函数
if __name__ == '__main__':
train,labels = createTrainSet()
labels = ['buying','maint','doors','persons','lug_boot','safety']
labels1 = labels[:]
myTree = createTree(train,labels)
# print(myTree)
# treePlotter.createPlot(myTree)
test,labels,testend = createTestSet()
# print(classify(myTree,labels1,['med', 'low', '2','4','small','high']))
print(testend)
print(test)
allnum = len(testend)
right = 0
count = 0
for i in test:
# print(i)
# print(testend[count])
result = classify(myTree,labels,i)
print(result)
if result == testend[count][6]:
right+=1
count+=1
print("正确率为",right/allnum*100,'%')
运行结果
模型对于该测试集正确率为90%左右。
2. C4.5
(1)定义
鉴于ID3算法的不足即偏向选择取值较多的属性,所以C4.5则是改用信息增益率对属性进行划分。信息增益率对可取值数目较少的属性有所偏好。
(2)公式
定义信息增益率为:
G
a
i
n
r
a
t
i
o
(
D
,
a
)
=
G
a
i
n
(
D
,
a
)
I
V
(
a
)
Gainratio(D,a)=\frac{Gain(D,a)}{IV(a)}
Gainratio(D,a)=IV(a)Gain(D,a),
其中
I
V
(
a
)
=
−
∑
v
=
1
V
∣
D
v
∣
∣
D
∣
l
o
g
2
∣
D
v
∣
∣
D
∣
IV(a) = -\sum_{v=1}^{V}\frac{\left | D^{v} \right |}{\left | D \right |}log_{2}\frac{\left | D^{v} \right |}{\left | D \right |}
IV(a)=−∑v=1V∣D∣∣Dv∣log2∣D∣∣Dv∣ ,称为属性a的“固有值” ,属性a的可能取值数目越多(即V越大),则IV(a)的值通常就越大。
采用了一个启发式方法:先从候选划分属性中找出信 息增益高于平均水平的属性,再从中选取增益率最高的。
(3)代码
相比于ID3变动不大,就在 chooseBestFeatureToSplit() 函数处改动即可,根据公式做出相应的变化,,代码如下:
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量,numfeature为特征的维度,因为最后一列为标签,所以需要减去1
baseEntropy = calcShannonEnt(dataSet) #用来记录最小信息熵,初始值为原始数据集对应的信息熵
bestinfoGainratio = 0.0 #信息增益率初始化为0,
bestFeature = -1 #最优的划分特征初始化为-1
for i in range(numFeatures): #遍历所有的特征
featList = [example[i] for example in dataSet] #创建list用来存每个样本在第i维度的特征值
uniqueVals = set(featList) #获取该特征下的所有不同的值,即根据该特征可以划分为几类,可以去除重复值
newEntropy = 0.0 #初始化熵为0
IV = 0.0
for value in uniqueVals: #遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) #依据这个值,将样本划分为几个子集,有几个value,就有几个子集
prob = len(subDataSet)/float(len(dataSet)) #计算p值
newEntropy += prob * calcShannonEnt(subDataSet) #计算每个子集对应的信息熵,并全部相加,得到划分后数据的信息熵
IV -= prob * log(prob,2)
infoGain = baseEntropy - newEntropy #将原数据的信息熵-划分后数据的信息熵,得到信息增益
if IV == 0.0: #除以0处理
infoGainratio = 0.0
else:
infoGainratio = float(infoGain) / float(IV)
if (infoGainratio > bestinfoGainratio): #如果这个信息增益率比当前记录的最佳信息增益率还大,就将该增益和划分依据的特征记录下来
bestinfoGainratio = infoGainratio #更新信息增益率,找到最大的信息增益率
bestFeature = i #记录信息增益率最大的索引值
return bestFeature #returns an integer
数据集测试结果:正确率在89%左右
3. CART
(1)概念
分类问题中,假设D有K个类,样本点属于第k类的概率为
p
k
p_k
pk , 则概率分布的基尼值定义为:
G
i
n
i
(
D
)
=
∑
k
=
1
K
p
k
(
1
−
p
k
)
=
1
−
∑
k
=
1
K
p
k
2
Gini(D)=\sum_{k=1}^{K}p_k(1-p_k)=1-\sum_{k=1}^{K}p_{k}^{2}
Gini(D)=∑k=1Kpk(1−pk)=1−∑k=1Kpk2,
反映了随机抽取两个样本,其类别标记不一致的概率。Gini(D)值越小,数据集D的纯度越高。
- 基尼值越小,纯度越高,集合的有序程度越高,分类的效果越好;
- 基尼指数为 0 时,表示集合类别一致;
给定数据集D,属性a的基尼指数定义为:
G i n i i n d e x ( D , a ) = ∑ v = 1 V ∣ D v ∣ ∣ D ∣ G i n i ( D v ) Gini_{index}(D,a)=\sum_{v=1}^{V}\frac{\left | D^{v} \right |}{\left | D \right |}Gini(D^v) Giniindex(D,a)=∑v=1V∣D∣∣Dv∣Gini(Dv)
在候选属性集合A中,选择那个使得划分后基尼指数最小的属性作为最有划分属性。
(2)代码实现
不再需要香农熵,改用基尼指数来划分属性
def calcProbabilityEnt(dataSet):
numEntries = len(dataSet) #数据条数
feaCounts = 0
fea1 = dataSet[0][len(dataSet[0])-1]
for feaVec in dataSet:
if feaVec[-1] == fea1:
feaCounts += 1
probabilityEnt = float(feaCounts) / numEntries
return probabilityEnt
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量,numfeature为特征的维度,因为最后一列为标签,所以需要减去1
if numFeatures == 1: #当只有一个特征时
return 0
bestGini = 1 #最佳基尼指数
bestFeature = -1 #最优的划分特征初始化为-1
for i in range(numFeatures): #遍历所有的特征
featList = [example[i] for example in dataSet] #创建list用来存每个样本在第i维度的特征值
feaGini = 0 #定义特征的值的基尼系数
uniqueVals = set(featList) #获取该特征下的所有不同的值,即根据该特征可以划分为几类,可以去除重复值
for value in uniqueVals: #遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) #依据这个值,将样本划分为几个子集,有几个value,就有几个子集
prob = len(subDataSet)/float(len(dataSet)) #计算p值
probabilityEnt = calcProbabilityEnt(subDataSet)
feaGini += prob * (2 * probabilityEnt * (1 - probabilityEnt))
if (feaGini < bestGini):
bestGini = feaGini
bestFeature = i #记录基尼指数最小的索引值
return bestFeature #returns an integer
(3)运行结果
数据集测试结果 :正确率在70%左右。