聲明:本文是站在回歸分析角度講的,分類的理解可能跟這有點不一樣。
1.前言
隨機森林也是集成方法的一種,是對Bagging算法的改進。
隨機森林主要有兩步組成:
1)有放回的隨機抽取樣本數據,形成新的樣本集。這部分和Bagging算法一樣,但是有兩點需要注意:
a)新的樣本集的大小和原始樣本集的大小是一樣的。假如原始樣本有1000個數據,那么新樣本集也要包括1000個數據,只是新樣本集里面會含有部分重復的數據,這樣可以避免過度擬合的問題。
b)每生成一個決策樹,都需要重新對原始數據進行取樣。假如進行k次訓練(即生成k課樹),那么就需要重復k次這個動作
2)無放回的隨機抽取屬性列。假如有12個屬性(即12列),從這12個屬性列中隨機抽取無重復的n列(一般建議是總屬性的1/3)進行運算。每次訓練都需要重新抽取
2.算法實現思路
該算法的核心就是如何實現上述兩個步驟,過程如下:
1)有放回的隨機抽取樣本數據
a)定義一個需要抽取的數據的索引列表
b)使用隨機函數隨機生成和數據集同樣大小的數值填充到索引列表中
c)對數據索引列表排序
d)抽取包含在索引列表中的數據重新組成樣本集,並抽取對應的標簽值組成標簽集
2)無放回的隨機抽取屬性列
a)定義一個需要抽取的屬性的索引列表
b)使用隨機函數隨機生成和屬性數量同樣大小的數值填充到索引列表中
c)對屬性索引列表排序
d)抽取包含在屬性索引列表中的屬性重新組成屬性集
3)從新的樣本集中按照新的屬性集抽取樣本數據集及標簽值,然后進行分析
3.實現過程
實驗數據還是使用分析紅酒口感時使用的數據。原始數據來源
1)划分數據。
把數據划分成訓練集和測試集,並把數據和標簽拆分開。這里多說一點,在進行分析時,一定要把數據分成訓練集和測試集,不能在訓練集上訓練出模型后,然后再用訓練集得出的預測值用於后續分析。因為剛開始的時候對隨機森林的理解有點偏差,所以沒有划分訓練集和測試集,結果反應均方誤差隨模型個數變化的曲線一直是波浪線。
import numpy as np import matplotlib.pyplot as plt import os from sklearn.tree import DecisionTreeRegressor import random ##運行腳本所在目錄 base_dir=os.getcwd() data=np.loadtxt(base_dir+r"\wine.txt",delimiter=";") dataLen = len(data) ##矩陣的長度:行數 dataWid = len(data[0]) ##矩陣的寬度:列數 ''' 第一步:划分訓練集和測試集 ''' ##測試集大小:這里選擇30%作為測試集,70%作為訓練集 nSample = int(dataLen * 0.30) ##在0~dataLen直接隨機生成nSample個點 idxTest = random.sample(range(dataLen), nSample) idxTest.sort() #定義訓練集和測試集標簽 xTrain = [] #訓練集 xTest = [] #測試集 yTrain = [] #訓練集標簽 yTest = [] #測試集標簽 ##划分數據:每行數據最后一個是標簽值 for i in range(dataLen): row = data[i] if i not in idxTest: xTrain.append(row[0:dataWid-1]) yTrain.append(row[-1]) else : xTest.append(row[0:dataWid-1]) yTest.append(row[-1])
2)使用隨機森林算法訓練數據
這里還是使用sklearn包中的二元決策樹函數DecisionTreeRegressor作為主要的分析函數。
另外還需要說兩個隨機函數(不是numpy里面的):
random.choice(range(n)):在range形成的列表里面隨機抽取一個
random.sample(range(n),m):在range形成的列表里面無重復的隨機抽取m個數
''' 第二步:使用隨機森林算法訓練數據 ''' modelList = [] ##模型列表:決策樹的個數 predList = [] ##預測值列表 mse = [] ##均方差列表 allPredictions = [] ##預測值累加和列表 numTreesMax = 100 ##最大樹數目 treeDepth = 12 ##每個樹的深度 nAttr = 4 ##隨機抽取的屬性數目,建議值:回歸問題1/3 ''' 隨機森林思路: 對應每個決策樹: 1.有放回的隨機抽取和樣本數據大小一樣的數據集 2.無放回的隨機抽取小於總屬性個數的屬性 ''' ''' 整個循環過程: 1.外層循環生成模型 2.在循環內部 a)有放回的在訓練集上重新生成樣本數據索引列表idxList b)根據idxList生成樣本數據 c)無放回的隨機抽取屬性值索引列表attList d)根據idxList、attList生成用戶訓練的數據 e)進行訓練 f)在測試集上執行類似步驟,並進行預測 g)測試集上產生的預測值加入列表待用 ''' for iTrees in range(numTreesMax): ##定義決策樹 modelList.append(DecisionTreeRegressor(max_depth=treeDepth)) ##隨機抽取的樣本數據集和標簽集 xList = [] yList = [] ##進行隨機抽取時樣本數據集的索引列表和屬性索引列表 idxList = [] attList = [] ##構造隨機樣本數據集的索引列表 for idx in range(len(xTrain)): idxList.append(random.choice(range(len(xTrain)))) idxList.sort() ##記得排序 ##構造隨機樣本數據集 for idx in idxList: xList.append(xTrain[idx]) yList.append(yTrain[idx]) ##構造隨機屬性列表:dataWid-1,是因為最后一列是標簽值 attList = random.sample(range(dataWid-1),nAttr) attList.sort() ##記得排序 ##構造測試數據集 xTrain1 = [] yTrain1 = [] for i in range(len(xList)): ##只讀取抽取到的列 row = [xList[i][j] for j in attList] xTrain1.append(row) ##yList每行只有一個標簽值 yTrain1.append(yList[i]) ##開始訓練 modelList[-1].fit(xTrain1, yTrain1) ##獲取預測值 ---測試集需要抽取相同的列進行預測 xTest1 = [] for i in range(len(xTest)): ##只讀取抽取到的列 row = [xTest[i][j] for j in attList] xTest1.append(row) latestOutSamplePrediction = modelList[-1].predict(xTest1) ##預測值添加到列表 predList.append(list(latestOutSamplePrediction))
3)通過誤差累加和尋找最佳樹數目
篩選方法:不斷累加模型在測試集上的誤差值,直到這個和值基本保持不變了(即圖像尾部區域直線),此時的模型個數是最優的模型個數。為了方便理解下面循環,用個圖解釋下:
(圖1)
測試集經過在所有的模型上計算后,會形象一個二維的列表(如上圖1所示)。每一行代表一個模型的預測結果。第一次循環就是n1的數據,第二次循環是n1+n2的值,第三次循環存儲的是n1+n2+n3的值,依次類推,直到把模型列表循環完成。
這里有一點需要說明:因為是隨機抽取樣本數據和隨機抽取屬性,所以最優解時的模型的數目不是固定的。
##通過累積均方誤差觀察隨機森林性能 for iModels in range(len(modelList)): prediction = [] ##此循環的目的:每個模型都是把前面的所有的模型的對應列的預測值加起來,形成一個新列表 ##說明:len(xTest) 每個模型的預測都會生成len(xTest)列的一行數據,這里len(xTest) 和len(yTest)是一樣的 for iPred in range(len(xTest)): prediction.append(sum([predList[i][iPred] for i in range(iModels + 1)]) / (iModels + 1)) ##添加到列表 allPredictions.append(prediction) ##計算新的離差 errors = [(yTest[i] - prediction[i]) for i in range(len(yTest))] ##均方差:即離差的平方和的平均數 mse.append(sum([e * e for e in errors]) / len(yTest)) print('Minimum MSE') print(min(mse)) ##0.372633316412 print(mse.index(min(mse))) ## 不確定
4)繪圖觀察誤差平方隨模型數目變化的曲線
''' 第四步:繪圖觀察誤差平方隨模型數目變化的曲線 ''' ####模型個個數+1,繪圖用:即模型列表中的從0開始的下標變成從1開始 的編號 nModels = [i + 1 for i in range(len(modelList))] ##繪圖 plt.plot(nModels,mse) plt.axis('tight') plt.xlabel('Number of Trees in Ensemble') plt.ylabel('Mean Squared Error') plt.ylim((0.0, max(mse))) plt.show()
此次運行結果大概在90以后曲線區域平緩。
4.使用RandomForestRegressor函數實現上述過程
RandomForestRegressor是sklearn包中提供的實現隨機森林算法的函數。下面還會用到另外一個函數:train_test_split。這兩個函數的其他參數很容易理解,重點是說下參數:random_state,這個值在實際環境中取默認值None即可,但是在開發環境中需要指定一個任意固定值,是讓內部的隨機生成器生成的結果固定,方便研究其他變量引起的變化。
from sklearn.model_selection import train_test_split from sklearn import ensemble from sklearn.metrics import mean_squared_error import numpy as np import matplotlib.pyplot as plt import os ##運行腳本所在目錄 base_dir=os.getcwd() data=np.loadtxt(base_dir+r"\wine.txt",delimiter=";") dataLen = len(data) ##矩陣的長度:行數 dataWid = len(data[0]) ##矩陣的寬度:列數 ''' 第一步:把訓練數據和標簽數據分開 ''' xList = [] ##樣本數據集 yList = [] ##標簽集 ##划分數據:樣本數據集和標簽集 for i in range(dataLen): row = data[i] xList.append(row[0:dataWid-1]) yList.append(row[-1]) ##把列表轉成數組 X = np.array(xList) Y = np.array(yList) ##取30%的數據作為測試集,70%的數據作為訓練集 ##random_state設置成固定值是為了多次運行代碼保持一致的結果,在開發階段調整模型。真實環境設置默認值;None xTrain, xTest, yTrain, yTest = train_test_split(X, Y, test_size=0.30, random_state=0) ##樹的數目:建議是嘗試值100~500 nTreeList = range(1, 100, 1) ##均方誤差 mse = [] for iTrees in nTreeList: depth =12 ##樹最大深度,為了保持跟上面實驗一致設為12,建議設置:None maxFeat = 4 ##最大屬性值個數 ##定義模型 wineRFModel = ensemble.RandomForestRegressor(n_estimators=iTrees, max_depth=depth, max_features=maxFeat,random_state=0) ##開始訓練 wineRFModel.fit(xTrain,yTrain) #計算測試集上的預測值 prediction = wineRFModel.predict(xTest) ##計算均方差並加入到列表 mse.append(mean_squared_error(yTest, prediction)) print('Minimum MSE') print(min(mse)) #0.378757382093 print(mse.index(min(mse))) #45 #繪制均方誤差隨模型數目的變化曲線 plt.plot(nTreeList, mse) plt.xlabel('Number of Trees in Ensemble') plt.ylabel('Mean Squared Error') plt.ylim([0.0, 1.1*max(mse)]) plt.show()
按照上面的設置每次生成的最佳模型數目都是27,但是random_state改成None后,最優解時的樹的數目也不固定了。
5.總結
上述兩種方法使用的數據一樣,參數也盡可能保持一致,生成的結果就均方誤差而言也基本一致。但是第一種方法最佳模型數目是99時所需要的時間也遠遠少於第二種方法模型數目是27的所需的時間。其時間比大概是1:4的樣子,也就是說后者所需時間是前者所需時間的4倍,所以針對一些可以自己動手寫代碼的部分,並且不會產生太大偏差的時候,還是自己寫代碼比較好。這個例子告訴我們:並不是所有的現成類庫的性能都優於自己寫的代碼性能。