日本の古文書で機械学習を試す(10) CNNの学習結果を使って類似画像検索

画像から文字種を識別するために学習させたCNNの出力を使って、類似画像検索を試してみる。

###モデル生成

モデルは[日本の古文書で機械学習を試す(8)](https://qiita.com/ariera/items/64ee1d88265e8dbc4e37)あたりで作ったものを流用しようかとも思ったが、せっかくなので学習データを増やして新たに作成してみる。前回までに[人文学オープンデータ共同利用センター](http://codh.rois.ac.jp/)の[日本古典籍くずし字データセット](http://codh.rois.ac.jp/char-shape/book/)から「好色一代男」「雨月物語」「おらが春」「養蚕秘録」「物類称呼」の5作品のデータをすべて学習用に使い、「当世料理」のデータをテストデータとする。

まずは、5作品のデータを読み込んで学習データを生成する。ついでに、文字番号とUTF-8の文字コードを変換する辞書、文字画像番号から画像ファイルパスを取得する配列、文字種ごと、作品ごとの画像数をカウントした辞書を生成しておく。コード中の　img_to_traindata　関数の中身は、[前回](https://qiita.com/ariera/items/64ee1d88265e8dbc4e37)と同じなので省略する。

```python:学習データ生成
#「好色一代男」「雨月物語」「おらが春」「養蚕秘録」「物類称呼」の5作品のデータを学習データとする。
# データが1個しかない文字種も学習データとして使う
import glob, os
# 5作品の文字画像データのルートディレクトリ
img_roots = ["../200003076/characters/*", "../200014740/characters/*", "../200003967/characters/*", "../200021660/characters/*",  "../brsk00000/characters/*"]
img_rows = 28
img_cols = 28
X_train = []
Y_train = []
train_filenames = []
char_index_dict = {}
char_book_count = {}
index_char_dict = {}
nb_classes = 0

for i, r in enumerate(img_roots):
    chars = glob.glob(r) # 各作品の画像のルートディレクトリ内のファイル／ディレクトリ一覧
    for char in chars:
        if os.path.isdir(char): # ディレクトリなら(ディレクトリ名はUTF-8文字コード)
            
            char_code =os.path.basename(char) # 文字コード = ディレクトリ名
            if(char_code in char_index_dict.keys()): # 既に辞書にある文字コードなら
                y = char_index_dict[char_code]
            else: # 辞書にない文字コードなら
                y = nb_classes
                char_index_dict[char_code] = y # 文字種から番号を調べるためのdict
                index_char_dict[y] = char_code # 番号から文字種を調べるためのdict
                nb_classes = nb_classes + 1 # 判別文字種数をインクリメント
             
            img_files = glob.glob(char+"/*.jpg") # ディレクトリ内の画像ファイルを全部読み込む
            for img_file in img_files:           # ディレクトリ(文字種)内の全ファイルに対して
                x = img_to_traindata(img_file, img_rows, img_cols) # 各画像ファイルを読み込んで行列に変換
                X_train.append(x) # 学習用データ(入力)に画像を変換した行列を追加
                Y_train.append(y) # 学習用データ(出力)に正解の文字種番号を追加
                train_filenames.append(img_file) # ファイル名をリスト化(画像番号からファイル名を取得するため)
            
            # 文字種ごと、作品ごとの画像数を格納
            if(char_code not in char_book_count.keys()):
                char_book_count[char_code] = {}
            char_book_count[char_code][i] = len(img_files)
```
len(X_train)で学習データ数を出力してみると、227975 だった。nb_classes(文字種の数)は3578だった。

次にテストデータを生成する。学習データにない文字種は対象外とする。

```python:テストデータ生成
# 「当世料理」のデータのうち、学習データにある文字種のみをテストデータとする
X_test = []
Y_test = []
test_filenames = []
img_root = "../200021637/characters/*"
chars = glob.glob(img_root)
use_char_count = 0
nouse_char_count = 0
for char in chars:
    if os.path.isdir(char) and os.path.basename(char) in char_index_dict:  # 学習データにある文字コード
        use_char_count = use_char_count + 1
        img_files = glob.glob(char+"/*.jpg")
        for img_file in img_files:
            x = img_to_traindata(img_file, img_rows, img_cols)        # 各画像ファイルを読み込んで行列に変換
            X_test.append(x)                                          # テストデータ(入力)＝画像データ
            Y_test.append(char_index_dict[os.path.basename(char)])    # テストデータ(出力)＝正解の文字種番号
            test_filenames.append(img_file) # ファイル名をリスト化(画像番号からファイル名を取得するため)
    elif os.path.isdir(char): # 学習データにない文字コード
        nouse_char_count = nouse_char_count + 1
print(str(use_char_count) + ", " + str(nouse_char_count))
```
上のコードの出力によると、テストデータの文字種数は406、学習データになかった文字種数は11だった。len(X_test)でテストデータ数を出力してみると、4813だった。

次に、学習、テストデータの形式を変換してから、ファイル保存しておく。

```python:学習、テストデータの形式変換、ファイル保存
from keras.utils import np_utils
import numpy as np

# 学習、テストデータをlistからnumpy.ndarrayに変換
X_train = np.array(X_train, dtype=float)
X_test = np.array(X_test, dtype=float)
Y_train = np.array(Y_train, dtype=float)
Y_test = np.array(Y_test, dtype=float)

# 文字種を表す数字をカテゴリカルデータ(ベクトル)に変換
Y_train = np_utils.to_categorical(Y_train, nb_classes)
Y_test = np_utils.to_categorical(Y_test, nb_classes)

# 作成した学習データ、テストデータをファイル保存
np.save('5books_models/X_train.npy', X_train)
np.save('5books_models/X_test.npy', X_test)
np.save('5books_models/Y_train.npy', Y_train)
np.save('5books_models/Y_test.npy', Y_test)
np.save('5books_models/train_filenames.npy', train_filenames)
np.save('5books_models/test_filenames.npy', test_filenames)

# index_char_dictをCSVファイルに保存
file = open('5books_models/index_char_dict.csv', 'w')
for k in index_char_dict.keys():
    file.write(str(k) + "," + index_char_dict[k] + "\n")
file.close()

# 番号、文字列、作品ごとの画像数をファイルに出力
file = open('5books_models/5books_chars.csv', 'w')
for char in char_index_dict.keys():
    file.write(str(char_index_dict[char]) + ",")
    file.write(char + ",")
    for i in range(len(img_roots)):
        if char in char_book_count.keys() and i in char_book_count[char].keys():
            file.write(str(char_book_count[char][i]) + ",")
        else:
            file.write("0,")
    file.write("\n")
file.close()
```
保存したものを読み込むときはこんな感じ。画像データを読み込んで配列を作り直すのと比べたら、かなり速く読み込める。

```python:学習、テストデータをファイルから読み込む
# 保存した学習データ、テストデータを読み込む
import numpy as np

X_train = np.load('5books_models/X_train.npy')
Y_train = np.load('5books_models/Y_train.npy')
X_test = np.load('5books_models/X_test.npy')
Y_test = np.load('5books_models/Y_test.npy')
train_filenames = np.load('5books_models/train_filenames.npy')
test_filenames = np.load('5books_models/test_filenames.npy')
```

この後、モデル定義して学習させるのだが、モデルは[日本の古文書で機械学習を試す(8)](https://qiita.com/ariera/items/64ee1d88265e8dbc4e37)の「複数作品のデータを使ってモデル生成」と同じ畳み込み3層のモデルを生成した。エポック70にしただけで、ソースは全く同じなので省略する。

ちなみに、学習には1エポック当たり20分、70エポックで約24時間かかった(!) [前回](https://qiita.com/ariera/items/64ee1d88265e8dbc4e37)の学習データ205175個、5クラス問題では1エポック当たり約15分、[前々回](https://qiita.com/ariera/items/64ee1d88265e8dbc4e37)の学習データ100422個、1061クラス問題で1エポック当たり約5分だった。

学習した中で、一番精度が良かったのが67エポック目のモデルで、学習データに対する精度0.937、テストデータに対する精度0.605だった。テストデータに対する精度はなかなか良くならない。この先はこのモデルを使って類似画像検索を行う。

###中間層の学習結果を取り出す

前置きが長くなったがここからが本題。モデルの出力をベクトルとみなして、ベクトルの類似度が高いものを類似画像として出力したい。

学習モデルのうち、どの層のデータを類似画像検索に使うかを決めるため、まずは学習したモデルを読み込んで層構造を表示させる。

```python:モデルの層構造表示
# 一番精度が良かったモデルを読み込む
from keras.models import load_model
model = load_model('5books_models/model_67_0.605.h5')

# 学習したモデルのレイヤー(層)構造を確認
model.summary()
```
結果はこうなった。

```text:出力結果
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 32)        9248      
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 12, 12, 32)        0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 10, 10, 32)        9248      
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 5, 5, 32)          0         
_________________________________________________________________
dropout_3 (Dropout)          (None, 5, 5, 32)          0         
_________________________________________________________________
flatten_2 (Flatten)          (None, 800)               0         
_________________________________________________________________
dense_3 (Dense)              (None, 128)               102528    
_________________________________________________________________
dropout_4 (Dropout)          (None, 128)               0         
_________________________________________________________________
dense_4 (Dense)              (None, 3578)              461562    
=================================================================
Total params: 582,906
Trainable params: 582,906
Non-trainable params: 0
_________________________________________________________________

```
どの層の出力を使うと良いかを試すため、一番下の全結合dense_4層からflatten_2層までと、最後の畳み込みconv2d_6層の5種類を抽出して、それぞれ新しいモデルを作り、5番目のモデルの構造を表示させてみる。

```python:モデルの層構造表示
# 中間層のデータを出力するモデルを生成(5種類)
from keras.models import Model
hidden_layer_models = [] # 5種類のモデルを格納する配列
# 対象とする層
layer_names = ['dense_4', 'dropout_4', 'dense_3', 'flatten_2', 'conv2d_6']

# 各層について、入力層=元の入力層、出力層=5種類のそれぞれとした新モデルを生成
for layer_name in layer_names:
    hidden_layer_models.append(Model(inputs=model.input, outputs=model.get_layer(layer_name).output))

# 生成した新しいモデルの構造を確認
hidden_layer_models[4].summary()
```
意図した通りに、元の入力層から3つ目の畳み込み層までが抽出されている。

```text:出力結果
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4_input (InputLayer)  (None, 28, 28, 1)         0         
_________________________________________________________________
conv2d_4 (Conv2D)            (None, 26, 26, 32)        320       
_________________________________________________________________
conv2d_5 (Conv2D)            (None, 24, 24, 32)        9248      
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 12, 12, 32)        0         
_________________________________________________________________
conv2d_6 (Conv2D)            (None, 10, 10, 32)        9248      
=================================================================
Total params: 18,816
Trainable params: 18,816
Non-trainable params: 0
_________________________________________________________________

```

次に、227975個の学習データを新モデルに入力して、出力のベクトルを取得する。後ろの4層の出力は1次元ベクトルだが、畳み込み層の出力のみ10x10x32の多次元行列なので、1列に並べてベクトル化する必要がある。

```python:学習データをベクトル化
# 学習データを新モデルに入力して、出力データ(ベクトル)を取得
hidden_output = [] # 5種類 x 227975データ x 出力を格納
for i, hidden_layer_model in enumerate(hidden_layer_models):
    hidden_output.append(hidden_layer_model.predict(X_train))

# conv2d_6の出力10x10x32を1列に並べる
hidden_output4 = []
for out in hidden_output[4]:
    hidden_output4.append(out.flatten())
hidden_output4 = np.array(hidden_output4, dtype=float)
```
確認のため、元の28x28の画像の画素値を1列に並べただけのベクトルを作る。(これより精度が上がっていないと、わざわざ学習させる意味がないので)

```python:元画像データをそのままベクトル化
# 元の28x28x1行列を1列に並べただけのデータを作る
hidden_output5 = []
for img_data in X_train:
    hidden_output5.append(img_data.flatten())
hidden_output5 = np.array(hidden_output5, dtype=float)
hidden_output.append(hidden_output5)
```
最終的にできたベクトルは、全部で6種類 x 227975画像のデータ。並べてみると、畳み込み層から結合層の間で次元数が小さくなっていることに気がついた。元々MNISTの10クラス分類用のモデルを少しだけ修正したものなので、3578クラス分類にそのまま適用するには無理があるのかもしれない。テストデータの精度がなかなか上がらないのはそのせいか？？

| 層|次元数|説明 |
|:--------|:----------|:-----------|
| dense_4| 3578 | CNNの最後の出力の結合層|
| dropout_4|128 |dense_4の1つ前|
| dense_3| 128 | dense_4の2つ前|
| flatten_2| 800 |dense_4の3つ前 |
| conv2d_6| 3200(10x10x32) | 3つ目(最後)の畳み込み層 |
| 元画像| 784 (24x24) | 元画像の画素値を並べただけ |

### 類似ベクトルを求めるための準備

自力でシコシコ全データのコサイン類似度を計算せずに、入力ベクトルの類似ベクトルを求める方法を検索していたら、YahooのNGTというライブラリが高速で良さそうなのだが、Windows環境に導入するにはコンパイルとか大変そうだったので、NGTとよく比較されているgemsimのKeyedVectorsを使うことにした。類似単語や類似文書を探すのによく使われるライブラリらしい。

まずはAnacondaプロンプトから
`> conda install gensim`
でサクッとgensimをインストール。

それから、6種類のベクトルを、KeyedVectorsで読み込めるように、Word2Vec形式でファイルに出力する。Word2Vec形式は1行目がデータ数(227975)とベクトルの次元、2行目以降が単語とその単語のベクトルを横に並べたデータ。今回は単語のかわりに、画像番号-画像のパスを出力した。

```python:ベクトルをファイル保存
# 学習データのベクトル(新モデルの出力)を、Word2Vecの形式でファイル保存
for i, output in enumerate(hidden_output):
    file = open('5books_models/hidden_output_vec_'+str(i)+'.txt', 'w')
    file.write(str(len(output)) + " " + str(len(output[0])) + "\n")

    for img_index, img_data in enumerate(X_train):
        file.write(str(img_index) + "-" + train_filenames[img_index] + " ") # 番号-ファイル名
        for data in output[img_index]:
            file.write(str(data) + " ")
        file.write("\n")
    file.close()
```
ファイルの中身はこうなっている。

```text:Word2Vecの形式で保存したファイルの中身
227975 128
0-../200003076/characters\U+3031\U+3031_200003076_00005_1_X0455_Y1765.jpg 0.0 0.0 0.0 0.08517532 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.7376558 0.0 0.0 0.076180086 0.0 0.0 0.716135 0.0 0.0 0.08609759 0.0 0.0 0.0 0.0 0.1443302 0.0 0.0 0.0 0.8975136 0.0 0.12174219 0.0 0.0 0.0 0.0 0.0 0.0 0.68014574 0.0 0.0 0.0 0.3138621 0.0 0.0941485 0.66846204 0.0 1.0076846 0.0 0.0 0.30567327 0.0 0.080581725 0.0 0.0 0.28241405 0.0 0.0 1.177963 0.0 0.0 0.0 0.3088567 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.45398924 0.0 1.7876724 0.25604373 0.0 0.0 0.0 0.0 0.0 0.0 0.02805686 0.0 0.7151221 0.4068477 0.0 0.0 0.7231041 0.4341987 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.35771322 0.50233877 0.0 0.40137863 0.0 0.0 0.24937543 0.0705165 0.49611527 0.7315356 0.0 0.42676476 0.58468497 0.0 0.0 0.0 0.20150018 0.0 0.0 0.0 0.044898316 0.0 0.68008065 0.01981572 0.0 0.0 0.0 0.7041694 0.6397317 0.0 
1-../200003076/characters\U+3031\U+3031_200003076_00005_2_X1591_Y2184.jpg 0.0 0.0 0.0 0.25420845 0.0 0.0 0.0 0.0 0.0 0.010237396 0.0 0.0 0.0 0.0 0.5042252 0.0 0.0849196 0.4395324 0.0 0.0 0.54965365 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.04886893 0.0 0.0 0.0 0.97699153 0.0 0.0 0.0 0.045532376 0.0 0.0 0.0 0.0 0.1579537 0.0 0.0 0.0 0.0 0.581619 0.27023745 0.974125 0.0 0.99255264 0.0 0.0 0.0 0.0 0.59868896 0.0 0.0 0.33456394 0.0 0.0 1.1824338 0.0 0.0 0.0 0.31439435 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.07665476 0.0 1.7932621 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.3613395 0.0 1.1031724 0.28345218 0.0 0.0 0.77971184 0.36496827 0.0 0.083772644 0.0 0.0 0.0 0.0 0.0 0.0 0.2708289 0.0 0.74034977 0.0 0.0 0.7824321 0.22141036 0.78064585 0.7677625 0.0 0.16334346 0.38017797 0.0 0.0 0.0 0.014155477 0.2936823 0.0 0.0 0.55165875 0.0 0.658348 0.2434754 0.0 0.0 0.0 0.4257703 0.9631592 0.0 
```
次に作成した6種類のファイルを読み込ませてみる。

```python:ベクトルをファイル保存
# 学習データのベクトルを読み込み(+処理時間計測)
import time
start = time.time()

from gensim.models.keyedvectors import KeyedVectors
vectors = []
for i in range(6):
    vectors.append(KeyedVectors.load_word2vec_format('5books_models/hidden_output_vec_'+str(i)+'.txt', binary=False))

elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")
```
読み込みがとんでもなく遅かったので、処理時間を計ってみたら2175秒(約36分)、あまりに遅いので、keyedvectors ネイティブの形式で保存し直してみたら、読み込み速度は何と4.8秒！この差は一体何なんだ？？

```python:ベクトルファイル変換
# 読み込んだベクトルファイルをKeyedVectors nativeの形式で保存
for i, vec in enumerate(vectors):
    vec.save('5books_models/hidden_output_vec_'+str(i)+'.kv')

# KeyedVectors native から読み込む(+処理時間計測)
import time
start = time.time()

from gensim.models.keyedvectors import KeyedVectors
vectors = []
for i in range(6):
    vectors.append(KeyedVectors.load('5books_models/hidden_output_vec_'+str(i)+'.kv', mmap='r'))

elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")
```

### 類似画像検索

テストデータのうち、モデルでの文字種識別で正解したデータと間違えたデータで類似画像検索結果がどう違うかを見てみたいので、まずテストデータを正解したデータと間違えたデータに分ける。

```python:テストデータを分ける
# テストデータを正解したデータと間違えたデータに分ける
test_correct_indexes= [] # 正解した画像番号を入れる配列
test_miss_indexes = []   # 間違えた画像番号を入れる配列

for i, x in enumerate(X_test): # 各テストデータについて
    # 正解データ
    correct_index = np.where(Y_test[i] == True)[0][0]  # データがTrueになっている箇所のインデックス
    ## どの文字かを判別する
    x = np.array([x])
    preds = model.predict(x)
    pred_index = preds.argmax() # 確率の一番高い文字の番号

    if correct_index == pred_index: # 正解
        test_correct_indexes.append(i)
    else: # 間違い
        test_miss_indexes.append(i)
print(test_miss_indexes)
```
そして、正解したデータ(上のコードのprintで出力されなかった)データを適当に選んで、6種類のモデルに入力し、ベクトル化する。


```python:類似画像を探す元データをモデルに入力して、6種類のベクトルに変換
src_index = 2510 # 何番目のテストデータを使うか

# チェックするデータをモデルに入力してベクトル化
output = []
for i in range(5):
    output.append(hidden_layer_models[i].predict(np.array([X_test[src_index]])).flatten())
output.append(X_test[src_index].flatten()) # 6つ目は画像の画素値をそのまま並べたもの
```
そして、6種類のモデルについて、それぞれ類似画像TOP10を抽出して、配列resultに格納する。dense_4のモデルの検索結果(result[0])を表示させてみたがサッパリ分からない。

```python:類似画像検索実行
# チェックするデータのベクトルを入力して類似画像抽出
result = []

import time # 遅いので処理時間を測ってみる
start = time.time()

# チェックするデータのベクトルを入力して類似画像抽出
result = []
for i, vec in enumerate(vectors):
    result.append(vec.most_similar(positive=[output[i]], negative=[], topn=10))
    elapsed_time = time.time() - start
    print (str(i) + "_elapsed_time:{0}".format(elapsed_time) + "[sec]")
    start = time.time()
print(result[0]) # dense_4のモデルの結果を表示
```
```text:検索結果
[('36658-../200003076/characters\\U+308B\\U+308B_200003076_00018_1_X1520_Y2755.jpg', 0.9879545569419861),
 ('3771-../200003076/characters\\U+304B\\U+304B_200003076_00120_1_X0858_Y0919.jpg',  0.9863320589065552),
(あと8行は省略)]
```
処理時間の出力結果はこうなった。ベクトルの次元数とほぼ比例して処理時間が増えている。
0_elapsed_time:17.97260284423828[sec]
1_elapsed_time:0.5217998027801514[sec]
2_elapsed_time:0.49208593368530273[sec]
3_elapsed_time:4.044452905654907[sec]
4_elapsed_time:11.511914491653442[sec]
5_elapsed_time:4.958259344100952[sec]

### 検索結果を可視化

ファイル名を列挙されてもサッパリ分からなかったので、検索結果を可視化してみる。可視化するときに正解の文字種番号ではなくて文字そのものを表示したいので、学習、テストデータのYデータ(正解データ)を文字に変換する関数を作っておく。

```python:可視化の準備
# 保存したCSVからindex_char_dictを読み込む
index_char_dict = {}
import csv
with open('5books_models/index_char_dict.csv', 'r') as f:
    csvdata = csv.reader(f)
    data = [x for x in csvdata]
    index_char_dict = dict((int(d[0]),d[1]) for d in data)

# Y_trainまたはY_testのの1データ(正解を表すone hotベクトル)から、正解の文字を取得する関数を定義 
def get_char(vec):
    correct_index = np.where(vec == True)[0][0] # 正解番号
    char_code = index_char_dict[correct_index]  # 正解番号からUTF-8の文字コード
    char = chr(int(char_code.replace('U+', ''), 16)) # UTF-8の文字コードから文字
    return char
```

そして、検索結果を表示する。result[0]～result[5]にそれぞれ10個の類似画像の画像番号とベクトルが入っているので、画像番号を取得→学習用データのXデータ(元画像データを行列化したもの）を取り出す→Xデータをmatplotlibで画像として表示という流れで表示する。

```python:類似検索結果可視化
# 検索結果を表示
import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure(figsize=(10,7)) # 全体の表示領域のサイズ(横, 縦)
fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.5, wspace=0.05)

# 元画像
ax = fig.add_subplot(7, 10, 1, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし
ax.title.set_text(get_char(Y_test[src_index])) # グラフタイトルとして正解番号を表示
ax.imshow(X_test[src_index].reshape((img_rows, img_cols)), cmap='gray')

# 類似画像
for n, re in enumerate(result):
    for i in range(10):
        char_index = int(re[i][0].split("-")[0]) # 画像番号を取得(ファイル名と切り離す)
        ax = fig.add_subplot(7, 10, (n+1)*10+i+1, xticks=[], yticks=[]) # 縦分割数、横分割数、何番目か、メモリ表示なし1
        ax.title.set_text(get_char(Y_train[char_index])) # グラフタイトルとして正解番号を表示
        ax.imshow(X_train[char_index].reshape((img_rows, img_cols)), cmap='gray')
```

出力された画像をいくつか並べてみる。まずは正解した画像データから。一番上が入力したテストデータの画像、2列目はdense_4モデルで検索した類似画像(左から類似画像が高い順に10個)、3列目=dropout_4モデル、4列目=dense_3モデル、5列目=flatten_2モデル、6列目=conv2d_6モデル、7列目=画素値を並べただけのデータ、各文字の上に何の文字かを表示した。
![類似画像検索14(4810)正解.png](https://qiita-image-store.s3.amazonaws.com/0/306431/cc735325-d0de-484a-f497-6529d08390d9.png)

一番下の画素値を並べただけのは論外だが、一番上の列は文字種識別結果を表すベクトルなので、かなり近い画像が抽出されている。

元々の学習モデルは6列目～2列目まで順に学習しているはずなのだが、6、5列目でそこそこ近い画像が抽出され、4、3列目では外れが多く、2列目では入力と同じ「黒」の文字だけが抽出されている。

各座標の画素値が近い(7列目)のと、抽出した特徴量が近い(6～3列目)と、文字種識別結果が近い(2列目)のでは、抽出される画像が違うということだろう。

![類似画像検索17(640)正解.png](https://qiita-image-store.s3.amazonaws.com/0/306431/8914828f-1302-e61c-b3c8-947c28f6b33e.png)
これも正解したデータだが、文字の形としては2列目左端よりも、3列目左端、4列目左端や、3～4列目の真ん中ぐらいの文字のほうが近く見える。これが「ぎ」というのは私には読めない(笑)

![類似画像検索18(3119).png](https://qiita-image-store.s3.amazonaws.com/0/306431/1568ecca-0986-b555-e226-80a9276a9cb5.png)
これは間違えたデータとして分類されているが、正解がカタカナの「ミ」で抽出されたのがひらがなの「み」なので、ほぼ正解といっていいのではないだろうか。このデータは珍しく、画素値を並べただけのデータでも近いものが抽出されている。

![類似画像検索20(4566).png](https://qiita-image-store.s3.amazonaws.com/0/306431/9fa0c7d6-4474-fa40-c15a-a77d6c1a62ee.png)
これも間違えたデータ。正解の「進」が類似画像中に１個も出てこない！ 5～6列目のほうが、「しんにょう」が一致している分、正解に近いかも。

![類似画像検索21(3614).png](https://qiita-image-store.s3.amazonaws.com/0/306431/7a842334-f25b-6e11-954e-417a3f9794eb.png)
これも間違えたデータ。これも正解の「小」が類似画像中に１個も出てこない。でも画像としてはかなり類似するものが抽出されていると思う。これは文脈から推測しないと読めないパターンでは？

[日本の古文書で機械学習を試す(4) ](https://qiita.com/ariera/items/103631729680f82c1226)でやりかけた、次に来る文字を予測するLSTM(RNN)などを組み合わせると良いのか？ 単語辞書みたいなものを作って辞書に出てくる単語を形成する文字のスコアを上げるとかすればよいのか？


### おまけ、matplotlib の日本語表示でハマった話

環境は、Windows10, Python3.6.7, matplotlib3.0.2

類似画像を可視化するときに、正解文字を日本語で表示させたのだが、matplotlibのデフォルトのままだと文字化けして□になってしまう。

色々なサイトで調べた結果、
(1)
[IPAのサイト](https://ipafont.ipa.go.jp/node26)から、IPAゴシックのzipデータをダウンロードして、解凍した ipaexg.ttf を
C:\Users\[ユーザー名]\Anaconda3\Lib\site-packages\matplotlib\mpl-data\fonts\ttf
のフォルダに置く。

(2)
C:\Users\[ユーザー名]\Anaconda3\Lib\site-packages\matplotlib\mpl-data\matplotlibrc
を
C:\Users\[ユーザー名]\.matplotlib
にコピーし、コピーした方のmatplotlibrcを開いて
font.family : IPAexGothic
の行を追加。

(3)
C:\Users\[ユーザー名]\.matplotlib にあるfontList.jsonと fontList.py3k.cache (キャッシュファイル) を削除

という手順が判明した。しかしその通りにやっても文字化けは解消せず、
C:\Users\[省略]\site-packages\matplotlib\font_manager.py:1241: UserWarning: findfont: Font family ['IPAexGothic'] not found. Falling back to DejaVu Sans.
  (prop.get_family(), self.defaultFamily[fontext]))
というメッセージが出る。
(指定されたIPAexGothicが見つからないからデフォルトのDejaVu Sansを使いますということ)

ネットでいろいろ調べても、「キャッシュファイルを削除してください」という答えしか書いていないところが多い。

Jupyter Notebook上で次のように書いても同じメッセージが出て、DejaVuSans.ttf の場所が表示される。

```python:フォントチェック
import matplotlib
matplotlib.font_manager.findfont('IPAexGothic')
```
しかし、表示されるフォルダが、
C:\Users\[ユーザー名]\Anaconda3\Lib\site-packages\matplotlib\mpl-data\fonts\ttf
ではなく
C:\Users\[ユーザー名]\Anaconda3\envs\\[仮想環境名]\lib\site-packages\matplotlib\mpl-data\fonts\ttf
になっている。Anacondaのデフォルト環境ではなく、古文書用に1つ仮想環境を作って実行していたので、こちらのフォルダにもフォントを ipaexg.ttf をコピーした。

これで解決！と思ったら、まだ文字化けがなおらない。

やけくそで
C:\Windows\Fonts
にも ipaexg.ttf をコピーしてみるがやっぱりダメ。Windowsを再起動してもダメ。

で、なぜかJupyter Notebook上で以下のコードを実行したら日本語が表示されるようになった。ネタ元は[ここ](https://github.com/matplotlib/matplotlib/issues/10201)。

```python:フォントチェック
import matplotlib
matplotlib.font_manager.findfont('IPAexGothic', rebuild_if_missing=True)
```

この後、C:\Windows\Fonts\ipaexg.ttf を削除しようとしたら、使用中と言われて削除できなかった。(Windows再起動後も)

上で試した処理のうち、どれが必要なものでどれが不要なものなのか、単にキャッシュを削除したつもりでどこかに残っていて時間が経って解決したのか、よく分からないのが気持ち悪いが、とりあえず解決したのでメモしておく。