はじめに
こんにちは、DMMで機械学習エンジニアをしている二見です。 2020年に新卒で入社し、普段は検索やレコメンドの改善に従事しています。
DMMでは50を超えるサービスを提供しており、様々なデジタルコンテンツを取り扱っています。 また、各サービスには多くのユーザーの膨大なデータが日々蓄積されてます。 我々のグループでは、それらのビックデータを活用した改善施策を行うことで、各事業部の成長に日々貢献しております。
本記事では、新たにディープニューラルネットワークベースの先端的なレコメンドシステムを構築し、サービスに導入した事例を紹介したいと思います。 レコメンドエンジンの導入を検討している方・ニューラルネットベースのレコメンドに興味がある方の参考になれば幸いです。
目次
DMMにおけるレコメンドへの取り組み
我々のグループでは、ユーザーの購買促進のためにレコメンドを各サービスに提供しています。 弊グループではEKSクラスター上にレコメンド開発基盤を構築しており、データの前処理からモデルの学習・推論までをバッチ処理することで、日々レコメンドを生成しています。
本記事で取り扱うレコメンドは「あなたへのおすすめ」としてユーザーごとに作品を推薦するのものです。 これらのレコメンドにはユーザーの閲覧ログや購買ログといったImplicit Feedbackを学習データとして用いております。 従来のレコメンドはSparkベースのレコメンドになっており、Spark上で利用できる機械学習ライブラリであるSparkMLのALSを採用したレコメンドとなっています。
背景
これまで弊グループが運用していたレコメンドには課題がありました。 それはレコメンドアルゴリズムがSparkMLに依存しており改善策の幅が狭まっていたことです。 Sparkは非常に優れた分散処理フレームワークではありますが、SparkMLで利用できるアルゴリズムには限りがあります。 これまではレコメンドの実装手段がSparkMLのみであったため、選択肢も狭まり新しい改善案を出すことが難しい状況になっていました。
一方で近年のトレンドとして、画像認識や自然言語処理で幅広く活用されているディープニューラルネットワークがレコメンド分野にも応用されつつあります。 ニューラルネットベースのアルゴリズムだと、さまざまなデータを柔軟に取り扱うことができるのも大きなメリットです。
これらの背景から、今回新しい枠用に用意したレコメンドアルゴリズムではEKS Cluster上にGPUを動かせる環境を作り、Tensorflow Recommendersを利用したディープニューラルネットベースのレコメンドアルゴリズムを構築することにしました。
Tensorflow Recommendersとは
Tensorflow RecommendersとはTensorflowをベースとしたレコメンドシステムの構築に特化したライブラリです。 tf.kerasをベースとしてレコメンドモデルを簡潔に定義でき、複雑なモデルも柔軟に構築できます。
Tensorflow Recommendersを利用するメリットとして、以下の2つが挙げられます。
1. Tensorflowベースで扱いやすい
1つはTensorflowベースでモデルを定義でき、非常に扱いやすい点です。 特にtf.kerasだとモデルへ入力するために必要な前処理もレイヤーとして定義でき、生データを入力としたend-to-endでのモデルの構築が可能です。
前処理レイヤーを使用する | TensorFlow Core
Tensorflow Recommendersのチュートリアルにあるように、user_idの埋め込みモデルも以下のようにして簡単に定義できます。
user_model = tf.keras.Sequential([ tf.keras.layers.StringLookup( vocabulary=unique_user_ids, mask_token=None), tf.keras.layers.Embedding(len(unique_user_ids) + 1, embedding_dimension) ])
2. ScaNNが使える
2つ目はScaNNによる効率的なベクトル類似検索ができることです。 ScaNN(Scalable Nearest Neighbors)とはGoogleが発表した近似最近傍探索(ANN)手法の1つで、大規模なデータにおいても高速に類似ベクトルを探索できます。 2020年に発表されたばかりの比較的新しい手法ではありますが、Tensorflow Recommendersではtfrs.layers.factorized_top_k.ScaNNとしてAPIが提供されています。
Announcing ScaNN: Efficient Vector Similarity Search
従来のレコメンドではユーザーとアイテムのベクトルからナイーブな方法によって全ての組み合わせの計算を行い、レコメンドを生成しておりました。 しかし、我々の扱うサービスには非常に多くのユーザーと配信作品があるためこれらの方法では計算コストもかかり、非効率な状態でした。 これらの問題はScaNNを利用することによって、スコアリングする候補集合を絞り込むことができます。
実装で工夫したところ
今回は、Tensorflow RecommendersのチュートリアルにあるMulti-task Recommendersをベースに実装を行いました。
実装において、特に工夫した点は学習データの入力パイプラインと推論部分です。
学習データの入力パイプライン
扱うデータが大規模なことから、弊グループではETLにはSparkを採用しており、データをparquetファイルとして保存しています。
そこで、parquetファイルをtf.data.Dataset
で読み込むためにpetastormを用いることにしました。
petastormはUber製のライブラリで、parquet形式のデータセットをTensorFlow、PyTorch、PySparkで扱うことができます。
from petastorm import make_batch_reader from petastorm.tf_utils import make_petastorm_dataset with make_batch_reader(dataset_path) as train_reader: train_dataset = make_petastorm_dataset(train_reader)\ .unbatch()\ .map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)\ .shuffle(train_size)\ .batch(hparams['batch_size'])\ .prefetch(buffer_size=tf.data.AUTOTUNE)\ .cache() model.fit(train_dataset, epochs=10)
上記のようにしてtf.data.Dataset
形式でparquetファイルを読み込むことができます。
また、tf.data.Dataset
を扱う際には以下の点で注意が必要です。大規模なデータではパフォーマンスに大きく影響します。
- map()を行う際には
num_parallel_calls
を指定し、非同期で並列処理させる - prefetch()で次のステップで入力するデータを準備しておく
いずれも、tf.data.AUTOTUNE
を指定してあげることで値を動的に調整してくれます。
tf.dataのパフォーマンスについてより詳細を知りたい方はぜひ、ドキュメントを一読しておくことをおすすめします。
推論
次に推論部分ですが、推論も含めすべてSparkクラスタ上で完結させたかったため、以下のようなpandas_udfを定義し、分散推論を行っています。
from typing import Iterator import pandas as pd import pyspark.sql.functions as F import pyspark.sql.types as T def predict_func(model_path): @F.pandas_udf(T.StructType([ T.StructField("items", T.ArrayType(T.StringType()), True), T.StructField("scores", T.ArrayType(T.FloatType()), True), ])) def predict(iter: Iterator[pd.Series]) -> Iterator[pd.DataFrame]: import tensorflow as tf import tensorflow_recommenders as tfrs model = tf.saved_model.load(model_path) for x in iter: y = model.predict(x) yield pd.DataFrame({ 'items': y['items'].numpy().tolist(), 'scores': y['scores'].numpy().tolist(), }) return predict
user_id
を入力として、item_id
とそのscore
が返ってくるモデルを定義しておき、saved_model形式に保存しておきます。
以下のようなuser_idを含むテーブルがあった場合には、このようにして推論できます。
df.printSchema() df.show() """ root |-- user_id: string (nullable = true) +-------+ |user_id| +-------+ | user_A| | user_B| | user_C| +-------+ """ df = df.withColumn('y', predict_func(model_path)('user_id'))\ .withColumn('exploded', F.explode(F.arrays_zip(F.col('y.items'), F.col('y.scores'))))\ .select(F.col('user_id'), F.col('exploded.0').alias('item_id'), F.col('exploded.1').alias('score'))\ df.printSchema() df.show() """ root |-- user_id: string (nullable = true) |-- item_id: string (nullable = true) |-- score: float (nullable = true) +---------+---------+-------+ | user_id| item_id| score| +---------+---------+-------+ | user_A| item_a| 3.43| | user_A| item_b| 2.36| | user_B| item_c| 6.34| | user_B| item_a| 3.27| | user_C| item_b| 6.87| | user_C| item_e| 5.84| +---------+---------+-------+ """
これらの推論結果に対して購買済みの作品を除くなど細かな後処理を施し、各ユーザーごとのレコメンドを生成しています。
まとめ
今回はTensorflow Recommendersを活用して、既存のレコメンドから一新したニューラルネットワークベースのレコメンドの実装例をご紹介しました。 本記事ではユーザーに作品を推薦する、User2Itemレコメンドの改善に焦点を当てました。 Item2Itemレコメンドのような作品から作品を推薦する場合にでも、今回のようにTensorflow Recommendersを活用してアルゴリズムの一新ができると考えています。 User2Itemレコメンドについても、ユーザー特徴量やアイテムのコンテキスト特徴量など加えることでさらに改善できる余地もあるかと思います。
また、弊グループが提供しているレコメンドはDMMのサービスの様々な箇所にされております。 各サービスによってドメインが大きく異なるため、サービスそれぞれにフィットしたレコメンドが求められます。 今回の事例を参考に、今後もよりよいレコメンド体験をユーザーに提供できればと思います。
おわりに
今回は、新たにTensorflow Recommendersを活用してレコメンドアルゴリズムを一新した話をしました。 レコメンドは大規模なデータになるほど扱いが難しくなりますが、Tensorflow Recommendersはそれらの障壁を取り除く様々な機能が実装されています。 また、Tensorflow Recommendersは2020年にリリースされたばかりなので今後の新たな機能にも注目していきたいです。
最後に、DMM Groupでは一緒に働いてくれる仲間を募集しています。ご興味のある方はぜひ下記の募集ページをご確認ください!!
データサイエンスを駆使した事業成長を手がける「Growth Scienceグループ」の正体とは? - DMM inside