昨日終わりました。
コンペ概要
ネットの海外旅行の代理店であるbooking社が主催しているコンペです。(https://www.bookingchallenge.com/)
とはいうものの、ACMという国際学会のワークショップとして行われるのでスポンサー的な立ち位置なのでしょうか。いわゆる学会コンペです。
目的は、顧客の情報から次に予約する行き先を推薦するということ。大雑把にいえば分類タスクでもあるし、推薦タスクでもあります。
PyDataでbooking社の社員がプレゼンをした動画が残っていたのでそれも参考になります。
学会のワークショップというところでわざわざやるタスクということもあって、前例が全然ありません。したがって、実験を回すことよりもモデリングに時間を割いていました。(それも普通に頭使う)
メタ的な所
- カラム: 9なので少ない
- 行き先のクラス数: 4万近く(普通にLGBM回すのは不可)
- 最終的に推薦する行き先の数: 4つ
- 評価指標: top4のaccuracy
- 議論の場所: slack(ただ本質共有はほぼなかった)
solution
僕はNNモデル、チームメイトはLightGBMのモデリングを担当しました。とりあえず今僕がすぐに記事に起こせるのはNNモデルだけなのでLGBMは書けるときに書きます。
前処理
例えば予測するクラス数が4万件ありますが、以下のグラフのようにカバー率でみると、1万件あれば9割以上の行き先をカバーできることがわかります。
このコンペで正解率9割を超えることなんてまず無いので(せいぜい高くて6割)私は行き先を頻度の降順にして上から1万件とってきました。
特徴量に関してもいくつかつくりました。例えば今までの行き先の個数や旅行している日数などです。
モデリング
最終的に単純NN
- Label Encoding済みの行き先リスト
- embedding(NLPと思って)
- denseを4層
- 1万次元に整形
ざっくりこんなところです。
実際はRNNを試していたのですが、GoogleのYouTubeの推薦タスクで単純な層を積んだだけのモデルを使用していて実験してみたらうまくいったのでこうなりました。
Multi Loss
予測する行き先は1万件(本当は4万件あるが減らした)あり、中々正解にたどり着けません。そこで提案したのが、行き先の国(195件)を同時に予測して両方のロスを落としていけば学習がより効率的に進むのではないかという仮説を立ててやってみました。
https://stackoverflow.com/questions/53994625/how-can-i-process-multi-loss-in-pytorch
参考にしたのが上のリンクです。
PyTorchではlossを足しても良いらしく、私は重みを付けてlossを算出しました。
loss = loss_main * 0.8 + loss_sub * 0.2
lossはCross Entropyを使いました。評価指標はtop4のAccuracyですが、うまくいくはずがありませんでした。(一応と思って実験もしたけどやっぱりCEと比較にならないくら酷い)
loss計算時にあえて使わないデータを用意した
何言ってるかわからないと思うのでこれから説明します。
4万件ある行き先を1万件に減らしたところで、データの10%弱はある一定の値に変換されています。しかし、最終的にこの一定の値の予測値を出したとしても3万件の中からランダムで選ばなければならないし、できる限りこの一定の値を出力させたくないというお気持ちがありました。
そこで、targetがその一定の値でないindexを学習させる関数に渡しておいてloss計算ではtargetが一定の値でない(上位1万件に含まれているtarget)データのみを使用しています。
結果的にこのおかげで過学習に近い変な動きはしないようになりました。
モデリングの試行錯誤
とりあえず旅行の行き先を数値化してしまえば任意のNLPのタスク(many to one)としてみることができることはわかっていました。
booking社のプレゼンではどこもRNNを用いていました。自分も最初はRNNで実装しました。特に工夫したところはないです。
その後、チームメイトが以下の論文を紹介してきてモデリングを変更することにしました。
YouTubeの推薦アルゴリズムの変遷を追う〜深層学習から強化学習まで〜
Googleが2016年に公開した推薦アルゴリズムをDeep Learningに置き換えたら精度が上がったよという論文です。動画の数も圧倒的で今回のタスクに近似できるのではということでアイデアをパクりました。
まさにこれです。1つ目の水色の台形部分はレコメンドモデルといって、候補をN個に絞ります。そこから更にランキングモデルという順位付けるモデル(最終的に推薦する物を選別する)を用いています。
しかも、レコメンドモデルのアーキテクチャもシンプルなもので、dense層の積み重ねでした。
自分もレコメンドモデルとランキングモデルを実装しましたが、ランキングモデルのバグが一向にとれなくて結局レコメンドモデルのアーキテクチャを真似してランキングまで取得するようにしました。
この論文では、Top-K推薦問題に適応するアルゴリズムの開発についても触れられています。K個分のアイテムの組み合わせが必要となり、空間爆発がおきるとかでちゃんと頭使わなきゃいけないとか言ってます。
チームメイトにも相談しましたが、勾配の更新について書かれていて結局よくわからんし時間かかりそうだからやめようという結論に至りましたが、やってたらもしかしたら良くなってたかもしれません。(上に貼った記事のリンクの下の方に詳しくあるので気になったらみてください)
没になったmetric learning
めちゃくちゃ頑張ったのに報われなくて凄い悲しかった。metric learning自体はネットで検索すればどこにでもありますが、具体的にはword2vecで行き先をembeddingしてそれをmetric learningに投げてembeddingした空間上で学習してよ〜〜というお気持ちです。
PyTorchではうまくいかず、LightGBMで1次元ごとに予測させるという手法をとって試したものの、全く精度が上がらず結局没になりました。ただ、発想としては悪くなかったのかもしれません。metric learningのプロならきっと使えたと思っています。(何の空間としてみるのか、どう学習させるのか、アーキテクチャはどうするのかといったところが全て未知数だったので)
やっぱり経験が必要なんだなと実感しました。
データセットの工夫
paddingについて
学習データというのは今までの旅行のデータということになるので、少なくて1件多くて40件以上あります。とりあえず20件の旅行データがあれば十分じゃないかということで、多ければ切るし少なければpaddingしようという考えに至りました。
これがdatasetのクラスなのですが、めちゃくちゃなコードになってます。
city_id = pd.DataFrame(
map( # padding
lambda x: (
[0] * (max(self.max_len - len(x), 0)) + x # padding
)[
:self.max_len
],
memo["city_list"].tolist()
)
).values
ちゃんと追えばわかるとは思いますが、必要な分だけpaddingして上から20個だけとってくるっていう仕様になっています。
自作した
PyTorchに入っているdataset、dataloaderは今回のタスクでは全く通用しなかったので自分で作るしかありませんでした。そのおかげでかなり実装力がついたとは思っています。
class TestDataset:
def __init__(self, head, batch_size, max_len, train, cwd):
self.max_len = max_len
self.head = head
self.data, self.iter, self.feature_columns = preprocess(head, batch_size, None, cwd, train)
print("test loaded")
def get_batch(self):
for i in range(self.data["batch"].max()+1):
...(省略)
yield training_set, memo.index
get_batch
を呼べば次のバッチのデータが返ってくる仕組みになってます。なんだかんだyieldも初めて使いましたが、関数をイテレータのように扱えるのがめちゃくちゃ便利だなーって思いました。
没になったpadding最小化計画
これもまたbooking社のプレゼンですが、旅行回数が違うデータを旅行回数順でソートしちゃってそこからバッチを切れば必然的に意味のないpaddingの数が少なくなるよねというお話です。
結局実装はしましたが、RNN以外だとshapeが全く合わなくてもういいやってことでやめました。
没になったword2vec
普通にPyTorchのembeddingのほうが優秀だった。(逐一更新してくれるから?)
doc2vecもだめ。しょうがないよね。
一番大変だったこと
リーク処理 特徴量にリークがあってかなりの時間でデバッグしてた気がする。
書いたコードに責任を持てるようになりたいし、結局バグが治ったから良いけど予防することも大事だなと。(solutionとしては要らない部分なので省略)
まとめ
- 謎タスクだけどやりがいはあった
- LB更新が1回しか無いのがつらい
- チームいないとモチベがもたない
- もう一回やりたいとは思わない
- スクラッチ実装多くて勉強になった
変なコンペでしたが勉強になりました。まだ順位はわかっていませんが、順位とか気にする前に体力が持たなかったのであんまり気にかけてません。
10位以内に入れば国際学会への論文提出が命じられるのでもし入賞してたらこれとチームメイトが作ったLGBMモデルの概要を書くのみです。といっても、チームメイトの英語強者が書いてくれるそうなので結果待ちです。
日本人で参加したのが僕とupuraさんのチームだけっぽいのでしんみりしてました。
この経験を次に活かしたい!!!