【ちょっとコード】Vertex AI マルチモーダル検索の利用シーン整理と最新実装例2024
ロンラン株式会社 CEO 兼 CTO の武部です。
ロンランは「市民スポーツ DX」を掲げて toC サービスを提供しているスタートアップです。現在提供中のサービス Athty の体験改善のため、2023 年夏頃から話題の Vertex AI マルチモーダル検索 と、出来立てほやほやの Gemini の R&D を行いました。
Gemini はまだプレビュー版で商用利用ができず、大規模で本格的な運用に向けては機能不足もあったため、GA 後に記事をお届けしたいと思います。
この記事では、現在商用利用も可能な Vertex AI マルチモーダル検索を対象に、次について紹介します。
- マルチモーダル検索の特性から考える利用シーンと注意点
- 対象読者:プロダクトマネージャー、アーキテクト、ソフトウェアエンジニア
- GCP 上の UI を活用し、ちょっとコードを書く程度で実現できる最新の実装・設定
- 対象読者:ソフトウェアエンジニア。利用可能な GCP 環境があり、簡単な Python コードが読解できること
- 所要時間:2 時間から 4 時間程度
AI を活用したアプリケーションの今日
私は 2015 年頃から機械学習/深層学習に取り組んでいるのですが、昨今は Google をはじめとしたビッグ・テックが提供する AI サービスの API / フルマネージドサービスの恩恵で、さまざまな AI 活用機能が Web アプリケーション開発の延長線上で組み上げられる時代になったと感じています。
Web 系のソフトウェアエンジニア、特にバックエンドエンジニアであれば、AI アプリケーションの開発はもちろんのこと、運用に必要なパイプラインの設計と設定も得意なところです。データ量、負荷、セキュリティ、フェイルセーフなプログラム、モニタリング、そしてコスト管理の経験豊富なソフトウェアエンジニアが、ワンストップで AI アプリケーションを開発し始めています。
このおかげで、機械学習エンジニアやデータサイエンティストは基礎研究に集中したり、専門的なタスクのためのモデル構築に注力できます。彼らの取り組みから、私たちの社会を飛躍的に変化させるブレイクスルーが生まれています。
他方、ソフトウェアエンジニアが担う応用領域の敷居が下がっていることで、希少職種である機械学習エンジニアやデータサイエンティスト不在でもビジネスニーズを実現できるケースがかなり増えてきた実感があります。
加えて、今回取り上げているマルチモーダル検索を構成する Google Cloud Platform のサービス群は、製品発表から少し時間が経ったこともあり UI 上で設定できることが増えています。逆にいうと、コードでなければ実現できない箇所が減り、導入負担が以前よりもさらに減っています。ソフトウェアエンジニアであれば、Python の基礎を学べばアプリケーション&パイプラインを構築できると感じました。
マルチモーダル検索のインパクト
あらためてマルチモーダル検索がもたらす可能性について。2023 年 8 月に Google からポストされた次の記事は大きなインパクトでした。
個人的に衝撃的だったのは次の三点です。
- 業界固有のデータセット収集や、追加のトレーニングを必要とせず、すぐに使い始められる
- 画像を対象に、テキストクエリでのセマンティック検索を実現
- 検索レスポンスは極めて速く msec 単位
いつかそんな時代が来るとは思っていましたが、とんでもない早さでやってきましたね。
マルチモーダル検索の特性を踏まえた利用シーンと注意点
プロダクトマネージャーやユーザー寄りのアーキテクトにとっては、マルチモーダル検索の利活用シーンこそが一番の関心ごとではないでしょうか。
私も普段そういった立場で、自社やお客様のためにサービス・機能を考案する立場です。今回の R&D では「マルチモーダル検索を実際のアプリケーション/サービスでどう活用できるか」という視点で情報を整理し、特性を把握しました。まとめると、次のとおりです:
-
画像やテキストでクエリできる
- なおこのような検索手法を専門的には 最近傍探索 と表現されます
- テキストの場合、意味をもつ自然な英語でクエリできる(例:
A person with clothes a black shirt
)
-
結果は類似度/マッチングの高い順にソートされた、アイテム ID のリスト
- このリストには フィルタを適用できる。フィルタには次の二種がある:
-
カテゴリ
: カテゴリやカラーといったテキストのフィルタ -
数値
:価格や在庫いった数値によるフィルタ(2023 年 12 月時点では Pre-GA)
-
- このリストには フィルタを適用できる。フィルタには次の二種がある:
テキストクエリについては早合点しないでください!これはプロンプトです。チャットのような UI を前提としているわけではありません。
さて、結果をこのまま活用可能かどうかについては、よく考える必要があると思います。
画像クエリの場合、結果は類似度が高いアイテム順に並び、テキストクエリの場合、結果はマッチしていそうなアイテム順に並びます。
となると「リストをどこで区切るか」が難しいところです。際限なく「似ているかもしれないアイテム」を表示しても、ユーザー体験はよくありません。前述の Google の記事で紹介されているメルカリの 580 万件のアイテムを用いたデモでも、検索キーワードによってはわりと早い段階で、人間が見て感じる「似たアイテム」が枯渇してもなお、なんらかのアイテムがリストに含まれるケースが確認できます。
極論、類似しているアイテムがまったくなかった場合でもなんらかの結果リストは返ってきます(デモで iPhone 20
のようなキーワードで検索してみれば分かります)。
Vertex AI マルチモーダル検索に、存在しないアイテムをクエリした結果
OK。では結果スコアが一定の値より低いアイテムは切り捨ててしまえば... という発想に至ります。しかしこれも難しいです。結果スコア(※この後に触れる Vector Search では distance
という値)は、アイテムの類似可能性を表す絶対値ではありません。実際 distance
値が低くとも「これがまさに探していたアイテム!」というケースがありました。これについては、本記事の後半で紹介しているテキストクエリの結果を見て頂くと分かります。
つまりVertex AI マルチモーダル検索の利用シーンでは「結果リストのルーズさ」を許容できる必要があります。
もしルーズさ許容できず、解消または緩和する必要がある場合、前述の「フィルタ」が活用できるかも検討のポイントになるでしょう。
もし画像ではなくテキストを活用したいなら
画像での検索では意味が薄いケースもあると思います。たとえば「書籍の推薦」などです。この場合は、表紙画像ではなく、ジャンルや要約といったテキストを元に類似アイテムを探すべきでしょう。
こういったケースではマルチモーダル エンベディングよりもテキスト エンベディングが有用です。どちらも全体的なパイプラインとアプリケーションコードはほぼ同じで、選択する model が異なる程度です。
マルチモーダル エンベディングを選ぶか、テキストエンベディングを選ぶかをまとめると、次のようになります。
ソース | たとえば | Embedding 方式 | 利用モデル |
---|---|---|---|
画像 | ただの画像 | マルチモーダル エンベディング | multimodalembedding |
画像 と テキスト | 画像とタイトル・説明等テキストのセット | マルチモーダル エンベディング | multimodalembedding |
テキスト、ドキュメントのみ | タイトル、説明文、文章、分類ラベル、メタテキスト... | テキスト Embedding | textembedding-gecko または textembedding-gecko-multilingual |
マルチモーダル検索の構成要素とフロー
実装へと進む前に、構成要素とフローを理解しておくと全容がつかみやすいです。とはいってもかなりシンプルで、整理すると次のたった三要素です。
-
Embeddings
- アイテムごとに Embedding 結果、1408 次元ベクトルを得ます
-
Index -> Deploy -> Index Point
- Embeddings をまとめた Index を作成することで、大量のデータに対する高速な検索(推論)が可能になる
- それを Deploy し Index endpoint を作成することで、Index へアクセス可能になる
- Index は更新が可能。更新はバッチまたはリアルタイムを選択可能
-
Vector Search
- 画像やテキストを一度 Embedding し、それを元に検索(推論)する
では、実装をはじめてゆきましょう。お手元に今回利用する画像をご用意ください。10 枚以下ぐらいで大丈夫です。え、ない?そんな時は 猫の画像データセット でも使いましょう。
環境構築
Python コードの実行環境を準備します。次のページを参考にしますが、Colab 利用などでも構わないでしょう。
mkdir multimodalembedding # お好みのフォルダ名をどうぞ
cd multimodalembedding
python3 -m venv .venv
. ./.venv/bin/activate
gsutil cp "gs://vertex-ai/generative-ai/vision/multimodalembedding/*" .
取得してきたリソース一式に含まれている requirements.txt
の google-cloud-aiplatform
ライブラリはバージョンが少し古く、この後のステップでコケてしまいます。今日時点の最新を調べてみると 1.38.1 だったので、修正します。
- google-cloud-aiplatform==1.25.0
+ google-cloud-aiplatform==1.38.1
ではライブラリをインストール。
pip3 install -r requirements.txt
環境構築は以上です。
Embeddings
Embeddings も先ほどと同じページを元に進めます。
私は最初、上記ページの cURL サンプルで試行したため、検索対象にしたい画像群を gcs にアップロードし、cURL で Embedding 結果を取得していました。後で気づいたのですが、環境構築時に gcs から取得したリソース一式に含まれている predict_request_gapic.py
を利用すると、ローカルの画像を指定できてラクでした。
次のように実行します。 {YOUR_PROJECT}
の箇所はご自身の GCP プロジェクト ID に置き換え、sample1.jpg
の値はご自身で用意した画像ファイルに置き換えてください。
python3 predict_request_gapic.py \
--project "{YOUR_PROJECT}" \
--image_file "sample1.jpg"
Embedding 結果は標準出力に表示されます。ひとまず数個のファイルで試すなら、predict_request_gapic.py
をファイルの数だけ実行し、結果を保存しておけばよいでしょう。たくさんの画像ファイルで試したい場合は、効率のよく取得できるスクリプトを別途用意しましょう。
Index
Index フォーマットへの変換
Index 作成は次のページを元に進めます。
いくつかの import 形式を選べますが、今回は JSONL 形式を選択します。先ほど取得した Embedding 結果を次のフォーマットに変える必要があります。
{id: "2311016138", embedding: [0.1, 0.2, 0.3, ...]}
{id: "2311016147", embedding: [0.1, 0.2, 0.3, ...]}
{id: "2311016296", embedding: [0.1, 0.2, 0.3, ...]}
...
id
には、アイテムを一意に特定する ID を設定します。embedding
には、Embedding 結果を設定します。
前述の Embeddings 取得方式には二種、cURL で取得する方式と predict_request_gapic.py
を用いる方式がありましたが、それぞれ結果フォーマットが異なります。採用した方式の結果フォーマットに合わせて、適当な変換スクリプトを作成しましょう。今回私は唯一このときだけ、プログラムを自作しました。20 行程度の小さな変換コードです。
JSONL ファイルは、GCS にフォルダを作成してアップロードしておきます。
Index の作成
順当に GCP ドキュメントを読み進めると次のページですが...
このページで紹介されているコードベースのガイドは、GCP コンソールに UI で用意されていました。コード書くの面倒なので せっかくなので UI を試してゆきます。
CGP Console > Vertex AI > ベクトル検索 へ進み、+新しいインデックスを作成 をクリック。
設定メニューが開きます。インデックスの作成に必要な情報を入力してゆきます。
- コンテンツ URI には作成した JSONL ファイルを配置した GCS フォルダのパスを指定
-
アルゴリズム は
Tree-AH
かブルートフォース
を選択できます。私はお勧めされている、初期値のTree-AH
を選択しました -
ディメンジョン は Embedding 結果の次元数。生成時に合わせて
1408
を指定 -
近似近傍数 はいったん
100
としましたが、結果リストの精度次第で調整します -
更新方法 は
リアルタイム
を試してみたいと思います -
シャドーサイズ は
小
を選択 - 詳細オプション
- すべてデフォルトのままとしましたが、距離測定のタイプは
ドット積距離
以外にユークリッド(L_2)距離
マンハッタン(L_1)距離
コンサイン距離(非推奨)
が選択可能です。
- すべてデフォルトのままとしましたが、距離測定のタイプは
作成 を押して 30 分ぐらい待つと、Index が作成されました。
Index のデプロイ
Index が作成された後は、利用できるようにするためデプロイします。
作成済みで未デプロイの Index は、Index 一覧の行に「デプロイ」という文字リンクが出現するため、そこからデプロイできます。
あわせてインデックス エンドポイントを作成できます。操作フローがスムーズ。
インデックスエンドポイントの作成は数秒です。アクセス は 標準
としました。
インデックス作成に戻り、インスタンスタイプを選択。今回はお試しのため最小の e2-standard-2
を選択しました。
インデックス作成項目が埋まりました。なお ID 値は自動生成です。
デプロイ ボタンを押して 30 分ほど待つと、デプロイが完了し、インデックスへアクセスできるようになりました。
Vector Search を実行してみる
お膳立ては整いました。さぁ見せてもらおうか、Vertex AI のマルチモーダル検索の性能とやらを!
なんとクエリ用コードが GCP コンソールの UI 上に用意されています。必要なパラメータも埋め込み済みです。親切が過ぎる。
画像とテキスト、それぞれをクエリしてみます。
クエリデータにも 1408 次元の Embedding を用いる必要があります。先ほどの Embedding 取得と同じ手順で、cURL または predict_request_gapic.py
を用いて Embedding 結果を得ます。
画像でクエリし、類似画像をリストしてみる
predict_request_gapic.py
を用いる例です。クエリ画像から Embedding 結果を得ます。
python3 predict_request_gapic.py \
--project "{YOUR_PROJECT}" \
--image_file "predict-test.jpg"
Embedding 結果は GCP コンソールの UI 上から取得した Python コードの feature_vectore
に配列で設定します。今回はお試しなのでハードコードしてしまいます。
datapoint = aiplatform_v1.IndexDatapoint(
- feature_vector="<FEATURE_VECTOR>"
+ feature_vector=[0.1, 0.2, 0.3, ...]
)
コードを実行してみると、次の結果が得られました。
nearest_neighbors {
neighbors {
datapoint {
datapoint_id: "2311016138"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.95913964509963989
}
neighbors {
datapoint {
datapoint_id: "2311016147"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.899713397026062
}
neighbors {
datapoint {
datapoint_id: "2311016296"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.700594425201416
}
neighbors {
datapoint {
datapoint_id: "2311016160"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.68775737285614014
}
neighbors {
datapoint {
datapoint_id: "2311016230"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.65240639448165894
}
}
検査してゆくと、上位の二件はクエリした画像と同一アイテムの別カットでした!三件目以降とは distance
値にかなり差がありますね。
テキストでクエリし、マッチしていそうな画像をリストしてみる
predict_request_gapic.py
を用いて、クエリテキストを Embedding 結果に変えます。テキストを対象にする場合は predict_request_gapic.py
に --text
オプション引数を指定して実行します。
python3 predict_request_gapic.py \
--project "{YOUR_PROJECT}" \
--text "A person with clothes a black shirt"
こちらも同様に、Embedding 結果を feature_vectore
に配列で設定した後、コードを実行します。結果は次のとおり。
nearest_neighbors {
neighbors {
datapoint {
datapoint_id: "2311016160"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.1305399090051651
}
neighbors {
datapoint {
datapoint_id: "2311016138"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.058376774191856384
}
neighbors {
datapoint {
datapoint_id: "2311016147"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.048217065632343292
}
neighbors {
datapoint {
datapoint_id: "2311016230"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.04690319299697876
}
neighbors {
datapoint {
datapoint_id: "2311016296"
crowding_tag {
crowding_attribute: "0"
}
}
distance: 0.034360706806182861
}
}
検査してゆくと、最初の一件目が黒い T シャツをきた人物の画像でした。素晴らしい!👏
面白くてこの後もいろいろ試したのですが、どうやっても高い推薦精度に驚きました。
Q & A
Q. Index をリアルタイム(ストリーミング)で作成したけど、GCS フォルダに配置したら即座に Index 更新されるってこと?
いえ、されませんでした。そういったものではなかったですね。
GCP Console の UI 上に Index の upsert 機能 と delete 機能がありました。同等のコードが次の公式ドキュメントにあります。
実際には手動で upsert / delete することは考えづらいので、なんらかのイベントトリガーで index 更新を自動化しましょう。
最後に
かなり手軽に、且つちょっとのコードで、新世代の検索であるマルチモーダル検索を試すことができました。
利用シーンやドメインの専門性次第での判断になりますが、少なくとも私は汎用的なアイテム推薦においては、Vertex AI の マルチモーダル検索と Vector Search を積極的に選択したいと感じました。今回の記事では詳しく触れませんでしたが、コストもかなり安価です。
Gemini の GA 版が届く日も待ち遠しいですね。
宣伝
アーキテクト、テックリード、技術顧問のお仕事をお受けしています。まずはお気軽にご相談ください 👍
X(Twitter)や当社 HP のお問い合わせフォームからご連絡ください 📬
得意領域
- AI 領域を含む、GCP を活用したインターネットサービスのアーキテクチャ設計や実装
- Vue 3 / Nuxt 3 での Web アプリケーション開発
- 既存レガシーシステムのモダナイズ
- チームビルディング、採用を含む技術顧問
Discussion