Donkey car シミュレーターで自動運転AIを作ってみた

## 1.はじめに
　この記事では、ラジコンカーのシミュレーターであるDonkey carシミュレーターに深層強化学習であるPPO(Proximal Policy Optimization)アルゴリズムを適用させて、自動運転AIを作ってみます。
 　[前回の記事](https://qiita.com/gashu/items/a9ffa292fbe1cb37bfdd)の応用編で、よりリアルな環境下でもPPOが動作するのか試してみます！中身のアルゴリズム等は基本同じです。

## 2. Donkey Carとは
　Donkey Car は、Pythonベースで構築されたオープンソースの自動運転プラットフォームで、実際のラジコンカーとPC上のシミュレーターの両方に対応しています。
　ユーザーは、カメラを搭載したラジコンカーを使って実機での走行テストができるほか、仮想環境であるDonkey Simulatorを使って手軽に学習・検証が可能です。
 * 実機のラジコンカーに対応：Raspberry Piやカメラを使って本物の車体で実験できる
 * シミュレーター対応：PC上で完結するため、物理ハードがなくてもすぐに試せる
 * 深層学習の入門に最適：カメラ画像からステア・スロットルを予測するエンドツーエンド制御が体験できる
 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/a5de3728-9c03-4db0-a403-d93b4744c9e7.png"  width="300">

## 3. Donkey Car シミュレーター
　Donkey Carは仮想環境で運転できるシミュレーターを提供しています。良いなと思った点の１つに、**OpenAI Gym環境で動作可能である**といったところです。他にも前方カメラ画像、ステアリング、スロットル、クラッシュ検知などの情報をリアルタイムで取得可能といった特徴があります。

#### OpenAI Gymインターフェース
OpenAI Gym同様に「状態 state」の入出力と、「行動 action]の入力、「報酬 reward」を取得することができます。
##### ●状態 state
エージェントは毎ステップ、環境から状態を観測します。Donkey Carシミュレーターでは、主に以下の情報が「状態」として扱われます。
* **前方カメラ画像**
　RGBの120×160ピクセルの画像となります。今回はこの画像データをinputデータとして使用します。
 ↓取得した前方カメラ画像
 ![11507_cam_image_array_.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/cae66a82-0cc2-409c-9f73-fa49b12c6391.jpeg)

* **センサー情報**
　その他に位置情報、速度情報、クラッシュしたかなどの情報を取得することができます。

#### ●行動 action
エージェントが出力する「行動」は、車両の運転に関わる以下の2つの連続値です
* **ステアリング角**：左 -1.0 ～ 右 1.0（0が直進）
* **スロットル**：0.0（停止）～ 1.0（全開）
ブレーキ設定もできるらしいですが、今回はラジコンカーらしくスロットルのみの設定にします。

またデフォルトのアクションだと速度が速すぎるかつ、ステアリングが急になるといった挙動がありますので、
* **ステアリング角**：左 -0.5 ～ 右 0.5（0が直進）
* **スロットル**：0.0（停止）～ 0.3（全開）
と設定しました。こうすることで挙動がマイルドになり、学習難易度が優しくなることを期待しました。

#### ●報酬 reward
報酬設計はとりあえずデフォルトで行きます。デフォルトでは以下になっているようです。
* ゲームオーバーの有無、中心線からのずれ（CTE）、速度の3つを組み合わせて算出されるようになっています。最大値は約1、最小値はおよそ-2とされています。

詳細の報酬設計はわからなかったのですが、ポイントとして、
* 中心線に近ければ近いほど＋速度が速いほど報酬UP
* コースから大きく外れた場合、ゲームオーバーとなるようです。

次回の記事では自作の報酬設計を試してみようと思っていますが、今回はデフォルトのままで行きます！

 ## 4. 実装
 #### 動作環境
 * OS : Windows11
 * CPU : Ryzen 7 8845HS
 * GPU : NVIDIA RTX3050
Donkey Car SimulatorはUnityベースのシミュレータとなりますので、GPUが必要となってきます。
 また、
 * Python 3.11.11
 * PyTorch 2.6.0+cu126
 * gym_donkeycar 1.3.1
 * OpenCV 4.11.0
 となっております。
 
 Donkey car simulatorの環境構築については公式ドキュメントを参考にしました。

 https://docs.donkeycar.com/

https://github.com/tawnkramer/gym-donkeycar

 ### end-to-endモデル構成
 今回のモデル構成は以下のようになっています。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/02a725b8-19d2-46e8-8f00-88c8827afc95.png)
入力データとして4スタックした前方カメラ画像とし、ニューラルネットワークを通してステアリング角とスロットルを推論するといった流れです。人間が視覚から得た情報を使って車を操作するのと一緒です。まさに人工知能って感じですね:grin:

PPOのニューラルネットワークの構成は以下のようにしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/57322cc0-5ff5-4503-80c7-e3a598ef877c.png"  width="400">
PPOのネットワーク構造の詳細は[前の記事](https://qiita.com/gashu/items/a9ffa292fbe1cb37bfdd)をご覧ください。また今回は軽量化という意味を込めてCNNを3層にしてみました。特徴をつかむ能力が落ちてしまうことが懸念されますが、軽量化になりますので、うまくいくか実験です。

### 環境の作成
~~~python
def cvt_img(rgb_img):
    gray_img = cv2.cvtColor(rgb_img, cv2.COLOR_RGB2GRAY)
    gray_img = gray_img[45:,:]
    return gray_img / 255 #正規化
~~~
取得する画像をOpenCVで加工します。画像はRGBの形で来ますので、グレースケールに変換します。そして、画像の上の部分はいらないので軽量化のためカットします。また、**画像の正規化がめちゃ重要でした。** 最初これを忘れており、全く学習が進みませんでした。汗
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/45ecba04-6218-46a2-a553-1f8a892945df.png"  width="400">

~~~python
env = gym.make("donkey-generated-roads-v0")
action_repeat = 2

class Wrapper():
    def __init__(self, env):
        self.env = env

    def reset(self):
        rgb_img = self.env.reset()
        gray_img = cvt_img(rgb_img)
        self.img_stack = [gray_img] * 4
        return np.array(self.img_stack)

    def step(self, action):
        rewards = 0
        for _ in range(action_repeat):
            obs, reward, done, info = self.env.step(action)
            gray_img = cvt_img(obs)
            self.img_stack.pop(0)
            self.img_stack.append(gray_img)
            rewards += reward
            if done:
                rewards -= 100
                break
        return np.array(self.img_stack), rewards, done, info
        
    def close(self):
        self.env.close()
~~~
環境のクラスを作成しました。これで画像の4スタックができるようになりました。また報酬の追加設定で、コース外でゲームオーバーになった場合に-100のペナルティを与えるようにしました。

### ニューラルネットワーク
~~~python
class PPO(nn.Module):
    def __init__(self):
        super().__init__()
        self.cnn_base = nn.Sequential(
            nn.Conv2d(4, 32, 8, 4),
            nn.ReLU(),
            nn.Conv2d(32, 64, 4, 2),
            nn.ReLU(),
            nn.Conv2d(64, 64, 3, 1),
            nn.ReLU()
        )
        
        self.v = nn.Sequential(nn.Linear(5120, 100), nn.ReLU(), nn.Linear(100, 1))
        self.fc = nn.Sequential(nn.Linear(5120, 100), nn.ReLU())
        self.alpha_head = nn.Sequential(nn.Linear(100, 2), nn.Softplus())
        self.beta_head = nn.Sequential(nn.Linear(100, 2), nn.Softplus())   
        
        self.apply(self.weight_init_kaiming)

    
    @staticmethod
    def weight_init_kaiming(m):
        if isinstance(m, nn.Conv2d):
            nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity="relu")
            if m.bias is not None:
                nn.init.constant_(m.bias, 0.0)
        elif isinstance(m, nn.Linear):
            nn.init.kaiming_normal_(m.weight, mode="fan_in", nonlinearity="relu")
            if m.bias is not None:
                nn.init.constant_(m.bias, 0.0)
        
    
    def forward(self, x):
        x = self.cnn_base(x) 
        x = x.view(-1, 64*5*16)
        v = self.v(x)
        x = self.fc(x)
        alpha = self.alpha_head(x) + 1
        beta = self.beta_head(x) + 1
        return (alpha, beta), v
~~~
前述の構成通りとなっています。He初期化で初期化するようにしています。

### 学習
PPOアルゴリズムと学習のループは[前の記事](https://qiita.com/gashu/items/a9ffa292fbe1cb37bfdd)とほぼほぼ同様になっています。
ハイパーパラメーターは以下の通りです。
| 項目 | 値 |
|:-:|:-:|
| 学習率  | 0.001  |
| 割引率  | 0.99  |
| エポック数  | 8  |
| バッチサイズ  | 128  |
| クリッピング係数  | 0.1  |
| エントロピー係数  | 0.01  |
| 価値関数の損失係数  | 2  |

## 5. 結果
　結果をご覧ください。いい具合に自動運転ができているのではないでしょうか。
#### 学習初期
スタート直後すぐにコース外へ突入していきました。
![donkey car after 100 episodes.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/cde704a7-09ab-48ec-8b8f-bd9dbc23e33e.gif)


#### 学習中期
少しずつ道を覚えてきて、コース上を走れる距離が長くなってきました。まだカーブの処理がうまくできないようです。
<iframe width="560" height="315" src="https://www.youtube.com/embed/i54dYvkfeis?si=lXbw3GbNf6AUVvCn" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


#### 学習後期
完全にコツをつかみ、非常に滑らかにトレースして走行することができました！連続したカーブでのスロットルやステアリングがとても上手にできるようになりました。
<iframe width="560" height="315" src="https://www.youtube.com/embed/ud9TgcnVzO0?si=vlsDSBQPQhAZANfx" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

学習が進むにつれてどんどんと運転がうまくなっていっていることが見えます。学習後期になると非常に滑らかに走行しています。これも報酬を高く獲得するために学習していった結果となります。複雑な制御を入れずとも、ここまでできてしまうのがすごいですね！恐るべしです、、人間の操作でもここまで滑らかに操作するのは難しいのではないでしょうか。

#### 報酬の移動平均
報酬の移動平均です。ある瞬間からコツをつかんで一気に報酬が上がっていくのが見えます。コツをつかむと学習が速いです。ちゃんとAIが学習している様子がわかります！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4053577/fa1f39ee-72ac-49de-ada7-e57d0491f583.png"  width="350">


#### 課題
個人的には大満足なのですが、課題感としては、
* 車速が遅い
やはり今回スロットルを0.3まで絞っているため、ゆっくりとした走行になっています。より速度を上げるためには、**スロットルの上限値を上げる＋早くゴールしたら高い報酬を与える**などの工夫が必要そうです。

この辺もゆくゆくは改善できたらいいなと考えています。
とりあえず今回は自動運転を作ってみることが目標だったので、このくらいにしておきます。

## まとめ
　今回、PPOを用いた強化学習により、Donkey Car シミュレーター上で安定した自動運転AIを構築することができました。機械が自分で学習して成長していくところは、近年のAI技術の凄さを感じ取れます。
　ちなみに今回のエンドツーエンド(E2E)のモデルですが、自動運転の実用化には安全性などの理由でまだ実現できていないようです。将来的にはE2Eのモデルが理想的と言われており、研究が盛んに行われていますので、今後の進化が楽しみですね！

 また何個か実装して感じるのは、やはり「報酬設計」がすべての行動を決めるといった点です。今回は特にいじったりはしていないのですが、
 **次回は自作の報酬設計でカスタムした自動運転AIを作ってみたいと思います。**

## 参考

https://qiita.com/gashu/items/a9ffa292fbe1cb37bfdd