我的Kaggle初体验 — Grupo Bimbo Inventory Demand

大数据

作者:豆豆叶

这个暑假利用在西班牙交流的时间,我开始着手做自己的第一个Kaggle比赛,总得感觉还是收获特别多,所以也希望和小伙伴分享自己的经验(编程、计算、模型、体验)。这次最终排名在11/1969,获得了一块金牌。

虽说自己是Kaggle新手,但是我自己已经有三年多机器学习经验啦,做过算法也做过应用,对大部分常用模型都有一定的了解。但是关于怎样在Kaggle中获得好成绩,在比赛开始的时候我还什么都不知道,所以前两个星期各种做无用功。

这个比赛的目的是预测 Grupo Bimbo Inventory Demand:Grupo Bimbo公司服务于墨西哥几乎所有的零售商,给他们提供食物和饮料产品;每个零售商每周进货和退货的数据,在Grupo Bimbo的数据库都有记录;这个进货和退货的过程很大程度上都伴随的成本(包括运输成本,损耗成本等);这次比赛提供了整个墨西哥7周(标签为3-11周)的数据,前5周(3-9周)有数据的demand label作为训练集合,后2周(10,11)没有label作为test集;这两周中前一周(第10周)作为public leaderboard,后一周(第11周)作为private leaderboard用来作为最终的排名。

我就用时间顺序来叙述一下整个过程。

Problem Formulation Got Wrong

一开始我觉得这是一个时间序列预测的问题,所以我首先想到的是HMM,给一个(客户,产品)的Tuple,对其历史数据做建模。这样大概做了一周,我发觉意义不大,主要原因是有相当比例的客户每周是会引进新的产品的,并不是每个(客户,产品)都有足够的历史数据来支撑模型的估算。我后来想想,我应该做一些Data Exploration的工作,尤其是在建模之前,不然就会吃很多亏,以为数据是什么样,而实际上并不是这样。

我后来想想应该把它设计成一个监督学习的问题,这样才能最大程度发挥数据的潜能。这样做最为直接的办法是,用3-8周的数据为第9周做特征,得到的模型可以预测第10周(用4-9周的数据做特征),这样做了之后我在leaderboard上的成绩很快就进入了前20%。

Validation Strategy Got Wrong

但是很快我就发现这样做会碰到一个瓶颈:本地Cross Validation的结果明明提升了,但是在Leaderboard上的结果却没有。仔细思考一番就会发现,原因在于我local validation的split (random split)和train/test split (split by time)是不一致的,这样在local validation得到的超参数在train/test split上并不是最优的。想到这个层次后,我就调整了自己的validation strategy,用3-7周为第8周做特征,然后训练,类似的用第9周做validation,得到的超参数在第9周重新训练模型,在第10周也就是public leaderboard做test。这样做了之后我很快就进入10%。

Feature Engineering

接下来做的事情就是增加更多的特征,但是我自己并不是特别有做特征的经验,这个时候怎么办呢?我点开Leaderboard(LB)上排名前20的人,一个人一个人的看他们以前都参加过什么比赛,在哪些比赛中表现的比较好,那些比赛他们用了什么方法。一般Prize Winner都会分享自己的solution,我想他们既然能做好一定是有一些常规经验可以借鉴的。

前面提到由于历史数据有很大比例的缺失(因为客户每周都会引进新的产品)如何对这部分缺失的历史数据做补全就变得挺重要了。LB上有些人在过去的比赛中曾经用过Field-aware Factorization Machine(FFM)赢过一些forecasting性质的比赛,这是一种Factorization Machine的变体,具有比较广泛的实用性。受这些内容的启发,并在了解了其基本原理后,我就用网上找的代码实现了一些基于FFM的特征,于是不出意外我就很快进入前20了。

再然后我感觉我对自己使用XGBoost缺乏经验,里面有很多参数我都不太清楚。我就去网上找有没有相关经验分享的Slides和视频。Kaggle每隔一段时间就会邀请一些全站排名很高的人来做讲座,网上都有他们的视频,这些视频通常都会分享一些调参数和做特征的经验,看了这些资料后我在XGB的调参有了很大的进步,自己的LB排名一度到了第二,截图留念!

大数据

在关于做特征的方向上,我也做了一些探索性的工作。我把Validation集上的Error拿出来做了一些简单的分析。发现产生较大Error的原因主要有两方面,一方面是就是我之前所说的Missing History,这类Error可以通过一些协同过滤的办法有效规避,另一方面是一些客户会不定期的退货,相比进货量,退货量的预测是非常困难的。作为供货方的Grupo Bimbo并没有提供这方面的信息,某种程度来说,我个人觉得这个问题是Unsolvable的。
Validation Strategy Got Wrong Again

之后又做了一些特征,排名也始终在前三。这个时候我突然发现了一个严重的问题,那就是比赛的最终排名是依据第11周的结果而不是第10周,所以我无论怎么优化第10周都不代表我能很高的预测第11周。在传统time series的预测问题中,为了预测第11周,可以先预测第10周然后再假设第10周的数据已知,然后再预测第11周。但是这里并不适用,因为第10周的数据被downsample了。所以我觉得可能有必要直接做一个独立的Two-Week-Ahead Forescasting的模型。

我做了一个简单的假想实验:假设现在第10周是private board,而第9周是public board,也就是说我只能在第8周上train模型,并且做特征的时候也不能用最近的那周的数据,因为对第10周来说,我并没有第9周的完整数据。现在有两个方案:

  1. 还是像之前那样做,在第8周上训练,然后把第10周当作第9周一样做预测
  2. 重新做特征,把特征中关于上一周的信息都抹掉,重新在第8周上训练,然后在第9周上验证,最后在第10周上测试。

这两个方案用来测试第10周的时候都没有用到第9周的数据,实验结果表明前一种方案比后一种要差0.002。这个差别已经在public LB上影响排名了,并且随着更多特征的加入,这个差别还可能会更大。另一方面我注意到它们训练得到的XGB模型也是不太一样的,在前一种方案里,上周的历史特征会比较dominate这个模型,其importance score非常高,而后一种方案里,历史特征被比较均匀的分布在上两周,上三周,上四周等。基于这些观察,我倾向于后一种方案才是正确的validation方案。

这个时候离比赛结束还有不到两个月,也是从这个时候开始,因为调整了validation strategy,我的public LB分就没再增长过,直到比赛的最后一天。

Solution Implementation

由于数据规模比较大(Billion级别,数据量大于通常的Kaggle比赛),用Python笔记本的内存已经吃不动了,我一开始采取的方案是先把所有数据导入到MySQL,做了Index后,从Matlab里每次从数据库retrieve一小撮数据做处理。这样虽然能装下笔记本的内存,但是计算时间实在太高了,每次过一遍所有数据做一个特征要几个小时。后来我完全放弃了MySQL+Matlab的组合,而选择使用C++和Python的组合。

准确来说,我用C++做特征工程,其中用标准库实现一些简单的数据结构(链表等,方便数据的Index),原来需要几个小时的特征工程,现在只需要几分钟了,由于用C++可以精确管理内存,整个过程的内存开销也基本压缩到2G左右。对于内存开销过大的模型,比如FFM,XGB,我则在Server并行着跑,然后把结果写在硬盘上。最终我的整个方案的迭代周期被缩短到一小时以内。

No Progress

自从我调整了Validation Strategy后,我在单个模型的Performance上似乎遇到了瓶颈,主要表现在于新加了特征后,虽然训练集(第8周),验证集(第9周)的结果都提升了,但是测试集(第10周)反而会变差。仔细思考一番后,我觉得有可能是以下原因:

  • 不同周的数据本身就不满足相同分布,过分的最求某几周的Performance会导致其他周的结果变差
  • 模型太过复杂(特征太多),所以出现了Overfitting Backtest
  • 代码Bug

卡了几天后,我就把这个事情放一边了出去欢快的玩耍了,去了马德里和塞维利亚玩了一圈,看看欧洲杯什么的。

Team Up

在我出去玩的这段时间,又有很多新的队伍参加进来,其中包括超过一半Kaggle全站排名前50的竞争者。排名靠前的人发现自己的Progress放缓后也开始纷纷Merge Team,力求提升排名。

在离比赛差不多还有20天的时候,我也觉得应该找人Merge Team,说不定不同的Team会有不同的思路。我在比赛论坛发了帖子,此时我的public LB排名已经掉出前十了,但是我相信可能有一半排名比我高的Team并没有察觉One-week-ahead 和Two-week-ahead的不同,或者察觉了但是不觉得这是一个严重的问题:后天会不会下雨应该和明天会不会下雨差不多概率吧。

最后一个对Kaggle非常有经验的小伙伴(Kaggle全站排名前20)联系了我,当时他排在大约前2%这样。我们Merge后交流了一下彼此工作,我从他那了解到FTRL模型,虽然扮演的角色和FFM差不多,但是原理很不一样,如果和我的模型组合到一起说不定会有更多的提升;他从我这了解了更加正确的Validation Strategy,也调整了他当下调参的策略。

组队后我开始着手ensemble两人的模型,算是有了一些progress。比起我几周前的最好Single Model大约提生了不到0.003,考虑到正确的validation办法还能存在0.002的advantage,我觉得在private leaderboard上有超越在public LB分比我们高0.005的人的可能性(而且事实证明这确实发生了)这样就可能进入前10了。

也是在组队的这段时间,我陆续发现我之前的C++程序有一些不大不小的Bug。。。Orz,这大概解释我为什么我的Progress突然就Stop了,泪。

Final Result

最终的结果我们排在了1969个队伍的第11位,我已经挺满意啦。最终,我们确实超过了很多原本public LB排在我们之前的队伍,但同时也有一些聪明的队伍超过了我们。我相信最终前10的队伍确实都做了足够丰富的特征,并且绝大部分也使用了正确的validation方案。撒花!

大数据

最后我有一些感想:

  1. Kaggle比赛的门槛并不高,但是每个人刚开始参加能获得什么样的成绩很大程度上跟这个人的学习能力和数学编程基础比较有联系。与其说是机器学习的比赛,不如说是对相关基础的一个全面考察的过程。
  2. Kaggle比赛是有经验可循的,但是这些经验都是建立在正确理解问题的基础上的。所以我觉得并不存在绝对的经验,更重要的是学会用科学的方法去验证或否定这些经验。比如在这次比赛中,传统的Cross Validation和Backtest Validation都不是最优方案,独立发现正确的Validation Strategy至关重要。
  3. 如果想要获奖一定要在每个方面做到极致,这也是我觉得跟前三名的差距。我相信从方案上看,我觉得排名靠前的队伍的大致方向都是差不多的,但是在特征工程方面,在模型的验证和Ensemble方面,肯定存在差距。我们的最终方案大概包括不到20个特征,而我看到第4名的队伍有超过100+的特征集合,所以那5个千分点也是Significant!

更多kaggle相关请戳>>>