【学习笔记】集成学习(六):Boosting
Datawhale组队学习第27期:集成学习
本次学习的指导老师萌弟的教学视频
本贴为学习记录帖,有任何问题欢迎随时交流~
部分内容可能还不完整,后期随着知识积累逐步完善。
开始时间:2021年7月23日
最新更新:2021年7月28日(Task6 Boosting)
一、知识补充:PAC学习
1. 概念
-
PAC:可能近似正确,即:Probably Approximate Correct
-
能否从假设空间 H \mathcal{H} H中学习一个好的假设 h h h。(只关心能不能找到)
-
两个条件(PAC辨识条件):
- 近似正确:足够小的泛化误差
- 可能正确:给定一个 δ \delta δ,存在一个假设 h h h满足 P ( h 近 似 正 确 ) ≥ 1 − δ P(h近似正确)\ge 1 - \delta P(h近似正确)≥1−δ
- 满足以上两个条件,称为PAC可学习。
-
模型在短时间利用多项式级别(少量)的样本找到一个假设 h ′ h' h′满足:
P ( E ( h ′ ) ≤ ϵ ) ≥ 1 − δ , 其 中 0 < ϵ 且 δ < 1 P(E(h') \le \epsilon) \ge 1 - \delta,其中0<\epsilon且\delta < 1 P(E(h′)≤ϵ)≥1−δ,其中0<ϵ且δ<1 -
特性:
- 不依赖于分布
- 样本来自于同一分布
2. Hoeffding不等式
-
一群独立的随机变量 X 1 , . . . X n ∈ R X_1,...X_n \in \R X1,...Xn∈R,随机变量有限个且 a i < X i < b i a_i < X_i < b_i ai<Xi<bi,记 S n = ∑ i = 1 n X i S_n = \sum\limits_{i=1}^{n}X_i Sn=i=1∑nXi,则有:
P ( ∣ S n − E ( S n ) ∣ ≥ t ) ≤ 2 ⋅ e x p { − 2 t 2 ∑ i = 1 n ( b i − a i ) 2 } P(|S_n - E(S_n)|\ge t) \le 2 \cdot exp\{- \frac{2t^2} {\sum\limits_{i=1}^{n}(b_i - a_i)^2} \} P(∣Sn−E(Sn)∣≥t)≤2⋅exp{ −i=1∑n(bi−ai)22t2} -
机器学习中,对于随机变量 x i x_i xi,有 x ˉ = 1 n ∑ i = 1 n x i \bar x = \frac{1}{n}\sum\limits_{i=1}^{n}x_i xˉ=n1i=1∑nxi,即 n x ˉ = ∑ i = 1 n x i n \bar x = \sum\limits_{i=1}^{n}x_i nxˉ=i=1∑nxi,则有:
P ( ( x ˉ − E ( x ˉ ) ) ≥ t ) ≤ e x p { − 2 n t 2 } P((\bar x - E(\bar x))\ge t) \le exp\{-2nt^2\} P((xˉ−E(xˉ))≥t)≤exp{ −2nt2}P ( ∣ v − μ ∣ > ϵ ) < 2 ⋅ e x p { − 2 ϵ 2 N } P(|v - \mu|>\epsilon) < 2\cdot exp\{-2\epsilon^2N\} P(∣v−μ∣>ϵ)<2⋅exp{ −2ϵ2N}
其中, v v v是采样计算的经验误差, μ \mu μ是期望的真实误差, N N N是采样数量。
-
由泛化误差 E ( h ) E(h) E(h)和经验误差 E ^ ( h ) \hat E(h) E^(h)可知, E ( E ^ ( h ) ) = E ( h ) E(\hat E(h)) = E(h) E(E^(h))=E(h),因此有:
∀ h ∈ H P ( ∣ E ( h ) − E ^ ( h ) ∣ ≥ ϵ ) ≤ 2 ⋅ e x p { − 2 n ϵ 2 } \forall h \in \mathcal{H} \ \ \ P(|E(h) - \hat E(h)|\ge \epsilon) \le2 \cdot exp\{-2n\epsilon^2 \} ∀h∈H P(∣E(h)−E^(h)∣≥ϵ)≤2⋅exp{ −2nϵ2}- 进一步,可以得到:
P ( ∀ h ∈ H : ∣ E ( h ) − E ^ ( h ) ∣ ≤ ϵ ) = 1 − P ( ∃ h ∈ H : ∣ E ( h ) − E ^ ( h ) ∣ ≥ ϵ ) = 1 − p ( ( ∣ E ( h 1 ) − E ^ ( h 1 ) ∣ ≥ ϵ ) ∨ . . . ∨ ( ∣ E ( h ∣ H ∣ ) − E ^ ( h ∣ H ∣ ∣ ) ≥ ϵ ) ) ≥ 1 − ∑ i = 1 ∣ H ∣ P ( ∣ E ( h i ) − E ^ ( h i ) ∣ ≥ ϵ ) ≥ 1 − 2 ∣ H ∣ e x p { − 2 n ϵ 2 } \begin{aligned} &\ \ \ \ \ P(\forall h \in \mathcal{H}: |E(h) - \hat E(h)|\le \epsilon) \\ &= 1 - P(\exist h \in \mathcal{H}: |E(h) - \hat E(h)| \ge \epsilon)\\ &=1 - p((|E(h_1) - \hat E(h_1)| \ge \epsilon) \vee... \vee(|E(h_{|\mathcal{H}|}) - \hat E(h_{|\mathcal{H}|}|) \ge \epsilon)) \\ &\ge 1 - \sum\limits_{i=1}^{|\mathcal{H}|} P(|E(h_i) - \hat E(h_i)|\ge\epsilon) \\ &\ge 1 - 2|\mathcal{H}|exp\{-2n\epsilon^2 \} \end{aligned} P(∀h∈H:∣E(h)−E^(h)∣≤ϵ)=1−P(∃h∈H:∣E(h)−E^(h)∣≥ϵ)=1−p((∣E(h1)−E^(h1)∣≥ϵ)∨...∨(∣E(h∣H∣)−E^(h∣H∣∣)≥ϵ))≥1−i=1∑∣H∣P(∣E(hi)−E^(hi)∣≥ϵ)≥1−2∣H∣exp{ −2nϵ2}
-
从上述不等式可以看出, ∣ E ( h ) − E ^ ( h ) ∣ |E(h)-\hat E(h)| ∣E(h)−E^(h)∣小于给定的误差发生的可能性的下界与假设空间 H \mathcal{H} H、样本量 n n n有关。
-
当样本量足够大或假设空间足够小时,可以保证学习到的假设 h ′ h' h′的泛化误差与经验误差足够接近。
-
当样本量小且假设空间很大(模型越复杂)时,训练的表现 E ( h ) E(h) E(h)与真实样本的表现 E ^ ( h ) \hat E(h) E^(h)相差较大,即过拟合。
-
3. PAC可学习条件
P ( E ( h ′ ) ≤ ϵ ) ≥ 1 − δ , 其 中 0 < ϵ 且 δ < 1 P(E(h') \le \epsilon) \ge 1 - \delta,其中0<\epsilon且\delta < 1 P(E(h′)≤ϵ)≥1−δ,其中0<ϵ且δ<1
- 样本量满足条件: n > M = ln 2 ∣ H ∣ δ 2 ϵ 2 n > M = \frac{\ln\frac{2|\mathcal{H}|}{\delta}}{2\epsilon^2} n>M=2ϵ2lnδ2∣H∣,其中误差为:
δ = 2 ∣ H ∣ e x p { − 2 n ϵ 2 } \begin{aligned} \delta &= 2|\mathcal{H}|exp\{-2n\epsilon^2 \} \\ \end{aligned} δ=2∣H∣exp{ −2nϵ2}
二、Boosting概念
1. 思想
- 对同一组数据集反复学习
- 通过减少偏差的形式来降低测试误差(与Bagging的本质区别)
2. 常见模型
- AdaBoosting
- GradientBoosting
- Xgboost
- LightGBM
- Catboost
3. 理论来源
- 强可学习与弱可学习
- 弱学习:识别错误率小于1/2,准确率略高于随机猜测
- 强学习:识别准确率很高,并能在多项式时间内完成的学习算法
- PAC学习框架下,强可学习和弱可学习是等价的。
- 意味着如果发现了弱可学习算法,可以通过提升方法使得学习增强
- 大多数boosting都是改变训练数据集的概率分布(训练数据不同样本的权值),针对不同概率分布的数据调用弱分类算法学习一系列的弱分类器。
- 需要回答两个关键问题:
- 每一轮学习如何改变数据的概率分布
- 各个弱分类器如何组合
4. 精度分析
推导
-
设定基模型为 f f f,通过给定样本 X X X,每轮训练一个模型,再根据错误率进行bootstrap抽样(非均匀),训练出m个模型。
-
最终模型是m个子模型的线性组合,记为 F = ∑ i = 1 m r i f i F = \sum\limits_{i=1}^{m}r_if_i F=i=1∑mrifi,其中 r i r_i ri为第 i i i个模型的权重系数
-
f f f训练的样本来自总体 X X X,可以记 E ( f i ) = μ E(f_i)=\mu E(fi)=μ和 V a r ( f i ) = σ 2 Var(f_i) = \sigma^2 Var(fi)=σ2
E ( F ) = E ( ∑ i = 1 m r i f i ) = ∑ i = 1 m r i E ( f i ) V a r ( F ) = V a r ( ∑ i = 1 m r i f i ) = ∑ i = 1 m V a r ( r i f i ) + ∑ i ≠ j m c o v ( r i f i , r j f j ) = ∑ i = 1 m r i 2 V a r ( f i ) + ∑ i ≠ j m ρ i j r i r j V a r ( f i ) V a r ( f j ) \begin{aligned} E(F) &= E(\sum\limits_{i=1}^{m}r_if_i) \\ &= \sum\limits_{i=1}^{m}r_iE(f_i) \\ Var(F) &= Var(\sum\limits_{i=1}^{m}r_if_i) \\ &= \sum\limits_{i=1}^{m}Var(r_if_i) + \sum\limits_{i \ne j}^{m}cov(r_if_i, r_jf_j) \\ &= \sum\limits_{i=1}^{m}r_i^2Var(f_i) + \sum\limits_{i \ne j}^{m}\rho_{ij}r_ir_j\sqrt{Var(f_i)}\sqrt{Var(f_j)} \\ \end{aligned} E(F)Var(F)=E(i=1∑mrifi)=i=1∑mriE(fi)=Var(i=1∑mrifi)=i=1∑mVar(rifi)+i=j∑mcov(rifi,rjfj)=i=1∑mri2Var(fi)+i=j∑mρijrirjVar(fi)Var(fj) -
由bootstrap采样可知每个子模型仍是近似同方差,但期望会因为样本选取的权重发生变化(bagging是均匀采样,因此子模型的均值和方差是近似的)。因此有:
E ( F ) = ∑ i = 1 m r i E ( f i ) V a r ( F ) = ∑ i = 1 m r i 2 V a r ( f i ) + ∑ i ≠ j m ρ i j r i r j V a r ( f i ) V a r ( f j ) = σ 2 ∑ i = 1 m r i 2 + σ 2 ∑ i ≠ j m ρ i j r i r j ≈ σ 2 ∑ i = 1 m r i 2 + σ 2 ∑ i ≠ j m r i r j = σ 2 ∑ i = 1 m ∑ j = 1 m r i r j \begin{aligned} E(F) &= \sum\limits_{i=1}^{m}r_iE(f_i) \\ Var(F) &= \sum\limits_{i=1}^{m}r_i^2Var(f_i) + \sum\limits_{i \ne j}^{m}\rho_{ij}r_ir_j\sqrt{Var(f_i)}\sqrt{Var(f_j)} \\ &= \sigma^2 \sum\limits_{i=1}^{m}r_i^2 + \sigma^2\sum\limits_{i \ne j}^{m}\rho_{ij}r_ir_j \\ &\approx \sigma^2 \sum\limits_{i=1}^{m}r_i^2 + \sigma^2\sum\limits_{i \ne j}^{m}r_ir_j \\ &= \sigma^2 \sum\limits_{i=1}^{m}\sum\limits_{j=1}^{m}r_ir_j \end{aligned} E(F)Var(F)=i=1∑mriE(fi)=i=1∑mri2Var(fi)+i=j∑mρijrirjVar(fi)Var(fj)=σ2i=1∑mri2+σ2i=j∑mρijrirj≈σ2i=1∑mri2+σ2i=j∑mrirj=σ2i=1∑mj=1∑mrirj
基于偏差与方差理论分析
- Boosting模型是在基模型训练完后是使用线性组合来融合模型,显然每轮抽取的bootstrap样本是跟前面的样本有联系(按错误率修改了抽样权值),样本与样本之间的相关关系 ρ i j \rho_{ij} ρij会接近于1,同时各个子模型的权值之和 ∑ i = 1 m r i ≈ 1 \sum\limits_{i=1}^{m}r_i \approx 1 i=1∑mri≈1。
[ E ( F ) E ( f ) ] 2 = [ ∑ i = 1 m r i E ( f i ) μ ] 2 [ E ( F ) − E ( f ) ] 2 = [ μ − ∑ i = 1 m r i E ( f i ) ] 2 V a r ( F ) V a r ( f ) = ∑ i = 1 m ∑ j = 1 m r i r j ≈ 1 V a r ( F ) − V a r ( f ) = ( 1 − ∑ i = 1 m ∑ j = 1 m r i r j ) σ 2 ≈ 0 \begin{aligned} {[\frac{E(F)}{E(f)}]^2} &= [\sum\limits_{i=1}^{m}r_i \frac{E(f_i)}{\mu}]^2 \\ [E(F) - E(f)]^2 &= [\mu - \sum\limits_{i=1}^{m}r_iE(f_i)]^2 \\ \frac{Var(F)}{Var(f)} &= \sum\limits_{i=1}^{m}\sum\limits_{j=1}^{m}r_ir_j \approx 1 \\ Var(F)-Var(f) &= (1 - \sum\limits_{i=1}^{m}\sum\limits_{j=1}^{m}r_ir_j)\sigma^2 \approx 0 \\ \end{aligned} [E(f)E(F)]2[E(F)−E(f)]2Var(f)Var(F)Var(F)−Var(f)=[i=1∑mriμE(fi)]2=[μ−i=1∑mriE(fi)]2=i=1∑mj=1∑mrirj≈1=(1−i=1∑mj=1∑mrirj)σ2≈0
- 从推导式子可以发现,组合模型与基模型的 V a r Var Var是十分接近的,主要发生变动的是模型的 B i a s Bias Bias方面。从Boosting模型生成步骤来看,每个子模型的都是由上一次的子模型迭代所得,类似于梯度下降法不断迭代到极小值,从而减小 B i a s Bias Bias。
基模型的选择
- 从上述推导可以发现,Boosting模型主要改变的是整体模型的 B i a s Bias Bias,而组合模型的 V a r Var Var大致与基模型的 V a r Var Var相当,因此Boosting对基模型选择的是弱模型(高 B i a s Bias Bias低 V a r Var Var模型),通过提高分类错误的样本被选中的概率(分布),不断迭代训练子模型来降低 B i a s Bias Bias来提高模型的精度。
三、Adaboost
1. 基本思想
- 提高前一轮分类器错误分类样本的权重,降低正确分类样本的权重
- 各个弱分类器采用加权表决的方式组合
- Adaboost实际上是增加了模型的复杂度,减少了偏差
2. 基本步骤
-
训练器的权重初始化
-
训练多个分类器模型,计算训练集的分类误差率(分类错误加起来)
-
计算分类器在总模型的重要性
-
更新训练数据集的权重分布
- 注意: Z m Z_m Zm是规范化因子,使得 D m + 1 D_{m+1} Dm+1称为概率分布
-
构建基本分类器的线性组合,得到最终分类器
3. 代码实现
- 选用数据集
diabetes
,二分类问题 - 定义弱分类器,单层决策树
# 定义单层决策树算法(弱分类器)
class DecisionStump:
def __init__(self, x, y):
self.x = np.array(x)
self.y = np.array(y)
self.n = self.x.shape[0] # 样本量n
self.w = None # 权重初始化
self.threshold_value = 0 # 阈值初始化
self.threshold_pos = 0 # 参数下标初始化
self.threshold_res = 0 # 取值初始化
def train(self, w, steps=1000):
""" 用于返回所有参数中阈值最小的那一个 w 是长度为n的向量,表示n个样本的权值 threshold_value 为阈值 threshold_pos 为第几个参数(参数下标) threshold_tag 取值空间为{-1, +1},大于阈值则分为某个threshold_tag,小于则相反(这里的如果相等?) """
min_ = float("inf") # 最小值初始化为无穷大inf
# 注意区分与__init__函数self的属性区别
threshold_value = 0
threshold_pos = 0
threshold_tag = 0
self.w = np.array(w) # 更新权重值,并将实际w转成ndarray类型
# 分别以m个属性中的任意一个建立单层决策树
for i in range(self.n):
# value表示阈值,errcnt 表示错误的数量
# 找到当前参数下最小的阈值(当前属性下决策树错误率最低)
value, errcnt = self.findmin(i, 1, steps) # 找最小值
if errcnt < min_:
min_ = errcnt
threshold_value = value
threshold_pos = i
threshold_tag = 1
# 对self属性值进行更新
self.threshold_value = threshold_value
self.threshold_pos = threshold_pos
self.threshold_res = threshold_tag
print(self.threshold_value, self.threshold_pos, self.threshold_res)
return min_
# 找出第i个参数的最小阈值,tag为1或-1
def findmin(self, i, tag, steps):
t = 0
# 测试当前阈值情况下的预测能力(这里并没有作用,可以视为创建空间或初始化)
tmp = self.predintrain(self.x, i, t, tag).transpose()
errcnt = np.sum((tmp != self.y) * self.w) # 计算当前阈值下错误的个数
# 定义上下界(表示从第i个变量中最小值和最大值直接寻找阈值)
bottom = np.min(self.x[i, :]) # 该属性下的最小值,即下界
up = np.max(self.x[i, :]) # 该属性下的最大值,即上界
# 初始化
minerr = float("inf") # 初始化错误的数量minerr为无穷大
value = 0 # 初始化阈值为0
st = (up - bottom) / steps # 表示间隔
# 寻找最小阈值value和分类误差率minerr
for t in np.arange(bottom, up, st):
tmp = self.predintrain(self.x, i, t, tag).transpose() # 转置成(1,测试集样本量)
errcnt = np.sum((tmp != self.y) * self.w) # 返回分类误差率e_m
if errcnt < minerr:
minerr = errcnt
value = t
return value, minerr
# 返回训练时按照阈值为t时预测的结果
def predintrain(self, test_set, i, t, tag):
# 测试集是(变量数, 样本量)的结构
# 将测试集转成矩阵行数与样本量一致的数组
test_set = np.array(test_set).reshape(self.n, -1)
# 生成矩阵行数与测试集列数一致的且值为1的数组(这里测试集列数是测试集样本量)
pre_y = np.ones((np.array(test_set).shape[1], 1))
# 表示选择第i行(第i个变量)的数据,如果小于阈值t,则修改对应值为-1
# 将对每个预测值进行判断,小于阈值的取-1,返回pre_y是(测试集样本量, 1)的结构
pre_y[test_set[i, :] * tag < t * tag] = -1
return pre_y
# 弱分类器进行预测
def pred(self, test_x):
# 测试集转成行数与样本量一致的二维数组结构
test_x = np.array(test_x).reshape(self.n, -1)
pre_y = np.ones((np.array(test_x).shape[1], 1)) # 生成行数与样本量一致且值为1的二维数组
pre_y[test_x[self.threshold_pos, :] * self.threshold_res <
self.threshold_value * self.threshold_res] = -1
return pre_y
- 定义Adaboost
class AdaBoost:
def __init__(self, x, y, weaker=DecisionStump):
# 本函数用于初始化变量及参数
# 初始化数据集(需要将训练集转成ndarray类型,这里本身已经是)
self.x = np.array(x)
self.y = np.array(y).flatten("F") # flatten降至一维,这里没有用,F表示按列展开,原代码的有问题
# 初始化分类器
self.weaker = weaker
# 初始化得分,计算组合分类器的预测得分总和
self.sums = np.zeros(self.y.shape) # 初始化与y.shape一样维度的零矩阵
# 初始化权值w
# 这里np.ones是根据样本量生成一个结构为(n,1)、值为1的二维数组,并降维到(n,)的一维数组
# 生成的值皆为1的一维数组后,对每一个值除以样本量n(利用numpy的广播机制)
# 最后得到的w的每个值都为1/n,即权重初始化是均匀分布的
# self.w = np.ones((self.x.shape[1], 1)).flatten("F") / self.x.shape[1]
self.w = np.ones(self.x.shape[1]) / self.x.shape[1] # 效果同上,这里是简化代码
# 初始化弱分类器
self.q = 0 # 初始化弱分类器的实际个数
self.g = {
} # 初始化所弱分类器的字典(原代码train函数中的放到此处)
self.alpha = {
} # 初始化每个弱分类器的参数
def train(self, m=5):
# 给每个弱分类器的字典赋予默认key和默认value
# g = {i: None, ...} , alpha = {i: None, ...}
for i in range(m):
self.g.setdefault(i)
self.alpha.setdefault(i)
for i in range(m):
self.g[i] = self.weaker(self.x, self.y)
# 根据当前权值进行第i个弱分类器训练,g[i]是weaker,因此train是weaker的方法
# 默认权重是均匀分布的,即权值都是1/n
e = self.g[i].train(self.w)
# 计算第i个分类器的系数(权重)
self.alpha[i] = 1.0 / 2 * 2 * np.log((1 - e) / e)
res = self.g[i].pred(self.x) # res返回的是第i个分类器的输出(重点判断误分类)
# 计算当前次数训练的精确度
print("weak classfier acc", accuracy_score(self.y, res))
print("======================================================")
# 规范化因子
z = self.w * np.exp(-self.alpha[i] * self.y * res.transpose()) # 不一样
self.w = (z / z.sum()).flatten("F") # 更新权值
self.q = i
# errorcnt返回分错的点数,当为0时表示没有分错
if self.errorcnt(i) == 0:
print("%d个弱分类器可以将错误率降到0" % (i + 1))
break
# 返回误分类的点
def errorcnt(self, t):
# 计算组合分类器的得分总和
self.sums = self.sums + self.g[t].pred(self.x).flatten('F') * self.alpha[t]
pre_y = np.zeros(np.array(self.sums).shape)
pre_y[self.sums >= 0] = 1
pre_y[self.sums < 0] = -1
t = (pre_y != self.y).sum()
return t
# 模型预测(最终的分类器)
def pred(self, test_x):
test_x = np.array(test_x)
sums = np.zeros(test_x.shape[1])
for i in range(self.q + 1):
sums = sums + self.g[i].pred(test_x).flatten("F") * self.alpha[i]
pre_y = np.zeros(np.array(sums).shape)
pre_y[sums >= 0] = 1
pre_y[sums < 0] = -1
return pre_y
- 主程序调用
# 加载训练集和测试集
data_set = pd.read_csv('diabetes.csv')
x = data_set.values[:, :8]
y = data_set.values[:, 8]
# 划分数据集
# test_size测试集样本比例; random_state随机种子
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2,
random_state=0)
# 数组变换,方便矩阵运算;简化算法,修改y的取值空间为{-1,+1}
x_train = x_train.transpose()
y_train[y_train == 1] = 1
y_train[y_train == 0] = -1
x_test = x_test.transpose()
y_test[y_test == 1] = 1
y_test[y_test == 0] = -1
# 模型训练
print(x_train.shape, x_test.shape)
ada = AdaBoost(x_train, y_train) # 放进训练集的x和y,默认为弱分类器weak
ada.train(10) # 设置弱分类器的数量为10
# 模型预测
y_pre = ada.pred(x_test)
print("total test", len(y_pre))
print('true pred', len(y_pre[y_pre == y_test]))
print('acc', accuracy_score(y_test, y_pre))