距上篇文章已通過了9個月 orz。。趁着期末複習,把博客補一補。。html
在前面的文章中介紹了決策樹的 ID3,C4.5 算法。咱們知道了 ID3 算法是基於各節點的信息增益的大小 \(\operatorname{Gain}(D, a)=\operatorname{Ent}(D)-\sum_{v} \frac{\left|D^{v}\right|}{|D|} \operatorname{Ent}\left(D^{v}\right)\) 進行劃分,可是存在偏向選取特徵值較多的特徵的問題,所以提出了 C4.5 算法,即以信息增益比爲標準進行劃分 \(\operatorname{Gain}_{-} \operatorname{ratio}(D, a)=\frac{\operatorname{Gain}(D, a)}{I V(a)}\) 其中 \(I V(a)=-\sum_{v=1}^{V} \frac{\left|D^{v}\right|}{|D|} \log \frac{\left|D^{v}\right|}{|D|}\) 。可是,你可能注意到了,ID3 和 C4.5 算法都不能用來作迴歸問題。這篇文章,將介紹 CART(Classification and Regression Tree) 樹的原理,及其實現。python
與前面介紹的決策樹不一樣,CART 樹爲二叉樹,其劃分是基於基尼係數(Gini Index)進行。算法
先來看看基尼值
\[ \operatorname{Gini}(D)=\sum_{k=1}^{K} \sum_{k^{\prime} \neq k} p_{k} ( 1 - p_{k})=1-\sum_{k=1}^{K} p_{k}^{2} \]
上式從直觀上反應了從數據集中任取2個樣本,其類別不一致的機率,其值越小,純度越高。app
基尼係數
\[ Gini\_Index(D,a)=\sum_{v=1}^{V}\frac{|D^v|}{|D|}Gini(D^v) \]dom
離散值函數
也許你已經發現,CART 樹在對離散值作劃分的時候,若該特徵只有兩個屬性值,那很容易,一邊一種就好,可是當屬性值大於等於 3 的時候呢?好比 ['青年', '中年', '老年'],這時候應該如何作劃分?固然是把全部的方式都遍歷一遍啦,存在如下三種狀況 [(('青年'), ('中年', '老年')), (('中年'), ('青年', '老年')), (('老年'), ('中年', '青年'))]。到這裏我想到了這幾個問題:測試
連續值spa
以前介紹的都是離散值的處理,那麼,當遇到連續值的時候,CART 樹又是怎麼處理的呢?由於是二叉樹,因此確定是選取一個值,大於這個值的分到一個節點中去,小於的分到另外一節點中。code
那麼,這裏就涉及到具體的操做了,通常會在劃分時先將這一列特徵值進行排序,若是有 N 個樣本,那麼最多會有 N - 1 種狀況,從頭至尾遍歷,每次選擇兩個值的中點做爲劃分點,而後計算基尼係數,最後選擇值最小的作劃分。htm
若是你關注算法複雜度的話,會發現 CART 樹每次作劃分的時候都須要遍歷全部狀況,速度就會很慢。在 XGBoost 和 LightGBM 中,好像是採用了策略對這一計算進行了加速(挖個坑,後面看 XGBoost 和 LightGBM 的時候補上)。
用 CART 來作分類問題相信有了 C4.5 與 ID3 的基礎,再加上面的介紹,確定也很容易就知道怎麼作了。這裏我來說講如何用 CART 樹來作迴歸問題。
思考一個問題,樹模型並不像線性模型那樣,能夠算出一個 y 值,那麼咱們如何肯定每一個葉子節點的預測值呢?在數學上,迴歸樹能夠看做一個分段函數,每一個葉子節點肯定一個分段區間,葉子節點的輸出爲函數在該節點上的值,且該值爲一個定值。
假設 CART 樹 T 將特徵空間劃分爲 |T| 個區域 \(R_i\) ,而且在每一個區域對應的值爲 \(b_i\) ,對應的假設函數爲
\[ h(x)=\sum_{i=1}^{|T|} b_{i} \mathbb{I}\left(x \in R_{i}\right) \]
那麼,問題在這裏就變成了如何劃分區域 \(R_i\) 和如何肯定每一個區域 \(R_i\) 上對應的值 \(b_i\)。
假設區域 \(R_i\) 已知,那咱們可使用最小平方損失 \(\sum_{x^{(i)} \in R_j}(y^{(i)}-h(x^{i}))^2 = \sum_{x^{(i)} \in R_j}(y^{(i)}-b_j)^2\) ,來求對應的 \(b_j\) ,顯然有 \(b_j=avg(y^{(i)}|x^{(i)} \in R_j)\) 。
爲了劃分區域,可採用啓發式的方法,選擇第 \(u\) 個屬性和對應的值 \(v\),做爲劃分屬性和劃分閾值,定義兩個區域 \(R_1(u,v)=\{x|x_u\le v\}\) 和 \(R_2=\{x|x_u>v\}\) ,而後經過求解下式尋找最優的劃分屬性和劃分閾值
\[ \min _{u, v}\left[\min _{b_{1}} \sum_{x^{(i)} \in R_{1}(u, v)}\left(y^{(i)}-b_{1}\right)^{2}+\min _{b_{2}} \sum_{x^{(i)} \in R_{2}(u, v)}\left(y^{(i)}-b_{2}\right)^{2}\right] \\ b_i=avg(y^{(i)}|x^{(i)} \in R_i) \]
再對兩個區域重複上述劃分,直到知足條件中止。
下面又到了愉快的代碼時間,這裏我只寫了分類的狀況,迴歸樹只需將裏面使用的基尼係數改爲上面最小化的式子便可。
def createDataSetIris(): ''' 函數:獲取鳶尾花數據集,以及預處理 返回: Data:構建決策樹的數據集(因打亂有必定隨機性) Data_test:手動劃分的測試集 featrues:特徵名列表 labels:標籤名列表 ''' labels = ["setosa","versicolor","virginica"] with open('iris.csv','r') as f: rawData = np.array(list(csv.reader(f))) features = np.array(rawData[0,1:-1]) dataSet = np.array(rawData[1:,1:]) #去除序號和特徵列 np.random.shuffle(dataSet) #打亂(以前若是不加array()獲得的會是引用,rawData會被一併打亂) data = dataSet[0:,1:] return rawData[1:,1:], data, features, labels rawData, data, features, labels = createDataSetIris() def calcGiniIndex(dataSet): ''' 函數:計算數據集基尼值 參數:dataSet:數據集 返回: Gini值 ''' counts = [] #每一個標籤在數據集中出現的次數 count = len(dataSet) #數據集長度 for label in labels: counts.append([d[-1] == label for d in dataSet].count(True)) gini = 0 for value in counts: gini += (value / count) ** 2 return 1 - gini def binarySplitDataSet(dataSet, feature, value): ''' 函數:將數據集按特徵列的某一取值換分爲左右兩個子數據集 參數:dataSet:數據集 feature:數據集中某一特徵列 value:該特徵列中的某個取值 返回:左右子數據集 ''' matLeft = [d for d in dataSet if d[feature] <= value] matRight = [d for d in dataSet if d[feature] > value] return matLeft,matRight def classifyLeaf(dataSet, labels): ''' 函數:求數據集最多的標籤,用於結點分類 參數:dataSet:數據集 labels:標籤名列表 返回:該標籤的index ''' counts = [] for label in labels: counts.append([d[-1] == label for d in dataSet].count(True)) return np.argmax(counts) #argmax:使counts取最大值的下標 def chooseBestSplit(dataSet, labels, leafType=classifyLeaf, errType=calcGiniIndex, threshold=(0.01,4)): ''' 函數:利用基尼係數選擇最佳劃分特徵及相應的劃分點 參數:dataSet:數據集 leafType:葉結點輸出函數(當前實驗爲分類) errType:損失函數,選擇劃分的依據(分類問題用的就是GiniIndex) threshold: Gini閾值,樣本閾值(結點Gini或樣本數低於閾值時中止) 返回:bestFeatureIndex:劃分特徵 bestFeatureValue:最優特徵劃分點 ''' thresholdErr = threshold[0] #Gini閾值 thresholdSamples = threshold[1] #樣本閾值 err = errType(dataSet) bestErr = np.inf bestFeatureIndex = 0 #最優特徵的index bestFeatureValue = 0 #最優特徵劃分點 #當數據中輸出值都相等時,返回葉結點(即feature=None,value=結點分類) if err == 0: return None, dataSet[0][-1] #檢驗數據集的樣本數是否小於2倍閾值,如果則再也不劃分,返回葉結點 if len(dataSet) < 2 * thresholdSamples: return None, labels[leafType(dataSet, labels)] #dataSet[0][-1] #嘗試全部特徵的全部取值,二分數據集,計算err(本實驗爲Gini),保留bestErr for i in range(len(dataSet[0]) - 1): featList = [example[i] for example in dataSet] uniqueVals = set(featList) #第i個特徵的可能取值 for value in uniqueVals: leftSet,rightSet = binarySplitDataSet(dataSet, i, value) if len(leftSet) < thresholdSamples or len(rightSet) < thresholdSamples: continue # print(len(leftSet), len(rightSet)) gini = (len(leftSet) * calcGiniIndex(leftSet) + len(rightSet) * calcGiniIndex(rightSet)) / (len(leftSet) + len(rightSet)) if gini < bestErr: bestErr = gini bestFeatureIndex = i bestFeatureValue = value #檢驗Gini閾值,如果則再也不劃分,返回葉結點 if err - bestErr < thresholdErr: return None, labels[leafType(dataSet, labels)] return bestFeatureIndex,bestFeatureValue def createTree_CART(dataSet, labels, leafType=classifyLeaf, errType=calcGiniIndex, threshold=(0.01,4)): ''' 函數:創建CART樹 參數:同上 返回:CART樹 ''' feature,value = chooseBestSplit(dataSet, labels, leafType, errType, threshold) # print(features[feature]) #是葉結點則返回決策分類(chooseBestSplit返回None時代表這裏是葉結點) if feature is None: return value #不然建立分支,遞歸生成子樹 # print(feature, value, len(dataSet)) leftSet,rightSet = binarySplitDataSet(dataSet, feature, value) myTree = {} myTree[features[feature]] = {} myTree[features[feature]]['<=' + str(value) + ' contains' + str(len(leftSet))] = createTree_CART(leftSet, np.array(leftSet)[:,-1], leafType, errType,threshold) myTree[features[feature]]['>' + str(value) + ' contains' + str(len(rightSet))] = createTree_CART(rightSet, np.array(rightSet)[:,-1], leafType, errType,threshold) return myTree CARTTree = createTree_CART(data, labels, classifyLeaf, calcGiniIndex, (0.01,4)) treePlotter.createPlot(CARTTree)