ドッペルゲンガーがいたので人工知能(笑)で判別してみた(後編)

こんにちは！Liaroのエンジニアの@eve_ykです。事の発端については[前編](http://qiita.com/eve_yk/items/e0ccd7917237fed1607f)を見て下さい(泣)
前回は識別器を学習させるためのデータ集め・加工を行いました。
今回は識別器のモデルを記述し、実際に学習・評価(顔識別)をしていきます！

## 3. 識別器のモデルを構築する
次は学習させる顔画像識別器のモデルを[Chainer](http://chainer.org/)で記述します。
(前回のデータセット作成も合わせて)以下のコードを参考にさせていただきました。
[https://github.com/mitmul/chainer-cifar10](https://github.com/mitmul/chainer-cifar10)



```py:YTNet
class YTNet(chainer.Chain):

    def __init__(self):
        """
            モデルの定義
        """
        super(YTNet, self).__init__(
            conv1=L.Convolution2D(3, 32, 5, stride=1, pad=2),
            bn1  =L.BatchNormalization(32),
            conv2=L.Convolution2D(32, 32, 5, stride=1, pad=2),
            bn2  =L.BatchNormalization(32),
            conv3=L.Convolution2D(32, 64, 5, stride=1, pad=2),
            fc4=F.Linear(16384, 4096),
            fc5=F.Linear(4096, 2),
        )
        self.train = True

    def __call__(self, x, t):
    	"""
    	    forword処理
	    """
        h = F.max_pooling_2d(F.relu(self.conv1(x)), 3, stride=2)
        h = F.max_pooling_2d(F.relu(self.conv2(h)), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.dropout(F.relu(self.fc4(h)), ratio=0.5, train=self.train)
        h = self.fc5(h)

        self.loss = F.softmax_cross_entropy(h, t)
        self.accuracy = F.accuracy(h, t)

        if self.train:
            return self.loss
        else:
            self.pred = F.softmax(h)
            return self.pred
```


## 4. モデルを学習する
3.のコードをもとに、モデルを学習させるコードを書きます。
今回は比較的すぐに学習が終わりますが、もっと大規模なデータセットを扱おうとした時は、学習の進捗を表示するのが精神衛生上良いです。

```py:train.py
# -*- coding: utf-8 -*-

import argparse
import os
import six
import chainer
import chainer.functions as F
import chainer.links as L
import numpy as np
from chainer import optimizers
from chainer import cuda
from chainer import serializers
from chainer import Variable
from progressbar import ProgressBar


class YTNet(chainer.Chain):

    def __init__(self):
        """
            モデルの定義
        """
        super(YTNet, self).__init__(
            conv1=L.Convolution2D(3, 32, 5, stride=1, pad=2),
            bn1  =L.BatchNormalization(32),
            conv2=L.Convolution2D(32, 32, 5, stride=1, pad=2),
            bn2  =L.BatchNormalization(32),
            conv3=L.Convolution2D(32, 64, 5, stride=1, pad=2),
            fc4=F.Linear(16384, 4096),
            fc5=F.Linear(4096, 2),
        )
        self.train = True

    def __call__(self, x, t):
        """
            forword処理
        """
        h = F.max_pooling_2d(F.relu(self.conv1(x)), 3, stride=2)
        h = F.max_pooling_2d(F.relu(self.conv2(h)), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.dropout(F.relu(self.fc4(h)), ratio=0.5, train=self.train)
        h = self.fc5(h)

        self.loss = F.softmax_cross_entropy(h, t)
        self.accuracy = F.accuracy(h, t)

        if self.train:
            return self.loss
        else:
            self.pred = F.softmax(h)
            return self.pred

def one_epoch(args, model, optimizer, data, label, epoch, train):
    """
        1epochのトレーニング、もしくは評価処理を行う
    """
    model.train = train
    xp = cuda.cupy if args.gpu >= 0 else np

    sum_accuracy = 0
    sum_loss = 0

    p = ProgressBar(min_value=0, max_value=data.shape[0]) # 進捗確認用
    perm = np.random.permutation(data.shape[0])
    for i in xrange(0, data.shape[0], args.batchsize):
        p.update(i)
        
        # minibatchを作成
        target = perm[i:i + args.batchsize]
        x = xp.array(data[target], dtype=xp.float32)
        t = xp.array(label[target], dtype=xp.int32)

        # Variableを作成
        volatile = 'off' if train else 'on'
        x = Variable(x, volatile=volatile)
        t = Variable(t, volatile=volatile)

        # パラメータ更新、もしくはラベル予測
        if train:
            optimizer.update(model, x, t)
        else:
            pred = model(x, t).data
        
        sum_loss += float(model.loss.data) * t.data.shape[0]
        sum_accuracy += float(model.accuracy.data) * t.data.shape[0]

        del x, t

    print "" # 改行用
    if train:
        print "train epoch " + str(epoch)
        print "   train loss : " + str(sum_loss / data.shape[0])
        print "   train acc  : " + str(sum_accuracy / data.shape[0])
    else:
        print "test epoch " + str(epoch)
        print "   test loss : " + str(sum_loss / data.shape[0])
        print "   test acc  : " + str(sum_accuracy / data.shape[0])

def load_dataset(datadir):
    """
        データセットをロードする
    """
    train_data = np.load('%s/train_data.npy' % datadir)
    train_labels = np.load('%s/train_label.npy' % datadir)
    test_data = np.load('%s/test_data.npy' % datadir)
    test_labels = np.load('%s/test_label.npy' % datadir)

    return train_data, train_labels, test_data, test_labels

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--gpu", type=int, default=-1)
    parser.add_argument("--batchsize", type=int, default=10)
    parser.add_argument('--data_dir', type=str, default='dataset')
    parser.add_argument('--output_dir', type=str, default='result')
    args = parser.parse_args()

    # model,optimizer作成
    model = YTNet()
    optimizer = optimizers.Adam(alpha=0.00005)
    optimizer.setup(model)

    # データセットロード
    dataset = load_dataset(args.data_dir)
    tr_data, tr_labels, te_data, te_labels = dataset

    # メインループ
    for epoch in range(1, 20):
        # 訓練
        one_epoch(args, model, optimizer, tr_data, tr_labels, epoch, True)
        # 評価
        one_epoch(args, model, optimizer, te_data, te_labels, epoch, False)

    # モデルの保存
    if not os.path.exists(args.output_dir):
        os.makedirs(args.output_dir)
    serializers.save_npz(args.output_dir + "YTNet.chainermodel", model)
    serializers.save_npz(args.output_dir + "YTNet.state", optimizer)
```

20epochほど学習させると訓練データでのAccuracyが99%を超えサチっていました。


## 5. 性能を評価する
いよいよ、性能の評価をします。
以下の10枚の画像を使ってテストします。この中の5枚が@eve_ykで5枚がスーパーマラドーナ田中さんです。
顔のサンプルは[前回の記事](http://qiita.com/eve_yk/items/e0ccd7917237fed1607f)にあるので、当てられるかみなさんも挑戦してみてください。

![test.JPG](https://qiita-image-store.s3.amazonaws.com/0/110468/73942355-6da2-abf7-8947-e8f099236f79.jpeg)

少し悩みますが、人の目で見ればわかりますね。眼鏡の色とか違います。
正解は、1,2,4,7,10がeve_yk、3,5,6,8,9が田中さんです。

さて、今回学習したCNNではどうでしょうか？
以下のコードでテストします。

```py:test.py
# coding:utf-8

import os
import sys
import argparse
import glob
import cv2
import numpy as np
from chainer import Variable
from chainer import serializers
from train import YTNet

def transpose_opencv2chainer(x):
	"""
		opencvのnpy形式からchainerのnpy形式に変換
		opencv  => (height, width, channel)
		chainer => (channel, height, width)
	"""
	return x.transpose(2,0,1)

file2labels = {"01.jpg":"eve_yk", "02.jpg":"eve_yk", "03.jpg":"tanaka",
			   "04.jpg":"eve_yk", "05.jpg":"tanaka", "06.jpg":"tanaka",
			   "07.jpg":"eve_yk", "08.jpg":"tanaka", "09.jpg":"tanaka",
			   "10.jpg":"eve_yk"}


if __name__ == "__main__":
	parser = argparse.ArgumentParser(description='CNN用データセット作成')
	parser.add_argument('--input_path',   required=True, type=str)
	parser.add_argument('--model_path',  required=True, type=str)
	args = parser.parse_args()

	# jpgファイル一覧取得
	test_files  = glob.glob(args.input_path+"/*.jpg")

	# モデルのロード
	model = YTNet()
	model = serializers.load_npz(args.model_path, , model)

	# 一枚ずつ評価
	collect_count = 0.0
        test_count = 0.0
	for file_path in test_files:
		image = cv2.imread(file_path)
		if image is None:
			# 読み込み失敗
			continue
                test_count += 1.0

		# ディレクトリ構造からファイル名を取得
		file_name = file_path.split("/")[-1]
                print file_name+"("+file2labels[file_name]+") :",

		# chainer用形式に変換
		image = transpose_opencv2chainer(image)
		x = Variable(np.asarray([image], dtype=np.float32), volatile="on")
		t = Variable(np.asarray([[0]], dtype=np.int32), volatile="on")

		# 評価
		pred = model(x, t).data
		if int(pred) == 0: # tanaka
			print u"識別結果「tanaka」"
			if file2labels[file_name] == u"tanaka":
				collect_count += 1.0
		else: # eve_yk
			print u"識別結果「eve_yk」"
			if file2labels[file_name] == u"eve_yk":
				collect_count += 1.0	

	print u"total:{}%".format(collect_count/test_count*100)
```



結果はこの通り
```
python test.py --input_path test/ --model_path result/YTNet.chainermodel
08.jpg(tanaka) : 識別結果「tanaka」
09.jpg(tanaka) : 識別結果「eve_yk」
07.jpg(eve_yk) : 識別結果「eve_yk」
01.jpg(eve_yk) : 識別結果「eve_yk」
03.jpg(tanaka) : 識別結果「tanaka」
06.jpg(tanaka) : 識別結果「eve_yk」
02.jpg(eve_yk) : 識別結果「eve_yk」
05.jpg(tanaka) : 識別結果「tanaka」
04.jpg(eve_yk) : 識別結果「eve_yk」
10.jpg(eve_yk) : 識別結果「eve_yk」
total:8.0/10
```

6と9の田中さんをeve_ykと間違えてしまいました。
訓練データの量の違いから、偏った予測をしてしまうのでしょうか。
しかし、人の目で見ても悩む画像を誤っている気がします。やはり、中途半端なシステムでドッペルゲンガーを見分けるのは難しいようです。

## おわりに
自分のそっくりさんを識別する顔分類器を作成しました。
精度はまぁまぁといったところ。データ数や前処理など、妥協したところが多くあった中では良かった方かと思います。
頑張ってデータを集めてもっと多人数の分類をしたり、前処理などを頑張って精度向上にチャレンジすると面白そうですね！

Liaro、eve_ykはスーパーマラドーナを応援しています！

## 参考
[https://github.com/mitmul/chainer-cifar10](https://github.com/mitmul/chainer-cifar10)

[吉本興業株式会社 芸人プロフィール | スーパーマラドーナ](http://search.yoshimoto.co.jp/talent_prf/?id=387)