masato-ka's diary

日々思ったこととか、やったことの備忘録。

ディープラーニングとJetson nanoでEnd-to-Endな自動走行を実現した話〜Jetpilotを作ってみた〜

1. この記事について

 Jetson nanoを搭載した移動ロボットJetbotを作成し、前回はJetbotを使った単眼vSLAMを実行させた。

masato-ka.hatenablog.com

 しかしvSLAMはPC側で処理される。Jetson nanoはPCへ映像を送信するだけだ。これでは搭載されているGPUが活躍していない。せっかくなのでJetson nanoに搭載されているGPUの威力を体感したかった。

 そこで今回はディープラーニング を利用して画像から直接判断して走行する、End-to-Endな自動走行にチャレンジしてみた。コースを追従するだけであれば、単純な画像認識によるライントレースと簡単なルールベースの仕組みで十分だろう。しかし、今回は前提となるルールを作るのではなくデータだけ与えて、ディープラーニングで解くのがポイントとなる。以下の画像のようにJetbotがコース上を追従して走行していくイメージだ。

f:id:masato-ka:20190823163621p:plain
End-to-Endの自動走行イメージ

2. End-to-Endのディープラーニングによるラジコン自動走行の方式

 End-to-Endの自動走行について説明する。ここでいうEnd-to-Endの自動走行とは以下のNVIDIAの論文のように車両で撮影した画像から直接その車両を制御する信号をディープラーニングで推論することを目指している。以下、論文の例では走行中の画像を撮影して、道路の状況に即して最適なハンドル操作角、つまりステアリング制御を行なっている。

 実車を公道など複雑な環境で自動走行する場合、SLAMなど自己位置推定の技術に加えて、ディープラーニングを利用して障害物や標識を探し、走行ルールに則り車を制御するのが一般的かもしれない。そのためEnd-to-Endな使い方は知る限り実車では例が少ない。しかし、近年草の根の盛り上がりがあるDIY Robot CarやAI RC Carと呼ばれる分野でいくつもの先例がみられる。

2.1 Jetbotのroad_followingサンプル

 Jetbotのサンプルリポジトリにはroad_followingというサンプルがある。これは自動走行したいコースの各ポイントで画像を撮影する。撮影した画像のそれぞれで、進行方向となる点をマーキングしその画像上でのX,Y座標をラベルとする。この学習データを元に転移学習されたResNetはコース走行時に、画像場で推定された進行方向の座標(X,Y)を返す。この座標を目標にPID制御でJetbotを動かすと言うものだ。上位ランクのJetRacerも基本的には同じ仕組みになっている。

https://github.com/NVIDIA-AI-IOT/jetbot/tree/master/notebooks/road_following

画像上での進行方向を推論し、その進行方向を目標値としてPID制御によって車両を制御している。正確にはEnd-to-Endにはなっていないが、実現方式としては近い。

2.2 DonkeyCar

   ラジコンカーにRaspberryPiまたはJetson nanoをマウントして作成するDonkeyCarでは人間の操作を直接学習さる。手順としては以下の通りだ。

  1. ラジコン操作で人間が実際に教示走行させる
  2. 教示走行で走行画像と合わせてそのタイミングで人間が操作した速度とステアリングの値をラベルとして記録する。
  3. 学習データを元に入力画像に対して最適な速度とステアリングを学習する。

 DonkeyCarではいくつかのDNNのネットワークモデルが提供されている。上記のNVIDIAの論文に記載している素朴なCNNを使い、分類問題として解く方法や、回帰問題として学習させる方法がある。また、3D-CNNやRNN, LSTMを利用し、前後の画像列から時系列での動作を意識したモデルを学習をさせることもできる。DonkeyCarにどんなモデルが実装されているかは以下のリポジトリからKerasでの実装を確認できる。

donkeycar/keras.py at dev · autorope/donkeycar · GitHub

 学習は1万枚程度の画像を使い素のネットワークから学習する。1万枚と聞くとかなりの量に感じるが、コースを10周もすれば集まるようだ。 DonkeyCarでは画像から車両の制御情報を直接推論するため、End-to-Endな方式と呼んで問題ないだろう。

2.3 今回実装した方法

 road_followingのデモでは画像一枚ごとに自分で画像上でマーキングを行いラベルをつける必要がある。また、End-to-EndではなくPID制御がシステムに入っている。DonkeyCarは教示走行により学習データを集めるため、比較的に簡単にデータを収集できる。また、End-to-Endの学習を実現している。しかし、DonkeyCarではネットワークをゼロから学習するため、Jetson nano上で学習するのに時間がかかる。  そこで今回はroad_followingで使用されている転移学習のコードをベースにDonkeyCar同様、画像とコントローラからの指令値をペアにした学習データを与えて学習させる方法をとった。

3. 実装

 ソフトウェアの実装ではROSを使ったコードで試作し実現性を確認した。その後、公開、利用がしやすいようにJupyter notebookとして再実装した。このnotebookは3つのノートブックで構成されている。学習データの収集にはUSB接続のゲームコントローラを必要とする。詳細な利用方法や実装については以下リポジトリを掲載する。プロジェクト名はJetpilot名付けた。

github.com

以下、実装時のポイントについて解説していく。

3.1 学習データの取得 (data_collection.ipynb)

 学習データの収集はコースをJetbotをゲームコントローラで走行させる。コースはテープなど2本のラインで構成された1週可能なものを想定しいてる。走行時に画像と一緒にコントローラの入力値を画像のファイル名として[speed]_[steering]_[uuid].jpgという形で保存していく。画像の保存はカメラの画像取得タイミングと同期しているため、カメラのFPSを10Hzに設定するとサンプリングの間隔は10Hzになる。ただ正確に計測していないので10Hzは厳密ではない。以下映像のようなコースの場合、3〜5周ほど周回ささせ700〜900枚程度になる。

※ 音が出るので再生には注意


Jetbotとディープラーニングで自動走行を学習させる

 Jetbotのサンプルはtraitletsというライブラリとipywidgetを使ってJupyter notebook上でインタラクティブなアプリを実装している。今回もこれらを利用してゲームコントローラの入力処理や画像の取得処理、Jetbotの制御を行なっている。    Jetbotの運動制御はmobile.pyのMobileControllerクラスで実装している。このクラスにSpeedとSteeringの値を入力することでJetbotを制御できる。Jetbotは単純な対向二輪型のロボットのため、制御に使う逆運動学は既知の式を利用している。

車輪ロボット(対向2輪型)の運動を計算してみよう | Tajima Robotics

Speedについては入力値をある範囲で区切り、離散化している。Steeringの値は60cmを最大値とする旋回半径として与えられ、与えられた値が大きくなるにつれ、旋回半径が短くなるようにしている。つまり、コントローラを大きく倒すほどJetbotが急激に曲がることになる。

3.2 学習(train_data.ipynb)

 学習スクリプトはroad_followingのサンプルをベースにResNet18をベースに転移学習を行った。出力はtorch.nn.Linear(512,2)としてSpeed とSteeringの値を出すようにする。MSE-Lossを使用して回帰問題として学習させる。 road_followingの学習スクリプトから大きな変更はない。road_followingでは画像上でのXY座標を回帰問題として求めるのに対して今回はその時のコントローラの出力値を出力するだけだ。違うのは与えるデータだけ。そのため、Datasetクラスの実装で画像名からのラベルデータの取り出し部分を変更している。

 ResNet18のLayer1からLayer3までは学習しないようにフリーズすると少ないエポック数でそれっぽく学習できた。ResNetの畳み込み層はLayer4までなので最後の特徴抽出層とその後の全結合層のみ学習させる。次のコードで層をフリーズさせることができる。

# ResNet18のLayer1からLayer3までを学習しないようフリーズさせる場合は、学習前に追加
for param in model.layer1.parameters():
    param.requires_grad = False
for param in model.layer2.parameters():
    param.requires_grad = False
for param in model.layer3.parameters():
    param.requires_grad = False

 それ以外にもvalidation lossが下がらない場合に過学習しないよう、学習を止めるEarlyStoppingを設定した。10〜30エポック程度で停止する。 Jetson nano上でそのまま学習させることもできるが、Google Coraboratoryなどで学習させると短時間に学習できる。

3.3 走行の様子(live_demo.ipynb)

 学習したモデルを使い実際にJetbotを走行させる。各フレームで入力された画像からスピードとステアリングを直接推論する。推論された値はそのままSpeedとSteeringになるので前述のMobileControllerへ直接与えることで走行することができる。  走行中のフレームレートを稼ぎたいため、推論では半精度の浮動小数点で計算をしている。以下が、走行時の動画となる。

※ 音が出るので再生には注意


Jetbotとディープラーニングで自動走行を学習させる

見事コース一周に成功。しかし、数回走らせたうちの1回成功なので精度的にはまだまだ改良の余地がある。

3.4 Grad-CAMでの評価

 実際に走行できたが、このモデルはなにを見てスピードとステアリングを決めているのか?Grad-CAMと呼ばれる手法を用いて、各画像の推論時に画像のどの部分が結果に一番寄与したのかを可視化した。今回はSteeringの推論にて画像中のどの部分が特徴として一番寄与したかを可視化している。動画中の色が赤に近づくほど結果への寄与が高いことになる。


Grad-CAMによるテストデータの可視化

基本的には白線部分をみているようだが、大きくカーブする場面では画像全体を均一に特徴として捉えている。過学習っぽいのかな。

4. やってみて学んだこと

以下、やってみて自分で感じたことなどを記載しておく。ごくごく当たり前の内容も含まれているが、自分でやってみると以外に見落としがちだった。

4.1 データ取得のための調整

 そもそもディープラーニングで学習する前にデータ取得での地道な作業が重要だった。データ取得での調整ポイントは次の通りだ。

  1. コースのレーンの色がはっきりとわかるようにする。(コースとそれ以外のコントラスト)
  2. 速度が0のデータや誤操作時の画像の除去といった学習データのクレンジング
  3. 学習走行時に周回ごとに操作がなるべくブレないようにする。これもデータを綺麗にする意味で重要
  4. カメラの設置角を調整して画像内でのコースの割合を大きくするといったハードウェア周りの調整。

 一般的にデータの前処理は地味だが重要な作業という。今回の場合も例外にもれず、身をもって体感した。また、ロボットなどハードウェアが絡む場合ではハードウェアの調整も重要なポイントとなる。

4.2 エッジ学習のやり方

 Jetson nanoで試行錯誤しながら学習するには処理が遅い。そこで、同じコードでGoogle Coraboratoryなど他の環境で学習し、エポック数やデータセット、バッチサイズなどのハイパーパラメータを調整しておく。「あたり」をつけたパラメータを使いEdgeで学習させると効率的。そのため、エッジ側とクラウド側で同じ環境、同じコードで学習できることが重要。今回はJupyter notebookが役に立った。

 こうなると全部クラウドでと考えてしまうが、データの収集、学習から推論まで エッジでできるのは手軽さがあり、データの転送など煩雑な操作がない分、手軽さがある。クラウド側とエッジ側の分担や使いわけはまだまだ課題だろう。

 また、エッジのように利用するデータを少なくしなければならない場合は転移学習は強力な手法になる。ゼロから学習させると画像の枚数が多くなり、Jetson nanoでの学習は現実的に厳しい。Jetson nano上で学習させるなら転移学習させるのが現実解なのだろう。

4.4 過学習に気をつけろ

 最初は何も考えず、 70エポックをフルで回していた。Early Stopping入れることで短い時間で割と良い結果が出ることに気がついた。エポックの回数をむやみに増やせば良いというものでもないのだな。

4.5 GPUメモリリーク

 当初Jetson nano上で400枚程度の画像でGPUのメモリが足りなくなり、学習が止まってしまう問題があった。これはGPU上のメモリに保存されているLossの値をトータルLossの計算でそのまま加算していたため、トータルLossの値がGPUのメモリを確保し続け、メモリが足りなくなる事象が発生していた。    GPUGPUに得意なことしかさせない。GPUを使わない処理や必要のないデータはGPUのメモリから外すようにするというのが特にエッジGPUでは重要だろう。

ちなみにこの修正は本家のリポジトリへプルリクエストを送ってみた。マージされるといいな。

Fix GPU memory leaks. by masato-ka · Pull Request #117 · NVIDIA-AI-IOT/jetbot · GitHub

5 まとめ

ディープラーニングを使ったEnd-to-Endな自動走行を実現した。ちゃんと教示走行を学習してそれっぽく動く姿は非常に興味深かった。今後はモデルを変えたり、センサ情報や、画像上に移った物体の情報で動きを変える(標識の画像で一時停止や反転など)ようなものに発展させたい。

お礼

この記事を執筆するにあたり、茅場町 コワーキングスペースCo-Edoの会議室を実験、撮影にて利用させていただきました。

www.coworking.tokyo.jp