摘要:要找到有情人,通常是一個漫長的過程。在scikit-learn中,如何模擬人類的決策過程?決策樹的目的是構建一顆最優的二叉分割樹,因此從根節點往下,每一個結點的條件選擇,都是為了讓樹結構最簡潔。
姻緣天注定,一段婚姻一段緣,是報恩報怨,還是討債還債,在一開始,誰都不知道。但可以確定一點,今生定會在一起,無論是長相廝守,還是短暫快樂。
佛曰:和有情人,做快樂事,別問是劫還是緣。
要找到有情人,通常是一個漫長的過程,也許會經歷很多場相親活動。時間久了,對相親也有更多的認識,并且積累和記錄一些數據,如下表:
序號 | 身高 | 房子 | 車子 | 長相 | 工作 | 約否 |
---|---|---|---|---|---|---|
1 | 1.80 | 有 | 無 | 6.5 | fact | 約 |
2 | 1.62 | 有 | 無 | 5.5 | IT | 約 |
3 | 1.71 | 無 | 有 | 8.5 | bank | 約 |
4 | 1.58 | 有 | 有 | 6.3 | bank | 約 |
5 | 1.68 | 無 | 有 | 5.1 | IT | 不約 |
6 | 1.63 | 有 | 無 | 5.3 | bank | 不約 |
7 | 1.78 | 無 | 無 | 4.5 | IT | 不約 |
8 | 1.64 | 無 | 無 | 7.8 | fact | 不約 |
9 | 1.65 | 無 | 有 | 6.6 | bank | 約嗎? |
前面8條數據為過往的相親記錄,最后字段為是否愿意繼續約會的記錄。如前4條數據,會愿意繼續約會,后面四條,直接pass。將這八條記錄作為訓練集,第9條是一條新數據,作為預測,決斷是否要繼續約會呢?
對數據進行一些處理,將其中的離散變量(類別變量)進行替換,保存成csv格式如下表。1表示有,0表示無,目標值的-1表示沒有意義,待預測。對于屬性“工作”列,使用字典:{'IT': 0, 'bank': 1, 'fact':2}進行映射。
height,house,car,handsome,job,is_date
1.80,1,0,6.5,2,1
1.62,1,0,5.5,0,1
1.71,0,1,8.5,1,1
1.58,1,1,6.3,1,1
1.68,0,1,5.1,0,0
1.63,1,0,5.3,1,0
1.78,0,0,4.5,0,0
1.64,0,0,7.8,2,0
1.65,0,1,6.6,0,-1
01 scikit-learn模擬
程序員喜歡用代碼說話,喜歡用代碼模擬,那么在scikit-learn中,如何模擬人類的決策過程呢?
# sklearn_dt.py
import pandas as pd
from sklearn.tree import DecisionTreeClassifier,export_graphviz
df = pd.read_csv('sklearn_data.csv')
train,test = df.query("is_date != -1"), df.query("is_date == -1")
y_train, X_train = train['is_date'],train.drop(['is_date'], axis=1)
X_test = test.drop(['is_date'], axis=1)
model = DecisionTreeClassifier(criterion='gini')
model.fit(X_train, y_train)
print model.predict(X_test)
上面代碼,使用pandas加載數據集,然后選擇出訓練集與測試集,倒數第三行構建了一個決策樹模型,全部使用默認參數,最后對測試數據進行預測。
需要注意一個參數,criterion='gini',這是決策樹構建的核心算法,參數“gini”指示了程序使用基尼不純度來進行構建決策樹。對于分類問題,scikit-learn還支持使用參數值為"entropy",表示使用熵的信息增益來構建決策樹。對于回歸問題,scikit-learn使用mse(均方誤差)來構建。
多次運行上面的程序,程序會在某些時刻輸出0,某些時間輸出1。
02 信息增益,基尼不純度
上面雖然只是用一行代碼便構建了一個決策樹,但實際上這一行代碼后面,做了很多事情,也就是決策樹構建的過程。
由前面的圖片可以看出,決策樹是一顆二叉樹,左右兩個分支,中間一個條件判斷,條件滿足走左分支,條件不滿足,走右分支。一層層往下,因此可以用遞歸過程來構建。唯一的問題,在當前的結點,應該選擇哪個條件來進行分割。
決策樹的目的是構建一顆最優的二叉分割樹,因此從根節點往下,每一個結點的條件選擇,都是為了讓樹結構最簡潔。換一句簡單的說,就是盡量的找出能使當前數據集盡量分開的條件。
專業術語來說,是使得數據集的不純度越來小,即純度越來越大,使得數據盡可能的分開;不純度也可以理解成數據集的混亂程度(熵),數據越混亂,不純度越高,熵越大,也即不確定性越大。比如數據集里面,約會和不約會的概率都是0.5,那么表示不確定性很多,即不純度很大,純度很小。
我們的目的,就是找到一個條件進行分割,使得這種不確定性減小,如長相小于等于5.4的數據,一定不約,其不純度為0,表示完全純凈了,都是不約,沒有不確定性。
決策樹常用的算法有ID3,C4.5,C5.0,CART,其中前面三種,都是用的熵的信息增益(率)來表示。最后一個CART(Classification And Regression Tree),即分類回歸樹,Scikit-learn實現的便是這種算法,從名字知道,既能用于分類,也用于回歸問題,其中分類問題可以使用基尼(Gini)不純度和熵的信息增益,回歸問題使用了方差降低(Variance Reduction,同mse的均方誤差)的算法。
維基上面有各種算法的數學公式,對分類問題,都是基于各個類別的概率的簡單計算。在構建樹的時候,算法會嘗試在當前數據集的所有特征上進行切分,找到概率計算出來的最優切分,并將當前條件做為切分點。
以上面的數據為例,使用gini不純度進行分割,最開始數據集的不純度為0.5(根結點的impurity=0.5),在嘗試了所有將數據切分一分為二(比如,切分按是否有房切分,長相大于7劃分,長相小于5劃分)的條件后,發現handsome<=5.4的劃分,是最優的劃分。
因此,決策樹的構建過程,主要為兩個步驟,一是數據二叉劃分,不同的實現方法,有不同的數據劃分,對離散變量,通常是按集合的方法,將數據集劃分成兩類,比如{'bank', 'IT', 'fact'}這個集合,通常會劃分成{'bank'}, {'IT','fact'};{'IT'},{'bank','fact'};{'bank','IT'},{'fact'}這三個。而對于連續型數據,{3.8, 4.5, 7.8}這樣的集體,則會按兩個數的平均值進行劃分。
數據分割完后,使用一種度量方法,來計算當前結點應該選擇哪一個條件進行最優切分,選中的條件,即為當前的決策。
03 決策之樹,決策過程
從上面的算法中,可以簡單理解為,決策樹就是找到當前最優的條件進行將數據一分為二,最優的條件即為當前的決策。
使用圖表,來分析具體的決策過程,在scikit-learn程序中,添加如下代碼:
export_graphviz(model.tree_,
out_file='tree.dot',
feature_names=df.columns,
max_depth=None,
# 下面幾個參數,需要使用最新的scikit-learn 0.17版本才能用
class_names=["is_date:no", "is_date:yes"],
rounded=True,
filled=True,
)
對生成的決策樹圖片的說明:
- 第一行為決策條件(非葉子結點),比如根節點handsome<=5.4,條件為真走左邊,為假走右邊。
- impurity表示當前數據集的基尼不純度
- samples表示當前數據集的樣本數
- value表示當前數據集各個類別(按類別順序從小到大排序,第0類,第1類……)的數量,如果value中的值相等,當前節點沒有著色,為白色。
- class表示當前數據集中的多數類別,如果value中各個值相等,則應該是按順序取值。
運行上面的代碼,會輸出一個tree.dot的文件,再使用如下的命令,可以生成一個圖片(開篇圖片左邊部分):
$ sudo apt-get install graphviz # 需要安裝程序
$ dot -Tpng tree.dot -o tree.png
分析圖片的數據,總結出決策規則如下:
- 長相小于等于5.4的,一定不約;
- 不滿足前面條件,但:有房子的,一定約;
-
不滿足前面條件,但:有車,約,沒有車的,不約;
知識星球.jpeg
對待測數據,使用上面的規則:
- 長相大于5.4,不滿足規則1,繼續下一條;
- 沒有房子,不滿足規則2,繼續下一條規則;
- 有車,符合第3條規則,約。
04 spark模擬
上面的決策過程,換成簡單的程序思維來表達,就是if-else的條件判斷,使用spark的實現的決策樹,可以打印出來if-else的條件決策過程:
# spark_dt.py
from pprint import pprint
from pyspark import SparkContext
from pyspark.mllib.tree import DecisionTree
from pyspark.mllib.regression import LabeledPoint
sc = SparkContext()
data = sc.textFile('spark_data.csv').map(lambda x: x.split(',')).map(lambda x: (float(x[0]), int(x[1]), int(x[2]), float(x[3]), int(x[4]), int(x[5])))
train = data.filter(lambda x: x[5]!=-1).map(lambda v: LabeledPoint(v[-1], v[:-1]))
test = data.filter(lambda x: x[5]==-1)#.map(lambda v: LabeledPoint(v[-1], v[:-1]))
model = DecisionTree.trainClassifier(train,
numClasses=2,
categoricalFeaturesInfo={1:2, 2:2, 4:3},
impurity='gini',
maxDepth=5,
)
print 'The predict is:', model.predict(test).collect()
print 'The Decision tree is:', model.toDebugString()
通過上面的model.toDebugString(),打印出決策的過程,見開篇圖片右邊部分。
05 結尾
上面演示了兩種不同的決策樹實現,scikit-learn和spark,在演示數據上,輸出可能會不同,因為各自的實現是有差別的。主要在于對離散變量(工作屬性)的處理和數據的分割上面。
決策樹還會涉及剪枝的問題,完全生成的決策樹會伴隨著數據的噪聲導致過擬合,實際應用通常使用隨機森林來防止過擬合。關于隨機森林,請持續關注。