VGG -16を用いた転移学習でゴミ分別アプリを作ってみた

#アプリ概要
Web上で缶と瓶の画像ファイルをアップロードすると缶か瓶かを表示するアプリ。

#開発環境
まず開発環境を整えました。個人的にはやっぱりプログラミングするうえでこれが一番大変（マニュアル通りやってもうまく行くことが少ないため）。
まず行ったこととして、

1. Anacondaをインストール
2. VScodeをインストール
3. VScodeでAnaconda Powershell promptを使えるようにする

1と2はただダウンロードサイトでボタンをポチポチするだけなのでそこまで面倒ではなかったのですが、一番大変だったのは3でした。（必ずしもやる必要はないのですが。。。）

なぜかというと、1と2を終えた後にVSCodeを起動すると、VSCode上にターミナルが現れるんですが、そのターミナルにanaconda関係(conda activate 〇〇)のコマンドを入力しても全く反応してくれませんでした。さすがにAnaconda Powershell promptをいちいち起動させてそこから実行するのは面倒だったので、とりあえずできる限りのことをしようと試行錯誤していました。

わかったことは、
1.VS Codeにはwindows power shellが使われている（当初の私はwindows power shellという単語すら知らなかったです。。。）
2.windows power shellとanacondaは相性が悪い
3.windows power shellでanacondaを使えるようにしたのがAnaconda Powershell promptらしい

つまり、VScodeのターミナルをAnaconda Powershell promptにすれば解決はしそう。その方法を調べたところ、下記のサイトが参考になりました。


https://qiita.com/_meki/items/5b4f06318f1a0986c55c

https://qiita.com/Takuya-Shuto-engineer/items/d233e5686b0508cdf9ab

上のリンクはanaconda promptをVScodeで使えるようにする方法、下のリンクはanaconda power shellをVScodeで使えるようにする方法です。
これでVScodeでanaconda power shellを使えるようになりました。（conda activateもできるようになりました）
これで開発環境は整ったので、いざコーディングへ。

#画像収集
さて、これからは画像をひたすら収集していきます。とりあえずgoogleで缶と瓶で検索した画像をローカルに落としていきます。
BeautifulSoupでパースしてからダウンロードしようとしましたが、なぜかrequestモジュールが使えませんでした。なので、ChromeDriverを
使ってひたすらダウンロード。ChromeDriverを使った画像のダウンロードは以下のサイトが参考になりました。

https://usimaru.net/python-google_images_download-install/

ソースコードは以下です

~~~
from google_images_download import google_images_download

#インスタンスの生成
response = google_images_download.googleimagesdownload()

argument = {"keywords":"瓶",
            "limit":300,
            "chromedriver":"F:\\chromedriver_win32\\chromedriver.exe"}

response.download(argument)
~~~
個々のkewyordsのパラメーターを検索したい文字にすれば大丈夫です。

#画像の水増し
画像を収集したら次は水増しします。水増しのコードは以下の通りです。
画像は左右反転、閾値処理、ぼかし、モザイク、縮小、90度傾ける処理を行いました。処理の組み合わせをすべて行ったので、水増しした画像は一枚につき2×2×2×2×2×2の64枚水増ししました。ダウンロードした画像にはあまり缶や瓶が横たわってる画像がなかったので、90度傾ける処理が重要かなと思いました。

~~~
import os
import glob
import numpy as np
import cv2

#ファイルパスに\が含まれているとエラーが出るので、/に変更する関数
def file_rename(file_list):
    for file in file_list:
        file.replace("\\","/")
        return file_list

def scratch_image(img, flip=False, thr=True, filt=True, tilt=True, resize=True, erode=True):
    # 水増しの手法を配列にまとめる
    methods = [flip, thr, filt, tilt, resize, erode]
        
    # flip は画像の左右反転
    # thr  は閾値処理
    # filt はぼかし
    # resizeはモザイク
    # erode は縮小をするorしないを指定している
    # imgの型はOpenCVのcv2.read()によって読み込まれた画像データの型
    # 水増しした画像データを配列にまとめて返す
    
    # 画像のサイズを習得、ぼかしに使うフィルターの作成
    img_size = img.shape
    filter1 = np.ones((3, 3))
    # オリジナルの画像データを配列に格納
    images = [img]
    #モザイクの比率を設定
    mozaic_ratio = 5
    # ----------------------------ここから書いて下さい----------------------------
    # 手法に用いる関数
    scratch = np.array([
       
        #画像の左右反転
        lambda x: cv2.flip(x,-1),
        
        #閾値処理
        lambda x: cv2.threshold(x,100,255,cv2.THRESH_TRUNC)[1],
        
        #ぼかし
        lambda x: cv2.GaussianBlur(x,(5,5),0),
        
        #90度傾ける
        lambda x: cv2.rotate(x,cv2.ROTATE_90_CLOCKWISE),

        #モザイク処理
        lambda x: cv2.resize(cv2.resize(x,(x.shape[1]//mozaic_ratio,x.shape[0]//mozaic_ratio)),
                            (cv2.resize(x,(x.shape[1]//mozaic_ratio,x.shape[0]//mozaic_ratio)).shape[1]*mozaic_ratio,
                             cv2.resize(x,(x.shape[1]//mozaic_ratio,x.shape[0]//mozaic_ratio)).shape[0]*mozaic_ratio)),
        
        #縮小する
        lambda x: cv2.erode(x,filter1)
        
    ])
    
    # 関数と画像を引数に、加工した画像を元と合わせて水増しする関数
    doubling_images = lambda f, imag: (imag + [f(i) for i in imag])
    
    # doubling_imagesを用いてmethodsがTrueの関数で水増ししてください
    for func in scratch[methods]:
        images = doubling_images(func,images)
    
    return images
    
    #メイン関数
#保存したディレクトリに格納されているファイルのリストを作成
data_path_list = ["F:\\gar_separation\\downloads\\can\\*",
                    "F:\\gar_separation\\downloads\\bottle\\*"]

data_files = glob.glob(data_path_list[2])
file_rename(data_files)
print(data_files)
can = []
bottle = []


#img = cv2.imread(data_files[76])
#print(data_files[76])
#print(img)


for file in data_files:
    img = cv2.imread(file)
    image_list = scratch_image(img)
    #can = can + image_list
    #bottle = bottle + image_list
    


#ひたすら保存していく。
name_list = ["can_data","bottle_data"]
#ディレクトリ作成
for name in name_list:
    os.makedirs("F:\\gar_separation\\"+str(name),exist_ok = True)

#作成したimage_listをひたすら格納していく(ディレクトリ名+番号+jpg)
for image in range(len(can)):
    cv2.imwrite("F:\\gar_separation\\"+name_list[2]+"\\"+str(image)+".jpg",can[image])
#cv2.imwrite("F:\\gar_separation\\"+name_list[0]+"\\0.jpg",can[0])

#image_list = scratch_image(img)
~~~

これで大量に水増しができました。続いて学習です。今回はVGG-16の入力層と出力層を改造します。入力層はサイズが50×50のRGB画像であるため、(50,50,3)とします。出力層は缶と瓶の二種類なので、Dense = 2としました。
#学習モデル作成

~~~
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,Activation
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential,load_model
from tensorflow.keras import optimizers


#画像ファイルを取得
path_bottle_validation_data = os.listdir(r"F:\aidemy_gar_separation\data\bottle\validation_data")
path_can_validation_data = os.listdir(r"F:\aidemy_gar_separation\data\can\validation_data")
path_bottle_test_data = os.listdir(r"F:\aidemy_gar_separation\data\bottle\test_data")
path_can_test_data = os.listdir(r"F:\aidemy_gar_separation\data\can\test_data")





#それぞれラベリング。0→瓶　1→
img_bottle_validation = []
img_can_validation = []
img_bottle_test = []
img_can_test = []



for i in range(len(path_bottle_validation_data)):
    img = cv2.imread(r"F:\aidemy_gar_separation\data\bottle\validation_data\\" + path_bottle_validation_data[i])
    img_bottle_validation.append(img)

for i in range(len(path_can_validation_data)):
    img = cv2.imread(r"F:\aidemy_gar_separation\data\can\validation_data\\" + path_can_validation_data[i])
    img_can_validation.append(img)


for i in range(len(path_bottle_test_data)):
    img = cv2.imread(r"F:\aidemy_gar_separation\data\bottle\test_data\\" + path_bottle_test_data[i])
    img_bottle_test.append(img)

for i in range(len(path_can_test_data)):
    img = cv2.imread(r"F:\aidemy_gar_separation\data\can\test_data\\" + path_can_test_data[i])
    img_can_test.append(img)



X_train = np.array(img_bottle_validation + img_can_validation)
y_train = np.array([0]*len(img_bottle_validation) + [1]*len(img_can_validation))

X_test = np.array(img_bottle_test + img_can_test + img_plastic_bottle_test)
y_test = np.array([0]*len(img_bottle_test) + [1]*len(img_can_test))



rand_index = np.random.permutation(np.arange(len(X_train)))
X_train = X_train[rand_index]
y_train = y_train[rand_index]
rand_index = np.random.permutation(np.arange(len(X_test)))
X_test = X_test[rand_index]
y_test = y_test[rand_index]

y_train = to_categorical(y_train)
y_test = to_categorical(y_test)


input_tensor = Input(shape=(50, 50, 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(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))

model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

for layer in model.layers[:15]:
    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))    

#テストデータの最初の10この表示
for i in range(10):
    cv2.imshow("sample"+str(i),X_test[i])
    print(y_test[i])
    cv2.imwrite("F:/aidemy_gar_separation/data/predict_data/"+str(i)+".jpg",X_test[i])


pred = np.argmax(model.predict(X_test[0:10]),axis = 1)
print(pred)

model.summary()

result_dir = 'F:/aidemy_gar_separation/results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save('model.h5')
~~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/170172e8-ffc2-7119-a382-8487867b34ea.png)

これでモデルのファイル(h5)ができました。最後にFlaskというWEBフレームワークを使いながらwebアプリとして作っていきます。
#WEBアプリ作成
最後にWEBアプリを作成します。ただ、HTMLとCSSの知識が皆無でしたのでなんだかんだ苦労しました。
作成したコードは以下の通りです。まずFlask(サーバー側)は以下の通り

~~~
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:
            return render_template('no_image.html')
        file = request.files['file']
        if file.filename == '':
            return render_template('no_image.html')
        if not allowed_file(file.filename):
            return render_template('invalid_image.html')
        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,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)
~~~    

続いて、HTML(クライアント側)は以下の通り

~~~
<!DOCTYPE html>
<html lang='ja'>    
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content="device-width, initial-scale=1.0">
    <meta http-equiv='X-UA-Compatible' content="ie=edge">
    <title>Earth Cleaner</title>
    <link rel='stylesheet' href="./static/stylesheet.css">
</head>
<body>
    <header>   
        <h1>Earth Cleaner</h1>
    </header>
    {% block body %}{% endblock %}
    <footer>
        <h>&copy; Shumpei Ueda</h>   
    </footer>
</body>
</html>
~~~

~~~
{% extends 'base.html' %}

{% block body%}
<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>
{% endblock %}
~~~

~~~
{% extends 'base.html' %}

{% block body %}
<div class='main'>    
    <h2> 不適切拡張子です</h2>
    <p>戻るボタンを押してください</p>
    <button type=“button” onclick="location.href='/'" style="position: absolute; left: 50%; top: 40%"/>戻る</button>
</div>
{% endblock %}
~~~

~~~
{% extends 'base.html' %}

{% block body %}
<div class='main'>    
    <h2> 画像が入力されておりません</h2>
    <p>戻るボタンを押してください</p>

    <button type=“button” onclick="location.href='/'" style="position: absolute; left: 50%; top: 40%"/>戻る</button>
</div>
{% endblock %}
~~~


~~~
*{
    margin: 0px;
    padding: 0px;   
}

header {
    color: #fff;
    width: 1000px;
    background-color: #2187e7;
    height: 50px;
    margin-right: auto;
    margin-left: auto;
}  

.main {
    height: 550px;
}

body {
    margin-right: auto;
    margin-left: auto;
    width :1000px;
    height:700px;
    background-image: url("background.jpg");
    background-repeat: no-repeat;
    background-position: top;
    background-size:1000px 700px;
  }


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{
    width: 100%;
    height: 30px;
    background-color: #2187e7;
    color: #fff;
    text-align: left;
}


small {
    margin: 15px 25px;
    position: absolute;
    left: 0;
    bottom: 0;
}
~~~
最終的なポートフォリオはこちらになりました。
https://garbage-separation.herokuapp.com/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/9e6d3fe0-8900-c47e-66ee-7906f3f2d3c0.png)
とても長い道のりでしたが、ついに完成いたしました。早速認識してみましょう。
まず瓶の画像
![DSC_0003.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/bf8ce973-de86-d6ec-4c71-216fc173bbf9.jpeg)
読み込むと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/aa117d8f-21cf-ce1e-037a-22500b4cf9c8.png)
瓶と認識してくれました。続いて缶の画像です。![DSC_0002.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/cacc8095-e0b6-3325-4737-0096a1d94ed2.jpeg)
これも読み込むと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1444898/2d0384e0-34b9-dbf3-a2d6-725ffcf9565c.png)
ちゃんと缶と認識してくれました。しっかりと動いてくれてよかったです。


#最後に
今回はAidemyさんの最終成果物としてVGG-16を転移学習して缶と瓶を識別するためのWEBアプリを作成いたしました。
難しかったことは主に
1. 開発環境
2. 機械学習の数学的理解 
3. FLASKおよびHTML


です。私は大学の時にドローンの研究をやっていましたが、その際にプログラミング（C言語）をしていたため、ぶっちゃけプログラミングの部分は特に難しいとは感じませんでした。やっぱり冒頭にも申し上げましたが、一番苦労したのは開発環境です。開発環境は構築するのにすごい手間がかかりました。ただひとついえることは、エラーがあるとそこに成長するチャンスがたくさん存在するってことですね。エラーのおかげで大分パソコンに詳しくなりました（まだまだひよっこですが）具体的には、

-VSCodeとAnacondaをインストールする
-VSCodeでcondaコマンドを検索するが動かない
-Googleで検索
→Qiitaの存在を知った
-jsonファイルなどがおかしいなどと書いてある（実際は違ったが）
→jsonファイルが何なのかを知った
-Powershell とanaconda は相性が悪い
→Powershellというものを知り、それがVSCodeのIDEに使われていることを知った
・・・・・・
こんな感じですね。もしもエラーなくすべてがうまくいっていたらVSCodeとAnacondaの知識しかつかなかったです。

2の数学的理解に関しては、大学の時に最適化数学を勉強したことがあったので、それに通ずるものがあったおかげで、何とか理解することができました。やっぱりパラメーターをポチポチいじるのだけでは面白くないので、背景の数学を理解することは重要だと思います。

それとやはりyoutubeは偉大だと感じました。最近のyoutubeの教材はすごいです。無料ですがとても分かりやすく教えてくれます。僕は今回の講座で一番苦戦したのはFLASKです。まずWebに関しての知識（サーバー、クライアントや、HTTP通信の仕組みなど）が皆無でした。ネットで調べても出るには出るのですがやはり難しいです。ですが、youtubeで検索するととても分かりやすく説明してくれる動画がたくさんありました。中でも一番感動したのは、FLASKを用いてTo-do listを作成する以下の動画です。
https://www.youtube.com/watch?v=jP7p2okKdJA
この動画は何回も見ました。この動画を見るともれなくSQLの知識まで付きます。


今回3か月のプレミアムプランで新しく身についた知識は以下の通りです。自己満足ですがひたすら羅列していきます。

- 開発環境編
    - Anaconda
    - VScode
    - Windows Power Shell
    - Qiitaの存在

- AI知識編
    - CNN
    - VGG-16
    - Inception V3
    - 転移学習
    - スクレイピング
    - データクレンジング
    - OpenCV
    - AInowの存在
    - Deep ageの存在
    - keras
    - tensorflow
    - kaggleの存在

- webアプリ編
    - git
    - flask
    - HTMLおよびCSS
    - heroku
    - SQL
    - GET、POSTmethod
    - bootstrapの存在

- その他
    - progateの存在


3か月でこれだけの知識がついたのは正直自分でも驚きました。やっぱり成果物を作ると経験値が大幅に上がるということを身をもって実感しました。まだまだ知識レベルとしてはひよっこレベルなので、これからもより深く学びたいと思います。

最後に3か月間の間でしたが質問に対して丁寧に教えてくださったAIDEMYのチューターの皆様、本当にありがとうございました!!
これからもよろしくお願いいたします。