Aidemy 受講生が年齢当てAIアプリを作った




#はじめに
文系出身プログラミング初学者が、勉強開始2ヶ月目で年齢当てAIアプリをつくったので、ここに記述します。Aidemy【AIアプリ開発講座】の受講生です。(2021年10月現在)

#目次
1.始める前の準備
 　データセットを用意する
2.実装環境の紹介
3.コード全文
4.コード解説
　必要なモジュールをインポート
　データを取得
　データを訓練用とテスト用に分割
　モデルを構築
　モデルの学習、予測
5.もっと精度を上げるには
6.参照リンク
　・顔画像から年齢を予測-Qiita
　・pythonでEddicientNet+MultiOutputを使って年齢予測の実装
7.さいごに

#1. 始める前の準備
機械学習をするにはデータが必要となります。自分で作成したデータでも、Kaggleやオープンデータセットからでも良いのでデータを予めダウンロードしておきます。
###データセットを用意する

今回はKaggleにあったUTKFaceというデータを使用しました。UTKFaceは0歳から105歳までの人間の顔画像と年齢、性別、人種が記述されているデータセットです。今回はこの中から顔画像と年齢のデータのみを取得しました。

・[UTKFace大規模な顔のデータセット]
(https://susanqq.github.io/UTKFace/)

#2. 実装環境
GoogleColaboratory(GPU)

＊Aidemy「男女の識別(深層学習発展)」講座の添削課題のコードをベースにしつつ、下記の参照ブログも活用させて頂きながら作成しました。

#3. コード全文
```python
# 必要なモジュールをインポート
import cv2
import numpy as np
import matplotlib.pyplot as plt
from keras.layers import Dense, Dropout, Flatten, Input
from keras.applications.vgg16 import VGG16
from keras.models import Model, Sequential
from tensorflow.keras import optimizers
from sklearn.metrics import mean_squared_error
import os, zipfile, io, re
from keras.utils.np_utils import to_categorical


# Zipファイルを読み込む
z = zipfile.ZipFile('自分の環境に合わせてファイルパスを記述する')

# 画像入力サイズ
image_size=100
img = []

# 画像ファイルパスのみ取得
imgfiles = [ x for x in z.namelist() if re.search(r"^UTKFace.*jpg$", x)]
X=[]
Y=[]

#for imgfile in imgfiles[:1]:
for imgfile in imgfiles:    
# ZIPから画像読み込み
    image = Image.open(io.BytesIO(z.read(imgfile)))
# RGB変換
    image = image.convert('RGB')
# リサイズ
    image = image.resize((image_size, image_size))
    # 画像から配列に変換
    data = np.asarray(image)
    file = os.path.basename(imgfile)
    #print(file)
    file_split = [i for i in file.split('_')]
    #print(file_split)
    X.append(data)
    Y.append(int(file_split[0]))
z.close()

# Numpy配列
X = np.array(X)
y = np.array(Y)
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

# １０歳から６０歳までのデータを取り込む
index = np.where((y > 10) & (y < 61))
X = X[index]
y = y[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):]

# データ型の変換＆正規化
X_train = X_train.astype('float32') / 255
X_test = X_test.astype('float32') / 255

# モデルにvgg16を使う
input_tensor = Input(shape=(100, 100, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

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

# vggと、top_modelを連結
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
# vggの層の重みを変更不能にする
for layer in model.layers[:19]:
    layer.trainable = False
# コンパイル
model.compile(loss='mse',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9))

# 学習
model.fit(X_train, y_train, batch_size=100, epochs=20, validation_data=(X_test, y_test))

# testデータ30件の予測値
preds=model.predict(X_test[0:30])

# testデータ30件の画像と正解値＆予測値を出力
plt.figure(figsize=(16, 6))
for i in range(30):
    plt.subplot(3, 10, i+1)
    plt.axis("off")
    pred = round(preds[i][0],1)
    true = y_test[i]
    if abs(pred - true) < 5.4:
        plt.title(str(true) + '\n' + str(pred))
    else:
        plt.title(str(true) + '\n' + str(pred), color = "red")
    plt.imshow(X_test[i])
plt.show()
```

#4. コード解説


###必要なモジュールをインポート
```python
#必要なモジュールをインポート
import cv2
import numpy as np
import matplotlib.pyplot as plt
from keras.layers import Dense, Dropout, Flatten, Input
from keras.applications.vgg16 import VGG16
from keras.models import Model, Sequential
from tensorflow.keras import optimizers
from sklearn.metrics import mean_squared_error
import os, zipfile, io, re
from keras.utils.np_utils import to_categorical
```
###データの取得
ダウンロードしておいたUTKFaceのZipファイルを読み込みます。
ファイルパスは自分の環境に合わせて記述します。

```python
#Zipファイルを読み込みます
z = zipfile.ZipFile('自分の環境に合わせてファイルパスを記述します')
```
画像入力データを100にします。100である必要はなく、違う大きさでも大丈夫ですが、大き過ぎず、小さ過ぎないサイズに揃える必要があります。また、画像の色を今回は分かりやすいようにカラーにしましたが、必要に応じて白黒でも良いです。

```python
# 画像入力サイズを100にします
image_size=100
img = []

# 画像ファイルパスのみ取得します
imgfiles = [ x for x in z.namelist() if re.search(r"^UTKFace.*jpg$", x)]

X=[]
Y=[]

for imgfile in imgfiles:    
# ZIPから画像読み込みます
    image = Image.open(io.BytesIO(z.read(imgfile)))
# RGB（カラー画像）変換します
    image = image.convert('RGB')
# リサイズ（今回は100）
    image = image.resize((image_size, image_size))
# 画像からNumpy配列に変換します
    data = np.asarray(image)
    file = os.path.basename(imgfile)
    file_split = [i for i in file.split('_')]
    X.append(data)
    Y.append(int(file_split[0]))
z.close()
```
機械学習では必ず画像データをNumpy配列にします


```python
#Numpy配列にします
X = np.array(X)
y = np.array(Y)
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
```

初め、UTKFaceにある0歳から105歳までの全年齢を取り込んで実行したところ、精度があまり良くありませんでした。10歳から60歳までの画像データは十分あったのですが、それ以外の年齢層の画像データが少ないためだと思われます。なので、今回は判定する年齢を10歳から60歳までに限定することにします。

```python
#　１０歳から６０歳までの画像を取り込みます
index = np.where((y > 10) & (y < 61))
X = X[index]
y = y[index]
```
###データを訓練用とテスト用に分割
データを訓練用とテスト用に分けます。
今回は訓練用8割、テスト用を2割にしました。

```python
# データの分割をします
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):]
```
データ型の数字が0から1になるように255で割ることにします。
0から1と小さい数字に変換することで、学習がしやすくなるからです。

```python
# データ型の変換＆正規化
X_train = X_train.astype('float32') / 255
X_test = X_test.astype('float32') / 255
```
###モデルの構築
モデルの構築をします。
Aidemyの「男女の識別（深層学習発展）」講座のテキストで習った、Vgg16を使用することにします。Vgg16以外にもモデルはたくさんありますが、できる限り講座で教わったものを使いたかったので、Vgg16にします。

```python
#　モデルにvgg１６を使用します
input_tensor = Input(shape=(100, 100, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)
# vggのoutputを受け取り、1クラス分類する層を定義します
# その際中間層を下のようにいくつか入れると精度が上がります
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(1))
```

```python
# vggと、top_modelを連結します
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
# vggの層の重みを変更不能にします
for layer in model.layers[:19]:
    layer.trainable = False
```
今回、損失関数をMSE(二乗平均誤差)にしました。

MSEについての説明は下記のリンクが分かりやすいと思います。
[損失関数／評価関数］平均二乗誤差（MSE：Mean Squared Error）／RMSE（MSEの平方根）とは？]
(https://atmarkit.itmedia.co.jp/ait/articles/2105/24/news019.html)


```pyhon
# コンパイルします
model.compile(loss='mse',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9))
```
###モデルの学習
今回epoch数を最終的に20にしました。初めepoch数を1、5、8、10と増やしていった結果、精度が良くなかったので、取り込むデータを10歳から60歳までの人に限定し、さらにepoch数を20に増やして学習させました。


```python
# 学習を行います
model.fit(X_train, y_train, batch_size=100, epochs=20, validation_data=(X_test, y_test))
```
学習結果が出ました。

```
Epoch 1/20
145/145 [==============================] - 53s 282ms/step - loss: 160.9948 - val_loss: 94.3863
Epoch 2/20
145/145 [==============================] - 34s 238ms/step - loss: 117.7160 - val_loss: 93.2173
Epoch 3/20
145/145 [==============================] - 34s 238ms/step - loss: 103.6921 - val_loss: 120.2020
Epoch 4/20
145/145 [==============================] - 34s 238ms/step - loss: 106.4828 - val_loss: 84.1454
Epoch 5/20
145/145 [==============================] - 35s 239ms/step - loss: 99.8083 - val_loss: 84.0747
Epoch 6/20
145/145 [==============================] - 34s 237ms/step - loss: 99.5817 - val_loss: 93.7258
Epoch 7/20
145/145 [==============================] - 34s 237ms/step - loss: 97.9828 - val_loss: 88.4010
Epoch 8/20
145/145 [==============================] - 35s 239ms/step - loss: 98.2729 - val_loss: 90.6476
Epoch 9/20
145/145 [==============================] - 35s 239ms/step - loss: 93.6212 - val_loss: 81.7669
Epoch 10/20
145/145 [==============================] - 34s 238ms/step - loss: 94.1987 - val_loss: 79.7704
Epoch 11/20
145/145 [==============================] - 34s 238ms/step - loss: 94.7794 - val_loss: 79.6666
Epoch 12/20
145/145 [==============================] - 34s 237ms/step - loss: 93.0207 - val_loss: 82.5291
Epoch 13/20
145/145 [==============================] - 34s 238ms/step - loss: 93.7706 - val_loss: 87.5701
Epoch 14/20
145/145 [==============================] - 34s 238ms/step - loss: 93.1326 - val_loss: 77.2366
Epoch 15/20
145/145 [==============================] - 34s 238ms/step - loss: 92.1037 - val_loss: 75.6866
Epoch 16/20
145/145 [==============================] - 35s 238ms/step - loss: 91.4197 - val_loss: 76.0775
Epoch 17/20
145/145 [==============================] - 34s 238ms/step - loss: 92.4192 - val_loss: 77.7524
Epoch 18/20
145/145 [==============================] - 34s 238ms/step - loss: 91.3217 - val_loss: 75.2684
Epoch 19/20
145/145 [==============================] - 35s 239ms/step - loss: 89.6997 - val_loss: 79.1030
Epoch 20/20
145/145 [==============================] - 34s 238ms/step - loss: 91.5178 - val_loss: 76.4674
<keras.callbacks.History at 0x7f66c0a86110>
```

###モデルの予測
テストデータを30枚だけ出力し、予測値がどう出ているか確認してみます。
画像サイズを16×3、3行10列に並べて、実際の年齢を上段、予測年齢を下段に出力してみます。

```python
# testデータ30件の予測値
preds=model.predict(X_test[0:30])
# testデータ30件の画像と正解値＆予測値を出力
plt.figure(figsize=(16, 6))
for i in range(30):
    plt.subplot(3, 10, i+1)
    plt.axis("off")
    pred = round(preds[i][0],1)
    true = y_test[i]
    if abs(pred - true) < 5.4:
        plt.title(str(true) + '\n' + str(pred))
    else:
        plt.title(str(true) + '\n' + str(pred), color = "red")
    plt.imshow(X_test[i])
plt.show()
```

さて、AIが予測した年齢はいかに？


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2127720/435be804-d02a-2b45-51a3-ffdb884f9962.png)

ん？そんなに精度が良くない？笑　
一番初めは全員10.5歳と予測していたのに比べれば、随分マシになったのでこれにて良しとします！

#5. 精度をもっと上げるには
もっと精度を上げるには、

・モデルをVGGではないモデルに変える
・Denseの数字を変える
・ダウンサンプリングする。つまり、今回10歳から60歳までの人に限定しましたが、発想を変えて中央値である10歳から60歳までの人のデータを減らして、それ以外の年齢層の画像と均等になるようにデータを調整する
・ダウンサンプリングとは逆に、10歳から60歳の中央値以外の人の画像を増やして、10歳から60歳までの画像と均一になるように調整する

などの方法が考えられます。

いくつか試した結果がこれなので、今回はこれにて終了とします。
今後はハイパーパラーターを調整して、もっと精度を上げることにも挑戦したいと思います。
#6. 参照リンク

・[pythonでEfficientNet + Multi Output を使って年齢予測の実装]
(https://www.acceluniverse.com/blog/developers/2020/03/pythonefficientnet-multi-outpu.html)
・[顔画像から年齢を予測-Qiita]
(https://qiita.com/ha9kberry/items/314afb56ee7484c53e6f)



参考にさせて頂きました。とても助かりました！ありがとうございます！

#7. さいごに
　勉強開始２ヶ月目でAIアプリを作ることができたなんて、嬉しい驚きです。これもひとえに、サポートしてくださったチューターさんのお陰です！改めて、ここに感謝申し上げます。どうもありがとうございました！