1、C4.5決策樹概述html
C4.5決策樹是ID3決策樹的改進算法,它解決了ID3決策樹沒法處理連續型數據的問題以及ID3決策樹在使用信息增益劃分數據集的時候傾向於選擇屬性分支更多的屬性的問題。它的大部分流程和ID3決策樹是相同的或者類似的,能夠參考個人上一篇博客:http://www.javashuo.com/article/p-rqvlfpnq-dq.htmlgit
C4.5決策樹和ID3決策樹相同,也能夠產生一個離線的「決策樹」,並且對於連續屬性組成的C4.5決策樹數據集,C4.5算法能夠避開「測試集中的取值不存在於訓練集」這種狀況,因此不須要像ID3決策樹那樣,預先將測試集中不存在於訓練集中的屬性的取值,「手動地」加入到決策樹中的問題。可是對於同時有離散屬性和連續屬性的數據集,離散屬性部分仍舊是須要進行將存在於測試集,不存在於訓練集中的取值(注意,是取值不是向量!)給刪除或者加入到樹的構造過程當中。github
C4.5不是一個簡單的決策樹構造算法,它是一組算法,包括C4.5的構造和C4.5剪枝的問題,剪枝問題在ID3決策樹、C4.5決策樹和CART樹都實現完的時候再統一實現。算法
流行的C4.5決策樹構造算法是沒有進行修正的過程的,這會致使一個很嚴重的問題:C4.5決策樹在構造樹的時候傾向於選擇連續屬性做爲最佳分割屬性。因此C4.5須要一個修正的過程,在進行連續屬性的信息增益率的計算的時候,要進行修正。api
C4.5使用的是信息增益率來劃分「最好的」屬性,這個「信息增益率」和ID3決策樹使用的「信息增益」有什麼區別呢?app
信息增益(Information Gain):對於某一種劃分的信息增益能夠表示爲「指望信息 - 該種劃分的香農熵」。它的公式能夠表示爲:IG(T)=H(C)-H(C|T)。其中C表明的是分類或者聚類C,T表明的是則是當前選擇進行劃分的特徵。這條公式表示了:選擇特徵T進行劃分,則其信息增益爲數據集的指望信息減去選擇該特徵T進行劃分後的指望信息。這裏要明確的是:指望信息就是香農熵。熵是信息的指望,因此熵的表示應該爲全部信息出現的機率和其指望的總和,即:機器學習
當咱們把這條熵公式轉換爲一個函數:calculateEntropy(dataSet,feature = NULL)的時候,上面這個計算過程能夠變成如下的僞代碼:分佈式
1 while dataSet != NULL: 2 feat = -1 3 for i in range(featureNum): 4 IG = calculateEntropy(dataSet) - calculateEntropy(dataSet,feature[i]) 5 if IG > IGMAX: 6 IGMAX = IG 7 feat = feature[i] 8 #IGMAX此時保存的即爲最大的信息增益,feat保存的即爲最大的信息增益所對應的特徵 9 dataSet = dataSet - feature[i]#這裏不是減法,而是在數據集中去除該列
由上面的僞代碼,也能夠理解到「信息增益最大的時候,熵減最多」。這裏的數學理解就是:信息增益的公式能夠看做A - B,其中B是改變的,A是一個常量,那麼B越小A - B的值就會越大,B越小則表明熵越小,當B達到最小的時候,A - B最大,此時熵最小,也便是熵減最多。函數
信息增益率/信息增益比(GainRatio):在選擇決策樹中某個結點上的分支屬性時,假設該結點上的數據集爲DataSet,其中包含Feature個描述屬性,樣本總數爲len(DataSet)或者DataSet.shape[0],設描述屬性feature(不一樣於Feature,Feature是屬性的個數,取值爲DataSet.shape[1],feature是某一個具體的屬性)總共有M個不一樣的取值,則利用描述屬性feature能夠將數據集DataSet劃分爲M個子集,設這些子集爲{DataSet1,DataSet2,…,DataSetN,…,DataSetM},而且這些子集中樣本在同一個子集中對應的feature屬性的取值應該是相同的(若feature屬性爲離散屬性,則取值爲某一個離散值,若爲連續屬性,則取值爲<=num,>num之一),用{len1,len2,…,lenN,…,lenM}表示每一個對應的子集的樣本的數量,則用描述屬性feature來劃分給定的數據集DataSet所獲得的信息增益率/信息增益比爲:學習
Gain(feature)的算法和上面ID3決策樹計算信息增益的算法是同樣的,事實上,求GainRatio的過程就是在上述的計算信息增益的過程當中加上一個對其「稀釋」的做用,使得取值多的feature不會佔據主導地位,因爲在計算信息增益的時候,是一個累加的公式,log(p)必定是一個負值,這樣就致使Gain(feature)會一直地往上增大,即便增大幅度很小,而除以劃分屬性的熵公式(以下)則能夠儘可能的把這種微小的累加所帶來的影響降到最低。
C4.5處理連續屬性的數據的過程:假設當前正在處理的屬性feature爲一個連續型屬性,當前正在劃分的數據集的樣本數量爲total,則:
①將該節點上的全部數據樣本按照連續型描述屬性的具體取值,由小到大進行排序,獲得屬性值的取值序列{A1,A2,…,AN,…,Atotal};
②在得到的取值序列{A1,A2,…,AN,…,Atotal}中生成total - 1個分割點,其中,第n(1<= n <= total - 1)個分割點的取值爲(An + An+1 )/ 2,得到的這個分割點,能夠將數據集DataSet劃分爲兩個子集,即描述屬性feature的取值在[ A1,(An + An+1 )/ 2 ],((An + An+1 )/ 2 ,Atotal]這兩個區間的數據樣本。
③從total - 1個分割點中選擇當前描述屬性feature的最佳分割點,這個分割點能夠獲得最大的信息增益。(注意,信息增益,而非信息增益率,這是對C4.5的修正)
④計算當前描述屬性feature的信息增益率,若是它的信息增益率是全部的描述屬性中最大的,則選擇其做爲當前結點的劃分描述屬性。
下面舉例說明C4.5算法對於連續型描述屬性的處理方法:假設一個連續型屬性的取值序列爲{32,25,46,56,60,52,42,36,23,51,38,43,41,65}。
①對連續序列進行升序排序,產生一個新的有序連續序列:{23,25,32,36,38,41,42,43,46,51,52,56,60,65};
②對新的有序連續序列產生分割點,共產生13個分割點:{24,23.5,34,37,39.5,41.5,42.5,44.5,48.5,51.5,54,58,62.5};
③選擇最佳分割點。對於第一個分割點,計算取值在[23,24]的數量和在(24,65]中的數量,而後計算其信息增益IG1,然後對於第二個分割點,計算取值在[23,25]的數量和在(25,65]的數量,計算其信息增益IG2,以此類推,最後選擇最大的信息增益IGMAX,此時對應的分割點爲最大分割點。
④選擇最大分割點後,對於這個分割點,計算信息增益率GainRatio,則這個GainRatio則表明了這個描述屬性feature的GainRatio。
C4.5修正:C4.5的修正在上面的處理連續屬性的數據的過程③中體現了出來,它選擇的並非能得到最大的信息增益率的分割點,而是選擇能得到最大的信息增益的分割點。這樣作的緣由是,當咱們選擇信息增益率來做爲C4.5的連續型屬性的數據集劃分的依據時,它會傾向於選擇連續型屬性來做爲劃分的描述屬性具體算法流程以下:
①將該節點上的全部數據樣本按照連續型描述屬性的具體取值,由小到大進行排序,獲得屬性值的取值序列{A1,A2,…,AN,…,Atotal};
②在得到的取值序列{A1,A2,…,AN,…,Atotal}中生成total - 1個分割點,其中,第n(1<= n <= total - 1)個分割點的取值爲(An + An+1 )/ 2,得到的這個分割點,能夠將數據集DataSet劃分爲兩個子集,即描述屬性feature的取值在[ A1,(An + An+1 )/ 2 ],((An + An+1 )/ 2 ,Atotal]這兩個區間的數據樣本,計算這個分割點的信息增益。
③選擇信息增益最大的分割點做爲該描述屬性feature的最佳分割點。
④計算最佳分割點的信息增益率做爲當前的描述屬性的信息增益率,對最佳分割點的信息增益進行修正,減去log2(N-1)/|D|(N是連續特徵的取值個數,D是訓練數據數目)。
2、準備數據集
Python3實現機器學習經典算法的數據集都採用了著名的機器學習倉庫UCI(http://archive.ics.uci.edu/ml/datasets.html),其中分類系列算法採用的是Adult數據集(http://archive.ics.uci.edu/ml/datasets/Adult),測試數據所在網址:http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data,訓練數據所在網址:http://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test。
Adult數據集經過收集14個特徵來判斷一我的的收入是否超過50K,14個特徵及其取值分別是:
age: continuous.
workclass: Private, Self-emp-not-inc, Self-emp-inc, Federal-gov, Local-gov, State-gov, Without-pay, Never-worked.
fnlwgt: continuous.
education: Bachelors, Some-college, 11th, HS-grad, Prof-school, Assoc-acdm, Assoc-voc, 9th, 7th-8th, 12th, Masters, 1st-4th, 10th, Doctorate, 5th-6th, Preschool.
education-num: continuous.
marital-status: Married-civ-spouse, Divorced, Never-married, Separated, Widowed, Married-spouse-absent, Married-AF-spouse.
occupation: Tech-support, Craft-repair, Other-service, Sales, Exec-managerial, Prof-specialty, Handlers-cleaners, Machine-op-inspct, Adm-clerical, Farming-fishing, Transport-moving, Priv-house-serv, Protective-serv, Armed-Forces.
relationship: Wife, Own-child, Husband, Not-in-family, Other-relative, Unmarried.
race: White, Asian-Pac-Islander, Amer-Indian-Eskimo, Other, Black.
sex: Female, Male.
capital-gain: continuous.
capital-loss: continuous.
hours-per-week: continuous.
native-country: United-States, Cambodia, England, Puerto-Rico, Canada, Germany, Outlying-US(Guam-USVI-etc), India, Japan, Greece, South, China, Cuba, Iran, Honduras, Philippines, Italy, Poland, Jamaica, Vietnam, Mexico, Portugal, Ireland, France, Dominican-Republic, Laos, Ecuador, Taiwan, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand-Netherlands.
最終的分類標籤有兩個:>50K, <=50K.
下一步是分析數據:
一、數據預處理:C4.5算法能處理連續屬性和連續屬性,因此這裏不須要數據預處理的過程,整個原生的數據集就是訓練集/測試集。
二、數據清洗:
數據中含有大量的不肯定數據,這些數據在數據集中已經被轉換爲‘?’,可是它仍舊是沒法使用的,數據挖掘對於這類數據進行數據清洗的要求規定,若是是可推算數據,應該推算後填入;或者應該經過數據處理填入一個平滑的值,然而這裏的數據大部分沒有相關性,因此沒法推算出一個合理的平滑值;因此全部的‘?’數據都應該被剔除而不該該繼續使用。爲此咱們要用一段代碼來進行數據的清洗:
1 def cleanOutData(dataSet):#數據清洗 2 for row in dataSet: 3 for column in row: 4 if column == '?' or column=='': 5 dataSet.remove(row
這段代碼只是示例,它有它不能處理的數據集!好比上述這段代碼是沒法處理相鄰兩個向量都存在‘?’的狀況的!修改思路有多種,一種是循環上述代碼N次直到沒有'?'的狀況,這種算法簡單易實現,只是給上述代碼加了一層循環,然而其複雜度爲O(N*len(dataset));另一種實現是每次找到存在'?'的列,回退迭代器一個距離,大體的僞代碼爲:
1 def cleanOutData(dataSet): 2 for i in range(len(dataSet)): 3 if dataSet[i].contain('?'): 4 dataSet.remove(dataSet[i]) ( dataSet.drop(i) ) 5 i-=1
上述代碼的複雜度爲O(n)很是快速,可是這種修改迭代器的方式會引發編譯器的報錯,對於這種報錯能夠選擇修改編譯器使其忽略,可是不建議使用這種回退迭代器的寫法。
三、數據歸一化:
決策樹這樣的概念模型不須要進行數據歸一化,由於它關心的是向量的分佈狀況和向量之間的條件機率而不是變量的值,進行數據歸一化更難以進行劃分數據集,由於Double類型的判等很是難作且不許確。
四、數據集讀入:
綜合上訴的預處理和數據清洗的過程,數據集讀入的過程爲:
1 #讀取數據集 2 def createDateset(filename): 3 with open(filename, 'r')as csvfile: 4 dataset= [line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 5 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 6 cleanoutdata(dataset) #清洗數據 7 del (dataset[-1]) #去除最後一行的空行 8 #precondition(dataset) #預處理數據 9 labels=['age','workclass','fnlwgt','education','education-num', 10 'marital-status','occupation', 11 'relationship','race','sex','capital-gain','capital-loss','hours-per-week', 12 'native-country'] 13 labelType = ['continuous', 'uncontinuous', 'continuous', 14 'uncontinuous', 15 'continuous', 'uncontinuous', 16 'uncontinuous', 'uncontinuous', 'uncontinuous', 17 'uncontinuous', 'continuous', 'continuous', 18 'continuous', 'uncontinuous'] 19 20 return dataset,labels,labelType 21 22 def cleanoutdata(dataset):#數據清洗 23 for row in dataset: 24 for column in row: 25 if column == '?' or column=='': 26 dataset.remove(row) 27 break
對比ID3的讀入過程,少了一個對於連續型屬性的清洗過程,增長了一個labelType的列表來表示當前的屬性是連續型屬性仍是離散型屬性。
3、訓練算法
訓練算法既是構造C4.5決策樹的過程,構造結束的原則爲:若是某個樹分支下的數據所有屬於同一類型,則已經正確的爲該分支如下的全部數據劃分分類,無需進一步對數據集進行分割,若是數據集內的數據不屬於同一類型,則須要繼續劃分數據子集,該數據子集劃分後做爲一個分支繼續進行當前的判斷。
用僞代碼表示以下:
if 數據集中全部的向量屬於同一分類:
return 分類標籤
else:
if 屬性特徵已經使用完:
進行投票決策
return 票數最多的分類標籤
else:
尋找信息增益率最大的數據集劃分方式(找到要分割的屬性特徵T)
根據屬性特徵T建立分支
if 屬性爲連續型屬性:
讀入取值序列並升序排列
選擇信息增益最大的分割點做爲子樹的劃分依據
else:
for 屬性特徵T的每一個取值
成爲當前樹分支的子樹
劃分數據集(將T屬性特徵的列丟棄或屏蔽)
return 分支(新的數據集,遞歸)
根據上面的僞代碼,能夠獲得下面一步一部的訓練算法流程,其中不少的過程和在ID3決策樹中的過程是類似的甚至如出一轍的。
一、尋找信息增益率最大的數據集劃分方式(找到要分割的屬性特徵T):
1 #計算香農熵/指望信息 2 def calculateEntropy(dataSet): 3 ClassifyCount = {}#分類標籤統計字典,用來統計每一個分類標籤的機率 4 for vector in dataSet: 5 clasification = vector[-1] #獲取分類 6 if not clasification in ClassifyCount.keys():#若是分類暫時不在字典中,在字典中添加對應的值對 7 ClassifyCount[clasification] = 0 8 ClassifyCount[clasification] += 1 #計算出現次數 9 shannonEntropy=0.0 10 for key in ClassifyCount: 11 probability = float(ClassifyCount[key]) / len(dataSet) #計算機率 12 shannonEntropy -= probability * log(probability,2) #香農熵的每個子項都是負的 13 return shannonEntropy 14 15 #連續型屬性不須要將訓練集中有,測試集中沒有的值補全,離散性屬性須要 16 # def addFeatureValue(featureListOfValue,feature): 17 # feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc', 18 # 'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'], 19 # [],[],[],[],[]] 20 # for featureValue in feat[feature]: #feat保存的是全部屬性特徵的全部可能的取值,其結構爲feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ] 21 # featureListOfValue.append(featureValue) 22 23 #選擇最好的數據集劃分方式 24 def chooseBestSplitWay(dataSet,labelType): 25 isContinuous = -1 #判斷是不是連續值,是爲1,不是爲0 26 HC = calculateEntropy(dataSet)#計算整個數據集的香農熵(指望信息),即H(C),用來和每一個feature的香農熵進行比較 27 bestfeatureIndex = -1 #最好的劃分方式的索引值,由於0也是索引值,因此應該設置爲負數 28 gainRatioMax=0.0 #信息增益率=(指望信息-熵)/分割得到的信息增益,即爲GR = IG / split = ( HC - HTC )/ split , gainRatioMax爲最好的信息增益率,IG爲各類劃分方式的信息增益 29 30 continuousValue = -1 #設置若是是連續值的屬性返回的最好的劃分方式的最好分割點的值 31 for feature in range(len(dataSet[0]) -1 ): #計算feature的個數,因爲dataset中是包含有類別的,因此要減去類別 32 featureListOfValue=[vector[feature] for vector in dataSet] #對於dataset中每個feature,建立單獨的列表list保存其取值,其中是不重複的 33 addFeatureValue(featureListOfValue,feature) #增長在訓練集中有,測試集中沒有的屬性特徵的取值 34 if labelType[feature] == 'uncontinuous': 35 unique=set(featureListOfValue) 36 HTC=0.0 #保存HTC,即H(T|C) 37 split = 0.0 #保存split(T) 38 for value in unique: 39 subDataSet = splitDataSet(dataSet,feature,value) #劃分數據集 40 probability = len(subDataSet) / float(len(dataSet)) #求得當前類別的機率 41 split -= probability * log(probability,2) #計算split(T) 42 HTC += probability * calculateEntropy(subDataSet) #計算當前類別的香農熵,並和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C) 43 IG=HC-HTC #計算對於該種劃分方式的信息增益 44 if split == 0: 45 split = 1 46 gainRatio = float(IG)/float(split) #計算對於該種劃分方式的信息增益率 47 if gainRatio > gainRatioMax : 48 isContinuous = 0 49 gainRatioMax = gainRatio 50 bestfeatureIndex = feature 51 else: #若是feature是連續型的 52 featureListOfValue = set(featureListOfValue) 53 sortedValue = sorted(featureListOfValue) 54 splitPoint = [] 55 for i in range(len(sortedValue)-1):#n個value,應該有n-1個分割點splitPoint 56 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) 57 58 #C4.5修正,再也不使用信息增益率來選擇最佳分割點 59 # for i in range(len(splitPoint)): #對於n-1個分割點,計算每一個分割點的信息增益率,來選擇最佳分割點 60 # HTC = 0.0 61 # split = 0.0 62 # gainRatio = 0.0 63 # biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 64 # print(i) 65 # probabilityBig = len(biggerDataSet)/len(dataSet) 66 # probabilitySmall = len(smallerDataSet)/len(dataSet) 67 # HTC += probabilityBig * calculateEntropy(biggerDataSet) 68 # HTC += probabilityBig * calculateEntropy(smallerDataSet) 69 # if probabilityBig != 0: 70 # split -= probabilityBig * log(probabilityBig,2) 71 # if probabilitySmall != 0: 72 # split -= probabilitySmall *log(probabilitySmall,2) 73 # IG = HC - HTC 74 # if split == 0: 75 # split = 1; 76 # gainRatio = IG/split 77 # if gainRatio>gainRatioMax: 78 # isContinuous = 1 79 # gainRatioMax = gainRatio 80 # bestfeatureIndex = feature 81 # continuousValue = splitPoint[i] 82 IGMAX = 0.0 83 for i in range(len(splitPoint)): 84 HTC = 0.0 85 split = 0.0 86 87 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 88 probabilityBig = len(biggerDataSet) / len(dataSet) 89 probabilitySmall = len(smallerDataSet) / len(dataSet) 90 HTC += probabilityBig * calculateEntropy(biggerDataSet) 91 HTC += probabilityBig * calculateEntropy(smallerDataSet) 92 if probabilityBig != 0: 93 split -= probabilityBig * log(probabilityBig, 2) 94 if probabilitySmall != 0: 95 split -= probabilitySmall * log(probabilitySmall, 2) 96 IG = HC - HTC 97 if IG>IGMAX: 98 IGMAX = IG 99 continuousValue = splitPoint[i] 100 N = len(splitPoint) 101 D = len(dataSet) 102 IG -= log(featureListOfValue - 1, 2) / abs(D) 103 GR = float(IG) / float(split) 104 if GR > gainRatioMax: 105 isContinuous = 1 106 gainRatioMax = GR 107 bestfeatureIndex = feature 108 109 return bestfeatureIndex,continuousValue,isContinuous
這裏須要解釋的地方有幾個:
1)信息增益的計算:
通過前面對信息增益的計算,來到這裏應該很容易能看得懂這段代碼了。IG表示的是對於某一種劃分方式的信息增益,由上面公式可知:IG = HC - HTC,HC和HTC的計算基於相同的函數calculateEntropy(),惟一不一樣的是,HC的計算相對簡單,由於它是針對整個數據集(子集)的;HTC的計算則相對複雜,由條件機率得知HTC能夠這樣計算:
因此咱們能夠反覆調用calculateEntropy()函數,而後對於每一次計算結果進行累加,這就能夠獲得HTC。
2)addFeatureValue()函數
增長這一個函數的主要緣由是:在測試集中可能出現訓練集中沒有的特徵的取值的狀況,這在我所使用的adlut數據集中是存在的。慶幸的是,adult數據集官方給出了每種屬性特徵可能出現的全部的取值,這就創造瞭解決這個機會的條件。如上所示,在第二部分準備數據集中,每一個屬性特徵的取值已經給出,那咱們就能夠在建立保存某一屬性特徵的全部不重複取值的時候加上沒有存在的,可是可能出如今測試集中的取值。這就是addFeatureValue()的功用了。
3)chooseBestSplitWay()函數中的修正部分:
這是C4.5修正和不修正的區別之處,下面是不修正的代碼:
1 else: #若是feature是連續型的 2 featureListOfValue = set(featureListOfValue) 3 sortedValue = sorted(featureListOfValue) 4 splitPoint = [] 5 for i in range(len(sortedValue)-1):#n個value,應該有n-1個分割點splitPoint 6 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) 7 8 #C4.5修正,再也不使用信息增益率來選擇最佳分割點 9 for i in range(len(splitPoint)): #對於n-1個分割點,計算每一個分割點的信息增益率,來選擇最佳分割點 10 HTC = 0.0 11 split = 0.0 12 gainRatio = 0.0 13 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 14 print(i) 15 probabilityBig = len(biggerDataSet)/len(dataSet) 16 probabilitySmall = len(smallerDataSet)/len(dataSet) 17 HTC += probabilityBig * calculateEntropy(biggerDataSet) 18 HTC += probabilityBig * calculateEntropy(smallerDataSet) 19 if probabilityBig != 0: 20 split -= probabilityBig * log(probabilityBig,2) 21 if probabilitySmall != 0: 22 split -= probabilitySmall *log(probabilitySmall,2) 23 IG = HC - HTC 24 if split == 0: 25 split = 1; 26 gainRatio = IG/split 27 if gainRatio>gainRatioMax: 28 isContinuous = 1 29 gainRatioMax = gainRatio 30 bestfeatureIndex = feature 31 continuousValue = splitPoint[i] 32 return bestfeatureIndex,continuousValue,isContinuous
這段代碼自己是沒有錯誤的,它也能根據連續型屬性的非修正算法來進行劃分,可是它的問題在於,它老是優先選擇連續型屬性來做爲劃分描述屬性,以下所示:
這些屬性大多數都是連續型屬性,這就使得咱們本來解決「ID3決策樹傾向於選擇取值多的屬性」轉變爲「C4.5決策樹傾向於選擇連續型屬性」的問題。
因此根據上述的「修正」過程,獲得下面的修正代碼:
1 else: #若是feature是連續型的 2 featureListOfValue = set(featureListOfValue) 3 sortedValue = sorted(featureListOfValue) 4 splitPoint = [] 5 for i in range(len(sortedValue)-1):#n個value,應該有n-1個分割點splitPoint 6 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) 7 8 #C4.5修正,再也不使用信息增益率來選擇最佳分割點 9 IGMAX = 0.0 10 for i in range(len(splitPoint)): 11 HTC = 0.0 12 split = 0.0 13 14 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 15 probabilityBig = len(biggerDataSet) / len(dataSet) 16 probabilitySmall = len(smallerDataSet) / len(dataSet) 17 HTC += probabilityBig * calculateEntropy(biggerDataSet) 18 HTC += probabilityBig * calculateEntropy(smallerDataSet) 19 if probabilityBig != 0: 20 split -= probabilityBig * log(probabilityBig, 2) 21 if probabilitySmall != 0: 22 split -= probabilitySmall * log(probabilitySmall, 2) 23 IG = HC - HTC 24 if IG>IGMAX: 25 IGMAX = IG 26 continuousValue = splitPoint[i] 27 N = len(featureListOfValue) 28 D = len(dataSet) 29 IG -= log(N - 1, 2) / abs(D) 30 GR = float(IG) / float(split) 31 if GR > gainRatioMax: 32 isContinuous = 1 33 gainRatioMax = GR 34 bestfeatureIndex = feature 35 36 37 return bestfeatureIndex,continuousValue,isContinuous
這種算法所運行的結果比較傾向於平均化:
因爲 將連續性數據和離散型數據的處理方式統一的放在同一個chooseBestSplitWay中會致使這個函數很是的臃腫混亂,因此後面將它解析了,具體看完整代碼。
4)在處理連續型屬性的時候,屬性取值序列進行了一次去重操做。
這個操做能夠沒有,可是測試結果表示,進行一次去重操做反而能夠提升程序的運行效率和正確率。
爲何要進行這個去重操做?
考慮下面一種連續性屬性的取值序列:{A1,A2,…,An},其中An-m,An-m+1,…,An (0 <= m < n )是相等的,這種數據序列出現的機率很是大,好比age序列(已經升序):{1,2,3,4,…,80,80,80,80}
①若是按照上述的連續值處理的操做來作的話,那麼對於這個沒有去重的取值序列來講,將有屢次取值爲某一個數,那麼這屢次取同一個數來進行數據集劃分的操做將是如出一轍的,加上C4.5自己就是一個低效的算法,若是重複值很是多,會致使算法更加的低效。
②另一個狀況就是,考慮它的分割點狀況,在最後的{…,80,80,80,…,80}的序列中,顯然分割點爲(80+80)/ 2 = 80,那麼在進行劃分數據集的時候,將會劃分爲[min,80]以及(80,max]的狀況,這樣又會遇到一個問題,即劃分的數據子集中,頗有可能出現空集的狀況。這樣就會致使咱們計算出來的機率probability的取值爲0,這樣又要對log(p)的計算進行0值的檢查。若是對數據集進行去重,自己是不會影響到信息熵和最佳分割點的,由於在劃分數據集的時候,probability的計算是針對不去重列表的。並且對於去重後的分割點列表,對於每一個取值,劃分數據集能夠保證不會出現空值,這就極大程度地下降了程序的運行效率。
二、劃分數據集
其實在上一步就已經使用到了劃分數據集了,它沒有像我上面給到的流程那樣,在建立子樹後才劃分數據集,而是先進行劃分,而後再進行建立子樹,緣由在於劃分數據集後計算信息增益會變的更加通用,能夠僅僅使用calculateEntropy()這個函數,而不須要在calculateEntropy()函數的前面增長一個劃分條件,因此咱們應該將「劃分數據集」提早到「尋找最好的屬性特徵以後」馬上進行:
1 #劃分數據集 2 def splitDataSet(dataSet,featureIndex,value):#根據離散值劃分數據集,方法同ID3決策樹 3 newDataSet=[] 4 for vec in dataSet:#將選定的feature的列從數據集中去除 5 if vec[featureIndex] == value: 6 rest = vec[:featureIndex] 7 rest.extend(vec[featureIndex + 1:]) 8 newDataSet.append(rest) 9 return newDataSet 10 11 12 def splitContinuousDataSet(dataSet,feature,continuousValue):#根據連續值來劃分數據集 13 biggerDataSet = [] 14 smallerDataSet = [] 15 for vec in dataSet: 16 rest = vec[:feature] 17 rest.extend(vec[feature + 1:])#將當前列中的feature列去除,其餘的數據保留 18 if vec[feature]>continuousValue:#若是feature列的值比最佳分割點的大,則放在biggerDataSet中,不然放在smallerDataSet中 19 biggerDataSet.append(rest) 20 else: 21 smallerDataSet.append(rest) 22 return biggerDataSet,smallerDataSet
劃分數據集的算法被分割爲離散型屬性的分割和連續型屬性的分割,他們也能夠如上述的chooseBestSplitWay()同樣寫在一塊兒,可是解構出來會顯得更加明瞭。
三、投票表決:
增長投票表決這個過程主要是由於:建立分支的過程就是建立樹的過程,而這個過程不管是原始數據集,仍是數據集的子集,都應該是基於相同的依據來進行建立的,因此這裏採用的遞歸的方式來建立樹,這就存在一個遞歸的結束條件。這個算法的遞歸結束條件應該是:使用完全部的數據集的屬性,而且已經根據全部的屬性的取值構建了其全部的子樹,全部的子樹下都達到全部的分類。可是存在這樣一種狀況:已經處理了數據集的全部屬性特徵,可是分類標籤並非惟一的,好比孿生兄弟性格不同,他們的全部屬性特徵可能相同,但是分類標籤並不同,這就須要一個算法來保證在這裏能獲得一個表決結果,它表明了依據這些屬性特徵,所能達到的分類結果中,「最有可能」出現的一個,因此採用的是投票表決的算法:
1 #返回出現次數最多的類別,避免產生全部特徵所有用完沒法判斷類別的狀況 2 def majority(classList): 3 classificationCount = {} 4 for i in classList: 5 if not i in classificationCount.keys(): 6 classificationCount[i] = 0 7 classificationCount[i] += 1 8 sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True) 9 return sortedClassification[0][0] 10 11 #dict字典轉換爲list列表 12 def dict2list(dic:dict): 13 keys=dic.keys() 14 values=dic.values() 15 lst=[(key,value)for key,value in zip(keys,values)] 16 return lst
這裏惟一須要注意的是排序過程:由於dict沒法進行排序,因此代碼dict應該轉換爲list來進行排序,見dict2list()函數
四、樹建立:
樹建立的過程就是將上面的局部串接成爲總體的過程,它也是上面的建立分支過程的實現:
1 #建立樹 2 def createTree(dataSet,labels,labelType): 3 classificationList = [feature[-1] for feature in dataSet] #產生數據集中的分類列表,保存的是每一行的分類 4 if classificationList.count(classificationList[0]) == len(classificationList): #若是分類別表中的全部分類都是同樣的,則直接返回當前的分類 5 return classificationList[0] 6 if len(dataSet[0]) == 1: #若是劃分數據集已經到了沒法繼續劃分的程度,即已經使用完了所有的feature,則進行決策 7 return majority(classificationList) 8 bestFeature,continuousValue,isContinuous = chooseBestSplitWay(dataSet,labelType) #計算香農熵和信息增益來返回最佳的劃分方案,bestFeature保存最佳的劃分的feature的索引,在C4.5中要判斷該feature是連續型仍是離散型的,continuousValue是當前的返回feature是continuous的的時候,選擇的「最好的」分割點 9 bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具體值 10 print(bestFeatureLabel) 11 Tree = {bestFeatureLabel:{}} 12 del(labels[bestFeature]) #刪除當前進行劃分是使用的feature避免下次繼續使用到這個feature來劃分 13 del(labelType[bestFeature])#刪除labelType中的feature類型,保持和labels同步 14 if isContinuous == 1 :#若是要劃分的feature是連續的 15 #構造以當前的feature做爲root節點,它的連續序列中的分割點爲葉子的子樹 16 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,bestFeature,continuousValue)#根據最佳分割點將數據集劃分爲兩部分,一個是大於最佳分割值,一個是小於等於最佳分割值 17 subLabels = labels[:] 18 subLabelType = labelType[:] 19 Tree[bestFeatureLabel]['>'+str(continuousValue)] = createTree(biggerDataSet,subLabels,subLabelType)#將大於分割值的數據集加入到樹中,而且遞歸建立這顆子樹 20 subLabels = labels[:] 21 subLabelType = labelType[:] 22 Tree[bestFeatureLabel]['<'+str(continuousValue)] = createTree(smallerDataSet,subLabels,subLabelType)#將小於等於分割值的數據集加入到樹中,而且遞歸建立這顆子樹 23 else:#若是要劃分的feature是非連續的,下面的步驟和ID3決策樹相同 24 #構造以當前的feature做爲root節點,它的全部存在的feature取值爲葉子的子樹 25 featureValueList = [feature[bestFeature]for feature in dataSet] #對於上述取出的bestFeature,取出數據集中屬於當前feature的列的全部的值 26 uniqueValue = set(featureValueList) #去重 27 for value in uniqueValue: #對於每個feature標籤的value值,進行遞歸構造決策樹 28 subLabels = labels[:] 29 subLabelType = labelType[:] 30 Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels,subLabelType) 31 return Tree
算法同我上面所寫出來的流程同樣,先進行兩次判斷:
1)是否餘下全部的取值都是同類?
2)是否已經用完了全部的屬性特徵?
這兩個判斷都是終結這個遞歸算法的根本。然後就是取得對於「原始數據集」的最佳分割方案,而後對於這個分割方案,構建出分支,把這個方案所獲得的bestFeature的全部可能的取值構建新的下屬分支即子樹,自此,「原始數據集」的操做就結束了,下面都是對於這個數據集進行一次或屢次劃分的子集的分支構建方案了。而在進行遞歸調用建立子樹的時候,傳入的labels應該是已經複製過的labels,不然,因爲Python不是值傳遞而是引用傳遞的緣由,在子樹建立中將影響到父節點的labels。
在建立樹的過程當中,也是應該分爲當前所選擇的最佳分割方案是連續型屬性仍是離散性屬性,若是是離散性屬性的話,操做的流程和ID3決策樹應該是同樣的,而若是它是連續型屬性的話,建立的子樹結點的儲存值應該是一個二元組(屬性,分割點)。
在建立連續型屬性的子樹的時候有一個很致命的問題:遞歸。
如同通常的樹建立算法同樣,咱們的算法能夠簡單表示爲
1 def createTree(): 2 Tree = {} 3 Tree[left] = createTree() 4 Tree[right] = createTree() 5 return Tree
這樣看來遞歸對於建立樹算法沒什麼影響,然而,真正致命的問題在於參數的傳遞。若是沒有進行參數複製的話,在建立左子樹的時候,將會修改參數的值,而這些參數傳遞給建立右子樹的函數的時候,將是「髒數據」和「錯誤數據」。因此在兩次遞歸的前面,須要將參數值進行保存,並傳遞它的一個副本給這個遞歸算法,保證回溯的時候能傳遞正確的參數給下一個遞歸函數,對於不是尾遞歸的函數,這個問題老是會遇到的。
看看構造出來的C4.5決策樹:
這只是一部分……事實上,運行完成這棵樹的耗時很是長,由於數據集很是大,在沒有使用分佈式的計算的前提下,咱們最好要把這棵樹保存在本地上,而後下次進行測試算法的時候讀取離線的樹,而不是再次生成,《機器學習實戰》中給咱們提供了這樣一種保存樹的方式:
五、保存樹(讀取樹):
1 def storeTree(inputree,filename): 2 fw = open(filename, 'wb') 3 pickle.dump(inputree, fw) 4 fw.close() 5 6 def grabTree(filename): 7 fr = open(filename, 'rb') 8 return pickle.load(fr)
它借用pickle模塊來直接將樹保存下來,可是這個保存下來的樹不是可視化的。
4、測試算法
樹已經構造完成了,下一步就是使用這棵樹的過程了,這也是測試算法的過程。咱們的樹是一個字典,因此咱們測試算法的過程應該是循着這個字典查值的過程:
一、預處理、清洗測試集
預處理和清洗過程和上面對訓練集的過程是同樣的。
二、測試過程
測試過程須要一個classify()函數和一個count()函數。classify()函數負責將上面構造樹的代碼所構造出來的樹接受,而且根據傳入的向量進行分類,而後返回預測的分類標籤,count()函數負責計算這個數據集的正確率:
1 #測試算法 2 def classify(inputTree,featLabels,testVector,labelType): 3 root = list(inputTree.keys())[0] #取出樹的第一個標籤,即樹的根節點 4 dictionary = inputTree[root] #取出樹的第一個標籤下的字典 5 featIndex = featLabels.index(root) 6 classLabel = '<=50K' 7 if labelType[featIndex] == 'uncontinuous':#若是是離散型的標籤,則按照ID3決策樹的方法來測試算法 8 for key in dictionary.keys():#對於這個字典 9 if testVector[featIndex] == key: 10 if type(dictionary[key]).__name__ == 'dict': #若是還有一個新的字典 11 classLabel = classify(dictionary[key],featLabels,testVector,labelType)#遞歸向下尋找到非字典的狀況,此時是葉子節點,葉子節點保存的確定是類別 12 else: 13 classLabel = dictionary[key]#葉子節點,返回類別 14 else:#若是是連續型的標籤,則在取出子樹的每個分支的時候,還須要判斷是>n仍是<=n的狀況,只有這兩種狀況,因此只須要判斷是不是其中一種 15 firstBranch = list(dictionary.keys())[0] #取出第一個分支,來肯定這個double值 16 if str(firstBranch).startswith('>'): #若是第一個分支是">n"的狀況,則取出n,爲1:n 17 number = firstBranch[1:] 18 else: #若是第一個分支是「<=n」的狀況,則取出n,爲2:n 19 number = firstBranch[2:] 20 if float(testVector[featIndex])>float(number):#若是測試向量是>n的狀況 21 string = '>'+str(number) #設置一個判斷string,它是firstBranch的還原,可是爲了節省判斷branch是哪種,直接使用字符串鏈接的方式連接 22 else: 23 string = "<="+str(number) #設置一個判斷string,它是firstBranch的還原,可是爲了節省判斷branch是哪種,直接使用字符串鏈接的方式連接 24 for key in dictionary.keys(): 25 if string == key: 26 if type(dictionary[key]).__name__ == 'dict':#若是還有一個新的字典 27 classLabel = classify(dictionary[key],featLabels,testVector,labelType) 28 else: 29 classLabel = dictionary[key] 30 return classLabel 31 32 def test(mytree,labels,filename,labelType,mydate): 33 with open(filename, 'r')as csvfile: 34 dataset=[line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 35 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 36 cleanoutdata(dataset) #數據清洗 37 del(dataset[0]) #刪除第一行和最後一行的空白數據 38 del(dataset[-1]) 39 #precondition(dataset) #預處理數據集 40 clean(dataset,mydate) #把測試集中的,不存在於訓練集中的離散數據清洗掉 41 total = len(dataset) 42 correct = 0 43 error = 0 44 for line in dataset: 45 result=classify(mytree,labels,line,labelType=labelType)+'.' 46 if result==line[14]: #若是測試結果和類別相同 47 correct = correct + 1 48 else : 49 error = error + 1 50 51 return total,correct,error
因爲構建樹的時候,咱們採用的是字典包含字典的過程,因此當咱們找到一個字典的鍵(Key),能夠直接判斷它的值(Value)是否仍舊是一個字典,若是是,則說明它下面還有分支,還有子樹,不然說明這已經到達了葉子節點,可直接獲取到分類標籤。這個classify()也是一個遞歸向下查找的過程,它經過第一個參數,將樹不斷地進行剪枝,最後達到只剩下一個葉子節點的目的。
建立樹的時候,對於連續型屬性的保存方式是(屬性,分割點)的二元組,因此在測試算法的時候應該將其拆開來,讀取分割點,而後進行判斷來進入左子樹或者右子樹。
測試結果:
未通過修正的算法:
通過修正後的算法:
官方數據:
在後面回過頭來對這個算法進行剪枝操做興許能提升點正確率:)
5、完整代碼
長註釋部分是非修正的算法,沒有將它從程序中移除,保留了另一種實現思路。其中addFeatureValue()函數的實現我沒有放上來,由於我沒有將離散屬性中測試集的全部取值在訓練過程當中加入,而是將測試集中出現了訓練集中沒有的取值的時候,直接將其去除。若是採用前者的方式,將會出現一條徹底擬合的從根到葉子的路徑,屬於這一條惟一的向量。addFeatureValue()的實現思路以下:
1 def addFeatureValue(featureListOfValue,feature): 2 for featureValue in feat[feature]: #feat保存的是全部屬性特徵的全部可能的取值,其結構爲feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ] 3 featureListOfValue.append(featureValue)
下面是針對adult數據集的可運行完整代碼:
1 #encoding=utf-8 2 from math import log 3 import operator 4 import pickle 5 6 #讀取數據集 7 def createDateset(filename): 8 with open(filename, 'r')as csvfile: 9 dataset= [line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 10 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 11 cleanoutdata(dataset) #清洗數據 12 del (dataset[-1]) #去除最後一行的空行 13 #precondition(dataset) #預處理數據 14 labels=['age','workclass','fnlwgt','education','education-num', 15 'marital-status','occupation', 16 'relationship','race','sex','capital-gain','capital-loss','hours-per-week', 17 'native-country'] 18 labelType = ['continuous', 'uncontinuous', 'continuous', 19 'uncontinuous', 20 'continuous', 'uncontinuous', 21 'uncontinuous', 'uncontinuous', 'uncontinuous', 22 'uncontinuous', 'continuous', 'continuous', 23 'continuous', 'uncontinuous'] 24 25 return dataset,labels,labelType 26 27 def cleanoutdata(dataset):#數據清洗 28 for row in dataset: 29 for column in row: 30 if column == '?' or column=='': 31 dataset.remove(row) 32 break 33 34 #計算香農熵/指望信息 35 def calculateEntropy(dataSet): 36 ClassifyCount = {}#分類標籤統計字典,用來統計每一個分類標籤的機率 37 for vector in dataSet: 38 clasification = vector[-1] #獲取分類 39 if not clasification in ClassifyCount.keys():#若是分類暫時不在字典中,在字典中添加對應的值對 40 ClassifyCount[clasification] = 0 41 ClassifyCount[clasification] += 1 #計算出現次數 42 shannonEntropy=0.0 43 for key in ClassifyCount: 44 probability = float(ClassifyCount[key]) / len(dataSet) #計算機率 45 shannonEntropy -= probability * log(probability,2) #香農熵的每個子項都是負的 46 return shannonEntropy 47 48 # def addFetureValue(feature): 49 50 #劃分數據集 51 def splitDataSet(dataSet,featureIndex,value):#根據離散值劃分數據集,方法同ID3決策樹 52 newDataSet=[] 53 for vec in dataSet:#將選定的feature的列從數據集中去除 54 if vec[featureIndex] == value: 55 rest = vec[:featureIndex] 56 rest.extend(vec[featureIndex + 1:]) 57 newDataSet.append(rest) 58 return newDataSet 59 60 61 def splitContinuousDataSet(dataSet,feature,continuousValue):#根據連續值來劃分數據集 62 biggerDataSet = [] 63 smallerDataSet = [] 64 for vec in dataSet: 65 rest = vec[:feature] 66 rest.extend(vec[feature + 1:])#將當前列中的feature列去除,其餘的數據保留 67 if vec[feature]>continuousValue:#若是feature列的值比最佳分割點的大,則放在biggerDataSet中,不然放在smallerDataSet中 68 biggerDataSet.append(rest) 69 else: 70 smallerDataSet.append(rest) 71 return biggerDataSet,smallerDataSet 72 73 74 #連續型屬性不須要將訓練集中有,測試集中沒有的值補全,離散性屬性須要 75 def addFeatureValue(featureListOfValue,feature): 76 feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc', 77 'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'], 78 [],[],[],[],[]] 79 for featureValue in feat[feature]: #feat保存的是全部屬性特徵的全部可能的取值,其結構爲feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ] 80 featureListOfValue.append(featureValue) 81 82 def calGainRatioUnContinuous(dataSet,feature,HC): 83 # addFeatureValue(featureListOfValue,feature) #增長在訓練集中有,測試集中沒有的屬性特徵的取值 84 featureListOfValue = [vector[feature] for vector in dataSet] # 對於dataset中每個feature,建立單獨的列表list保存其取值,其中是不重複的 85 unique = set(featureListOfValue) 86 HTC = 0.0 # 保存HTC,即H(T|C) 87 split = 0.0 # 保存split(T) 88 for value in unique: 89 subDataSet = splitDataSet(dataSet, feature, value) # 劃分數據集 90 probability = len(subDataSet) / float(len(dataSet)) # 求得當前類別的機率 91 split -= probability * log(probability, 2) # 計算split(T) 92 HTC += probability * calculateEntropy(subDataSet) # 計算當前類別的香農熵,並和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C) 93 IG = HC - HTC # 計算對於該種劃分方式的信息增益 94 if split == 0: 95 split = 1 96 gainRatio = float(IG) / float(split) # 計算對於該種劃分方式的信息增益率 97 return gainRatio 98 99 def calGainRatioContinuous(dataSet,feature,HC): 100 featureListOfValue = [vector[feature] for vector in dataSet] # 對於dataset中每個feature,建立單獨的列表list保存其取值,其中是不重複的 101 featureListOfValue = set(featureListOfValue) 102 sortedValue = sorted(featureListOfValue) 103 splitPoint = [] 104 IGMAX = 0.0 105 GR = 0.0 106 continuousValue = 0.0 107 for i in range(len(sortedValue)-1):#n個value,應該有n-1個分割點splitPoint 108 splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) 109 for i in range(len(splitPoint)): 110 HTC = 0.0 111 split = 0.0 112 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 113 probabilityBig = len(biggerDataSet) / len(dataSet) 114 probabilitySmall = len(smallerDataSet) / len(dataSet) 115 HTC += probabilityBig * calculateEntropy(biggerDataSet) 116 HTC += probabilitySmall * calculateEntropy(smallerDataSet) 117 IG = HC - HTC 118 if IG>IGMAX: 119 IGMAX = IG 120 split -= probabilityBig * log(probabilityBig, 2) 121 split -= probabilitySmall * log(probabilitySmall, 2) 122 continuousValue = splitPoint[i] 123 N = len(featureListOfValue) 124 D = len(dataSet) 125 IG -= log(N - 1, 2) / abs(D) 126 GR = float(IG) / float(split) 127 return GR,continuousValue 128 129 #選擇最好的數據集劃分方式 130 def chooseBestSplitWay(dataSet,labelType): 131 isContinuous = -1 #判斷是不是連續值,是爲1,不是爲0 132 HC = calculateEntropy(dataSet)#計算整個數據集的香農熵(指望信息),即H(C),用來和每一個feature的香農熵進行比較 133 bestfeatureIndex = -1 #最好的劃分方式的索引值,由於0也是索引值,因此應該設置爲負數 134 GRMAX=0.0 #信息增益率=(指望信息-熵)/分割得到的信息增益,即爲GR = IG / split = ( HC - HTC )/ split , gainRatioMax爲最好的信息增益率,IG爲各類劃分方式的信息增益 135 continuousValue = -1 #設置若是是連續值的屬性返回的最好的劃分方式的最好分割點的值 136 for feature in range(len(dataSet[0]) -1 ): #計算feature的個數,因爲dataset中是包含有類別的,因此要減去類別 137 if labelType[feature] == 'uncontinuous': 138 GR = calGainRatioUnContinuous(dataSet,feature,HC) 139 if GR>GRMAX: 140 GRMAX = GR 141 bestfeatureIndex = feature 142 isContinuous = 0 143 else: #若是feature是連續型的 144 GR ,bestSplitPoint = calGainRatioContinuous(dataSet,feature,HC) 145 if GR>GRMAX: 146 GRMAX = GR 147 continuousValue = bestSplitPoint 148 isContinuous = 1 149 bestfeatureIndex = feature 150 return bestfeatureIndex,continuousValue,isContinuous 151 # featureListOfValue = set(featureListOfValue) 152 # sortedValue = sorted(featureListOfValue) 153 # splitPoint = [] 154 # for i in range(len(sortedValue)-1):#n個value,應該有n-1個分割點splitPoint 155 # splitPoint.append((float(sortedValue[i])+float(sortedValue[i+1]))/2.0) 156 157 #C4.5修正,再也不使用信息增益率來選擇最佳分割點 158 # for i in range(len(splitPoint)): #對於n-1個分割點,計算每一個分割點的信息增益率,來選擇最佳分割點 159 # HTC = 0.0 160 # split = 0.0 161 # gainRatio = 0.0 162 # biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 163 # probabilityBig = len(biggerDataSet)/len(dataSet) 164 # probabilitySmall = len(smallerDataSet)/len(dataSet) 165 # HTC += probabilityBig * calculateEntropy(biggerDataSet) 166 # HTC += probabilityBig * calculateEntropy(smallerDataSet) 167 # if probabilityBig != 0: 168 # split -= probabilityBig * log(probabilityBig,2) 169 # if probabilitySmall != 0: 170 # split -= probabilitySmall *log(probabilitySmall,2) 171 # IG = HC - HTC 172 # if split == 0: 173 # split = 1; 174 # gainRatio = IG/split 175 # if gainRatio>gainRatioMax: 176 # isContinuous = 1 177 # gainRatioMax = gainRatio 178 # bestfeatureIndex = feature 179 # continuousValue = splitPoint[i] 180 # IGMAX = 0.0 181 # for i in range(len(splitPoint)): 182 # HTC = 0.0 183 # split = 0.0 184 # biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,feature,splitPoint[i]) 185 # probabilityBig = len(biggerDataSet) / len(dataSet) 186 # probabilitySmall = len(smallerDataSet) / len(dataSet) 187 # HTC += probabilityBig * calculateEntropy(biggerDataSet) 188 # HTC += probabilitySmall * calculateEntropy(smallerDataSet) 189 # IG = HC - HTC 190 # if IG>IGMAX: 191 # split -= probabilityBig * log(probabilityBig, 2) 192 # split -= probabilitySmall * log(probabilitySmall, 2) 193 # IGMAX = IG 194 # continuousValue = splitPoint[i] 195 # N = len(splitPoint) 196 # D = len(dataSet) 197 # IG -= log(N - 1, 2) / abs(D) 198 # GR = float(IG) / float(split) 199 # if GR > GRMAX: 200 # isContinuous = 1 201 # GRMAX = GR 202 # bestfeatureIndex = feature 203 # return bestfeatureIndex,continuousValue,isContinuous 204 205 #返回出現次數最多的類別,避免產生全部特徵所有用完沒法判斷類別的狀況 206 def majority(classList): 207 classificationCount = {} 208 for i in classList: 209 if not i in classificationCount.keys(): 210 classificationCount[i] = 0 211 classificationCount[i] += 1 212 sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True) 213 return sortedClassification[0][0] 214 215 #dict字典轉換爲list列表 216 def dict2list(dic:dict): 217 keys=dic.keys() 218 values=dic.values() 219 lst=[(key,value)for key,value in zip(keys,values)] 220 return lst 221 222 #建立樹 223 def createTree(dataSet,labels,labelType): 224 classificationList = [feature[-1] for feature in dataSet] #產生數據集中的分類列表,保存的是每一行的分類 225 if classificationList.count(classificationList[0]) == len(classificationList): #若是分類別表中的全部分類都是同樣的,則直接返回當前的分類 226 return classificationList[0] 227 if len(dataSet[0]) == 1: #若是劃分數據集已經到了沒法繼續劃分的程度,即已經使用完了所有的feature,則進行決策 228 return majority(classificationList) 229 bestFeature,continuousValue,isContinuous = chooseBestSplitWay(dataSet,labelType) #計算香農熵和信息增益來返回最佳的劃分方案,bestFeature保存最佳的劃分的feature的索引,在C4.5中要判斷該feature是連續型仍是離散型的,continuousValue是當前的返回feature是continuous的的時候,選擇的「最好的」分割點 230 bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具體值 231 print(bestFeatureLabel) 232 Tree = {bestFeatureLabel:{}} 233 del(labels[bestFeature]) #刪除當前進行劃分是使用的feature避免下次繼續使用到這個feature來劃分 234 del(labelType[bestFeature])#刪除labelType中的feature類型,保持和labels同步 235 if isContinuous == 1 :#若是要劃分的feature是連續的 236 #構造以當前的feature做爲root節點,它的連續序列中的分割點爲葉子的子樹 237 biggerDataSet,smallerDataSet = splitContinuousDataSet(dataSet,bestFeature,continuousValue)#根據最佳分割點將數據集劃分爲兩部分,一個是大於最佳分割值,一個是小於等於最佳分割值 238 subLabels = labels[:] 239 subLabelType = labelType[:] 240 Tree[bestFeatureLabel]['>'+str(continuousValue)] = createTree(biggerDataSet,subLabels,subLabelType)#將大於分割值的數據集加入到樹中,而且遞歸建立這顆子樹 241 subLabels = labels[:] 242 subLabelType = labelType[:] 243 Tree[bestFeatureLabel]['<'+str(continuousValue)] = createTree(smallerDataSet,subLabels,subLabelType)#將小於等於分割值的數據集加入到樹中,而且遞歸建立這顆子樹 244 else:#若是要劃分的feature是非連續的,下面的步驟和ID3決策樹相同 245 #構造以當前的feature做爲root節點,它的全部存在的feature取值爲葉子的子樹 246 featureValueList = [feature[bestFeature]for feature in dataSet] #對於上述取出的bestFeature,取出數據集中屬於當前feature的列的全部的值 247 uniqueValue = set(featureValueList) #去重 248 for value in uniqueValue: #對於每個feature標籤的value值,進行遞歸構造決策樹 249 subLabels = labels[:] 250 subLabelType = labelType[:] 251 Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels,subLabelType) 252 return Tree 253 254 def storeTree(inputree,filename): 255 fw = open(filename, 'wb') 256 pickle.dump(inputree, fw) 257 fw.close() 258 259 def grabTree(filename): 260 fr = open(filename, 'rb') 261 return pickle.load(fr) 262 263 #測試算法 264 def classify(inputTree,featLabels,testVector,labelType): 265 root = list(inputTree.keys())[0] #取出樹的第一個標籤,即樹的根節點 266 dictionary = inputTree[root] #取出樹的第一個標籤下的字典 267 featIndex = featLabels.index(root) 268 classLabel = '<=50K' 269 if labelType[featIndex] == 'uncontinuous':#若是是離散型的標籤,則按照ID3決策樹的方法來測試算法 270 for key in dictionary.keys():#對於這個字典 271 if testVector[featIndex] == key: 272 if type(dictionary[key]).__name__ == 'dict': #若是還有一個新的字典 273 classLabel = classify(dictionary[key],featLabels,testVector,labelType)#遞歸向下尋找到非字典的狀況,此時是葉子節點,葉子節點保存的確定是類別 274 else: 275 classLabel = dictionary[key]#葉子節點,返回類別 276 else:#若是是連續型的標籤,則在取出子樹的每個分支的時候,還須要判斷是>n仍是<=n的狀況,只有這兩種狀況,因此只須要判斷是不是其中一種 277 firstBranch = list(dictionary.keys())[0] #取出第一個分支,來肯定這個double值 278 if str(firstBranch).startswith('>'): #若是第一個分支是">n"的狀況,則取出n,爲1:n 279 number = firstBranch[1:] 280 else: #若是第一個分支是「<=n」的狀況,則取出n,爲2:n 281 number = firstBranch[2:] 282 if float(testVector[featIndex])>float(number):#若是測試向量是>n的狀況 283 string = '>'+str(number) #設置一個判斷string,它是firstBranch的還原,可是爲了節省判斷branch是哪種,直接使用字符串鏈接的方式連接 284 else: 285 string = "<="+str(number) #設置一個判斷string,它是firstBranch的還原,可是爲了節省判斷branch是哪種,直接使用字符串鏈接的方式連接 286 for key in dictionary.keys(): 287 if string == key: 288 if type(dictionary[key]).__name__ == 'dict':#若是還有一個新的字典 289 classLabel = classify(dictionary[key],featLabels,testVector,labelType) 290 else: 291 classLabel = dictionary[key] 292 return classLabel 293 294 def test(mytree,labels,filename,labelType,mydate): 295 with open(filename, 'r')as csvfile: 296 dataset=[line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 297 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 298 cleanoutdata(dataset) #數據清洗 299 del(dataset[0]) #刪除第一行和最後一行的空白數據 300 del(dataset[-1]) 301 #precondition(dataset) #預處理數據集 302 clean(dataset,mydate) #把測試集中的,不存在於訓練集中的離散數據清洗掉 303 total = len(dataset) 304 correct = 0 305 error = 0 306 for line in dataset: 307 result=classify(mytree,labels,line,labelType=labelType)+'.' 308 if result==line[14]: #若是測試結果和類別相同 309 correct = correct + 1 310 else : 311 error = error + 1 312 313 return total,correct,error 314 315 #C4.5決策樹不須要清洗掉連續性數據 316 # def precondition(mydate):#清洗連續型數據 317 # #continuous:0,2,4,10,11,12 318 # for each in mydate: 319 # del(each[0]) 320 # del(each[1]) 321 # del(each[2]) 322 # del(each[7]) 323 # del(each[7]) 324 # del(each[7]) 325 326 #C4.5決策樹不須要清洗掉測試集中連續值出現了訓練集中沒有的值的狀況,可是離散數據集中仍是須要清洗的 327 def clean(dataset,mydate):#清洗掉測試集中出現了訓練集中沒有的值的狀況 328 for i in [1,3,5,6,7,8,9,13]: 329 set1=set() 330 for row1 in mydate: 331 set1.add(row1[i]) 332 for row2 in dataset: 333 if row2[i] not in set1: 334 dataset.remove(row2) 335 set1.clear() 336 337 def main(): 338 dataSetName = r"C:\Users\yang\Desktop\adult.data" 339 mydate, label ,labelType= createDateset(dataSetName) 340 labelList = label[:] 341 labelTypeList = labelType[:] 342 Tree = createTree(mydate, labelList,labelType=labelTypeList) 343 print(Tree) 344 storeTree(Tree, r'C:\Users\yang\Desktop\tree.txt') # 保存決策樹,避免下次再生成決策樹 345 346 #Tree=grabTree(r'C:\Users\yang\Desktop\tree.txt')#讀取決策樹,若是已經存在tree.txt能夠直接使用決策樹不須要再次生成決策樹 347 total, correct, error = test(Tree, label, r'C:\Users\yang\Desktop\adult.test',labelType,mydate) 348 # with open(r'C:\Users\yang\Desktop\trees.txt', 'w')as f: 349 # f.write(str(Tree)) 350 accuracy = float(correct)/float(total) 351 print("準確率:%f" % accuracy) 352 353 if __name__ == '__main__': 354 main()
6、總結
太慢了!!!太慢了!!!太慢了!!!
C4.5是真的很是很是很是低效!低效!低效!
不過對比ID3的優勢仍是很是顯著的,尤爲在能處理既有離散型屬性又有連續型屬性的數據集的能力上,很強。
樹的深度也比ID3那種單純的快速分割數據集的增加不一樣,不會產生「過擬合」的狀況,不過ID3決策樹是數據集劃分得太快,C4.5是劃分的太慢。
C4.5代碼放在GitHub:https://github.com/hahahaha1997/C4.5DecisionTree
轉載註明出處:https://www.cnblogs.com/DawnSwallow/p/9622398.html