機械学習モデルという『関数』を使ったはじめての類似画像検索

# 画像類似検索とは？
画像で検索すると似ている画像が検索結果として返ってくるとかいうアレ。
Googleの画像検索とかがいい例ですね。

https://images.google.co.jp/

今回はそれを機械学習などの説明を極力省いて実装していきます。
ちなみに当方も機械学習については深く理解している訳ではないので、まさかりは大歓迎です。
というより是非飛ばしてください!

### 環境
Python 3.6 / GoogleCoraboratory GPU

## データ収集

今回はラーメンの画像を類似検索することにします。
最初に検索用の画像を一定数用意するところから始めます。1000枚もあれば十分ですかね。
適当にScrapingしてとってきました。Githubに上げておくので、宜しければお使いください。

https://github.com/K-jun1221/reverse-image-search

ダウンロードした画像はGoogle ColaboratoryからアクセスできるようにDriveに上げておきます。
その後ColaboratoryでDriveをマウントして簡単にアクセスできるようにしておきます。

```python

from google.colab import drive
drive.mount('./gdrive')

!ls "./gdrive/My Drive/"
```

実際に画像をロードして確認してみましょう。

```python

from IPython.display import Image

img_path = "./gdrive/My Drive/Google Colaboratory/ラーメン類似検索/ramen_images/ramen1.jpg"
Image(img_path)
```
![ramen1.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/c7886afb-c761-1662-c784-074db3747082.jpeg)
無事確認できました。ラーメンラーメンラーメン

## 画像の数値化

それでは、画像を数値に変換して他の画像との類似度を測れるようにしていきます。
画像の数値化にはVGG19という画像判定で有名なモデルを使用します。他の人の記事などをみているとVGG16を使っていたりするみたいですが、あんま変わらんやろってことでVGG19でやってみました 笑 

具体的な使用方法の前に、VGG19の中身がどうなっているかを覗いてみましょう。

```python

from keras.applications.vgg19 import VGG19

base_model = VGG19(weights="imagenet")
base_model.summary()
```

```
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels.h5
574717952/574710816 [==============================] - 6s 0us/step
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 224, 224, 3)       0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 224, 224, 64)      1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 224, 224, 64)      36928     
_________________________________________________________________
...
_________________________________________________________________
flatten (Flatten)            (None, 25088)             0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
_________________________________________________________________
```

ここであなたに魔法の言葉を伝授いたしましょう。
#### 『機械学習モデルは関数』

大事なことなのでもう一回。
#### 『機械学習モデルは関数』

このVGG１９というのは『引数』を与えると結果として『戻り値』が返ってくるただの関数です。先ほどのモデルの中身は全て忘れてください。これは関数これは関数と念仏のように繰り返しましょう。
それではVGG19という『関数』はどのような引数をとってどのような値を返すのか。これはモデルが教えてくれます。

```python

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)
```

この機械学習モデルは 『height: 224px, width: 224px, 3チャンネル』の画像を因数として受け取り『長さ1000の配列』を返す関数になります。

本当かどうか確かめるために実際に『関数』に画像を1枚『引数』として与えて、『戻り値』を確認してみましょう。

```python

img_path = "./gdrive/My Drive/Google Colaboratory/ラーメン類似検索/ramen_images/ramen1.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("配列の中身", result)
print("配列の長さ: ", len(result[0]))
```
```
配列の中身:
[[
  2.45506726e-06 4.99846719e-05 2.89384534e-05 6.99102111e-06
  4.65853464e-06 8.83888697e-06 1.61514363e-05 3.09021679e-08
  ...
  1.39145470e-06 1.06100217e-07 2.68440857e-07 3.49369566e-06
  8.51037203e-06 9.79270339e-07 1.54556838e-05 4.74406625e-05
]]
配列の長さ: 1000
```

期待した通りの結果が返ってきましたね。それではこの『関数』を使って画像を実際に数値化していきます。

### 中間層の抽出

さてさて、御察しの方もいるかもしれませんが、先ほどの『関数』、そのままでは使うことはできません。
VGG19はそもそも画像を1000種類に分類するために作られたモデル。ラーメンなんて掠りもいたしません。
欲しいのは、このモデルの中間層の特徴量です。

#### 言い換えましょう。
先ほどの『関数』はいくつか余計な『関数』が内部でメソッドチェーンされているため利用できません。なので、この余分な『関数』がチェーンされない関数を新しく作成し直します。

```python

model = Model(
  inputs=base_model.input,
  outputs=base_model.get_layer("fc2").output
)
```

この"fc2"という層が今回取り出したい中間層になります。Summaryでモデルの中身を確認した時にも存在がみて取れますね。

```
...
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              102764544 
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
predictions (Dense)          (None, 1000)              4097000   
=================================================================
```

新しく作った『関数』も『引数』と『戻り値』を確認してみましょう。

```python

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の配列を返すようになっていますね。

この関数も1枚試してみましょう。

```python
img_path = "./gdrive/My Drive/Google Colaboratory/ラーメン類似検索/ramen_images/ramen1.jpg"
img = image.load_img(img_path, target_size=(224, 224))
input = image.img_to_array(img)
result = model.predict(np.array([input]))
print("実際の値", result)
print("配列の長さ: ", len(result[0]))

```

```
実際の値 [[0.59220475 0.         0.         ... 2.8250904  0.         3.1016169 ]]
配列の長さ:  4096
```

ちゃんと返って来ていますね。この『関数』が今回メインで使っていく機械学習モデルになります。
似た画像を『引数』として与えると、似た『戻り値』が返ってきます。なので、この返ってきた配列をベクトルに見立てて2点間の距離を計算したものが類似度になります。

## Annoyの活用

上記までで画像類似度の計算手順は分かりました。
しかしこのままですと下記の手順を踏むことになり、計算量的に実用的ではありません。

*  1000枚分のベクトルをどこかしらに保存
*  入力画像を『関数』でベクトルに変換
*  得られたベクトルとの類似度を1000枚分計算
*  類似度の値が小さい順に結果として返す。

このベクトルの類似度計算量を少なくするために、今回はAnnoyというライブラリを活用していきます。
ちなみに似たようなライブラリは他にもありますので、お好きなものを選んでください。
https://qiita.com/wasnot/items/20c4f30a529ae3ed5f52

### Annoy
https://github.com/spotify/annoy
近似最近傍探索と呼ばれる検索方法を用いており、厳密な解を追い求めない代わりに計算量が少なく済む。
要するにいい感じに計算してくれるライブラリ。

では、実際にAnnoyを使っていきます。先程生成したベクトルをindexとともにAnnoyのモデルに登録してみましょう。

##### モデルのロード

```python

!pip install annoy
from annoy import AnnoyIndex

dim = 4096 
annoy_model = AnnoyIndex(dim)
```
##### Indexと一緒にベクトルを登録

```python

from keras.preprocessing import image
from keras.applications.vgg19 import preprocess_input

img_path = "./gdrive/My Drive/ramen_images/ramen1.jpg"
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(i, fc2_features[0])
```
これを1000枚分繰り返します。Indexを一緒に登録することも忘れずに。

```python

for i in range (1, 1001):
    img_path = "./gdrive/My Drive/ramen_images/ramen" +str(i)+ ".jpg"
    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(i, fc2_features[0])
```

最後にモデルをビルドして完了です!モデルをファイルとして保存しておき後から簡単に呼び出して使えるようにしておきます。

```python

annoy_model.build(1000)
annoy_model.save("./gdrive/My Drive/ramen_images_next.ann")
```


生成されたファイルをロードすることで瞬時に検索が行えるようになります。

## 実際に検索してみる


では、モデルをロードして使ってみましょう！get_nns_by_itemという関数を用いるとすでに登録されているベクトルを用いて近い画像ベクトルのindexを検索してくれます。

```python

items = trained_model.get_nns_by_item(2, 3, search_k=-1, include_distances=False)
print(items)
```

```
[2, 109, 948]
```

確認してみましょう！

ramen2.png
![ramen2.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/1240d7fa-de96-6c84-b4de-dda6bb3791c1.jpeg)

ramen109.png
![ramen109.png](https://qiita-image-store.s3.amazonaws.com/0/185233/ecff19c1-438e-8707-4e40-cac812420aa5.png)

ramen948.png
![ramen948.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/cc26c7cd-de29-6c2c-db6f-91a23f21d442.jpeg)

たしかに似ていますね!

### 任意の画像での検索

これを任意の画像で検索する場合も簡単です。入力としてベクトルを与えてやればいいだけ。そしてベクトルに変換する『関数』は前と同じです。例として以下の画像で検索をかけてみます。

![ramen1001.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/4d69aca3-4ce1-19da-92bc-db5fce25e67a.jpeg)


まずは『関数』でベクトル化

```python

img_path = "./gdrive/My Drive/ramen_images/ramen1001.jpg"
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で検索。get_nns_by_vectorという関数を使います。

```python

result = trained_model.get_nns_by_vector(fc2_features[0], 3, search_k=-1, include_distances=False)
print(result)
```
```
[352, 577, 413]
```
ramen352.png
![ramen352.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/53a69053-2448-96c9-b596-b8241a635cb4.jpeg)

ramen577.png
![ramen577.jpg](https://qiita-image-store.s3.amazonaws.com/0/185233/924d5a59-6088-f976-467e-035bd9432ec0.jpeg)

結構似ている気がする...!

## まとめ
今回は画像の類似検索を実装してみました！ライブラリを用いてかんたんにサクサクッと実装できるので暇な休日のお供に如何でしょうか！今回はラーメンで行いましたが、色々なことに応用できるので試してみると面白いかと思います！
異性の好みとかを学習したら面白いかもしれませんね...

ではでは、素敵な類似検索を開発してみてください！