はじめに
過去コンペまとめ記事の三作目です。タイトルにもあるように今回は2017年11月にkaggleで開催されたMercari Price Suggestion Challengeをまとめたいと思います。これまでにToxicコンペとPort Segroコンペのまとめ記事も書いてますのでよければそちらもご覧下さい。以下にリンクをまとめておきます。
コンペ概要
メルカリコンペは株式会社メルカリが主催した、商品の販売価格予測コンペです。テーブルデータですが文章データも含まれているためNLPの知識が必要になります。 また、kernel only コンペなので実験環境と実行時間に制限があることも特徴です。
メルカリは誰でも簡単に売買ができるフリマアプリで、出品時に売り手が商品の価格設定をする必要があります。その際、価格を相場以上にすると売れませんし、相場以下にすると損をしてしまいます。出品する前にきちんと相場を調べておけば解決できますが、手間がかかりますし、そもそも調べ方がわからない人も少なくありません。そのような背景から、出品時に適切な販売価格を提示してくれるシステムがあると便利そうです。そのようなシステム導入を視野に入れて、メルカリコンペでは出品時に商品の適切な販売価格を予測することを目的とします。
賞金・期間・参加者数は以下の通りで、 同じNLPコンペであるToxicコンペと比較すると参加者は少ない印象です。ルールが特殊(kernel only & 2stage制 )であることは理由の一つかもしれません。
賞金 |
期間 |
参加チーム数 |
参加者数 |
$60,000 |
2017/11/22 ~ 2018/02/22 |
2,384 |
2,782 |
特別ルール
メルカリコンペでは通常のコンペには無い2つの特別ルールが設けられていました。それぞれ kernel only, 2stage と呼ばれるものです。
kernel only
kernel onlyコンペでは予測結果のファイルだけでなく、前処理〜モデリング〜ファイル出力までのスクリプトをkernelごと提出する必要があります。
kernelはマシンスペックや実行時間が決まっているため、通常コンペのように潤沢な計算資源と計算時間を活用することができません。当時のkernelの環境は 4cores / 16GB RAM / 1GB disk / GPUなし で計算時間を60分未満にする必要がありました。
2stage
2stageの特徴はprivate LB用の評価データが配布されないことです。通常は配布されたテストデータの一部をpublic LB用の評価データ、残りをprivate LB用の評価データとするのが基本です。一方の2stageコンペでは、配布されたテストデータの全量がpublic LB用の評価データ(stage 1)、競技者には非公開のテストデータがprivate LB用の評価データ(stage 2)となります。
特別ルールの影響
kernel only × 2stage のコンペでは計算時間の観点で注意が必要です。それはprivate LB用の評価データでスコア計算をする際もkernelの時間制約を満たす必要があるためです。メルカリコンペでは private LB用の評価データサイズがpublic LB用の評価データサイズの約5倍と大きかったので、stage2で計算時間制約を守れず(あるいはメモリ制約を守れず)スコアがつかなかった競技者が続出しました。(769/2384チームがスコアなし)事前に対策をすれば防ぐことは可能ですが、private LB用の評価データは非公開(サイズのみ公開される)であるため計算時間の見積もりには注意が必要です。
データの種類とタスク
テーブルデータ(数値データ+カテゴリデータ+文章データ)を用いた回帰問題です。商品ごとに販売価格を予測するモデルを構築します。テーブルデータですが文章データも含まれるためNLPの知識が必要になります。
目的変数の分布は下図(左)の通りで歪んだ分布となっています。下図(右)はlogをとった場合の分布ですが、①分布が正規分布に近づく②評価指標がlogをとった上でのRMSEである(後で説明します)ことからlogをとった上で予測することが定石だったようです。画像は後ほど紹介しますがこのEDA kernelから拝借しました。
データサイズは以下のとおりです。test_stg.tsv(コンペ開催中は非公開のstage2用の評価データ)のサイズがtest.tsvの約5倍であることに注意する必要があります。2stageコンペでは必ず確認しましょう。データサイズは比較的小さい印象です。
ファイル名 |
データサイズ |
レコード数 |
カラム数 |
train.tsv |
322M |
1,482,535 |
8 |
test.tsv |
147M |
693,359 |
7 |
test_stg2.tsv |
737M |
3,460,725 |
7 |
ちなみにtrain.tsvのheadはこんな感じです。商品ごとに商品名、ブランド名、価格などのデータが与えられています。
各カラムの意味は以下のとおりです。正確な説明はデータ説明ページを御覧ください。
カラム |
意味 |
train_id |
商品ID |
name |
商品名 |
item_condition_id |
商品の状態 |
category_name |
3分類のカテゴリ |
brand_name |
ブランド名 |
price |
価格(目的変数) |
shipping |
送料が売手負担か否か |
item_description |
商品説明 |
評価方法
評価方法はRMSLE (Root Mean Squared Logarithmic Error)です。つまりlog1pをとった上でRMSEで評価することになります。評価指標がRMSEなのでLightGBMやNNを使えば評価指標を直接最適化することができます。
RMSLEの直感的説明は後で紹介するこのページで紹介されています。以下の図は¥3,000の商品に対してRMSLEを変化させたときの推定値の誤差範囲を表しています。
仮にRMLSE=1.0の場合は推定値の誤差範囲は¥1,103 ~ ¥8,156となります。図から分かる通り、logをとるため誤差範囲の上振れ幅と下振れ幅は等しくなりません。
ちなみに一位のチームがコンペ終了後に公開したkernelはRMLSE=0.3875で、誤差範囲は¥2,051 ~ ¥4,387でした。これは実際のプロダクト上でも稼働可能なスコアだそうです。
提出方法
提出のフォーマットは以下の通りで、IDごとに予測価格を出力します。
test_id |
price |
0 |
1.5 |
1 |
50 |
2 |
500 |
3 |
100 |
勉強になる Kernel と Discussion
上位解法ほどのスコアは出せませんが、kernelとdiscussionは良いアイディアと実装で溢れています。どのアプローチも勉強になったので簡単にまとめておきます。
EDAとNLPチュートリアルがまとめられています。以降の解法を読む上で一度データを確認しておきたい方は、EDA部分だけでも見ておくことをおすすめします。
NLPチュートリアルは本コンペの目的と直接の関係はありませんが、NLPで有名な手法(よく使われる手法??)が順に紹介されています。説明されている項目は以下のとおりです。
- 正規表現を用いた文章のtokenize
- WordCloudを用いた頻出単語の可視化
- tf-idfによる特徴量生成
- SVD + tSNEによる次元圧縮と可視化
- K-means, LDAによるクラスタリングと可視化
- トピック毎に単語の分布を確認
文章データの分析では「特徴量抽出 -> クラスタリング -> 次元圧縮 -> 可視化 -> 結果解釈 」の順に作業することはままあると思うので初学者におすすめです。
基本的な特徴量抽出をした上でRidge 回帰でモデリングしています。処理手順を箇条書きにすると以下のとおりです。
- 各カラムの欠損補完と型変換
- name, category_nameからCountVectorizerで出現回数の特徴量を作成
- item_descriptionからTfidfVectorizerでtf-idfの特徴量を作成
- brand_nameをLabelBinarizerでダミー変数化
- item_condition_id, shippingをダミー変数化
- 各特徴量をconcatしてRidge回帰モデルを学習
これらのアイディアは他のkernelでも頻出するので、理解を深めたい方は一度kernelをForkして手を動かしてみると良いかもしれません。
Ridge回帰モデルの変数重要度をELI5で可視化しています。下図を見ればわかるように単に回帰係数を眺めるよりも重要変数の把握が捗ります。
変数重要度の算出アルゴリズムとしてpermutation importance, LIME, SHAPなどが知られていますが、雰囲気だけでも知りたい方はkaggle learnのMachine Learning Explainabilityがおすすめです。
entity embeddingを応用したNNのベースラインモデルです。 各変数をembedding層でベクトル表現 -> 系列データにGRUを適用 -> 全変数をconcateしてDense + dropoutの構造でNNを構築しています。kerasを使ったtokenizeから学習までの必要最低限のコードなので初学者におすすめです。
FTRL, FM_FTRL, LightGBMのensembleをしています。FTRL, FM_FTRLの実装はToxicコンペでも登場したwordbatchライブラリを使用しています。予測時には加重平均をとっており、重みはFTRL : FTRL_FM : LightGBM = 0.18 : 0.55 : 0.27としています。重みの決め方についての言及はありませんが、FTRL_FM > LightGBMの関係をみるにFTRL_FMも強力な手法なのでしょうか。(知識不足でよくわかりません)
kernelコンペの性質上シンプルな解法が多い中、比較的複雑なモデルがあったので紹介します。シングルモデルですがシルバーメダル相当の強力なNNモデルで、構造は以下の通りです。
正直なところNN初学者の私にはこの図以上のことは説明できません。図のprice statistics部分では category_name, brand_name, shippingをgroupbyのkeyとしてmedian, mean, stdの統計量を算出した後、それらを四則演算することで特徴量を作成しています。
本コンペの1位チームがコンペ終了後に公開したkernelです。1st place solutionの核となる部分だけまとめた解法で、解法自体はシンプルですが非常に強力なkernelです。後でも紹介しますがtkmさんの解説動画を見ると理解が捗ると思います。モデルは隠れ層が3層のMLPです。
上位解法概要
上位解法はNN解法が目立ちます。LBを見ると1位のスコアが飛び抜けており、discussionを見る限り1位・2位の方が途中でチームマージしてトップを走り続けたようです。1位解法は非常にシンプルなNNで構成されており、多様性の出し方やモデリング方法など必見です。今回も1位〜3位解法をまとめます。
最終的な解法は12個のMLPのアンサンブルです。いろいろ試行錯誤した結果、複雑なモデルからシンプルなモデルに収束したようです。チームマージ前とチームマージ後の解法が共有されていたため順にまとめようと思います。(マージ後の最終モデルが見所です)ちなみにスコアの推移は以下の通りで、マージ直後のモデルでも1位相当のスコアを記録していることが分かります。
Pawelさんの解法(チームマージ前、当時暫定2位)
複雑なモデルを作成していたそうですが、詳細な説明がないため雰囲気しか分かりません。ポイントは以下の3つだそうです。
- モデルは複数のRidge回帰のアンサンブルです。各Ridge回帰で使用する説明変数を変える(使用するカテゴリ変数の組合せを変える)ことで多様性を出したそうです。
- 残差MLPを学習させたそうです。具体的には、(1)で作成したRidge回帰の予測値と目的変数の残差を新たな目的変数として学習したMLPを作成したそうです。
- 残差LightGBMを上記の残差MLPと同様に作成したそうです。
個人的に残差モデルを作ってboostingさせるアイディアは初見だったので勉強になりました。
Kostantinさんの解法(チームマージ前、当時暫定1位)
モデルはMLPとCNNのアンサンブルです。こちらも詳細な説明がないため雰囲気だけまとめます。
- 3つのMLPを学習させたようです。特徴量は公開kernelと変わりないですが、Ridge回帰とeli5で算出した変数重要度を用いていくつかの変数に手直しを加えたとのこと。(そこが知りたい)
- conv1dを使ったCNN学習させたようです。スコアはそれほど高くないが、アンサンブルにとても貢献したそうです。
チームマージ時点
kernel onlyの制約上、上記2つの解法を愚直にアンサンブルすることはできず、双方のアイディアを取り入れた一つのモデルを構築するのに苦戦したそうです。(ここで2週間ほど費やしたらしい) 結局、3種のデータセットと4つのモデルを使ったアンサンブルに落ち着いたとのこと。多様性を出すために以下の項目を組合せたようですが、どのように組合せたかはわかりません。
- different tokenization, with/without stemming
- countvectorizer / tfidfvectorizer
最終的なモデル
モデルは適切にチューニングされた12個のMLPアンサンブルです。実験を通じて同じデータで異なるモデルを学習させるよりも、異なるデータを同じモデルに学習させたほうが多様性を表現できることに気づき、モデルはMLP(MLPの構造は一種類ではない)と決めたそうです。 計算時間制約を考えてもMLPは都合が良かったとのこと。
解法のポイントを7つに分けてまとめます。(正直知らない工夫ばかりでテンション上がりました)
1. シンプルなMLP
1位解法最大の特徴です。3種類のデータセットに対して4つずつMLPモデルを構築しています。各データセットでMLPの構造は基本的に同じですが、計算時間を考慮して入力層のユニット数は少し調節されていました。(192が2つと128がひとつ)
確かに全てMLPですが、データセットを変えたり、後で説明する2種類のMLPを用意したり、学習/ 推論の直前で特徴量を2値化したりと多様性を与える工夫がちりばめられています。
これらのMLPのアイディアを凝縮したkernelが公開されているので是非みていただきたいです。最終モデルに使用したMLPと同じもので、モデルでシンプルさがよく分かると思います。(tkmさんの解説動画もあります)
www.kaggle.com
最終モデルのソースコードはgithubに共有されているので詳細を知りたい方はこちらをご覧ください。
github.com
2. 2種類のMLP
構築したMLPは2種類に大別できます。一つは損失をHuber lossとした回帰モデルで、もう一つが分類モデルを経由した回帰モデルです。各データセットに対して2つずつ学習させていました。評価指標がRMSEであるにも関わらずHuber lossで最適化する点や、回帰問題をあえて分類モデルで解くあたりが独創的だと感じました。ちなみにHuber lossモデルは隠れ層が3つ、分類モデルは隠れ層が2つの構造です。分類モデルを経由した回帰モデル作成の手順を簡単にまとめておきます。
- 目的変数の値域を64のビンに区切り、各ビンの中点をそのビンの代表値に設定する。
- 目的変数と各ビンの代表値とのユークリッド距離を算出し、softmaxに通すことで64個の確率的クラスラベルを用意する。(softmaxのexp(ax)のaはハイパーパラメータ)
- 多クラス分類問題としてMLP(出力層をsoftmax)を学習
- MLPの予測確率と各ビンの代表値の加重平均を最終的な予測値とする(回帰問題に帰着)
この分類モデルを経由した回帰モデルは、過学習することなく単体で良いスコアを記録したようです。それに加えてモデルの多様性の観点からもアンサンブルに貢献したそうです。
どのデータセットもCountVectorizerとTfidfVectorizerを用いたスパース特徴量が基本になります。使用カラム(name, item_description, category_name)、Cleaningの有無、ベクトル表現方法(CountVectorizer, TfidfVectorizer)、ngramの範囲、binary表現有無 などの組合せから、様々なスパース行列を作成し、データセットごとに使用するスパース行列の組合せを変えています。
4. 特徴量の2値化
学習/推論の直前で特徴量を全て2値化(非ゼロか否か)する処理を加えています。この処理をするモデルはデータセットごとに2つでHuber lossモデルと分類モデルの1つずつでした。2値化するための閾値はいくつか試したそうですが、それによるスコア改善は見られなかったようです。
5. 学習速度のチューニング
学習速度のチューニングは非常に重要だったらしく、特にepochごとにbatch sizeを2倍にすることは学習を早めると同時にスコア向上につながったそうです。それに加えてepochごとに学習率を上げることで、徹底的に学習速度を速める工夫をしています。これらのパラメータは2epoch目で最良のvalid score、3epoch目で過学習するようにチューニングしたそうです。
6. L2正則化とPRELU
1層目にL2正則化を加えることは効果的だったようです。またtensorflowモデル(あとで説明しますが2種のpackageでモデルを作っている)ではRELUよりもPRELUが効果的だったそうです。
7. tensorflowとMXNetでの実装
最終的に選択した2つのsubmit(コンペ終了前に提出用のsubmitを2つ選択するルールがある)はtensorflowとMXNetそれぞれで実装したモデルにしたそうです。packageを複数使った理由は読み取れませんでしたが、tensorflowは各コアで別のモデルを学習でき、MXNetはスパースデータに対するCPUのMLP実装が効率的で実行時間が短かいなどの特徴があったのだとか。メモリ使用量やmultiprocessingに関しても説明されていますので興味のある方はdiscussionを読んでみてください。
4つのモデル(Redge×1, NN×3)を構築しています。その他にもLightGBMを学習させたようですが上手く機能しなかったそうです。同じデータで異なるモデルを学習させる方法はうまくいかなかったので、データを変えて異なるNNモデルを複数用意したとのこと。RidgeとNNについてそれぞれ簡単にまとめます。
Ridge回帰
特徴量は1-ngramとアレンジを加えたbigramsです。1-ngramsは商品名と商品説明に対してそれぞれ適用したそうです。アレンジを加えたbigramsの処理手順は以下のとおりです。
- nameとitem_descriptionの先頭5単語をconcat
- np.uniqueで重複を除外
- とりうる全ての組合せで2-ngrams
このようなbigramsのとり方は初見だったので参考になりました。
NN
特徴量を変えたNNを3つ構築しています。特徴量のバリエーションは以下のとおりです。また、1位解法と同様に、epochごとにbatch sizeを2倍にして学習速度を速めたそうです。
- name, descriptionにCountVectorizerしたものを特徴量にしたNN
- name, descriptionに対してanalyzer='char'でCountVectorizerしたものを特徴量にしたNN
- fastTextによるembeddingを層(name, descriptionで重みを共有)をもつNN
2nd place solution kernelが公開されているのでモデルの詳細を知りたい方はこちらをご覧ください。NN構造は公開kernelと比較的似ているため省略します。
モデルはNNとFMのensembleです。最終出力は2モデルの加重平均(NN×0.6 + FM×0.4)としたそうです。各モデルで使用した特徴量とNNに関してはモデル構造を簡単にまとめます。NNの構造構築に特に注力したそうです。
FM
使用した特徴量は以下のとおりです。
- name, countvector
- brand onehot
- desctiption, tfidfvector
- category1-3 onehot
- condition
- shipping
NN
使用した特徴量は以下のとおりです。
- name + brand, maxlength=8, 10
- description, maxlength= 64, 72
- category3, onehot
- condition
- shipping
- lenght of description
- mean and std price
- count of the category3
NNは図のような構造をしています。(図は私が作成したので間違いがある可能性があります。input1~4の項目は自信が持てなかったので記入しませんでした。)モデルの詳細を知りたい方は 3rd place solution kernelをご覧ください。
またNNの学習パラメータは1epochずつ以下のように設定したようです。7epoch目でwomen, beautyカテゴリのデータに絞っているのは、データの大多数が女性向け商品であるため女性向け商品を重要視して学習させるためだそうです。
エポック |
バッチサイズ |
データ数 |
学習率 |
最適化手法 |
1 |
907 |
600k |
0.0055 |
adam |
2 |
907 |
600k |
0.0055 |
adam |
3 |
1007 |
600k |
0.0055 |
adam |
4 |
1007 |
all |
0.0055 |
adam |
5 |
1424 |
400k |
0.0055 |
adam |
6 |
1424 |
800k |
0.008 |
adagrad |
7 |
900 |
women, beauty |
0.008 |
adagrad |
その他の上位解法
discussionは解法の宝庫ですね。余力がある方はこちらもぜひ。
日本語記事
メルカリコンペの閉会式に関するまとめ記事です。閉会式では上位入賞者の解法発表やパネルディスカッション形式のトークなどが企画されたようで、その時の動画が共有されています。パネルディスカッションは日本語動画ですので英語が苦手な方でも見ることができます。
メルカリコンペを題材にした機械学習初心者向けのkaggleチュートリアルです。コンペ概要説明〜データの確認〜前処理〜ランダムフォレストによるモデリング まで丁寧に説明されています。
tkmさんの優勝コード解説動画です。コードやモデルのどこがすごいのか丁寧に説明されているのでおすすめです。
St_Hakkyさんもメルカリコンペのまとめ記事を書いていました。twitterのつぶやきや企業がコンペに出す意味についてもまとめられています。
おわりに
今回はメルカリコンペをまとめました。ベジネスでの活用シーンもイメージしやすく、馴染みのあるアプリが題材ということでいつも以上に楽しめた気がします。特に1st place solutionは衝撃的で、やや興奮状態でまとめ記事を書いた記憶があります。シンプルでありながら工夫が各所に散りばめられていて本当にかっこいいです。kaggleには他にもすごい解法や知見があると思うとわくわくしますね...!! 次回はAvitoかQuoraあたりをまとめるつもりですので良ければそちらも御覧ください。