たぬきっぽい生き物を画像で識別

# はじめに

この記事は Aidemy Premium plan AIアプリ開発コースの最終成果物としてAIを用いた画像認識アプリを開発する過程を記録したものです。

昨今、たぬきとアライグマ、レッサーパンダとアライグマを混同する場面をよく見かけるので、たぬき、アライグマ、レッサーパンダを識別するWebアプリを作成しました。

# 目次
1. 実行環境
2. 学習する画像の取得
3. CNNモデルの作成及び学習
4. HTMLとFlaskコードの作成
5. アプリの動作確認
6. renderへのデプロイ
7. 反省

# 実装環境
・Python 3.11.5
・Google Colaboratory
・Visual Studio Code

# 学習する画像の取得
スクレイピングは使用せず、以下より、必要な画像データをダウンロードしました。
・[たぬき](https://paperswithcode.com/dataset/ntlnp-wildlife-image-dataset)
・[アライグマ](https://www.kaggle.com/datasets/debasisdotcom/racoon-detection)
・[レッサーパンダ](https://www.kaggle.com/datasets/maximilien2505/panda-or-redpanda/code)

取得した画像はgoogledriveへ保存しました。

たぬきはおよそ400枚、アライグマとレッサーパンダは200枚ほどの画像が得られました。
たぬきは国立公園の赤外線カメラで撮影されらビデオクリップのため、後ろ姿や、画面の端にわずかに映っているものが多く、また半分以上は夜に撮影された白黒のものでした。

# CNNモデルの作成及び学習
CNNモデルを作成します。
CNNとは人間の脳の視覚野と似た構造を持つ 「畳み込み層」 という層を使って特徴抽出を行うニューラルネットワークです。

###### 画像の読み込みとデータの分割
```python:seikabutu.py
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers

# ファイルパスの指定。
path_tanuki = os.listdir('/content/drive/MyDrive/Aidemy_seikabutu/0_tanuki')
path_raccoon = os.listdir('/content/drive/MyDrive/Aidemy_seikabutu/1_raccoon')
path_redpanda = os.listdir('/content/drive/MyDrive/Aidemy_seikabutu/2_redpanda')

img_tanuki = []
img_raccoon = []
img_redpanda = []

for i in range(len(path_tanuki)):
    img = cv2.imread('/content/drive/MyDrive/Aidemy_seikabutu/0_tanuki/' + path_tanuki[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_tanuki.append(img)

for i in range(len(path_raccoon)):
    img = cv2.imread('/content/drive/MyDrive/Aidemy_seikabutu/1_raccoon/' + path_raccoon[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_raccoon.append(img)

for i in range(len(path_raccoon)):
    img = cv2.imread('/content/drive/MyDrive/Aidemy_seikabutu/2_redpanda/' + path_redpanda[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_redpanda.append(img)

X = np.array(img_tanuki + img_raccoon + img_redpanda)
y =  np.array([0]*len(img_tanuki) + [1]*len(img_raccoon) + [2]*len(img_redpanda) )

rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

# データの分割
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]

# 正解ラベルをone-hotの形にします
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
```

###### 転移学習
vgg16による転移学習を行います。

```python:seikabutu.py
# モデルにvggを使います
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

# vggのoutputを受け取り、3クラス分類する層を定義します
# その際中間層を下のようにいくつか入れると精度が上がります
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(3, activation='softmax'))

# vggと、top_modelを連結します
model = Model(vgg16.inputs, top_model(vgg16.output))

# vggの層の重みを変更不能にします
for layer in model.layers[:19]:
    layer.trainable = 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=100, epochs=10, validation_data=(X_test, y_test))

# 画像を一枚受け取り、たぬき、アライグマ、レッサーパンダかを判定する関数
def tanuki(img):
    img = cv2.resize(img, (50, 50))
    pred = np.argmax(model.predict(np.array([img])))
    if pred == 0:
        return 'tanuki'
    elif pred == 1:
        return 'raccoon'
    else:
        return 'redpanda'


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

#--- 学習状況の表示
# 学習の経過を出力
print("Train Accuracy per epoch:", history.history['accuracy'])
print("Validation Accuracy per epoch:", history.history['val_accuracy'])
print("Train Loss per epoch:", history.history['loss'])
print("Validation Loss per epoch:", history.history['val_loss'])


# 学習の経過を出力・プロット
plt.figure(figsize=(12, 4))

# accuracy
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Train Accuracy')
plt.plot(history.history['val_accuracy'], label='Validation Accuracy')
plt.legend()
plt.title('Epoch vs Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')

# loss
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Train Loss')
plt.plot(history.history['val_loss'], label='Validation Loss')
plt.legend()
plt.title('Epoch vs Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')

plt.tight_layout()
plt.show()

#モデルを保存。
model.save("model.h5")
```

#### 精度評価
```
Epoch 1/10
7/7 [==============================] - 8s 394ms/step - loss: 18.6079 - accuracy: 0.6349 - val_loss: 2.6838 - val_accuracy: 0.8758
Epoch 2/10
7/7 [==============================] - 0s 36ms/step - loss: 7.6384 - accuracy: 0.8190 - val_loss: 6.4342 - val_accuracy: 0.8571
Epoch 3/10
7/7 [==============================] - 0s 40ms/step - loss: 7.3724 - accuracy: 0.8534 - val_loss: 3.4127 - val_accuracy: 0.8820
Epoch 4/10
7/7 [==============================] - 0s 35ms/step - loss: 5.3934 - accuracy: 0.8721 - val_loss: 5.7248 - val_accuracy: 0.8571
Epoch 5/10
7/7 [==============================] - 0s 39ms/step - loss: 3.0915 - accuracy: 0.8846 - val_loss: 2.5686 - val_accuracy: 0.8696
Epoch 6/10
7/7 [==============================] - 0s 36ms/step - loss: 2.8033 - accuracy: 0.8752 - val_loss: 1.8669 - val_accuracy: 0.8571
Epoch 7/10
7/7 [==============================] - 0s 39ms/step - loss: 1.3109 - accuracy: 0.8565 - val_loss: 2.1816 - val_accuracy: 0.8758
Epoch 8/10
7/7 [==============================] - 0s 35ms/step - loss: 0.9462 - accuracy: 0.8970 - val_loss: 2.7910 - val_accuracy: 0.8323
Epoch 9/10
7/7 [==============================] - 0s 41ms/step - loss: 1.0220 - accuracy: 0.9189 - val_loss: 1.8187 - val_accuracy: 0.8944
Epoch 10/10
7/7 [==============================] - 0s 38ms/step - loss: 0.5920 - accuracy: 0.9048 - val_loss: 2.5537 - val_accuracy: 0.8509
6/6 [==============================] - 1s 107ms/step - loss: 2.5537 - accuracy: 0.8509
Test loss: 2.5537383556365967
Test accuracy: 0.850931704044342
Train Accuracy per epoch: [0.6349453926086426, 0.8190327882766724, 0.8533541560173035, 0.8720749020576477, 0.8845553994178772, 0.8751950263977051, 0.8564742803573608, 0.8970358967781067, 0.9188767671585083, 0.9048361778259277]
Validation Accuracy per epoch: [0.8757764101028442, 0.8571428656578064, 0.8819875717163086, 0.8571428656578064, 0.8695651888847351, 0.8571428656578064, 0.8757764101028442, 0.8322981595993042, 0.8944099545478821, 0.850931704044342]
Train Loss per epoch: [18.60788917541504, 7.63840389251709, 7.37244987487793, 5.393449783325195, 3.0915095806121826, 2.8033158779144287, 1.3109358549118042, 0.946219265460968, 1.0219534635543823, 0.5919668674468994]
Validation Loss per epoch: [2.683757781982422, 6.434231281280518, 3.412703037261963, 5.724792003631592, 2.5685875415802, 1.8669427633285522, 2.1816020011901855, 2.791016101837158, 1.8187100887298584, 2.5537359714508057]
```

精度はおよそ85%でした。

![スクリーンショット 2024-03-11 21.43.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3738343/269d03fa-5ad3-6115-c281-20ab657c40c6.png)

あまり綺麗に収束していません。
もう少しepochs数を増やして精度を上げたいと思います。

#### epochs=50で検証
![スクリーンショット 2024-03-11 21.48.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3738343/d7eb38f5-7133-004c-0f21-0fcc3f65859d.png)

やはり綺麗に収束しません。
テスト用データの数が少ないからかもしれません。
これ以上epochs数を増やしても大きな変化はありませんでした。

# HTMLとFlaskコードの作成
[Flask入門のためのHTML&CSS 1.2.1 HTMLテンプレートの解説1/5](https://premium.aidemy.jp/courses/1020/exercises/gMnGkIqmgK)
[Flask入門のためのHTML&CSS 1.3.1 CSSテンプレートの解説1/5](https://premium.aidemy.jp/courses/1020/exercises/gMnGkIqmgK)を参考にそれぞれ、HTML、CSSファイルを作成します。
#### HTML
```HTML
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tanukisyu Classifier</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <img class="header_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <a class="header-logo" href="#">tanukish Classifier</a>
    </header>

    <div class="main">    
        <h2> AIが送信された画像がたぬき、アライグマ、レッサーパンダかを識別します</h2>
        <p>画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="submit!" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    <footer>
        <img class="footer_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <small>&copy; 2019 Aidemy, inc.</small>   
    </footer>
</body>
</html>
```

#### CSS
```css
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tanukisyu Classifier</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <img class="header_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <a class="header-logo" href="#">tanukish Classifier</a>
    </header>

    <div class="main">    
        <h2> AIが送信された画像がたぬき、アライグマ、レッサーパンダかを識別します</h2>
        <p>画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="submit!" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
    </div>

    <footer>
        <img class="footer_img" src="https://aidemyexstorage.blob.core.windows.net/aidemycontents/1621500180546399.png" alt="Aidemy">
        <small>&copy; 2019 Aidemy, inc.</small>   
    </footer>
</body>
</html>
```

# アプリの動作確認
#### [Tanukish　Classifier](https://tanikish-app.onrender.com)
![スクリーンショット 2024-03-11 22.19.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3738343/10a6d012-40c3-fe2c-ca1c-d0db8981ef62.png)

無事Webアプリが作成でしました。
試しに以下のたぬき画像をアップロードしてみると
![h2306obihirozoo4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3738343/13ffa455-0973-48ef-1737-29571093a9cd.png)

![スクリーンショット 2024-03-11 22.32.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3738343/a2945fc1-034e-4dde-ab80-e95c7173560e.png)

…アライグマとのことです。
まだまだ多くののテストデータが必要なようです。

# 7. 反省
上記の他にもいろいろな画像で試しましたが、精度はあまりよくありません。
CNNモデルでは精度は90％以上でていたので、この結果は残念です。
たぬきにおいては学習用データの数は多いものの、姿がはっきりと映っていない画像が多いことがネックでした。
今後はスクレイピングを行い、テストデータをもっと増やして検証を行いたいです。