机器学习算法竞赛实战-基础篇
机器学习算法竞赛实战-基础篇
开始学习
一直想学,前面看过觉得太难,这回一定要坚持看完!
第1章 初见竞赛
竞赛流程
问题建模
分析数据进而抽象出建模目标和方案。自行利用主办方提供的数据构造训练集与测试集。
数据探索
EDA(探索性数据分析),Exploratory Data Analysis。在大致了解问题建模方式后,需结合对赛题背景业务的理解去看数据长什么样子、数据是否和描述相符、包含哪些信息等。首先需要对数据有清晰认知,主要是宽表中各个字段的取值含义、范围和数据结构等。然后更深层次地结合标签分析特征的分布状态、训练集与测试集的同分布情况、特征之间的业务关联以及隐含信息表征等。
特征工程
Feature Engineering。特征决定机器学习预测效果上限,算法不断逼近这个上限。最费时的模块。
模型训练
选模型、调参数
模型融合
找找队友,看看Code
第2章 问题建模
赛题理解
从直观上梳理问题,分析问题可解的方法、赛题背景等
业务理解:从个人生活的直观角度对业务进行分析
数据理解:在问题建模阶段,只需对数据做基本的分析。可以将数据理解分为数据基础层和数据描述层两个部分。主办方提供的原始数据质量良莠不齐,往往需要对原始数据进行清洗、加工和计算等处理。
- 数据基础层:重点关注每个数据字段的来源、生产过程、取数逻辑、计算逻辑等,了解这些才能正确理解、选取并使用每一个原始字段,从而加工计算得出所需的更多衍生字段,数据最终通常以数据表格形式呈现。
- 数据描述层:主要是在处理好的数据基础层上进行统计分析和概括描述,该层重点在于尽可能地通过一些简单统计量(如均值、最值、分布、增幅、趋势等)来概括整体数据的状况。具体使用哪些统计量依据数据呈现的具体情况而定。例如,对于时间序列问题,可以统计其增幅、趋势和周期;对于常规的数值特征,则可以观察期均值、最值和方差等统计量;对于存在多类别的样本集合,则可以使用分布、分位点等进行描述。
评价指标
分类指标:
- 错误率(error rate)与精度(accuracy)
错误率:分类错误的样本数占样本总数的比例
精度:分类正确的样本数占样本总数的比例
精度=1-错误率
- 查准率/准确率(precision)、查全率/召回率(recall)
查准率P反映真实情况与预测结果都为正例的样例在预测结果为正例的样例中的占比
查全率R反映真实情况与预测结果都为正例的样例在真实情况为正例的样例中的占比
(查准率与查全率是一对矛盾的度量,一般来讲,查准率高时,查全率偏低;查全率高时,查准率偏低)
,在查准率与查全率之间取得一个平衡
# 构建一个计算准确率、召回率和F1-score的评价代码
y_train = np.array([1,0,1,0,1,0,1,0,1,1])
y_pred = np.array([1,1,1,1,0,0,0,0,1,0])
precision = precision_score(y_train,y_pred) #准确率
recall = recall_score(y_train,y_pred) #召回率
f1 = f1_score(y_train,y_pred) #f1度量
print(precision,recall,f1)
0.6 0.5 0.5454545454545454
- ROC与AUC
先根据学习器的预测结果对样例进行排序,按此顺序逐个把样例作为正例进行预测,每次计算出“真正例率”(True Positive Rate,简称TPR)和“假正例率”(False Positive Rate,简称FPR),分别以他们为纵、横轴作图,就得到了ROC曲线。
,真正例率TPR反映真正例在实际情况为正例的样例中的占比
,假正例率FPR反映假正例在实际情况为反例的样例中的占比
ROC曲线对正负样本的数量和分布不敏感。
AUC定义为ROC下方的面积,在互联网的搜索、推荐和广告的排序业务中都极为常见。AUC作为一个数值,其值越大就代表分类器的效果越好。
值得一提的还有AUC的排序特性。相对于准确率、召回率等指标,AUC指标本身和模型预测的概率绝对值无关,它只关注样本间的排序效果,因此特别适合用作排序相关问题建模的评价指标。AUC是一个概率值,我们随机挑选一个正样本与一个负样本,由当前分类算法根据计算出的分数将这个正样本排在负样本前面的概率就是AUC值。
为什么AUC与模型预测的分数值无关是个很好的特性?假设采用的是准确率等指标,而模型预测的分数是个概率值,那么必须选择一个阈值来决定把哪些样本预测为1,哪些预测为0。阈值的选择不同,准确率的值就会不同。而AUC可以直接使用模型预测分数本身,参考的是相对顺序。在竞赛中,省去了参赛者试探阈值的麻烦。
- 对数损失
对数损失可用于评价分类器的概率输出。对数损失通过惩罚错误的分类来实现对分类器的准确度的量化。最小化对数损失基本等价于最大化分类器的准确度。为了计算对数损失,分类器必须提供概率结果,即把输入样本喂入模型后,预测得到每个类别的概率值(0~1),而不只是预测最可能的类别。
AUC与对数损失的区别
对数损失主要评价模型预测的概率是否足够准确,更关注和观察数据的吻合程度;AUC评价的则是模型把正样本排列到前面的能力。两者侧重不同,故应用不同。对于广告CTR问题,如果考虑广告排序效果,则可以选择AUC,这样不会受极端值影响。此外,对数损失反映了评价偏差,更偏向于将样本数量多的那类划分准确。由于使用AUC或对数损失可以避免把预测概率转换成类别的麻烦,在各种数据竞赛的分类问题中,AUC和对数损失基本是最常见的模型评价指标。
回归指标:
- 平均绝对误差,又称L1范数损失
MAE不是二阶连续可微的,其二阶导数总为0。
- 均方误差,又称L2范数损失
MSE的量纲与数据标签不一致,为了保证量纲的一致性,通常需要对均方误差进行开方(均方根误差RMSE)
平均绝对误差MAE与均方误差MSE的区别
均方误差对误差(真实值-预测值)取了平方,若误差>1,则均方误差会进一步增大误差。如果数据中存在异常点,那误差值就会很大,而误差的平方则会远大于误差的绝对值。因此,相对于使用平均绝对误差计算损失,使用均方误差的模型会赋予异常点更大的权重。简而言之,均方误差对异常值更加敏感。
为什么在XGBoost里通常选择Huber损失替换MAE?
由于MAE不是连续可导的(0处不可导),所以需要使用可导目标函数来逼近平均绝对误差。而对于均方误差MSE,梯度又会随着损失的减小而减小,使预测结果更加精确。在这种情况下,Huber损失就非常有用,它会由于梯度的减小而落在最小值附近。比起均方误差MSE,Huber损失对异常点更加健壮。因此,Huber损失结合了MAE和MSE的优点。但是Huber损失可能需要我们不断调整超参数delta。
- 平均绝对百分比误差
MAPE与MAE一样,不存在二阶导数。但不用于MAE,平均绝对百分比误差MAPE除了考虑预测值与真实值的误差,还考虑了误差与真实值之间的比例。因此真实值越大,误差会越小。
样本选择
主办方提供的数据往往令人脑壳疼,主要是以下四个原因:
- 数据集过大严重影响了模型性能:过大的数据集会严重影响各种特征工程和建模方式的快速验证
- 对数据进行采样处理,然后在小数据集上建模分析
- 特定业务场景下,可以过滤一些对建模没有意义的数据
- 噪声和异常数据导致准确率不够
- 采集数据时操作不当导致信息表征出现错误
- 数据本身的特性存在合理范围内的抖动导致噪声与异常-看是否能够解码出正确数据
- 数据噪声的存在具有两面性,噪声的存在会导致数据的质量变低,影响模型效果;另一方面,可以通过在训练集中引入噪声数据的方法使模型健壮性更强。
- 当处理噪声数据时,首先考虑是否为采集错误导致的,再去权衡模型的泛化性和当前效果。有时去噪会导致模型泛化性能变差。要去噪,首先要识别出噪声,然后采取直接过滤或者修改噪声数据等多种办法,噪声数据可能是特征值不对,比如特征值缺失、超出特征值域范围等;也可能是标注不对,比如二分类问题的正样本标注成了负样本。
- 样本数据冗余或不相关数据没有给模型带来收益
- 数据中存在的冗余不仅会影响模型性能,更会引入噪声与异常。数据冗余的一个典型解决方案就是进行特征选择。
- 正负样本分布不均衡导致数据存在倾斜-进行数据采样
问题1:在数据量非常大的情况下,为了降低成本,如何提高模型的训练速度?
- 方法1:简单随机抽样,分为有放回与无放回
- 方法2:分层采样-按照规定的比例从不同类别中随机抽取样本
问题2:针对正负样本分布不均衡的问题,如何通过数据采样解决这类问题?
- 方法1:评分加权处理
- 分布不均衡的问题包括欺诈交易识别和垃圾邮件识别等,其正负样本的数据分布差距极大。考虑正负样本的重要性,在模型训练以及评价的时候可以设计相应的得分权重,使得模型能够学习到需要获得关注的部分。
- 此方法的具体操作步骤是:首先遍历所有样本,根据样本是否满足某个要求来给予其权重。
- 加权的直观含义从业务上理解就是认为一个正样本的价值大于多个负样本的,因此希望模型在训练的时候能够更多地从正样本身上学到关键信息,当它学得不好的时候,就要对它加大惩罚力度。
- 方法2:欠采样
- 从数量较多的一类样本中随机选取一部分并剔除,使得最终样本的目标类别不太失衡。常用方法有随机欠采样和Tomek Links,其中Tomek Links先是找出两个各相指标都非常接近的相反类样本,然后删除这类样本中标签(label)占比高的,这类算法能够为分类器提供一个非常好的决策边界。
- 方法3:过采样
- 主要是对样本较少的类别进行重新组合,构造新样本。常用的方法有随机过采样和SMOTE算法。SMOTE算法并不是简单地复制已有的数据,而是在原有数据的基础上通过算法产生新生数据。
思考:在什么场景下需要处理样本的不均衡问题?
- 如果竞赛任务对于召回有特别大的需求,即对每个正样本的预测都远远比负样本的预测更重要,那么这时候假如不做任何处理,对结果影响较大
- 如果竞赛的评价指标是AUC,那么在实战中会发现这时处理样本不均衡问题与否对于结果的差别不太大。(但细微提升也是好的)
- 如果在竞赛任务中正负样本同等重要,即预测正确一个正样本与预测准确一个负样本是同等重要的,那么不做处理问题也不大
线下评估
由于需要数据集对模型的效果进行线下验证,所以需要考虑如何对数据进行划分,构建合适的线下验证集。针对不同类型的问题,需要不同的线下验证方式。
书中将这些问题大致分为强时序性与弱时序性两类,然后以此确定线下验证方式。
- 强时序性问题:对于含有明显时间序列因素的赛题,可看作强时序性问题,即线上数据的时间都在离线数据集之后。因此要将最接近测试集的数据作为验证集对模型的效果进行评估(采用时间上最接近测试集的数据做验证集,且验证集的时间分布在训练集之后)
- 弱时序性问题:这类问题的验证方式主要为K折交叉验证(K-fold Cross Validation)
定义:先将总数据集D划分为k个大小相似的互斥子集,每个子集都尽可能保持数据分布的一致性(即从D中分层采样得到)。然后每次用K-1个子集的并集作为训练集,余下的自己作为测试集。这样可以获得K组训练/测试集,从而可进行k次训练和测试,最终返回这k个测试结果的均值。
注意:
- 交叉验证法评估结果的稳定性和保真性很依赖K的取值,K通常取10,常用有5,20等
- 给定k值,仍有多种划分方式。故通常要随机使用不同的划分重复p次,最终的评估结果是这p次k折交叉验证结果的均值,常见有10次10折交叉验证
以下为交叉验证代码,其中参数NFOLDS用来控制折数**(未实际验证)**
from sklearn.model_selection import KFold
NFOLDS = 5 #五折交叉验证
folds = KFold (n_split = NFOLDS,shuffle=True,random_state=2021)#random_state只要是一个固定的值就可以了,不一定是2021
for trn_idx,val_idx in folds.split(X_train,y_train):
train_df,train_label = X_train.iloc[trn_idx, :],y_train[trn_idx]
valid_df,valid_label = X_train.iloc[val_idx, :],y_train[val_idx]
参数random_state默认设置为None,这意为着每次进行KFold(…, shuffle=True)时,打散都是不同的。
为了保证结果的可重复性(在相同的平台上),应该给random_state设定一个固定的值。
第3章 数据探索
如何确保自己准备好竞赛使用的算法模型?如何为数据集选择最合适的算法?如何定义可用于算法模型的特征变量?数据探索可以帮助回答以上三点。
一般而言,数据探索可以分为三个部分:
- 首先是赛前数据探索,帮助我们对数据有个整体性的认识,并发现数据中存在的问题,比如缺失值、异常值和数据冗余等
- 其次是竞赛中的数据探索,通过分析数据发现变量的特点,帮助提取有价值的特征,这里可以从单变量、多变量和变量分布进行分析
- 最后是模型的分析,可以分为重要性分析和结果误差分析,帮助我们从结果发现问题,并进一步优化
数据初探
赛前数据探索,主要包含分析思路、分析方法和明确目的。
- 分析思路
在实际竞赛中,最好使用多种探索思路和方法来探索每个变量并比较结果。在完全理解数据集后,就可以进入数据预处理阶段和特征提取阶段了,以便根据所期望的业务结果转换数据集。此步骤的目标是确信数据集已准备好应用于机器学习算法。
- 分析方法
数据探索的分析主要采用以下方法:
- 单变量可视化分析:提供原始数据集中每个字段的摘要统计信息
- 多变量可视化分析:用来了解不同变量之间的交互关系
- 降维分析:有助于发现数据中特征变量之间方差最大的字段,并可以在保留最大信息量的同时减少数据维度。
可以检查每个变量的分布,定义一些丢失值,最终找到替换它们的可能方法。
- 明确目的
在竞赛中跳过数据探索阶段可能会导致数据倾斜、出现异常值和过多的缺失值,产生以下糟糕结果:
- 生成不准确的模型
- 在错误的数据上生成精确的模型
- 为模型选择错误的变量
- 资源的低效利用,包括模型的重建
数据探索阶段必须要明确:
- 数据集基本情况:比如数据有多大,每个字段各是什么类型
- 重复值、缺失值和异常值:去除重复值,缺失值是否严重,缺失值是否有特殊含义,如何发现异常值
- 特征之间是否冗余:可以通过特征间相似性特征来找出冗余特征
- 是否存在时间信息:当存在时间信息时,通常要进行相关性、趋势性、周期性和异常点的分析,同时有可能涉及潜在的数据穿越问题
- 标签分布:对于分类问题,是否存在类别分布不均衡。对于回归问题,是否存在异常值,整体分布如何,是否需要进行目标转换
- 训练集与测试集的分布:是否有很多在测试集中存在的特征字段在训练集中没有
- 单变量/多变量分布:熟悉特征的分布情况,以及特征和标签的关系
数据探索最基本的步骤之一是获取对数据的基本描述,通过获取对数据的基本描述从而获得对数据的基本感觉。以下方法有助于我们认识数据:
DataFrame.describe()
:查看数据的基本分布,具体是对每列数据进行统计,统计值包含频次、均值、方差、最小值、分位数、最大值等。DataFrame.head(n)
:可以直接加载数据集的前n行,n默认为5DataFrame.shape
:得到数据集的行列情况DataFrame.info()
:可以快速获得对数据集的简单描述,比如每个变量的类型、数据集的大小和缺失值情况。
下面通过一段代码展示nunique和缺失值的情况:
stats = []
for col in train.columns:
stats.append((col, train[col].nunique(), train[col].isnull().sum() * 100 / train.shape[0], train[col].value_counts(normalize=True, dropna=False).values[0] * 100, train[col].dtype))
stats_df = pd.DataFrame(stats, columns=['Feature', 'Unique_values', 'Percentage of missing values', 'Percentage of values in the biggest category', 'type'])
stats_df.sort_values('Percentage of missing values', ascending=False)[:10]
上图展示了经过上述代码生成的数据基本信息,我们从中找到特殊变量进行细致分析,这里选择nunique值低和缺失值多的变量进行观察。一般而言,nunique为1是不具备任何意义的,表示所有值都一样,不存在区分性,需要进行删除。可以发现有些变量的缺失值很多,比如缺失比例达到95%以上,我们可以考虑将其删除。
用柱状图的形式可以更加直观地展示变量的缺失值分布情况,以下为变量缺失值可视化图的具体生成代码:
missing = train.isnull().sum()
missing = missing[missing > 0]
missing.sort_values(inplace=True)
missing.plot.bar()
变量分析
单变量分析
单变量可以分为标签、连续型和类别型
- 标签
标签是最重要的变量,首先应当观察标签的分布情况。对于房屋价格预测,其标签SalePrice为连续型变量。
通过可视化的方式观察SalePrice的分布情况
sns.distplot(train['SalePrice'], color='b', bins=100, hist_kws={'alpha': 0.4})
可见,SalePrice呈偏离正态分布,属于向右倾斜类型,存在峰值状态,一些异常值在500000以上。我们最终会想办法去掉这些异常值,得出能够让算法模型很好学习的、符合正态分布的变量。
下面对SalePrice进行对数转换,并生成可视化图
sns.distplot(np.log(train['SalePrice']), color='b', bins=100, hist_kws={'alpha': 0.4})
可以看出 ,对数转换后的标签的分布为正态分布形式,比较适合算法模型学习。
- 连续型
类似于标签的查看方式,这里主要使用直方图这种可视化方式观察值的分布、每个值出现的频率等。以下为连续型变量的分布可视化的生成代码:
df_num = train.select_dtypes(include = ['float64', 'int64'])
df_num.hist(figsize=(16, 20), bins=50, xlabelsize=8, ylabelsize=8)
实际中要对全部的变量进行查看,分析每一个变量的分布情况。
接着进行更加科学的分析,首先是相关性分析。相关性分析只能比较数值间特征,所以对于字母或字符串特征,需要先进行编码,并将其转换为数值,然后再看有什么关联。在实际竞赛中,相关性分析可以很好地过滤掉与标签没有直接关系的特征。
正相关和负相关
- 正相关:如果一个特征增加导致另一个特征增加,则它们呈正相关。值1表示完全正相关
- 多重线性:现在假设特征A和特征B完全正相关,这意味着这两个特征值包含高度相似的信息,信息几乎没有或完全没有差异。这称为多重线性,因为两个特征包含几乎相同的信息。
- 负相关:如果一个特征增加导致另一个特征减少,则它们呈负相关。值-1表示完全负相关
在搭建或训练模型时,如果同时使用这两个特征,可能其中一个会是多余的。我们应尽量消除冗余特征,因为它会使训练时间变长,同时影响其他优势
以下代码为生成有关SalePrice的相似性矩阵图
corrmat = train.corr()
f, ax = plt.subplots(figsize=(20, 9))
sns.heatmap(corrmat, vmax=0.8, square=True)
从生成的相似性矩阵中,可以找出与房价相关性最强的变量,其中OverallQual(总评价)、GarageCars(车库)、TotalBsmtSF(地下室面积)、GrLivArea(生活面积)等特征与SalePrice呈正相关
从相似性矩阵中,我们还能发现变量之间的关系,如何利用相似性矩阵进行分析就成为了关键
- 类别型
数据探索的目的是为了帮助我们了解数据并且构建有效特征。
比如,我们找到了与标签有着强相关的特征,那么就可以围绕着这个强相关特征进行一系列的扩展,具体可以进行交叉组合,比如强相关加弱相关、强相关加强相关等组合,挖掘更高维度的潜在信息。
首先,观察类别型变量的基本分布情况,即观察每个属性的频次。根据频次,我们不仅可以发现热点属性和极少出现的属性,还可以进一步分析出现这些情况的原因,比如淘宝网的女性用户多于男性,主要是因为平台在服饰和美妆业务方面拥有强大的影响力。这是从业务角度考虑,自然也有可能是数据采样的原因。
对部分类别变量的分布进行可视化展示
df_not_num = train.select_dtypes(include = ['O'])
fig, axes = plt.subplots(round(len(df_not_num.columns) / 3), 3, figsize=(12, 30))
for i, ax in enumerate(fig.axes):
if i < len(df_not_num.columns):
ax.set_xticklabels(ax.xaxis.get_majorticklabels(), rotation=45)
sns.countplot(x=df_not_num.columns[i], alpha=0.7, data=df_not_num, ax=ax)
fig.tight_layout()
多变量分析
单变量分析太过于单一,不足以挖掘变量之间的内在联系,获取更加细粒度的信息,所以有必要进行多变量分析。分析特征变量与特征变量之间的关系有助于构建更好的特征,同时降低构建冗余特征的概率值。
此处选用本赛题中需要特别关注的特征变量进行分析
从相似性矩阵中,我们已知房屋评价与SalePrice呈正相关。进一步扩展分析,通过可视化来考虑房屋评价和房屋位置是否存在某种联系。
plt.style.use('seaborn-white')
type_cluster = train.groupby(['Neighborhood','OverallQual']).size()
type_cluster.unstack().plot(kind='bar',stacked=True, colormap= 'PuBu', figsize=(13,11), grid=False)
plt.xlabel('OverallQual', fontsize=16)
plt.show()
上图为不同房屋位置的评价分布条状图,我们可发现颜色越深代表评价越高。NoRidge、NridgHt和StoneBr都有不错的评价
再进一步看看不同位置房屋的SalePrice
var = 'Neighborhood'
data = pd.concat([train['SalePrice'], train[var]], axis=1)
f, ax = plt.subplots(figsize=(26, 12))
fig = sns.boxplot(x=var, y="SalePrice", data=data)
高评价位置对应高SalePrice,说明房屋位置评价与房屋售价有比较强的相关性。除了通过这样的分析证明原始特征与SalePrice强相关外,还可以通过分析来构建新的特征。
既然房屋位置和房屋评价的组合能够出现更高售价的房屋,那么我们可以构造这两个类别特征的交叉组合特征来进行更细致的描述,也可以构造这个组合特征下的房屋均价等。
模型分析
学习曲线
学习曲线是机器学习中被广泛使用的效果评估工具,能够反映训练集和验证集在训练迭代中的分数变化情况,帮助我们快速了解模型的学习效果。我们可以通过学习曲线来观察模型是否过拟合,通过判断拟合程度来确定如何改进模型
学习曲线广泛应用于机器学习中的模型评估,模型会随着训练迭代逐步学习(优化其内部参数),例如神经网络模型。这时用于评估学习的指标可能会最大化(分类准确率)或者最小化(回归误差),这也意味着得分越高(低)表示学习到的信息越多(少)。
以下是学习曲线图中观察到的一些常见形状
- 欠拟合学习模型
欠拟合是指模型无法学习到训练集中数据所展现的信息,这里可以通过训练损失的学习曲线来确定是否发生欠拟合。在通常情况下,欠拟合学习曲线可能是一条平坦的线或者有着相对较高的损失,也就表明该模型根本无法学习训练集
- 过拟合学习模型
过拟合是指模型对训练集学习得很好,包括统计噪声或训练集中的随机波动。过拟合的问题在于,模型对于训练数据的专业化程度越高,对新数据的泛化能力就越差,这会导致泛化误差增加。泛化误差的增加可以通过模型在验证集上的表现来衡量。如果模型的容量超出了问题所需的容量,而灵活性又过多,则会经常发生这种情况。如果模型训练时间过长,也会发生过拟合。
特征重要性分析
通过模型训练可以得到特征重要性。对于树模型(如LightGBM和XGBoost),通过计算特征的信息增益或分裂次数得到特征的重要性得分。对于模型LR和SVM,则是使用特征系数作为特征重要性得分,例如LR(逻辑回归),每个特征各对应一个特征系数w,w越大,那么改特征对模型预测结果的影响就会越大,就可以认为该特征越重要。我们假定特征性得分和特征系数w都是在衡量特征在模型中的重要性,都可以起到特征选择的作用。
误差分析
误差分析是通过模型预测结果来发现问题的关键。
一般而言,回归问题中看预测结果的分布,分类问题中看混淆矩阵等。
在真实问题中,误差分析会更加细致。比如,在进行一个用户违约预估的二分类任务中,验证集结果中有200个错误分类样本,进一步分析发现有70%的错误分类样本是由于大量特征缺失而导致的误判,这时就需要调整,既可以通过挖掘更多能够描述这些误判样本的特征信息帮助增强模型的预测能力,还可以在模型训练中赋予这些误判样本更高的权重。
第4章 特征工程
数据预处理
尽量得到标准、干净、连续的数据,供数据统计、数据挖掘等使用,视情况尝试对缺失值进行处理,比如是否要填充,填充什么。此外,有些竞赛提供的数据集以及对应的存储方式可能使得需要占用超过参赛者本身硬件条件的内存,故有必要进行一定的内存优化,这也有助于在有限的内存空间对更大的数据集进行操作。
缺失值处理
除了XGBoost和LightGBM等算法在训练时可以直接处理缺失值以外,其他很多例如LR、DNN、CNN、RNN等都并不能对缺失值进行直接处理。故而在数据准备阶段,要比构建算法阶段花更多时间,因为像填补缺失值这样的操作需要细致处理。
- 区分缺失值
首先,需找到缺失值表现形式。除了None、NA和NaN外,还有例如-1或-999来填充的缺失值。还有一种看上去像缺失值,但实际上有实际意义的业务,此时需特殊对待。例如没有填“婚姻状态”的用户可能是对自己隐私比较敏感,应为其单独设为一个分类;没有“驾龄”可能是没有车,为其填充0比较合适。
- 处理方法
数据缺失可以分为类别特征的缺失和数值特征的缺失两种。
- 对于类别特征,通常会填充一个新类别,如0,-1等。
- 对于数值特征,可以均值填充(但对异常值较为敏感),中位数填充(对异常值不敏感)。填充时一定要考虑所选择的填充方法会不会影响数据的准确性。
填充方法总结如下:
- 类别特征:可选择最常见的一类填充方法,即众数;或直接填一个新类别
- 数值特征:可填平均数、中位数、最大最小值等,具体情况具体分析
- 有序数据(如时间序列):可填充相邻值next或previous
- 模型预测填充:普通的填充仅是一个结果的常态,并未考虑其他特征间相互作用的影响,可以对含有缺失值的那一列进行建模并预测其中缺失值的结果。方法虽然复杂但随后得到的结果直觉上比直接填充要好。
异常值处理
实际数据中可能会发现某个或某些字段(特征)根据某个变量(如时间序列问题中的时间)排序后,经观察存在一些数值远高于或低于其一定范围内的其他数值。还有些不合理的存在,这些都可以视作异常值,他们可能会给算法性能带来负面影响。
- 寻找异常值
首先,找到异常值,总结了两种方法:
- 通过可视化分析。简单使用散点图(Matplotlib),严重偏离密集区域的点都可当作异常值来处理
- 通过简单的统计分析,即根据基本的统计方法来判断数据是否异常,例如四分位数间距、极差、均差、标准差等,这种方法适合于挖掘单变量的数值型数据。(seaborn库的箱型图)
- 处理异常值
- 删除含有异常值的记录。优点:可消除含有异常值的样本带来的不确定性。缺点:减少了样本量
- 视为缺失值。优点:将异常值集中化为一类,增加了数据的可用性。缺点:将异常值和缺失值混为一谈,会影响数据的准确性、
- 平均值(中位数修正)。用对应同类别的数值使用平均值修正该异常值。优缺点同“视为缺失值”
- 不处理。直接在有异常值的数据集上进行数据挖掘。这就听天由命看异常值来源了。
离散型异常值(离散属性定义范围以外的所有值均为异常值)、知识型异常值(如大学生脱发情况:从无)等,都可以当做类别缺失值来处理。
- 优化内存
数据集太大而自己的硬件条件有限就有可能会因为内存不够导致代码出现memory error,介绍Python的内存回收机制和数值类型优化这两种有助于优化内存的常见方法。
- 内存回收机制:在Python的内存回收机制中,gc模块主要运用“引用计数”来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记清除”来解决容器对象可能产生的循坏引用问题,通过“隔代回收”以空间换取时间来进一步提高垃圾回收的效率。一般来讲,在我们删除一些变量时,使用gc.collect()来释放内存。(慎用)
- 数值类型优化。竞赛中常使用的数据保存格式是csv以及txt,在进行处理时,需要将其读取为表格型数据,即DataFrame格式。需要利用pandas进行操作。pandas可以在底层将数值型数据表示成NumPy数组,并使其在内存中连续存储。这种存储方式不仅消耗的空间较少,还使我们能够快速访问数据。
我们可以用np.iinfo类来确认每一个int型子类型的最大值和最小值
import numpy as np
np.iinfo(np.int8).min
np.iinfo(np.int8).max
- 对于类别型的变量,若其编码ID的数字较大、极不连续且种类较少,则可以从0开始编码(自然数编码),这样可以减少变量的内存占用。
- 对于数值型的变量,常常由于存在浮点数使得内存占用过多,可以考虑先将其最小值和最大值归一化,然后再乘以100、1000等,再取整,节省内存空间。
特征变换
连续变量无量纲化
无量纲化指的是将不同规格的数据转换到同一规格。常见无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,特征值服从标准正态分布。区间缩放法利用了边界信息,将特征的取值区间缩放到某个特定的范围,例如[0,1]
单特征转换是构建一些模型(如线性回归、KNN、神经网络)的关键,对于决策树相关模型并无影响。还有些纯粹的工程原因,即在进行回归预测时,对目标取对数处理,不仅可以缩小数据范围,而且压缩变量尺度使数据更平稳。
然而,数据要求不仅是通过参数化方法施加的。如果特征没有被规范化,例如当一个特征的分布位于0附近且范围不超过(-1,1),而另一个特征的分布范围在数十万数量级时,会导致分布于0附近的特征变得完全无用。
- 标准化:最简单的转换是标准化(零-均值规范化)。标准化需要计算特征的均值和标准差。
- 区间缩放:区间缩放思路有很多种,常见的一种使利用最大最小值进行缩放。
2.2 连续变量数据变换
1.log变换
进行log变换可以将倾斜数据变得接近正态分布,因为大多数机器学习模型不能很好地处理非正态分布数据,比如右倾数据。可以应用log(x+1)变换来修正倾斜,其中+1的目的是防止数据等于0,同时保证x都是正的。取对数不会改变数据的性质和相关关系,但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
扩展:cbox-cox变换,一种自动寻找最佳正态分布变换函数的方法。
连续变量数据变换
log变换可以将倾斜数据变得接近正态分布。
离散化后的特征对异常数据有很强的健壮性,更便于探索数据的相关性。常用的离散化分为无监督和有监督两种。
无监督的离散化分桶操作可以将连续变量离散化,同时使数据平滑,即降低噪声的影响。一般分为等频和等距两种分桶方式。
- 等频:区间的边界值要经过选择,使得每个区间包含数量大致相等的变量实例。这种分桶方式可以将数据变成均匀分布。
- 等距:将实例从最小到最大值,均分为N等份,每份的间距是相等的。这里只考虑边界,每等份的实例数量可能不等。等距可以保持数据原有的分布,并且区间越多,对数据原貌保持得越好。
有监督的离散化对目标有很好的区分能力,常用的是使用树模型返回叶子节点来进行离散化。如在GBDT+LR经典模型中,就是先使用GBDT来将连续值转化为离散值。具体方法:用训练集中的所有连续值和标签输出来训练LightGBM,共训练两棵决策树,第一棵有4个叶子节点,第二棵树有3个叶子节点。如果某一个样本落在第一棵树的第三个叶子节点上,落在第二棵树的第一个叶子节点上,那么它的编码就是0010100,一共7个离散特征,其中会有两个取值为1的位置,分别对应每棵树中样本落点的位置。最终我们会获得num_trees*num_leaves维特征。
类别特征转换
在实际数据中,特征并不总是数值,还有可能是类别。对于离散型的类别特征进行编码,一般分为两种情况:自然数编码(特征有意义)和独热(one-hot)编码(特征无意义)。
自然数编码:一列有意义的类别特征(即有顺序关系)可以使用自然数进行编码,利用自然数的大小关系可以保留其顺序关系。以下是两种自然数编码的常用方式:
①调用sklearn中函数:
from sklearn import preprocessing
from f in columns:
le = preprocessing.LableEncoder()
le.fit(data[f})
②自定义实现(速度快)
for f in columns:
data[f] = data[f].fillna(-999)
data[f] = data[f].map(dict(zip(data[f].unique(),range(0,data[f].nunique()))))
独热编码:当类别特征没有意义(即无顺序关系)时,需要使用独热编码。例如,红>蓝>绿不代表任何东西,进行独热编码后,每个特征的取值对应一维特征,最后得到一个样本数×类别数大小的0~1矩阵。可直接调用sklearn中API进行生成(或者是使用哑变量的方式)
不规则特征变换
不规则特征可能包含样本的很多信息,比如身份证号,各段表示不同的信息。一般不会提供这种比较敏感的信息。
特征提取
机器学习模型很难识别复杂的模式,特别是很难学习到不同特征组合交叉的信息,所以我们可以基于对数据集的直觉分析和业务理解创建一些特征来帮助模型有效学习。下面我们将介绍结构化数据的特征提取方式。
(结构化数据由明确定义的数据类型组成,非结构化数据由音频、视频和图片等不容易搜索的数据组成。)
类别相关的统计特征
类别特征又可以称为离散特征,除了每个类别属性的特定含义外,还可以构造连续型的统计特征,以挖掘更多有价值的信息,比如构造目标编码、count、nunique和ratio等特征。另外,也可以通过类别特征间的交叉组合构造更加细粒度的特征。
- 目标编码
目标编码可以理解为用目标变量(标签)的统计量对类别特征进行编码,即根据目标变量进行有监督的特征构造。如果是分类问题,可以统计目标均值、中位数和最值。目标编码的方式可以很好地替代类别特征,或者作为新特征。
使用目标变量时,非常重要的一点是不能泄露任何验证集的信息。所有基于目标编码的特征都应该在训练集上计算,测试集则由完整的训练集来构造。更严格一点,我们在统计训练集的特征时,需要采用K折交叉统计法构造目标编码特征,从而最大限度地防止信息泄露。如用五折交叉统计构造特征时,我们将样本划分为五份,对于其中每一份数据,我们都将用另外四份来计算每个类别取值对应目标变量的频次、比例或者均值,简单来说就是未知的数据(一份)在已知的数据(四份)里面取特征。
目标编码方法对于基数较低的类别特征通常很有效,但对于基数较高的类别特征,可能会有过拟合的风险。因为会存在一些类别出现频次非常低,统计出来的结果不具有代表性。一般会加入平滑性来降低过拟合风险。在处置妥当的情况下,无论是线性模型,还是非线性模型,目标编程都是最佳的编码方式和特征构造方式。
- count nunique ratio
count:计数特征,用于统计类别特征的出现频次
nunique和ratio常常会涉及多个类别特征的联合构造。例如在广告点击率预测问题中,对于用户ID和广告ID,使用nunique可以反映用户对广告的兴趣宽度,也就是统计用户ID看过几种广告ID;使用ratio可以反映用户对某类广告的偏好程度,即统计用户ID点击某类广告ID的频次占用户点击所有广告ID频次的比例。
- 类别特征之间交叉组合
交叉组合能够描述更细粒度的内容。对类别特征进行交叉组合在竞赛中是一项非常重要的工作,这样可以进行很好的非线性特征拟合。如用户年龄和用户性别可以组合成“年龄_性别”这样的新特征。一般我们可以对两个类别或三个类别特征进行组合,也称作二阶组合或三阶组合。简单来讲,就是对两个类别特征进行笛卡尔积的操作,产生新的类别特征。
并非所有组合都是需要考虑的,我们会从两个方面进行分析。
- 业务逻辑方面:比如用户操作系统版本与用户所在城市的组合是没有实际意义的。
- 类别特征的基数:如果基数过大,那么可能导致很多类别只会出现一次,在一轮训练中,每个类别只会被训练一次,显然特征对应权重的置信度是很低的。
数值相关的统计特征
这里所说的数值特征,我们认为是连续的。数值特征的大小是有意义的,通常不需要处理就可以直接“喂”给模型进行训练。除了之前对数值特征进行各种变换外,还存在一些其他常见的数值特征构造方式。
- 数值特征之间的交叉组合:一般对数值特征进行加减乘除等算术操作类的交叉组合。这需要我们结合业务理解和数据分析进行构造。
- 类别特征和数值特征之间的交叉组合:除了类别特征之间和数值特征之间的交叉组合外,还可以构造类别特征与数值特征之间的交叉组合。这类特征通常是在类别特征的某个类别中计算数值特征的一些统计量,比如均值、中位数和最值等。
- 按行统计相关特征:行统计在构造时会包含更多的列,直接对多列进行统计。
时间特征
在实际数据中,通常给出的时间特征是时间戳属性,所以首先需要将其分离成多个维度,比如年月日小时分钟秒钟。如果你的数据源来自于不同的地理数据源,还需要利用时区将数据标准化。除了分离出来的基本时间特征外,还可以构造时间差特征,即计算出各个样本的时间与未来某一个时间的数值差距,这样这个差距是UTC的时间差,从而将时间特征转换为连续值,比如用户首次行为日期与用户注册日期的时间差、用户当前行为与用户上次行为的时间差等。
多值特征
在竞赛中,可能会遇到某一列特征中每行都包含多个属性的情况,这就是多值特征。例如广告大赛中的兴趣类目,其中包含5个兴趣特征组,每个兴趣特征组都包含若干个兴趣ID。对于多值特征,通常可以进行稀疏化或者向量化的处理,这种操作一般出现在自然语言处理中,比如文本分词后使用TF-IDF(词频-逆文档频率)、LDA(隐含狄利克雷分布)、NMF(非负矩阵分解)等方式进行处理,这里则可以将多值特征看作文本分词后的结果,然后做相同的处理。
对多值特征最基本的处理办法是完全展开,即把这列特征所包含的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,可以方便地将多值特征展开,只考虑每个属性在这个特征的出现频次。
还有一种情况,比如在广告算法大赛中,需要根据用户的历史点击行为预测用户的属性标签。这时候用户的点击序列尤为重要,当我们构造好用户对应的历史点击序列后,除了使用上述的TF-IDF等方法外,还可以提取点击序列中商品或广告的嵌入表示,比如用Word2Vec、DeepWalk等方法获取embedding向量表示。因为要提取用户单个特征,所以可以对序列中的嵌入向量进行聚合统计,这种方法本质上是假设用户点击过的商品或广告等同重要,是一种比较粗糙的处理方式。我们可以引入时间衰减因素,或者使用序列模型,如RNN、LSTN、GRU,套用NLP的方法进行求解。
特征选择
当我们添加新特征时,需要验证它是否确实能够提高模型预测的准确度,以确定不是加入了无用的特征,因为这样只会增加算法运算的复杂度,这时候就要通过特征选择算法自动选择出特征集中的最优子集,帮助模型提供更好的性能。特征选择算法用于从数据中识别并删除不需要、不相关以及冗余特征。这些特征可能会降低模型的准确度和性能,特征选择的方法主要有先验的特征关联性分析以及后验的特征重要性分析。、
特征关联性分析
特征关联性分析是使用统计量来为特征之间的相关性进行评分。特征按照分数进行排序,要么保留,要么从数据集中删除。关联性分析方法通常是针对单变量的,并独立考虑特征或者因变量。常见的特征关联性分析方法有皮尔逊相关系数、卡方检验、互信息法和信息增益等。这些方法速度快、使用方便,但是忽略了特征之间的关系,以及特征和模型之间的关系。
- 皮尔逊相关系数
不仅可以衡量变量之间的线性相关性,解决共线变量问题,还可以衡量特征与标签的相关性。共线变量是指变量之间存在高度相关关系,这会降低模型的学习可用性,可解释性以及测试集的泛化性能。但这三个特性都是我们想增加的,所以删除共线变量是一个有价值的步骤。我们将为删除共线变量建立一个基本的阈值(根据想要保留的特征数量决定)。
下面代码用于解决特征与标签不具有相关性的问题,根据皮尔逊相关系数的计算提取top300的相似特征:
def feature_select_pearson(train,features):
featureSelect = features[:]
#进行皮尔逊相关性计算
corr=[]
for feat in featureSelect:
corr.append(abs(train[[feat,'target']].fillna(0).corr().values[0][1]))
se = pd.Series(corr,index=featureSelect).sort_values(ascending=False)
feature_select = se[:300}.index.tolist()
#返回特征选择后的训练集
return train[feature_select]
- 卡方检验
用于检验特征变量与因变量的相关性。对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验恰好可以进行独立性检验,所以使用与特征选择。如果检验结果是某个特征与标签独立,则可以去除该特征。
- 互信息法
互信息是对一个联合分布中两个变量之间相互影响关系的度量,也可以用于评价两个变量之间的相关性。互信息法之所以能够用于特征选择,可以从两个角度进行解释:基于KL散度和基于信息增益。互信息越大说明两个变量相关性越高。
但是想把互信息直接用于特征选择其实不太方便,由于:
- 它不属于度量方式,也没有办法归一化,无法对不同数据集上的结果进行比较
- 对于连续变量的计算不是很方便(X和Y都是集合,xi和y都是离散的取值),通常连续变量需要先离散化,而互信息的结果对离散化的方式很敏感。
特征重要性分析
在实际竞赛中,经常用到的一种特征选择方法是基于树模型评估特征的重要性分数。特征的重要性分数越高,说明特征在模型中被用来构建决策树的次数越多。这里我们以XGBoost为例来介绍树模型评估特征重要性的三种计算方法(weight、gain和cover)。(LightGBM也可以返回特征重要性)
- weight计算方式:该方式比较简单,计算特征在所有树中被选为分裂特征的次数,并将以此作为评估特征重要性的依据
params ={
'max_depth':10,
'subsample':1,
'verbose_eval':True,
'seed':12,
'objective':'binary:logistic'
}
xgtrain = xgb.DMatrix(x,label=y)
bst = xgb.train(params,xgtrain,numm_boost_round=10)
importance = bst.get_score(fmap='',importance_type='weight')
- gain计算方式:gain表示平均增益。在进行特征重要性评估时,使用gain表示特征在所有树中作为分裂节点的信息增益之和再除以该特征出现的频次。
importance =bst.get_score(fmap='',importance_type='gain')
- cover计算方式:cover是特征对每棵树的覆盖率,即特征被分到该节点的样本的二阶导数之和,而特征度量的标准就是平均覆盖率值。
importance = bst.get_score(fmap='',importance_type='cover')
技巧:虽然特征重要性可以帮助我们快速分析特征在模型训练过程中的重要性,但不能将其当做绝对的参考依据。一般而言,只要特征不会导致过拟合,我们就可以选择重要性高的特征进行分析和扩展,对于重要性低的特征,可以考虑将之从特征集中移除,然后观察线下效果,再做进一步判断。
封装方法
可以将一组特征的选择视作一个搜索问题,在这个问题中,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集,搜索过程可以是系统性的,比如最佳优先搜索;也可以是随机的,比如随机爬山算法,或者启发式方法,比如通过向前和向后搜索来添加和删除特征(类似前剪枝和后剪枝算法)。这种方法比较耗时。
- 启发式方法:分为前向搜索和后向搜索。前向搜索是每次增量地从剩余未选中的特征中选出一个并将其加入特征集中,待特征集中的特征数量达到初设阈值时,意味着贪心选出了错误率最小的特征子集。既然有增量加,就会有增量减,后者称为后向搜索,即从特征全集开始,每次删除其中的一个特征并评价,知道特征集中的特征数量达到初设阈值,就选出了最佳的特征子集
- 因为启发式方法会导致局部最优,所以加入模拟退火方式进行改善,这种方式不会因为新加入的特征不能改善效果而舍弃该特征,而是对其添加权重后放入已选特征集。这种启发式方法是很耗时间耗资源的。
- 递归消除特征法:用一个基模型来进行多轮训练,每轮训练都会先消除若干权值系数的特征,再基于新特征集进行下一轮训练。可以使用feature_selection库的RFE类来进行特征选择
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
#递归消除特征法,返回特征选择后的数据
#参数estimator为基模型
#参数n_feature_to_select 为选择的特征个数
RFE(estimator=LogisticRegression(),n_features_to_select=2).fit_transform(data,target)
技巧:在使用封装方法进行特征选择时,用全量数据训练并不是最明智的选择。应先对大数据进行采样,再对小数据使用封装方法
以上三种特征选择方法按需使用或组合使用,建议优先考虑特征重要性,其次是特征关联性。
此外还有null importance。其思想:将构建好的特征和正确的标签喂给树模型得到一个特征重要性分数,再将特征和打乱后的标签喂给树模型得到一个特征重要性分数,然后对比两个分数,如果前者没有超过后者,那么这个特征就是一个无用的特征。
第5章 模型训练
参考资料 :《机器学习算法竞赛实战》整理 | 五、模型训练
线性模型
Lasso回归
Lasso回归是对普通的线性回归采用L1正则化进行优化,通过惩罚或限制估计值的绝对值之和,可以使某些系数为零,从而达到特征稀疏化和特征选择的效果。当我们需要一些自动的特征、变量选择,或者处理高度相关的预测因素时,很方便。
from sklearn.linear_model import Lasso
lasso_model = Lasso(alpha = 0.1, normalize = True)
只保留不相关的特征,其他为0,可能会导致信息损失
Ridge回归
Ridge回归是对普通的线性回归采用L2正则化进行优化,对特征的权重系数设置了惩罚项。
from sklearn.linear_model import Ridge
ridge_model = Ridge(alpha = 0.05, normalize = True)
不会减少特征数量,不利于特征缩减。
两者合并:Elastic Net Regression
树模型
本节将介绍竞赛中常见的树模型,这些模型简单易用,能够带来高收益。
可将树模型分为随机森林(Random Forest, RF)和梯度提升树(GBDT), 这两者最大的差异是前者并行、后者串行。在梯度提升树部分我们将介绍如今竞赛中大火的三种树模型: XGBoost、 LightGBM 和CatBoost。能够灵活运用这三种模型是竞赛中的必备技能。接下来将详细介绍各种树模型的数学形式、优缺点、使用细节和应用场景。
随机森林
随机森林就是通过集成学习的思想将多个决策树集成在一起,各个决策树之间没有任何关联。随机森林算法对多个决策树的结果进行投票得到最终结果,也是最简单的bagging思想 。
随机森林的优点:
- 不仅可以解决分类和回归问题,还可以同时处理类别特征和数值特征;
- 不容易过拟合,通过平均决策树的方式,降低过拟合的风险;
- 非常稳定,即使数据集中出现了一个新的数据点,整个算法也不会受到过多影响,新的数据点只会影响到一棵决策树,很难对所有决策树都产生影响。
很多缺点都是相对而言的:
- 随机森林算法虽然比决策树算法更复杂,计算成本更高,但是其拥有天然的并行特性,在分布式环境下可以很快地训练。
- 梯度提升树需要不断地训练残差,进行所以结果准确度更高,但是随机森林更不容易过拟合,更加稳定,这也是因为其Bagging的特性。
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(max_ features=' auto', oob_ score=True, random state=1, n_ jobs=-1)
梯度提升树
梯度提升树(GBDT)是基于Boosting改进而得的,在Boosting算法中,一系列基学习器都需要串行生成,每次学习一棵树,学习目标是上棵树的残差。和AdaBoost 一样,梯度提升树也是基于梯度下降函数。梯度提升树算法已被证明是Boosting算法集合中最成熟的算法之一,它的特点是估计方差增加,对数据中的噪声更敏感(这两个问题都可以通过使用子采样来减弱),以及由于非并行操作而导致计算成本显著,因此要比随机森林慢很多。
梯度提升树是XGBoost、LightGBM和CatBoost的基础。
XGBoost
- 采用稀疏感知算法,XGBoost可以利用稀疏矩阵,节省内存(不需要密集矩阵)和节省计算时间(零值以特殊方式处理)。
- 近似树学习(加权分位数略图),这类学习方式能得到近似的结果,但比完整的分支切割探索要省很多时间。
- 在一台机器上进行并行计算(在搜索最佳分割阶段使用多线程),在多台机器上进行类似的分布式计算。
- 利用名为核外计算的优化方法,解决在磁盘读取数据时间过长的问题。将数据集分成多个块存放在磁盘中,使用一个独立的线程专门从磁盘读取数据并加载到内存中,这样一来,从磁盘读取数据和在内存中完成数据计算就能并行运行。
- XGBoost还可以有效地处理缺失值,训练时对缺失值自动学习切分方向。基本思路是在每次的切分中,让缺失值分别被切分到决策树的左节点和右节点,然后通过计算增益得分选择增益大的切分方向进行分裂,最后针对每个特征的缺失值,都会学习到一个最优的默认切分方向。
import xgboost as xgb
params = {'eta': 0.01, ' max depth': 11, 'objective': 'reg:linear', 'eval_ metric': 'rmse' }
dtrain = xgb.DMatrix(data=X_train, label=y_train)
dtest = xgb.DMatrix(data=X_valid, label=y_valid)
watchlist = [(train.data, 'train'), (valid_data, 'valid_ data')]model=xgb. train(params, train_data,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)y_pred = model. predict(xgb .DMatrix(X_test), ntree_limit=model.best_ntree_limit)
LightGBM
LightGBM是微软的一个团队在Github上开发的一个开源项目,高性能的LightGBM算法具有分布式和可以快速处理大量数据的特点。LightGBM虽然基于决策树和XGBoost而生,但它还遵循其他不同的策略。
XGBoost使用决策树对一个变量进行拆分,并在该变量上探索不同的切割点(按级别划分的树生长策略),而LightGBM则专注于按叶子节点进行拆分,以便获得更好的拟合(这是按叶划分的树生长策略)。这使得LightGBM能够快速获得很好的数据拟合,并生成能够替代XGBoost的解决方案。从算法上讲,XGBoost将决策树所进行的分割结构作为一个图来计算,使用广度优先搜索(BFS),而LightGBM使用的是深度优先搜索(DFS)。
主要特点
- 比XGBoost准确性更高,训练时间更短。
- 支持并行树增强,即使在大型数据集上也能提供比 XGBoost更好的训练速度。
- 通过使用直方图算法将连续特征提取为离散特征,实现了惊人的快速训练速度和较低的内存使用率。
- 通过使用按叶分割而不是按级别分割来获得更高精度,加快目标函数收敛过程,并在非常复杂的树中捕获训练数据的底层模式。使用num_leaves和max_depth超参数控制过拟合。
import lightgbm as lgb
params = {'num_leaves': 54, 'objective': 'regression', 'max_depth': 18,'learning_rate': 0.01, 'boosting': 'gbdt', 'metric': 'rmse', 'lambda_11': 0.1}
model = lgb.LGBMRegressor(**params, n_estimators = 20000, nthread = 4, n_jobs = -1)
model.fit(x_train, y_train, eval_set=[(X_train, y_train), (X_valid, y_valid)], eval_metric='rmse', verbose=1000, early_stopping_rounds=200)
y_pred= model.predict(X_test, num_iteration=model.best_iteration_)
CatBoost
CatBoost是由俄罗斯搜索引擎Yandex在2017年7月开源的一个GBM算法,它最强大的点是能够采用将独热编码和平均编码混合的策略来处理类别特征。
CatBoost用来对类别特征进行编码的方法并不是新方法,是均值编码,该方法已经成为一种特征工程方法,被广泛应用于各种数据科学竞赛中,如Kaggle。
均值编码,也称为似然编码、影响编码或目标编码,可将标签转换为基于它们的数字,并与目标变量相关联。如果是回归问题,则基于级别典型的平均目标值转换标签;如果是分类问题,则仅给定标签的目标分类概率(目标概率取决于每个类别值)。均值编码可能看起来只是一个简单而聪明的特征工程技巧,但实际上它也有副作用,主要是过拟合,因为会把目标信息带入预测中。
主要特点
- 支持类别特征,因此我们不需要预处理类别特征(例如通过label encoding或独热编码)。事实上,CatBoost文档中讲到不要在预处理期间使用独热编码,因为“这会影响训练速度和结果质量”。
- 提出了一种全新的梯度提升机制(Ordered Boosting),不仅可以减少过拟合的风险,也大大提高了准确性。
- 支持开箱即用的GPU训练(只需设置task_type=“GPU”)。
- 训练中使用了组合类别特征,利用了特征之间的联系,极大丰富了特征维度。
- 在树分裂选择节点的时候能够将所有类别特征之间的组合考虑进来,即能够对两个类别特征进行组合。
- 目前还支持输入文本特征,因此不需要像以前那样先进行烦琐的操作获得标准化输入,再喂给模型。
from catboost import CatBoostRegressor
params = {'learning_rate': 0.02, 'depth': 13,'bootstrap_type': 'Bernoulli', 'od_type': 'Iter', 'od_wait': 50, 'random_seed': 11}
model = CatBoostRegressor(iterations=20000, eval_metric='RMSE', **params)
model.fit(X_train, y_train, eval_set=(X_valid, y_valid), cat_features=[], use_best_model=True, verbose=False)
y_pred = model.predict(X_test)
模型深入对比
每类树模型都其与众不同的地方,接下来将从决策树的生长策略、梯度偏差、类别特征处理和参数对比四个方面深入理解这些树模型,帮助参赛者更好地将它们应用到竞赛中。
XGBoost,LightGBM 和 CatBoost是三个非常核心的树模型,本节将对它们进行分析,因为三者之间有着千丝万缕的关系,只有厘清其中的关系,才能更好地运用这三个模型。
- 决策树生长策略
- XGBoost使用的是Level-wise按层生长,可以同时分裂同一层的叶子,从而进行多线程优化,不容易过拟合,但很多叶子节点的分裂增益较低,会影响性能。
- LightGBM使用的是Leaf-wise分裂方式,每次都从当前叶子中选择增益最大的结点进行分裂,循环迭代,但会生长出非常深的决策树,从而导致过拟合,这时可以调整参数max_depth来防止过拟合。
- CatBoost 使用的是oblivious-tree(对称树),这种方式使得节点是镜像生长的。相对于传统的生长策略,oblivious-tree能够简单拟合方案,快速生成模型,这种树结构起到了正则化的作用,因此并不容易过拟合。
- 梯度偏差(Gradient bias)
- XGBoost和LightGBM中的提升树算法都是有偏梯度估计,在梯度估计中使用的数据与目前建立的模型所使用的数据是相同的,这样会导致数据发生泄漏,从而产生过拟合。
- CatBoost改进了提升树算法,将原来的有偏梯度估计转换为了无偏梯度估计。具体做法是利用所有训练集(除第i条)建立模型,然后使用第1条到第i-1条数据来建一个修正树M,累加到原来的模型上。
- 类别特征处理
- XGBoost并不能处理类别特征,因此需要我们根据数据实际情况进行独热编码、count编码和目标编码。
- LightGBM 直接支持类别特征,不需要独热展开。这里使用many-vs-many的切分方式来处理类别特征,并且可以把搜索最佳分割点的时间复杂度控制在线性级别,和原来one-vs-other方式的时间复杂度几乎一致。该算法先按照每个类别对应的标签均值(即avg(y)=Sum(y)/Count(y))进行排序,然后根据排序结果依次枚举最优分割点。和数值型特征的切分方式不同,它是将某一类别当作一类,然后将其余所有类别作为一类。
- CatBoost在处理类别特征方面做了更细致的操作。或许在使用LightGBM时,还需要对类别特征进行更多的编码方式,但对于CatBoost,则可以选择不进行多余的编码方式。具体实现流程是首先对输入的样本集随机排序,然后针对类别特征中的某个取值,在将每个样本的该特征转换为数值型时,都基于排在该样本之前的类别标签取均值。对所有的类别特征值结果都进行如式(5-10)所示的运算,使之转化为数值结果,
- 参数对比
神经网络
随着拥有数据量的增加,神经网络战胜传统机器学习模型的可能性也会加大。
- 多层感知机:含有多个隐藏层的神经网络
- 卷积神经网络 :广泛应用于计算机视觉领域
- 循环神经网络:更擅长对序列数据进行建模处理
实战案例(未实际运行)
#接第5章实战案例代码,构造训练集和测试集
x_train = data[:ntrain][all_cols]
x_test = data[ntrain:][all_cols]
#对售价进行log处理
y_train = np.log1p(data[data.SalePrice.notnull()]['SalePrice'].values)
XGBoost:使用比较常规的五折交叉验证
import xgboost as xgb
from sklearn.model_selection import KFold
kf = KFold(n_splits=5,shuffle=True,random_state=2020)
for i,(train_index,valid_index)in enumerate(kf.split(x_train,y_train)):
trn_x,trn_y,val_x,val_y = x_train.iloc[train_index],y_train[train_index],x_train.iloc[valid_index],y_train[valid_index]
params ={'eta':0.01,'max_depth':11,'objective':'reg:linear','eval_metric':'mae'}
dtrain = xgb.DMatrix(data=trn_x,label=trn_y)
dtest = xgb.DMatrix(data=val_x,label=val_y)
watchlist =[(dtrain,'train'),(dtest,'valid_data')]
model=xgb.train(params,dtrain,num_boost_round=20000,evals=watchlist,early_stopping_rounds=200,verbose_eval=500)
多层感知机:要确保数据中没有缺失值,并且要对数据进行归一化处理。
from sklearn. model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
x_train = x_train. fillna(0)
x_train = StandardScaler(). fit_transform(x_train)
trn_x, val_x, trny, val_y = train_test_split(x_train, y_train, random_state=2020)
def create_mlp(shape):
x_input = Input((shape, ))
X = Dropout(0.2)(BatchNormalization()(
Dense(256, activation=' relu')(X_input)))
X = Dropout(0.2)(BatchNormalization()(Dense(128, activation=' relu')(X)))
X = Dropout(0.2)(BatchNormalization()(Dense(64, activation=' relu')(X)))
X = Dense(1)(X)
model = Model(inputs=X_input, outputs=X)
model. compile(optimizer=' adam', loss=' mse', metrics=[' mae'])
return modelmlp_model = create_mlp(trn_x. shape[1])
mlp_model.fit(x=trn_x, y=trn_y, validation_data=(val_x, val_y), epochs=30, batch_size=16)
第6章 模型融合
本章将向大家介绍在算法竞赛中提分的关键步骤,这也是最后阶段的惯用方法,即模型融合(或者集成学习),通过结合不同子模型的长处进行模型融合,当然这是在理想状态下。
本章主要分为构建多样性、训练过程融合和训练结果融合三部分。
模型融合常常是竞赛取得胜利的关键,相比之下具有差异性的模型融合往往能给结果带来很大提升。了解的模型融合方法越多,最后取胜的概率就会越高。
本章从这三个部分介绍不同模型融合方法的应用场景,同时给出使用技巧和应用代码。
构建多样性
介绍三种模型融合中构建多样性的方式,分别是特征多样性、样本多样性和模型多样性。其中多样性是指子模型之间存在着差异,可以通过降低子模型融合的同质性来构建多样性,好的多样性有助于模型融合效果的提升。
特征多样性
构建多个有差异的特征集并分别建立模型,可使特征存在于不同的超空间(hyperspace),从而建立的多个模型有不同的泛化误差,最终模型融合时可以起到互补的效果。在竞赛中,队友之间的特征集往往是不一样的,在分数差异不大的情况下,直接进行模型融合基本会获得不错的收益。
另外,像随机森林中的max_features,XGBoost中的colsample_bytree 和LightGBM中的feature_fraction都是用来对训练集中的特征进行采样的,其实本质上就是构建特征的多样性。
样本多样性
样本多样性也是竞赛中常见的一种模型融合方式,这里的多样性主要来自不同的样本集。
具体做法是将数据集切分成多份,然后分别建立模型。我们知道很多树模型在训练的时候会进行采样(sampling),主要目的是防止过拟合,从而提升预测的准确性。
有时候将数据集切分成多份并不是随机进行的,而是根据具体的赛题数据进行切分,需要考虑如何切分可以构建最大限度的数据差异性,并用切分后的数据分别训练模型。
例如,在天池“全球城市计算AI挑战赛”中,竞赛训练集包含从2019年1月1日到1月25日共25天的地铁刷卡数据记录,要求预测1月26日每个地铁站点每十分钟的平均出入客流量(2019年1月26日是周六)。显然,工作日和周末的客流量分布具有很大差异,这时会面临一个问题,若只保留周末的数据进行训练,则会浪费掉很多数据;若一周的数据全部保留,则会对工作日的数据产生一定影响。这时候就可以尝试构建两组有差异性的样本分别训练模型,即整体数据保留为一组,周末数据为一组。当然,模型融合后的分数会有很大提升。
模型多样性
不同模型对数据的表达能力是不同的,比如FM能够学习到特征之间的交叉信息,并且记忆性较强;树模型可以很好地处理连续特征和离散特征(如LightGBM 和CatBoost),并且对异常值也具有很好的健壮性。把这两类在数据假设、表征能力方面有差异的模型融合起来肯定会达到一定的效果。
对于竞赛而言,传统的树模型(XGBoost,LightGBM、CatBoost)和神经网络都需要尝试一遍,然后将尝试过的模型作为具有差异性的模型融合在一起。
还有很多其他构建多样性的方法,比如训练目标多样性、参数多样性和损失函数选择的多样性等,这些都能产生非常好的效果。
训练过程融合
模型融合的方式有两种,第一种是训练过程融合,比如我们了解到的随机森林和XGBoost,基于这两种模型在训练中构造多个决策树进行融合,这里的多个决策树可以看作多个弱学习器。其中随机森林通过Bagging的方式进行融合,XGBoost通过Boosting的方式进行融合。
Bagging
Bagging的思想很简单,即从训练集中有放回地取出数据(Bootstrapping),这些数据构成样本集,这也保证了训练集的规模不变,然后用样本集训练弱分类器。重复上述过程多次,取平均值或者采用投票机制得到模型融合的最终结果。
当在不同的样本集上训练模型时,Bagging通过减小误差之间的差来减少分类器的方差,因此Bagging可以降低过拟合的风险。Bagging算法的效率在于训练数据的不同,各模型之间存在着很大的差异,并且在加权融合的过程中可以使训练数据的错误相互抵消。
Boosting
Boosting的思想其实并不难理解,首先训练一个弱分类器,并把这个弱分类器分错类的样本记录下来,同时给予这个弱分类器一定的权重;然后建立一个新的弱分类器,新的弱分类器基于前面记录的错误样本进行训练,同样,我们也给予这个分类器一个权重。重复上面的过程,直到弱分类器的性能达到某一指标,例如当再建立的新弱分类器并不会使准确率显著提升时,就停止选代。最后,把这些弱分类器各自乘上相应的权重并全部加起来,就得到了最后的强分类器。其实,基于Boosting的算法是比较多的,有Adaboost、LightGBM、XGBoost和CatBoost等。
训练结果融合
模型融合的第二种方式是训练结果融合,主要分为加权法、Stacking和Blending,这些方法都可以有效地提高模型的整体预测能力,在竞赛中也是参赛者必须要掌握的方法。
加权法
加权法对于一系列任务(比如分类和回归)和评价指标(如AUC,MSE 或 Logloss)都是很有效的,比如我们有10个算法模型并都预测到了结果,直接对这10个结果取平均值或者给予每个算法不同的权重,即得到了融合结果。加权法通常还能减少过拟合,因为每个模型的结果可能存在一定的噪声,加权法能够平滑噪声,提高模型的泛化性。
-
分类问题:对于分类问题,需要注意不同分类器的输出结果范围一致,因为输出的预测结果可以是0/1值,也可以是介于0和1之间的概率。另外,投票法(Voting)也是一种特殊的加权法。
-
回归问题:对于回归问题,使用加权法会非常简单。这里主要介绍算术平均和几何平均。
- 在2019腾讯广告算法大赛中,选择几何平均的效果远远好于选择算术平均,这是由于评分规则是平均绝对百分比误差(SMAPE),此时如果选择算术平均则会使模型融合的结果偏大,这不符合平均绝对百分比误差的直觉,越小的值对评分影响越大,算术平均会导致出现更大的误差,所以选择几何平均,能够使结果偏向小值。
算术平均:基于算术平均数的集成方法在算法中是用得最多的,因为它不仅简单,而且基本每次使用该算法都有较大概率能获得很好的效果。
几何平均:根据很多参赛选手的分享,基于几何平均数的加权法在算法中使用得还不是很多,但在实际情况中,有时候基于几何平均数的模型融合效果要稍好于基于算术平均数的效果。
- 排序问题
一般推荐问题中的主要任务是对推荐结果进行排序,常见的评价指标有mAP(mean Average Precision),NDCG(Normalized Discounted Cumulative Gain),MRR(Mean Reciprocal Rank)和AUC,这里主要介绍MRR和AUC。
MRR:给定推荐结果q,如果q在推荐序列中的位置是r,那么MRR(q)就是1/r。可以看出,如果向用户推荐的产品在推荐序列中命中,那么命中的位置越靠前,得分也就越高。显然,排序结果在前在后的重要性是不一样的,因此我们不仅要进行加权融合,还需要让结果偏向小值。这时候就要对结果进行转换,然后再用加权法进行融合,一般而言使用的转换方式是log变换。
其基本思路如下:首先,输人三个预测结果文件,每个预测结果文件都包含M条记录,每条记录各对应N个预测结果,最终输出三个预测结果文件的整合结果。统计三个预测结果文件中记录的所有推荐商品(共N个商品)出现的位置,例如商品A,在第一份文件中的推荐位置是1,在第二个文件的推荐位置是3,在第三个文件中未出现,此时我们计算商品A的得分为log1+log3+log(N+1),此处我们用N+1来表示未出现,即在N个推荐商品中是找不到商品A的,所以只能是N+1。对每条记录中的商品按计算得分由小到大排序,取前N个作为这条记录的最终推荐结果。
AUC:作为排序指标,一般使用排序均值的融合思路,使用相对顺序来代替原先的概率值。很多以AUC为指标的比赛均取得了非常不错的成绩。使用过程如下:对每个分类器中分类的概率进行排序,然后用每个样本排序之后得到的排名值(rank)作为新的结果。对每个分类器的排名值求算术平均值作为最终结果。
Stacking 融合
使用加权法进行融合虽然简单,但需要人工来确定权重,因此可以考虑更加智能的方式,通过新的模型来学习每个分类器的权重。这里我们假设有两层分类器,如果在第一层中某个特定的基分类器错误地学习了特征空间的某个区域,则这种错误的学习行为可能会被第二层分类器检测到,这与其他分类器的学习行为一样,可以纠正不恰当的训练。上述过程便是Stacking融合的基本思想。
这里需要注意两点:第一,构建的新模型一般是简单模型,比如逻辑回归这样的线性模型;第二,使用多个模型进行Stacking融合会有比较好的结果。
Stacking融合使用基模型的预测结果作为第二层模型的输入。然而,我们不能简单地使用完整的训练集数据来训练基模型,这会产生基分类器在预测时就已经“看到”测试集的风险,因此在提供预测结果时出现过度拟合问题。所以我们应该使用Out-of-Fold的方式进行预测,也就是通过K折交叉验证的方式来预测结果。这里我们将Stacking融合分为训练阶段和测试阶段两部分,将并以流程图的形式展示每部分的具体操作。如图6.2所示为训练阶段。
特征加权的线性堆叠,可参考相应论文“Feature-Weighted Linear Stacking two layer stacking",其实就是对传统的Stacking融合方法在深度上进行扩展。通过传统的Stacking融合方法得到概率值,再将此值与基础特征集进行拼接,重新组成新的特征集,进行新一轮训练。
Blending 融合
不同于Stacking融合使用K折交叉验证方式得到预测结果,Blending融合是建立一个Holdout集,将不相交的数据集用于不同层的训练,这样可以在很大程度上降低过拟合的风险。
假设构造两层Blending,训练集等分为两部分(train_one和train_two),测试集为test。第一层用train_one训练多个模型,将train_two和test的预测结果合并到原始特征集合中,作为第二层的特征集。第二层用train_two的特征集和标签训练新的模型,然后对test预测得到最终的融合结果。
实战案例
以stacking为例。选择ExtraTreesRegressor、RandomForestRegressor、Ridge、Lasso作为基学习器,Ridge为最终分类器。
依然采用5折交叉验证
kf = KFold(n_splits=5, shuffle=True, random_state=2020)
然后构建一个sklearn中模型的功能类,初始化参数然后训练和预测。这段代码可复用性很高,建议完善、储存。
class SklearnWrapper(object):
def __init__(self, clf, seed=0, params=None):
params['random_state'] = seed
self.clf = clf(**params)
def train(self, x_train, y_train):
self.clf.fit(x_train, y_train)
def predict(self, x):
return self.clf.predict(x)
封装交叉验证函数。可复用性也很高。
def get_oof(clf):
oof_train = np.zeros((x_train.shape[0],))
oof_test = np.zeros((x_test.shape[0],))
oof_test_skf = np.empty((5, x_test.shape[0]))
for i, (train_index, valid_index) in enumerate(kf.split(x_train, y_train)):
trn_x, trn_y, val_x, val_y = x_train.iloc[train_index], y_train[train_index],\
x_train.iloc[valid_index], y_train[valid_index]
clf.train(trn_x, trn_y)
oof_train[valid_index] = clf.predict(val_x)
oof_test_skf[i, :] = clf.predict(x_test)
oof_test[:] = oof_test_skf.mean(axis=0)
return oof_train.reshape(-1, 1), oof_test.reshape(-1, 1)
预测四个模型的验证集结果和测试集结果。并辅助最后一步的stacking融合操作:
et_params = {
'n_estimators': 100,
'max_features': 0.5,
'max_depth': 12,
'min_samples_leaf': 2,
}
rf_params = {
'n_estimators': 100,
'max_features': 0.2,
'max_depth': 12,
'min_samples_leaf': 2,
}
rd_params={'alpha': 10}
ls_params={ 'alpha': 0.005}
et = SklearnWrapper(clf=ExtraTreesRegressor, seed=2020, params=et_params)
rf = SklearnWrapper(clf=RandomForestRegressor, seed=2020, params=rf_params)
rd = SklearnWrapper(clf=Ridge, seed=2020, params=rd_params)
ls = SklearnWrapper(clf=Lasso, seed=2020, params=ls_params)
et_oof_train, et_oof_test = get_oof(et)
rf_oof_train, rf_oof_test = get_oof(rf)
rd_oof_train, rd_oof_test = get_oof(rd)
ls_oof_train, ls_oof_test = get_oof(ls)
最后就是stacking部分,使用ridge模型。
def stack_model(oof_1, oof_2, oof_3, oof_4, predictions_1, predictions_2, predictions_3, predictions_4, y):
train_stack = np.hstack([oof_1, oof_2, oof_3, oof_4])
test_stack = np.hstack([predictions_1, predictions_2, predictions_3, predictions_4])
oof = np.zeros((train_stack.shape[0],))
predictions = np.zeros((test_stack.shape[0],))
scores = []
for fold_, (trn_idx, val_idx) in enumerate(kf.split(train_stack, y)):
trn_data, trn_y = train_stack[trn_idx], y[trn_idx]
val_data, val_y = train_stack[val_idx], y[val_idx]
clf = Ridge(random_state=2020)
clf.fit(trn_data, trn_y)
oof[val_idx] = clf.predict(val_data)
predictions += clf.predict(test_stack) / 5
score_single = sqrt(mean_squared_error(val_y, oof[val_idx]))
scores.append(score_single)
print(f'{fold_+1}/{5}', score_single)
print('mean: ',np.mean(scores))
return oof, predictions
oof_stack , predictions_stack = stack_model(et_oof_train, rf_oof_train, rd_oof_train, ls_oof_train, et_oof_test, rf_oof_test, rd_oof_test,ls_oof_test, y_train)
实际运行后发现,基分类器的分类效果差别很大,且最终融合后的模型效果确实要比基分类器的模型效果好很多。