决策树是机器学习中一种非常常见的分类与回归方法,可以认为是if-else结构的规则。分类决策树是由节点和有向边组成的树形结构,节点表示特征或者属性, 而边表示的是属性值,边指向的叶节点为对应的分类。在对样本的分类过程中,由顶向下,根据特征或属性值选择分支,递归遍历直到叶节点,将实例分到叶节点对应的类别中。 决策树的学习过程就是构造出一个能正取分类(或者误差最小)训练数据集的且有较好泛化能力的树,核心是如何选择特征或属性作为节点, 通常的算法是利用启发式的算法如ID3,C4.5,CART等递归的选择最优特征。选择一个最优特征,然后按照此特征将数据集分割成多个子集,子集再选择最优特征, 直到所有训练数据都被正取分类,这就构造出了决策树。决策树有如下特点:
本文主要介绍基于ID3的算法构造决策树。
训练数据集有多个特征,如何递归选择最优特征呢?信息熵增益提供了一个非常好的也非常符合人们日常逻辑的判断准则,即信息熵增益最大的特征为最优特征。在信息论中,熵是用来度量随机变量不确定性的量纲,熵越大,不确定性越大。熵定义如下:
此处log一般是以2为底,假设一个产品成品率为100%次品率为0%那么熵就为0,如果是成品率次品率各为50%,那么熵就为1,熵越大,说明不确定性越高,非常符合我们人类的思维逻辑。假设分类标记为随机变量Y,那么H(Y)表示随机变量Y的不确定性,我们依次选择可选特征,如果选择一个特征后,随机变量Y的熵减少的最多,表示得知特征X后,使得类Y不确定性减少最多,那么就把此特征选为最优特征。信息熵增益的公式如下:
决策树基于信息熵增益的ID3算法步骤如下:
#encoding:utf-8
import pandas as pd
import numpy as np
class DecisionTree:
def __init__(self):
self.model = None
def calEntropy(self, y): # 计算熵
valRate = y.value_counts().apply(lambda x : x / y.size) # 频次汇总 得到各个特征对应的概率
valEntropy = np.inner(valRate, np.log2(valRate)) * -1
return valEntropy
def fit(self, xTrain, yTrain = pd.Series()):
if yTrain.size == 0:#如果不传,自动选择最后一列作为分类标签
yTrain = xTrain.iloc[:,-1]
xTrain = xTrain.iloc[:,:len(xTrain.columns)-1]
self.model = self.buildDecisionTree(xTrain, yTrain)
return self.model
def buildDecisionTree(self, xTrain, yTrain):
propNamesAll = xTrain.columns
#print(propNamesAll)
yTrainCounts = yTrain.value_counts()
if yTrainCounts.size == 1:
#print('only one class', yTrainCounts.index[0])
return yTrainCounts.index[0]
entropyD = self.calEntropy(yTrain)
maxGain = None
maxEntropyPropName = None
for propName in propNamesAll:
propDatas = xTrain[propName]
propClassSummary = propDatas.value_counts().apply(lambda x : x / propDatas.size)# 频次汇总 得到各个特征对应的概率
sumEntropyByProp = 0
for propClass, dvRate in propClassSummary.items():
yDataByPropClass = yTrain[xTrain[propName] == propClass]
entropyDv = self.calEntropy(yDataByPropClass)
sumEntropyByProp += entropyDv * dvRate
gainEach = entropyD - sumEntropyByProp
if maxGain == None or gainEach > maxGain:
maxGain = gainEach
maxEntropyPropName = propName
#print('select prop:', maxEntropyPropName, maxGain)
propDatas = xTrain[maxEntropyPropName]
propClassSummary = propDatas.value_counts().apply(lambda x : x / propDatas.size)# 频次汇总 得到各个特征对应的概率
retClassByProp = {}
for propClass, dvRate in propClassSummary.items():
whichIndex = xTrain[maxEntropyPropName] == propClass
if whichIndex.size == 0:
continue
xDataByPropClass = xTrain[whichIndex]
yDataByPropClass = yTrain[whichIndex]
del xDataByPropClass[maxEntropyPropName]#删除已经选择的属性列
#print(propClass)
#print(pd.concat([xDataByPropClass, yDataByPropClass], axis=1))
retClassByProp[propClass] = self.buildDecisionTree(xDataByPropClass, yDataByPropClass)
return {'Node':maxEntropyPropName, 'Edge':retClassByProp}
def predictBySeries(self, modelNode, data):
if not isinstance(modelNode, dict):
return modelNode
nodePropName = modelNode['Node']
prpVal = data.get(nodePropName)
for edge, nextNode in modelNode['Edge'].items():
if prpVal == edge:
return self.predictBySeries(nextNode, data)
return None
def predict(self, data):
if isinstance(data, pd.Series):
return self.predictBySeries(self.model, data)
return data.apply(lambda d: self.predictBySeries(self.model, d), axis=1)
dataTrain = pd.read_csv("xiguadata.csv", encoding = "gbk")
decisionTree = DecisionTree()
treeData = decisionTree.fit(dataTrain)
print(pd.DataFrame({'预测值':decisionTree.predict(dataTrain), '正取值':dataTrain.iloc[:,-1]}))
import json
print(json.dumps(treeData, ensure_ascii=False))
训练结束后,使用一个递归的字典保存决策树模型,使用格式json工具格式化输出后,可以简洁的看到树的结构。
dataTrain <- read.csv("xiguadata.csv", header = TRUE)
trainDecisionTree <- function(dataTrain){
calEntropy <- function(y){ # 计算熵
values <- table(unlist(y)); # 频次汇总 得到各个特征对应的概率
valuesRate <- values / sum(values);
logVal = log2(valuesRate);# log2(0) == infinite
logVal[is.infinite(logVal)]=0;
valuesEntropy <- -1 * t(valuesRate) %*% logVal;
if (is.nan(valuesEntropy)){
valuesEntropy = 0;
}
return(valuesEntropy);
}
propNamesAll <- names(dataTrain)
propNamesAll <- propNamesAll[length(propNamesAll) * - 1]
print(propNamesAll)
buildDecisionTree <- function(propNames, dataSet){
classColumn = dataSet[, length(dataSet)]#最后一列是类别标签
classSummary <- table(unlist(classColumn))# 频次汇总
defaultRet = c(propNames[1], names(classSummary)[which.max(classSummary)]);
if (length(classSummary) == 1){#如果所有的都是同一类别,那么标记为叶节点
return(defaultRet);
}
if (length(propNames) == 1){#如果只剩一种属性了,那么返回样本数量最多的类别作为节点
return(defaultRet);
}
entropyD <- calEntropy(classColumn)
propGains = sapply(propNames, function(propName){ # propName 对应的是"色泽" "根蒂" "敲声" "纹理" "脐部" "触感"
propDatas <- dataSet[c(propName)]
propClassSummary <- table(unlist(propDatas))# 频次汇总
retGain <- sapply(names(propClassSummary), function(propClass){# propClass 对应色泽的种类 如 浅白 青绿 乌黑
dataByPropClass <- subset(dataSet, dataSet[c(propName)] == propClass); #筛选出色泽等于 种类 propClass 的数据集
entropyDv <- calEntropy(dataByPropClass[, length(dataByPropClass)]) #最后一列是标记是否为好瓜
Dv = propClassSummary[c(propClass)][1]
return(entropyDv * Dv);# 这里没有直接除|D|,最后累加后再除,等价的
});
return(entropyD - sum(retGain)/sum(propClassSummary));
});
#print(propGains);
maxEntropyProp = propGains[which.max(propGains)];#选择信息熵增益最大的属性
propName = names(maxEntropyProp)[1]
#print(propName)
propDatas <- dataSet[c(propName)]
propClassSummary <- table(unlist(propDatas))# 频次汇总
propClassSummary <- propClassSummary[which(propClassSummary > 0)]
propClassNames <- names(propClassSummary)
#propClassNames = c(propClassNames[1])
retGain <- sapply(propClassNames, function(propClass){# propClass 对应色泽的种类 如 浅白 青绿 乌黑
dataByPropClass <- subset(dataSet, dataSet[c(propName)] == propClass); #筛选出色泽等于 种类 propClass 的数据集
leftClassNames = propNames[which(propNames==propName) * -1] #去掉这个属性,递归构造决策树
ret = buildDecisionTree(leftClassNames, dataByPropClass);
return(ret);
});
#names(retGain) = propClassNames
retList = retGain
#retList = list()
#for (propClass in propClassNames){
# retList[propClass] = retGain[propClass]
#}
#print(retList)
#索引1表示选择的属性名称 索引2对应的类别,如果有子树那么就是frame,否则就是类别
ret = list(propName, retList)
#ret = data.frame(c(retList))
#names(ret) = c(propName)
return(ret);
}
retProp = buildDecisionTree(propNamesAll, dataTrain);
return(retProp);
}
decisionTree = trainDecisionTree(dataTrain)
#print(decisionTree)
library("rpart")
library("rpart.plot")
dataTrain <- read.csv("xiguadata.csv", header = TRUE)
print(dataTrain)
fit <- rpart(HaoGua~.,data=dataTrain,control = rpart.control(minsplit = 1, minbucket = 1),method="class")
printcp(fit)
rpart.plot(fit, branch = 1, branch.type = 1, type = 2, extra = 102,shadow.col='gray', box.col='green',border.col='blue', split.col='red',main="DecisionTree")
#library(jsonlite)
#dataJson = toJSON(decisionTree)
#c <- file( "result.txt", "w" )
#writeLines(dataJson, c )
#close( c ) #这里需要主动关闭文件
#for (k in propNames) {
# eachData <- dataSet[c(k)]
# values <- table(unlist(eachData))# 频次汇总
# #print(values)
# print(k)
# total <- 0
# for (m in names(values)) {
# #print(m)
# #print(values[m][1])
# data3 <- subset(dataSet, dataSet[c(k)] == m)
# entropyDv <- calEntropy(data3[, length(data3)])
# #print(entropyDv)
# total = total + entropyDv*values[c(m)][1]
# }
# GainDv <- entropyD - total / sum(values);+
# print(GainDv)
#}
R语言代码包含本人自己编写的R语言ID3算法,最后使用R的rpart包训练了一个决策树。
色泽 根蒂 敲声 纹理 脐部 触感 HaoGua
青绿 蜷缩 浊响 清晰 凹陷 硬滑 是
乌黑 蜷缩 沉闷 清晰 凹陷 硬滑 是
乌黑 蜷缩 浊响 清晰 凹陷 硬滑 是
青绿 蜷缩 沉闷 清晰 凹陷 硬滑 是
浅白 蜷缩 浊响 清晰 凹陷 硬滑 是
青绿 稍蜷 浊响 清晰 稍凹 软粘 是
乌黑 稍蜷 浊响 稍糊 稍凹 软粘 是
乌黑 稍蜷 浊响 清晰 稍凹 硬滑 是
乌黑 稍蜷 沉闷 稍糊 稍凹 硬滑 否
青绿 硬挺 清脆 清晰 平坦 软粘 否
浅白 硬挺 清脆 模糊 平坦 硬滑 否
浅白 蜷缩 浊响 模糊 平坦 软粘 否
青绿 稍蜷 浊响 稍糊 凹陷 硬滑 否
浅白 稍蜷 沉闷 稍糊 凹陷 硬滑 否
乌黑 稍蜷 浊响 清晰 稍凹 软粘 否
浅白 蜷缩 浊响 模糊 平坦 硬滑 否
青绿 蜷缩 沉闷 稍糊 稍凹 硬滑 否