JetRacerの速度制御について考えてみた。
この記事について
この記事はAI RC Car アドベントカレンダー 2019 3日目の記事です。3日目の記事はJetRacerの速度制御についてNVIDIAのリポジトリのブランチから考えてみたいと思います。この記事はある程度JetRacerの走行の仕組みを理解されている方向けになります。なるべく補足を入れていきますが、わかりづらい点はご容赦ください。
JetRacerの速度制御の必要性
JetRacerで利用するディープラーニングでは、撮影した画像に対して進行方向を画像上のX,Y座標として教師データを与えていきます。学習したAIモデルは入力画像のX軸方向の座標を出力します。この座標を元にステアリングの制御値を生成する。仕組みです。これだけの仕組みですが、十分にコースに追従した走行が可能となります。
以下、JetRacerリポジトリで公開されている学習の様子です。
一方で速度についてはパラメータとして与えられ、一定の速度でコース上を周回します。しかし、コースの直線部分もカーブ部分も均一な速度となるため、早すぎるとカーブではコースアウトしやすくなり、遅すぎると全体的に速度が遅くなってしまいます。そのため、JetRacerの速度をいかに最適に設定するかは周回速度を競うために重要な問題となります。
[速度の図]
JetRacerの速度制御方法
速度制御の方法としてもっとも簡単に思いつくのはステアリングの制御値に合わせて速度を加減速させる方法です。制御値の絶対値の大きさに比例して速度を遅くすれば直線部分とカーブ部分で速度のメリハリをつけることができます。全体的に速度を上げて走行させることができるようになります。
しかし、この方法には課題があります。まず、パラメータをヒューリスティックに決めなければいけません。走行させる速度の段階や、ステアリングの値に合わせてどういったルールで速度を落としていくのかを調整しなければいけません。ある程度数式で近似する方法もあれば、IF-THENのルールに落とし込む方法もあるでしょう。何れにしても走行のたびに試行錯誤が必要です。 また、速度の調整が遅れてしまうという問題があります。ステアリングの値が大きくなる時点で車体はすでにカーブに入っています。この時点で速度を落としても車体の慣性により十分に速度を落とすことはできません。実際の車の運転でもカーブ前に十分に速度を落として、。カーブを抜けるとともに速度を上げていく操作をします。ルールベースの方法ではこういったアクセル操作を行うことはできません。
コースの状態を把握しながら速度を加減速するためには、現在撮影している画像からその後のステアリングの状態、先のカーブの状態を推定する必要があります。しかし、現在のコースの状態のみからそれを把握するのは難しくなります。このアクセル操作を実現するためにはいかに、コースの形状を学習させていくかが鍵となりそうです。
circuit_learningに見る速度制御へのディープラーニングの適用
NVIDIAのJetRacerリポジトリにはcircuit_learningと呼ばれるブランチが存在します。これはJetRacerの速度制御をディープラーニングで実現させるサンプルノートブックが含まれています。このブランチのnotebook/circuit_learningフォルダ内がそれです。
GitHub - NVIDIA-AI-IOT/jetracer at circuit_learning
このcircuit_learningは前述のコースの形状を学習させるために工夫が施されています。まず、あらかじめ学習させたステアリングのみを制御するモデルを使いコースを走行します。走行時に、推論したステアリング値をラベルとしてコースの画像を収集します。収集された画像は時系列順に連番が振られています。これが速度制御モデルの学習データとなります。
circuit_learningの学習ではこの学習データを学習しますが、ラベルデータの扱いを工夫しています。N番目の画像のラベルはN+TIMESTEPまでの画像のラベルに係数をかけて足し合わせたものです。この時、係数は指数的に値が小さくなる関数を設定します。これは画像の枚数が増えるにつれて、値が小さくなるようにする工夫です。ここの画像のラベルは前述の通り、ステアリング値です。先のステアリング値を見ていくことで、将来、コースがどう変化するかを見ています。さらに指数的に変化する係数をかけることで、よりコースの近い状態が影響が大きくなるようにしているようです。つまり、カーブが近づくにつれて値が大きくなるラベルを設定することができます。実際のコードはtrain_model.pyのCircuitDataset
クラスで実現しています。
コード上からラベルづけに関係する部分を抜粋すると以下のようになります。
# 重みの作成、num_timestepsは先読みする枚数が設定されている。 gain = torch.exp(-3e-2*torch.linspace(0.0, num_timesteps, num_timesteps)) # ラベルをつける画像からnum_timesteps枚分のラベルを取得する。 for i in range(self.num_timesteps): path = os.path.splitext(os.path.basename(self.image_paths[idx + i]))[0] x = float(int(path.split('_')[1]) - 50) / 50.0 target[i] = x # 重みをかけて正規化し、総和を取る label = torch.sum(self.gain * torch.abs(target) / torch.sum(self.gain), dim=0, keepdim=True)
上記を学習させると、カーブが近づくにつれて出力される値は「大きく」なります。そのため、実際に速度制御に使う場合は逆数を取るなど工夫が必要です。サンプルではlive.demo.ipynbで速度をcar.throttle = np.exp(-15*out) * speed_gain.value + speed_offset.value
と計算しています。ここでoutは推論結果の出力です。
まとめ
この方法で速度制御をしっかりできるようになりましたと記載できればかっこいいのですが、今現在これで本当に速度が制御できるかわかっていません。何度かテストデータに対して推論を行い、速度変化をシミュレートしてましたが、思うような結果が出ていません。また、計算に使う画像の枚数や、ラベル計算式のハイパーパラメーターなど依然としてヒューリスティックな部分が残っているのも気になります。大きな方針としては良いのでしょうがまだまだ改良が必要です。引き続き、色々試してみたいと思います。