ペットコンペ振り返り

kaggleのPetFinder.my Adoption Predictionコンペに参戦したので、振り返ります。

www.kaggle.com

コンペの概要

マレーシアのPetFinder.myという動物福祉プラットフォームがホスト

マレーシアで里親を探しているペット(犬、猫)が、どれくらいのスピードで引き取られるか(AdoptioinSpeed)を予測するコンペ

AdoptioinSpeedは以下の5通り

0 - Pet was adopted on the same day as it was listed. 
1 - Pet was adopted between 1 and 7 days (1st week) after being listed. 
2 - Pet was adopted between 8 and 30 days (1st month) after being listed. 
3 - Pet was adopted between 31 and 90 days (2nd & 3rd month) after being listed. 
4 - No adoption after 100 days of being listed. (There are no pets in this dataset that waited between 90 and 100 days).

評価指標はquadratic weighted kappa

データ

以下のように、表形式のデータ、テキストデータ、画像データなどが用意されている

  1. Tabular Data
  2. Text
  3. Images
  4. Image MetaData(JSON形式)
  5. Sentiment Data(JSON形式)

 コンペ初期

コンペ終了まで残り1ヶ月くらいの時期に参戦したので、ベースラインとなるモデルがいくつか公開されていて、僕は以下のカーネルをベースにしてコンペに参戦しました。 www.kaggle.com

まずはこのカーネルのモデルのパラメータをOptunaを使ってチューニングしました。

このカーネルは0.427のスコアを出していますが、僕の最初のサブミットは0.417(Stratified5KFold lgb cv mean QWK score : 0.4448、手動閾値)からのスタートになりました。

このベースラインとなるモデルにさらに

  • Discussionで上がっているマレーシアの人口や地理やGDPの統計情報
  • Densenet121によるimage features extraction
  • ['pure_breed', 'Description_length']など他のカーネルの特徴量

を加え、スコアを0.448(StratifiedKFold lgb cv mean QWK score : 0.4751、手動閾値)まで伸ばします。

この時外部データのcat-and-dog-breeds-parameters(コンペのBreed名とは異なるBreed名で登録されていたりするので、コード中で修正を加えたりした)がどうにも上手く活用できていないまま突っ込んでいました。

そこで以下の作業をして

  • cat-and-dog-breeds-parametersを除く
  • ["breed_prefer"]という["Breed1", "Breed2"]によるTarget Mean Encoding特徴量を追加(過学習を避ける為に['breed_prefer_count']<100 or ['breed_prefer_var']>1.5 の場合-1を代入)

スコアを0.458(StratifiedKFold lgb cv mean QWK score : 0.4718、手動閾値)まで伸ばします。

コンペ中期

このあたりからスコアが伸び悩んだので、あらためてEDAをすることにしました。

ここまでのモデルで、RescuerID_COUNTのimportanceが上位だったので、RescuerID関連の特徴量を探ります。RescuerID_COUNTが上位にくるということはRescuerIDは何らかの時系列的要素を含んでいるんじゃないかなと思いながらEDAしていました。というのも、このRescuerIDはtrainとtestで重複するものがなく、また同じRescuerIDであってもDescriptionを見比べると必ずしも同一人物がRescuerしたという訳でもなさそうだったので、例えばこれがPetFinder.myサイトへの掲載タイミングだったとしたら、時系列を被せない為にRescuerIDがtrainとtestで重複するものがないという点にも納得がいくなとか思っていました。

ただそれは多分間違いで、後々RescuerIDというのは組織IDみたいなもので、必ずしも同一人物がRescuerする訳ではなく、掲載タイミングもそのRescuerID内で共通する訳ではないという考えに変わりました。

何はともあれ、RescuerIDによっては同じ掲載タイミングのものを含んでいるものもあるだろうという考えをもとに、そのRescuerID内のあるペットは優先的に選ばれやすいペットであるかを特徴量として組み込むことを試みました。

  • RescuerIDをもとに['Age', 'MaturitySize', ...]等のvarのaggregation(EDAをしてそのRescuerIDがどういう分布を持つかを表すには何となくvarがいいかなという気がしたのでとりあえずvarのみでaggregationしました。df[df['RescuerID_COUNT'] < 10]に関してはサンプルが少ないということで-1を代入することにしました)
  • df['health1'] = ((df[ ['Health', 'Vaccinated', 'Dewormed', 'Sterilized'] ]==1)*1).sum(axis=1)(健康指標で1が多いペットほど健康的なので引き取られやすい?)また一番上の特徴量と掛け算した['RescuerID_Age_var_health1', ...]みたいなものも作成
  • df['health3'] = ((df[ ['Vaccinated', 'Dewormed', 'Sterilized'] ]==3)*1).sum(axis=1)(健康指標で3が多いペットほど健康状態が不明なので引き取られにくい?)また一番上の特徴量と掛け算した['RescuerID_Age_var_health3', ...]みたいなものも作成
  • df['Age_better'] = df['Age'] / (df['RescuerID_Age_var'] + 2)、df.loc[df['RescuerID_Age_var'] < 0, 'Age_better'] = -1(正直この作り方がいいのかはわかりませんでしたがそこそこimportanceは出てたはずなのでひとまずこれを採用しました。のちにチームを組む@FujitaAtsunoriさんはdf['RescuerID_Age_MEAN_DIFF'] = df['Age'] - df['RescuerID_Age_MEAN']みたいにしてました。分母の+2はエラー対策です)

以上の特徴量を加えたところ、スコアを0.463(StratifiedKFold lgb cv mean QWK score : 0.4881、手動閾値)まで伸ばします。

しかし、ここで卒業旅行が連続で入るため、どうしても作業時間が足りなくなるのでチームメイトを探していたところ、@FujitaAtsunoriさんもチームメイトを募集していてスコアが近かったのでチームマージすることになりました。

チーム atfujita & Y.Nakama

ということで、僕がベトナムで慈愛を積んでいる間にatfujitaさんがマージ作業を進めてくれました。本当にありがたかったです...! 

ここでvalidationについて、今までのStratifiedKFoldはAdoptionSpeedに対してStratifiedしていましたが、ここからのStratifiedKFoldはDiscussionに上がっていたRescuerID_Adoption_MEANに対してStratifiedするようなやり方を採用しました。

そしてマージしたところ、fujita_lgb, fujita_xgb, nakama_lgbのridge_stackでスコアが0.465 (Counter({2: 1074, 4: 1156, 3: 666, 1: 864, 0: 188}))まで伸びました。

ここでどうにも手動閾値が使い物にならなくなってきているようだったので(StratifiedKFoldではどうにもリークがあり、trainデータを学習させた時の閾値に汎化性能がなくなっていたのだと思う)、自動閾値というものを実装することにしました。

  • 自動閾値(trainデータを["State", "Type", "AdoptionSpeed"]でgroupbyしてAdoptionSpeedの分布を["State", "Type"]のグループごとに求め、その分布をtestデータに当てはめてtestデータでのAdoptionSpeedの分布の期待値を算出し、それをもとに閾値を求める)

これにより、スコアが0.470(Counter({4: 1103, 2: 1061, 3: 838, 1: 831, 0: 115}))まで伸びます。

 またどういう層の人が住んでいる地域なのかも特徴量に追加しました。

  • state_HDI(Human Development Index)

これにより、スコアが0.473(StratifiedKFold Final OOF QWK = 0.51325、自動閾値)まで伸びます。

ただ、これはシングルモデルで試しても効果がなかったので、乱数的な要素かもしれません。

そして次にモデルの多様性を増やすことを試みます。

まだlgbとxgbしか使っていないので、ペットコンペということもありcatboostを新たに追加し、fujita_lgb, fujita_xgb, nakama_lgb, nakama_xgb, nakama_catのridge_stackを試みます。すると

スコアが0.477(StratifiedKFold Final OOF QWK = 0.51453、自動閾値)まで伸びました。

[kaggler-ja] NAKAMA

copypasteさん(@copypaste_ds)、カレーさん(@currypurin)とチームマージし、[kaggler-ja] NAKAMAが誕生します。

お二人のモデルの内容については割愛します。

チームマージ後に新しく試したこととしては以下があります。

  • 個人的なモデルの部分で['Fee', 'RescuerID_COUNT', 'Quantity', ...]といった特徴量をnp.log1pで裾を短くする
  • NIMA特徴量(美しさ的なスコア1~10を目的変数にした10クラス分類モデルで、美しさ的なスコアは人間の直感でラベリングされたもの)
  • モデルの多様性として0~4のAdoptionSpeedを0~1に変換して、lossをxentropyにしたモデルを追加
  • カーネルで公開されたStratifiedGroupKFoldを使う

www.kaggle.com

  • GroupKFoldにおけるitrをもとに、StratifiedKFoldのitrを決定する(カレーさんによる実験)

 

f:id:NmaViv:20190401210934p:plain

GroupKの平均itrから0.05倍刻みで増加させたitrをStratifiedKに適用した時のQWK推移

カレーさんがvalidationをGroupKFoldで行っていて、CVにリークがなく、OptimizedRounderをそのまま使用しても汎化性能のある閾値が取れていました。

またカレーさんのモデルが加わったことで、チーム atfujita & Y.Nakamaの時は上手くいかなかったStratifiedKFold+OptimizedRounderも安定したスコアが出るようになりました。Counterを比較してみても、atfujita & Y.Nakamaの時よりもいい分布になっていそうです。

  • StratifiedKFold(OptimizedRounder)スコア0.479(Final OOF QWK = 0.5084, Counter({1: 1002, 4: 984, 2: 947, 3: 945, 0: 70}))

そして組み合わせを試したところ以下のような結果になりました。

  • StratifiedGroupKFold(OptimizedRounder)
    LB: 0.477 CV: 0.46563
  • StratifiedGroupKFold(自動閾値
    LB: 0.481 CV: 0.46607
  • StratifiedKFold(round数)(自動閾値
    LB: 0.481 CV: 0.50999
  • StratifiedKFold(round数*1.3)(自動閾値
    LB: 0.484 CV: 0.51380

最終的に、

  1. 攻めのStratifiedKFold(round数*1.35)(自動閾値
  2. 守りのStratifiedGroupKFold(OptimizedRounder)

の二つを最終サブとして選択しました。

LBは以下の通りです。

f:id:NmaViv:20190401220308p:plain

Public Leaderboard

試したけど取り入れてないもの

  • 自分のモデルでxlearnを試したもののStratifiedKFoldでcv mean QWK score : 0.4397だったので結局取り入れなかった、調整が下手だったのかも
  • ['refundable_in_Description', 'chinese_in_Description','RM_in_Description']みたいな特徴量を作ったものの、あまり効かなかったので結局使っていない

反省点

  • RescuerID_COUNTのimportanceが上位で、自動閾値を実装するときもTypeを意識していたのに、どうしてRescuerID_COUNTをRescuerID_dog_COUNT、RescuerID_cat_COUNTに分断しなかったんだろう(['only_dog_RescuerID ', 'only_cat_RescuerID']みたいな特徴量を作っていたがあまり効かなかったので、こちらに気づくべきだった)
  • 僕のモデルでIMG特徴量が重複するものが34個、IMG特徴量+AdoptionSpeedが重複するものが29個あり(カーネルでも同じ写真があると話題に上がっていましたが)、これは「IMG特徴量が一致するときAdoptionSpeedも一致しがちな根拠としては,その写真に2匹のペットが写っていて,別々にリストアップされる(つまり異なるレコードでIMG特徴量が一致する)が引き取りは2匹でないとダメみたいなものが存在するため」みたいな仮説を立てたものの、結局上手く使えなかった、リークの一因になっていたりするのかな
  • shake対策として、ks_sampling、aucでtrain, testの2値分類のimportance比較により、shake要因となる特徴量を除くことを試みたが、微妙な感じで終わってしまった
  • 上記のshake対策をやっておいて、train/testを分類するモデルを作成してtestっぽくないtrainを何割か削除みたいなことをアイデアとして思いつかなかった

【追記(4/10)】

4/10にコンペの結果が発表されました。

金メダル圏→銀メダル圏と残念な結果となりましたが、一緒に闘ってくださったチームメイトには感謝しかありません。

上位のソリューションが公開されているので、確認してみると本当に色々なことをやっているので、しっかりと反省して次に活かしていきたいです。

反省点(追記)

  • PetFinder.myのサイトを調べてはいたが、登録まではしていなかった。上位チームの中にはサイトに登録して、そこで写真に対するレーティングがあることを知り、そのレーティングを特徴量として追加していた、やれることはやっておくべきだった 
  • RescuerIDに時系列的要素があるのではないかと疑っていたが、RescuerIDは自動生成されたもので、最初の4, 5文字に対してソートをかけることで、古参ユーザーたちの類似を見出せたらしい
  • Chinese Descriptionに対しては、['chinese_in_Description']しか試していなかったが、English DescriptionとChinese Descriptionで分けて、['Description_length', 'Description_word_length', 'Description_word_unique']など検討するべきだった
  • NNとgloveとFastText取り入れることできなかった(経験値不足)

ペットコンペ反省会でいっぱい反省します。