本文目的 git
當前spark(1.3版)隨機森林實現,沒有包括OOB錯誤評估和變量權重計算。而這兩個功能在實際工做中比較經常使用。OOB錯誤評估能夠代替交叉檢驗,評估模型總體結果,避免交叉檢驗帶來的計算開銷。如今的數據集,變量動輒成百上千,變量權重有助於變量過濾,去掉無用變量,提升計算效率,同時也能夠幫助理解業務。因此,本人在原始代碼基礎上,擴展了這兩個功能,下面記錄實現過程,做爲備忘錄(參考代碼)。github
總體思路 算法
Random Forest實現中,大多數內部對象是私有(private[tree])的,因此擴展代碼使用了org.apache.spark.mllib.tree的命名空間,複用這些內部對象。實現過程當中,須要放回抽樣數據,Random Forest的原始實現是放在局部變量baggedInput中,外部沒法訪問,因此擴展代碼必須冗餘部分原始代碼,用於訪問baggedInput變量。原始實現中,會先將LabeledPoint轉成裝箱對象TreePoint,可是在計算OOB時,須要LabeledPoint,因此須要實現TreePoint到LabeledPoint的轉換邏輯。對於連續變量,取每一個箱的中間值做爲反轉後的結果;離散變量不須要作修改。固然爲何取中間值,爲何取三分之一或其餘什麼地方?由於在裝箱過程當中,數據的分佈信息已丟失,因此取0.5是一個能夠接受的選擇。apache
OOB錯誤評估 服務器
原理 數據結構
OOB是Out-Of-Bag的縮寫,顧名思義,使用那些out of bag的數據進行錯誤度量。隨機森林在訓練開始時,會根據樹的數量n,進行n次有放回採樣,用於訓練每一棵樹。放回採樣,必然致使一部分數據被選中,另一部分數據沒有選中,選中了就放到bag中,沒有選中的就是out of bag。平均而言,每次放回採用中,37%的數據不會被選中,詳細推導見附錄【Out Of Bag機率】。這些沒有選中的數據不參與建模,因此能夠做爲驗證數據,評估模型效果。對於每一條記錄,若參與了m棵樹的建模,則n-m樹沒有參與建模,那麼就能夠將這剩下的n-m棵樹做爲子森林,進行分類驗證,列子以下:併發
Tree 1dom |
Tree 2機器學習 |
Tree 3分佈式 |
Tree 4 |
Tree 5 |
Tree 6 |
Tree 7 |
|
Data 1 |
* |
* |
* |
||||
Data 2 |
* |
* |
* |
* |
|||
Data 3 |
* |
* |
* |
||||
Data 4 |
* |
* |
* |
* |
|||
Data 5 |
* |
* |
* |
上面的表格中,每行表示一個記錄,每列表示一顆樹,"*"表示該記錄參與了某棵樹的建模。對於Data 1,參與了Tree 2,4,6的建模過程,那麼可選取Tree 3,5,7做爲子隨機森林模型,並計算Data 1的分類結果。
實現
上面提到了OOB的原理和評估方法,下面介紹如何實現,
private def computeOobError( strategy: Strategy, baggedInput: RDD[BaggedPoint[TreePoint]], bins: EnhancedRandomForest.BinList, forest: RandomForestModel): Double = {
val actualPredictPair = baggedInput .map(x => { val labeledPoint = EnhancedTreePoint.treePointToLabeledPoint( x.datum, bins, strategy) (x.subsampleWeights.zipWithIndex.filter(_._1 == 0) .map(_._2).map(index => forest.trees(index)), labeledPoint) }) .filter(_._1.size > 0) // 過濾掉無樹的森林 .map({ case (oobTrees, labeledPoint) => { val subForest = new RandomForestModel(strategy.algo, oobTrees) (labeledPoint.label, subForest.predict(labeledPoint.features)) } })
val totalCount = actualPredictPair.count assert(totalCount > 0, s"OOB error measure: data number is zeror") strategy.algo match { case Regression => actualPredictPair.map(x => Math.pow(x._1 - x._2, 2)).reduce(_ + _) / totalCount case Classification => actualPredictPair.filter(x => x._1 != x._2).count.toDouble / totalCount }
} |
上面函數截取了oob實現的主邏輯,baggedInput是一個特殊的數據結構,用於記錄每一條記錄選中的信息。好比須要創建10棵樹,那麼bagged有10列,每一列記錄了在當前這棵樹,該記錄被選中了幾回。因此,使用0標示那些沒有選中的記錄。獲得了那些當前數據沒有參與建模的樹後,構建字隨機森林模型並預測結果,最後經過真實數據與預測數據,計算OOB錯誤評估。對於迴歸問題,使用平方錯誤;分類問題,使用錯誤率。固然,錯誤的評估方式,後面還能夠擴展。
變量重要性
原理
如何描述變量重要性,一種直觀的理解是變量越重要,若是混淆它,那麼模型效果會越差。混淆處理有幾種方案,好比根據某種隨機分佈,加減隨機值,或者隨機填充值。可是須要額外的參數,而且會影響原有的數據分佈。因此,採起隨機排序混淆變量,這樣不會改變原始的數據分佈,也不須要而外的參數。
具體作法是以下:
I(i)越高,說明變量i越重要;若是I(i) = 0,那麼說明變量i沒有什麼做用;若是I(i) < 0,那麼說明變量i有很明顯的噪音,對模型產生了負面影響。
實現
實現的難點是隨機排序,在大規模分佈式數據上,實現隨機排序至少須要一次排序,會很是消耗計算資源。擴展代碼中使用了一個小技巧,利用spark隨機森林的內部結構TreePoint,避免排序,提升隨機排序效率。由於TreePoint是裝箱數據,每一個變量的值是箱索引,通常不超過100個。因此只須要將箱索引進行隨機排序,就能夠達到對整個數據進行隨機排序的目的。
private def computeVariableImportance( strategy: Strategy, baggedInput: RDD[BaggedPoint[TreePoint]], bins: EnhancedRandomForest.BinList, forest: RandomForestModel, oobError: Double): Array[Double] = {
(0 until bins.size).par.map(featureIndex => {
val binCount = if (strategy.categoricalFeaturesInfo.contains(featureIndex)) { // category feature strategy.categoricalFeaturesInfo(featureIndex) } else { // continuous feature bins(featureIndex).size } val shuffleBinFeature = Random.shuffle((0 until binCount).toList) // 每一個元素對應shuffle後的數據 val shuffleOneFeatureBaggedInput = baggedInput.map(x => { val currentFeatureBinIndex = x.datum.binnedFeatures(featureIndex) x.datum.binnedFeatures(featureIndex) = shuffleBinFeature(currentFeatureBinIndex) x }) computeOobError(strategy, shuffleOneFeatureBaggedInput, bins, forest) - oobError }).toArray
} |
上面的使用中,使用list.par.map的併發操做,同時對全部的變量計算重要性,具體的調度有spark服務器控制,最大限度利用spark的資源。
聚合模型穩定的理論依據
隨機森林背後的主要思想是聚合模型(Ensemble Model)。爲何聚合模型效果好於單一模型(理論推導,請參考附錄【聚合模型錯誤評估】)?直觀的理解,當不少模型進行投票時,有一些模型會犯錯,另一些模型正確,那麼正確的投票會與錯誤的投票抵消,總體上只要最終正確的投票多於錯誤的投票,哪怕多一票,那麼就會獲得正確的結果。因爲相互抵消,因此聚合效果比單一模型穩定。聚合模型中,須要模型間具備較大差別,這樣才能覆蓋數據的不一樣方面,這也是爲何隨機森林在數據的行和列兩個維度上,添加隨機過程,用於增大模型之間的差別。
引用一句諺語,"三個臭皮匠,頂一個諸葛亮",能夠形象的解釋。好比這裏有101個臭皮匠,假設他們對一件事情的判斷正確的機率是0.57,而諸葛亮對這件事情判斷正確的機率是0.9。那麼,假設這101個臭皮匠經過投票判斷,那麼機率能夠達到0.92(R代碼:sum(dbinom(51:101,101,0.57))),比諸葛亮強!
總結
經過擴展這兩個功能,從新溫習了臺大《機器學習技法》相關課程,同時在真實數據上檢驗了Random Forest的模型效果。實踐檢驗了整理,學以至用,感受很知足。同時,在閱讀org.apache.spark.mllib.tree源代碼的時候,學習到了一些分佈式數據集上算法實現的技巧。但願這些分享對你有用。
參考資料
附錄
Out Of Bag機率
設N爲樣本大小,那麼N次有放回抽樣中,一次沒有選中的機率能夠表示以下,
當N趨近於無求大時,P(OOB)會收斂到常量,下面給出證實,
其中數學常數定義以下:
聚合模型錯誤評估
下面經過聚合迴歸模型進行簡單推導,gt是相同數據集D中,使用T個算法生成的T個模型中隨機選擇的一個模型,G是這T個模型聚合,使用平均做爲最終結果,有
f模型用於生成數據D,須要用T個機器學習算法逼近。如今指望研究(G(x)-f(x))2與avg((gt(x)-f(x))2)的關係。x是固定值,後面的公式爲了簡單,會省略。
直觀的理解,是誤差(Bias),是方差(Variance),任意單一模型的平方錯誤指望大於等於平均模型的錯誤的平方。並且,模型差別若是越大,那麼方差variance越大,那麼Bias越小,也就是聚合模型與f越接近。