目录
一.创建数据集
乳腺癌数据集breast-cancer.txt含有10个属性(包括决策属性)共286条样本。接下来我将一半样本作训练集,一半样本作测试集。
来源(UCI Machine Learning Repository: Breast Cancer Data Set)
属性信息
1. Class: no-recurrence-events, recurrence-events
2. age: 10-19, 20-29, 30-39, 40-49, 50-59, 60-69, 70-79, 80-89, 90-99.
3. menopause: lt40, ge40, premeno.
4. tumor-size: 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59.
5. inv-nodes: 0-2, 3-5, 6-8, 9-11, 12-14, 15-17, 18-20, 21-23, 24-26, 27-29, 30-32, 33-35, 36-39.
6. node-caps: yes, no.
7. deg-malig: 1, 2, 3.
8. breast: left, right.
9. breast-quad: left-up, left-low, right-up, right-low, central.
10. irradiat: yes, no.
部分样本展示
no-recurrence-events,40-49,premeno,20-24,6-8,no,2,right,left_low,yes
no-recurrence-events,50-59,ge40,25-29,0-2,no,1,left,right_low,no
no-recurrence-events,60-69,ge40,15-19,0-2,no,2,left,left_up,yes
no-recurrence-events,40-49,premeno,10-14,0-2,no,2,right,left_up,no
no-recurrence-events,50-59,ge40,20-24,0-2,yes,2,right,left_up,no
no-recurrence-events,40-49,premeno,15-19,12-14,no,3,right,right_low,yes
no-recurrence-events,40-49,premeno,25-29,0-2,no,2,left,left_up,yes
no-recurrence-events,50-59,ge40,30-34,6-8,yes,2,left,left_low,no
no-recurrence-events,30-39,premeno,10-14,0-2,no,2,left,right_low,no
no-recurrence-events,50-59,premeno,50-54,0-2,yes,2,right,left_up,yes
no-recurrence-events,50-59,ge40,35-39,0-2,no,2,left,left_up,no
no-recurrence-events,50-59,premeno,10-14,3-5,no,1,right,left_up,no
no-recurrence-events,40-49,premeno,10-14,0-2,no,2,left,left_low,yes
no-recurrence-events,50-59,ge40,15-19,0-2,yes,2,left,central,yes
no-recurrence-events,50-59,premeno,25-29,0-2,no,1,left,left_low,no
no-recurrence-events,60-69,ge40,25-29,0-2,no,3,right,left_low,no
recurrence-events,50-59,premeno,15-19,0-2,no,2,left,left_low,no
recurrence-events,40-49,premeno,40-44,0-2,no,1,left,left_low,no
recurrence-events,50-59,ge40,35-39,0-2,no,2,left,left_low,no
recurrence-events,50-59,premeno,25-29,0-2,no,2,left,right_up,no
recurrence-events,30-39,premeno,0-4,0-2,no,2,right,central,no
recurrence-events,50-59,ge40,30-34,0-2,no,3,left,?,no
recurrence-events,50-59,premeno,25-29,0-2,no,2,left,right_up,no
recurrence-events,50-59,premeno,30-34,0-2,no,3,left,right_up,no
recurrence-events,40-49,premeno,35-39,0-2,no,1,right,left_up,no
recurrence-events,40-49,premeno,20-24,0-2,no,2,left,left_low,no
recurrence-events,50-59,ge40,20-24,0-2,no,2,right,central,no
recurrence-events,40-49,premeno,30-34,0-2,no,3,right,right_up,no
recurrence-events,50-59,premeno,25-29,0-2,no,1,right,left_up,no
recurrence-events,60-69,ge40,40-44,0-2,no,2,right,left_low,no
二.构造决策树(诊断是否复发乳腺癌)
什么是决策树?
决策树是表示基于特征对实例进行分类的树形结构。从给定的训练数据集中,依据特征选择的准则,递归的选择最优划分特征,并根据此特征将训练数据进行分割,使得各子数据集有一个最好的分类的过程。
决策树算法3要素:
(1)特征选择
(2)决策树生成
(3)决策树剪枝
关于决策树生成:
决策树的生成过程就是使用满足划分准则的特征不断的将数据集划分为纯度更高,不确定性更小的子集的过程。
如何建造决策树?
1.信息增益生成决策树 (ID3算法)
信息熵
信息增益(ID3算法)
定义: 以某特征划分数据集前后的熵的差值。
在熵的理解那部分提到了,熵可以表示样本集合的不确定性,熵越大,样本的不确定性就越大。因此可以使用划分前后集合熵的差值来衡量使用当前特征对于样本集合D划分效果的好坏。
分前样本集合D的熵是一定的 ,entroy(前),使用某个特征A 划分数据集D,计算划分后的数据子集的熵 entroy(后)。
信息增益 = entroy(前) - entroy(后)
做法:计算使用所有特征划分数据集D ,得到多个特征划分数据集D 的信息增益,从这些信息增益中选择最大的,因而当前结点的划分特征便是使信息增益最大的划分所使用的特征。
代码实现
计算数据属性的信息增益,然后划分数据属性的优先级:
from math import log
import operator
import matplotlib.pyplot as plt
def calcShannonEnt(dataSet): # 计算香农熵
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key]) / numEntries
shannonEnt -= prob * log(prob, 2)
return shannonEnt
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
def chooseBestFeatureToSplit(dataSet): # 选择信息增益最大的特征
numFeatures = len(dataSet[0]) - 1
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
newEntropy = 0.0
for value in uniqueVals:
subdataSet = splitdataSet(dataSet, i, value)
prob = len(subdataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subdataSet)
infoGain = baseEntropy - newEntropy
if infoGain > bestInfoGain:
bestInfoGain = infoGain
bestFeature = i
return bestFeature
def majorityCnt(classList): # 多数决决定分类
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
根据计算出的数据属性的优先级 ,生成决策树并实现树的可视化。
def createTree(dataSet, labels): # 生成字典形式的树
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]
if len(dataSet[0]) == 1:
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
del (labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitdataSet(dataSet, bestFeat, value), subLabels)
return myTree
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
def getNumLeafs(myTree):
numLeafs = 0
# firstStr = myTree.keys()[0] 2.7的语法,3.6不适用
firstSides = list(myTree.keys())
firstStr = firstSides[0] # 找到输入的第一个元素
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[
key]).__name__ == 'dict': # test to see if the nodes are dictonaires, if not they are leaf nodes
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree):
maxDepth = 1
firstSides = list(myTree.keys())
firstStr = firstSides[0] # 找到输入的第一个元素
# firstStr = myTree.keys()[0] #注意这里和机器学习实战中代码不同,这里使用的是Python3,而在Python2中可以写成这种形式
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]) == dict:
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0]) / 2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1]) / 2.0 + cntrPt[1]
createPlot.ax1.text(xMid, yMid, txtString, va="center", ha="center", rotation=30)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstSides = list(myTree.keys())
firstStr = firstSides[0] # 找到输入的第一个元素
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs)) / 2.0 / plotTree.totalW, plotTree.yOff)
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0 / plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
plotTree.xOff = plotTree.xOff + 1.0 / plotTree.totalW
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
plotTree.yOff = plotTree.yOff + 1.0 / plotTree.totalD
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) # no ticks
# createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5 / plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
def classify(inputTree, featLabels, testVec): # 决策树执行分类
firstStr = list(inputTree.keys())[0]
secondDict = inputTree[firstStr]
# print(secondDict)
featIndex = featLabels.index(firstStr)
try:
for key in secondDict.keys():
if testVec[featIndex] == key: # 查找测试样本中是否有标签与决策树中的相同
if type(secondDict[key]).__name__ == "dict":
classLabels = classify(secondDict[key], featLabels, testVec)
else:
classLabels = secondDict[key]
return classLabels
except:
return secondDict[key]
取偶数样本数据构造训练集
def file_train(filename): # 取数据集偶数作为训练集
fr = open(filename)
lines = fr.readlines()
# print(lines)
res = []
i = 0
for line in lines:
line = line.strip()
temp = line.split(",")
if i % 2 == 0: # 取总数据集里的偶数
res.append(temp)
i += 1
labels = ["Class", "age", "menopause", "tumor-size", "inv-nodes", "node-caps", "deg-malig", "breast", "breast-quad", "irradiat"]
return res, labels
取奇数样本数据构造测试集
def file_test(filename): # 取数据集的奇数作为测试集
fr = open(filename)
lines = fr.readlines()
# print(lines)
res = []
i = 0
for line in lines:
line = line.strip()
temp = line.split(",")
if i % 2 == 1: # 取总数据集里的奇数
res.append(temp)
i += 1
labels = ["Class", "age", "menopause", "tumor-size", "inv-nodes", "node-caps", "deg-malig", "breast", "breast-quad", "irradiat"]
return res, labels
def getAnswer(filename): # 获得真实的答案
fr = open(filename)
lines = fr.readlines()
i = 0
storage = []
for it in lines:
it = it.strip()
temp = it.split(",")
if i % 2 == 1:
storage.append(temp[-1])
i += 1
return storage
开始训练模型,查看模型的正确率
def test():
# 训练模型
dataSet, labels = file_train("breast-cancer.txt")
labelsBackup = labels[:]
tree = createTree(dataSet, labels)
createPlot(tree)
dataSet_test, labels_test = file_test("breast-cancer.txt")
labels_testBackup = labels_test
j = 0
resTest = []
for item in dataSet_test:
testAns = classify(tree, labels_testBackup, item)
print("第%d次测试" % j, testAns)
resTest.append(testAns)
j += 1
aList = getAnswer("breast-cancer.txt")
count = 0
for x in range(len(aList)):
if resTest[x] == aList[x]:
count += 1
print("训练出的模型的正确率:", count / len(aList))
if __name__ == '__main__':
test()
代码运行结果
可视化决策树图
2.信息增益率决策树(C4.5)
信息增益比 = 惩罚参数 * 信息增益
注意:其中的H A ( D ) ,对于样本集合D,将当前特征A作为随机变量(取值是特征A 的各个特征值),求得的经验熵。
(之前是把集合类别作为随机变量,现在把某个特征作为随机变量,按照此特征的特征取值对集合D进行划分,计算熵H A ( D ))
信息增益比本质: 是在信息增益的基础之上乘上一个惩罚参数。特征个数较多时,惩罚参数较小;特征个数较少时,惩罚参数较大。
惩罚参数:数据集D以特征A作为随机变量的熵的倒数,即:将特征A取值相同的样本划分到同一个子集中(之前所说数据集的熵是依据类别进行划分的)
代码实现(C4.5与ID3代码差异不大,修改chooseBestFeatureToSplit(dataSet)即可)
#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):#给定数据集、划分数据集的特征 、特征所对应的值
retDataSet = [] #创建一个备份数据集,避免原始数据被修改
for featVec in dataSet: #遍历数据集
if featVec[axis] == value: #该特征维度下和value值相等的样本划分到一起,并将该特征去除掉维度去掉
#将axis维度两边的数据进行拼接,就将该特征维度给去除掉
reducedFeatVec = featVec[:axis] #取得[0,axis)的一个列表
reducedFeatVec.extend(featVec[axis+1:]) #取得[axis+1,结束]的一个列表
retDataSet.append(reducedFeatVec)
return retDataSet
#选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #特征数量,numfeature为特征的维度,因为最后一列为标签,所以需要减去1
baseEntropy = calcShannonEnt(dataSet) #用来记录最小信息熵,初始值为原始数据集对应的信息熵
bestinfoGainratio = 0.0 #信息增益率初始化为0,
bestFeature = -1 #最优的划分特征初始化为-1
for i in range(numFeatures): #遍历所有的特征
featList = [example[i] for example in dataSet] #创建list用来存每个样本在第i维度的特征值
uniqueVals = set(featList) #获取该特征下的所有不同的值,即根据该特征可以划分为几类,可以去除重复值
newEntropy = 0.0 #初始化熵为0
IV = 0.0
for value in uniqueVals: #遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) #依据这个值,将样本划分为几个子集,有几个value,就有几个子集
prob = len(subDataSet)/float(len(dataSet)) #计算p值
newEntropy += prob * calcShannonEnt(subDataSet) #计算每个子集对应的信息熵,并全部相加,得到划分后数据的信息熵
IV -= prob * log(prob,2)
infoGain = baseEntropy - newEntropy #将原数据的信息熵-划分后数据的信息熵,得到信息增益
if IV == 0.0: #除以0处理
infoGainratio = 0.0
else:
infoGainratio = float(infoGain) / float(IV)
if (infoGainratio > bestinfoGainratio): #如果这个信息增益率比当前记录的最佳信息增益率还大,就将该增益和划分依据的特征记录下来
bestinfoGainratio = infoGainratio #更新信息增益率,找到最大的信息增益率
bestFeature = i #记录信息增益率最大的索引值
return bestFeature #returns an integer
代码运行结果
可视化决策树图
3.基尼指数(CART算法 — 分类树)
定义:基尼指数(基尼不纯度),表示在样本集合中一个随机选中的样本被分错的概率。
注意: 基尼指数越小表示集合中被选中的样本被分错的概率越小,也就是说集合的纯度越高,反之,集合越不纯。
即 基尼指数(基尼不纯度)= 样本被选中的概率 * 样本被分错的概率
书中公式:
说明:
1. p k 表示选中的样本属于k kk类别的概率,则这个样本被分错的概率是( 1 − p k ) (1 - p_k)(1−pk)
2. 样本集合中有K KK个类别,一个随机选中的样本可以属于这k kk个类别中的任意一个,因而对类别就加和
3. 当为二分类是,G i n i ( p ) = 2 p ( 1 − p )
样本集合D的 Gini指数 :假设集合中有K个类别,则:
基于特征A划分样本集合D之后的基尼指数:
需要说明的是CART是个二叉树,也就是当使用某个特征划分样本集合只有两个集合:1. 等于给定的特征值的样本集合D1;2 不等于给定的特征值的样本集合D2。
实际上是对拥有多个取值的特征的二值处理。
代码实现(不用计算香农熵,改用基尼指数来划分属性,同时修改 chooseBestFeatureToSplit(dataSet)即可)
def calcProbabilityEnt(dataSet):
numEntries = len(dataSet) # 数据条数
feaCounts = 0
fea1 = dataSet[0][len(dataSet[0]) - 1]
for feaVec in dataSet:
if feaVec[-1] == fea1:
feaCounts += 1
probabilityEnt = float(feaCounts) / numEntries
return probabilityEnt
# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数量,numfeature为特征的维度,因为最后一列为标签,所以需要减去1
if numFeatures == 1: # 当只有一个特征时
return 0
bestGini = 1 # 最佳基尼指数
bestFeature = -1 # 最优的划分特征初始化为-1
for i in range(numFeatures): # 遍历所有的特征
featList = [example[i] for example in dataSet] # 创建list用来存每个样本在第i维度的特征值
feaGini = 0 # 定义特征的值的基尼系数
uniqueVals = set(featList) # 获取该特征下的所有不同的值,即根据该特征可以划分为几类,可以去除重复值
for value in uniqueVals: # 遍历该特征维度下对应的所有特征值
subDataSet = splitDataSet(dataSet, i, value) # 依据这个值,将样本划分为几个子集,有几个value,就有几个子集
prob = len(subDataSet) / float(len(dataSet)) # 计算p值
probabilityEnt = calcProbabilityEnt(subDataSet)
feaGini += prob * (2 * probabilityEnt * (1 - probabilityEnt))
if (feaGini < bestGini):
bestGini = feaGini
bestFeature = i # 记录基尼指数最小的索引值
return bestFeature # returns an integer
基尼指数越大,集合的不确定性越高,这点和信息熵类似。故此,每次为决策树构建下一节点时总是取基尼指数小的属性。
代码运行结果
可视化决策树图
三.总结
信息增益的理解:
对于待划分的数据集D,其 entroy(前)是一定的,但是划分之后的熵 entroy(后)是不定的,entroy(后)越小说明使用此特征划分得到的子集的不确定性越小(也就是纯度越高),因此 entroy(前) - entroy(后)差异越大,说明使用当前特征划分数据集D 的话,其纯度上升的更快。而我们在构建最优的决策树的时候总希望能更快速到达纯度更高的集合,这一点可以参考优化算法中的梯度下降算法,每一步沿着负梯度方法最小化损失函数的原因就是负梯度方向是函数值减小最快的方向。同理:在决策树构建的过程中我们总是希望集合往最快到达纯度更高的子集合方向发展,因此我们总是选择使得信息增益最大的特征来划分当前数据集D。
缺点:信息增益偏向取值较多的特征
原因:当特征的取值较多时,根据此特征划分更容易得到纯度更高的子集,因此划分之后的熵更低,由于划分前的熵是一定的,因此信息增益更大,因此信息增益比较偏向取值较多的特征。
信息增益率:
缺点:信息增益比偏向取值较少的特征
原因: 当特征取值较少时H A ( D ) 的值较小,因此其倒数较大,因而信息增益比较大。因而偏向取值较少的特征。
使用信息增益率:基于以上缺点,并不是直接选择信息增益率最大的特征,而是现在候选特征中找出信息增益高于平均水平的特征,然后在这些特征中再选择信息增益率最高的特征。
其中信息增益和信息增益率通常用于离散型的特征划分,ID3和C4.5通常情况下都是多叉树,也就是根据离散特征的取值会将数据分到多个子树中;而CART树为二叉树,使用基尼指数作为划分准则,对于离散型特征和连续行特征都能很好的处理。