Yolo が遅延するのでプロファイラーを使ってみた

# Yolo の検出遅延
(前回紹介したカメラ付きプラレール+Yolo 検出についての続きですが、機械学習から少し離れ、デバッグ寄りの内容になります。)
Darknet の Yolo 実装では、以下のようなコマンドラインで MJPG Streamer の URL を渡すことにより、プラレールカメラのストリームを Yolo に渡すことが出来ます。

````
$ ./darknet detector demo cfg/voc.data cfg/tiny-yolo-voc.cfg tiny-yolo-voc.weights http://192.168.1.104:8080/?action=stream -i 0 -thresh 0.2
````

しかし、ブラウザ上の画像はほぼリアルタイムに表示されるのに対して、Yolo の検出は、この動画のように数秒遅れることがわかりました。

[![Alt text](https://img.youtube.com/vi/B9J7V_OcrIE/0.jpg)](https://www.youtube.com/watch?v=B9J7V_OcrIE)

この遅延の原因、ボトルネックを調べることにしました。
調査については、ソースに自前のデバッグコードを埋め込み、関数の Call ごとにかかった時間を出力する、という方法がありますが、今回は C 言語で実装されたアプリケーションの調査となるため、ツールが充実しているので、既存のプロファイラーを使うことにしました。

※なお、こちらの内容は、Yolo のソースコード 2017/04/13 版の内容です。現在はソースコードが異なる可能性が有るため、この記事の内容通りにならない可能性が有ります。[^1]

# プロファイラーについて
プロファイラーとは、プログラムの動作を分析するための道具です。一般的に、実行中のデータを集積して動作を分析しますので、「動的」な分析になります。
Linux では、代表的なプロファイラーとしては、以下の3つがあるようです。

* gprof(GNU)
* valgrind/callgrind(Valgrind™ Developers)
* gperftools(Google)

それぞれ試しましたが、gprof、valgrind はソースコードの改変は不要で、自動で Instrumentation するものの実行速度が非常に遅くなります。gperftools はソースコードに変更が必要なものの、試した中ではこの中で最も高速でした。
このため今回は、 gperftools を使用して原因を調査しました。[^2]

# gperftools プロファイラーを使用する準備
まずはインストールです。自分が使用している Ubuntu 16.04 LTS では、以下で簡単にインストールできます。

````
$ sudo apt-get install google-perftools
$ sudo apt-get install libgoogle-perftools-dev
````
バージョン(2.4-0ubuntu5)がインストールされます。
さらに、GUI で結果を見るための kcachegrind をインストールします。

````
$ sudo apt-get install kcachegrind
````
バージョン (4:15.12.3-0ubuntu2)がインストールされます。
次に、darknet の Makefile を変更します。デバッグ情報の埋め込み(-g)と、リンクするライブラリの追加(-lprofiler)を行います。

````
$ diff Makefile.org Makefile
@@ -18,8 +18,9 @@ OBJDIR=./obj/
--- Makefile.org
+++ Makefile
 CC=gcc
 NVCC=nvcc 
-OPTS=-Ofast
-LDFLAGS= -lm -pthread 
+# OPTS=-Ofast
+OPTS=-Ofast -g
+LDFLAGS= -lm -pthread -lprofiler
 COMMON= 
 CFLAGS=-Wall -Wfatal-errors 
````

最後に、ソースコードにプロファイル用のコードを追加します。
今回は、demo.c の demo() 関数のプロファイルを収集します。また、この中には永久ループがありますが、これを有限のループ(500コマで終了)に変更し、プロファイルのデータが正常に保存されるようにします。

````
diff --git src/demo.c.org src/demo.c
index 27fcb99..5b0ed29 100644
--- src/demo.c.org
+++ src/demo.c
@@ -9,6 +9,9 @@
 #include "demo.h"
 #include <sys/time.h>
 
+//kmori
+#include <gperftools/profiler.h>
+
 #define FRAMES 3
 #define DEMO 1
 
@@ -98,6 +101,8 @@ double get_wall_time()
 
 void demo(char *cfgfile, char *weightfile, float thresh, int cam_index, const char *filename, char **names, int classes, int frame_skip, char *prefix, float hier_thresh)
 {
+    // kmori
+    ProfilerStart("demo.prof");
     //skip = frame_skip;
     image **alphabet = load_alphabet();
     int delay = frame_skip;
@@ -169,8 +174,11 @@ void demo(char *cfgfile, char *weightfile, float thresh, int cam_index, const ch
     }
 
     double before = get_wall_time();
-
-    while(1){
+    
+    // kmori
+    // for perf test
+    // while(1){
+    for( int f_c=0 ; f_c < 500; f_c++){
         ++count;
         if(1){
             if(pthread_create(&fetch_thread, 0, fetch_in_thread, 0)) error("Thread creation failed");
@@ -222,6 +230,8 @@ void demo(char *cfgfile, char *weightfile, float thresh, int cam_index, const ch
             before = after;
         }
     }
+    //kmori
+    ProfilerStop();
 }
 #else
 void demo(char *cfgfile, char *weightfile, float thresh, int cam_index, const char *filename, char **names, int classes, int frame_skip, char *prefix, float hier_thresh)
````

# 実行結果を見る
実行すると、"demo.prof" というファイルが生成されます。
これを、google-pprof で開きます。

````
$ google-pprof darknet demo.prof 
Using local file darknet.
Using local file demo.prof.
Welcome to pprof!  For help, type 'help'.
(pprof) text
Total: 9922 samples
    2926  29.5%  29.5%     5304  53.5% resize_image
    1570  15.8%  45.3%     1572  15.8% cv::ResizeArea_Invoker::operator
    1558  15.7%  61.0%     1558  15.7% set_pixel
     694   7.0%  68.0%     2381  24.0% show_image_cv
     687   6.9%  74.9%      687   6.9% bzero
     432   4.4%  79.3%      432   4.4% strerror_l
     307   3.1%  82.4%      307   3.1% get_pixel (inline)
     270   2.7%  85.1%      270   2.7% constrain_image (inline)
     149   1.5%  86.6%      149   1.5% set_pixel (inline)
     109   1.1%  87.7%      109   1.1% rgbgr_image (inline)
(pprof) 
````

GUIで見てみます。
````(pprof) callgrind
````

こちらのような画面が開きます。
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/5faa812f-641f-08a9-0e9c-c195e8b22ea8.png)
Self 列で降順のソートを行っています。Self 列は、この画面では該当の関数の実行時間の割合を表します。
resize_image()という関数に時間がかかっています。
Source Code タブで resize_image() のソースを見ますと、下記の箇所で時間がかかっています。
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/87878b65-364c-54c0-f344-eab09593cf21.png)

これで、resize が遅延の原因であることがわかりましたが、ではどうすればいいか。拡大縮小ルーチンの高速化も考えられますが、そもそもこの部分の拡大縮小は必要なのでしょうか。
これを見るために、Callers タブを見ます。最も Count が多いのは、center_crop_image でした。
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/3417d540-2508-a916-d033-9981a49e506a.png)
さらに、center_crop_image の呼び出し元を調べると、fetch_in_threadにたどり着きます。Source Code タブを見ます。
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/6f26f1a7-cbdf-a804-1f92-6b50e4399611.png)
なぜか、1440x1080にリサイズしています。
周りのコードを見てみると、このリサイズは必須ではないようです。以下のようにして、Resize をしないよう変更します。

````
void *fetch_in_thread(void *ptr)
{
    image raw = get_image_from_stream(cap);
    if(DEMO){
        /*in = center_crop_image(raw, 1440, 1080);
        free_image(raw); */
        in = raw; /* No resize */
````

# 修正した結果
修正をした結果です。遅延はだいぶ減りましたので、ある程度自動運転ができそうです。

[![Alt text](https://img.youtube.com/vi/GuXyhwFOHxA/0.jpg)](https://www.youtube.com/watch?v=GuXyhwFOHxA)

プロファイル結果です。
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/1a20bc51-5758-6c35-047e-62eea86b5fdf.png)
依然、Resize が有りますがこれは、実際の検出(prediction)に使用するデータのための Resize であるため必須です。これは必須であるものの、比較的高速です。
このほかを見ても、全体的には問題のないレベルの遅延に収まったと言えそうです。

# プラレール自動運転について
遅延も解消したので、再度プラレール自動運転について考えてみたいと思います。

これまで、自動運転の実現にあたっては、まずレールを認識させたいと思い、レールの画像を作成して学習させ、レールをオブジェクトとして検出させる方向で検討していました。
しかし、うまく学習できるようなデータがなかなか出来ません。
これは、列車が通る場所としての「レール」は、カーブやポイント、上り下り坂、踏切など種類や形や色が多いためです。このような状況で学習用のレールの画像がなかなか作成できません。

またこの作業をしているうちに、プラレールにとってレールは、離散的(Discrete)ではなく連続的(Continuous)なものとして扱うべきであり、Yolo で学習させるのは必ずしも適切ではないのではないかと感じるようになりました。

特に、色や形が様々であるが、全てまとめて「レール」としたいということならば、いっそ、より大雑把な「クラス」(道路、モノ、その他)に分類すれば良いと言えます。その場合、OpenCV のほうが、レールなどの状況により特化したロジックが実現できそうです。

そこで改めて、レールや道路をどのように扱うかについて、Google で調査してみたところ、Udacity の「NANODEGREE PROGRAM - Self-Driving Car Engineer」[^3]にたどり着きました。

このコースの成果として、高速道路のレーン検出についての受講者の成果が youtube や GitHub で公開されています。コース受講生のどの成果も、OpenCV で二値化など何らかの処理を施したうえでの特徴検出、線形補間によってレーンを検出しているようでした。Yolo のようなオブジェクト検出は、信号や看板、並走する周りの車などで使用するようです。

オブジェクトの検出を、路面のような環境の検知とは別々に行うことになり、この方が理にかなっている感じがします。

このため今後、レールの検出については Yolo は一旦置いておいて、OpenCV について調べてみることにします。

ちなみに OpenCV によるレールの検出について、自分のプラレールの場合、Udacity とは異なり、カメラの視点が低く、位置が車輪の前方です。カーブでは視界からほとんどレールがなくなってしまいます（＾＾；(下図参照)
![image](https://qiita-image-store.s3.amazonaws.com/0/172071/9133e275-7185-617d-6f64-9af9e62db679.png)

このため、Udacity の自動運転コースで公開された成果はそのままでは使えませんが、参考にはしたいと思います。

[^1]: 2017/05/05 現在、ソースはこちらの記事の時点より大きく変わっています。なおこの記事のソースについては、commit 当時のコメントが "working on TED demo" となっていたので、おそらく、TED Talks で何らかのデモやプレゼンをされたようです。

[^2]: Linux のプロファイラーの比較については、こちらの記事が参考になります。http://gernotklingler.com/blog/gprof-valgrind-gperftools-evaluation-tools-application-level-cpu-profiling-linux/

[^3]: いま大人気のコースのようです。コース概要はこちら。https://www.udacity.com/drive