【BigQuery】画像から類似画像を検索！マルチモーダルエンベディングの簡単解説

# はじめに
Google CloudのBigQueryでマルチモーダルエンベディングが導入されています。
例としてテキストから画像検索が挙がっていましたが、画像から画像検索がなかったのでPythonを駆使して試してみました。

https://cloud.google.com/blog/ja/products/data-analytics/bigquery-multimodal-embeddings-generation

# 前準備
検索するためのデータベースをBigQuery上に事前に作成しておきます。

## 画像の用意
検索したい画像をCloud Storageにアップロードします。
今回はImagen3に作ってもらった10枚の画像を使用してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/c823c168-bdcd-4ee9-a671-5272549e8010.png)


## BigQueryの外部接続の作成
「外部データソースへの接続」から「Vertex AI リモートモデル、リモート関数、BigLake（Cloud リソース）」を選び、接続IDを入力します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/3a4e48ba-b64f-4d30-a073-8a9a9332fcb1.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/5146e384-9449-43eb-840b-374997e4f93e.png)


## オブジェクトテーブルの作成
BigQueryのオブジェクトテーブルを使用することで、非構造データを扱うことができるようになります。
以下のクエリを実行します。
```sql
CREATE OR REPLACE EXTERNAL TABLE
 `DATASET.object-table`
WITH CONNECTION `LOCATION.CONNNECTION_ID`
OPTIONS
( object_metadata = 'SIMPLE',
   uris = ['gs://BUCKET_NAME/*']
);
```
実行が完了すると、検索したい画像のオブジェクトテーブルが作成されます。

## 接続のサービスアカウントに権限付与
作成された外部接続の詳細を開くと、サービスアカウントIDが記載されています。
このサービスアカウントIDに対して、以下のロールを付与します。
- Vertex AI ユーザー
- Storage オブジェクト閲覧者

## エンベディングモデルの追加
`multimodalembedding@001`エンドポイントを使用する BigQuery モデルを作成します。
以下のクエリを実行します。
```sql
CREATE OR REPLACE MODEL
 DATASET.multimodal_embedding_model REMOTE
WITH CONNECTION `LOCATION.CONNNECTION_ID`
OPTIONS (endpoint = 'multimodalembedding@001')
```

## エンベディングの生成
ML.GENERATE_EMBEDDING 関数を使用することで、エンベディングを生成できます。
以下のクエリを実行します。
```sql
CREATE OR REPLACE TABLE `DATASET.object-table_embeddings`
AS
SELECT * FROM ML.GENERATE_EMBEDDING(
  MODEL `DATASET.multimodal_embedding_model`,
  TABLE `DATASET.object-table`)
WHERE content_type = 'image/jpeg'
```

実行が完了すると、`DATASET.object-table_embeddings`テーブルができています。
プレビューで中身を見てみると、画像のエンベディングされた結果が格納されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/3dbcdf5b-6326-4a65-82ba-e238b69c78f0.png)

# 画像で画像を検索する
## 検索したい画像
以下の画像に似た画像を検索してみたいと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/0f8e0f37-3e35-4bf1-9fda-9125abbd6067.png" width=40%>

## 検索
Pythonを使用します。
まず画像をマルチモーダルエンベディングでエンベディングに変換します。

```python
import vertexai
from vertexai.vision_models import Image, MultiModalEmbeddingModel

vertexai.init(project=PROJECT_ID, location=LOCATION)

model = MultiModalEmbeddingModel.from_pretrained("multimodalembedding@001")
image = Image.load_from_file("/path/image.png")

embeddings = model.get_embeddings(
    image=image,
    dimension=1408,
)
```

その後、BigQueryの`VECTOR_SEARCH`関数を使用したクエリをPython上から実行します。
BigQuery上で画像を読み込むには、オブジェクトテーブルをまた作成する必要があります。
今回はSQLクエリ上にエンベディング結果を埋め込むことで、別途オブジェクトテーブルを用意しないようにしました。

```python
from google.cloud import bigquery

client = bigquery.Client()
client = bigquery.Client(project=PROJECT_ID)

query = f'''
CREATE OR REPLACE TABLE `DATASET.vector_search_results` AS
WITH search AS(
  SELECT {embeddings.image_embedding} as ml_generate_embedding_result
)
SELECT base.uri AS gcs_uri, distance
FROM
  VECTOR_SEARCH(
    TABLE `DATASET.object-table_embeddings`,
    'ml_generate_embedding_result',
    TABLE search,
    'ml_generate_embedding_result',
    top_k => 3);
'''

result = client.query(query)
```

この実行が終わると、BigQuery上に検索結果のテーブルが作成されます。
そのテーブルに対して、検索結果を表示するコードを実行します。
```python
import io
from PIL import Image
import matplotlib.pyplot as plt
import tensorflow as tf

def printImages(results):
    image_results_list = list(results)
    amt_of_images = len(image_results_list)

    fig, axes = plt.subplots(nrows=amt_of_images, ncols=2, figsize=(20, 20))
    fig.tight_layout()
    fig.subplots_adjust(hspace=0.5)
    for i in range(amt_of_images):
        gcs_uri = image_results_list[i][0]
        text = image_results_list[i][1]
        f = tf.io.gfile.GFile(gcs_uri, 'rb')
        stream = io.BytesIO(f.read())
        img = Image.open(stream)
        axes[i, 0].axis('off')
        axes[i, 0].imshow(img)
        axes[i, 1].axis('off')
        axes[i, 1].text(0, 0, text, fontsize=10)
    plt.show()

query = """
  SELECT * FROM `DATASET.vector_search_results`
  ORDER BY distance;
"""

printImages(client.query(query))
```

検索画像とそのベクトルの距離が表示されます。
ベクトルの距離が近いほど、入力画像と近いものを指しています。
一番上に色味と形状が似ている画像がきていて、靴という共通点で検索の上位に来ていることが分かります！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/5d2ffb66-f396-4922-be4a-06703c85e855.png)

# テキストから画像検索
次にテキストから画像検索を試してみます。
テキストとして「スーツに合うもの」で検索してみます。
```python
embeddings = model.get_embeddings(
    contextual_text="スーツに合うもの",
    dimension=1408,
)
```

`embeddings.text_embedding`を使用して、同様にBigQueryにクエリを実行します。
検索を実行すると、ネクタイの画像が1位に来ていて、テキストの内容に合った画像が検索できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3968617/f34a190c-3d65-418f-8b6a-99179a0ed878.png)


# おわりに
今回BigQueryのマルチモーダルエンベディングを使用して、画像から画像検索を試してみました。
検索用のオブジェクトテーブルを使わないで、SQLのWITH句とPythonを駆使して実現させました。
BigQueryでマルチモーダルのベクトル検索が簡単に行えるので、クロスモダリティの検索の幅が広がりそうです！