决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。在机器学习中,决策树是一个预测模型,他代表的是对象属性与对象值之间的一种映射关系。Entropy = 信息熵(系统的凌乱程度),使用算法ID3, C4.5和C5.0生成树算法使用熵。
决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。
分类树(决策树)是一种十分常用的分类方法。他是一种监管学习,所谓监管学习就是给定一堆样本,每个样本都有一组属性和一个类别,这些类别是事先确定的,那么通过学习得到一个分类器,这个分类器能够对新出现的对象给出正确的分类。这样的机器学习就被称之为监督学习。
给出如下的一组数据,一共有十个样本(学生数量),每个样本有分数,出勤率,回答问题次数,作业提交率四个属性,最后判断这些学生是否是好学生。最后一列给出了人工分类结果。
我们假设决策树为二叉树,且类似于下图:
上面图来源
因此机器学习决策树就是类似于这样的二叉树结构,来分类数据。
信息量:信息多少的量度,可以说是事件发生的不确定度。根据信息的出现概率来计算信息量。计算公式为 I = − l o g 2 P ( i ) I = -log_2 P(i) I=−log2P(i)
举个简单的例子:①太阳从东边升起西边落下②抽奖中了1E
对应①来说,出现该事件的概率几乎是100%; 对应②来说,出现该事件的概率是十分小的,设为 1 256 \frac{1}{256} 2561
如果有一天,别人告诉你太阳今天从东边升起西边落下,你会觉得他莫名其妙,因为该事件的发生概率太大,所以对于你来说信息量不大。但是如果有人对你说,你抽奖中了1E,对于你来说信息量就巨大了。
所以 I ① = − l o g 2 1 = 0 ( b i t ) , I ② = − l o g 2 1 256 = 8 ( b i t ) I_①=-log_21 = 0(bit), I_②=-log_2\frac{1}{256} = 8(bit) I①=−log21=0(bit),I②=−log22561=8(bit)
因此事件出现的概率越低,信息量越大,不确定度越大。
信息熵:直到1948年,香农提出了“信息熵”的概念,信息熵这个词是从热力学中借用过来的。热力学中的热熵是表示分子状态混乱程度的物理量。香农用信息熵的概念来描述信源的不确定度。
通常,一个信源发送出什么符号是不确定的,衡量它可以根据其出现的概率来度量。概率大,出现机会多,不确定性小;反之不确定性就大。由于信息量乘上相应的概率,就是整个信源的信息熵,所以信息熵也可以理解为事件不确定度的期望。
计算公式为 H = ∑ M > i > N − P ( i ) l o g 2 [ P ( i ) ] = ∑ M > i > N − P ( i ) I i H=\sum_{M>i>N}-P(i)log_2[P(i)]=\sum_{M>i>N}-P(i)I_i H=∑M>i>N−P(i)log2[P(i)]=∑M>i>N−P(i)Ii
例如一个信源的发送概率如下 [ P ( ′ 发 送 A 符 号 ′ ) = 0.4 , P ( ′ 发 送 B 符 号 ′ ) = 0.6 ] [P('发送A符号')=0.4, P('发送B符号')=0.6] [P(′发送A符号′)=0.4,P(′发送B符号′)=0.6]。
I A = − l o g 2 0.4 = 1.32 b i t I_A = -log_20.4=1.32bit IA=−log20.4=1.32bit, I B = − l o g 2 0.6 = 0.73 b i t I_B = -log_20.6=0.73bit IB=−log20.6=0.73bit,所以信息熵为 H = 0.4 I A + 0.6 I B = 0.966 b i t H=0.4I_A+0.6I_B=0.966bit H=0.4IA+0.6IB=0.966bit, 所以信源的不确定度的期望为0.966bit。即如果信源发送无穷个符号的情况下,发送A的概率会无限接近0.4,发送B的概率会无限接近0.6,所以总的平均不确定度就是该信源的信息熵。
这里还是借助《机器学习实战》里的代码。
计算信息熵
def calcShannonEnt(dataSet):
# 计算dataSet最后一列 即标签的信息熵
numEntries = len(dataSet)
labelCounts = {}
for feaVec in dataSet:
# 取出数据集中每一行的最后一个元素 因为通常那个位置是用于摆放标签
currentLabel = feaVec[-1]
# 统计标签出现次数
labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1
shannonEnt = 0.0
# 计算全部标签情况下的信息熵
for key in labelCounts:
# 计算每个标签的出现概率
prob = float(labelCounts[key]) / numEntries
# 计算信息熵
shannonEnt -= prob * log(prob, 2)
return shannonEnt
[解析]: 在此处的dataSet数据集每行的最后一列数据存放的是数据的标签。通过取出所有的不同标签,计算标签在数据集的出现次数,计算出现概率即标签出现的行数÷总行数=出现概率。再通过出现概率算出信息熵,也即是不确定度期望。
所以该算法是用于计算数据集中的标签的信息熵, 即不确定度期望
分割数据集
def splitDataSet(dataSet, axis, value):
retDataSet = []
# 遍历数据集每一行
for featVec in dataSet:
# 如果axis列中 含有value特征的 则返回去掉该特征后的向量; 若没有该特征 则不理会
# 例如 数据集a = [[1, 2, 3], [1, 5, 7],]
# 如果数据集中的第0列数据是1, 则去掉1后输出
# splitDataSet(a, 0, 1)
# >> [[2, 3], [5, 7]]
# 如果数据集中的第1列数据是2, 则去掉2后输出
# splitDataSet(a, 1, 2)
# >> [[1, 3]]
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
[解析]: 算法的功能是将每行的第axis列中(每列的元素可看作是一个特征),若该值是value,则抽取出这行,并将这行中第axis列的value值去掉,然后append到要返回的数据集中。即找出符合条件的特征行并返回其他维度的特征,用于分割数据集。
计算最佳分割维度
def chooseBestFeatureToSplit(dataSet):
# 获取数据集中的数据维度, 默认最后一列是标签
numFeature = len(dataSet[0]) - 1
# 初始数据集信息熵
baseEntropy = calcShannonEnt(dataSet)
# 初始最好数据熵增益
bestInfoGain = 0.0
# 初始最好特征索引
bestFeature = -1
# 遍历所有维度的特征
for i in range(numFeature):
# 取出所有数据集行中的第i维特征
featList = [example[i] for example in dataSet]
# 特征去重
uniqueVals = set(featList)
# 新的数据熵, 用于保存划分完数据集以后的信息熵
newEntropy = 0.0
# 遍历所有的特征, 求和算出在被特征分割后的数据集的信息熵
for value in uniqueVals:
# 获得分割后的数据集
subDataSet = splitDataSet(dataSet, i, value)
# 算出分割后的数据集在总数据集的出现概率
probability = len(subDataSet) / float(len(dataSet))
# 计算分割后数据集的信息熵
newEntropy += probability * calcShannonEnt(subDataSet)
# 信息增益 = 初始信息熵 - 特征分割后的总信息熵
infoGain = baseEntropy - newEntropy
# 找出最小的newEntropy 使得infoGain = baseEntropy - newEntropy最大 以证明该维度的分割方案是最佳的
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature
[解析]: 该算法是计算出哪一维度的特征分割后的数据集分类效果最佳。
首先初始化特征的总维度、数据集的信息熵。遍历每一维度的特征,即从第0维开始遍历到第n维,数据集中最后一列默认存放最后一个特征是标签。假设从第0维开始,将数据集中每一行的第0维特征取出,然后做去重工作,得到的特征集为{A, B, C}。通过拿到的不重复的特征,通过splitDataSet函数分割数据集算法,拿到符合该特征下的数据集(假设用A去分割,即数据即中若每行中的第0维特征是A,则取出此行,并取出行是已经去掉了第0维特征的),通过遍历所有的特征ABC,最终得到的3个子数据集且子数据集之间是不重复的,三个数据集合并一起就是总的数据集。
[讨论] 该3个数据集为什么是不重复且能合成总数据集?
由于是满足特征条件的行才会提取出来。例如数据集长这样:
[1 1 yes]
[1 1 yes]
[1 0 no ]
[0 1 no ]
[0 1 no ]
如果用第0维的各个特征作分割,因为第0维的特征只有01,就会分割成2个子数据集
A
[1 yes]
[1 yes]
[0 no ]
和
B
[1 no ]
[1 no ]
所以遍历每个特征,并用该特征分割数据集,是能保证数据完整性和不重复性的。
接着上文所说。我们已经通过提取第0维的所有特征,并分割出不同的数据集,但是怎么评估用第0维分割数据集是否是最好的呢?从较前处的上文可知道,信息熵是信息不确定度的量纲。我们就可以通过计算分割后的数据集信息熵进行评估。引用上面[讨论]的例子,假如用了第0维特征作为分割数据集,这时候我们得到了2个子数据集,下一步通过算出子数据集的行数与数据总行数的商,先算出子数据集的出现概率。即子数据集出现的概率为 P ( A ) = 3 5 , P ( B ) = 2 5 P(A)=\frac{3}{5}, P(B)=\frac{2}{5} P(A)=53,P(B)=52, 然后通过calcShannonEnt函数算法,根据A, B两个子数据集里面的标签算出不确定度期望——信息熵。最后通过AB子数据集的信息熵乘上出现概率,就是总的分割数据后的信息熵。
A中 P ( y e s ) = 2 3 , P ( n o ) = 1 3 P(yes)=\frac{2}{3},P(no)=\frac{1}{3} P(yes)=32,P(no)=31, H A = − 2 3 × l o g 2 2 3 − 1 3 × l o g 2 1 3 = 0.91 H_A=-\frac{2}{3}×log_2\frac{2}{3} - \frac{1}{3}×log_2\frac{1}{3} = 0.91 HA=−32×log232−31×log231=0.91
B中 P ( n o ) = 1 P(no)=1 P(no)=1, H B = − 1 × l o g 2 1 = 0 H_B = -1×log_21=0 HB=−1×log21=0
AB两个熵代表的意思是,A中有不同的标签所以不确定度不为0,因为会出现yes也会出现no的情况,yes和no的构成的信息混乱程度是0.91,而B中没有不同的标签所以不准确度为0即绝对是出现no。
而分割后数据集的总信息熵为 H = P ( A ) H A + P ( B ) H B = 3 5 × 0.91 + 0 = 0.546 H=P(A)H_A+P(B)H_B=\frac{3}{5}×0.91 + 0 = 0.546 H=P(A)HA+P(B)HB=53×0.91+0=0.546
分割后数据集的总信息熵代表的意思是, 用该维度的特征分割数据后,求各个子数据集的不确定度的均值。
又如果用第1维分割数据集,即有01两个特征, 01分割成两个子数据集
A
[1 yes]
[1 yes]
[0 no ]
[0 no ]
B
[1 no ]
P ( A ) = 4 5 , P ( B ) = 1 5 P(A)=\frac{4}{5}, P(B)=\frac{1}{5} P(A)=54,P(B)=51, H A = − P ( y e s ) l o g 2 P ( y e s ) − P ( n o ) l o g 2 P ( n o ) = − 1 2 l o g 2 1 2 − 1 2 l o g 2 1 2 = 1 H_A=-P(yes)log_2P(yes)-P(no)log_2P(no) = -\frac{1}{2}log_2\frac{1}{2}-\frac{1}{2}log_2\frac{1}{2}=1 HA=−P(yes)log2P(yes)−P(no)log2P(no)=−21log221−21log221=1, H B = − P ( n o ) l o g 2 P ( n o ) = − 1 × l o g 2 1 = 0 H_B=-P(no)log_2P(no)=-1×log_21=0 HB=−P(no)log2P(no)=−1×log21=0
H = P ( A ) H A + P ( B ) H B = 4 5 × 1 + 1 5 × 0 = 0.8 H=P(A)H_A+P(B)H_B=\frac{4}{5}×1+\frac{1}{5}×0=0.8 H=P(A)HA+P(B)HB=54×1+51×0=0.8
这样就可以使用该维度的信息熵,来判断用该维度分割数据后,数据分类结果的好坏了。即例如第n维的信息熵是0,里面有6个特征,则表示的是,用第n维度特征做分割,分割出来的6个子数据集中,他们的信息熵都为0,即6个数据集拿到的标签都是相同的,这样我们就可以通过这维度的特征作为标准,用以分类数据。又如上文的计算可得,用第0维分割数据后,信息熵为0.546,而用第1维分割数据后,信息熵为0.8,在数学角度上看,第0维分割比第一维好,从肉眼观察数据可得,也是如此。第0维分割的两个类别,A的yes占了0.67,B的no是1,因此就能完成了一个粗糙的分类器。而相对于第1维分割的两个类别,A的yes和no都占了0.5,B的no占了1,但是当数据在A数据集的时候,就有比较大的可能会分类错误。所以用信息熵来衡量分割效果是比较科学的。
回到代码,代码已经到了通过遍历所有特征,拿到用该特征分割后的数据集,通过这些数据集算出信息熵,再算出该维度分割后产生的信息熵的步骤。通过遍历所有的维度,找出信息熵最小的维度,然后返回此维度的i值,作为最佳分割方式。
统计传入的标签集,返回出现次数最多的那个标签
def majorityCnt(classList):
# 统计classList中,里面每一个分类的出现次数,并返回出现次数最多的那个分类
classCount = dict()
for vote in classList:
classCount[vote] = classCount.get(vote, 0) + 1
# 根据字典的值进行排序
sortedClassCount = sorted(classCount.items(), key=itemgetter(1), reverse=True)
return sortedClassCount[0][0]
创建决策树,直到深度最大为止。另外在sklearn的决策树算法中,可以设置树的深度。
此算法是通过计算信息熵,拿到信息熵最小的分割数据的方案,依此作为节点,进行分割。产生后的叶子如果可以分割,继续计算信息熵,拿到信息熵最小的分割数据的方案,依此作为节点,再进行分割。这是一个递归的过程。
def createTree(dataSet, labels):
# 获取数据集的标签索引
classList = [example[-1] for example in dataSet]
# 如果classList里的全部标签是一样的,则不用分类,直接返回标签
if classList.count(classList[0]) == len(classList):
return classList[0]
# 如果数据集没有任何维度的数据,只有标签,则返回标签出现次数最多的那个标签
if len(dataSet[0]) == 1:
return majorityCnt(classList)
# 获取最佳分割的维度 bestFeat 的索引值, 即数据集的所在列
bestFeat = chooseBestFeatureToSplit(dataSet)
# 通过拿到最佳维度 bestFeat,转化成该维度对应的标签
bestFeatLabel = labels[bestFeat]
# 把最佳的特征标签放进字典
myTree = {bestFeatLabel: {}}
# 删除最佳的那个维度
del labels[bestFeat]
# 遍历数据集 将第bestFeat列的数据取出, 即取出特征
featValues = [example[bestFeat] for example in dataSet]
# 特征去重
uniqueVals = set(featValues)
for value in uniqueVals:
# 取得labels维度标签的副本, 使得不改变这个labels标签
subLabels = labels[:]
# 递归
# 将该列的每个特征都先用splitDataSet进行分割,将分割后的数据再进行分类
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
data = [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
label = ['no surfacing', 'flippers']
createTree(data, label)
>> {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}