昨年末に開催されたASHRAEコンペで銅メダルを獲得できました。

今更ですが復習として取り組み内容の記録になります。
どんなコンペ?
ビルでの省エネの施策を実施した場合の効果を見積もるために、比較対象としての施策をしない場合のエネルギー使用量を予測するコンペでした。
評価指標:RMSLE
Train: 2016年(20,216,100レコード)
Test:2017年-2018年(41,697,600レコード)
特徴量:4種メータ(電気、蒸気、冷水、温水)、天気、位置、ビルIDなど
ターゲット:1時間毎のメーター使用量
データサイズが大きいものの、特徴量が少なく問題設定もシンプルで比較的とっつきやすいコンペであったと思います。
しかし途中から大学?かどこかの機関が公表しているビルのエネルギー量と今回の一部のビルのエネルギー量が同じであることが発見されてしまい(リーク)、結構な人が離脱されていました‥。
勉強になった解法など
上位陣の解法を読む限り差が出たポイントは下記であると感じました。
・外れ値の除去
・Postprocessing
・モデルの多様性
上記ポイントと個人的に気になった解法に関してまとめていきます。
外れ値の除去
メーター使用量をプロットすると下記のように全く使用されていない時期があったり、特定の期間に異常に高い値を示すビルがあったりしたため、モデルに入れる前の前処理を行うことでスコアを改善することができていました。
全部のビル(1449種)×メーター(4種)の値を泥臭く1個1個可視化して前処理しているチームが多そうでしたが、ある程度自動的に値を発見する方法も紹介されていました。
また、単純に値が0だったら削除するというわけではなく、同じ場所IDにあるビルで同時刻に発生している0のみを削除するなどチームによって違う処理が行われていたようです。
と思っていましたが、親切にも一目でわかるような可視化方法がNoteBookでシェアされています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Load data train = pd.read_csv('/kaggle/input/ashrae-energy-prediction/train.csv') train['timestamp'] = pd.to_datetime(train.timestamp) train = train.set_index(['timestamp']) # Plot missing values per building/meter f,a=plt.subplots(1,4,figsize=(20,30)) for meter in np.arange(4): df = train[train.meter==meter].copy().reset_index() df['timestamp'] = pd.to_timedelta(df.timestamp).dt.total_seconds() / 3600 df['timestamp'] = df.timestamp.astype(int) df.timestamp -= df.timestamp.min() missmap = np.empty((1449, df.timestamp.max()+1)) missmap.fill(np.nan) for l in df.values: if l[2]!=meter:continue missmap[int(l[1]), int(l[0])] = 0 if l[3]==0 else 1 a[meter].set_title(f'meter {meter:d}') sns.heatmap(missmap, cmap='Paired', ax=a[meter], cbar=False) |
横軸が日付、縦軸がビルのIDのヒートマップがメーター毎に4列あります。水色がターゲットの値が0、白が欠損値、茶色がそれ以外の値となります。
実務でも異なるジャンルに対する目的変数の分布を確認する必要性があったりするので勉強になります…。
他にも電力のメーターを表すmeter=0はコンセントを挿しているだけで消費するからあり得ないとして?全削除しているチームもあり、ここら辺の処理も何がベストかというのは無くデータ依存で色々やる必要性がありそうです。
Postprocessing
上記の前処理を行うことで予測値の平均が上がってしまうため1未満の数字(0.8~0.99)を掛けて予測値の分布を小さくするアプローチも有効であったようです。
と思いましたが、過去の地震コンペでも同様の手法が使われていたようで単なるリサーチ不足でした…。
2位のチームは掛ける係数の値を実験的に決めていました。
Disucussionからは学習とテスト期間を読み取れませんでしたが、前半を学習データ、後半をテストデータと想定している?みたいで、最初に下記のように外れ値がないデータを取り出して予測を行うとCV:0.15, LB:0.11となります。ここからPostprocessingを行うとスコアが悪くなることを確認したようです。

続いて学習データと2月、テストデータの11月のメーターを0として予測を行うとCV:1.16, LB:1.255という結果になり、ここからPostprocessing(*0.94)を行うとLBが1.23に改善できたようです。

最後に学習データの外れ値を取り除くとCV:0.17, LB:1.19になり、Postprocessingを行うとLB:1.14になることを確認しています。(下の画像だとテストデータに外れ値が無いですが、LBのスコアから考えると外れ値があるデータに対しての予測のようです)

つまり、学習データに外れ値が無い場合はpostprocessingを行うとスコアが悪くなるので行わず、外れ値がある場合は除去した上でpostprocessingを行うことでスコアが改善できるということが言えそうです。
学習データに外れ値があり、テストデータにない場合はどうなるか?ということが疑問なので要確認です。(実験してもスコアが上記のようにならなかったので検証中です…)
モデルの多様性
これは他のコンペでも同様ですが、特に今回のコンペでは特徴量設計で工夫する余地が大きくなく、モデルの多様性を上げて分散を小さくすることが重要であったように思います。
モデルはXGBoost/LightGBM/CatBoost/NeuralNetworkを使用するのは勿論ですが、その他にもカテゴリー(メーター、ビル、場所、及びこれらの組み合わせ)毎にモデルを作ったり、Validationの期間をずらしたデータを用いることで多様性を上げていました。

ここら辺は単調なため個人的にはモチベーションがわきにくいところではありますが、やればほぼ確実にスコアが伸びるところなので高速で行えるようにパイプラインを作成する必要を感じます…。
その他 – Entity embedding
Notebookでカテゴリー変数を多次元化しニューラルネットで学習させるものが紹介されていました。以前のコンペでも使用されていたようで論文もありました。
これはカテゴリー変数を多次元に拡張してニューラルネットワークで学習させることで、カテゴリー間の関係性をより捉えるというもので、学習後に得られる重みを取り出して他の機械学習のアルゴリズムの特徴量として使用することもできます。
今回ビルの種類が1449と非常に多く、普通にラベルエンコーディングを使って決定木のモデルで学習を行うと分岐回数が増えて木が複雑になることが考えられるため、多次元化して似たようなビルをモデルに教えてあげることができれば効きそうな気がします。
ここら辺はデータ依存の部分もあると思いますが汎用性がありそうな技術であるため、また別の記事で検証してみます。
個人的な反省点
銅メダルですが一応メダルが取れた理由を振り返ると
・データのノイズを除去することで精度が上がる事に気付けた
・手元の実験から異なるBuilding id、month、site idの時に汎化性能が最も落ちやすいものを評価した上でvalidationを工夫できた
・ローカルのマシンを購入したので重たい処理もメモリをそこまで気にせず取り組めた事
・運が良かった
などがあります。振り返ってみると大した優位差があったわけではないことがわかります・・。今回のコンペだと正直は運要素が大きかったように思います。
次に上位になれなかった理由ですが、
・データクレンジングを全てのビルのターゲットを確認して修正していなかったこと
・Jupyter Notebookのみ使用していて実験効率(例:寝る前に計算回す、ログを取って思考する)が低かったこと
・リークの利用の仕方が甘かったこと(リークしていないsite idに絞ってモデルを作成する発想の検証を怠ったこと)
・モデル選択を後回しにして結果的にLightGBMしか使用せずアンサンブルして予測の分散を下げられなかったこと
・類似している過去コンペ(地震コンペ、電線コンペ)から異常値を含むデータに対する処理(postprocess)を学べていなかったこと
などがあります。
振り返ると基本的なことができていないので、これではゴールドメダルは厳しいなということがわかります…。
とは言え学びも多いコンペだったので次に活かしたいと思います。