こんにちは。研究開発部の深澤(@fufufukakaka)です。
本記事ではクックパッドマートにおける item-to-item レコメンデーションについて、その概要とアルゴリズムの変遷についてお話したいと思います。
item-to-item レコメンデーションとは
レコメンデーションにはいくつかタスクが存在しますが、今回はその中でも item-to-item レコメンデーションについてお話します。
item-to-item レコメンデーションでは、「ある商品について、その商品を軸におすすめできるアイテム」を表出します。表現の仕方はサービスによって様々ですが、よく この商品を買っている人にはこちらもおすすめです
, この商品に関連する商品
などと表現されています。
さて、その item-to-item レコメンデーションの中にも実は更に種類があります。それは商品間のスコア(距離,類似度,etc) をどの軸で表現するか、です。大きく分けて以下の2種類になるかと思います。
- ある商品について、その商品に関する行動データ(商品a を買った後によく商品b が買われている、など) を利用してスコアを計算する行動データを使った方式。
この商品を買っている人にはこちらもおすすめです
といったレコメンドを実現する際に選択されます- 行動データを使って似ている商品を出すことを目的としたモデルもあります
- 商品名や商品画像を使って商品間の類似度を使うコンテンツデータを使った方式。この方式でおすすめ商品を表出している場合は
この商品に関連する商品
に適したレコメンドになります
今回は item-to-item レコメンデーションかつ行動データを使ったモデルの話になります。
クックパッドマートにおける item-to-item レコメンデーション
クックパッドマートでは早くから item-to-item レコメンデーションが取り入れられています。
各商品の詳細ページ下部に、この商品を買っている人におすすめ
といったセクションがあります。ここで出ている 6 商品は機械学習モデルによって毎日更新されています。この機能は2020年に追加され、いくつかモデルの変遷を経て現在、こちらの機能はクックパッドマートの売上においてある程度の貢献を果たすに至っています。
ここからは実際にどのような変更があったのかをお話します。
アルゴリズムの変遷
Item2Vec 時代
初めは Item2Vec でレコメンドを実装していました。Item2Vec は https://arxiv.org/abs/1603.04259 で提案された手法です。僕は前職の先輩方が使っていたのを見て使いはじめました。
Item2Vec は単語の意味ベクトルを獲得する Word2Vec をレコメンドに応用したもので、Word2Vec が単語を対象にするのに対して、Item2Vec では商品を対象にし、商品ごとの意味ベクトルを獲得します。これによって商品Aと商品Bの距離が計算でき、「商品Aに距離が近い商品群」が取得できます。
"距離"の意味合いは学習のさせ方によって異なります。当初は1回の注文で一緒に購入された商品を軸に学習させました。具体的には order を context、その order に含まれる item_id を word とみなして Word2Vec の文脈で扱います。
Item2Vec の魅力はとても簡単なコードでレコメンドが実装できる点にあると思います。gensim を駆使することで数行でレコメンドの大枠が整います。例えば user_id, order_id, item_id, cv_datetime
という dataframe から学習させたい場合には
from gensim.models import word2vec # df.columns = ["user_id", "order_id", "item_id", "event_timestamp"] のようになっていると仮定します order_product_dict = ( df.groupby(["order_id"])["item_id"] .apply(lambda x: [str(v) for v in x.tolist()]) .to_dict() ) # word2vec_params というパラメータを集約した yaml or json を事前に読み込んでいると仮定します item2vec = word2vec.Word2Vec( order_product_dict.values(), vector_size=word2vec_params["vector_size"], window=word2vec_params["window"], alpha=word2vec_params["alpha"], sample=word2vec_params["sample"], epochs=word2vec_params["epochs"], ns_exponent=word2vec_params["ns_exponent"], min_alpha=word2vec_params["min_alpha"], hs=0, negative=1, sg=1, seed=42, min_count=1, workers=4, )
このようなコードでレコメンドモデルの学習自体は完了します。 推薦したいときは most_similar 関数でコサイン類似度に基づいて、クエリとなる商品と "距離" の近い商品を指定した数だけ取得できます。
item2vec.wv.most_similar(target_product_id, topn=10)
このように Item2Vec による推薦は gensim に乗っかることでかなり簡単に実装できます。このモデルを使ったレコメンドが 2020年9月頃からアプリに載り始めました。その後、以下のような変更・施策を試してきました。
- 編集距離を用いた名寄せ
- 当初はキャンペーン用に商品が複製+prefix に SALE など文字列が付いた、実体は同じだけど違う id の商品がたくさんありました。これを編集距離ベースで名寄せしていました。現在は複製されずにセールの運用が可能になったため、あわせてこちらも退役させています
- 直近出品された商品の中から推薦する枠を混ぜる
- 学習データを1年分としているのですが、直近出品された商品をフィーチャーする目的として枠を用意していました。具体的には6商品を普通に推薦し、3商品を直近枠として混ぜていました。現在、RecVAE に変わったタイミングで6商品のみとなり、このタイミングで外しました(まだ実装自体は残っています)
- 特定のカテゴリの商品には予め決めたレコメンドを出す
- これは Item2Vec に限った話ではないですが、例えばワイン系の商品のレコメンドではワインによく合う(だろうとこちらが考えて決めた)おつまみリストを出すようにする、という施策を試しました。相談を受けて試してみたところ、有意な変化が見られず、そのまま元に戻しました
Item2Vec からの転換
Item2Vec での運用は非常に手軽だったため、アルゴリズム本体以外の細やかな変更を色々試せました。が、ずっと思っていた心残りがありました。オフラインテストでの精度指標があまり高くなかったのです。
Metrics | Value |
---|---|
Hit@10 | 0.149 |
MRR@10 | 0.0549 |
NDCG@10 | 0.0604 |
Precision@10 | 0.0176 |
Recall@10 | 0.1012 |
これはある時点でのデータを抽出して Item2Vec での学習を実施した際の結果です。NDCG@10 が 0.06 となっていますが、これは感覚的にはとても良い数字とは言えないものでした。
Item2Vec を倒すために Matrix Factorization・特徴量をいくつか混ぜた Factorization Machine 系のものをニューラルベースで実装し試したのですが、精度指標が多少上回っても推薦の偏りが強くなったり(Coverage が下がってしまった) となかなか満足いくアルゴリズムが出せずにいました。
RecBole による実験
そんなことを考えつつ過ごしていたときに、以下のブログでも紹介した RecBole を知り、これを PoC だけでなく実践で試してみようと考えました。
RecBole についての紹介は前述した記事で記載した紹介を引用します。
RecBole は中国人民大学・北京大学の研究室が共同で始めたプロジェクトのようで、去年の11月に arxiv に登場しました。今年の8月に提供しているモジュールがv1を迎えて、本格的に色々な人が利用するようになったようです。 RecBole 最大の魅力は、上述してきた再現性の難しいレコメンドモデルを統一したインタフェースで実装し、比較を容易にしているところにあります。そして実装されているモデル、適用できるデータセットの数が凄まじいです。モデルは現時点で70以上(モデルリストがすごい )、データセットは20以上のものについて即座に試せます。どれくらい即座に試せるかと言うと
pip install recbole python run_recbole.py --model=<your favorite model> --dataset_name ml-100k
これだけで、レコメンド界隈の中で最も有名なベンチマークである MovieLens-100k データセットに対して70以上のモデルを即座に(追加の設定が必要なやつもありますが)試せます。これだけのモデル・データを試すことができる環境はそうないと思われます。また70以上の収録されているモデルたちは全て PyTorch ベースで丁寧に再実装が行われており信頼性は非常に高いです。predict 関数などの基本的なインタフェースは統一されており、実験のし易い環境が整えられています。
RecBole は user に対する item への推薦を仮定しているため、item-to-item のレコメンドで RecBole を適用するためにはログを変形する必要があります。
具体的には user1: item1,item2
というログが得られたらこれを変形して item1→item2, item2→item1
というように関連して購入されたと思われるセッション内でペアをつくり、各アイテムを user とみなしたデータを作ります。
実際には「あるユーザがその日購入した商品履歴」を軸にしてペアを作っています。
こうしたデータを作り、RecBole で実験を行ったところ、以下の結果を得ました。
Name | hit@10 | mrr@10 | ndcg@10 | precision@10 | recall@10 |
---|---|---|---|---|---|
RecVAE | 0.3264 | 0.1368 | 0.1358 | 0.0617 | 0.1864 |
MacridVAE | 0.3111 | 0.1537 | 0.1352 | 0.0661 | 0.1612 |
EASE | 0.2598 | 0.1352 | 0.1123 | 0.0657 | 0.1298 |
NeuMF | 0.2884 | 0.1102 | 0.1115 | 0.0546 | 0.1554 |
NNCF | 0.2598 | 0.1072 | 0.0995 | 0.0528 | 0.1277 |
ItemKNN | 0.2339 | 0.1039 | 0.0954 | 0.0461 | 0.123 |
NGCF | 0.2116 | 0.1017 | 0.0786 | 0.0436 | 0.0914 |
BPR | 0.2069 | 0.1014 | 0.0765 | 0.0458 | 0.0847 |
MultiDAE | 0.2095 | 0.0949 | 0.0755 | 0.0427 | 0.0907 |
DGCF | 0.18 | 0.0909 | 0.0619 | 0.0386 | 0.0653 |
Item2Vec | 0.149 | 0.0549 | 0.0604 | 0.0176 | 0.1012 |
SLIMElastic | 0.1672 | 0.0861 | 0.0597 | 0.0419 | 0.054 |
LightGCN | 0.1734 | 0.0881 | 0.0593 | 0.037 | 0.0604 |
DMF | 0.1732 | 0.0843 | 0.0586 | 0.0392 | 0.0585 |
CDAE | 0.1524 | 0.083 | 0.0517 | 0.0336 | 0.0462 |
SpectralCF | 0.1543 | 0.0804 | 0.0502 | 0.0336 | 0.0446 |
LINE | 0.1331 | 0.0732 | 0.0419 | 0.0315 | 0.0282 |
Pop | 0.1174 | 0.0424 | 0.0315 | 0.0234 | 0.0332 |
ENMF | 0.073 | 0.0355 | 0.0211 | 0.0184 | 0.0111 |
GCMC | 0.003 | 0.0011 | 0.0015 | 0.0003 | 0.003 |
30モデル弱を実験し、RecVAE など VAE 系が NDCG@10 で 0.13 程度を達成しました。 これは Item2Vec の成績と比較するとかなりよかったので、RecVAE と Item2Vec をオンラインで比較してみることにしました。
RecVAE について
RecVAE はユーザとアイテムのヒストリーを行列にした上で、 Variational Auto-Encoder というニューラルネットワークで圧縮・復元の学習を行い、ユーザとアイテムのヒストリー行列を正確に復元できるように学習したモデルです。 Multi-VAE の構造を元にしつつ composite-prior の導入や Encoder 層の改良を行っています。
Paper-With-Code のレコメンドに関するリーダーボードで NDCG による評価が行われている Movielens 20M (https://paperswithcode.com/sota/collaborative-filtering-on-movielens-20m) では、RecVAE は 2019 年に発表されたモデルでありながらまだ 3位の位置を保っており、かなり強いモデルです。
先程のブログでも紹介した、社内のデータを使った実験でも殆どのケースで RecVAE は上位に入っていました。
インターリービングによるオンラインテスト
オンラインでの比較にはABテストが実施されることが一般的ですが、今回は実験的な意味合いも含めてインターリービングを実施しました。
インターリービングとは推薦システムなどランキングを出力するタイプのアルゴリズムに関して適用できる手法で、比較したい2つのランキングアルゴリズムの出力を混ぜて一つのランキングを出力します。その混ぜたランキングについてユーザが何らかのアイテム(アイテムα)にコンバージョンした際、どちらのアルゴリズムによって出力されたアイテムなのかという観点から各アルゴリズムにポイントを割り振り、アルゴリズムA・Bのどちらが一方より優れていたか、をクエリ(アイテムα)ごとに出します。
インターリービングが ABテストと最も異なっている点は、群を分けることなく同じユーザ群を使った実験が可能で、群差を気にする必要がない点です。また、同じユーザに直接アルゴリズムA, Bを提示して暗黙的な直接比較を要求するので、ABテストよりもはっきり結果が出やすい点が特徴的です。CVR など性能を定量的に測れない(相対的にしかわからない)などデメリットも多いですが、前述したメリットに着目して実施してみることにしました。
今回は Pairwise Preference multileaving(PPM) を使ってランキングを生成してみました。こちらのライブラリ(mpkato/interleaving) を利用すると以下のように実装できます。
In [1]: import interleaving In [2]: a = [1, 2, 3, 4, 5] In [3]: b = [4, 3, 5, 1, 2] In [4]: method = interleaving.PairwisePreference([a, b]) In [5]: interleaved_ranking = method.interleave() In [6]: type(interleaved_ranking) Out[6]: interleaving.ranking.PairwisePreferenceRanking In [7]: interleaved_ranking Out[7]: [4, 1, 2, 3, 5] In [8]: interleaved_ranking = method.interleave() In [9]: interleaved_ranking Out[9]: [4, 1, 3, 2, 5]
評価するときは以下のようになります。
In [10]: my_ranking = interleaving.PairwisePreferenceRanking([a, b], contents=interleaved_ranking) # ランキングクラスを復元する In [11]: interleaving.PairwisePreference.evaluate(my_ranking, [0, 1]) # アイテム4と1がクリックされたとする Out[11]: [(0, 1)] # ランキングAが勝ったことを示す
2022年2月~3月で Item2Vec・RecVAE のランキングを混ぜてアプリに表出し、そのデータを集めました。結果は以下のようになりました。
Name | Value |
---|---|
RecVAE 勝利数 | 1295 |
Item2Vec 勝利数 | 1149 |
引き分け回数 | 899 |
これについて Binomial sign test (有意水準 5%) を行い、RecVAE が有意に Item2Vec より優れていることがわかりました。
RecVAE と Item2Vec の比較(定性評価)
2022年 3月頃のレコメンド結果からいくつかのケースを抽出して考察してみます。現時点(2022/10/05)でクローズされていない商品にはリンクをつけております。
ケース1. アトランティックサーモン
クックパッドマートの中でも非常に人気な商品であるアトランティックサーモン (https://cookpad-mart.com/products/1592) について、RecVAE と Item2Vec のレコメンドをそれぞれ見てみました。
- Item2Vec
- お刺身切落しお買得Wパック(サーモン)養殖解凍生食用(https://cookpad-mart.com/products/34300)
- 生サーモン切り落とし100g(生食用)(養殖)(https://cookpad-mart.com/products/17652)
- 真鯛切落し150g(https://cookpad-mart.com/products/43892)
- 【おためし割】サーモン人気セット(https://cookpad-mart.com/products/45868)
- 訳ありアボカド🥑2~3個(サイズ異なる)(https://cookpad-mart.com/products/16160)
- RecVAE
- 【おすすめ】和風サラダ ベビーリーフミックス(https://cookpad-mart.com/products/7365)
- ⑰黒毛和牛切落し300g
- 国産 サンふじ 4個(46玉サイズ)
- お刺身用つぶ貝開き(解凍 生食用)(https://cookpad-mart.com/products/34967)
- 玉ねぎ お得用(https://cookpad-mart.com/products/16888)
Item2Vec は比較的同一系統の商品を提案していますが、RecVAE はサーモンと併せて使えそう・ついでにこちらも、といった観点で食材を提案している様子が見て取れます。
ケース2. 有機じゃがいも
続いて、【有機】じゃがいも400g
(https://cookpad-mart.com/products/7520) という商品のレコメンドを確認します。
- Item2Vec
- 有機たまねぎ800g(https://cookpad-mart.com/products/7521)
- 【有機】ピーマン120g(https://cookpad-mart.com/products/7915)
- 希少な国産有機にんにく(https://cookpad-mart.com/products/7524)
- 【有機】人参300g甘~い!
- 黒毛和牛モモステーキ【1枚150g】(https://cookpad-mart.com/products/11357)
- RecVAE
- 【大特売】長ネギ 250g(https://cookpad-mart.com/products/47639)
- 5種のサラダセット (https://cookpad-mart.com/products/10696)
- アボカドのプロが毎朝選別!【大玉 食べごろアボカド】(https://cookpad-mart.com/products/12375)
- お徳用 ささみ 大袋 約500g(https://cookpad-mart.com/products/13520)
- きまぐれなブロッコリーALサイズ
Item2Vec はこちらでも同系統(といってもじゃがいもばかり出しているのではなく、有機という特徴を捉えている)の商品を出しており、RecVAE はそれとは対照的な商品推薦を行っています。
ケース3. ささみ
最後に お徳用 ささみ 大袋 約500g
(https://cookpad-mart.com/products/1605) の推薦結果を確認します。
- Item2Vec
- ★☆⑦【売筋第1位】国産豚 挽肉 細挽 300g(https://cookpad-mart.com/products/31159)
- お徳用 鶏むね挽肉 大袋 約500g(https://cookpad-mart.com/products/1606)
- 鶏ひき肉(むね肉)(https://cookpad-mart.com/products/1666)
- 大葉 100枚
- 【少量PC】スーパーフルーツトマト約170-200g
- RecVAE
- ★☆⑦【売筋第1位】国産豚 挽肉 細挽 300g(https://cookpad-mart.com/products/31159)
- 【理由ありお買い得品】椎茸パック(不揃いパック)
- 赤たまご(https://cookpad-mart.com/products/5617)
- 【国産】レモン(3個パック)(https://cookpad-mart.com/products/52012)
- 農家の朝採りきゅうり(https://cookpad-mart.com/products/36458)
やはりここでも、同様の傾向が見られました。Item2Vec は行動データを使ったモデルではありますが、クエリとなる商品に対して置き換え可能な商品候補を上位に出す傾向が強そうなことがわかります。
Item2Vec は Word2Vec の仕組みを転用しているため、周辺の商品から出現しそうな商品を予測することで獲得した重みを使った推薦を行っています。「私はカレーが好きです」という文章があったら、カレーの代わりにシチューなどが単語として入り得ますが、そうしたときにカレー・シチューが単語として近いベクトルを持つようになるといったイメージです。
Item2Vec ではこれがどうなるかというと、「ブロッコリー・ささみ・ピーマン」という商品履歴があった際に、これが「ブロッコリー・鶏むね肉・ピーマン」となってもタンパク質を重視したヘルシーな商品の並びという意図は変化しなさそうです。このようなときに、ささみと鶏むね肉は近いベクトルを持つことになります。こうしたことが起きていると考えると、Item2Vec の推薦傾向が、行動データを通じて似ているアイテムを出す、というものであることが納得できるかと思います。
RecVAE の内部でどのような学習がなされているか、Item2Vec よりも学習の仕組みが複雑であるため詳細に考察を述べることは難しいのですが、VAE によって商品履歴を復元する過程で「この商品にフラグが立っていたらこちらにもフラグが立つだろう」という推論がうまく行えているのだろうと思います。
レコメンドのアーキテクチャ
この結果を受けて現在レコメンドモデルは RecVAE (in RecBole) となっています。アーキテクチャは以下のとおりです。
- レコメンドは日次バッチで更新されます。一連の流れは Kuroko2 という社内ジョブ管理システムによってキックされます
- まず Redshift から 3ヶ月分のカート追加イベントを取得します (Queuery を使用して取得します)
- Hako によって定義された ECS 環境が立ち上がり、カート追加イベントを受け取って RecBole を使った RecVAE の学習が行われます
- およそ 3時間程度で学習が完了します
- 全アイテムに対するレコメンドリストを出力し、S3 に格納します
- このとき、オフラインテストのメトリクス群も取得し、S3 に保存します
- オフラインテストのメトリクスは社内ツールの
Metrics Tracer
というメトリクス監視ツールに取り込まれます。このツールを使って機械学習モデルに関する簡単な監視をしています
- レコメンドリストをクックパッドマートの DB に取り込みます
- 最終的に Backend サービスから API でレコメンドが配信され、アプリに表示されます
RecVAE (in RecBole) に変えてみて
オフライン・オンラインでの評価でいずれも良い成績を見せた RecVAE ですが、実際のコンバージョンにも大きく寄与しています。
変更後、それまでの水準から比較するとおよそ4,5倍ほどレコメンド経由でのカート追加数が増加しました。 その分学習にかかる時間や計算リソース、学習コードの複雑さは若干増しましたが、それを上回るメリットが得られたなと感じています。
今回のレコメンドでは期待されるものが Item2Vec ではなく RecVAE という結果になりましたが、「この商品に関連している商品もどうですか」などといった関連商品を表示する文言とともに表示される枠であれば Item2Vec もシチュエーションに適合して良い成績を残したと考えられます。
RecBole で実装したおかげで追加されるモデルとの比較実験を行う環境が簡単に用意できるので、今後もオフラインテストのメトリクスを監視しつつ新しいモデルをどんどん試していこうと思います。
まとめ
本記事ではクックパッドマートにおける item-to-item レコメンデーションの変遷の概要をお伝えさせていただきました。 様々な試行錯誤を経てクックパッドマートにおけるレコメンドの重要性は強くなっています。また、レコメンド以外にもクックパッドマートには様々な機械学習の技術が用いられており、日々進化しています。
この記事を読んでいただきありがとうございました。 機械学習の技術をプロダクトで活かしたい方がいらっしゃいましたら、ぜひ新卒・中途採用にご応募ください。