H&Mコンペで銀メダルを獲得したソリューション

アンドパッドのデータ基盤チームに所属している成松です。 先日までkaggleで開催されていたH&M Personalized Fashion Recommendationsにて、私が参加したチームが2,952チーム中22位で銀メダルを獲得しました! そこで、本記事ではH&Mコンペの簡単な概要説明と私個人のSolution(Private 36位相当)について紹介します。

コンペ概要

本コンペは、指定されたテスト期間中に購入されそうなH&Mのファッションアイテム12個をユーザごとに予測し精度を競うという内容でした。データとしては、ユーザや商品の属性(ユーザ年齢、商品カテゴリなど)を記したメタデータ(customers.csv, articles.csv)とトランザクションデータ(transactions_train.csv)、そして商品の画像データが与えられました。また、本コペではMAP@12が評価指標として設定されていたため、ground truthをいかに高い推薦順位で予測できるかが非常に重要でした。

Solution

ここからは、私個人のsolution(Private LB: 0.03118で36位相当)について説明していきます。 他の参加者と同じように私のモデルもrecall(ユーザごとに推薦商品となり得る候補商品を抽出する)パートとrank(特徴量作成 & ランク学習により候補商品内の推薦順位を決定する)パートに分かれています。

recallパート

上位のいくつかのsolutionでも述べられているように、本コンペではどうやって候補商品を抽出するかがスコアを大きく左右する重要なポイントでした。私は幾つかの試行錯誤の後、以下の4つの方法で候補商品を抽出しました。

  1. 直近購入した商品と同じ、もしくは色違いの商品を抽出する
  2. 直近購入した商品と類似するproduct_codeの商品を抽出する
  3. 予測対象の週に発売される新商品のうち、販売数が多くなりそうな商品TOP50を抽出する
  4. 直近の販売数が多い商品を抽出する

私が用意した計算環境では候補商品を300個抽出するのが限界であったため、上記4つの抽出方法を上から順番に適用させていき、ユーザごとの候補商品数が300に達した時点で抽出作業を終了させました。 それでは、それぞれのrecall methodについて具体的に説明していきます。

1. 直近購入した商品、もしくは色違いの商品を抽出する

ユーザの中には同じ商品、もしくは色違いの商品をリピート購入するユーザが一定数存在していたため、過去に購入した商品・色違い商品を候補商品として抽出しました。また、リピート購入に至るまでの日数を調査したところ商品全体では30日以内でのリピート購入が多い一方で、"Socks & Tights"カテゴリの商品は前回購入日からの日数が経過してもリピート購入されやすいなど、商品カテゴリによって特性が若干異なっていました。

そのため、私は以下のいずれかの条件に合う商品を抽出しました。

  1. 直近1ヶ月以内に購入された商品・色違い商品
    • 季節性のトレンドも考慮し、やや広めに期間をとりました
  2. "Socks & Tights"カテゴリで、2020-06-01以降に購入された商品・色違い商品
    • 2020-06-01は適当に決めました
  3. 直近3回の買い物で購入した商品うち、リピート購入がされやすいカテゴリに属する商品・色違い商品
    • 直近1ヶ月での買い物がなかったユーザに対する処置です
2. 直近購入した商品と類似するproduct_codeの商品を抽出する

過去に購入した商品と類似する商品(例えば、人気の組み合わせやセットアップ、同じコラボの商品など)も後日購入される可能性が高いと考え抽出することにしました。同一商品や色違い商品であれば、article_idproduct_codeを使ってすぐに抽出できるのですが、それ以外の類似商品をルールベースで一つ一つ抽出するのは現実的ではなかったので、何かしらの方法でproduct_codeの情報をembeddingし、そのembedding結果からcosine類似度で商品間の類似度を測定することにしました。 product_codeをembeddingする方法として、トランザクションデータからcustomer x itemのmatrixを作成しPCAやNMFなどで次元圧縮する方法や、商品メタデータのカテゴリデータや商品説明文に対してword2vecを適用する方法などが考えられましたが、トランザクションデータと各種メタデータの両方を含めてembeddingした方が良い結果が得られるのではと思い、私はPytorch-BigGraphを使用する方法を採用しました。「誰が何を購入したか」「このユーザはどの年代か」「この商品はどのカテゴリに属しているか」など計14種類のrelationを含めた大規模なナレッジグラフを作成し、それをPytorch-BigGraphでembeddingしました。ちなみに、この計算は50epochで30時間ぐらいかかりました。embeddingの結果としてproduct_codeごとに256次元の配列を取得できるので、この配列をもとにproduct_code間のcosine類似度を計算し類似度を測定しました。

こうやってみると、これ本当に類似しているのか?みたいな商品が多く含まれてしまっている...

3. 予測対象の週に発売される新商品のうち、販売数が多くなりそうな商品TOP50を抽出する

対象の週に新しく発売される商品(発売日の情報は与えられていなかったため、商品が最初に購入された日付で判断)のうち販売数が多くなりそうな上位50商品を商品候補として抽出しました。上位50商品の抽出には、商品カテゴリ特徴量 + detail_descをTF-IDF + SVDで作成した特徴量をLightGBMで予測させ、その予測結果を使いました。

4. 直近の販売数が多い商品を抽出する

直近でよく売れている商品はその後も購入されやすいという仮説のもと、直近の売れ筋商品も候補商品として抽出を行いました。ただし、全ユーザに対して全商品の売れ筋商品を画一的に抽出するのではなく、対象ユーザの年齢層に応じた売れ筋商品を抽出するようにしました。これは年齢層ごとの売れ筋商品TOP-Nを調べたところ、年齢層によって(特に20歳未満)は他の年齢層の売れ筋商品との重複率が低いという結果だったためです。

また売れ筋商品を集計する際の期間ですが、予測対象週の直前1週間よりも直前2週間のトランザクションデータを用いて集計を行った方がCVもLBも僅かにスコアが良いという結果だったので、直前2週間の年齢層ごとの売れ筋商品を事前に集計し、その中から販売数の多い順に候補商品として抽出を行いました。

rankパート

recallパートでユーザごとに候補商品300個が抽出できたので、rankパートではこの300個の推薦順位をランク学習によって決定させます。そして、その予測値が高いTOP12が最終的な推薦商品となります。

特徴量

ユーザの購買履歴に関する特徴量や、購入した商品に関する特徴量、ユーザに関する特徴量、ユーザ全体の販売実績に関する特徴量などを最終的に120個作成しました。その中でも代表的なものをいくつか下記に抜粋します。

  • 購買履歴に関する特徴量
    • 過去にその商品を購入した回数
    • 過去にそのproduct_codeの商品を購入した回数
    • 過去に同じ色の商品を購入した回数
    • 前回購入日からの経過日数
    • 総購入回数
    • 直近1ヶ月の購入回数
    • 平均単価
    • その商品を購入したユーザの平均年齢
  • 商品に関する特徴量
    • 商品カテゴリ
  • ユーザに関する特徴量
    • 年齢
  • 販売実績に関する特徴量
    • 直近1週間での販売個数、ランキング
  • Graph embedding(Pytorch-BigGraph)に関する特徴量
    • ユーザのembeddingをUMAPで次元圧縮したもの
  • その他
    • racall_type(recall methodのカテゴリ)
    • recall_typeごとの抽出順番
学習 & 推論

学習はLightGBM(objective = "lambdarank")を使って学習を行いました。binaryでの学習も試したのですが、私のモデルの場合はランク学習の方が僅かにCV, LBともに高いスコアでした。また、recallパートで抽出されなかったground truthを学習時にだけ追加することも検証したのですが、CV-LBの相関が壊れてしまったため、最終的にはrecallパートで抽出されたものだけを使って学習を行いました。

Solutionの全体像

ここまでが本コンペにおいて私が構築したモデルの説明になるのですが、最終的なsolutionでは、このモデルの予測結果を用いてさらに別のモデルを作成しました。上記で説明してきたものをモデルAとすると、モデルAの予測結果を用いて再びrecallパートからやり直したモデルA'が最終的な私のsolutionです。モデルAのrecallパートで抽出した300個の候補抽出にはノイズとなる商品(類似しているがリピート購入はされにくい商品、人気がない商品など)が多く含まれていたため、このノイズ商品を別の商品に置き換えれば候補商品の中により多くのground truthを含めることができ予測精度の向上が見られるのではないかと考えました。そこでモデルA'のrecallパートでは、モデルAの予測値が低い下位150個の商品が含まれないように抽出を行いました(recall method自体はモデルAと同じものを利用した)。別モデルの予測結果をrecallパートに含めて、候補商品の制度を高めたことで、最終的にPublic LBが+0.0004, Private LBが+0.0006だけ上昇しました。

感想

本コンペは、自分でtrain / testデータセットを作成するところから考える必要があったことや、全137万ユーザへの予測を行う必要があったことで処理するデータ量が膨れ上がった(私の場合、testデータセットは最終的に4.1億rows x 120columnsぐらいになった)ことなどの理由で非常に時間とお金と根気が必要とされるコンペでした。その一方で、普段はあまり気にしない省メモリ化・高速化の実装ができたことや、何より馴染みのある領域でレコメンデーションタスクに挑戦できたのは非常に良い経験でした!

(ただ、正直にいうと金メダルが欲しかったです...)

最後に

データ基盤チームはANDPADのミッション「幸せを築く人を、幸せに。」を実現するべく日々の業務に取り組んでいます。 データの力でプロダクトを輝かせることに興味の有る方はぜひご応募ください! また、ANDPADではデータ基盤チームに限らず様々なチームがエンジニアを募集しています。 興味を持たれた方は下記リンクからご応募ください。

engineer.andpad.co.jp