Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Transformers4Recで簡単にセッションベースなレコメンデーションを試してみた

アクロクエスアドベントカレンダー 12月14日 の記事です。
こんにちは。最近テニス熱が再燃している@Ssk1029Takashiです。

深層学習の界隈はここ最近はTransformerアーキテクチャが様々な分野で高い精度を出しています。
そんな中NVIDIAから、Transformerを使ってセッションベースのレコメンドを学習できるTransformers4Recというライブラリがリリースされています。
github.com
簡単に精度が高いレコメンドが試せるライブラリとのことなので、チュートリアルをベースに試してみました。

ブログの内容は以下になります。
注意点として、ライブラリの使い方に主眼を置いているので、モデルの詳細な中身や前処理の具体的なコードの説明はこの記事では説明していません。

セッションベースのレコメンデーションとは

ライブラリの話に入る前に、そもそもセッションベースのレコメンデーションとは何かを説明します。

ここでいうセッションとは、ユーザーがサイトの操作を開始して、何か商品を購入するまでにどの商品を見たかを指します。
例えば、ECサイトで一つの服を買うまでに、帽子を見たり、違うブランドの服を見たりしてから、最終的には購入に至るというのが一般的なユーザーの行動かと思います。

レコメンドでは一般的にはユーザーに焦点を当てて、行動履歴をとり、そのユーザー、もしくは属性が近いユーザーがよく見るものをレコメンドします。
ただし、この手法は問題が指摘されており、ユーザーも時間が経てば見たいもの・買いたいものも全く変わるので、欲しくないアイテムがレコメンドされるケースも出てしまいます。
例えば、テレビのような家電はそんなに頻繁に買わないのに、一回見ただけでやたらとおすすめされて煩わしいという経験がある人も多いかと思います。

https://developer.nvidia.com/blog/transformers4rec-building-session-based-recommendations-with-an-nvidia-merlin-library/

この問題を解決するために、セッションベースレコメンデーションでは、ユーザーの履歴ではなく、セッション内の行動によってのみ、次にレコメンドする商品を決めています。
これにより、短期的にユーザーがどのようなものが欲しいかをより正確にできるようにするというのが利点になります。

Transformers4Recとは

NVIDIAから出ているTransformerベースのモデルでセッションベースのレコメンドを高精度で実現できるライブラリです。
公式サイトの説明によると以下が主なうれしいポイントのようです。

  1. PyTorchとTF kerasでのカスタムモデル定義に対応しているので、どちらかしか使えないという人もモデルの拡張ができる
  2. HuggingFace上のTransformerモデル(XLNet、GPT-2など)をライブラリから利用できるため、複数モデルの検証が楽
  3. ライブラリの機能でNVIDIA Triton Serverというリアルタイム推論サーバーを簡単に立てられるので、デプロイが簡単

より詳細には以下のページに書いてあるので、ぜひ読んでみてください。
developer.nvidia.com

実際に使ってみる

Transformers4Recは以下のページにExampleが公開されているので、こちらに沿って試してみます。
nvidia-merlin.github.io

検証ではRecsys Challange 2015で使用されたyoochooseのデータセットを使いました
データにはとある小売業者のECサイトでのユーザーがセッションごとのどの商品をクリックしたかという履歴が含まれています。

具体的には以下のようにセッションID、日時、商品ID、商品カテゴリが含まれています。

データ例

実行環境の立ち上げ

当記事で検証時の環境は以下になります

項目 内容
OS Ubuntu 20.04 LTX
GPU RTX3090
メモリ 128GB

nvidia-dockerから以下のコマンドで実行用のコンテナを立ち上げます。

docker run --gpus all  --name transformers4rec  -it -p 8888:8888 -p 8797:8787 -p 8796:8786 --ipc=host --cap-add SYS_NICE nvcr.io/nvidia/merlin/merlin-pytorch:22.11 /bin/bash

そのあとコンテナ内でJupyter Labを立ち上げれば実行環境の構築は完了です。

cd / ; jupyter-lab --allow-root --ip='0.0.0.0' --NotebookApp.token=''

学習データ準備

このままだと学習データとして利用できないので、前処理します。
実行する内容は以下の流れになります。

  1. 同一セッション内で同一商品が連続しているデータをまとめる
  2. セッションID・日時でソートする
  3. 各履歴ごとに特徴量を計算して置き換える

計算する特徴量は以下の2つになります。

  • 週のうち何日目に発生したイベントかを正規化した値(et_dayofweek_sin-list_seq)
  • どれくらい新しい新しい商品か(product_recency_days_log_norm-list_seq)

データ処理の実装自体は以下のページに乗っているので、重要な箇所以外はこの記事では省略するので参照してください。
nvidia-merlin.github.io

最終的にデータの形式は以下のようにセッションごとに一行のデータになります。

データ変換後のテーブル

ここまで実行した前処理は以下のコードでNVTablarの機能で処理内容を保存することができます。

workflow.save('workflow_etl')

今回はあくまで検証用なので、使用する日付のデータを区切って、学習・評価・テスト用にデータを分割します。

from transformers4rec.data.preprocessing import save_time_based_splits
sessions_gdf = sessions_gdf[sessions_gdf.day_index>=178]
save_time_based_splits(data=nvt.Dataset(sessions_gdf),
                       output_dir= "./preproc_sessions_by_day",
                       partition_col='day_index',
                       timestamp_col='session_id', 
                      )

上記を実行すると以下のように日付と学習・評価・テストでデータを分けてparquet形式で保存されます。

preproc_sessions_by_day/
    179/
        valid.parquet
        test.parquet
        train.parquet
    182/
        valid.parquet
        test.parquet
        train.parquet
    181/
        valid.parquet
        test.parquet
        train.parquet
    178/
        valid.parquet
        test.parquet
        train.parquet
    180/
        valid.parquet
        test.parquet
        train.parquet

学習

ここまででデータはそろえられたので、学習を実行しましょう。

以下のコードでインプットのデータ形式を定義します。

from merlin_standard_lib import Schema

SCHEMA_PATH = "schema_demo.pb"
schema = Schema().from_proto_text(SCHEMA_PATH)
schema = schema.select_by_name(
   ['item_id-list_seq', 'category-list_seq', 'product_recency_days_log_norm-list_seq', 'et_dayofweek_sin-list_seq']
)

上記のコードで出てくるschema_demo.pbというのは、protocol buffers形式で以下の内容を記載したファイルになります。
内容としては各カラムごとのカラム名データ形式を定義しています。

feature {
  name: "session_id"
  type: INT
  int_domain {
    name: "session_id"
    min: 1
    max: 9249733 
    is_categorical: false
  }
  annotation {
    tag: "groupby_col"
  }
}
feature {
  name: "item_id-list_seq"
  value_count {
    min: 2
    max: 185
  }
  type: INT
  int_domain {
    name: "item_id/list"
    min: 1
    max: 52742
    is_categorical: true
  }
  annotation {
    tag: "item_id"
    tag: "list"
    tag: "categorical"
    tag: "item"
  }
}
feature {
  name: "category-list_seq"
  value_count {
    min: 2
    max: 185
  }
  type: INT
  int_domain {
    name: "category-list_seq"
    min: 1
    max: 337
    is_categorical: true
  }
  annotation {
    tag: "list"
    tag: "categorical"
    tag: "item"
  }
}
feature {
  name: "product_recency_days_log_norm-list_seq"
  value_count {
    min: 2
    max: 185
  }
  type: FLOAT
  float_domain {
    name: "product_recency_days_log_norm-list_seq"
    min: -2.9177291
    max: 1.5231701
  }
  annotation {
    tag: "continuous"
    tag: "list"
  }
}
feature {
  name: "et_dayofweek_sin-list_seq"
  value_count {
    min: 2
    max: 185
  }
  type: FLOAT
  float_domain {
    name: "et_dayofweek_sin-list_seq"
    min: 0.7421683
    max: 0.9995285
  }
  annotation {
    tag: "continuous"
    tag: "time"
    tag: "list"
  }
}

インプットを定義したら、次は学習するモデルを定義します。

from transformers4rec import torch as tr

max_sequence_length, d_model = 20, 320
# Define input module to process tabular input-features and to prepare masked inputs
input_module = tr.TabularSequenceFeatures.from_schema(
    schema,
    max_sequence_length=max_sequence_length,
    continuous_projection=64,
    aggregation="concat",
    d_output=d_model,
    masking="mlm",
)

# Define Next item prediction-task 
prediction_task = tr.NextItemPredictionTask(hf_format=True, weight_tying=True)

# Define the config of the XLNet Transformer architecture
transformer_config = tr.XLNetConfig.build(
    d_model=d_model, n_head=8, n_layer=2, total_seq_length=max_sequence_length
)

# Get the end-to-end model 
model = transformer_config.to_torch_model(input_module, prediction_task)

ここで注意なのは、公式exampleのnotebookにはtr.NextItemPredictionTaskメソッドの引数にhf_format=Trueは定義されていませんが、この定義がないとエラーになります。
どうやら最新版のnvidia-merlinのDockerイメージを使用すると発生する問題のようですが、はまってしまいました。。

後は学習のハイパーパラメータなどの設定をして、学習・評価します。

recsys_trainer = tr.Trainer(
    model=model,
    args=training_args,
    schema=schema,
    compute_metrics=True)
from transformers4rec.torch.utils.examples_utils import fit_and_evaluate
OT_results = fit_and_evaluate(recsys_trainer, start_time_index=178, end_time_index=180, input_dir='./preproc_sessions_by_day')

こちらの処理が環境すると以下のように、評価結果が分かります。

{'indexed_by_time_eval_/next-item/avg_precision@10': [0.08110713958740234,
  0.06632552295923233,
  0.13616926968097687],
 'indexed_by_time_eval_/next-item/avg_precision@20': [0.08427499234676361,
  0.0700748860836029,
  0.143798828125],
 'indexed_by_time_eval_/next-item/ndcg@10': [0.10763730853796005,
  0.08923665434122086,
  0.17738889157772064],
 'indexed_by_time_eval_/next-item/ndcg@20': [0.11937256157398224,
  0.10299879312515259,
  0.20573727786540985],
 'indexed_by_time_eval_/next-item/recall@10': [0.19306358695030212,
  0.1627039611339569,
  0.30797773599624634],
 'indexed_by_time_eval_/next-item/recall@20': [0.23969171941280365,
  0.21724942326545715,
  0.4220779240131378]}

今回はデータを絞ったのもあり精度としては伸びしろがありますが、それでもrecall@10で0.2付近が出ているのはさすがですね。

ここまで出来たら、作成したモデルと前処理のworkflowをまとめて保存します。
ここで保存したモデルとworkflowは推論時に使用します。

from nvtabular.inference.triton import export_pytorch_ensemble
from nvtabular.workflow import Workflow
workflow = Workflow.load("workflow_etl")

export_pytorch_ensemble(
    model,
    workflow,
    sparse_max=recsys_trainer.get_train_dataloader().dataset.sparse_max,
    name= "t4r_pytorch",
    model_path= "/workspace/TF4Rec/models/",
    label_columns =[],
)

推論

推論にはNVIDIAのライブラリであるNVIDIA triton inference serverを使用します。
詳細な説明は省略しますが、フレームワークを問わず学習済みモデルをロードして、gRPC・HTTPリクエスト経由で推論ができるライブラリです。
詳細には以下のページを参照してください。
developer.nvidia.com

推論用サーバーを立ち上げる

まずは推論用のtriton serverを立ち上げます。
今Dockerコンテナを起動しているシェルとは別のシェルを開いたのち、以下の手順で今回のコードを実行しているコンテナの中からtriton severを立ち上げます。

docker exec -it transformers4rec /bin/bash
tritonserver --model-repository=/workspace/TF4Rec/models/ --model-control-mode=explicit
推論を実行する

まずは、立ち上げているtriton serverに接続するためのクライアントを作成します。

import tritonhttpclient
try:
    triton_client = tritonhttpclient.InferenceServerClient(url="localhost:8000", verbose=True)
    print("client created.")
except Exception as e:
    print("channel creation failed: " + str(e))
triton_client.is_server_live()

クライアントを作成した後は、triton serverにモデルを学習モデルを読み込むようにリクエストを送信ます。

triton_client.load_model(model_name="t4r_pytorch")
triton_client.get_model_repository_index()

モデルを読み込めたら、テスト用のデータを送信して推論してみます。
ここで、ポイントなのは推論時にクライアント側で前処理の必要がないということです。
先ほど、モデルを保存するときに一緒に前処理のworkflowも保存しているので、サーバー側で前処理から推論までend-to-endで実行してくれます。

import pandas as pd
interactions_merged_df = pd.read_parquet("/workspace/data/interactions_merged_df.parquet")
interactions_merged_df = interactions_merged_df.sort_values('timestamp')
batch = interactions_merged_df[-50:]
sessions_to_use = batch.session_id.value_counts()
filtered_batch = batch[batch.session_id.isin(sessions_to_use[sessions_to_use.values>1].index.values)]
import nvtabular.inference.triton as nvt_triton
import tritonclient.grpc as grpcclient

inputs = nvt_triton.convert_df_to_triton_input(filtered_batch.columns, filtered_batch, grpcclient.InferInput)

output_names = ["output"]

outputs = []
for col in output_names:
    outputs.append(grpcclient.InferRequestedOutput(col))
    
MODEL_NAME_NVT = "t4r_pytorch"

with grpcclient.InferenceServerClient("localhost:8001") as client:
    response = client.infer(MODEL_NAME_NVT, inputs)
    print(col, ':\n', response.as_numpy(col))

この状態で出力される値は、セッションごとの各アイテムの推薦度のlogitsが出力されているので、以下のコードでitem_idに直した形で出力します。

from transformers4rec.torch.utils.examples_utils import visualize_response
visualize_response(filtered_batch, response, top_k=5, session_col='session_id')

以下の出力が得られればOKです。

- Top-5 predictions for session `11457123`: 3170 || 429 || 1301 || 70 || 2909

- Top-5 predictions for session `11467406`: 475 || 1216 || 1085 || 1672 || 597

- Top-5 predictions for session `11528554`: 999 || 166 || 1672 || 1157 || 33

- Top-5 predictions for session `11336059`: 1672 || 206 || 184 || 1157 || 33

- Top-5 predictions for session `11445777`: 1672 || 289 || 597 || 2707 || 33

- Top-5 predictions for session `11493827`: 206 || 30 || 61 || 2 || 69

- Top-5 predictions for session `11425751`: 800 || 1157 || 166 || 302 || 429

- Top-5 predictions for session `11399751`: 475 || 1672 || 33 || 597 || 1216

- Top-5 predictions for session `11311424`: 27 || 70 || 33 || 1348 || 3170

- Top-5 predictions for session `11257991`: 2034 || 997 || 800 || 429 || 1157

- Top-5 predictions for session `11561822`: 555 || 423 || 1672 || 3225 || 33

- Top-5 predictions for session `11421333`: 800 || 445 || 1157 || 2034 || 429

- Top-5 predictions for session `11270119`: 1569 || 61 || 597 || 1672 || 1216

- Top-5 predictions for session `11401481`: 1672 || 61 || 1219 || 848 || 423

- Top-5 predictions for session `11394056`: 206 || 61 || 69 || 2 || 313

このように推論対象にした各セッションごとに次にお勧めするTop5を得ることができました。

まとめ

この記事ではexampleを題材にして、Transformers4Recの大まかな使い方を見ていきました。
使ってみてよかった点は以下になります。

  1. モデルの定義が標準を使う分には楽
  2. 学習時に実施した前処理を保存して、推論時に前処理→推論を推論サーバー上でend-to-endで実行できるのは手間が減って楽

というのが特によかったです。
ただ、ドキュメントがまだ整備され切っていないところもあるので、カスタマイズが必要になると手探りになる部分もあります。

なので、まず簡単にセッションベースのレコメンデーションを試すときにはTransformers4Recを使ってみるのもよさそうです。
それではまた。

Acroquest Technologyでは、キャリア採用を行っています。


  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。


www.wantedly.com