这次比赛真的是惊险无比,毕竟差点就进不了复赛了啊!!虽然一直在队友面前风轻云淡的说对排名不在意,可是止步初赛决不是我想要的成绩!最后两周是真心有些心理压力的。
这篇博文主要是把比赛期间的一些尝试和心态成长记录下来,并非是模型实现方案的记录。
比赛开始
先不管过程如何惊险,赛题背景还是要先交代下的。这次做的是广告转化率的预测,就是给你连续7天的用户点击广告的各种维度信息历史记录。然后再给你第8天的相同维度的信息记录,要你判断该记录有多大的可能性会产生购买。即预测的结果是概率,评价指标为logloss。
赛题是很常见的广告预测场景,可以说是网上一抓一大把的经验博客总结。比赛开始之初,lake也拿来了他们师兄去年参加腾讯广告赛的一些资料给我们学习参考,我也搜了不少去年腾讯赛的网上博客,感觉其实特征提取思路上大家都差不多。模型也以xgb跟lgb为主。由于上次比赛我已经尝试了xgb,所以这次开赛直接拿了上次的代码来改。
初始的线下训练思路与以前一致。7天时间,用滑窗划分了最后3天作为线下的3个同步验证集。第一次先用基础特征提交baseline,后面再慢慢加特征。结果不到三天,线上排行榜就出现了3个分水岭:0.081、0.082、0.083。81以下的是前三名,81~82人数也很少,82~83就比较多了,基本是baseline,83以上的更多,也是baseline,而我的成绩就在83以上。
由于这个比赛的特征势必会很多,在如何做特征选择的问题上,网上的博客采取的比较多的方案是wrapper,其实就是丢进模型里面跑下,结果好就留着,结果不好就丢掉。但是考虑到线上线下数据集毕竟分布不完全一样,线下不好的线上可能会好。最终我又找了另一个特征评价指标:sklearn.feature_selection里面的f_classif,它会输出单特征与标签之间的F值相关度以及p值(显著程度),我最开始的筛选方式就是,把相关度高的特征留着丢进模型检验下,相关度太低的去掉。之后很长的一段时间里,我都有采用这个指标进行特征选择的参考,虽然最后发现这种方式是有点问题的。 = =
关于特征提取的思路,主要是按照原始特征,单特征维度,两特征交叉维度的思路提取。单特征就比如提取用户或商品的历史次数,购买次数,转化率之类的。交叉特征就比如用户跟商品交叉,用户跟类别交叉。结合xgb的特点,提取的特征大多都是数值型的统计特征,结合F值和三个模型验证集的结果进行特征取舍。由于上一次比赛用了穿越特征导致成绩被取消,这次比赛一开始坚决没有用测试集当天的数据统计特征,结果就是线上误差一直在0.083以上。
尝试两天无果,我们终于开始怀疑到底能不能用穿越特征,为此还专门找了实习时研究过广告预测的师兄,师兄表示测试集样本时间点以前的数据特征,在正式工程环境下也是提取得到的,用户行为是一直不断被记录,都是已知的。所以他根本不认为使用测试集样本时间点以前的样本特征是作弊行为。于是最后我们决定,样本时间点前的测试集数据也用来提取特征,但不提取时间点之后的未来特征,以防后期模型不合格被取消成绩。这样又加入了一些样本时间之前的行为统计数据,线上误差瞬间降到了0.08168。很明显,大家都用了这些特征。
随后的几天时间大概是成长最快的时间,先是被师兄指出树模型不需要onehot,尽管一直能理解树模型不适合处理太稀疏的特征,但是关于为什么字符型特征不需要onehot的点还是纠结了好久。我一直觉得就算树模型不适合onehot,把不太稀疏的字符型特征onehot处理后再丢进树模型也应该比没有onehot的效果来得好。然而网上的一些博客论证了的结果却恰恰相反,其实至今依旧没有完全搞明白,只是先把onehot特征改了,这个坑留着以后讨论。
另一个学习到的技能点就是贝叶斯平滑,这个在翻看网上的博客经验总结时一直都有被提及,也专门去查了下原理(也就是B分布的原理)。老实说,数学推导过程依旧没太看懂,但至少理解了B分布的应用条件和场景。有这些知识补充,起码不会被乱用。比如后期提取商品与性别的交叉特征的转化率时,就会知道应该对各个性别分别进行贝叶斯平滑,而不是放在一块做贝叶斯平滑。关于贝叶斯的参数求解,网上大多数是采用极大似然法求解,要跑个梯度下降才能求解出参数。为了保证算法运行效率,我最终选择矩估计法求解参数,不过暂时还没有去验证这两种方法求解出的参数差多少。
第一周就这样边提取边筛选特征,当特征加到三四十个时,线上误差也降到了8137的成绩,当时这个成绩排名在前40,算是比较满意的了。随后队友又从kaggle上挖到一篇文章,讲如何计算出线上测试集的标签平均值的,并且耗费了一次提交机会计算出A榜的样本均值是0.0169,这算是很有用的一个参考指标了,具体的原理跟计算方法已经归纳到了另一篇博客中。
尝试FFM模型
会想着尝试FFM模型,一个原因是师兄提到“不要总是用xgb模型,找点适合id特征的比如FM或deep&wide模型。广告预测中id特征很重要”,另一个原因则是考虑到后期的模型融合,如果队友们做的又全都是树模型,差异不够大,提升也有限。
FFM模型目前github上实现的是c++的接口,是直接在命令行里面跑训练的。虽然官方也有python包装的API项目,但却已经弃更好久,也有很多人公开了自己封装的python接口,但也没有一个比较权威的版本。因此保险点,还是选择用python生成训练数据集文件,在python中调用命令行执行,再读取结果文件的方式。当然这些接口也都是自己实现的,毕竟对于我自己来说,封装一些接口并不是什么难事,至少这样又bug也都是自己写出来的,不存在别人封装出bug的风险。FFM的原理其实网上的博客也没有看得特别明白(FM倒是容易理解一点,FFM就有点一知半解),不过既然是多项式拟合的模型,特征的提取上自然以线性特征和稀疏特征为主,当然基本是基于之前xgb的特征改编的。后来在不断尝试中发现,FFM在稀疏特征的拟合上确实要比数值型特征更优秀,就基本是往稀疏化方向去转化了。
FFM模型训练得还算蛮快,尤其是在设置了多线程之后。然而设置了多线程的缺点就是每次跑出来的结果不相同,这个问题随着特征的不断增加越发严重,仔细观察了下训练过程的输出,造成结果差异较大的一个主要原因是迭代次数不同。由于FFM实现的early stopping机制是只要有一次误差提升就立刻结束训练,而不像xgb那样还会有个rounds参数可以指定多少次迭代内没提升才停止训练。这导致了FFM迭代次数的偶然性比较大。于是最后我将训练过程改为了先抽样产生训练集和测试集,迭代固定的次数后,再从训练过程的误差记录中选取最佳迭代次数,再用全部训练数据重新训练一遍。除此之外,为了进一步降低训练的偏差,在正式预测时,是训练了3个正式模型,将三个模型的预测结果平均后作为最终结果的。
与树模型不同,多项式模型是不允许训练数据有空值的,因此在缺失值填充上也着实花费了一番心思,不断的想找相关性强的特征维度作为均值填充,但是收效甚微。也就成功填充了一两个维度的特征。纠结了几天后突然发现,在多项式模型中,只要特征取值为0,就代表该特征不对模型结果产生影响,所以实在填充不了特征,取值为0不失为一种方式。犯蠢的事情还不止这一件:比如之前做特征处理的时候,排名越小的特征产生购买的可能性越大(也就是呈负相关),我就会把该特征的取值进行反转,变成正相关再丢进模型训练。然而事实上,这与模型目标函数中给该特征训练一个负值系数再做个常数偏移并没有什么差别。总之就是做特征处理的时候还是要多动下脑子,别想到一个idea不经推敲就随便乱试。
做FFM模型的期间,线上的提交基本是一天xgb、一天FFM穿插着检验的,调了一星期模型,FFM的最优成绩为8192。后来又开了两个脑洞,一个是将属性列表特征onehot完后做成一个多选的field特征(即一条记录在这个field有多个特征取值为1);另一个脑洞是将带缺失值的数值型特征另设一个“是否确实”的特征,与原特征合为一个field。前一个脑洞做了,但是效果并不好(甚至还发散了),也不知道有没有数学理论上的依据(毕竟FFM的推导还没能理解)。后一个脑洞还来不及实现。
FFM模型调试的后期,训练过程已经变得很慢了(远慢过xgb),不仅慢而且台式机的内存也不够用,最后只好借了队友的服务器来跑。然而不知道是不是服务器的资源限制,虽然服务器的空间充足,训练时间却比台式机还长,基本跑一次程序要去到好几个小时。由于训练时间太长,再加上误差降不下去,所以后来就停更了。
xgb模型的艰难险阻
前面说到那一周同步调试FFM模型跟XGB模型,但实际上那段时间XGB模型无论怎么加减特征,误差都一直在8150附近徘徊,再也没办法突破。自己的心态也开始慢慢变得有些急躁不安。尤其是由于xgb模型的colsample(特征采样)参数设置小于0,导致有时只是无意中改变下特征的顺序,本地误差都可能有不小的波动,给特征的筛选带来困难,心态就更崩了。此时我的特征大概是40维左右,通过比赛群我也知道,模型其实大家没什么差别,但大多数人做的都是上百维的特征,40多维真的是太少了。虽然觉得自己能只用几十维特征就做到别人上百维的效果,也是有点小厉害的,但是结果提升不上去毕竟是事实。另外还有队友的师兄一直在强调的“你们的特征提得太保守了!做这种比赛就是不要想那么多疯狂提特征,随便试,越多越好。特征筛选优化什么的等后期再说”。尽管一直对他们一直说的无脑提特征不太赞同,但也是一种不小的压力。更重要的是,在这段看着排名不断下滑,新特征却一直没能提升成绩的时间里,我越来越厌恶“玄学法”提特征,继而也开始讨厌凭运气获得的比赛名次。或许我是个相信能力多过相信玄学的人,至少我觉得靠玄学取胜在我身上从来没有应验过。
我不知道为什么在师兄们看来,比赛拿个前几名是件轻而易举的事情,即便当时的他们也不具备太多的理论基础,都是边做边学。为什么我从未觉得从几千只队伍中挤入决赛是件很简单的事,与队友们的乐观不同,我觉得能在这种比赛中拿个十几二十名都已经很难得了。我把这种困难的直觉归咎于近年算法比赛人数的大爆发,但有没有前几名在实习面试中的差距依旧摆在那里。或许对自己来说,比赛过程中学到的知识、提升的能力才是最重要的,我开始尝试将比赛的注意力从名次和成绩提升转移到经验和知识的积累。我还有自己的脑洞,还想把上次盐城比赛没来得及实现的idea实现一次,尽管已经没有线上排行榜来检验模型的成果。我还想把这两次比赛中发现和整理归纳出来的编程技巧系统地检验一次,整理成技术博客。还想研究下现实工程项目中的数据倾斜问题应该怎么解决……这些都比单纯的调参提特征来的有意思,所以为什么我要把时间精力都花在“特征玄学”上?就算最后真的提到了强特征,我又能学到什么??
又经过了一周的逛街和课程作业放空下自己,心态终于有所好转,我最后还是弄清楚自己并不是喜欢顺着别人的寻常路子走的人,我还是想走自己的路。或许过段时间或者等暑假之后会把自己留下的坑一一填完,但目前终究是没有时间给我折腾我的那些idea,因为我的排名已经滑出了前100,甚至跌到了200的边缘。无奈之下我终究决定听从一次队友师兄的意见,开始了疯狂无脑提特征。很快特征数量提升到了80多个,随后又提升到了100个。而线上成绩终于有了些起色,误差先是降到了8116+,之后又降到了8100+,只是依旧没能突破8100的门槛。老实说,看到这样的成绩提升我心里并不是高兴,而是有点犯抽的。我相信新提取的这几十维特征里面肯定不是全部有用,甚至应该有一些垃圾特征拉低了实验结果,只是暂时没有精力去一一检验特征的合理性。事实上,我也确实很快就后悔了,因为我发现我根本不知道应该怎么做特征筛选!
特征是肯定要筛的。根据上次比赛的经验,如果引入了一些坏特征,可能会让xgb的效果变差,哪怕就是那么一两个坏特征也足够毁了一个模型。另外一部分原因,也是我们得知的一个小道消息:同校的另一只队伍只用30多维特征,就将线上误差做到了0.080+。与此同时另一个名词也引起了我们的注意,就是滑窗法提特征。虽然之前我也一直采用了滑窗法,但我是基于自己的理解用的,而且我的滑窗法主要目的是解决线下检验的问题,与大多数人所说的滑窗法是一种提取特征的方式并不相同。关于滑窗我们找了很多网上资料,也找了师兄答疑。具体到这次比赛,大家所做的滑窗法似乎就是每天的统计特征都固定用样本前n天的数据统计。而我先前统计特征则是用了样本当天以前的所有数据,这样虽然前面天数的统计数据量比后面的少,但经过贝叶斯平滑后应该会削弱这个影响,相反能更充分的利用了训练数据集。事实上从我在本地做的实验测试结果来看,我采用的统计方式比固定天数的滑窗采样效果要更好。关于滑窗法,以后还会进一步总结,只是现在既然滑窗没问题,那问题就依旧是在特征上。
100多维特征,分批加减一些?本地好像看起来没多大差距。何况一次删一个,还是一次删几个,效果都不是线性的。有可能你删了一部分效果变差,再接着删一部分效果又突然变好了呢?不同于以往一次增删两三个特征,几十维特征可能产生的特征组合实在是太多。唯一庆幸的就是,在后知后觉地懂得把colsample参数改成1,避免特征顺序影响模型结果后,我的本地误差和线上误差基本能保持同步升降,然而依旧有可能有例外。结果就是,接下来几天的特征筛选工作又被我做成了“玄学”。
B榜第一天,模型的成绩变成了300+,将我们狠狠的吓了一跳。其实原因很好理解,B榜数据下载后,连着A榜的数据就可以得到25号当天的完整数据信息,因此一些涉及当天的统计特征的准确率会大大提升,自然结果也会更好一些。只是这个300多名的成绩告诉了我们最后真的很有可能会跌出复赛晋级线的500名,毕竟B榜第一天并非所有队伍都有提交。何况最后两天组队截止后,每天一支队伍就只有一次提交机会。最终讨论后我们当即决定提取未来特征,无论犯不犯规先保住复赛资格再说(何况主办方还没明确说明穿越特征违规呢)。而我则有另一个思路,我打算用递增法将特征从头筛选一次。
有这个想法主要是因为那几天翻技术圈找灵感的时候,发现别人的一篇baseline分析。里面有一些可以借鉴的点,也有一些跟我的分析不一致的点。但有一个特点,别人是认真统计了数据规律,画了曲线图,再决定的特征处理方案。后来我在跟keng交流时,我们都觉得我们在数据分析这块做的确实是欠缺了点。而我也想起了曾经我也是喜欢先分析画图再着手提特征的人,对比现在的“玄学”比赛,画个曲线图看看跟标签的相关程度的方式来筛选特征,要更加实在一些。于是就花了两天的时间从原始特征开始,一个个特征挨着画跟标签的统计曲线图,筛选过了再填充到训练特征集中。这个过程虽然漫长,但是确实让我发现了很多在曲线上并没有相关性的脏特征。更重要的,也被我发现了f_classif指标的滥用。仔细到网上一查,才知道f_classif的F值指的是方差分析的F值,而不是我自以为的混淆矩阵的F值。方差分析适用的是线性相关(或者说单调相关?)的特征,而且是离散后的特征。有了这个认识后,这个评价指标就被我弃用了,他不适用树模型的情况。
由于是从头筛选特征,两天的时间我最终只来得及将单维度特征重新筛了一遍(当然筛选并不只是筛选,还会根据统计规律做些数据清洗或者加一些新特征),交叉维度特征依旧保持原样。但结果依旧是惊人的,本地误差狂降了50,达到8055。尽管提交到线上的成绩仅仅是比原先的误差降低了一丁点,但我依旧相信本地验证出来的结果:这种筛选方法是很有效的。随后又随手加了两三个未来特征,线下狂降200的误差,线上从600名外瞬间飙升至130+名,这下终于可以松口气了。之后就是又把剩余没筛选完的特征继续筛选完,包括未来特征,最终初赛成绩为106名。
过两天就开始复赛了,到时训练数据的量级大幅提升,预测日期也从原来的寻常日期变为特殊日期。总的来说复赛挑战会更大,但确实也是我感兴趣的点。若有机会,我还想在复赛重新开工FFM模型,如果还有机会还可以浅尝下神经网络,尽管自己在这块的知识储备真的很匮乏。
目标成绩…没有目标吧,尽可能拿个好成绩,也不想说一定要进决赛什么的。成绩是顺带,更多的是希望自己能有更多的知识和经验收获。