文章原发布于本人公众号,零度芝士,公众号上排版用心一点。
本文以周志华老师的《机器学习》第4章内容为基础对决策树进行介绍,代码以及关于sklearn的部分参考了博主Jack-Cui的文章.
图为本文主要提纲
【本文约11000字,预计阅读时长20分钟】
01
基本流程
决策树(decision tree)是一类常见的机器学习算法,是根据已有的信息对新的示例进行判断,例如银行可通过图1的决策树决定是否给某申请人放贷.决策树一般包含一个根节点、若干个内部节点和若干个叶节点,叶节点对应决策结果,其他节点则对应于一个属性测试,其基本流程符合“分而治之”的学习策略.
图1 银行房贷决策树
决策树的生成是一个递归过程,其基本算法如图2所示.
图2 决策树基本算法
02
特征选择
由图2所示算法可以看出,决策树学习的关键在于如何选择最优划分属性.原则上,随着划分过程的不断进行,我们希望决策树的分支节点所包含的样本尽可能属于同一类别,即节点的“纯度”越来越高.一般有信息增益、增益率和基尼指数等判定指标.
2.1 信息增益
信息熵是度量样本集合纯度的一种指标,信息熵越小,样本集合的纯度越高.假定当前样本集合D中第k类样本所占比例为pk(k=1,2,...,|y|),则D的信息熵定义为:
Ent(D)的值越小,则D的纯度越高.
但当选定一个属性来对样本集合进行分类后,不确定性程度会减小,划分前后信息熵的差值就可以衡量这个属性划分带来的“纯度提升”.假定离散属性a有V个可能的取值{a1,a2,...,aV},划分后得到V个分支节点,其中第v个分支节点包含了D中所有在属性a上取值为av的样本,记为Dv.计算出Dv的信息熵并对分支节点赋予权重|DV|/|D|,得到信息增益:
Gain(D,a)的值越大,则用属性a划分获得的“纯度提升”越大.因此我们可以用信息增益作为决策树算法中最优划分属性的选择指标,选择属性a*=arg maxGain(D,a).
使用信息增益为准则的为ID3决策树算法.
2.2 增益率
信息增益准则对可取值数目较多的属性有所偏好,为减少这种偏好带来的不利影响,引入新准则“增益率”来作为划分属性:
其中
增益率对可取值数目较少的属性有偏好,因此在使用时不是直接选择增益率最大的划分属性,而是先从候选划分属性中找出信息增益高于平均水平的属性,再从中选择增益率最大的.依照这样的划分属性选择准则的为C4.5决策树算法.
2.3 基尼指数
以基尼指数作为划分属性选择指标的为CART决策树算法。
介绍略。
注:上述关于信息、熵的定义和推导等详见信息论,但在机器学习算法中只需理解并会使用即可。
03
剪枝处理
剪枝(pruning)是决策树算法为了避免过拟合的主要手段,即通过测试集来“剪掉”一些结点,进而获得泛化性能的提升,主要有预剪枝和后剪枝两种策略。
3.1 预剪枝
预剪枝就是在训练决策树的同时进行剪枝,自上向下。例如,基于信息增益准则,选取“脐部”划分训练集,但是否应该进行这个划分呢?预剪枝需要考虑划分前后的泛化性能(用在测试集上的正确率评估)。
划分前,所有样例集中在根节点,则根节点被标记为叶节点,其类别标记为训练样例数最多的类别(若最多的类不唯一,任选一类),在这里标记为“好瓜”。故测试集所有样例将被标记为“好瓜”,仅编号{4,5,8}样例被分类正确,正确率为3/7。
划分后,图中节点②、③、④分别包含编号为{1,2,3,14}、{6,7,15,17}、{10,16}的训练样例,故分别被标记为“好瓜”、“好瓜”、“坏瓜”。此时,测试集被划分正确的比例为5/7。
由划分后在测试集上的正确率高于划分前可判定,决定划分。
预剪枝不仅降低了“过拟合”的风险,还可以减少训练时间开销和测试开销。但预剪枝本质是基于“贪心”来禁止划分,可能会带来欠拟合风险。
图3 基于数据表4.2生成的未剪枝决策树
图4 基于数据表4.2生成的预剪枝决策树
3.2 后剪枝
后剪枝是先从训练集生成一颗完整的决策树,然后基于测试集自下而上进行剪枝操作。
后剪枝决策树通常比预剪枝决策树保留了更多的分支,其欠拟合风险很小,泛化性能更优,但训练时间开销要更大。
图5 基于数据表4.2生成的后剪枝决策树
04
连续值与缺失值
4.1 连续值处理
在决策树算法中,如果出现连续属性,可通过离散化技术将其转化为离散变量,比如二分法,C4.5决策树算法正是采用这种机制。
假定连续属性a在样本集D上出现了n个不同的取值,将其从小到大进行排列{a1,a2,...,an}。基于划分点t可将基本集D分为大于t和小于等于t的两类。我们在此考察包含n-1个元素的候选划分点集合:
通过信息增益最大化原则选取最优的划分点:
4.2 缺失值处理
现实任务中常常有不完整样本,即样本的某些属性值缺失。如果简单地放弃不完整样本,仅用无缺失值的样本来进行学习,是对数据信息的极大浪费,所以有必要考虑利用有缺失值的训练样例来进行学习。
由此有两个问题需要解决:(1)如何在属性值缺失的情况下进行划分属性选择?(2)给定划分属性,如样本在该属性值上缺失,如何对样本进行划分?
在C4.5决策树算法中,首先设定所有样本的初始权值为1,然后设定规则更新权重。对于问题(1),通过引入权重推广信息增益和信息熵的公式。对于问题(2),将缺失属性值的样本以不同权重同时划入所有子节点。
具体公式细节略。
05
具体代码
运行平台:Windows Anaconda3
"""
Author:朱朱
代码在Jack Cui的基础上进行适当修改
修改主要集中在创建决策树函数部分,按西瓜书所示算法改进了流程
剪枝部分独立完成
"""
# -*- coding: UTF-8 -*-
from math import log
import operator
import pandas as pd
from matplotlib.font_manager import FontProperties
import matplotlib.pyplot as plt
"""
数据集说明:
dataset:样本集合,仅含属性值和标记值.
如基础数据:
[['青绿', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是'],
['乌黑', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '是'],
['乌黑', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是'],
['青绿', '蜷缩', '沉闷', '清晰', '凹陷', '硬滑', '是'],
['浅白', '蜷缩', '浊响', '清晰', '凹陷', '硬滑', '是'],
['青绿', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '是'],
['乌黑', '稍蜷', '浊响', '稍糊', '稍凹', '软粘', '是'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '硬滑', '是'],
['乌黑', '稍蜷', '沉闷', '稍糊', '稍凹', '硬滑', '否'],
['青绿', '硬挺', '清脆', '清晰', '平坦', '软粘', '否'],
['浅白', '硬挺', '清脆', '模糊', '平坦', '硬滑', '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '软粘', '否'],
['青绿', '稍蜷', '浊响', '稍糊', '凹陷', '硬滑', '否'],
['浅白', '稍蜷', '沉闷', '稍糊', '凹陷', '硬滑', '否'],
['乌黑', '稍蜷', '浊响', '清晰', '稍凹', '软粘', '否'],
['浅白', '蜷缩', '浊响', '模糊', '平坦', '硬滑', '否'],
['青绿', '蜷缩', '沉闷', '稍糊', '稍凹', '硬滑', '否']]
labels:属性名列表.
['色泽', '根蒂', '敲声', '纹理', '脐部', '触感']
"""
"""
函数说明:计算给定数据集的信息熵
Parameters:
dataset - 数据集
Returns:
Ent - 信息熵
"""
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)计数
Ent = 0.0 #信息熵
for key in labelCounts: #计算信息熵
prob = float(labelCounts[key]) / numEntires #选择该标签(Label)的概率
Ent -= prob * log(prob, 2) #利用公式计算
return Ent #返回信息熵
"""
函数说明:按照给定特征划分数据集
Parameters:
dataSet - 待划分的数据集
axis - 划分数据集的特征
value - 需要返回的特征的值
Returns:
无
"""
def splitDataSet(dataSet, axis, value):
retDataSet = [] #创建返回的数据集列表
for featVec in dataSet: #遍历数据集
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]