TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

DCNNによるマンガキャラクターの顔パーツ検出(追試2)

前回の日記で初期値の変更や正則化やBatch Normalization、フィルター枚数などを変更した場合の効果について調べた。

ここでは、さらに畳み込み層のゼロパティングの有無とカーネルサイズの影響について調べた。

ゼロパティング

畳み込み層のパディングは、一般的に入力サイズと出力サイズを同じにしたい場合に行われる。
しかし、パディングを行うと周辺部の値に良くない影響が出る。
今回の顔パーツ検出のような回帰問題では、あえてパディングを行う必要がないと考えられるため、前回まではパディングを行わなかった。

ここでは、パディングを行った場合の効果について念のため確かめた。
パディングの値として0を使用するゼロパティングを行った。

ゼロパティング training loss test loss
なし 2.97E-05 3.94E-04
あり 3.26E-05 4.22E-04

※1000エポック後のloss
予想の通り、ゼロパディングを行うとlossが増える結果となった。

カーネルサイズ

畳み込み層で使用するカーネルサイズは画像から特徴を抽出するフィルターのサイズとなる。
カーネルサイズが小さくなると表現できるフィルターのパターンが少なくなる。
逆にカーネルサイズが大きいと、より大きなパターンのフィルターとなるが、大きな範囲で特徴を抽出することになり、汎化能力が落ちることが予測される。
また、パディングを行わない場合、畳み込み層の出力のサイズは -カーネルサイズ+1 となるため、カーネルサイズが大きいと層が進むにつれサイズが小さくなることも悪影響があると思われる。

前回までは1,2,3層のカーネルサイズを他の論文を参考にして、4,5,5としていた。
ここでは、それよりも小さいサイズと大きなサイズで数パターン調べた。
結果は以下のようなグラフになった。
f:id:TadaoYamaoka:20170207234225p:plain

小さい場合は明らかにlossが増えている。
大きくした場合もlossが少し増えていそうである。

この結果からは、3,4,4が最もtest lossが小さくなった。
ただし、測定1回のみの結果なので、誤差の範囲かもしれない。
また、カーネルサイズが大きい場合もさらに学習を進めれば、最終的な検出性能は向上するかもしれない。
(論文とか見てもカーネルサイズは説明なしに決められているが、何かノウハウがあるのだろうか?)

このように、最適なハイパーパラメータを決めるには、グリッドサーチなどの手法で複数の値の組み合わせで実験が必要になる。
今回はある程度傾向が見えたのでここまでとする。

DCNNによるマンガキャラクターの顔パーツ検出(追試)

前回の日記では正則化など行わずにRMSpropを使用して学習を行ったが、初期値の変更や正則化やBatch Normalizationなどを行った場合にどうなるか試してみた。

初期値の変更

ChainerのConvolution2Dのパラメータの初期値は、デフォルトではHeの初期値で初期化される。
これは活性化関数がReLUの場合に最適な初期値になる。(ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装の解説が分かりやすかった。)
wscaleで初期値の標準偏差の値を変更できる。

wscaleはデフォルトで1.0になっているが、試しに√2にしてみた。

1000エポック学習した後、training lossとtest lossは以下のような結果になった。
※エポック単位でぶれがあるので最後の3エポックの平均値とした。

wscale training loss test loss
1.0 2.97E-05 3.94E-04
√2 3.22E-05 5.18E-04

lossが増えてしまった。
wscaleは下手に変更しない方がよい。

正則化

Chainerでは

optimizer.add_hook(chainer.optimizer.WeightDecay(0.0005))

のように記述することで正則化を行うことができる。

正則化を行うことで汎化性能が向上する場合がある。
正則化を行った場合の、lossは以下のようになった。

正則化 training loss test loss
なし 2.97E-05 3.94E-04
あり 9.16E-04 9.89E-04

正則化を行わない方が、test lossが小さくなっている。
テスト用データは学習用データと分けているが、同じコミックの画像を使っているので、汎化性能の効果が見れていないだけかもしれない。
顔パーツ検出のような回帰問題では、正確な位置が必要なため、正則化が悪影響を与えているかもしれない。
もう少し検証してみる必要がありそうだ。

Batch Normalization

Batch Normalizationを行うことで過学習の抑制ができる。
畳み込み層にBatch Normalizationを入れてみた。

Batch Normalizationを入れた結果、以下のようになった。

Batch Normalization training loss test loss
なし 2.97E-05 3.94E-04
あり 9.14E-05 6.14E-04

training loss、test lossともに悪くなった。
回帰問題には適さないのかもしれない。

フィルター枚数を倍

フィルター枚数を倍にすることでどれくらい精度が上がるか試してみた。

フィルター(1,2,3層目の枚数) training loss test loss
16,32,64 2.97E-05 3.94E-04
32,64,128 2.47E-05 4.65E-04

training lossは下がっているが、test lossは悪化している。
過学習しているのかもしれない。

学習時間は、元は約17分で、フィルター枚数を倍にすると、約37分になった。
学習時間の増加に対して精度が向上してない。
フィルター枚数は倍にする必要はないようだ。

学習アルゴリズムをAdamに変更

AdamはRMSpropより新しいアルゴリズムで最近の研究ではよく使用されているらしい。
学習アルゴリズムをAdamに変更した結果、以下のようになった。

正則化 training loss test loss
RMSprop 2.97E-05 3.94E-04
Adam 2.64E-05 3.78E-04

training loss、test lossともに下がっている。
この問題にはAdamの方が適しているかもしれない。
(下記の他のコミックを混ぜた場合はRMSpropの方が収束が速かったので一概に良いとは言えないようだ。)

他のコミックで試してみる

学習したモデルがどれくらいの汎化性能があるか他のコミックで試してみた。

f:id:TadaoYamaoka:20170206221623p:plain

輪郭、目、口ともに大きくずれてしまっている。
一つのコミックから学習しても、汎化性能は獲得できていないようだ。

学習データに他のコミックを混ぜてみる

学習データの20%くらいに他のコミックの画像を混ぜて学習してみた。
f:id:TadaoYamaoka:20170206221842p:plain
結果、見事に検出できるようになった。この画像以外もかなりの精度で検出している。

1000エポック後のlossの値は大きくなったが、学習に時間がかかるようになるのは直観的に納得できる。

他のコミック training loss test loss
なし 2.97E-05 3.94E-04
あり 3.60E-05 4.97E-04


元のコミックの画像の検出精度は見た目上ほとんど変わっていない。
f:id:TadaoYamaoka:20170206225544p:plain

つまり、顔パーツの特徴をより汎用的に学習できたと言える。

いろいろ試した結果、他のコミックの画像をブレンドするのが一番効果が分かりやすかった。

DCNNによるマンガキャラクターの顔パーツ検出

以前の日記でdlibを使用してマンガのキャラクターの顔パーツ検出を行った。

しかし、dlibの方法は、マンガキャラクターには有効ではなく、顔パーツを検出できなかった。

そこで、DCNNを使用して顔パーツ検出(顔器官検出)ができるか試してみた。


dlibで実装されている方法は、くわしくは理解できていないが、論文のタイトルからは回帰木とアンサンブル学習を使用したアルゴリズムのようだ。
実際の人の写真に対してはうまく機能しているが、マンガキャラクターのような線画に対しては機能しないようである。

DCNNは様々な画像に対して汎用性があるため、マンガキャラクターに対しても有効に機能すると思われる。

DCNNは画像の分類に使用される例が多いが、画像分類の場合は、出力として画像の分類に対応するラベルごとの確率を出力することになり、出力層の関数には、softmaxが使用される。

一方、顔パーツを検出する場合は、パーツの検出点(ランドマーク)ごとの位置座標を実数で出力することになる。
そのような場合は、DCNNの出力層を恒等関数、誤差関数を平均2乗誤差(MSE)とし、回帰問題として学習する。

出力層を変えるだけで、画像分類とネットワーク構成は同じで、畳み込み層とプーリング層を複数層つなげる構成とする。

ここでは、ネットワーク構成を、入力層から順に以下の構成とした。

ネットワーク構成
  • フィルター16個、カーネルサイズ4×4、活性化関数をReLUとした畳み込み層
  • Maxプーリング2×2
  • フィルター32個、カーネルサイズ5×5、活性化関数をReLUとした畳み込み層
  • Maxプーリング2×2
  • フィルター64個、カーネルサイズ5×5、活性化関数をReLUとした畳み込み層
  • ノード数400、活性化関数をReLUとした全結合層
  • ノード数を検出点(ランドマーク)数×2とした全結合層

f:id:TadaoYamaoka:20170218110038p:plain

出力層のノード数は、検出点(ランドマーク)のx座標とy座標の値とするため、検出点(ランドマーク)の2倍のノード数となっている。

畳み込み層で、ゼロパティングは行わない。


入力画像は、グレイスケールの100×100の画像とした。

グレイスケールの値の範囲は、0~1(白を0、黒を1)とする。
画像の座標の値の範囲も0~1とする。
これは、値に0~255や0~100を使うと、学習時の計算でオーバーフローが起きるためである。
(実際に試したところ、学習開始後すぐにlossがnanになった。)

学習用とテスト用の画像の準備は、dlibのimglabツール(以前の日記参照)を使用して、手動でコミックから顔画像の矩形範囲と検出点(ランドマーク)を入力して、xmlファイルとして保存する。

これを、画像の中心が顔の中心(左右の眼の中間の位置)になり、画像の幅が左右の目の端から端までの距離の1.25倍となるように正規化する。

学習方法

準備した学習用データからランダムに16個選び、Data Augmentation(以前の日記参照)を行ったものをミニバッチとする。

学習アルゴリズムSGDを使用した。

ChainerとGPU(GeForce 1080)を使用して、ミニバッチを100イテレーション×1000エポック学習させた結果、以下の通りになった。

f:id:TadaoYamaoka:20170204150004p:plain

学習データの損失関数(MSE)の値は、0.00076901まで下がった。
非常に小さい値のようだが、座標の範囲が0~1で、平均2乗誤差であるため、座標のずれとして、100×100倍する必要がある。
つまり、平均2.77(\sqrt{7.69})ピクセルのずれがある。

学習したモデルを使用して、学習用データとは別のテスト用データで、顔パーツを予測すると以下の通りとなった。

f:id:TadaoYamaoka:20170204150240p:plain

検出点(ランドマーク)は、顔の輪郭3点、左右の目それぞれに4点、口に4点である。

目に関しては正確に検出できている。
輪郭の中心と、口の位置がずれているが、たいぶ近い位置が検出ができているのではないだろうか。

画像の傾きにも堅牢なようだ。

しかし、以下のようなデフォルメした表現の場合は、ずれが大きくなっている。

f:id:TadaoYamaoka:20170204150655p:plain

RMSpropで学習

上記ではSGDで学習を行ったが、SGDは収束が遅いため、他のアルゴリズムの方が有効な場合がある。

そこで、RMSpropを使用して学習を行ってみた。

学習係数をデフォルト(lr=0.01)のまま学習させると、以下のように値が発散した後、勾配消失してしまい固定座標しか出力しなくなってしまった。

f:id:TadaoYamaoka:20170204151056p:plain


学習係数を調整して、lr=0.0001としたところうまく学習できた。

f:id:TadaoYamaoka:20170204151302p:plain

学習データの損失関数の値は、0.0000306まで下がった。
SGDの損失関数の値の約0.04倍であり、より早く収束できている。


RMSpropで学習したモデルを使用して、テスト用データの顔パーツを予測すると以下のようになった。

f:id:TadaoYamaoka:20170204151816p:plain

SGDで学習したモデルではデフォルメした目では座標が大きくずれていたが、RMSpropで学習したモデルでは、より近い位置が検出できている。

f:id:TadaoYamaoka:20170204151955p:plain

目と口がはっきりした画像では、かなり正確な位置が検出できている。

まとめ

DCNNによる顔パーツ検出は、マンガキャラクターに対しても有効であることが確かめられた。

使用したソースコードは、GitHubで公開したので、同じことを試したい方は参考にしてほしい。(学習データはアップできないので、各自で用意してほしい。)
github.com


以上、DCNNと顔パーツ検出をオリジナルブレンドしてみた結果報告でした。

音声スペクトルモニター(Audio Spectrum Monitor)をバージョンアップ

Androidアプリの音声スペクトルモニター(Audio Spectrum Monitor)をバージョンアップしました。

前回のアプデートから1年以上更新していませんでしたが、ボーカル音程モニター(Vocal Pitch Monitor)の方で改善した内容を反映しました。

あと、前回の日記で書いたゼロパティングによるスペクトル推定の精度向上も行っています。

play.google.com

ゼロパティングを使用して自己相関関数でのピッチ推定の精度を向上する

前回の日記で自己相関関数によるピッチ推定の精度をN倍の位置のピークを使用することで向上できることを示した。

Nを大きくすることで精度を高めることができるが、Nの上限はフレーム長により制限される。

単純にフレーム長を長くした場合、応答性とのトレードオフとなる。

応答性を変えずにフレーム長を大きくする手法としてゼロパティングがある。(以前の日記参照)

ここでは、ゼロパティングを使用してフレーム長を大きくした場合のピッチ推定の誤差について検証する。

フレーム長を2倍にしてゼロパティングを行い、周波数の範囲を100Hzから900Hzとして、N倍位置のピークから推定したピッチの誤差をcent単位で求める。

size = 8192
t = np.arange(0, size) / fs
han = np.hanning(size)

errors = []
fset = np.linspace(100, 900, 8001)
for f in fset:
    y = np.sin(2 * np.pi * f * t)
    y[size/2:]=0 # ゼロパディング
    Y = np.fft.fft(y*han)
    acf = np.fft.ifft(abs(Y)**2)
    n = pick_peak(np.real(acf[0:size/2]))
    n1 = n
    for N in range(2, int(size / 2 / n1) - 1):
        nn = int(n*N)-10 + pick_peak(np.real(acf[int(n*N)-10:int(n*N)+10]))
        n = nn / N
    f0 = fs / n
    cent = (math.log(f0, 2) - math.log(f, 2)) * 12 * 100
    errors.append(cent)
plt.plot(fset, errors)
plt.show()

f:id:TadaoYamaoka:20170114120638p:plain

比較のために、前回の日記で検証したゼロパディングを行わない場合の誤差のグラフは以下の通りとなる。

f:id:TadaoYamaoka:20170114104521p:plain

100Hzあたりの低い周波数では、誤差のばらつきが大きくなっているが、高い周波数では、ゼロパティングを行った方が誤差が抑えられている。

1cent程度の誤差が求められる音楽のチューニング用途では不十分であるが、ゼロパティングを使用することである程度ピッチ推定の精度を高めることができる。

自己相関関数でのピッチ推定の精度向上

以前の日記で、自己相関関数でのピッチ推定の誤差について検証した。

自己相関関数で求めたピッチは、高周波数で誤差が大きくなることを示した。
ここでは、それを改善する方法について検証する。


自己相関関数のピークの位置をnとすると、周波数fは以下の式で計算できる。

{\displaystyle
f = \frac{f_s}{n}
}
f_sは、サンプリング周波数


自己相関関数は、周期的な関数となるため、ピークの整数倍の位置にもピークが現れる。
つまり、ピークのN倍の位置から周波数を求めて、N倍することでも周波数fを求めることができる。

周波数fは、nの逆数の関数であるため、nが大きいほどピッチの誤差が小さくなる。
N倍の位置n_Nのピークを使って周波数fを求める式は以下のようになる。

{\displaystyle
f = \frac{f_s}{n_N} N
}


N倍の位置のピークを使って、ピッチを推定した場合、誤差がどれくらい抑えられるか検証する。

前回の日記で検証した、N倍しないピークを使ったときの誤差と比較するため、周波数の範囲を100Hzから900Hzとして、cent単位で誤差を求める。

import numpy as np
import matplotlib.pyplot as plt
fs = 44100
size = 4096
t = np.arange(0, size) / fs
han = np.hanning(size)

#ピークピッキング
def pick_peak(data):
    peaks_val = []
    peaks_index = []
    for i in range(2, data.size):
        if data[i-1] - data[i-2] >= 0 and data[i] - data[i-1] < 0:
            peaks_val.append(data[i-1])
            peaks_index.append(i-1)
    max_index = peaks_val.index(max(peaks_val))
    return peaks_index[max_index]

import math
errors = []
fset = np.linspace(100, 900, 8001)
for f in fset:
    y = np.sin(2 * np.pi * f * t)
    Y = np.fft.fft(y*han)
    acf = np.fft.ifft(abs(Y)**2)
    n = pick_peak(np.real(acf[0:size/2]))
    n1 = n
    for N in range(2, int(size / 2 / n1) - 1):
        nn = int(n*N)-10 + pick_peak(np.real(acf[int(n*N)-10:int(n*N)+10]))
        n = nn / N
    f0 = fs / n
    cent = (math.log(f0, 2) - math.log(f, 2)) * 12 * 100
    errors.append(cent)
plt.plot(fset, errors)
plt.show()

f:id:TadaoYamaoka:20170114104521p:plain

Nは、ピークのnの整数倍とした場合、誤差が大きくピークの位置からずれるため、2倍、3倍と順番にピークの位置を調べてn/Nの値でnを更新している。
Nの上限は、自己相関関数のフレーム長/2を超えない範囲としている。

比較のために、前回の日記で検証した誤差のグラフを示す。
f:id:TadaoYamaoka:20170107165103p:plain


N倍の位置のピークからピッチ推定した場合、周波数が高くなっても誤差が増えていない。一方、N倍しないピークを使ってピッチ推定した場合は、周波数が高くなるほど誤差が増えている。

N倍の位置のピークからピッチ推定した場合の誤差は、-6.9~7.8centの範囲に抑えられている。

音楽のチューニング用途としては1cent程度までの精度が必要である。
それからするとまだ誤差が大きい。

さらに誤差を小さくするには、Nの値をさらに大きい値にするなどの対応が必要となる。
そのためには、フレーム長を大きくするなどの対応が必要となる。

スマホでのFFTの処理時間

FFTでスペクトル推定を行う際、フレーム長を長くするほどより周波数領域で精度を上げることができる。

しかし、フレーム長を長くすると実行時間も長くなる。

スマホでリアルタイムで解析を行うような場合、実行時間について考慮が必要になる。

ここでは、フレーム長を長くした場合、実行時間がどのように変わるかを検証した。

測定条件は以下の通り。

  • FFTの実装は大浦版FFTJavaに移植したものを使用
  • 1000回繰り返し実行したときの処理時間を測定
  • 5回測定して平均をとる
  • スマホの機種は003SH(CPUはMSM8255 1GHz)

測定結果

フレーム長 時間(ms)
4096 3,216
8192 6,771
16384 14,263
32768 30,336

グラフにすると以下のようになる。

f:id:TadaoYamaoka:20170109103457p:plain

横軸のフレーム長は2のn乗となっているため、対数スケールになっている。

線形スケールにすると以下のようになる。

f:id:TadaoYamaoka:20170109103945p:plain

ほぼ線形に増えているように見える。
なお、理論上の計算量は、O(N logN)である。


スマホでリアルタイムに解析を行う場合のフレーム長について考えてみる。

秒間60回解析を行うとすると、最低でも1000/60≒16.6(ms)以内に計算を行う必要がある。

上記の計測結果から、フレーム長として使用できるは16384までとなる。
FFT以外の処理も行うことを考慮すると、その下の8192が上限ではないかと思う。

秒間の解析回数を減らせばフレーム長は長くできるが、リアルタイム性と解析精度のバランスをとってフレーム長を決める必要がある。電池の消耗についても考慮すべきである。

今回はあえて古い機種で測定したが、最新の機種では当然より速く計算できる。
以前に測定した結果では、新しい機種では実行時間は1/3程になる。