[機械学習]人物の画像から、その人の世代を予測してみる！

# はじめに
はじめまして。

学生時代にC言語を学び、仕事ではオブジェクト指向の言語でアプリを作成していたのですが、新しい知識や技術を知らんといけんかなーと常々思っていました。
最近よく耳にするようになった「AI」とはなんぞやということで一念発起。
オンラインスクール[「Aidemy」](https://aidemy.net/)の「AIアプリ開発講座」に通い始めました。

このブログはAidemy Premiumの講座カリキュラムの一環である、受講修了条件を満たすために作成した成果物について記載しています。

## 目次
[- `何の学習モデルを作成するか`](#何の学習モデルを作成するか)
[- `作成環境`](#作成環境)
[- `学習データの準備`](#学習データの準備)
[- `機械学習モデルの作成webアプリの作成`](#機械学習モデルの作成webアプリの作成)
　[-`1. GoogleCoraboratoryにて学習モデルの作成`](#1googlecoraboratoryにて学習モデルの作成)
　　-[`1.1 学習データのダウンロード`](#11-学習データのダウンロード)
　　-[`1.2 顔画像の検出用モジュールのダウンロード`](#12顔画像の検出用モジュールのダウンロード)
　　-[`1.3 学習モデルの生成`](#13-学習モデルの生成)
　　　-[`1.3.1 学習データの加工`](#131-学習データの加工)
　　　-[`1.3.2 学習`](#132学習)
　　-[`1.4 考察`](#14-考察)
　[-`2. webアプリの作成`](#2webアプリの作成)
[- `さいごに`](#さいごに)

## 何の学習モデルを作成するか
AidemyのAIアプリ開発講座では学習成果として、機械学習のモデルを組み込んだWebアプリを作成します（本ブログの作成も成果物の一環です）

折角なので何の機械学習をしたモデルを作ろうか…
…
…………
悩むこと一晩。そういえば、コンビニのレジでお客さんの世代や性別を入力していたよな、ということを思い出し、**人物画像から各世代**が特定出来たら有用かなと思い、それを作ってみようと決めました。

# 作成環境
+ PC：WindowsPC
+ Webブラウザ：Google Chrome
+ 学習モデル作成：Google Coraboratory
+ 言語：Python

## 学習データの準備
機械学習の方法の一つとして、ある目的をもったモデルを作成するために、
解答のあるデータを大量かつ何度も読み込ませる必要があります。

今回、人の画像から年代を特定するためには、
　**人の画像(データ） + 年齢（解答）**
のセットが必要となります。

スクレイピングで人の画像を収集しようと考えたのですが、
- 収集した人物の生年月日と撮影日付が分からないと撮影時点の年齢が分からない。
- 世代をキーワードにして検索で収集してくる画像が本当にその世代か分からない。

という点を考慮して、断念しました。

そこであらかじめ用意されているデータセットはないかと検索していたところ、
[IMDB-WIKIデータセット](https://data.vision.ee.ethz.ch/cvl/rrothe/imdb-wiki/)のサイトを見つけました。

このサイト、NVIDIA ChaLearn LAP 2015 最優秀論文賞を受賞された方々の論文で、ありがたいことに使用されたデータのダウンロードができる模様です。

初めての機械学習、先輩の胸を借りようと、利用させていただくことにしました。

## 機械学習モデルの作成・Webアプリの作成
使用するデータが決定できましたので、Webアプリの作成をしていきます。
このような流れで作成しました。

  1.　GoogleColaboratoryにて学習モデルの作成・ダウンロード
  2.　ローカル環境でWebアプリを作成、Flaskを用いてWebアプリ確認

## 1.　GoogleCoraboratoryにて学習モデルの作成
GoogleCoraboratoryはGoogleドライブにある一つの機能で、Pythonの実行環境があります。ここで学習モデルの作成を行います。

※GoogleCoraboratoryの使用にはGoogleアカウントの取得が必要です。

# 1.1 学習データのダウンロード
IMDB-WIKIデータセットをGoogleCoraboratoryに展開します。
GoogleCoraboratory上で次のコードを実行します。
```python
import urllib.request
import tarfile
import os

# URLを指定
url = "https://data.vision.ee.ethz.ch/cvl/rrothe/imdb-wiki/static/wiki_crop.tar"
#ファイル名を取得
save_name = url.split('/')[-1]

#ファイル名確認
print(save_name)

# ダウンロードする
mem = urllib.request.urlopen(url).read()

# ファイルへ保存
with open(save_name, mode='wb') as f:
    f.write(mem)

#tarファイルを展開
with tarfile.open('wiki_crop.tar', 'r') as tar:
    tar.extractall(path='.')
```

Google Coraboratory上にIMDB-WIKIデータセットが展開されます。


# 1.2　顔画像の検出用モジュールのダウンロード
IMDB-WIKIの人物画像には、服装や背景など顔以外の部分が含まれています。
世代の推測には服装や髪形なども考慮に入れるべきでしょうが、撮影された年代や国によっては流行りや文化などが異なることから、今回、人物画像から顔を抽出して学習モデルを生成しようと考えました。

画像から顔を検出するには、OpenCV (Open Source Computer Vision Library) を使用しました。

次のコードを同様に実行して、OpenCVの使用準備を行います。
```python
import urllib.request
import zipfile
import os


# URLを指定
url = "https://github.com/opencv/opencv/archive/4.8.0.zip"
#ファイル名を取得
save_name = url.split('/')[-1]

#ファイル名確認
print(save_name)

# ダウンロードする
mem = urllib.request.urlopen(url).read()

# ファイルへ保存
with open(save_name, mode='wb') as f:
    f.write(mem)

#zipファイルを展開
with zipfile.ZipFile(save_name,'r') as inputFile:
    inputFile.extractall()
```


# 1.3 学習モデルの生成
IMDB-WIKIデータセットより学習モデルを生成していきます。


なお、IMDB-WIKIに準備されている画像データについて、世代別に確認したところ、次のような分布となっていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/6a8bcd35-25b2-6ead-0590-b3878d149809.png)
10代未満と80代以上の方の画像データが極端に少なかったため除外させていただきました。
**学習モデルは10代から70代の7クラス分類**としました。

また、女性ではお化粧されていたり、男性では髭を生やしていたりすることを考慮した時、女性と男性の人物画像をまとめて学習させることは、特徴のノイズになるかもと思い、今回は**男性の画像を対象**としました。

※男性女性に限らず、外見のスタイルに関して、個人の考え方を否定するものではありません。御了承ください。

# 1.3.1 学習データの加工
IMDB-WIKIのデータを学習用に加工していきます。

今回学習に際し、IMDB-WIKIの構造から次のデータを対象外としました。
+ face_scoreと呼ばれる顔の検出を表す数値が3未満またはinf（画像に顔がない）ものは除外 → はっきり顔がわかるものを使用
+ second_face_scoreと呼ばれる、画像内に複数の顔がある場合（NaN以外）は除外
+ 顔抽出を行うため、画像サイズが256*256未満は除外


これらを満たす条件で、10代から70代のデータを、それぞれ100人分収集します。

また、各データの年齢の算出には、photo_takenと呼ばれる写真が撮影された年（月と日は不明）がわかりますので、撮影日付を年の真ん中と仮定して、dobと呼ばれる生年月日より算出するようにしました。

以下、コードを記載していきます。

```python
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.io import loadmat
from datetime import datetime
from tensorflow.keras.utils import to_categorical
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
import collections
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import EarlyStopping

#----------------------------------------------
#写真撮影年(taken)から生年月日(dob)を引き、年齢を算出したのち、世代を算出
def calc_generation(taken, dob):
    #dob（成年月日）は、Matlab serial date numberにフォーマットされた値
    #python日付を取り扱うには次の処理
    #python_datetime = datetime.fromordinal(matlab_serial_date_numer - 366)

    birth = datetime.fromordinal(max(int(dob) - 366, 1))

    # 写真撮影日付は年しかないため、年齢算出には、撮影日付を年の真ん中（月単位）として仮定する
    if birth.month < 7:
        age_year = taken - birth.year
    else:
        age_year = taken - birth.year - 1

    #世代を算出(10クラス分類するため、10代なら1、20代なら2…とする)
    generation = (age_year // 10)

    #10歳未満は件数が少なすぎるため今回は対象としない
    if generation < 1:
      return 0

    #80歳以上は今回対象としない
    if generation >= 8:
      return 0

    return generation
#----------------------------------------------

#展開したwiki_cropフォルダ内のフォルダ一覧を取得
#※お使いの仮想環境のディレクトリ構造等によってファイルパスは異なります。
wiki_path = './wiki_crop/'

#メタ情報を格納したmatlab ファイルの読み込み
meta = loadmat("./wiki_crop/wiki.mat")

#各メタ情報を個別に格納
full_path = meta["wiki"][0, 0]["full_path"][0] #画像のパス
dob = meta["wiki"][0, 0]["dob"][0]  # Matlab serial date number　成年月日
gender = meta["wiki"][0, 0]["gender"][0] #性別 1:男性, 0:女性
photo_taken = meta["wiki"][0, 0]["photo_taken"][0] # 撮影年
face_score = meta["wiki"][0, 0]["face_score"][0] #検出器スコア (高いほど良い)。 Inf は、画像内に顔が見つからず
second_face_score = meta["wiki"][0, 0]["second_face_score"][0] # 2 番目に高いスコアを持つ顔の検出スコア
#複数の顔が含まれる画像を無視する場合に便利です。 2 番目の顔が検出されなかった場合、second_face_score は NaN
#age = [calc_age(photo_taken[i], dob[i]) for i in range(len(dob))]
name = meta["wiki"][0, 0]["name"][0]

#対象とするデータのIndexを検出器スコアを元に収集
DataIndexList = []
for i in range(len(face_score)):
  #顔のない画像(inf)はskip
  if np.isinf(face_score[i]):
    continue
  #検出器スコアが3より小さいものは除外する
  elif face_score[i] < 3:
    continue
  #2番目の検出器スコアがNaN以外（画像内に複数顔がある）の場合もskip(今回は検証しない)
  if not np.isnan(second_face_score[i]):
    continue
  DataIndexList.append(i)

#世代ごとに集める顔画像と正解の箱（リスト）を準備
img_Teens = []
img_Twenties = []
img_Thirties = []
img_Forties = []
img_Fifties = []
img_Sixties = []
img_Seventies = []

y_Teens = []
y_Twenties = []
y_Thirties = []
y_Forties = []
y_Fifties = []
y_Sixties = []
y_Seventies = []

#カスケードファイルを指定して、検出器を作成
cascade_file = "./opencv-4.8.0/data/haarcascades/haarcascade_frontalface_default.xml"
cascade = cv2.CascadeClassifier(cascade_file)

#対象データの準備
for i in range(len(DataIndexList)):
  #世代ごとに集める画像の数
  collectNum = 100
  collectCounter = 0

  if collectCounter > collectNum * 7:
    break

  #世代の算出
  generation = calc_generation(photo_taken[DataIndexList[i]],dob[DataIndexList[i]])
  #世代が0歳以下（写真撮影年<生年月日）の誤りのあるデータがあるため除外
  #および10歳未満は対象としない
  if generation <= 0:
    continue

  #今回は男性のみを対象とする
  #1男性, 0女性,　NaN未設定
  if (gender[DataIndexList[i]] == 0) or (np.isnan(gender[DataIndexList[i]])) :
    continue

  #画像データの読み込み
  filepath = wiki_path + str(full_path[DataIndexList[i]][0])
  img = cv2.imread(filepath)

  #工夫：グレースケールの画像は除外
  img_blue, img_green, img_red = cv2.split(img)
  if (img_blue==img_green).all() & (img_green==img_red ).all():
    continue

  #検証 画像の大きさを取得
  height, width, channels = img.shape[:3]
  #工夫：サイズが小さい画像は除外する
  if height < 256 and width < 256:
    continue

  img_face = img
  #画像の読み込んでグレースケール
  img_gray = cv2.cvtColor(img_face, cv2.COLOR_RGB2GRAY)

  #工夫：顔検出
  facerect = cascade.detectMultiScale(img_gray, scaleFactor=1.2, minNeighbors=5, minSize=(100, 100))

  #顔検出の結果失敗なら除外
  if len(facerect) == 0:
    continue
  else:
    rect = facerect[len(facerect) - 1]#複数顔画像があった場合、最初に設定された矩形を選択

    #顔部分を切り取り
    img_face_ex = img_face[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]]


  #顔画像を学習データとして使用。世代毎に集める数を収集
  my_img = cv2.cvtColor(img_face_ex, cv2.COLOR_BGR2RGB)
  my_img = cv2.resize(my_img, (224,224))

  if generation  == 1 :
    if len(img_Teens) < collectNum:
      img_Teens.append(my_img)
      y_Teens.append(generation - 1)
      collectCounter += 1

  elif generation  == 2 :
    if len(img_Twenties) < collectNum:
      img_Twenties.append(my_img)
      y_Twenties.append(generation - 1)
      collectCounter += 1

  elif generation  == 3 :
    if len(img_Thirties) < collectNum:
      img_Thirties.append(my_img)
      y_Thirties.append(generation - 1)
      collectCounter += 1

  elif generation  == 4 :
    if len(img_Forties) < collectNum:
      img_Forties.append(my_img)
      y_Forties.append(generation - 1)
      collectCounter += 1

  elif generation  == 5 :
    if len(img_Fifties) < collectNum:
      img_Fifties.append(my_img)
      y_Fifties.append(generation - 1)
      collectCounter += 1

  elif generation  == 6 :
    if len(img_Sixties) < collectNum:
      img_Sixties.append(my_img)
      y_Sixties.append(generation - 1)
      collectCounter += 1

  elif generation  == 7 :
    if len(img_Seventies) < collectNum:
      img_Seventies.append(my_img)
      y_Seventies.append(generation - 1)
      collectCounter += 1

#for -- end

X = np.array(img_Teens + img_Twenties + img_Thirties + img_Forties + img_Fifties + img_Sixties + img_Seventies)
y = np.array(y_Teens + y_Twenties + y_Thirties + y_Forties + y_Fifties + y_Sixties + y_Seventies)


#画像枚数分の等差数列を配列ndarrayとして生成(配列の要素のIndex用)後、Indexをランダムに並べ替え
rand_index = np.random.permutation(np.arange(len(X)))
#ランダムなIndexで画像の要素を並べ替え
X = X[rand_index]
y = to_categorical(y[rand_index]) #one-hotベクトル
```


# 1.3.2　学習
学習方法として、転移学習を行いました。転移学習とは既に学習済みのモデルを転用することで、効率的に新たな学習モデルの生成ができる手法です。今回はVGG16と呼ばれるモデルで転移学習しました。

また、学習には時間がかかりますので、凡化性能が低下する過学習の状態の前に学習を終了させるEarlyStoppingを仕込みました。

その後各種設定を変えつつ2週間程度、試行錯誤・悪戦苦闘してみて、一番正解率が良かったコードを記載します。

以下、先ほどのコードの続きです。
``` python
# データの分割(訓練データ80%, 検証データ残り20%)
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):]

#vgg16のインスタンスの生成
input_tensor = Input(shape=(224, 224, 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(448, activation='relu'))
top_model.add(Dropout(rate=0.1)) #ドロップアウト

top_model.add(Dense(224, activation='relu'))
top_model.add(Dropout(rate=0.1))

top_model.add(Dense(64, activation='relu'))
top_model.add(Dropout(rate=0.1)) #ドロップアウト

#7クラス分類の出力用の全結合層を追加
top_model.add(Dense(7, activation='softmax')) #one-hotベクトル

#モデルの連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

#vgg16の重みの固定
for layer in model.layers[:19]:
    layer.trainable = False
#コンパイル
model.compile(loss='categorical_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])

# EarlyStopping コールバックの設定
early_stopping = EarlyStopping(
    monitor='val_loss', # 監視する値（例：検証データの損失）
    patience=7,         # 指定したエポック数以上改善がない場合に訓練を停止
    verbose=1,          # 進行状況のメッセージ表示
    restore_best_weights=True # 最も良いモデルの重みを復元
)

#学習
history = model.fit(X_train, y_train, batch_size=32, epochs=30, validation_data=(X_test, y_test), callbacks=[early_stopping])


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

#acc, val_accのプロット
plt.plot(history.history["accuracy"], label="accuracy", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_accuracy", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()

plt.plot(history.history["loss"], label="loss", ls="-", marker="o")
plt.plot(history.history["val_loss"], label="val_loss", ls="-", marker="x")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.show()

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

このコードで実行した結果は次のようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/d725cfa7-eee1-9ecb-4fd1-1b9639c8e4b8.png)

青色の線が訓練データに対する正解率、オレンジの線が検証データに対する正解率です。VGG16の層の後に追加した全結合層の設定や、学習時の損失関数の設定など色々試行錯誤して、ようやく正解率が40%を超えた設定でした。（他の設定では良くても30%程度でしたので、この数値が出たときは感動しました）

※注意※
今回記載したコードを何回か実行したところ、正解率を再現しないことがありました。アカデミーのチューターの方と相談したところ、学習時のランダムの固定をし忘れていたためとのことでした。とほほ…
<br>

ちなみに損失結果は次のような結果でした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/36279007-6d03-55e9-e47d-4d8ce7bf4d48.png)


# 1.4 考察
学習モデルが作成できましたので、モデルを使用して予測してみました。

IMDB-WIKIのデータより、モデル生成時には使用しなかった、男性の各年代データ20個分をモデルに予測させた結果を混合行列で表しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/267dea9a-adeb-8a52-ae68-f49a106e8594.png)

縦軸横軸の0は10代、1は20代…6は70代を表しています。

作成した学習モデルでは、
- 10代から70代までのすべての世代の顔画像を3、つまり40代と推測したケースが多数見られた
- 低年齢と高齢の方ではある程度、正解率に近い判断ができている
- 3の40代では8割正解できている
- 逆に2の30代と、4の50代の正解率が極端に悪い

男性の顔は大体40代くらいが、各世代に共通する特徴をもっているのか？というような結果となりました。
今は学習に使用した40代のデータの精査が必要だったかもと思っています。

ちなみに、2018年の記事によると、ファミリーマートとローソンでは、前述しました客層ボタンは廃止されたとのことです（[参考サイト](https://www.yomiuri.co.jp/fukayomi/20180717-OYT8T50002/))。
<br>

参考サイトの記事には、
> 「Ｔカードのデータと比較した客層ボタンのデータの精度が低かったこと」

とあり、人の判断でも年代を特定することは、難しいのかもしれません。

## 2.　Webアプリの作成
作成したモデルを使用して、Webアプリを作成しました。

Pythonのコードは以下です。
``` python
import os
from flask import Flask, request, redirect, render_template, flash, session
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 = ["10","20","30","40","50","60","70"]
image_size = 224

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

app = Flask(__name__)
app.secret_key = 'hogehoge' #session

def allowed_file(filename):
    #次の2つの条件をand確認
    #1. filenameの中に.が含まれているかどうか
    #2. 拡張子の確認。filenameを文字列の最後から「.で１回」区切り、
    #取り出した1番目の要素を小文字に変換し、ALLOWED_EXTENSIONSのどれかに該当するかどうか
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

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


@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):
            #flash(request.url) #flashの確認のため故意にrequest.urlを表示

            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            pred_answer = "予測前"

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img) #引数に与えられた画像をNumpy配列に変換
            data = np.array([img]) #Numpy配列のリストに変換
            #変換したデータ(Numpy配列のリスト)をモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "この方は" + classes[predicted] + " 代と予測します"

            return render_template("index.html",answer=pred_answer)
        else :
            flash('選択されたファイルは画像ではありません')
            return redirect(request.url)            

    #POSTリクエストがなされないとき（単にURLにアクセスしたとき）
    return render_template("index.html",answer="")

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

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>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="#">Generation Classifier</a>
    </header>

    <div class="main">    
        <h2> AIが送信された男性の顔画像から年代を識別します</h2>
        <h3> ※10代以上から70代までの判別します。</h3>
        <p>男性画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file" onchange="previewFile(this);">
            <!-- <input class="file_choose" type="file" name="file"> -->
            <input class="btn" value="送信する" type="submit">
        </form>
        <div class="answer">{{answer}}</div>
        {% with messages = get_flashed_messages() %}
            {% for message in messages %}
                <li>
                    <i>
                        <font color="orange">{{ message }}</font>
                    </i>
                </li>
            {% endfor %}
        {% endwith %}
    </div>
  <footer>
      <small>thanks to Aidemy, 2023</small>   
  </footer>
</body>
</html>
```

htmlのcssファイルは次のようになっています。
``` css
header {
    background-color: #da70d6;
    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 20px 0px;
    text-align: center;
}

h3 {
    color: #444444;
    margin: 0px 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: #da70d6;
    height: 110px;
    margin: -8px;
    bottom: 0;
    position: relative;
}

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

small {
    margin: 15px 25px;
    position: absolute;
    left: 0;
    bottom: 0;
}
```
<br>

Local環境にFlaskを導入し、Pythonを実行しました。
ブラウザで当該ページに移動しますと、次のような画面が表示されるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/cc5f2440-0692-3739-75d7-503d8f630567.png)

「ファイルを選択」を押すと、ファイルダイアログが表示されますので、
男性画像を選択し、「送信する」を押します。


試しに、適当な自己紹介で有名な70代のタレントさんの画像を送信してみます。<br>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/2a7d8604-3feb-989f-2f55-4d59a6c7dfc6.png)
<br>

予測結果が表示されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659213/f7aeedec-4e22-a268-8b49-bfe3b4d7c713.png)

…また、40代!

## さいごに
初めてのAIモデルの作成、顔画像から年代予測を試みたのですが、思うような正解率が出ませんでした。
今回使用したIMDB-WIKIは多様な人種の画像群となっていますので、人種毎に学習モデルを作成すると、より興味深い結果が得られたかもしれません。

AIの奥深さ、面白さが体感でき、良い経験となりました。
<br>

折角学んだ良い機会。
<br>

今後、現場で携わることがなかったとしても、今回限りとせず、他のAIを作成してみたり、知識を増やしてみたり、何かに役立っていければと思いました。

<br>
最後にこの場を借りて、学習機会を頂けましたAidemyのスタッフの方々に感謝を申し上げます。<br><br>
最後までお読みいただき、ありがとうございました。<br><br>




***
IMDB-WIKI 引用：
@article{Rothe-IJCV-2018,
  author = {Rasmus Rothe and Radu Timofte and Luc Van Gool},
  title = {Deep expectation of real and apparent age from a single image without facial landmarks},
  journal = {International Journal of Computer Vision},
  volume={126},
  number={2-4},
  pages={144--157},
  year={2018},
  publisher={Springer}
}

@InProceedings{Rothe-ICCVW-2015,
  author = {Rasmus Rothe and Radu Timofte and Luc Van Gool},
  title = {DEX: Deep EXpectation of apparent age from a single image},
  booktitle = {IEEE International Conference on Computer Vision Workshops (ICCVW)},
  year = {2015},
  month = {December},
}
***