0. 前言
之前的学习过程中,构造决策树的过程以及用其进行预测的过程都是通过调用算法库来实现的,为了更好地理解该算法,将参照《机器学习实战》一书来实现这个过程。
1. 构造决策树
数据
不浮出水面可否生存 | 是否有脚蹼 | 属于鱼类 |
---|---|---|
是 | 是 | 是 |
是 | 是 | 是 |
是 | 否 | 否 |
否 | 是 | 否 |
否 | 是 | 否 |
我们通过两种特征来区分某种动物是否属于鱼类。但是需要思考的是,最开始该如何选取特征,选第一个还是第二个,哪一种选择获得的效果更好。这需要通过计算他们的信息增益来决定。通过计算每个特征值划分数据之后的信息增益,比较计算结果,信息增益更大的就是值得选择的特征。
信息熵(entropy):熵是描述信息的不确定度的,是随机变量不确定度的度量。熵越大,信息的不确定度越大,信息越“混乱”,越不符合决策树分类的需求。
Ent(D) = - ∑ k = 1 n \sum_{k=1}^n ∑k=1n p k p_k pk l o g 2 log_2 log2 p k p_k pk
D:样本集合; p k p_k pk:第k类样本所占的比例(k=1,2,3…n)
计算信息熵(香农熵)
def calShannonEnt(dataSet):
"""计算数据集的香农熵"""
numEntries = len(dataSet) # 数据集中实例总数
labelCounts = {} # 创建数据字典,键值记录label出现的次数
for featVec in dataSet:
currentLable = featVec[-1]
if currentLable not in labelCounts.keys():
labelCounts[currentLable] = 0
labelCounts[currentLable] += 1
print(featVec, labelCounts)
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries # 计算概率p
shannonEnt -= prob*log(prob, 2)
return shannonEnt
测试一下这个函数:需要先创建一个数据集,再调用创建数据集的方法和计算香农熵的方法:
from math import log
def createDataSet():
"""创建数据集"""
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
def calShannonEnt(dataSet):
"""计算数据集的香农熵"""
numEntries = len(dataSet) # 数据集中实例总数
labelCounts = {} # 创建数据字典,键值记录label出现的次数
for featVec in dataSet:
currentLable = featVec[-1]
if currentLable not in labelCounts.keys():
labelCounts[currentLable] = 0
labelCounts[currentLable] += 1
print(featVec, labelCounts)
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries # 计算概率p
shannonEnt -= prob*log(prob, 2)
return shannonEnt
myData, labels = createDataSet()
print("数据集:", myData)
print(calShannonEnt(myData))
输出:
数据集: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
# 这里输出的是数据字典统计label次数的过程,每遍历到一个样本,就根据其所属类别更改label的值
[1, 1, 'yes'] {'yes': 1}
[1, 1, 'yes'] {'yes': 2}
[1, 0, 'no'] {'yes': 2, 'no': 1}
[0, 1, 'no'] {'yes': 2, 'no': 2}
[0, 1, 'no'] {'yes': 2, 'no': 3}
# 计算所得的香农熵
0.9709505944546686
划分数据集
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
测试:
myData, labels = createDataSet()
print("数据集:", myData)
print(splitDataSet(myData, 0, 1))
输出结果:
数据集: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
[[1, 'yes'], [1, 'yes'], [0, 'no']]
选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
"""选择最好的数据集划分"""
numFeatures = len(dataSet[0]) - 1 # 特征总个数
baseEntropy = calShannonEnt(dataSet) # 计算数据集的信息熵
bestInfoGain, bestFeature = 0.0, -1 # 最优的信息增益值, 最优的Feature编号
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # 获取各实例第i+1个特征
uniqueVals = set(featList)
newEntropy = 0.0 # 创建一个新的信息熵
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calShannonEnt(subDataSet)
# 比较所有特征中的信息增益,返回最好特征划分的索引值。
infoGain = baseEntropy - newEntropy
print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
测试:
myData, labels = createDataSet()
print("数据集:", myData)
print("choose", chooseBestFeatureToSplit(myData))
输出结果:
数据集: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
[1, 1, 'yes'] {'yes': 1}
[1, 1, 'yes'] {'yes': 2}
[1, 0, 'no'] {'yes': 2, 'no': 1}
[0, 1, 'no'] {'yes': 2, 'no': 2}
[0, 1, 'no'] {'yes': 2, 'no': 3}
[1, 'no'] {'no': 1}
[1, 'no'] {'no': 2}
[1, 'yes'] {'yes': 1}
[1, 'yes'] {'yes': 2}
[0, 'no'] {'yes': 2, 'no': 1}
infoGain= 0.4199730940219749 bestFeature= 0 0.9709505944546686 0.5509775004326937
[1, 'no'] {'no': 1}
[1, 'yes'] {'yes': 1}
[1, 'yes'] {'yes': 2}
[0, 'no'] {'yes': 2, 'no': 1}
[0, 'no'] {'yes': 2, 'no': 2}
infoGain= 0.17095059445466854 bestFeature= 1 0.9709505944546686 0.8
choose 0
根据输出结果,可以看出,选择0的话,信息熵更大,即是更优的选择。
创建决策树
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]
# count()统计括号中的值在list中出现的次数
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) # 选择最优列,得到最优列对应的label含义
bestFeatLabel = labels[bestFeat] # 获取label的名称
myTree = {bestFeatLabel: {}}
featValues = [example[bestFeat] for example in dataSet] # 取出最优列并对它的branch做分类
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
# 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
print(myTree)
return myTree
测试:
myData, labels = createDataSet()
print("数据集:", myData)
myTree = createTree(myData, labels)
输出结果:
数据集: [[1, 1, 'yes'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
{'no surfacing': {0: 'no', 1: 'yes'}}
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
根据结果可以看出,该树的根节点是no surfacing,flippers是第一层被划分的节点。