[Python]初心者が機械学習で作成した犬種判別アプリつくってみた

# 目次
[1.はじめに](##-1.はじめに)
[2.目的](##-2.目的)
[3.実行環境](##-3.実行環境)
[4.実施内容](##-4.実施内容)
[5.アプリケーション](##-5.アプリケーション)

## 1.はじめに

これまでプログラミングをしっかり学んだことがない私が、どこまでできるものかと思いチャレンジしてみました。


## 2.目的

学習過程でMNISTというデータセットを利用して10種類の数値を画像認識をするコードがあったので、
以下のことを実施してみたいと思いアプリ作成実施


- 既に用意されたデータではなく、自身で収集したデータでのアプリ制作
- 識別種類をもっと増やした場合のアプリ制作


## 3.実行環境
- Python 3.9.12
- Windows10
- Google Colaboratory
- Visual Studio Code (1.72.2)


## 4.実施内容

### 　4-1.元データの収集
#### 　　4-1-1.各犬種の写真収集

- 当初の目論見では、必要なデータ数の目途もたたない状態だったため、必要なデータ数をざっくり調べてみたところ、もちろんケースによるが、1犬種あたり少なくとも100枚程度ということが分かり、ここで挫折しかける。

- しかし、それでも頑張ろうとしていたところ、識別すべき犬種数を調べたところ100種以上ということが分かり、ここで挫折。

- 自身で1からデータ収集することは諦め、Kaggleというデータセットが用意されているサイトがあるということを知り、そこに駆け込む。

>https://www.kaggle.com/

#### 　　4-1-2.元データに対してラベリング

- 元データとなる画像は集められたが、これをmodel作成にどのように使うかで迷う。

- MNISTのデータセットでは、すでに各画像に対し、それが何を意味するものかを機械が分かるようにラベリングされていたが、今回はそこも自身で実施する必要に気付く。

- 各画像は各犬種単位でフォルダ分けされていたため、そのフォルダ名を glob を利用して取得。

- その上で、一つめの犬種フォルダの画像にはindex番号 0 二つめの犬種フォルダの画像には index番号 1としていくことで、画像フォルダ単位でラベリングを実施。

- 具体的には以下のように enumurate()関数と for in を利用。

```
import glob
import numpy as np
import cv2

X = []
y = []
dog_breed = []

main_pass = '.pyフォルダが格納されているフォルダパス'
dogbreed_pass = glob.glob(main_pass +'/practice_dog/*') 

for index,a in enumerate(dogbreed_pass):
  pic_list = glob.glob(a +'/*') 
  a = a.rsplit('/',) 
  a = a[-1] 
  dog_breed.append(a) 

  for b in pic_list: 
    buf = np.fromfile(b, np.uint8)
    image = cv2.imdecode(buf, cv2.IMREAD_UNCHANGED)

    X.append(image)  
    y.append(index)  
```
rsplit部分について補足
関数としては以下のような形で、第二引数で指定した数だけ、後ろから区切るものです。
```
rsplit('区切り文字', 整数)
```
変数aには、犬種が入っているフォルダのパスがそのまま入っています。
ここでのフォルダ名を後々の犬種判別のときにも利用したいと考えたため、
フォルダのパスはいらないけれど、画像が格納されているフォルダの名称は欲しい。
そんな、状況でrsplit関数で後ろから/で区切った最初の文字列を取得しています。


#### 　　4-1-3.TrainデータとTestデータに分ける
機械学習を行う際には、元データを2種に分割する。
一つは元データから法則性を学ぶために利用するTrainデータ
もう一つは、上記で学んだ法則性の精度を測定するために利用するTestデータ

以下のように、X_train,y_train,X_test,y_testとかく変数へ格納する。
なお、変数Xは画像データを、変数yはラベルデータが入っている。

ただ、ここで後々困ることが発生したため、先にその内容を記述。

#### 　失敗例
```
train_rate = 0.8

X_train = X[:int(len(X)*train_rate)]
y_train = y[:int(len(y)*train_rate)]
X_test = X[int(len(X)*train_rate):]
y_test = y[int(len(y)*train_rate):]
```

#### 　成功例
```
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

train_rate = 0.8

X_train = X[:int(len(X)*train_rate)]
y_train = y[:int(len(y)*train_rate)]
X_test = X[int(len(X)*train_rate):]
y_test = y[int(len(y)*train_rate):]
```
違いは、成功例部分の冒頭に記載した3行のコード

なぜ、このコードが必要な理由は以下。
今回のコードの場合appendでデータを順番に変数に格納していっていた。
そのため、変数の中では犬種ごとに綺麗にデータが分かれていた。

言葉だとイメージしにくいので以下の画像を参照
5種類の犬種ごとのデータがあったとした場合を想定

#### 　TrainデータとTestデータに分ける前
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/482a674f-4f15-9ab0-6a47-c6a5964b1948.png" width=80%>

こんな感じです。
上記画像の番号は変数内に入っている順番です。

しかし、これを「失敗例」のようにそのまま分割してしまうと、以下のようになります。
#### 　失敗例
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/c22ffee4-75f8-2f55-e215-5fa44e957794.png" width=80%>

これの問題点は、5種類の犬種を分類したいのに、ここでTrainデータとして学習しているのは、4種類の犬種だけであること。
また、上記の学習の精度を判定するためのTestデータが1種類の犬種しかないこと。

これでは、当たり前ですが学習していない犬種を判別することができないので、判別の精度は低いままでした。

それを、
#### 　成功例
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/38e060d4-203f-b2d1-8a60-2f6f60b717b5.png" width=80%>

このように、犬種の並びをランダムにすることで、すべての犬種がTrainデータに入り、同様にすべての犬種がTestデータにも格納されることで、適切に学習とその精度の判定を行うことができるようになりました。



### 　4-2.model作成

4-1.までで、機械学習のmodel作成に必要な元データの準備ができたので、ここからは実際にmodelの作成を実施。


#### 　4-2−1.version.1
<!-- 実施コード code_2 result_2 -->


```
#②モデルの作成

from tensorflow.keras.models import Model,Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from keras.utils.np_utils import to_categorical
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras import optimizers
import matplotlib.pyplot as plt

import os
import numpy as np


##転移学習
# vgg16のインスタンスの生成
input_tensor = Input(shape=(100,100,3))
vgg16 = VGG16(include_top=False,weights='imagenet',input_tensor=input_tensor)

top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dense(128,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(count_dog_breed,activation='softmax'))

# モデルの連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

# vgg16の重みの固定
for layer in model.layers[:19]:
  layer.trainabile = False

#コンパイル
model.compile(loss= 'categorical_crossentropy',optimizer=optimizers.SGD(lr=1e-4,momentum=0.9),metrics=['accuracy'])
#breakpoint()

#学習
history = model.fit(X_train,y_train,batch_size=50,epochs=70,validation_data=(X_test,y_test))

#精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

"plt.plot(history.history['accuracy'], label=""acc"", ls=""-"", marker=""o"")"
"plt.plot(history.history[""val_accuracy""], label=""val_acc"", ls=""-"", marker=""x"")"
"plt.ylabel(""accuracy"")"
"plt.xlabel(""epoch"")"
"plt.legend(loc=""best"")"
plt.show()

# 予測（検証データの先頭の3枚）
pred = np.argmax(model.predict(X_test[0:3]), axis=1)
print(pred)

#モデル全体の確認
model.summary()

#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)

#学習済みモデルを保存
model.save(main_pass +'/model.h5')

"print(""■■終了■■"")"
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/bbfc903d-4c67-4905-c3f4-0f3a74445367.png" width=60%>

Test accuracy: 0.579


まずは、正解率云々は置いておいて、動くものを作成してみました。
正解率57.9％
初めての起動にしては悪くなさそう。

---
#### 　4-2−2.version.2
<!-- 実施コード code_3 result_3-->

```
# 変更箇所のみ記載

top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dense(128,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(56,activation='relu'))
top_model.add(Dense(count_dog_breed,activation='softmax'))
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/203ff28d-cd70-7c31-4e1c-5ee959498357.png" width=60%>

Test accuracy: 0.612

少しmodelに要素を追加してみた。
top_model.add(Dense(56,activation='relu'))を追加

正解率61.2％

このまま順調にいけばもっと正解率が上がるのではないかと、
そんな風に簡単に考えていました。




---
#### 　4-2−3.version.3
<!-- 実施コード code_4 result_4-->


```
# 変更箇所のみ記載

top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dense(128,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(56,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(count_dog_breed,activation='softmax'))
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/4d008472-be44-f469-2d87-69ebf78ba0fe.png" width=60%>

Test accuracy: 0.496

version.2のグラフを見ると、
epoch数を重ねるごとに「acc」「val_acc」の値が乖離していることから
過学習が進んでいることが見えたため、
top_model.add(Dropout(0.3))　を追加。
結果の正解率は49.6％へ下落

しかし、過学習は抑えられたようにも見える。

---
#### 　4-2−4.version.4
<!-- 実施コード code_5 result_5-->

```
# 変更箇所のみ記載

history = model.fit(X_train,y_train,batch_size=50,epochs=130,validation_data=(X_test,y_test))
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/da9ff14d-c2a3-0cca-9850-31f46378d616.png" width=60%>

Test accuracy: 0.618

正解率61.8％
epochs=130 が足りないのかと思い増やしてみた。
確かに正解率上がったけど、version.2と大差ないようにも見える。

epoch数を増やしたことにより、Google Colabでの処理時間も長くなり、
検証もしにくくなってきた。

---


#### 　4-2−5.version.5
<!-- 実施コード code_6 result_6-->



```
top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dense(128,activation='relu')) 
top_model.add(Dropout(0.4))
top_model.add(Dense(56,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(28,activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(count_dog_breed,activation='softmax'))
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/015cf2f2-c0b2-46db-9016-2f84a60f4e35.png" width=60%>

Test accuracy: 0.376

過学習にはDropoutをすれば良いかと思い、
top_model.add(Dropout(0.4))
top_model.add(Dropout(0.3))
top_model.add(Dropout(0.2))
を追加し実行。

まさかの正解率が37.6％へ激減。
これもepoch数を増やせば正解率上がるかとも思いましたが、
処理時間がかかりすぎるため、モデルの調整はこれで終了。

---


### 最終的なコード(version.2をベース)


```
#②モデルの作成

from tensorflow.keras.models import Model,Sequential
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from keras.utils.np_utils import to_categorical
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras import optimizers
import matplotlib.pyplot as plt

import os
import numpy as np

##転移学習
# vgg16のインスタンスの生成
input_tensor = Input(shape=(image_size,image_size,3))
vgg16 = VGG16(include_top=False,weights='imagenet',input_tensor=input_tensor)

top_model=Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256,activation='relu'))
top_model.add(Dense(128,activation='relu'))
top_model.add(Dropout(0.3))
top_model.add(Dense(56,activation='relu'))
top_model.add(Dense(count_dog_breed,activation='softmax'))

# モデルの連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

# vgg16の重みの固定
for layer in model.layers[:19]:
  layer.trainabile = False

# コンパイル
model.compile(loss= 'categorical_crossentropy',optimizer=optimizers.SGD(lr=1e-4,momentum=0.9),metrics=['accuracy'])

# 学習
history = model.fit(X_train,y_train,batch_size=50,epochs=70,validation_data=(X_test,y_test))

```
### 　4-3.modelの検証

今回は上記のような、場当たり的なアプローチで実施をしてみたが、
もっとスマートに正解率が上がらない原因を追求していきたい。

そこで、以下では、混合行列を利用することで、違う切り口での検証ができる方法を紹介。

    
```
#③評価各種

#混同行列を利用

from sklearn.metrics import classification_report

y_true = y_test_2 #テストデータの正解ラベル
y_pred = model.predict(X_test) #テストデータの予測結果
y_predicted = y_pred.argmax(axis=1)


#変数dog_breedには元データ読み込み時のコードで犬種名が格納されている
d = classification_report(y_true,y_predicted,
        target_names = dog_breed,
        output_dict=True)

from pprint import pprint
pprint(d)

import pandas as pd
df = pd.DataFrame(d)
df.T

```
参考にさせていただいた記事は以下

- 多クラス混同行列とその評価指標〜マクロ平均、マイクロ平均〜
  https://analysis-navi.com/?p=553

- 【python】sklearn 0.20でclassification_reportの仕様が変わっていた
  https://www.haya-programming.com/entry/2019/03/18/052035


---
#### 　4-3−1.指標の紹介


ざっくり指標についても紹介
precision(精度)：予測で「正」とされたデータの中で、実際に「正」だったデータの割合。
recall(再現率)：実際に「正」のデータの中で、予想でも「正」と判定されたデータの割合。


ここでは、予測データをベースに考えるのか、実データをベースに考えるかだと思って頂ければ十分です。

#### 　4-3−2.出力結果の紹介

実際の出力結果は以下の通り
数値は色々な見方があるが、ここではprecision(精度)を基準に考えてみる。
上位5位と下位5位が以下

- 上位5位
![22C846EB-A372-4185-BFCC-CB40FA90B1AF.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/bb66dc3a-4b2e-1076-2b7c-52a42e8d5030.jpeg)

- 下位5位
![3D624E01-6D3B-44CD-9E0B-0CB864B42DBE.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/5b6ffbd9-2594-18e2-a765-0f4544dcf258.jpeg)

---
#### 　4-3−3.考察

ここでいくつかの疑問
一般的にprecision,recall,f1-scoreはどれも同じような傾向を示すことが多い。
しかし、今回の「n02112018-Pomeranian」はprecisionでは順位が1位だが、recall(再現率)では、43位になっている。

ここから、
予測データが「正」と判断したものと実際の「正」「誤」のデータの結果は一致していた。
しかし、実際のデータが「正」と判断したものと、予測データの「正」「誤」のデータはそこまで一致していなかったことが推測される。

これをより端的に言ってしまえば、予測データが正しいと思ったものを確認してみたら、その通りだったけど、実際のデータが正しいと思ったものについては、いまいちだった。

そのため、予測データの「誤」中に正しく分類されていないデータがあると考えられる。

なぜ、そのような事象になったのか、一旦、実際の画像データを見てみる。


![29CC2260-6AF4-4073-BBC5-6D1932B0BA6E.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/42f2d21a-5133-b908-7fc4-129741de0aec.jpeg)

流石にわからないですね。



他の犬種のことも調べてみます。
逆に、全般的にprecisionが低かった犬種を調べてみたところ、共通項は見えてきました。
それは、厳密に犬種は違いがあるが、同じようなルーツの犬種があることでした。

具体的にはワースト1位の「n02106030-collie」を調べてみましょう。
データ中に「collie」の文字があるもので検索した結果が以下。

![938E5511-9987-4AD5-A20A-2EB7A041A66A.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/58ca297e-3493-eee8-8840-5c7521c36c34.jpeg)

やはり複数ありますね。


同様に、ワースト2位の「n02113624-toy-poodle」も「poodle」で調べてみましょう。

![7887966F-3385-4CF6-8BF6-301337E1CB65.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/169ca52b-3ba1-8512-7cf3-a58885fc3f16.jpeg)

なるほど、これは原因の一つになりそうですね。


逆のアプローチで上位の犬種は種類が少ないのかも確認してみます。
1位の「n02112018-Pomeranian」を「Pomeranian」で調べてみます。


![1B1BF4D4-C11D-4C1C-93D1-AA2641EB66CD.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/da3d7966-1eaf-e3ed-fbf1-86505ed6d457.jpeg)

ふむ、やはり仮説はある程度正しそうに見えてきました。

念のため、2位の「n02102480-Sussex_spaniel」も「spaniel」で調べてみます。

![90467B40-A9AA-448C-9BF3-2BB7AD76A443.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/d640c9ae-5353-210d-4162-12c34a86ea3d.jpeg)


！！
むしろ今までで1番多い。

この理由を調べるために、実際の画像データを確認してみました。


![77DBDF74-C8F7-46DF-9F97-DED65985436E.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/1f28401e-70d0-9800-669f-a66487d0f379.jpeg)

![0B8058F4-A19D-45E3-9127-997E82B6A4F3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/488260ab-3ba3-68a9-47b8-f2cc62d7f512.jpeg)

![63752567-95D7-482C-8BB2-4C689967026D.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2920833/3962f740-6fb5-16b6-678f-8a9058795337.jpeg)

なるほど、確かにパッと見ただけで、違う犬種と判断できそうですね。


以上のことから、同系統の犬種がある場合、精度は低くなりがち。しかし、同系統であっても見た目が異なる犬種は機械学習でも違いを判断しやすいため、精度は高くなる。

色々と他にも検証方法はあるかと思いますが、初めての機械学習での検証はここまで。
これからも学習を続け、より実用性の高いものを作成して参ります。



---
## 5.アプリケーション

以下に上記のアプリケーションを公開しています。

heroku url
https://dog-breed-v6-20221028.herokuapp.com/

---