一、基本内容
- 提升树的分类
- | 二分类问题 | 回归问题 |
---|---|---|
模型 | Adaboost的特例,每个弱分类器的高度为2,并且权重为1 | |
损失函数 | 指数损失函数 | 平方误差损失函数 |
优化方式 | 通过经验风险最小化拟合新的弱分类器 | 通过残差拟合新的弱分类器 |
针对不同的问题,不同的损失函数有不同的优化方式,GBDT提出了一般决策优化问题。
- 一般损失函数定义
L ( y , f ( x ) ) L(y,f(x)) L(y,f(x))
- 优化目标:加入新弱分类器的模型损失要低于旧模型:
L ( y , f m ( x ) ) < L ( y , f m − 1 ( x ) ) L(y,f_m(x))< L(y,f_{m-1}(x)) L(y,fm(x))<L(y,fm−1(x))
在点
f
m
−
1
(
x
)
f_{m-1}(x)
fm−1(x)上对
L
(
y
,
f
m
(
x
)
)
L(y,f_m(x))
L(y,fm(x))泰勒展开可得:
L
(
y
,
f
m
(
x
)
)
=
L
(
y
,
f
m
−
1
(
x
)
)
+
∂
L
(
y
,
f
m
−
1
(
x
)
)
∂
f
m
−
1
(
x
)
∣
f
m
−
1
(
x
)
⋅
T
(
X
i
,
θ
m
)
+
α
L(y,f_m(x))=L(y,f_{m-1}(x)) + \frac{\partial L(y,f_{m-1}(x))}{\partial f_{m-1}(x)}|_{f_{m-1}(x)}·T(X_i,\theta_m)+\alpha
L(y,fm(x))=L(y,fm−1(x))+∂fm−1(x)∂L(y,fm−1(x))∣fm−1(x)⋅T(Xi,θm)+α
所以有:
L
(
y
,
f
m
−
1
(
x
)
)
−
L
(
y
,
f
m
(
x
)
)
≈
−
∂
L
(
y
,
f
m
−
1
(
x
)
)
∂
f
m
−
1
(
x
)
∣
f
m
−
1
(
x
)
⋅
T
(
x
i
,
θ
m
)
≥
0
L(y,f_{m-1}(x))-L(y,f_m(x)) \thickapprox -\frac{\partial L(y,f_{m-1}(x))}{\partial f_{m-1}(x)}|_{f_{m-1}(x)}·T(x_i,\theta_m)\geq 0
L(y,fm−1(x))−L(y,fm(x))≈−∂fm−1(x)∂L(y,fm−1(x))∣fm−1(x)⋅T(xi,θm)≥0
当
T
(
x
i
,
θ
m
)
≈
−
∂
L
(
y
,
f
m
−
1
(
x
)
)
∂
f
m
−
1
(
x
)
∣
f
m
−
1
(
x
)
=
y
−
y
p
r
e
d
T(x_i,\theta_m) \thickapprox-\frac{\partial L(y,f_{m-1}(x))}{\partial f_{m-1}(x)}|_{f_{m-1}(x)} = y-y_{pred}
T(xi,θm)≈−∂fm−1(x)∂L(y,fm−1(x))∣fm−1(x)=y−ypred时,有
L
(
y
,
f
m
−
1
(
x
)
)
−
L
(
y
,
f
m
(
x
)
)
≥
0
L(y,f_{m-1}(x))-L(y,f_m(x)) \geq 0
L(y,fm−1(x))−L(y,fm(x))≥0,因此只需将新加入的弱学习器拟合负梯度即可实现梯度优化
[!tip]
与Adaboost模型不同, GBDT是基于梯度优化的,而Adaboost是基于权重优化的,重点训练了错分类的样本,对异常值较为敏感,GBDT通过优化损失函数的负梯度作为近似残差,指导每棵树的生长。梯度提供了方向和幅度信息,能更精确地找到优化路径。
二、基于分类的GBDT
- 基本实现
对于最后的回归输出进行
s
i
g
m
o
i
d
sigmoid
sigmoid函数映射变化:
y
=
1
1
+
e
−
f
m
(
x
)
y = \frac{1}{1+e^{-f_m(x)}}
y=1+e−fm(x)1
- 损失函数:交叉熵损失函数:
L = − y log y − ( 1 − y ) log ( 1 − y ) L = -y \log y-(1-y) \log(1-y) L=−ylogy−(1−y)log(1−y)
- 将变换后的特征代入交叉熵损失函数中,可得:
y = log ( 1 + e − f m ( x ) ) + ( 1 − y ) f m ( x ) y=\log (1+e^{-f_m(x)})+(1-y)f_m(x) y=log(1+e−fm(x))+(1−y)fm(x)
- 损失函数的负梯度为:
r m ( x , y ) = − [ 1 1 + e − f m − 1 ( x ) − y ] = y − y m − 1 r_m(x,y)=-[\frac{1}{1+e^{-f_{m-1}(x)}} - y] = y - y_{m-1} rm(x,y)=−[1+e−fm−1(x)1−y]=y−ym−1
三、代码实现
- 参数初始化:模型是通过残差拟合弱学习器的,第一个弱学习器没有残差值,需要初始化
if self.task == "regression":
self.init_value = np.mean(y) # 对于回归,初始预测值为均值
elif self.task == "classification":
self.init_value = np.log(np.mean(y) / (1 - np.mean(y))) # 对于分类,初始预测值为对数几率
y_pred = np.full(y.shape, self.init_value)
- 弱学习器梯度更新:新弱分类器拟合损失函数的负梯度
for i in range(self.n_estimators):
if self.task == "regression":
# 计算残差(负梯度)
residual = y - y_pred
elif self.task == "classification":
# 计算负梯度(即目标值的梯度)
prob = 1 / (1 + np.exp(-y_pred)) # Sigmoid
residual = y - prob
# 拟合残差(负梯度)
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residual)
self.trees.append(tree)
# 更新预测值,因为是拟合残差,只需在旧集成模型中加入残差值
y_pred += self.learning_rate * tree.predict(X)
- 输出预测:通过多个弱分类器的预测结果相加
def predict(self, X):
y_pred = np.full((X.shape[0],), self.init_value) # 需要用初始值去计算结果
for tree in self.trees:
y_pred += self.learning_rate * tree.predict(X)
if self.task == "classification":
# 对分类任务,返回概率值
return 1 / (1 + np.exp(-y_pred))
return y_pred
- 完整代码
class GBDT:
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3, task="regression"):
assert task in ["regression", "classification"], "Task must be 'regression' or 'classification'."
self.n_estimators = n_estimators
self.learning_rate = learning_rate
self.max_depth = max_depth
self.task = task
self.trees = [] # 用于存储每一棵弱学习器
self.init_value = None # 初始预测值
def fit(self, X, y):
# 初始化预测值
if self.task == "regression":
self.init_value = np.mean(y) # 对于回归,初始预测值为均值
elif self.task == "classification":
self.init_value = np.log(np.mean(y) / (1 - np.mean(y))) # 对于分类,初始预测值为对数几率
y_pred = np.full(y.shape, self.init_value)
for i in range(self.n_estimators):
if self.task == "regression":
# 计算残差(负梯度)
residual = y - y_pred
elif self.task == "classification":
# 计算负梯度(即目标值的梯度)
prob = 1 / (1 + np.exp(-y_pred)) # Sigmoid
residual = y - prob
# 拟合残差(负梯度)
tree = DecisionTreeRegressor(max_depth=self.max_depth)
tree.fit(X, residual)
self.trees.append(tree)
# 更新预测值
y_pred += self.learning_rate * tree.predict(X)
def predict(self, X):
y_pred = np.full((X.shape[0],), self.init_value)
for tree in self.trees:
y_pred += self.learning_rate * tree.predict(X)
if self.task == "classification":
# 对分类任务,返回概率值
return 1 / (1 + np.exp(-y_pred))
return y_pred
def predict_class(self, X, threshold=0.5):
if self.task != "classification":
raise ValueError("This method is only available for classification tasks.")
prob = self.predict(X)
return (prob >= threshold).astype(int)