Pythonで「飛行機」 or 「車」を自動識別してみよう！

# はじめに
このブログは私がAidemyの「AIアプリ開発コース」を受講し、
その成果物としてCNNを使ったAI画像識別アプリを作成する過程を記録したものです。

まずは私の自己紹介をすると、エンジニア初心者です。IT企業でNon TechのPMをしていますが、Githubの使い方はもちろんCodeをみるだけで拒否反応がでるくらい初心者でした。

今後のキャリア転向として、起業家としてプロダクトをリリースしたいと考えています。エンジニアリングを理解していることで、自分で一人でプロダクトが作れずとも、ソリューションのクリエイティビティを広げることができるだけでなく、適切な開発のロードマップを管理することができると考えています。

したがって、本コースの期待値として、①アプリ開発の基礎概念を学ぶ。②自分の手を動かし、簡単な動くものを作ってみる。③エンジニアにTechがわかるPdMとしてディレクションできるようになる。をゴールとしました。

またサラリーマンをしながらの学習になるため、時間がかなり限られており（おそらくほかの受講者でも多いはず）、本ブログでの成果物は難易度が高いものを避け、講座の内容を振り返る簡易的なものとしました。

##### 成果物
画像識別アプリ：https://aidemy-app3.onrender.com/

# 目次
1. 実行環境

1. フロント開発（画像識別アプリのUI）
    - HTML
    - CSS

1. バックエンド開発（機械学習モデル）
    - Kaggelでの教師データ収集
    - モデルの定義と学習
    - モデルのダウンロード

1. Pythonで実行


1. Renderとデプロイ

1. アプリのテスト

# 実行環境
- VS Code
- Google Cloablatory
- Python (3.7.10)
- コマンドプロンプト

# フロント開発（画像識別アプリのUI）
#### HTML
簡易的な識別アプリをつくるため、「Flask入門のためのHTML&CSS」の講座を参考にします。下記コードです。また成果物のアプリフォルダ[aidemy_app]を作成し、[templates] -> [index.html]に保存します。
```ruby:title.rb

<!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>Number 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="#">Number 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
HTML同様にCSSも「Flask入門のためのHTML&CSS」のコードを参考にする。下記がコードです。成果物フォルダに、[aidemy_app] -> [static] ->[sytlesheet.css]に保存する。

```ruby:title.rb

header {
    background-color: #76B55B;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}

.header-logo {
    color: #fff;
    font-size: 25px;
    margin: 15px 25px;
}

.header_img {
    height: 25px;
    margin: 15px 25px;
}

.main {
    height: 370px;
}

h2 {
    color: #444444;
    margin: 90px 0px;
    text-align: center;
}

p {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

.answer {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #F7F7F7;
    height: 110px;
    margin: -8px;
    position: relative;
}

.footer_img {
    height: 25px;
    margin: 15px 25px;
}

small {
    margin: 15px 25px;
    position: absolute;
    left: 0;
    bottom: 0;
}

```


# バックエンド開発（機械学習モデル）
#### Kaggleでの教師データ収集
今回は教師データをKaggleから収集しました。Kaggleとは、任意の課題に対してのAIの精度コンペで、AIの知識・技術を向上したい人や経験を積みたい人におすすめのコミュニティです。データセットを見たり使用したりすることができるため、今回は作りたいアプリのデータセット（飛行機と車の画像）をKaggleからダウンロードします。

ダウンロードしたデータセットは、容量が多いのでローカルではなく、Google Drive上で管理します。作成したモデルはDriveに作成した「Aidemy成果物」で実行します（%cd /content/drive/MyDrive/Aidemy成果物/）。

#### モデルの定義と学習
モデルの定義と生成は「男女識別」のコースで学習したコードを活用します。下記コードをGoogle ColaboratlyでRunします。

```ruby:title.rb

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_airplanes = os.listdir('./Dataset/airplanes')
path_cars = os.listdir('./Dataset/cars')

img_airplanes = []
img_cars = []

for i in range(len(path_airplanes)):
    img = cv2.imread('./Dataset/airplanes/' + path_airplanes[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_airplanes.append(img)

for i in range(len(path_cars)):
    img = cv2.imread('./Dataset/cars/' + path_cars[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    img_cars.append(img)

X = np.array(img_airplanes + img_cars)
y =  np.array([0]*len(img_airplanes) + [1]*len(img_cars))

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)

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

# vggのoutputを受け取り、2クラス分類する層を定義します
# その際中間層を下のようにいくつか入れると精度が上がります
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(2, 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'])

# 学習を行います
model.fit(X_train, y_train, batch_size=100, epochs=10, validation_data=(X_test, y_test))

# 画像を一枚受け取り、男性か女性かを判定する関数
def gender(img):
    img = cv2.resize(img, (50, 50))
    pred = np.argmax(model.predict(np.array([img])))
    if pred == 0:
        return 'airplanes'
    else:
        return 'cars'

# 精度の評価（適切なモデル名に変えて、コメントアウトを外してください）
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

# pred_gender関数に顔写真を渡して性別を予測します
img = cv2.imread('./Dataset/airplanes/' + path_airplanes[0])
b,g,r = cv2.split(img)
img1 = cv2.merge([r,g,b])
plt.imshow(img1)
plt.show()
print(gender(img))
```


#### モデルのダウンロード
モデルが生成されたら、h5拡張子でダウンロードします。ダウンロードしたファイル[model.h5]は、成果物フォルダ：[aidemy_app]に保存します。
```ruby:title.rb

model.summary()

#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save(os.path.join(result_dir, 'model.h5'))

files.download( '/content/results/model.h5' )

```

# Pythonで実行

フロント開発の項目で用意したHTMLとCSS、またバックエンド開発で用意したモデルをPythonファイルで実行します。コードは下記です。

```ruby:qiita.rb
import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image

import numpy as np

classes = ["飛行機","車"]
image_size = 50

UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

model = load_model('./model.h5')#学習済みモデルをロード


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"

            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")

if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)
```



# Renderとデプロイ
コマンドプロンプトで下記手順でGitにPushします。

①cdコマンドで、対象フォルダの[aidemy_app]に移動します。
```ruby:title.rb

>cd C:\Users\yushirot\aidemy_git\aidemy_app

```

②git add .コマンドで、現在いるフォルダで追跡するように指示します。
```ruby:title.rb

C:\Users\yushirot\aidemy_git\aidemy_app>git add .

```

③gitにコミットします。その際に、共同作業者への通知や忘備録としてどのような内容のリクエストなのかコメントで残しておきます。
```ruby:title.rb

C:\Users\yushirot\aidemy_git\aidemy_app>git commit -m "変更を実装。"

```

④Pushして、Renderにデプロイをします（GithubとRenderがConnectしている前提）。
```ruby:title.rb

C:\Users\yushirot\aidemy_git\aidemy_app>git push -u origin main

```

⑤Renderへのデプロイ
Issueがなければ、下記のようにRenderへのデプロイが完了します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493129/bad6a551-05b9-1fe2-e469-10071f91da99.png)

下記URLが発行され、アクセスすると作成したウェブアプリが開きます。
https://aidemy-app3.onrender.com


# アプリのテスト
アプリが起動したら、実際に画像をアップロードして、「飛行機」か「車」の識別ができているかテストしてみましょう。

下記の飛行機の画像をアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493129/dca2682b-e999-77c5-73a5-236c924374f8.png)


識別できました！


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493129/b9c3b094-508a-86bd-2b68-ef1363060c4e.png)



続いて車の画像をアップロードします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493129/7c378ff8-a8ea-02ef-1cf6-2d3a5337ce17.png)

こちらも識別できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493129/565912fc-3f20-0800-6b77-43eba30612f3.png)


両方ともうまく識別できましたね。これにてアプリ開発完了です。

# おわりに

講座をうけるときは、エンジニアリングについてまったくわかっていない状態でしたが、実際に手を動かしてみることで、プロダクト開発の裏側を知ることができました。自分でスクラッチからコーディングすることはできませんが、自分が作りたいものの先行事例を探し、コードを一部編集したり、カスタマイズしていくことで、アイデアを形にしていくプロセスを学ぶことができました。また開発に興味を持つことができるようになったので、今後もエンジニアリングにアンテナを張って興味関心を広げていけたらと思います。