2019年1月

初探Kaggle之再探微软恶意软件预测挑战赛

0x01 基础理解

数据方面:我们还能相信CV吗

训练集和测试集的分布存在差异性,所以可能导致过拟合现象,即在训练集上的CV分数比较高,而在预测集上的提升效果不是很明显,可能CV分数提高一个百分点,预测分数都提高不了一个千分点,这也就造成了CV分数和LB分数的鸿沟,应该去做的是在不降低太多CV分数的情况下,缩小CV和LB分数的差距,提高模型的泛化能力。当训练集和测试集的特征有同样的分布时,我们可以相信CV,根据CV分数继续去训练模型,但是当训练集和测试集上的特征分布变化时,CV可能适合做个参考,而应该相信测试集的分数,即LB分数,关注测试集的数据分布和特征分布,并和训练集的分布做对比,选取泛化能力强的特征进行模型训练。

特征工程方面:我们能提高模型的上限吗

特征工程博大精深,花50%的时间在上面都不为过,这可能决定着模型训练的上限,常见的特征编码有标准化、归一化、二值化、One-hot编码、频率编码、统计计算、标签编码等,常见的新特征提取有数值特征的简单数据变换、类别特征之间交叉组合、类别特征和数值特征之间交叉组合、树算法创造新特征等,常见的特征选择方法有filter、Wrapper、Embedded。

无量纲化中的标准化和归一化可以使不同规格的数据转换到同一规格下,如果不转换的化,就不好放在一起比较;
对定量特征二值化,设定一个阈值,大于阈值的赋值为1,小于阈值的赋值为0;
对定性特征One-hot编码,对于类别型特征,一般算法无法对其直接处理,需要向量化类别型特征,将定性数据编码为定量数据,比较常用的处理方式是One-hot编码、频率编码、标签编码。对于无序类别特征,使用One-hot编码虽然会产生大量的稀疏特征,但是不失为一种可用的方式。但是对于有序类别特征就不方便使用One-hot编码了,可能需要自己定义映射映射有序类别特征;
缺失值计算,缺失值可填充为均值;
数据变换,有基于多项式、指数函数、对数函数的数据变换。

模型评价指标:数据挖掘比赛常用logloss、AUC

在调参之前,我们首先要明确模型评价指标,因为拿到一个问题,可能问题的官方评价标准是AUC或是logloss等等,评价标准不一样,我们训练时参数设置也不一样。在一些数据挖掘比赛中,比如Kaggle,logloss和AUC是最常见的模型评价指标,这是因为很多机器学习对分类问题的结果都是概率,如果要计算accuracy,就要把概率转换为类别,这就要设置阈值,阈值的设置很大程度上会影响accuracy的计算,使用AUC或logloss可以避免将概率转换为类别。

从理论上说,模型评价指标涉及到损失函数和性能评价指标。模型训练的目标函数=损失函数(代价函数)+正则项,损失函数是用来估量模型的预测值和真实值的不一致程度,损失函数可以理解为经验风险,而正则项主要用来惩罚,防止模型过于复杂,目标函数可以理解为结构风险。损失函数有log对数损失函数又叫做交叉熵损失函数(逻辑回归)、平方损失函数(最小二乘法)、指数损失函数(Adaboost)、Hinge损失函数(SVM)等损失函数。在lightgbm中metric参数(设置损失函数)中有binary_cross_entropy选项,即二分类交叉熵,就可以套用逻辑回归的损失函数即对数损失函数来计算。

AUC只能用于二分类,指ROC曲线下面区域的面积,ROC曲线的x轴、y轴可以根据二分类的混淆矩阵计算出,AUC反映的是分类器对样本的排序能力。

调参方面:调包侠vs调参怪

调参的目标是偏差和方差的协调。以树系列算法为例,树系列算法主要基于bagging和boosting。基于bagging的随机森林的子模型都有较低的偏差,整体模型的训练过程旨在降低方差,所以需要较少的子模型数(n_estimators默认为10)且子模型不为弱模型(max_depth默认为None)。基于boosting的Gradient Tree Boosting的子模型都有较低的方差,整体模型的训练过程在降低偏差,所以需要较多的子模型数(n_estimators默认为100)且子模型为弱模型(max_depth默认为3)。

树类型算法参数主要可以分为过程影响类和子模型影响类。在子模型不变的情况下,可以改变过程影响类参数,比如n_estimators、learning_rate来改变整体的训练过程,过程影响类参数可以很大影响模型的整体性能,另外我们还可以微调子模型的参数来进一步提高模型的性能。对随机森林来说,增加“子模型数”(n_estimators)可以明显降低整体的方差,而不会对子模型的偏差和方差,还可以调整叶子节点最小样本数、分裂所需最小样本树来细致的调整子树的结构。

模型融合方面:锦上添花or背水一战

模型融合基本上是机器学习训练模型的最后步骤了。如果我们前期把问题理解好,做好特征工程、参数调整,那么最后的模型融合可以让我们的模型性能再上一层楼,但是如果我们前期做的不好时,也不要放弃,模型融合可以让我们翻一翻身。

理论上来说,模型融合可以带来三个方面的好处:首先,从统计的方面来看,由于学习任务的假设空间很大,可能有多个假设在训练集上达到同等性能,此时若使用单学习器可能因误选而导致泛化性能不佳,结合多个学习器可以减小这一风险。第二,从计算的方面来看,学习算法往往会陷入局部极小,有的局部极小点所对应的泛化性能可能很糟糕,而通过多次运行之后进行结合,可以降低陷入糟糕局部极小点的风险。第三,从表示的方面来看,某些学习任务的真实假设可能不在当前学习算法所考虑的假设空间中,此时若使用单学习器肯定无效,而通过结合多个学习器,由于相应的假设空间有所扩大,有可能学得更好的近似。具体的结合策略有平均法、投票法和学习法(典型代表为Stacking)。

0x02 具体分析:微软恶意软件预测

对数据的理解是特征工程的基础,所以数据方面可以归到特征工程类,模型的评价指标不同,调包时设置的参数不同,可以归到调参方面,所以上面说过的五个方面可以归类到三个方面:特征工程、调参、 模型融合。这是大多数数据挖掘比赛的核心,所以我们分别从这三个方面看一下Kagglers是如何处理微软恶意软件预测比赛的。

特征工程

这次比赛的特征工程比较难做,原因可能在于微软提供赛题数据的时候移除了部分时间序列数据(最最原始的数据应该是按照时间划分好的),微软也在赛题中说明了这点,时间序列数据可能很关键。到目前为止总计有793只队伍参加比赛,0.690+的分数只有50只队伍,只有一人达到0.700,而排名在50-150之间的分数0.688-0.690都是靠模型融合才达到的(前二十名的队伍可能单模型就达到0.690+),也就是可能只有少于50人真正做了有一定提升效果的特征工程。

LongYin给出了几点做好特征工程的提示。如果我是一个黑客,我想去攻击windows的机器,那么我会怎么做呢?第一,我会尝试寻找流行软件的漏洞,如果一个软件有很多用户,而我攻击成功,那么收益就会最大化(但是这种软件的防护性大多很好),这就是计数特征非常重要的原因;第二,我会攻击一些防护差的机器,(尤其是一些新的软件,不像老产品经过很长时间的维护,安全性很好一样,新的产品的防护大多很弱,因此攻击者有很多机会攻击新产品,这就是boolean型特征非常重要的原因。LongYin提到了CountryIdentifier, Wdft_RegionIdentifier, Census_OSBranch等特征的计数特征提升了他的模型效果。有人根据此次提示很好的提升了模型的性能,但是也有人尝试了count feature、frequency encoding和mean encoding,效果不是很好,LongYin建议先划分训练集和验证集,再做特征工程,也有其他kaggler说将训练集和测试集分开做count feature效果好。这点我不是很理解,如果说训练集和测试集某一个feature的分布有差异,对训练集、测试集单独做那个特征的count feature和对训练集和测试集一起做那个特征的count feature,我想后者效果应该好于前者啊,因为单独做的话,训练集的count feature只能反映训练集的分布,训练的时候按照count feature训练泛化能力应该弱的不能很好适应测试集了,如果混合训练集测试集做count feature,那么测试集的数据会影响count feature,相当于不知不觉把测试集的一些数据分布特征带入到了模型训练中去,训练出的模型泛化能力应该很强啊。所以总的来说,我觉得混合数据集做特征工程比数据集单独做特征工程得到的模型的泛化能力强。但是我试验了两种方法,效果差异可以忽略不计。

olivier发现了此次比赛中一个很怪异的现象:CV分数和LB分数差距较大,而且就算是相差不大的CV分数,得到的LB分数可能相差很大,让Kagglers不知道该不该相信CV分数。这个现象可能是数据的分布问题造成的,测试集中的很多数据在训练集中没有出现过,比如测试集中包含的AvSigVersion的值很多没有出现在训练集中。针对这一问题,olivier提出了一种解决思路:对抗性验证方法。对抗性验证处理的是,训练集和测试集差异性较大,此时验证集已经不适合评价该模型了,因为验证集来源于训练集,所以需要一个验证集能够代表测试集。基本思想就是,通过一个具有区分力的分类器,选出被分类器分错的训练集,认为这些数据和测试集很像,作为验证集。具体操作:将训练集和测试集分别进行二类别标记,比如训练集类别设为0,测试集类别设为1。然后将这两部分数据进行混合,再分为新的训练集和测试集,使用新的训练集训练一个二分类网络,对新的测试数据进行测试,选择新的测试集里面原来的训练集数据得分进行排序,选出得分最差的所需数量的数据作为我们需要的验证集。为什么这么操作呢,是因为我们认为选出的这些数据和测试集最接近。olivier通过这种方法做实验得出0.97的AUC,所以他猜测训练集和测试集真的不一样,而对抗验证中最大的特征是AvSigVersion和EngineVersion,这正验证了前面说到的测试集中AvSigVersion的值很多没有出现在训练集中,olivier试着把这两个不太友好的特征去除去训练模型,发现效果更差了,所以这两个特征要被保留,但是需要被处理才能减少扰动。有些kagglers开始尝试对抗验证方法构造训练集的子集进行训练,但是效果并不是很好。

调参

调参只能提升模型少许性能,对于此处用到的lightgbm,无非是增加树的数目;先设置稍微大一点的learning_rate,以防止迭代太慢;设置叶子节点数目;设置树的深度;设置特征选择的比例等等。比如

Python版
param = {'num_leaves': 60,
 'min_data_in_leaf': 60, 
 'objective':'binary',
 'max_depth': -1,
 'learning_rate': 0.1,
 "boosting": "gbdt",
 "feature_fraction": 0.8,
 "bagging_freq": 1,
 "bagging_fraction": 0.8 ,
 "bagging_seed": 11,
 "metric": 'auc',
 "lambda_l1": 0.1,
 "random_state": 133,
 "verbosity": -1}

R语言版
params = list(
    boosting_type = 'gbdt', 
    objective = 'binary',
    metric = 'auc', 
    nthread = 4, 
    learning_rate = 0.05, 
    max_depth = 5,
    num_leaves = 40,
    sub_feature = 0.1*i, 
    sub_row = 0.1*i, 
    bagging_freq = 1,
    lambda_l1 = 0.1, 
    lambda_l2 = 0.1
)

因为此次比赛中官方的模型评价标准是AUC,所以大多数人都把metric参数设置成了‘auc’,但是olivier发出了一个提醒:在以AUC为评价标准的比赛中,设置metric参数为‘binary cross entropy’也许比设置为‘auc’更好,因为auc作为一个度量标准时可能不稳定,可能触发early_stopping早停(early_stopping设置较小的情况下),而binary cross entropy更为稳定,模型效果可能会好一些。ps:我试验了两种参数值,发现相差无几。

模型融合

此次比赛中关键点之一就是模型融合,我觉得做模型融合有利有弊,如果前期就做模型融合,虽然能提升不少成绩,但是会本末倒置,一味地在融模型,而没有做真正的特征工程,尽管在做特征工程时可能会一直卡住收益很低,但是每个阶段都有每个阶段该做的事,所以前期需要把重心放在前期对问题的理解和分析上面,到了后期再做模型融合再提升一下模型性能。除非特征工程真的太难了,模型效果一直不好,可以尝试融合一下模型看看效果怎样,或许能得到一些启发。这次恶意软件预测的特征工程就比较难做,所以开始一直卡住,有人提出了几种融合的方式,例如融合了已经公布出来的五六个kernel:python版的lightgbm、R语言版的lightgbm、python版的ftrl,这很大的提升了LB分数,这也许是提升了模型在测试集上的泛化能力,现在大家的分数都挤在0.688-0.690之间一直无法突破。

0x03 具体做法:代码级

我引用了四五个kernel进行模型融合,最好的成绩是LB:0.688,暂时排名121/793。这里贴出R语言版的kernel。

# A quick and dirty lightgbm run. Still need to do parameter tuning and cv

library(data.table)
library(lightgbm)

# read data
dtrain <- fread('../input/train.csv', drop = 'MachineIdentifier')
dtest <- fread('../input/test.csv')

# check size in memory
print(object.size(dtrain), units='Gb')
print(object.size(dtest), units='Gb')

#sample 1/2 of training data. Will optimize later so we can use all of the data
rows = sample(1:nrow(dtrain), size = nrow(dtrain)/2, replace=FALSE)

#set dtrain to only sample rows
dtrain <- dtrain[rows, ]
print(object.size(dtrain), units='Gb')

#assign target variable to y_train
y_train <- dtrain$HasDetections
dtrain[, HasDetections := NULL]

# assign test data ids to id_test
id_test <- dtest$MachineIdentifier
dtest[, MachineIdentifier := NULL]

# save # rows in dtrain and dtest
nrow_dtrain <- nrow(dtrain)
nrow_dtest <- nrow(dtest)

# combine dtrain and dtest for preprocessing
alldata <- rbindlist(list(dtrain, dtest))

# remove dtrain and dtest
rm(dtrain, dtest)
gc()

# get vector of character columns
char_cols <- names(which(sapply(alldata, class) == 'character'))

# convert character columns to integer
alldata[, (char_cols) := lapply(.SD, function(x) as.integer(as.factor(x))), .SDcols = char_cols]

# split back into dtrain and dtest
dtrain <- alldata[1:nrow_dtrain, ]
dtest <- alldata[(nrow_dtrain+1):nrow(alldata)]

rm(alldata); gc();

# Set up processed data for lgbm
x_train <- lgb.Dataset(data = data.matrix(dtrain), label = y_train)
x_test <- data.matrix(dtest)

rm(dtrain, dtest); gc();

for (i in c(5, 6, 7, 8, 9)){
params = list(
boosting_type = 'gbdt', 
objective = 'binary',
metric = 'auc', 
nthread = 4, 
learning_rate = 0.05, 
max_depth = 5,
num_leaves = 40,
sub_feature = 0.1*i, 
sub_row = 0.1*i, 
bagging_freq = 1,
lambda_l1 = 0.1, 
lambda_l2 = 0.1
)

# train model
lgbm_mod <- lgb.train(params = params,
  nrounds = 5120,
  data = x_train, 
  verbose = 1
)

# make predictions
preds <- predict(lgbm_mod, data = x_test)

# set up submission frame
sub <- data.table(
MachineIdentifier = id_test,
HasDetections = preds
)

# write to disk
fwrite(sub, file = sprintf('submission_lgbm_%s.csv', i), row.names = FALSE)
rm(params, lgbm_mod, preds, sub); gc();
}

0x04 想法

目前单模型最好的成绩CV:0.736,LB:0.681,CV-LB=0.736-0.681=0.055,差值有点大,而top20成绩队伍的CV有的还比我低,但是LB都在0.690+,差值在0.040左右,感觉主要原因还是我的模型泛化能力不足。目前做特征工程一直卡住,尝试了一些特征都无法提高单模型的成绩,可能是因为我只分析了训练集而没有太关注训练集和测试集的对比,接下来打算对比分析一下训练集和测试集,不求CV有多高,只求缩小一下CV和LB的差距。用到的代码在我的github

0x05 Ref

https://www.kaggle.com/c/microsoft-malware-prediction/discussion/76252
https://www.zhihu.com/question/28641663/answer/41653367
https://www.zhihu.com/question/52398145
http://www.csuldw.com/2016/03/26/2016-03-26-loss-function/
https://www.zhihu.com/question/39840928
https://www.zhihu.com/question/34470160/answer/114305935
https://www.kaggle.com/c/microsoft-malware-prediction/discussion/75993
https://www.kaggle.com/c/microsoft-malware-prediction/discussion/75223
https://www.kaggle.com/hung96ad/lightgbm