ニートがプログラミングするブログ(はてな出張所)

ニートがプログラミングするブログです。今は主にコンピュータビジョンに関することをやっています。

LSGAN-AE (Least Squares Generative Adversarial Networks with AutoEncoder)

追記:コードを修正したので記事も修正しました。

追記2:他のデータセットで試したらうまくいかなかったので、もう少し修正します。

 

まだ研究中なので間違ってる部分があるかもしれません。
LSGAN(Least Squares Generative Adversarial Networks)とAE(AutoEncoder)を組み合わせることで、それなりにディープ(generatorだけで14層)なネットワークでも学習させることに成功しました。
ただし単純に組み合わせただけではうまくいかないので、いろいろと工夫が必要になります。
この記事ではその工夫について書いていきます。

・ネットワーク構造
LSGANで使うネットワークはgeneratorとdiscriminator、AEで使うネットワークはencoderとdecoderです。
このうちgeneratorとdecoderの重みを共有させることで、LSGANとAEをくっつけます。
その際、generator/decoderとdiscriminator/encoderは対称にして、学習が必要なパラメータ数を同じにします。
ただし単純に入力チャネル数と出力チャネル数を反対にした重みを使うだけではうまくいきません。
そこで演算の順番も対称にします。
具体的には、generator/decoderではupsample->convolution->bias->activationと進みますが、discriminator/encoderではactivation->bias->convolution->pooling、という順番で演算を行います。

・重みの初期化
重みWはHeの方法で初期化し、bは0で初期化します。

・アップサンプル
オリジナルのDCGANではupsampleの方法としてtransposed convolutionが使われていますが、ここではzero padding upsampleを使います。
(実際はどういう名前なのか知らないので、勝手にこういう名前にしました。)
通常のupsampleで縦横を2倍にする場合には次のようになります。
元のデータ
1 2 3
4 5 6
2倍にしたデータ
1 1 2 2 3 3
1 1 2 2 3 3
4 4 5 5 6 6
4 4 5 5 6 6
一方でzero padding upsampleは次のようになります。
元のデータ
1 2 3
4 5 6
2倍にしたデータ
1 0 2 0 3 0
0 0 0 0 0 0
4 0 5 0 6 0
0 0 0 0 0 0
poolingで例えると、上がaverage poolingで下がmax poolingみたいな感じです。
zero padding upsampleの実装方法は、全てが値が0のチャネルを追加して、Pixel Shufflerで並べ替えることで実現しています。

・プーリング
poolingはmax poolingを使います。

・畳み込み
convolutionはストライドが1のものを使います。
generatorにおいてupsampleした直後は5x5のカーネルを使い、それ以外では3x3カーネルを使います。

・活性化関数
generator/decoderの中間層ではmax(-1.0,x)を使い、出力層ではidentity(x)を使います。
discriminator/encoderの最初の層はidentity(x)を使い、それ以降はmax(0.0,x+0.5)を使います。
reluはmax(0.0,x)と表されるので、上の活性化関数はreluを左下にシフトしたもので、下の方はreluを左にシフトした活性化関数です。

・入力
画像は255.0で除算して[0,1]とします。
ノイズは[-1,1]の一様乱数です。

・損失関数
generatorとdecoderは重み共有しているので、損失関数は3個必要となります。

encoderの損失関数は次のようになります。
loss_enc = (Enc(Gen(z))-z)^2 + weight_decay
weight_decay = W^2*const(W^2)
ノイズzをgeneratorに通して生成した画像をencoderに通してもとのzとの二乗誤差を最小にします。
weight decayはWにしか適用せず、bには適用しません。
また、最終層のWにも適用しません。
weight decayを追加するときに、元の損失関数とのバランスをとるために、適当な値(0.01~0.00001)を掛けます。
この値をどのくらいにすればいいか分からなかったので、重み自身の二乗を定数として掛けることでバランスをとるようにしました。
定数なので、逆伝播の計算時には微分しません。
TensorFlowではtf.stop_gradient()で定数化します。

discriminatorの損失関数は次のようになります。
loss_dis = [(Dis(x)-1.0)^2;(Dis(x_fake)-(-1.0))^2;(Dis(x_dec)-(0.0))^2] + weight_decay
少し見づらいですが、説明していきます。
[x;y;z]はxとyとzを一つのミニバッチまとめにしていることを意味します。
別々に計算して後で足し合わせる、という方法ではうまくいきません。
また、x_fakeはあらかじめノイズzをgeneratorに通してnumpy arrayにしたものを再びネットワークに通しています。
x_decも同様にあらかじめ本物の画像xをencoderに通してdecoderに通したものをnumpy arrayにしています。
そしてxとx_fake、x_decをnp.concatenate()で一つにまとめてdeiscriminatorに入力します。
Dis(x)は1を出力するようにして、Dis(x_fake)とDis(x_dec)は-1を出力するようにします。
LSGANの論文では1と0でもいいように書いてありますが、そちらではうまくいきませんでした。
ちなみにdiscriminatorとgeneratorは対称になっているので、discriminatorの出力はgeneratorへの入力と同じ次元だけあります。
weight decayはencoderと同様です。

generatorの損失関数は次のようになります。
loss_gen = (Dis(Gen([z;Enc(x)]))-[0.0;Dis(x)])^2 + (Dec(Enc(x))-x)^2/const(mean( (Dec(Enc(x))-x)^2)*const(mean( (Dis(x)-0.0)^2)  )
右辺の左項は本物画像xをあらかじめencoderに通したものとノイズzを一つにまとめてgeneratorに通して画像化したものをdiscriminatorに通して、出力値が0と本物画像をdiscriminatorに通したときの値となるようにします。
右辺の右項は本物画像xの復元誤差を最小にしています。
左項とのバランスを取るために二乗誤差の平均の逆数を定数化したものと、本物画像をdiscriminatorに通したものの二乗平均を定数化したものを掛けています。

・最適化手法
学習率が0.0001のRMSPropを使います。

・ミニバッチ
ミニバッチに入れるデータ数と構成も重要です。
下の数字の整数倍となるように、ミニバッチを構成する必要があります。
encoderにはノイズを6とします。
disciminatorには本物画像2、偽物画像2、本物画像を復元したものを2とします。
generator/decoderには本物画像を6、ノイズ3、本物画像をencoderに通したものを3とします。

・結果
以前はsugyanさんのアイドル画像データセットを使っていましたが、もう配布をやめてしまったようなので使わないほうがいいと思い、これからはCelebAデータセットを使うようにしました。
CelebAデータセットは論文でも良く使われている、20万枚もの顔画像がある大規模データセットです。
今回はCelebAデータセットの中でも、画像サイズや位置がそろっているものを使いました。
そのままでは使いづらいので、各画像の中央150x150を切り取り64x64に縮小しました。
以下の画像は上から順番に、本物画像、偽物画像、本物画像のアナロジー、偽物画像のアナロジー、となっています。
本物画像のアナロジーは左端と右端が本物画像で、その隣が本物画像をencoderに通したものをdecoderに通したもので、真ん中が両端の画像のencoderの値を適当な比率で足し合わせた偽物画像となります。
偽物画像の中には微妙なものも混じっています。
一応100万ステップ程度学習させてますが、もっとやればもう少し良くなるかもしれません。

 本物画像

f:id:suzuichiblog:20170921134731j:plain

 偽物画像

f:id:suzuichiblog:20170921134756j:plain

 本物画像のアナロジー

f:id:suzuichiblog:20170921134810j:plain

 偽物画像のアナロジー

f:id:suzuichiblog:20170921134820j:plain


・感想
上で書いた全てのコツは経験則に基づいているので、理論的になぜそうなるのかは説明できません。
DCGANをさらにディープにしようと思い立って、いろいろ試しているうちに半年以上かかってしまいました。
なんか時間をかなり無駄にした感があります。
なぜかというと、この半年程度の間にWGAN-GPのようなディープでもきちんと学習できる方法が発表されてしまったからです。
まあ今更愚痴ってもしょうがないですが。
適当に書いたものですがソースコードgithubに上げておきました。

ただソースコードはpython2と古いtensorflowで書かれているので、python3や最新のtensorflowでは動かないかもしれません。

Batch Normalizationを使わないDCGAN

自己符号化器を使った事前学習をDCGANに適用したものは、顔のようなバリエーションが比較的少ないものならば機能しました。
しかし背景画像のようなバリエーションが豊富なものにはうまく機能しませんでした。
本来ならDCGANの論文にあるようにBatch Normalizationを使うべきなのですが、TensorFlowでの使い方がよくわかりませんでした。
tf.contrib.layers.batch_normを使ってみましたが、おそらく私の実装ミスのせいでうまくいきませんでした。
そこでいろいろと試行錯誤をして良い感じの設定にすることで、Batch Normalizationを使わなくても画像を生成させるのに成功しました。

1.重要なポイント
・最適化手法:Adamを使い学習率を0.0002に、beta1を0.5に設定する。DCGANではお決まりの方法です。


・discriminatorの出力:discriminatorの出力はオリジナル画像ならば0をgeneratorが作った画像ならば1を出力するようにトレーニングする。

 

・ネットワークの構造:disriminatorとgeneratorのネットワークは対称となるようにしなければなりません。どちらか一方のフィルタ数を多くするとうまくいきません。


・活性化関数:generatorではeluを、discriminatorでは傾き0.2のleaky reluを使用する。ただしどちらも最終層では恒等関数にする。活性化関数をreluにしたり、両方eluにしたりするとうまく生成されません。


・重みの初期化:Xavierの方法(名前は違うかもしれません)で初期化する。標準正規分布の乱数にscaleを掛けたものを初期値とする。scale=sqrt(2.0/s)で、sは現在の層のニューロンと前の層のニューロンがいくつつながっているかを示している。畳み込みの場合には、s=カーネルの高さ×カーネルの幅×入力チャネル、となる。全結合の場合には前の層のニューロン数をsとする。詳しくは「ゼロから作る Deep Learning」の本かgithubにある実装を見てください。


・Global Average Pooling:Discriminatorで数回畳み込んだ後で全結合ではなくGlobal Average Poolingを使用して出力層と結合する。Global Average PoolingについてははじめてのGANがわかりやすいです。


・入力:入力ベクトルzは標準正規分布の乱数を使用する。-1から1までの一様乱数の方がいろんな画像が出力されやすいです。

2.実験結果
今回はPlaces2というデータセットを利用しました。
このデータセットは学術研究か教育目的においてのみ使用できます。
たぶんこの記事も学術研究にぎりぎり当てはまるので使用しました。
Place2には水族館や城、ベッドルームなど多くのカテゴリのデータがあります。
今回はより複雑なデータから画像を生成できることを示すために、その中から「ベッドルーム」「塔」「城」「水族館」「日本庭園」の5カテゴリのデータを使用しました。
一応それっぽいのは生成できましたが、日本庭園感が強すぎますね。
他のデータはどこへ行ってしまったのでしょうか?
それに全体的に似たり寄ったりな画像が出来てしまいました。 途中までは割りといろんな画像が生成されるのに、学習を多くさせると同じような画像しか生成しなくなってしまう。

生成画像

f:id:suzuichiblog:20170215155806j:plain

 

DCGANでよくあるアレ

f:id:suzuichiblog:20170215155826j:plain

 

3.感想など
ソースコードgithubにあげてあるのでご自由にお使いください。削除しました。
正直言ってなぜこのような設定でうまくいくのか分かりませんが、とりあえず生成できて安心しました。
今後はきれいに画像を拡大できるようにしたいです。
そうすれば今回作った64x64の画像を512x512のような大きな画像に出来るはずです。

ディープラーニングを使わない顔認識まとめ


sugyanさんのアイドルデータセットで99.6%程度出たのでまとめておきます。
精度としては1200枚中4枚しか間違わないレベルです。
ちなみに間違えた4枚は次の通りです。

f:id:suzuichiblog:20170125180125j:plain


1.データの水増し
基本的にデータ数は多いほうが良いのでちょっとしたテクニックを使って増やしておきます。
左右反転した画像もトレーニングデータに加えます。
またガンマ補正によって少し暗くしたり明るくした画像もトレーニングデータに加えます。

2.顔検出
顔検出ではdlibを使います。
OpenCVにも顔検出機能がありますがdlibの方が良いでしょう。
理由としてはOpenCVはインストールが面倒で、精度がdlibより低く、次のステップで使う顔の特徴点検出機能がない、という点が上げられます。
python版のdlibならばpip install dlibですぐに使えます。

3.顔の特徴点検出
目の端や鼻の頂点などの特徴点を検出しておきます。
dlibの場合はhttp://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2からdatファイルをダウンロードしておく必要があります。

4.顔の位置あわせ
画像のサイズや、顔の傾きなどが合っているほうが顔認識がしやすいので、回転・平行移動・拡縮をしておきます。
procrustes analysisや参考文献[1]のAppendix Dの方法で相似変換行列(similarity transform matrix)を求めます。
だいたい画像の一辺が230ピクセル程度になるようにしておきます。

5.パッチの切り取り
ステップ3で検出した特徴点の中から、輪郭を除いて適当に13箇所程度選びます。
その点を中心として50x50のパッチを切り取り30x30に縮小します。
ただしトレーニング時には60x60で切り取り36x36に縮小し、この領域内から30x30を切り出します。
こうする理由は特徴点の位置が多少ずれてもきちんと顔が認識できるようにしたいからです。

また各データごとにパッチごとに、チャネル別に平均を求めて、その値を引いておきます。

データの各値はあらかじめ255で割っておきます。

6.特徴量の抽出(DeepID)
DeepIDについては以前の記事を参考にしてください。
ただ今回はverificationは入れていません。
入れても入れなくてもあまり結果に影響しないみたいなので、計算量削減のため省きました。
また、畳み込み層と隠れ層の後の活性化関数をreluや恒等関数からeluに変更しました。
畳み込み層のカーネルサイズは3x3でパディングはありません。
チャネル数も入力層から近い順に10、20、30としています。
重みの初期値はXavierの方法にしました。
最適化手法はAdamを使いました。

特徴点ごとに300次元の特徴量が取れるので、13箇所の合計で3900次元の特徴ベクトルとなります。

この値は少し大きいので0.1を掛けておきます。

7.特徴量の加工
ステップ6で取れた特徴量をそのまま使用しても結構良い精度がでますが少し手を加えます。
具体的には特徴ベクトルの多くの値を0(スパース)にして、全ての値が0以上を取るようにします。
これを実現するために、入力層→隠れ層→出力層というネットワークを作りました。
入力は各特徴点で得られた300次元ベクトルです。
隠れ層も300次元で活性化関数にReLUを使います。
出力層は40人の分類問題なので40次元として、活性化関数にsoftmaxを使います。
ネットワークのトレーニング後に隠れ層の出力を新たな特徴量とします。
このネットワークを特徴点ごとに作るので、13個作ることになります。
新たに得られた300次元の特徴量を連結して3900次元にします。

この値は少し大きいので0.1を掛けておきます。

8.識別器のトレーニング
識別器は2クラスのsoftmax回帰としました。
実際は、片方の重みをwとしたときに、もう片方の重みが-wとなるようにしたsoftmax回帰で、これをlogistic回帰風に実装しました。
説明が下手なのでうまく実装方法を言えませんが、とりあえずsoftmaxにしとけば大丈夫です。
特に制約の無いsoftmax回帰を普通にやれば片方の重みはもう片方の重みにマイナスを掛けたものになると思います。
この識別器をクラス数分用意します。

9.テスト
以上のステップで作ったネットワークを使って、最終的に一番高い値を取ったクラスをその画像の人だとします。
ただしテストに使うのは元の画像とそれを左右反転した画像で、出力確率の和が一番大きいクラスをその画像の人だとします。

10.感想
なんとなく顔認識の勉強を始めて1、2年程度経ちました。
そして今回結構良い成果を出せたと思います。
とはいっても、まだまだ問題点があります。
一つ目は、一応40クラスの中では一番高いスコアを出していても、その値が1e-10のような小さい値を取っているものもあります。
つまり40人の内の誰でもないということになってしまっています。
二つ目は、遮蔽物や表情などの変化に対応できていない点です。
最初にあげた誤識別例のように、メガネやマスクなどで顔の一部が隠れる場合や、目を瞑ったり口をあけたりして顔の形が変わってしまう場合があります。
あるいは結構斜めを向いていたり、顔検出で顔がアップになりすぎたりして、特徴点がうまく検出できない場合もあります。
そういった状況になるとうまく認識が出来なくなってしまいます。
たぶん、こういった問題を解決する手法が出てくると思うので、そういった論文が出たらまた実装しようかと思います。

あまりに良い結果過ぎるのでプログラムミスがあるかもしれませんが、その場合はご容赦ください。

参考文献
[1]Cootes, Tim, E. Baldock, and J. Graham. "An introduction to active shape models." Image processing and analysis (2000): 223-248.

自己符号化器を用いたDCGANの事前学習

注意:まだ研究中なのでこのページの内容は間違っていたりしてると思います。
2016年12月2日追記: やはり背景画像のようなバリエーションが豊富なものに対してはうまくいきませんでした。そこで今は別の方法を検討中です。

2016年12月16日追記:いろいろと試してみましたが背景画像にはどれもうまくいきませんでした。

今さらですがDCGANに手を出してみました。
ただ試すだけでは味気ないので少しアレンジしてみました。
データはsugyanさんのアイドル画像データセットを使いました。

1.前置き
昨年の今頃にDCGAN(Deep Convolutional Generative Adversarial Networks)が話題となっていました。
DCGANとは、一様乱数の100次元ベクトルからきれいな画像を生成するgeneratorを作る、というものです。
例えばDCGANを使えば、アニメ顔を生成したり、アイドルの顔を生成したりできます。
DCGANの詳しい内容はこちらのページをご覧ください。
ところで、こちらのページによれば、DCGANは学習が難しいらしいです。
うまいこと学習させるにはパラメータを調整したりすることが必要みたいです。

この記事では、自己符号化器(Autoencoder)を使ってDCGANのgeneratorとdiscriminatorを事前学習します。
さらに、DCGANのdiscriminatorの出力層を複数にします。
その結果、調整が必要なパラメータは学習率と畳み込み層のフィルタ数くらいで、batch normalizationがなくてもAdamのbeta1を調整しなくてもDCGANをうまいこと学習させられるようになりました。

2.DCGANと自己符号化器
10年ほど前では、DeepLearningで扱うような深いネットワークはうまいこと学習できませんでした。
しかし、事前学習を行うことで、そういったネットワークでもきちんと学習が進むということが分かりました。
そこで私は、学習が難しいDCGANでも事前学習をすればきちんと学習が進むのではないか、と考えました。
事前学習の方法の一つに自己符号化器があります。
自己符号化器とは、入力と出力が同じになるようにネットワークを学習させるというものです。
ここでは、画像を入力して、100次元ベクトルに圧縮して、元の画像に近い画像を出力する、という自己符号化器を考えます。
入力画像を100次元ベクトルに圧縮する部分をエンコーダ、100次元ベクトルから出力画像を生成する部分をデコーダと呼びます。
このデコーダの部分はDCGANのgeneratorと同じような働きをします。
そのためDCGANのgeneratorの初期値に、通常の方法でトレーニングした自己符号化器のデコーダのウェイトをセットします。
そして一様乱数ベクトルをデコーダに通した出力と本物の画像とを区別できるようにDCGANのdiscriminatorをあらかじめ学習させておけば事前学習が完了します。

3.実装上の注意点
これを実装していく上での注意点は以下の3点です。
1点目はdiscriminatorの出力層の数です。
2点目は活性化関数についてです。
3点目は学習率についてです。
DCGANのdiscriminatorの出力は畳み込み層の数だけ作ります。
これはDeepID2+という方法をぱくったものです。
以前書いた顔認識の記事に書いてあるので、詳しくはそちらをご覧ください。
出力層が一つの状態で、エンコーダやデコーダ、generatorの出力、discriminatorの出力層を除く箇所全てで活性化関数にreluを使うと白い画像しか生成されなくなります。
これを避けるために、discriminatorに複数の出力層を設けて誤差を複数個所から逆伝播させます。
ただし全ての箇所でreluを使わなくても、DeepID2+のように出力数を複数にせずに自己符号化器で事前学習すると、最初はうまくいきますが最終的には白い画像を出力します。
次に、活性化関数についてです。
上で述べたような箇所全てでeluやtanhを使うと、格子状やドット状のノイズが出てきてきれいな画像が生成されません。
ただし一部で使うだけならばノイズが出てこない場合もあります。

最後は学習率についてです。
自己符号化器は、学習率が大きすぎると平均画像しか出力しなくなるので、1e-4程度に小さく設定します。
DCGANのgeneratorは学習率が高すぎると格子状のノイズしか出力されなくなるので、1e-4程度に小さくします。
discriminatorでも学習率が高すぎると、学習が進んでくるとgeneratorがおかしな画像を生成したりするようになるので、学習率を1e-4程度に小さくしておきます。
以下に学習初期のgeneratorの出力画像の一例を示します。
右にあるように、ノイズが多いが顔に見えるようなものが出てきていれば学習はうまくいきます。
ただしバッチサイズが10の場合で、数万ステップ程度待たなければまともな画像が出てこないので、我慢強く待ちましょう。
左にあるような灰色の画像や、顔っぽいものすらない画像が出力された場合は、学習率を下げることが必要になります。

f:id:suzuichiblog:20161201061558j:plain



実装について詳しりたい方はgithubにあるソースコードをご覧ください。
ただし、TensorFlowにもpythonにもまだ不慣れなので、おそらくソースコードは非効率な書き方をしてると思います。
dcgan_with_ae.pyはdiscriminatorの出力層が一つのもの、dcgan_with_ae_multi_output.pyは出力層が複数のものです。

4.結果と感想
実験結果は次の通りです。
事前学習がないと顔すら現れません。
一方で事前学習を行ったほうはきちんと顔が出来ています。

f:id:suzuichiblog:20161201061615j:plain

DCGANでよくある二つのzの遷移画像

f:id:suzuichiblog:20161201062958j:plain



DCGANを使うのは何番煎じかわかりませんが、自己符号化器を使うDCGANの事前学習は二番煎じくらいにはなっているんじゃないかなと思います。
(論文を探してないので分かりませんが、きっとどこかの誰かが既にやっているでしょう。)
もともとDCGANのパラメータ調整をしたくなかったのに、結局学習率というパラメータを調整しなければならなくなってしまいました。
自己符号化器やgeneratorの学習率は高くしすぎるとすぐにおかしな画像が出てくるので調整しやすいです。
一方でdiscriminatorは学習がかなり進んだ後でないとおかしな画像を出力しないので調整が面倒です。

今回うまくいったのは、顔という変化のパターンが少ないものだったからかもしれません。
そのため今後は風景などのパターン数が多そうなものでも学習できるかを確認していきたいです。

5.おまけ
自己符号化器とDCGANの設計図(?)を載せておきます。
まずは自己符号化器です。
自己符号化器は入力画像を5回畳み込んで100次元ベクトルにして、5回逆畳み込みして画像を出力します。
逆畳み込み(deconvolution)は転置畳み込み(?)(transposed convolution)とも呼ばれてるみたいです。
逆畳み込みについては[1]がわかりやすいです。
活性化関数は、エンコーダとデコーダの最後の層ではtanhで、それ以外ではreluです。

f:id:suzuichiblog:20161201061627j:plain



次はDCGANのgeneratorと自己符号化器のデコーダとの違いについてです。
最初にも書きましたが、DCGANのgeneratorと自己符号化器のデコーダは同じような働きをします。
しかし、入力が一様乱数か否か、あるいは生成画像がきれいかどうかという点で異なっています。
generatorの活性化関数は、最後の層ではtanhで、それ以外ではreluです。

f:id:suzuichiblog:20161201061640j:plain



最後はDCGANのdiscriminatorです。
4回畳み込んで、畳み込むごとにシグモイド関数で[0,1]の値を出力させます。
DCGANのdiscriminatorを学習させるときは、オリジナル画像を入力すると1を、偽画像を入力すると0を出力するようにします。
このようにすることで、DCGANのgeneratorが出力する画像を元画像のようにできます。
discriminatorの活性化関数は、畳み込み層ではreluで、出力層ではシグモイド関数(入力にウェイトを掛けてバイアスを足したものをそのままtf.nn.sigmoid_cross_entropy_with_logitsに入れています)です。

f:id:suzuichiblog:20161201061653j:plain



[1]Dumoulin, Vincent, and Francesco Visin. "A guide to convolution arithmetic for deep learning." arXiv preprint arXiv:1603.07285 (2016).

ディープラーニングを使わない顔認識3 CNN編

「ゼロから作るDeepLearning」を読んで畳み込みニューラルネットワーク(CNN)を実装したので顔認識で試してみました。
この本自体もgithubにあるソースコードも読みやすいのでお勧めの一冊です。
(私は、amazonでは品切れだったので、yodobashi.comで買いました。yodobashi.comには今も在庫があるようです。)
今回はCNNを使っていますが、3回しか畳み込み+プーリングを行わなわず、そこまでディープではないので、ディープラーニングを使わないというタイトルにしました。
その結果sugyanさんのアイドルデータセットのcase5で98%超の精度が出ました。
また、トレーニング全体が終わるまでに1日程度かかりました。
以下に方法を書いていきます。

1.データの水増し data augmentation
2.顔検出 face detection
3.顔の特徴点検出 facial landmarks detection
4.顔の位置合わせ face alignment
以上の4つのステップは以前の記事とほぼ同じです。
ただし、データの水増しは左右反転のみです。
画像サイズは約200x200です。

5.パッチの切り取り crop patches
ステップ4で検出した各特徴点のうち顔の輪郭部分は使わず、それ以外の部分から15箇所を適当に選びます。
それらの点を中心として60x60のパッチを切り取り40x40に縮小します。

6.特徴量の抽出 feature extraction
特徴量の抽出にはCNNを使いました。
CNNには様々なネットワーク構造が考えられますが、DeepID2+に近い方法を取りました。
DeepIDシリーズにはDeepID[1]、DeepID2[2]、DeepID2+[3]、DeepID3[4]などの方法があります。
どのDeepIDでも基本的にはネットワーク構造は同じです。
(DeepID3は他3つのネットワーク構造をさらに深くしたものです。)
今回使ったネットワークの基本形は、input->conv->relu->pool->conv->relu->pool->conv->relu->pool->affine->relu->affine->softmaxwithloss、です。
ちなみにaffineは全結合層のことです。
DeepIDシリーズでは4回畳み込みを行い、4回目の畳み込みの出力と3回目のプーリングの出力を隠れ層につなぐ、という方法を取っています。

f:id:suzuichiblog:20161031220725j:plain


[1]の図2
しかしそれを実装するのは少し面倒なので上記のような一般的なCNNの構造にしました。
一般的なCNNでは最終層からのみ教師信号を送りますが、DeepID2+では途中からも教師信号を送ります。
上の構造では最後のプーリング層の後に隠れ層・出力層があり、そこから教師信号を送っています。
これを、全てのプーリング層の後に隠れ層・出力層を追加して、そこからも教師信号を送ります。
図にすると次のようになります。

f:id:suzuichiblog:20161031220744j:plain


[4]の図2
さらにDeepID2+ではidentificationの教師信号だけでなく、verificationの教師信号も送ります。
identificationとは入力された顔の画像が誰であるかを答えるものです。
入力画像が、その人に対応するノードの出力が1に、それ以外の出力が0に近づくように学習します。
一方でverificationは入力された二つの顔画像が同一人物であるかを答えるというものです。
二枚の画像の類似度(今回はユークリッド距離)が、同じ人ならば0に近づくように、違う人ならばある閾値以上の値となるように学習します。
verificationの教師信号も追加したネットワーク構造は次のようになります。
ただし、verificationの教師信号には定数λ=0.05を掛けて逆伝播しています。
下図でIdはidentificationの教師信号、Veはverificationの教師信号を表しています。

f:id:suzuichiblog:20161031220755j:plain


[3]の図2
各層の設定は次の通りです。
input:3チャネル(カラー画像)、サイズ40x40
conv:フィルタサイズ5x5、ストライド1、パディングなし、出力チャネルは入力層から近い順に20・40・60
pool:フィルタサイズ2x2、ストライド2、パディングなし、maxプーリング
affine(隠れ層):ノード数100
affine(出力層):ノード数40(40人の分類問題なので)
DeepID2+では最後の隠れ層(のReLU後)の値を特徴ベクトルとして扱いますが、ここでは全ての隠れ層(のReLU後)の特徴ベクトル(3箇所)を連結したものを特徴として扱います。
つまり3x100で300次元の特徴ベクトルになるということです。

入力画像の前処理としては、正規化と平均減算を行いました。
正規化とは各値が[0,1]となるようにすることです。
画像では各値が[0,255]という値を取っているので、各値を255で除算しました。
平均減算は、データごとに別々に平均を求め、その平均を各値から引くというものです。

そして、ステップ5で作った15種類のパッチを使い別々にCNNをトレーニングします。
すると画像1枚につき特徴ベクトルとして300x15=4500次元のベクトルが出来ます。

7.識別器 classifier
ステップ6で得られた4500次元の特徴ベクトルを使い、二値分類器であるlogistic regressionを人数分(ここでは40人分)作ります。

8.テスト test
ステップ6で作ったCNNにテスト画像を入れて特徴ベクトルを計算し、それをステップ7で作ったlogistic regressionに入れます。
その結果一番高い値を取ったクラスを、その画像が所属するクラスとします。


以上のような方法を取ることで結構良い精度の結果を得ることが出来ます。
ただ切り取るパッチのサイズや数をどうすれば良いかというのがよくわかりません。
あとはこれが1600クラス分類にも適用できるかが問題となります。
またWebサービスとして我慢できるスピードで出来るかも問題となります。
とりあえず作ってみて、AV女優顔認識システムにCNN版を追加してみようと思います。

 

2016年11月9日追記:
1600人分類をしましたが、いくつか変更を加えました。
まずは、CNNの畳み込み層の出力チャネルを10・20・30としました。
次に、隠れ層の後の活性化関数をReLUから恒等関数(入力と出力が同じ)にしました。
CNNのトレーニングに4日、識別器(logistic regression)のトレーニングに3日程度かかりました。
以前よりは精度が改善されたと思いますが、まだまだ精度が低い気がします。




参考文献
[1]Sun, Yi, Xiaogang Wang, and Xiaoou Tang. "Deep learning face representation from predicting 10,000 classes." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2014.
[2]Sun, Yi, et al. "Deep learning face representation by joint identification-verification." Advances in Neural Information Processing Systems. 2014.
[3]Sun, Yi, Xiaogang Wang, and Xiaoou Tang. "Deeply learned face representations are sparse, selective, and robust." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2015.
[4]Sun, Yi, et al. "Deepid3: Face recognition with very deep neural networks." arXiv preprint arXiv:1502.00873 (2015).

ディープラーニングを使わない顔認識2

今回は前に書いた方法を改良したので、その方法を書いておこうと思います。
その前に以前のはてブのコメントで処理負荷を知りたいという意見があったので書いておきます。
使用したデータはsugyanさんのアイドルデータセットです。
(何度も使ってすみません。。。)
このデータセットは40人のアイドルの画像がそれぞれ180枚あります。
ここでは150枚をトレーニングに使う画像、30枚をテスト用の画像としました。
またトレーニングでは反転画像も使用したので枚数は上記の2倍です。
このデータセットでは既に、顔検出と位置あわせが済んでいるのでその部分はスキップしました。
pythonで作った簡易版ですが、特徴量を取り出すのに約10分、40クラス分のlogistic regressionのトレーニングに約20分、テストに約120秒です。
テスト画像は30x40=1200枚なので、40クラス問題ならば1枚あたりの処理時間は約0.1秒ということになります。
精度に関しては、簡易版とは言っていますが、OpenCV付属の顔認識機能よりは良いです。
OpenCVではEigenface、Fisherface、LBPHの3種類があります。
これに上記のデータセットを流し込んだだけですが、精度はそれぞれ40%、61%、41%でした。
一方で私の方法は94%なのでsugyanさんの96%には劣りますが、それなりの精度は出ていると思います。
(ちなみにC++で作ったオリジナル版では97%が出ています。)

で、本題ですが実はそれほど改良したわけではありません。
改良した部分は標準化の部分とlogistic regressionの重みの部分です。
標準化では各次元の値を平均0分散1となるようにします。
そのときに、たまたま大きな(あるいは小さな)値を取ったために結果に影響してしまう、という状況を避けたいというのが動機です。
そこで標準化の結果1以上(あるいは-1以下)になったら、強制的に1(あるいは-1)にセットしなおします。

logistic regression、というかニューラルネットワークでは重み上限という方法があります。
この方法は重みの二乗和をある値以下に制限するというものです。
以前はこの方法を使っていましたが、上記の方法と同様に、各重みの値が0.1以上(あるいは-0.1以下)になったら、強制的に0.1(あるいは-0.1)にセットしなおすことにしました。
とりあえず、こんな方法で精度が1%程度上がりました。

 


それからせっかくなのでpythonで作った簡易版のソースコードを公開しました。
使い方を以下に書いておきます。
1.パッケージのインストール
以下のパッケージを使うのでpipコマンドでインストールします。
dlib
skimage
PIL
numpy
scipy

2.スクリプトとデータのダウンロード
githubのページからスクリプトをダウンロードします。
dlibの顔の特徴点検出機能を使うので、http://dlib.net/files/shape_predictor_68_face_landmarks.dat.bz2をダウンロードして解凍して、スクリプトと同じとこに置きます。

3.データの用意
スクリプトと同じ場所に"data"フォルダを作ります。
"data"フォルダの下に、"test"フォルダと"train"フォルダを作ります。
それぞれのフォルダの下に、各人のフォルダを作ってデータを格納します。
具体的には以下のような構造になります。

.
├── align.py
├── data
│   ├── test
│   │   ├── person1
│   │   │   ├── p1_01.jpg
│   │   │   └── p1_02.jpg
│   │   ├── person2
│   │   │   ├── p2_01.jpg
│   │   │   └── p2_02.jpg
│   │   ├── person3
│   │   │   ├── p3_01.jpg
│   │   │   └── p3_02.jpg
│   │   └── person4
│   │       ├── p4_01.jpg
│   │       └── p4_02.jpg
│   └── train
│       ├── person1
│       │   ├── p1_01.jpg
│       │   └── p1_02.jpg
│       ├── person2
│       │   ├── p2_01.jpg
│       │   └── p2_02.jpg
│       ├── person3
│       │   ├── p3_01.jpg
│       │   └── p3_02.jpg
│       └── person4
│           ├── p4_01.jpg
│           └── p4_02.jpg
├── hog.py
├── lr.py
├── shape_predictor_68_face_landmarks.dat
└── train_and_test.py

 

4.実行
あとは"python train_and_test.py"コマンドを実行すればそのうち終わります。
また、デフォルトでは顔の検出をしますが、必要ない場合はtrain_and_test.py内で"FACE_DETECTION_FLAG = False"としてください。
同様に、デフォルトは顔の位置あわせをしますが、必要ない場合はtrain_and_test.py内で"ALIGN_FLAG = False"としてください。

最後に宣伝ですが、AV女優の画像検サービスをやっているのでよろしくお願いします。

 

 

トレーニングデータ数と正解率との関係

今回はトレーニングデータ数と正解率との関係を調べました。
使用したデータはsugyanさんのアイドルデータセットです。
このデータセットは40人のアイドルの画像がそれぞれ180枚あります。
ここでは150枚をトレーニングに使う画像、30枚をテスト用の画像としました。
トレーニング・テストの手法はHOGを使った(たぶん)オリジナルの方法で、前回のものを少し改善しました。
ただし、pythonで作った簡易版なのでC++で作ったオリジナル版には数%ですが劣ります。
ここで使用した手法は後日詳しいやり方を書こうと思います。

トレーニングデータ数を一人当たり1、5、10、30、60、90、120、150枚と変えたときの正解率を示したものが下の図になります。
ただし、トレーニングに使用した画像は反転したものも含めるので上記の2倍の枚数となります。
またテスト画像は30枚で固定しています。
あたりまえかもしれませんが、データ数を増やしたほうが正解率が上がります。
ただし、30枚の時点でほぼ9割なので40クラス問題の場合にはそれほど多くのものは必要ないのかもしれません。
あくまで40クラス問題でなので、100クラスや1000クラス問題となればトレーニングデータ数は150枚でも足りないのかもしれません。

 

f:id:suzuichiblog:20160908180916p:plain

 

ちなみにトレーニングデータ150枚を使い、OpenCV付属の顔認識手法のEigenFaceやFisherFace、LBPHで試した結果、正解率はそれぞれ40%、61%、49%でした。
ただし、画像は32x32と小さいサイズです。
なぜこんなに小さいかというと、あまり大きい画像だとトレーニングに時間がかかるからです。
例えば32x32ならば20~30分程度でトレーニングとテストが終わりますが、100x100の画像にすると3時間経ってもトレーニングが終わりませんでした。(面倒になったので途中で停止させました。)
ちなみにpythonの簡易版ですが私の手法ではトレーニングが30分程度、40人x30枚=1200枚のテストが120秒程度で終わります。
(今回使用したデータセットは既に顔検出と顔の位置あわせが済んでいるため、ここではこれらのステップを飛ばしています。そのため実践的にはもう少し時間がかかるかもしれません。)

感想としては、トレーニング手法の改善ももちろん大事ですがデータ数も大事だと思いました。
そのため最近は画像を生成する手法であるDCGANに注目しています。
例えば、アイドルの顔画像を生成したりアニメのキャラクターの顔を生成したりするものがあります。
私もDeepでもConvolutionalでもないただのGenerative Adversarial Networksっぽい物を作りました。
試しで作ったので識別器も生成器も1層のニューラルネットなのでGANもどきと呼んでいましたが、さすがにそんなのではうまく行きませんでした。
なぜうまくいかなかったことをわざわざ書いたかといえば、GANもどき→がんもどき→おでんの具みたいというネタを書きたかったからです。
それはさておき、例えばある人のトレーニング画像が無表情の1枚しかなくても、他の人の画像に笑顔のものがあれば、そこからある人の笑顔の画像を作り出せたらデータ数に困ることが無くて便利そうだなと思います。
そんなわけで、今後はデータを生成する手法にも注目していきたいと思いました。