One Class SVMを用いた教師なし笑顔検出

#概要

プログラミングの練習として、何かを実装しようと考え、笑顔検出をすることにしました。
やたらとDeepLearningが騒がれている今日ですが、かなり古臭い手法で実装しています。
思い付きで作った適当なシステムゆえ、ご指摘点などありましたらコメントいただけると幸いです。

笑顔検出の流れとしては、以下の通りです。

1. WEBカメラで画像を取得する。
2. OpenCVのカスケード識別器で顔を検出し、その範囲を切り取る。
3. 切り取り画像のHOG特徴を取得する。
4. One Class SVMを用いて「笑顔（正常状態）」か「笑顔じゃない（異常状態）」を判別する。

カスケード識別器を使うためには[こちら](https://github.com/opencv/opencv/tree/master/data/haarcascades)のhaarcascade_frontalface_alt.xmlが必要になるので、cloneしてください。

#One Class SVMについて
One Class SVMは、教師データが集まりにくい問題でよく使われています。今回は異常検知問題として用いてみます。つまり、笑顔の状態を「正常」、それ以外を「異常」と定義します。

通常のSVM(Support Vector Machine)では、教師あり学習を行い、識別境界を求めています。なので、笑顔の検知問題で言えば、「笑顔」のほかに「真顔」などのデータを学習するイメージです。
一方、One Class SVMでは、教師なし学習を行い、「正常」か「異常」かを判別します。なので、「笑顔」の学習データさえあれば、「笑顔」と「それ以外」を判別できます。

#実装
フェーズとしては3段階あります。
1. 学習データ（「笑顔（正常状態）」）を集める。（data_collect()）
2. 集めたデータを用いてOne Class SVMを学習する。（train()）
3. 学習済みOne Class SVMを用いて笑顔検知する。（main()）


```python:main.py
import numpy as np
import cv2
from skimage.feature import hog
from sklearn.svm import OneClassSVM
from sklearn.decomposition import PCA
import pickle

n = 3
n_dim = 4
alpha = - 1.0e+6
th = 20 #3
nu = 0.2 #0.1 #入力データの異常値の割合
font = cv2.FONT_HERSHEY_COMPLEX
train_data = "./dataset/train/train.csv"
weights = "./dataset/weights/weights.sav"
weights_pca = "./dataset/weights/weights_pca.sav"

f_ = cv2.CascadeClassifier()  # "./cascades/haarcascade_fullbody.xml"
f_.load(cv2.samples.findFile("./cascades/haarcascade_frontalface_alt.xml"))

def preprocess(image):
    frame = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    frame = cv2.equalizeHist(frame)
    return frame

def data_collect():
    feature = []
    capture = cv2.VideoCapture(0)

    while (True):
        ret, frame = capture.read()
        frame = preprocess(frame)
        face = f_.detectMultiScale(frame)  # ,scaleFactor=1.2

        for rect in face:
            cv2.rectangle(frame, tuple(rect[0:2]), tuple(rect[0:2] + rect[2:4]), (255, 255, 0), thickness=2)
            face_frame = frame[rect[1]:rect[1] + rect[3], rect[0]:rect[0] + rect[2]]
            face_frame = cv2.resize(face_frame, (60, 60))
            hog_f_, im = hog(face_frame, visualise=True,transform_sqrt=True)
            feature = np.append(feature,hog_f_)
            np.savetxt(train_data,feature.reshape(-1,2025), delimiter=",")
            cv2.putText(frame, "please smile for collecting data!", (10, 100), font,
                             1, (255, 255, 0), 1, cv2.LINE_AA)
        cv2.waitKey(1)
        cv2.imshow("face", frame)

def train():
    x_train = np.loadtxt(train_data,delimiter=",")
    pca = PCA(n_components=n_dim)
    clf = OneClassSVM(nu=nu, gamma=40/n_dim)#1/n_dim
    z_train = pca.fit_transform(x_train)
    clf.fit(z_train)

    pickle.dump(pca, open(weights_pca, "wb"))
    pickle.dump(clf,open(weights,"wb"))

def main():
    clf = pickle.load(open(weights,"rb"))
    pca = pickle.load(open(weights_pca, "rb"))
    capture = cv2.VideoCapture(0)

    while(True):
        ret,frame = capture.read()
        frame = preprocess(frame)
        face = f_.detectMultiScale(frame)

        for rect in face:
            cv2.rectangle(frame,tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]),(255,255,0),thickness=2)
            face_frame = frame[rect[1]:rect[1]+rect[3],rect[0]:rect[0]+rect[2]]
            face_frame = cv2.resize(face_frame,(60,60))
            feature , _ = hog(face_frame,visualise=True,transform_sqrt=True)
            z_feature = pca.transform(feature.reshape(1,2025))
            score = clf.predict(z_feature.reshape(1,n_dim))
            if score[0]== 1:
                cv2.putText(frame, "smile!", (10, 100), font,
                             1, (255, 255, 0), 1, cv2.LINE_AA)
        cv2.waitKey(1)
        cv2.imshow("face",frame)#まさかのv,uで指定

if __name__ == '__main__':
    data_collect() #まず、この関数のみで回して、笑顔データの収集
    #train() #笑顔データ(csv)を読んで、svmの学習
    #main() #学習したmodelで笑顔検知

```
（なお、正確な実装は[こちら](https://github.com/satolab12/One-Class-SVM)を参照ください。
One Class SVMは、scikit-learnの実装を用いました。実際に使用する際は、以下のようにディレクトリを作ってください。コードに間違いなどありましたらご指摘いただけると幸いです。
OneClassSVM/
　├ dataset/
　│　└ train/
　│　└ weights/
　├ cascades/　　
　└ main.py


####実装補足
カスケード分類器で顔の画像を切り取ったのち、60×60にresizeしています。（サイズは適当に決めました。）
SVMの学習の前にPCAでHOG特徴の次元を削減しています。この理由は、HOGの次元が2000を超えるために呪いを危惧したためです。n_dimで削減後の次元数を指定できます。今回は4としました。
また、One Class SVMの学習において、ハイパパラメータの調整で少し手間取りました。
OneClassSVMのfitの引数にnuとgammaがあります。nuは学習データの異常の割合、gammaは1/特徴量の次元数らしいです。しかしそもそも学習データに異常値がないので、どうすればと悩んだ結果、結局main()をまわしつつよいパラメータを探し、train()をしました。最終的にnu=0.3,gamma=50/特徴量の次元数としました。お使いになる際は、nuとgammaの調整が必要となるかもしれません。


#実験
main()を回して実験をしました。顔が正面を向いているときの精度はそこそこ(体感的には8割ぐらい)ですが、学習データに微笑みレベルの笑顔を入れてしまったので、真顔と笑顔の識別境界の学習が難しくなってしまったかもしれません。

#参考文献
[OpenCV Haar-Cascadeによる顔検出](https://qiita.com/tnoce/items/c819c85a85c16d246be8)　
[One Class SVMを用いた異常値検知](https://www.slideshare.net/YutoMori2/one-class-svm)