You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
25 KiB
Markdown

2 years ago
# 17集成学习机器学习模型如何“博采众长”?
你好,我是黄佳。恭喜你连闯4关,成功来到最后一关“裂变关”。
回忆一下这一路的旅程在获客关中我们给用户分组画像在变现关中我们关注用户的生命周期价值在激活关中我们预测了App的激活数字在留存关中我们分析了与用户流失相关的因素。
![](https://static001.geekbang.org/resource/image/8c/95/8ce98a4187cc752f3ab9d7ebb988da95.jpg?wh=2000x1000)
那么在裂变关中,我们将从数据中寻找蛛丝马迹,发现“易速鲜花”运营中最佳的“裂变方案”。不过,除了介绍运营中的裂变方案外,今天,我们还要好好讲一讲集成学习。
为什么要专门拿出一讲来谈集成学习呢?我们在[第9讲](https://time.geekbang.org/column/article/419218)说过,我们用机器学习建模的过程,就是和过拟合现象持续作斗争的过程。而集成学习在机器学习中是很特别的一类方法,能够处理回归和分类问题,而且它对于避免模型中的过拟合问题,具有天然的优势。那么,集成学习的优势是怎么形成的?学习了今天的课程后你就会找到答案。
## 定义问题
老规矩,我们先来定义今天要解决的问题。
说起裂变你可能并不会感到陌生。裂变是让产品自循环、自传播的重要工具。像邀请新人得红包、分享App领优惠券、友情助力拿赠品、朋友圈打卡退学费等等都是裂变的玩法。
最近“易速鲜花”运营部门提出了两个裂变思路。方案一是选择一批热销商品让老用户邀请朋友扫码下载App并成功注册朋友越多折扣越大。我们把这个方案命名为“疯狂打折”它走的是友情牌。方案二是找到一个朋友一起购买第二件商品就可以免费赠送这叫“买一送一”。
提出两个裂变方案之后,运营部门收集了[转化数据](https://github.com/huangjia2019/geektime/tree/main/%E8%A3%82%E5%8F%98%E5%85%B317)。那么,我们今天的目标就是**根据这个数据集,来判断一个特定用户在特定的裂变促销之下,是否会转化。**
![](https://static001.geekbang.org/resource/image/b9/5a/b9fa22305a417d65f9704360ecc5825a.png?wh=797x182)
这个问题和我们之前预测用户是否会流失非常相似,也是一个二元分类问题。
下面我们就导入相关的包,并读入数据:
```
import pandas as pd #导入Pandas
import numpy as np #导入NumPy
df_fission = pd.read_csv('易速鲜花裂变转化.csv') #载入数据
df_fission.head() #显示数据
```
输出如下:
![](https://static001.geekbang.org/resource/image/db/f7/db26f6b958f0493277d021e598324df7.png?wh=789x409)
在这个数据集中共有10000个数据样本也就是10000个用户的信息。其他字段都很好理解我们重点来看看“裂变方案”字段。这个字段表示的是该用户所导流到的裂变类型一个用户看到的是“疯狂打折”优惠页还是“买一送一”优惠页是随机分配的结果。对于一个用户二者只能属于其一。而与之对应的“是否转化”字段就**是我们用来预测转化率的标签****了****。**
下面我们进入数据可视化和预处理的环节。
## 数据可视化和预处理
在数据可视化部分,我只想看一看转化和未转化的比例,也就是购买产品和未购买产品的比例。看这个比例是为了看这个数据集中的各类别样本数是否平衡。
```
import matplotlib.pyplot as plt #导入pyplot模块
import seaborn as sns #导入Seaborn
fig = sns.countplot('是否转化', data=df_fission) #创建柱状计数图
fig.set_ylabel("数目") #Y轴标题
plt.show() #显示图像
```
输入如下:
![](https://static001.geekbang.org/resource/image/67/a1/67b604b7a681b0d8fe1e65b0c96961a1.png?wh=389x260)
结果显示在10000个用户中大概有2000个用户购买了产品转化率大概在20%。说明运营人员给我们的是一个并不平衡的分类数据集。这个结论将为我们后续的算法选择做出指导。
下面,我们把数据集中的类别变量,转变为机器学习模型能够读取的虚拟变量(也叫哑编码或哑变量):
```
# 把二元类别文本数字化
df_fission['性别'].replace("女",0,inplace = True)
df_fission['性别'].replace("男",1,inplace=True)
# 显示数字类别
print("Gender unique values",df_fission['性别'].unique())
# 把多元类别转换成多个二元哑变量,然后贴回原始数据集
df_fission = pd.get_dummies(df_fission, drop_first = True)
df_fission # 显示数据集
```
输出如下:
![](https://static001.geekbang.org/resource/image/45/fe/454f8b349de840356a56e455e0ed0afe.png?wh=944x176)
然后,我们再来构建标签和特征数据集,并拆分出训练集和测试集,最后对特征进行归一化缩放。我们对这些步骤已经非常熟悉了,我就不需再过多解释了:
```
X = df_fission.drop(['用户码','是否转化'], axis = 1) # 构建特征集
y = df_fission.是否转化.values # 构建标签集
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size = 0.2)
from sklearn.preprocessing import MinMaxScaler #导入归一化缩放器
scaler = MinMaxScaler() #创建归一化缩放器
X_train = scaler.fit_transform(X_train) #拟合并转换训练集数据
X_test = scaler.transform(X_test) #转换测试集数据
```
这样,数据集就准备好啦,现在,我们进入算法选择环节。
## 算法选择:集成学习
我们前面说,判断一个特定用户在特定的裂变促销之下是否会转化,属于二元分类问题。对于二分类问题,我们在[第15讲](https://time.geekbang.org/column/article/423893)中也提到过它不仅可以用逻辑回归解决还可以通过SVM、集成学习、神经网络等多种模型完成。现在我们就借助这一讲的项目着重来了解一下集成学习方法的原理和应用。
**集成学习(ensemble learning),是通过构建出多个模型(这些模型可以是比较弱的模型),然后将它们组合起来完成任务的机器学习算法**。所以,它实际上并不是一种机器学习算法,而是一个机器学习算法的家族。通常情况下,集成学习将几个表现一般的模型集成在一起,就能大幅提升模型的性能,这就是集成学习的天然优势。
在这个算法家族中很多算法都是“网红”算法比如随机森林、梯度提升机英文叫GB或GBDT和极限梯度提升eXtreme Gradient Boosting即XGBoost有时候简称XGB这些都是非常流行的机器学习算法在很多许多领域都取得了成功并且还是很多人赢得各种机器学习竞赛的主要方法。
那么,为什么几个表现一般的模型集成在一起,性能会大幅提升?下面,我们来探究一下。首先,请你回忆一下,我们在[第9讲](https://time.geekbang.org/column/article/419218)中讲过,机器学习在训练时有一个从欠拟合到过拟合的过程:
![](https://static001.geekbang.org/resource/image/55/37/557e4ff908038d3d43e5653903e1ee37.png?wh=865x273 "从欠拟合到过拟合")
如果结合我们已经比较熟悉的损失曲线,就可以用下面这张图来描述这一过程:
![](https://static001.geekbang.org/resource/image/1d/e1/1d4032ed253404d751383fa3df6a9ee1.png?wh=516x400 "损失、偏差、方差与模型复杂度之间的关系")
我们说,当给定一个学习任务,在训练初期,模型对训练集的拟合还未完善,训练集和测试集上面的损失也都比较大,这时候的模型处于欠拟合状态。由于模型的拟合能力还不强,数据集的改变是无法使模型的效率产生显著变化的,所以,如果我们把此时的模型应用于训练集和测试集的数据,都会出现“**高偏差**”。
随着训练次数增多,模型的拟合能力在调整优化的过程中会变得越来越强,训练集上和测试集上的损失也都会不断下降。
当充分训练之后,模型已经完全拟合了训练集数据,训练集上的损失也变得非常小。但是,这时候的模型很容易受数据的影响,数据的轻微扰动都会导致模型发生显著变化。这时候,如果我们把模型应用于不同的数据集(包括测试集),会出现**很高的方差**,也就是过拟合的状态。
总的来说就是,模型在欠拟合状态会出现“高偏差”,在过拟合状态会出现“高方差”,这都不符合我们的预期。只有在拟合刚刚好的时候,模型才是相对成功的。这时候,模型的偏差和方差处于平衡态,均不会太高。
你可能注意到,在上面的讲述中,我引入了“方差”和“偏差”这两个新概念。方差,是从统计学中引用过来的概念,它表示的是一组数据距离其均值的离散程度。而“偏差”是机器学习里概念,用来衡量模型的准确程度。
在机器学习中,“低偏差”和“低方差”是我们希望达到的效果。可是,一般来说, 低偏差与低方差是鱼与熊掌不可兼得的,这被称作偏差-方差窘境 (bias-variance dilemma)。下面这张打靶图,就形象地说明了这一点:
![](https://static001.geekbang.org/resource/image/1b/65/1b4846d4f28a0954b7e8eecc38b0f265.png?wh=497x439 "方差和偏差对预测结果所造成的影响")
其实,机器学习性能优化领域的最核心问题,就是不断地在“欠拟合-过拟合”之间,也就是“偏差-方差”之间,探求最佳平衡点,换句话说,就是训练集优化和测试集泛化的平衡点。而机器学习的性能优化是有顺序的,我们一般是先减小偏差,再聚焦于降低方差。
那说了这么多,你也许有点不耐烦了:这些内容和集成学习到底有什么关系呢?
其实啊,集成学习的优点就在于,它可以通过组合一些比较简单的算法,来保留这些算法训练出的模型“方差低”的优势;在此基础之上,集成学习又能引入复杂的模型,来扩展简单算法的预测空间。所以,集成学习是同时减小方差和偏差的大招。
集成学习的核心思想是训练出多个模型并将这些模型进行组合。根据分类器的训练方式和组合预测的方法集成学习中两种最重要的方法就是降低偏差的Boosting和降低方差的Bagging。
下面,我们就一边讲解这两种方法,一边用它们来解决“特定用户在特定的裂变促销之下是否会转化成功”的预测问题。
我们先来看Boosting。
## 降低偏差Boosting方法
Boosting方法是把梯度下降的思想应用在了机器学习算法的优化上让弱模型对数据的拟合逐渐增强。它的基本思路就是持续地通过新模型来优化同一个基模型基模型也就是Boosting开始时的初始模型当一个新的弱模型加入进来的时候Boosting就在原有模型的基础上整合这个新模型然后形成新的基模型。而对这个新的基模型的训练则会一直聚集于之前模型的误差点也就是原模型预测出错的样本这样做的目标是不断减小模型的预测误差。
在Boosting方法中有三种很受欢迎的算法分别是AdaBoost、GBDT 和XGBoost。其中AdaBoost会对样本进行加权GBDT在AdaBoost的基础上还会定义一个损失函数通过梯度下降来优化模型而XGBoost则在GBDT的基础上进一步优化了梯度下降的方式。
这三种算法都可以用来解决我们这一讲的分类问题(其实也可以用于回归问题)。下面,我们来逐一做个讲解。
### 1\. AdaBoost算法
我们先来使用AdaBoost算法。在处理分类问题时AdaBoost 会先给不同的样本分配不同的权重被分错的样本的权重在Boosting 过程中会增大新模型会因此更加关注这些被分错的样本反之被分正确的样本的权重会减小。接着AdaBoost会将修改过权重的新数据集输入到新模型进行训练产生新的基模型。最后AdaBoost会把每次得到的基模型组合起来并根据其分类错误率对模型赋予权重集成为最终的模型。
下面我们用AdaBoost算法来预测一下用户在特定的裂变促销之下是否会转化并给出评估分数
```
from sklearn.ensemble import AdaBoostClassifier # 导入AdaBoost 模型
dt = DecisionTreeClassifier() # 选择决策树分类器作为AdaBoost 的基准算法
ada = AdaBoostClassifier(dt) # AdaBoost 模型
ada.fit(X_train, y_train) # 拟合模型
y_pred = ada.predict(X_test) # 进行预测
print("AdaBoost 测试准确率: {:.2f}%".format(ada.score(X_test, y_test)*100))
print("AdaBoost 测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
AdaBoost 测试准确率: 78.75%
AdaBoost 测试F1分数: 50.18%
```
结果显示AdaBoost算法测试准确率是78.75%F1分数为50.18%。这个F1分数并不是很理想我们需要考虑一下有没有更优的算法。而下面要介绍的GBDT算法就对AdaBoost算法做出了进一步的改进。
### 2\. GBDT算法
GBDT算法也叫梯度提升Granding Boosting算法它是梯度下降和Boosting方法结合的产物。因为常见的梯度提升都是基于决策树模型机器学习中就把决策树模型简称为树所以我们这里会把它称作是GBDT即梯度提升决策树Granding Boosting Decision Tree
我们知道前面的AdaBoost算法只是对样本进行加权但GBDT 算法与之不同它还会定义一个损失函数并对损失和机器学习模型所形成的函数进行求导每次生成的模型都是沿着前面模型的负梯度方向一阶导数进行优化直到发现全局最优解。也就是说在GBDT的每一次迭代中当前的树所学习的内容是之前所有树的结论和损失在学习中GBDT会拟合得到一棵新的树而这棵新的树就相当于是之前每一棵树的效果累加。
下面我们用GBDT算法来预测用户是否转化并给出评估分数
```
from sklearn.ensemble import GradientBoostingClassifier # 导入梯度提升模型
gb = GradientBoostingClassifier() # 梯度提升模型
gb.fit(X_train, y_train) # 拟合模型
y_pred = gb.predict(X_test) # 进行预测
print(" 梯度提升测试准确率: {:.2f}%".format(gb.score(X_test, y_test)*100))
print(" 梯度提升测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
梯度提升测试准确率: 87.00%
梯度提升测试F1 分数: 61.19%
```
结果显示GBDT算法的测试准确率是87.00%F1分数为61.19%。F1分数果然大幅提升看来GBDT算法还不错。其实还有比GBDT更厉害的集成学习算法它就是算法XGBoost算法。
### 3\. XGBoost算法
XGBoost算法也叫极端梯度提升eXtreme Gradient Boosting有时候也直接叫作XGB。它和GBDT 类似也会定义一个损失函数。不过不同的是GBDT 只用到一阶导数信息而XGBoost会利用泰勒展开式把损失函数展开到二阶后求导。由于利用了二阶导数信息XGBoost在训练集上的收敛会更快。
在使用XGBoost之前我们需要通过pip语句安装XGBoost包当然你也可以在Anaconda的Environments环境界面中直接搜索并安装XGBoost包
```
pip install xgboost
```
下面我们用XGBoost算法来预测用户是否转化并给出评估分数
```
from xgboost import XGBClassifier # 导入XGB 模型
xgb = XGBClassifier() # XGB 模型
xgb.fit(X_train, y_train) # 拟合模型
y_pred = xgb.predict(X_test) # 进行预测
print("XGB 测试准确率: {:.2f}%".format(xgb.score(X_test, y_test)*100))
print("XGB 测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
XGB 测试准确率: 87.00%
XGB 测试F1分数: 63.17%
```
结果显示XGBoost算法的测试准确率是87.00%F1分数为63.17%。
可以看出在上述三种算法中XGBoost算法的性能是最佳的。虽然它的预测准确率和GBDT相同但是它的F1分数高而F1分数是我们更重视的指标。
下面我们再来介绍并使用能降低方差的Bagging方法。
## 降低方差Bagging方法
Bagging 是Bootstrap Aggregating 的缩写有人把它翻译为套袋法、装袋法或者自助聚合到现在还没有一个统一的叫法。所以我们就直接用它的英文名称Bagging。
Bagging算法的基本思想是从原始的数据集中抽取数据形成K个随机的新训练集然后训练出K个不同的模型。
具体来讲Bagging算法首先会在原始样本集中随机抽取K轮每轮抽取n个训练样本作为一个训练集。这时候有些样本可能被多次抽取而有些样本可能一次都没有被抽取这叫做有放回的抽取。在抽取K轮之后就会形成K个训练集注意这K个训练集是彼此独立的。这个过程也叫作bootstrap可译为“自举”或“自助采样”
接着Bagging算法会每次使用一个训练集并通过相同的机器学习算法如决策树、神经网络等得到一个模型。因为有K个训练集所以一共可以得到K个模型。我们把这些模型称为“基模型”base estimator或者“基学习器”。
最后对于这K个模型Bagging算法会用不同的方式得到基模型的集成结果如果是分类问题Bagging算法会对K个模型投票进而得到分类结果如果是回归问题Bagging算法会计算K个模型的均值将其作为最后的结果。
因为随机抽取数据的方法能减少可能的数据干扰所以经过Bagging 的模型将会具有“低方差””。一般来说Bagging有三种常见的算法决策树的Bagging、随机森林算法和极端随机森林。
### 1\. 决策树的Bagging
多数情况下的Bagging都是基于决策树的。构造随机森林的第一步其实就是对多棵决策树进行Bagging我们把它称为树的聚合Bagging of Tree
决策树这种模型具有显著的低偏差、高方差的特点。也就是说决策树模型受数据的影响特别大一不小心训练集准确率就接近100% 了而这种效果又不能移植到其他的数据集上所以这是很明显的过拟合现象。集成学习的Bagging算法就是从决策树模型开始着手解决它太过于精准又不易泛化的问题。当然Bagging的原理并不仅限于决策树它还可以扩展到其他机器学习算法。
下面我们用决策树的Bagging算法来预测用户是否转化并给出评估分数
```
from sklearn.ensemble import BaggingClassifier # 导入Bagging 分类器
from sklearn.tree import DecisionTreeClassifier # 导入决策树分类器
from sklearn.metrics import (f1_score, confusion_matrix) # 导入评估指标
dt = BaggingClassifier(DecisionTreeClassifier()) # 只使用一棵决策树
dt.fit(X_train, y_train) # 拟合模型
y_pred = dt.predict(X_test) # 进行预测
print(" 决策树测试准确率: {:.2f}%".format(dt.score(X_test, y_test)*100))
print(" 决策树测试F1 分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
bdt = BaggingClassifier(DecisionTreeClassifier()) # 树的Bagging
bdt.fit(X_train, y_train) # 拟合模型
y_pred = bdt.predict(X_test) # 进行预测
print(" 决策树Bagging 测试准确率: {:.2f}%".format(bdt.score(X_test, y_test)*100))
print(" 决策树Bagging 测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
决策树测试准确率: 85.10%
决策树测试F1分数: 58.17%
决策树Bagging 测试准确率: 85.55%
决策树Bagging 测试F1 分数: 58.66%
```
结果显示对于这个数据集来说决策树Bagging的测试集F1分数有提升但是优势不明显。不过没关系我们可以试一下更常见的随机森林算法它其实是经过改进的“决策树Bagging算法”。
### 2\. 随机森林算法
当我们说到集成学习最关键的一点是各个基模型的相关度要小差异性要大。模型间差异越大也就是异质性越强集成的效果越好。两个准确率为99% 的模型,如果其预测结果都一致,也就没有提高的余地了。因此,对决策树做集成,关键就在于各棵树的差异性是否够大。
为了实现基模型差异化的目标,随机森林在树分叉时,增加了对特征选择的随机性,而并不总是考量全部的特征。这个小小的改进,就进一步提高了各棵树的差异。
下面,我们用随机森林算法来预测用户是否转化,并给出评估分数:
```
from sklearn.model_selectifrom sklearn.ensemble import RandomForestClassifier # 导入随机森林模型
rf = RandomForestClassifier() # 随机森林模型
rf.fit(X_train, y_train) # 拟合模型
y_pred = rf.predict(X_test) # 进行预测
print(" 随机森林测试准确率: {:.2f}%".format(rf.score(X_test, y_test)*100))
print(" 随机森林测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
随机森林测试准确率: 87.20%
随机森林测试F1分数: 61.79%
```
可以看到随机森林测试F1分数很明显地高于决策树模型和决策树的Bagging。
### 3\. 极端随机森林算法
最后,我们再来看极端随机森林算法。极端森林算法在随机森林的基础上,又增加了进一步的随机性。
随机森林算法在树分叉时会随机选取m个特征作为考量对于每一次分叉它还会遍历所有的分支然后选择基于这些特征的最优分支。这在本质上仍属于贪心算法greedy algorithm即在每一步选择中都采取在当前状态下最优的选择。而极端随机森林算法则不同它一点也不“贪心”它甚至不回去考量所有的分支而是随机选择一些分支从中拿到一个最优解。
下面,我们用极端随机森林算法来预测用户是否转化,并给出评估分数:
```
from sklearn.ensemble import ExtraTreesClassifier # 导入极端随机森林模型
ext = ExtraTreesClassifier() # 极端随机森林模型
ext.fit(X_train, y_train) # 拟合模型
y_pred = ext.predict(X_test) # 进行预测
print(" 极端随机森林测试准确率: {:.2f}%".format(ext.score(X_test, y_test)*100))
print(" 极端随机森林测试F1分数: {:.2f}%".format(f1_score(y_test, y_pred)*100))
```
输出如下:
```
极端随机森林测试准确率: 87.05%
极端随机森林测试F1分数: 60.58%
```
结果显示对于“预测一个特定用户在特定的裂变促销下是否会转化”这个问题来说极端随机森林测试F1分数要高于决策树和树的Bagging但是低于随机森林算法。
最后,我们可以把上述各个集成学习算法的得分,都显示在一张图中,来比较一下它们在这个问题上的优劣。
![](https://static001.geekbang.org/resource/image/4d/84/4d2bf3a6b9f18e1164824e485b972684.png?wh=937x316)
图中可以看出对于这个问题XGBoost、随机森林和极端随机森林都是比较好的选择。
## 总结一下
好,到这里,我们关于集成学习的介绍就结束了,我们来做个总结。
集成学习模型是将多种同质或者异质的模型集成组合在一起,来形成更优的模型。而在这个过程的目标就是,减少机器学习模型的方差和偏差,找到机器学习模型在欠拟合和过拟合之间的最佳平衡点。
那么怎么降低偏差呢我们可以用Boosting方法把梯度下降的思想应用在机器学习算法的优化上使弱模型对数据的拟合逐渐增强。Boosting也常应用于决策树算法上其中的GBDT 和XGBoost算法都非常受欢迎。
那怎么降低方差呢这就要借助Bagging方法了。Bagging方法通常基于决策树在随机生成数据集上训练出各种各样不同的树。其中的随机森林算法是在树分叉时增加了对特征选择的随机性。随机森林在很多问题上都是一个很强的算法。我们可以把它作为一个基准如果找到了比随机森林还优秀的模型你就可以很欣慰了。
那么最后如果让我在集成学习家族的算法里给你推荐两种常用且效果好的算法基于我个人的经验我会觉得XGBoost、GBDT和随机森林是优于其它几种的。
## 思考题
在这一讲的最后我给你留3个思考题
1. 我在使用这些集成学习算法时都只使用了默认参数请你用GridSearchCV进行参数的网格搜索找出针对这个问题更优的参数训练出更好的模型
2. 请你绘出上述各个算法的混淆矩阵;
3. 请你使用深度神经网络解决这个问题,看看是深层网络的性能好,还是集成学习的性能好?
欢迎你在留言区和我分享你的观点,如果你认为这节课的内容有收获,也欢迎把它分享给你的朋友,我们下一讲再见!
![](https://static001.geekbang.org/resource/image/fe/83/fe84199aa2a33ef737b51a0809b3a183.jpg?wh=2284x1280)