簡介
7月1日,Kaggle 舉辦的M5沃爾瑪銷量時間序列競賽剛剛結果。6月一整月,我的精力主要都投入到了這個比賽中。Kaggle基於同一個數據集舉辦了兩場競賽,其中Accrucy是點估計,Uncertainty則是對分位數的估計。這兩場比賽從3月3日開始,但我從6月才開始參加,相當於在最終一個月的時間完成了這場比賽。
這是我的Feature賽首戰,很幸運兩場比賽都進入了Top2%的銀牌區:
剛看到這個成績時我不敢相信自己的眼睛。尤其是看到A/B榜排名變化的時候。事實上很多獲勝者都經歷了非常大的shake up。這和數據集的特性有關。下面進行一個簡單的總結吧。
預備
這是我Feature賽的首戰經歷。在參加之前遇到了許多意想不到的困難。但是無論如何,這些困難都是可以被技巧克服的。但tricks不能代表一切。在參加比賽前最好確保一定的知識儲備。
首先你需要具備機器學習的基礎知識。此外之前我拿Titanic和House Price Prediction這兩個比賽練過手。此外需要熟悉numpy和pandas操作數據的方法。Feature比賽幾乎都是多個數據表,因此需要對多表聯結(concat, merge, join)、數據重塑(melt, dcast)和分組(groupby)等知識進行反復練習。關於Boosting方法的原理一定要熟悉,主要是各個參數的涵義等等。后期調參的時候肯定會用得到。
遇到的問題
內存
內存容量是打Featured比賽都會遇到的問題,也是我打這個比賽的最大瓶頸。因為時序數據需要進行數據重塑,原本的數據容量被加大。所以很多算法和tricks其實無法在這個數據上施展拳腳。從內存的角度來說,微軟的LightGBM幾乎是最佳的算法,它的內存消耗和運行時間都經過極大優化。LightGBM還支持分批次訓練,每次訓練只需要用一部分數據,這些都在保證准確率的同時節省了內存和運行時間。
此外,Python中的垃圾回收與和改變字節數也能大大降低內存。比方將64位浮點數轉為32位等等。普通的8G筆記本電腦肯定是不夠的,盡管我在比賽期間加裝了一塊內存條,內存升級到了16G,但在一些時候還是無濟於事。后期我的模型訓練幾乎全是在Kaggle雲Notebook中完成的。不得不說Kaggle Notebook的免費配置還是挺給力的,它不僅能下載輸出文件,還能將其他Notebook的輸出文件直接作為新Notebook的輸入文件來運行。
時間序列相關知識
在此之前我從未做過時序相關的數據項目,這導致我剛加入的時候看不懂Public Kernel的代碼。但這也是我參加這個比賽的主要目的,就是向Kagglers學習。因為算法很大程度上是要滿足“預測未來”的需求。按照Kaggle比賽的套路,在充分理解競賽規則后,首先從Public Kernel中找一個代碼風格好的,進行二次開發。
在辛苦地閱讀每一個函數之后,我發現大部分參賽者在Feature Engineering部分大量使用了lag rolling mean/std的方法。其次創建了很多的時間變量,包括年份、星期幾、季度。
在FE方面,我僅僅提取了是否為weekends這個變量。因為顯然周末逛超市的人更多。后來輸出的feature importance圖中也證實了這個特征對預測有一定效果。
數據集的划分
一般的tabular data是通過隨機划分的辦法划分訓練集、驗證集和測試集。但對於時序數據來說。這樣的做法會導致“時間穿越”問題,“時間穿越”本質上是一種data leakage,會導致嚴重的過擬合問題。因此必須根據時間順序來划分訓練/驗證/測試集。
而M5比賽本身又為數據集的划分增添了一定的復雜性。訓練集為2011-01-29 ~ 2011-04-24共1913天的數據 。而驗證集是 2016-04-25 ~ 2016-05-22(共28天)的數據。A榜基於驗證集計算評價指標,而B榜則基於延后28天,即2016-05-23 ~2016-06-19的預測數據。也就是說,A榜和B榜沒有任何交集。三者的划分如下所示:
在比賽結束前的一個月,Kaggle會放出2016-04-25 ~ 2016-05-22日的數據完整結果,即A榜的答案。這也使得A榜的提交結果對於比賽的參考價值不大。此外Kaggle釋出的full label data的id變量有所區別,而Public Kernel中給出的思路大多是擬合A榜,這使得很多高分Kernel都是過擬合A榜的結果。如果想對這些代碼進行二次開發的話,必須通讀並理解它們,而且改為對B榜的擬合。之前分享了 一個簡單的 baseline 方案,可以作為一開始的框架來用。
在比賽中,我最初采取的是其他選手(@ragnar)分享的GroupKFold
交叉驗證,其中將“年-星期”作為分組。這樣同組的樣本就會盡量在同一折中出現,但我認為這個做法仍然無法避免“時間穿越”問題,但好在他使用了很多rolling的特征,盡可能地避免了這個問題。而且GroupKFold
的好處是它使用了全部的訓練數據,讓模型學習到了更多的信息。而且這個方法在前期也確實取得了很高的分數。
但是,我在比賽后期改用了3折TimeSeriesSplit
的交叉驗證方案。這是因為GroupKFold
運行耗費的時間太久了,每次運行至少3小時起步,比賽結束前幾天我耗不起這個時間,所以采用了更為輕便的TimeSeriesSplit
。
我認為對不同cv方法的比較和驗證是非常重要的,這是比賽的核心工作。其實可用的cv方法還有很多,有時需要大量的實踐積累才能得到有效的結論。其次,使用其他數據來驗證也是必要的。
A/B 榜的問題
這場比賽A、B榜的排名發生了翻天覆地的變化,最終的優勝者幾乎都是shake up好幾百名的選手。很多選手在賽后也感嘆這場比賽像“lottery”。這對於我們的啟示是不要盲目相信A榜,要相信自己的local cv結果。但A榜也不是完全沒有價值。即使絕對排名無法參考,也可以用來檢驗某種方法的好壞。而Uncertainty賽的A榜更有參考價值。因為對分位數的估計是沒有label的,因此只能盲猜方法的好壞。
損失函數 Loss Function
其中\(Y_t\)是特定時間序列在t時刻的真實值。\({{{\hat Y}_t}}\)是預測值。n是訓練樣本的長度(歷史觀察數),h是預測范圍。在本場比賽中,訓練集來自2011-01-29到2016-05-22共1941天的銷量歷史數據。因此n=1941。要預測未來28天的銷量,因此h=28.
對於這個數據量來說,網格搜索法用不了、Stacking用不了。調參只能手動調。在可用算法缺乏的情況下,損失函數(Loss Function)的選擇就成了比賽獲勝的關鍵。由於預測的目標是銷量,可以將其看做正常的連續變量或計數變量。那么Poisson損失函數更符合后者,還有的參賽者使用了tweedie loss、以及自定義的損失函數,比如:
def custom_asymmetric_train(y_pred, y_true):
y_true = y_true.get_label()
residual = (y_true - y_pred).astype('float')
grad = np.where(residual < 0, -2 * residual, -2 * residual * 1.15)
hess = np.where(residual < 0, 2, 2 * 1.15)
return grad, hess
我對以上幾種損失函數進行了時間序列交叉驗證,同時進行手動調參,盡量提升單個模型的得分。
之后我對以上幾種損失函數模型進行了融合,也就是非常簡單的mean-based blending。這一步提升了模型的泛化能力,降低了過擬合。得到了融合后的預測。之后,我嘗試了一些硬編碼處理。通過觀測標簽,發現大量的0銷量,因此我將極小的預測值編碼為0。這時可以根據A榜的分數變化來觀測效果。我另外又嘗試了乘以一個系數,如0.98/1.01等等。發現效果比較好。這種技巧屬於magic multiplier,在不熟悉原理的情況下應當謹慎使用。
總結
需要總結的東西太多了。我常常覺得打Kaggle競賽最重要的是心態。重要的是從這個過程中學到新東西而不是名次。盡量要保持住“即使沒有獎牌也要做下去”熱情。
Kaggle比賽的時間線問題也很重要。一場Kaggle比賽動輒持續數個月,從頭一直關注到尾很容易感到筋疲力盡。從比賽的后半段參加是一個比較偷懶的選擇。但我的教訓是不要像我一樣將模型融合拖到提交的最后一天才做。
我在這場比賽中非常感謝的一位選手是@kyakovlev。他分享的Notebook對我產生了很大的啟發。他分享的M5 - Witch Time(https://www.kaggle.com/kyakovlev/m5-witch-time)Notebook 可能是這場比賽最有趣的彩蛋。
他對一個base submission表做了一系列魔法騷操作,對表乘以了一系列系數(選取這些數字的理由非常神奇),最后在A榜上取得了超高分數。相當於以自己做了一個反面例子,展示過擬合的過程。同時也無情拆穿了很多的copy-paster。因為確實存在很多的無腦參與者直接下載Public Kernel的結果提交。甚至還有到處刷"Great Notebook!"這樣的無營養評論的。@kyakovlev的這個文章是對他們的最佳諷刺。
無論如何,盡管有很多Kaggler的無私分享讓你省去了前期數據清理、探索性數據分析的時間。但深入理解問題的核心是最重要的,不然也無法對現有的Kernel做出改動。一定要仔細測試那些高分Public Kernel是不是有價值的,並且基於自己的判斷搭建Pipeline和Local CV。
至於Uncertainty比賽就可以把Accuracy賽的預測結果拿來直接用了。我的做法是將我的預測文件通過Public Kernel分享的方法直接轉化為分位數的估計,再與另一個Public Kernel融合起來降低過擬合。如果做法正確的話兩個比賽的結果應該是比較一致的。
要學習的還有很多,相比於無私分享Kernel的明星選手來說,我僅僅是基於別人的代碼做了非常微小的工作。此外非常感謝國內的一些Kaggle愛好者的分享,如Kaggle競賽寶典、Coggle數據科學等微信公眾號。他們分享的以往時序競賽的Baseline讓我對基本概念有了准確並快速的了解。
Happy Kaggling!
Kaggle Profile:
https://www.kaggle.com/rikdifos/
我的解決方案已上傳github: