ラベル画像のエンベディング

Date:

Share post:

前回の投稿(ラベル画像の切り出し)でエンベディングの元ネタとして有効そうなラベル画像が生成できましたので、今回は実際にエンベディングしてみて、同じラベルに関する異なる画像、違うラベルに関する画像それぞれの距離感に関して確認してみたいと思います。

全体の流れや方法に関しては以前の投稿「画像検索」に準じて行います。

PostgreSQL関連の準備

Mac+PostgreSQLでベクトル検索」でPostgreSQLのインストールと動作確認は済んでいるので、ここにラベル画像ベクトルを管理するためのテーブルを用意していきます。

まずはテーブルを作成します。
DBは前述の投稿で生成した「testdb」をそのまま使用します。

testdb=# CREATE TABLE items (
    id bigserial PRIMARY KEY,
    name text,
    embedding vector(2048)
);
CREATE TABLE

また、PythonからPostgreSQLにアクセスするためにはパッケージ「psycopg2」が必要なので、これをインストールします。

# pip install psycopg2

エンベディング

では実際にエンベディングを行なっていきます。

エンベディングの実装に関しては、以前作成したResNet50(「重み」にはimagenetを指定)を利用する方法を今回も採用します。

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に格納していきますが、DB「testdb」のオーナには現時点でパスワードが設定されていないので、改めてパスワードを設定します(今回は実験用なのでパスワードも簡単なもので)。

postgres=# ALTER USER toshi WITH PASSWORD 'toshipw';
ALTER ROLE

エンベディングの実行およびPostgreSQLへの格納は以下のように行います。

import psycopg2
from embedding import embedding
import os

LABEL_IMAGES = "./label_images"

conn = psycopg2.connect(
    host="127.0.0.1",
    database="testdb",
    user="toshi",
    password="toshipw"
)
cur = conn.cursor()

images = os.listdir(LABEL_IMAGES)
for image in images:
    print(f"Processing {image}...")
    image_path = os.path.join(LABEL_IMAGES, image)

    vector = embedding(image_path)
    cur.execute("INSERT INTO items (name, embedding) VALUES (%s, %s)", (image, vector.tolist()))

conn.commit()
cur.close()
conn.close()

フォルダ「./label_images」には前回生成したラベル画像を格納しておきます。
それらを全て読み込みつつ、ファイル名とエンベディング結果(ベクトル)をテーブル「items」に格納していきます。

コサイン距離の確認

生成されたベクトルが、どの程度の精度を実現できているかを確認してみます。
同じラベルを写した画像のベクトルは「近い」、異なるラベルを写した画像のベクトルは「遠い」と分類され、両者の差が明確であればあるほど精度が高い(同じものを同じ、違うものを違うと的確に区別できる)と判断します。

まずは、生成したベクトル全てに関して総当たりで距離を算出します。
具体的な実装内容は以下の通り。

import psycopg2
from embedding import embedding
import os

conn = psycopg2.connect(
    host="127.0.0.1",
    database="testdb",
    user="toshi",
    password="toshipw"
)
cur = conn.cursor()

cur.execute("""
    SELECT
        name,
        embedding
    FROM items
    ORDER BY name
""")
rows = cur.fetchall()
for row in rows:
    print("----------")
    print(row[0])

    cur.execute("""
        SELECT
            name,
            embedding <=> %s::vector AS distance
        FROM items
        ORDER BY distance
    """, (row[1],))

    v_rows = cur.fetchall()
    for v_row in v_rows:
        print(v_row)

cur.close()
conn.close()

結果は以下の通り。

ラベル画像の切り出し」におけるセットnのm番目の画像を「nm」のように数字2つの組み合わせで示しています。
つまり、1つ目の数字が同じであれば、同じラベルに関する画像と言うことになります。

コサイン距離は0〜2の範囲の小数になりますが、スペース的に細かな数字を表記することが難しいため、小数第一位の数字のみ記述しました。
つまり、「0」は0以上0.1未満、「1」は0.1以上0.2未満の小数であることを意味します。

1112131415212223242531323334354142434445
110000445443333344544
12000555454433344444
1300444443333344444
140455443333344444
15555443333344544
2100003333432323
220003333433333
23103333433323
2403232422323
253333422323
31000133434
3210233434
331133333
34233333
3533333
410000
42000
4300
440
45

基本的には期待通りの結果になっています。
同じラベルの画像相互では距離が「0」(0.1未満)となることが大半で、限られたケースで「1」や「2」になっている程度です。
一方で、異なるラベルの画像との距離は「2」(0.2)以上であり、90%以上は「3」(0.3)以上です。

いくつかポイントになりそうな距離を具体的に見てみましょう。
なお、同じラベルに関する最大距離には35が絡みますが、35は背景の映り込み部分が大きく、見た目でも他の画像とは違って見えるので、これを除外した場合に関しても抽出してみました。

同じラベルに関する最小距離(42-43)0.024057833544022023
同じラベルに関する最大距離(34-35)0.2340515825328603
35を除く同じラベルに関する最大距離(32-33)0.12959717844480667
異なるラベルに関する最小距離(24-44)0.2601928593368079

上記状況を見る限り、閾値を0.25辺りにすれば、同じラベルか異なるラベルかの線引きはできます。
35のような特殊ケースを切り捨てるように割り切れば、0.15辺りを閾値にすることで、同じラベルであることの認定精度が高まるでしょう。

ただ、見た目の印象と距離感が合っていないんですよね。
前述の各ケースに関して、距離順に画像も交えて並べてみましょう。

4243距離
0.024057833544022023
3233距離
0.12959717844480667
3435距離
0.2340515825328603
2444距離
0.2601928593368079

42-43の距離が小さいのは納得の結果ですが、それと比較して32-33が5倍以上の距離になっています。
見た目の印象では、そこまでの違いはないように思いますが。

一方で、34-35に関しては、確かに35に余計な写り込みがあるものの、人間の認識としては同じラベルとして問題なく判断できるのに対し、24-44は明らかに別ものが写っていると認識できるにも関わらず、距離的には両者に大きな違いがありません。

素人考えですが、見た目が似ている画像でも細かな違いが距離として反映されてしまうのは、むしろ精度が良いと肯定的に捉えることができるように思います。
よって、問題は見た目が明らかに違う画像に対して距離が思ったほど大きくない点にあるかと思います。
個人的な印象では、34-35の距離が0.234程度であれば、24-44の方は少なくとも倍の0.5以上は欲しいところです。

この辺に関してGemini先生のご意見を伺ったところ、

ResNet50がImageNetで学習した際に重視した特徴(物体分類に有効な特徴)と、私たちが今回求めている「ラベルデザインの類似性」という特徴が、必ずしも一致しないことが主な原因と考えられます。

とのことで、24-44の比較に関しても、

見た目は大きく違いますが、どちらも「ワインラベル」という共通のカテゴリに属し、文字やシンプルな図形が含まれています。これらの共通の「ラベル」としての特徴を強く捉え、詳細なデザインの違いを人間が感じるほどには区別しない可能性があります。そのため、コサイン距離が思ったほど離れないのかもしれません。

と、おっしゃっています。

要は、現在のエンベディング方法(ResNet50+ImageNet)においては、対象が「ワインラベル」であることの判断の方がメインで、その細かな違いを区別するようには学習されていないってことなんでしょうね。

まとめ

悲観的なことを書きましたが、少なくとも「画像検索の精度確認(4)」でボトルの全体像を対象としていた頃と比較すれば、ずいぶん正確な識別ができるようになったと思います。
途中でも書きましたが、今回使用した画像程度であれば、閾値を適切に設定することで同じラベルと違うラベル(つまりは同じ商品か違う商品か)を、かなり正確に識別できそうです。

ただ、今回使用した画像(ラベル)は比較的個性がはっきりしており、人間が直接識別するのであれば間違いようのないレベルの差異があります。
世の中には見た目が似ているラベルも多数あるため、今の精度では、それらを十分な正確性で識別していくことは難しいと思われます。

やはりラベルの識別に特化したエンベディング方法が必要な気がします。

Related articles

ラベル画像の切り出し

前回の投稿(Mac+PostgreS...

フロント担当のTIL 2025/06

「Today I Learned(今日学んだこと)」を記録するTIL、6月は新マシンが納品され、GPU仮想化からローカルLLM環境の構築まで、いろいろ試行錯誤しました。

Mac+PostgreSQLでベクトル検索

前回の投稿で予告したように、今回から...

ラベル画像のトリミング(6)

「ラベル画像のトリミング(5)」では...