見出し画像

【Python】月と太陽の画像分析に挑戦

このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。


初めまして。
Aidemy Premiumの学習の一環で、月と太陽の画像を判別するアプリを作成しました。制作したアプリについて解説したいと思います。

今回が初投稿の初心者ですが、がんばって書きますのでよろしくお願いします。


1.作成環境

OS Windows10
言語 Python3, HTML, CSS
ブラウザ Google Chrome
テキストエディタ Google Colabolatory, Visual Studio Code

2.アプリ概要

アプリの機能についてご説明します。
完成したサイトはこちらです。

月と太陽の画像をアップロードしてみます。

画像サイズの関係でサイトが見切れていますが、ご了承ください。
このようにAIが画像を判別して「月」または「太陽」という結果を返してくれます。

3.作成手順

今回は以下の流れで作成しました。

  1. データセットの用意

  2. データの前処理

  3. 教師データとテストデータに分類

  4. 機械学習のモデルを作成

  5. モデルの学習

  6. モデルのテスト

  7. Webアプリケーションの作成

今回はGoogle Colabolatoryを利用して機械学習のモデルを作成しました。
Webアプリケーションを作成する際に必要なHTMLとCSSのコードはVisual Studio Codeで作成しました。


(1)データセットの用意

Kaggleで公開されているデータセットを利用しました。
以下にリンクを掲載します。
データの内訳は、月の画像が1344枚、太陽の画像が850枚です。


(2)データの前処理

ダウンロードしたデータを機械学習に利用しやすい形に加工します。

Google Colabolatory上でデータを扱う場合、データをGoogle Driveにアップロードした後、Google ColabolatoryからGoogle Driveにアクセスできるようにする必要があります。

コードは以下の通りです。

from google.colab import drive
drive.mount('/content/drive')

これを実行するとアカウントの認証を求められます。
アカウントへのアクセスを許可すると

Mounted at /content/drive

という結果が表示されます。画面左のファイルを開くと

/content/drive/MyDrive

の中に、Google Driveにアップロードしたファイルが確認できます。


ダウンロードした画像の大きさを確認すると
縦横ともに200~300ピクセルとなっています。

今回は60×60にリサイズして使用します。
あまり小さくし過ぎると特徴が薄れてしまうことも考えられますが、60×60でも十分特徴を表せているように思えます。

このサイズでモデルの学習もできていました。

リサイズを行うコードは以下の通りです。

#データの整形

import cv2
import glob

#moonの画像データリストを作成
list_moon_picture = glob.glob('/content/drive/MyDrive/moon/*.jpg')

for i,img in enumerate(list_moon_picture):
  original_img = cv2.imread(img)
  resized_img = cv2.resize(original_img, (60,60))
  cv2.imwrite('/content/drive/MyDrive/moon_resize/'+str(i)+'_resize.jpg', resized_img)


#sunの画像データリストを作成
list_sun_picture = glob.glob('/content/drive/MyDrive/sun/*.jpg')

for i,img in enumerate(list_sun_picture):
  original_img = cv2.imread(img)
  resized_img = cv2.resize(original_img, (60,60))
  cv2.imwrite('/content/drive/MyDrive/sun_resize/'+str(i)+'_resize.jpg', resized_img)

globモジュールを利用して、list_moon_pictureにダウンロードした月の画像を読み込みます。
その後、cv2.resizeを利用して画像のサイズを60×60に変更します。
最後にcv2.imwriteを利用して、リサイズした画像をmoon_resizeフォルダに保存します。

太陽の画像でも同様の処理を行います。

リサイズした画像を再度Google Driveに保存することで、次からは直接リサイズした画像を読み込んで進められるようにします。
つまり2回目以降はこの手順をスキップできます。

(3)教師データとテストデータに分類

リサイズしたデータを、モデル学習用の教師データと検証用のテストデータに分類します。

まずはリサイズして保存したデータを読み込みます。
コードは以下の通りです。

#整形済みデータの確認

list_resized_moon_picture = glob.glob('/content/drive/MyDrive/moon_resize/*.jpg')

list_data_moon = []
for i in list_resized_moon_picture:
  img = cv2.imread(i)
  list_data_moon.append(img)

list_resized_sun_picture = glob.glob('/content/drive/MyDrive/sun_resize/*.jpg')

list_data_sun = []
for i in list_resized_sun_picture:
  img = cv2.imread(i)
  list_data_sun.append(img)

globモジュールを利用して画像を読み込んだ後、cv2で読み込み直してからlist_data_moonに格納します。

太陽の画像でも同様の処理を行います。


画像を読み込んだら教師データとテストデータに分類します。
コードは以下の通りです。

#教師データとテストデータに分類
import numpy as np
from tensorflow.keras.utils import to_categorical #one-hotベクトル用
from tensorflow.keras.preprocessing.image import ImageDataGenerator #標準化用

X = np.array(list_data_moon + list_data_sun)
y = np.array([0]*len(list_data_moon) + [1]*len(list_data_sun))

#データのシャッフル
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):]
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)

#教師データの標準化
# ジェネレーターの生成
datagen = ImageDataGenerator(samplewise_center=True, samplewise_std_normalization=True)
#samplewise_center データの平均を0にする。
#samplewise_std_normalization 入力を標準偏差で正規化する。
# 標準化
g = datagen.flow(X_train, y_train, shuffle=False)
X_train, y_train = g.next()

#正解ラベルをone-hotベクトルで求める
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

Xを画像データ、yをラベルデータとして、それぞれに月と太陽のデータを格納します。

次にnumpy.random.permutationでデータをランダムに入れ替えた後、教師データとテストデータが8:2の比率になるようにデータを分割します。

教師データのみ、imageDataGeneratorを利用して標準化します。

最後にラベルデータをone-hotベクトルに変更します。

(4)機械学習のモデルを作成

今回はVGG16モデルを利用した転移学習を行います。
全体のコードは以下の通りです。

from tensorflow.keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization 
#全結合層、過学習予防、平滑化、インプット、バッチごとの標準化
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential #インスタンス
from tensorflow.keras import optimizers #最適化関数

#モデル作成

#モデルの入力画像のオプション
image_size = 60
input_tensor = Input(shape=(image_size,image_size, 3))

#転移学習モデルVGG16
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
#weightsの指定で、imagenetで学習した重みが読み込まれる

#モデルの定義
#転移学習に加えるモデル
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(32, activation='sigmoid'))
top_model.add(Dropout(0.5))
# top_model.add(BatchNormalization())
top_model.add(Dense(2, activation='softmax'))
#sigmoid関数は、0~1の範囲で値を出力する。
#softmax関数は、出力先の合計が1になるようにする。

#vggとtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

#vgg16による特徴抽出部分の重みを15層までに固定
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'])
#損失関数はcategorical_crossentropy
#最適化関数はSGDを指定。
#Ir=学習率
#momentum=確率的勾配降下法。極値に近づけるための計算方法のパラメータ
#評価関数はaccuracy(正解率)

まずinput_tensorでVGG16に入力するデータの型を指定します。
今回は60×60にリサイズしたので、RGBの3色と合わせて(60,60,3)となります。

#モデルの入力画像のオプション
image_size = 60
input_tensor = Input(shape=(image_size,image_size, 3))

VGG16モデルの設定では、include_topをfalseにしてVGGの特徴抽出部分のみを用います。それ以降は自分で作成したモデル(top_model)と結合させます。

#転移学習モデルVGG16
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
#weightsの指定で、imagenetで学習した重みが読み込まれる

top_modelの設定では、まずSequential()でモデルを作成し、その後.add()を利用して層を増やしていきます。

Flattenは入力データを1次元に平坦化しています。

Denseでユニット数32の全結合層を作成します。今回は活性化関数にシグモイド関数を指定しました。また過学習を防ぐために、次の行でDropoutを設定します。

BatchNormalizationは入力データのバッチごとに標準化を行う層ですが、今回はすでに標準化を行っているのでコメントアウトします。

最後に出力層を設定します。今回は2値分類なのでユニット数を2にします。活性化関数にはソフトマックス関数を指定します。

ソフトマックス関数は、出力先の合計が1になるような確率値に変換する関数です。最も確率値の高いものが分類結果となります。

#モデルの定義
#転移学習に加えるモデル
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(32, activation='sigmoid'))
top_model.add(Dropout(0.5))
# top_model.add(BatchNormalization())
top_model.add(Dense(2, activation='softmax'))
#softmax関数は、出力先の合計が1になるようにする。

モデルが作成出来たら、VGG16モデルと連結します。

#vggとtop_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

更新によって、vgg16による特徴抽出部分の重みが崩れてしまうので、以下のようにして固定します。

#vgg16による特徴抽出部分の重みを15層までに固定
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'])
#損失関数はcategorical_crossentropy
#最適化関数はSGDを指定。
#Ir=学習率
#momentum=確率的勾配降下法。極値に近づけるための計算方法のパラメータ
#評価関数はaccuracy(正解率)


(5)モデルの学習

作成したモデルに教師データを学習させます。
コードは以下の通りです。

#モデルの学習
history = model.fit(X_train, y_train, batch_size=64, epochs=10, 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]) #正解率

バッチサイズは64、エポック数は10に設定しました。
実行結果は以下の通りです。

最後の2行でテストデータの損失関数と正解率の値が表示されています。
損失関数は約0.12、正解率は約0.96となっています。

モデルの学習過程の可視化も行います。
コードは以下の通りです。

# 学習の経過を出力
import matplotlib.pyplot as plt

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()

実行すると学習過程を表したグラフが表示されます。

青い線が教師データ、オレンジ色の線がテストデータです。

(6)モデルのテスト

モデルに画像を送信して、月と太陽が正しく判別できているかを確認します。
コードは以下の通りです。

# 画像を一枚受け取り、月と太陽を判別する。
def fun(img):
    img = cv2.resize(img, (60, 60))
    pred = np.argmax(model.predict(np.array([img])))
    if pred == 0:
        return 'moon'
    elif pred == 1:
        return 'sun'

img = cv2.imread('/content/drive/MyDrive/sun_resize/500_resize.jpg')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print(fun(img))
plt.imshow(img)
plt.show()

画像を判別する関数fun(img)を定義し、その返り値が'moon' 'sun'となるようにします。

cv2で画像を読み込むとb,g,rの順になります。
matplotlibで正しく表示できるようにr,g,bの順番に変更しておきます。

ためしに何枚か画像を読み込んでみます。

月と太陽の画像が正しく判別されました。

(7)Webアプリケーションの作成

画像を送信して判別する過程をwebアプリケーション上で実行できるようにします。

今回は機械学習モデルの開発がメインですので、HTMLとCSSのコードはテンプレートをお借りします。

まずは機械学習のモデルをダウンロードします。
コードは以下の通りです。

import os
from google.colab import files

#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' )

実行するとmodel.h5ファイルがダウンロードされます。

次にWebアプリ用のフォルダ(名前は何でも良い)を作成します。
中身の構成は以下の通りです。

model.h5seikabutu.py
│
├─staticstylesheet.css
│
├─templatesindex.html
│
└─Uploads

seikabutu.pyの中身は以下の通りです。

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 = 60
UPLOAD_FOLDER = "static"
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, 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, images=filepath)

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

if __name__ == "__main__":
    app.run()

次にstylesheet.cssです。

body{
    background:#f7f7f7;
    -webkit-animation:colour 15s linear infinite;
    -moz-animation:colour 15s linear infinite;
    
}

@-webkit-keyframes colour{
    0%{
      background:#f8f8f8
    }
  
    40%{
      background:#c3f3b0
    }
  
    80%{
      background:#fafafa
    }
  
    100%{
      background:#e7f8aa
    }
  
}
  

header {
    background-color: #fff;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}
   
.header-logo {
    color: #000;
    font-size: 25px;
    margin: 15px 25px;
    text-decoration: none;
}
   
.main {
    min-height: 100vh;
    position: relative;
    padding-bottom: 50px;
    box-sizing: border-box;
}
   
h2 {
    color: #444444;
    margin: 80px 0px 30px;
    text-align: center;
}
   
p {
    color: #444444;
    margin: 40px 0px 30px 0px;
    text-align: center;
}
   
.answer {
    color: #444444;
    margin: 5px 0px 30px 0px;
    text-align: center;
    font-weight: bold;
    font-size: 18px;

}

.image {
    width: 30%;
    display: block;
    margin: 30px auto 0px;
}

form {
    text-align: center;
}

footer {
    background-color: #c0cac4;
    margin: -8px;
    width: 100%;
    position: absolute;
   }
   
   small {
    margin: 15px 25px;
    left: 0;
    bottom: 0;
   }

最後にindex.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>Moon and Sun Classifier</title>
    <link rel="stylesheet" href="./static/stylesheet.css">
    
</head>
<div id="home">
<body>
    <header>
        <a class="header-logo" href="#">太陽と月の判別</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>

        <img class="image" src="{{images}}">
        <div class="answer">{{answer}}</div>
    </div>
</div>
</body>
</html>

この状態でpythonファイル(seikabutu.py)を実行すると、アプリ概要でお見せしたサイトが立ち上がります。

実行する際の注意ですが、pythonファイルまでのファイルパスに日本語が含まれていると、UnicodeDecodeErrorが発生して実行できない事象が確認されました。
ファイルパスが半角英数字のみの場所で実行するようにしましょう。

4.結果と考察

(1)エポック数

モデルの学習でお見せした画像をもう一度掲載します。

テストデータでの正解率はおよそ0.96と、そこそこ高い値になっていることがわかります。

実際エポック数ごとの学習過程を見てみると、4を終えた時点でテストデータの正解率は0.9を超えています。

エポック数を増やし過ぎると過学習が起こり、正解率が低下してしまうのでこれ以上増やさなくても良いと思います。

(2)中間層

今回、学習モデルにVGG16の転移学習を利用しました。自分で作成したモデルは中間層がユニット数32の全結合層のみとなっていますが、中間層を増やした場合についても検証してみました。

ますはユニット数64の全結合層を加えてみます。
コードは以下の通りです。

#モデルの定義
#転移学習に加えるモデル
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(64, activation='sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(32, activation='sigmoid'))
top_model.add(Dropout(0.5))
# top_model.add(BatchNormalization())
top_model.add(Dense(2, activation='softmax'))
#sigmoid関数は、0~1の範囲で値を出力する。
#softmax関数は、出力先の合計が1になるようにする。

続いて学習過程です。

最終的な値は、損失関数が約0.39、正解率は約0.84となっています。
しかしテストデータの正解率はエポック数4のあたりでピークを迎えており、それ以降は減少し続けています。
つまりエポック数4以降では過学習が起きていると考えられます。

さらに中間層を加えてみます。
今度はユニット数256の全結合層です。
コードは以下の通りです。

#モデルの定義
#転移学習に加えるモデル
top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation="sigmoid"))
top_model.add(Dropout(0.5))
top_model.add(Dense(64, activation='sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(32, activation='sigmoid'))
top_model.add(Dropout(0.5))
# top_model.add(BatchNormalization())
top_model.add(Dense(2, activation='softmax'))
#sigmoid関数は、0~1の範囲で値を出力する。
#softmax関数は、出力先の合計が1になるようにする。

続いて学習過程です。

最終的な値は、損失関数が約0.63、正解率は約0.64となっています。
一般的に中間層の数を増やすと、分析の柔軟性や結果は向上すると言われていますが、層を追加しない場合と比べて結果は下がってしまいました。

今のところ原因がわからないので、その解明は今後の課題となりそうです。

損失関数と正解率を考慮し、中間層はユニット数32の全結合層のみが良いという結論になりました。

(3)画像判別ミスの原因

前述したように、テストデータの正解率は0.96と高い値をマークしています。
しかし作成したアプリに画像を送信したところ、結果が間違っているケースが確認できました。

このように太陽の画像を送信しても、結果が月になっています。

原因として考えられるのは、データの偏りです。
kaggleからダウンロードしたデータの一部をお見せします。

月の画像は単体で映っているもの、つまり日本国旗のような見た目がほとんどです。
一方で太陽の画像は単体で映っているものだけではなく、景色の一部として他の物体が映りこんでいるものがあります。

判別結果が間違っていた画像は、太陽が単体で映っています。
おそらくですが、日本国旗のような見た目は月の特徴だと判断されてしまった可能性があります。

この判別ミスを回避する方法として、月の画像にも景色の一部として他の物体が映りこんでいるデータを用意することが考えられます。

(4)今後の展望

今後の展望として、以下の項目が挙げられます。

  • 画像データの偏りの修正

  • 適切なパラメータの設定

  • 転移学習のモデルはVGG16が最適なのか

  • 多クラス分類

画像データの偏りについては、前述の通りです。

モデルのパラメータについては、今回検証できなかったものがいくつかあります。例えば画像サイズやドロップアウトです。
各パラメータの適切な値を研究し、より高い精度を実現したいです。

これは転移学習モデルのVGG16も同様で、それ以外の転移学習モデルについても検証してみたいです。

最後に多クラス分類についてですが、元々今回のモデル開発は太陽系惑星の画像分析を目的としていました。
しかし時間などの都合から断念し、月と太陽のみに絞った二値分類モデルを作成しました。
機会があれば多クラス分類や、その他天体以外の画像分析にも挑戦してみたいです。

5.まとめ

Aidemy Premiumでは、Pythonの基本的な使い方から機械学習の体系的な知識まで幅広い内容を学習できました。
最終的には簡単なものですがモデルを作成することもできました。
独学ではここまでの内容を学習することができなかったと思うので、大変感謝しております。

この記事が気に入ったらサポートをしてみませんか?