京东赛回顾——比赛历程及赛后思考

Posted by Yuna on August 6, 2018

如期而至-用户购买时间预测:https://jdata.jd.com/html/detail.html?id=2

离京东赛结束其实已经过去了半个多月,自从决赛答辩结束后就顺便在北京浪了几天,然后就继续投入到无休止的实验室开发项目中。但其实静下来回想,这次京东赛给我的冲击和收获真的是挺大的,远超过了之前的两次比赛。其中最大的感受就是“想成为前排大佬真的是要靠实力上分的!”

也许是之前的比赛排名太靠后对比起来没什么感觉,也可能是这次进了决赛对于其他队伍的答辩内容看得更认真的,这次在北京参加答辩真的是感到了一种赤裸裸的实力碾压!老实说看到自己的方案被前排虐惨时心里还是有些欣慰的。之前在打IJCAI时,我还曾经困扰了好久,觉得现在的算法比赛快玩成了RP竞赛,甚至为此还请教了航神,问他“是不是现在的算法竞赛中个人实力只能尽量提高你获得好名次的可能性,而实际取得的名次的好坏很多时候还是要看运气?”。当时航神也明确告诉我:“运气只是一方面,实力还是占了大部分的。”,而现在京东赛算是给我强势证明了这一点。所以感受到这些水平差距时心里很高兴,也很踏实。

比赛历程

言归正传,比赛的背景不想多说,简要说下这次比赛的情况。京东赛没有分初复赛,只有AB榜,历时一个多月。刚开始时跟队友是各自做自己的S1和S2模型,与前两次比赛不同的是,这次比赛几乎是从一开始的S1模型上就碰了壁。比赛群里面公开的baseline分数0.34,而我自己的模型却始终在0.32以下,这还是用了滑窗倍增的,没有滑窗倍增的模型分数只有0.27。就这样折腾了一周多后,我想也许我又碰到了第一次比赛时碰到的“脏特征”,于是就参照着BASELINE的特征一点点排查,最终发现了那个“脏特征”是“距离上次时间间隔”为代表的时间间隔类特征。当时脑袋就有点懵了,对照在jupyter上进行数据分析时画出的各种统计图表,再结合实际业务场景,无论如何都觉得这个特征一定是强特,而且线下的验证结果也显示有明显提升,但线上结果却正好相反。实在想不通,但事实就是如此,于是丢掉这类特征继续改模型,然而两周过去,我的模型依旧没能超过baseline。

两周时间,从一开始的踌躇满志到怀疑人生。队友虽然也有类似的问题,但是他们的思路却很简单:不好的特征就丢掉就好,而且分数也渐渐从baseline提升了上去,而我却像卡入了死循环,弄不懂为什么理论上的强特变成了坏特征,弄不懂为啥线上线下结果不同步,明明就是严格参照着用户的取样方式构建的训练集,数据分布应该是一致的啊?最后终于决定换个问题,把S1交给队友做,自己先做S2。这其实也是考虑到队友们还在忙于做S1,怕到时候S2来不及。

一换到S2问题,情况立马就好转了,而我也发现原先卡着我的坏特征在S2里面能起作用,线上线下结果也比较一致,立马就开始来了精神。排名一路上到160,然后就逐渐感觉到了瓶颈,老实说当时看到队友S1都做到110名了,心里还是有点小郁闷的。然后就迎来了一个大转折,其实说到底就是一个意外(所以老实说这次我们的排名有很大的运气成分)。当时我想着特征做不上去了,那就尝试着堆数据量吧。于是原本是用1个月间隔滑窗的,改成了15天间隔的滑窗。这一改,S2成绩biu~的飞上了40+名,把我们都shock到了。当时就感慨,只不过多了将近一倍数据量,结果居然会相差那么大!于是负责S1的队友也试着把间隔改成了15天,但却只有一点点的提升,几乎没什么效果。后来我又尝试将间隔改成7天,发现结果再次直线下滑(总分就只有0.30),观察预测的结果,发现这次的预测均值集中在三四天左右,而之前的均值却是在七八天的。结果分布上的巨大差异,让我开始怀疑成绩提升到底是不是因为数据量的倍增。

比赛进入最后5天,我用最新的S1和S2再提交了一次(之前我为了测试S2效果,S1部分一直都是维持一样的),S2成绩上到了30名,S1则在100名,总成绩70+。感觉到了S2的巨大潜力,也不甘心就这样止步在50名外,我们开始了找队友历程,期望能找到S1好但S2差的进行优势互补,毕竟从排行榜情况来看,大家的偏科都有点严重。然而终归是时间太赶了,即便是有这么好的S2成绩,也依然鲜少有人还需要组队。主动来找我们的大都是百名之后的,成绩更差我们也看不上。而前排的大都已经找好了队友,也有还没满位的想合作,却得知我们是三个人,只好作罢。随着S1前50名的找完都没有结果后,我们心里也越来越忐忑,但又只好继续一路往后找。直到组队截止前一天才找到一个S1排在70多,S2排在20多的愿意和我们组队。简单聊了一下,新队友也是数据新手,模型构建的思路跟我们差不多,估计是特征比我们要好一些。因为只剩最后一天了,我们也就稍稍交换了下特征,各自提交。我拿新队友的S1结合我的S2提交了结果,发现我的S2比新队友的S2成绩更好。而另一个搭档也尝试着将两个S1模型进行了融合,成绩也有明显的提升,最后成绩在30多名。

随后初赛切换数据集,中间有差不多一天的空闲期。于是在这个空闲期里,我终于有时间闲下来好好思考一些之前比赛出现的各种诡异情况。包括S2的突然飞升,包括让我百思不得其解的S1脏特征。之前在寻求队友时,曾经跟其他队伍小小的交换了一下心得,得知那个S1在排前30的队员,他们并没有做滑窗倍增,不仅是没有做,而且他们只用了2个月的用户数据,也就是8w条训练样本。当时我就震惊了,我想难道特征真的会差这么多?但对方却很隐晦的表示是数据的问题。虽然不理解,但还是将对方的做法告诉了其他的队友,让他们尝试。动作比较快的是那个滑窗倍增的队友,他将3个月用户改为2个月用户后,效果其实没有太多变化。既然没效果,又在忙着找队友,当时也就没细想。但现在又忍不住重新思考,因为另一个队友带来了新的消息,只是用不太多的特征,用2个月用户训练第3个月,线上效果异常的好。而我注意到了这个异常好的模型,“时间间隔”类的特征赫然就在其中。也许是闲下来了脑子思考比较灵活,也许是现成的两个案例就摆在面前。几乎是没费太多的功夫,在午休的时候很快便想通了这其中的关节。原来真的是因为数据构建方式上出了问题,赛题提供的用户本就是二三四月有购买的用户集群,若是再用一二三月去训练四月,二三月没有发生购买的用户就会在四月出现严重的数据穿越。而我也终于明白,明显是强特的时间间隔特征,为何成了糟蹋整个模型结果的罪魁祸首。也理解了为何之前滑窗倍增的队友,将三个月改两个月后没太多效果——还是因为出现了数据穿越。

这个发现让我感到了深深的懊恼,也开始反省自己在比赛中为什么总是出现这样的问题。这个数据穿越并不是一个藏得多深的陷阱,它是光明正大的摆在那里的,稍微对数据理解得细致些都应该能反应过来,说白点前期能被这问题困扰这么久只能说是自己没用脑,太过专注于眼前的模型变化,却没能把视角跳出来从数据更宏观的层面去观察和理解它,这才导致自己再一次钻进了死胡同。是的,类似的情况在之前的比赛早就不止一次出现了。而这一次的后果同样是让我们浪费了太多的机会,A榜都结束了,才意识到,能改变什么?或许自己在这次比赛后真的应该停下来缓一缓,好好思考消化,调整下自己的状态。一直连续着打三个比赛,却总是再三出现类似的状况,这种反复的错误让我真的很不舒服。

随后在对S2的代码进行整理的时候,我又偶然的发现了一个bug,从而解开了为什么改变滑窗的间隔,会导致结果分布巨大差异的疑问。这个bug其实很简单,我在把滑窗从一个月改成15天时,忘了剔除掉最后半个月的那组训练数据。最后半个月的数据只有半个月的标签,而我之前滑窗时觉得越靠近期的数据应该越有有代表性,所以还加了时间序列的特征。于是导致最终预测的结果过分拟合了最后一组数据的标签分布。在剔除掉那组标签不完全的半个月的训练数据后,预测的结果分布重新回到了一个月滑窗时预测峰值在12/13天的情况。于是另外一个疑问也解决了,为什么15天滑窗的结果那么好,而7天滑窗的结果那么差。这几个结果的差别就是数据分布的峰值位置。很显然线上测试集真实标签的均值也是在8号左右,之前对训练数据集真实标签的分布统计时,就发现了S2的标签其实是呈现一个线性的逐渐递减的趋势。而由于我们采用的MSE损失函数,使得预测出来的结果永远都是呈现一个类似正态分布的曲线。说白点,就是结果的数据分布与真实分布不一致。我想到了网上曾经看到有大神直接优化损失函数以便拟合真实规律,却不知道应该从何入手。想到像IJCAI比赛那样手动调整最终的结果分布,也不知道应该怎么处理峰值的数据,只是整体偏移肯定还是拟合不了真实的分布的。最后思考结果就是,反正时间也不多了,既然加半个月的效果勉强不错,那就将错就错吧。只可惜这样做模型的结构就不方便再做改动了,比如B榜线下尝试的stacking也因为会导致分布变化而不敢冒险使用。

整个比赛大致就这么过来了,最终S2还是我的单模型,S1在各种融合方案的折腾后,也升了上去,最后B榜的成绩保住了第10。虽然之后在资料审核阶段,关于代码复现失败的问题抓狂了很久才明白库的版本不一致会影响复现,整数计算后如果出现浮点精度误差也可能会复现失败。然后等待审核结果中又是各种忐忑不安。但最后还是顺利通过了。

北京的决赛之旅

其实这次比赛我很期待能进决赛,更多的不是因为名次,而是想去决赛现场结识些真正的大佬。所以其实决赛能拿多少名我是不太关心的,当然也不能太没有志气,起码保住第8(前十有2个可能是内部员工所以没有决赛资格)还是要的,如果可以的话当然也不妨向第7名挑战下(主要是第7名的奖品我比较有兴趣)。当然等进到决赛群里后我就没有太多这方面的想法了,因为我发现我们几乎是唯一一支全新手的队伍,或者说是一支没太多资历的队伍。其他的要不手握几个大型比赛的top名次,要不就已经在算法领域工作多年。

重新整理了下对我们自家模型的理解的。关于数据穿越,就算不是全部队伍都知道,也肯定大半队伍都是有意识到的,好歹都已经是前十的水平了。不过既然也算是前10的亮点,自然答辩时还是要提的。至于S2结果的数据分布,这是我们的亮点也是不足,我不打算回避我们有些取巧的手段。虽然有些误打误撞,但好歹我们是意识到了,也尽力尝试解决了。同时也很期待其他队伍在这个问题上的解决方案,相信一定会有惊喜。

事实就是确实没让我感到失望,而且光是这个问题就让我感觉到了前排大佬的实力性碾压。其实总的来说,8~10名的差距并不是很大,六七的方案稍胜一筹,然后四五名是个坎,一二三又是个坎,突然觉得京东的决赛排名等级划分实在太合理了。

以S2的结果分布为例:最后两名并没有提到与此相关的问题。然后是第八名的我们提及了结果分布,采取的方案就是前面说的半个月标签的取巧。第七名的方案就更惊艳一些,不仅是自己编写开源了一个特征选择的代码,在模型设计上还叠加了残差模型,以及将结果进行exp1m的指数变换。虽然没有图表对比,他们也没很明确的说明这两个操作对S2的结果分布有多大影响,只是说了提升不少。相比之下第六名的方案就相对没那么有亮点了,数据分布上采用的是人工离散化修正,即每个预测值范围对应一个最终的预测日期,具体的范围划分是试出来的,整体曲线上的变化就是比之前的正态曲线显得更平缓一些更靠前一些。因此最后第七名成功反超第六名。

第五名在特征上采用了Hawkes过程建模应用,来表达用户一段时间内行为的累计效应(这东西我之前听都没听说过)。在S2的结果调整上,采用了log的指数变换及后期人工规则调整。第四名又更进一步,在意识到S2的评价指标与MSE指标的差别后,直接选用了分位数回归,避开了正态分布的坑。第三名是两个实力派选手,特征上比较突出的操作就有用户和商品的词袋模型,原题6个商品类目的重新聚类划分成30个子类。模型框架上S1S2分别定义了多种标签分别训练模型,随后再一起丢入第二层模型训练最终结果,使得S1和S2的结果在两层堆叠中可以相互借鉴。最后在S2结果分布问题上,通过推导证明S2评分函数有一阶导和二阶导后,直接在这基础上自定义了xgb模型的损失函数从而实现S2大幅提升,震惊全场。第二名的方案则过于平淡无奇,最终排名被第三名踢下去也不奇怪。然后第一名的方案又再次惊艳,这是一个把数据分析、特征和模型都做到了极致的队伍,比如对不同类目之间的关系性分析,发现两个目标品类之间近似于替代品,然后又统计分析推断para1是类似于容量规格的参数,从而构造了进一步的时间推算类特征。在S2评价指标的问题上集合各家之长,既采用了log指数变换,又尝试了分位数回归,最后又叠加了残差模型。结果就是评委席也给了他们很高的评价,认为他们的分析是最贴近实际业务的队伍。

后面的晚宴就是各种背后“恩怨情仇”的交流了,比如第一名的队员拉着第二名(也就是原第三名)的成员“我们当时专门去算过发现S2二阶不可导的!所以才没想去改目标函数”,第二名回答“我算的就是可导的啊~,算完还专门找我们的博士再验证了一次的,对吧?”(说完还很哥们的拍拍隔壁队友的肩膀)。还有第一名内部原本其实是两组队伍,从初赛周榜就一直在暗中争夺较劲,互相藏分互相猜测对手的套路,都希望后期一举打击对手。其中一组的特征做得极好,另一组则一直觉得不可能是特征差距然后在拼命堆模型,最后“放下仇恨”决定强强联合后才发现原来真的是特征差距。看着他们一群人的“高水平”嘻哈打闹,只能在一边默默感慨,自己到底还是太菜了。

最后的总结和反思

虽说成绩不错,总算是没白费了这一整个学期的努力。但是也不打算逃避问题所在。比如比赛中接二连三的智商掉线问题,让我想到了以前在做项目开发时的一句经验之谈“项目负责人不应该让自己陷入具体的代码开发之中”。一旦陷进去,可能就会忽视掉一些宏观整体暴露出的问题。而我感觉这一次次的错误似乎就在不断的印证这句话,就好像跟队友们讨论的,我们总是太后知后觉了。队友说是我们经验不足,不过真的只是经验不足吗?那为啥又一次次的在不同的场景犯类似的错?

仔细想想,从数据穿越到S2的数据分布不一致都是不应该到后期才意识到的。甚至包括其他队伍的一些想法,也并不是从来没想过。比如回归问题的残差模型,在第一次比赛结束时,我就有构思过,这次比赛却完全没想起来这么一样idea。再比如para1代表容量,其实比赛早期做数据分析时,我就发现不同类目的para1取值有着很大的差别,当时就猜测过会造成类目之间差别巨大的通用参数,有可能是规格容量。却没有再进一步仔细的深挖验证,也没有想过针对这个猜测构造什么更加有力的操作或特征。整个比赛的过程中,似乎每天花费最多的时间就是在堆特征,筛选特征,关注模型的效果升了多少降了多少,然后就是抓紧时间训练模型,务必不要浪费每天的提交机会。似乎在这样的埋头苦干下,脑子中很多的idea,很多的发现,就这么让它从脑子里溜走了。

我决定让自己停一停,尽管我知道一个top8的成绩其实并不足够。但我觉得应该停一下,一方面是真的需要静下心来好好消化之前的比赛收获,把没有来得及验证的想法再认真理一理。另一方面则是基础知识确实也要稳扎稳打一点。其次就是不愿意让自己继续陷入这种忙碌赶时间的状态之中,希望自己能静下心一步步走,也希望等过两个月或年末继续重回比赛时,能表现得更成熟些,不要再犯错。


决赛录像及答辩PPT分享: https://jdtech.jd.com/#/detail?id=926ea7a201f241a7a521113e3b5e9096
S2代码: https://github.com/YunaQiu/jdata2018-buyDate