一.簡介
決策樹是一種基於樹結構來進行決策的分類算法,我們希望從給定的訓練數據集學得一個模型(即決策樹),用該模型對新樣本分類。決策樹可以非常直觀展現分類的過程和結果,一旦模型構建成功,對新樣本的分類效率也相當高。
最經典的決策樹算法有ID3、C4.5、CART,其中ID3算法是最早被提出的,它可以處理離散屬性樣本的分類,C4.5和CART算法則可以處理更加復雜的分類問題,本文重點介紹ID3算法
二.基於買西瓜這個場景建立模型
1.引入例子
舉個例子:夏天買西瓜時,我一般先選瓜皮有光澤的(新鮮),再拍一拍選聲音清脆的(成熟),這樣挑出來的好瓜的可能就比較大了。那么我挑西瓜的決策樹是這樣的

那么我們是如何挑選最優划分的屬性的呢
通過學習我們可以得知
2.利用信息增益選擇最優划分屬性
樣本有多個屬性,該先選哪個樣本來划分數據集呢?
原則是隨着划分不斷進行,我們希望決策樹的分支節點所包含的樣本盡可能屬於同一分類,即“純度”越來越高。先來學習一下“信息熵”和“信息增益”。
下面引入兩個概念
信息熵(information entropy)
樣本集合D中第k類樣本所占的比例(k=1,2,...,|Y|),|Y|為樣本分類的個數,則D的信息熵為:

Ent(D)的值越小,則D的純度越高。直觀理解一下:假設樣本集合有2個分類,每類樣本的比例為1/2,Ent(D)=1;只有一個分類,Ent(D)= 0,顯然后者比前者的純度高。
在西瓜樣本集中,共有17個樣本,其中正樣本8個,負樣本9個,樣本集的信息熵為:

信息增益(information gain)
使用屬性a對樣本集D進行划分所獲得的“信息增益”的計算方法是,用樣本集的總信息熵減去屬性a的每個分支的信息熵與權重(該分支的樣本數除以總樣本數)的乘積,通常,信息增益越大,意味着用屬性a進行划分所獲得的“純度提升”越大。因此,優先選擇信息增益最大的屬性來划分。設屬性a有V個可能的取值,則屬性a的信息增益為:

西瓜樣本集中,以屬性“色澤”為例,它有3個取值{青綠、烏黑、淺白},對應的子集(色澤=青綠)中有6個樣本,其中正負樣本各3個,(色澤=烏黑)中有6個樣本,正樣本4個,負樣本2個,(色澤=淺白)中有5個樣本,正樣本1個,負樣本4個。





就像這樣我們計算另外幾個屬性的信息增益,選擇信息增益最大的屬性作為根節點來進行划分,然后再對每個分支做進一步划分。
現在我們來實現下這第一步
3.代碼實現計算信息增益選擇划分。
#導入數據以及相關包
import pandas as pd
import numpy as np
from collections import Counter
from math import log2
fr = open(r'D:\baidu\watermalon.txt',encoding="utf-8")
listWm = [inst.strip().split(' ') for inst in fr.readlines()]
print(listWm)
看到數據如下
根據之前所學的算下信息熵
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 = shannonEnt - prob * log(prob, 2)
return shannonEnt
print(calcShannonEnt(listWm))

先定義一個划分數據集的函數
# 划分數據集,axis:按第幾個屬性划分,value:要返回的子集對應的屬性值
def splitDataSet(dataSet, axis, value):
retDataSet = []
featVec = []
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
print(chooseBestFeatureToSplit(listWm))
輸出為三,即最先應該選擇序號為三的那一列屬性作為分割
4.遞歸構建決策樹
通常一棵決策樹包含一個根節點、若干個分支節點和若干個葉子節點,葉子節點對應決策結果(如好瓜或壞瓜),根節點和分支節點對應一個屬性測試(如色澤=?),每個結點包含的樣本集合根據屬性測試的結果划分到子節點中。
在上一節中,我們對整個訓練集選擇的最優划分屬性就是根節點,第一次划分后,數據被向下傳遞到樹分支的下一個節點,再這個節點我們可以再次划分數據,構建決策樹是一個遞歸的過程,而遞歸結束的條件是:所有屬性都被遍歷完,或者每個分支下的所有樣本都屬於同一類。
還有一種情況就是當划分到一個節點,該節點對應的屬性取值都相同,而樣本的類別卻不同,這時就把當前節點標記為葉節點,並將其類別設為所含樣本較多的類別。例如:當划分到某一分支時,節點中有3個樣本,其最優划分屬性為色澤,而色澤的取值只有一個“淺白”,3個樣本中有2個好瓜,這時我們就把這個節點標記為葉節點“好瓜”。
import operator # 此行加在文件頂部
# 通過排序返回出現次數最多的類別
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] # 類別向量
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]
uniqueValue = set(featValues) # 該屬性所有可能取值,也就是節點的分支
for value in uniqueValue: # 對每個分支,遞歸構建樹
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(
splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree
最后使用數據測試一下算法,。因為生成的樹是中文表示的,因此使用json.dumps()方法來打印結果。如果是不含中文,直接print即可。
# -*- coding: cp936 -*-
import trees
import json
fr = open(r'C:\Python27\py\DecisionTree\watermalon.txt')
listWm = [inst.strip().split('\t') for inst in fr.readlines()]
labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感']
Trees = trees.createTree(listWm, labels)
print json.dumps(Trees, encoding="cp936", ensure_ascii=False)
結果如下
{"紋理": {"模糊": "否", "清晰": {"根蒂": {"稍蜷": {"色澤": {"烏黑": {"觸感": {"軟粘": "否", "硬滑": "是"}}, "青綠": "是"}}, "蜷縮": "是", "硬挺": "否"}}, "稍糊": {"觸感": {"軟粘": "是", "硬滑": "否"}}}}
5.使用Matplotlib繪制決策樹
字典形式的決策樹仍然不易理解,下面我們利用Matplotlib庫的annotate(注釋)模塊繪制決策樹,就可以很直觀的看出決策樹的結構
# -*- coding: cp936 -*-
import matplotlib.pyplot as plt
# 設置決策節點和葉節點的邊框形狀、邊距和透明度,以及箭頭的形狀
decisionNode = dict(boxstyle="square,pad=0.5", fc="0.9")
leafNode = dict(boxstyle="round4, pad=0.5", fc="0.9")
arrow_args = dict(arrowstyle="<-", connectionstyle="arc3", shrinkA=0,
shrinkB=16)
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(unicode(nodeTxt, 'cp936'), xy=parentPt,
xycoords='axes fraction',
xytext=centerPt, textcoords='axes fraction',
va="top", ha="center", bbox=nodeType,
arrowprops=arrow_args)
def getNumLeafs(myTree):
numLeafs = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree):
maxDepth = 0
firstStr = myTree.keys()[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__ == 'dict':
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
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, unicode(txtString, 'cp936'))
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = myTree.keys()[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)
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()
# -*- coding: cp936 -*-
import trees
import treePlotter
import json
fr = open(r'C:\Python27\py\DecisionTree\watermalon.txt')
listWm = [inst.strip().split('\t') for inst in fr.readlines()]
labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感']
Trees = trees.createTree(listWm, labels)
print json.dumps(Trees, encoding="cp936", ensure_ascii=False)
treePlotter.createPlot(Trees)

三、ID3、C4.5和CART的算法代碼實現(使用Sklearn)
1.導入相關庫
#導入相關庫
import pandas as pd
import graphviz
from sklearn.model_selection import train_test_split
from sklearn import tree
f = open('watermelon2.csv','r')
data = pd.read_csv(f)
x = data[["色澤","根蒂","敲聲","紋理","臍部","觸感"]].copy()
y = data['好瓜'].copy()
print(data)

特征函數數值化,決策樹學習,最后將得到的決策樹繪出
#將特征值數值化
x = x.copy()
for i in ["色澤","根蒂","敲聲","紋理","臍部","觸感"]:
for j in range(len(x)):
if(x[i][j] == "青綠" or x[i][j] == "蜷縮" or data[i][j] == "濁響" \
or x[i][j] == "清晰" or x[i][j] == "凹陷" or x[i][j] == "硬滑"):
x[i][j] = 1
elif(x[i][j] == "烏黑" or x[i][j] == "稍蜷" or data[i][j] == "沉悶" \
or x[i][j] == "稍糊" or x[i][j] == "稍凹" or x[i][j] == "軟粘"):
x[i][j] = 2
else:
x[i][j] = 3
y = y.copy()
for i in range(len(y)):
if(y[i] == "是"):
y[i] = int(1)
else:
y[i] = int(-1)
#需要將數據x,y轉化好格式,數據框dataframe,否則格式報錯
x = pd.DataFrame(x).astype(int)
y = pd.DataFrame(y).astype(int)
print(x)
print(y)
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.2)
print(x_train)
#決策樹學習
clf = tree.DecisionTreeClassifier(criterion="entropy") #實例化
clf = clf.fit(x_train, y_train)
score = clf.score(x_test, y_test)
print(score)
# 加上Graphviz2.38絕對路徑
import os
os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin'
feature_name = ["色澤","根蒂","敲聲","紋理","臍部","觸感"]
dot_data = tree.export_graphviz(clf ,feature_names= feature_name,class_names=["好瓜","壞瓜"],filled=True,rounded=True,out_file =None)
graph = graphviz.Source(dot_data)

四.C4.5算法
1.C4.5算法簡介
C4.5算法是用於生成決策樹的一種經典算法,是ID3算法的一種延伸和優化。C4.5算法對ID3算法主要做了一下幾點改進:
(1)通過信息增益率選擇分裂屬性,克服了ID3算法中通過信息增益傾向於選擇擁有多個屬性值的屬性 作為分裂屬性的不足;
(2)能夠處理離散型和連續型的屬性類型,即將連續型的屬性進行離散化處理;
(3)構造決策樹之后進行剪枝操作;
(4)能夠處理具有缺失屬性值的訓練數據。
2.分裂屬性的選擇
- 信息增益率
分裂屬性選擇的評判標准是決策樹算法之間的根本區別。區別於ID3算法通過信息增益選擇分裂屬性,C4.5算法通過信息增益率選擇分裂屬性。
屬性A的“分裂信息”(split information):

其中,訓練數據集S通過屬性A的屬性值划分為m個子數據集, |Sj|表示第j個子數據集中樣本數量, |S|表示划分之前數據集中樣本總數量。
通過屬性A分裂之后樣本集的信息增益:

通過屬性A分裂之后樣本集的信息增益率:

通過C4.5算法構造決策樹時,信息增益率最大的屬性即為當前節點的分裂屬性,隨着遞歸計算,被計算的屬性的信息增益率會變得越來越小,到后期則選擇相對比較大的信息增益率的屬性作為分裂屬性。
3.C4.5算法優缺點分析
優點:
(1)通過信息增益率選擇分裂屬性,克服了ID3算法中通過信息增益傾向於選擇擁有多個屬性值的屬性作為分裂 屬性的不足;
(2)能夠處理離散型和連續型的屬性類型,即將連續型的屬性進行離散化處理;
(3)構造決策樹之后進行剪枝操作;
(4)能夠處理具有缺失屬性值的訓練數據。
缺點:
(1)算法的計算效率較低,特別是針對含有連續屬性值的訓練樣本時表現的尤為突出。
(2)算法在選擇分裂屬性時沒有考慮到條件屬性間的相關性,只計算數據集中每一個條件屬性與決策屬性之間的期望信息,有可能影響到屬性選擇的正確性。
4.python實現
## 信息增益率
def chooseBestFeatureToSplit_4(dataSet, labels):
"""
選擇最好的數據集划分特征,根據信息增益值來計算
:param dataSet:
:return:
"""
# 得到數據的特征值總數
numFeatures = len(dataSet[0]) - 1
# 計算出基礎信息熵
baseEntropy = calcShannonEnt(dataSet)
# 基礎信息增益為0.0
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=dataSet, axis=i, value=value)
# 計算出權重
prob = len(subDataSet) / float(len(dataSet))
# 計算出當前特征值的熵
newEntropy += prob * calcShannonEnt(subDataSet)
# 計算出“信息增益”
infoGain = baseEntropy - newEntropy
infoGain = infoGain/newEntropy
#print('當前特征值為:' + labels[i] + ',對應的信息增益值為:' + str(infoGain)+"i等於"+str(i))
#如果當前的信息增益比原來的大
if infoGain > bestInfoGain:
# 最好的信息增益
bestInfoGain = infoGain
# 新的最好的用來划分的特征值
bestFeature = i
#print('信息增益最大的特征為:' + labels[bestFeature])
return bestFeature
五.CART算法
1.CART算法的認識
Classification And Regression Tree,即分類回歸樹算法,簡稱CART算法,它是決策樹的一種實現,通常決策樹主要有三種實現,分別是ID3算法,CART算法和C4.5算法。
CART算法是一種二分遞歸分割技術,把當前樣本划分為兩個子樣本,使得生成的每個非葉子結點都有兩個分支,因此CART算法生成的決策樹是結構簡潔的二叉樹。由於CART算法構成的是一個二叉樹,它在每一步的決策時只能是“是”或者“否”,即使一個feature有多個取值,也是把數據分為兩部分。在CART算法中主要分為兩個步驟
(1)將樣本遞歸划分進行建樹過程
(2)用驗證數據進行剪枝
2.CART算法的原理
上面說到了CART算法分為兩個過程,其中第一個過程進行遞歸建立二叉樹,那么它是如何進行划分的 ?
設x1,x2…xn代表單個樣本的n個屬性,y表示所屬類別。CART算法通過遞歸的方式將n維的空間划分為不重疊的矩形。划分步驟大致如下
(1)選一個自變量xi,再選取xi的一個值vi,vi把n維空間划分為兩部分,一部分的所有點都滿足xi<=vi,另一部分的所有點都滿足xi>vi,對非連續變量來說屬性值的取值只有兩個,即等於該值或不等於該值。
(2)遞歸處理,將上面得到的兩部分按步驟(1)重新選取一個屬性繼續划分,直到把整個n維空間都划分完。在划分時候有一個問題,它是按照什么標准來划分的 ? 對於一個變量屬性來說,它的划分點是一對連續變量屬性值的中點。假設m個樣本的集合一個屬性有個m連續的值,那么則會有m-1個分裂點,每個分裂點為相鄰兩個連續值的均值。每個屬性的划分按照能減少的雜質的量來進行排序,而雜質的減少量定義為划分前的雜質減去划分后的每個節點的雜質量划分所占比率之和。而雜質度量方法常用Gini指標,假設一個樣本共有C類,那么一個節點A的Gini不純度可定義為
3.python借助sklearn實現
只需要將DecisionTreeClassifier函數的參數criterion的值改為gini:

六、總結
決策樹其實是通過物體的特征把它進行分類,不同的人有不同的分法,每種方法的結果可能不同,但都有它的道理,但我們要做的就是寫出最能滿足要求的那種。
學習參考:
https://blog.csdn.net/weixin_46129506/article/details/120987574
