🦆

超小型なVLM、Moondreamを試す(応用編)

2025/01/27に公開

※今回の記事はあまり実用的ではない実験的な内容です。

MoondreamのVQA機能を使って検索チューニング実験

基礎編はこちら:
https://zenn.dev/naviplus/articles/10ff0823fdc272

課題設定

画像の類似検索を考えます。
右の画像の中から左の白いアヒルに似たものを探したいとします。

シンプルな実装を考えると、画像のベクトル化を行ってベクトル間の距離を利用して近いものを探し出すと良さそうです。

しかしもう少し踏み込んで考えてみると問題点もあります。
アヒルの類似画像を探そうとしている人の「類似」の基準はいくつも考えられて

  • 動物の種類を特定して、「アヒル」が映っているという基準で画像を探したい
  • アヒルに限らず、イラスト調のかわいらしい動物がみつけたい
  • 画像の全体的な雰囲気を基準に探したい

というように、検索で出てきてほしいものは状況や人によって変わります。
こういった類似の条件を簡単に調整できないか?というのが今回のテーマです。

実験内容

今回やってみたいのは、以下のようなアプローチです。
VQAの機能を使って、画像に対して重視したい場所に関する質問を投げかけ、回答を得ます。
その回答の類似度を見ることで狙った部分の類似性による検索ができるのではないか?というものです。

イメージを図示するとこんな感じ。

画像自体の類似度ではなく「動物名は?」のような質問の答えを使います。
そうすると、上側の例だと、「アヒルである」ということが重視され、赤文字のものが類似度が高くなりそうですし、下側の例だとまた別のCGっぽい画像の類似度を高くすることができそうです。

今回は、Moondreamで質問を投げかけ、出てきた答えをmultilingual-e5-smallでベクトル化して類似度を出してみます。

実験結果

Moondreamを使ってVQA(画像に対して質問)するには、前回の基礎編でキャプション生成を行ったのとほぼ同じ形で、

query = "What's in this image?"
encoded_image = model.encode_image(image)
model.query(encoded_image, query)["answer"]

とするだけで可能です。

テキストのベクトル化はサンプルそのままなのでここでは省略・・・。

まずは結果をまとめて:

キャプションを利用した類似度

結果の図の一番上は、Moondreamのキャプション生成の機能を利用してそれぞれの画像をテキスト化し、類似度を見ています。
例えばクエリ画像(左の画像)のキャプションはこのような感じになっています。

The image presents a 3D rendering of a gray and white duck standing on a gray surface with a cracked texture. The duck has a yellow beak and orange feet, and is facing towards the right side of the image. The duck's wings are a gray and white pattern, and its tail is gray and white. The background features a gray surface with a cracked texture, creating a 3D effect. The duck's position and the crack's texture suggest a sense of depth and perspective, with the duck positioned slightly off-center to the right.

画像は、ひび割れたテクスチャを持つ灰色の表面に立つ灰色と白のアヒルの3Dレンダリング。アヒルは黄色いくちばしとオレンジ色の足を持ち、画像の右側を向いている。アヒルの翼は灰色と白の模様で、尾は灰色と白です。背景のグレーの表面にはひび割れたテクスチャがあり、立体感を生み出しています。アヒルの位置とひび割れの質感が奥行きと遠近感を暗示し、アヒルの位置はやや右寄りにずれている。

キャプションは画像全体を説明したものになっており、画像全体を捉えた類似検索になっていると考えられます。
類似の画像と判定された画像を見てみると、なんとなくアヒルの画像や雰囲気の似ている画像が選ばれています。

VQAを経由した類似度

下3つは質問を投げかけた回答を使ったものです。
質問内容は画像内にある通り、

Answer simply the name and characteristics of the animal.

その動物の名前と特徴を簡単に答えなさい。

Typical colors are

代表的な色は?

What is the art style of this image?

この画像のアートスタイルは?

と3種類実験しまして、それぞれ「被写体」「色合い」「画風」を重視した検索がしたいというものです。

結果を見ると、被写体の動物重視ではアヒル/マガモの成鳥が重視され、色合い重視では灰色っぽいものが重視されてそうです。
VQAの回答だけだと、情報量はかなり落ちているので、うまく行ってなさそうな結果もありますが、テキスト化されているので、どういう文章になっているか確認できるので人間に理解しやすく、調整しやすいという利点はありそうです。

ベクトル検索実装のTips

今回、テキストをベクトル化して近似最近傍探索を行っています。
こういったベクトル検索をさくっと試したい場合、DuckDBを利用するととても楽だったので紹介します。

ベクトル検索用のExtensionがあり、これを利用するとすぐに利用できます。
https://duckdb.org/docs/extensions/vss.html
しかも、pandasとのデータのやり取りも非常に楽です。

まず、pandasのDataFrameで、以下のようにデータが入っているとします。
v_capやv_q1というのがベクトル化した列とします。

cap q1 ... v_cap v_q1 ...
The image... white duck ... [0.111...] [0.222...] ...

これらをDuckDBに読み込み、検索できるように設定する例です。

import duckdb

# vss extensionを有効化します
con = duckdb.connect()
con.install_extension("vss")
con.load_extension("vss")

vec_len = 384
# duck_mainというテーブルを作ります
con.sql("drop table IF EXISTS duck_main;")
# pandasのdf変数をそのまま読み込みます (こんな指定ができる。便利。)
con.sql("create table duck_main AS SELECT * FROM df;")

# ベクトルとして扱われるよう変換
for c in ['v_cap', 'v_q1']:
  con.sql(f"DROP INDEX IF EXISTS idx_{c};")
  con.sql(f"ALTER TABLE duck_main ALTER {c} TYPE FLOAT[{vec_len}]")

# 検索用にindexを設定
for c in ['v_cap', 'v_q1']:
  con.sql(f"CREATE INDEX idx_{c} ON duck_main USING HNSW ({c});")

これで完了。

続いて検索は

ret = con.sql(f"""
SELECT *, array_distance(v_{col}, {inp}::FLOAT[{vec_len}]) as dist
FROM duck_main
ORDER BY array_distance(v_{col}, {inp}::FLOAT[{vec_len}])
LIMIT 5;
""").df()

このような形で上位5件を検索し、DataFrame化して取得できます。
とても便利。

考察

類似検索の精度を上げようと思うと、本来は距離学習などを利用してモデルを学習したり、画像内の物体を検出しておいて利用したり、色々と手法はあるかと思います。

今回は精度というよりも自然言語による指定で手軽に調整ができるのではないか?という観点で実験してみました。
なんとなく検索結果が変わるというのは確かめられましたが、実用レベルにもっていくにはまだまだ工夫が必要そうです。実際は通常のベクトル検索と併用してRerankのような使い方をするのが良いのかもしれません。
Moondreamのような小型で扱いやすいモデルが出てくることでこういった実験もさっと試せるので面白いですね。

NaviPlusテックブログ

Discussion