集成学习原理小结(AdaBoost & lightGBM demo)
本博客中使用到的完整代码请移步至: 我的github:https://github.com/qingyujean/Magic-NLPer,求赞求星求鼓励~~~
集成学习系列文章:
集成学习原理小结(AdaBoost & lightGBM demo)
梯度提升树(GBDT)原理小结
XGBoost使用
随机森林(Random Forest)原理小结
1. 集成学习概述
1.1 集成学习是什么?
Ensemble Learnig 是一组individual learner的组合,集成学习通过将多个学习器进行结合,常能够获得比单一学习器显著优越的泛化性能,尤其是对“弱学习器”更为明显。
- 同质集成
集成中只包含同种类型(同种类学习算法)的个体学习器,个体学习器又称为基学习器(base learner)。 - 异质集成
集成包含不同类型(不同类的学习算法)的个体学习器,个体学习器又称为组件学习器(component learner)。
目前来说,同质
个体学习器的应用是最广泛的,一般我们常说的集成学习的方法都是指的同质
个体学习器。而同质个体学习器使用最多的模型是CART决策树
和神经网络
。
要获得好的集成,个体学习器应“好而不同”:
- 个体学习器应有一定的
“准确性”
,即学习器不能太坏 - 并且要有
“多样性”
,即学习器之间具有差异
1.2. 为什么要进行集成学习?
多个学习器的结合可能从3个方面带来好处:
- 统计
从统计上来看,结合多个学习器能减小由于单学习器的误选而导致的泛化性能不佳的风险。 - 计算
从计算上来看,结合多个学习器能降低陷入局部糟糕极小点的风险。 - 表示
从表示上来看,结合多个学习器使得相应的假设空间有所扩大,从而可能学得对真实假设更好的近似。
2. 常见的集成学习算法
根据个体学习的生成方式,或者说是按照个体学习器之间是否存在依赖关系,目前集成学习方法大致分为2类:
- 个体学习器之间存在强依赖关系,必须串行生成的序列化方法,代表:Boosting
- 个体学习器之间不存在强依赖关系,可同时生成的并行化方法,代表:Bagging和RF
2.1 Boosting
Boosting是一族可将弱学习器提升为强学习器的算法,其著名代表是AdaBoost
。Boosting算法要求基学习器能对特定的数据分布进行学习,这可通过”重赋权法“
(re-weighting)实施;对于无法接受带权样本的基学习算法,则可通过”重采样法“
(re-sampling)来处理。例如在分类问题中,Boosting方法通过改变训练样本的权重,学习各个分类器,并将这些分类器进行线性组合,提高分类的性能。
Boosting方法的思想:从弱学习算法出发,通过改变训练数据的概率分布(训练数据的权值分布),针对不同的训练数据分布 调用弱学习算法得到一系列弱学习分类器,然后组合这些弱分类器以构成一个强分类器。
【注意】Boosting算法在每一轮都要检查当前生成的基学习器是否满足基本条件:即检查当前基分类器是否比随机猜测好,一旦条件不满足,则当前基学习器应当被抛弃,且学习过程停止。此时初始设置的学习轮数T可能还远远未达到,而使得最终集成中只包含很少的基学习器而导致性能不佳。若采用“重采样法”,则可避免训练过程过早停止。
从偏差-方差
分解角度来看,Boosting主要关注降低偏差
。Boosting能基于泛化性能相当弱的学习器构建出很强的集成。
2.1.1 AdaBoost算法
适用问题:二类分类
模型特点:弱分类器的线性组合
模型类型:判别模型
损失函数:指数损失
学习策略:极小化加法模型的指数损失
学习算法:前向分步加法算法
AdaBoost算法:
- 每一轮迭代中如何改变训练数据的权值或概率分布?
提高那些被前一轮弱分类器错误分类的样本的权值,而降低那些被正确分类的样本的权重
- 如何将弱分类器组合成一个强分类器?
加权多数表决
算法描述
算法说明
算法训练误差分析
Adaboost最基本的性质是它能在学习过程中不断减少训练误差
(即在训练数据集上的分类误差率)。
- Adaboost的训练误差是以指数速率下降的
- AdaBoost具有适应性,能适应弱分类器各自的训练误差率。(Ada:Adaptive)
学习算法—前向分步算法
AdaBoost算法的另一解释:AdaBoost算法是模型为加法模型,损失函数为指数函数,学习算法为前向分步算法的二类分类学习方法。
考虑加法模型:
f ( x ) = ∑ m = 1 M β m b ( x ; γ m ) f(x)=\sum_{m=1}^{M}\beta_mb(x;\gamma_m) f(x)=m=1∑Mβmb(x;γm)
其中, b ( x ; γ m ) b(x;\gamma_m) b(x;γm)为基函数, γ m \gamma_m γm为基函数的参数, β m \beta_m βm为基函数的系数。
在给定训练数据以及损失函数 L ( y , f ( x ) ) L(y,f(x)) L(y,f(x))的条件下,加法模型 f ( x ) f(x) f(x)的学习目标就是其经验风险极小化,即损失函数极小化问题:
min β m , γ m ∑ i = 1 N L ( y i , ∑ m = 1 M β m b ( x i ; γ m ) ) \min\limits_{\beta_m,\gamma_m} \sum\limits_{i=1}^N L\big(y_i, \sum\limits_{m=1}^M \beta_m b(x_i;\gamma_m) \big) βm,γmmini=1∑NL(yi,m=1∑Mβmb(xi;γm))
这通常是一个复杂的优化问题。
前向分步算法解决这一优化问题的想法是:因为学习的是加法模型,所以可以分步学习,每一步仅学习一个基函数 b ( x ; γ m ) b(x;\gamma_m) b(x;γm)及其系数 β m \beta_m βm,逐步的逼近上述优化目标,如此便可简化优化难度和复杂度。
AdaBoost算法是前向分步加法算法的特例,模型是由基本分类器组成的加法模型
,损失函数是指数函数
:
L
(
y
,
f
(
x
)
)
=
exp
[
−
y
f
(
x
)
]
L(y,f(x))=\exp [-yf(x)]
L(y,f(x))=exp[−yf(x)]。
指数损失
函数是分类任务原本的损失函数—0-1损失
的一致的替代损失函数。由于这个替代函数具有更好的数学性质,例如它是连续可微函数,因此使用它来替代0-1损失作为优化目标。
算法评价
Adaboost的主要优点有:
- 1)Adaboost作为分类器时,分类精度很高
- 2)在Adaboost的框架下,可以使用各种回归分类模型来构建弱学习器,非常灵活。
- 3)作为简单的二元分类器时,构造简单,结果可理解。
- 4)不容易发生过拟合
Adaboost的主要缺点有:
- 1)对异常样本敏感,异常样本在迭代中可能会获得较高的权重,影响最终的强学习器的预测准确性。
2.1.2 梯度提升树
可参阅如下内容:
2.2 Bagging
bagging是并行式
集成学习方法的著名代表。
bagging希望集成中的个体学习器尽可能相互独立,而这在现实任务中几乎无法做到,但可以设法使基学习器具有较大差异,或者说使得基学习器更具有“多样性”。
bagging基学习器的多样性通过“样本扰动”
达到,而这是通过”自主采样法“
(bootstrap sampling)实现。
自主采样 bootstrap sampling:
给定包含m个样本的数据集D,每次随机、有放回的挑选一个样本,执行m次,最后得到一个包含m个样本的数据集D’。
一个样本在m次采样中始终不被抽取到的概率是 ( 1 − 1 m ) m (1-\frac{1}{m})^m (1−m1)m,而 lim m → ∞ ( 1 − 1 m ) m → 1 e ≈ 0.368 \lim\limits_{m\rightarrow\infty}(1-\frac{1}{m})^m \rightarrow\frac{1}{e}\approx0.368 m→∞lim(1−m1)m→e1≈0.368,即初始训练集中有63.2%的样本会出现在采样集中。
从偏差-方差
分解角度看,bagging主要关注降低方差
,因此它在不剪枝决策树、神经网络等易受样本扰动的学习器上效果更为明显。
随机森林
随机森林(Random Forest)是Bagging的一个扩展变体,其除了有bagging方法的样本扰动
外,还有自己特有的属性扰动
,这使得最终集成的泛化性能可通过个体学习器的差异度的增加而进一步提升。
RF简单、容易实现、计算开销小,在很多任务中都展现出强大的性能,被誉为“代表集成学习技术水平的方法”
。值得一提的是,RF的训练效率常优于bagging,因为在个体决策树的构建过程中,bagging使用的是“确定性”决策树,在选择划分属性时要对结点的所有属性进行考察,而RF使用的“随机型”决策树,即只需考察一个属性子集。
随机森林更详尽的说明,可参见:随机森林(Random Forest)原理小结。
2.3 Boosting vs Bagging
方法 | leaner 弱依赖方法 代表:Bagging | leaner 强依赖方法 代表:Boosting |
---|---|---|
集成方式 | 并行 | 串行 |
偏差-方差分析 | 主要关注降低方差 因此它在不剪枝决策树、神经网络等易受样本扰动的学习器上效果更为明显 | 主要关注降低偏差 因此Boosting能基于泛化性能相当弱的学习器构建出很强的集成 |
适用范围 | 高噪声 | 低噪声 |
样例 | Random Forest | AdaBoost、GBDT |
简单的理解:
- 单个模型太强时,容易过拟合,所以Bagging就是让模型不要那么容易过拟合,降低方差
- 当个模型太弱时,容易欠拟合,所以Boosting就是让模型一步一步去逐渐学好,降低偏差,但是也可能会引起过拟合。
3. 常见的结合策略
3.1 平均法
主要针对数值型的输出(例如回归任务)。
- 简单平均法
- 加权平均法
【注意】实验和应用均显示出:加权平均法未必优于简单平均法
,加权平均法可能因为训练数据不充分或存在噪声,较容易过拟合。一般而言,当个体学习器性能相差较大时,宜使用加权平均法,而在个体学习器性能相近时,简单平均法更适宜。
3.2 投票法
主要针对分类任务,模型的输出可以是类标签(也叫“硬投票”
,hard voting),也可以是类概率(也叫“软投票”
,soft voting)。
- 绝对多数投票法(标记得票过半数,则预测为该标记,否则拒绝预测)
- 相对多数投票法(得票最多)
- 加权投票法
【注意】虽然分类器估计的类概率一般都不太准确(这里的不准确是指与真实分布相比),但基于类概率进行结合却往往比直接基于类标记结合性能更好
。
【注意】弱基学习器类型不同,则其类概率不能直接进行比较。此种情况下,需要将类概率转化为类标记再进行投票。
3.3 学习法(Stacking)
当 训练数据很多
时,一种更为强大的结合策略是“学习法”
,即通过另一个学习器来进行结合。典型的方法叫 Stacking
。 Stacking 先从初始数据集
训练出初级学习器
,然后“生成”一个新的数据集(次级训练集
)用于训练次级学习器
。生成次级训练集时,可采用交叉验证或留一法的方式,避免过拟合。
次级学习器的输入属性表示和次级学习算法对Stacking集成的泛化性能有很大影响。有研究表明,将初级学习器的输出类概率作为次级学习器的输入属性,用多响应线性回归作为次级学习算法效果较好。
4. 如何增强多样性?
在集成学习中需有效地生成多样性大的个体学习器,如何增强多样性呢?一般思路是在学习过程中引入随机性,常见的做法是从以下角度增加扰动:
- 数据样本扰动
数据样本的扰动通常是基于采样法
,例如在Bagging中的自助采样
,AdaBoost中的序列采样
。这类方法对 “不稳定学习器" 很有帮助,如决策树、神经网络等。而对于SVM,、线性学习器、朴素贝叶斯、k近邻法等稳定学习器帮助不大。 - 输入属性扰动
每个学习器只用初始属性集中的若干个子属性构成的集合。这对有大量冗余的数据集帮助很大。若数据只包含少量属性,或冗余属性很少,则不适宜使用该方法。 - 输出表示扰动
对输出的表示进行操纵以增强多样性,如把分类输出转换为回归输出后构建个体学习器;还可以把原任务拆解为多个可以同时求解的子任务 - 算法参数扰动
通过随机设置不同的参数,产生差别较大的个体学习器。例如不同的决策树使用的属性选择机制可替换为其他属性选择机制;神经网络隐层神经元数、初始连接权值等。
【注意】使用单一学习器时通常需使用交叉验证等方法来取得参数,这里事实上已经使用了不同参数来训练多个学习器,只不过最终仅选择其中一个进行使用。而集成学习相当于把这些学习器都利用起来,可见,集成学习实际开销并没有比使用单一学习器大很多。
不同的多样性增强机制可同时使用,例如随机森林RF就使用了样本扰动
和属性扰动
。
5. 代码示例
5.1 AdaBoost demo
使用sklearn AdaBoost解决分类问题。数据集:白酒数据,共有13个特征,3个类别,在下面仅使用2个类别和2个特征作为示例。
先加载数据:
# Wine dataset and rank the 13 features by their respective importance measures
df_wine = pd.read_csv(data_dir+'wine.data',
header=None,
names=['Class label', 'Alcohol', 'Malic acid', 'Ash', 'Alcalinity of ash', 'Magnesium',
'Total phenols', 'Flavanoids', 'Nonflavanoid phenols', 'Proanthocyanins', 'Color intensity',
'Hue', 'OD280/OD315 of diluted wines', 'Proline'])
print('Class labels', np.unique(df_wine['Class label'])) # 一共有3个类
df_wine = df_wine[df_wine['Class label']!=1] # 去掉一个类
y = df_wine['Class label'].values
X = df_wine[['Alcohol', 'OD280/OD315 of diluted wines']].values
输出:
Class labels [1 2 3]
选取2个特征,去除一个类别,划分数据集:
le = LabelEncoder()
y = le.fit_transform(y)
print('Class labels', np.unique(y))
print('numbers of features:', X.shape[1])
# 划分训练集测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1, stratify=y)
X_train.shape
输出:
Class labels [0 1]
numbers of features: 2
(95, 2)
先使用一颗决策树分类,作为GBDT的对比参照:
# 先使用决策树做分类,作为GBDT的对比参照
tree = DecisionTreeClassifier(criterion='entropy',
random_state=1,
max_depth=1)
tree = tree.fit(X_train, y_train)
y_train_pred = tree.predict(X_train)
y_test_pred = tree.predict(X_test)
tree_train = accuracy_score(y_train, y_train_pred)
tree_test = accuracy_score(y_test, y_test_pred)
print('Decision tree train/test accuracies %.3f/%.3f' % (tree_train, tree_test))
输出:
Decision tree train/test accuracies 0.916/0.875
再使用AdaBoost分类:
# 使用AdaBoost分类
ada = AdaBoostClassifier(base_estimator=tree,
n_estimators=500,
learning_rate=0.1,
random_state=1)
ada = ada.fit(X_train, y_train)
y_train_pred = ada.predict(X_train)
y_test_pred = ada.predict(X_test)
ada_train = accuracy_score(y_train, y_train_pred)
ada_test = accuracy_score(y_test, y_test_pred)
print('AdaBoost train/test accuracies %.3f/%.3f' % (ada_train, ada_test))
输出:
AdaBoost train/test accuracies 1.000/0.917
绘制决策边界,对比决策树和AdaBoost的分类效果:
# 绘制决策边界
def plot_decision_regions(X, y, classifier_list, classifier_names):
x_min = X[:, 0].min() - 1
x_max = X[:, 0].max() + 1
y_min = X[:, 1].min() - 1
y_max = X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, 0.1),
np.arange(y_min, y_max, 0.1))
f, axarr = plt.subplots(1, 2, sharex='col', sharey='row', figsize=(8, 3))
for idx, clf, tt in zip([0, 1],classifier_list,classifier_names):
clf.fit(X, y)
Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
axarr[idx].contourf(xx, yy, Z, alpha=0.3)
axarr[idx].scatter(X[y==0, 0], X[y==0, 1], c='blue', marker='^')
axarr[idx].scatter(X[y==1, 0], X[y==1, 1], c='red', marker='o')
axarr[idx].set_title(tt)
axarr[0].set_ylabel('Alcohol', fontsize=12)
plt.text(10.2, -0.5, s='OD280/OD315 of diluted wines', ha='center', va='center', fontsize=12)
plt.show()
plot_decision_regions(X_train, y_train, [tree, ada], ['Decision Tree', 'AdaBoost'])
输出:
其实使用AdaBoost+DecisionTree分类树
基本就实现了GBDT分类问题
。
5.2 lightGBM demo
数据说明:某城市各企业用电数据,从2015-1-1至2016-8-31,任务是预测未来一个月(即2016-9月)的用电情况
方法:特征工程+lightGBM
lightGBM 安装:pip3 install lightgbm
lightGBM官方文档:https://lightgbm.readthedocs.io/en/latest/Python-Intro.html
lightGBM有2种Python API:
- 1种是 sklearn 风格的Python API
- 还有一套自己原生的Python API
下面是使用 sklearn 风格 Python API的示例。
加载数据:
# 加载数据
df = pd.read_csv(data_dir + 'tianchi_power_AI.csv')
# 格式转换 将时间字符串转换为pandas认识的时间字段/列
df['record_date'] = pd.to_datetime(df['record_date'])
df.head()
输出:
按每年每个月分组求和,也就是将userId 按月份分组:
# 按每年每个月分组求和
base_df = df[['record_date', 'power_consumption']].groupby(by='record_date').agg('sum')
base_df = base_df.reset_index()
print(base_df['record_date'].min())
print(base_df['record_date'].max())
base_df.head()
输出:
2015-01-01 00:00:00
2016-08-31 00:00:00
特征工程
直接对每天的总量进行回归拟合,先提取用以回归的特征。
(1)提取年、月、日、星期几、一月中的第几天、第几季度等特征:
df_test = base_df[ (base_df['record_date']>='2016-08-01') &
(base_df['record_date']<='2016-08-30')]
# Timedelta: 时间差类型
df_test['record_date'] = df_test['record_date']+pd.Timedelta('31 days') # 8月日期转9月日期
base_df = pd.concat([base_df, df_test]).sort_values(['record_date']) # 包含了2016-9月预测月以及之前的所有月
# 提取年、月、日、星期几、一月中的第几天、第几季度等特征
base_df['dayofweek'] = base_df['record_date'].apply(lambda x: x.dayofweek)
base_df['dayofyear'] = base_df['record_date'].apply(lambda x: x.dayofyear)
base_df['day'] = base_df['record_date'].apply(lambda x: x.day)
base_df['month'] = base_df['record_date'].apply(lambda x: x.month)
base_df['year'] = base_df['record_date'].apply(lambda x: x.year)
# 映射到第几季度
def map_season(month):
month_dict = {1:1, 2:1, 3:1, 4:2, 5:2, 6:2, 7:3, 8:3, 9:3, 10:4, 11:4, 12:4}
return month_dict[month]
base_df['season'] = base_df['month'].apply(lambda x: map_season(x))
base_df.head()
输出:
(2)提取均值、标准差等统计信息特征:
# 按每年每月分组 计算统计信息:均值、标准差
base_df_stats = base_df[ ['power_consumption', 'year', 'month'] ].groupby(by=['year', 'month']).agg(['mean', 'std'])
base_df_stats.columns = base_df_stats.columns.droplevel(0)
base_df_stats = base_df_stats.reset_index()
base_df_stats.head()
合并特征:
base_df_stats['1_m_mean'] = base_df_stats['mean'].shift(1) # 向下移动1
base_df_stats['2_m_mean'] = base_df_stats['mean'].shift(2) # 向下移动2
base_df_stats['1_m_std'] = base_df_stats['std'].shift(1) # 向下移动1
base_df_stats['2_m_std'] = base_df_stats['std'].shift(2) # 向下移动2
data_df = pd.merge(base_df, base_df_stats[ ['year', 'month', '1_m_mean', '2_m_mean', '1_m_std', '2_m_std'] ],
how='inner', on=['year', 'month'])
data_df = data_df[~pd.isnull(data_df['2_m_mean'])] # 去掉Nan数据
data_df.tail()
lightGBM建模
准备训练数据、预测数据:
import lightgbm as lgb
from sklearn.model_selection import GridSearchCV
train_data = data_df[data_df['record_date']<'2016-09-01']\
[['dayofweek','dayofyear','day','month','year','season','1_m_mean','2_m_mean','1_m_std','2_m_std']]
test_data = data_df[data_df['record_date']>='2016-09-01']\
[['dayofweek','dayofyear','day','month','year','season','1_m_mean','2_m_mean','1_m_std','2_m_std']]
train_target = data_df[data_df['record_date']<'2016-09-01'][ ['power_consumption'] ]
train_lgb = train_data.copy()
# 日期/时间格式转换为str类型
train_lgb[ ['dayofweek','dayofyear','day','month','year','season'] ] = \
train_lgb[ ['dayofweek','dayofyear','day','month','year','season'] ].astype(str)
test_lgb = test_data.copy()
test_lgb[ ['dayofweek','dayofyear','day','month','year','season'] ] = \
test_lgb[ ['dayofweek','dayofyear','day','month','year','season'] ].astype(str)
X_lgb = train_lgb.values
y_lgb = train_target.values.reshape(X_lgb.shape[0],)
print(X_lgb.shape, y_lgb.shape, type(X_lgb))
print(X_lgb[0, :])
输出:
(550, 10) (550,) <class 'numpy.ndarray'>
['6' '60' '1' '3' '2015' '1' 2795163.0535714286 3961383.0967741935
769697.8649992085 303629.48662213905]
设置模型参数:
# 模型参数
estimator = lgb.LGBMRegressor(colsample_bytree=0.8, # 建每棵树时使用的属性列的比例(属性采样比例)
subsample=0.9, # 使用训练样本的比例(样本采样比例)
subsample_freq=5) # 采样频率
param_grid = {
'learning_rate': [0.01, 0.02, 0.05, 0.1],
'n_estimators': [100, 200, 400, 800, 1000, 1200, 1500, 2000], # 要学习的boosted trees 个数
'num_leaves': [128, 1024, 4096], # base learner的最大叶子节点数
}
# 训练参数
fit_params = {'categorical_feature': [0,1,2,3,4,5]} # 哪些列是类别型特征,list of int则表示索引
开始训练:
import warnings
warnings.filterwarnings("ignore") # 不打印warning信息
gbm = GridSearchCV(estimator, param_grid)
gbm.fit(X_lgb, y_lgb, **fit_params)
print('Best parameters found by grid search are:', gbm.best_params_)
输出:
Best parameters found by grid search are: {'learning_rate': 0.02, 'n_estimators': 1200, 'num_leaves': 128}
使用最佳参数,重新训练模型,并进行预测:
lgbm = lgb.LGBMRegressor(colsample_bytree=0.8, # 建每棵树时使用的属性列的比例(属性采样比例)
subsample=0.9, # 使用训练样本的比例(样本采样比例)
subsample_freq=5, # 采样频率
learning_rate=0.02,
n_estimators=1200,
num_leaves=128,
objective='regression_l1'
)
lgbm.fit(X_lgb, y_lgb)
X_pred = test_lgb.values
print(X_pred.shape)
y_pred = lgbm.predict(X_pred)
print(y_pred.shape)
输出:
(30, 10)
(30,)
查看特征重要性:
print('Plot feature importances...')
ax = lgb.plot_importance(lgbm, max_num_features=5) # max_num_features 显示最重要的5个特征
plt.show()
输出:
Plot feature importances...
完整代码地址
完整代码请移步至我的github:https://github.com/qingyujean/Magic-NLPer,求赞求星求鼓励~~~
最后:如果本文中出现任何错误,请您一定要帮忙指正,感激~
参考
[1] 统计学习方法(第2版) 李航
[2] 机器学习(西瓜书) 周志华
[3] 集成学习之Adaboost算法原理小结 刘建平