機器學習超詳細實踐攻略(10):隨機森林算法詳解及小白都能看懂的調參指南

一、什麼是隨機森林

前面我們已經介紹了決策樹的基本原理和使用。但是決策樹有一個很大的缺陷:因爲決策樹會非常細緻地劃分樣本,如果決策樹分得太多細緻,會導致其在訓練集上出現過擬合,而如果決策樹粗略地劃分樣本,又不能很好地擬合樣本。

爲了解決這個兩難困境,聰明的專家們想出了這樣的思路:既然我增加單棵樹的深度會適得其反,那不如我不追求一個樹有多高的精確度,而是訓練多棵這樣的樹來一塊預測,一棵樹的力量再大,也是有限的,當他們聚成一個集體,它的力量可能是難以想象的,也就是我們常說的:“三個臭皮匠賽過諸葛亮”。這便是集成學習的思想。

這裏多提一句,正是因爲每棵樹都能夠用比較簡單的方法細緻地擬合樣本,我們可以多用幾棵樹來搭建準確率更高的算法,後邊要說到的一些工業級的算法,比如GBDT、XGBOOST、LGBM都是以決策樹爲積木搭建出來的。

所以在學習這些算法的過程中,我們也要把決策樹算法看成一塊塊積木,學完了基本的積木算法之後,對於現在常用的那幾個工業級的算法,只需要理清兩個問題:

1)這個算法利用了哪個集成學習思想

2)這個算法具體怎麼把這個思想實現出來的。

隨機森林就是決策樹們基於bagging集成學習思想搭建起來的。

隨機森林的算法實現思路非常簡單,只需要記住一句口訣:抽等量樣本,選幾個特徵,構建多棵樹。

下面,我們詳細解釋這個口訣的含義:

1)抽等量樣本

隨機森林訓練每棵樹之前,都會從訓練集中隨機抽出一部分樣本來訓練。所以說訓練每棵樹用到的樣本其實都是有差別的,這樣就保證了不同的樹可以重點學習不同的樣本。而爲了達到抽等量樣本的目的,抽樣方式一般是有放回的抽樣,也就是說,在訓練某棵樹的時候,這一次被抽到的樣本會被放回數據集中,下一次還可能被抽到,因此,原訓練集中有的樣本會多次被抽出用來訓練,而有的樣本可能不會被使用到。

但是不用擔心有的樣本沒有用到,只要訓練的樹的棵數足夠多,大多數訓練樣本總會被取到的。有極少量的樣本成爲漏網之魚也不用擔心,後邊我們會篩選他們出來用來測試模型。

2)選幾個特徵

在訓練某棵樹的時候,也不是將樣本的所有特徵都用來訓練,而是會隨機選擇一部分特徵用來訓練。這樣做的目的就是讓不同的樹重點關注不同的特徵。在scikit-learn中,用“max_features”這個參數來控制訓練每棵樹選取的樣本數。

3)構建多棵樹。

通過1)、2)兩個步驟,訓練多棵樹。魯迅曾經說過:世界上本沒有森林,長得樹多了,就成了森林。正是一棵棵決策樹構成了整個隨機森林。具體構建樹的數量,在scikit-learn中,用“n_estimators”這個參數來控制。
在這裏插入圖片描述

那最終的預測結果怎麼得到呢?隨機森林是非常民主的算法,最終的結果由每棵決策樹綜合給出:如果是分類問題,那麼對於每個測試集,樹都會預測出一個類別進行投票,最終統計票數多的那個類別爲最終類別。看看,這算法儼然是一個遵循:“少數服從多數”的原則的小型民主社會;如果是迴歸問題,那就更簡單了,各個樹得到的結果相加求得一個平均值爲最終迴歸結果。

從上邊的流程中可以看出,隨機森林的隨機性主要體現在兩個方面:**數據集的隨機選取、每棵樹所使用特徵的隨機選取。**以上兩個隨機性使得隨機森林中的決策樹都能夠彼此不同,提升系統的多樣性,從而提升分類性能。

二、隨機森林的特點

以上就是隨機森林的構建原理,下面,我們說說隨機森林算法的優缺點:

1、優點:

1)實現簡單,泛化能力強,可以並行實現,因爲訓練時樹與樹之間是相互獨立的;

2)相比單一決策樹,能學習到特徵之間的相互影響,且不容易過擬合;

3)能直接特徵很多的高維數據,因爲在訓練過程中依舊會從這些特徵中隨機選取部分特徵用來訓練;

4)相比SVM,不是很怕特徵缺失,因爲待選特徵也是隨機選取;

5)訓練完成後可以給出特徵重要性。當然,這個優點主要來源於決策樹。因爲決策樹在訓練過程中會計算熵或者是基尼係數,越往樹的根部,特徵越重要。

2、缺點

1)在噪聲過大的分類和處理迴歸問題時還是容易過擬合;

2)相比於單一決策樹,它的隨機性讓我們難以對模型進行解釋。

三、隨機森林的使用

和決策樹類似,隨機森林同樣可以分爲分類森林(RandomForestClassifier )和迴歸森林(RandomForestRegressor),在scikit-lean中調用方式和決策樹相同。

讓我們在手寫識別數據集上實現一個分類森林。

#導入必要的包
from sklearn.ensemble import RandomForestClassifier 
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split,GridSearchCV,cross_val_score
from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt
import numpy as np

#導入數據集
data = load_digits()
x = data.data
y = data.target

用隨機森林訓練並進行交叉驗證:

RF = RandomForestClassifier(random_state = 66)
score = cross_val_score(RF,x,y,cv=10).mean()
print('交叉驗證得分: %.4f'%score)

輸出:

交叉驗證得分: 0.9278

四、隨機森林調參

一)隨機森林參數介紹

除了和決策樹有的參數之外,對於隨機森林,只需要掌握8個新增的參數即可,我將這8個參數分成三類:

1、用於調參的參數:
  • max_features(最大特徵數): 這個參數用來訓練每棵樹時需要考慮的最大特徵個數,超過限制個數的特徵都會被捨棄,默認爲auto。可填入的值有:int值,float(特徵總數目的百分比),“auto”/“sqrt”(總特徵個數開平方取整),“log2”(總特徵個數取對數取整)。默認值爲總特徵個數開平方取整。值得一提的是,這個參數在決策樹中也有但是不重要,因爲其默認爲None,即有多少特徵用多少特徵。爲什麼要設置這樣一個參數呢?原因如下:考慮到訓練模型會產生多棵樹,如果在訓練每棵樹的時候都用到所有特徵,以來導致運算量加大,二來每棵樹訓練出來也可能千篇一律,沒有太多側重,所以,設置這個參數,使訓練每棵樹的時候只隨機用到部分特徵,在減少整體運算的同時還可以讓每棵樹更注重自己選到的特徵。
  • n_estimators:隨機森林生成樹的個數,默認爲100。
2、控制樣本抽樣參數:
  • bootstrap:每次構建樹是不是採用有放回樣本的方式(bootstrap samples)抽取數據集。可選參數:True和False,默認爲True。
  • oob_score:是否使用袋外數據來評估模型,默認爲False。

boostrap和 oob_score兩個參數一般要配合使用。如果boostrap是False,那麼每次訓練時都用整個數據集訓練,如果boostrap是True,那麼就會產生袋外數據。

先解釋一下袋外數據的概念:

在一個含有n個樣本的原始訓練集中,我們每次隨機取出一個樣本並記錄,並在抽取下一個樣本之前將該樣本放回原始訓練集,即下次採樣時這個樣本依然可能被採集到,這樣採集n次,最終得到一個和原始訓練集一樣大的子數據集。

由於是隨機採樣,這樣每次的子數據集和原始數據集都不同,用這些子數據集來各自訓練一棵樹,這些樹的參數自然也就各不相同了。

然而有放回抽樣也會有自己的問題。由於是有放回,一些樣本可能在同一個自助集中出現多次,而其他一些卻可能被忽略,一般來說,每一次抽樣,某個樣本被抽到的概率是 1/n ,所以不被抽到的概率就是 1-1/n ,所以n個樣本都不被抽到的概率就是:(11n)n(1-\frac{1}{n})^n 用洛必達法則化簡,可以得到這個概率收斂於(1/e),約等於0.37。

因此,如果數據量足夠大的時候,會有約37%的訓練數據被浪費掉,沒有參與建模,這些數據被稱爲袋外數據(out of bag data,簡寫爲oob)。

爲了這些數據不被浪費,我們也可以把他們用來作爲集成算法的測試集。也就是說,在使用隨機森林時,我們可以不劃分測試集和訓練集,只需要用袋外數據來測試我們的模型即可。

當然,這需要樣本數據n和分類器個數n_estimators都很大,如果數據集過少,很可能就沒有數據掉落在袋外,自然也就無法使用oob數據來測試模型了。

當bootstrap參數取默認值True時,表示抽取數據集時採用這種有放回的隨機抽樣技術。如果希望用袋外數據來測試,則需要在實例化時就將oob_score這個參數調整爲True,訓練完畢之後,我們可以用隨機森林的另一個重要屬性:oob_score_來查看我們的在袋外數據上測試的結果,代碼如下:

#訓練一個模型,讓oob_score=True
RF1 = RandomForestClassifier(n_estimators=25,oob_score=True)
RF1 =RF1.fit(x,y)
#查看oob_score_,即袋外誤差
RF1.oob_score_

輸出:0.9393433500278241

這個就是沒有參與訓練的數據集在模型上的測試得分。

3、不重要參數
  • max_samples:構建每棵樹需要抽取的最大樣本數據量,默認爲None,即每次抽取樣本數量和原數據量相同。
  • n_jobs::設定fit和predict階段並列執行的CPU核數,如果設置爲-1表示並行執行的任務數等於計算機核數。默認爲None,即採用單核計算。
  • verbose:控制構建數過程的冗長度,默認爲0。一般不需要管這個參數。
  • warm_start:當設置爲True,重新使用之前的結構去擬合樣例並且加入更多的估計器(estimators,在這裏就是隨機樹)到組合器中。默認爲 False

以上這幾個參數只需要簡單瞭解即可,大多數參數在使用過稱重不用調整,只是需要注意一點,n_jobs默認爲None,爲了加快速度,可以把n_jobs設置爲-1。

二)隨機森林調參順序

介紹完了這些參數,接下來就要介紹隨機森林的調參順序了,隨機森林的調參順序一般遵循先重要後次要、先粗放後精細的原則,即先確定需要多少棵樹參與建模,再對每棵樹做細緻的調參,精益求精。

相對於xgboost等算法,隨機森林的調參還是相對比較簡單,因爲各個參數之間互相影響的程度很小,只需要按步驟調整即可。

結合我們決策樹文章中提到的參數以及今天所講的兩個參數,隨機森林中主要用來調參的參數有6個:

  • n_estimators:
  • criterion
  • max_depth
  • min_samples_split
  • min_samples_leaf
  • max_features

調參順序如下:

1)選擇criterion參數(決策樹劃分標準)

和決策樹一樣,這個參數只有兩個參數 ‘entropy’(熵) 和 ‘gini’(基尼係數)可選,默認爲gini,這裏簡單測試一下就好

RF = RandomForestClassifier(random_state = 66)
score = cross_val_score(RF,x,y,cv=10).mean()
print('基尼係數得分: %.4f'%score)
RF = RandomForestClassifier(criterion = 'entropy',random_state = 66)
score = cross_val_score(RF,x,y,cv=10).mean()
print('熵得分: %.4f'%score)

輸出:

基尼係數得分: 0.9278
熵得分: 0.9249

這裏看到,依舊是選用gini係數模型效果更好。

2)探索n_estimators的最佳值。

接下來纔是進入真正的調參環節。根據上述調參原則,我們先看看用幾棵樹模型的表現最好。一般來說,樹的棵數越多,模型效果表現越好,但樹的棵數達到一定的數量之後,模型精確度不再上升,訓練這個模型的計算量卻逐漸變大。這個時候,再加樹的數量就沒必要了。就好比你餓的時候每吃一個饅頭就特別頂飽,但是吃到一定數量的饅頭之後,再吃就要撐着了。

只要找到這個臨界值,這個參數就調好了。爲了觀察得分隨着樹增多的變化,我們依然繪製決策樹調參時的學習曲線。

###調n_estimators參數
ScoreAll = []
for i in range(10,200,10):
    DT = RandomForestClassifier(n_estimators = i,random_state = 66) #,criterion = 'entropy'
    score = cross_val_score(DT,data.data,data.target,cv=10).mean()
    ScoreAll.append([i,score])
ScoreAll = np.array(ScoreAll)

max_score = np.where(ScoreAll==np.max(ScoreAll[:,1]))[0][0] ##這句話看似很長的,其實就是找出最高得分對應的索引
print("最優參數以及最高得分:",ScoreAll[max_score])  
plt.figure(figsize=[20,5])
plt.plot(ScoreAll[:,0],ScoreAll[:,1])
plt.show()

輸出:

最優參數以及最高得分: [120. 0.95560035]
在這裏插入圖片描述

根據曲線,我們進一步縮小範圍,搜索100~130之間的得分。(這裏可以根據經驗自己指定)

###進一步縮小範圍,調n_estimators參數
ScoreAll = []
for i in range(100,130):
    DT = RandomForestClassifier(n_estimators = i,random_state = 66)   #criterion = 'entropy',
    score = cross_val_score(DT,data.data,data.target,cv=10).mean()
    ScoreAll.append([i,score])
ScoreAll = np.array(ScoreAll)

max_score = np.where(ScoreAll==np.max(ScoreAll[:,1]))[0][0] ##這句話看似很長的,其實就是找出最高得分對應的索引
print("最優參數以及最高得分:",ScoreAll[max_score])  
plt.figure(figsize=[20,5])
plt.plot(ScoreAll[:,0],ScoreAll[:,1])
plt.show()

輸出:

最優參數以及最高得分: [117. 0.95727946]
在這裏插入圖片描述

可以看到,117爲得分最高點,我們暫定n_estimators爲117,接着調下邊的參數。

3)探索max_depth(樹的最大深度)最佳參數

###粗調max_depth參數
ScoreAll = []
for i in range(10,30,3):
    DT = RandomForestClassifier(n_estimators = 117,random_state = 66,max_depth =i ) #,criterion = 'entropy'
    score = cross_val_score(DT,data.data,data.target,cv=10).mean()
    ScoreAll.append([i,score])
ScoreAll = np.array(ScoreAll)

max_score = np.where(ScoreAll==np.max(ScoreAll[:,1]))[0][0] ##這句話看似很長的,其實就是找出最高得分對應的索引
print("最優參數以及最高得分:",ScoreAll[max_score])  
plt.figure(figsize=[20,5])
plt.plot(ScoreAll[:,0],ScoreAll[:,1])
plt.show()

輸出:

最優參數以及最高得分: [16. 0.95727946]
在這裏插入圖片描述

轉折點在16,但是16之後一直沒有變化,可以說明就算不限制,所有樹的最大深度也就是16左右,因爲我們以步長爲3搜索的,所以還需要進一步搜索一下16附近的值。

精細搜索之後發現,16這個值就是轉折點,所以暫定max_depth = 16。

4)探索min_samples_split(分割內部節點所需的最小樣本數)最佳參數
min_samples_split最小值就是2,我們就從2開始調起。

###調min_samples_split參數
ScoreAll = []
for i in range(2,10):
    RF = RandomForestClassifier(n_estimators = 117,random_state = 66,max_depth =16,min_samples_split = i ) #,criterion = 'entropy'
    score = cross_val_score(RF,data.data,data.target,cv=10).mean()
    ScoreAll.append([i,score])
ScoreAll = np.array(ScoreAll)

max_score = np.where(ScoreAll==np.max(ScoreAll[:,1]))[0][0] ##這句話看似很長的,其實就是找出最高得分對應的索引
print("最優參數以及最高得分:",ScoreAll[max_score])  
plt.figure(figsize=[20,5])
plt.plot(ScoreAll[:,0],ScoreAll[:,1])
plt.show()

輸出:

最優參數以及最高得分: [2. 0.95727946]
在這裏插入圖片描述

可以看到,隨着min_samples_split增大,模型得分下降,說明沒有出現過擬合現象,min_samples_split暫定2。

5)探索min_samples_leaf(分割內部節點所需的最小樣本數)最佳參數

###調min_samples_leaf參數
ScoreAll = []
for i in range(1,15,2):
    DT = RandomForestClassifier(n_estimators = 117,random_state = 66,max_depth =16,min_samples_leaf = i,min_samples_split = 2 ) 
    score = cross_val_score(DT,data.data,data.target,cv=10).mean()
    ScoreAll.append([i,score])
ScoreAll = np.array(ScoreAll)

max_score = np.where(ScoreAll==np.max(ScoreAll[:,1]))[0][0] ##這句話看似很長的,其實就是找出最高得分對應的索引
print("最優參數以及最高得分:",ScoreAll[max_score])  
plt.figure(figsize=[20,5])
plt.plot(ScoreAll[:,0],ScoreAll[:,1])
plt.show()

輸出:

最優參數以及最高得分: [1. 0.95727946]
在這裏插入圖片描述

5)對每棵樹用到的最大特徵數max_features調參:

正常來說,只要這個值不要設置得太小,所有特徵都會被整個森林抽取到用來訓練, 所以相對來說這個值對整個模型的影響不是太大。但這個值越大,單棵樹需要考慮的特徵越多,雖然模型的表現可能會更好,但是增加這個值會導致算法運行速度變慢,所以需要我們考慮去找一個平衡值。

#調max_features參數
param_grid = {
    'max_features':np.arange(0.1, 1)}

rfc = RandomForestClassifier(random_state=66,n_estimators = 117,max_depth = 16,min_samples_leaf =1 ,min_samples_split =4 )
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
print(GS.best_params_)
print(GS.best_score_)

輸出:

{‘max_features’: 0.1}
0.9560335195530726

如果時間充裕,接下來也可以將min_samples_leaf和min_samples_split作爲網格參數聯調一下,因爲這兩個參數會相互影響,這裏暫時省略這一步。

此時,我們得到的參數如下:

參數
n_estimators 117
max_depth 16
min_samples_leaf 1
min_samples_split 2
max_features 0.1

6)在得到的最優參數附近進行小範圍網格搜索

因爲手動調參時,這些參數可能會相互影響,導致我們得到的參數還不是最優的。所以在最優參數附近進行小範圍的網格搜索,排出相互影響的因素,尤其是在數據集量比較少時,小範圍搜索一下可能會有意外收穫。

import time
start = time.time()

param_grid = {
  'n_estimators':np.arange(140, 150),
  'max_depth':np.arange(15, 18),
  'min_samples_leaf':np.arange(1, 8),
  'min_samples_split':np.arange(2, 5),
   'max_features':np.arange(0.1, 1)
}

rfc = RandomForestClassifier(random_state=66)
GS = GridSearchCV(rfc,param_grid,cv=10)
GS.fit(data.data,data.target)
end = time.time()
print("循環運行時間:%.2f秒"%(end-start))
print(GS.best_params_)
print(GS.best_score_)

輸出如下:

循環運行時間:3052.67秒
{‘max_depth’: 16, ‘max_features’: 0.1, ‘min_samples_leaf’: 1, ‘min_samples_split’: 3, ‘n_estimators’: 143}
0.9599332220367279

可以看到,精確度又上升了一點,此時,我們得到的新舊參數對比如下:

參數 舊值 新值
n_estimators 117 143
max_depth 16 16
min_samples_leaf 1 1
min_samples_split 2 3
max_features 0.1 0.1

對比一下新舊參數,可以看到參數變化還是比較大的,尤其是樹的棵數這個參數。當然,這裏主要原因是我們用來做示例的這個預置的數據集只有1797條數據,導致參數的隨機性太大,在實際使用中數據集數量都是十萬百萬級的,不會出現我們手動調整的參數和小範圍網格搜索參數差別這麼大的情況。

最後需要說明的一點是:隨機森林相對於決策樹來說運行較慢,在調參時可以將參數搜索範圍設置得小一些。

當然,這只是我個人的調參順序,僅用來參考,沒必要這麼固化。實際調參時可以根據實際情況做一些調整。還是那句話:正是調參過程充滿的各種不確定性,纔是調參的真正魅力所在。

五、本系列相關文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章