2016年3月22日火曜日

Fully Convolutional Networks 〜 Chainerによる実装 〜

in English

はじめに


 先のページで Chainer を用いてシーン認識を行った。今回は、簡略化した Fully Convolutional Networks(FCN) を Chainer を使って実装してみる。(ここに追記した。)

計算機環境


 これまでと同じく、Amazon EC2 にある g2.2xlarge を利用した。GPU を搭載したインスタンスである。

データセット


 今回使うデータセットは VOC2012 である。以下のような領域分割用の教師データも含まれている。
領域分割の教師データの数は2913枚、これを4:1に分割し、前者を訓練データ、後者をテストデータとした。

number of train number of test
2330 580

訓練データ数は10で、テストデータ数は5で割り切れるように端数を切り捨てた(それぞれ訓練時のミニバッチサイズである)。文献のアルゴリズムは任意サイズの画像を受け付けるが、ここでの実装では、簡単のため、固定サイズ(224$\times$224)に制限した。これに伴い、入力画像をあらかじめこのサイズに変換した。
画像の中心部分に収まる最大の正方形をリサイズしただけである。VOC2012では、以下の21クラスを扱う。

label category
0 background
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 potted plant
17 sheep
18 sofa
19 train
20 tv/monitor

ネットワークの構造


 ネットワークの構造は以下の通りである。文献では、pool5 の後ろにも層は存在するが、簡単化のため、これらを削除した。
  1. name: 層の名前
  2. input: 入力featureマップの1辺のサイズ
  3. in_channels: 入力featureマップ数
  4. out_channels: 出力featureマップ数
  5. ksize: カーネルのサイズ
  6. stride: ストライドのサイズ
  7. pad: paddingのサイズ
  8. output: 出力featureマップの1辺のサイズ
pool3、pool4、pool5 の出力に接続する層を以下に示す。これらの層を通ったあとの出力データ名をp3、p4、p5とする。これら3つのデータのfeatureマップの数はクラスの数に等しくなる。
featureマップを拡大する層(Deconvolution層)は以下の通りである。
p4にupsample_pool4が、p5にupsample_pool5が適用され、これら2つのデータとp3とが加算され、最後にupsample_finalで元画像のサイズに拡大される。これで文献の FCN-8s に相当するものが出来上がる。

ネットワークの実装


 上記の構造をそのままChainerで記述する。

-- myfcn.py -- ラベルは0から20までであるが、物体の境界線には-1を配置し、softmax_cross_entropyの計算時に境界の寄与を無視するようにした(Chainerの仕様ではラベルが-1の画素は評価されない)。また、関数calculate_accuracyでも、境界線上の画素を省くようにしてある。3つのデータを加算する関数 add の定義は以下の通りである。

-- add.py --

訓練


 訓練時のスクリプトは以下の通りである。

-- train.py -- -- mini_batch_loader.py --
関数 copy_model はここに掲載されているものを利用した。VGGNet.pyはここからダウンロードした。train.pyでしていることは、
  1. オブジェクトmini_batch_loaderを作る。
  2. VGGNet.pyで定義されたネットワークのオブジェクトを作る。
  3. MyFcn.pyで定義されたネットワークのオブジェクトを作る。
  4. VGGNetのパラメータをMyFcnにコピーする。
  5. 最適化アルゴリズムとして MomentumSGD を選択する。
  6. あとは、通常の訓練過程である。
データを一括読み込みすると、ビデオメモリーが足りなくなるので、要求されるごとに読み込むようにしてある。

結果


 62エポックで計算を打ち切った。学習時の精度と損失は以下の通りである。

-- 訓練画像--
-- テスト画像 --
1枚の画像の中で何%の画素が正解したかを計算し(境界線上の画素は除く)、全画像について平均したものを正解率(accuracy)と定義した。訓練画像に対しては理想的な振る舞いを示すが、テスト画像に対しては損失が途中から上昇し始めている。過学習しているように見える。最終的な正解率は、訓練画像で0.99、テスト画像で0.82程度である。以下に入力画像、予測画像、教師画像の例を示す。数値はこの画像の正解率である。
以下に、その他の画像の予測例を示す。予測画像と教師画像を並べてある。

-- 訓練画像--

-- テスト画像--
背景で精度を稼いでしまうので、満足の行く結果とするには90%以上の精度を実現する必要がある。

21 件のコメント:

  1. 論文からの実装がなかなかできず苦しんでいたのですが、本当に参考になりました。コードを見ることで論文の読み方や実装の仕方などが見えてくるので、よろしければこれからも実装の手順なども紹介してくださると嬉しいです。

    有意義な記事、ありがとうございます。

    返信削除
    返信
    1. お役に立てて何よりです。
      間違い等あればご指摘ください。

      削除
  2. FCNの実装方法についてわかりやすくまとめられており、参考にさせていただいております。
    実装にあたってとても初歩的な質問で恐縮ですが、プログラム中で参照されている
    「voc2012_myfcn_{i}.pkl」というファイルはどのような構造で何が書かれて
    いるのでしょうか
    私自身このような分野で初心者なもので申し訳ありませんが、ご返信を
    いただけると幸いです。

    返信削除
    返信
    1. 学習結果を保存したファイルです。PythonのcPickleというモジュールを使って、オブジェクトのserializationを行っています。読み込む時は同じモジュールにあるload関数を使います。詳細はserializationという単語でググってみて下さい。

      削除
    2. とてもご丁寧にご返信いただきありがとうございます。
      早速試してみようと思います。

      削除
  3. add.pyについてですが、chainer.Variable同士は普通に加算できるのではないでしょうか。
    つまりmyfcn.py の 94 行目、
    h = add(p3, u4, u5)

    h = p3 + u4 + u5
    手元で試していないので、間違っていたらすみません。

    返信削除
    返信
    1. ここ
      http://docs.chainer.org/en/stable/tutorial/function.html
      を見て書きました。
      backpropagationの定義が必要だと思います。
      違うのかな。

      削除
    2. https://github.com/pfnet/chainer/blob/master/chainer/functions/math/basic_math.py
      ここで既に定義されているので、variable同士の基本的な演算(四則演算や絶対値など)はそのまま可能だと思います。もちろん定義されても問題はないと思いますが、今後の参考まで。

      削除
    3. 大変ありがとうございます。
      胸のつかえが取れました。

      削除
  4. わかりやすい記事ありがとうございます、ところでloss関数のところがわからないのですが、デコーダーの出力のシェイプはクラス数だけチャンネル数をもったw x h x channel で ターゲットのシェイプは w x h だと思うのですがどのようにlossを計算しているのでしょうか、softmax_cross_entropy で適切に処理されているのでしょうか?

    返信削除
    返信
    1. はい。channelの軸に沿ってargmaxが実行されます。
      あと、上記のプログラムではcalculate_accuracyをわざわざ実装していますが、F.accuracy(ignore_label=-1,...)とすれば良いことが後で分かりました。
      詳細はこちらをご覧ください。
      http://seiya-kumada.blogspot.jp/2016/07/fully-convolutional-networks-chainer-1.html

      削除
    2. ありがとうございます、大変勉強になります

      > channelの軸に沿って
      というのは、ピクセルごとにsoftmaxの結果がもっとも大きかったchannelのindexとtargetのピクセルのindexのクロスエントロピーを計算できる、ということでしょうか? ミニバッチにした場合は、バッチの合計が得られるのでしょうか?

      削除
    3. softmax_cross_entropyにミニバッチを渡しときは、ミニバッチ内での平均値が返ります。なので、上記プログラムでは
      loss.data * TRAIN_BATCH_SIZE
      のようにバッチサイズをかけて合計値に戻しています。

      削除
  5. 非常に勉強になります。
    ただ、当方まだchainer初心者ですので基本的なところから質問させて下さい。

    IMAGE_DIR_PATH は '.../JPEGImage_224' などと定義されていますが、
    png形式の画像ファイルがあることを想定しているという理解で
    正しいのでしょうか。

    また SEGMENTATION_CLASS_DIR_PATHにはpkl形式のファイルがあることを
    想定しているようですが、
    VOC2012のsegmentationの教師データはpng形式ですよね。
    それをpkl形式にあらかじめ変換しておくということになるのでしょうか。
    plt.imread したデータを即 pickle.dump という感じでしょうか。
    なぜこのような一手間をかけるのでしょうか。

    返信削除
    返信
    1. 最初の質問に対してはYESです。
      あと、一手間かけた理由は以下の通りです。
      これを実装したころは、VOC2012データセットのラベル画像(png)がインデックスモードで
      保存されていることを理解しておらず、わざわざpngからカラーテーブルを取り出し
      RGBからインデックスへ変換する関数を書いておりました。この関数の結果をpklで
      保存していたわけです。
      こちらでは、このあたりを修正したバージョンを解説しました。参考になるかもしれません。
      http://seiya-kumada.blogspot.jp/2016/07/fully-convolutional-networks-chainer-1.html

      削除
    2. ありがとうございます。
      こちらのページに辿り着いた縁で
      色々勉強させて頂いています。
      ご指摘のページの修正版のプログラムを試しに実行してみましたが、
      train_32s_with_any_size.pyの98行目で
      cudaErrorIllegalAddress: an illegal memory access was encountered
      とのエラーで落ちてしまいます。
      その手前の95行目のloss.backward()でloss.dataがおかしな値になり、
      これを参照するだけでエラーが発生しているように見えます。
      一体何が起こっているのか、お心あたりはありますでしょうか。
      math.isnan()のチェックを入れたのは何故でしょうか。

      削除
    3. CNNに限らずDeepなネットワークは、設定するハイパーパラメータの値、あるいは入力値によっては、nanを返すことがあります(backpropagationのとき計算する勾配計算で0割りが発生しているのかもしれませんが、確かな原因は追跡していません)。今回のプログラムを組み始めた当初も、頻繁にnan が出ていましたが、ここにアップしたプログラムではそのような症状は治まっています。と思ったのですが、まだ出るのですね。何をしたらnanが出なくなったのかメモしておけば良かったのですが残念ながら記録を残していません。。。

      削除
  6. やはりゼロ除算ですか。勉強になります。
    Amazon EC2 をお使いとのことですが、
    私は手持ちのGPU環境で実行していますので
    何らかの違いはあるのでしょう。
    私の環境では1エポック目の数十個以内のtraining dataで
    エラーが生じるのでかなりの確率だと思います。
    ここまで来たらきちんと実行させてみたかったですが残念です。
    どうもありがとうございました。

    返信削除
  7. こちらの記事を参考に
    http://lmb.informatik.uni-freiburg.de/Publications/2016/OB16a/oliveira16icra.pdf
    を実装しました.

    こちらの各層の出力を見てみますと以下の図のように,hightとlow交互の出力が逆畳み込みをかけるにしたがって強く表れてきます.
    https://www.dropbox.com/s/c7nuh98r6bfae0j/result1.png?dl=0

    こちらの,記事の各層の出力はどのようなものになっているのでしょうか?
    私以外にもFCNにて同じような現象が起きている人はいないかと思い質問させて頂きました.


    また,上の演算結果がNanになる問題はこちらの方法で解決できるかもしれません.
    https://groups.google.com/forum/#!topic/chainer-jp/CEiBNsDD4Y4

    返信削除
    返信
    1. 匿名さんが参考にされた論文の方が、ネットワークの構造が詳細ですね。時間を見つけて読んでみます。私が読んだfcnの文献は少々読み取り難い箇所があります。Nanの件も情報ありがとうございます。
      fcnについての私の最後の投稿はこれです。
      http://seiya-kumada.blogspot.jp/2016/07/fully-convolutional-networks-chainer-2.html
      fcn32s -> fcn16s と精度はあがりましたが、fcn8sはだめだめでした。なので投稿していません。

      削除
  8. ワガママなお願いだとは思うのですが、実験に用いたテキストファイルをアップしていただけないでしょうか?
    下記のようなエラーが発生したのですが、プログラム上のミスなのかテキストが間違っているのか分からず困っております。

    Invalid operation is performed in: SoftmaxCrossEntropy (Forward)
    Expect: in_types[1].ndim == in_types[0].ndim - 1
    Actual: 4 != 3

    返信削除