一.簡介
xgboost在整合學習中佔有重要的一席之位,通常在各大競賽中作為殺器使用,同時它在工業落地上也很方便,目前針對大資料領域也有各種分散式實現版本,比如xgboost4j-spark,xgboost4j-flink等。xgboost的基礎也是gbm,即梯度提升模型,它在此基礎上做了進一步優化...
二.損失函式:引入二階項
xgboost的損失函式構成如下,即一個經驗損失項+正則損失項:
這裡\(n\)表示樣本數,\(F_{m-1}\)表示前\(m-1\)輪模型,\(f_m\)表示第\(m\)輪新訓練模型,所以\(F_m=F_{m-1}+f_m\),\(\Omega(f_m)\)是對第\(m\)輪新訓練模型進行約束的正則化項,在前面第6小節做過探索,對損失函式近似做二階泰勒展開,並對近似損失函式做優化,通常會收斂的更快更好,接下里看下對第\(i\)個樣本的經驗項損失函式做二階展開:
這裡:
對於第\(m\)輪,\(L(y_i,F_{m-1}(x_i))\)為常數項,不影響優化,可以省略掉,所以損失函式可以表示為如下:
這便是xgboost的學習框架,針對不同問題,比如迴歸、分類、排序,會有不同的\(L(\cdot)\)以及\(\Omega(\cdot)\),另外由於需要二階資訊,所以\(L(\cdot)\)必須要能二階可微,接下來對基學習器為決策樹的情況做推導
三.基學習器:迴歸決策樹
下面推導一下基學習器為迴歸樹的情況,當選擇決策樹時,它的正則化項如下:
其中,\(j=1,2,...,T\)表達對應的葉節點編號,\(\omega_j\)表示落在第\(j\)個葉節點的樣本的預測值,即:
\(I_j\)表示第\(j\)個葉子節點所屬區域,所以決策樹的損失函式可以改寫為如下:
這其實是關於\(\omega\)的一元二次函式,直接寫出它的最優解:
這裡\(G_j=\sum_{i\in I_j}g_i,H_j=\sum_{i\in I_j}h_i\),可見\(L_2\)正則項起到了縮小葉子節點權重的效果,減少其對整個預測結果的影響,從而防止過擬合,將\(\omega_j^*\)帶入可得損失值:
特徵選擇
很顯然,上面的損失函式可以直接用於特徵選擇中,對某節點在分裂前的評分為:
分裂後,左右子節點的評分和為:
所以分裂所能帶來的增益:
這裡\(G=G_L+G_R,H=H_L+H_R\)
四.程式碼實現
這部分對xgboost中的迴歸樹做簡單實現,大體流程其實與CART迴歸樹差不多,下面說下它與CART迴歸樹不一樣的幾個點:
(1)這裡fit
與之前的CART迴歸樹有些不一樣了,之前是fit(x,y)
,而現在需要fit(x,g,h)
;
(2)特徵選擇不一樣了,之前是求平方誤差的增益,現在需要利用一階和二階導數資訊,見上面的\(Score\)
(3)葉子節點的預測值不一樣了,之前是求均值,現在需利用一階和二階導數資訊,見上面的\(w_j^*\)
接下來對xgboost所需要用到的迴歸樹做簡單實現
import os
os.chdir('../')
import numpy as np
from ml_models.wrapper_models import DataBinWrapper
"""
xgboost基模型:迴歸樹的實現,封裝到ml_models.ensemble
"""
class XGBoostBaseTree(object):
class Node(object):
"""
樹節點,用於儲存節點資訊以及關聯子節點
"""
def __init__(self, feature_index: int = None, feature_value=None, y_hat=None, score=None,
left_child_node=None, right_child_node=None, num_sample: int = None):
"""
:param feature_index: 特徵id
:param feature_value: 特徵取值
:param y_hat: 預測值
:param score: 損失函式值
:param left_child_node: 左孩子結點
:param right_child_node: 右孩子結點
:param num_sample:樣本量
"""
self.feature_index = feature_index
self.feature_value = feature_value
self.y_hat = y_hat
self.score = score
self.left_child_node = left_child_node
self.right_child_node = right_child_node
self.num_sample = num_sample
def __init__(self, max_depth=None, min_samples_split=2, min_samples_leaf=1, gamma=1e-2, lamb=1e-1,
max_bins=10):
"""
:param max_depth:樹的最大深度
:param min_samples_split:當對一個內部結點劃分時,要求該結點上的最小樣本數,預設為2
:param min_samples_leaf:設定葉子結點上的最小樣本數,預設為1
:param gamma:即損失函式中的gamma
:param lamb:即損失函式中lambda
"""
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.min_samples_leaf = min_samples_leaf
self.gamma = gamma
self.lamb = lamb
self.root_node: self.Node = None
self.dbw = DataBinWrapper(max_bins=max_bins)
def _score(self, g, h):
"""
計算損失損失評分
:param g:一階導數
:param h: 二階導數
:return:
"""
G = np.sum(g)
H = np.sum(h)
return -0.5 * G ** 2 / (H + self.lamb) + self.gamma
def _build_tree(self, current_depth, current_node: Node, x, g, h):
"""
遞迴進行特徵選擇,構建樹
:param x:
:param y:
:param sample_weight:
:return:
"""
rows, cols = x.shape
# 計算G和H
G = np.sum(g)
H = np.sum(h)
# 計算當前的預測值
current_node.y_hat = -1 * G / (H + self.lamb)
current_node.num_sample = rows
# 判斷停止切分的條件
current_node.score = self._score(g, h)
if rows < self.min_samples_split:
return
if self.max_depth is not None and current_depth > self.max_depth:
return
# 尋找最佳的特徵以及取值
best_index = None
best_index_value = None
best_criterion_value = 0
for index in range(0, cols):
for index_value in sorted(set(x[:, index])):
left_indices = np.where(x[:, index] <= index_value)
right_indices = np.where(x[:, index] > index_value)
criterion_value = current_node.score - self._score(g[left_indices], h[left_indices]) - self._score(
g[right_indices], h[right_indices])
if criterion_value > best_criterion_value:
best_criterion_value = criterion_value
best_index = index
best_index_value = index_value
# 如果減少不夠則停止
if best_index is None:
return
# 切分
current_node.feature_index = best_index
current_node.feature_value = best_index_value
selected_x = x[:, best_index]
# 建立左孩子結點
left_selected_index = np.where(selected_x <= best_index_value)
# 如果切分後的點太少,以至於都不能做葉子節點,則停止分割
if len(left_selected_index[0]) >= self.min_samples_leaf:
left_child_node = self.Node()
current_node.left_child_node = left_child_node
self._build_tree(current_depth + 1, left_child_node, x[left_selected_index], g[left_selected_index],
h[left_selected_index])
# 建立右孩子結點
right_selected_index = np.where(selected_x > best_index_value)
# 如果切分後的點太少,以至於都不能做葉子節點,則停止分割
if len(right_selected_index[0]) >= self.min_samples_leaf:
right_child_node = self.Node()
current_node.right_child_node = right_child_node
self._build_tree(current_depth + 1, right_child_node, x[right_selected_index], g[right_selected_index],
h[right_selected_index])
def fit(self, x, g, h):
# 構建空的根節點
self.root_node = self.Node()
# 對x分箱
self.dbw.fit(x)
# 遞迴構建樹
self._build_tree(1, self.root_node, self.dbw.transform(x), g, h)
# 檢索葉子節點的結果
def _search_node(self, current_node: Node, x):
if current_node.left_child_node is not None and x[current_node.feature_index] <= current_node.feature_value:
return self._search_node(current_node.left_child_node, x)
elif current_node.right_child_node is not None and x[current_node.feature_index] > current_node.feature_value:
return self._search_node(current_node.right_child_node, x)
else:
return current_node.y_hat
def predict(self, x):
# 計算結果
x = self.dbw.transform(x)
rows = x.shape[0]
results = []
for row in range(0, rows):
results.append(self._search_node(self.root_node, x[row]))
return np.asarray(results)
下面簡單測試一下功能,假設\(F_0(x)=0\),損失函式為平方誤差的情況,則其一階導為\(g=F_0(x)-y=-y\),二階導為\(h=1\)
#構造資料
data = np.linspace(1, 10, num=100)
target1 = 3*data[:50] + np.random.random(size=50)*3#新增噪聲
target2 = 3*data[50:] + np.random.random(size=50)*10#新增噪聲
target=np.concatenate([target1,target2])
data = data.reshape((-1, 1))
import matplotlib.pyplot as plt
%matplotlib inline
model=XGBoostBaseTree(lamb=0.1,gamma=0.1)
model.fit(data,-1*target,np.ones_like(target))
plt.scatter(data, target)
plt.plot(data, model.predict(data), color='r')
[<matplotlib.lines.Line2D at 0x1d8fa8fd828>]
分別看看lambda和gamma的效果
model=XGBoostBaseTree(lamb=1,gamma=0.1)
model.fit(data,-1*target,np.ones_like(target))
plt.scatter(data, target)
plt.plot(data, model.predict(data), color='r')
[<matplotlib.lines.Line2D at 0x1d8eb88cf60>]
model=XGBoostBaseTree(lamb=0.1,gamma=100)
model.fit(data,-1*target,np.ones_like(target))
plt.scatter(data, target)
plt.plot(data, model.predict(data), color='r')
[<matplotlib.lines.Line2D at 0x1d8fc9e3b38>]