GBDT分類的原理及Python實現
程序員大咖
點擊右側關注,免費進階高級!
作者:李小文
Github: https://gith
ub.com/tushu
shu
提到GBDT分類相信大家應該都不會覺得陌生,本文就GBDT分類的基本原理進行講解,並手把手、肩並肩地帶您實現這一演算法。
完整實現代碼請參考本人的github:
//github.com/tushushu/imylu/blob/master/imylu/ensemble/gbdt_base.pyhttps:
https:
//github.com/tushushu/imylu/blob/master/imylu/ensemble/gbdt_classifier.py
https:
//github.com/tushushu/imylu/blob/master/examples/gbdt_classifier_example.py
一. 原理篇
我們用人話而不是大段的數學公式來講講GBDT分類是怎麼一回事。
1.1 溫故知新
GBDT分類只是在GBDT回歸上做了一點點改造,而GBDT分類又是建立在回歸樹的基礎上的。 之前寫過一篇GBDT回歸的文章,鏈接如下:
GBDT回歸的原理及Python實現
之前寫過一篇回歸樹的文章,鏈接如下:
回歸樹的原理及Python實現
1.2 Sigmoid函數
如果對邏輯回歸或者神經網路有所了解的話,那麼對Sigmoid函數應該不會感到陌生,它的函數表達式是:
不難得出:
所以,Sigmoid函數的值域是(0, 1),導數為y * (1 - y)
1.3 改造GBDT回歸
根據《GBDT回歸》可知,假設要做m輪預測,預測函數為Fm,初始常量或每一輪的回歸樹為fm,輸入變數為X,有:
由於是回歸問題,函數F的值域在(-∞, +∞),而二分類問題要求預測的函數值在(0, 1),所以我們可以用Sigmoid函數將最終的預測值的值域控制在(0, 1)之間,其函數表達式如下:
1.4 預測見面
以預測相親是否見面來舉例,見面用1表示,不見面用0表示。從《回歸樹》那篇文章中我們可以知道,如果需要通過一個常量來預測同事的年齡,平均值是最佳選擇之一。那麼預測二分類問題,這個常量該如何計算呢?我們簡單證明一下:
結論,如果要用一個常量來預測y,用log(sum(y)/sum(1-y))是一個最佳的選擇。
1.5 見面的殘差
我們不妨假設三個相親對象是否見面分別為[1, 0, 1],那麼預測是否見面的初始值z = log((1+0+1)/(0+1+0)) = 0.693,所以我們用0.693這個常量來預測同事的年齡,即Sigmoid([0.693, 0.693, 0.693]) = [0.667, 0.667, 0.667]。每個相親對象是否見面的殘差 = 是否見面 - 預測值 = [1, 0, 1] - [0.667, 0.667, 0.667],所以殘差為[0.333, -0.667, 0.333]
1.6 預測見面的殘差
為了讓模型更加準確,其中一個思路是讓殘差變小。如何減少殘差呢?我們不妨對殘差建立一顆回歸樹,然後預測出準確的殘差。假設這棵樹預測的殘差是[1, -0.5, 1],將上一輪的預測值和這一輪的預測值求和,之後再求Sigmoid值,每個相親對象是否見面 = Sigmoid([0.693, 0.693, 0.693] + [1, -0.5, 1]) = [0.845, 0.548, 0.845],顯然與真實值[1, 0, 1]更加接近了, 每個相親對象是否見面的殘差此時變為[0.155, -0.548, 0.155],預測的準確性得到了提升。
1.7 GBDT
重新整理一下思路,假設我們的預測一共迭代3輪 是否見面:[1, 0, 1]
第1輪預測:Sigmoid(0.693, 0.693, 0.693) = [0.667, 0.667, 0.667]
第1輪殘差:[0.333, -0.667, 0.333]
第2輪預測:Sigmoid(0.693, 0.693, 0.693 + [1, -0.5, 1]) (第1顆回歸樹)) = Sigmoid([1.693, 0.193, 1.693]) = [0.845, 0.548, 0.845]
第2輪殘差:[0.155, -0.548, 0.155]
第3輪預測:Sigmoid(0.693, 0.693, 0.693 + 1, -0.5, 1 + 2, -1, 2) = Sigmoid([3.693, -0.807, 3.693]) = [0.976, 0.309, 0.976]
第3輪殘差:[0.024, -0.309, 0.024]
看上去殘差越來越小,而這種預測方式就是GBDT演算法。
1.8 公式推導
看到這裡,相信您對GBDT已經有了直觀的認識。這麼做有什麼科學依據么,為什麼殘差可以越來越小呢?前方小段數學公式低能預警。
因此,我們需要通過用第m-1輪的預測值和殘差來得到函數fm,進而優化函數fm。而回歸樹的原理就是通過最佳劃分區域的均值來進行預測,與GBDT回歸不同,要把這個均值改為1.7式11。所以fm可以選用回歸樹作為基礎模型,將初始值,m-1顆回歸樹的預測值相加再求Sigmoid值便可以預測y。
二. 實現篇
本人用全宇宙最簡單的編程語言——Python實現了GBDT分類演算法,沒有依賴任何第三方庫,便於學習和使用。簡單說明一下實現過程,更詳細的注釋請參考本人github上的代碼。
2.1 導入回歸樹類
回歸樹是我之前已經寫好的一個類,在之前的文章詳細介紹過,代碼請參考:
https: /github.com/tushushu/imylu/blob/master/imylu/tree/regression
from ..tree.regression_tree import RegressionTree
2.2 創建GradientBoostingBase類
初始化,存儲回歸樹、學習率、初始預測值和變換函數。
class GradientBoostingBase(object)
def
__init__(self)
:self.trees =
None
self.lr =
None
self.init_val =
None
self.fn =
lambda
x: sigmoid(x)2.3 計算初始預測值
初始預測值,見1.7式10。
def _get_init_val( self
n = len(y)
y_sum = sum(y)
return
log((y_sum) / (n - y_sum))2.4 匹配葉結點
計算訓練樣本屬於回歸樹的哪個葉子結點。
while left and right if split left else rightdef _match_node(self, row, tree):
nd = tree.root
nd = nd.
nd = nd.
return nd
2.5 獲取葉節點
獲取一顆回歸樹的所有葉子結點。
def _get_leaves(self, tree)
nodes = []
que = [tree.root]
while
que:node = que.pop(
0
)if
node.leftis
None
or
node.rightis
None
:nodes.append(node)
continue
left_node = node.left
right_node = node.right
que.append(left_node)
que.append(right_node)
return
nodes2.6 劃分區域
將回歸樹的葉子結點,其對應的所有訓練樣本存入字典。
def _divide_regions( self
regions = {
node:
[]for
nodein
nodes}for
i, rowin
enumerate(X):node =
self
._match_node(row, tree)regions[node].append(i)
return
regions2.7 計算預測值
見1.7式11。
def _get_score( self
numerator = denominator =
0
for
idxin
idxs:
numerator += residuals[idx]
denominator += y_hat[idx] * (
1
- y_hat[idx])return
numerator / denominator2.8 更新預測值
更新回歸樹各個葉節點的預測值。
def _update_score( self
nodes =
self
._get_leaves(tree)regions =
self
._divide_regions(tree, nodes, X)for
node, idxsin
regions.items():node.score =
self
._get_score(idxs, y_hat, residuals)tree._get_rules()
2.9 計算殘差
def _get_residuals( self
return
[yi -self
.fn(y_hat_i)for
yi, y_hat_iin
zip(y, y_hat)]2.10 訓練模型
訓練模型的時候需要注意以下幾點:
控制樹的最大深度max_depth;
控制分裂時最少的樣本量min_samples_split;
訓練每一棵回歸樹的時候要乘以一個學習率lr,防止模型過擬合;
對樣本進行抽樣的時候要採用有放回的抽樣方式。
def fit( self
self
.init_val =self
._get_init_val(y) n = len(y)
y_hat = [
self
.init_val] * nresiduals =
self
._get_residuals(y, y_hat)
self
.trees = []self
.lr = lrfor
_
in
range(n_estimators):idx = range(n)
if
subsample isnot
None:
k = int(subsample * n)
idx = choices(population=idx, k=k)
X_sub = [X[i]
for
iin
idx]residuals_sub = [residuals[i]
for
iin
idx]y_hat_sub = [y_hat[i]
for
iin
idx] tree = RegressionTree()
tree.fit(X_sub, residuals_sub, max_depth, min_samples_split)
self
._update_score(tree, X_sub, y_hat_sub, residuals_sub)y_hat = [y_hat_i + lr * res_hat_i
for
y_hat_i,res_hat_i
in
zip(y_hat, tree.predict(X))]residuals =
self
._get_residuals(y, y_hat)self
.trees.append(tree)2.11 預測一個樣本
def _predict( self
return
self
.fn(self
.init_val + sum(self
.lr * tree._predict(Xi)for
treein
self
.trees))2.12 預測多個樣本
def predict( self
return
[int(self
._predict(Xi) >= threshold)for
Xiin
X]三. 效果評估
3.1 main函數
使用著名的乳腺癌數據集,按照7:3的比例拆分為訓練集和測試集,訓練模型,並統計準確度。
@run_time def main()
print(
"Tesing the accuracy of GBDT classifier..."
)X, y = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=
20
) clf = GradientBoostingClassifier()
clf.fit(X_train, y_train, n_estimators=
2
,lr=
0.8
, max_depth=3
, min_samples_split=2
) get_acc(clf, X_test, y_test)
3.2 效果展示
最終準確度93.082%,運行時間14.9秒,效果還算不錯~
3.3 工具函數
本人自定義了一些工具函數,可以在github上查看
//github.com/tushushu/imylu/blob/master/imylu/utils.pyhttps:
run_time - 測試函數運行時間
load_breast_cancer - 載入乳腺癌數據
train_test_split - 拆分訓練集、測試集
get_acc - 計算準確度
總結
GBDT分類的原理:GBDT回歸加Sigmoid
GBDT分類的實現:一言難盡
【點擊成為源碼大神】
TAG:Python開發 |