#MIRU2022 参加レポート

MIRU2022参加レポート

こんにちは。ZOZO ResearchのResearcherの平川と古澤です。2022年7月25日(月)から7月28日(木)にかけて画像の認識・理解シンポジウムMIRU2022に参加しました。この記事では、MIRU2022でのZOZO Researchのメンバーの取り組みやMIRU2022の様子について報告します。

目次

MIRU2022

MIRUとは、Meeting on Image Recognition and Understandingという画像の認識・理解についてのシンポジウムです。2022年の今回はアクリエひめじ(姫路市文化コンベンションセンター)においてオフラインとオンラインのハイブリッド形式で開催されました。数年ぶりに現地参加も可能ということで1243名の方々が参加されたそうです。ZOZO NEXTは、このMIRU2022にゴールドスポンサーとして協賛させていただきました。

sites.google.com

企業展示

企業展示ブースの様子

企業展示ブースの様子

企業展示ブースでは、ZOZO Researchにおける取り組みについてポスターを用いて紹介しました。ZOZOの多角的なファッションサービスとそこから得られる情報資産を活用した研究事例について紹介させていただきました。大変うれしいことに多くの方々に興味を持っていただき、お話をさせていただくことができました。ブースまで足を運んでくださった皆さま、誠にありがとうございました。展示していたポスターはこちらです。

インタラクティブセッション

ZOZO Researchからはロングオーラル1件とインタラクティブセッション2件の計3件を発表しました。以下に、各研究のサマリーを示します。

[OL3B-3]条件付き集合変換を用いたファッションコーディネートの補完 (ロングオーラル)

中村 拓磨、斎藤 侑輝 (ZOZO Research)
ファッションコーディネート補完問題は、複数の衣服やアクセサリーの組み合わせからなるファッションコーディネートを推薦する技術を実現するための重要な課題として知られています。コーディネート補完問題は、完成したコーディネートに対する評価値計算を前提とする従来手法を用いる場合、補完候補アイテム集合から評価値が最大になるアイテムの組み合わせを探索する問題に帰着します[1][2]。しかしながら、補完候補アイテム集合の要素数が増大するにつれて、探索コストが増大するという課題があります。そこで、本研究ではコーディネート補完問題を指定の条件下における集合検索問題として定式化し、入力アイテム集合と補完候補アイテム集合の属性を反映した特徴量を生成可能なモデルとその学習手法を提案しました。提案手法は指定の条件下で入力アイテムの集合と相補的なアイテム集合を直接的に予測可能であるため、探索空間の増大に伴い推論時の計算量が増大する問題を原理的に解決するアプローチと言えます。実データを用いた性能比較実験では提案手法が入力アイテム集合と相補的なアイテム集合を予測できていること及び出力集合の要素の属性情報を制御可能であることを示しました。
[1] Cucurull, Guillem, Perouz Taslakian, and David Vazquez. "Context-aware visual compatibility prediction." Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2019.
[2] Saito, Y., Nakamura, T., Hachiya, H. and Fukumizu, K.: Exchangeable Deep Neural Networks for Set-to-Set Matching and Learning, ECCV2020: Proceedings, Part XVII, p. 626‒646. 2020.

[IS3-27]ファッション推薦問題に向けた階層的集合マッチングモデルの検討

長瀬准平(ZOZO Research, 芝浦工大)、斎藤侑輝(ZOZO Research)、石渡哲哉(芝浦工大)
ファッションコーディネート間のマッチング問題はファッションに関する様々な推薦タスクへの応用が期待される重要な問題です。コーディネートは複数のファッションアイテムからなる集合と見なせますが[2]、本研究ではコーディネートの集合間のマッチング問題を新たに提起し、階層的な相互作用を考慮した深層学習モデルを提案しました。提案手法は、置換不変な特徴量抽出器であるCrossSimilarity関数[2]を拡張した、コーディネートとアイテムという異なる階層の相互作用を考慮可能な階層的集合マッチングモデルから成ります。実データを用いた比較検証実験では従来手法と比較してマッチング精度が改善することを確認しました。
[2] Saito, Y., Nakamura, T., Hachiya, H. and Fukumizu, K.: Exchangeable Deep Neural Networks for Set-to-Set Matching and Learning, ECCV2020: Proceedings, Part XVII, p. 626‒646. 2020.

[IS3-55]身体と衣服の採寸情報を考慮する仮想試着のためのレイアウト生成モデルの検討

後藤 亮介、中村 拓磨 (ZOZO Research)
オンラインショッピングにおいては購入以前に衣服のサイズを正確に把握することが困難であるという課題があります。近年では画像ベース仮想試着技術の研究が盛んに行われていますが着用者の体型や衣服のサイズを明示的に考慮した研究がなされていないのが現状です[3][4][5]。本研究では、ZOZOTOWNのデータから着用者の身長と衣服の寸法を含むデータセットを構築し、衣服と身体の寸法を明示的に考慮したレイアウト生成モデルのベースラインモデルを学習しました。更に、身長や着丈の情報を反映できていることを確認するための定量評価指標を提案しました。
[3] Han, Xintong, et al. “Viton: An image-based virtual try-on network.” Proceedings of the IEEE conference on computer vision and pattern recognition. 2018
[4] Choi, Seunghwan, et al. “Viton-hd: High-resolution virtual try-on via misalignment-aware normalization.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2021
[5] Neuberger, Assaf, et al. “Image based virtual try-on network from unpaired data.” Proceedings of the IEEE/CVF Conference on Computer Vision and Pattern Recognition. 2020.

気になった研究発表

私たちが個人的に興味を持った研究について紹介します。

[OL1B-4] Cross-Modal Recipe Embeddingを用いたマスクに基づく食事画像生成

陳 仲涛、本部 勇真、楊 景、柳井 啓司 (電気通信大学)
敵対的生成ネットワークによる画像生成の技術の進展により、リアルな画像の生成が可能となってきています。一方で、料理画像を生成するタスクでは、料理は食材だけでなく作り方や盛り付けによっても見た目や形が大きく変わるという特性上、レシピや盛り付けを反映したリアルな料理画像の生成が難しかったそうです。この研究では、レシピテキストと料理領域から料理を料理領域に盛り付けた画像を生成するMask-based Recipe Embedding GAN (MRE-GAN)を提案されていました。レシピテキストから料理画像を生成するRDE-GANというモデルとセマンティック領域適応正規化を組みわせることで、より安定的に学習ができるようになったとのことです。実験結果として、スープのレシピから枝豆を抜くと生成された料理画像から枝豆が消えるという面白い事例を示されておりました。ファッションの分野でも仮想試着などの文脈で画像生成が注目を集めています。個人的には料理の調理方法というのは洋服の着こなしとも対応しているのかなと感じました。将来的にそういった研究もできると面白そうです。

[OL2A-2] 深層モデルの汎化性能改善を目的とした特徴抽出器の事後学習

山田 陵太、佐藤 育郎、田中 正行、井上 中順、川上 玲 (東京工業大学/デンソーITラボ)
深層モデルにおいて、局所解周りの平坦さがモデルの汎化性能と関係していることが示唆されています。例えばSharpness-Aware Minimizationと呼ばれる解の平坦性まで考慮した最適化手法では、より汎化性能の高い解を得やすいということが知られています。このため局所解近傍における損失形状の平坦化が重要です。この研究では、既に充分に学習された深層モデルを初期状態として、そこからさらに平坦な局所解を探索できる事後学習法を提案されていました。提案手法では、まず深層モデルの前半と後半を抽出器と識別器にわけ、識別器側にミニバッチ損失を最小化するような摂動を加えていました。その後、摂動された識別器の平均を最小化するように、さらに抽出器のパラメータを最適化されていました。このように構築されたモデルを評価すると、4つのうち3つのデータセットにおいて、Sharpness-Aware Minimizationによって学習されたモデルを事後的に性能改善できたそうです。一般の学習済み深層学習モデルの汎化性能を向上させられる最適化手法という点で非常に興味深いと感じました。摂動のハイパーパラメータを訓練データとテストデータのミニバッチ損失最小解までの距離から決定されていましたが、個人的にはこのパラメータがデータセットごとにどの程度変わるかという点にも興味を持ちました。

[IS3-73] 布生地の風合いの画像認識

鈴木大智、相澤清晴(東京大学)
ECサイトで衣服を販売する際の課題として衣服に使用されている生地の風合いを伝えることが困難であるという点が挙げられます。衣服の手触りに関する先行研究では手触りとの相関が強い物理特性として、生地の厚さ、柔らかさ、粗さが挙げられているそうです。こちらの研究ではKES(Kawabata Evaluation System)と呼ばれる計測機器を用いて生地の物理特性を測定し、衣服の表面画像から生地の厚さ、柔らかさ、粗さを予測モデルを構築するためのデータセットが提案されています。個人的には衣服の表面画像から計測に長時間を要する衣服の物理特性を予測できるという点が非常に興味深いと思いました。将来的には手触りの情報と仮想試着技術を組み合わせることにより、より実店舗に近いユーザ体験を実現できるかもしれませんね。

[OL3A-3] Database-adaptive transfer learning for question answering-based re-ranking in cross-modal retrieval

Rintaro Yanagi, Ren Togo, Takahiro Ogawa, Miki Haseyama (北海道大学)
ECサイトにおいてキーワード検索の精度はユーザー体験を左右する重要な要素です。画像のキャプションと検索キーワードのテキストベースのマッチンングによるアプローチでは、検索対象の画像へのキャプション付与が必須であるという課題があります。近年ではキャプション付与のコストを軽減するアプローチとして画像と検索キーワードを同一空間に埋め込む手法が提案されていますが、類似画像や曖昧な検索キーワードに対する頑健性には改善の余地があります。こちらの研究では、システムがユーザーに対して質問を生成することにより、検索キーワードに含まれる情報を対話的に補完するシステムが提案されています。ZOZOTOWNの商品検索においても、システムがユーザーの意図を汲み取って対話的に商品検索する仕組みを導入すれば、より良いユーザー体験を実現できるかもしれません。

オフライン参加によって得られた気付き

初日のチュートリアルでは、Transformer誕生の歴史や複数画像から3次元自由視点画像を生成する技術など、近年のコンピュータービジョン分野のトレンドを俯瞰的に知ることができて大変勉強になりました。MIRU2022に投稿された論文はCLIP、自己教師あり学習、ドメイン適用、NeRFがキーワードになる研究が多かったように感じました。インタラクティブセッションでは著者とのディスカッションを通じて弊社のサービスに活かせそうなアイデアを得ることもできました。MIRU2022では様々な研究者や学生の方々と直接オフラインで議論する機会も沢山あり、個人的にはオンライン参加よりも実りの多い時間を過ごせたと思います。

最後に

ZOZO NEXTでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できるエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。

zozonext.com

おまけ

学会の合間に明石焼き風たこ焼きを食べに行きました。

学会の合間に食べた明石焼き風たこ焼き

ご満悦。

MIRU2022を堪能できてご満悦の表情

ZOZOTOWNホーム画面におけるパーソナライズの取り組み

OGP

はじめに

こんにちは、ML・データ部推薦基盤ブロックの寺崎(@f6wbl6)と佐藤(@rayuron)です。

ZOZOTOWNのホーム画面は2021年3月にリニューアルされ、「モジュール」と呼ばれる単位で商品が表示されるようになりました。

本記事ではユーザーごとにパーソナライズされたモジュール(以降、パーソナライズモジュール)のロジックやシステム構成、および導入時に実施したA/Bテストの内容と結果をご紹介します。

先に結論から言ってしまいますが、今回のパーソナライズモジュールでは機械学習モデルを使わず、ユーザーの回遊行動を分析した結果を元にしたルールベースのロジックを使用しています。本記事のポイントは大きく以下の3点です。

  1. ルールベースのパーソナライズロジック
  2. 機械学習モデル導入を見越したシステム設計
  3. ホーム画面のパーソナライズによる効果

本記事がこれから同様のタスクに取り組む方の参考になれば幸いです。

続きを読む

Vertex AI Pipelinesによる機械学習ワークフローの自動化

ogp

はじめに

こんにちは。検索基盤部の倉澤です。

私たちは、ZOZOTOWNの検索機能の改善に取り組んでいます。ZOZOTOWNのおすすめ順検索ではランキング学習を用いた検索機能の改善に取り組んでおり、A/Bテストにて効果を測定しています。

ランキング学習やElasticsearch Learning to Rankプラグインについては過去の記事で紹介していますので、併せてご覧ください。

techblog.zozo.com techblog.zozo.com

私たちは、機械学習モデルの開発からデプロイまでの一連の処理を実行するワークフローの構築にGoogle Cloud Platform(GCP)のVertex AI Pipelinesを利用しています。

本記事では、Vertex AI Pipelines採用前の運用とその課題点について説明し、次にVertex AI Pipelinesで構築したワークフロー概要とその運用について紹介します。

目次

Vertex AI Pipelines採用の背景

Vertex AI Pipelines採用に至った背景として、従来の運用と抱えていた課題点を紹介します。

従来の運用

vertex pipelines導入前の作業手順

Vertex AI Pipelinesを採用する以前は、GitHubで管理されているスクリプトを、開発者が各自GCPに立てたインスタンスのJupyter Notebook上で順に実行していました。

機械学習モデルの学習期間や特徴量などのパラメータは設定ファイルで管理しており、papermillでNotebookを自動実行して機械学習モデルを生成します。そして、ElasticsearchのLearning to Rankプラグインで指定された形式にモデルを変換し、手動でデプロイを行っていました。

papermillとは

Jupyter Notebookを実行するライブラリとして記載したpapermillについて簡単に説明します。

papermillは、Jupyter Notebookに定義された各セルを実行するPythonライブラリです。 実行時にパラメータを渡すことで予めセルに定義されたデフォルトのパラメータの上書きが可能です。実行されたJupyter Notebookは別名のNotebookに保存できます。

私たちは、papermillをCLIで実行していました。

papermill input.ipynb output.ipynb -f parameter.yaml

設定ファイルは以下のようにYAMLファイルとして定義できます。

# parameter.yaml
train_start_date: 20220101
train_end_date: 20220102
valid_start_date: 20220103
valid_end_date: 20220104
test_start_date: 20220105
test_end_date: 20220106
features:
  - feature1
  - feature2
  - feature3

抱えていた課題と解決策

従来の運用フローでは、モデルの数だけ同様の作業を手動で繰り返しており、以下の点を課題に感じていました。

  • 各タスクの実施作業と実施完了の確認作業の工数が多い
  • 実行前に設定ファイルの変更に対するレビューが無いので、誤りがあった場合は機械学習モデルを再度生成し直す必要がある
  • 機械学習モデル生成の一連のタスクが途中で失敗した際に、一から再実行する必要がある

これらの課題を解決するために、各タスクを依存関係通りに実行でき、さらに再実行時にはキャッシュが利用できるワークフローエンジンの導入を検討しました。

候補となるワークフローエンジンはいくつかありましたが、弊社MLOpsブロックがVertex AI Pipelinesの実行環境の整備を進めていることもあり、より導入コストが低いVertex AI Pipelinesを選びました。

Vertex AI Pipelinesの実行環境については過去の記事で紹介していますので、併せてご覧ください。

techblog.zozo.com

Vertex AI Pipelinesによるワークフローの構築

私たちが構成した機械学習モデルの開発からデプロイまでのワークフローの概要を紹介します。以下の図は、実際に運用しているVertex AI Pipelinesのコンソール画面から確認できるワークフローの全体像です。

vertex pipelinesで構築したワークフロー図

本ワークフローではおおよそ以下のことを行っております。

  1. 学習データセット生成に必要な期間のデータが揃っているかの確認
  2. 学習データセットの生成
  3. ハイパーパラメータチューニング及び最適なパラメータによる学習
  4. 評価及びオフライン評価結果の描画
  5. デプロイ判定

それぞれ順に説明します。

1. 学習データセット生成に必要な期間のデータが揃っているかの確認

学習に必要となる期間のデータが、対象のBigQueryテーブルに存在しているかの欠損チェックを行います。以下のようなAssertionクエリをコンポーネントから実行し、指定期間のデータが存在しているかを確認します。

-- check_bq_table.sql

DECLARE target_dates ARRAY<DATE>;
DECLARE x INT64 DEFAULT 1;

SET target_dates =  (
SELECT ARRAY_AGG(date ORDER BY date) FROM UNNEST(GENERATE_DATE_ARRAY('{{ start_date }}', '{{ end_date }}', INTERVAL 1 DAY) ) AS date
);

WHILE x <= ARRAY_LENGTH(target_dates) DO
  ASSERT EXISTS(
    SELECT {{ period_column }}
    FROM `{{ full_table_id }}`
    WHERE DATE({{ period_column }}) = target_dates [ORDINAL(x)]
  ) AS 'target date does not exist in this table';

  SET x = x+ 1;
END WHILE;

2. 学習データセットの生成

学習・検証・テストのデータセットの生成をします。 以下のYAMLファイルはコンポーネントの入出力を定義し、学習・検証・テストのデータセットを出力しています。

# component.yaml

name: Extract Dataset
description: Prepare train/valid/test data
inputs:
  - name: project_id
    type: String
  - name: job_name
    type: String
  - name: execute_date
    type: String

outputs:
  - name: train_valid_data
    description: train/validデータセット
    type: Dataset
  - name: test_data
    description: testデータセット
    type: Dataset

implementation:
  container:
    image: gcr.io/your_project_id/sample_component:gitsha-xxxx
    command: [
        python,
        -m, src,
        --project_id, {inputValue: project_id},
        --job_name, {inputValue: job_name},
        --execute_date, {inputValue: execute_date},

        --output_train_valid_data_path, {outputPath: train_valid_data},
        --output_test_data_path, {outputPath: test_data},
    ]

生成したデータセットはCloud Storage FUSEによってマウントされたGoogle Cloud Storageのバケットに格納され、そのパスを後続のコンポーネントへと渡しています。コマンドライン引数で定義されているoutput_train_valid_data_pathoutput_test_data_pathがこれに該当します。

実行ファイルの中でデータセットの出力先となるパスをコマンドライン引数として受け取り、データセットを保存します。その後、後続のコンポーネントにてこのパスからデータセットを読み込むという流れになります。

3. ハイパーパラメータチューニング及び最適なパラメータによる学習

前段で生成された学習データセットと検証データセットを用いてモデルのハイパーパラメータチューニングを行います。その結果出力された最適なパラメータでモデルの学習をします。

4. 評価及びオフライン評価結果の描画

テストデータセットを用いて学習済みモデルの評価をします。コンポーネント内でオフライン指標として定めているnDCGを計算します。Vertex AI Pipelinesのコンソール画面はマークダウン形式での表示が可能なので、オフライン指標の計算結果を以下のように出力しています。

また、評価時にはベースラインモデルのオフライン指標も計算し、ベースラインモデルからのアップリフト値も併せて表示しています。

オフライン指標

5. デプロイ判定

学習したモデルのオフライン指標及びベースラインモデルからのアップリフト値によって、デプロイして良いモデルなのか判定します。このデプロイ判定のコンポーネントの後にElasticsearchへモデルをアップロードするコンポーネントを用意しています。

Vertex AI Pipelines導入後の運用

A/Bテスト時には、コントロール群に適用するモデル(以下、コントロールモデル)とトリートメントモデル群に適用するモデル(以下、トリートメントモデル)をそれぞれ開発する必要があります。また、複数の実験を同時に行う場合はさらに多くのモデルが必要になります。

このA/Bテスト時のモデル開発における運用について紹介します。

A/Bテストのモデル開発時のブランチ戦略について

Vertex AI Pipelinesで利用するコンポーネントやパイプラインのソースコードなどはGitHubで管理しています。ここでは、A/Bテストで用いるコントロールモデル及びトリートメントモデル開発時のGitHubのブランチ戦略について簡単に紹介します。

コントロールモデルとトリートメントモデルのパイプラインの構成自体には基本的に大きな違いはなく、データセットを取得するSQLクエリや学習時のパラメータ値が異なります。

A/Bテストの度にトリートメントモデル用のSQLファイルや設定ファイルを新規に作成すると冗長な構造となってしまいます。そこで私たちは、ブランチごとにモデルの開発を分ける運用を採用しました。

  • main: コントロールモデルのデプロイ用ブランチ
    • コントロールモデルは定期的に学習及びデプロイされるようにスケジューリング
  • .*-abtest-treatment-[1-9]: トリートメントモデルの開発用及びデプロイ用ブランチ
    • prefixには各A/Bテストの名前がわかる任意の値を付与
    • suffixにはトリートメントモデルの数に応じて番号を付与

ブランチ戦略

トリートメントモデルのパイプラインは、開発ブランチからマージされた時にCIが実行するようにしています。 A/Bテストの結果、トリートメントモデルが勝った場合はそのブランチをmainブランチへマージし、負けた場合はそのままブランチを削除する運用にしています。

まとめ

Vertex AI Pipelinesを導入したことにより、冒頭に記載した以下の課題はおおよそ解決しました。

  • 各タスクの実施作業と実施完了の確認作業の工数が多い
  • 実行前に設定ファイルの変更に対するレビューが無いので、誤りがあった場合は機械学習モデルを再度生成し直す必要がある
  • 機械学習モデル生成の一連のタスクが途中で失敗した際に、一から再実行する必要がある

コントロールモデルにおいては一通りの開発が終わり、現在はモデルの学習やデプロイの作業に工数を割くことはほとんどなくなりました。ただ、トリートメントモデルの開発やデプロイは現在も運用によりカバーしている側面もあるので、改善に向けて開発に取り組んでいます。

さいごに、ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。検索機能の改善に興味のある方は、以下のリンクからご応募ください。

hrmos.co hrmos.co

JAXによるスケーラブルな機械学習

OGP

はじめに

こんにちは、ZOZO NEXT ZOZO ResearchのSai Htaung Khamです。ZOZO NEXTは、ファッション領域におけるユーザーの課題を想像しテクノロジーの力で解決すること、より多くの人がファッションを楽しめる世界の創造を目指す企業です。

ZOZO NEXTでは多くのアルゴリズムを研究開発しており、その中でJAXというライブラリを使用しています。JAXは高性能な機械学習のために設計されたPythonのライブラリです。NumPyに似ていますが、より強力なライブラリであると考えることができます。NumPyとは異なり、JAXはマルチGPU、マルチTPU、そして機械学習の研究に非常に有用な自動微分(Autograd)をサポートしています。

JAXはNumPyのAPIのほとんどをミラーリングしているので、NumPyライブラリに慣れている人なら非常に導入しやすいです。Autogradを使えば、Pythonのネイティブ関数とNumPyの関数を自動的に微分できます。JAXの詳細な機能については、JAXの公式GitHubリポジトリを参照してください。

そもそも、なぜJAXなのか?

機械学習アルゴリズムを構築する際、多くのMLエンジニアはTensorflowやPyTorchといった信頼性の高いMLフレームワークを利用することでしょう。成熟したMLフレームワークには成熟したエコシステムがあり、本番環境への統合や保守が容易になるため、良い決断です。当研究所では、これらのフレームワークを用いて実装された多くのアルゴリズムを持っています。

しかし、いくつかのアルゴリズムはNumPyライブラリを用いて、純粋なPythonで実装されています。その中には例えば、研究者やMLエンジニアが社内用に設計した埋め込みアルゴリズムがあります。埋め込みアルゴリズムは類似商品を効率よく抽出できるため、商品推薦などに有用です。実装がPythonであるため、このアルゴリズムは計算の実行時間にボトルネックがあります。お気づきのように、フレームワークを使用しない場合、パラメータの更新やモデルのダンプ等も自前で実装する必要があります。そのため、新しいアイデアをすぐに試すことが難しく、なかなか前に進めません。また、ライブラリや学習プロセスもCPUデバイスに限定されるため、拡張性がありません。共有メモリアーキテクチャを利用してマルチプロセスでアルゴリズムを実行できましたが、GPUやTPUなどの複数のホストやデバイスで実行し、垂直方向・水平方向にスケールできる状態が望ましいです。

そこで、拡張性・保守性の高い別のフレームワークにプログラムを移植する方法を検討した結果、以下のような特徴を持つJAXを採用しました。

  1. Single Program Multiple Dataアーキテクチャによる水平方向のスケーラビリティ
  2. NumPyのAPIをミラーリング
  3. Pythonに対応
  4. Autogradのサポート
  5. エコシステムまでオープンソース化されている(FlaxやHaikuなど)

特に(2)の性質によってNumPyで書かれたアルゴリズムを効率よく移植できるという点が、既存の他のフレームワークにはない利点でした。

本記事を読むことで分かること

本記事では、実世界のデータを使った機械学習を、JAXライブラリで実行する方法について説明します。通常、機械学習の理論を学び問題を解くときには、理解を深めるために小さなデータを使用します。しかし、実世界のデータに応用するとデータ量、モデルを格納するメモリサイズ、学習と評価のスケーラビリティなど多くの困難に直面することになります。ありがたいことに、現代のクラウドコンピューティングの革命と価格設定により、スケーラブルな機械学習は誰でも利用できるようになりました。

典型的な機械学習パイプライン

典型的な機械学習プロジェクトはデータの準備からモデルのサービングまで多くのステージで構成されますが、本記事で取り扱うのはデータの準備とモデルの学習に当たる部分です。特にJAXライブラリのパフォーマンスと、クラウドコンピューティング上でのスケーラビリティを実現する方法について説明します。

データって本当に大きいの? いつ、どこで、どうやって処理するの?

データと質の高いデータ変換が機械学習プロジェクトの成功の中心であることは、すべてのMLエンジニアが理解していることです。現実の機械学習プロジェクトでは、1台のマシンでETL(抽出、変換、ロード)プロセスを行えるような量のデータを扱うことは稀です。当研究所では、Google CloudやAWSなどのクラウドコンピューティングリソースに広く依存しており、通常、クラウドストレージやクラウドデータウェアハウスを使用してデータを管理しています。

データストレージとアルゴリズムをホストするマシンの統合

ボトルネックに要注意!

クラウドストレージは、1台のマシンに収まりきらない大量のデータを保存するのにとても役立ちます。しかし、モデルの学習に利用するためには、ストレージからデータを読み出す効率的な方法を見つける必要があります。多くのMLエンジニアがGPUデバイスを使った学習中に遭遇する問題の1つは、GPUデバイスが十分に活用し切れず、学習プロセスに必要以上の時間がかかってしまうことです。次のTensorFlowモデルのプロファイリング結果をご覧ください。

モデルのプロファイリング 参照:[モデルのプロファイリング]

よく観察すると、ディスクからデータを取得している間、GPUデバイスはほとんどの時間、アイドル状態であることに気づかれると思います。一般的には、学習中はGPUデバイスをビジー状態にしたいものです。これは、データ入力パイプラインにボトルネックがあることを示しています。

TF Dataってヒーローなの?

データ入力パイプラインのボトルネックを解消するために、TF DataというTensorFlowが提供する便利なツールを利用することにします。従来の方法では、下図のようにディスクからデータを順次読み込んでいました。下図のMapは、正規化、画像補強などのデータの変換処理です。

モデルへのデータの順次取り込み 参照:[モデルへのデータの順次取り込み]

しかし、この方法では学習処理にデータ転送待ちが発生し、GPUデバイスがアイドル状態になってしまうというボトルネックが発生しています。そこで下図のように読み込みとデータ変換を並列に行うことで、学習の待ち時間が少なくなります。

TF Data Pipelineによる効率的なデータ変換 参照:[TF Data Pipelineで効率的なデータ変換]

TFデータパイプラインのコンポーネントは再利用可能です。トレーニングやサービングフェーズに適用できます。TF DataライブラリはホストCPUを利用してデータを並列に処理しているので、CPUの性能が高ければ高いほど、データの読み込みや前処理が高速になることを念頭に置いておくことが重要です。

データ前処理パイプラインとして、Apache BeamやTFX Transformを使用する方法もありますが、今回は説明しません。本記事では、TF DataとJAXを使用して、スケーラブルな機械学習を共有します。

処理を高速化してみよう!

効果的なデータ前処理パイプラインを手に入れたことで、モデルの学習と評価のステップに移行します。JAXの便利なライブラリにvmapとpmapがあります。本記事では、vmapとpmapを使用してマルチGPUデバイスでの学習処理を高速化します。

#vmapによるauto-vectorization
import numpy as np
import jax.numpy as jnp
import jax

def convolve(x, w):
  output = []
  for i in range(1, len(x)-1):
    output.append(jnp.dot(x[i-1:i+2], w))
  return jnp.array(output)

x = np.arange(5)
w = np.array([3., 1., 3.])
batch_size = 10
xs = np.arange(5 * batch_size).reshape(-1, 5)
ws = np.stack([w] * batch_size)
print(f"The shape of the x and w : {xs.shape, ws.shape}")

print("Process each sample.")
for sample in xs:
    print(convolve(sample, w))

print("Auto-vectorization with vmap:")
print(jax.vmap(convolve)(xs, ws))
#vmap処理とサンプル単位処理の比較結果
The shape of the x and w : ((10, 5), (10, 3))
Process each sample.
[ 7. 14. 21.]
[42. 49. 56.]
[77. 84. 91.]
[112. 119. 126.]
[147. 154. 161.]
[182. 189. 196.]
[217. 224. 231.]
[252. 259. 266.]
[287. 294. 301.]
[322. 329. 336.]
Auto-vectorization with vmap:
[[  7.  14.  21.]
 [ 42.  49.  56.]
 [ 77.  84.  91.]
 [112. 119. 126.]
 [147. 154. 161.]
 [182. 189. 196.]
 [217. 224. 231.]
 [252. 259. 266.]
 [287. 294. 301.]
 [322. 329. 336.]]

まずはvmapに関して説明します。vmapはコードを変更することなく関数をベクトル化(auto-vectorization)するものです。auto-vectorizationにより、vmap APIで関数をラップする以外にコードを変更することなく処理を高速化できます。これは、特にバッチ処理の際に非常に便利です。vmapの機能はまだまだあるので、以下のリンクから確認してください。

jax.readthedocs.io

pmapの使い方は、vmapとよく似ています。しかし、pmapはMPIのようなCollective operationを提供し、プログラムが複数のデバイス上で通信しデバイスをまたいで合計や平均などの演算「MapReduce」を実行できます。このAPIにより、プログラムはスケールアウトできます。

#マルチデバイスでpmapを適用する
@partial(jax.pmap, axis_name="num_devices")
def update(params: Params, x: jnp.ndarray, y: jnp.ndarray) -> Tuple[Params, jnp.ndarray]:
    loss, grads = jax.value_and_grad(loss_func)(params, x, y)

    grads = jax.lax.pmean(grads, axis_name="num_devices")
    loss = jax.lax.pmean(loss, axis_name="num_devices")

    new_params = jax.tree_multimap(
        lambda param, g: param - g * step_size, params, grads
    )

    return new_params, loss

上記のコードサンプルでは、異なるデバイスでloss_func関数から返された結果に対してCollective meanを実行し、パラメータを更新しています。このコードブロックは、アクセラレータデバイスの数を気にすることなく、どのマシン上でも実行できます。バックグラウンドでJAXによって自動的にスケールアウトし、管理されます。ただし、アクセラレータデバイスの数に応じて、デバイスの次元を一致させる必要があります。デバイスの次元とは、デバイス間でデータを均等に分割するために使用される仮想的なメトリックディメンジョンのことです。例えば、8台のデバイスがある場合、同時に処理するサンプルは少なくとも8個必要です。

環境設定

本記事では、JAXライブラリを用いて2つのデータセットを検証します。1つ目はMNISTの手書き数字データセット、2つ目はカスタムデータセットです。まずはMNIST手書き数字データセットのためのシンプルな多層パーセプトロン(MLP)を構築しました。

以下の図は使用したインフラ設定です。

使用したインフラ設定

MLPのハイパーパラメータ設定です。

MLPのハイパーパラメータ

以下の図はアクセラレータ毎の平均実行の時間、異なるアクセラレータでのアルゴリズム実行時間の比較です。

アクセラレータ毎の平均実行時間

JAXが提供するpmap APIを使えば、簡単に複数のデバイスでモデルを実行し、学習と配信のためにスケールアウトさせることができました。CPUでは各エポックに約3.34秒かかるのに対し、4GPUでは1.09秒であることが確認されました。この図は、より多くの並列処理を行うほど、この特定のアルゴリズムの実行時間が短縮することを示しています。

以下の4GPUでの学習と各エポックでの実行時間図は、4つのGPUアクセラレータを用いた場合の、各エポックにおけるモデル学習と実行時間の性能を示しています。

4GPUで学習と各エポックの実行時間 4GPUで学習と各エポックの実行時間

上の図から、モデルはトレーニングデータセットでうまく学習し、バリデーションデータセットでもうまくいっていることが確認できました。また、学習処理は4つのGPUデバイス全てに均等に分散されています。最初のエポックを終えるのに約1.8秒、その後のエポックでは約1.09秒かかっています(左下図)。最初のエポックでは、クラウドストレージから画像をリモートで読み込んで、パイプラインデータ変換に応じた前処理を行う必要があります。その後、パイプラインのキャッシュ機能を使ってデータをローカルにキャッシュし、次のエポックに備えることで、実行時間を大幅に短縮しています。

GPU使用率の観点から、GPUは最初からビジー状態であり、トレーニングの最初のエポックの終わりである1.87秒付近にいくつかのピークあることがわかりました(右下図)。これは、GPU(特にgpu_2)がパイプラインからのデータロードと変換同時にいくつかの処理を保持していることを物語っています。データパイプラインがリモートストレージデバイスからデータをロードするのと並行して、学習処理が開始されていることがわかります。GPUデバイスは約40%のピークにあり、すべてのGPUを100%利用するにはMLPのレイヤーがかなり小さいので妥当なところです。

私たちが発見した興味深い事実は、クラウドストレージの場所は私たちのアルゴリズムをホストしているマシンと異なる場合、リモートデータ取得により最初のエポックに余分な時間が追加されるということです。これは通常、クラウドインフラの仕様で、ユーザーがアクセスするエッジロケーションにデータをダウンロードする必要があるためです。ホストマシンから初めてアクセスした後、データはエッジロケーションに保存され、トレーニングの実行時間が大幅に改善されます。

以下の図はトレーニングマシンとクラウドストレージが異なる地域または場所にある場合の結果です。

トレーニングマシンとクラウドストレージが異なる地域または場所にある場合

最初のアクセス後、エッジロケーションのキャッシュにより、ランタイムが改善されました。

最初のアクセス後、エッジロケーションのキャッシュにより、ランタイムが改善されました

計算量が大きいほどGPUのデバイス使用率が高くなることを検証するため、より大きなMLPレイヤーと大きな画像でテストを行いました。ハードウェアの仕様は、前回のMNIST手書きデータセットでの実験と同じにしています。

Custom Dataset Environment Setting

以下の図は各エポックにおける異なるアクセラレータのアルゴリズム実行時間の比較です。

各エポックにおける異なるアクセラレータの平均実行時間

このシナリオでは、シングルGPUでの学習が最高のランタイムパフォーマンスをもたらすことが観察され、興味深いです。シングルGPUでは、CPUよりも約12倍高速になります。この特定のデータセットとマルチGPUデバイスの場合、マルチデバイスで実行する際のMap-Reduce操作のオーバーヘッドが原因だと思われます。

4GPUで学習と各エポックの実行時間 4GPUで学習と各エポックの実行時間

予想通り、層数と中間素子数が増えれば増えるほど、計算負荷が大きくなります。最初のエポック(0-28秒)の間(右下図)、ホスト上で起きているデータの前処理とGPU上のトレーニングステップが同時に実行されていることが観察されます。もちろん、MLPのレイヤーに生のピクセルを入力しているため、モデルの学習にはあまり期待できません。より良い結果を得るためには、畳み込みニューラルネットワークを使用することが望ましいでしょう。

まとめ

結論として、並列処理とキャッシュを備えたTF Dataライブラリを使用することでGPUデバイスのポテンシャルを引き出し、より高速な学習が可能になることが確認できました。GoogleやAWSのような大手ベンダーのクラウドストレージにデータを保存しつつ、データ取得を高速に行うことを説明しました。TF Dataではクラウドストレージだけでなく、BigQueryやBigtableなどからもリモートでデータを読み込むことができます。詳しい使い方はドキュメントをご覧ください。

また、マルチデバイス処理に関するvmapやpmapなど、JAXの便利な機能のデモンストレーションをしました。JAXは、NumPyのAPIのほとんどがJAXでミラーリングされているため、NumPyに慣れている人であれば簡単に使用できます。さらに、Autogradは、Pythonのネイティブ関数とNumPyの関数の微分を自動化できます。pmapの使用に適合するようにプログラムを開発すれば、JAXがバックグラウンド処理を引き受けてくれるのでCPUやマルチGPUデバイスに関係なくどこでもこのプログラムを実行できます。

私見ですが、JAXは非常に柔軟な使い方ができ、PoCを素早く行うためのコーディングが容易です。しかし、機械学習アルゴリズムを複数のプラットフォームで運用することを目指すのであれば、JAXはまだ成熟していないと言えます。TensorflowやPyTorchのような、強力なエコシステムを持ち、広く採用されている他のフレームワークに目を向けたほうがよいと思います。

さらに、JAXによるスケーラブルなインフラを実証するため、シンプルなMLPアルゴリズムを採用しました。JAXの複雑で高度なモデルを使って、MLの問題を解決出来ます。コードを入れ替えるだけ、本記事で取り上げたことはほとんど同じです。私は、FlaxやHaikuのような深層学習用のJAXフレームワークを使用することをお勧めします。JAXの公式GitHubリポジトリのチェックを忘れないでください。JAXを使ったハンズオンを楽しんでください。

本記事をシンプルにするため、コードブロックでの説明を省略し、すべてJupyter Notebookにまとめました。ぜひご覧ください。

ZOZO NEXTでは、機械学習を適切に使用して課題を解決できるMLエンジニアを募集しています。今回は、JAXを使ってレガシーアルゴリズムを改善した方法を紹介しました。MLアルゴリズムをファッションビジネスへ応用することに興味がある方は、以下のリンクからぜひご応募ください。

hrmos.co

hrmos.co

hrmos.co

Vertex Feature Storeの機械学習システムへの導入

OGP

こんにちは、データシステム部推薦基盤ブロックの寺崎(@f6wbl6)です。現在、推薦基盤ブロックではデータサイエンス部MLOpsブロックのメンバーと協力しながらMLOps基盤の構築を進めています。本記事ではMLOps基盤構築の一環として進めているVertex Feature Storeの機械学習システムへの導入に関する知見およびVertex Feature Storeを導入する上での制限や課題をご紹介します。

MLOps基盤に関する取り組みについては以下のテックブログでも取り上げていますので、こちらもご参照ください。

techblog.zozo.com

techblog.zozo.com

techblog.zozo.com

続きを読む

Elasticsearch Learning to Rankプラグインの使い方とポイント

ogp

検索基盤部の内田です。私たちは、約1年前よりヤフー株式会社と協力し、検索機能の改善に取り組んでいます。現在、ZOZOTOWNのおすすめ順検索に用いている、ランキング学習を利用した検索機能も、その取り組みの一部です。

本記事では、Elasticsearch上で、ランキング学習により構築した機械学習モデルを用いた検索を行うためのプラグイン「Elasticsearch Learning to Rank」の簡単な使い方を紹介します。また、このプラグインをZOZOTOWNに導入し、実際に運用して得られた知見をご紹介します。ランキング学習の話題性が世の中で増していますが、検索エンジンを絡めた情報はまだ世の中に少ない印象があります。そのため、本記事が皆さんの参考になれば幸いです。

ランキング学習のイメージ

ランキング学習(Learning to Rank, LTR)とは、機械学習の枠組みのひとつであり、情報検索におけるランキングを予測するモデルを構築する手法です。イメージとしては「機械学習で、入力されたパラメータに基づいて賢く候補を並び替えるもの」と言えます。

ランキングモデル概要図

Learning to Rankプラグインの使い方

ZOZOTOWNの検索システムは、Elasticsearchを利用して構築しています。弊社のElasticsearchに関する取り組みは、過去の記事でも紹介しているので、併せてご覧ください。

techblog.zozo.com

Elasticsearchには、ランキング学習で構築したモデルを利用して検索する機能がデフォルトでは用意されていません。Learning to Rankプラグイン(以下、LTRプラグイン)の利用は、その機能を導入するための比較的低コストな手段の1つです。本章では、LTRプラグインを使った検索の実行方法を簡単に紹介します。なお、Elasticsearchへのプラグインの導入方法は解説しません。プラグインの導入に関する情報はプラグインのREADMEElasticsearchのドキュメントを参考ください。

LTRプラグインを用いて、ランキング学習で構築したモデルを利用した検索を実現するには、下記の3つの手順を踏む必要があります。

  1. Elasticsearchで計算する特徴量セットを定義する
  2. 機械学習モデルを構築してElasticsearchにアップロードする
  3. アップロードしたモデルを検索クエリで指定して検索する

なお、LTRプラグインを初めて利用する際は、feature storeの有効化が必要です。feature storeは、特徴量セットやモデルに関するメタデータを格納するインデックスに相当します。

PUT _ltr

1. Elasticsearchで計算する特徴量セットを定義する

本記事では、概要を説明します。詳細を確認したい場合は、公式ドキュメントを参照ください。

elasticsearch-learning-to-rank.readthedocs.io

機械学習モデルを用いたスコアリングでは、Elasticsearchが各特徴量の値を計算します。ただし、それらの特徴量の定義は、事前にElasticsearchへ登録しておく必要があります。そして、その特徴量の定義方法は、以下の3種類の方法が存在します。

  • mustache
    • テンプレート言語mustacheを交えたElasticsearchのクエリDSLで記述する方法
  • derived_expression
    • Luceneの式で記述する方法
    • 基本的な演算子や算術関数が利用可能で、他の特徴量を参照する簡単な特徴量も定義可能
    • なお、LTRプラグインの公式ドキュメントにはderived_expressionsと書かれている箇所があるが、正しくはderived_expression
  • script_feature
    • Elasticsearchのスクリプト言語(Painlessなど)で記述する方法
    • 複雑な特徴量を定義可能

上記の選択肢にある、script_featureによる特徴量定義は、検索の実行パフォーマンスを低下させる可能性が高いため、可能な限り避けることをおすすめします。基本的にはmustacheで特徴量を定義し、mustacheで定義した特徴量の値を参照する特徴量がある場合はderived_expressionの記法で定義します。

例えば、検索対象となる商品が「商品名(title)」と「人気度(popularity)」を持つとします。

PUT my_index
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "popularity": {
        "type": "integer"
      }
    }
  }
}

ここで、下記の特徴量ベクトルを定義するとします。

  1. 商品名と検索キーワードのマッチ度
  2. 人気度の値
  3. それらの積

この場合、以下のように記述できます。

POST _ltr/_featureset/my_featureset
{
  "featureset": {
    "features": [
      {
        "name": "title_relevance",
        "params": ["keyword"],
        "template_language": "mustache",
        "template": {
          "match": {
            "title": "{{keyword}}"
          }
        }
      },
      {
        "name": "popularity_value",
        "params": [],
        "template_language": "mustache",
        "template": {
          "function_score": {
            "query": {
              "match_all": {}
            },
            "field_value_factor": {
              "field": "popularity",
              "missing": 0
            }
          }
        }
      },
      {
        "name": "product_of_relevance_and_popularity",
        "params": [],
        "template_language": "derived_expression",
        "template": "title_relevance * popularity_value"
      }
    ]
  }
}

2. 機械学習モデルを構築してElasticsearchにアップロードする

elasticsearch-learning-to-rank.readthedocs.io

特徴量が定義できたら、機械学習でモデルを構築し、そのモデルをアップロードします。LTRプラグイン自体には、機械学習の機能は備わっていないため、モデルの学習には外部の機械学習ライブラリを利用する必要があります。なお、本記事の執筆時点でLTRプラグインが対応している機械学習ライブラリはRanklibXGBoostの2つです。

POST _ltr/_featureset/my_featureset/_createmodel
{
  "model": {
    "name": "my_model",
    "model": {
      "type": "model/xgboost+json",
      "definition": "(機械学習ライブラリを使って構築し保存したモデル)"
    }
  }
}

なお、学習に用いるデータは、サービスで収集した実際のユーザの行動ログを利用するのが良いです。また、特徴量ベクトルの生成には、LTRライブラリの特徴量ログ出力機能が役に立つ場合があるので、確認しておくと良いでしょう。

3. アップロードしたモデルを検索クエリで指定して検索する

elasticsearch-learning-to-rank.readthedocs.io

前述までの手順で、機械学習モデルを用いた検索システムを動かす準備は整いました。しかし、1つ大きな問題点があります。機械学習モデルを用いたスコア計算は計算コストが高いという問題です。もし、検索クエリに一致した商品(ドキュメント)全てに対して機械学習モデルを適用しスコアを算出した場合、レスポンスタイムの悪化などを招きます。その結果、検索の総合的なパフォーマンスを劣化させる可能性が高まります。

そのため、機械学習モデルを検索に取り入れる際には、基本的にリランキングを行います。簡単なクエリで計算したスコアを用いて一次的なランキングを行い、それらの上位N件に対して機械学習モデルでスコアを再計算し並び替え、最終的な検索結果を出力します。リランキング対象とする件数は、Elasticsearchクラスタのマシンリソースなどを考慮し、検索の精度向上とスループットやレスポンスタイム悪化のトレードオフを見ながら許容できる範囲で調整します。

GET my_index/_search
{
  "query": { // 簡単なクエリで絞り込みと一次ランキング
    "match": {
      "title": "ジャケット"
    }
  },
  "rescore": { // 機械学習モデルでのリランキング
    "window_size": 1000,  // 一次ランキングの上位1000件をリランキングする
    "query": {
      "rescore_query": {
        "sltr": {
          "params": {
            "keyword": "ジャケット"
          },
          "model": "my_model"
        }
      }
    }
  }
}

実運用から得られた知見と注意点

本章では、LTRプラグインをサービスに適用し、実運用から得られた知見や注意点を紹介します。LTRプラグインの導入に迷われている方、困っている方の参考になれば幸いです。なお、運用する中で独自に行った改修のいくつかは、LTRプラグインへコミットし、コミュニティに還元しています。

レスポンスタイムに悪影響を与える設定項目の調査

前述の通り、機械学習モデルによるリランキングを行うため、レスポンスタイムの悪化が想定されました。そのため、事前に負荷試験を実施し、レスポンスタイムに影響を与える設定項目の発見、及び一定のスループットを保てるラインを調査しました。

その調査の結果、想定していた通り、リランキングによりレスポンスタイムが悪化することが分かりました。また、モデルの特徴量数・複雑度やリランキング対象を操作することで、悪化の程度を調整できることも分かりました。

調査結果の概要は以下の通りです。

  • モデルを構成する特徴量数の調整
    • モデルを構成する特徴量の数を増やすと、レスポンスタイムが悪化することが確認された
  • モデルの複雑度の調整
    • モデルを構成する決定木の数やそれらの高さをコントロールしたところ、モデルが複雑になるとレスポンスタイムが悪化することが確認された
  • 特徴量セットで定義する特徴量数の調整
    • レスポンスタイムに変化はなかった
    • 特徴量セットに定義された特徴量は毎回全て計算されるのではなく、モデルで参照される特徴量のみが都度計算されるように見受けられる
  • リランキング対象数の調整
    • 対象数を増やすと、レスポンスタイムが悪化することが確認された

特徴量が負の値を持つ場合の注意点

Elasticsearchは、クエリを実行して得られるスコアに負の値を許容しません。そのため、前述の特徴量セットの定義で紹介した以下のクエリでは、対応するフィールドの値を参照しているため、popularityフィールドが負の値を持つ場合にエラーとなります。これは、Luceneの仕様によるものです。クエリ側で回避することが難しいため、特徴量セットを定義する際や、学習用のデータを作る際には負の値の扱いに注意が必要です。

{
  "name": "popularity_value",
  "params": [],
  "template_language": "mustache",
  "template": {
    "function_score": {
      "query": {
        "match_all": {}
      },
      "field_value_factor": {
        "field": "popularity",
        "missing": 0
      }
    }
  }
}

クライアントライブラリが存在しない

LTRプラグインが提供する拡張DSL("sltr"など)に対応したクライアントライブラリが存在しません。LTRプラグインの機能を利用するには、クライアントを利用せずにクエリを構築しAPIにリクエストを投げるか、Elasticsearchのクライアントを独自に拡張DSLに対応させる必要があります。

弊社では、LTRプラグインのソースコードに含まれるQueryBuilder系のコードを参考にし、クライアントライブラリを拡張しています。

プラグインのバージョン制約

基本的に、Elasticsearchのプラグインは、使用しているElasticsearchのバージョンに合わせてビルドされたパッケージをインストールする必要があります。

しかし、LTRプラグインは、自身のバージョンアップがあっても、Elasticsearchの旧バージョン向けにはパッケージの配布が行われません。基本的に最新の機能や修正は、Elasticsearchの最新バージョンでしか利用できないという問題が発生しています。

例えば、LTRプラグインが更新された時点で、最新のElasticsearchのバージョンが7.16.2だったとしましょう。この場合、バージョン7.16.1以前のElasticsearchでは、LTRプラグインの最新バージョンで追加された機能を使ったり、バグの修正を適用することができません。

弊社では、LTRプラグインのリポジトリをクローンし、本家で新機能がリリースされた場合は、利用しているElasticsearchのバージョンに合わせて独自にプラグインをビルドし運用しています。

まとめ

本記事では、ElasticsearchのLTRプラグインの簡単な使い方と、運用を通して得られた知見や注意点を紹介しました。今後も継続的に改善していきますので、是非ZOZOTOWNの検索機能をお試しください。

さいごに、ZOZOでは検索エンジニア・MLエンジニア・サーバサイドエンジニアのメンバーを募集しています。検索機能の改善にご興味のある方は、以下のリンクからご応募ください。

hrmos.co hrmos.co

また、本記事で紹介した施策にご協力いただいたヤフー株式会社の皆さんに改めて感謝いたします。

Vertex AIで作るBizDevメンバー向けモデリング環境

OGP

はじめに

こんにちは、ZOZO NEXTのApplied MLチームでMLエンジニアをしている柳です。機械学習を使ってビジネス上の課題解決をする仕事に取り組んでいます。今回は、BizDevメンバーのAutoML Tables活用をサポートする中で出会った課題やその解決方法について紹介します。

概要

ZOZOTOWNでは様々なプロモーション施策が日々打たれています。ZOZOTOWNをご利用の方は、メールやアプリ上でキャンペーンやクーポンの配信を受け取ることも多いのではないでしょうか。このような配信施策では、ターゲットを絞ることが重要です。無闇矢鱈に多数のユーザーに配信をしてしまうと配信コストがかかります。さらに、興味のないキャンペーンが大量に通知されるとユーザー体験も損なわれます。そのため、個々のユーザーの興味を抽出し、それに合わせた配信をするのが理想です。

弊社では、MLのビジネス活用を進めるBizDevメンバーを中心に、このような課題に取り組んでいます。そこでよく使われているのが、GCPのAutoML Tablesです。以前から存在するサービスですが、Vertex AIの登場に伴ってその一機能としても提供されるようになりました。専門的なMLライブラリの扱い方を覚える必要がなく、ビジネス課題をMLを使って解決するのに集中できる便利なツールです。私たちのようなMLエンジニアは特徴量の作り方のディスカッションを時々するくらいで、基本的にはBizDevメンバーがモデリングから配信まで行っていました。

しかし、このようなAutoMLのビジネス活用が拡大していく中で、徐々に技術的負債が溜まっていることもわかってきました。それらは大別すると以下のように分類できます。

  • コードの管理やレビュー環境に関する課題
  • モデルの学習や評価方法に関する課題

特に後者はある程度MLを使った経験がないとなかなか気づきづらいようなものでした。本記事ではこれら課題の具体的な内容と、それを解決するための取り組みについて紹介します。

AutoML Tablesによるモデリング

本章では、AutoML Tablesの説明と、配信施策での使用例を紹介します。

基本的な使い方

AutoML Tablesの使い方は概ね以下の流れです。詳しくは公式ドキュメントを参照してください。

  1. BigQueryテーブルなどに、学習用の表形式データを用意する
    1. のデータをVertex AIのデータセットとしてインポートし、特徴量として利用するカラム、ラベルとして利用するカラムの選択、及び回帰や分類など課題の種類と最適化指標を指定し学習を開始する
  2. コンソールで精度や特徴量の重要度を確認し、モデルがうまくできていそうかをチェックする
  3. 用途に合わせて推論用データを作成し、推論する

配信施策における使い方

配信施策では「ユーザーがある対象に興味を持っているか」を予測するようなモデルを作ります。以下では、例として「ユーザーがカテゴリXに興味を持っているか」を予測するモデルの作り方を考えてみます。様々な方法が考えられますが、ここでは次のようにアプローチしてみましょう。

  • 学習
    • 二値分類を解く
    • ある期間にカテゴリXの商品を購入したユーザーを正例とし、負例はカテゴリXの商品を購入していないユーザーから正例と同じ数だけサンプリングする
  • 推論
    • AutoML Tablesの二値分類モデルでは、バッチ推論をすると各ユーザーに0から1の予測値が付与される
    • この値の上位Kユーザーを最終的な推論結果とする

特徴量は各ユーザーの年齢などの属性情報や、ZOZOTOWNでの実際の行動履歴を用います。BigQueryを使って正例・負例ユーザーを抽出し、特徴量をjoinすればデータセットは完成です。

あとは、前述のようにAutoML Tablesを利用することで作業は完了します。弊社のBizDevメンバーは普段からBigQueryを使って分析しているので、データ抽出用のSQLを難なく書くことができます。そのため、BizDevメンバーだけでモデル作成から配信用のユーザー抽出まで行えます。

発生した課題

弊社では前述の通り、 BizDevメンバーがAutoML Tablesを活用してきました。しかし、利用の拡大に伴い、以下のような問題が見られるようになりました。

SQLの管理不足からバグが生じやすくなった課題

いくらAutoML Tablesがノーコードで学習・推論してくれると言っても、それに投入するデータを作るにはSQLを書く必要があります。施策が変われば抽出したいユーザーも変わり、予測に有効な特徴量も変わってきます。そのような場合、往々にして以前の施策で使っていたSQLを流用して新規施策用のデータを抽出することになります。上記の例で言えば、「カテゴリXに興味あるユーザーを当てるためのSQLを、カテゴリYを当てるためのものに変えよう」ということです。また、配信期間が変われば特徴量を計算する期間も変わってきます。このような際に、しばしばSQLをローカルで直接書き換え利用することが行われていました。修正や継ぎ足しが行われたSQLは可読性が低下し、バグが入りやすくなります。

実際に、学習したモデルの特徴量の重要度に違和感があり調べてみたところ、SQLにバグが混入していたということがありました。これは仕組みを作って防ぐべき問題です。

オフライン評価が未整備である課題

MLモデルの改善を正しい方向に進める上で、適切なオフライン評価を設定することは非常に重要です。上記の例で作成したいのは「予測上位K件に正例ユーザー(カテゴリX購入者)をできるだけ多く含めることができる」モデルです。そのため、本来であれば以下のようなPrecision@KやRecall@Kなどのメトリクスで評価をすべきです。 メトリクス

しかし、AutoML Tablesではこれらのメトリクスは自動で計算されません。その代わりに、二値分類のAUCなどが計算されコンソールに表示されます。通常の二値分類タスクであればこれで問題ありませんが、今回は負例をサンプリングしているため、サンプリング方法に敏感な指標となってしまいます。正例との識別が難しい負例をより多くサンプルするようにすれば、二値分類の精度は低くなります。逆に識別が簡単な負例を多くサンプルすれば二値分類の精度は上がります。私たちが本当に欲しいモデルは正例を精度良く抽出できるようなモデルであり、負例のサンプリング方法によってメトリクスが上下するのは好ましくありません。

モデルを正しく改善していくために、本来評価したいPrecision@KやRecall@Kなどのメトリクスが確認できるように環境を整理する必要がありました。

繰り返し作業が発生する課題

前述の通り、AutoMLで自動化できるのはあくまでも学習・推論作業のみであり、データの抽出は当然自分でやらなければなりません。そのため、BizDevメンバーが毎回決められた手順でSQLを逐次実行しており、繰り返し作業や計算の各ステップが終わるまでのソワソワして待つ時間が生じていました。

解決方法

本章では、上記課題を解決するために取り組んだ解決方法を紹介します。

SQL管理の厳格化

まずは、シンプルにデータ抽出用SQLのGit管理を厳格化することにしました。SQLごとに「正例抽出用」「特徴量の抽出用」など役割を明確にし、集計期間や集計対象カテゴリなど、パラメトライズできる部分をクエリパラメータにしました。そして、新たに特徴量や学習ターゲットを追加する際には、GitHub上でプルリクエストを作る運用方針にしています。

適切なオフライン評価の実装

適切なオフライン評価をするために、AutoMLの外部に評価機能を実装しました。例えば、上記の例では、カテゴリXの購入ユーザーを時系列に沿って学習用と評価用に分割します。この評価用に分けられたユーザーをground truthとして、モデルのprecision@Kやrecall@Kを計算します。こうすることで、負例サンプリングの方法に鈍感な評価ができるようになります。そして、これらのメトリクスの評価は後述のパイプラインに組み込み、コンソール上で確認できるようにしています。

Vertex Pipelinesによる自動化

繰り返し作業の自動化をするため、以下のワークフローをVertex Pipelines上に実装しました。なお、Vertex Pipelinesは先日GA版になった機能です。

  1. データ抽出
  2. AutoML Tablesによる学習
  3. AutoML外での評価・バッチ推論

構築したワークフローは以下の通りです。

構築したパイプライン

このワークフローにより、前述のメトリクスは以下のように可視化されます。

メトリクスの可視化例

Vertex Pipelinesはパイプライン定義を記入したJSONファイルをアップロードすることで、GCPコンソールから実行できます。なお、このJSONファイルの管理・更新はMLエンジニアが担当します。BizDevメンバーにはJSONファイルを渡し、適宜パラメータを変更して施策に合ったモデリングをしてもらいます。これにより、ノーコードの環境を維持しつつ、BizDevメンバーの作業負荷の軽減を実現しました。

また、MLエンジニアがコード類を管理し、個々の現場でSQLを修正して利用することがなくなったため、バグが混入するリスクも減少しました。

AutoML Tablesのパイプラインコンポーネントに関するTips

最後にVertex PipelinesでAutoML Tablesを使う際のTipsを紹介します。なお、Vertex Pipelinesについては過去の記事でも紹介しているのでご参照ください。

techblog.zozo.com

また、AutoML Tablesを使ったパイプラインについては、以下の公式ブログが参考になります。

cloud.google.com

特徴量の指定方法

学習コンポーネントに渡したテーブルの中から特定の特徴量のみ学習に使う方法を紹介します。上記の公式ブログから学習コンポーネントの部分を抜粋します。

from google_cloud_pipeline_components import aiplatform as gcc_aip

@kfp.dsl.pipeline(name="automl-tab-beans-training-v2", 
                  pipeline_root=PIPELINE_ROOT)
def pipeline(
    bq_source: str = "bq://aju-dev-demos.beans.beans1",
    display_name: str = DISPLAY_NAME,
    project: str = PROJECT_ID,
    gcp_region: str = "us-central1",
    api_endpoint: str = "us-central1-aiplatform.googleapis.com",
    thresholds_dict_str: str = '{"auRoc": 0.95}',
):
    dataset_create_op = gcc_aip.TabularDatasetCreateOp(
        project=project, display_name=display_name, bq_source=bq_source
    )
 
    training_op = gcc_aip.AutoMLTabularTrainingJobRunOp(
        project=project,
        display_name=display_name,
        optimization_prediction_type="classification",
        optimization_objective="minimize-log-loss",
        budget_milli_node_hours=1000,
        column_transformations=[
            {"numeric": {"column_name": "Area"}},
            {"numeric": {"column_name": "Perimeter"}},
            {"numeric": {"column_name": "MajorAxisLength"}},
            ... other columns ...
            {"categorical": {"column_name": "Class"}},
        ],
        dataset=dataset_create_op.outputs["dataset"],
        target_column="Class",
    )
    ...

学習コンポーネントは AutoMLTabularTrainingJobRunOp です。ここで使用する特徴量を指定する際に column_transformations という変数を設定しています。

しかし、Pythonコンポーネントのドキュメントには、次のように書かれています。

Consider using column_specs as column_transformations will be deprecated eventually.

つまり、現在は column_specs という変数の利用が推奨されています。column_transformations は辞書のリストが入る仕様ですが、column_specs では仕様が以下のように変更されています。

{"Area": "numeric", "Perimeter": "numeric",...}

バッチ推論と結果の取得

上記の AutoMLTabularTrainingJobRunOp で作成されたモデルを ModelBatchPredictOp に渡すことでバッチ推論が可能です。

batch_prediction_op = gcc_aip.ModelBatchPredictOp(
    project=<プロジェクト名>,
    location=<リージョン名>,
    job_display_name=<好きなディスプレイ名>,
    model=training_op.outputs["model"], # AutoMLTabularTrainingJobRunOpの出力
    bigquery_source_input_uri=<推論用BigQueryテーブル名>,
    bigquery_destination_output_uri=<推論結果の書き込み先>,
    instances_format="bigquery",
    predictions_format="bigquery",
)

bigquery_source_input_uri は推論対象のBigQueryのテーブルです。詳しくはドキュメントを参照してください。

bigquery_destination_output_uri には、 bq://<project> もしくは bq://<project>.<dataset> 形式で推論結果の出力先を指定します。テーブル名などはコンポーネントによって一意のものが自動で付与されます。

また、バッチ推論コンポーネントで作られた推論結果のテーブルは、コンポーネントによって一意の名前がつけられており、ユーザー側で指定することができません。このテーブル名を取得するには、バッチ推論リソースにアクセスする必要があります。例えば、以下のようなコンポーネントで直接 curl を使って取得します。

@component(
    base_image=base_image,
    output_component_file=None,
)
def get_batch_predict_info_op_wrapper(
    batch_predict_job: Input[Artifact], # バッチ推論の出力
    data_path: OutputPath(str), # テーブル名の書き込み先
):
    import os
    import subprocess

    uri = batch_predict_job.uri
    url = uri.replace(
        "aiplatform://v1/",
        "https://us-central1-aiplatform.googleapis.com/v1/",
    )

    os.makedirs(os.path.dirname(data_path), exist_ok=True)

    cmd = 'curl -X GET -H "Content-Type: application/json" -H "Authorization: Bearer "$(gcloud auth application-default print-access-token) "{url}" > "{data_path}"'.format(
        url=url, data_path=data_path
    )

    subprocess.call(cmd, shell=True)

最後に

本記事ではZOZOTOWNにおけるMLのビジネス応用の一例と、それを改善するための取り組みを紹介しました。

ZOZO NEXTでは、機械学習を適切に使用して課題を解決できるMLエンジニアを募集しています。今回は配信施策について紹介しましたが、検索や推薦の領域でもML活用が進んでいます。

ご興味のある方は、以下のリンクからぜひご応募ください。

hrmos.co

hrmos.co

hrmos.co

Kubeflow PipelinesからVertex Pipelinesへの移行による運用コスト削減

こんにちは、技術本部 データシステム部 MLOpsブロックの平田(@TrsNium)です。約2年半ぶりの執筆となる今回の記事では、MLOps向け基盤を「Kubeflow Pipelines」から「Vertex Pieplines」へ移行して運用コストを削減した取り組みを紹介します。

目次

はじめに

弊社ではML(Machine Learning)のモデル生成や特徴量生成にGKE(Google Kubernetes Engine)上でセルフホストしたKubeflow Pipelinesを使用していました。しかし、構築・運用コストが大きすぎるという課題感がありました。具体的にはKubeflowの依存するIstioKubernetes Applicationsのバージョンが古く、Kubernetesクラスタのバージョンアップデートをできなかったり、Kubeflowの内部ステートを保持しているMySQLが実際のステートと一致しない状況が発生していました。

詳しくは、中山(@Civitaspo)が過去の記事「KubeflowによるMLOps基盤構築から得られた知見と課題」で、構築や運用に関する課題感を紹介しているので、併せてご覧ください。 techblog.zozo.com

このような運用課題へアプローチしていたところ、Google I/O 2021Vertex AIの発表がありました。その後、Vertex AIのコンポーネントの1つであるVertex Pipelinesを調査し、Kubeflow Pipelinesの恩恵を享受しつつ運用コストを大幅に削減できる確信が得られたため、Kubeflow PipelinesからVertex Pipelinesへの移行を開始しました。

Vertex Pipelinesとは

Vertex Pipelinesは、GCPが提供しているKubeflow Pipelinesのフルマネージドサービスです。似たサービスにCloud AI Platform Pipelinesがありますが、明確に違いがあります。

Cloud AI Platform PipelinesではKubeflow PipelinesをGKEやCloud SQLをプロビジョニングして構築するのに対し、Vertex Pipelinesでは構築が不要です1。これにより、GKEやCloud SQLを管理する必要がなくなります。また、ワークフローが動いてない間の待機時間はCloud AI Platform PipelinesではGKEやCloud SQLの料金が必要なのに対し、Vertex Pipelinesではそれらの料金が発生しません。

つまり、構築や運用コストの面でKubeflow PipelinesやCloud AI Platform Pipelinesと比べ、Vertex Pipelinesには大きなアドバンテージがあります。

また、2つ目の違いは、Kubeflow PipelinesのSDK(kfp)のバージョンが異なる点です。Cloud AI Platform Pipelinesや、私たちがこれまで利用していたKubeflow PipelinesではSDKのバージョンがV1だったのに対し、Vertex PipelinesではV2です。なお、Kubeflow Pipelines 1.6以上のバージョンであればSDK V1はSDK V2と互換性がありますが、それ以外はありません。

Vertex Pipelinesへの移行

本章では、Kubeflow PipelinesからVertex Pipelinesへの移行の流れを説明します。

移行前に運用していたKubeflow Pipelinesのバージョンが1.2であり、SDK V2との互換性がないため、SDK V2でワークフローを記述し直す必要がありました。また、Kubeflow Pipelinesは、AWS(Amazon Web Services)やGCP、オンプレミス等で動作するようにKubernetesの様々な機能を駆使して設計されています。

一方、Vertex Pipelinesでは、それらの機能をGCPのサービスに置き換えているため、ワークフロー実行時の挙動が異なることがあります。提供されて間もないサービスなこともあり、Cloud Monitoringで取得可能なメトリクスが多くなく、ワークフローを外部から監視できる仕組みがありません。

これらの課題に対し、移行時にどのように解決していったのか、説明します。

Vertex Pipelinesへ移行するワークフロー

Vertex Pipelinesへ移行するワークフローは、WEARユーザーのコーディネート画像からアイテム特徴量を抽出し、Firestoreへそれを保存するような処理を行っています。対象のコーディネート画像が3000万件以上と膨大にあるため、日次の差分で処理をしています。

下図がワークフローの全体像です。

このワークフローでは、日次の差分データを取得するためにデータ基盤チームが管理するBigQueryからコーディネート情報を全件取得し、前日の全件取得との差分から新規コーディネート情報を一覧化しています。このコーディネート情報には、ユーザーの情報とコーディネート画像のURLが含まれています。

そして、コーディネート画像はAmazon S3に保存されていますが、データ基盤にはCDN経由のURLを格納しているため、S3へ直接取得するためのパス情報がありません。また、S3から直接画像を取得する料金と、CDN経由で画像をダウンロードする料金にさほど差がないため、CDN経由で画像を取得するようにしています。ただし、CDNに大量のリクエストを送ることになるので、DDoSと誤判定されないように固定の外部アドレスを使用しアクセスします。

上記の要件を満たすようにVertex Pipelinesへ移行した結果、ワークフローは以下の構成になりました。

なお、移行にあたり取り組んだ主な内容は以下の通りです。

  1. ワークフローのKubeflow Pipelines SDK V2への移行
  2. スケジュール実行されているワークフローへ前回実行分が終わるまでの待機処理を追加
  3. Vertex Pipelinesの監視

各取り組みの詳細を紹介します。

1. ワークフローのKubeflow Pipelines SDK V2への移行

Kubeflow Pipelines SDK V1からSDK V2への移行に際し、影響のある変更点として以下の点が挙げられます。

  • コンパイラのデータ型の制約が厳しくなった
  • ContainerOp APIが非推奨になった
  • Kubeflow PipelinesのPlaceholderを使用できなくなった

コンパイラのデータ型の制約が厳しくなった

Kubeflow Pipelinesでは、コンテナ化されたコマンドラインプログラムをコンポーネントとして記述できます。そして、そのコンポーネントはyamlで定義する方法の他に、Pythonで処理を定義しコンポーネントにできます。しかし、SDK V2では、Pythonで記述するコンポーネントの入出力に必ずデータ型を注釈する必要があるよう、仕様が変更されました。そこでサポートされる基本的なデータ型は str, int, float, bool, dict, list です。

他にもGCP関連のコンポーネントで使用される型や、大量のデータをアーティファクトとしてやりとりするための型が用意されています。今回移行するパイプラインでは、中間データはBigQueryに保存しておりアーティファクトは使用していないため、基本的なデータ型とGCP関連の型に関する修正を行いました。

from typing import NamedTuple

# SDK V2では動作しない
# NamedTupleに型を指定しても動かない
#   ref. https://github.com/kubeflow/pipelines/issues/5912#issuecomment-872112664
@component
def example(a: float, b: float) -> NamedTuple(
  'output',
  [
    ('sum', 'product'),
  ]):
  sum_ = a + b
  product_value = a * b

  from collections import namedtuple
  output = namedtuple('output', ['sum', 'product'])
  return output(sum_value, product_value)

# SDK V2で動作する
@component
def example(a: float, b: float) -> typing.Dict:
  sum_value = a + b
  product_value = a * b

  return dict['sum': sum_value, 'product': product_value]

基本的なデータ型は、コンパイラに型を正しく伝えるために全てのPython関数に対して型アノテーションをつけるように変更しました。また、GCP関連の型に関しては、以前str型で値を受け渡しできていたものができなくなり、専用のGCPProjectID型等を使用する必要があります。

しかし、GCPProjectID型等でデータの受け渡しをしてもコンパイラから型が間違っているとエラーが起きる状態になっています。この問題に関しては、メンテナーが改修したりドキュメントを整備しているようなので対応を待っている状態です2。なお、現状の回避策として、GCPProjectID型等で定義されている型をString等にコンポーネントのyamlを書き換え運用をしています。

また、SDK V2移行に伴い、ExitHandler APIが正しく動作しなくなりました。 ExitHandlerは、ExitHandler内に記述したタスクが終了したら終了ハンドラーを呼ぶオペレーターです3。これは、ExitHandlerを使用した際にWITH句内に記述しているタスクへ正しくパラメータが伝わっていないことが原因でした。このコンパイラ起因の問題は、Pull Requestで修正を加え、既にmergeされています。

ContainerOp APIが非推奨になった

SDK V2ではContainerOp APIが非推奨になり、代わりにコンポーネントを使用する必要があります。

ただし、Vertex PipelinesはVPCネイティブではないため、タスク毎に動的に外部アドレスが割り当てられます。これは、CDNへアクセスする際に固定の外部アドレスでアクセスしなければならない要件を満たすことができません。今回は、この問題を回避するためにGKE上でPodとしてタスクを実行するコンポーネントを作成しました。

下図がそのコンポーネントのイメージです。

上図のように、Vertex Pipelinesのワーカー内でGKEとの認証を通しPodを作成します。そして、Podが作成されたらPodが実行を正常または異常終了するまで待っています。

静的な外部アドレスをCloud NATにアタッチしたネットワーク環境下でGKEを構築することにより、外部へアクセスする際の外部アドレスを固定化できます。そして、そのGKEのPod上でタスクを実行することにより固定の外部アドレスでCDNへアクセスすることが可能となります。今回は既にセットアップされたGKEがあったためこの方法をとりましたが、Cloud RunのVPCコネクタを使用することで外部アドレスを固定しアクセスできます。

Kubeflow PipelinesのPlaceholderを使用できなくなった

Vertex PipelinesではKubeflow Pipelinesで使用できていたPlaceholderが使用不可能になりました。例えば、Placeholderには次のようなものがあります。

{{workflow.uid}}, {{workflow.name}}, {{workflow.status}}, {{workflow.creationTimestamp.Y}}, {{workflow.creationTimestamp.m}}, {{workflow.creationTimestamp.d}}

これらはワークフローの名前、終了ステータス、実行時間を取得するものです。しかし、このPlaceholderが使用できなくなったため、自前で代わりになるものを実装したり運用でカバーする必要が出てきました。

例えば、ワークフローの終了ステータスを取得するPlaceholderは、exit_handler内でSlackへ通知をする処理をしていました。しかし、ステータスの取得が不可能になったので、後述するCloud Scheduler + Cloud Functionsで代替機能を作りました。また、実行日時の取得もPythonのdatetimeモジュール等を使用して置き換えています。

2. スケジュール実行されているワークフローへ前回実行分が終わるまでの待機処理を追加

Kubeflow Pipelinesのスケジュールドワークフロー(Recurring Run)には、前回実行分が終わっていない場合に、後続のワークフローを待機させる機能がありました。

しかし、Vertex Pipelinesのスケジューリング機能はCloud Scheduler + Cloud Functionで構成されており、前回実行分を考慮せずに後続のワークフローをキックするようになっています。そこで、ワークフローのタスク内部から前回実行分のワークフローが終了しているかを確認し、終了していなければsleepして待つ実装をし、同等の機能を担保します。

def wait_previous_execution(pipeline_name: str, project: str, region: str):
    from google.cloud.aiplatform_v1.services.pipeline_service import PipelineServiceClient
    from google.api_core.client_options import ClientOptions
    from google.cloud.aiplatform_v1.types.pipeline_service import ListPipelineJobsRequest
    from datetime import datetime

    import time
    import pytz

    CURRENT_TIME = pytz.UTC.localize(datetime.utcnow())

    option = ClientOptions(api_endpoint=f"{region}-aiplatform.googleapis.com")
    client = PipelineServiceClient(client_options=option)


    REQUEST = ListPipelineJobsRequest(parent=f"projects/{project}/locations/{region}", filter='state="PIPELINE_STATE_RUNNING"')
    def _get_running_pipelines():
        result = client.list_pipeline_jobs(REQUEST)

        pipelines = [pipeline for pipeline in result if pipeline.pipeline_spec['pipelineInfo']['name']==pipeline_name]

        sorted_pipelines = sorted(
            pipelines, key=lambda pipeline: pipeline.create_time, reverse=True
        )
        # Ignore pipelines created after this one.
        filtered_pipelines = [
            pipeline
            for pipeline in sorted_pipelines
            if CURRENT_TIME > pipeline.create_time
        ]
        return filtered_pipelines

    running_pipelines = _get_running_pipelines()
    # Wait for the other pipelines to finish
    # The pipeline executing this function is also counted, so the condition is greater than 1
    while len(running_pipelines) > 1:
        time.sleep(120)
        running_pipelines = _get_running_pipelines()
    return None

3. Vertex Pipelinesの監視

私たちのチームでは普段からサービスの監視等にはCloud Monitoringを使用しています。しかし、Cloud Monitoringで利用できるVertex Pipelinesのメトリクスに有用なものが少ないため、監視の仕組みを内製しています。監視はCloud Scheduler + Cloud Functionsで行っており、Cloud Schedulerから定期的にCloud Functionsを叩き、アラートの閾値に達していないかの確認しています。以下が監視の仕組みのイメージです。

Cloud Functions内では以下のようなコードを使用し、Vertex PipelinesのAPIを叩き監視対象のパイプラインの成功可否と実行時間SLOが満たされているかをチェックします。

import crontab
from datetime import datetime, timedelta
import json
import requests
import os
import pytz
import math
import typing

"""
cron: "*/5 * * * *"
project: something-dev
pipelines:
- name: something
  slo_execution_time: 4h
  slo_time_format: '%Hh'
  region: asia-east1
environment: dev
slack_webhook: something
...省略
"""

SLACK_MESSAGE_FORMAT = """
{{
    "text": "{text}",
    "attachments": [
        {{
            "color": "{color}",
            "text": "{attachment_text}",
            "fields": {fields}
        }}
    ]
}}
"""

CRON = os.environ.get("cron")
ENV = os.environ.get("environment")
PROJECT = os.environ.get("project")
PIPELINES = os.environ.get("pipelines")
SLACK_WEBHOOK = os.environ.get("slack_webhook")

from google.cloud.aiplatform_v1.services.pipeline_service import PipelineServiceClient
from google.api_core.client_options import ClientOptions
from google.cloud.aiplatform_v1.types.pipeline_service import ListPipelineJobsRequest


class MonitorVertexPipelines:
    def __init__(
        self,
        project: str,
        monitor_schedule: crontab._crontab.CronTab,
        monitor_targets: dict,
    ):
        self.project = project
        self.monitor_schedule = monitor_schedule
        self.monitor_targets = monitor_targets

    def __n_times_previous_time(self, start, n):
        assert n > 0, "n must be greater than or equal to 1"

        def _previous(p, cnt):
            if cnt == 0:
                return p
            else:
                return _previous(
                    p
                    + timedelta(
                        seconds=math.floor(self.monitor_schedule.previous(now=p))
                    ),
                    cnt - 1,
                )

        return _previous(start, n)

    def __get_pipelines(self, region: str, name: str, filter: str):
        option = ClientOptions(api_endpoint=f"{region}-aiplatform.googleapis.com")
        client = PipelineServiceClient(client_options=option)

        request = ListPipelineJobsRequest(
            parent=f"projects/{self.project}/locations/{region}", filter=filter
        )
        pipelines = client.list_pipeline_jobs(request)
        return [
            pipeline
            for pipeline in pipelines
            if pipeline.pipeline_spec["pipelineInfo"]["name"] == name
        ]

    def finished_pipelines(self, current_time: datetime = datetime.utcnow()):
        previous_schedule_time = pytz.UTC.localize(
            self.__n_times_previous_time(current_time, 2)
        )

        result = []
        for target in self.monitor_targets:
            if not all(
                [
                    must_included_key in target.keys()
                    for must_included_key in ("region", "name")
                ]
            ):
                continue

            pipeline_name = target.get("name")
            pipelines = self.__get_pipelines(
                target.get("region"),
                pipeline_name,
                filter='state!="PIPELINE_STATE_RUNNING"',
            )
            pipelines = [
                pipeline for pipeline in pipelines if pipeline.end_time is not None
            ]

            # Ignore pipelines ended before previous monitoring time.
            pipelines = [
                pipeline
                for pipeline in pipelines
                if previous_schedule_time < pipeline.end_time
            ]
            result += pipelines
        return result

    def pipelines_not_satisfy_slo(
        self, current_time: datetime = datetime.utcnow()
    ):
        # NOTE default strptime value is 1900-01-01T00:00:00.000
        # ref. https://docs.python.org/3/library/datetime.html#technical-detail
        DEFAULT_STRPTIME = datetime.strptime("", "")
        previous_schedule_time = pytz.UTC.localize(
            self.__n_times_previous_time(current_time, 2)
        )
        current_schedule_time = pytz.UTC.localize(
            self.__n_times_previous_time(current_time, 1)
        )

        result = []
        for target in self.monitor_targets:
            if not all(
                [
                    must_included_key in target.keys()
                    for must_included_key in (
                        "region",
                        "name",
                        "slo_execution_time",
                        "slo_time_format",
                    )
                ]
            ):
                continue

            pipeline_name = target.get("name")
            slo_execution_time = target.get("slo_execution_time")
            slo_format = target.get("slo_time_format")
            slo = datetime.strptime(slo_execution_time, slo_format) - DEFAULT_STRPTIME
            pipelines = self.__get_pipelines(
                target.get("region"),
                pipeline_name,
                filter='state="PIPELINE_STATE_RUNNING"',
            )

            pipelines = [
                pipeline
                for pipeline in pipelines
                if previous_schedule_time < pipeline.create_time + slo
                and pipeline.create_time + slo < current_schedule_time
            ]
            result += pipelines
        return result


def monitor_vertex_pipelines(request):
    """Responds to any HTTP request.
    Args:
        request (flask.Request): HTTP request object.
    Returns:
        The response text or any set of values that can be turned into a
        Response object using
        `make_response <https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response>`.
    """

    now = datetime.utcnow()
    monitor_schedule = crontab.CronTab(CRON)
    monitor_targets = json.loads(PIPELINES)

    monitor = MonitorVertexPipelines(PROJECT, monitor_schedule, monitor_targets)
    finished_pipelines = monitor.finished_pipelines(now)
    pipelines_not_satisfy_slo = monitor.pipelines_not_satisfy_slo(now)

    for pipeline in finished_pipelines:
        notify_to_slack(
            ENV,
            pipeline,
            "monitor pipeline status",
            lambda pipeline: pipeline.state.name != "PIPELINE_STATE_SUCCEEDED",
        )

    for pipeline in pipelines_not_satisfy_slo:
        notify_to_slack(ENV, pipeline, "monitor pipeline slo", lambda _: True)

    return None


def notify_to_slack(
    env: str, pipeline: dict, attachment_text: str, danger_condition: typing.Callable
):
    display_name = pipeline.display_name
    pipeline_name = pipeline.pipeline_spec["pipelineInfo"]["name"]
    state = pipeline.state.name
    start_time = pipeline.start_time
    end_time = pipeline.end_time

    fields = [
        {"title": "Display Name", "value": display_name, "short": False},
        {"title": "Pipeline Name", "value": pipeline_name, "short": False},
        {"tile": "State", "value": state, "short": False},
        {"title": "Start Time", "value": str(start_time), "short": False},
        {"title": "End Time", "value": str(end_time), "short": False},
    ]

    if danger_condition(pipeline):
        text = "<!channel>" if env in ("stg", "prd") else ""
        color = "danger"
    else:
        text = ""
        color = "good"

    DATA = SLACK_MESSAGE_FORMAT.format(
        text=text,
        color=color,
        attachment_text=attachment_text,
        fields=json.dumps(fields),
    )
    requests.post(SLACK_WEBHOOK, data=DATA)
    return None

今後の展望

以上のように、Kubeflow PipelinesからVertex Pipelinesへの移行を実施してきました。現在は、よりVertex Pipelinesを快適に使えるよう、以下のことに取り組んでいます。

  • 各プロジェクトで使える便利共通コンポーネント集の作成
  • Vertex Pipelines用のテンプレートリポジトリの作成

各プロジェクトで使える便利共通コンポーネント集の作成

前述のGKE上でPodとしてjobを実行するコンポーネントであったり、GCPのSecret Managerから秘匿情報を取得するような便利コンポーネントをまとめたリポジトリです。このリポジトリをフェッチし、コンポーネントをロードするだけで、それらを利用できるような世界観を目指しています。リポジトリのCI/CD・テスト等の基本的な仕組みはできており、後はコンポーネントを追加するだけの状態まで到達しています。

Vertex Pipelines用のテンプレートリポジトリの作成

Vertex PipelinesのCI/CD、監視、スクリプト類がまとまったテンプレートリポジトリを用いて「開発の高速化/SREのキャッチアップコストの低下」を実現させるための取り組みです。前述の監視やSDK V2でパイプラインを記述する知見は社内に多くないので、先回りをし、より便利な環境を整えていくことで開発者/SREがストレスフリーにVertex Pipelinesを利用できる環境を目指しています。

まとめ

本記事ではKubeflow PipelinesからVertex Pipelinesへの移行により運用コストを削減させる取り組みを紹介をしました。Kubeflow PipelinesからVertex Pipelinesへ移行するコストは高いですが、Kubeflow Pipelinesをセルフホストした際の構築・運用コストからは解放されました。

現在、私たちのチームではバッチ処理の実行環境の整備以外にも、汎用的なML系サービスのサービング環境も構築中です。ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

corp.zozo.com hrmos.co

参考


  1. GCPのコンソールなどから確認する方法はありませんが、gVisor上に構築されるようです
  2. 同様のIssue(convert string to GCSPath object or create one #4710)と、それに対するメンテナー動向(Update KFP samples to use types that are compatible in v2. #5801
  3. ExitHandlerの例: exit_handler/exit_handler.py

ZOZO Researchと同志社大学の共同研究 〜研究の際に配慮した点と研究成果の紹介〜

OGP

こんにちは。株式会社ZOZO NEXTにあるZOZO ResearchのApplied MLチーム所属の後藤です。社内の様々な課題を機械学習を活用して解決する仕事に取り組んでいます。

弊社(当時は株式会社ZOZOテクノロジーズ)では2019年1月より、ZOZO Researchと同志社大学 桂井研究室の共同研究を開始しました。本記事では、共同研究を行う際のポイントと、その成果を紹介します。

続きを読む

自己学習するAIと推薦システムへの応用 〜 Open Ended Learningの紹介

opg

こんにちは、データシステム部のAnirudh Gururaj Jamkhandiです。私はECにおけるユーザーの購買率向上を目指して、推薦アルゴリズムの研究開発に携わっています。

高機能な計算機の登場により、現在では様々な業界で機械学習が飛躍的に利用されています。特に、深層学習は特定のタスクにおいては、人間の能力をはるかに超える結果を出しています。しかし、人間にとっては初歩的な能力である、自ら問題を生成したり他のタスクに一般化する能力はまだありません。近年、そういった課題を解決するための学習アルゴリズムの開発が盛んに行われています。本記事では、そのようなアルゴリズムの1つである「Open Ended Learning」を紹介します。

目次

はじめに

この10年間、機械学習の技術はこれまでになく発展し、実社会への導入が行われてきました。Artificial Intelligence Index Report 2019によると、グローバル企業の50%以上が、少なくとも1つの機能にAIを採用していると言われています。AIは楽観的な予測と大規模な投資が行われてきた一方で、特に自動運転・家事代行・音声アシスタント技術の開発においては、失望・信頼の喪失や投資減(「AIウィンター」)の時期も見られます。このような落ち込みの理由として考えられるのは、学習アルゴリズムが汎化されない、あるいは不測の事態にうまく適応できないことです。この問題は、アルゴリズムに収益を依存している企業、特にECにも大きく影響します。従って、不測の事態でもうまく機能する学習アルゴリズムの開発は実サービスにおいて重要な課題となっています。

ZOZOでは、機械学習アルゴリズムが様々な場面で利用されています。例えば、ユーザーへのアイテムのレコメンド、画像検索などがあります。これらのタスクでは、各領域の専門家が根本的な問題を特定し、指標やインプットを最適化することで問題解決する必要があります。特に推薦システムでは、データの少ない新規ユーザー、新規アイテム、多様性などに対応するモデル開発をすることになります。このような複雑な課題を認識し、かつ解決できるアルゴリズムはあるのでしょうか。本記事では、このような多様で複雑な問題を生成・特定し、同時に未知の状況にもうまく一般化して解決できる「Open Ended Learning(以下、OEL)」という手法を紹介します。

Open Ended Learningの紹介

state-of-the-artとされる既存手法からさらに改善する方法は、問題を選んだり時には作ったりして、それを解決しようとするアプローチでした。そうすることでアルゴリズムが改善され、それが課題解決に役立ちます。一方、人工生命の研究者が提唱する自然進化に基づくアプローチは、問題を解決するだけでなく、問題を自動生成するアルゴリズムを作ることです。OELとは、学習モデルが好奇心を絶やさず、自ら挑戦的な学習機会を生み出すような学習のことです。設定した問題のみを解決する機械学習アルゴリズムとは異なり、OELは私たちの想像を超える驚きを生み出してくれる可能性も秘めています。

Open Ended Learningの研究の現状

人工生命の研究者たちは、以前からOELの研究・調査をしてきました。しかし、取り組むべき課題の複雑さが増すにつれ、進化のために利用できるデータが足りないことに気付き、この研究が活発化し始めました。主要な例としては、Uber AIのWangらが「二足歩行ロボット」に適用したPOET(Paired Open Ended Trailbrazer)やDeepMindが「かくれんぼ」や「旗取りゲーム」などに応用した研究があります。いずれの研究も、ある環境で学んだ経験を別の環境に応用させるOELによって汎化性能を改善しています。最近よく見られるGenerative Adversarial Networks(GAN)OELの一種です。

ZOZOでも様々な状況でのレコメンデーションをシミュレートすることで、既存性能を超えるアルゴリズム開発を試みています。強化学習に基づき、逐次的なユーザー行動のモデル化を行い、長期的なエンゲージメントを最大化する手法です。

次に、このようなシステムとユーザー行動の相互作用を使って動的に変化させる、つまりインタラクティブな推薦システムの設計を紹介します

インタラクティブな推薦システムの問題設定

この推薦システムでは、セッションにおける報酬の最大化を目標とします。

セッション最適化では、状態  \mathrm{S}、行動  \mathrm{A}、報酬関数  \mathrm{R}、遷移確率  \mathrm{P}、割引係数  \gamma を持つMarkov Decision Process(MDP)としてモデル化できます。

  • 状態  \mathrm{S}

    • ユーザーの特徴(デモグラ、興味など)と過去の行動に関連した情報(過去の推薦結果、閲覧・クリック・カートに追加したアイテム、満足度など)の両方を表す
  • 行動  \mathrm{A}

    • 選択されたアイテム、Iは推薦アイテム候補
    • 選択し得るアイテムサイズkが固定されていると仮定すると、 \mathrm{A} \mathrm{A} \subset \mathrm{I} s.t.  \mathrm{|A|} = \mathrm{J} であり、 \mathrm{J} はアイテムサイズを表す
  • 遷移確率  \mathrm{P}(\mathrm{S^{'}} | \mathrm{S}, \mathrm{A})

    • 状態  \mathrm{S} で行動  \mathrm{A} をとったときに状態が  \mathrm{S^{'}} になる確率を表す
  • 報酬  \mathrm{R}(\mathrm{S}, \mathrm{A})

    • 行動  \mathrm{A} による期待報酬であり、行動  \mathrm{A} のアイテムに対するユーザーエンゲージメントの度合を表す

私たちはこのような推薦システムの様々な部分をモデル化するためにRecSimフレームワークを使用し、ユーザー、アイテム、ユーザー×アイテム間の相互作用のモデル化にOELを利用しました。

インタラクティブな推薦をサポートするRecSimとそのコンポーネント

RecSimは、推薦システムに強化学習を用いるためのシミュレーションプラットフォームです。推薦候補アイテム群(以下、ドキュメント)に対してユーザー行動のシミュレーションを実施する環境を作成できます。このフレームワークは、いくつかのコンポーネントで構成されています。

以下にコンポーネントの特徴を示します。

  • 環境は、ドキュメントモデル、ユーザーモデル、ユーザー選択モデルで構成される
  • エージェントは、推薦結果を作成するためのモデル(以下、ポリシー)を持ち、ドキュメントとユーザーの特徴を利用して推薦する
  • ドキュメントモデルは、ドキュメントの特徴(品質などの潜在的な特徴と、評価や人気などの観測可能な特徴)の事前分布からドキュメントをサンプリングする
  • ユーザーモデルは、ユーザー特徴(満足度、興味などの潜在的特徴、年齢などの観測可能な特徴、セッションの長さなどの行動的特徴)の事前分布から、ユーザーをサンプリングする
  • ユーザー選択モデルは、エージェントのレコメンデーションに対するユーザーの反応をエミュレートする
    • 具体的には、推薦されたドキュメントの特徴とユーザーの特徴を用いて、利用しそうなドキュメントを選択する
  • ユーザー遷移モデルは、ユーザー選択モデルからドキュメントが選択された後に、このモデルを介してユーザー状態を更新する

これらのコンポーネントによって強化学習を行います。エージェントが環境と相互作用し、その相互作用に対するフィードバックを受け取り、期待報酬を最大化することでアクションの選択を最適化します。

次に、どのように推薦システムをシミュレートするのかを説明します。

シミュレーションの各ステップは、4つのプロセスで構成されています。図1は各コンポーネントとステップの関連を示しています。

  1. ユーザーモデルからユーザー状態を、ドキュメントモデルからドキュメントの特徴を要求し、それらをエージェントに送る
  2. エージェント(推薦アルゴリズム)は、現在のポリシーを使用して、ドキュメントセットを返す
  3. ユーザー選択モデルがドキュメントを選択する
  4. ユーザー遷移モデルを用いてユーザーモデルを更新し、報酬によってエージェントポリシーを更新する

このステップ内のプロセスは、あらかじめ設定された終了条件が満たされるまで繰り返されます。そして、最初のステップから最終状態までのすべての状態を集めたものがエピソードです。

recsim_block

図1:RecSimコンポーネントの全体像(引用:https://arxiv.org/pdf/1909.04847.pdf

RecSimフレームワークを用いたアルゴリズムの設計

課題設定として、長期的なユーザー行動がモデル化された環境を目指します。なぜなら、過去の研究でユーザーの潜在的な状態は、レコメンデーションやサービスの変化に伴ってゆっくりと変化することが確認されているためです。この環境では、CTRは高いが満足度が弱いドキュメントもあれば、CTRは低いが満足度が高いドキュメントもあります。そのため、課題はこの2つのバランスをとり、長期的に最適なトレードオフを実現することです。満足度は潜在的な変数ですが、このシステムダイナミクスは部分的に観測可能です。満足度は、エンゲージメントの増減から推測できます。

このような環境に関するシミュレータは、以下のように設計されています。

ドキュメントモデル

モジュール特徴量の事前分布からモジュールをサンプリングします。モジュールの特徴量としては、CTR、CVR、価格などを使用します。そして、全モジュールのCTR、CVR、価格の平均と分散を求め、それぞれの特徴をガウス分布に当てはめてモジュール特徴量の事前分布とします。

ユーザーモデル

ユーザー特徴量の事前分布からユーザーをサンプリングします。各ユーザーは、net positive exposure ( \mathrm{npe_t}) と呼ばれる特徴量と、satisfaction ( \mathrm{sat_t}) と呼ばれる特徴量を持ちます。満足度は増加の抑制のため、ロジスティック関数を用いて表します。

 \mathrm{sat_t} = \sigma \, ( \tau \cdot \mathrm{npe_t})

ここで  \tau は ユーザー固有の感度パラメータ、t はエピソード内の時間ステップです。ユーザーがドキュメントを選択すると、 \mathrm{npe_t} は次のように進化します。

 \mathrm{npe_{t+1}} = \beta \cdot \mathrm{npe_t} + 2(\mathcal{C} - 0.5) + \mathcal{N}(0,\eta)

ここで、 \beta はユーザー固有の記憶割引(忘却因子)、 \eta はイノベーションの標準偏差です、 \mathcal{C} は CTR です。そして、これが「ユーザー遷移モデル」です。

 \mathcal{s_d} は、長期的なエンゲージメントの反応( \mu_k, \sigma_k)とパルス消費の反応( \mu_c, \sigma_c)を線形に補うパラメータを持つ対数正規分布で下記の定義とします。 \mu \sigmaはそれぞれの平均CTRと標準偏差です。

 \mu_s = (\mathcal{C} \cdot \mu_c + (1-\mathcal{C}) \cdot \mu_k) \cdot \mathrm{sat_t}
 \sigma_s = \mathcal{C} \cdot \sigma_c + (1-\mathcal{C}) \cdot \sigma_k
 \mathcal{s_d} ~  log \mathcal{N}(\mu_s,\sigma_s)


このように、ユーザーの状態は( \mathrm{sat_t}, \tau, \beta, \eta, \sigma_k, \mu_k, \sigma_c, \mu_c) の組み合わせで定義され、ユーザー状態の唯一のダイナミクスは満足度として表されます。

ユーザー選択モデル

ユーザー反応をシミュレーションするために、CatBoostをモジュールのクリック確率予測に用います。

報酬機能

ここでは目標を累積報酬で表します。また、報酬は目標に向けた中間的なフィードバック(正または負)を提供します。今回は推薦結果によってユーザーの総合的なエンゲージメントを向上させることを目標としています。全体的な報酬機能は、以下の2つの要素で構成されています。

  • ランキングベースの報酬 : エージェントはモジュールの順位を直接予測する代わりに、順位式  \alpha, \phi, \delta の係数を予測するように学習します。そして、予測された係数を用いて、各モジュールのスコアを計算します。モジュールiのスコアは次のように与えられます。

     \mathrm{rankscore(i)} = \mathrm{ctr(i)^{\alpha}} \cdot \mathrm{cvr(i)^{\phi}}  \cdot \mathrm{price(i)^{\delta}}
    次に、上位k個のスコアを抽出し、ポジションバイアス \mathrm{W}を割り当てます。そして、最終的なランキング報酬は、モジュール報酬の加重和として計算します。
     \mathrm{R} = \sum\limits_{i=0}^\mathrm{T} \mathrm{rankscore(i)} \cdot \mathrm{W}

  • エンゲージメントベースの報酬:ユーザー選択モデルがエージェントの推薦に対するユーザーの反応を予測すると、ユーザーは選択したモジュールに  \mathcal{s_d} 秒(先に定義した)エンゲージメントします。エンゲージメント時間は、エージェントの推薦に対するユーザーの満足度としてフィードバックします。つまり、 \mathcal{s_d}を報酬として使用します。

「ランキングベースの報酬」はモジュールを適切にランキングした場合の報酬で、「エンゲージメントベースの報酬」はモジュールをクリックした場合の報酬です。最終的な報酬は、ランキングベースの報酬とエンゲージメントベースの報酬の合計です。

エージェント

POETの後継となるEnhanced POETというOELアルゴリズムを使用しています。POETの基本的な考え方は以下の通りです。

  1. ノベルティサーチ :

    従来、機械学習・深層学習・進化アルゴリズムを含む学習アルゴリズムは、特定の目的関数を解決するために使用されてきました。生物学的な進化は人間の知能を生み出す重要な要因の1つであり、自然界では全体的な目標がなく、ある機能のために進化した機能が他の機能に使われることもあります。従って、推論のルールをハードコーディングしたり、特定の性能指標で高得点を取るために学習するのではなく、新規性や興味深さを優先します。実際に、ある目的を完全に無視することで、その目的を追求するよりも早く最適化している事例もあります。

  2. ゴールの切り替え :

    1つのエージェントのみを使って新規性のある行動を生み出すのではなく、様々なニッチなタスクと各タスクのそれぞれで良い結果を出すエージェントを保持します。各エージェントは、自分のニッチなタスクで最適化された後、別のニッチな問題でも再度評価されます。もしそのエージェントが他のニッチなタスクで良い結果を出せば、そのエージェントは新しい目的のために最適化されます。従って、興味深い方向にアイデアを追いかけることでアルゴリズムは多様な結果を生み出し、問題を解決できます。

  3. 最小基準共進化(Minimal Criterion Coevolution)と品質の多様性 :

    自然界では、繁殖するために長く生き残るという基本的な原則に従っています。このシンプルな原理により、私たちは多様で複雑な環境を作り出すことができます。MCCでは相互作用する2つの集団を進化させることで、他の母集団に対して閾値(最小基準)を満たすことで生存できるようになり、オープンエンド(制約のなさ)を促進します。

アルゴリズム:POET

algorithm_image

図2:POETアルゴリズムの疑似コード(引用:https://arxiv.org/pdf/1901.01753.pdf

このアルゴリズムは、図2のようにランダムに初期化された1つの「環境⇔エージェント」のペアから始まります。その後、POETはメインループの中で以下の3つのタスクを実行します。

  1. ペアになったエージェントを各環境に最適化する
    この際の最適化アルゴリズムには、進化戦略を用います。

  2. N(mutate)インターバルごとに対象となる環境パラメータを変異させる
    残したい環境は、ペアとなったエージェントがある閾値を超えている環境です。ある環境が変異元の対象となった場合、まずその環境の子環境を作るために数回の変異(新規性の探索)を行います。その後、新しい環境が簡単すぎず、かつ難しすぎない(品質の多様性を確保する)ことを保証するために、元のペアエージェントとMCCで照合します。これらの操作後に残った環境を異なる環境とペアになったエージェントとテストし、最も良いパフォーマンスを示したものを新しいペアエージェントとして残します。
    mutation_image

    図3:平原の環境に切り株が発生した環境変異の例(引用:https://arxiv.org/pdf/2003.08536.pdf

  3. N(transfer)インターバルごとに対象となる環境パラメータを変異させる
    このステップではすべてのエージェントが各環境でテストされ、どれかが元のペアのエージェントよりも優れた性能を発揮した場合、より優れたエージェントに置き換えられます。図4の  \theta はエージェントのポリシーです。
    goal_switching

    図4:ゴールの切り替えを行いながら変異するステップ(引用:https://icml.cc/media/icml-2019/Slides/4336.pdf

アルゴリズム:Enhanced POET

POETを汎用的に利用するために、従来では環境の分布パラメータとして表現されていた部分が環境エンコーディング(EE)と環境キャラクタリゼーション(EC)に分離されました。

EEには座標を入力して幾何学的なパターンを生成するニューラルネットワークであるCPPN(Compositional Pattern Production Network)を提案し、ECにはPATA-EC(Performance of All Transferred Agents Environmental Characterization)という指標を提案しています。これは環境の新規性評価には相応の対処が必要であるという考えに基づき、新環境ではすべてのエージェントでその環境との報酬を算出します。そして、相対的なエージェントの順番がどれだけ違うかによって新規性を評価します。

このように新しい環境を生成することで、新たな挑戦を続けるアルゴリズムがPOETです。ペアエージェントは、ニューラルネットワークで表現され、期待報酬を最大化するために状態(ユーザー状態とRecSimでシミュレートされたモジュールの状態)と行動(ランキング生成のための係数)を対応させるポリシーを学習します。

トレーニングプロセス

training_process

図5:トレーニングプロセスの全体像(引用:https://arxiv.org/pdf/1902.00851.pdf

図5は、エージェントがユーザーと相互作用し、報酬(≒エンゲージメント)を最大化するトレーニングプロセスです。学習は、シミュレータがドキュメントモデルとユーザーモデルからそれぞれの特徴量を要求し、エージェントに送信することから始まります。エージェント(Enhanced POET)は、現在のポリシーを使ってランキング係数を予測し、推薦結果を生成します。ユーザー選択モデルは、その推薦結果に対してモジュール特徴量とユーザー状態を考慮し、ユーザーの選択を予測します。シミュレータは、ユーザー遷移モデルを用いてユーザーモデルを更新し、ユーザーの反応と報酬を用いてエージェントポリシーを更新します。

ZOZOにおけるOpen Ended Learningの推薦システムへの応用

問題設定

現在、ZOZOTOWNのトップページのデザインは「モジュール」構造になっています。「モジュール」とは、図6のようにセール対象商品や新作商品などの特集化されたコンテンツ集合を表しています。

図6:「チェックしたアイテム」モジュールの例

そして、このページではユーザー体験を向上させるために、主に2種類のパーソナライズド・レコメンデーションを提供しています。図7のようなモジュール内での商品推薦とモジュール順序の最適化です。本記事では、後者のモジュール順序の最適化に焦点を当てます。

図7:モジュール・ランキングによるZOZOTOWNのカスタマイズ

また、ZOZOTOWNには、毎日多くの新規ユーザーが訪問し、新着アイテムも多く追加されます。このようなデータの少ないユーザー、アイテムではコールドスタート問題が発生します。

さらに、レコメンデーションに多様性を持たせることで、ユーザー体験の向上が期待できます。

ZOZOTOWNでは、RecSimフレームワークを使用した推薦環境でモジュールランキングのオフライン実験を行いました。

なお、学習プロセス全体は、大規模な実データに対応するためFiber Frameworkを用いて並列化させます。本ケースでは、20の環境とそのペアエージェントをアクティブなものとして実験しています。学習の進捗状況の測定には、研究論文と同様に累積新規環境作成・解決数 ANNECSという指標を使用しています。ANNECSスコアは、新しい環境がMCCをクリアし、かつ設定した報酬の閾値を超えた環境数のカウンターです。そのため、本スコアはニッチであり、有意義な変異を実現した指標となります。

実験結果と考察

結果としては、図8のように学習が進むにつれANNECSスコアは増加し、アルゴリズムがますます有意義な課題を生み出していることを示しました。最終的に10,000イテレーションの学習後、ANNECSスコアは130以上となりました。

annecs_graph

図8:イテレーション数とANNECSスコア

この実験の主な目的は、OELを推薦タスクに応用して複雑な問題を解決すると同時に新しい問題を見つけ出し、またある環境での進化が別の環境でどのように適応するかを確認することでした。そして、シミュレータを使ったオンライントレーニングで20の環境とそのペアエージェントを作成した後、これらをオフライン評価で比較し、最も性能の良いモデルを1つ選定しました。

MODEL PRECISION@10 RECALL@10 NDCG@10
Collaborative Filtering 1.929 13.669 0.0599
NCF 2.522 17.878 0.0868
LamdaMART 2.321 17.683 0.0849
BPRMF 2.483 17.6 0.0837
Direct Curriculum using ES 2.462 17.532 0.0828
Enhanced POET 2.598 17.789 0.0891

このモデルを他のランキングモデルと比較したところ、OELを用いた推薦システムで他の手法よりも優れた結果を得ることができました。この結果とANNECSスコアの増加グラフにより、アルゴリズムが新しい問題を見つけ、それを解決できたことを示しています。

結論と今後の課題

OELが何を発見し、どのような未来をもたらすかはわかりません。そのため、この不確実性に懐疑的な人はPOETのようなシステムを「ランダム性をもたらすアルゴリズム」と解釈するかもしれません。しかし、進化の考え方にヒントを得て不確実性を取り入れたOELは、昨今興味深い研究や多くのコンテストで盛り上がりを見せています。将来的には、レコメンデーションシステムをはじめとする様々なアプリケーションで、このようなおもしろいアルゴリズムが利用されるかもしれません。

ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

hrmos.co

意思決定の理由の可視化が可能なグラフ構造の学習アルゴリズムの紹介

OGP

ZOZO研究所の清水です。弊社の社会人ドクター制度を活用しながら、「社内外に蓄積されているデータからビジネスへの活用が可能な知見を獲得するための技術」の研究開発に取り組んでいます。

弊社の社会人ドクター制度に関しては、以下の記事をご覧ください。 technote.zozo.com

私が現在取り組んでいるテーマの1つに、「機械学習が導き出した意思決定の理由の可視化」があります。この分野は「Explainable Artificial Intelligence(XAI)」と呼ばれ、近年注目を集めています。

図.XAIに関連する文献数の推移(引用:https://arxiv.org/abs/1910.10045

その中でも今回はユーザに対するアイテムの推薦問題に焦点を当て、「なぜこのユーザに対して、このアイテムが推薦されたのか?」という推薦理由の可視化が可能なモデルを紹介します。

本記事の概要

  • 機械学習から得られた意思決定の理由を明確にすることの必要性が増している
    • 「XAI」と呼ばれる研究領域が注目されている
  • Attentionを用いて推薦理由を可視化・解釈可能な、Knowledge Graph Attention Networkという手法を紹介する
  • ZOZOTOWNに蓄積されているデータにKnowledge Graph Attention Networkを適用してみた結果の一部を紹介する

目次

背景

機械学習が抱える解釈性の課題

近年、機械学習が人間の意思決定を支援したり、代替するような場面が徐々に増えてきています。弊社のサービスにおいても、例えばZOZOTOWNでユーザに推薦するアイテムの選定をする場面など、あらゆる場面で機械学習が活用されています。

機械学習によるアイテムの推薦では、蓄積された過去の購買履歴データなどの情報から、「このユーザにこのアイテムを推薦すべき」という情報を得ます。そして、得られた情報に基づいて、アイテムの推薦が行われています。以下では、この一連を仕組みを「推薦システム」と呼びます。なお、推薦システムの内部で利用されている技術に関しては、過去の記事で紹介しているので、併せてご覧ください。 techblog.zozo.com

しかし、この推薦システムを含む、機械学習を用いた意思決定システムは「なぜそのような決定に至ったのか」という意思決定の理由については不明瞭である場合が多いです。不明瞭なままでは良くないとされる場面の分かりやすい具体例としては、自動運転の技術を搭載した自動車に関するものが挙げられます。自動運転の技術を搭載した自動車が事故を起こした場合、事故を起こした際の意思決定の理由が解釈できなければ、改善点を考察することや責任の所在を明らかにすることは困難です。そして、この問題が自動運転の技術を実用化する上での1つのハードルとなっていることは間違いないでしょう。

なお、このような機械学習が抱える解釈性の課題から、消費者庁が発表したAI利活用ガイドラインにおける「AI利活用原則(AI利用者が留意すべき事項)」には「透明性の原則」が含まれています。

Explainable Artificial Intelligence

この問題に対して、「なぜそのような決定に至ったのか」を説明するための研究分野が存在します。この分野はExplainable Artificial Intelligence(XAI)と呼ばれ、特に中身が複雑なディープラーニング技術の実応用が話題となっている現代において注目を集めています。

下図はディープラーニングやアンサンブル学習を用いるような精度の高いモデルほど、モデルの解釈性は低くなるというトレードオフの関係を表しています。

図.モデルの解釈性と精度のトレードオフについて記述した図(引用:https://arxiv.org/abs/1910.10045

図における「高い精度であるが解釈性は低い」と位置付けされているモデルが、近年多くの分野で大きな成果を発揮しています。このことを背景とし、モデルを解釈しようとする当分野も注目を集めています。

この分野の研究に関する情報は、以下の資料にまとまっています。

Explainable Recommendation

説明可能な推薦システムに関する研究も多数発表されており、Explainable Recommendation: A Survey and New Perspectives にはそれらが体系的にまとまっています。対象問題が推薦システムであるため、機械学習によって「なぜこのユーザに、このアイテムが推薦されたのか」という推薦理由を把握することを目的としています。

前述の文献などにおいてEplainable Recommendationは、説得力・有効性・ユーザ満足度などを向上させるのに役立つとされています。そして、実際に様々な企業からこの分野に関連する研究成果が発表されています。

以下はその一例です。

また、以下の事例が実際にサービス化されている分かりやすい例です。

このEplainable Recommendationのアプローチの仕方は、大きく分けて以下の2種類です。

  • model-agnostic approach(=post-hoc approach)
    • 推薦モデルとは別に推薦理由を解釈(説明)するためのモデルを学習する方法
  • model-intrinsic approach
    • 何らかの工夫により、予め解釈可能な推薦モデルを学習する方法

model-agnostic approach

model-agnostic approachでは、まず推薦モデルを学習させ、その次に推薦の理由を説明するためのモデルの学習を別途行います。

このアプローチでは、事後の学習によって推薦理由の解釈を得るため、推薦モデルから直接理由が得られている訳ではありません。故に「本当に意思決定用のモデルを正確に説明できている(理由が正確に表現できている)」という保証はありません。また、そこに対して様々な工夫もされていますが、今回は詳しく扱いません。

しかし、この方法を用いると意思決定モデル自体を、どれだけ複雑にしても問題にならないというメリットがあります。

また、人間の意思決定メカニズムは以下のステップで行われる場合もあります。

  1. まず直感的な意思決定を行う
  2. その意思決定に対して、後から理由付けを行う

この意思決定のパターンを再現しているという意味では面白いアプローチです。具体的には、「とあるアイテムに一目惚れして購入を決意した後に、なぜこのアイテムが気に入ったのかを後から考える」ような流れを再現していると言えます。

このアプローチの関連研究として以下のものが挙げられます。

model-intrinsic approach

前述のmodel-agnostic approachと比較し、model-intrinsic approachは、意思決定の理由を推薦モデルから直接獲得できる点が異なります。

こちらのアプローチでは、最初から合理的な理由に基づいて意思決定を行うような状態の再現を目標としています。具体的には、どのポイント(ブランドや値段など)をどのくらい重要視するのかなどを考慮しながら、最終的にそのアイテムを購入するかを決定する流れを再現しています。この状況において、他者から「なぜそれにしたの?」と質問された場合に回答する理由は、後付けしたものではなく、購入に至った正確な理由であるはずです。

こちらのアプローチの難しい点としては、このモデルの出力が直接推薦に活用されるため、説明可能性を担保しながらも高い推薦精度を実現する必要があることです。また、闇雲に活用したい全ての補助情報をモデルに学習させることは推薦モデル自体の精度の低下や計算時間の増加を招くため、どの補助情報を活用するかについても精査する必要があります。

なお、関連研究を本記事の末尾にいくつか挙げているので、興味のある方はそちらをご覧ください。

Attentionを用いた意思決定の理由の解釈

model-intrinsic approachの中でも、近年注目されているAttentionなどを用いることで高い推薦精度を保ちながら、意思決定の理由を直接的に解釈可能とする方法があります。この方法を用いることで、分析者は「なぜこのユーザにこのアイテムを推薦したか」という理由を、いくつかの要素とその寄与の大きさに分けて把握することが可能になります。「いくつかの要素」の部分は入力データとした情報に含まれる要素(ユーザ・アイテム・それらの補助情報など)となり、「寄与の大きさ」の部分はAttentionで表現します。

この類の手法は近年脚光を浴びており、多くの手法が提案されています。その中でも今回はKnowledge Graph Attention Network(KGAT)を次章で紹介します。KGATはAttentionを用いて、どの繋がりを重視するかを考慮しながら、グラフ構造のデータを学習するGraph Attention Networks(GATs)をベースとした推薦モデルの一種です。

図.グラフ構造のイメージ

Knowledge Graph Attention Network

概要

Wangらは、ユーザとアイテムの2部グラフと、アイテムとアイテムの補助情報からなる知識グラフを使ったGATsベースの推薦モデルを提案しました。アイテムの補助情報とは、ZOZOTOWNのデータで言えば、ショップやブランドなどのアイテムに付随する情報です。

このモデルはGraph Neural Networksと呼ばれるモデル群において、関係性の学習にAttentionを採用したGATsに、補助情報(知識グラフ)を取り入れたモデルとして位置付けられます。ディープラーニングにより特徴量を自動的に獲得するend-to-end方式の学習を実現することで、複雑な顧客の嗜好やネットワーク構造の学習を可能にしています。

また、補助情報を用いることで、購買履歴データが十分に蓄積されていないユーザに対しても精度の高い推薦を実現します。さらに、得られたAttentionを分析することで推薦の根拠を示すことができるため、このモデルはXAIの分野においてはmodel-intrinsic approachに位置付けられます。

構築された推薦システムの出力の根拠を人間が解釈可能な形で示してくれるため、実際のマーケティングにおけるデータに応用することで強力な成果を発揮することが期待できます。

モデル構造と学習

KGATは3つのレイヤーを通して学習を遂行します。

それぞれのレイヤーの概要を以下で解説します。

  • CKG Embedding Layer
  • Attentive Embedding Propagation Layer
  • Prediction Layer

本記事では、より多くの方に概要だけでも理解していただけるよう、数式を記載せずに言葉で解説をしていきます。それが逆に分かりにくい方は、原著の論文と照らし合わせながら読んでいただけると幸いです。

KGATのモデルの全体像は以下の図の通りです。

図.KGATの構造のイメージ (引用: https://arxiv.org/abs/1905.07854

CKG Embedding Layer

ユーザとアイテムの2部グラフと、アイテムやユーザの補助情報からなる知識グラフを併せた「協調知識グラフ」の構造を保持した(各ノード・エッジに対する)埋め込み表現を獲得します。

学習の際は、TransRというグラフ埋め込みの手法を用いて各Tripletをベクトル化し、グラフ上に存在するTripletと存在しないTripletの差を最大化するようにパラメータを更新します。なお、Tripletとは、先頭ノード・ エッジ・末尾ノードの3点セットのことを指します。

図.Tripletの例

これにより、「ユーザ」と「ユーザが購入したアイテム」、「ユーザ」と「ユーザの補助情報」、「アイテム」と「アイテムの補助情報」のノード同士が埋め込み空間上で近くに配置されるように埋め込み表現が学習されていきます。

図.CKG Embedding Layerのイメージ

Attentive Embedding Propagation Layer

各ノードやエッジに対する埋め込み表現をもとに、各Tripletに対して重要度を算出します。そして、この重要度をもとにどの関係性を重視するかを考慮しながら、Prediction Layerにおいて購買確率の算出に利用するための埋め込み表現を、各アイテムとユーザに対して算出します。

この層は、 L層の構造を有しており、 L次近傍( L個先の隣接したノード)までの関係性を考慮可能です。周辺ノードの埋め込み表現を各Tripletの重要度で重み付けした平均値を算出し、周辺ノードの特徴を集約することで新しい埋め込み表現を獲得します。そして、このレイヤーから得られた各アイテムとユーザに対する新たな埋め込み表現を、次のPrediction Layerにおいて購買確率の算出に利用します。

つまり、推薦において重要と判断される L次近傍の関係性に従って、より洗練された、各ユーザとアイテムに関する購買確率の計算に用いるための埋め込み表現を獲得します。

図.Attentive Embedding Propagation Layerのイメージ

Prediction Layer

Attentive Embedding Propagation Layerから得られた各ユーザと各アイテムに対する埋め込み表現をもとに、各ユーザとアイテムのペアについて、購買確率を算出します。そして、実際に購買が発生したユーザとアイテムのペアに対して計算されるスコア(購買確率)と、発生していないペアに対して計算されるスコアの差が大きくなるように学習を遂行します。

図.Prediction Layerのイメージ

学習の仕組み(まとめ)

長めの説明となってしまいましたが、学習の仕組みを簡単にまとめると、以下のようになります。

入出力

  • 入力
    • 購買履歴データと、アイテムやユーザの補助情報を併せたデータ
  • 出力
    • 各ユーザが各アイテムを購入する確率

損失関数

  • Pairwise Ranking損失
    • 埋め込み空間上におけるノードの位置関係が入力データの構造に則っているかに関する損失
  • Bayesian Personalized Ranking損失
    • 埋め込み表現などから算出した購買確率がユーザの行動を再現できているかに関する損失

上記の双方を考慮して学習を遂行。

実験

ここまで紹介してきたKGATを、ZOZOTOWNに蓄積されている購買履歴データと各ユーザとアイテムの補助情報に適用し、得られた結果を用いて実際に推薦理由の可視化を行ってみた例を紹介します。さらに、推薦精度の評価実験を行い、KGATが推薦精度の面でどの程度有効であるのかを確認した結果を紹介します。

実験条件

今回の実験では、2020年2月〜2021年1月の1年間の購買回数が5回以上60回未満のユーザからランダムにサンプリングを行い、抽出された購買履歴データを利用します。

補助情報にはアイテムのブランド・ショップ・カテゴリ・価格帯、ユーザの年代・性別・お気に入りブランドやショップなどの全17種類のデータを用いました。購買履歴データは約30万件、補助情報は約70万件です。

こちらの文献の実験条件を参考に、各グラフの埋め込み表現の次元数は64、Attentive Embedding Propagation Layerは[64・32・16]次元の3層としました。また、確認する精度指標も同様の決め方で、TopN精度(Recall・NDCG)としています。

比較手法は補助情報を用いない手法のベースラインとしてBPRMF、同じく補助情報を含んだグラフ構造を学習する手法であるCKEとCFKGとしています。

推薦理由の可視化の例

まず、記事の前半でメイントピックとして解説してきた「推薦理由の可視化」についてです。KGATの出力結果をそのまま用いることで、各ユーザに対して「なぜそのアイテムが推薦されたか」を容易に説明できます。今回はどのような形で説明可能になったのかを紹介するために1つだけ例を紹介します。

以下の図は、実際に得られた結果の中から抽出してきた例で、「ユーザ u_1に対してアイテム i_5が推薦されている」状況を表現しています。ユーザ u_1から、実際に推薦されたアイテム i_5までのノードとエッジを辿り、それらの重要度を確認することで推薦理由を把握できます。

図.推薦理由の可視化マップの例

このグラフを見ると、ユーザ u_1に対してアイテム i_5が推薦された理由は、以下の点であることが分かります。

  • 同ブランド e_1のアイテム i_1を過去に購入していること
  • ブランド e_1をお気に入り登録していること
  • アイテム i_1と同じショップ e_2のアイテムであること
  • アイテム i_1と同じタイプ e_3のアイテムであること

また、重要度(エッジ上に記載されている数値)を確認することで、それぞれがどの程度推薦に寄与しているのかを定量的に把握できます。

この結果を活用し、ユーザ u_1にアイテム i_5を推薦する際、アイテム i_5がブランド e_1のアイテムであることを強調するなどの施策が容易に考えられます。また、推薦するアイテムと併せて、単純に推薦理由とスコアを並べて表示する施策も考えられます。これにより、ユーザは「だからこのアイテムを良いと感じるのか」「だからあまり良いと思わないアイテムが推薦されたのか」のように、納得感を持って買い物を楽しめるかもしれません。

今回は結果の活用事例として推薦理由の可視化のみを紹介しました。しかし、実際には他にも各ノード(アイテム・ユーザ・補助情報)やエッジに対して埋め込み表現が得られているので、これらを分析することも施策立案の一助となります。実際に適用して得られた結果を多角的に分析した結果、改めてとても汎用性の高いモデルだなと感じています。

推薦精度に関する評価

以下に示す結果の表を見ると、KGATは同様の補助情報を用いる他の手法と比較して、精度面でも有効なモデルであることが分かります。特にBPRMFよりも精度が高いことから、補助情報を活用することの有効性を示唆しています。

また、CKEやCFKGよりも精度が高いことから、KGATが上述した学習アルゴリズムを通して補助情報を含んだグラフ構造を上手に学習できていることが考えられます。

表.評価実験の結果比較

また、下図より、他の手法と比較して学習データ内にまだ多くの購買履歴データが蓄積されていないユーザ群に対しても、ある程度頑健な推薦ができていることが分かります。つまり、コールドスタート問題にも対応できていると言えます。

図.学習データに含まれる購買履歴数で層別した各ユーザ群に対する推薦の精度

推薦精度に関する評価実験の結果をまとめると、以下のことが分かります。

  • KGATが他の類似したモデルと比較して、高い推薦精度が期待できるモデルであること
  • 過去にZOZOTOWNであまり商品を購入していないユーザに対しても、ユーザやアイテムの補助情報を上手に学習し、効果的な推薦ができていること

関連手法

XAIの文脈でKGATを紹介しましたが、Attentionを活用した解釈可能な推薦システムの研究としては以下のものも挙げられます。

また、知識グラフの学習をベースとする解釈可能な推薦システムの研究としては、以下などが挙げられます。

終わりに

今回はXAIの文脈で、KGATの紹介をしました。この分野は現在非常にアツく、様々な方法が提案されているので、将来的にはそれらを網羅的に紹介する記事も執筆したいと思います。最後までお読みいただき、ありがとうございました!

ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください!

tech.zozo.com

画像ベースの仮想試着の実用化に向けた課題とアプローチ

OGP

こんにちは。ZOZO研究所の後藤です。普段はZOZOTOWNの推薦システムの開発や社内で利用するための機械学習システムの開発に携わっています。

本記事では、近年目覚ましい進展を見せている画像ベースの仮想試着の研究を紹介し、実用化を考える際に解決すべき課題とアプローチの考察も併せて紹介します。

続きを読む
カテゴリー