1、ID3決策樹概述html
ID3決策樹是另外一種很是重要的用來處理分類問題的結構,它形似一個嵌套N層的IF…ELSE結構,可是它的判斷標準再也不是一個關係表達式,而是對應的模塊的信息增益。它經過信息增益的大小,從根節點開始,選擇一個分支,如同進入一個IF結構的statement,經過屬性值的取值不一樣進入新的IF結構的statement,直到到達葉子節點,找到它所屬的「分類」標籤。git
它的流程圖是一課沒法保證平衡的多叉樹,每個父節點都是一個判斷模塊,經過判斷,當前的向量會進入它的某一個子節點中,這個子節點是判斷模塊或者終止模塊(葉子節點),當且僅當這個向量到達葉子節點,它也就找到了它的「分類」標籤。github
ID3決策樹和KNN的區別不一樣,它經過一個固定的訓練集是能夠造成一顆永久的「樹」的,這課樹能夠進行保存而且運用到不一樣的測試集中,惟一的要求就是測試集和訓練集須要是結構等價的。這個訓練過程就是根據訓練集建立規則的過程,這也是機器學習的過程。算法
ID3決策樹的一個巨大缺陷是:它將產生過分匹配問題。這裏在不討論信息增益的前提下,有這樣一個例子:人的屬性中有性別和年齡兩個屬性,因爲人的性別只有男和女兩種,年齡有不少種分支,當它有超過兩個分支的時候,在用信息增益選擇新的屬性的時候,會選擇年齡而不是性別,由於ID3決策樹在使用信息增益來劃分數據集的時候會傾向於選擇屬性分支更多的一個;另一個缺陷是,人的年齡假定爲1~100,若是不進行離散化,即區間的劃分,那麼在選擇年齡這個屬性的時候,這棵決策樹會產生最多100個分支,這是很是可怕並且浪費空間和效率的,考慮這 樣一種狀況:兩我的的其餘全部屬性徹底相同,他們的分類都是"A",然而在年齡這一個樹節點中分支了,而這個年齡下有一個跟這兩我的很像,卻不屬於「A」類別的人,因爲ID3決策樹沒法處理連續性數據,那麼這兩我的頗有可能被劃分到兩個分類中,這是不合理的,這也是下一節的C4.5決策樹考慮的問題。api
前面提到了信息增益,這是ID3決策樹劃分數據集的根本。這裏在理論上解釋一下信息增益和香農熵,下面會在訓練算法的時候,經過算法和數據來解釋信息增益和香農熵。app
首先解釋一個熵的概念:熵指的是一個系統「內在的混亂程度」,在這裏也就是表明信息的「有序程度」。熵增的方向就是信息混亂度越大的方向,熵減的方向就是信息趨於「有序」的方向,因此說咱們要劃分數據集來使得數據集局部愈發趨於「有序化」。之因此是說數據集局部,是由於ID3在進行數據集劃分的時候,選擇一個使信息增益最大,即熵減最多的特徵進行劃分,然後該屬性在後續的劃分中將再也不被考慮,因此這是一個遞歸的過程,也是一個不斷局部化數據集的過程。機器學習
信息增益(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最大,此時熵最小,也便是熵減最多。學習
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.
下一步是分析數據:
一、數據預處理:
上面提到了,ID3是沒法處理連續型數據的,因此連續型數據應該在數據預處理這一步進行清理,處理方法有兩種:
一、直接清洗掉:這也是所採用的方法,由於轉換離散數據的前提是,對於連續型數據的劃分要足夠好,好比年齡、身高等的劃分,5劃分和10劃分之間的差距是很是大的,無論哪種劃分都會破壞數據本來的結構,因此這裏採用的是直接清洗掉數據的方法,對於連續數據的使用延遲到C4.5和CART的實現中:
1 def precondition(mydate):#清洗連續型數據 2 #continuous:0,2,4,10,11,12 3 for each in mydate: 4 del(each[0]) 5 del(each[1]) 6 del(each[2]) 7 del(each[7]) 8 del(each[7]) 9 del(each[7])
這裏要注意在Python中用del清洗數據的時候,某一個數據被del了,它的索引爲i,那麼del執行完成後 i+1 的值的索引會變爲i,如上所示連續型數據所在的列爲0,2,4,10,11,12,可是須要清除的列應該是0,1,2,7,7,7。
二、將連續型數據轉換爲離散數據:
這裏的實現方法能夠根據本身的劃分構造一個和KNN同樣的字典,而後掃描一次數據集,將數據集中的連續數據轉換爲離散的數據。
二、數據清洗:
數據中含有大量的不肯定數據,這些數據在數據集中已經被轉換爲‘?’,可是它仍舊是沒法使用的,數據挖掘對於這類數據進行數據清洗的要求規定,若是是可推算數據,應該推算後填入;或者應該經過數據處理填入一個平滑的值,然而這裏的數據大部分沒有相關性,因此沒法推算出一個合理的平滑值;因此全部的‘?’數據都應該被剔除而不該該繼續使用。爲此咱們要用一段代碼來進行數據的清洗:
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 def createDateset(filename): 2 with open(filename, 'r')as csvfile: 3 dataset= [line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 4 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 5 cleanoutdata(dataset) #清洗數據 6 del (dataset[-1]) #去除最後一行的空行 7 precondition(dataset) #預處理數據 8 labels=['workclass','education', 9 'marital-status','occupation', 10 'relationship','race','sex', 11 'native-country'] 12 return dataset,labels 13 14 def cleanoutdata(dataset):#數據清洗 15 for row in dataset: 16 for column in row: 17 if column == '?' or column=='': 18 dataset.remove(row) 19 break 20 21 def precondition(mydate):#清洗連續型數據 22 #continuous:0,2,4,10,11,12 23 for each in mydate: 24 del(each[0]) 25 del(each[1]) 26 del(each[2]) 27 del(each[7]) 28 del(each[7]) 29 del(each[7])
這裏是先進行預處理仍是先進行數據清洗取決於所使用的數據集中,連續型數據和髒數據哪一種更多,先處理更少的那一種能有效地減小處理量。
3、訓練算法
訓練算法既是構造ID3決策樹的過程,構造的原則爲:若是某個樹分支下的數據所有屬於同一類型,則已經正確的爲該分支如下的全部數據劃分分類,無需進一步對數據集進行分割,若是數據集內的數據不屬於同一類型,則須要繼續劃分數據子集,該數據子集劃分後做爲一個分支繼續進行當前的判斷。
用僞代碼表示以下:
if 數據集中全部的向量屬於同一分類:
return 分類標籤
else:
if 屬性特徵已經使用完:
進行投票決策
return 票數最多的分類標籤
else:
尋找信息增益最大的數據集劃分方式(找到要分割的屬性特徵T)
根據屬性特徵T建立分支
for 屬性特徵T的每一個取值
成爲當前樹分支的子樹
劃分數據集(將T屬性特徵的列丟棄或屏蔽)
return 分支(新的數據集,遞歸)
根據上面的僞代碼,就能夠一步一步地完善代碼:
一、尋找信息增益最大的數據集劃分方式(找到要分割的屬性特徵T):
1 #計算香農熵/指望信息 2 def calculateEntropy(dataSet): 3 ClassifyCount = {}#分類標籤統計字典,用來統計每一個分類標籤的機率 4 for vector in dataSet: 5 clasification = vector[-1] #獲取分類 6 if not clasification not in ClassifyCount.keys():#若是分類暫時不在字典中,在字典中添加對應的值對 7 ClassifyCount[clasification] = 0 8 ClassifyCount[clasification] += 1 #計算出現次數 10 shannonEntropy=0.0 11 for key in ClassifyCount: 12 probability=float(ClassifyCount[key]) / dataSet.shape[0] #計算機率 13 shannonEntropy -= probability * log(probability,2) #香農熵的每個子項都是負的 14 return shannonEntropy 15 16 #選擇最好的數據集劃分方式 17 def chooseBestSplitWay(dataSet): 18 HC = calculateEntropy(dataSet)#計算整個數據集的香農熵(指望信息),即H(C),用來和每一個feature的香農熵進行比較 19 bestfeatureIndex = -1 #最好的劃分方式的索引值,由於0也是索引值,因此應該設置爲負數 20 gain=0.0 #信息增益=指望信息-熵,gain爲最好的信息增益,IG爲各類劃分方式的信息增益 21 for feature in range(len(dataSet[0]) -1 ): #計算feature的個數,因爲dataset中是包含有類別的,因此要減去類別 22 featureListOfValue=[vector[feature] for vector in dataSet] #對於dataset中每個feature,建立單獨的列表list保存其取值,其中是不重複的 23 addFeatureValue(featureListOfValue,feature) #增長在訓練集中有,測試集中沒有的屬性特徵的取值 24 unique=set(featureListOfValue) 25 HTC=0.0 #保存HTC,即H(T|C) 26 for value in unique: 27 subDataSet = splitDataset(dataSet,feature,value) #劃分數據集 28 probability = len(subDataSet) / float(len(dataSet)) #求得當前類別的機率 29 HTC += probability * calculateEntropy(subDataSet) #計算當前類別的香農熵,並和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C) 30 IG=HC-HTC #計算對於該種劃分方式的信息增益 31 if(IG > gain): 32 gain = IG 33 bestfeatureIndex = feature 34 return bestfeatureIndex 35 36 37 def addFeatureValue(featureListOfValue,feature): 38 for featureValue in feat[feature]: #feat保存的是全部屬性特徵的全部可能的取值,其結構爲feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ] 39 featureListOfValue.append(featureValue)
這裏須要解釋的地方有幾個:
1)信息增益的計算:
通過前面對信息增益的計算,來到這裏應該很容易能看得懂這段代碼了。IG表示的是對於某一種劃分方式的信息增益,由上面公式可知:IG = HC - HTC,HC和HTC的計算基於相同的函數calculateEntropy(),惟一不一樣的是,HC的計算相對簡單,由於它是針對整個數據集(子集)的;HTC的計算則相對複雜,由條件機率得知HTC能夠這樣計算:
因此咱們能夠反覆調用calculateEntropy()函數,而後對於每一次計算結果進行累加,這就能夠獲得HTC。
2)addFeatureValue()函數
增長這一個函數的主要緣由是:在測試集中可能出現訓練集中沒有的特徵的取值的狀況,這在我所使用的adlut數據集中是存在的。慶幸的是,adult數據集官方給出了每種屬性特徵可能出現的全部的取值,這就創造瞭解決這個機會的條件。如上所示,在第二部分準備數據集中,每一個屬性特徵的取值已經給出,那咱們就能夠在建立保存某一屬性特徵的全部不重複取值的時候加上沒有存在的,可是可能出如今測試集中的取值。這就是addFeatureValue()的功用了。
二、劃分數據集
其實在上一步就已經使用到了劃分數據集了,它沒有像我上面給到的流程那樣,在建立子樹後才劃分數據集,而是先進行劃分,而後再進行建立子樹,緣由在於劃分數據集後計算信息增益會變的更加通用,能夠僅僅使用calculateEntropy()這個函數,而不須要在calculateEntropy()函數的前面增長一個劃分條件,因此咱們應該將「劃分數據集」提早到「尋找最好的屬性特徵以後」馬上進行:
1 #劃分數據集 2 def splitDataSet(dataSet,featureIndex,value): 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
劃分數據集的方式就是將0~傳入的featureIndex的全部的列複製到新的rest列表中,而後跳過這一列,從下一列開始到最後一列extend到列表的末尾中,而後再將這個rest列表做爲新的數據集傳回。
三、投票表決:
增長投票表決這個過程主要是由於:建立分支的過程就是建立樹的過程,而這個過程不管是原始數據集,仍是數據集的子集,都應該是基於相同的依據來進行建立的,因此這裏採用的遞歸的方式來建立樹,這就存在一個遞歸的結束條件。這個算法的遞歸結束條件應該是:使用完全部的數據集的屬性,而且已經根據全部的屬性的取值構建了其全部的子樹,全部的子樹下都達到全部的分類。可是存在這樣一種狀況:已經處理了數據集的全部屬性特徵,可是分類標籤並非惟一的,好比孿生兄弟性格不同,他們的全部屬性特徵可能相同,但是分類標籤並不同,這就須要一個算法來保證在這裏能獲得一個表決結果,它表明了依據這些屬性特徵,所能達到的分類結果中,「最有可能」出現的一個,因此採用的是投票表決的算法:
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]
這裏惟一須要注意的是排序過程:由於dict沒法進行排序,因此代碼dict應該轉換爲list來進行排序:
1 #dict字典轉換爲list列表 2 def dict2list(dic:dict): 3 keys = dic.keys() 4 values = dic.values() 5 lst = [(key,value)for key,value in zip(keys,values)] 6 return lst
四、樹建立:
樹建立的過程就是將上面的局部串接成爲總體的過程,它也是上面的建立分支過程的實現:
1 #建立樹 2 def createTree(dataSet,labels): 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 = chooseBestSplitWay(dataSet) #計算香農熵和信息增益來返回最佳的劃分方案,bestFeature保存最佳的劃分的feature的索引 9 bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具體值 10 Tree = {bestFeatureLabel:{}} 11 del(labels[bestFeature]) #刪除當前進行劃分是使用的feature避免下次繼續使用到這個feature來劃分 12 featureValueList = [feature[bestFeature]for feature in dataSet] #對於上述取出的bestFeature,取出數據集中屬於當前feature的列的全部的值 13 uniqueValue = set(featureValueList) #去重 14 for value in uniqueValue: #對於每個feature標籤的value值,進行遞歸構造決策樹 15 subLabels = labels[:] 16 Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels) 17 return Tree
算法同我上面所寫出來的流程同樣,先進行兩次判斷:
1)是否餘下全部的取值都是同類?
2)是否已經用完了全部的屬性特徵?
這兩個判斷都是終結這個遞歸算法的根本。然後就是取得對於「原始數據集」的最佳分割方案,而後對於這個分割方案,構建出分支,把這個方案所獲得的bestFeature的全部可能的取值構建新的下屬分支即子樹,自此,「原始數據集」的操做就結束了,下面都是對於這個數據集進行一次或屢次劃分的子集的分支構建方案了。而在進行遞歸調用建立子樹的時候,傳入的labels應該是已經複製過的labels,不然,因爲Python不是值傳遞而是引用傳遞的緣由,在子樹建立中將影響到父節點的labels。
自此,咱們的ID3決策樹就已經構建完成,如今咱們徹底能夠獲得一棵獨立的決策樹,它是離線的。看看咱們的樹長什麼樣:
這只是一部分……事實上,運行完成這棵樹的耗時很是長,由於數據集很是大,在沒有使用分佈式的計算的前提下,咱們最好要把這棵樹保存在本地上,而後下次進行測試算法的時候讀取離線的樹,而不是再次生成,《機器學習實戰》中給咱們提供了這樣一種保存樹的方式:
五、保存樹(讀取樹):
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): 3 root = list(inputTree.keys())[0] #取出樹的第一個標籤,即樹的根節點 4 dictionary = inputTree[root] #取出樹的第一個標籤下的字典 5 featIndex = featLabels.index(root) 6 for key in dictionary.keys(): #對於這個字典 7 if testVector[featIndex] == key: 8 if type(dictionary[key]).__name__ == 'dict': #若是還有一個新的字典 9 classLabel = classify(dictionary[key],featLabels,testVector) #遞歸向下尋找到非字典的狀況,此時是葉子節點,葉子節點保存的確定是類別 10 else: 11 classLabel=dictionary[key] #葉子節點,返回類別 12 return classLabel 13 14 def test(myTree,labels,filename,sum,correct,error): 15 for line in dataSet: 16 result=classify(myTree,labels,line)+'.' 17 if result==line[8]: #若是測試結果和類別相同 18 correct = correct + 1 19 else : 20 error = error + 1 21 print("準確率:%f"% correct / sum ) 22 return sum,correct,error
因爲構建樹的時候,咱們採用的是字典包含字典的過程,因此當咱們找到一個字典的鍵(Key),能夠直接判斷它的值(Value)是否仍舊是一個字典,若是是,則說明它下面還有分支,還有子樹,不然說明這已經到達了葉子節點,可直接獲取到分類標籤。這個classify()也是一個遞歸向下查找的過程,它經過第一個參數,將樹不斷地進行剪枝,最後達到只剩下一個葉子節點的目的。
看看結果 :
跟官方的數據進行對比(官方的是錯誤率):
5、完整代碼
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=['workclass','education', 15 'marital-status','occupation', 16 'relationship','race','sex', 17 'native-country'] 18 return dataset,labels 19 20 def cleanoutdata(dataset):#數據清洗 21 for row in dataset: 22 for column in row: 23 if column == '?' or column=='': 24 dataset.remove(row) 25 break 26 27 #計算香農熵/指望信息 28 def calculateEntropy(dataSet): 29 ClassifyCount = {}#分類標籤統計字典,用來統計每一個分類標籤的機率 30 for vector in dataSet: 31 clasification = vector[-1] #獲取分類 32 if not clasification not in ClassifyCount.keys():#若是分類暫時不在字典中,在字典中添加對應的值對 33 ClassifyCount[clasification] = 0 34 ClassifyCount[clasification] += 1 #計算出現次數 35 shannonEntropy=0.0 36 for key in ClassifyCount: 37 probability=float(ClassifyCount[key]) / dataSet.shape[0] #計算機率 38 shannonEntropy -= probability * log(probability,2) #香農熵的每個子項都是負的 39 return shannonEntropy 40 41 # def addFetureValue(feature): 42 43 #劃分數據集 44 def splitDataSet(dataSet,featureIndex,value): 45 newDataSet=[] 46 for vec in dataSet:#將選定的feature的列從數據集中去除 47 if vec[featureIndex] == value: 48 rest = vec[:featureIndex] 49 rest.extend(vec[featureIndex + 1:]) 50 newDataSet.append(rest) 51 return newDataSet 52 53 54 def addFeatureValue(featureListOfValue,feature): 55 feat = [[ 'Private', 'Self-emp-not-inc', 'Self-emp-inc', 56 'Federal-gov', 'Local-gov', 'State-gov', 'Without-pay', 'Never-worked'], 57 [],[],[],[],[]] 58 for featureValue in feat[feature]: #feat保存的是全部屬性特徵的全部可能的取值,其結構爲feat = [ [val1,val2,val3,…,valn], [], [], [], … ,[] ] 59 featureListOfValue.append(featureValue) 60 61 #選擇最好的數據集劃分方式 62 def chooseBestSplitWay(dataSet): 63 HC = calculateEntropy(dataSet)#計算整個數據集的香農熵(指望信息),即H(C),用來和每一個feature的香農熵進行比較 64 bestfeatureIndex = -1 #最好的劃分方式的索引值,由於0也是索引值,因此應該設置爲負數 65 gain=0.0 #信息增益=指望信息-熵,gain爲最好的信息增益,IG爲各類劃分方式的信息增益 66 for feature in range(len(dataSet[0]) -1 ): #計算feature的個數,因爲dataset中是包含有類別的,因此要減去類別 67 featureListOfValue=[vector[feature] for vector in dataSet] #對於dataset中每個feature,建立單獨的列表list保存其取值,其中是不重複的 68 addFeatureValue(featureListOfValue,feature) #增長在訓練集中有,測試集中沒有的屬性特徵的取值 69 unique=set(featureListOfValue) 70 HTC=0.0 #保存HTC,即H(T|C) 71 for value in unique: 72 subDataSet = splitDataSet(dataSet,feature,value) #劃分數據集 73 probability = len(subDataSet) / float(len(dataSet)) #求得當前類別的機率 74 HTC += probability * calculateEntropy(subDataSet) #計算當前類別的香農熵,並和HTC想加,即H(T|C) = H(T1|C)+ H(T2|C) + … + H(TN|C) 75 IG=HC-HTC #計算對於該種劃分方式的信息增益 76 if(IG > gain): 77 gain = IG 78 bestfeatureIndex = feature 79 return bestfeatureIndex 80 81 #返回出現次數最多的類別,避免產生全部特徵所有用完沒法判斷類別的狀況 82 def majority(classList): 83 classificationCount = {} 84 for i in classList: 85 if not i in classificationCount.keys(): 86 classificationCount[i] = 0 87 classificationCount[i] += 1 88 sortedClassification = sorted(dict2list(classificationCount),key = operator.itemgetter(1),reverse = True) 89 return sortedClassification[0][0] 90 91 #dict字典轉換爲list列表 92 def dict2list(dic:dict): 93 keys=dic.keys() 94 values=dic.values() 95 lst=[(key,value)for key,value in zip(keys,values)] 96 return lst 97 98 #建立樹 99 def createTree(dataSet,labels): 100 classificationList = [feature[-1] for feature in dataSet] #產生數據集中的分類列表,保存的是每一行的分類 101 if classificationList.count(classificationList[0]) == len(classificationList): #若是分類別表中的全部分類都是同樣的,則直接返回當前的分類 102 return classificationList[0] 103 if len(dataSet[0]) == 1: #若是劃分數據集已經到了沒法繼續劃分的程度,即已經使用完了所有的feature,則進行決策 104 return majority(classificationList) 105 bestFeature = chooseBestSplitWay(dataSet) #計算香農熵和信息增益來返回最佳的劃分方案,bestFeature保存最佳的劃分的feature的索引 106 bestFeatureLabel = labels[bestFeature] #取出上述的bestfeature的具體值 107 Tree = {bestFeatureLabel:{}} 108 del(labels[bestFeature]) #刪除當前進行劃分是使用的feature避免下次繼續使用到這個feature來劃分 109 featureValueList = [feature[bestFeature]for feature in dataSet] #對於上述取出的bestFeature,取出數據集中屬於當前feature的列的全部的值 110 uniqueValue = set(featureValueList) #去重 111 for value in uniqueValue: #對於每個feature標籤的value值,進行遞歸構造決策樹 112 subLabels = labels[:] 113 Tree[bestFeatureLabel][value] = createTree(splitDataSet(dataSet,bestFeature,value),subLabels) 114 return Tree 115 116 def storeTree(inputree,filename): 117 fw = open(filename, 'wb') 118 pickle.dump(inputree, fw) 119 fw.close() 120 121 def grabTree(filename): 122 fr = open(filename, 'rb') 123 return pickle.load(fr) 124 125 #測試算法 126 def classify(inputTree,featLabels,testVector): 127 root = list(inputTree.keys())[0] #取出樹的第一個標籤,即樹的根節點 128 dictionary = inputTree[root] #取出樹的第一個標籤下的字典 129 featIndex = featLabels.index(root) 130 for key in dictionary.keys():#對於這個字典 131 if testVector[featIndex] == key: 132 if type(dictionary[key]).__name__ == 'dict': #若是還有一個新的字典 133 classLabel = classify(dictionary[key],featLabels,testVector)#遞歸向下尋找到非字典的狀況,此時是葉子節點,葉子節點保存的確定是類別 134 else: 135 classLabel=dictionary[key]#葉子節點,返回類別 136 return classLabel 137 138 def test(mytree,labels,filename,sum,correct,error): 139 with open(filename, 'r')as csvfile: 140 dataset=[line.strip().split(', ') for line in csvfile.readlines()] #讀取文件中的每一行 141 dataset=[[int(i) if i.isdigit() else i for i in row] for row in dataset] #對於每一行中的每個元素,將行列式數字化而且去除空白保證匹配的正確完成 142 cleanoutdata(dataset) #數據清洗 143 del(dataset[0]) #刪除第一行和最後一行的空白數據 144 del(dataset[-1]) 145 precondition(dataset) #預處理數據集 146 # clean(dataset) #把測試集中的,不存在於訓練集中的數據清洗掉 147 sum = len(dataset) 148 for line in dataset: 149 result=classify(mytree,labels,line)+'.' 150 if result==line[8]: #若是測試結果和類別相同 151 correct = correct + 1 152 else : 153 error = error + 1 154 155 return sum,correct,error 156 157 def precondition(mydate):#清洗連續型數據 158 #continuous:0,2,4,10,11,12 159 for each in mydate: 160 del(each[0]) 161 del(each[1]) 162 del(each[2]) 163 del(each[7]) 164 del(each[7]) 165 del(each[7]) 166 167 # def clean(dataset):#清洗掉測試集中出現了訓練集中沒有的值的狀況 168 # global mydate 169 # for i in range(8): 170 # set1=set() 171 # for row1 in mydate: 172 # set1.add(row1[i]) 173 # for row2 in dataset: 174 # if row2[i] not in set1: 175 # dataset.remove(row2) 176 # set1.clear() 177 178 dataSetName=r"C:\Users\yang\Desktop\adult.data" 179 mydate,label=createDateset(dataSetName) 180 labelList=label[:] 181 182 Tree=createTree(mydate,labelList) 183 184 sum = 0 185 correct = 0 186 error = 0 187 188 storeTree(Tree,r'C:\Users\yang\Desktop\tree.txt') #保存決策樹,避免下次再生成決策樹 189 190 # Tree=grabTree(r'C:\Users\yang\Desktop\tree.txt')#讀取決策樹,若是已經存在tree.txt能夠直接使用決策樹不須要再次生成決策樹 191 sum,current,unreco=test(Tree,label,r'C:\Users\yang\Desktop\adult.test',sum,correct,error) 192 # with open(r'C:\Users\yang\Desktop\trees.txt', 'w')as f: 193 # f.write(str(Tree)) 194 print("準確率:%f" % correct / sum)
6、總結
因爲ID3決策樹仍是存在着兩個巨大的缺陷,下一節將是實現C4.5決策樹,下下節是CART分類迴歸樹,這兩種樹將彌補這種缺點。另外是使用的Adult數據集的問題,所獲得的結果(正確率)超過官方所給的數據,究其緣由應該是數據清洗的時候,我把大多數的噪聲數據清洗掉了,這對數據集的破壞很是大,其實若是能夠的話,仍是應該進行填補和填充的。另外就是使用Iris數據集應該可使得正確率很是高,由於其屬性特徵的數目很少,取值也很少,ID3決策樹在這方面仍是趨於一個弱勢,因此纔會有C4.5,C5.0和CART的出現。C4.5和CART會繼續用Python3實現,C5.0試一下哈哈。
原創博客,碼字不易,轉載註明出處:https://www.cnblogs.com/DawnSwallow/p/9452586.html
github:https://github.com/hahahaha1997/DecisionTree