深入淺出學習決策樹(二)
接著上篇文章 深入淺出學習決策樹(一) 繼續 介紹決策樹相關內容。
迴歸問題中的決策樹
在預測數值變數時,構造樹的想法保持不變,但質量標準會發生變化。

其中 n 是葉子中的樣本數, Yi 是目標變數的值。簡單地說,通過最小化均值周圍的方差,我們尋找以這樣的方式劃分訓練集的特徵,即每個葉子中的目標特徵的值大致相等。
例
讓我們生成一些由函式分配並帶有一些噪音的資料。

然後我們將在其上訓練一棵樹並顯示它所做的預測。
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) from sklearn.tree import DecisionTreeRegressor 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)) plt.show()

我們看到決策樹用分段常數函式逼近資料。
3.最近鄰法
最近鄰方法 (k-Nearest Neighbors,或k-NN)是另一種非常流行的分類方法,有時也用於迴歸問題。與決策樹一樣,這是最易於理解的分類方法之一。潛在的直覺是你看起來像你的鄰居。更正式地,該方法遵循緊湊性假設:如果足夠好地測量示例之間的距離,則類似示例更可能屬於同一類。
根據最近鄰法,綠球將被分類為“藍色”而不是“紅色”。

再舉一個例子,如果您不知道如何在線上列表上標記藍芽耳機,您可以找到5個類似的耳機,如果其中4個被標記為“配件”,只有1個被標記為“技術”,那麼你還會在“配件”下標註它。
要對測試集中的每個樣本進行分類,需要按順序執行以下操作:
- 計算訓練集中每個樣本的距離。
- 從訓練集中選擇 k個 樣本,距離最小。
- 測試樣本的類別將是那些 k個 最近鄰居中最常見的類別。
該方法非常容易適應迴歸問題:在步驟3中,它不返回類,而是返回數字 - 鄰居之間目標變數的平均值(或中值)。
這種方法的一個顯著特點是它的懶惰 - 計算只在預測階段進行,當需要對測試樣本進行分類時。預先沒有從訓練樣例構建模型。相反,回想一下,對於本文前半部分的決策樹,樹是基於訓練集構建的,並且測試用例的分類通過遍歷樹而相對快速地發生。
最近鄰是一種經過深入研究的方法。存在許多重要的定理,聲稱在“無窮無盡”的資料集上,它是最佳的分類方法。經典著作“統計學習要素”的作者認為k-NN是理論上理想的演算法,其使用僅受計算能力和 維數災難的 限制。
真實應用中最近鄰方法
- 在某些情況下,k-NN可以作為一個良好的起點(基線);
- 在Kaggle比賽中,k-NN通常用於構建元特徵(即k-NN預測作為其他模型的輸入)或用於堆疊/混合;
- 最近鄰居方法擴充套件到推薦系統等其他任務。最初的決定可能是在我們想要提出建議的人的 最近鄰居 中受歡迎的產品(或服務)的推薦;
- 實際上,在大型資料集上,近似搜尋方法通常用於最近鄰居。有許多開源庫可以實現這樣的演算法; 看看Spotify的圖書館 Annoy 。
使用k-NN進行分類/迴歸的質量取決於幾個引數:
- 鄰居的數量 k 。
- 樣本之間的距離度量(常見的包括漢明,歐幾里得,餘弦和閔可夫斯基距離)。請注意,大多數這些指標都需要縮放資料。簡單來說,我們不希望“薪水”功能(大約數千)影響距離超過“年齡”,通常小於100。
- 鄰居的權重(每個鄰居可以貢獻不同的權重;例如,樣本越遠,權重越低)。
類 KNeighborsClassifier
在Scikit學習
該類的主要引數 sklearn.neighbors.KNeighborsClassifier
是:
- 權重:(
uniform
所有權重相等),distance
(權重與測試樣本的距離成反比),或任何其他使用者定義的函式; - 演算法(可選):
brute
,ball_tree
,KD_tree
或auto
。在第一種情況下,通過訓練集上的網格搜尋來計算每個測試用例的最近鄰居。在第二和第三種情況下,示例之間的距離儲存在樹中以加速找到最近鄰居。如果將此引數設定為auto
,則將根據訓練集自動選擇查詢鄰居的正確方法。 - leaf_size(可選):如果查詢鄰居的演算法是BallTree或KDTree,則切換到網格搜尋的閾值;
- 指標:
minkowski
,manhattan
,euclidean
,chebyshev
,或其他。
4.選擇模型引數和交叉驗證
學習演算法的主要任務是能夠探索到看不見的資料。由於我們無法立即檢查新的傳入資料的模型效能(因為我們還不知道目標變數的真實值),因此有必要犧牲一小部分資料來檢查模型的質量。
這通常以兩種方式之一完成:
- 留出資料集的一部分( 保留/保持集 )。我們保留訓練集的一小部分(通常從20%到40%),在剩餘資料上訓練模型(原始集合的60-80%),並計算模型的效能指標(例如準確度) - 套裝。
- 交叉驗證 。這裡最常見的情況是 k折交叉驗證 。

在k倍交叉驗證中,模型在原始資料集的不同( K-1 )子集上訓練 K 次(白色)並檢查剩餘子集(每次都是不同的子集,如上所示以橙色表示)。我們獲得 K 模型質量評估,通常是平均值,以給出分類/迴歸的總體平均質量。
與保持集方法相比,交叉驗證可以更好地評估新資料的模型質量。但是,當您擁有大量資料時,交叉驗證在計算上非常昂貴。
交叉驗證是機器學習中非常重要的技術,也可以應用於統計和計量經濟學。它有助於超引數調整,模型比較,特徵評估等。更多細節可以在 這裡 找到(Sebastian Raschka的部落格文章)或任何關於機器(統計)學習的經典教科書。
5.應用例項和複雜案例
客戶流失預測任務中的決策樹和最近鄰法
讓我們將資料讀入 DataFrame
並預處理它。暫時將 狀態 儲存在單獨的 Series
物件中,並將其從資料框中刪除。我們將訓練沒有 State 功能的第一個模型,然後我們將看看它是否有幫助。
df = pd.read_csv('../../data/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)

讓我們為訓練( X_train
, y_train
)分配70%的集合,為保留集合( X_holdout
, y_holdout
)分配30%。保持裝置不會參與調整模型的引數。我們將在調整結束後使用它來評估最終模型的質量。讓我們訓練2個模型:決策樹和k-NN。我們不知道哪些引數是好的,所以我們假設一些隨機的引數:樹深度為5,最近鄰居的數量等於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)
讓我們用一個簡單的指標評估保留集的預測質量 - 正確答案的比例(準確度)。決策樹做得更好 - 正確答案的百分比約為94%(決策樹)與88%(k-NN)。請注意,此效能是通過使用隨機引數實現的。
from sklearn.metrics import accuracy_score tree_pred = tree.predict(X_holdout) print(accuracy_score(y_holdout, tree_pred)) # 0.94 knn_pred = knn.predict(X_holdout) print(accuracy_score(y_holdout, knn_pred)) # 0.88
現在,讓我們使用交叉驗證來識別樹的引數。我們將調整每個分割時使用的最大深度和最大特徵數。以下是GridSearchCV如何工作的本質:對於每個唯一的值對, max_depth
並 max_features
使用5倍交叉驗證計算模型效能,然後選擇最佳的引數組合。
from sklearn.model_selection import GridSearchCV, cross_val_score tree_params = {'max_depth': range(1,11), 'max_features': range(4,19)} tree_grid = GridSearchCV(tree, tree_params, cv=5, n_jobs=-1, verbose=True) tree_grid.fit(X_train, y_train)
讓我們列出交叉驗證的最佳引數和相應的平均精度。
print(tree_grid.best_params_) # {'max_depth': 6, 'max_features': 17} print(tree_grid.best_score_) # 0.942 print(accuracy_score(y_holdout, tree_grid.predict(X_holdout))) # 0.946
讓我們畫出結果樹。由於它不完全是玩具示例(其最大深度為6),因此圖片並不小,但如果您在本地開啟從課程倉庫下載的相應圖片,則可以仔細瀏覽樹的全貌。
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())

現在,讓我們調整k-NN 的鄰居 k 的數量:
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(1, 10)} knn_grid = GridSearchCV(knn_pipe, knn_params, cv=5, n_jobs=-1, verbose=True) knn_grid.fit(X_train, y_train) print(knn_grid.best_params_, knn_grid.best_score_) # ({'knn__n_neighbors': 7}, 0.886)
這裡,樹被證明比最近鄰演算法更好:交叉驗證和保持的準確率分別為94.2%/ 94.6%。決策樹表現得非常好,甚至隨機森林(現在讓我們把它想象成一堆一起工作得更好的樹木)在這個例子中,儘管訓練時間更長,卻無法達到更好的效能(95.1%/ 95.3%)。
from sklearn.ensemble import RandomForestClassifier forest = RandomForestClassifier(n_estimators=100, n_jobs=-1, random_state=17) print(np.mean(cross_val_score(forest, X_train, y_train, cv=5))) # 0.949 forest_params = {'max_depth': range(1, 11), 'max_features': range(4, 19)} forest_grid = GridSearchCV(forest, forest_params, cv=5, n_jobs=-1, verbose=True) forest_grid.fit(X_train, y_train) print(forest_grid.best_params_, forest_grid.best_score_) # ({'max_depth': 9, 'max_features': 6}, 0.951)
決策樹的複雜案例
為了繼續討論相關方法的優缺點,讓我們考慮一個簡單的分類任務,其中一棵樹表現良好但是以“過於複雜”的方式進行。讓我們在一個平面上建立一組點(2個特徵),每個點將是兩個類中的一個(紅色為+1,黃色為-1)。如果將其視為分類問題,則看起來非常簡單:類由一行分隔。
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');

但是,決策樹構建的邊界過於複雜; 加上樹本身很深。另外,想象一下,樹會對訓練集的30 x 30方格以外的空間進行推廣有多麼糟糕。
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 complexifies everything');

雖然解決方案只是一條直線 x1 = x2 ,但我們得到了這種過於複雜的結構。
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())

一個最近鄰居的方法比樹更好,但仍然不如線性分類器(我們的 下一個主題 )。
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');

MNIST手寫數字識別任務中的決策樹和k-NN
現在讓我們看看這兩種演算法如何在現實世界中執行任務。我們將 sklearn
在手寫數字上使用內建資料集。這個任務就是k-NN工作得非常好的例子。
這裡的圖片是8x8矩陣(每個畫素的白色強度)。然後將每個這樣的矩陣“展開”到長度為64的向量中,並且我們獲得物件的特徵描述。
我們畫一些手寫的數字。我們看到它們是可區分的。
from sklearn.datasets import load_digits data = load_digits() X, y = data.data, data.target 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');

接下來,讓我們進行與上一個任務相同的實驗,但是,這一次,讓我們更改可調引數的範圍。
讓我們選擇70%的資料集用於訓練( 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)
讓我們用隨機引數訓練決策樹和k-NN,並對我們的保持集進行預測。我們可以看到k-NN做得更好,但請注意這是隨機引數。
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)
tree_pred = tree.predict(X_holdout) knn_pred = knn.predict(X_holdout) print(accuracy_score(y_holdout, knn_pred), accuracy_score(y_holdout, tree_pred)) # (0.97, 0.666)
現在讓我們像以前一樣使用交叉驗證來調整我們的模型引數,但是現在我們將考慮到我們擁有比上一個任務更多的功能:64。
tree_params = {'max_depth': [1, 2, 3, 5, 10, 20, 25, 30, 40, 50, 64], 'max_features': [1, 2, 3, 5, 10, 20 ,30, 50, 64]} tree_grid = GridSearchCV(tree, tree_params, cv=5, n_jobs=-1, verbose=True) tree_grid.fit(X_train, y_train)
讓我們看看最佳引數組合以及交叉驗證的相應準確度:
print(tree_grid.best_params_, tree_grid.best_score_) # ({'max_depth': 20, 'max_features': 64}, 0.844)
這已經超過了66%但不是97%。k-NN在此資料集上的效果更好。在一個最近鄰居的情況下,我們能夠在交叉驗證上達到99%的猜測。
print(np.mean(cross_val_score(KNeighborsClassifier(n_neighbors=1), X_train, y_train, cv=5))) # 0.987
讓我們在同一個資料集上訓練一個隨機森林,它在大多數資料集上比k-NN更好。但我們這裡有一個例外。
print(np.mean(cross_val_score(RandomForestClassifier(random_state=17), X_train, y_train, cv=5))) # 0.935
你指出我們 RandomForestClassifier
在這裡沒有調整任何引數是正確的。即使進行調整,訓練精度也不會像一個最近鄰居那樣達到98%。

這個實驗的 結論 (以及一般建議):首先檢查資料上的簡單模型:決策樹和最近鄰居(下次我們還將邏輯迴歸新增到此列表中)。情況可能就是這些方法已經足夠好了。
最近鄰法的複雜案例
讓我們考慮另一個簡單的例子。在分類問題中,其中一個特徵將與響應向量成比例,但這對最近鄰方法沒有幫助。
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) # first feature is proportional to target x1 = 0.3 * y # other features are noise 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()
與往常一樣,我們將研究交叉驗證和保持集的準確性。讓我們構造反映這些量對 n_neighbors
最近鄰方法中引數的依賴性的曲線。這些曲線稱為驗證曲線。
可以看出,即使你在很大範圍內改變最近鄰居的數量,具有歐幾里德距離的k-NN也不能很好地解決問題。
X_train, X_holdout, y_train, y_holdout = train_test_split(X, y, test_size=0.3, random_state=17) from sklearn.model_selection import cross_val_score cv_scores, holdout_scores = [], [] n_neighb = [1, 2, 3, 5] + list(range(50, 550, 50)) for k in n_neighb: knn = KNeighborsClassifier(n_neighbors=k) cv_scores.append(np.mean(cross_val_score(knn, X_train, y_train, cv=5))) knn.fit(X_train, y_train) holdout_scores.append(accuracy_score(y_holdout, knn.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();

相反,儘管對最大深度有限制,但決策樹很容易“檢測”資料中的隱藏依賴性。
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)) # Decision tree. CV: 1.0, holdout: 1.0
在第二個例子中,樹完全解決了問題,而k-NN遇到了困難。然而,這比使用歐幾里德距離更不利於該方法。它不允許我們揭示一個特徵比其他特徵好得多。
6.決策樹的優缺點和最近鄰法
決策樹的利弊
優點:
- 生成明確的人類可理解的分類規則,例如“如果年齡<25且對摩托車感興趣,則拒絕貸款”。此屬性稱為模型的可解釋性。
- 決策樹可以很容易地視覺化,即模型本身(樹)和某個測試物件(樹中的路徑)的預測都可以“被解釋”。
- 快速培訓和預測。
- 少量模型引數。
- 支援數字和分類功能。
缺點:
sklearn
最近鄰法的利弊
優點:
- 簡單的實施。
- 容易。
- 通常,該方法不僅是分類或迴歸的良好第一解決方案,而且是推薦。
- 它可以通過選擇正確的度量或核心來適應某個問題(簡而言之,核心可以為複雜物件(如圖形)設定相似性操作,同時保持k-NN方法相同)。順便說一句, Alexander Dyakonov ,前任前1名kaggler,喜歡最簡單的k-NN,但卻擁有調整物件的相似度量。
- 良好的可解釋性。也有例外:如果鄰居數量很大,可解釋性就會惡化(“我們沒有給他貸款,因為他與350個客戶類似,其中70個是壞客戶,比平均水平高12%對於資料集“)。
缺點:
- 與演算法的組合相比,被認為是快速的方法,但是在現實生活中用於分類的鄰居的數量通常很大(100-150),在這種情況下演算法將不像決策樹那樣快速地執行。
- 如果資料集具有許多變數,則很難找到正確的權重並確定哪些特徵對於分類/迴歸不重要。
- 依賴於物件之間的選定距離度量。預設選擇歐幾里德距離通常是沒有根據的。您可以通過網格搜尋引數找到一個好的解決方案,但這對於大型資料集來說變得非常耗時。
- 沒有理論上的方法可以選擇鄰居的數量 - 只有網格搜尋(儘管對於所有模型的所有超引數都是如此)。在少數鄰居的情況下,該方法對異常值敏感,即傾向於過度擬合。
- 通常,由於“維數的詛咒”而存在許多功能時,它不能很好地工作。ML社群的知名成員Pedro Domingos教授在他的熱門論文“機器學習中幾個有用的事情”中談到了這 一點 ; 在 本章 的深度學習書中也描述了“維度的詛咒” 。
這是很多資訊,但是,希望這篇文章很長一段時間對你很有幫助:)
更多文章歡迎訪問: http://www.apexyun.com
公眾號:銀河系1號
聯絡郵箱:[email protected]
(未經同意,請勿轉載)