昨今の投稿で触れてきたエンベディング及びベクトル検索を使用して、本来の目的である画像検索を実施してみます。
PostgreSQLの下準備
ベクトル検索に際してはPythonのプログラムからPostgreSQLにアクセスできるようにする必要がありますが、今までの工程ではPostgreSQLへのアクセスパスワードが設定されていないため、改めて設定しておきます。
postgres=# ALTER USER postgres WITH PASSWORD 'postgres';
また、先の投稿では登録されるベクトルの次元を暫定的に3次元としてあったので、ここもエンベディングの結果に合わせて2048次元に変更します。
加えて、当該画像のファイル名を合わせて登録するようにカラムを追加して、テーブルを作り直します。
testdb=# CREATE TABLE items (
id bigserial PRIMARY KEY,
name text,
embedding vector(2048)
);
CREATE TABLE
Pythonの下準備
Pythonに関してはPostgreSQLにアクセスするための機能「psycopg2」を追加する必要があります。
なお、パッケージとしては「psycopg2」が存在しますが、これはインストール後にビルドが必要になるようで、私の環境ではビルド時にエラーが発生してしまいました。
エラーの原因を追求しても良いのですが、手っ取り早くビルド済のものをインストールする方法もあるようなので、今回はこちらで対処します。
# pip install psycopg2-binary
また、先の投稿で作成したエンベディング関数ですが、使い回しがしやすいように「embedding.py」として独立させておきます。
import numpy as np
import tensorflow as tf
model = tf.keras.applications.ResNet50(include_top=False, pooling="avg", weights='imagenet')
def embedding(image_path):
image_bytes = tf.io.read_file(image_path)
decoded_image = tf.image.decode_jpeg(image_bytes, channels=3)
resized_image = tf.image.resize(decoded_image, [224, 224])
vector = model.predict(np.array([resized_image.numpy()]))[0]
return vector
画像データの登録処理
比較対象となる画像群をエンベディングして、PostgreSQLに登録する処理を以下のように実装しました。
画像群はディレクトリ「images」の配下にまとめて格納しておき、プログラム内でそれらの名称を全て取得しつつ、各名称を指定して前述のembedding関数を実行します。
結果は画像ファイル名と合わせてテーブル「items」に登録します。
import psycopg2
from embedding import embedding
import os
conn = psycopg2.connect(
host="127.0.0.1",
database="testdb",
user="postgres",
password="postgres"
)
cur = conn.cursor()
images = os.listdir("./images")
for image in images:
vector = embedding("./images/" + image)
cur.execute("INSERT INTO items (name, embedding) VALUES (%s, %s)", (image, vector.tolist()))
conn.commit()
cur.close()
conn.close()
embedding関数が返すvectorの型はNumPy配列(numpy.ndarray)であり、そのままではINSERTの入力値として使えないため、リスト型に変換しています。
画像検索処理
画像検索に関しては、指定した画像をエンベディングし、その結果(ベクトル)との近さを条件とする検索を行います。
ただ、今回はベクトル検索の傾向を色々と確認してみたいので、先に登録対象とした画像群のそれぞれの画像と他の画像の距離を全て抽出しつつ、距離の近い順で並べた結果を取得してみます。
具体的な内容は以下の通り。
import psycopg2
from embedding import embedding
import os
conn = psycopg2.connect(
host="127.0.0.1",
database="testdb",
user="postgres",
password="postgres"
)
cur = conn.cursor()
images = os.listdir("./images")
for image in images:
vector = embedding("./images/" + image)
cur.execute("""
SELECT
name,
embedding <=> %s::vector AS distance
FROM items
ORDER BY distance
""", (vector.tolist(),))
rows = cur.fetchall()
print("----------")
print(image)
for row in rows:
print(row)
cur.close()
conn.close()
ベクトル検索において重要な「距離」に関しては、今回は「コサイン距離」を採用してみました。
実験データ
今回使用する画像データは以下の11点です。
1.jpg | 2.jpg | 3.jpg | 4.jpg |
5.jpg | 6.jpg | 7.jpg | 8.jpg |
9.jpg | 10.jpg | 11.jpg |
「1.jpg」を基本形とし、他の画像を作成してみました。
「2.jpg」から「7.jpg」までは「1.jpg」と同じマウスを被写体とし、撮影方法を変えています。
- 「2.jpg」は「1.jpg」と比較して対象物が小さく写るように撮影
- 「3.jpg」は「2.jpg」と同定度のサイズですが、位置が左寄りにずらして撮影
- 「4.jpg」は「1.jpg」と同程度のサイズとして写るようにしつつ、向きを90度ずらして撮影
- 「5.jpg」は「1.jpg」と同程度のサイズ、同じ方向で、背景を白に変えて撮影
- 「6.jpg」は「1.jpg」の状態に対して、真上からではなく、やや手前から斜めに撮影
- 「7.jpg」は「4.jpg」の状態に対して、真上からではなく、やや手前から斜めに撮影
「8.jpg」は「1.jpg」とは色の異なるマウスを、「1.jpg」と同程度のサイズに写るよう撮影したものです。
「9.jpg」は「1.jpg」と色合いの酷似したマウスを、「1.jpg」と同程度のサイズに写るよう撮影したものです。
「10.jpg」「11.jpg」は上記までの画像との比較用として、全く異なる対象物を撮影したものです。
「10.jpg」に関しては配置や撮影の角度を「1.jpg」に近い状態にしています。
「11.jpg」に関しては、他の画像と比較して、ほとんど共通点がないようなものとして用意しました。
実験結果
先のデータに対する実験結果を以下に整理します。
全ての比較結果を記載すると冗長になるので、特徴的なもののみ抜粋します。
なお、自身との比較結果が最も上位にあり、かつ距離が0になることは自明なので、下記比較結果では除外してあります。
数値はコサイン距離を示しており、0〜2の範囲になります。
1.jpgとの比較
今回の主目的である「1.jpg」との比較です。
1 | 5.JPG | 0.11841988708423767 | |
2 | 2.JPG | 0.1407688128183303 | |
3 | 3.JPG | 0.20300479642237867 | |
4 | 9.JPG | 0.21921023962016217 | |
5 | 7.JPG | 0.22920902768692297 | |
6 | 6.JPG | 0.24923588062509827 | |
7 | 4.JPG | 0.33580066865584346 | |
8 | 8.JPG | 0.43069954975501756 | |
9 | 10.JPG | 0.5030188558405837 | |
10 | 11.JPG | 0.6265555390769518 |
1〜3位は納得の結果かと思います。
「7.jpg」「6.jpg」「4.jpg」(同じマウスの撮影角度を変えたもの)よりも「9.jpg」(別のマウスだが色と構図が似ている)が上位に来ているのが印象的です。
撮影されている対象物が何であるかという点に関してはあまり考慮されず、あくまで絵的な類似性が重視されているようです。
ただ、その場合は「2.jpg」「3.jpg」よりも「9.jpg」の方が似ていると判断されても良いようにも思いますが。
また、「7.jpg」「6.jpg」「4.jpg」を比較した場合、私の印象としては「6.jpg」「4.jpg」「7.jpg」もしくは「4.jpg」「6.jpg」「7.jpg」の順になりそうに思うのですが、なぜか「7.jpg」が「4.jpg」「6.jpg」よりも似ていると判断された点は謎です。
「8.jpg」に関しては色の違いが大きかったようです。構図は「7.jpg」「6.jpg」「4.jpg」よりも似ているのですが、この順位に甘んじています。
9,10位も納得の結果ですね。
2.jpgとの比較
「1.jpg」と比較して対象物が小さく写った画像が対象です。
1 | 3.JPG | 0.09840151369905992 | |
2 | 1.JPG | 0.1407688128183303 | |
3 | 5.JPG | 0.2180842174679397 | |
4 | 9.JPG | 0.22571964609296968 | |
5 | 7.JPG | 0.2740340911302601 | |
6 | 6.JPG | 0.277223437651009 | |
7 | 4.JPG | 0.34104425067126587 | |
8 | 8.JPG | 0.38560333128253665 | |
9 | 10.JPG | 0.5066518234441573 | |
10 | 11.JPG | 0.6036497635369562 |
個人的興味は「1.jpg」(対象物の見た目のサイズの違い)と「3.jpg」(対象物の位置の違い)のどちらが影響するかという点でした。
結果は「3.jpg」の勝利で、距離的にも「1.jpg」と「5.jpg」(背景色の違い)よりも近いと判断されています。この結果だけ見れば多少の位置の違いは影響を与えないと期待できそうです。
「1.jpg」と比較して「5.jpg」(さらに背景色が違う)がこの順位となるのは順当かと。
4位以降は「1.jpgとの比較」と同じ結果です。
8.jpgとの比較
複数のマウス画像の中で、マウスの色が異なっている点で特徴的な画像との比較です。
1 | 3.JPG | 0.3537774949060002 | |
2 | 2.JPG | 0.38560333128253665 | |
3 | 7.JPG | 0.41801104143869827 | |
4 | 1.JPG | 0.43069954975501756 | |
5 | 6.JPG | 0.4661933272359252 | |
6 | 5.JPG | 0.48786951947700863 | |
7 | 9.JPG | 0.4883972665533808 | |
8 | 10.JPG | 0.5239584780856646 | |
9 | 4.JPG | 0.5850812517863409 | |
10 | 11.JPG | 0.6915559256321544 |
順位がどうこう言う以前に、距離が最小でも0.3を超えています。
他の比較を見ても、「同じ物を写した」と言って良いボーダーは0.2辺りにありそうなので、0.3を超えていたら考慮しなくて良いのではないかと思っています。
なお、特徴的なのは8,9位で、対象物が同種のもの(マウス)であることよりも、構図的な類似性(同じ背景の真ん中に、上下いっぱいになる程度のサイズで何か縦長なものが写っている)ということの方が重視されたのでしょうかね?
どちらにしても距離が0.5を超えているので似てない判定で良いのですが。
9.jpgとの比較
「1.jpg」とは色合いや構図は酷似しているものの、別のマウスを写した画像との比較です。
本比較のポイントは、他の画像との距離がどの程度遠い(似ていない)と判断されるかです。
1 | 1.JPG | 0.21921023962016217 | |
2 | 2.JPG | 0.22571964609296968 | |
3 | 5.JPG | 0.26658100433218623 | |
4 | 3.JPG | 0.2877597163239666 | |
5 | 7.JPG | 0.30588486304108753 | |
6 | 6.JPG | 0.32612541913122584 | |
7 | 4.JPG | 0.3535695958455044 | |
8 | 8.JPG | 0.4883972665533808 | |
9 | 10.JPG | 0.5183403064345349 | |
10 | 11.JPG | 0.6216443223784578 |
最も似ている(近い)とされた画像は「1.jpg」で、距離は約0.22です。
ただ、この距離以下でなければ似ていないと判断すると、最初に示した「1.jpg」と他の画像の比較のように、同じ対象物を写した画像の多くが除外されてしまう可能性があります。
疑わしきは除外して確実に同じと言える場合のみを採用するか、多少怪しくても近いと判断されたものを採用するかが課題になりそうです。
11.jpgとの比較
ほとんどオマケのような比較ですが、見た目似ていないにも関わらず距離が近いと判断されるケースがないかどうかの確認です。
1 | 10.JPG | 0.5104636059923182 | |
2 | 2.JPG | 0.6036497635369562 | |
3 | 3.JPG | 0.6100208333336778 | |
4 | 9.JPG | 0.6216443223784578 | |
5 | 1.JPG | 0.6265555390769518 | |
6 | 6.JPG | 0.643298562452018 | |
7 | 7.JPG | 0.6552266245988223 | |
8 | 5.JPG | 0.6758433107380102 | |
9 | 4.JPG | 0.6807029975827883 | |
10 | 8.JPG | 0.6915559256321544 |
最も近いもので0.5を超えていますので論外と言うことで良いかと。
まとめ
一部の順位に疑問はあるものの、距離が0.2前半程度で近いと判断されたものは、人間の目で見ても似ている画像と言えるかと思います。
ただ、途中で何度が触れましたが、重要なのは絵的な類似性であり、色合いや構図などが似ていることが重要なようです。
言い換えれば、縦のものを横にしただけで類似度はかなり下がるので、ゴリゴリのAIのようにリンゴの写真に関して「被写体はリンゴである」というような認知能力を持った上での検索を行なっている訳ではないという点は認識しておくべきでしょう。
今回はエンベディング時の深層学習モデルとして「ResNet50」、ベクトル検索の距離判定に「コサイン距離」を採用しましたが、この辺を変更することで違った結果が得られるのかもしれません。
その辺は次回以降の課題と言うことで。