決策樹和 K 近鄰分類
機器學(xué)習(xí)介紹
假設(shè)用 P 來評估計算機程序在某任務(wù)類 T 上的性能,若一個程序利用經(jīng)驗 E 在任務(wù) T 上獲得了性能改善,則我們就說關(guān)于 T 和 P, 該程序?qū)?E 進行了學(xué)習(xí)。
在不同的問題設(shè)定下,T、P、E 可能指完全不同的東西。機器學(xué)習(xí)中一些流行的任務(wù) T 包括:
分類:基于特征將實例分為某一類。
回歸:基于實例的其他特征預(yù)測該實例的數(shù)值型目標特征。
聚類:基于實例的特征實現(xiàn)實例的分組,從而讓組內(nèi)成員比組間成員更為相似。
異常檢測:尋找與其他樣本或組內(nèi)實例有很大區(qū)別的實例。
經(jīng)驗 E 指的是數(shù)據(jù)(沒有數(shù)據(jù)我們什么也干不了)。根據(jù)訓(xùn)練方式,機器學(xué)習(xí)算法可以分為監(jiān)督(supervised)和無監(jiān)督(unsupervised)兩類。無監(jiān)督學(xué)習(xí)需要訓(xùn)練含有很多特征的數(shù)據(jù)集,然后學(xué)習(xí)出這個數(shù)據(jù)集上有用的結(jié)構(gòu)性質(zhì)。而監(jiān)督學(xué)習(xí)的數(shù)據(jù)集除了含有很多特征外,它的每個樣本都要有一個標簽(label)或目標(target)。
示例
分類和回歸屬于監(jiān)督學(xué)習(xí)問題。例如,作為信貸機構(gòu),我們可能希望根據(jù)客戶累積的數(shù)據(jù)預(yù)測貸款違約情況。在這里,經(jīng)驗 E 是已有的訓(xùn)練數(shù)據(jù),即實例(客戶)的集合,一組特征(例如年齡、薪水、貸款類型、以往違約記錄等),一個目標變量(他們是否會違約)。由于需要預(yù)測的目標變量是「他們是否會違約」,所以這是一個二元分類問題。如果你轉(zhuǎn)而預(yù)測貸款會超期多久,那么需要預(yù)測的目標變量變成了一個連續(xù)值(時間),這就成為一個回歸問題了。
決策樹
決策樹是分類與回歸問題中常用的方法之一。其實不僅是機器學(xué)習(xí)領(lǐng)域,在每天的日常決策中,我們都在使用決策樹。流程圖實際上就是決策樹的可視化表示,例如,下面是俄羅斯國立高等經(jīng)濟研究大學(xué)(Higher School of Economics)提供的關(guān)于「如何在學(xué)院網(wǎng)站上發(fā)表論文」的流程圖:

用機器學(xué)習(xí)的術(shù)語來說,可以把它看成一個簡單的分類器,根據(jù)內(nèi)容(書、小冊子、論文)、新聞類型、原發(fā)表物類型(科學(xué)期刊、通訊)等來確定合適的發(fā)表類型(書、文章、書的章節(jié)、預(yù)印本、Higher School of Economics and the Media 稿件)。
決策樹常常是專家經(jīng)驗的概括,是一種分享特定過程知識的方式。例如,在引入可擴展機器學(xué)習(xí)算法之前,銀行業(yè)的信用評分任務(wù)是由專家解決的,能否放貸是基于一些直觀(或經(jīng)驗)的規(guī)則,這些規(guī)則就可以表示為決策樹的形式,如下圖所示:

作為機器學(xué)習(xí)算法的決策樹基本上和上圖差不多,它合并一連串邏輯規(guī)則,使之成為一個樹形的數(shù)據(jù)結(jié)構(gòu),這些規(guī)則的形式為「特征 a 的值小于 x,特征 b 的值小于 y … => 類別 1」。
如何構(gòu)建決策樹
年齡、房產(chǎn)、收入、教育,這么多的特征首先應(yīng)該關(guān)注哪個呢?
為了回答上述問題,先看一個簡單的游戲,即「20個問題」游戲,這個游戲是這樣玩的:A 心里想著一個名人,B 問 A 20 個問題,A 只能回答「是」或「否」,20 個問題之后 B 要猜出 A 心里想的那個名人是誰。首先問一個可以最大程度壓縮剩余選項數(shù)目的問題會使 B 占據(jù)極大優(yōu)勢,例如詢問「是不是安吉麗娜·朱莉?」,最多剔除一個選項,而詢問「這個名人是女人嗎?」將消除大約一半的選項。就是說,「性別」特征相比「安吉麗娜·朱莉」、「西班牙人」、「喜歡足球」等其他特征更能區(qū)分名人數(shù)據(jù)集。
熵
熵是一個在物理、信息論和其他領(lǐng)域中廣泛應(yīng)用的重要概念,可以衡量獲得的信息量。對于具有 N 種可能狀態(tài)的系統(tǒng)而言,熵的定義如下:

其中,pi是系統(tǒng)位于第 i 個狀態(tài)的概率。熵可以描述為系統(tǒng)的混沌程度,熵越高,系統(tǒng)的有序性越差,反之亦然。熵將幫助我們高效的分割數(shù)據(jù),類似幫助我們找出在「20個問題」游戲中先問什么問題較好。
玩具示例
為了解釋熵是如何有利于構(gòu)建決策樹模型的,讓我們來看一個玩具示例,在這個示例中將基于球的位置預(yù)測它的顏色。


將球分為「位置小于等于 12、位置大于 12」這兩組,如下圖所示。


可見,兩組的熵都下降了,且右邊這組降得更多。由于熵實際上是系統(tǒng)混沌(或不確定)的程度,熵的下降被稱為信息增益。數(shù)學(xué)上,基于變量 Q(在這個例子中是變量「x ≤ 12」)所作的分割,得到的信息增益(IG)定義為:

其中,q 是分割的組數(shù),Ni是變量 Q 等于第 i 項時的樣本數(shù)目。在玩具示例中,有 2 個組(q=2),一組有 13 個元素(N 1=13),另一組有 7 個(N2=7)。因此,信息增益為:

結(jié)果表明,根據(jù)「坐標小于或等于12」將球分為兩組帶來了一個更有序的系統(tǒng)。讓我們繼續(xù)分組,直到每組中的球顏色都一樣。


決策樹構(gòu)建算法
在之前的例子中構(gòu)建的決策樹是最優(yōu)的:它只需提 5 個「問題」(基于變量 Q),就完全擬合了訓(xùn)練集。其他分割條件會使得到的樹更深,即需要更多「問題」才能獲得答案。
構(gòu)建決策樹的流行算法(如 ID3 或 C4.5)的核心,是貪婪最大化信息增益:在每一步,算法都會選擇能在分割后給出最大信息增益的變量。接著遞歸重復(fù)這一流程,直到熵為零(或者,為了避免過擬合,直到熵為某個較小的值)。不同的算法使用不同的推斷,通過「提前停止」或「截斷」以避免構(gòu)建出過擬合的樹。
分類問題中其他的分割質(zhì)量標準
上面我們討論了熵是如何衡量樹的分區(qū)的,但還有其他指標來衡量分割的好壞:

實踐中幾乎從不使用錯分率,而基尼不確定性和信息增益的效果差不多。
二元分類問題的熵和基尼不確定性為:

其中p+是對象具有標簽 + 的概率。以p+為坐標,繪制上面兩個函數(shù)的圖像。
import warnings
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
sns.set()
warnings.filterwarnings('ignore')
plt.figure(figsize=(6, 4))
xx = np.linspace(0, 1, 50)
plt.plot(xx, [2 * x * (1-x) for x in xx], label='gini')
plt.plot(xx, [4 * x * (1-x) for x in xx], label='2*gini')
plt.plot(xx, [-x * np.log2(x) - (1-x) * np.log2(1 - x)
for x in xx], label='entropy')
plt.plot(xx, [1 - max(x, 1-x) for x in xx], label='missclass')
plt.plot(xx, [2 - 2 * max(x, 1-x) for x in xx], label='2*missclass')
plt.xlabel('p+')
plt.ylabel('criterion')
plt.title('Criteria of quality as a function of p+ (binary classification)')
plt.legend()
上圖可見,熵的圖像和兩倍的基尼不確定性圖像非常接近。因此,在實踐中,這兩個指標的效果基本上是一樣的。
示例
下面用一棵決策樹擬合一些合成數(shù)據(jù)。這些合成數(shù)據(jù)屬于兩個不同的類別,這兩個類別的均值不同,但都呈現(xiàn)正態(tài)分布。
# 第一類
np.random.seed(17)
train_data = np.random.normal(size=(100, 2))
train_labels = np.zeros(100)
# 第二類
train_data = np.r_[train_data, np.random.normal(size=(100, 2), loc=2)]
train_labels = np.r_[train_labels, np.ones(100)]
下面繪制數(shù)據(jù)。通俗地講,這種情況下的分類問題就是構(gòu)造一個「邊界」,能夠較好的分開兩個類別(紅點和黃點)。這個「邊界」若是一條直線的話可能太過簡單,若是沿著每個紅點畫出的蛇形曲線又太過復(fù)雜(這將導(dǎo)致其在新數(shù)據(jù)上的表現(xiàn)很差)。從直覺上說,某種平滑的邊界,在新數(shù)據(jù)上的效果會比較好。
plt.figure(figsize=(10, 8))
plt.scatter(train_data[:, 0], train_data[:, 1], c=train_labels, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.plot(range(-2, 5), range(4, -3, -1))
下面訓(xùn)練一棵 sklearn 決策樹,區(qū)分這兩類數(shù)據(jù)點。最后可視化所得的邊界。
from sklearn.tree import DecisionTreeClassifier
# 編寫一個輔助函數(shù),返回之后的可視化網(wǎng)格
def get_grid(data):
x_min, x_max = data[:, 0].min() - 1, data[:, 0].max() + 1
y_min, y_max = data[:, 1].min() - 1, data[:, 1].max() + 1
return np.meshgrid(np.arange(x_min, x_max, 0.01), np.arange(y_min, y_max, 0.01))
# max_depth參數(shù)限制決策樹的深度
clf_tree = DecisionTreeClassifier(criterion='entropy', max_depth=3,
random_state=17)
# 訓(xùn)練決策樹
clf_tree.fit(train_data, train_labels)
# 可視化
xx, yy = get_grid(train_data)
predicted = clf_tree.predict(np.c_[xx.ravel(),
yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(train_data[:, 0], train_data[:, 1], c=train_labels, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
通過 pydotplus 和 export_graphviz 庫我們可以方便的看到?jīng)Q策樹本身是怎樣的。使用 StringIO() 函數(shù)開辟一個緩存空間保存決策樹,通過 export_graphviz() 函數(shù)以 DOT 格式導(dǎo)出決策樹的 GraphViz 表示,然后將其寫入 out_file 中。使用 graph_from_dot_data() 函數(shù)讀入數(shù)據(jù)并通過 Image() 函數(shù)顯示決策樹。
from ipywidgets import Image
from io import StringIO
import pydotplus
from sklearn.tree import export_graphviz
dot_data = StringIO()
export_graphviz(clf_tree, feature_names=['x1', 'x2'],
out_file=dot_data, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(value=graph.create_png())
上圖表明,在最深的一層,樹將空間「切割」為8個矩形,也就是說,樹有8個葉節(jié)點。在每個矩形之中,樹將根據(jù)數(shù)量較多的對象的標簽做出預(yù)測。
我們?nèi)绾巍缸x懂」這顆決策樹?
上個示例中,總共有 200 個合成數(shù)據(jù)(樣本),每個分類各有 100 個合成數(shù)據(jù)。初始狀態(tài)的熵是最大的,即 S=1。接著,通過比較x2與 1.211 的大小進行第一次分割,將樣本分成兩組(你可以在上圖中找到這一部分邊界)?;谶@一次分割,左右兩組的熵都下降了。這一過程持續(xù)進行,直到樹的深度達到 3。在上圖中,屬于第一類的樣本數(shù)量越多,該節(jié)點的橙色就越深,屬于第二類的樣本越多,該節(jié)點的藍色就越深。若兩類樣本的數(shù)量相等,則為白色,比如根節(jié)點的兩類樣本數(shù)量相同,所以它是白色的
決策樹如何應(yīng)用到數(shù)值特征?
假設(shè)有一個數(shù)值特征「年齡」,該特征有大量的唯一值。決策樹將通過查看「年齡 < 17」、「年齡 < 22.87」這樣的二元屬性尋找最好的分割,分割的好壞由某種信息增益標準衡量。但在構(gòu)建樹的每一步中,會有過多的二元屬性可供選擇,比如「薪水」同樣能以很多方式進行分割,為了解決這一問題,我們經(jīng)常使用啟發(fā)式算法來限制選擇的屬性數(shù)量。
看一個例子,假設(shè)有如下數(shù)據(jù)集:
data = pd.DataFrame({'Age': [17, 64, 18, 20, 38, 49, 55, 25, 29, 31, 33],
'Loan Default': [1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1]})
data
使用 sort_values() 方法根據(jù)年齡進行升序排列。
data.sort_values('Age')
訓(xùn)練一個決策樹模型,并可視化。
age_tree = DecisionTreeClassifier(random_state=17)
age_tree.fit(data['Age'].values.reshape(-1, 1), data['Loan Default'].values)
dot_data = StringIO()
export_graphviz(age_tree, feature_names=['Age'],
out_file=dot_data, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(value=graph.create_png())
上圖可見,該決策樹使用以下 5 個值來評估年齡:43.5、19、22.5、30、32。如果你仔細觀察,你會發(fā)現(xiàn)它們就是目標變量(Loan Default)出現(xiàn)變化(從 1「切換」到 0 或從 0「切換」到 1)時那兩個年齡的平均值。比如,一個 38 歲的客戶沒能償還貸款(目標變量為 1),而一個 49 歲的客戶還貸了(目標變量為 0),那么樹使用的評估值就是 38 和 49 的均值,即 43.5。樹尋找那些目標變量發(fā)生變化的值,以此作為「切割」的閾值。
下面考慮一個更復(fù)雜的例子,把「薪水」變量(以千美元每年為單位)加入數(shù)據(jù)集。
data2 = pd.DataFrame({'Age': [17, 64, 18, 20, 38, 49, 55, 25, 29, 31, 33],
'Salary': [25, 80, 22, 36, 37, 59, 74, 70, 33, 102, 88],
'Loan Default': [1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1]})
data2.sort_values('Age')
上表可見,如果根據(jù)年齡排序,目標變量(Loan Default)將切換(從 1 到 0 或從 0 到 1)5 次。
下表可見,如果根據(jù)薪水排序,它將切換 7 次。
data2.sort_values('Salary')
下面看看樹將如何選擇特征。
age_sal_tree = DecisionTreeClassifier(random_state=17)
age_sal_tree.fit(data2[['Age', 'Salary']].values, data2['Loan Default'].values)
dot_data = StringIO()
export_graphviz(age_sal_tree, feature_names=['Age', 'Salary'],
out_file=dot_data, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(value=graph.create_png())
上圖表明,樹同時根據(jù)薪水和年齡進行分區(qū),有些節(jié)點的分割閾值選擇了年齡,有些選擇了薪水。樹為何選擇這些特征?因為根據(jù)基尼不確定性質(zhì)量標準,它們提供了更好的分區(qū)。
結(jié)論:決策樹處理數(shù)值特征最簡單的啟發(fā)式算法是升序排列它的值,然后只關(guān)注目標變量發(fā)生改變的那些值。
此外,當數(shù)據(jù)集具有大量數(shù)值特征,且每個特征具有大量唯一值時,只選擇最高的N個閾值,即,僅僅使用提供最高增益的前N個值。這一過程可以看成是構(gòu)造了一棵深度為 1 的樹,計算熵(或基尼不確定性),然后選擇最佳閾值用于比較。比方說,如果我們根據(jù)「薪水 ≤ 34.5」分割,左子組的熵為 0(所有客戶都是「不好的」),而右邊的熵為 0.954(3 個「不好的」,5 個「好的」,你可以自行確認這一點,這將作為作業(yè)的一部分),信息增益大概是 0.3。如果我們根據(jù)「薪水 ≤ 95」分割,左邊的子組的熵會是 0.97(6 個「不好的」,4 個「好的」),而右邊的熵會是 0(該組只包含 1 個對象),信息增益大約是 0.11。如果以這樣的方式計算每種分區(qū)的信息增益,那么在使用所有特征構(gòu)造一棵大決策樹之前就可以選出每個數(shù)值特征的閾值。
樹的關(guān)鍵參數(shù)
理論上講,我們可以構(gòu)建一個決策樹,直到每個葉節(jié)點只有一個實例,但這樣做容易過擬合,導(dǎo)致其在新數(shù)據(jù)上的表現(xiàn)不佳。如果你這么做,在樹的最深處,可能會存在由無關(guān)緊要的特征組成的分區(qū),例如根據(jù)「客戶褲子的顏色」這一特征進行分區(qū),這是我們不希望發(fā)生。
但在兩種情況下,樹可以被構(gòu)建到最大深度(每個葉節(jié)點只有一個實例):
隨機森林。它將構(gòu)建為最大深度的單個樹的響應(yīng)進行平均。
決策樹修剪。在這種方法中,樹首先被構(gòu)造成最大深度。然后,從底部開始,基于交叉驗證來比較有分區(qū)/無分區(qū)情形下樹的質(zhì)量情況,進而移除樹的一些節(jié)點。
下圖是過擬合的決策樹給出的分界。

常見的解決決策樹過擬合的方法為:
人工限制深度或葉節(jié)點的最少樣本數(shù)。
對樹進行剪枝。
scikit-learn 的 DecisionTreeClassifier 類
sklearn.tree.DecisionTreeClassifier 類的主要參數(shù)為:
max_depth 樹的最大深度;
max_features 搜索最佳分區(qū)時的最大特征數(shù)(特征很多時,設(shè)置這個參數(shù)很有必要,因為基于所有特征搜索分區(qū)會很「昂貴」);
min_samples_leaf 葉節(jié)點的最少樣本數(shù)。
樹的參數(shù)需要根據(jù)輸入數(shù)據(jù)設(shè)定,通常通過交叉驗證可以確定參數(shù)范圍。
回歸問題中的決策樹
當對數(shù)值變量進行預(yù)測時,我們構(gòu)造決策樹的思路和分類問題時所用的思路是一樣的,但衡量決策樹好壞的質(zhì)量標準改變了,現(xiàn)在它的質(zhì)量標準如下:

其中,? 是葉節(jié)點中的樣本數(shù),yi是目標變量的值。簡單來說,通過最小化方差,使每個葉子中的目標特征的值大致相等,以此來劃分訓(xùn)練集的特征。
示例
讓我們基于以下函數(shù)生成一些帶噪數(shù)據(jù):

接著在生成的數(shù)據(jù)上訓(xùn)練一顆決策樹,并進行預(yù)測,調(diào)用 plt 方法畫出結(jié)果示意圖。
from sklearn.tree import DecisionTreeRegressor
n_train = 150
n_test = 1000
noise = 0.1
def f(x):
x = x.ravel()
return np.exp(-x ** 2) + 1.5 * np.exp(-(x - 2) ** 2)
def generate(n_samples, noise):
X = np.random.rand(n_samples) * 10 - 5
X = np.sort(X).ravel()
y = np.exp(-X ** 2) + 1.5 * np.exp(-(X - 2) ** 2) + \
np.random.normal(0.0, noise, n_samples)
X = X.reshape((n_samples, 1))
return X, y
X_train, y_train = generate(n_samples=n_train, noise=noise)
X_test, y_test = generate(n_samples=n_test, noise=noise)
reg_tree = DecisionTreeRegressor(max_depth=5, random_state=17)
reg_tree.fit(X_train, y_train)
reg_tree_pred = reg_tree.predict(X_test)
plt.figure(figsize=(10, 6))
plt.plot(X_test, f(X_test), "b")
plt.scatter(X_train, y_train, c="b", s=20)
plt.plot(X_test, reg_tree_pred, "g", lw=2)
plt.xlim([-5, 5])
plt.title("Decision tree regressor, MSE = %.2f" %
(np.sum((y_test - reg_tree_pred) ** 2) / n_test))
plt.show()
上圖表明,決策樹使用分段的常數(shù)函數(shù)逼近數(shù)據(jù)。
最近鄰方法
最近鄰方法(K 近鄰或 k-NN)是另一個非常流行的分類方法。當然,也可以用于回歸問題。和決策樹類似,這是最容易理解的分類方法之一。這一方法遵循緊密性假說:如果樣本間的距離能以足夠好的方法衡量,那么相似的樣本更可能屬于同一分類。
比如,根據(jù)最近鄰方法,下圖中的綠球?qū)⒈环诸悶椤杆{色」而不是「紅色」,因為它與藍球的距離更近。

再舉一個例子,如果你不知道藍牙耳機屬于什么類別,你可以查找 5 個相似的耳機,如果其中 4 個標記為「配件」類,只有 1 個標記為「科技」類,那么根據(jù)最近鄰方法,它屬于「配件」類。
在最近鄰方法中,為了對測試集中的每個樣本進行分類,需要依次進行以下操作:
計算訓(xùn)練集中每個樣本之間的距離。
從訓(xùn)練集中選取 k 個距離最近的樣本。
測試樣本的類別將是它 k 個最近鄰中最常見的分類。
在回歸問題中應(yīng)用最近鄰方法很簡單,僅需將上述步驟做一個小小的改動:第三步不返回分類,而是返回一個數(shù)字,即目標變量在鄰居中的均值或中位數(shù)。
這一方式的顯著特點是它具有惰性:當需要對測試樣本進行分類時,計算只在預(yù)測階段進行。由于這種特點,最近鄰方法事先并不基于訓(xùn)練樣本創(chuàng)建模型,這與上文提到的決策樹不同。決策樹是基于訓(xùn)練集構(gòu)建的,在預(yù)測階段僅通過遍歷決策樹就可以實現(xiàn)快速地分類。
最近鄰方法的實際應(yīng)用
在某些案例中,k-NN 可以作為一個模型的基線。
在 Kaggle 競賽中,k-NN 常常用于構(gòu)建元特征(即 k-NN 的預(yù)測結(jié)果作為其他模型的輸入),或用于堆疊/混合。
最近鄰方法還可以擴展到推薦系統(tǒng)等任務(wù)中。
在大型數(shù)據(jù)集上,常常使用逼近方法搜索最近鄰。
k-NN 分類/回歸的效果取決于一些參數(shù):
鄰居數(shù) k。
樣本之間的距離度量(常見的包括 Hamming,歐幾里得,余弦和 Minkowski 距離)。注意,大部分距離要求數(shù)據(jù)在同一尺度下,例如「薪水」特征的數(shù)值在千級,「年齡」特征的數(shù)值卻在百級,如果直接將他們丟進最近鄰模型中,「年齡」特征就會受到比較大的影響。
鄰居的權(quán)重(每個鄰居可能貢獻不同的權(quán)重,例如,樣本越遠,權(quán)重越低)。
scikit-learn 的 KNeighborsClassifier 類
sklearn.neighbors.KNeighborsClassifier 類的主要參數(shù)為:
weights:可設(shè)為 uniform(所有權(quán)重相等),distance(權(quán)重和到測試樣本的距離成反比),或任何其他用戶自定義的函數(shù)。
algorithm(可選):可設(shè)為 brute、ball_tree、KD_tree、auto。若設(shè)為 brute,通過訓(xùn)練集上的網(wǎng)格搜索來計算每個測試樣本的最近鄰;若設(shè)為 ball_tree 或 KD_tree,樣本間的距離儲存在樹中,以加速尋找最近鄰;若設(shè)為 auto,將基于訓(xùn)練集自動選擇合適的尋找最近鄰的方法。
leaf_size(可選):若尋找最近鄰的算法是 BallTree 或 KDTree,則切換為網(wǎng)格搜索所用的閾值。
metric:可設(shè)為 minkowski、manhattan、euclidean、chebyshev 或其他。
選擇模型參數(shù)和交叉驗證
機器學(xué)習(xí)算法的主要任務(wù)是可以「泛化」未曾見過的數(shù)據(jù)。由于我們無法立刻得知模型在新數(shù)據(jù)上的表現(xiàn)(因為還不知道目標變量的真值),因此有必要犧牲一小部分數(shù)據(jù),來驗證模型的質(zhì)量,即將一小部分數(shù)據(jù)作為留置集。
通常采用下述兩種方法之一來驗證模型的質(zhì)量:
留置法。保留一小部分數(shù)據(jù)(一般是 20% 到 40%)作為留置集,在其余數(shù)據(jù)上訓(xùn)練模型(原數(shù)據(jù)集的 60%-80%),然后在留置集上驗證模型的質(zhì)量。
交叉驗證。最常見的情形是 k 折交叉驗證,如下圖所示。

在 k 折交叉驗證中,模型在原數(shù)據(jù)集的 K-1K?1 個子集上進行訓(xùn)練(上圖白色部分),然后在剩下的 1 個子集上驗證表現(xiàn),重復(fù)訓(xùn)練和驗證的過程,每次使用不同的子集(上圖橙色部分),總共進行 K 次,由此得到 K 個模型質(zhì)量評估指數(shù),通常用這些評估指數(shù)的求和平均數(shù)來衡量分類/回歸模型的總體質(zhì)量。
相比留置法,交叉驗證能更好地評估模型在新數(shù)據(jù)上的表現(xiàn)。然而,當你有大量數(shù)據(jù)時,交叉驗證對機器計算能力的要求會變得很高。
交叉驗證是機器學(xué)習(xí)中非常重要的技術(shù),同時也應(yīng)用于統(tǒng)計學(xué)和經(jīng)濟學(xué)領(lǐng)域。它有助于我們進行超參數(shù)調(diào)優(yōu)、模型比較、特征評估等其他重要操作。
應(yīng)用樣例
在客戶離網(wǎng)率預(yù)測任務(wù)中使用決策樹和最近鄰方法
首先讀取數(shù)據(jù)至 DataFrame 并進行預(yù)處理。將 State 特征從 DateFrame 轉(zhuǎn)移到單獨的 Series 對象中。我們訓(xùn)練的第一個模型將不包括 State 特征,之后再考察 State 特征是否有用。
df = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/telecom_churn.csv')
df['International plan'] = pd.factorize(df['International plan'])[0]
df['Voice mail plan'] = pd.factorize(df['Voice mail plan'])[0]
df['Churn'] = df['Churn'].astype('int')
states = df['State']
y = df['Churn']
df.drop(['State', 'Churn'], axis=1, inplace=True)
df.head()
之后將數(shù)據(jù)集的 70% 劃分為訓(xùn)練集(X_train,y_train),30% 劃分為留置集(X_holdout,y_holdout)。留置集的數(shù)據(jù)在調(diào)優(yōu)模型參數(shù)時不會被用到,在調(diào)優(yōu)之后,用它評定所得模型的質(zhì)量。
接下來,訓(xùn)練 2 個模型:決策樹和 k-NN。一開始,我們并不知道如何設(shè)置模型參數(shù)能使模型表現(xiàn)好,所以可以使用隨機參數(shù)方法,假定樹深(max_dept)為 5,近鄰數(shù)量(n_neighbors)為 10。
from sklearn.model_selection import train_test_split, StratifiedKFold
from sklearn.neighbors import KNeighborsClassifier
X_train, X_holdout, y_train, y_holdout = train_test_split(df.values, y, test_size=0.3,
random_state=17)
tree = DecisionTreeClassifier(max_depth=5, random_state=17)
knn = KNeighborsClassifier(n_neighbors=10)
tree.fit(X_train, y_train)
knn.fit(X_train, y_train)
使用準確率(Accuracy)在留置集上評價模型預(yù)測的質(zhì)量。
from sklearn.metrics import accuracy_score
tree_pred = tree.predict(X_holdout)
accuracy_score(y_holdout, tree_pred)
knn_pred = knn.predict(X_holdout)
accuracy_score(y_holdout, knn_pred)
從上可知,決策樹的準確率約為 94%,k-NN 的準確率約為 88%,于是僅使用我們假定的隨機參數(shù)(即沒有調(diào)參),決策樹的表現(xiàn)更好。
現(xiàn)在,使用交叉驗證確定樹的參數(shù),對每次分割的 max_dept(最大深度 h)和 max_features(最大特征數(shù))進行調(diào)優(yōu)。GridSearchCV() 函數(shù)可以非常簡單的實現(xiàn)交叉驗證,下面程序?qū)γ恳粚?max_depth 和 max_features 的值使用 5 折驗證計算模型的表現(xiàn),接著選擇參數(shù)的最佳組合。
from sklearn.model_selection import GridSearchCV, cross_val_score
tree_params = {'max_depth': range(5, 7),
'max_features': range(16, 18)}
tree_grid = GridSearchCV(tree, tree_params,
cv=5, n_jobs=-1, verbose=True)
tree_grid.fit(X_train, y_train)
列出交叉驗證得出的最佳參數(shù)和相應(yīng)的訓(xùn)練集準確率均值。
tree_grid.best_params_
tree_grid.best_score_
accuracy_score(y_holdout, tree_grid.predict(X_holdout))
繪制所得的決策樹。
dot_data = StringIO()
export_graphviz(tree_grid.best_estimator_, feature_names=df.columns,
out_file=dot_data, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(value=graph.create_png())
現(xiàn)在,再次使用交叉驗證對 k-NN 的 k 值(即鄰居數(shù))進行調(diào)優(yōu)。
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_jobs=-1))])
knn_params = {'knn__n_neighbors': range(6, 8)}
knn_grid = GridSearchCV(knn_pipe, knn_params,
cv=5, n_jobs=-1,
verbose=True)
knn_grid.fit(X_train, y_train)
knn_grid.best_params_, knn_grid.best_score_
knn_grid.best_params_
accuracy_score(y_holdout, knn_grid.predict(X_holdout))
從上可知,在 1-9 范圍(range 不包括 10)內(nèi)最優(yōu)的 k 值為 7,其交叉驗證的準確率約為 88.5%,調(diào)優(yōu)后 k-NN 在留置集上的準確率約為 89%。
綜上所述,在這個任務(wù)里,決策樹有著 94%/94.6%(留置法/交叉驗證調(diào)優(yōu)后)的準確率,k-NN 有著 88%/89%(留置法/交叉驗證調(diào)優(yōu)后)的準確率,顯然決策樹的表現(xiàn)更好。
使用 RandomForestClassifier() 方法再訓(xùn)練一個隨機森林(可以把它想象成一群互相協(xié)作的決策樹),看看能否在這個任務(wù)上有更好的表現(xiàn)。
from sklearn.ensemble import RandomForestClassifier
forest = RandomForestClassifier(n_estimators=100, n_jobs=-1,
random_state=17)
np.mean(cross_val_score(forest, X_train, y_train, cv=5))
forest_params = {'max_depth': range(8, 10),
'max_features': range(5, 7)}
forest_grid = GridSearchCV(forest, forest_params,
cv=5, n_jobs=-1, verbose=True)
forest_grid.fit(X_train, y_train)
forest_grid.best_params_, forest_grid.best_score_
accuracy_score(y_holdout, forest_grid.predict(X_holdout))
從上可知,隨機森林有著 95.3% 的準確率。不得不說,決策樹在這個任務(wù)上的表現(xiàn)非常好,即使是訓(xùn)練時間長得多的隨機森林也無法取得比它更好的表現(xiàn)。
決策樹的復(fù)雜情況
為了繼續(xù)討論決策樹和 k-NN 的優(yōu)劣,讓我們考慮另外一個簡單的分類任務(wù),在這個任務(wù)中決策樹的表現(xiàn)不錯但得到的分類邊界過于復(fù)雜。
首先,在一個平面上創(chuàng)建一組具有 2 個分類的數(shù)據(jù)點,每個數(shù)據(jù)點是兩個分類中的一個(紅色表示 x1>x2,黃色表示x1<x2),其實用一條直線 x1=x2就可以完成它們的分類,那么決策樹會這么做嗎?
def form_linearly_separable_data(n=500, x1_min=0, x1_max=30,
x2_min=0, x2_max=30):
data, target = [], []
for i in range(n):
x1 = np.random.randint(x1_min, x1_max)
x2 = np.random.randint(x2_min, x2_max)
if np.abs(x1 - x2) > 0.5:
data.append([x1, x2])
target.append(np.sign(x1 - x2))
return np.array(data), np.array(target)
X, y = form_linearly_separable_data()
plt.scatter(X[:, 0], X[:, 1], c=y, cmap='autumn', edgecolors='black')
訓(xùn)練一個決策樹對上面的數(shù)據(jù)進行分類,并繪制分類邊界。
tree = DecisionTreeClassifier(random_state=17).fit(X, y)
xx, yy = get_grid(X)
predicted = tree.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(X[:, 0], X[:, 1], c=y, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.title('Easy task. Decision tree compexifies everything')
可視化決策樹。
dot_data = StringIO()
export_graphviz(tree, feature_names=['x1', 'x2'],
out_file=dot_data, filled=True)
graph = pydotplus.graph_from_dot_data(dot_data.getvalue())
Image(value=graph.create_png())
從上可知,決策樹構(gòu)建的邊界過于復(fù)雜,而且樹的深度過深,產(chǎn)生了過擬合現(xiàn)象。
再訓(xùn)練一個 k-NN 模型,看看它在這個任務(wù)上的表現(xiàn)情況。
knn = KNeighborsClassifier(n_neighbors=1).fit(X, y)
xx, yy = get_grid(X)
predicted = knn.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)
plt.pcolormesh(xx, yy, predicted, cmap='autumn')
plt.scatter(X[:, 0], X[:, 1], c=y, s=100,
cmap='autumn', edgecolors='black', linewidth=1.5)
plt.title('Easy task, kNN. Not bad')

從上可知,最近鄰方法的表現(xiàn)比決策樹好一點,但仍然比不上線性分類器x1=x2。
在 MNIST 手寫數(shù)字識別任務(wù)中應(yīng)用決策樹和 k-NN
現(xiàn)在可以看看這兩個算法應(yīng)用到實際任務(wù)上的表現(xiàn)如何,首先載入 sklearn 內(nèi)置的 MNIST 手寫數(shù)字數(shù)據(jù)集,該數(shù)據(jù)庫中手寫數(shù)字的圖片為 8x8 的矩陣,矩陣中的值表示每個像素的白色亮度。
from sklearn.datasets import load_digits
data = load_digits()
X, y = data.data, data.target
X[0, :].reshape([8, 8])
繪制一些 MNIST 手寫數(shù)字。
f, axes = plt.subplots(1, 4, sharey=True, figsize=(16, 6))
for i in range(4):
axes[i].imshow(X[i, :].reshape([8, 8]), cmap='Greys')
使用 train_test_split() 方法分割數(shù)據(jù)集,其中的 70% 作為訓(xùn)練集(X_train,y_train),30% 作為留置集(X_holdout,y_holdout)。
X_train, X_holdout, y_train, y_holdout = train_test_split(
X, y, test_size=0.3, random_state=17)
使用隨機參數(shù)訓(xùn)練決策樹和 k-NN。
tree = DecisionTreeClassifier(max_depth=5, random_state=17)
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_neighbors=10))])
tree.fit(X_train, y_train)
knn_pipe.fit(X_train, y_train)
訓(xùn)練好之后,分別在留置集上做出預(yù)測。
tree_pred = tree.predict(X_holdout)
knn_pred = knn_pipe.predict(X_holdout)
accuracy_score(y_holdout, knn_pred), accuracy_score(
y_holdout, tree_pred) # (0.976, 0.666)
從上可知,k-NN 做得更好,不過別忘了我們用的是隨機參數(shù)?,F(xiàn)在,使用交叉驗證調(diào)優(yōu)決策樹模型,因為這次任務(wù)所需考慮的特征比之前任務(wù)中的更多,所以可以增加參數(shù)的大小。
tree_params = {'max_depth': [10, 20, 30],
'max_features': [30, 50, 64]}
tree_grid = GridSearchCV(tree, tree_params,
cv=5, n_jobs=-1, verbose=True)
tree_grid.fit(X_train, y_train)
查看交叉驗證得到的最佳參數(shù)組合和相應(yīng)的準確率。
tree_grid.best_params_, tree_grid.best_score_
調(diào)優(yōu)后決策樹模型的準確率達到了 84.4%,但還不到使用隨機參數(shù)的 k-NN 的準確率(97%)?,F(xiàn)在,使用交叉驗證調(diào)優(yōu) k-NN 模型。
np.mean(cross_val_score(KNeighborsClassifier(
n_neighbors=1), X_train, y_train, cv=5))
從上可知,調(diào)優(yōu)后的 k-NN 在這一數(shù)據(jù)集上可以達到 98.7% 的準確率。
下面在這一數(shù)據(jù)集上訓(xùn)練隨機森林模型,在大多數(shù)數(shù)據(jù)集上,它的效果比 k-NN 要好。
np.mean(cross_val_score(RandomForestClassifier(
random_state=17), X_train, y_train, cv=5))
從上可知,在這個數(shù)據(jù)集中隨機森林的準確率(93.5%)不如 k-NN(98.7%)。當然,我們沒有對隨機森林的參數(shù)進行任何調(diào)優(yōu),但即使經(jīng)過調(diào)優(yōu),訓(xùn)練精確度也無法超過 k-NN。
決策樹、k-NN、隨機森林在這個數(shù)據(jù)集上的準確率如下所示:

從這個任務(wù)中得到的結(jié)論(同時也是一個通用的建議):首先查看簡單模型(決策樹、最近鄰)在你的數(shù)據(jù)上的表現(xiàn),因為可能僅使用簡單模型就已經(jīng)表現(xiàn)得足夠好了。
最近鄰方法的復(fù)雜情形
下面考慮另一種情況,即在一個分類問題中,某個特征直接和目標變量成比例的情況。
def form_noisy_data(n_obj=1000, n_feat=100, random_seed=17):
np.seed = random_seed
y = np.random.choice([-1, 1], size=n_obj)
# 第一個特征與目標成比例
x1 = 0.3 * y
# 其他特征為噪聲
x_other = np.random.random(size=[n_obj, n_feat - 1])
return np.hstack([x1.reshape([n_obj, 1]), x_other]), y
X, y = form_noisy_data()
使用最近鄰方法訓(xùn)練模型后,查看交叉驗證和留置集的準確率,并繪制這兩個準確率隨 n_neighbors 最近鄰數(shù)目 參數(shù)變化的曲線,這樣的曲線被稱為驗證曲線。
from sklearn.model_selection import cross_val_score
X_train, X_holdout, y_train, y_holdout = train_test_split(
X, y, test_size=0.3, random_state=17)
cv_scores, holdout_scores = [], []
n_neighb = [1, 2, 3, 5] + list(range(50, 550, 50))
for k in n_neighb:
knn_pipe = Pipeline([('scaler', StandardScaler()),
('knn', KNeighborsClassifier(n_neighbors=k))])
cv_scores.append(np.mean(cross_val_score(
knn_pipe, X_train, y_train, cv=5)))
knn_pipe.fit(X_train, y_train)
holdout_scores.append(accuracy_score(
y_holdout, knn_pipe.predict(X_holdout)))
plt.plot(n_neighb, cv_scores, label='CV')
plt.plot(n_neighb, holdout_scores, label='holdout')
plt.title('Easy task. kNN fails')
plt.legend()
上圖表明,即使我們嘗試在較廣范圍內(nèi)改變 n_neighbors 參數(shù),基于歐幾里得距離的 k-NN 在這個問題上依舊表現(xiàn)不佳。
下面用決策樹訓(xùn)練一個模型,看看它在這個任務(wù)上的表現(xiàn)如何。
tree = DecisionTreeClassifier(random_state=17, max_depth=1)
tree_cv_score = np.mean(cross_val_score(tree, X_train, y_train, cv=5))
tree.fit(X_train, y_train)
tree_holdout_score = accuracy_score(y_holdout, tree.predict(X_holdout))
print('Decision tree. CV: {}, holdout: {}'.format(
tree_cv_score, tree_holdout_score))
在這一任務(wù)中,決策樹完美地解決了問題,在交叉驗證和留置集上都得到了 100% 的準確率。其實,k-NN 之所以在這個任務(wù)上表現(xiàn)不佳并非該方法本身的問題,而是因為使用了歐幾里得距離,因為歐幾里得距離沒能察覺出有一個特征(成比例)比其他所有特征(噪聲)更重要。
決策樹和最近鄰方法的優(yōu)勢和劣勢
決策樹
優(yōu)勢:
生成容易理解的分類規(guī)則,這一屬性稱為模型的可解釋性。例如它生成的規(guī)則可能是「如果年齡不滿 25 歲,并對摩托車感興趣,那么就拒絕發(fā)放貸款」。
很容易可視化,即模型本身(樹)和特定測試對象的預(yù)測(穿過樹的路徑)可以「被解釋」。
訓(xùn)練和預(yù)測的速度快。
較少的參數(shù)數(shù)目。
支持數(shù)值和類別特征。
劣勢:
決策樹對輸入數(shù)據(jù)中的噪聲非常敏感,這削弱了模型的可解釋性。
決策樹構(gòu)建的邊界有其局限性:它由垂直于其中一個坐標軸的超平面組成,在實踐中比其他方法的效果要差。
我們需要通過剪枝、設(shè)定葉節(jié)點的最小樣本數(shù)、設(shè)定樹的最大深度等方法避免過擬合。
不穩(wěn)定性,數(shù)據(jù)的細微變動都會顯著改變決策樹。這一問題可通過決策樹集成方法來處理。
搜索最佳決策樹是一個「NP 完全」(NP-Complete)問題。實踐中使用的一些推斷方法,比如基于最大信息增益進行貪婪搜索,并不能保證找到全局最優(yōu)決策樹。
倘若數(shù)據(jù)中出現(xiàn)缺失值,將難以創(chuàng)建決策樹模型。Friedman 的 CART 算法中大約 50% 的代碼是為了處理數(shù)據(jù)中的缺失值(現(xiàn)在 sklearn 實現(xiàn)了這一算法的改進版本)。
這一模型只能內(nèi)插,不能外推(隨機森林和樹提升方法也是如此)。也就是說,倘若你預(yù)測的對象在訓(xùn)練集所設(shè)置的特征空間之外,那么決策樹就只能做出常數(shù)預(yù)測。比如,在我們的黃球和藍球的例子中,這意味著模型將對所有位于 >19 或 <0 的球做出同樣的預(yù)測。
最近鄰方法
優(yōu)勢:
實現(xiàn)簡單。
研究很充分。
通常而言,在分類、回歸、推薦問題中第一個值得嘗試的方法就是最近鄰方法。
通過選擇恰當?shù)暮饬繕藴驶蚝耍梢赃m應(yīng)某一特定問題。
劣勢:
和其他復(fù)合算法相比,這一方法速度較快。但是,現(xiàn)實生活中,用于分類的鄰居數(shù)目通常較大(100-150),在這一情形下,k-NN 不如決策樹快。
如果數(shù)據(jù)集有很多變量,很難找到合適的權(quán)重,也很難判定哪些特征對分類/回歸不重要。
依賴于對象之間的距離度量,默認選項歐幾里得距離常常是不合理的。你可以通過網(wǎng)格搜索參數(shù)得到良好的解,但在大型數(shù)據(jù)集上的耗時很長。
沒有理論來指導(dǎo)我們?nèi)绾芜x擇鄰居數(shù),故而只能進行網(wǎng)格搜索(盡管基本上所有的模型,在對其超參數(shù)進行調(diào)整時都使用網(wǎng)格搜索的方法)。在鄰居數(shù)較小的情形下,該方法對離散值很敏感,也就是說,有過擬合的傾向。
由于「維度的詛咒」,當數(shù)據(jù)集存在很多特征時它的表現(xiàn)不佳。
決策樹和隨機森林分析應(yīng)用
import warnings
import pydotplus
from io import StringIO
from IPython.display import SVG
from sklearn.metrics import accuracy_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier, export_graphviz
from sklearn import preprocessing
from sklearn.model_selection import GridSearchCV, cross_val_score
import collections
from sklearn.preprocessing import LabelEncoder
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
%matplotlib inline
plt.rcParams['figure.figsize'] = (10, 8)
warnings.filterwarnings('ignore')
簡單示例練習(xí)
該數(shù)據(jù)集表示了 A 會不會和 B 進行第二次約會。而數(shù)據(jù)集中的特征包括:外貌,口才,酒精消費,以及第一次約會花了多少錢。
創(chuàng)建示例數(shù)據(jù)集
# 創(chuàng)建示例數(shù)據(jù)集,并對數(shù)據(jù)類別進行獨熱編碼
def create_df(dic, feature_list):
out = pd.DataFrame(dic)
out = pd.concat([out, pd.get_dummies(out[feature_list])], axis=1)
out.drop(feature_list, axis=1, inplace=True)
return out
# 保證獨熱編碼后的特征在訓(xùn)練和測試數(shù)據(jù)中同時存在
def intersect_features(train, test):
common_feat = list(set(train.keys()) & set(test.keys()))
return train[common_feat], test[common_feat]
features = ['Looks', 'Alcoholic_beverage', 'Eloquence', 'Money_spent']
訓(xùn)練數(shù)據(jù)
df_train = {}
df_train['Looks'] = ['handsome', 'handsome', 'handsome', 'repulsive',
'repulsive', 'repulsive', 'handsome']
df_train['Alcoholic_beverage'] = [
'yes', 'yes', 'no', 'no', 'yes', 'yes', 'yes']
df_train['Eloquence'] = ['high', 'low', 'average', 'average', 'low',
'high', 'average']
df_train['Money_spent'] = ['lots', 'little', 'lots', 'little', 'lots',
'lots', 'lots']
df_train['Will_go'] = LabelEncoder().fit_transform(
['+', '-', '+', '-', '-', '+', '+'])
df_train = create_df(df_train, features)
df_train
測試數(shù)據(jù)
df_test = {}
df_test['Looks'] = ['handsome', 'handsome', 'repulsive']
df_test['Alcoholic_beverage'] = ['no', 'yes', 'yes']
df_test['Eloquence'] = ['average', 'high', 'average']
df_test['Money_spent'] = ['lots', 'little', 'lots']
df_test = create_df(df_test, features)
df_test
# 保證獨熱編碼后的特征在訓(xùn)練和測試數(shù)據(jù)中同時存在
y = df_train['Will_go']
df_train, df_test = intersect_features(train=df_train, test=df_test)
df_train
df_test
使用 scikit-learn 提供的方法來繪制決策樹
dt = DecisionTreeClassifier(criterion='entropy', random_state=17)
dt.fit(df_train, y)
tree_str = export_graphviz(
dt, feature_names=df_train.columns, out_file=None, filled=True)
graph = pydotplus.graph_from_dot_data(tree_str)
SVG(graph.create_svg())
計算熵和信息增益
我們換另外一個例子:假設(shè)有 9 個藍色球和 11 個黃色球。如果球是藍色,則讓球的標簽是 1,否則為 0。
balls = [1 for i in range(9)] + [0 for i in range(11)] # 生成數(shù)據(jù)

接下來將球分成如下兩組:

# 數(shù)據(jù)分組
# 8 藍色 和 5 黃色
balls_left = [1 for i in range(8)] + [0 for i in range(5)]
# 1 藍色 和 6 黃色
balls_right = [1 for i in range(1)] + [0 for i in range(6)]
實現(xiàn)香農(nóng)熵計算函數(shù) entropy()。
from math import log
def entropy(a_list):
lst = list(a_list)
size = len(lst) * 1.0
entropy = 0
set_elements = len(set(lst))
if set_elements in [0, 1]:
return 0
for i in set(lst):
occ = lst.count(i)
entropy -= occ/size * log(occ/size, 2)
return entropy
列表 ball_left 給出狀態(tài)的熵是多少?
entropy(balls_left) # 8 藍色 и 5 黃色
如果有一個 6 面立方體等概率骰子,其熵是多少?
entropy([1, 2, 3, 4, 5, 6]) # 6 面等概率骰子熵
實現(xiàn)信息增益的計算函數(shù) information_gain(root, left, right)。
# 信息增益計算
def information_gain(root, left, right):
''' root - 初始數(shù)據(jù), left and right - 分組數(shù)據(jù)'''
return entropy(root) - 1.0 * len(left) / len(root) * entropy(left) \
- 1.0 * len(right) / len(root) * entropy(right)
將初始數(shù)據(jù)集拆分為 balls_left 和 balls_right 后的信息增益是多少?
information_gain(balls, balls_left, balls_right)
實現(xiàn)基于信息增益劃分函數(shù) best_feature_to_split。
def best_feature_to_split(X, y):
'''信息增益用于特征分割'''
out = []
for i in X.columns:
out.append(information_gain(y, y[X[i] == 0], y[X[i] == 1]))
return out
通過遞歸調(diào)用 best_feature_to_split 實現(xiàn)一個簡單的樹構(gòu)建策略,并輸出每一步的熵變化。
def btree(X, y):
clf = best_feature_to_split(X, y)
param = clf.index(max(clf))
ly = y[X.iloc[:, param] == 0]
ry = y[X.iloc[:, param] == 1]
print('Column_' + str(param) + ' N/Y?')
print('Entropy: ', entropy(ly), entropy(ry))
print('N count:', ly.count(), '/', 'Y count:', ry.count())
if entropy(ly) != 0:
left = X[X.iloc[:, param] == 0]
btree(left, ly)
if entropy(ry) != 0:
right = X[X.iloc[:, param] == 1]
btree(right, ry)
best_feature_to_split(df_train, y)
btree(df_train, y)
構(gòu)建 Adult 數(shù)據(jù)集決策樹
UCI Adult 人口收入普查數(shù)據(jù)集,其具有以下一些特征:
Age – 連續(xù)數(shù)值特征
Workclass – 連續(xù)數(shù)值特征
fnlwgt – 連續(xù)數(shù)值特征
Education – 類別特征
Education_Num – 連續(xù)數(shù)值特征
Martial_Status – 類別特征
Occupation – 類別特征
Relationship – 類別特征
Race – 類別特征
Sex – 類別特征
Capital_Gain – 連續(xù)數(shù)值特征
Capital_Loss – 連續(xù)數(shù)值特征
Hours_per_week – 連續(xù)數(shù)值特征
Country – 類別特征
Target – 收入水平,二元分類目標值
加載并讀取該數(shù)據(jù)集:
data_train = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/adult_train.csv', sep=';')
data_train.tail()
data_test = pd.read_csv(
'https://labfile.oss.aliyuncs.com/courses/1283/adult_test.csv', sep=';')
data_test.tail()
對數(shù)據(jù)集進行一些必要的清洗。同時,將目標值轉(zhuǎn)換為 0,1 二元數(shù)值。
# 移除測試集中的錯誤數(shù)據(jù)
data_test = data_test[(data_test['Target'] == ' >50K.')
| (data_test['Target'] == ' <=50K.')]
# 將目標編碼為 0 和 1
data_train.loc[data_train['Target'] == ' <=50K', 'Target'] = 0
data_train.loc[data_train['Target'] == ' >50K', 'Target'] = 1
data_test.loc[data_test['Target'] == ' <=50K.', 'Target'] = 0
data_test.loc[data_test['Target'] == ' >50K.', 'Target'] = 1
輸出測試數(shù)據(jù)概覽表,查看特征和目標值的各項統(tǒng)計指標。
data_test.describe(include='all').T
查看訓(xùn)練數(shù)據(jù)集目標分布計數(shù),同時繪制各項特征的關(guān)聯(lián)分布圖像。
data_train['Target'].value_counts()
fig = plt.figure(figsize=(25, 15))
cols = 5
rows = np.ceil(float(data_train.shape[1]) / cols)
for i, column in enumerate(data_train.columns):
ax = fig.add_subplot(rows, cols, i + 1)
ax.set_title(column)
if data_train.dtypes[column] == np.object:
data_train[column].value_counts().plot(kind="bar", axes=ax)
else:
data_train[column].hist(axes=ax)
plt.xticks(rotation="vertical")
plt.subplots_adjust(hspace=0.7, wspace=0.2)
檢查數(shù)據(jù)的類型
data_train.dtypes
data_test.dtypes
測試數(shù)據(jù)中,年齡 Age 是 object 類型,我們需要修復(fù)其為整數(shù)類型。
data_test['Age'] = data_test['Age'].astype(int)
將測試數(shù)據(jù)中浮點類型特征全部處理成整數(shù)類型,以便與訓(xùn)練數(shù)據(jù)對應(yīng)。
data_test['fnlwgt'] = data_test['fnlwgt'].astype(int)
data_test['Education_Num'] = data_test['Education_Num'].astype(int)
data_test['Capital_Gain'] = data_test['Capital_Gain'].astype(int)
data_test['Capital_Loss'] = data_test['Capital_Loss'].astype(int)
data_test['Hours_per_week'] = data_test['Hours_per_week'].astype(int)
繼續(xù)對數(shù)據(jù)預(yù)處理,首先區(qū)分數(shù)據(jù)集中的類別和連續(xù)特征。
# 從數(shù)據(jù)集中選擇類別和連續(xù)特征變量
categorical_columns = [c for c in data_train.columns
if data_train[c].dtype.name == 'object']
numerical_columns = [c for c in data_train.columns
if data_train[c].dtype.name != 'object']
print('categorical_columns:', categorical_columns)
print('numerical_columns:', numerical_columns)
對連續(xù)特征使用中位數(shù)對缺失數(shù)據(jù)進行填充,而類別特征則使用眾數(shù)進行填充。
# 填充缺失數(shù)據(jù)
for c in categorical_columns:
data_train[c].fillna(data_train[c].mode(), inplace=True)
data_test[c].fillna(data_train[c].mode(), inplace=True)
for c in numerical_columns:
data_train[c].fillna(data_train[c].median(), inplace=True)
data_test[c].fillna(data_train[c].median(), inplace=True)
對類別特征進行獨熱編碼,以保證數(shù)據(jù)集特征全部為數(shù)值類型方便后續(xù)傳入模型。
data_train = pd.concat([data_train[numerical_columns],
pd.get_dummies(data_train[categorical_columns])], axis=1)
data_test = pd.concat([data_test[numerical_columns],
pd.get_dummies(data_test[categorical_columns])], axis=1)
set(data_train.columns) - set(data_test.columns)
data_train.shape, data_test.shape
獨熱編碼之后發(fā)現(xiàn)測試數(shù)據(jù)中沒有 Holland,為了與訓(xùn)練數(shù)據(jù)對應(yīng),這里需要創(chuàng)建零值特征進行補齊。
data_test['Country_ Holand-Netherlands'] = 0
set(data_train.columns) - set(data_test.columns)
data_train.head(2)
data_test.head(2)
X_train = data_train.drop(['Target'], axis=1)
y_train = data_train['Target']
X_test = data_test.drop(['Target'], axis=1)
y_test = data_test['Target']
建立默認參數(shù)決策樹模型
使用訓(xùn)練數(shù)據(jù)創(chuàng)建一個決策樹分類器。挑戰(zhàn)規(guī)定 max_depth=3,random_state=17。
按要求構(gòu)建決策樹,并輸出其在測試集上的準確度
tree = DecisionTreeClassifier(max_depth=3, random_state=17)
tree.fit(X_train, y_train)
tree_predictions = tree.predict(X_test)
accuracy_score(y_test, tree_predictions)
對決策樹模型進行調(diào)參
使用 GridSearchCV 網(wǎng)格搜索對決策樹進行調(diào)參并返回最佳參數(shù)。
決策樹參數(shù) random_state = 17,GridSearchCV 參數(shù) cv=5,并對 max_depth 參數(shù)在 [8,10] 范圍進行網(wǎng)格搜索。
tree_params = {'max_depth': range(8, 11)}
locally_best_tree = GridSearchCV(DecisionTreeClassifier(random_state=17),
tree_params, cv=5)
locally_best_tree.fit(X_train, y_train)
print("Best params:", locally_best_tree.best_params_)
print("Best cross validaton score", locally_best_tree.best_score_)
構(gòu)建上面最佳參數(shù)決策樹,并輸出其在測試集上的準確度
tuned_tree = DecisionTreeClassifier(max_depth=9, random_state=17)
tuned_tree.fit(X_train, y_train)
tuned_tree_predictions = tuned_tree.predict(X_test)
accuracy_score(y_test, tuned_tree_predictions)
建立隨機森林分類模型
利用 scikit-learn 提供的隨機森林算法建立相應(yīng)的分類預(yù)測模型。
構(gòu)建 RandomForestClassifier 隨機森林分類器。規(guī)定參數(shù) n_estimators=100 且 random_state=17,其余默認。
rf = RandomForestClassifier(n_estimators=100, random_state=17)
rf.fit(X_train, y_train)
輸出在測試集上的分類預(yù)測準確度。
forest_predictions = rf.predict(X_test)
accuracy_score(y_test,forest_predictions)