【新人赛】工业蒸汽量预测建模算法小结

零、前言

之前分别以个人名义参加过两次比赛,一次是阿里天池的糖尿病预测比赛,还有一次是 DataCastle 的游戏氪金用户预测比赛,这两次比赛成绩都是一般般,而且最后由于组内各种杂事最后不了了之。这次抽出时间打算好好的从头到尾调一次比赛,虽然还是自己一个人单干……考虑到自己前两次的失败经验,我决定先从新人赛下手试试,主要目的是明确比赛项目结构和调参整个的流程积累一下经验。最后截止到10月28日晚上时分数是0.1233,排名是2/398。对这个成绩还算比较满意,虽然人肉调参过程中有不少的运气成分在,但是还是学到了不少东西,后面会详细说明。我参加的是第一赛季,截止时间是2019年1月31日,下个月会回来继续尝试优化模型。

一、题目分析

题目背景是火力发电中,目标是将化学能转变为内能,内能表现在蒸汽上,蒸汽量越大,能量转化效率越高,我们的目标就是预测蒸汽量大小。数据维度是38维特征向量,数据规模是训练集有2889条数据,数据量很小。而且拿到数据之后,检查数据情况,首先就是看数据的分布情况。首先特征向量没有缺失值,然后每个维度的分布范围的数量级都差不多,都在正负10内。然后查看各个特征之间的pearson相关系数。

可以看到有些特征之间两两之间存在较强的线性相关,因此在这里考虑我考虑使用PCA降维,但是实际后面做交叉验证的时候发现效果没有特别好的提升。然后还有就是可以考虑用和目标值的卡方分析做特征选择,但是我考虑尝试一下用Lasso回归看看能不能有去掉的特征,但是最后也没有去掉无用特征。

然后直接用所有目标值的中位数和均值作为baseline试了一下误差是多少,都是0.9左右。

二、建模

2.1 线性模型:Lasso and Ridge

首先是用sklearn做StandardScaler,然后分别尝试了Lasso、Ridge和ElasticNet,做超参数搜索,发现正则化系数都是特别小,然后用L1正则也没能很好的进行特征选择,直接用用这个简单的流程提交了一次,第一次是一点多的误差,是倒数几名,然后后来发现忘记使用pipeline导致测试集没有做特征归一化,重新提交了一次,误差是0.6左右。在评论区看到有的大神可以用线性模型做到0.13的损失,很强很强…

最后别忘了看一下测试数据的y值的分布和训练数据的y值的分布是否接近,这是一个很简单的检验方法。

2.2 神经网络

然后尝试了神经网络,由于数据量很小的原因,很定不能使用容量太大的模型,尝试的层数是二到四层,然后层的宽度不超过32,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 使用BatchNormalization试试
def buildModel():
model = Sequential()
model.add(Dense(units=32, activation="relu", input_shape=(38,)))
model.add(BatchNormalization())
model.add(Dropout(0.5))
model.add(Dense(units=28, activation="relu"))
model.add(BatchNormalization())
model.add(Dropout(0.3))
# model.add(Dense(units=8, activation="relu"))
# model.add(Dropout(0.1))
# model.add(BatchNormalization())
model.add(Dense(units=1, kernel_regularizer=l2(0.05)))
model.compile(loss="mse", optimizer=Adam(0.001))
return model

以上代码是尝试的最好的一次网络拓扑,注意尝试过程中一定要从浅层模型开始训练起来,直到足以达到最好的学习曲线,验证误差等于训练误差,然后再增加模型容量直到过拟合,然后再加正则化,这个小经验很简单,但是对于训练这个小模型来说很重要,最后的结果也还不错。然后这个模型做的时候懒了…没有做数据标准化…首先使用简单的浅层模型,不加正则化都达不到线性回归的效果。

在尝试的过程中发现:

  1. weight decay的效果不如dropout,会增加模型的偏差,dropout的影响不是那么明显。L2正则化的效果没有想象的强,但是Dropout出奇的有效,Dropout层的参数是有多少比例的units被Dropout掉。
  2. batchnormalization的效果很不错,虽然会导致训练速度下降。
  3. Dropout和BatchNormalization二者的先后顺序要注意以下,我在网上查到的是Dropout在前,但是我尝试的时候发现现在这样的效果反而更不错。
  4. 激活函数尝试了sigmoid,效果不好。并且有强烈的离散现象,就是最后输出的预测值只有5-6个左右的离散可能性。估计有可能是学习的太慢了。在我设置的epochs中没能收敛。

观察学习曲线,验证集上的效果比测试集上的效果好,这个和我在model1中做实验时出现的现象综合考虑,更加确定有异常点。 此时异常点在训练集中,只是因为正则的效果比较好,使得模型忽略了异常,不过这个点会影响训练导致颠簸。然后考虑排除异常点可能能提升效果,但是我也没做。。。在层数到4层是,怎么调都没有达到岭回归的baseline,加深到四层之后发现都没有能训练到过拟合,于是思路是减小正则,batchsize减小,加大epoch。使用此时的模型进行提交,发现本地的训练和验证的误差都小于0.1,但是在服务端的分数是0.8,这要么是异常点太多了,要么是我在本地发生的过拟合。 在STEP2的基础上我进行以下尝试:

  1. 交换BN层和Dropout层
  2. 加入学习率衰减
  3. 使用Adam Optimizer
  4. 去掉early stopping

发现三个策略都很好使。。。学习率衰减加上更多的epoch,使得学习曲线也变得更好观察,然后adam optimizer训练速度变慢了,但是效果有所提升。去掉early stop是因为在调参的时候将模型训练的过拟合方便观察学习曲线的形状。

总结一下对于学习曲线的诊断:

  1. train loss 不断下降,test loss不断下降,说明网络仍在学习;
  2. train loss 不断下降,test loss趋于不变,说明网络过拟合;
  3. train loss 趋于不变,test loss不断下降,说明数据集100%有问题;
  4. train loss 趋于不变,test loss趋于不变,说明学习遇到瓶颈,需要减小学习率或批量数目;
  5. train loss 不断上升,test loss不断上升,说明网络结构设计不当,训练超参数设置不当,数据集经过清洗等问题。

虽然本地的效果还不错,然是提交上去分数也是不能看,还不如线性模型的0.6,一次是0.8,一次是1,惨不忍睹。然后考虑祭出大杀器:xgboost。

2.3 boosting:xgboost

对于xgb大杀器,网上有很多调参指南,这里给出一篇很不错的英文文章。但是一开始我不是按照流程来的,一开始我是自己写了一个按照顺序自动调参的小函数(与其说是自动调参,不如说是前向分步的GridSearch)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def optimizeXGBR(X, y, param_grid, cv=4):
steps = len(param_grid)
gs_history = []

# 先给一个经验的主要参数
param = dict(learning_rate=0.1, gamma = 0.1, subsample = 0.8, colsample_bytree = 0.8)

for step in range(steps):
gs = GridSearchCV(estimator = XGBRegressor(**param),
param_grid = param_grid[step],
scoring='neg_mean_squared_error',
n_jobs=4, iid=True, cv=cv)
gs.fit(X, y)
gs_history.append(gs)
print(gs.best_params_, gs.best_score_)
param.update(gs.best_params_)

# 更新去掉下一步要被搜索的
if step+1 != steps:
for i in param_grid[step+1].keys():
if i in param:
param.pop(i)
return gs_history, param


param_grid = []
# STEP1:在大量的默认参数下,选一个还不错的基模型个数
param_grid.append({'n_estimators': [1000], 'booster':['dart'], 'learning_rate': [0.1]})
param_grid.append({'rate_drop':[0.05, 0.1, 0.2],
'skip_drop':[0.5, 0.75, 0.9]
})

# STEP2: 在初始值下(主要就是指上面的几个参数),调整主要的树参数
param_grid.append({'max_depth':[4,5,6], 'min_child_weight':[60,65,70,75,80]})

# STEP3:然后调整稍微不那么重要的两个树参数
param_grid.append({'subsample':[0.45,0.5,0.55],
'colsample_bytree':[0.45,0.5,0.55],
'reg_lambda': [0.3,0.4,0.5],
'reg_alpha': [0.7,0.85,1]
})

# STEP4: 正则化
# param_grid.append({'reg_lambda': [0.3,0.4,0.5], 'reg_alpha': [0.7,0.85,1]})


# STEP5:确定树深度与叶权重之后,调整gamma
# gamma:Minimum loss reduction required to make a further partition on a leaf node of the tree.
param_grid.append({ 'gamma':[0.1,0.5,1]})

# STEP6:微调lr
# param_grid.append({'learning_rate': [0.1]})

history, param = optimizeXGBR(X_train, y_train, param_grid)

具体函数如上所示,定义一个每次希望的超参数搜索范围,然后分步的用GridSearch去搜索,每次更新得到的最优参数。得到最优参数后再看学习曲线的状态。然后用这个方法调了几次之后得到一个差不多的范围,然后我又开始了惨绝人寰的手动调参…根据学习曲线的状态微调min_child_weight这个参数,因为这个参数是对正则化比较重要的树参数,然后小幅调整随机抽样率,两个正则化参数调的不多因为发现效果不是特别的好。对于gamma参数,不知道是否应该调,因为xgboost自身是有后剪枝的过程的,如果gamma是一个正数不知道是不是会使得xgboost变成贪心的s树生长策略,但是大部分时候将其设为0效果都不错。然后学习曲线收敛之后尝试增加树的深度。按照这种策略得到的参数得到了最佳的一个单模型成绩:0.1272。

但是人肉调参的步骤还是太累了,然后使用上面链接中的调参步骤好好的走一遍流程。整个过程不用详细说,用这么几点提一下。

  1. xgboost.cv函数加上early_stop能够很方便的得到最佳的基模型个数,然后再调参过程中要不断地校准n_estimators这个参数。
  2. 得到最优参数后缩小learning_rate重新找到最优的n_estimators

得到最优参数后,在xgb.cv中观察得分,发现验证集上的rmse在0.33到0.34附近波动,这个结果比我之前的调参结果要差一些,而且,观察学习曲线,发现妥妥的过拟合状态。但是出于对流程的尊重,我还是直接用这个提交了一次,结果也可以接受,分数是0.13多。这个我也是不太明白为什么gridsearch得到的结果过拟合这么严重,仔细想想我之前的optimizeXGBR那个函数得到的结果也是严重的过拟合。

2.4 Gaussian Process Regression

看到这个方法的原因是,在参数搜索的过程中,我尝试使用了hyperoptBayesianOptimization两个工具来代替GridSearch最后的效果一般(现在想想其实效果应该也可以接受,得到的rmse和用标准流程调参得到的结果差不多,只是我没有提交)。然后对贝叶斯最优化稍微学习了一下原理,这里不展开谈,但是里面是涉及到GPR的,然后学习了一下GPR,发现GPR可以认为是一种线性模型,不过其中的基函数是核函数,其中每个核函数来自于一个样本点,我需要实际调整的参数差不多只有径向基函数的带宽以及正则化系数,调参过程不要太容易,不过训练起来还是太慢了,得到的结果挺一般的,分数是0.1415。

这个模型其实就算一个尝试吧,也算沉积学习一下高斯过程回归,之后有机会填坑的话写一篇高斯过程回归和贝叶斯调参的文章。

另外还尝试了SVRRandomForest这两个的效果都不是特别的好,RandomForest也没有太仔细的调参,在这里不展开说明了。

2.5 ensemble

最后实际差不多了,是时候祭出第二个大杀器了:模型集成。本来想考虑使用stacking工具的,然后也是参考了一篇文章,说对于平均分数差不多的模型,简单的平均就可以提高成绩,并且这种方法是基于结果文件的,做起来特别的简单。然后我使用三次xgboost的结果和一次GPR的结果做了简单的平均,这提交的结果是0.1233。对此我只能说:真香!

三、总结

整个过程也没啥出彩的地方,就是调参过程比较细致外加了一点点的运气吧。为下一场新人赛做准备。

参考:

  1. 稍微深入地介绍贝叶斯优化
  2. 比xgboost强大的LightGBM:调参指南(带贝叶斯优化代码)
  3. 贝叶斯优化 Bayesian Optimization(转载)
  4. 强大而精致的机器学习调参方法:贝叶斯优化
  5. HyperOpt中文文档导读
  6. kaggle比赛集成指南
  7. 分享一波关于做Kaggle比赛,Jdata,天池的经验,看完我这篇就够了。
  8. Complete Guide to Parameter Tuning in XGBoost (with codes in Python)
本站总访问量