本文图片来源即部分代码参考自鲁东大学人工智能学院课程内容,整理出相关内容供大家更好的学习和理解决策树算法。
该课程在b站的链接,该视频非我上传
以下是百度百科的解释:
决策树(Decision Tree)是在已知各种情况发生概率的基础上,通过构成决策树来求取净现值的期望值大于等于零的概率,评价项目风险,判断其可行性的决策分析方法,是直观运用概率分析的一种图解法。由于这种决策分支画成图形很像一棵树的枝干,故称决策树。在机器学习中,决策树是一个预测模型,他代表的是对象属性与对象值之间的一种映射关系。Entropy = 系统的凌乱程度,使用算法ID3, C4.5和C5.0生成树算法使用熵。这一度量是基于信息学理论中熵的概念。
决策树是一种树形结构,其中每个内部节点表示一个属性上的测试,每个分支代表一个测试输出,每个叶节点代表一种类别。
分类树(决策树)是一种十分常用的分类方法。它是一种监督学习,所谓监督学习就是给定一堆样本,每个样本都有一组属性和一个类别,这些类别是事先确定的,那么通过学习得到一个分类器,这个分类器能够对新出现的对象给出正确的分类。这样的机器学习就被称之为监督学习。
简单来说决策树(Decision Treee)类似“二十个问题”的游戏,一方在脑海里想某个事物,其他参与者通过提问问题,得到“是”/“否”的答案,最终猜出结果。
举一个具体的例子:是否放贷的问题
这是一个银行是否放贷的案例,一个用户来银行办理贷款,银行根据用户的信息判断是否给用户放贷。可以看出这个案例中用户的主要评估信息有:
根据对以上信息的评估,最终得出是否要给用户办理贷款。
决策树的建立就是从这些特征中挑选出一种特征作为根节点,例如图中首先挑选年龄作为根节点,此时该特征分裂出三种情况,就是图中的0,1,2三种情况。
然后从剩下的特征中再选取一组特征作为子树的根节点,此时选取的特征为是否有工作,此时又会分裂出两种情况。其中最右侧的右子树,既年龄为0的子树,通过是否有工作就可以完全区分开决策的结果,此时这颗子树就无需再选取剩余特征作为决策条件。
相反如果还不能完全区分开结果,就需要重复选取剩余特征,分裂条件,最后直到结果被完全区分开,或者条件已经被用完了,但是任然没有完全区分开,其结果选取可能性最大的结果作为输出。例如:当条件都用完了,此时有房子的情况下任然不能完全区分开结果,其中2个人放贷,1个人没有放贷,那么选择放贷作为该节点的输出。
可以看出,决策树的构建是一个不断递归的构建子树的过程,直到结果被完全区分开或者特征被用完后跳出递归。
但是你可能发现了特征的选取顺序不同,就会得到完全不同的决策树,上面的例子中是先选取年龄,在选取是否有工作,最后选取是否有房子来构建的决策树,其中有否有贷款的条件还未考虑就直接得到了一颗完整的决策树。
下面来看另一种不同特征选取顺序构建的决策树:
先选取是否有房子,然后再选取是否有工作,就已经可以将数据集完全区分开。
从上面的例子中可以看出,特征的选择顺序对构建一颗决策树起着至关重要的影响特征选的好可能很容易就将数据集区分开,特征选取的不好,可能会导致决策树的高度很高,但区分效果却很差。
那么该如何选取特征?
信息熵:与事件发生的概率成反比,事件发生可能性越大熵越小,事件发生可能性越小熵越大。
L ( x i ) = − l o g 2 P ( x i ) L(x_i) = -log_2P(x_i) L(xi)=−log2P(xi)
平均信息熵:既对事件不同情况的熵求平均值
H = − ∑ i = 1 N P ( x i ) l o g 2 P ( x i ) H=-\sum^{N}_{i=1}{P(x_i)log_2P(x_i)} H=−i=1∑NP(xi)log2P(xi)
例如有2个事件A、B
熵越大,事件的发生越无序
熵越小,事件的发生越确定
例如上述例子中,当A和B发生的概率都为1/2时,两种事件都有可能发生,此时的熵最大,等于1。因为你无法确定哪个事件更有可能发生。
当A的事件为1/4,B的事件为3/4时,此时求得的熵为0.81,因为明显B比A可能发生。
当继续减小A的事件发生的概率为1/8,B的事件为7/8时,此时求得的熵为0.54,因为明显B比A发生的可能性更大了,不确定性就更小了。
极端情况,当A的发生概率为0,B的发生概率为1,此时B一定会发生,结果为确定因素,求得的熵也为0。
信息增益:用重新选择特征的后求得熵减去原来的熵得到的结果就是信息增益,信息增益越大,就说明此次选择的结果比原的结果更好。
根据年龄的划分,分别求取age=0,1,2三种情况下的信息熵。
然后根据不同情况的概率求取平均熵。
最后求得的熵为0.84
下面看根据是否有房子进行划分
求得的熵为0.55
0.55 < 0.84
这就说明选取是否有房子进行划分比根据年龄进行划分得到的效果好。
按照这种方法,分别求取其他划分的熵,最后选取熵最小的划分结果作为根节点,然后在划分后的子树中继续计算熵,选取熵最小的划分。
找到可以令平均熵最小的特征维度对数据集进行分割;
对分割后的数据集再找寻可以使平均熵最小的特征维度,在对数据集进行分割;
重复上面步骤直到用完所有特征或者子集中目标标签全部相同;
首先给出课程中使用的代码,后面我会给出pandas读取数据集并作处理的代码。
首先要引入必要的包,求熵需要用到log函数
from math import log
定义数据集,最后一个维度是标签
#创建数据集
def createDataSet():
"""DateSet 基础数据集
Args:
无需传入参数
Returns:
返回数据集和对应的label标签
"""
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
计算一个数据集的熵
def calcShannonEnt(dataSet):
""" 计算给定数据集的香农熵
Args:
dataSet 数据集
Returns:
返回 每一组feature下的某个分类下,香农熵的信息期望
"""
#数据集的行数
numEntries = len(dataSet)
#收集所有目标标签(最后一个维度)
labels = [featVec[-1] for featVec in dataSet]
#去重、获取标签种类
keys = set(labels)
shannonEnt = 0.0
for key in keys :
# 计算每种标签出现的次数
prob = float(labels.count(key)) / numEntries
# 计算香农熵,以 2 为底求对数
shannonEnt -= prob * log(prob, 2)
# print '---', prob, prob * log(prob, 2), shannonEnt
return shannonEnt
分割数据集,将axis维 等于value的数据集提取出来
def splitDataSet(dataSet, axis, value):
"""通过遍历dataSet数据集,求出axis对应的colnum列的值为value的行)
就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
Args:
dataSet 数据集 待划分的数据集
axis 表示每一行的axis列 划分数据集的特征
value 表示axis列对应的value值 需要返回的特征的值。
Returns:
axis列为value的数据集【该数据集需要排除axis列】
"""
retDataSet = []
for featVec in dataSet:
# axis列为value的数据集【该数据集需要排除index列】
# 判断axis列的值是否为value
if featVec[axis] == value:
# [:axis]表示前axis行,即若 axis为2,就是取 featVec 的前 axis 行
reducedFeatVec = featVec[:axis]
'''
请百度查询一下: extend和append的区别
list.append(object) 向列表中添加一个对象object
list.extend(sequence) 把一个序列seq的内容添加到列表中
1、使用append的时候,是将new_media看作一个对象,整体打包添加到music_media对象中。
2、使用extend的时候,是将new_media看作一个序列,将这个序列和music_media序列合并,并放在其后面。
result = []
result.extend([1,2,3])
print result
result.append([4,5,6])
print result
result.extend([7,8,9])
print result
结果:
[1, 2, 3]
[1, 2, 3, [4, 5, 6]]
[1, 2, 3, [4, 5, 6], 7, 8, 9]
'''
reducedFeatVec.extend(featVec[axis + 1:])
# [axis+1:]表示从跳过 axis 的 axis+1行,取接下来的数据
# 收集结果值 axis列为value的行【该行需要排除axis列】
retDataSet.append(reducedFeatVec)
return retDataSet
在一个数据集中找到使熵减少量最大的维度,即找到使信息增益最大的维度
def chooseBestFeatureToSplit(dataSet):
"""选择最好的特征
Args:
dataSet 数据集
Returns:
bestFeature 最优的特征列
"""
# 求第一行有多少列的 Feature, 最后一列是label列嘛
numFeatures = len(dataSet[0]) - 1
# label的信息熵
baseEntropy = calcShannonEnt(dataSet)
# 最优的信息增益值, 和最优的Featurn编号
bestInfoGain, bestFeature = 0.0, -1
# iterate over all the features
for i in range(numFeatures):
# create a list of all the examples of this feature
# 获取每一个实例的第i+1个feature,组成list集合
featList = [example[i] for example in dataSet]
# get a set of unique values
# 获取剔重后的集合,使用set对list数据进行去重
uniqueVals = set(featList)
# 创建一个临时的信息熵
newEntropy = 0.0
# 遍历某一列的value集合,计算该列的信息熵
# 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
# gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
# 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
infoGain = baseEntropy - newEntropy #信息增益
#print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
返回一个list中出现次数最多的元素,用于决策树构建时如果用完了所有特征都没有划分开的那种情况下,返回出现最多的结果
def majorityCnt(classList):
"""选择出现次数最多的一个结果
Args:
classList label列的集合
Returns:
bestFeature 最优的特征列
"""
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
# 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
# print 'sortedClassCount:', sortedClassCount
return sortedClassCount[0][0]
决策树的构建,决策树的构建是一个递归的过程,注意出口条件
def createTree(dataSet, labels, lab_sel):
#获取分类标签
classList = [example[-1] for example in dataSet]
# 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
# 第一个停止条件: 所有的类标签完全相同,则直接返回该类标签。
# count() 函数是统计括号中的值在list中出现的次数
if classList.count(classList[0]) == len(classList):
return classList[0]
# 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
# 第二个停止条件: 使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
if len(dataSet[0]) == 1:
return majorityCnt(classList)
# 选择最优的列,得到最优列对应的label含义
bestFeat = chooseBestFeatureToSplit(dataSet)
# 获取label的名称
bestFeatLabel = labels[bestFeat]
#lab_sel用与保存标签,因为labels是可变列表,在下面执行del后就会删除里面的某个标签,而且是全局的删除,所以要用lab_sel草存下来
lab_sel.append(labels[bestFeat])
# 初始化myTree
myTree = {bestFeatLabel: {}}
del (labels[bestFeat])
#print("labels[bestFeat]", labels)
# 取出最优列,然后它的branch做分类
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
# 求出剩余的标签label
subLabels = labels[:]
#遍历创建决策树
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels, lab_sel)
return myTree
进行分类
def classify(inputTree, featLabels, testVec):
"""给输入的节点,进行分类
Args:
inputTree 决策树模型
featLabels Feature标签对应的名称
testVec 测试输入的数据
Returns:
classLabel 分类的结果值,需要映射label才能知道名称
"""
# 获取决策树节点
firstStr = next(iter(inputTree))
# 下一个字典
secondDict = inputTree[firstStr]
# 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
featIndex = featLabels.index(firstStr)
# 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
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
整合各个模块
def fishTest():
#创建数据和结果标签
dataSet, labels = createDataSet()
# print myDat, labels
lab_sel = []
myTree = createTree(dataSet, labels, lab_sel)
return myTree, lab_sel
测试
#测试
myTree, lab_sel = fishTest()
print(myTree)
print(lab_sel)
testVec = [0, 1, 1, 2]
result = classify(myTree ,lab_sel,testVec)
print(result)
下面是一个基于pandas读取标准数据集的完整代码块,供大家参考理解
其中数据集是如下格式的csv文件,大家可以自己创建并放入dataSet文件夹下
代码部分
import math
import numpy as np
import pandas as pd
def load_data():
"""
加载数据集
:return: DataFrame
"""
df = pd.read_csv("./dataSet/01_decisionTree_data.csv")
return df
def cal_shannonEnt(data_df):
"""
计算该数据集的香农熵
:param data_df: 数据集
:return:
"""
row_num = len(data_df)
data_np_array = np.array(data_df)
label_set = set(data_np_array[:, -1])
label_set = {label: 0 for label in label_set}
for row in data_np_array:
label_set[row[-1]] += 1
shannonEnt = 0.0
for key in label_set:
prob = float(label_set[key]) / row_num
shannonEnt -= prob * math.log(prob, 2)
return shannonEnt
def split_data_set(data_df, index, value):
"""
删除index和index列下值不等于value的行
:param data_df: dataFrame
:param index: 列坐标
:param value: 比较值
:return: 剔除后的dataFrame
"""
# data_np_array = np.array(data_df)
# res_data_set = []
# for row in data_np_array:
# print(row)
# if row[index] == value:
# reducedRow = list(row[:index])
# reducedRow.extend(row[index + 1:])
# res_data_set.append(reducedRow)
# print(np.array(res_data_set))
# return np.array(res_data_set)
# 得到df的列名
col_name = data_df.columns[index]
# 取col_name列下等于value的行
new_df = data_df[data_df[col_name] == value]
# 删除index列
new_df = new_df.drop(col_name, axis=1)
print(new_df)
return new_df
def majority_cnt(label_list):
"""
选择出现最多的标签返回
:param label_list: list
:return:
"""
# 用dict计算出不同标签的个数
label_dict = dict()
label_set = set(label_list)
for i in label_set:
label_dict[i] = 0
# 遍历list
for i in label_list:
label_dict[i] += 1
# sort这个dict取最大
sorted_label = sorted(label_dict.items(), key=lambda i: i[0], reverse=False)
# 返回次数最大的标签
return sorted_label[0][0]
def build_tree(data_df):
"""
建树相关的代码
:param data_df: dataFrame
:return:
"""
# 取出标签列表
label_list = list(data_df["label"])
# 若这个矩阵的所有标签都一致 则不再分类 直接返回该类标签
if label_list.count(label_list[0]) == len(label_list):
return label_list[0]
# 若该数据集只剩下一个特征和label列了 那么不再分类
if len(data_df.iloc[0, :]) == 2:
return majority_cnt(label_list)
# 得到最优特征
best_feature_index = find_best_feature_4_split(data_df)
# 通过index得到feature_name
feature_name = data_df.columns[best_feature_index]
# 初始化树
tree = {feature_name: {}}
# 把data_df处理一下 选定了最优特征了就需要把这列特征删除 然后在剩余的矩阵里再递归选择
unique_val = set(list(data_df[feature_name]))
data_df.drop(feature_name, axis=1, inplace=True)
for val in unique_val:
tree[best_feature_index][val] = build_tree(split_data_set(data_df, best_feature_index, val))
return tree
def find_best_feature_4_split(data_df):
"""
找到最合适的特征做分割
:param data_df: dataFrame
:return:
"""
# print(data_df)
# 求特征数
feature_num = len(data_df.columns) - 1
# 计算原矩阵信息熵
base_shannonEnt = cal_shannonEnt(data_df)
# print(base_shannonEnt)
# 最优信息增益值 对应特征index
best_info_gain, best_feature_index = 0.0, -1
for index in range(feature_num):
# 获得该列的list
col_list = data_df.iloc[:, index]
col_list = list(col_list)
# 该列去重
unique_value = list(set(col_list))
tmp_ent = 0.0
for value in unique_value:
sub_data_set = split_data_set(data_df, index, value)
# print(sub_data_set)
prob = len(sub_data_set) / float(len(data_df))
tmp_ent += prob * cal_shannonEnt(sub_data_set)
# print(tmp_ent)
info_gain = base_shannonEnt - tmp_ent
if info_gain > best_info_gain:
best_info_gain = info_gain
best_feature_index = index
"""
经过打印可以看到下面结果:
nosurfacing flippers label
0 1 1 yes
1 1 1 yes
2 1 0 no
3 0 1 no
4 0 1 no
# 这是原矩阵的香农熵 可以看到此时熵很大 所以很无序
0.9709505944546686
# 下面两个小矩阵是针对第一列遍历0,1两种结果取得的子矩阵 split_data_set()函数的作用是除去index列和index列上不等于value的行
flippers label
3 1 no
4 1 no
flippers label
0 1 yes
1 1 yes
2 0 no
# 可以看到两个小矩阵的香农熵之和为0.55 小了很多
0.5509775004326937
nosurfacing label
2 1 no
nosurfacing label
0 1 yes
1 1 yes
3 0 no
4 0 no
# 同理,这两个矩阵香农熵0.8 比按照第一列的value=0分割混乱(熵大)
0.8
注:信息增益的算法就是原矩阵的香农熵(0.97)-分割后的香农熵(越小则增益越大),增益作为最终选择特征的标准来做判断
"""
return best_feature_index
if __name__ == '__main__':
data_df = load_data()
# cal_shannonEnt(data_df)
# split_data_set(data_df, 1, 1)
# find_best_feature_4_split(data_df)
# build_tree(data_df)
# majority_cnt(list(data_df["label"]))
build_tree(data_df)
以上就是全部内容了,希望对大家的学习有所帮助,传统的机器学习可能比不上现在的深度学习那么火热,但是牢牢的掌握这些基础的机器学习算法,对我们更好的理解和突破深度学习上的瓶颈也是必要的。