笔者这次是温习和回顾集成算法,所以又自己手推实现了一遍Adaboost。之前有两篇关于Adaboost的文章:
- 一篇是初学的时候的手推:机器学习_手写算法:Adaboost简单实现
- 一篇是基于《机器学习实战》一书写的:机器学习_AdaBoost(机器学习实战)
本次的不同之处有两点:
- 温习而非初学
- sklearn接口方式推导
一、集成学习及AdaBoost的优缺点
1.1 关于集成学习
在《多样性团队》一书提及了一个研究领域——感知盲点。每个人都通过某种参照框架来感知和理解世界,但我们看不到自己的参照架构本身。感知盲点的存在说明:我们往往意识不到自己能从其他人身上学到不少东西。这也解释了人口多样性(人种、性别、年龄、阶层等方面的差异)在某些情况下能够增加群体智慧。
对于算法模型而言也是同样的,一个模型所感知的样本间的区别是有限的,所以我们需要增加模型“团队“的多样性,让不同模型对于相同样本给出不同决策并融合(加权平均,stacking),同样的我们也可以多个专家团队从不同方面进行决策再汇总(Bagging),也可以在一个专家的判断基础上再进行多次判断(Boosting)。上面提及的是,多样性在某些情况下能够增加群体智慧,即算法团队的多样性和准确性的结合具有一定的难度,所以我们需要一个较为合理的损失函数来让算法收敛。
本文只要关注的Boosting,是采用的指数损失作为收敛目标。
l
o
s
s
=
e
−
y
t
r
u
e
∗
y
p
r
e
d
loss = e^{-y_{true} * y_{pred}}
loss=e−ytrue∗ypred
[注:]
- 参照西瓜书
1.2 AdaBoost优缺点
优点:泛化错误率低,易于编码,可以应用在大部分分类器上, 无参数调整
缺点:对离群点敏感
二、Adaboost框架手推
2.1 基模型拟合
基于样本权重训练模型
estimator.fit(X, y, sample_weight=sample_weight.flatten())
样本权重作用于基模型
样本的权重如何在模型中起作用,这个其实涉及到损失函数的优化,针对不同情况改变损失函数构成部分的权重。
针对平方误差损失函数,我们增加模型的泛化能力可以增加一个正则项(限制权重不至于过大)
m
s
e
=
∑
i
=
1
n
(
y
i
t
r
u
e
−
w
i
∗
x
i
)
2
+
1
2
∑
j
=
1
m
(
w
i
)
2
mse=\sum_{i=1}^{n}(y_i^{true} - w_i * x_i)^2 + \frac{1}{2}\sum_{j=1}^{m}(w_i)^2
mse=i=1∑n(yitrue−wi∗xi)2+21j=1∑m(wi)2
针对交叉熵,有Focal loss的算法(感兴趣同学可以自己找下论文看下),调整正负样本的损失权重(
α
\alpha
α ),增加难分类样本的损失(
(
1
−
p
)
γ
(1-p)^\gamma
(1−p)γ )。
f
l
=
−
y
α
(
1
−
p
)
γ
l
o
g
(
p
)
−
(
1
−
y
)
(
1
−
α
)
p
γ
l
o
g
(
1
−
p
)
fl = - y \alpha (1-p)^\gamma log(p) - (1-y)(1-\alpha )p^\gamma log(1-p)
fl=−yα(1−p)γlog(p)−(1−y)(1−α)pγlog(1−p)
而我们样本权重其实有点类似于Focal loss中增加难分类样本的损失,我们是直接对不同样本增加权重,而非基于预测结果增加权重。对于交叉熵而言我们的损失可以改为:
l
o
s
s
w
e
i
g
h
t
=
∑
i
=
1
n
w
x
i
(
−
y
i
l
o
g
(
p
i
)
−
(
1
−
y
i
)
l
o
g
(
1
−
p
i
)
)
loss_{weight}= \sum_{i=1}^{n} w_{x_i}(-y_ilog(p_i) - (1-y_i)log(1-p_i))
lossweight=i=1∑nwxi(−yilog(pi)−(1−yi)log(1−pi))
然后基于链式求导,梯度下降收敛权重
# 以logistic为例
def fit_on_batch(self, x, y, sample_weight):
pred = self.predict(x)
loss_sigmoid_devaration = (-y * 1 / (pred + self.epsilon) +、
(1-y)* 1 / (1 - pred + self.epsilon)) * sample_weight / sum(sample_weight)
sigmoid_linear_devaration = pred - pred * pred
w_d = x.T.dot(loss_sigmoid_devaration * sigmoid_linear_devaration)
self.w -= self.learning_rate * w_d
对于决策时的基尼指数(Gini index)而言,权重的更改迭代具有天然优势,对于特征的基尼指数会发生变化,因此对于树的分裂产生了影响。
G
i
n
i
(
D
v
)
=
1
−
∑
k
=
1
m
p
k
2
;
p
k
=
∑
i
n
x
w
e
i
g
h
t
(
x
∈
k
)
Gini(D_v)=1 - \sum_{k=1}^{m}p^2_k; \ \ \ \ \ p_k=\sum_i^{n} x_{weight} (x \in k)
Gini(Dv)=1−k=1∑mpk2; pk=i∑nxweight(x∈k)
G
i
n
i
_
i
n
d
e
x
(
D
t
,
F
e
a
t
u
r
e
a
)
=
∑
v
=
1
m
D
v
w
e
i
g
h
t
_
s
u
m
D
G
i
n
i
(
D
v
)
Gini\_index(D_t, Feature_a)=\sum_{v=1}^{m}\frac{D_v^{weight\_sum}}{D}Gini(D_v)
Gini_index(Dt,Featurea)=v=1∑mDDvweight_sumGini(Dv)
2.2 模型误差计算&基分类器权重
基分类器的错分情况
基分类器权重:
α
=
1
2
l
n
(
1
−
e
r
r
o
r
e
r
r
o
r
)
\alpha=\frac{1}{2}ln(\frac{1-error}{error})
α=21ln(error1−error) 从表达式中可看出是 增大了准确率高的基分类器权重
def iboost_error(self, y_true, iboost_pred, sample_weight):
incorrect = y_true != iboost_pred
# print(f'incorrect.shape: {incorrect.shape}')
return np.average(incorrect.flatten(), weights=sample_weight.flatten())
def iboost_weight(self, error):
return 0.5 * np.log( (1-error) / error)
2.3 样本权重刷新
权重的更新依旧是指数函数推导出来的: D t + 1 = D t ∗ e − α t ∗ y t r u e ∗ y p r e d D_{t+1}=D_{t}*e^{-\alpha_{t}\ *\ y_{true}\ *\ y_{pred}} Dt+1=Dt∗e−αt ∗ ytrue ∗ ypred ,从表达式的含义来看,我们是想增大错误分类的权重,所以在实现的时候可以进行简略, 即不对分类正确的实例进行权重更改,标准化之后分类正确的实例依旧会减少。
D t + 1 = { D t ∗ e α t = > D t ∗ e α t y t r u e = y p r e d D t ∗ e − α t = > D t y t r u e ≠ y p r e d D_{t+1}= \begin{cases} D_{t}*e^{\alpha_{t}} \ \ => D_{t}*e^{\alpha_{t}}& y_{true}=y_{pred} \\ D_{t}*e^{-\alpha_{t}} => D_{t}& y_{true}\not=y_{pred} \end{cases} Dt+1={Dt∗eαt =>Dt∗eαtDt∗e−αt=>Dtytrue=ypredytrue=ypred
def flushed_sample_weight(self, y_true, iboost_pred, sample_weight):
iboost_error = self.iboost_error(y_true, iboost_pred, sample_weight)
if iboost_error == 0:
return sample_weight, 1, 0
iboost_w = self.iboost_weight(iboost_error)
# 保证权重为正,且简略权重更新
sample_weight *= np.exp(iboost_w * (y_true != iboost_pred) * (sample_weight > 0))
return sample_weight / np.sum(sample_weight), iboost_w, iboost_error
2.4 基于上述手推框架的Adaboost实现
详细脚本可以查看笔者的github:优快云/Adaboost.py
from collections import namedtuple
from sklearn.tree import DecisionTreeClassifier
model_info = namedtuple('model_info', 'model weight error')
# 基分类器想x1拟合->计算误差&计算模型权重-> 更新样本权重 -> 基分类器想x2拟合 .....
def _boost(self, estimator, X, y, sample_weight, print_flag=False):
try:
estimator.fit(X, y, sample_weight=sample_weight.flatten(), verbose=200 if print_flag else np.inf)
except:
estimator.fit(X, y, sample_weight=sample_weight.flatten())
y_pred = (estimator.predict(X) > 0.5).reshape((-1, 1)) * 1
# 更新权重, 计算当前模型的误差, 计算当前模型的权重
sample_weight, iboost_w, iboost_error = self.flushed_sample_weight(y, y_pred, sample_weight)
# 模型保存
iboost_model = model_info( model = estimator, weight = iboost_w * self.learning_rate, error = iboost_error)
self.boost_models.append(iboost_model)
return iboost_model, sample_weight
三、基于Logistic的Adaboost 和 基于决策时的Adaboost的比对
一般情况下:
- 基于logistic的Ababoost的训练结果相对于基分类器提升较少。
- 基于决策树的Ababoost的训练结果相对于基分类器提升较大。
- 基于决策树的Ababoost的训练结果优于基于logistic的adaboost。
大家可以自己去尝试各种基模型的adaboost。
[test-0] dtree-f1:0.82449; my-lr f1: 0.83465; lr-f1:0.82305;
my-adaboost-dtree:0.89167; my-adaboost-lr:0.83137; adaboost-sklearn:0.82500
[test-1] dtree-f1:0.74265; my-lr f1: 0.76471; lr-f1:0.77043;
my-adaboost-dtree:0.84252; my-adaboost-lr:0.76471; adaboost-sklearn:0.79518
[test-2] dtree-f1:0.75090; my-lr f1: 0.81911; lr-f1:0.83154;
my-adaboost-dtree:0.88727; my-adaboost-lr:0.82712; adaboost-sklearn:0.82090
[test-3] dtree-f1:0.81618; my-lr f1: 0.89062; lr-f1:0.90909;
my-adaboost-dtree:0.92481; my-adaboost-lr:0.89062; adaboost-sklearn:0.89552
def test(n):
print('--'*25)
print('loading data ...')
X, y = make_classification(n_samples=1000, n_features=13, n_informative=8, n_classes=2)
x_tr, x_te, y_tr, y_te = train_test_split(X, y, test_size=0.25)
y_tr = y_tr.flatten()
y_te = y_te.flatten()
print('--'*25)
print('Compare my logistic ...')
m_lr = BaseLogistic(batch_size=256, epochs=500, epsilon=1e-15, learning_rate=0.1, early_stopping=20)
m_lr.fit(x_tr, y_tr, np.ones(len(y_tr), dtype=np.float64) / len(y_tr), verbose=200)
pred_te = m_lr.predict(x_te)
f1_ = f1_score(y_te, pred_te>0.5)
lr = LogisticRegression()
lr.fit(x_tr, y_tr)
lr_pred = lr.predict(x_te)
f1_lr = f1_score(y_te, lr_pred>0.5)
print(f'my-lr f1: {f1_:.3f}; lr-f1:{f1_lr:.3f}')
print('--'*25)
print('test adaboost ...')
tree_ = DecisionTreeClassifier(max_depth=3)
tree_.fit(x_tr, y_tr)
tree_pred = tree_.predict(x_te)
tree_adb = f1_score(y_te, tree_pred>0.5)
# adaboost-基分类器决策树
adb = SampleAdaboost(50)
adb.fit(x_tr, y_tr)
adb_pred = adb.predict(x_te)
f1_adb = f1_score(y_te, adb_pred>0.5)
# adaboost-基分类器logistic
adb_lr = SampleAdaboost(50, base_model='logistic')
adb_lr.fit(x_tr, y_tr)
adb_lr_pred = adb_lr.predict(x_te)
adb_lr_f1 = f1_score(y_te, adb_lr_pred>0.5)
# adaboost-基分类器决策树-sklearn
adb_sk = AdaBoostClassifier()
adb_sk.fit(x_tr, y_tr)
adb_sk_pred = adb_sk.predict(x_te)
adb_sk_f1 = f1_score(y_te, adb_sk_pred>0.5)
info_out = f'[test-{n}] dtree-f1:{tree_adb:.5f}; my-lr f1: {f1_:.5f}; lr-f1:{f1_lr:.5f}; \n\tmy-adaboost-dtree:{f1_adb:.5f}; my-adaboost-lr:{adb_lr_f1:.5f}; adaboost-sklearn:{adb_sk_f1:.5f}'
print(info_out)
print('Done')
return info_out
欢迎大家指正。