TensorFlow2 + Keras による画像分類に挑戦5 ～分類に失敗する画像を観察してみる～

# はじめに

TensorFlow2 + Keras を利用した画像分類（Google Colaboratory 環境）についての勉強メモ（第5弾）です。題材は、ド定番である手書き数字画像（MNIST）の分類です。

- TensorFlow2 + Keras による画像分類に挑戦 シリーズ
  - [1. とりあえず動かす](https://qiita.com/code0327/items/7d3c7bd3327ff049243a)
  - [2. 入力データを詳しくみてみる](https://qiita.com/code0327/items/2c969ca4675d5a3691ef)
  - [3. MNISTデータを可視化してみる](https://qiita.com/code0327/items/a6edd5b27256429c3819)
  - [4. 学習済みモデルで予測させてみる](https://qiita.com/code0327/items/1047adc050ab6d75ad5c)
  - [5. 分類に失敗する画像を観察してみる](https://qiita.com/code0327/items/5dfc1b2ed143c1f9bd2b)
  - [6. 自分で用意した画像の前処理・分類をしてみる](https://qiita.com/code0327/items/d1a6b63915871a17020a)
  - [7. 層タイプ・活性化関数について理解する](https://qiita.com/code0327/items/68dd7beb1196320c8c15)
  - [8. 最適化アルゴリズムと損失関数を選択する](https://qiita.com/code0327/items/4f8f656bed23140b8962)
  - [9. モデルの学習、セーブ＆ロードをしてみる](https://qiita.com/code0327/items/951f418c81dbbd65c1fc)


[前回](https://qiita.com/code0327/items/1047adc050ab6d75ad5c) は、学習済みモデルを使って予測（分類）を行ないました。今回は、[TF公式HPのチュートリアル](https://www.tensorflow.org/tutorials/quickstart/beginner) で作成するモデルで、**予測（分類）に失敗する画像はどんなものか？**また、**どんな誤分類が生じるのか？**を観察していきます。

具体的には、matplotlib で、次のような出力をしていきます

>■ 正解値「6」に対して正しく予測（分類）できなかったケース
![x6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/a81333ce-96f6-4348-87e9-53039499e972.png)

また、次のような誤分類の関係を表した図も作成していきます。

>![ヒートマップ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/3def002d-3db0-fc35-c2cf-58f44c47f56f.png)


# 分類に失敗した手書き数字データの抽出

テスト用の入力データ（手書き数字のイメージ）10,000件のうち、TFチュートリアルのモデルを使って**正しく分類できなかったものを抽出**していきます。

ところで、学習済みモデルについて、`model.evaluate(x_test,  y_test, verbose=2)` で評価したところ 正解率（accuracy）が  0.9759 になりました。よって、$(1-0.9759)\times 10,000=241$ 枚の手書き数字が分類に失敗しています（学習の実行毎に生成される学習済みモデルは僅かに違ってくるので、その結果、分類に失敗する画像も微妙に違ってきますので留意ください）。

この抽出処理には、データ分析に便利なライブラリ「pandas」を使用します。なお、pandas の基礎についての解説は含めないので、そのあたりは他の記事を参考にしてください。

次のようなコードで、データフレームの列に正解値と予測値を格納します。ここで、行のインデックスは、`x_test` のインデックスと一致します。

```py:
import numpy as np
import pandas as pd
p_test = model.predict_classes(x_test) # 予測
df = pd.DataFrame({'正解値':y_test, '予測値':p_test})
display(df.head(5))  # 先頭から5行を表示
display(df.tail(5))  # 末尾から5行を表示
```

実行結果は、次のようになります。先頭から5件、末尾から5件については、正解値と予測値が全て一致しています。

![pd1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/59b90ab1-8ec1-ca5d-b79d-7aa57475d2d6.png)

つづいて、**正解値と予測値が一致していない行を抽出**して新たなデータフレーム `df2` に格納します。また、ソートもかけます。

```py:
# 「正解」と「予測」が一致していない行を抽出
df2 = df[df['正解値']!=df['予測値']]
display(df2.head(5))

# 昇順にソート 第1キー'正解値'、第2キー'予測値'
df2 = df2.sort_values(['正解値', '予測値'])
display(df2.head(5))
```

実行結果は、次のようになります。
![pd2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/d3ab27fb-2281-0f3a-1dcc-eb1b865626b1.png)

これより、`x_test[9634]` は「**正解は「0」であるが、予測は「1」となった**」ということが分かります。

# 分類に失敗した手書き数字画像の一覧表示

分類に失敗した `x_test[9634]` は、**どんな手書き数字なのか**、また、**誤って「1」と予測しているが、それはどの程度の確信によるものだったのか？**が気になりますよね。

**どんな手書き数字なのか**は、[第2回](https://qiita.com/code0327/items/a6edd5b27256429c3819)で解説したように matplotlib の `imshow(...)` を使えば画像出力して確認できます。

また、**誤って「1」と予測したのはどの程度の確信によるものだったのか**は、[第4回](https://qiita.com/code0327/items/1047adc050ab6d75ad5c) で解説したように、`model.predict(...)` で得られる情報のなかから取り出すことができます。具体的には次のようにすれば、求めることができます。

```py:
idn = 9634
s = model.predict( np.array([x_test[idn]]) ) # 出力層の値
s = s[0]
print( f'予測分類は「{s.argmax()}」で、出力層の対応ニューロンの出力値{s[s.argmax()]:.2f}' ) 
print( f'正解分類は「{y_test[idn]}」で、出力層の対応ニューロンの出力値{s[y_test[idn]]:.2f}' )
```
> 予測分類は「1」で、出力層の対応ニューロンの出力値0.62
> 正解分類は「0」で、出力層の対応ニューロンの出力値0.00

あるいは、[第4回](https://qiita.com/code0327/items/1047adc050ab6d75ad5c) の最後で示したコード（予測分類のレポート出力）を使えば、次のようにも確認できます。

![9634.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/721e652b-b570-f766-09c4-ac6258323fd8.png)

正解である「0」には、かすってもいませんね。左上にゴミが載っている画像なので、仕方ないのかもしれませんが・・・こういったことを観察できれば、**前処理に関する何らかのヒント**を得られる可能性があります。

しかし、これらを個別に出力するは手間なので、**まとめて出力**してみたいと思います。


```py:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as pe

for t in range(0,10):

  print(f'■ 正解値「{t}」に対して正しく予測（分類）できなかったケース')

  # 正解値が t の行を抽出
  index_list = list(df2[df2['正解値']==t].index.values)

  # matplotlib 出力
  n_cols = 7
  n_rows = ((len(index_list)-1)//n_cols)+1
  fig, ax = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(6.5, 0.9*n_rows), dpi=120)
  for i,ax in enumerate( np.ravel(ax) ):
    if i < len(index_list):
      
      p = index_list[i]
      ax.imshow(x_test[p],interpolation='nearest',vmin=0.,vmax=1.,cmap='Greys')

      # 予測（分類）を左上に表示
      t = ax.text(1, 1, f'{p_test[p]}', verticalalignment='top', fontsize=8, color='tab:red')
      t.set_path_effects([pe.Stroke(linewidth=2, foreground='white'), pe.Normal()]) 

      # 予測（分離）に対応する出力層のニューロンの値を括弧で表示
      t = ax.text(5, 2, f'({s_test[p].max():.1f})', verticalalignment='top', fontsize=6, color='tab:red')
      t.set_path_effects([pe.Stroke(linewidth=2, foreground='white'), pe.Normal()]) 
      
      # 目盛などを非表示に
      ax.tick_params(axis='both', which='both', left=False, labelleft=False, 
                     bottom=False, labelbottom=False)
      
      # 青色でインデックスを表示
      ax.set_title(index_list[i],fontsize=7,pad=1.5,color='tab:blue')

    else :
      ax.axis('off') # 余白処理

  plt.show()
```

実行結果です（テキストで出力される部分のみ少し整形しています）。

- 各画像枠の上部の<font color='blue'>**水色の番号**</font>が`x_test` の**インデックス番号**
- 画像内の左上部の<font color='red'>**赤色の数字</font>**が、**予測値**（つまり、誤って予測（分類）した値）
- 画像内の左上部の<font color='red'>**赤色の数字</font>**が、出力層の上記に対応する**ニューロンの出力値**
 - 例えば、最初の例では「<font color='red'>1 (0.6)</font>」となっていますが、これは（誤って）「1」と分類しており、出力層の「1」に対応するニューロンの出力値が「0.6」であった、という意味です。
 
■ 正解値「0」に対して正しく予測（分類）できなかったケース
![x0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/11b52ad5-d181-ff47-28e9-ede0a4bf823c.png)

■ 正解値「1」に対して正しく予測（分類）できなかったケース
![x1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/a25b838d-922b-aaa8-a76a-bbd641926271.png)

■ 正解値「2」に対して正しく予測（分類）できなかったケース
![x2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/47ad3380-018a-d451-ac66-f29d5e18e1d4.png)

■ 正解値「3」に対して正しく予測（分類）できなかったケース
![x3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/4cf428b2-704b-3579-6779-71de53049872.png)

■ 正解値「4」に対して正しく予測（分類）できなかったケース
![x4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/be0f82f8-16ce-cb92-bc41-c2bce4a5c587.png)

■ 正解値「5」に対して正しく予測（分類）できなかったケース
![x5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/93c21b2b-e39b-6a0d-e012-e16dbaa9c000.png)

■ 正解値「6」に対して正しく予測（分類）できなかったケース
![x6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/a81333ce-96f6-4348-87e9-53039499e972.png)

■ 正解値「7」に対して正しく予測（分類）できなかったケース
![x7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/5d697660-0d53-aad6-9315-f97474b8725a.png)

■ 正解値「8」に対して正しく予測（分類）できなかったケース
![x8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/a55176a1-9e32-238a-50b8-b3f679c9df2a.png)

■ 正解値「9」に対して正しく予測（分類）できなかったケース
![x9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/c85f2014-9cf9-2724-ddd3-5d6e698f9e42.png)

### 感想

こうやって眺めてみると、どう考えても認識不可能なデータ（たとえ人間が判断しても・・・）も混じっているということが分かります。その一方で、人間的な感覚から言えば、なんでそんな誤認識するの？という結果もあります。興味深いですね。


# クロス集計表

**正解値と予測値についてのクロス集計表**を作成してみます。これにより**どの数字を、どの数字に誤分類しやすいのか？**といった情報を得ることができます。クロス集計は、`crosstab(...)` で簡単に作成できます。

```py:
import pandas as pd

p_test = model.predict_classes(x_test) # 予測
df = pd.DataFrame({'正解値':y_test, '予測値':p_test})

# クロス集計表
dfc = pd.crosstab(index=df['正解値'], columns=df['予測値']) 
display(dfc)
```

実行結果は次のようになります。

![ct.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/9d644d1e-08e7-3f7b-8d5a-9e9de1384be1.png)

「4」を「9」に誤分類するケースが 19件 で一番多いようです（なんとなく分かりますね）。次いで「3」を「5」に誤分類するのが 13件、「9」を「3」に誤分類するのが 12件 になっています（これは、人間的な感覚とは違っていて面白いところですね）。

## ビジュアル化

クロス集計表を matplotlib を使ってヒートマップ的に出力してみます。コードが長くなるので先に結果を示します。

![ヒートマップ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/3def002d-3db0-fc35-c2cf-58f44c47f56f.png)


```py:matplotlib_日本語出力準備処理
!pip install japanize-matplotlib
import japanize_matplotlib
```

```py:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as pe
import matplotlib.ticker as ticker
import matplotlib.colors

p_test = model.predict_classes(x_test) # 予測
df = pd.DataFrame({'正解値':y_test, '予測値':p_test})

# クロス集計表
dfc = pd.crosstab(index=df['正解値'], columns=df['予測値']) 
#display(dfc)

for i in dfc.index.values :
  dfc.at[i,i] = 0.0

# ヒートマップ的に出力
plt.figure(dpi=160)

plt.imshow(dfc,interpolation='nearest',cmap='Oranges')
plt.plot([0,0],[9,9])

n = len(dfc.columns) # 項目数
plt.gca().set_xticks(range(n))
plt.gca().set_xticklabels(dfc.columns)
plt.gca().set_yticks(range(n))
plt.gca().set_yticklabels(dfc.columns)

plt.tick_params(axis='x', which='both', direction=None, 
                top=True, bottom=False, labeltop=True, labelbottom=False)
plt.tick_params(axis='both', which='both', top=False, left=False )

# グリッドに関する設定
plt.gca().set_xticks(np.arange(-0.5, n-1), minor=True);
plt.gca().set_yticks(np.arange(-0.5, n-1), minor=True);
plt.grid( which='minor', color='white', linewidth=1)

plt.gca().xaxis.set_label_position('top') 
plt.xlabel('予測値')
plt.ylabel('正解値')

plt.plot([-0.5,n-0.5],[-0.5,n-0.5],color='black',linewidth=0.75)

# 相関係数を表示（文字に縁取り付き）
tp = dict(horizontalalignment='center',verticalalignment='center')
ep = [pe.Stroke(linewidth=3, foreground='white'),pe.Normal()]
for y,i in enumerate(dfc.index.values) :
  for x,c in enumerate(dfc.columns.values) :
    if x != y :
      if dfc.at[i,c] !=  0:
        t = plt.text(x, y, f'{dfc.at[i,c]}',**tp)
        t.set_path_effects(ep) 
```


# 次回

- 次回は、自分で作成した手書きでデータを入力とした予測を行ないます。Google Colab. に画像をアップする手順、画像の読込み、リサイズ、その他の前処理などを含みます。

![予測.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152805/6c7dc26d-9846-9c60-5547-436214924453.png)