【機械学習】撮り貯めた建築写真で類似画像検索をする【TensorFlow】

Pocket
LinkedIn にシェア
LINEで送る

このサイトにもAIの波が押し寄せてきました。色んな建物をおじゃましては写真を撮り続けて10年、ネット上にアップロード公開や販売をさせてもらってきていますが、せっかくだから類似画像検索とか、画像から設計者を予測するとか、イケてるアングルを探索して自動レンダリングするとか――機械学習を試したいって気持ちが日々強くなったわけです。
色々SNS等もやりましたが現在では自分のドメインに建築写真ギャラリーページを設け6,000枚の画像が閲覧可能となっています。それぞれの画像について類似検索してみることにします。

環境

Anaconda 1.19.1
Python 3.6.1
TensorFlow-gpu 1.15.0
CUDA Toolkit 10.0
cuDNN 7.6

やってみよう

まだ慣れていないこともあってちょいちょい実装間違いから単語の使い方の誤りまで色々問題があると思いますがそれらは指摘してもらえるとありがたいです。

作業フォルダは D:\Python\Annoy でやってる前提で話します。

画像の準備

まずはサムネイル画像(150x150px)6000枚を自分のサーバからローカルPCにダウンロードをします。

ちなみに、この内400枚をデータセットとして公開していますのでよければ使って下さい。

【DL】建築写真データセット400枚【画像】

画像データの処理

画像を数値化する必要があるということでおそらく一番有名所のVGG16モデルを使うことに。
まだですね、プログラム実装をやりながら勉強しているレベルなもんですから「VGG16の中身がどうなってるか確認~」とかかっこいいことしません。正直ベースで生きさせてもらっていますのでごめん。
本当は「どこの層に対して再学習させています~」とか把握していないといけないんでしょうけど、並行して勉強中。

コーディング

詰みそうになった場所とか苦労話とかは特に求められていないと思うので省略します。
ちょっとずつ説明していきます。

使用するライブラリ

Anacondaをインストールしている人だったら、追加でこの2つだけでOKのはずです。
  pip install pillow
  pip install annoy

練習:まずは画像1枚読み込んでみる

正直言うと今回の趣旨とは関係ないですが1枚画像を読んでみます。
base_dirとthumb_dirを設定しておきます。この2変数は最後までお世話になります。
Imageで画像表示ができます。

from IPython.display import Image
import os

base_dir = “D:/python/annoy/”
thumb_dir = os.path.join(base_dir, “thumb/”)
img_path = os.path.join(thumb_dir, “1595153765.jpg”)
Image(img_path)

出力結果

読めました。ハイ。

GPUが利用可能かを確認

Tensorflow-gpuをインストールしていますけど、だいたいのケースで上手く連携できていませんので、明示的にチェックをしておきます。以下のコードを打ちます。

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

出力結果

[name: “/device:CPU:0”
device_type: “CPU”
memory_limit: 268435456
locality {
}
incarnation: 14458053627491058360
, name: “/device:GPU:0”
device_type: “GPU”
memory_limit: 2210712780
locality {
bus_id: 1
links {
}
}
incarnation: 15322543064031778486
physical_device_desc: “device: 0, name: GeForce GTX 1060 3GB, pci bus id: 0000:01:00.0, compute capability: 6.1”
]

TensorFlow-gpuを使用するためにはCUDAとcuDNNという2つが必要になります。いろんなバージョンがありますがTensorflow公式サイトに対応表が載っています。私はこれ通りにやったらダメでしたけど。でもコンパイル時に英語で「あなたはバージョン○○を用意しているようだけど、バージョン△△を必要なんです。」とエラーを出してくれるから、言われたバージョン△△のcuDNNをダウンロードすれば通るようになりました。
  テスト済みのビルド構成 GPU – TensorFlow
  https://www.tensorflow.org/install/source_windows#gpu

GPUの利用制限の設定を変更

GPU利用可能だって表示はされていたんですが、実行中にCUBLAS_STATUS_ALLOC_FAILEDエラーが出る。
以下の設定をしたら私は解消できました。VRAMの利用制限を書けるおまじないだそうです。

from tensorflow.keras.backend import set_session
config = tf.ConfigProto()
config.gpu_options.allow_growth = True
config.log_device_placement = True
config.gpu_options.per_process_gpu_memory_fraction = 0.8 # 80% use
sess = tf.Session(config=config)
set_session(sess)

出力結果

Device mapping:
/job:localhost/replica:0/task:0/device:GPU:0 -> device: 0, name: GeForce GTX 1060 3GB, pci bus id: 0000:01:00.0, compute capability: 6.1

注意:kerasだとkeras.backend.tensorflow_backendですが、TensorFlowだとtensorflow.keras.backendです。

VGG16モデルを開く

最初にVGG16を読み込みます。コードはテンプレ。
summary()で今の中身を見ることができます。(勉強中)

from tensorflow.keras.applications.vgg16 import VGG16
#base_model = VGG16(weights=’imagenet’)
base_model = tf.keras.applications.vgg16.VGG16(weights=’imagenet’)
base_model.summary()

出力結果

Model: “vgg16”
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_2 (InputLayer) [(None, 224, 224, 3)] 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 224, 224, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 224, 224, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 112, 112, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 112, 112, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 112, 112, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 56, 56, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 56, 56, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 56, 56, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 28, 28, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 14, 14, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 7, 7, 512) 0
_________________________________________________________________
flatten (Flatten) (None, 25088) 0
_________________________________________________________________
fc1 (Dense) (None, 4096) 102764544
_________________________________________________________________
fc2 (Dense) (None, 4096) 16781312
_________________________________________________________________
predictions (Dense) (None, 1000) 4097000
=================================================================
Total params: 138,357,544
Trainable params: 138,357,544
Non-trainable params: 0

さらにbase_modelの中身をちょっと見てみます。(勉強中)

print(base_model.input)
print(base_model.output)

出力結果

Tensor(“input_1:0”, shape=(?, 224, 224, 3), dtype=float32)
Tensor(“predictions/Softmax:0”, shape=(?, 1000), dtype=float32)

これは「この機械学習モデルは幅高さ224pxで、RGBの3チャンネルの画像を受け取って長さ1000の配列を返す関数」だってことを示しているらしい。(勉強中)

画像を数値に変換

とりあえず1枚の画像を読み込んで、この入力サンプルに対する予測値の出力を生成します。

from tensorflow.keras.preprocessing import image
import numpy as np
img_path = “D:/python/annoy/thumb/1551256482.jpg”
img = image.load_img(img_path, target_size=(224, 224))
input = image.img_to_array(img)
result = base_model.predict(np.array([input]))

print(“array”, result)
print(“length: “, len(result[0]))

出力結果

array [[5.24274128e-07 3.58941907e-06 1.58142115e-07 1.45409444e-07
2.98365677e-07 2.05845510e-07 1.36824568e-07 2.98391706e-06
・・・略・・・
9.89705882e-07 1.09583432e-07 2.21363749e-07 8.78324840e-07
2.77168766e-07 7.01869212e-08 2.90152839e-05 8.11956052e-05]]
length: 1000

これは問題なく読めていることを示しているそうです。(勉強中)

中間層の抽出

ところでVGG16は先程のprint(base_model.output)で出ていたように、1000種類に分類するために作られたモデルだそうですから、欲しいのはこのモデルの中間層の特徴量ということになるらしいです。
fc2層を取り出します。

from tensorflow.keras import Model, layers
model = Model(inputs=base_model.input, outputs=base_model.get_layer(“fc2”).output)
print(model.input)
print(model.output)

出力結果

Tensor(“input_1:0”, shape=(?, 224, 224, 3), dtype=float32)
Tensor(“fc2/Relu:0”, shape=(?, 4096), dtype=float32)

summary()で出力したときに見た、長さ4096の配列だということが確認できます。

このモデルmodelで画像を1枚入力してみる

img = image.load_img(img_path, target_size=(224, 224))
input = image.img_to_array(img)
result = model.predict(np.array([input]))
print(“array”, result)
print(“len: “, len(result[0]))

出力結果

Tensor(“input_2:0”, shape=(?, 224, 224, 3), dtype=float32)
Tensor(“fc2_1/Relu:0”, shape=(?, 4096), dtype=float32)
実際の値 [[4.3103333 0.9731809 0. 0. 2.5254257 0.5112577]]
配列の長さ: 4096

うまく処理できているようです。
この読み込んだ変数modelは最後までお世話になります。

近似最近傍探索ライブラリAnnoy

このAnnoyというライブラリはこのベクトルの類似度計算量を少なくするために利用するそうです。厳密ではない代わりに計算量が少ないとのこと。(勉強中)

まずはモデルをロードします。

from annoy import AnnoyIndex
dim = 4096
annoy_model = AnnoyIndex(dim)

フォルダ内の全画像のベクトルを登録

全画像をモデルに登録します。add_item()関数です。
画像の取得は、globを使うと特定のフォルダのファイルパスの一覧が取得できます。
for文の中で、ファイルパスを引数にload_img()関数で画像を読み込み、img_to_array()で配列にして、predict(x)にセットします。

import glob

numimg=0
glob_dir = os.path.join(thumb_dir, “*.jpg”)
files = glob.glob(glob_dir)
for file in files:
print(file)
img_path = file
img = image.load_img(img_path, target_size=(224, 224))
x = image.img_to_array(img)
x = np.expand_dims(x, axis=0)
x = preprocess_input(x)
fc2_features = model.predict(x)
annoy_model.add_item(numimg, fc2_features[0])
numimg += 1

print(‘num files=’ + str(numimg))

出力結果

D:/python/annoy/thumb/2130982323.jpg
D:/python/annoy/thumb/3459852133.jpg
・・・略・・・
num files=5756
[0, 4, 3523, 4335, 3521, 2770, ・・・ , 5049, 2870]

6000枚もなかった。ちょっとサバ読んじゃってました。

ビルドして、結果をann形式で保存する

最後にモデルをビルドします。基本的にはこれで準備完了ですが、ビルド結果を保存することが出来ます。.ANN形式だそうです。
これで次回以降はビルド結果を読み込めば再開できます。

annoy_model.build(numimg)

save_path = os.path.join(base_dir, “result.ann”)
annoy_model.save(save_path)

# 保存結果を読み込む場合はこれ
#annoy_model.unload()
#trained_model.load(save_path)

実際こんな感じで出力されました。ファイルサイズが500MBとなかなかビックです。

準備完了。そして検証へ

さてこれで検索準備ができましたので、早速試してみましょう! 記事だとあっさり書いてますが、かなり躓きまくってます、特にTensorflow環境構築でハマってかなりしんどかったです。1回できちゃうと「あの苦労は何だったんだ」って感じですけどね。

では確認してみます。get_nns_by_item()関数の第一引数に指定します。
第二引数はマッチング上位から何個のインデックスを得るかを指定します。

annoy_model.unload()

trained_model = AnnoyIndex(4096)
trained_model.load(‘D:/python/annoy/result.ann’)
# インデックス0付近の10000個のデータを返す。全データがこの値より小さいときは実データ数になるっぽい
print(trained_model.get_nns_by_item(0, 10000))
# 第一引数に検索したいインデックス番号を指定。第二引数は得る個数。今回は7個getしよう。
items = trained_model.get_nns_by_item(1, 7, search_k=-1, include_distances=False)
print(items)

出力結果

[1, 4257, 1117, 2861, 708, 1303, 3982]

まあ、インデックス1の画像に似ているランキング1位は、なんとインデックス1です!
ってのは当然ですので・・・次点のインデックス4257以降の画像を見てみましょうか。

インデックス1

インデックス4257, 1117, 2861
  
インデックス 708, 1303, 3982
  

おおー。なんか期待したより良い感じに結果が出ている気がする。
もう2つくらい見てみましょうか。

items = trained_model.get_nns_by_item(2486, 7, search_k=-1, include_distances=False)
print(items)

出力結果

[2486, 2969, 795, 4053, 5484, 4567, 3227]

インデックス2486

インデックス2969, 795, 4053
   
インデックス5484, 4567, 3227
   

ビルファサード編

インデックス1310

インデックス3931, 5301, 4053
   
インデックス3871, 684, 4559
   

ホテル編

インデックス5491
 

インデックス5498, 5512, 5487
   
インデックス5441, 622, 5484
   

いいじゃないですか! なんかすっごい楽しくなってきた。
ちなみにですね、副産物的なもんだと思っていますが、建築グラビアのギャラリーには建築じゃない写真も含まれているのですが(理由はFlickrで公開していた全写真を単純にこのドメインに引っ越ししたから。)、それらの写真もちゃんと類似画像検索できている点です。

木の通り編

インデックス4764
 

インデックス3333, 2506, 3651
   
インデックス2490, 2872, 4904
   

食べ物編

インデックス1609
 

インデックス3669, 2265, 2267
   
インデックス529, 2046, 4047
   

成果物

実は既にデータ連携を全自動化しましたので、建築写真ギャラリーで稼働させています。ギャラリーページをよければ覗いてみて下さい。「似た画像の建築物」という箇所が増えています。
  ギャラリー – 建築グラビア
  https://christinayan01.jp/architecture/gallery/

ファイルの公開

今回使用したPythonファイルや画像データセットは下記場所に公開してあります。
  ImageSimilar by christinayan01 – GitHub

次回の章で自動化した方法を紹介しようか思います。

参考サイト

機械学習モデルという『関数』を使ったはじめての類似画像検索 – Qiita
【Python】KerasでVGG16を使って画像認識をしてみよう! – みんな栄養に頼りすぎてる
[Windows] failed to create cublas handle: CUBLAS_STATUS_ALLOC_FAILED – Github
TensorFlow-GPUのインストールで詰まったときのメモ – 盆暗の学習記録
cuDNN Archive – NVIDIA Developer
テスト済みのビルド構成 GPU – TensorFlow
【annoy】高维空间求近似最近邻 – 博客园

1 thought on “【機械学習】撮り貯めた建築写真で類似画像検索をする【TensorFlow】

コメントを残す