機器學習決策樹ID3算法,手把手教你用Python實現


本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是機器學習專題的第21篇文章,我們一起來看一個新的模型——決策樹。

決策樹的定義

決策樹是我本人非常喜歡的機器學習模型,非常直觀容易理解,並且和數據結構的結合很緊密。我們學習的門檻也很低,相比於那些動輒一堆公式的模型來說,實在是簡單得多。

其實我們生活當中經常在用決策樹,只是我們自己沒有發現。決策樹的本質就是一堆if-else的組合,舉個經典的例子,比如我們去小攤子上買西瓜。水果攤的小販都是怎么做的?拿起西瓜翻滾一圈,看一眼,然后伸手一拍,就知道西瓜甜不甜。我們把這些動作相關的因素去除,把核心本質提取出來,基本上是這么三條:

  1. 西瓜表面的顏色,顏色鮮艷的往往比較甜
  2. 西瓜拍打的聲音,聲音清脆的往往比較甜
  3. 西瓜是否有瓜藤,有藤的往往比較甜

這三條顯然不是平等的,因為拍打的聲音是最重要的,可能其次表面顏色,最后是瓜藤。所以我們挑選的時候,肯定也是先聽聲音,然后看瓜藤,最后看顏色。我們把其中的邏輯抽象出來然后整理一下,變成一棵樹結構,於是這就成了決策樹。

這個決策樹本質上做的還是分類的工作,將西瓜分成了甜的和不甜的。也就是說決策樹是一個樹形的分類器,這個也是決策樹的基本定義。另外從圖中我們還有一個啟示,在這個問題當中,決策樹的特征都是離散值,而不是連續值。也就是說決策樹可以接受像是類別、標識這樣非數值型的特征,而邏輯回歸這些模型則不太行。

如果你對這些細節還理解不深刻也沒有關系,我們可以先放一放,至少我們明白了決策樹的大概結構以及工作原理。

對於每一條數據來說,它分類的過程其實就是在決策樹上遍歷的過程。每到一個中間節點都會面臨一次判斷,根據判斷的結果選擇下一個子樹。而樹上的葉子節點代表一種分類,當數據到了葉子節點,這個葉子節點的值就代表它的分類結果。

決策樹的訓練

明白了決策樹的結構和工作原理之后,下面就是訓練的過程了。

在理清楚原理之前,我們先來看下數據。我們根據上面決策樹的結構,很容易發現,訓練數據應該是這樣的表格:

分類 聲音是否清脆 是否有瓜藤 是否有光澤
不甜
不甜

那么最后我們想要實現什么效果呢?當然是得到的准確率越高越好,而根據決策樹的原理,樹上的每一個葉子節點代表一個分類。那么我們顯然希望最后到達葉子節點的數據盡可能純粹,舉個例子,如果一個葉子節點代表甜,那么我們肯定希望根據樹結構被划歸到這里的數據盡可能都是甜的,不甜的比例盡可能低。

那么我們怎么實現這一點呢?這就需要我們在越頂層提取規則的時候,越選擇一些區分度大的特征作為切分的依據。所謂區分度大的特征,也就是能夠將數據很好分開的特征。這是明顯的貪心做法,使用這樣的方法,我們只可以保證在盡可能高層取得盡可能好的分類結果,但是並不能保證這樣得到的模型是最優的。生成最優的決策樹本質上也是一個NP問題,我們當前的做法可以保證在盡量短的時間內獲得一個足夠優秀的解,但是沒辦法保證是最優解。

回到問題本身,我們想要用區分度大的特征來進行數據划分。要做到這一點的前提就是首先定義區分度這個概念,將它量化,這樣我們才好進行選擇。否則總不能憑感覺去衡量區分度,好在這個區分度還是很好解決的,我們只需要再一次引入信息熵的概念就可以了。

信息熵與信息增益

信息熵這個詞很令人費解,它英文原文是information entropy,其實一樣難以理解。因為entropy本身是物理學和熱力學當中的概念,用來衡量物體分散的不均勻程度。也就是說熵越大,說明物體分散得程度越大,可以簡單理解成越散亂。比如我們把房間里一盒整理好的乒乓球打翻,那么里面的乒乓球顯然會散亂到房間的各個地方,這個散亂的過程可以理解成熵增大的過程。

信息熵也是一樣的含義,用來衡量一份信息的散亂程度。熵越大,說明信息越雜亂無章,否則說明信息越有調理。信息熵出自大名鼎鼎的信息學巨著《信息論》,它的作者就是赫赫有名的香農。但是這個詞並不是香農原創,據說是計算機之父馮諾依曼取的,他去這個名字的含義也很簡單,因為大家都不明白這個詞究竟是什么意思。

之前我們曾經在介紹交叉熵的時候詳細解釋過這個概念,我們來簡單回顧一下。對於一個事件X來說,假設它發生的概率是P(X),那么這個事件本身的信息量就是:

比如說世界杯中國隊奪冠的概率是1/128,那么我們需要用8個比特才能表示,說明它信息量很大。假如巴西隊奪冠的概率是1/4,那么只要2個比特就足夠了,說明它的信息量就很小。同樣一件事情,根據發生的概率不同,它的信息量也是不同的。

那么信息熵的含義其實就是信息量的期望,也就是用信息量乘上它的概率:

同樣,假設我們有一份數據集合,其中一共有K類樣本,每一類樣本所占的比例是,那么我們把這個比例看成是概率的話,就可以寫出這整個集合的信息熵:

理解了信息熵的概念之后,再來看信息增益就很簡單了。信息增益說白了就是我們划分前后信息熵的變化量,假設我們選擇了某一個特征進行切分,將數據集D切分成了D1和D2。那么就叫做信息增益,也就是切分之后信息熵與之前的變化量。

我們根據熵的定義可以知道,如果數據變得純粹了,那么信息熵應該會減少。減少得越多,說明切分的效果越好。所以我們就找到了衡量切分效果的方法,就是信息增益。我們根據信息增益的定義,可以很簡單地理出整個決策樹建立的過程。就是我們每次在選擇切分特征的時候,都會遍歷所有的特征,特征的每一個取值對應一棵子樹,我們通過計算信息增益找到切分之后增益最大的特征。上層的結構創建好了之后, 通過遞歸的形式往下繼續建樹,直到切分之后的數據集變得純粹,或者是所有特征都使用結束了為止。

這個算法稱為ID3算法,它也是決策樹最基礎的構建算法。這里有一個小細節, 根據ID3算法的定義,每一次切分選擇的是特征,而不是特征的取值。並且被選中作為切分特征的特征的每一個取值都會建立一棵子樹,也就是說每一個特征在決策樹當中都只會最多出現一次。因為使用一次之后,這個特征的所有取值就都被使用完了。

舉個例子,比如拍打聲音是否清脆這個特征,我們在一開始就選擇了它。根據它的兩個取值,是和否都建立了一棵子樹。那么如果我們在子樹當中再根據這個特征拆分顯然沒有意義,因為子樹中的所有數據的這個特征都是一樣的。另外,ID3算法用到的所有特征必須是離散值,因為連續值無法完全切分。如果西瓜的重量是一個特征,那么理論上來說所有有理數都可能是西瓜的質量,我們顯然不可能窮盡所有的取值。

這一點非常重要,不僅關系到我們實現的決策樹是否正確,也直接關系到我們之后理解其他的建樹算法。

代碼實現

理解了算法原理和流程之后,就到了我們緊張刺激的編碼環節。老實講決策樹的算法實現並不難,比之前的FP-growth還要簡單,大家不要有壓力。

首先,我們來創造實驗數據:

import numpy as np
import math def create_data():  X1 = np.random.rand(50, 1)*100  X2 = np.random.rand(50, 1)*100  X3 = np.random.rand(50, 1)*100   def f(x):  return 2 if x > 70 else 1 if x > 40 else 0   y = X1 + X2 + X3  Y = y > 150  Y = Y + 0  r = map(f, X1)  X1 = list(r)   r = map(f, X2)  X2 = list(r)   r = map(f, X3)  X3 = list(r)  x = np.c_[X1, X2, X3, Y]  return x, ['courseA', 'courseB', 'courseC'] 

這份數據模擬的是學生考試,一共考三門,一共要考到150分以上才算是通過。由於ID3算法只能接受離散值的特征,所以我們要先將連續值轉成離散值,我們根據每一門的考試分數,生成三個檔次。大於70分的是2檔,40到70分的是1檔,小於40分的是0檔。

為了方便編碼,我們把預測值Y放在特征的最后,並且返回這三個特征的名稱,方便以后用來建樹。

我們運行一下數據查看一下結果:

下面,我們實現計算集合信息熵的函數。這個函數也很簡單,我們只需要計算出每個類別的占比,然后套用一下信息熵的公式即可。

from collections import Counter
 def calculate_info_entropy(dataset):  n = len(dataset)  # 我們用Counter統計一下Y的數量  labels = Counter(dataset[:, -1])  entropy = 0.0  # 套用信息熵公式  for k, v in labels.items():  prob = v / n  entropy -= prob * math.log(prob, 2)  return entropy 

有了信息熵的計算函數之后,我們接下來實現拆分函數,也就是根據特征的取值將數據集進行拆分的函數。

def split_dataset(dataset, idx):
 # idx是要拆分的特征下標  splitData = defaultdict(list)  for data in dataset:  # 這里刪除了idx這個特征的取值,因為用不到了  splitData[data[idx]].append(np.delete(data, idx))  return list(splitData.values()), list(splitData.keys()) 

本質上就是根據特征取值歸類的過程,我們可以隨便調用測試一下:

和我們預期一樣,根據特征的取值將數據分成了若干份。接下來我們就要實現核心的特征的選擇函數了,也就是要選擇信息增益最大的特征對數據進行切分。

def choose_feature_to_split(dataset):
 n = len(dataset[0])-1  m = len(dataset)  # 切分之前的信息熵  entropy = calculate_info_entropy(dataset)  bestGain = 0.0  feature = -1  for i in range(n):  # 根據特征i切分  split_data, _ = split_dataset(dataset, i)  new_entropy = 0.0  # 計算切分后的信息熵  for data in split_data:  prob = len(data) / m  new_entropy += prob * calculate_info_entropy(data)  # 獲取信息增益  gain = entropy - new_entropy  if gain > bestGain:  bestGain = gain  feature = i  return feature 

到這里,我們所有工具方法都已經開發完了,下面就到了我們緊張刺激的建樹部分了。建樹其實並沒有什么大不了的,無非是通過遞歸來重復調用上面的方法來創造每一個分支節點而已。如果你熟悉樹形數據結構,會發現它和其他樹形數據結構的構建過程並沒有什么兩樣。

我們來看下代碼,整個過程也只有十幾行而已。

def create_decision_tree(dataset, feature_names):
 dataset = np.array(dataset)  counter = Counter(dataset[:, -1])  # 如果數據集值剩下了一類,直接返回  if len(counter) == 1:  return dataset[0, -1]   # 如果所有特征都已經切分完了,也直接返回  if len(dataset[0]) == 1:  return counter.most_common(1)[0][0]   # 尋找最佳切分的特征  fidx = choose_feature_to_split(dataset)  fname = feature_names[fidx]   node = {fname: {}}  feature_names.remove(fname)   # 遞歸調用,對每一個切分出來的取值遞歸建樹  split_data, vals = split_dataset(dataset, fidx)  for data, val in zip(split_data, vals):  node[fname][val] = create_decision_tree(data, feature_names[:])  return node 

我們運行一下這段代碼,會得到一份dict,這個dict當中的層次結構其實就是決策樹的結構:

我們這樣看可能不太清楚,但是我們把這個dict展開就得到了下圖的這棵樹結構:

我們觀察一下上圖當中紅圈的部分,這個節點只有兩個分叉,而其他的節點都有三個分叉。這並不是代碼有bug,而是說明數據當中缺失了這種情況,所以少了一個分叉。這其實非常正常,當我們訓練數據的樣本量不夠的時候,很有可能無法覆蓋所有的情況,就會出現這種沒有分叉的情況。

到這里雖然決策樹是實現完了,但是還沒有結束,還有一個關鍵的部分我們沒有做,就是預測。我們訓練完了,總得把模型用起來,顯然需要一個預測的函數。這個預測的函數也簡單,它介紹一條數據以及我們訓練完的樹結構,返回分類的結果。其實也是一個遞歸調用的過程:

def classify(node, feature_names, data):
 # 獲取當前節點判斷的特征  key = list(node.keys())[0]  node = node[key]  idx = feature_names.index(key)   # 根據特征進行遞歸  pred = None  for key in node:  # 找到了對應的分叉  if data[idx] == key:  # 如果再往下依然還有子樹,那么則遞歸,否則返回結果  if isinstance(node[key], dict):  pred = classify(node[key], feature_names, data)  else:  pred = node[key]   # 如果沒有對應的分叉,則找到一個分叉返回  if pred is None:  for key in node:  if not isinstance(node[key], dict):  pred = node[key]  break  return pred 

我們來創造一些簡單的數據測試一下:

基本上和我們的預期一致,說明我們決策樹就實現完了。

總結

我們的決策樹雖然構建完了,但是仍然有很多不完美的地方。比如說,目前我們的模型只能接受離散值的特征,如果是連續值則無法進行拆分。而且我們每個特征只能用一次,有時候我們希望能夠多次使用同一個特征。在這種情況下ID3就無法實現了。所以我們還需要引入其他的優化。

在后序的文章當中我們將會討論這些相關的優化,以及決策樹這個模型本身的一些特性。如果對此感興趣,一定不要錯過。

喜歡的話,順手點個關注吧


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM