TadaoYamaokaの開発日記

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

コンピュータ将棋におけるディープラーニングの考察

先日の日記で、コンピュータ将棋とAlphaGoについて言及したが、次のPonanzaがディープラーニングに成功したということで、どのように適用しているのかが気になっている。

そこで適当に考察してみる。

電王戦のインタビューでもプロの棋譜との一致率について言及していたことから、学習しているのは評価関数ではなく方策(policy)であると思われる。

入力に盤面の情報を与えて、出力に指し手の確率を出力する関数である。

入力

ポイントとなるのが、入力の盤面の情報の与え方であると思う。

AlphaGoでは19×19の各座標の白石、黒石、空きで3チャンネル(2値)の情報と、アタリやシチョウなどの若干の囲碁の知識を特徴量として各チャンネルに割り当て、合計で48チャンネルのデータを入力としていた。

将棋では、駒の種類が多いため、成りを含めた14種類のコマ数分のチャンネルが必要になる。
また、持ち駒もあるため、9×9の座標の他に、持ち駒の表現が必要になる。

畳み込みでは、位置の情報が重要であるため、なんらかの方法で位置に対してノイズとならないように入力してあげる必要がある。

また、AlphaGoでは盤面の情報の他にアタリやシチョウの場所なども与えているので、将棋の知識から2歩の場所などは与えた方がよさそうである。

出力

出力は、AlphaGoでは石を置く場所を表す19×19の1チャンネルで良かったが、将棋では座標の他に、どの駒が移動するか、成るのか、持ち駒から指すのかという情報が必要になる。

インタビューで、ルールを教えなくても正しい結果がでるようになったと言っていたので、移動については考慮せず、座標と駒のみを出力して、どこから移動したかは後からロジックで見つけているのかもしれない。
その場合、移動元が一意に決まらないケースがたまにあるという問題がある。

移動元と移動先の座標をそれぞれのチャンネルとして出力しているのかもしれない。

ネットワーク構成

ニューラルネットワークの構成は、AlphaGoではプーリングなしの13層の畳み込みニューラルネットワークだったが、似たような構成でよいかもしれない。

学習

以上の入力と出力、ネットワーク構成の設計ができたら、AlphaGoと同様にプロの棋譜から指し手を教師データとして誤差逆伝播で行うことができる。
AlphaGoのSL policy networkでは50GPUで3週間かかっているので、おそらく収束にはかなりの計算リソースが必要になると思われる。

方策改善

AlphaGoのRL policy networkと同様の方法で、自己対戦により強化学習の手法で、方策改善を行うことができる。

AlphGoの論文では以下の論文が引用されている。
Williams, R. J. Simple statistical gradient-following algorithms for connectionist reinforcement learning. Mach. Learn. 8, 229–256 (1992).

この論文で提案されているのは、REINFORCEアルゴリズムという、関数近似手法を使い強化学習を一般化したものである。

価値関数の学習

AlphaGoのValue networkと同様の方法で、方策から価値関数を学習することができる。

大量の異なる局面からRL policy networkの方策に従い最後までプレイしたときの報酬から、価値関数を回帰により学習する。

対戦時の方法

AlphaGoでは、モンテカルロ木探索の着手予測にSL policy networkを使い、プレイアウトの結果とValue networkの結果の平均をとることで、うまく従来の手法と組み合わせている。
ディープラーニングでは読みの部分で弱点があるので、プレイアウトで補っているものと思われる。

将棋は、囲碁以上に読みが重要なゲームであるため、ディープラーニングのみで指しても強くないと思われる。
探索とうまく組み合わせる必要がある。

先日の日記で書いたように、従来の探索を使用し、学習した方策を枝刈りに使用し、学習した価値関数を探索の末端の評価関数に使用することでディープラーニングを従来の探索と組み合わせて使えると思う。

以上、1行もコードを書かずに勝手に考察してみました。

PonanzaもAlphaGoのように論文を書いてくれることを期待してます。

そうしたら来年のコンピュータ将棋はディープラーニング一色に染まりそうですね。

追記

上で書いた内容は、専門家でない私の勝手な考察なので、おそらく全くの的外れです。
あまり参考にしないでください。

チェスでの先行研究の情報を追記しておきます。

チェスでのディープラーニングの実装があるようです。
Deep learning for... chess · Erik Bernhardsson
これは、方策ではなく評価関数を学習しています。

この論文では、評価関数と実現確率を学習しているようです。363個の特徴を入力しており、入力特徴の設計の参考になるかもしれません。
[1509.01549] Giraffe: Using Deep Reinforcement Learning to Play Chess

このサイトで研究がまとめられています。
https://chessprogramming.wikispaces.com/Deep+Learning

追記その2

後日、実際に実装して試してみました。
将棋でディープラーニングする - TadaoYamaokaの開発日記
将棋でディープラーニングする その2(ニューラルネットワークの構成) - TadaoYamaokaの開発日記
将棋でディープラーニングする その3(棋譜から学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その4(ネットワーク構成の変更) - TadaoYamaokaの開発日記
将棋でディープラーニングする その5(入力特徴に手番を追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その6(BatchNormalizationを追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その7(最適化手法の変更) - TadaoYamaokaの開発日記
将棋でディープラーニングする その8(出力に移動元を追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その9(王手を入力特徴に追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その10(入力特徴から盤面の空の位置を削除) - TadaoYamaokaの開発日記
将棋でディープラーニングする その11(Kerasの実装) - TadaoYamaokaの開発日記
将棋でディープラーニングする その12(Wide ResNetを試す) - TadaoYamaokaの開発日記
将棋でディープラーニングする その13(ハイパーパラメータの調整) - TadaoYamaokaの開発日記
将棋でディープラーニングする その14(floodgateの棋譜で学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その15(強化学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その16(対局できるようにする) - TadaoYamaokaの開発日記
将棋でディープラーニングする その17(強化学習の実装) - TadaoYamaokaの開発日記
将棋でディープラーニングする その18(報酬に応じた勾配) - TadaoYamaokaの開発日記
将棋でディープラーニングする その19(報酬に応じた勾配 その2) - TadaoYamaokaの開発日記
将棋でディープラーニングする その20(バリューネットワーク) - TadaoYamaokaの開発日記
将棋でディープラーニングする その21(elmoの学習データ) - TadaoYamaokaの開発日記
将棋でディープラーニングする その22(評価値と勝率の関係) - TadaoYamaokaの開発日記
将棋でディープラーニングする その23(バリューネットワークの実装) - TadaoYamaokaの開発日記
将棋でディープラーニングする その24(歩の持ち駒の上限) - TadaoYamaokaの開発日記
将棋でディープラーニングする その25(C++でミニバッチ作成) - TadaoYamaokaの開発日記
将棋でディープラーニングする その26(学習の高速化) - TadaoYamaokaの開発日記
将棋でディープラーニングする その27(対局できるようにする) - TadaoYamaokaの開発日記
将棋でディープラーニングする その28(学習の高速化その2) - TadaoYamaokaの開発日記
将棋でディープラーニングする その29(強化学習【修正版】) - TadaoYamaokaの開発日記
将棋でディープラーニングする その30(探索アルゴリズム) - TadaoYamaokaの開発日記
将棋でディープラーニングする その31(DNNのスループット) - TadaoYamaokaの開発日記
将棋でディープラーニングする その32(転移学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その33(マルチタスク学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その34(強化学習【成功】) - TadaoYamaokaの開発日記
将棋でディープラーニングする その35(マルチタスク学習(補足)) - TadaoYamaokaの開発日記
将棋でディープラーニングする その36(PUCTアルゴリズムの実装) - TadaoYamaokaの開発日記
将棋でディープラーニングする その37(利き数を入力特徴に追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その38(学習継続中) - TadaoYamaokaの開発日記
将棋でディープラーニングする その39(ブートストラップ) - TadaoYamaokaの開発日記
将棋でディープラーニングする その40(入力特徴に履歴追加) - TadaoYamaokaの開発日記
将棋でディープラーニングする その41(モーメントありSGD) - TadaoYamaokaの開発日記
将棋でディープラーニングする その42(ValueNetの出力をtanhにする) - TadaoYamaokaの開発日記
将棋でディープラーニングする その43(ValueNetの出力をtanhにする2) - TadaoYamaokaの開発日記
将棋でディープラーニングする その44(L2正則化) - TadaoYamaokaの開発日記
将棋でディープラーニングする その45(高速化) - TadaoYamaokaの開発日記
将棋でディープラーニングする その46(出力ラベルの表現方法) - TadaoYamaokaの開発日記
将棋でディープラーニングする その47(全結合) - TadaoYamaokaの開発日記
将棋でディープラーニングする その48(ResNet) - TadaoYamaokaの開発日記
将棋でディープラーニングする その49(再学習) - TadaoYamaokaの開発日記
将棋でディープラーニングする その50(ブートストラップ【訂正】) - TadaoYamaokaの開発日記
将棋でディープラーニングする その51(ディリクレノイズ) - TadaoYamaokaの開発日記
将棋でディープラーニングする その52(自己対局で教師局面生成) - TadaoYamaokaの開発日記

Bonanzaメソッドの解説

昨日、電王戦 Ponanza×佐藤天彦名人の第1局をニコニコ生放送で見ていました。

コンピュータ将棋には以前より興味があり、初めの頃から電王戦はウォッチしていました。

名人に勝ったPonanzaは、次はディープラーニングを使うということですが、昨日の対戦で使われたバージョンは、探索はミニマックス法の改良で、評価関数を機械学習で最適化するという方法で作られています。

ミニマックス法の並列化や高速化は、チェスのプログラムで昔から改良され続けてきました。
将棋プログラムでも基本は同じでありコンピュータ将棋での技術的な進歩はそれほどなかったと思います。

評価関数については、2006年のBonanzaの登場以前は、プログラマーがロジックを組んで調整していたものが、Bonanzaが3駒関係の特徴量の線形和をプロの棋譜から機械学習したことでブレイクスルーが起きました。

Bonanza登場以降は、ほぼすべての将棋プログラムがBonanzaの改良となっています。

Ponanzaは、他の将棋プログラムの一歩先を行っており、評価関数の学習に、プロの棋譜を使用しないで、自己対戦の結果を使って強化学習を行っているようです。

Bonanzaが取り入れた、評価関数の機械学習が今の将棋プログラムの重要な要素となっています。

ここでは、Bonanzaで行っている機械学習の方法(通称Bonanzaメソッド)について解説します。

Bonanzaメソッド

学習する局面の集合を{\bf P}、特徴ベクトルを{\bf v}、局面pの合法手数をM_p棋譜で選択された子局面をp_1、m番目の合法手による子局面をp_mとすると、

目的関数J({\bf P}, {\bf v})を、以下のように設計する。

\displaystyle
J({\bf P}, {\bf v}) = \sum_{p \in {\bf P}} \sum_{m=2}^{M_p} T_p[\xi(p_m, {\bf v}) - \xi(p_1, {\bf v})]

ここで、\xi(p_m, {\bf v})は、Minimax探索の値である。
T_p(x)は損失関数で、局面pの手番がMaxプレイヤーの場合、T_p(x)=T(+x)、Minプレイヤーの場合、T_p(x)=T(-x)となる。
T(x)は、一価の単調増加関数であればよく、Bonanzaではシグモイド関数

\displaystyle
1/(1+e^{-0.0273x})

が使われている。

勾配法を使用して、目的関数を最小化するように特徴ベクトル{\bf v}を学習する。

解説

プロの棋譜で選ばれた手の正しい評価値が分かるわけではないので、直接評価関数を学習することができない。
そこで、現在の評価関数を使用して、プロの棋譜で選ばれた局面の評価値と、それ以外の局面の評価値の差の合計を損失関数T(x)としている。

プロの棋譜と違う手を選択した場合、その評価値を低く見積もった場合ほど、損失関数の値は大きくなる。

全局面分について損失関数を計算した合計が目的関数となっている。

損失関数を微分可能な関数にすることで、勾配法を用いて、特徴ベクトル{\bf v}を最適化できる。

勾配法は、目的関数をv_iについて偏微分を行う。

\displaystyle
\frac{\partial J({\bf P}, {\bf v})}{\partial v_i} = \sum_{p \in {\bf P}} \sum_{m=2}^{M_p} \frac{d T_p(x)}{dx}[\frac{\partial \xi(p_m, {\bf v})}{\partial v_i} - \frac{\partial \xi(p_1, {\bf v})}{\partial v_i}]

各パラメータv_iについて、偏微分の値に、学習率を掛けてパラメータを更新する。

上記式で、T_p(x)v_iについての偏微分は、合成関数の微分の法則

\displaystyle
y=f(x), x=g({\bf v})
のとき、\displaystyle
\frac{\partial y}{\partial v_i}=\frac{d f}{d x} \frac{\partial g}{\partial v_i}

を使用している。

\xi(p_m, {\bf v})微分は、探索結果の末端局面の評価関数の微分で代替する。
末端局面の評価関数は3駒関係の線形和であるため、微分可能である。

Bonanzaでは収束性をよくするために、束縛条件(持ち駒の評価値の合計を一定)と、L1正則化項を目的関数に加えている。

目的関数が全局面の損失関数の合計となっていることからわかるように、バッチ学習を行っている。

当時のコンピュータで、45833局の学習で、約1ヶ月かかっているようである。
2009-11-30

この部分は、ミニバッチにすることで、より収束が速くなると思われる。

おまけ

私は、以前にコンピュータ囲碁のAlphaGoのクローンを途中まで作成したことがある。
その際に、rollout policyをプロの棋譜から機械学習を行った。
rollout policyで学習するのはどの手を選択するかの方策であるため、出力をsoftmax関数とすることで、プロの棋譜で選択された手から直接学習を行うことができた。

また、AlphaGoのRL policy networkでも行われているように、強化学習の手法で方策を改善でき、Value Networkで方策から価値関数(評価関数)を学習できる。

学習した方策を枝刈りに使用し、学習した評価関数を探索の末端の評価関数に使用することで、もしかしたらAlphaGoの手法はコンピュータ将棋にも応用できるかもしれない。
その場合、方策で使用する関数が3駒関係でも有効なのかは試してないのでわからない。

3Dモデルから学習データを生成する その3(顔の向きを変える)

前回に続きBlenderで読み込んだ初音ミクMMDモデルを使って、マンガキャラクターの顔パーツ検出用学習データの生成を試みる。

前回は、トゥーンレンダリングを使用して3Dモデルからマンガ風の画像を生成した。
今回は、学習データのバリエーションを増やすため、3Dモデルの顔の向きを変えることを試みる。

Blenderで顔の向きを変えるには、ボーンを操作してポーズを付ける。

ボーンの操作

顔の向きを変えるため、首のボーンを回転させる。

デフォルトレイアウトの画面右上のOutliner領域から
Scene→初音ミク初音ミク_arm→Pose→センター→上半身→首
をクリックする。

f:id:TadaoYamaoka:20170402220122p:plain

TransformのRotation Modeを「XYZ Euler」に変更し、RotationのYの値を「30°」に設定する。

f:id:TadaoYamaoka:20170402220309p:plain

Pythonのコードでは以下のように記述する。

bpy.data.objects['初音ミク_arm'].pose.bones['首'].rotation_mode = 'XYZ'
bpy.data.objects['初音ミク_arm'].pose.bones['首'].rotation_euler.y = radians(30)

レンダリングを行うと以下のようになる。
f:id:TadaoYamaoka:20170402220611p:plain

顔の向きを変えることができた。

同様の方法で、上下を含めて顔の向きをランダムに変更することで、学習データを生成できる。

次回は、3Dモデル上の顔のランドマーク位置からスクリーン上での座標を計算で求める予定。

ボーカル音程モニター(Volcal Pitch Monitor)のバージョンアップ

約3ヶ月ぶりに、ボーカル音程モニター(Volcal Pitch Monitor)のバージョンアップしました。

play.google.com

変更点は以下の通りです。

1.4.4
・横方向にピンチズームできるようにした
・前回の縦軸の位置を保存するようにした
・縦軸に半音を表示するオプションを追加
・ボタンを非表示にするオプションを追加
・細かいバグ修正

細かなUIの改善になります。


解析精度は通常の音声であれば、ほぼ間違わない精度になっていますが、
楽器のアタック時の音を拾ってしまうので、
どうにかできないかという要望をもらっています。

今の方式だとちょっと難しいので、
何らかの方法で2値分類できるようにしたいですが、
スマホの処理能力を考えるとできることが限られてきます。

本当はニューラルネットワークを試したいのですが、
メモリ使用量が増えるとか課題があるので、
今後のテーマとして取り組みたいと思っています。

3Dモデルから学習データを生成する その2(トゥーンレンダリング)

前回に続きBlenderを使用してMMD初音ミクの3Dモデルから、マンガキャラクターの顔パーツ検出用学習データの生成を試みる。

BlenderMMDのモデルを読み込むとそのままでは、シェーディングが有効になっており、レンダリングを行うと陰影のある画像となる。
陰影をなくして、マンガの学習データとして適した画像にしたい。

ゲームやCGアニメなどで用いられているトゥーンレンダリングの技法で、陰影をなくした画像にすることができる。
トゥーンレンダリングは、影の有り無しで2階調で表現するが、マンガ風にするには、影部分は不要なので、1階調とする。
また、輪郭を線で描画する。

Blenderでそれらを行うには、マテリアルのシェーディングをオフにして、Freestyle機能で輪郭に線を描画する。

マテリアルのシェーディングをオフにする

GUIから行うには、Compositingレイアウトで、マテリアルタブを選択し、材質を選んでShadelessにチェックする。

Pythonのコードから行うには、以下のように実行する。

for m in bpy.data.materials:
    m.use_shadeless = True

顔と背景を白色にする

グレースケール化したときに顔は白色としたいので、材質3(顔)、材質16(目の白い部分)を白色にする。

bpy.data.materials['材質3'].diffuse_color = (1.0, 1.0, 1.0)
bpy.data.materials['材質16'].diffuse_color = (1.0, 1.0, 1.0)

背景も白色にする。

bpy.data.worlds["World"].horizon_color = (1.0, 1.0, 1.0)

Freestyle機能で輪郭に線を描画する

DefaultレイアウトでSceneタブ(カメラアイコン)を選んで、Freestyleにチェックする。
またデフォルトでは線が太すぎるので、Line Thicknessを0.5pxにする。
Pythonでは以下のように実行する。

scene = bpy.data.scenes['Scene']
scene.render.use_freestyle = True
scene.render.line_thickness = 0.5

この状態でレンダリングを行うと、
f:id:TadaoYamaoka:20170329224531p:plain
このように、鼻の位置に不要な線が描画される。

この不要な線をなくすには、LineStyleタブを選んで、Freesyleのプロパティで、Crease Angleを120°に設定する。

scene.render.layers.active.freestyle_settings.crease_angle = radians(120)

レンダリングを行うと、
f:id:TadaoYamaoka:20170329224908p:plain
このようになり、グレースケール化するとこうなる。
f:id:TadaoYamaoka:20170329225126p:plain

以上のようにして、3Dモデルからマンガに近い画像が生成できる。

3Dモデルから学習データを生成する

以前よりDCNNによるマンガキャラクターの顔パーツ検出に取り組んでいるが、学習データを手作業で準備するのが大変なので自動化できる方法を模索している。

写真を線画化するする方法や、Free-Form Deformationを使う方法を試しているが、ここでは3Dモデルから2Dの学習データを生成する方法を試してみる。

3Dモデルを使用して、ランダムに顔の向きに回転を加えて2D画像を生成するれば、1つの3Dモデルから異なる学習データを大量に生成できる。
顔パーツの検出点も手動で入力しなくても回転元の座標から計算で求めることができる。

マンガの顔の表現は3Dモデルの回転とは表現が異なるため、Free-Form Deformationによる方法の方がよりマンガに適した変形を加えることができると思うが、デフォーマーを手動で作成するのは大変である。
一旦3Dモデルで顔の向きを変えてそこからデフォーマーを学習するとデフォーマーを手動で作成しなくてよくなると考えている。

使用する3Dモデルデータ

元となる3Dモデルを一から作成するにはモデリングのスキルが全くないため、フリーで公開されている3Dモデルを使用することにする。
そこで、MMD初音ミクのモデルを使用することにした。

モデリングソフト

3Dモデルに回転を加えて2D画像を生成する過程を自動的に行いたいので、Pythonによるコントロールが可能なBlenderを使用して生成を行う。

BlenderMMDのモデルを取り込むプラグイン(mmd_tools)が公開されているのでそれを使用してBlenderMMD初音ミクのモデルを取り込んだ。

さて、そこから2D画像を生成したいのだが、Blenderは元より3Dモデリングの知識がほとんどないため、以下のことを順番に模索しながら行っていきたいと思う。

  • カメラを顔の中心に向ける
  • ライトを正面向きの平行光源にする
  • トゥーンレンダリング(セルシェーディング)を行う
  • 顔の向きを変える
  • レンダリングした画像を保存する

BlenderPythonを使用する

BlenderPythonを使用するには画面上部のScreen LayoutをScriptingに変更する。
コンソールが表示され、Pythonを対話形式で使用可能になる。

f:id:TadaoYamaoka:20170325225512p:plain

カメラを顔の中心に向ける

MMD初音ミクのモデルを読み込んだ後、顔の中心にカメラを向ける。
読み込んだ初音ミクのモデルは、足元が原点となっており、正面が負のY軸方法で、頭の方向が正のZ軸方向となっている。

メッシュが体のパーツごとにグループ分けされており、顔の中心はグループ「目.L」と「目.R」の中心から求めることができる。

カメラは正面から顔に向けるので、x, y座標は0で、z座標を目の中心の位置に合わせる。

目の中心のz座標は以下のようにして求める。

eyes_index = [bpy.data.objects['初音ミク_mesh'].vertex_groups['目.L'].index, bpy.data.objects['初音ミク_mesh'].vertex_groups['目.R'].index]

eyes_z = []
for v in bpy.data.objects['初音ミク_mesh'].data.vertices:
    for g in v.groups:
        if g.group in eyes_index:
            eyes_z.append(v.co.z)

eyes_center_z = min(eyes_z) + (max(eyes_z) - min(eyes_z)) / 2

上記コードはモデルの全メッシュの頂点に振られたグループ番号が目のグループ番号と一致するものを抜き出し、そのZ座標の中間の値を求めている。


カメラの位置を上記の目の中心のZ座標に配置して、顔の正面に向けるには、

camera = bpy.data.objects['Camera']
camera.location = [0, -1.5, eyes_center_z]
camera.rotation_euler = [pi/2, 0, 0]

camera.locationで、カメラの座標を指定して、camera.rotation_eulerでカメラの向きを指定している。

カメラの座標は、顔の目の高さに合わせて、少しY軸方向に退いた位置に配置している。

カメラの向きは、オイラー角をラジアン単位で指定する。
カメラの向きは、[0, 0, 0]の時、負のZ軸方向で、スクリーンの上側が正のY軸方向を向いているので、X軸方向に90°回転することで、顔の正面に向けている。

f:id:TadaoYamaoka:20170325222138p:plain

ライトを正面向きの平行光源にする

ライトを正面向き(正のY軸方向)に向けて、光源の種類を平行光源にする。

lamp_obj = bpy.data.objects['Lamp']
lamp_obj.location = [0, -8, 0]

lamp_obj.rotation_euler = [pi/2, 0, 0]

lamp = bpy.data.lamps['Lamp']
lamp.type = 'SUN'

lamp_obj.locationでライトの座標を正面から少し退いた位置に配置している。
lamp_obj.rotation_eulerで、向きを顔の正面に向けている。
lamp.typeで、光源の種類を平行光源(SUN)に指定している。

レンダリングを行う

一旦ここで、レンダリングの結果を確かめてみる。
学習データとして顔の領域を100px×100pxに切り出すので、それよりも少し広い150px×150pxの解像度でレンダリングを行う。
レンダリングの解像度は、Python側でのコードが見つけられなかったのでBlenderのUIで設定を行った。
※2017/3/30追記
Pythonで設定するには以下のように実行する。

scene = bpy.data.scenes['Scene']
scene.render.resolution_x = 150
scene.render.resolution_y = 150

F12キーを押して、レンダリングを行うと以下のようになる。
f:id:TadaoYamaoka:20170325223328p:plain

陰影が付いており、そのままではマンガの学習データとしては適していない。
次回以降で、トゥーンレンダリング(セルシェーディング)を行い、マンガの学習データとして使用できる方法を試す予定。

SSDによる物体検出を試してみた

先日の日記でYOLOv2による物体検出を試してみたが、YOLOと同じくディープラーニングで物体の領域検出を行うアルゴリズムとしてSSD(Single Shot MultiBox Detector)がある。

YOLOv2の方が精度が高いとYOLOv2の論文に書かれているが、SSDの精度も高いようなので試してみた。

オリジナルのSSDの実装は、Caffeが使用されているが、WindowsでビルドできるCaffeとバージョンが異なるものが使用されており、トライしてみたがビルドがうまくいかなかった。

Python系のフレームワークの実装がないか調べたところ、TensorFlowの実装が2つとKerasの実装が1つとChainerの実装が1つ見つかった。

TensorFlowの実装
ssd_tensorflow
SSD-Tensorflow
Kerasの実装
ssd_keras
Chainerの実装
chainer-SSD

個人的にはChainerを使い慣れているが、他に比べて更新が古くてメンテされていないようだったので、熱心に更新されていたTensorFlowの実装を試すことにした。

github.com

生のTensorFlowは記述が冗長なので、あまりさわりたくないが、この実装ではTF-Slimが使用されており少しは読みやすくなっている。


学習済みモデルとデモも配布されていたので、README.mdの「SSD minimal example」の通り実行すると、問題なく動作した。

f:id:TadaoYamaoka:20170317233452p:plain

実行環境には、WindowsにインストールしたTensorFlowを使用した。


これを使って、先日のYOLOv2で試した動画で、物体検出を試してみた。

www.nicovideo.jp

そもそもアニメが学習データに入っていないので比較することには意味がないかもしれないが、YOLOv2と比べて検知漏れや物体でないところでの誤検知、キャラクターがaeroplaneになることが多い。

実行速度は、FPS:45.8であった。YOLOv2よりは遅いがリアルタイムに近い速度が出ている。


なお、TensorFlowの実装には、動画に対して検出するコードは含まれていなかったので、自分で作成した。
以下に作成した動画対応のコードを示しておく。

SSD minimal example」の最後の「In [11]」の部分を以下のコードに置き換える。

VOC_LABELS = {
    0: 'none',
    1: 'aeroplane',
    2: 'bicycle',
    3: 'bird',
    4: 'boat',
    5: 'bottle',
    6: 'bus',
    7: 'car',
    8: 'cat',
    9: 'chair',
    10: 'cow',
    11: 'diningtable',
    12: 'dog',
    13: 'horse',
    14: 'motorbike',
    15: 'person',
    16: 'pottedplant',
    17: 'sheep',
    18: 'sofa',
    19: 'train',
    20: 'tvmonitor',
}

colors = [(random.randint(0,255), random.randint(0,255), random.randint(0,255)) for i in range(len(VOC_LABELS))]

def write_bboxes(img, classes, scores, bboxes):
    """Visualize bounding boxes. Largely inspired by SSD-MXNET!
    """
    height = img.shape[0]
    width = img.shape[1]
    for i in range(classes.shape[0]):
        cls_id = int(classes[i])
        if cls_id >= 0:
            score = scores[i]
            ymin = int(bboxes[i, 0] * height)
            xmin = int(bboxes[i, 1] * width)
            ymax = int(bboxes[i, 2] * height)
            xmax = int(bboxes[i, 3] * width)
            cv2.rectangle(img, (xmin, ymin), (xmax, ymax),
                                 colors[cls_id],
                                 2)
            class_name = VOC_LABELS[cls_id]
            cv2.rectangle(img, (xmin, ymin-6), (xmin+180, ymin+6),
                                 colors[cls_id],
                                 -1)
            cv2.putText(img, '{:s} | {:.3f}'.format(class_name, score),
                           (xmin, ymin + 6),
                           cv2.FONT_HERSHEY_PLAIN, 1,
                           (255, 255, 255))


import time

vid = cv2.VideoCapture('path/to/movie')
if not vid.isOpened():
    raise IOError(("Couldn't open video file or webcam. If you're "
    "trying to open a webcam, make sure you video_path is an integer!"))

vidw = vid.get(cv2.CAP_PROP_FRAME_WIDTH)
vidh = vid.get(cv2.CAP_PROP_FRAME_HEIGHT)
fps = vid.get(cv2.CAP_PROP_FPS)

fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
out = cv2.VideoWriter('path/to/output.avi', int(fourcc), fps, (int(vidw), int(vidh)))

prev_time = time.time()
frame_cnt = 0
while True:
    retval, img = vid.read()
    if not retval:
        print("Done!")
        break
    
    rclasses, rscores, rbboxes =  process_image(img)
    write_bboxes(img, rclasses, rscores, rbboxes)
    out.write(img)
    frame_cnt += 1

curr_time = time.time()
exec_time = curr_time - prev_time
print('FPS:{0}'.format(frame_cnt/exec_time))

vid.release()
out.release()