Aidemy Premium Plan(AIアプリ開発コース3か月)の振り返り
はじめに
現在、私はテストエンジニアをしておりますが、プログラミングに関しては初学者です。
そんな私が、Aidemy Premium Plan(AIアプリ開発コース3か月)でどのくらいできるようになったのかを振り返りたいと思います。
※ブログ、プログラム共に初めてなので内容について読みにくい箇所や誤りの部分(特にコード)も多々あるかと思いますが、その点はどうかご了承いただければ幸いです。
成果物の概要
女性限定ではありますが、4タイプに顔を分類するアプリを作成しました。よく巷でも見掛けるアプリですが、Aidemyで習った技術を表現するのには、うってつけのアプリだと思っております。
※4タイプ:[Cute] / [Elegant] / [Cool]/ [Feminine]
実行環境
python 3.9.7
anaconda 2021.11
numpy 1.20.3
tensorflow 2.8.0
keras 2.8.0
OpenCV 4.5.5
flask 1.1.2
pillow 9.0.1
データセット作成詳細
画像収集
まず、女性の顔を4タイプに分類するための写真を収集する必要があったので、スクレイピングをするのですが、私は、icrawlerを使用して画像を収集いたしました。
icrawlerとはウェブクローラのライブラリで、容易に画像や動画をスクレイピングできるものです。外部ライブラリなので、以下の様にpipでインストールしておく必要があります。
pip install icrawler
画像収集プログラム
以下のプログラムで私の場合は4タイプの女性の顔を、各タイプ200枚収集するようにプログラムを組みました。
私が4タイプの検索ワードを選定する際、こちらのサイトに掲載されている芸能人の方々を参考にし、その方の名前を検索ワードにしました。
※収集した画像については、肖像権なども考慮して記載は控えさせていただきますこと、ご了承いただけますと幸いです。
from icrawler.builtin import BingImageCrawler
crawler = BingImageCrawler(storage={"root_dir": './保存先フォルダ名'})
crawler.crawl(keyword='検索ワード(日本語もOK)', max_num=200) #max_num=上限1000 まで可能
顔の部分のみトリミングして保存する方法
次に取得したデータのうち必要なデータは顔のみなので、顔の部分のみ切り取る処理をいたしました。
import cv2
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import matplotlib.pyplot as plot
import os
# 入力ファイルのパスの指定
in_jpg = "xxxx" # 画像保存先フォルダ名
out_jpg = "yyyy" # 顔部分をトリミングした後の画像保存先フォルダ名
if not os.path.isdir(out_jpg):
# 顔部分をトリミングした後の画像保存先フォルダが存在しない場合、保存先フォルダを作成する
os.makedirs(out_jpg)
#保存している画像データを取得する関数
def get_file(dir_path):
filenames = os.listdir(dir_path)
return filenames
pic = get_file(in_jpg)
for i in pic:
# 画像の読み込み
image_gs = cv2.imread(in_jpg + '/' + i)
# 顔認識用特徴量ファイルを読み込む --- (カスケードファイルのパスを指定)
cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
# 顔認識の実行
face_list = cascade.detectMultiScale(image_gs,scaleFactor=1.1,minNeighbors=1,minSize=(100,100))
# 顔だけ切り出して保存
no = 0
for rect in face_list:
x = rect[0]
y = rect[1]
width = rect[2]
height = rect[3]
dst = image_gs[y:y + height, x:x + width]
save_path = out_jpg + '/' + 'out_(' + str(i) +')' + str(no) + '.jpg'
# 認識結果の保存
a = cv2.imwrite(save_path, dst)
no += 1
プログラム実行後、しっかりと顔部分のトリミングができたのですが、その一方で、顔でない部分もいくつか抽出されてきました。
この辺りの画像は、すごく手間でしたが手動で削除いたしました。
データの水増しと整形並びに学習データの作成
from PIL import Image
import os, glob
import numpy as np
from PIL import ImageFile
# IOError: image file is truncated (0 bytes not processed)回避のため
ImageFile.LOAD_TRUNCATED_IMAGES = True
# indexを教師ラベルとして割り当てるため、
# 0にはCuteを指定し、1にはElegantを指定
# 2にはCoolを指定し、3にはFeminineを指定
classes = ["Cute", "Elegant", "Cool", "Feminine"]
num_classes = len(classes)
image_size = 64
X_train = []
X_test = []
y_train = []
y_test = []
※データ水増し & 訓練データ80% / テストデータ20% に分割
for index, classlabel in enumerate(classes):
photos_dir = "./" + classlabel
files = glob.glob(photos_dir + "/*.jpg")
files1 = len(files)*0.2
for i, file in enumerate(files):
image = Image.open(file)
image = image.convert("RGB")
image = image.resize((image_size, image_size))
if i < files1:
# angleに代入される値
# -30, -25, -20 ... 20, 25, 30 と画像を5度ずつ回転
for angle in range(-30, 30, 5):
img_r = image.rotate(angle)
data = np.asarray(img_r)
X_test.append(data)
y_test.append(index) # indexを教師ラベルとして割り当てるため、0にはCuteを、1にはElegantを、2にはCoolを、3にはFeminineを指定
img_trains = img_r.transpose(Image.FLIP_LEFT_RIGHT) # FLIP_LEFT_RIGHT は 左右反転
data = np.asarray(img_trains)
X_test.append(data)
y_test.append(index) # indexを教師ラベルとして割り当てるため、0にはCuteを、1にはElegantを、2にはCoolを、3にはFeminineを指定
else:
# angleに代入される値
# -30, -25, -20 ... 20, 25, 30 と画像を5度ずつ回転
for angle in range(-30, 30, 5):
img_r = image.rotate(angle)
data = np.asarray(img_r)
X_train.append(data)
y_train.append(index) # indexを教師ラベルとして割り当てるため、0にはCuteを、1にはElegantを、2にはCoolを、3にはFeminineを指定
img_trains = img_r.transpose(Image.FLIP_LEFT_RIGHT) # FLIP_LEFT_RIGHT は 左右反転
data = np.asarray(img_trains)
X_train.append(data)
y_train.append(index) # indexを教師ラベルとして割り当てるため、0にはCuteを、1にはElegantを、2にはCoolを、3にはFeminineを指定
X_train = np.array(X_train)
X_test = np.array(X_test)
y_train = np.array(y_train)
y_test = np.array(y_test)
np.savez(r'C:\Users\xxxx\Desktop\作業フォルダ\保存したときのファイル名', X_train, X_test, y_train, y_test)
画像データの水増しは、上記コードの通り、Pillow (PIL)の image.rotateで画像回転とimage.transposeで左右反転をしております。
image.rotateは、整数指定で反時計回り、負の数指定で時計回りの角度で回転します。
image.transpose(Image.FLIP_LEFT_RIGHT)は、画像を左右反転いたします。
データの整形では、画像のサイズを64x64にリサイズしております。
また、訓練データとテストデータは8:2で分割するようにしていて、ラベルについてはone-hotベクトル形式に変換しています。
そして最後に、np.savez()によって指定したパス文字列(保存したときのファイル名)に拡張子.npzが付与されたファイル名で保存しております。
※画像データは、この時で10248枚に増えました。
訓練用画像データ=8160枚 / テスト用画像データ=2088枚
学習フェーズ
ここではAIを作るためのメインのプログラムになります。
このプログラムを実行すると、機械学習を行います。
今回のモデルでは、全結合層は変更した上で、VGG16(*1)の転移学習を用いております。VGG16で抽出した特徴量については、重みが更新されると崩れてしまうので、私は今回15層までに固定しています。
以下のコードは、モデルを学習させた後、作成したモデルの重みを保存、モデルの評価を行い、最後にサマリを出力しています。
(*1)VGGモデルには、重みがある層(畳み込み層と全結合層)を16層重ねたものと19層重ねたものがあり、それぞれVGG16やVGG19と呼ばれます。VGG16は、畳み込み13層+全結合層3層=16層のニューラルネットワークになっています。
モデルの作成・学習・評価
from tensorflow.keras import optimizers
from keras.applications.vgg16 import VGG16
from keras.layers import Dense, Dropout, Flatten, Input
from keras.models import Model, Sequential
from keras.utils.np_utils import to_categorical
import numpy as np
npz = np.load("npzファイルを指定")
X_train = npz['arr_0'] #訓練用画像データ
X_test = npz['arr_1'] #テスト用の画像データ
y_train = npz['arr_2'] #訓練用のラベルデータ
y_test = npz['arr_3'] #テスト用のラベルデータ
# 入力データの各画素値を0-1の範囲で正規化(学習コストを下げるため)
X_train = X_train.astype("float") / 255
X_test = X_test.astype("float") / 255
# to_categorical()にてラベルをone hot vector化
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
# ImageNetで事前学習した重みも読み込まれます
input_tensor = Input(shape=(64,64,3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
#新しく他の層を追加するために、あらかじめVGGとは別のモデル(ここではtop_model)を定義し、以下のようにして結合
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(128, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(4, activation='softmax'))
model=Model(inputs=vgg16.input,outputs=top_model(vgg16.output))
# modelの15層目までがvggのモデル
for layer in model.layers[:15]:
layer.trainable = False
# loss = 損失関数 / optimizer = 最適化アルゴリズム / metrics = 評価関数
model.compile(loss='categorical_crossentropy', optimizer=optimizers.SGD(lr=1e-4, momentum=0.9), metrics=['accuracy'])
# モデルに学習させる
model.fit(X_train, y_train, validation_data=(X_test, y_test), batch_size=32, epochs=15)
# 作成したモデルの重みを保存
model.save('保存するファイル名.h5')
# モデルを評価する
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])
model.summary()
精度の出力結果は以下の通りです。
Epoch 11以降からval_lossが上昇してきているため、過学習を起こしている可能性がありましたが、ここまで試行錯誤した結果、一番良い結果だったためこのモデルを使用しました。
※一番良いといっても、精度はよくないです。。。
WEBアプリの作成
サーバーサイド側の制作
Flaskを利用して実装。完成後Herokuにデプロイしています。※Herokuから以下のアナウンスがあり、私がデプロイしたアプリもこれにより終了いたしました。
【アナウンス内容】
2022 年 11 月 28 日以降、無料の製品プランの提供を停止し、無料の dyno とデータ サービスのシャットダウンを開始する予定です。
import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import load_model, Sequential
from keras.models import Model
from tensorflow.keras.preprocessing import image
#import tensorflow.compat.v1 as tf
import numpy as np
classes = ['Cute', 'Feminine', 'Cool', 'Elegant']
image_size = 64
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('./保存するファイル名.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__":
port = int(os.environ.get('PORT', 8080))
app.run(host ='0.0.0.0',port = port)
クライアントサイド側の制作
HTML(CSS)で書かれたプログラムを動かすことで、Webブラウザにページを表示をするようにします。アプリの見た目部分を作成することです。
HTMLとCSSは以下の通りです。
<!--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>FACE DENTIFICATION PROGRAM</title>
<link rel="stylesheet" href="/static/stylesheet.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Hachi+Maru+Pop&display=swap" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Charm:wght@700&display=swap" rel="stylesheet">
</head>
<body>
<header>
<a class="header-logo" href="#">FACE DENTIFICATION PROGRAM</a>
</header>
<div class="main">
<h3> AIがあなたの顔をキュート、エレガント、フェミニン、クールの4種類の系統から判別します<br>女性専用の判別機となっております</h3>
<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>
</body>
</html>
/*CSS*/
header {
background-color: #d618f4;
height: 60px;
margin: -8px;
display: flex;
justify-content: space-between;
}
.header-logo {
color: rgb(0, 0, 0);
font-size: 25px;
font-weight: bolder;
margin: 15px 25px;
font-family: 'Charm', cursive;
}
.main {
background-image:url(./cute.jpg),url(./cool.jpg),url(./elegant.jpg),url(./feminine.jpg);
background-repeat: no-repeat,no-repeat,no-repeat,no-repeat;
height: 535px;
background-size: 24%, 24%, 24%, 24%;
background-position: top left,bottom right,bottom left, top right;
}
h3 {
color: #ea2ad3;
margin: 90px 0px;
text-align: center;
font-size: 23px;
font-family: 'Hachi Maru Pop', cursive;
}
h2 {
color: #ea2ad3;
margin: 90px 0px;
text-align: center;
font-family: 'Hachi Maru Pop', cursive;
}
p {
color: #ea2ad3;
margin: 70px 0px 30px 0px;
text-align: center;
font-size: 18px;
font-weight: bold;
font-family: 'Hachi Maru Pop', cursive;
}
.answer {
color: #ea2ad3;
margin: 70px 0px 30px 0px;
text-align: center;
font-family: 'Hachi Maru Pop', cursive;
}
form {
text-align: center;
}
footer {
background-color: #F7F7F7;
height: 50px;
margin: -8px;
position: relative;
}
自作アプリ公開
以下が私が作成したアプリになります。Herokuにデプロイしました。※Herokuから以下のアナウンスがあり、私のアプリもこれにより終了いたしました。以下のURLはすでに使用不可になっております。
※これまでに何度も記載しておりますが、決して精度は良くないので、その点はご了承ください。
【アナウンス内容】
2022 年 11 月 28 日以降、無料の製品プランの提供を停止し、無料の dyno とデータ サービスのシャットダウンを開始する予定です。
考察と振り返り
上記「成果物の概要」でも記載していますが、わたしは、4タイプに顔を分類するアプリを作成しましたが、精度があまり上がらず、アプリが完成するまで4週間ぐらい掛かってしまいました。
反省としては、4タイプだからと、女性4人分のデータしか収集していない点だと感じております。
各タイプ複数人のデータを収集した学習モデルにしていれば、もう少し精度が上がるのではないかと、あくまで初心者の推測ですが、この点は今後実装していきたいと思っております。
最後に
AIエンジニアに興味を持ち、数あるAIスクールの中からAidemyを選択したことは私にとって正解でした。
全くプログラムについて知識もない状態でしたが、挫折することなく何とか、カリキュラムをこなすことが出来ました。
ひとえにAidemy講師の方々のご支援のおかげです。
ありがとうございました。
わからないことだらけでしたが、SlackでAidemy講師の方々に、質問が無制限にできるというサービスが私にとってすごく重宝しました。
しかも、レスポンスがその日の内にあるのもすごく助かりました。
そのおかげもあり、多い時では一日に何回も質問をしましたが、その日の内に疑問点を解決することが出来ました。
※回答の内容も本当にわかりやすく丁寧です。
何より、このカリキュラムを独学でしていた場合、100%挫折していたと思います。
3カ月前はプログラムのことなんて、右も左もわからなかった私が、調べながらですが、アプリを実装できるまでになるなんて、すごく自信にもなりましたし、これからも学んでいきたいと思います。
改めて、Aidemy講師の方々には、重ねてお礼申し上げます。