Keras+Colaboratoryでハーフタレントの顔識別したときのメモ

# 概要
Keras+GoogleColabで顔識別を試してみた。 ハーフ系タレントを見事見分けられるか？？

# 目的
- 顔認識システムを作りたい。
- 過去に試した時の備忘録が消えてしまったので、再度やってみた。今回はメモを残す。

# 関連記事
- [顔認識のテストのためにスクレイピングでハーフタレントの画像収集してみた](https://qiita.com/kiwsdiv/items/8aa53e4afaf3b148ca3f)
- [Colaboratoryで学習したモデル+CPU環境でハーフタレントの顔識別したときのメモ](https://qiita.com/kiwsdiv/items/2f72b20c345cbfeee34e)

# やったこと
スクレイピングでデータ収集、データ前処理、モデル定義、Tensorboadで学習過程の可視化、Prediction、GradCamなど。試行錯誤なので無駄なこともやってるかも。

## データ準備
画像はスクレイピングで収集。([顔認識のテストのためにスクレイピングでハーフタレントの画像収集してみた](https://qiita.com/kiwsdiv/items/8aa53e4afaf3b148ca3f))

今回はハーフタレント（ハリー杉山、Joy、ウエンツ、ユージ、+ ラウール）で試してみた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/89333490-f298-8d4f-abcf-bc787b26cff4.png)

データを1:5の割合でtrain,testに分類。

```python
#2割をテストデータに移行
import shutil
import random
import glob
import os
print('Making train data & test data...')
datadir  ="Orgdata/"
traindir ="data/train/"
testdir  ="data/test/"
SearchName = os.listdir(datadir)

test_num = 0.2 # 20%をテスト画像とする
if not os.path.exists(traindir):
  os.makedirs(os.path.join(traindir))
if not os.path.exists(testdir):
  os.makedirs(os.path.join(testdir))
for name in SearchName:
  print(name)
  in_dir = datadir+ name
  in_png = glob.glob(in_dir+"/*")
  img_file_name_list=os.listdir(in_dir)
  # img_file_name_listをシャッフル、そのうち2割をtest_imageディテクトリに入れる
  random.shuffle(in_png)
  if not os.path.exists(testdir+name):
    os.makedirs(testdir+name,exist_ok=True)
  if not os.path.exists(traindir+name):
    os.makedirs(traindir+name,exist_ok=True)
  for i_fl in range(len(in_png)):
    if i_fl < int(len(in_png)*test_num):
      shutil.copy(str(in_png[i_fl]),testdir+name)
    else:
      shutil.copy(str(in_png[i_fl]),traindir+name)

print('done!')
```

## Data Augmentation
KerasのImageDataGeneratorを使ってみる。(このクラスの存在を知らなかったのでnumpyで関数を作っていた…。) 顔画像には[閾値処理、ぼかし処理、回転処理]が有効らしい。ただ、ぼかし処理はImageDataGeneratorにはないので、自作する必要がある。まずはImageDataGeneratorをつかった水増しを行う。
参考：[自前のDeep Learning用のデータセットを拡張して水増しする](https://qiita.com/halspring/items/7692504afcba97ece249)

```python
import os
import glob
import numpy as np
from keras.preprocessing.image import ImageDataGenerator,load_img,img_to_array,array_to_img

def draw_images(generator,x,img_name,dir_name,index):
  save_name = img_name+'_ext'
  g = generator.flow(x,batch_size=1,save_to_dir=output_dir,save_prefix=save_name,save_format='png')
  # 1つの入力画像から何枚拡張するかを指定
  n_aug = 10
  for i in range(n_aug):
    bach = g.next()

print('Data Augmentation...')
train_dir ="data/train"
SearchName=os.listdir(train_dir)

for i in range(len(SearchName)):
  name = SearchName[i]
  print(str(i+1)+'/'+str(len(SearchName))+':'+name)
  # InputDir
  input_dir = train_dir+"/"+name
  # Orginalimage
  images     = glob.glob(os.path.join(input_dir+"/*.png"))
  image_file = os.listdir(input_dir)
  # OutputDir
  output_dir = input_dir
  if not (os.path.exists(output_dir)):
    os.makedirs(output_dir)

  # SettingImageDataGenerator
  generator=ImageDataGenerator(
            rotation_range      = 30,  # [deg]まで回転
            width_shift_range   = 0.1, # 水平方向にランダムでシフト
            height_shift_range  = 0.1, # 垂直方向にランダムでシフト
            channel_shift_range = 50.0,# 色調をランダム変更
            shear_range         = 0.39,# 斜め方向(pi/8まで)に引っ張る
            horizontal_flip     = True,# 水平方向にランダムで反転
            vertical_flip       = False# 垂直方向にランダムで反転
            )
  
  # 読み込んだ画像を順に拡張
  for i in range(len(images)):
    img      = load_img(images[i])
    img_name = image_file[i].split('.')[0] # 元ファイル名を保存する
    # 画像を配列化して転置a
    x = img_to_array(img)
    x = np.expand_dims(x,axis=0)
    # 画像の拡張
    draw_images(generator,x,img_name,output_dir,i)

print('done!')
```

最終的には下記のようなデータ数となった。各150枚スクレイピングして、ごみを目視で取り除く作業をしたらこの位になった。大体同じくらいの数にそろえた方がいいかもしれない。

|      |original|test|train|  
|----  |----|----|---- | 
|Other |170 |34  |1495 |  
|Joy   |74  |14  |660  | 
|Harry |70  |14  |615  |  
|Uentsu|131 |26  |1155 | 
|Raul  |55  |11  |484  | 
|Yuji  |63  |12  |559  | 

## 正規化とラベリング
正規化してラベリングを行う。

```python
from keras.utils.np_utils import to_categorical
import cv2
import os
import numpy as np

def normalized(rgb):
  norm = np.zeros((rgb.shape[0],rgb.shape[1],3),np.float32)
  b = rgb[:,:,0]
  g = rgb[:,:,1]
  r = rgb[:,:,2]
  norm[:,:,0] = cv2.equalizeHist(b)
  norm[:,:,1] = cv2.equalizeHist(g)
  norm[:,:,2] = cv2.equalizeHist(r)
  return norm

def make_label(datadir):
  print('loading'+datadir+'...')
  SearchName = os.listdir(datadir)
  # 教師データのラベル付け
  data  = []
  label = []
  for i in range(len(SearchName)):
    name = SearchName[i]
    img_file_name_list = os.listdir(datadir+"/"+name)
    print("{}:Number of data is {}.".format(name,len(img_file_name_list)))

    for img_file in img_file_name_list:
      img_path = os.path.join(datadir+"/"+name+"/",img_file)
      img = np.uint8(normalized(cv2.imread(img_path)))
      if(img is None or len(img)==0):
        print('image'+img_file+':NoImage')
        continue
      else:
        data.append(img)
        label.append(np.uint8(i))
  
  print('done!')
  return(data,label)

print('Labeling...')
SaveDir ='data/'
if not os.path.exists(SaveDir):
  os.mkdir(os.path.join(SaveDir))

# TrainData
X_train,y_train = make_label('data/train')
X_train = np.array(X_train)
y_train = to_categorical(y_train)

# TestData
X_test,y_test = make_label('data/test')
X_test = np.array(X_test)
y_test = to_categorical(y_test)

# 顔認識する対象のclass Name作成
def classes_name(datadir):
  SearchName = os.listdir(datadir)
  # ラベル付け
  classes = []
  for i in range(len(SearchName)):
    name = SearchName[i]
    classes.append(name)
  return classes

classes = classes_name('data/train')
# classes = ['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
print(classes)

print('Labeling done!')
```
## Model
(180x180x3)の画像を6クラス分。

```python
from keras.layers import Activation,Conv2D,Dense,Flatten,MaxPooling2D
from keras.models import Sequential

# CNNで学習するときの画像のサイズを設定（サイズが大きいと学習に時間がかかる）
ImgSize = (180,180,3)

# モデルの定義
model = Sequential()
model.add(Conv2D(input_shape=ImgSize,filters=32,kernel_size=(3,3),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(filters=32,kernel_size=(3,3),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Conv2D(filters=32,kernel_size=(3,3),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation("sigmoid"))
model.add(Dense(128))
model.add(Activation('sigmoid'))

# 分類したい人数を入れる
model.add(Dense(len(classes)))
model.add(Activation('softmax'))

# コンパイル
print("Compilingmodel...")
model.compile(optimizer='sgd',loss = 'categorical_crossentropy',metrics = ['accuracy'])
print("done")
model.summary()
```

モデルを可視化すると下記のようになる。
```python
from tensorflow.keras.utils import plot_model
# グラフで可視化。
plot_model(model, show_shapes=True)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/928b5224-e9c3-0e89-441c-3e6aa746bd18.png)

## 学習
### コールバックの作成
```python
from keras.callbacks import ModelCheckpoint
# CheckPoint
print("Deifining callbacks...")
filepath = "weights/weights.best.hdf5"
checkpoint = ModelCheckpoint(filepath,monitor='val_accuracy',verbose=1,save_best_only=True,mode='max')
callbacks_list = [checkpoint]
os.makedirs('weights',exist_ok=True)
print("done.")
```

### Tensorboad
学習の進捗をリアルタイムでVisualizeするためにTensorboadを使用してみた。必須ではない。
```python
# Load the Tensor Board notebook extension
%load_ext tensorboard
from keras.callbacks import TensorBoard

logdir = "logs"
os.makedirs(logdir,exist_ok=True)
tf_callback = TensorBoard(log_dir=logdir,histogram_freq=1)
callbacks_list = [checkpoint,tf_callback]

# Visualize
%tensorboard --logdir logs
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/e7fc153b-d262-1448-0cf5-04bf2e59157f.png)

### Model fitting
約5000枚のデータをバッチサイズ64、100エポック学習させる。Google Colab上で1エポック4sec程度。~~学習元のデータが不十分なのか、70エポック程度で過学習気味？accも0.5程度で頭打ち。~~ データクレンジングしたら改善した。
```python
batch_size = 64
nb_epoch   = 100
print("Fitting model...")

history = model.fit(X_train, y_train, batch_size=batch_size, epochs = nb_epoch, verbose = 1, callbacks = callbacks_list, validation_data=(X_test,y_test))
print("done.")

# This save the trained model weights to this file with number of epochs
print("Saving model and weights...")
os.makedirs('models',exist_ok=True)
model.save('models/vgg16_model.hdf5')
model.save_weights('weights/model_weight_{}.hdf5'.format(nb_epoch))
print("done.")
```

### 精度の評価・表示
```
import matplotlib.pyplot as plt

# 汎化精度の評価・表示
score = model.evaluate(X_test, y_test, batch_size=32, verbose=0)
print('validation loss:{0[0]}\nvalidation accuracy:{0[1]}'.format(score))

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

# loss, val_loss
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()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/b312a78a-281b-cac0-53b7-319a6cd53fc4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/734e5036-6ec4-3591-ca23-7a03f5f51b97.png)


## Prediction

modelとweightを読み込む。これらのファイルをダウンロードして置けば、別のPC環境でも実行可能。
```python
from keras.models import load_model
model = load_model('models/vgg16_model.hdf5')
model.load_weights("weights/weights.best.hdf5")
```
testデータの中から適当なデータを突っ込んでみる。
1つの画像をPredictするのに0.15秒弱＠Let's Note

```python
# テスト用のコード
from keras.preprocessing import image
import numpy as np
import matplotlib.pyplot as plt
import cv2
from google.colab.patches import cv2_imshow

def normalized(rgb):
  # print(rgb)
  # returnrgb/255.0
  norm = np.zeros((rgb.shape[0],rgb.shape[1],3),np.float32)
  b = rgb[:,:,0]
  g = rgb[:,:,1]
  r = rgb[:,:,2]
  norm[:,:,0] = cv2.equalizeHist(b)
  norm[:,:,1] = cv2.equalizeHist(g)
  norm[:,:,2] = cv2.equalizeHist(r)
  return norm

# 画像を読み込んで予測する
def img_predict(filename):
  print(filename)
  # 画像を読み込んで4次元テンソルへ変換
  # 学習時に cv2.equalizeHist で normalized したので、同様に正規化を行う。
  img = cv2.imread(filename)
  x   = np.uint8(normalized(img))
  x   = np.expand_dims(x, axis=0)

  # 表示
  cv2_imshow(img)

  # 指数表記を禁止にする
  np.set_printoptions(suppress=True)

  # 画像の人物を予測    
  pred = model.predict(x)[0]
  
  # 結果を表示する
  print(classes[np.argmax(pred)] + ':{:.2f}'.format(pred[np.argmax(pred)]*100) + '%' )

  print(classes)
  print(pred*100)
```
```python
import glob
tests  = glob.glob('./data/test/*')
for human in tests:
  # テスト用の画像が入っているディレクトリのpathを()に入れてください
  test  = glob.glob(human + '/*')
  # 数字は各自入力
  i    = np.random.randint(len(test))
  img_predict(test[i])
  print('\n')
```

- Other
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/6469985f-20c1-e5cc-a99d-7fec9289a8de.png)
Other:99.82%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[99.82031     0.04009435  0.00082934  0.03350555  0.01194384  0.09332196]

- Joy
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/f75023dc-adc0-756c-495d-f0a5a549fc8c.png)
Joy:38.14%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[37.25051    38.13593     0.01143323  1.0881307  13.000186   10.513814  ]

- Harry
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/1e8ff557-b970-8220-0a65-25dbc66a0e44.png)
Harry:98.07%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[ 0.08365683  0.69369125 98.06667     0.6573652   0.49559665  0.0030142 ]

- Uentsu
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/bb31c042-eca3-7a71-76cd-689990cc9dd0.png)
Uentsu:64.66%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[14.70626     5.4450717   0.35880703 64.6608      4.9400964   9.888976  ]

- Raul
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/1bc55345-d38d-0dff-c00c-e41a07048b6a.png)
Uentsu:78.08%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[ 8.502379    1.1384983   0.36339712 78.08337    11.412714    0.4996482 ]

- Yuji
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/fefbd821-f440-6175-5acd-7dbf7d05434f.png)
Yuji:94.60%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[ 0.11124698  1.4917603   0.00512221  3.3407028   0.44914895 94.60202   ]

そこそこPredictionできている？

### Confusion Matrix
Confusion Matrixをプロットしてみた。
```python
import numpy as np
from sklearn.metrics import confusion_matrix
import pandas as pd
import seaborn as sn
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

def label_class(y_test):
  classes = np.zeros((len(y_test),))
  for i in range(len(y_test)):
    cls        = np.nonzero(y_test[i,:])[0][0]
    classes[i] = np.uint8(cls)
  return classes

def print_cmx(y_true,y_pred,xlabels,normalize=False):
  labels = sorted(list(set(y_true)))
  cmx_data = confusion_matrix(y_true,y_pred,labels=labels)
  if normalize:
    cmx_data = cmx_data.astype('float')/cmx_data.sum(axis=1)[:,np.newaxis]
  df_cmx = pd.DataFrame(cmx_data,index=labels,columns=labels)
  fig = plt.figure(figsize=(12,7))
  ax  = fig.add_subplot(1,1,1)
  
  fmt = '.2f' if normalize else 'd'
  sn.heatmap(df_cmx,annot=True,fmt=fmt,square=True)
  ax.set_xticklabels(xlabels)
  fig.autofmt_xdate(rotation=45)
  ax.set_yticklabels(xlabels)
  
  # fig.autofmt_ydate(rotation=90)
  plt.ylabel("actual")
  plt.xlabel("predict")
  plt.show()
```

```python
predict_prob    = model.predict(X_test) 
predict_classes = np.argmax(predict_prob,axis=1)
true_classes    = np.argmax(y_test,axis=1)
xlabels         = classes

print_cmx(true_classes, predict_classes, xlabels)
print_cmx(true_classes, predict_classes, xlabels,normalize=True)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/15544641-d216-6e9e-4105-7feb80449136.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/9d31f8b8-7a23-d551-8421-e274ca93b65d.png)

全体的にUentsuに間違えがち。特に、Harry を Uentsuに間違えがち。Uentsuの元データが多く、芸歴も長いのでいろんな画像があるからだろうか？

試しに、オリジナルデータ全て識別させた結果を見てみる。学習に使ったデータが含まれるので、識別率は良くなるはず。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/a5e5a7ce-eb31-1fd6-7574-24cfc5534141.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/629c42df-c243-0c19-d600-e746bb0dbf62.png)

## GradCAM
重みの可視化する。Pooling直前の最後のConv層の重みを見るらしい。ここでは"conv2d_2"を指定。

下記エラーが出て困っていたところ、非常に参考になるQiita記事を見つけたので、参考にさせていただきました。
> RuntimeError: tf.gradients is not supported when eager execution is enabled. Use tf.GradientTape instead.

[kerasとtensorflowでGrad-CAMを実装してみた](https://qiita.com/Kensuke-N/items/cea3920f19bebc9e0d19)

バージョン依存によるエラーのようです。2019年に動いたコードが動かなくなって一番はまったのはここでした。他にも以下の記事も参考にしました。
- [Tensorflow2系で指定したレイヤーから勾配を取得したい場合](https://www.aipacommander.com/entry/2019/05/14/000250)
- [Tensorflow1系の勾配計算をTensorflow2系以降で行う時に躓いた話【Keras】](https://kenkyujinsei.com/2020/10/20/tensorflow1%E7%B3%BB%E3%81%AE%E5%8B%BE%E9%85%8D%E8%A8%88%E7%AE%97%E3%82%92tensorflow2%E7%B3%BB%E4%BB%A5%E9%99%8D%E3%81%A7%E8%A1%8C%E3%81%86%E6%99%82%E3%81%AB%E8%BA%93%E3%81%84%E3%81%9F%E8%A9%B1/)

現バージョンは以下。
```python
import tensorflow as tf
import keras
print('tensorflow version: ', tf.__version__)
print('keras version: ', keras.__version__)
```
```
tensorflow version:  2.8.2
keras version:  2.8.0
```
GradCamの関数を定義して、適当な画像で試してみる。

```python
import numpy as np
import cv2

# 画像用
from keras.preprocessing.image import array_to_img, img_to_array, load_img
# モデル読み込み用
from keras.models import load_model
# Grad−CAM計算用
from tensorflow.keras import models
import tensorflow as tf

IMAGE_SIZE=(180,180)
target_layer = 'conv2d_2'

def grad_cam(input_model, x, layer_name=target_layer):
    """
    Args: 
        input_model(object): モデルオブジェクト
        x(ndarray): 画像
        layer_name(string): 畳み込み層の名前
    Returns:
        output_image(ndarray): 元の画像に色付けした画像
    """

    # 画像の前処理
    # 読み込む画像が1枚なため、次元を増やしておかないとmode.predictが出来ない
    X = np.expand_dims(x, axis=0)
    preprocessed_input = X.astype('float32') / 255.0    

    grad_model = models.Model([input_model.inputs], [input_model.get_layer(layer_name).output, input_model.output])

    with tf.GradientTape() as tape:
        conv_outputs, predictions = grad_model(preprocessed_input)
        class_idx = np.argmax(predictions[0])
        loss = predictions[:, class_idx]

    # 勾配を計算
    output = conv_outputs[0]
    grads = tape.gradient(loss, conv_outputs)[0]

    gate_f = tf.cast(output > 0, 'float32')
    gate_r = tf.cast(grads > 0, 'float32')

    guided_grads = gate_f * gate_r * grads

    # 重みを平均化して、レイヤーの出力に乗じる
    weights = np.mean(guided_grads, axis=(0, 1))
    cam = np.dot(output, weights)

    # 画像を元画像と同じ大きさにスケーリング
    cam = cv2.resize(cam, IMAGE_SIZE, cv2.INTER_LINEAR)
    # ReLUの代わり
    cam  = np.maximum(cam, 0)
    # ヒートマップを計算
    heatmap = cam / cam.max()

    # モノクロ画像に疑似的に色をつける
    jet_cam = cv2.applyColorMap(np.uint8(255.0*heatmap), cv2.COLORMAP_JET)
    # RGBに変換
    rgb_cam = cv2.cvtColor(jet_cam, cv2.COLOR_BGR2RGB)
    # もとの画像に合成
    output_image = (np.float32(rgb_cam) + x / 2)  

    return output_image
```
```python
# Modeland Weight Setting
model=load_model('models/vgg16_model.hdf5') # load
model.load_weights("weights/weights.best.hdf5")

# 画像を読み込む
test = glob.glob('Orgdata/Uentsu/*')
i    = np.random.randint(len(test))
filename = test[i]

# Predictioin
img_predict(filename)

# GradCam
img = cv2.imread(filename)
x   = np.uint8(normalized(img)) # 正規化する

gradimg = grad_cam(model, x, target_layer)
gradimg = array_to_img(gradimg)

# 表示
plt.imshow(gradimg)
plt.show()
```
Orgdata/Uentsu/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/6540f913-d6b6-a828-db86-30c8b31d8f3e.png)
Uentsu:99.83%
['Other', 'Joy', 'Harry', 'Uentsu', 'Raul', 'Yuji']
[ 0.01584092  0.02424594  0.02174441 99.82527     0.03469932  0.07820116]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599506/df0b6eb4-867b-83f9-fdc4-b41b2e21abde.png)

目と口のあたりで判断しているのだろうか…。

# 結論
学習、識別まで一通り試すことができた。([Google Colaboratoryのノート全体](https://github.com/iwatadive28/Face_Predict_Keras/blob/main/Colaboratory/Face_Predict_Keras.ipynb))

データを増やす、正確にラベリングする、怪しいデータ(JOYか誰かわからないような画像)を省くことで識別率が上がったが、testデータでの識別率がいまいち。元データが大事。

モデルさえ作成できればGPUのないPCでも識別処理可能なので、会議中にWEBカメラに映る顔をリアルタイムで識別し続け、会議中に映して遊ぶこともできた。

同じ要領で自分の画像をGoogle Photoから抽出して、OpenCVで切り取り、学習させることもできたので、顔認識を用いるアプリに応用してみたい。

# 参考
- [KerasのCNNで、顔認識AIを作って見た〜スクレイピングからモデルまで〜](https://qiita.com/yottyann1221/items/a08300b572206075ee9f)
- [TensorFlowによるディープラーニングで、アイドルの顔を識別する](https://memo.sugyan.com/entry/20160112/1452558576)
- [kerasとtensorflowでGrad-CAMを実装してみた](https://qiita.com/Kensuke-N/items/cea3920f19bebc9e0d19)
- [Tensorflow2系で指定したレイヤーから勾配を取得したい場合](https://www.aipacommander.com/entry/2019/05/14/000250)
- [Tensorflow1系の勾配計算をTensorflow2系以降で行う時に躓いた話【Keras】](https://kenkyujinsei.com/2020/10/20/tensorflow1%E7%B3%BB%E3%81%AE%E5%8B%BE%E9%85%8D%E8%A8%88%E7%AE%97%E3%82%92tensorflow2%E7%B3%BB%E4%BB%A5%E9%99%8D%E3%81%A7%E8%A1%8C%E3%81%86%E6%99%82%E3%81%AB%E8%BA%93%E3%81%84%E3%81%9F%E8%A9%B1/)
- [自前のDeep Learning用のデータセットを拡張して水増しする](https://qiita.com/halspring/items/7692504afcba97ece249)