はじめに

YOLOとはYou Only Look Onceの略とのことですがまあ当て字ですかね。速度に特化した画像検出・認識用ニューラルネットワークで、Cベースのdarknetというフレームワーク上に構築されています。元論文はこちらです。特徴は、全画像からいきなりクラス確率と物体のバウンディングボックス座標を推論する点です。R-CNNなどと違って複数の候補領域に対して何度も推論しないでよいので、Look Onceだから速い、というわけですね。

TensorFlowに変換した例はすでにあるようなので、今回はchainer化をしてみます。Cで組まれているものをわざわざスクリプト言語に変換するのは中身を理解するためです。今回の記事では学習済の係数を使った推論のみを行いますが、論文を読んだ感じだと学習手順が最大の特徴の一つに思われるので、そのうちトレースしたいです。

コードはこちらを参照してください。

学習済係数データの取得

YOLOには通常サイズ、small、tinyなどがあるようですが今回はtinyだけを変換します。

tiny-yoloとyolo-tinyはバージョン違いの別物みたいです。tiny-yoloのほうがBatchNormalizationとかを使っていて今風ですが、今回はtensorflowモデルにすでに変換されている、古い方のyolo-tinyを使います。

TensorFlow版のyolo-tinyの係数はこちらにあるので、係数をnumpyで吐き出します。

import tensorflow as tf
from YOLO_tiny_tf import *
import numpy as np

c=YOLO_TF()
with c.sess.as_default():
    for v in tf.trainable_variables():
        name = v.name
        ary = v.eval()
        np.save("npy/"+name, ary)

これでnpyフォルダ以下にnumpy形式で係数が書き出せます。tf.trainable_valiables()で、chainerでいうところのLinkに含まれるパラメータをリストアップできます。eval()はchainerでいうところのdataですね。

なお、YOLO_tensorflowでは係数をテキストで書き出すように改造したdarknetをつかってdarknetからの係数変換を行っています。

Chainer化

これをchainerに変換していきます。

yolo-tinyの構成はgithubの履歴をたどるとこれですね。3x3 convolutionとmax poolingを繰り返すかんじです。この記述にならってせっせとChainを記述します。

これぐらい段数が多くてもとのモデルがわかってる場合は、KerasみたいにLinkとFunctionの区別をしないでSequentialに書けるほうが楽ですね。

さて、こうやって組んだChainの係数を差し替えるわけですが、ChainerはDefine by run なので、いちどForward演算をしないとそもそもWやbなどにアクセスできません。今回はいちど0行列をつっこんでForwardしたあとに重みを書き換えました。initialW,initial_biasなどで指定してもよいのだろうとは思います。

Convolution_2dの重みは、TensorFlowとChainerとでは順番がだいぶ違って、

  • TensorFlow: [カーネル縦、カーネル横、出力チャネル、入力チャネル]
  • Chainer: [出力チャネル、入力チャネル、カーネル縦、カーネル横]

というように並んでいるので、気をつけて並び替えします。全結合も縦横が逆ですね。

また、leaky_reluのslopeが、chainerのデフォルトは0.2ですが、YOLOは0.1なのに注意です。

Chainの記述と変換用のコードはこちらです。smallなど他のバリエーションも同様にできるはずです。

出力の解釈

Pascal VOCという20クラス分類問題のデータセットに対して係数は最適化されています。

全画面を7x7のグリッドに区切り、グリッドごとのクラス確率と、そのグリッド内に中心をもつバウンディングボックスを1グリッドにつき最大2つずつ、そして、その2つのバウンディングボックス信頼度、というすべての情報を直接推論します。

バウンディングボックスの座標は中心位置のx,yをグリッドサイズで0から1.0に正規化した数値、サイズは幅と高さを画像サイズで0から1.0に正規化した数値の平方根になっています。平方根にする理由は、サイズが大きくなったときの誤差のペナルティを減らしたいからだそうです。

最終段の出力ベクトルは、

  • 7x7x20 のグリッドごとの20クラスの確率
  • 7x7x2 の各グリッドに2組のバウンディングボックスごとの信頼度
  • 7x7x2x4 の各グリッドに2組のバウンディングボックス座標(0-1で正規化してある)

という順番で並んでいるので、reshapeしたあと、信頼度の高いバウンディングボックスを特定して、その座標とクラスを取り出します。

推論時はクラス確率とバウンディングボックスの信頼度を乗算してしきい値で切ればよいですが、学習時は重要なところだけロス関数に乗りやすいような工夫をするそうです。

使用例

Flickrの Commercial use & mods allowed 画像から適当に試してみます。

Train
train_marked.png
いいですね。

Sheep
sheep_marked.png
めっちゃ羊です。

Bicycle
bicycle_marked.png
ちょっと人のバウンディングボックスがずれました。群衆は引っかかりませんね。

Birds
birds_marked.png
右のひとも見つけてほしいところですが。しきい値を下げれば出てくるかな。

Cat
cat_marked.png
めっちゃ猫です。

PottedPlant
plant_marked.png
まあこんなもんでしょうか。。

感想

論文にしつこいぐらい繰り返し書いてあるように、1つのネットワークでいきなりバウンディングボックスの座標まで出力するという大胆なつくりです。その大胆なアイデアをちゃんと動くようにするための学習過程がポイントと思います。

一方でこれまた論文にも書いてあるように、バウンディングボックスの精度は、とくにtinyではそれほどよくありません。このあたりは用途次第とは思います。

TensorFlowとchainerのモデル変換ははじめてやりましたが、係数の並び方だけ注意すればできそうですね。汎用ツールもやればできると思いますが、趣味の範疇は超えそうです。

追記

YOLOはver2というのが出ています。
- ペンパイナッポーとアッポーペンを識別する(ChainerでYOLO ver2)
- leetenkiさんの学習までふくむ実装
もご興味あれば参照ください。