コンテンツにスキップするには Enter キーを押してください

chainer メモ(その3)機械学習で顔認識!CNN を実装

 

はじめに

いままでchainerについてちょいちょい書いているのですが(その1)(その2)、実際に画像のクラス分けをしてみたいので、その途中段階までをまとめます。
顔認識ということですが、乃木坂のメンバーを認識してみようと思います。
画像集めの問題もあって今回は入力画像をメンバー4人+それ以外人たちの5つのクラスの誰であるかを判別します。




 

データセット

データですが、乃木坂メンバーからとりあえず、秋元真夏、白石麻衣、西野七瀬、生田絵梨花、そして誰やねんって人を大量に入れたやつ
それぞれの画像を300枚ほどあつめてOpencvを利用して顔画像のみを切り取り128×128のサイズにして保存しています。
データはDropboxで公開しときます。
こんな感じの画像
ss-2016-10-01-22-09-23

 

コード

コードの全文はgithubにあります。
学習用のファイルの中で、必要なとこだけ書いてます。
学習モデルはとりあえず、Alexnetほぼほぼそのままです。
なので、顔認識としての性能は正直期待しすぎないでください。
Alexnetをファインチューニングして行う場合は


を見てください。このページを見た前提で書いてしまっているのですが、、、


class AlexLike(chainer.Chain):
    insize = 128
    def __init__(self, n_out):
        super(AlexLike, self).__init__(
            conv1=L.Convolution2D(None,  64, 8, stride=4),
            conv2=L.Convolution2D(None, 128,  5, pad=2),
            conv3=L.Convolution2D(None, 128,  3, pad=1),
            conv4=L.Convolution2D(None, 128,  3, pad=1),
            conv5=L.Convolution2D(None, 64,  3, pad=1),
            fc6=L.Linear(None, 1024),
            fc7=L.Linear(None, 1024),
            fc8=L.Linear(None, n_out),
        )
        self.train = True

    def __call__(self, x):
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.fc7(h)), train=self.train)
        h = self.fc8(h)
        return h

訓練データのフォルダ構造が以下のようになっているとします。
-args.path
|- the_others
|- 画像1
|- 画像2
|- …
|- akimoto
|- 画像1
|- 画像2
|- …
|- shiraishi
|- 画像1
|- 画像2
|- …
|- ikuta
|- 画像1
|- 画像2
|- …

このとき、the_othersが0, akimotoが1, ikutaが2というようにラベル付されていきます。


def main():
	...
	#学習させる画像の入ったフォルダパスとそのラベルがセットにされた配列のリスト
	pathsAndLabels = []
	label_i = 0
	data_list = glob.glob(args.path + "*")
	print(data_list)
	for datafinderName in data_list:
        	pathsAndLabels.append(np.asarray([datafinderName+"/", label_i]))
		label_i = label_i + 1

	train, test = image2TrainAndTest(pathsAndLabels,channels=args.channel) 
	
	# 入力層と出力層の数を引数で渡す。
	model = L.Classifier(alexLike.AlexLike(len(pathsAndLabels)))
	
	# ここから下は普通に
	optimizer = chainer.optimizers.Adam()
	optimizer.setup(model)
	
	train_iter = chainer.iterators.SerialIterator(train, args.batchsize)
	test_iter = chainer.iterators.SerialIterator(test, args.batchsize,
                                                 repeat=False, shuffle=False)

	updater = training.StandardUpdater(train_iter, optimizer, device=args.gpu)
	trainer = training.Trainer(updater, (args.epoch, 'epoch'), out=args.out)
	trainer.extend(extensions.Evaluator(test_iter, model, device=args.gpu))
	...
	trainer.run()
	...
	
if __name__ == '__main__':
	main()

次に、具体的に画像をどのように学習用のデータに変形しているか
こちらも必要なとこだけ
ポイントは入力用の画像データを H(高さ)xW(幅)xK(深) から KxHxWにするところです。
pillowのImage.open(path)で読み込んだ画像は前者の行列の形になっているのですが
CNNでは後者の形にしないと入らないので、変えてあげます。


import numpy as np
from PIL import Image
import glob
from chainer.datasets import tuple_dataset


def image2TrainAndTest(pathsAndLabels, size=128, channels=1):

    #まず、全データを配列に入れてからシャッフルする。
    allData = []
    for pathAndLabel in pathsAndLabels: #ここは人数分の5回まわる
        path = pathAndLabel[0] #その人の画像が入ったディレクトリ
        label = pathAndLabel[1] #その人のラベル
        imagelist = glob.glob(path + "*") # glob(ディレクトリ/*)とすることで当てはまるファイル名を取ってくる
        for imgName in imagelist: #その人の画像ファイル数、回る
            allData.append([imgName, label])
    allData = np.random.permutation(allData) #シャッフルする

    if channels == 1:
        #省略...
    else:
        imageData = []
        labelData = []
        for pathAndLabel in allData:
            img = Image.open(pathAndLabel[0])
	    	# ここが、上記したポイント部分
            # Image.openで取り入れた画像のデータは
            # [ [ [r,g,b], [r,g,b], ... ,[r,g,b] ],
            #   [ [r,g,b], [r,g,b], ... ,[r,g,b] ],
            #   ...
            #   [ [r,g,b], [r,g,b], ... ,[r,g,b] ] ]
            # のようにrgbがおなじ位置に入ってる感じなのですが、これをそれぞれ赤の画像、緑の画像、青の画像の様にして行列にしないといけません。
            # 以下のようなデータにする
            # [ [ [ r, r, r, ... ,r ],      ここから
            #     ...
            #     [ r, r, r, ... ,r ] ],        ここまでが赤の画像
            #
            #   [ [ g, g, g, ... ,g ],      ここから
            #     ...
            #     [ g, g, g, ... ,g ] ],        ここまでが緑の画像
            #
            #     [ b, b, b, ... ,b ],      ここから
            #     ...
            #     [ b, b, b, ... ,b ] ] ]       ここまでが青の画像
            r,g,b = img.split()
            rImgData = np.asarray(np.float32(r)/255.0)
            gImgData = np.asarray(np.float32(g)/255.0)
            bImgData = np.asarray(np.float32(b)/255.0)
            imgData = np.asarray([rImgData, gImgData, bImgData])
            imageData.append(imgData)
            labelData.append(np.int32(pathAndLabel[1]))

	# 最初の87.5%を学習用に、後の12.5%をテスト用に(この数はお好きに)
        threshold = np.int32(len(imageData)/8*7)
        train = tuple_dataset.TupleDataset(imageData[0:threshold], labelData[0:threshold])
        test  = tuple_dataset.TupleDataset(imageData[threshold:],  labelData[threshold:])

return train, test

これで学習できるようになるはずです。

 

CNNで指定している数の決め方

2016/11/02現在、chainerでは入力の値を明記しなくても動くようになっているので(Noneとすれば動く)、とりあえず使えればいいやという人は以下”CNNで指定している数の求め方”を読む必要はありません。
ただし、学習がどのように行われているか知りたい人は、ぜひ読んでみてください。

上記のAlexクラスのCNNで書いてある数をどうやって決めているか
CNNってそもそも何?という人はどっかで読んできておくか、一番下に載せている参考を見てみてください。
というよりここから書くCNNのことは参考ページの省略版(劣化版…)ですので、全然わからない人は参考をどうぞ
具体的にコードを見ながら書いていきます。

conv1=L.Convolution2D(input_channel, 32, 8, stride=4)
・input_channelは入力チャンネルの数。カラーなら3、グレーなら1です。
・32は出力チャンネルの数
・8はフィルターの大きさ
・stride=4はフィルターをどれだけ動かすか

ここでポイントなのが、画像サイズとここに上げた数値の関係

CNNはフィルターをスライドしながら畳み込みをしていくので、フィルターがはみ出ないように数値の設定には決まりがあります。
・画像のサイズからフィルターの大きさを引いたものがstrideで割り切れること
例えば (128 – 8)/ 4 の余りはゼロー

conv1では画像サイズなどを考慮して数値を設定する必要がありますが
con2からconv5までは適当でも大丈夫だと思います。(この発言が適当だったり)
ただ、全結合層(Linear)と同じように上の層の出力層の数と次の層の入力層の数は等しくしてください。

もう一つのポイント

・conv5の出力層の数からfc6の入力層の数。なぜ288になったのか
重用なのは最終的なフィルターのサイズとconv5の出力チャンネル(32)

conv1. (128(画像サイズ) – 8(フィルタサイズ) / 4(stride)) + 1 = 31
出力されたフィルターのサイズ。これが32チャンネルあるがチャンネル数は最後にかければよいので考えなくてよい
pool1. (31-3)/2+1 = 15
プーリング層でもフィルタサイズ3でstrideが2なので同じように計算していきます。
conv2. (15+2*2-5)/1+1 = 15
パディングが2なので、両端に2づつ増えた(2*2)と考え、srtideが1なので1で割り、あとは同じように計算します。
pool2. (15-3)/2+1 = 7
conv3. (7+1*2-3)/1+1 = 7
パディングが1なので、両端に1づつ増えた(1*2)、あとは同じ
conv4. (7+1*2-3)/1+1 = 7
conv5. (7+1*2-3)/1+1 = 7
pool5. (7-3)/2+1 = 3

最終的なフィルターのサイズは3なので(ちゃんと計算してみると意味あるのかって思うぐらい小さかった)
3×3(フィルタサイズ)x32(conv5の出力チャンネル) = 288
となるわけです。
あとは全結合層でつなげて終わりです。

    insize = 128

    def __init__(self, input_channel, n_out):
        super(Alex, self).__init__(
            conv1=L.Convolution2D(input_channel,  32, 8, stride=4),
            conv2=L.Convolution2D(32, 256,  5, pad=2),
            conv3=L.Convolution2D(256, 256,  3, pad=1),
            conv4=L.Convolution2D(256, 256,  3, pad=1),
            conv5=L.Convolution2D(256, 32,  3, pad=1),
            fc6=L.Linear(288, 144),
            fc7=L.Linear(144, 50),
            fc8=L.Linear(50, n_out),
        )
        self.train = True

    def __call__(self, x):
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.fc7(h)), train=self.train)
        h = self.fc8(h)
        return h

 

学習結果

python facePredictionTraining.py -g 0 -p ./images/

学習結果のグラフです。ss-2016-10-01-19-10-27
悪くない!

 

参考

https://qiita.com/icoxfog417/items/5aa1b3f87bb294f84bac
CNNのことが概念から詳しく書いてあります。








コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です