Open18

Vertex AI Matching Engineのチュートリアルを試す

kurehajimekurehajime

プロジェクトの設定

プロジェクト名は自分で決めたやつ。

gcloud config set project \
    "ai-tutorial-2023"
kurehajimekurehajime

サンプル画像データのダウンロードと確認

まずは花の画像を用意する。

いきなり知らないツール出てきた。
gsutilはGoogle Cloud Storageを便利に操作するツールらしい。
gs://という謎のプロトコルのURLが扱えるのはそういうことか。
このURLって世界で一意なのだろうか。それとも自分の中で一意で、cloud-samples-dataは最初から入ってるってことなのだろうか。

gsutil -m cp \
    "gs://cloud-samples-data/ai-platform/flowers/daisy/*.jpg" \
    ~/data/flowers/daisy/
kurehajimekurehajime

Terraform の実行

Terraform初めて使う。「なんかitamaeみたいなやつでしょ」くらいの認識。

$ TF_VAR_project_id="ai-tutorial-2023" \
    terraform plan
$ TF_VAR_project_id="ai-tutorial-2023" \
    terraform apply -auto-approve

terraform planでこれから実行されることを確認して、terraform applyで適用する感じか。
引数でなく環境変数でプロジェクトIDを指定するのはなんでだろう。

kurehajimekurehajime

エンベディングの作成

エンべディングとは、画像やテキストの特徴をパラメータ化する処理であると自分は理解している。
そのパラメータの距離が近いと似た特徴を持っていて、遠いとあまり似ていないことを意味する。
なのでこれから花の画像の特徴量を抽出する。

エンべディングをする処理はpythonで書かれていた。

        self._model = tf.keras.applications.EfficientNetB0(
            include_top=False, pooling="avg"
        )

EfficientNetB0というモデルを使ってるらしい。

軽くググったらこんな説明が出てきた。

EfficientNet-b0 は、ImageNet データベースの 100 万枚を超えるイメージで学習を行った畳み込みニューラル ネットワークです。このネットワークは、イメージを 1000 個のオブジェクト カテゴリ (キーボード、マウス、鉛筆、多くの動物など) に分類できます。結果として、このネットワークは広範囲のイメージに対する豊富な特徴表現を学習しています。ネットワークのイメージ入力サイズは 224 x 224 です。MATLAB® の他の事前学習済みのネットワークについては、事前学習済みの深層ニューラル ネットワークを参照してください。

ImageNetもWikipediaで調べてみる。

ImageNetは、物体認識ソフトウェアの研究で用いるために設計された大規模な画像データベースである。ImageNetでは、1400万を超える画像に手作業でアノテーションを行い、画像にどのような物体が写っているかを示している。また、100万枚以上の画像にバウンディングボックスも付与されている。ImageNetには、20,000を超えるカテゴリがあり、その中には「気球(balloon)」や「イチゴ(strawberry)」といった数百枚の画像で構成される一般的な物体カテゴリも含まれる。

いま手もとにある花の画像だけを使ってエンべディングするのではなく、あらかじめ多様な画像で学習済みのモデルを使ってエンべディングするということだろうか。

kurehajimekurehajime

お手本通りやったのにエラーになった

$ cd vectorizer; gcloud builds submit --tag "us-central1-docker.pkg.dev/ai-tutorial-2023/vectorizer/vectorizer:v1"
ERROR: (gcloud.builds.submit) The required property [project] is not currently set.
It can be set on a per-command basis by re-running your command with the [--project] flag.

You may set it for your current workspace by running:

  $ gcloud config set project VALUE

or it can be set temporarily by the environment variable [CLOUDSDK_CORE_PROJECT]

project がセットされてないという。何もしてないのに壊れた。

そういえばgcloud config set project ~というコマンドなんかやったなぁと思い、一番最初に実行した

gcloud config set project \
    "ai-tutorial-2023"

をもう一度実行したら先に進むようになった。

kurehajimekurehajime

Cloud Run jobを実行する。

gcloud beta run jobs create \
    vectorizer --image \
    "us-central1-docker.pkg.dev/ai-tutorial-2023/vectorizer/vectorizer:v1" \
    --cpu 4 --memory 2Gi \
    --parallelism 5 --region \
    us-central1 --service-account \
    "vectorizer@ai-tutorial-2023.iam.gserviceaccount.com" \
    --tasks 5 \
    --set-env-vars="DESTINATION_ROOT=gs://ai-tutorial-2023-flowers/embeddings" \
    --execute-now

5個のインスタンスが並列して動いてるっぽい。
どうやって作業分担しているのか。お互いの作業がかぶらないのか。

ソースを見ると、main関数がtask_indexという引数を受け取ってる。
たぶんこれインスタンスごとに番号振られているのだろう。花の種類も5種類だから、それで棲み分けできている。

def main(destination_root: str, task_index: int) -> None:
    flower = SampleDataVectorizer.FLOWERS[task_index]
kurehajimekurehajime

ちょっと気になるのでベクトルデータのアップロードも見てみたい。

    def __init__(self, flower: str, destination: str):
        self._flower = flower
        self._client = storage.Client()

        self._blobs = self._client.list_blobs(
            self.BUCKET, prefix=f"{self.PREFIX}{flower}/"
        )

storage.Client()からlist_blobsを取っている。
そしてblobsをループしながら、vectorizeしてidとembeddingを持ったJSONファイルを保存している。

   def vectorize_and_upload(self) -> None:
        data = []

        for blob in self._blobs:
            name = blob.name.split("/")[-1]

            logger.info("downloading %s", name)
            raw = self._download_as_tensor(blob)

            logger.info("vectorizing %s", name)
            embedding = self._vectorize(raw)

            data.append(
                {
                    "id": f"{self._flower}/{name}",
                    "embedding": embedding,
                }
            )

        blob = self._dst_bucket.blob(f"{self._dst_base}/{self._flower}.json")
        with blob.open(mode="w") as f:
            for datapoint in data:
                f.write(json.dumps(datapoint) + "\n")
kurehajimekurehajime

インデックスの作成とデプロイ

🕛 60 分
インデックスの作成には数十分から1時間ほどかかります。コンソールでステータスが確認できます。

なんと…。

待つしかない。Cloud Shell Editorが途中でタイムアウトしないか心配だ。

kurehajimekurehajime

インデックスの作成が終わった。
20:50分頃に開始して、21:34分に終わった。
ベクター数2,871個。
画像ではなくただのJSONを扱ってるのにこんなに時間がかかるのか…。

kurehajimekurehajime

インデックス エンドポイントの作成。

インデックス エンドポイントはインデックスによる近似再近傍探索を実行するための API を提供するためのリソースです。インデックス エンドポイントには設定した VPC 内からのみアクセスできます。

外部から気軽にAPIたたけないのか…。

kurehajimekurehajime

作成したインデックスをインデックス エンドポイントにデプロイ

デプロイには数分から数十分かかります。表示されたコマンドを実行するか、コンソールからステータスが確認できます。

コンソールで確認できると書いてるけど、ステータス欄には既に準備完了と書いてある。

一瞬で終わった…?

…と思ったけどEndpoint for flower searchをクリックしたら詳細画面に飛んで、そこには「デプロイ中」の表示がぐるぐる回っていた。紛らわしい。

kurehajimekurehajime

20分くらいかかって終わった。
インデックスの作成から1時間10分くらい。
たった3000たらずでこれはなぁ。

kurehajimekurehajime

クエリの実行と確認

インデックスの作成と同じEfficientNetB0で対象画像をベクトル化して、それをMatchingEngineIndexEndpointに投げて似たベクトルの画像を探す。

def main(index_endpoint_name: str, deployed_index_id: str, image_path: str) -> None:
    print("===== Started making vector =====")

    model = tf.keras.applications.EfficientNetB0(include_top=False, pooling="avg")

    raw = tf.io.read_file(image_path)
    image = tf.image.decode_jpeg(raw, channels=3)
    image = tf.image.resize(image, [224, 224])

    vector = model.predict(np.array([image.numpy()]))[0].tolist()

    # https://github.com/googleapis/python-aiplatform/blob/v1.22.0/google/cloud/aiplatform/matching_engine/matching_engine_index_endpoint.py#L85
    endpoint = MatchingEngineIndexEndpoint(index_endpoint_name=index_endpoint_name)

    print("===== Started query =====")
    # https://github.com/googleapis/python-aiplatform/blob/v1.22.0/google/cloud/aiplatform/matching_engine/matching_engine_index_endpoint.py#L902
    res = endpoint.match(
        deployed_index_id=deployed_index_id, queries=[vector], num_neighbors=5
    )
    print("===== Finished query =====")

    for neighbor in res[0]:
        print(f"{neighbor.id}: distance={neighbor.distance}")

これに似た画像を探すと

これが引っかかる。

似てなくはないが、そこまでドンピシャな感じはしない。

kurehajimekurehajime

インデックスに含まれない画像でのクエリ

さっきのはインデックスに含まれる画像で試したので完全とは言えなかった。
自分自身があるのに自分自身がトップに来ないのは、いくらベクトル検索がベストエフォートとはいえ不思議な感じもする。

インデックスに含まれない画像で試したらこれとこれが引っかかる。

kurehajimekurehajime

Updater のデプロイ

画像が追加されるたびに1時間半のインデックス再作成が生じるとなったらやってられない。
Updater という追加でベクトル化する方法があるらしい。

ストリーミング アップデート

こっちはあっさり終わった。

kurehajimekurehajime

後片付け

  • GCPにログインをして、削除したいプロジェクトを表示。
  • 上部にあるメニューボタンを選択。
  • メニューが表示されるので、「IAMと管理」>「設定」を選択。
  • 設定画面となるので、「シャットダウン」を選択。
kurehajimekurehajime

おしまい

チュートリアルが一通り終わった。
画像をアップロード、ベクトル化、JSONの登録、インデックスの作成、問い合わせ…などざっくりとした流れはわかった。

ただ環境構築周りはTerraformにおまかせだったので、その辺まだ不安がある。
JOBの作成やネットワーク作成などGCPの基本操作の知識にも求められる。
まずは慣れないとなぁ。