一、基礎概念
決策樹是一類極為常用的機器學習方法,尤其是在分類場景。決策樹通過樹形結構來遞歸地將樣本分割到不同的葉子結點中去,并根據每個葉子結點中的樣本構成對該結點中的樣本進行分類。
我們可以從兩個視角來理解決策樹模型。
- 第一種視角是將決策樹視為一組規(guī)則的集合。對一棵完整的決策樹來說,從根節(jié)點到每一個葉子結點都對應了一條規(guī)則,不同的規(guī)則之間互斥且完備。
- 第二種視角是從條件概率的角度來理解決策樹。我們對每個葉子結點的分類,都是依據該結點包含的樣本集合分屬于不同分類的概率來決定的。從這個角度來看,決策樹本質上也是一種概率模型。
與其他機器學習方法一樣,使用決策樹進行預測時,我們的目標是盡可能地在新樣本上預測得更準確。那么,一方面我們要在訓練集上得到盡可能高的預測精度,另一方面,我們要通過正則化參數來保證模型沒有過擬合。
假設樹的葉子結點個數為
,
為樹
的葉子結點,每個葉子結點有
個樣本,假設
類的樣本有
個,其中
,
為葉子結點上的經驗熵(empirical entropy),
為正則化參數,那么決策樹學習的損失函數可以表示為:
決策樹學習的目標就是最小化上述函數,該函數無法使用常規(guī)的梯度下降來直接求解,因此我們一般使用啟發(fā)式的方法來尋找最優(yōu)決策樹,具體來說,就是遞歸地選擇最優(yōu)特征來分割數據集。如果某次分割后的子集可以完全正確劃分到某一類,那么該子集可以歸到一個葉子結點;否則,繼續(xù)從這些子集中選擇最優(yōu)特征進行下一次劃分,直到所有子集都能被正確分類。
以上思路會構建一棵完整的樹,但是正如前文所述,我們還需要保證模型沒有過擬合,因此我們需要對決策樹進行剪枝。決策樹剪枝通常有預剪枝和后剪枝兩種方法。
總的來說,完整的決策樹包含特征選擇、決策樹構建和決策樹剪枝三大方面。
二、特征選擇
為了構建一棵性能良好的決策樹,我們需要從訓練集中不斷選取最具有區(qū)分度(分類能力)的特征。一般來說,我們通過三個指標來實現這一目標。
1. 信息增益
為了說明信息增益,我們需要引入信息熵的概念。在信息論和概率論中,熵是一種描述隨機變量不確定性的度量方式,也可以用來描述樣本集合的不純度。熵越低,樣本的不確定性就越低,純度則越高。
假設當前樣本數據集中的第
個類所占比例為
,那么該樣本數據集的熵可以定義為:
假設離散隨機變量的聯合概率分布為:
條件熵表示在已知隨機變量
的條件下對
的不確定性的度量,它可以定義為在給定
的條件下
的條件概率分布的熵對
的數學期望。條件熵可以表示為:
其中,。在利用實際數據進行計算時,熵和條件熵中的概率計算都是基于極大似然估計得到,對應的熵和條件熵也叫經驗熵和經驗條件熵。
信息增益是指在得到了某個特征X的信息之后,使得類Y的信息不確定性減少的程度?;蛘哒f,信息增益代表了某特征帶來的分類確定性的增量,特征的信息增益越大,目標分類的確定性也就越大。
假設訓練集的經驗熵為
,給定特征
的條件下
的經驗條件熵為
,那么信息增益可定義為經驗熵
與經驗條件熵之差:
構建決策樹時可以使用信息增益進行特征選擇,特征的信息增益越大,代表了其分類能力越強,ID3算法就是基于信息增益做特征選擇的。
我們舉一個例子來演示信息增益的計算。
例1:假設有20位同學,其中有10位喜歡籃球,10位不喜歡籃球。在20位同學中有12位男同學,其中9位喜歡籃球,3位不喜歡籃球;有8位女同學,其中1位喜歡籃球,7位不喜歡籃球。那么性別(男/女)的信息增益是多少?
import numpy as np
def entropy(freq: list) -> float:
"""計算信息熵
"""
freq = np.array([i for i in freq if i > 0])
proba = freq / freq.sum()
entropy = - (proba * np.log2(proba)).sum()
return entropy
if __name__ == '__main__':
# 原始數據
like_basketball = [10, 10]
male_like_basketball = [9, 3]
female_like_basketball = [1, 7]
# 經驗熵
entropy_init = entropy(like_basketball)
# 條件熵
entropy_cond = 10 / 20 * entropy(male_like_basketball) + \
10 / 20 * entropy(female_like_basketball)
# 信息增益
info_gain = entropy_init - entropy_cond
print('經驗熵:{0}\n條件熵:{1}\n信息增益:{2}'.format(
entropy_init, entropy_cond, info_gain))
結果為:
經驗熵:1.0
條件熵:0.6774212838293646
信息增益:0.3225787161706354
2. 信息增益率
信息增益存在一個問題:當某個特征分類取值較多時,該特征的信息增益計算結果會放大。取極端情況,如有一個特征為編號,每個樣本對應了唯一的一個編號,這種情況下的信息純度很高,那么基于這個特征得到的信息增益就很大。
信息增益率可以解決信息增益的上述問題。特征對數據集
的信息增益率可以定義為其信息增益
與數據集
關于特征
取值的熵
的比值:
其中,
表示特征
的取值個數。C4.5算法是基于信息增益率進行特征選擇的。
我們仍以例1來演示,通過前文可知,一共有10名男同學和10名女同學,那么我們可以據此計算出:
gender_cnt = [12, 8]
entropy_gender = entropy(gender_cnt)
gain_rate = info_gain / entropy_gender
print('信息增益率:{0}'.format(gain_rate))
結果為:
信息增益率:0.33222979419649123
3. 基尼系數
基尼系數也是一種較好的特征選擇方法。假設樣本有個類,樣本屬于第
類的概率為
,則該樣本類別概率分布的基尼系數為:
對于給定訓練集,
是屬于第
類樣本的集合,則該訓練集的基尼指數為:
如果訓練集根據特征
的某一取值
劃分為
和
兩個部分,那么在特征
這個條件下,訓練集
的基尼系數為:
與信息熵的定義相似,訓練集的基尼指數
表示該集合的不確定性(不純度),
表示訓練集經過
的劃分后的不確定性(不純度)。在分類過程中,我們總是希望不確定性越低越好,即
越小越好。CART算法使用基尼系數來進行特征選擇。
仍以例1來演示基尼系數的計算。
def gini(freq: list) -> float:
"""計算基尼系數
"""
freq = np.array([i for i in freq if i > 0])
proba = freq / freq.sum()
g = 1 - (proba ** 2).sum()
return g
gini_male = 12 / 20 * gini(male_like_basketball) + 8 / 20 * gini(female_like_basketball)
print('基尼系數:{0}'.format(gini_male))
結果為:
基尼系數:0.3125
三、決策樹模型
三大經典決策樹模型分別為ID3、C4.5、CART,它們都是通過遞歸地選擇最優(yōu)特征來構建決策樹。如前文所述,在評估最優(yōu)特征時,它們分別使用了信息增益、信息增益率和基尼系數三個指標。
ID3和C4.5算法僅有決策樹的生成,不包含決策樹剪枝的部分,因此容易過擬合。CART算法除了用于分類外,還可用于回歸,也包含決策樹剪枝,因此現在應用更為廣泛。
1. ID3
ID3算法的全稱為Iterative Dichotomiser 3,即迭代二叉樹。其核心是基于信息增益遞歸地選擇最優(yōu)特征構造決策樹。
簡單來闡述,ID3算法的思路為:
- 首先預設一個決策樹根節(jié)點,然后對所有特征計算信息增益;
- 選擇一個信息增益最大的特征作為最優(yōu)特征,根據該特征的不同取值建立子結點;
- 接著對每個子結點遞歸地調用上述方法,直到信息增益很小或者沒有特征可選時,將這些子結點作為葉子結點,并以該葉子結點上的多數類作為預測類。
給定訓練集、特征集合
以及信息增益閾值
,ID3算法的流程如下:
- 如果
中所有實例屬于同一類別
,那么所構建的決策樹
為單結點樹,并且類
即為該結點的預測類。
- 如果
不是單結點樹,則計算特征集合
中各特征對
的信息增益,選擇信息增益最大的特征
。
- 如果
的信息增益小于閾值
,則將
視為單結點樹,并將
中所屬數量最多的類
作為該結點的預測類,并返回
。
- 否則,對
的每一取值
,按照
將
劃分為若干非空子集
,以
中的多數類作為預測類并構建子結點,由結點和子結點構成樹
并返回。
- 對第
個子結點,以
為訓練集,以
為特征集,遞歸地調用上述步驟,即可得到決策樹子樹
并返回。
2. C4.5
C4.5算法實際上是對ID3算法的改進。
- ID3算法使用信息增益做特征選擇,傾向于選擇取值水平較多的特征。針對這一問題,C4.5算法改為使用信息增益率。
- ID3算法不可以處理缺失值,C4.5算法可以。
- ID3算法不支持連續(xù)值特征,C4.5算法支持。
- ID3算法不支持后剪枝,C4.5算法支持后剪枝。
給定訓練集、特征集合
以及信息增益閾值
,C4.5算法的流程如下:
- 如果
中所有實例屬于同一類別
,那么所構建的決策樹
為單結點樹,并且類
即為該結點的預測類。
- 如果
不是單結點樹,則計算特征集合
中各特征對
的信息增益,選擇信息增益最大的特征
。
- 如果
的信息增益率小于閾值
,則將
視為單結點樹,并將
中所屬數量最多的類
作為該結點的預測類,并返回
。
- 否則,對
的每一取值
,按照
將
劃分為若干非空子集
,以
中的多數類作為預測類并構建子結點,由結點和子結點構成樹
并返回。
- 對第
個子結點,以
為訓練集,以
為特征集,遞歸地調用上述步驟,即可得到決策樹子樹
并返回。
3. CART
CART算法的全稱為分類與回歸樹(classification and regression tree),它既可用于分類,又可用于回歸,這是它與ID3/C4.5之間的主要區(qū)別之一,此處我們僅討論CART算法用于分類的場景。此外,CART算法中的特征選擇使用的是基尼系數。最后,CART算法不僅包含了決策樹的生成算法,還包括了決策樹的剪枝算法。
CART生成的決策樹為二叉樹,內部結點取值為“是”和“否”,這種方法等價于遞歸地二分每個特征,將特征空間劃分為有限個子空間,并在這些子空間上確定預測的概率分布,即前述的預測條件概率分布。
其算法流程為:
- 給定訓練集
和特征集
,對于每個特征
及其所有取值
,根據
將數據集劃分為
和
兩個部分,并計算
時的基尼系數。
- 取基尼系數最小的特征及其相應的劃分點作為最優(yōu)特征和最優(yōu)化分點,據此將當前結點劃分為兩個子結點,將訓練集根據特征取值分配到兩個子結點中。
- 對兩個子結點遞歸地調用上述步驟,直至滿足停止條件,最終生成CART分類決策樹。
4. 對比
| 決策樹 | 模型分類 | 樹結構 | 特征選擇 | 連續(xù)值處理 | 缺失值處理 | 剪枝處理 |
|---|---|---|---|---|---|---|
| ID3 | 分類 | 多叉樹 | 信息增益 | 不可以 | 不可以 | 不可以 |
| C4.5 | 分類 | 多叉樹 | 信息增益率 | 可以 | 可以 | 可以 |
| CART | 分類 | 二叉樹 | 基尼系數 | 可以 | 可以 | 可以 |
四、決策樹剪枝
決策樹剪枝一般包含兩種方法:預剪枝(pre-pruning)和后剪枝(post-pruning)。
1. 預剪枝
預剪枝,是指在決策樹生成過程中提前停止樹的增長的一種剪枝算法。其主要思路有:
- 提前設定決策樹的深度,當達到這一深度時,停止生長。
- 當某結點的所有樣本屬于同一類別,停止生長。
- 提前設定某個閾值,當某結點的樣本數小于該閾值時,停止生長。
- 提前設定某個閾值,當分裂帶來的性能提升小于該閾值時,停止生長。
預剪枝方法直接、簡單高效,適用于大規(guī)模求解問題。目前在主流的集成學習模型中,很多算法用到了預剪枝的思想。但因為決策樹的構建使用的是啟發(fā)式方法,具有局部最優(yōu)的問題,預剪枝提前停止樹的生長,存在一定的欠擬合風險。
2. 后剪枝
主流的后剪枝方法有四種:悲觀錯誤剪枝(Pessimistic Error Pruning,PEP),最小錯誤剪枝(Minimum Error Pruning,MEP),代價復雜度剪枝(Cost-Complexity Pruning,CCP)和基于錯誤的剪枝(Error-Based Pruning,EBP)。C4.5采用悲觀錯誤剪枝,CART采用代價復雜度剪枝。
后剪枝主要通過極小化決策樹整體損失函數來實現。前文我們提到,決策樹學習的目標是最小化如下損失函數:
其中,經驗熵可以表示為:
兩式合并有:
其中,為模型的經驗誤差項,
表示決策樹的復雜度(結點數),
為正則化參數,用于調控經驗誤差項和正則化項之間的權重關系。
決策樹后剪枝就是在正則化參數確定的情況下,選擇損失函數
最小的決策樹模型。給定算法生成的決策樹
和正則化參數
,后剪枝算法的流程如下:
- 計算每個樹節(jié)點的經驗熵
。
- 遞歸地自底向上回縮,假設一組葉子結點回縮到父節(jié)點前后的數分別為
和
,其對應的損失函數分別為
和
,如果
,則進行剪枝,將父節(jié)點變?yōu)樾碌娜~子結點。
- 重復上一步,直至得到損失函數最小的子樹
。
CART算法使用的正是后剪枝方法。CART后剪枝首先通過計算子樹的損失函數來實現剪枝并得到一個子樹序列,然后通過交叉驗證的方法從子樹序列中選取最優(yōu)子樹。
- 初始化
,最優(yōu)子樹集合
。
- 從葉子結點開始自下而上計算內部節(jié)點
的損失函數
、葉子結點數
,以及正則化閾值
,更新
。
- 得到所有節(jié)點的
值集合
。
- 從M中選擇最大的值
,自上而下地訪問子樹
的內部節(jié)點,如果
,則進行剪枝,并決定葉子結點
的預測值。
-
,
。
- 如果
不為空,則回到步驟4。否則已得到了所有的可選最優(yōu)子樹集合
。
- 采用交叉驗證在
選擇最優(yōu)子樹
。
五、優(yōu)缺點
1. 優(yōu)點
- 簡單直觀,生成的決策樹很直觀。
- 基本不需要預處理,不需要提前歸一化和處理缺失值。
- 使用決策樹預測的代價是
。
為樣本數。
- 既可以處理離散值也可以處理連續(xù)值。很多算法只是專注于離散值或者連續(xù)值。
- 可以處理多維度輸出的分類問題。
- 相比于神經網絡之類的黑盒分類模型,決策樹在邏輯上可以很好解釋。
- 可以交叉驗證的剪枝來選擇模型,從而提高泛化能力。
- 對于異常點的容錯能力好,健壯性高。
2. 缺點
- 決策樹算法非常容易過擬合,導致泛化能力不強??梢酝ㄟ^設置節(jié)點最少樣本數量和限制決策樹深度來改進。
- 決策樹會因為樣本發(fā)生一點的改動,導致樹結構的劇烈改變。這個可以通過集成學習之類的方法解決。
- 尋找最優(yōu)的決策樹是一個NP難題,我們一般是通過啟發(fā)式方法,容易陷入局部最優(yōu)。可以通過集成學習的方法來改善。
- 有些比較復雜的關系,決策樹很難學習,比如異或。這個就沒有辦法了,一般這種關系可以換神經網絡分類方法來解決。
- 如果某些特征的樣本比例過大,生成決策樹容易偏向于這些特征。這個可以通過調節(jié)樣本權重來改善。
六、代碼實戰(zhàn)
1. sklearn
在sklearn中,使用決策樹進行分類預測非常簡單,下面是一個來自官方文檔的例子。
from sklearn.tree import DecisionTreeClassifier
X = [[0, 0], [1, 1]]
Y = [0, 1]
clf = DecisionTreeClassifier()
clf = clf.fit(X, Y)
# 預測
print(clf.predict([[2, 2]])
# 預測概率
print(clf.predict_proba([[2, 2]])
我們還可以將決策樹通過可視化的方式呈現出來。
from sklearn.datasets import load_iris
from sklearn import tree
import matplotlib.pyplot as plt
# 以iris數據為例
iris = load_iris()
clf = tree.DecisionTreeClassifier()
clf = clf.fit(iris.data, iris.target)
# 可視化
plt.figure(figsize=(36, 24))
tree.plot_tree(clf, feature_names=iris.feature_names,
filled=True, proportion=True, fontsize=14)

2. PySpark
在PySpark中使用決策樹模型稍顯復雜。
import numpy as np
import pandas as pd
from pyspark.sql import SparkSession
from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.classification import DecisionTreeClassifier, RandomForestClassifier
from pyspark.ml.tuning import ParamGridBuilder, CrossValidator, TrainValidationSplit
from pyspark.ml.evaluation import BinaryClassificationEvaluator
spark = SparkSession.builder.appName('test').getOrCreate()
# 準備數據
# 數據可以從Hive中讀取,或從pandas.DataFrame格式創(chuàng)建等。
# 此處假設一份用于二分類預測模型訓練的數據已準備好,
data = YOUR_PYSPARK_DATAFRAME
features = YOUR_FEATURE_COLUMN_NAMES
label_col = YOUR_LABEL_COLUMNS
# 數據集分割
traindf, testdf = data.randomSplit([0.8, 0.2], seed=1)
# 特征向量化
vec_assembler = VectorAssembler(inputCols=features, outputCol='features')
# 決策樹
dtree = DecisionTreeClassifier(
seed=1,
labelCol=label_col,
featuresCol='features',
predictionCol='pred',
probabilityCol='proba',
maxDepth=5,
minInstancesPerNode=3,
impurity='gini',
maxBins=10
)
# 訓練模型
pipeline = Pipeline(stages=[vec_assembler, dtree])
model = pipeline.fit(traindf)
# 特征重要性
feat_importances = list(zip(features, model.stages[1].featureImportances))
df_importances = pd.DataFrame(sorted(feat_importances, key=lambda x: x[1], reverse=True),
columns=['feature', 'importances'])
df_importances.head()
# 預測
df_pred = model.transform(testdf)
to_array = F.udf(lambda x: x.toArray().tolist(), ArrayType(DoubleType()))
df_pred = df_pred.withColumn('proba_score', to_array('proba')[1])
我們還可以在PySpark中使用網格搜索來確定最佳參數。
# 特征向量化
vec_assembler = VectorAssembler(inputCols=features, outputCol='features')
# 隨機森林
dtree = DecisionTreeClassifier(
seed=1,
labelCol=label_col,
featuresCol='features',
predictionCol='pred',
probabilityCol='proba',
impurity='gini',
# maxDepth=5,
# minInstancesPerNode=3,
# maxBins=10
)
# 流水線
pipeline = Pipeline(stages=[vec_assembler, dtree])
# 設置網格參數
param_grid = ParamGridBuilder() \
.baseOn({dtree.labelCol:'label'}) \
.baseOn({dtree.featuresCol: 'features'}) \
.baseOn({dtree.predictionCol: 'pred'}) \
.baseOn({dtree.probabilityCol: 'proba'}) \
.addGrid(dtree.minInstancesPerNode, [3, 5, 7]) \
.addGrid(dtree.maxDepth, [10, 12, 15, 20]) \
.addGrid(dtree.maxBins, [5, 10, 15]) \
.build()
# 模型評估
evaluator = BinaryClassificationEvaluator()
# 交叉驗證
cv = CrossValidator(
estimator=pipeline,
estimatorParamMaps=param_grid,
evaluator=evaluator,
numFolds=5,
seed=1024
)
# 開始執(zhí)行
a = time.time()
cvModel = cv.fit(traindf)
b = time.time()
print(b - a)
# 打印最佳參數
params = cvModel.getEstimatorParamMaps()
avg_metrics = cvModel.avgMetrics
all_params = list(zip(params, avg_metrics))
best_param = sorted(all_params, key=lambda x: x[1], reverse=True)[0]
for p, v in best_param[0].items():
print("{}: {}".format(p.name, v))